987 lines
38 KiB
Python
987 lines
38 KiB
Python
# Writes the sizzle reel's music: an original FastTracker II module (.xm) in the idiom of the 90s
|
|
# PC demoscene, generated rather than sourced, so it is ours outright and carries no licence of
|
|
# anyone else's. The engine plays XM natively through libxmp (scene 99 exercises the format), so
|
|
# the reel can run as a real Singe script with this as its soundtrack instead of being rendered.
|
|
#
|
|
# python3 util/reelMusic.py [<output.xm>] (default: .builddir/reelMusic.xm)
|
|
#
|
|
# Nothing here is sampled from a record or lifted from another module: every instrument is
|
|
# synthesised a sample at a time, and the patterns are written a row at a time. It is hard rock --
|
|
# distorted guitars doubled hard left and right, a kit hit like a kit, and a riff in E rather than
|
|
# an arpeggio in A.
|
|
#
|
|
# What keeps it from sounding like a chiptune is where the work goes. The drums are 16 bit at
|
|
# 22050 Hz, each built as a transient, a body, and a tail and then soft clipped together, which is
|
|
# what punch is. The guitars are a power chord distorted as a chord, root, fifth and octave through
|
|
# one saturator, so the intermodulation between them is real rather than three clean notes played at
|
|
# once; then a cabinet: high pass, low pass, and a presence bump.
|
|
#
|
|
# The trap in baking a filter into a sample: the filter travels with the pitch. A cabinet designed
|
|
# at the rate the data was generated lands an octave and a half lower when the sample is played at
|
|
# low E, and the chord comes out muffled. So every filter here is designed against the rate the
|
|
# data will really play at -- 6922 Hz for a riff on the bottom two strings, 3461 Hz for the bass,
|
|
# 22050 Hz for a drum struck at C-4 -- which is what the RATE arguments below are for.
|
|
#
|
|
# The format is FastTracker II 1.04: a 336 byte header with its order table, then the patterns as
|
|
# packed rows, then the instruments each carrying their samples with the data delta encoded. A
|
|
# sample rendered at 22050 Hz needs relative note +17 and finetune -28 for C-4 to play it back at
|
|
# the rate it was made at, which is what tuning() works out.
|
|
import math
|
|
import os
|
|
import struct
|
|
import sys
|
|
import wave
|
|
|
|
|
|
BASE = 8363.0 # What C-4 plays a sample at with the linear frequency table.
|
|
RATE = 22050 # What the drums are rendered at: real rate, real air.
|
|
TONE_RATE = 21977 # The tuned instruments: 84 samples a cycle at concert C-4, exactly.
|
|
TONE_CYCLE = 84
|
|
GUITAR_RATE = 6922 # What the data plays at on low E, which is where the riff lives.
|
|
BASS_RATE = 3461 # An octave below that, where the bass guitar lives.
|
|
LEAD_RATE = 13844 # An octave above, where a solo sits.
|
|
ROWS = 128 # Rows to a pattern: four bars at eight rows to the beat.
|
|
CHANNELS = 14
|
|
SPEED = 3 # Ticks a row.
|
|
BPM = 150 # Arena tempo: fast enough to lift, slow enough to ring.
|
|
HEADER_SIZE = 276
|
|
INSTRUMENT_HEADER = 263
|
|
SAMPLE_HEADER = 40
|
|
PEAK = 30000 # Where a sample's loudest point sits in 16 bits, leaving headroom.
|
|
|
|
# The channels, laid out like a band: a kit, a bass, two rhythm guitars and a lead.
|
|
KICK, SNARE, HAT, CRASH, TOM, BASS, LEFT, RIGHT, CHUG, LEAD, HARM, OCT, STAB, FX = range(CHANNELS)
|
|
|
|
# Where each sits. The two rhythm guitars go hard left and hard right, which is the sound of every
|
|
# rock record ever made: the same part played twice and thrown to the walls.
|
|
PANNING = { KICK: 0x80, SNARE: 0x80, HAT: 0x60, CRASH: 0xA8, TOM: 0x70, BASS: 0x80,
|
|
LEFT: 0x18, RIGHT: 0xE8, CHUG: 0x78, LEAD: 0x90, HARM: 0x64, OCT: 0x88,
|
|
STAB: 0xC0, FX: 0x40 }
|
|
|
|
NOTES = { "C": 0, "C#": 1, "D": 2, "D#": 3, "E": 4, "F": 5, "F#": 6, "G": 7, "G#": 8, "A": 9, "A#": 10, "B": 11 }
|
|
NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
|
|
|
|
|
|
def note(name):
|
|
if name is None:
|
|
return 0
|
|
return 1 + int(name[-1]) * 12 + NOTES[name[:-1]]
|
|
|
|
|
|
def transpose(name, semitones):
|
|
value = note(name) - 1 + semitones
|
|
|
|
return "%s%d" % (NAMES[value % 12], value // 12)
|
|
|
|
|
|
# The relative note and finetune that make C-4 play a sample back at the rate it was rendered at.
|
|
def tuning(rate):
|
|
exact = 12.0 * math.log(rate / BASE, 2)
|
|
rel = int(round(exact))
|
|
|
|
return rel, int(round((exact - rel) * 128))
|
|
|
|
|
|
# ===== Real samples ===========================================================================
|
|
#
|
|
# If util/reelSamples.py has prepared the FreePats sound banks, the module is built from real
|
|
# recordings: a Fender through an amplifier rack (CC0), a Yamaha bass (CC0), and a real kit
|
|
# (CC BY 4.0, so a reel using it owes Bjorn Muldjord a credit). With no samples there, everything
|
|
# falls back to the synthesised set below and the module owes nobody anything.
|
|
|
|
SAMPLES = os.environ.get("REEL_SAMPLES", "")
|
|
REALRATE = 32000
|
|
|
|
|
|
def loadWave(path):
|
|
with wave.open(path, "rb") as handle:
|
|
frames = handle.readframes(handle.getnframes())
|
|
|
|
return list(struct.unpack("<%dh" % (len(frames) // 2), frames)), handle.getframerate()
|
|
|
|
|
|
# What relative note and finetune make the tracker play a sample at its own rate when the written
|
|
# note is the one it was recorded at. MIDI 60 is C-4, which the format calls note 49.
|
|
def sampleTuning(rate, midi):
|
|
exact = 12.0 * math.log(rate / BASE, 2) - (midi - 60)
|
|
rel = int(round(exact))
|
|
|
|
return rel, int(round((exact - rel) * 128))
|
|
|
|
|
|
# ===== Synthesis ==============================================================================
|
|
|
|
class Noise:
|
|
# Deterministic, so two runs of this program write the same file.
|
|
def __init__(self, seed=12345):
|
|
self.state = seed
|
|
|
|
def next(self):
|
|
self.state = (self.state * 1103515245 + 12345) & 0x7FFFFFFF
|
|
|
|
return ((self.state >> 11) % 4096 - 2048) / 2048.0
|
|
|
|
|
|
def saturate(value, drive=1.0):
|
|
# The soft clip that turns level into loudness: it rounds the top off a transient instead of
|
|
# squaring it, and the harmonics it adds are what make a kick audible on a small speaker.
|
|
return math.tanh(value * drive) / math.tanh(drive) if drive > 0 else value
|
|
|
|
|
|
def normalise(samples, peak=PEAK):
|
|
loudest = max(abs(v) for v in samples) or 1.0
|
|
|
|
return [int(max(-32768, min(32767, round(v * peak / loudest)))) for v in samples]
|
|
|
|
|
|
def lowpass(samples, cut, rate=RATE, passes=1):
|
|
# One pole, run as many times as the slope wants. Cheap, and the right shape for drums.
|
|
alpha = 1.0 - math.exp(-2.0 * math.pi * cut / rate)
|
|
for _ in range(passes):
|
|
out = []
|
|
last = 0.0
|
|
for value in samples:
|
|
last += alpha * (value - last)
|
|
out.append(last)
|
|
samples = out
|
|
|
|
return samples
|
|
|
|
|
|
def highpass(samples, cut, rate=RATE):
|
|
low = lowpass(samples, cut, rate)
|
|
|
|
return [value - low[i] for i, value in enumerate(samples)]
|
|
|
|
|
|
def bandpass(samples, low, high, rate=RATE):
|
|
return lowpass(highpass(samples, low, rate), high, rate, 2)
|
|
|
|
|
|
def envelope(length, attack, decay, hold=0.0):
|
|
# Attack in samples, then a hold, then an exponential decay: the shape every drum has.
|
|
out = []
|
|
for i in range(length):
|
|
if i < attack:
|
|
out.append(i / float(max(1, attack)))
|
|
elif i < attack + hold:
|
|
out.append(1.0)
|
|
else:
|
|
out.append(math.exp(-(i - attack - hold) / float(max(1, decay))))
|
|
|
|
return out
|
|
|
|
|
|
# The kick: a sine whose pitch falls off a cliff, a sub that hangs on under it, and a click on the
|
|
# front so it reads on a speaker that cannot make 50 Hz. Soft clipped together, which is the punch.
|
|
def kick(length=None):
|
|
length = length or int(0.42 * RATE)
|
|
noise = Noise(7)
|
|
body = []
|
|
phase = 0.0
|
|
for i in range(length):
|
|
t = i / float(RATE)
|
|
pitch = 47.0 + 190.0 * math.exp(-t * 75.0) # 237 Hz down to 47 in about 14 ms.
|
|
phase += 2.0 * math.pi * pitch / RATE
|
|
body.append(math.sin(phase) * math.exp(-t * 13.0))
|
|
sub = []
|
|
phase = 0.0
|
|
for i in range(length):
|
|
t = i / float(RATE)
|
|
phase += 2.0 * math.pi * 46.0 / RATE
|
|
sub.append(math.sin(phase) * math.exp(-t * 5.5))
|
|
click = lowpass([noise.next() for _ in range(length)], 4200)
|
|
shape = envelope(length, 2, int(0.0012 * RATE))
|
|
out = []
|
|
for i in range(length):
|
|
mix = body[i] * 1.0 + sub[i] * 0.9 + click[i] * shape[i] * 1.2
|
|
out.append(saturate(mix, 3.0))
|
|
|
|
return normalise(out)
|
|
|
|
|
|
# The snare: a shell that rings at two pitches, the wires as bandpassed noise, a bright transient,
|
|
# and a short tail so it sounds like a drum in a room rather than a burst of static.
|
|
def snare(length=None, bright=1.0):
|
|
length = length or int(0.34 * RATE)
|
|
noise = Noise(31)
|
|
hiss = bandpass([noise.next() for _ in range(length)], 400, 7000 * bright)
|
|
tail = bandpass([noise.next() for _ in range(length)], 900, 3000)
|
|
out = []
|
|
for i in range(length):
|
|
t = i / float(RATE)
|
|
shell = (math.sin(2.0 * math.pi * 182.0 * t) + 0.7 * math.sin(2.0 * math.pi * 276.0 * t)) * math.exp(-t * 30.0)
|
|
wires = hiss[i] * math.exp(-t * 19.0)
|
|
room = tail[i] * math.exp(-t * 7.0) * 0.35
|
|
crack = hiss[i] * math.exp(-t * 900.0) * 1.6
|
|
out.append(saturate(shell * 0.55 + wires * 1.1 + room + crack, 1.6))
|
|
|
|
return normalise(out)
|
|
|
|
|
|
# The clap: four bursts a few milliseconds apart and then the tail, which is the whole trick.
|
|
def clap(length=None):
|
|
length = length or int(0.30 * RATE)
|
|
noise = Noise(57)
|
|
raw = bandpass([noise.next() for _ in range(length)], 700, 5200)
|
|
out = [0.0] * length
|
|
for offset in (0, int(0.009 * RATE), int(0.018 * RATE), int(0.026 * RATE)):
|
|
gain = 1.0 - offset / float(length) * 1.5
|
|
for i in range(offset, min(length, offset + int(0.02 * RATE))):
|
|
out[i] += raw[i] * math.exp(-(i - offset) / (0.004 * RATE)) * gain
|
|
for i in range(length):
|
|
t = i / float(RATE)
|
|
out[i] = saturate(out[i] + raw[i] * math.exp(-t * 11.0) * 0.5, 1.4)
|
|
|
|
return normalise(out)
|
|
|
|
|
|
def hat(length=None, decay=45.0, cut=9000.0, seed=97):
|
|
length = length or int(0.07 * RATE)
|
|
noise = Noise(seed)
|
|
raw = highpass([noise.next() for _ in range(length)], cut)
|
|
out = []
|
|
for i in range(length):
|
|
t = i / float(RATE)
|
|
out.append(saturate(raw[i] * math.exp(-t * decay), 1.2))
|
|
|
|
return normalise(out, int(PEAK * 0.8))
|
|
|
|
|
|
def crash(length=None):
|
|
length = length or int(1.6 * RATE)
|
|
noise = Noise(181)
|
|
raw = highpass([noise.next() for _ in range(length)], 3500)
|
|
out = []
|
|
for i in range(length):
|
|
t = i / float(RATE)
|
|
# Two decays: the strike, then the wash.
|
|
out.append(raw[i] * (math.exp(-t * 14.0) * 0.8 + math.exp(-t * 2.0) * 0.5))
|
|
|
|
return normalise(out, int(PEAK * 0.85))
|
|
|
|
|
|
def tom(pitch=150.0, length=None):
|
|
length = length or int(0.22 * RATE)
|
|
noise = Noise(211)
|
|
raw = bandpass([noise.next() for _ in range(length)], 200, 2500)
|
|
out = []
|
|
phase = 0.0
|
|
for i in range(length):
|
|
t = i / float(RATE)
|
|
f = pitch * (1.0 + 0.5 * math.exp(-t * 40.0))
|
|
phase += 2.0 * math.pi * f / RATE
|
|
out.append(saturate(math.sin(phase) * math.exp(-t * 13.0) + raw[i] * math.exp(-t * 60.0) * 0.5, 1.5))
|
|
|
|
return normalise(out)
|
|
|
|
|
|
# ----- The guitars ----------------------------------------------------------------------------
|
|
#
|
|
# A power chord is not three notes distorted, it is three notes distorted together: the saturator
|
|
# multiplies them against each other and the sum and difference tones it makes are most of what a
|
|
# heavy guitar is. So the root, the fifth and the octave are summed first and clipped once. The
|
|
# loop is sixteen cycles of the root, which is twenty-four of the fifth and thirty-two of the
|
|
# octave, so every component closes and the seam is silent.
|
|
|
|
def cabinet(samples, rate, low=95.0, high=4300.0, presence=0.45):
|
|
# A speaker in a box: nothing under the low corner, little over the high one, and a lift where
|
|
# a guitar speaker lifts. Designed against the rate the sample will play at, not be made at.
|
|
body = lowpass(highpass(samples, low, rate), high, rate, 2)
|
|
shine = bandpass(samples, 1500.0, 3200.0, rate)
|
|
|
|
return [value + shine[i] * presence for i, value in enumerate(body)]
|
|
|
|
|
|
def strings(ratios, cycles, drive, rate, weights=None, peak=PEAK):
|
|
length = cycles * TONE_CYCLE
|
|
raw = []
|
|
for i in range(length):
|
|
phase = i / float(TONE_CYCLE)
|
|
where = i / float(length)
|
|
value = 0.0
|
|
for n, ratio in enumerate(ratios):
|
|
gain = (weights or [1.0] * len(ratios))[n]
|
|
# A string is a saw with the top rolled off and a little life in the phase.
|
|
for harmonic in range(1, 13):
|
|
drift = 0.008 * math.sin(2.0 * math.pi * (where + n * 0.3 + harmonic * 0.05))
|
|
value += gain * math.sin(2.0 * math.pi * harmonic * ratio * (phase + drift)) / (harmonic ** 1.1)
|
|
raw.append(value)
|
|
hot = [saturate(v, drive) for v in raw]
|
|
|
|
return normalise(cabinet(hot, rate), peak), length
|
|
|
|
|
|
# The riff guitar: root, fifth, octave, driven hard.
|
|
def powerChord():
|
|
return strings([1.0, 1.5, 2.0], 16, 9.0, GUITAR_RATE, [1.0, 0.85, 0.6])
|
|
|
|
|
|
# The same chord with the gain backed off, for the parts that want to be heard through rather than
|
|
# over: the intro and the verses.
|
|
def cleanerChord():
|
|
return strings([1.0, 1.5, 2.0], 16, 4.0, GUITAR_RATE, [1.0, 0.8, 0.55])
|
|
|
|
|
|
# One note for the lead, distorted enough to sing and sustain.
|
|
def leadGuitar():
|
|
return strings([1.0, 2.0], 32, 7.0, LEAD_RATE, [1.0, 0.25])
|
|
|
|
|
|
# The bass guitar: a pick attack on the front, the string under it, and just enough grit.
|
|
def bassGuitar():
|
|
return strings([1.0, 2.0, 3.0], 16, 2.6, BASS_RATE, [1.0, 0.4, 0.15])
|
|
|
|
|
|
# ----- The kit --------------------------------------------------------------------------------
|
|
#
|
|
# Hit like a kit rather than programmed like a drum machine: the kick is a thud with a beater on it
|
|
# instead of a sub sine, the snare has a shell and a room, and the cymbals are struck.
|
|
|
|
def rockKick(length=None):
|
|
length = length or int(0.30 * RATE)
|
|
noise = Noise(7)
|
|
beater = lowpass([noise.next() for _ in range(length)], 5200)
|
|
out = []
|
|
phase = 0.0
|
|
for i in range(length):
|
|
t = i / float(RATE)
|
|
pitch = 58.0 + 120.0 * math.exp(-t * 90.0) # A shorter, higher drop than a dance kick.
|
|
phase += 2.0 * math.pi * pitch / RATE
|
|
body = math.sin(phase) * math.exp(-t * 17.0)
|
|
skin = math.sin(2.0 * math.pi * 210.0 * t) * math.exp(-t * 60.0) * 0.35
|
|
click = beater[i] * math.exp(-t * 320.0) * 1.3
|
|
out.append(saturate(body + skin + click, 2.4))
|
|
|
|
return normalise(out)
|
|
|
|
|
|
def rockSnare(length=None):
|
|
length = length or int(0.40 * RATE)
|
|
noise = Noise(31)
|
|
wires = bandpass([noise.next() for _ in range(length)], 500, 8500)
|
|
room = bandpass([noise.next() for _ in range(length)], 700, 4000)
|
|
out = []
|
|
for i in range(length):
|
|
t = i / float(RATE)
|
|
shell = (math.sin(2.0 * math.pi * 198.0 * t) + 0.65 * math.sin(2.0 * math.pi * 301.0 * t)) * math.exp(-t * 26.0)
|
|
out.append(saturate(shell * 0.7 + wires[i] * math.exp(-t * 16.0) * 1.15
|
|
+ room[i] * math.exp(-t * 5.5) * 0.45
|
|
+ wires[i] * math.exp(-t * 700.0) * 1.7, 1.7))
|
|
|
|
return normalise(out)
|
|
|
|
|
|
def ride(length=None):
|
|
length = length or int(0.9 * RATE)
|
|
noise = Noise(151)
|
|
wash = highpass([noise.next() for _ in range(length)], 4000)
|
|
out = []
|
|
for i in range(length):
|
|
t = i / float(RATE)
|
|
ping = math.sin(2.0 * math.pi * 1180.0 * t) * math.exp(-t * 26.0) * 0.5
|
|
out.append(saturate(ping + wash[i] * (math.exp(-t * 18.0) * 0.7 + math.exp(-t * 3.0) * 0.35), 1.3))
|
|
|
|
return normalise(out, int(PEAK * 0.85))
|
|
|
|
|
|
|
|
class Instrument:
|
|
def __init__(self, name, samples, loop, rate, volume=64, envelope=None, sustain=None, fadeout=0, midi=None):
|
|
self.name = name
|
|
self.samples = samples
|
|
self.loop = loop
|
|
self.volume = volume
|
|
self.envelope = envelope or []
|
|
self.sustain = sustain
|
|
self.fadeout = fadeout
|
|
self.relative, self.finetune = tuning(rate) if midi is None else sampleTuning(rate, midi)
|
|
|
|
|
|
# The guitars a real set carries, as (name, file, MIDI note it was recorded at). A riff note is
|
|
# played by whichever of them was recorded nearest it, so nothing is stretched further than a few
|
|
# semitones and the amplifier's own character survives.
|
|
GUITARS = [("gtrC2", 36), ("gtrE2", 40), ("gtrA2", 45), ("gtrD3", 50), ("gtrE3", 52), ("gtrG3", 55),
|
|
("gtrB3", 59), ("gtrCs4", 61), ("gtrE4", 64), ("gtrG4", 67), ("gtrB4", 71), ("gtrC5", 72),
|
|
("gtrD5", 74)]
|
|
# The softer takes of the same guitar, by the note they were recorded at.
|
|
SOFT = [("sftC2", 36), ("sftE2", 40), ("sftA2", 45), ("sftG3", 55), ("sftB3", 59), ("sftD5", 74)]
|
|
|
|
# Filled by instruments(): which instrument name to use for a written note, and whether the set
|
|
# being used is the real one.
|
|
CHOICE = {}
|
|
REAL = False
|
|
|
|
|
|
# Every take of each drum and each guitar note, in the order to rotate through them.
|
|
TAKES = {
|
|
"kick": ("kick", "kickB", "kickC"),
|
|
"kickMid": ("kickMid", "kickMidB"),
|
|
"snare": ("snare", "snareB", "snareC", "snareD"),
|
|
"hat": ("hat", "hatB", "hatC", "hatD"),
|
|
"crash": ("crash", "crashB"),
|
|
"tom": ("tom", "tomB"),
|
|
"gtrE2": ("gtrE2", "gtrE2b", "gtrE2c", "gtrE2d"),
|
|
"gtrA2": ("gtrA2", "gtrA2b", "gtrA2c", "gtrA2d"),
|
|
"gtrD3": ("gtrD3", "gtrD3b", "gtrD3c"),
|
|
"gtrE3": ("gtrE3", "gtrE3b"),
|
|
"gtrG3": ("gtrG3", "gtrG3b", "gtrG3c", "gtrG3d"),
|
|
"gtrCs4": ("gtrCs4", "gtrCs4b"),
|
|
"gtrE4": ("gtrE4", "gtrE4b", "gtrE4c"),
|
|
"gtrB4": ("gtrB4", "gtrB4b"),
|
|
"gtrC5": ("gtrC5", "gtrC5b"),
|
|
"gtrD5": ("gtrD5", "gtrD5b"),
|
|
}
|
|
TURN = {}
|
|
|
|
|
|
# The next take of something. Every note written goes through this, so a bar of eighths on the hat
|
|
# uses four recordings of the hat rather than one of them eight times. Playing the identical
|
|
# waveform on every hit is what makes a sampled kit sound like a drum machine.
|
|
def take(n, name):
|
|
options = [item for item in TAKES.get(name, (name,)) if item in n]
|
|
|
|
if not options:
|
|
return name
|
|
TURN[name] = (TURN.get(name, -1) + 1) % len(options)
|
|
|
|
return options[TURN[name]]
|
|
|
|
|
|
def realInstruments():
|
|
out = []
|
|
# The kit, with its velocity layers: a hard hit and a softer one for the kick, the snare and the
|
|
# hat, plus a ghost snare, an open hat, a ride, a china and two toms.
|
|
volumes = { "kick": 64, "kickMid": 58, "snare": 64, "snareMid": 56, "ghost": 40, "hat": 50,
|
|
"hatMid": 42, "open": 50, "ride": 46, "china": 52, "crash": 58, "tom": 62,
|
|
"tomLow": 62 }
|
|
for label in ("kick", "kickB", "kickC", "kickMid", "kickMidB", "snare", "snareB", "snareC",
|
|
"snareD", "snareMid", "ghost", "hat", "hatB", "hatC", "hatD", "hatMid", "open",
|
|
"ride", "china", "crash", "crashB", "tom", "tomB", "tomLow"):
|
|
path = os.path.join(SAMPLES, label + ".wav")
|
|
if not os.path.isfile(path):
|
|
continue
|
|
data, rate = loadWave(path)
|
|
out.append(Instrument(label, data, False, rate, volumes.get(label.rstrip("BCD"), 56)))
|
|
data, rate = loadWave(os.path.join(SAMPLES, "bass.wav"))
|
|
out.append(Instrument("bass", data, False, rate, 44, None, None, 900, 28))
|
|
for name, midi in GUITARS:
|
|
for label in TAKES.get(name, (name,)):
|
|
path = os.path.join(SAMPLES, label + ".wav")
|
|
if os.path.isfile(path):
|
|
data, rate = loadWave(path)
|
|
out.append(Instrument(label, data, False, rate, 46, None, None, 4200, midi))
|
|
# The softer takes of the same strings, for anything that is not hit flat out.
|
|
for name, midi in SOFT:
|
|
data, rate = loadWave(os.path.join(SAMPLES, name + ".wav"))
|
|
out.append(Instrument(name, data, False, rate, 42, None, None, 4200, midi))
|
|
# Three muted guitars rather than one. A single low E sample stretched over every pitch goes
|
|
# nasal as it climbs, which is what made the guitars sound like bagpipes; a mute taken from the
|
|
# string that would really be played keeps the register honest.
|
|
for label, source, midi in (("mute", "gtrE2", 40), ("muteMid", "gtrG3", 55), ("muteHigh", "gtrB3", 59)):
|
|
data, rate = loadWave(os.path.join(SAMPLES, source + ".wav"))
|
|
out.append(Instrument(label, data, False, rate, 46, [(0, 64), (1, 40), (3, 14), (5, 0)], None, 0, midi))
|
|
|
|
return out
|
|
|
|
|
|
def synthInstruments():
|
|
crunch, crunchLoop = powerChord()
|
|
clean, cleanLoop = cleanerChord()
|
|
solo, soloLoop = leadGuitar()
|
|
bass, bassLoop = bassGuitar()
|
|
|
|
return [
|
|
Instrument("kick", rockKick(), False, RATE, 64),
|
|
Instrument("snare", rockSnare(), False, RATE, 64),
|
|
Instrument("hat", hat(int(0.06 * RATE), 55.0, 9500), False, RATE, 44),
|
|
Instrument("crash", crash(), False, RATE, 52),
|
|
Instrument("tom", tom(170.0), False, RATE, 56),
|
|
Instrument("ride", ride(), False, RATE, 46),
|
|
Instrument("bass", bass, bassLoop, TONE_RATE, 64,
|
|
[(0, 64), (2, 62), (16, 54), (48, 50)], 3, 600),
|
|
Instrument("crunch", crunch, crunchLoop, TONE_RATE, 62,
|
|
[(0, 56), (1, 64), (10, 58), (40, 54)], 3, 700),
|
|
Instrument("clean", clean, cleanLoop, TONE_RATE, 56,
|
|
[(0, 50), (1, 60), (12, 50), (40, 44)], 3, 800),
|
|
Instrument("mute", crunch, crunchLoop, TONE_RATE, 54,
|
|
[(0, 64), (2, 40), (6, 16), (12, 0)], None, 0),
|
|
Instrument("lead", solo, soloLoop, TONE_RATE, 60,
|
|
[(0, 20), (2, 64), (14, 58), (60, 56)], 3, 380),
|
|
]
|
|
|
|
|
|
def instruments():
|
|
global REAL
|
|
|
|
REAL = bool(SAMPLES) and os.path.isfile(os.path.join(SAMPLES, "gtrE2.wav"))
|
|
out = realInstruments() if REAL else synthInstruments()
|
|
# Which guitar plays a written note. With the real set, the nearest recording; with the
|
|
# synthesised one, the same two waveforms whatever the note.
|
|
CHOICE.clear()
|
|
for octave in range(0, 8):
|
|
for step, letter in enumerate(NAMES):
|
|
name = "%s%d" % (letter, octave)
|
|
midi = note(name) + 11
|
|
if REAL:
|
|
CHOICE[name] = min(GUITARS, key=lambda pair: abs(pair[1] - midi))[0]
|
|
else:
|
|
CHOICE[name] = "crunch"
|
|
|
|
return out
|
|
|
|
|
|
# The guitar to use for a note: the nearest recording of one.
|
|
def guitarFor(name, kind="crunch"):
|
|
if not REAL:
|
|
return kind
|
|
|
|
return CHOICE.get(name, "gtrE2")
|
|
|
|
|
|
def numbering(kit):
|
|
return {item.name: n + 1 for n, item in enumerate(kit)}
|
|
|
|
|
|
# ===== The patterns ===========================================================================
|
|
|
|
class Pattern:
|
|
def __init__(self, rows=ROWS):
|
|
self.rows = rows
|
|
self.cells = [[None] * CHANNELS for _ in range(rows)]
|
|
|
|
def put(self, row, channel, name=None, instrument=0, volume=0, effect=0, param=0):
|
|
if 0 <= row < self.rows:
|
|
pitch = note(name) if isinstance(name, str) else (name or 0)
|
|
self.cells[row][channel] = (pitch, instrument, volume, effect, param)
|
|
|
|
def pack(self):
|
|
out = bytearray()
|
|
for row in self.cells:
|
|
for cell in row:
|
|
if cell is None:
|
|
out.append(0x80)
|
|
continue
|
|
pitch, instrument, volume, effect, param = cell
|
|
mask = 0x80
|
|
body = bytearray()
|
|
if pitch:
|
|
mask |= 0x01
|
|
body.append(pitch)
|
|
if instrument:
|
|
mask |= 0x02
|
|
body.append(instrument)
|
|
if volume:
|
|
mask |= 0x04
|
|
body.append(volume)
|
|
if effect:
|
|
mask |= 0x08
|
|
body.append(effect)
|
|
if param:
|
|
mask |= 0x10
|
|
body.append(param)
|
|
out.append(mask)
|
|
out += body
|
|
|
|
return bytes(out)
|
|
|
|
|
|
# ===== The music ==============================================================================
|
|
#
|
|
# Arena rock in D, which is what the owner asked for after three passes of something darker: a major
|
|
# key, chords left to ring instead of chugged, and a hook that sings over the top. The progression
|
|
# is the one every anthem is built on -- D, A, B minor, G, the first, fifth, sixth and fourth of the
|
|
# key -- and the chorus starts on the fourth, because starting a chorus above where the verse sat is
|
|
# most of what "lift" means.
|
|
#
|
|
# Power chords throughout, so the thirds are left out and the melody decides the mood. The hook
|
|
# rises to a held D and stays there; the last chorus puts it an octave up, which is the oldest trick
|
|
# there is for making a final chorus feel bigger without playing anything louder.
|
|
|
|
# The key. A mixolydian: the major third with the flat seventh, which is why a G chord sits next to
|
|
# an A one in this style. Every ornament, passing tone and harmony note is snapped to it, because
|
|
# moving a note by a tone without asking what key you are in is how wrong notes get in.
|
|
KEY = (9, 11, 1, 2, 4, 6, 7)
|
|
|
|
|
|
def inKey(name):
|
|
value = note(name) - 1
|
|
best = min(range(-6, 7), key=lambda shift: (abs(shift), 0) if (value + shift) % 12 in KEY else (99, 0))
|
|
|
|
return "%s%d" % (NAMES[(value + best) % 12], (value + best) // 12)
|
|
|
|
|
|
# What the second voice plays over each chord: the tones a power chord leaves out.
|
|
COUNTER = { "A": ("C#5", "E4"), "D": ("F#4", "A4"), "E": ("G#4", "B4"), "G": ("B4", "D5"),
|
|
"C": ("E4", "G4"), "B": ("D5", "F#4"), "F": ("A4", "C5") }
|
|
|
|
|
|
# The riff. A, with the stops that make this style: a chord, a hole, a chord off the beat. Nothing
|
|
# is held for a bar anywhere in this track -- that was the slow part and it is gone.
|
|
VERSE = [
|
|
(0, "A2", 5, True), (8, "A2", 3, False), (14, "G2", 5, True), (24, "A2", 3, False),
|
|
(32, "A2", 5, True), (40, "D2", 3, False), (46, "A2", 5, True), (56, "E2", 5, True),
|
|
(64, "A2", 5, True), (72, "A2", 3, False), (78, "G2", 5, True), (88, "A2", 3, False),
|
|
(96, "D2", 5, True), (104, "D2", 3, False), (110, "E2", 5, True), (120, "A2", 7, True),
|
|
]
|
|
|
|
# The chorus: the fourth, the first, the fifth, the first, a chord to a half bar, bright and moving.
|
|
CHORUS = [
|
|
(0, "D2", 6, True), (16, "A2", 6, True), (32, "E2", 6, True), (48, "A2", 6, True),
|
|
(64, "D2", 6, True), (80, "A2", 6, True), (96, "G2", 6, True), (112, "A2", 6, True),
|
|
]
|
|
|
|
# The hook, rhythmic rather than sung: two short notes and an answer, twice, then the same idea
|
|
# higher. A major pentatonic, A B C# E F#.
|
|
HOOK = [
|
|
(0, "A4", 4), (6, "C#5", 4), (12, "E5", 8), (24, "C#5", 6),
|
|
(32, "A4", 4), (38, "B4", 4), (44, "C#5", 10), (58, "A4", 5),
|
|
(64, "E5", 4), (70, "F#5", 4), (76, "E5", 8), (88, "C#5", 6),
|
|
(96, "B4", 4), (102, "C#5", 4), (108, "A4", 14),
|
|
]
|
|
|
|
# The solo: sixteenth runs up and down the pentatonic, which is what this guitarist actually does,
|
|
# rather than long held notes.
|
|
SOLO = [
|
|
(0, "A4", 2), (2, "B4", 2), (4, "C#5", 2), (6, "E5", 2), (8, "F#5", 6), (16, "E5", 2),
|
|
(18, "C#5", 2), (20, "B4", 2), (22, "A4", 6),
|
|
(32, "C#5", 2), (34, "E5", 2), (36, "F#5", 2), (38, "A5", 8), (48, "F#5", 2), (50, "E5", 2),
|
|
(52, "C#5", 2), (54, "B4", 8),
|
|
(64, "E5", 2), (66, "F#5", 2), (68, "A5", 2), (70, "F#5", 2), (72, "E5", 6), (80, "C#5", 2),
|
|
(82, "B4", 2), (84, "A4", 10),
|
|
(96, "B4", 2), (98, "C#5", 2), (100, "E5", 2), (102, "F#5", 2), (104, "A5", 12),
|
|
(120, "E5", 7),
|
|
]
|
|
ANSWER = [
|
|
(24, "A4", 4), (30, "C#5", 4), (36, "E5", 6), (88, "F#5", 4), (94, "E5", 4), (100, "C#5", 10),
|
|
]
|
|
LIFT = SOLO
|
|
|
|
|
|
def panning(pattern):
|
|
for channel, where in PANNING.items():
|
|
for row in range(0, 6):
|
|
cell = pattern.cells[row][channel]
|
|
|
|
if cell is None:
|
|
pattern.put(row, channel, None, 0, 0, 0x08, where)
|
|
break
|
|
if cell[3] == 0:
|
|
pattern.cells[row][channel] = (cell[0], cell[1], cell[2], 0x08, where)
|
|
break
|
|
|
|
|
|
# ----- Playing it like a player ----------------------------------------------------------------
|
|
#
|
|
# The first attempt at this was sparse and repetitive, which sounded programmed. The second fixed
|
|
# that by randomising everything -- which offbeat the bass took, which hat dropped out, where a fill
|
|
# landed -- and the owner's word for the result was "haphazard". He is right: random is not human.
|
|
# A player repeats a figure on purpose and varies it where the phrase turns.
|
|
#
|
|
# So every rhythm below is written out once, as rows inside a bar, and played the same way every bar.
|
|
# The only thing left to chance is a decibel or two on a hit, because a drummer's accents are
|
|
# consistent and their absolute level is not. Variation happens where music puts it: the fourth bar
|
|
# of a four bar phrase.
|
|
|
|
BAR = 32 # Rows in a bar at eight rows to the beat.
|
|
|
|
# The groove. Kick on one, on the "a" of one, and on three; snare on two and four with a ghost
|
|
# before the last of them; eighths on the hat, accented on the beat; the hat left open on the
|
|
# "and" of two. This is one bar of rock drumming, written once.
|
|
KICK_ROWS = ((0, 0x50), (6, 0x3E), (16, 0x4C))
|
|
SNARE_ROWS = ((8, 0x50), (24, 0x50))
|
|
GHOST_ROWS = ((20, 0x24),)
|
|
HAT_ROWS = ((0, 0x2E), (4, 0x1E), (8, 0x2A), (12, 0x1E), (16, 0x2E), (20, 0x1E), (24, 0x2A), (28, 0x20))
|
|
OPEN_ROW = 12
|
|
|
|
# The fill that ends a four bar phrase, in the last bar: toms down the kit, snare on the last two.
|
|
FILL = ((16, "tom", 0x3A), (20, "tom", 0x3E), (24, "tomLow", 0x42), (26, "tomLow", 0x42),
|
|
(28, "snare", 0x46), (30, "snare", 0x4A))
|
|
|
|
# The right hand. Sixteenths, accented on the beat, with two deliberate holes: the sixteenth before
|
|
# beat four and the last one of the bar. Written once and played the same every bar, which is what
|
|
# makes it a part rather than a texture.
|
|
DRIVE_ROWS = ((0, 0x44), (2, 0x30), (4, 0x38), (6, 0x30), (8, 0x40), (10, 0x30), (12, 0x38),
|
|
(16, 0x44), (18, 0x30), (20, 0x38), (22, 0x30), (24, 0x40), (26, 0x30), (28, 0x38))
|
|
|
|
# The bass. Root on one, root on the "a" of one, root on three, octave on the "and" of two and the
|
|
# "and" of four: a figure, not a scatter of sixteenths.
|
|
BASS_ROWS = ((0, 0, 0x42), (6, 0, 0x34), (12, 12, 0x30), (16, 0, 0x3E), (22, 0, 0x32), (28, 12, 0x32))
|
|
|
|
# The stabs: the "and" of four in the second bar of a phrase, and twice in the fourth.
|
|
STAB_ROWS = { 1: ((28, 0x42),), 3: ((20, 0x3E), (28, 0x44)) }
|
|
|
|
|
|
class Rng:
|
|
# Only used for a decibel or two on a hit, so two runs still write the same file.
|
|
def __init__(self, seed):
|
|
self.state = seed * 2654435761 & 0xFFFFFFFF
|
|
|
|
def next(self, under):
|
|
self.state = (self.state * 1103515245 + 12345) & 0x7FFFFFFF
|
|
|
|
return (self.state >> 13) % under
|
|
|
|
|
|
def human(rng, level, spread=2):
|
|
return max(0x11, min(0x50, level - rng.next(spread * 2 + 1) + spread))
|
|
|
|
|
|
def muteFor(name):
|
|
if not REAL:
|
|
return "mute"
|
|
midi = note(name) + 11
|
|
|
|
return "mute" if midi < 48 else ("muteMid" if midi < 58 else "muteHigh")
|
|
|
|
|
|
def guitarSoft(name):
|
|
if not REAL:
|
|
return guitarFor(name)
|
|
midi = note(name) + 11
|
|
|
|
return min(SOFT, key=lambda pair: abs(pair[1] - midi))[0]
|
|
|
|
|
|
# The kit: the written groove every bar, the written fill in the last bar of each phrase.
|
|
def kit(pattern, n, rng, ride=False, halfTime=False, fills=True):
|
|
plate = "ride" if (ride and "ride" in n) else "hat"
|
|
bars = pattern.rows // BAR
|
|
for bar in range(bars):
|
|
at = bar * BAR
|
|
last = fills and (bar % 4 == 3)
|
|
for row, level in KICK_ROWS:
|
|
pattern.put(at + row, KICK, "C4", n[take(n, "kick")], human(rng, level))
|
|
for row, level in SNARE_ROWS:
|
|
if last and row == 24:
|
|
continue # The fill has that beat.
|
|
pattern.put(at + row, SNARE, "C4", n[take(n, "snare")], human(rng, level))
|
|
for row, level in GHOST_ROWS:
|
|
if not last and "ghost" in n:
|
|
pattern.put(at + row, SNARE, "C4", n["ghost"], human(rng, level))
|
|
for row, level in HAT_ROWS:
|
|
if last and row >= 16:
|
|
continue
|
|
what = "open" if (row == OPEN_ROW and bar % 2 == 1 and "open" in n) else plate
|
|
pattern.put(at + row, HAT, "C4", n[take(n, what)], human(rng, level))
|
|
if last:
|
|
for row, what, level in FILL:
|
|
pattern.put(at + row, TOM, transpose("C4", 4 - row // 8), n[take(n, what)], human(rng, level))
|
|
|
|
|
|
# The chords, with the written bass figure under them.
|
|
def chords(pattern, n, rng, figure, level=0x40, bass=True, octave=False, soft=False):
|
|
for row, chord, hold, accent in figure:
|
|
pick = guitarSoft if (soft or not accent) else guitarFor
|
|
pattern.put(row, LEFT, chord, n[take(n, pick(chord))], human(rng, level))
|
|
pattern.put(row, RIGHT, chord, n[guitarSoft(chord)], human(rng, level - 1), 0x0E, 0xD1)
|
|
if row + hold < pattern.rows:
|
|
pattern.put(row + hold, LEFT, 97)
|
|
pattern.put(row + hold, RIGHT, 97)
|
|
if bass:
|
|
# The figure repeats every bar, on whatever chord the bar is on.
|
|
for bar in range(pattern.rows // BAR):
|
|
at = bar * BAR
|
|
chord = chordAt(figure, at)
|
|
for row, step, level2 in BASS_ROWS:
|
|
pattern.put(at + row, BASS, inKey(transpose(chord, -12 + step)), n["bass"], human(rng, level2))
|
|
|
|
|
|
# Which chord a row falls on.
|
|
def chordAt(figure, row):
|
|
chord = figure[0][1]
|
|
for at, name, hold, accent in figure:
|
|
if at <= row:
|
|
chord = name
|
|
return chord
|
|
|
|
|
|
def drive(pattern, n, rng, figure, level=0x00):
|
|
for bar in range(pattern.rows // BAR):
|
|
at = bar * BAR
|
|
chord = chordAt(figure, at)
|
|
for row, own in DRIVE_ROWS:
|
|
pattern.put(at + row, CHUG, chord, n[take(n, muteFor(chord))], human(rng, own + level))
|
|
|
|
|
|
def stabs(pattern, n, rng, figure, level=0x00, per=0):
|
|
for bar in range(pattern.rows // BAR):
|
|
at = bar * BAR
|
|
chord = chordAt(figure, at)
|
|
above = transpose(chord, 12)
|
|
for row, own in STAB_ROWS.get(bar % 4, ()):
|
|
pattern.put(at + row, STAB, above, n[take(n, guitarFor(above))], human(rng, own + level))
|
|
pattern.put(at + row + 3, STAB, 97)
|
|
|
|
|
|
# The second voice: a written line over each chord, the third and the fifth of it, in a fixed
|
|
# rhythm. Two notes a bar, always in the same place.
|
|
def counter(pattern, n, rng, figure, level=0x30, every=16):
|
|
for bar in range(pattern.rows // BAR):
|
|
at = bar * BAR
|
|
chord = chordAt(figure, at)
|
|
tones = COUNTER.get(chord[:-1], ("A4", "D5"))
|
|
for i, row in enumerate((4, 20)):
|
|
name = tones[i % len(tones)]
|
|
pattern.put(at + row, HARM, name, n[guitarSoft(name)], human(rng, level))
|
|
pattern.put(at + row + 10, HARM, 97)
|
|
|
|
|
|
def crashes(pattern, n, rng, rows, level=0x4A):
|
|
for row in rows:
|
|
pattern.put(row, CRASH, "C4", n[take(n, "crash")], human(rng, level))
|
|
|
|
|
|
# The tune. The ornaments are written in rather than diced: a grace note into the first long note of
|
|
# each half, and nothing else.
|
|
GRACED = (32, 96)
|
|
|
|
|
|
def lead(pattern, n, rng, tune, channel=LEAD, level=0x4E, octave=0, ornament=True):
|
|
for row, name, hold in tune:
|
|
played = transpose(name, octave)
|
|
if ornament and row in GRACED:
|
|
grace = inKey(transpose(played, -2))
|
|
pattern.put(row - 1, channel, grace, n[guitarSoft(grace)], human(rng, level - 12))
|
|
pattern.put(row, channel, played, n[take(n, guitarFor(played))], human(rng, level))
|
|
if row + hold < pattern.rows:
|
|
pattern.put(row + hold, channel, 97)
|
|
|
|
|
|
def song():
|
|
kitList = instruments()
|
|
n = numbering(kitList)
|
|
out = []
|
|
# No slow bits anywhere: the riff opens it, and every entry after that is the riff, the chorus or
|
|
# the solo. Nothing fades in, nothing drops to half time, nothing holds a chord for a bar.
|
|
plan = [("riff", 1), ("riff", 2), ("chorus", 3), ("riff", 4), ("chorus", 5), ("solo", 6),
|
|
("chorus", 7), ("riff", 8), ("chorus", 9), ("solo", 10), ("chorus", 11)]
|
|
for kind, seed in plan:
|
|
rng = Rng(seed)
|
|
pattern = Pattern()
|
|
|
|
if seed == 1:
|
|
pattern.put(0, 0, None, 0, 0, 0x0F, SPEED)
|
|
if kind == "riff":
|
|
chords(pattern, n, rng, VERSE, 0x42)
|
|
drive(pattern, n, rng, VERSE)
|
|
kit(pattern, n, rng, seed % 3 == 0)
|
|
stabs(pattern, n, rng, VERSE)
|
|
crashes(pattern, n, rng, (0, 64), 0x44)
|
|
if seed > 2:
|
|
lead(pattern, n, rng, ANSWER, LEAD, 0x4A)
|
|
elif kind == "chorus":
|
|
chords(pattern, n, rng, CHORUS, 0x46)
|
|
drive(pattern, n, rng, CHORUS, -4)
|
|
kit(pattern, n, rng)
|
|
counter(pattern, n, rng, CHORUS, 0x30)
|
|
stabs(pattern, n, rng, CHORUS)
|
|
crashes(pattern, n, rng, (0, 32, 64, 96))
|
|
lead(pattern, n, rng, HOOK, LEAD, 0x4C)
|
|
lead(pattern, n, rng, [(r, inKey(transpose(name, -3)), h) for r, name, h in HOOK],
|
|
HARM, 0x36, 0, False)
|
|
else:
|
|
chords(pattern, n, rng, VERSE, 0x3E)
|
|
drive(pattern, n, rng, VERSE, -2)
|
|
kit(pattern, n, rng, True)
|
|
crashes(pattern, n, rng, (0, 64), 0x40)
|
|
lead(pattern, n, rng, SOLO, LEAD, 0x4E, 0, False)
|
|
panning(pattern)
|
|
out.append(pattern)
|
|
|
|
return kitList, out, list(range(len(out))), 0
|
|
|
|
|
|
# ===== Writing the file =======================================================================
|
|
|
|
def text(value, length):
|
|
raw = value.encode("ascii", "replace")[:length]
|
|
|
|
return raw + b"\0" * (length - len(raw))
|
|
|
|
|
|
def delta16(samples):
|
|
out = bytearray()
|
|
last = 0
|
|
for value in samples:
|
|
out += struct.pack("<h", ((value - last + 32768) & 0xFFFF) - 32768)
|
|
last = value
|
|
|
|
return bytes(out)
|
|
|
|
|
|
def envelopeBytes(points):
|
|
out = bytearray()
|
|
for tick, level in (points + [(0, 0)] * 12)[:12]:
|
|
out += struct.pack("<HH", tick, level)
|
|
|
|
return bytes(out)
|
|
|
|
|
|
def instrumentBytes(item):
|
|
body = text(item.name, 22) + bytes([0]) + struct.pack("<H", 1)
|
|
body += struct.pack("<I", SAMPLE_HEADER)
|
|
body += bytes(96)
|
|
body += envelopeBytes(item.envelope)
|
|
body += envelopeBytes([])
|
|
points = min(12, len(item.envelope))
|
|
sustain = item.sustain if item.sustain is not None else 0
|
|
kind = 0x01 | (0x02 if item.sustain is not None else 0)
|
|
body += bytes([points, 0, sustain, 0, max(0, points - 1), 0, 0, 0, kind, 0, 0, 0, 0, 0])
|
|
body += struct.pack("<H", item.fadeout)
|
|
body += bytes(22)
|
|
header = struct.pack("<I", INSTRUMENT_HEADER) + body
|
|
assert len(header) == INSTRUMENT_HEADER, "instrument header is %d bytes" % len(header)
|
|
|
|
# 16 bit: every length field is in bytes, and bit 4 of the type byte is what says so. A
|
|
# tuned instrument loops its whole sample, which is a whole number of cycles, so the loop is
|
|
# seamless; a drum is a one shot and loops not at all.
|
|
total = len(item.samples) * 2
|
|
length = item.loop * 2 if item.loop else 0
|
|
sample = struct.pack("<III", total, 0, length)
|
|
sample += bytes([item.volume, item.finetune & 0xFF, (0x01 if item.loop else 0x00) | 0x10, 128, item.relative & 0xFF, 0])
|
|
sample += text(item.name, 22)
|
|
assert len(sample) == SAMPLE_HEADER, "sample header is %d bytes" % len(sample)
|
|
|
|
return header + sample + delta16(item.samples)
|
|
|
|
|
|
def write(path, name="Singe Reel"):
|
|
kit, patterns, order, restart = song()
|
|
out = bytearray()
|
|
out += b"Extended Module: "
|
|
out += text(name, 20)
|
|
out += bytes([0x1A])
|
|
out += text("Singe reelMusic.py", 20)
|
|
out += struct.pack("<H", 0x0104)
|
|
out += struct.pack("<I", HEADER_SIZE)
|
|
out += struct.pack("<HHHHHHH", len(order), restart, CHANNELS, len(patterns), len(kit), 1, SPEED)
|
|
out += struct.pack("<H", BPM)
|
|
out += bytes(order) + bytes(256 - len(order))
|
|
assert len(out) == 60 + HEADER_SIZE, "header is %d bytes" % len(out)
|
|
|
|
for pattern in patterns:
|
|
packed = pattern.pack()
|
|
out += struct.pack("<I", 9) + bytes([0]) + struct.pack("<HH", pattern.rows, len(packed))
|
|
out += packed
|
|
for item in kit:
|
|
out += instrumentBytes(item)
|
|
|
|
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
|
with open(path, "wb") as handle:
|
|
handle.write(bytes(out))
|
|
rowTime = 2.5 / BPM * SPEED
|
|
|
|
print("%s %.0f KB" % (path, len(out) / 1024.0))
|
|
print("%d channels, %d patterns, %d instruments; %s" % (CHANNELS, len(patterns), len(kit),
|
|
("real samples from %s" % SAMPLES) if REAL else ("synthesised, drums 16 bit at %d Hz" % RATE)))
|
|
for item in kit:
|
|
print(" %-8s %6d samples %-7s relative %+3d finetune %+4d" %
|
|
(item.name, len(item.samples), "looped" if item.loop else "one shot", item.relative, item.finetune))
|
|
print("%d beats a minute at %d ticks a row: %.1f seconds a pattern, %.0f seconds through, looping from entry %d" %
|
|
(BPM, SPEED, rowTime * ROWS, rowTime * ROWS * len(order), restart))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
target = sys.argv[1] if len(sys.argv) > 1 else os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".builddir", "reelMusic.xm")
|
|
write(target)
|