ALSA e Gambas - Subsistema Seq: introduzione

Da Gambas-it.org - Wikipedia.

Preambolo

Nella precedente sezione abbiamo visto in linea teorica la struttura generale di ALSA ed il suo rapporto con i Client, ossia con i programmi-utente che interagiscono con esso.

Nella presente sezione, invece, affronteremo praticamente l'argomento della programmazione di ALSA con Gambas.

Metodologicamente intendiamo mostrare il rapporto fra ALSA e Gambas attraverso la costruzione di un semplice programma capace di inviare semplici dati Midi, e spiegandone adeguatamente ogni passaggio fondamentale della sua codifica.

Innanzitutto imposteremo il nostro progetto assegnandogli una classe principale, ove inseriremo i comandi essenziali, ed una classe speciale ove scriveremo tutte le funzioni specifiche di ALSA.


Nella classe principale - che chiameremo: FMain.class - scriveremo innanzitutto le informazioni per creare la classe secondaria, che chiameremo: CAlsa.class .
Scriveremo la funzione che chiama la routine analoga in CAlsa.class, contenente il nome del nostro client che sarà attribuito effettivamente con una specifica funzione di ALSA che richiameremo in CAlsa.class .


Cominciamo poi a strutturare la Classe che richiama le funzioni di ALSA: CALsa.class [Nota 1]. Per poter utilizzare le funzioni di ALSA è necessario usare la libreria delle API di ALSA (attualmente: /usr/lib/x86_64-linux-gnu/libasound.so.2.0.0) specificandola nel codice del progetto Gambas in maniera adeguata con l'istruzione "Library ":

Library "libasound:2.0.0"

Tale libreria è propriamente un'interfaccia ai drivers di ALSA [Nota 2].


Pertanto, il nostro codice comincerà ad assumere queste linee iniziali (distingueremo il codice della classe principale, e quello della classe particolare delle funzioni di ALSA):

In FMain.class:

Public alsa as CAlsa


Public SUB Form_Open()
 
' Crea un Oggetto della Classe "CAlsa":
 alsa = New CAlsa as "alsa"
 
' Invoca la prima procedura della Classe "CAlsa":
 alsa.alsa_open("Progetto sequencer in Gambas")
 
End


In CAlsa.class:

Library "libasound:2.0.0"

Richiamare le funzioni di ALSA

Tutte le funzioni di ALSA [Nota 3], che si rendono a noi necessarie, presenti nella libreria delle API di ALSA sono esterne al sistema Gambas, e devono pertanto essere debitamente "richiamate". Per mezzo di tale chiamata esse vengono rese disponibili ed effettivamente utilizzabili nella nostra programmazione. Praticamente la chiamata avviene con una dichiarazione di una normale subroutine di Gambas mediante il termine "Extern" [Nota 4][Nota 5].

Le funzioni proprie di ALSA sono degli handle [Nota 6] che ritornano sempre un codice di errore: " error = snd_seq_xxx(yyy, ....) ", laddove la variabile "error" è un integer. Un valore di ritorno pari a zero significa che l'uso della funzione esterna ha avuto successo.

Il Client e le sue porte

Linee generali

Nella sezione precedente abbiamo detto che il dispositivo ALSA svolge sostanzialmente due compiti: quello di scheduling, pianificare l'esecuzione di processi, ossia pianificare secondo una precisa sequenza, come in un elenco, l'invio degli eventi e quello di dispatching, ossia di indirizzarli all'esatta destinazione nel momento giusto. La fase della gestione reale degli eventi è lasciata ai programmi applicativi-utente (come i sequencer), i quali, in qualità di Client [Nota 6 bis], comunicano tali dati con il dispositivo ALSA (più precisamente con uno dei suoi sub-sistemi; per il Midi: seq) che invece fa da Server.
Abbiamo anche detto che il nostro applicativo deve possedere le funzioni di un Client. Più precisamente una parte di esso svolgerà le funzioni di Client. Si rende pertanto necessario creare appunto un Client, capace di comunicare con gli altri dispositivi esterni e certamente con il dispositivo ALSA. Per creare il nostro Client dobbiamo utilizzare particolari funzioni di ALSA, che dovranno essere preventivamente "richiamate"; attraverso con una particolare dichiarazione contenente l'istruzione "Extern".

Il nostro progetto seguirà più o meno la struttura base di ogni applicazione in ALSA:

1) aprire il subsistema specifico (in questo caso "seq" del dispositivo ALSA mediante l'apposita funzione;

2) impostare i parametri di detta funzione;

