Digitalt ljud och
programmering i Java
Christoffer Weidmar
Läsåret 02/03
Handledare Martin Sandgren
Versionshistoria .......................................................................................................................... 3
Inledning..................................................................................................................................... 4
Syfte ....................................................................................................................................... 4
Omfattning ............................................................................................................................. 4
Metodik och handledning ....................................................................................................... 5
Rapporten ............................................................................................................................... 6
Bakgrundsfakta........................................................................................................................... 7
Kraven på programmet ........................................................................................................... 7
Hjälpmedel ............................................................................................................................. 7
Programspråket Java .............................................................................................................. 8
Historik ............................................................................................................................... 8
Strukturen i Java ................................................................................................................. 8
Objektorientering ............................................................................................................... 9
Objektorienterade metoder ............................................................................................... 10
Filhantering ...................................................................................................................... 11
Det binära systemet .......................................................................................................... 11
Det hexadecimala systemet .............................................................................................. 11
Filer och filformat ............................................................................................................ 12
Digitalt ljud ...................................................................................................................... 14
Ljudvågor ......................................................................................................................... 14
Analogt ljud ...................................................................................................................... 14
Digitala format ................................................................................................................. 14
Sampling och DA omvandling .......................................................................................... 15
DA omvandlare ................................................................................................................ 17
Olika filformat .................................................................................................................. 17
Mp3 formatet .................................................................................................................... 18
Mp3 kodning .................................................................................................................... 18
Program för hantering av Mp3 filer ..................................................................................... 19
Problemformulering ......................................................................................................... 19
Delproblem (a), beskriv mp3 filer och deras uppbyggnad ............................................... 20
Delproblem (b), sök efter liknande program .................................................................... 21
Delproblem (c), gör ett huvudprogram ............................................................................ 21
Delproblem (d), gör testfiler............................................................................................. 22
Delproblem (e), öppna och kontrollera en fil ................................................................... 22
Delproblem (f), läs ut ID3 v1 information ....................................................................... 23
Delproblem (g), läs ut ID3 v2 information ...................................................................... 25
Delproblem (h), läs ut mp3 Header information .............................................................. 35
Lösning (h5), testprogram för mp3 header ...................................................................... 41
Delproblem (i), Användargränssnitt ................................................................................. 41
Delproblem (j), felhantering ............................................................................................. 45
Framtida förbättringar ...................................................................................................... 46
Slutkommentar ..................................................................................................................... 47
Referenser............................................................................................................................. 49
Internet ............................................................................................................................. 49
Böcker .............................................................................................................................. 49
Programvara ..................................................................................................................... 49
Exempel på användargränssnitt ................................................................................................ 50
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 2 av 52
Versionshistoria
Version
Datum
Kommentar
1
januari 2003
Första version. Mest programkod.
2
februari 2003-04-26
Mellanversion efter kommentarer från handledaren.
Mera allmänna beskrivningar.
3
10 mars 2003
Programkoden mera komplett. Utförligare
beskrivningar om filhantering och digitalt ljud.
En del kommentarer om programkoden.
4
22 maj 2003
Lagt till ett enkelt användargränssnitt. Rättat en del
fel i koden.
Kompletterat rapporten med mycket mer detaljerade
beskrivningar om problem/lösning, programmets
struktur och programkoden.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 3 av 52
Inledning
Syfte
Mitt syfte med projektet var från början främst att bygga ett program som jag
själv har nytta av, och samtidigt lära mig mera om programmering eftersom det
är ett område jag kanske kan tänka mig att arbeta med i framtiden. Före
projektet hade jag endast ”lekt” litet med Visual Basic och spelmotorer så
projektet kändes som ett bra tillfälle att komma igång mera på allvar.
Efter en del funderande bestämde jag mig för att ta fram ett enkelt och
lättöverskådligt sorteringsprogram för musikfiler i mp3 format. Svårigheten
verkade rimlig och programmet var något jag själv kunde ha nytta av. Det
programspråk som jag valde blev Java eftersom det är ganska nytt och är
”trendigt” inom programmeringsvärlden. Java är också vanligt i ”open source”
kretsar (d v s där människor utvecklar program och låter andra använda koden
gratis), och därför finns det en hel del kod åtkomlig via nätet som man kan lära
sig mycket av och i vissa fall också använda som ”halvfabrikat”.
Jag hade också planer på att publicera mitt program på en egen webbsida där det
ska vara lätt att ladda ner det körbara programmet och där jag även tänker lägga
den färdiga koden som bidrag till ”open source”. Därför skriver jag också de
flesta kommentarerna på engelska.
Det operativsystem jag valde var Windows XP. Anledningen var helt enkelt att
jag hade det på min hemdator redan. Det hade varit intressant att lära sig även
Linux, men det kändes som ett för stort steg att ta i detta projekt.
Omfattning
Det visade sig att arbetet var mycket mera krävande än jag trott från början. Det
var svårt att lära sig programmera på egen hand. Det tog många timmar bara att
få ihop en fungerande utvecklingsmiljö. Och dessutom krävdes det väldigt
mycket arbete för att ta reda på och förstå hur de musikfiler programmet
använder egentligen är uppbyggda. Det finns en hel del information på nätet
men mycket är ofullständigt och i en del fall stämde inte informationen alls med
hur filerna ser ut i verkligheten. För att förstå hur filerna är uppbyggda blev det
också nödvändigt att förstå litet mera om vad digitalt ljud egentligen är.
Eftersom den teoretiska delen och de praktiska problemen med att avkoda
filerna var större än jag väntat mig fick mina planer för programmet justeras.
Från början hade jag tänkt mig ett program som skulle vara komplett och
nästintill professionellt, men jag fick nöja mig med ett program där ”kärnan”,
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 4 av 52
d v s logiken för att läsa och avkoda informationen fungerar men
användargränssnittet inte är komplett. Att bygga ett helt komplett fönsterbaserat
användargränssnitt skulle troligen ta lika mycket som hela det övriga arbetet. Jag
planerar att fortsätta arbeta på det på lediga stunder under sommaren.
Metodik och handledning
Som jag nämnde tidigare var arbetet svårare, eller framför allt mera omfattande
än jag trott från början. Jag tvingades även byta handledare mitt i arbetet vilket
gjorde att jag kanske gjorde litet för mycket arbete på egen hand i början och
även kanske tog mig ”vatten över huvudet”. I programmeringsavsnitten har jag
fått en del hjälp av min pappa som har en del erfarenhet av C++ och andra
programspråk. Jag fick hjälp framför allt med de avsnitt som hanterar
beräkningar och bithantering. Vi har också samarbetat med användargränssnittet. När man arbetar med ”objektorienterad” teknik bygger man inte
alltid allting helt själv, utan många funktioner finns i klassbibliotek. I Javabiblioteket ”Swing” från Sun Microsystems finns t ex massor av funktioner för
att bygga fönster, menyer och andra användbara funktioner för
användargränssnitt. Eftersom Java är vanlig inom ”open source” rörelsen finns
det normalt en hel del programkod på nätet som man kan ha nytta av. Oftast går
programmeringen ut på att använda komponenter där man kan, förändra dem där
de inte passar och skriva egen kod för de delar som är speciella för programmet.
När jag letade på nätet efter liknande program hittade jag inga som fungerade
precis så som jag ville ha det. Jag hittade heller inga färdiga komponenter.
Däremot hittade jag så småningom en del program som på olika sätt hanterade
ID3 taggar. De flesta var dock inriktade på de enklare, s k ID3 version 1. I en del
fall kunde kod jag hittade på nätet hjälpa mig att bättre förstå hur taggarna är
uppbyggda i verkligheten men det var väldigt litet jag kunde använda mig ”rakt
av”. Koden hängde i många fall samman med annan programkod som både var
svår att förstå och ofta saknades. Däremot kunde jag lära mig mycket om
programmering genom att läsa andras programkod. I några fall har jag gjort på
samma sätt som i befintliga program, men även i dessa fall har jag skrivit in och
testat koden för att förstå hur den fungerar.
För användargränssnittet har jag använt mig av halvfärdig kod. Jag har dels
använt en del från Sun Microsystems som har flera användbara ”tutorials” på
nätet, dels har jag tagit programkod från en bok i Java Swing (se referenserna).
Detta gäller framför allt menyer och fönster.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 5 av 52
Rapporten
Förutom inledningen redovisas projektarbetet i denna rapport i tre delar. Den
första ger en teoretisk bakgrund till arbetet. Jag ger bakgrundsfakta om digitalt
ljud, viktiga filformat och hur mp3-filer är uppbyggda. Syftet med det är att det
skall vara lättare att förstå hur programmet fungerar eftersom vissa ljudtekniska
begrepp som t ex samplingsfrekvens används i programmet.
I den andra delen förklarar jag programmets krav, vilka tekniska problem som
har lösts och hur de har lösts. Jag försöker förklara hur programmet fungerar i
stort, och jag beskriver dessutom de viktigare programavsnitten med bland annat
exempel på programkod. De delar som inte beskrivs i detalj i detta avsnitt bör
man kunna förstå genom att titta i programmets kommentarer. Sist i detta avsnitt
finns också en sammanfattning där jag berättar om mina intryck från arbetet och
vad jag tycker att jag har lärt mig.
Den tredje och sista delen är själva programkoden och dokumentationen av
denna. Programdokumentationen består av programlistor med kommentarer plus
klass/objektdiagram och andra dokument som skapats med hjälp av
utvecklingsmiljön. Där finns också ett par bilder som visar hur
användargränssnittet ser ut.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 6 av 52
Bakgrundsfakta
Kraven på programmet
Det är naturligtvis viktigt att tänka igenom vad programmet skall göra. I det här
fallet var det inte så svårt eftersom det var ett program jag själv ville använda.
Det svåra var att begränsa omfattningen. Jag ville från början ha med en hel
mängd funktioner, men efter att ha insett hur lång tid det skulle kräva bestämde
jag mig för att ”skala ner” min kravlista till några basfunktioner. När dessa
fungerar kan jag sedan bygga ut programmet med nya funktioner. Listan var:
1. Programmet skall fungera för Windows XP.
2. Det skall ha ett enkelt grafiskt gränssnitt som kan byggas ut. Till att böra
med skall programmet styras med ett menysystem och mus.
3. När menyerna fungerar skulle det vara bra även med kortkommandon för
vanliga funktioner.
4. Programmet skall ha ett fönster där det ska vara möjligt att utforska filer i
datorn.
5. Datorn ska ha en sorteringsfunktion som skall fungera för mp3 filer. De
skall alltså kunna skiljas ut från andra filer.
6. Mp3 filerna skall visas i ett separat fönster.
7. Programmet skall kunna sortera ut intressant information ur mp3 filerna
som t ex titlar, artister och annan intressant information som genre,
ljudkvalitet och låtarnas längd.
8. Informationen skall kunna sorteras, helst på flera olika sätt.
Sorteringsfunktionen skall också stödja sortering efter ”Beats per
minute” (BPM), d v s takten/ hastigheten i låten. Detta är ett stöd när man
skall välja musik för att mixa.
I den nuvarande versionen finns de flesta funktioner med förutom sortering, men
det grafiska gränssnittet är inte komplett.
Hjälpmedel
Jag hade ju inga pengar att köpa program för så jag letade efter gratis
utvecklingsverktyg på nätet. Bl a har Sun gratis ”SDK” (Software Development
Kit”) som innehåller kompilator, länkare och en del hjälpmedel för felsökning.
Men det kändes krångligt att arbeta med bara redigeringsprogram och
kompilator. Jag hittade inga bra gratisprogram för Java men jag fick tips om
verktyget ”Together Controlcenter” från Togethersoft (numera Borland). Detta
är en mycket trevlig utvecklingsmiljö som innehåller ”allt” och efter att ha
förklarat min situation fick jag låna en utbildningslicens av Togethersoft.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 7 av 52
Programspråket Java
Historik
Java började utvecklas under början av 90-talet av Sun Microsystems.
Grundtanken var att konstruera ett universellt och portabelt språk. Den som
utvecklar program skall inte behöva tänka på vilken sorts maskin programmet
skall köras på eller vilket operativsystem som används. Koden skall i stort sett
vara densamma om det så skrivs för en kaffekokare eller en superdator, och
oavsett om operativsystemet är Linux, Windows, Mac-OS eller något helt annat.
Sun kom med den första prototypen 1995. Det var många som höjde på
ögonbrynen och tyckte att det verkade onödigt med ett språk till. Just då hade
språket ’C’ blivit det som gällde bland ”riktiga programmerare” och även det
hade tagits fram med målsättningen att ge mycket flyttbar kod. De tidiga
versionerna av Java fick mycket kritik för att vara långsamma.
Men sedan dess har det hänt mycket och de senaste två åren har många börjat
använda Java för både inbäddade system (t ex telefoner) och för stora
affärssystem. Mycket av Javas växande popularitet beror också på att det på ett
lätt och framför allt säkert sätt tidigt integrerades med Internet för skapandet av
både webbsidor och serverlösningar för webbaserade system.
Strukturen i Java
Java kan lite förenklat sägas vara en vidareutveckling av språket C, men där man
har försökt ta bort de största nackdelarna. Programmerare gillar ’C’ därför att
det inte har några begränsningar. Den som utvecklar kan komma åt allting i
datorn på maskinnivå vilket kan ge snabba program. Men man offrar en del
säkerhet och det finns möjlighet att göra fel som är svåra att hitta. I ’C’ kan man
t ex arbeta med något som kallas ”pekare”, vilket betyder att man kan arbeta
direkt mot vissa minnesadresser. Det gör att man kan kringgå kompilatorns
typkontroll och t ex försöka använda en textsträng som heltal. Javakompilatorn
har många fler typkontroller och tillåter inte direkt pekarhantering. Men den
största och viktigaste skillnaden är att Java är helt objektorienterat till skillnad
från C++ som kan sägas vara ’C’ med påbyggnad för objektorientering.
Javas flyttbarhet uppnås genom att programmeraren aldrig arbetar direkt mot
operativsystemet utan använder sig av något som kallas ”virtuell maskin”
(JVM). Denna innehåller allt det som krävs för att ett program skall kunna köra i
en viss maskin. Eftersom samma stöd finns överallt behöver programmeraren
inte bry sig om i vilken miljö programmet skall användas.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 8 av 52
Förutsättningen för att det skall fungera är givetvis att någon dessförinnan byggt
en virtuell maskin för den hårdvara och det operativsystem som man vill
använda. Sun har tagit fram en del stöd och många företag har tagit fram
virtuella maskiner för sina produkter och därför finns det idag Java-stöd för
stordatorer, Mac, de flesta PC och även handdatorer och vissa mobiltelefoner.
Det finns också Javabibliotek för vissa processorer för de som utvecklar sin egen
hårdvara, men för att det skall fungera krävs mycket arbete.
Den främsta nackdelen med Java är prestanda och minnesbehov. Det ”indirekta”
sättet att arbeta mot en virtuell maskin istället för direkt mot operativsystemet
ger en del tidsförluster. Speciellt lång tid tar det vid uppstart av programmen och
det märks tydligt i det utvecklingsverktyg jag använde som i sig är byggt i Java.
Kritikerna menar också att Java är minneskrävande. Detta har jag dock inte
märkt något av eftersom mitt program är ganska litet.
Objektorientering
Tidigare programmeringsspråk som t ex ’C’ hade ofta formen av långa
textdokument med snårigt sammanlänkad kod där också mycket små ändringar i
en del kunde få katastrofala konsekvenser för andra delar av programmet. Java
är ett helt objektorienterat språk. Det innebär att man bygger programmet som
ett antal objekt som samverkar och tillsammans utgör ett program. Objekten
innehåller både de data som logiskt tillhör objektet och de funktioner som kan
utföras med objektets datamängd. Objektets data är normalt ”gömda” så att det
bara är objektets egna funktioner som kan komma åt dem. Detta kallas
”inkapsling” och har flera fördelar:
- De som använder funktionerna blir inte beroende av hur data är
definierade och behöver därför inte påverkas bara för att data ändras lite.
- Risken för fel blir mindre eftersom man har ett funktionellt gränssnitt som
är enkelt att förstå för den som skall använda funktionen.
Ett Javaprogram består av ett antal ”klasser”. Dessa innehåller funktioner och
data som kan kallas instansvariabler. En klass kan ses som en ”mall”, d v s en
definition av vad objekt av en viss typ skall innehålla. När programmet sedan
körs skapas verkliga objekt (instanser) som programmet kan använda. Utifrån
klassens definitioner skapas instansvariablerna genom att minnesutrymme
skapas och de tilldelas startvärden mm.
När man strukturerar sitt program väljer man att definiera sina klasser på ett
sådant sätt att det blir ”naturligt”. Om man t ex skall bygga ett program som
registrerar försäkringar blir kanske ”Person”, ”Bil” eller ”Försäkring” naturliga
klasser. Programmet blir då lättare att förstå, och ändringar och tillägg kan
sedan göras utan att hela systemet påverkas.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 9 av 52
Mitt program handlar om filhantering och några exempel på klasser jag
använder är ”mp3header” och ”mp3file”.
Java har också ett bra standardbibliotek med klasser som är färdiga att använda.
Ofta använder man sig av något som kallas för ”arv”. Det betyder att man skapar
nya klasser som dels ”ärver” alla funktioner och instansvariabler från
föräldraklassen, dels får de tillägg i form av ytterligare variabler och/eller
funktioner som man själv vill lägga till. Eftersom biblioteken i Java är så
omfattande blir de litet svåröverskådliga för den som är ovan. Man ser också
spåren av tidigare Java-versioner och det är ibland förvirrande när samma sak
kan göras på litet olika sätt beroende på om man använder gamla eller nya
klasser. Speciellt gäller detta användargränssnitten där man tidigare använde
biblioteket Awt men nu rekommenderas att använda det nyare Swing biblioteket.
Java räknas nu som ett framtidens programmeringsspråk med regelbundna
uppdateringar och en ny inriktning på databaser och stora system med många
transaktioner. Java är numera vanligt i bankvärlden. Nackdelen är användargränssnitten där Microsoft fortfarande verkar bättre.
Objektorienterade metoder
När man utvecklar mindre program är det vanligt att man börjar skriva kod
direkt. Den som programmerar kan oftast hålla det mesta i huvudet eftersom det
är man själv som hittat på allt. Men om man arbetar i större system där det finns
flera personer involverade behövs det ett sätt för att kunna får en översikt över
systemet, förstå vad andra gör och hur det man själv utvecklar skall passa ihop
med övriga delar. Det kan då vara bra att rita en modell över systemet, ungefär
som man använder ritningar när man bygger hus. När man utvecklar större
system börjar man därför ofta med att göra en eller flera ritningar. I större
projektgrupper finns ibland personer som är specialister på att strukturera
systemen och de kallas ibland arkitekter precis som vid husbyggnad. Modellerna
är ofta bra som hjälpmedel för det egna tänkandet eftersom man lättare ser hur
saker kan hänga samman när man tittar på modellen, och det kan vara ett stöd
för minnet om man senare behöver ändra i programmet.
När man arbetar objektorienterat i industrin verkar det vara UML (Universal
Modeling Language) som är mest använt. Det är en standard som utvecklats av
ett antal kända metodexperter och som innehåller en mängd olika modeller för
att beskriva olika aspekter av ett system. I mitt projekt har jag använt mig litet
av klassdiagram och det är enkelt att rita med Together Controlcenter (TC). När
man ritar en klass skapar programmet automatiskt ett ”skal” för motsvarande
klass i Javakod och när man sedan lägger till kod i klassen uppdateras
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 10 av 52
klassdiagrammet automatiskt. När man sedan är klar med programmet har man
samtidigt en komplett ritning som garanterat stämmer med det som finns i
koden. Ett problem med att göra ritningar är att man medan man arbetar
upptäcker nya problem som gör att de tidigare lösningarna inte är så lyckade.
Det gäller t ex var man placerar olika datastrukturer och i vilken klass man
lägger en viss funktion. Det blir besvärligt att ändra ritningen efterhand som
man gör förändringar men TC hanterar mycket av detta automatiskt.
Filhantering
Det binära systemet
Allting i en dator är ju i grunden uppbyggt av ”ettor och nollor”. Men vad menas
egentligen med det? För att kunna förstå måste man förstå det binära
talsystemet. En ”bit”, eller Binary Digit, är den talenhet en dator hanterar på den
lägsta nivån. En bit kan ha två värden; ett eller noll, ungefär som en
strömbrytare som kan vara av eller på. Om man utökar antalet ”strömbrytare”
till två får man dubbelt så många kombinationsmöjligheter. De två bitarna kan
ha värdena 00, 01, 10 och 11. För varje tillkommande bit dubblar man antalet
kombinationsmöjligheter och de olika bitarnas positioner representerar talet 2n-1
där n är antalet bitar. Om man har åtta bitar får man 256 möjligheter och kan då
representera talen 0-255. Man brukar kalla detta en byte. En kilobyte är 1024
byte och större enheter är gigabyte och terabyte.
Det hexadecimala systemet
Eftersom datorns minne arbetar med binära värden skriver man ibland talen i
binär form i programmen. Detta gäller speciellt när man vill hantera enskilda
bitar eftersom logiken blir tydligare. T ex kan man i Java skriva ett 8-bitars
värde som: 1010 0110 som i det här fallet alltså betyder 166. Detta sätt att skriva
tal blir dock opraktiskt när man hanterar större siffror och därför arbetar man
ofta med det hexadecimala systemet som är ännu mera kompakt än det
decimala. I det hexadecimala systemet skrivs siffrorna som i basen 16, d v s
160, 161 o s v, och för att kunna representera tal upp till 15 används bokstäverna
A-F där A representerar 10 och F representerar 15. Varje 8-bitars tal kan då
skrivas som två hexadecimala siffror, och talet 166 ovan blir då i hexadecimal
representation: A6. Det är speciellt vanligt att man använder hexadecimal
representation för stora tal som t ex minnesadresser.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 11 av 52
Filer och filformat
Datorns processor är gjord för att mycket snabbt kunna hantera binär
information. De instruktioner som processorn utför ”längst ner”, i maskinkod, är
egentligen enkla instruktioner som att t ex addera två tal, skriva ett tal i ett
register och liknande. När man skriver ett program med språk som Java är
instruktionerna mera avancerade men dessa instruktioner översätts till enkla för
en snabb hantering av processorn. Ett datorprogram i sig består av binär
information, och när ett program kör finns en del av programmet i det
dynamiska minnet (RAM). Samtidigt finns i minnet även den information som
programmet arbetar med för tillfället som t ex variabler eller text.
Datorns hårddisk fungerar på motsvarande sätt som datorns RAM, d v s binär
information lagras, fast permanent på ett magnetiskt eller optiskt media.
Förutom hårddiskar används CD-skivor, DVD eller magnetband som ofta
används för säkerhetskopiering. Medan innehållet i RAM är temporärt och
varierar efter hand som olika program körs, är hårddiskens innehåll organiserat i
och finns kvar även när datorns stängts av. En fil är en mängd information som
hänger samman, och som datorn hanterar som en enhet. Filerna är i grunden
uppbyggda av ettor och nollor men har olika betydelse och kodning beroende på
hur operativsystem och tillämpningsprogram använder informationen.
Datorns operativsystem, som Linux eller Windows, innehåller ett antal
standardfunktioner för att hantera filer. Dessa funktioner finns i den del av
datorns operativsystem som kallas filsystem. De flesta av funktionerna använder
man när man arbetar med Windows ”filsökare” och t ex raderar eller flyttar filer.
Funktionerna finns även i standardbibliotek som man kan komma åt när man
skriver sina program. I Java finns de flesta sådana funktioner i ett bibliotek som
kallas för java.io (där ’io’ står för in/ut, d v s inmatning och utmatning).
Det finns oändligt många sätt att organisera data i filer. I princip kan var och en
som skriver program hitta på vad som helst. Huvudsaken är att den som gör
program som använder informationen förstår hur filerna är uppbyggda och kan
anpassa sig till det. Därför finns det en stor mängd filtyper. Men eftersom det
skulle vara svårt och opraktiskt att utbyta information om alla program skulle ha
sina egna unika filformat så har det efter hand bildats standarder. Eftersom
musik är intressant för så många så finns det många program som stödjer t ex
mp3-filer. Detta är ingen officiell standard och det finns små variationer men i
stort kan de flesta program hantera alla mp3-filer.
Oftast används de sista tre tecknen i filnamnet för att markera vilken typ av fil
det är. Word-filer slutar exempelvis på ’.doc’, enkla textfiler kallas ’.txt’ och de
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 12 av 52
filer som är intressanta för mitt program heter ’.mp3’. Filerna organiseras av
operativsystemet i olika mappar och undermappar som bildar en trädstruktur.
Det finns en del funktioner i biblioteket java.io för att navigera i filträden.
För användaren, och de program som arbetar med filerna, ser det ut som om
informationen ligger samlat i en enda följd. Men så är egentligen inte fallet, utan
för att kunna utnyttja hårddisken bättre är informationen lagrad i olika avsnitt
(sektorer) som länkas samman av operativsystemet. Det betyder att en fil rent
fysiskt kan vara utspridd över en större del av hårddiskens yta.
Som tidigare nämnts så är all information binär. På ett sätt kan man därför säga
att alla filer egentligen är binära. Men eftersom så mycket av datoranvändningen
går ut på att hantera text pratar man ofta om två grundtyper av filer, dels
”textfiler”, dels ”binärfiler”. En textfil kan man skapa genom att t ex använda
tillbehörsprogrammet ”Anteckningar” i Windows. Om man på detta sätt skapar
en fil och skriver texten ”Java” kommer det att lagras i filen på det standardsätt
för att lagra text som kallas ASCII. Det betyder att varje tecken är kodat som ett
8-bitars värde. Om man öppnar filen med ett redigeringsprogram som kan
hantera binära värden så kan man se att texten ”Java” representeras som fyra
sådana värden som i hexadecimal form blir: 4a 61 76 61
Eftersom alla textfiler i Windows lagras på precis samma sätt kan alla program
som hanterar text förstå ASCII formatet. Med ”binärfiler” menas då alla filer
som inte är rena textfiler. Exempel på filer som är nästan helt binära är
musikfiler och bilder. Men i praktiken är nästan alla filer i ett blandat format.
Det gäller även Word och andra textbaserade filer. För att hantera formatering i
Word (t ex fetstil) finns binär information insprängd i filerna. Det kan man se
om man skapar en Word-fil och skriver texten ”Java” även i denna. Filen blir då
24 kbyte stor och först från position 2560 (hexadecimalt) hittar man själva
texten i ASCII-kod. De filer mitt program arbetar med är i högsta grad
”blandade” vilket ställer krav på programmet som beskrivs senare i rapporten.
Mitt program går till stora delar ut på att arbeta med filsystemet och att avkoda
olika typer av beskrivande information som finns på olika ställen i filerna. Detta
har varit en av de stora utmaningarna för mig. Som en introduktion går jag i
följande avsnitt kortfattat igenom vad digitalt ljud är och hur det representeras i
datorn. Man behöver inte förstå digitalt ljud i detalj för att kunna använda mitt
program och förstå programkoden. Men eftersom begrepp som t ex
”samplingsfrekvens”, ”lager” och ”bithastighet” används i programmet för
beräkningar och avkodning anser jag att det är viktigt med en beskrivning av
vad detta egentligen betyder.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 13 av 52
Digitalt ljud
Ljudvågor
Ljudvågor är vågformade rörelser i ett medium. Precis som vattenvågor sprider
sig över en vattenyta sprider sig ljudvågorna i t ex luften genom att sätta
luftmolekylerna i rörelse. När ljudvågorna når våra öron sätts trumhinnan i
rörelse så att den svänger i samma takt som luften och rör sig proportionellt mot
energin i vågen. Örats olika delar översätter detta till nervimpulser (elektriska
signaler) som hjärnan kan tolka som ljud. Ljud kan beskrivas av två viktiga
storheter:
- Frekvens: Hur snabba svängningarna är (vilka toner).
- Amplitud: Hur starka svängningarna är (vilken styrka).
Kombinationen av frekvens och amplitud ger ljud som vi kan uppfatta som t ex
tal, buller eller musik. Ljud, precis som andra vågrörelser i naturen, är
kontinuerliga, d v s variationer i nivå och styrka kan vara oändligt små eller
stora. Sådana förlopp kallas också analoga. Det finns alltså inga speciella ”steg”
mellan olika nivåer och frekvenser utan dessa varierar kontinuerligt.
De flesta ljud i naturen som t ex fågelkvitter, tal, skratt och vindens sus i träden
innehåller ett stort antal frekvenser och nivåer. Ett ljud som endast består av en
enda frekvens uppfattas ofta som störande och ”disharmoniska”.
Analogt ljud
För inte speciellt länge sedan var den utrustning som användes för att spela in
och spela upp ljud analog. Vanliga bandspelare representerade ljudet i form av
variationer i magnetbandet, och i vinylskivor graverade man in analoga spår som
fick skivspelarens pickupnål att vibrera med en frekvens och amplitud som
motsvarade ljudet. Teoretiskt sett borde detta vara pefekt men tekniska
svårigheter gjorde att ljudkvaliteten ändå inte var perfekt och man fick
störningar som knaster och brus. Begränsningarna gjorde att man började forska
efter andra sätt och eftersom digitaltekniken började bli avancerad tack vare
datorernas utveckling var det naturligt att söka efter digitala lösningar.
Digitala format
För många kanske digitalt ljud fortfarande verkar främmande och nytt men i
praktiken använder de flesta av oss digitalt ljud dagligen. Varje gång vi lyssnar
på radio eller sätter på en CD lyssnar vi på ett digitalt ljud, d v s där
informationen lagrats på ett medium som ettor och nollor som kan avkodas av
den utrustning som spelar upp ljudet för oss.
Vid inspelning måste det någonstans i kedjan ske en översättning från det
analoga formatet till ett digitalt format. Detta kallas AD omvandling. När man
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 14 av 52
sedan spelar upp ljudet igen måste det någonstans ske en översättning åt andra
hållet eftersom ljud per definition är analogt. För några år sedan var det vanligt
att inspelning och mixning av musik fortfarande skedde på vanliga bandspelare
och det var inte förrän man skulle pressa CD skivorna som ljudet digitaliserades.
Därefter kom de digitala bandspelarna (DAT) där man redan vid inspelningen
skapade digital information. Numera är det vanligt att man spelar in direkt på
hårddisk.
Vid avspelning från t ex en CD spelare finns en DA omvandlare som utifrån den
digitala informationen på en CD skiva genererar analoga signaler som skickas
till förstärkaren vilken i sin tur genererar analoga spänningsvariationer för att
styra högtalarna. Resultatet blir analogt ljud som når våra öron.
Eftersom analogt ljud är oändligt variationsrikt i både frekvensinnehåll och
ljudstyrka skulle det i teorin krävas ett oändligt antal bitar och en oändligt snabb
utrustning för att helt utan fel representera ljudinnehållet i en viss inspelning.
Uppgiften är egentligen omöjlig, för eftersom det digitala systemet representeras
av tal med fasta steg mellan kan man aldrig uppnå den fullständigt kontinuerliga
signal som finns i det naturliga ljudet. Så snart man ersätter de kontinuerliga
variationerna med fasta (s k diskreta) steg förlorar man information. Ett digitalt
ljud blir därför alltid en förenkling av det analoga.
Lyckligtvis kan människan inte uppfatta hur fina nyanser som helst, och med
modern teknik kan inspelning och avspelning av digitalt ljud göras med så hög
kvalitet att det börjar bli svårt att skilja mellan verkligt och inspelat ljud. I varje
fall är moderna digitala inspelningar helt överlägsna de gamla analoga. Många
hävdar att det nya ljudet är ”kallare” än det analoga, men vid blindtester går det
inte att se några klara skillnader utan upplevelsen beror på vilken teknik (t ex
filter och effekter) som används vid inspelning och avspelning.
Sampling och DA omvandling
Den krets som omvandlar det analoga ljudet till digitalt ljud fungerar på följande
sätt: I skissen nedan visas en enkel sinusformad ljudsignal. För att digitalisera
denna tar kretsen prov på (d v s samplar, låneord från engelskans sample)
ljudnivån (amplituden) vid ett antal jämna och väldigt små tidsintervall. Varje
nivå representeras sedan som ett siffervärde som lagras i digital form.
Översättning av amplituden till ett digitalt värde kallas kvantisering.
Noggrannheten i värdena, d v s hur många olika nivåer som kan lagras, avgörs
givetvis av antalet bitar. I modern utrustning som DAT-bandspelare används
typiskt 16 bitar vilket gör att amplituden för varje mätvärde (sampel) kan
översättas till ett värde som varierar mellan 0 och 65535. Detta gör det möjligt
att återge amplitudvariationer mellan lägsta och högsta värde på 96 dB. En
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 15 av 52
förutsättning för hög kvalitet är givetvis att mätutrustningen (AD omvandlaren)
har mycket hög noggrannhet.
Fig: Sampling av en sinusvåg
Förutom antalet bitar är den sk samplingsfrekvensen avgörande för kvaliteten.
Samplingsfrekvensen beskriver det tidsintervall mellan vilka AD omvandlaren
samplar ljudnivån. En frekvens på 10 Hz (intervaller per sekund) betyder att det
är 1/10 sekund mellan varje sampel. Det är naturligtvis alldeles för långa
intervall för att kunna återge musik. Den frekvens som ofta används är 44100
Hz. Denna frekvens är naturligtvis inte vald av en slump. Enligt forskaren Harry
Nyquist är den högsta frekvens som ett digitalt system kan återge halva
samplingsfrekvensen. Detta kallas Nyquistteoremet. Med en samplingsfrekvens
på 44100 Hz kan man alltså återge frekvenser på drygt 22000 Hz vilket är
lämpligt eftersom det mänskliga örat kan uppfatta frekvenser mellan 20 och
20000 Hz. Med ännu högre samplingsfrekvenser uppnås ännu högre kvalitet och
många moderna utrustningar för hårddiskinspelning arbetar med 96 kHz (96000
Hz) samplingsfrekvens. Detta borde egentligen inte ge några hörbara skillnader,
men kvaliteten upplevs ändå högre eftersom vissa ljud är oerhört komplexa och
upplevelsen ges av samverkan mellan olika övertoner. Övertonerna är inte
hörbara men bidrar ändå till ”klangfärgen”. Det kan vara detta som gör att de
tidiga digitala systemen upplevdes som ”kalla” av ljudentusiasterna.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 16 av 52
DA omvandlare
Som nämndes ovan så tar AD omvandlaren bara prov på ljudstyrkan
(amplituden). Ljudfrekvenserna återskapas genom att DA omvandlaren spelar
upp ljudinformationen genom att omvandla amplitudvärdena till spänningar och
skicka detta till ljudkretsarna i intervaller motsvarande samplingsfrekvensen. På
så sätt återskapas både frekvens och ljud med en noggrannhet som motsvarar
inspelningsförhållandena.
Fig: DA omvandlaren
Olika filformat
Det finns en stor mängd format för att lagra digitalt ljud på hårddisk. Mängden
information kan vara väldigt stor. Med 44.1 kHz sampling och 16 bitar krävs ca
10 Mbyte för att lagra en minuts musik. Ett exempel på filformat som i stort sett
lagrar den digitala informationen oförändrad är Wave. Microsoft ligger bakom
denna standard och de flesta ljudprogram för in- och avspelning kan hantera
detta format. Informationen i en Wave-fil består nästan enbart av ”råa” 16-bitars
samplingsvärden i binär form. Om det är två kanaler (d v s stereo) ligger ljudet
oftast med vartannat amplitudvärde för höger och vartannat för vänster kanal.
Utrymmeskrävande format som Wave fungerar bra men man fyller snabbt
hårddisken och i bärbara spelare skulle det inte få plats speciellt mycket musik.
En annan nackdel är den distribution av musik via nätet som blivit så populär.
Även med ADSL blir överföringstiden så lång att Wave-filer blir opraktiska.
Därför har ett antal komprimerade format tagits fram.
Komprimeringen av ljudfilerna bygger på algoritmer (betyder metoder/
strategier) som går ut på att man utnyttjar kännedomen om hur de mänskliga
sinnena uppfattar ljud. Genom att utnyttja denna kunskap kan man reducera
informationsmängden kraftigt utan att ljudkvaliteten försämras lika mycket.
Populära komprimerade ljudformat är t ex mp3 och Aiff.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 17 av 52
Mp3 formatet
En mp3-fil, eller MPEG-3 (Moving Pictures Experts Group) fil som
förkortningen egentligen står för är en ytterst komprimerad musikfil. Det blev så
småningom något av en revolution när musikfiler med denna metodik kunde
komprimeras ner till en tiondel av storleken med en mycket liten förlust i
ljudkvalitet. Patentet för mp3 formatet togs av en professor vid Frauenhofer
institutet i Tyskland redan 1989, men det var först år 1998 då ”Winamp”, en
musikspelare som stödde formatet mp3, kom som formatet började komma till
allmän användning.
Rättigheterna till mp3 och WinAmp släpptes fria av Frauenhofer institutet vilket
givetvis har en stor del i varför det blev en nästintill lavinartad effekt med
spridande av musikfiler över nätet. Problemet med piratkopiering var fött och
sedan dess har det pågått en ständig kamp mellan skivbolag och artister och
förespråkare av de fria ljudformaten. Flera alternativa format har släppts men
eftersom licensrättigheterna har varit kontrollerade av utvecklare eller på annat
sätt innehållit begränsade licenser har de inte blivit lika populära. Numera finns
stöd för mp3 avspelning i t ex bärbara spelare, stationära CD-spelare, i
handdatorer och telefoner.
Mp3 kodning
Anledningen till att MP3 formatet kan krympa filstorleken till cirka en tiondel
utan att förlora särskilt mycket i kvalitet är en snillrik kodning som bygger på
små förenklingar i ljudinformationen som knappast är hörbara:
 Basljud har låg frekvens och sång har vanligen högre frekvens. Med hjälp
