Alsa e Gambas: Ricezione dei dati Midi con l'uso dei File Descriptor

Da Gambas-it.org - Wikipedia.

Introduzione

L'uso dei File Descriptors [nota 1] è da considerarsi - allo stato attuale - il metodo più opportuno e corretto fra quelli individuati e praticabili per la ricezione dei messaggi Midi. E' inoltre pienamente coerente con molti programmi (ad esempio: aseqdump, aplaymidi, arecordmidi, etc.), scritti in C, che infatti procedono al controllo dei file descriptors per vedere se c'è qualche dato Midi da leggere; dato che viene restituito poi da una specifica funzione di ALSA.

Procederemo quindi alla realizzazione del nostro applicativo in Gambas seguendo, per quanto compatibile, l'organizzazione del codice dei predetti programmi in C. Tale codifica nella sua parte che ci interessa si sviluppa in sintesi con le seguenti funzioni di ALSA (che noi dovremo poi nel nostro applicativo ovviamente richiamare e gestire):

  • npfds = snd_seq_poll_descriptors_count(seq, POLLIN): con questa funzione viene chiesto ad ALSA quanti descrittori vi sono per essere letti. La variabile npfds è un Integer; mentre POLLIN è una costante integer di ALSA uguale ad 1 (il suo valore può essere facilmente individuato lanciando da terminale il comando: grep -r POLLIN /usr/include/* ) ed andrà dichiarata anticipatamente.
  • pfds = alloca(sizeof(*pfds * npfds): con questa riga di codice viene allocata sufficiente memoria per contenere i descrittori. Essa è pari al singolo file descriptor moltiplicato per il numero dei file descriptor restituiti dalla precedente funzione di ALSA. La variabile pfds è un puntatore che nei codici in C di quei programmi è dichiarato come puntatore ad una struct pollfd; è insomma un Puntatore ad un insieme di descrittori [nota 2]. E' dunque necessario verificare, ai fini della stesura del codice del nostro applicativo in Gambas, quanti byte sono occupati da questa struttura, e quindi indispensabili per noi poi da riservare.


La struct pollfd è così descritta dal comando man poll:

struct pollfd {
  int fd; /* file descriptor */
  short events; /* requested events */
  short revents; /* returned events */
}

Da tale descrizione possiamo vedere che la struttura è costituita da tre valori: un Integer e due Short, per una somma complessiva pari ad: 8 byte occupati. Quindi nel nostro applicativo dovremo riservare una corrispondente area di memoria.

  • snd_seq_poll_descriptors(seq, pfds, npfds, POLLIN): con questa funzione si chiede ad ALSA di riempire la memoria puntata da pfds con i descrittori rilevanti individuati dalla precedente prima funzione. Questa chiamata passa il puntatore alla zona di memoria riservata, ed il numero di descrittori che possono essere scritti nella zone di memoria, affinché ALSA non vada a scrivere fuori di quell'area.

Per ciò che riguarda la scrittura del codice del nostro applicativo in Gambas, si tratta semplicemente di seguire sostanzialmente la medesima procedura [Nota 3]: chiedere ad ALSA quanti descrittori servono, riservare la memoria per essi, e poi chiedere nuovamente ad ALSA di riempire la zona puntata dal Puntatore pfds. Quando la variabile pfds è stata riempita, vi troveremo npfds volte la struttura struct pollfd. Siccome ALSA non sa quanto spazio è stato riservato, è necessario passargli questo puntatore, così ALSA non va a scrivere fuori dello spazio di memoria riservato. In Gambas, poi, con un Puntatore o un Memory Stream potremo ottenere il file descriptor; oppure si potrà dichiarare una propria Struttura, simile a quella struct pollfd, chiedere quindi ad ALSA di riempirla, e poi accedere ai singoli file descriptor con delle linee di codice del tipo seguente:

For i = 0 to npfds -1
  miodescrip = miastruttura[i].fd
  ...etc...

