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. Si tenga presente che quanto scritto di seguito è riferito a Gambas 3 (e non più alla versione 2).

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 (FMain.class) scriveremo innanzitutto le informazioni per creare la classe secondaria CAlsa.class . Scriveremo la funzione che chiama la routine analoga in CAlsa.class, e 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 |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 in maniera adeguata: |2|

Library "libasound:2"

Tale libreria è propriamente un'interfaccia ai drivers di ALSA |3|.


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()
  alsa = NEW CAlsa as "alsa"
  alsa.alsa_open("Progetto sequencer in Gambas-3")
End


In CAlsa.class:

Library "libasound:2"

Richiamare le funzioni di ALSA

Tutte le funzioni di ALSA |3 bis|, 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" |4| |4 bis| |5|.

Le funzioni proprie di ALSA sono degli handle |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 il codice 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, comunicano tali dati con il dispositivo ALSA (più precisamente con uno dei suoi sub-sistemi; per il Midi: seq). Abbiamo anche detto che, pertanto, il nostro applicativo deve possedere le funzioni di un Client. Una parte di esso svolgerà le funzioni di Client. Per ottenere ciò, è 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, come abbiamo visto, con una particolare dichiarazione contenente il termine "Extern".

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

1) aprire il dispositivo ALSA mediante specifica 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 i flag, dal terzo al sesto 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 Stream o Strutture oppure una Classe specifica.

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 campi 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:

Type un singolo byte
Flags un singolo byte
Tag un singolo byte
Queue un singolo byte
Time, composto da:
Tick o Tv_sec un integer
Tv_nsec un integer
Source, composto da:
Id_client un singolo byte
Porta_client un singolo byte
Dest, composto da:
Client_dest un singolo byte
Porta_dest un singolo byte

Da considerare che i campi Time, Source e Dest sono dati strutturati, cioé contengono al loro interno altri campi. Per questi dati il campo è 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 il campo |7|:

  • 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 campi 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 campo non serve e può essere posto a zero;
  • Source contiene l'indirizzo del sorgente dell'evento. E' formato da due campi: il primo l'identificativo numerico, il secondo il numero della porta;
  • Dest contiene l'indirizzo del destinatario dell'evento. E' formato da due campi: 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:TypeFlagsTagQueueTimestamp Real / TickTimestamp Tv_nsec / 0Id_clientPorta_clientClient_destPorta_dest


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:

  • real time, corrisponde all'orologio, e la risoluzione è in nanosecondi;
  • song ticks, corrisponde ai tick del Midi.

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.
I dati attinenti agli specifici messaggi Midi sono i seguenti:

Canale un singolo byte
Nota un singolo byte
Velocity un singolo byte |8|
(Off_Velocity) |9| un singolo byte
1° Valore |10| un Integer
2° Valore 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:CanaleNotaVelocity-(Off_Velocity)1° Valore2° Valore

In ALSA il tipo di evento Midi viene distinto mediante una definizione identificativa, rappresentata da una costante numerica, che dovremo scrivere come costanti all'interno di CAlsa.class, ed assegnare al campo "type" della struttura sopra descritta dell'evento Midi:

(Const SND_SEQ_EVENT_NOTE As Byte = 5)
Const SND_SEQ_EVENT_NOTEON As Byte = 6
Const SND_SEQ_EVENT_NOTEOFF As Byte = 7
Const SND_SEQ_EVENT_KEYPRESS As Byte = 8
Const SND_SEQ_EVENT_CONTROLLER As Byte = 10
Const SND_SEQ_EVENT_PGMCHANGE As Byte = 11
Const SND_SEQ_EVENT_CHANPRESS As Byte = 12
Const SND_SEQ_EVENT_PITCHBEND As Byte = 13


Nel nostro progetto 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. Più precisamente questa funzione 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

Al termine di ciascuna routine per l'invio dell'evento Midi desiderato viene 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. 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 |11|, precedentemente descritta.

Detta funzione di ALSA 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] Vedi D. Blengino: Libreria da usare.

[3] "Una libreria, come lo stesso nome indica, contiene una raccolta di subroutine da utilizzare più volte da più programmi. ". Vedi D. Blengino: ibidem.

[3 bis] 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] Vedi D. Blengino: La Dichiarazione Esterna.

[4 bis] 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 vedi anche: 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] Vedi anche D. Blengino: Definizione degli eventi Midi in ALSA.

[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] Questo dato è utilizzato solo con l'evento ALSA: SND_SEQ_EVENT_NOTE.

[10] Questo dato è definito "Duration" nel caso di utilizzo dell'evento ALSA: SND_SEQ_EVENT_NOTE.

[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).


Riferimenti

[1] Midi Sequencer

[2] The C library reference: Sequencer interface

[3] Sequencer Interface - main file