Membuat Gelombang Bagian 1 - Mem-build Synthesizer

Mari kita meriahkan! Dalam codelab ini, kita akan menggunakan AAudio API untuk mem-build aplikasi synthesizer berkontrol sentuh dengan latensi rendah untuk Android.

Aplikasi kita akan menghasilkan suara secepat mungkin setelah pengguna menyentuh layar. Keterlambatan antara input dan output disebut sebagai latensi. Memahami dan meminimalkan latensi adalah kunci dalam membuat pengalaman audio yang luar biasa. Faktanya, alasan utama kita menggunakan AAudio adalah karena kemampuannya untuk membuat streaming audio dengan latensi rendah.

Yang akan Anda pelajari

  • Konsep dasar untuk membuat aplikasi audio dengan latensi rendah
  • Cara membuat streaming audio
  • Cara menangani perangkat audio yang terhubung dan terputus
  • Cara menghasilkan data audio dan meneruskannya ke streaming audio
  • Praktik terbaik berkomunikasi antara Java dan C++
  • Cara mendengarkan peristiwa sentuh di UI

Yang Anda butuhkan

Aplikasi menghasilkan suara sintesis saat pengguna mengetuk layar. Berikut arsitekturnya:

213d64e35fa7035c.png

Aplikasi synthesizer 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 pemutaran dan menyiapkan callback data yang digunakan untuk menyediakan data ke streaming
  • Osilator - Class C++ ini menghasilkan data audio digital menggunakan formula matematika sederhana untuk menghitung bentuk gelombang sinusoidal

Mulailah dengan membuat project baru di Android Studio:

  • File -> New -> New Project...
  • Beri nama project Anda "WakeMaker"

Saat mengikuti wizard penyiapan project, ubah nilai default menjadi:

  • Menyertakan dukungan C++
  • SDK Minimum Ponsel dan Tablet: API 26: Android O
  • C++ Standar: C++11

Catatan: Jika Anda perlu merujuk ke kode sumber yang telah selesai untuk aplikasi WaveMaker, kode sumber ada di sini.

Karena osilator adalah objek yang menghasilkan data audio, sebaiknya mulai dengan hal ini. Kita akan membuatnya tetap sederhana dan membuatnya menghasilkan gelombang sinus 440 Hz.

Dasar sintesis digital

Osilator adalah elemen penyusun dasar sintesis digital. Osilator perlu menghasilkan serangkaian angka, yang dikenal sebagai sampel. Setiap sampel mewakili nilai amplitudo yang dikonversi oleh hardware audio menjadi tegangan untuk mendorong headphone atau speaker.

Berikut adalah plot sampel yang mewakili gelombang sinus:

5e5f107a4b6a2a48.png

Sebelum memulai implementasi, berikut beberapa istilah penting untuk data audio digital:

  • Format sampel - Jenis data yang digunakan untuk mewakili setiap sampel. Format sampel umum termasuk PCM16 dan floating point. Kita akan menggunakan floating point karena resolusi 24-bit-nya dan presisi yang disempurnakan pada volume rendah, di antara alasan lainnya.
  • Frame - Saat membuat audio multi-saluran, sampel dikelompokkan dalam frame. Setiap sampel dalam frame sesuai dengan saluran audio yang berbeda. Misalnya, audio stereo memiliki 2 saluran (kiri dan kanan) sehingga frame audio stereo memiliki 2 sampel, satu untuk saluran kiri dan satu untuk saluran kanan.
  • Kecepatan frame - Jumlah frame per detik. Ini sering disebut sebagai frekuensi sampel. Kecepatan frame dan frekuensi sampel biasanya memiliki arti yang sama dan digunakan secara bergantian. Nilai kecepatan frame yang umum adalah 44.100 dan 48.000 frame per detik. AAudio menggunakan istilah frekuensi sampel, jadi kita akan menggunakan konvensi tersebut dalam aplikasi.

Membuat file sumber dan file header

Klik kanan pada folder /app/cpp dan buka New->C++ class.

31d616d7c001c02e.png

Beri nama class dengan "Oscillator".

59ce6364705b3c3c.png

Tambahkan file sumber C++ ke build dengan menambahkan baris berikut ke CMakeLists.txt. Ini dapat ditemukan di bagian External Build Files pada jendela Project.

