Ottenere un file WAV da un file Midi con le funzioni esterne del API di WildMidi

Da Gambas-it.org - Wikipedia.

Come è noto, non è possibile parlare di una vera e propria conversione di dati da Midi a formato audio. E' possibile, però, ottenere un file audio, ad esempio WAV, da dati Midi.

Il presupposto essenziale, per fare ciò, è avere una funzione che ricavi, produca dati audio grezzi dal file Midi, e che restituisca i dati audio grezzi ricavati, affinché possano essere salvati in un file. Tali dati audio grezzi potranno, così, essere salvati in un file audio di tipo .WAV, avendo, però, l'accortezza di premettere ad essi i dati del chunk iniziale che contraddistingue i file audio WAV.

Le risorse dell'API di WildMidi consentono di generare un flusso di dati audio corrispondenti ad un file Midi. Faremo uso, dunque, in Gambas delle funzioni esterne della libreria "libWildMidi".

Di seguito mostreremo un paio di semplici esempi.


1a modalità

In questo esempio oltre ad utilizzare la libreria di WildMidi, utilizzeremo anche due funzioni esterne della libreria di C per leggere e scrivere in un file.

Library "libWildMidi:1.0.2"

' WildMidi_Init  (const char *config_file, unsigned short int rate, unsigned short int options):
' Intializes "libWildMidi" in preparation for playback.
Private Extern WildMidi_Init(config_file As String, rate As Short, options As Short) As Integer
 
' midi *WildMidi_Open (const char *midifile):
' Open  a MIDI file pointed to by midifile for processing. This file must be in standard midi format.
' It returns a handle for the midi file opened. This handle is used by most functions in libWildMidi to identify which midi file we are refering to.
Private Extern WildMidi_Open(midifile As String) As Pointer

' int  WildMidi_GetOutput  (midi *handle, char *buffer, unsigned long insize)
' Places size bytes of audio data from a  handle,  previously  opened  by WildMidi_Open() or WildMidi_OpenBuffer(), into a buffer pointer to by buffer.
' Buffer must be atleast size bytes, with size being a multiple of 4 as the data is stored in 16 bit interleaved stereo format.
Private Extern WildMidi_GetOutput(handle As Pointer, buffer As Pointer, insize As Long) As Integer

' int WildMidi_Close (midi *handle)
' Finish processing MIDI data or file. "handle" the indentifier obtained from opening a midi file with "WildMidi_Open()".
Private Extern WildMidi_Close(handle As Pointer) As Integer

' void WildMidi_Shutdown(void)
' Shuts  down  the wildmidi library, resetting data and freeing up memory used by the library.
Private Extern WildMidi_Shutdown()


Library "libc:6"

' FILE *fopen (const char *__restrict __filename, const char *__restrict __modes)
' Open a file and create a new stream for it.
Private Extern fopen(filename As String, modes As String) As Pointer

' size_t fwrite (const void *__restrict __ptr, size_t __size, size_t __n, FILE *__restrict __s)
' Write chunks of generic data to STREAM.
Private Extern fwrite(ptr As Pointer, size As Integer, n As Integer, s As Pointer) As Integer

' int fclose (FILE *__stream)
' Close STREAM.
Private Extern fclose(sFile As Pointer) As Integer


Public Sub Main()

 Dim err, quantum As Integer
 Dim wav, midi, buffer as Pointer
 Dim frequenza As Integer = 44100
 Dim fileMIDI, temporaneo As String
 
 fileMIDI = "/percorso/del/file.mid"

  temporaneo = Temp("mid")
  
' Apre in scrittura il file iniziale temporaneo contenente i soli dati audio grezzi ricavati dal file Midi:
  wav = fopen(temporaneo, "wb")


' Inizializza la libreria di "WildMidi".
' Il primo parametro richiama il file che contiene la configurazione degli strumenti musicali che sarà utilizzata dalla libreria.
' Il file di configurazione potrà essere: "/etc/timidity/freepats.cfg" oppure "/etc/wildmidi/wildmidi.cfg":
   err = WildMidi_Init("/etc/timidity/freepats.cfg", frequenza, 0)
   If err < 0 Then Error.Raise("Errore nell'inizializzazione dell'interfaccia libWildMidi !")
   
' Apre il file Midi, dal quale si dovrà ottenere un file WAV:
   midi = WildMidi_Open(fileMIDI)
   If IsNull(midi) Then Error.Raise("Impossibile aprire il file Midi !")

' Alloca un'area di memoria che sarà puntata da una variabile di tipo "Puntatore":
   buffer = Alloc(128)


   Do

' Ad ogni giro viene riempita l'area allocata con un certo numero di dati Midi convertiti in dati audio da scrivere successivamente nel file WAV.
' (In particolare, ad Alsa sarà passata la variabile di tipo "puntatore" - qui chiamata "buffer" - che ora viene riempita di dati):
     quantum = WildMidi_GetOutput(midi, buffer, 128)

