MIDI Text Player

Share your advanced PureBasic knowledge/code with the community.
chris319
Enthusiast
Enthusiast
Posts: 782
Joined: Mon Oct 24, 2005 1:05 pm

MIDI Text Player

Post by chris319 »

This (Windows) program plays MIDI notes from a text file. You can specify any of the notes on the scale, the octave, the duration (includng rests) and the general MIDI instrument.

Follow carefully the instructions for cutting and pasting the text between the === lines into a separate file called "midi.txt".

All bug reports and suggestions for enhancements are welcome.

Code: Select all

;midi_text_player.pb
;written in PureBasic
;version 1.2
;updated on 12/15/07
;by chris319
;COPYRIGHT (C) 2007 Chris Clementson


;==========================================
CUT THE TEXT BETWEEN THIS LINE And THE Next === LINE BELOW And
PASTE INTO A FILE NAMED "midi.txt". THE PROGRAM WILL Read
THE TUNE FROM "midi.txt", Or YOU MAY CHANGE THE FILE NAME IN THE CODE.

THIS NIFTY LITTLE PROGRAM PLAYS MIDI NOTES FROM A TEXT SCRIPT.
YOU CAN SPECIFY ANY OF THE NOTES IN THE SCALE, THE OCTAVE OF THE
NOTE, THE DURATION OF NOTES And RESTS, And THE GENERAL MIDI
INSTRUMENT WHICH WILL BE USED To PLAY THE NOTE.

THERE MUST BE ONLY ONE NOTE, REST Or MEASURE DEMARCATOR (BAR) PER LINE.
EACH NOTE IS REPRESENTED BY A LETTER A - G. SHARPS ARE DENOTED With A
TRAILING "#" And FLATS ARE DENOTED BY A TRAILING "b", e.g. B-FLAT CAN
BE ENTERED As Bb Or A#. YOU MAY ALSO REPRESENT RESTS With THE LETTER "R"
And NO OCTAVE NUMBER. ALPHABETIC CHARACTERS MAY BE UPPER Or LOWER Case.
THERE MUST BE NO SPACES Or OTHER CHARACTERS.

AFTER EACH NOTE IS A NUMBER REPRESENTING THE OCTAVE, e.g. C4. AFTER THE
OCTAVE ARE ONE Or TWO LETTERS REPRESENTING THE DURATION OF THE NOTE.
If THE FIRST OF THE TWO CHARACTERS IS "D", THE NOTE THAT FOLLOWS IS A DOTTED NOTE.
THE NOTES ARE As FOLLOWS:

DW - DOTTED WHOLE NOTE
W - WHOLE NOTE
DH - DOTTED HALF NOTE
H - HALF NOTE
DQ - DOTTED QUARTER NOTE
Q - QUARTER NOTE
DE - DOTTED EIGHTH NOTE
E - EIGHTH NOTE
DS - DOTTED SIXTEENTH NOTE
S - SIXTEENTH NOTE NOTE
DT - DOTTED THIRTY-SECOND NOTE
T - THIRTY-SECOND NOTE
DF - DOTTED SIXTY-FOURTH NOTE
F - SIXTY-FOURTH NOTE

YOU MUST APPEND A NOTE DURATION To RESTS As WELL, e.g. A QUARTER REST WOULD BE
REPRESENTED As "RQ".

A NOTE MAY OPTIONALLY BE FOLLOWED BY A COMMA And A GENERAL MIDI INSTRUMENT
NUMBER. "C4Q,41" WOULD PLAY A QUARTER NOTE AT C4 IN THE VIOLIN VOICE. If THE
INSTRUMENT NUMBER IS LEFT OFF, THE LAST-PLAYED INSTRUMENT WILL BE USED.

THE BEGINNING OF YOUR SONG MUST BE DEMARCATED With THE WORD "START"
And THE End With "END". YOU MAY PLACE HEADER INFORMATION And NOTES ABOVE THE
WORD "START". YOU MAY ALSO DEMARCATE MEASURES IN YOUR TEXT FILE With THE "|"
CHARACTER. THIS IS MERELY A VISUAL AID And WILL BE DISREGARDED BY THE PLAYER.

BELOW IS THE SONG Data. BE SURE To INCLUDE THE WORDS "START" And "END".

start
c8q,10
c8q
g8q
g8q
|
a8q
a8q
g8h
|
f6q,74
f6q
e6q
e6q
|
d6q
d6q
c6h
rh
c6h,1
e6h
g6h
End

CUT And PASTE THE ABOVE TEXT (INCLUDING THIS LINE) To A FILE NAMED "MIDI.TXT".
;==========================================


;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


Global hMidiOut

bpm.f = 120
bps.f = bpm / 60
beat.f = (1 / bps) * 1000 ;IN MILLISECONDS

Global Dim Instrument(16)
Global channel, note, previous_note

Procedure MidiOutMessage(hMidi,iStatus,iChannel,iData1,iData2)
  dwMessage = iStatus | iChannel | (iData1 << 8 ) | (iData2 << 16)
  ProcedureReturn midiOutShortMsg_(hMidi, dwMessage) ;
