Audio Manager
AudioManager is the shared audio facade for Hs2d games. It owns registered audio resources, music state, active SFX tracking, looped SFX, volume multipliers, and background behavior. Scenes should call the manager; platform-specific code should create and register GameAudioResource objects.
Source References
| Source | What to read there |
|---|---|
../games/hosanna-ui/src/hosanna-game/audio/AudioManager.ts | Core API: resource registry, SFX, looped SFX, music, backgrounding, volumes, diagnostics. |
../games/hosanna-ui/src/hosanna-game/audio/roku/RokuGameAudioManager.ts | Roku registration, URI fallback order, stream counts, SFX trigger budget, and roAudioPlayer music wrapper. |
../games/hosanna-ui/src/hosanna-game/audio/web/WebGameAudioManager.ts | Web registration from descriptor arrays and ready manifests. |
../games/hosanna-ui/src/hosanna-game/runtime/GameRuntime.ts | Runtime _tick(deltaMs) integration for audio timing. |
../hosanna-ui-game-samples-public/src/hosanna-game-examples/ExampleGamesController.ts | Example shell audio lifecycle: set runtime audio manager, clear context audio, register bundle resources. |
../hosanna-ui-game-samples-public/src/hosanna-game-examples/native-shoot-em-up/VerticalShooterLevelScene.ts | Music, SFX, looped hyperspace SFX, diagnostics snapshot, and scene cleanup. |
../hosanna-ui-game-samples-public/src/hosanna-game-examples/native-shoot-em-up/VerticalShooterPauseScene.ts | Pause overlay backgrounding pattern. |
../hosanna-ui-game-samples-public/src/hosanna-game-examples/diagnostics/SoundRigController.ts | Diagnostic UI that exercises music, pause/resume, SFX, and stress playback. |
Resource Contract
AudioManager stores resources by string key. A resource can implement any subset of the optional methods:
export interface GameAudioResource {
Trigger(volume?: number, streamIndex?: number): boolean;
GetMaxStreams?(): number;
GetDurationMs?(): number;
PlayLoop?(volume?: number): boolean;
Pause?(): boolean;
Resume?(): boolean;
Stop?(): void;
IsPlaying?(): boolean;
}
| Method | Used for |
|---|---|
Trigger | Short SFX and fallback music playback. Required. |
GetMaxStreams | Diagnostics and stream capacity totals. |
GetDurationMs | Active SFX tracking and loop-repeat default. |
PlayLoop | Preferred music playback. |
Pause / Resume | Music pause and backgrounding. |
Stop | Music stop and optional SFX stop. |
IsPlaying | Web autoplay self-heal when an initial music start reported success but did not actually play. |
Register directly only when platform setup code already has a resource:
audioManager.setResource('my-game.audio.hit', hitResource);
const hit = audioManager.getResource('my-game.audio.hit');
Gameplay code should normally rely on bundle registration.
Bundle Registration
Audio resources come from ready asset bundle manifests.
| Platform | Helper | Notes |
|---|---|---|
| Roku | createRokuGameAudioManager(assetBundles) | Creates an AudioManager with a Roku timer-backed nowMs, then registers ready bundle audio. |
| Roku | registerRokuGameAudioBundleResources(assetBundles, audioManager) | Scans ready manifest readyKeys, creates Roku resources, and adds menu aliases. |
| Web | registerWebGameAudioBundleResources(assetBundles, audioManager, createRoObject) | Scans descriptor arrays or ready manifests and creates web audio resources through the provided factory. |
Registration accepts .wav, .mp3, and .m4a assets. Music keys get one stream. SFX get multiple streams: Roku uses 8 streams and web uses 2 streams.
Roku URI resolution prefers:
asset.pathfrom the ready manifest.- A packaged URI derived from
asset.urlwhen it contains/asset-bundles/. pkg:/asset-bundles/<bundleId>/<asset.fileName>.
Web URI resolution prefers:
asset.path.- Absolute
http://orhttps://asset.url. pkg:/asset-bundles/<bundleId>/<asset.fileName>.- Raw
asset.url.
The packaged fallback avoids a web dev server returning HTML for missing local audio and avoids Roku audio creation silently failing on a non-package path.
// Roku: createRokuGameAudioManager already registers ready bundle resources.
const audioManager = createRokuGameAudioManager(assetBundles);
// Use registerRokuGameAudioBundleResources only when adding resources to an
// existing AudioManager after bundle state changes.
registerRokuGameAudioBundleResources(nextAssetBundles, audioManager);
// Web: register resources with a platform-specific resource factory.
registerWebGameAudioBundleResources(
assetBundles,
webAudioManager,
(objectType, uri, streams) => CreateObject(objectType, uri, streams),
);
The examples shell calls its registerAudioResources(assetBundles, audioManager) hook whenever it builds a new GameExampleContext.
Runtime Tick
GameRuntime calls:
audioManager._tick(deltaMs);
This service tick happens before gameplay FPS throttling and uses every raw host delta, including a host tick that does not produce a gameplay frame. It is required for looped SFX timing. Do not create a second audio tick in game scenes. Pass the same audio manager to the runtime and to scenes that need direct audio controls.
SFX
Use playSfx for short sounds:
const didPlay = audioManager.playSfx('my-game.audio.hit', 0.75, 1);
if (!didPlay) {
// Optional gameplay fallback, telemetry, or silence.
}
| API | Behavior |
|---|---|
playSfx(name, volume = 1, streamIndex = 0) | Triggers a registered resource after applying the SFX volume multiplier. |
stopSfx(name) | Calls Stop when the resource exposes it. |
getLastPlayedSfx() | Returns the last successfully triggered SFX key. |
getSfxMaxStreams(name) | Reads one resource's stream capacity, returning 0 on missing/failed probes. |
Missing resources warn once per key and return false. This keeps registration gaps visible without flooding logs every frame.
Use stream indexes to spread repeated sounds across available streams:
this.context.audioManager.playSfx(
NATIVE_SHOOT_EM_UP_ASSETS.shootSoundKey,
0.32,
projectile.index % 2,
);
On Roku, SFX resources are wrapped with a small trigger budget so accidental rapid-fire loops do not overwhelm device audio.
Looped SFX
Looped SFX repeatedly trigger a short SFX resource. They are useful for effects such as thrust, hyperspace, charge, or engine rumble when the underlying resource is not true streaming music.
private hyperspaceSfxHandle: number | undefined;
private startHyperspaceMode(): void {
this.stopHyperspaceSfx();
this.hyperspaceSfxHandle = this.context.audioManager.playLoopedSfx(
NATIVE_SHOOT_EM_UP_ASSETS.hyperspaceSoundKey,
0.48,
2,
3000,
360,
);
}
private stopHyperspaceSfx(): void {
if (this.hyperspaceSfxHandle === undefined) return;
this.context.audioManager.stopLoopedSfx(this.hyperspaceSfxHandle);
this.hyperspaceSfxHandle = undefined;
}
| API | Behavior |
|---|---|
playLoopedSfx(name, volume, streamIndex, durationMs, repeatMs) | Triggers immediately, then returns a handle for repeat playback. Returns undefined if the initial trigger fails. |
stopLoopedSfx(handle) | Stops one looped SFX entry. |
clearAllLoopedSfx() | Removes all looped SFX entries. |
durationMs = -1 means run until stopped. repeatMs = 0 means use the resource duration or the default SFX duration. Repeats are clamped to at least 35 ms.
Always store the handle and stop it when the source action ends, the scene exits, the player dies, or the level restarts.
Music
Use music APIs for long-running tracks:
audioManager.playMusic('my-game.audio.music', 0.35);
audioManager.pauseMusic();
audioManager.playMusic('my-game.audio.music', 0.35); // resumes if paused
audioManager.stopMusic();
| API | Behavior |
|---|---|
playMusic(name, volume = 0.35) | Stops current music, then calls PlayLoop or Trigger. If the same music is paused, it tries Resume. |
pauseMusic() | Calls Pause on current music and records pausedMusic only when pause succeeds. |
stopMusic() | Calls Stop on current music when possible and clears music/background state. |
getCurrentMusic() | Returns the current music key. |
getPausedMusic() | Returns the paused music key. |
playMusic also self-heals stalled web starts. If the resource implements IsPlaying and reports that the current track is not actually playing, the manager clears currentMusic and tries to start it again. This handles browser autoplay rejection after a user gesture later makes playback legal.
The Hs2dLevelScene.toggleMusic(musicKey, volume) helper uses getCurrentMusic, getPausedMusic, pauseMusic, and playMusic to implement a simple play/pause toggle.
Backgrounding
Pause overlays and app background events should call setBackgrounded:
override onEnter(game: Hs2dGame): void {
super.onEnter(game);
this.options.audioManager?.setBackgrounded(true);
}
override onExit(game: Hs2dGame): void {
this.options.audioManager?.setBackgrounded(false);
super.onExit(game);
}
Background mode pauses current music only when the manager sees music playing and not already user-paused. When foregrounded, it resumes only the music that this background call paused. User-paused music stays paused.
This is the vertical shooter pause pattern: the level owns music playback, and the pause overlay owns temporary backgrounding.
Volumes
Use local call volume for tuning a specific sound and global multipliers for settings:
audioManager.setMusicVolumeMultiplier(settings.musicVolume);
audioManager.setSfxVolumeMultiplier(settings.sfxVolume);
audioManager.playMusic('my-game.audio.music', 0.28);
audioManager.playSfx('my-game.audio.explosion', 0.55, 0);
Volume multipliers clamp to 0..1. Changing the music multiplier restarts the currently playing music at the new effective volume unless that music is paused.
Roku SFX volume uses a perceptual square-root transform before calling roAudioResource.Trigger, because roAudioPlayer music has no equivalent volume API and otherwise buries moderate SFX levels.
Diagnostics
Use these APIs for HUDs and diagnostic rigs:
const playback = audioManager.getPlaybackSnapshot();
const label =
`SFX ${playback.activeSfx}/${playback.maxSfx} ` +
`last ${playback.lastPlayedSfx ?? '-'}`;
| API | Use |
|---|---|
getPlaybackSnapshot() | Returns { activeSfx, maxSfx, lastPlayedSfx }. Active SFX includes looped SFX. |
getActiveSfxCount() | Count active transient and looped SFX. |
getMaxSfxStreams() | Sum GetMaxStreams across registered resources, cached after first read. |
getLastPlayedSfx() | Last successfully played SFX key. |
getSfxMaxStreams(name) | One resource's max stream count. |
isBackgrounded() | Whether the manager is in background mode. |
The vertical shooter HUD includes active/max SFX counts from getPlaybackSnapshot. The sound rig uses the same manager to test music, pause/resume, SFX, stress playback, and failure states.
Cleanup
Use clear() when leaving a game context:
audioManager.clear();
clear() stops music, clears looped SFX, clears active SFX tracking, and resets the last SFX marker. Individual scenes should stop scene-owned looped SFX in dispose() and stop music only when they own the current music key:
override dispose(): void {
this.stopHyperspaceSfx();
if (this.context.audioManager.getCurrentMusic() === NATIVE_SHOOT_EM_UP_ASSETS.musicSoundKey) {
this.context.audioManager.stopMusic();
}
}
Do And Don't
| Do | Don't |
|---|---|
| Register audio from ready asset bundle manifests. | Create audio resources in gameplay update loops. |
Check playSfx and playMusic return values when failure matters. | Assume missing audio fails loudly every frame. It warns once per key. |
| Store looped SFX handles and stop them on lifecycle changes. | Fire looped SFX without a handle you can later clear. |
Let pause overlays call setBackgrounded(true/false). | Let pause overlays permanently stop level-owned music. |
| Use volume multipliers for user settings. | Bake settings directly into every SFX volume constant. |
| Use diagnostics snapshots in HUDs and rigs. | Probe platform audio resources directly from gameplay code. |