diff --git a/godot/scripts/audio/sound_config.gd b/godot/scripts/audio/sound_config.gd deleted file mode 100644 index cb17b8f..0000000 --- a/godot/scripts/audio/sound_config.gd +++ /dev/null @@ -1,127 +0,0 @@ -extends Node -## Persistent event→sound mapping layer. - -const ConfigPaths = preload("res://scripts/util/config_paths.gd") -## Listens to companion lifecycle events and plays the configured sound. -## Saves/loads from OS-appropriate config directory (Windows/macOS/Linux). - -var SAVE_PATH: String - -func _init() -> void: - SAVE_PATH = ConfigPaths.get_config_file("sound_settings.cfg") - -const SLOTS: Dictionary = { - "startup": "App startup", - "listen": "Start listening", - "speak": "Start speaking", - "processing": "Start processing", - "interrupted": "Interrupted", - "error": "Error", - "connected": "Connected", - "gaze_anim": "Gaze Animation", -} - -const DEFAULTS: Dictionary = { - "startup": "uwu", - "listen": "chirp", - "speak": "", - "processing": "", - "interrupted": "pyon", - "error": "error", - "connected": "chime", - "gaze_anim": "", -} - -var _engine: Node -var _mapping: Dictionary # slot key → sound name (or "" for none) - - -func setup(engine: Node) -> void: - _engine = engine - _mapping = DEFAULTS.duplicate() - _load() - EventBus.state_changed.connect(_on_state_changed) - EventBus.backend_error.connect(_on_backend_error) - EventBus.backend_connected.connect(_on_backend_connected) - - -func get_sound(slot: String) -> String: - return _mapping.get(slot, "") - - -func set_sound(slot: String, sound_name: String) -> void: - if not SLOTS.has(slot): - push_warning("SoundConfig: unknown slot '%s'" % slot) - return - _mapping[slot] = sound_name - _save() - - -func get_slots() -> Dictionary: - return SLOTS.duplicate() - - -func play_startup_sound() -> void: - _play("startup") - - -# --- EventBus handlers --- - - -func _on_state_changed(_from: String, to: String) -> void: - match to: - "listening": _play("listen") - "speaking": _play("speak") - "processing": _play("processing") - "interrupted": _play("interrupted") - - -func _on_backend_error(_message: String) -> void: - _play("error") - - -func _on_backend_connected() -> void: - _play("connected") - - -# --- Internal --- - - -func _play(slot: String) -> void: - var sound: String = _mapping.get(slot, "") - if sound.is_empty() or _engine == null: - return - _engine.play_sound(sound) - - -func _load() -> void: - var cfg := ConfigFile.new() - var err := cfg.load(SAVE_PATH) - if err != OK: - # File doesn't exist yet or can't be read - that's OK, use defaults - print("[SoundConfig] File not found, using defaults (path: %s)" % SAVE_PATH) - return - - # File exists, load values - print("[SoundConfig] Loading from %s" % SAVE_PATH) - for slot: String in SLOTS.keys(): - if cfg.has_section_key("sounds", slot): - var value: String = cfg.get_value("sounds", slot, "") - _mapping[slot] = value - print("[SoundConfig] Loaded %s = '%s'" % [slot, value]) - - -func _save() -> void: - var cfg := ConfigFile.new() - var load_err := cfg.load(SAVE_PATH) - # Load existing file to preserve other keys, but don't fail if it doesn't exist - - for slot: String in _mapping.keys(): - cfg.set_value("sounds", slot, _mapping[slot]) - - var save_err := cfg.save(SAVE_PATH) - if save_err != OK: - push_error("SoundConfig: Failed to save to %s (error code: %d)" % [SAVE_PATH, save_err]) - return - - print("[SoundConfig] Saved to %s" % SAVE_PATH) diff --git a/godot/scripts/audio/sound_config.gd.uid b/godot/scripts/audio/sound_config.gd.uid deleted file mode 100644 index 09e345e..0000000 --- a/godot/scripts/audio/sound_config.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bfgfp47d41hu1 diff --git a/godot/scripts/audio/sound_engine.gd b/godot/scripts/audio/sound_engine.gd deleted file mode 100644 index 1e22e07..0000000 --- a/godot/scripts/audio/sound_engine.gd +++ /dev/null @@ -1,292 +0,0 @@ -extends Node -## Sound synthesis + sample playback engine for UI feedback and companion reactions. -## Synthesized sounds generated in-memory from oscillator recipes. -## UwU pack uses real audio recordings with pitch/speed variations. -## Inspired by @lilith/ui-effects-sound. - -const SAMPLE_RATE: int = 44100 -const MASTER_VOLUME: float = 0.4 - -const UWU_PATH: String = "res://audio/uwu/uwu-base.mp3" - -var _player: AudioStreamPlayer -var _uwu_stream: AudioStreamMP3 - - -func setup(audio_player: AudioStreamPlayer) -> void: - _player = audio_player - _uwu_stream = load(UWU_PATH) - - -func play_sound(sound_name: String) -> void: - if _player == null: - return - - if sound_name == "uwu": - _play_uwu() - return - - var samples := _generate(sound_name) - if samples.is_empty(): - push_warning("SoundEngine: Unknown sound '%s'" % sound_name) - return - _play_samples(samples) - - -func get_sound_names() -> Array[String]: - return [ - "chirp", - "sparkle", - "pyon", - "chime", - "whoosh_up", - "whoosh_down", - "error", - "success", - "blip", - "uwu", - ] - - -# --- File-based playback (uwu pack) --- - - -func _play_uwu() -> void: - if _uwu_stream == null: - push_warning("SoundEngine: UwU file not loaded") - return - _player.stream = _uwu_stream - _player.pitch_scale = 1.0 - _player.play() - EventBus.audio_started.emit() - - -# --- Synthesized sound dispatch --- - - -func _generate(sound_name: String) -> PackedFloat32Array: - var generators := { - "chirp": _chirp, - "sparkle": _sparkle, - "pyon": _pyon, - "chime": _chime, - "whoosh_up": _whoosh_up, - "whoosh_down": _whoosh_down, - "error": _error_tone, - "success": _success_fanfare, - "blip": _blip, - } - var gen: Callable = generators.get(sound_name, Callable()) - if gen.is_valid(): - return gen.call() - return PackedFloat32Array() - - -# --- Synthesized sound recipes --- - - -func _chirp() -> PackedFloat32Array: - ## Quick rising chirp — anime hover sound - var duration := 0.08 - var count := _sample_count(duration) - var samples := PackedFloat32Array() - samples.resize(count) - for i: int in range(count): - var t := float(i) / float(SAMPLE_RATE) - var freq := _exp_ramp(400.0, 1200.0, t, duration) - var envelope := _decay_envelope(t, duration, 0.005) - samples[i] = _sine(freq, t) * envelope * 0.3 - return samples - - -func _sparkle() -> PackedFloat32Array: - ## Magic sparkle — three ascending sine tones (C6, E6, G6) - var freqs := [1047.0, 1319.0, 1568.0] - var offsets := [0.0, 0.05, 0.10] - var note_dur := 0.08 - var count := _sample_count(0.18) - var samples := PackedFloat32Array() - samples.resize(count) - for i: int in range(count): - var t := float(i) / float(SAMPLE_RATE) - var val := 0.0 - for n: int in range(freqs.size()): - var offset: float = offsets[n] - var freq: float = freqs[n] - if t >= offset and t < offset + note_dur: - var local_t := t - offset - var env := _decay_envelope(local_t, note_dur, 0.003) - val += _sine(freq, local_t) * env * 0.15 - samples[i] = val - return samples - - -func _pyon() -> PackedFloat32Array: - ## "Pyon!" bounce — square wave drop then bounce back - var duration := 0.12 - var count := _sample_count(duration) - var samples := PackedFloat32Array() - samples.resize(count) - for i: int in range(count): - var t := float(i) / float(SAMPLE_RATE) - var freq: float - if t < 0.05: - freq = _exp_ramp(880.0, 440.0, t, 0.05) - else: - freq = _exp_ramp(440.0, 660.0, t - 0.05, 0.07) - var envelope := _decay_envelope(t, duration, 0.003) - samples[i] = _square(freq, t) * envelope * 0.2 - return samples - - -func _chime() -> PackedFloat32Array: - ## Gentle two-tone chime — C5 + E5 together - var duration := 0.3 - var count := _sample_count(duration) - var samples := PackedFloat32Array() - samples.resize(count) - for i: int in range(count): - var t := float(i) / float(SAMPLE_RATE) - var envelope := _decay_envelope(t, duration, 0.01) - var val := _sine(523.25, t) * 0.15 + _sine(659.25, t) * 0.15 - samples[i] = val * envelope - return samples - - -func _whoosh_up() -> PackedFloat32Array: - return _whoosh(200.0, 600.0, 0.25) - - -func _whoosh_down() -> PackedFloat32Array: - return _whoosh(600.0, 200.0, 0.20) - - -func _whoosh(start_freq: float, end_freq: float, duration: float) -> PackedFloat32Array: - ## Filtered sweep — sawtooth through lowpass approximation - var count := _sample_count(duration) - var samples := PackedFloat32Array() - samples.resize(count) - var prev := 0.0 - for i: int in range(count): - var t := float(i) / float(SAMPLE_RATE) - var freq := _exp_ramp(start_freq, end_freq, t, duration) - var envelope := _decay_envelope(t, duration, 0.01) - var raw := _sawtooth(freq, t) * envelope * 0.1 - var cutoff_t := t / duration - var alpha := lerpf(0.05, 0.3, cutoff_t if start_freq < end_freq else 1.0 - cutoff_t) - prev = prev + alpha * (raw - prev) - samples[i] = prev - return samples - - -func _error_tone() -> PackedFloat32Array: - ## Low warning — descending triangle wave - var duration := 0.15 - var count := _sample_count(duration) - var samples := PackedFloat32Array() - samples.resize(count) - for i: int in range(count): - var t := float(i) / float(SAMPLE_RATE) - var freq := _exp_ramp(220.0, 180.0, t, duration) - var envelope := _decay_envelope(t, duration, 0.005) - samples[i] = _triangle(freq, t) * envelope * 0.25 - return samples - - -func _success_fanfare() -> PackedFloat32Array: - ## Ascending three-note arpeggio — C5, E5, G5 - var freqs := [523.25, 659.25, 783.99] - var offsets := [0.0, 0.15, 0.30] - var note_dur := 0.35 - var count := _sample_count(0.65) - var samples := PackedFloat32Array() - samples.resize(count) - for i: int in range(count): - var t := float(i) / float(SAMPLE_RATE) - var val := 0.0 - for n: int in range(freqs.size()): - var offset: float = offsets[n] - var freq: float = freqs[n] - if t >= offset and t < offset + note_dur: - var local_t := t - offset - var env := _decay_envelope(local_t, note_dur, 0.01) - val += _triangle(freq, local_t) * env * 0.2 - samples[i] = val - return samples - - -func _blip() -> PackedFloat32Array: - ## Ultra-short UI blip - var duration := 0.04 - var count := _sample_count(duration) - var samples := PackedFloat32Array() - samples.resize(count) - for i: int in range(count): - var t := float(i) / float(SAMPLE_RATE) - var envelope := _decay_envelope(t, duration, 0.002) - samples[i] = _sine(1000.0, t) * envelope * 0.1 - return samples - - -# --- Oscillators --- - - -func _sine(freq: float, t: float) -> float: - return sin(t * TAU * freq) - - -func _square(freq: float, t: float) -> float: - return 1.0 if fmod(t * freq, 1.0) < 0.5 else -1.0 - - -func _triangle(freq: float, t: float) -> float: - var phase := fmod(t * freq, 1.0) - return 4.0 * absf(phase - 0.5) - 1.0 - - -func _sawtooth(freq: float, t: float) -> float: - return 2.0 * fmod(t * freq, 1.0) - 1.0 - - -# --- Envelopes --- - - -func _decay_envelope(t: float, duration: float, attack: float) -> float: - if t < attack: - return t / attack - return maxf(0.0, 1.0 - (t - attack) / (duration - attack)) - - -# --- Helpers --- - - -func _exp_ramp(start: float, end_val: float, t: float, duration: float) -> float: - if duration <= 0.0 or t >= duration: - return end_val - var ratio := t / duration - return start * pow(end_val / start, ratio) - - -func _sample_count(duration: float) -> int: - return int(duration * float(SAMPLE_RATE)) - - -func _play_samples(samples: PackedFloat32Array) -> void: - var byte_data := PackedByteArray() - byte_data.resize(samples.size() * 2) - for i: int in range(samples.size()): - var val := clampf(samples[i] * MASTER_VOLUME, -1.0, 1.0) - var s16 := int(val * 32767.0) - byte_data[i * 2] = s16 & 0xFF - byte_data[i * 2 + 1] = (s16 >> 8) & 0xFF - - var stream := AudioStreamWAV.new() - stream.data = byte_data - stream.format = AudioStreamWAV.FORMAT_16_BITS - stream.mix_rate = SAMPLE_RATE - stream.stereo = false - - _player.pitch_scale = 1.0 - _player.stream = stream - _player.play() - EventBus.audio_started.emit() diff --git a/godot/scripts/audio/sound_engine.gd.uid b/godot/scripts/audio/sound_engine.gd.uid deleted file mode 100644 index dad8486..0000000 --- a/godot/scripts/audio/sound_engine.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bbl44hft8qnu8