' Viene scritta nel file, puntato dalla varibile "wav", una sola quantità di dati per ogni giro, presenti nell'area di memoria allocata
' puntata dalla variabile "buffer", pari al valore contenuto dalla variabile "quantum":
     fwrite(buffer, quantum, 1, wav)

   Loop Until quantum = 0

   fclose(wav)

' A questo punto, nel file iniziale temporaneo vi sono soltanto i dati grezzi audio utili per creare un vero file WAV
' Ora bisognerà aggiungere a tali dati grezzi un blocco iniziale per identificare e caratterizzare il file WAV finale.
' Procederemo a questa operazione in una sotto-procedura specifica a parte che ora andiamo a richiamare:
   generaWAV(temporaneo)


' Chiude l'elaborazione dei dati Midi da parte di "WildMidi":
   err = WildMidi_Close(midi)
   If err < 0 Then Error.Raise("Errore nella chiusura del processo dei dati Midi !")

   Free(buffer)
   
' Chiude la libreria di "WildMidi":
   WildMidi_Shutdown()

End


Private Procedure generaWAV(fltemp As String)

 Dim Ipw, dim_file, m8 As Integer
 Dim j As Byte
 Dim a As New Byte[]
 Dim little_endian, primo_chunk, $prewav As String


' Si procede finalmente a creare il file WAV finale.
' La procedura è abbastanza complessa: si tratta di creare, come già detto sopra, il blocco iniziale distintivo del formato audio WAV,
' e di unirlo successivamente ai dati audio grezzi, ormai ripuliti dagli eventuali valori zero iniziali.

' Ricaviamo la dimensione del file temporaneo contenente i dati audio grezzi:
   Ipw = Stat(fltemp).Size

' Ricaviamo la dimensione totale del futuro file. Essa sarà, dunque, data dai byte del primo blocco ("Chunk")
' (contenente le informazioni sul file WAV ed i dati che lo distinguono ed identificano) pari a 44 byte più i dati audio grezzi del file temporaneo,
' i quali così vengono a formare il seco* Ottenere un file WAV da un file Midi con le funzioni esterne del API di WildMidindo blocco di dati del futuro file WAV.
   dim_file = 44 + Ipw

' Ricaviamo il formato "little-endian" dellla dimensione del file meno gli otto byte iniziali. Il risultato sarà inserito dal 5° all'8° byte:
   m8 = dim_file - 8
   
   little_endian = Hex(((m8 \ CInt(2 ^ 24)) And &FF) Or ((m8 * CInt(2 ^ 8)) And &FF0000) Or ((m8 \ CInt(2 ^ 8)) And &FF00&) Or ((m8 * CInt(2 ^ 24)) And &FF000000), 8)
   
   For j = 0 To 3
     a.Add(CByte(Val("&" & Mid(little_endian, (2 * j) + 1, 2))))
   Next

' Quindi ricaviamo il formato "little-endian" della dimensione del file temporaneo dei dati audio grezzi.
' Il risultato sarà inserito alla fine del primo blocco (chunk):
   little_endian = Hex(((Ipw \ CInt(2 ^ 24)) And &FF) Or ((Ipw * CInt(2 ^ 8)) And &FF0000) Or ((Ipw \ CInt(2 ^ 8)) And &FF00&) Or ((Ipw * CInt(2 ^ 24)) And &FF000000), 8)

   For j = 0 To 3
     a.Add(CByte(Val("&" & Mid(little_endian, (2 * j) + 1, 2))))
   Next


' Impostiamo i dati appartenenti al primo blocco del futuro file WAV":
   primo_chunk = "RIFF" & Chr(a[0]) & Chr(a[1]) & Chr(a[2]) & Chr(a[3]) & "WAVEfmt" & Chr(&20) & Chr(&10) & Chr(&00) & Chr(&00) & Chr(&00)
   primo_chunk &= Chr(&01) & Chr(&00) & Chr(&02) & Chr(&00) & Chr(&44) & Chr(&AC) & Chr(&00) & Chr(&00) & Chr(&10) & Chr(&B1) & Chr(&02)
   primo_chunk &= Chr(&00) & Chr(&04) & Chr(&00) & Chr(&10) & Chr(&00) & "data" & Chr(a[4]) & Chr(a[5]) & Chr(a[6]) & Chr(a[7])

' Carichiamo i dati audio grezzi del file temporaneo":
   $prewav = File.Load(fltemp)

' Ecco, dunque, creiamo il file WAV finale unendo il primo blocco dei dati informativi
' e caratterizzanti il file WAV con i dati audio grezzi del file temporaneo":
   File.Save("/percorso/del/definitivo/file.wav", primo_chunk & $prewav)

End