Ottenuti tali descrittori di file, bisognerà inserirli nel message loop di Gambas; quindi procedere nel modo solito con un event-driven. In sostanza sarebbe necessario utilizzare nativamente in Gambas un meccanismo che generi un evento, quando il file descriptor diventa "ready". Ciò non era possibile, ma su nostra sollecitazione Benôit Minisini con la revisione SVN #4160 di Gambas 3 ha messo a punto un "trucco" (come defnito da lui stesso) per consentire di leggere un file descriptor passato dalle funzioni [Nota 4]. La soluzione dopo vari test risulta funzionante.

  • snd_seq_event_input(seq, &event): questa è in fine la funzione esterna di ALSA utilizzata dai programmi, scritti in C, sopra elencati, dalla quale si vanno a leggere i giusti, veri ed utili messaggi Midi.


La scrittura in Gambas: obiettivo e strategia

Ricordiamo che il nostro obiettivo è quello di "ricevere" messaggi Midi. Per fare ciò, si dovranno risolvere tre problemi:

  • avere una porta attraverso la quale far entrare i dati;
  • attivare la connessione dal dispositivo Midi esterno;
  • attivare la procedura capace di intercettare i messaggi Midi che dovranno essere letti esclusivamente dalla funzione di ALSA "snd_seq_event_input(seq_&event()", quando sarà sollevato un evento attraverso i dati provenienti dal file descriptor passato da ALSA.


La strategia attiene invece alla modalità di intercettazione dei messaggi Midi: essa sarà attuata ponendo il file descriptor, passato dalla funzione di ALSA "snd_seq_poll_descriptor()", sotto osservazione per la Lettura (Read) mediante un comando simile a: hfile = OPEN "......" For Read Watch. Normalmente, il programma "dorme", ma quando giungono dati al quel file descriptor, esso diventa "ready ", noi non andiamo a leggere i dati dal file descriptor, cioè dalla variabile hfile, ma il meccanismo OPEN... deve "svegliare" il programma e sollevare un evento Gambas, come "File_Read()". In fine andremo a leggere all'interno di questa seconda routine sollevata i veri e utili dati degli Eventi Midi di ALSA Midi attraverso l'apposita funzione esterna: "snd_seq_event_input()".
Ripetiamo: bisogna ottenere da ALSA il file descriptor giusto. Tale file descriptor non va letto, ossia non vanno raccolti i dati da tale file, ma bisogna utilizzarlo soltanto per ottenere un evento "_Read": quando tale File desciptor passa un valore, significa che c'è "altrove" un Evento Midi di ALSA da leggere.
I dati Midi utili vanno letti da ALSA. L'Evento "File_Read()" deve essere scatenato quando un dato Midi è pronto per essere letto. Questo dato però va letto dall'apposita funzione di ALSA e non dal file descriptor, il quale ci deve esclusivamente e semplicemente dire se ci sono in arrivo dei dati da leggere ("altrove").

Se non il programmatore non comprende l'importanza della distinzione fra questi due passaggi: avviso e lettura dei veri dati (ossia se non si comprende appieno la strategia da seguire), non sarà possibile utilizzare il metodo dell'interrogazione dei file descriptor per ottenere gli Eventi Midi in ricezione.


La scrittura in Gambas: il codice

Considereremo qui i due passaggi sopra accennati: la creazione della porta di Entrata dell'applicativo e le routine per la ricezione dei messaggi Midi attraverso l'uso dei file descriptor.

Creazione della porta con capacità Write

Dovremo effettuare una sottoscrizione della porta del nostro dispositivo con capacità scrivibile; dovremo cioè creare una porta scrivibile da parte del dispositivo Midi esterno, e quindi leggibile da parte del nostro applicativo. Dunque, oltre alla funzione di creazione della porta di uscita del nostro applicativo, si dovrà inserire nel codice anche la funzione esterna per la creazione della porta di entrata dei dati dal dispositivo esterno. Tale funzione di Alsa in C è così dichiarata: int snd_seq_create_simple_port(snd_seq_t *seq, const char *name, unsigned int caps, unsigned int type) , quindi come quella di uscita, e nel nostro codice Gambas sarà così dichiarata:

Private Extern snd_seq_create_simple_port(seq As Pointer, name As String, caps As Integer, type As Integer) As Integer

