Créer un synthétiseur

Faisons du bruit ! Dans cet atelier de programmation, nous allons utiliser l'API AAudio pour créer une appli de synthétiseur tactile à faible latence pour Android.

Lorsque l'utilisateur touche l'écran, l'appli produit un son le plus rapidement possible. Le délai entre cette action et le son produit s'appelle la latence. Afin de la réduire au maximum pour un flux audio de qualité, il est donc essentiel de comprendre ce concept. En fait, nous utilisons AAudio principalement pour sa capacité à créer des flux audio à faible latence.

Points abordés

  • Concepts de base pour créer des applications audio à faible latence
  • Créer des flux audio
  • Gérer les appareils audio connectés et déconnectés
  • Générer des données audio et les transmettre à un flux audio
  • Bonnes pratiques de communication entre Java et C++
  • Détecter les événements tactiles sur votre interface utilisateur

Prérequis

L'appli produit un son synthétisé lorsque l'utilisateur appuie sur l'écran. Voici l'architecture :

213d64e35fa7035c.png

L'application de synthétiseur comporte quatre éléments :

  • Une interface utilisateur : écrite en Java, la classe MainActivity est chargée de recevoir les événements tactiles et de les transférer au pont JNI.
  • Un pont JNI : ce fichier C++ utilise JNI pour fournir un mécanisme de communication entre l'interface utilisateur et les objets C++. Il transmet les événements de l'interface utilisateur au moteur audio.
  • Un moteur audio : cette classe C++ crée le flux audio de lecture et configure le rappel de données utilisé pour transmettre les données au flux.
  • Un oscillateur : cette classe C++ génère des données audio numériques à l'aide d'une formule mathématique simple pour calculer une forme d'onde sinusoïdale.

Commencez par créer un projet dans Android Studio :

  • File -> New -> New Project... (Fichier -> Nouveau -> Nouveau projet)
  • Nommez votre projet WaveMaker

À mesure que vous parcourez l'assistant de configuration du projet, définissez les valeurs par défaut comme suit :

  • Inclure C++ support (Prise en charge C++)
  • SDK minimal pour téléphone et tablette : API 26, Android O
  • Standard C++ : C++11

Remarque : Si vous devez vous référer au code source finalisé de l'application WaveMaker, cliquez ici.

L'oscillateur étant l'objet qui produit les données audio, il est logique que ce soit notre point de départ. Nous allons faire simple pour qu'il crée une onde sinusoïdale de 440 Hz.

Principes de base de la synthèse numérique

L'oscillateur est un élément fondamental de la synthèse numérique. Il doit produire une série de nombres, appelés échantillons. Chaque échantillon représente une valeur d'amplitude que le matériel audio convertit en tension pour alimenter un casque ou une enceinte.

Voici un graphique d'échantillons représentant une onde sinusoïdale :

5e5f107a4b6a2a48.png

Avant de commencer l'implémentation, voici quelques termes importants liés aux données audio numériques :

  • Format d'échantillon : type de données utilisé pour représenter chaque échantillon. Les formats les plus courants sont PCM 16 et à virgule flottante. Nous utiliserons celui à virgule flottante pour sa résolution 24 bits et sa plus grande précision à faible volume, entre autres.
  • Trame : lors de la génération d'un flux audio multicanal, les échantillons sont regroupés dans des trames. Chaque échantillon de la trame correspond à un canal audio différent. Par exemple, l'audio stéréo comprend deux canaux (gauche et droit). Une trame audio stéréo contient ainsi deux échantillons : un pour le canal de gauche et l'autre pour le canal de droite.
  • Fréquence d'images : nombre d'images par seconde. On parle souvent de taux d'échantillonnage. La fréquence d'images et le taux d'échantillonnage signifient généralement la même chose et sont interchangeables. La fréquence d'images courante est de 44 100 ou 48 000 images par seconde. Étant donné que l'API AAudio emploie le terme taux d'échantillonnage, nous l'utilisons également pour notre application.

Créer les fichiers source et d'en-tête

Effectuez un clic droit sur le dossier /app/cpp, puis sélectionnez New ->C++ class (Nouveau > Classe C++).

31d616d7c001c02e.png

Nommez la classe "Oscillator".

59ce6364705b3c3c.png

Ajoutez le fichier source C++ au build en intégrant les lignes suivantes au fichier CMakeLists.txt. Il figure dans la section External Build Files de la fenêtre du projet.

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

Vérifiez que le projet a bien été compilé.

Ajouter le code