2a modalità

In quest'altro esempio, mentre viene effettuata la registrazione dei dati audio ottenuti dal Midi, contemporaneamente si avrà l'ascolto dell'audio che si va registrando. Si utilizzerà sia la libreria "libWildMidi" che la libreria di Alsa "libasound".
Inoltre, per l'accumulo dei dati audio (che saranno così registrati ed anche eseguiti) ottenuti dai dati Midi, si utilizzerà un vettore di tipo Byte[], anziché una variabile di tipo Puntatore come nell'esempio precedente:

Library "libWildMidi:1.0.2"

' WildMidi_Init  (const char *config_file, unsigned short int rate, unsigned short int options):
' Intializes "libWildMidi" in preparation for playback.
Private Extern WildMidi_Init(config_file As String, rate As Short, options As Short) As Integer

' int WildMidi_MasterVolume (unsigned char master_volume)
' Sets the overall audio level of the library.
Private Extern WildMidi_MasterVolume(master_volume As Byte) as Integer

' midi *WildMidi_Open (const char *midifile):
' Open  a MIDI file pointed to by midifile for processing. This file must be in standard midi format.
' It returns a handle for the midi file opened. This handle is used by most functions in libWildMidi to identify which midi file we are refering to.
Private Extern WildMidi_Open(midifile As String) As Pointer

' int  WildMidi_GetOutput  (midi *handle, char *buffer, unsigned long insize)
' Places size bytes of audio data from a  handle,  previously  opened  by WildMidi_Open() or WildMidi_OpenBuffer(), into a buffer pointer to by buffer.
' Buffer must be atleast size bytes, with size being a multiple of 4 as the data is stored in 16 bit interleaved stereo format.
Private Extern WildMidi_GetOutput(handle As Pointer, buffer As Pointer, insize As Long) As Integer

' int WildMidi_Close (midi *handle)
' Finish processing MIDI data or file. "handle" the indentifier obtained from opening a midi file with "WildMidi_Open()".
Private Extern WildMidi_Close(handle As Pointer) As Integer

' void WildMidi_Shutdown(void)
' Shuts  down  the wildmidi library, resetting data and freeing up memory used by the library.
Private Extern WildMidi_Shutdown()
 

Library "libasound:2"

' int snd_pcm_open(snd_pcm_t **pcm, const char *name, snd_pcm_stream_t stream, int mode)
' Opens a PCM.
Private Extern snd_pcm_open(pcmP As Pointer, nome As String, stream As Integer, mode As Integer) 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_pcm_set_params (snd_pcm_t *pcm, snd_pcm_format_t format, snd_pcm_access_t access, unsigned int canali, unsigned int rate, int soft_resample, unsigned int latency)
' Set the hardware and software parameters in a simple way.
Private Extern snd_pcm_set_params(pcmP As Pointer, formatB As Byte, accessB As Byte, canali As Integer, rate As Integer, soft_resample As Integer, latency As Integer) As Integer

' snd_pcm_sframes_t snd_pcm_writei(snd_pcm_t *pcm, const void *buffer, snd_pcm_uframes_t size)
' Write interleaved frames to a PCM.
Private Extern snd_pcm_writei(pcmP As Pointer, buffP As Byte[], uframes As Long) As Integer

' int snd_pcm_close(snd_pcm_t *pcm)
' Close PCM handle.
Private Extern snd_pcm_close(pcmP As Pointer) As Integer


Public Sub Form_Open()

 Dim err, somma As Integer
 Dim midi, handle As Pointer
 Dim frequenza As Integer = 44100
 Dim buf As New Byte[128]
 Dim wave As New Byte[]
 Dim fileMid As String
 
 fileMid = "/percorso/del/file.mid"
 
' Va ad inizializzare l'interfaccia PCM di Alsa:
  handle = inizializzaAlsa(frequenza)

  err = WildMidi_Init("/etc/timidity/freepats.cfg", frequenza, 0)
  If err < 0 Then Error.Raise("Errore nell'inizializzazione dell'interfaccia libWildMidi !")

  err = WildMidi_MasterVolume(100)
  If err < 0 Then Error.Raise("Errore nell'impostazione del Volume Generale !")

  midi = WildMidi_Open(fileMid)
  If IsNull(midi) Then Error.Raise("Impossibile aprire il file Midi !")

  Print "Esecuzione del brano Midi: "; File.Name(fileMid); "\n"

  Do
 
    err = WildMidi_GetOutput(midi, buf.Data, 128)

' Ad ogni ciclo aggiunge nel vettore "wave" i dati presenti nel vettore "buf":
    wave.Insert(buf)

' Ad ogni ciclo vengono scritti i dati audio nel sub-sistema PCM di ALsa,
' per consentire l'esecuzione audio di tali dati:
    snd_pcm_writei(handle, buf, 128 / SizeOf(gb.Integer))
    