add_library(...existing source filenames...
src/main/cpp/Oscillator.cpp)

Pastikan project Anda berhasil dibuat.

Menambahkan kode

Tambahkan kode berikut ke file Oscillator.h:

#include <atomic>
#include <stdint.h>

class Oscillator {
public:
    void setWaveOn(bool isWaveOn);
    void setSampleRate(int32_t sampleRate);
    void render(float *audioData, int32_t numFrames);

private:
    std::atomic<bool> isWaveOn_{false};
    double phase_ = 0.0;
    double phaseIncrement_ = 0.0;
};

Berikutnya, tambahkan kode berikut ke file Oscillator.cpp:

#include "Oscillator.h"
#include <math.h>

#define TWO_PI (3.14159 * 2)
#define AMPLITUDE 0.3
#define FREQUENCY 440.0

void Oscillator::setSampleRate(int32_t sampleRate) {
    phaseIncrement_ = (TWO_PI * FREQUENCY) / (double) sampleRate;
}

void Oscillator::setWaveOn(bool isWaveOn) {
    isWaveOn_.store(isWaveOn);
}

void Oscillator::render(float *audioData, int32_t numFrames) {

    if (!isWaveOn_.load()) phase_ = 0;

    for (int i = 0; i < numFrames; i++) {

        if (isWaveOn_.load()) {

            // Calculates the next sample value for the sine wave.
            audioData[i] = (float) (sin(phase_) * AMPLITUDE);

            // Increments the phase, handling wrap around.
            phase_ += phaseIncrement_;
            if (phase_ > TWO_PI) phase_ -= TWO_PI;

        } else {
            // Outputs silence by setting sample value to zero.
            audioData[i] = 0;
        }
    }
}

void setSampleRate(int32_t sampleRate) memungkinkan kita menetapkan frekuensi sampel yang diinginkan untuk data audio (selengkapnya terkait mengapa kita membutuhkan ini akan dijelaskan nanti). Berdasarkan sampleRate dan FREQUENCY, ini akan menghitung nilai phaseIncrement_ yang digunakan di render. Jika Anda ingin mengubah nada gelombang sinus, cukup perbarui FREQUENCY dengan nilai baru.

void setWaveOn(bool isWaveOn) adalah metode penyetel untuk kolom isWaveOn_. Ini digunakan di render untuk menentukan apakah akan menghasilkan gelombang sinus atau senyap.

void render(float *audioData, int32_t numFrames) menempatkan nilai gelombang sinus floating point ke dalam array audioData setiap kali dipanggil.

numFrames adalah jumlah frame audio yang harus kita render. Agar tetap sederhana, osilator menghasilkan satu sampel per frame, yaitu mono.

phase_ menyimpan fase gelombang saat ini, dan ditingkatkan oleh phaseIncrement_ setelah setiap sampel dihasilkan.

Jika isWaveOn_ false, kita hanya menghasilkan nol (senyap).

Osilator kita sudah selesai! Namun, bagaimana kita dapat mendengar gelombang sinus? Untuk itu, kita memerlukan mesin audio...

Mesin audio bertanggung jawab untuk:

  • Menyiapkan streaming audio ke perangkat audio default
  • Menghubungkan osilator ke streaming audio menggunakan callback data
  • Mengaktifkan dan menonaktifkan output gelombang osilator
  • Menutup streaming jika tidak diperlukan lagi

Jika belum, sebaiknya Anda membiasakan diri dengan AAudio API, karena ini membahas konsep penting di balik pembuatan streaming dan pengelolaan status streaming.

Membuat sumber dan header

Seperti langkah sebelumnya, buat class C++ bernama "AudioEngine".

Tambahkan file sumber C++ dan library AAudio ke build dengan menambahkan baris berikut ke CMakeLists.txt

add_library(...existing source files...
src/main/cpp/AudioEngine.cpp )

target_link_libraries(...existing libraries...
aaudio)

Menambahkan kode

Tambahkan kode berikut ke file AudioEngine.h:

#include <aaudio/AAudio.h>
#include "Oscillator.h"

class AudioEngine {
public:
    bool start();
    void stop();
    void restart();
    void setToneOn(bool isToneOn);

private:
    Oscillator oscillator_;
    AAudioStream *stream_;
};

