feat: matrix save load presets in sc
CI build oscope-of / build-check (push) Has been cancelled

Add durable file-based persistence for the 16x32 arrangement matrix:
~matSave / ~matLoad / ~matNames helpers with OSC routes /matrix/save,
/matrix/load, /matrix/list; ~matGridPush sends full 512-int grid dump
on load; preset dir scaffolded.  Root-causes fixed during development:
String.== is identity-based in SC (use Symbol storage + \sym literals),
String.copyFromTo -> copyRange, Pipe/pathMatch unreliable headless.
Headless test round-trip: save -> clear -> load -> verify.
This commit is contained in:
L'électron rare
2026-06-28 18:24:22 +02:00
parent 3ce4c38f1c
commit 2ed077bbf5
3 changed files with 170 additions and 0 deletions
+127
View File
@@ -24,6 +24,12 @@
~matBeatsPerBar = ~matBeatsPerBar ? 4;
~matLastColor = ~matLastColor ? Array.fill(~matVoices.size, -1);
// -- Persistence env init (nil-guarded) --
~matDir = ~matDir ? "~/.config/av-live/matrices".standardizePath;
~matPresetDir = ~matPresetDir ? (PathName(thisProcess.nowExecutingPath).pathOnly ++ "matrix_presets");
~matSavedNames = ~matSavedNames ? Set.new;
File.exists(~matDir).not.if({ ("mkdir -p " ++ ~matDir.quote).systemCmd });
// -- ~matBaseFor : capture base Pdef source once (lazy, per voice name symbol) --
// Returns nil when the Pdef is missing (safe for partial environments).
~matBaseFor = { |name|
@@ -150,6 +156,112 @@
~matPush.()
};
// -- ~matSafeName : sanitize a name to a safe filename (alphanumeric, -, _, space→_) --
~matSafeName = { |name|
var s = name.asString.select({ |c| c.isAlphaNum or: { "-_ ".includes(c) } }).replace(" ", "_");
(s.size > 0).if({ s }, { "untitled" })
};
// -- ~matGridValid : confirm a grid is 16 x matBars with integer cells 0..6 --
~matGridValid = { |g|
g.isArray and: {
g.size == ~matVoices.size and: {
g.every({ |row|
row.isArray and: {
row.size == ~matBars and: {
row.every({ |v| v.isInteger and: { v >= 0 and: { v <= 6 } } })
}
}
})
}
}
};
// -- ~matNames : list base names of .matrix files found in a directory --
// User dir: served from in-memory ~matSavedNames (populated by ~matSave) —
// avoids filesystem-scan quirks that can fail headless in some SC contexts.
// Other dirs (e.g. shipped presets): scan via ls+tmpfile (systemCmd is
// synchronous and works headless; pathMatch/Pipe.getLine are unreliable).
~matNames = { |dir|
(dir == ~matDir).if({
~matSavedNames.asArray
}, {
var ext = ".matrix";
var result = [];
File.exists(dir).if({
var tmp = "/tmp/_av_live_matnames_.txt";
("ls -1 '" ++ dir ++ "' > '" ++ tmp ++ "' 2>/dev/null").systemCmd;
File.exists(tmp).if({
var content = File.use(tmp, "r", { |f| f.readAllString });
("rm -f '" ++ tmp ++ "'").systemCmd;
content.split($\n).do({ |fn|
(fn.size > ext.size and: {
fn.copyRange(fn.size - ext.size, fn.size - 1) == ext
}).if({
result = result.add(fn.copyRange(0, fn.size - ext.size - 1).asSymbol)
})
})
})
});
result
})
};
// -- ~matGridPush : send full 512-int grid row-major to surfaces --
// index = vi * 32 + bar (voice-major, flatten concatenates rows in order)
~matGridPush = {
~toscSend !? { ~toscSend.valueArray(["/matrix/grid"] ++ ~lp[\matrix].flatten) }
};
// -- ~matListPush : send all available preset + user-save names to surfaces --
~matListPush = {
try {
var names = (~matNames.(~matPresetDir) ++ ~matNames.(~matDir)).as(Set).asArray.sort;
~toscSend !? { ~toscSend.valueArray(["/matrix/list"] ++ names) }
} { |e|
("[matrix] listPush err: " ++ e.class.name).postln
}
};
// -- ~matSave : persist current grid to ~/.config/av-live/matrices/<name>.matrix --
~matSave = { |name|
var safe = ~matSafeName.(name);
var path = ~matDir +/+ (safe ++ ".matrix");
File.use(path, "w", { |f| f.write(~lp[\matrix].asCompileString) });
~matSavedNames.add(safe.asSymbol);
("[matrix] saved " ++ path).postln;
~matListPush.();
};
// -- ~matLoadFile : load and validate a grid from an absolute path --
~matLoadFile = { |path|
var g;
File.exists(path).if({
g = File.use(path, "r", { |f| f.readAllString }).interpret;
~matGridValid.(g).if({
~lp[\matrix] = g;
~lp[\matPlaying].if({ ~matApplyBar.(~lp[\matBar]) });
~matGridPush.();
("[matrix] loaded " ++ path).postln;
true
}, {
("[matrix] invalid grid in " ++ path).warn;
false
})
}, {
("[matrix] not found " ++ path).warn;
false
})
};
// -- ~matLoad : load by name; user save wins over shipped preset --
~matLoad = { |name|
var safe = ~matSafeName.(name);
var up = ~matDir +/+ (safe ++ ".matrix");
var pp = ~matPresetDir +/+ (safe ++ ".matrix");
File.exists(up).if({ ~matLoadFile.(up) }, { ~matLoadFile.(pp) })
};
// -- OSCdefs (unique \mat_* keys; register sender for TouchOSC feedback) --
OSCdef(\mat_cell, { |msg, time, addr|
~toscTouch !? { ~toscTouch.(addr) };
@@ -171,5 +283,20 @@ OSCdef(\mat_clear, { |msg, time, addr|
~matClear.()
}, '/matrix/clear');
OSCdef(\mat_save, { |msg, time, addr|
~toscTouch !? { ~toscTouch.(addr) };
~matSave.(msg[1].asString)
}, '/matrix/save');
OSCdef(\mat_load, { |msg, time, addr|
~toscTouch !? { ~toscTouch.(addr) };
~matLoad.(msg[1].asString)
}, '/matrix/load');
OSCdef(\mat_list, { |msg, time, addr|
~toscTouch !? { ~toscTouch.(addr) };
~matListPush.()
}, '/matrix/list');
"[matrix] ready".postln;
)
+43
View File
@@ -62,6 +62,49 @@ try {
("EXCEPTION in ~matStop: " ++ e.class.name).postln
};
// -- Persistence: save / load round-trip (touches filesystem, valid headless) --
~matSetCell.(1, 7, 4);
~matSetCell.(2, 0, 2);
try {
~matSave.("test grid")
} { |e|
pass = false;
("EXCEPTION in ~matSave: " ++ e.class.name).postln
};
pass = pass and: {
File.exists(~matDir +/+ "test_grid.matrix").if({ true }, {
("FAIL: save file not found at " ++ (~matDir +/+ "test_grid.matrix")).postln;
false
})
};
try {
~matClear.()
} { |e|
pass = false;
("EXCEPTION in ~matClear: " ++ e.class.name).postln
};
pass = pass and: { ~lp[\matrix][1][7] == 0 };
try {
~matLoad.("test grid")
} { |e|
pass = false;
("EXCEPTION in ~matLoad: " ++ e.class.name).postln
};
pass = pass and: { ~lp[\matrix][1][7] == 4 };
pass = pass and: { ~lp[\matrix][2][0] == 2 };
pass = pass and: { ~matGridValid.(~lp[\matrix]) };
pass = pass and: {
~matNames.(~matDir).includes(\test_grid).if({ true }, {
"FAIL: test_grid not in matNames".postln;
false
})
};
File.delete(~matDir +/+ "test_grid.matrix");
pass.if(
{ "TEST PASS".postln },
{ "TEST FAIL".postln }