# Prepares the real instrument samples the reel's music is built from. # # python3 util/reelSamples.py # # The sources are FreePats sound banks, fetched by hand and kept outside the repository because they # are hundreds of megabytes: # # * Electric Guitar FSBS (dist2) a Fender through an amplifier and an effects rack, CC0 1.0 # https://freepats.zenvoid.org/ElectricGuitar/distorted-electric-guitar.html # * Picked Bass YR a Yamaha RBX, CC0 1.0 # https://freepats.zenvoid.org/ElectricGuitar/clean-electric-bass.html # * MuldjordKit a real drum kit, Creative Commons Attribution 4.0 # https://freepats.zenvoid.org/Percussion/acoustic-drum-kit.html # # The guitar and the bass are public domain and carry no obligation at all. The kit is CC BY, so a # reel that uses it owes Bjorn Muldjord a credit; everything here is arranged so the kit can be # swapped for the synthesised one in reelMusic.py if that is unwelcome. # # What this does to each sample: down to one channel at 32000 Hz, the silence in front cut off so a # hit lands the moment it is triggered, a length that suits its part, a short fade so a truncated # tail cannot click, and a peak normalised into 16 bits with headroom. Nothing else -- the playing # is theirs. import math import os import struct import subprocess import sys import wave import numpy from scipy.signal import fftconvolve RATE = 32000 PEAK = 30000 SILENT = 0.012 # A hit starts where the signal first passes this fraction of its peak. FADE = 0.02 # Seconds of fade at the end, so a cut tail does not click. # What to take, what to call it, the MIDI note it was recorded at (None for a drum), and how many # seconds of it to keep. # What to take, what to call it, the MIDI note it was recorded at (None for a drum), and how many # seconds of it to keep. Where a name ends in a letter -- kickB, gtrA2c -- it is another take of the # same thing, and the module rotates through them so no two hits are the same waveform. That # rotation is the difference between a sampled instrument and a machine gun. WANTED = [ # The kit: three velocities of the kick, the snare and the hat, and three takes of each of # those, so a bar of eighths never repeats a sample. ("25-KdrumL.flac", "kick", None, 0.60), ("24-KdrumL.flac", "kickB", None, 0.60), ("23-KdrumL.flac", "kickC", None, 0.60), ("13-KdrumL.flac", "kickMid", None, 0.55), ("21-KdrumL.flac", "kickMidB", None, 0.55), ("56-Snare.flac", "snare", None, 0.70), ("55-Snare.flac", "snareB", None, 0.70), ("53-Snare.flac", "snareC", None, 0.70), ("51-Snare.flac", "snareD", None, 0.70), ("30-Snare.flac", "snareMid", None, 0.60), ("5-Snare.flac", "ghost", None, 0.40), ("29-HihatClosed.flac", "hat", None, 0.22), ("28-HihatClosed.flac", "hatB", None, 0.22), ("27-HihatClosed.flac", "hatC", None, 0.22), ("26-HihatClosed.flac", "hatD", None, 0.22), ("14-HihatClosed.flac", "hatMid", None, 0.20), ("29-HihatOpen.flac", "open", None, 0.60), ("5-RideL.flac", "ride", None, 1.10), ("1-China.flac", "china", None, 1.80), ("9-CrashL.flac", "crash", None, 2.40), ("8-CrashL.flac", "crashB", None, 2.40), ("13-Tom2.flac", "tom", None, 0.55), ("10-Tom2.flac", "tomB", None, 0.55), ("13-Tom4.flac", "tomLow", None, 0.70), # The guitar, four takes of everything the riff and the solo actually use. ("E2_s1_01.flac", "gtrE2", 40, 1.70), ("E2_s1_02.flac", "gtrE2b", 40, 1.70), ("E2_s1_03.flac", "gtrE2c", 40, 1.70), ("E2_s1_04.flac", "gtrE2d", 40, 1.70), ("A2_s2_01.flac", "gtrA2", 45, 1.70), ("A2_s2_02.flac", "gtrA2b", 45, 1.70), ("A2_s2_03.flac", "gtrA2c", 45, 1.70), ("A2_s2_04.flac", "gtrA2d", 45, 1.70), ("C2_s1_01.flac", "gtrC2", 36, 1.70), ("D3_s3_01.flac", "gtrD3", 50, 1.50), ("D3_s3_02.flac", "gtrD3b", 50, 1.50), ("D3_s3_03.flac", "gtrD3c", 50, 1.50), ("E3_s3_01.flac", "gtrE3", 52, 1.50), ("E3_s3_02.flac", "gtrE3b", 52, 1.50), ("G3_s4_01.flac", "gtrG3", 55, 1.60), ("G3_s4_02.flac", "gtrG3b", 55, 1.60), ("G3_s4_03.flac", "gtrG3c", 55, 1.60), ("G3_s4_04.flac", "gtrG3d", 55, 1.60), ("B3_s5_01.flac", "gtrB3", 59, 2.00), ("C#4_s5_01.flac", "gtrCs4", 61, 2.00), ("C#4_s5_02.flac", "gtrCs4b", 61, 2.00), ("E4_s6_01.flac", "gtrE4", 64, 2.20), ("E4_s6_02.flac", "gtrE4b", 64, 2.20), ("E4_s6_03.flac", "gtrE4c", 64, 2.20), ("G4_s6_01.flac", "gtrG4", 67, 2.20), ("B4_s6_01.flac", "gtrB4", 71, 2.40), ("B4_s6_02.flac", "gtrB4b", 71, 2.40), ("C5_s6_01.flac", "gtrC5", 72, 2.40), ("C5_s6_02.flac", "gtrC5b", 72, 2.40), ("D5_s6_01.flac", "gtrD5", 74, 2.60), ("D5_s6_02.flac", "gtrD5b", 74, 2.60), # The softer takes, for the doubled side and the second voice. ("E2_s1_soft_01.flac", "sftE2", 40, 1.60), ("A2_s2_soft_01.flac", "sftA2", 45, 1.60), ("C2_s1_soft_01.flac", "sftC2", 36, 1.60), ("G3_s4_soft_01.flac", "sftG3", 55, 1.50), ("B3_s5_soft_01.flac", "sftB3", 59, 1.80), ("D5_s6_soft_01.flac", "sftD5", 74, 2.20), ("E.flac", "bass", 28, 1.30), ] # ===== The room =============================================================================== # # These recordings are close miked and dry, and a record is not: a kit is in a room and a guitar # comes back off the far wall. So each sample gets the space baked into it, which is exactly what the # sample libraries of the era did -- their drums arrived with the room already on them. # # The reverb is built rather than sampled: a handful of discrete early reflections, which is what # tells the ear how big the room is, and then exponentially decaying noise for the tail. The guitars # get a slap instead, one repeat a tenth of a second later, which is the oldest guitar effect there # is and the reason a single tracked part sounds wide. # By instrument: how wet the room is, how long it rings, and the slap in milliseconds with its level. TREATMENT = { "kick": (0.10, 0.28, None), # Tight: a kick with a long tail is a mess. "snare": (0.30, 0.45, None), "ghost": (0.26, 0.40, None), "hat": (0.16, 0.30, None), "open": (0.20, 0.40, None), "ride": (0.20, 0.50, None), "china": (0.24, 0.60, None), "crash": (0.22, 0.60, None), "tom": (0.28, 0.45, None), "gtr": (0.08, 0.30, (112, 0.34)), # A slap a tenth of a second back, a third of the level. "sft": (0.08, 0.30, (112, 0.28)), "bass": (0.00, 0.00, None), # Dry: reverb on a bass is mud. } def treatmentFor(label): for prefix in sorted(TREATMENT, key=len, reverse=True): if label.startswith(prefix): return TREATMENT[prefix] return (0.15, 0.35, None) def impulse(rate, seconds, seed=1234): # The early reflections first, at the times a room this size would give them, then the tail. length = int(seconds * rate) out = numpy.zeros(length) state = seed for at, gain in ((0.011, 0.5), (0.017, 0.38), (0.023, 0.30), (0.031, 0.26), (0.043, 0.20)): index = int(at * rate) if index < length: out[index] += gain for i in range(int(0.008 * rate), length): state = (state * 1103515245 + 12345) & 0x7FFFFFFF noise = ((state >> 13) % 2001 - 1000) / 1000.0 out[i] += noise * math.exp(-5.0 * i / float(length)) * 0.22 # Nothing below eighty hertz or above eight kilohertz: a room does not reflect either well. out[0] += 1.0 # The direct sound, so the mix keeps its front. return out def space(samples, rate, wet, seconds, slap): if not samples: return samples x = numpy.array(samples, dtype=float) if slap: ms, level = slap for tap, gain in ((ms, level), (ms * 2, level * 0.45)): shift = int(ms and tap * rate / 1000.0) if shift < len(x): padded = numpy.concatenate([numpy.zeros(shift), x]) * gain x = numpy.concatenate([x, numpy.zeros(shift)]) + padded[:len(x) + shift] if wet > 0: wetted = fftconvolve(x, impulse(rate, seconds), mode="full") wetted = wetted[:len(x) + int(seconds * rate)] dry = numpy.concatenate([x, numpy.zeros(len(wetted) - len(x))]) x = dry * (1.0 - wet) + wetted / (numpy.abs(wetted).max() or 1) * numpy.abs(dry).max() * wet peak = numpy.abs(x).max() or 1.0 return [int(max(-32768, min(32767, round(v * PEAK / peak)))) for v in x] def find(root, name): for base, dirs, files in os.walk(root): if name in files: return os.path.join(base, name) return None def decode(path): raw = subprocess.run(["ffmpeg", "-v", "error", "-i", path, "-ac", "1", "-ar", str(RATE), "-f", "s16le", "-"], capture_output=True, check=True).stdout return list(struct.unpack("<%dh" % (len(raw) // 2), raw)) def prepare(samples, seconds): peak = max(abs(v) for v in samples) or 1 floor = peak * SILENT start = next((i for i, v in enumerate(samples) if abs(v) > floor), 0) start = max(0, start - int(0.002 * RATE)) # Two milliseconds of run up. out = samples[start:start + int(seconds * RATE)] fade = int(FADE * RATE) for i in range(max(0, len(out) - fade), len(out)): out[i] = int(out[i] * (len(out) - i) / float(fade)) peak = max(abs(v) for v in out) or 1 return [int(max(-32768, min(32767, v * PEAK / peak))) for v in out] def main(): if len(sys.argv) < 3: sys.exit("usage: reelSamples.py ") source, out = sys.argv[1], sys.argv[2] os.makedirs(out, exist_ok=True) missing = [] for name, label, midi, seconds in WANTED: path = find(source, name) if path is None: missing.append(name) continue wet, rings, slap = treatmentFor(label) data = space(prepare(decode(path), seconds), RATE, wet, rings, slap) with wave.open(os.path.join(out, label + ".wav"), "wb") as handle: handle.setnchannels(1) handle.setsampwidth(2) handle.setframerate(RATE) handle.writeframes(struct.pack("<%dh" % len(data), *data)) print("%-8s %6d samples %4.2f s %s" % (label, len(data), len(data) / float(RATE), "midi %d" % midi if midi else "one shot")) if missing: print("\nnot found: %s" % ", ".join(missing)) if __name__ == "__main__": main()