Put a sound in a room, a hall or a cave with a Reverb node (#328)
game-maker / gate (push) Successful in 4m0s
game-maker / suite (push) Failing after 13m51s

A sound FX had no space around it. The Reverb node folds the sound
with an impulse response: one it makes itself, or a sound asset as the
answer of a real space. It works on a voice and on a bus.

- Type: Room, Hall, Cave and Plate have fixed numbers. Custom shows
  Decay (up to 10 s), Damping and Pre-delay. Sound shows the pick of a
  sound asset and Pre-delay. The rows are derived ports, the sound pick
  an idle param, so they only appear for their type.
- The made impulse response is seeded noise per side, 60 dB down at
  Decay, through a low-pass closing from 20 kHz toward 500 Hz as far
  as Damping says. It is made once per rate, decay and damping, the
  last 16 are kept.
- A sound as impulse response: the sound FX compilers now get the
  project, so a node can turn a sound asset into its file. The build
  key holds that file, so a cut or swapped sound builds again. The
  instrument compiler does the same for a Reverb inside an instrument.
- The library loads that file with the voice, a sequence or before a
  bus builds its chain. A missing file leaves the reverb silent.
- Mix blends dry and reverb and is the only number that moves while
  the sound plays, as the issue said. The voice rings for pre-delay
  plus decay, or as long as its sound.
- The open point from #325 about sends on project buses is #351, not
  part of this.

Tests:
- Unit reverb (new) 4, soundfx 57 (4 new), sound-library 104 (2 new),
  green.
- Counter-checks, each turns one test red: the impulse file not loaded
  with the voice, the tail without the length of the sound.
- e2e 34 (melektaus-atx): ALL PASSED, 81 checks (1 new). Chrome
  offline: silent during the pre-delay, 35.9 dB fall over 0.6 s of a
  1 s decay (36 expected), the dry half at 0.25 for mix 0.5. A sound
  with one sample at 0.2 s gives the impulse back 0.25 s later and
  nowhere else.
- Gate (melektaus-atx) over the end of this series #318 to #328:
  CHECK EXIT 0, 267 files, 4134 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-07 21:27:22 +02:00
1 parent 071f194326
commit d349251a01
14 files changed
+544 -47

No files matched your search

+4 -3
View File
@@ -82,10 +82,11 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
sampleRate: () => soundLibrary.liveContext().sampleRate,
/**
* The nodes a stage of a voice becomes, in any context – an offline one renders what it does.
* `at` is when its voice starts, which a Flanger's sweep counts from (#341).
* `at` is when its voice starts, which a Flanger's sweep counts from (#341), `impulse` the
* sound a Reverb that takes one has (#328).
*/
stageNodes: (context: BaseAudioContext, stage: FxStage, at?: number) =>
stageNodes(context, stage, undefined, at),
stageNodes: (context: BaseAudioContext, stage: FxStage, at?: number, impulse?: AudioBuffer) =>
stageNodes(context, stage, undefined, at, impulse),
/** The gain in dB a Saturation's auto-gain gives at a drive (#323). */
saturationAutoGain,
/** What drives the two shares of a Mix "Both at center", in any context (unstarted). */
+1 -1
View File
@@ -311,7 +311,7 @@ export class GraphCanvas extends Widget {
: asset.type === 'particles'
? particleLibrary.buildFor(asset).problems
: asset.type === 'soundfx'
? soundFxBuildFor(asset).problems
? soundFxBuildFor(asset, this.state.project).problems
: asset.type === 'instrument'
? instrumentCheck(asset, instrumentSourcesOf(this.state.project)).problems
: asset.type === 'song'
+5 -3
View File
@@ -100,7 +100,7 @@ export class SoundFxEditor extends AssetTabEditor<SoundFxAsset, GraphCanvas> {
play(): void {
const asset = this.asset();
const sound = findAssetOf(this.state.project, this.previewSoundId, 'sound');
const plan = asset && soundFxBuildFor(asset).compiled;
const plan = asset && soundFxBuildFor(asset, this.state.project).compiled;
if (!sound?.file || !plan) return;
const props = resolveAssetProps(this.state.project, sound);
const fx = { plan, params: { ...this.values } };
@@ -123,7 +123,7 @@ export class SoundFxEditor extends AssetTabEditor<SoundFxAsset, GraphCanvas> {
}
protected override infoText(asset: SoundFxAsset): string {
const build = soundFxBuildFor(asset);
const build = soundFxBuildFor(asset, this.state.project);
if (!build.compiled) return build.reason;
return `${build.effects} ${build.effects === 1 ? 'effect' : 'effects'}`;
}
@@ -133,7 +133,9 @@ export class SoundFxEditor extends AssetTabEditor<SoundFxAsset, GraphCanvas> {
const sound = findAssetOf(this.state.project, this.previewSoundId, 'sound');
this.soundPicker.setText(sound?.name ?? 'No sound');
this.soundPicker.setEnabled(Boolean(asset && sound));
this.playButton.setEnabled(Boolean(asset && sound?.file && soundFxBuildFor(asset).compiled));
this.playButton.setEnabled(
Boolean(asset && sound?.file && soundFxBuildFor(asset, this.state.project).compiled),
);
this.stopButton.setEnabled(Boolean(asset));
this.syncParams(asset);
}