Files
feuerwerk/src/core/music/needs.ts
T
adminandClaude Opus 5.5 a295d233ed Give sounds, instruments, patterns and songs a sound FX (#352)
A sound FX could only be picked where something was played: on a Play
node, a track or an item of a song. A sound that should always sound
through the same effect needed that pick everywhere it was used. Now
the asset says it once, and every place that plays it follows.

- New row Sound FX on a sound, an instrument, a pattern and a song,
  right before Bus. A sound goes through it on every voice, an
  instrument on every note after its graph, a pattern on all its
  notes together and a song on its output.
- The fx input of Play sound, Play note, Play pattern and Play song
  and the Sound FX of a song item now read "From the asset" while
  empty. None is a pick of its own, stored as `#none` like `#master`.
  A pick replaces the FX of the asset, it does not come on top.
- A pattern item's Instrument reads "From the asset" instead of None.
  The meaning is unchanged.
- A double-click on a sound item opens the sound, as one on a pattern
  item opens the pattern.
- The tabs play through the FX of their asset. The Pattern and Song
  tab put it on a bus of their own between the output and the bus of
  the asset. The keys of the Pattern tab play there too.
- Notes of a pattern or song go through the FX of their instrument.
  Its chain is loaded with the other needs of the sequence.

Decided without asking:
- Old items and Play nodes with an empty fx now mean "From the
  asset". No asset has an FX yet, so nothing sounds different.
- The label is "From the asset" as on Loop and Bus, not "From asset".
- A sound used as a Sample, as the impulse of a Reverb or as the test
  sound of the Sound FX tab plays without its own FX.

Findings:
- In e2e, `__gm.inspectorField('Sound FX')` and `('Instrument')` find
  the group of the Assets panel first. The check asks for the last
  match.

Tests:
- Unit asset-fx (new): 8 green. Six counter-checks each turned red.
- Field lists of sound, pattern and song: Sound FX added.
- e2e 65 (melektaus-atx): ALL PASSED, 46 checks (3 new). Counter-check
  without the FX bus of the tab: that check red.
- Gate (melektaus-atx): CHECK EXIT 0, 268 files, 4142 tests.
- Not checked: how it sounds, the Sound, Instrument and Pattern tab
  in the browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 22:02:26 +02:00

56 lines
2.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { instrumentBuildFor, instrumentSourcesOf } from '../instrument/compile';
import { findAssetOf, type Project } from '../model/Project';
import { assetFxOf } from '../properties/assetFx';
import { playedSoundById } from '../properties/playedSound';
import { soundFxBuildFor } from '../soundfx/compile';
import { DRY_VOICE, type FxParams, type FxPlan, type FxVoice } from '../soundfx/nodes';
import type { SequenceNeeds } from './playbacks';
import type { Sequence } from './sequence';
/**
* A sound FX by its asset id with values for its parameters: null where there is none, it is gone or
* it does not compile – what plays through it then plays dry, as with `Play sound`.
*/
export function fxVoiceOf(
project: Project,
fxId: string,
tempo: number,
params: FxParams = {},
): FxVoice | null {
const fx = fxId ? findAssetOf(project, fxId, 'soundfx') : undefined;
const plan = fx && soundFxBuildFor(fx, project).compiled;
return plan ? { plan, params, tempo } : null;
}
/** The sound FX a clip goes through. */
export function clipFx(project: Project, fxId: string, tempo: number): FxVoice {
return fxVoiceOf(project, fxId, tempo) ?? DRY_VOICE;
}
/** Every layer and FX of every note and every file and FX of every clip a sequence plays. */
export function sequenceNeeds(project: Project, sequence: Sequence): SequenceNeeds {
const sources = instrumentSourcesOf(project);
const seen = new Set<string>();
const layers: FxPlan[] = [];
for (const { instrument, note } of sequence.notes) {
const key = `${instrument}\u0000${note}`;
const asset = findAssetOf(project, instrument, 'instrument');
if (seen.has(key) || !asset) continue;
if (!seen.has(instrument)) {
seen.add(instrument);
const fx = fxVoiceOf(project, assetFxOf(asset.props), 120);
if (fx) layers.push(fx.plan);
}
seen.add(key);
layers.push(...instrumentBuildFor(asset, note, sources).layers);
}
const files = new Set<string>();
for (const clip of sequence.clips ?? []) {
const file = playedSoundById(project, clip.sound);
if (file) files.add(file);
const fx = clipFx(project, clip.fx, 120);
if (fx !== DRY_VOICE) layers.push(fx.plan);
}
return { layers, files: [...files] };
}