__________________________________________________________________________________________ Sound-Programmierung in Java -1- Einführung Dieses Tutorial soll eine kurze Einführung in die Möglichkeiten darstellen, die die Sprache Java bietet, um mit ihr Sounds, bzw Musik wiederzugeben, oder auch eigenständig aufnehmen zu können. Das Tutorial ist in drei Teile gegliedert. Teil I und II beschäftigen sich ausschliesslich mit der Java Sound API V0.99, die als Teil des Java Media Frameworks (JMF) vertrieben wird. Teil III bietet darüber hinaus eine knappe Einführung in das Java Media Framework. Zum Zeitpunkt der Textbearbeitung, befand sich die Java Sound API schon in der Version 1.0, die fester Bestandteil des Java SDK 1.3 RC1 ist. Nach Angaben von Sun sollen aber keine gravierenden Änderungen zwischen der Version 0.99 und 1.0 stattgefunden haben; daher sind die Aussagen, die hier getroffen werden auch für die aktuellere Version gültig. Das Anliegen dieses Tutorials ist nicht, die gesamte API, bzw. das gesamte JMF mit all ihren Klassen und Methoden zu beschreiben, sondern es soll dem Leser als Erleichterung dienen, seine Programme um die Möglichkeiten der Java Sound API bzw. des JMF zu erweitern. Zu nennen wäre hier beispielsweise das Abspielen von Sounddateien in Programmen (z.B. die korrekte Aussprache einer Vokabel in einem Vokabeltrainer) oder aber auch die Möglichkeit, eigene Sounddateien zu kreieren. Für eine komplette Übersicht über alle Klassen und Methoden der Java Sound API und des Java Media Framework sei der Leser auf die Java Sound Page von Sun [1] verwiesen. Das Tutorial ist so angelegt, neben unausweichlichen Theorieaspekten der Soundgenerierung, ohne die man die Klassengliederung der API und des JMF nicht nachvollziehen kann, vor allem an möglichst einfach gehaltenen Beispielprogrammen das Einbinden von Sound in die eigenen Programme zu zeigen. Hierfür gibt es die sogenannten „Wie kann man ...“ - Abschnitte im Text, die sich einfachen Fragestellungen widmen (zum Beispiel Wie kann man ... Sounddateien wiedergeben, die sich auf der Festplatte befinden?) und dem Leser hoffentlich bei Standardproblemen bzw. fragen helfen. Man kann diese Abschnitte also als eine Sammlung von „Kochrezepten“ verstehen, die bei auftretenden Problemen helfen sollen. Im Text wird auch außerdem auf Probleme mit der API hingewiesen, die uns beim Verfassen des Textes aufgefallen sind. Einige Klassen befinden sich noch in einem - nach unserer Meinung - nicht ganz ausgereiftem Stadium und erfüllen nicht alle Aufgaben so, wie sie von Sun in der Dokumentation spezifiziert wurden. __________________________________________________________________________________________ Sound-Programmierung in Java -2- Die Java Sound API gliedert sich in die vier Unterpakete: javax.sound.sampled javax.sound.sampled.spi javax.sound.midi javax.sound.midi.spi In diesem Text werden nur die beiden Pakete javax.sound.sampled , javax.sound.midi näher beschrieben. Die beiden anderen Unterpakete und die darin enthaltenen Klassen, sind für Hersteller gedacht, um mit ihnen – z.B. eigene Audiokomprimierungsverfahren - einfach ihre Erweiterungen in die Java Sound API einbinden zu können. Wer nähere Informationen zu diesen beiden Paketen benötigt, findet eine kurze Einführung in der offiziellen Sun-Dokumentation zur Sound API, die man unter [1] finden kann. Teil I dieses Tutorials widmet sich dem Paket javax.sound.sampled und den dazu passenden Problem- und Fragestellungen. Teil II hingegen beschäftigt sich mit dem javax.sound.midi - Paket und dessen Anwendung in der Praxis. Teil III bespricht die audiophilen Aspekte des Java Media Framework. __________________________________________________________________________________________ Sound-Programmierung in Java -3- TEIL I javax.sound.sampled __________________________________________________________________________________________ Sound-Programmierung in Java -4- Kapitel 1 - Einführung in Sampled Audio Das Paket javax.sound.sampled richtet seinen Hauptaugenmerk auf den Transport von AudioDaten, d.h. in erster Linie auf das Abspielen und Aufnehmen von digitalem Sound. Die wichtigste Aufgabe ist hierbei, wie die AudioBytes in das System hinein- und wieder heraus transportiert werden. Die Java Sound API kann den Audio-Datentransport auf zwei verschiedene Arten bewerkstelligen. Es gibt auf der einen Seite den gepufferten und gestreamten Weg und auf der anderen den in-memory ungepufferten. Der erste bietet sich bei kleinen Sounddateien an (um auf unser Vokabeltrainerbeispiel zurückzukommen wären dies die Vokabel-Sounddateien), der zweite bei sehr grossen Dateien (zum Beispiel bei gegrabbten Audio-Tracks einer CD). Beide Ansätze werden genauer im Kapitel 2 - Abspielen von Audio-Files besprochen. Kommen wir jetzt zu den grundlegenden Eigenschaften der API. Was benötigt man um Soundfiles mit der API abspielen zu können ? Um Sound wiederzugeben oder aufzunehmen werden 3 wichtige Dinge benötigt: in einem bestimmten Format vorliegende Audio-Daten (z.B. den Audio-Track einer CD als Wave Datei), einen sogenannten Mixer und eine Line. Was sind formatierte Audio-Daten ? Als formatierte Audio-Daten werden Sounds bezeichnet, die in einem Standard-Sounddateiformat vorliegen; dazu zählen beispielsweise WAV - Dateien oder AU - Dateien. Die Java Sound API unterscheidet zwischen Datenformaten und Fileformaten. Das Datenformat einer Sounddatei bestimmt, wie das Programm, das die Sounddatei bzw. dessen Audiodaten wiedergibt, diese interpretieren soll. Repräsentiert werden diese Informationen z.B. durch die Sample-Rate (d.h. wie viele Samples der Original-Audiodaten pro Sekunde gespeichert werden) oder durch die Anzahl der Kanäle (d.h. 1 Kanal = Mono-, 2 Kanäle = Stereoqualität usw.). Das Datenformat einer Sounddatei wird in der Java Sound API durch die Klasse AudioFormat spezifiziert. Das Fileformat einer Sounddatei bestimmt die innere Struktur einer Audiodatei, z.B. die Angabe in welchem Audioformat die Sounddatei vorliegt. __________________________________________________________________________________________ Sound-Programmierung in Java -5- Standard-Audiofileformate gibt es einige. Zu nennen wären hier WAVE (die Windows .wav-Dateien), AIFF, oder AU. Diese verschiedenen Formate besitzen alle eine unterschiedliche interne Strukturen die die Informationen über die Sounddatei widerspiegeln. In der Java Sound API wird das Fileformat durch die Klasse AudioFileFormat spezifiziert. Sie enthält Informationen über das Audiofileformat der verknüpften Sounddatei, deren Länge in Bytes usw. Was ist ein Mixer ? In der Java Sound API werden Audiogeräte, die sich in oder an der Maschine befinden, durch die sogenannte Mixer Klasse repräsentiert. Der Anspruch dieser Klasse liegt darin, die AudioIn - und Outputströme in das jeweilige Gerät zu koordinieren oder zu beeinflussen (beispielsweise dieLaustärke bei der Ausgabe); der Mixer fungiert also in ähnlicher Weise wie ein Mischpult. In der Java Sound API sind den Eingängen und Ausgängen einer Soundkarte keine eigenen Mixer zugeordnet, sondern sogenannte Ports die in der Klasse Port zusammengefaßt werden. Diese Ports (z.B. der Mikrofoneingang an der Soundkarte) werden durch den Mixer gesteuert. Da in den hier besprochenen Standardproblemen die Mixer nicht benötigt werden, finden sie in diesem Tutorial auch keine weitere Beachtung und sollen an dieser Stelle nur der Vollständigkeit halber erwähnt werden. Wichtig sind die Mixerobjekte erst dann, wenn ein anderes installiertes Gerät als die Standardsoundkarte zur Sounderzeugung bzw. –aufnahme benutzt wird. Anzumerken ist hier, daß sich die Portobjekte in der Version 0.99 (und nach den Angaben in der Java Sound Mailingliste[4] auch in der Version 1.0) noch in nicht nutzbarem Stadium befinden. Es ist nicht möglich, etwa den Mikrofoneingang und dessen Einstellungen über ein Portobjekt zu beeinflussen oder ihn auf stumm zu schalten. Was ist eine Line ? Als Line wird in der Java Sound API der Pfad beschrieben, auf dem Audiodaten in das System hineingelangen oder hinausgelangen. Ein Beispiel für den Pfad in das System wäre das Mikrofon, ein Pfad aus dem System heraus die angeschlossenen Lautsprecher. Java Sound API – Objekte, die als Line bezeichnet werden, sind Ports, TargetDataLine, SourceDataLine und Clip. Kommen wir nun nach den theoretischen Grundlagen der API zur Anwendung dieser Klassen und damit zunächst zur Soundgenerierung. __________________________________________________________________________________________ Sound-Programmierung in Java -6- Kapitel 2 - Die Soundgenerierung Die wichtigste Klasse in dem Paket javax.sound.sampled ist die statische Klasse AudioSystem. Mit Hilfe dieser Klasse hat man Zugriff auf die installierten Audiogeräte in einem System. Dies bedeutet im einzelnen, daß mit der Klasse AudioSystem : - Mixer - Objekte erzeugt kann mit denen man auf bestimmte Geräte im System speziell zugreifen kann - das man Lineobjekte erzeugen kann um den Strom von Audiodaten in das System herein und wieder heraus zu ermöglichen - das man mit ihrer Hilfe Audiodaten konvertieren kann zwischen verschiedenen Audiodatenformaten - streams zu Audiodaten geöffnet werden, um diese zu lesen oder zu schreiben. Wichtigster Punkt ist aber, daß man mit ihr direkt auf die Soundkarte zugreifen kann ohne, vorher umständlich ein Mixer - Objekte anzulegen und dann über diese die Soundkarte und die Soundgenerierung zu steuern. Da die Klasse AudioSystem für unseren Ansatz vollkommen ausreicht, werden in diesem Tutorial keine Mixer – Objekte benutzt. Bevor wir mit dem Generieren von Sound beginnen, müssen wir noch auf die verschiedenen Sicherheitsaspekte hinweisen, wenn der Java Security Manager im Einsatz ist. Folgende Zugriffe sind per default erlaubt und sollten beim Programmieren mit der API berücksichtigt werden: Applets Soundwiedergabe möglich, Soundaufnahme nicht möglich Applikationen, die mit keinem security manager laufen, können Sound sowohl aufnehmen als auch wiedergeben Applikationen, die mit dem default security manager laufen, können Sound wiedergeben, aber keinen Sound aufnehmen Beim Generieren von Sound kommen wir zunächst wieder zum schon besprochenen Lineobjekt zurück. Wir haben zwei Ansatzmöglichkeiten den Sound mit der API wiederzugeben. Zum Einen können Audio-files vor dem Abspielen komplett in den Arbeitsspeicher des Rechners geladen werden (was __________________________________________________________________________________________ Sound-Programmierung in Java -7- nur bei sehr kleinen Dateien zu empfehlen ist), zum anderen können jeweils nur kleine Teile des Audiofiles in den Speicher geladen und dann abgespielt werden. Dies spiegelt sich in den Klassen wider, die zum Abspielen benutzt werden können: die Clip Klasse, die den ersten Ansatz verfolgt, und die SourceDataLine Klasse, die den zweiten Ansatz realisiert. Beide werden zu den Lineobjekten gezählt. Wie kann man ... Audiofiles als Clip abspielen ? Erster Schritt beim Abspielen einer Sounddatei mit Hilfe der Clip-Klasse sollte sein, ein File Objekt zu erzeugen, das auf die wiederzugebende Sounddatei verweist. File file = new File("ding.wav"); In unserem Beispiel soll eine Wave-Datei mit dem Namen „ding.wav“ wiedergegeben werden. Als nächsten Schritt müssen wir von dem Java Sound System einen sogenannten AudioInputStream anfordern, der später die Daten der Sounddatei für uns in den Speicher des Rechners liest. AudioInputStream stream = AudioSystem.getAudioInputStream(file); Als Übergabeparameter geben wir der getAudioInputStream - Methode das File - Objekt. Danach benötigt man ein Line - Objekt, das den Eingang für das Java Sound System spezifiziert. D.h. welche Eingangsquelle das Java Sound System bei der Soundgenerierung benutzen soll. In unserem Fall, handelt es sich um ein Clip - Objekt, da wir die Audiodaten vor der Wiedergabe komplett in den Hauptspeicher lesen und von dort aus die Wiedergabe durchführen wollen. Um an dieses Clip Objekt über das Java Audiosystem zu gelangen, muss im voraus ein sogenanntes LineInfo - Objekt erzeugt werden. Dieses definiert dem Java AudioSystem welche Art von Dateneingangsquelle wir bei der Wiedergabe unseres Sound benutzen wollen. Ein solches Objekt (in der Java Sound API repräsentiert durch die Line.info Klasse) kann aber nicht direkt erzeugt werden, sondern nur ein Objekt seiner Unterklassen Port.info oder DataLine.info. Da die Port - Objekte in der aktuellen Version des Paketes noch nicht vollständig implementiert sind, werden hier nur die DataLine - Objekte (Clip, SourceDataLine und TargetDataLine) benutzt und besprochen. Wir werden also nun ein DataLine - Objekt erzeugen: DataLine.Info info = newDataLine.Info(Clip.class,stream.getFormat()); Als Parameter erwartet der Konstruktor den Klassentyp, der zur Wiedergabe benutzt werden soll (also Clip - Klasse oder SourceDataLine - Klasse) und das Audiodatenformat der Sounddatei, die __________________________________________________________________________________________ Sound-Programmierung in Java -8- abgespielt werden soll. Das Format unserer Datei bekommen wir einfach, indem wir die AudioInputStream - Methode getFormat() benutzen. Nachdem wir diese Vorarbeiten geleistet haben, fordern wir unser Clip - Objekt von dem Soundsystem an. Hierzu wird dem AudioSystem die vorbereitete DataLine.info Klasse übergeben: Clip clip = (Clip) AudioSystem.getLine(info); Als nächstes müssen wir für unsere Applikation dieses Clip – Objekt reservieren, damit kein anderes Programm dieses Objekt währendessen nutzen kann. Dies geschieht durch eine open - Anweisung: clip.open(stream); Der Anweisung muß der AudioInputStream übergeben werden, der mit der wiederzugebenden AudioDatei verknüpft ist. Nachdem der Clip geöffnet und so für unsere Anwendung reserviert wurde, können wir die Wiedergabe starten. Dies geschieht, indem wir eine start - Anweisung benutzen: clip.start(); Nun wird der Clip bzw. unsere Sounddatei wiedergegeben. Um die Wiedergabe zu unterbrechen genügt eine einfache stop - Anweisung: clip.stop(); Gibt man nun wieder eine start - Anweisung, wird die Wiedergabe exakt an der Stelle fortgesetzt, an der vorher die stop - Anweisung gegeben wurde. Ist die Wiedergabe des Clips beendet und man der Clip bzw. die Datei soll nicht noch einmal wiedergeben werden, gibt man durch eine close - Anweisung alle gebundenen Resourcen wieder frei: clip.close(); Dies sind die typischen Schritte bei der Wiedergabe einer Sounddatei über den Clip - Ansatz. Hier nun ein komplettes Beispielprogramm PlaySound.java : import javax.sound.sampled.*; import java.io.*; class PlaySound { static File file = null; static AudioInputStream stream = null; static Clip clip = null; public static void main (String args[]) { System.out.println("Spiele wav ..."); //bestimme welche Sounddatei abgespielt werden soll __________________________________________________________________________________________ Sound-Programmierung in Java -9- file = new File("ding.wav"); //versuche EingabeStream auf die abzuspielende Sounddatei zu //bekommen try { stream = AudioSystem.getAudioInputStream(file); }catch(UnsupportedAudioFileExceptione){System.out.println("Kein unterstuetztes AudioFormat!");System.exit(0);} catch(IOException e2){System.out.println("Fehler beim Oeffnen der Sounddatei!");System.exit(1);} //erzeuge LineObjekt das spezifiziert welche Line vom AudioSystem zur //Wiedergabe benutzt werden soll DataLine.Info info = new DataLine.Inf(Clip.class,stream.getFormat()); //versuche gewuenschte Eingangs-Line beim AudioSystem anzumelden try{ clip = (Clip) AudioSystem.getLine(info); }catch(LineUnavailableException e){System.out.println("Line konnte nicht benutzt werden");System.exit(1);} //oeffne das Clipobjekt und reserviere es somit fuer diese //Applikation try{ clip.open(stream); }catch(LineUnavailableException e){System.out.println("Fehler beim Öffnen des AudioStreams");System.exit(1);} catch(IOException e){} //beginne die Wiedergabe clip.start(); //warte solange wie das AudioSystem den Clip wiedergibt while(clip.isActive()){} //stoppe Clip nachdem er komplett wiedergegeben wurde clip.stop(); //schliesse den Clip und gib somit die gebundenen Resourcen wieder //frei clip.close(); //beende ordnungsgemaess das Programm System.exit(0); } } __________________________________________________________________________________________ Sound-Programmierung in Java - 10 - Dieses Programm spielt eine Wave - Datei in der vorher besprochenen Weise ab. Hinzugekommen ist dabei noch die Clip - Methode [Clip].isActive(). Diese gibt ein false zurück, wenn die Wiedergabe des Soundclips gestoppt wurde, entweder weil das Ende der Audiodatei erreicht wurde, oder weil eine stop - Anweisung vorher erfolgt ist. An dieser Stelle wird gut sichtbar, welche Methoden Java Exceptions auslösen können und um welche es sich dabei handelt. Für eine komplette Übersicht der bisher behandelten Methoden, die Exceptions auslösen können, sei auf das Ende dieses Kapitels verwiesen. Weitere Clip – Methoden, die hier nicht weiter besprochen werden, aber in manchen Anwendungen hilfreich sein können, sind: void loop(int count) - startet die Wiedergabe eines Clips und wiederholt diesen count - mal long getMicrosecondLength() - gibt die Länge des Sounds in Mikrosekunden an long getMicrosecondPosition() - gibt die aktuelle Position bei der Wiedergabe in Mikrosekunden an Nun zeigen wir, wie man den zweiten Ansatz vollziehen kann bei der Wiedergabe einer Sounddatei, nämlich ... Wie kann man ... Audiofiles als SourceDataLine abspielen ? Zunächst erzeugt man wie beim ersten Ansatz ein File Objekt. Danach wird wieder ein Objekt vom Typ AudioInputStream generiert. Als nächster Schritt liegt die Erzeugung eines DataLine.info Objekt vor. Num kommen wir zum ersten Unterschied zum ersten Ansatz. Dem Konstruktor der DataLine.info Klasse wird nun nämlich mitgeteilt das eine SourceDataLine gewünscht wird. Man ersetzt also Clip.class durch SourceDataLine.class und erhält: DataLine.Info info = new DataLine.Info(SourceDataLine.class, stream.getFormat()); Als nächste wird vom AudioSystem ein Objekt vom Typ SourceDataLine angefordert: SourceDataLine sl = (SourceDataLine)AudioSystem.getLine(info); Dies geschieht auf gleichem Wege wie bei dem Clip - Ansatz. Die nächsten Schritte sind ebenfalls gleich, da nun eine open - Anweisung - gefolgt von einer start Anweisung - gemacht werden muß. __________________________________________________________________________________________ Sound-Programmierung in Java - 11 - Vor dem eigentlich Lesen der Audiodaten aus der Sounddatei muss noch ein Byte-Array angelegt werden, in dem die gelesenen Audiodaten gepuffert werden. Ist dies erledigt, folgt das Auslesen der Audioinformationen aus der Sounddatei. Da die Klasse AudioInputStream eine von der Klasse InputStream abgeleitete Klasse darstellt, wird hierzu die bekannte read - Methode benutzt. Es wird solange die Anweisung int numBytesRead = stream.read(ba,0,1024); wie numBytesRead nicht den Wert -1 annimmt. Wird dieser Wert nämlich zurückgeliefert, ist das Ende der Sounddatei beim Auslesen der Audioinformationen erreicht. Die Methode read schreibt in das vorher angelegte ByteArray ba die ausgelesenen Audiodaten der Quelldatei. Hierbei werden die Arrayfelder 0 bis 1024 benutzt. Nach der read - Anweisung wird das ByteArray der SourceDataLine - Methode write übergeben, was die Wiedergabe der darin enthaltenen Audiodaten veranlasst. Nach dem Verlassen der read - write -Schleife, also nachdem das Ende der Quelldatei erreicht und alle Audiodaten ausgelesen wurden, wird die SourceDataLine - Methode drain() aufgerufen, die solange blockt, bis alle sich noch im internen Puffer befindlichen Audiodaten verarbeitet wurden; somit ist gewährleistet, daß die Sounddatei komplett wiedergegeben wurde. Danach erfolgen - wie schon von dem Clip - Ansatz her bekannt - noch stop - und close Anweisungen. Damit wären alle Schritte abgehandelt, die nötig sind, um mit Hilfe einer SourceDataLine eine Sounddatei wiederzugeben. Nun wollen wir dazu ein komplettes Beispiel zeigen, nämlich PlaySoundStreamed.java: import javax.sound.sampled.*; import javax.sound.midi.*; import java.io.*; class PlaySoundStreamed { static File file = null; //die zu streamende Datei static AudioInputStream stream = null;//ueber diesen Stream //werden die AudioDaten aus der //Datei "file" gelesen static SourceDataLine sl = null; //ueber diese DataLine wird die //Datei "file" abgespielt static byte ba[] = null; // Array enthaelt die //Sounddateibytes die aktuell aus //dem AudioInputStream "stream" __________________________________________________________________________________________ Sound-Programmierung in Java - 12 - //gelesen wurden und dann ueber //die SourceDataLine "sl" //abgespielt werden static int numBytesRead = 0; //Anzahl der gelesenen Bytes aus //dem InputStream static PlaySoundStreamed pss = null; public PlaySoundStreamed(File fi) { //erzeuge den InputStream auf die Datei "file" try { stream = AudioSystem.getAudioInputStream(file); }catch(UnsupportedAudioFileException e) {System.out.println("AudioFormat wird nicht unterstuetzt!");} catch(IOException e2){System.out.println("Fehler beim Oeffnen der Quelldatei!");} //erzeuge DataLine.info die spezifiziert welche Art von Eingabe man //haben will beim AudioSystem DataLine.Info info = new DataLine.Info (SourceDataLine.class, stream.getFormat()); //versuche mit Hilfe von "info" die gewünschte Eingabe beim //AudioSystem zu bekommen try{ sl = (SourceDataLine) AudioSystem.getLine(info); }catch(LineUnavailableException e){System.out.println("Line konnte nicht benutzt werden");System.exit(1);} } public static void main (String args[]) { file = new File("1-welcome.wav"); pss = new PlaySoundStreamed(file); System.out.println("Spiele wav ..."); // oeffne und reserviere somit fuer diese Applikation die //SourceDataLine try{ sl.open(); }catch(LineUnavailableException e){System.out.println("Fehler beim Öffnen des AudioStreams");System.exit(1);} __________________________________________________________________________________________ Sound-Programmierung in Java - 13 - // starte die Line zum Abspielen des Sounds sl.start(); // lege den Buffer an in den die Sounddateidaten eingelesen werden //ueber die AudioInputStream ba = new byte[1024]; //Spiele Sound while(true) { //lese SounddateiDaten in Buffer ein try { numBytesRead = stream.read(ba,0,1024); }catch(IOException e){System.out.println("Fehler beim Lesen der Sounddatei-Bytes"); System.exit(1);} //wenn Ende der Sounddatei bzw. Streams erreicht beende Schleife if (numBytesRead == -1) break; //Ende der Sounddatei erreicht! // schreibe gelesene Sounddateidaten in SourceDataLine und veranlasse //somit das Abspielen der Daten sl.write(ba,0,ba.length); } // blockiere bis die letzten Daten abgespielt wurden sl.drain(); // halte die Line an und schliesse die Line sl.stop(); sl.close(); sl=null; System.out.println(" ... Fertig!"); System.exit(0); } } Wie man sieht, gibt es bis auf die while - Schleife keine großen Unterschiede zur PlaySound Applikation. In dieser Schleife werden jeweils kleine Pakete von Sounddaten aus der Quelldatei gelesen und dann auf die SourceDataLine geschrieben. Dieses Schreiben veranlasst, daß Java Sound System, die empfangenen Sounddaten über die Soundkarte abzuspielen. Ungewöhnlich erscheint vielleicht der __________________________________________________________________________________________ Sound-Programmierung in Java - 14 - Aufruf der Methode [SourceDataLine].drain() nach dem Verlassen der Schleife. Diese Methode ist wichtig, damit beim Austritt aus der Schleife nicht direkt die stop - Anweisung folgt und dadurch die Wiedergabe gestoppt wird, obwohl sich eventuell noch nicht wiedergegebene Sounddaten im System befinden. Die Methode blockt also solange, bis alle Daten korrekt abgespielt wurden. Eine verfeinerte Version des obigen Programms findet man im Anhang A, bei dem man jederzeit die Wiedergabe durch Tastendruck beenden kann (siehe PlaySoundStreamedT.java). Zum Schluß dieses Kapitels, fassen wir in einer Tabelle noch einmal die Methoden der Java Sound API zusammen, die die Möglichkeit haben, Java Exceptions zu erzeugen: Methode AudioSystem.getAudioInputStream( ) AudioSystem.getLine( ) [Line – Objekt].open( ) [AudioInputStream – Objekt].read( ) [AudioOutputStream – Objekt].write( ) erzeugte Exception UnsupportedAudioFileException, IOException, LineUnavailableException, SecurityException, IllegalArgumentException LineUnavailableException IOException IOException __________________________________________________________________________________________ Sound-Programmierung in Java - 15 - Kapitel 3 - Die Aufnahme Die Aufnahme einer Sounddatei gestaltet sich in großen Abschnitten ähnlich wie das Abspielen von Dateien. Neu hinzugekommen sind Objekte vom Typ TargetDataLine. Was ist eine TargetDataLine ? Über eine TargetDataLine hat man die Möglichkeit Audiodaten, die in das Java Sound System hinein gelangen, zu lesen. Daher stellen diese Line - Objekte die wichtigsten Werkzeuge dar, mit denen bei der Soundaufnahme gearbeitet wird. TargetDataLine bekommt seine Daten direkt von einem „vorgeschaltenen“ Mixer - Objekte oder von der AudioSystem - Klasse der Java Sound API. Diese beiden Objekte können Audiodaten, die sie eventuell vorher noch bearbeitet haben (beispielsweise Hall hinzufügen oder die Balance zwischen linken und rechtem Kanal beinflussen) direkt in den internen Puffer einer TargetDataLine hineinschreiben. An diese Daten gelangt man dann über eine read - Methode, deren Übergabeparameter gleich denen der read - Methode bei der Klasse InputStream sind. Dies resultiert aus der Tatsache, das TargetDataLine eine von InputStream abgeleitete Klasse darstellt. Diese zu verarbeitenden Audioinformationen können beispielsweise über ein angeschlossenes Mikrofon oder über den Line-In - Eingang der Soundkarte in das Java Sound System gelangen. Wie kann man ... selber Audiodaten aufnehmen ? Man entscheidet zunächst, in welchem Format und mit welcher Qualität die Daten aufgezeichnet werden sollen. Hierzu wird ein AudioFormat - Objekt erzeugt, das diese Punkte genau spezifiziert. AudioFormat af = new AudioFormat((float)11025.0,16,2, true,false); Danach erfolgt das schon bekannte Erzeugen eines DataLine.info - Objektes. DataLine.info info = new DataLine.Info (TargetDataLine.class,af); Zu beachten ist jetzt hierbei, das dem Konstruktor als erster Parameter TargetDataLine.class übergeben wird, da wir ja solch ein Line - Objekt im folgenden Schritt vom AudioSystem erhalten wollen: __________________________________________________________________________________________ Sound-Programmierung in Java - 16 - TargetDataLine tl = (TargetDataLine) AudioSystem.getLine(info); Als nächstes müssen wir unsere TargetDataLine tl für unsere Applikation reservieren, damit keine andere Anwendung diese für sich beanspruchen kann. Das geschieht durch eine einfache open Anweisung: tl.open(af); Als Übergabeparamter haben wir hier nicht den AudioInputStream wie bei dem Verfahren zur Soundwiedergabe gesehen, sondern das gewünschte Audioformat, in dem über die TargetDataLine nachher die Audioaufnahmen gemacht werden sollen. Nun wird die start() - Methode der TargetDataLine aufgerufen: tl.start(); Alle Audiodaten, die in das AudioSystem fliessen, können nun mit der eben schon erwähnten read - Methode über die TargetDataLine gelesen werden. Zur Verdeutlichung, sei an dieser Stelle folgendes Beispielprogramm mit dem Namen CaptureAudio.java eingebracht: import javax.sound.sampled.*; import java.io.*; class CaptureAudio extends Thread { static AudioInputStream stream = null; //uber diesen Stream werden //die aufgenommenen AudioDaten //ausgelesen static AudioFormat af = null; //gibt das AudioFormat an in dem //die Aufnahme gemacht werden soll static byte ba[] = null, // Array enthaelt die Sounddateibytes //die aktuell aus dem AudioInputStream //"stream" gelesen wurden und dann ueber //die SourceDataLine "sl" abgespielt //werden caB[] = null; static int numBytesRead = 0;// Anzahl der gelesenen Bytes static CaptureAudio ca = null; static boolean capture = true; static SourceDataLine sl = null; static TargetDataLine tl = null; static ByteArrayOutputStream baOut = null; static ByteArrayInputStream baIn = null; static DataLine.Info info; __________________________________________________________________________________________ Sound-Programmierung in Java - 17 - public CaptureAudio() { // bestimmt das Soundformat in dem die Aufnahme gemacht werden soll af = new AudioFormat((float)11025.0,16,2,true,false); // bestimmt welche Art von Line vom AudioSystem angefordert wird info = new DataLine.Info (TargetDataLine.class,af); // versuche Line von AudioSystem anzufordern try { tl = (TargetDataLine) AudioSystem.getLine(info); }catch(LineUnavailableException e) {System.out.println("AudioSystem hat keine Line frei"); System.exit(1);} //oeffne und reserviere somit fuer diese Applikation die //TargetDataLine try { tl.open(af); }catch(LineUnavailableException e){System.out.println("Fehler beim Öffnen des AudioStreams");System.exit(1);} } public void run() { baOut = new ByteArrayOutputStream(); int numBytesRead = 0; //Puffer in den die aufgenommenen Daten eingelesen werden ba = new byte [64]; // starte die Line zum Aufnehmen des Sounds tl.start(); // starte Aufnahme while(capture) { //lese SounddateiDaten in Buffer ein numBytesRead = tl.read(ba,0,ba.length); //schreibe in aktuelle AudioDatenbytes auf OutputStream baOut.write(ba,0,numBytesRead); } // blockiere bis die letzten Daten geschrieben wurden __________________________________________________________________________________________ Sound-Programmierung in Java - 18 - tl.drain(); // halte die Line an und schliesse die Line tl.stop(); tl.close(); tl=null; System.out.println(" ... Fertig mit Aufnahme"); System.out.println("...STOP"); System.out.println("Spiele aufgenommenen Sound ab ..."); //lese aufgenommene Audiodaten aus caB = baOut.toByteArray(); System.out.println("Anzahl der gecaptureten Bytes:"+caB.length); //uebergebe alle gelesenen Audiodaten an Inputstream baIn = new ByteArrayInputStream(caB); //oeffne InputStream auf gelesene Audiodaten stream = new AudioInputStream(baIn, af,caB.length / af.getFrameSize()); //erzeuge DataLine.info die spezifiziert welche Art von Eingabe man //haben will beim AudioSystem info = new DataLine.Info(SourceDataLine.class, stream.getFormat()); //versuche mit Hilfe von "info" die gewünschte Eingabe beim //AudioSystem zu bekommen try{ sl = (SourceDataLine) AudioSystem.getLine(info); }catch(LineUnavailableException e){System.out.println("Line konnte nicht benutzt werden");System.exit(1);} try{ // öffne Line zum Abspielen des Sounds sl.open(); }catch(LineUnavailableException e){System.out.println("Fehler beim Öffnen des AudioStreams");System.exit(1);} // starte die Line zum Abspielen der aufgenommenen Audiodaten sl.start(); while(true) { //lese SounddateiDaten in Buffer ein try __________________________________________________________________________________________ Sound-Programmierung in Java - 19 - { numBytesRead = stream.read(ba,0,64); }catch(IOException e){System.out.println("Fehler beim Lesen der SounddateiDaten");System.exit(1);} //wenn Ende der Sounddatei bzw. Streams erreicht beende Schleife if (numBytesRead == -1) break; //Ende der Sounddatei erreicht! // schreibe gelesene Sounddateidaten in //SourceDataLine und veranlasse somit das Abspielen //der Daten sl.write(ba,0,ba.length); } // blockiere bis die lezten Daten abgespielt wurden sl.drain(); // halte die Line an und schliesse die Line sl.stop(); sl.close(); System.out.println(" ... Fertig!"); System.exit(0); } public static void main (String args[]) { ca = new CaptureAudio(); //starte den Aufnahmethread ca.start(); System.out.println("Aufnahme ..."); System.out.println("Druecke <RETURN> fuer Aufnahme-STOP"); try { int c = System.in.read(); }catch(IOException e){System.exit(1);} capture = false; } } Dieses Beispielprogramm dem Nutzer, eine Audioaufnahme zu machen, die das Programm anschließend noch einmal abspielt. Das Programm beginnt zunächst damit, im Konstruktor die oben beschriebenen Schritte durchzuführen bzw. abzuarbeiten. Es wird ein Audioformat gewählt (für eine genaue Beschreibung, __________________________________________________________________________________________ Sound-Programmierung in Java - 20 - welche Formate man wählen kann und wie man sie erstellt s. Kapitel 4 - Audioformate): In unserem Beispiel soll in einer Qualität von 11025Hz, 16Bit und Stereo aufgenommen werden. Als nächstes wird eine TargetDataLine erzeugt und geöffnet, die Line wird gestartet und die Aufnahme kann beginnen. Dies wird in der run - Methode des Threads erledigt. Man durchläuft hier solange eine Schleife, bis der Nutzer die Return-Taste betätigt und dadurch der boolschen Variable capture den Wert false zuweist. In dieser Schleife wird die TargetDataLine - Methode read()mit einem vorher angelegten Bytearray aufgerufen. Dieses Array stellt als erster Parameter einen Puffer dar. Der zweite Paramter benennt die Position des Arrays, an der begonnen werden soll es mit gelesenen Audiodaten zu füllen; der dritte Parameter gibt den Index an, bis zu dem dies durchgeführt werden soll. Dieses Vorgehen entspricht dem bei einem InputStream und sollte daher dem Leser nicht neu vorkommen. Die read - Methode liefert die Anzahl der beim letzten Aufruf gelesenen Bytes zurück. Als nächsten Schritt in der while -Schleife wird das gelesene Array ba in den ByteArrayOutputStream baOut geschrieben. An dieser Stelle könnte auch ein anderer Stream auftreten, zum Beispiel einer, der die Daten auf die Festplatte schreibt. Dieser read - write - Zyklus wird, wie schon erwähnt, so lange wiederholt, bis der User die Return Taste betätigt. Geschieht dies, wird nach der Schleife die TargetDataLine - Methode drain() aufgerufen. Damit wird dem System Zeit gegeben, eventuell noch im internen Puffer der TargetDataLine - Klasse befindliche Audiodaten korrekt auszulesen und zu schreiben (in diesem Beispiel auf den baOut - Stream). Die Methode blockt solange, bis dies vollständig erledigt ist. Zuletzt wird die TargetDataLine noch gestoppt und dann geschlossen, damit alle damit gebundenen Resourcen dem System wieder zur Verfügung gestellt werden können. Nun wird in das ByteArray caB die bei der Aufnahmen gelesenen Audiodaten mit der toByteArray() - Methode geschrieben. Das Array caB ByteArrayInputStream dient als Eingabe für den baIn. Dieser wiederum ist der erste Paramter um einen AudioInputStream zu erstellen. Der zweite Parameter, ist das Audioformat in dem die Audiodaten vorliegen, welche durch den AudioInputStream gelesen werden sollen. Als dritten Parameter benötigt der Konstruktor die Grösse der zu lesenden Datei in Frames. Was ist ein Frame ? Eine Sounddatei ist nicht nur durch die Länge in Bytes charakterisiert, sondern sie ist auch in eine Abfolge von sogenannten Frames unterteilt. Ein Frame enthält die Audiodaten für alle Kanäle, die die __________________________________________________________________________________________ Sound-Programmierung in Java - 21 - Datei zu einem bestimmten Zeitpunkt benutzt. Bei den Wave - Dateien entspricht die Anzahl der Frames, in die die Audiodatei aufgeteilt ist, der Anzahl der Samples pro Sekunde. Kommen wir jetzt wieder zu unserem Beispielprogramm zurück. Nachdem wir also unseren AudioInputStream erzeugt haben, fahren wir in der schon bekannten Weise (siehe Kapitel 2 – Soundgenerierung) fort. Wir erzeugen ein DataLine.info - Objekt und beziehen damit vom AudioSystem eine SourceDataLine sl. Was nun folgt ist schon aus dem Beispiel PlaySoundStreamed aus dem vorhergehenden Kapitel bekannt. In einer while – Schleife, die ver-lassen wird, wenn das Ende des Streams erreicht wurde (typisch hier der bei read zurückgelieferte Wert -1), wird mit read auf dem AudioInputStream gelesen und mit write auf der SourceDataLine geschrieben. Zum Schluss ... Wie kann man ... aufgenommene Sounds auf Platte speichern ? Bei dieser Aufgabenstellung ist die Methode write() der AudioSystem Klasse der Java Sound API die entsprechende Lösung. Man ruft die Methode mit dem ersten Parameter - einem AudioInputStream - auf, als zweiten spezifiert man das gewünschte Zielaudioformat der Soundatei und als dritten und letzten Paramter erwartet die Methode ein File Objekt. Um dieses Verfahren zu verdeutlichen verändern wir die run - Methode der im vorherigen Abschnitt besprochenen Applikation CaptureAudio.java wie folgt: public void run() { baOut = new ByteArrayOutputStream(); int numBytesRead = 0; ba = new byte [64]; // starte die Line zum Aufnehmen des Sounds tl.start(); // starte Aufnahme while(capture) { //lese SounddateiDaten in Buffer ein numBytesRead = tl.read(ba,0,ba.length); __________________________________________________________________________________________ Sound-Programmierung in Java - 22 - baOut.write(ba,0,numBytesRead); } // blockiere bis die lezten Daten abgespielt wurden tl.drain(); // halte die Line an und schliesse die Line tl.stop(); tl.close(); tl=null; System.out.println(" ... Fertig mit Aufnahme"); System.out.println("...STOP"); System.out.println("Schreibe aufgenommenen Sound ..."); caB = baOut.toByteArray(); System.out.println("Anzahl der gecaptureten Bytes:"+caB.length); File file = new File("my.wav"); baIn = new ByteArrayInputStream(caB); stream = new AudioInputStream(baIn, af,caB.length / af.getFrameSize()); try { AudioSystem.write(stream, AudioFileFormat.Type.WAVE,file); }catch(IOException e){System.out.println( "Kann Datei nicht schreiben!");} System.out.println(" ... Fertig!"); System.exit(0); } Man sieht, daß hier die write Methode mit dem vorher erzeugten AudioInputStream stream aufgerufen wird. Das zweite übergebene Objekt AudioFileFormat.Type.WAVE bestimmt, daß die zu schreibende Datei, im WAVE - Audioformat erstellt werden soll. Näheres zu den verschiedenen Audioformaten, die vom Java Sound System unterstützt werden, findet man im Kapitel 4 – Audioformate. Als dritter Parameter ist noch das vorher erzeugte File Objekt zu nennen. __________________________________________________________________________________________ Sound-Programmierung in Java - 23 - Soviel also zu den Verfahren, mit denen typischerweise Audioaufzeichnungen mit der Java Sound API durchgeführt werden. Weitere Informationen findet man natürlich wieder in der Java Sound API Dokumentation[2] oder dem offiziellen Java Sound API Programmers Guide[3]. Kapitel 4 - Audioformate __________________________________________________________________________________________ Sound-Programmierung in Java - 24 - Dieses Kapitel beschäftigt sich mit den verschiedenen Audioformaten, die von der Java Sound API unterstützt werden und wie man von einem Format in ein anderes konvertiert. Doch zunächst müssen wir uns folgende Frage stellen. Was sind überhaupt formatierte Audiodaten ? Formatierte Audiodaten beziehen sich auf Sounds, die sich in einem bestimmten Standard Audioformat befinden. Die Java Sound API unterscheidet zwei große Gruppen von formatierten Audiodaten: Zum einen die Gruppe der Datenformate und zum andere Seite die Gruppe der Fileformate. Was sind Datenformate ? Das Datenformat einer Sounddatei beschreibt, wie die Abfolge der „rohen“ Bytes in einer Sounddatei zu interpretieren ist. Es ist beispielsweise wichtig zu wissen, aus wie vielen Bits sich ein Sample (die kleinste Einheit, in die man Audiodaten aufteilen kann) zusammensetzt, oder wie die sogenannte Samplerate (gibt an, wie schnell auf ein Sample ein nächstes folgt) lautet. In der Java Sound API wird das Datenformat einer Sounddatei durch ein AudioFormat Objekt spezifiziert. In diesem Objekt sind folgende Eigenschaften eines Soundformates gespeichert: encoding Technik: zu nennen sind hier das PCM - Verfahren (das üblichste) und das ULAW/ALAW - Verfahren Anzahl der Kanäle d.h. wieviele Kanäle benutzt werden: 1 entspricht hierbei Mono und 2 Stereo Samplerate: die Anzahl der Samples pro Sekunde und pro benutzten Kanal Anzahl der Bits pro Sample: also wieviel Bits benutzt werden, um die Informationen eines Samples zu speichern Framerate Grösse eines Frames in Bytes Byte – Order: d.h. sind die Audiodaten als little Endian oder big Endian zu lesen An dieser Stelle soll nun nicht weiter auf die einzelnen Felder der Klasse eingegangen werden. Hierzu sei auf das Sun Tutorial[3] verwiesen, das die Klassen näher und ausführlicher beschreibt. Wie kann man ... nun ein geeignetes Audioformat finden ? __________________________________________________________________________________________ Sound-Programmierung in Java - 25 - Diese Frage stellt sich beispielsweise, wenn man in seinem Programm die Möglichkeit einbauen möchte, daß der User eigene Sounds aufnehmen kann (etwa bei einem selbstprogrammierten Audio Recorder). Wie wir schon bei der CaptureAudio.java Anwendung aus dem vorhergehenden Kapitel gesehen haben, wird bei der Konstruktion eines AudioInputStreams ein AudioFormat Objekt benötigt, um zu bestimmen, in welcher Qualität der aufgenommene Sound später vorliegen soll. Typischerweise nutzt man folgendes Objekt mit eventuellen Veränderungen: AudioFormat af = new AudioFormat(11025.0,16,2,true,false); Wichtig sind hierbei die drei ersten Werte, da sie die Qualität bestimmen, in der später der Sound vorliegt. Um CD - Qualität zu erlangen würde man 11025.0 (bestimmt die Samplerate) durch den Wert 44100.0 ersetzen, da dies der Samplerate einer CD entspricht. Der Wert 16 im Konstruktor bestimmt, daß für ein Sample 16 Bit benutzt werden sollen. Hier könnte man den Wert durch eine 8 ersetzen und somit festlegen, daß eine geringere Qualität verlangt wird, um die Sounddatei möglichst klein zu halten. Eine weitere Möglichkeit die Dateigröße zu minimieren, wäre, den Wert 2 (bestimmt eine Stereoqualität) durch eine 1 zu ersetzen und dadurch die Aufnahme nur in Monoqualität durchzuführen. Die beiden letzten Werte können auf einem Windows - Rechner so übernommen werden. Für eine nähere Beschreibung sei auf die Java Sound API Dokumentation[2] oder an den Java Sound Programmer´s Guide[3] verwiesen. Wie kann man ... von einem Audiodatenformat in ein anderes konvertieren ? Grundsätzlich ist eine Konvertierung von Audiodaten zulässig und sogar vorgesehen in der Java Sound API. Leider mussten wir beim Verfassen dieses Tutorials feststellen, daß ähnlich dem unfertigen Zustand der Port Objekte, auch die Konvertierung noch nicht endgültig implementiert wird. In [3] wird zwar beschrieben, wie man eine Konvertierung in eigene Programme einbauen kann, doch realisieren lässt sie sich so nicht. Es werden bisher nur einige wenige Konvertierungen unterstützt und der vielleicht wichtigste Aspekt, nämlich das Herunterkonvertieren von einer guten, speicherplatzzehrenden Formatierung auf ein schlechteres aber dafür weitaus weniger speicherplatzbenötigendes Format kann nicht durchgeführt werden. Mit Hilfe des Java Sound Boards (ein wichtiger Anlaufpunkt bei Problemen mit der API wie wir festgestellt haben)[4] konnten wir folgende unterstützten Konvertierungen zusammentragen: __________________________________________________________________________________________ Sound-Programmierung in Java - 26 - Quellformat Zielformat 16 Bit, PCM signed/unsigned 8 Bit, ULAW/ALAW 8 Bit, ULAW/ALAW 16 Bit, PCM signed/unsigned PCM signed PCM unsigned PCM unsigned PCM signed PCM little Endian PCM big Endian PCM big Endian PCM little Endian Die Tabelle zeigt, welche Werte jeweils bei der Konvertierung von einem Format in ein anderes verändert werden dürfen. Alle anderen Parameter des Audioformates dürfen nicht verändert werden., weshalb eine „Herunterkonvertierung“, wie oben beschrieben, auch nicht möglich ist. An einem kleinen Beispiel wollen wir nun zeigen wie man generell eine Konvertierung durchführen kann. Hier der Beispielcode der AudioDataConverter.java Klasse: import javax.sound.sampled.*; import java.io.File; import java.io.IOException; class AudioDataConverter { static AudioFormat sAudioFormat = null; static AudioFormat tAudioFormat = null; static AudioInputStream highResStream = null; static AudioInputStream lowResStream = null; static File sFile = null; public static void main (String args[]) { // pruefe ob Datei angegeben wurde die konvertiert werden sollte if (args.length == 0) throw new IllegalArgumentException ("Syntax: AudioDataConverter <path_to_wav-File>"); // erzeuge FileObject mit Referenz auf Quelldatei sFile = new File(args[0]); try __________________________________________________________________________________________ Sound-Programmierung in Java - 27 - { // erstelle AudioInputStream auf Quelldatei highResStream = AudioSystem.getAudioInputStream(sFile); // ermittle AudioFormat der Quelldatei sAudioFormat = highResStream.getFormat(); // bestimme Werte des Originalformates AudioFormat.Encoding enc = sAudioFormat.getEncoding(); float sampleRate = sAudioFormat.getSampleRate(); int sampleBits = sAudioFormat.getSampleSizeInBits(); int channels = sAudioFormat.getChannels(); int frameSize = sAudioFormat.getFrameSize(); float frameRate = sAudioFormat.getFrameRate(); boolean endian = sAudioFormat.isBigEndian(); // erstelle gewuenschtes Zielformat tAudioFormat = new AudioFormat(enc ,sampleRate, sampleBits, channels, frameSize, frameRate,(!endian)); // pruefe ob das Soundsystem auf der Maschine die Konvertierung // unterstuetzt if(AudioSystem.isConversionSupported(tAudioFormat, sAudioFormat)) { // erstelle AudioInputStream auf die Quelldatei mit vorher festgelegtem //Ziel-AudioDatenFormat lowResStream = AudioSystem.getAudioInputStream(tAudioFormat, highResStream); AudioSystem.write(lowResStream, AudioFileFormat.Type.WAV,new File("convert.wav")); lowResStream.close(); highResStream.close(); } else { System.out.println("Die Konvertierung wird vom AudioSystem nicht unterstuetzt."); } }catch(UnsupportedAudioFileException e){System.out.println("Die von Ihnen spezifizierte Sounddatei hat ein vom AudioSystem nicht unterstuetztes Format.");} __________________________________________________________________________________________ Sound-Programmierung in Java - 28 - catch(IOException e){System.out.println("Fehler beim Lesen der Quelldatei.");} System.exit(0); } } Unser Programm erwartet beim Aufruf den Pfad zu der zu konvertierenden Audiodatei als Übergabeparameter. Als nächsten Schritt erzeugen wir einen AudioInputStream auf diese Datei, genauso als wollten wir sie wiedergeben. Daraufhin wird das Audioformat unserer Quelldatei bestimmt, in der Variablen sAudioFormat zwischengespeichert sind und dann die einzelnen Audioformat-parameter bestimmt. Dies ist notwendig, da wir ja nur die in der obigen Tabelle angegebenen Werte bei der Konvertierung verändern dürfen und alle anderen unberührt bleiben müssen. Unser Programm soll hierbei den ausgelesen Zustand des bigEndian - Parameters der Quelldatei invertieren, d.h. eine Datei, die vor dem Aufruf im bigEndian - Format geschrieben wurde, wird durch unser Programm in das littleEndian - Format konvertiert und als „convert.wav“ geschrieben. Nachdem wir die Parameter der Quelldatei ausgelesen haben, erzeugen wir unser Zielformat tAudioFormat, bei dem alle Parameter bis auf den invertierten bigEndian - Wert gleich bleiben. Wenn wir also Quell - und Zielaudioformat bestimmt sind, wird das AudioSystem befragt, ob diese Konvertierung überhaupt von dem System unterstützt wird. Die geschieht mit dem Aufruf von boolean AudioSystem.isConversionSupported(Zielformat, Quellformat); Diese Methode ist sehr wichtig bei der Konvertierung, da man damit vorher prüfen kann, ob die Konvertierungsart überhaupt vom System unterstützt wird. Um die Zieldatei zu schreiben und die Konvertierung durchzuführen, wird die schon bekannte write() - Methode der AudioSystem - Klasse benutzt int AudioSystem.write(AudioInputStream,Zieldatenformat, Zielfile - Objekt); Nachdem wir uns also mit den (bisher noch unzulänglichen) Möglichkeiten der Audiodatenkonvertierung beschäftigt haben, wollen wir uns nun den Audiofileformaten zuwenden, die von der Java Sound API unterstützt werden. Was sind Audiofileformate ? __________________________________________________________________________________________ Sound-Programmierung in Java - 29 - Audiofileformate spezifizieren, wie die rohen Audiodaten in einem Soundfile angeordnet sind. In der Java Sound API sind die verschiedenen Audioformate durch die Klasse AudioFormat repräsentiert, und ein Audiofile durch die Klasse AudioFileFormat. Die API unterstützt folgende Standardaudioformate, definiert als AudioFileFormat.Type.X - Objekte, wobei X für AIFC AIFF SND AU WAVE steht. Die Java Sound API unterstützt die Konvertierung zwischen oben genannten Standardaudioformaten. Wie kann man ... zwischen verschiedenen Audiofileformaten konvertieren ? Die Konvertierung zwischen verschiedenen Audiofileformaten erweist sich als einfacher und vor allem als vollständig in die API implementiert. Das Vorgehen bei dieser Aufgabe wollen wir uns an der entscheidenden Methode des Beispielprogramms FileConverter.java, dessen kompletten Quellcode in Anhang A zu finden ist, ansehen. Hierbei handelt es sich um die Methode write(): private static void write (AudioInputStream in,AudioFileFormat.Type fileType,File file) { System.out.println("Konvertiere Quelldatei nach <"+file.getName()+">"); int writtenBytes = 0; try { writtenBytes = AudioSystem.write(in, fileType, file); } catch(IllegalArgumentException e){System.out.println("Das von Ihnen gewaehlte Soundformat wird nicht unterstuetzt!"); System.exit(0);} catch (IOException e){System.out.println("Fehler beim Schreiben in Zieldatei!");System.exit(1);} System.out.println("... Konvertierung erfolgreich beendet"); System.out.println("\nGeschrieben : "+writtenBytes+" Bytes als __________________________________________________________________________________________ Sound-Programmierung in Java - 30 - "+fileType.getExtension()); } Die Methode erwartet einen - bereits auf die Quelldatei geöffneten - AudioInputStream, das Zielformat, in welches die Quelldatei konvertiert werden soll und schliesslich ein File Objekt, daß die Zieldatei spezifiziert. Die eigentliche Formatkonvertierung führt die uns schon bekannte write() - Methode der AudioSystem Klasse durch. Zum Abschluss noch die neu hinzugekommenen Methoden und die von ihnen erzeugten Exceptions: Methode AudioSystem.write( ) erzeugte Exception IllegalArgumentException, IOException Kapitel 5 - Kontrollen __________________________________________________________________________________________ Sound-Programmierung in Java - 31 - Dieses Kapitel soll sich mit den noch verbleibenden Aspekten des Paketes javax.sound.sampled beschäftigen. Erwähnenswert sind beispielsweise noch die sogenannten Control Objekte der API. Sie dienen dazu, etwa die Lautstärke der Wiedergabe zu regeln, oder die Balance zwischen rechtem und linken Kanal zu verändern. Es gibt vier verschiedene Subklassen der Control Objekte, die man in seinen Programmen nutzen kann: BooleanControl - repräsentiert Kontrollen, die durch 2 verschiedene Zustände charakterisiert werden können; Beispiel: Stummschalten einer Line an/aus FloatControl - erlaubt Kontrolle über Objekte, die mit Fließkommazahlen arbeiten; Beispiel: Lautstärke der Wiedergabe eines Sounds EnumControl - wird benutzt, wenn man die Wahl zwischen verschiedenen Kontrollobjekten implementieren möchte; Beispiel: User hat die Wahl zwischen verschiedenen festgelegten Lautstärkeeinstellungen CompoundControl - ermöglicht den gleichzeitigen Zugriff auf Control Objekte, die zu einer Gruppe zusammengefaßt sind; Beispiel: ein Equalizer In diesem Tutorial beschränken wir uns darauf, eine kurze Einführung in die FloatControl Objekte zu geben, da sie uns in der Praxis am wichtigsten erscheinen. Für eine weiterführende Beschreibung der anderen Klassen, sei der Leser auf [2] und [3] verwiesen. Die von der Java Sound API zur Verfügung gestellten FloatControl Objekte werden durch folgende FloatControl.Type.X Objekte repräsentiert: X steht für: AUX_RETURN AUX_SEND BALANCE MASTER_GAIN PAN REVERB_RETURN REVERB_SEND SAMPLE_RATE VOLUME __________________________________________________________________________________________ Sound-Programmierung in Java - 32 - Möchte man dem User in einem Programm z.B. die Möglichkeit geben, die Lautstärke, in der eine Sounddatei wiedergegeben wird, selber zu regeln, geschieht dies darin, sich ein FloatControl Objekt zu generieren, das vom Typ FloatControl.Type.MASTER_GAIN ist. Damit kommen wir zu der Frage ... Wie kann man ... auf FloatControl Objekte erzeugen ? Zunächst einmal ist die Frage zu klären, welches FloatControl Objekt man überhaupt benutzen möchte. In unserem folgenden Beispiel soll die Lautstärke geregelt werden, in der die Soundausgabe erfolgt. Wir benötigen also ein Objekt vom Typ FloatControl.Type.MASTER_GAIN. Als ersten Schritt müssen wir prüfen, ob wir auf der Line, auf der wir die Ausgabe durchführen wollen, überhaupt die Möglichkeit haben, das von uns gewünschte FloatControl Objekt zu nutzen. Hierzu rufen wir die Line - Methode isControlSupported(FloatControl.Type.X) auf. Sie gibt einen boolschen Wert zurück, an dem wir erkennen können, ob unser gewünschtes Control - Objekt verfügbar ist. Schauen wir uns diesen Schritt an einem beispielhaften Auszug aus der Klasse PlayBack.java an (das vollständige Programm AudioPlayer und seine Klassen steht in Anhang A): private void createClip()throws LineUnavailableException { DataLine.Info info = new DataLine.Info(Clip.class, audioFormat); clip = (Clip) AudioSystem.getLine(info); if (clip.isControlSupported (FloatControl.Type.MASTER_GAIN)) { clipGainControl = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN); } if (clip.isControlSupported(FloatControl.Type.PAN)) { clipPanControl = (FloatControl) clip.getControl(FloatControl.Type.PAN); } } In dieser Methode createClip() wird zunächst ein Clip Objekt als Line erzeugt auf der die Soundausgabe erfolgen soll. Im nächsten Schritt wird geprüft, ob die Line eine Lautstärkeregelung __________________________________________________________________________________________ Sound-Programmierung in Java - 33 - zulässt. Hierzu wird die Methode isControlSupported() benutzt. Wenn sie ein true zurückliefert, wird das gewünschte Control Objekt erzeugt, in dem man die Line - Methode getControl(X) aufgerufen wird. Wichtig hierbei ist der vorgestellte Cast - Operator, der das zurückgelieferte Control - Objekt, zu einem FloatControl - Objekt castet. Zum Schluss wird versucht, Zugriff auf die Balance - Steuerung zwischen rechtem und linkem Kanal zu erhalten, in dem ein Control Objekt vom Typ FloatControl.Type.PAN erzeugt wird. Nachdem wir nun also gesehen haben, wie wir Control Objekte erzeugen können, die die von uns gewünschten Aufgaben erfüllen bleibt noch ... Wie kann man ... Werte bei FloatControl Objekten verändern ? Kommen wir noch einmal auf unser Beispiel zurück, bei dem der User eines Programmes selbst bestimmen können soll, wie laut oder leise die Wiedergabe einer Sounddatei erfolgen soll. Die Eingabe des neuen Wertes könnte beispielsweise über Swing Elemente ( zum Beispiel über ein Slider Objekt) erfolgen. Dieser Ansatz wird in dem Beispielprogramm AudioPlayer, das im Anhang A zu finden ist, durchgeführt. Über ein Slider Objekt kann der User die Lautstärke und die Balance zwischen linkem und rechtem Kanal beeinflussen. Der gewünschte Wert muss nun über ein FloatControl Objekt auf die benutzte Line angewendet werden. Im vorherigen Abschnitt haben wir ja schon gezeigt, wie wir bei dem AudioPlayer Programm, die FloatControl Elemente erzeugen. Nehmen wir beispielsweise das PAN Objekt. Um hierbei die gewünschte Balance einzustellen, wird die setValue() - Methode der FloatControl Klasse benutzt. Dies geschieht wie folgt: void [FloatControl-Objekt].setValue(float neuerWert); Der so gesetzte neue Wert, wird sofort auf die benutzte Line angewendet. In unserem Beispielprogramm sieht dies wie folgt aus: clipPanControl.setValue(fPan); Zum Schluss seien noch folgende FloatControl - Methoden genannt, die bei der Arbeit mit den Control Objekten nützlich sein können,: float [FloatControl-Objekt].getMaximum(); - gibt den maximalen Wert zurück, den das Element annehmen kann float [FloatControl-Objekt].getMinimum(); - gibt den minimalsten Wert zurück, den das Element annehmen kann float [FloatControl-Objekt].getValue(); - gibt den aktuellen Wert des Elementes an Zum Ende dieses Kapitels sei noch eine letzte nützliche Klasse genannt, die sogenannten LineListener. Mit Hilfe dieser Klasse kann der Status einer Line - also der Status eines Clips, __________________________________________________________________________________________ Sound-Programmierung in Java - 34 - einer Source- bzw. TargetDataLine oder eines Ports - überwacht werden und kann daher auch zu den Kontrollen gezählt werden, die von der Java Sound API zur Verfügung gestellt werden. Line - Objekte erzeugen sogenannte LineEvent - Objekte, wenn sie sich in einem bestimmten Zustand befinden. Diese Zustände sind START, STOP, OPEN, CLOSE. Wird zum Beispiel die Wiedergabe eines Sounds gestartet, so wird ein Start - LineEvent - Objekt erzeugt und an die LineListener übergeben, die an die gestartete Line gebunden sind. Wie kann man ... LineListener benutzen ? Man geht hierbei ähnlich vor, wie bei AWT - Events. Typischerweise benutzt man eine innere Klasse. Schauen wir uns folgendes Beispiel an: clip.addLineListener(new LineListener () { public void update (LineEvent e){ if (e.getType() == LineEvent.Type.STOP) { //schliesse den Clip und gib somit die gebundenen Resourcen wieder frei clip.close(); } if(e.getType() == LineEvent.Type.CLOSE) { //beende Programm System.exit(0); } }}); In diesem Beispiel wird ein LineListener an ein Line Objekt clip gebunden, um dessen Status zu überwachen. Hierbei sollen STOP und CLOSE Zustände behandelt werden. Man implementiert also eine innere Klasse LineListener und schreibt die Methode update(). Diese Methode wird immer dann aufgerufen, wenn das Line Objekt, das an diesen LineListener gebunden ist, ein LineEvent Objekt erzeugt. Das erzeugte Objekt wird anschließend der update Methode übergeben. Um festzustellen, um welchen Event es sich handelt, wird die LineEvent Methode getType() benutzt. Der gelieferte Wert wird dann mit den folgenden möglichen LineEvent.Type Objekten verglichen: __________________________________________________________________________________________ Sound-Programmierung in Java - 35 - LineEvent.Type.START LineEvent.Type.STOP LineEvent.Type.OPEN LineEvent.Type.CLOSE und es wird entsprechend reagiert. Zum Ende des Kapitels, wieder alle neu hinzugekommenen Methoden, die die Möglichkeit haben, Exceptions zu erzeugen: Methode [Line – Objekt].getControl() [FloatControl – Objekt].setValue( ) erzeugte Exception IllegalArgumentException IllegalArgumentException __________________________________________________________________________________________ Sound-Programmierung in Java - 36 - TEIL II javax.sound.midi Kapitel 6 - MIDI - Wiedergabe __________________________________________________________________________________________ Sound-Programmierung in Java - 37 - Dieses Kapitel beschäftigt sich mit der Wiedergabe von Midi - Audiodateien mit Hilfe der Java Sound API. Es werden einige Midi - spezifische Audrücke benutzt werden müssen, die in diesem Tutorial nicht ausführlich besprochen werden können. Für alle, die mehr über die Midi - Spezifikation erfahren wollen, sei die offizielle Midi – Seite, die man unter [5] findet, empfohlen. Kommen wir nun zu unserer ersten Frage - bzw. Problemstellung. Wie kann mann ... mit der Java Sound API Midi - Dateien wiedergeben ? Die wichtigste Klasse im Paket javax.sound.midi ist die sogenannte statische Klasse MidiSystem. Sie kann mit der statischen Klasse AudioSystem aus dem sampled - Paket der Java Sound API verglichen werden. Das MidiSystem bzw. die Klasse erstellt die beiden wichtigsten Klassen, die bei der Wiedergabe von Mididateien eine Rolle spielen, nämlich einmal die Sequencer Klasse und zum anderen die Synthesizer Klasse. Ein Sequencer kann man sich als Steuerung für den Synthesizer vorstellen. Er „liest“ die Noten aus einer Midi - Datei aus und spielt sie auf dem richtigen Instrument auf dem Synthesizer ab. Daher liegt es nahe, als ersten Schritt bei der Erstellung eines Programms, das Midi - Dateien bearbeiten soll, ein Sequencer und Synthesizer Objekt zu erzeugen. Dies geschieht über die MidiSystem - Methoden getSequencer() und getSynthesizer() wie folgt: Sequencer sequencer = MidiSystem.getSequencer(); Synthesizer synthesizer = MidiSystem.getSynthesizer(); Als nächsten Schritt öffnet man den Sequencer und den Synthesizer mit einfachen open() - Methoden: sequencer.open(); synthesizer.open(); Somit werden die beiden Objekte exklusiv für diese Anwendung reserviert und kein anderes Programm kann sie benutzen. Um nun eine Verbindung zwischen dem Synthesizer und dem Sequencer herzustellen und damit die Wiedergabe zu ermöglichen, werden sogenannte Receiver und Transmitter Objekte aus der API benötigt. Ein Transmitter wird immer mit einem Receiver verbunden damit der Receiver die vom Transmitter ausgesendeten Daten empfängt und an seine Klasse weitergibt, welche die Daten dann verarbeitet. In unserem Beispiel wird für den Sequencer ein Transmitter benötigt (da der Sequencer ja später den Synthesizer steuern soll) und für den Synthesizer ein Receiver Objekt, damit dieses die Daten vom Sequencer aufnimmt und an ihn weitergibt: Transmitter trans = sequencer.getTransmitter(); Receiver receive = synthesizer.getReceiver(); __________________________________________________________________________________________ Sound-Programmierung in Java - 38 - Im nächsten Schritt soll dem erstellten Transmitter Objekt ein Receiver Objekt zugewiesen, so daß dieses später den Empfänger der „auszusendenden“ Daten kennt. Dies wird durch den Aufruf der Transmitter Methode setReceiver() gewährleistet und wie folgt in die Praxis umgesetzt: trans.setReceiver(receive); Nach solchen vorbereitenden Massnahmen müssen wir uns vor der Wiedergabe der Datei noch mit dieser selbst auseinandersetzen. Nachdem wir ein File Objekt erzeugt haben, das sich auf die wiederzugebende Midi - Datei bezieht, muss damit ein sogenanntes Sequence Objekt erzeugt werden. In der Java Sound API werden die Midi - Informationen einer Midi - Datei durch Sequence Objekte dargestellt. Wir verfahren also nun weiter, indem wir File file = new File (Pfad zu einer Midi - Datei); Sequence sequence = new Sequence (file); Objekte erzeugen. Der letzte Schritt, den wir vor dem Abspielen der Midi - Datei file durchführen müssen, ist, die erstellte sequence dem Sequencer zu übergeben, damit dieser die sequence abarbeiten kann und dadurch die Midi - Datei auf dem Synthesizer abgespielt wird. Folgende Anweisung übernimmt diese Aufgabe: sequencer.setSequence(sequence); Um die Wiedergabe der Midi - Datei file zu starten, ist nur noch eine start - Anweisung nötig und der Sequencer beginnt die Musikwiedergabe auf dem Synthesizer: sequencer.start(); Diese Vorgehensweise wollen wir uns nun an einem konkreten Beispiel ansehen, nämlich an dem Programm PlayMidi.java: import javax.sound.midi.*; import java.io.IOException; import java.io.File; class PlayMidi { static PlayMidi playMidi = null; static Synthesizer synth = null; static Sequencer sequencer = null; static Receiver synthReceiver = null; static Transmitter seqTransmitter =null; public static void main(String args[]) { if (args.length == 0) throw new IllegalArgumentException ("Syntax: java __________________________________________________________________________________________ Sound-Programmierung in Java - 39 - PlayMidi <midifile>"); try { //erzeuge Sequencer und öffne diesen sequencer = MidiSystem.getSequencer(); sequencer.open(); //erzeuge Transmitter seqTransmitter = sequencer.getTransmitter(); //erzeuge Synthesizer und öffne diesen synth = MidiSystem.getSynthesizer(); synth.open(); //erzeuge Receiver, der mit dem Transmitter verbunden wird, um Daten//uebertragung zwischen Synthesizer und Sequenzer zu erlauben synthReceiver = synth.getReceiver(); seqTransmitter.setReceiver(synthReceiver); //erzeuge File Objekt auf uebergebenen Midi-Datei-Pfad File midiFile = new File(args[0]); //erzeuge Sequence mit der Midi-Datei Sequence sequence = MidiSystem.getSequence(midiFile); //uebergib dem Sequencer die Sequence, die er abspielen soll sequencer.setSequence(sequence); //lass Sequencer von MetaL. ueberwachen, um festzustellen, wann //Ende der Midi-Datei bzw. Sequence erreicht sequencer.addMetaEventListener(new MetaEventListener() { public void meta(MetaMessage event) { if (event.getType() == 47) { sequencer.stop(); //gib dem Synthesizer Zeit, die letzte Note klingen zu lassen try { Thread.sleep(1000); }catch(InterruptedException e){} sequencer.close(); synthesizer.close(); System.exit(0); } }}); }catch(MidiUnavailableException e) {System.out.println("Kein MidiGeraet verfuegbar."); System.exit(0);} catch(InvalidMidiDataException e){System.out.println("Die Datei __________________________________________________________________________________________ Sound-Programmierung in Java - 40 - "+args[0]+" enthaelt keine gueltigen MidiDaten."); System.exit(0);} catch(IOException e){System.out.println("Konnte Datei "+args[0]+" nicht oeffnen."); System.exit(1);} System.out.println("Spiele Midi-Datei ..."); System.out.println("Druecke <RETURN> fuer STOP"); //starte den Sequencer und somit die Wiedergabe sequencer.start(); //warte bis Nutzer Return drueckt oder Ende der Midi-Datei erreicht wird try { int c = System.in.read(); }catch(IOException e){System.exit(1);} sequencer.stop(); System.out.println("...STOP"); //gib die gebundenen Resourcen wieder frei sequencer.close(); synthesizer.close(); System.exit(0); } } In diesem Beispielcode findet man alle oben beschriebenen Schritte wieder. Wichtigste Neuerung ist hier die Einbindung einer sogenannten MetaEventListener Klasse. Vergleichbar ist diese Klasse mit der LineListener Klasse aus dem sampled - Paket in Teil II dieses Tutorials. Sie dient ebenfalls zur Überwachung der Wiedergabe einer Datei. Der Unterschied besteht allerdings darin, daß keine LineEvent Objekte erzeugt werden, wenn bestimmte Situationen während der Wiedergabe auftreten, sondern sogenannte MetaEvent Objekte. Diese MetaEvents können von einem Sequencer Objekt interpretiert werden. MetaEvent Objekten sind bestimmte Nummern nach der Midi - Spezifikation zugeordnet. In unserem Beispiel wird ein Event mit der Nummer 47 abgefragt. Dies bedeutet Ende der Sequence und der Sequencer selbst kann durch eine stop() - Anweisung beendet werden. Nach der stop() - Anweisung sollte dem Synthesizer noch eine gewisse Zeitspanne gegeben werden, um die letzte Note auch vollkommen verklingen zu lassen. Stoppt man einfach den Sequencer und beendet das Programm, wirkt die Wiedergabe der letzten Note unnatürlich abgehakt. Daher in unserem Beispiel die Thread.sleep(1000); Anweisung damit das Programm nach einer Wartezeit von einer Sekunde beendet wird. __________________________________________________________________________________________ Sound-Programmierung in Java - 41 - Wenn die Geräte Sequencer und Synthesizer nicht mehr benötigt werden, sollten ihre Resourcen, durch einfache close() - Anweisungen dem System wieder zur Verfügung gestellt werden, damit andere Programme nun auch diese Geräte nutzen können: sequencer.close(); synthesizer.close(); Mit Hilfe der Java Sound API ist es darüber hinaus möglich, nicht nur den eingebauten Synthesizer zu benutzen, sondern auch andere, an das System angeschlossene, Midi - Geräte. Die führt zu der Frage ... Wie kann man ... andere Midi - Geräte ansprechen ? Um herauszufinden, welche Midi - Geräte überhaupt in einem System zur Verfügung stehen, kann die statische Klasse MidiSystem befragt werden. Sie kann eine Liste mit Informationen über alle verfügbaren Geräte erstellen, auf die man über die Java Sound API Zugriff hat. Informationen über die Eigenschaften eines Midi - Gerätes, werden in sogenannten MidiDevice.info Objekten gespeichert. Dies geschieht wie folgt: MidiDevice.info info[] = MidiSystem.getMidiDeviceInfo(); Im Array info sind nun Informationen über alle verfügbaren Geräte gespeichert. Jedem Gerät ist ein MidiDevice.info Objekt zugeordnet. Um an ein bestimmtes Midi - Gerät zu gelangen, muss ein MidiDevice - Objekt erzeugt werden. Dies geschieht, indem man dem MidiSystem ein MidiDevice.info Objekt übergibt, das die Informationen des gewünschten Objektes enthält. Nehmen wir an, in unserem Array info hätten wir unter Index 2 unser gesuchtes Midi - Gerät gefunden. Um Zugriff auf dieses Gerät zu erhalten, fragen wir beim MidiSystem an. Dies geschieht mit folgender Anweisung: MidiDevice unserDevice = MidiSystem.getMidiDevice(info[2]); Wenn wir Zugriff auf dieses Gerät haben, müssen wir wieder - wie schon im letzten Abschnitt - eine open() Anweisung benutzen, um das Gerät exklusiv für unsere Anwendung zu reservieren: unserDevice.open(); Um nun überhaupt mit dem Gerät arbeiten zu können, benötigen wir von ihm entweder ein Receiver Objekt oder ein Transmitter Objekt. Dies geschieht wiederum mit den schon bekannten Methoden getReceiver() oder getTransmitter(). Nehmen wir einfach an, daß unser Midi - Gerät unserDevice eine Referenz auf einen Synthesizer darstellt. Dann benötigen wir natürlich ein __________________________________________________________________________________________ Sound-Programmierung in Java - 42 - Receiver - Objekt von ihm, damit ein anderer Transmitter Daten an unseren Receiver senden kann. Dies erfolgt wie im vorangegangenen Abschnitt: Receiver unserReceiver = unserDevice.getReceiver(); Sollten wir unser Gerät nicht mehr weiter benötigen, geben wir die Resourcen wieder durch eine close() - Anweisung frei: unserDevice.close(); Dies waren die entscheidenden Schritte, wie man das Midi - System nutzt, um Midi - Geräte von ihm anzufordern und auf einfache Weise eine Midi - Datei auf diesen wiederzugeben. Im nächsten Kapitel beschäftigen wir uns mit der Dateienwiedergabe, sondern damit selbst einzelne Noten auf einem Midi - Gerät wiederzugeben. Zum Schluss, in einer Tabelle zusammengefaßt, alle neuen Methoden, die die Möglichkeit haben, Java Exceptions auszulösen: Methode MidiSystem.getSequencer( ) MidiSystem.getSequencer( ) [MidiDevice – Objekt].open( ) [MidiDevice – Objekt].getTransmitter ( ) MidiDevice – Objekt].getReceiver ( ) Sequence – Konstruktor [Sequencer – Objekt].setSequence( ) MidiSystem.getMidiDevice( ) erzeugte Exception MidiUnavailableException MidiUnavailableException MidiUnavailableException MidiUnavailableException MidiUnavailableException InvalidMidiDataException InvalidMidiDataException, IOException MidiUnavailableException, IllegalArgumentException Kapitel 7 - Tonwiedergabe __________________________________________________________________________________________ Sound-Programmierung in Java - 43 - Dieses Kapitel beschäftigt sich mit der Wiedergabe einzelner Töne bzw. Noten auf Midi - Geräten. Eine praktische Anwendung hierfür wäre ein virtuelles Keyboard. Der User könnte z.B. , über eine Swing - Anwendung, die ein Keyboard zeichnet, auf diesem spielen. Die Problemstellung wäre also zu erkennen, welche Noten gespielt werden sollen, und wie man das Midi - System dazu veranlasst, diese auch wiederzugeben. Die erste Frage soll hier nicht weiter beantwortet werden, da es sich dabei um ein Swing bzw. AWT - Problem handelt. Wir kommen daher zu ... Wie kann man ... einzelne Noten wiedergeben mit dem Midi - System ? Zunächst fordert man ein Synthesizer Objekt bei dem MidiSystem an, oder man erzeugt sich ein MidiDevice wie im vorigen Kapitel beschrieben. Der Einfachheit halber, werden wir hier nur ein Synthesizer vom MidiSystem beziehen. Synthesizer synthesizer = MidiSystem.getSynthesizer(); Haben wir dies erledigt, müssen wir noch den Synthesizer für unsere Anwendung reservieren, indem wir die open() - Methode benutzen. Nun kommt ein uns neues Java Sound API Objekt, das sogenannte MidiChannel Objekt, hinzu. Nach der MIDI - Spezifikation kann ein Synthesizer verschiedene Kanäle - sogenannte Channels besitzen, auf denen er Töne generiert bzw. wiedergibt. Ein MidiChannel Objekt ist also die Umsetzung der Spezifikation in die API Umgebung. Um an die von unserem Synthesizer unterstützten Kanäle zu gelangen, müssen wir folgenden Schritt in unser Programm implementieren: MidiChannel channel[] = synthesizer.getChannels(); Mit Hilfe der Methode [Synthesizer-Objekt].getChannels(); erhält man ein Array von MidiChannel Objekten, die die verfügbaren Kanäle des Synthesizers repräsentieren. Um eine Note wiederzugeben, müssen wir den Kanal, auf dem die Note beim Synthesizer gespielt werden soll, angeben, welche Note gespielt und wie stark diese Note „angeschlagen“ werden soll. Das „Anschlagen“ muß man sich wie bei einem realen Klavier vorstellen. Je stärker die einzelne Klaviertaste gespielt wird, desto lauter erklingt der entsprechende Ton. In der Java Sound API wird dies als Velocity bezeichnet In unserem Programm sieht dieses Vorgehen wie folgt aus: channels[0].noteOn(zu spielende note, velocity); __________________________________________________________________________________________ Sound-Programmierung in Java - 44 - Die zu spielende Note muss als Integer - Wert zwischen 0 und 127 angegeben werden. Dies richtet sich nach der schon erwähnten Midi - Spezifikation. Für eine genaue Auflistung der einzelnen Noten und deren entsprechenden Integer - Werte sei auf [5] verwiesen. Ebenso wie die zu spielende Note, wird der Velocity - Wert als Integer übergeben. Als Gegenstück zum noteOn - Befehl gibt es noch den noteOff - Befehl, den man einem Synthesizer Objekt geben kann: channels[0].noteOff(zu spielende Note); Dieser Befehl signalisiert dem Synthesizer, das die „virtuelle“ Taste nicht mehr gedrückt ist, und er entsprechend reagieren soll. An einem einfachen Beispielprogramm, wollen wir uns nun diese Schritte in der Praxis ansehen. Bei dem Demo handelt es sich um die PlayMidiSelf.java Applikation: import javax.sound.midi.*; public class PlayMidiSelf { static int note = 0; //nimmt zu spielende Note auf static int velocity = 0; //nimmt Tastenanschlagstaerke auf static int duration = 0; //nimmt Zeit auf, fuer die die Taste //gedrueckt bleiben soll static int instrument = 0;//nimmt die Nummer des Instrumentes auf, //auf dem Note gespielt werden soll static Synthesizer synth = null; static MidiChannel[]channels = null; static Soundbank sb = null;//Referenz auf die benutzte Soundbank static Instrument inst [] = null;//Liste der unterstuetzten //Instrumente durch die Soundbank public static void main(String[] args) { if (args.length < 4) throw new IllegalArgumentException("Syntax: java PlayMidiSelf <note number> <velocity> <duration> <instrument>"); try { synth = MidiSystem.getSynthesizer(); } catch (MidiUnavailableException e) { } try __________________________________________________________________________________________ Sound-Programmierung in Java - 45 - { synth.open(); } catch (MidiUnavailableException e) { e.printStackTrace(); System.exit(1); } //bestimme die verfuegbaren Midi-Kanaele channels = synth.getChannels(); //bestimme die Soundbank die der Synthesizer benutzt sb = synth.getDefaultSoundbank(); //bestimme welche Instrumente die Soundbank zur Verfuegung stellt inst = sb.getInstruments(); //passe die ueber Konsole gelesenen Werte an note = Integer.parseInt(args[0]); note = Math.min(127, Math.max(0, note)); velocity = Integer.parseInt(args[1]); velocity = Math.min(127, Math.max(0, velocity)); duration = Integer.parseInt(args[2]); duration = Math.max(0, duration); instrument = Integer.parseInt(args[3]); instrument = Math.max(0,Math.min(instrument,inst.length)); System.out.println("Spiele auf Instrument Nr. "+instrument+" mit der Bezeichnung :"+inst[instrument-1].getName()); //wechsle auf das gewuenschte Instrument channels[0].programChange(instrument); //spiele die gewuenschte Note channels[0].noteOn(note, velocity); //halte die Note ueber die gewuenschte Zeit try { Thread.sleep(duration); } catch (InterruptedException e) { } //signalisiere, das Taste nicht mehr gedrueckt channels[0].noteOff(note); //gib Synthesizer genug Zeit die Note ausklingen zu lassen bevor /Programm beendet wird try __________________________________________________________________________________________ Sound-Programmierung in Java - 46 - { Thread.sleep(1000); } catch (InterruptedException e) { } System.exit(0); } } Das Programm verlangt vom Nutzer vor dem Start auf Konsolenebene als Programmparameter eine Note, die es spielen soll, die Velocity, mit der die Note angespielt wird und wie lange die „virtuelle“ Taste auf dem Synthesizer gehalten werden soll (die duration). Es ist möglich, dem Synthesizer vorzuschreiben, auf welchem virtuellen Instrument er die Note spielen soll. Dieses Instrument kann aus einer Liste, der sogenannten Soundbank, ausgewählt werden; die gewählte Nummer stellt den vierten Parameter dar. Es ist möglich, mit der API eine andere Soundbank in einen Synthesizer zu laden oder nur einzelne Instrumente oder Geräuschefekte aus einer bereits im Synthesizer befindlichen Bank auszutauschen. Diese Punkte gehen aber weit über den knappen Rahmen dieses Tutorials hinaus. Von daher sei auf [2] oder [3] verwiesen. Im weiteren Verlauf benutzen wir, daher nur die bereits im Synthesizer befindliche Soundbank und deren Instrumente und Geräuschefekte. Nun aber wieder zu unserem Programm und dessen Aufbau. Zu Beginn wird ein Synthesizer Objekt vom MidiSystem angefordert auf dem später dann, die vom User bestimmte Note gespielt wird. Dann wird dieses Synthesizer Objekt durch eine open() Anweisung für unsere Anwendung reserviert und anschließend bestimmt, welche Kanäle zur Verfügung gestellt werden. Als neuen Schritt finden wir die getDefaultSoundbank() Anweisung, die das Format Soundbank unsereSB = [Synthesizer-Objekt].getDefaultSoundbank(); hat. Mit Hilfe dieser Methode bestimmen wir die aktuell im Synthesizer befindliche Soundbank. Mit der Soundbank unsereSB haben wir nun die Möglichkeit, uns eine Liste von den Instrumenten erstellen zu lassen, die momentan in unserem Synthesizer verfügbar sind. Die Instrumente einer Soundbank werden in der Java Sound API durch die Instrument Objekte repräsentiert. Wir erstellen also nun durch inst = sb.getInstruments(); ein Array von Instrument Objekten mit den Namen inst. In den darauffolgenden Zeilen werden die von der Konsolenebene übergebenen Parameter in ein bestimmtes Format gebracht. __________________________________________________________________________________________ Sound-Programmierung in Java - 47 - Wenn dies erfolgt ist, wird der Syntesizer angewiesen, auf dem Kanal, über den später die Note gespielt werden soll, auf das vom Nutzer gewünschte Instrument zu wechseln: channels[0].programChange(instrument); Man benutzt also die programChange Methode eines MidiChannel Objektes, die folgendes Format hat: [MidiChannel - Objekt].programChange(int-Wert der Nummer von gewünschten Instrument entspricht); Hat man das Programm des Synthesizers auf das gewünschte Instrument umgestellt, erfolgen die schon besprochenen noteOn() und noteOff() - Anweisungen an den Synthesizer, die das Anspielen der Note auslösen. An diesem kleinen Beispiel lässt sich erkennen, wie eine größere Applikation aussehen könnte, die ein Midi - Keyboard simuliert, auf dem gespielt und zwischen verschiedenen Instrumenten gewählt werden kann. Kapitel 8 - Aufzeichnung von Midi-Informationen __________________________________________________________________________________________ Sound-Programmierung in Java - 48 - Dieses Kapitel beschäftigt sich mit der Aufnahme von Midistücken und dem Sichern des Stückes auf der Festplatte. Wie kann man ... eigene Midistücke als eigenen Miditrack aufzeichnen ? Wir wollen im folgenden zeigen, wie man eine eigene Sequence aufzeichnet, die dann von jedem anderen Midi - Gerät abgespielt werden kann. Als einleitenden Schritt fordern wir beim MidiSystem zunächst ein Sequencer Objekt an. Mit einer getReceiver() Anweisung erzeugen wir ein Receiver Objekt, das unsere Midi – Daten, die aufgezeichnet werden sollen, an den Sequencer weitergibt. Wie man vermuten kann, ist der Sequencer bei dieser Problemstellung also unsere zentrale Klasse und spielt die wichtigste Rolle bei der Aufzeichnung einer Sequence. Bis jetzt haben wir also folgende Schritte in unserem Programm abzuarbeiten: Sequencer sequencer = MidiSystem.getSequencer(); Receiver seqReceiver = sequencer.getReceiver(); Nach diesen - für uns nicht neuen - Schritten, müssen wir nun ein eigenes Sequence - Objekt erzeugen. Sequence Objekte kennen wir schon aus dem Kapitel 6, bei dem wir eine Midi - Datei bzw. die darin enthaltene Sequence an Midi - Informationen wiedergegeben haben. Neu ist hierbei allerdings, daß wir ja unsere eigene Sequence erzeugen wollen. Wir müssen also zunächst ein „leeres“ Sequence Objekt konstruieren, das wir über folgende Anweisung durchführen: Sequence unsereSeq = new Sequence(Sequence.PPQ, 10); Dem Konstruktor müssen wir zwei Midi - spezifische Argumente übergeben: einmal eine sogenannte timing - resolution und andererseits noch ein divisionType. Es sei hier nur soviel erwähnt, daß es zwei verschiedene Verfahren gibt, wie man Midi - Sequencen in Zeiteinheiten aufteilt: das hier verwendete PPQ (Zeiteinheit in Takte pro Viertelnote) oder das SMPTE (Takte pro Frame, ein Format, das aus der Filmindustrie stammt) - Verfahren. Man übergibt dem Sequence - Konstruktor also entweder als ersten Parameter ein Sequence.PPQ oder ein Sequence.X (X ist entweder: SMPTE_24, SMPTE_25, SMPTE_30, oder SMPTE_30DROP) Objekt. Für eine genauere Beschreibung dieser beiden Begriffe sei auch an dieser Stelle wieder auf [3] oder [5] verwiesen. Der zweite Wert, den wir dem Sequence - Konstruktor übergeben müssen, bezieht sich darauf, wieviele Takte pro gewählter Zeiteinheit benutzt werden sollen. In unserem Fall also 10 Takte pro Viertelnote. Auch der nun folgende Schritt ist neu für uns. Midi - Sequencen können verschiedene sogenannte Tracks besitzen, die jeweils parallel von einem Synthesizer abgearbeitet werden. Diese Tracks enthalten unter anderen Informationen darüber, wann der Synthesizer welche Note spielen soll. Um einen Track in einer Sequence zu erzeugen, wird Track track = unsereSeq.createTrack(); __________________________________________________________________________________________ Sound-Programmierung in Java - 49 - ausgeführt. Jetzt wird der Sequencer angewiesen, unsere Sequence zu bearbeiten, in dem wir sequencer.setSequence(unsereSeq); ausführen. Wenn wir nun sequencer.recordEnable(track, -1); in unserem Programm abarbeiten lassen, versetzen wir den Sequencer in Aufnahmebereitschaft. Um dies zu erreichen, müssen wir ihm mitteilen, auf welchem Track er in der aktuellen Sequence bearbeitet die Aufnahme durchführen soll und auf welchem Kanal die zu speichernden Midi Informationen herein-kommen werden. Übergibt man hier eine -1, wird jeder zur Verfügung stehende Kanal benutzt, d.h. alle Midi – Informationen, die der Sequencer auf seinen Kanälen erhält, werden in die Sequence geschrieben; gibt man die Nummer eines bestimmten Kanals an, werden nur die Informationen, die den Sequencer dort erreichen, auch wirklich in die Sequence geschrieben. Nach diesen umfangreicheren Befehlen, erfolgt nun der wesentlich einfachere sequencer.startRecording(); und die Aufnahme beginnt, d.h. alle Informationen, die nun den Sequencer erreichen, werden auch in die Sequence geschrieben. Um die Aufnahme zu stoppen, wird einfach sequencer.stopRecording(); ausgeführt. Zuletzt bleibt noch, die Sequence auch wirklich auf einer Platte zu sichern. Hierzu bedient man sich der write() - Methode, die die MidiSystem - Klasse zur Verfügung stellt. MidiSystem.write(unsereSeq, 0, File - Objekt); Als ersten Parameter, erwartet die Methode die zu schreibende Sequence, als zweiten einen Integer Wert, der den Midifile - Typ bestimmt (für Näheres über die verschiedenen Midifiletypen siehe [5]) und schliesslich ein File Objekt, das die Zieldatei spezifiziert. Nachdem wir gesehen haben, wie man eine Sequence erzeugen und diese als Midi - File schreiben kann, bleibt die Frage, wie man überhaupt die Informationen über die zu spielenden Noten in eine Sequence eingebracht werden. Daher kommen wir zu ... Wie kann man ... eigene Noten in eine Sequence schreiben ? Um diese Frage zu lösen, benötigen wir sogenannte MidiMessages. Diese MidiMessages sind nach der Midi - Spezifikation wie folgt definiert. Es gibt drei verschiedene Gruppen von Messages, nämlich: 1. Short - Message __________________________________________________________________________________________ Sound-Programmierung in Java - 50 - 2. Sysex - Message 3. Meta - Message Wir werden hier nur die erste Art benutzen. Für eine Übersicht über die anderen Arten sei auch hier wieder auf [5] und in knapper Form auf [3] verwiesen. Kommen wir zu den Short - Messages. In einer solchen Nachricht sind Midi - Daten (z.B. „spiele Note x mit dem Anschlag y“) gekapselt. Diese Eigenschaft werden wir ausnutzen und somit eine Möglichkeit haben, in unsere Sequence eine Abfolge von Short - Messages zu schreiben, in denen unsere Noten gekapselt sind. Eine Short - Message, wird in der Java Sound API durch sogenannte ShortMessage Objekte repräsentiert. Bei der Erstellung einer Short - Message verfährt man wie folgt: ShortMessage meineNote = new ShortMessage(); erzeugt zunächst einmal ein „leeres“ ShortMessage Objekt, das noch keinerlei Midi - Informationen enthält. Diese gelangen erst durch meineNote.setMessage(ShortMessage.NOTE_ON, 0, 60, 93); in das Objekt. setMessage erwartet als ersten Parameter ein Kommando (die wichtigsten wären ShortMessage.NOTE_ON, ShortMessage.NOTE_OFF), als zweiten die Nummer des Midi – Kanals, auf den die Message bei einem Gerät gesendet werden soll, drittens die zu spielende Note als Integer - Wert (hier die 60, die einem mittleren C entspricht) und schliesslich einen Integer, der bestimmt, wie stark die Note auf dem virtuellen Keyboad angeschlagen werden soll, also die Velocity. Um nun eine solche Midi - Nachricht an ein Midi - Gerät zu schicken, benötigen wir ein dem Gerät zugeordnetes Receiver - Objekt und benutzen die Receiver - Methode send(). Konkret sieht dies wie folgt aus: [Receiver-Obj. des Gerätes zu dem Nachricht geschickt wird].send(shortM,time); shortM ist hierbei die zu sendende Short - Message bzw. das entsprechende Java Objekt. Noch neu für uns ist der Wert time. Dieser im long - Format vorliegende Wert gibt den sogenannten Zeitstempel der Midi - Nachricht an. Zeitstempel bedeutet, daß einer Midi - Nachricht eine Zeit zugeordnet werden kann zu der das Midi - Gerät die Nachricht verarbeiten soll. Setzt man diesen Wert auf eine -1, wird dem Gerät überlassen, wann es die Nachricht verarbeitet; eine genaue Zeit wird also nicht vorgeschrieben. Näheres zu den Zeitstempeln bei Midi - Messages findet man wieder unter [5] und [3]. In unseren einfachen Beispielen werden wir nur den Wert -1 benutzen und es somit dem Gerät überlassen, wann es die Nachricht verarbeitet. __________________________________________________________________________________________ Sound-Programmierung in Java - 51 - Nach diesen theoretischen Aspekten kommen wir nun zu einem praktischen Beispiel, in dem alle - in diesem Kapitel neu hinzugekommenen - Sachverhalte angewendet werden. Bei diesem Programm handelt es sich um die RecMidi.java Applikation: import javax.sound.midi.*; import java.io.File; import java.io.IOException; class RecMidi { static Sequencer sequencer = null; static Receiver seqReceiver = null; static ShortMessage message = null; static Sequence newSequence = null; static Track track = null; static void main(String args[]) { try { sequencer = MidiSystem.getSequencer(); sequencer.open(); seqReceiver = sequencer.getReceiver(); }catch(MidiUnavailableException e) {System.out.println("Synthesizer nicht verfuegbar!"); System.exit(0);} try { //erzeuge unsere eigene Sequence in die wir später unsere Noten schreiben newSequence = new Sequence(Sequence.PPQ, 10); //erzeuge einen Track in dieser Sequence der unsere Noten aufnimmt track = newSequence.createTrack(); //übergib dem Sequencer unsere Sequence als seine aktuelle Sequence sequencer.setSequence(newSequence); }catch(InvalidMidiDataException e){System.out.println("Kein gueltiges MidiFormat!");System.exit(0);} //Sequencer vorbereiten für Aufnahme sequencer.recordEnable(track, 0); //beginne die Aufnahme sequencer.startRecording(); __________________________________________________________________________________________ Sound-Programmierung in Java - 52 - // erzeuge nun die MidiMessages die aufgenommen werden sollen // und schicke diese zum Sequencer damit dieser sie aufnimmt in Sequence message = new ShortMessage(); try { message.setMessage(ShortMessage.NOTE_ON, 0, 60, 93); seqReceiver.send(message, -1); message.setMessage(ShortMessage.NOTE_OFF, 0, 60, 93); seqReceiver.send(message, -1); message.setMessage(ShortMessage.NOTE_ON, 0, 62, 93); seqReceiver.send(message, -1); message.setMessage(ShortMessage.NOTE_OFF, 0, 62, 93); seqReceiver.send(message, -1); message.setMessage(ShortMessage.NOTE_ON, 0, 64, 93); seqReceiver.send(message, -1); message.setMessage(ShortMessage.NOTE_OFF, 0, 64, 93); seqReceiver.send(message, -1); }catch(InvalidMidiDataException e) {System.out.println("MidiDaten nicht korrekt!"); System.exit(0);} //Aufnahme beendet sequencer.stopRecording(); //Erzeuge File - Objekt das Zieldatei bestimmt File file = new File("my.mid"); //schreibe Midi - File auf Platte try { MidiSystem.write(newSequence, 0, file); }catch(IOException e){System.out.println("Fehler beim Schreiben des MidiFiles!");System.exit(1);} //stoppe den Sequencer sequencer.stop(); try { Thread.sleep(5000); } catch (InterruptedException e) { } System.exit(0); } __________________________________________________________________________________________ Sound-Programmierung in Java - 53 - } Das Programm erzeugt eine kleine Midi - Datei „my.mid“. In dieser Datei ist eine kleine Sequence enthalten, die eine Abfolge von drei Tönen auf einem Track beinhaltet. Begonnen wird mit dem mittleren C (die Note 60), dann die Note 62 und schliesslich 64. Schauen wir uns das Programm nun genauer an. Zu Beginn werden die schon vertrauten Dinge veranlasst. Man fordert einen Sequencer bei dem MidiSystem an, fordert von diesem wiederum einen Receiver an, über den wir später unsere Midi Messages an den Sequencer schicken können. Daraufhin erzeugen wir eine Sequence, in die der Track eingebettet ist, der unsere Noten aufnimmt: newSequence = new Sequence(Sequence.PPQ, 10); Wir können sehen, daß in dem Beispiel das „Takte - Pro - Viertelnote - Zeiteinheiten“ - Verfahren benutzt wird (PPQ), und daß pro Viertelnote 10 Takte zu erfolgen haben. Im Anschluss daran, wird ein Track in unserer Sequence erzeugt track = newSequence.createTrack(); und die Sequence an den Sequencer übergeben: sequencer.setSequence(newSequence); Die nächsten Schritte versetzen den Sequencer in Aufnahmebereitschaft: sequencer.recordEnable(track, 0); sequencer.startRecording(); Er wird angewiesen, nur die Midi – Informationen, die auf seinem Kanal 0 eintreffen (bestimmt durch den zweiten Integer - Parameter), in unsere Sequence und speziell dort im Track track aufzunehmen. Im nächsten Anweisungsblock, werden unsere ShortMessage - Objekte erzeugt, die über den Receiver seqReceiver und dessen send() - Methode an den Sequencer geschickt werden und von diesem in unsere Sequence bzw. in unseren Track übernommen werden. Zum Schluss wird die Aufnahme durch sequencer.stopRecording(); gestoppt; was noch bleibt, ist, unser kleines Midi - Stück in eine Datei mit dem Namen „my.mid“ zu schreiben. Dies übernehmen folgende Anweisungen: File file = new File("my.mid"); MidiSystem.write(newSequence, 0, file); Zum Schluss wird dem System noch die Zeit gegeben, die Aufgabe zu erfüllen, bevor der Sequencer gestoppt und das Programm beendet wird. __________________________________________________________________________________________ Sound-Programmierung in Java - 54 - Dieses kleine Beispielprogramm sollte zeigen, wie man eigene Midi - Stücke aufnehmen kann und diese anschließend in eine Midi - Datei schreibt. Eine Anwendung hierfür wäre beispielsweise unsere schon erwähnte Swing - Applikation, bei der ein User auf einem „virtuellen“ Keyboard spielen kann. Vor-stellbar wäre diesen Ansatz um die Möglichkeit zu erweitern, sein Spiel auf dem Keyboard als Midi - Datei auf der Festplatte zu konservieren. Bei den in diesem Kapitel neu hinzugekommenen Methoden die die Möglichkeit haben, Java Exceptions zu erzeugen, handelt es sich um: Methode erzeugte Exception [Sequencer – Objekt].recordEnable( ) MidiSystem.write( ) IllegalArgumentException IllegalArgumentException, IOException IllegalArgumentException IllegalStateException [MidiMessage – Objekt].setMessage( ) [Receiver – Objekt].send( ) Mit dem Schluss des Teils II haben wir die kurze Einführung zum Thema Java Sound API beendet. In Teil III wird kurz auf eine weitere Möglichkeit eingegangen Java zur Soundgenerierung zu benutzen: das sogenannte Java Media Framework TEIL III Java Media Framework __________________________________________________________________________________________ Sound-Programmierung in Java - 55 - (javax.media.*) Kapitel 9 - Musik - Player : Die Grundfunktion Das Java Media Framework ist durch die Unterstützung von fast allen gängigen Audio- und Videoformaten relativ einfach als universeller Player nutzbar. Wenn man im Internet nach Beispielen __________________________________________________________________________________________ Sound-Programmierung in Java - 56 - für die Nutzung des Java Media Framework sucht, wird man fast nur Applets oder mit Unterstützung von Swing erstellte Programme finden. Der Grund liegt darin, daß es direkt im Java Media Framework Klassen für eine grafische Benutzeroberfläche gibt. Im folgenden wird vor allem die Funktion von Java Media Framework als Musik - Player betrachtet. Dies wird aber der Übersichtlichkeit halber nicht mit Hilfe der grafischen Benutzeroberfläche gezeigt. In den Kapiteln 9.1 bis 9.5 wird das Grundgerüst des in Kapitel 9.6 abgedruckten Players schrittweise beschrieben. Im Kapitel 10 wird beschrieben, wie mit dem Java Media Framework eigene Musikstücke aufgenommen werden können. Der Umgang mit der grafischen Benutzeroberfläche wird mit Hilfe eines Beispielprogrammes von SUN in Kapitel 11 erläutert. Einige interessante Möglichkeiten, die das Java Media Framework noch bietet, werden kurz im Kapitel 12 beschrieben. In Anhang B finden sich die hier im Text benutzten Quellen. Kapitel 9.1 - Initialisierung Um einen Player zu erzeugen, muß man zuerst eine Variable vom Type Player definieren Player wplayer = null und danach den Player initialisieren wplayer = Manager.createPlayer(url) Falls der Player hier aus irgendeinem Grund nicht initialisiert werden konnte, wird NoPlayerException zurückgegeben. Außerdem kann es eine IOException auftreten, da der Player versucht, die in einer Url stehende Datei zu öffnen. Wie man sehen kann, muß der Player mit einer Url aufgerufen werden. Um die Url zu erzeugen, benötigt man nur einen Dateinamen oder direkt eine Internet-Adresse. Wir gehen im folgenden von der Situation aus, daß wir eine Datei von der Festplatte abspielen möchten. Bei der Eingabe eines Dateinamens mit Pfadangabe, ist es nur wichtig zu wissen, daß anstatt des in DOS üblichen Backslash nur ein Slash benutzt wird. Da die Datei auf der Festplatte liegt, muß vor dem Dateinamen noch ein __________________________________________________________________________________________ Sound-Programmierung in Java - 57 - "file:" eingefügt werden. Nun muß die Url (hier als src bezeichnet) nur noch in ein für den Player gültiges Format geändert werden url = new URL(src) Hierbei kann es zu einer MalformedURLException kommen, falls die übergebene Adresse falsch war. Bevor der Player gestartet werden kann, müssen noch die Befehle wplayer.realize() und wplayer.prefetch() ausgeführt werden. Diese Befehle sorgen dafür, daß die Variablen, Puffer, usw. für den Player eingerichtet werden. Mit Hilfe des in Kapitel 12 beschriebenen ControllerListener ist es möglich, zu erfahren, wann realize und prefetch abgeschlossen sind, da RealizeCompleteEvent und PrefetchCompleteEvent beim Beenden von realize und prefetch ausgelöst werden. Neben der hier beschriebenen Methode createPlayer() gibt es auch noch die Methode createProcessor(), die es erlaubt, Musik nicht nur abzuspielen, sondern vorher auch noch zu bearbeiten. Solange das Java Media Framework nur zum Abspielen von Musik benutzt wird, sollte createPlayer() reichen. Kapitel 9.2 - Play, Stop Nachdem der Player initialisiert wurde, kann jetzt begonnen werden, das bei der Initialisierung angegebene Musikstück abzuspielen. Um den Player zu starten, genügt es einfach wplayer.start() aufrufen. Um das Abspielen zu stoppen, reicht wplayer.stop(). Damit der Player nicht unnötig viele Ressourcen nutzt, ist es besser nach dem Befehl wplayer.stop() die belegten Ressourcen mit wplayer.deallocate() wieder frei zu geben. Dies sollte aber auf jeden Fall beim Beenden des Players geschehen (siehe Kapitel 9.5). Wenn der Player mit wplayer.stop() angehalten wurde, kann mit dem Befehl wplayer.start() an der gleichen Stelle im Musikstück das Abspielen fortgesetzt werden. Kapitel 9.3 - Aktuelle Spielzeit auslesen, neue Spielzeit setzen __________________________________________________________________________________________ Sound-Programmierung in Java - 58 - Mit Time AktuelleZeit = wplayer.getMediaTime() kann die aktuelle Spielzeit vom Musikstück ausgelesen werden. Mit wplayer.setMediaTime(new Time(0)) kann man z.B. die aktuelle Spielzeit auf den Anfang des Musikstückes setzen. Die Befehle setMediaTime und getMediaTime können aufgerufen werden, während der Player ein Musikstück abspielt. Bei setMediaTime springt der Player dann sofort zu der angegebenen Stelle und spielt die Musik weiter ab. Um die Gesamtspiellänge zu ermitteln, kann man Time Gesamtlaenge = wplayer.getDuration() benutzen. Kapitel 9.4 - Schneller Vorlauf Bei einigen Formaten ist Java Media Framework sogar in der Lage, Dateien in doppelter Geschwindigkeit abzuspielen. Die aktuelle Spielgeschwindigkeit kann man mit getRate() abgefragt werden. Wenn man aber die Geschwindigkeit mit setRate((int) Geschwindigkeit) setzen will, muß vorher sichergestellt sein, daß der Player gerade kein Stück abspielt. Das Abspielen mit doppelter Geschwindigkeit ist z.B. bei *.wav ,*.au ,*.aif möglich. Nicht möglich ist es dagegen bei *.mp3. Kapitel 9.5 - Player schließen Um den Player zu schließen, sollte man am besten folgendermaßen vorgehen : - Player stoppen __________________________________________________________________________________________ Sound-Programmierung in Java - 59 - - Ressourcen freigeben Player schließen Wie schon in Kapitel 1.2 beschrieben, wird der Player mit stop() gestoppt und die Ressourcen werden mit deallocate() freigegeben. Um den Player endgültig zu beenden, genügt der Befehl close(). Kapitel 9.6 - Ein Beispielprogramm Dieses Beispielprogramm zeigt einen Musik-Player, der die Grundfunktionen Start, Stop, Zurücksetzen der aktuellen Zeit auf Null und das Einstellen der Wiedergabe mit doppelter Geschwindigkeit beherrscht. Das Programm wurde mit folgenden Dateitypen getestet : *.wav, *.mp3, *.au, *.aif Das Programm ist wie folgt aufzurufen : java WavPlayer <Musikdatei> import javax.media.*; // JMF import javax.media.protocol.*; // JMF import java.io.*; // für Bildschirmausgabe... import java.net.*; // URL... public class WavPlayer { Player wplayer = null; public static void main (String args[]) { if (args.length == 0) throw new IllegalArgumentException ("Fehler : Keine Datei angegeben! z.B. WavPlayer test.wav"); WavPlayer wplay = new WavPlayer(args[0]); } private void Menu() __________________________________________________________________________________________ Sound-Programmierung in Java - 60 - { System.out.println("Funktionen :"); System.out.println(" e = Ende"); System.out.println(" s = Stop"); System.out.println(" a = Start"); System.out.println(" o = Startposition 0"); System.out.println(" r = Rate 1 oder 2"); } public WavPlayer(String datei) { String src = "file:"+datei;// Dateinamen für lokale Festplatte ändern URL url = null; // Datei - Adresse ändern ; Player initialisieren try { // Den gesamten Dateinamen in url - Format umändern if ((url = new URL(src)) == null) { System.out.println("Fehler URL=null ;"+src+";"+url); System.out.println("Bitte die Schreibweise überprüfen"); System.out.println("z.B. WavPlayer test.wav oder WavPlayer e:/test.wav"); return; } try { // Player erzeugen wplayer = Manager.createPlayer(url); } catch (NoPlayerException e) { System.out.println("Fehler - create Player"); } } catch (MalformedURLException e) { // Fehler bei url - Erstellung System.out.println("Fehler URL : "+datei); } catch (IOException e) { System.out.println("Fehler IO"); } wplayer.realize(); wplayer.prefetch(); // Player ist bereit ... char Taste = 'b'; // dummy - Taste zur Initialisierung float Rate; // Programmschleife Menu(); __________________________________________________________________________________________ Sound-Programmierung in Java - 61 - do { try { Taste = (char) System.in.read(); } catch (IOException e) { System.out.println("Fehler beim einlesen der Taste"); System.exit(2); } switch(Taste) { case 's' : wplayer.stop();wplayer.deallocate();break; case 'a' : { wplayer.start(); break; } case 'r' : { wplayer.stop(); //Rate kann nicht geändert werden, wenn der player läuft Rate = wplayer.getRate(); System.out.println("aktuelle - Rate : " + Rate); if (Rate == 1.0) { Rate = (float) 2.0; } else { Rate = (float) 1.0; } System.out.println("neue - Rate : " + wplayer.setRate(Rate)); System.out.println("Falls sich der Player jetzt ausgeschaltet hat, bitte a für Start eingeben"); break; } case 'o' : wplayer.setMediaTime(new Time(0));break; } } while (Taste != 'e'); // Player - Ende wplayer.stop(); // falls der Player nicht manuell beendet wurde wplayer.deallocate(); wplayer.close(); System.exit(0); } } __________________________________________________________________________________________ Sound-Programmierung in Java - 62 - Kapitel 10 - Aufnahme von Musikstücken Dieses Kapitel bezieht sich auf das Programm SoundRecorder.java (und den dazugehörigen Programmteil StateHelper.java) im Anhang A. Es ist noch zu erwähnen, daß dieses Programm aus den Hilfe-Dateien von SUN [6] herauskopiert ist. Leider war es nicht möglich, das Programm auszuführen. __________________________________________________________________________________________ Sound-Programmierung in Java - 63 - Zuerst muß man ein Eingabegerät mit Vector deviceList = CaptureDeviceManager.getDeviceList (new AudioFormat(AudioFormat.LINEAR, 44100, 16, 2)) auswählen. Hier werden die Geräte ausgesucht, die eine Samplerate von 44 kHz mit 16 Bit und Stereo beherrschen. Als nächstes muß ein Gerät ausgesucht werden. Für das Mikrofon als Eingabegerät muß nun mit CaptureDeviceInfo di = (CaptureDeviceInfo) deviceList.firstElement() ein Player, wie in Kapitel 9.1 beschrieben, initialisiert werden. Anstatt der Url gibt man hier den oben erzeugten MediaLocator ein. Der Aufruf sieht also wie folgt aus : Player p = Manager.createPlayer(di.getLocator()) Mit p.start() beginnt der Player mit der Aufnahme und endet mit p.stop(). Es stellt sich nun die Frage , wie die Aufnahme gespeichert werden kann. Für diesen Fall gibt es DataSink. Mit Hilfe von DataSink kann man aufgenommene Musikstücke relativ einfach auf der Festplatte speichern. Um die Daten zu speichern, die der Player jetzt vom Mikrofon bekommt, muß zuerst der Dateityp mit p.setContentDescriptor(new FileTypeDescriptor(FileTypeDescriptor.WAVE)) festgelegt werden (hier *.wav). Danach wird die Ausgabe des Players mit DataSource source = p.getDataOutput() geholt. Als letzte Variable fehlt jetzt noch der Dateiname, unter dem die Daten gespeichert werden sollen, und der mit MediaLocator dest = newMediaLocator(Dateiname) gesetzt wird. Nun wird der DataSink mit DataSink filewriter = Manager.createDataSink(source, dest) erzeugt. Nachdem der DataSink mit open() geöffnet wurde, kann man mit start() die Aufnahme beginnen und mit stop() beenden. Mit close() wird der DataSink wieder geschlossen. Um das Erzeugen zu großer Dateien zu verhindern kann mit StreamWriterControl swc = (StreamWriterControl) p.getControl ("javax.media.control.StreamWriterControl"); If (swc != null) swc.setStreamSizeLimit(größe in Byte); die maximale Dateigröße festgelegt werden. __________________________________________________________________________________________ Sound-Programmierung in Java - 64 - Kapitel 11 - Musik-Player und Swing Kapitel 11.1 - Grafische Oberfläche __________________________________________________________________________________________ Sound-Programmierung in Java - 65 - Wie bereits in Kapitel 9 beschrieben, gibt es die Möglichkeit mit einer grafischen Oberfläche zu arbeiten. Das Aussehen läßt sich mit Hilfe von java.awt.* (AWT Layout Manager) steuern. Die Einstellungen lassen sich durch Component visual = wplayer.getVisualComponent() verändern; so kann z.B. durch visual.width und visual.height die Breite und Höhe des Players verändert werden. Weiterhin besteht die Möglichkeit, ein Control Panel einzurichten. Dafür muß man Component control = wplayer.getControlPanelComponent() mit getContentPane().add("South", control) zum Fenster hinzufügen. In diesem Control Panel sind standardmäßig Buttons für Start, Stop, Pause und eine Laufleiste für die aktuelle Wiedergabe - Position im Musikstück vorhanden. Falls ein Musikstück z.B. zuerst noch aus dem Internet heruntergeladen werden muß, kann mit getCachingControl und getProgressBar noch eine Anzeige des Download – Fortschritts erzeugt werden. Es ist außerdem möglich, dem Fenster mit getGainControl einen Lautstärkeregler hinzuzufügen. Kapitel 11.2 - Ein Beispielprogramm MDIApp.java : Dies ist ein Beispielprogramm, das aus den Internet-Seiten von SUN [7] entnommen ist. Es zeigt einen Sound-Player unter Swing. import javax.media.*; import com.sun.media.ui.*; import javax.media.protocol.*; import javax.swing.*; import javax.swing.event.*; import java.awt.*; import java.awt.event.*; import java.net.*; import java.io.*; __________________________________________________________________________________________ Sound-Programmierung in Java - 66 - import java.util.Vector; public class MDIApp extends Frame { /************************************************************************* * MAIN PROGRAM / STATIC METHODS *************************************************************************/ public static void main(String args[]) { MDIApp mdi = new MDIApp(); } static void Fatal(String s) { MessageBox mb = new MessageBox("JMF Error", s); } /************************************************************************* * VARIABLES *************************************************************************/ JMFrame jmframe = null; JDesktopPane desktop; FileDialog fd = null; CheckboxMenuItem cbAutoLoop = null; Player player = null; Player newPlayer = null; String filename; /************************************************************************* * METHODS *************************************************************************/ public MDIApp() { super("Java Media Player"); // Add the desktop pane setLayout( new BorderLayout() ); desktop = new JDesktopPane(); __________________________________________________________________________________________ Sound-Programmierung in Java - 67 - desktop.setDoubleBuffered(true); add("Center", desktop); setMenuBar(createMenuBar()); setSize(640, 480); setVisible(true); try { UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel"); } catch (Exception e) { System.err.println("Could not initialize java.awt Metal lnf"); } addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent we) { System.exit(0); } } ); Manager.setHint(Manager.LIGHTWEIGHT_RENDERER, new Boolean(true)); } private MenuBar createMenuBar() { ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent ae) { String command = ae.getActionCommand(); if (command.equals("Open")) { if (fd == null) { fd = new FileDialog(MDIApp.this, "Open File", FileDialog.LOAD); fd.setDirectory("/movies"); } fd.show(); if (fd.getFile() != null) { String filename = fd.getDirectory() + fd.getFile(); openFile("file:" + filename); } } else if (command.equals("Exit")) { dispose(); System.exit(0); } } }; __________________________________________________________________________________________ Sound-Programmierung in Java - 68 - MenuItem item; MenuBar mb = new MenuBar(); // File Menu Menu mnFile = new Menu("File"); mnFile.add(item = new MenuItem("Open")); item.addActionListener(al); mnFile.add(item = new MenuItem("Exit")); item.addActionListener(al); // Options Menu Menu mnOptions = new Menu("Options"); cbAutoLoop = new CheckboxMenuItem("Auto replay"); cbAutoLoop.setState(true); mnOptions.add(cbAutoLoop); mb.add(mnFile); mb.add(mnOptions); return mb; } /** * Open a media file. */ public void openFile(String filename) { String mediaFile = filename; Player player = null; // URL for our media file URL url = null; try { // Create an url from the file name and the url to the // document containing this applet. if ((url = new URL(mediaFile)) == null) { Fatal("Can't build URL for " + mediaFile); return; } // Create an instance of a player for this media try { player = Manager.createPlayer(url); } catch (NoPlayerException e) { Fatal("Error: " + e); } __________________________________________________________________________________________ Sound-Programmierung in Java - 69 - } catch (MalformedURLException e) { Fatal("Error:" + e); } catch (IOException e) { Fatal("Error:" + e); } if (player != null) { this.filename = filename; JMFrame jmframe = new JMFrame(player, filename); desktop.add(jmframe); } } } class JMFrame extends JInternalFrame implements ControllerListener { Player mplayer; Component visual = null; Component control = null; int videoWidth = 0; int videoHeight = 0; int controlHeight = 30; int insetWidth = 10; int insetHeight = 30; boolean firstTime = true; public JMFrame(Player player, String title) { super(title, true, true, true, true); getContentPane().setLayout( new BorderLayout() ); setSize(320, 10); setLocation(50, 50); setVisible(true); mplayer = player; mplayer.addControllerListener((ControllerListener) this); mplayer.realize(); addInternalFrameListener( new InternalFrameAdapter() { public void internalFrameClosing(InternalFrameEvent ife) { mplayer.close(); } } ); } public void controllerUpdate(ControllerEvent ce) { if (ce instanceof RealizeCompleteEvent) { __________________________________________________________________________________________ Sound-Programmierung in Java - 70 - mplayer.prefetch(); } else if (ce instanceof PrefetchCompleteEvent) { if (visual != null) return; if ((visual = mplayer.getVisualComponent()) != null) { Dimension size = visual.getPreferredSize(); videoWidth = size.width; videoHeight = size.height; getContentPane().add("Center", visual); } else videoWidth = 320; if ((control = mplayer.getControlPanelComponent()) != null) { controlHeight = control.getPreferredSize().height; getContentPane().add("South", control); } setSize(videoWidth + insetWidth, videoHeight + controlHeight + insetHeight); validate(); mplayer.start(); } else if (ce instanceof EndOfMediaEvent) { mplayer.setMediaTime(new Time(0)); mplayer.start(); } } } Kapitel 12 - Weitere ausgewählte Funktionen Es gibt noch einige interessante Funktionen des Java Media Framework, die hier zumindest noch Erwähnung finden sollten. Wenn beispielsweise der Player nach einer bestimmten Zeit aufhören soll, ein Musikstück wiederzugeben, so kann dazu setStopTime(Zeit) benutzt werden. Manchmal ist es auch nötig mehrere Musikstücke gleichzeitig abzuspielen; in diesem Fall ist es ratsam, die beiden Player zu synchronisieren. Dies geschieht mit getTimeBase, setTimeBase und syncStart. In Kapitel 9.1 wurde schon vom ControllerListener gesprochen. Nun zu der Frage, wofür der denn da ist. Nicht nur realize und prefetch lösen Events aus, sondern fast alles hier __________________________________________________________________________________________ Sound-Programmierung in Java - 71 - Beschrieben kann Events auslösen. Wenn ein Musikstück zu ende ist, wird z.B. EndOfMediaEvent ausgelöst. Dies kann dazu benutzt werden, wieder an den Anfang zurückzuspringen. Weitere Events sind : CachingControlEvent (siehe oben in diesem Kapitel); StartEvent (nach dem Start der Wiedergabe); StopByRequestEvent (falls versucht wird, einen bereits gestoppten Player noch mal zu stoppen); ControllerColsedEvent (falls der Controller mit close() geschlossen wurde); uvm. Anhang A Programmbeispiele __________________________________________________________________________________________ Sound-Programmierung in Java - 72 - In diesem Anhang finden sich alle Beispiele, die aus Platzmangel im Fliesstext keine Berücksichtigung finden konnten. Das erste Beispielprogramm, ist das Programm PlaySoundStreamedT.java aus Kapitel 2 Soundgenerierung import javax.sound.sampled.*; import javax.sound.midi.*; import java.io.*; class PlaySoundStreamedT extends Thread { static File file = null; // die zu streamende Datei static AudioInputStream stream = null;//ueber diesen Stream werden //die AudioDaten aus der Datei //"file" gelesen static AudioFormat af = null; // gibt das AudioFormat der Datei "file" __________________________________________________________________________________________ Sound-Programmierung in Java - 73 - //an static SourceDataLine sl = null;// ueber diese DataLine wird die //Datei "file" abgespielt static byte ba[] = null;// Array enthaelt die Sounddateibytes die //aktuell aus dem AudioInputStream "stream" //gelesen wurden und dann ueber die //SourceDataLine "sl" abgespielt werden static int numBytesRead = 0;// Anzahl der gelesenen Bytes aus dem //Soundfile static long frames = 0, // Anzahl der Frames aus der die //Sounddatei besteht die in "file" //angegeben ist bSize = 0; // Groesse der Sounddatei die in "file" //angegeben ist in Bytes static PlaySoundStreamedT pss = null; static boolean play = true; public PlaySoundStreamedT(File fi) { // Erzeuge den InputStream auf die Datei "file" try { stream = AudioSystem.getAudioInputStream(file); }catch(UnsupportedAudioFileException e){System.out.println("Die gewaehlte Sounddatei wird nicht unterstuetzt!"); System.exit(1);} catch(IOException e2){System.out.println("Fehler beim Erzeugen des AudioInputStreams");System.exit(1);} //bestimme das Format der abzuspielenden Datei af = stream.getFormat(); // bestimmt die Länge der Sounddatei in Frames frames = stream.getFrameLength(); // bestimmt die Länge der Sounddatei in Bytes bSize = frames * af.getFrameSize(); System.out.println("Die Dateigroesse betraegt:"+bSize+" in Bytes"); //erzeuge DataLine.info die spezifiziert welche Art von Eingabe man haben //will beim AudioSystem __________________________________________________________________________________________ Sound-Programmierung in Java - 74 - DataLine.Info info = new DataLine.Info (SourceDataLine.class,af); //versuche mit Hilfe von "info" die gewünschte Eingabe beim AudioSystem zu //bekommen try{ sl = (SourceDataLine) AudioSystem.getLine(info); }catch(LineUnavailableException e){System.out.println("Line konnte nicht benutzt werden");System.exit(1);} } //die Hauptmethode der Applikation public void run() { //oeffne und reserviere somit fuer diese Applikation die SourceDataLine try{ sl.open(); }catch(LineUnavailableException e){System.out.println("Fehler beim Öffnen des AudioStreams");System.exit(1);} //starte die Line zum Abspielen des Sounds sl.start(); int ava=0; //lege den Buffer an in den die Sounddateidaten eingelesen werden ueber //die AudioInputStream ba = new byte[1024]; //gib dem User an wieviele Bytes an Audiodaten zu lesen sind try { ava = stream.available(); }catch(IOException e){System.exit(1);} System.out.println("Bytes zu lesen:"+ava); //Spiele Sound while(play) { //lese SounddateiDaten in Buffer ein try { numBytesRead = stream.read(ba,0,1024); }catch(IOException e){System.out.println("Fehler beim Lesen der SounddateiDaten"); System.exit(1);} //wenn Ende der Sounddatei bzw. Streams erreicht beende Schleife __________________________________________________________________________________________ Sound-Programmierung in Java - 75 - if (numBytesRead == -1) break;//Ende der Sounddatei erreicht! //schreibe gelesene Sounddateidaten in SourceDataLine und veranlasse somit //das Abspielen der Daten sl.write(ba,0,ba.length); } // blockiere bis die lezten Daten abgespielt wurden sl.drain(); // halte die Line an und schliesse die Line sl.stop(); sl.close(); sl=null; System.out.println(" ... Fertig!"); return; } // main - Methode der Applikation public static void main (String args[]) { if(args.length == 0) throw new IllegalArgumentException ("Syntax: java PlaySoundStreamedT <file>"); file = new File(args[0]); pss = new PlaySoundStreamedT(file); pss.start(); System.out.println("Spiele wav ..."); System.out.println("Druecke <RETURN> fuer STOP"); try { int c = System.in.read(); }catch(IOException e){System.exit(1);} play = false; System.out.println("...STOP"); System.exit(0); } } Dieses Programm erwartet beim Start als Parameter den Name oder den Pfad der wiederzugebenden Sounddatei. Das Abspielen der Datei kann jederzeit, durch den Druck auf die RETURN – Taste, __________________________________________________________________________________________ Sound-Programmierung in Java - 76 - beendet werden. Dieses Beispielprogramm soll einen einfachen auf Konsolenebene laufenden Audio Player realisieren. Nach dem schon gezeigten einfachen Kommandozeilenplayer, kommen wir nun zu einer Swing Applikation, die einen Player realisiert, der sich vollständig über GUI - Elemente steuern läßt. Dieses Beispielprogramm wurde in Kapitel 5 - Kontrollen erwähnt, in dem die Control – Objekte der Java Sound API vorgestellt wurden. Das Programm gliedert sich in folgende drei Klassen: AudioPlayer.java - die Klasse, welche die Applikation startet AudioPlayerPanel.java - die Klasse, die die GUI aufbaut PlayBack.java - die Klasse, die die Wiedergabe der Sounddateien durchführt Hier nun zuerst den Quellcode der AudioPlayer.java - Klasse: import java.awt.BorderLayout; import java.awt.GridLayout; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowEvent; import java.awt.event.WindowAdapter; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.IOException; import javax.sound.sampled.UnsupportedAudioFileException; import javax.sound.sampled.LineUnavailableException; import javax.swing.JFrame; import javax.swing.JCheckBox; import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.JLabel; import javax.swing.JSlider; import javax.swing.JFileChooser; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; __________________________________________________________________________________________ Sound-Programmierung in Java - 77 - public class AudioPlayer extends JFrame { private JButton loadB; private JLabel fileName; private JFileChooser fileChooser; private AudioPlayerPanel panel; public AudioPlayer() { super("AudioPlayer1.0"); //überwache das Schliessen des Applikationsfensters this.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent we) { System.exit(0); } }); JPanel filePanel = new JPanel(); filePanel.setLayout(new FlowLayout()); //erstelle einen LOAD - Button mit einem entsprechenden Listener loadB = new JButton("Load..:"); loadB.addActionListener(new ActionListener() { public voidactionPerformed (ActionEvent ae) { loadAudioFile(); } }); filePanel.add(loadB); fileName = new JLabel("Kein AudioFile geladen!"); filePanel.add(fileName); panel = new AudioPlayerPanel(filePanel); this.getContentPane().add(panel); } //Methode wird aufgerufen wenn Load-Button gedrueckt wurde private void loadAudioFile() { //erzeuge ein FileChooser Element um Auswahl der Datei die abgespielt //werden soll zu vereinfachen __________________________________________________________________________________________ Sound-Programmierung in Java - 78 - if (fileChooser == null) { fileChooser = new JFileChooser(); } int nOption = fileChooser.showOpenDialog(this); if (nOption != JFileChooser.APPROVE_OPTION) { return; } //bestimme die vom User gewählte Datei File audioFile = fileChooser.getSelectedFile(); if (panel.setAudioFile(audioFile)) { fileName.setText(audioFile.getName()); } } public static void main(String[] args) { AudioPlayer ap = new AudioPlayer(); ap.pack(); ap.show(); } } Nun folgt der Quellcode für AudioPlayerPanel.java: import java.awt.BorderLayout; import java.awt.GridLayout; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowEvent; import java.awt.event.WindowAdapter; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; __________________________________________________________________________________________ Sound-Programmierung in Java - 79 - import java.io.IOException; import javax.sound.sampled.UnsupportedAudioFileException; import javax.sound.sampled.LineUnavailableException; import javax.swing.JFrame; import javax.swing.JCheckBox; import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.JLabel; import javax.swing.JSlider; import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; public class AudioPlayerPanel extends JPanel { protected JButton loadB; protected JLabel fileName; protected JButton startB; protected JButton stopB; protected JButton pauseB; protected JButton resumeB; protected JSlider gainS; protected JSlider panS; private PlayBack playBack; private File audioFile; private File dataFile; public AudioPlayerPanel(JPanel northPanel) { playBack = new PlayBack(); this.setLayout(new BorderLayout()); this.add("North", northPanel); Jpanel controlPanel = new JPanel(); controlPanel.setLayout(new GridLayout(0, 1)); this.add("South", controlPanel); Jpanel subControlPanel1 = new JPanel(); __________________________________________________________________________________________ Sound-Programmierung in Java - 80 - subControlPanel1.setLayout(new FlowLayout()); controlPanel.add(subControlPanel1); //ueber START - Button ist das Starten der Wiedergabe der //gelesenen Audiodatei möglich startB = new JButton("Start"); startB.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { //starte die Wiedergabe startPlayback(); } }); subControlPanel1.add(startB); //ueber den STOP-Button haelt man die Wiedergabe an stopB = new JButton("Stop"); stopB.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { //stoppe die Wiedergabe stopPlayback(); } }); subControlPanel1.add(stopB); JPanel subControlPanel2 = new JPanel(); subControlPanel2.setLayout(new FlowLayout()); controlPanel.add(subControlPanel2); //PAUSE-Button veranlasst die Wiedergabe zu pausieren pauseB = new JButton("Pause"); pauseB.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { //pausiere mit der Wiedergabe pausePlayback(); } }); subControlPanel2.add(pauseB); //RESUME-Button ermöglicht es die pausierte Wiedergabe __________________________________________________________________________________________ Sound-Programmierung in Java - 81 - //fortzusetzen resumeB = new JButton("Resume"); resumeB.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { //setze pausierte Wiedergabe fort resumePlayback(); } }); subControlPanel2.add(resumeB); startB.setEnabled(false); stopB.setEnabled(false); pauseB.setEnabled(false); resumeB.setEnabled(false); //mit Volume laest sich Wiedergabelautstaerke regeln subControlPanel1.add(new JLabel("Volume")); gainS = new JSlider(JSlider.HORIZONTAL, -90, 24, 0); gainS.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent ce) { //passe Lautstaerke an changeGain(); } }); subControlPanel1.add(gainS); //mit Balance laesst sich Verhaeltnis rechter/linker Kanal //beeinflussen subControlPanel2.add(new JLabel("Balance")); panS = new JSlider(JSlider.HORIZONTAL, -100, 100, 0); panS.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent ce) { //passe Balance an changePan(); } }); subControlPanel2.add(panS); __________________________________________________________________________________________ Sound-Programmierung in Java - 82 - } //Methode bestimmt aktuelles Audiofile das wiedergegeben werden soll public boolean setAudioFile(File file) { dataFile = file; //erzeuge ein Playback Objekt mit dem die Datei wiedergegeben wird try { playBack = new PlayBack(dataFile, this); } catch (UnsupportedAudioFileException e) { JOptionPane.showMessageDialog(null, "Das Audioformat wird nicht unterstuetzt."); return false; } catch (IllegalArgumentException e) { JOptionPane.showMessageDialog(null, "Das Audioformat wird nicht unterstuetzt."); return false; } catch (LineUnavailableException e) { JOptionPane.showMessageDialog(null, "Keine Line zum Abspielen vorhanden!"); return false; } catch (IOException e) { JOptionPane.showMessageDialog(null, "IOFehler!."); return false; } audioFile = dataFile; startB.setEnabled(true); stopB.setEnabled(false); pauseB.setEnabled(false); resumeB.setEnabled(false); return true; } //startet die Wiedergabe bei einem Playback Objekt private void startPlayback() { __________________________________________________________________________________________ Sound-Programmierung in Java - 83 - playBack.start(); startB.setEnabled(false); stopB.setEnabled(true); pauseB.setEnabled(true); resumeB.setEnabled(false); } //stoppt die Wiedergabe bei einem Playback Objekt protected void stopPlayback() { playBack.stop(); startB.setEnabled(true); stopB.setEnabled(false); pauseB.setEnabled(false); resumeB.setEnabled(false); } //pausiert die Wiedergabe bei einem Playback Objekt private void pausePlayback() { playBack.pause(); startB.setEnabled(false); stopB.setEnabled(true); pauseB.setEnabled(false); resumeB.setEnabled(true); } //setzt die Wiedergabe bei einem Playback Objekt fort private void resumePlayback() { playBack.resume(); startB.setEnabled(false); stopB.setEnabled(true); pauseB.setEnabled(true); resumeB.setEnabled(false); } //aendert die Wiedergabelautstaerke bei einem Playback Objekt private void changeGain() { int nValue = gainS.getValue(); float fGain = (float) nValue; playBack.setGain(fGain); __________________________________________________________________________________________ Sound-Programmierung in Java - 84 - } //aendert die Balance bei einem Playback Objekt private void changePan() { int nValue = panS.getValue(); float fPan = nValue * 0.01F; playBack.setPan(fPan); } } Zuletzt noch die wichtigste Klasse der Applikation, nämlich PlayBack.java: import java.io.File; import java.io.IOException; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.Clip; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.DataLine; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.SourceDataLine; import javax.sound.sampled.UnsupportedAudioFileException; import javax.sound.sampled.FloatControl; public class PlayBack implements Runnable { private static boolean isRunning; private static boolean paused = false; private static Thread thread = null; private static File audioFile; private static AudioInputStream stream; private static FloatControl clipGainControl; private static FloatControl clipPanControl; private static AudioFormat audioFormat; private static Clip clip; private static AudioPlayerPanel parent = null; public PlayBack() { __________________________________________________________________________________________ Sound-Programmierung in Java - 85 - audioFile = null; } public PlayBack(File file,AudioPlayerPanel parent)throws UnsupportedAudioFileException, LineUnavailableException,IOException { this(); audioFile = file; this.parent = parent; initAudioInputStream(audioFile); } private void initAudioInputStream(File file)throws UnsupportedAudioFileException,LineUnavailableException,IOException { //erzeuge AudioInputStream auf Quelldatei try { stream = AudioSystem.getAudioInputStream(file); } catch (IOException e) { throw new IllegalArgumentException("Kann keinen AudioInputStream erzeugen auf: " + file); } if (stream == null) { throw new IllegalArgumentException("Kann keinen AudioInputStream erzeugen auf: " + file); } //bereite den Clip vor um die Wiedergabe zu starten initClip(); } //Methode erstellt alle nötigen Voraussetzungen um Clip abzuspielen private void initClip()throws LineUnavailableException,IOException { DataLine.Info info = new DataLine.Info(Clip.class,audioFormat); clip = (Clip) AudioSystem.getLine(info); //pruefe welche Control Objekte zur Verfügung stehen if (clip.isControlSupported(FloatControl.Type.MASTER_GAIN)) __________________________________________________________________________________________ Sound-Programmierung in Java - 86 - { clipGainControl = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN); } if (clip.isControlSupported(FloatControl.Type.PAN)) { clipPanControl = (FloatControl) clip.getControl(FloatControl.Type.PAN); } //reserviere Clip fuer diese Anwendung clip.open(stream); } //starte die Wiedergabe in dem Thread erzeugt wird public void start() { thread = new Thread(this); thread.start(); } //stoppe die Wiedergabe und setze entsprechend die Button //eigenschaften bei Panel public void stop() { clip.stop(); clip.setFramePosition(0); parent.startB.setEnabled(true); parent.stopB.setEnabled(false); parent.pauseB.setEnabled(false); parent.resumeB.setEnabled(false); isRunning = false; } //pausiere Wiedergabe public void pause() { clip.stop(); isRunning = false; paused = true; } //setzte Wiedergabe fort public void resume() { isRunning = true; __________________________________________________________________________________________ Sound-Programmierung in Java - 87 - paused = false; thread = new Thread(this); thread.start(); } //im Thread wird Wiedergabe gestartet public void run() { isRunning = true; clip.start(); while(clip.isActive() && isRunning){} if(paused) clip.stop(); else stop(); } //setzt den neuen Lautstaerkewert public void setGain(float fGain) { if(clipGainControl != null) clipGainControl.setValue(fGain); } //setzt den neuen Balance-Wert public void setPan(float fPan) { if(clipPanControl != null) clipPanControl.setValue(fPan); } } SoundRecorder.java : Ein Beispielprogramm zur Aufnahme von Sound-Dateien, kopiert aus den Hilfe-Dateien von SUN [6]. Aufruf : java SoundRecorder import import import import import import import javax.media.*; javax.media.protocol.*; javax.media.format.*; javax.media.control.*; java.io.*; java.net.*; java.util.*; public class SoundRecorder { public static void main (String args[]) { SoundRecorder srec = new SoundRecorder(); __________________________________________________________________________________________ Sound-Programmierung in Java - 88 - } public SoundRecorder() { CaptureDeviceInfo di = null; Processor p = null; StateHelper sh = null; Vector deviceList = CaptureDeviceManager.getDeviceList(new AudioFormat(AudioFormat.LINEAR, 44100, 16, 2)); if (deviceList.size() > 0) di = (CaptureDeviceInfo)deviceList.firstElement(); else // Exit if we can't find a device that does linear, // 44100Hz, 16 bit, // stereo audio. System.exit(-1); try { p = Manager.createProcessor(di.getLocator()); sh = new StateHelper(p); } catch (IOException e) { System.exit(-1); } catch (NoProcessorException e) { System.exit(-1); } // Configure the processor if (!sh.configure(10000)) System.exit(-1); // Set the output content type and realize the processor p.setContentDescriptor(new FileTypeDescriptor(FileTypeDescriptor.WAVE)); if (!sh.realize(10000)) System.exit(-1); // get the output of the processor DataSource source = p.getDataOutput(); // create a File protocol MediaLocator with the location of the // file to which the data is to be written MediaLocator dest = new MediaLocator("file://foo.wav"); // create a datasink to do the file writing & open the sink to // make sure we can write to it. DataSink filewriter = null; try { filewriter = Manager.createDataSink(source, dest); filewriter.open(); } catch (NoDataSinkException e) { System.exit(-1); } catch (IOException e) { System.exit(-1); } catch (SecurityException e) { System.exit(-1); } // if the Processor implements StreamWriterControl, we can // call setStreamSizeLimit // to set a limit on the size of the file that is written. StreamWriterControl swc = (StreamWriterControl) p.getControl("javax.media.control.StreamWriterControl"); //set limit to 5MB if (swc != null) swc.setStreamSizeLimit(5000000); // now start the filewriter and processor try { filewriter.start(); } catch (IOException e) { System.exit(-1); } // Capture for 5 seconds __________________________________________________________________________________________ Sound-Programmierung in Java - 89 - sh.playToEndOfMedia(5000); sh.close(); // Wait for an EndOfStream from the DataSink and close it... filewriter.close(); } } StateHelper.java : Dieses Programm wird für SoundRecorder.java benötigt und ist ebenfalls aus den Hilfe-Dateien von SUN [6] kopiert. import javax.media.*; public class StateHelper implements javax.media.ControllerListener { Player player = null; boolean configured = false; boolean realized = false; boolean prefetched = false; boolean eom = false; boolean failed = false; boolean closed = false; public StateHelper(Player p) { player = p; p.addControllerListener(this); } public boolean configure(int timeOutMillis) { long startTime = System.currentTimeMillis(); synchronized (this) { if (player instanceof Processor) ((Processor)player).configure(); else return false; while (!configured && !failed) { try { wait(timeOutMillis); } catch (InterruptedException ie) { } if(System.currentTimeMillis()- startTime > timeOutMillis) break; __________________________________________________________________________________________ Sound-Programmierung in Java - 90 - } } return configured; } public boolean realize(int timeOutMillis) { long startTime = System.currentTimeMillis(); synchronized (this) { player.realize(); while (!realized && !failed) { try { wait(timeOutMillis); } catch (InterruptedException ie) { } if(System.currentTimeMillis()- startTime > timeOutMillis) break; } } return realized; } public boolean prefetch(int timeOutMillis) { long startTime = System.currentTimeMillis(); synchronized (this) { player.prefetch(); while (!prefetched && !failed) { try { wait(timeOutMillis); } catch (InterruptedException ie) { } if(System.currentTimeMillis()- startTime > timeOutMillis) break; } } return prefetched && !failed; } public boolean playToEndOfMedia(int timeOutMillis) { long startTime = System.currentTimeMillis(); eom = false; synchronized (this) { player.start(); while (!eom && !failed) { __________________________________________________________________________________________ Sound-Programmierung in Java - 91 - try { wait(timeOutMillis); } catch (InterruptedException ie) { } if (System.currentTimeMillis() - startTime > timeOutMillis) break; } } return eom && !failed; } public void close() { synchronized (this) { player.close(); while (!closed) { try { wait(100); } catch (InterruptedException ie) { } } } player.removeControllerListener(this); } public synchronized void controllerUpdate(ControllerEvent ce) { if (ce instanceof RealizeCompleteEvent) { realized = true; } else if (ce instanceof ConfigureCompleteEvent) { configured = true; } else if (ce instanceof PrefetchCompleteEvent) { prefetched = true; } else if (ce instanceof EndOfMediaEvent) { eom = true; } else if (ce instanceof ControllerErrorEvent) { failed = true; } else if (ce instanceof ControllerClosedEvent) { closed = true; } else { return; } notifyAll(); } } __________________________________________________________________________________________ Sound-Programmierung in Java - 92 - Anhang B Quellenangaben [1] http://java.sun.com/products/java-media/sound/ [2] http://java.sun.com/products/jdk/1.3/docs/guide/sound API - Doku [3] http://java.sun.com/products/jdk/1.3/docs/guide/sound/ [4] http://archives.java.sun.com/archives/javasound-interest.html [5] http://www.midi.org [6] http://java.sun.com/products/java-media/jmf/2.0/specdownload.html [7] http://java.sun.com/products/java-media/jmf/2.0/solutions/SwingJMF.html __________________________________________________________________________________________ Sound-Programmierung in Java - 93 -