// Module 5: OSC Communication Setup with ROBUST Synth Pool // Save as "5_osc_communication.scd" (REPLACE PREVIOUS VERSION) ( // Clear any existing OSC definitions OSCdef.freeAll; // Variables to track current pen type and color ~currentPenType = \pen; ~currentColor = (r: 0.0, g: 0.0, b: 1.0); // Default blue ~currentPadValues = (x: 0.0, y: 0.0, pressure: 1.0); //Default pad values // Touch state management ~touchState = ( isActive: false, currentTouchKey: nil ); ~synthParams = ( out: 0, freq: 440, amp: 0.5, ampAttack: 0.01, ampRelease: 1, filterAttack: 0, filterRelease: 0, filterMin: 200, filterMax: 5000, pitchAttack: 0, pitchRelease: 0, pitchRatio: 2, redAmt: 0.5, greenAmt: 0.5, blueAmt: 0.5 ); // ========== ROBUST SYNTH POOL SYSTEM ========== // Synth Pool Configuration ~poolSize = 16; // Number of pre-allocated synths ~synthPool = Array.newClear(~poolSize); // Array instead of dictionary ~activeSynths = IdentityDictionary.new; // Track which synths are in use ~freeIndices = Array.series(~poolSize); // Available synth indices ~synthPoolGroup = nil; ~poolInitialized = false; // Clean up existing pool completely ~cleanupSynthPool = { "Cleaning up synth pool...".postln; // Set flag ~poolInitialized = false; // Return all active synths ~activeSynths.keys.do({ |key| try { ~returnSynthToPool.value(key); } { |error| // Silent cleanup - don't print errors during cleanup }; }); // Free the group (this frees all synths) if(~synthPoolGroup.notNil, { try { ~synthPoolGroup.free; } { |error| // Silent cleanup }; ~synthPoolGroup = nil; }); // Clear data structures ~synthPool = Array.newClear(~poolSize); ~activeSynths.clear; ~freeIndices = Array.series(~poolSize); // Reset touch state ~touchState.isActive = false; ~touchState.currentTouchKey = nil; "Synth pool cleaned up".postln; }; // Initialize the synth pool ~initializeSynthPool = { // Clean up first ~cleanupSynthPool.value; // Wait for server s.sync; // Create a group to hold all pool synths ~synthPoolGroup = Group.new; s.sync; "Creating synth pool with % synths...".format(~poolSize).postln; // Create all synths at once ~poolSize.do({ |i| try { var synth = Synth(\rgbSynth, [ \out, ~sourceBus ? 0, \gate, 0, // Start with gate closed (silent) \amp, 0, \freq, 440 ], ~synthPoolGroup); // Store in array ~synthPool[i] = synth; } { |error| "Error creating synth %: %".format(i, error).postln; }; }); // Wait for all synths to be created s.sync; // Mark as initialized ~poolInitialized = true; "Synth pool initialized with % synths".format(~poolSize).postln; "Free synths: %".format(~freeIndices.size).postln; }; // Get a free synth from the pool ~getSynthFromPool = { |key| var synthIndex, synth = nil; if(~poolInitialized.not, { "Error: Synth pool not initialized!".postln; nil; }, { if(~freeIndices.size > 0, { // Get the first available synth index synthIndex = ~freeIndices.removeAt(0); synth = ~synthPool[synthIndex]; // Check if synth is valid if(synth.notNil and: { synth.isPlaying }, { // Mark it as active ~activeSynths[key] = (synth: synth, index: synthIndex); // Update parameters with current settings try { synth.set( \ampAttack, ~synthParams.ampAttack, \ampRelease, ~synthParams.ampRelease, \filterAttack, ~synthParams.filterAttack, \filterRelease, ~synthParams.filterRelease, \pitchAttack, ~synthParams.pitchAttack, \pitchRelease, ~synthParams.pitchRelease, \redAmt, ~synthParams.redAmt, \greenAmt, ~synthParams.greenAmt, \blueAmt, ~synthParams.blueAmt ); } { |error| "Error setting synth parameters: %".format(error).postln; }; synth; }, { // Synth is invalid, put index back and return nil ~freeIndices.add(synthIndex); "Warning: Synth % is invalid".format(synthIndex).postln; nil; }); }, { "Warning: No free synths in pool!".postln; nil; }); }); }; // Return a synth to the pool ~returnSynthToPool = { |key| var synthData; if(~activeSynths.notNil, { synthData = ~activeSynths[key]; if(synthData.notNil, { var synth = synthData.synth; var index = synthData.index; // Release the synth safely if(synth.notNil, { try { synth.set(\gate, 0, \amp, 0); } { |error| // Silent error handling }; }); // Return to free pool if(~freeIndices.notNil, { ~freeIndices.add(index); }); // Remove from active tracking ~activeSynths.removeAt(key); }); }); }; // Start a synth with specific parameters ~startPoolSynth = { |key, freq, amp, duration=nil| var synth = ~getSynthFromPool.value(key); if(synth.notNil, { // Start the synth try { synth.set( \gate, 1, \freq, freq, \amp, amp ); // If duration is specified, schedule automatic release if(duration.notNil, { SystemClock.sched(duration, { ~returnSynthToPool.value(key); nil; }); }); synth; } { |error| "Error starting synth: %".format(error).postln; ~returnSynthToPool.value(key); nil; }; }, { nil; }); }; // Update an active synth ~updatePoolSynth = { |key, freq=nil, amp=nil| var synthData; if(~activeSynths.notNil, { synthData = ~activeSynths[key]; if(synthData.notNil, { var synth = synthData.synth; if(synth.notNil, { try { if(freq.notNil, { synth.set(\freq, freq); }); if(amp.notNil, { synth.set(\amp, amp); }); } { |error| // Silent error handling }; }); }); }); }; // Update all active synths with new envelope/color parameters ~updateAllPoolSynths = { if(~activeSynths.notNil, { ~activeSynths.keysValuesDo({ |key, synthData| var synth = synthData.synth; if(synth.notNil, { try { synth.set( \ampAttack, ~synthParams.ampAttack, \ampRelease, ~synthParams.ampRelease, \filterAttack, ~synthParams.filterAttack, \filterRelease, ~synthParams.filterRelease, \pitchAttack, ~synthParams.pitchAttack, \pitchRelease, ~synthParams.pitchRelease, \redAmt, ~synthParams.redAmt, \greenAmt, ~synthParams.greenAmt, \blueAmt, ~synthParams.blueAmt ); } { |error| // Silent error handling during updates }; }); }); }); }; // Get pool status ~getPoolStatus = { "=== Synth Pool Status ===".postln; "Pool initialized: %".format(~poolInitialized).postln; "Total synths: %".format(~poolSize).postln; if(~activeSynths.notNil, { "Active synths: %".format(~activeSynths.size).postln; }, { "Active synths: 0 (activeSynths is nil)".postln; }); if(~freeIndices.notNil, { "Free synths: %".format(~freeIndices.size).postln; }, { "Free synths: 0 (freeIndices is nil)".postln; }); "========================".postln; }; // ========== TOUCH HANDLING ========== // Function to handle touch begin (gets synth from pool) ~handleTouchBegin = { var x = ~currentPadValues.x; var y = ~currentPadValues.y; var pressure = ~currentPadValues.pressure; var freq = x.linexp(-0.5, 0.5, 100, 2000); var amp = pressure.linlin(1, 8, 0.1, 0.5); // Only create sounds for envelope-controlling pen types if([\pen, \monoline, \marker].includes(~currentPenType.asSymbol), { // End previous touch if it exists if(~touchState.currentTouchKey.notNil, { ~returnSynthToPool.value(~touchState.currentTouchKey); }); // Create unique key for this touch ~touchState.currentTouchKey = "touch_" ++ UniqueID.next; // Get synth from pool and start it ~startPoolSynth.value(~touchState.currentTouchKey, freq, amp); ~touchState.isActive = true; ["Touch BEGIN - Got synth from pool:", ~currentPenType, freq, amp].postln; }); }; // Function to handle touch movement (updates pool synth) ~handleTouchMove = { var x = ~currentPadValues.x; var y = ~currentPadValues.y; var pressure = ~currentPadValues.pressure; var freq = x.linexp(-0.5, 0.5, 100, 2000); var amp = pressure.linlin(1, 8, 0.1, 0.5); // Only update if we have an active touch for envelope-controlling pens if(~touchState.isActive and: { ~touchState.currentTouchKey.notNil } and: { [\pen, \monoline, \marker].includes(~currentPenType.asSymbol) }, { // Update the existing synth parameters ~updatePoolSynth.value(~touchState.currentTouchKey, freq, amp); }); }; // Function to handle touch end (returns synth to pool) ~handleTouchEnd = { if(~touchState.currentTouchKey.notNil, { ~returnSynthToPool.value(~touchState.currentTouchKey); ~touchState.currentTouchKey = nil; ~touchState.isActive = false; ["Touch END - Returned synth to pool"].postln; }); }; // Smart trigger function ~smartTriggerSound = { var pressure = ~currentPadValues.pressure; // Detect touch begin: pressure goes from low to high if(pressure > 2 and: { ~touchState.isActive.not }, { ~handleTouchBegin.value; }); // Detect touch movement: pressure stays high and we have active touch if(pressure > 1 and: { ~touchState.isActive }, { ~handleTouchMove.value; }); // Detect touch end: pressure goes to very low or zero if(pressure <= 1 and: { ~touchState.isActive }, { ~handleTouchEnd.value; }); }; // Function to update effect parameters AND update all active synths ~changeEffectParams = { var x = ~currentPadValues.x; var y = ~currentPadValues.y; var pressure = ~currentPadValues.pressure; var paramsChanged = false; // Update synthesis parameters switch(~currentPenType.asSymbol, // Pen - Controls amplitude envelope \pen, { var ampAttack = y.linexp(-0.5, 0.5, 0.001, 5); var ampRelease = x.linexp(-0.5, 0.5, 0.001, 10); ~synthParams.ampAttack = ampAttack; ~synthParams.ampRelease = ampRelease; paramsChanged = true; }, // Monoline - Controls filter envelope \monoline, { var filterAttack = y.linexp(-0.5, 0.5, 0.001, 5); var filterRelease = x.linexp(-0.5, 0.5, 0.001, 10); ~synthParams.filterAttack = filterAttack; ~synthParams.filterRelease = filterRelease; paramsChanged = true; }, // Marker - Controls pitch envelope \marker, { var pitchAttack = y.linexp(-0.5, 0.5, 0.001, 5); var pitchRelease = x.linexp(-0.5, 0.5, 0.001, 10); ~synthParams.pitchAttack = pitchAttack; ~synthParams.pitchRelease = pitchRelease; paramsChanged = true; }, // Pencil - Effect preset 1 \pencil, { // Apply Preset 1 effects - with safety checks if(~filterSynth.notNil and: { ~filterSynth.isPlaying }, { try { ~filterSynth.set( \cutoff, x.linexp(-0.5, 0.5, 20, 18000), \res, y.linlin(-0.5, 0.5, 0, 1) ); } { |error| // Silent error handling }; }); if(~lfoSynth.notNil and: { ~lfoSynth.isPlaying }, { try { ~lfoSynth.set( \freq, x.linlin(-0.5, 0.5, 0, 15) ); } { |error| // Silent error handling }; }); if(~reverbSynth.notNil and: { ~reverbSynth.isPlaying }, { try { ~reverbSynth.set( \room, y.linlin(-0.5, 0.5, 0.1, 0.9) ); } { |error| // Silent error handling }; }); }, // Crayon - Effect preset 2 \crayon, { // Apply Preset 2 effects - with safety checks if(~lfoSynth.notNil and: { ~lfoSynth.isPlaying }, { try { ~lfoSynth.set( \freq, x.linlin(-0.5, 0.5, 15, 1) ); } { |error| // Silent error handling }; }); if(~delaySynth.notNil and: { ~delaySynth.isPlaying }, { try { ~delaySynth.set( \delaytime, x.linlin(-0.5, 0.5, 0.01, 1.0) ); } { |error| // Silent error handling }; }); if(~filterSynth.notNil and: { ~filterSynth.isPlaying }, { try { ~filterSynth.set( \cutoff, y.linexp(-0.5, 0.5, 20, 18000), \res, pressure.linlin(1, 5, 0, 1) ); } { |error| // Silent error handling }; }); if(~reverbSynth.notNil and: { ~reverbSynth.isPlaying }, { try { ~reverbSynth.set( \mix, y.linlin(-0.5, 0.5, 0, 1) ); } { |error| // Silent error handling }; }); } ); // Update all active synths if envelope parameters changed if(paramsChanged, { ~updateAllPoolSynths.value; }); }; // ========== OSC RESPONDERS ========== // ----- OSC Pad Values ----- OSCdef(\xOSC, { |msg, time, addr, port| var x = msg[1].asFloat; ~currentPadValues.x = x; ~changeEffectParams.value; if(~touchState.isActive, { ~handleTouchMove.value; }); }, '/aspectX'); OSCdef(\yOSC, { |msg, time, addr, port| var y = msg[1].asFloat; ~currentPadValues.y = y; ~changeEffectParams.value; if(~touchState.isActive, { ~handleTouchMove.value; }); }, '/aspectY'); OSCdef(\pressureOSC, { |msg, time, addr, port| var pressure = msg[1].asFloat; ~currentPadValues.pressure = pressure; ~changeEffectParams.value; ~smartTriggerSound.value; }, '/pressure'); // ----- OSC Pen Types ----- OSCdef(\penOSC, { |msg, time, addr, port| var penType = msg[1].asFloat; if (penType == 1.0) { ~currentPenType = \pen; ["Current pen type:", ~currentPenType].postln; } }, '/pen'); OSCdef(\monolineOSC, { |msg, time, addr, port| var penType = msg[1].asFloat; if (penType == 1.0) { ~currentPenType = \monoline; ["Current pen type:", ~currentPenType].postln; } }, '/monoline'); OSCdef(\markerOSC, { |msg, time, addr, port| var penType = msg[1].asFloat; if (penType == 1.0) { ~currentPenType = \marker; ["Current pen type:", ~currentPenType].postln; } }, '/marker'); OSCdef(\pencilOSC, { |msg, time, addr, port| var penType = msg[1].asFloat; if (penType == 1.0) { ~currentPenType = \pencil; if(~initializePreset1.notNil, { try { ~initializePreset1.value; } { |error| }; }); ["Current pen type:", ~currentPenType].postln; } }, '/pencil'); OSCdef(\crayonOSC, { |msg, time, addr, port| var penType = msg[1].asFloat; if (penType == 1.0) { ~currentPenType = \crayon; if(~initializePreset2.notNil, { try { ~initializePreset2.value; } { |error| }; }); ["Current pen type:", ~currentPenType].postln; } }, '/crayon'); OSCdef(\fountainPenOSC, { |msg, time, addr, port| var penType = msg[1].asFloat; if (penType == 1.0) { ~currentPenType = \fountainPen; if(~initializePreset3.notNil, { try { ~initializePreset3.value; } { |error| }; }); ["Current pen type:", ~currentPenType].postln; } }, '/fountainPen'); OSCdef(\waterColorOSC, { |msg, time, addr, port| var penType = msg[1].asFloat; if (penType == 1.0) { ~currentPenType = \waterColor; if(~initializePreset4.notNil, { try { ~initializePreset4.value; } { |error| }; }); ["Current pen type:", ~currentPenType].postln; } }, '/waterColor'); // ----- OSC RGB Colors ----- OSCdef(\redOSC, { |msg, time, addr, port| var component = msg[1].asFloat; ~currentColor.r = component; ~synthParams.redAmt = component; ~updateAllPoolSynths.value; ["Color changed (red):", component].postln; }, '/r'); OSCdef(\greenOSC, { |msg, time, addr, port| var component = msg[1].asFloat; ~currentColor.g = component; ~synthParams.greenAmt = component; ~updateAllPoolSynths.value; ["Color changed (green):", component].postln; }, '/g'); OSCdef(\blueOSC, { |msg, time, addr, port| var component = msg[1].asFloat; ~currentColor.b = component; ~synthParams.blueAmt = component; ~updateAllPoolSynths.value; ["Color changed (blue):", component].postln; }, '/b'); // ========== CLEANUP ========== // Cleanup function ~cleanupOSCSystem = { // End any active touch ~handleTouchEnd.value; // Reset touch state ~touchState.isActive = false; ~touchState.currentTouchKey = nil; "OSC system cleaned up".postln; }; // Register cleanup with CmdPeriod CmdPeriod.add({ ~cleanupOSCSystem.value; ~cleanupSynthPool.value; }); // ========== INITIALIZATION ========== // Start the OSC server on port 57120 thisProcess.openUDPPort(57120); "========================================".postln; "OSC Communication with ROBUST Synth Pool loaded".postln; "========================================".postln; "Available functions:".postln; " ~initializeSynthPool.value - Initialize 16 synths".postln; " ~getPoolStatus.value - Show pool status".postln; " ~cleanupSynthPool.value - Clean up pool".postln; " ~cleanupOSCSystem.value - Clean up OSC".postln; "".postln; "IMPORTANT: Run ~initializeSynthPool.value after effects chain is ready!".postln; "========================================".postln; )