Comunidad Gambas-es
Enviar datos MIDI a un softsynth vía ALSA - Versión para impresión

+- Comunidad Gambas-es (https://gambas-es.org)
+-- Foro: Gambas (https://gambas-es.org/forum-3.html)
+--- Foro: Aplicaciones/Fragmentos de Código (https://gambas-es.org/forum-8.html)
+--- Tema: Enviar datos MIDI a un softsynth vía ALSA (/thread-1563.html)



Enviar datos MIDI a un softsynth vía ALSA - vuott - 06-12-2023

Notas breves sobre cómo enviar "Mensajes MIDI" a un softsynth vía A.L.S.A., de la manera más sencilla posible.

Como es sabido, un flujo de datos MIDI no contiene - como es el caso del audio - la información que describe las características de la onda sonora, sino información que será utilizada por otros programas para ejecutar un sonido localizado entre una colección. Esta colección estandarizada de sonidos de varios instrumentos musicales, en su mayoría de formato WAV, está contenida en un archivo especial, llamado banco de fuentes de sonido.

El programa que debe utilizar los sonidos, contenidos en los archivos del banco de fuentes sonoras (soundfont bank), para que una nota MIDI pueda ser escuchada, se llama softsynth.
El protocolo MIDI a través de un "Mensaje MIDI" informa al softsynth de que debe usar una fuente de sonido WAV (que en la práctica corresponde a un instrumento musical) para cambiar a una cierta frecuencia de sonido durante un período de tiempo determinado.

Por lo tanto, un programa, que produce "Mensajes MIDI", debe entregar los datos necesarios al softsynth. Solo así el "Mensaje MIDI" podrá proporcionar la producción del sonido requerido.

Actualmente, la comunicación entre un programa que envía datos MIDI y el softsynth suministrado se realiza a través del sistema de sonido A.L.S.A. que se encarga de gestionar el protocolo de transmisión, recepción y eventual temporización de los datos MIDI, así como la relación con el sistema operativo y el hardware de audio suministrado.

ALSA actúa como un "Servidor" central que proporciona funciones y servicios a los programas que deben tratar con él para la reproducción de audio.
Dichos programas, por lo tanto, se convierten en "Clientes" de ALSA, y se relacionan con dicho sistema central de sonido para poder comunicarse con otros "Clientes" y con el sistema operativo.
Los "Clientes" del "Servidor" de ALSA son visibles en el archivo "/proc/asound/seq/clients".

Por lo tanto, si queremos en Gambas realizar cualquier programa que envíe "Mensajes MIDI" al softsynth en punto para su gestión sonora, deberá relacionarse con ALSA.

El sistema ALSA se compone de algunos subsistemas. El sub-sistema que gestiona los datos MIDI se llama "secuenciador" de ALSA e identificado con la abreviatura "seq". Por lo tanto, será necesario abrir ese sub-sistema para poder, por un lado, transformar nuestro programa en un "cliente" de ALSA y, por otro, aprovechar los recursos que ese sub-sistema proporciona.

Para operar directamente con ALSA y sus recursos, nuestro programa Gambas deberá utilizar las funciones externas de ALSA mediante la instrucción "Extern", después de haber declarado la biblioteca compartida de ALSA, que contiene las funciones externas que servirán para el envío de "Mensajes de Midi".
La biblioteca compartida externa de ALSA se declarará como sigue:
Cita: Library "libasound:2.0.0"
La función externa de ALSA que permite hacer de nuestro programa Gambas un "Cliente" de ALSA es:
 snd_seq_open()
De los cuatro parámetros formales aquí se encuentra en particular el primero, que es un puntero de puntero, y que en Gambas se debe reproducir: VarPtr(Pointer); así como el tercero: el argumento pasado será una Constante para la configuración del "secuenciador" de ALSA para los datos en "Salida".
Esta función externa figurará en nuestro programa como sigue:
Cita: Private Extern snd_seq_open(handle As Pointer, name As String, streams As Integer, mode As Integer) As Integer
Una vez terminado, el sub-sistema "seq" debe cerrarse para liberar la memoria utilizada para la gestión de los recursos proporcionados por ALSA.
El cierre se efectuará con la función externa de ALSA declarada en Gambas:
Cita:Private Extern snd_seq_close(handle As Pointer) As Integer
La gestión de los posibles errores con las funciones externas de ALSA se realizará con la función externa específica declarada en Gambas:
Cita:Private Extern snd_strerror(err As Integer) As String
Por lo tanto, al disponer la apertura del sub-sistema "seq" de ALSA mediante un 'ToggleButton', tendremos hasta ahora el siguiente código:
Código:
Private midi As Pointer
 
 
Library "libasound:2.0.0"
 
Private Const SND_SEQ_OPEN_OUTPUT As Integer = 1
 
Private Extern snd_seq_open(handle As Pointer, name As String, streams As Integer, mode As Integer) As Integer
Private Extern snd_strerror(err As Integer) As String
Private Extern snd_seq_close(handle As Pointer) As Integer
 
Public Sub ToggleButton1_Click()
 
 If ToggleButton1.Value Then
   Dim rit As Integer
   rit = snd_seq_open(VarPtr(midi), "default", SND_SEQ_OPEN_OUTPUT, 0)
   If rit < 0 Then Error.Raise("Errore: " & snd_strerror(rit))
 Else
   snd_seq_close(midi)
 Endif
 
End
Al hacer clic en 'ToggleButton', nuestro programa se convertirá en un "Cliente" de ALSA. Esto puede comprobarse en el archivo de sistema "/proc/asound/seq/clients". Si ya ha lanzado el sofsynth, nuestro programa recibirá el número de identificación 129 de "Cliente" de ALSA.

Nuestro programa MIDI ya está listo para enviar datos MIDI al softsynth a través de ALSA.
ALSA es muy estricto y requiere que el "Mensaje MIDI" esté compuesto por varios datos específicos que se deben tener en un área de memoria reservada, formada por 28 bytes.
Cada "Mensaje MIDI" está representado en el protocolo de ALSA con un "Evento MIDI" que consiste en la zona de memoria asignada de 28 bytes, para la cual el protocolo de ALSA prevé el uso de una Estructura.
Gambas nos ofrece más de una opción para crear ese área de memoria reservada. Dado que nuestro ejemplo se limitará solo a "Mensajes MIDI" de encendido y apagado de una nota MIDI, podremos utilizar con tranquilidad un vector de tipo Byte[].
Al hacer clic en un 'Botón', de los 28 bytes, solo valoraremos algunos:
Al elemento de índice cero asignaremos un entero que represente de vez en cuando el "Mensaje MIDI" de Note-ON (encendido de la nota MID) o de Note-OFF (apagado de la nota MIDI que se está ejecutando).
Al elemento de índice 3 asignaremos el valor 253 para indicar que el envío del "Evento MIDI" de ALSA es directo.
Al elemento del índice 14 le asignaremos el número de identificación del softsynth, al que enviar el "Evento MIDI" de ALSA, y que suele ser 128, como se puede comprobar en el archivo "/proc/asound/seq/clients".
Al elemento de índice 17 le asignaremos un número de nota MIDI, que se ejecutará o silenciará.
Al elemento del índice 18 asignaremos el valor 100 de "velocidad del tacto".
El elemento de índice 16 representa el canal MIDI, que permanecerá - en nuestro caso - preestablecido en cero (canal MIDI 1). Si lo ponemos a 9 (canal MIDI 10), escucharemos un instrumento de percusión.

Constituido el "Evento MIDI" de ALSA en el vector de tipo Byte[], es necesario enviarlo al softsynth a través de ALSA.
Esto se hace en nuestro caso esencial a través de una función externa específica, como se indica en nuestro programa:
Cita:Private Extern snd_seq_event_output_direct(handle As Pointer, ev As Byte[])

Todo está en orden.
A continuación, el código completo de nuestro sencillo y esencial programa Gambas:
Código:
Private midi As Pointer
 Private evento As New Byte[28]
 
 
 Library "libasound:2.0.0"
 
 Private Const SND_SEQ_OPEN_OUTPUT As Integer = 1
 Private Const SND_SEQ_QUEUE_DIRECT As Byte = 253
 Private Enum SND_SEQ_EVENT_NOTEON = 6, SND_SEQ_EVENT_NOTEOFF
 
 Private Extern snd_seq_open(handle As Pointer, name As String, streams As Integer, mode As Integer) As Integer
 Private Extern snd_strerror(err As Integer) As String
 Private Extern snd_seq_event_output_direct(handle As Pointer, ev As Byte[])
 Private Extern snd_seq_close(handle As Pointer) As Integer
 
 
 Public Sub Form_Open()
  
  Button1.Enabled = False
  
End

 
 Public Sub ToggleButton1_Click()
 
  If ToggleButton1.Value Then
    Dim rit As Integer
    rit = snd_seq_open(VarPtr(midi), "default", SND_SEQ_OPEN_OUTPUT, 0)
    If rit < 0 Then Error.Raise("Errore: " & snd_strerror(rit))
    Button1.Enabled = True
  Else
    snd_seq_close(midi)
    Button1.Enabled = False
  Endif
 
 End

Public Sub Button1_MouseDown()

  evento[0] = SND_SEQ_EVENT_NOTEON
  evento[3] = SND_SEQ_QUEUE_DIRECT
  evento[14] = 128
  evento[17] = 64
  evento[18] = 100
  
  snd_seq_event_output_direct(midi, evento)

End


Public Sub Button1_MouseUp()

  evento[0] = SND_SEQ_EVENT_NOTEOFF
  evento[3] = SND_SEQ_QUEUE_DIRECT
  evento[14] = 128
  evento[17] = 64
  evento[18] = 0
  
  snd_seq_event_output_direct(midi, evento)

End

Si ha instalado softsynth Fluidsynth en el sistema, entonces el programa, descrito anteriormente, debería conectarse automáticamente a ese softsynth.
En caso contrario, proceder como sigue:
1) desde Terminal lanzar esta línea: ~$ fluidsynth reload 0 ;
2) sin cerrar el terminal, comprobar en la utilidad "Monitor de sistema" que Fluidsynth está presente entre los procesos activos;
3) si Fluidsynth está presente entre los procesos, verificar también que esté presente en el num. 128 en el fichero /proc/asound/seq/clients;
4) en caso afirmativo - sin cerrar el Terminal - lanzar el código, mostrado arriba.
Si utiliza el softsynth de Fluidsynth, debe tener instalado en su sistema el paquete Fluid (R3) General MIDI SoundFont (GM): fluid-soundfont-gm