av Frauenhofer-kodningen sorteras de frekvens som hörs minst
konsekvent bort. En vanlig lyssnare hör inte dessa förändringar eftersom
de till stora delar sker i ”periferin”. Denna form av att sortera bort vissa
frekvenser kallas för ” Minimal audition threshold”.
 De frekvenser som är mera hörbara framhävs mer istället för de
frekvenser som är borttagna. Detta kallas för ”Masking effect”
 Vidare används ”Joint Stereo”, d v s en metodik som gör att höger och
vänster ljudkanal smälts ihop. Redan med denna manöver nästan halveras
filstorleken. MP3 är alltså till stora del ett mono-format, men vid
avspelning upplever lyssnaren ändå en stereoeffekt.
 Vidare används s k Huffman kodning, vilket är en avancerad matematisk
algoritm som sorterar bort vissa frekvenser som förekommer mera sällan
än andra. Detta kan ses som en vidareutveckling av Frauenhofer formatet.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 18 av 52
Program för hantering av Mp3 filer
Problemformulering
Syftet och de övergripande kraven på programmet beskrivs i den första delen av
rapporten. Efter hand delade jag in uppgiften i några olika delar. Detta var
egentligen inte så metodiskt som det kan verka eftersom jag fick hoppa en hel
del mellan olika uppgifter men i stort sett var arbetet indelat i några huvuddelar:
a. Ta reda på hur mp3 filer är uppbyggda och hur man kan filtrera ut den
intressanta informationen.
b. Sök efter liknande program på nätet och se om några komponenter kan
vara användbara.
c. Gör ett huvudprogram för att prova att utvecklingsmiljön fungerar och att
allting är rätt installerat. Detta program kan sedan användas för att driva
olika tester.
d. Lägg upp några lämpliga musikfiler för test.
e. Börja själva projektet med att göra ett program som kan öppna en fil och
se om det är en mp3-fil.
f. Lägg till funktioner som kan hantera ID3 v1 taggar och läsa all
information som kan finnas i en sådan. Gör även testprogram för detta.
g. Lägg till funktioner som kan hantera ID3 v2 taggar och läsa all
information som kan finnas i en sådan. Gör även testprogram för detta.
h. Lägg till funktioner som kan läsa den information som finns i själva mp3
filen (den s k ”headern”). Gör även testprogram för detta.
i. Gör ett enkelt menysystem och bygg ut användargränssnittet med
funktioner för att visa informationen och presentera i både filer och
fönster i menysystemet.
j. Gör funktioner för felhantering.
Mitt program innehåller det mesta förutom att användargränssnittet inte är
komplett. Det som tagit absolut längst tid är (g) och (h). Det tog tid att förstå hur
filerna är uppbyggda och det vart svårt rent programtekniskt.
I följande avsnitt beskriver jag kortfattat de olika momenten (a) – (j) och vilka
problem jag stötte på. De mest besvärliga avsnitten beskriver jag mera ingående
och jag tar också med några exempel på programkod.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 19 av 52
Delproblem (a), beskriv mp3 filer och deras uppbyggnad
Lösning: Eftersom programmet går ut på att hantera mp3 filer är det naturligtvis
viktigt att förstå exakt hur en mp3 fil är uppbyggd. Informationen i en sådan fil
är ”blandad”, d v s det finns dels ren textinformation, dels binär information av
flera olika typer. Den binära informationen är ganska komplicerad. Viss
information ligger väldigt tätpackad i delar av byte och detta gör att programmet
i flera fall måste arbeta med enskilda bitar. Även sifferinformation som t ex
uppgifter om olika längder ligger lagrad på ett sätt som gör att det inte ”bara” är
att läsa den, vilket beskrivs längre fram i detta avsnitt. I stort kan mp3 filerna
sägas bestå av följande tre huvuddelar:
1. mp3 header: Binär information. I denna del finns en del data som främst
används av mp3-spelarna. Informationen talar om t ex vilken typ av
kodning som används (det finns ett antal olika). Mitt program använder
ett par parametrar, bl a för att räkna ut hur långt musikstycket är.
2. mp3-kodad ljudinformation: Binär information som innehåller själva
ljudinformationen på det format som beskrevs ovan. Mitt program läser
inte detta för närvarande.
3. mp3 textinfo: Större delen av den information mitt program använder
finns i s k ”taggar”, d v s speciella avsnitt med beskrivande information
som kan ligga både före och efter ljudinformationen i filen.
Själva ljudinformationen är naturligtvis det viktigaste för de program som spelar
in och spelar av musiken. Men för mitt ändamål är den andra informationen
viktigast eftersom det är i dessa delar den information jag är intresserad av finns.
Det har för ”taggarna” uppstått en informell standard som heter ”ID3” och de
flesta håller sig till detta format även om det finns olika variationer. Det finns i
grunden två olika typer av ”taggar”. Den enklaste, och som kom först, är helt
textbaserad och har en fast storlek. Denna kallas ofta ”ID3 v1”. Ett nyare format
kallas ”ID3 v2” och är betydligt mera komplicerat eftersom informationen
varierar i både storlek och innehåll och innehåller blandad binär och textbaserad
information. En annan svårighet är att det finns olika nyare versioner som t ex
”ID3 v2.2” o s v, men de förändringar som görs numera är mest inriktade på att t
ex lägga in bildinformation, länkar till webbadresser och olika typer av certifikat
för att hantera licensrättigheter. Detaljerna för de olika delarna beskrivs närmare
i de avsnitt som beskriver respektive programavsnitt.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 20 av 52
Delproblem (b), sök efter liknande program
Lösning: Jag hittade inga komponenter som jag kunde använda direkt. Men jag
hittade en del ”open source” kod som jag kunde lära mig av. Detta hjälpte mig
att förstå hur filerna var uppbyggda. Jag lärde mig också mera om Java-program
och hur man praktiskt läser information från filer. Speciellt lärde jag mig om hur
man hanterar ”exceptions” (kan på svenska kallas ”undantag”), d v s feltillstånd
i programmen. När man t ex läser filer och gör beräkningar är det mycket som
kan gå fel och för att programmet inte bara skall krascha eller hänga sig kan man
använda undantagshanteringen för att ta hand om felet så att programmet kan
fortsätta fungera ändå.
Delproblem (c), gör ett huvudprogram
Lösning: I Java skapar man ett huvudprogram genom att skriva en funktion som
döps till main och som ser ut på ett speciellt sätt. När programmet startas i
Javamiljön kommer koden för main att vara det som körs igång allra först.
I mitt program finns klassen Driver som innehåller main som alltså är
huvudprogrammet. I main finns den kod som behövs för att starta programmet
och skapa de klasser som behövs för att köra igång allting. Det ser ut så här:
public class Driver {
public static void main(String[] argv) {
...
GuiDriver myGuiDriver = new GuiDriver();
myGuiDriver.kickIt();
}
}
I funktionen main används argv för att man skall kunna skicka olika typer av
information till programmet vid uppstart. För närvarande använder jag inte
denna funktion i mitt program. I mitt program gör main egentligen inte så
mycket. Först startas användargränssnittet genom att skapa en instans av klassen
GuiDriver. Sedan anropas funktionen kickIt() i denna klass och den blir sedan
huvudprogrammet. Det är praktiskt eftersom mycket kommer att handla om att
arbeta med användargränssnittet och starta olika delprogram.
Kommentar: Det är inte alltid lätt att avgöra var funktioner och data skall ligga.
Man märker efter hand att man behöver komma åt information som finns på
andra ställen och det verkar inte finnas några egentliga regler för var man lägger
olika saker. Det verkar som om man får gå på känsla och erfarenhet. Det är svårt
när man är nybörjare.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 21 av 52
Delproblem (d), gör testfiler
Lösning: Jag hämtade hem några program från nätet av typ ”tag editors”, d v s
program för att läsa och skriva in information i mp3 filer. De visade sig fungera
ganska dåligt. Några kraschade, andra verkade inte visa rätt information. Det
program som verkade bäst var Winamp, som innehåller funktioner för att läsa
och skriva information i den fil som spelas för tillfället. Jag tog några filer och
skrev in information i taggarna som jag sedan kunde använda för test.
Delproblem (e), öppna och kontrollera en fil
Lösning: Jag definierade klassen mp3file som innehåller allmän information om
en mp3-fil. Denna får ”ärva” från biblioteksklassen File som finns i java.io.
Genom detta kan jag efter hand lägga till en del funktioner som är speciella för
min filhantering, och samtidigt kan jag använda de funktioner och datastrukturer
som redan finns för att arbeta med filer. Deklarationen blir:
Import java.io.File;
public class mp3file extends java.io.File {
private final String MP3_EXTENSION = "mp3";
..
public mp3file(String dir, String filename) {
super(dir, filename);
}
}
Funktionen som har samma namn som klassen, d v s mp3file, är en s k
”konstruktor”. Det är en funktion som anropas när man skapar ett objekt av
denna klass, d v s när man t ex skriver xx = new mp3file(..). Denna funktion gör
för tillfället endast ett anrop till sin ”förälders” konstruktor för att knyta filens
namn och katalog till objektet. För att prova att det fungerar att skapa ett objekt
finns följande kod i Driver.main():
String dir = "C:\\Users\\Gemensamma downloads\\Java\\JAVA
projekt\\testfiler";
String testFilename = "test.mp3";
testfile = new mp3file(dir, testFilename);
I Driver.main() anropas sedan en annan funktion i mp3file för att kontrollera om
det är en mp3 fil:
if (testfile.isMp3File()) {
... // Ok i så fall
}
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 22 av 52
Koden för detta i mp3file är enkel:
public boolean isMp3File() {
return ((this.getName()).toLowerCase().
endsWith(MP3_EXTENSION));
}
Funktionen getName() finns i java.io.File och den returnerar en textsträng som
här sedan omvandlas till små bokstäver genom toLowerCase() som är en
standardfunktion i Java-bibliotekets String klass. Genom funktionen
endsWith()jämförs de sista tecknen i filnamnet med texten ”mp3”. Om de sista
tecknen stämmer returneras ”true”, annars ”false”.
Kommentar:
Syftet med denna funktion är bara att snabbt filtrera bort alla filer som inte kan
vara mp3-filer. För att avgöra om filen verkligen är av rätt format behövs andra
kontroller. Men jag väntar med det eftersom jag får göra andra kontroller sedan
när jag letar efter information.
Delproblem (f), läs ut ID3 v1 information
Lösning: Vissa filer har inga taggar alls, vissa har en tagg av ena sorten (v1 eller
v2) och vissa har båda typerna. Den äldre varianten v1 är alltid 128 byte stor1.
Om den finns så ligger den alltid sist i filen. Alla version 1 taggar börjar med
bokstäverna TAG. Sedan följer, alltid i samma ordning, följande information:
Sångtitel 30 Tecken
Artist
30 Tecken
Album
År
30 Tecken
4 Tecken
Kommentar 28 Tecken
Spår
1 Byte
Genre
1 Byte
De första fem fälten är alltid ren textinformation men ofta lämnas vissa fält
tomma. Fältet för spårnummer är numeriskt och det är även fältet för genre. Det
är kodat enligt en princip som de flesta följer. Det är dock ingen riktig standard.
Så här kan en ID3v1 tag se ut om man tittar på den i ett redigeringsprogram:
TAGSetting sun
1
Chemical Brothers
Surrender
1999CoOol
En byte = ett tecken
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 23 av 52
De sista två tecknen är binära. Med WinVi kan man se att spåret är 6 och genren
19, d v s Techno. Men titeln är fel – den borde egentligen vara ”The sunshine
underground” och det visar att man inte alltid kan lita på informationen.
Programlogiken blir ganska enkel även om det tog tid att få det att fungera. Först
definierar jag en klass som döps till id3v1. Den innehåller bl a en funktion
kallad hasTag() som undersöker om det finns en tagg av v1 typ. Den tittar dels
på filens längd, som naturligtvis måste vara större än 128 tecken (TAG_LEN
nedan), dels kontrollerar den om de första tre tecknen är lika med textsträngen
”TAG” (finns i konstanten TAG_START):
public boolean hasTag() throws IOException {
RandomAccessFile inputFile = null;
inputFile = new RandomAccessFile(mp3_file, "r");
if (inputFile.length() <= TAG_LEN) {
return false;
}
long filePos = inputFile.length() - TAG_LEN;
inputFile.seek(filePos);
byte buffer[] = new byte[3];
if (inputFile.read(buffer, 0, 3) != 3) {
throw new IOException("Could not read TAG");
}
inputFile.close();
String testTag = new String(buffer, 0, 3, UNICODE);
if (!testTag.equals(TAG_START)) {
return false;
}
return true;
}
I klassen finns även datastrukturer för att spara undan den information som
hittas i taggen. De fylls på med funktionen readTag(). Först läser denna funktion
hela v1 taggen, och sedan fyller den på datastrukturerna genom att läsa tecknen
från de fasta positionerna på följande sätt:
inputFile = new RandomAccessFile(mp3_file, "r");
inputFile.seek(inputFile.length() - TAG_LEN +
TAG_START.length());
byte[] buffer = new byte[TAG_LEN - TAG_START.length()];
if (inputFile.read(buffer, 0, TAG_LEN –
TAG_START.length())
!= TAG_LEN - TAG_START.length()) {
throw new IOException("Could not read tag");
}
String tag = new String(buffer, 0, TAG_LEN –
TAG_START.length(),UNICODE);
title = tag.substring(0, 30).trim();
artist = tag.substring(30, 60).trim();
... // Resten av informationen
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 24 av 52
För att testa funktionerna finns i Driver.main() anrop av readTag() för testfiler
och resultatet kontrolleras genom att innehållet i datastrukturerna skrivs ut.
När man läser filer använder man s k ”strömmar”. Ett objekt av typen
java.io.File beskriver bara sökvägen till filen. Sedan måste man koppla den till
en ström som man kan se som en ”rörledning” där data kan skickas till och från
en hårddisk eller t ex en port. Det finns två sätt att göra det på. Det ena sättet är
en Input/OutputStream och det andra är RandomAccessFile. Man kan egentligen
använda vilket som helst i de flesta fall men enligt läroböckerna verkar det som
om t ex OutputStream är tänkt att använda när man matar ut en mängd data i en
följd till en fil. RandomAccessFile verkar användas när man skall ”hoppa” till
olika bestämda ställen i en fil och läsa små datamängder åt gången.
Från början använde jag InputStream men övergick till RandomAccess eftersom
det verkade bättre för v2 taggarna.
Kommentarer: När man använder klassen Input/OutputStream kan man läsa och
skriva data till filer på två sätt, antingen binärt eller som text. Om man använder
binärt format skriver och läser man informationen som den är, d v s utan
omvandlingar. Men om man läser eller skriver textsträngar omvandlas de mellan
det format som gäller för operativsystemet (som kan vara olika) och den
representation som gäller för strängarna i minnet. I Java använder man alltid en
kodning som heter ”unicode” som är en 16-bitars standard. ASCII är ju bara på
8 bitar och kan därför inte klara t ex japanska vilket unicode kan.
När man använder RandomAccessFile är det alltid binär läsning så i exemplet
ovan omvandlar jag bufferten till olika textsträngar. Jag hittade ett exempel på
nätet hur det går till men fick problem i början. Anledningen var att när man
skapar en textsträng från en mängd byte genom tag = new String(buffer, ..)
måste man också ange att unicode skall användas för att konverteringen skall
fungera. Detta hittade jag till slut i en tutorial på nätet.
Delproblem (g), läs ut ID3 v2 information
Lösning: Från början verkade problemet vara likadant som för v1 taggarna. Men
det visade sig vara mycket mera komplicerat. Det tog tid att hitta information
om formaten, och det blev också mycket svårare eftersom olika saker verkar
gäller för olika versioner av ID3V2 standarden, och felen i vissa filer gjorde inte
saken lättare. Efter hand som jag förstod hur komplicerat detta skulle bli fick jag
dela in problemet i olika delar.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 25 av 52
Allt detta gjorde jag inte i en följd utan jag fick hoppa mellan olika delproblem
efter hand som jag lärde mig mera om hur det fungerade:
 g1 : Ta reda på hur v2 taggarna är uppbyggda.
 g2 : Skapa klasser där informationen kan lagras.
 g3 : Skriv en funktion som kontrollerar om det finns en v2 tagg i en mp3