3) ricevere e/o inviare dati Midi al dispositivo;

4) chiudere il dispostivo.

Scrittura ed invio degli eventi Midi

Linee generali

Dobbiamo innanzitutto comprendere tre questioni:

1. che cosa vuole ALSA;

2. con quali strumenti preparare e modificare la zona di memoria richiesta;

3. l'organizzazione generale del programma.


Riguardo al punto 1, ALSA vuole una zona di memoria contenente, in posizioni specifiche, i valori adatti per la preparazione e per la realizzazione dell'evento Midi. Nello specifico, ALSA vuole una zona di memoria contigua, dove nel primo byte c'è il tipo di evento, nel secondo byte i flag, dal terzo al sesto byte il timestamp e via dicendo.

Riguardo al punto 2, va definita una zona di memoria. Per preparare e modificare questa zona di memoria è possibile usare una delle seguenti risorse:

Riguardo infine al punto 3, l'organizzazione generale significa: ogni volta che viene inviato un evento ad ALSA, è necessario passargli una zona di memoria che sarà sempre la stessa, ri-scrivendola ogni volta, evitando (anche se potrebbe essere un’alternativa) di crearla al momento. Per preparare un evento, inoltre, visto che ci sono membri comuni, si userà una sola routine comune a tutti gli eventi.


La struttura virtuale degli eventi MIDI in ALSA

Gli eventi Midi gestiti dal subsistema seq di ALSA sono temporizzati mediante dei timestamp, e sono organizzati secondo un'apposita struttura precisa di dati: [Nota 7]

Type un singolo byte
Flags un singolo byte
Tag un singolo byte
Queue un singolo byte
Time, composto da:
Tick o Real-Time Tv_sec un integer
Real-Time Tv_nsec un integer
Source, composto da:
Client un singolo byte
Port un singolo byte
Dest, composto da:
Client un singolo byte
Port un singolo byte

Da considerare che i membri Time, Source e Dest sono dati strutturati, cioé contengono al loro interno altri membri. Per questi dati il membro è uno solo, ma per ragioni tecniche viene spezzato in due istruzioni. Quindi un evento in ALSA contiene 4 dati semplici + 3 strutturati "doppi" (totale sette), ma i tre strutturati vengono scritti, per ragioni tecniche, con un passaggio doppio, e quindi il totale delle scritture diventa 10.

In particolare riguardo ai membri della Struttura ALSA degli Eventi Midi: [Nota 7 bis]:

  • Type contiene il tipo di evento;
  • Flags può contenere il Timestamp mode (Real-Time o Tick-Time, Assoluto o Relativo), il Data storage type ed il scheduling priority;
  • Tag può contenere un marcatore arbitrario;
  • Queue contiene il valore per lo Scheduling queue;
  • Time contiene al suo interno due mebri entrambi di un integer: il primo ed il secondo per il timestamp specificato in Real time; oppure solo il primo per il timestamp specificato in Midi Tick, in tal caso il secondo membro non serve e può essere posto a zero;
  • Source contiene l'indirizzo del sorgente dell'evento. E' formato da due membri: il primo l'identificativo numerico, il secondo il numero della porta;
  • Dest contiene l'indirizzo del destinatario dell'evento. E' formato da due membri: il primo l'identificativo numerico, il secondo il numero della porta.

Complessivamente i dati generali, comuni a tutti i messaggi Midi, occupano ben 16 byte della memoria allocata:
Byte:01234 5 6 78 9 10 1112131415
┴—┴—┴—┴—┴———┴————┴—┴—┴—┴—
Dati:TypeFlagsTagQueueTick_time / Real_time Tv_sec0 / Real_time Tv_nsecSource_clientPort_clientDest_clientDdest_port


Dopo i precedenti dati generali comuni dovranno seguire nella zona di memoria riservata i dati specifici del particolare messaggio Midi.

Il Timestamp

Il Timestamp di un evento può essere specificato in: [Nota 7 ter]

  • tick_time, corrisponde ai tick del Midi.
  • real_time, corrisponde all'orologio, e la risoluzione è in secondi e in nanosecondi;

Il Timestamp può inoltre essere:

  • assoluto, quando il tempo è determinato dal momento in cui la coda di eventi ha inizio;
  • relativo, quando il tempo è determinato dal momento in cui l'evento della coda è stato inviato.

Gli eventi ed i messaggi MIDI in particolare

