Dans cet atelier de programmation, nous allons créer un échantillonneur audio. L'appli enregistre et lit des données audio issues du micro intégré au téléphone.
Elle enregistre jusqu'à 10 secondes d'audio lorsque vous maintenez le bouton Record (Enregistrer) enfoncé. Lorsque vous maintenez le bouton Play (Lecture) enfoncé, l'appli lit une fois la piste audio enregistrée. Vous pouvez également activer l'option Loop (Boucle) pour lire l'enregistrement en boucle jusqu'à ce que vous relâchiez le bouton Play (Lecture). Chaque fois que vous appuyez sur le bouton Record (Enregistrer), l'enregistrement audio précédent est écrasé.
Points abordés
- Concepts de base pour créer un flux d'enregistrement à faible latence
- Comment stocker et lire des données audio enregistrées depuis un micro ?
Conditions préalables
Avant de commencer cet atelier de programmation, nous vous recommandons de suivre l'atelier de programmation Créer un synthétiseur. Il aborde certains concepts de base non traités ici pour créer des flux audio.
Prérequis
- Android Studio version 3.0.0 ou 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 certaines connaissances en C++ (utile, mais non obligatoire)
Notre appli d'échantillonnage comporte quatre composants :
- 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.
- 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.
- Moteur audio : cette classe C++ crée les flux audio d'enregistrement et de lecture.
- Enregistrement audio : cette classe C++ stocke les données audio en mémoire.
Voici l'architecture :
Cloner le projet
Clonez le dépôt de l'atelier de programmation sur github.
git clone https://github.com/googlecodelabs/android-wavemaker2
Importer un projet dans Android Studio
Ouvrez Android Studio et importez le projet :
- File -> New -> Import project... (Fichier -> Nouveau -> Importer le projet…)
- Sélectionnez le dossier android-wavemaker2.
Exécuter le projet
Choisissez la configuration d'exécution de base.
Appuyez ensuite sur CTRL+R pour compiler et exécuter le modèle d'application. Il devrait pouvoir être compilé et exécuté, mais n'est pas encore fonctionnel. Vous lui ajouterez des fonctionnalités au cours de cet atelier de programmation.
Ouvrir le module de base
Les fichiers sur lesquels vous allez travailler pour cet atelier de programmation sont stockés dans le module base
. Développez ce module dans la fenêtre du projet en veillant à sélectionner l'affichage Android.
Remarque : Le code source terminé de l'application WaveMaker2 se trouve dans le module final
.
L'objet SoundRecording
représente les données audio enregistrées dans la mémoire. Il permet à l'appli d'écrire dans la mémoire des données issues du micro et de les lire.
Commençons par déterminer comment stocker ces données audio.
Sélectionner un format audio
Nous devons d'abord choisir un format audio pour nos échantillons. Deux formats sont pris en charge par AAudio :
float
: virgule flottante à simple précision (4 octets par échantillon)int16_t
: entiers de 16 bits (2 octets par échantillon)
Pour obtenir un son de bonne qualité à faible volume et pour d'autres raisons, nous utilisons des échantillons float
. Si votre capacité de mémoire n'est pas suffisante, vous pouvez sacrifier la fidélité et gagner de l'espace en utilisant des entiers de 16 bits.
Choisir la quantité d'espace de stockage nécessaire
Supposons que nous souhaitions stocker 10 secondes de données audio. Avec un taux d'échantillonnage de 48 000 échantillons par seconde, correspondant au taux d'échantillonnage le plus courant sur les appareils Android modernes, il faut allouer de la mémoire pour 480 000 échantillons.
Ouvrez le fichier base/cpp/SoundRecording.h et définissez cette constante en haut du fichier.
constexpr int kMaxSamples = 480000; // 10s of audio data @ 48kHz
Définir le tableau de stockage
Nous disposons maintenant de toutes les informations dont nous avons besoin pour définir un tableau de valeurs float
. Ajoutez la déclaration suivante à SoundRecording.h :
private:
std::array<float,kMaxSamples> mData { 0 };
Le { 0 }
utilise l'initialisation agrégée pour définir toutes les valeurs du tableau sur 0.
Implémenter write
Le prototype de la méthode write
est le suivant :
int32_t write(const float *sourceData, int32_t numFrames);
Cette méthode reçoit un tableau d'échantillons audio dans sourceData
. La taille du tableau est spécifiée par numFrames
. La méthode doit afficher le nombre d'échantillons qu'elle écrit réellement.
Vous pouvez l'implémenter en stockant le prochain indice d'écriture disponible. Au départ, sa valeur est de 0 :
Une fois l'échantillon écrit, on passe à l'indice d'écriture suivant :
Cette méthode peut facilement être implémentée en tant que boucle for
. Ajoutez le code suivant à la méthode write
dans SoundRecording.cpp.
for (int i = 0; i < numSamples; ++i) {
mData[mWriteIndex++] = sourceData[i];
}
return numSamples;
Mais attendez… Et si jamais nous essayons d'écrire plus d'échantillons que nous ne pouvons en stocker ? Ça ne va pas aller ! Une erreur de segmentation apparaîtra, résultant d'une tentative d'accès à un indice de tableau hors limites.
Ajoutons une option de vérification qui modifie numSamples
si mData
n'a pas assez d'espace. Ajoutez ce qui suit au-dessus du code existant.
if (mWriteIndex + numSamples > kMaxSamples) {
numSamples = kMaxSamples - mWriteIndex;
}
Implémenter read
La méthode read
est semblable à write
. Nous stockons le prochain indice de lecture.
Puis, nous l'incrémentons de 1 après la lecture de chaque échantillon.
Nous répétons cette opération jusqu'à ce que le nombre d'échantillons demandés ait été lu. Que se passe-t-il lorsque la limite des données disponibles est atteinte ? Deux comportements sont possibles :
- Si la lecture en boucle est activée : l'indice de lecture est remis à zéro (le début du tableau de données).
- Si la lecture en boucle est désactivée : il ne se passe rien, on n'incrémente pas l'indice de lecture.
Pour ces deux comportements, nous devons savoir à quel moment la limite des données disponibles a été atteinte. Heureusement, c'est à cela que sert mWriteIndex
. Il indique le nombre total d'échantillons qui ont été écrits dans le tableau de données.
Cela nous permet d'implémenter la méthode read
dans SoundRecording.cpp.
int32_t framesRead = 0;
while (framesRead < numSamples && mReadIndex < mWriteIndex){
targetData[framesRead++] = mData[mReadIndex++];
if (mIsLooping && mReadIndex == mWriteIndex) mReadIndex = 0;
}
return framesRead;
AudioEngine
effectue les tâches principales suivantes :
- Créer une instance de
SoundRecording
- Créer un flux d'enregistrement pour enregistrer des données depuis le micro
- Écrire les données enregistrées dans l'instance
SoundRecording
dans le rappel du flux d'enregistrement - Créer un flux de lecture pour lire les données enregistrées
- Lire les données enregistrées à partir de l'instance
SoundRecording
dans le rappel du flux de lecture - Répondre aux événements d'interface utilisateur pour l'enregistrement, la lecture simple et la lecture en boucle
Commençons par créer l'instance SoundRecording
.
Démarrez avec quelque chose de facile. Créez une instance de SoundRecording
dans AudioEngine.h :
private:
SoundRecording mSoundRecording;
Nous avons deux flux à créer : le flux de lecture et le flux d'enregistrement. Lequel faut-il créer en premier ?
Il faut d'abord créer le flux de lecture, car il ne présente qu'un seul taux d'échantillonnage, et celui-ci permet d'obtenir la latence la plus faible. Une fois créé, nous pouvons renseigner son taux d'échantillonnage dans l'outil de création de flux d'enregistrement. Cela permet de nous assurer que les flux de lecture et d'enregistrement disposent du même taux d'échantillonnage, ce qui signifie que nous n'aurons pas de travail de rééchantillonnage à faire entre les flux.
Propriétés du flux de lecture
Insérez les propriétés suivantes dans le StreamBuilder
, qui créera le flux de lecture :
- Direction : non spécifiée, réglée par défaut sur "sortie"
- Taux d'échantillonnage : non spécifié, réglé par défaut sur le taux avec la latence la plus faible
- Format : float
- Nombre de canaux : 2 (stéréo)
- Mode de performance : faible latence
- Mode de partage : exclusif
Créer le flux de lecture
Nous avons désormais tout ce qu'il nous faut pour créer et ouvrir le flux de lecture. Ajoutez le code suivant tout en haut de la méthode start
dans 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;
}
Notez que les modèles de méthode pour les rappels de données et d'erreurs ont été créés pour vous. Si vous avez besoin d'un récapitulatif sur leur fonctionnement, consultez le premier atelier de programmation.
Enregistrer les propriétés du flux
Utilisez les propriétés suivantes pour créer le flux d'enregistrement :
- Direction : entrée (l'enregistrement est une entrée, tandis que la lecture est une sortie)
- Taux d'échantillonnage : identique au flux de sortie
- Format : float
- Nombre de canaux : 1 (mono)
- Mode de performance : faible latence
- Mode de partage : exclusif
Créer le flux d'enregistrement
Ajoutez à présent le code suivant sous le code précédemment ajouté dans 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;
}
Passons maintenant à la partie amusante : stocker dans l'objet SoundRecording
les données enregistrées depuis le micro.
Lors de la création du flux d'enregistrement, nous avions défini le rappel de données comme ::recordingDataCallback
. Cette méthode appelle AudioEngine::recordingCallback
, dont le prototype est le suivant :
aaudio_data_callback_result_t AudioEngine::recordingCallback(float *audioData,
int32_t numFrames)
Les données audio sont fournies dans audioData.
. Leur taille (en échantillons) est de numFrames
, car il n'y a qu'un échantillon par trame étant donné que notre enregistrement est en mono.
Il suffit de procéder comme suit :
- Consulter
mIsRecording
pour savoir s'il faut procéder à l'enregistrement - Sinon, ignorer les données entrantes
- Si l'on est en train d'enregistrer :
- Fournir
audioData
àSoundRecording
à l'aide de sa méthodewrite
- Vérifier la valeur renvoyée par
write
(si elle est égale à zéro, cela signifie queSoundRecording
est plein et qu'il faut arrêter l'enregistrement) - Renvoyer
AAUDIO_CALLBACK_RESULT_CONTINUE
pour que les rappels continuent
Ajoutez le code suivant à recordingCallback
:
if (mIsRecording) {
int32_t framesWritten = mSoundRecording.write(audioData, numFrames);
if (framesWritten == 0) mIsRecording = false;
}
return AAUDIO_CALLBACK_RESULT_CONTINUE;
À l'instar du flux d'enregistrement, le flux de lecture appelle playbackDataCallback
lorsqu'il a besoin de nouvelles données. Cette méthode appelle AudioEngine::playbackCallback,
, dont le prototype est le suivant :
aaudio_data_callback_result_t AudioEngine::playbackCallback(float *audioData, int32_t numFrames)
Dans cette méthode, nous devons :
- Remplir le tableau avec des zéros en utilisant
fillArrayWithZeros
- Si les données enregistrées sont en cours de lecture (indiqué par
mIsPlaying
), il faut procéder comme suit : - Lire
numFrames
de données à partir demSoundRecording
- Passer
audioData
de mono à stéréo en utilisantconvertArrayMonoToStereo
- Si le nombre de trames lues est inférieur au nombre de trames demandées, c'est que la limite des données enregistrées a été atteinte (arrêter la lecture en définissant
mIsPlaying
surfalse
)
Ajoutez le code suivant à 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;
Démarrer et arrêter l'enregistrement
La méthode setRecording
permet de démarrer et d'arrêter l'enregistrement. Voici son prototype :
void setRecording(bool isRecording)
Lorsque l'on appuie sur le bouton d'enregistrement, isRecording
a la valeur true. Lorsque le bouton est relâché, isRecording
est défini sur false.
Une variable membre mIsRecording
permet d'activer/de désactiver le stockage des données dans recordingCallback
. Il suffit de définir sa valeur sur isRecording
.
Il y a un autre comportement à ajouter. Lorsque l'enregistrement démarre, toutes les données existantes dans mSoundRecording
doivent être écrasées. Pour ce faire, utilisez clear
afin de réinitialiser sur zéro l'indice d'écriture de mSoundRecording
.
Voici le code pour setRecording
:
if (isRecording) mSoundRecording.clear();
mIsRecording = isRecording;
Démarrer et arrêter la lecture
Le contrôle de lecture est semblable à celui de l'enregistrement. La fonction setPlaying
est appelée lorsque l'utilisateur appuie sur le bouton de lecture ou bien le relâche. La variable membre mIsPlaying
active/désactive la lecture dans playbackCallback
.
Lorsque l'utilisateur appuie sur le bouton de lecture, il faut que la lecture démarre au début des données audio enregistrées. Pour ce faire, vous pouvez utiliser setReadPositionToStart
, qui vous permet de réinitialiser la tête de lecture de mSoundRecording
sur zéro.
Voici le code pour setPlaying
:
if (isPlaying) mSoundRecording.setReadPositionToStart();
mIsPlaying = isPlaying;
Activer/Désactiver la lecture en boucle
Enfin, lorsque l'option LOOP (Boucle) est activée ou désactivée, la commande setLooping
est appelée. La manipulation de cette méthode est simple. Il suffit de transmettre l'argument isOn
à mSoundRecording
.setLooping
:
Voici le code pour setLooping
:
mSoundRecording.setLooping(isOn);
Les applications qui enregistrent des données audio doivent demander l'autorisation RECORD_AUDIO
à l'utilisateur. Une grande partie du code gérant les autorisations est déjà écrite. Toutefois, il convient encore de déclarer que notre appli utilise cette autorisation.
Ajoutez la ligne suivante au fichier manifests/AndroidManifest.xml dans la section <manifest>
:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
Il est temps de voir si votre dur labeur s'est avéré payant. Compilez et exécutez l'application. L'interface utilisateur suivante doit s'afficher.
Appuyez sur le bouton rouge pour lancer l'enregistrement. L'enregistrement se poursuit tant que vous maintenez le bouton enfoncé (jusqu'à 10 secondes). Appuyez sur le bouton vert pour lire les données enregistrées. La lecture se poursuit tant que vous maintenez le bouton enfoncé, jusqu'à la fin des données audio. Si l'option LOOP (Boucle) est activée, les données audio sont lues en boucle indéfiniment.
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