123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162(* 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 Dynamics.mli *)typemode=Compressor|Limiter|Gateletmodes=[Compressor;Limiter;Gate]letname=functionCompressor->"compressor"|Limiter->"limiter"|Gate->"gate"typedetector=Peak|Rmstypesettings={mode:mode;threshold:float;ratio:float;knee:float;attack:float;release:float;makeup:float;detector:detector;lookahead:float;}letcompressor={mode=Compressor;threshold=-20.;ratio=4.;knee=6.;attack=0.005;release=0.1;makeup=0.;detector=Rms;lookahead=0.;}letlimiter={compressorwithmode=Limiter;threshold=-1.;knee=0.;attack=0.0005;release=0.05;detector=Peak;lookahead=0.005}letgate={compressorwithmode=Gate;threshold=-40.;ratio=10.;knee=0.;attack=0.001;release=0.1;detector=Peak}letrate=float_of_intSignal.rateletcurve(s:settings)(x:float):float=lett=s.thresholdandw=s.kneeinmatchs.modewith|Gate->ifx>=tthenxelset+.((x-.t)*.s.ratio)|Compressor|Limiter->letr=ifs.mode=LimiterthenFloat.infinityelseFloat.max1.s.ratioinletover=x-.tinif2.*.over<-.wthenxelseif2.*.Float.absover<=w&&w>0.thenx+.((1./.r)-.1.)*.((over+.(w/.2.))**2.)/.(2.*.w)elset+.(over/.r)(* the look-ahead's line, per side: the last [Array.length] samples *)typeline={samples:floatarray;mutableat:int}letlongest_lookahead=Signal.samples0.02typet={mutablereduction:float;(* dB, smoothed *)mutablemean_square:float;(* the RMS detector's *)left:line;right:line;heard:line;(* the detector's input, for the look-ahead window's loudest *)mutablelast_makeup:float;(* nan: none yet *)}letcreate():t=letline()={samples=Array.make(longest_lookahead+1)0.;at=0}in{reduction=0.;mean_square=0.;left=line();right=line();heard=line();last_makeup=Float.nan}letreduction(t:t):float=t.reduction(* [x] in, the sample [d] ago out *)letdelayed(l:line)(d:int)(x:float):float=letn=Array.lengthl.samplesinl.samples.(l.at)<-x;lety=l.samples.((l.at-d+n)modn)inl.at<-(l.at+1)modn;y(* a one-pole's coefficient for a time constant: 63% of the way in
* [seconds] *)letpole(seconds:float):float=exp(-1./.(Float.max1e-5seconds*.rate))letrms_pole=pole0.01letprocess?(key:Signal.stereooption)(t:t)(s:settings)(out:Signal.stereo):unit=letn=Array.lengthout.leftinletattack=poles.attackandrelease=poles.releaseinletd=minlongest_lookahead(Signal.sampless.lookahead)inletfrom_makeup=ifFloat.is_nant.last_makeupthens.makeupelset.last_makeupinfori=0ton-1doletheard=matchkeywithSomek->k|None->outinletv=Float.max(Float.absheard.left.(i))(Float.absheard.right.(i))in(* the limiter hears the loudest of its look-ahead window: the
* samples not out yet, one of which is the next out *)letv=ifs.mode=Limiter&&d>0thenbeginignore(delayedt.hearddv);letn=Array.lengtht.heard.samplesandloudest=ref0.infork=1tod+1doloudest:=Float.max!loudestt.heard.samples.((t.heard.at-k+n)modn)done;!loudestendelsevin(* the level, in dB *)letlevel=matchs.detectorwith|Peak->20.*.log10(Float.max1e-6v)|Rms->t.mean_square<-(rms_pole*.t.mean_square)+.((1.-.rms_pole)*.v*.v);10.*.log10(Float.max1e-12t.mean_square)in(* the curve's reduction, smoothed: fast up, slow down; the limiter
* down at once (its window saw the peak coming: no overshoot) *)letwanted=level-.curveslevelinleta=ifwanted>t.reductionthenifs.mode=Limiterthen0.elseattackelsereleaseint.reduction<-(a*.t.reduction)+.((1.-.a)*.wanted);letgain=Mix.of_decibels(Effect.rampfrom_makeups.makeupin-.t.reduction)inout.left.(i)<-gain*.delayedt.leftdout.left.(i);out.right.(i)<-gain*.delayedt.rightdout.right.(i)done;t.last_makeup<-s.makeupletknobs:Effect.knoblist=[{name="mode";control=Selector(List.mapnamemodes);initial=0.};{name="threshold";control=Knob(-60.,0.);initial=-20.};{name="ratio";control=Knob(1.,20.);initial=4.};{name="attack";control=Knob(0.0005,0.1);initial=0.005};{name="release";control=Knob(0.01,1.);initial=0.1};{name="makeup";control=Knob(0.,24.);initial=0.};]letfx():Effect.t=lett=create()ands=refcompressorinletset(knob:string)(x:float)=matchknobwith|"mode"->letmode=List.nthmodes(max0(min(List.lengthmodes-1)(Control.indexx)))in(* the mode's own knee, detector and look-ahead; the knobs kept *)letm=matchmodewithCompressor->compressor|Limiter->limiter|Gate->gateins:={!swithmode;knee=m.knee;detector=m.detector;lookahead=m.lookahead}|"threshold"->s:={!swiththreshold=x}|"ratio"->s:={!swithratio=x}|"attack"->s:={!swithattack=x}|"release"->s:={!swithrelease=x}|"makeup"->s:={!swithmakeup=x}|_->()inList.iter(fun(k:Effect.knob)->setk.namek.initial)knobs;{name="dynamics";knobs;set;process=(funout->processt!sout);meters=(fun()->[("reduction",t.reduction)])}