MIDI Text Player

Share your advanced PureBasic knowledge/code with the community.
Mistrel
Addict
Addict
Posts: 3415
Joined: Sat Jun 30, 2007 8:04 pm

Post by Mistrel »

I don't know anything about MIDI standards but this link might help.

http://www.midi.org/about-midi/dls/dls2spec.shtml
chris319
Enthusiast
Enthusiast
Posts: 782
Joined: Mon Oct 24, 2005 1:05 pm

Post by chris319 »

My objective for this program is to keep it very, very simple for user and programmer alike, especially to keep the notation simple as well as the programming. What I've posted is more a demo program than a complete application.

As I said I wrote a test program using the timer to initiate "MIDI" events. I will post that when I'm on my regular computer. Again, it is merely a proof-of-concept.

Here are some ideas for a multichannel version. Feedback welcome.

Rather than individual notes there would be chords. A chord consists of 1 to 10 notes of the same duration played in unison. Traditionally a chord consists of two or more notes so we are cheating a little bit by having one-note "chords".

Each music file would have a header which would specify tempo in BPM and give the instrument definitions. Each line of the header could have a comment demarcated by a "," viz.:

T,120,song tempo
I1,68,instrument 1 = baritone sax
I2,57,instrument 2 = trumpet
start,start of musical data

I am thinking there would be maybe 16 channels, with an instrument assigned to each.

I don't see it as burdensome having to specify the octave for each note and it will simplify the parser by omitting it. I also don't want to make the notation too cryptic for the user.

For a multichannel version I am thinking an event would be specified thus:

en,50,a4C5e5,a4C5e5
er
Qn,50,c5e5g5,c5e5g5

Here are the definitions, keeping in mind that we want to keep the parser simple.

Each "event" would occupy one line consisting of comma-separated values (so that the file could be edited in a spreadsheet program). The first field is the note duration and type. The first character in this field specifies the duration, thus:

w - WHOLE NOTE
h - HALF NOTE
q - QUARTER NOTE
e - EIGHTH NOTE
s - SIXTEENTH NOTE NOTE
t - THIRTY-SECOND NOTE
f - SIXTY-FOURTH NOTE

W - DOTTED WHOLE NOTE
H - DOTTED HALF NOTE
Q - DOTTED QUARTER NOTE
E - DOTTED EIGHTH NOTE
S - DOTTED SIXTEENTH NOTE
T - DOTTED THIRTY-SECOND NOTE
F - DOTTED SIXTY-FOURTH NOTE

Uppercase letters are dotted and lowercase letters are undotted.

The second character in this field specifies "N" for note and "R" for rest (case insensitive).

The second field specifies velocity/volume.

The third and subsequent fields specify chords (1 to 10 notes) for each channel. Each note in a chord is specified by a character pair. The first character is the note and the second character is the octave (sorry). Naturals are specified with a lower-case character and sharps are specified with an uppercase character. There would be no flats, so B-flat would have to be specified as A-sharp. Alternatively, there could be a two character field with notes specified thus:

a# - A sharp (case insensitive)
a - A natural (note name followed by an explicit space -- might be hard for users to grasp)
ab - A flat (lowercase "B" looks like a flat symbol)

Octaves would be specified as - through 9:

http://www.harmony-central.com/MIDI/Doc/table2.html

In keeping with the concept of constant character positions, "-" would specify octave -1 and "0" through "9" would specify the remaining octaves. Thus all that is required to parse the string is a series of Mid() statements.

In the above three lines, we have played an A chord in channels one and two with an eighth-note duration and a velocity of 50. We have then "played" an eighth rest, followed by a C chord with a dotted quarter-note duration (uppercase Q) and velocity of 50.

To skip a channel where no event is desired, a field would be left blank, e.g.:

qn,50,a,a,,a

In the above example, A is played in the first, second and fourth channels.

Internally, note durations would be converted to a number of "ticks" (really pseudo ticks) as follows:

