timmy-talking-turd/video/make_music.py

72 lines
3.5 KiB
Python

#!/usr/bin/env python3
import wave, math
from pathlib import Path
import numpy as np
SR=48000; DURATION=76.0; BPM=100; BEAT=60/BPM
rng=np.random.default_rng(118)
t=np.arange(int(SR*DURATION))/SR
left=np.zeros_like(t); right=np.zeros_like(t)
def add(buf,start,sound,gain=1.0):
i=int(start*SR); j=min(len(buf),i+len(sound));
if i<0 or i>=len(buf): return
buf[i:j]+=sound[:j-i]*gain
def kick():
d=.24; x=np.arange(int(SR*d))/SR; phase=2*np.pi*(72*x + (88-72)*(1-np.exp(-x*28))/28)
return np.sin(phase)*np.exp(-x*18)
def snare():
d=.20; x=np.arange(int(SR*d))/SR; n=rng.normal(0,1,len(x)); tone=np.sin(2*np.pi*190*x)
return (.75*n+.25*tone)*np.exp(-x*20)
def hat(openhat=False):
d=.17 if openhat else .055; x=np.arange(int(SR*d))/SR; n=rng.normal(0,1,len(x));
n=np.concatenate([[0],np.diff(n)]); return n*np.exp(-x*(20 if openhat else 60))*.35
def pluck(freq,d=.34):
x=np.arange(int(SR*d))/SR; sig=(np.sin(2*np.pi*freq*x)+.35*np.sin(2*np.pi*2*freq*x)+.12*np.sin(2*np.pi*3*freq*x));
return sig*np.exp(-x*8)*.25
def bass(freq,d=.48):
x=np.arange(int(SR*d))/SR; env=(1-np.exp(-x*30))*np.exp(-x*3.8); sig=np.sin(2*np.pi*freq*x)+.24*np.sin(2*np.pi*2*freq*x)
return sig*env*.32
def pad(freq,d):
x=np.arange(int(SR*d))/SR; env=np.minimum(1,x/.45)*np.minimum(1,(d-x)/.6); env=np.maximum(env,0)
return (np.sin(2*np.pi*freq*x)+.22*np.sin(2*np.pi*(freq*1.003)*x))*env*.055
K=kick(); S=snare(); H=hat(); OH=hat(True)
roots=[65.41,51.91,77.78,58.27] # C2 Ab1 Eb2 Bb1
chords=[[261.63,311.13,392.00],[207.65,261.63,311.13],[155.56,196.00,233.08],[233.08,293.66,349.23]]
# Four-bar phrases; energy opens gradually, drops under safety section, returns.
for beat_i in range(int(DURATION/BEAT)+1):
sec=beat_i*BEAT; bar=beat_i//4; pos=beat_i%4; section_gain=.72 if 31<=sec<43 else 1.0
add(left,sec,K,.58*section_gain); add(right,sec,K,.58*section_gain)
if pos in (1,3): add(left,sec,S,.18*section_gain); add(right,sec,S,.22*section_gain)
for sub in range(2):
hs=sec+sub*BEAT/2; pan=.86 if (beat_i*2+sub)%2 else 1.0
add(left,hs,H,.10*pan*section_gain);add(right,hs,H,.10*(2-pan)*section_gain)
if pos==3 and bar%2: add(left,sec+BEAT/2,OH,.07*section_gain);add(right,sec+BEAT/2,OH,.11*section_gain)
root=roots[(bar//2)%4]
if pos in (0,2) or (pos==3 and bar%2):
b=bass(root*(2 if pos==2 else 1));add(left,sec,b,.75*section_gain);add(right,sec,b,.70*section_gain)
# pads every 2 bars
for bar in range(int(DURATION/(BEAT*4))+1):
if bar%2: continue
start=bar*4*BEAT; chord=chords[(bar//2)%4]
for idx,f in enumerate(chord):
p=pad(f,8*BEAT);add(left,start,p,.78 if idx!=1 else .58);add(right,start,p,.58 if idx!=1 else .78)
# syncopated bright plucks outside serious segment
melody=[392,466.16,523.25,622.25,523.25,466.16,392,349.23]
for i in range(int(DURATION/(BEAT/2))):
sec=i*BEAT/2
if sec<7 or 31<=sec<43 or i%2==0: continue
p=pluck(melody[(i//2)%len(melody)])
add(left,sec,p,.50 if i%4==1 else .30);add(right,sec,p,.30 if i%4==1 else .50)
# subtle stereo width and fade
fade=np.ones_like(t);fade[:SR*2]=np.linspace(0,1,SR*2);fade[-SR*3:]=np.linspace(1,0,SR*3)
left*=fade;right*=fade
peak=max(np.max(np.abs(left)),np.max(np.abs(right)));left=np.tanh(left*1.25)/(max(1,peak)*1.15);right=np.tanh(right*1.25)/(max(1,peak)*1.15)
stereo=np.stack([left,right],axis=1);pcm=(np.clip(stereo,-1,1)*32767).astype('<i2')
out=Path(__file__).with_name('music-bed.wav')
with wave.open(str(out),'wb') as w:w.setnchannels(2);w.setsampwidth(2);w.setframerate(SR);w.writeframes(pcm.tobytes())
print(out)