Chapitre 6. Combining PCM and MIDI: miniFMsynth

Le synthétiseur miniFMsynth.c montre le traitement d'événement MIDI et la lecture PCM.

/* miniFMsynth 1.0 by Matthias Nagorni    */
/* This program uses callback-based audio */
/* playback as proposed by Paul Davis on  */
/* the linux-audio-dev mailinglist.       */

#include <stdio.h>   
#include <stdlib.h>
#include <alsa/asoundlib.h>
#include <math.h>

#define POLY 10
#define GAIN 5000.0
#define BUFSIZE 512

snd_seq_t *seq_handle;
snd_pcm_t *playback_handle;
short *buf;
double phi[POLY], phi_mod[POLY], pitch, modulation, velocity[POLY], attack, decay, sustain, release, env_time[POLY], env_level[POLY];
int harmonic, subharmonic, transpose, note[POLY], gate[POLY], note_active[POLY];

snd_seq_t *open_seq() {

    snd_seq_t *seq_handle;
    
    if (snd_seq_open(&seq_handle, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0) {
        fprintf(stderr, "Error opening ALSA sequencer.\n");
        exit(1);
    }
    snd_seq_set_client_name(seq_handle, "miniFMsynth");
    if (snd_seq_create_simple_port(seq_handle, "miniFMsynth",
        SND_SEQ_PORT_CAP_WRITE|SND_SEQ_PORT_CAP_SUBS_WRITE,
        SND_SEQ_PORT_TYPE_APPLICATION) < 0) {
        fprintf(stderr, "Error creating sequencer port.\n");
        exit(1);
    }
    return(seq_handle);
}

snd_pcm_t *open_pcm(char *pcm_name) {

    snd_pcm_t *playback_handle;
    snd_pcm_hw_params_t *hw_params;
    snd_pcm_sw_params_t *sw_params;
            
    if (snd_pcm_open (&playback_handle, pcm_name, SND_PCM_STREAM_PLAYBACK, 0) < 0) {
        fprintf (stderr, "cannot open audio device %s\n", pcm_name);
        exit (1);
    }
    snd_pcm_hw_params_alloca(&hw_params);
    snd_pcm_hw_params_any(playback_handle, hw_params);
    snd_pcm_hw_params_set_access(playback_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);
    snd_pcm_hw_params_set_format(playback_handle, hw_params, SND_PCM_FORMAT_S16_LE);
    snd_pcm_hw_params_set_rate_near(playback_handle, hw_params, 44100, 0);
    snd_pcm_hw_params_set_channels(playback_handle, hw_params, 2);
    snd_pcm_hw_params_set_periods(playback_handle, hw_params, 2, 0);
    snd_pcm_hw_params_set_period_size(playback_handle, hw_params, BUFSIZE, 0);
    snd_pcm_hw_params(playback_handle, hw_params);
    snd_pcm_sw_params_alloca(&sw_params);
    snd_pcm_sw_params_current(playback_handle, sw_params);
    snd_pcm_sw_params_set_avail_min(playback_handle, sw_params, BUFSIZE);
    snd_pcm_sw_params(playback_handle, sw_params);
    return(playback_handle);
}

double envelope(int *note_active, int gate, double *env_level, double t, double attack, double decay, double sustain, double release) {

    if (gate)  {
        if (t > attack + decay) return(*env_level = sustain);
        if (t > attack) return(*env_level = 1.0 - (1.0 - sustain) * (t - attack) / decay);
        return(*env_level = t / attack);
    } else {
        if (t > release) {
            if (note_active) *note_active = 0;
            return(*env_level = 0);
        }
        return(*env_level * (1.0 - t / release));
    }
}

int midi_callback() {

    snd_seq_event_t *ev;
    int l1;
  
    do {
        snd_seq_event_input(seq_handle, &ev);
        switch (ev->type) {
            case SND_SEQ_EVENT_PITCHBEND:
                pitch = (double)ev->data.control.value / 8192.0;
                break;
            case SND_SEQ_EVENT_CONTROLLER:
                if (ev->data.control.param == 1) {
                    modulation = (double)ev->data.control.value / 10.0;
                } 
                break;
            case SND_SEQ_EVENT_NOTEON:
                for (l1 = 0; l1 < POLY; l1++) {
                    if (!note_active[l1]) {
                        note[l1] = ev->data.note.note;
                        velocity[l1] = ev->data.note.velocity / 127.0;
                        env_time[l1] = 0;
                        gate[l1] = 1;
                        note_active[l1] = 1;
                        break;
                    }
                }
                break;        
            case SND_SEQ_EVENT_NOTEOFF:
                for (l1 = 0; l1 & POLY; l1++) {
                    if (gate[l1] && note_active[l1] && (note[l1] == ev->data.note.note)) {
                        env_time[l1] = 0;
                        gate[l1] = 0;
                    }
                }
                break;        
        }
        snd_seq_free_event(ev);
    } while (snd_seq_event_input_pending(seq_handle, 0) > 0);
    return (0);
}

int playback_callback (snd_pcm_sframes_t nframes) {

    int l1, l2;
    double dphi, dphi_mod, f1, f2, f3, freq_note, sound;
      
    memset(buf, 0, nframes * 4);
    for (l2 = 0; l2 < POLY; l2++) {
        if (note_active[l2]) {
            f1 = 8.176 * exp((double)(transpose+note[l2]-2)*log(2.0)/12.0);
            f2 = 8.176 * exp((double)(transpose+note[l2])*log(2.0)/12.0);
            f3 = 8.176 * exp((double)(transpose+note[l2]+2)*log(2.0)/12.0);
            freq_note = (pitch > 0) ? f2 + (f3-f2)*pitch : f2 + (f2-f1)*pitch;
            dphi = M_PI * freq_note / 22050.0;                                    
            dphi_mod = dphi * (double)harmonic / (double)subharmonic;
            for (l1 = 0; l1 < nframes; l1++) {
                phi[l2] += dphi;
                phi_mod[l2] += dphi_mod;
                if (phi[l2] > 2.0 * M_PI) phi[l2] -= 2.0 * M_PI;
                if (phi_mod[l2] > 2.0 * M_PI) phi_mod[l2] -= 2.0 * M_PI;
                sound = GAIN * envelope(&note_active[l2], gate[l2], &env_level[l2], env_time[l2], attack, decay, sustain, release)
                             * velocity[l2] * sin(phi[l2] + modulation * sin(phi_mod[l2]));
                env_time[l2] += 1.0 / 44100.0;
                buf[2 * l1] += sound;
                buf[2 * l1 + 1] += sound;
            }
        }    
    }
    return snd_pcm_writei (playback_handle, buf, nframes); 
}
      
int main (int argc, char *argv[]) {

    int nfds, seq_nfds, l1;
    struct pollfd *pfds;
    
    if (argc < 10) {
        fprintf(stderr, "miniFMsynth <device> <FM> <harmonic> <subharmonic> <transpose> <a> <d> <s> <r>\n"); 
        exit(1);
    }
    modulation = atof(argv[2]);
    harmonic = atoi(argv[3]);
    subharmonic = atoi(argv[4]);
    transpose = atoi(argv[5]);
    attack = atof(argv[6]);
    decay = atof(argv[7]);
    sustain = atof(argv[8]);
    release = atof(argv[9]);
    pitch = 0;
    buf = (short *) malloc (2 * sizeof (short) * BUFSIZE);
    playback_handle = open_pcm(argv[1]);
    seq_handle = open_seq();
    seq_nfds = snd_seq_poll_descriptors_count(seq_handle, POLLIN);
    nfds = snd_pcm_poll_descriptors_count (playback_handle);
    pfds = (struct pollfd *)alloca(sizeof(struct pollfd) * (seq_nfds + nfds));
    snd_seq_poll_descriptors(seq_handle, pfds, seq_nfds, POLLIN);
    snd_pcm_poll_descriptors (playback_handle, pfds+seq_nfds, nfds);
    for (l1 = 0; l1 < POLY; note_active[l1++] = 0);
    while (1) {
        if (poll (pfds, seq_nfds + nfds, 1000) > 0) {
            for (l1 = 0; l1 < seq_nfds; l1++) {
               if (pfds[l1].revents > 0) midi_callback();
            }
            for (l1 = seq_nfds; l1 < seq_nfds + nfds; l1++) {    
                if (pfds[l1].revents > 0) { 
                    if (playback_callback(BUFSIZE) < BUFSIZE) {
                        fprintf (stderr, "xrun !\n");
                        snd_pcm_prepare(playback_handle);
                    }
                }
            }        
        }
    }
    snd_pcm_close (playback_handle);
    snd_seq_close (seq_handle);
    free(buf);
    return (0);
}

Il utilise plusieurs paramètres,

   miniFMsynth <device> <FM> <harmonic> <subharmonic> <transpose> <a> <d> <s> <r>

dispositif PCM, force de modulation de fréquence, harmonique de l'oscillateur principal (nombre entier), sous harmonique de l'oscillateur principal (nombre entier), Offset pour les deux oscillateurs (nombre entier), attaque, delay, sustain, release.

Quelque exemple

Le miniFMsynth réagit sur des événements de pitch et de modulation. Puisqu'il n'est pas optimisé pour la performance (vous n'utiliseriez par exemple jamais la fonction "sin" dans un "vrai" programme), vous pourriez devoir diminuer la polyphony dans la source en modifiant #define POLY.

Après avoir lu, et compris, les chapitres précédents, il devrait être facile de comprendre la partie MIDI du programme. Quant à la lecture PCM, miniFMsynth utilise polling, cette technique est plus avancée que la sortie direct décrit dans le chapitre 2. Regardons certains détail

A)

    snd_pcm_t *open_pcm(char *pcm_name)

Pour la plupart des fonctions de snd_pcm_hw_params utilisées ici voir le chapitre 2. Cependant, ici nous n'indiquons pas la taille du buffer, mais nous définissons la taille de la periode à la place.

Puisque nous voulons utiliser le polling pour la sortie PCM, nous devons également définir le paramètre "avail_min" . Un poll dans un fichier PCM descripteur retournera des données seulement si des frames avail_min peuvent être transmis au dispositif.

Nous initialisons snd_pcm_sw_params_t avec la configuration courante appelée snd_pcm_sw_params_current, puis définissons avail_min avec snd_pcm_sw_params_set_avail_min et activons cette configuration en utilisant snd_pcm_sw_params.

B)

   double envelope(int *note_active, int gate, double *env_level, double t, double attack, double decay, double sustain, double release)

