Writing SMF Standard MIDI File

Started by Midimaster, December 08, 2023, 16:12:34

Previous topic - Next topic

Midimaster

Writing Standard MIDI Files (SMF, "*.mid")

This tutorial is based on three sources:
https://www.syntaxbomb.com/audio/bb-saving-midi-file-by-b32-1-years-ago/msg2633/#msg2633
http://www.music.mcgill.ca/~ich/classes/mumt306/StandardMIDIfileformat.html
http://www.music.mcgill.ca/~ich/classes/mumt306/SMFformat.html


History

In the BlitzMax archives a found a code snippet which is from 2006 and was written in BlitzPlus.
Because it contains a lot of fix values, it is not really flexible for use and not qualified for learning how to expand it. So I now decided to bring it to BlitzMax NG


The Code

Here is a first runnable code, which produces a MIDI-File of a piano playing a chromatic-scale over 4 octaves.

It will be the base for some lessons in the next days about what happens in the code.

SuperStrict
Const MIDI_TYPE:Int =1
Graphics 800,600
Global Tempo:Int=120
Global Channel:Int[16], Instrument:Int[16]
Global NumTracks:Int = 1
Global TicksPerQuarter:Int = 96

Global MidiBuffer:TBank=CreateBank(9999)
Global MidiAdress: Byte Ptr = MidiBuffer.Lock()
Global Writer:Int
Global TrackCounter:Int


WriteHeader
WriteTempoTrack
WriteAllTracks
SaveMidiSong( "test.mid")
End


Function WriteHeader()
' standard header
MIDI_WriteLine "MThd"

' size header  always 6 bytes
WriteBytes ([0,0,0,6])

' midi Type
WriteBytes ([0, MIDI_TYPE])

' number of tracks
WriteBytes ([0, NumTracks+1])

' time base
WriteBytes ([0 , TicksPerQuarter])
End Function


Function WriteTempoTrack()
'[4] track header
MIDI_WriteLine "MTrk"

' temp. a empty track length
Local LengthPointer:Int = Writer
WriteBytes ([0,0,0,0])
TrackCounter=0

WriteSignature 4,4
WriteTempo Tempo

' End of track
WriteDeltaTime(1) 
WriteBytes ([$FF, $2F, 0])

Print "Trackcounter=" + TrackCounter + " " + Hex(Trackcounter)
' re-write the track length
OverWriteTrackLength LengthPointer, TrackCounter
End Function


Function WriteSignature(Numerator:Int, Denominator:Int)
'time signature: sysex-code "$ff-$58-4" num den metronom? eights?
Local den:Int
Select Denominator
Case 2
den = 1
Case 4
den = 2
Case 8
den = 3
Default
den = 2  ' 4/4
Numerator=4
End Select 

WriteDeltaTime(0)
WriteBytes ([$FF, $58,  4, Numerator, den, 24, 8])
End Function


Function WriteTempo(BPM:Int)
Local tt:Int  = 60000000 / BPM
Local t1:Int  = (tt Shr 16)
Local t2:Int  = (tt Shr 8) Mod 256
Local t3:Int  = (tt Mod 256)

WriteDeltaTime(0) 
WriteBytes ([$FF, $51,  3, t1, t2, t3])
EndFunction


Function OverWriteTrackLength(LengthPointer:Int, TrackLength:Int)
Local t4:Int= TrackLength          Mod 256
Local t3:Int= (TrackLength Shr 8)  Mod 256
Local t2:Int= (TrackLength Shr 16) Mod 256
Local t1:Int= (TrackLength Shr 24) Mod 256
OverWriteBytes LengthPointer, [t1,t2,t3,t4]
End Function


Function WriteDeltaTime( Value:Int)
WriteBytes GetVarLen(Value:Int)
End Function


Function WriteAllTracks()
For Local CurTrack:Int = 0 Until NumTracks
' track header
MIDI_WriteLine "MTrk"

' temp.empty track length
Local LengthPointer:Int = Writer
WriteBytes ([0,0,0,0])
TrackCounter=0

WriteNotes(CurTrack)

' End of track 
WriteDeltaTime(1) 
WriteBytes ([$FF, $2F, 0])
Print "Trackcounter=" + TrackCounter + " " + Hex(Trackcounter)
' re-write the track length
OverWriteTrackLength LengthPointer, TrackCounter
Next
End Function


