Fügen Sie Java Sound mit SPI MP3-Funktionen hinzu

Die digitale Audiowelt hat sich in den letzten zehn Jahren rasant verändert und alle möglichen neuen und aufregenden Audiodateiformate eingeführt: AU, AIF, MIDI und WAV, um nur einige zu nennen. Die jüngste Einführung des MP3-Dateiformats hat die Musikwelt in Brand gesetzt, und der Trend zeigt keine Anzeichen einer Verlangsamung, da neue, besser klingende und kompaktere Audioformate ältere, weniger effiziente ersetzen. Wie kann ein Computersubsystem wie das Java Sound-Audiosystem mit diesen Änderungen umgehen?

Dank einer neuen Funktion in Java 2 1.3 - dem Java Service Provider Interface (SPI) - stellt die JVM zur Laufzeit Informationen zum Audiosubsystem bereit. Java Sound verwendet das SPI zur Laufzeit, um Soundmixer, Dateireader und -schreiber sowie Dienstprogramme zur Formatkonvertierung für ein Java-Soundprogramm bereitzustellen. Dadurch können ältere Java-Programme, auch Java 1.02-Programme, die neu hinzugefügten Funktionen ohne Änderungen und ohne Neukompilierung nutzen. In der Tat können Java Sound um weitere Funktionen erweitert werden, um die Vorteile neuer Dateiformate, gängiger Komprimierungsmethoden oder sogar hardwarebasierter Soundprozessoren zu nutzen.

In diesem Artikel betrachten wir das SPI anhand eines Beispiels aus der Praxis: Java Sound wurde erweitert, um MP3-Sounddateien zu lesen, zu konvertieren und abzuspielen.

Hinweis: Informationen zum Herunterladen des vollständigen Quellcodes für diesen Artikel finden Sie unter Ressourcen.

Um das Service Provider Interface (SPI) zu verstehen, ist es hilfreich, sich eine JVM als einen Anbieter von Diensten für ein Java-Programm vorzustellen - den Verbraucher dieser Dienste. Der Verbraucher verwendet eine bekannte Schnittstelle, um einen von JVM bereitgestellten Dienst anzufordern. Bei Java Sound fordert das Java-Programm beispielsweise die Wiedergabe einer Audiodatei mit einer der öffentlichen Soundmethoden an. In Java 2 Version 1.3 fragt sich das AudioSystem selbst ab, ob es den angegebenen Sounddateityp verarbeiten kann. Wenn dies möglich ist, wird der Ton abgespielt. Wenn dies nicht möglich ist, wird eine Ausnahme ausgelöst, normalerweise sun.audio.InvalidAudioExceptionfür ältere Java-Audioprogramme, die die Pakete sun.audiooder verwenden java.applet. Im Gegensatz dazu javax.soundwerfen neuere Java Sound-Programme, die das Paket verwenden, normalerweise dasjavax.sound.sampled.UnsupportedAudioException. In beiden Fällen teilt Ihnen die JVM mit, dass der angeforderte Dienst nicht bereitgestellt werden kann.

In Java 2 Version 1.2 wurde das Sound-Subsystem für die Verarbeitung von Audiodateien vieler Typen erweitert: WAV, AIFF, MIDI und die meisten AU-Typen. Mit dieser Verbesserung - wie von Zauberhand - konnten die älteren Programme, die die Pakete sun.audiooder verwenden java.applet, neue Audiodateitypen verarbeiten. Diese Entwicklung war ein Segen für Java-Audio-Benutzer, erlaubte es den Benutzern jedoch nicht, die JVM zu erweitern. Java-Audioprogramme waren weiterhin auf die vom JVM-Hersteller bereitgestellten Audiodateitypen beschränkt.

Mit dem SPI von Java 2 Version 1.3 sehen wir eine strukturierte Methode zum Erweitern der JVM. Java Sound kann diese Dienstanbieter abfragen. Wenn eine Audiodatei angezeigt wird, gibt einer der Dienstanbieter möglicherweise an, dass er den Audiodateityp lesen oder konvertieren kann. Dann verwendet das Sound-Subsystem diesen Dienstanbieter, um den Sound abzuspielen.

