Chapitre 2. Basic PCM audio

Pour écrire une simple application PCM pour ALSA nous avons besoin d'abord d'un handle pour le dispositif PCM. Nous devons indiquer la direction du stream PCM, qui peut être playback ou capture. Nous devons également fournir quelques informations au sujet de la configuration que nous voulons employer, comme la taille du buffer, le taux d'échantillonnage, le format de données PCM. Ainsi, d'abord nous écrirons :

    /* Handle for the PCM device */ 
    snd_pcm_t *pcm_handle;          

    /* Playback stream */
    snd_pcm_stream_t stream = SND_PCM_STREAM_PLAYBACK;

    /* This structure contains information about    */
    /* the hardware and can be used to specify the  */      
    /* configuration to be used for the PCM stream. */ 
    snd_pcm_hw_params_t *hwparams;

Les interfaces ALSA les plus importantes des dispositifs PCM sont "plughw" et "hw". Si vous utilisez l'interface "plughw", vous n'avez pas besoin de vous préocupper des carte sons. Si votre carte son ne supporte pas le taux d'échantillonnage ou le format d'échantillon que vous spécifiez, vos données seront automatiquement converties. Ceci s'applique également au type d'accès et au nombre de canaux.

Avec l'interface "hw", vous devez vérifier que votre matériel supporte les paramètres que vous voulez utilisés.

    /* Name of the PCM device, like plughw:0,0          */
    /* The first number is the number of the soundcard, */
    /* the second number is the number of the device.   */
    char *pcm_name;

Alors on initialise les variables et on assigne une structure hwparams

    /* Init pcm_name. Of course, later you */
    /* will make this configurable ;-)     */
    pcm_name = strdup("plughw:0,0");
  

    /* Allocate the snd_pcm_hw_params_t structure on the stack. */
    snd_pcm_hw_params_alloca(&hwparams);

Maintenant nous allons ouvrir l'interface PCM

    /* Open PCM. The last parameter of this function is the mode. */
    /* If this is set to 0, the standard mode is used. Possible   */
    /* other values are SND_PCM_NONBLOCK and SND_PCM_ASYNC.       */ 
    /* If SND_PCM_NONBLOCK is used, read / write access to the    */
    /* PCM device will return immediately. If SND_PCM_ASYNC is    */
    /* specified, SIGIO will be emitted whenever a period has     */
    /* been completely processed by the soundcard.                */
    if (snd_pcm_open(&pcm_handle, pcm_name, stream, 0) < 0) {
      fprintf(stderr, "Error opening PCM device %s\n", pcm_name);
      return(-1);
    }

Avant de pouvoir écrire des données PCM dans la carte son, nous devons indiquer le type d'accès, le format d'échantillon, le taux d'échantillonnage, le nombre de canaux, le nombre de périodes et la taille de la période.

D'abord, il faut initialiser la structure hwparams avec la configuration totale de l'espace de la carte son.

    /* Init hwparams with full configuration space */
    if (snd_pcm_hw_params_any(pcm_handle, hwparams) < 0) {
      fprintf(stderr, "Can not configure this PCM device.\n");
      return(-1);
    }

Des informations sur les configurations possibles peuvent être obtenues grâce à un ensemble de fonctions :

    snd_pcm_hw_params_can_<capability>
    snd_pcm_hw_params_is_<property>
    snd_pcm_hw_params_get_<parameter>

Les paramètres disponibles les plus importants, les types d'accès, la taille du buffer, le nombre de canaux, le format d'échantillon, le taux d'échantillonnage et le nombre de périodes, peuvent être affichés avec l'ensemble de fonctions :

snd_pcm_hw_params_test_<parameter>

Ces fonctions sont particulièrement importantes si l'interface "hw" est utilisé. L'espace de configuration peut être limité à une certaine configuration grâce à l'ensemble de fonctions :

snd_pcm_hw_params_set_<parameter>

Pour cet exemple, nous supposons que la carte son peut être configurée pour la lecture 16 bit stéréo échantillonée à 44100 hertz. Donc, il faudra déterminer l'espace de configuration :

    int rate = 44100; /* Sample rate */
    int exact_rate;   /* Sample rate returned by */
                      /* snd_pcm_hw_params_set_rate_near */ 
    int dir;          /* exact_rate == rate --> dir = 0 */
                      /* exact_rate < rate   --> dir = -1 */
                      /* exact_rate > rate  --> dir = 1 */
    int periods = 2;       /* Number of periods */
    int periodsize = 8192; /* Periodsize (bytes) */

Le type d'accès indique la manière dont les données multicanales sont stockées dans le buffer. Pour l'accès INTERLEAVED, chaque frame du buffer contient les données consécutives d'échantillon pour les canaux. Pour des données 16 bit stéréo, ceci signifie que le buffer contient des caractères alternatifs de données d'échantillon pour le canal gauche et droit.