mentre sarà richiamata all'interno della medesima routine della nostra classe CAlsa.class, ove è presente anche la funzione per la creazione del porta in uscita

 err = snd_seq_create_simple_port(handle, "Seq-Out", SND_SEQ_PORT_CAP_WRITE, SND_SEQ_PORT_TYPE_MIDI_GENERIC + SND_SEQ_PORT_TYPE_APPLICATION)

laddove SND_SEQ_PORT_CAP_WRITE, SND_SEQ_PORT_TYPE_MIDI_GENERIC, SND_SEQ_PORT_TYPE_APPLICATION sono tre costanti:

Private Const SND_SEQ_PORT_CAP_WRITE As Integer = 2
Private Const SND_SEQ_PORT_TYPE_MIDI_GENERIC As Integer = 2
Private Const SND_SEQ_PORT_TYPE_APPLICATION As Integer = 1048576

Connessione con il dispositivo Midi esterno

Ovviamente, creata la porta scrivibile da parte di un dispositivo Midi esterno, si dovrà prevedere la connessione con tale dispositivo Midi.
Utilizzeremo a tal proposito la funzione esterna di Alsa: int snd_seq_connect_from(seq as pointer, myport as integer, src_client as integer, src_port as integer) , che sarà così dichiarata nel nostro codice Gambas:

Private Extern snd_seq_connect_from(seq As Pointer, myport As Integer, src_client As Integer, src_port As Integer) As Integer

e sarà così richiamata in routine (ponendo il caso che il numero identificativo Client del dispositivo Midi esterno sia 24 e la porta 0):

err = snd_seq_connect_from(handle, inport, 24, 0)

Il codice per la ricezione dei messaggi Midi mediante i file descriptor

Come abbiamo avuto modo di esporre ampiamente nei paragrafi precedenti, nella scrittura del codice faremo uso di tre funzioni esterne di ALSA: le prime due ci daranno la possibilità di ricavare il file descriptor da passare al comando OPEN, la terza quella di raccogliere i messaggi Midi a noi utili.

private Const POLLIN As Short = 1
private fd As File

Library "libasound:2"

......
' 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 Pollfd, 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


' Routine di "Avviso": i dati ricevuti dal "file descriptor", posto alla fine, avvisano dell'arrivo dei dati di un Evento Midi di ALSA, i quali però saranno raccolti effettivamente da un'altra funzione esterna di ALSA nell'altra successiva routine:
Public Procedure OsservaColTrucco()

 Dim npfds, i, numfd As Integer
 Dim pfds As Pointer
 Dim st As Stream

 npfds = snd_seq_poll_descriptors_count(handle, POLLIN)

' Alloca la memoria necessaria, tenendo conto del numero di byte occupati dalla "struct pollfd".
 pfds = Alloc(SizeOf(gb.Pointer), npfds)

 snd_seq_poll_descriptors(handle, pfds, npfds, POLLIN)

 st = Memory pfds For Read

 For i = 0 To npfds - 1

' Va comunque a leggere dal 1° byte:
   Seek #st, i * SizeOf(gb.Pointer)
   Read #st, numfd

' Solo per vedere in console quale numero di fd è stato passato da ALSA: [Nota 5]
   Print "numfd = "; numfd

' La seguente riga di comando è quella essenziale con il "trucco" per "osservare" il "file descriptor" passato da ALSA: il programma così resta in attesa di dati provenienti dal file descriptor per poter scatenare la sub-routine 'File_Read()'.
   fd = Open "." & CStr(numfd) For Read Watch

 Next

 Free(pfds)

End


Public Sub File_Read()   ' Routine dove saranno letti e raccolti i "veri" dati relativi agli Eventio Midi di ALSA

 Dim i As Integer
 Dim pEventi as Pointer
 Dim st as Stream
 Dim b as Byte

 Do  [Nota 6]

' Questa è la funzione di ALSA che restituirà i "veri" dati degli Eventi Midi nella variabile pEventi.
   i = snd_seq_event_input(handle, VarPtr(pEventi))

   If i < 0 Then Break

