Files
Cozypaw-Hospital/docs/superpowers/specs/2026-05-10-sprint-19-audio-manager.md
T
2026-05-10 00:39:40 +02:00

7.0 KiB
Raw Blame History

Sprint 19 — AudioManager & SFX Design Spec

Goal

Implement an AudioManager autoload that provides background music (per floor, with cross-fade) and SFX feedback for all player interactions. Real audio assets sourced from freesound.org (CC0/CC-BY). All 7 SFX events and 4 music tracks wired up by end of sprint.

Architecture

AudioManager (Autoload)

scripts/autoload/AudioManager.gd — Node with three child AudioStreamPlayers:

AudioManager (Node)
├── MusicA (AudioStreamPlayer)    # Cross-fade ping-pong player A
├── MusicB (AudioStreamPlayer)    # Cross-fade ping-pong player B
└── SfxPlayer (AudioStreamPlayer) # Single SFX player (no polyphony this sprint)

Startup: reads GameState.music_volume and GameState.sfx_volume, applies to players. Connects to GameState.state_changed.

_on_game_state_changed(): reads GameState.current_room, calls _derive_floor_from_room(), compares to _current_floor. If floor changed → play_floor_music(new_floor).

play_floor_music(floor: int) -> void: public API. If floor == _current_floor → no-op. Otherwise: inactive player loads new stream, tweens volume from 0 → GameState.music_volume over 0.8 s; active player tweens volume → 0 over 0.8 s, then stops. Players swap active/inactive roles.

play_sfx(event: String) -> void: looks up event key in _SFX_MAP: Dictionary, loads stream, plays on SfxPlayer. Volume = GameState.sfx_volume.

set_music_volume(vol: float) -> void and set_sfx_volume(vol: float) -> void: update GameState and apply immediately to active players.

get_current_floor() -> int: returns _current_floor (-1 if none set).

Internal State

var _current_floor: int = -1
var _active_player: AudioStreamPlayer  # points to MusicA or MusicB

Floor Derivation

_derive_floor_from_room(room: String) -> int — pure function, no side effects:

Room strings Floor
"reception", "giftshop", "restaurant", "emergency" 0
"xray", "pharmacy", "lab", "patient_rooms" 1
"ultrasound", "delivery_room", "nursery" 2
"garden_party" 3
anything else -1

SFX Event Mapping

Event key Trigger location When
chest_tap room_chest.gd Chest tapped, before spawn
item_spawn room_chest.gd Item tween starts (fly-out)
item_drag_start holdable_item.gd _on_drag_picked_up
item_drop_hand holdable_item.gd Item attached to hand slot
item_drop_outfit outfit_item.gd Outfit applied to character
item_return_chest holdable_item.gd _try_return_to_chest succeeds
item_drop_floor holdable_item.gd _on_drag_released — no target found

All calls: AudioManager.play_sfx("event_key") (autoload, one line, no wiring).

Asset Specification

Directory Layout

assets/audio/music/
    floor_0.ogg
    floor_1.ogg
    floor_2.ogg
    floor_3.ogg
assets/audio/sfx/
    chest_tap.ogg
    item_spawn.ogg
    item_drag_start.ogg
    item_drop_hand.ogg
    item_drop_outfit.ogg
    item_return_chest.ogg
    item_drop_floor.ogg

Music — freesound.org Search Terms

File Search terms Character
floor_0.ogg "children hospital happy loop" Heiter, belebte Lobby
floor_1.ogg "calm medical ambient loop" Ruhig, klinisch
floor_2.ogg "nursery lullaby gentle loop" Sanft, Wiegenlied
floor_3.ogg "garden birds outdoor loop" Draußen, Vogelgezwitscher

Requirements: CC0 or CC-BY, loopable (seamless start/end), 3060 s duration, .ogg or convertible.

SFX — freesound.org Search Terms

File Search terms Target duration
chest_tap.ogg "wooden box tap" < 0.3 s
item_spawn.ogg "soft whoosh pop" < 0.5 s
item_drag_start.ogg "soft pickup" < 0.3 s
item_drop_hand.ogg "light click" < 0.2 s
item_drop_outfit.ogg "fabric swoosh" < 0.5 s
item_return_chest.ogg "soft click" < 0.2 s
item_drop_floor.ogg "light thud" < 0.3 s

Requirements: CC0 or CC-BY, child-appropriate, no startling sounds.

SFX Map in Code

const _SFX_MAP: Dictionary = {
    "chest_tap":        "res://assets/audio/sfx/chest_tap.ogg",
    "item_spawn":       "res://assets/audio/sfx/item_spawn.ogg",
    "item_drag_start":  "res://assets/audio/sfx/item_drag_start.ogg",
    "item_drop_hand":   "res://assets/audio/sfx/item_drop_hand.ogg",
    "item_drop_outfit": "res://assets/audio/sfx/item_drop_outfit.ogg",
    "item_return_chest":"res://assets/audio/sfx/item_return_chest.ogg",
    "item_drop_floor":  "res://assets/audio/sfx/item_drop_floor.ogg",
}

Music Map in Code

const _MUSIC_MAP: Dictionary = {
    0: "res://assets/audio/music/floor_0.ogg",
    1: "res://assets/audio/music/floor_1.ogg",
    2: "res://assets/audio/music/floor_2.ogg",
    3: "res://assets/audio/music/floor_3.ogg",
}

Integration with Existing Scripts

room_chest.gd

Add two AudioManager.play_sfx() calls:

  1. In the chest tap handler (before spawn guard): AudioManager.play_sfx("chest_tap")
  2. In spawn_items(), at the start of each item's tween: AudioManager.play_sfx("item_spawn")

holdable_item.gd

  • _on_drag_picked_up: add AudioManager.play_sfx("item_drag_start")
  • _try_return_to_chest (on success, before tween): add AudioManager.play_sfx("item_return_chest")
  • _on_drag_released (after hand slot attach): add AudioManager.play_sfx("item_drop_hand")
  • _on_drag_released (no target branch): add AudioManager.play_sfx("item_drop_floor")

outfit_item.gd

  • _on_drag_released (after apply_outfit_item call): add AudioManager.play_sfx("item_drop_outfit")

project.godot

Add AudioManager to the autoload list:

[autoload]
AudioManager="*res://scripts/autoload/AudioManager.gd"

GameState Integration

AudioManager observes GameState.state_changed. No changes to GameState are required — music_volume and sfx_volume are already persisted in v3.

set_music_volume / set_sfx_volume write back to GameState so values persist on save.

Testing

File: test/unit/test_audio_manager.gd

Testable (pure logic, no actual audio):

  • _derive_floor_from_room() — all 12 room strings → correct floor int
  • _derive_floor_from_room("unknown")-1
  • play_floor_music() with same floor → _current_floor unchanged (no-op)
  • get_current_floor() → returns -1 before any music played
  • play_sfx() with unknown key → no crash (guard required)

Not testable (manual on device):

  • Actual audio playback
  • Cross-fade quality and timing
  • Volume balance SFX vs. music
  • Loop seamlessness

Out of Scope

  • Polyphonic SFX (multiple simultaneous sounds)
  • Per-room music (12 tracks) — floor-level is sufficient for this sprint
  • Character voice sounds (Häschen-Schnuffeln, Kätzchen-Miau) — separate sprint
  • Settings UI for volume sliders — volume API exists, UI in a later sprint
  • AudioBus setup (Master/Music/SFX buses) — single bus is sufficient for now