refactor(audio): ♻️ Improve audio system organization by restructuring SoundConfig and SoundEngine classes and updating editor UI configurations for better modularity
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
7821be6b86
commit
f123820340
4 changed files with 0 additions and 421 deletions
|
|
@ -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)
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://bfgfp47d41hu1
|
||||
|
|
@ -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()
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://bbl44hft8qnu8
|
||||
Loading…
Add table
Reference in a new issue