' Prepara, quindi, il Puntatore per estrarvi i dati Midi restituiti dalla precedente funzione di ALSA:
    st = Memory pEventi For Read

' In questo caso va a leggere dal 1° byte per ottenere il "tipo" di Evento Midi secondo il protocollo di ALSA:
    Seek #st, 0
    Read #st, b

' Filtro, basato sul numero del "Type", per eliminare gli Eventi Midi che non ci interessano:
    If b > 14 Then Return

' Quindi le consuete funzioni per leggere gli altri byte, al fine di ottenere i restanti messaggi Midi che ci interessano.
   ............

 Loop
  
End

Esempio pratico

Mostriamo di seguito un esempio pratico, nel quale una tastiera Midi esterna (o altro dispositivo Midi), connessa al sistema interno di ALSA (ID=14:0), invia dei dati Midi, che saranno intercettati dal programma appresso esposto.
Per connettere il dispositivo esterno Midi ad ALSA, lanciare nel Terminale la seguente riga di comando:

~$ aconnect 24:0 14:0

laddove "24" è il numero identificativo del Client-dispositivo esterno (ovviamente si scriverà un altro numero, qualora esso sia diverso da questo).

Private Const POLLIN As Short = 1
Private handle As Pointer
Private fd As File


Library "libasound:2.0.0"

Public Struct snd_seq_event
  type As Byte                ' byte indice 0
  flag As Byte
  tag As Byte 
  queue As Byte 
  tick_o_Tv_sec As Integer
  Tv_nsec As Integer
  source_id As Byte
  source_porta As Byte
  dest_id As Byte
  dest_porta As Byte
    channel As Byte           ' byte indice 16
    note As Byte
    velocity As Byte
    off_velocity As Byte
      param As Integer        ' byte indice 20
      value As Integer        ' byte indice 24
End Struct

Private Const SND_SEQ_OPEN_DUPLEX As Integer = 3
Private Const SND_SEQ_PORT_CAP_WRITE As Integer = 2
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_CONTROLLER = 10, SND_SEQ_EVENT_PGMCHANGE

' int snd_seq_open(snd_seq_t **seqp, const char * name, Int streams, Int mode)
' Open the ALSA sequencer.
Private Extern snd_seq_open(Pseq 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_create_simple_port(snd_seq_t* seq, const char* name, unsigned int caps, unsigned int type)
' Create a port - simple version.
Private Extern snd_seq_create_simple_port(seq As Pointer, name As String, caps As Integer, type As Integer) 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_connect_from(seq as pointer, myport as integer, src_client as integer, src_port as integer)
' Simple subscription (w/o exclusive & time conversion).
Private Extern snd_seq_connect_from(seq As Pointer, myport As Integer, src_client As Integer, src_port As Integer) 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, events As Short)
' 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


Public Sub Main()

 Dim err, id As Integer
 Dim inport As Integer

 err = snd_seq_open(VarPtr(handle), "default", SND_SEQ_OPEN_DUPLEX, 0)
 If err < 0 Then error.RAISE("Errore nell'apertura di ALSA !")

 snd_seq_set_client_name(handle, "Esempio Client")
 id = snd_seq_client_id(handle)
 Print "Alsa Client-ID = "; id

' Per poter leggere i dati ricevuti dalla propria porta, essa viene posta con capacità "Write", ossia "scrivibile" da parte dell'Altro dispositivo Client che invia appunto i dati:
 inport = snd_seq_create_simple_port(handle, "Porta applicativo", SND_SEQ_PORT_CAP_WRITE, SND_SEQ_PORT_TYPE_MIDI_GENERIC + SND_SEQ_PORT_TYPE_APPLICATION)
 Print "Numero della porta input dell'applicazione = "; inport
 If inport < 0 Then error.Raise("Errore nella creazione della porta !")

' Si pongono: 14 (id del sistema audio interno ALSA) e 0 (num. della sua porta) per connettere il nostro Client ad ALSA e "ricevere" dati direttamente da essa:
 err = snd_seq_connect_from(handle, inport, 14, 0)
 If err < 0 Then error.Raise("Error subscribe input device")


