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.

Code: BASIC
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 
...on the way to China.

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.





...on the way to China.

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.

Code: BASIC
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

Code: BASIC
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

Code: BASIC
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.


Code: BASIC
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.




...on the way to China.

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:

Code: BASIC
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.






...on the way to China.

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

Code: BASIC
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:

Code: BASIC
....
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:

Code: BASIC
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 

...on the way to China.

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:

Code: BASIC
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:

Code: BASIC
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 

 
...on the way to China.

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 2 x HP Z24's . 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
...on the way to China.

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


...on the way to China.

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

Code: BASIC
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.

Code: BASIC

	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


Code: BASIC
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.




...on the way to China.