ALSA e Gambas - Inviarsi una Eco~ nel futuro

Da Gambas-it.org - Wikipedia.

Nella progettazione del nostro sequencer può risultare utile verificare il punto di esecuzione dell'applicativo, come ad esempio conoscere quando il programma sta eseguendo degli eventi Midi. Per sapere ciò che in quel momento sta facendo il nostro client-sequencer, dobbiamo inviare un particolare tipo di evento: l'Eco.
Il sequencer riceve l'Evento Eco anch'esso temporizzato, e ce lo restituirà nel momento stabilito dal proprio timestamp. Quando il programma ci ritorna l'Eco, che noi abbiamo precedentemente inviato, siamo in grado di individuare il punto del processo in cui si trova il sequencer.

L'invio dell'Evento Eco può essere una soluzione per poter inviare ad Alsa i dati Midi in maniera costante, ricorrente e nel momento opportuno. Infatti, Alsa non è in grado di gestire una quantità elevata di dati: non è possibile inviarle molte centinaia di dati relativi agli eventi Midi ed ai relativi Tempi Delta contenuti in un file Midi. Pertanto, se ne dovrà inviare una quantità adeguata, e comunque tale che Alsa non ne rimanga mai priva, fintanto che l'esecuzione del file Midi non è terminata. Se avvenisse l'interruzione del flusso di dati, vi sarebbe irrimediabilmente una spiacevole interruzione dell'esecuzione del brano. Allora, il sequencer dovrà "sapere" il momento opportuno per inviare ad Alsa un altro gruppo di dati, perché siano gestiti. L'importante, per la corretta e coerente esecuzione del brano Midi, è non causare interruzioni nel flusso di dati Midi da inviare ad Alsa. Riportando quanto sottolineato da Clemens Ladisch all'interno del codice sorgente del suo programma Aplaymidi, esistono tre possibilità su come far attendere il sequencer sino al momento del successivo invio di un altro gruppo di dati Midi ad Alsa. Esse sono:

  • 1) inviare ad Alsa un evento Eco, ed attendere che esso torni al sequencer;
  • 2) attendere un avviso di EVENT_STOP per la coda degli eventi che viene inviato dalla porta Timer di sistema (questa richiederebbe una sottoscrizione);
  • 3) attendere che il pool di uscita sia vuoto (in questo caso si utilizzerebbe la funzione: snd_seq_sync_output_queue(snd_seq_t *seq, size_t size), la quale attende sino a che tutti gli eventi sono stati processati.

Fra le tre possibilità qui mostreremo la prima, quella dell'invio di un evento "Eco".


Scrittura in Gambas

Poiché l'Eco è un evento come qualsiasi altro, e tale è definito e riconosciuto da ALSA, in Gambas lo invieremo utilizzando le medesime modalità che noi conosciamo per inviare un qualsiasi tipo di evento. Avremo solo l'accortezza di indicare il sequencer stesso come destinatario dell'evento Eco.

Dunque, il codice in astratto così:

Private Const SND_SEQ_EVENT_ECHO As Integer = 50    ' Questa costante di ALSA individua il "tipo" di Evento "Echo"

 ......

 Dim ev_Midi As snd_seq_event_t
 
 With ev_midi
   .type = SND_SEQ_EVENT_ECHO
   .flags = SND_SEQ_TIME_STAMP_TICK
   .tag = 0
   .queue = coda
   .tick_o_tv_sec = timestamp_in_tick_midi
   .tv_nsec = 0
   .source_client = id
   .source.port = s_port
   .dest_client = id      ' Invia l'Evento Midi "Echo" a se stesso
   .dest_port = s_port    ' Invia l'Evento Midi "Echo" alla sua porta quale Client di ALSA
   .channel = 0
   .note = 0
   .velocity = 0
   .off_velocity = 0
   .param = 0
   .value = 0
 End With

End

L'evento Eco verrà, poi, intercettato come qualsiasi altro Evento Midi ALSA con le funzioni che già conosciamo per ricevere gli Eventi Midi.


Esempio pratico

Mostriamo di seguito un esempio pratico in ambiente grafico, nel quale verranno inviati in sequenza vari Eventi Midi, temporizzati in modalità tick time Midi.
Alla fine della Coda di tali Eventi Midi è posto un Evento Midi di tipo "Echo" che farà sollevare un Evento, il quale determinerà la ripetizione dell'invio di tutti i suddetti Eventi Midi, e così via fino a quando non si cliccherà nuovamente sul ToggleButton, posto sul Form, per arrestare l'esecuzione della Coda degli Eventi Midi.

La Classe principale avrà il seguente codice:

Private strum As String[] = ["Acustic Grand Piano", "Bright Acustic Piano", "Electric Grand Piano", "Honky-tonk",
 "Electric Piano 1", "Electric Piano 2", "Harpsichord", "Clavinet", "Celesta", "Glockenspiel", "Music Box", "Vibraphone",
 "Marimba", "Xylophone", "Tubular Bells", "Dulcimer", "Hammond Organ", "Percussive Organ", "Rock Organ", "Church Organ",
 "Reed Organ", "Accordion", "Harmonica", "Tango Accordion", "Acoustic Guitar (nylon)", "Acoustic Guitar (steel)",
 "Electric Guitar (jazz)", "Electric Guitar (clean)", "Electric Guitar(muted)", "Overdriven Guitar", "Distortion Guitar",
 "Guitar Harmonics", "Acoustic Bass", "Electric Bass (finger)", "Electric Bass (pick)", "Fretless Bass", "Slap Bass 1",
 "Slap Bass 2", "Synth Bass 1", "Synth Bass 2", "Violin", "Viola", "Cello", "Contrabass", "Tremolo Strings",
 "Pizzicato Strings", "Orchestral Harp", "Timpani", "String Ensemble 1", "String Ensemble 2", "SynthStrings 1",
 "SynthStrings 2", "Choir Aahs", "Voice Oohs", "Synth Voice", "Orchestra Hit", "Trumpet", "Trombone", "Tuba", "Muted Trumpet",
 "French Horn", "Brass Section", "Synth Brass 1", "Synth Brass 2", "Soprano Sax", "Alto Sax", "Tenor Sax", "Baritone Sax",
 "Oboe", "English Horn", "Basson", "Clarinet", "Piccolo", "Flute", "Recorder", "Pan Flute", "Bottle Blow", "Shakuhachi",
 "Whistle", "Ocarina", "Lead 1 (square)", "Lead 2 (sawtooth)", "Lead 3 (caliope lead)", "Lead 4 (chiff lead)",
 "Lead 5 (charang)", "Lead 6 (voice)", "Lead 7 (fifths)", "Lead 8(brass+lead)", "Pad 1 (new age)", "Pad 2 (warm)",
 "Pad 3 (polysynth)", "Pad 4 (choir)", "Pad 5 (bowed)", "Pad 6 (metallic)", "Pad 7 (halo)", "Pad 8 (sweep)", "FX 1 (rain)",
 "FX 2 (soundtrack)", "FX 3 (crystal)", "FX 4 (atmosphere)", "FX 5 (brightness)", "FX 6 (goblins)", "FX 7 (echoes)",
 "FX 8 (sci-fi)", "Sitar", "Banjo", "Shamisen", "Koto", "Kalimba", "Bagpipe", "Fiddle", "Shanai", "Tinkle Bell", "Agogo",
 "Steel Drums", "Woodblock", "Taiko Drum", "Melodic Tom", "Synth Drum", "Reverse Cymbal", "Guitar Fret Noise", "Breath Noise",
 "Seashore", "Bird Tweet", "Telephone Ring", "Helicopter", "Applause", "Gunshot"]

Private Const id_dev As Integer = 128             ' 14 = midi out oppure  128 (solitamente) = softsynth
Private Const p_dev As Integer = 0                ' Porta del Client destinatario dei dati Midi: solitamente 0
Public alsa As CAlsa                              ' Classe che incapsula le funzioni ALSA


Public Sub Form_Open()

' Crea ("istanzia") un Oggetto della Classe "CAlsa" per poterla usare:
 With alsa = New CAlsa As "Alsa"
' Apre ALSA e assegna un nome al Client:
   Me.Title = "|| Client ALSA " & .AlsaIni("Programma di prova")
' Sceglie la periferica (Softsynth) su cui suonare:
   .ImpostaDispositivo(id_dev, p_dev)
 End With

End


Public Sub ToggleButton1_Click()

 If ToggleButton1.Value Then
   alsa.Descrittori()
   InviaEventiMidi()
 Else
   With alsa
' Invia un messaggio "Control Change" di "All notes off":
     .ControlChange(0, 0, 0, 123, 0)
     .Flush()
     .StopCoda()
   End With
   Me.Close
 Endif

End


Private Procedure InviaEventiMidi()

 With alsa
' Avvia la "Coda" degli Eventi Midi ALSA:
   .AvvioCoda()
   .ControlChange(0, 0, 0, 7, 50)
   .ProgramChange(0, 0, 0, 0, strum.Find("Violin"))
   .NoteON(0, 0, 0, 64, 100)
   .NoteOFF(0, 96, 0, 64, 0)

   .ProgramChange(0, 130, 0, 0, strum.Find("Acoustic Guitar (nylon)"))
   .NoteON(0, 130, 0, 66, 100)
   .NoteOFF(0, 182, 0, 66, 0)

   .ProgramChange(0, 182, 0, 0, strum.Find("Flute"))
   .NoteON(0, 182, 0, 68, 100)
   .NoteOFF(0, 255, 0, 68, 0)
' Invia l'Evento Midi Echo:
   .Echo(0, 255)
' Dispone infine l'invio di tutti gli Eventi Midi bufferizzati nella "Coda":
   .Flush()
 End With

End


Public Sub Alsa_EventoEcho()

 InviaEventiMidi()

End

La Classe secondaria, che chiameremo "CAlsa", sarà la seguente:

Private handle As Pointer
Private id As Integer
Private s_port As Integer
Private que As Integer
Private dclient As Byte
Private dport As Byte
Private in_port As Integer
Private p As Pointer
Private st As Stream
Private fl As File
Private Const POLLIN As Short = 1
Event EventoEcho


Library "libasound:2.0.0"

Public Struct snd_seq_event_t
  type As Byte
  flags As Byte
  tag As Byte
  queue As Byte
  tick_o_tv_sec As Integer
  tv_nsec As Integer
  source_client As Byte
  source_port As Byte
  dest_client As Byte
  dest_port As Byte
  channel As Byte
  note As Byte
  velocity As Byte
  off_velocity As Byte
  param As Integer
  value As Integer
End Struct

Private Const SND_SEQ_PORT_CAP_READ As Integer = 1
Private Const SND_SEQ_PORT_CAP_WRITE As Integer = 2
Private Const SND_SEQ_OPEN_DUPLEX As Integer = 3
Private Const SND_SEQ_PORT_TYPE_MIDI_GENERIC As Integer = 2
Private Const SND_SEQ_PORT_TYPE_APPLICATION As Integer = 1048576
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, SND_SEQ_EVENT_START = 30, SND_SEQ_EVENT_STOP = 32, SND_SEQ_EVENT_ECHO = 50

' int snd_seq_open (snd_seq_t **seqp, const char * name, int streams, int mode)
' Open the ALSA sequencer.
Private Extern snd_seq_open(seqp As Pointer, name As String, streams As Integer, mode As Integer) As Integer

' int snd_seq_set_client_name (snd_seq_t* seq, const char* name)
' Set client name.
Private Extern snd_seq_set_client_name(seq As Pointer, name As String) As Integer

' int snd_seq_client_id (snd_seq_t * seq)
' Get the client id.
Private Extern snd_seq_client_id(seq As Pointer) As Integer

' int snd_seq_create_simple_port (snd_seq_t* seq, const char* name, unsigned int caps, unsigned int type)
' Creates a port with the given capability and type bits.
Private Extern snd_seq_create_simple_port(seq As Pointer, name As String, caps As Integer, type As Integer) As Integer

' int snd_seq_alloc_queue (snd_seq_t * seq)
' Allocate a queue.
Private Extern snd_seq_alloc_queue(seq As Pointer) As Integer

' int snd_seq_control_queue(snd_seq_t * seq, int q, int type, int value, snd_seq_event_t * ev)
' Queue controls - start/stop/continue.
Private Extern snd_seq_control_queue(seq As Pointer, q As Integer, type As Integer, value As Integer, ev As Pointer) As Integer

' int snd_seq_poll_descriptors_count (snd_seq_t * seq, short events)
' Returns the number of poll descriptors.
Private Extern snd_seq_poll_descriptors_count(seq As Pointer, events As Short) As Integer

' int snd_seq_poll_descriptors (snd_seq_t * seq, struct pollfd * pfds, unsigned int space, short events)
' Get poll descriptors.
Private Extern snd_seq_poll_descriptors(seq As Pointer, pfds As Pointer, space As Integer, events As Short) As Integer

' int snd_seq_event_input (snd_seq_t * seq, snd_seq_event_t ** ev)
' Retrieve an event from sequencer.
Private Extern snd_seq_event_input(seq As Pointer, ev As Pointer) As Integer

' int snd_seq_event_output_buffer (snd_seq_t * seq, snd_seq_event_t *ev)
' Output an event.
Private Extern snd_seq_event_output_buffer(seq As Pointer, ev As Snd_seq_event_t) As Integer

' int snd_seq_drain_output (snd_seq_t * seq)
' Drain output buffer to sequencer.
Private Extern snd_seq_drain_output(seq As Pointer) As Integer

' const char * snd_strerror (int errnum)
' Returns the message for an error code.
Private Extern snd_strerror(errnum As Integer) As String

' int snd_seq_close (snd_seq_t * seq)
' Close the sequencer.
Private Extern snd_seq_close(seq As Pointer) As Integer


Public Function AlsaIni(nome As String) As String

 Dim err As Integer

 err = snd_seq_open(VarPtr(handle), "default", SND_SEQ_OPEN_DUPLEX, 0)
 Print "Apertura del subsistema 'seq' di ALSA = "; IIf(err == 0, "corretta !", "errata !")
 If err < 0 Then error.RAISE("Errore: " & snd_strerror(err))

 snd_seq_set_client_name(handle, nome)
 id = snd_seq_client_id(handle)
 Print "Alsa ClientID = "; id

' Crea la Porta di Uscita dei dati Midi del nostro Client ALSA:
 s_port = snd_seq_create_simple_port(handle, "Uscita dati", SND_SEQ_PORT_CAP_READ, SND_SEQ_PORT_TYPE_MIDI_GENERIC + SND_SEQ_PORT_TYPE_APPLICATION) 
 Print "Porta del programma = "; s_port
 If s_port < 0 Then error.Raise("Errore: " & snd_strerror(s_port))

' Crea la Porta di Entrata dei dati Midi del nostro Client ALSA:
 in_port = snd_seq_create_simple_port(handle, "Entrata dati", SND_SEQ_PORT_CAP_WRITE, SND_SEQ_PORT_TYPE_MIDI_GENERIC + SND_SEQ_PORT_TYPE_APPLICATION)
 If err < 0 Then error.Raise("Errore: " & snd_strerror(in_port))

 que = snd_seq_alloc_queue(handle)
 If que < 0 Then error.Raise("Errore: " & snd_strerror(que))

 Return CStr(id) & ":" & CStr(s_port)

End


Public Procedure ImpostaDispositivo(client As Integer, port As Integer)

 dclient = client
 dport = port
 
End


Public Procedure AvvioCoda()

 Dim err As Integer

 err = snd_seq_control_queue(handle, que, SND_SEQ_EVENT_START, 0, 0)
 If err < 0 Then Error.Raise("Errore: " & snd_strerror(err))

End


Public Procedure ControlChange(flg As Byte, tick As Integer, canale As Byte, prm As Integer, value As Integer)

 Dim ev_midi As New Snd_seq_event_t
 Dim err As Integer

 With ev_midi
   .type = SND_SEQ_EVENT_CONTROLLER
   .flags = flg
   .tick_o_tv_sec = tick
   .queue = que
   .source_client = id
   .source_port = s_port
   .dest_client = dclient
   .dest_port = dport
   .channel = canale
   .param = prm
   .value = value
 End With

 err = snd_seq_event_output_buffer(handle, ev_midi)
 If err < 0 Then Error.Raise("Errore: " & snd_strerror(err))

End


Public Procedure ProgramChange(flg As Byte, tick As Integer, canale As Byte, non_usato As Byte, value As Integer)

 Dim err As Integer
 Dim ev_midi As New Snd_seq_event_t

 With ev_midi
   .type = SND_SEQ_EVENT_PGMCHANGE
   .flags = flg
   .tick_o_tv_sec = tick
   .queue = que
   .source_client = id
   .source_port = s_port
   .dest_client = dclient
   .dest_port = dport
   .channel = canale
   .value = value
 End With

 err = snd_seq_event_output_buffer(handle, ev_midi)
 If err < 0 Then Error.Raise("Errore: " & snd_strerror(err))

End


Public Procedure NoteON(flg As Byte, tick As Integer, canale As Byte, nota As Byte, vel As Byte)

 Dim ev_midi As New Snd_seq_event_t
 Dim err As Integer

 With ev_midi
   .type = SND_SEQ_EVENT_NOTEON
   .flags = flg
   .tick_o_tv_sec = tick
   .queue = que
   .source_client = id
   .source_port = s_port
   .dest_client = dclient
   .dest_port = dport
   .channel = canale
   .note = nota
   .velocity = vel
 End With

 err = snd_seq_event_output_buffer(handle, ev_midi)
 If err < 0 Then Error.Raise("Errore: " & snd_strerror(err))

End


Public Procedure NoteOFF(flg As Byte, tick As Integer, canale As Byte, nota As Byte, vel As Byte)

 Dim ev_midi As New Snd_seq_event_t
 Dim err As Integer

 With ev_midi
   .type = SND_SEQ_EVENT_NOTEOFF
   .flags = flg
   .tick_o_tv_sec = tick
   .queue = que
   .source_client = id
   .source_port = s_port
   .dest_client = dclient
   .dest_port = dport
   .channel = canale
   .note = nota
   .velocity = vel
 End With

 err = snd_seq_event_output_buffer(handle, ev_midi)
 If err < 0 Then Error.Raise("Errore: " & snd_strerror(err))

End


Public Procedure Echo(flg As Byte, tick As Integer)

 Dim ev_midi As New Snd_seq_event_t
 Dim err As Integer

 With ev_midi
   .type = SND_SEQ_EVENT_ECHO
   .flags = flg
   .tick_o_tv_sec = tick
   .queue = que
   .source_client = id
   .source_port = s_port
   .dest_client = id
   .dest_port = in_port
 End With

 err = snd_seq_event_output_buffer(handle, ev_midi)
 If err < 0 Then Error.Raise("Errore: " & snd_strerror(err))

End


Public Procedure Flush()

 Dim err As Integer

 err = snd_seq_drain_output(handle)
 If err < 0 Then Error.Raise("Errore: " & snd_strerror(err))

End


Public Procedure StopCoda()

 Dim err As Integer
 
 err = snd_seq_control_queue(handle, que, SND_SEQ_EVENT_STOP, 0, 0)
 If err < 0 Then Error.Raise("Errore: " & snd_strerror(err))

 st.Close
 fl.Close
 Free(p)    ' Dealloca la memoria precedentemente riservata
 p = 0      ' Assicura che comunque il Puntatore non punti ad un indirizzo di memoria rilevante
 snd_seq_close(handle)

End


Public Procedure Descrittori()

 Dim pdc, i, fd As Integer

 pdc = snd_seq_poll_descriptors_count(handle, POLLIN)
 p = Alloc(SizeOf(gb.Pointer), pdc)

 snd_seq_poll_descriptors(handle, p, pdc, POLLIN)
 st = Memory p For Read
 For i = 0 To pdc - 1
   Seek #st, i * SizeOf(gb.Pointer)
   Read #st, fd
   fl = Open "." & CStr(fd) For Read Watch
 Next

End


Public Sub File_Read()

 Dim ev As Pointer

' Funzione di ALSA che restiruisce i veri dati utili degli "Eventi Midi":
 snd_seq_event_input(handle, VarPtr(ev))

 If Byte@(ev) == SND_SEQ_EVENT_ECHO Then Raise EventoEcho

End