Tambahkan kemampuan MP3 ke Java Sound dengan SPI

Dunia audio digital telah berubah dengan cepat selama sepuluh tahun terakhir, memperkenalkan semua jenis format file audio yang baru dan menarik: AU, AIF, MIDI, dan WAV, untuk beberapa nama. Kedatangan format file MP3 baru-baru ini telah membuat dunia musik terbakar, dan tren tidak menunjukkan tanda-tanda melambat karena format audio baru, terdengar lebih baik, dan lebih ringkas menggantikan format audio lama yang kurang efisien. Bagaimana subsistem komputer seperti sistem audio Java Sound dapat mengatasi perubahan tersebut?

Berkat fitur baru di Java 2 1.3 - Java Service Provider Interface (SPI) - JVM menyediakan informasi subsistem audio pada waktu proses. Java Sound menggunakan SPI pada waktu proses untuk menyediakan mixer suara, pembaca file dan penulis, dan utilitas konversi format ke program suara Java. Itu memungkinkan program Java yang lebih lama, bahkan program Java 1.02, untuk memanfaatkan fungsi yang baru ditambahkan tanpa perubahan dan tanpa kompilasi ulang. Memang, lebih banyak fungsi dapat ditambahkan ke Java Sound untuk memanfaatkan format file baru, metode kompresi populer, atau bahkan prosesor suara berbasis perangkat keras.

Pada artikel ini, kita akan melihat SPI yang diilustrasikan dengan contoh dunia nyata: Java Sound diperluas untuk membaca, mengonversi, dan memutar file suara MP3.

Catatan: Untuk mengunduh kode sumber lengkap untuk artikel ini, lihat Sumber.

Untuk memahami Service Provider Interface (SPI), ada baiknya untuk memikirkan JVM sebagai penyedia layanan untuk program Java - konsumen layanan tersebut. Konsumen menggunakan antarmuka yang dikenal untuk meminta layanan yang disediakan JVM. Misalnya, dengan Java Sound, program Java meminta untuk memutar file audio dengan salah satu metode suara publik. Di Java 2 versi 1.3, AudioSystem menanyakan dirinya sendiri untuk melihat apakah ia dapat menangani jenis file suara yang diberikan. Jika bisa, suara dimainkan. Jika tidak bisa, pengecualian akan dilemparkan, biasanya sun.audio.InvalidAudioExceptionuntuk program audio Java lama yang menggunakan sun.audioatau java.appletpaket. Sebaliknya, program Java Sound yang lebih baru yang menggunakan javax.soundpaket biasanya membuang filejavax.sound.sampled.UnsupportedAudioException. Apa pun itu, JVM memberi tahu Anda bahwa ia tidak dapat menyediakan layanan yang diminta.

Di Java 2 versi 1.2, subsistem suara ditingkatkan untuk menangani berbagai jenis file audio: WAV, AIFF, MIDI, dan sebagian besar jenis AU. Dengan peningkatan itu - seolah-olah secara ajaib - program lama yang menggunakan paket sun.audioatau java.appletmampu menangani jenis file audio baru. Perkembangan itu mewakili berkah bagi pengguna audio Java, tetapi masih tidak memungkinkan pengguna untuk memperluas JVM. Program audio Java masih terbatas pada jenis file audio yang disediakan oleh pembuat JVM.

Dengan SPI Java 2 versi 1.3, kami melihat metode yang dirancang untuk memperluas JVM. Java Sound mengetahui cara menanyakan penyedia layanan tersebut dan, ketika disajikan dengan file audio, salah satu penyedia layanan mungkin menunjukkan bahwa ia tahu cara membaca jenis file audio atau mengetahui cara mengonversinya. Kemudian subsistem suara menggunakan penyedia layanan itu untuk memutar suara.

Selanjutnya, kami memeriksa cara menambahkan penyedia layanan baru untuk memanfaatkan satu jenis file audio populer, jenis audio MP3 atau MPEG Layer 3 yang dikembangkan dalam standar ISO Motion Picture Expert Group yang dirilis beberapa tahun lalu.

Mempersiapkan layanan baru

Penyedia layanan menambahkan layanan ke JVM dengan menyediakan file kelas yang menjalankan layanan dan mencantumkan layanan tersebut di META-INF/servicesdirektori khusus file JAR . Direktori itu mencantumkan semua penyedia layanan, dan subsistem JVM mencari layanan tambahan di sana. Dengan mengingat informasi tersebut, mari kita lihat bagaimana implementasi Java Sound menyediakan pembaca file audio untuk jenis file audio sampel standar: WAV, AIFF, dan AU.

rt.jarFile penting JRE , terletak di jre/libdirektori instalasi Java, berisi sebagian besar kelas Java runtime JRE. Jika Anda mengekstrak rt.jarfile, Anda akan menemukan bahwa file tersebut berisi META-INF/servicesdirektori, di dalamnya Anda akan menemukan beberapa file yang diberi nama dengan javax.soundawalan. Salah satu file - javax.sound.sampled.spi.AudioFileReader- berisi daftar kelas yang menyediakan kemampuan membaca untuk subsistem Java Sound. Saat membuka file berenkode UTF-8 itu, Anda akan melihat:

# Penyedia untuk membaca file audio com.sun.media.sound.AuFileReader com.sun.media.sound.AiffFileReader com.sun.media.sound.WaveFileReader 

