From 95e94ce028918327006e67da8dace646a41fb68e Mon Sep 17 00:00:00 2001 From: Dave Schmenk Date: Wed, 8 Nov 2017 20:35:53 -0800 Subject: [PATCH] Music sequencer as a module --- src/mockingboard/cvtmid.py | 48 +++++++++++--- src/mockingboard/makefile | 9 +-- src/mockingboard/mbtest.pla | 25 ++++---- src/mockingboard/seqplay.pla | 118 +++++++++++++++++------------------ src/vmsrc/cmd.pla | 50 ++++++++------- 5 files changed, 143 insertions(+), 107 deletions(-) diff --git a/src/mockingboard/cvtmid.py b/src/mockingboard/cvtmid.py index 19b5ad2..61b9b49 100755 --- a/src/mockingboard/cvtmid.py +++ b/src/mockingboard/cvtmid.py @@ -4,11 +4,31 @@ import sys #from mido import MidiFile import mido -mid = mido.MidiFile(sys.argv[1]) +optarg = 1 +timescale = 16.0 # Scale time to 16th of a second +extperchan = 9 # Default to standard MIDI channel 10 for extra percussion +if len(sys.argv) == 1: + print 'Usage:', sys.argv[0], '[-p extra_percussion_channel] [-t timescale] MIDI_file' + sys.exit(0) +# Parse optional arguments +while optarg < (len(sys.argv) - 1): + if sys.argv[optarg] == '-t': # Override tempo percentage + timescale = float(sys.argv[optarg + 1]) * 16.0 + optarg += 2 + if sys.argv[optarg] == '-p': # Add extra percussion channel + extperchan = int(sys.argv[optarg + 1]) - 1 + optarg += 2 +mid = mido.MidiFile(sys.argv[optarg]) +timeshift = timescale totaltime = 0 +eventtime = 0.0 for msg in mid: + eventtime += msg.time * timeshift + #print '; time = ', msg.time if msg.type == 'note_on' or msg.type == 'note_off': - deltatime = int(msg.time * 16 + 0.5) + #if eventtime > 0.0 and eventtime < 0.5: + # eventtime = 0.5 + deltatime = int(round(eventtime)) octave = int(msg.note / 12 - 1) onote = int(msg.note % 12) lrchan = int(msg.channel & 1) @@ -20,15 +40,29 @@ for msg in mid: if octave < 0: octave = 0 totaltime += deltatime - if msg.channel == 9: + if msg.channel == 9 or msg.channel == extperchan: # # Percussion # - if vol > 0 and deltatime > 0: - print '\t!BYTE\t${0:02X}, ${1:02X}, ${2:02X}'.format(deltatime, msg.note >> 3, 2) + if vol > 0: + print '\t!BYTE\t${0:02X}, ${1:02X}, ${2:02X}\t; Percussion {3:d} Chan {4:d} Dur {5:d}'.format(deltatime, msg.note ^ 0x40, (lrchan << 7) | vol, msg.note, msg.channel + 1, vol) + if extperchan == 9: # Play percussion on both channels if no extended percussion + print '\t!BYTE\t${0:02X}, ${1:02X}, ${2:02X}\t; Percussion {3:d} Chan {4:d} Dur {5:d}'.format(0, msg.note ^ 0x40, vol, msg.note, msg.channel + 1, vol) + eventtime = 0.0 else: # # Note # - print '\t!BYTE\t${0:02X}, ${1:02X}, ${2:02X}'.format(deltatime, 0x80 | (octave << 4) | onote, (lrchan << 7) | vol) -print '\t!BYTE\t$00, $00, $00' + print '\t!BYTE\t${0:02X}, ${1:02X}, ${2:02X}\t; Note {3:d} Chan {4:d} Vol {5:d}'.format(deltatime, 0x80 | (octave << 4) | onote, (lrchan << 7) | vol, msg.note, msg.channel + 1, vol) + eventtime = 0.0 + elif msg.type == 'set_tempo': + pass + #timeshift = msg.tempo / 500000.0 * timescale + #print '; timescale = ', timescale + elif msg.type == 'time_signature': + pass + elif msg.type == 'control_chage': + pass + elif msg.type == 'program_change': + pass +print '\t!BYTE\t${0:02X}, $00, $00'.format(int(eventtime + 0.5)) diff --git a/src/mockingboard/makefile b/src/mockingboard/makefile index cb56b5c..3124a29 100755 --- a/src/mockingboard/makefile +++ b/src/mockingboard/makefile @@ -2,7 +2,7 @@ AFLAGS = -o $@ MBTEST = mbtest.bin SPKRTEST = spkrtest.bin -SEQPLAY = seqplay.bin +SEQPLAY = seqplay.rel PLASM = ../plasm # # Image filetypes for Virtual ][ @@ -33,6 +33,7 @@ $(SPKRTEST): test.seq spkrtest.pla spkrvm.s $(PLASM) $(PLASM) -AO < spkrtest.pla > spkrtest.a acme -o $(SPKRTEST) spkrvm.s -$(SEQPLAY): seqplay.pla seqvm.s $(PLASM) - $(PLASM) -AO < seqplay.pla > seqplay.a - acme -o $(SEQPLAY) seqvm.s +$(SEQPLAY): seqplay.pla test.seq $(PLASM) + $(PLASM) -AOM < seqplay.pla > seqplay.a + acme --setpc 4094 -o $(SEQPLAY) seqplay.a + acme --setpc 1024 -o testseq.bin test.seq diff --git a/src/mockingboard/mbtest.pla b/src/mockingboard/mbtest.pla index 6916338..0d51a9e 100755 --- a/src/mockingboard/mbtest.pla +++ b/src/mockingboard/mbtest.pla @@ -398,12 +398,12 @@ def mbSequence(track) mbVIA1=>T1L = $F9C2 // 16 Ints/sec mbVIA1=>T1C = $F9C2 // 16 Ints/sec mbVIA1->IFR = $40 // Clear interrupt -// mbVIA1->IER = $C0 // Enable Timer1 interrupt + mbVIA1->IER = $C0 // Enable Timer1 interrupt //enableInts repeat //puts("seqTime = "); puti(seqTime); puts(" eventTime = "); puti(eventTime); putln while eventTime == seqTime - note = seqEvent->percnote + note = seqEvent->percnote if note & $80 // // Note event @@ -460,17 +460,18 @@ def mbSequence(track) // period = seqEvent->perchanvol if period - psgWrite(mbVIA1, MIXER, $1C) // NG on C, Tone on B, A - psgWrite(mbVIA1, CENVAMP, $10) - psgWrite(mbVIA1, NGFREQ, note) - psgWrite(mbVIA1, ENVPERIOD+1, period) - psgWrite(mbVIA1, ENVSHAPE, $00) // Single decay - if mbVIA2 + if (period & $80) + psgWrite(mbVIA1, MIXER, $1C) // NG on C, Tone on B, A + psgWrite(mbVIA1, CENVAMP, $10) + psgWrite(mbVIA1, ENVSHAPE, (note >> 4) & $04) + psgWrite(mbVIA1, NGFREQ, (note >> 1) & $1F) + psgWrite(mbVIA1, ENVPERIOD+1, period & $7F) + elsif mbVIA2 psgWrite(mbVIA2, MIXER, $1C) // NG on C, Tone on B, A psgWrite(mbVIA2, CENVAMP, $10) - psgWrite(mbVIA2, NGFREQ, note) + psgWrite(mbVIA2, ENVSHAPE, (note >> 4) & $04) + psgWrite(mbVIA2, NGFREQ, (note >> 1) & $1F) psgWrite(mbVIA2, ENVPERIOD+1, period) - psgWrite(mbVIA2, ENVSHAPE, $00) // Single decay fin else eventTime = -1 // Exit out @@ -571,7 +572,9 @@ def mbSequence(track) end heap = *freemem if mbSearch - mbSequence(@toneTrack) + repeat + mbSequence(@toneTrack) + until ^$C000 > 127 ^$C010 fin done diff --git a/src/mockingboard/seqplay.pla b/src/mockingboard/seqplay.pla index 4f7fd48..c192206 100755 --- a/src/mockingboard/seqplay.pla +++ b/src/mockingboard/seqplay.pla @@ -1,3 +1,6 @@ +include "../inc/cmdsys.plh" +include "../inc/fileio.plh" +include "../inc/args.plh" // // Usage is documented following the source in this file... // @@ -88,14 +91,10 @@ word[5] arpeggioDuration = DUR16TH, DUR16TH, DUR16TH/2, DUR16TH/3, DUR16TH/4 // // These are utility sequences/routines needed to test the music sequencer code. // -asm toneTrack -include "ultima3.seq" -end -asm putc(ch)#0 - LDA ESTKL,X - INX - ORA #$80 - JMP $FDED +word arg +word ref +asm defs + !SOURCE "../vmsrc/plvmzp.inc" end /////////////////////////////////////////////////////////////////////////////// // @@ -477,17 +476,18 @@ def mbSequence(yield, func)#0 // period = seqEvent->perchanvol if period - psgWrite(mbVIA1, MIXER, $1C) // NG on C, Tone on B, A - psgWrite(mbVIA1, CENVAMP, $10) - psgWrite(mbVIA1, NGFREQ, note) - psgWrite(mbVIA1, ENVPERIOD+1, period) - psgWrite(mbVIA1, ENVSHAPE, $00) // Single decay - if mbVIA2 + if (period & $80) + psgWrite(mbVIA1, MIXER, $1C) // NG on C, Tone on B, A + psgWrite(mbVIA1, CENVAMP, $10) + psgWrite(mbVIA1, ENVSHAPE, (note >> 4) & $04) + psgWrite(mbVIA1, NGFREQ, (note >> 1) & $1F) + psgWrite(mbVIA1, ENVPERIOD+1, period & $7F) + elsif mbVIA2 psgWrite(mbVIA2, MIXER, $1C) // NG on C, Tone on B, A psgWrite(mbVIA2, CENVAMP, $10) - psgWrite(mbVIA2, NGFREQ, note) + psgWrite(mbVIA2, ENVSHAPE, (note >> 4) & $04) + psgWrite(mbVIA2, NGFREQ, (note >> 1) & $1F) psgWrite(mbVIA2, ENVPERIOD+1, period) - psgWrite(mbVIA2, ENVSHAPE, $00) // Single decay fin else if seqRepeat @@ -662,7 +662,7 @@ def spkrSequence(yield, func)#0 // Percussion event // if seqEvent->perchanvol - spkrPWM($D000, 0, 64) // Play some random sample as percussion + //spkrPWM($D000, 0, 64) // Play some random sample as percussion else if seqRepeat musicPlay(seqTrack, TRUE) @@ -774,7 +774,7 @@ end // // Get a keystroke and convert it to upper case // -export def getUpperKey#1 +export def getKey#1 byte key while ^$C000 < 128 @@ -782,58 +782,31 @@ export def getUpperKey#1 loop key = ^$C000 & $7F ^$C010 - if key >= 'a' and key <= 'z' - key = key - $20 - fin return key end /////////////////////////////////////////////////////////////////////////////// // -// More utility routines to test the getUpperKey routine -// -def putln#0 - putc($0D) -end -def puts(str)#0 - byte i - - for i = 1 to ^str - putc(^(str+i)) - next -end +// More utility routines to test the getKey routine // // Sample background process // def backgroundProc#0 ^$0400++ end -// -// Test functionality -// -def test#0 - byte key - puts("Press to exit:") - while TRUE - key = getUpperKey - when key - is $0D - return - is 'P' - musicPlay(@toneTrack, TRUE) - break - is 'S' - musicStop - break - otherwise - putc(key) - wend - loop -end - -musicPlay(@toneTrack, TRUE) -test -musicStop +arg = argNext(argFirst) +if ^arg + ref = open(arg, sysbuf) + if ref + read(ref, heapmark(), heapavail()) + close(ref) + musicPlay(heapmark(), TRUE) + getKey + musicStop + else + puts("File not found.\n") + fin +fin done //////////////////////////////////////////////////////////////////////////////// @@ -844,14 +817,20 @@ musicPlay(trackPtr, trackRepeat) Params: Pointer to a track sequence created from the cvtmidi.py tool Repeat flag - TRUE or FALSE. + The first time its is called, it will try and search for a MockingBoard. + However, it is noted that this can cause problems if a Z-80 card is installed. + The scanning routine might cause a hang if it encounters a Z-80 card before + it finds a MockingBoard. In order to make this robust, it might be best to + prompt the user to search for the MockingBoard, enter the actual MockingBoard + slot, or skip the MockingBoard and use the internal speaker. musicStop() Stop playing a track sequence in the getUpperKey routine The getUpperKey routine will call a dummy sequence routine that will keep the correct timing for any background processing -getUpperKey() - Wait for a keypress and return the upper case character +getKey() + Wait for a keypress and return the character While waiting for the keypress, the track sequence will be played though either the MockingBoard (if present) or the internal speaker. Optionally, a background function can be called periodically based on the sequencer @@ -872,3 +851,20 @@ spkrPWM(samples, speed, len) Pointer to 8 bit pulse width samples Speed to play through samples Length of sample + +The main routines for sequencing music are: + +mbSequence(yield, func) +spkrSequence(yield, func) +noSequence(yield, func) + + All three try and provide more functionality than would be present in + previous music sequencers. The MockingBoard sequencer will attempt to play up + to 9 tones per sound generator (18 if a MockingBoard II is found). Up to + four notes will be played simultaneously on the internal speaker. In order + to play more notes than the hardware normally supports, a technique using + arpeggio (playing multiple notes in a quick sequence rather than concurrently) + pulls off this feat. The sequencers will immediately return if a keypress is + detected. Finally, during the sequencing, a background function can be periodically + called every 'yield' time which has a resolution of a 16th of a second. Pass + in zero for 'yield' and/or 'func' to disable any background calls. diff --git a/src/vmsrc/cmd.pla b/src/vmsrc/cmd.pla index efbc46a..61d1ff4 100755 --- a/src/vmsrc/cmd.pla +++ b/src/vmsrc/cmd.pla @@ -56,6 +56,7 @@ byte hpmarkstr = "HEAPMARK" byte hpalignstr = "HEAPALLOCALIGN" byte hpallocstr = "HEAPALLOC" byte hprelstr = "HEAPRELEASE" +byte hpavlstr = "HEAPAVAIL" byte memsetstr = "MEMSET" byte memcpystr = "MEMCPY" byte uisgtstr = "ISUGT" @@ -68,30 +69,31 @@ byte modadrstr = "MODADDR" byte argstr = "ARGS" byte autorun = "AUTORUN" byte prefix[] // overlay with exported symbols table -word exports = @sysstr, @syscall -word = @callstr, @call -word = @putcstr, @cout -word = @putlnstr, @crout -word = @putsstr, @prstr -word = @getcstr, @cin -word = @getsstr, @rdstr -word = @hpmarkstr, @markheap -word = @hpallocstr,@allocheap -word = @hpalignstr,@allocalignheap -word = @hprelstr, @releaseheap -word = @memsetstr, @memset -word = @memcpystr, @memcpy -word = @uisgtstr, @uword_isgt -word = @uisgestr, @uword_isge -word = @uisltstr, @uword_islt -word = @uislestr, @uword_isle -word = @loadstr, @loadmod -word = @execstr, @execmod -word = @modadrstr, @lookupstrmod -word = @machidstr, MACHID -word = @argstr, @cmdln -word = 0 -word syslibsym = @exports +word exports = @sysstr, @syscall +word = @callstr, @call +word = @putcstr, @cout +word = @putlnstr, @crout +word = @putsstr, @prstr +word = @getcstr, @cin +word = @getsstr, @rdstr +word = @hpmarkstr, @markheap +word = @hpallocstr,@allocheap +word = @hpalignstr,@allocalignheap +word = @hprelstr, @releaseheap +word = @hpavlstr, @availheap +word = @memsetstr, @memset +word = @memcpystr, @memcpy +word = @uisgtstr, @uword_isgt +word = @uisgestr, @uword_isge +word = @uisltstr, @uword_islt +word = @uislestr, @uword_isle +word = @loadstr, @loadmod +word = @execstr, @execmod +word = @modadrstr, @lookupstrmod +word = @machidstr, MACHID +word = @argstr, @cmdln +word = 0 +word syslibsym = @exports // // Utility functions //