Catatan pemrograman OpenSL ES

PERINGATAN: OpenSL ES tidak digunakan lagi. Developer harus menggunakan library Oboe open source yang tersedia di GitHub. Oboe adalah wrapper C++ yang menyediakan API yang sangat mirip dengan AAudio. Oboe memanggil AAudio saat tersedia, dan melakukan fallback ke OpenSL ES jika AAudio tidak tersedia.

Catatan di bagian ini melengkapi spesifikasi OpenSL ES 1.0.1.

Inisialisasi objek dan antarmuka

Dua aspek model pemrograman OpenSL ES yang mungkin kurang dipahami developer baru adalah perbedaan antara objek dan antarmuka, serta urutan inisialisasi.

Secara singkat, objek OpenSL ES mirip dengan konsep objek dalam bahasa pemrograman seperti Java dan C++, kecuali bahwa objek OpenSL ES hanya terlihat melalui antarmuka terkaitnya. Ini mencakup antarmuka awal untuk semua objek, yang disebut SLObjectItf. Tidak ada handle untuk objek itu sendiri; yang ada hanya handle untuk antarmuka SLObjectItf objek.

Objek OpenSL ES akan dibuat terlebih dahulu, yang menampilkan SLObjectItf, lalu direalisasikan. Proses ini mirip dengan pola pemrograman umum di mana objek terlebih dahulu dibuat (yang tidak boleh gagal selain karena kekurangan memori atau parameter yang tidak valid), lalu inisialisasi diselesaikan (yang mungkin gagal karena kekurangan resource). Langkah realisasi memberikan implementasi tempat logis untuk mengalokasikan resource tambahan jika diperlukan.

Sebagai bagian dari API untuk membuat objek, aplikasi menentukan array antarmuka yang diinginkan dan nantinya akan diperoleh. Perlu diperhatikan, array ini tidak otomatis memperoleh antarmuka; array ini hanya menunjukkan maksud di masa mendatang untuk memperolehnya. Antarmuka dibedakan menjadi implisit atau eksplisit. Antarmuka eksplisit harus dicantumkan dalam array jika akan diperoleh nanti. Antarmuka implisit tidak perlu dicantumkan dalam array pembuatan objek, tetapi tidak apa-apa jika dicantumkan. OpenSL ES memiliki satu lagi jenis antarmuka yang disebut dinamis, yang tidak perlu ditentukan dalam array pembuatan objek, dan dapat ditambahkan nanti setelah objek dibuat. Implementasi Android menyediakan sebuah fitur praktis untuk menghindari kerumitan ini, yang dijelaskan dalam Antarmuka dinamis saat pembuatan objek.

Setelah objek dibuat dan direalisasikan, aplikasi harus memperoleh antarmuka untuk setiap fitur yang diperlukannya, dengan menggunakan GetInterface pada SLObjectItf awal.

Terakhir, objek tersedia untuk digunakan melalui antarmukanya, meskipun perlu diketahui bahwa objek tertentu memerlukan penyiapan lebih lanjut. Secara khusus, pemutar audio dengan sumber data URI perlu persiapan lebih banyak agar dapat mendeteksi error koneksi. Lihat bagian Pengambilan data pemutar audio untuk detailnya.

Setelah aplikasi selesai menangani objek tersebut, Anda harus secara eksplisit menghancurkannya; lihat bagian Menghancurkan di bawah.

Pengambilan data pemutar audio

Untuk pemutar audio dengan sumber data URI, Object::Realize mengalokasikan resource, tetapi tidak menghubungkan ke sumber data (menyiapkan) atau memulai pengambilan data. Hal tersebut terjadi setelah status pemutar disetel ke SL_PLAYSTATE_PAUSED atau SL_PLAYSTATE_PLAYING.

Sebagian informasi mungkin tetap tidak diketahui hingga relatif menjelang akhir urutan ini. Secara khusus, mula-mula Player::GetDuration menampilkan SL_TIME_UNKNOWN dan MuteSolo::GetChannelCount berhasil ditampilkan dengan jumlah channel nol atau hasil error SL_RESULT_PRECONDITIONS_VIOLATED. Semua API ini menampilkan nilai yang sesuai setelah nilai tersebut diketahui.

Properti lain yang awalnya tidak diketahui akan menyertakan frekuensi sampel dan jenis konten media sebenarnya berdasarkan pemeriksaan header konten (kebalikan dari jenis MIME dan jenis penampung yang ditentukan aplikasi). Semua ini juga ditentukan nanti selama persiapan/pengambilan data, tetapi tidak ada API untuk memperolehnya.

