// Evaluate the block below to start the mapping MIDI -> OSC.
var on, off, cc;
var osc;


// Send OSC messages to Hydra
//var hydra = NetAddr.new("127.0.0.1", 3333);
//OSCFunc({ |msg, time, tidalAddr|
//	var latency = time - Main.elapsedTime;
//		msg = msg ++ ["time", time, "latency", latency];
//			msg.postln;
//				hydra.sendBundle(latency, msg)
//}, '/play2').fix;


osc = NetAddr.new("127.0.0.1", 6010);

MIDIClient.init;
MIDIIn.connectAll;

// Initial: sends velocity on note's channel
// Disabled in favor of launchpad control.
// on = MIDIFunc.noteOn({ |val, num, chan, src|
// 	osc.sendMsg("/ctrl", num.asString, val/127);
// });
// on = MIDIFunc.noteOn({ |val, num, chan, src|
// 	// it will be passed the arguments val, num, chan, and src, corresponding to the message value (e.g. velocity, control value, etc.)
// 	osc.sendMsg("/ctrl", "note", (num - 60));
// });
// 
// off = MIDIFunc.noteOff({ |val, num, chan, src|
// 	osc.sendMsg("/ctrl", "note", 0);
// });
// 
// cc = MIDIFunc.cc({ |val, num, chan, src|
// 	osc.sendMsg("/ctrl", num.asString, val/127);
// });
// 

// Simple MIDI->OSC bridge - passes CC numbers directly as control names
on = MIDIFunc.noteOn({ |val, num, chan, src|
	osc.sendMsg("/ctrl", num.asString, val/127);
	~lcxlChordCheck.value(num, val);
});

off = MIDIFunc.noteOff({ |val, num, chan, src|
	osc.sendMsg("/ctrl", num.asString, 0);
	~lcxlChordCheck.value(num, 0);
});

cc = MIDIFunc.cc({ |val, num, chan, src|
	osc.sendMsg("/ctrl", num.asString, val/127);
});

if (~stopMidiToOsc != nil, {
	~stopMidiToOsc.value;
});

~stopMidiToOsc = {
	on.free;
	off.free;
	cc.free;
};

// --- Panic chord: hold push-buttons 73+74+91+92 together to flip a
// persistent "^93" toggle in Tidal (rest of the mute/gF wiring stays in
// BootTidal.hs — this just detects the chord + drives the LED, since only
// this layer sees raw button state to do an edge-triggered flip-flop).
// LCXL LED write validated via plain Note-On (see reference_lcxl_led_stall
// memory: `aseqsend -p 20:0 90 29 3C ...` lights a button LED). Wrapped in
// try{} so a missing/renamed device never blocks the boot.
// --- LCXL LED driver (SysEx). Validated on hardware 2026-07-27.
//
// WHY SYSEX AND NOT NOTE-ON: the Launchpad protocol only lights a control if
// the CURRENT template maps that exact note/CC on that exact MIDI channel. Our
// buttons are CC-mapped on User 1, so Note-On writes were silently dropped —
// the knobs lit, the buttons never did, and nothing reported an error. The
// SysEx "Set LEDs" message updates any control regardless of its note/CC value
// or channel, so it cannot be broken by how the template happens to be laid
// out. (Programmer's Reference, "Launch Control XL System Exclusive Protocol".)
//
//   F0 00 20 29 02 11 78 <template> <index> <value> [<index> <value> ...] F7
//
// Template 00h-07h = User 1-8. We are always on User 1 = 0.
~lcxlTemplate = 0;

// Control index map (contiguous, unlike the scattered note numbers):
//   00-07 top knob row | 08-0F mid knob row | 10-17 bottom knob row
//   18-1F top button row | 20-27 bottom button row
//   28-2B Device/Mute/Solo/RecordArm | 2C-2F Up/Down/Left/Right
~lcxlIdx = (
	knobA: (0..7), knobB: (8..15), knobC: (16..23),
	btnTop: (24..31), btnBot: (32..39),
	side: (40..43), arrows: (44..47)
);

// Colour = (16 * green) + red + flags, green/red each 0-3.
// flags 12 = normal, 8 = flashing. Brightness reads poorly on this hardware —
// prefer FLASH over dim/full when a state must be unmissable.
~lcxlCol = (
	off: 12,
	redLo: 13, red: 15,
	amberLo: 29, amber: 63,
	yellow: 62,
	greenLo: 28, green: 60,
	redFlash: 11, amberFlash: 59, yellowFlash: 58, greenFlash: 56
);