Ajoutez le code suivant au fichier 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;
};

Ajoutez ensuite le code suivant au fichier 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) permet de définir le taux d'échantillonnage souhaité pour les données audio (nous préciserons plus tard pourquoi nous en avons besoin). En fonction du sampleRate et de la FREQUENCY, il calcule la valeur de phaseIncrement_, utilisée dans render. Si vous voulez modifier la hauteur de l'onde sinusoïdale, il suffit de remplacer FREQUENCY par une nouvelle valeur.

void setWaveOn(bool isWaveOn) est une méthode setter pour le champ isWaveOn_. Elle est utilisée dans render pour déterminer s'il faut générer l'onde sinusoïdale ou non.

void render(float *audioData, int32_t numFrames) place les valeurs d'onde sinusoïdale à virgule flottante dans le tableau audioData à chaque appel.

numFrames correspond au nombre de trames audio que nous devons afficher. Pour simplifier les choses, l'oscillateur produit un seul échantillon par trame, (c'est-à-dire, mono).

phase_ stocke la phase d'onde actuelle, qui est incrémentée de phaseIncrement_ après la génération de chaque échantillon.

Si isWaveOn_ est défini sur false, les valeurs produites sont nulles (silence).

Voilà pour ce qui est de l'oscillateur ! Mais comment pouvons-nous entendre l'onde sinusoïdale ? Pour cela, nous avons besoin d'un moteur audio.

Le moteur audio a plusieurs responsabilités :

  • Configurer un flux audio sur l'appareil audio par défaut
  • Connecter l'oscillateur au flux audio par un rappel de données
  • Activer et désactiver la création d'une onde par l'oscillateur
  • Fermer le flux lorsqu'il n'est plus nécessaire

Si ce n'est pas déjà fait, familiarisez-vous avec l'API AAudio qui couvre les concepts clés de la création de flux et de la gestion de l'état des flux.

Créer la source et les en-têtes

Comme à l'étape précédente, créez une classe C++ intitulée "AudioEngine".

Ajoutez le fichier source C++ et la bibliothèque AAudio au build en intégrant les lignes suivantes au fichier CMakeLists.txt

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

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

Ajouter le code

Ajoutez le code suivant au fichier 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_;
};

Ajoutez ensuite le code suivant au fichier 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);
}

Voilà ce que fait le code.

Démarrer le moteur

La méthode start() configure un flux audio. Les flux audio dans AAudio sont représentés par l'objet AAudioStream, et pour en créer un, nous avons besoin d'un AAudioStreamBuilder :

AAudioStreamBuilder *streamBuilder;
AAudio_createStreamBuilder(&streamBuilder);

Nous pouvons maintenant utiliser streamBuilder pour définir différents paramètres sur le flux.

Le format audio est à virgule flottante :

AAudioStreamBuilder_setFormat(streamBuilder, AAUDIO_FORMAT_PCM_FLOAT);

Le résultat sera en mono (un seul canal) :

AAudioStreamBuilder_setChannelCount(streamBuilder, 1);

Remarque : Nous n'avons pas défini les paramètres ci-dessous, car nous voulons que l'API AAudio les gère automatiquement :

  • ID de l'appareil audio : nous voulons utiliser l'appareil audio par défaut, plutôt que d'en définir un précis, comme l'enceinte intégrée. La liste des appareils audio possibles peut être obtenue en utilisant AudioManager.getDevices().
  • Sens du flux : par défaut, un flux de sortie est créé. Si nous voulions effectuer un enregistrement, nous devrions spécifier un flux d'entrée à la place.
  • Le taux d'échantillonnage (nous y reviendrons plus en détail ultérieurement).

Mode performance

Pour réduire au maximum la latence, nous avons défini le mode de performance en conséquence :

AAudioStreamBuilder_setPerformanceMode(streamBuilder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);

AAudio ne garantit pas que le flux obtenu présentera une faible latence. Voici, entre autres, pourquoi :

  • Vous avez spécifié un taux d'échantillonnage, un format d'échantillon ou des échantillons par trame non natifs (nous y reviendrons plus en détail ci-dessous), ce qui peut entraîner un rééchantillonnage ou une conversion du format. Le rééchantillonnage est un processus qui consiste à recalculer les valeurs d'échantillon avec un autre taux. Ces deux facteurs (le rééchantillonnage et la conversion du format) peuvent augmenter la charge de calcul et/ou la latence.
  • Aucun flux à faible latence n'est disponible, probablement parce que votre application ou d'autres les utilisent tous.