Antarmuka status pengambilan data berguna untuk mendeteksi kapan semua informasi tersedia, atau kapan aplikasi dapat mengkuerinya secara berkala. Perlu diperhatikan bahwa sebagian informasi, seperti durasi streaming MP3, mungkin tidak pernah diketahui.

Antarmuka status pengambilan data juga berguna untuk mendeteksi error. Daftarkan callback dan aktifkan setidaknya peristiwa SL_PREFETCHEVENT_FILLLEVELCHANGE dan SL_PREFETCHEVENT_STATUSCHANGE. Jika kedua peristiwa ini dikirim bersamaan, dan PrefetchStatus::GetFillLevel melaporkan level nol, serta PrefetchStatus::GetPrefetchStatus melaporkan SL_PREFETCHSTATUS_UNDERFLOW, ini menandakan adanya error yang tidak dapat dipulihkan dalam sumber data. Hal tersebut mencakup ketidakmampuan untuk terhubung ke sumber data karena nama file lokal tidak ada atau URI jaringan tidak valid.

OpenSL ES versi berikutnya diharapkan akan menambahkan dukungan yang lebih eksplisit untuk menangani error dalam sumber data. Akan tetapi, untuk kompatibilitas biner mendatang, kami akan terus mendukung metode saat ini untuk melaporkan error yang tidak dapat dipulihkan.

Singkatnya, urutan kode yang direkomendasikan adalah:

  1. Engine::CreateAudioPlayer
  2. Object:Realize
  3. Object::GetInterface untuk SL_IID_PREFETCHSTATUS
  4. PrefetchStatus::SetCallbackEventsMask
  5. PrefetchStatus::SetFillUpdatePeriod
  6. PrefetchStatus::RegisterCallback
  7. SL_IID_PLAY untuk Object::GetInterface
  8. Play::SetPlayState ke SL_PLAYSTATE_PAUSED, atau SL_PLAYSTATE_PLAYING

Catatan: Persiapan dan pengambilan data terjadi di sini; callback akan diaktifkan dengan update status berkala.

Menghancurkan

Pastikan untuk menghancurkan semua objek saat keluar dari aplikasi. Objek harus dihancurkan dengan urutan terbalik dari pembuatannya, karena tidak aman menghancurkan objek yang memiliki objek dependen. Misalnya, hancurkan dengan urutan ini: pemutar dan perekam audio, campuran output, lalu engine-nya.

OpenSL ES tidak mendukung pembersihan sampah memori otomatis atau penghitungan referensi antarmuka. Setelah Anda memanggil Object::Destroy, semua antarmuka yang masih ada dan berasal dari objek terkait akan menjadi tidak ditentukan.

Implementasi Android OpenSL ES tidak mendeteksi penggunaan antarmuka tersebut secara tidak tepat. Terus menggunakan antarmuka tersebut setelah objeknya dimusnahkan dapat menyebabkan aplikasi mengalami error atau berperilaku dengan cara yang tidak dapat diprediksi.

Sebaiknya tetapkan antarmuka objek utama dan semua antarmuka terkait secara eksplisit ke NULL sebagai bagian dari urutan penghancuran objek, yang mencegah penyalahgunaan secara tidak disengaja atas handle antarmuka yang sudah tidak berlaku.

Penggeseran stereo

Jika Volume::EnableStereoPosition digunakan untuk mengaktifkan penggeseran stereo sumber mono, ada reduksi sebesar 3 dB dari total tingkat kekuatan suara. Reduksi ini diperlukan agar total level kekuatan suara tetap konstan saat sumber digeser dari satu channel ke channel lainnya. Oleh karena itu, aktifkan penempatan stereo hanya jika diperlukan. Untuk informasi selengkapnya, lihat artikel Wikipedia tentang penggeseran audio.

Callback dan thread

Handler callback umumnya dipanggil secara sinkron bila implementasi mendeteksi suatu peristiwa. Titik ini asinkron jika menyangkut aplikasi, jadi sebaiknya Anda menggunakan mekanisme sinkronisasi yang tidak memblokir untuk mengontrol akses ke variabel yang dibagikan antara aplikasi dan handler callback. Dalam kode contoh, misalnya untuk antrean buffer, kami telah menghilangkan sinkronisasi ini atau menggunakan sinkronisasi yang memblokir demi penyederhanaan. Akan tetapi, sinkronisasi non-blokir yang tepat sangat penting untuk kode produksi.

