웨이브 만들기 1부: 신시사이저 빌드

이번 시간에는 소리가 나는 앱을 만들어 보겠습니다. 이 Codelab에서는 AAudio API를 사용하여 터치로 제어되며 지연 시간이 짧은 Android용 신시사이저 앱을 빌드하겠습니다.

사용자가 화면을 터치하면 최대한 빨리 소리가 나는 앱을 만듭니다. 입력과 출력 간의 지연을 지연 시간이라고 합니다. 탁월한 오디오 환경을 만들려면 지연 시간을 이해하고 최소화하는 것이 중요합니다. 실제로 AAudio를 사용하는 주된 이유는 지연 시간이 짧은 오디오 스트림을 만들 수 있기 때문입니다.

학습할 내용

  • 지연 시간이 짧은 오디오 앱을 만들기 위한 기본 개념
  • 오디오 스트림을 만드는 방법
  • 오디오 기기 연결 및 연결 끊김을 처리하는 방법
  • 오디오 데이터를 생성하여 오디오 스트림에 전달하는 방법
  • 자바와 C++ 간의 통신 권장사항
  • UI에서 터치 이벤트를 수신하는 방법

필요한 항목

앱은 사용자가 화면을 탭하면 합성된 소리를 생성합니다. 아키텍처는 다음과 같습니다.

213d64e35fa7035c.png

신시사이저 앱에는 다음 네 가지 구성요소가 있습니다.

  • UI - 자바로 작성된 MainActivity 클래스는 터치 이벤트를 수신하고 JNI 브리지로 전달하는 역할을 합니다.
  • JNI 브리지 - 이 C++ 파일은 JNI를 사용하여 UI와 C++ 객체 간의 통신 메커니즘을 제공합니다. 이벤트를 UI에서 오디오 엔진으로 전달합니다.
  • 오디오 엔진 - 이 C++ 클래스는 녹음 및 재생 오디오 스트림을 만들고 스트림에 데이터를 공급하는 데 사용되는 데이터 콜백을 설정합니다.
  • 오실레이터 - 이 C++ 클래스는 사인파를 계산하는 간단한 수학 수식을 사용하여 디지털 오디오 데이터를 생성합니다.

먼저 Android 스튜디오에서 새 프로젝트를 만듭니다.

  • File -> New -> New Project...
  • 프로젝트 이름을 'WaveMaker'로 지정

프로젝트 설정 마법사를 진행하면서 기본값을 다음과 같이 변경합니다.

  • C++ 지원 포함
  • 스마트폰 및 태블릿 최소 SDK: API 26, Android O
  • C++ 표준: C++11

참고: 완성된 WaveMaker 앱용 소스 코드를 참고해야 하는 경우 여기에서 찾을 수 있습니다.

오실레이터는 오디오 데이터를 생성하는 객체 역할을 하므로, 오실레이터부터 시작하는 것이 합리적입니다. 단순하게 작업하기 위해 440Hz 사인파를 만들겠습니다.

디지털 합성 기본사항

오실레이터디지털 합성의 기본 구성요소입니다. 우리가 빌드하는 오실레이터는 샘플이라는 일련의 숫자를 생성해야 합니다. 각 샘플은 오디오 하드웨어에 의해 전압으로 변환되어 헤드폰이나 스피커를 구동하는 진폭 값을 나타냅니다.

다음은 사인파를 나타내는 샘플 도표입니다.

5e5f107a4b6a2a48.png