Vous pouvez vérifier le mode performance de votre flux en utilisant AAudioStream_getPerformanceMode.

Ouvrir le flux

Une fois tous les paramètres définis (nous aborderons plus tard le rappel de données), nous ouvrons le flux et vérifions le résultat :

aaudio_result_t result = AAudioStreamBuilder_openStream(streamBuilder, &stream_);

Si le résultat est différent de AAUDIO_OK, nous l'enregistrons dans la fenêtre Android Monitor d'Android Studio et affichons false.

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

Définir le taux d'échantillonnage avec l'oscillateur

Nous n'avons pas délibérément défini le taux d'échantillonnage du flux, car nous voulons utiliser son taux d'échantillonnage natif, c'est-à-dire le taux qui évite un rééchantillonnage et une augmentation de la latence. Maintenant que le flux est ouvert, nous pouvons l'interroger pour découvrir le taux d'échantillonnage natif :

int32_t sampleRate = AAudioStream_getSampleRate(stream_);

Nous demandons ensuite à l'oscillateur de produire des données audio en utilisant ce taux d'échantillonnage :

oscillator_.setSampleRate(sampleRate);

Définir la taille de la mémoire tampon

La taille de la mémoire tampon interne du flux affecte directement la latence du flux. Plus cette taille est élevée, plus la latence est importante.

Nous allons définir une taille de mémoire tampon deux fois supérieure à celle d'une utilisation intensive. Une utilisation intensive est une quantité discrète de données écrites lors de chaque rappel. Cela constitue un bon compromis entre la latence et la protection contre la sous-utilisation de la mémoire tampon. Vous pouvez en savoir plus sur l'ajustement de la taille de la mémoire tampon dans la documentation AAudio.

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

Démarrer le flux

Maintenant que tout est configuré, nous pouvons démarrer le flux pour qu'il commence à consommer des données audio et à déclencher des rappels de données.

result = AAudioStream_requestStart(stream_);

Rappel des données

Alors, comment pouvons-nous intégrer des données audio dans le flux ? Deux options s'offrent à nous :

Nous utiliserons la seconde option, car elle convient mieux aux applications à faible latence. La fonction de rappel de données est appelée depuis un thread à priorité élevée à chaque fois que le flux nécessite des données audio.

Fonction dataCallback

Nous commençons par définir la fonction de rappel dans l'espace de noms global :

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

Dans ce cas précis, le paramètre userData est un pointeur vers l'objet Oscillator. Nous pouvons donc l'utiliser pour afficher les données audio dans le tableau audioData. Voici comment :

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

Notez que nous convertissons également le tableau audioData en nombres à virgule flottante, car il s'agit du format attendu par la méthode render().

Enfin, la méthode renvoie une valeur indiquant au flux de continuer à consommer des données audio.

return AAUDIO_CALLBACK_RESULT_CONTINUE;

Configurer le rappel

Maintenant que nous disposons de la fonction dataCallback, nous pouvons facilement dire au flux de l'utiliser à partir de la méthode start() (:: indique que la fonction figure dans l'espace de noms global) :

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

Démarrer et arrêter l'oscillateur

Nous pouvons facilement activer et désactiver la création d'une onde par l'oscillateur, car nous n'avons qu'une seule méthode qui transmet l'état du signal sonore à l'oscillateur :

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

Il convient de noter que même lorsque l'onde de l'oscillateur est désactivée, sa méthode render() produit encore des données audio avec des zéros (voir la section Éviter la latence de préchauffage ci-dessus).

Mettre de l'ordre

Nous avons fourni une méthode start() qui crée le flux. Nous devons donc aussi en proposer une (stop()) pour le supprimer. Cette méthode peut être appelée chaque fois que le flux n'est plus nécessaire (par exemple, lorsque notre application se ferme). Le flux s'arrête alors, ce qui arrête les rappels et ferme le flux, provoquant ainsi sa suppression.

AAudioStream_requestStop(stream_);
AAudioStream_close(stream_);

Gérer les déconnexions du flux à l'aide du rappel d'erreur

Lorsque le flux de lecture démarre, il utilise l'appareil audio par défaut. Il peut s'agir de l'enceinte intégrée, d'un casque ou de tout autre appareil audio tel qu'une interface audio USB.

Que se passe-t-il en cas de changement d'appareil audio par défaut ? Par exemple, si l'utilisateur commence la lecture via l'enceinte, puis connecte un casque audio. Dans ce cas, le flux audio est déconnecté de l'enceinte, et votre application ne peut plus écrire d'échantillons audio dans la sortie. La lecture s'arrête simplement.

