72 lines
3.5 KiB
Python
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)
|