// Module 5: OSC Communication Setup - FIXED FOR PROPER TOUCH MANAGEMENT // Save as "5_osc_communication.scd" (REPLACE THE 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, currentSynth: nil, lastTriggerTime: 0, minTriggerInterval: 0.1 // Minimum time between new synths (100ms) ); ~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 ); // Track active synths to prevent memory leaks ~activeSynths = IdentityDictionary.new; ~synthCounter = 0; // Function to clean up old synths ~cleanupSynths = { ~activeSynths.keysValuesDo({ |key, synth| if(synth.isPlaying.not, { ~activeSynths.removeAt(key); }); }); // If too many synths are active, force cleanup of oldest ones if(~activeSynths.size > 5, { // Reduced from 10 to 5 var oldestKeys = ~activeSynths.keys.asArray.sort.copyRange(0, ~activeSynths.size - 3); oldestKeys.do({ |key| if(~activeSynths[key].notNil, { ~activeSynths[key].set(\gate, 0); ~activeSynths.removeAt(key); }); }); }); }; // Function to create a new synth safely (only when needed) ~createSafeSynth = { |synthType = \rgbSynth, args| var synth, synthId; // Clean up old synths first ~cleanupSynths.value; // Create new synth synthId = ~synthCounter; ~synthCounter = ~synthCounter + 1; // Add default parameters if not provided args = args ++ [ \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 ]; synth = Synth(synthType, args); ~activeSynths[synthId] = synth; // Schedule automatic cleanup SystemClock.sched(~synthParams.ampAttack + ~synthParams.ampRelease + 1, { if(~activeSynths[synthId].notNil, { ~activeSynths[synthId].set(\gate, 0); ~activeSynths.removeAt(synthId); }); nil; // Don't reschedule }); synth; }; // Function to handle touch begin (creates new synth) ~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); // Reduced max amplitude // Only create sounds for envelope-controlling pen types if([\pen, \monoline, \marker].includes(~currentPenType.asSymbol), { // Release previous synth if it exists if(~touchState.currentSynth.notNil, { ~touchState.currentSynth.set(\gate, 0); }); // Create new synth ~touchState.currentSynth = ~createSafeSynth.value(\rgbSynth, [ \out, ~sourceBus ? 0, \freq, freq, \amp, amp ]); ~touchState.isActive = true; ~touchState.lastTriggerTime = SystemClock.seconds; ["Touch BEGIN - Sound triggered:", ~currentPenType, freq, amp].postln; }); }; // Function to handle touch movement (updates existing 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.currentSynth.notNil } and: { [\pen, \monoline, \marker].includes(~currentPenType.asSymbol) }, { // Update the existing synth parameters if(~touchState.currentSynth.isPlaying, { ~touchState.currentSynth.set(\freq, freq, \amp, amp); // Don't log every movement to avoid spam }); }); }; // Function to handle touch end (releases synth) ~handleTouchEnd = { if(~touchState.currentSynth.notNil, { ~touchState.currentSynth.set(\gate, 0); ~touchState.currentSynth = nil; ~touchState.isActive = false; ["Touch END - Sound released"].postln; }); }; // Smart trigger function that decides what to do based on pressure changes ~smartTriggerSound = { var currentTime = SystemClock.seconds; 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 safely ~changeEffectParams = { var x = ~currentPadValues.x; var y = ~currentPadValues.y; var pressure = ~currentPadValues.pressure; // Update synthesis parameters but DON'T log every change to avoid spam 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; }, // 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; }, // 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; }, // Pencil - Effect preset 1 \pencil, { // Apply Preset 1 effects - with safety checks if(~filterSynth.notNil and: { ~filterSynth.isPlaying }, { ~filterSynth.set( \cutoff, x.linexp(-0.5, 0.5, 20, 18000), \res, y.linlin(-0.5, 0.5, 0, 1) ); }); if(~lfoSynth.notNil and: { ~lfoSynth.isPlaying }, { ~lfoSynth.set( \freq, x.linlin(-0.5, 0.5, 0, 15), \intensity, pressure.linlin(1, 8, 0, 1) ); }); if(~reverbSynth.notNil and: { ~reverbSynth.isPlaying }, { ~reverbSynth.set( \room, y.linlin(-0.5, 0.5, 0.1, 0.9) ); }); }, // Crayon - Effect preset 2 \crayon, { // Apply Preset 2 effects - with safety checks if(~lfoSynth.notNil and: { ~lfoSynth.isPlaying }, { ~lfoSynth.set( \freq, x.linlin(-0.5, 0.5, 15, 1), \intensity, x.linlin(-0.5, 0.5, 0, 1) ); }); if(~delaySynth.notNil and: { ~delaySynth.isPlaying }, { ~delaySynth.set( \delaytime, x.linlin(-0.5, 0.5, 0.01, 1.0) ); }); if(~filterSynth.notNil and: { ~filterSynth.isPlaying }, { ~filterSynth.set( \cutoff, y.linexp(-0.5, 0.5, 20, 18000), \res, pressure.linlin(1, 5, 0, 1) ); }); if(~reverbSynth.notNil and: { ~reverbSynth.isPlaying }, { ~reverbSynth.set( \mix, y.linlin(-0.5, 0.5, 0, 1) ); }); }, // Fountain pen - Effect preset 3 (placeholder) \fountainPen, { // Apply Preset 3 effects (TBD in documentation) }, // Water color - Effect preset 4 (placeholder) \waterColor, { // Apply Preset 4 effects (TBD in documentation) } ); }; // ----- OSC Pad Values ----- // OSC responder for x coordinate OSCdef(\xOSC, { |msg, time, addr, port| var x = msg[1].asFloat; // Update current pad value and change effects ~currentPadValues.x = x; ~changeEffectParams.value; // Handle touch movement if active if(~touchState.isActive, { ~handleTouchMove.value; }); }, '/aspectX'); // OSC responder for y coordinate OSCdef(\yOSC, { |msg, time, addr, port| var y = msg[1].asFloat; // Update current pad value and change effects ~currentPadValues.y = y; ~changeEffectParams.value; // Handle touch movement if active if(~touchState.isActive, { ~handleTouchMove.value; }); }, '/aspectY'); // OSC responder for pressure coordinate - THIS IS THE KEY ONE OSCdef(\pressureOSC, { |msg, time, addr, port| var pressure = msg[1].asFloat; // Update current pad value and change effects ~currentPadValues.pressure = pressure; ~changeEffectParams.value; // Use smart trigger logic instead of always creating new synths ~smartTriggerSound.value; }, '/pressure'); // ----- OSC Pen Types ----- (unchanged) 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, { ~initializePreset1.value; }); ["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, { ~initializePreset2.value; }); ["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, { ~initializePreset3.value; }); ["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, { ~initializePreset4.value; }); ["Current pen type:", ~currentPenType].postln; } }, '/waterColor'); // ----- OSC RGB Colors ----- (unchanged) OSCdef(\redOSC, { |msg, time, addr, port| var component = msg[1].asFloat; ~currentColor.r = component; ~synthParams.redAmt = component; ["Color changed:", ~currentColor].postln; }, '/r'); OSCdef(\greenOSC, { |msg, time, addr, port| var component = msg[1].asFloat; ~currentColor.g = component; ~synthParams.greenAmt = component; ["Color changed:", ~currentColor].postln; }, '/g'); OSCdef(\blueOSC, { |msg, time, addr, port| var component = msg[1].asFloat; ~currentColor.b = component; ~synthParams.blueAmt = component; ["Color changed:", ~currentColor].postln; }, '/b'); // Cleanup function ~cleanupOSCSystem = { // End any active touch ~handleTouchEnd.value; // Clean up all synths ~activeSynths.keysValuesDo({ |key, synth| synth.set(\gate, 0); }); ~activeSynths.clear; // Reset touch state ~touchState.isActive = false; ~touchState.currentSynth = nil; "OSC system cleaned up".postln; }; // Register cleanup with CmdPeriod CmdPeriod.add({ ~cleanupOSCSystem.value; }); // Start the OSC server on port 57120 (default SuperCollider port) thisProcess.openUDPPort(57120); "OSC server ready on port 57120 - FIXED FOR TOUCH MANAGEMENT".postln; "Now only creates synths on touch BEGIN, updates on MOVE, releases on END".postln; "Ready to receive data from iDraw OSC app".postln; "To cleanup system, run: ~cleanupOSCSystem.value".postln; )