RE: Enviar datos MIDI a un softsynth vía ALSA - Shell - 06-12-2023

Bonus labor. Smile

Tengo que leerlo con más tranquilidad, es un tema interesante.

No sabía lo que era un softsynth, pero si lo he usado con los bancos de instrumentos.
Cuando las tarjetas de música eran más grandes de lo habitual. Más allá de una Sound Blaster 2.0.

Hoy reproducir con bancos de sonidos es más fácil. Cualquier "tarjetita" de sonido incorporadas en el pc puede hacerlo.
Sobretodo lo uso en emulación "DosBox" con juegos de msdos. Puedo afirmar que se nota.
Era el tiempo de msdos y cuando se escuchaba aquello era algo totalmente distinto a lo habitual.

La rom de la propia tarjeta de sonido incorporaba un banco de instrumentos. Ya existían dos tipos de bancos de sonidos, dos extensiones.
Hoy puedes bajarte de Internet aquellos bancos de sonidos que incorporaban las tarjetas de sonido.

Los juegos usaban esos bancos de sonidos "concretos", si le pones otro, puede sonar muy diferente y no gustar.

Y esto es midi, no hablamos de los mod,s3m, tiempo de Amiga ( SoundTrackers, había muchas aplicaciones de sonido terminadas en "Tracker", eran
como pistas, era varias pistas que cada una, era un instrumento).

