# 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 [] (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(" 1 else os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".builddir", "reelMusic.xm") write(target)