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
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:
1 parent
7babf88636
commit
5671a38044
47 files changed
+2066
-42
No files matched your search
@@ -7,9 +7,11 @@ import { effectiveScale, gameClock } from '../core/time';
|
||||
import { activeDrag, Button, isRendering, renderCount, requestRender, ui, Widget } from '../ui';
|
||||
import { particleLibrary } from '../viewport/ParticleLibrary';
|
||||
import { shaderLibrary } from '../viewport/ShaderLibrary';
|
||||
import { soundLibrary } from '../viewport/SoundLibrary';
|
||||
import { textureLibrary } from '../viewport/TextureLibrary';
|
||||
import type { CentreTab, Editor } from './Editor';
|
||||
import { currentFontPicker } from './font/FontPicker';
|
||||
import { currentSoundPicker } from './sound/SoundPicker';
|
||||
import { currentImagePicker } from './texture/ImagePicker';
|
||||
import type { EditorState } from './state';
|
||||
|
||||
@@ -361,6 +363,22 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
/** The same for the font picker, whose rows are the preview. */
|
||||
fontPicker: () => currentFontPicker()?.debugInfo() ?? null,
|
||||
fontPickerRow: (file: string) => currentFontPicker()?.rowGlobal(file) ?? null,
|
||||
/** And for the sound picker, whose rows carry a ▶ of their own. */
|
||||
soundPicker: () => currentSoundPicker()?.debugInfo() ?? null,
|
||||
soundPickerRow: (file: string) => currentSoundPicker()?.rowGlobal(file) ?? null,
|
||||
soundPickerPlay: (file: string) => currentSoundPicker()?.playGlobal(file) ?? null,
|
||||
/**
|
||||
* Voices started, stopped and in flight. CI runs the browser with `--mute-audio`, so nothing
|
||||
* is ever heard there: these counters are the only way a check can see that a graph played
|
||||
* anything at all, the way `renderStats` is for a repaint that did not happen.
|
||||
*/
|
||||
sound: {
|
||||
stats: () => soundLibrary.stats(),
|
||||
duration: (file: string) => soundLibrary.duration(file),
|
||||
/** Decodes ahead of a check, so a play can be asserted without racing the network. */
|
||||
load: (file: string) => soundLibrary.load(file).then((buffer) => buffer?.duration ?? null),
|
||||
stopAll: () => soundLibrary.stopAll(),
|
||||
},
|
||||
/** Which typeface a text ended up drawn in, and how wide it made it. */
|
||||
textInfo: (id: string) => viewport.textInfo(id),
|
||||
gameTextInfo: (id: string) => gameView.textInfo(id),
|
||||
|
||||
Reference in new issue
Block a user