Als Nächstes untersuchen wir, wie Sie neue Dienstanbieter hinzufügen können, um einen beliebten Audiodateityp zu nutzen, den MP3- oder MPEG Layer 3-Audiotyp, der im vor einigen Jahren veröffentlichten ISO-Standard der Motion Picture Expert Group entwickelt wurde.

Neue Dienste vorbereiten

Dienstanbieter fügen der JVM Dienste hinzu, indem sie die Klassendateien bereitstellen, die den Dienst ausführen, und diese Dienste im speziellen META-INF/servicesVerzeichnis einer JAR-Datei auflisten . In diesem Verzeichnis werden alle Dienstanbieter aufgelistet, und JVM-Subsysteme suchen dort nach zusätzlichen Diensten. Schauen wir uns vor diesem Hintergrund an, wie die Implementierung von Java Sound Audiodateileser für die standardmäßigen abgetasteten Audiodateitypen bereitstellt: WAV, AIFF und AU.

Die wichtige rt.jarDatei der JRE , die sich im jre/libVerzeichnis einer Java-Installation befindet, enthält die meisten Laufzeit-Java-Klassen der JRE. Wenn Sie die rt.jarDatei entpacken , werden Sie feststellen, dass sie ein META-INF/servicesVerzeichnis enthält , in dem sich mehrere Dateien befinden, die mit einem javax.soundPräfix benannt sind. Eine dieser Dateien - javax.sound.sampled.spi.AudioFileReader- enthält eine Liste von Klassen, die dem Java Sound-Subsystem die Lesefähigkeit bieten. Beim Öffnen dieser UTF-8-codierten Datei sehen Sie:

# Anbieter zum Lesen von Audiodateien com.sun.media.sound.AuFileReader com.sun.media.sound.AiffFileReader com.sun.media.sound.WaveFileReader 

In den obigen Klassen sind die Dienstanbieter aufgeführt, die dem Java Sound-Subsystem die Möglichkeit zum Lesen von Audiodateien bieten. Das Subsystem instanziiert diese Klassen, verwendet sie zur Beschreibung des Audiodateidatenformats und erhält eine AudioInputStreamaus der Datei. META-INF/servicesEnthält in ähnlicher Weise andere SPI-Dateien zum Auflisten von MIDI-Geräten, Mischpulten, Soundbanks, Formatkonvertern und anderen Teilen des Java Sound-Subsystems.

Der Vorteil dieser Architektur: Das Java Sound-Subsystem wird erweiterbar. Genauer gesagt können andere JAR-Dateien, die dem JRE-Klassenpfad hinzugefügt wurden, andere Dienstanbieter enthalten, die zusätzliche Dienste bereitstellen. Das Audio-Subsystem kann alle Dienstanbieter abfragen und den entsprechenden Dienst mit der Anforderung des Verbrauchers abgleichen. Für den Verbraucher bleibt völlig transparent, wie die Dienste verfügbar werden und abgefragt werden. Folglich können ältere Programme mit den richtigen Dienstanbietern jetzt mit neuen Audiodateitypen ausgeführt werden - eine große Funktion.

Lassen Sie uns nun vom Theoretischen zum Konkreten übergehen, indem wir untersuchen, wie ein neuer Dienst bereitgestellt werden kann: MP3-Audiodateien.

Implementierung des SPI

In diesem Abschnitt werden wir Schritt für Schritt ein konkretes Beispiel für die Erweiterung des Java Sound-Audiosubsystems mithilfe des SPI durchgehen. Zunächst gibt es zwei grundlegende Klassen, die einen MP3-Decoder mit dem Java Sound-Subsystem verbinden, damit er MP3-Dateien abspielen kann:

  • Der BasicMP3FileReader(erweitert AudioFileReader) weiß, wie man MP3-Dateien liest
  • Der BasicMP3FormatConversionProvider(erweitert FormatConversionProvider) weiß, wie man einen MP3-Stream in einen konvertiert, den das Java Sound-Subsystem abspielen kann