Function WriteNotes(Track:Int)
For Local note:Int = 32 To 96
'   notes [status]   [byte1]   [byte2]   [delta]
'           on/off     note      volume    time
'  on:
Send $90 + Channel[Track], note, 80, 1
' off:
Send $80 + Channel[Track], note,  0, 31
Next
End Function


Function Send( status:Int,  Note:Int,  Volume:Int, Delta:Int )
WriteDeltaTime Delta 
WriteBytes ([status, Note, Volume])
End Function


Function MIDI_WriteLine( text$ )
For Local i:Int = 0 Until Len(text)
MidiAdress[Writer+i] = text[i]
Next
Writer:+        Len(text)
TrackCounter:+  Len(text)
End Function


Function WriteBytes(Array:Int[])
For Local i:Int = 0 Until Array.length
MidiAdress[Writer+i] = Array[i]
Next
Writer:+        Array.length
TrackCounter:+  Array.length
End Function


Function OverWriteBytes(Position:Int, Array:Int[])
For Local i:Int = 0 Until Array.length
MidiAdress[Position+i] = Array[i]
Next
End Function


Function GetVarLen:Int[](Value:Int)
If value<128 Then Return [value]

Local Array:Byte[4]
Local first:Int
While Value>0
Local b:Byte = value Mod 128
If first>0
b = b|$80
EndIf
array[first] = b
value = value / 128
first:+1
Wend
Local OutPut:Int[first]

For Local i:Int=0 Until first
OutPut[i]= array[first-i-1]
Next
Return OutPut
End Function


Function SaveMidiSong(FileName:String)
Local Out:TStream =WriteStream(FileName)
For Local i:Int=0 Until Writer
out.WriteByte(MidiAdress[i])
Next
CloseStream Out
ShowMidiAsHex
End Function


Function ShowMidiAsHex()
Local t:String
For Local i:Int=0 Until writer
If i Mod 10 = 0
Print t
t= "Pos:" + Right("    " + (i+1),4)+": "
EndIf 
Local value:Int = MidiAdress[i]
t=t + "  " + hex2(Value)
Next
End Function


Function Hex2$(Value:Int)
Return Hex(Value)[6..]
End Function
...back from North Pole.

Midimaster

The header chunk

Each MIDI-file has one header, where the fundamental properties are stored. This header has a size of 14 bytes.

The first 4 Bytes are the identifier "MThd" or hex:
4D-54-68-64

...followed by INTEGER value (4 Bytes) which inform about the size of the content part of header (which means without the 4 identifier-bytes, and the 4 size-bytes). So the size is always...
00-00-00-06 

Now follow 3 parameters, each two bytes long:

Midi-Format 00-01
possible values are 00-00 or 00-01 or 00-02
which have influence to the design of the tracks-section. In my tutorial, I use Format 1, which means the file has several independent Tracks. The minimum is 2 tracks: a Tempo-Information-track and minimum one Music Track.


Number of Tracks 00-xx
This is minimum 2 if you use Format 1: a Tempo-Information-track and minimum one Music Track.


Time-Base 00-xx
This informs about the "resolution" of the data and how the delta timing is to be interpreted. A typical value is 96 or hex: 60, which means a delta of 96 represents a Quarter note.  Means a Eights would be 48 and a Half note has a length of 192.


The track chunk

Each track start with the 8 "header"-bytes:

The first 4 Bytes are the identifier "MTrk" or hex:
4D  54  72  6B


...followed by INTEGER value (4 Bytes) which inform about the size of the content part of header (which means without the 4 identifier-bytes, and the 4 size-bytes). The size is related to the amount of data that will follow now. So we cannot write the size until we finished adding the data-part. At the end we will return to this point to re-write them. At the moment we add:
00-00-00-00


Now follow the data Bytes
xx-xx-xx-xx-....


At the end of each track we need a 4-byte "end-of-track"-flag,(... which will increase the number of data and need to be added to the size-Parameter!!!)
01-FF-2F-00

The 01 in this sequence is a "DELTA-TIME". We will hear more about it later.





...back from North Pole.

Midimaster

#2
The Tempo-Track

The first track in a Format-1-Song need to be an "empty" track (means: no music). It contains only information about time signature and tempo.

Function WriteTempoTrack()
        '[4] track header
        MIDI_WriteLine "MTrk"
       
        ' temp. a empty track length
        Local LengthPointer:Int = Writer
        WriteBytes ([0,0,0,0])
        TrackCounter=0

        WriteSignature 4,4
        WriteTempo Tempo

        ' End of track
        WriteDeltaTime(1) 
        WriteBytes ([$FF, $2F, 0])
       
        Print "Trackcounter=" + TrackCounter + " " + Hex(Trackcounter)
        ' re-write the track length
        OverWriteTrackLength LengthPointer, TrackCounter
