Take one sound at the Output of an instrument, mixed by a Mix #331

The Output summed any number of edges as layers. That made it the only
sound input of its kind, and it read as if a note could be several
voices. A note was always one voice, but the graph did not say so.
Now the Output takes one sound like every other sound input, and
several are put together with a Mix in front of it (decided by the
user after #331).

- The Output's input is no longer `many`. A second edge replaces the
  first, as at any data input.
- With only one drum sounding for a note, a Mix is its other side. A
  drum kit keeps the volume of each drum.
- An instrument saved with several edges into the Output keeps them
  but does not compile. The Output says "Takes one sound: put a Mix in
  front of it to play several". No saved project has one: the only
  instrument there (cbbf160bb7e8) has a single edge.
- An Output with nothing in it is a problem now, not silence.
- The compiler gathers only triggers from several edges. The player
  still takes a list of layers, which an instrument fills with one.

Tests:
- instrument.test.ts: drum kit through a Mix, a second edge replacing
  the first, an old Output with two edges, an empty Output (33 tests)
- Counter-check: `many` back on the Output turns 15 tests red, the
  new one about the second edge among them
- e2e 34 on melektaus-atx: ALL PASSED, 47 checks
- Gate on melektaus-atx: CHECK EXIT 0, 244 test files, 3789 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-06 12:59:52 +02:00
1 parent 23bdc3f775
commit 076c564415
3 files changed
+61 -20

No files matched your search

+11 -10
View File
@@ -1,5 +1,5 @@
import { graphEmitter } from '../graph/emitGraph';
import { type DeclaredPort, type Graph, graphFingerprint, type GraphNode } from '../graph/Graph';
import { type DeclaredPort, edgesInto, type Graph, graphFingerprint, type GraphNode } from '../graph/Graph';
import type { GraphLookup } from '../graph/lookup';
import { resolveDef, terminalNode } from '../graph/nested';
import { inlineValue, type NodeDef } from '../graph/NodeRegistry';
@@ -29,6 +29,7 @@ import {
INSTRUMENT_PARAMETERS,
INSTRUMENT_SAMPLE,
INSTRUMENT_SOUND_FX,
InstrumentOutput,
isInstrumentNodeDef,
loops,
noteOutputKey,
@@ -73,7 +74,7 @@ export function instrumentSourcesOf(project: Project): InstrumentSources {
}
export interface InstrumentBuild {
/** What the note is made of, each played on its own and summed; empty is silence. */
/** What the note is made of: the one sound at the Output, or none for silence. */
layers: readonly FxPlan[];
/** Per node id, what keeps the graph from compiling. */
problems: Record<string, string>;
@@ -82,8 +83,8 @@ export interface InstrumentBuild {
}
/**
* What travels along an edge of an instrument: sounds (none where nothing plays for this note, more
* than one only into the Output), a number, or whether a trigger is on for this note.
* What travels along an edge of an instrument: sounds (none where nothing plays for this note), a
* number, or whether a trigger is on for this note.
*/
type Value = { plans: FxPlan[] } | { amount: FxAmount } | { on: boolean };
@@ -116,7 +117,6 @@ export function compileInstrument(graph: Graph, note: number, sources: Instrumen
emit: (def, inputs, node) => emitNode(def, inputs, node),
unfed: (node, port) => {
if (port.type === 'number') return { amount: { value: asNumber(inlineValue(node, port)) } };
if (port.type === 'audio' && port.many) return SILENT;
walk.problem(
node,
port.type === 'trigger'
@@ -139,10 +139,8 @@ export function compileInstrument(graph: Graph, note: number, sources: Instrumen
);
return null;
},
gather: (_node, port, values) =>
port.type === 'trigger'
? { on: values.some((v) => 'on' in v && v.on) }
: { plans: values.flatMap((v) => ('plans' in v ? v.plans : [])) },
// Only a trigger takes more than one edge: either one turns it on.
gather: (_node, _port, values) => ({ on: values.some((v) => 'on' in v && v.on) }),
});
function emitNode(def: Def, inputs: Record<string, Value>, node: GraphNode): Record<string, Value> {
@@ -214,7 +212,10 @@ export function compileInstrument(graph: Graph, note: number, sources: Instrumen
const output = graph.nodes.find((n) => n.type === INSTRUMENT_OUTPUT);
if (!output) return { layers: [], problems: walk.problems, reason: 'No Output node' };
const arrived = walk.input(output, { key: 'in', label: 'in', type: 'audio', many: true });
// Saved while the Output still took any number of sounds: only the first would be heard.
if (edgesInto(graph, output.id, 'in').length > 1)
walk.problem(output, 'Takes one sound: put a Mix in front of it to play several');
const arrived = walk.input(output, InstrumentOutput.inputs[0]!);
const failed = walk.firstProblem();
if (failed !== null || !arrived || !('plans' in arrived))
return { layers: [], problems: walk.problems, reason: failed ?? 'Cannot compile' };
+2 -2
View File
@@ -234,8 +234,8 @@ export const InstrumentOutput: InstrumentNodeDef = {
category: 'sound',
instrument: true,
description:
'What is heard of a note. It takes any number of sounds and plays them together – the layers of the instrument.',
inputs: [{ ...audioIn, many: true }],
'What is heard of a note. It takes one sound; to play several together, put a Mix in front of it.',
inputs: [audioIn],
outputs: [],
params: [],
};