En el cual los instrumentos era un archivo añadido al archivo de la canción  (Tenías que ir cambiando disquetes, según
la aplicación te iba pidiendo archivos de sonido o samples. Y a veces según el tipo de extensión iba todo en el mismo archivo mod.
Instrumentos y la canción.

Era otro mundo.

Saludos


RE: Enviar datos MIDI a un softsynth vía ALSA - jguardon - 06-12-2023

Fantástica masterclass, voutt. Me quito el sombrero!
Me gustaría incluirlo en nuestro viejo y oxidado blog para futuras referencias y como punto de partida para más artículos como éste dedicados al audio en Gambas, por supuesto con su atribución a vos, su autor.

¿Da usted su permiso, cónsul?

Saludos


RE: Enviar datos MIDI a un softsynth vía ALSA - vuott - 06-12-2023

(06-12-2023, 16:38)jguardon escribió: ¿Da usted su permiso, cónsul?
Claro que sì.

De todos modos, a todos los que quieran profundizar en este tema, sugiero la lectura de mi "magnum opus " sobre Gambas, MIDI y ALSA en la wiki del foro italiano:

https://www.gambas-it.org/wiki/index.php/Gestione_del_MIDI_con_ALSA


RE: Enviar datos MIDI a un softsynth vía ALSA - vuott - 07-12-2023

Otro código similar, pero más complejo, usando una Estructura para crear el Evento Midi de ALSA:
Código:
Private cb As ComboBox
Private Const ALTURA_TECLAS_BLANCAS As Single = 0.35
Private bb As New Byte[3]
Private instrumenta 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"]


Public Sub Form_Open()

 With Me
   .W = Screen.AvailableWidth * 0.5
   .H = Screen.AvailableHeight * 0.2
   .Center
 End With
 CreaClient()
 With cb = New ComboBox(Me) As "Combo"
   .W = 170
   .H = 25
   .X = (Me.W * 0.94) - .W
   .Y = 0
   .List = instrumenta
   .Placeholder = "Instrumentos musicales"
 End With

 CreaTeclado()

End

Private Procedure CreaTeclado()

 Dim pn As Panel
 Dim negras As New Byte[40]
 Dim teclas As Button[]
 Dim by, c, n As Byte
 
 With pn = New Panel(Me)
   .W = Me.W * 0.88
   .H = Me.H * 0.2
   .X = (Me.W / 2) - (pn.W / 2)
   .Y = Me.H * 0.2
   .Border = Border.Sunken
   .Background = &8b4513
 End With

 Repeat 
   negras = 25 + (12 * by / 5)
   negras[by + 1] = 27 + (12 * by / 5)
   negras[by + 2] = 30 + (12 * by / 5)
   negras[by + 3] = 32 + (12 * by / 5)
   negras[by + 4] = 34 + (12 * by / 5)
   by += 5
 Until by == negras.Count

 teclas = New Button[109]

 For t As Short = 0 To teclas.Max
   With teclas[t] = New Button(Me) As "Teclas"
     .W = 0
     If t > 23 Then
       If negras.Exist(t) Then  ' Establece las teclas negras
         .W = Me.W * 0.013
         .H = ((Me.H * ALTURA_TECLAS_BLANCAS) * 66.66) / 100
         Select Case t 
           Case 25 + (12 * n)
             .X = teclas[t - 1].X + (((Me.W * 0.025) / 2))
           Case 27 + (12 * n)
             .X = teclas[t - 1].X + (((Me.W * 0.025) / 2))
           Case 30 + (12 * n)
             .X = teclas[t - 1].X + (((Me.W * 0.025) / 2))
           Case 32 + (12 * n)
             .X = teclas[t - 1].X + (((Me.W * 0.025) / 2))
           Case 34 + (12 * n)
             .X = teclas[t - 1].X + (((Me.W * 0.025) / 2))
             Inc n
         End Select
         .Y = Me.H * 0.375
         .Background = Color.Black
         .Tag = t
       Else
' Establece las teclas blancas:
         .W = Me.W * 0.018
         .H = Me.H * ALTURA_TECLAS_BLANCAS
         .X = (.W * c) + (Me.W / 16)
         .Y = Me.H * 0.37
         .Background = Color.White
         .Tag = t
         .Lower
         Inc c
       Endif
     Endif 
   End With
 Next

End


Public Sub Teclas_MouseDown()

 bb[0] = 0
 bb[1] = Last.Tag
 bb[2] = &64

 EnvioMIDI(SND_SEQ_EVENT_NOTEON, bb)

 If Last.Background = Color.Black Then Last.Background = Color.DarkGray
 Me.Title = "Nota Midi:  " & Last.Tag

End

Public Sub Teclas_MouseUp()

 bb[0] = 0
 bb[1] = Last.Tag
 bb[2] = 0

 EnvioMIDI(SND_SEQ_EVENT_NOTEOFF, bb)

 If Last.Background = Color.DarkGray Then Last.Background = Color.Black
 Me.Title = " "

End

'''''''''''''''''''''''''''''''''''''''''''''''

Private seq As Pointer


Library "libasound:2"

Public Struct snd_seq_event_t   ' Estructura del Evento Midi de ALSA
  type As Byte
  flags As Byte
  tag As Byte
  queue As Byte
  tick_time As Integer
  real_time 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_OPEN_OUTPUT As Integer = 1
Private Const SND_SEQ_EVENT_PGMCHANGE As Byte = 11
Private Const SND_SEQ_QUEUE_DIRECT As Byte = 253
Private Enum SND_SEQ_EVENT_NOTEON = 6, SND_SEQ_EVENT_NOTEOFF

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

 ' const char * snd_strerror (int errnum)
' Returns the message for an error code.
Private Extern snd_strerror(err As Integer) As String
  
' int snd_seq_event_output (snd_seq_t *handle, snd_seq_event_t *ev)
' Output an event.
Private Extern snd_seq_event_output(handle As Pointer, ev As Snd_seq_event_t)

' 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

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


Private Procedure CreaClient()

 Dim rit As Integer

 rit = snd_seq_open(VarPtr(seq), "default", SND_SEQ_OPEN_OUTPUT, 0)
 If rit < 0 Then Error.Raise("Error: " & snd_strerror(rit))

End


Public Sub Combo_Change()  ' Establece el instrumento musical:

 Dim evento As New Snd_seq_event_t

 With evento
   .queue = SND_SEQ_QUEUE_DIRECT
   .dest_client = 128
   .dest_port = 0
   .channel = 0
 End With

' Establece el tipo de instrumento musical mediante el Mensaje Midi "Program-Change":
 Mensaje(evento, SND_SEQ_EVENT_PGMCHANGE, [0, 0, 0], cb.Index)

End


Private Procedure EnvioMIDI(tipo As Byte, mid As Byte[])

 Dim evento As New Snd_seq_event_t

 With evento
   .queue = SND_SEQ_QUEUE_DIRECT
   .dest_client = 128
   .dest_port = 0
   .channel = mid[0]
 End With

' Establece el mensaje Midi "Note-ON":
 Mensaje(evento, tipo, mid, 0)

End


Private Procedure Mensaje(ev As Snd_seq_event_t, tipo As Byte, nota As Byte[], strum As Integer)

 With ev
   .type = tipo
   .channel = nota[0]
   .note = nota[1]
   .velocity = nota[2]
   .value = strum
 End With

' Inserta el evento de ALSA en el búfer:
 snd_seq_event_output(seq, ev)

' Envia el Evento:
 snd_seq_drain_output(seq)

End


Public Sub Form_Close()

 snd_seq_close(seq)

End