ALSA e Gambas - Subsistema Seq: introduzione
Indice
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 spiegandone possibilmente ogni passaggio fondamentale.
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 ": [Nota 1]
Library "libasound:2.0.0"
Tale libreria è propriamente un'interfaccia ai drivers di ALSA [Nota 2].
Pertanto, un nostro ipotetico codice - relativo allla gestione in Gambas delle risorse esterne di ALSA - 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", una "Istanza" della Classe "CAlsa": alsa = New CAlsa as "alsa" ' Invoca la prima procedura della Classe "CAlsa": alsa.alsa_open("Progetto sequencer in Gambas") End
Nella Classe, che chiameremo ad esempio CAlsa.class, progettata per gestire le funzioni esterne di ALSA, avremo:
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 7], 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 8]
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 | ||
rReal-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 snd_seq_event_t degli Eventi Midi: [nota 9]:
- 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, ossia in formato "Orario" e più speficamente il primo in secondi e l'altro in nanosecondi; 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 Midi. E' formato da due membri: il primo l'identificativo numerico del Client sorgente dei dati, il secondo il numero della sua porta;
- dest contiene l'indirizzo del destinatario dell'Evento Midi. E' formato da due membri: il primo l'identificativo numerico del Client destinatario, il secondo il numero della sua porta.
Complessivamente questi primi dati generali, qui sopra considerati, occupano ben 16 byte della memoria allocata:
Byte: | 0 | 1 | 2 | 3 | 4 5 6 7 | 8 9 10 11 | 12 | 13 | 14 | 15 | |||||||||
┴— | ┴— | ┴— | ┴— | ┴——— | ┴———— | ┴— | ┴— | ┴— | ┴— | ||||||||||
Dati: | Type | │ | Flags | │ | Tag | │ | Queue | │ | Tick_time / Real_time Tv_sec | │ | 0 / Real_time Tv_nsec | │ | Source_client | │ | Port_client | │ | Dest_client | │ | Ddest_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 10]
- tick_time, corrisponde ai tick del Midi.
- real_time, corrisponde al formato orario, e la sua 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 | ||
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: | 16 | 17 | 18 | 19 | 20 21 22 23 | 24 25 26 27 | ||||||
(da 0 a 15 i dati comuni)... | ┴— | ┴— | ┴— | ┴— | ┴————— | ┴————— | ||||||
Dati: | channel | │ | note | │ | velocity | | | off_velocity | │ | duration/param | │ | value |
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 (un po' più generica) (16 byte complessivamente: dal byte n° 0 al byte n° 15);
- la seconda, quella dei valori specifici del particolare singolo Evento Midi (12 byte complessivamente: dal byte n° 16 al byte n° 27);
per un tottale di 28 byte.
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_buffer(snd_seq_t * seq, snd_seq_event_t * ev)
che pone in uscita un Evento Midi e più precisamente accoda un Evento Midi in un buffer intermedio.
Tale funzione ritorna il numero (integer) di Eventi Midi rimanenti da far uscire, oppure un codice di errore se negativo.
Passare ad ALSA i valori presenti nella memoria allocata
Al termine di ciascuna routine, finalizzata all'invio di ciascun Evento Midi, come qui sopra appena descritto, deve essere richiamata 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 (il predetto buffer intermedio) al subsitema "seq" di 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.
Come già accennato, questa funzione va invocata dopo ogni Evento Midi: ciò assicura che l'Evento venga processato in tempo, svuotando il buffer intermedio precedentemente riempito di dati dalla funzione: "snd_seq_event_output()" [nota 11] sopra descritta.
Note
[1] Si suggerisce di dare un'occhiata alla pagina: Extern: richiamare funzioni esterne a Gambas
Si precisa che la conoscenza dell'istruzione "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.
[7] Sul rapporto Client/Server in ALSA: https://www.alsa-project.org/alsa-doc/alsa-lib/seq.html
[8] Vedere: https://www.alsa-project.org/alsa-doc/alsa-lib/structsnd__seq__event__t.html
[9] Vedi anche D. Blengino: Definizione degli eventi Midi in ALSA.
[10] Vedere: https://www.alsa-project.org/alsa-doc/alsa-lib/unionsnd__seq__timestamp__t.html
[11] 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).