Chapitre 7. Scheduling MIDI events: miniArp

Dans le chapitre section 5, nous avons utilisé la sortie immédiate des événements MIDI. Cependant, les événements MIDI sont habituellement programmés dans une file d'attente qui est controlée par un temporisateur matériel.

L'arpeggiator miniArp.c fournit un exemple simple de programmeur de tache MIDI

/* miniArp.c by Matthias Nagorni */

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

#define TICKS_PER_QUARTER 128
#define MAX_SEQ_LEN        64

int queue_id, port_in_id, port_out_id, transpose, bpm0, bpm, tempo, swing, sequence[3][MAX_SEQ_LEN], seq_len;
snd_seq_t *seq_handle;
char seq_filename[1024];
snd_seq_tick_time_t tick;

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, "miniArp");
  if ((port_out_id = snd_seq_create_simple_port(seq_handle, "miniArp",
            SND_SEQ_PORT_CAP_READ|SND_SEQ_PORT_CAP_SUBS_READ,
            SND_SEQ_PORT_TYPE_APPLICATION)) < 0) {
    fprintf(stderr, "Error creating sequencer port.\n");
  }
  if ((port_in_id = snd_seq_create_simple_port(seq_handle, "miniArp",
            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);
}

void set_tempo() {

  snd_seq_queue_tempo_t *queue_tempo;

  snd_seq_queue_tempo_malloc(&queue_tempo);
  tempo = (int)(6e7 / ((double)bpm * (double)TICKS_PER_QUARTER) * (double)TICKS_PER_QUARTER);
  snd_seq_queue_tempo_set_tempo(queue_tempo, tempo);
  snd_seq_queue_tempo_set_ppq(queue_tempo, TICKS_PER_QUARTER);
  snd_seq_set_queue_tempo(seq_handle, queue_id, queue_tempo);
  snd_seq_queue_tempo_free(queue_tempo);
}

snd_seq_tick_time_t get_tick() {

  snd_seq_queue_status_t *status;
  snd_seq_tick_time_t current_tick;
  
  snd_seq_queue_status_malloc(&status);
  snd_seq_get_queue_status(seq_handle, queue_id, status);
  current_tick = snd_seq_queue_status_get_tick_time(status);
  snd_seq_queue_status_free(status);
  return(current_tick);
}

void init_queue() {

  queue_id = snd_seq_alloc_queue(seq_handle);
  snd_seq_set_client_pool_output(seq_handle, (seq_len<<1) + 4);
}

void clear_queue() {

  snd_seq_remove_events_t *remove_ev;

  snd_seq_remove_events_malloc(&remove_ev);
  snd_seq_remove_events_set_queue(remove_ev, queue_id);
  snd_seq_remove_events_set_condition(remove_ev, SND_SEQ_REMOVE_OUTPUT | SND_SEQ_REMOVE_IGNORE_OFF);
  snd_seq_remove_events(seq_handle, remove_ev);
  snd_seq_remove_events_free(remove_ev);
}

void arpeggio() {

  snd_seq_event_t ev;
  int l1;
  double dt;
 
  for (l1 = 0; l1 < seq_len; l1++) {
    dt = (l1 % 2 == 0) ? (double)swing / 16384.0 : -(double)swing / 16384.0;
    snd_seq_ev_clear(&ev);
    snd_seq_ev_set_note(&ev, 0, sequence[2][l1] + transpose, 127, sequence[1][l1]);
    snd_seq_ev_schedule_tick(&ev, queue_id,  0, tick);
    snd_seq_ev_set_source(&ev, port_out_id);
    snd_seq_ev_set_subs(&ev);
    snd_seq_event_output_direct(seq_handle, &ev);
    tick += (int)((double)sequence[0][l1] * (1.0 + dt));
  }
  snd_seq_ev_clear(&ev);
  ev.type = SND_SEQ_EVENT_ECHO; 
  snd_seq_ev_schedule_tick(&ev, queue_id,  0, tick);
  snd_seq_ev_set_dest(&ev, snd_seq_client_id(seq_handle), port_in_id);
  snd_seq_event_output_direct(seq_handle, &ev);
}

void midi_action() {

  snd_seq_event_t *ev;

  do {
    snd_seq_event_input(seq_handle, &ev);
    switch (ev->type) {
      case SND_SEQ_EVENT_ECHO:
        arpeggio(); 
        break;
      case SND_SEQ_EVENT_NOTEON:
        clear_queue();
        transpose = ev->data.note.note - 60;
        tick = get_tick();
        arpeggio();
        break;        
      case SND_SEQ_EVENT_CONTROLLER:
        if (ev->data.control.param == 1) {           
          bpm = (int)((double)bpm0 * (1.0 + (double)ev->data.control.value / 127.0));
          set_tempo();
        } 
        break;
      case SND_SEQ_EVENT_PITCHBEND:
        swing = (double)ev->data.control.value;        
        break;
    }
    snd_seq_free_event(ev);
  } while (snd_seq_event_input_pending(seq_handle, 0) > 0);
}

void parse_sequence() {

  FILE *f;
  char c;
  
  if (!(f = fopen(seq_filename, "r"))) {
    fprintf(stderr, "Couldn't open sequence file %s\n", seq_filename);
    exit(1);
  }
  seq_len = 0;
  while((c = fgetc(f))!=EOF) {
    switch (c) {
      case 'c': 
        sequence[2][seq_len] = 0; break; 
      case 'd': 
        sequence[2][seq_len] = 2; break;
      case 'e': 
        sequence[2][seq_len] = 4; break;
      case 'f': 
        sequence[2][seq_len] = 5; break;
      case 'g': 
        sequence[2][seq_len] = 7; break;
      case 'a': 
        sequence[2][seq_len] = 9; break;
      case 'h': 
        sequence[2][seq_len] = 11; break;
    }                    
    c =  fgetc(f);
    if (c == '#') {
      sequence[2][seq_len]++;
      c =  fgetc(f);
    }
    sequence[2][seq_len] += 12 * atoi(&c);
    c =  fgetc(f);
    sequence[1][seq_len] = TICKS_PER_QUARTER / atoi(&c);
    c =  fgetc(f);
    sequence[0][seq_len] = TICKS_PER_QUARTER / atoi(&c);    
    seq_len++;
  }
  fclose(f);
}

void sigterm_exit(int sig) {

  clear_queue();
  sleep(2);
  snd_seq_stop_queue(seq_handle, queue_id, NULL);
  snd_seq_free_queue(seq_handle, queue_id);
  exit(0);
}

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

  int npfd, l1;
  struct pollfd *pfd;
    
  if (argc < 3) { 
    fprintf(stderr, "\n\nminiArp <beats per minute> <sequence file>\n");
    exit(1);
  }
  bpm0 = atoi(argv[1]);
  bpm = bpm0;
  strcpy(seq_filename, argv[2]); 
  parse_sequence();
  seq_handle = open_seq();
  init_queue();
  set_tempo();
  arpeggio();
  snd_seq_start_queue(seq_handle, queue_id, NULL);
  snd_seq_drain_output(seq_handle);
  npfd = snd_seq_poll_descriptors_count(seq_handle, POLLIN);
  pfd = (struct pollfd *)alloca(npfd * sizeof(struct pollfd));
  snd_seq_poll_descriptors(seq_handle, pfd, npfd, POLLIN);
  transpose = 0;
  swing = 0;
  tick = 0;
  signal(SIGINT, sigterm_exit);          
  signal(SIGTERM, sigterm_exit);  
  arpeggio();
  while (1) {
    if (poll(pfd, npfd, 100000) > 0) {
      for (l1 = 0; l1 < npfd; l1++) {
        if (pfd[l1].revents > 0) midi_action(); 
      }
    }  
  }
}

