#!/usr/bin/env python
import sys
import zipfile
import XML
import struct
import ctypes
import math
import datetime
TOTAL_SEMITONES = 120
SAMPLERATE = 44100
class InputException(Exception):
def __init__(self, message):
Exception.__init__(self)
self.message = message
class Volume:
def __init__(self, left, right):
self.left = left
self.right = right
def __mul__(self, other):
return Volume(self.left * other.left, self.right * other.right)
def __eq__(self, other):
return self.left == other.left and self.right == other.right
def isPanned(self):
return self.left != self.right
def makeVolume(xvolume):
v = float(xvolume)
return Volume(v,v)
def makePanning(xpanning):
p = float(xpanning)
return Volume(math.sqrt(2.0 * (1.0 - p)), math.sqrt(2.0 * p))
class Instrument:
NAMES = ["seed", "modes", "fat", "width",
"overtones", "sharpness", "harmonicity", "decaylow", "decayhigh",
"filterlow", "fslopelow", "fsweeplow", "filterhigh", "fslopehigh", "fsweephigh",
"gain", "attack", "release",
"dummy",
"q_decaydiff", "q_decaylow", "q_harmonicity", "q_sharpness", "q_width",
"q_f_low", "q_fs_low", "q_fsw_low", "q_f_high", "q_fs_high", "q_fsw_high",
"q_gain", "q_attack", "q_release"
]
def __init__(self, number, name, params, legacy):
if legacy:
# Duplicate filter sweep parameter
params = params[:11] + [params[13]] + params[11:17] + [0.0] + params[20:27] + [params[29]] + params[27:33]
names = Instrument.NAMES
self.number = number
self.name = name
self.params = params
for i,p in enumerate(self.params):
self.__dict__[names[i]] = p
self.volume = Volume(1.0, 1.0)
self.maxsamples = 0
self.title = "%02X|%s" % (self.number, self.name)
class Reverb:
NAMES = ["mix", "pan", "delaymin", "delaymax", "delayadd",
"halftime", "filterlow", "filterhigh", "dampenlow", "dampenhigh",
"n", "seed", "dummy1", "dummy2", "dummy3",
"q_mixpan", "q_flow", "q_fhigh", "q_dlow", "q_dhigh"
]
def __init__(self, params):
names = Reverb.NAMES
self.params = (params + [0.0] * len(names))[:len(names)]
for i,p in enumerate(self.params):
self.__dict__[names[i]] = p
self.p_min_delay = math.floor(self.delaymin * 100 + 0.5) * 256
self.p_max_delay = math.floor(self.delaymax * 100 + 0.5) * 256
self.p_add_delay = math.floor(self.delayadd * 100 + 0.5) * 256
self.p_filter_low = min(1, quantize(math.pow(self.filterlow, 2), self.q_flow))
self.p_filter_high = min(1, quantize(math.pow(self.filterhigh, 2), self.q_fhigh))
self.p_dampen_low = min(1, quantize(math.pow(self.dampenlow, 2), self.q_dlow))
self.p_dampen_high = min(1, quantize(math.pow(self.dampenhigh, 2), self.q_dhigh))
self.p_num_delays = math.floor(self.n * 100 + 0.5) * 2
self.p_seed = math.floor(self.seed * 100 + 0.5) * 2048
self.p_decay_mul = math.pow(2.0, 1.0 / (self.halftime * SAMPLERATE))
self.p_max_decay = math.pow(self.p_decay_mul, -self.p_max_delay)
mix = self.mix * 10 / math.sqrt(self.p_num_delays)
self.p_volumes = [quantize(mix * math.sqrt(1 + s - 2 * s * self.pan), self.q_mixpan) for s in [1,-1]]
def __eq__(self, other):
return all(p1 == p2 for p1,p2 in zip(self.params, other.params))
class Note:
NOTEBASES = {
"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11
}
NOTENAMES = {
0: "C-", 1: "C#", 2: "D-", 3: "D#", 4: "E-", 5: "F-",
6: "F#", 7: "G-", 8: "G#", 9: "A-", 10: "A#", 11: "B-"
}
def __init__(self, tname, column, line, songpos, pat, patline, note, instr, velocity):
note = str(note)
self.line = int(line)
self.songpos = int(songpos)
self.pat = int(pat)
self.patline = patline
if note == "OFF":
self.off = True
self.tone = None
self.instr = 0
self.velocity = 0
else:
self.off = False
octave = int(note[2])
notebase = Note.NOTEBASES[note[0]]
sharp = int(note[1] == "#")
self.tone = octave * 12 + notebase + sharp
self.instr = int(str(instr), 16)
try:
self.velocity = 127 if str(velocity) == "" or str(velocity) == ".." else int(str(velocity), 16)
except ValueError:
print "Track '%s' column %d pattern %d line %d: Illegal velocity value '%s'" % (tname, column, pat, patline, str(velocity));
self.velocity = 127
def notename(tone):
return Note.NOTENAMES[tone%12] + str(tone/12)
def instplugins(xinst):
xplugins = xinst.PluginProperties
if xplugins:
return xplugins
return xinst.PluginGenerator
def isactive(xdevice):
if not xdevice:
return False
if xdevice.IsActive.Value:
return float(xdevice.IsActive.Value) != 0.0
else:
return str(xdevice.IsActive) == "true"
def f2i(f):
return struct.unpack('I', struct.pack('f', f))[0]
def i2f(i):
return struct.unpack('f', struct.pack('I', i))[0]
def quantize(value, level):
bit = 1 << int(math.floor(level * 31))
mask = 0x100000000 - bit
add = bit >> 1
i = f2i(value)
i = (i + add) & mask
if i == 0x80000000:
i = 0x00000000
q = i2f(i)
return q
def makeParamBlock(inst, uses_panning):
modes = max(1, math.floor(0.5 + inst.modes * 100))
fat = max(1, math.floor(0.5 + inst.fat * 100))
seed = math.floor(0.5 + inst.seed * 100)
overtones = math.floor(0.5 + inst.overtones * 100)
decaydiff = inst.decayhigh - inst.decaylow
decaylow = inst.decaylow
harmonicity = inst.harmonicity * 2 - 1
sharpness = inst.sharpness * 5 - 4
width = 100 * math.pow(inst.width, 5)
fsweeplow = -math.pow(inst.fsweeplow - 0.5, 3) * 100 * TOTAL_SEMITONES / SAMPLERATE
fsweephigh = -math.pow(inst.fsweephigh - 0.5, 3) * 100 * TOTAL_SEMITONES / SAMPLERATE
fslopelow = math.pow((1 - inst.fslopelow), 3)
fslopehigh = -math.pow((1 - inst.fslopehigh), 3)
filterlow = (inst.filterlow * 2 - 1) * TOTAL_SEMITONES
filterhigh = (inst.filterhigh * 2 - 1) * TOTAL_SEMITONES
gain = math.pow(4096, inst.gain - 0.25)
attack = 2.0 if inst.attack == 0.0 else 1 / (inst.attack * inst.attack) / SAMPLERATE
release = -(2.0 if inst.release == 0.0 else 1 / inst.release / SAMPLERATE)
decaydiff = quantize(decaydiff, inst.q_decaydiff)
decaylow = quantize(decaylow, inst.q_decaylow)
harmonicity = quantize(harmonicity, inst.q_harmonicity)
sharpness = quantize(sharpness, inst.q_sharpness)
width = quantize(width, inst.q_width)
filterlow = quantize(filterlow, inst.q_f_low)
fslopelow = quantize(fslopelow, inst.q_fs_low)
fsweeplow = quantize(fsweeplow, inst.q_fsw_low)
filterhigh = quantize(filterhigh, inst.q_f_high)
fslopehigh = quantize(fslopehigh, inst.q_fs_high)
fsweephigh = quantize(fsweephigh, inst.q_fsw_high)
gain = quantize(gain, inst.q_gain)
attack = quantize(attack, inst.q_attack)
release = quantize(release, inst.q_release)
maxdecay = max(decaylow, decaylow + decaydiff)
releasetime = inst.maxtime * SAMPLERATE + 1.0 / -release if release != 0.0 else float('inf')
decaytime = (math.log(0.01, maxdecay)) * 4096 if maxdecay < 1.0 else float('inf')
if math.isinf(releasetime) and math.isinf(decaytime):
raise InputException("Instrument '%s' has infinite duration" % inst.title)
inst.maxsamples = int(min(releasetime, decaytime) + 65535) & -65536
left_volume = inst.volume.left * inst.velocity_quantum * 128
right_volume = inst.volume.right * inst.velocity_quantum * 128
volume = (left_volume + right_volume) / 2
pan = right_volume / volume - 1
volume = quantize(volume, 0.65)
pan = quantize(pan, 0.55)
return [int(modes), int(fat), int(seed), int(overtones),
decaydiff, decaylow, harmonicity, sharpness, width,
filterlow, filterhigh, fslopelow, fslopehigh, fsweeplow, fsweephigh,
gain, inst.maxsamples, release, attack,
volume] + ([pan] if uses_panning else [])
class Track:
def __init__(self, number, column, name, notes, volume, instruments):
self.number = number
self.column = column
self.name = name
self.notes = notes
self.volume = volume
self.notemap = dict()
self.tav_repr = dict()
self.note_lengths = dict()
self.title = "%s, column %d" % (self.name, self.column)
self.labelname = ""
for c in self.name:
if c.isalnum() or c == '_':
self.labelname += c
self.instr = None
self.latest_note = 0
self.max_length = 0
prev = None
for note in notes:
if prev is not None and not prev.off:
if prev.instr is None:
raise InputException("Track '%s' column %d pattern %d line %d: Undefined instrument" % (name, column, prev.pat, prev.patline));
if self.instr is not None and prev.instr != self.instr:
raise InputException("Track '%s' column %d pattern %d line %d: More than one instrument in track" % (name, column, prev.pat, prev.patline))
self.instr = prev.instr
length = note.line - prev.line
if length < 0:
raise InputException("Track '%s' column %d pattern %d: Reversed note order from %d to %d" % (name, column, prev.pat, prev.patline, note.patline))
if prev.tone is None:
raise InputException("Track '%s' column %d pattern %d line %d: Toneless note" % (name, column, prev.pat, prev.patline))
self.latest_note = max(self.latest_note, prev.line)
self.max_length = max(self.max_length, length)
tav = (prev.tone, prev.velocity)
self.notemap[prev] = tav
if length not in self.note_lengths:
self.note_lengths[length] = 0
self.note_lengths[length] += 1
prev = note
if not prev.off:
raise InputException("Track '%s' column %d pattern %d line %d: Note not terminated (insert OFF)" % (name, column, prev.pat, prev.patline))
if len(self.note_lengths) == 1:
for l in self.note_lengths:
self.singular_length = l if l <= 255 else None
else:
self.singular_length = None
self.tavs = sorted(set(self.notemap.values()), key = (lambda (t,v) : (t,v)))
for i,tav in enumerate(self.tavs):
if tav[0] is None:
raise InputException("Track '%s' has a toneless note" % name)
self.tav_repr[tav] = i
self.longest_sample = None
self.sample_length_sum = None
class Music:
def __init__(self, tracks, instruments, length, ticklength, n_reverb_tracks, reverb, master_volume):
self.tracks = tracks
self.instruments = instruments
self.length = length
self.ticklength = ticklength
self.n_reverb_tracks = n_reverb_tracks
self.reverb = reverb
self.master_volume = master_volume
# Reorder instruments according to reverb
self.instrument_map = dict()
with_reverb = set(t.instr for t in self.tracks[:self.n_reverb_tracks])
without_reverb = set(t.instr for t in self.tracks[self.n_reverb_tracks:])
instruments_with_reverb = []
instruments_without_reverb = []
for instr in self.instruments:
if instr is None:
continue
self.instrument_map[instr.number] = instr
if instr.number in with_reverb:
if instr.number in without_reverb:
raise InputException("Instrument '%s' is used both with and without reverb" % instr.title)
instruments_with_reverb.append(instr)
if instr.number in without_reverb:
instruments_without_reverb.append(instr)
self.instruments = instruments_with_reverb + instruments_without_reverb
self.n_reverb_instruments = len(instruments_with_reverb)
# Calculate track order and velocity set
self.track_order = []
self.uses_panning = False
for instr in self.instruments:
velocities = set()
instr.columns = 0
instr.latest_note = 0
volume = None
for ti,track in enumerate(self.tracks):
if track.instr == instr.number:
self.track_order.append(ti)
instr.columns += 1
instr.latest_note = max(instr.latest_note, track.latest_note)
track.volume *= self.master_volume
if volume is not None and not track.volume == volume:
raise InputException("Track '%s' has different volume/panning than previous tracks with same instrument" % track.title)
volume = track.volume
for t,v in track.tavs:
velocities.add(v)
instr.volume *= volume
instr.velocities = sorted(list(velocities))
if instr.volume.isPanned():
self.uses_panning = True
quantum = 128
while quantum > 1:
if all(v == 127 or v % quantum == 0 for v in instr.velocities):
break
quantum /= 2
instr.velocity_quantum = quantum
# Calculate longest sample
self.max_maxsamples = 0
self.max_total_samples = 0
self.end_of_sound = 0
for instr in self.instruments:
instr.maxtime = 0
tonecount = dict()
velocitycount = dict()
for ti,track in enumerate(self.tracks):
if track.instr != instr.number:
continue
instr.maxtime = max(instr.maxtime, track.max_length * ticklength)
for note in track.notes:
if note.tone is not None:
tonecount[note.tone] = tonecount[note.tone] + 1 if note.tone in tonecount else 1
velocitycount[note.velocity] = velocitycount[note.velocity] + 1 if note.velocity in velocitycount else 1
instr.tonecount = tonecount
instr.velocitycount = velocitycount
instr.tones = sorted(list(tonecount.keys()))
instr.tonemap = dict()
for i,t in enumerate(instr.tones):
instr.tonemap[t] = i
instr.paramblock = makeParamBlock(instr, self.uses_panning)
instr.end_of_sound = instr.latest_note * ticklength * SAMPLERATE + instr.maxsamples
if instr.number in with_reverb:
instr.end_of_sound += reverb.halftime * 10 * SAMPLERATE
self.max_maxsamples = max(self.max_maxsamples, instr.maxsamples)
self.max_total_samples = max(self.max_total_samples, instr.maxsamples * len(instr.tones))
self.end_of_sound = max(self.end_of_sound, instr.end_of_sound)
self.datainit = None
self.out = None
def dataline(self, data):
if len(data) > 0:
line = self.datainit
first = True
for d in data:
if not first:
line += ","
line += str(d)
first = False
line += "\n"
self.out += line
def comment(self, c):
self.out += "\t; %s\n" % c
def label(self, l):
self.out += "%s:\n" % l
def notelist(self, datafunc, trackterm, prefix):
for ti in self.track_order:
track = self.tracks[ti]
self.comment(track.title)
self.label("%s%s_%d" % (prefix, track.labelname, track.column))
prev_n = None
pat_data = []
for n in track.notes:
if track.singular_length and n.off and n.line > 0:
continue
if prev_n is not None:
pat_data += datafunc(track,prev_n,n)
if not n.off if prev_n is None else n.songpos != prev_n.songpos:
self.dataline(pat_data)
pat_data = []
self.comment("Position %d, pattern %d" % (n.songpos, n.pat))
prev_n = n
pat_data += datafunc(track,prev_n,None)
self.dataline(pat_data)
self.dataline(trackterm)
self.out += "\n"
def lendata(self, t, pn, n):
if n is None:
return []
step = n.line-pn.line
return [-1 - (step >> 8), step & 255] if step > 127 else [step]
def samdata(self, t, pn, n):
if pn.off:
return [0]
return [1 + t.tav_repr[t.notemap[pn]]]
def export(self):
self.datainit = "\tdb\t"
self.out = ""
spt = int(self.ticklength * SAMPLERATE)
total_samples = max((self.length * self.ticklength) * SAMPLERATE, self.end_of_sound)
def roundup(v):
return (int(v) & -0x10000) + 0x10000
global infile
self.out += "; Music converted from %s %s\n" % (infile, str(datetime.datetime.now())[:-7])
self.out += "\n"
self.out += "%%define MUSIC_LENGTH %d\n" % self.length
self.out += "%%define TOTAL_SAMPLES %d\n" % roundup(total_samples)
self.out += "%%define MAX_TOTAL_INSTRUMENT_SAMPLES %d\n" % roundup(self.max_total_samples)
self.out += "\n"
self.out += "%%define SAMPLES_PER_TICK %d\n" % spt
self.out += "%%define TICKS_PER_SECOND %.9f\n" % (1.0 / self.ticklength)
self.out += "\n"
self.out += "%%define NUM_TRACKS_WITH_REVERB %d\n" % self.n_reverb_instruments
self.out += "%%define NUM_TRACKS_WITHOUT_REVERB %d\n" % (len(self.instruments) - self.n_reverb_instruments)
if self.n_reverb_instruments > 0:
reverb = self.reverb
self.out += "\n"
self.out += "%%define REVERB_NUM_DELAYS %d\n" % reverb.p_num_delays
self.out += "%%define REVERB_MIN_DELAY %d\n" % reverb.p_min_delay
self.out += "%%define REVERB_MAX_DELAY %d\n" % reverb.p_max_delay
self.out += "%%define REVERB_ADD_DELAY %d\n" % reverb.p_add_delay
self.out += "%%define REVERB_RANDOMSEED %d\n" % reverb.p_seed
self.out += "%%define REVERB_MAX_DECAY %.9f\n" % reverb.p_max_decay
self.out += "%%define REVERB_DECAY_MUL %.9f\n" % reverb.p_decay_mul
self.out += "%%define REVERB_FILTER_HIGH 0x%08X ; %.9f\n" % (f2i(reverb.p_filter_high), reverb.p_filter_high)
self.out += "%%define REVERB_FILTER_LOW 0x%08X ; %.9f\n" % (f2i(reverb.p_filter_low), reverb.p_filter_low)
self.out += "%%define REVERB_DAMPEN_HIGH 0x%08X ; %.9f\n" % (f2i(reverb.p_dampen_high), reverb.p_dampen_high)
self.out += "%%define REVERB_DAMPEN_LOW 0x%08X ; %.9f\n" % (f2i(reverb.p_dampen_low), reverb.p_dampen_low)
self.out += "%%define REVERB_VOLUME_LEFT 0x%08X ; %.9f\n" % (f2i(reverb.p_volumes[0]), reverb.p_volumes[0])
self.out += "%%define REVERB_VOLUME_RIGHT 0x%08X ; %.9f\n" % (f2i(reverb.p_volumes[1]), reverb.p_volumes[1])
if self.uses_panning:
self.out += "\n%define USES_PANNING\n"
# Instrument parameters
self.out += "\n\n\tsection iparam data align=4\n"
self.out += "\n_InstrumentParams:\n"
for instr in self.instruments:
self.label(".i%02d" % instr.number)
self.comment(instr.title)
self.out += "\tdd\t"
first = True
for p in instr.paramblock:
if not first:
self.out += ","
if isinstance(p, float):
self.out += "0x%08X" % f2i(p)
else:
self.out += "%d" % p
first = False
self.out += "\n"
self.out += "\n"
# Instrument tones
self.out += "\n\n\tsection itones data align=1\n"
self.out += "\n_InstrumentTones:\n"
for instr in self.instruments:
self.label(".i%02d" % instr.number)
self.comment(instr.title)
self.out += "\tdb\t"
prev_tone = 0
for tone in instr.tones:
self.out += str(tone - prev_tone) + ","
prev_tone = tone
first = False
self.out += "%d\n" % (-129 + instr.columns)
# Track data
self.out += "\n\n\tsection trdata data align=1\n"
self.out += "\n_TrackData:\n"
for ti in self.track_order:
track = self.tracks[ti]
instr = self.instrument_map[track.instr]
self.label(".t_%s_%d" % (track.labelname, track.column))
self.comment(track.title)
# List tones and velocities
tavdata = [track.singular_length if track.singular_length else 0]
prev_tone_id = 0
for t,v in track.tavs:
tone_id = instr.tonemap[t]
vol = (v + instr.velocity_quantum / 2) / instr.velocity_quantum
tavdata += [tone_id - prev_tone_id, vol]
prev_tone_id = tone_id
tavdata += [-128]
self.dataline(tavdata)
# Lengths of notes
self.out += "\n\tsection notelen data align=1\n"
self.out += "\n_NoteLengths:\n"
self.notelist(self.lendata, [0], "L_")
# Samples for notes
self.out += "\n\tsection notesamp data align=1\n"
self.out += "\n_NoteSamples:\n"
self.notelist(self.samdata, [], "S_")
return self.out
def makeDeltas(self, init_delta, lines_per_beat):
beats_per_line = 1.0/lines_per_beat
deltas = []
for t in self.tracks:
tdeltas = []
delta = init_delta
note_i = 0
for p in range(0, self.length):
while t.notes[note_i].line <= p:
if not t.notes[note_i].off:
delta = p * beats_per_line
note_i += 1
tdeltas.append(delta)
deltas.append(tdeltas)
return deltas
def extractTrackNotes(xsong, tr, column):
outside_pattern = 0
xsequence = xsong.PatternSequence.PatternSequence
if not xsequence:
xsequence = xsong.PatternSequence.SequenceEntries.SequenceEntry
xpatterns = xsong.PatternPool.Patterns.Pattern
tname = str(xsong.Tracks.SequencerTrack[tr].Name)
notes = []
pattern_top = 0
prev_instr = None
for posn,xseq in enumerate(xsequence):
patn = int(xseq.Pattern)
xpat = xpatterns[patn]
nlines = int(xpat.NumberOfLines)
if tr in [int(xmt) for xmt in xseq.MutedTracks.MutedTrack]:
off = Note(tname, column, pattern_top, posn, patn, 0, "OFF", None, 127)
notes.append(off)
else:
xtrack = xpat.Tracks.PatternTrack[tr]
for xline in xtrack.Lines.Line:
index = int(xline("index"))
if index < nlines:
line = pattern_top + index
xcol = xline.NoteColumns.NoteColumn[column - 1]
if xcol.Note and str(xcol.Note) != "---":
instr = str(xcol.Instrument)
if instr == ".." and str(xcol.Note) != "OFF":
if prev_instr is None:
raise InputException("Track '%s' column %d pattern %d line %d: Unspecified instrument" % (tname, column, patn, index))
instr = prev_instr
prev_instr = instr
note = Note(tname, column, line, posn, patn, index, xcol.Note, instr, xcol.Volume)
notes.append(note)
if (note.velocity == 0 or note.velocity > 127) and not note.off:
raise InputException("Track '%s' column %d pattern %d line %d: Illegal velocity value" % (tname, column, patn, index))
# Check for illegal uses of panning, delay and effect columns
def checkColumn(x, allow_zero, msg):
if x and not (str(x) == "" or str(x) == ".." or (allow_zero and str(x) == "00")):
raise InputException("Track '%s' column %d pattern %d line %d: %s" % (tname, column, patn, index, msg))
checkColumn(xcol.Panning, False, "Panning column used")
checkColumn(xcol.Delay, True, "Delay column used")
for xeff in xline.EffectColumns.EffectColumn.Number:
checkColumn(xeff, True, "Effect column used")
else:
outside_pattern += 1
pattern_top += nlines
# Add inital OFF and remove redundant OFFs
if len(notes) > 0 and notes[0].line == 0:
notes2 = []
off = False
else:
notes2 = [Note(tname, column, 0, 0, int(xsequence[0].Pattern), 0, "OFF", 0, 127)]
off = True
for n in notes:
if n.off:
if not off:
notes2.append(n)
off = True
else:
notes2.append(n)
off = False
if outside_pattern > 0:
print " * Track '%s': %d note%s outside patterns ignored" % (tname, outside_pattern, "s" * (outside_pattern > 1))
return notes2
def pickupReverb(xdevices, reverb, tname, ticklength):
xplugin = xdevices.AudioPluginDevice
if len(xplugin) > 1:
raise InputException("Track '%s' has more than one plugin device" % tname);
if isactive(xplugin):
plugin_id = str(xplugin.PluginIdentifier)
if plugin_id not in ["MetaEffect", "OidosReverb"]:
raise InputException("Track '%s' has an unknown plugin device" % tname);
params = [float(x) for x in xplugin.Parameters.Parameter.Value]
new_reverb = Reverb(params)
if reverb is not None and not new_reverb == reverb:
raise InputException("Track '%s' has different reverb from an earlier track" % tname);
reverb = new_reverb
return reverb
def makeTracks(xsong, ticklength):
instruments = []
reverb_tracks = []
non_reverb_tracks = []
reverb = None
for ii,xinst in enumerate(xsong.Instruments.Instrument):
params = [float(v) for v in instplugins(xinst).PluginDevice.Parameters.Parameter.Value]
if params:
legacy = str(instplugins(xinst).PluginDevice.PluginIdentifier) == "MetaSynth"
instrument = Instrument(ii, str(xinst.Name), params, legacy)
instrument.volume = makeVolume(instplugins(xinst).Volume)
instruments.append(instrument)
else:
instruments.append(None)
for xgrouptrack in xsong.Tracks.SequencerGroupTrack:
tname = xgrouptrack.Name
xdevices = xgrouptrack.FilterDevices.Devices
for xmixer in xdevices.GroupTrackMixerDevice:
if isactive(xmixer):
if float(xmixer.Volume.Value) != 1.0 or float(xmixer.PostVolume.Value) != 1.0:
raise InputException("Group track '%s' has non-zero volume" % tname);
if float(xmixer.Panning.Value) != 0.5 or float(xmixer.PostPanning.Value) != 0.5:
raise InputException("Group track '%s' has non-center panning" % tname);
if isactive(xdevices.SendDevice):
raise InputException("Group track '%s' uses Send" % tname);
if isactive(xdevices.AudioPluginDevice):
raise InputException("Group track '%s' uses reverb" % tname);
for tr,xtrack in enumerate(xsong.Tracks.SequencerTrack):
if str(xtrack.State) != "Active":
continue
tname = str(xtrack.Name)
ncols = int(xtrack.NumberOfVisibleNoteColumns)
xdevices = xtrack.FilterDevices.Devices
xdevice = xdevices.SequencerTrackDevice
if not xdevice:
xdevice = xdevices.TrackMixerDevice
volume = makeVolume(xdevice.Volume.Value)
volume *= makePanning(xdevice.Panning.Value)
volume *= makeVolume(xdevice.PostVolume.Value)
volume *= makePanning(xdevice.PostPanning.Value)
while isactive(xdevices.SendDevice):
if isactive(xdevices.AudioPluginDevice):
raise InputException("Track '%s' uses both reverb and send" % tname);
if str(xdevices.SendDevice.MuteSource) != "true":
raise InputException("Track '%s' uses send without Mute Source" % tname);
volume *= makeVolume(xdevices.SendDevice.SendAmount.Value)
volume *= makePanning(xdevices.SendDevice.SendPan.Value)
dest = int(float(xdevices.SendDevice.DestSendTrack.Value))
xdevices = xsong.Tracks.SequencerSendTrack[dest].FilterDevices.Devices
xdevice = xdevices.SequencerSendTrackDevice
if not xdevice:
xdevice = xdevices.SendTrackMixerDevice
volume *= makeVolume(xdevice.Volume.Value)
volume *= makePanning(xdevice.Panning.Value)
volume *= makeVolume(xdevice.PostVolume.Value)
volume *= makePanning(xdevice.PostPanning.Value)
for column in range(1, ncols + 1):
if str(xtrack.NoteColumnStates.NoteColumnState[column - 1]) != "Active":
continue
notes = extractTrackNotes(xsong, tr, column)
track_instrs = []
for note in notes:
if not note.off:
instr = instruments[note.instr]
if instr is None:
raise InputException("Track '%s' column %d pattern %d line %d: Undefined instrument (%d)" % (tname, column, note.pat, note.patline, note.instr));
if note.instr not in track_instrs:
track_instrs.append(note.instr)
for instr in track_instrs:
track = Track(tr, column, tname, notes, volume, instruments)
if isactive(xdevices.AudioPluginDevice):
reverb_tracks.append(track)
else:
non_reverb_tracks.append(track)
reverb = pickupReverb(xdevices, reverb, tname, ticklength)
for xtrack in xsong.Tracks.SequencerSendTrack:
xdevices = xtrack.FilterDevices.Devices
if xdevices.AudioPluginDevice:
reverb = pickupReverb(xdevices, reverb, tname, ticklength)
return (reverb_tracks + non_reverb_tracks), len(reverb_tracks), reverb, instruments
def makeMusic(xsong):
xgsd = xsong.GlobalSongData
if xgsd.PlaybackEngineVersion and int(xgsd.PlaybackEngineVersion) >= 4:
lines_per_minute = float(xgsd.BeatsPerMin) * float(xgsd.LinesPerBeat)
print "New timing format: %d ticks per minute" % lines_per_minute
else:
lines_per_minute = float(xgsd.BeatsPerMin) * 24.0 / float(xgsd.TicksPerLine)
print "Old timing format: %d ticks per minute" % lines_per_minute
ticklength = 60.0 / lines_per_minute
print
tracks,n_reverb_tracks,reverb,instruments = makeTracks(xsong, ticklength)
xpositions = xsong.PatternSequence.PatternSequence.Pattern
if not xpositions:
xpositions = xsong.PatternSequence.SequenceEntries.SequenceEntry.Pattern
xpatterns = xsong.PatternPool.Patterns.Pattern
length = 0
for xpos in xpositions:
patn = int(xpos)
xpat = xpatterns[patn]
nlines = int(xpat.NumberOfLines)
length += nlines
xmstdev = xsong.Tracks.SequencerMasterTrack.FilterDevices.Devices.SequencerMasterTrackDevice
if not xmstdev:
xmstdev = xsong.Tracks.SequencerMasterTrack.FilterDevices.Devices.MasterTrackMixerDevice
master_volume = makeVolume(xmstdev.Volume.Value)
master_volume *= makePanning(xmstdev.Panning.Value)
master_volume *= makeVolume(xmstdev.PostVolume.Value)
master_volume *= makePanning(xmstdev.PostPanning.Value)
return Music(tracks, instruments, length, ticklength, n_reverb_tracks, reverb, master_volume)
def printMusicStats(music, ansi):
def form(s):
f = ""
for c in s:
if c == 'T':
f += "\033[33m%s\033[0m" if ansi else "%s"
elif c == 'V':
f += "\033[31m%02X\033[0m" if ansi else "%02X"
elif c == 'L':
f += "\033[36m%d\033[0m" if ansi else "%d"
elif c == 'N':
f += ":%d" if ansi else ":%d"
elif c == 'H':
f += "\033[32m%s\033[0m" if ansi else "%s"
elif c == 'X':
f += "\033[34mx\033[0m" if ansi else "x"
elif c == 'I':
f += "\033[31m(%d bits)\033[0m" if ansi else "(%d bits)"
elif c == 'B':
f += "\033[35m%.f\033[0m" if ansi else "%.f"
elif c == 'D':
f += "\033[35m%dm%02ds\033[0m" if ansi else "%dm%02ds"
else:
f += c
return f
print "Music length: %d ticks at %0.2f ticks per minute" % (music.length, 60.0 / music.ticklength)
max_memory_mb = 0
max_memory_instr = None
total_burden = 0
ii = None
for ti,tn in enumerate(music.track_order):
if music.n_reverb_tracks > 0 and ti == 0:
print
print "Tracks with reverb:"
if music.n_reverb_tracks > 0 and ti == music.n_reverb_tracks:
print
print "Tracks without reverb:"
track = music.tracks[tn]
if track.instr != ii:
print
ii = track.instr
instr = music.instrument_map[ii]
modes = instr.paramblock[0]
fat = instr.paramblock[1]
longest = float(instr.paramblock[16]) / SAMPLERATE
memory = len(instr.tones) * longest
memory_mb = memory * SAMPLERATE * 16 / 1024 / 1024
if memory_mb > max_memory_mb:
max_memory_mb = memory_mb
max_memory_instr = instr.title
burden = modes * fat * len(instr.tones) * longest
total_burden += burden
print form("H") % instr.title
print " Memory: " + form(" tones X longest = %d X %.3f = B (B %s)") % (len(instr.tones), longest, memory, memory_mb, "MB")
print " Burden: " + form(" modes X fat X tones X longest = %d X %d X %d X %.3f = B") % (modes, fat, len(instr.tones), longest, burden)
tones = ""
for t in instr.tones:
tones += form(" TN") % (notename(t), instr.tonecount[t])
print " Tones: " + tones
velocities = ""
for v in instr.velocities:
velocities += form(" VN") % (v, instr.velocitycount[v])
vbits = int(round(math.log(128 / instr.velocity_quantum, 2)))
print " Velocities:" + velocities + form(" I") % vbits
v2 = track.volume * track.volume
volume = math.log((v2.left + v2.right) / 2, 10) * 10
panning = round(v2.right / (v2.left + v2.right) * 100) - 50
ptext = "Center" if panning == 0 else "%d L" % -panning if panning < 0 else "%d R" % panning
print " Volume: %+.1f dB %s" % (volume, ptext)
print form(" H") % track.title
lengths = ""
for l in sorted(track.note_lengths.keys()):
lengths += form(" LN") % (l, track.note_lengths[l])
print " Lengths: " + lengths
tnotes = ""
for t,v in track.tavs:
num_notes = 0
for n in [note for note in track.notes if not note.off and note.instr == track.instr]:
if track.notemap[n] == (t,v):
num_notes += 1
tnotes += form(" T/VN") % (notename(t), v, num_notes)
print " Notes: " + tnotes
seconds = int(round(total_burden / 5000))
print
print "Maximum instrument memory: " + form("B %s (H)") % (max_memory_mb, "MB", max_memory_instr)
print "Total burden: " + form("B (approximately D on a fast %s)") % (total_burden, seconds / 60, seconds % 60, "CPU")
def writefile(filename, s):
f = open(filename, "wb")
f.write(s)
f.close()
print "Wrote file %s" % filename
ansi = False
files = []
for a in sys.argv[1:]:
if a == "-ansi":
ansi = True
else:
files.append(a)
if len(files) != 2:
print "Usage: %s [-ansi]