Pour l'accès NONINTERLEAVED, chaque période contient d'abord toutes les données d'échantillon pour le premier canal suivi des données d'échantillon pour le deuxième canal et ainsi de suite.

    /* Set access type. This can be either    */
    /* SND_PCM_ACCESS_RW_INTERLEAVED or       */
    /* SND_PCM_ACCESS_RW_NONINTERLEAVED.      */
    /* There are also access types for MMAPed */
    /* access, but this is beyond the scope   */
    /* of this introduction.                  */
    if (snd_pcm_hw_params_set_access(pcm_handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED) < 0) {
      fprintf(stderr, "Error setting access.\n");
      return(-1);
    }
  
    /* Set sample format */
    if (snd_pcm_hw_params_set_format(pcm_handle, hwparams, SND_PCM_FORMAT_S16_LE) < 0) {
      fprintf(stderr, "Error setting format.\n");
      return(-1);
    }

    /* Set sample rate. If the exact rate is not supported */
    /* by the hardware, use nearest possible rate.         */ 
    exact_rate = snd_pcm_hw_params_set_rate_near(pcm_handle, hwparams, rate, &dir);
    if (dir != 0) {
      fprintf(stderr, "The rate %d Hz is not supported by your hardware.\n 
                       ==> Using %d Hz instead.\n", rate, exact_rate);
    }

    /* Set number of channels */
    if (snd_pcm_hw_params_set_channels(pcm_handle, hwparams, 2) < 0) {
      fprintf(stderr, "Error setting channels.\n");
      return(-1);
    }

    /* Set number of periods. Periods used to be called fragments. */ 
    if (snd_pcm_hw_params_set_periods(pcm_handle, hwparams, periods, 0) < 0) {
      fprintf(stderr, "Error setting periods.\n");
      return(-1);
    }

L'unité de la taille du buffer dépend de la fonction. Parfois elle est donnée en bytes, parfois le nombre de frames doit être indiqué. Une frame réprésente le vecteur de données d'échantillon pour tous les canaux. Pour des données 16 bit stéréo, une frame a une longueur de quatre bytes.

    /* Set buffer size (in frames). The resulting latency is given by */
    /* latency = periodsize * periods / (rate * bytes_per_frame)     */
    if (snd_pcm_hw_params_set_buffer_size(pcm_handle, hwparams, (periodsize * periods)>>2) < 0) {
      fprintf(stderr, "Error setting buffersize.\n");
      return(-1);
    }

Si votre matériel ne supporte pas une taille de buffer de 2^n, vous pouvez utiliser la fonction :

     snd_pcm_hw_params_set_buffer_size_near

Ceci fonctionne de la même façon que :

     snd_pcm_hw_params_set_rate_near

Maintenant appliquons la configuration du dispositif PCM indiqué par pcm_handle. Ceci préparera également le dispositif PCM.

    /* Apply HW parameter settings to */
    /* PCM device and prepare device  */
    if (snd_pcm_hw_params(pcm_handle, hwparams) < 0) {
      fprintf(stderr, "Error setting HW params.\n");
      return(-1);
    }

Après que le dispositif PCM soit configuré, nous pouvons commencer à écrire des données PCM. Les premiers accès en écriture lancerons le PCM playback. Pour l'accès en écriture de type interleaved, nous utiliserons la fonction :

    /* Write num_frames frames from buffer data to    */ 
    /* the PCM device pointed to by pcm_handle.       */
    /* Returns the number of frames actually written. */
    snd_pcm_sframes_t snd_pcm_writei(pcm_handle, data, num_frames);

Pour le type noninterleaved

    /* Write num_frames frames from buffer data to    */ 
    /* the PCM device pointed to by pcm_handle.       */ 
    /* Returns the number of frames actually written. */
    snd_pcm_sframes_t snd_pcm_writen(pcm_handle, data, num_frames);

Après l'ouverture de PCM playback, nous devons nous assurer que notre application envoie assez de données au buffer de la carte son. Autrement, un buffer underrun (survient quand la mémoire tampon se vide) se produira. Après qu'un underrun se soit produit, le snd_pcm_prepare devra être utilisé.

    unsigned char *data;
    int pcmreturn, l1, l2;
    short s1, s2;
    int frames;

    data = (unsigned char *)malloc(periodsize);
    frames = periodsize >> 2;
    for(l1 = 0; l1 < 100; l1++) {
      for(l2 = 0; l2 < num_frames; l2++) {
        s1 = (l2 % 128) * 100 - 5000;  
        s2 = (l2 % 256) * 100 - 5000;  
        data[4*l2] = (unsigned char)s1;
        data[4*l2+1] = s1 >> 8;
        data[4*l2+2] = (unsigned char)s2;
        data[4*l2+3] = s2 >> 8;
      }
      while ((pcmreturn = snd_pcm_writei(pcm_handle, data, frames)) < 0) {
        snd_pcm_prepare(pcm_handle);
        fprintf(stderr, "------------------------------ Buffer Underrun---------------------------------- \n");
      }
    }

Pour arrêter la lecture, nous pouvons utiliser :

     snd_pcm_drop

ou

     snd_pcm_drain

La première fonction arrêtera immédiatement la lecture et détruira les frames en attente. La deuxième fonction arretera la lecture après que les frames restante aient été jouées.

    /* Stop PCM device and drop pending frames */
    snd_pcm_drop(pcm_handle);

    /* Stop PCM device after pending frames have been played */ 
    snd_pcm_drain(pcm_handle);