import numpy as np, scipy.signal as sig, scipy.io.wavfile as wav, os, json
SR=44100
ROOT=os.path.dirname(__file__)
rng=np.random.default_rng(2035)
def hz(m): return 440*2**((m-69)/12)
def tone(note,dur,kind='bass',vel=1):
 n=max(1,int(dur*SR));t=np.arange(n)/SR;f=hz(note)
 if kind=='bass':
  # FM metallic low-string voice, sub foundation and palm-muted decay.
  a=np.exp(-t*5)+.16; x=np.sin(2*np.pi*f*t+2.8*np.exp(-t*9)*np.sin(2*np.pi*f*2*t));x=np.tanh(3*x)*.46+.32*np.sin(2*np.pi*f*t);x*=a
 elif kind=='lead':
  x=np.sin(2*np.pi*f*t+1.3*np.sin(2*np.pi*f*2*t))*.6+np.sin(2*np.pi*f*1.004*t)*.25;x*=np.minimum(t/.012,1)*np.minimum((dur-t)/.065,1)
 elif kind=='pad':
  x=sum(np.sin(2*np.pi*f*k*t+v*np.sin(2*np.pi*.2*t))*.22/k for k,v in [(1,1),(2,.6),(3,.4)]);x*=np.sin(np.pi*t/dur)**1.1
 elif kind=='pluck':
  x=(np.sin(2*np.pi*f*t)+.35*np.sin(2*np.pi*f*3*t))*np.exp(-t*8)
 x*=np.minimum(t/.003,1)*np.minimum((dur-t)/.015,1)
 return x*vel

def drum(kind):
 dur={'kick':.45,'snare':.32,'hat':.09,'open':.38,'crash':1.6,'tom':.4}[kind];t=np.arange(int(dur*SR))/SR;noise=rng.normal(0,1,len(t))
 if kind=='kick':
  phase=2*np.pi*(45*t+100*.026*(1-np.exp(-t/.026)));x=np.sin(phase)*np.exp(-t*10)+noise*np.exp(-t*180)*.18;x=np.tanh(x*1.7)*.8
 elif kind=='snare':
  no=sig.sosfilt(sig.butter(2,1500,fs=SR,btype='high',output='sos'),noise);x=.55*no*np.exp(-t*16)+.32*np.sin(2*np.pi*185*t)*np.exp(-t*24)
 elif kind in ['hat','open','crash']:
  no=sig.sosfilt(sig.butter(2,7000,fs=SR,btype='high',output='sos'),noise);x=no*np.exp(-t*({'hat':65,'open':12,'crash':3}[kind]))*.22
 else:x=np.sin(2*np.pi*(85*t+40*.05*(1-np.exp(-t/.05))))*np.exp(-t*10)*.65
 return x*np.minimum(t/.001,1)