Sixty-fourth note = 2 "ticks"
Dotted sixty-fourth note = 3 "ticks"
Eighth note = 4 "ticks"
Dotted eighth note = 6 "ticks"
etc.

The program would use a timer whose time base is calculated from the number of beats per minute, thus the definition of beats given above remains constant.

Also internally, the user's note file would be compiled into a series of events, each with a start time in pseudo ticks, midi note, velocity and channel. For each note in the user's list, a start event and end event would be given. Basically, a note would start with a velocity specified by the user. A second event would stop the note by playing it with a velocity of 0. The start time of the "stop" event would be calculated as the start time of the note plus the duration in beats. Each time the timer-servicing procedure is invoked, a pseudo-tick counter is incremented. Each time an event (or more than one event if they begin at the same time), an event counter is incremented.

It occurs to me that in addition to notes and rests we should also support slurs/ties. Perhaps

qn - quarter note
qr - quarter rest
t or s - tie or slur to next note

Feedback?
chris319
Enthusiast
Enthusiast
Posts: 782
Joined: Mon Oct 24, 2005 1:05 pm

Post by chris319 »

Here is a test program I wrote which uses the timer to trigger events.

Code: Select all

;USES TIMER TO PLAY MIDI EVENTS
;CREATED ON 12/15/2007

Global Dim ticks(10)
Global Dim note(10)
Global Dim volume(10)
Global Dim channel(10)
Global system_tick, midi_event

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


Procedure.l Roundoff(number.f) ;THE PROPER WAY TO ROUND A NUMBER
  If number - Int(number) >= 0.5 
    ProcedureReturn number.f + 0.1 
  EndIf 
    ProcedureReturn number.f 
EndProcedure

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)
If Is_MIDI(channel) <> 0
  StopNote(channel, previous_note)
  MidiOutMessage(hMidiOut, $90, channel, note , volume)
Else
  ;PLAY WAV SAMPLE
EndIf
EndProcedure

Procedure tick() ;THIS ROUTINE IS INVOKED BY THE TIMER
While ticks(midi_event) = system_tick
  PlayNote(channel(midi_event), note(midi_event), volume(midi_event))
  ;Debug(Str(midi_event) + "  " + Str(system_tick))
  midi_event = midi_event + 1
Wend
system_tick = system_tick + 1
EndProcedure


InitSound()