구현을 시작하기 전에 디지털 오디오 데이터와 관련한 중요한 용어를 확인하겠습니다.

  • 샘플 형식 - 각 샘플을 나타내는 데 사용되는 데이터 유형입니다. 일반적인 샘플 형식에는 PCM16, 부동 소수점 등이 있습니다. 여기서는 24비트 분해능을 제공하고 낮은 볼륨에서도 향상된 정밀도를 얻을 수 있는 등 여러 이유로 부동 소수점을 사용합니다.
  • 프레임 - 다중 채널 오디오를 생성할 때 샘플이 프레임으로 그룹화됩니다. 프레임의 각 샘플은 서로 다른 오디오 채널에 해당합니다. 예를 들어 스테레오 오디오에는 왼쪽과 오른쪽의 두 채널이 있으므로 스테레오 오디오의 프레임에는 왼쪽 채널과 오른쪽 채널용으로 하나씩 샘플 2개가 있습니다.
  • 프레임 속도 - 초당 프레임 수입니다. 샘플링 레이트라고 부르는 경우가 많습니다. 프레임 속도와 샘플링 레이트는 일반적으로 서로 같은 의미로 사용됩니다. 일반적인 프레임 속도 값은 44,100fps 및 48,000fps(초당 프레임 수)입니다. AAudio에서는 샘플링 레이트라는 용어가 사용되므로 우리가 빌드하는 앱에서도 이 표기를 사용합니다.

소스 및 헤더 파일 만들기

/app/cpp 폴더를 마우스 오른쪽 버튼으로 클릭하고 New -> C++ Class로 이동합니다.

31d616d7c001c02e.png

클래스 이름을 'Oscillator'로 지정합니다.

59ce6364705b3c3c.png

다음 줄을 CMakeLists.txt에 추가하여 C++ 소스 파일을 빌드에 추가합니다. Project 창의 External Build Files 섹션에서 찾을 수 있습니다.

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

프로젝트가 성공적으로 빌드되는지 확인합니다.

코드 추가

다음 코드를 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;
};

다음 코드를 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)를 사용하면 오디오 데이터에 원하는 샘플링 레이트를 설정할 수 있습니다. 필요한 이유는 이후에 자세히 알아보겠습니다. sampleRateFREQUENCY를 기반으로 render에서 사용되는 phaseIncrement_ 값이 계산됩니다. 사인파의 음높이를 변경하려면 FREQUENCY를 새 값으로 업데이트하면 됩니다.

void setWaveOn(bool isWaveOn)isWaveOn_ 필드의 setter 메서드입니다. render에서 사인파를 출력할지 또는 무음을 출력할지 결정하는 데 사용됩니다.

void render(float *audioData, int32_t numFrames)는 호출될 때마다 audioData 배열에 부동 소수점 사인파 값을 넣습니다.

numFrames는 렌더링해야 하는 오디오 프레임의 수입니다. 작업을 단순화하기 위해, 프레임당 단일 샘플(즉, 모노)을 출력하는 오실레이터로 만들겠습니다.

phase_는 현재 웨이브 위상을 저장하며 각 샘플이 생성된 후에 phaseIncrement_ 값을 기준으로 증가합니다.

isWaveOn_false면 0(무음)을 출력합니다.

이렇게 오실레이터가 완성되었습니다. 사인파를 들으려면 어떻게 해야 할까요? 오디오 엔진이 필요합니다.

우리가 만드는 오디오 엔진은 다음을 처리합니다.

  • 오디오 스트림을 기본 오디오 기기로 설정
  • 데이터 콜백을 사용하여 오디오 스트림에 오실레이터 연결
  • 오실레이터의 웨이브 출력 켜기/끄기
  • 더 이상 필요하지 않은 스트림 종료

아직 하지 않았다면 AAudio API의 내용을 숙지하면 좋습니다. 스트림 빌드와 스트림 상태 관리에 관한 주요 개념을 다루고 있습니다.

소스 및 헤더 만들기

이전 단계와 마찬가지로 'AudioEngine'이라는 C++ 클래스를 만듭니다.

다음 줄을 CMakeLists.txt에 추가하여 C++ 소스 파일과 AAudio 라이브러리를 빌드에 추가합니다.

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

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

코드 추가

다음 코드를 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_;
};

다음 코드를 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);
}

코드의 역할은 다음과 같습니다.

엔진 시작

