320 lines
10 KiB
Python
320 lines
10 KiB
Python
# Usage: python music.py, in a venv with numpy.
|
|
import wave
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
|
|
SR = 48000
|
|
BEAT = 60 / 112
|
|
BARS = 42
|
|
N = round(BARS * 4 * BEAT * SR)
|
|
OUT = Path(__file__).parent / "out" / "music.wav"
|
|
TARGET_LUFS = -16.0
|
|
CEILING = 10 ** (-1.0 / 20)
|
|
|
|
rng = np.random.default_rng(1120)
|
|
dry = np.zeros((N, 2))
|
|
send = np.zeros((N, 2))
|
|
|
|
# (bass root, pad voicing, arpeggio tones): D A Bm G
|
|
CHORDS = [
|
|
(38, [50, 57, 62, 66, 69], [62, 66, 69, 74]),
|
|
(45, [49, 57, 61, 64, 69], [61, 64, 69, 73]),
|
|
(47, [47, 59, 62, 66, 71], [62, 66, 71, 74]),
|
|
(43, [43, 59, 62, 67, 71], [62, 67, 71, 74]),
|
|
]
|
|
ARP = [0, 1, 2, 1, 3, 2, 1, 2]
|
|
INTRO = [(0, 74), (4, 73), (6, 76), (8, 78), (10, 74), (12, 79), (14, 78)]
|
|
COUNTER = [(0, 78, 2), (2, 76, 1), (3, 74, 1), (4, 76, 3), (7, 73, 1),
|
|
(8, 74, 2), (10, 78, 2), (12, 79, 2), (14, 78, 1), (15, 76, 1)]
|
|
|
|
|
|
def at(beat):
|
|
return round(beat * BEAT * SR)
|
|
|
|
|
|
def hz(m):
|
|
return 440.0 * 2 ** ((m - 69) / 12)
|
|
|
|
|
|
def times(sec):
|
|
return np.arange(int(sec * SR)) / SR
|
|
|
|
|
|
def attack(sig, sec):
|
|
a = max(1, int(sec * SR))
|
|
sig[:a] *= np.linspace(0.0, 1.0, a)
|
|
return sig
|
|
|
|
|
|
def tail(sig, sec=0.02):
|
|
r = int(sec * SR)
|
|
ramp = np.linspace(1.0, 0.0, r)
|
|
sig[-r:] *= ramp if sig.ndim == 1 else ramp[:, None]
|
|
return sig
|
|
|
|
|
|
def put(sig, beat, gain=1.0, pan=0.0, rev=0.2):
|
|
i = at(beat)
|
|
if sig.ndim == 1:
|
|
th = (pan + 1) * np.pi / 4
|
|
sig = np.stack([sig * np.cos(th), sig * np.sin(th)], 1) * np.sqrt(2)
|
|
n = min(len(sig), N - i)
|
|
dry[i:i + n] += sig[:n] * gain
|
|
send[i:i + n] += sig[:n] * gain * rev
|
|
|
|
|
|
def band_noise(n, lo, hi):
|
|
spec = np.fft.rfft(rng.standard_normal(n))
|
|
f = np.fft.rfftfreq(n, 1 / SR)
|
|
spec *= (f > lo) * (f < hi)
|
|
out = np.fft.irfft(spec, n)
|
|
return out / np.abs(out).max()
|
|
|
|
|
|
def pluck(m, dur=1.0, tau=0.45, partials=8):
|
|
t = times(dur)
|
|
s = np.zeros_like(t)
|
|
for h in range(1, partials + 1):
|
|
if hz(m) * h > 15000:
|
|
break
|
|
s += np.sin(2 * np.pi * hz(m) * h * t) * np.exp(-t * (1 + 0.8 * (h - 1)) / tau) / h ** 1.4
|
|
return tail(attack(s * 0.5, 0.002))
|
|
|
|
|
|
def mallet(m, dur=1.6, tau=0.7):
|
|
t = times(dur)
|
|
f = hz(m)
|
|
s = (np.sin(2 * np.pi * f * t) * np.exp(-t / tau)
|
|
+ 0.25 * np.sin(2 * np.pi * 2 * f * t) * np.exp(-t / (tau / 3))
|
|
+ 0.12 * np.sin(2 * np.pi * 4.0 * f * t) * np.exp(-t / (tau / 8)))
|
|
return tail(attack(s * 0.5, 0.003))
|
|
|
|
|
|
def tick(level):
|
|
t = times(0.08)
|
|
s = (np.sin(2 * np.pi * 1750 * t) * np.exp(-t / 0.009)
|
|
+ 0.5 * np.sin(2 * np.pi * 2950 * t) * np.exp(-t / 0.005)
|
|
+ 0.3 * band_noise(len(t), 2000, 8000) * np.exp(-t / 0.0015))
|
|
return tail(attack(s * level, 0.0003), 0.01)
|
|
|
|
|
|
def kick(level=1.0, decay=0.22):
|
|
t = times(0.7)
|
|
f = 46 + 70 * np.exp(-t / 0.032)
|
|
s = np.sin(2 * np.pi * np.cumsum(f) / SR) * np.exp(-t / decay)
|
|
return tail(attack(s * level, 0.0015))
|
|
|
|
|
|
def snap():
|
|
t = times(0.3)
|
|
e = sum(np.exp(-np.clip(t - d, 0, None) / 0.004) * (t >= d) for d in (0.0, 0.008, 0.016))
|
|
e = e + 0.45 * np.exp(-t / 0.055)
|
|
s = band_noise(len(t), 900, 5000) * e
|
|
return tail(attack(s * 0.5, 0.0004))
|
|
|
|
|
|
def shaker():
|
|
t = times(0.09)
|
|
s = band_noise(len(t), 5000, 12000) * np.exp(-t / 0.022)
|
|
return tail(attack(s * 0.3, 0.003))
|
|
|
|
|
|
def bass(m, dur):
|
|
t = times(dur)
|
|
w = 2 * np.pi * hz(m) * t
|
|
s = (np.sin(w) + 0.28 * np.sin(2 * w) + 0.08 * np.sin(3 * w)) * np.exp(-t / 0.9)
|
|
return tail(attack(np.tanh(1.4 * s) / np.tanh(1.4), 0.004), 0.04)
|
|
|
|
|
|
def pad(voices, sec, rise, release=0.6, decay=None):
|
|
t = times(sec + release)
|
|
out = np.zeros((len(t), 2))
|
|
for m in voices:
|
|
for side, cents in ((0, -6), (1, 6)):
|
|
f = hz(m) * 2 ** (cents / 1200)
|
|
ph = rng.uniform(0, 2 * np.pi, 4)
|
|
for h, a in enumerate((1.0, 0.4, 0.18, 0.07), 1):
|
|
out[:, side] += a * np.sin(2 * np.pi * f * h * t + ph[h - 1])
|
|
env = np.ones(len(t))
|
|
r = int(rise * SR)
|
|
env[:r] = np.sin(np.linspace(0, np.pi / 2, r)) ** 2
|
|
if decay:
|
|
env *= np.exp(-t / decay)
|
|
else:
|
|
k = int(sec * SR)
|
|
env[k:] *= np.cos(np.linspace(0, np.pi / 2, len(t) - k)) ** 2
|
|
return tail(out * env[:, None] / len(voices) * 0.35, 0.05)
|
|
|
|
|
|
def bar_start(b):
|
|
return b * 4
|
|
|
|
|
|
def intro(b):
|
|
for beat in range(4):
|
|
put(tick(1.0 if beat == 0 else 0.6), bar_start(b) + beat, 0.16, 0.0, 0.1)
|
|
for beat, m in INTRO:
|
|
if beat // 4 == b:
|
|
put(pluck(m, 2.2, 0.9), beat, 0.55, 0.15, 0.35)
|
|
|
|
|
|
def build(b):
|
|
root, voices, tones = CHORDS[b % 4]
|
|
s = bar_start(b)
|
|
for beat in range(4):
|
|
put(tick(1.0 if beat == 0 else 0.6), s + beat, 0.14, 0.0, 0.1)
|
|
put(pluck(tones[ARP[beat * 2]], 1.4, 0.6), s + beat, 0.38, 0.25 if beat % 2 else -0.25, 0.3)
|
|
for beat in (0, 2):
|
|
put(kick(0.55), s + beat, 0.8, 0.0, 0.03)
|
|
put(pad(voices, 4 * BEAT, 1.6 if b == 4 else 0.25), s, 0.55 + 0.05 * (b - 4), rev=0.35)
|
|
|
|
|
|
def groove(b, lift, arp_up=0, sixteenths=False, four=False, counter=False):
|
|
root, voices, tones = CHORDS[b % 4]
|
|
s = bar_start(b)
|
|
for beat in (range(4) if four else (0, 2)):
|
|
put(kick(0.9), s + beat, 0.95, 0.0, 0.03)
|
|
for beat in (1, 3):
|
|
put(snap(), s + beat, 0.42 * lift, 0.05, 0.25)
|
|
for beat in range(4):
|
|
put(tick(0.7 if beat else 1.0), s + beat, 0.09, -0.1, 0.1)
|
|
if lift > 1.0:
|
|
for beat in range(4):
|
|
put(shaker(), s + beat + 0.5, 0.2 * lift, 0.35, 0.1)
|
|
step = 0.25 if sixteenths else 0.5
|
|
for k in range(int(4 / step)):
|
|
m = tones[ARP[k % 8]] + arp_up
|
|
put(pluck(m, 0.9, 0.32), s + k * step, 0.33 * lift * (1.0 if k % 2 == 0 else 0.8),
|
|
-0.35 if k % 2 else 0.35, 0.28)
|
|
for off, dur, interval in ((0, 1.4, 0), (1.5, 0.45, 0), (2, 1.4, 0), (3.5, 0.45, 7)):
|
|
put(bass(root + interval, dur * BEAT), s + off, 0.5, 0.0, 0.02)
|
|
put(pad(voices, 4 * BEAT, 0.25), s, 0.42, rev=0.35)
|
|
if counter:
|
|
for beat, m, length in COUNTER:
|
|
if beat // 4 == b % 4:
|
|
put(mallet(m, length * BEAT + 0.8, 0.6), s + beat % 4, 0.42, 0.3, 0.4)
|
|
|
|
|
|
def interlude(b):
|
|
root, voices, tones = CHORDS[b % 4]
|
|
s = bar_start(b)
|
|
put(pad(voices, 4 * BEAT, 0.6 if b == 32 else 0.25), s, 0.7, rev=0.45)
|
|
put(pluck(tones[3], 2.4, 1.0), s, 0.45, 0.2, 0.45)
|
|
put(pluck(tones[1], 2.0, 0.9), s + 2, 0.35, -0.2, 0.45)
|
|
if b % 2:
|
|
put(pluck(tones[2], 1.6, 0.8), s + 3, 0.28, 0.1, 0.45)
|
|
|
|
|
|
def ending():
|
|
s = bar_start(40)
|
|
put(kick(1.0, 0.45), s, 1.0, 0.0, 0.05)
|
|
put(snap(), s, 0.35, 0.0, 0.4)
|
|
put(bass(38, 3.6), s, 0.6, 0.0, 0.05)
|
|
put(pad([50, 57, 62, 66, 69, 74], 4.2, 0.01, 0.0, decay=1.3), s, 0.9, rev=0.45)
|
|
for m, pan in ((62, -0.3), (66, 0.3), (69, -0.15), (74, 0.15), (78, 0.0)):
|
|
put(mallet(m, 4.2, 1.1), s, 0.32, pan, 0.45)
|
|
put(pluck(86, 3.0, 1.2), s, 0.2, 0.0, 0.5)
|
|
|
|
|
|
def arrange():
|
|
for b in range(40):
|
|
if b < 4:
|
|
intro(b)
|
|
elif b < 8:
|
|
build(b)
|
|
elif b < 20:
|
|
groove(b, 1.0)
|
|
elif b < 32:
|
|
groove(b, 1.12, arp_up=12, counter=True)
|
|
elif b < 36:
|
|
interlude(b)
|
|
else:
|
|
groove(b, 0.85 + 0.1 * (b - 36), sixteenths=b >= 38, four=b >= 38)
|
|
ending()
|
|
|
|
|
|
def reverb(x):
|
|
n_ir = int(2.4 * SR)
|
|
t = np.arange(n_ir) / SR
|
|
f = np.fft.rfftfreq(n_ir, 1 / SR)
|
|
nfft = 1 << int(np.ceil(np.log2(N + n_ir)))
|
|
out = np.zeros_like(x)
|
|
for c in range(2):
|
|
ir = np.fft.irfft(np.fft.rfft(rng.standard_normal(n_ir)) / (1 + (f / 5000) ** 2), n_ir)
|
|
ir *= np.exp(-t / 0.32)
|
|
ir = np.concatenate([np.zeros(int(0.015 * SR)), ir])[:n_ir]
|
|
ir /= np.sqrt(np.sum(ir ** 2))
|
|
out[:, c] = np.fft.irfft(np.fft.rfft(x[:, c], nfft) * np.fft.rfft(ir, nfft), nfft)[:N]
|
|
return out
|
|
|
|
|
|
def biquad(f, b, a):
|
|
z = np.exp(-2j * np.pi * f / SR)
|
|
return (b[0] + b[1] * z + b[2] * z ** 2) / (a[0] + a[1] * z + a[2] * z ** 2)
|
|
|
|
|
|
def lufs(x):
|
|
f = np.fft.rfftfreq(len(x), 1 / SR)
|
|
k = (biquad(f, (1.53512485958697, -2.69169618940638, 1.19839281085285),
|
|
(1.0, -1.69065929318241, 0.73248077421585))
|
|
* biquad(f, (1.0, -2.0, 1.0), (1.0, -1.99004745483398, 0.99007225036621)))
|
|
y = np.fft.irfft(np.fft.rfft(x, axis=0) * k[:, None], len(x), axis=0)
|
|
c = np.concatenate([[0.0], np.cumsum(np.sum(y ** 2, 1))])
|
|
starts = np.arange(0, len(x) - 19200 + 1, 4800)
|
|
p = (c[starts + 19200] - c[starts]) / 19200
|
|
p = p[-0.691 + 10 * np.log10(p + 1e-20) > -70]
|
|
p = p[-0.691 + 10 * np.log10(p) > -0.691 + 10 * np.log10(p.mean()) - 10]
|
|
return -0.691 + 10 * np.log10(p.mean())
|
|
|
|
|
|
def run_min(x, w):
|
|
n = len(x)
|
|
xp = np.concatenate([x, np.ones(-(-(n + w) // w) * w - n)]).reshape(-1, w)
|
|
pre = np.minimum.accumulate(xp, 1).ravel()
|
|
suf = np.minimum.accumulate(xp[:, ::-1], 1)[:, ::-1].ravel()
|
|
return np.minimum(suf[:n], pre[w - 1:w - 1 + n])
|
|
|
|
|
|
def limit(x, half=480):
|
|
need = np.minimum(1.0, CEILING / np.maximum(np.abs(x).max(1), 1e-12))
|
|
hold = run_min(np.concatenate([np.ones(half), need, np.ones(half)]), 2 * half + 1)
|
|
h = half // 2
|
|
c = np.concatenate([[0.0], np.cumsum(np.concatenate([np.ones(h), hold[:N], np.ones(h)]))])
|
|
gain = (c[2 * h + 1:] - c[:-2 * h - 1]) / (2 * h + 1)
|
|
return np.clip(x * gain[:, None], -CEILING, CEILING)
|
|
|
|
|
|
def master():
|
|
mix = dry + reverb(send) * 0.32
|
|
fade = np.ones(N)
|
|
k = int(1.4 * SR)
|
|
fade[-k:] = np.cos(np.linspace(0, np.pi / 2, k)) ** 2
|
|
mix *= fade[:, None]
|
|
g = 1.0
|
|
for _ in range(4):
|
|
g *= 10 ** ((TARGET_LUFS - lufs(limit(mix * g))) / 20)
|
|
return limit(mix * g)
|
|
|
|
|
|
def write(x):
|
|
OUT.parent.mkdir(parents=True, exist_ok=True)
|
|
pcm = np.round(x * 8388607).astype("<i4").view(np.uint8).reshape(-1, 4)[:, :3]
|
|
with wave.open(str(OUT), "wb") as w:
|
|
w.setnchannels(2)
|
|
w.setsampwidth(3)
|
|
w.setframerate(SR)
|
|
w.writeframes(pcm.tobytes())
|
|
|
|
|
|
def main():
|
|
arrange()
|
|
out = master()
|
|
write(out)
|
|
print(f"{OUT}: {N / SR:.3f} s, peak {20 * np.log10(np.abs(out).max()):.2f} dBFS, "
|
|
f"{lufs(out):.2f} LUFS")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|