End Function

For creating the MIDI-File I use a TBANK and a Pointer-approach to fill it with the values.

A global variable Writer counts the bytes we already have written. It will be increased by the functions WriteBytes() and MIDI_WriteLine()
As a second counter, we start/reset TrackCounter inside the function to count the bytes. It will also be increased by the functions WriteBytes() and MIDI_WriteLine()

To find back to the 4 "size"-bytes, we define a pointer LengthPointer. At the end of the function we can insert here the real amount of bytes (TrackCounter) we used for the data.



The Tempo-Block

Function WriteTempo(BPM:Int)
        Local tt:Int  = 60000000 / BPM
        Local t1:Int  = (tt Shr 16)
        Local t2:Int  = (tt Shr 8) Mod 256
        Local t3:Int  = (tt Mod 256)

        WriteDeltaTime(0) 
        WriteBytes ([$FF, $51,  3, t1, t2, t3])
EndFunction

The tempo needs to be calculated in msecs/Quarternote and converted into 3 bytes. The data sequence FF-51-3-...  is a META-EVENT-Message. (About this mysterious Delta-time more to come...)




The Signature-Block

Function WriteSignature(Numerator:Int, Denominator:Int)
        'time signature: sysex-code "$ff-$58-4" num den metronom? eights?
        Local den:Int
        Select Denominator
            Case 2
                den = 1
            Case 4
                den = 2           
            Case 8
                den = 3           
            Default
                den = 2  ' 4/4
                Numerator=4
        End Select 

        WriteDeltaTime(0)
        WriteBytes ([$FF, $58,  4, Numerator, den, 24, 8])
End Function

I did not find complete information about this block. The data sequence FF-58-4-...  is again a META-EVENT-Message. Here we save the 4/4 or 3/4 or 6/8 measure of our song. The denominator needs a calculation to be conformed. The last two values need more exploration.



The DELTA-Time

This defines the time distance of the current event to the last event. (If you used time base 96 in the global MIDI-Header,...) a Delta-time of 96 would exactly wait for a Quarter note to fire this event after the previous event. As long as your DELTA-Time is below 127, it needs only one byte in the Midi-file. If the time distance is bigger, this needs a heavy calculation, because DELTA-values need to be divided in 7bit-Byte-sequences.


Function WriteDeltaTime( Value:Int)
    WriteBytes GetVarLen(Value:Int)
End Function

Function GetVarLen:Int[](Value:Int)
        If value<128 Then Return [value]

        Local Array:Byte[4]
        Local first:Int   
        While Value>0
            Local b:Byte = value Mod 128
            If first>0
                b = b|$80
            EndIf
            array[first] = b
            value = value / 128
            first:+1
        Wend
        Local OutPut:Int[first]
       
        For Local i:Int=0 Until first
            OutPut[i]= array[first-i-1]
        Next
        Return OutPut
End Function

This means each DeltaTime-value can have the length of 1 to 4 Bytes in the midi-file.




...back from North Pole.

Midimaster

The Music Tracks

The next tracks in a Format-1-Song are music tracks. They contain information about Note-On/Offs, Control changes, Program changes, etc...

The format is the same as in the Tempo Track:

Function WriteAllTracks()
For Local CurTrack:Int = 0 Until NumTracks
' track header
MIDI_WriteLine "MTrk"

' temp.empty track length
Local LengthPointer:Int = Writer
WriteBytes ([0,0,0,0])
TrackCounter=0

WriteNotes(CurTrack)

' End of track 
WriteDeltaTime(1) 
WriteBytes ([$FF, $2F, 0])
Print "Trackcounter=" + TrackCounter + " " + Hex(Trackcounter)
' re-write the track length
OverWriteTrackLength LengthPointer, TrackCounter
Next
End Function


Function WriteNotes(Track:Int)
For Local note:Int = 32 To 96
'   notes [status]   [byte1]   [byte2]   [delta]
'           on/off     note      volume    time
'  on:
Send $90 + Channel[Track], note, 80, 1
' off:
Send $80 + Channel[Track], note,  0, 31
Next
End Function

Function Send( status:Int,  Note:Int,  Volume:Int, Delta:Int )
WriteDeltaTime Delta 
WriteBytes ([status, Note, Volume])
End Function

In this first version of the MIDI-File-creator we have only a simulation of notes. We send 48 notes of a chromatic scale.

