A game that stays silent is only half a game, so a sound is an asset like any other (#45)
game-maker / suite (push) Successful in 12m1s

Sounds follow the road fonts already paved: the file goes to the server
content-addressed under its sha256, the project keeps a named asset that
points at it, and nothing is ever re-encoded. The format is read from the
file's own signature, not from what the browser claims - the same .m4a
arrives as audio/mp4, audio/x-m4a or as nothing at all.

Playing is two nodes. "Play sound" takes the asset and may raise or lower
its volume; every call is its own voice, so two jumps in a row are two
jumps heard. Its loop input is a choice of three - from the asset, on,
off - because a checkbox has to spend one of its two positions on "not
set" and would lose a case: an asset that loops could then never be
played once.

Below the graph core stays browser-free: NodeContext hands out an asset
id, BehaviorSystem resolves it against the project and folds the asset's
own volume and loop into the call, and only viewport/SoundLibrary knows
about Web Audio. A run without a sink plays nothing instead of throwing.

The CI browser is muted, so the ear is no witness: the debug hook counts
started and stopped voices, and step 34 reads that counter.
This commit is contained in:
admin committed 2026-09-15 07:54:28 +02:00
1 parent 7babf88636
commit 5671a38044
47 files changed
+2066 -42

No files matched your search

+11
View File
@@ -129,6 +129,16 @@ export interface FontAsset extends AssetBase {
file: string;
}
/**
* `file` names the sound in `data/sounds/` the way a font names its typeface: content-addressed
* `{sha256}.{ext}`, empty when the upload failed. Nothing wears a sound – a graph plays it with
* `Play sound`, and `props` says how loud and whether it repeats (see `properties/sounds.ts`).
*/
export interface SoundAsset extends AssetBase {
type: 'sound';
file: string;
}
/**
* The graph is the motion of one particle: every output of its `Output` node is a function of the
* particle's age and its own random number (see `core/particles/compile.ts`). A placed object is
@@ -153,6 +163,7 @@ export type Asset =
| StateMachineAsset
| ShaderAsset
| FontAsset
| SoundAsset
| ParticlesAsset;
/** The assets edited as a node graph – what the graph canvas and the graph commands work on. */
+6 -2
View File
@@ -12,6 +12,7 @@ export const ASSET_TYPES = [
'statemachine',
'shader',
'font',
'sound',
'particles',
] as const;
@@ -31,17 +32,19 @@ export const ASSET_TYPE_LABELS: Record<AssetType, string> = {
statemachine: 'State machine',
shader: 'Shader',
font: 'Font',
sound: 'Sound',
particles: 'Particles',
};
/**
* Textures and animations are not placeable: they are pictures a sprite wears through `texture`,
* as a shader is worn through `shader` and a font through `font`. A state machine is not either:
* a behavior refers to it by id, and a Variables asset is declarations a graph reads by id.
* a behavior refers to it by id, and a Variables asset is declarations a graph reads by id. A
* sound is nowhere on screen at all – a graph plays it, so there is nothing to place.
*/
export type PlaceableAssetType = Exclude<
AssetType,
'scene' | 'texture' | 'animation' | 'statemachine' | 'shader' | 'font' | 'variables'
'scene' | 'texture' | 'animation' | 'statemachine' | 'shader' | 'font' | 'sound' | 'variables'
>;
const NOT_PLACEABLE: ReadonlySet<AssetType> = new Set([
@@ -51,6 +54,7 @@ const NOT_PLACEABLE: ReadonlySet<AssetType> = new Set([
'statemachine',
'shader',
'font',
'sound',
'variables',
]);
+20
View File
@@ -0,0 +1,20 @@
import type { SoundAsset } from './Asset';
import { findAsset, type Project } from './Project';
/** Mirrors `SoundStore::isValidName` on the server, where the name is derived from the bytes. */
const FILE_PATTERN = /^[a-f0-9]{64}\.(mp3|ogg|wav|m4a)$/;
export function isSoundFile(name: string): boolean {
return FILE_PATTERN.test(name);
}
/** The file a `sound` value points at, empty when the id is unset or no longer resolves. */
export function soundFileOf(project: Project, id: string): string {
if (id === '') return '';
const asset = findAsset(project, id);
return asset?.type === 'sound' ? asset.file : '';
}
export function isSoundAsset(asset: { type: string }): asset is SoundAsset {
return asset.type === 'sound';
}