Linux Embedded

Le blog des technologies libres et embarquées

OpenSL-ES sous Android

Introduction

Dans la continuité de l'article de présentation de jack [1], nous
allons cette fois-ci nous intéresser à une autre pile audio orientée basse latence: OpenSL-ES

OpenSL est une API standardisée proposée par la fondation Khronos [2] à
destination des appareils mobiles.

OpenSL couvre de nombreux domaines relatifs à l'audio, que ce soit l'échange de
buffers audio avec les interfaces d'entrées et de sorties, l'utilisation d'effets
sonores (filtres, spatialisation) ou même la gestion du MIDI et des sonneries.

C'est l'interface qui a été retenue par Android pour fournir aux développeurs un
accès bas niveau à la pile audio. Elle se différentie d'Audioflinger par un
dialogue plus direct avec le matériel et des temps d'accès meilleurs et plus
stables. En contrepartie l'API d'OpenSL n'est disponible sous Android que via
sa couche native (NDK) elle n'est donc pas accessible directement depuis les
applications java. Son accès depuis une application graphique nécessitera de
passer par une interface JNI.

Structure préliminaire

Tout d'abord nous allons définir une structure pour encapsuler les différents
objets que nous allons utiliser, cette structure va contenir les objets OpenSL
que nous allons créer ainsi que nos objets métier. Nous reviendrons sur le rôle
de chacun de ces paramètres tout au long de l'article

typedef struct AudioContext
{
    /////structures d'opensl
    // engine interfaces
    SLObjectItf engineObject;
    SLEngineItf engineEngine;

    // output mix interfaces
    SLObjectItf outputMixObject;

    // buffer queue player interfaces
    SLObjectItf bqPlayerObject;
    SLPlayItf bqPlayerPlay;
    SLAndroidSimpleBufferQueueItf bqPlayerBufferQueue;

    ///configuration pour opensl
    int samplerate;
    int buffersize;

    ////buffer contenant nos samples de sortie
    int16_t* audioBuffer;

    ///paramètre pour notre oscillateur
    int note;
    uint16_t oscpos;

} AudioContext;

Initialisation d'OpenSL

Afin d'utiliser OpenSL, nous allons dans un premier temps instancier le moteur
OpenSL puis récupérer l'interface de notre moteur (SLEngineItf) depuis l'objet
générique (SLObjectItf).

Le cycle de vie des objets OpenSL passe par plusieurs états : après leur création
ils sont en état Unrealized, il est alors nécessaire d'appeler la méthode Realize
afin de les initialiser.

SLresult engineInit(AudioContext *ctx)
{
    SLresult result;
    // create engine
    result = slCreateEngine(&(ctx->engineObject), 0, NULL, 0, NULL, NULL);
    if(result != SL_RESULT_SUCCESS)
        return result;

    // realize the engine
    result = (*ctx->engineObject)->Realize(ctx->engineObject, SL_BOOLEAN_FALSE);
    if(result != SL_RESULT_SUCCESS)
        return result;

    // get the engine interface, which is needed in order to create other objects
    result = (*ctx->engineObject)->GetInterface(
        ctx->engineObject,
        SL_IID_ENGINE,
        &(ctx->engineEngine));

    if(result != SL_RESULT_SUCCESS)
        return result;

    return result;
}

Création du périphérique de sortie audio

Nous allons ensuite ouvrir le périphérique audio de sortie

SLresult mixerInit(AudioContext *ctx)
{
    SLresult result;

    //nombre d'interface à créer
    const SLuint32 nbInterface = 1;
    //tableau de interface à créer, ici une seule interface de type
    //volume
    const SLInterfaceID ids[] = {SL_IID_VOLUME};
    //tableau indique que nous n'avons par besoin de récupérer
    //l'interface de l'objet
    const SLboolean req[] = {SL_BOOLEAN_FALSE};
    result = (*ctx->engineEngine)->CreateOutputMix(
        ctx->engineEngine,
        &(ctx->outputMixObject),
        nbInterface,
        ids,
        req);
    if(result != SL_RESULT_SUCCESS)
        return result;

    result = (*ctx->outputMixObject)->Realize(ctx->outputMixObject, SL_BOOLEAN_FALSE);
    if(result != SL_RESULT_SUCCESS)
        return result;

    return result;
}

