//! Audio-Treiber auf tinyaudio — Frontend-Gegenstück zur headless //! Emitter-Logik in engine::audio (wie `Gpu` zu `scene`: die Engine kennt //! dieses Modul nie). //! //! Eingebettete Fassung des irl3d-Mixers (../irl3d/src/audio.rs), auf die //! WDS-Regeln gebracht: kein Panic — kein Gerät oder fehlende/kaputte //! WAVs sind eine Meldung und Stille, nie fatal ([`Audio::open`] liefert //! `None`, alle Aufrufe darauf entfallen still beim Aufrufer). //! //! Mono, 44100 Hz (fest, siehe engine::wav). Zwei Voice-Klassen: //! - 8 SFX-Voices: einmal durchlaufen, dann geräumt; sind alle belegt, //! wird die am weitesten gespielte verdrängt (`play`). //! - Ambient-Voices: eine je Emitter, loopen immer; ihr Gain ist die //! Distance ramp, die das Frontend jeden Frame aus //! `Session::emitter_gains` zieht ([`Audio::set_ambient_gains`]). //! //! Gemischt wird im Callback-Thread in f32 über alle aktiven Voices //! (Sample × Gain), am Ende auf [-1, 1] geklemmt. Der Mixer hängt hinter //! einem Mutex; Frontend-Seite fasst ihn nur kurz pro Frame/SFX an. use std::sync::{Arc, Mutex}; use tinyaudio::{run_output_device, OutputDevice, OutputDeviceParameters}; use crate::engine::wav; const SFX_VOICES: usize = 8; /// Callback-Puffergröße in Samples (~6 ms bei 44,1 kHz). const BUFFER_LEN: usize = 256; /// Dekodierte Mono-Samples, geteilt zwischen Cache und laufenden Voices. #[derive(Clone)] pub struct Sound { samples: Arc>, } impl Sound { pub fn new(w: wav::Wav) -> Self { Sound { samples: Arc::new(w.samples) } } } struct Voice { samples: Arc>, cursor: usize, gain: f32, } #[derive(Default)] struct Mixer { sfx: Vec>, /// Loop-Voices der Ambient-Emitter, Index = Emitter-Reihenfolge. ambient: Vec, } impl Mixer { fn mix(&mut self, out: &mut [f32]) { for s in out.iter_mut() { let mut acc = 0.0f32; for slot in self.sfx.iter_mut() { if let Some(v) = slot { acc += v.samples[v.cursor] as f32 * v.gain; v.cursor += 1; if v.cursor >= v.samples.len() { *slot = None; } } } for v in self.ambient.iter_mut() { // Auch bei Gain 0 weiterlaufen lassen: ein Emitter, in dessen // Radius man zurückkehrt, spielt mitten in seiner Schleife — // nicht jedes Mal von vorn. acc += v.samples[v.cursor] as f32 * v.gain; v.cursor = (v.cursor + 1) % v.samples.len(); } *s = (acc / 32768.0).clamp(-1.0, 1.0); } } } pub struct Audio { mixer: Arc>, _device: OutputDevice, // lebt, solange Audio lebt } impl Audio { /// Ausgabegerät öffnen. `None`, wenn keins da ist (Meldung, Spiel läuft /// stumm weiter). pub fn open() -> Option { // pipewire-pulse/PulseAudio respektiert diese Variable als // Ziel-Latenz des Streams (Default ~50–200 ms) — herabsetzen für // snappige SFX. Muss vor `run_output_device` gesetzt sein; auf // Windows/macOS ignoriert. // SAFETY: einmalig beim Start, bevor weitere Threads laufen (wird // in render::run vor dem stdin-Thread gerufen). unsafe { std::env::set_var("PULSE_LATENCY_MSEC", "15"); } let mixer = Arc::new(Mutex::new(Mixer { sfx: (0..SFX_VOICES).map(|_| None).collect(), ambient: Vec::new(), })); let cb_mixer = Arc::clone(&mixer); let device = run_output_device( OutputDeviceParameters { channels_count: 1, sample_rate: wav::SAMPLE_RATE as usize, channel_sample_count: BUFFER_LEN, }, move |buf: &mut [f32]| { cb_mixer.lock().unwrap().mix(buf); }, ); match device { Ok(device) => Some(Audio { mixer, _device: device }), Err(e) => { eprintln!("[audio] kein Ausgabegerät ({e}) — Spiel läuft stumm"); None } } } /// Einmal-SFX starten: freier Slot, sonst wird die am weitesten /// gespielte Voice verdrängt. pub fn play(&self, s: &Sound) { if s.samples.is_empty() { return; } let mut m = self.mixer.lock().unwrap(); let target = m.sfx.iter().enumerate() .max_by_key(|(_, slot)| slot.as_ref().map_or(usize::MAX, |v| v.cursor)) .map_or(0, |(i, _)| i); m.sfx[target] = Some(Voice { samples: Arc::clone(&s.samples), cursor: 0, gain: 1.0, }); } /// Die Ambient-Loop-Voices anlegen — einmal beim Start, Reihenfolge = /// `Session::emitters` (die Gains kommen dann parallel dazu, deshalb /// bekommt auch ein nicht ladbarer Sound seine — stille — Voice). pub fn set_ambients(&self, sounds: &[Option]) { let mut m = self.mixer.lock().unwrap(); m.ambient = sounds.iter().map(|s| Voice { samples: s.as_ref().map_or_else(|| Arc::new(vec![0]), |s| Arc::clone(&s.samples)), cursor: 0, gain: 0.0, }).collect(); } /// Distance-ramp-Gains auf die Ambient-Voices legen — jeden Frame, aus /// `Session::emitter_gains`. pub fn set_ambient_gains(&self, gains: &[f32]) { let mut m = self.mixer.lock().unwrap(); for (v, g) in m.ambient.iter_mut().zip(gains) { v.gain = *g; } } }