Die beiden Klassen teilen Java Sound mit, dass MP3-Funktionen verfügbar sind.

Hinweis: Für die Zwecke dieses Artikels habe ich die Klassen extrem einfach gehalten. Es gibt viele Arten von codiertem MPEG-Audio, aber der in diesem Artikel bereitgestellte grundlegende MP3-Dienst unterstützt nur die MPEG-Versionen 1 oder 2, Schicht 3. Er unterstützt keine mehrkanaligen Film-Soundtracks. Für einen vollwertigen MPEG-Decoder sollte man die von Matthias Pfisterer entwickelte kostenlose Quelle Tritonus Java Sound-Implementierung untersuchen, die in Resources verfügbar ist.

Implementierung: Teil 1, der BasicMP3FileReader

Wir beginnen mit der Implementierung der BasicMP3FileReaderKlasse, die die abstrakte Klasse erweitert javax.sound.sampled.spi.AudioFileReaderund die Implementierung der folgenden Methoden erfordert:

  • public abstract AudioFileFormat getAudioFileFormat (InputStream-Stream) löst UnsupportedAudioFileException, IOException aus;
  • public abstract AudioFileFormat getAudioFileFormat (URL url) löst UnsupportedAudioFileException, IOException aus;
  • public abstract AudioFileFormat getAudioFileFormat (Dateidatei) löst UnsupportedAudioFileException, IOException aus;
  • public abstract AudioInputStream getAudioInputStream (InputStream-Stream) löst UnsupportedAudioFileException, IOException aus;
  • public abstract AudioInputStream getAudioInputStream (URL url) löst UnsupportedAudioFileException, IOException aus;
  • public abstract AudioInputStream getAudioInputStream (Dateidatei) löst UnsupportedAudioFileException, IOException aus;

Beachten Sie, dass alle Methoden UnsupportedAudioFileExceptionund IOException, die Java Sound signalisieren, dass Probleme mit der MP3-Datei bestehen. Diese Ausnahmen sollten immer dann ausgelöst werden, wenn eine Datei nicht lesbar ist, die Bytes nicht übereinstimmen oder die Abtastraten oder Datengrößen aus dem Ruder laufen.

Also notice the two groups of methods to implement. The first group provides an AudioFileFormat object from one of three inputs: InputStream, URL, or File. As its ultimate goal, the getAudioFileFormat() method provides an AudioFileFormat object that describes the encoding, sample rate, sample size, number of channels, and other attributes of the audio stream. While the code contains the details of that conversion, we can summarize by noting that it reads the bytes from the stream, and those bytes are tested to ensure that the stream is, in fact, an MP3 stream, that it describes its sample rate, and that all the necessary fields are present.

Since that SPI code provides support for a new encoding, we have to invent such a class -- BasicMP3Encoding. That simple class contains a static final field to describe the new MP3 encoding in a manner similar to descriptions for existing encodings for PCM, ALAW, and ULAW in the javax.sound.sampled.AudioFormat class.

We also implement the BasicMP3FileFormatType class in a manner similar to javax.sound.sampled.AudioFileFormat, as seen below:

public class BasicMP3Encoding extends AudioFormat.Encoding { public static final AudioFormat.Encoding MP3 = new BasicMP3Encoding( "MP3" ); public BasicMP3Encoding( String encodingName ) { super( encodingName ); } } 

BasicMP3FileReader's second group of methods provides an AudioInputStream from the same inputs. Since an InputStream can be pulled from a URL or File, we can use the getAudioInputStream() method with the InputStream parameter to implement the other two methods.

This is shown here:

public AudioInputStream getAudioInputStream( URL url ) throws UnsupportedAudioFileException, IOException { InputStream inputStream = url.openStream(); try { return getAudioInputStream( inputStream ); } catch ( UnsupportedAudioFileException e ) { inputStream.close(); throw e; } catch ( IOException e ) { inputStream.close(); throw e; } } 