Come è noto, i messaggi Midi si dividono in:

  • messaggi di Stato, che identificano il numero del Canale ed il tipo di evento Midi trasmesso;
  • messaggi di Dati, che sono i valori relativi ai messaggi di Stato, specificandone caratteristiche modificabili qualitative e quantitative.

Tenendo conto qui delle finalità meramente didattiche del nostro progetto applicativo, considereremo soltanto gli eventi Midi fondamentali direttamente legati alla esecuzione musicale: i Channel Voice Messages.

In tale circostanza l'evento Midi è organizzato da ALSA utilizzando due Strutture ed in base al tipo di Messaggio Midi. La prima Struttura, come abbiamo già visto, è comune a tutti i tipi di Messaggi Midi. La seconda Struttura è diversa a seconda del Messaggio Midi, ricevuto o da inviare.

In particolare per ospitare i dati dei Messaggi:

  • NoteOn;
  • NoteOff;
  • Polyphonic Aftertouch;

verrà utilizzata una Struttura che è dichiarata nel file d'intestazione di Alsa seq_event.h come segue:

typedef struct snd_seq_ev_note {
	unsigned char channel;      ' /**< channel number */
	unsigned char note;         ' /**< note */
	unsigned char velocity;     ' /**< velocity */
	unsigned char off_velocity; ' /**< note-off velocity; only for #SND_SEQ_EVENT_NOTE */
	unsigned int duration;      ' /**< duration until note-off; only for #SND_SEQ_EVENT_NOTE */
} snd_seq_ev_note_t;

Come specificato nel relativo commento, l'ultimo membro della suddetta Struttura è preso in considerazione solo nel caso di uso dell'evento Midi ALSA Event_NOTE.

Invece, per ospitare i dati dei restanti Messaggi:

  • Control Change;
  • Program Change;
  • Channel Aftertouch (Key Pressure);
  • Pitch Bend (Pitch Wheel);

verrà utilizzata una Struttura che è dichiarata nel file d'intestazione di Alsa seq_event.h come segue:

typedef struct snd_seq_ev_ctrl {
	unsigned char channel;     ' /**< channel number */
	unsigned char unused[3];   ' /**< reserved */
	unsigned int param;        ' /**< control parameter */
	signed int value;          ' /**< control value */
} snd_seq_ev_ctrl_t;

Volendo fondere le due precedenti Strutture, otteniamo un'unica organizzazione dei dati i cui membri, che andranno a contenere i dati degli specifici Messaggi Midi, sono i seguenti:

channel un singolo byte
note / unused un singolo byte
velocity / unused un singolo byte [Nota 8]
off_velocity / unused un singolo byte
duration / param un Integer
---- / value un Integer


Ciascun evento Midi utilizza soltanto 2 o 3 (a seconda del messaggio) di questi dati sopra descritti, i quali nella memoria riservata occupano i rispettivi byte come appresso riportato:

Byte:1617181920 21 22 2324 25 26 27
(da 0 a 15 i dati comuni)...┴—┴—┴—┴—┴—————┴—————
Dati:channelnotevelocity|off_velocityduration/paramvalue

In ALSA il tipo di evento Midi viene distinto mediante una definizione identificativa, rappresentata da una enumerazione, e da assegnare al membro "type " della struttura sopra descritta dell'evento Midi:

Private Enum SND_SEQ_EVENT_NOTEON = 6, SND_SEQ_EVENT_NOTEOFF, SND_SEQ_EVENT_KEYPRESS, 
             SND_SEQ_EVENT_CONTROLLER = 10, SND_SEQ_EVENT_PGMCHANGE, SND_SEQ_EVENT_CHANPRESS, SND_SEQ_EVENT_PITCHBEND

Per vedere tramite codice Gambas il numero identificativo costante di alcuni più importanti eventi Midi, come gestiti ALSA, potremo adottare un breve applicativo, composto oltre che dal progetto principale Gambas, anche da una libreria condivisa .so, da noi appositamente scritta all'interno del medesimo progetto Gambas.

Library "/tmp/libalsa"

' void Eventi_ALSA()
' Mostra i valori delle Costanti identificatrici degli eventi ALSA.
Private Extern Eventi_ALSA()


Public Sub Main()
 
 Creaso()
 
 Eventi_ALSA()
 
End


