Inviare dati Midi da Arduino a Gambas
Questa pagina prende in considerazione due casi:
- il caso in cui i dati Midi da inviare sono prestabiliti dal codice caricato in Arduino, e pertanto saranno inviati in modo automatizzato, ossia con i tempi stabiliti dal predetto codice;
- il caso il cui i dati Midi sono inviati quando l'utente chiude un circuito con un interruttore.
Indice
Dati inviati da Arduino in modalità automatizzata prestabilita da codice
Mostriamo un esempio pratico in cui verranno inviati da Arduino al nostro programma Gambas alcuni messaggi MIDI ad intervalli prestabiliti.
In questo caso il ciclo infinito di Arduino procederà come segue:
- invio di un messaggio Midi Program Change per impostare lo strumento musicale da utilizzare;
- invio di un messaggio Note On (status 144, nota 64, velocità 100);
- attesa per 700 millisecondi;
- invio di un messaggio Note Off (status 128, nota 64, velocità 0);
- attesa per 200 millisecondi;
- incremento del 2° valore del Program Change relativo allo strumento musicale da utilizzare.
Dunque il codice per Arduino sarà il seguente:
byte noteON[] = {144, 64, 100}; byte noteOFF[] = {128, 64, 0}; byte controlchange[] = {176, 0, 0}; int count = 0; int str = 0; void setup() { Serial.begin(57600); /* Invia il messaggio di Control Change */ for (count=0;count<3;count++) { Serial.write(controlchange[count]); } } void loop() { /* Invia il messaggio di Program Change */ Serial.write(192); Serial.write(str); /* Invia il messaggio di Note On */ for (count=0;count<3;count++) { Serial.write(noteON[count]); } delay(700); /* Invia il messaggio di Note Off */ for (count=0;count<3;count++) { Serial.write(noteOFF[count]); } delay(200); /* Incrementa la variabile "str" per cambiare strumento musicale del soundfont bank utilizzato */ ++str; if (str==128) str = 0; }
Il programma Gambas raccoglierà i valori inviati da Arduino (è necessario attivare il Componente gb.net) e li invierà ad Alsa - attraverso un'apposita Classe secondaria che chiameremo CAlsa - per l'esecuzione sonora.
Il codice dell'applicativo Gambas comunica con la porta seriale "/dev/ttyUSB0", o potrà essere anche “/dev/ttyACM0”, (ovviamente il numero finale può essere anche diverso, se sono stati connessi altri dispositivi analoghi).
Private SerialPort1 As SerialPort Private bb As New Byte[] Private Const outdevice As Integer = 14 ' client 14: «Midi Through» Private Const outport As Integer = 0 ' porta d'uscita Public alsa As CAlsa Public Sub Form_Open() ' Crea la classe "Clasa" per poterla usare e gestire le funzioni di Alsa: With alsa = New CAlsa As "alsa" ' Apre Alsa e gli assegna un nome: .alsa_open("Gambas-Midi-Arduino") ' Sceglie la periferica su cui suonare: .setdevice(outdevice, outport) End With End Public Sub Button1_Click() With SerialPort1 = New SerialPort As "portaseriale" .PortName = "/dev/ttyUSB0" .Speed = 57600 .Parity = 0 .DataBits = 8 .StopBits = 1 .FlowControl = 0 .Open End With End Public Sub portaseriale_Read() Dim b As Byte ' Legge i dati dalla porta... Read #SerialPort1, b bb.Push(b) Select Case bb[0] Case 128 To 143 If bb.Count = 3 Then Print "NoteOff: ", bb[0], bb[1], bb[2] Print "----------------------------" bb[0] = bb[0] And 15 alsa.noteoff(bb[0], bb[1], bb[2]) alsa.flush() bb.Clear Endif Case 144 To 159 If bb.Count = 3 Then Print "NoteOn: ", bb[0], bb[1], bb[2] bb[0] = bb[0] And 15 alsa.noteon(bb[0], bb[1], bb[2]) alsa.flush() bb.Clear Endif Case 176 To 191 If bb.Count = 3 Then Print "Control Change: ", bb[0], bb[1], bb[2] bb[0] = bb[0] And 15 alsa.controller(bb[0], bb[1], bb[2]) alsa.flush() bb.Clear Endif Case 192 To 207 If bb.Count = 2 Then Print "Program Change: ", bb[0], bb[1] bb[0] = bb[0] And 15 alsa.programchange(bb[0], bb[1]) alsa.flush() bb.Clear Endif End Select End Public Sub Form_Close() If SerialPort1.Status = Net.Active Then SerialPort1.Close End
Per la gestione dei dati Midi con il sub-sistema seq di Alsa mediante alcune sue funzioni esterne creeremo un'apposita Classe secondaria, chiamata CAlsa, il cui codice sarà il seguente:
Private handle As Pointer Private id As Integer Private outport As Integer Private outq As Integer Private dclient As Byte Private dport As Byte Public ev As Pointer Private p As Stream Private Const SND_SEQ_PORT_CAP_READ As Integer = 1 Private Const SND_SEQ_PORT_TYPE_MIDI_GENERIC As Integer = 2 Private Const SND_SEQ_OPEN_DUPLEX As Integer = 3 Private Const SND_SEQ_EVENT_NOTEON As Byte = 6 Private Const SND_SEQ_EVENT_NOTEOFF As Byte = 7 Private Const SND_SEQ_EVENT_CONTROLLER As Byte = 10 Private Const SND_SEQ_EVENT_PGMCHANGE As Byte = 11 Private Const SND_SEQ_PORT_TYPE_APPLICATION As Integer = 1048576 Private Const SIZE_OF_SEQEV As Integer = 32 Library "libasound:2.0.0" ' int snd_seq_open (snd_seq_t **seqp, Private 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, Private 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, Private 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_alloc_named_queue (snd_seq_t * seq, const char * name) ' Allocate a queue with the specified name. Private Extern snd_seq_alloc_named_queue(seq As Pointer, name As String) As Integer ' int snd_seq_connect_to (snd_seq_t *seq, int my_port, int dest_client, int dest_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 ' int snd_seq_event_output_buffer (snd_seq_t *handle, snd_seq_event_t *ev) ' Output an event onto the lib buffer without draining buffer. Private Extern snd_seq_event_output_buffer(handle As Pointer, ev As Pointer) As Integer ' int snd_seq_drain_output (snd_seq_t *handle) ' Drain output buffer to sequencer. Private Extern snd_seq_drain_output(handle 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 Pointer Public Sub alsa_open(nome As String) Dim err As Integer err = snd_seq_open(VarPtr(handle), "default", SND_SEQ_OPEN_DUPLEX, 0) printerr("Apertura Alsa", err) If err < 0 Then error.RAISE("Error opening alsa") snd_seq_set_client_name(handle, nome) id = snd_seq_client_id(handle) Print "Alsa ClientID="; id err = snd_seq_create_simple_port(handle, "Seq-Out", SND_SEQ_PORT_CAP_READ, SND_SEQ_PORT_TYPE_MIDI_GENERIC + SND_SEQ_PORT_TYPE_APPLICATION) Print "Porta d'Uscita = "; err If err < 0 Then error.Raise("Error creating output port") outport = err err = snd_seq_alloc_named_queue(handle, "outqueue") printerr("Creazione della coda dei dati ", err) If err < 0 Then error.Raise("Error creating out queue") outq = err ' Alloca un evento per gestirlo: ev = Alloc(SizeOf(gb.Byte), SIZE_OF_SEQEV) p = Memory ev For Write End Public Sub setdevice(client As Integer, port As Integer) Dim err As Integer dclient = client dport = port err = snd_seq_connect_to(handle, outport, client, dport) printerr("Subscribe outport", err) If err < 0 Then error.Raise("Errore nella sottoscrizione del dispositivo di Uscita !") End Private Sub prepareev(type As Byte) As Pointer Dim i As Integer Dim ts, flags, tag As Byte ' Pulisce innanzitutto l'area di memoria dei dati dell'evento Midi: For i = 0 To SIZE_OF_SEQEV - 1 Seek #p, i Write #p, 0 As Byte Next Seek #p, 0 Write #p, type As Byte ts = 0 flags = 0 Write #p, flags As Byte tag = 0 Write #p, tag As Byte Write #p, outq As Byte Write #p, ts As Integer ts = 0 Write #p, ts As Integer Write #p, id As Byte Write #p, outport As Byte Write #p, dclient As Byte Write #p, dport As Byte End ' °°°°°°°°°°° GESTIONE DEI SINGOLI MESSAGGI MIDI °°°°°°°°°°° Public Sub noteon(channel As Byte, note As Byte, velocity As Byte) Dim err As Integer prepareev(SND_SEQ_EVENT_NOTEON) Write #p, channel As Byte Write #p, note As Byte Write #p, velocity As Byte err = snd_seq_event_output_buffer(handle, ev) printerr("Noteon = ", err) End Public Sub noteoff(channel As Byte, note As Byte, velocity As Byte) Dim err As Integer prepareev(SND_SEQ_EVENT_NOTEOFF) Write #p, channel As Byte Write #p, note As Byte Write #p, velocity As Byte err = snd_seq_event_output_buffer(handle, ev) printerr("Note OFF = ", err) End Public Sub controller(channel As Byte, valore1 As Integer, valore2 As Integer) Dim err As Integer prepareev(SND_SEQ_EVENT_CONTROLLER) Write #p, channel As Byte Seek #p, 20 Write #p, valore1 As Integer Write #p, valore2 As Integer err = snd_seq_event_output_buffer(handle, ev) printerr("Controller = ", err) End Public Sub programchange(channel As Byte, valore1 As Byte) Dim err As Integer prepareev(SND_SEQ_EVENT_PGMCHANGE) Write #p, channel As Byte Seek #p, 24 Write #p, valore1 As Byte err = snd_seq_event_output_buffer(handle, ev) printerr("Program Change = ", err) End Public Sub flush() Dim err As Integer err = snd_seq_drain_output(handle) Printerr("Flush", err) End ' °°°°°°°°°°° GESTIONE DEGLI ERRORI °°°°°°°°°°° Public Sub errmsg(err As Integer) As String Return String@(snd_strerror(err)) End Private Sub printerr(operation As String, err As Integer) If err < 0 Then Print operation; ": err = "; err; " ("; errmsg(err); ")" End
Dati inviati da Arduino da parte dell'utente chiudendo il circuito con un interruttore
In quest'altro caso sarà invece l'utente a decidere quali messaggi Midi, tra quelli disponibili, inviare ed i tempi d'invio. In sostanza faremo l'esempio di una sorta di tastierina Midi composta da 24 note (compresi i semitoni) utilizzando Arduino UNO.
Il lato hardware
Il problema che, in ogni caso, vogliamo affrontare è quello di avere a disposizione un numero di note disponibili maggiore del numero limitato di piedini fornito da Arduino UNO. La soluzione da adottare per aumentare la disponibilità funzionale dei piedini esistenti è quella, suddividendo in modo adeguato i piedini disponibili fra piedini in Uscita (Output) e piedini in Entrata (Input), di collegare ciascun piedino in Uscita a più piedini in Entrata. Ovviamente fra il piedino in Uscita ed il piedino in Entrata vi sarà un interruttore (che sarà azionato dal corrispondente tasto della tastiera Midi). Ogni collegamento del tipo appena descritto attiene ad una nota della nostra tastiera Midi.
Lo schema dunque di collegamento di base di ciascun piedino in Uscita con un piedino in Entrata è il seguente:
OUTPUT INPUT pin pin ◉ ◉ │ │ │ │ └────/ ────┤ │ ᕒ R = 100 ㏀ __│__ ░░░░░
Nel nostro caso, al fine di ottenere la disponibilità di 24 note Midi, imposteremo i piedini 3, 4 e 5 in Uscita ed i piedini 6, 7, 8, 9, 10, 11, 12, e 13 in Entrata. Ebbene, come già accennato sopra, ciascun piedino in Uscita (3, 4 e 5) sarà collegato ad ogni piedino in Entrata. Dunque avremo 3 piedini a disposizione in Uscita e 8 piedini in Entrata: 3 x 8 = 24 .
Cosicché, in tale prospettiva di un collegamento molteplice fra ciascun piedino in Uscita e i piedini in Entrata, lo schema avrà sostanzialmente questo tenore:
E 8 ◉─────┈┈┈┈┈ etc. n ┊ t 100 kOhm ┊ r 7 ◉──────────────┬───┬───┬───────────────WWWW──────────────┤ a │ │ │ │ t │ │ │ 100 kOhm │ a 6 ◉──┬───┬───┬───│───│───│───────────────WWWW──────────────┤ │ │ │ │ │ │ │ \ \ \ \ \ \ │ │ │ │ │ │ │ │ ⏄ ⏄ ⏄ ⏄ ⏄ ⏄ │ │ │ │ │ │ │ ┊ ┊ ┊ │ U 5 ◉──┴───│───│───┴───│───│───┴───│───│─────┈┈┈┈ │ s │ │ │ │ │ │ │ c 4 ◉──────┴───│───────┴───│───────┴───│─────┈┈┈┈ │ i │ │ │ │ t 3 ◉──────────┴───────────┴───────────┴────┈┈┈┈┈ │ a │ │ __│__ ░░░░░
La parte software
Il problema che sorge, avendo così congeniato il funzionamento dell'hardware di Arduino UNO, è quello di differenziare il segnale che giunge ad un piedino in Entrata. Tale questione si pone con forte evidenza, poiché ad un medesimo piedino in Entrata sono collegati tre piedini in Uscita. Dunque la domanda è: come distiguere da quale dei tre piedini in Uscita è giunto il segnale ad un piedino in Entrata ? Tale distinzione verrà effettuata a livello del codice, con il quale faremo funzionare Arduino.
Nel caso qui ipotizzato, le note Midi da inviare saranno 24 (dal Do centrale: nota n. 60, al Si sopra il 5° rigo: nota n. 83). Il numero Midi di ciascuna nota sarà posto in una matrice a due dimensioni: una che fa riferimento al numero di ciascun pin in Uscita, l'altra che fa riferimento al numero di ciascun pin in Entrata. Per comodità didattica in questo esempio faremo coincidere, dunque, il numero di ciascun pin di Arduino con il rispettivo numero d'indice della dimensione di appartenenza della matrice.
Il codice da passare ad Arduino sarà il seguente:
byte pin[14] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13}; // pin Uscita - pin Entrata byte matrice[6][14]; byte note[6][14] = { {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 60, 61, 62, 63, 64, 65, 66, 67}, {0, 0, 0, 0, 0, 0, 68, 69, 70, 71, 72, 73, 74, 75}, {0, 0, 0, 0, 0, 0, 76, 77, 78, 79, 80, 81, 82, 83}, }; void setup() { byte b = 0; /* Imposta i pin di Uscita */ for (b = 3; b < 6; ++b) pinMode(pin[b], OUTPUT); /* Imposta i pin di Entrata */ for (b = 6; b < 14; ++b) pinMode(pin[b], INPUT); Serial.begin(57600); } void loop() { byte u = 0; byte e = 0; byte r = 0; /* Prende in considerazione i pin in Uscita dal n. 3 al n. 5 */ for (u = 3; u < 6; ++u) { digitalWrite(pin[u], HIGH); // pin attuale dell'output /* Prende in considerazione i pin in Entrata dal n. 6 al n. 13 */ for (e = 6; e < 14; ++e) { r = digitalRead(pin[e]); if (r != matrice[u][e]) { Serial.write(128 + (16 * r)); Serial.write(nota[u][e]); Serial.write(100 * r); matrice[u][e] = r; } } digitalWrite(pin[u], LOW); } delay(10); }
Il codice Gambas
Per quanto riguarda il codice del programma Gambas, che dovrà ricevere ed interpretare i dati Midi inviati da Arduino, si potrà prendere in considerazione il codice Gambas del programma del paragrafo precedente.