result = LoadSound(129, "C:\Programs\PureBasic 4.10\source\Flugelhorn_5C_Tuned.wav")
If result = 0:   MessageRequester("Error", "Unable to open MIDI output.", #MB_ICONERROR): End: EndIf
result = LoadSound(130, "C:\Programs\PureBasic 4.10\source\Flugelhorn_5C_Tuned.wav")
If result = 0:   MessageRequester("Error", "Unable to open MIDI output.", #MB_ICONERROR): End: EndIf


timesig$ = "4/4"
one_beat = Val(Right(timesig$, 1))
one_beat = one_beat / 4
bpm.f = 60
beat.f = (60 / bpm) * 1000 ;MILLISECONDS

Global timebase.f = (beat * 0.0625 * one_beat) / 2 ;ABLE TO RESOLVE DOTTED SIXTY-FOURTH NOTE
;SIXTY-FOURTH NOTE = 2 * timebase
int_timebase = Roundoff(timebase)

dwhole = 192 ;TICKS
whole = 128 ;TICKS
dhalf = 96 ;TICKS
half = 64 ;TICKS
dquarter = 48 ;TICKS
quarter = 32 ;TICKS
deighth = 24 ;TICKS
eighth = 16 ;TICKS
dsixteenth = 12 ;TICKS
sixteenth = 8 ;TICKS
dthirsec = 6 ;TICKS
thirsec = 4 ;TICKS
dsixfour = 3 ;TICKS
sixfour = 2 ;TICKS

system_tick = 1

;DATA FOR A COUPLE OF NOTES
ticks(1) = 20: note(1) = 66: volume(1) = 50: channel(1) = 1
ticks(2) = 20: note(2) = 66: volume(2) = 50: channel(2) = 2
ticks(3) = 20 + quarter: note(3) = 66: volume(3) = 0: channel(3) = 1
ticks(4) = 20 + quarter: note(4) = 66: volume(4) = 0: channel(4) = 2
ticks(5) = 20 + quarter: note(5) = 68: volume(5) = 50: channel(5) = 1
ticks(6) = 20 + quarter: note(6) = 68: volume(6) = 50: channel(6) = 2
ticks(7) = 20 + quarter + quarter: note(7) = 68: volume(7) = 0: channel(7) = 1
ticks(8) = 20 + quarter + quarter: note(8) = 68: volume(8) = 0: channel(8) = 2

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

;SET INSTRUMENTS
instrument(1) = 65
instrument(2) = 10
For channel = 1 To 2
If instrument(channel) <= 128
  SetInstrument(channel, instrument(channel))
  Is_MIDI(channel) = 1
Else
  Is_MIDI(channel) = 0
EndIf
Next

midi_event = 1

If OpenWindow(1,0,0,200,100,"MIDI Ticks",#PB_Window_SystemMenu|#PB_Window_ScreenCentered) = 0
  MessageRequester("Error", "Unable to open window.", #MB_ICONERROR)
  End 
EndIf 

;BEGIN MIDI PLAYBACK
tick_clock = SetTimer_(WindowID(1), 1, int_timebase, @tick()) 

Repeat 
  Window_Event = WaitWindowEvent() 
Until Window_Event = #PB_Event_CloseWindow 

midiOutClose_(@hMidiOut)

If tick_clock 
  KillTimer_(WindowID(1), tick_clock) 
EndIf
 
FreeSound(129)
FreeSound(130)

End
User avatar
electrochrisso
Addict
Addict
Posts: 989
Joined: Mon May 14, 2007 2:13 am
Location: Darling River

Post by electrochrisso »

Keep up the good work chris. :)
PureBasic! Purely the best 8)
chris319
Enthusiast
Enthusiast
Posts: 782
Joined: Mon Oct 24, 2005 1:05 pm

Post by chris319 »

*** THIS IS VERSION 2.02 WHICH SENDS STOP MESSAGES TO THE MIDI DEVICE. ***

This version plays general MIDI notes using up to 16 channels. The program's time base is supplied by the timer. The shortest note it can play is a thirty-second note (or rest). It supports ties, and there can be multiple notes played on the same instrument (chords).

Please, if you have ideas or suggestions, let's discuss them first so as not to break what I've already done.

Here is what a typical input file looks like:

Code: Select all

t,120
i1,57
i2,10
start
wt,100,c4
hn,100,c4
qr
Hn,100,a3C4e4,a5C6e6
end
The fields are comma delimited and can be edited with a spreadsheet program. The first line sets the tempo to 120 bpm. The next two lines set instruments one and two to trumpet and glockenspiel. "Start" defines the beginning of the note data and "end" defines the end.

The first comma-delimited field is duration. The first character is the type of note, with lowercase being undotted and uppercase being dotted. The second character can be "n" for note, "r" for rest, or "t" for tie (to the NEXT note). The second field is velocity/volume. The remaining comma-delimited fields are the individual channels, up to 16 of them. Lowercase characters are naturals and uppercase characters are sharps.

The fourth line defines a whole note with a velocity/volume of 100 and a pitch of C natural in octave 4. The next line represents a WHOLE note which is tied to the NEXT note. The next line is a quarter rest. The next line is a DOTTED half note (signified by the uppercase "H"). Velocity is 100, and two A major chords follow, played on channel 1 and channel 2 respectively.

The program itself first parses the lines contained in the music file and creates one event for each tie or rest, and two events for each note (start and stop). It then compiles the events in order of start time.

Version 2.01 reads all of the lines in the note file to an array of strings called ev$().

Right now there isn't much of a front end or user interface to this thing.

Please report any problems or post any suggestions.

3/29/2008 -- please see updated version below.
Last edited by chris319 on Sun Mar 30, 2008 7:59 am, edited 5 times in total.
superadnim
Enthusiast
Enthusiast
Posts: 480
Joined: Thu Jul 27, 2006 4:06 am

Post by superadnim »

Just an idea, why not support constants for the input files?, you can search&replace using a hash list or something similar, that way you don't have to memorize all the codes (doesn't have to be mandatory, you could still use the enumerations)
chris319
Enthusiast
Enthusiast
Posts: 782
Joined: Mon Oct 24, 2005 1:05 pm

Post by chris319 »

superadnim wrote:Just an idea, why not support constants for the input files?, you can search&replace using a hash list or something similar, that way you don't have to memorize all the codes (doesn't have to be mandatory, you could still use the enumerations)
I don't follow you. For anyone who knows basic music notation, the note pitches and durations are straightforward. Do you mean constants for the chords? That would fit well into the parser. Chords would be prepended with some symbol and then have something like a7s for A minor seventh suspended.

