use sim_kernel::{
Cx, Datum, DatumStore, Diagnostic, Error, Expr, NumberLiteral, Ref, Result, Severity, Symbol,
Tick,
};
use sim_lib_stream_core::ClockDomain;
use crate::{Instant, TempoMap, tempo::midi_tick_duration};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct ClockIndex(u64);
impl ClockIndex {
pub fn new(value: u64) -> Self {
Self(value)
}
pub fn value(self) -> u64 {
self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IndexConversion {
index: ClockIndex,
diagnostics: Vec<Diagnostic>,
}
impl IndexConversion {
fn exact(index: u64) -> Self {
Self {
index: ClockIndex::new(index),
diagnostics: Vec::new(),
}
}
fn inexact(index: u64, message: impl Into<String>) -> Self {
Self {
index: ClockIndex::new(index),
diagnostics: vec![Diagnostic {
severity: Severity::Warning,
message: message.into(),
source: None,
span: None,
code: Some(Symbol::qualified("stream/clock", "inexact-conversion")),
related: Vec::new(),
}],
}
}
pub fn index(&self) -> ClockIndex {
self.index
}
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
pub fn is_exact(&self) -> bool {
self.diagnostics.is_empty()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ClockChart {
Frames {
frames_per_second: u64,
},
Midi {
tpq: u32,
tempo_map: TempoMap,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Clock {
id: Symbol,
domain: ClockDomain,
chart: ClockChart,
}
impl Clock {
pub fn frame(id: Symbol, frames_per_second: u64) -> Result<Self> {
Self::frame_with_domain(id, ClockDomain::Sample, frames_per_second)
}
pub fn frame_with_domain(
id: Symbol,
domain: ClockDomain,
frames_per_second: u64,
) -> Result<Self> {
if domain == ClockDomain::MidiTick {
return Err(Error::Eval(
"frame clock domain must not be midi-tick".to_owned(),
));
}
if frames_per_second == 0 {
return Err(Error::Eval("frame clock rate must be non-zero".to_owned()));
}
Ok(Self {
id,
domain,
chart: ClockChart::Frames { frames_per_second },
})
}
pub fn midi(id: Symbol, tpq: u32, tempo_map: TempoMap) -> Result<Self> {
if tpq == 0 {
return Err(Error::Eval("midi clock TPQ must be non-zero".to_owned()));
}
Ok(Self {
id,
domain: ClockDomain::MidiTick,
chart: ClockChart::Midi { tpq, tempo_map },
})
}
pub fn id(&self) -> &Symbol {
&self.id
}
pub fn domain(&self) -> ClockDomain {
self.domain
}
pub fn chart(&self) -> &ClockChart {
&self.chart
}
pub fn index_for_instant(&self, instant: Instant) -> Result<IndexConversion> {
match &self.chart {
ClockChart::Frames { frames_per_second } => {
frame_index_for_instant(instant, *frames_per_second)
}
ClockChart::Midi { tpq, tempo_map } => midi_index_for_instant(instant, *tpq, tempo_map),
}
}
pub fn instant_for_index(&self, index: ClockIndex) -> Result<Instant> {
match &self.chart {
ClockChart::Frames { frames_per_second } => {
Instant::new(i128::from(index.value()), i128::from(*frames_per_second))
}
ClockChart::Midi { tpq, tempo_map } => midi_instant_for_index(index, *tpq, tempo_map),
}
}
pub fn tick_for_index(&self, cx: &mut Cx, index: ClockIndex) -> Result<Tick> {
let id = cx.datum_store_mut().intern(Datum::Node {
tag: Symbol::qualified("stream/clock", "index"),
fields: vec![
(Symbol::new("clock"), Datum::Symbol(self.id.clone())),
(
Symbol::new("index"),
Datum::Number(NumberLiteral {
domain: Symbol::qualified("stream/clock", "index"),
canonical: index.value().to_string(),
}),
),
],
})?;
Ok(Tick::new(self.id.clone(), Ref::Content(id)))
}
pub fn index_expr(&self, index: ClockIndex) -> Expr {
Expr::Extension {
tag: Symbol::qualified("stream/clock", "index"),
payload: Box::new(Expr::Map(vec![
(
Expr::Symbol(Symbol::new("clock")),
Expr::Symbol(self.id.clone()),
),
(
Expr::Symbol(Symbol::new("index")),
Expr::Number(NumberLiteral {
domain: Symbol::qualified("stream/clock", "index"),
canonical: index.value().to_string(),
}),
),
])),
}
}
}
fn frame_index_for_instant(instant: Instant, frames_per_second: u64) -> Result<IndexConversion> {
let numerator = instant
.numerator()
.checked_mul(i128::from(frames_per_second))
.ok_or_else(|| Error::Eval("frame clock conversion overflowed".to_owned()))?;
let denominator = instant.denominator();
let index = checked_index(numerator / denominator)?;
if numerator % denominator == 0 {
Ok(IndexConversion::exact(index))
} else {
Ok(IndexConversion::inexact(
index,
"instant is not an exact frame boundary",
))
}
}
fn midi_index_for_instant(
instant: Instant,
tpq: u32,
tempo_map: &TempoMap,
) -> Result<IndexConversion> {
let starts = tempo_map.segment_start_instants(tpq)?;
let segment_index = starts.partition_point(|start| *start <= instant) - 1;
let segment = tempo_map.segments()[segment_index];
let elapsed = instant.checked_sub(starts[segment_index])?;
let numerator = elapsed
.numerator()
.checked_mul(1_000_000)
.and_then(|value| value.checked_mul(i128::from(tpq)))
.ok_or_else(|| Error::Eval("midi clock conversion overflowed".to_owned()))?;
let denominator = elapsed
.denominator()
.checked_mul(i128::from(segment.us_per_quarter))
.ok_or_else(|| Error::Eval("midi clock denominator overflowed".to_owned()))?;
let delta_ticks = checked_index(numerator / denominator)?;
let index = segment
.start_tick
.checked_add(delta_ticks)
.ok_or_else(|| Error::Eval("midi clock index overflowed".to_owned()))?;
if numerator % denominator == 0 {
Ok(IndexConversion::exact(index))
} else {
Ok(IndexConversion::inexact(
index,
"instant is not an exact midi tick boundary",
))
}
}
fn midi_instant_for_index(index: ClockIndex, tpq: u32, tempo_map: &TempoMap) -> Result<Instant> {
let starts = tempo_map.segment_start_instants(tpq)?;
let segments = tempo_map.segments();
let segment_index = segments.partition_point(|segment| segment.start_tick <= index.value()) - 1;
let segment = segments[segment_index];
let elapsed_ticks = index.value() - segment.start_tick;
starts[segment_index].checked_add(midi_tick_duration(
elapsed_ticks,
tpq,
segment.us_per_quarter,
)?)
}
fn checked_index(value: i128) -> Result<u64> {
u64::try_from(value)
.map_err(|_| Error::Eval("clock index must fit in an unsigned 64-bit value".to_owned()))
}