Each note has two events: Note-On and Note-Off. Both need to store a DeltaTime before we store the message. 1 means something like "immediately" and 31 means a Triplet Eights. Our time base for Quarters is 96, and the sum from 1+31=32. So 32 this is a third of the Quarter Note (96/3=32). In the next lesson we will see how to calculate real time Millisecs() into DeltaTime



Note-On Note-Off

The NOTE-On command is a combination of $90 and a MIDI-Channel between 0 and 15 (in hex: $00 to $0F). In a second byte the note follows $00 to $7F. The third byte defines the volume of this note $00 to $7F

The NOTE-OFF command is a combination of $80 and a MIDI-Channel between 0 and 15 (in hex: $00 to $0F). In a second byte the note follows $00 to $7F. The third is $00. It is also allowed (and often used) to send the end of a note with a NOTE-ON command, which has a volume of $00.






...back from North Pole.

Midimaster

#4
Recording MIDI from an external Keyboard

BlitzMax NG offers per default a complete MIDI communication module audio.midi.

We connect a MIDI Keyboard to the Computer via USB.

To start the communication we need to...

  • Define an instance of TMidiIn
  • Check, if there is minimum one MIDI-In-Port available
  • Try to open this port

Type MIDI
    Global MidiIn:TMidiIn
   
    Function StartUp()

        ' step 1: define an instance
        MidiIn = New TMidiIn.Create()
       
        ' step 2: search for a port
        If MidiIn.GetPortCount() > 0
            Print "avaiable  In-Device:" + MidiIn.GetPortName(0)
        Else
            Print " NO In-Device!"
        EndIf
       
        ' step 3: open a port
        If Midi.TryCatch(0)=True
            Print "all ok"
        Else
            Print " Cannot Open Device!"
        EndIf
    End Function


    Function TryCatch:Int(Port:Int)
        Try
            MidiIn.openPort(Port)
            Return True   
        Catch Exception:Object
            Print "PROBLEM WITH MIDI *IN* PORT" + Exception.ToString()
        End Try
        Return False
    End Function


If this was successful, we can now continuously check if there are any Midi-Events for us and store them in a TList:

....
Repeat
    Cls
    MIDI.Check
    Flip
Until AppTerminate()
...
Type TMidiMessage
    Global List:TList = New TList
    Field Size:Int, Typ:Int, Channel:Int, Note:Int, Volume:Int, Delta:Double
   
    Function Add( Event:Byte[], delta:Double)
        Local loc:TMidiMessage=New TMidiMessage 
        For Local i:Int = 0 Until Event.length
            Local v:Int = Event[i]           
            Select i
                Case 0
                    loc.Typ     = v & $F0
                    loc.Channel = v & $0F
                    loc.Delta   = delta
                    loc.Size    = Event.length
                Case 1
                    loc.Note    = v
                Case 2
                    loc.Volume  = v
            End Select
        Next
        List.AddLast loc
        loc.Show
    End Function
End Type

Type MIDI
    ....
    Function Check()
        Local delta:Double
        Local Event:Byte[] = midiIn.GetMessage(delta)
       
        If Event.length=0 Return
        TMidiMessage.Add Event, delta
    End Function   
End Type

A MIDI-Event has a length of 1, 2 or 3 bytes and got a timestamp, when it reached the computer. The timestamp tells us the time, that passed since the last MIDI-Event. Independent of when we opened the Port, the very first MIDI-Event gets the timestamp 0.


Here is the complete code:

SuperStrict
Import audio.midi

Graphics 800,600
MIDI.Startup

Repeat
    Cls
    MIDI.Check
    Flip
Until AppTerminate()
MIDI.Close


Type TMidiMessage
    Global List:TList = New TList

    Field Size:Int, Typ:Int, Channel:Int, Note:Int, Volume:Int, Delta:Double
   
    Function Add( Event:Byte[], delta:Double)
        Local loc:TMidiMessage=New TMidiMessage 
        For Local i:Int = 0 Until Event.length
            Local v:Int = Event[i]           
            Select i
                Case 0
                    loc.Typ     = v & $F0
                    loc.Channel = v & $0F
                    loc.Delta   = delta
                    loc.Size    = Event.length
                Case 1
                    loc.Note    = v
                Case 2
                    loc.Volume  = v
            End Select
        Next
        List.AddLast loc
        loc.Show
    End Function

    Method Show()
        Print "Message (" + Size + ") after " + Int(Delta*1000) + "msec ---> T:" + Hex2(Typ) + "   C:" + Channel + "   N:" + Note + "   V:" + Volume
    End Method