Kelas di atas mencantumkan penyedia layanan yang menyediakan kemampuan membaca file audio ke subsistem Java Sound. Subsistem membuat instance kelas-kelas itu, menggunakannya untuk mendeskripsikan format data file audio, dan mendapatkan AudioInputStreamdari file. Demikian pula, META-INF/servicesberisi file SPI lain untuk menghitung perangkat MIDI, mixer, bank suara, konverter format, dan bagian lain dari subsistem Suara Java.

Keuntungan arsitektur itu: subsistem Suara Java menjadi dapat diperluas. Untuk lebih spesifik, file JAR lain yang ditambahkan ke jalur kelas JRE mungkin berisi penyedia layanan lain yang menyediakan layanan tambahan. Subsistem audio dapat menanyakan semua penyedia layanan dan mencocokkan layanan yang sesuai dengan permintaan konsumen. Bagi konsumen, bagaimana layanan tersedia dan ditanyakan tetap sepenuhnya transparan. Akibatnya, dengan penyedia layanan yang tepat, program lama sekarang dapat berjalan dengan jenis file audio baru - sebuah fitur besar.

Sekarang mari beralih dari teori ke konkret dengan mempelajari bagaimana menyediakan layanan baru: file audio MP3.

Menerapkan SPI

Pada bagian ini, kita akan membahas langkah demi langkah melalui contoh konkret untuk memperluas subsistem audio Java Sound menggunakan SPI. Untuk memulai, ada dua kelas dasar yang menghubungkan dekoder MP3 ke subsistem Suara Java sehingga dapat memutar file MP3:

  • The BasicMP3FileReader(meluas AudioFileReader) tahu bagaimana membaca file MP3
  • The BasicMP3FormatConversionProvider(extends FormatConversionProvider) tahu bagaimana mengubah aliran MP3 ke salah satu subsistem Java Sound yang dapat diputar

Kedua kelas tersebut memberi tahu Java Sound bahwa kemampuan MP3 tersedia.

Catatan: Untuk keperluan artikel ini, saya telah membuat kelas-kelasnya sangat sederhana. Ada banyak jenis audio MPEG yang disandikan, tetapi layanan MP3 dasar yang disediakan dalam artikel ini hanya mendukung MPEG versi 1 atau 2, lapisan 3. Tidak mendukung soundtrack film multichannel. Untuk decoder MPEG yang lengkap, kita harus menyelidiki sumber gratis implementasi Tritonus Java Sound yang dikembangkan oleh Matthias Pfisterer, tersedia di Sumber.

Implementasi: Bagian 1, BasicMP3FileReader

Kita mulai dengan mengimplementasikan BasicMP3FileReaderkelas, yang memperluas kelas abstrak javax.sound.sampled.spi.AudioFileReaderdan mengharuskan kita mengimplementasikan metode berikut:

  • publik abstrak AudioFileFormat getAudioFileFormat (aliran InputStream) melempar UnsupportedAudioFileException, IOException;
  • publik abstrak AudioFileFormat getAudioFileFormat (URL url) melempar UnsupportedAudioFileException, IOException;
  • publik abstrak AudioFileFormat getAudioFileFormat (File file) melempar UnsupportedAudioFileException, IOException;
  • publik abstrak AudioInputStream getAudioInputStream (aliran InputStream) melempar UnsupportedAudioFileException, IOException;
  • publik abstrak AudioInputStream getAudioInputStream (URL url) melempar UnsupportedAudioFileException, IOException;
  • publik abstrak AudioInputStream getAudioInputStream (File file) melempar UnsupportedAudioFileException, IOException;

Perhatikan bahwa semua metode membuang UnsupportedAudioFileExceptiondan IOException, yang memberi sinyal ke Java Sound bahwa ada masalah dengan file MP3. Pengecualian tersebut harus diterapkan setiap kali file tidak dapat dibaca, byte tidak cocok, atau kecepatan sampel atau ukuran data tampak rusak.

Perhatikan juga dua kelompok metode yang akan diterapkan. Kelompok pertama menyediakan AudioFileFormatobjek dari satu dari tiga input: InputStream, URL, atau File. Sebagai tujuan akhirnya, getAudioFileFormat()metode ini menyediakan AudioFileFormatobjek yang mendeskripsikan encoding, laju sampel, ukuran sampel, jumlah saluran, dan atribut lain dari aliran audio. Meskipun kode tersebut berisi detail konversi tersebut, kita dapat meringkasnya dengan mencatat bahwa ia membaca byte dari aliran, dan byte tersebut diuji untuk memastikan bahwa aliran tersebut, pada kenyataannya, aliran MP3, yang menggambarkan laju sampelnya, dan semua bidang yang diperlukan ada.

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.

Kelompok metode ketiga, dua versi getAudioInputStream(), menyediakan aliran audio yang diterjemahkan dari aliran input MP3 yang diberikan. Sederhananya, penyedia konversi memeriksa bahwa konversi didukung dan, jika ya, mengembalikan aliran input audio linier yang didekodekan dari aliran audio MP3 yang dikodekan. Jika konversi tidak didukung, sebuah IllegalArgumentExceptiondilempar. Pada saat itu, kode penyedia layanan kami harus benar-benar mulai mendekode aliran data MPEG. Dengan demikian, di situlah pertemuan karet dengan jalan, seperti yang digambarkan di bawah ini:

if (isConversionSupported (targetFormat, audioInputStream.getFormat ())) {return baru DecodedMpegAudioInputStream (targetFormat, audioInputStream); } melempar IllegalArgumentException baru ("konversi tidak didukung");