VOGONS

Common searches


First post, by Peter Swinkels

User metadata
Rank Oldbie
Rank
Oldbie

I found this GWBasic code at https://bisqwit.iki.fi/jutut/kuvat/programmin … s/midiplay.html :

 0 REM MIDI PLAYER EXAMPLE PROGRAM COPYRIGHT (C) 2010 JOEL YLILUOMA
1 DEFINT A-Z: OPTION BASE 0: KEY OFF: REM http://iki.fi/bisqwit/

2 DIM adl(180, 11), chins(17), chpan(17), chpit(17)
3 FOR i = 0 TO 180: FOR x = 0 TO 11: READ adl(i, x): NEXT x, i
4 GOTO 100 ' Begin program

5 'Set up OPL parameters (In: c = channel (0..17))
6 'Out: p=I/O port, q = per-OPL channel (0..8), o=operator offset
7 p = &H388 + 2 * (c\9): q = c MOD 9: o = (q MOD 3) + 8 * (q\3): RETURN

8 'OPL_NoteOff(c): c=channel. CHANGES: q,p,o
9 GOSUB 5: OUT p, &HB0 + q: OUT p+1, chpit(c) AND &HDF: RETURN

10 'OPL_NoteOn(c,h#): c=channel,h#=hertz (0..131071). CHANGES: q,p,o,x
11 GOSUB 5
12 x = &H2000: WHILE h# >= 1023.5: h# = h#/2: x=x+&H400: WEND 'Calculate octave
13 x = x + CINT(h#)
14 OUT p, &HA0 + q: OUT p+1, x AND 255: x = x \ 256
15 OUT p, &HB0 + q: OUT p+1, x: chpit(c) = x: RETURN

16 'Technically, AdLib requires some delay between consecutive OUT commands.
17 'However, BASIC is slow enough, that this is not an issue really.
18 'The paradigm "OUT p,index: OUT p+1,value" works perfectly.

20 'OPL_Touch(c,v): c=channel, v=volume. CHANGES: q,p,o,i,v
21 'The formula below: SOLVE(V=127*127 * 2^( (A-63) / 8), A)
22 IF v < 72 THEN v = 0 ELSE v = LOG(v) * 11.541561# - 48.818955#
25 'OPL_Touch_Real(c,v): Same as OPL_Touch, except takes logarithmic volume.
26 GOSUB 5: i = chins(c)
27 OUT p, &H40+o: q = adl(i, 2): GOSUB 29
28 OUT p, &H43+o: q = adl(i, 3)
29 OUT p+1, (q OR 63) - v + ((q AND 63) * v) \ 63: RETURN

30 'OPL_Patch(c): c=channel. CHANGES: q,p,o,x,i
31 GOSUB 5: i = chins(c): q = p+1
32 FOR x = 0 TO 1
33 OUT p, &H20+o+x*3: OUT q, adl(i, 0+x)
34 OUT p, &H60+o+x*3: OUT q, adl(i, 4+x)
35 OUT p, &H80+o+x*3: OUT q, adl(i, 6+x)
36 OUT p, &HE0+o+x*3: OUT q, adl(i, 8+x)
37 NEXT: RETURN

38 'OPL_Pan(c): c=channel. CHANGES: q,p,o
39 GOSUB 5: OUT p, &HC0+q: OUT p+1, adl(chins(c),10)-chpan(c): RETURN

40 'OPL_Reset(). CHANGES: c,q,p,o,x,y,v,i
41 'Detect OPL3 (send pulses to timer, opl3 enable register)
42 c=0: GOSUB 5: FOR y = 3 TO 4: OUT p, 4: OUT p+1, y*32: NEXT: ?INP(p)
43 c=9: GOSUB 5: FOR y = 0 TO 2: OUT p, 5: OUT p+1, y AND 1: NEXT
44 'Reset OPL3 (turn off all notes, set vital settings)
45 c=0: GOSUB 5: OUT p, 1: OUT p+1, 32 'Enable wave
46 c=0: GOSUB 5: OUT p,&HBD:OUT p+1, 0 'Set melodic mode
47 c=9: GOSUB 5: OUT p, 5: OUT p+1, 1 'Enable OPL3
48 c=9: GOSUB 5: OUT p, 4: OUT p+1, 0 'Select mode 0 (also try 63 for fun!)
50 'OPL_Silence(): Silence all OPL channels. CHANGES q,p,o,v,c,i
51 v = 0: FOR c = 0 TO 17: GOSUB 8: GOSUB 25: NEXT: RETURN

70 'ReadString(): Read N bytes from file. Input: x. Output: s$. CHANGES x
71 s$ = "": WHILE x > 0: x = x - 1: GET #1: s$ = s$ + b$: WEND: RETURN
Show last 531 lines

75 'ReadVarLen(): Read variable length int from file. Output: x#. CHANGES x
76 x# = 0
77 GET #1: x = ASC(b$): x# = x# * 128 + (x AND 127): IF x >= 128 THEN 77
78 RETURN

80 'ConvertInteger(): Parses s$ as big-endian integer, stores to x#. CHANGES x
81 x# = 0: FOR x=1 TO LEN(s$): x# = x# * 256 + ASC(MID$(s$, x, 1)): NEXT: RETURN
82 'The reason why # (DOUBLE) is used to store integer values
83 'is that GW-BASIC has only one integer type: INTEGER.
84 'It supports values in the range -32768..+32737. Making it unsuitable
85 'for storing values such as delta-times and track lengths, both of which
86 'are often greater than +32767 in common MIDI files. QuickBASIC supports
87 'the LONG datatype instead, but I was aiming for GW-BASIC compatibility
88 'in order to utilize my cool syntax highlighter for the majority
89 'of the duration of demonstrating this program on Youtube. :)

94 'Ethical subroutine
95 GOTO 97
96 KILL filename$
97 RETURN

100 '*** MAIN MIDI PLAYER PROGRAM ***
101 'Information about each track:
102 DIM tkPtr%(100), tkDelay#(100), tkStatus(100), playwait#
103 'The same, but for loop-beginning:
104 DIM loPtr%(100), loDelay#(100), loStatus(100), loopwait#
105 'The same, but cached just in case loop-beginning must be written:
106 DIM rbPtr%(100), rbDelay#(100), rbStatus(100)

109 'Persistent settings for each MIDI channel.
110 DIM ChPatch(15), ChBend#(15), ChVolume(15), ChPanning(15), ChVibrato(15)

120 'For each active note, we need the following:
121 ' Original note number (ORIGINAL, not simulated)
122 ' Keyoff (Search Key)
123 ' Aftertouch (Search Key)
124 ' Simulated note number
125 ' Keyon -> OPL_NoteOn
126 ' Bend -> OPL_NoteOn
127 ' Keyon/touch pressure
128 ' Ctrl:Volume
129 ' Adlib channel number
130 ' All of above
131 ' MIDI channel (multi search key)
132 ' Bend
133 ' Ctrl:Volume
134 ' Ctrl:Pan
140 DIM ActCount(15) 'number of active notes on each midi channel
141 DIM ActTone(15,127) 'orignotenumber -> simulated notenumber
142 DIM ActAdlChn(15,127) 'orignotenumber -> adlib channel
143 DIM ActVol(15,127) 'orignotenumber -> pressure
144 DIM ActRev(15,127) 'orignotenumber -> activeindex (index to ActList)
145 DIM ActList(15,99) 'activeindex -> orignotenumber (index to ActVol etc)

146 DIM chon(17),chage#(17)'adlchannel -> is_on,age (for channel allocation)
148 DIM chm(17),cha(17) 'adlchannel -> midichn,activeindex (for collisions)
149 DIM chx(17),chc(17) 'adlchannel -> x coordinate, color (for graphics)

160 filename$ = "chmmr.mid" '<- FILENAME OF THE MIDI TO BE PLAYED
161 GOSUB 40 ' Reset AdLib
162 GOSUB 40 ' ...twice (just in case someone misprogrammed OPL3 previously)
163 PLAYmode$ = "T255P64"
164 PLAYmode% = 255 * 64 'Event tick frequency
165 PLAYbuflen = 2 'Number of events to keep buffered
166 COLOR 8: CLS : txtline = 2: PRINT "Press Q to quit; Space to pause"
169 GOSUB 200 ' Load and play MIDI file

170 'Begin main program (this simply monitors the playing status)
171 'Beware that the timer routine accesses a lot of global variables.
172 paused = 0
173 hlt(0) = &HCBF4' HLT; RETF.
174 hlt = VARPTR(hlt(0))
175 CALL hlt
176 ' ^This subroutine saves power. It also makes DOSBox faster.
177 ' However, QuickBASIC has a different syntax for calling ASM
178 ' subroutines, so the line 175 will have to be changed when
179 ' porting to QuickBASIC. Disable it, or try this (qb /Lqb.qlb):
180 'DEF SEG = VARSEG(hlt(0)): CALL ABSOLUTE(hlt)

182 y$ = INKEY$
183 IF y$ <> " " THEN 186
184 paused = 1 - paused
185 IF paused THEN ? "Pause": PLAY STOP ELSE ? "Ok": PLAY ON
186 IF y$ <> "q" AND y$ <> CHR$(27) AND y$ <> CHR$(3) THEN 174
190 PLAY OFF: PRINT "End!": GOSUB 50: KEY ON: END

200 'Subroutine: Load MIDI file
210 OPEN filename$ FOR RANDOM AS #1 LEN = 1: FIELD #1, 1 AS b$ 'Open file

211 'QuickBASIC has BINARY access mode, which allows to use INPUT$() rather
212 'than the silly fixed-size GET command, but GW-BASIC does not support
213 'BINARY, so we use RANDOM here. The difference between BINARY and INPUT
214 'is that INPUT chokes on EOF characters and translates CRLFs, making it
215 'unsuitable for working with MIDI files.

220 x = 4: GOSUB 70: IF s$ <> "MThd" THEN ERROR 13'Invalid type file
230 x = 4: GOSUB 70: GOSUB 80: IF x# <> 6 THEN ERROR 13
231 x = 2: GOSUB 70: GOSUB 80: Fmt = x#
232 x = 2: GOSUB 70: GOSUB 80: TrackCount = x#: IF TrackCount>100 THEN ERROR 6
233 x = 2: GOSUB 70: GOSUB 80: DeltaTicks = x#
234 InvDeltaTicks# = PLAYmode% / (240000000# * DeltaTicks)
240 Tempo# = 1000000# * InvDeltaTicks#
241 bendsense# = 2 / 8192#
250 FOR tk = 1 TO TrackCount
251 x = 4: GOSUB 70: IF s$ <> "MTrk" THEN ERROR 13'Invalid type file
252 x = 4: GOSUB 70: GOSUB 80: TrackLength% = x#: y% = LOC(1)
253 GOSUB 75: tkDelay#(tk) = x# 'Read next event time
254 tkPtr%(tk) = LOC(1) 'Save first event file position
255 GET #1, y% + TrackLength% 'Skip to beginning of next track header
270 NEXT
275 FOR a = 0 TO 15: ChVolume(a) = 127: NEXT
281 PLAY ON: ON PLAY(PLAYbuflen) GOSUB 300 'Set up periodic event handler
282 began = 0: loopStart = 1: playwait# = 0: PLAY "MLMB"

300 'Subroutine: Timer callback. Called when the PLAY buffer is exhausted.
301 IF began THEN playwait# = playwait# - 1#
302 'For each track where delay=0, parse events and read new delay.
303 WHILE playwait# < .5#: GOSUB 310: WEND
304 'Repopulate the PLAY buffer
305 WHILE PLAY(0) < PLAYbuflen: PLAY PLAYmode$: WEND: RETURN

310 'Subroutine: Process events on any track that is due
311 FOR tk = 1 TO TrackCount
312 rbPtr%(tk) = tkPtr%(tk)
313 rbDelay#(tk) = tkDelay#(tk)
314 rbStatus(tk) = tkStatus(tk)
315 IF tkStatus(tk) < 0 OR tkDelay#(tk) > 0 THEN 319
316 GOSUB 350 'Handle event
317 GOSUB 75: tkDelay#(tk) = tkDelay#(tk) + x# 'Read next event time
318 tkPtr%(tk) = LOC(1) 'Save modified file position
319 NEXT
320 IF loopStart = 0 THEN 338
321 'Save loop begin
322 FOR tk = 1 TO TrackCount
323 loPtr%(tk) = rbPtr%(tk)
324 loDelay#(tk) = rbDelay#(tk)
325 loStatus(tk) = rbStatus(tk)
326 NEXT
327 loopwait# = playwait#
328 loopStart = 0
329 GOTO 338
330 'Return to loop beginning
331 FOR tk = 1 TO TrackCount
332 tkPtr%(tk) = loPtr%(tk)
333 tkDelay#(tk) = loDelay#(tk)
334 tkStatus(tk) = loStatus(tk)
335 NEXT
336 loopEnd = 0
337 playwait# = loopwait#
338 'Find shortest delay from all tracks
339 nd# = -1
340 FOR tk = 1 TO TrackCount
341 IF tkStatus(tk) < 0 THEN 343
342 IF nd# = -1 OR tkDelay#(tk) < nd# THEN nd# = tkDelay#(tk)
343 NEXT
344 'Schedule the next playevent to be processed after that delay
345 FOR tk = 1 TO TrackCount: tkDelay#(tk) = tkDelay#(tk) - nd#: NEXT
346 t# = nd# * Tempo#: IF began THEN playwait# = playwait# + t#
347 FOR a = 0 TO 17: chage#(a) = chage#(a) + t#: NEXT
348 IF t# < 0 OR loopEnd THEN 330 ELSE RETURN 'Loop or done

350 'Subroutine: Read an event from track, and read next delay. Input: tk

351 'Note that we continuously access the disk file during playback.
352 'This is perfectly fine when we are running on a modern OS, possibly
353 'under the encapsulation of DOSBox, but if you're running this on
354 'vanilla MS-DOS without e.g. SMARTDRV, you are going to find this
355 'program very unsuitable for your MIDI playing needs, for the disk
356 'drive would probably make more noise than the soundcard does.
357 'We cannot cache tracks because of memory constraints in GW-BASIC.
358 'In QBASIC, we could do better, but it would be cumbersome to implement.

359 GET #1, tkPtr%(tk) + 1: b = ASC(b$)
360 IF b < &HF0 THEN 380
361 'Subroutine for Fx special events
362 '? tk;":";HEX$(b);" at $";hex$(LOC(1))
363 IF b = &HF7 OR b = &HF0 THEN 76'SysEx. Ignore Varlen.
364 IF b = &HF3 THEN GET #1: RETURN
365 IF b = &HF2 THEN GET #1: GET #1: RETURN
370 'Subroutine for special event FF
371 GET #1: evtype = ASC(b$): GOSUB 75: x = x#: GOSUB 70
372 IF evtype = &H2F THEN tkStatus(tk) = -1
373 IF evtype = &H51 THEN GOSUB 80: Tempo# = x# * InvDeltaTicks#
374 IF evtype = 6 AND s$ = "loopStart" THEN loopStart = 1
375 IF evtype = 6 AND s$ = "loopEnd" THEN loopEnd = 1
376 IF evtype < 1 OR evtype > 6 THEN RETURN
377 txtline = 3 + (txtline - 2) MOD 20
378 LOCATE txtline, 1: COLOR 8: PRINT "Meta"; evtype; ": "; s$: RETURN

380 'Subroutine for any normal event (80..EF)
381 IF b < 128 THEN b = tkStatus(tk) OR &H80: GET #1, tkPtr%(tk)
382 MidCh = b AND 15: tkStatus(tk) = b
383 '? tk;":";HEX$(b);" at $";hex$(LOC(1))
384 ON b\16 - 7 GOTO 400,420,460,470,490,492,495 'Choose event handler

400 'Event: 8x Note Off
401 GET #1: note = ASC(b$): GET #1': vol=ASC(b$)
402 ChBend#(MidCh) = 0
403 n = ActRev(MidCh,note): IF n = 0 THEN RETURN
404 m = MidCh: GOTO 600 'deallocate active note, and return

420 'Event: 9x Note On
421 GET #1: note = ASC(b$): GET #1: vol = ASC(b$)
422 IF vol = 0 THEN 402' Sometimes noteoffs are optimized as 0-vol noteons
423 IF ActRev(MidCh,note) THEN RETURN 'Ignore repeat notes w/o keyoffs
424 'Determine the instrument and the note value (tone)
425 tone = note: i = ChPatch(MidCh)
426 IF MidCh = 9 THEN i = 128+note-35: tone = adl(i,11) 'Translate percussion
427 '(MIDI channel 9 always plays percussion and ignores the patch number)
429 'Allocate AdLib channel (the physical sound channel for the note)
430 bs# = -9: c = -1
431 FOR a = 0 TO 17
432 s# = chage#(a)
433 IF chon(a) = 0 THEN s# = s# + 3d3 ' Empty channel = privileged
434 IF chins(a) = i THEN s# = s# + .2# ' Same instrument = good
435 IF i<128 AND chins(a)>127 THEN s# = s#*2+9'Percussion is inferior to melody
436 IF s# > bs# THEN bs# = s#: c = a ' Best candidate wins
437 NEXT
438 IF chon(c) THEN m=chm(c): n=cha(c): GOSUB 600 'Collision: Kill old note
439 chon(c) = 1: chins(c) = i: chage#(c) = 0: began = 1
440 'Allocate active note for MIDI channel
441 '"Active note" helps dealing with further events affecting this note.
442 n = ActCount(MidCh) + 1
443 ActList(MidCh, n) = note
444 ActRev(MidCh,note) = n
445 ActCount(MidCh) = n
449 'Record info about this note
450 ActTone(MidCh,note) = tone
451 ActAdlChn(MidCh,note) = c
452 ActVol(MidCh,note) = vol
453 chm(c) = MidCh: cha(c) = n ' Save this note's origin so collision works.
454 GOSUB 30 ' OPL_Patch
455 GOSUB 530' OPL_Pan with ChPanning
456 GOSUB 540' OPL_Touch with ChVolume
457 chx(c) = 1 + (tone+63)MOD 80: chc(c) = 9+(chins(c)MOD 6)
458 LOCATE 20-c, chx(c): COLOR chc(c): PRINT "#";
459 GOTO 520 ' OPL_NoteOn with ChBend, and return

460 'Event: Ax Note touch
461 GET #1: note = ASC(b$): GET #1: vol = ASC(b$)
462 IF ActRev(MidCh,note) = 0 THEN RETURN'Ignore touch if note is not active
463 c = ActAdlChn(MidCh,note)
464 LOCATE 20-c, chx(c): COLOR chc(c): PRINT "&";
465 ActVol(MidCh,note) = vol
466 GOTO 540 'update note volume, and return

470 'Event: Bx Controller change
471 GET #1: ctrlno = ASC(b$): GET #1: value = ASC(b$)
472 IF ctrlno = 1 THEN ChVibrato(MidCh) = value 'TODO: handle
473 IF ctrlno = 6 THEN bendsense# = value / 8192#
474 IF ctrlno = 7 THEN ChVolume(MidCh) = value: mop = 2: GOTO 500
475 IF ctrlno = 10 THEN 482 'Pan
476 IF ctrlno = 121 THEN 486 'Reset controllers
477 IF ctrlno = 123 THEN mop=5: GOTO 500 'All notes off on channel
478 'Other ctrls worth considering:
479 ' 0 = choose bank (bank+patch identifies the instrument)
480 ' 64 = sustain pedal (when used, noteoff does not produce an adlib keyoff)
481 RETURN
482 'Ctrl 10: Alter the panning of the channel:
483 p = 0: IF value < 48 THEN p = 32 ELSE IF value > 79 THEN p = 16
484 ChPanning(MidCh) = p
485 mop=4: GOTO 500
486 'Ctrl 121: Reset all controllers to their default values.
487 ChBend#(MidCh)=0: ChVibrato(MidCh)=0: ChPan(MidCh)=0
488 mop=1: GOSUB 500: mop=2: GOSUB 500: mop=4: GOTO 500

490 'Event: Cx Patch change
491 GET #1: ChPatch(MidCh) = ASC(b$): RETURN

492 'Event: Dx Channel after-touch
493 GET #1: vol = ASC(b$): mop=3:GOTO 500 'TODO: Verify, is this correct action?

495 'Event: Ex Wheel/bend
496 GET #1: a = ASC(b$): GET #1
497 ChBend#(MidCh) = (a + ASC(b$) * 128 - 8192) * bendsense#
498 mop = 1: GOTO 500 'Update pitches, and return

500 'Subroutine: Update all live notes. Input: MidCh,mop
501 'Update when mop: 1=pitches; 2=volumes; 3=pressures; 4=pans, 5=off
502 x1 = ActCount(MidCh)
503 FOR a = 1 TO x1
504 note = ActList(MidCh, a)
505 c = ActAdlChn(MidCh,note): ON mop GOSUB 508,509,510,530,511
506 NEXT
507 RETURN
508 tone = ActTone(MidCh,note): GOTO 520
509 vol = ActVol(MidCh, note): GOTO 540
510 ActVol(MidCh,note) = vol: GOTO 540
511 m = MidCh: n = a: GOTO 600

520 'Subroutine: Update note pitch. Input: c,MidCh,tone. CHANGES q,p,o,x,h#
521 ' 907*2^(n/12) * (8363) / 44100
522 h# = 172.00093# * EXP(.057762265#*(tone+ChBend#(MidCh))): GOTO 10'OPL_NoteOn

530 'Subroutine: Update note pan. Input: c,MidCh
531 chpan(c) = ChPanning(MidCh): GOTO 38 'OPL_Pan

540 'Subroutine: Update note volume. Input: c,MidCh,vol. CHANGES q,p,o,v
541 v = vol * ChVolume(MidCh): GOTO 20 'OPL_Touch

600 'Subroutine: Deallocate active note (m = MidCh, n = index). CHANGES c,x,q
601 'Uses m instead of MidCh because called also from alloc-collision code.
602 x = ActCount(m) ' Find how many active notes
603 q = ActList(m,n) ' q=note to be deactivated
604 ActRev(m,q) = 0 ' that note is no more
605 ActCount(m) = x-1 ' The list is now shorter
606 c = ActAdlChn(m,q) ' But wait, which adlib channel was it on, again?
607 chon(c)=0: chage#(c)=0: GOSUB 8'OPL_NoteOff
608 LOCATE 20-c, chx(c): COLOR 1: PRINT ".";
610 IF n = x THEN RETURN' Did we delete the last note?
611 q = ActList(m,x) ' q = last note in list
612 ActList(m,n) = q ' move into the deleted slot
613 ActRev(m,q) = n
614 cha(ActAdlChn(m,q)) = n
615 RETURN

760 'This FM Instrument Data comes from Miles Sound System, as used
761 'in the following PC games: Star Control III and Asterix, under
762 'the name AIL (Audio Interface Library). Today, AIL has been
763 'released as "open-source freeware" under the name Miles Sound System.
764 'AIL was used in more than fifty PC games, but so far, I have found
765 'this particular set of General MIDI FM patches only in SC3 and Asterix.
766 'Other games using AIL used different sets of FM patches. There is no
767 'particular reason for preferring this patch set, and in fact, the
768 'descendant of this program, ADLMIDI, http://iki.fi/source/adlmidi.html ,
769 'features a large set of different FM patch sets to choose from.

773 'In the Youtube video, I enter this huge blob of DATA lines very quickly
774 'by using a preprogrammed input TSR, "inputter", which I made just for
775 'this purpose.
776 'It inputs the inline command "COLOR 0,4", turning text into black on red,
777 'and then starts entering DATA lines in an arbitrary geometrical fashion.
778 'After inputting, the text color is reset to normal with "COLOR 7,0".
779 'Of course, the whole time, the poor syntax highlighter TSR (synhili)
780 'does its best to make sense of whatever is being displayed on the screen,
781 'causing the text to stay red only for a short while, whereever the cursor
782 'was last. The resulting effect looks very cool, and most importantly,
783 'the long sequence of DATA gets input in a non-boring manner.

790 'The data bytes are:
791 ' [0,1] AM/VIB/EG/KSR/Multiple bits for carrier and modulator respectively
792 ' [2,3] KSL/Attenuation settings for carrier and modulator respectively
793 ' [4,5] Attack and decay rates for carrier and modulator respectively
794 ' [6,7] Sustain and release rates for carrier and modulator respectively
795 ' [8,9] Wave select settings for carrier and modulator respectively
796 ' [10] Feedback/connection bits for the channel (also stereo/pan bits)
797 ' [11] For percussive instruments (GP35..GP87), the tone to play

800 DATA 1, 1,143, 6,242,242,244,247,0,0, 56, 0: REM GM1:AcouGrandPiano
801 DATA 1, 1, 75, 0,242,242,244,247,0,0, 56, 0: REM GM2:BrightAcouGrand
802 DATA 1, 1, 73, 0,242,242,244,246,0,0, 56, 0: REM GM3:ElecGrandPiano
803 DATA 129, 65, 18, 0,242,242,247,247,0,0, 54, 0: REM GM4:Honky-tonkPiano
804 DATA 1, 1, 87, 0,241,242,247,247,0,0, 48, 0: REM GM5:Rhodes Piano
805 DATA 1, 1,147, 0,241,242,247,247,0,0, 48, 0: REM GM6:Chorused Piano
806 DATA 1, 22,128, 14,161,242,242,245,0,0, 56, 0: REM GM7:Harpsichord
807 DATA 1, 1,146, 0,194,194,248,248,0,0, 58, 0: REM GM8:Clavinet
808 DATA 12,129, 92, 0,246,243,244,245,0,0, 48, 0: REM GM9:Celesta
809 DATA 7, 17,151,128,243,242,242,241,0,0, 50, 0: REM GM10:Glockenspiel
810 DATA 23, 1, 33, 0, 84,244,244,244,0,0, 50, 0: REM GM11:Music box
811 DATA 152,129, 98, 0,243,242,246,246,0,0, 48, 0: REM GM12:Vibraphone
812 DATA 24, 1, 35, 0,246,231,246,247,0,0, 48, 0: REM GM13:Marimba
813 DATA 21, 1,145, 0,246,246,246,246,0,0, 52, 0: REM GM14:Xylophone
814 DATA 69,129, 89,128,211,163,243,243,0,0, 60, 0: REM GM15:Tubular Bells
815 DATA 3,129, 73,128,117,181,245,245,1,0, 52, 0: REM GM16:Dulcimer
816 DATA 113, 49,146, 0,246,241, 20, 7,0,0, 50, 0: REM GM17:Hammond Organ
817 DATA 114, 48, 20, 0,199,199, 88, 8,0,0, 50, 0: REM GM18:Percussive Organ
818 DATA 112,177, 68, 0,170,138, 24, 8,0,0, 52, 0: REM GM19:Rock Organ
819 DATA 35,177,147, 0,151, 85, 35, 20,1,0, 52, 0: REM GM20:Church Organ
820 DATA 97,177, 19,128,151, 85, 4, 4,1,0, 48, 0: REM GM21:Reed Organ
821 DATA 36,177, 72, 0,152, 70, 42, 26,1,0, 60, 0: REM GM22:Accordion
822 DATA 97, 33, 19, 0,145, 97, 6, 7,1,0, 58, 0: REM GM23:Harmonica
823 DATA 33,161, 19,137,113, 97, 6, 7,0,0, 54, 0: REM GM24:Tango Accordion
824 DATA 2, 65,156,128,243,243,148,200,1,0, 60, 0: REM GM25:Acoustic Guitar1
825 DATA 3, 17, 84, 0,243,241,154,231,1,0, 60, 0: REM GM26:Acoustic Guitar2
826 DATA 35, 33, 95, 0,241,242, 58,248,0,0, 48, 0: REM GM27:Electric Guitar1
827 DATA 3, 33,135,128,246,243, 34,248,1,0, 54, 0: REM GM28:Electric Guitar2
828 DATA 3, 33, 71, 0,249,246, 84, 58,0,0, 48, 0: REM GM29:Electric Guitar3
829 DATA 35, 33, 74, 5,145,132, 65, 25,1,0, 56, 0: REM GM30:Overdrive Guitar
830 DATA 35, 33, 74, 0,149,148, 25, 25,1,0, 56, 0: REM GM31:Distorton Guitar
831 DATA 9,132,161,128, 32,209, 79,248,0,0, 56, 0: REM GM32:Guitar Harmonics
832 DATA 33,162, 30, 0,148,195, 6,166,0,0, 50, 0: REM GM33:Acoustic Bass
833 DATA 49, 49, 18, 0,241,241, 40, 24,0,0, 58, 0: REM GM34:Electric Bass 1
834 DATA 49, 49,141, 0,241,241,232,120,0,0, 58, 0: REM GM35:Electric Bass 2
835 DATA 49, 50, 91, 0, 81,113, 40, 72,0,0, 60, 0: REM GM36:Fretless Bass
836 DATA 1, 33,139, 64,161,242,154,223,0,0, 56, 0: REM GM37:Slap Bass 1
837 DATA 33, 33,139, 8,162,161, 22,223,0,0, 56, 0: REM GM38:Slap Bass 2
838 DATA 49, 49,139, 0,244,241,232,120,0,0, 58, 0: REM GM39:Synth Bass 1
839 DATA 49, 49, 18, 0,241,241, 40, 24,0,0, 58, 0: REM GM40:Synth Bass 2
840 DATA 49, 33, 21, 0,221, 86, 19, 38,1,0, 56, 0: REM GM41:Violin
841 DATA 49, 33, 22, 0,221,102, 19, 6,1,0, 56, 0: REM GM42:Viola
842 DATA 113, 49, 73, 0,209, 97, 28, 12,1,0, 56, 0: REM GM43:Cello
843 DATA 33, 35, 77,128,113,114, 18, 6,1,0, 50, 0: REM GM44:Contrabass
844 DATA 241,225, 64, 0,241,111, 33, 22,1,0, 50, 0: REM GM45:Tremulo Strings
845 DATA 2, 1, 26,128,245,133,117, 53,1,0, 48, 0: REM GM46:Pizzicato String
846 DATA 2, 1, 29,128,245,243,117,244,1,0, 48, 0: REM GM47:Orchestral Harp
847 DATA 16, 17, 65, 0,245,242, 5,195,1,0, 50, 0: REM GM48:Timpany
848 DATA 33,162,155, 1,177,114, 37, 8,1,0, 62, 0: REM GM49:String Ensemble1
849 DATA 161, 33,152, 0,127, 63, 3, 7,1,1, 48, 0: REM GM50:String Ensemble2
850 DATA 161, 97,147, 0,193, 79, 18, 5,0,0, 58, 0: REM GM51:Synth Strings 1
851 DATA 33, 97, 24, 0,193, 79, 34, 5,0,0, 60, 0: REM GM52:SynthStrings 2
852 DATA 49,114, 91,131,244,138, 21, 5,0,0, 48, 0: REM GM53:Choir Aahs
853 DATA 161, 97,144, 0,116,113, 57,103,0,0, 48, 0: REM GM54:Voice Oohs
854 DATA 113,114, 87, 0, 84,122, 5, 5,0,0, 60, 0: REM GM55:Synth Voice
855 DATA 144, 65, 0, 0, 84,165, 99, 69,0,0, 56, 0: REM GM56:Orchestra Hit
856 DATA 33, 33,146, 1,133,143, 23, 9,0,0, 60, 0: REM GM57:Trumpet
857 DATA 33, 33,148, 5,117,143, 23, 9,0,0, 60, 0: REM GM58:Trombone
858 DATA 33, 97,148, 0,118,130, 21, 55,0,0, 60, 0: REM GM59:Tuba
859 DATA 49, 33, 67, 0,158, 98, 23, 44,1,1, 50, 0: REM GM60:Muted Trumpet
860 DATA 33, 33,155, 0, 97,127,106, 10,0,0, 50, 0: REM GM61:French Horn
861 DATA 97, 34,138, 6,117,116, 31, 15,0,0, 56, 0: REM GM62:Brass Section
862 DATA 161, 33,134,131,114,113, 85, 24,1,0, 48, 0: REM GM63:Synth Brass 1
863 DATA 33, 33, 77, 0, 84,166, 60, 28,0,0, 56, 0: REM GM64:Synth Brass 2
864 DATA 49, 97,143, 0,147,114, 2, 11,1,0, 56, 0: REM GM65:Soprano Sax
865 DATA 49, 97,142, 0,147,114, 3, 9,1,0, 56, 0: REM GM66:Alto Sax
866 DATA 49, 97,145, 0,147,130, 3, 9,1,0, 58, 0: REM GM67:Tenor Sax
867 DATA 49, 97,142, 0,147,114, 15, 15,1,0, 58, 0: REM GM68:Baritone Sax
868 DATA 33, 33, 75, 0,170,143, 22, 10,1,0, 56, 0: REM GM69:Oboe
869 DATA 49, 33,144, 0,126,139, 23, 12,1,1, 54, 0: REM GM70:English Horn
870 DATA 49, 50,129, 0,117, 97, 25, 25,1,0, 48, 0: REM GM71:Bassoon
871 DATA 50, 33,144, 0,155,114, 33, 23,0,0, 52, 0: REM GM72:Clarinet
872 DATA 225,225, 31, 0,133,101, 95, 26,0,0, 48, 0: REM GM73:Piccolo
873 DATA 225,225, 70, 0,136,101, 95, 26,0,0, 48, 0: REM GM74:Flute
874 DATA 161, 33,156, 0,117,117, 31, 10,0,0, 50, 0: REM GM75:Recorder
875 DATA 49, 33,139, 0,132,101, 88, 26,0,0, 48, 0: REM GM76:Pan Flute
876 DATA 225,161, 76, 0,102,101, 86, 38,0,0, 48, 0: REM GM77:Bottle Blow
877 DATA 98,161,203, 0,118, 85, 70, 54,0,0, 48, 0: REM GM78:Shakuhachi
878 DATA 98,161,153, 0, 87, 86, 7, 7,0,0, 59, 0: REM GM79:Whistle
879 DATA 98,161,147, 0,119,118, 7, 7,0,0, 59, 0: REM GM80:Ocarina
880 DATA 34, 33, 89, 0,255,255, 3, 15,2,0, 48, 0: REM GM81:Lead 1 squareea
881 DATA 33, 33, 14, 0,255,255, 15, 15,1,1, 48, 0: REM GM82:Lead 2 sawtooth
882 DATA 34, 33, 70,128,134,100, 85, 24,0,0, 48, 0: REM GM83:Lead 3 calliope
883 DATA 33,161, 69, 0,102,150, 18, 10,0,0, 48, 0: REM GM84:Lead 4 chiff
884 DATA 33, 34,139, 0,146,145, 42, 42,1,0, 48, 0: REM GM85:Lead 5 charang
885 DATA 162, 97,158, 64,223,111, 5, 7,0,0, 50, 0: REM GM86:Lead 6 voice
886 DATA 32, 96, 26, 0,239,143, 1, 6,0,2, 48, 0: REM GM87:Lead 7 fifths
887 DATA 33, 33,143,128,241,244, 41, 9,0,0, 58, 0: REM GM88:Lead 8 brass
888 DATA 119,161,165, 0, 83,160,148, 5,0,0, 50, 0: REM GM89:Pad 1 new age
889 DATA 97,177, 31,128,168, 37, 17, 3,0,0, 58, 0: REM GM90:Pad 2 warm
890 DATA 97, 97, 23, 0,145, 85, 52, 22,0,0, 60, 0: REM GM91:Pad 3 polysynth
891 DATA 113,114, 93, 0, 84,106, 1, 3,0,0, 48, 0: REM GM92:Pad 4 choir
892 DATA 33,162,151, 0, 33, 66, 67, 53,0,0, 56, 0: REM GM93:Pad 5 bowedpad
893 DATA 161, 33, 28, 0,161, 49,119, 71,1,1, 48, 0: REM GM94:Pad 6 metallic
894 DATA 33, 97,137, 3, 17, 66, 51, 37,0,0, 58, 0: REM GM95:Pad 7 halo
895 DATA 161, 33, 21, 0, 17,207, 71, 7,1,0, 48, 0: REM GM96:Pad 8 sweep
896 DATA 58, 81,206, 0,248,134,246, 2,0,0, 50, 0: REM GM97:FX 1 rain
897 DATA 33, 33, 21, 0, 33, 65, 35, 19,1,0, 48, 0: REM GM98:FX 2 soundtrack
898 DATA 6, 1, 91, 0,116,165,149,114,0,0, 48, 0: REM GM99:FX 3 crystal
899 DATA 34, 97,146,131,177,242,129, 38,0,0, 60, 0: REM GM100:FX 4 atmosphere
900 DATA 65, 66, 77, 0,241,242, 81,245,1,0, 48, 0: REM GM101:FX 5 brightness
901 DATA 97,163,148,128, 17, 17, 81, 19,1,0, 54, 0: REM GM102:FX 6 goblins
902 DATA 97,161,140,128, 17, 29, 49, 3,0,0, 54, 0: REM GM103:FX 7 echoes
903 DATA 164, 97, 76, 0,243,129,115, 35,1,0, 52, 0: REM GM104:FX 8 sci-fi
904 DATA 2, 7,133, 3,210,242, 83,246,0,1, 48, 0: REM GM105:Sitar
905 DATA 17, 19, 12,128,163,162, 17,229,1,0, 48, 0: REM GM106:Banjo
906 DATA 17, 17, 6, 0,246,242, 65,230,1,2, 52, 0: REM GM107:Shamisen
907 DATA 147,145,145, 0,212,235, 50, 17,0,1, 56, 0: REM GM108:Koto
908 DATA 4, 1, 79, 0,250,194, 86, 5,0,0, 60, 0: REM GM109:Kalimba
909 DATA 33, 34, 73, 0,124,111, 32, 12,0,1, 54, 0: REM GM110:Bagpipe
910 DATA 49, 33,133, 0,221, 86, 51, 22,1,0, 58, 0: REM GM111:Fiddle
911 DATA 32, 33, 4,129,218,143, 5, 11,2,0, 54, 0: REM GM112:Shanai
912 DATA 5, 3,106,128,241,195,229,229,0,0, 54, 0: REM GM113:Tinkle Bell
913 DATA 7, 2, 21, 0,236,248, 38, 22,0,0, 58, 0: REM GM114:Agogo Bells
914 DATA 5, 1,157, 0,103,223, 53, 5,0,0, 56, 0: REM GM115:Steel Drums
915 DATA 24, 18,150, 0,250,248, 40,229,0,0, 58, 0: REM GM116:Woodblock
916 DATA 16, 0,134, 3,168,250, 7, 3,0,0, 54, 0: REM GM117:Taiko Drum
917 DATA 17, 16, 65, 3,248,243, 71, 3,2,0, 52, 0: REM GM118:Melodic Tom
918 DATA 1, 16,142, 0,241,243, 6, 2,2,0, 62, 0: REM GM119:Synth Drum
919 DATA 14,192, 0, 0, 31, 31, 0,255,0,3, 62, 0: REM GM120:Reverse Cymbal
920 DATA 6, 3,128,136,248, 86, 36,132,0,2, 62, 0: REM GM121:Guitar FretNoise
921 DATA 14,208, 0, 5,248, 52, 0, 4,0,3, 62, 0: REM GM122:Breath Noise
922 DATA 14,192, 0, 0,246, 31, 0, 2,0,3, 62, 0: REM GM123:Seashore
923 DATA 213,218,149, 64, 55, 86,163, 55,0,0, 48, 0: REM GM124:Bird Tweet
924 DATA 53, 20, 92, 8,178,244, 97, 21,2,0, 58, 0: REM GM125:Telephone
925 DATA 14,208, 0, 0,246, 79, 0,245,0,3, 62, 0: REM GM126:Helicopter
926 DATA 38,228, 0, 0,255, 18, 1, 22,0,1, 62, 0: REM GM127:Applause/Noise
927 DATA 0, 0, 0, 0,243,246,240,201,0,2, 62, 0: REM GM128:Gunshot
928 DATA 16, 17, 68, 0,248,243,119, 6,2,0, 56, 35: REM GP35:Ac Bass Drum
929 DATA 16, 17, 68, 0,248,243,119, 6,2,0, 56, 35: REM GP36:Bass Drum 1
930 DATA 2, 17, 7, 0,249,248,255,255,0,0, 56, 52: REM GP37:Side Stick
931 DATA 0, 0, 0, 0,252,250, 5, 23,2,0, 62, 48: REM GP38:Acoustic Snare
932 DATA 0, 1, 2, 0,255,255, 7, 8,0,0, 48, 58: REM GP39:Hand Clap
933 DATA 0, 0, 0, 0,252,250, 5, 23,2,0, 62, 60: REM GP40:Electric Snare
934 DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 47: REM GP41:Low Floor Tom
935 DATA 12, 18, 0, 0,246,251, 8, 71,0,2, 58, 43: REM GP42:Closed High Hat
936 DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 49: REM GP43:High Floor Tom
937 DATA 12, 18, 0, 5,246,123, 8, 71,0,2, 58, 43: REM GP44:Pedal High Hat
938 DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 51: REM GP45:Low Tom
939 DATA 12, 18, 0, 0,246,203, 2, 67,0,2, 58, 43: REM GP46:Open High Hat
940 DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 54: REM GP47:Low-Mid Tom
941 DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 57: REM GP48:High-Mid Tom
942 DATA 14,208, 0, 0,246,159, 0, 2,0,3, 62, 72: REM GP49:Crash Cymbal 1
943 DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 60: REM GP50:High Tom
944 DATA 14, 7, 8, 74,248,244, 66,228,0,3, 62, 76: REM GP51:Ride Cymbal 1
945 DATA 14,208, 0, 10,245,159, 48, 2,0,0, 62, 84: REM GP52:Chinese Cymbal
946 DATA 14, 7, 10, 93,228,245,228,229,3,1, 54, 36: REM GP53:Ride Bell
947 DATA 2, 5, 3, 10,180,151, 4,247,0,0, 62, 65: REM GP54:Tambourine
948 DATA 78,158, 0, 0,246,159, 0, 2,0,3, 62, 84: REM GP55:Splash Cymbal
949 DATA 17, 16, 69, 8,248,243, 55, 5,2,0, 56, 83: REM GP56:Cow Bell
950 DATA 14,208, 0, 0,246,159, 0, 2,0,3, 62, 84: REM GP57:Crash Cymbal 2
951 DATA 128, 16, 0, 13,255,255, 3, 20,3,0, 60, 24: REM GP58:Vibraslap
952 DATA 14, 7, 8, 74,248,244, 66,228,0,3, 62, 77: REM GP59:Ride Cymbal 2
953 DATA 6, 2, 11, 0,245,245, 12, 8,0,0, 54, 60: REM GP60:High Bongo
954 DATA 1, 2, 0, 0,250,200,191,151,0,0, 55, 65: REM GP61:Low Bongo
955 DATA 1, 1, 81, 0,250,250,135,183,0,0, 54, 59: REM GP62:Mute High Conga
956 DATA 1, 2, 84, 0,250,248,141,184,0,0, 54, 51: REM GP63:Open High Conga
957 DATA 1, 2, 89, 0,250,248,136,182,0,0, 54, 45: REM GP64:Low Conga
958 DATA 1, 0, 0, 0,249,250, 10, 6,3,0, 62, 71: REM GP65:High Timbale
959 DATA 0, 0,128, 0,249,246,137,108,3,0, 62, 60: REM GP66:Low Timbale
960 DATA 3, 12,128, 8,248,246,136,182,3,0, 63, 58: REM GP67:High Agogo
961 DATA 3, 12,133, 0,248,246,136,182,3,0, 63, 53: REM GP68:Low Agogo
962 DATA 14, 0, 64, 8,118,119, 79, 24,0,2, 62, 64: REM GP69:Cabasa
963 DATA 14, 3, 64, 0,200,155, 73,105,0,2, 62, 71: REM GP70:Maracas
964 DATA 215,199,220, 0,173,141, 5, 5,3,0, 62, 61: REM GP71:Short Whistle
965 DATA 215,199,220, 0,168,136, 4, 4,3,0, 62, 61: REM GP72:Long Whistle
966 DATA 128, 17, 0, 0,246,103, 6, 23,3,3, 62, 44: REM GP73:Short Guiro
967 DATA 128, 17, 0, 9,245, 70, 5, 22,2,3, 62, 40: REM GP74:Long Guiro
968 DATA 6, 21, 63, 0, 0,247,244,245,0,0, 49, 69: REM GP75:Claves
969 DATA 6, 18, 63, 0, 0,247,244,245,3,0, 48, 68: REM GP76:High Wood Block
970 DATA 6, 18, 63, 0, 0,247,244,245,0,0, 49, 63: REM GP77:Low Wood Block
971 DATA 1, 2, 88, 0,103,117,231, 7,0,0, 48, 74: REM GP78:Mute Cuica
972 DATA 65, 66, 69, 8,248,117, 72, 5,0,0, 48, 60: REM GP79:Open Cuica
973 DATA 10, 30, 64, 78,224,255,240, 5,3,0, 56, 80: REM GP80:Mute Triangle
974 DATA 10, 30,124, 82,224,255,240, 2,3,0, 56, 64: REM GP81:Open Triangle
975 DATA 14, 0, 64, 8,122,123, 74, 27,0,2, 62, 72: REM GP82
976 DATA 14, 7, 10, 64,228, 85,228, 57,3,1, 54, 73: REM GP83
977 DATA 5, 4, 5, 64,249,214, 50,165,3,0, 62, 70: REM GP84
978 DATA 2, 21, 63, 0, 0,247,243,245,3,0, 56, 68: REM GP85
979 DATA 1, 2, 79, 0,250,248,141,181,0,0, 55, 48: REM GP86
980 DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 53: REM GP87

There are a lot of OUT statements for OPL-3 related stuff accessing some Yamaha chip. I can't find much documentation about the subject online. Does anyone know a few good sources documenting this chip?

Do not read if you don't like attention seeking self-advertisements!

Did you read it anyway? Well, you can find all sorts of stuff I made using various programming languages over here:
https://github.com/peterswinkels

Reply 2 of 8, by Peter Swinkels

User metadata
Rank Oldbie
Rank
Oldbie

@DerBaum: Thank you, do you know of any other sources of information? Those that focus more on programming for the chip? Something with C or even assembly language examples would be nice.

Do not read if you don't like attention seeking self-advertisements!

Did you read it anyway? Well, you can find all sorts of stuff I made using various programming languages over here:
https://github.com/peterswinkels

Reply 3 of 8, by mkarcher

User metadata
Rank l33t
Rank
l33t

I found http://www.shipbrook.net/jeff/sb.html for the OPL2 stuff quite usable, even back in the day when it was distributed as TXT file in BBS networks. There is one major quirk, though: This document perpetuates the popular belief, that OPL2-based PC sound cards can generate timer interrupts. This is false for all mainstream sound cards. The OPL2 indeed has a timer function that works perfectly even on PC sound cards. You can poll the timer output status bits whether a timer interrupt occurred. This is actually used to detect OPL2/OPL3 presence. But the timer interrupt output pin is generally not connected to anything, so you can't generate an interrupt on the PC CPU. Furthermore, most sources (including this one) claim the Adlib card to generate IRQ0 / INT08, which is not even accessible from an ISA card.

For OPL3, you might want to refer to https://www.fit.vutbr.cz/~arnost/opl/opl3.html .

Reply 4 of 8, by Peter Swinkels

User metadata
Rank Oldbie
Rank
Oldbie

@mkarcher: thank you.

Can anyone explain these lines:

20 'OPL_Touch(c,v): c=channel, v=volume. CHANGES: q,p,o,i,v
21 'The formula below: SOLVE(V=127*127 * 2^( (A-63) / 8), A)
22 IF v < 72 THEN v = 0 ELSE v = LOG(v) * 11.541561# - 48.818955#

The volume specified by the v variable is recalculated but does anyone know why those exact fractional numbers are used? Also what does "A" in the comment stand for?

Do not read if you don't like attention seeking self-advertisements!

Did you read it anyway? Well, you can find all sorts of stuff I made using various programming languages over here:
https://github.com/peterswinkels

Reply 5 of 8, by mkarcher

User metadata
Rank l33t
Rank
l33t
Peter Swinkels wrote on 2022-08-06, 20:35:
Can anyone explain these lines: […]
Show full quote

Can anyone explain these lines:

20 'OPL_Touch(c,v): c=channel, v=volume. CHANGES: q,p,o,i,v
21 'The formula below: SOLVE(V=127*127 * 2^( (A-63) / 8), A)
22 IF v < 72 THEN v = 0 ELSE v = LOG(v) * 11.541561# - 48.818955#

The volume specified by the v variable is recalculated but does anyone know why those exact fractional numbers are used? Also what does "A" in the comment stand for?

The comment is an instruction for a computer algebra system like Derive, Maple or Mathematica. Most likely Wolfram Alpha can do that, too, as it is based on Mathematica. The "A" is the value that is to be programmed into the "A"mplitude register of the OPL3 chips, whereas v is the "v"olume requested by the user. SOLVE(...,A) means that you should transform the formula that calculates V from A (and is to be found in an OPL3 specification) to the inverse formula that calculates A from V. The basic code doesn't use the name "A". "A" in that formula is the "new v", whereas "V" in that formula is the "old v". You don't need to have a computer algebra system, if you are fluent in school maths, though. I use LOG2 to mean the logarithm to the base of 2, and LN to mean the natural logarithm. Each line of my code block is derived from the previous line of the code block by the operation indicated in the comment. These operations generally do not change for which value pair of A an V this formula is true.

V = 127*127 * 2^((A-63)/8)           : REM original formula
V = 127*127 * 2^(A/8) / 2^(63/8) : REM split the power term
V = 127*127 * 2^(A/8) / 234.753 : calculated the fixed power of two
V = 68.706 * 2^(A/8) : REM merged constants
LOG2(V) = LOG2(68.706) + A/8 : REM LOG2(...) on both sides
LN(V)/LN(2) = LN(68.706)/LN(2) + A/8 : REM apply the identity LOG2(x) = LN(x) / LN(2)
LN(V) = LN(68.706) + A*LN(2)/8 : REM multiply both sides by LN(2)
LN(V) = 4.2298 + A*0.08664 : REM calculate logarithms of constants
LN(V) = 4.2298 + A/11.542 : REM 1/0.08664 = 11.542
11.542 * LN(V) = 4.2298 * 11.542 + A : REM multiply both sides by 11.542
11.542 * LN(V) = 48.818 + A : REM perform the multiplication
11.542 * LN(V) - 48.818 = A : REM subtract 48.818 on both sides

I was too lazy to carry around enough decimal digits to achieve the same level of precision as the original formula, but knowing that LOG in basic is the natural logarithm (which I wrote as LN), you should immediately recognise that the final form of the relation between A and V is the formula in your basic program.

Reply 6 of 8, by Peter Swinkels

User metadata
Rank Oldbie
Rank
Oldbie

@mkarcher: Thank you.

For anyone who lkes a challenge, I have been trying to untangle the GW-Basic code and this is the result:

'1. Out: q = per-OPL channel (0..8)
'2. LBOUND/UBOUND
'3. Combine arrays into structures.
'4. The program fails to return to the start of MIDI file.
' -It appears the ShortestDelay() function has to return -1 when the end of the MIDI file is reached.

'$DYNAMIC
DEFINT A-Z
OPTION BASE 0
OPTION EXPLICIT 'This program is 100% Quick Basic 4.5 compatible, but OPTION EXPLICIT is convenient when using VBDOS.

CONST EVENTBUFFERSIZE = 2
CONST EVENTTICKFREQUENCY = 16320
CONST FALSE = 0
CONST MEBEND = &HE0
CONST MECONTROLLER = &HB0
CONST MEINSTRUMENT = &HC0
CONST MEMETA = &HFF
CONST MENOTEOFF = &H80
CONST MENOTEON = &H90
CONST MEPOLYPHONIC = &HA0
CONST MEPRESSURE = &HD0
CONST MESONGPOSITION = &HF2
CONST MESONGSELECT = &HF3
CONST MESYSEXSTART = &HF0
CONST MESYSEXEND = &HF7
CONST MMEENDOFTRACK = &H2F
CONST MMESETTEMPO = &H51
CONST MMETCOPYRIGHT = &H2
CONST MMETCUE = &H7
CONST MMETGENERIC = &H1
CONST MMETINSTRUMENT = &H4
CONST MMETLYRIC = &H5
CONST MMETMARKER = &H6
CONST MMETTRACK = &H3
CONST MOPOFF = 1
CONST MOPPAN = 2
CONST MOPPITCH = 3
CONST MOPPRESSURE = 4
CONST MOPVOLUME = 5
CONST MTHDBLOCKDEFAULTLENGTH = &H6
CONST OPLBASE = &H388
CONST OPLENABLEOPL3 = &H1
CONST OPLENABLEWAVE = &H20
CONST OPLMEDOLICMODE = &H0
CONST OPLMODE0 = &H0
CONST SIGN14BIT = &H2000
CONST TRUE = -1

TYPE ChannelStr
ActiveIndex AS INTEGER
Age AS LONG
ColorV AS INTEGER
MIDIChannel AS INTEGER
IsOn AS INTEGER
x AS INTEGER
END TYPE

TYPE ChannelInstrumentStr
Instrument AS INTEGER
Show last 1023 lines
 Panning AS INTEGER
Pitch AS INTEGER
END TYPE

TYPE ChannelSettingsStr
Bend AS LONG
Volume AS INTEGER
Panning AS INTEGER
Patch AS INTEGER
Vibrato AS INTEGER
END TYPE

TYPE MIDIStr
BendSense AS DOUBLE
Format AS INTEGER
InvDeltaTicks AS DOUBLE
Tempo AS DOUBLE
TrackCount AS LONG
END TYPE

TYPE StatusStr
Began AS INTEGER
LoopEnd AS INTEGER
LoopStart AS INTEGER
LoopWait AS LONG
Paused AS INTEGER
PlayWait AS LONG
END TYPE

TYPE TrackStr
Delay AS LONG
Pointer AS LONG
Status AS INTEGER
END TYPE

DECLARE FUNCTION GetOctave# (Hertz AS DOUBLE)
DECLARE FUNCTION GetShortestDelay& (TrackCount AS LONG)
DECLARE FUNCTION ParseBEDWord& (Buffer AS STRING)
DECLARE FUNCTION ParseBEWord (Buffer AS STRING)
DECLARE FUNCTION ReadVariableLengthInteger& ()
DECLARE FUNCTION RemoveSpecialCharacters$ (Text AS STRING)
DECLARE FUNCTION ScheduleEventsAfterDelay# (Delay AS LONG, MIDI AS MIDIStr, Status AS StatusStr)
DECLARE FUNCTION SetUpOPL (Channel, Operator, q)
DECLARE SUB ChangePanning (Channel, Value)
DECLARE SUB ControllerChange (Channel, MIDI AS MIDIStr)
DECLARE SUB DeallocateActiveNote (Channel, Note)
DECLARE SUB Display (Row, Column, ColorV, Text AS STRING)
DECLARE SUB GetInstrumentData ()
DECLARE SUB HandleSpecialTrackEvent (Byte, Track, MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB HandleSpecialTrackMetaEvent (Track, MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB HandleTrackEvent (Track, MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB Initialize (Status AS StatusStr)
DECLARE SUB Main (MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB NoteOff (Channel)
DECLARE SUB NoteOn (Channel, Status AS StatusStr)
DECLARE SUB NoteTouch (Channel)
DECLARE SUB OpenMIDI (FileName AS STRING, MIDI AS MIDIStr)
DECLARE SUB OPLNoteOn (Channel, Hertz AS DOUBLE)
DECLARE SUB OPLNoteOff (Channel)
DECLARE SUB OPLPatch (Channel)
DECLARE SUB OPLReset ()
DECLARE SUB OPLSilence ()
DECLARE SUB OPLTouchReal (Channel, Volume)
DECLARE SUB ProcessTrackEvents (MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB ResetAllControllers (Channel)
DECLARE SUB Quit ()
DECLARE SUB ReturnToLoopStart (MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB SaveLoopStart (MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB UpdateAllLiveNotes (Channel, MIDIOperation, Volume)
DECLARE SUB UpdateNotePan (ActiveChannel, Channel)
DECLARE SUB UpdateNoteVolume (ActiveChannel, Channel, Volume)

DIM MIDI AS MIDIStr
DIM Status AS StatusStr

DIM SHARED ActiveNoteCount(0 TO 15)
DIM SHARED Adlib(0 TO 180, 0 TO 11)
DIM SHARED Channels(0 TO 17) AS ChannelStr
DIM SHARED ChannelInstruments(0 TO 17) AS ChannelInstrumentStr
DIM SHARED ChannelSettings(0 TO 15) AS ChannelSettingsStr
DIM SHARED Loops(0 TO 0) AS TrackStr
DIM SHARED LoopsBackup(0 TO 0) AS TrackStr
DIM SHARED Tracks(0 TO 0) AS TrackStr

DIM SHARED ActTone(0 TO 15, 0 TO 127) 'orignotenumber -> simulated notenumber
DIM SHARED ActAdlChn(0 TO 15, 0 TO 127) 'orignotenumber -> adlib channel
DIM SHARED ActVol(0 TO 15, 0 TO 127) 'orignotenumber -> pressure
DIM SHARED ActRev(0 TO 15, 0 TO 127) 'orignotenumber -> activeindex (index to ActList)
DIM SHARED ActList(0 TO 15, 0 TO 99) 'activeindex -> orignotenumber (index to ActVol etc)

Initialize Status
CALL Main(MIDI, Status)
Quit
END

PlayerLoop:
IF Status.Began THEN Status.PlayWait = Status.PlayWait - 1

DO WHILE Status.PlayWait = 0
ProcessTrackEvents MIDI, Status
LOOP

DO WHILE PLAY(0) < EVENTBUFFERSIZE
PLAY "T255 P64"
LOOP

RETURN

'The data bytes are:
' [0,1] AM/VIB/EG/KSR/Multiple bits for carrier and modulator respectively
' [2,3] KSL/Attenuation settings for carrier and modulator respectively
' [4,5] Attack and decay rates for carrier and modulator respectively
' [6,7] Sustain and release rates for carrier and modulator respectively
' [8,9] Wave select settings for carrier and modulator respectively
' [10] Feedback/connection bits for the channel (also stereo/pan bits)
' [11] For percussive instruments (GP35..GP87), the tone to play

Instruments:
DATA 1, 1,143, 6,242,242,244,247,0,0, 56, 0: REM GM1:AcouGrandPiano
DATA 1, 1, 75, 0,242,242,244,247,0,0, 56, 0: REM GM2:BrightAcouGrand
DATA 1, 1, 73, 0,242,242,244,246,0,0, 56, 0: REM GM3:ElecGrandPiano
DATA 129, 65, 18, 0,242,242,247,247,0,0, 54, 0: REM GM4:Honky-tonkPiano
DATA 1, 1, 87, 0,241,242,247,247,0,0, 48, 0: REM GM5:Rhodes Piano
DATA 1, 1,147, 0,241,242,247,247,0,0, 48, 0: REM GM6:Chorused Piano
DATA 1, 22,128, 14,161,242,242,245,0,0, 56, 0: REM GM7:Harpsichord
DATA 1, 1,146, 0,194,194,248,248,0,0, 58, 0: REM GM8:Clavinet
DATA 12,129, 92, 0,246,243,244,245,0,0, 48, 0: REM GM9:Celesta
DATA 7, 17,151,128,243,242,242,241,0,0, 50, 0: REM GM10:Glockenspiel
DATA 23, 1, 33, 0, 84,244,244,244,0,0, 50, 0: REM GM11:Music box
DATA 152,129, 98, 0,243,242,246,246,0,0, 48, 0: REM GM12:Vibraphone
DATA 24, 1, 35, 0,246,231,246,247,0,0, 48, 0: REM GM13:Marimba
DATA 21, 1,145, 0,246,246,246,246,0,0, 52, 0: REM GM14:Xylophone
DATA 69,129, 89,128,211,163,243,243,0,0, 60, 0: REM GM15:Tubular Bells
DATA 3,129, 73,128,117,181,245,245,1,0, 52, 0: REM GM16:Dulcimer
DATA 113, 49,146, 0,246,241, 20, 7,0,0, 50, 0: REM GM17:Hammond Organ
DATA 114, 48, 20, 0,199,199, 88, 8,0,0, 50, 0: REM GM18:Percussive Organ
DATA 112,177, 68, 0,170,138, 24, 8,0,0, 52, 0: REM GM19:Rock Organ
DATA 35,177,147, 0,151, 85, 35, 20,1,0, 52, 0: REM GM20:Church Organ
DATA 97,177, 19,128,151, 85, 4, 4,1,0, 48, 0: REM GM21:Reed Organ
DATA 36,177, 72, 0,152, 70, 42, 26,1,0, 60, 0: REM GM22:Accordion
DATA 97, 33, 19, 0,145, 97, 6, 7,1,0, 58, 0: REM GM23:Harmonica
DATA 33,161, 19,137,113, 97, 6, 7,0,0, 54, 0: REM GM24:Tango Accordion
DATA 2, 65,156,128,243,243,148,200,1,0, 60, 0: REM GM25:Acoustic Guitar1
DATA 3, 17, 84, 0,243,241,154,231,1,0, 60, 0: REM GM26:Acoustic Guitar2
DATA 35, 33, 95, 0,241,242, 58,248,0,0, 48, 0: REM GM27:Electric Guitar1
DATA 3, 33,135,128,246,243, 34,248,1,0, 54, 0: REM GM28:Electric Guitar2
DATA 3, 33, 71, 0,249,246, 84, 58,0,0, 48, 0: REM GM29:Electric Guitar3
DATA 35, 33, 74, 5,145,132, 65, 25,1,0, 56, 0: REM GM30:Overdrive Guitar
DATA 35, 33, 74, 0,149,148, 25, 25,1,0, 56, 0: REM GM31:Distorton Guitar
DATA 9,132,161,128, 32,209, 79,248,0,0, 56, 0: REM GM32:Guitar Harmonics
DATA 33,162, 30, 0,148,195, 6,166,0,0, 50, 0: REM GM33:Acoustic Bass
DATA 49, 49, 18, 0,241,241, 40, 24,0,0, 58, 0: REM GM34:Electric Bass 1
DATA 49, 49,141, 0,241,241,232,120,0,0, 58, 0: REM GM35:Electric Bass 2
DATA 49, 50, 91, 0, 81,113, 40, 72,0,0, 60, 0: REM GM36:Fretless Bass
DATA 1, 33,139, 64,161,242,154,223,0,0, 56, 0: REM GM37:Slap Bass 1
DATA 33, 33,139, 8,162,161, 22,223,0,0, 56, 0: REM GM38:Slap Bass 2
DATA 49, 49,139, 0,244,241,232,120,0,0, 58, 0: REM GM39:Synth Bass 1
DATA 49, 49, 18, 0,241,241, 40, 24,0,0, 58, 0: REM GM40:Synth Bass 2
DATA 49, 33, 21, 0,221, 86, 19, 38,1,0, 56, 0: REM GM41:Violin
DATA 49, 33, 22, 0,221,102, 19, 6,1,0, 56, 0: REM GM42:Viola
DATA 113, 49, 73, 0,209, 97, 28, 12,1,0, 56, 0: REM GM43:Cello
DATA 33, 35, 77,128,113,114, 18, 6,1,0, 50, 0: REM GM44:Contrabass
DATA 241,225, 64, 0,241,111, 33, 22,1,0, 50, 0: REM GM45:Tremulo Strings
DATA 2, 1, 26,128,245,133,117, 53,1,0, 48, 0: REM GM46:Pizzicato String
DATA 2, 1, 29,128,245,243,117,244,1,0, 48, 0: REM GM47:Orchestral Harp
DATA 16, 17, 65, 0,245,242, 5,195,1,0, 50, 0: REM GM48:Timpany
DATA 33,162,155, 1,177,114, 37, 8,1,0, 62, 0: REM GM49:String Ensemble1
DATA 161, 33,152, 0,127, 63, 3, 7,1,1, 48, 0: REM GM50:String Ensemble2
DATA 161, 97,147, 0,193, 79, 18, 5,0,0, 58, 0: REM GM51:Synth Strings 1
DATA 33, 97, 24, 0,193, 79, 34, 5,0,0, 60, 0: REM GM52:SynthStrings 2
DATA 49,114, 91,131,244,138, 21, 5,0,0, 48, 0: REM GM53:Choir Aahs
DATA 161, 97,144, 0,116,113, 57,103,0,0, 48, 0: REM GM54:Voice Oohs
DATA 113,114, 87, 0, 84,122, 5, 5,0,0, 60, 0: REM GM55:Synth Voice
DATA 144, 65, 0, 0, 84,165, 99, 69,0,0, 56, 0: REM GM56:Orchestra Hit
DATA 33, 33,146, 1,133,143, 23, 9,0,0, 60, 0: REM GM57:Trumpet
DATA 33, 33,148, 5,117,143, 23, 9,0,0, 60, 0: REM GM58:Trombone
DATA 33, 97,148, 0,118,130, 21, 55,0,0, 60, 0: REM GM59:Tuba
DATA 49, 33, 67, 0,158, 98, 23, 44,1,1, 50, 0: REM GM60:Muted Trumpet
DATA 33, 33,155, 0, 97,127,106, 10,0,0, 50, 0: REM GM61:French Horn
DATA 97, 34,138, 6,117,116, 31, 15,0,0, 56, 0: REM GM62:Brass Section
DATA 161, 33,134,131,114,113, 85, 24,1,0, 48, 0: REM GM63:Synth Brass 1
DATA 33, 33, 77, 0, 84,166, 60, 28,0,0, 56, 0: REM GM64:Synth Brass 2
DATA 49, 97,143, 0,147,114, 2, 11,1,0, 56, 0: REM GM65:Soprano Sax
DATA 49, 97,142, 0,147,114, 3, 9,1,0, 56, 0: REM GM66:Alto Sax
DATA 49, 97,145, 0,147,130, 3, 9,1,0, 58, 0: REM GM67:Tenor Sax
DATA 49, 97,142, 0,147,114, 15, 15,1,0, 58, 0: REM GM68:Baritone Sax
DATA 33, 33, 75, 0,170,143, 22, 10,1,0, 56, 0: REM GM69:Oboe
DATA 49, 33,144, 0,126,139, 23, 12,1,1, 54, 0: REM GM70:English Horn
DATA 49, 50,129, 0,117, 97, 25, 25,1,0, 48, 0: REM GM71:Bassoon
DATA 50, 33,144, 0,155,114, 33, 23,0,0, 52, 0: REM GM72:Clarinet
DATA 225,225, 31, 0,133,101, 95, 26,0,0, 48, 0: REM GM73:Piccolo
DATA 225,225, 70, 0,136,101, 95, 26,0,0, 48, 0: REM GM74:Flute
DATA 161, 33,156, 0,117,117, 31, 10,0,0, 50, 0: REM GM75:Recorder
DATA 49, 33,139, 0,132,101, 88, 26,0,0, 48, 0: REM GM76:Pan Flute
DATA 225,161, 76, 0,102,101, 86, 38,0,0, 48, 0: REM GM77:Bottle Blow
DATA 98,161,203, 0,118, 85, 70, 54,0,0, 48, 0: REM GM78:Shakuhachi
DATA 98,161,153, 0, 87, 86, 7, 7,0,0, 59, 0: REM GM79:Whistle
DATA 98,161,147, 0,119,118, 7, 7,0,0, 59, 0: REM GM80:Ocarina
DATA 34, 33, 89, 0,255,255, 3, 15,2,0, 48, 0: REM GM81:Lead 1 squareea
DATA 33, 33, 14, 0,255,255, 15, 15,1,1, 48, 0: REM GM82:Lead 2 sawtooth
DATA 34, 33, 70,128,134,100, 85, 24,0,0, 48, 0: REM GM83:Lead 3 calliope
DATA 33,161, 69, 0,102,150, 18, 10,0,0, 48, 0: REM GM84:Lead 4 chiff
DATA 33, 34,139, 0,146,145, 42, 42,1,0, 48, 0: REM GM85:Lead 5 charang
DATA 162, 97,158, 64,223,111, 5, 7,0,0, 50, 0: REM GM86:Lead 6 voice
DATA 32, 96, 26, 0,239,143, 1, 6,0,2, 48, 0: REM GM87:Lead 7 fifths
DATA 33, 33,143,128,241,244, 41, 9,0,0, 58, 0: REM GM88:Lead 8 brass
DATA 119,161,165, 0, 83,160,148, 5,0,0, 50, 0: REM GM89:Pad 1 new age
DATA 97,177, 31,128,168, 37, 17, 3,0,0, 58, 0: REM GM90:Pad 2 warm
DATA 97, 97, 23, 0,145, 85, 52, 22,0,0, 60, 0: REM GM91:Pad 3 polysynth
DATA 113,114, 93, 0, 84,106, 1, 3,0,0, 48, 0: REM GM92:Pad 4 choir
DATA 33,162,151, 0, 33, 66, 67, 53,0,0, 56, 0: REM GM93:Pad 5 bowedpad
DATA 161, 33, 28, 0,161, 49,119, 71,1,1, 48, 0: REM GM94:Pad 6 metallic
DATA 33, 97,137, 3, 17, 66, 51, 37,0,0, 58, 0: REM GM95:Pad 7 halo
DATA 161, 33, 21, 0, 17,207, 71, 7,1,0, 48, 0: REM GM96:Pad 8 sweep
DATA 58, 81,206, 0,248,134,246, 2,0,0, 50, 0: REM GM97:FX 1 rain
DATA 33, 33, 21, 0, 33, 65, 35, 19,1,0, 48, 0: REM GM98:FX 2 soundtrack
DATA 6, 1, 91, 0,116,165,149,114,0,0, 48, 0: REM GM99:FX 3 crystal
DATA 34, 97,146,131,177,242,129, 38,0,0, 60, 0: REM GM100:FX 4 atmosphere
DATA 65, 66, 77, 0,241,242, 81,245,1,0, 48, 0: REM GM101:FX 5 brightness
DATA 97,163,148,128, 17, 17, 81, 19,1,0, 54, 0: REM GM102:FX 6 goblins
DATA 97,161,140,128, 17, 29, 49, 3,0,0, 54, 0: REM GM103:FX 7 echoes
DATA 164, 97, 76, 0,243,129,115, 35,1,0, 52, 0: REM GM104:FX 8 sci-fi
DATA 2, 7,133, 3,210,242, 83,246,0,1, 48, 0: REM GM105:Sitar
DATA 17, 19, 12,128,163,162, 17,229,1,0, 48, 0: REM GM106:Banjo
DATA 17, 17, 6, 0,246,242, 65,230,1,2, 52, 0: REM GM107:Shamisen
DATA 147,145,145, 0,212,235, 50, 17,0,1, 56, 0: REM GM108:Koto
DATA 4, 1, 79, 0,250,194, 86, 5,0,0, 60, 0: REM GM109:Kalimba
DATA 33, 34, 73, 0,124,111, 32, 12,0,1, 54, 0: REM GM110:Bagpipe
DATA 49, 33,133, 0,221, 86, 51, 22,1,0, 58, 0: REM GM111:Fiddle
DATA 32, 33, 4,129,218,143, 5, 11,2,0, 54, 0: REM GM112:Shanai
DATA 5, 3,106,128,241,195,229,229,0,0, 54, 0: REM GM113:Tinkle Bell
DATA 7, 2, 21, 0,236,248, 38, 22,0,0, 58, 0: REM GM114:Agogo Bells
DATA 5, 1,157, 0,103,223, 53, 5,0,0, 56, 0: REM GM115:Steel Drums
DATA 24, 18,150, 0,250,248, 40,229,0,0, 58, 0: REM GM116:Woodblock
DATA 16, 0,134, 3,168,250, 7, 3,0,0, 54, 0: REM GM117:Taiko Drum
DATA 17, 16, 65, 3,248,243, 71, 3,2,0, 52, 0: REM GM118:Melodic Tom
DATA 1, 16,142, 0,241,243, 6, 2,2,0, 62, 0: REM GM119:Synth Drum
DATA 14,192, 0, 0, 31, 31, 0,255,0,3, 62, 0: REM GM120:Reverse Cymbal
DATA 6, 3,128,136,248, 86, 36,132,0,2, 62, 0: REM GM121:Guitar FretNoise
DATA 14,208, 0, 5,248, 52, 0, 4,0,3, 62, 0: REM GM122:Breath Noise
DATA 14,192, 0, 0,246, 31, 0, 2,0,3, 62, 0: REM GM123:Seashore
DATA 213,218,149, 64, 55, 86,163, 55,0,0, 48, 0: REM GM124:Bird Tweet
DATA 53, 20, 92, 8,178,244, 97, 21,2,0, 58, 0: REM GM125:Telephone
DATA 14,208, 0, 0,246, 79, 0,245,0,3, 62, 0: REM GM126:Helicopter
DATA 38,228, 0, 0,255, 18, 1, 22,0,1, 62, 0: REM GM127:Applause/Noise
DATA 0, 0, 0, 0,243,246,240,201,0,2, 62, 0: REM GM128:Gunshot
DATA 16, 17, 68, 0,248,243,119, 6,2,0, 56, 35: REM GP35:Ac Bass Drum
DATA 16, 17, 68, 0,248,243,119, 6,2,0, 56, 35: REM GP36:Bass Drum 1
DATA 2, 17, 7, 0,249,248,255,255,0,0, 56, 52: REM GP37:Side Stick
DATA 0, 0, 0, 0,252,250, 5, 23,2,0, 62, 48: REM GP38:Acoustic Snare
DATA 0, 1, 2, 0,255,255, 7, 8,0,0, 48, 58: REM GP39:Hand Clap
DATA 0, 0, 0, 0,252,250, 5, 23,2,0, 62, 60: REM GP40:Electric Snare
DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 47: REM GP41:Low Floor Tom
DATA 12, 18, 0, 0,246,251, 8, 71,0,2, 58, 43: REM GP42:Closed High Hat
DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 49: REM GP43:High Floor Tom
DATA 12, 18, 0, 5,246,123, 8, 71,0,2, 58, 43: REM GP44:Pedal High Hat
DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 51: REM GP45:Low Tom
DATA 12, 18, 0, 0,246,203, 2, 67,0,2, 58, 43: REM GP46:Open High Hat
DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 54: REM GP47:Low-Mid Tom
DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 57: REM GP48:High-Mid Tom
DATA 14,208, 0, 0,246,159, 0, 2,0,3, 62, 72: REM GP49:Crash Cymbal 1
DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 60: REM GP50:High Tom
DATA 14, 7, 8, 74,248,244, 66,228,0,3, 62, 76: REM GP51:Ride Cymbal 1
DATA 14,208, 0, 10,245,159, 48, 2,0,0, 62, 84: REM GP52:Chinese Cymbal
DATA 14, 7, 10, 93,228,245,228,229,3,1, 54, 36: REM GP53:Ride Bell
DATA 2, 5, 3, 10,180,151, 4,247,0,0, 62, 65: REM GP54:Tambourine
DATA 78,158, 0, 0,246,159, 0, 2,0,3, 62, 84: REM GP55:Splash Cymbal
DATA 17, 16, 69, 8,248,243, 55, 5,2,0, 56, 83: REM GP56:Cow Bell
DATA 14,208, 0, 0,246,159, 0, 2,0,3, 62, 84: REM GP57:Crash Cymbal 2
DATA 128, 16, 0, 13,255,255, 3, 20,3,0, 60, 24: REM GP58:Vibraslap
DATA 14, 7, 8, 74,248,244, 66,228,0,3, 62, 77: REM GP59:Ride Cymbal 2
DATA 6, 2, 11, 0,245,245, 12, 8,0,0, 54, 60: REM GP60:High Bongo
DATA 1, 2, 0, 0,250,200,191,151,0,0, 55, 65: REM GP61:Low Bongo
DATA 1, 1, 81, 0,250,250,135,183,0,0, 54, 59: REM GP62:Mute High Conga
DATA 1, 2, 84, 0,250,248,141,184,0,0, 54, 51: REM GP63:Open High Conga
DATA 1, 2, 89, 0,250,248,136,182,0,0, 54, 45: REM GP64:Low Conga
DATA 1, 0, 0, 0,249,250, 10, 6,3,0, 62, 71: REM GP65:High Timbale
DATA 0, 0,128, 0,249,246,137,108,3,0, 62, 60: REM GP66:Low Timbale
DATA 3, 12,128, 8,248,246,136,182,3,0, 63, 58: REM GP67:High Agogo
DATA 3, 12,133, 0,248,246,136,182,3,0, 63, 53: REM GP68:Low Agogo
DATA 14, 0, 64, 8,118,119, 79, 24,0,2, 62, 64: REM GP69:Cabasa
DATA 14, 3, 64, 0,200,155, 73,105,0,2, 62, 71: REM GP70:Maracas
DATA 215,199,220, 0,173,141, 5, 5,3,0, 62, 61: REM GP71:Short Whistle
DATA 215,199,220, 0,168,136, 4, 4,3,0, 62, 61: REM GP72:Long Whistle
DATA 128, 17, 0, 0,246,103, 6, 23,3,3, 62, 44: REM GP73:Short Guiro
DATA 128, 17, 0, 9,245, 70, 5, 22,2,3, 62, 40: REM GP74:Long Guiro
DATA 6, 21, 63, 0, 0,247,244,245,0,0, 49, 69: REM GP75:Claves
DATA 6, 18, 63, 0, 0,247,244,245,3,0, 48, 68: REM GP76:High Wood Block
DATA 6, 18, 63, 0, 0,247,244,245,0,0, 49, 63: REM GP77:Low Wood Block
DATA 1, 2, 88, 0,103,117,231, 7,0,0, 48, 74: REM GP78:Mute Cuica
DATA 65, 66, 69, 8,248,117, 72, 5,0,0, 48, 60: REM GP79:Open Cuica
DATA 10, 30, 64, 78,224,255,240, 5,3,0, 56, 80: REM GP80:Mute Triangle
DATA 10, 30,124, 82,224,255,240, 2,3,0, 56, 64: REM GP81:Open Triangle
DATA 14, 0, 64, 8,122,123, 74, 27,0,2, 62, 72: REM GP82
DATA 14, 7, 10, 64,228, 85,228, 57,3,1, 54, 73: REM GP83
DATA 5, 4, 5, 64,249,214, 50,165,3,0, 62, 70: REM GP84
DATA 2, 21, 63, 0, 0,247,243,245,3,0, 56, 68: REM GP85
DATA 1, 2, 79, 0,250,248,141,181,0,0, 55, 48: REM GP86
DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 53: REM GP87

REM $STATIC
SUB ChangePanning (Channel, Value)
DIM Panning

';;;;;

Panning = &H0
IF Value < &H30 THEN
Panning = &H20
ELSEIF Value > &H4F THEN
Panning = &H10
END IF

ChannelSettings(Channel).Panning = Panning
END SUB

SUB ControllerChange (Channel, MIDI AS MIDIStr)
DIM Controller
DIM Value

Controller = ASC(INPUT$(&H1, 1))
Value = ASC(INPUT$(&H1, 1))

';;;

SELECT CASE Controller
CASE &H1
ChannelSettings(Channel).Vibrato = Value
CASE &H6
MIDI.BendSense = Value / &H2000
CASE &H7
ChannelSettings(Channel).Volume = Value
UpdateAllLiveNotes Channel, MOPPAN, 0
CASE &HA
ChangePanning Channel, Value
UpdateAllLiveNotes Channel, MOPPAN, 0
CASE &H79
ResetAllControllers Channel
UpdateAllLiveNotes Channel, MOPPAN, 0
CASE &H7B
UpdateAllLiveNotes Channel, MOPVOLUME, 0
END SELECT
END SUB

SUB DeallocateActiveNote (Channel, Note)
DIM ActiveNoteCount
DIM DeactivatedNoteChannel
DIM NoteToDeactivate

ActiveNoteCount = ActiveNoteCount(Channel)
NoteToDeactivate = ActList(Channel, Note)
ActRev(Channel, NoteToDeactivate) = 0
ActiveNoteCount(Channel) = ActiveNoteCount - 1
DeactivatedNoteChannel = ActAdlChn(Channel, NoteToDeactivate)
Channels(DeactivatedNoteChannel).IsOn = FALSE
Channels(DeactivatedNoteChannel).Age = 0
OPLNoteOff DeactivatedNoteChannel

Display 20 - DeactivatedNoteChannel, Channels(DeactivatedNoteChannel).x, 1, "."

IF NOT Note = ActiveNoteCount THEN
NoteToDeactivate = ActList(Channel, ActiveNoteCount)
ActList(Channel, Note) = NoteToDeactivate
ActRev(Channel, NoteToDeactivate) = Note
Channels(ActAdlChn(Channel, NoteToDeactivate)).ActiveIndex = Note
END IF
END SUB

SUB Display (Row, Column, ColorV, Text AS STRING)
COLOR ColorV
LOCATE Row, Column
PRINT Text;
END SUB

SUB GetInstrumentData ()
DIM Column
DIM Instrument

RESTORE Instruments
FOR Instrument = LBOUND(Adlib, 1) TO UBOUND(Adlib, 1)
FOR Column = LBOUND(Adlib, 2) TO UBOUND(Adlib, 2)
READ Adlib(Instrument, Column)
NEXT Column
NEXT Instrument
END SUB

FUNCTION GetOctave# (Hertz AS DOUBLE)
DIM Octave AS DOUBLE

'';;;

Octave = &H2000
DO WHILE Hertz >= 1023.5
Hertz = Hertz / 2
Octave = Octave + &H400
LOOP

GetOctave# = Octave + CINT(Hertz)
END FUNCTION

FUNCTION GetShortestDelay& (TrackCount AS LONG)
DIM ShortestDelay AS LONG
DIM Track

ShortestDelay = -1
FOR Track = 0 TO TrackCount - 1
IF Tracks(Track).Status >= 0 THEN
IF ShortestDelay = -1 OR Tracks(Track).Delay < ShortestDelay THEN
ShortestDelay = Tracks(Track).Delay
END IF
END IF
NEXT Track

GetShortestDelay& = ShortestDelay
END FUNCTION

SUB HandleSpecialTrackEvent (Byte, Track, MIDI AS MIDIStr, Status AS StatusStr)
DIM Ignored AS LONG
DIM StringV AS STRING

SELECT CASE Byte
CASE MESONGPOSITION
SEEK #1, SEEK(1) + &H2
CASE MESONGSELECT
SEEK #1, SEEK(1) + &H1
CASE MESYSEXSTART, MESYSEXEND
Ignored = ReadVariableLengthInteger&
CASE ELSE
HandleSpecialTrackMetaEvent Track, MIDI, Status
END SELECT
END SUB

SUB HandleSpecialTrackMetaEvent (Track, MIDI AS MIDIStr, Status AS StatusStr)
DIM EventType
DIM StringV AS STRING
STATIC TextRow

EventType = ASC(INPUT$(&H1, 1))
StringV = INPUT$(ReadVariableLengthInteger&, 1)

SELECT CASE EventType
CASE MMETMARKER
IF StringV = "loopStart" THEN
Status.LoopStart = TRUE
ELSEIF StringV = "loopEnd" THEN
Status.LoopEnd = TRUE
END IF
CASE MMEENDOFTRACK
Tracks(Track).Status = -1 ';;;
CASE MMESETTEMPO
MIDI.Tempo = ParseBEDWord&(CHR$(&H0) + StringV) * MIDI.InvDeltaTicks
END SELECT

SELECT CASE EventType
CASE MMETCOPYRIGHT, MMETGENERIC, MMETINSTRUMENT, MMETLYRIC, MMETMARKER, MMETTRACK
IF TextRow = 0 THEN TextRow = 2
TextRow = 3 + (TextRow - 2) MOD 20
Display TextRow, 1, 8, "Meta" + STR$(EventType) + ": " + RemoveSpecialCharacters$(StringV)
END SELECT
END SUB

SUB HandleTrackEvent (Track, MIDI AS MIDIStr, Status AS StatusStr)
DIM Byte
DIM Channel

SEEK #1, Tracks(Track).Pointer + &H1
Byte = ASC(INPUT$(&H1, 1))

IF Byte < MESYSEXSTART THEN
IF Byte < &H80 THEN '';;;;
Byte = Tracks(Track).Status OR &H80
SEEK #1, Tracks(Track).Pointer
SEEK #1, SEEK(1) + &H1
END IF

Channel = Byte AND &HF
Tracks(Track).Status = Byte
SELECT CASE Byte AND &HF0
CASE MENOTEOFF
NoteOff Channel
CASE MENOTEON
NoteOn Channel, Status
CASE MEPOLYPHONIC
NoteTouch Channel
CASE MECONTROLLER
ControllerChange Channel, MIDI
CASE MEINSTRUMENT
ChannelSettings(Channel).Patch = ASC(INPUT$(&H1, 1))
CASE MEPRESSURE
UpdateAllLiveNotes Channel, MOPPRESSURE, ASC(INPUT$(&H1, 1))
CASE MEBEND
ChannelSettings(Channel).Bend = ((ASC(INPUT$(&H1, 1)) + (ASC(INPUT$(&H1, 1)) * &H80)) - SIGN14BIT) * MIDI.BendSense
UpdateAllLiveNotes Channel, MOPPITCH, 0
END SELECT
ELSE
HandleSpecialTrackEvent Byte, Track, MIDI, Status
END IF
END SUB

SUB Initialize (Status AS StatusStr)
DIM Channel

SCREEN 0
WIDTH 80, 25
PALETTE
COLOR 7, 0
CLS

GetInstrumentData
OPLReset
OPLReset

FOR Channel = 0 TO 15
ChannelSettings(Channel).Volume = 127 ';;
NEXT Channel

Status.Began = FALSE
Status.LoopEnd = FALSE
Status.LoopStart = TRUE
Status.LoopWait = 0
Status.Paused = FALSE
Status.PlayWait = 0

PLAY ON
ON PLAY(EVENTBUFFERSIZE) GOSUB PlayerLoop
PLAY "ML MB"
END SUB

SUB Main (MIDI AS MIDIStr, Status AS StatusStr)
DIM FileName AS STRING
DIM KeyStroke AS STRING

FileName = COMMAND$

IF FileName = "" THEN
PRINT "No file specified."
ELSE
OpenMIDI FileName, MIDI
PLAY "T255 P64 P64"

Display 1, 1, 8, "Press Q to quit; Space to pause"

DO
KeyStroke = INKEY$

SELECT CASE KeyStroke
CASE " "
Status.Paused = NOT Status.Paused
IF Status.Paused THEN
PRINT "Pause"
PLAY STOP
ELSE
PRINT "Ok"
PLAY ON
END IF
CASE CHR$(3), CHR$(27), "q", "Q"
EXIT DO
END SELECT
LOOP
END IF
END SUB

SUB NoteOff (Channel)
DIM ActiveNote
DIM Note

Note = ASC(INPUT$(&H1, 1))
SEEK #1, SEEK(1) + &H1

ChannelSettings(Channel).Bend = 0
ActiveNote = ActRev(Channel, Note)
IF NOT ActiveNote = 0 THEN DeallocateActiveNote Channel, ActiveNote
END SUB

SUB NoteOn (Channel, Status AS StatusStr)
DIM ActiveNote
DIM AllocatedChannel
DIM bs AS DOUBLE '';;;What is this variable?
DIM CandidateChannel
DIM Instrument
DIM Note
DIM s AS DOUBLE '';;;What is this variable?
DIM Tone
DIM Volume

'';;;;

Note = ASC(INPUT$(&H1, 1))
Volume = ASC(INPUT$(&H1, 1))
IF Volume = 0 THEN
'Sometimes noteoffs are optimized as 0-vol noteons
ActiveNote = ActRev(Channel, Note)
IF NOT ActiveNote = 0 THEN DeallocateActiveNote Channel, ActiveNote
ELSE
IF ActRev(Channel, Note) = 0 THEN
Tone = Note
Instrument = ChannelSettings(Channel).Patch
IF Channel = 9 THEN
Instrument = (&H80 + Note) - &H23
Tone = Adlib(Instrument, 11) 'Translate percussion
END IF

'(MIDI channel 9 always plays percussion and ignores the patch number)
'Allocate AdLib channel (the physical sound channel for the note)
bs = -9
AllocatedChannel = -1
FOR CandidateChannel = 0 TO 17
s = Channels(CandidateChannel).Age
IF NOT Channels(CandidateChannel).IsOn THEN s = s + 3000# ' Empty channel = privileged
IF ChannelInstruments(CandidateChannel).Instrument = Instrument THEN s = s + .2# ' Same instrument = good
IF Instrument < &H80 AND ChannelInstruments(CandidateChannel).Instrument > &H7F THEN s = (s * 2) + 9 'Percussion is inferior to melody
IF s > bs THEN
bs = s
AllocatedChannel = CandidateChannel
'Best candidate wins
END IF
NEXT CandidateChannel

IF Channels(AllocatedChannel).IsOn THEN
DeallocateActiveNote Channels(AllocatedChannel).MIDIChannel, Channels(AllocatedChannel).ActiveIndex
END IF

Channels(AllocatedChannel).IsOn = TRUE
ChannelInstruments(AllocatedChannel).Instrument = Instrument
Channels(AllocatedChannel).Age = 0
Status.Began = TRUE

ActiveNote = ActiveNoteCount(Channel) + 1
ActList(Channel, ActiveNote) = Note
ActRev(Channel, Note) = ActiveNote
ActiveNoteCount(Channel) = ActiveNote

ActTone(Channel, Note) = Tone
ActAdlChn(Channel, Note) = AllocatedChannel
ActVol(Channel, Note) = Volume
Channels(AllocatedChannel).MIDIChannel = Channel
Channels(AllocatedChannel).ActiveIndex = ActiveNote
OPLPatch AllocatedChannel
UpdateNotePan AllocatedChannel, Channel
UpdateNoteVolume AllocatedChannel, Channel, Volume

Channels(AllocatedChannel).x = 1 + (Tone + &H3F) MOD &H50
Channels(AllocatedChannel).ColorV = 9 + (ChannelInstruments(AllocatedChannel).Instrument MOD &H6)

Display 20 - AllocatedChannel, Channels(AllocatedChannel).x, Channels(AllocatedChannel).ColorV, "#"

OPLNoteOn AllocatedChannel, 172.00093# * EXP(.057762265# * (Tone + ChannelSettings(Channel).Bend))
UpdateNoteVolume AllocatedChannel, Channel, Volume
END IF
END IF
END SUB

SUB NoteTouch (Channel)
DIM ActiveChannel
DIM Note
DIM Volume

Note = ASC(INPUT$(&H1, 1))
Volume = ASC(INPUT$(&H1, 1))
IF NOT ActRev(Channel, Note) = 0 THEN
ActiveChannel = ActAdlChn(Channel, Note)

Display 20 - ActiveChannel, Channels(ActiveChannel).x, Channels(ActiveChannel).ColorV, "&"

ActVol(Channel, Note) = Volume
UpdateNoteVolume ActiveChannel, Channel, Volume
END IF
END SUB

SUB OpenMIDI (FileName AS STRING, MIDI AS MIDIStr)
DIM Position AS LONG
DIM Track
DIM TrackLength AS LONG

OPEN FileName$ FOR INPUT LOCK READ AS #1
CLOSE #1

OPEN FileName$ FOR BINARY LOCK READ WRITE AS #1
IF INPUT$(&H4, 1) = "MThd" THEN
IF ParseBEDWord&(INPUT$(&H4, 1)) = MTHDBLOCKDEFAULTLENGTH THEN
MIDI.Format = ParseBEWord(INPUT$(&H2, 1))
MIDI.TrackCount = ParseBEWord(INPUT$(&H2, 1))
REDIM Loops(0 TO MIDI.TrackCount - 1) AS TrackStr
REDIM LoopsBackup(0 TO MIDI.TrackCount - 1) AS TrackStr
REDIM Tracks(0 TO MIDI.TrackCount - 1) AS TrackStr

'';;;
MIDI.InvDeltaTicks = EVENTTICKFREQUENCY / (240000000# * ParseBEWord(INPUT$(&H2, 1)))
MIDI.Tempo = 1000000 * MIDI.InvDeltaTicks
MIDI.BendSense = &H2 / &H2000
'';;;
FOR Track = 0 TO MIDI.TrackCount - 1
IF INPUT$(&H4, 1) = "MTrk" THEN
TrackLength = ParseBEDWord&(INPUT$(&H4, 1))
Position = LOC(1)
Tracks(Track).Delay = ReadVariableLengthInteger&
Tracks(Track).Pointer = LOC(1)
SEEK #1, (Position + TrackLength) + &H1
ELSE
ERROR 13
END IF
NEXT Track
ELSE
ERROR 6
END IF
ELSE
ERROR 13
END IF
END SUB

SUB OPLNoteOff (Channel)
DIM Operator
DIM Port
DIM q ''What is this variable?

Port = SetUpOPL(Channel, Operator, q)
OUT Port, &HB0 + q
OUT Port + &H1, ChannelInstruments(Channel).Pitch AND &HDF
END SUB

SUB OPLNoteOn (Channel, Hertz AS DOUBLE)
DIM Octave
DIM Port
DIM q ''What is this variable?

Octave = GetOctave#(Hertz)
Port = SetUpOPL(Channel, 0, q)

OUT Port, &HA0 + q
OUT Port + &H1, Octave AND &HFF

Octave = Octave \ &H100

OUT Port, &HB0 + q
OUT Port + &H1, Octave

ChannelInstruments(Channel).Pitch = Octave
END SUB

SUB OPLPatch (Channel)
DIM Instrument
DIM Operator
DIM Port
DIM q ''What is this variable?
DIM x ''What is this variable?

Port = SetUpOPL(Channel, Operator, q)
Instrument = ChannelInstruments(Channel).Instrument
q = Port + &H1
FOR x = &H0 TO &H1
OUT Port, &H20 + Operator + (x * &H3)
OUT q, Adlib(Instrument, &H0 + x)
OUT Port, &H60 + Operator + (x * &H3)
OUT q, Adlib(Instrument, &H4 + x)
OUT Port, &H80 + Operator + (x * &H3)
OUT q, Adlib(Instrument, &H6 + x)
OUT Port, &HE0 + Operator + (x * &H3)
OUT q, Adlib(Instrument, &H8 + x)
NEXT x
END SUB

SUB OPLReset ()
DIM Operator
DIM Port
DIM q ''What is this variable?
DIM y ''What is this variable?

Port = SetUpOPL(0, Operator, q)

FOR y = &H3 TO &H4
OUT Port, &H4
OUT Port + &H1, y * &H20
NEXT y

Port = SetUpOPL(9, Operator, q)
FOR y = &H0 TO &H2
OUT Port, &H5
OUT Port + &H1, y AND &H1
NEXT y

Port = SetUpOPL(0, Operator, q)
OUT Port, &H1
OUT Port + &H1, OPLENABLEWAVE

Port = SetUpOPL(0, Operator, q)
OUT Port, &HBD
OUT Port + &H1, OPLMEDOLICMODE

Port = SetUpOPL(9, Operator, q)
OUT Port, &H5
OUT Port + &H1, OPLENABLEOPL3

Port = SetUpOPL(9, Operator, q)
OUT Port, &H4
OUT Port + &H1, OPLMODE0

OPLSilence
END SUB

SUB OPLSilence ()
DIM Channel

FOR Channel = 0 TO 17
OPLNoteOff Channel
OPLTouchReal Channel, 0
NEXT Channel
END SUB

SUB OPLTouchReal (Channel, Volume)
DIM Instrument
DIM Operator
DIM Port
DIM q ''What is this variable?

Port = SetUpOPL(Channel, Operator, q)
Instrument = ChannelInstruments(Channel).Instrument

OUT Port, &H40 + Operator
q = Adlib(Instrument, 2)
OUT Port + &H1, (q OR &H3F) - Volume + ((q AND &H3F) * Volume) \ &H3F

OUT Port, &H43 + Operator
q = Adlib(Instrument, 3)
OUT Port + &H1, (q OR &H3F) - Volume + ((q AND &H3F) * Volume) \ &H3F
END SUB

FUNCTION ParseBEDWord& (Buffer AS STRING)
DIM DWord AS LONG

DWord = ASC(MID$(Buffer, &H4, &H1))
DWord = DWord OR (ASC(MID$(Buffer, &H3, &H1)) * &H100&)
DWord = DWord OR (ASC(MID$(Buffer, &H2, &H1)) * &H10000)
ParseBEDWord& = DWord OR (ASC(MID$(Buffer, &H1, &H1)) * &H1000000)
END FUNCTION

FUNCTION ParseBEWord (Buffer AS STRING)
DIM Word

Word = ASC(MID$(Buffer, &H2, &H1))
ParseBEWord = Word OR (ASC(MID$(Buffer, &H1, &H1)) * &H100)
END FUNCTION

SUB ProcessTrackEvents (MIDI AS MIDIStr, Status AS StatusStr)
DIM Track

FOR Track = 0 TO MIDI.TrackCount - 1
LoopsBackup(Track).Pointer = Tracks(Track).Pointer
LoopsBackup(Track).Delay = Tracks(Track).Delay
LoopsBackup(Track).Status = Tracks(Track).Status
IF Tracks(Track).Status >= 0 AND Tracks(Track).Delay <= 0 THEN
HandleTrackEvent Track, MIDI, Status
Tracks(Track).Delay = Tracks(Track).Delay + ReadVariableLengthInteger&
Tracks(Track).Pointer = LOC(1)
END IF
NEXT Track

IF Status.LoopStart THEN
SaveLoopStart MIDI, Status
IF ScheduleEventsAfterDelay#(GetShortestDelay&(MIDI.TrackCount), MIDI, Status) < 0 OR Status.LoopEnd THEN
ReturnToLoopStart MIDI, Status
END IF
END IF

DO WHILE ScheduleEventsAfterDelay#(GetShortestDelay&(MIDI.TrackCount), MIDI, Status) < 0 OR Status.LoopEnd
IF Status.LoopStart THEN
ReturnToLoopStart MIDI, Status
END IF
LOOP
END SUB

SUB Quit ()
PLAY OFF
PRINT "End!"
OPLSilence
CLOSE #1
COLOR 7, 0
END SUB

FUNCTION ReadVariableLengthInteger& ()
DIM Byte
DIM VariableLengthInteger AS LONG

VariableLengthInteger = &H0
DO
Byte = ASC(INPUT$(&H1, 1))
VariableLengthInteger = (VariableLengthInteger * &H80) OR (Byte AND &H7F)
LOOP UNTIL Byte < &H80

ReadVariableLengthInteger = VariableLengthInteger
END FUNCTION

FUNCTION RemoveSpecialCharacters$ (Text AS STRING)
DIM Position
DIM Result AS STRING

Result = Text
FOR Position = 1 TO LEN(Text)
SELECT CASE MID$(Result, Position, 1)
CASE IS < " ", IS > "~"
MID$(Result, Position, 1) = "?"
END SELECT
NEXT Position

RemoveSpecialCharacters$ = Result
END FUNCTION

SUB ResetAllControllers (Channel)
''Ctrl 121;;;
ChannelSettings(Channel).Bend = 0
ChannelSettings(Channel).Vibrato = 0
ChannelInstruments(Channel).Panning = 0
UpdateAllLiveNotes Channel, MOPOFF, 0
UpdateAllLiveNotes Channel, MOPPAN, 0
END SUB

SUB ReturnToLoopStart (MIDI AS MIDIStr, Status AS StatusStr)
DIM Track AS LONG

FOR Track = 0 TO MIDI.TrackCount - 1
Tracks(Track).Pointer = Loops(Track).Pointer
Tracks(Track).Delay = Loops(Track).Delay
Tracks(Track).Status = Loops(Track).Status
NEXT Track

Status.LoopEnd = FALSE
Status.PlayWait = Status.LoopWait
END SUB

SUB SaveLoopStart (MIDI AS MIDIStr, Status AS StatusStr)
DIM Track AS LONG

FOR Track = 0 TO MIDI.TrackCount - 1
Loops(Track).Pointer = LoopsBackup(Track).Pointer
Loops(Track).Delay = LoopsBackup(Track).Delay
Loops(Track).Status = LoopsBackup(Track).Status
NEXT Track

Status.LoopWait = Status.PlayWait
Status.LoopStart = FALSE
END SUB

FUNCTION ScheduleEventsAfterDelay# (Delay AS LONG, MIDI AS MIDIStr, Status AS StatusStr)
DIM Channel
DIM t AS DOUBLE '';;;;What is this variable?
DIM Track

FOR Track = 0 TO MIDI.TrackCount - 1
Tracks(Track).Delay = Tracks(Track).Delay - Delay
NEXT Track

t = Delay * MIDI.Tempo
IF Status.Began THEN
Status.PlayWait = Status.PlayWait + t
END IF

FOR Channel = 0 TO 17
Channels(Channel).Age = Channels(Channel).Age + t
NEXT Channel

ScheduleEventsAfterDelay# = t
END FUNCTION

FUNCTION SetUpOPL (Channel, Operator, q)
DIM Port

Port = &H0
'';;;;
IF Channel >= 0 AND Channel <= 17 THEN
Port = OPLBASE + (&H2 * (Channel \ 9))
q = Channel MOD 9
Operator = (q MOD 3) + 8 * (q \ 3)
END IF

SetUpOPL = Port
END FUNCTION

SUB UpdateAllLiveNotes (Channel, MIDIOperation, Volume)
DIM ActiveChannel
DIM ActiveCount
DIM ActiveNote
DIM Note

ActiveCount = ActiveNoteCount(Channel)

FOR ActiveNote = 1 TO ActiveCount
Note = ActList(Channel, ActiveNote)
ActiveChannel = ActAdlChn(Channel, Note)
SELECT CASE MIDIOperation
CASE MOPOFF
DeallocateActiveNote Channel, ActiveNote
CASE MOPPAN
UpdateNotePan ActiveChannel, Channel
CASE MOPPITCH
OPLNoteOn ActiveChannel, 172.00093# * EXP(.057762265# * (ActTone(Channel, Note) + ChannelSettings(Channel).Bend))
CASE MOPPRESSURE
ActVol(Channel, Note) = Volume
UpdateNoteVolume ActiveChannel, Channel, ActVol(Channel, Note)
CASE MOPVOLUME
UpdateNoteVolume ActiveChannel, Channel, ActVol(Channel, Note)
END SELECT
NEXT ActiveNote
END SUB

SUB UpdateNotePan (ActiveChannel, Channel)
DIM Operator
DIM Port
DIM q '';;;What is this variable?

ChannelInstruments(ActiveChannel).Panning = ChannelSettings(Channel).Panning
Port = SetUpOPL(ActiveChannel, Operator, q)
OUT Port, &HC0 + q '';;;
OUT Port + &H1, Adlib(ChannelInstruments(ActiveChannel).Instrument, 10) - ChannelInstruments(ActiveChannel).Panning
END SUB

SUB UpdateNoteVolume (ActiveChannel, Channel, Volume)
';;;;
IF Volume * ChannelSettings(Channel).Volume < 72 THEN
OPLTouchReal ActiveChannel, 0
ELSE
OPLTouchReal ActiveChannel, LOG(Volume * ChannelSettings(Channel).Volume) * 11.541561# - 48.818955#
END IF
END SUB

What is the challenge? Well, my program works except for the fact it tries to read beyond a MIDI file's end instead of looping back. This can be replicated in the original program by changing the following line so that it always executes RETURN instead of going to line 330 if the condition is met:

348 IF t# < 0 OR loopEnd THEN 330 ELSE RETURN 'Loop or done

Some parts of the original GWBasic program are really hard to properly untangle.

Do not read if you don't like attention seeking self-advertisements!

Did you read it anyway? Well, you can find all sorts of stuff I made using various programming languages over here:
https://github.com/peterswinkels

Reply 7 of 8, by Peter Swinkels

User metadata
Rank Oldbie
Rank
Oldbie

For anyone interested, I got my version of Bisqwit's MIDI player to loop:

'$DYNAMIC
DEFINT A-Z
OPTION BASE 0
OPTION EXPLICIT ''Disable for Q(uick)BASIC.

CONST EVENTBUFFERSIZE = 2
CONST EVENTTICKFREQUENCY = 16320
CONST FALSE = 0
CONST MEBEND = &HE0
CONST MECONTROLLER = &HB0
CONST MEINSTRUMENT = &HC0
CONST MEMETA = &HFF
CONST MENOTEOFF = &H80
CONST MENOTEON = &H90
CONST MEPOLYPHONIC = &HA0
CONST MEPRESSURE = &HD0
CONST MESONGPOSITION = &HF2
CONST MESONGSELECT = &HF3
CONST MESYSEXSTART = &HF0
CONST MESYSEXEND = &HF7
CONST MMEENDOFTRACK = &H2F
CONST MMESETTEMPO = &H51
CONST MMETCOPYRIGHT = &H2
CONST MMETCUE = &H7
CONST MMETGENERIC = &H1
CONST MMETINSTRUMENT = &H4
CONST MMETLYRIC = &H5
CONST MMETMARKER = &H6
CONST MMETTRACK = &H3
CONST MOPOFF = 1
CONST MOPPAN = 2
CONST MOPPITCH = 3
CONST MOPPRESSURE = 4
CONST MOPVOLUME = 5
CONST MTHDBLOCKDEFAULTLENGTH = &H6
CONST OPLBASE = &H388
CONST OPLENABLEOPL3 = &H1
CONST OPLENABLEWAVE = &H20
CONST OPLMEDOLICMODE = &H0
CONST OPLMODE0 = &H0
CONST SIGN14BIT = &H2000
CONST TRUE = -1

TYPE ChannelStr
ActiveIndex AS INTEGER
Age AS LONG
ColorV AS INTEGER
MIDIChannel AS INTEGER
IsOn AS INTEGER
x AS INTEGER
XCharacter AS INTEGER
XColor AS INTEGER
END TYPE

TYPE ChannelInstrumentStr
Instrument AS INTEGER
Panning AS INTEGER
Pitch AS INTEGER
END TYPE

Show last 1054 lines
TYPE ChannelSettingsStr
Bend AS LONG
Volume AS INTEGER
Panning AS INTEGER
Patch AS INTEGER
Vibrato AS INTEGER
END TYPE

TYPE MIDIStr
BendSense AS DOUBLE
Format AS INTEGER
InvDeltaTicks AS DOUBLE
Tempo AS DOUBLE
TrackCount AS LONG
END TYPE

TYPE StatusStr
Began AS INTEGER
LoopEnd AS INTEGER
LoopStart AS INTEGER
LoopWait AS LONG
Paused AS INTEGER
PlayWait AS LONG
TextRow AS INTEGER
END TYPE

TYPE TrackStr
Delay AS LONG
Pointer AS LONG
Status AS INTEGER
END TYPE

DECLARE FUNCTION AllocateChannel (Instrument)
DECLARE FUNCTION GetOctave# (Hertz AS DOUBLE)
DECLARE FUNCTION GetShortestDelay& (TrackCount AS LONG)
DECLARE FUNCTION ParseBEDWord& (Buffer AS STRING)
DECLARE FUNCTION ParseBEWord (Buffer AS STRING)
DECLARE FUNCTION ReadVariableLengthInteger& ()
DECLARE FUNCTION RemoveSpecialCharacters$ (Text AS STRING)
DECLARE FUNCTION ScheduleEventsAfterDelay# (Delay AS LONG, MIDI AS MIDIStr, Status AS StatusStr)
DECLARE FUNCTION SetUpOPL (Channel, Operator, ChannelInsidePair)
DECLARE SUB ChangePanning (Channel, Value)
DECLARE SUB ControllerChange (Channel, MIDI AS MIDIStr)
DECLARE SUB DeallocateActiveNote (Channel, Note)
DECLARE SUB Display (Row, Column, ColorV, Text AS STRING)
DECLARE SUB DisplayNote (AllocatedChannel, Tone)
DECLARE SUB DisplayKeys ()
DECLARE SUB GetInstrumentData ()
DECLARE SUB HandleSpecialTrackEvent (MIDISpecialEvent, Track, MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB HandleSpecialTrackMetaEvent (Track, MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB HandleTrackEvent (Track, MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB Initialize (Status AS StatusStr)
DECLARE SUB Main (MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB NoteOff (Channel)
DECLARE SUB NoteOn (Channel, Status AS StatusStr)
DECLARE SUB NoteTouch (Channel)
DECLARE SUB OpenMIDI (FileName AS STRING, MIDI AS MIDIStr)
DECLARE SUB OPLNoteOn (Channel, Hertz AS DOUBLE)
DECLARE SUB OPLNoteOff (Channel)
DECLARE SUB OPLPatch (Channel)
DECLARE SUB OPLReset ()
DECLARE SUB OPLSilence ()
DECLARE SUB OPLTouchReal (Channel, Volume)
DECLARE SUB ProcessTrackEvents (MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB ResetAllControllers (Channel)
DECLARE SUB Quit ()
DECLARE SUB ReturnToLoopStart (MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB SaveLoopStart (MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB UpdateAllLiveNotes (Channel, MIDIOperation, Volume)
DECLARE SUB UpdateNotePan (ActiveChannel, Channel)
DECLARE SUB UpdateNoteVolume (ActiveChannel, Channel, Volume)

DIM MIDI AS MIDIStr
DIM Status AS StatusStr

DIM SHARED ActiveNoteCount(0 TO 15)
DIM SHARED Adlib(0 TO 180, 0 TO 11)
DIM SHARED Channels(0 TO 17) AS ChannelStr
DIM SHARED ChannelInstruments(0 TO 17) AS ChannelInstrumentStr
DIM SHARED ChannelSettings(0 TO 15) AS ChannelSettingsStr
DIM SHARED Loops(0 TO 0) AS TrackStr
DIM SHARED LoopsBackup(0 TO 0) AS TrackStr
DIM SHARED Tracks(0 TO 0) AS TrackStr

DIM SHARED ActTone(0 TO 15, 0 TO 127) 'orignotenumber -> simulated notenumber
DIM SHARED ActAdlChn(0 TO 15, 0 TO 127) 'orignotenumber -> adlib channel
DIM SHARED ActVol(0 TO 15, 0 TO 127) 'orignotenumber -> pressure
DIM SHARED ActRev(0 TO 15, 0 TO 127) 'orignotenumber -> activeindex (index to ActList)
DIM SHARED ActList(0 TO 15, 0 TO 99) 'activeindex -> orignotenumber (index to ActVol etc)

Initialize Status
CALL Main(MIDI, Status)
Quit
END

PlayerLoop:
IF Status.Began THEN Status.PlayWait = Status.PlayWait - 1

DO WHILE Status.PlayWait = 0
ProcessTrackEvents MIDI, Status
LOOP

DO WHILE PLAY(0) < EVENTBUFFERSIZE
PLAY "T255 P64"
LOOP

RETURN

'The data bytes are:
' [0,1] AM/VIB/EG/KSR/Multiple bits for carrier and modulator respectively
' [2,3] KSL/Attenuation settings for carrier and modulator respectively
' [4,5] Attack and decay rates for carrier and modulator respectively
' [6,7] Sustain and release rates for carrier and modulator respectively
' [8,9] Wave select settings for carrier and modulator respectively
' [10] Feedback/connection bits for the channel (also stereo/pan bits)
' [11] For percussive instruments (GP35..GP87), the tone to play

Instruments:
DATA 1, 1,143, 6,242,242,244,247,0,0, 56, 0: REM GM1:AcouGrandPiano
DATA 1, 1, 75, 0,242,242,244,247,0,0, 56, 0: REM GM2:BrightAcouGrand
DATA 1, 1, 73, 0,242,242,244,246,0,0, 56, 0: REM GM3:ElecGrandPiano
DATA 129, 65, 18, 0,242,242,247,247,0,0, 54, 0: REM GM4:Honky-tonkPiano
DATA 1, 1, 87, 0,241,242,247,247,0,0, 48, 0: REM GM5:Rhodes Piano
DATA 1, 1,147, 0,241,242,247,247,0,0, 48, 0: REM GM6:Chorused Piano
DATA 1, 22,128, 14,161,242,242,245,0,0, 56, 0: REM GM7:Harpsichord
DATA 1, 1,146, 0,194,194,248,248,0,0, 58, 0: REM GM8:Clavinet
DATA 12,129, 92, 0,246,243,244,245,0,0, 48, 0: REM GM9:Celesta
DATA 7, 17,151,128,243,242,242,241,0,0, 50, 0: REM GM10:Glockenspiel
DATA 23, 1, 33, 0, 84,244,244,244,0,0, 50, 0: REM GM11:Music box
DATA 152,129, 98, 0,243,242,246,246,0,0, 48, 0: REM GM12:Vibraphone
DATA 24, 1, 35, 0,246,231,246,247,0,0, 48, 0: REM GM13:Marimba
DATA 21, 1,145, 0,246,246,246,246,0,0, 52, 0: REM GM14:Xylophone
DATA 69,129, 89,128,211,163,243,243,0,0, 60, 0: REM GM15:Tubular Bells
DATA 3,129, 73,128,117,181,245,245,1,0, 52, 0: REM GM16:Dulcimer
DATA 113, 49,146, 0,246,241, 20, 7,0,0, 50, 0: REM GM17:Hammond Organ
DATA 114, 48, 20, 0,199,199, 88, 8,0,0, 50, 0: REM GM18:Percussive Organ
DATA 112,177, 68, 0,170,138, 24, 8,0,0, 52, 0: REM GM19:Rock Organ
DATA 35,177,147, 0,151, 85, 35, 20,1,0, 52, 0: REM GM20:Church Organ
DATA 97,177, 19,128,151, 85, 4, 4,1,0, 48, 0: REM GM21:Reed Organ
DATA 36,177, 72, 0,152, 70, 42, 26,1,0, 60, 0: REM GM22:Accordion
DATA 97, 33, 19, 0,145, 97, 6, 7,1,0, 58, 0: REM GM23:Harmonica
DATA 33,161, 19,137,113, 97, 6, 7,0,0, 54, 0: REM GM24:Tango Accordion
DATA 2, 65,156,128,243,243,148,200,1,0, 60, 0: REM GM25:Acoustic Guitar1
DATA 3, 17, 84, 0,243,241,154,231,1,0, 60, 0: REM GM26:Acoustic Guitar2
DATA 35, 33, 95, 0,241,242, 58,248,0,0, 48, 0: REM GM27:Electric Guitar1
DATA 3, 33,135,128,246,243, 34,248,1,0, 54, 0: REM GM28:Electric Guitar2
DATA 3, 33, 71, 0,249,246, 84, 58,0,0, 48, 0: REM GM29:Electric Guitar3
DATA 35, 33, 74, 5,145,132, 65, 25,1,0, 56, 0: REM GM30:Overdrive Guitar
DATA 35, 33, 74, 0,149,148, 25, 25,1,0, 56, 0: REM GM31:Distorton Guitar
DATA 9,132,161,128, 32,209, 79,248,0,0, 56, 0: REM GM32:Guitar Harmonics
DATA 33,162, 30, 0,148,195, 6,166,0,0, 50, 0: REM GM33:Acoustic Bass
DATA 49, 49, 18, 0,241,241, 40, 24,0,0, 58, 0: REM GM34:Electric Bass 1
DATA 49, 49,141, 0,241,241,232,120,0,0, 58, 0: REM GM35:Electric Bass 2
DATA 49, 50, 91, 0, 81,113, 40, 72,0,0, 60, 0: REM GM36:Fretless Bass
DATA 1, 33,139, 64,161,242,154,223,0,0, 56, 0: REM GM37:Slap Bass 1
DATA 33, 33,139, 8,162,161, 22,223,0,0, 56, 0: REM GM38:Slap Bass 2
DATA 49, 49,139, 0,244,241,232,120,0,0, 58, 0: REM GM39:Synth Bass 1
DATA 49, 49, 18, 0,241,241, 40, 24,0,0, 58, 0: REM GM40:Synth Bass 2
DATA 49, 33, 21, 0,221, 86, 19, 38,1,0, 56, 0: REM GM41:Violin
DATA 49, 33, 22, 0,221,102, 19, 6,1,0, 56, 0: REM GM42:Viola
DATA 113, 49, 73, 0,209, 97, 28, 12,1,0, 56, 0: REM GM43:Cello
DATA 33, 35, 77,128,113,114, 18, 6,1,0, 50, 0: REM GM44:Contrabass
DATA 241,225, 64, 0,241,111, 33, 22,1,0, 50, 0: REM GM45:Tremulo Strings
DATA 2, 1, 26,128,245,133,117, 53,1,0, 48, 0: REM GM46:Pizzicato String
DATA 2, 1, 29,128,245,243,117,244,1,0, 48, 0: REM GM47:Orchestral Harp
DATA 16, 17, 65, 0,245,242, 5,195,1,0, 50, 0: REM GM48:Timpany
DATA 33,162,155, 1,177,114, 37, 8,1,0, 62, 0: REM GM49:String Ensemble1
DATA 161, 33,152, 0,127, 63, 3, 7,1,1, 48, 0: REM GM50:String Ensemble2
DATA 161, 97,147, 0,193, 79, 18, 5,0,0, 58, 0: REM GM51:Synth Strings 1
DATA 33, 97, 24, 0,193, 79, 34, 5,0,0, 60, 0: REM GM52:SynthStrings 2
DATA 49,114, 91,131,244,138, 21, 5,0,0, 48, 0: REM GM53:Choir Aahs
DATA 161, 97,144, 0,116,113, 57,103,0,0, 48, 0: REM GM54:Voice Oohs
DATA 113,114, 87, 0, 84,122, 5, 5,0,0, 60, 0: REM GM55:Synth Voice
DATA 144, 65, 0, 0, 84,165, 99, 69,0,0, 56, 0: REM GM56:Orchestra Hit
DATA 33, 33,146, 1,133,143, 23, 9,0,0, 60, 0: REM GM57:Trumpet
DATA 33, 33,148, 5,117,143, 23, 9,0,0, 60, 0: REM GM58:Trombone
DATA 33, 97,148, 0,118,130, 21, 55,0,0, 60, 0: REM GM59:Tuba
DATA 49, 33, 67, 0,158, 98, 23, 44,1,1, 50, 0: REM GM60:Muted Trumpet
DATA 33, 33,155, 0, 97,127,106, 10,0,0, 50, 0: REM GM61:French Horn
DATA 97, 34,138, 6,117,116, 31, 15,0,0, 56, 0: REM GM62:Brass Section
DATA 161, 33,134,131,114,113, 85, 24,1,0, 48, 0: REM GM63:Synth Brass 1
DATA 33, 33, 77, 0, 84,166, 60, 28,0,0, 56, 0: REM GM64:Synth Brass 2
DATA 49, 97,143, 0,147,114, 2, 11,1,0, 56, 0: REM GM65:Soprano Sax
DATA 49, 97,142, 0,147,114, 3, 9,1,0, 56, 0: REM GM66:Alto Sax
DATA 49, 97,145, 0,147,130, 3, 9,1,0, 58, 0: REM GM67:Tenor Sax
DATA 49, 97,142, 0,147,114, 15, 15,1,0, 58, 0: REM GM68:Baritone Sax
DATA 33, 33, 75, 0,170,143, 22, 10,1,0, 56, 0: REM GM69:Oboe
DATA 49, 33,144, 0,126,139, 23, 12,1,1, 54, 0: REM GM70:English Horn
DATA 49, 50,129, 0,117, 97, 25, 25,1,0, 48, 0: REM GM71:Bassoon
DATA 50, 33,144, 0,155,114, 33, 23,0,0, 52, 0: REM GM72:Clarinet
DATA 225,225, 31, 0,133,101, 95, 26,0,0, 48, 0: REM GM73:Piccolo
DATA 225,225, 70, 0,136,101, 95, 26,0,0, 48, 0: REM GM74:Flute
DATA 161, 33,156, 0,117,117, 31, 10,0,0, 50, 0: REM GM75:Recorder
DATA 49, 33,139, 0,132,101, 88, 26,0,0, 48, 0: REM GM76:Pan Flute
DATA 225,161, 76, 0,102,101, 86, 38,0,0, 48, 0: REM GM77:Bottle Blow
DATA 98,161,203, 0,118, 85, 70, 54,0,0, 48, 0: REM GM78:Shakuhachi
DATA 98,161,153, 0, 87, 86, 7, 7,0,0, 59, 0: REM GM79:Whistle
DATA 98,161,147, 0,119,118, 7, 7,0,0, 59, 0: REM GM80:Ocarina
DATA 34, 33, 89, 0,255,255, 3, 15,2,0, 48, 0: REM GM81:Lead 1 squareea
DATA 33, 33, 14, 0,255,255, 15, 15,1,1, 48, 0: REM GM82:Lead 2 sawtooth
DATA 34, 33, 70,128,134,100, 85, 24,0,0, 48, 0: REM GM83:Lead 3 calliope
DATA 33,161, 69, 0,102,150, 18, 10,0,0, 48, 0: REM GM84:Lead 4 chiff
DATA 33, 34,139, 0,146,145, 42, 42,1,0, 48, 0: REM GM85:Lead 5 charang
DATA 162, 97,158, 64,223,111, 5, 7,0,0, 50, 0: REM GM86:Lead 6 voice
DATA 32, 96, 26, 0,239,143, 1, 6,0,2, 48, 0: REM GM87:Lead 7 fifths
DATA 33, 33,143,128,241,244, 41, 9,0,0, 58, 0: REM GM88:Lead 8 brass
DATA 119,161,165, 0, 83,160,148, 5,0,0, 50, 0: REM GM89:Pad 1 new age
DATA 97,177, 31,128,168, 37, 17, 3,0,0, 58, 0: REM GM90:Pad 2 warm
DATA 97, 97, 23, 0,145, 85, 52, 22,0,0, 60, 0: REM GM91:Pad 3 polysynth
DATA 113,114, 93, 0, 84,106, 1, 3,0,0, 48, 0: REM GM92:Pad 4 choir
DATA 33,162,151, 0, 33, 66, 67, 53,0,0, 56, 0: REM GM93:Pad 5 bowedpad
DATA 161, 33, 28, 0,161, 49,119, 71,1,1, 48, 0: REM GM94:Pad 6 metallic
DATA 33, 97,137, 3, 17, 66, 51, 37,0,0, 58, 0: REM GM95:Pad 7 halo
DATA 161, 33, 21, 0, 17,207, 71, 7,1,0, 48, 0: REM GM96:Pad 8 sweep
DATA 58, 81,206, 0,248,134,246, 2,0,0, 50, 0: REM GM97:FX 1 rain
DATA 33, 33, 21, 0, 33, 65, 35, 19,1,0, 48, 0: REM GM98:FX 2 soundtrack
DATA 6, 1, 91, 0,116,165,149,114,0,0, 48, 0: REM GM99:FX 3 crystal
DATA 34, 97,146,131,177,242,129, 38,0,0, 60, 0: REM GM100:FX 4 atmosphere
DATA 65, 66, 77, 0,241,242, 81,245,1,0, 48, 0: REM GM101:FX 5 brightness
DATA 97,163,148,128, 17, 17, 81, 19,1,0, 54, 0: REM GM102:FX 6 goblins
DATA 97,161,140,128, 17, 29, 49, 3,0,0, 54, 0: REM GM103:FX 7 echoes
DATA 164, 97, 76, 0,243,129,115, 35,1,0, 52, 0: REM GM104:FX 8 sci-fi
DATA 2, 7,133, 3,210,242, 83,246,0,1, 48, 0: REM GM105:Sitar
DATA 17, 19, 12,128,163,162, 17,229,1,0, 48, 0: REM GM106:Banjo
DATA 17, 17, 6, 0,246,242, 65,230,1,2, 52, 0: REM GM107:Shamisen
DATA 147,145,145, 0,212,235, 50, 17,0,1, 56, 0: REM GM108:Koto
DATA 4, 1, 79, 0,250,194, 86, 5,0,0, 60, 0: REM GM109:Kalimba
DATA 33, 34, 73, 0,124,111, 32, 12,0,1, 54, 0: REM GM110:Bagpipe
DATA 49, 33,133, 0,221, 86, 51, 22,1,0, 58, 0: REM GM111:Fiddle
DATA 32, 33, 4,129,218,143, 5, 11,2,0, 54, 0: REM GM112:Shanai
DATA 5, 3,106,128,241,195,229,229,0,0, 54, 0: REM GM113:Tinkle Bell
DATA 7, 2, 21, 0,236,248, 38, 22,0,0, 58, 0: REM GM114:Agogo Bells
DATA 5, 1,157, 0,103,223, 53, 5,0,0, 56, 0: REM GM115:Steel Drums
DATA 24, 18,150, 0,250,248, 40,229,0,0, 58, 0: REM GM116:Woodblock
DATA 16, 0,134, 3,168,250, 7, 3,0,0, 54, 0: REM GM117:Taiko Drum
DATA 17, 16, 65, 3,248,243, 71, 3,2,0, 52, 0: REM GM118:Melodic Tom
DATA 1, 16,142, 0,241,243, 6, 2,2,0, 62, 0: REM GM119:Synth Drum
DATA 14,192, 0, 0, 31, 31, 0,255,0,3, 62, 0: REM GM120:Reverse Cymbal
DATA 6, 3,128,136,248, 86, 36,132,0,2, 62, 0: REM GM121:Guitar FretNoise
DATA 14,208, 0, 5,248, 52, 0, 4,0,3, 62, 0: REM GM122:Breath Noise
DATA 14,192, 0, 0,246, 31, 0, 2,0,3, 62, 0: REM GM123:Seashore
DATA 213,218,149, 64, 55, 86,163, 55,0,0, 48, 0: REM GM124:Bird Tweet
DATA 53, 20, 92, 8,178,244, 97, 21,2,0, 58, 0: REM GM125:Telephone
DATA 14,208, 0, 0,246, 79, 0,245,0,3, 62, 0: REM GM126:Helicopter
DATA 38,228, 0, 0,255, 18, 1, 22,0,1, 62, 0: REM GM127:Applause/Noise
DATA 0, 0, 0, 0,243,246,240,201,0,2, 62, 0: REM GM128:Gunshot
DATA 16, 17, 68, 0,248,243,119, 6,2,0, 56, 35: REM GP35:Ac Bass Drum
DATA 16, 17, 68, 0,248,243,119, 6,2,0, 56, 35: REM GP36:Bass Drum 1
DATA 2, 17, 7, 0,249,248,255,255,0,0, 56, 52: REM GP37:Side Stick
DATA 0, 0, 0, 0,252,250, 5, 23,2,0, 62, 48: REM GP38:Acoustic Snare
DATA 0, 1, 2, 0,255,255, 7, 8,0,0, 48, 58: REM GP39:Hand Clap
DATA 0, 0, 0, 0,252,250, 5, 23,2,0, 62, 60: REM GP40:Electric Snare
DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 47: REM GP41:Low Floor Tom
DATA 12, 18, 0, 0,246,251, 8, 71,0,2, 58, 43: REM GP42:Closed High Hat
DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 49: REM GP43:High Floor Tom
DATA 12, 18, 0, 5,246,123, 8, 71,0,2, 58, 43: REM GP44:Pedal High Hat
DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 51: REM GP45:Low Tom
DATA 12, 18, 0, 0,246,203, 2, 67,0,2, 58, 43: REM GP46:Open High Hat
DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 54: REM GP47:Low-Mid Tom
DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 57: REM GP48:High-Mid Tom
DATA 14,208, 0, 0,246,159, 0, 2,0,3, 62, 72: REM GP49:Crash Cymbal 1
DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 60: REM GP50:High Tom
DATA 14, 7, 8, 74,248,244, 66,228,0,3, 62, 76: REM GP51:Ride Cymbal 1
DATA 14,208, 0, 10,245,159, 48, 2,0,0, 62, 84: REM GP52:Chinese Cymbal
DATA 14, 7, 10, 93,228,245,228,229,3,1, 54, 36: REM GP53:Ride Bell
DATA 2, 5, 3, 10,180,151, 4,247,0,0, 62, 65: REM GP54:Tambourine
DATA 78,158, 0, 0,246,159, 0, 2,0,3, 62, 84: REM GP55:Splash Cymbal
DATA 17, 16, 69, 8,248,243, 55, 5,2,0, 56, 83: REM GP56:Cow Bell
DATA 14,208, 0, 0,246,159, 0, 2,0,3, 62, 84: REM GP57:Crash Cymbal 2
DATA 128, 16, 0, 13,255,255, 3, 20,3,0, 60, 24: REM GP58:Vibraslap
DATA 14, 7, 8, 74,248,244, 66,228,0,3, 62, 77: REM GP59:Ride Cymbal 2
DATA 6, 2, 11, 0,245,245, 12, 8,0,0, 54, 60: REM GP60:High Bongo
DATA 1, 2, 0, 0,250,200,191,151,0,0, 55, 65: REM GP61:Low Bongo
DATA 1, 1, 81, 0,250,250,135,183,0,0, 54, 59: REM GP62:Mute High Conga
DATA 1, 2, 84, 0,250,248,141,184,0,0, 54, 51: REM GP63:Open High Conga
DATA 1, 2, 89, 0,250,248,136,182,0,0, 54, 45: REM GP64:Low Conga
DATA 1, 0, 0, 0,249,250, 10, 6,3,0, 62, 71: REM GP65:High Timbale
DATA 0, 0,128, 0,249,246,137,108,3,0, 62, 60: REM GP66:Low Timbale
DATA 3, 12,128, 8,248,246,136,182,3,0, 63, 58: REM GP67:High Agogo
DATA 3, 12,133, 0,248,246,136,182,3,0, 63, 53: REM GP68:Low Agogo
DATA 14, 0, 64, 8,118,119, 79, 24,0,2, 62, 64: REM GP69:Cabasa
DATA 14, 3, 64, 0,200,155, 73,105,0,2, 62, 71: REM GP70:Maracas
DATA 215,199,220, 0,173,141, 5, 5,3,0, 62, 61: REM GP71:Short Whistle
DATA 215,199,220, 0,168,136, 4, 4,3,0, 62, 61: REM GP72:Long Whistle
DATA 128, 17, 0, 0,246,103, 6, 23,3,3, 62, 44: REM GP73:Short Guiro
DATA 128, 17, 0, 9,245, 70, 5, 22,2,3, 62, 40: REM GP74:Long Guiro
DATA 6, 21, 63, 0, 0,247,244,245,0,0, 49, 69: REM GP75:Claves
DATA 6, 18, 63, 0, 0,247,244,245,3,0, 48, 68: REM GP76:High Wood Block
DATA 6, 18, 63, 0, 0,247,244,245,0,0, 49, 63: REM GP77:Low Wood Block
DATA 1, 2, 88, 0,103,117,231, 7,0,0, 48, 74: REM GP78:Mute Cuica
DATA 65, 66, 69, 8,248,117, 72, 5,0,0, 48, 60: REM GP79:Open Cuica
DATA 10, 30, 64, 78,224,255,240, 5,3,0, 56, 80: REM GP80:Mute Triangle
DATA 10, 30,124, 82,224,255,240, 2,3,0, 56, 64: REM GP81:Open Triangle
DATA 14, 0, 64, 8,122,123, 74, 27,0,2, 62, 72: REM GP82
DATA 14, 7, 10, 64,228, 85,228, 57,3,1, 54, 73: REM GP83
DATA 5, 4, 5, 64,249,214, 50,165,3,0, 62, 70: REM GP84
DATA 2, 21, 63, 0, 0,247,243,245,3,0, 56, 68: REM GP85
DATA 1, 2, 79, 0,250,248,141,181,0,0, 55, 48: REM GP86
DATA 0, 0, 0, 0,246,246, 12, 6,0,0, 52, 53: REM GP87

REM $STATIC
FUNCTION AllocateChannel (Instrument)
DIM AllocatedChannel
DIM CandidateChannel
DIM bs AS DOUBLE ''???
DIM s AS DOUBLE ''

AllocatedChannel = -1
bs = -9
FOR CandidateChannel = 0 TO 17
s = Channels(CandidateChannel).Age

IF NOT Channels(CandidateChannel).IsOn THEN
s = s + 3000
END IF

IF ChannelInstruments(CandidateChannel).Instrument = Instrument THEN
s = s + .2
END IF

IF Instrument < &H80 AND ChannelInstruments(CandidateChannel).Instrument > &H7F THEN
s = (s * 2) + 9
END IF

IF s > bs THEN
bs = s
AllocatedChannel = CandidateChannel
END IF
NEXT CandidateChannel

AllocateChannel = AllocatedChannel
END FUNCTION

SUB ChangePanning (Channel, Value)
DIM Panning

Panning = &H0
IF Value < &H30 THEN
Panning = &H20
ELSEIF Value > &H4F THEN
Panning = &H10
END IF

ChannelSettings(Channel).Panning = Panning
END SUB

SUB ControllerChange (Channel, MIDI AS MIDIStr)
DIM Controller
DIM Value

Controller = ASC(INPUT$(&H1, 1))
Value = ASC(INPUT$(&H1, 1))

SELECT CASE Controller
CASE &H1
ChannelSettings(Channel).Vibrato = Value
CASE &H6
MIDI.BendSense = Value / &H2000
CASE &H7
ChannelSettings(Channel).Volume = Value
UpdateAllLiveNotes Channel, MOPPAN, 0
CASE &HA
ChangePanning Channel, Value
UpdateAllLiveNotes Channel, MOPPAN, 0
CASE &H79
ResetAllControllers Channel
UpdateAllLiveNotes Channel, MOPPAN, 0
CASE &H7B
UpdateAllLiveNotes Channel, MOPVOLUME, 0
END SELECT
END SUB

SUB DeallocateActiveNote (Channel, Note)
DIM ActiveNoteCount
DIM DeactivatedNoteChannel
DIM NoteToDeactivate

ActiveNoteCount = ActiveNoteCount(Channel)
NoteToDeactivate = ActList(Channel, Note)
ActRev(Channel, NoteToDeactivate) = 0
ActiveNoteCount(Channel) = ActiveNoteCount - 1
DeactivatedNoteChannel = ActAdlChn(Channel, NoteToDeactivate)
Channels(DeactivatedNoteChannel).IsOn = FALSE
Channels(DeactivatedNoteChannel).Age = 0
OPLNoteOff DeactivatedNoteChannel

IF Channels(DeactivatedNoteChannel).XCharacter = 32 THEN
Display 20 - DeactivatedNoteChannel, Channels(DeactivatedNoteChannel).x, 1, "."
ELSE
Display 20 - DeactivatedNoteChannel, Channels(DeactivatedNoteChannel).x, Channels(DeactivatedNoteChannel).XColor, CHR$(Channels(DeactivatedNoteChannel).XCharacter)
END IF

IF NOT Note = ActiveNoteCount THEN
NoteToDeactivate = ActList(Channel, ActiveNoteCount)
ActList(Channel, Note) = NoteToDeactivate
ActRev(Channel, NoteToDeactivate) = Note
Channels(ActAdlChn(Channel, NoteToDeactivate)).ActiveIndex = Note
END IF
END SUB

SUB Display (Row, Column, ColorV, Text AS STRING)
COLOR ColorV
LOCATE Row, Column
PRINT Text;
END SUB

SUB DisplayKeys ()
Display 1, 1, 8, "Press Q to quit; Space to pause"
END SUB

SUB DisplayNote (AllocatedChannel, Tone)
DIM x
DIM y

Channels(AllocatedChannel).x = 1 + (Tone + &H3F) MOD &H50
Channels(AllocatedChannel).ColorV = 9 + (ChannelInstruments(AllocatedChannel).Instrument MOD &H6)
y = 20 - AllocatedChannel
x = Channels(AllocatedChannel).x
Channels(AllocatedChannel).XCharacter = SCREEN(y, x, FALSE)
Channels(AllocatedChannel).XColor = SCREEN(y, x, TRUE)
Display y, x, Channels(AllocatedChannel).ColorV, "#"
END SUB

SUB GetInstrumentData ()
DIM Column
DIM Instrument

RESTORE Instruments
FOR Instrument = LBOUND(Adlib, 1) TO UBOUND(Adlib, 1)
FOR Column = LBOUND(Adlib, 2) TO UBOUND(Adlib, 2)
READ Adlib(Instrument, Column)
NEXT Column
NEXT Instrument
END SUB

FUNCTION GetOctave# (Hertz AS DOUBLE)
DIM Octave AS DOUBLE

Octave = &H2000
DO WHILE Hertz >= 1023.5
Hertz = Hertz / 2
Octave = Octave + &H400
LOOP

GetOctave# = Octave + CINT(Hertz)
END FUNCTION

FUNCTION GetShortestDelay& (TrackCount AS LONG)
DIM ShortestDelay AS LONG
DIM Track

ShortestDelay = -1
FOR Track = 0 TO TrackCount - 1
IF Tracks(Track).Status >= 0 THEN
IF ShortestDelay = -1 OR Tracks(Track).Delay < ShortestDelay THEN
ShortestDelay = Tracks(Track).Delay
END IF
END IF
NEXT Track

GetShortestDelay& = ShortestDelay
END FUNCTION

SUB HandleSpecialTrackEvent (MIDISpecialEvent, Track, MIDI AS MIDIStr, Status AS StatusStr)
DIM Ignored AS LONG
DIM StringV AS STRING

SELECT CASE MIDISpecialEvent
CASE MESONGPOSITION
SEEK #1, SEEK(1) + &H2
CASE MESONGSELECT
SEEK #1, SEEK(1) + &H1
CASE MESYSEXSTART, MESYSEXEND
Ignored = ReadVariableLengthInteger&
CASE ELSE
HandleSpecialTrackMetaEvent Track, MIDI, Status
END SELECT
END SUB

SUB HandleSpecialTrackMetaEvent (Track, MIDI AS MIDIStr, Status AS StatusStr)
DIM EventType
DIM StringV AS STRING

EventType = ASC(INPUT$(&H1, 1))
StringV = INPUT$(ReadVariableLengthInteger&, 1)

SELECT CASE EventType
CASE MMETMARKER
IF StringV = "loopStart" THEN
Status.LoopStart = TRUE
ELSEIF StringV = "loopEnd" THEN
Status.LoopEnd = TRUE
END IF
CASE MMEENDOFTRACK
Tracks(Track).Status = -1 ';;;
Status.LoopEnd = TRUE
CASE MMESETTEMPO
MIDI.Tempo = ParseBEDWord&(CHR$(&H0) + StringV) * MIDI.InvDeltaTicks
CASE MMETCOPYRIGHT, MMETGENERIC, MMETINSTRUMENT, MMETLYRIC, MMETMARKER, MMETTRACK
Status.TextRow = 3 + (Status.TextRow - 2) MOD 20
Display Status.TextRow, 1, 8, "Meta" + STR$(EventType) + ": " + RemoveSpecialCharacters$(StringV)
END SELECT
END SUB

SUB HandleTrackEvent (Track, MIDI AS MIDIStr, Status AS StatusStr)
DIM Byte
DIM Channel

SEEK #1, Tracks(Track).Pointer + &H1
Byte = ASC(INPUT$(&H1, 1))

IF Byte < MESYSEXSTART THEN
IF Byte < &H80 THEN
Byte = Tracks(Track).Status OR &H80
SEEK #1, Tracks(Track).Pointer
SEEK #1, SEEK(1) + &H1
END IF

Channel = Byte AND &HF
Tracks(Track).Status = Byte
SELECT CASE Byte AND &HF0
CASE MENOTEOFF
NoteOff Channel
CASE MENOTEON
NoteOn Channel, Status
CASE MEPOLYPHONIC
NoteTouch Channel
CASE MECONTROLLER
ControllerChange Channel, MIDI
CASE MEINSTRUMENT
ChannelSettings(Channel).Patch = ASC(INPUT$(&H1, 1))
CASE MEPRESSURE
UpdateAllLiveNotes Channel, MOPPRESSURE, ASC(INPUT$(&H1, 1))
CASE MEBEND
ChannelSettings(Channel).Bend = ((ASC(INPUT$(&H1, 1)) + (ASC(INPUT$(&H1, 1)) * &H80)) - SIGN14BIT) * MIDI.BendSense
UpdateAllLiveNotes Channel, MOPPITCH, 0
END SELECT
ELSE
HandleSpecialTrackEvent Byte, Track, MIDI, Status
END IF
END SUB

SUB Initialize (Status AS StatusStr)
DIM Channel

SCREEN 0
WIDTH 80, 25
PALETTE
COLOR 7, 0
CLS

GetInstrumentData
OPLReset
OPLReset

FOR Channel = 0 TO 15
ChannelSettings(Channel).Volume = 127 ';;
NEXT Channel

Status.Began = FALSE
Status.LoopEnd = FALSE
Status.LoopStart = TRUE
Status.LoopWait = 0
Status.Paused = FALSE
Status.PlayWait = 0
Status.TextRow = 2

PLAY ON
ON PLAY(EVENTBUFFERSIZE) GOSUB PlayerLoop
PLAY "ML MB"
END SUB

SUB Main (MIDI AS MIDIStr, Status AS StatusStr)
DIM FileName AS STRING
DIM KeyStroke AS STRING

FileName = COMMAND$

IF FileName = "" THEN
PRINT "No file specified."
ELSE
OpenMIDI FileName, MIDI
PLAY "T255 P64 P64"

DisplayKeys

DO
KeyStroke = INKEY$

SELECT CASE KeyStroke
CASE " "
Status.Paused = NOT Status.Paused
IF Status.Paused THEN
PRINT "Pause"
PLAY STOP
ELSE
PRINT "Ok"
PLAY ON
END IF
CASE "q", "Q"
EXIT DO
END SELECT
LOOP
END IF
END SUB

SUB NoteOff (Channel)
DIM ActiveNote
DIM Note

Note = ASC(INPUT$(&H1, 1))
SEEK #1, SEEK(1) + &H1

ChannelSettings(Channel).Bend = 0
ActiveNote = ActRev(Channel, Note)
IF NOT ActiveNote = 0 THEN DeallocateActiveNote Channel, ActiveNote
END SUB

SUB NoteOn (Channel, Status AS StatusStr)
DIM ActiveNote
DIM AllocatedChannel
DIM Instrument
DIM Note
DIM Tone
DIM Volume

Note = ASC(INPUT$(&H1, 1))
Volume = ASC(INPUT$(&H1, 1))

IF Volume = 0 THEN
ActiveNote = ActRev(Channel, Note)
IF NOT ActiveNote = 0 THEN
DeallocateActiveNote Channel, ActiveNote
END IF
ELSE
IF ActRev(Channel, Note) = 0 THEN
Tone = Note
Instrument = ChannelSettings(Channel).Patch
IF Channel = 9 THEN
Instrument = (&H80 + Note) - &H23
Tone = Adlib(Instrument, 11)
END IF

AllocatedChannel = AllocateChannel(Instrument)

IF Channels(AllocatedChannel).IsOn THEN
DeallocateActiveNote Channels(AllocatedChannel).MIDIChannel, Channels(AllocatedChannel).ActiveIndex
END IF

Channels(AllocatedChannel).IsOn = TRUE
ChannelInstruments(AllocatedChannel).Instrument = Instrument
Channels(AllocatedChannel).Age = 0
Status.Began = TRUE

ActiveNote = ActiveNoteCount(Channel) + 1
ActList(Channel, ActiveNote) = Note
ActRev(Channel, Note) = ActiveNote
ActiveNoteCount(Channel) = ActiveNote

ActTone(Channel, Note) = Tone
ActAdlChn(Channel, Note) = AllocatedChannel
ActVol(Channel, Note) = Volume
Channels(AllocatedChannel).MIDIChannel = Channel
Channels(AllocatedChannel).ActiveIndex = ActiveNote
OPLPatch AllocatedChannel
UpdateNotePan AllocatedChannel, Channel
UpdateNoteVolume AllocatedChannel, Channel, Volume

DisplayNote AllocatedChannel, Tone

OPLNoteOn AllocatedChannel, 172.00093# * EXP(.057762265# * (Tone + ChannelSettings(Channel).Bend))
UpdateNoteVolume AllocatedChannel, Channel, Volume
END IF
END IF
END SUB

SUB NoteTouch (Channel)
DIM ActiveChannel
DIM Note
DIM Volume

Note = ASC(INPUT$(&H1, 1))
Volume = ASC(INPUT$(&H1, 1))
IF NOT ActRev(Channel, Note) = 0 THEN
ActiveChannel = ActAdlChn(Channel, Note)
Display 20 - ActiveChannel, Channels(ActiveChannel).x, Channels(ActiveChannel).ColorV, "&"
ActVol(Channel, Note) = Volume
UpdateNoteVolume ActiveChannel, Channel, Volume
END IF
END SUB

SUB OpenMIDI (FileName AS STRING, MIDI AS MIDIStr)
DIM Position AS LONG
DIM Track
DIM TrackLength AS LONG

OPEN FileName$ FOR INPUT LOCK READ AS #1
CLOSE #1

OPEN FileName$ FOR BINARY LOCK READ WRITE AS #1
IF INPUT$(&H4, 1) = "MThd" THEN
IF ParseBEDWord&(INPUT$(&H4, 1)) = MTHDBLOCKDEFAULTLENGTH THEN
MIDI.Format = ParseBEWord(INPUT$(&H2, 1))
MIDI.TrackCount = ParseBEWord(INPUT$(&H2, 1))

REDIM Loops(0 TO MIDI.TrackCount - 1) AS TrackStr
REDIM LoopsBackup(0 TO MIDI.TrackCount - 1) AS TrackStr
REDIM Tracks(0 TO MIDI.TrackCount - 1) AS TrackStr

MIDI.InvDeltaTicks = EVENTTICKFREQUENCY / (240000000# * ParseBEWord(INPUT$(&H2, 1)))
MIDI.Tempo = 1000000 * MIDI.InvDeltaTicks
MIDI.BendSense = &H2 / &H2000

FOR Track = 0 TO MIDI.TrackCount - 1
IF INPUT$(&H4, 1) = "MTrk" THEN
TrackLength = ParseBEDWord&(INPUT$(&H4, 1))
Position = LOC(1)
Tracks(Track).Delay = ReadVariableLengthInteger&
Tracks(Track).Pointer = LOC(1)
SEEK #1, (Position + TrackLength) + &H1
ELSE
ERROR 13
END IF
NEXT Track
ELSE
ERROR 6
END IF
ELSE
ERROR 13
END IF
END SUB

SUB OPLNoteOff (Channel)
DIM ChannelInsidePair
DIM Operator
DIM Port

Port = SetUpOPL(Channel, Operator, ChannelInsidePair)
OUT Port, &HB0 + ChannelInsidePair
OUT Port + &H1, ChannelInstruments(Channel).Pitch AND &HDF
END SUB

SUB OPLNoteOn (Channel, Hertz AS DOUBLE)
DIM ChannelInsidePair
DIM Octave
DIM Port

Octave = GetOctave#(Hertz)
Port = SetUpOPL(Channel, 0, ChannelInsidePair)

OUT Port, &HA0 + ChannelInsidePair
OUT Port + &H1, Octave AND &HFF

Octave = Octave \ &H100

OUT Port, &HB0 + ChannelInsidePair
OUT Port + &H1, Octave

ChannelInstruments(Channel).Pitch = Octave
END SUB

SUB OPLPatch (Channel)
DIM ChannelInsidePair
DIM Instrument
DIM Operator
DIM Port
DIM x ''What is this variable?

Port = SetUpOPL(Channel, Operator, ChannelInsidePair)
Instrument = ChannelInstruments(Channel).Instrument
ChannelInsidePair = Port + &H1
FOR x = &H0 TO &H1
OUT Port, &H20 + Operator + (x * &H3)
OUT ChannelInsidePair, Adlib(Instrument, &H0 + x)
OUT Port, &H60 + Operator + (x * &H3)
OUT ChannelInsidePair, Adlib(Instrument, &H4 + x)
OUT Port, &H80 + Operator + (x * &H3)
OUT ChannelInsidePair, Adlib(Instrument, &H6 + x)
OUT Port, &HE0 + Operator + (x * &H3)
OUT ChannelInsidePair, Adlib(Instrument, &H8 + x)
NEXT x
END SUB

SUB OPLReset ()
DIM ChannelInsidePair
DIM Operator
DIM Port
DIM y ''What is this variable?

Port = SetUpOPL(0, Operator, ChannelInsidePair)

FOR y = &H3 TO &H4
OUT Port, &H4
OUT Port + &H1, y * &H20
NEXT y

Port = SetUpOPL(9, Operator, ChannelInsidePair)
FOR y = &H0 TO &H2
OUT Port, &H5
OUT Port + &H1, y AND &H1
NEXT y

Port = SetUpOPL(0, Operator, ChannelInsidePair)
OUT Port, &H1
OUT Port + &H1, OPLENABLEWAVE

Port = SetUpOPL(0, Operator, ChannelInsidePair)
OUT Port, &HBD
OUT Port + &H1, OPLMEDOLICMODE

Port = SetUpOPL(9, Operator, ChannelInsidePair)
OUT Port, &H5
OUT Port + &H1, OPLENABLEOPL3

Port = SetUpOPL(9, Operator, ChannelInsidePair)
OUT Port, &H4
OUT Port + &H1, OPLMODE0

OPLSilence
END SUB

SUB OPLSilence ()
DIM Channel

FOR Channel = 0 TO 17
OPLNoteOff Channel
OPLTouchReal Channel, 0
NEXT Channel
END SUB

SUB OPLTouchReal (Channel, Volume)
DIM ChannelInsidePair
DIM Instrument
DIM Operator
DIM Port

Port = SetUpOPL(Channel, Operator, ChannelInsidePair)
Instrument = ChannelInstruments(Channel).Instrument

OUT Port, &H40 + Operator
ChannelInsidePair = Adlib(Instrument, 2)
OUT Port + &H1, (ChannelInsidePair OR &H3F) - Volume + ((ChannelInsidePair AND &H3F) * Volume) \ &H3F

OUT Port, &H43 + Operator
ChannelInsidePair = Adlib(Instrument, 3)
OUT Port + &H1, (ChannelInsidePair OR &H3F) - Volume + ((ChannelInsidePair AND &H3F) * Volume) \ &H3F
END SUB

FUNCTION ParseBEDWord& (Buffer AS STRING)
DIM DWord AS LONG

DWord = ASC(MID$(Buffer, &H4, &H1))
DWord = DWord OR (ASC(MID$(Buffer, &H3, &H1)) * &H100&)
DWord = DWord OR (ASC(MID$(Buffer, &H2, &H1)) * &H10000)
ParseBEDWord& = DWord OR (ASC(MID$(Buffer, &H1, &H1)) * &H1000000)
END FUNCTION

FUNCTION ParseBEWord (Buffer AS STRING)
DIM Word

Word = ASC(MID$(Buffer, &H2, &H1))
ParseBEWord = Word OR (ASC(MID$(Buffer, &H1, &H1)) * &H100)
END FUNCTION

SUB ProcessTrackEvents (MIDI AS MIDIStr, Status AS StatusStr)
DIM Track

FOR Track = 0 TO MIDI.TrackCount - 1
LoopsBackup(Track).Pointer = Tracks(Track).Pointer
LoopsBackup(Track).Delay = Tracks(Track).Delay
LoopsBackup(Track).Status = Tracks(Track).Status
IF Tracks(Track).Status >= 0 AND Tracks(Track).Delay <= 0 THEN
HandleTrackEvent Track, MIDI, Status
IF Status.LoopEnd THEN

ReturnToLoopStart MIDI, Status

COLOR , 0
CLS
Status.TextRow = 2
DisplayKeys
ELSE
Tracks(Track).Delay = Tracks(Track).Delay + ReadVariableLengthInteger&
Tracks(Track).Pointer = LOC(1)
END IF
END IF
NEXT Track

IF Status.LoopStart THEN
SaveLoopStart MIDI, Status
END IF

DO WHILE ScheduleEventsAfterDelay#(GetShortestDelay&(MIDI.TrackCount), MIDI, Status) < 0
LOOP
END SUB

SUB Quit ()
PLAY OFF
COLOR 9
PRINT "End!"
OPLSilence
CLOSE #1
COLOR 7, 0
END SUB

FUNCTION ReadVariableLengthInteger& ()
DIM Byte
DIM VariableLengthInteger AS LONG

VariableLengthInteger = &H0
DO
Byte = ASC(INPUT$(&H1, 1))
VariableLengthInteger = (VariableLengthInteger * &H80) OR (Byte AND &H7F)
LOOP UNTIL Byte < &H80

ReadVariableLengthInteger = VariableLengthInteger
END FUNCTION

FUNCTION RemoveSpecialCharacters$ (Text AS STRING)
DIM Position
DIM Result AS STRING

Result = Text
FOR Position = 1 TO LEN(Text)
SELECT CASE MID$(Result, Position, 1)
CASE IS < " ", IS > "~"
MID$(Result, Position, 1) = "?"
END SELECT
NEXT Position

RemoveSpecialCharacters$ = Result
END FUNCTION

SUB ResetAllControllers (Channel)
ChannelSettings(Channel).Bend = 0
ChannelSettings(Channel).Vibrato = 0
ChannelInstruments(Channel).Panning = 0
UpdateAllLiveNotes Channel, MOPOFF, 0
UpdateAllLiveNotes Channel, MOPPAN, 0
END SUB

SUB ReturnToLoopStart (MIDI AS MIDIStr, Status AS StatusStr)
DIM Track AS LONG

FOR Track = 0 TO MIDI.TrackCount - 1
Tracks(Track).Pointer = Loops(Track).Pointer
Tracks(Track).Delay = Loops(Track).Delay
Tracks(Track).Status = Loops(Track).Status
NEXT Track

Status.LoopEnd = FALSE
Status.PlayWait = Status.LoopWait
END SUB

SUB SaveLoopStart (MIDI AS MIDIStr, Status AS StatusStr)
DIM Track AS LONG

FOR Track = 0 TO MIDI.TrackCount - 1
Loops(Track).Pointer = LoopsBackup(Track).Pointer
Loops(Track).Delay = LoopsBackup(Track).Delay
Loops(Track).Status = LoopsBackup(Track).Status
NEXT Track

Status.LoopWait = Status.PlayWait
Status.LoopStart = FALSE
END SUB

FUNCTION ScheduleEventsAfterDelay# (Delay AS LONG, MIDI AS MIDIStr, Status AS StatusStr)
DIM AgeIncrease AS DOUBLE
DIM Channel
DIM Track

AgeIncrease = Delay * MIDI.Tempo

FOR Channel = 0 TO 17
Channels(Channel).Age = Channels(Channel).Age + AgeIncrease
NEXT Channel

FOR Track = 0 TO MIDI.TrackCount - 1
Tracks(Track).Delay = Tracks(Track).Delay - Delay
NEXT Track

IF Status.Began THEN
Status.PlayWait = Status.PlayWait + AgeIncrease
END IF

ScheduleEventsAfterDelay# = AgeIncrease
END FUNCTION

FUNCTION SetUpOPL (Channel, Operator, ChannelInsidePair)
DIM Port

Port = &H0

SELECT CASE Channel
CASE 0 TO 8
Port = OPLBASE
ChannelInsidePair = Channel
CASE 9 TO 17
Port = OPLBASE + &H2
ChannelInsidePair = Channel - 9
END SELECT

Operator = (ChannelInsidePair MOD &H3) OR (&H8 * (ChannelInsidePair \ &H3))

SetUpOPL = Port
END FUNCTION

SUB UpdateAllLiveNotes (Channel, MIDIOperation, Volume)
DIM ActiveChannel
DIM ActiveCount
DIM ActiveNote
DIM Note

ActiveCount = ActiveNoteCount(Channel)

FOR ActiveNote = 1 TO ActiveCount
Note = ActList(Channel, ActiveNote)
ActiveChannel = ActAdlChn(Channel, Note)
SELECT CASE MIDIOperation
CASE MOPOFF
DeallocateActiveNote Channel, ActiveNote
CASE MOPPAN
UpdateNotePan ActiveChannel, Channel
CASE MOPPITCH
OPLNoteOn ActiveChannel, 172.00093# * EXP(.057762265# * (ActTone(Channel, Note) + ChannelSettings(Channel).Bend))
CASE MOPPRESSURE
ActVol(Channel, Note) = Volume
UpdateNoteVolume ActiveChannel, Channel, ActVol(Channel, Note)
CASE MOPVOLUME
UpdateNoteVolume ActiveChannel, Channel, ActVol(Channel, Note)
END SELECT
NEXT ActiveNote
END SUB

SUB UpdateNotePan (ActiveChannel, Channel)
DIM ChannelInsidePair
DIM Operator
DIM Port

ChannelInstruments(ActiveChannel).Panning = ChannelSettings(Channel).Panning
Port = SetUpOPL(ActiveChannel, Operator, ChannelInsidePair)
OUT Port, &HC0 + ChannelInsidePair
OUT Port + &H1, Adlib(ChannelInstruments(ActiveChannel).Instrument, 10) - ChannelInstruments(ActiveChannel).Panning
END SUB

SUB UpdateNoteVolume (ActiveChannel, Channel, Volume)
IF Volume * ChannelSettings(Channel).Volume < 72 THEN
OPLTouchReal ActiveChannel, 0
ELSE
OPLTouchReal ActiveChannel, LOG(Volume * ChannelSettings(Channel).Volume) * 11.541561# - 48.818955#
END IF
END SUB

Do not read if you don't like attention seeking self-advertisements!

Did you read it anyway? Well, you can find all sorts of stuff I made using various programming languages over here:
https://github.com/peterswinkels

Reply 8 of 8, by Peter Swinkels

User metadata
Rank Oldbie
Rank
Oldbie
'$DYNAMIC
DEFINT A-Z
OPTION BASE 0

CONST DEFAULTTEMPO = 1000000
CONST EVENTBUFFERSIZE = 2
CONST EVENTTICKFREQUENCY = 16320
CONST FALSE = 0
CONST GP01 = 93
CONST MAXIMUMSIGNEDBEND = &H2000
CONST MCALLNOTESOFF = &H7B
CONST MCBEND = &H6
CONST MCMODULATION = &H1
CONST MCPAN = &HA
CONST MCRESETALLCONTROLLERS = &H79
CONST MCVOLUME = &H7
CONST MEBEND = &HE0
CONST MECHANNELMASK = &HF
CONST MECONTROLLER = &HB0
CONST MEFIRST = &H80
CONST MEINSTRUMENT = &HC0
CONST MEMASK = &HF0
CONST MEMETA = &HFF
CONST MENOTEOFF = &H80
CONST MENOTEON = &H90
CONST MEPOLYPHONIC = &HA0
CONST MEPRESSURE = &HD0
CONST MESONGPOSITION = &HF2
CONST MESONGSELECT = &HF3
CONST MESYSEXEND = &HF7
CONST MESYSEXSTART = &HF0
CONST MMEENDOFTRACK = &H2F
CONST MMESETTEMPO = &H51
CONST MMETCOPYRIGHT = &H2
CONST MMETCUE = &H7
CONST MMETGENERIC = &H1
CONST MMETINSTRUMENT = &H4
CONST MMETLYRIC = &H5
CONST MMETMARKER = &H6
CONST MMETTRACK = &H3
CONST MOPOFF = 1
CONST MOPPAN = 2
CONST MOPPITCH = 3
CONST MOPPRESSURE = 4
CONST MOPVOLUME = 5
CONST MTHDBLOCKDEFAULTLENGTH = &H6
CONST NOCHANNEL = -1
CONST NULL = 0
CONST OPL01WAVEON = &H20
CONST OPL04IRQRESET = &H80
CONST OPL04MODE0 = &H0
CONST OPL04TIMERS12ON = &H60
CONST OPL05OPL3OFF = &H0
CONST OPL05OPL3ON = &H1
CONST OPLATTENUATIONMASK = &H3F
CONST OPLBASE = &H388
CONST OPLB0FREQUENCYMASK = &H3
CONST OPLB0BLOCKMASK = &H1C
CONST OPLB0NOTEON = &H20
CONST OPLBDMEDOLICMODE = &H0
Show last 1103 lines
CONST OPLC0RIGHTSPEAKERENABLE = &H20
CONST OPLC0LEFTSPEAKERENABLE = &H10
CONST OPLDATA1 = OPLBASE + &H1
CONST OPLDATA2 = OPLBASE + &H3
CONST OPLINDEX1 = OPLBASE + &H0
CONST OPLINDEX2 = OPLBASE + &H2
CONST OPLMAXIMUMATTENUATION = &H3F
CONST OPLMAXIMUMVOLUME = &H3F
CONST PERCUSSIONONLY = &H9
CONST PERCUSSIVENOTES = &H6
CONST SIGN14BIT = &H2000
CONST TRUE = -1

TYPE ActiveNoteStr
AdlibChannel AS INTEGER
Index AS INTEGER
Note AS INTEGER
Volume AS INTEGER
END TYPE

TYPE ChannelStr
ActiveIndex AS INTEGER
Age AS LONG
ColorV AS INTEGER
MIDIChannel AS INTEGER
IsOn AS INTEGER
x AS INTEGER
XCharacter AS INTEGER
XColor AS INTEGER
END TYPE

TYPE ChannelInstrumentStr
Instrument AS INTEGER
Panning AS INTEGER
Pitch AS INTEGER
END TYPE

TYPE ChannelSettingsStr
Bend AS DOUBLE
Panning AS INTEGER
Patch AS INTEGER
Vibrato AS INTEGER
Volume AS INTEGER
END TYPE

TYPE MIDIStr
BendSense AS DOUBLE
Format AS INTEGER
InvDeltaTicks AS DOUBLE
Tempo AS DOUBLE
TrackCount AS LONG
END TYPE

TYPE OPLStr
DataV AS INTEGER
Index AS INTEGER
Operator AS INTEGER
SubChannel AS INTEGER
END TYPE

TYPE StatusStr
Began AS INTEGER
LoopEnd AS INTEGER
LoopStart AS INTEGER
LoopWait AS LONG
PlayWait AS LONG
TextRow AS INTEGER
END TYPE

TYPE TrackStr
Delay AS LONG
Pointer AS LONG
Status AS INTEGER
END TYPE

DECLARE FUNCTION AllocateChannel (Instrument)
DECLARE FUNCTION GetHertz# (Note, Bend AS DOUBLE)
DECLARE FUNCTION GetOctave (Hertz AS DOUBLE)
DECLARE FUNCTION GetShortestDelay& (TrackCount AS LONG)
DECLARE FUNCTION IsRunningStatus (Byte)
DECLARE FUNCTION ParseBEDWord& (Buffer AS STRING)
DECLARE FUNCTION ParseBEWord (Buffer AS STRING)
DECLARE FUNCTION ReadVariableLengthInteger& ()
DECLARE FUNCTION RemoveSpecialCharacters$ (Text AS STRING)
DECLARE FUNCTION ScheduleEventsAfterDelay# (Delay AS LONG, MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB ChangePanning (Channel, DataV)
DECLARE SUB ControllerChange (Channel, MIDI AS MIDIStr)
DECLARE SUB DeallocateActiveNote (Channel, Note)
DECLARE SUB Display (Row, Column, ColorV, Text AS STRING)
DECLARE SUB DisplayKeys ()
DECLARE SUB DisplayNote (AllocatedChannel, Note)
DECLARE SUB GetInstrumentData ()
DECLARE SUB HandleSpecialTrackEvent (MIDISpecialEvent, Track, MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB HandleSpecialTrackMetaEvent (Track, MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB HandleTrackEvent (Track, MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB Initialize (Status AS StatusStr)
DECLARE SUB Main (MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB NoteOff (Channel)
DECLARE SUB NoteOn (Channel, Status AS StatusStr)
DECLARE SUB NoteTouch (Channel)
DECLARE SUB OpenMIDI (FileName AS STRING, MIDI AS MIDIStr)
DECLARE SUB OPLNoteOff (Channel)
DECLARE SUB OPLNoteOn (Channel, Hertz AS DOUBLE)
DECLARE SUB OPLPatch (Channel)
DECLARE SUB OPLReset ()
DECLARE SUB OPLSetUp (Channel, OPL AS OPLStr)
DECLARE SUB OPLSilence ()
DECLARE SUB OPLTouchReal (Channel, Volume)
DECLARE SUB ProcessTrackEvents (MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB Quit ()
DECLARE SUB ResetAllControllers (Channel)
DECLARE SUB ReturnToLoopStart (MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB SaveLoopStart (MIDI AS MIDIStr, Status AS StatusStr)
DECLARE SUB UpdateAllLiveNotes (Channel, MIDIOperation, Volume)
DECLARE SUB UpdateNotePan (ActiveChannel, Channel)
DECLARE SUB UpdateNoteVolume (ActiveChannel, Channel, Volume)

DIM MIDI AS MIDIStr
DIM Status AS StatusStr

DIM SHARED ActiveNote(0 TO 15, 0 TO 127) AS ActiveNoteStr
DIM SHARED ActiveNoteCount(0 TO 15)
DIM SHARED ActiveNoteList(0 TO 15, 0 TO 99)
DIM SHARED Channels(0 TO 17) AS ChannelStr
DIM SHARED ChannelInstruments(0 TO 17) AS ChannelInstrumentStr
DIM SHARED ChannelSettings(0 TO 15) AS ChannelSettingsStr
DIM SHARED Instruments(0 TO 180, 0 TO 6)
DIM SHARED Loops(0 TO 0) AS TrackStr
DIM SHARED LoopsBackup(0 TO 0) AS TrackStr
DIM SHARED Tracks(0 TO 0) AS TrackStr

Initialize Status
CALL Main(MIDI, Status)
Quit
END

PlayerLoop:
IF Status.Began THEN Status.PlayWait = Status.PlayWait - 1

DO WHILE Status.PlayWait = 0
ProcessTrackEvents MIDI, Status
LOOP

DO WHILE PLAY(0) < EVENTBUFFERSIZE
PLAY "P64"
LOOP

RETURN

'The data words are:
'[0] Amplitude Modulator/Vibrato/Envelope Generator/Key Scaling Rate/Etc.
'[1] Key Scaling Level/Attenuation settings
'[2] Attack and decay rates
'[3] Sustain and release rates
'[4] Wave select settings
'[5] Feedback/connection bits
'[6] Percussive instrument (GP35..GP87) notes.

InstrumentData:
DATA &H101, &H68F, &HF2F2, &HF7F4, &H0, &H38, &H0: 'GM1:AcouGrandPiano
DATA &H101, &H4B, &HF2F2, &HF7F4, &H0, &H38, &H0: 'GM2:BrightAcouGrand
DATA &H101, &H49, &HF2F2, &HF6F4, &H0, &H38, &H0: 'GM3:ElecGrandPiano
DATA &H4181, &H12, &HF2F2, &HF7F7, &H0, &H36, &H0: 'GM4:Honky-tonkPiano
DATA &H101, &H57, &HF2F1, &HF7F7, &H0, &H30, &H0: 'GM5:Rhodes Piano
DATA &H101, &H93, &HF2F1, &HF7F7, &H0, &H30, &H0: 'GM6:Chorused Piano
DATA &H1601, &HE80, &HF2A1, &HF5F2, &H0, &H38, &H0: 'GM7:Harpsichord
DATA &H101, &H92, &HC2C2, &HF8F8, &H0, &H3A, &H0: 'GM8:Clavinet
DATA &H810C, &H5C, &HF3F6, &HF5F4, &H0, &H30, &H0: 'GM9:Celesta
DATA &H1107, &H8097, &HF2F3, &HF1F2, &H0, &H32, &H0: 'GM10:Glockenspiel
DATA &H117, &H21, &HF454, &HF4F4, &H0, &H32, &H0: 'GM11:Music box
DATA &H8198, &H62, &HF2F3, &HF6F6, &H0, &H30, &H0: 'GM12:Vibraphone
DATA &H118, &H23, &HE7F6, &HF7F6, &H0, &H30, &H0: 'GM13:Marimba
DATA &H115, &H91, &HF6F6, &HF6F6, &H0, &H34, &H0: 'GM14:Xylophone
DATA &H8145, &H8059, &HA3D3, &HF3F3, &H0, &H3C, &H0: 'GM15:Tubular Bells
DATA &H8103, &H8049, &HB575, &HF5F5, &H1, &H34, &H0: 'GM16:Dulcimer
DATA &H3171, &H92, &HF1F6, &H714, &H0, &H32, &H0: 'GM17:Hammond Organ
DATA &H3072, &H14, &HC7C7, &H858, &H0, &H32, &H0: 'GM18:Percussive Organ
DATA &HB170, &H44, &H8AAA, &H818, &H0, &H34, &H0: 'GM19:Rock Organ
DATA &HB123, &H93, &H5597, &H1423, &H1, &H34, &H0: 'GM20:Church Organ
DATA &HB161, &H8013, &H5597, &H404, &H1, &H30, &H0: 'GM21:Reed Organ
DATA &HB124, &H48, &H4698, &H1A2A, &H1, &H3C, &H0: 'GM22:Accordion
DATA &H2161, &H13, &H6191, &H706, &H1, &H3A, &H0: 'GM23:Harmonica
DATA &HA121, &H8913, &H6171, &H706, &H0, &H36, &H0: 'GM24:Tango Accordion
DATA &H4102, &H809C, &HF3F3, &HC894, &H1, &H3C, &H0: 'GM25:Acoustic Guitar1
DATA &H1103, &H54, &HF1F3, &HE79A, &H1, &H3C, &H0: 'GM26:Acoustic Guitar2
DATA &H2123, &H5F, &HF2F1, &HF83A, &H0, &H30, &H0: 'GM27:Electric Guitar1
DATA &H2103, &H8087, &HF3F6, &HF822, &H1, &H36, &H0: 'GM28:Electric Guitar2
DATA &H2103, &H47, &HF6F9, &H3A54, &H0, &H30, &H0: 'GM29:Electric Guitar3
DATA &H2123, &H54A, &H8491, &H1941, &H1, &H38, &H0: 'GM30:Overdrive Guitar
DATA &H2123, &H4A, &H9495, &H1919, &H1, &H38, &H0: 'GM31:Distorton Guitar
DATA &H8409, &H80A1, &HD120, &HF84F, &H0, &H38, &H0: 'GM32:Guitar Harmonics
DATA &HA221, &H1E, &HC394, &HA606, &H0, &H32, &H0: 'GM33:Acoustic Bass
DATA &H3131, &H12, &HF1F1, &H1828, &H0, &H3A, &H0: 'GM34:Electric Bass 1
DATA &H3131, &H8D, &HF1F1, &H78E8, &H0, &H3A, &H0: 'GM35:Electric Bass 2
DATA &H3231, &H5B, &H7151, &H4828, &H0, &H3C, &H0: 'GM36:Fretless Bass
DATA &H2101, &H408B, &HF2A1, &HDF9A, &H0, &H38, &H0: 'GM37:Slap Bass 1
DATA &H2121, &H88B, &HA1A2, &HDF16, &H0, &H38, &H0: 'GM38:Slap Bass 2
DATA &H3131, &H8B, &HF1F4, &H78E8, &H0, &H3A, &H0: 'GM39:Synth Bass 1
DATA &H3131, &H12, &HF1F1, &H1828, &H0, &H3A, &H0: 'GM40:Synth Bass 2
DATA &H2131, &H15, &H56DD, &H2613, &H1, &H38, &H0: 'GM41:Violin
DATA &H2131, &H16, &H66DD, &H613, &H1, &H38, &H0: 'GM42:Viola
DATA &H3171, &H49, &H61D1, &HC1C, &H1, &H38, &H0: 'GM43:Cello
DATA &H2321, &H804D, &H7271, &H612, &H1, &H32, &H0: 'GM44:Contrabass
DATA &HE1F1, &H40, &H6FF1, &H1621, &H1, &H32, &H0: 'GM45:Tremulo Strings
DATA &H102, &H801A, &H85F5, &H3575, &H1, &H30, &H0: 'GM46:Pizzicato String
DATA &H102, &H801D, &HF3F5, &HF475, &H1, &H30, &H0: 'GM47:Orchestral Harp
DATA &H1110, &H41, &HF2F5, &HC305, &H1, &H32, &H0: 'GM48:Timpany
DATA &HA221, &H19B, &H72B1, &H825, &H1, &H3E, &H0: 'GM49:String Ensemble1
DATA &H21A1, &H98, &H3F7F, &H703, &H101, &H30, &H0: 'GM50:String Ensemble2
DATA &H61A1, &H93, &H4FC1, &H512, &H0, &H3A, &H0: 'GM51:Synth Strings 1
DATA &H6121, &H18, &H4FC1, &H522, &H0, &H3C, &H0: 'GM52:SynthStrings 2
DATA &H7231, &H835B, &H8AF4, &H515, &H0, &H30, &H0: 'GM53:Choir Aahs
DATA &H61A1, &H90, &H7174, &H6739, &H0, &H30, &H0: 'GM54:Voice Oohs
DATA &H7271, &H57, &H7A54, &H505, &H0, &H3C, &H0: 'GM55:Synth Voice
DATA &H4190, &H0, &HA554, &H4563, &H0, &H38, &H0: 'GM56:Orchestra Hit
DATA &H2121, &H192, &H8F85, &H917, &H0, &H3C, &H0: 'GM57:Trumpet
DATA &H2121, &H594, &H8F75, &H917, &H0, &H3C, &H0: 'GM58:Trombone
DATA &H6121, &H94, &H8276, &H3715, &H0, &H3C, &H0: 'GM59:Tuba
DATA &H2131, &H43, &H629E, &H2C17, &H101, &H32, &H0: 'GM60:Muted Trumpet
DATA &H2121, &H9B, &H7F61, &HA6A, &H0, &H32, &H0: 'GM61:French Horn
DATA &H2261, &H68A, &H7475, &HF1F, &H0, &H38, &H0: 'GM62:Brass Section
DATA &H21A1, &H8386, &H7172, &H1855, &H1, &H30, &H0: 'GM63:Synth Brass 1
DATA &H2121, &H4D, &HA654, &H1C3C, &H0, &H38, &H0: 'GM64:Synth Brass 2
DATA &H6131, &H8F, &H7293, &HB02, &H1, &H38, &H0: 'GM65:Soprano Sax
DATA &H6131, &H8E, &H7293, &H903, &H1, &H38, &H0: 'GM66:Alto Sax
DATA &H6131, &H91, &H8293, &H903, &H1, &H3A, &H0: 'GM67:Tenor Sax
DATA &H6131, &H8E, &H7293, &HF0F, &H1, &H3A, &H0: 'GM68:Baritone Sax
DATA &H2121, &H4B, &H8FAA, &HA16, &H1, &H38, &H0: 'GM69:Oboe
DATA &H2131, &H90, &H8B7E, &HC17, &H101, &H36, &H0: 'GM70:English Horn
DATA &H3231, &H81, &H6175, &H1919, &H1, &H30, &H0: 'GM71:Bassoon
DATA &H2132, &H90, &H729B, &H1721, &H0, &H34, &H0: 'GM72:Clarinet
DATA &HE1E1, &H1F, &H6585, &H1A5F, &H0, &H30, &H0: 'GM73:Piccolo
DATA &HE1E1, &H46, &H6588, &H1A5F, &H0, &H30, &H0: 'GM74:Flute
DATA &H21A1, &H9C, &H7575, &HA1F, &H0, &H32, &H0: 'GM75:Recorder
DATA &H2131, &H8B, &H6584, &H1A58, &H0, &H30, &H0: 'GM76:Pan Flute
DATA &HA1E1, &H4C, &H6566, &H2656, &H0, &H30, &H0: 'GM77:Bottle Blow
DATA &HA162, &HCB, &H5576, &H3646, &H0, &H30, &H0: 'GM78:Shakuhachi
DATA &HA162, &H99, &H5657, &H707, &H0, &H3B, &H0: 'GM79:Whistle
DATA &HA162, &H93, &H7677, &H707, &H0, &H3B, &H0: 'GM80:Ocarina
DATA &H2122, &H59, &HFFFF, &HF03, &H2, &H30, &H0: 'GM81:Lead 1 squareea
DATA &H2121, &HE, &HFFFF, &HF0F, &H101, &H30, &H0: 'GM82:Lead 2 sawtooth
DATA &H2122, &H8046, &H6486, &H1855, &H0, &H30, &H0: 'GM83:Lead 3 calliope
DATA &HA121, &H45, &H9666, &HA12, &H0, &H30, &H0: 'GM84:Lead 4 chiff
DATA &H2221, &H8B, &H9192, &H2A2A, &H1, &H30, &H0: 'GM85:Lead 5 charang
DATA &H61A2, &H409E, &H6FDF, &H705, &H0, &H32, &H0: 'GM86:Lead 6 voice
DATA &H6020, &H1A, &H8FEF, &H601, &H200, &H30, &H0: 'GM87:Lead 7 fifths
DATA &H2121, &H808F, &HF4F1, &H929, &H0, &H3A, &H0: 'GM88:Lead 8 brass
DATA &HA177, &HA5, &HA053, &H594, &H0, &H32, &H0: 'GM89:Pad 1 new age
DATA &HB161, &H801F, &H25A8, &H311, &H0, &H3A, &H0: 'GM90:Pad 2 warm
DATA &H6161, &H17, &H5591, &H1634, &H0, &H3C, &H0: 'GM91:Pad 3 polysynth
DATA &H7271, &H5D, &H6A54, &H301, &H0, &H30, &H0: 'GM92:Pad 4 choir
DATA &HA221, &H97, &H4221, &H3543, &H0, &H38, &H0: 'GM93:Pad 5 bowedpad
DATA &H21A1, &H1C, &H31A1, &H4777, &H101, &H30, &H0: 'GM94:Pad 6 metallic
DATA &H6121, &H389, &H4211, &H2533, &H0, &H3A, &H0: 'GM95:Pad 7 halo
DATA &H21A1, &H15, &HCF11, &H747, &H1, &H30, &H0: 'GM96:Pad 8 sweep
DATA &H513A, &HCE, &H86F8, &H2F6, &H0, &H32, &H0: 'GM97:FX 1 rain
DATA &H2121, &H15, &H4121, &H1323, &H1, &H30, &H0: 'GM98:FX 2 soundtrack
DATA &H106, &H5B, &HA574, &H7295, &H0, &H30, &H0: 'GM99:FX 3 crystal
DATA &H6122, &H8392, &HF2B1, &H2681, &H0, &H3C, &H0: 'GM100:FX 4 atmosphere
DATA &H4241, &H4D, &HF2F1, &HF551, &H1, &H30, &H0: 'GM101:FX 5 brightness
DATA &HA361, &H8094, &H1111, &H1351, &H1, &H36, &H0: 'GM102:FX 6 goblins
DATA &HA161, &H808C, &H1D11, &H331, &H0, &H36, &H0: 'GM103:FX 7 echoes
DATA &H61A4, &H4C, &H81F3, &H2373, &H1, &H34, &H0: 'GM104:FX 8 sci-fi
DATA &H702, &H385, &HF2D2, &HF653, &H100, &H30, &H0: 'GM105:Sitar
DATA &H1311, &H800C, &HA2A3, &HE511, &H1, &H30, &H0: 'GM106:Banjo
DATA &H1111, &H6, &HF2F6, &HE641, &H201, &H34, &H0: 'GM107:Shamisen
DATA &H9193, &H91, &HEBD4, &H1132, &H100, &H38, &H0: 'GM108:Koto
DATA &H104, &H4F, &HC2FA, &H556, &H0, &H3C, &H0: 'GM109:Kalimba
DATA &H2221, &H49, &H6F7C, &HC20, &H100, &H36, &H0: 'GM110:Bagpipe
DATA &H2131, &H85, &H56DD, &H1633, &H1, &H3A, &H0: 'GM111:Fiddle
DATA &H2120, &H8104, &H8FDA, &HB05, &H2, &H36, &H0: 'GM112:Shanai
DATA &H305, &H806A, &HC3F1, &HE5E5, &H0, &H36, &H0: 'GM113:Tinkle Bell
DATA &H207, &H15, &HF8EC, &H1626, &H0, &H3A, &H0: 'GM114:Agogo Bells
DATA &H105, &H9D, &HDF67, &H535, &H0, &H38, &H0: 'GM115:Steel Drums
DATA &H1218, &H96, &HF8FA, &HE528, &H0, &H3A, &H0: 'GM116:Woodblock
DATA &H10, &H386, &HFAA8, &H307, &H0, &H36, &H0: 'GM117:Taiko Drum
DATA &H1011, &H341, &HF3F8, &H347, &H2, &H34, &H0: 'GM118:Melodic Tom
DATA &H1001, &H8E, &HF3F1, &H206, &H2, &H3E, &H0: 'GM119:Synth Drum
DATA &HC00E, &H0, &H1F1F, &HFF00, &H300, &H3E, &H0: 'GM120:Reverse Cymbal
DATA &H306, &H8880, &H56F8, &H8424, &H200, &H3E, &H0: 'GM121:Guitar FretNoise
DATA &HD00E, &H500, &H34F8, &H400, &H300, &H3E, &H0: 'GM122:Breath Noise
DATA &HC00E, &H0, &H1FF6, &H200, &H300, &H3E, &H0: 'GM123:Seashore
DATA &HDAD5, &H4095, &H5637, &H37A3, &H0, &H30, &H0: 'GM124:Bird Tweet
DATA &H1435, &H85C, &HF4B2, &H1561, &H2, &H3A, &H0: 'GM125:Telephone
DATA &HD00E, &H0, &H4FF6, &HF500, &H300, &H3E, &H0: 'GM126:Helicopter
DATA &HE426, &H0, &H12FF, &H1601, &H100, &H3E, &H0: 'GM127:Applause/Noise
DATA &H0, &H0, &HF6F3, &HC9F0, &H200, &H3E, &H0: 'GM128:Gunshot
DATA &H1110, &H44, &HF3F8, &H677, &H2, &H38, &H23: 'GP35:Ac Bass Drum
DATA &H1110, &H44, &HF3F8, &H677, &H2, &H38, &H23: 'GP36:Bass Drum 1
DATA &H1102, &H7, &HF8F9, &HFFFF, &H0, &H38, &H34: 'GP37:Side Stick
DATA &H0, &H0, &HFAFC, &H1705, &H2, &H3E, &H30: 'GP38:Acoustic Snare
DATA &H100, &H2, &HFFFF, &H807, &H0, &H30, &H3A: 'GP39:Hand Clap
DATA &H0, &H0, &HFAFC, &H1705, &H2, &H3E, &H3C: 'GP40:Electric Snare
DATA &H0, &H0, &HF6F6, &H60C, &H0, &H34, &H2F: 'GP41:Low Floor Tom
DATA &H120C, &H0, &HFBF6, &H4708, &H200, &H3A, &H2B: 'GP42:Closed High Hat
DATA &H0, &H0, &HF6F6, &H60C, &H0, &H34, &H31: 'GP43:High Floor Tom
DATA &H120C, &H500, &H7BF6, &H4708, &H200, &H3A, &H2B: 'GP44:Pedal High Hat
DATA &H0, &H0, &HF6F6, &H60C, &H0, &H34, &H33: 'GP45:Low Tom
DATA &H120C, &H0, &HCBF6, &H4302, &H200, &H3A, &H2B: 'GP46:Open High Hat
DATA &H0, &H0, &HF6F6, &H60C, &H0, &H34, &H36: 'GP47:Low-Mid Tom
DATA &H0, &H0, &HF6F6, &H60C, &H0, &H34, &H39: 'GP48:High-Mid Tom
DATA &HD00E, &H0, &H9FF6, &H200, &H300, &H3E, &H48: 'GP49:Crash Cymbal 1
DATA &H0, &H0, &HF6F6, &H60C, &H0, &H34, &H3C: 'GP50:High Tom
DATA &H70E, &H4A08, &HF4F8, &HE442, &H300, &H3E, &H4C: 'GP51:Ride Cymbal 1
DATA &HD00E, &HA00, &H9FF5, &H230, &H0, &H3E, &H54: 'GP52:Chinese Cymbal
DATA &H70E, &H5D0A, &HF5E4, &HE5E4, &H103, &H36, &H24: 'GP53:Ride Bell
DATA &H502, &HA03, &H97B4, &HF704, &H0, &H3E, &H41: 'GP54:Tambourine
DATA &H9E4E, &H0, &H9FF6, &H200, &H300, &H3E, &H54: 'GP55:Splash Cymbal
DATA &H1011, &H845, &HF3F8, &H537, &H2, &H38, &H53: 'GP56:Cow Bell
DATA &HD00E, &H0, &H9FF6, &H200, &H300, &H3E, &H54: 'GP57:Crash Cymbal 2
DATA &H1080, &HD00, &HFFFF, &H1403, &H3, &H3C, &H18: 'GP58:Vibraslap
DATA &H70E, &H4A08, &HF4F8, &HE442, &H300, &H3E, &H4D: 'GP59:Ride Cymbal 2
DATA &H206, &HB, &HF5F5, &H80C, &H0, &H36, &H3C: 'GP60:High Bongo
DATA &H201, &H0, &HC8FA, &H97BF, &H0, &H37, &H41: 'GP61:Low Bongo
DATA &H101, &H51, &HFAFA, &HB787, &H0, &H36, &H3B: 'GP62:Mute High Conga
DATA &H201, &H54, &HF8FA, &HB88D, &H0, &H36, &H33: 'GP63:Open High Conga
DATA &H201, &H59, &HF8FA, &HB688, &H0, &H36, &H2D: 'GP64:Low Conga
DATA &H1, &H0, &HFAF9, &H60A, &H3, &H3E, &H47: 'GP65:High Timbale
DATA &H0, &H80, &HF6F9, &H6C89, &H3, &H3E, &H3C: 'GP66:Low Timbale
DATA &HC03, &H880, &HF6F8, &HB688, &H3, &H3F, &H3A: 'GP67:High Agogo
DATA &HC03, &H85, &HF6F8, &HB688, &H3, &H3F, &H35: 'GP68:Low Agogo
DATA &HE, &H840, &H7776, &H184F, &H200, &H3E, &H40: 'GP69:Cabasa
DATA &H30E, &H40, &H9BC8, &H6949, &H200, &H3E, &H47: 'GP70:Maracas
DATA &HC7D7, &HDC, &H8DAD, &H505, &H3, &H3E, &H3D: 'GP71:Short Whistle
DATA &HC7D7, &HDC, &H88A8, &H404, &H3, &H3E, &H3D: 'GP72:Long Whistle
DATA &H1180, &H0, &H67F6, &H1706, &H303, &H3E, &H2C: 'GP73:Short Guiro
DATA &H1180, &H900, &H46F5, &H1605, &H302, &H3E, &H28: 'GP74:Long Guiro
DATA &H1506, &H3F, &HF700, &HF5F4, &H0, &H31, &H45: 'GP75:Claves
DATA &H1206, &H3F, &HF700, &HF5F4, &H3, &H30, &H44: 'GP76:High Wood Block
DATA &H1206, &H3F, &HF700, &HF5F4, &H0, &H31, &H3F: 'GP77:Low Wood Block
DATA &H201, &H58, &H7567, &H7E7, &H0, &H30, &H4A: 'GP78:Mute Cuica
DATA &H4241, &H845, &H75F8, &H548, &H0, &H30, &H3C: 'GP79:Open Cuica
DATA &H1E0A, &H4E40, &HFFE0, &H5F0, &H3, &H38, &H50: 'GP80:Mute Triangle
DATA &H1E0A, &H527C, &HFFE0, &H2F0, &H3, &H38, &H40: 'GP81:Open Triangle
DATA &HE, &H840, &H7B7A, &H1B4A, &H200, &H3E, &H48: 'GP82
DATA &H70E, &H400A, &H55E4, &H39E4, &H103, &H36, &H49: 'GP83
DATA &H405, &H4005, &HD6F9, &HA532, &H3, &H3E, &H46: 'GP84
DATA &H1502, &H3F, &HF700, &HF5F3, &H3, &H38, &H44: 'GP85
DATA &H201, &H4F, &HF8FA, &HB58D, &H0, &H37, &H30: 'GP86
DATA &H0, &H0, &HF6F6, &H60C, &H0, &H34, &H35: 'GP87

REM $STATIC
FUNCTION AllocateChannel (Instrument)
DIM AllocatedChannel
DIM CandidateChannel
DIM HighestScore AS DOUBLE
DIM Score AS DOUBLE

AllocatedChannel = NOCHANNEL
HighestScore = &H80000000
Score = &H0
FOR CandidateChannel = LBOUND(Channels) TO UBOUND(Channels)
IF Channels(CandidateChannel).IsOn THEN
Score = &H0
IF ChannelInstruments(CandidateChannel).Instrument = Instrument THEN
Score = Score + .2
ELSEIF Instrument < &H80 AND ChannelInstruments(CandidateChannel).Instrument > &H7F THEN
Score = (Score * &H2) + &H9
END IF
ELSE
Score = (Score + Channels(CandidateChannel).Age) + &HBB8
END IF

IF Score > HighestScore THEN
HighestScore = Score
AllocatedChannel = CandidateChannel
END IF
NEXT CandidateChannel

AllocateChannel = AllocatedChannel
END FUNCTION

SUB ChangePanning (Channel, DataV)
DIM Panning

Panning = NULL
IF DataV < &H30 THEN
Panning = OPLC0RIGHTSPEAKERENABLE
ELSEIF DataV >= &H50 THEN
Panning = OPLC0LEFTSPEAKERENABLE
END IF

ChannelSettings(Channel).Panning = Panning
END SUB

SUB ControllerChange (Channel, MIDI AS MIDIStr)
DIM Controller
DIM DataV

Controller = ASC(INPUT$(&H1, 1))
DataV = ASC(INPUT$(&H1, 1))

SELECT CASE Controller
CASE MCMODULATION
ChannelSettings(Channel).Vibrato = DataV
UpdateAllLiveNotes Channel, MOPPAN, NULL
CASE MCBEND
MIDI.BendSense = DataV / MAXIMUMSIGNEDBEND
CASE MCVOLUME
ChannelSettings(Channel).Volume = DataV
CASE MCPAN
ChangePanning Channel, DataV
CASE MCRESETALLCONTROLLERS
ResetAllControllers Channel
CASE MCALLNOTESOFF
UpdateAllLiveNotes Channel, MOPVOLUME, NULL
END SELECT
END SUB

SUB DeallocateActiveNote (Channel, ActiveNote)
DIM DeactivatedNoteChannel
DIM NoteToDeactivate

NoteToDeactivate = ActiveNoteList(Channel, ActiveNote)
ActiveNote(Channel, NoteToDeactivate).Index = &H0
DeactivatedNoteChannel = ActiveNote(Channel, NoteToDeactivate).AdlibChannel
Channels(DeactivatedNoteChannel).Age = &H0
Channels(DeactivatedNoteChannel).IsOn = FALSE

OPLNoteOff DeactivatedNoteChannel

IF Channels(DeactivatedNoteChannel).XCharacter = 32 THEN
Display 20 - DeactivatedNoteChannel, Channels(DeactivatedNoteChannel).x, 1, "."
ELSE
Display 20 - DeactivatedNoteChannel, Channels(DeactivatedNoteChannel).x, Channels(DeactivatedNoteChannel).XColor, CHR$(Channels(DeactivatedNoteChannel).XCharacter)
END IF

IF NOT ActiveNote = ActiveNoteCount(Channel) THEN
NoteToDeactivate = ActiveNoteList(Channel, ActiveNoteCount(Channel))
ActiveNoteList(Channel, ActiveNote) = NoteToDeactivate
ActiveNote(Channel, NoteToDeactivate).Index = ActiveNote
Channels(ActiveNote(Channel, NoteToDeactivate).AdlibChannel).ActiveIndex = ActiveNote
END IF

ActiveNoteCount(Channel) = ActiveNoteCount(Channel) - 1
END SUB

SUB Display (Row, Column, ColorV, Text AS STRING)
COLOR ColorV
IF NOT Column = NULL THEN LOCATE , Column
IF NOT Row = NULL THEN LOCATE Row
PRINT Text;
END SUB

SUB DisplayKeys ()
Display 1, 1, 15, "Press Q to quit"
END SUB

SUB DisplayNote (AllocatedChannel, Note)
DIM x
DIM y

Channels(AllocatedChannel).x = (Note MOD 80) + 1
Channels(AllocatedChannel).ColorV = 9 + (ChannelInstruments(AllocatedChannel).Instrument MOD &H6)
y = 20 - AllocatedChannel
x = Channels(AllocatedChannel).x
Channels(AllocatedChannel).XCharacter = SCREEN(y, x, FALSE)
Channels(AllocatedChannel).XColor = SCREEN(y, x, TRUE)
Display y, x, Channels(AllocatedChannel).ColorV, "#"
END SUB

FUNCTION GetHertz# (Note, Bend AS DOUBLE)
'''!!!
GetHertz# = 172.00093# * (EXP(.057762265# * (Note + Bend)))
END FUNCTION

SUB GetInstrumentData ()
DIM Column
DIM Instrument

RESTORE InstrumentData
FOR Instrument = LBOUND(Instruments, 1) TO UBOUND(Instruments, 1)
FOR Column = LBOUND(Instruments, 2) TO UBOUND(Instruments, 2)
READ Instruments(Instrument, Column)
NEXT Column
NEXT Instrument
END SUB

FUNCTION GetOctave (Hertz AS DOUBLE)
'''!!!
DIM Octave

Octave = &H0
DO WHILE Hertz >= 1023.5
Hertz = Hertz / &H2
Octave = Octave + &H400
LOOP

GetOctave = Octave + CINT(Hertz)
END FUNCTION

FUNCTION GetShortestDelay& (TrackCount AS LONG)
DIM ShortestDelay AS LONG
DIM Track

ShortestDelay = -1
FOR Track = 0 TO TrackCount - 1
IF Tracks(Track).Status >= 0 THEN
IF ShortestDelay = -1 OR Tracks(Track).Delay < ShortestDelay THEN
ShortestDelay = Tracks(Track).Delay
END IF
END IF
NEXT Track

GetShortestDelay& = ShortestDelay
END FUNCTION

SUB HandleSpecialTrackEvent (MIDISpecialEvent, Track, MIDI AS MIDIStr, Status AS StatusStr)
DIM Ignored AS LONG

SELECT CASE MIDISpecialEvent
CASE MESONGPOSITION
SEEK #1, SEEK(1) + &H2
CASE MESONGSELECT
SEEK #1, SEEK(1) + &H1
CASE MESYSEXSTART, MESYSEXEND
Ignored = ReadVariableLengthInteger&
CASE ELSE
HandleSpecialTrackMetaEvent Track, MIDI, Status
END SELECT
END SUB

SUB HandleSpecialTrackMetaEvent (Track, MIDI AS MIDIStr, Status AS StatusStr)
DIM EventType
DIM StringV AS STRING

EventType = ASC(INPUT$(&H1, 1))
StringV = INPUT$(ReadVariableLengthInteger&, 1)

SELECT CASE EventType
CASE MMETMARKER
IF StringV = "loopStart" THEN
Status.LoopStart = TRUE
ELSEIF StringV = "loopEnd" THEN
Status.LoopEnd = TRUE
END IF
CASE MMEENDOFTRACK
Tracks(Track).Status = -1
Status.LoopEnd = TRUE
CASE MMESETTEMPO
MIDI.Tempo = ParseBEDWord&(CHR$(&H0) + StringV) * MIDI.InvDeltaTicks
CASE MMETCOPYRIGHT, MMETGENERIC, MMETINSTRUMENT, MMETLYRIC, MMETMARKER, MMETTRACK
Status.TextRow = 3 + (Status.TextRow - 2) MOD 20
Display Status.TextRow, 1, 8, "Meta" + STR$(EventType) + ": " + RemoveSpecialCharacters$(StringV)
END SELECT
END SUB

SUB HandleTrackEvent (Track, MIDI AS MIDIStr, Status AS StatusStr)
DIM Byte
DIM Channel
DIM MIDIEvent

SEEK #1, Tracks(Track).Pointer + &H1
Byte = ASC(INPUT$(&H1, 1))

IF Byte < MESYSEXSTART THEN
IF IsRunningStatus(Byte) THEN
Byte = Tracks(Track).Status OR MEFIRST
SEEK #1, Tracks(Track).Pointer + &H1
END IF

Tracks(Track).Status = Byte

Channel = Byte AND MECHANNELMASK
MIDIEvent = Byte AND MEMASK

SELECT CASE MIDIEvent
CASE MENOTEOFF
NoteOff Channel
CASE MENOTEON
NoteOn Channel, Status
CASE MEPOLYPHONIC
NoteTouch Channel
CASE MECONTROLLER
ControllerChange Channel, MIDI
CASE MEINSTRUMENT
ChannelSettings(Channel).Patch = ASC(INPUT$(&H1, 1))
CASE MEPRESSURE
UpdateAllLiveNotes Channel, MOPPRESSURE, ASC(INPUT$(&H1, 1))
CASE MEBEND
ChannelSettings(Channel).Bend = (((ASC(INPUT$(&H1, 1)) AND &H7F) OR (ASC(INPUT$(&H1, 1)) * &H80)) - SIGN14BIT) * MIDI.BendSense
UpdateAllLiveNotes Channel, MOPPITCH, NULL
END SELECT
ELSE
HandleSpecialTrackEvent Byte, Track, MIDI, Status
END IF
END SUB

SUB Initialize (Status AS StatusStr)
DIM Channel

SCREEN 0
WIDTH 80, 25
PALETTE
COLOR 7, 0
CLS

GetInstrumentData
OPLReset
OPLReset

FOR Channel = LBOUND(ChannelSettings) TO UBOUND(ChannelSettings)
ChannelSettings(Channel).Volume = &H7F
NEXT Channel

Status.Began = FALSE
Status.LoopEnd = FALSE
Status.LoopStart = TRUE
Status.LoopWait = 0
Status.PlayWait = 0
Status.TextRow = 2

PLAY ON
ON PLAY(EVENTBUFFERSIZE) GOSUB PlayerLoop
PLAY "ML MB T255"
END SUB

FUNCTION IsRunningStatus (Byte)
IsRunningStatus = ((Byte AND MEFIRST) = &H0)
END FUNCTION

SUB Main (MIDI AS MIDIStr, Status AS StatusStr)
DIM FileName AS STRING
DIM KeyStroke AS STRING

FileName = COMMAND$

IF FileName = "" THEN
Display 1, 1, 15, "No file specified."
ELSE
OpenMIDI FileName, MIDI
PLAY "T255 P64 P64"

DisplayKeys

DO
KeyStroke = INKEY$

SELECT CASE KeyStroke
CASE "q", "Q"
EXIT DO
END SELECT
LOOP
END IF
END SUB

SUB NoteOff (Channel)
DIM ActiveNote
DIM Note

Note = ASC(INPUT$(&H1, 1))
SEEK #1, SEEK(1) + &H1

ChannelSettings(Channel).Bend = &H0
ActiveNote = ActiveNote(Channel, Note).Index
IF NOT ActiveNote = &H0 THEN
DeallocateActiveNote Channel, ActiveNote
END IF
END SUB

SUB NoteOn (Channel, Status AS StatusStr)
DIM ActiveNoteCount
DIM AllocatedChannel
DIM Instrument
DIM NoteToBePlayed
DIM TrackNote
DIM Volume

TrackNote = ASC(INPUT$(&H1, 1))
Volume = ASC(INPUT$(&H1, 1))

IF Volume = NULL THEN
ActiveNoteCount = ActiveNote(Channel, TrackNote).Index
IF NOT ActiveNoteCount = 0 THEN
DeallocateActiveNote Channel, ActiveNoteCount
END IF
ELSE
IF ActiveNote(Channel, TrackNote).Index = &H0 THEN
IF Channel = PERCUSSIONONLY THEN
Instrument = GP01 + TrackNote
NoteToBePlayed = Instruments(Instrument, PERCUSSIVENOTES)
ELSE
Instrument = ChannelSettings(Channel).Patch
NoteToBePlayed = TrackNote
END IF

AllocatedChannel = AllocateChannel(Instrument)

IF Channels(AllocatedChannel).IsOn THEN
DeallocateActiveNote Channels(AllocatedChannel).MIDIChannel, Channels(AllocatedChannel).ActiveIndex
END IF

Channels(AllocatedChannel).Age = &H0
ChannelInstruments(AllocatedChannel).Instrument = Instrument
Channels(AllocatedChannel).IsOn = TRUE
Status.Began = TRUE

ActiveNoteCount = ActiveNoteCount(Channel) + 1
ActiveNote(Channel, TrackNote).Index = ActiveNoteCount
ActiveNoteList(Channel, ActiveNoteCount) = TrackNote
ActiveNoteCount(Channel) = ActiveNoteCount

ActiveNote(Channel, TrackNote).AdlibChannel = AllocatedChannel
ActiveNote(Channel, TrackNote).Note = NoteToBePlayed
ActiveNote(Channel, TrackNote).Volume = Volume
Channels(AllocatedChannel).MIDIChannel = Channel
Channels(AllocatedChannel).ActiveIndex = ActiveNoteCount
OPLPatch AllocatedChannel
UpdateNotePan AllocatedChannel, Channel
UpdateNoteVolume AllocatedChannel, Channel, Volume

DisplayNote AllocatedChannel, TrackNote

OPLNoteOn AllocatedChannel, GetHertz#(TrackNote, ChannelSettings(Channel).Bend)
UpdateNoteVolume AllocatedChannel, Channel, Volume
END IF
END IF
END SUB

SUB NoteTouch (Channel)
DIM ActiveChannel
DIM Note
DIM Volume

Note = ASC(INPUT$(&H1, 1))
Volume = ASC(INPUT$(&H1, 1))
IF NOT ActiveNote(Channel, Note).Index = &H0 THEN
ActiveChannel = ActiveNote(Channel, Note).AdlibChannel
Display 20 - ActiveChannel, Channels(ActiveChannel).x, Channels(ActiveChannel).ColorV, "&"
ActiveNote(Channel, Note).Volume = Volume
UpdateNoteVolume ActiveChannel, Channel, Volume
END IF
END SUB

SUB OpenMIDI (FileName AS STRING, MIDI AS MIDIStr)
DIM TicksPerQuarterNote
DIM Track
DIM TrackDelayPosition AS LONG
DIM TrackLength AS LONG

OPEN FileName$ FOR INPUT LOCK READ AS #1
CLOSE #1

OPEN FileName$ FOR BINARY LOCK READ WRITE AS #1
IF INPUT$(&H4, 1) = "MThd" THEN
IF ParseBEDWord&(INPUT$(&H4, 1)) = MTHDBLOCKDEFAULTLENGTH THEN
MIDI.Format = ParseBEWord(INPUT$(&H2, 1))
MIDI.TrackCount = ParseBEWord(INPUT$(&H2, 1))

REDIM Loops(0 TO MIDI.TrackCount - 1) AS TrackStr
REDIM LoopsBackup(0 TO MIDI.TrackCount - 1) AS TrackStr
REDIM Tracks(0 TO MIDI.TrackCount - 1) AS TrackStr

TicksPerQuarterNote = ParseBEWord(INPUT$(&H2, 1))
'''!!!
MIDI.InvDeltaTicks = EVENTTICKFREQUENCY / (240000000# * TicksPerQuarterNote)
MIDI.Tempo = DEFAULTTEMPO * MIDI.InvDeltaTicks
'''!!!

MIDI.BendSense = &H2 / MAXIMUMSIGNEDBEND

FOR Track = 0 TO MIDI.TrackCount - 1
IF INPUT$(&H4, 1) = "MTrk" THEN
TrackLength = ParseBEDWord&(INPUT$(&H4, 1))
TrackDelayPosition = LOC(1)
Tracks(Track).Delay = ReadVariableLengthInteger&
Tracks(Track).Pointer = LOC(1)
SEEK #1, (TrackDelayPosition + TrackLength) + &H1
ELSE
ERROR 13
END IF
NEXT Track
ELSE
ERROR 6
END IF
ELSE
ERROR 13
END IF
END SUB

SUB OPLNoteOff (Channel)
DIM OPL AS OPLStr

OPLSetUp Channel, OPL
OUT OPL.Index, &HB0 OR OPL.SubChannel
OUT OPL.DataV, ChannelInstruments(Channel).Pitch AND (OPLB0FREQUENCYMASK OR OPLB0BLOCKMASK)
END SUB

SUB OPLNoteOn (Channel, Hertz AS DOUBLE)
DIM Octave
DIM OPL AS OPLStr

Octave = GetOctave(Hertz)
OPLSetUp Channel, OPL

OUT OPL.Index, &HA0 OR OPL.SubChannel
OUT OPL.DataV, Octave AND &HFF&

OUT OPL.Index, &HB0 OR OPL.SubChannel
OUT OPL.DataV, (((Octave AND &HFF00&) \ &H100&) AND (OPLB0FREQUENCYMASK OR OPLB0BLOCKMASK)) OR OPLB0NOTEON

ChannelInstruments(Channel).Pitch = (Octave AND &HFF00) \ &H100&
END SUB

SUB OPLPatch (Channel)
DIM Instrument
DIM OPL AS OPLStr

Instrument = ChannelInstruments(Channel).Instrument

OPLSetUp Channel, OPL

OUT OPL.Index, &H20 + OPL.Operator
OUT OPL.DataV, Instruments(Instrument, &H0) AND &HFF&
OUT OPL.Index, &H60 + OPL.Operator
OUT OPL.DataV, Instruments(Instrument, &H2) AND &HFF&
OUT OPL.Index, &H80 + OPL.Operator
OUT OPL.DataV, Instruments(Instrument, &H3) AND &HFF&
OUT OPL.Index, &HE0 + OPL.Operator
OUT OPL.DataV, Instruments(Instrument, &H4) AND &HFF&

OUT OPL.Index, &H20 + OPL.Operator + &H3
OUT OPL.DataV, (Instruments(Instrument, &H0) AND &HFF00&) / &H100&
OUT OPL.Index, &H60 + OPL.Operator + &H3
OUT OPL.DataV, (Instruments(Instrument, &H2) AND &HFF00&) / &H100&
OUT OPL.Index, &H80 + OPL.Operator + &H3
OUT OPL.DataV, (Instruments(Instrument, &H3) AND &HFF00&) / &H100&
OUT OPL.Index, &HE0 + OPL.Operator + &H3
OUT OPL.DataV, (Instruments(Instrument, &H4) AND &HFF00&) / &H100&
END SUB

SUB OPLReset ()
DIM OPL AS OPLStr

OPLSetUp NULL, OPL
OUT OPL.Index, &H4
OUT OPL.DataV, OPL04TIMERS12ON
OUT OPL.Index, &H4
OUT OPL.DataV, OPL04IRQRESET

OPLSetUp PERCUSSIONONLY, OPL
OUT OPL.Index, &H5
OUT OPL.DataV, OPL05OPL3OFF
OUT OPL.Index, &H5
OUT OPL.DataV, OPL05OPL3ON
OUT OPL.Index, &H5
OUT OPL.DataV, OPL05OPL3OFF

OPLSetUp NULL, OPL
OUT OPL.Index, &H1
OUT OPL.DataV, OPL01WAVEON

OUT OPL.Index, &HBD
OUT OPL.DataV, OPLBDMEDOLICMODE

OPLSetUp PERCUSSIONONLY, OPL
OUT OPL.Index, &H5
OUT OPL.DataV, OPL05OPL3ON

OPLSetUp PERCUSSIONONLY, OPL
OUT OPL.Index, &H4
OUT OPL.DataV, OPL04MODE0

OPLSilence
END SUB

SUB OPLSetUp (Channel, OPL AS OPLStr)
OPL.DataV = &H0
OPL.Index = &H0
OPL.Operator = &H0
OPL.SubChannel = &H0

SELECT CASE Channel
CASE &H0 TO &H8
OPL.DataV = OPLDATA1
OPL.Index = OPLINDEX1
OPL.SubChannel = Channel
CASE &H9 TO &H11
OPL.DataV = OPLDATA2
OPL.Index = OPLINDEX2
OPL.SubChannel = Channel - &H9
END SELECT

OPL.Operator = (OPL.SubChannel MOD &H3) OR (&H8 * (OPL.SubChannel \ &H3))
END SUB

SUB OPLSilence ()
DIM Channel

FOR Channel = LBOUND(Channels) TO UBOUND(Channels)
OPLNoteOff Channel
OPLTouchReal Channel, &H0
NEXT Channel
END SUB

SUB OPLTouchReal (Channel, Volume)
DIM Attenuation
DIM Instrument
DIM KSLA
DIM OPL AS OPLStr

Instrument = ChannelInstruments(Channel).Instrument

OPLSetUp Channel, OPL

KSLA = Instruments(Instrument, 1) AND &HFF&
Attenuation = ((KSLA AND OPLATTENUATIONMASK) * Volume) \ OPLMAXIMUMVOLUME

OUT OPL.Index, &H40 + OPL.Operator
OUT OPL.DataV, ((KSLA OR OPLMAXIMUMATTENUATION) - Volume) + Attenuation

KSLA = (Instruments(Instrument, 1) AND &HFF00&) / &H100&
Attenuation = ((KSLA AND OPLATTENUATIONMASK) * Volume) \ OPLMAXIMUMVOLUME

OUT OPL.Index, &H43 + OPL.Operator
OUT OPL.DataV, ((KSLA OR OPLMAXIMUMATTENUATION) - Volume) + Attenuation
END SUB

FUNCTION ParseBEDWord& (Buffer AS STRING)
DIM DWord AS LONG

DWord = ASC(MID$(Buffer, &H4, &H1))
DWord = DWord OR (ASC(MID$(Buffer, &H3, &H1)) * &H100&)
DWord = DWord OR (ASC(MID$(Buffer, &H2, &H1)) * &H10000)
ParseBEDWord& = DWord OR (ASC(MID$(Buffer, &H1, &H1)) * &H1000000)
END FUNCTION

FUNCTION ParseBEWord (Buffer AS STRING)
DIM Word

Word = ASC(MID$(Buffer, &H2, &H1))
ParseBEWord = Word OR (ASC(MID$(Buffer, &H1, &H1)) * &H100)
END FUNCTION

SUB ProcessTrackEvents (MIDI AS MIDIStr, Status AS StatusStr)
DIM Track

FOR Track = 0 TO MIDI.TrackCount - 1
LoopsBackup(Track).Delay = Tracks(Track).Delay
LoopsBackup(Track).Pointer = Tracks(Track).Pointer
LoopsBackup(Track).Status = Tracks(Track).Status

IF Tracks(Track).Status >= 0 AND Tracks(Track).Delay <= 0 THEN
HandleTrackEvent Track, MIDI, Status
IF Status.LoopEnd THEN

ReturnToLoopStart MIDI, Status

COLOR , 0
CLS
Status.TextRow = 2
DisplayKeys
ELSE
Tracks(Track).Delay = Tracks(Track).Delay + ReadVariableLengthInteger&
Tracks(Track).Pointer = LOC(1)
END IF
END IF
NEXT Track

IF Status.LoopStart THEN
SaveLoopStart MIDI, Status
END IF

DO WHILE ScheduleEventsAfterDelay#(GetShortestDelay&(MIDI.TrackCount), MIDI, Status) < 0
LOOP
END SUB

SUB Quit ()
PLAY OFF
Display NULL, NULL, 9, "End!"
OPLSilence
CLOSE #1
COLOR 7, 0
END SUB

FUNCTION ReadVariableLengthInteger& ()
DIM Byte
DIM VariableLengthInteger AS LONG

VariableLengthInteger = &H0
DO
Byte = ASC(INPUT$(&H1, 1))
VariableLengthInteger = (VariableLengthInteger * &H80) OR (Byte AND &H7F)
LOOP UNTIL Byte < &H80

ReadVariableLengthInteger = VariableLengthInteger
END FUNCTION

FUNCTION RemoveSpecialCharacters$ (Text AS STRING)
DIM Position
DIM Result AS STRING

Result = Text
FOR Position = 1 TO LEN(Text)
SELECT CASE MID$(Result, Position, 1)
CASE IS < " ", IS > "~"
MID$(Result, Position, 1) = "?"
END SELECT
NEXT Position

RemoveSpecialCharacters$ = Result
END FUNCTION

SUB ResetAllControllers (Channel)
ChannelInstruments(Channel).Panning = &H0
ChannelSettings(Channel).Bend = &H0
ChannelSettings(Channel).Vibrato = &H0
UpdateAllLiveNotes Channel, MOPOFF, NULL
UpdateAllLiveNotes Channel, MOPPAN, NULL
END SUB

SUB ReturnToLoopStart (MIDI AS MIDIStr, Status AS StatusStr)
DIM Track AS LONG

FOR Track = 0 TO MIDI.TrackCount - 1
Tracks(Track).Delay = Loops(Track).Delay
Tracks(Track).Pointer = Loops(Track).Pointer
Tracks(Track).Status = Loops(Track).Status
NEXT Track

Status.LoopEnd = FALSE
Status.PlayWait = Status.LoopWait
END SUB

SUB SaveLoopStart (MIDI AS MIDIStr, Status AS StatusStr)
DIM Track AS LONG

FOR Track = 0 TO MIDI.TrackCount - 1
Loops(Track).Delay = LoopsBackup(Track).Delay
Loops(Track).Pointer = LoopsBackup(Track).Pointer
Loops(Track).Status = LoopsBackup(Track).Status
NEXT Track

Status.LoopWait = Status.PlayWait
Status.LoopStart = FALSE
END SUB

FUNCTION ScheduleEventsAfterDelay# (Delay AS LONG, MIDI AS MIDIStr, Status AS StatusStr)
DIM AgeIncrease AS DOUBLE
DIM Channel
DIM Track

AgeIncrease = Delay * MIDI.Tempo

FOR Channel = LBOUND(Channels) TO UBOUND(Channels)
Channels(Channel).Age = Channels(Channel).Age + AgeIncrease
NEXT Channel

FOR Track = 0 TO MIDI.TrackCount - 1
Tracks(Track).Delay = Tracks(Track).Delay - Delay
NEXT Track

IF Status.Began THEN
Status.PlayWait = Status.PlayWait + AgeIncrease
END IF

ScheduleEventsAfterDelay# = AgeIncrease
END FUNCTION

SUB UpdateAllLiveNotes (Channel, MIDIOperation, Volume)
DIM ActiveChannel
DIM ActiveCount
DIM ActiveNote
DIM Note

ActiveCount = ActiveNoteCount(Channel)

FOR ActiveNote = 1 TO ActiveCount
Note = ActiveNoteList(Channel, ActiveNote)
ActiveChannel = ActiveNote(Channel, Note).AdlibChannel
SELECT CASE MIDIOperation
CASE MOPOFF
DeallocateActiveNote Channel, ActiveNote
CASE MOPPAN
UpdateNotePan ActiveChannel, Channel
CASE MOPPITCH
OPLNoteOn ActiveChannel, GetHertz#(ActiveNote(Channel, Note).Note, ChannelSettings(Channel).Bend)
CASE MOPPRESSURE
ActiveNote(Channel, Note).Volume = Volume
UpdateNoteVolume ActiveChannel, Channel, ActiveNote(Channel, Note).Volume
CASE MOPVOLUME
UpdateNoteVolume ActiveChannel, Channel, ActiveNote(Channel, Note).Volume
END SELECT
NEXT ActiveNote
END SUB

SUB UpdateNotePan (ActiveChannel, Channel)
DIM OPL AS OPLStr

ChannelInstruments(ActiveChannel).Panning = ChannelSettings(Channel).Panning
OPLSetUp ActiveChannel, OPL
OUT OPL.Index, &HC0 + OPL.SubChannel
OUT OPL.DataV, Instruments(ChannelInstruments(ActiveChannel).Instrument, 5) - ChannelInstruments(ActiveChannel).Panning
END SUB

SUB UpdateNoteVolume (ActiveChannel, Channel, Volume)
IF Volume = &H0 THEN
OPLTouchReal ActiveChannel, &H0
ELSE
OPLTouchReal ActiveChannel, (LOG((Volume * ChannelSettings(Channel).Volume) / &H3F01) * &H8) + &H3F
END IF
END SUB

Do not read if you don't like attention seeking self-advertisements!

Did you read it anyway? Well, you can find all sorts of stuff I made using various programming languages over here:
https://github.com/peterswinkels