Création du player audio

Afin de générer du son nous allons utiliser un Player. Cet objet aura pour rôle
de transférer des données entre sa source et sa destination:

  • sa destination sera notre sortie son
  • sa source sera une fonction de callback qui sera appelée par le Player. Cette fonction fournira des buffers audio au player.

Dans un permier temps nous allons définir le format des données audio que nous
allons fournir au player dans notre callback.

    //conversion du samplerate désiré vers la constante opensl associée
    SLuint32 sr = convertSamplerate(ctx->samplerate);

    //definition du format de donnée
    SLDataFormat_PCM format_pcm = {
        SL_DATAFORMAT_PCM, // les données seront au format PCM
        1,  //nombre de canaux audio
        sr, //frequence d'echantillonnage des données
        SL_PCMSAMPLEFORMAT_FIXED_16,  //nos données seront au format 16bits
        SL_PCMSAMPLEFORMAT_FIXED_16, //et contenue dans des mots de 16bits
        SL_SPEAKER_FRONT_CENTER, //cannaux de sortie
        SL_BYTEORDER_LITTLEENDIAN //endianness des données
    };

    SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {
        SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, //constante à utiliser pour Android.
        1//nombre de buffer dans la queue
    };

    //définition de la source audio
    SLDataSource audioSrc = {
        &loc_bufq, //les données proviennent de la queue opensl
        &format_pcm //et elles sont au format définie précedement
    };

OpenSLES a la particularité d'utiliser des fréquences d'échantillonnage définies
en milihertz, nous allons utiliser une fonction pour faire la conversion depuis
notre samplerate défini en Hz vers les constantes OpenSL associées

SLuint32 convertSamplerate(int samplerate)
{
    switch(samplerate) {
    case 8000:
        return SL_SAMPLINGRATE_8;
        break;
    case 11025:
        return SL_SAMPLINGRATE_11_025;
        break;
    case 16000:
        return SL_SAMPLINGRATE_16;
        break;
    case 22050:
        return SL_SAMPLINGRATE_22_05;
        break;
    case 24000:
        return SL_SAMPLINGRATE_24;
        break;
    case 32000:
        return SL_SAMPLINGRATE_32;
        break;
    case 44100:
        return SL_SAMPLINGRATE_44_1;
        break;
    case 48000:
        return SL_SAMPLINGRATE_48;
        break;
    case 64000:
        return SL_SAMPLINGRATE_64;
        break;
    case 88200:
        return SL_SAMPLINGRATE_88_2;
        break;
    case 96000:
        return SL_SAMPLINGRATE_96;
        break;
    case 192000:
        return SL_SAMPLINGRATE_192;
        break;
    default:
        return -1;
    }
}

Nous allons maintenant définir la sortie audio de notre Player : cela consiste
juste à indiquer le pointeur vers notre mixer de sortie.

    //reference notre sortie audio
    SLDataLocator_OutputMix loc_outmix = {
        SL_DATALOCATOR_OUTPUTMIX,
        ctx->outputMixObject
    };

    //on va diriger la sortie de notre objet vers la sortie audio
    SLDataSink audioSnk = {
        &loc_outmix,
        NULL
    };

Nous pouvons enfin instancier notre player et récupérer les interfaces associées

    //nous pouvons enfin creer notre lecteur
    const SLInterfaceID ids1[] = {SL_IID_VOLUME, SL_IID_ANDROIDSIMPLEBUFFERQUEUE};
    const SLboolean req1[] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};
    result = (*ctx->engineEngine)->CreateAudioPlayer(
        ctx->engineEngine, //notre moteur opensl
        &(ctx->bqPlayerObject),
        &audioSrc, //source audio
        &audioSnk, //destination audio
        2, //nombre d'interfaces
        ids1, //type d'interface
        req1 //recupération des interfaces
        );
    if(result != SL_RESULT_SUCCESS)
        return result;

    result = (*ctx->bqPlayerObject)->Realize(ctx->bqPlayerObject, SL_BOOLEAN_FALSE);
    if(result != SL_RESULT_SUCCESS)
        return result;

    //récupération de d'interface du lecteur
    result = (*ctx->bqPlayerObject)->GetInterface(
        ctx->bqPlayerObject,
        SL_IID_PLAY,
        &(ctx->bqPlayerPlay)
        );
    if(result != SL_RESULT_SUCCESS)
        return result;

    //récupération de l'interface vers les queues du lecteur
    result = (*ctx->bqPlayerObject)->GetInterface(
        ctx->bqPlayerObject,
        SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
        &(ctx->bqPlayerBufferQueue)
        );
    if(result != SL_RESULT_SUCCESS)
        return result;