MiniArp posséde deux paramètres, le tempo (battements par minute) et le nom de fichier de la séquence où la boucle doit être réalisée. Ce fichier de séquence se compose d'une ligne de caractères simples. Chaque note est caractérisée par 4 ou 5 caractères vwxyz, où

Y et Z sont tous les deux représentés comme une fraction d'un quart de note : longueur = 1/4 de 1/y. Une séquence valide serait :

    c488c#488d488d#488e484c584g584c684e684g584c684e544c584g484e488d#488d488c#488

MiniArp traite les événements de pitch et de modulation. N'importe quel événement NOTEON transposera la séquence selon la différence entre la note pressée et le C moyen.

Maintenant, quels sont les secrets de ce programme ? Beaucoup de techniques ont déjà été vues. Les fonctions intéressantes sont init_queue, set_tempo, arpeggio, clear_queue, et get_tick. Un signal handler pour SIGINT et SIGTERM est implementé pour éviter les notes persistantes.

A)

void init_queue()

Cette partie défini une file d'attente et la taille du buffer. Cette taille est passée au snd_seq_set_client_pool_output comme nombre d'événements. Pour les files d'attente il pourrait être utile de regarder /proc/asound/seq/queues.

B) void set_tempo()

void init_queue()

La période de programme des événements peut être indiquée en temps réel ou dans les ticks. Si vous voulez changer le tempo, il est pratique de modifier le temps dans les ticks. Ceci changera même le tempo des événements qui ont déja été programmés dans la file d'attente.

