mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
577 lines
22 KiB
Python
577 lines
22 KiB
Python
"""Hype electro-pop instrumental for the MobileGL showreel, synthesised from score.py.
|
|
usage: python music.py out.wav"""
|
|
import sys
|
|
import numpy as np
|
|
from scipy.signal import fftconvolve, butter, sosfiltfilt
|
|
from dsp import SR, saw, pulse, sine, svf, onepole_hp, pingpong, compress, limit, mtof, adsr
|
|
import score as S
|
|
|
|
N = int(S.END * SR)
|
|
rng = np.random.default_rng(1234)
|
|
|
|
|
|
def idx(t):
|
|
return int(round(t * SR))
|
|
|
|
|
|
class Bus:
|
|
def __init__(self, name):
|
|
self.name = name
|
|
self.l = np.zeros(N)
|
|
self.r = np.zeros(N)
|
|
|
|
def add(self, t, sig, pan=0.0, gain=1.0):
|
|
"""sig: mono array or (l, r) tuple; pan -1..1 (equal-power)."""
|
|
i0 = idx(t)
|
|
if isinstance(sig, tuple):
|
|
l, r = sig
|
|
else:
|
|
a = (pan + 1) * np.pi / 4
|
|
l, r = sig * np.cos(a) * 1.4142, sig * np.sin(a) * 1.4142
|
|
if i0 < 0:
|
|
l, r, i0 = l[-i0:], r[-i0:], 0
|
|
n = min(len(l), N - i0)
|
|
if n <= 0:
|
|
return
|
|
self.l[i0:i0 + n] += gain * l[:n]
|
|
self.r[i0:i0 + n] += gain * r[:n]
|
|
|
|
def apply(self, g):
|
|
self.l *= g
|
|
self.r *= g
|
|
|
|
|
|
def noise(n):
|
|
return rng.standard_normal(n)
|
|
|
|
|
|
def tarr(n):
|
|
return np.arange(n) / SR
|
|
|
|
|
|
# --------------------------------------------------------------------------------------------------
|
|
# automation helpers
|
|
def curve(points):
|
|
"""Piecewise-exponential-ish automation over the whole song from [(time, value)] (log interp)."""
|
|
ts = np.array([p[0] for p in points])
|
|
vs = np.log(np.array([p[1] for p in points]))
|
|
return np.exp(np.interp(np.arange(N) / SR, ts, vs))
|
|
|
|
|
|
def lin_curve(points):
|
|
ts = np.array([p[0] for p in points])
|
|
vs = np.array([p[1] for p in points])
|
|
return np.interp(np.arange(N) / SR, ts, vs)
|
|
|
|
|
|
def sidechain(depth, rel=0.16):
|
|
"""Gain curve ducking on every kick."""
|
|
g = np.ones(N)
|
|
kn = int(0.6 * SR)
|
|
t = tarr(kn)
|
|
shape = 1 - depth * np.where(t < 0.004, t / 0.004, np.exp(-(np.maximum(t - 0.004, 0) / rel) ** 1.6))
|
|
for tk, v in S.events('kick'):
|
|
i0 = idx(tk)
|
|
n = min(kn, N - i0)
|
|
g[i0:i0 + n] = np.minimum(g[i0:i0 + n], 1 - (1 - shape[:n]) * min(v, 1.0))
|
|
return g
|
|
|
|
|
|
def gaps():
|
|
"""Mute window (one beat of silence) before each drop."""
|
|
g = np.ones(N)
|
|
for b in (16, 56, 88):
|
|
a0, a1 = idx(S.tb(b - 1, 3)), idx(S.tb(b))
|
|
f = int(0.006 * SR)
|
|
g[a0:a1] = 0
|
|
g[a0 - f:a0] = np.linspace(1, 0, f)
|
|
g[a1 - f // 2:a1] = 0
|
|
return g
|
|
|
|
|
|
# --------------------------------------------------------------------------------------------------
|
|
# drums
|
|
def kick(v):
|
|
n = int(0.5 * SR)
|
|
t = tarr(n)
|
|
f = 46 + 130 * np.exp(-t / 0.032) + 40 * np.exp(-t / 0.006)
|
|
body = sine(f, 0.0) * (np.exp(-t / 0.32) * np.minimum(t / 0.002, 1))
|
|
click = svf(noise(n), np.full(n, 3500.0), 0.2, 1) * np.exp(-t / 0.004) * 0.6
|
|
x = np.tanh(1.6 * (body + click)) * 0.95
|
|
return x * v
|
|
|
|
|
|
def clap(v):
|
|
n = int(0.35 * SR)
|
|
t = tarr(n)
|
|
nz = svf(noise(n), np.full(n, 1300.0), 0.35, 1) * 2.2
|
|
env = np.zeros(n)
|
|
for k, d in enumerate((0.0, 0.009, 0.018, 0.027)):
|
|
m = t >= d
|
|
env[m] += np.exp(-(t[m] - d) / (0.006 if k < 3 else 0.11)) * (0.8 if k < 3 else 1.0)
|
|
body = sine(np.full(n, 190.0) + 40 * np.exp(-t / 0.01), 0.25) * np.exp(-t / 0.05) * 0.35
|
|
return (nz * env + body) * v * 0.8
|
|
|
|
|
|
def snare(v, pitch=1.0):
|
|
n = int(0.25 * SR)
|
|
t = tarr(n)
|
|
nz = svf(noise(n), np.full(n, 4200.0 * pitch), 0.1, 1) * 1.8 * np.exp(-t / 0.07)
|
|
body = sine(np.full(n, 200.0 * pitch) + 60 * np.exp(-t / 0.008), 0.0) * np.exp(-t / 0.06)
|
|
return (nz * 0.7 + body * 0.6) * v
|
|
|
|
|
|
METAL = np.array([205.3, 304.4, 369.6, 522.7, 540.0, 800.0]) * 1.7
|
|
|
|
|
|
def metal(n):
|
|
x = np.zeros(n)
|
|
for f in METAL:
|
|
x += pulse(np.full(n, f), 0.5, rng.random())
|
|
return x / len(METAL)
|
|
|
|
|
|
def hat(v, decay=0.035, n_s=0.12):
|
|
n = int(n_s * SR)
|
|
t = tarr(n)
|
|
x = 0.55 * metal(n) + 0.6 * noise(n)
|
|
x = svf(x, np.full(n, 8500.0), 0.2, 2)
|
|
return x * np.exp(-t / decay) * v * 0.55
|
|
|
|
|
|
def shaker(v):
|
|
n = int(0.09 * SR)
|
|
t = tarr(n)
|
|
x = svf(noise(n), np.full(n, 6500.0), 0.3, 1)
|
|
env = np.minimum(t / 0.012, 1) * np.exp(-t / 0.03)
|
|
return x * env * v * 0.5
|
|
|
|
|
|
def crash(v):
|
|
n = int(2.6 * SR)
|
|
t = tarr(n)
|
|
x = 0.5 * metal(n) + noise(n)
|
|
x = svf(x, np.full(n, 5200.0), 0.1, 2)
|
|
xl = x * np.exp(-t / 0.75)
|
|
xr = np.roll(x, 37) * np.exp(-t / 0.7)
|
|
return (xl * v * 0.3, xr * v * 0.3)
|
|
|
|
|
|
def impact(v):
|
|
n = int(3.0 * SR)
|
|
t = tarr(n)
|
|
boom = sine(30 + 45 * np.exp(-t / 0.35), 0.0) * np.exp(-t / 0.9) * np.minimum(t / 0.003, 1)
|
|
crack = svf(noise(n), 6000 * np.exp(-t / 0.25) + 120, 0.2, 0) * np.exp(-t / 0.5)
|
|
x = np.tanh(1.3 * boom) * 0.9 + crack * 0.35
|
|
return x * v
|
|
|
|
|
|
def riser(dur, v):
|
|
n = int(dur * SR)
|
|
t = tarr(n)
|
|
u = t / dur
|
|
nz = svf(noise(n), 300 * (9000 / 300) ** (u ** 1.4), 0.55, 1)
|
|
tone = saw(110 * 2 ** (u ** 1.3 * 2), 0.0) * 0.15
|
|
tone = svf(tone, 400 + 5000 * u ** 2, 0.3, 0)
|
|
env = u ** 2.2
|
|
x = (nz * 0.9 + tone) * env * v * 0.55
|
|
return (x, np.roll(x, 211))
|
|
|
|
|
|
def reverse_swell(v, dur):
|
|
"""Reversed crash-ish swell that ends exactly on the drop."""
|
|
l, r = crash(1.0)
|
|
n = int(dur * SR)
|
|
return (l[:n][::-1] * v * 1.4, r[:n][::-1] * v * 1.4)
|
|
|
|
|
|
# --------------------------------------------------------------------------------------------------
|
|
# tonal voices
|
|
DETUNE = np.array([-0.24, -0.15, -0.06, 0.0, 0.06, 0.15, 0.24]) # semitones
|
|
PANS = np.array([-0.9, -0.6, -0.3, 0.0, 0.3, 0.6, 0.9])
|
|
|
|
|
|
def supersaw(midi, dur, v, att=0.006, rel=0.25, spread=1.0):
|
|
n = int((dur + rel) * SR)
|
|
l = np.zeros(n)
|
|
r = np.zeros(n)
|
|
for d, p in zip(DETUNE * spread, PANS):
|
|
x = saw(np.full(n, float(mtof(midi + d))), rng.random())
|
|
a = (p + 1) * np.pi / 4
|
|
l += x * np.cos(a)
|
|
r += x * np.sin(a)
|
|
env = adsr(n, att, 0.3, 0.75, int(dur * SR), rel)
|
|
return (l * env * v / 3.2, r * env * v / 3.2)
|
|
|
|
|
|
def pluck(midi, dur, v, bright=1.0):
|
|
n = int((dur + 0.35) * SR)
|
|
t = tarr(n)
|
|
f = np.full(n, float(mtof(midi)))
|
|
x = 0.6 * saw(f, rng.random()) + 0.4 * pulse(f * 1.003, 0.35, rng.random())
|
|
cut = 250 + bright * 4200 * np.exp(-t / 0.09)
|
|
x = svf(x, cut, 0.25, 0)
|
|
return x * np.exp(-t / 0.22) * np.minimum(t / 0.002, 1) * v
|
|
|
|
|
|
def keys(midi, dur, v):
|
|
"""FM electric piano stab."""
|
|
n = int((dur + 0.4) * SR)
|
|
t = tarr(n)
|
|
f = float(mtof(midi))
|
|
idx_env = 2.2 * np.exp(-t / 0.15) + 0.4
|
|
mod = sine(np.full(n, f), 0.0) * idx_env
|
|
car = np.sin(2 * np.pi * f * t + mod)
|
|
tine = sine(np.full(n, f * 14.0), 0.0) * np.exp(-t / 0.03) * 0.15
|
|
env = np.exp(-t / 0.6) * np.minimum(t / 0.002, 1)
|
|
rs = int(dur * SR)
|
|
if rs < n:
|
|
env[rs:] *= np.exp(-(t[rs:] - t[rs]) / 0.06)
|
|
return (car + tine) * env * v
|
|
|
|
|
|
def bell(midi, dur, v):
|
|
n = int((dur + 1.6) * SR)
|
|
t = tarr(n)
|
|
f = float(mtof(midi))
|
|
mod = sine(np.full(n, f * 3.5), 0.0) * (1.4 * np.exp(-t / 0.4) + 0.1)
|
|
car = np.sin(2 * np.pi * f * t + mod)
|
|
sub = np.sin(2 * np.pi * f * t) * 0.5
|
|
env = np.exp(-t / 1.1) * np.minimum(t / 0.004, 1)
|
|
return (car * 0.6 + sub) * env * v
|
|
|
|
|
|
def mono_line(notes, kind, glide=0.035, vib=0.12):
|
|
"""Monophonic lead over a whole note list [(t, dur, midi, vel)] -> (l, r) full-length arrays (sparse
|
|
regions zero). Glides between notes that touch; vibrato fades in on held notes."""
|
|
if not notes:
|
|
return np.zeros(N), np.zeros(N)
|
|
notes = sorted(notes)
|
|
t0 = notes[0][0] - 0.05
|
|
t1 = notes[-1][0] + notes[-1][1] + 0.6
|
|
i0, i1 = idx(t0), min(idx(t1), N)
|
|
n = i1 - i0
|
|
tt = (np.arange(n) + i0) / SR
|
|
pitch = np.full(n, float(notes[0][2]))
|
|
gate = np.zeros(n)
|
|
vel = np.zeros(n)
|
|
since = np.full(n, 10.0)
|
|
for k, (ts, d, m, v) in enumerate(notes):
|
|
a, b = idx(ts) - i0, min(idx(ts + d) - i0, n)
|
|
nxt = idx(notes[k + 1][0]) - i0 if k + 1 < len(notes) else n
|
|
seg = slice(a, min(nxt, n))
|
|
prev = pitch[a - 1] if a > 0 else m
|
|
u = (tt[seg] - ts)
|
|
pitch[seg] = m + (prev - m) * np.exp(-u / glide) if k > 0 and notes[k - 1][0] + notes[k - 1][1] >= ts - 0.02 else m
|
|
gate[a:b] = 1
|
|
vel[a:min(nxt, n)] = v
|
|
since[seg] = u
|
|
vibr = vib * np.sin(2 * np.pi * 5.6 * tt) * np.clip((since - 0.18) / 0.25, 0, 1)
|
|
f = mtof(pitch + vibr)
|
|
# amp: attack 4ms, release 70ms (one-pole on gate)
|
|
up, dn = np.exp(-1 / (0.004 * SR)), np.exp(-1 / (0.07 * SR))
|
|
from scipy.signal import lfilter
|
|
att = lfilter([1 - up], [1, -up], gate)
|
|
rel = lfilter([1 - dn], [1, -dn], gate)
|
|
env = np.where(gate > 0, att, rel) * vel
|
|
if kind == 'lead':
|
|
x = 0.45 * saw(f * 2 ** (0.08 / 12), 0.1) + 0.45 * saw(f * 2 ** (-0.08 / 12), 0.6) + 0.35 * pulse(f, 0.3, 0.3)
|
|
cut = 900 + 5200 * np.clip(env, 0, 1) * (0.55 + 0.45 * np.exp(-np.minimum(since, 5) / 0.15))
|
|
x = svf(x, cut, 0.3, 0)
|
|
sub = sine(f / 2, 0.0) * 0.25
|
|
x = (x + sub) * env
|
|
l = x + 0.0
|
|
r = 0.45 * saw(f * 2 ** (0.12 / 12), 0.9)
|
|
r = svf(r, cut, 0.3, 0) * env * 0.6 + x * 0.7
|
|
l = l * 1.0
|
|
out = (l, r)
|
|
else: # 'chop': vowel-ish formant voice
|
|
src = saw(f, 0.2) * 0.7 + pulse(f, 0.5, 0.0) * 0.3
|
|
vowel = 0.5 + 0.5 * np.sin(2 * np.pi * tt * S.BPM / 60 / 2)
|
|
f1 = 700 - 300 * vowel
|
|
f2 = 1220 - 400 * vowel
|
|
x = svf(src, f1, 0.85, 1) + 0.7 * svf(src, f2, 0.85, 1) + 0.25 * svf(src, np.full(n, 2600.0), 0.8, 1)
|
|
x = x * env * 1.6
|
|
out = (x, x)
|
|
L, R = np.zeros(N), np.zeros(N)
|
|
L[i0:i1] = out[0][:n]
|
|
R[i0:i1] = out[1][:n]
|
|
return L, R
|
|
|
|
|
|
# --------------------------------------------------------------------------------------------------
|
|
def build():
|
|
B = {k: Bus(k) for k in ('kick', 'drums', 'bass', 'chords', 'pad', 'arp', 'lead', 'keys', 'fx')}
|
|
|
|
# --- drums / fx from the shared event list
|
|
for t, k, v in S.EVENTS:
|
|
if k == 'kick':
|
|
B['kick'].add(t, kick(v))
|
|
elif k == 'clap':
|
|
B['drums'].add(t, clap(v), 0.0, 0.7)
|
|
elif k == 'snare':
|
|
B['drums'].add(t, snare(v), 0.1, 0.6)
|
|
elif k == 'roll':
|
|
B['drums'].add(t, snare(v, 1.0 + 0.5 * v), 0.0, 0.38)
|
|
elif k == 'hat':
|
|
B['drums'].add(t, hat(v), 0.25, 0.75)
|
|
elif k == 'ohat':
|
|
B['drums'].add(t, hat(v, 0.12, 0.35), -0.2, 0.6)
|
|
elif k == 'shaker':
|
|
B['drums'].add(t, shaker(v), -0.35, 0.8)
|
|
elif k == 'crash':
|
|
B['fx'].add(t, crash(v), gain=0.9)
|
|
elif k == 'impact':
|
|
B['fx'].add(t, impact(v), gain=0.8)
|
|
elif k == 'heart':
|
|
B['kick'].add(t, kick(v) * 0.6)
|
|
B['kick'].add(t + 0.16, kick(v) * 0.35)
|
|
elif k == 'riser':
|
|
B['fx'].add(t, riser(v, 1.0), gain=0.42)
|
|
elif k == 'drop':
|
|
B['fx'].add(t - S.BAR, reverse_swell(0.5, S.BAR), gain=0.6)
|
|
|
|
# --- harmony
|
|
for bar in range(S.NBARS):
|
|
name, a, b = S.section(bar)
|
|
i = bar - a
|
|
voicing, root = S.chord(bar)
|
|
t = S.tb(bar)
|
|
drop = name.startswith('drop')
|
|
# pads
|
|
if name in ('intro', 'build1', 'verse', 'break', 'build2', 'build3', 'outro') or drop:
|
|
pv = {'intro': 1.2, 'build1': 0.45, 'verse': 0.3, 'break': 0.6, 'build2': 0.4, 'build3': 0.4,
|
|
'outro': 0.5}.get(name, 0.3)
|
|
for m in voicing:
|
|
B['pad'].add(t, supersaw(m - 12 if m > 60 else m, S.BAR, pv * 0.35, att=0.35, rel=0.8,
|
|
spread=0.6))
|
|
# big chords
|
|
if drop or name in ('build2', 'build3') or (name == 'build1' and i >= 4) or (name == 'outro'):
|
|
vv = 1.0 if drop else 0.45
|
|
if name == 'outro' and i >= 4:
|
|
if i == 4:
|
|
for m in voicing + (voicing[0] + 12, voicing[1] + 12):
|
|
B['chords'].add(t, supersaw(m, 4 * S.BAR - 0.2, 0.9, rel=1.5))
|
|
else:
|
|
if drop:
|
|
# rhythmic stabs: the classic pumping offbeat-driven pattern
|
|
for s, l in ((0, 3), (3, 3), (6, 4), (10, 2), (12, 4)):
|
|
for m in voicing + (voicing[2] + 12,):
|
|
B['chords'].add(S.tb(bar, 0, s), supersaw(m, l * S.S16 - 0.01, vv, rel=0.12))
|
|
else:
|
|
for m in voicing + (voicing[2] + 12,):
|
|
B['chords'].add(t, supersaw(m, S.BAR - 0.02, vv, rel=0.2))
|
|
# bass
|
|
if name not in ('intro', 'break') and not (name == 'outro' and i >= 4):
|
|
if drop or name == 'outro':
|
|
for s in range(0, 16, 2):
|
|
m = root + (12 if s in (6, 14) and drop else 0)
|
|
B['bass'].add(S.tb(bar, 0, s + 1), bassnote(m, S.S16 * 1.6, 0.9), 0.0)
|
|
B['bass'].add(t, subnote(root, S.BAR - 0.03, 0.9))
|
|
elif name == 'verse':
|
|
for s, l, o in ((0, 3, 0), (3, 2, 12), (6, 2, 0), (8, 3, 0), (11, 1, 12), (12, 2, 7), (14, 2, 12)):
|
|
B['bass'].add(S.tb(bar, 0, s), bassnote(root + o, l * S.S16, 0.6))
|
|
B['bass'].add(S.tb(bar, 0, s), subnote(root + o if o != 12 else root, l * S.S16, 0.5))
|
|
else: # builds: plain quarter-note roots, saving the 8th-note pump for the drops
|
|
bv = 0.7 if name == 'build1' else 0.55 + 0.25 * (i / 8)
|
|
for q in range(4):
|
|
B['bass'].add(S.tb(bar, q), bassnote(root, S.BEAT * 0.8, bv))
|
|
B['bass'].add(S.tb(bar, q), subnote(root, S.BEAT * 0.8, bv * 0.85))
|
|
elif name == 'outro' and i == 4:
|
|
B['bass'].add(t, subnote(root, 3 * S.BAR, 0.9))
|
|
# arp
|
|
if name in ('intro', 'build1', 'verse', 'build2', 'build3', 'break') or drop:
|
|
if name == 'intro' and i < 2:
|
|
continue
|
|
tones = [voicing[0] + 12, voicing[1] + 12, voicing[2] + 12, voicing[0] + 24]
|
|
order = [0, 1, 2, 3, 2, 1, 2, 3, 0, 1, 2, 3, 2, 3, 1, 2]
|
|
step = 2 if name == 'break' else 1
|
|
av = {'intro': 0.5, 'build1': 0.4, 'verse': 0.42, 'break': 0.32}.get(name, 0.3)
|
|
for s in range(0, 16, step):
|
|
if name in ('intro', 'build1', 'build2', 'build3'):
|
|
bright = 0.25 + 0.75 * (i + s / 16) / (b - a)
|
|
else:
|
|
bright = 0.7
|
|
B['arp'].add(S.tb(bar, 0, s), pluck(tones[order[s]], S.S16, av * (1.0 if s % 4 == 0 else 0.75),
|
|
bright), pan=0.35 * np.sin(s * 0.9))
|
|
# keys (verse stabs, break bells)
|
|
if name == 'verse':
|
|
for s in (2, 6, 10, 14) if i % 2 else (2, 6, 7, 10, 14):
|
|
for m in voicing:
|
|
B['keys'].add(S.tb(bar, 0, s), keys(m + 12, S.S16 * 1.5, 0.22), pan=-0.2)
|
|
if name == 'break':
|
|
BREAK = [[(0, 4, 72), (4, 4, 69), (8, 8, 72)], [(0, 6, 77), (6, 2, 76), (8, 8, 72)],
|
|
[(0, 4, 74), (4, 4, 71), (8, 8, 74)], [(0, 6, 79), (6, 2, 77), (8, 8, 74)],
|
|
[(0, 4, 76), (4, 4, 72), (8, 8, 76)], [(0, 6, 81), (6, 2, 79), (8, 8, 76)],
|
|
[(0, 8, 77), (8, 8, 76)], [(0, 8, 74), (8, 4, 71), (12, 4, 74)]]
|
|
for ts, d, m in S.notes_in(bar, BREAK[i]):
|
|
B['keys'].add(ts, bell(m, d, 0.3), pan=0.1)
|
|
|
|
# --- lead lines
|
|
lead = []
|
|
chop = []
|
|
lead_hi = []
|
|
for bar in range(S.NBARS):
|
|
name, a, b = S.section(bar)
|
|
i = bar - a
|
|
if name.startswith('drop'):
|
|
for ts, d, m in S.notes_in(bar, S.hook_bar(i)):
|
|
lead.append((ts, d * 0.92, m, 1.0))
|
|
if name != 'drop1' or i >= 8:
|
|
chop.append((ts, d * 0.92, m + 12, (0.6 if name == 'drop1' else 0.7 if name == 'drop2' else 0.85)
|
|
* (1.0 if i >= 8 else 0.7)))
|
|
if i >= 8:
|
|
lead_hi.append((ts, d * 0.92, m + 12, 0.5 + 0.03 * (i - 8)))
|
|
elif name == 'outro' and i < 4:
|
|
for ts, d, m in S.notes_in(bar, S.hook_bar(i if i < 3 else 7)):
|
|
lead.append((ts, d * 0.92, m, 1.0 - 0.12 * i))
|
|
elif name == 'build3' and i >= 4:
|
|
for ts, d, m in S.notes_in(bar, S.hook_bar(i - 4)):
|
|
lead.append((ts, d * 0.9, m, 0.45 + 0.1 * (i - 4)))
|
|
lead.append((S.tb(108), S.BAR * 3, 76 + 2, 0.9))
|
|
ll, lr = mono_line(lead, 'lead')
|
|
B['lead'].add(0, (ll, lr), gain=0.42)
|
|
hl, hr = mono_line(lead_hi, 'lead', vib=0.08)
|
|
B['lead'].add(0, (hr, hl), gain=0.16)
|
|
cl, cr = mono_line(chop, 'chop', glide=0.02, vib=0.05)
|
|
B['lead'].add(0, (cl, cr), gain=0.2)
|
|
return B
|
|
|
|
|
|
def bassnote(m, dur, v):
|
|
n = int((dur + 0.05) * SR)
|
|
t = tarr(n)
|
|
f = np.full(n, float(mtof(m)))
|
|
x = saw(f, 0.0) * 0.7 + saw(f * 1.004, 0.5) * 0.5
|
|
x = svf(x, 180 + 900 * np.exp(-t / 0.07), 0.35, 0)
|
|
env = np.minimum(t / 0.003, 1) * np.where(t < dur, 1.0, np.exp(-(t - dur) / 0.012))
|
|
return np.tanh(x * 1.5) * env * v * 0.55
|
|
|
|
|
|
def subnote(m, dur, v):
|
|
n = int((dur + 0.05) * SR)
|
|
t = tarr(n)
|
|
x = sine(np.full(n, float(mtof(m))), 0.0)
|
|
env = np.minimum(t / 0.004, 1) * np.where(t < dur, 1.0, np.exp(-(t - dur) / 0.015))
|
|
return x * env * v * 0.6
|
|
|
|
|
|
# --------------------------------------------------------------------------------------------------
|
|
def reverb_ir(seconds, damp, seed):
|
|
r = np.random.default_rng(seed)
|
|
n = int(seconds * SR)
|
|
t = tarr(n)
|
|
out = []
|
|
for ch in range(2):
|
|
x = r.standard_normal(n) * np.exp(-t / (seconds / 6.9))
|
|
x = svf(x, damp * np.exp(-t / seconds * 1.5) + 400, 0.0, 0)
|
|
x[: int(0.012 * SR)] *= np.linspace(0, 1, int(0.012 * SR))
|
|
out.append(x / np.sqrt(np.sum(x ** 2)))
|
|
return out
|
|
|
|
|
|
def conv(l, r, ir):
|
|
return fftconvolve(l, ir[0])[:N], fftconvolve(r, ir[1])[:N]
|
|
|
|
|
|
def mix(B):
|
|
sc_hard = sidechain(0.85)
|
|
sc_mid = sidechain(0.55)
|
|
sc_soft = sidechain(0.3)
|
|
# section automation of the chord / pad / arp low-pass
|
|
T = S.tb
|
|
chord_cut = curve([(0, 300), (T(12), 500), (T(16) - 0.01, 9000), (T(16), 11000), (T(32), 11000),
|
|
(T(48), 600), (T(55, 3), 8000), (T(56), 12000), (T(72), 12000), (T(80), 500),
|
|
(T(87, 3), 8000), (T(88), 13000), (T(104), 13000), (T(108), 6000), (T(112) + 3, 300)])
|
|
pad_cut = curve([(0, 450), (T(8), 2400), (T(16), 2600), (T(72), 2600), (T(73), 1600), (T(80), 2600),
|
|
(T(108), 3000), (T(112) + 3, 300)])
|
|
arp_cut = curve([(0, 900), (T(8), 3000), (T(16), 7000), (T(112), 7000)])
|
|
for k in ('chords',):
|
|
B[k].l = svf(B[k].l, chord_cut, 0.15, 0)
|
|
B[k].r = svf(B[k].r, chord_cut, 0.15, 0)
|
|
B['pad'].l = svf(B['pad'].l, pad_cut, 0.1, 0)
|
|
B['pad'].r = svf(B['pad'].r, pad_cut, 0.1, 0)
|
|
B['arp'].l = svf(B['arp'].l, arp_cut, 0.1, 0)
|
|
B['arp'].r = svf(B['arp'].r, arp_cut, 0.1, 0)
|
|
|
|
B['chords'].apply(sc_hard)
|
|
B['pad'].apply(sc_hard)
|
|
B['bass'].apply(sc_hard)
|
|
B['arp'].apply(sc_mid)
|
|
B['keys'].apply(sc_soft)
|
|
B['lead'].apply(sc_soft)
|
|
g = gaps()
|
|
for k in ('chords', 'pad', 'bass', 'arp', 'keys', 'lead'):
|
|
B[k].apply(g)
|
|
|
|
gains = {'kick': 0.95, 'drums': 1.0, 'bass': 0.68, 'chords': 0.3, 'pad': 0.4, 'arp': 0.28, 'lead': 1.4,
|
|
'keys': 0.42, 'fx': 0.45}
|
|
L = np.zeros(N)
|
|
R = np.zeros(N)
|
|
for k, b in B.items():
|
|
L += b.l * gains[k]
|
|
R += b.r * gains[k]
|
|
|
|
# sends
|
|
hall = reverb_ir(2.8, 5000, 7)
|
|
room = reverb_ir(0.7, 7000, 9)
|
|
send_hall_l = B['lead'].l * 0.35 + B['keys'].l * 0.35 + B['pad'].l * 0.3 + B['arp'].l * 0.25 + B['fx'].l * 0.3
|
|
send_hall_r = B['lead'].r * 0.35 + B['keys'].r * 0.35 + B['pad'].r * 0.3 + B['arp'].r * 0.25 + B['fx'].r * 0.3
|
|
send_room_l = B['drums'].l * 0.4
|
|
send_room_r = B['drums'].r * 0.4
|
|
hl, hr = conv(send_hall_l, send_hall_r, hall)
|
|
rl, rr = conv(send_room_l, send_room_r, room)
|
|
dl, dr = pingpong(B['lead'].l * 0.5 + B['arp'].l * 0.3 + B['keys'].l * 0.3,
|
|
B['lead'].r * 0.5 + B['arp'].r * 0.3 + B['keys'].r * 0.3, S.S16 * 3, 0.38, 3500.0)
|
|
L += hl * 0.4 + rl * 0.3 + dl * 0.2
|
|
R += hr * 0.4 + rr * 0.3 + dr * 0.2
|
|
|
|
# stutters (glitch repeats of the first 16th of the beat)
|
|
for t0, _ in S.events('stutter'):
|
|
a = idx(t0)
|
|
pattern = [2, 2, 1, 1, 0.5, 0.5, 0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25] # in 16ths
|
|
src_l, src_r = L[a:a + idx(S.S16 * 2)].copy(), R[a:a + idx(S.S16 * 2)].copy()
|
|
p = a
|
|
for k, s in enumerate(pattern):
|
|
n = idx(S.S16 * s)
|
|
f = np.ones(n)
|
|
fd = min(64, n // 4)
|
|
f[:fd] = np.linspace(0, 1, fd)
|
|
f[-fd:] = np.linspace(1, 0, fd)
|
|
gain = 1.0 - 0.02 * k
|
|
L[p:p + n] = src_l[:n] * f * gain
|
|
R[p:p + n] = src_r[:n] * f * gain
|
|
p += n
|
|
|
|
# master
|
|
L = onepole_hp(L, 22.0)
|
|
R = onepole_hp(R, 22.0)
|
|
L, R = compress(L, R, -9.0, 2.0, 0.01, 0.15, 1.5)
|
|
L, R = np.tanh(L * 0.9) / 0.9, np.tanh(R * 0.9) / 0.9
|
|
return L, R
|
|
|
|
|
|
def master(L, R, target_lufs=-10.5):
|
|
import pyloudnorm as pyln
|
|
meter = pyln.Meter(SR)
|
|
loud = meter.integrated_loudness(np.stack([L, R], 1))
|
|
g = 10 ** ((target_lufs - loud) / 20)
|
|
L, R = L * g, R * g
|
|
L, R = limit(L, R, 0.89, 0.005, 0.08)
|
|
fade = int(1.5 * SR)
|
|
L[-fade:] *= np.linspace(1, 0, fade) ** 2
|
|
R[-fade:] *= np.linspace(1, 0, fade) ** 2
|
|
return L, R, loud
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import soundfile as sf
|
|
B = build()
|
|
L, R = mix(B)
|
|
L, R, pre = master(L, R)
|
|
import pyloudnorm as pyln
|
|
after = pyln.Meter(SR).integrated_loudness(np.stack([L, R], 1))
|
|
print(f'pre {pre:.1f} LUFS -> {after:.1f} LUFS, peak {max(abs(L).max(), abs(R).max()):.3f}, '
|
|
f'dc {L.mean():.4f}')
|
|
sf.write(sys.argv[1], np.stack([L, R], 1).astype(np.float32), SR, subtype='PCM_24')
|