mirror of
https://github.com/iAmInActions/random-scripts.git
synced 2024-11-24 21:20:13 +00:00
f43fcc1013
A small set of scripts showing off midi playback on linux without a sound card.
1122 lines
57 KiB
Python
1122 lines
57 KiB
Python
#!/usr/bin/env python2
|
|
|
|
# THIS VERSION HAS BEEN MODIFIED BY mueller_minki TO OUTPUT TO A BASH FILE AND SUPPORT PARSING NEWER MIDI FORMATS.
|
|
# THE ORIGINAL FILE IS AVAILABLE ON GITHUB
|
|
|
|
# MIDI beeper (plays MIDI without sound hardware)
|
|
# Version 1.67, (c) 2007-2010,2015-2020 Silas S. Brown. License: GPL
|
|
|
|
# MIDI beeper is a Python 2 program to play MIDI by beeping
|
|
# through the computer's beeper instead of using proper
|
|
# sound circuits. It emulates chords/polyphony.
|
|
# It sounds awful, but it might be useful when no sound device
|
|
# is attached. It should work on any machine that has the
|
|
# "beep" Linux package, including NSLU2 network storage devices.
|
|
# (On NSLU2 do 'sudo modprobe isp4xx_beeper' before running)
|
|
|
|
# Can also play MIDI files using square-wave synthesis with aplay
|
|
# (e.g. on Raspberry Pi) - set aplay below if you want this instead.
|
|
# Set it to a volume level, e.g. aplay = 100
|
|
aplay = 0
|
|
|
|
# Can also convert MIDI files to RISC OS Maestro music files
|
|
# for playing (but not typesetting well) on 'vanilla' RISC OS.
|
|
# Set riscos_Maestro = 1 below if you want this.
|
|
riscos_Maestro = 0
|
|
# If running this *on* RISC OS (Python 2.3) you might find it useful to set:
|
|
# import sys ; sys.argv.append("$.!Boot.Loader.test/mid") # (or whatever)
|
|
|
|
# Can also convert MIDI files to BBC Micro programs
|
|
# (printed to standard output). Set bbc_micro below:
|
|
bbc_micro = 0 # or run with --bbc
|
|
acorn_electron = 0 # or run with --electron: Acorn Electron version (more limited)
|
|
bbc_binary = 0 # or run with --bbc-binary: make the above use direct memory access instead of DATA (packs more in but harder to save/edit)
|
|
bbc_ssd = 0 # or run with --bbc-ssd: writes an SSD image (for an emulator) instead of printing keystrokes to standard output (set environment DFS_TITLE to title the disk; disk will contain one BBC program for each MIDI file on the command line + bootloader)
|
|
bbc_sdl = 0 # or run with --bbc-sdl: makes the BBC Micro code compatible with R.T.Russell's BBC BASIC for SDL. Code still runs on the real BBC too, but is larger.
|
|
|
|
# HiBasic (Tube) support (~30k for programs) fully works
|
|
# Bas128 support (64k for programs) works but (a) bank-switching delays impact the timing of shorter notes and (b) bbc_binary option can cause "Wrap" errors during input. However bbc_binary and bbc_ssd options should pack data into a smaller space so normal BASIC can be used.
|
|
|
|
force_monophonic = 1 # set this to 1 to have only the top line (not normally necessary)
|
|
|
|
# History is in https://github.com/ssb22/midi-beeper.git
|
|
# and https://gitlab.com/ssb22/midi-beeper.git
|
|
# and https://bitbucket.org/ssb22/midi-beeper.git
|
|
# and https://gitlab.developers.cam.ac.uk/ssb22/midi-beeper
|
|
# but some early versions are missing from these repositories
|
|
|
|
# ----------------------------------------------------
|
|
|
|
import os,sys
|
|
def delArg(a):
|
|
found = a in sys.argv
|
|
if found: sys.argv.remove(a)
|
|
return found
|
|
if delArg('--bbc'): bbc_micro = 1
|
|
if delArg('--electron'): acorn_electron = 1
|
|
if delArg('--bbc-binary'): bbc_binary=bbc_micro=1
|
|
if delArg('--bbc-ssd'): bbc_ssd=bbc_micro=1
|
|
if delArg('--bbc-sdl'): bbc_sdl=bbc_micro=1
|
|
assert not (bbc_sdl and (bbc_binary or bbc_ssd)), "bbc_sdl not compatible with bbc_binary or bbc_ssd"
|
|
|
|
if aplay:
|
|
rate = 41000 # can just about manage 3 or 4 channels on a Raspberry Pi if it isn't doing anything else
|
|
o = os.popen("aplay -q -t raw -c 1 -f U8 -r %d" % rate,"w")
|
|
def init(): pass
|
|
def chord(freqs,millisecs):
|
|
samples = millisecs * rate / 1000 ; halfPeriods = []
|
|
for f in freqs: halfPeriods.append(rate/2.0/f)
|
|
assert not 0 in halfPeriods
|
|
nextFlips = map(int,halfPeriods) ; counts = [1]*len(halfPeriods)
|
|
val = 0
|
|
if nextFlips: next=[aplay/len(halfPeriods)]*len(halfPeriods)
|
|
else: next = []
|
|
t = 0
|
|
while t < samples:
|
|
while t in nextFlips:
|
|
i = nextFlips.index(t)
|
|
val += next[i]
|
|
next[i] = -next[i]
|
|
counts[i] += 1
|
|
nextFlips[i] = int(counts[i]*halfPeriods[i]) # necessary especially at low rates as periods are rarely integers
|
|
o.write(chr(val)) ; t += 1
|
|
def add_midi_note_chord(noteNos,microsecs):
|
|
chord(map(to_freq,noteNos),microsecs / 1000)
|
|
elif bbc_micro or acorn_electron:
|
|
# This is a compact BBC Micro program to multiplex up to
|
|
# 9 channels of sound onto the BBC Micro's 3 channels.
|
|
# Basically uses ENVELOPEs to do the pitch multiplexing.
|
|
# BBC Micro's BASIC encouraged the use of @% through Z% by reserving memory for them (heap is typically small), so code can be quite obscure.
|
|
bbc_micro = ["FOR C%=16 TO 19:SO.C%,0,0,0:N.\n" # flush all sound buffers, just in case
|
|
"N%=0:" # N% = next available envelope number (1-16 if not using BPUT#, otherwise 1-4 but we don't want to redefine envelopes that are already associated with notes in the buffer)
|
|
"DIM c%(8)\n" # c% is the current value of each 'channel'; 252 i.e. 4*63 is used for silence. Data read tells the program what changes to make to this array for the next chord and for how long to sound it (see add_midi_note_chord below).
|
|
"FOR D%=0 TO 8:c%(D%)=252:N.\n" # all channels start with silence
|
|
# The next few lines can be abbreviated thus: "REP.:C%=0:REP.:READD%:c%(C%)=(D%A.63)*4:I%=(D%DIV64)+1:C%=C%+I%:U.I%=4:READD%:REP.:U.AD.-6>3:F.I%=0TO6S.3:S%=0:T%=0:IFc%(I%)=252:V%=0:EL.IFc%(I%+1)=252:V%=1:EL.S%=1:Q%=c%(I%+1)-c%(I%):IFc%(I%+2)=252:V%=2:EL.R%=c%(I%+2)-c%(I%+1):T%=1:V%=3" (237 keystrokes, out of a limit of 238). But Bas128 is still too slow, even with the read loop all on 1 line like this.
|
|
"REP.:C%=0\n" # C% is the write-index for our 'current chord' c%
|
|
"REP.:READ D%\n" # lowest 6 bits = semitone no. (which we multiply by 4 to get pitch number); highest 2 bits = array-pointer increment - 1 (so we can increment 1, 2 or 3 places; an "increment" of 4 means end of chord)
|
|
"c%(C%)=(D% AND 63)*4\n" # set pitch
|
|
"I%=(D% DIV 64)+1:C%=C%+I%:U.I%=4\n" # c% array now all set
|
|
"READ D%\n" # This will be the duration of the chord just specified
|
|
"REP.:U.ADVAL(-6)>3\n" # BBC Micro quirk: contrary to what the manual says (in at least some printings), the number of notes in each channel's "to be played" buffer before the program waits can be 5 not 4 (on at least some versions of the BBC). This, together with the current note, means we might need a total of 6*3=18 envelopes, and we have only 16 slots. Hence the ADVAL loop to avoid filling the buffer completely.
|
|
"FOR I%=0 TO 6 STEP 3\n" # handling our 'channels' as triples, up to 3 being arpeggiated into one BBC Micro channel; I% will be the index-start of each triple
|
|
"S%=0:" # will be set to 1 if the second section of the envelope is used
|
|
"T%=0\n" # will be set to 1 if the third section of the envelope is used
|
|
"IF c%(I%)=252:V%=0:" # no volume if entire channel is silent (see special case below)
|
|
"ELSE IF c%(I%+1)=252:V%=1:" # entire channel has just one note, so play it at "volume 1". TODO: if c%(I%) is high, consider 'wobbling' the pitch to mask the SN76489's tuning inaccuracy of high notes, e.g. by setting S%=1:Q%=1. This can be done inline (using the fact that BBC BASIC represents true as -1) using something like "S%=-(c%(I%)>150):Q%=S%:" here. Would need to check if 150 really is a good threshold, and, if it works, also modify the datBytes string in make_bbcMicro_DFS_image: beware line-length bytes etc; will probably have to stop using the 'abbreviated' version if these extra 2 assignments would make the line too long. Also check the bbc_sdl .replace of V%=1 below doesn't undo it (and consider turning it off if INKEY(-256) detects SDL-etc, as that environment has better tuning to begin with)
|
|
"ELSE S%=1:Q%=c%(I%+1)-c%(I%):" # channel has at least 2 notes, so set Q% to the first pitch difference, and set S% to enable 2nd section of envelope
|
|
"IF c%(I%+2)=252:V%=2:" # channel has exactly 2 notes, so play it at "volume 2"
|
|
"ELSE R%=c%(I%+2)-c%(I%+1):T%=1:V%=3\n" # channel has 3 notes, so play it at "volume 3", set R% to second pitch difference, and set T% to enable 3rd section of envelope
|
|
# (here ends what can be abbreviated as per the 'abbreviated' comment above)
|
|
"IF V%:" # The following operations are done only if volume is not 0. We special-case volume 0 so it doesn't use an envelope at all; this (along with the ADVAL loop above) seems to make things a little more robust, as short pauses between notes are frequent. A special-case of volume 0 is needed anyway in the Electron version below: the Electron uses a ULA with only 1 channel and 1 volume; the last 6 envelope parameters are ignored and setting them to 0 does NOT switch off the sound like it does on the BBC.
|
|
"V%=V%*24+55:" # 79, 103 or 127
|
|
"N%=N%+1:IF N%=17:N%=1" # next available envelope number
|
|
"\nIF V%:" # (still only if volume is not 0)
|
|
"ENV.N%," # setting envelope number N%
|
|
"3," # length of each step in centiseconds
|
|
"0," # first section should sound the 1st note
|
|
"Q%," # second section adds Q% to the pitch for each step
|
|
"R%," # third section adds R% to the pitch for each step
|
|
"1," # first section should have 1 step (for sounding the 1st note)
|
|
"S%," # second section should have either 0 steps (if not used) or 1 step (for sounding note + Q%)
|
|
"T%," # third section should have either 0 steps (if not used) or 1 step (for sounding note + Q% + R%)
|
|
"V%,0,0,-V%," # ADSR (attack, decay, sustain, release) change per step
|
|
"V%," # attack final volume
|
|
"V%" # decay final volume
|
|
":V%=N%\n" # for the SOUND command below
|
|
"SO.513+(I%DIV3)," # 512 = sync=2 i.e. 3 channels are to receive a note before it is to start; +1 because we're not using channel 0
|
|
"V%," # envelope number or 0
|
|
"c%(I%)," # first pitch of the arpeggio (or plain pitch if no arpeggio)
|
|
"D%\n" # duration
|
|
"N.:U.D%=0:END"]
|
|
if acorn_electron:
|
|
# Cut-down version of the above code for the Electron:
|
|
bbc_micro=["""SO.1,0,0,0
|
|
N%=0:DIM c%(2)
|
|
FOR D%=0 TO 2:c%(D%)=252:N.
|
|
REP.:C%=0
|
|
REP.:READ D%
|
|
c%(C%)=(D% AND 63)*4
|
|
I%=(D% DIV 64)+1:C%=C%+I%:U.I%=4
|
|
READ D%
|
|
REP.:U.ADVAL(-6)>3
|
|
S%=0:T%=0:P%=c%(0)
|
|
IF P%=252:V%=0:ELSE V%=1:IF c%(1)<>252:S%=1:Q%=c%(1)-P%:IF c%(2)<>252:R%=c%(2)-c%(1):T%=1
|
|
IF V%:N%=N%+1:IF N%=17:N%=1
|
|
IF V%:ENV.N%,3,0,Q%,R%,1,S%,T%,126,0,0,-126,126,126:V%=N%
|
|
SO.1,V%,P%,D%
|
|
U.D%=0:END"""] # the 126,etc is there so that if this program is accidentally run on the BBC Micro instead of the Electron it'll at least sound something
|
|
current_array = [63]*3
|
|
else: current_array = [63]*9
|
|
if bbc_sdl: bbc_micro[0]="COLOUR 128:COLOUR 7:CLS\n"+bbc_micro[0] # BBC SDL defaults to white background: be easier on the eyes by having dark mode like the original BBC
|
|
if bbc_ssd:
|
|
bbc_micro = [] # we'll put tokenised program in later
|
|
bbc_binary = 1
|
|
bbc_files = []
|
|
elif bbc_binary: # don't use AUTO; change to read RAM
|
|
lines = ("E%=TOP:" + bbc_micro[0]).split("\n") ; bbc_micro = []
|
|
for i in range(len(lines)):
|
|
bbc_micro.append("%d%s" % (i+1,lines[i]))
|
|
bbc_micro=[
|
|
"IF(PA. A.&FF00)>&E00:PA.=&E00:*ROM" # reclaim space from Model B DFS if applicable (may or may not be needed depending on which DFS is in use and how much space it takes, which we won't know at code-generation time, so test at runtime if we're above E00 and not in second-processor addresses. If using Acorn's DFS on Model B, may be able to reduce PAGE from &1900 to &1800 if not using *BUILD, to &1700 if no other ROMs will borrow space from DFS, to &1300 if using OPEN on max 1 file, or to &1100 if not using OPEN or SPOOL/EXEC, but Watford DFS and others will be different so it's safer to just turn it off.)
|
|
"NEW"]+bbc_micro
|
|
# Could see the Mode 0 memory map with: MO.0:V.23;12;0;0;0;0;28,0,12,63,0
|
|
# For larger MIDIs, can see sound queues etc (but not BASIC stack) via: MO.6:V.23;12;0;0;0;0;23;0;0;0;0;0:RUN
|
|
# (can also try MO.4)
|
|
bbc_micro = ["\n".join(bbc_micro).replace("READ D%","D%=?E%:E%=E%+1")]
|
|
keystroke_limit = 238
|
|
if bbc_sdl: keystroke_limit -= 5 # assuming up to 3 keystrokes for the backward-compatibility line number (can be up to 5, but if it gets as high as 4 then we're looking at a 230k+ program which is not going to fit in any version of the BBC Micro anyway so we might as well disregard the BBC Micro's keystroke buffer limit), + 2 keystrokes for "D." to "DATA"
|
|
def add_midi_note_chord(noteNos,microsecs):
|
|
duration = int((microsecs*20+500000)/1000000)
|
|
while duration > 254: # unlikely but we should cover this
|
|
add_midi_note_chord(noteNos,254*1000000/20)
|
|
duration -= 254
|
|
if not duration: return
|
|
def f(n): # convert to SOUND/4 and bound the octaves
|
|
n -= 47 # MIDI note 69 (A4) is pitch 88 i.e. 4*22
|
|
while n<0: n+=12 # TODO: unless we want to make a bass line using SOUND 0,-V,3,D with SOUND 1,E,P+188,D (won't work on acorn_electron; envelope E will have to set its volume params to 0, or stick with single note), or SO.0,-V,2,D for approx. 1 tone below note 0 (which doesn't tie up channel 1). Would need to know total number of notes there'll be before deciding if can do this. Anyway such low notes are rather indistinct on BBC hardware.
|
|
while n>=63: n-=12 # we're using 63 for rest
|
|
return n
|
|
if acorn_electron: noteNos = noteNos[-3:]
|
|
noteNos = map(f,noteNos[-9:])
|
|
while len(noteNos)<3: noteNos.append(63)
|
|
if bbc_sdl and (len(noteNos)>6 or (acorn_electron and len(noteNos)>2)) and not '1.13+' in bbc_micro[0]:
|
|
bbc_micro[0]="REM As there are chords with three\nREM notes per channel, you will need\nREM BBC SDL 1.13+ or 'real' BBCBASIC\nREM for the ENVELOPEs to sound right.\nREM\n"+bbc_micro[0] # see bbcsdl bug #3
|
|
if not acorn_electron:
|
|
# Divide the notes evenly among BBC channels,
|
|
# and if need arpeggiation, prefer it in the bass.
|
|
for a,b in [(9,0),(7,0),(9,3),(8,3),(9,6),(9,6)]:
|
|
if len(noteNos)<a: noteNos.insert(len(noteNos)-b,63)
|
|
# Check range of arpeggiation pitch increments, adjust
|
|
# octave as needed (too high shouldn't happen in
|
|
# sensible music, but double-bass too low is possible)
|
|
for i in range(0,len(noteNos),3):
|
|
for j in range(i+1,i+3):
|
|
if noteNos[j]==63: break
|
|
while noteNos[j]>noteNos[j-1]+31: noteNos[j]-=12
|
|
while noteNos[j]<noteNos[j-1]-32: noteNos[j]+=12
|
|
# Now calculate the DATA numbers:
|
|
o = [] ; curSkip = 0
|
|
for i in range(len(current_array)):
|
|
if noteNos[i]==current_array[i] and o and curSkip<2:
|
|
curSkip += 1 ; continue
|
|
if curSkip: o[-1] += curSkip*64
|
|
curSkip = 0 ; current_array[i] = noteNos[i]
|
|
if noteNos[i:] == current_array[i:]:
|
|
o.append(noteNos[i]+3*64) ; break # last change
|
|
else: o.append(noteNos[i])
|
|
o.append(duration)
|
|
if bbc_binary:
|
|
for i in o: bbc_micro.append(int(i))
|
|
else: # self-contained typeable BBC BASIC, assuming AUTO
|
|
o = ",".join(map(lambda x:("%d"%x), o))
|
|
if len(bbc_micro)>1 and len(bbc_micro[-1])+len(o)+1 <= keystroke_limit: bbc_micro[-1] += ','+o
|
|
else: bbc_micro.append("D."+o)
|
|
def init():
|
|
global dedup_microsec_quantise
|
|
dedup_microsec_quantise = 50000 # 1000000/20
|
|
elif riscos_Maestro:
|
|
allowed_BPMs = [40, 50, 60, 65, 70, 80, 90, 100, 115, 130, 145, 160, 175, 190, 210]
|
|
default_bpm = max(allowed_BPMs) # theoretically gives the most accuracy
|
|
hemi_microsecs = 3750000/default_bpm # for now (ms/hemi = beat/hemi / (b/min * min/microsec) = 1/16 / (bpm / 60000000) = 60000000/16/bpm)
|
|
def add_midi_note_chord(noteNos,microsecs):
|
|
noteNos.reverse()
|
|
global current_time
|
|
for n in current_chord[:]:
|
|
if n.noteNo in noteNos: noteNos.remove(n.noteNo) # just extend the currently-playing note
|
|
else: # stop that note:
|
|
assert n.noteNo not in noteNos # shouldn't have any duplicates in that list
|
|
n.end(current_time)
|
|
n.quantiseTo(hemi_microsecs)
|
|
foundC = False
|
|
for i in [0,4,6,2,1,3,5,7]: # allocate channels in that order so 2-stave (0-3,4-6/7), 3-stave (0,1-4,5-6/7) and 4-stave (0-1,2-3,4-5,6/7) views work vaguely sensibly
|
|
c = riscos_channels[i]
|
|
if not c or c[-1].endTime <= n.startTime:
|
|
c.append(n) ; foundC = True ; break
|
|
if not foundC: sys.stderr.write("Insufficient RISC OS channels: dropping note %d\n" % n.noteNo)
|
|
current_chord.remove(n)
|
|
for noteNo in noteNos: current_chord.append(MidiNote(noteNo,current_time)) # newly-started notes
|
|
current_time += microsecs
|
|
def maestroData():
|
|
queues = []
|
|
for c in riscos_channels:
|
|
timeCountFrom = 0 ; chan = []
|
|
barHemisLeft = 64 # assumes 4/4 with no anacrusis
|
|
for n in c:
|
|
nn,barHemisLeft = n.note(hemi_microsecs,timeCountFrom,barHemisLeft)
|
|
chan.append(nn)
|
|
timeCountFrom = n.endTime
|
|
queues.append(''.join(chan))
|
|
staves = 0
|
|
for q in queues:
|
|
if q and staves<4: staves += 1 # helps with more accurate playing if each part has its own stave (pity there's a maximum of 4)
|
|
if not staves: staves = 1
|
|
return Maestro_header+setBPM_block(default_bpm)+setVolumes_block()+musicData_block(queues)+setStaves_block(staves)+setInstruments_block()
|
|
def init():
|
|
global current_chord,current_time,riscos_channels
|
|
current_chord = [] ; current_time = 0 ; riscos_channels = [[],[],[],[],[],[],[],[]]
|
|
else: # beep
|
|
# NSLU2 hack:
|
|
try: event=open("/proc/bus/input/devices").read()
|
|
except IOError: event=""
|
|
if "ixp4xx beeper" in event:
|
|
h=event[event.find("Handlers=",event.index("ixp4xx beeper")):]
|
|
event="-e /dev/input/"+(h[:h.find("\n")].split()[-1])
|
|
os.system("sync") # just in case (beep has been known to crash NSLU2 Debian Etch in rare conditions)
|
|
else: event=""
|
|
|
|
def init():
|
|
global cumulative_params
|
|
cumulative_params = []
|
|
min_pulseLength, max_pulseLength = 10,20 # milliseconds
|
|
repetitions_to_aim_for = 1 # arpeggiating each chord only once will do if it's brief
|
|
def chord(freqList,millisecs):
|
|
if not millisecs: return ""
|
|
elif not freqList: return " -D %d" % (millisecs,) # rest
|
|
elif len(freqList)==1: return " -n -f %d -l %d" % (freqList[0],millisecs) # one note
|
|
else:
|
|
pulseLength = max(min(millisecs/len(freqList)/repetitions_to_aim_for,max_pulseLength),min_pulseLength)
|
|
return (" -D 0".join([chord([f],pulseLength) for f in freqList]))*max(1,millisecs/pulseLength/len(freqList)) # (max with 1 means at least 1 repetition - prefer a slight slow-down to missing a chord out)
|
|
# (the above -D 0 is necessary because Debian 5's beep adds a default delay otherwise)
|
|
|
|
command_line_len = 80000 # reduce this if you get "argument list too long" (NB the real limit is slightly more than this value)
|
|
|
|
def runBeep(params):
|
|
while " -n" in params: # not entirely silence
|
|
params=params[params.find(" -n")+3:] # discard the initial "-n" and any delay before it
|
|
brkAt = params.find(" -n",command_line_len)
|
|
if brkAt>-1: thisP,params = params[:brkAt],params[brkAt:]
|
|
else: thisP,params = params,""
|
|
os.system("echo 'beep "+event+" "+thisP+"' >> \""+midiFile+".sh\"")
|
|
os.system("chmod +x \""+midiFile+".sh\"")
|
|
|
|
def add_midi_note_chord(noteNos,microsecs):
|
|
millisecs = microsecs / 1000
|
|
if noteNos and cumulative_params and not "-D" in cumulative_params[-1].split()[-2:]: cumulative_params.append("-D 0") # necessary because Debian 5's beep adds a default delay otherwise
|
|
cumulative_params.append(chord(map(to_freq,noteNos),millisecs))
|
|
|
|
def make_bbcMicro_DFS_image(datFiles):
|
|
opt4 = 3 # exec !BOOT
|
|
disk_title = os.environ.get("DFS_TITLE","")
|
|
disk_title += "\0"*max(0,12-len(disk_title))
|
|
# catalogue is 31 items but we'll do !BOOT separately
|
|
catNames,catInfo,catNo = ["\0"*8]*31,["\0"*8]*31,0
|
|
data = "*BASIC\r"
|
|
assert all(len(f)<=7 and re.match('^[A-Za-z0-9]*$',f) for f,_ in datFiles), "please keep DFS filenames to 7-char alphanumeric "+repr([f for f,_ in datFiles])
|
|
if "BOOT_COPYRIGHT" in os.environ: data += "\rREM "+os.environ["BOOT_COPYRIGHT"]+"\r\r" # TODO: document this?
|
|
if len(datFiles)==1: data += ('LOAD "%s"\rLIST\rRUN\r' % datFiles[0][0])
|
|
else: data += "*CAT\r"+"REP.:U.AD.-6=15:".join(('CH."%s"\r' % f) for f,_ in datFiles)
|
|
nextSector = 2+int((len(data)+255)/256)
|
|
catNames[0]="!BOOT $"
|
|
catInfo[0]="".join([
|
|
"\0"*4, # !BOOT lsb-msb Load, lsb-msb Exec
|
|
chr(len(data)&0xFF)+chr(len(data)>>8), # !BOOT len
|
|
"\0", # no >64k options or high start-sector bits
|
|
"\2", # starts on sector 2
|
|
])
|
|
data += "\0"*((256-(len(data)%256))&0xFF) # pad !BOOT
|
|
for fname,datBytes in datFiles:
|
|
catNo += 1; assert catNo<31,"Catalogue full"
|
|
lomem_set = "\xd2=\xb8P+"+str(len(datBytes)-1)
|
|
assert not acorn_electron, "make_bbcMicro_DFS_image is hard-coded to use the BBC Micro reader, not Electron"
|
|
datBytes="\r\x00\x00"+chr(len(lomem_set)+4)+lomem_set+"\r\x00\n@E%=\xb8P:\xe3C%=16\xb819:\xd4C%,0,0,0:\xed:N%=0:\xdec%(8):\xe3D%=0\xb88:c%(D%)=252:\xed\r\x00\x14\xe5\xf5:C%=0:\xf5:D%=?E%:E%=E%+1:c%(C%)=(D%\x8063)*4:I%=(D%\x8164)+1:C%=C%+I%:\xfdI%=4:D%=?E%:E%=E%+1:\xf5:\xfd\x96-6>3:\xe3I%=0\xb86\x883:S%=0:T%=0:\xe7c%(I%)=252:V%=0:\x8b\xe7c%(I%+1)=252:V%=1:\x8bS%=1:Q%=c%(I%+1)-c%(I%):\xe7c%(I%+2)=252:V%=2:\x8bR%=c%(I%+2)-c%(I%+1):T%=1:V%=3\r\x00\x1e'\xe7V%:V%=V%*24+55:N%=N%+1:\xe7N%=17:N%=1\r\x00(4\xe7V%:\xe2N%,3,0,Q%,R%,1,S%,T%,V%,0,0,-V%,V%,V%:V%=N%\r\x002$\xd4513+(I%\x813),V%,c%(I%),D%:\xed:\xfdD%=0\r\xff"+datBytes # This essentially tokenises the program with the 'abbreviated' version of the loop (and indirection instead of READ). If changing it, the embedded binary line lengths will also need updating.
|
|
catNames[catNo]=fname+' '*(7-len(fname))+'$'
|
|
catInfo[catNo] = "".join([
|
|
"\0"*4, # lsb-msb Load, lsb-msb Exec (apparently not used for BASIC programs)
|
|
chr(len(datBytes)&0xFF),chr(len(datBytes)>>8),
|
|
chr((2+int(len(data)/256))>>8), # should be max 2 bits (protected by whole-disk-size assert below)
|
|
chr((2+int(len(data)/256))&0xFF), # start sector
|
|
])
|
|
data += datBytes
|
|
data += "\0"*((256-(len(data)%256))&0xFF) # pad
|
|
sectors = 2+len(data)/256
|
|
if sectors<400: sectors=400 # 40 tracks
|
|
elif sectors<800: sectors=800 # 80 tracks
|
|
else: assert 0, "Disk image too full" # (can in theory go to 1023 sectors, but no real hardware would support it)
|
|
return "".join([
|
|
disk_title[:8],
|
|
"".join(catNames),
|
|
disk_title[8:12],
|
|
"\1", # disk cycle (BCD, incremented each time catalogue is written)
|
|
chr((1+catNo)*8),
|
|
chr((sectors>>8)+16*opt4),
|
|
chr(sectors&0xFF),
|
|
"".join(catInfo),
|
|
data.rstrip("\0")])
|
|
|
|
dedup_microsec_quantise = 0 # for handling 'rolls' etc (currently used by bbc_micro; TODO: default 'beep' cmd also?)
|
|
def dedup_midi_note_chord(noteNos,microsecs):
|
|
if force_monophonic and noteNos: noteNos=[max(noteNos)]
|
|
else: noteNos.sort()
|
|
global dedup_chord,dedup_microsec
|
|
if dedup_microsec_quantise and not microsecs==None:
|
|
global dedup_microsec_error
|
|
microsecs += dedup_microsec_error ; oldM = microsecs
|
|
microsecs = int((microsecs+dedup_microsec_quantise/2)/dedup_microsec_quantise) * dedup_microsec_quantise
|
|
dedup_microsec_error = oldM - microsecs
|
|
if noteNos == dedup_chord and microsecs:
|
|
# it's just an extention of the existing one
|
|
dedup_microsec += microsecs
|
|
return
|
|
elif microsecs==0: return # too short, quantise out (and don't have to change dedup_chord because the next one might immediately revert to it if this is a 'roll' effect)
|
|
else: # microsecs==None (flush) or a note change
|
|
add_midi_note_chord(dedup_chord,dedup_microsec)
|
|
dedup_chord,dedup_microsec = noteNos,microsecs
|
|
|
|
A=440 # you can change this if you want to re-pitch
|
|
midi_note_to_freq = []
|
|
import math,re
|
|
for i in range(128): midi_note_to_freq.append((A/32.0)*math.pow(2,(len(midi_note_to_freq)-9)/12.0))
|
|
assert midi_note_to_freq[69] == A # (comment this out if using floating-point tuning because it might fail due to rounding)
|
|
def to_freq(n):
|
|
if n==int(n): return midi_note_to_freq[int(n)]
|
|
else: return (A/32.0)*math.pow(2,(n-9)/12.0)
|
|
|
|
# Begin RISC OS Maestro code
|
|
|
|
Maestro_header = 'Maestro\x0a\x02'
|
|
|
|
def BASIC_int(n): return chr(0x40)+chr(n>>24)+chr((n>>16)&0xFF)+chr((n>>8)&0xFF)+chr(n&0xFF)
|
|
|
|
class MidiNote:
|
|
def __init__(self,noteNo,startTime):
|
|
self.noteNo,self.startTime = noteNo,startTime
|
|
def end(self,time): self.endTime = time
|
|
def timeLen(self): return self.endTime - self.startTime
|
|
def quantiseTo(self,resolution):
|
|
origLen = self.endTime - self.startTime
|
|
self.startTime = int(self.startTime/resolution) * resolution
|
|
self.endTime = int(self.endTime/resolution) * resolution
|
|
if self.endTime == self.startTime: self.endTime += resolution # try to avoid quantising it out completely
|
|
def lenAndDotsList(self,hemiLen,barHemisLeft): # could be several notes tied
|
|
hemisLeft = int(self.timeLen()/hemiLen)
|
|
lTry,lH = 1,64 ; ret = []
|
|
while hemisLeft:
|
|
while hemisLeft >= lH and lH <= barHemisLeft:
|
|
lHT = lH ; dots = 0 ; dotVal = lH/2
|
|
while dotVal and lHT+dotVal <= hemisLeft and dots<3 and lHT+dotVal <= barHemisLeft:
|
|
dots += 1 ; lHT += dotVal ; dotVal /=2
|
|
hemisLeft -= lHT ; barHemisLeft -= lHT ; ret.append((lTry,dots))
|
|
if not barHemisLeft:
|
|
barHemisLeft = 64 # TODO: assumes 4/4
|
|
lTry,lH =1,64 ; continue
|
|
lTry <<= 1 ; lH >>= 1
|
|
assert not (lH==0 and hemisLeft)
|
|
return ret,barHemisLeft
|
|
def note(self,hemiLen,timeCountFrom,barHemisLeft):
|
|
assert timeCountFrom <= self.startTime
|
|
ret = []
|
|
if timeCountFrom < self.startTime:
|
|
oet,self.endTime,self.startTime = self.endTime,self.startTime,timeCountFrom
|
|
ldl,barHemisLeft = self.lenAndDotsList(hemiLen,barHemisLeft)
|
|
for length,dots in ldl: ret.append(note(None,length,dots))
|
|
self.startTime,self.endTime = self.endTime,oet
|
|
ldl,barHemisLeft = self.lenAndDotsList(hemiLen,barHemisLeft)
|
|
if not ldl: return "".join(ret),barHemisLeft # note must have been completely quantised out
|
|
for length,dots in ldl[:-1]: ret.append(note(self.noteNo,length,dots,tieWithNext=True))
|
|
ret.append(note(self.noteNo,ldl[-1][0],ldl[-1][1]))
|
|
return "".join(ret),barHemisLeft
|
|
|
|
def note(midiNote,length=4,dots=0,clef="treble",stemDown=None,tieWithNext=False,beamWithNext=False):
|
|
if not midiNote==None: # midiNote=None for a rest
|
|
def f(mn):
|
|
m = mn % 12
|
|
if m >= 5: m += 1
|
|
return int(m/2) + 7*int(mn/12)
|
|
r = f({"treble":71,"bass":50}[clef]) # middle line
|
|
r -= f(midiNote) # -1 = 1 space above the mid-line
|
|
if stemDown==None: stemDown = (r>0)
|
|
r = 16-r
|
|
while r<1: r += 7 # force 8ve ranges
|
|
while r>31: r -= 7 # (assuming we can't change clef)
|
|
sharp = (f(midiNote) == f(midiNote-1))
|
|
r *= 8
|
|
if tieWithNext: r += 4
|
|
if beamWithNext: r += 2
|
|
if stemDown: r += 1
|
|
else: r = sharp = 0
|
|
r2 = 0
|
|
while length:
|
|
length = int(length/2) ; r2 += 1
|
|
assert r2 < 8, "length too short"
|
|
r2 *= 32
|
|
assert 0 <= dots <= 3 ; r2 += dots*8
|
|
if sharp: r2 += 2
|
|
else: r2 += 1 # natural (TODO: figure out if actually needed and omit if not)
|
|
return chr(r) + chr(r2)
|
|
|
|
def playLen(secondNoterestByte): # for gate-byte sync
|
|
if type(secondNoterestByte)==str:
|
|
secondNoterestByte = ord(secondNoterestByte)
|
|
numDots = (secondNoterestByte >> 3) & 3
|
|
secondNoterestByte = int(secondNoterestByte/32)
|
|
l = 8 # so can *=1.5 3 times and still have an integer
|
|
for i in range(secondNoterestByte,7): l *= 2
|
|
dotVal = l*0.5
|
|
for i in range(numDots):
|
|
l += dotVal ; dotVal /= 2
|
|
return l
|
|
|
|
def gatesBytes(notesRestQueues):
|
|
assert len(notesRestQueues) == 8
|
|
lenLeft = [0]*8 ; nrq = notesRestQueues[:] ; r = [] ; barLeft = 64*8
|
|
while any(nrq):
|
|
b = 0 ; toSub = 0
|
|
for i in range(8):
|
|
if nrq[i] and not lenLeft[i]:
|
|
b |= (1 << i)
|
|
lenLeft[i] = playLen(nrq[i][1])
|
|
assert lenLeft[i] > 0, lenLeft[i]
|
|
nrq[i] = nrq[i][2:]
|
|
if lenLeft[i] and (not toSub or lenLeft[i]<toSub): toSub=lenLeft[i]
|
|
if b: r.append(chr(b))
|
|
assert toSub > 0
|
|
for i in range(8):
|
|
if lenLeft[i]: lenLeft[i] -= toSub
|
|
assert 0 in lenLeft
|
|
barLeft -= toSub
|
|
assert barLeft >= 0
|
|
if not barLeft:
|
|
r.append(chr(0)+chr(32)) # barline (needed for reliable playing)
|
|
barLeft = 64*8
|
|
return "".join(r) # (gatesBytes can also include 0 followed by time signature codes etc)
|
|
|
|
# blocks in any order:
|
|
|
|
def musicData_block(notesRestQueues): # each queue is a string of 2-byte returns from note() above
|
|
assert len(notesRestQueues) <= 8
|
|
while len(notesRestQueues) < 8: notesRestQueues.append("")
|
|
gb = gatesBytes(notesRestQueues)
|
|
l = [gb]+notesRestQueues
|
|
r = []
|
|
for i in l: r.append(BASIC_int(len(i)))
|
|
return chr(1)+''.join(r)+''.join(l)
|
|
|
|
def setStaves_block(numStaves=1,numPercStaves=1):
|
|
assert 1 <= numStaves <= 4 and 0 <= numPercStaves <= 1
|
|
return chr(2)+chr(numStaves-1)+chr(numPercStaves-1)
|
|
|
|
def setInstruments_block(voiceNumberList=[1]*8): # voice 5 is probably the perceptual loudest, might be useful if using unpowered speakers on a RISC OS Raspberry Pi; voice 1 (default) sounds more gentle though (but still better include it or the volume block might not be interpreted)
|
|
assert len(voiceNumberList) == 8
|
|
r = []
|
|
for i in range(8): r.append(chr(i)+chr(voiceNumberList[i]))
|
|
return chr(3)+''.join(r)
|
|
|
|
def setVolumes_block(volumesList=[7]*8):
|
|
assert len(volumesList)==8
|
|
for x in volumesList: assert 0<=x<=7
|
|
return chr(4)+''.join(map(chr,volumesList))
|
|
|
|
def setPans_block(stereoPosList):
|
|
assert len(stereoPosList)==8
|
|
for x in stereoPosList: assert -3<=x<=3
|
|
return chr(5)+''.join(map(lambda x:chr(x+3),stereoPosList))
|
|
|
|
def setBPM_block(bpm): return chr(6)+chr(allowed_BPMs.index(bpm))
|
|
|
|
# End RISC OS Maestro code
|
|
|
|
# Some of the code below was taken from an old version of
|
|
# Python Midi Package by Max M,
|
|
# with much cutting-down and modifying
|
|
from types import StringType
|
|
from cStringIO import StringIO
|
|
from struct import pack, unpack
|
|
def getNibbles(byte): return (byte >> 4 & 0xF, byte & 0xF)
|
|
def setNibbles(hiNibble, loNibble):
|
|
return (hiNibble << 4) + loNibble
|
|
def readBew(value):
|
|
return unpack('>%s' % {1:'B', 2:'H', 4:'L'}[len(value)], value)[0]
|
|
def readVar(value):
|
|
sum = 0
|
|
for byte in unpack('%sB' % len(value), value):
|
|
sum = (sum << 7) + (byte & 0x7F)
|
|
if not 0x80 & byte: break
|
|
return sum
|
|
def varLen(value):
|
|
if value <= 127:
|
|
return 1
|
|
elif value <= 16383:
|
|
return 2
|
|
elif value <= 2097151:
|
|
return 3
|
|
else:
|
|
return 4
|
|
def to_n_bits(value, length=1, nbits=7):
|
|
bytes = [(value >> (i*nbits)) & 0x7F for i in range(length)]
|
|
bytes.reverse()
|
|
return bytes
|
|
def toBytes(value):
|
|
return unpack('%sB' % len(value), value)
|
|
def fromBytes(value):
|
|
if not value:
|
|
return ''
|
|
return pack('%sB' % len(value), *value)
|
|
NOTE_OFF = 0x80
|
|
NOTE_ON = 0x90
|
|
AFTERTOUCH = 0xA0
|
|
CONTINUOUS_CONTROLLER = 0xB0
|
|
PATCH_CHANGE = 0xC0
|
|
CHANNEL_PRESSURE = 0xD0
|
|
PITCH_BEND = 0xE0
|
|
BANK_SELECT = 0x00
|
|
MODULATION_WHEEL = 0x01
|
|
BREATH_CONTROLLER = 0x02
|
|
FOOT_CONTROLLER = 0x04
|
|
PORTAMENTO_TIME = 0x05
|
|
DATA_ENTRY = 0x06
|
|
CHANNEL_VOLUME = 0x07
|
|
BALANCE = 0x08
|
|
PAN = 0x0A
|
|
EXPRESSION_CONTROLLER = 0x0B
|
|
EFFECT_CONTROL_1 = 0x0C
|
|
EFFECT_CONTROL_2 = 0x0D
|
|
GEN_PURPOSE_CONTROLLER_1 = 0x10
|
|
GEN_PURPOSE_CONTROLLER_2 = 0x11
|
|
GEN_PURPOSE_CONTROLLER_3 = 0x12
|
|
GEN_PURPOSE_CONTROLLER_4 = 0x13
|
|
BANK_SELECT = 0x20
|
|
MODULATION_WHEEL = 0x21
|
|
BREATH_CONTROLLER = 0x22
|
|
FOOT_CONTROLLER = 0x24
|
|
PORTAMENTO_TIME = 0x25
|
|
DATA_ENTRY = 0x26
|
|
CHANNEL_VOLUME = 0x27
|
|
BALANCE = 0x28
|
|
PAN = 0x2A
|
|
EXPRESSION_CONTROLLER = 0x2B
|
|
EFFECT_CONTROL_1 = 0x2C
|
|
EFFECT_CONTROL_2 = 0x2D
|
|
GENERAL_PURPOSE_CONTROLLER_1 = 0x30
|
|
GENERAL_PURPOSE_CONTROLLER_2 = 0x31
|
|
GENERAL_PURPOSE_CONTROLLER_3 = 0x32
|
|
GENERAL_PURPOSE_CONTROLLER_4 = 0x33
|
|
SUSTAIN_ONOFF = 0x40
|
|
PORTAMENTO_ONOFF = 0x41
|
|
SOSTENUTO_ONOFF = 0x42
|
|
SOFT_PEDAL_ONOFF = 0x43
|
|
LEGATO_ONOFF = 0x44
|
|
HOLD_2_ONOFF = 0x45
|
|
SOUND_CONTROLLER_1 = 0x46
|
|
SOUND_CONTROLLER_2 = 0x47
|
|
SOUND_CONTROLLER_3 = 0x48
|
|
SOUND_CONTROLLER_4 = 0x49
|
|
SOUND_CONTROLLER_5 = 0x4A
|
|
SOUND_CONTROLLER_7 = 0x4C
|
|
SOUND_CONTROLLER_8 = 0x4D
|
|
SOUND_CONTROLLER_9 = 0x4E
|
|
SOUND_CONTROLLER_10 = 0x4F
|
|
GENERAL_PURPOSE_CONTROLLER_5 = 0x50
|
|
GENERAL_PURPOSE_CONTROLLER_6 = 0x51
|
|
GENERAL_PURPOSE_CONTROLLER_7 = 0x52
|
|
GENERAL_PURPOSE_CONTROLLER_8 = 0x53
|
|
PORTAMENTO_CONTROL = 0x54
|
|
EFFECTS_1 = 0x5B
|
|
EFFECTS_2 = 0x5C
|
|
EFFECTS_3 = 0x5D
|
|
EFFECTS_4 = 0x5E
|
|
EFFECTS_5 = 0x5F
|
|
DATA_INCREMENT = 0x60
|
|
DATA_DECREMENT = 0x61
|
|
NON_REGISTERED_PARAMETER_NUMBER = 0x62
|
|
NON_REGISTERED_PARAMETER_NUMBER = 0x63
|
|
REGISTERED_PARAMETER_NUMBER = 0x64
|
|
REGISTERED_PARAMETER_NUMBER = 0x65
|
|
ALL_SOUND_OFF = 0x78
|
|
RESET_ALL_CONTROLLERS = 0x79
|
|
LOCAL_CONTROL_ONOFF = 0x7A
|
|
ALL_NOTES_OFF = 0x7B
|
|
OMNI_MODE_OFF = 0x7C
|
|
OMNI_MODE_ON = 0x7D
|
|
MONO_MODE_ON = 0x7E
|
|
POLY_MODE_ON = 0x7F
|
|
SYSTEM_EXCLUSIVE = 0xF0
|
|
MTC = 0xF1
|
|
SONG_POSITION_POINTER = 0xF2
|
|
SONG_SELECT = 0xF3
|
|
TUNING_REQUEST = 0xF6
|
|
END_OFF_EXCLUSIVE = 0xF7
|
|
SEQUENCE_NUMBER = 0x00
|
|
TEXT = 0x01
|
|
COPYRIGHT = 0x02
|
|
SEQUENCE_NAME = 0x03
|
|
INSTRUMENT_NAME = 0x04
|
|
LYRIC = 0x05
|
|
MARKER = 0x06
|
|
CUEPOINT = 0x07
|
|
PROGRAM_NAME = 0x08
|
|
DEVICE_NAME = 0x09
|
|
MIDI_CH_PREFIX = 0x20
|
|
MIDI_PORT = 0x21
|
|
END_OF_TRACK = 0x2F
|
|
TEMPO = 0x51
|
|
SMTP_OFFSET = 0x54
|
|
TIME_SIGNATURE = 0x58
|
|
KEY_SIGNATURE = 0x59
|
|
SPECIFIC = 0x7F
|
|
FILE_HEADER = 'MThd'
|
|
TRACK_HEADER = 'MTrk'
|
|
TIMING_CLOCK = 0xF8
|
|
SONG_START = 0xFA
|
|
SONG_CONTINUE = 0xFB
|
|
SONG_STOP = 0xFC
|
|
ACTIVE_SENSING = 0xFE
|
|
SYSTEM_RESET = 0xFF
|
|
META_EVENT = 0xFF
|
|
def is_status(byte):
|
|
return (byte & 0x80) == 0x80
|
|
class MidiToBeep:
|
|
def update_time(self, new_time=0, relative=1):
|
|
if relative:
|
|
self._relative_time = new_time
|
|
self._absolute_time += new_time
|
|
else:
|
|
self._relative_time = new_time - self._absolute_time
|
|
self._absolute_time = new_time
|
|
if self._relative_time:
|
|
# time was advanced, so output something
|
|
d = {}
|
|
for c,v in self.current_notes_on: d[v+self.semitonesAdd[c]]=1
|
|
if self.need_to_interleave_tracks: self.tracks[-1].append([d.keys(),self._relative_time*self.microsecsPerDivision])
|
|
else: dedup_midi_note_chord(d.keys(),self._relative_time*self.microsecsPerDivision)
|
|
def reset_time(self):
|
|
self._relative_time = 0
|
|
self._absolute_time = 0
|
|
def rel_time(self): return self._relative_time
|
|
def abs_time(self): return self._absolute_time
|
|
def reset_run_stat(self): self._running_status = None
|
|
def set_run_stat(self, new_status): self._running_status = new_status
|
|
def get_run_stat(self): return self._running_status
|
|
def set_current_track(self, new_track): self._current_track = new_track
|
|
def get_current_track(self): return self._current_track
|
|
def __init__(self):
|
|
self._absolute_time = 0
|
|
self._relative_time = 0
|
|
self._current_track = 0
|
|
self._running_status = None
|
|
self.current_notes_on = []
|
|
self.rpnLsb = [0]*16
|
|
self.rpnMsb = [0]*16
|
|
self.semitoneRange = [1]*16
|
|
self.semitonesAdd = [0]*16
|
|
self.microsecsPerDivision = 10000
|
|
def note_on(self, channel=0, note=0x40, velocity=0x40):
|
|
if velocity and not channel==9: self.current_notes_on.append((channel,note))
|
|
def note_off(self, channel=0, note=0x40, velocity=0x40):
|
|
try: self.current_notes_on.remove((channel,note))
|
|
except ValueError: pass
|
|
def aftertouch(self, channel=0, note=0x40, velocity=0x40): pass
|
|
def continuous_controller(self, channel, controller, value):
|
|
# Interpret "pitch bend range":
|
|
if controller==64: self.rpnLsb[channel] = value
|
|
elif controller==65: self.rpnMsb[channel] = value
|
|
elif controller==6 and self.rpnLsb[channel]==self.rpnMsb[channel]==0:
|
|
self.semitoneRange[channel]=value
|
|
def patch_change(self, channel, patch): pass
|
|
def channel_pressure(self, channel, pressure): pass
|
|
def pitch_bend(self, channel, value):
|
|
# Pitch bend is sometimes used for slurs
|
|
# so we'd better interpret it (only MSB for now; full range is over 8192)
|
|
self.semitonesAdd[channel] = (value-64)*self.semitoneRange[channel]/64.0
|
|
def sysex_event(self, data): pass
|
|
def midi_time_code(self, msg_type, values): pass
|
|
def song_position_pointer(self, value): pass
|
|
def song_select(self, songNumber): pass
|
|
def tuning_request(self): pass
|
|
def header(self, format=0, nTracks=1, division=96):
|
|
self.division=division
|
|
self.need_to_interleave_tracks = (format==1)
|
|
self.tracks = [[]][:]
|
|
def eof(self):
|
|
if self.need_to_interleave_tracks:
|
|
while True: # delete empty tracks
|
|
try: self.tracks.remove([])
|
|
except ValueError: break
|
|
while self.tracks:
|
|
minLen = min([t[0][1] for t in self.tracks])
|
|
d = {}
|
|
for t in self.tracks: d.update([(n,1) for n in t[0][0]])
|
|
dedup_midi_note_chord(d.keys(),minLen)
|
|
for t in self.tracks:
|
|
t[0][1] -= minLen
|
|
if t[0][1]==0: del t[0]
|
|
while True: # delete empty tracks
|
|
try: self.tracks.remove([])
|
|
except ValueError: break
|
|
def meta_event(self, meta_type, data): pass
|
|
def start_of_track(self, n_track=0):
|
|
self.reset_time()
|
|
self._current_track += 1
|
|
if self.need_to_interleave_tracks: self.tracks.append([])
|
|
def end_of_track(self): pass
|
|
def sequence_number(self, value): pass
|
|
def text(self, text): pass
|
|
def copyright(self, text): pass
|
|
def sequence_name(self, text): pass
|
|
def instrument_name(self, text): pass
|
|
def lyric(self, text): pass
|
|
def marker(self, text): pass
|
|
def cuepoint(self, text): pass
|
|
def program_name(self,progname): pass
|
|
def device_name(self,devicename): pass
|
|
def midi_ch_prefix(self, channel): pass
|
|
def midi_port(self, value): pass
|
|
def tempo(self, value):
|
|
# TODO if need_to_interleave_tracks, and tempo is not already put in on all tracks, and there's a tempo command that's not at the start and/or not on 1st track, we may need to do something
|
|
self.microsecsPerDivision = value/self.division
|
|
def smtp_offset(self, hour, minute, second, frame, framePart): pass
|
|
def time_signature(self, nn, dd, cc, bb): pass
|
|
def key_signature(self, sf, mi): pass
|
|
def sequencer_specific(self, data): pass
|
|
|
|
class RawInstreamFile:
|
|
def __init__(self, infile=''):
|
|
if infile:
|
|
if isinstance(infile, StringType):
|
|
infile = open(infile, 'rb')
|
|
self.data = infile.read()
|
|
infile.close()
|
|
else:
|
|
self.data = infile.read()
|
|
else:
|
|
self.data = ''
|
|
self.cursor = 0
|
|
def setData(self, data=''):
|
|
self.data = data
|
|
def setCursor(self, position=0):
|
|
self.cursor = position
|
|
def getCursor(self):
|
|
return self.cursor
|
|
def moveCursor(self, relative_position=0):
|
|
self.cursor += relative_position
|
|
def nextSlice(self, length, move_cursor=1):
|
|
c = self.cursor
|
|
slc = self.data[c:c+length]
|
|
if move_cursor:
|
|
self.moveCursor(length)
|
|
return slc
|
|
def readBew(self, n_bytes=1, move_cursor=1):
|
|
return readBew(self.nextSlice(n_bytes, move_cursor))
|
|
def readVarLen(self):
|
|
MAX_VARLEN = 4
|
|
var = readVar(self.nextSlice(MAX_VARLEN, 0))
|
|
self.moveCursor(varLen(var))
|
|
return var
|
|
class EventDispatcher:
|
|
def __init__(self, outstream):
|
|
self.outstream = outstream
|
|
self.convert_zero_velocity = 1
|
|
self.dispatch_continuos_controllers = 1
|
|
self.dispatch_meta_events = 1
|
|
def header(self, format, nTracks, division):
|
|
self.outstream.header(format, nTracks, division)
|
|
def start_of_track(self, current_track):
|
|
self.outstream.set_current_track(current_track)
|
|
self.outstream.start_of_track(current_track)
|
|
def sysex_event(self, data):
|
|
self.outstream.sysex_event(data)
|
|
def eof(self):
|
|
self.outstream.eof()
|
|
def update_time(self, new_time=0, relative=1):
|
|
self.outstream.update_time(new_time, relative)
|
|
def reset_time(self):
|
|
self.outstream.reset_time()
|
|
def channel_messages(self, hi_nible, channel, data):
|
|
stream = self.outstream
|
|
data = toBytes(data)
|
|
if (NOTE_ON & 0xF0) == hi_nible:
|
|
note, velocity = data
|
|
if velocity==0 and self.convert_zero_velocity:
|
|
stream.note_off(channel, note, 0x40)
|
|
else:
|
|
stream.note_on(channel, note, velocity)
|
|
elif (NOTE_OFF & 0xF0) == hi_nible:
|
|
note, velocity = data
|
|
stream.note_off(channel, note, velocity)
|
|
elif (AFTERTOUCH & 0xF0) == hi_nible:
|
|
note, velocity = data
|
|
stream.aftertouch(channel, note, velocity)
|
|
elif (CONTINUOUS_CONTROLLER & 0xF0) == hi_nible:
|
|
controller, value = data
|
|
if self.dispatch_continuos_controllers:
|
|
self.continuous_controllers(channel, controller, value)
|
|
else:
|
|
stream.continuous_controller(channel, controller, value)
|
|
elif (PATCH_CHANGE & 0xF0) == hi_nible:
|
|
program = data[0]
|
|
stream.patch_change(channel, program)
|
|
elif (CHANNEL_PRESSURE & 0xF0) == hi_nible:
|
|
pressure = data[0]
|
|
stream.channel_pressure(channel, pressure)
|
|
elif (PITCH_BEND & 0xF0) == hi_nible:
|
|
hibyte, lobyte = data
|
|
value = (hibyte<<7) + lobyte
|
|
stream.pitch_bend(channel, value)
|
|
else:
|
|
raise ValueError, 'Illegal channel message!'
|
|
def continuous_controllers(self, channel, controller, value):
|
|
stream = self.outstream
|
|
stream.continuous_controller(channel, controller, value)
|
|
def system_commons(self, common_type, common_data):
|
|
stream = self.outstream
|
|
if common_type == MTC:
|
|
data = readBew(common_data)
|
|
msg_type = (data & 0x07) >> 4
|
|
values = (data & 0x0F)
|
|
stream.midi_time_code(msg_type, values)
|
|
elif common_type == SONG_POSITION_POINTER:
|
|
hibyte, lobyte = toBytes(common_data)
|
|
value = (hibyte<<7) + lobyte
|
|
stream.song_position_pointer(value)
|
|
elif common_type == SONG_SELECT:
|
|
data = readBew(common_data)
|
|
stream.song_select(data)
|
|
elif common_type == TUNING_REQUEST:
|
|
stream.tuning_request(time=None)
|
|
def meta_events(self, meta_type, data):
|
|
stream = self.outstream
|
|
if meta_type == SEQUENCE_NUMBER:
|
|
number = readBew(data)
|
|
stream.sequence_number(number)
|
|
elif meta_type == TEXT:
|
|
stream.text(data)
|
|
elif meta_type == COPYRIGHT:
|
|
stream.copyright(data)
|
|
elif meta_type == SEQUENCE_NAME:
|
|
stream.sequence_name(data)
|
|
elif meta_type == INSTRUMENT_NAME:
|
|
stream.instrument_name(data)
|
|
elif meta_type == LYRIC:
|
|
stream.lyric(data)
|
|
elif meta_type == MARKER:
|
|
stream.marker(data)
|
|
elif meta_type == CUEPOINT:
|
|
stream.cuepoint(data)
|
|
elif meta_type == PROGRAM_NAME:
|
|
stream.program_name(data)
|
|
elif meta_type == DEVICE_NAME:
|
|
stream.device_name(data)
|
|
elif meta_type == MIDI_CH_PREFIX:
|
|
channel = readBew(data)
|
|
stream.midi_ch_prefix(channel)
|
|
elif meta_type == MIDI_PORT:
|
|
port = readBew(data)
|
|
stream.midi_port(port)
|
|
elif meta_type == END_OF_TRACK:
|
|
stream.end_of_track()
|
|
elif meta_type == TEMPO:
|
|
b1, b2, b3 = toBytes(data)
|
|
stream.tempo((b1<<16) + (b2<<8) + b3)
|
|
elif meta_type == SMTP_OFFSET:
|
|
hour, minute, second, frame, framePart = toBytes(data)
|
|
stream.smtp_offset(
|
|
hour, minute, second, frame, framePart)
|
|
elif meta_type == TIME_SIGNATURE:
|
|
nn, dd, cc, bb = toBytes(data)
|
|
stream.time_signature(nn, dd, cc, bb)
|
|
elif meta_type == KEY_SIGNATURE:
|
|
sf, mi = toBytes(data)
|
|
stream.key_signature(sf, mi)
|
|
elif meta_type == SPECIFIC:
|
|
meta_data = toBytes(data)
|
|
stream.sequencer_specific(meta_data)
|
|
else:
|
|
meta_data = toBytes(data)
|
|
stream.meta_event(meta_type, meta_data)
|
|
class MidiFileParser:
|
|
def __init__(self, raw_in, outstream):
|
|
self.raw_in = raw_in
|
|
self.dispatch = EventDispatcher(outstream)
|
|
self._running_status = None
|
|
def parseMThdChunk(self):
|
|
raw_in = self.raw_in
|
|
header_chunk_type = raw_in.nextSlice(4)
|
|
header_chunk_zise = raw_in.readBew(4)
|
|
if header_chunk_type != 'MThd': raise TypeError, "It is not a valid midi file!"
|
|
self.format = raw_in.readBew(2)
|
|
self.nTracks = raw_in.readBew(2)
|
|
self.division = raw_in.readBew(2)
|
|
if header_chunk_zise > 6:
|
|
raw_in.moveCursor(header_chunk_zise-6)
|
|
self.dispatch.header(self.format, self.nTracks, self.division)
|
|
def parseMTrkChunk(self):
|
|
self.dispatch.reset_time()
|
|
dispatch = self.dispatch
|
|
raw_in = self.raw_in
|
|
dispatch.start_of_track(self._current_track)
|
|
raw_in.moveCursor(4)
|
|
tracklength = raw_in.readBew(4)
|
|
track_endposition = raw_in.getCursor() + tracklength
|
|
while raw_in.getCursor() < track_endposition:
|
|
time = raw_in.readVarLen()
|
|
dispatch.update_time(time)
|
|
peak_ahead = raw_in.readBew(move_cursor=0)
|
|
if (peak_ahead & 0x80):
|
|
status = self._running_status = raw_in.readBew()
|
|
else:
|
|
status = self._running_status
|
|
hi_nible, lo_nible = status & 0xF0, status & 0x0F
|
|
if status == META_EVENT:
|
|
meta_type = raw_in.readBew()
|
|
meta_length = raw_in.readVarLen()
|
|
meta_data = raw_in.nextSlice(meta_length)
|
|
dispatch.meta_events(meta_type, meta_data)
|
|
elif status == SYSTEM_EXCLUSIVE:
|
|
sysex_length = raw_in.readVarLen()
|
|
sysex_data = raw_in.nextSlice(sysex_length-1)
|
|
if raw_in.readBew(move_cursor=0) == END_OFF_EXCLUSIVE:
|
|
eo_sysex = raw_in.readBew()
|
|
dispatch.sysex_event(sysex_data)
|
|
elif hi_nible == 0xF0:
|
|
data_sizes = {
|
|
MTC:1,
|
|
SONG_POSITION_POINTER:2,
|
|
SONG_SELECT:1,
|
|
}
|
|
data_size = data_sizes.get(hi_nible, 0)
|
|
common_data = raw_in.nextSlice(data_size)
|
|
common_type = lo_nible
|
|
dispatch.system_common(common_type, common_data)
|
|
else:
|
|
data_sizes = {
|
|
PATCH_CHANGE:1,
|
|
CHANNEL_PRESSURE:1,
|
|
NOTE_OFF:2,
|
|
NOTE_ON:2,
|
|
AFTERTOUCH:2,
|
|
CONTINUOUS_CONTROLLER:2,
|
|
PITCH_BEND:2,
|
|
}
|
|
data_size = data_sizes.get(hi_nible, 0)
|
|
channel_data = raw_in.nextSlice(data_size)
|
|
event_type, channel = hi_nible, lo_nible
|
|
dispatch.channel_messages(event_type, channel, channel_data)
|
|
def parseMTrkChunks(self):
|
|
for t in range(self.nTracks):
|
|
self._current_track = t
|
|
self.parseMTrkChunk()
|
|
self.dispatch.eof()
|
|
class MidiInFile:
|
|
def __init__(self, outStream, infile=''):
|
|
self.raw_in = RawInstreamFile(infile)
|
|
self.parser = MidiFileParser(self.raw_in, outStream)
|
|
def read(self):
|
|
p = self.parser
|
|
p.parseMThdChunk()
|
|
p.parseMTrkChunks()
|
|
def setData(self, data=''):
|
|
self.raw_in.setData(data)
|
|
|
|
try: any
|
|
except: # Python 2.3 (RISC OS?)
|
|
def any(x):
|
|
for i in x:
|
|
if i: return True
|
|
return False
|
|
def all(x):
|
|
for i in x:
|
|
if not i: return False
|
|
return True
|
|
|
|
if acorn_electron: name = "MIDI to Acorn Electron"
|
|
elif (bbc_micro or bbc_micro==[]): name = "MIDI to BBC Micro"
|
|
elif riscos_Maestro: name = "MIDI to Maestro"
|
|
else: name = "MIDI Beeper"
|
|
sys.stderr.write(name+" (c) 2007-2010, 2015-2022 Silas S. Brown. License: Apache 2\n")
|
|
if len(sys.argv)<2:
|
|
sys.stderr.write("Syntax: python2 convert.py [options] MIDI-filename ...\nOptions: --bbc | --electron | --bbc-binary | --bbc-ssd\n") # (all BBC-Micro related)
|
|
sys.exit(1)
|
|
for midiFile in sys.argv[1:]:
|
|
init() ; dedup_chord,dedup_microsec = [],0
|
|
dedup_microsec_error = 0
|
|
sys.stderr.write("Parsing MIDI file "+midiFile+"\n")
|
|
MidiInFile(MidiToBeep(), open(midiFile,"rb")).read()
|
|
dedup_midi_note_chord([],None) # ensure flushed
|
|
if bbc_micro or bbc_micro==[]:
|
|
if bbc_ssd:
|
|
bbcFile = midiFile.replace(os.extsep+"midi","").replace(os.extsep+"mid","")
|
|
if os.sep in bbcFile: bbcFile=bbcFile[bbcFile.rindex(os.sep)+1:]
|
|
if not 0<len(bbcFile)<=7: bbcFile="TUNE%d" % (1+len(bbc_files))
|
|
bbc_files.append((bbcFile,"".join(chr(x) for x in (bbc_micro+[255,0]))))
|
|
# and reset:
|
|
bbc_micro = []
|
|
for i in xrange(len(current_array)): current_array[i]=63
|
|
# else (BBC non-SSD) we'll end below (TODO: per-file?)
|
|
elif riscos_Maestro:
|
|
add_midi_note_chord([],0)
|
|
maestroFile = midiFile.replace(os.extsep+"midi","").replace(os.extsep+"mid","") + ',af1'
|
|
assert not maestroFile == midiFile
|
|
sys.stderr.write("Writing Maestro file "+maestroFile+"... ")
|
|
open(maestroFile,'wb').write(maestroData())
|
|
sys.stderr.write("Finished\n")
|
|
elif not aplay:
|
|
sys.stderr.write("Converting "+midiFile+"\n")
|
|
runBeep(" ".join(cumulative_params))
|
|
if bbc_ssd and bbc_files:
|
|
ssdFile=os.environ.get("DFS_TITLE","tunes")+".ssd"
|
|
sys.stderr.write("Writing output to %s\n" % ssdFile)
|
|
open(ssdFile,"wb").write(make_bbcMicro_DFS_image(bbc_files))
|
|
elif bbc_micro:
|
|
if bbc_binary: # need to get it in via indirection
|
|
bbc_micro += [255,0]
|
|
# :EQUD&12345678 - 14 keystrokes for 4 bytes
|
|
# + 7 for every 60 bytes, total = 14/4+7/60
|
|
if len(bbc_micro) >= 15000: sys.stderr.write("This might exceed BeebEm's 32K keystroke limit. Try pasting 150 lines at a time.\n") # TODO: could check the *exact* limit, but would have to count the code at the start etc
|
|
use_input_loop = ( 8000 < len(bbc_micro) < 15000 ) # saves keystrokes, but might not save actual typing time in a museum etc because EQUD can be copied and the EQUD version is more 'chunked' and easier to track; might be useful to get around max-keystroke limitations on emulators IF we're not exceeding them anyway: typical limit 32k keystrokes which is about 9000 bytes via EQUD method and 16000 via input loop, but allow for program overhead in both cases. Input loop is slower to paste into an emulator (especially at speed=1) due to the overheads of running a BASIC input/eval loop: if we're way too big then you'll need to chunk it anyway (try 150 lines at a time) so might as well use EQUD in this case. Also, don't use this if data is so big as to leave no room for A$ (although if that's the case then we might have bigger problems re c% + stack); above limits allow for this in default screen modes (6 on Electron and 7 on BBC)
|
|
bbc_micro[0]=bbc_micro[0].replace("NEW","NEW\n0LOMEM=TOP+"+str(len(bbc_micro)-1))
|
|
if use_input_loop: bbc_micro.insert(1,"P%=TOP:LOMEM=P%+"+str(len(bbc_micro)-1)) # need to set now for A$
|
|
else: bbc_micro.insert(1,"P%=TOP")
|
|
bbc_offset = 0 ; i = 2
|
|
if use_input_loop:
|
|
bbc_micro.insert(i,'REP.:I.A$:IF LEN(A$):F.A%=1TO193STEP8:!P%=EVAL("&"+MID$(A$,A%,8)):P%=P%+4:N.:U.RIGHT$(A$,1)="*":EL.:U.0') ; i += 1 # horrible mix of if/else and repeat/until on 1 line in immediate mode so it copes with any extra blank lines that buggy emulators might insert
|
|
while i < len(bbc_micro)-100:
|
|
buf = []
|
|
for j in range(i,i+100): buf.append("%02X" % bbc_micro[j])
|
|
for j in range(0,100,4): buf[j],buf[j+1],buf[j+2],buf[j+3] = buf[j+3],buf[j+2],buf[j+1],buf[j] # LSB-MSB
|
|
del bbc_micro[i+1:i+100]
|
|
bbc_micro[i] = "".join(buf) ; i += 1
|
|
bbc_micro[i-1] += '*'
|
|
# Make up the rest (or if not use_input_loop) :
|
|
# TODO: Bas128 gives a "Wrap" error if P% crosses a 16k boundary (even if it's only doing EQUB), so may want a "use plain old indirection" option (low priority because Bas128 has timing issues anyway)
|
|
while i<len(bbc_micro):
|
|
buf = ["[OPT2"]
|
|
while i<len(bbc_micro) and len(buf)<16:
|
|
for fmt,nBytes in [("EQUD&%X",4),("EQUW%d",2),("EQUB%d",1)]:
|
|
if i+nBytes <= len(bbc_micro):
|
|
if nBytes==2 and i+3==len(bbc_micro) and bbc_micro[-1]==0: continue # in this case it might save a couple of keystrokes to end with EQUB *then* EQUW (if bbc_micro[i] is small; worst-case 255,255,0 is the same either way and the BRK optimisation could save 2 keystrokes if doing the EQUB second)
|
|
val = 0
|
|
for j in range(nBytes):
|
|
val += (bbc_micro[i+j]<<(j*8))
|
|
if nBytes==1: buf.append({0:"BRK",10:"ASLA",0x18:"CLC",0x38:"SEC",0x58:"CLI",0x78:"SEI",0xb8:"CLV",0xD8:"CLD",0xF8:"SED",0x4A:"LSRA",0xEA:"NOP",0x40:"RTI",0x60:"RTS"}.get(val,fmt%val)) # occasionally save a couple of keystrokes over the EQUB (usually with a BRK at end)
|
|
else: buf.append(fmt % val)
|
|
del bbc_micro[i:i+nBytes]
|
|
bbc_offset += nBytes
|
|
break
|
|
bbc_micro.insert(i,":".join(buf)+']') ; i += 1
|
|
# TODO: other methods to reduce keystrokes? (Another thing that could be done is to automatically detect repeats in the MIDI file and get the BBC to do its own repeating instead of writing the data out twice, but this is likely to work only on automatically-generated MIDI files with strict quantised rhythm, otherwise it's likely to be subtly different on the repeat.)
|
|
if not use_input_loop: bbc_micro.append("LOMEM=P%") # because we didn't set it before (and might want to change MODE or something before running; anyway having it here makes it clearer what's going on if you see the screen at the end of the paste)
|
|
elif len(bbc_micro)>1 and len(bbc_micro[-1])<233: bbc_micro[-1] += ",255,0"
|
|
else: bbc_micro.append("D.255,0")
|
|
if not bbc_binary:
|
|
bbc_micro = "\n".join(bbc_micro).split("\n")
|
|
if bbc_sdl:
|
|
# bbc_sdl doesn't recognise keyword abbreviations, so use longhand:
|
|
bbc_micro = "\n".join(bbc_micro).replace("D.","DATA").replace("N.","NEXT").replace("U.","UNTIL").replace("SO.","SOUND").replace("REP.","REPEAT").replace("ENV.","ENVELOPE")
|
|
# Work around bbc_sdl bug #3 (on 1.12 and below) in the case of 2 notes per channel, by using a 0-length 3rd step that negates the 2nd step's change, which should clear up any piece with 6 notes or fewer per chord:
|
|
bbc_micro = bbc_micro.replace("V%=1","V%=1:Q%=0:R%=0")
|
|
if acorn_electron: bbc_micro=bbc_micro.replace("Q%=c%(1)-P%","Q%=c%(1)-P%:R%=-Q%")
|
|
else: bbc_micro=bbc_micro.replace("V%=2","V%=2:R%=-Q%")
|
|
# Add 1 octave if BBC BASIC for SDL (or BBC BASIC for Windows) is detected, because it's pitched an octave lower than the real BBC (well, we could use *VOICE c,5 to emphasize the first harmonic, but we'd have to check which versions support it and it's not quite the same) :
|
|
bbc_micro=bbc_micro.replace("N%=0","A%=-48*((INKEY(-256)AND219)=83):N%=0")
|
|
if acorn_electron: bbc_micro=bbc_micro.replace("P%,","P%+A%,")
|
|
else: bbc_micro=bbc_micro.replace("c%(I%),","c%(I%)+A%,")
|
|
# add line numbers, in case we're on a real BBC (as we can't use AUTO, which cannot be conditioned on INKEY(-256)); already added line numbers to DATA lines (so we know max length on real BBC) but others need adding:
|
|
bbc_micro = bbc_micro.split("\n")
|
|
for i in xrange(len(bbc_micro)):
|
|
bbc_micro[i]=str(i+1)+bbc_micro[i]
|
|
# If not bbc_sdl (and not bbc_binary), use AUTO.
|
|
# AUTO automatically stops once the line number would be >= 32768. We can use this to avoid having to put an Escape into the keyboard buffer.
|
|
# TODO: If user is pasting this in multiple chunks, and emulator adds a spurious newline at the beginning of each chunk (e.g. BeebEm 3 on Mac), AUTO start number needs decreasing (unless user makes sure not to include the newline at the end of each chunk if the emulator will add its own at the start of the next)
|
|
elif len(bbc_micro) > 3277: bbc_micro.insert(0,"AU."+str(32768-len(bbc_micro))+",1") # (although if this is the case, program is extremely likely to exhaust the memory even in Bas128)
|
|
else: bbc_micro.insert(0,"AU."+str(32770-10*len(bbc_micro)))
|
|
print "\n".join(bbc_micro)
|