End Type


Type MIDI
    Const OFF:Int=0, NO_DEVICE:Int=1, CLOSED:Int=2, OPEN:Int=3
    Global MidiIn:TMidiIn, InStarted:Int
   
    Function StartUp()
        ' step 1: define an instance
        If InStarted = OFF Then
            MidiIn = New TMidiIn.Create()
            InStarted = NO_DEVICE
        EndIf
       
        ' step 2: search for a port
        If InStarted = NO_DEVICE
            If (MidiIn.GetPortCount() > 0)
                Print "avaiable  In-Device:" + MidiIn.GetPortName(0)
                InStarted = CLOSED
            Else
                Print " NO In-Device!"
            EndIf
        EndIf
       
        ' step 3: open a port
        If InStarted = CLOSED
            If Midi.TryCatch(0)=True
                Print " Take In-Device:" + MidiIn.GetPortName(0)
                InStarted = OPEN
            Else
                Print " Cannot Open Device!"
            EndIf
        EndIf
    End Function


    Function Close()
        If InStarted>0
            MidiIn.ClosePort
              MidiIn.free()
            InStarted=0
        EndIf
    End Function


    Function TryCatch:Int(Port:Int)
        Try
            MidiIn.openPort(Port)
            Return True   
        Catch Exception:Object
            Print "PROBLEM WITH MIDI *IN* PORT" + Exception.ToString()
            Notify "MIDI IN PROBLEM with " + midiIn.getPortName(Port)   
        End Try
        Return False
    End Function


    Function Check()
        If InStarted<OPEN Return
       
        Local delta:Double
        Local Event:Byte[] = midiIn.GetMessage(delta)
       
        If Event.length=0 Return
        TMidiMessage.Add Event, delta
    End Function   
End Type


Function Hex2$(Value:Int)
        Return Hex(Value)[6..]
End Function

...back from North Pole.

Midimaster

Complete Piano-MIDI-Recorder

Here is the final step. We combine the MIDI-Recording-Device with the MIDI-File-Saver.

We only want to create a minimalistic real time E-Piano recording software. A musician with no skills in MIDI wants to
record his handmade melody and store it as MIDI-File. He has no ambitions to edit the song afterwards or to display its score.

Therefore we need no META-Events like TEMPO or SIGNATURE. And we need no multi-tracks. So this is the best opportunity to learn MIDI-FORMAT 0


MIDI-Format 0

MIDI-Format 0 has only one track, where you can store all informations. 


Real Time Delta to MIDI-File-Delta

The values of the incoming Delta differs from the MIDI-FILE Delta! The incoming Delta reports real time seconds. But the MidiFile Delta expects note related Delta.

In our Song with default tempo=120 a Quarter note lasts 0.5sec real time and equals 96 ticks in the MidiFile. So we need to calculate

DeltaInFile = DeltaInReal x 2 x 96


Only this is new:

Type MIDIFile
...
Function WriteSingleTrack(List:TList)
' track header
MIDI_WriteLine "MTrk"

' temp.empty track length
Local LengthPointer:Int = Writer
WriteBytes ([0,0,0,0])
TrackCounter=0

WriteNotes List

' End of track 
WriteDeltaTime(96) 
WriteBytes ([$FF, $2F, 0])
' re-write the track length
OverWriteTrackLength LengthPointer, TrackCounter
End Function


Function WriteNotes(List:TList)
Local deltafactor:Double = TICKS_PER_QUARTER * 2

For Local Message:TMidiMessage = EachIn List
Local Delta:Int = Message.Delta * deltafactor
Select Message.Size
Case 2
Write_Message (Message.Typ + Message.Channel), Message.Note, Delta
Case 3
Write_Message (Message.Typ + Message.Channel), Message.Note, Message.Volume, Delta
End Select
Next
End Function


Function Write_Message( Status:Int,  Note:Int,  Volume:Int, Delta:Int )
WriteDeltaTime Delta 
WriteBytes ([Status, Note, Volume])
End Function


Function Write_Message( Status:Int,  Value:Int, Delta:Int )
WriteDeltaTime Delta 
WriteBytes ([Status, Value])
End Function

WriteNotes() now uses the TList, which we filled in the TMidiMessage type. For the two different Message-lengths we use the same function Write_Message() twice. One has 4 the other has 3 parameters.

Here is the complete code:

SuperStrict
Import audio.midi
Graphics 800,600
MIDI.Startup