Notre player est maintenant prêt à être utilisé.

Il ne nous reste plus qu'à :

  • enregistrer notre callback auprès de la queue source,
  • passer le Player en lecture
  • et enfin mettre en queue un premier buffer de donnés.
SLresult setupPlayerCallback(AudioContext* ctx)
{
    SLresult result;

    // register callback on the buffer queue
    result = (*ctx->bqPlayerBufferQueue)->RegisterCallback(ctx->bqPlayerBufferQueue, playerCallback, ctx);
    if(result != SL_RESULT_SUCCESS) return result;

    // set the player's state to playing
    result = (*ctx->bqPlayerPlay)->SetPlayState(ctx->bqPlayerPlay, SL_PLAYSTATE_PLAYING);

    //mise en queue d'un premier buffer
    (*ctx->bqPlayerBufferQueue)->Enqueue(
        ctx->bqPlayerBufferQueue,
        ctx->audioBuffer,
        ctx->buffersize*sizeof(int16_t));

    return result;
}

Notre callback fournit des buffers audio au player. L'application
graphique donne la note à jouer en modifiant la variable note. Pour
couper le son, c'est la valeur 0 qui est fournie. Le générateur proposé ici consiste à créer une sinusoïde de la fréquence de la note voulue ou insérer du silence quand il n'y a
plus rien à jouer.

//conversion de la note sur l'echelle MIDI vers sa frequence associée
double miditofreq(int note)
{
    return pow(2, (note-69)/12.) * 440.;
}

void playerCallback(SLAndroidSimpleBufferQueueItf bq, void* opaque)
{
    AudioContext* ctx = (AudioContext*)opaque;

    int i;
    //si il n'y plus de note, nous insérons du silence
    if (ctx->note == 0)
    {
        memset(ctx->audioBuffer, 0, ctx->buffersize * sizeof(int16_t));
    }
    //sinon nous générons une sinusoide de la fréquence de la note
    else
    {

        double freq = miditofreq(ctx->note);
        double scale =  (2 * M_PI * freq) / ctx->samplerate;

        int t = ctx->oscpos;
        for (i = 0; i < ctx->buffersize; i++)
        {

            ctx->audioBuffer[i] =  (sin(t++ * scale) * 5000);
        }
        //nous sauvegardons la position du générateur afin d'eviter d'avoir une
        //discontinuité entre les buffers
        ctx->oscpos = t;
    }
    //mise en queue du buffer audio
    (*bq)->Enqueue(bq, ctx->audioBuffer, ctx->buffersize * sizeof(int16_t));
}

Nettoyage des interfaces

Une fois terminée l'utilisation d'OpenSL il nous faudra libérer les ressources associées:

void audiocontext_release(AudioContext* ctx)
{
    //libération du player
    if (ctx->bqPlayerObject != NULL)
    {
        SLuint32 state = SL_PLAYSTATE_PLAYING;
        (*ctx->bqPlayerPlay)->SetPlayState(ctx->bqPlayerPlay, SL_PLAYSTATE_STOPPED);
        while (state != SL_PLAYSTATE_STOPPED)
        {
            (*ctx->bqPlayerPlay)->GetPlayState(ctx->bqPlayerPlay, &state);
        }
        (*ctx->bqPlayerObject)->Destroy(ctx->bqPlayerObject);
        ctx->bqPlayerObject = NULL;
        ctx->bqPlayerPlay = NULL;
        ctx->bqPlayerBufferQueue = NULL;
    }

    //libération de l'interface de sortie
    if (ctx->outputMixObject != NULL)
    {
        (*ctx->outputMixObject)->Destroy(ctx->outputMixObject);
        ctx->outputMixObject = NULL;
    }

    //libération du moteur OpenSL
    if (ctx->engineObject != NULL) {
        (*ctx->engineObject)->Destroy(ctx->engineObject);
        ctx->engineObject = NULL;
        ctx->engineEngine = NULL;
    }

    //libération du buffer audio
    free(ctx->audioBuffer);
    free(ctx);
}