What symbol would work to indicate a chord?
chris319
Enthusiast
Enthusiast
Posts: 782
Joined: Mon Oct 24, 2005 1:05 pm

Post by chris319 »

The version posted on 12/22 had problems with handling tied chords properly. Version 2.01 corrects this and adds a little bit more error handling. It can be found a few posts back (I have replaced the old code with the new code in that post).

NOTE: Notes now tie to the subsequent note, e.g.

Code: Select all

ht,100,a4C5e5,a4C5e5
ht,100,a4C5e5,a4C5e5
hn,100,a4C5e5,a4C5e5
will tie three half notes together. The first and second notes are designated as ties ("t"), while the final note must be designated as a (non-tied) note ("n").
Dare
Addict
Addict
Posts: 1965
Joined: Mon May 29, 2006 1:01 am
Location: Outback

Post by Dare »

Hi.

Just out of curiosity, when stopping notes why do you send a play signal ($90) with a velocity (strike/force) of 0 and not a stop status ($80)?

I would have thought that using play + vel 0 would keep the note going (very softly) until it naturally died away. As some voices never die away (some organs spring to mind in GM) might this not eventually build up to some heavy overhead esp for a multi-timbral device?

However you probably have a sneaky reason for doing this and I'd like to learn it. :)

Thanks.
Dare2 cut down to size
chris319
Enthusiast
Enthusiast
Posts: 782
Joined: Mon Oct 24, 2005 1:05 pm

Post by chris319 »

No sneaky reason -- it was done that way in the original code this was cribbed from:

Code: Select all

Procedure StopNote(channel, note)
  MidiOutMessage(hMidiOut, $90, channel, Note, 0)
EndProcedure
I could just as easily make this $80.
Dare
Addict
Addict
Posts: 1965
Joined: Mon May 29, 2006 1:01 am
Location: Outback

Post by Dare »

Okay, thanks.
Dare2 cut down to size
chris319
Enthusiast
Enthusiast
Posts: 782
Joined: Mon Oct 24, 2005 1:05 pm

Post by chris319 »

OK I have changed the Tick() procedure so that if the event velocity = 0, the program will send a stop message to the MIDI device. See what you think.

Thanks for pointing this out.
chris319
Enthusiast
Enthusiast
Posts: 782
Joined: Mon Oct 24, 2005 1:05 pm

Post by chris319 »

Some minor error-handling fixes:

Code: Select all

;midi_text_player.pb 
;written in PureBasic 
;version 2.03 
;USES TIMER TO PLAY MIDI EVENTS 
;UPDATED ON 3/29/2008 BY chris319 
;(C) 2007 - 2008 CHRIS CLEMENTSON 

;N.B.: EACH NOTE IN A CHORD IS TREATED AS A SEPARATE EVENT 