start() 메서드는 오디오 스트림을 설정합니다. AAudio의 오디오 스트림은 AAudioStream 객체로 표현되며, 이 객체를 만들려면 AAudioStreamBuilder가 필요합니다.

AAudioStreamBuilder *streamBuilder;
AAudio_createStreamBuilder(&streamBuilder);

이제 streamBuilder를 사용하여 스트림에서 다양한 매개변수를 설정할 수 있습니다.

오디오 형식은 부동 소수점 숫자입니다.

AAudioStreamBuilder_setFormat(streamBuilder, AAUDIO_FORMAT_PCM_FLOAT);

다음과 같이 모노(채널 1개)로 출력합니다.

AAudioStreamBuilder_setChannelCount(streamBuilder, 1);

참고: AAudio에서 자동으로 처리되도록 하기 위해 일부 매개변수를 설정하지 않는다면 다음과 같은 매개변수가 해당됩니다.

  • 오디오 기기 ID - 명시적으로 지정하지 않고 내장 스피커와 같은 기본 오디오 기기를 사용하겠습니다. 가능한 오디오 기기의 목록은 AudioManager.getDevices()를 사용하여 얻을 수 있습니다.
  • 스트림 방향 - 기본적으로 출력 스트림이 생성됩니다. 녹음하려는 경우에는 대신 입력 스트림을 지정합니다.
  • 샘플링 레이트(이후에 자세히 알아봅니다.)

성능 모드

지연 시간을 가능한 한 짧게 하기 위해 짧은 지연 시간 성능 모드를 설정합니다.

AAudioStreamBuilder_setPerformanceMode(streamBuilder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);

AAudio는 결과 스트림에서 이러한 짧은 지연 시간 성능 모드를 보장하지 않습니다. 이 모드가 보장되지 않는 이유는 다음과 같습니다.

  • 네이티브가 아닌 샘플링 레이트, 샘플 형식 또는 프레임당 샘플 수(아래에서 자세히 설명)를 지정했으며 이로 인해 리샘플링이나 형식 변환이 발생할 수도 있습니다. 리샘플링은 샘플 값을 다른 레이트로 다시 계산하는 프로세스입니다. 리샘플링과 형식 변환은 모두 계산 부하나 지연 시간을 늘릴 수 있습니다.
  • 앱 또는 다른 앱에서 모두 사용 중이기 때문에 짧은 지연 시간 스트림을 사용할 수 없습니다.

AAudioStream_getPerformanceMode를 사용하여 스트림의 성능 모드를 확인할 수 있습니다.

스트림 열기

모든 매개변수가 설정되면(데이터 콜백에 관해서는 이후에 다룸) 스트림을 열고 결과를 확인합니다.

aaudio_result_t result = AAudioStreamBuilder_openStream(streamBuilder, &stream_);

결과가 AAUDIO_OK아니라면 Android 스튜디오의 Android Monitor 창에 출력을 기록하고 false를 반환합니다.

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

오실레이터 샘플링 레이트 설정

스트림의 샘플링 레이트를 의도적으로 설정하지 않았습니다. 리샘플링 및 추가된 지연 시간을 방지하는 네이티브 샘플링 레이트를 사용할 계획이기 때문입니다. 스트림을 열었으면 이제 쿼리하여 네이티브 샘플링 레이트를 확인할 수 있습니다.

int32_t sampleRate = AAudioStream_getSampleRate(stream_);

그런 다음 이 샘플링 레이트를 사용하여 오디오 데이터를 생성하도록 오실레이터에 지시합니다.

oscillator_.setSampleRate(sampleRate);

버퍼 사이즈 설정

스트림의 내부 버퍼 사이즈는 스트림의 지연 시간에 직접 영향을 미칩니다. 버퍼 사이즈가 클수록 지연 시간이 길어집니다.

버퍼 사이즈를 버스트 크기의 두 배로 설정하겠습니다. 버스트는 각 콜백 중에 기록되는 개별적인 데이터 양입니다. 이렇게 설정하면 지연 시간과 언더런 보호 간의 균형을 극대화할 수 있습니다. AAudio 문서에서 버퍼 사이즈 조정에 관해 자세히 알아볼 수 있습니다.

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