Un clavier minimaliste

Il ne nous reste plus qu'a créer une interface graphique qui va venir jouer les
notes. Notre interface d'exemple est minimaliste. Elle consiste en une série de boutons. À
chaque bouton est associé une note, nous allons détecter les évènements de
"toucher" sur ces boutons pour activer ou désactiver leur note.

public class MainActivity extends Activity implements OnTouchListener {

    private long audiocontext;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        int samplerate = 44100;
        int buffersize = 64;
        //à partir d'android 4.2, android est capable de nous indiquer ses
        //préfèrences audio
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
        {
            AudioManager audiomanager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
            samplerate =Integer.getInteger(audiomanager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE), 44100);
            buffersize = Integer.getInteger(audiomanager.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER), 64);
        }
        audiocontext = audioStart(samplerate, buffersize);

        setContentView(R.layout.activity_main);

        Button C4 = (Button)findViewById(R.id.C4Btn);
        C4.setOnTouchListener(this);
        Button D4 = (Button)findViewById(R.id.D4Btn);
        D4.setOnTouchListener(this);
        Button E4 = (Button)findViewById(R.id.E4Btn);
        E4.setOnTouchListener(this);
        Button F4 = (Button)findViewById(R.id.F4Btn);
        F4.setOnTouchListener(this);
        Button G4 = (Button)findViewById(R.id.G4Btn);
        G4.setOnTouchListener(this);
        Button A4 = (Button)findViewById(R.id.A4Btn);
        A4.setOnTouchListener(this);
        Button B4 = (Button)findViewById(R.id.B4Btn);
        B4.setOnTouchListener(this);
    }

    public boolean onTouch (View v, MotionEvent event)
    {
        if (event.getAction() == MotionEvent.ACTION_DOWN)
        {
            switch(v.getId())
            {
                case R.id.C4Btn:
                    audioNoteOn(audiocontext, 60);
                break;

                case R.id.D4Btn:
                    audioNoteOn(audiocontext, 62);
                    break;

                case R.id.E4Btn:
                    audioNoteOn(audiocontext, 64);
                    break;

                case R.id.F4Btn:
                    audioNoteOn(audiocontext, 65);
                    break;

                case R.id.G4Btn:
                    audioNoteOn(audiocontext, 67);
                    break;

                case R.id.A4Btn:
                    audioNoteOn(audiocontext, 69);
                    break;

                case R.id.B4Btn:
                    audioNoteOn(audiocontext, 71);
                    break;
                default:
                    break;
            }

        }
        else if (event.getAction() == MotionEvent.ACTION_UP)
        {
            audioNoteOff(audiocontext);
        }

        return true;
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    //// méthodes jni
    public static native long audioStart(int samplerate, int buffersize);
    public static native boolean audioStop(long ctx);
    public static native boolean audioNoteOn(long ctx, int tone);
    public static native boolean audioNoteOff(long ctx);

    static
    {
        System.loadLibrary("Blog_opensl");
    }
}

Voila il ne vous reste plus qu'a créer le prochain synthé sur votre mobile qui
rendra fou de jalousie Jean-Michel Jarre.

Le code complet de cet article est disponible à l'adresse [3]

[1] http://www.linuxembedded.fr/2013/03/prise-en-main-de-jack
[2] http://www.khronos.org/opensles/
[3] https://github.com/Openwide-Ingenierie/blog_opensl

    • le 10 avril 2018 à 09:54

      Merci.

      Les tuto sur OpenSL ne sont pas légion. En français qui plus est.
      Je vais tester tout ça :)

      Une extension sur la partie record, serait cool.
      merci

    • le 21 avril 2018 à 22:38

      Super article qui semble clair, mais les fonctions audioStart, audioStop, audioNoteOn et audioNoteOff ne sont pas fourni ce qui est dommage.
      Mais bon peut être que si on a tout compris on est sensé pouvoir les faire soit même.
      Je vais essayer :)

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.