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