스트림 시작

모든 설정을 마쳤으므로 이제 스트림을 시작하여 오디오 데이터를 소비하고 데이터 콜백을 트리거하기 시작할 수 있습니다.

result = AAudioStream_requestStart(stream_);

데이터 콜백

오디오 데이터를 스트림으로 가져오려면 어떻게 해야 할까요? 다음 2가지 옵션이 있습니다.

여기서는 지연 시간이 짧은 앱에 더 적합한 또 다른 접근 방식을 사용합니다. 이 방식에서는 스트림에 오디오 데이터가 필요할 때마다 우선순위가 높은 스레드에서 데이터 콜백 함수가 호출됩니다.

dataCallback 함수

먼저 전역 네임스페이스에 콜백 함수를 정의합니다.

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

여기서 userData 매개변수는 Oscillator 객체를 가리키는 포인터입니다. 따라서 이 매개변수를 사용하여 오디오 데이터를 audioData 배열로 렌더링할 수 있습니다. 방법은 다음과 같습니다.

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

audioData 배열을 부동 소수점 숫자로 변환할 수도 있습니다. 이 render() 메서드에서 요구되는 형식이기 때문입니다.

마지막으로, 메서드는 오디오 데이터를 계속 소비하도록 스트림에 지시하는 값을 반환합니다.

return AAUDIO_CALLBACK_RESULT_CONTINUE;

콜백 설정

이제 dataCallback 함수가 있으므로 스트림에 start() 메서드의 데이터를 사용하도록 간단히 지시할 수 있습니다. :: 기호는 함수가 전역 네임스페이스에 선언되어 있음을 나타냅니다.

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

오실레이터 시작 및 중지

오실레이터의 웨이브 출력을 켜고 끄는 것은 간단하며 알림음 상태를 오실레이터에 전달하는 단일 메서드만 있습니다.

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

오실레이터의 웨이브가 꺼져 있어도 render() 메서드는 여전히 0으로 채워진 오디오 데이터를 생성합니다(위의 준비 지연 시간 방지 참고).

정리

스트림을 만드는 start() 메서드를 제공했으므로 이제 스트림을 삭제하는 해당하는 stop() 메서드도 제공해야 합니다. 이 메서드는 스트림이 더 이상 필요하지 않을 때마다(예: 앱이 종료될 때) 호출할 수 있습니다. 스트림을 중지하여 콜백을 중지되고, 스트림을 종료하여 삭제합니다.

AAudioStream_requestStop(stream_);
AAudioStream_close(stream_);

오류 콜백을 사용하여 스트림 연결 끊김 처리

재생 스트림이 시작되면 기본 오디오 기기가 사용됩니다. 기본 오디오 기기는 내장 스피커나 헤드폰일 수 있고 USB 오디오 인터페이스 같은 다른 오디오 기기일 수도 있습니다.

기본 오디오 기기가 변경되면 어떻게 될까요? 사용자가 스피커를 통해 재생을 시작한 후에 헤드폰을 연결하는 경우를 예로 들 수 있습니다. 이 경우 오디오 스트림이 스피커에서 연결 해제되고 앱에서 더 이상 오디오 샘플을 출력에 작성할 수 없게 됩니다. 다시 말해서 재생이 중지됩니다.

아마도 사용자가 기대하는 상황은 아닐 것입니다. 오디오는 헤드폰을 통해 계속 재생되어야 합니다. (단, 재생을 중지하는 것이 더 적절한 다른 시나리오도 있습니다.)

스트림 연결 해제를 감지하는 콜백 및 필요한 경우 새 오디오 기기로 스트림을 다시 시작하는 함수가 필요합니다.

오류 콜백 설정

스트림 연결 끊김 이벤트를 수신하려면 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);
   }
}

