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>
56 lines
2.2 KiB
TypeScript
56 lines
2.2 KiB
TypeScript
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] };
|
||
}
|