123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271(* Claude Code
*
* Copyright (C) 2026 Yoann Padioleau
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public License
* (LGPL) as published by the Free Software Foundation; either version
* 2 of the License, or (at your option) any later version.
*)(* See Reverb.mli *)typekind=Schroeder|Freeverb|Plateletkinds=[Schroeder;Freeverb;Plate]letname=functionSchroeder->"schroeder"|Freeverb->"freeverb"|Plate->"plate"typesettings={kind:kind;seconds:float;damping:float;mix:float}letrate=float_of_intSignal.rate(*****************************************************************************)(* Delay lines, combs, all-passes *)(*****************************************************************************)(* the last [Array.length samples] samples written, [at] the next *)typeline={samples:floatarray;mutableat:int}letline(n:int):line={samples=Array.make(max2n)0.;at=0}(* [k] samples ago, 1 the last one written *)letread(l:line)(k:int):float=letn=Array.lengthl.samplesinl.samples.((l.at-k+n)modn)(* the same between two samples, linear *)letread_between(l:line)(k:float):float=letwhole=Float.to_intkinletfrac=k-.float_of_intwholein((1.-.frac)*.readlwhole)+.(frac*.readl(whole+1))letwrite(l:line)(x:float):unit=l.samples.(l.at)<-x;l.at<-(l.at+1)modArray.lengthl.samples(* the all-pass, (z^-D - g) / (1 - g z^-D): w = x + g z, y = z - g w,
* w written *)letall_pass(l:line)(d:int)(g:float)(x:float):float=letz=readldinletw=x+.(g*.z)inwritelw;z-.(g*.w)(* the comb's feedback for a decay of 60 dB in [seconds]: a comb of
* [d] samples loses 60 dB after seconds / (d / rate) trips *)letfeedback_for(d:int)(seconds:float):float=10.**(-3.*.float_of_intd/.rate/.Float.max0.05seconds)(*****************************************************************************)(* Schroeder's *)(*****************************************************************************)typeschroeder={combs:linearray;passes:linearray}letschroeder_combs=Array.map(funms->Signal.samples(ms/.1000.))[|29.7;37.1;41.1;43.7|]letschroeder_passes=[|Signal.samples0.005;Signal.samples0.0017|]letschroeder():schroeder={combs=Array.map(fund->line(d+1))schroeder_combs;passes=Array.map(fund->line(d+1))schroeder_passes}letschroeder_sample(r:schroeder)(seconds:float)(x:float):float=letsum=ref0.inArray.iteri(funil->letd=schroeder_combs.(i)inlety=x+.(feedback_fordseconds*.readld)inwritely;sum:=!sum+.y)r.combs;lety=ref(0.25*.!sum)inArray.iteri(funil->y:=all_passlschroeder_passes.(i)0.7!y)r.passes;!y(*****************************************************************************)(* Freeverb *)(*****************************************************************************)(* Jezar's numbers, in samples at 44,100; the right channel's all 23
* longer *)letfreeverb_combs=[|1116;1188;1277;1356;1422;1491;1557;1617|]letfreeverb_passes=[|556;441;341;225|]letspread=23typechannel={fcombs:linearray;stores:floatarray(* each comb's low-pass *);fpasses:linearray}typefreeverb={fleft:channel;fright:channel}letchannel(extra:int):channel={fcombs=Array.map(fund->line(d+extra+1))freeverb_combs;stores=Array.make80.;fpasses=Array.map(fund->line(d+extra+1))freeverb_passes;}letfreeverb():freeverb={fleft=channel0;fright=channelspread}(* Freeverb's all-pass, as Jezar wrote it: not quite one (its
* feed-forward is -1 where a true all-pass has -g), kept *)letfreeverb_pass(l:line)(d:int)(x:float):float=letz=readldinwritel(x+.(0.5*.z));z-.xletfreeverb_sample(c:channel)(extra:int)(seconds:float)(damp:float)(x:float):float=letsum=ref0.inArray.iteri(funil->letd=freeverb_combs.(i)+extrainletout=readldinc.stores.(i)<-(out*.(1.-.damp))+.(c.stores.(i)*.damp);writel(x+.(c.stores.(i)*.feedback_fordseconds));sum:=!sum+.out)c.fcombs;lety=ref!suminArray.iteri(funil->y:=freeverb_passl(freeverb_passes.(i)+extra)!y)c.fpasses;!y(*****************************************************************************)(* Dattorro's plate *)(*****************************************************************************)(* the paper's lengths are at 29,761 Hz *)letscale(d:int):int=Float.to_int(Float.round(float_of_intd*.rate/.29761.))(* the input's four diffusers *)letdiffusers=[|(scale142,0.75);(scale107,0.75);(scale379,0.625);(scale277,0.625)|](* a half of the tank: a modulated all-pass, a delay, the damping, an
* all-pass, a delay *)typehalf={modulated:line;mod_length:int;first:line;first_length:int;mutabledamped:float;pass:line;pass_length:int;second:line;second_length:int;phase:float;(* the modulation's, 0 or a quarter turn *)}(* room for the modulation's excursion, in samples *)letexcursion_room=64lethalf(m:int)(d1:int)(p:int)(d2:int)(phase:float):half=letm=scalemandd1=scaled1andp=scalepandd2=scaled2in{modulated=line(m+excursion_room);mod_length=m;first=line(d1+1);first_length=d1;damped=0.;pass=line(p+1);pass_length=p;second=line(d2+1);second_length=d2;phase;}typeplate={mutablebandwidth:float;inputs:linearray;left:half;right:half;mutabletime:float}letplate():plate={bandwidth=0.;inputs=Array.map(fun(d,_)->line(d+1))diffusers;left=half6724453180037200.;right=half908421726563163(Float.pi/.2.);time=0.;}(* the loop's length, the four delays and four all-passes, in seconds:
* a sound goes round it through [decay] four times *)letloop_seconds=float_of_int(672+4453+1800+3720+908+4217+2656+3163)/.29761.letexcursion=float_of_int(scale16)(* one half's sample: its input, the other half's end fed back in *)lettank(h:half)(time:float)(decay:float)(damping:float)(x:float):unit=(* the modulated all-pass, decay diffusion 1 (-0.7), its length
* wobbling by 16 samples at 1 Hz *)letd=float_of_inth.mod_length+.(excursion*.sin((2.*.Float.pi*.time)+.h.phase))inletz=read_betweenh.modulateddinletw=x+.(-0.7*.z)inwriteh.modulatedw;lety=z-.(-0.7*.w)inletdelayed=readh.firsth.first_lengthinwriteh.firsty;h.damped<-((1.-.damping)*.delayed)+.(damping*.h.damped);writeh.second(all_passh.passh.pass_length0.5(decay*.h.damped))letplate_sample(p:plate)(seconds:float)(damping:float)(x:float):float*float=letdecay=Float.min0.99(10.**(-3.*.loop_seconds/.(4.*.Float.max0.05seconds)))in(* the input: band-limited (bandwidth 0.9995), then diffused *)p.bandwidth<-(0.9995*.x)+.(0.0005*.p.bandwidth);letv=refp.bandwidthinArray.iteri(funi(d,g)->v:=all_passp.inputs.(i)dg!v)diffusers;letl=p.leftandr=p.rightinletleft_end=readl.secondl.second_lengthandright_end=readr.secondr.second_lengthintanklp.timedecaydamping(!v+.(decay*.right_end));tankrp.timedecaydamping(!v+.(decay*.left_end));p.time<-p.time+.(1./.rate);ifp.time>1.thenp.time<-p.time-.1.;(* the output: seven taps each side, mostly from the other half *)letat(line:line)k=readline(scalek)inletyl=atr.first266+.atr.first2974-.atr.pass1913+.atr.second1996-.atl.first1990-.atl.pass187-.atl.second1066andyr=atl.first353+.atl.first3627-.atl.pass1228+.atl.second2673-.atr.first2111-.atr.pass335-.atr.second121in(0.6*.yl,0.6*.yr)(*****************************************************************************)(* The effect *)(*****************************************************************************)typet={schroeder:schroeder;freeverb:freeverb;plate:plate;mutablelast_mix:float(* nan: none yet *)}letcreate():t={schroeder=schroeder();freeverb=freeverb();plate=plate();last_mix=Float.nan}letprocess(t:t)(s:settings)(out:Signal.stereo):unit=letn=Array.lengthout.leftandfrom_mix=ifFloat.is_nant.last_mixthens.mixelset.last_mixinArray.iteri(funixl->letxr=out.right.(i)inletx=(xl+.xr)/.2.inletwl,wr=matchs.kindwith|Schroeder->lety=schroeder_samplet.schroeders.secondsxin(y,y)|Freeverb->(* Jezar's fixed gain (0.015) into the eight combs, and his
* wet scale (3) out *)letdamp=0.4*.s.dampingandx=0.015*.(xl+.xr)in(3.*.freeverb_samplet.freeverb.fleft0s.secondsdampx,3.*.freeverb_samplet.freeverb.frightspreads.secondsdampx)|Plate->plate_samplet.plates.seconds(0.7*.s.damping)xinletmix=Effect.rampfrom_mixs.mixininout.left.(i)<-xl+.(mix*.wl);out.right.(i)<-xr+.(mix*.wr))out.left;t.last_mix<-s.mixletknobs:Effect.knoblist=[{name="kind";control=Selector(List.mapnamekinds);initial=2.};{name="time";control=Knob(0.3,8.);initial=2.};{name="damping";control=Knob(0.,1.);initial=0.3};{name="mix";control=Knob(0.,1.);initial=0.25};]letfx():Effect.t=lett=create()ands=ref{kind=Plate;seconds=0.;damping=0.;mix=0.}inletset(knob:string)(x:float)=matchknobwith|"kind"->s:={!swithkind=List.nthkinds(max0(min(List.lengthkinds-1)(Control.indexx)))}|"time"->s:={!swithseconds=x}|"damping"->s:={!swithdamping=x}|"mix"->s:={!swithmix=x}|_->()inList.iter(fun(k:Effect.knob)->setk.namek.initial)knobs;{name="reverb";knobs;set;process=(funout->processt!sout);meters=(fun()->[])}