@chris319
Many thanks for your very interesting MIDI program.
However, there are a few minor shortcomings. Like having to specifiy the octave and duration for every note even when identical with the preceding note. Also, the inability to set the tempo or volume from within the MIDI tune.
I decided to rehash your program:
Code:
; Midi_Text_Player 17-Dec-07
; Version 1.2AKJ
; Written by Chris319 and modified by AKJ
; Copyright (C) 2007 Chris Clementson
; www.purebasic.fr/english/viewtopic.php?t=29344
; For related notes, see:
; www.borg.com/~jglatt/tech/lowmidi.htm
; For a MIDI file decoder written in PureBasic 3, see:
; www.freesoundeditor.com/incageneng.html?DownloadSrceseng.htm~main
; (AKJ has a PureBasic 4.10 version of it)
;128 voices
;1. Acoustic Grand Piano 65. Soprano Sax
;2. Bright Acoustic Piano 66. Alto Sax
;3. Electric Grand Piano 67. Tenor Sax
;4. Honky-tonk Piano 68. Baritone Sax
;5. Electric Piano 1 69. Oboe
;6. Electric Piano 2 70. English Horn
;7. Harpsichord 71. Bassoon
;8. Clavi 72. Clarinet
;9. Celesta 73. Piccolo
;10. Glockenspiel 74. Flute
;11. Music Box 75. Recorder
;12. Vibraphone 76. Pan Flute
;13. Marimba 77. Blown Bottle
;14. Xylophone 78. Shakuhachi
;15. Tubular Bells 79. Whistle
;16. Dulcimer 80. Ocarina
;17. Drawbar Organ 81. Lead 1 (square)
;18. Percussive Organ 82. Lead 2 (sawtooth)
;19. Rock Organ 83. Lead 3 (calliope)
;20. Church Organ 84. Lead 4 (chiff)
;21. Reed Organ 85. Lead 5 (charang)
;22. Accordion 86. Lead 6 (voice)
;23. Harmonica 87. Lead 7 (fifths)
;24. Tango Accordion 88. Lead 8 (bass + lead)
;25. Acoustic Guitar (nylon) 89. Pad 1 (new age)
;26. Acoustic Guitar (steel) 90. Pad 2 (warm)
;27. Electric Guitar (jazz) 91. Pad 3 (polysynth)
;28. Electric Guitar (clean) 92. Pad 4 (choir)
;29. Electric Guitar (muted) 93. Pad 5 (bowed)
;30. Overdriven Guitar 94. Pad 6 (metallic)
;31. Distortion Guitar 95. Pad 7 (halo)
;32. Guitar harmonics 96. Pad 8 (sweep)
;33. Acoustic Bass 97. FX 1 (rain)
;34. Electric Bass (finger) 98. FX 2 (soundtrack)
;35. Electric Bass (pick) 99. FX 3 (crystal)
;36. Fretless Bass 100. FX 4 (atmosphere)
;37. Slap Bass 1 101. FX 5 (brightness)
;38. Slap Bass 2 102. FX 6 (goblins)
;39. Synth Bass 1 103. FX 7 (echoes)
;40. Synth Bass 2 104. FX 8 (sci-fi)
;41. Violin 105. Sitar
;42. Viola 106. Banjo
;43. Cello 107. Shamisen
;44. Contrabass 108. Koto
;45. Tremolo Strings 109. Kalimba
;46. Pizzicato Strings 110. Bag pipe
;47. Orchestral Harp 111. Fiddle
;48. Timpani 112. Shanai
;49. String Ensemble 1 113. Tinkle Bell
;50. String Ensemble 2 114. Agogo
;51. SynthStrings 1 115. Steel Drums
;52. SynthStrings 2 116. Woodblock
;53. Choir Aahs 117. Taiko Drum
;54. Voice Oohs 118. Melodic Tom
;55. Synth Voice 119. Synth Drum
;56. Orchestra Hit 120. Reverse Cymbal
;57. Trumpet 121. Guitar Fret Noise
;58. Trombone 122. Breath Noise
;59. Tuba 123. Seashore
;60. Muted Trumpet 124. Bird Tweet
;61. French Horn 125. Telephone Ring
;62. Brass Section 126. Helicopter
;63. SynthBrass 1 127. Applause
;64. SynthBrass 2 128. Gunshot
EnableExplicit
Global hMidiOut
Procedure MidiOutMessage(hMidi, iStatus, iChannel, iData1, iData2)
; Typically iData1 is the note and iData2 is the velocity (volume)
Protected dwMessage
dwMessage = iStatus | iChannel | (iData1 << 8 ) | (iData2 << 16)
ProcedureReturn midiOutShortMsg_(hMidi, dwMessage)
EndProcedure
Procedure SetInstrument(channel, instrument)
MidiOutMessage(hMidiOut, $C0, channel, instrument, 0)
EndProcedure
Procedure StopNotes(channels$, note)
Protected p, channel
For p = 1 To Len(channels$)
channel = Asc(Mid(channels$, p, 1))-48
If channel>9: channel-7: EndIf ; Channel # in decimal 1..15
MidiOutMessage(hMidiOut, $90, channel, note, 0) ; Volume = 0
Next p
EndProcedure
Procedure PlayNotes(channels$, note, volume)
Protected p, channel
Static previous_channels$, previous_note
StopNotes(previous_channels$, previous_note)
For p = 1 To Len(channels$)
channel = Asc(Mid(channels$, p, 1))-48
If channel>9: channel-7: EndIf ; Channel # in decimal 1..15
MidiOutMessage(hMidiOut, $90, channel, note, volume)
Next p
previous_channels$ = channels$: previous_note = note
;!!! Debug "Note = "+Str(note)
EndProcedure
NewList Note$()
Define tempo, beat.f, dur.f, measure_beats.f=0.0
Define tempodef, instrumdef, octavedef, durdef.f, volumedef
Define midi.MIDIOUTCAPS, devices, devnum, midiport, *hMidiOut
Define startfound.b, line_count, ln$, inp$, p
Define instrum$, instrum, instrument, channel, channels$
Define transpose, octave, octavedelta, note, c$, dotted.b, volume
; Set defaults (all values must be valid)
tempodef = 120 ; Beats per minute
instrumdef = 1 ; Acoustic Grand Piano
octavedef = 5 ; Default octave is one above that with middle C
durdef = 1.0 ; Default duration = quarter note
volumedef = 5 ; Default volume (0..9)
; Enumerate MIDI devices
devices = midiOutGetNumDevs_()
Debug "There are "+Str(devices)+" MIDI devices available"
For devnum = -1 To devices-1 ; -1 is for the MIDI Mapper virtual device
If midiOutGetDevCaps_(devnum, @midi, SizeOf(MIDIOUTCAPS))=0
If midi\wVoices > 0
midiport = devnum
Break
EndIf
EndIf
Next devnum
Debug "Using MIDI device "+Str(midiport)+": "+PeekS(@midi\szPname,32)
If midiOutOpen_(@hMidiOut, midiport, 0, 0, 0)<>#MMSYSERR_NOERROR
MessageRequester("Error", "Unable to open MIDI output", #MB_ICONERROR)
End
EndIf
; Read the MIDI text file
Debug ""
If OpenFile(1, "midi.txt")=0
MessageRequester("Error", "Failed to open MIDI text file", #MB_ICONERROR)
End
EndIf
; Build linked list of notes/rests/barlines and instrument channels
; Acceptable syntax includes these and more:
; C Note C with current tempo, octave, duration, volume
; c5 Use octave 5
; CQ Duration = quarter note
; CQ>> Up 2 octaves
; C##5q C double-sharp
; Cq5# Order-independent
; Cb5q| C flat and trailing barline
; CB5+QD Louder and with duration = dotted quarter note
; Cb.,10,74 Instruments = glockenspiel and flute in unison
; RW. Rest with duration = dotted whole note
; T150 Tempo = 150 beats per minute
; Apart from the leading letter, the code characters can be in any order
instrum$="" ; List of instrument numbers, each held in 1 byte
startfound = #False ; True when tune found
line_count = 0
ln$ = ReadString(1)
While Not Eof(1)
line_count + 1
ln$ = UCase(Trim(ln$)) ; Omit outer spaces
If ln$ = "START" ; Marks start of tune
startfound=#True
ElseIf startfound
If ln$ ; Ignore blank line
If FindString("';", Left(ln$,1) ,1)=0 ; Ignore comment line
If ln$="END": Break: EndIf ; Marks end of tune
ln$ = ReplaceString(ln$, "|", " | ") ; Separate barline from note
; While FindString(ln$, " ", 1)
; ln$ = ReplaceString(ln$, " ", " ") ; Remove multiple spaces
; Wend
Debug Str(line_count) + " '" + ln$+"'"
; Split the line into individual notes/rests/tempos/barlines
For note = 1 To CountString(ln$, " ")+1
inp$ = StringField(ln$, note, " ")
If inp$="|" ; Barline
AddElement(Note$()): Note$() = "|"
Continue
EndIf
; Process any instrument #s, replacing them by channel #s
; A maximum of 15 channels are available in this program
p = FindString(inp$, ",", 1) ; Any instrument?
If p
channels$ = " " ; Leading code of a space
For instrum = 1 To CountString(inp$, ",") ; For each selected instrument
instrument = Val(StringField(inp$,instrum+1,","))
If instrument<1 Or instrument>128: instrument=1: EndIf
channel = FindString(instrum$, Chr(instrument), 1)
If channel=0 ; If new instrument #
instrum$ + Chr(instrument)
channel = Len(instrum$)
SetInstrument(channel, instrument)
Debug Space(5)+"Channel "+Str(channel)+" = instrument "+Str(instrument)
EndIf
channels$ + UCase(Hex(channel))
Next instrum
AddElement(Note$()): Note$() = channels$ ; Store channel #s in hex
inp$ = Left(inp$, p-1) ; Remove commas and instrument #s
EndIf ; p
If Len(inp$): AddElement(Note$()): Note$() = inp$: EndIf ; Store note/rest/tempo definition
Next note
EndIf ; Comment
EndIf ; Blank line
EndIf ; Start
ln$ = ReadString(1)
Wend ; Eof()
CloseFile(1)
; Ensure a default instrument is defined
If Len(instrum$)=0
SetInstrument(1, instrumdef) ; Channel 1, default instrument
Debug Space(5)+"Channel 1 = instrument "+Str(instrumdef)
EndIf ; Len(instrum$)
; Play the notes
Debug ""
Debug "START"
channels$ = "1"
transpose = 0 ;!!! Number of octaves by which to transpose notes
octave = octavedef
dur = durdef
volume = volumedef
tempo = tempodef
beat = 60000.0/tempo ; Beat time in milliseconds
measure_beats = 0.0
ForEach Note$()
inp$ = Note$()
Debug inp$
Select Left(inp$, 1)
Case " " ; Channel #s in hexadecimal 1..F
channels$ = Mid(inp$, 2, 999)
Continue
Case "|" ; Barline
If measure_beats > 0.0
Debug "Beats = " + StrF(measure_beats)
measure_beats = 0.0
EndIf
Continue
Case "T" ; Tempo
tempo = Val(Mid(inp$, 2, 999))
If tempo<10 Or tempo>1000: tempo = tempodef: EndIf
beat = 60000.0/tempo ; Beat time in milliseconds
Continue
Case "A": note = 9
Case "B": note = 11
Case "C": note = 0
Case "D": note = 2
Case "E": note = 4
Case "F": note = 5
Case "G": note = 7
Case "R": note = 255 ; Rest
Default: MessageRequester("Error", "Invalid note within "+inp$, #MB_ICONERROR): End
EndSelect
; Determine note/rest attributes/modifiers
dotted = #False
octavedelta = 0
For p = 2 To Len(inp$)
c$ = Mid(inp$, p, 1)
Select c$
Case "#": note + 1 ; Sharp note
Case "B": note - 1 ; Flat note
Case "+": volume + 1 ; Slightly louder
Case "-": volume - 1 ; Slightly quieter
Case ">": octavedelta + 1 ; Higher octave
Case "<": octavedelta - 1 ; Lower octave
Case "0","1","2","3","4","5","6","7","8","9": octave = Val(c$)
Case ".","D": dotted = #True
Case "W": dur = 4.0 ; Whole note
Case "H": dur = 2.0 ; Half note
Case "Q": dur = 1.0 ; Quarter note
Case "E": dur = 0.5 ; Eighth note
Case "S": dur = 0.25 ; Sixteenth note
Case "T": dur = 0.125 ; Thirty-second note
Case "F": dur = 0.0625 ; Sixty-fourth note
Default : MessageRequester("Error", "Invalid attribute within "+inp$, #MB_ICONERROR): End
EndSelect
Next p
octave + octavedelta
If octave<0: octave = 0: EndIf
If octave>9: octave = 9: EndIf
note + (octave+transpose)*12
If note<1: note=1: EndIf ; Avoid possible problems with note<=0
If volume<0: volume=0: EndIf
If volume>9: volume=9: EndIf
; Play instruments in unison
If note < 255: PlayNotes(channels$, note, volume*14): EndIf
If dotted
Delay(beat*dur*1.5): measure_beats + dur*1.5
Else
Delay(beat*dur): measure_beats + dur
EndIf
If note < 255: StopNotes(channels$, note): EndIf
Next Note$()
midiOutClose_(@hMidiOut)
If measure_beats>0.0
Debug "Beats = " + StrF(measure_beats)
EndIf
Debug "END"
End
The above program can enable instruments to start/stop playing in unison.
Notice that your routines PlayNote() and StopNote() are now called PlayNotes() and StopNotes() and have a more general functionality.
The revised version of midi.txt is:
Code:
Tune Syntax
-----------
Each tune must be between 'Start' and 'End' lines.
Each tune line is case-insensitive and typically consists of one or more note/rest/tempo definitions, separated by spaces.
In each definition, the note/rest/tempo letter (A..G, R, T) is required as the first character, but any other attributes are optional and may appear in any order.
Additionally, a cosmetic barline '|' may appear just before or after any note/rest/tempo definition (optionally separated by spaces).
Use T immediately followed by the number of beats per minute to set the tempo. N.B. any further attributes are ignored.
If no tempo is set, it defaults to 120 bpm.
Attributes and Modifiers
------------------------
Use > and < to go up/down an octave, or specify octave 0..9.
Use + and - to increase/decrease the volume (0..9) slightly.
To emulate a specific musical instrument, immediately follow the note/rest definition with a comma and the instrument number (1..128).
Any previously emulated instrument will be silenced.
For multiple instruments to play in unison, use the syntax:
<note/rest defn>,<instrument#>,<instrument#>,...,<instrument#>
If no instrument is specified, instrument 1 is assumed.
Throughout the entire tune, a maximum of 15 instruments may be used.
If any of the following attributes are missing, they will be replicated from the previous musical note or rest:
Instrument (1..128)
Tempo (10..1000) in beats per minute
Octave (0..9) including the effects of any > or < modifiers
Volume (0..9) including the effects of any + or - modifiers
Duration (W, H, Q, E, S, T, F) excluding any dot modifier
The effects of the following modifiers are NOT carried forward from the previous note:
Dotted note modifier (. or D)
Note accidentals, sharp (#) and flat (b)
If a rest (as opposed to a note) definition changes the current octave or volume, it will take effect when the next note is played.
Comment lines start with ' or ;
Example tune at a tempo of 120 beats per minute:
Start
; Twinkle, Twinkle Little Star
t120 c8q,10 c g g
; Below, the barlines are separated by optional spaces
| aq a gh |
; Below, '<<' causes a drop of two octaves
f<<q,10,74 f e e|
; Below, three instruments are playing the last three notes in unison
d d ch|r c,10,74,1 e g
End