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

+48 -8
View File
@@ -1,6 +1,6 @@
/**
* An instrument (#331) is a graph that makes a note a sound: Notes nodes bring the note in, Samples
* play sounds for it, the effects of a sound FX shape them and the Output sums them. It is compiled
* play sounds for it, the effects of a sound FX shape and mix them, and the Output takes one. It is compiled
* once per note; a trigger is on only for the notes it stands for.
*/
import { describe, expect, it } from 'vitest';
@@ -183,7 +183,7 @@ describe('compiling a note', () => {
expect(way(compile(f, 67).layers[0])?.pitch).toEqual([]);
});
it('a drum kit: one Notes node with single notes, a Sample on each, both into the Output', () => {
it('a drum kit: one Notes node with single notes, a Sample on each, mixed into the Output', () => {
const f = emptyInstrument();
const notes = f.edit.addNode('InstrumentNotes', {
from: '36',
@@ -193,13 +193,16 @@ describe('compiling a note', () => {
});
const kick = f.edit.addNode('InstrumentSample', { sound: f.sound('Kick', KICK).id, followNote: false });
const snare = f.edit.addNode('InstrumentSample', { sound: f.sound('Snare', SNARE).id, play: 'oneshot' });
const mix = f.edit.addNode('SoundFxMix', { mix: 0.5 });
const output = f.edit.addNode('InstrumentOutput');
f.edit.connect(notes, 'n36', kick, 'trigger');
f.edit.connect(notes, 'n38', snare, 'trigger');
f.edit.connect(kick, 'out', output, 'in');
f.edit.connect(snare, 'out', output, 'in');
// The Output takes both: a second edge into it does not replace the first.
expect(f.instrument.graph.edges.filter((e) => e.to.node === output.id)).toHaveLength(2);
f.edit.wire([
[notes, 'n36', kick, 'trigger'],
[notes, 'n38', snare, 'trigger'],
[kick, 'out', mix, 'a'],
[snare, 'out', mix, 'b'],
[mix, 'out', output, 'in'],
]);
// Only one drum sounds for a note, so the Mix is the other side and plays it at its own volume.
const files = (note: number) => compile(f, note).layers.map((plan) => way(plan)?.source?.file);
expect(files(36)).toEqual([KICK]);
expect(files(38)).toEqual([SNARE]);
@@ -219,6 +222,43 @@ describe('compiling a note', () => {
expect(def.outputs).toHaveLength(4 + 12);
});
it('takes one sound at the Output: a second edge replaces the first', () => {
const f = emptyInstrument();
const notes = f.edit.addNode('InstrumentNotes');
const a = f.edit.addNode('InstrumentSample', { sound: f.sound('A', KICK).id });
const b = f.edit.addNode('InstrumentSample', { sound: f.sound('B', SNARE).id });
const output = f.edit.addNode('InstrumentOutput');
f.edit.wire([
[notes, 'any', a, 'trigger'],
[notes, 'any', b, 'trigger'],
[a, 'out', output, 'in'],
[b, 'out', output, 'in'],
]);
const into = f.instrument.graph.edges.filter((e) => e.to.node === output.id);
expect(into.map((e) => e.from.node)).toEqual([b.id]);
expect(compile(f, 60).layers.map((plan) => way(plan)?.source?.file)).toEqual([SNARE]);
});
it('says so of an Output saved with more than one sound, which it played together then', () => {
const f = makeInstrument();
const sample = nodeOf(f, 'InstrumentSample');
sample.params.sound = f.sound('A', KICK).id;
const output = nodeOf(f, 'InstrumentOutput');
const edge = f.instrument.graph.edges.find((e) => e.to.node === output.id)!;
f.instrument.graph.edges.push({ ...edge, id: 'second' });
expect(compile(f, 60)).toMatchObject({
layers: [],
problems: { [output.id]: 'Takes one sound: put a Mix in front of it to play several' },
});
});
it('an Output with nothing in it says so', () => {
const f = makeInstrument();
const output = nodeOf(f, 'InstrumentOutput');
f.instrument.graph.edges = f.instrument.graph.edges.filter((e) => e.to.node !== output.id);
expect(compile(f, 60)).toMatchObject({ layers: [], reason: 'Nothing comes in: connect it to a Sample' });
});
it('two Notes nodes over one Sample: a trigger takes more than one edge, either one plays it', () => {
const f = emptyInstrument();
const low = f.edit.addNode('InstrumentNotes', { from: '48', to: '52' });