EndProcedure

Procedure SetInstrument(channel, instrument)
  MidiOutMessage(hMidiOut, $C0,  channel, instrument, 0)
EndProcedure

Procedure StopNote(channel, note)
  MidiOutMessage(hMidiOut, $90, channel, Note, 0)
EndProcedure

Procedure PlayNote(channel, note, volume)
  StopNote(channel, previous_note)
  MidiOutMessage(hMidiOut, $90, channel, note , volume)
EndProcedure

midi.MIDIOUTCAPS
devices = midiOutGetNumDevs_()

For devnum = - 1 To devices - 1
  If midiOutGetDevCaps_(devnum,@midi,SizeOf(MIDIOUTCAPS))=0
    If midi\wVoices > 0
      midiport=devnum
    EndIf
  EndIf
Next

*hMidiOut.l
If midiOutOpen_(@hMidiOut,midiport,0,0,0) <> #MMSYSERR_NOERROR
  MessageRequester("Error", "Unable to open MIDI output.", #MB_ICONERROR)
  End
EndIf

instrument(1) = 57  ;TRUMPET
instrument(2) = 10  ;GLOCKENSPIEL

For ct = 1 To 2
  SetInstrument(ct, instrument(ct))
Next

OpenFile(1, "midi.txt")

;LOOK For "START" To INDICATE START OF PLAYBACK
;ACCEPTABLE NOTATION
;a5q
;a#5q
;ab5q
;ab5dq
;ab5dq,10 -- INSTRUMENT = GLOCKENSPIEL


line_count = 0
inp$ = ""

        While UCase(inp$) <> "START"
            inp$ = ReadString(1)
            line_count = line_count + 1
        Wend


While Not Eof(1)
            inp$ = ReadString(1)
            line_count = line_count + 1

If inp$ = "" Or Left(inp$, 1) = "'": Goto blankline: EndIf ;BLANK LINE OR APOSTROPHE (COMMENT)

;Debug(inp$ + "  " + Str(line_count))

            ;MEASURE DEMARCATOR
            measure$ = "Measure " + " " + Right(inp$, Len(inp$) - 1)
            If Left(inp$, 1) = "|"
              If measure_beats.f > 0
              Debug StrF(measure_beats)
                ;measure$ = measure$ + "  " + Str(measure_beats)
              ;EndIf
              measure_beats = 0
              EndIf
Debug measure$
              Goto nextnote
            EndIf          
            
            If UCase(inp$) = "END": Debug StrF(measure_beats): End: EndIf
            
tmp$ = ""
ct = 1
While Mid(inp$, ct, 1) <> "," And ct <= Len(inp$)
  tmp$ = tmp$ + Mid(inp$, ct, 1)
  ct = ct + 1
Wend


ct = ct + 1
instrum$ = ""
While ct <= Len(inp$)
  instrum$ = instrum$ + Mid(inp$, ct, 1)
  ct = ct + 1
Wend

If Val(instrum$) > 0: instrument = Val(instrum$): EndIf

inp$ = tmp$
            in2$ = Right(inp$, 2)
            
            If UCase(Left(in2$, 1)) = "D"
              in3$ = Right(inp$, 3)
              octave = Val(Left(in3$, 1))
            Else
              octave = Val(Left(in2$, 1)) ;EXTRACT OCTAVE
            EndIf

            note$ = Left(inp$, 2)
            dur$ = UCase(Right(inp$, 2))

            If Right(note$, 1) <> "#" And Right(note$, 1) <> "b"
              note$ = UCase(Left(note$, 1)) ;NATURAL
            Else
              note$ = UCase(Left(note$, 1)) + Right(note$, 1) ;SHARP OR FLAT
            
            EndIf
            
            If note$ = "C"
                note = 0
            ElseIf note$ = "C#" Or note$ = "Db"
                note = 1
            ElseIf note$ = "D"
                note = 2
            ElseIf note$ = "D#" Or note$ = "Eb"
                note = 3
            ElseIf note$ = "E"
                note = 4
            ElseIf note$ = "F"
                note = 5
            ElseIf note$ = "F#" Or note$ = "Gb"
                note = 6
            ElseIf note$ = "G"
                note = 7
            ElseIf note$ = "G#" Or note$ = "Ab"
                note = 8
            ElseIf note$ = "A"
                note = 9
            ElseIf note$ = "A#" Or note$ = "Bb"
                note = 10
            ElseIf note$ = "B"
                note = 11
            ElseIf note$ = "R" ;REST
                note = 255
            Else
                MessageRequester("Error", "Invalid note on line " + Str(line_count)+":"+Chr(13)+note$, #MB_ICONERROR)
                End   
            EndIf

            note = (octave * 12) + note

            Select Right(dur$, 1)
            Case "Q"
                dur.f = 1 ;QUARTER NOTE
            Case "H"
                dur = 2 ;HALF NOTE
            Case "W"
                dur = 4 ;WHOLE 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 note duration on line " + Str(line_count)+":"+Chr(13)+dur$, #MB_ICONERROR)
              End
            EndSelect

dotted$ = Left(dur$, 1)
If UCase(dotted$) = "D"
  dur = dur * 1.5
ElseIf Asc(dotted$) > 57 And dotted$ <> "R" 
  MessageRequester("Error", "Invalid note duration on line " + Str(line_count)+":"+Chr(13)+dur$, #MB_ICONERROR)
EndIf

measure_beats = measure_beats + dur

If note < 255
For channel = 1 To 2
  PlayNote(channel, note, 50)
Next
EndIf

Delay (beat * dur)

If note < 255
For channel = 1 To 2
  StopNote(channel, note)
Next
;Else
;  Delay(beat * dur)
EndIf

nextnote:
previous_note = note
blankline:
Wend

CloseFile(1)

midiOutClose_(@hMidiOut)

End
Last edited by chris319 on Sat Dec 15, 2007 10:07 pm, edited 2 times in total.
chris319
Enthusiast
Enthusiast
Posts: 782
Joined: Mon Oct 24, 2005 1:05 pm

Post by chris319 »

Oh yeah, the tempo can be changed by modifying the bpm variable.
User avatar
electrochrisso
Addict
Addict
Posts: 989
Joined: Mon May 14, 2007 2:13 am
Location: Darling River

Post by electrochrisso »

I like it. :D
Would it possible to do multitrack note play like a sequencer?
chris319
Enthusiast
Enthusiast
Posts: 782
Joined: Mon Oct 24, 2005 1:05 pm

Post by chris319 »

It will take some reworking, but I want to get it to play the same/different notes with the same/different instruments (chords). This will involve rewriting the parser to support multiple notes on a single line. This will probably require a change in the text file format. Probably the note duration (whole, half, quarter, etc.) will come first, followed by one or more note/instrument pairs.
User avatar
electrochrisso
Addict
Addict
Posts: 989
Joined: Mon May 14, 2007 2:13 am
Location: Darling River

Post by electrochrisso »

To do what I suggest would you need to have notes on different midi channels like drums on 10, bass on 1 and piano on 2 etc.
If you could get it to do that I was thinking that it would be possible to make a simple sequencer for writing background tracks for programs and games that could be written easily in pure basic.
Just a thought.
chris319
Enthusiast
Enthusiast
Posts: 782
Joined: Mon Oct 24, 2005 1:05 pm

Post by chris319 »

Yes, it could support multiple channels. In fact I will steer the architecture of future versions that way per your request.
chris319
Enthusiast
Enthusiast
Posts: 782
Joined: Mon Oct 24, 2005 1:05 pm

Post by chris319 »

NOTE: The code in the original post has changed. The previous version did not handle dotted notes properly. Please update your copy.

This version (1.1) has a feature which counts beats per measure.
User avatar
electrochrisso
Addict
Addict
Posts: 989
Joined: Mon May 14, 2007 2:13 am
Location: Darling River

Post by electrochrisso »

Thanks for that, much appreciated. :D
chris319
Enthusiast
Enthusiast
Posts: 782
Joined: Mon Oct 24, 2005 1:05 pm

Post by chris319 »

Version 1.2 is hereby released. I have modified the O.P. with the new code.

The new version allows one to assign different instruments to different MIDI channels (up to 16 now but you could easily modify the code). All instruments will play the same notes in unison. Steering different notes into different channels will require redoing the format of the input file. That will have to come later.
User avatar
electrochrisso
Addict
Addict
Posts: 989
Joined: Mon May 14, 2007 2:13 am
Location: Darling River

Post by electrochrisso »

Good one chris319! I will have some fun and make up a tune. :)
PureBasic! Purely the best 8)
akj
Enthusiast
Enthusiast
Posts: 668
Joined: Mon Jun 09, 2003 10:08 pm
Location: Nottingham

Post by akj »

@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: Select all

; 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: Select all

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
Anthony Jordan
User avatar
electrochrisso
Addict
Addict
Posts: 989
Joined: Mon May 14, 2007 2:13 am
Location: Darling River

Post by electrochrisso »

8) And thanks for links.
PureBasic! Purely the best 8)
Dare
Addict
Addict
Posts: 1965
Joined: Mon May 29, 2006 1:01 am
Location: Outback

Post by Dare »

Hi akj,

Looks like you have some good stuff on your site!

I might try converting some to 4.

Cheers.
Dare2 cut down to size
Mistrel
Addict
Addict
Posts: 3415
Joined: Sat Jun 30, 2007 8:04 pm

Post by Mistrel »

Fancy! Thanks for sharing. :)
chris319
Enthusiast
Enthusiast
Posts: 782
Joined: Mon Oct 24, 2005 1:05 pm

Post by chris319 »

I'll have a look at your code, but this is all going to be rewritten anyway to be able to steer notes to different channels. I'll post my ideas when ready. I have already written some code which utilizes the timer to trigger events.

This is and isn't MIDI because it does not comply with any MIDI standards, but plays back through General MIDI.
Post Reply