Handler callback dipanggil dari thread non-aplikasi internal yang tidak dikaitkan ke runtime Android, sehingga tidak memenuhi syarat untuk menggunakan JNI. Karena thread internal ini sangat penting bagi integritas implementasi OpenSL ES, handler callback juga tidak boleh memblokir atau melakukan tugas yang berlebihan.

Jika handler callback perlu menggunakan JNI atau mengeksekusi tugas yang tidak proporsional terhadap callback, handler tersebut harus memposting peristiwa untuk diproses oleh thread lain. Contoh beban kerja callback yang dapat diterima antara lain rendering dan pengantrean buffer output berikutnya (untuk AudioPlayer), pemrosesan buffer input yang baru diisi, dan mengantrekan buffer kosong berikutnya (untuk AudioRecorder), atau API sederhana, misalnya sebagian besar jenis Get. Lihat bagian Performa di bawah mengenai beban kerja.

Perlu diperhatikan, proses konversi ini aman: thread aplikasi Android yang telah memasukkan JNI diizinkan memanggil API OpenSL ES secara langsung, termasuk melakukan panggilan yang memblokir. Akan tetapi, panggilan yang memblokir tidak direkomendasikan dari thread utama, karena dapat mengakibatkan Aplikasi Tidak Merespons (ANR).

Penentuan mengenai thread yang memanggil handler callback sangat bergantung pada implementasinya. Alasan fleksibilitas ini adalah untuk memungkinkan optimalisasi di masa mendatang, terutama di perangkat multi-core.

Thread yang menjalankan handler callback tidak dijamin memiliki identitas yang sama di berbagai panggilan yang berbeda. Oleh karena itu, jangan berharap pthread_t yang ditampilkan oleh pthread_self() atau pid_t yang ditampilkan oleh gettid() akan konsisten di berbagai panggilan. Dengan alasan yang sama, jangan gunakan API Thread Local Storage (TLS) seperti pthread_setspecific() dan pthread_getspecific() dari callback.

Implementasi ini menjamin bahwa beberapa callback serentak dengan jenis yang sama, untuk objek yang sama, tidak akan terjadi. Akan tetapi, callback serentak dengan jenis berbeda untuk objek yang sama dimungkinkan di thread berbeda.

Performa

Karena OpenSL ES adalah C API native, thread aplikasi non-runtime yang memanggil OpenSL ES tidak memiliki overhead terkait runtime seperti jeda pembersihan sampah memori. Dengan satu pengecualian yang dijelaskan di bawah, tidak ada manfaat performa tambahan dari penggunaan OpenSL ES selain yang satu ini. Secara khusus, penggunaan OpenSL ES tidak menjamin peningkatan seperti latensi audio yang lebih rendah dan prioritas penjadwalan yang lebih tinggi dibandingkan dengan yang biasanya diberikan oleh platform. Di sisi lain, karena platform Android dan implementasi perangkat tertentu terus berkembang, aplikasi OpenSL ES akan dapat memanfaatkan peningkatan performa sistem di masa mendatang.

Salah satu evolusi tersebut adalah dukungan agar latensi output audio lebih rendah. Dasar-dasar untuk latensi output yang lebih rendah disertakan pertama kali dalam Android 4.1 (API level 16), lalu progres yang berlanjut terjadi dalam Android 4.2 (API level 17). Peningkatan ini tersedia melalui OpenSL ES untuk implementasi perangkat yang mengklaim fitur android.hardware.audio.low_latency. Jika perangkat tidak mengklaim fitur ini tetapi mendukung Android 2.3 (API level 9) atau yang lebih baru, Anda tetap dapat menggunakan API OpenSL ES tetapi latensi output-nya mungkin lebih tinggi. Jalur latensi output yang lebih rendah hanya digunakan jika aplikasi meminta ukuran buffer dan frekuensi sampel yang kompatibel dengan konfigurasi output native perangkat. Semua parameter ini bersifat khusus perangkat dan harus diperoleh seperti dijelaskan di bawah.

Mulai Android 4.2 (API level 17), aplikasi dapat membuat kueri untuk mengetahui frekuensi sampel output native platform atau output optimal dan ukuran buffer untuk stream output utama perangkat. Jika dikombinasikan dengan pengujian fitur yang baru disebutkan, sekarang aplikasi dapat mengonfigurasi sendiri secara tepat untuk mencapai output latensi yang lebih rendah di perangkat yang mengklaim dukungan.

Untuk Android 4.2 (API level 17) dan yang lebih lama, buffer berjumlah dua atau lebih diperlukan untuk mencapai latensi yang lebih rendah. Mulai Android 4.3 (API level 18), buffer berjumlah satu sudah cukup untuk mencapai latensi yang lebih rendah.

