use std::sync::{Arc, Mutex};
use sim_citizen_derive::non_citizen;
use sim_kernel::{
CORE_FUNCTION_CLASS_ID, ClassId, ClassRef, Cx, Error, Expr, Object, ObjectCompat, Result,
Symbol,
};
use sim_lib_stream_audio::{PcmBuffer, PcmSpec};
use sim_lib_stream_core::{
MidiPacket, StreamItem, StreamMetadata, StreamPacket, StreamStats, StreamValue,
};
#[non_citizen(
reason = "live stream handle; reconstruct stream/Metadata and stream/Packet descriptors then open explicitly",
kind = "handle",
descriptor = "stream/Metadata"
)]
#[derive(Clone)]
pub struct StreamHandle {
inner: Arc<HandleInner>,
}
struct HandleInner {
metadata: StreamMetadata,
kind: HandleKind,
}
enum HandleKind {
Source {
stream: Arc<StreamValue>,
},
PcmSink {
spec: PcmSpec,
state: Mutex<SinkState>,
},
MidiSink {
tpq: u16,
state: Mutex<SinkState>,
},
Pipeline {
source: StreamHandle,
sink: Option<StreamHandle>,
},
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct RunReport {
pub packets: usize,
pub written: usize,
}
#[derive(Clone, Debug)]
pub enum StageKind {
Identity,
}
#[non_citizen(
reason = "live stream stage handle; reconstruct stream/StageDescriptor then realize explicitly",
kind = "handle",
descriptor = "stream/StageDescriptor"
)]
#[derive(Clone, Debug)]
pub struct StageHandle {
kind: StageKind,
}
#[derive(Clone, Debug, Default)]
struct SinkState {
packets: Vec<StreamPacket>,
stats: StreamStats,
closed: bool,
}
impl StreamHandle {
pub fn source(metadata: StreamMetadata, stream: Arc<StreamValue>) -> Self {
Self::new(metadata, HandleKind::Source { stream })
}
pub fn pcm_sink(metadata: StreamMetadata, spec: PcmSpec) -> Self {
Self::new(
metadata,
HandleKind::PcmSink {
spec,
state: Mutex::new(SinkState::default()),
},
)
}
pub fn midi_sink(metadata: StreamMetadata, tpq: u16) -> Self {
Self::new(
metadata,
HandleKind::MidiSink {
tpq,
state: Mutex::new(SinkState::default()),
},
)
}
pub fn pipeline(source: StreamHandle, sink: Option<StreamHandle>) -> Result<Self> {
if !source.is_source_like() {
return Err(Error::Eval(
"stream/pipe expects a source stream first".to_owned(),
));
}
if sink.as_ref().is_some_and(|handle| !handle.is_sink()) {
return Err(Error::Eval(
"stream/pipe sink position must be a sink handle".to_owned(),
));
}
Ok(Self::new(
source.metadata().clone(),
HandleKind::Pipeline { source, sink },
))
}
fn new(metadata: StreamMetadata, kind: HandleKind) -> Self {
Self {
inner: Arc::new(HandleInner { metadata, kind }),
}
}
pub fn metadata(&self) -> &StreamMetadata {
&self.inner.metadata
}
pub fn is_sink(&self) -> bool {
matches!(
self.inner.kind,
HandleKind::PcmSink { .. } | HandleKind::MidiSink { .. }
)
}
pub fn is_pipeline_with_sink(&self) -> bool {
matches!(&self.inner.kind, HandleKind::Pipeline { sink: Some(_), .. })
}
pub fn next_packet(&self) -> Result<Option<StreamItem>> {
match &self.inner.kind {
HandleKind::Source { stream } => stream.next_packet(),
HandleKind::Pipeline { source, .. } => source.next_packet(),
HandleKind::PcmSink { .. } | HandleKind::MidiSink { .. } => Err(Error::Eval(
"stream/next! expects a source stream handle".to_owned(),
)),
}
}
pub fn write_packet(&self, packet: StreamPacket) -> Result<()> {
match &self.inner.kind {
HandleKind::PcmSink { spec, state } => {
let StreamPacket::Pcm(pcm) = &packet else {
return Err(Error::Eval(
"PCM memory sink expects PCM stream packets".to_owned(),
));
};
PcmBuffer::from_packet(*spec, pcm)?;
write_sink_packet(state, packet)
}
HandleKind::MidiSink { tpq, state } => {
let StreamPacket::Midi(midi) = &packet else {
return Err(Error::Eval(
"MIDI memory sink expects MIDI stream packets".to_owned(),
));
};
ensure_midi_tpq(*tpq, midi)?;
write_sink_packet(state, packet)
}
HandleKind::Source { .. } | HandleKind::Pipeline { .. } => Err(Error::Eval(
"stream/write! expects a sink stream handle".to_owned(),
)),
}
}
pub fn run(&self) -> Result<RunReport> {
match &self.inner.kind {
HandleKind::Source { .. } => {
let mut report = RunReport::default();
while self.next_packet()?.is_some() {
report.packets += 1;
}
Ok(report)
}
HandleKind::Pipeline { source, sink } => run_pipeline(source, sink.as_ref()),
HandleKind::PcmSink { .. } | HandleKind::MidiSink { .. } => Err(Error::Eval(
"stream/run! expects a source or pipeline handle".to_owned(),
)),
}
}
pub fn cancel(&self) -> Result<()> {
match &self.inner.kind {
HandleKind::Source { stream } => stream.cancel(),
HandleKind::Pipeline { source, sink } => {
source.cancel()?;
if let Some(sink) = sink {
sink.cancel()?;
}
Ok(())
}
HandleKind::PcmSink { state, .. } | HandleKind::MidiSink { state, .. } => {
let mut state = state
.lock()
.map_err(|_| Error::PoisonedLock("stream sink"))?;
state.closed = true;
state.stats.closed = true;
state.stats.cancelled = true;
Ok(())
}
}
}
pub fn stats(&self) -> Result<StreamStats> {
match &self.inner.kind {
HandleKind::Source { stream } => stream.stats(),
HandleKind::Pipeline { source, .. } => source.stats(),
HandleKind::PcmSink { state, .. } | HandleKind::MidiSink { state, .. } => state
.lock()
.map_err(|_| Error::PoisonedLock("stream sink"))
.map(|state| state.stats.clone()),
}
}
pub fn done(&self) -> Result<bool> {
match &self.inner.kind {
HandleKind::Source { stream } => stream.is_done(),
HandleKind::Pipeline { source, sink } => {
let source_done = source.done()?;
let sink_done = match sink {
Some(sink) => sink.done()?,
None => true,
};
Ok(source_done && sink_done)
}
HandleKind::PcmSink { state, .. } | HandleKind::MidiSink { state, .. } => state
.lock()
.map_err(|_| Error::PoisonedLock("stream sink"))
.map(|state| state.closed),
}
}
pub fn sink_packets(&self) -> Result<Vec<StreamPacket>> {
match &self.inner.kind {
HandleKind::PcmSink { state, .. } | HandleKind::MidiSink { state, .. } => state
.lock()
.map_err(|_| Error::PoisonedLock("stream sink"))
.map(|state| state.packets.clone()),
_ => Err(Error::Eval(
"stream/sink-packets expects a sink stream handle".to_owned(),
)),
}
}
pub fn graph_lisp_expr(&self) -> Expr {
match &self.inner.kind {
HandleKind::Pipeline { source, sink } => {
let mut args = vec![source.graph_lisp_atom()];
if let Some(sink) = sink {
args.push(sink.graph_lisp_atom());
}
Expr::Call {
operator: Box::new(Expr::Symbol(Symbol::qualified("stream", "pipe"))),
args,
}
}
HandleKind::Source { .. }
| HandleKind::PcmSink { .. }
| HandleKind::MidiSink { .. } => self.graph_lisp_atom(),
}
}
fn graph_lisp_atom(&self) -> Expr {
Expr::String(self.metadata().id().to_string())
}
fn is_source_like(&self) -> bool {
matches!(
self.inner.kind,
HandleKind::Source { .. } | HandleKind::Pipeline { .. }
)
}
fn close_sink(&self) -> Result<()> {
match &self.inner.kind {
HandleKind::PcmSink { state, .. } | HandleKind::MidiSink { state, .. } => {
let mut state = state
.lock()
.map_err(|_| Error::PoisonedLock("stream sink"))?;
state.closed = true;
state.stats.closed = true;
Ok(())
}
_ => Ok(()),
}
}
}
impl StageHandle {
pub fn identity() -> Self {
Self {
kind: StageKind::Identity,
}
}
pub fn is_identity(&self) -> bool {
matches!(self.kind, StageKind::Identity)
}
}
impl Object for StreamHandle {
fn display(&self, _cx: &mut Cx) -> Result<String> {
Ok(format!("#<stream-handle {}>", self.metadata().id()))
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl ObjectCompat for StreamHandle {
fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
cx.factory()
.class_stub(ClassId(0), Symbol::qualified("stream", "Handle"))
}
}
impl Object for StageHandle {
fn display(&self, _cx: &mut Cx) -> Result<String> {
Ok("#<stream-stage identity>".to_owned())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl ObjectCompat for StageHandle {
fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
cx.factory()
.class_stub(CORE_FUNCTION_CLASS_ID, Symbol::qualified("stream", "Stage"))
}
}
fn run_pipeline(source: &StreamHandle, sink: Option<&StreamHandle>) -> Result<RunReport> {
let mut report = RunReport::default();
while let Some(item) = source.next_packet()? {
report.packets += 1;
if let Some(sink) = sink {
sink.write_packet(item.packet().clone())?;
report.written += 1;
}
}
if let Some(sink) = sink {
sink.close_sink()?;
}
Ok(report)
}
fn write_sink_packet(state: &Mutex<SinkState>, packet: StreamPacket) -> Result<()> {
let mut state = state
.lock()
.map_err(|_| Error::PoisonedLock("stream sink"))?;
if state.closed {
return Err(Error::Eval(
"cannot write to a closed stream sink".to_owned(),
));
}
state.stats.pushed += 1;
state.stats.accepted += 1;
state.packets.push(packet);
Ok(())
}
fn ensure_midi_tpq(expected: u16, packet: &MidiPacket) -> Result<()> {
if packet.tpq() != expected {
return Err(Error::Eval(format!(
"MIDI packet TPQ {} does not match sink TPQ {expected}",
packet.tpq()
)));
}
Ok(())
}