123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120(* 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 Leslie.mli *)letrate=float_of_intSignal.ratelethorn_slow=0.8lethorn_fast=6.8letdrum_slow=0.7letdrum_fast=5.6lethorn_seconds=0.5letdrum_seconds=1.2letcrossover=800.letsound_speed=343.(* m/s *)(* ours: the horn's mouth's circle, the drum opening's, their levels
* facing and turned away *)lethorn_radius=0.15letdrum_radius=0.1(* the microphones, a quarter turn left and right of the listener *)letmicrophones=[|0.25;-0.25|](* a delay line: what a rotor emitted, [at] the next to write *)typeline={samples:floatarray;mutableat:int}typerotor={line:line;radius:float;mutableangle:float;(* turns *)mutablespeed:float;(* turns a second *)facing:float;(* its level turned towards a microphone, 1 - facing away (0.6 + 0.4 cos) *)way:float;(* 1 or -1: the drum turns the other way *)}typet={horn_rotor:rotor;drum_rotor:rotor;low:Filter.biquad;high:Filter.biquad;low_memory:Filter.memory;high_memory:Filter.memory;}letrotorradiusfacingwayspeed={line={samples=Array.make640.;at=0};radius;angle=0.;speed;facing;way}letcreate():t={horn_rotor=rotorhorn_radius0.41.horn_slow;drum_rotor=rotordrum_radius0.2(-1.)drum_slow;low=Filter.biquadLow_pass~cutoff:crossover~q:0.707;high=Filter.biquadHigh_pass~cutoff:crossover~q:0.707;low_memory=Filter.silence();high_memory=Filter.silence();}lethorn(t:t):float=t.horn_rotor.speedletdrum(t:t):float=t.drum_rotor.speed(* what microphone [m] hears of rotor [r] now: the sound emitted a
* distance's time ago -- the mouth nearer, less long ago -- at the
* mouth's level towards it *)letheard(r:rotor)(m:float):float=leta=2.*.Float.pi*.(r.angle-.m)inletdelay=((r.radius*.(1.-.cosa))/.sound_speed*.rate)+.1.inletn=Array.lengthr.line.samplesinletwhole=Float.to_intdelayinletfrac=delay-.float_of_intwholeinletatk=r.line.samples.((r.line.at-k+(2*n))modn)inletx=((1.-.frac)*.atwhole)+.(frac*.at(whole+1))in(1.-.r.facing+.(r.facing*.cosa))*.x(* one sample: the rotor's speed nearer its target, its angle on *)letturn(r:rotor)(target:float)(seconds:float):unit=r.speed<-r.speed+.((target-.r.speed)*.(1.-.exp(-1./.(seconds*.rate))));r.angle<-r.angle+.(r.way*.r.speed/.rate);r.angle<-r.angle-.Float.of_int(Float.to_intr.angle)letwrite(l:line)(x:float):unit=l.samples.(l.at)<-x;l.at<-(l.at+1)modArray.lengthl.samplesletprocess_mix(t:t)~(fast:bool)~(from_mix:float)~(mix:float)(s:Signal.stereo):unit=letn=Array.lengths.leftinleth=t.horn_rotorandd=t.drum_rotorinfori=0ton-1doletx=(s.left.(i)+.s.right.(i))/.2.inwriteh.line(Filter.stept.hight.high_memoryx);writed.line(Filter.stept.lowt.low_memoryx);turnh(iffastthenhorn_fastelsehorn_slow)horn_seconds;turnd(iffastthendrum_fastelsedrum_slow)drum_seconds;letmix=Effect.rampfrom_mixmixininletmicm=heardhm+.hearddmins.left.(i)<-((1.-.mix)*.x)+.(mix*.micmicrophones.(0));s.right.(i)<-((1.-.mix)*.x)+.(mix*.micmicrophones.(1))doneletprocess(t:t)~(fast:bool)(s:Signal.stereo):unit=process_mixt~fast~from_mix:1.~mix:1.sletknobs:Effect.knoblist=[{name="fast";control=Switch;initial=0.};{name="mix";control=Knob(0.,1.);initial=1.}]letfx():Effect.t=lett=create()andfast=reffalseandmix=ref1.andlast_mix=ref1.inletset(knob:string)(x:float)=matchknobwith"fast"->fast:=Control.onx|"mix"->mix:=x|_->()inletprocesss=process_mixt~fast:!fast~from_mix:!last_mix~mix:!mixs;last_mix:=!mixin{name="leslie";knobs;set;process;meters=(fun()->[("horn",hornt);("drum",drumt)])}