Desktop build plays music and sound through the system sound output; desktop build documented
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
db521ed2ee
commit
2475a8ca3c
5 files changed
+682
-5
No files matched your search
+105
-1
@@ -1,6 +1,7 @@
|
||||
// Audio engine: music (XM players with crossfades) and synthesised sound effects.
|
||||
// The browser pulls samples through dc_audio_render() from our JS plugin (www/dc_plugin.js),
|
||||
// which starts the AudioContext on the first user interaction.
|
||||
// which starts the AudioContext on the first user interaction. Desktop builds play through
|
||||
// the system's sound output (module `native` below).
|
||||
|
||||
use crate::sfx;
|
||||
use crate::tracks::TRACKS;
|
||||
@@ -244,6 +245,109 @@ pub extern "C" fn dc_audio_render(frames: u32) -> *const f32 {
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ desktop sound output
|
||||
|
||||
// The game renders a little ahead into a queue each frame; the sound card's thread takes from it.
|
||||
// The engine itself stays on the game thread, like in the browser.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod native {
|
||||
use super::STATE;
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
// seconds rendered ahead: covers a slow frame or two
|
||||
const AHEAD: f32 = 0.07;
|
||||
|
||||
struct Out {
|
||||
_stream: cpal::Stream,
|
||||
queue: Arc<Mutex<VecDeque<f32>>>, // interleaved stereo
|
||||
rate: f32,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static OUT: RefCell<Option<Out>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
// opens the default output device and starts the engine; without one the game stays silent
|
||||
pub fn start() {
|
||||
match open() {
|
||||
Ok(out) => {
|
||||
super::dc_audio_init(out.rate);
|
||||
OUT.with(|o| *o.borrow_mut() = Some(out));
|
||||
}
|
||||
Err(e) => eprintln!("DungeonCraft: no sound ({})", e),
|
||||
}
|
||||
}
|
||||
|
||||
fn open() -> Result<Out, String> {
|
||||
let dev = cpal::default_host().default_output_device().ok_or("no output device")?;
|
||||
let cfg = dev.default_output_config().map_err(|e| e.to_string())?;
|
||||
let rate = cfg.sample_rate() as f32;
|
||||
let channels = cfg.channels() as usize;
|
||||
let queue = Arc::new(Mutex::new(VecDeque::new()));
|
||||
let sc: cpal::StreamConfig = cfg.config();
|
||||
use cpal::SampleFormat as F;
|
||||
let stream = match cfg.sample_format() {
|
||||
F::F32 => build::<f32>(&dev, &sc, channels, queue.clone()),
|
||||
F::I16 => build::<i16>(&dev, &sc, channels, queue.clone()),
|
||||
F::U16 => build::<u16>(&dev, &sc, channels, queue.clone()),
|
||||
F::I32 => build::<i32>(&dev, &sc, channels, queue.clone()),
|
||||
f => Err(format!("sample format {:?}", f)),
|
||||
}?;
|
||||
stream.play().map_err(|e| e.to_string())?;
|
||||
Ok(Out { _stream: stream, queue, rate })
|
||||
}
|
||||
|
||||
fn build<T: cpal::SizedSample + cpal::FromSample<f32>>(dev: &cpal::Device, sc: &cpal::StreamConfig, channels: usize, queue: Arc<Mutex<VecDeque<f32>>>) -> Result<cpal::Stream, String> {
|
||||
let channels = channels.max(1);
|
||||
dev.build_output_stream(
|
||||
sc.clone(),
|
||||
move |out: &mut [T], _: &cpal::OutputCallbackInfo| {
|
||||
let mut q = queue.lock().unwrap_or_else(|e| e.into_inner());
|
||||
for frame in out.chunks_mut(channels) {
|
||||
// an empty queue (a stalled frame) plays silence
|
||||
let (l, r) = if q.len() >= 2 { (q.pop_front().unwrap_or(0.0), q.pop_front().unwrap_or(0.0)) } else { (0.0, 0.0) };
|
||||
for (c, s) in frame.iter_mut().enumerate() {
|
||||
let v = match (channels, c) {
|
||||
(1, _) => (l + r) * 0.5,
|
||||
(_, 0) => l,
|
||||
(_, 1) => r,
|
||||
_ => 0.0,
|
||||
};
|
||||
*s = T::from_sample(v);
|
||||
}
|
||||
}
|
||||
},
|
||||
|e| eprintln!("DungeonCraft: sound output: {}", e),
|
||||
None,
|
||||
)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// called once per frame: tops the queue up to AHEAD seconds
|
||||
pub fn pump() {
|
||||
OUT.with(|o| {
|
||||
let o = o.borrow();
|
||||
let Some(o) = o.as_ref() else { return };
|
||||
let want = (o.rate * AHEAD) as usize;
|
||||
let have = o.queue.lock().unwrap_or_else(|e| e.into_inner()).len() / 2;
|
||||
if have >= want {
|
||||
return;
|
||||
}
|
||||
STATE.with(|s| {
|
||||
let mut s = s.borrow_mut();
|
||||
let (mv, sv, muted) = (s.music_vol, s.sfx_vol, s.muted);
|
||||
if let Some(e) = s.engine.as_mut() {
|
||||
e.render(want - have, mv, sv, muted);
|
||||
o.queue.lock().unwrap_or_else(|e| e.into_inner()).extend(e.buf.iter().copied());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in new issue
Block a user