Ce n'est probablement pas ce qu'attend l'utilisateur. Le contenu audio devrait continuer d'être diffusé via le casque. (Toutefois, il existe d'autres scénarios où l'arrêt de la lecture peut être plus approprié.)

Nous avons besoin d'un rappel pour détecter la déconnexion du flux, ainsi que d'une fonction pour redémarrer le flux sur le nouvel appareil audio, le cas échéant.

Configurer le rappel d'erreur

Pour détecter l'événement de déconnexion du flux, définissez une fonction de type 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);
   }
}

Cette fonction est appelée chaque fois que le flux rencontre une erreur. Si l'erreur est AAUDIO_ERROR_DISCONNECTED, nous pouvons redémarrer le flux.

Notez que le rappel ne peut pas redémarrer directement le flux audio. Au lieu de cela, pour redémarrer le flux, nous créons une fonction std::function qui renvoie vers AudioEngine::restart(), puis appelle la fonction à partir d'un std::thread distinct.

Enfin, nous avons défini le rappel errorCallback comme nous l'avons fait pour dataCallback dans start().

AAudioStreamBuilder_setErrorCallback(streamBuilder, ::errorCallback, this);

Redémarrer le flux

Étant donné que la fonction de redémarrage peut être appelée à partir de plusieurs threads (par exemple, si nous recevons plusieurs événements de déconnexion à la suite dans un court laps de temps), nous protégeons les sections critiques du code avec un std::mutex.

void AudioEngine::restart(){

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

Voilà pour le moteur audio. Il n'y a pas beaucoup plus que nous puissions faire ici…

Nous avons besoin d'un moyen permettant à notre interface utilisateur Java de communiquer avec nos classes C++. C'est là que JNI intervient. Ses signatures de méthodes ne sont peut-être pas son meilleur atout, mais heureusement, il n'y en a que trois.

Renommez le fichier native-lib.cpp en jni-bridge.cpp. Vous pouvez laisser le nom de fichier tel quel. Toutefois, il convient d'insister sur le fait que ce fichier C++ est destiné aux méthodes JNI. Veillez à remplacer CMakeLists.txt par le fichier renommé (en conservant le nom de la bibliothèque native-lib).

Ajoutez le code suivant à 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();
}

}

Le pont JNI est assez simple :

  • Nous créons une instance statique du AudioEngine.
  • startEngine() démarre le moteur audio, tandis que stopEngine() l'arrête.
  • touchEvent() convertit les événements tactiles en appels de méthode pour activer/désactiver le signal sonore.

Créons maintenant notre interface utilisateur et raccordons-la au backend.

Disposition

La disposition est très simple (nous l'améliorerons dans les prochains ateliers de programmation). Il s'agit simplement d'un élément FrameLayout avec TextView au centre :

4a039cdf72e4846f.png

Remplacez res/layout/activity_main.xml par ce qui suit :

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

Ajoutez la ressource de chaîne pour @string/tap_anywhere à res/values/strings.xml :

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

MainActivity

Modifiez maintenant MainActivity.java avec le code suivant :

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

Voici ce que fait ce code :

  • Les méthodes private native void sont toutes définies dans jni-bridge.cpp. Nous devons les déclarer ici pour pouvoir les utiliser.
  • Les événements cycle de vie d'une activité onCreate() et onDestroy() appellent le pont JNI pour démarrer et arrêter le moteur audio.
  • Nous remplaçons onTouchEvent() pour recevoir tous les événements tactiles de notre Activity et les transmettons directement au pont JNI pour activer ou désactiver le signal sonore.

Allumez votre émulateur ou votre appareil de test, puis exécutez dessus l'application WaveMaker. En appuyant sur l'écran, vous devriez entendre une onde sinusoïdale claire.

D'accord, notre application ne remportera aucun prix pour sa créativité musicale, mais elle devrait montrer les techniques fondamentales nécessaires à la production de contenus audio synthétisés à faible latence sous Android.

Ne vous inquiétez pas ! Dans les prochains ateliers de programmation, nous ferons en sorte que notre application soit beaucoup plus intéressante. Merci d'avoir suivi cet atelier de programmation. Pour toute question, veuillez les poser dans le groupe Android NDK.

Complément d'informations

Échantillons audio haute performance

Guide sur les applications audio haute performance disponible dans la documentation Android NDK

Bonnes pratiques pour les contenus audio et vidéo sous Android – Google I/O 2017