Add an Audio Effect variant of the device, alongside the MIDI one
The device is a pure observer — it reads the LOM and talks to GameSense and never looks at a note — so it does not have to sit in the path of anything you play. But a Max MIDI Effect without midiin -> midiout swallows MIDI instead of passing it on, so the shipped device routes every note you play through Max's scheduler on its way to your instrument. Pointless cost for this device. max/scale-lighting-audio.maxpat is the same patcher as an Audio Effect, declaring no I/O at all: no midiin/midiout, no plugin~/plugout~. Park it on a dedicated empty Audio track and it is provably out of every signal path. The two patchers differ by exactly midiin, midiout and the patchline between them — @watch 1 and everything else are deliberately left identical on both sides, so an A/B measures the passthrough and nothing else. Both register the same GameSense game, so they have to be loaded one at a time; that and a way to actually measure the difference (compare note onsets across recorded takes, not the CPU meter) are in the README. The patcher test now runs over both files, and two new tests pin the distinction: the MIDI variant must wire midiin to midiout, and the audio variant must declare none of those four objects — the kind of thing a later edit would otherwise silently undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -507,42 +507,82 @@ test('status says whether the board is being driven', async () => {
|
||||
|
||||
section('Max patcher');
|
||||
|
||||
test('scale-lighting.maxpat is valid JSON with the objects we wired', () => {
|
||||
const raw = fs.readFileSync(path.join(REPO, 'max', 'scale-lighting.maxpat'), 'utf8');
|
||||
const patch = JSON.parse(raw) as {
|
||||
patcher: {
|
||||
boxes: { box: { id: string; text?: string } }[];
|
||||
lines: { patchline: { source: [string, number]; destination: [string, number] } }[];
|
||||
};
|
||||
interface Patcher {
|
||||
patcher: {
|
||||
boxes: { box: { id: string; text?: string } }[];
|
||||
lines: { patchline: { source: [string, number]; destination: [string, number] } }[];
|
||||
};
|
||||
}
|
||||
|
||||
function readPatcher(file: string): Patcher {
|
||||
return JSON.parse(fs.readFileSync(path.join(REPO, 'max', file), 'utf8')) as Patcher;
|
||||
}
|
||||
|
||||
// Both variants drive the same device; they differ only in what they let
|
||||
// through, which is the whole reason the audio one exists.
|
||||
for (const file of ['scale-lighting.maxpat', 'scale-lighting-audio.maxpat']) {
|
||||
test(`${file} is valid JSON with the objects we wired`, () => {
|
||||
const patch = readPatcher(file);
|
||||
const boxes = patch.patcher.boxes.map((b) => b.box);
|
||||
const texts = boxes.map((b) => b.text ?? '');
|
||||
const expectedTexts = [
|
||||
'live.thisdevice',
|
||||
'js scale-observer.js',
|
||||
// Step 3: the lighting controls, and blanking the board on device deletion.
|
||||
'lights 1',
|
||||
'lights 0',
|
||||
'retry',
|
||||
'closebang',
|
||||
'shutdown',
|
||||
];
|
||||
for (const expected of expectedTexts) {
|
||||
assert.ok(texts.includes(expected), `${file} is missing ${expected}`);
|
||||
}
|
||||
assert.ok(
|
||||
texts.some((t) => t.startsWith('node.script scale-device.js')),
|
||||
`${file} is missing node.script`
|
||||
);
|
||||
|
||||
// Every patch cord must point at a box that exists.
|
||||
const ids = new Set(boxes.map((b) => b.id));
|
||||
for (const { patchline } of patch.patcher.lines) {
|
||||
assert.ok(ids.has(patchline.source[0]), `dangling source ${patchline.source[0]}`);
|
||||
assert.ok(
|
||||
ids.has(patchline.destination[0]),
|
||||
`dangling destination ${patchline.destination[0]}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// A Max MIDI Effect without this swallows MIDI instead of passing it on.
|
||||
test('the MIDI variant passes MIDI through', () => {
|
||||
const patch = readPatcher('scale-lighting.maxpat');
|
||||
const boxes = patch.patcher.boxes.map((b) => b.box);
|
||||
const texts = boxes.map((b) => b.text ?? '');
|
||||
const expectedTexts = [
|
||||
'live.thisdevice',
|
||||
'js scale-observer.js',
|
||||
'midiin',
|
||||
'midiout',
|
||||
// Step 3: the lighting controls, and blanking the board on device deletion.
|
||||
'lights 1',
|
||||
'lights 0',
|
||||
'retry',
|
||||
'closebang',
|
||||
'shutdown',
|
||||
];
|
||||
for (const expected of expectedTexts) {
|
||||
assert.ok(texts.includes(expected), `patcher is missing ${expected}`);
|
||||
}
|
||||
assert.ok(
|
||||
texts.some((t) => t.startsWith('node.script scale-device.js')),
|
||||
'patcher is missing node.script'
|
||||
);
|
||||
const idOf = (text: string) => boxes.find((b) => b.text === text)?.id;
|
||||
|
||||
// Every patch cord must point at a box that exists.
|
||||
const ids = new Set(boxes.map((b) => b.id));
|
||||
for (const { patchline } of patch.patcher.lines) {
|
||||
assert.ok(ids.has(patchline.source[0]), `dangling source ${patchline.source[0]}`);
|
||||
assert.ok(ids.has(patchline.destination[0]), `dangling destination ${patchline.destination[0]}`);
|
||||
const from = idOf('midiin');
|
||||
const to = idOf('midiout');
|
||||
assert.ok(from && to, 'patcher is missing midiin/midiout');
|
||||
assert.ok(
|
||||
patch.patcher.lines.some(
|
||||
({ patchline }) => patchline.source[0] === from && patchline.destination[0] === to
|
||||
),
|
||||
'midiin is not connected to midiout'
|
||||
);
|
||||
});
|
||||
|
||||
// The point of the audio variant: it declares no I/O, so nothing the user plays
|
||||
// is ever routed through Max. Adding a passthrough here would silently undo it.
|
||||
test('the audio variant is not in the path of anything', () => {
|
||||
const texts = readPatcher('scale-lighting-audio.maxpat').patcher.boxes.map(
|
||||
(b) => b.box.text ?? ''
|
||||
);
|
||||
for (const forbidden of ['midiin', 'midiout', 'plugin~', 'plugout~']) {
|
||||
assert.ok(
|
||||
!texts.includes(forbidden),
|
||||
`the audio variant must not declare I/O, but has ${forbidden}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user