Play tunes on 14 instrument presets next to the sfxr sounds (#329)
The Preset menu only had sfxr's effects: each a fixed pitch on every key that dies away by itself. Now its top half holds instruments to play melodies, basses and drums on. - Instruments: Square Lead, Pulse Lead, Echo Lead, Octave Arp, Synth Brass, Detuned Saws, String Pad, Organ, Flute, Bell, Pluck, Triangle Bass, Pluck Bass and Drum Kit. A line in the menu parts them from the seven effects below. - Their Chips follow the note, and all but Bell, Pluck and the drums hold it while the key is held. - They come out the same every time they are picked. Randomize and Mutate make a variant, as before. - They use what the graph already has: two Chips through a Mix for Detuned Saws, String Pad, Organ, Flute and Bell, an Envelope on a Filter's cutoff for Brass, Pluck and Pluck Bass, a Pitch for the detune and the octave, a Delay for Echo Lead and Bell. - Drum Kit uses single notes 36 to 46: kick, snare, closed and open hat on their General MIDI keys, each ending by itself. - A small Sketch helper lays the nodes on a grid. The sfxr presets use it too and come out as before. - Numbers keep to the steps of their rows. Pulse Lead's duty is 0.13, not 0.125, because the field rounds to 0.01. Tests: - Unit chip-presets: 12 green (5 new). Every instrument compiles without a problem, doubles its frequency an octave up, holds unless struck and is the same on every pick. The drum kit sounds on exactly 36, 38, 42 and 46. Countercheck with Follow note forced off: red. - e2e 34 (CI image on stein2, melektaus-atx was off): ALL PASSED, 65 checks (1 new). A first run had the scheduler's looped-bar check red by 10 ticks between two clock reads, which the presets do not touch; the second run was green. - Gate (stein2): CHECK EXIT 0, 256 files, 3974 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f3ae19431a
commit
a2d0a02645
4 files changed
+467
-43
No files matched your search
@@ -9,15 +9,24 @@ import type { Graph, GraphNode } from '../../src/core/graph/Graph';
|
||||
import {
|
||||
CHIP_PRESETS,
|
||||
chipPresetGraph,
|
||||
EFFECT_PRESETS,
|
||||
hasChipNumbers,
|
||||
mutatedParams,
|
||||
randomizedParams,
|
||||
TONE_PRESETS,
|
||||
} from '../../src/core/instrument/chipPresets';
|
||||
import { compileInstrument, instrumentSourcesOf } from '../../src/core/instrument/compile';
|
||||
import { CHIP_ROWS, ENVELOPE_ROWS, starterInstrumentGraph } from '../../src/core/instrument/nodes';
|
||||
import type { InstrumentAsset } from '../../src/core/model/Asset';
|
||||
import type { FlatDef } from '../../src/core/properties/PropertySchema';
|
||||
import { CRUSH_ROWS, FILTER_ROWS, fxChainOf } from '../../src/core/soundfx/nodes';
|
||||
import {
|
||||
CRUSH_ROWS,
|
||||
type FxChipPlan,
|
||||
type FxEnvelopePlan,
|
||||
FILTER_ROWS,
|
||||
type FxPlan,
|
||||
fxChainOf,
|
||||
} from '../../src/core/soundfx/nodes';
|
||||
import { makeProject } from '../helpers';
|
||||
|
||||
/** A seeded random, so a draw is the same on every run. */
|
||||
@@ -47,9 +56,17 @@ function withinRows(node: GraphNode, keys: string[]): void {
|
||||
}
|
||||
|
||||
describe('a preset (#329)', () => {
|
||||
it('is a chain from every note through Chip, Envelope, Filter and Bitcrusher, ending by itself', () => {
|
||||
it('has the instruments first in the menu, then the sounds of sfxr', () => {
|
||||
expect(CHIP_PRESETS.map((p) => p.value)).toEqual([
|
||||
...TONE_PRESETS.map((p) => p.value),
|
||||
...EFFECT_PRESETS.map((p) => p.value),
|
||||
]);
|
||||
expect(TONE_PRESETS).toHaveLength(14);
|
||||
});
|
||||
|
||||
it('of sfxr is a chain from every note through Chip, Envelope, Filter and Bitcrusher, ending by itself', () => {
|
||||
const sources = instrumentSourcesOf(makeProject().project);
|
||||
for (const { value } of CHIP_PRESETS) {
|
||||
for (const { value } of EFFECT_PRESETS) {
|
||||
const graph = chipPresetGraph(value, seeded());
|
||||
expect(
|
||||
graph.nodes.map((n) => n.type),
|
||||
@@ -129,6 +146,89 @@ describe('a preset (#329)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/** The Chips and the Envelopes on the ways of a plan, a Mix's two ways included. */
|
||||
function walkPlan(plan: FxPlan): { chips: FxChipPlan[]; envelopes: FxEnvelopePlan[] } {
|
||||
const envelopes = plan.stages.flatMap((stage) => (stage.kind === 'envelope' ? [stage.envelope] : []));
|
||||
if ('mix' in plan) {
|
||||
const a = walkPlan(plan.a);
|
||||
const b = walkPlan(plan.b);
|
||||
return { chips: [...a.chips, ...b.chips], envelopes: [...a.envelopes, ...b.envelopes, ...envelopes] };
|
||||
}
|
||||
return { chips: plan.chip ? [plan.chip] : [], envelopes };
|
||||
}
|
||||
|
||||
describe('an instrument preset', () => {
|
||||
const sources = () => instrumentSourcesOf(makeProject().project);
|
||||
const build = (graph: Graph, note: number) => {
|
||||
const built = compileInstrument(graph, note, sources());
|
||||
return { ...built, ...walkPlan(built.layers[0] ?? { pitch: [], stages: [] }) };
|
||||
};
|
||||
/** Those that die away however long the key is held. */
|
||||
const STRUCK = new Set(['bell', 'pluck', 'drumKit']);
|
||||
|
||||
it('plays every key at its own pitch, without a problem, and holds it unless it is struck', () => {
|
||||
for (const { value } of TONE_PRESETS.filter((p) => p.value !== 'drumKit')) {
|
||||
const graph = chipPresetGraph(value);
|
||||
const [c4, c5] = [60, 72].map((note) => build(graph, note));
|
||||
expect(c4!.reason, value).toBe('');
|
||||
expect(c4!.problems, value).toEqual({});
|
||||
// The first Chip follows the note: an octave up is twice the frequency.
|
||||
expect(c5!.chips[0]!.frequency.value / c4!.chips[0]!.frequency.value, value).toBeCloseTo(2, 6);
|
||||
const held = c4!.envelopes.some((envelope) => envelope.sustain.value > 0);
|
||||
expect(held, value).toBe(!STRUCK.has(value));
|
||||
for (const node of graph.nodes.filter((n) => n.type === 'InstrumentChip'))
|
||||
withinRows(node, Object.keys(CHIP_ROWS));
|
||||
}
|
||||
});
|
||||
|
||||
it('comes out the same every time it is picked', () => {
|
||||
const shape = (graph: Graph) => graph.nodes.map(({ type, x, y, params }) => ({ type, x, y, params }));
|
||||
for (const { value } of TONE_PRESETS)
|
||||
expect(shape(chipPresetGraph(value, seeded(1))), value).toEqual(
|
||||
shape(chipPresetGraph(value, seeded(2))),
|
||||
);
|
||||
});
|
||||
|
||||
it('builds what it says: two Chips a little apart, a filter swept by an Envelope, an arpeggio', () => {
|
||||
const saws = build(chipPresetGraph('detunedSaws'), 60);
|
||||
expect(saws.chips.map((chip) => chip.wave)).toEqual(['saw', 'saw']);
|
||||
const ways = saws.layers[0]!;
|
||||
expect('mix' in ways && 'pitch' in ways.b && ways.b.pitch.map((p) => p.value)).toEqual([0.1]);
|
||||
const bass = chipPresetGraph('pluckBass');
|
||||
const filter = byType(bass, 'SoundFxFilter');
|
||||
const sweep = bass.edges.find((e) => e.to.node === filter.id && e.to.port === 'cutoff');
|
||||
expect(bass.nodes.find((n) => n.id === sweep?.from.node)?.params).toMatchObject({ from: 180, to: 2400 });
|
||||
expect(byType(chipPresetGraph('octaveArp'), 'InstrumentChip').params).toMatchObject({
|
||||
arpeggio: 12,
|
||||
repeat: 0.08,
|
||||
});
|
||||
});
|
||||
|
||||
it('Drum Kit: kick, snare and hi-hats on their General MIDI keys, each ending by itself', () => {
|
||||
const graph = chipPresetGraph('drumKit');
|
||||
const notes = byType(graph, 'InstrumentNotes').params;
|
||||
expect(notes).toMatchObject({ singleNotes: true, name36: 'Kick', name38: 'Snare', name42: 'Closed hat' });
|
||||
const sounding = [];
|
||||
for (let note = 36; note <= 46; note++) {
|
||||
const drum = build(graph, note);
|
||||
expect(drum.reason, String(note)).toBe('');
|
||||
if (drum.layers.length === 0) continue;
|
||||
sounding.push(note);
|
||||
expect(
|
||||
drum.envelopes.every((envelope) => envelope.sustain.value === 0),
|
||||
String(note),
|
||||
).toBe(true);
|
||||
}
|
||||
expect(sounding).toEqual([36, 38, 42, 46]);
|
||||
// The kick drops from its own 160 Hz on every key, the snare is a tone and noise together.
|
||||
const kick = build(graph, 36).chips;
|
||||
expect(kick).toHaveLength(1);
|
||||
expect(kick[0]).toMatchObject({ wave: 'sine', frequency: { value: 160 } });
|
||||
expect(kick[0]!.slide.value).toBeLessThan(0);
|
||||
expect(build(graph, 38).chips.map((chip) => chip.wave)).toEqual(['triangle', 'noise']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Randomize and Mutate (#329)', () => {
|
||||
/** A preset with a parameter fed into the Chip's duty, which neither may touch. */
|
||||
function fedPreset(): Graph {
|
||||
|
||||
Reference in new issue
Block a user