Semua antarmuka OpenSL ES untuk efek output menghalangi jalur latensi yang lebih rendah.

Urutan yang direkomendasikan adalah seperti berikut:

  1. Memeriksa ketersediaan API level 9 atau yang lebih tinggi untuk mengonfirmasi penggunaan OpenSL ES.
  2. Memeriksa ketersediaan fitur android.hardware.audio.low_latency menggunakan kode seperti ini:

    Kotlin

    import android.content.pm.PackageManager
    ...
    val pm: PackageManager = context.packageManager
    val claimsFeature: Boolean = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY)

    Java

    import android.content.pm.PackageManager;
    ...
    PackageManager pm = getContext().getPackageManager();
    boolean claimsFeature = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY);
  3. Memeriksa ketersediaan API level 17 atau yang lebih baru untuk mengonfirmasi penggunaan android.media.AudioManager.getProperty().
  4. Mendapatkan frekuensi sampel output native atau output optimal dan ukuran buffer untuk stream output utama perangkat menggunakan kode seperti berikut:

    Kotlin

    import android.media.AudioManager
    ...
    val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
    val sampleRate: String = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)
    val framesPerBuffer: String = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER)

    Java

    import android.media.AudioManager;
    ...
    AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    String sampleRate = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
    String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
    Perlu diperhatikan bahwa sampleRate dan framesPerBuffer adalah string. Terlebih dahulu periksa apakah ada nilai null, lalu konversi ke int menggunakan Integer.parseInt().
  5. Sekarang gunakan OpenSL ES untuk membuat AudioPlayer dengan pencari lokasi data antrean buffer PCM.

Catatan: Anda dapat menggunakan aplikasi pengujian Audio Buffer Size untuk menentukan ukuran buffer dan frekuensi sampel native untuk aplikasi audio OpenSL ES di perangkat audio Anda. Anda juga dapat membuka GitHub untuk melihat sampel ukuran-buffer-audio.

Tidak banyak pemutar audio yang memiliki latensi rendah. Jika aplikasi Anda memerlukan lebih dari beberapa sumber audio, sebaiknya campur audio di level aplikasi. Pastikan untuk menghancurkan pemutar audio jika aktivitas dijeda, karena pemutar audio adalah resource global yang digunakan bersama aplikasi lain.

Untuk menghindari gangguan yang terdengar, handler callback antrean buffer harus mengeksekusi dalam jangka waktu yang singkat dan dapat diprediksi. Tindakan ini biasanya menyiratkan tidak adanya pemblokiran tak-terikat pada mutex, kondisi, atau operasi I/O. Sebagai gantinya, pertimbangkan try lock, lock dan wait dengan waktu tunggu, serta algoritme yang tidak memblokir.

Komputasi yang diperlukan untuk merender buffer berikutnya (untuk AudioPlayer) atau menggunakan buffer sebelumnya (untuk AudioRecord) harus memerlukan waktu yang kurang lebih sama untuk setiap callback. Hindari algoritme yang mengeksekusi selama jumlah waktu yang tidak tertentu atau yang komputasinya bersifat burst. Komputasi callback dianggap burst jika waktu CPU yang dihabiskan di callback tertentu secara signifikan lebih besar daripada rata-rata. Singkatnya, waktu eksekusi CPU handler memiliki varian yang mendekati nol, dan handler tidak memblokir selama waktu yang tidak terikat adalah hal yang ideal.

Audio latensi rendah hanya dimungkinkan untuk output berikut:

  • Speaker di perangkat.
  • Headphone berkabel.
  • Headset berkabel.
  • Line keluar.
  • Audio digital USB.

Di perangkat tertentu, latensi speaker lebih tinggi daripada jalur lain karena adanya pemrosesan sinyal digital untuk koreksi dan perlindungan speaker.

Mulai dari Android 5.0 (API Level 21), input audio dengan latensi yang lebih rendah didukung di perangkat tertentu. Untuk memanfaatkan fitur ini, terlebih dahulu pastikan bahwa output latensi rendah tersedia seperti yang dijelaskan di atas. Kemampuan output latensi rendah adalah prasyarat untuk fitur input latensi rendah. Kemudian, buat AudioRecorder dengan frekuensi sampel dan ukuran buffer yang sama dengan yang akan digunakan untuk output. Antarmuka OpenSL ES untuk efek input menghalangi jalur latensi rendah. Preset rekaman SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION harus digunakan untuk latensi rendah; preset ini menonaktifkan pemrosesan sinyal digital khusus perangkat yang dapat menambahkan latensi ke jalur input. Untuk mengetahui informasi selengkapnya tentang preset rekaman, lihat bagian Antarmuka konfigurasi Android di atas.