Si gate==1, la note est toujours pressé ==> l'enveloppe est dans le secteur attaque, decay, sustain . Si gate==0, l'enveloppe est dans le secteur release. Si t>release, la cellule respective d'oscillateur est libéré par le réglage de note_active à zéro.

C)

   int midi_callback()

Cette partie, en gros, a déjà été vu dans le chapitre 4. Quand un événement NOTEON est reçu, une nouvelle cellule d'oscillateur est initialisée en plaçant la note_active respective à 1. Ceci fonctionne seulement si la polyphony n'est pas excessive, autrement la note est omise.

Si cela ne vous plait pas, vous pouvez le modifier et laissez par exemple la cellule d'oscillateur avec un plus petit env_level qui sera libéré et utilisé pour la nouvelle note. Il y a un détail important au sujet des événements de note : Quelques claviers (par exemple MP 9000 de Kawai) n'envoient pas d'événements NOTEOFF, mais envoient un NOTEON avec la vélocité 0 à la place. Dans ce cas-ci, vous devez ajouter les lignes suivantes

    if ((ev->type == SND_SEQ_EVENT_NOTEON) && (ev->data.note.velocity == 0)) 
         ev->type = SND_SEQ_EVENT_NOTEOFF;

directement après snd_seq_event_input

D)

    int playback_callback (snd_pcm_sframes_t nframes

Cette partie traite les données PCM et les envoi au dispositif à l'aide de snd_pcm_write. Cette fonction renverra des données immédiatement, puisque nous utilisons seulement le playback_callback, si nous connaisont le nombre nframes qui peuvent être transmis au dispositif PCM. MiniFMsynth écrit toujours des gros morceaux de taille fixe, ainsi nframes équivaut toujours à BUFSIZE.

E)

    int main (int argc, char *argv[])

Ici, nous utilisons le polling pour l'entrée d'événement MIDI et pour la lecture PCM. La fonction poll renverra des données seulement si un événement MIDI peut être lu à partir de l'entrée du buffer du séquenceur ou si au moins des frames avail_min (= =BUFSIZE) peuvent être transmis au dispositif PCM.

Si les données PCM ne sont pas fourni au dispositif de PCM avant que le buffer de la carte son soit vide, un buffer underrun se produit, ceci arrêtera la lecture. Dans ce cas-ci, la valeur de retour du snd_pcm_write sera plus petite que BUFSIZE, nous devrons alors relancer la lecture à l'aide de snd_pcm_prepare.