use bevy_asset::prelude::*;
use bevy_ecs::prelude::*;
use bevy_platform::sync;
use firewheel::{FirewheelConfig, FirewheelCtx, backend::AudioBackend, clock::AudioClock};
use std::num::NonZeroU32;
#[cfg(target_arch = "wasm32")]
mod web;
#[cfg(target_arch = "wasm32")]
use web::InnerContext;
#[cfg(not(target_arch = "wasm32"))]
mod os;
#[cfg(not(target_arch = "wasm32"))]
use os::InnerContext;
mod seedling_context;
pub use seedling_context::{SeedlingContext, SeedlingContextError, SeedlingContextWrapper};
#[derive(Debug, Resource)]
pub struct AudioContext(InnerContext);
impl AudioContext {
pub fn new<B>(settings: FirewheelConfig) -> Self
where
B: AudioBackend + 'static,
B::StreamError: Send + Sync + 'static,
{
AudioContext(InnerContext::new::<B>(settings))
}
pub fn now(&mut self) -> AudioClock {
self.with(|c| c.audio_clock_corrected())
}
pub fn with<F, O>(&mut self, f: F) -> O
where
F: FnOnce(&mut SeedlingContext) -> O + Send,
O: Send + 'static,
{
self.0.with(f)
}
}
#[derive(Resource, Debug, Clone)]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct SampleRate(sync::Arc<sync::atomic::AtomicU32>);
impl SampleRate {
pub fn get(&self) -> NonZeroU32 {
self.0
.load(sync::atomic::Ordering::Relaxed)
.try_into()
.unwrap()
}
}
#[derive(Resource, Debug)]
pub struct AudioStreamConfig<B: AudioBackend = firewheel::cpal::CpalBackend>(pub B::Config);
pub(crate) fn initialize_context<B>(
firewheel_config: crate::prelude::FirewheelConfig,
commands: &mut Commands,
) -> Result
where
B: AudioBackend + 'static,
B::StreamError: Send + Sync + 'static,
{
let context = AudioContext::new::<B>(firewheel_config);
commands.insert_resource(context);
Ok(())
}
pub(crate) fn start_stream<B>(
config: Res<AudioStreamConfig<B>>,
server: Res<AssetServer>,
mut context: ResMut<AudioContext>,
mut commands: Commands,
) -> Result
where
B: AudioBackend + 'static,
B::Config: Clone + Send + Sync + 'static,
{
context.with(|context| {
let context = context.downcast_mut::<FirewheelCtx<B>>().expect(
"Attempted to initialize audio context with unexpected backend type. \
`bevy_seedling` expects a single context.",
);
context
.start_stream(config.0.clone())
.map_err(|e| format!("failed to start audio stream: {e:?}"))?;
let raw_sample_rate = context.stream_info().unwrap().sample_rate;
let sample_rate = SampleRate(sync::Arc::new(sync::atomic::AtomicU32::new(
raw_sample_rate.get(),
)));
commands.insert_resource(sample_rate.clone());
server.register_loader(crate::sample::SampleLoader { sample_rate });
commands.trigger(StreamStartEvent {
sample_rate: raw_sample_rate,
});
Ok(())
})
}
#[derive(Event, Debug)]
pub struct StreamStartEvent {
pub sample_rate: NonZeroU32,
}
#[derive(Event, Debug)]
pub struct PreStreamRestartEvent;
pub(crate) fn pre_restart_context(mut commands: Commands) {
commands.trigger(PreStreamRestartEvent);
}
#[derive(Event, Debug)]
pub struct StreamRestartEvent {
pub previous_rate: NonZeroU32,
pub current_rate: NonZeroU32,
}
pub(crate) fn restart_context<B>(
stream_config: Res<AudioStreamConfig<B>>,
mut commands: Commands,
mut audio_context: ResMut<AudioContext>,
sample_rate: Res<SampleRate>,
) -> Result
where
B: AudioBackend + 'static,
B::Config: Clone + Send + Sync + 'static,
B::StreamError: Send + Sync + 'static,
{
audio_context.with(|context| {
let context: &mut FirewheelCtx<B> = context
.downcast_mut()
.ok_or("only one audio context should be active at a time")?;
context.stop_stream();
context
.start_stream(stream_config.0.clone())
.map_err(|e| format!("failed to restart audio stream: {e:?}"))?;
let previous_rate = sample_rate.get();
let current_rate = context.stream_info().unwrap().sample_rate;
sample_rate
.0
.store(current_rate.get(), sync::atomic::Ordering::Relaxed);
commands.trigger(StreamRestartEvent {
previous_rate,
current_rate,
});
Ok(())
})
}