;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 

#DWHOLE = 96 ;TICKS -- DOTTED WHOLE NOTE 
#WHOLE = 64 ;TICKS -- WHOLE NOTE 
#DHALF = 48 ;TICKS -- DOTTED HALF NOTE 
#HALF = 32 ;TICKS -- HALF NOTE 
#DQUARTER = 24 ;TICKS -- DOTTED QUARTER NOTE 
#QUARTER = 16 ;TICKS -- QUARTER NOTE 
#DEIGHTH = 12 ;TICKS -- DOTTED EIGHTH NOTE 
#EIGHTH = 8 ;TICKS -- EIGHTH NOTE 
#DSIXTEENTH = 6 ;TICKS -- DOTTED SIXTEENTH NOTE 
#SIXTEENTH = 4 ;TICKS -- SIXTEENTH NOTE 
#DTHIRSEC = 3 ;TICKS -- DOTTED THIRTY-SECOND NOTE 
#THIRSEC = 2 ;TICKS -- THIRTY-SECOND NOTE 

Global Dim velocity(32768) 
Global Dim note(32768) 
Global Dim channel(32768) 
Global Dim start(32768) 

Global Dim compiled_velocity(32768) 
Global Dim compiled_note(32768) 
Global Dim compiled_channel(32768) 
Global Dim compiled_start(32768) 

Global system_tick, midi_event 

Global Dim instrument(16) 
;Global Dim Is_MIDI(16) 
Global channel, running_ticks, event_ct, line_count, header_count
Global previous_note, event_channels, bpm.f 
Global hMidiOut, note_rest$


Procedure.l Roundoff(number.f) ;THE PROPER WAY TO ROUND A NUMBER 
  If number - Int(number) >= 0.5 
    ProcedureReturn number.f + 0.1 
  EndIf 
    ProcedureReturn number.f 
EndProcedure 

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, $80, channel, Note, 0) 
EndProcedure 

Procedure PlayNote(channel, note, velocity) 
  MidiOutMessage(hMidiOut, $90, channel, note , velocity) 
EndProcedure 

Procedure Tick() ;THIS ROUTINE IS INVOKED BY THE TIMER 
While compiled_start(midi_event) = system_tick 

;If Is_MIDI(channel) <> 0 
If compiled_velocity(midi_event) > 0 
  MidiOutMessage(hMidiOut, $90, compiled_channel(midi_event), compiled_note(midi_event), compiled_velocity(midi_event)) 
Else 
  MidiOutMessage(hMidiOut, $80, compiled_channel(midi_event), compiled_note(midi_event), 0) 
EndIf 
  
;EMBEDDED CODE TO PRINT NOTE DURATION IN SECONDS 
;Global old_time.f 
;If old_time > 0: Debug (ElapsedMilliseconds() - old_time) / 1000: EndIf 
;If old_time = 0: old_time = ElapsedMilliseconds(): EndIf 

;Else 
  ;PLAY WAV SAMPLE 
;EndIf 

  midi_event + 1 
Wend 
system_tick + 1 
EndProcedure 

Procedure Parse() 
Dim ev$(1024) 
event_ct = 0 
running_ticks = 1 

line_count = 1 
While Eof(1) = 0 
  ev$(line_count) = ReadString(1) ;READ LINE FROM FILE 
  
If Right(ev$(line_count), 1) = ","
  t = Len(ev$(line_count))
  While Mid(ev$(line_count), t, 1) = ","
    t - 1
  Wend
  ev$(line_count) = Left(ev$(line_count), t)
EndIf

;Debug(ev$(line_count))
  
  line_count + 1 
Wend 

line_count = 1 
parse_loop: 
If LCase(ev$(line_count)) <> "end" 

;COUNT NUMBER OF CHANNELS IN EVENT, UP TO 16 CHANNELS 
event_channels = 0 
For ct = 3 To 18 
If StringField(ev$(line_count),ct,",") <> "" 
  event_channels + 1 