Repeat
Cls
MIDI.Check
Flip 0
Until AppTerminate()
MIDI.Close
MIDIFile.SaveSong "test.mid", TMidiMessage.List
End


Type MIDIFile
Const MIDI_TYPE:Int = 0 , NUM_TRACKS:Int = 1, TICKS_PER_QUARTER:Int = 96

Global MidiBuffer:TBank=CreateBank(99999)
Global MidiAdress: Byte Ptr = MidiBuffer.Lock()
Global Writer:Int , TrackCounter:Int

Function SaveSong(FileName:String, List:TList)
WriteHeader
WriteSingleTrack List
SaveMidiSong FileName
End Function


Function WriteHeader()
' standard header
MIDI_WriteLine "MThd"

' size header  always 6 bytes
WriteBytes ([0,0,0,6])

' midi Type
WriteBytes ([0, MIDI_TYPE])

' number of tracks
WriteBytes ([0, NUM_TRACKS])

' time base
WriteBytes ([0 , TICKS_PER_QUARTER])
End Function



Function OverWriteTrackLength(LengthPointer:Int, TrackLength:Int)
Local t4:Int= TrackLength          Mod 256
Local t3:Int= (TrackLength Shr 8)  Mod 256
Local t2:Int= (TrackLength Shr 16) Mod 256
Local t1:Int= (TrackLength Shr 24) Mod 256
OverWriteBytes LengthPointer, [t1,t2,t3,t4]
End Function


Function WriteDeltaTime( Value:Int)
WriteBytes GetVarLen(Value:Int)
End Function


Function WriteSingleTrack(List:TList)
' track header
MIDI_WriteLine "MTrk"

' temp.empty track length
Local LengthPointer:Int = Writer
WriteBytes ([0,0,0,0])
TrackCounter=0

WriteNotes List

' End of track 
WriteDeltaTime(96) 
WriteBytes ([$FF, $2F, 0])
' re-write the track length
OverWriteTrackLength LengthPointer, TrackCounter
End Function


Function WriteNotes(List:TList)
Local deltafactor:Double = TICKS_PER_QUARTER * 2

For Local Message:TMidiMessage = EachIn List
Local Delta:Int = Message.Delta * deltafactor
Select Message.Size
Case 2
Write_Message (Message.Typ + Message.Channel), Message.Note, Delta
Case 3
Write_Message (Message.Typ + Message.Channel), Message.Note, Message.Volume, Delta
End Select
Next
End Function


Function Write_Message( Status:Int,  Note:Int,  Volume:Int, Delta:Int )
WriteDeltaTime Delta 
WriteBytes ([Status, Note, Volume])
End Function


Function Write_Message( Status:Int,  Value:Int, Delta:Int )
WriteDeltaTime Delta 
WriteBytes ([Status, Value])
End Function


Function MIDI_WriteLine( text$ )
For Local i:Int = 0 Until Len(text)
MidiAdress[Writer+i] = text[i]
Next
Writer:+        Len(text)
TrackCounter:+  Len(text)
End Function


Function WriteBytes(Array:Int[])
For Local i:Int = 0 Until Array.length
MidiAdress[Writer+i] = Array[i]
Next
Writer:+        Array.length
TrackCounter:+  Array.length
End Function


Function OverWriteBytes(Position:Int, Array:Int[])
For Local i:Int = 0 Until Array.length
MidiAdress[Position+i] = Array[i]
Next
End Function


Function GetVarLen:Int[](Value:Int)
If value<128 Then Return [value]

Local Array:Byte[4]
Local first:Int
While Value>0
Local b:Byte = value Mod 128
If first>0
b = b|$80
EndIf
array[first] = b
value = value / 128
first:+1
Wend
Local OutPut:Int[first]

For Local i:Int=0 Until first
OutPut[i]= array[first-i-1]
Next
Return OutPut
End Function


Function SaveMidiSong(FileName:String)
Local Out:TStream =WriteStream(FileName)
For Local i:Int=0 Until Writer
out.WriteByte(MidiAdress[i])
Next
CloseStream Out
ShowMidiAsHex
End Function


Function ShowMidiAsHex()
Local t:String
For Local i:Int=0 Until writer
If i Mod 10 = 0
Print t
t= "Pos:" + Right("    " + (i+1),4)+": "
EndIf 
Local value:Int = MidiAdress[i]
t=t + "  " + hex2(Value)
Next
End Function

End Type




Type TMidiMessage
Global List:TList = New TList

