1
0
mirror of https://github.com/dschmenk/PLASMA.git synced 2024-10-18 07:24:16 +00:00

Music sequencer as a module

This commit is contained in:
Dave Schmenk 2017-11-08 20:35:53 -08:00
parent 165f16d048
commit 95e94ce028
5 changed files with 143 additions and 107 deletions

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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 <RETURN> 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.

View File

@ -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
//