diff --git a/sound_algo/data_only/sections.scd b/sound_algo/data_only/sections.scd new file mode 100644 index 0000000..b9473ff --- /dev/null +++ b/sound_algo/data_only/sections.scd @@ -0,0 +1,140 @@ +// sections.scd — Ableton-scene-style section slots +// 8 snapshot slots: armed clips + seq presets + harmony. Save, quantized recall, +// next/prev chaining. Loaded after touchosc_feedback.scd (boot step 6h). +// Additive, nil-guarded, idempotent on reload. Single top-level block. +// Feedback: /scene/state ... +( + +// -- Env init (nil-guarded; idempotent reload) -- +~lp = ~lp ? (); +~lp[\sceneN] = ~lp[\sceneN] ? 8; +~lp[\scenes] = ~lp[\scenes] ? Array.fill(~lp[\sceneN], nil); +~lp[\sceneCur] = ~lp[\sceneCur] ? -1; + +// 16 clip symbols (same order as ~toscPatterns) ++ the two sequencers +~scenePatterns = ~scenePatterns ? [ + \kick, \hats, \clap, \perc, + \sub, \acid, \arp, \lead, + \stab, \pad, \ride, \rim, + \tom, \reese, \bells, \sweep, + \melseq, \rhythmseq +]; + +// -- ~scenePush : push /scene/state to all TouchOSC clients -- +~scenePush = ~scenePush ? { + var fills = ~lp[\scenes].collect({ |s| s.notNil.binaryValue }); + ~toscSend !? { ~toscSend.valueArray(["/scene/state", ~lp[\sceneCur]] ++ fills) }; +}; + +// -- ~sceneSave : snapshot current engine state into slot i -- +~sceneSave = ~sceneSave ? { |i| + (~lp.notNil and: { i >= 0 and: { i < (~lp[\sceneN] ? 8) } }).if({ + ~lp[\scenes][i] = ( + armed: ~lp[\armed].copy, + melSel: ~lp[\melSel], + rhySel: ~lp[\rhySel], + harmony: (~ccHarmony.notNil).if({ + (root: ~ccHarmony[\root], + scale: ~ccHarmony[\scale], + octave: ~ccHarmony[\octave], + phrase: ~ccHarmony[\phrase]) + }, { nil }) + ); + ~scenePush.(); + }); +}; + +// -- ~sceneLaunch : diff target armed-set vs current, fire ~lpArm per clip -- +// ~lpArm is already quantized: whole section transitions at next bar boundary. +~sceneLaunch = ~sceneLaunch ? { |i| + var sc = ~lp[\scenes][i]; + sc.notNil.if({ + var target = sc[\armed]; + ~scenePatterns.do { |nm| + var want = target.includes(nm); + var have = ~lp[\armed].includes(nm); + (want and: { have.not }).if({ ~lpArm.(nm.asString, true) }); + (want.not and: { have }).if({ ~lpArm.(nm.asString, false) }); + }; + ~lp[\melSel] = sc[\melSel] ? ~lp[\melSel]; + ~lp[\rhySel] = sc[\rhySel] ? ~lp[\rhySel]; + (sc[\harmony].notNil and: { ~ccHarmony.notNil }).if({ + [\root, \scale, \octave, \phrase].do { |k| + sc[\harmony][k] !? { |v| ~ccHarmony[k] = v }; + }; + }); + ~lp[\sceneCur] = i; + ~scenePush.(); + }); +}; + +// -- ~sceneFilledIndices : list of non-nil slot indices -- +~sceneFilledIndices = ~sceneFilledIndices ? { + var idxs = []; + ~lp[\scenes].doWithIndex { |s, i| s.notNil.if({ idxs = idxs.add(i) }) }; + idxs +}; + +// -- ~sceneNext : advance to next filled slot (wrap-around) -- +~sceneNext = ~sceneNext ? { + var cur = ~lp[\sceneCur] ? -1; + var n = ~lp[\sceneN] ? 8; + var found = nil; + n.do { |offset| + var idx = (cur + 1 + offset) % n; + found.isNil.if({ + ~lp[\scenes][idx].notNil.if({ found = idx }); + }); + }; + found.notNil.if({ ~sceneLaunch.(found) }); +}; + +// -- ~scenePrev : retreat to previous filled slot (wrap-around) -- +~scenePrev = ~scenePrev ? { + var cur = ~lp[\sceneCur] ? -1; + var n = ~lp[\sceneN] ? 8; + var found = nil; + n.do { |offset| + var idx = ((cur - 1 - offset) % n + n) % n; + found.isNil.if({ + ~lp[\scenes][idx].notNil.if({ found = idx }); + }); + }; + found.notNil.if({ ~sceneLaunch.(found) }); +}; + +// -- ~sceneClear : clear slot i; reset ~sceneCur if it was the active scene -- +~sceneClear = ~sceneClear ? { |i| + ~lp[\scenes][i] = nil; + (~lp[\sceneCur] == i).if({ ~lp[\sceneCur] = -1 }); + ~scenePush.(); +}; + +// -- OSCdefs (unique keys with \scene_ prefix; call ~toscTouch for feedback) -- +OSCdef(\scene_save, { |msg, time, addr| + ~toscTouch !? { ~toscTouch.(addr) }; + ~sceneSave.(msg[1].asInteger); +}, '/scene/save'); + +OSCdef(\scene_launch, { |msg, time, addr| + ~toscTouch !? { ~toscTouch.(addr) }; + ~sceneLaunch.(msg[1].asInteger); +}, '/scene/launch'); + +OSCdef(\scene_next, { |msg, time, addr| + ~toscTouch !? { ~toscTouch.(addr) }; + ~sceneNext.(); +}, '/scene/next'); + +OSCdef(\scene_prev, { |msg, time, addr| + ~toscTouch !? { ~toscTouch.(addr) }; + ~scenePrev.(); +}, '/scene/prev'); + +OSCdef(\scene_clear, { |msg, time, addr| + ~toscTouch !? { ~toscTouch.(addr) }; + ~sceneClear.(msg[1].asInteger); +}, '/scene/clear'); + +"[sections] ready".postln; +) diff --git a/sound_algo/data_only/test/test_sections.scd b/sound_algo/data_only/test/test_sections.scd new file mode 100644 index 0000000..ca3f2fa --- /dev/null +++ b/sound_algo/data_only/test/test_sections.scd @@ -0,0 +1,59 @@ +// Headless test for sections.scd +// Run: /Applications/SuperCollider.app/Contents/MacOS/sclang \ +// sound_algo/data_only/test/test_sections.scd +// Expected output: TEST PASS (with no ERROR lines from sections.scd) +// +// Server is NOT booted. ~lpArm, ~toscSend, ~ccHarmony are stubbed. +( +var featureFile = PathName(thisProcess.nowExecutingPath).pathOnly + ++ "../sections.scd"; +var pass = true; + +// Minimal dummy environment matching shapes expected by sections.scd +~lp = ( + armed: Set[\kick], + melSel: 0, + rhySel: 0, + clock: TempoClock.default, + melodies: [[0]], + rhythms: [(name: "x", k: "", s: "", h: "")] +); + +// Stubs — no server, no real transport +~lpArm = { |n, o| }; +~ccHarmony = (root: 40, scale: \minor, octave: 1, phrase: 0); +~toscSend = { |...a| }; + +// Load the feature file (synchronous; stubs already in place) +featureFile.load; + +// Assert public API functions were wired +pass = pass and: { ~sceneSave.notNil }; +pass = pass and: { ~sceneLaunch.notNil }; +pass = pass and: { ~sceneNext.notNil }; + +// ~sceneSave.(0) must populate slot 0 +try { ~sceneSave.(0) } { |e| + pass = false; + ("EXCEPTION in ~sceneSave: " ++ e.class.name).postln; +}; +pass = pass and: { ~lp[\scenes][0].notNil }; + +// ~sceneLaunch.(0) must not raise +try { ~sceneLaunch.(0) } { |e| + pass = false; + ("EXCEPTION in ~sceneLaunch: " ++ e.class.name).postln; +}; + +// ~sceneNext must not raise (slot 0 filled; wraps back and launches 0 again) +try { ~sceneNext.() } { |e| + pass = false; + ("EXCEPTION in ~sceneNext: " ++ e.class.name).postln; +}; + +pass.if( + { "TEST PASS".postln }, + { "TEST FAIL".postln } +); +0.exit; +)