123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237(* 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 Sfx.mli *)typewave=Square|Sawtooth|Triangle|Sine|Noisetypet={wave:wave;frequency:float;slide:float;attack:float;sustain:float;decay:float;vibrato_rate:float;vibrato_depth:float;jump:float;jump_at:float;low_pass:float;low_pass_to:float;resonance:float;high_pass:float;echo:float;reverb:float;volume:float;}letdefault:t={wave=Square;frequency=440.;slide=440.;attack=0.;sustain=0.1;decay=0.1;vibrato_rate=0.;vibrato_depth=0.;jump=0.;jump_at=0.;low_pass=0.;low_pass_to=0.;resonance=0.707;high_pass=0.;echo=0.;reverb=0.;volume=0.5;}(* the echoes, each 0.4 of the last *)letfeedback=0.4letto_sound(s:t):Synth.t=letsource:Synth.source=matchs.wavewith|Square->WaveSquare|Sawtooth->WaveSawtooth|Triangle->WaveTriangle|Sine->WaveSine|Noise->Noiseinletattack=Float.max0.005s.attackinletvoice:Synth.voice={source;frequency=s.frequency;slide=(ifs.slide=s.frequencythenNoneelseSomes.slide);seconds=attack+.s.sustain+.s.decay;volume=s.volume;fade=false;effects=(ifs.vibrato_depth>0.then[Pitch_effect.Vibrato{rate=s.vibrato_rate;depth=s.vibrato_depth}]else[])@ifs.jump<>0.then[Pitch_effect.Jump{semitones=s.jump;at=s.jump_at}]else[];envelope=Some{attack;decay=0.;sustain=1.;release=s.decay};}inletsound=Synth.Voicevoiceinletsound=ifs.low_pass>0.thenletcutoff_to=ifs.low_pass_to>0.thens.low_pass_toelses.low_passinSynth.Filtered({kind=Low_pass;cutoff=s.low_pass;cutoff_to;q=s.resonance},sound)elsesoundinletsound=ifs.high_pass>0.thenSynth.Filtered({kind=High_pass;cutoff=s.high_pass;cutoff_to=s.high_pass;q=0.707},sound)elsesoundinletsound=ifs.echo>0.thenSynth.Echo({delay=s.echo;feedback},sound)elsesoundinifs.reverb>0.thenSynth.Reverb(s.reverb,sound)elsesoundletduration(s:t):float=Synth.duration(to_sounds)(*****************************************************************************)(* The presets *)(*****************************************************************************)letblip={defaultwithfrequency=880.;slide=880.;sustain=0.03;decay=0.04}(* C6, then G6: a fifth, 7 semitones *)letcoin={defaultwithfrequency=1047.;slide=1047.;sustain=0.08;decay=0.22;jump=7.;jump_at=0.07;volume=0.4}letjump={defaultwithfrequency=300.;slide=650.;sustain=0.04;decay=0.14;volume=0.4}letlaser={defaultwithwave=Sawtooth;frequency=1200.;slide=200.;sustain=0.02;decay=0.18;low_pass=8000.;low_pass_to=1000.}lethit={defaultwithwave=Noise;frequency=3000.;slide=1500.;sustain=0.01;decay=0.09;low_pass=5000.}letexplosion={defaultwithwave=Noise;frequency=1500.;slide=150.;sustain=0.1;decay=0.6;low_pass=4000.;low_pass_to=150.;volume=0.75;}letstep={defaultwithwave=Triangle;frequency=150.;slide=90.;sustain=0.;decay=0.05;volume=0.3}letpowerup={defaultwithfrequency=330.;slide=990.;sustain=0.25;decay=0.2;vibrato_rate=14.;vibrato_depth=0.5;volume=0.35}letpresets=[("blip",blip);("coin",coin);("jump",jump);("laser",laser);("hit",hit);("explosion",explosion);("step",step);("powerup",powerup);](*****************************************************************************)(* Mutate *)(*****************************************************************************)letvary~(seed:int)(s:t):t=ifseed=0thenselseletst=Random.State.make[|seed|]in(* times 0.7 to 1.4, evenly in ratio *)letscalex=x*.(0.7*.(2.**Random.State.floatst1.))in(* the same number of semitones for both ends of a slide *)letpitch=2.**((Random.State.floatst10.-.5.)/.12.)in{swithfrequency=s.frequency*.pitch;slide=s.slide*.pitch;attack=scales.attack;sustain=scales.sustain;decay=scales.decay;vibrato_rate=scales.vibrato_rate;jump_at=scales.jump_at;low_pass=scales.low_pass;low_pass_to=scales.low_pass_to;high_pass=scales.high_pass;}(*****************************************************************************)(* sfxr's buttons *)(*****************************************************************************)letrandom(category:string)~(seed:int):t=letst=Random.State.make[|seed;Hashtbl.hashcategory|]in(* in [lo, hi); evenly in ratio for [between_log], as the ear hears
* pitches and times *)letbetweenlohi=lo+.Random.State.floatst(hi-.lo)inletbetween_loglohi=lo*.((hi/.lo)**Random.State.floatst1.)inletchancep=Random.State.floatst1.<pinletone_ofl=List.nthl(Random.State.intst(List.lengthl))inmatchcategorywith|"coin"->letf=between_log400.1600.in{defaultwithwave=one_of[Square;Sawtooth];frequency=f;slide=f;sustain=between0.020.1;decay=between0.10.4;(* up an interval: a fourth, a fifth, an octave *)jump=one_of[5.;7.;12.];jump_at=between0.030.12;volume=0.4}|"laser"->letf=between_log500.3000.in{defaultwithwave=one_of[Square;Sawtooth;Sine];frequency=f;slide=f*.between0.10.5;sustain=between0.0.1;decay=between0.050.3;low_pass=(ifchance0.5thenbetween_log2000.10000.else0.);low_pass_to=0.}|"explosion"->letf=between_log300.3000.inletcutoff=between_log1500.6000.in{defaultwithwave=Noise;frequency=f;slide=f*.between0.10.5;sustain=between0.050.3;decay=between0.31.;low_pass=cutoff;low_pass_to=between_log100.400.;volume=0.7}|"powerup"->letf=between_log200.700.inletvibrato=chance0.5in{defaultwithwave=one_of[Square;Sawtooth];frequency=f;slide=f*.between1.53.;sustain=between0.10.4;decay=between0.10.4;vibrato_rate=(ifvibratothenbetween8.20.else0.);vibrato_depth=(ifvibratothenbetween0.31.else0.);volume=0.35}|"hit"->letf=between_log800.4000.in{defaultwithwave=(ifchance0.5thenNoiseelseSquare);frequency=f;slide=f*.between0.30.7;sustain=0.;decay=between0.050.2;low_pass=between_log2000.6000.}|"jump"->letf=between_log150.500.in{defaultwithfrequency=f;slide=f*.between1.52.5;sustain=between0.020.1;decay=between0.10.3;volume=0.4}|_->letf=between_log400.1500.in{defaultwithwave=one_of[Square;Sine];frequency=f;slide=f;sustain=between0.020.06;decay=between0.020.08}