diff --git a/sound_algo/data_only/matrix.scd b/sound_algo/data_only/matrix.scd index 20302d2..374448b 100644 --- a/sound_algo/data_only/matrix.scd +++ b/sound_algo/data_only/matrix.scd @@ -23,6 +23,8 @@ ~matBase = ~matBase ? IdentityDictionary.new; ~matBeatsPerBar = ~matBeatsPerBar ? 4; ~matLastColor = ~matLastColor ? Array.fill(~matVoices.size, -1); +~lp[\matLoopStart] = ~lp[\matLoopStart] ? 0; +~lp[\matLoopEnd] = ~lp[\matLoopEnd] ? (~matBars - 1); // -- Persistence env init (nil-guarded) -- ~matDir = ~matDir ? "~/.config/av-live/matrices".standardizePath; @@ -81,6 +83,12 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd }); }) }; +// -- ~matNextBar : compute next playhead bar clipped within the loop region -- +// Wraps nb back to loop start when nb overshoots le or undershoots ls. +~matNextBar = { var nb = ~lp[\matBar] + 1; var ls = ~lp[\matLoopStart] ? 0; var le = ~lp[\matLoopEnd] ? (~matBars - 1); + ((nb > le) or: { nb < ls }).if({ nb = ls }); + nb.clip(0, ~matBars - 1) }; + // -- ~matApplyBar : set each voice's Pdef to its cell color and (re)play or stop -- // Re-sourcing a playing Pdef updates it at the next quant boundary (smooth for v1). ~matApplyBar = { |bar| @@ -122,6 +130,19 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd }); ~toscSend !? { ~toscSend.("/matrix/trig", vi, amp.clip(0, 1)) } }; +// -- ~matLoopPush : send current loop region to surfaces -- +~matLoopPush = { ~toscSend !? { ~toscSend.("/matrix/loop", ~lp[\matLoopStart] ? 0, ~lp[\matLoopEnd] ? (~matBars - 1)) } }; + +// -- ~matSetLoop : set loop region (auto-ordered) and push to surfaces -- +~matSetLoop = { |s, e| var a = s.asInteger.clip(0, ~matBars - 1); var b = e.asInteger.clip(0, ~matBars - 1); + (a <= b).if({ ~lp[\matLoopStart] = a; ~lp[\matLoopEnd] = b }, { ~lp[\matLoopStart] = b; ~lp[\matLoopEnd] = a }); + ~matLoopPush.() }; + +// -- ~matSeek : jump playhead to bar; re-applies if playing -- +~matSeek = { |bar| ~lp[\matBar] = bar.asInteger.clip(0, ~matBars - 1); + ~toscSend !? { ~toscSend.("/matrix/playhead", ~lp[\matBar]) }; + ~lp[\matPlaying].if({ ~matApplyBar.(~lp[\matBar]) }) }; + // -- ~matStop : halt the playhead Routine and silence all matrix voices -- ~matStop = { ~matRoutine !? { ~matRoutine.stop }; @@ -139,13 +160,13 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd }); var clock = ~lp[\clock] ? TempoClock.default; ~matRoutine !? { ~matRoutine.stop }; ~lp[\matPlaying] = true; - ~lp[\matBar] = 0; + ~lp[\matBar] = ~lp[\matLoopStart] ? 0; ~matRoutine = Routine({ loop { ~matApplyBar.(~lp[\matBar]); ~toscSend !? { ~toscSend.("/matrix/playhead", ~lp[\matBar]) }; ~matBeatsPerBar.wait; - ~lp[\matBar] = (~lp[\matBar] + 1) % ~matBars + ~lp[\matBar] = ~matNextBar.value; } }).play(clock, quant: ~matBeatsPerBar); ~matPush.() @@ -227,7 +248,8 @@ File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd }); ~matListPush = { try { var names = (~matNames.(~matPresetDir) ++ ~matNames.(~matDir)).as(Set).asArray.sort; - ~toscSend !? { ~toscSend.valueArray(["/matrix/list"] ++ names) } + ~toscSend !? { ~toscSend.valueArray(["/matrix/list"] ++ names) }; + ~matLoopPush.() } { |e| ("[matrix] listPush err: " ++ e.class.name).postln } @@ -308,5 +330,15 @@ OSCdef(\mat_list, { |msg, time, addr| ~matListPush.() }, '/matrix/list'); +OSCdef(\mat_loop, { |msg, time, addr| + ~toscTouch !? { ~toscTouch.(addr) }; + ~matSetLoop.(msg[1].asInteger, msg[2].asInteger) +}, '/matrix/loop'); + +OSCdef(\mat_seek, { |msg, time, addr| + ~toscTouch !? { ~toscTouch.(addr) }; + ~matSeek.(msg[1].asInteger) +}, '/matrix/seek'); + "[matrix] ready".postln; ) diff --git a/sound_algo/data_only/test/test_matrix.scd b/sound_algo/data_only/test/test_matrix.scd index 6334ba5..599c6ac 100644 --- a/sound_algo/data_only/test/test_matrix.scd +++ b/sound_algo/data_only/test/test_matrix.scd @@ -141,6 +141,36 @@ pass = pass and: { ~trigLog[0] == "/matrix/trig" }; pass = pass and: { ~trigLog[1] == 5 }; pass = pass and: { ~trigLog[2] == 1.0 }; // color 2 spec[\amp]=1.0; base has no \amp +// -- Loop region -- +pass = pass and: { ~matNextBar.notNil and: { ~matSetLoop.notNil and: { ~matSeek.notNil } } }; + +~matSetLoop.(4, 8); +pass = pass and: { ~lp[\matLoopStart] == 4 }; +pass = pass and: { ~lp[\matLoopEnd] == 8 }; + +// Swapped args must auto-order +~matSetLoop.(10, 6); +pass = pass and: { ~lp[\matLoopStart] == 6 }; +pass = pass and: { ~lp[\matLoopEnd] == 10 }; + +// Wrap: loop 2..4 +~matSetLoop.(2, 4); +~lp[\matBar] = 4; +pass = pass and: { ~matNextBar.value == 2 }; // at le -> wraps to loop start +~lp[\matBar] = 2; +pass = pass and: { ~matNextBar.value == 3 }; // inside region -> advances +~lp[\matBar] = 0; +pass = pass and: { ~matNextBar.value == 2 }; // below ls -> snap to loop start + +// Seek +~matSeek.(7); +pass = pass and: { ~lp[\matBar] == 7 }; +~matSeek.(99); +pass = pass and: { ~lp[\matBar] == 31 }; // clamped to ~matBars - 1 + +// Reset loop to full range +~matSetLoop.(0, 31); + pass.if( { "TEST PASS".postln }, { "TEST FAIL".postln }