' OSSERVA IL FILE-DESCRIPTOR

 Dim npfds, i, numfd As Integer
 Dim pfds As Pointer
 Dim st As Stream

 npfds = snd_seq_poll_descriptors_count(handle, POLLIN)

' Alloca la memoria necessaria, tenendo conto del numero di byte occupati dalla "struct pollfd":
 pfds = Alloc(SizeOf(gb.Pointer), npfds)

 snd_seq_poll_descriptors(handle, pfds, npfds, POLLIN)

 st = Memory pfds For Read

 For i = 0 To npfds - 1
' Va a leggere dal 1° byte:
   Seek #st, i * SizeOf(gb.Pointer)
   Read #st, numfd

' Solo per vedere in console quale numero di fd è stato passato da ALSA:
   Print "numfd = "; numfd

' La seguente riga di comando è quella essenziale con il "trucco" per "osservare" il "file descriptor" passato da ALSA: il programma così resta in attesa di dati provenienti dal file descriptor per poter scatenare la sub-routine "File_Read()":
   fd = Open "." & CStr(numfd) For Read Watch
 Next

 Free(pfds)

End


Public Sub File_Read() ' Routine dove saranno letti e raccolti i "veri" dati relativi agli Eventi Midi di ALSA

 Dim i As Integer
 Dim pEventi As Pointer
 Dim midi As Snd_seq_event

 Do

' Questa è la funzione di ALSA che restituirà i "veri" dati degli Eventi Midi nella variabile "pEventi":
   i = snd_seq_event_input(handle, VarPtr(pEventi))
   If i < 0 Then Break

   midi = pEventi
   Select Case midi.type
     Case SND_SEQ_EVENT_NOTEON
       Print "Evento 'NoteOn': "; midi.channel, midi.note, midi.velocity
     Case SND_SEQ_EVENT_NOTEOFF
       Print "Evento 'NoteOff': "; midi.channel, midi.note, midi.velocity
     Case SND_SEQ_EVENT_CONTROLLER
       Print "Evento 'Control Change': "; midi.channel, midi.param, midi.value
     Case SND_SEQ_EVENT_PGMCHANGE
       Print "Evento 'Program Change': "; midi.channel, midi.value
   End Select

 Loop

End


Uso contemporaneo di due o più dispositivi Midi esterni

L'uso dei "file descriptor" ci permette, a differenza del file-device, la fruizione più ampia delle facoltà del sistema ALSA, come ad esempio poter utilizzare contemporaneamente due o più dispositivi Midi esterni.
Per fare ciò, bisognerà effettuare delle integrazioni alla routine attinente alle connessioni del nostro applicativo:

' int snd_seq_connect_from(snd_seq_t *seq, int myport, int src_client, int src_port)
' Simple subscription.
Private Extern snd_seq_connect_from(seq As Pointer, myport As Integer, src_client As Integer, src_port As Integer) As Integer

' int snd_seq_connect_to(snd_seq_t *seq, int myport, int src_client, int src_port)
' Simple subscription.
Private Extern snd_seq_connect_to(seq As Pointer, myport As Integer, src_client As Integer, src_port As Integer) As Integer

Public Sub setdevice(client As Integer, port As Integer)

 Dim err As Integer
 
 dclient = client
 dport = port

' Ci connettiamo ad ALSA per inviargli i dati Midi ricevuti:
 err = snd_seq_connect_to(handle, outport, 14, dport)
 printerr("Subscribe outport", err)

' Connettiamo un primo dispositivo Midi esterno al nostro applicativo per ricevere i dati Midi
 ' (ipotizziamo che il suo numero di Id-Client sia 24):
 err = snd_seq_connect_from(handle, inport, 24, 0)
 printerr("Subscribe inport", err)
 
' Inseriamo una seconda chiamata alla funzione esterna di ALSA per utilizzare contemporaneamente un secondo dispositivo Midi esterno (ipotizziamo che il suo numero di Id-Client sia 28):
 err = snd_seq_connect_from(handle, inport, 28, 0)
 printerr("Subscribe inport", err)

 If err < 0 Then error.Raise("Error subscribe output device")

End


Note