fil.
 g4 : Skriv funktioner för att läsa ut och beräkna storleksinformation och
positioner i filerna för olika typer av information.
 g5 : Skriv en funktion som kontrollerar om det finns en s k ”extended
header” och justera positionerna för det.
 g6 : Skriv funktioner för att läsa in, kontrollera och lagra de olika typerna
av information.
 g7 : Skriv testprogram för att kontrollera inläsningen.
Lösning (g1):
Den första svårigheten med V2 taggar är att informationen kan ha en obegränsad
storlek till skillnad från den fixa storleken för V1 taggen. Detta beror på att ID3
version 2 togs fram för att man skulle kunna lagra mera information. Den kan
därför innehålla många fler detaljer om låtar. Men den största skillnaden är att
V2 taggen dessutom kan innehålla i princip vilken binär eller textinformation
som helst. Exempel är bilder på artister, länkar till olika webbplatser och HTML
filer. Så här ser strukturen för V2 taggen ut i stort:
Header
10 byte
Förlängd header
Obegränsad storlek
Informationsblock (frames) Obegränsade i storlek
Utfyllnad (extra info)
Avslutning
Obegränsad i storlek
10 byte
Header:
Den första ”Headern” (överskriften) innehåller följande information:
identifierare:
versionsnummer
flaggor
storlek
4
2
1
4
byte
byte
byte
byte
(text)
(binärt)
(binärt)
(binärt)
Som framgår i beskrivningen av V1 taggen så är den konstant i storlek och
informationen är relativt enkel att avkoda. Men det enda som man från början
kan vara säker på för en V2 tagg är:
- Om den finns, så ligger den alltid allra först i filen
- Den börjar med de tre ASCII-tecknen ”ID3”
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 26 av 52
Version: Det som framför allt skiljer mellan olika versioner är att högre
versionsnummer har stöd för mera information. För varje högre versionssiffra
(1, 2, 3 o s v) har olika typer av ”ramar” (frames) tillkommit, Mellanversioner,
som t ex 3.1, 4.2
o s v innehåller oftast mindre justeringar.
Tyvärr verkar ID3 inte vara någon riktig officiell standard, d v s det finns ingen
stor organisation eller en massa företag som utvecklar den. Tidigare fanns en
förening (www.id3.org) som verkade ta mest men webbplatsen fungerar sällan
och frågan är vad som egentligen kommer att hända framöver. Eftersom ”mp3världen” mera fungerar som en löst sammansatt ”community” är det si och så
med hur olika personer följer standarden och det ger praktiska svårigheter.
Naturligtvis är det speciellt svårt när man som nybörjare börjar titta på innehållet
i filerna och det inte stämmer med beskrivningarna.
Flaggor: De finns i en byte där värdet för varje enskild bit har en viss betydelse.
För närvarande verkar det vanligaste vara att de fyra högsta bitarna i flagg-byten
%abcdxxxx har följande betydelse:
a: anger om ”unsynchronising” används för hantering av fel i filen
b: anger om det finns en förlängd header eller ej
c: anger om det är en taggversion under utveckling (d v s opålitlig)
d: anger om det finns en avslutning (de 10 tecknen enligt ovan)
I mitt program är jag mest intresserad av den bit som anger om det finns en
förlängd header eftersom programmet måste hoppa över den om den finns.
Storlek: Storleken på hela taggen anges alltså av fyra byte som man måste tolka
olika beroende på om det finns en förlängd header eller ej. Om den finns så
ingår den i den totala storleken. I denna storlek ingår även avslutningen på 10
byte (om den finns). Storleksinformationen ligger med den högsta (mest
signifikanta) byten först.
Just storleksinformationen var mycket förvirrande i början. Förutom att
storleken betyder olika beroende på hur flaggorna är satta så verkade siffrorna
över huvud taget inte stämma. När man tittade på testfilerna med ett
redigeringsprogram så verkade ibland storleken på headern vara större än hela
filen. Efter en hel del letande på nätet hittade jag en beskrivning som förklarade
detta. Tydligen är det så att olika versioner av MPEG-standarden innehåller
olika typer av synkroniseringsinformation, d v s som talar om för mp3-spelaren
när ljudinformationen och olika ljudblock börjar. I en del äldre varianter av
MPEG användes den högsta biten för ett antal byte i början av ett ljudblock för
sådan synkronisering. Detta verkar ha ändrats i nya varianter, men eftersom
vissa äldre mp3 spelare letar efter sådan information kan det hända att de ”spårar
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 27 av 52
ur” om de hittar information där den högsta biten är ett-ställd. Ett sätt att
undvika det är att informationen görs ”synch-safe”. Det betyder att man inte
använder den högsta biten utan siffrorna förskjuts ett steg per byte, med början i
den minst signifikanta byten. Ett exempel:
Bit nr:
Byte 1:
Byte 2:
Byte 3:
Byte 4:
7654
0000
0110
1001
1100
3210
0001
1101
0010
0111
Detta skulle kunna representera storleksinformationen för en väldigt stor ID3 v2
header, t ex med bildinformation. Här är alltså den högsta biten ett-ställd för
både byte 3 och 4. För att undvika detta gör man om värdet till ”synch-safe
integer” med följande algoritm:
- läs Byte 4 (minst signifikanta)
- spara värdet för den högsta biten i denna byte, B4.7
- nollställ B4.7
- läs B3, spara de två högsta bitarna B3.7 och B3.6
- skifta in den sparade B4.7 från höger i B3
- nollställ B3.7
- etc.
På detta sätt kommer det binära värdet att förändras till:
Bit nr: 7654 3210
Byte 1: 0000 1011
Byte 2: 0011 0110
Byte 3: 0010 0101
Byte 4: 0100 0111
I mitt program blir algoritmen den omvända, d v s siffrorna måste byggas upp
genom att börja med den mest signifikanta byten och skifta in rätt antal bitar
successivt från vänster och på det sättet bygga upp ett normalt heltal som anger
headerns storlek (se även avsnitt g4). Det var ganska svårt att inse hur detta
hängde ihop och eftersom några av testfilerna var felaktiga var det ännu svårare.
Informationsblock (frames): Den för mitt program intressanta informationen
finns i de frames, eller informationsblock, som ligger efter den inledande
informationen. Varje Frame har sin egen header som ser ut på följande sätt:
Frame ID: 4 tecken (text)
Storlek : 4 byte
(binärt)
Flaggor : 2 byte
(binärt)
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 28 av 52
Storleksinformationen på 4 byte är även i detta fallet ”synchsafe”. Storleken
inkluderar inte Frame-headern om 10 byte. Flaggorna kan innehålla information
som anger t ex komprimeringar och olika typer av kryptering. Mitt program
använder inte denna information utan hoppar över flaggorna.
Efter flaggorna följer sedan själva informationen, som kan vara i både text och
binärt format. Jag är i mitt program bara intresserad av textinformation. Jag
började med en typ av information och byggde efter hand ut med flera typer. En
svårighet är att informationen inte kommer i en viss ordning utan ibland kommer
låttiteln först, ibland sist etc. Man får därför leta efter de olika ID som används
som rubrik. Det finns ett ganska stort antal olika Frames. Några av de jag är
intresserad av att läsa är:
ID
TIT1:
TIT2:
TALB:
TOAL:
TRCK:
Betydelse
Grov kategorisering som t ex “klassiskt”.
Sångtitel.
Namnet på album som innehåller sången
Indikerar originalinspelning, t ex om
sången är en nyinspelning eller cover.
Spårets nummer, om låten är från ett album.
De fyra inledande tecknen är alltså de nyckelord mitt program söker efter för att
avgöra om det är information jag är intresserad av (se avsnitt g6).
Lösning (g2):
Det verkade logiskt att ha en klass för de viktigaste begreppen. Som beskrivs
ovan hade jag tidigare definierat klassen id3v1 för att ta hand om data och
funktioner för den första typen och nu tillkom id3v2 för v2 taggen. På samma
sätt som för v1 taggen definieras funktionen hasTag() och även funktionen
readTag(). I klassen id3v2 definieras även en del konstanter för olika positioner
i filen. Från början hade jag i id3v2 även en del datastrukturer för att spara
undan den inlästa informationen.
Det verkade naturligt att skilja mellan den övergripande informationen i taggen
(storlekar, flaggor mm) och själva informationsinnehållet i de olika blocken.
Anledningen var att både själva klassen i sig och readTag() växte och blev allt
mer omfattande eftersom hanteringen av taggen blev ganska komplicerad. I
läroböcker och tutorials står att om man har bara några få klasser med nästan
alla funktioner och all data i så är det ett tecken på att programmet inte är så
”objektorienterat” gjort och därför kan vara svårt att vidareutveckla. Då bör man
t ex dela upp klassen i två eller t o m fler delar.
I det här fallet är ju hanteringen av hela taggen, beräkning av storlek och
sökandet efter ramar ganska skilt från tagginnehållet så därför är det vettigt att
skapa en ny klass för själva informationen. Denna kallas v2Frames och ansvarar
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 29 av 52
för att läsa in informationsinnehållet. Efter ytterligare arbete kändes det ändå
naturligt att skapa ännu en klass med ansvar för att lagra och hantera ett enstaka
datablock. Denna döptes till v2Frame. På så sätt kommer v2Frames att vara
knuten till ett antal objekt (instanser) av klassen v2Frame.
Lösning g3, kontroll av header
Denna lösning gjordes på ungefär samma sätt som för v1 taggarna, d v s en
funktion hasTag() kontrollerar om det finns en tag. Skillnaden är främst att
funktionen letar allra först i filen och att den söker efter andra tecken.
Lösning g4, kontroll av storlekar etc
Ansvaret för att läsa storleksinformation, flaggor och annan allmän information
finns i id3v2.readTag().
Logiken ser ut på följande sätt:
- Kontrollera om det finns en v2 tagg genom att anropa hasTag()
- Skapa en buffert och läs in de fyra byte som innehåller totallängden
- Konvertera dessa fyra byte till heltalet tagLength.
- Läs den byte som innehåller flaggorna och kontrollera den bit som
markerar om det finns en ”extended header”
- Om det finns en sådan, läs in storleksinformationen för denna och
konvertera till heltalet extHeaderOffset
- Räkna ut på vilken position i filen som datablocken börjar genom
beräkningen: sizeOfTag = 10 + extHeaderOffset + tagLength
För att kontrollera flaggbitarna och för att räkna ut storleksinformationen
användes ett par funktioner som placerades i en speciell klass för ”diversefunktioner”. Den enklare av dessa ser ut på ungefär följande sätt:
public boolean bitIsSet(byte b, int pos) {
boolean isSet = false;
if ((pos >= 0) && (pos < 8)) {
isSet = ((b & (byte)(1 << pos)) != 0);
}
return isSet;
}
Här används de två binära operatorerna ’<<’ och ’&’. Operationen ’A & B’
kombinerar två binära tal bitvis. Om bitarna i en viss position är ettställda för
både A och B så blir även samma bit i resultatet ettställd. Om däremot denna bit
är nollställd för antingen A, B eller båda så blir denna bit noll i resultatet. Detta
kan man använda för att kontrollera om en viss bit är ettställd. Om t ex värdet
för A är 8, d v s den fjärde biten från ”höger” är ettställd så blir resultatet av ’A
& B’ bara skilt från noll om den fjärde biten är ettställd även i B.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 30 av 52
Skiftoperationen << flyttar här alla bitarna ett steg till vänster i en byte, så efter
operationen (1<<pos) får man ett tal där biten på plats ’pos’ är ettställd medan
alla andra bitar är nollställd. Denna kombineras sedan med ’b’ för att kontrollera
om resultatet blir skilt från noll.
Den andra viktiga funktionen som används här heter
utils.charsToSynchSafeLong() och den fungerar som den algoritm för att skapa
synchsafe heltal som beskrevs tidigare i dokumentet, fast tvärtom. Algoritmen
utgår från den mest signifikanta byten (den första) och gör följande:
- Beräkna hur många positioner, räknat i antal byte, som denna byte skall
skiftas (om det är den första byten av 4 skall den skiftas 3 byte åt vänster).
- Beräkna hur många bitar som egentligen tillhör nästa byte. Dessa bitar
finns längst till höger. Samtidigt vet man då hur många bitar som tillhör
denna byte, d v s resten.
- Använd operatorn ’&’ för att skaffa två tal genom att ”maska”, det ena
genom att maska med de bitar som tillhör denna byte, den andra med de
bitar som tillhör nästa byte.
- Skifta talen så många positioner till vänster som krävs för att bitarna skall
hamna på rätt ställe med hänsyn till både vilken byte det är och hur många
bitar som finns på ”fel” ställe i just denna byte. För detta används
operationen ’<<’
- Addera resultaten till en delsumma.
- Fortsätt på samma sätt med samtliga byte När alla byte hanterats
innehåller summan det konverterade heltalet.
Koden blir ungefär:
public static long charsToSynchSafeLong(byte[] in, int
firstPos, int noOfChars) {
for (int i=firstPos; i < (firstPos+noOfChars); i++) {
// Create a mask for bits belonging to the next byte.
bytesToShift = noOfChars - 1 - (i - firstPos);
synchMaskBits = 8 - bytesToShift;
maskByte = 0xff >>> synchMaskBits;
nextByte = in[i] & maskByte;
// Remaining bits belong to this byte
thisByte = (in[i] & (0xff - maskByte));
// Now add the values. This is shifted by
// bytesToShift positions, adjusted for the
// synchSafe bits belonging to the next byte.
bitsToShift = (bytesToShift * 8) - bytesToShift;
returnValue += (long)(thisByte) << bitsToShift;
// Remaining part belongs to left bits of next byte
returnValue += (long)(nextByte) << bitsToShift;
}
return returnValue;
}
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 31 av 52
Lösning g5, kontroll av extended header
När funktionen för att kontrollera en viss bit fungerade var det relativt enkelt att
kontrollera om filen har en extended header eftersom detta (se ovan) anges av en
viss bit i den byte som innehåller flaggorna:
hasExtendedHeader = numeric.bitIsSet(buffer[0],
EXT_HEADER_BIT);
Om biten var ettställd betyder det att man måste läsa in storleksinformationen,
d v s de fyra byte som kommer omedelbart efter. Även de måste konverteras
med funktionen utils.charsToSynchSafeLong().
Lösning g6, inläsning av informationen
Svårigheten var att bestämma hur klasserna skulle definieras och var olika delar
av inläsningen skulle ligga. Det finns säkert många olika sätt att göra det på.
Så här arbetar programmet i stort:
- id3v2.readTag() anropas av ett program. Dessförinnan har en mp3 fil
knutits till objektet genom konstruktorn som anropades när man skapade
id3v2 objektet.
- Först beräknar readTag() positionen där informationen börjar på det sätt
som beskrivs ovan.
- Därefter skapas ett v2Frames objekt:
theFrames = new v2Frames();
Konstruktorn v2Frames() förbereder inläsningen genom att skapa en
vektor, allFrames, där den inlästa informationen skall lagras efter hand.
- Sedan tar det nya objektet över ansvaret för inläsningen med anropet:
theFrames.readFrames(inputFile, HEADER_LEN +
extHeaderOffset, tagLength);
- En ytterligare klass, v2Frame, bröts ut. Denna är till för att lagra
innehållet i ett enda block. Den har en funktion för att läsa in sig själv,
kallad readFrame().
Funktionen readFrames() blev ganska omfattande och delades upp i några delar.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 32 av 52
Så här ser koden ut:
while (!endOfFile && fileOffset < tagLength) {
v2Frame theFrame = new v2Frame();
bytesRead = 0;
if ((bytesRead = theFrame.readFrame(theOpenFile,
filePos + fileOffset)) != 0) {
if (keepFrame(theFrame)) {
if (!storeFrame(theFrame)) {
endOfFile = true;
}
}
fileOffset += bytesRead;
noOfFramesRead++;
}
else {
endOfFile = true;
}
}
Funktionen arbetar alltså i en slinga tills den antingen läst hela taggen eller tills
något oväntat inträffar. I klassen v2Frames (där alltså de inlästa v2Frame
objekten lagras i en vektor) finns två viktiga hjälpfunktioner. Funktionen
keepFrame() kontrollerar om det inlästa blocket är ett av dem jag vill behålla. I
strängvektorn frameTypes finns de första tecknen för de typer jag är intresserad
av (se ovan) och koden för att kontrollera blir:
public boolean keepFrame(v2Frame theFrame) {
for (int i = 1; i < frameTypes.length; i++) {
if (frameTypes[i].equals(theFrame.getFrameType())) {
theFrame.setFrameId(i);
return true;
}
}
return false;
}
Hjälpfunktionen v2Frame.getFrameType() är en enkel hjälpfunktion som
returnerar den tidigare inlästa typen för detta block.
Som framgår ovan anropar readFrames() funktionen v2Frames.storeFrame()
när den genom anropet till keepFrame() kommit fram till att informationen skall
sparas. Uppgiften för storeFrame() är att kopiera blockets information till de
datastrukturer som sedan skall användas av användargränssnittet för att visa på
skärmen eller skriva ut informationen i en lista. Anledningen till att jag gör en
speciell funktion för detta är mest att det verkar vara en praktisk uppdelning. I
början var jag inte säker på hur användargränssnittet skulle se ut och det verkade
vettigt att lägga kopplingen till användargränssnittet på ett speciellt ställe.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 33 av 52
Kommentar: Koden för denna del av programmet är kanske inte så svår. Rent
tekniskt är den enklare än funktionerna för att omvandla byte till ”synchsafe”
heltal. Men det var svårt att hitta en riktigt bra struktur för var informationen
skulle lagras, var olika funktioner skulle placeras och vilken funktion som skulle
ansvara för vad. Jag gjorde flera ändringar och fortfarande har jag känslan av att
man skulle kunna göra helt annorlunda. Jag tror att det tar krävs mycket vana för
att få en riktigt bra design.
Lösning g7, testprogram
För att testa funktionerna för v1 och v2 taggar lade jag upp ett antal testfiler.
Vissa hade information i sig från början och en del fick jag komplettera genom
att mata in uppgifter genom Winamp.
Därefter lade jag direkt i Driver.main() upp anrop till readTag() etc och för att
kunna följa upp resultaten lade jag in många anrop där jag skrev ut olika data. I
de fall där jag fick oväntade resultat lade jag in några extra utskrifter för försöka
hitta var det gick snett. Exempel på ”debugutskrifter”:
I Driver.main():
System.out.println("Hej, detta är det nya projektet, innan
jag öppnar: " + dir + "\\" + testFilename);
I id3v2.readFrame() finns t ex:
System.out.println("Börjar med att anropa hasTag");
if (!hasTag()) {
System.out.println("Filen hade ingen läsbar Tag");
throw new IOException("Could not read tag");
}
Ett alternativ hade varit att använda sig av de testverktyg (s k ”debugger”) som
finns i utvecklingsmiljön. Men det tar ganska mycket tid att ställa allting i
ordning och det tar också längre tid att stega igenom programmet än det tar att
göra utskrifter. Om det är ett komplicerat program eller om man inte kan skriva
ut (t ex om det är ett system utan skärm eller felet är i själva utskriften) kan det
säkert vara bra att använda debuggern.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 34 av 52
Delproblem (h), läs ut mp3 Header information
Lösning: Det kan finnas en hel del information i ID3 v1 och v2 taggarna. Men
jag hittade inte ett par som jag tyckte självklara data, som t ex låtens spellängd
och samplingsfrekvens. Efter att ha läst en del dokument om mp3 standarden
förstod jag att denna information inte finns i taggarna därför att den redan ligger
i själva mp3 filen, i ett antal informationsblock före själva ljudinformationen.
Jag delade in arbetet på ungefär samma sätt som för ID3v2, d v s:
 h1 : Ta reda på hur mp3 informationen ser ut.
 h2 : Skapa klasser där informationen kan lagras.
 h3 : Skriv funktioner för att läsa ut informationen.
 h4: Skriv en funktion för att beräkna filens spellängd.
 h5 : Skriv testprogram för att kontrollera inläsningen.
Vissa av de steg som behövdes för v2 taggarna behövdes inte här.
Lösning (h1), mp3 informationen
Den information som jag är intresserad av finns i den s k mp3 headern. Precis
som med ID3 v2 taggen så tog det tid att klara ut hur denna är konstruerad.
Svårigheten är inte som i ID3 v2 att det finns många versioner av standarden, för
det verkar som om de flesta följer ungefär samma mönster. Men informationen
är väldigt kompakt och kodad på ett ganska komplicerat sätt. Det går inte att
bara läsa ett fält och översätta det till ett värde utan man måste avkoda bitfält
och dessutom kombinera olika värden med tabeller för olika kodningsversioner.
Mp3 headern består endast av 4 byte, där man bara använder sig av 2 ½ byte för
att lagra allmän information. Om man tittar med WinVi på de första fyra byten i
en musikfil kan det i binär (hexadecimal) form se ut på följande sätt:
ff fb 90 44
Översatt till binära siffror blir det alltså:
1111 1111 1111 1011 1001 0000 0100 0100
Efter att som vanligt letat bland mp3 och open-source sajterna på nätet hittade
jag information om vad detta betyder. Jag beskriver här detta steg för steg:
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 35 av 52
Mp3 headern kommer direkt efter v2 taggen (om en sådan finns) och innehållet
är enligt följande:
Sync:
De första tolv bitarna är alltså synkroniseringsinformation, d v s en följd av
ettställda bitar för att mp3 spelarna skall hitta rätt.
ID:
Därefter kommer en ID-bit som indikerar vilken av de två övergripande
ljukodningar som filen är gjord för: ID=0 betyder MPEG-2, medan ID=1
betyder MPEG-1. Den första versionen av MPEG-1 standarden kom redan 1992.
Sedan dess har det utvecklats flera olika beräkningsscheman kallade Layer-1,
Layer-2 och Layer-3. Det vanligaste för dagens musikfiler är Layer-3, och MP3
var från början en förkortning för ”MPEG-1 Layer 3”.
I exemplet ovan är ID-biten ettställd vilket alltså betyder MPEG-1. Detta var
förväntat eftersom MPEG-2 mest används för digital video.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 36 av 52
Layer:
För att spela upp musik i en dator är en variant lämplig, medan ett ännu mera
kompakt format kan vara lämplig för ”strömmande media”, d v s musik som
spelas upp direkt via nätet. Därför finns ett antal olika lager (”layer”) format
definierade. De två bitarna anger vilket av de tre Layer1-3 som använts. För att
avkoda detta krävs en tabell:
Bitvärden
0 0
0 1
1 0
1 1
Betydelse
Odefinierat
Layer III
Layer II
Layer I
Om man tittar på innehållet i exemplet ser man att bitvärdena är 0 1 vilket
betyder Layer III. Detta är också väntat och är egentligen ointressant för mitt
program. Men informationen är nödvändig för att man skall kunna avkoda
samplingsfrekvens och bithastighet.
Prot.Bit:
Används inte här.
Bitrate:
Bitrate, eller bithastigheten, anger vilken läshastighet som krävs för att spela upp
informationen utan störningar. Bithastigheten är ett kvalitetsmått, d v s hög
hastighet betyder vanligen högre kvalitet och den hänger samman med
samplingsfrekvensen. Högre frekvens vid samplingen gör att fler mätvärden
produceras som måste tas om hand av spelaren. En hög bithastighet ställer vid
spelning över en Internetförbindelse också höga krav på överföringen och därför
måste man ofta använda lägre hastigheter i ”strömmande media”.
Den vanligaste kvaliteten ligger runt 128 bitar i en mp3 fil avsedd för lokal
uppspelning.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 37 av 52
De fyra bitar som anger bitrate i kbit/sekund kräver en speciell tabell för att
kunna avkodas, där bitfält och kombinationen av kodning och lager används för
att hitta rätt värde. Så här ser den ut:
Bitrate MPEG-1, MPEG-1, MPEG-1,
MPEG-2, MPEG-2, MPEG-2,
value
layer I layer II layer III layer I layer II layer III
0 0 0 0
0 0 0 1 32
32
32
32
32
8
0 0 1 0 64
48
40
64
48
16
0 0 1 1 96
56
48
96
56
24
0 1 0 0 128
64
56
128
64
32
0 1 0 1 160
0 1 1 0 192
80
96
64
80
160
192
80
96
64
80
0 1 1 1 224
112
96
224
112
56
1 0 0 0 256
128
112
256
128
64
1 0 0 1 288
1 0 1 0 320
160
192
128
160
288
320
160
192
128
160
1 0 1 1 352
224
192
352
224
112
1 1 0 0 384
256
224
384
256
128
1 1 0 1 416
320
256
416
320
256
1 1 1 0 448
384
320
448
384
320
1 1 1 1
Fig: Olika lager och bithastigheter
I exemplet ovan var de fyra bitarna som indikerade bithastigheten: 1 0 0 1
och eftersom filen är kodad i MPEG1 layer III kan man ur tabellen utläsa att
bithastigheten är 128 bps.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 38 av 52
Frequency:
De två bitar som anger samplingsfrekvensen kräver också en tabell för att kunna
avkodas. Den ser ut så här:
Värde MPEG-1
MPEG-2
0 0
44100 Hz 22050 Hz
0 1
48000 Hz 24000 Hz
1 0
32000 Hz 16000 Hz
1 1
I exemplet var bitarnas värde: 0 0
Eftersom kodningen är MPEG-1 betyder det alltså att samplingsfrekvensen är
44.1 kHz, vilket är den vanligaste frekvensen för de låtar man brukar kunna
ladda ner från nätet. För professionella tillämpningar räcker detta inte och de
flesta ljudkort och musikprogram avsett för studios klarar 96 kHz eller t o m
högre frekvenser.
Mode:
Dessa bitar talar om ifall det är mono eller stereo. Som vanligt krävs en tabell:
Värde Mode
0 0
Stereo
0 1
Joint stereo
1 0
Dual channel
1 1
Mono
I exemplet är värdet: 0 1 och det betyder alltså ”joint stereo”. Jag är i mitt
program just nu inte intresserad av denna information men det kan vara
intressant vid mixning och det är möjligt att jag tar med det i en senare version.
Lösning (h2), klasser för att hantera mp3 header
Jag bedömde inte att det skulle behövas speciellt många funktioner eftersom
bithantering och sådant redan var gjort för ID3 v2. Jag definierade därför endast
en klass, mp3header, med ansvar för att läsa in informationen, omvandla till
användbara värden och lagra dessa. Jag insåg att det skulle behövas en del
hjälpfunktioner och datastrukturer för t ex de olika omvandlingstabellerna, men
jag kunde på det här stadiet inte riktigt förutse hur de skulle se ut.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 39 av 52
Lösning (h3), funktioner för att hantera inläsningen av headern
Det behövdes en funktion, readHeader(), med ansvar för att läsa de olika
bitfälten, omvandla dessa till användbara värden med hjälp av tabellerna ovan
och sedan lagra värdena. Programsekvensen är enkel:
- läs in hela headern på en gång i en byte vektor
- läs in bitfälten
- omvandla till de värden vi är intresserade av
- lagra värdena i objektet
Utmaningen är att hitta ett bra sätt att översätta bitarna till motsvarande värden.
Eftersom tabeller används i standarden kan det vara lämpligt att använda tabeller
även i programmet. Detta kan bli en bra lösning eftersom de flesta bitfälten
binärt bildar värdena 0, 1, 2 o s v och därför kan användas som index i t ex
vektorer. Det finns säkert många sätt att lösa detta men i readHeader() är det
gjort på följande sätt för alla värdena:
- maska bort alla ointressanta bitar
- skifta de bitar som finns kvar längst till höger
- använd det tal som blir resultatet som index i en vektor som innehåller det
värde vi letar efter.
För alla typer av data jag söker efter finns en vektor motsvarande beskrivningen
ovan. Som exempel kan man ta bithastigheten. Som visas ovan finns
bithastigheten i bit 3 och 4 av den tredje byten i mp3 headern. Den del av
readHeader() som filtrerar ut denna ser ut på följande sätt, där headerBuffer[2]
alltså är den tredje byten räknat från index 0:
…
private final int[] SampleRates ={44100, 48000, 32000, 44100};
…
bitMask = 0x0c; // 0000 1100
int sampleRateIndex = (bitMask & headerBuffer[2]) >>> 2;
sampleRate = SampleRates[sampleRateIndex];
Först maskas alltså alla bitar utom de två bort med operatorn ’&’, sedan skiftas
bitarna två positioner så att de hamnar längst till höger. På så sätt får man ett tal
mellan 0 och 3. Denna används sedan för att hitta rätt värde i den fördefinierade
(d v s konstanta) vektorn SampleRates.
Lösning (h4), beräkning av spellängd
Från början när jag bara kände till de två typerna av ID3 header tyckte jag att det
var underligt att det inte fanns något fält för låtarnas spellängd. I alla mp3
spelare visas spellängden så det var uppenbart att det fanns ett sätt att ta reda på
det. När jag sedan upptäckte mp3 headern trodde jag att det var där man skulle
kunna hitta spellängden men de beskrivningar jag hittat (se ovan) nämner inget
stöd för det, och jag har heller inte hittat något program på nätet.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 40 av 52
Det enda jag kunde komma på var att mp3 spelarna måste räkna ut spellängden
på något sätt, och om man tittar närmare på mp3 headern kan man faktiskt göra
en ungefärlig beräkning med den information som finns där. Algoritmen finns i
den interna funktionen mp3header.setPlayLength() och fungerar som:
Om s = filens storlek i kbit, och v = bithastigheten (bitrate)
i kbit/s, så blir den ungefärliga spellängden i sekunder: l =
s/v. Före divisionen omvandlas filens storlek från byte till
kbit genom att dividera med (1024*8), eftersom det går 1024
byte på en kbyte och 8 bitar på en byte.
Tester visar att resultatet stämmer ganska bra med det som visas i mp3 spelarna.
Det slog från början ett antal sekunder (min beräkning visade ibland någon
sekunder längre speltid). Men när jag i en senare version justerade filstorleken
genom att dra ifrån storleken för ID3 v1 och v2 header stämde det bättre. Det är
v2 headern som har betydelse eftersom den kan vara ganska stor i vissa fall.
Lösning (h5), testprogram för mp3 header
Dessa tester gjordes på samma sätt, d v s med testfiler och utskrifter inifrån Driver() och
readHeader().
Delproblem (i), Användargränssnitt
Lösning: Detta problem kan göras hur stort som helst. Från början hade jag
tänkte göra ett avancerat användargränssnitt men det hade krävt för mycket tid.
Jag insåg tidigt att jag skulle få begränsa mig till något som var relativt enkelt.
Problemet kan delas upp i följande delar:
1. Bestäm hur man skall arbeta med systemet.
2. Bygg menyer, knappar och annat som behövs för att det skall fungera som
det är tänkt.
3. Testa att allt fungerar.
Lösning (i1), arbetssätt
Användaren skall kunna göra följande i den första versionen:
- Välja vilken hårddisk programmet skall arbeta med. Helst skall man
kunna välja flera.
- Starta sökningar som letar upp alla mp3 filer på den eller de hårddiskar
som valts.
- Läsa taggar och mp3 header från de mp3 filer som hittats.
- Visa informationen i ett fönster på skärmen
- Skriva informationen till en textfil
I en kommande version bör man även kunna göra en HTML fil.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 41 av 52
Lösning (i2), Menyer och knappar
Jag använde Javas Swing bibliotek för att göra följande, där den mesta koden
finns i klassen GUIDriver:
Huvudmeny
Användaren skall ha ett huvudfönster med en vanlig meny. Den skall se ut som
det brukar i Windows, d v s med ”File” och ”Edit”.
Denna funktion byggdes med bland annat komponenterna JFrame (ett
huvudfönster) och en JMenuBar som är menyobjekt som kopplas till en JFrame.
Varje funktion som skall kunna väljas har en motsvarande JMenu.
Koden ser ut på följande sätt:
frame = new JFrame("MP3 File properties");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Create a menu bar
JMenuBar menuBar = new JMenuBar();
// File Menu, F
JMenu fileMenu = new JMenu(FILE_ACTION);
fileMenu.setMnemonic(KeyEvent.VK_F);
menuBar.add(fileMenu);
För att man skall kunna veta vilka val användaren gör måste man bygga
funktioner som kan hantera händelser. Varje gränssnittsobjekt kan ha ett antal
hanterare av olika slag. Man kopplar hanterarna till knappar, menyer eller annat
objekt. Exempel på en sådan hanteringsfunktion:
static class MenuActionListener implements ActionListener {
public void actionPerformed(ActionEvent actionEvent) {
if(SCAN_ACTION.equals(actionEvent.getActionCommand())) {
scanDrives();
}
if (..) {
...
}
Menyn File  Scan
När användaren väljer denna funktion skall programmet läsa in vilka mp3 filer
som finns på den eller de hårddiskar som valts. Funktionen anropar ScanDrives
som kontrollerar vilka hårddiskar som är valda och sedan anropar
hjälpfunktionen theManager.getFileList() i tur och ordning för de valda
diskarna. Funktionen läser in hela filträdet och lagrar alla filer den hittar i en
vektor (datatypen Vector) som sedan kan användas av de andra funktionerna.
Fördelen med att använda en vektor är att storleken kan växa efter hand.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 42 av 52
Menyn File  Output
Med denna funktion kan användaren välja placering och namn på den fil som
programmet skapar. Funktionen använder sig av Javabibliotekets JFileChooser
som är i stort sett färdig att användas.
Menyn File  Save
Funktionen går igenom listan på alla filer som tidigare lästs in med Scan
funktionen och anropar för varje fil funktionen doOneFile() som läser in v1
tagg, v2 tagg och mp3 header för varje fil och skapar ett mp3file objekt för varje.
Dessa skrivs sedan till disk genom att funktionen anropar mp3file.
streamToFile(). Den fil som används är den som användaren valt. Om ingen
valts används en standardfil C:\tmp\mp3collection.txt.
Funktionen streamToFile() formatterar inte informationen utan skriver rad för
rad med en rubrik före varje datatyp.
Menyn File  Display
Funktionen gör motsvarande som Save, fast informationen visas på bildskärmen.
För detta används en tabell av typen JTable som sedan kopplas till ett fönster.
För att man skall kunna bläddra om tabellen blir större än fönstret krävs ett
fönsterobjekt av typen JScrollPane. Funktionen löper igenom alla tidigare
inlästa filnamn, anropar doOneFile() precis som vid skrivning till disk. Därefter
kopieras informationen till tabellen som sedan knyts till fönstret enligt:
while (..) {
source = (File)fileIterator.next();
file = new mp3file(source.getPath());
doOneFile(file);
rowData[no] [0] = file.getName();
rowData[no] [1] = file.getArtist();
…
}
JTable fileTable = new JTable(rowData, columnNames);
fileTable.setAlignmentY(JTable.CENTER_ALIGNMENT);
JScrollPane tableScrollPane = new JScrollPane(fileTable);
contentPane.add(tableScrollPane, BorderLayout.CENTER);
Menyn Save  Exit
Man kan lämna programmet på två sätt, antingen genom att välja Exit i menyn
eller genom att stänga fönstret med den vanliga kryss-knappen. Från menyn
lämnar man programmet med systemanropet exit(). Motsvarande görs
automatiskt av fönsterhanteraren om fönstret skapats med valet
EXIT_ON_CLOSE.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 43 av 52
Menyn Edit  Drives
Denna meny används för att användaren skall kunna välja vilken eller vilka
hårddiskar som skall sökas igenom. Vid uppstarten av menysystemet anropas
GUIDriver.InitFileSearch(). Denna förbereder genom att kontrollera vilka
hårddiskar som finns installerade. För detta används systemfunktionen
getRoots().
För att kunna välja hårddiskar används kryssrutor, vilket i Java heter JCheckbox.
För att kunna använda dem måste de knytas till ett fönster, i detta fall av ”popup” typ som sedan kan stängas. JDialog är den vanligaste typen av sådant
fönster. Så här ser koden ut för att skapa dialogen där det finns lika många
kryssrutor som diskar (även disketter och CD fungerar):
JDialog itemDialog = new JDialog(frame, "Choose Drives");
itemDialog.setBounds(100, 200, 200, 200);
ActionListener DriveBoxListener = new
DriveBoxActionListener();
contentPane.setLayout(new GridLayout(0, 1));
for (int i = 0; i < drives.length; i++) {
driveBoxes[i] = new JCheckBox("Drive " + drives[i],
includeDrives[i]);
contentPane.add(driveBoxes[i]);
driveBoxes[i].addActionListener(DriveBoxListener);
}
Precis som för menyer så krävs något som tar hand om de händelser som kan
uppstå. I detta fallet är det att användaren väljer eller väljer bort en disk.
Hanteringsfunktionen här heter DriveBoxListener och den ändrar status från
”true” till ”false” eller tvärtom för den hårddisk som är kopplad till rutan.
Menyn Edit  Items
Denna funktion är inte aktiv ännu men tanken är att användaren skall kunna
välja vilka datafält som skall vara med på listor och bildskärm. Detta kommer att
hanteras med kryssrutor precis som vid val av hårddiskar.
Kommentar:
Det kan se enkelt ut att göra användargränssnitt eftersom så mycket verkar
färdigt. Men det är i praktiken svårare än att göra kod för t ex bithantering. När
man jobbar med egen kod eller kortare program som bara använder sig av
”primitiva” funktioner är det lätt att göra fel men också ganska lätt att testa. När
man jobbar med användargränssnitt har man ingen kontroll utan måste lita på att
biblioteksfunktionerna fungerar som det är tänkt. När de då inte gör det (det
händer nästan alltid) finns det egentligen inget bra sätt att testa, utan man får
experimentera för att försöka hitta en lösning där man kan gå runt problemet.
Det kan vara otroligt tidskrävande.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 44 av 52
Jag upptäckte att de beskrivningar av gränssnittsobjekten som finns på nätet inte
alltid stämmer eftersom det finns ett antal versioner av Java. Därför stämmer det
inte heller med läroböckerna och detta kan ta mycket tid att reda ut. Och ibland
går det helt enkelt inte att få saker att fungera. Vissa problem kan ta en kväll
eller mer.
En annan svårighet är att de olika klasserna hänger ihop på ett ibland mycket
komplicerat sätt. Man måste skapa objekten i en viss ordning, knyta dem till
varandra i en viss ordning etc, som man måste experimentera fram.
Exempel på hur användargränssnittet ser ut finns i bilaga 1 sist i rapporten.
Delproblem (j), felhantering
Många programmerare citerar ”Murphys lag” som säger ”allt som kan gå fel
kommer att gå fel”. Även om man försöker testa allt kommer det förmodligen att
finnas fel kvar. Några saker är viktiga för att programmet skall vara stabilt:
- Undantagshantering
- Egna kontroller
- Tester
Java verkar ha en bra felhantering. När saker går snett är det sällan som datorn
hänger sig. Istället får man oftast en s k ”Exception”. På svenska brukar det
kallas undantag. När programmet hoppar ur med en exception får man för det
mesta en utskrift som talar om var felet uppstod.
När man använder sig av systemanrop, t ex i Java.System eller Java.File, så kan
de ”kasta” undantag av olika slag för att signalera att det gick fel. Kompilatorn
kräver att man antingen tar hand om de felen eller talar om att man inte tar hand
om det. Om man vill kunna fånga felet måste man bygga in koden i ett
”fångstnät” som ser ut enligt:
try {
// Kod som kan ge fel
}
catch (Exception ETT_UNDANTAG) {
// Kod som hanterar felet
}
Om felet uppstår hoppar programmet ur den del som finns inom de första
parentesen och kör den kod som finns i ”catch” delen.
Om man inte vill bygga en felhantering måste man i klassens deklaration skriva
”throws ..” för att markera att klassen kan generera sådana fel.
Man kan definiera egna undantag också men det har jag inte gett mig in på i
detta projekt.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 45 av 52
Jag har lärt mig att man i princip inte kan testa för mycket. Men den viktigaste
lärdomen är nog att man skall arbeta i små steg med tester mellan. Om man gör
en massa ny kod innan man testar och det blir underliga fel så kan de vara svåra
att hitta. Men om man testar oftare när man gjort mindre tillägg så vet man ju
precis var felet måste vara. De svårare fel som uppstod i programmet var att
vissa filer var helt felaktiga och därför gav helt tokiga uppgifter om storlekar på
header o s v. Jag hade heller inte känt till att det i min dator fanns massor av
mycket små mp3-filer som används som ljudeffekter av olika program, och för
dessa verkar inget stämma i ens den mp3 headern som skall vara standard.
Framtida förbättringar
Förutom en del funktioner som t ex sortering så skulle det vara en klar
förbättring att använda flera ”trådar”. I Java fungerar trådar som en delprocess
som kan köra självständigt medan datorn utför andra uppgifter.
I det nuvarande programmet är allting en enda process och det är inte alltid så
lyckat. Om hårddisken är stor med många filer tar kommandot ”Scan” en del tid.
Under den tiden ”fryser” datorn menyn så att man inte kommer in förrän
kommandot är klart. Det vore också trevligt med något slags status som visar att
datorn jobbar. I Javabiblioteket finns en ”progress indicator” som verkar smidig
men det kräver att man kör flera parallella trådar.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 46 av 52
Slutkommentar
När man tittar tillbaka på arbetet känns det som att jag egentligen gjort flera
arbeten på en gång:
1. Rapport om digitalt ljud och filformat för digital musik
2. Programmeringsprojekt
3. Rapport med kommentarer om programmeringsprojektet, metodik mm.
Lärdomar:
 Det är svårt att bedöma hur lång tid saker tar. Det verkar som om allt tar
minst dubbelt så lång tid som man trodde från början, speciellt om det är
första gången man arbetar med något. Jag borde ha haft en mindre
ambitionsnivå från början men det var inte lätt att inse det eftersom jag
inte hade någon erfarenhet. Jag hade heller inga synpunkter från någon
handledare att utgå från eftersom jag inte hade någon handledning under
den första halvan av arbetet.
 Det är svårt att sätta gränser för omfattningen. Efter hand som tiden går är
det lätt att arbetet sväller ut. Om jag skulle göra något liknande igen
skulle jag försöka göra något ”minimalistiskt” från början och hellre
utvidga arbetet efter hand om det finns tid.
 P g a missförstånd lade jag ner för mycket tid på beskrivande text om
digitalt ljud. Jag ville göra handledaren nöjd och tyckte själv att det var
intressant. Men handledaren tyckte inte att det var så viktigt för arbetet.
Resultatet blev att jag fick problem på slutet eftersom min handledare bad
mig att komplettera med beskrivningar av själva programmet. Jag hade
tänkt göra detta men kanske inte så omfattande som handledaren
förväntade sig. Jag fick därför ägna en hel del fritid under min
utlandspraktik med att göra kompletterande beskrivningar. Det man kan
lära sig av detta är att det hade varit bättre att träffa handledaren mycket
oftare. Och det hade antagligen varit bra att skicka exempel på olika delar
i rapporten mycket tidigare för att få ”feedback”.
 Jag upptäckt också att det är mycket svårare än jag trodde att
programmera. Eller rättare sagt, det är svårare att programmera ”på
riktigt”. Om jag inte hade fått en del hjälp därhemma när jag körde fast,
och om jag inte hade kunnat hitta en del exempel på nätet att utgå från och
lära mig av hade jag aldrig hunnit få fram något körbart under projektet.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 47 av 52
Detta var mitt första riktiga program och det tagit väldigt mycket tid. Jag har
uppskattningsvis lagt ner minst 200-250 timmar hittills. Mer än halva tiden har
gått åt till att leta efter information och experimentera med olika filformat. Det
tog också tid att få ihop en utvecklingsmiljö och lära sig hur den fungerade.
Jag har lärt mig väldigt mycket om både ljud, datorer och programmering. Jag
tycker att Java fungerar mycket bra och vill gärna försöka lära mig mer. Jag har
också fått mer respekt för svårigheterna med programmering. Det krävs både
uthållighet, noggrannhet och logiskt tänkande.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 48 av 52
Referenser
Internet
http://www.ueberdosis.de/java/id3.html
http://wmcfproductions.com/mp3s.htm
http://www.flashguru.co.uk/000100.php
http://sourceforge.net/projects/jd3lib/
http://www.javazoom.net/javalayer/javalayer.html
http://www.javazoom.net/javalayer/javalayerme.html
http://www.programmersheaven.com/articles/userarticles/mark/tut3/tu
t3_1.htm
http://www.programmersheaven.com/zone29/cat1044/index.htm
http://www.programmersheaven.com/c/MsgBoard/wwwboard.asp?Bo
ard=7&src=28&Setting=
http://media.uis.edu/classes/com541f01/leepper/research2/mpeg.pdf
http://www.mpeg.org/MPEG/index.html
http://www.doc.ic.ac.uk/~nd/surprise_96/journal/vol3/sab/test.html
http://www.sp303.com/mp3.htm
http://www.diee.unica.it/pv2000/proceedings/papers/1.pdf
http://media.uis.edu/classes/com541f01/leepper/research2/mpeg.pdf
http://java.sun.com/docs/books/tutorial/uiswing/mini/index.html
http://developer.java.sun.com/developer/onlineTraining/Programming
/BasicJava1/front.html#swing
Böcker
- Börja med Java 2, Ivor Horton, Pagina Förlags AB 2001
- Definitive Guide to Swing for Java 2, John Zukowski, APress 2002
Programvara
Together Controlcenter v 6.0 från Togethersoft (sedan januari 2003 ägt av
Borland Inc.)
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 49 av 52
Exempel på användargränssnitt
Grundfönster med huvudmeny
Här syns valmöjligheterna i menyn File.
Fönster för att välja hårddisk
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 50 av 52
Fönster för att välja fil för utdata
Fönster för att visa utdata i tabell
Här visas fönstret komprimerat. Det kan expanderas. Man ser att viss
information saknas ibland. Ljudeffekten ”Heartbeats” saknar det mesta.
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 51 av 52
Exempel på utskrift till fil
När användaren väljer att skriva information till fil skrivs just nu all information.
I en kommande version kanske man bör kunna välja vad som skall ”dumpas”,
och det gäller speciellt om man gör en HTML fil för att visa på skärm.
Just nu ser det ut t ex så här:
FileName: Linkin Park - Lying From You.mp3
Song Title: Lying From You
Artist: Linkin Park
Album: Meteora
Year:2003
Comment:
track: 1
Genre: Not set
BPM:
Content:
Composer:
Length(s): 170
Sample rate: 44100
BitRate: 192
FileName: Summertime & Surinder Shinda.mp3
Song Title: raat da na bole (summertime re
Artist: Panjabi MC \raat da na bole
Album: Grassroots
Year: 1996
Comment:
track: 3
Genre: Jungle
BPM:
Content:
Composer:
Length(s): 303
Sample rate: 44100
BitRate: 128
Digitalt ljud och programmering i Java - Christoffer Weidmar -
sida 52 av 52