Private Procedure Creaso()
 
 File.Save("/tmp/libalsa.c", "#include <stdio.h>\n#include <alsa/seq_event.h>\n\n" &
           "void Eventi_ALSA() {\n\n" &
           "   printf(\"\e[1m      E V E N T I          Id\e[0m\\n\\n\");\n" &
           "   printf(\"SND_SEQ_EVENT_NOTEON =      %d\\n\", SND_SEQ_EVENT_NOTEON);\n" &
           "   printf(\"SND_SEQ_EVENT_NOTEOFF =     %d\\n\", SND_SEQ_EVENT_NOTEOFF);\n" &  
           "   printf(\"SND_SEQ_EVENT_KEYPRESS =    %d\\n\", SND_SEQ_EVENT_KEYPRESS);\n" & 
           "   printf(\"SND_SEQ_EVENT_CONTROLLER = %d\\n\", SND_SEQ_EVENT_CONTROLLER);\n" & 
           "   printf(\"SND_SEQ_EVENT_PGMCHANGE =  %d\\n\", SND_SEQ_EVENT_PGMCHANGE);\n" & 
           "   printf(\"SND_SEQ_EVENT_CHANPRESS =  %d\\n\", SND_SEQ_EVENT_CHANPRESS);\n" &
           "   printf(\"SND_SEQ_EVENT_PITCHBEND =  %d\\n\", SND_SEQ_EVENT_PITCHBEND);\n" &             
           "   printf(\"SND_SEQ_EVENT_START =      %d\\n\", SND_SEQ_EVENT_START);\n" &             
           "   printf(\"SND_SEQ_EVENT_CONTINUE =   %d\\n\", SND_SEQ_EVENT_CONTINUE);\n" &             
           "   printf(\"SND_SEQ_EVENT_STOP =       %d\\n\", SND_SEQ_EVENT_STOP);\n" &
           "   printf(\"SND_SEQ_EVENT_TEMPO =      %d\\n\", SND_SEQ_EVENT_TEMPO);\n" &
           "   printf(\"SND_SEQ_EVENT_SENSING =    %d\\n\", SND_SEQ_EVENT_SENSING);\n" &
           "   printf(\"SND_SEQ_EVENT_ECHO =       %d\\n\", SND_SEQ_EVENT_ECHO);\n\n}")
   
 Shell "gcc -o /tmp/libalsa.so /tmp/libalsa.c -shared -lasound -fPIC" Wait
 
End

Nel nostro progetto iniziale inseriremo per ciascun evento Midi un button per attivare dalla classe principale del nostro progetto ogni evento mediante una specifica routine da scrivere invece all'interno della classe secondaria (CAlsa.class).

Dimensione finale della zona di memoria riservata

La imprescindibile zona di memoria riservata sarà dunque impegnata dalle due parti:

  • la prima, quella dei dati comuni a tutti gli eventi Midi (16 byte complessivamente: dal byte n° 0 al byte n° 15);
  • la seconda, quella dei dati specifici del particolare singolo evento Midi (12 byte complessivamente: dal byte n° 16 al byte n° 27).

Come sappiamo, i singoli specifici eventi Midi utilizzeranno solo alcuni settori, dunque solo alcuni byte, della seconda parte della memoria allocata.

Accodare gli eventi in un buffer "intermedio"

Al termine della scrittura nella zona di memoria riservata di tutti i dati necessari per definire l'evento Midi desiderato, sarà inserita la funzione di ALSA, espressa in C:

int snd_seq_event_output(snd_seq_t * seq, snd_seq_event_t * ev)

che pone in uscita un evento e più precisamente accoda un evento in un buffer intermedio.
Tale funzione ritorna il numero (integer) di eventi rimanenti da far uscire, oppure un codice di errore se negativo.
Detta funzione di ALSA dovrà essere dichiarata con Extern, in questo modo:

Private Extern snd_seq_event_output(handle As Pointer, ev As Pointer) As Integer

e nella routine sarà invocata così:

err = snd_seq_event_output(handle, ev)        ' output an event

printerr("Evento Midi = ", err)


Passare ad ALSA i valori presenti nella memoria allocata

Nel nostro progetto esemplificativo iniziale al termine di ciascuna routine, finalizzata all'invio dell'evento Midi desiderato, viene prevista e richiamata la subroutine "flush( )", presente nella classe secondaria CAlsa.class, la quale contiene la funzione di ALSA, espressa in C:

int snd_seq_drain_output(snd_seq_t * seq)