Field Size:Int, Typ:Int, Channel:Int, Note:Int, Volume:Int, Delta:Double

Function Add( Event:Byte[], delta:Double)
Local loc:TMidiMessage=New TMidiMessage 
For Local i:Int = 0 Until Event.length
Local v:Int = Event[i]
Select i
Case 0
loc.Typ     = v & $F0
loc.Channel = v & $0F
loc.Delta   = delta
loc.Size    = Event.length
Case 1
loc.Note    = v
Case 2
loc.Volume  = v
End Select
Next
List.AddLast loc
loc.Show
End Function

Method Show()
Print "Message (" + Size + ") after " + Int(Delta*1000) + "msec ---> T:" + Hex2(Typ) + "   C:" + Channel + "   N:" + Note + "   V:" + Volume
End Method
End Type



Type MIDI
Const OFF:Int=0, NO_DEVICE:Int=1, CLOSED:Int=2, OPEN:Int=3

Global MidiIn:TMidiIn, InStarted:Int

Function StartUp()
If InStarted = OFF Then
MidiIn = New TMidiIn.Create()
InStarted = NO_DEVICE
EndIf

If InStarted = NO_DEVICE
If (MidiIn.GetPortCount() > 0)
Print "avaiable  In-Device:" + MidiIn.GetPortName(0)
InStarted = CLOSED
Else
Print " NO In-Device!"
EndIf
EndIf

If InStarted = CLOSED
If Midi.TryCatch(0)=True
Print " Take In-Device:" + MidiIn.GetPortName(0)
InStarted = OPEN
Else
Print " Cannot Open Device!"
EndIf
EndIf
End Function


Function Close()
If InStarted>0
MidiIn.ClosePort
  MidiIn.free()
InStarted=0
EndIf
End Function



Function TryCatch:Int(Port:Int)
Try
MidiIn.openPort(Port)
Return True
Catch Exception:Object
Print "PROBLEM WITH MIDI *IN* PORT" + Exception.ToString()
Notify "MIDI IN PROBLEM with " + midiIn.getPortName(Port)
End Try
Return False
End Function




Function Check:Int()
If InStarted<OPEN Return False

Local delta:Double
Local Event:Byte[] = midiIn.GetMessage(delta)

If Event.length=0 Return False
TMidiMessage.Add Event, delta
Return True
End Function
End Type


Function Hex2$(Value:Int)
Return Hex(Value)[6..]
End Function

 
...back from North Pole.

Baggey

#6
Awsome ;D

QuoteBlitzMax NG offers per default a complete MIDI communication module audio.midi.

I dont have this module audio.midi as default? So where can i download it Please?

Kind Regards Baggey
Running a PC that just Aint fast enough!? i7 4Ghz Quad core 32GB ram  2x1TB SSD and NVIDIA Quadro K1200 on Acer 24" . DID Technology stop! Or have we been assimulated!

Windows10, Parrot OS, Raspberry Pi Black Edition! , ZX Spectrum 48k, C64, Enterprise 128K, The SID chip. Im Misunderstood!

Midimaster

The module is part of BlitzMax NG since this official RELEASE BlitzMax_0.136.3.51

Update the complete BlitzMax here: https://blitzmax.org/downloads/

The single module is available here: https://github.com/bmx-ng/audio.mod
...back from North Pole.

Midimaster

Here is a ready to use app, which records MIDI from pianos in a single track and can play it back to the piano. You can also save your music as a Standard MIDI File.

https://www.midimaster.de/downloads/MidiPianoRecorder.exe


Player.gif

Of course, this is no complex MIDI-Sequencer, but a simple-to-use tool for musicians to record a piano song. It offers no selection of the devices, but take the first IN-Device it will find for RECORDING. And the last OUT-Device in the device-list FOR PLAYBACK. Mostly these devices are the external ones.

At minimum you need one IN-Device. If it is not available the app will wait until you connect it. The app cannot handle sudden disconnection of the device during recording or playback, etc...


...back from North Pole.

Midimaster

Playback recorded MDI

This lesson demonstrates how to play events back to the MIDI keyboard

As a first step we need to add the device related stuff to the MIDI Type. This is nearly the same as we wrote for MIDI-IN

To start the communication we need to...

  • Define an instance of TMidiOut
  • Check, if there is minimum one MIDI-Out-Port available
  • Try to open this port

Type MIDI
Const OFF:Int=0, NO_DEVICE:Int=1, CLOSED:Int=2, OPEN:Int=3

Global MidiIn:TMidiIn, InStarted:Int
Global MidiOut:TMidiOut, OutStarted:Int