이 함수는 스트림에 오류가 발생할 때마다 호출됩니다. 오류가 AAUDIO_ERROR_DISCONNECTED인 경우 스트림을 다시 시작할 수 있습니다.

콜백이 직접 오디오 스트림을 다시 시작할 수는 없습니다. 대신, 스트림을 다시 시작하려면 AudioEngine::restart()를 가리키는 std::function을 만든 후 별도의 std::thread에서 함수를 호출합니다.

마지막으로 start()dataCallback과 같은 방식으로 errorCallback을 설정합니다.

AAudioStreamBuilder_setErrorCallback(streamBuilder, ::errorCallback, this);

스트림 다시 시작

예를 들어 빠르게 연속해서 발생하는 여러 개의 연결 끊김 이벤트가 수신되는 경우처럼 여러 스레드에서 다시 시작 함수가 호출될 수 있기 때문에 std::mutex를 사용하여 코드의 중요한 섹션을 보호합니다.

void AudioEngine::restart(){

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

지금까지 오디오 엔진에 관해 설명했습니다. 이제 완료까지 얼마 남지 않았습니다.

자바에서 UI가 C++ 클래스와 통신할 방법이 필요하며, 바로 이 용도로 JNI가 사용됩니다. 메서드 서명이 그리 보기 좋지 않을 수도 있지만 다행히 세 가지뿐입니다.

파일 이름 native-lib.cppjni-bridge.cpp로 바꿉니다. 파일 이름을 그대로 둬도 되지만 이 C++ 파일이 JNI 메서드용이라는 것을 분명히 밝히고 싶습니다. 이름을 바꾼 파일로 CMakeLists.txt를 업데이트해야 합니다. 단, 라이브러리 이름은 native-lib 그대로 둡니다.

다음 코드를 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();
}

}

JNI 브리지는 상당히 단순합니다.

  • AudioEngine의 정적 인스턴스 만들기
  • startEngine()이 오디오 엔진 시작, stopEngine()이 오디오 엔진 중지
  • touchEvent()가 터치 이벤트를 메서드 호출로 변환하여 알림음을 켜고 끔

마지막으로 UI를 만들어 백엔드에 씁니다.

레이아웃

레이아웃은 아주 단순합니다(후속 Codelab에서 개선할 예정). 중앙에 TextView가 있는 FrameLayout입니다.

4a039cdf72e4846f.png

res/layout/activity_main.xml을 다음과 같이 업데이트합니다.

<?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>

res/values/strings.xml@string/tap_anywhere용 문자열 리소스를 추가합니다.

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

기본 활동

이제 다음 코드를 사용하여 MainActivity.java를 업데이트합니다.

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();
    }
}

코드의 역할은 다음과 같습니다.

  • private native void 메서드는 모두 jni-bridge.cpp에 정의되므로 사용하려면 여기서 선언해야 합니다.
  • 활동 수명 주기 이벤트 onCreate()onDestroy()는 JNI 브리지를 호출하여 오디오 엔진을 시작하고 중지합니다.
  • Activity의 모든 터치 이벤트를 수신하고 JNI 브리지에 직접 전달하여 알림음을 켜고 끄도록 onTouchEvent()를 재정의합니다.

테스트 기기나 에뮬레이터를 실행하고 WaveMaker 앱을 실행합니다. 화면을 탭하면 명료한 사인파가 생성되는 것이 들립니다.

이 앱은 음악적 창의성을 인정하는 상을 받지는 않겠지만 Android에서 지연 시간이 짧은 합성 오디오를 생성하는 데 필요한 기본적인 기법을 보여 줍니다.

걱정하지 마세요. 이후 Codelab에서는 훨씬 더 흥미로운 앱을 만들 것입니다! 이 Codelab을 완료해 주셔서 감사합니다. 궁금한 점이 있으면 android-ndk 그룹에 문의해 주세요.

추가 자료

고성능 오디오 샘플

Android NDK 문서의 고성능 오디오 가이드

Android 오디오 권장사항 동영상 - Google I/O 2017