def render(title,bpm,bars,mode):
 beat=60/bpm;length=bars*4*beat;buf=np.zeros((int((length+4)*SR),2),np.float64)
 drums={k:drum(k) for k in ['kick','snare','hat','open','crash','tom']}
 def add(x,start,g=.5,pan=0):
  i=int(start*SR);end=min(len(buf),i+len(x));xx=x[:end-i]*g
  buf[i:end,0]+=xx*np.sqrt((1-pan)/2);buf[i:end,1]+=xx*np.sqrt((1+pan)/2)
 def note(m,start,dur,kind,g,pan=0):add(tone(m,dur,kind),start,g,pan)
 roots=[28,28,31,29,28,34,33,29] # E minor/Phrygian colour, original progression.
 for bar in range(bars):
  tm=bar*4*beat;sec=bar//8;r=roots[(bar//2)%8]
  if mode=='menu':
   r=[40,41,43,38][(bar//4)%4]
   for m,p in [(r,-.6),(r+7,.6),(r+12,0)]:note(m,tm,4*beat+.6,'pad',.21,p)
   for j in range(8):
    if j in [0,3,5,7]:note(r+24+[0,7,12,3,7,15,12,7][j],tm+j*.5*beat,.75*beat,'pluck',.14,(-1)**j*.45)
   if bar>=8:
    for b in [0,2.75]:add(drums['kick'],tm+b*beat,.30)
    for b in [1.5,3.5]:add(drums['hat'],tm+b*beat,.3,.3)
   continue
  intro=(bar<4);breakdown=(mode=='combat' and 32<=bar<40);outro=(bar>=bars-4)
  busy=not intro and not breakdown
  # Asymmetric eighth/sixteenth chug pattern; power-octave layers.
  pattern=[(0,0),(.5,0),(.75,0),(1.5,7),(2,0),(2.5,1),(3,0),(3.5,3)] if mode=='combat' else [(0,0),(.25,0),(.75,0),(1,7),(1.75,0),(2,0),(2.25,1),(2.75,0),(3.25,6),(3.5,3)]
  if breakdown:pattern=[(0,0),(1.5,1),(3,0)]
  for pos,interval in pattern:
   du=(.18 if interval==0 else .31)*beat
   note(r+interval,tm+pos*beat,du,'bass',.59,-.16)
   note(r+12+interval,tm+pos*beat+.006,du,'bass',.23,.30)
  kicks=[0,1.5,2,2.75] if mode=='combat' else [0,.75,1.5,2,2.5,3.25]
  if breakdown:kicks=[0,2.5]
  for p in kicks:add(drums['kick'],tm+p*beat,.74)
  if not intro:
   for p in ([2] if breakdown else [1,3]):add(drums['snare'],tm+p*beat,.76,-.04)
  for j in range(8 if busy else 4):
   p=j*(.5 if busy else 1);add(drums['open' if j==7 and bar%2 else 'hat'],tm+p*beat,.55 if j%2==0 else .35,.4)
  if bar%8==0:add(drums['crash'],tm,.65,-.4)
  if bar%8==7:
   for k in range(4):add(drums['tom' if k<2 else 'snare'],tm+(3+k*.25)*beat,.50,(-.5+k/3))
  if sec in ([1,3,5,7] if mode=='combat' else [1,2,4,5,7]):
   motif=[0,7,10,12,7,3,1,0] if mode=='combat' else [12,7,13,12,6,3,7,1]
   for j in range(4):
    m=r+24+motif[(j+bar%2*4)%8];st=tm+(j+.15)*beat
    note(m,st,.6*beat,'lead',.16,-.10)
    note(m,st+.75*beat,.6*beat,'lead',.055,.55)
  if sec%2==0 or breakdown:
   for m,p in [(r+24,-.7),(r+31,.7)]:note(m,tm,4*beat+.4,'pad',.11,p)
 # Gentle stereo room, kept low so transients retain punch.
 dry=buf.copy()
 for delay,amount in [(.071,.07),(.113,.05),(.197,.035)]:
  s=int(delay*SR);buf[s:,0]+=dry[:-s,1]*amount;buf[s:,1]+=dry[:-s,0]*amount
 # DC removal + soft bus saturation then peak-normalize. No copyrighted samples.
 buf=sig.sosfilt(sig.butter(2,26,fs=SR,btype='high',output='sos'),buf,axis=0)
 buf=np.tanh(buf*1.2)
 # Fold tail to start for exact-bar loop, then peak normalize.
 n=int(length*SR);tail=buf[n:];loop=buf[:n].copy();loop[:len(tail)]+=tail
 loop*=.91/max(np.max(np.abs(loop)),1e-6)
 # Very short edge taper prevents discontinuity; musically loop aligns downbeat.
 ramp=np.linspace(0,1,int(.003*SR));loop[:len(ramp)]*=ramp[:,None];loop[-len(ramp):]*=ramp[::-1,None]
 path=os.path.join(ROOT,title+'_loop.wav');wav.write(path,SR,(loop*32767).astype(np.int16))
 # Listening version: fade in/out for standalone soundtrack playback.
 listen=loop.copy();fi=int(.04*SR);fo=int(3*SR);listen[:fi]*=np.linspace(0,1,fi)[:,None];listen[-fo:]*=np.linspace(1,0,fo)[:,None]
 wav.write(os.path.join(ROOT,title+'.wav'),SR,(listen*32767).astype(np.int16))
 return {'title':title,'bpm':bpm,'bars':bars,'duration_seconds':len(loop)/SR,'peak_dbfs':float(20*np.log10(np.max(np.abs(loop)))),'rms_dbfs':float(20*np.log10(np.sqrt(np.mean(loop**2)))),'loop_samples':len(loop)}
tracks=[render('01_Titan_Approach',96,24,'menu'),render('02_Hell_Like_Fight',138,64,'combat'),render('03_The_Auditor',154,48,'boss')]
open(ROOT+'/metadata.json','w').write(json.dumps(tracks,indent=2))
print(json.dumps(tracks,indent=2))
