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
- Android Studio 2.3.3 ou version ultérieure
- SDK Android 8.0 (niveau d'API 26)
- NDK et Build Tools installés
- Simulateur ou appareil Android équipé d'Android 8.0 (niveau d'API 26) ou d'une version ultérieure pour les tests
- Avoir des connaissances en C++ (utile, mais pas obligatoire)
L'appli produit un son synthétisé lorsque l'utilisateur appuie sur l'écran. Voici l'architecture :
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 :
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++).
Nommez la classe "Oscillator".
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 :
- Écrire directement dans le flux avec AAudioStream_write
- Utiliser une fonction de rappel de données AAudioStream_dataCallback
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 questopEngine()
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 :
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 dansjni-bridge.cpp
. Nous devons les déclarer ici pour pouvoir les utiliser. - Les événements cycle de vie d'une activité
onCreate()
etonDestroy()
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 notreActivity
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