Dalam codelab ini, kita akan mem-build sampler audio. Aplikasi ini merekam audio dari mikrofon bawaan ponsel dan memutarnya kembali.
Aplikasi merekam audio hingga 10 detik saat tombol Rekam ditekan. Saat menekan Putar, audio yang direkam akan diputar sekali (saat Anda menahan tombol). Atau, Anda dapat mengaktifkan Loop yang memutar ulang rekaman berulang kali hingga tombol Putar dilepas. Setiap kali Anda menekan Rekam, rekaman audio sebelumnya akan ditimpa.
Yang akan Anda pelajari
- Konsep dasar untuk membuat streaming rekaman dengan latensi rendah
- Cara menyimpan dan memutar data audio yang direkam dari mikrofon
Prasyarat
Sebelum memulai codelab ini, sebaiknya Anda mempertimbangkan untuk menyelesaikan codelab WaveMaker Bagian 1. Codelab tersebut mencakup beberapa konsep dasar untuk membuat streaming audio yang tidak dibahas di sini.
Yang Anda butuhkan
- Android Studio 3.0.0 atau yang lebih baru
- Android 8.0 (API level 26) SDK
- NDK dan Build Tools yang terinstal
- Simulator atau perangkat Android yang menjalankan Android 8.0 (API level 26) atau yang lebih baru untuk pengujian
- Sedikit pengetahuan tentang C++ akan membantu, tetapi tidak diwajibkan
Aplikasi sampler kita memiliki empat komponen:
- UI - Ditulis dalam Java, class MainActivity bertanggung jawab menerima peristiwa sentuh dan meneruskannya ke jembatan JNI
- Jembatan JNI - File C++ ini menggunakan JNI untuk menyediakan mekanisme komunikasi antara UI dan objek C++. Jembatan ini meneruskan peristiwa dari UI ke Mesin Audio.
- Mesin audio - Class C++ ini membuat streaming audio rekaman dan pemutaran.
- Rekaman suara - Class C++ ini menyimpan data audio dalam memori.
Berikut arsitekturnya:
Meng-clone project
Clone repositori codelab di github.
git clone https://github.com/googlecodelabs/android-wavemaker2
Mengimpor project ke Android Studio
Buka Android Studio dan impor project:
- File -> New -> Impor project...
- Pilih folder "android-wavemaker2"
Menjalankan project
Pilih konfigurasi base run.
Kemudian, tekan CTRL+R untuk mem-build dan menjalankan aplikasi template - aplikasi ini harus dikompilasi dan dijalankan, tetapi tidak berfungsi. Anda akan menambahkan fungsinya selama codelab ini.
Membuka modul dasar
File yang akan Anda kerjakan untuk codelab ini disimpan dalam modul base
. Luaskan modul ini di jendela Project, dengan memastikan tampilan Android dipilih.
Catatan: Kode sumber yang telah selesai untuk aplikasi WaveMaker2 dapat ditemukan di modul final
.
Objek SoundRecording
mewakili rekaman data audio dalam memori. Objek ini memungkinkan aplikasi menulis data dari mikrofon ke memori dan membaca data untuk pemutaran.
Mari mulai dengan mencari tahu cara menyimpan data audio ini.
Memilih format audio
Pertama, kita perlu memilih format audio untuk sampel. AAudio mendukung dua format:
float
: Floating point presisi tunggal (4 byte per sampel)int16_t
: Bilangan bulat 16-bit (2 byte per sampel)
Untuk kualitas suara yang bagus pada volume rendah dan alasan lainnya, kita menggunakan sampel float
. Jika kapasitas memori menjadi masalah, Anda dapat mengorbankan fidelitas dan mendapatkan ruang dengan menggunakan bilangan bulat 16-bit.
Memilih kapasitas penyimpanan yang diperlukan
Anggaplah kita ingin menyimpan 10 detik data audio. Pada frekuensi sampel sebesar 48.000 sampel per detik, yang merupakan frekuensi sampel paling umum di perangkat Android modern, kita perlu mengalokasikan memori untuk 480.000 sampel.
Buka base/cpp/SoundRecording.h dan tentukan konstanta ini di bagian atas file.
constexpr int kMaxSamples = 480000; // 10s of audio data @ 48kHz
Menentukan array penyimpanan
Sekarang kita memiliki semua informasi yang diperlukan untuk menentukan array float
. Tambahkan deklarasi berikut ke SoundRecording.h:
private:
std::array<float,kMaxSamples> mData { 0 };
{ 0 }
menggunakan inisialisasi gabungan untuk menetapkan semua nilai dalam array ke 0.
Mengimplementasikan write
Metode write
memiliki tanda tangan ini:
int32_t write(const float *sourceData, int32_t numFrames);
Metode ini menerima array sampel audio di sourceData
. Ukuran array ditentukan oleh numFrames
. Metode ini harus menampilkan jumlah sampel yang benar-benar ditulis.
Ini dapat diimplementasikan dengan menyimpan indeks tulis yang tersedia berikutnya. Awalnya bernilai 0:
Setelah masing-masing sampel ditulis, indeks tulis berikutnya berpindah sejumlah satu:
Hal ini dapat diimplementasikan dengan mudah sebagai loop for
. Tambahkan kode berikut ke metode write
di SoundRecording.cpp
for (int i = 0; i < numSamples; ++i) {
mData[mWriteIndex++] = sourceData[i];
}
return numSamples;
Tapi tunggu, bagaimana jika kita mencoba menulis lebih banyak sampel dari ruang yang kita miliki? Hal buruk akan terjadi! Kita akan mendapatkan kesalahan segmentasi yang disebabkan oleh upaya untuk mengakses indeks array di luar batas.
Mari tambahkan pemeriksaan yang mengubah numSamples
jika mData
tidak memiliki cukup ruang. Tambahkan kode berikut di atas kode yang ada.
if (mWriteIndex + numSamples > kMaxSamples) {
numSamples = kMaxSamples - mWriteIndex;
}
Mengimplementasikan read
Metode read
mirip dengan write
. Kita menyimpan indeks baca berikutnya.
Dan tambahkan setelah sampel dibaca.
Kita mengulanginya hingga membaca jumlah sampel yang diminta. Apa yang terjadi saat kita mencapai akhir dari data yang tersedia? Kita memiliki dua perilaku:
- Jika loop diaktifkan: Setel indeks baca ke nol - awal array data
- Jika loop dinonaktifkan: Jangan lakukan apa pun - jangan menambah indeks baca
Untuk kedua perilaku ini, kita perlu mengetahui kapan kita telah mencapai akhir dari data yang tersedia. Mudahnya, mWriteIndex
akan memberi tahu kita. Ini berisi jumlah total sampel yang telah ditulis ke array data.
Mengingat hal ini, sekarang kita dapat mengimplementasikan metode read
di SoundRecording.cpp
int32_t framesRead = 0;
while (framesRead < numSamples && mReadIndex < mWriteIndex){
targetData[framesRead++] = mData[mReadIndex++];
if (mIsLooping && mReadIndex == mWriteIndex) mReadIndex = 0;
}
return framesRead;
AudioEngine
menjalankan tugas utama berikut:
- Membuat instance
SoundRecording
- Membuat streaming rekaman untuk merekam data dari mikrofon
- Menulis data yang direkam ke dalam instance
SoundRecording
di callback streaming rekaman - Membuat streaming pemutaran untuk memutar kembali data yang direkam
- Membaca data yang direkam dari instance
SoundRecording
dalam callback streaming pemutaran - Merespons peristiwa UI untuk perekaman, pemutaran, dan loop
Mari mulai membuat instance SoundRecording
.
Mulailah dengan sesuatu yang mudah. Buat instance SoundRecording
di AudioEngine.h:
private:
SoundRecording mSoundRecording;
Kita memiliki dua streaming untuk dibuat: pemutaran dan perekaman. Mana yang harus dibuat terlebih dahulu?
Kita harus membuat streaming pemutaran terlebih dahulu karena streaming ini hanya memiliki satu frekuensi sampel yang memberikan latensi terendah. Setelah membuatnya, kita kemudian dapat menyediakan frekuensi sampel ke builder streaming rekaman. Ini memastikan bahwa streaming pemutaran dan perekaman memiliki frekuensi sampel yang sama, yang berarti kita tidak perlu melakukan resampling tambahan di antara streaming.
Properti streaming pemutaran
Gunakan properti ini untuk mengisi StreamBuilder
yang akan membuat streaming pemutaran:
- Tujuan: tidak ditentukan, default ke output
- Frekuensi sampel: tidak ditentukan, default ke frekuensi dengan latensi terendah
- Format: float
- Jumlah saluran: 2 (stereo)
- Mode performa: Latensi rendah
- Mode berbagi: Eksklusif
Membuat streaming pemutaran
Kini kita memiliki semua yang dibutuhkan untuk membuat dan membuka streaming pemutaran. Tambahkan kode berikut ke bagian atas metode start
di AudioEngine.cpp.
// Create the playback stream.
StreamBuilder playbackBuilder = makeStreamBuilder();
AAudioStreamBuilder_setFormat(playbackBuilder.get(), AAUDIO_FORMAT_PCM_FLOAT);
AAudioStreamBuilder_setChannelCount(playbackBuilder.get(), kChannelCountStereo);
AAudioStreamBuilder_setPerformanceMode(playbackBuilder.get(), AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStreamBuilder_setSharingMode(playbackBuilder.get(), AAUDIO_SHARING_MODE_EXCLUSIVE);
AAudioStreamBuilder_setDataCallback(playbackBuilder.get(), ::playbackDataCallback, this);
AAudioStreamBuilder_setErrorCallback(playbackBuilder.get(), ::errorCallback, this);
aaudio_result_t result = AAudioStreamBuilder_openStream(playbackBuilder.get(), &mPlaybackStream);
if (result != AAUDIO_OK){
__android_log_print(ANDROID_LOG_DEBUG, __func__,
"Error opening playback stream %s",
AAudio_convertResultToText(result));
return;
}
// Obtain the sample rate from the playback stream so we can request the same sample rate from
// the recording stream.
int32_t sampleRate = AAudioStream_getSampleRate(mPlaybackStream);
result = AAudioStream_requestStart(mPlaybackStream);
if (result != AAUDIO_OK){
__android_log_print(ANDROID_LOG_DEBUG, __func__,
"Error starting playback stream %s",
AAudio_convertResultToText(result));
closeStream(&mPlaybackStream);
return;
}
Perhatikan bahwa metode template untuk data dan callback error telah dibuat untuk Anda. Jika Anda harus merangkum cara kerjanya, lihat kembali codelab pertama.
Properti streaming rekaman
Gunakan properti berikut untuk membuat streaming rekaman:
- Tujuan: input (perekaman adalah input, sedangkan pemutaran adalah output)
- Frekuensi sampel: sama seperti streaming output
- Format: float
- Jumlah saluran: 1 (mono)
- Mode performa: Latensi rendah
- Mode berbagi: Eksklusif
Membuat streaming rekaman
Sekarang tambahkan kode berikut di bawah kode yang ditambahkan sebelumnya di start
.
// Create the recording stream.
StreamBuilder recordingBuilder = makeStreamBuilder();
AAudioStreamBuilder_setDirection(recordingBuilder.get(), AAUDIO_DIRECTION_INPUT);
AAudioStreamBuilder_setPerformanceMode(recordingBuilder.get(), AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStreamBuilder_setSharingMode(recordingBuilder.get(), AAUDIO_SHARING_MODE_EXCLUSIVE);
AAudioStreamBuilder_setFormat(recordingBuilder.get(), AAUDIO_FORMAT_PCM_FLOAT);
AAudioStreamBuilder_setSampleRate(recordingBuilder.get(), sampleRate);
AAudioStreamBuilder_setChannelCount(recordingBuilder.get(), kChannelCountMono);
AAudioStreamBuilder_setDataCallback(recordingBuilder.get(), ::recordingDataCallback, this);
AAudioStreamBuilder_setErrorCallback(recordingBuilder.get(), ::errorCallback, this);
result = AAudioStreamBuilder_openStream(recordingBuilder.get(), &mRecordingStream);
if (result != AAUDIO_OK){
__android_log_print(ANDROID_LOG_DEBUG, __func__,
"Error opening recording stream %s",
AAudio_convertResultToText(result));
closeStream(&mRecordingStream);
return;
}
result = AAudioStream_requestStart(mRecordingStream);
if (result != AAUDIO_OK){
__android_log_print(ANDROID_LOG_DEBUG, __func__,
"Error starting recording stream %s",
AAudio_convertResultToText(result));
return;
}
Sekarang kita masuk ke bagian yang menyenangkan: benar-benar menyimpan data rekaman dari mikrofon ke objek SoundRecording
.
Saat membuat streaming rekaman, kita menentukan callback data sebagai ::recordingDataCallback
. Metode ini memanggil AudioEngine::recordingCallback
yang memiliki tanda tangan berikut:
aaudio_data_callback_result_t AudioEngine::recordingCallback(float *audioData,
int32_t numFrames)
Data audio disediakan di audioData.
Ukurannya (dalam sampel) adalah numFrames
karena hanya ada satu sampel per frame, yang disebabkan oleh perekaman dalam mono.
Yang perlu kita lakukan adalah:
- Periksa
mIsRecording
untuk melihat apakah kita harus merekam - Jika tidak, abaikan data yang masuk
- Jika kita merekam:
- Sediakan
audioData
keSoundRecording
menggunakan metodewrite
- Periksa nilai hasil
write
. Jika nol, artinyaSoundRecording
sudah penuh dan kita harus berhenti merekam - Tampilkan
AAUDIO_CALLBACK_RESULT_CONTINUE
agar callback dilanjutkan
Tambahkan kode berikut ke recordingCallback
:
if (mIsRecording) {
int32_t framesWritten = mSoundRecording.write(audioData, numFrames);
if (framesWritten == 0) mIsRecording = false;
}
return AAUDIO_CALLBACK_RESULT_CONTINUE;
Mirip dengan streaming rekaman, streaming pemutaran akan memanggil playbackDataCallback
saat memerlukan data baru. Metode ini memanggil AudioEngine::playbackCallback,
yang memiliki tanda tangan berikut:
aaudio_data_callback_result_t AudioEngine::playbackCallback(float *audioData, int32_t numFrames)
Di dalam metode ini, kita perlu:
- Mengisi array dengan nol menggunakan
fillArrayWithZeros
- Jika kita memutar data yang direkam, yang ditunjukkan oleh
mIsPlaying
, maka: - Baca
numFrames
data darimSoundRecording
- Konversi
audioData
dari mono ke stereo menggunakanconvertArrayMonoToStereo
- Jika jumlah frame yang dibaca kurang dari jumlah frame yang diminta, kita telah mencapai akhir dari data yang direkam. Hentikan pemutaran dengan menyetel
mIsPlaying
kefalse
Tambahkan kode berikut ke playbackCallback
:
fillArrayWithZeros(audioData, numFrames * kChannelCountStereo);
if (mIsPlaying) {
int32_t framesRead = mSoundRecording.read(audioData, numFrames);
convertArrayMonoToStereo(audioData, framesRead);
if (framesRead < numFrames) mIsPlaying = false;
}
return AAUDIO_CALLBACK_RESULT_CONTINUE;
Memulai dan menghentikan perekaman
Metode setRecording
digunakan untuk memulai dan menghentikan perekaman. Berikut ini tanda tangannya:
void setRecording(bool isRecording)
Saat tombol rekam ditekan, isRecording
bernilai benar (true); saat tombol dilepas isRecording
bernilai salah (false).
Variabel anggota mIsRecording
digunakan untuk mengalihkan penyimpanan data di dalam recordingCallback
. Kita hanya perlu menyetelnya ke nilai isRecording
.
Terdapat satu lagi perilaku yang perlu kita tambahkan. Saat rekaman dimulai, data apa pun yang ada di mSoundRecording
harus ditimpa. Ini dapat dilakukan menggunakan clear
yang akan mereset indeks tulis mSoundRecording
ke nol.
Berikut kode untuk setRecording
:
if (isRecording) mSoundRecording.clear();
mIsRecording = isRecording;
Memulai dan menghentikan pemutaran
Kontrol pemutaran mirip dengan perekaman. setPlaying
dipanggil saat tombol putar ditekan atau dilepas. Variabel anggota mIsPlaying
mengalihkan pemutaran di dalam playbackCallback
.
Saat tombol putar ditekan, kita ingin agar pemutaran dimulai di awal data audio yang direkam. Ini dapat dilakukan menggunakan setReadPositionToStart
yang mereset head baca mSoundRecording
ke nol.
Berikut kode untuk setPlaying
:
if (isPlaying) mSoundRecording.setReadPositionToStart();
mIsPlaying = isPlaying;
Mengalihkan pemutaran berulang
Terakhir, saat tombol LOOP dialihkan, setLooping
dipanggil. Menangani masalah ini mudah. Kita cukup meneruskan argumen isOn
ke mSoundRecording
.setLooping
:
Berikut kode untuk setLooping
:
mSoundRecording.setLooping(isOn);
Aplikasi yang merekam audio harus meminta izin RECORD_AUDIO
dari pengguna. Sebagian besar kode penanganan izin sudah ditulis, tetapi kita masih perlu mendeklarasikan bahwa aplikasi menggunakan izin ini.
Tambahkan baris berikut ke manifests/AndroidManifest.xml di dalam bagian <manifest>
:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
Saatnya melihat apakah semua kerja keras Anda telah membuahkan hasil. Build dan jalankan aplikasi, dan Anda akan melihat UI berikut.
Ketuk tombol merah untuk mulai merekam. Perekaman berlanjut saat Anda terus menekan tombol, hingga maksimum 10 detik. Ketuk tombol hijau untuk memutar data yang direkam. Pemutaran akan dilanjutkan saat Anda terus menekan tombol hingga akhir data audio. Jika LOOP diaktifkan, data audio akan berulang selamanya.
Bacaan lebih lanjut
Sampel audio berperforma tinggi
Panduan audio berperforma tinggi pada dokumentasi Android NDK