use std::thread;
use sim_kernel::{Error, Result, Symbol};
use sim_lib_audio_graph_core::{Processor, Transport};
use sim_lib_stream_host::{ProcessRingPush, ProcessRingSnapshot, ProcessSharedRing};
use crate::{
LiveGraphConfig, LiveGraphRunner, LiveProcessReport, LiveSteadyStateSnapshot,
realtime_local_audio_profile,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LivePlacementSite {
Coroutine,
Thread,
HostCallback,
Process,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LivePlacementSnapshot {
site: LivePlacementSite,
runner: LiveSteadyStateSnapshot,
process_request_ring: Option<ProcessRingSnapshot>,
process_response_ring: Option<ProcessRingSnapshot>,
}
#[derive(Debug)]
pub struct LivePlacedNode<P> {
site: LivePlacementSite,
runner: LiveGraphRunner<P>,
process_request_ring: Option<ProcessSharedRing<ProcessSiteMessage>>,
process_response_ring: Option<ProcessSharedRing<ProcessSiteMessage>>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum ProcessSiteMessage {
Render { frames: usize, transport: Transport },
Rendered { frames: u32 },
}
impl LivePlacementSite {
pub const fn all() -> [Self; 4] {
[
Self::Coroutine,
Self::Thread,
Self::HostCallback,
Self::Process,
]
}
pub const fn name(self) -> &'static str {
match self {
Self::Coroutine => "coroutine",
Self::Thread => "thread",
Self::HostCallback => "host-callback",
Self::Process => "process",
}
}
pub fn symbol(self) -> Symbol {
Symbol::qualified("stream/site", self.name())
}
pub const fn runs_on_audio_clock_thread(self) -> bool {
matches!(self, Self::HostCallback)
}
pub const fn uses_process_ring(self) -> bool {
matches!(self, Self::Process)
}
pub const fn allows_realtime_pin(self) -> bool {
self.runs_on_audio_clock_thread()
}
fn validate_processor(self, realtime_pin: bool) -> Result<()> {
if realtime_pin && !self.allows_realtime_pin() {
return Err(Error::Eval(format!(
"realtime-pinned live audio node cannot run at {} site",
self.name()
)));
}
Ok(())
}
}
impl<P: Processor> LivePlacedNode<P> {
pub fn new(processor: P, config: LiveGraphConfig, site: LivePlacementSite) -> Result<Self> {
site.validate_processor(processor.realtime_pin())?;
let runner = if site.runs_on_audio_clock_thread() {
LiveGraphRunner::new_realtime(processor, config, &realtime_local_audio_profile())?
} else {
LiveGraphRunner::new(processor, config)?
};
let process_request_ring = site
.uses_process_ring()
.then(|| ProcessSharedRing::with_capacity(1))
.transpose()?;
let process_response_ring = site
.uses_process_ring()
.then(|| ProcessSharedRing::with_capacity(1))
.transpose()?;
Ok(Self {
site,
runner,
process_request_ring,
process_response_ring,
})
}
pub fn site(&self) -> LivePlacementSite {
self.site
}
pub fn runner(&self) -> &LiveGraphRunner<P> {
&self.runner
}
pub fn runner_mut(&mut self) -> &mut LiveGraphRunner<P> {
&mut self.runner
}
pub fn process_interleaved_f32(
&mut self,
input: Option<&[f32]>,
output: &mut [f32],
frames: usize,
transport: Transport,
) -> Result<LiveProcessReport> {
match self.site {
LivePlacementSite::Coroutine | LivePlacementSite::HostCallback => self
.runner
.process_interleaved_f32(input, output, frames, transport),
LivePlacementSite::Thread => {
run_on_worker_thread(&mut self.runner, input, output, frames, transport)
}
LivePlacementSite::Process => {
self.process_via_process_ring(input, output, frames, transport)
}
}
}
pub fn steady_state_snapshot(&self) -> LivePlacementSnapshot {
LivePlacementSnapshot {
site: self.site,
runner: self.runner.steady_state_snapshot(),
process_request_ring: self
.process_request_ring
.as_ref()
.map(|ring| ring.snapshot()),
process_response_ring: self
.process_response_ring
.as_ref()
.map(|ring| ring.snapshot()),
}
}
fn process_via_process_ring(
&mut self,
input: Option<&[f32]>,
output: &mut [f32],
frames: usize,
transport: Transport,
) -> Result<LiveProcessReport> {
let work = ProcessSiteMessage::Render { frames, transport };
{
let request_ring = self.process_request_ring.as_mut().ok_or_else(|| {
Error::Eval("process site is missing its request ring".to_owned())
})?;
accept_process_push(request_ring.try_push(work), "request")?;
}
let work = self
.process_request_ring
.as_mut()
.and_then(ProcessSharedRing::try_pop)
.ok_or_else(|| Error::Eval("process site request ring lost work".to_owned()))?;
let ProcessSiteMessage::Render { frames, transport } = work else {
return Err(Error::Eval(
"process site request ring received a response".to_owned(),
));
};
let report = run_on_worker_thread(&mut self.runner, input, output, frames, transport)?;
{
let response_ring = self.process_response_ring.as_mut().ok_or_else(|| {
Error::Eval("process site is missing its response ring".to_owned())
})?;
accept_process_push(
response_ring.try_push(ProcessSiteMessage::Rendered {
frames: report.frames(),
}),
"response",
)?;
}
let response = self
.process_response_ring
.as_mut()
.and_then(ProcessSharedRing::try_pop)
.ok_or_else(|| Error::Eval("process site response ring lost output".to_owned()))?;
match response {
ProcessSiteMessage::Rendered { frames } if frames == report.frames() => Ok(report),
ProcessSiteMessage::Rendered { .. } => Err(Error::Eval(
"process site response ring returned the wrong frame count".to_owned(),
)),
ProcessSiteMessage::Render { .. } => Err(Error::Eval(
"process site response ring received a request".to_owned(),
)),
}
}
}
impl LivePlacementSnapshot {
pub fn site(&self) -> LivePlacementSite {
self.site
}
pub fn runner(&self) -> &LiveSteadyStateSnapshot {
&self.runner
}
pub fn process_request_ring(&self) -> Option<ProcessRingSnapshot> {
self.process_request_ring
}
pub fn process_response_ring(&self) -> Option<ProcessRingSnapshot> {
self.process_response_ring
}
}
fn run_on_worker_thread<P: Processor>(
runner: &mut LiveGraphRunner<P>,
input: Option<&[f32]>,
output: &mut [f32],
frames: usize,
transport: Transport,
) -> Result<LiveProcessReport> {
match thread::scope(|scope| {
scope
.spawn(move || runner.process_interleaved_f32(input, output, frames, transport))
.join()
}) {
Ok(result) => result,
Err(_) => Err(Error::Eval(
"live audio worker thread panicked while processing".to_owned(),
)),
}
}
fn accept_process_push<T>(push: ProcessRingPush<T>, role: &str) -> Result<()> {
match push {
ProcessRingPush::Accepted => Ok(()),
ProcessRingPush::DroppedNewest(_) => {
Err(Error::Eval(format!("process site {role} ring is full")))
}
ProcessRingPush::Closed(_) => {
Err(Error::Eval(format!("process site {role} ring is closed")))
}
}
}