EndIf 
Next 

dur$ = StringField(ev$(line_count), 1, ",") 
vel$ = StringField(ev$(line_count), 2, ",") 
note_rest$ = LCase(Right(dur$, 1)) 

If note_rest$ <> "n" And note_rest$ <> "r" And note_rest$ <> "t"
  MessageRequester("Error", "Missing note, rest or tie designation on line " + Str(line_count + header_count)+".", #MB_ICONERROR) 
EndIf

Select Left(dur$, 1) 
  Case "w": duration = #WHOLE; - WHOLE NOTE 
  Case "h": duration = #HALF ; - HALF NOTE 
  Case "q": duration = #QUARTER ; - QUARTER NOTE 
  Case "e": duration = #EIGHTH ; - EIGHTH NOTE 
  Case "s": duration = #SIXTEENTH ; - SIXTEENTH NOTE NOTE 
  Case "t": duration = #THIRSEC ; - THIRTY-SECOND NOTE 
;  Case "f": duration = #SIXFOUR ; - SIXTY-FOURTH NOTE 
  Case "W": duration = #DWHOLE ;- DOTTED WHOLE NOTE 
  Case "H": duration = #DHALF ; - DOTTED HALF NOTE 
  Case "Q": duration = #DQUARTER ; - DOTTED QUARTER NOTE 
  Case "E": duration = #DEIGHTH ; - DOTTED EIGHTH NOTE 
  Case "S": duration = #DSIXTEENTH ; - DOTTED SIXTEENTH NOTE 
  Case "T": duration = #DTHIRSEC ; - DOTTED THIRTY-SECOND NOTE 
;  Case "F": duration = #DSIXFOUR ; - DOTTED SIXTY-FOURTH NOTE 
Default 
  MessageRequester("Error", "Invalid note duration on line " + Str(line_count + header_count)+".", #MB_ICONERROR) 
EndSelect 

If Mid(ev$(line_count - 1), 2, 1) <> "t"  ;: Debug ev$(line_count-1): End 

For channel_ct = 1 To 16 ;event_channels 
chord$ = StringField(ev$(line_count), channel_ct + 2, ",") 

pos = 1 

While pos < Len(chord$) 
  event_ct + 1 
  channel(event_ct) = channel_ct 

  If note_rest$ = "r" ;THIS IS A REST 
    velocity(event_ct) = 0 
  Else ;THIS IS A NOTE 
    velocity(event_ct) = Val(vel$) 
  EndIf 

  start(event_ct) = running_ticks 

  note$ = Mid(chord$, pos, 1) 
  
;LOWERCASE FOR NATURALS AND UPPERCASE FOR SHARPS 
Select note$ 
  Case "c": temp_note = 0  ;C NATURAL 
  Case "C": temp_note = 1  ;C SHARP 
  Case "d": temp_note = 2  ;D NATURAL 
  Case "D": temp_note = 3  ;D SHARP 
  Case "e": temp_note = 4  ;E NATURAL 
  Case "f": temp_note = 5  ;F NATURAL 
  Case "F": temp_note = 6  ;F SHARP 
  Case "g": temp_note = 7  ;G NATURAL 
  Case "G": temp_note = 8  ;G SHARP 
  Case "a": temp_note = 9  ;A NATURAL 
  Case "A": temp_note = 10 ;A SHARP 
  Case "b": temp_note = 11 ;B NATURAL 
Default 
  MessageRequester("Error", "Invalid note on line " + Str(line_count + header_count)+":"+Chr(13)+note$, #MB_ICONERROR) 
EndSelect 

If Mid(chord$, pos + 1, 1) = "-" 
  octave = 0 
Else 
  octave = Val(Mid(chord$, pos + 1, 1)) + 1 
EndIf 

note(event_ct) = (octave * 12) + temp_note 

If note_rest$ = "n" ;THIS IS A NOTE 
;HERE WE CREATE AN EVENT TO STOP THE NOTE WHEN IT IS FINISHED PLAYING 
  event_ct + 1 
  channel(event_ct) = channel_ct 
  velocity(event_ct) = 0 ;SHUT OFF THE NOTE 
  note(event_ct) = (octave * 12) + temp_note 
  start(event_ct) = running_ticks + duration 

ElseIf note_rest$ = "t" ;TIE TO NEXT NOTE 
event_ct + 1 
tie_ct = 0 
tie_duration = 0 

While Mid(ev$(line_count + tie_ct), 2, 1) = "t" 

tie_dur$ = StringField(ev$(line_count + tie_ct), 1, ",") 

Select Left(tie_dur$, 1) 
  Case "w": tie_duration + #WHOLE; - WHOLE NOTE 
  Case "h": tie_duration + #HALF ; - HALF NOTE 
  Case "q": tie_duration + #QUARTER ; - QUARTER NOTE 
  Case "e": tie_duration + #EIGHTH ; - EIGHTH NOTE 
  Case "s": tie_duration + #SIXTEENTH ; - SIXTEENTH NOTE NOTE 
  Case "t": tie_duration + #THIRSEC ; - THIRTY-SECOND NOTE 
;  Case "f": tie_duration + #SIXFOUR ; - SIXTY-FOURTH NOTE 
  Case "W": tie_duration + #DWHOLE ;- DOTTED WHOLE NOTE 
  Case "H": tie_duration + #DHALF ; - DOTTED HALF NOTE 
  Case "Q": tie_duration + #DQUARTER ; - DOTTED QUARTER NOTE 
  Case "E": tie_duration + #DEIGHTH ; - DOTTED EIGHTH NOTE 
  Case "S": tie_duration + #DSIXTEENTH ; - DOTTED SIXTEENTH NOTE 
  Case "T": tie_duration + #DTHIRSEC ; - DOTTED THIRTY-SECOND NOTE 
;  Case "F": tie_duration + #DSIXFOUR ; - DOTTED SIXTY-FOURTH NOTE 
Default 
  MessageRequester("Error", "Invalid note duration on line " + Str(line_count + header_count), #MB_ICONERROR) 
EndSelect 

;HERE WE CREATE AN EVENT TO STOP THE NOTE WHEN IT IS FINISHED PLAYING 
  channel(event_ct) = channel_ct 
  velocity(event_ct) = 0 ;SHUT OFF THE NOTE 
  note(event_ct) = (octave * 12) + temp_note 
  start(event_ct) = running_ticks + duration + tie_duration 

tie_ct + 1 

Wend 
  
EndIf 

pos = pos + 2 

Wend 

Next ;END OF For event_channels 

EndIf ; <> "t" 

running_ticks + duration 
line_count + 1 
Goto parse_loop 

EndIf 

EndProcedure 

Procedure Read_Header()

header_count = 0
 
While LCase(Left(st$,5)) <> "start" 
  st$ = LCase(ReadString(1))
  
  If Right(st$, 1) = ","
  t = Len(st$)
  While Mid(st$, t, 1) = ","
    t - 1
  Wend
  st$ = Left(st$, t)
  EndIf
header_count + 1  
  ;Debug st$
  ;End
   
  Select Left(st$, 1) 
    Case "t": bpm = Val(StringField(st$,2,",")) 
    Case "i" 
      inst$ = StringField(st$,1,",") 
      Select inst$ 
        Case "i1": instrument(1) = Val(StringField(st$,2,",")) 
        Case "i2": instrument(2) = Val(StringField(st$,2,",")) 
        Case "i3": instrument(3) = Val(StringField(st$,2,",")) 
        Case "i4": instrument(4) = Val(StringField(st$,2,",")) 
        Case "i5": instrument(5) = Val(StringField(st$,2,",")) 
        Case "i6": instrument(6) = Val(StringField(st$,2,",")) 
        Case "i7": instrument(7) = Val(StringField(st$,2,",")) 
        Case "i8": instrument(8) = Val(StringField(st$,2,",")) 
        Case "i9": instrument(9) = Val(StringField(st$,2,",")) 
        Case "i10": instrument(10) = Val(StringField(st$,2,",")) 
        Case "i11": instrument(11) = Val(StringField(st$,2,",")) 
        Case "i12": instrument(12) = Val(StringField(st$,2,",")) 
        Case "i13": instrument(13) = Val(StringField(st$,2,",")) 
        Case "i14": instrument(14) = Val(StringField(st$,2,",")) 
        Case "i15": instrument(15) = Val(StringField(st$,2,",")) 
        Case "i16": instrument(16) = Val(StringField(st$,2,",")) 
      EndSelect 
  EndSelect 
Wend
 
EndProcedure 

Procedure Compile() ;SORT EVENTS BY START TIMES 
index = 1 
For ct2 = 1 To running_ticks 
For ct1 = 1 To event_ct 

  If start(ct1) = ct2 ;And compiled_flag(ct1) = 0 
      compiled_velocity(index) = velocity(ct1) 
      compiled_note(index) = note(ct1) 
      compiled_channel(index) = channel(ct1) 
      compiled_start(index) = start(ct1) 
      index + 1 
  EndIf 

Next 
Next 

EndProcedure 

;InitSound() 
;result = LoadSound(129, "C:\Programs\PureBasic 4.10\source\Flugelhorn_5C_Tuned.wav") 
;If result = 0:   MessageRequester("Error", "Unable to open MIDI output.", #MB_ICONERROR): End: EndIf 
;result = LoadSound(130, "C:\Programs\PureBasic 4.10\source\Flugelhorn_5C_Tuned.wav") 
;If result = 0:   MessageRequester("Error", "Unable to open MIDI output.", #MB_ICONERROR): End: EndIf 

;timesig$ = "4/4" 
;one_beat = Val(Right(timesig$, 1)) 
;one_beat = one_beat / 4 
;bpm.f = 60 
;beat.f = 60000 / bpm 
;timebase.f = (beat * 0.0625 * one_beat) / 2 ;ABLE TO RESOLVE DOTTED SIXTY-FOURTH NOTE 
;SIXTY-FOURTH NOTE = 2 * timebase 
;int_timebase = Roundoff(timebase) 

;system_tick = 0 

ReadFile(1, "tpir.csv") 
read_header() 

;timesig$ = "4/4" 
;one_beat = Val(Right(timesig$, 1)) 
;one_beat = one_beat / 4 
timebase.f = 3600 / bpm ;ABLE TO RESOLVE DOTTED SIXTY-FOURTH NOTE 


;SIXTY-FOURTH NOTE = 2 * timebase 
int_timebase = Roundoff(timebase) 

system_tick = 0 

Parse() 
Compile() 

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 

For channel = 1 To 16 
;If instrument(channel) <= 128 
  SetInstrument(channel, instrument(channel)) 
;  Is_MIDI(channel) = 1 
;Else 
;  Is_MIDI(channel) = 0 
;EndIf 
Next 

midi_event = 1 

If OpenWindow(1,0,0,200,100,"MIDI Ticks",#PB_Window_SystemMenu|#PB_Window_ScreenCentered) = 0 
  MessageRequester("Error", "Unable to open window.", #MB_ICONERROR) 
  End 
EndIf 

;START TIMER BEGIN MIDI PLAYBACK 
tick_clock = SetTimer_(WindowID(1), 1, int_timebase, @tick()) 

Repeat 
  Window_Event = WaitWindowEvent() 
Until Window_Event = #PB_Event_CloseWindow 

midiOutClose_(@hMidiOut) 

If tick_clock 
  KillTimer_(WindowID(1), tick_clock) 
EndIf 
  
;FreeSound(129) 
;FreeSound(130) 

CloseFile(1) 

End
Post Reply