Play a sound through a graph of gain, pitch and EQ effects #311

A sound could only be played as it was uploaded. A Sound FX asset is
a node graph like a shader: Input, effects, Output. Play sound has a
new input fx that sends the voice through it.

- New asset type soundfx ("Sound FX"), not placeable, with a graph
  and its own tab. The heading in the asset panel is "Sound effects",
  so it does not take the click meant for the + menu entry.
- Nodes in core/soundfx/nodes.ts, category sound, port type audio
  that only fits audio: Input, Output, Gain (dB), Pitch (semitones),
  EQ (80 Hz and 12 kHz shelves, peaks at 250 Hz, 1 kHz and 4 kHz).
- compileSoundFx walks back from the Output with the shared graph
  emitter. An input takes one edge, so the result is one line: a
  pitch and a list of gain and filter stages. A node without input
  or a graph without Output reports a problem on the node.
- Pitch is the playback rate of the source. It applies to the whole
  sound and also changes its length, the node description says so.
- The BehaviorSystem resolves fx and hands the line to the AudioSink.
  An fx that is missing or does not compile plays the sound dry.
- SoundLibrary wires source, stages, volume, destination in that
  order. stats().last now also names pitch and stage count.
- The tab header picks one of the project's sounds and plays it
  through the graph as it stands. Stop there ends only its own voice.

Tests:
- soundfx.test.ts (new, 11), sound-library (9, +2 for the wiring),
  node-catalog (+1 for fx), graph and asset-groups adjusted
- e2e 34: ALL PASSED - 26 checks (+5: + menu opens the tab, palette,
  2 effects, tab Play and game Play through the FX at -12 st)
- Gate on melektaus-atx: typecheck, lint, prettier clean, 239 of 240
  test files green. The red one was sound-library (its fake
  AudioContext had no playbackRate), fixed afterwards, gate not
  re-run in full.
- Counter-check: without the audio rule in portsCompatible the port
  test fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-05 16:45:42 +02:00
1 parent 93c0a80b7d
commit de14f0f9e2
34 files changed
+899 -43

No files matched your search

+12 -1
View File
@@ -310,6 +310,15 @@ export interface SoundAsset extends AssetBase {
file: string;
}
/**
* What a sound is played through (#311): the graph runs from its `Input` over effect nodes to its
* `Output`, and a `Play sound` naming it sends the voice along that line (see `core/soundfx`).
*/
export interface SoundFxAsset extends AssetBase {
type: 'soundfx';
graph: Graph;
}
/**
* A tile a tilemap paints with: `pictures` names one block per animation step (a texture id for a
* sheet that holds nothing else, a frame id for one block cut out of a sheet), and `props` say
@@ -562,6 +571,7 @@ export type Asset =
| ShaderFunctionAsset
| FontAsset
| SoundAsset
| SoundFxAsset
| ParticlesAsset;
/** The assets whose `variables` are declared by the user – see `requireVariableOwner`. */
@@ -573,7 +583,7 @@ export function isVariableOwner(asset: Asset | undefined): asset is VariableOwne
/** The assets edited as a node graph – what the graph canvas and the graph commands work on. */
export type GraphAsset =
BehaviorAsset | StateMachineAsset | ShaderAsset | ShaderFunctionAsset | ParticlesAsset;
BehaviorAsset | StateMachineAsset | ShaderAsset | ShaderFunctionAsset | ParticlesAsset | SoundFxAsset;
const GRAPH_TYPES: ReadonlySet<AssetType> = new Set([
'behavior',
@@ -581,6 +591,7 @@ const GRAPH_TYPES: ReadonlySet<AssetType> = new Set([
'shader',
'shaderfunction',
'particles',
'soundfx',
]);
export function isGraphAsset(asset: Asset): asset is GraphAsset {
+6 -1
View File
@@ -24,6 +24,7 @@ export const ASSET_TYPES = [
'shaderfunction',
'font',
'sound',
'soundfx',
'particles',
] as const;
@@ -55,6 +56,7 @@ export const ASSET_TYPE_LABELS: Record<AssetType, string> = {
shaderfunction: 'Shader function',
font: 'Font',
sound: 'Sound',
soundfx: 'Sound FX',
particles: 'Particles',
};
@@ -66,7 +68,8 @@ export const ASSET_TYPE_LABELS: Record<AssetType, string> = {
* what a tilemap draws with, so it is placed no more directly than a texture is – what is placed
* is the tilemap. A curve is the way a key or a `Lerp` blends, picked by id like a state machine.
* A struct is a shape that variables and fields take on (#253), nothing that exists on its own.
* A shader function is part of other shaders (#303), worn by nothing itself.
* A shader function is part of other shaders (#303), worn by nothing itself. A sound FX is what a
* sound is played through (#311), heard but never seen.
*/
export type PlaceableAssetType = Exclude<
AssetType,
@@ -83,6 +86,7 @@ export type PlaceableAssetType = Exclude<
| 'shaderfunction'
| 'font'
| 'sound'
| 'soundfx'
| 'variables'
| 'struct'
>;
@@ -101,6 +105,7 @@ const NOT_PLACEABLE: ReadonlySet<AssetType> = new Set([
'shaderfunction',
'font',
'sound',
'soundfx',
'variables',
'struct',
]);