diff --git a/Cargo.lock b/Cargo.lock index 226da6e..62e0047 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,7 +45,7 @@ dependencies = [ "hashbrown 0.15.5", "objc2 0.5.2", "objc2-app-kit", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -132,6 +132,18 @@ dependencies = [ "libc", ] +[[package]] +name = "alsa" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c88dbbce13b232b26250e1e2e6ac18b6a891a646b8148285036ebce260ac5c3" +dependencies = [ + "alsa-sys", + "bitflags 2.11.1", + "cfg-if", + "libc", +] + [[package]] name = "alsa-sys" version = "0.3.1" @@ -440,6 +452,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-arena" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e8ed45f88ed32e6827a96b62d8fd4086d72defc754c5c6bd08470c1aaf648e" + [[package]] name = "atomic-waker" version = "1.1.2" @@ -758,7 +776,7 @@ dependencies = [ "bevy_reflect", "bevy_transform", "coreaudio-sys", - "cpal", + "cpal 0.15.3", "rodio", "tracing", ] @@ -2336,6 +2354,20 @@ dependencies = [ "coreaudio-sys", ] +[[package]] +name = "coreaudio-rs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" +dependencies = [ + "bitflags 1.3.2", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", +] + [[package]] name = "coreaudio-sys" version = "0.2.17" @@ -2375,14 +2407,14 @@ version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" dependencies = [ - "alsa", + "alsa 0.9.1", "core-foundation-sys", - "coreaudio-rs", + "coreaudio-rs 0.11.3", "dasp_sample", "jni 0.21.1", "js-sys", "libc", - "mach2", + "mach2 0.4.3", "ndk 0.8.0", "ndk-context", "oboe", @@ -2392,6 +2424,36 @@ dependencies = [ "windows 0.54.0", ] +[[package]] +name = "cpal" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b1f9c7312f19fc2fa12fd7acaf38de54e8320ba10d1a02dcbe21038def51ccb" +dependencies = [ + "alsa 0.10.0", + "coreaudio-rs 0.13.0", + "dasp_sample", + "jni 0.21.1", + "js-sys", + "libc", + "mach2 0.5.0", + "ndk 0.9.0", + "ndk-context", + "num-derive", + "num-traits", + "objc2 0.6.4", + "objc2-audio-toolbox", + "objc2-avf-audio", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.62.2", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -3350,15 +3412,6 @@ dependencies = [ "xml-rs", ] -[[package]] -name = "glam" -version = "0.29.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee" -dependencies = [ - "mint", -] - [[package]] name = "glam" version = "0.30.10" @@ -3372,6 +3425,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "glam" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f70749695b063ecbf6b62949ccccde2e733ec3ecbbd71d467dca4e5c6c97cca0" +dependencies = [ + "mint", +] + [[package]] name = "glob" version = "0.3.3" @@ -4244,15 +4306,16 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kira" -version = "0.9.6" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a9f9dff5e262540b628b00d5e1a772270a962d6869f8695bb24832ff3b394" +checksum = "22dc6835b2ca4b48601f11df172781f5e7677d40a6a466b1b873adcb34ec5d55" dependencies = [ - "cpal", - "glam 0.29.3", + "atomic-arena", + "cpal 0.17.1", + "glam 0.32.1", "mint", - "paste", - "ringbuf", + "pastey", + "rtrb", "send_wrapper", "symphonia", "triple_buffer", @@ -4424,6 +4487,15 @@ dependencies = [ "libc", ] +[[package]] +name = "mach2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -4903,10 +4975,35 @@ dependencies = [ "objc2 0.5.2", "objc2-core-data", "objc2-core-image", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-quartz-core", ] +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" +dependencies = [ + "bitflags 2.11.1", + "libc", + "objc2 0.6.4", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-avf-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-cloud-kit" version = "0.2.2" @@ -4917,7 +5014,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -4928,7 +5025,30 @@ checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2 0.6.4", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", ] [[package]] @@ -4940,7 +5060,7 @@ dependencies = [ "bitflags 2.11.1", "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -4950,6 +5070,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.11.1", + "block2 0.6.2", + "dispatch2", + "libc", + "objc2 0.6.4", ] [[package]] @@ -4960,7 +5084,7 @@ checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-metal", ] @@ -4973,7 +5097,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-contacts", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -4995,6 +5119,19 @@ dependencies = [ "objc2 0.5.2", ] +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "block2 0.6.2", + "libc", + "objc2 0.6.4", + "objc2-core-foundation", +] + [[package]] name = "objc2-io-kit" version = "0.3.2" @@ -5015,7 +5152,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-app-kit", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5027,7 +5164,7 @@ dependencies = [ "bitflags 2.11.1", "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5039,7 +5176,7 @@ dependencies = [ "bitflags 2.11.1", "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-metal", ] @@ -5050,7 +5187,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" dependencies = [ "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5066,7 +5203,7 @@ dependencies = [ "objc2-core-data", "objc2-core-image", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-link-presentation", "objc2-quartz-core", "objc2-symbols", @@ -5082,7 +5219,7 @@ checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5095,7 +5232,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5255,6 +5392,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a797f0e07bdf071d15742978fc3128ec6c22891c31a3a931513263904c982a" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -5868,22 +6011,13 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "ringbuf" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79abed428d1fd2a128201cec72c5f6938e2da607c6f3745f769fabea399d950a" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "rodio" version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1" dependencies = [ - "cpal", + "cpal 0.15.3", "lewton", ] @@ -5927,6 +6061,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rtrb" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ade083ccbb4bf536df69d1f6432cc23deb7acccff86b183f3923a6fd56a1153" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -8783,7 +8923,7 @@ dependencies = [ "ndk 0.9.0", "objc2 0.5.2", "objc2-app-kit", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-ui-kit", "orbclient", "percent-encoding", diff --git a/Cargo.toml b/Cargo.toml index 6ad9e89..9849c35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ solitaire_data = { path = "solitaire_data" } solitaire_engine = { path = "solitaire_engine" } bevy = "0.18" -kira = "0.9" +kira = "0.12" axum = "0.8" sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } diff --git a/solitaire_engine/src/audio_plugin.rs b/solitaire_engine/src/audio_plugin.rs index 93c55c1..3387a57 100644 --- a/solitaire_engine/src/audio_plugin.rs +++ b/solitaire_engine/src/audio_plugin.rs @@ -23,13 +23,10 @@ use std::io::Cursor; use bevy::prelude::*; -use kira::manager::backend::DefaultBackend; -use kira::manager::{AudioManager, AudioManagerSettings}; use kira::sound::static_sound::{StaticSoundData, StaticSoundHandle}; use kira::sound::Region; use kira::track::{TrackBuilder, TrackHandle}; -use kira::tween::Tween; -use kira::Volume; +use kira::{AudioManager, AudioManagerSettings, Decibels, DefaultBackend, Tween, Value}; use crate::events::{ CardFaceRevealedEvent, CardFlippedEvent, DrawRequestEvent, GameWonEvent, MoveRejectedEvent, @@ -46,6 +43,16 @@ const RECYCLE_VOLUME: f64 = 0.5; /// Volume amplitude for the ambient music loop placeholder. const AMBIENT_VOLUME: f64 = 0.05; +/// Converts a linear amplitude (0.0–1.0+) to the `Decibels` type used by +/// kira 0.12. Clamps to `Decibels::SILENCE` for non-positive amplitudes. +fn amplitude_to_decibels(amplitude: f32) -> Decibels { + if amplitude <= 0.0 { + Decibels::SILENCE + } else { + Decibels(20.0 * amplitude.log10()) + } +} + /// Returns `true` when a `DrawRequestEvent` will recycle the waste pile back /// to stock rather than drawing a new card. /// @@ -56,7 +63,7 @@ fn is_recycle(stock_len: usize) -> bool { } /// Pre-decoded sound effects. Cheap to clone (frames are an `Arc<[Frame]>`), -/// so we hand a fresh handle to `manager.play()` on every event. +/// so we hand a fresh handle to `track.play()` on every event. #[derive(Resource, Clone)] pub struct SoundLibrary { pub deal: StaticSoundData, @@ -104,7 +111,7 @@ impl Plugin for AudioPlugin { warn!("failed to decode embedded SFX assets; SFX disabled"); } - let (sfx_track, music_track) = match manager.as_mut() { + let (sfx_track, mut music_track) = match manager.as_mut() { Some(mgr) => { let sfx = mgr.add_sub_track(TrackBuilder::default()).ok(); let music = mgr.add_sub_track(TrackBuilder::default()).ok(); @@ -116,7 +123,7 @@ impl Plugin for AudioPlugin { // Start the ambient loop placeholder (card_flip.wav looped at very low // volume through music_track). let ambient_handle = - start_ambient_loop(manager.as_mut(), library.as_ref(), &music_track); + start_ambient_loop(manager.as_mut(), library.as_ref(), &mut music_track); app.insert_non_send_resource(AudioState { manager, @@ -190,20 +197,22 @@ fn decode(bytes: &'static [u8]) -> Option { fn start_ambient_loop( manager: Option<&mut AudioManager>, library: Option<&SoundLibrary>, - music_track: &Option, + music_track: &mut Option, ) -> Option { let manager = manager?; let lib = library?; let mut data = lib.flip.clone(); - // Loop the entire file from start to end. data.settings.loop_region = Some(Region::default()); - data.settings.volume = Volume::Amplitude(AMBIENT_VOLUME).into(); - if let Some(track) = music_track { - data.settings.output_destination = track.id().into(); - } + data.settings.volume = Value::Fixed(amplitude_to_decibels(AMBIENT_VOLUME as f32)); - match manager.play(data) { + let result = if let Some(track) = music_track.as_mut() { + track.play(data) + } else { + manager.play(data) + }; + + match result { Ok(handle) => Some(handle), Err(e) => { warn!("failed to start ambient loop: {e}"); @@ -213,16 +222,17 @@ fn start_ambient_loop( } fn play(audio: &mut AudioState, sound: &StaticSoundData) { - let Some(manager) = audio.manager.as_mut() else { - return; - }; + let data = sound.clone(); // Route SFX through the dedicated sfx_track so its volume is independent // of the music_track volume. - let mut data = sound.clone(); - if let Some(track) = &audio.sfx_track { - data.settings.output_destination = track.id().into(); - } - if let Err(e) = manager.play(data) { + let result = if let Some(track) = audio.sfx_track.as_mut() { + track.play(data) + } else if let Some(manager) = audio.manager.as_mut() { + manager.play(data) + } else { + return; + }; + if let Err(e) = result { warn!("failed to play SFX: {e}"); } } @@ -234,15 +244,17 @@ impl AudioState { /// explicit volume override so callers can play sounds at a fraction of their /// normal level. Silently does nothing when audio is unavailable. pub fn play_sfx_at_volume(&mut self, sound: &StaticSoundData, volume: f64) { - let Some(manager) = self.manager.as_mut() else { + let mut data = sound.clone(); + data.settings.volume = Value::Fixed(amplitude_to_decibels(volume as f32)); + + let result = if let Some(track) = self.sfx_track.as_mut() { + track.play(data) + } else if let Some(manager) = self.manager.as_mut() { + manager.play(data) + } else { return; }; - let mut data = sound.clone(); - data.settings.volume = Volume::Amplitude(volume).into(); - if let Some(track) = &self.sfx_track { - data.settings.output_destination = track.id().into(); - } - if let Err(e) = manager.play(data) { + if let Err(e) = result { warn!("failed to play SFX at volume {volume}: {e}"); } } @@ -250,13 +262,13 @@ impl AudioState { fn set_sfx_volume(audio: &mut AudioState, volume: f32) { if let Some(track) = audio.sfx_track.as_mut() { - track.set_volume(volume.clamp(0.0, 1.0) as f64, Tween::default()); + track.set_volume(amplitude_to_decibels(volume.clamp(0.0, 1.0)), Tween::default()); } } fn set_music_volume(audio: &mut AudioState, volume: f32) { if let Some(track) = audio.music_track.as_mut() { - track.set_volume(volume.clamp(0.0, 1.0) as f64, Tween::default()); + track.set_volume(amplitude_to_decibels(volume.clamp(0.0, 1.0)), Tween::default()); } } @@ -345,14 +357,17 @@ fn play_on_draw( if is_recycle(stock_len) { let mut data = lib.flip.clone(); - data.settings.volume = Volume::Amplitude(RECYCLE_VOLUME).into(); - if let Some(track) = &audio.sfx_track { - data.settings.output_destination = track.id().into(); - } - if let Some(manager) = audio.manager.as_mut() { - if let Err(e) = manager.play(data) { - warn!("failed to play recycle SFX: {e}"); - } + data.settings.volume = + Value::Fixed(amplitude_to_decibels(RECYCLE_VOLUME as f32)); + let result = if let Some(track) = audio.sfx_track.as_mut() { + track.play(data) + } else if let Some(manager) = audio.manager.as_mut() { + manager.play(data) + } else { + continue; + }; + if let Err(e) = result { + warn!("failed to play recycle SFX: {e}"); } } else { play(&mut audio, &lib.flip);