Untuk mendapatkan input dan output simultan, handler penyelesaian antrean buffer terpisah digunakan untuk setiap sisi. Urutan relatif semua callback atau sinkronisasi jam audio tidak dijamin, sekalipun kedua sisi menggunakan frekuensi sampel yang sama. Aplikasi Anda perlu melakukan buffer data dengan sinkronisasi buffer yang tepat.

Konsekuensi dari jam audio yang berpotensi independen adalah diperlukannya konversi frekuensi sampel asinkron. Teknik sederhana (meskipun tidak ideal untuk kualitas audio) untuk konversi frekuensi sampel asinkron adalah dengan menduplikasi atau menurunkan sampel sesuai keperluan hingga mendekati titik perpotongan-nol. Konversi yang lebih canggih dimungkinkan.

Mode performa

Mulai Android 7.1 (API Level 25), OpenSL ES memperkenalkan cara untuk menentukan mode performa bagi jalur audio. Opsinya adalah:

  • SL_ANDROID_PERFORMANCE_NONE: Tidak ada persyaratan performa tertentu. Mengizinkan efek hardware dan software.
  • SL_ANDROID_PERFORMANCE_LATENCY: Prioritas diberikan ke latensi. Tidak ada efek hardware atau software. Ini adalah mode defaultnya.
  • SL_ANDROID_PERFORMANCE_LATENCY_EFFECTS: Prioritas diberikan ke latensi dengan tetap mengizinkan efek hardware dan software.
  • SL_ANDROID_PERFORMANCE_POWER_SAVING: Prioritas diberikan ke penghematan daya. Mengizinkan efek hardware dan software.

Catatan: Jika Anda tidak memerlukan jalur latensi rendah dan ingin memanfaatkan efek audio bawaan perangkat (misalnya untuk meningkatkan kualitas akustik pemutaran video), Anda harus menyetel mode performa ke SL_ANDROID_PERFORMANCE_NONE secara eksplisit.

Untuk menetapkan mode performa, Anda harus memanggil SetConfiguration menggunakan antarmuka konfigurasi Android, sebagaimana ditunjukkan di bawah ini:

  // Obtain the Android configuration interface using a previously configured SLObjectItf.
  SLAndroidConfigurationItf configItf = nullptr;
  (*objItf)->GetInterface(objItf, SL_IID_ANDROIDCONFIGURATION, &configItf);

  // Set the performance mode.
  SLuint32 performanceMode = SL_ANDROID_PERFORMANCE_NONE;
    result = (*configItf)->SetConfiguration(configItf, SL_ANDROID_KEY_PERFORMANCE_MODE,
                                                     &performanceMode, sizeof(performanceMode));

Keamanan dan izin

Sejauh siapa-dapat-melakukan-apa, keamanan di Android dilakukan di level proses. Kode bahasa pemrograman Java tidak dapat melakukan apa pun melebihi kode native, dan kode native pun tidak dapat melakukan sesuatu yang melebihi kode bahasa pemrograman Java. Satu-satunya perbedaan di antara keduanya adalah API yang tersedia.

Aplikasi yang menggunakan OpenSL ES harus meminta izin yang akan diperlukan untuk API non-native serupa. Misalnya, jika aplikasi Anda merekam audio, maka aplikasi tersebut memerlukan izin android.permission.RECORD_AUDIO. Aplikasi yang menggunakan efek audio memerlukan android.permission.MODIFY_AUDIO_SETTINGS. Aplikasi yang memutar resource URI jaringan memerlukan android.permission.NETWORK. Untuk mengetahui informasi selengkapnya, lihat Bekerja dengan Izin Sistem.

Bergantung pada versi platform dan implementasi, parser konten media dan codec software dapat dijalankan di dalam konteks aplikasi Android yang memanggil OpenSL ES (codec hardware diabstraksi tetapi bergantung pada perangkat). Konten salah format yang didesain untuk mengeksploitasi kerentanan parser dan codec merupakan vektor serangan yang umum. Kami merekomendasikan Anda hanya memutar media dari sumber tepercaya, atau Anda melakukan partisi aplikasi sehingga kode yang menangani media dari sumber tidak tepercaya berjalan dalam lingkungan yang relatif telah di-sandbox. Misalnya, Anda dapat memproses media dari sumber tidak tepercaya dalam proses terpisah. Meskipun kedua proses ini akan tetap berjalan dengan UID yang sama, pemisahan ini menjadikan serangan lebih sulit dilakukan.