123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274(* 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 Mod_player.mli *)(*****************************************************************************)(* Types *)(*****************************************************************************)(* a channel: its Paula, the row's cell, the note's period (before the
* vibrato and the arpeggio bend it), the volume, and the effects'
* memories (a 300 or a 400 goes on with the last speeds) *)typechannel={paula:Paula.t;mutablecell:Mod.cell;mutableinstrument:int;(* the last one given, 1-based; 0: none yet *)mutablefinetune:int;mutableperiod:int;mutablevolume:int;mutabletarget:int;mutableslide_speed:int;mutablevibrato_speed:int;mutablevibrato_depth:int;mutablevibrato_pos:int;}typet={mutablesong:Mod.song;mutablesamples:Signal.tarray;(* the instruments' samples, as numbers *)channels:channelarray;loop:bool;mutablereading:Paula.reading;mutableseparation:float;mutableposition:int;mutablerow:int;mutabletick:int;mutablespeed:int;mutabletempo:int;mutableuntil_tick:float;(* samples left before the next tick *)(* a Bxx or a Dxy, done when the row ends: the position, the row *)mutablenext:(int*int)option;mutablefinished:bool;}lettick_samples(tempo:int):float=float_of_intSignal.rate*.2.5/.float_of_inttempo(* the instruments' samples as numbers, read once *)letsamples_of(song:Mod.song):Signal.tarray=Array.map(fun(i:Mod.instrument)->Array.init(String.lengthi.data)(Mod.samplei))song.instrumentsletcreate?(loop=true)(song:Mod.song):t=letchannel()={paula=Paula.create();cell=Mod.empty_cell;instrument=0;finetune=0;period=0;volume=0;target=0;slide_speed=0;vibrato_speed=0;vibrato_depth=0;vibrato_pos=0;}in{song;samples=samples_ofsong;channels=Array.init(Mod.channelssong)(fun_->channel());loop;reading=Hold;separation=1.;position=0;row=0;tick=0;speed=6;tempo=125;until_tick=0.;next=None;finished=Array.lengthsong.positions=0;}letset_reading(p:t)(r:Paula.reading):unit=p.reading<-rletset_separation(p:t)(s:float):unit=p.separation<-Float.max0.(Float.min1.s)letposition(p:t):int*int=(p.position,p.row)letsong(p:t):Mod.song=p.songletseek(p:t)~(position:int)~(row:int):unit=p.position<-max0(min(Array.lengthp.song.positions-1)position);p.row<-max0(min63row);p.tick<-0;p.until_tick<-0.;p.next<-None;p.finished<-Array.lengthp.song.positions=0letset_song(p:t)(song:Mod.song):unit=ifsong.instruments!=p.song.instrumentsthenp.samples<-samples_ofsong;p.song<-song;(* a position gone (the order list shortened): back to its last *)ifp.position>=Array.lengthsong.positionsthenp.position<-max0(Array.lengthsong.positions-1)letfinished(p:t):bool=p.finishedletchannel_period(p:t)(c:int):int=p.channels.(c).periodletchannel_volume(p:t)(c:int):int=p.channels.(c).volumeletspeed(p:t):int=p.speedlettempo(p:t):int=p.tempo(*****************************************************************************)(* Periods *)(*****************************************************************************)(* the period a finetune (eighths of a semitone) and [semitones] up make
* of [period] *)letbend(period:int)~(finetune:int)~(semitones:int):int=int_of_float(Float.round(float_of_intperiod/.Float.pow2.((float_of_intsemitones+.(float_of_intfinetune/.8.))/.12.)))letclamp_period(p:int):int=max113(min856p)(* ProTracker's vibrato: a quarter sine, 0 to 255 in 16 steps *)letvibrato_table=[|0;24;49;74;97;120;141;161;180;197;212;224;235;244;250;253;255;253;250;244;235;224;212;197;180;161;141;120;97;74;49;24|](*****************************************************************************)(* A row's first tick: the cells *)(*****************************************************************************)letvolume_slide(c:channel)(param:int):unit=letup=paramlsr4anddown=paramland0x0Finc.volume<-max0(min64(ifup>0thenc.volume+upelsec.volume-down))letplay_cell(p:t)(c:channel)(cell:Mod.cell):unit=c.cell<-cell;ifcell.instrument>0&&cell.instrument<=Array.lengthp.song.instrumentsthen(leti=p.song.instruments.(cell.instrument-1)inc.instrument<-cell.instrument;c.volume<-i.volume;c.finetune<-i.finetune);(* a new note restarts the channel's instrument, unless it's where a
* slide goes *)ifcell.period>0then(letperiod=bendcell.period~finetune:c.finetune~semitones:0inifcell.fx=3||cell.fx=5thenc.target<-periodelse(c.period<-period;c.vibrato_pos<-0;ifc.instrument>0then(letk=c.instrument-1inleti=p.song.instruments.(k)inletoffset=ifcell.fx=9thencell.param*256else0inifArray.lengthp.samples.(k)>0thenPaula.triggerc.paulap.samples.(k)~loop_start:i.loop_start~loop_length:i.loop_length~offset)));(* the effects of the first tick *)letx=cell.paramlsr4andy=cell.paramland0x0Finmatchcell.fxwith|3->ifcell.param>0thenc.slide_speed<-cell.param|4->ifx>0thenc.vibrato_speed<-x;ify>0thenc.vibrato_depth<-y|0xB->p.next<-Some(cell.param,0)|0xC->c.volume<-min64cell.param|0xD->p.next<-Some((matchp.nextwithSome(pos,_)->pos|None->p.position+1),min63((x*10)+y))|0xE->(matchxwith|1->c.period<-clamp_period(c.period-y)|2->c.period<-clamp_period(c.period+y)|0xA->c.volume<-min64(c.volume+y)|0xB->c.volume<-max0(c.volume-y)|0xC->ify=0thenc.volume<-0|_->())|0xF->ifcell.param=0then()elseifcell.param<32thenp.speed<-cell.paramelsep.tempo<-cell.param|_->()(*****************************************************************************)(* The other ticks: the effects going on *)(*****************************************************************************)letslide_to(c:channel):unit=ifc.target>0thenifc.period<c.targetthenc.period<-minc.target(c.period+c.slide_speed)elsec.period<-maxc.target(c.period-c.slide_speed)letvibrato_delta(c:channel):int=letd=vibrato_table.(c.vibrato_posland31)*c.vibrato_depth/128inifc.vibrato_posland32<>0then-delsed(* the period Paula plays this tick: the note's, bent by the arpeggio
* or the vibrato *)letheard_period(p:t)(c:channel):int=letcell=c.cellinmatchcell.fxwith|0whencell.param>0->lets=matchp.tickmod3with0->0|1->cell.paramlsr4|_->cell.paramland0x0Finbendc.period~finetune:0~semitones:s|4|6->c.period+vibrato_deltac|_->c.periodleteffect_tick(p:t)(c:channel):unit=letcell=c.cellinletx=cell.paramlsr4andy=cell.paramland0x0Finmatchcell.fxwith|1->c.period<-clamp_period(c.period-cell.param)|2->c.period<-clamp_period(c.period+cell.param)|3->slide_toc|4->c.vibrato_pos<-(c.vibrato_pos+c.vibrato_speed)land63|5->slide_toc;volume_slideccell.param|6->c.vibrato_pos<-(c.vibrato_pos+c.vibrato_speed)land63;volume_slideccell.param|0xA->volume_slideccell.param|0xEwhenx=0xC&&p.tick=y->c.volume<-0|_->()(*****************************************************************************)(* Ticks, rows, positions *)(*****************************************************************************)lettick(p:t):unit=ifnotp.finishedthen(ifp.tick=0then(letpattern=p.song.patterns.(p.song.positions.(p.position))inArray.iteri(funkc->play_cellpcpattern.(p.row).(k))p.channels)elseArray.iter(effect_tickp)p.channels;Array.iter(func->Paula.set_periodc.paula(clamp_period(heard_periodpc));Paula.set_volumec.paulac.volume)p.channels;p.tick<-p.tick+1;(* the row over: the next, or where a jump or a break says *)ifp.tick>=p.speedthen(p.tick<-0;letposition,row=matchp.nextwithSome(pos,r)->(pos,r)|None->ifp.row=63then(p.position+1,0)else(p.position,p.row+1)inp.next<-None;ifposition>=Array.lengthp.song.positionsthenifp.loopthen(p.position<-0;p.row<-row)else(p.finished<-true;Array.iter(func->Paula.stopc.paula)p.channels)else(p.position<-position;p.row<-row)))letfill(p:t)(out:Signal.stereo):unit=letn=Array.lengthout.leftin(* channels 1 and 4 left, 2 and 3 right: the Amiga's *)letnear=(1.+.p.separation)/.2.andfar=(1.-.p.separation)/.2.infori=0ton-1doifp.until_tick<=0.then(tickp;p.until_tick<-p.until_tick+.tick_samplesp.tempo);p.until_tick<-p.until_tick-.1.;letl=ref0.andr=ref0.inArray.iteri(funkc->letx=Paula.nextp.readingc.paulainletleft=kmod4=0||kmod4=3inl:=!l+.(x*.ifleftthennearelsefar);r:=!r+.(x*.ifleftthenfarelsenear))p.channels;(* two channels a side at full volume: 2, halved *)out.left.(i)<-0.5*.!l;out.right.(i)<-0.5*.!rdone