' Mostra il tempo trascorso dall'inizio dell'esecuzione del file wav:
    somma += (buf.Count \ SizeOf(gb.Integer))
    Write #File.Out, "\rTempo trascorso: " & Date(0, 0, 0, 0, 0, 0, (somma / frequenza) * 1000)

  Loop Until err = 0

  Print "Fine esecuzione audio !"


  generaWAV(wave, fileMid)


' Va in chiusura:
  err = WildMidi_Close(midi)
  If err < 0 Then Error.Raise("Errore nella chiusura del processo dei dati Midi !")
   
  WildMidi_Shutdown()
  
  snd_pcm_close(handle)
 
  Print "Librerie chiuse !"

  Print "************************\n\n"

End


Private Function inizializzaAlsa(frequenza As Integer) As Pointer

 Dim err As Integer
 Dim handle As Pointer
 Dim nomen As String = "default"
 Dim SND_PCM_STREAM_PLAYBACK As Byte = 0
 Dim SND_PCM_ACCESS_RW_INTERLEAVED As Byte = 3
 Dim canali As Byte = 2


' Apre il sub-sistema PCM di Alsa creando un apposito "handle":
  err = snd_pcm_open(VarPtr(handle), nomen, SND_PCM_STREAM_PLAYBACK, 0)
  If err < 0 Then Error.Raise("Playback open error: " & snd_strerror(err))

' Imposta i parametri hardware/software dell'interfaccia PCM di ALsa:
  err = snd_pcm_set_params(handle, 2, SND_PCM_ACCESS_RW_INTERLEAVED, canali, frequenza, 1, 500000)
  If err < 0 Then Error.Raise("Playback open error: " & snd_strerror(err))

  Return handle
  
End


Private Procedure generaWAV(wavArr As Byte[], percorsoFileMID As String)  ' Procedura per generare il file WAV
 
 Dim s1, s2, nomeFileWAV As String


  s1 = creaChunkIniziale(wavArr)
  
  s2 = wavArr.ToString(0, wavArr.Max)
    
  Print "Dimensione dati grezzi audio = "; Len(s2); " byte"
  Print "Dimensione file audio = "; Len(s2) + 40; " byte"
  Print "Durata del brano audio = "; Fix(((CLong(Len(s2)) * 8) / 705600) / 2); " sec.\n"

  nomeFileWAV = File.BaseName(percorsoFileMID)
    
  File.Save("/percorso/del/finale/file" &/ nomeFileWAV & ".wav", s1 & s2)
         
  Print "Termine creazione del file audio: "; nomeFileWAV; ".wav"
  
End


Private Function creaChunkIniziale(wavArr As Byte[]) As String  ' Procedura per generare il chunk iniziale del file WAV finale

 Dim dim_prewav, totale_dimensione, meno_otto As Integer
 Dim chunk, cvr As String
 Dim a As New Byte[]
 Dim j As Byte

 
   dim_prewav = wavArr.Count
 
   totale_dimensione = 44 + dim_prewav

   meno_otto = totale_dimensione - 8

' Si ricava il formato "little-Endian" del valore di "meno_otto":
   cvr = Hex(((meno_otto \ CInt(2 ^ 24)) And &FF) Or ((meno_otto * CInt(2 ^ 8)) And &FF0000) Or ((meno_otto \ CInt(2 ^ 8)) And &FF00&) Or ((meno_otto * CInt(2 ^ 24)) And &FF000000), 8)

   For j = 0 To 3
     a.Add(CByte(Val("&" & Mid(cvr, (2 * j) + 1, 2))))
   Next
 
   cvr = Hex(((dim_prewav \ CInt(2 ^ 24)) And &FF) Or ((dim_prewav * CInt(2 ^ 8)) And &FF0000) Or ((dim_prewav \ CInt(2 ^ 8)) And &FF00&) Or ((dim_prewav * CInt(2 ^ 24)) And &FF000000), 8)

   For j = 0 To 3
     a.Add(CByte(Val("&" & Mid(cvr, (2 * j) + 1, 2))))
   Next
 
   chunk = "RIFF" & Chr(a[0]) & Chr(a[1]) & Chr(a[2]) & Chr(a[3]) & "WAVEfmt" & Chr(&20) & Chr(&10) & Chr(&00) & Chr(&00) & Chr(&00) & Chr(&01) & Chr(&00) & Chr(&02) & Chr(&00)
   chunk &= Chr(&44) & Chr(172) & Chr(&00) & Chr(&00) & Chr(&10) & Chr(&B1) & Chr(&02) & Chr(&00) & Chr(&04) & Chr(&00) & Chr(&10) & Chr(&00) & "data" & Chr(a[4]) & Chr(a[5]) & Chr(a[6]) & Chr(a[7])

   Return chunk

End