The stream is tested by using the getAudioFileFormat( inputStream ) method to ensure it is an MP3 stream. Then we create a new generic AudioInputStream from the MP3 stream. For further details, read the BasicMP3FileReader.java source file.

Now that we have implemented the AudioFileReader, we are halfway to our goal. Let's look at how to implement the second half of our service provider, the FormatConversionProvider.

Implementation: Part 2, the BasicMP3FormatConversionProvider

Next, we implement BasicMP3FormatConversionProvider, which extends the abstract class javax.sound.sampled.spi.FormatConversionProvider. A format conversion provider converts from a source to a target audio format. To implement BasicMP3FormatConversionProvider, we must implement the following methods:

  • public abstract AudioFormat.Encoding[] getSourceEncodings();
  • public abstract AudioFormat.Encoding[] getTargetEncodings();
  • public abstract AudioFormat.Encoding[] getTargetEncodings( AudioFormat srcFormat );
  • public abstract AudioFormat[] getTargetFormats( AudioFormat.Encoding targetEncoding, AudioFormat sourceFormat );
  • public abstract AudioInputStream getAudioInputStream( AudioFormat.Encoding targetEncoding, AudioInputStream sourceStream );
  • public abstract AudioInputStream getAudioInputStream( AudioFormat targetFormat, AudioInputStream sourceStream );

As you can see, we have three groups of methods. The first group simply enumerates the source and target encodings that the format-conversion provider supports. The BasicMP3FormatConversionProvider class contains some large static arrays that describe the input and output formats supported by the underlying MPEG decoder.

For instance, the source formats are given below. The source encodings simply are derived from those formats when the class instantiates. Whenever someone calls the getSourceEncodings() method, the source encoding array is returned.

protected static final AudioFormat [] SOURCE_FORMATS = { // encoding, rate, bits, channels, frameSize, frameRate, big endian new AudioFormat( BasicMP3Encoding.MP3, 8000.0F, -1, 1, -1, -1, false ), new AudioFormat( BasicMP3Encoding.MP3, 8000.0F, -1, 2, -1, -1, false ), new AudioFormat( BasicMP3Encoding.MP3, 11025.0F, -1, 1, -1, -1, false ), new AudioFormat( BasicMP3Encoding.MP3, 11025.0F, -1, 2, -1, -1, false ), ... 

BasicMP3FormatConversionProvider's second group of methods, containing the getTargetFormats() method, proves rather tricky. We want getTargetFormats() to return a target AudioFormat that can be created from the given source AudioFormat. Additionally, the target encoding is given, and the target AudioFormat must be of that encoding. To perform that tricky maneuver, the BasicMP3FormatConversionProvider creates a hashtable to help speed the mapping. The hashtable maps the target format to another hashtable of possible target encodings. The target encodings each point to a set of target audio formats. If you find that difficult to visualize, just remember that the format-conversion provider contains data structures to quickly return a target AudioFormat from a given source AudioFormat.

Die dritte Gruppe von Methoden, zwei Versionen von getAudioInputStream(), stellt einen decodierten Audiostream aus dem angegebenen MP3-Eingabestream bereit. Einfach ausgedrückt, der Konvertierungsanbieter überprüft, ob die Konvertierung unterstützt wird, und gibt in diesem Fall einen decodierten linearen Audioeingangsstrom aus dem angegebenen codierten MP3-Audiostream zurück. Wenn die Konvertierung nicht unterstützt wird, wird eine IllegalArgumentExceptionausgelöst. Zu diesem Zeitpunkt muss unser Dienstanbietercode tatsächlich mit der Dekodierung des MPEG-Datenstroms beginnen. Hier trifft der Gummi auf die Straße, wie unten dargestellt:

if (isConversionSupported (targetFormat, audioInputStream.getFormat ())) {neuen DecodedMpegAudioInputStream (targetFormat, audioInputStream) zurückgeben; } neue IllegalArgumentException auslösen ("Konvertierung nicht unterstützt");