// Resolve the MIDIOut LAZILY, every call. Caching it at boot is exactly how
// this broke: a replug moves the ALSA port (20:0 -> 24:0) and the cached
// MIDIOut points at a device that no longer exists, so LEDs die silently while
// input still works. Cheap enough to re-detect per write.
~lcxlFindOut = {
	try {
		var dest = MIDIClient.destinations.detect({ |d|
			d.device.asString.containsi("Launch Control")
		});
		if (dest.notNil, { MIDIOut.newByName(dest.device, dest.name) }, { nil });
	} { nil };
};
~lcxlOut = ~lcxlFindOut.value;

// Explicit recovery after a replug. NOT automatic: MIDIClient.init re-scans the
// whole MIDI world and can disturb live input bindings, which is not something
// to do mid-set on a guess. Call it by hand when the LEDs have gone dead.
~lcxlReconnect = {
	MIDIClient.init;
	MIDIIn.connectAll;
	~lcxlOut = ~lcxlFindOut.value;
	"lcxl: reconnected -> ".post; ~lcxlOut.postln;
};

// Set one or many LEDs in a single message. pairs = [index, value, index, ...]
~lcxlLedRaw = { |pairs|
	var out = ~lcxlOut ?? { ~lcxlOut = ~lcxlFindOut.value };
	if (out.notNil, {
		try {
			out.sysex(Int8Array.newFrom(
				[0xF0, 0x00, 0x20, 0x29, 0x02, 0x11, 0x78, ~lcxlTemplate]
				++ pairs ++ [0xF7]
			));
		} { ~lcxlOut = nil };  // force re-resolve on the next write
	});
};
~lcxlLedSet = { |index, colour| ~lcxlLedRaw.value([index, colour]) };

// Turn every LED off and reset buffers/duty-cycle to defaults, so we always
// paint from a known state instead of inheriting the last session's mess.
~lcxlReset = { var o = ~lcxlOut ?? { ~lcxlFindOut.value }; if (o.notNil, { o.control(0, 0, 0) }) };

~lcxlChordBtns = [73, 74, 91, 92];
// Same four buttons as SysEx indices on the bottom button row (note 73 -> 20h).
~lcxlChordIdx = [0x20, 0x21, 0x26, 0x27];
~lcxlChordHeld = Set.new;
~lcxlPanicState = 0;
~lcxlLed = { |btnNum, on|
	var i = ~lcxlChordBtns.indexOf(btnNum);
	if (i.notNil, {
		~lcxlLedSet.value(~lcxlChordIdx[i], if(on, ~lcxlCol[\redFlash], ~lcxlCol[\off]));
	});
};
~lcxlChordCheck = { |num, val|
	if (~lcxlChordBtns.includes(num), {
		if (val > 0, { ~lcxlChordHeld.add(num) }, { ~lcxlChordHeld.remove(num) });
		if (~lcxlChordHeld.size == ~lcxlChordBtns.size, {
			~lcxlPanicState = 1 - ~lcxlPanicState;
			osc.sendMsg("/ctrl", "93", ~lcxlPanicState);
			~lcxlChordBtns.do({ |b| ~lcxlLed.value(b, ~lcxlPanicState == 1) });
			"panic-chord: ".post; ~lcxlPanicState.postln;
		});
	});
};

// Evaluate the line below to stop it.
// ~stopMidiToOsc.value;

// MIDI OUT
//~midiOut = MIDIOut.newByName("iO4", "iO4 iO4"); // Alesis iO4
//~midiOut = MIDIOut.newByName("HAPAX", "HAPAX MIDI 1");

// Load/compile all SynthDef files in the synthdefs directory
PathName("~/.local/share/SuperCollider/synthdefs").entries.do({ |file|
    if(file.extension == "scd", { file.fullPath.load });
});