Function StartUp()  ' is responsible for MIDI-IN
....
End Function

Function TryCatch:Int(Port:Int) ' is responsible for MIDI-IN
...
End Function

Function StartUp_OUT() ' new: responsible for MIDI-OUT
If OutStarted = OFF Then
MidiOut = New TMidiOut.Create()
OutStarted = NO_DEVICE
EndIf

If OutStarted = NO_DEVICE
If (MidiOut.GetPortCount() > 0)
For Local i:Int=0 Until MidiOut.GetPortCount()
Print " Available MIDI OUT Devices: " + MidiOut.GetPortName(i)
Next
OutStarted = CLOSED
Else
Print " NO MIDI-OUT-Device!"
EndIf
EndIf

If OutStarted = CLOSED
Local TakeThis:Int =   MidiOut.GetPortCount()-1
If Midi.TryCatchOut(0)=True
Print " Take OUT-Device:" + MidiOut.GetPortName(TakeThis)
OutStarted = OPEN
Else
Print " Cannot Open MIDI-OUT-Device!"
EndIf
EndIf
End Function

Function TryCatchOut:Int(Port:Int)
Try
MidiOut.OpenPort(Port)
Return True
Catch Exception:Object
Print "PROBLEM WITH MIDI *OUT* PORT" + Exception.ToString()
Notify "MIDI OUT PROBLEM with " + midiOut.GetPortName(Port)
End Try
Return False
End Function


Function Close()
If InStarted>0
...
EndIf
If OutStarted>0
MidiOut.ClosePort
  MidiOut.free()
OutStarted=OFF
EndIf
End Function

You can write additional code lines to enable the user to select a certain MIDI-OUT port. In our example we will always use the very last in the list. The first one is often the
default Microsoft MIDI-Mapper and the last one is often the external music instrument.


And this functions is for Playback.

The function Play() fetches a single future event from the data stack inside TMIDIMessage. The function GetNextEvent() return the event together with the delta-time after this event needs to be fired. So Play() calculates the time, when we will fire this event and stores
it in a global variable Timer:Int. During the following loops, the function checks whether it's time to fire now.
When the time has come, it fires the event to the MIDI-OUT-port and fetches the next future event from the stack.


Function Check()
If InStarted=OPEN
Record
ElseIf OutStarted=OPEN
Play
EndIf
End Function


Function Record:Int() ' same as prior Check(): responsible for MIDI-OUT
If InStarted<OPEN Return False

Local delta:Double
Local Event:Byte[] = midiIn.GetMessage(delta)
If TMIDIMessage.List.count()=0 Then delta=0
If Event.length=0 Return False
TMidiMessage.Add Event, delta
Return True
End Function


Function Play()
Global Timer:Int, NextDelta:Int, Event:Byte[]
If OutStarted<OPEN Return
If Timer>MilliSecs() Return

If Event<>Null
MidiOut.PutMessage(Event, Event.length)
EndIf

Event=TMIDIMessage.GetNextEvent(NextDelta)
If Event=Null
Close
Else
Timer=MilliSecs() + NextDelta
EndIf
End Function
End Type
 
   

We stored the recorded events in a TList inside TMIDIMessage. The function GetNextEvent() prepares a 2- or  a 3-byte Array, which MidiOut.PutMessage() needs to send it via MIDI. Additional the functions return the related DELTA in a ByVAR parameter


Type TMidiMessage
Global List:TList = New TList
Global PlayIndex:Int

Field Size:Int, Typ:Int, Channel:Int, Note:Int, Volume:Int, Delta:Double

....

Function GetNextEvent:Byte[](Delta:Int Var )
If PlayIndex= List.count()
PlayIndex=0
Return Null
EndIf

Local loc:TMidiMessage = TMidiMessage(List.ValueAtIndex(PlayIndex))
PlayIndex:+1
Local Event:Byte[]
If loc.Size=2
Event    = New Byte[2]
Else
Event    = New Byte[3]
Event[2] = loc.Volume
EndIf
Event[0] = loc.Typ + loc.Channel
Event[1] = loc.Note
Delta    = loc.Delta*1000
Return Event
End Function
End Type
We need to re-calculate the delta time. TMidiIn gave us real seconds as a DOUBLE, but we will check the Playback timing with the Millisecs()


What is next?


This code does not enable to read third-party SMF-File. At the moment we can only send back our recorded events. The next lesson will show how to playback the (self-made) MIDI-Files too.




...back from North Pole.