#!/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)