Le tempo est transmis au snd_seq_queue_tempo_set_tempo en micro-secondes par tick. La fonction snd_seq_queue_tempo_set_ppq défini les ticks par quart. Habituellement, on veut indiquer le tempo en beats par minute (bpm), par conséquent set_tempo calcule les paramètres corrects de tempo et de ppq en bpm.

C) void arpeggio()

void init_queue()

Ici, toutes les notes de la séquence sont programmées dans la file d'attente. D'abord, nous devons initialiser la structure d'événement, puis, définir un événement de note de longueur fixe en utilisant le snd_seq_ev_set_note. Cet événement de note est programmé en spécifiant son tick time avec snd_seq_ev_schedule_tick.

La source d'événement est le port de sortie du miniArp et nous utilisons snd_seq_set_subs pour spécifier à ALSA que nous voulons que l'événement soit transmis à tous les "abonnés" de ce port. Pour finir, nous utilisons snd_seq_event_output_direct pour la sortie unbuffered de l'événement de la file d'attente.

Les événements de note fonctionnent comme suit : quand c'est leur tour, ils créent un événement NOTEON et puis le convertissent en événement NOTEOFF, état qui se produit au temps = tempsdépart + la longueur de note.

Puisque nous voulons que la séquence fasse une boucle, nous programmons un événement SND_SEQ_EVENT_ECHO après chaque séquence. Cette fois, la destination est l'application elle-même. Quand l'événement SND_SEQ_EVENT_ECHO est expédié, il active la fonction poll dans main() pour le renvoyer et midi_action est appellé. midi_action, SND_SEQ_EVENT_ECHO auront comme conséquence un appel à arpeggi0 et la boucle est complète.

D) void clear_queue()

void init_queue()

Quand miniArp reçoit un événement NOTEON, la séquence est transposé. clear_queue est appelé à chaque événement NOTEON pour déclencher une nouvelle séquence. SND_SEQ_REMOVE_OUTPUT | SND_SEQ_REMOVE_IGNORE_OFF sont transmis à snd_seq_remove_events_set_condition pour indiquer à ALSA que seulement les événements de sortie devraient être enlevés de la file d'attente et les événements NOTEOFF ne doivent pas être supprimé.

La dernière condition s'assure que toutes les notes de la séquence reçoivent un événement NOTEOFF après que leur longueur se soit écoulée, même lorsqu'une nouvelle séquence est déclenchée.

E) snd_seq_tick_time_t get_tick(

void init_queue()

Cette fonction récupére la valeur courante tick à partir d'une structure de file d'attente.