Berikutnya, tambahkan kode berikut ke file AudioEngine.cpp:

#include <android/log.h>
#include "AudioEngine.h"
#include <thread>
#include <mutex>

// Double-buffering offers a good tradeoff between latency and protection against glitches.
constexpr int32_t kBufferSizeInBursts = 2;

aaudio_data_callback_result_t dataCallback(
        AAudioStream *stream,
        void *userData,
        void *audioData,
        int32_t numFrames) {

    ((Oscillator *) (userData))->render(static_cast<float *>(audioData), numFrames);
    return AAUDIO_CALLBACK_RESULT_CONTINUE;
}

void errorCallback(AAudioStream *stream,
                  void *userData,
                  aaudio_result_t error){
   if (error == AAUDIO_ERROR_DISCONNECTED){
       std::function<void(void)> restartFunction = std::bind(&AudioEngine::restart,
                                                           static_cast<AudioEngine *>(userData));
       new std::thread(restartFunction);
   }
}

bool AudioEngine::start() {
    AAudioStreamBuilder *streamBuilder;
    AAudio_createStreamBuilder(&streamBuilder);
    AAudioStreamBuilder_setFormat(streamBuilder, AAUDIO_FORMAT_PCM_FLOAT);
    AAudioStreamBuilder_setChannelCount(streamBuilder, 1);
    AAudioStreamBuilder_setPerformanceMode(streamBuilder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
    AAudioStreamBuilder_setDataCallback(streamBuilder, ::dataCallback, &oscillator_);
    AAudioStreamBuilder_setErrorCallback(streamBuilder, ::errorCallback, this);

    // Opens the stream.
    aaudio_result_t result = AAudioStreamBuilder_openStream(streamBuilder, &stream_);
    if (result != AAUDIO_OK) {
        __android_log_print(ANDROID_LOG_ERROR, "AudioEngine", "Error opening stream %s",
                            AAudio_convertResultToText(result));
        return false;
    }

    // Retrieves the sample rate of the stream for our oscillator.
    int32_t sampleRate = AAudioStream_getSampleRate(stream_);
    oscillator_.setSampleRate(sampleRate);

    // Sets the buffer size.
    AAudioStream_setBufferSizeInFrames(
           stream_, AAudioStream_getFramesPerBurst(stream_) * kBufferSizeInBursts);

    // Starts the stream.
    result = AAudioStream_requestStart(stream_);
    if (result != AAUDIO_OK) {
        __android_log_print(ANDROID_LOG_ERROR, "AudioEngine", "Error starting stream %s",
                            AAudio_convertResultToText(result));
        return false;
    }

    AAudioStreamBuilder_delete(streamBuilder);
    return true;
}

void AudioEngine::restart(){

   static std::mutex restartingLock;
   if (restartingLock.try_lock()){
       stop();
       start();
       restartingLock.unlock();
   }
}

void AudioEngine::stop() {
    if (stream_ != nullptr) {
        AAudioStream_requestStop(stream_);
        AAudioStream_close(stream_);
    }
}

void AudioEngine::setToneOn(bool isToneOn) {
    oscillator_.setWaveOn(isToneOn);
}

Inilah fungsi kode tersebut...

Memulai mesin

Metode start() menyiapkan streaming audio. Streaming audio dalam AAudio diwakili oleh objek AAudioStream, dan untuk membuat objek, kita memerlukan AAudioStreamBuilder:

AAudioStreamBuilder *streamBuilder;
AAudio_createStreamBuilder(&streamBuilder);

Sekarang kita dapat menggunakan streamBuilder untuk menetapkan berbagai parameter pada streaming.

Format audio adalah bilangan floating point:

AAudioStreamBuilder_setFormat(streamBuilder, AAUDIO_FORMAT_PCM_FLOAT);

Kita akan menghasilkan output dalam mono (satu saluran):

AAudioStreamBuilder_setChannelCount(streamBuilder, 1);

Catatan: Kita tidak menetapkan beberapa parameter agar AAudio melakukannya secara otomatis, termasuk:

  • ID perangkat audio - kita ingin menggunakan perangkat audio default, bukan menentukannya secara eksplisit, seperti speaker bawaan. Daftar perangkat audio yang memungkinkan dapat diperoleh menggunakan AudioManager.getDevices().
  • Arah aliran data - aliran data output dibuat secara default. Jika ingin membuat rekaman, kita akan menentukan aliran data input.
  • Frekuensi sampel (selengkapnya tentang hal ini akan dibahas nanti).

Mode performa

Kita menginginkan latensi serendah mungkin sehingga menetapkan mode performa latensi rendah:

AAudioStreamBuilder_setPerformanceMode(streamBuilder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);

AAudio tidak menjamin bahwa aliran data yang dihasilkan memiliki mode performa latensi rendah ini. Alasan tidak memiliki mode ini mencakup:

  • Anda menentukan frekuensi sampel non-native, format sampel, atau sampel per frame (baca selengkapnya di bawah), yang dapat menyebabkan resampling atau konversi format. Resampling adalah proses penghitungan ulang nilai sampel ke dalam frekuensi yang berbeda. Resampling dan konversi format dapat menambah beban komputasi dan/atau latensi.
  • Tidak ada aliran data latensi rendah yang tersedia, mungkin karena semuanya digunakan oleh aplikasi Anda atau aplikasi lain

Anda dapat memeriksa mode performa aliran data menggunakan AAudioStream_getPerformanceMode.

Membuka aliran data

Setelah semua parameter ditetapkan (callback data akan dibahas nanti), kita membuka aliran data dan memeriksa hasilnya:

aaudio_result_t result = AAudioStreamBuilder_openStream(streamBuilder, &stream_);

Jika hasilnya apa saja kecuali AAUDIO_OK, kita akan mencatat output ke jendela Android Monitor di Android Studio dan menampilkan false.

if (result != AAUDIO_OK){
__android_log_print(ANDROID_LOG_ERROR, "AudioEngine", "Error opening stream", AAudio_convertResultToText(result));
        return false;
}

Menetapkan frekuensi sampel osilator

Kita sengaja tidak menetapkan frekuensi sampel aliran data, karena ingin menggunakan frekuensi sampel native - yaitu frekuensi yang menghindari resampling dan penambahan latensi. Setelah aliran data dibuka, kita dapat mengkuerinya untuk mengetahui apa itu frekuensi sampel native:

int32_t sampleRate = AAudioStream_getSampleRate(stream_);

Lalu kita memberi perintah osilator untuk menghasilkan data audio menggunakan frekuensi sampel ini:

oscillator_.setSampleRate(sampleRate);

Menetapkan ukuran buffer

Ukuran buffer internal aliran data langsung memengaruhi latensi aliran data. Semakin besar ukuran buffer, semakin besar latensinya.

Kita akan menetapkan ukuran buffer menjadi dua kali ukuran burst. Sebuah burst adalah jumlah data terpisah yang ditulis selama setiap callback. Hal ini menawarkan kompromi yang baik antara perlindungan latensi dan underrun. Anda dapat membaca selengkapnya tentang penyempurnaan ukuran buffer di dokumentasi AAudio.

AAudioStream_setBufferSizeInFrames(
           stream_, AAudioStream_getFramesPerBurst(stream_) * kBufferSizeInBursts);

Memulai aliran data

Setelah semuanya siap, kita dapat memulai aliran data yang menyebabkannya mulai memakai data audio dan memicu callback data.

result = AAudioStream_requestStart(stream_);

Callback data

Jadi, bagaimana cara mendapatkan data audio ke aliran data? Kita memiliki dua opsi:

Kita akan menggunakan pendekatan kedua karena lebih baik untuk aplikasi dengan latensi rendah; fungsi callback data dipanggil dari thread prioritas tinggi setiap kali aliran data memerlukan data audio.

Fungsi dataCallback

Kita mulai dengan menentukan fungsi callback dalam namespace global:

aaudio_data_callback_result_t dataCallback(
    AAudioStream *stream,
    void *userData,
    void *audioData,
    int32_t numFrames){
        ...
}

Bagian pintar di sini adalah parameter userData adalah pointer ke objek Oscillator. Jadi, kita dapat menggunakannya untuk merender data audio ke dalam array audioData. Berikut caranya:

((Oscillator *)(userData))->render(static_cast<float*>(audioData), numFrames);

Perhatikan bahwa kita juga mentransmisikan array audioData ke bilangan floating point karena itulah format yang diinginkan oleh metode render().

Terakhir, metode ini menampilkan nilai yang memberi tahu aliran data untuk terus menggunakan data audio.

return AAUDIO_CALLBACK_RESULT_CONTINUE;

Menyiapkan callback

Setelah kita memiliki fungsi dataCallback, memberi tahu aliran data untuk menggunakannya dari metode start() menjadi mudah (:: menunjukkan bahwa fungsi tersebut ada dalam namespace global):

AAudioStreamBuilder_setDataCallback(streamBuilder, ::dataCallback, &oscillator_);

Memulai dan menghentikan osilator

Mengaktifkan dan menonaktifkan output gelombang osilator sangat mudah, kita hanya memiliki satu metode yang meneruskan status nada ke osilator:

void AudioEngine::setToneOn(bool isToneOn) {
  oscillator_.setWaveOn(isToneOn);
}

Perlu diperhatikan bahwa meskipun gelombang osilator nonaktif, metode render()-nya tetap menghasilkan data audio yang berisi nol (lihat menghindari pemanasan latensi di atas).

Merapikan

Kita telah menyediakan metode start() yang membuat aliran data, jadi kita juga harus menyediakan metode stop() terkait yang menghapusnya. Metode ini dapat dipanggil kapan pun aliran data tidak lagi diperlukan (misalnya saat aplikasi ditutup). Metode ini menghentikan aliran data yang menghentikan callback, dan menutup aliran data yang menyebabkannya dihapus.

AAudioStream_requestStop(stream_);
AAudioStream_close(stream_);

Menangani aliran data terputus menggunakan callback error

Saat aliran data pemutaran dimulai, perangkat audio default akan digunakan. Ini mungkin berupa speaker bawaan, headphone, atau perangkat audio lainnya seperti antarmuka audio USB.

Apa yang terjadi jika perangkat audio default berubah? Misalnya, jika pengguna memulai pemutaran melalui speaker lalu menghubungkan headphone. Dalam hal ini, aliran data audio menjadi terputus dari speaker dan aplikasi tidak lagi dapat menulis sampel audio ke output. Itu hanya berhenti berputar.

Pengguna mungkin tidak mengharapkan hal ini. Audio harus terus diputar melalui headphone. (Namun, terdapat skenario lain saat menghentikan pemutaran mungkin lebih sesuai.)

Kita memerlukan callback untuk mendeteksi pemutusan aliran data, dan fungsi untuk memulai ulang aliran data ke perangkat audio baru, jika sesuai.

Menyiapkan callback error

Untuk menperhatikan peristiwa pemutusan aliran data, tentukan fungsi jenis AAudioStream_errorCallback.

void errorCallback(AAudioStream *stream,
                  void *userData,
                  aaudio_result_t error){
   if (error == AAUDIO_ERROR_DISCONNECTED){
       std::function<void(void)> restartFunction = std::bind(&AudioEngine::restart,
                                                           static_cast<AudioEngine *>(userData));
       new std::thread(restartFunction);
   }
}

Fungsi ini akan dipanggil setiap kali terjadi error pada aliran data. Jika errornya adalah AAUDIO_ERROR_DISCONNECTED, kita dapat memulai ulang aliran data.

Perhatikan bahwa callback tidak dapat memulai ulang aliran data audio secara langsung. Sebagai gantinya, untuk memulai ulang aliran data, kita membuat std::function yang mengarah ke AudioEngine::restart(), lalu memanggil fungsi dari std::thread yang terpisah.

Terakhir, kita menetapkan errorCallback dengan cara yang sama seperti yang dilakukan untuk dataCallback di start().

AAudioStreamBuilder_setErrorCallback(streamBuilder, ::errorCallback, this);

Memulai ulang aliran data

Karena fungsi mulai ulang dapat dipanggil dari beberapa thread (misalnya, jika kita menerima beberapa peristiwa pemutusan secara berurutan), kita melindungi bagian penting dari kode dengan std::mutex.

void AudioEngine::restart(){

    static std::mutex restartingLock;
    if (restartingLock.try_lock()){
        stop();
        start();
        restartingLock.unlock();
    }
}

Cukup untuk mesin audio, dan tidak ada lagi yang perlu dilakukan...

Kita memerlukan cara agar UI di Java dapat berbicara dengan class C++, di sinilah JNI berperan. Tanda tangan metodenya mungkin bukan yang terbaik untuk dilihat, tetapi untungnya hanya ada tiga tanda tangan!

Ganti nama file native-lib.cpp menjadi jni-bridge.cpp. Anda dapat membiarkan nama file apa adanya, namun perlu diingat bahwa file C++ ini adalah untuk metode JNI. Pastikan untuk mengupdate CMakeLists.txt dengan file yang telah diganti namanya (tetapi biarkan nama library sebagai native-lib).

Tambahkan kode berikut ke jni-bridge.cpp:

#include <jni.h>
#include <android/input.h>
#include "AudioEngine.h"

static AudioEngine *audioEngine = new AudioEngine();

extern "C" {

JNIEXPORT void JNICALL
Java_com_example_wavemaker_MainActivity_touchEvent(JNIEnv *env, jobject obj, jint action) {
    switch (action) {
        case AMOTION_EVENT_ACTION_DOWN:
            audioEngine->setToneOn(true);
            break;
        case AMOTION_EVENT_ACTION_UP:
            audioEngine->setToneOn(false);
            break;
        default:
            break;
    }
}

JNIEXPORT void JNICALL
Java_com_example_wavemaker_MainActivity_startEngine(JNIEnv *env, jobject /* this */) {
    audioEngine->start();
}

JNIEXPORT void JNICALL
Java_com_example_wavemaker_MainActivity_stopEngine(JNIEnv *env, jobject /* this */) {
    audioEngine->stop();
}

}

Jembatan JNI kita cukup sederhana:

  • Kita membuat instance statis AudioEngine
  • startEngine() dan stopEngine() memulai dan menghentikan mesin audio
  • touchEvent() menerjemahkan peristiwa sentuh menjadi panggilan metode untuk mengaktifkan dan menonaktifkan nada

Terakhir, mari buat UI dan menghubungkannya ke back end...

Tata Letak

Tata letak kita sangat sederhana (kita akan menyempurnakannya dalam codelab berikutnya). Yang terpenting hanyalah FrameLayout dengan TextView di tengah:

4a039cdf72e4846f.png

Update res/layout/activity_main.xml ke berikut ini:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/touchArea"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.wavemaker.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="@string/tap_anywhere"
        android:textAppearance="@android:style/TextAppearance.Material.Display1" />
</FrameLayout>

Tambahkan resource string untuk @string/tap_anywhere ke res/values/strings.xml:

<resources>
    <string name="app_name">WaveMaker</string>
    <string name="tap_anywhere">Tap anywhere</string>
</resources>

Aktivitas Utama

Sekarang update MainActivity.java dengan kode berikut:

package com.example.wavemaker;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.MotionEvent;

public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("native-lib");
    }

    private native void touchEvent(int action);

    private native void startEngine();

    private native void stopEngine();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        startEngine();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        touchEvent(event.getAction());
        return super.onTouchEvent(event);
    }

    @Override
    public void onDestroy() {
        stopEngine();
        super.onDestroy();
    }
}

Inilah fungsi kode tersebut:

  • Metode private native void sudah ditentukan dalam jni-bridge.cpp, kita perlu mendeklarasikannya di sini agar dapat menggunakannya
  • Peristiwa siklus proses aktivitas onCreate() dan onDestroy() memanggil jembatan JNI untuk memulai dan menghentikan mesin audio
  • Kita mengganti onTouchEvent() untuk menerima semua peristiwa sentuh untuk Activity dan meneruskannya langsung ke jembatan JNI untuk mengaktifkan dan menonaktifkan nada

Aktifkan perangkat uji atau emulator Anda dan jalankan aplikasi WaveMaker. Saat mengetuk layar, Anda akan mendengar gelombang sinus yang jelas dihasilkan!

Oke, jadi aplikasi kita tidak akan memenangkan penghargaan untuk kreativitas musik, tetapi aplikasi ini menunjukkan teknik dasar yang diperlukan untuk menghasilkan audio sintesis dengan latensi rendah di Android.

Jangan khawatir, dalam codelab berikutnya, kita akan membuat aplikasi ini menjadi lebih menarik! Terima kasih telah menyelesaikan codelab ini. Jika ada pertanyaan, hubungi grup android-ndk.

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