(
// configure the sound server: here you could add hardware specific options
// see http://doc.sccode.org/Classes/ServerOptions.html
// Increased from 1024 * 256
s.options.numBuffers = 1024 * 256; // increase this if you need to load more samples
// Increased from 8192 * 32
s.options.memSize = 8192 * 32; // increase this if you get "alloc failed" messages
s.options.numWireBufs = 128; // increase if "exceeded number of wire buffer" messages
s.options.maxNodes = 1024 * 32; // increase this if you are getting drop outs and the message "too many nodes"
s.options.numOutputBusChannels = 12; // set this to your hardware output channel size, if necessary
s.options.numOutputBusChannels = 28; // MULTICHANNEL + tidal-ears test orbit
s.options.numInputBusChannels = 2; // set this to your hardware output channel size, if necessary
// boot the server and start SuperDirt
s.waitForBoot {
	~dirt = SuperDirt(2, s); // two output channels, increase if you want to pan across more channels
	~dirt.doNotReadYet = true; // Lazy-loading https://club.tidalcycles.org/t/superdirt-lazy-samples-loading/3148
	//~dirt.loadSoundFiles;   // load samples (path containing a wildcard can be passed in)
	// for example: ~dirt.loadSoundFiles("/Users/myUserName/Dirt/samples/*");
	~dirt.loadSoundFiles("/home/pln/.local/share/SuperCollider/downloaded-quarks/Dirt-Samples/*");
	~dirt.loadSoundFiles("/home/pln/Work/Sound/Samples/extra/*");

	// DRUM MACHINES
	~drumMachinesDir = PathName.new("/home/pln/Work/Sound/Samples/tidal-drum-machines/machines");
	~machines = ~drumMachinesDir.folders; //All drum machines
  	//*~machines = ['Linn9000','RolandTR909']; //Selected drum machines


	(
		~machines.do({
			arg machine;
			var folders = machine.folders;
			//postln("Loading machine: "++machine.folderName);
			folders.do({
				arg folder;
				~dirt.loadSoundFiles(folder.fullPath,namingFunction: { |x| x.basename.replace("-","")});
			});
		});
	);

	// s.sync; // optionally: wait for samples to be read
	//~dirt.start(57120, 0 ! 12);   // start listening on port 57120, create two busses each sending audio to channel 0
	~dirt.start(57120, [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26]);   // +orbit 13 (bus 26) = tidal-ears test

	//~dirt.soundLibrary.addMIDI(\midi, ~midiOut); // Connect `midi` synth to iO4

	//~looper = TidalLooper(~dirt); // Start TidalLooper on top :)
	// ~looper.linput = 1;

	// optional, needed for convenient access from sclang:
	(
		~d1 = ~dirt.orbits[0]; ~d2 = ~dirt.orbits[1]; ~d3 = ~dirt.orbits[2];
		~d4 = ~dirt.orbits[3]; ~d5 = ~dirt.orbits[4]; ~d6 = ~dirt.orbits[5];
		~d7 = ~dirt.orbits[6]; ~d8 = ~dirt.orbits[7]; ~d9 = ~dirt.orbits[8];
		~d10 = ~dirt.orbits[9]; ~d11 = ~dirt.orbits[10]; ~d12 = ~dirt.orbits[11];
	);

  // Setup SCLOrK Synths (installed via Quark)
  SCLOrkSynths.load;

	// Set-specific sample PRELOAD — generated by gig-up.sh (tools/setlist_samples.py)
	// from the recent set's tracks, so those samples are warm instead of
	// lazy-loading (crackle + xruns) on first play mid-gig. Absent = pure lazy.
	if(File.exists("/home/pln/Work/Sound/Tidal/preload.scd"), {
		"preload: warming the set's samples…".postln;
		thisProcess.interpreter.executeFile("/home/pln/Work/Sound/Tidal/preload.scd");
	}, {
		"preload: no preload.scd — lazy-loading samples on demand.".postln;
	});

	// =============================================
	// Mutable Instruments Global Effects
	// https://club.tidalcycles.org/t/mutable-instruments-ugens/2730/22
	// =============================================

	// MiVerb - global reverb per orbit
	(1..SuperDirt.maxSampleNumChannels).do { |numChannels|
		SynthDef("global_mi_verb" ++ numChannels, { |dryBus, effectBus, verbwet=0, verbtime=0.7, verbdamp=0.5, verbhp=0.05, verbfreeze=0, verbdiff=0.625, verbgain=0|
			var signal = In.ar(dryBus, ~dirt.numChannels);
			signal = MiVerb.ar(signal, verbtime, verbwet, verbdamp, verbhp, verbfreeze, verbdiff);
			Out.ar(effectBus, signal * verbgain);
		}, [\ir, \ir]).add;
	};

	// MiClouds - global granular processor per orbit
	(1..SuperDirt.maxSampleNumChannels).do { |numChannels|
		SynthDef("global_mi_clouds" ++ numChannels, { |dryBus, effectBus, cloudspitch=0, cloudspos=0.5, cloudssize=0.25, cloudsdens=0.4, cloudstex=0.5, cloudswet=0, cloudsgain=0, cloudsspread=0.5, cloudsrvb=0, cloudsfb=0, cloudsfreeze=0, cloudsmode=0, cloudslofi=0|
			var signal = In.ar(dryBus, ~dirt.numChannels);
			signal = MiClouds.ar(signal, cloudspitch, cloudspos, cloudssize, cloudsdens, cloudstex, cloudswet, 1, cloudsspread, cloudsrvb, cloudsfb, cloudsfreeze, cloudsmode, cloudslofi);
			Out.ar(effectBus, signal * cloudsgain);
		}, [\ir, \ir]).add;
	};

	// MiRipples - global resonant filter per orbit
	(1..SuperDirt.maxSampleNumChannels).do { |numChannels|
		SynthDef("global_mi_ripples" ++ numChannels, { |dryBus, effectBus, ripplescf=0.5, ripplesreson=0, ripplesdrive=1, ripplesgain=0|
			var signal = In.ar(dryBus, ~dirt.numChannels);
			signal = MiRipples.ar(signal, ripplescf, ripplesreson, ripplesdrive);
			Out.ar(effectBus, signal * ripplesgain);
		}, [\ir, \ir]).add;
	};

	// =============================================
	// spectral-freeze override: PV_Freeze -> PV_MagFreeze
	// =============================================
	// 2026-08-15: scsynth SIGSEGV'd four times in four minutes mid-session,
	// every coredump with the same frame #1 -- JoshPVUGens.so, on the JACK/
	// PipeWire audio callback thread. The only route into that library from
	// this rig is SuperDirt's `spectral-freeze` module (default-effects-extra
	// .scd), i.e. Tidal's `# freeze`, which 8 live tracks carry.
	//
	// The server was already starving when it died: `LocalBuf_allocBuffer:
	// alloc failed` and 18x `Buffer UGen: no buffer data` in the log, plus
	// `late` values of 6-13 SECONDS. SuperDirt builds one of these synths PER
	// EVENT, each allocating FFT(LocalBuf(2048)) per channel, so a dense
	// pattern under `# freeze` drains the RT pool -- and Josh's PV_Freeze does
	// not survive being handed the resulting null buffer.
	//
	// PV_MagFreeze is the core-SC sibling (JoshUGens' own help file compares
	// the two) with identical trigger semantics: freeze > 0 freezes magnitudes.
	// It is slightly more static -- PV_Freeze also advances phase between
	// frames -- but it is in libscsynth, null-checks its buffer, and cannot
	// take the server down. Re-`.add` replaces SuperDirt's definition by name.
	SynthDef("spectral-freeze" ++ ~dirt.numChannels, { |out, freeze|
		var signal, chain;
		signal = In.ar(out, ~dirt.numChannels);
		chain = signal.asArray.collect { |x| FFT(LocalBuf(2048), x) };
		signal = IFFT(PV_MagFreeze(chain, freeze));
		ReplaceOut.ar(out, signal)
	}, [\ir]).add;
	"[boot] spectral-freeze overridden: PV_MagFreeze (JoshPVUGens crashed scsynth 2026-08-15)".postln;

	// Register global effects on all orbits
	~dirt.orbits.do { |x|
		var verb = GlobalDirtEffect(\global_mi_verb, [\verbwet, \verbtime, \verbdamp, \verbhp, \verbfreeze, \verbdiff, \verbgain]);
		var clouds = GlobalDirtEffect(\global_mi_clouds, [\cloudspitch, \cloudspos, \cloudssize, \cloudsdens, \cloudstex, \cloudswet, \cloudsgain, \cloudsspread, \cloudsrvb, \cloudsfb, \cloudsfreeze, \cloudsmode, \cloudslofi]);
		var ripples = GlobalDirtEffect(\global_mi_ripples, [\ripplescf, \ripplesreson, \ripplesdrive, \ripplesgain]);
		x.globalEffects = x.globalEffects.addFirst(verb).addFirst(clouds).addFirst(ripples);
		x.initNodeTree;
	};

	// Sample watcher responder: lets tools/sample-watcher.py register a newly
	// dropped pack into THIS running rig, no reboot. Non-essential — if the
	// file is missing the rig boots exactly as before.
	if(File.exists("/home/pln/Work/Sound/Tidal/sample_watch.scd")) {
		"/home/pln/Work/Sound/Tidal/sample_watch.scd".load;
	} {
		"pv: sample_watch.scd not found — watcher responder not armed.".postln;
	};
};

s.volume = 2; // +6dB — SuperDirt defaults are conservative
StageLimiter.activate; // BatLib safety limiter on master out
s.latency = 0.3; // increase this if you get "late" messages
);