[1] Un File Descriptor è un handle di un file.

[2] ALSA non ritorna semplici file descriptor, ma multipli "struct pollfd". E' quindi indispensabile sapere quanti byte occupa una struttura come quella, e quindi quanta memoria è richiesta in totale.

[3] I programmi in C, sopra citati, prevedono, in vero anche un ciclo infinito ed una chiamata alle funzioni "snd_seq_nonblock()" di ALSA e "poll()" del sistema, ma nel nostro applicativo in Gambas, come vedremo, questi due elementi non saranno necessari.

[4] Così si è emulata la capacità di personalizzare il message loop, consentendo al programma di far monitorare dal sistema anche file arbitrari (nel nostro caso i file descriptor passati da ALSA) da noi desiderati, ossia anche gli eventi ai quali noi siamo appunto interessati.

[5] Oltre alla visualizzazione nella console di Gambas, è possibile vedere il numero del file descriptor, usato da ALSA, lanciando nel terminale - dopo aver trovato il PID del programma - i seguenti comandi:

  • lsof -p Pid_del_programma: si potrà notare che il numero del file descriptor, anche confrontandolo con quello mostrato dalla console di Gambas, è quello che nella lunga lista ha come "NAME" il percorso indicato: /dev/snd/seq;
  • strace -p Pid_del_programma: strace è un comando per vedere le chiamate di sistema effettuate da un processo qualunque. La prima riga mostra hee la chiamata di una particolare funzione viene invocata su un determinato file descriptor (es.: fd=3), e ritorna con il valore n (cioé ci sono num. n file descriptor pronti per essere letti. Nel nostro esempio questo file descriptor è il numero 3 ed è pronto per essere letto).

[6] Tutto il meccanismo si basa sul fatto di poter dire al sistema operativo: "quando ci sono dati da leggere, avvertimi". Il tutto si traduce, nel programma, che l'evento "File_Read()" viene scatenato. Il meccanismo consente a un programma di poter "dormire", perciò senza impegnare la CPU, per tutto il tempo che non ci sono dati disponibili. Se si lancia un "ps ax" in console, sarà possibile infatti notare che ci sono decine di processi che girano, anche se la CPU è scarica. Tutti quei programmi stanno aspettando qualcosa... ed è solo grazie a questo meccanismo che possono girare tutti insieme senza sovraccaricare la CPU.
Sempre per il risparmio di CPU, e questo è il punto centrale della questione, il sistema operativo si limita ad avvertire il programma -una volta sola- che qualche cosa è pronto da leggere. A questo punto il programma si sveglia, va a leggere ciò che c'è da leggere, e torna a dormire. Naturalmente, può passare un certo tempo tra l'avvertimento del sistema e la reazione (lettura del file) del programma (il programma potrebbe essere impegnato a fare qualche cosa d'altro). Se durante questo tempo arrivano altri dati, il sistema non manda ulteriori avvertimenti, cioè non verrà generato un secondo evento, e pertanto quei dati questi rischiano di accatastarsi uno sull'altro. Quindi, quando l'evento "File_Read()" si scatena, non si è in grado di sapere quanti eventi (o quanti byte) sono pronti da leggere - si sa solo che c'è "qualche cosa". Quindi bisogna leggere tutto quello che c'è, tutto ciò che è disponibile, non un solo dato per volta; e bisogna provare a leggere finché ce n'è. Se non si fa così, è probabile che alcuni dati rimangano indietro.
Immaginiamo, per esempio, di formare un accordo di tre note: sicuramente uno dei tasti verrà premuto prima degli altri. Il primo dei tasti causerà il passaggio del file descriptor allo stato "pronto", ma se il programma non è velocissimo a leggere quel tasto, può darsi che nel frattempo venga accodato un altro tasto. A questo punto il programma avrà in sospeso un solo evento gambas, non due - quindi con quell'unico evento occorre leggere due tasti.
Le istruzioni di lettura vanno dunque ripetute, comportando così l'utilizzo di un ciclo: "Do...Loop".
Dal ciclo si uscirà soltanto quando non c'è nulla da leggere dalla funzione esterna "snd_seq_event_input()".