Inviare dati Midi da Arduino a Gambas

Da Gambas-it.org - Wikipedia.

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.


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.
Pertanto il codice della Classe principale sarà il seguente:

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()
  
 Me.Center  
  
' 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"

' 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(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.