che sversa il contenuto della zona di memoria allocata al sequencer ALSA. Tale funzione ritorna un integer uguale a zero, se tutti gli eventi sono stati inviati al sequencer, e quindi se il buffer intermedio è stato svuotato. Ritorna un valore negativo ritorna un errore.
Più in particolare, questa funzione viene invocata dopo ogni evento; e questo assicura che l'evento venga processato in tempo, svuotando il buffer intermedio precedentemente riempito di dati dalla funzione: "snd_seq_event_output " [Nota 9] descritta nel precedente paragrafo.

Detta funzione "snd_seq_drain_output " dovrà essere dichiarata con Extern, in questo modo:

Private Extern snd_seq_drain_output(handle As Pointer) As Integer

Verrà scritta così in un'apposita routine:

Public Sub flush()
 
 Dim err As Integer
 
 err = snd_seq_drain_output(handle)    ' drain output buffer to sequencer
 Printerr("Flush", err)
 
End


Note

[1] Si suggerisce di dare un'occhiata al capitolo Interfacciare Gambas con librerie esterne, redatta da D. Blengino, presente nell'area "Guide della Comunità", ove sarà possibile avere le informazioni basilari necessarie per la gestione delle API di ALSA. Lì, tra l'altro, sono anche presenti esempi esplicativi della funzione Extern per la gestione di ALSA con Gambas, e l'intera codifica di una classe CAlsa.Class relativa all'esempio di una Drum Machine. Si precisa che la conoscenza della funzione Extern, relativa alle dichiarazioni esterne in Gambas, nonché delle API di ALSA (richiamate appunto dalla funzione Extern), è comunque presupposto necessario ed imprescindibile per la gestione in Gambas dei dati Midi con ALSA.

[2] Una libreria, come lo stesso nome indica, contiene una raccolta di subroutine da utilizzare più volte da più programmi.

[3] Ci sono due metodi per gestire con ALSA i dati Midi: il metodo definito "rawmidi" ed il metodo "sequencer". Il metodo rawmidi, che ha a che fare con i byte Midi essenziali, è ad un livello inferiore e semplice, mentre il metodo sequencer opera a livello più alto e complesso. In questa guida ci occuperemo della gestione dei dati Midi mediante il metodo sequencer.

[4] Una dichiarazione Extern fa riferimento all'ultima istruzione LIBRARY incontrata. Se l'ultima Library non è quella che contiene la funzione successiva alla libreria medesima, detta funzione non potrà essere richiamata dal programma. Pertanto, se si devono usare più librerie, queste o si dichiarano di volta in volta prima della funzione di riferimento (ma sempre all'esterno della routine contenente detta funzione), oppure si specificano all'interno di ogni loro dichiarazione.

[5] Sull'uso di Extern vedere: Extern: richiamare funzioni esterne a Gambas.

[6] Un Handle (maniglia) rappresenta "una variabile associata a un oggetto complesso, che lo identifica". Possiamo dire che in modo figurato questa maniglia è la protuberanza con la quale si interagisce con l'oggetto medesimo. In gambas, per esempio, qualsiasi oggetto (form, o altro) è un handle, e per creare un oggetto/handle in Gambas, come è noto, bisogna scrivere "NEW oggetto_da_creare", in ALSA invece la funzione per creare l'handle è, come sappiamo: variab_int = snd_seq_etc_etc(xxx, ......) as integer.

[6 bis] Sul rapporto Client/Server in ALSA: https://www.alsa-project.org/alsa-doc/alsa-lib/seq.html

[7] Vedere: https://www.alsa-project.org/alsa-doc/alsa-lib/structsnd__seq__event__t.html

[7 bis] Vedi anche D. Blengino: Definizione degli eventi Midi in ALSA.

[7 ter] Vedere: https://www.alsa-project.org/alsa-doc/alsa-lib/unionsnd__seq__timestamp__t.html

[8] Questo tipo di dato può essere definito anche Short, qualora non si intenda utilizzare l'evento ALSA: SND_SEQ_EVENT_NOTE. Si dovrà ovviamente eliminare il successivo dato Off_Velocity, poiché il tipo Short occupa 2 byte.

[9] Distinzione fra la funzione err = snd_seq_event_output(handle, ev) ed err = snd_seq_drain_output(handle):

  • err = snd_seq_event_output(handle, ev) --> accoda gli eventi in un buffer "intermedio". Quando tale buffer è pieno, viene svuotato automaticamente nel buffer-ALSA. Lo svuotamento di tale buffer intermedio può essere indotto anche se esso non è pieno.
  • err = snd_seq_drain_output(handle) --> svuota "a richiesta" il buffer intermedio (anche quindi se il buffer intermedio non è pieno).


Riferimenti