Membuat Lebih Banyak Gelombang - Sampler

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.

7eb653b71774dfed.png

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

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:

a37150c7e35aa3f8.png

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.

f65428e71e9bdbcf.png

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.

cae7ee7b54407790.png

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:

9b3262779d7a0a8c.png

Setelah masing-masing sampel ditulis, indeks tulis berikutnya berpindah sejumlah satu:

2971acee93b9869d.png

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.

488ab2652d0d281d.png

Dan tambahkan setelah sampel dibaca.

1a7fd22f4bbb4940.png

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

789c2ce74c3a839d.png

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 ke SoundRecording menggunakan metode write
  • Periksa nilai hasil write. Jika nol, artinya SoundRecording 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 dari mSoundRecording
  • Konversi audioData dari mono ke stereo menggunakan convertArrayMonoToStereo
  • Jika jumlah frame yang dibaca kurang dari jumlah frame yang diminta, kita telah mencapai akhir dari data yang direkam. Hentikan pemutaran dengan menyetel mIsPlaying ke false

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.

7eb653b71774dfed.png

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

Praktik Terbaik untuk video Audio Android - Google I/O 2017