Linux Embedded

Le blog des technologies libres et embarquées

Prise en main de Jack

Introduction

Lorsqu'on veux écrire une application audio sous linux on tombe
rapidement sur ce genre de schéma :

C'est un peu effrayant au début, mais il faut bien comprendre que les composants qui apparaissent dans ce schéma tiennent en fait différents rôles. On peut ainsi les classer en plusieurs catégories:

  • Les drivers: ils dialoguent directement avec le matériel. On trouve notamment alsa, oss, ffado
  • Les frameworks multimédias: ils facilitent la lecture des différents type de flux multimédias. On trouve notamment gstreamer, phonon, xine
  • Les couches de compatibilités: Elles servent à s'abstraire de la plateforme et fournir une interface unifiée on trouve notamment openAL, SDL
  • Les mixers audio: il servent à interconnecter différentes applications entre elles. Les deux principaux mixers audio sont Pulseaudio et Jack

Nous allons ici nous intéresser au cas de Jack [1].

Jack se présente sous la forme d'un service central qui gère les différents flux audio, que ce soit des flux matériels (carte son) ou applicatifs. Il connecte ainsi différentes applications entre elles en leur permettant d'échanger des buffers audio. Chacune des applications met à la disposition de jack des "ports" d'entrée ou de sortie sur lesquels elle sera en mesure de lire ou d'écrire ses données. Jack se charge alors de les interconnecter et de leur fournir des plages de temps CPU pour quelles puissent traiter leurs données.

Jack découpe le flux audio en tronçons de longueur fixe. Ces tronçons seront transmis aux différents composants. La taille de ces tronçons définit la latence du système, c'est à dire le délais qu'induit le traitement des données par le système. Jack garantit que tous les tronçons seront soit traités dans le temps impartis, soit rejetés. Un paquet rejeté provoque un craquement audible, mais le fait de rejeter les paquets trop lents garantit que l'audio ne dérive pas dans le temps. Un buffer sous-dimensionné va faire monter la charge CPU et donc provoquer plus de craquements. A l'inverse, une taille de buffer trop grande entraîne latence excessive. Le choix de la taille des buffer est donc un compromis entre la latence et le temps de traitement. Jack lui même n'ajoute pas de latence au système.

Les premières versions de jack nécessitaient un noyau temps réel (le patch préempt-RT pour le noyau linux) mais ce n'est plus nécessaire. Les noyaux mainstream récents conviennent très bien.

Et Pulseaudio dans tout ça ? Tout comme Jack, Pulseaudio a pour but de connecter les différentes sources audio, les effets audio et les puits audio. Pulseaudio est devenu le mixer de son standard sur la plupart des distributions de bureautique, mais les compromis choisis par Pulseaudio et Jack sont différents.

Jack a pour mot d'ordre le respect des latences, et ce, au détriment des performances du système. Pulseaudio va ménager les performances du système et assurer une qualité de son quitte à sacrifier de la latence [2]. On comprends pourquoi Pulseaudio est généralement recommandé pour une utilisation classique, alors que Jack est considéré comme un outil de référence dans le monde de la création audio.

Tutoriel: mise en place d'un effet de trémolo

Pour notre tutoriel, nous allons mettre en place un effet assez simple : un effet de trémolo [3]. Un trémolo consiste à faire varier l'amplitude de notre signal d'entrée en le multipliant par une oscillation basse fréquence afin de créer un effet de vibration.

Tout d'abord, connectons notre application au serveur Jack du système.

jack_client_t *client = NULL;
client = jack_client_open("MonClient", JackNullOption, NULL);
if (client == NULL)
{
    fprintf (stderr, "impossible de se connecter a jack\n");
    return 1;
}

Jack fonctionne principalement par callback. Nous allons donc nous enregistrer pour recevoir les évènements qui nous intéressent. Ici nous allons juste observer les changements de la fréquence d'échantillonnage. Les autres évènements ne nous seront pas utiles dans le cadre de notre application.

jack_set_sample_rate_callback(client, on_samplerate_change, appconf);

Notre fonction callback se définit de la façon suivante. Elle retourne 0 pour indiquer qu'elle n'a pas généré d'erreur dans son traitement.

int on_samplerate_change(jack_nframes_t nframes, void* data)
{
    fprintf (stdout, "jack samplerate has changed: %u\n", nframes);
    AppConf* conf = (AppConf*)data;
    conf->samplerate = nframes;
    return 0;
}

