use core::{num::NonZeroU32, usize};
use ringbuf::traits::Producer;
use thunderdome::Arena;
#[cfg(not(feature = "std"))]
use bevy_platform::prelude::{Box, Vec};
use firewheel_core::{
clock::InstantSamples,
dsp::{buffer::ChannelBuffer, declick::DeclickValues},
event::{NodeEvent, ProcEventsIndex},
log::RealtimeLogger,
node::{AudioNodeProcessor, ProcExtra, ProcStore},
StreamInfo,
};
use crate::{
backend::{AudioBackend, BackendProcessInfo},
graph::ScheduleHeapData,
processor::event_scheduler::{EventScheduler, NodeEventSchedulerData},
};
#[cfg(feature = "scheduled_events")]
use crate::context::ClearScheduledEventsType;
#[cfg(feature = "scheduled_events")]
use firewheel_core::node::NodeID;
#[cfg(feature = "scheduled_events")]
use smallvec::SmallVec;
#[cfg(feature = "musical_transport")]
use firewheel_core::clock::{InstantMusical, TransportState};
mod event_scheduler;
mod handle_messages;
mod process;
#[cfg(feature = "musical_transport")]
mod transport;
#[cfg(feature = "musical_transport")]
use transport::ProcTransportState;
pub struct FirewheelProcessor<B: AudioBackend> {
inner: Option<FirewheelProcessorInner<B>>,
drop_tx: ringbuf::HeapProd<FirewheelProcessorInner<B>>,
}
impl<B: AudioBackend> Drop for FirewheelProcessor<B> {
fn drop(&mut self) {
let Some(mut inner) = self.inner.take() else {
return;
};
inner.stream_stopped();
#[cfg(feature = "std")]
if std::thread::panicking() {
inner.poisoned = true;
}
let _ = self.drop_tx.try_push(inner);
}
}
impl<B: AudioBackend> FirewheelProcessor<B> {
pub(crate) fn new(
processor: FirewheelProcessorInner<B>,
drop_tx: ringbuf::HeapProd<FirewheelProcessorInner<B>>,
) -> Self {
Self {
inner: Some(processor),
drop_tx,
}
}
pub fn process_interleaved(
&mut self,
input: &[f32],
output: &mut [f32],
info: BackendProcessInfo<B>,
) {
if let Some(inner) = &mut self.inner {
inner.process_interleaved(input, output, info);
}
}
}
pub(crate) struct FirewheelProcessorInner<B: AudioBackend> {
nodes: Arena<NodeEntry>,
schedule_data: Option<Box<ScheduleHeapData>>,
from_graph_rx: ringbuf::HeapCons<ContextToProcessorMsg>,
to_graph_tx: ringbuf::HeapProd<ProcessorToContextMsg>,
event_scheduler: EventScheduler,
proc_event_queue: Vec<ProcEventsIndex>,
sample_rate: NonZeroU32,
sample_rate_recip: f64,
max_block_frames: usize,
clock_samples: InstantSamples,
shared_clock_input: triple_buffer::Input<SharedClock<B::Instant>>,
#[cfg(feature = "musical_transport")]
proc_transport_state: ProcTransportState,
hard_clip_outputs: bool,
pub(crate) extra: ProcExtra,
pub(crate) poisoned: bool,
debug_force_clear_buffers: bool,
}
impl<B: AudioBackend> FirewheelProcessorInner<B> {
pub(crate) fn new(
from_graph_rx: ringbuf::HeapCons<ContextToProcessorMsg>,
to_graph_tx: ringbuf::HeapProd<ProcessorToContextMsg>,
shared_clock_input: triple_buffer::Input<SharedClock<B::Instant>>,
immediate_event_buffer_capacity: usize,
#[cfg(feature = "scheduled_events")] scheduled_event_buffer_capacity: usize,
node_event_buffer_capacity: usize,
stream_info: &StreamInfo,
hard_clip_outputs: bool,
buffer_out_of_space_mode: BufferOutOfSpaceMode,
logger: RealtimeLogger,
debug_force_clear_buffers: bool,
store: ProcStore,
) -> Self {
Self {
nodes: Arena::new(),
schedule_data: None,
from_graph_rx,
to_graph_tx,
event_scheduler: EventScheduler::new(
immediate_event_buffer_capacity,
#[cfg(feature = "scheduled_events")]
scheduled_event_buffer_capacity,
buffer_out_of_space_mode,
),
proc_event_queue: Vec::with_capacity(node_event_buffer_capacity),
sample_rate: stream_info.sample_rate,
sample_rate_recip: stream_info.sample_rate_recip,
max_block_frames: stream_info.max_block_frames.get() as usize,
clock_samples: InstantSamples(0),
shared_clock_input,
#[cfg(feature = "musical_transport")]
proc_transport_state: ProcTransportState::new(),
hard_clip_outputs,
extra: ProcExtra {
scratch_buffers: ChannelBuffer::new(stream_info.max_block_frames.get() as usize),
declick_values: DeclickValues::new(stream_info.declick_frames),
logger,
store,
},
poisoned: false,
debug_force_clear_buffers,
}
}
}
pub(crate) struct NodeEntry {
pub processor: Box<dyn AudioNodeProcessor>,
pub prev_output_was_silent: bool,
event_data: NodeEventSchedulerData,
}
pub(crate) enum ContextToProcessorMsg {
EventGroup(Vec<NodeEvent>),
NewSchedule(Box<ScheduleHeapData>),
HardClipOutputs(bool),
#[cfg(feature = "musical_transport")]
SetTransportState(Box<TransportState>),
#[cfg(feature = "scheduled_events")]
ClearScheduledEvents(SmallVec<[ClearScheduledEventsEvent; 1]>),
}
pub(crate) enum ProcessorToContextMsg {
ReturnEventGroup(Vec<NodeEvent>),
ReturnSchedule(Box<ScheduleHeapData>),
#[cfg(feature = "musical_transport")]
ReturnTransportState(Box<TransportState>),
#[cfg(feature = "scheduled_events")]
ReturnClearScheduledEvents(SmallVec<[ClearScheduledEventsEvent; 1]>),
}
#[cfg(feature = "scheduled_events")]
pub(crate) struct ClearScheduledEventsEvent {
pub node_id: Option<NodeID>,
pub event_type: ClearScheduledEventsType,
}
#[derive(Clone)]
pub(crate) struct SharedClock<I: Clone> {
pub clock_samples: InstantSamples,
#[cfg(feature = "musical_transport")]
pub current_playhead: Option<InstantMusical>,
#[cfg(feature = "musical_transport")]
pub speed_multiplier: f64,
#[cfg(feature = "musical_transport")]
pub transport_is_playing: bool,
pub process_timestamp: Option<I>,
}
impl<I: Clone> Default for SharedClock<I> {
fn default() -> Self {
Self {
clock_samples: InstantSamples(0),
#[cfg(feature = "musical_transport")]
current_playhead: None,
#[cfg(feature = "musical_transport")]
speed_multiplier: 1.0,
#[cfg(feature = "musical_transport")]
transport_is_playing: false,
process_timestamp: None,
}
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd)]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BufferOutOfSpaceMode {
#[default]
AllocateOnAudioThread,
Panic,
DropEvents,
}