Les informations peuvent également être demandées directement. Le mécanisme de callbacks sert principalement pour être prévenu lors d'une modification. Par exemple nous allons demander la fréquence d'échantillonnage courante:

appconf->samplerate =  jack_get_sample_rate(client);

Comme expliqué précédemment, jack utilise un système de ports pour relier les applications entre elles. Pour notre application, nous allons déclarer un port en entrée pour recevoir nos buffers de données et un port de sortie pour les restituer. Pour chaque port, il nous faut définir le type de données qu'il transporte, ainsi que la direction du port (en entrée ou sortie). Chaque voie correspond à une entrée ou sortie mono : pour gérer du stéréo, il faudrait doubler le nombre de ports enregistrés.

appconf->input_port = jack_port_register(client, "input",
                                         JACK_DEFAULT_AUDIO_TYPE,
                                         JackPortIsInput,
                                         0);
appconf->output_port = jack_port_register(client, "output",
                                         JACK_DEFAULT_AUDIO_TYPE,
                                         JackPortIsOutput,
                                         0);

Maintenant, il faut définir la fonction qui sera appelée pour traiter nos données. Cette fonction sera appelée à chaque cycle de jack. La fonction aura pour charge de traiter tous les ports d'entrée et de sortie simultanément, les traitements se faisant de façon globale et non par port.

jack_set_process_callback(client, on_process, appconf);

Et voici notre fonction de traitement:

  • Jack nous informe que le cycle courant comporte nframes samples
  • Nous demandons donc à notre port d'entrée et notre port de sortie de nous retourner les deux buffers correspondants.
  • Jack utilise pour représenter ses échantillons des floats sur 32 bits compris entre -1.0 et 1.0.
  • Notre traitement se contente de recopier les échantillons du port d'entrée sur le port de sortie en modifiant leur amplitude
int on_process(jack_nframes_t nframes, void* data)
{
    AppConf* appconf = (AppConf*)data;
    jack_default_audio_sample_t *in = (jack_default_audio_sample_t*)jack_port_get_buffer (appconf->input_port, nframes);
    jack_default_audio_sample_t *out = (jack_default_audio_sample_t*)jack_port_get_buffer (appconf->output_port, nframes);

    int t = appconf->lfo_pos;
    //caclul de periode de notre LFO en samples
    float scale = PI * TONE_HZ  / appconf->samplerate ;
    for (jack_nframes_t i = 0; i < nframes; i++)
    {
        out[i] = in[i] * (sin(t++ * scale) / 2.0 + 1.0);
    }
    appconf->lfo_pos = t;
    return 0;
}

Maintenant que l'application est configurée, il est temps d'activer nos callbacks auprès de jack.

jack_activate(client);

Les traitements de jack s'exécutent en tache de fond. Notre programme principal ne doit pas se terminer, mais il n'a plus rien à faire.

g_running = 1;
while (g_running)
{
    sleep(1);
}

Enfin une fois notre programme terminé, Nous pouvons nettoyer notre contexte jack

jack_client_close(client);
appconf_destroy(appconf);

Nous avons maintenant une application jack entièrement fonctionnelle.

Et maintenant ?

Pour utiliser cet exemple, nous allons le connecter à une entrée audio et rediriger le résultat vers la sortie audio physique de la machine. Il existe un utilitaire graphique très pratique et agréable à utiliser pour cela :  QJackCtl [4].

Dans l'exemple ci-dessous, nous connectons donc l'entrée de notre application Jack à la sortie de vlc (out_1). De la même manière, la sortie de notre application est connectée aux sorties system (playback_1 et playback_2).

Attention : vlc doit être lancé et configuré pour exporter une sortie Jack (tools/preferences/audio et choisir "Jack Audio Output" comme "Output module"). Par ailleurs, sous Debian/Ubuntu, il est nécessaire d'installer le paquet vlc-plugin-jack.

En bonus...

Il est possible d'automatiser la connexion d'une application Jack aux sorties audio de la machine. Pour cela nous cherchons tous les ports de sortie associé à la ressource "system" et de type audio (les sorties audio sont des entrées au sens Jack d'où le JackPortIsInput) pour connecter alors notre port de sortie à ces derniers.

Voici le code réalisant ce petit miracle...:

const char* outportname = jack_port_name(appconf->output_port);
const char** systemports = jack_get_ports (client, "system:", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput);
int i = 0;
for (const char* systemport_name = systemports[0]; systemport_name != NULL; systemport_name = systemports[i++])
{
    jack_connect(client, outportname, systemport_name);
}

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.