use std::num::NonZeroU32;
use serde::{Deserialize, Serialize};
use crate::error::Error;
use crate::stream::StreamSpec;
use crate::time::{Frames, SampleRate, TimeBase};
pub const MORPH_BAND_MAX_SECONDS: f64 = 0.060;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CausalLayer {
Conv {
kernel: NonZeroU32,
dilation: NonZeroU32,
stride: NonZeroU32,
},
Upsample {
factor: NonZeroU32,
kernel: NonZeroU32,
},
}
impl CausalLayer {
fn left_context(self) -> u64 {
match self {
CausalLayer::Conv {
kernel, dilation, ..
} => u64::from(kernel.get() - 1) * u64::from(dilation.get()),
CausalLayer::Upsample { kernel, .. } => u64::from(kernel.get() - 1),
}
}
fn lookahead(self) -> u64 {
0
}
}
#[derive(Debug, thiserror::Error, PartialEq)]
#[non_exhaustive]
pub enum PlanError {
#[error("a causal plan needs at least one layer")]
Empty,
#[error("plan arithmetic overflows: {0}")]
Overflow(&'static str),
#[error(
"latency {latency_seconds:.4}s exceeds the {max_seconds:.4}s budget \
(block {block} + lookahead {lookahead})"
)]
BudgetExceeded {
latency_seconds: f64,
max_seconds: f64,
block: Frames,
lookahead: Frames,
},
}
impl From<PlanError> for Error {
fn from(err: PlanError) -> Self {
Error::validation(err)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LatencyBudget {
pub sample_rate: SampleRate,
pub block: Frames,
pub max_seconds: f64,
}
impl LatencyBudget {
pub fn morph(sample_rate: SampleRate, block: Frames) -> Self {
Self {
sample_rate,
block,
max_seconds: MORPH_BAND_MAX_SECONDS,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CausalPlan {
layers: Vec<CausalLayer>,
}
impl CausalPlan {
pub fn new(layers: Vec<CausalLayer>) -> Result<Self, PlanError> {
if layers.is_empty() {
return Err(PlanError::Empty);
}
Ok(Self { layers })
}
pub fn layers(&self) -> &[CausalLayer] {
&self.layers
}
pub fn total_stride(&self) -> Result<u64, PlanError> {
self.layers
.iter()
.filter_map(|layer| match layer {
CausalLayer::Conv { stride, .. } => Some(u64::from(stride.get())),
CausalLayer::Upsample { .. } => None,
})
.try_fold(1u64, |acc, s| acc.checked_mul(s))
.ok_or(PlanError::Overflow("total stride"))
}
pub fn total_upsample(&self) -> Result<u64, PlanError> {
self.layers
.iter()
.filter_map(|layer| match layer {
CausalLayer::Upsample { factor, .. } => Some(u64::from(factor.get())),
CausalLayer::Conv { .. } => None,
})
.try_fold(1u64, |acc, f| acc.checked_mul(f))
.ok_or(PlanError::Overflow("total upsample"))
}
pub fn receptive_field(&self) -> Result<u64, PlanError> {
let (mut num, mut den) = (1u64, 1u64);
let mut field = 1u64;
for layer in &self.layers {
let reach = layer.left_context();
let scaled = reach
.checked_mul(num)
.ok_or(PlanError::Overflow("receptive field"))?
.div_ceil(den);
field = field
.checked_add(scaled)
.ok_or(PlanError::Overflow("receptive field"))?;
match layer {
CausalLayer::Conv { stride, .. } => {
num = num
.checked_mul(u64::from(stride.get()))
.ok_or(PlanError::Overflow("receptive field"))?;
}
CausalLayer::Upsample { factor, .. } => {
den = den
.checked_mul(u64::from(factor.get()))
.ok_or(PlanError::Overflow("receptive field"))?;
}
}
let g = gcd(num, den);
num /= g;
den /= g;
}
Ok(field)
}
pub fn lookahead(&self) -> Frames {
Frames(self.layers.iter().map(|l| l.lookahead()).sum())
}
pub fn check(self, budget: &LatencyBudget) -> Result<CheckedPlan, PlanError> {
let stride = self.total_stride()?;
let stride = u32::try_from(stride)
.ok()
.and_then(NonZeroU32::new)
.ok_or(PlanError::Overflow("stride exceeds u32"))?;
let time_base = TimeBase::new(budget.sample_rate, stride);
let lookahead = self.lookahead();
let spec = StreamSpec::new(budget.block, lookahead, time_base)
.map_err(|_| PlanError::Overflow("zero-frame block"))?;
let latency = spec.latency_seconds();
if latency > budget.max_seconds {
return Err(PlanError::BudgetExceeded {
latency_seconds: latency,
max_seconds: budget.max_seconds,
block: budget.block,
lookahead,
});
}
Ok(CheckedPlan { plan: self, spec })
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CheckedPlan {
plan: CausalPlan,
spec: StreamSpec,
}
impl CheckedPlan {
pub fn plan(&self) -> &CausalPlan {
&self.plan
}
pub fn spec(&self) -> StreamSpec {
self.spec
}
}
fn gcd(mut a: u64, mut b: u64) -> u64 {
while b != 0 {
(a, b) = (b, a % b);
}
a.max(1)
}
#[cfg(test)]
mod tests {
use super::*;
fn nz(v: u32) -> NonZeroU32 {
NonZeroU32::new(v).unwrap()
}
fn conv(kernel: u32, dilation: u32, stride: u32) -> CausalLayer {
CausalLayer::Conv {
kernel: nz(kernel),
dilation: nz(dilation),
stride: nz(stride),
}
}
#[test]
fn stride_and_upsample_products() {
let plan = CausalPlan::new(vec![
conv(3, 1, 2),
conv(3, 2, 4),
CausalLayer::Upsample {
factor: nz(8),
kernel: nz(3),
},
])
.unwrap();
assert_eq!(plan.total_stride().unwrap(), 8);
assert_eq!(plan.total_upsample().unwrap(), 8);
assert_eq!(plan.lookahead(), Frames(0));
}
#[test]
fn receptive_field_composes_with_stride() {
let plan = CausalPlan::new(vec![conv(3, 1, 1), conv(3, 1, 2), conv(3, 1, 1)]).unwrap();
assert_eq!(plan.receptive_field().unwrap(), 1 + 2 + 2 + 4);
}
#[test]
fn empty_and_overflow_fail_closed() {
assert_eq!(CausalPlan::new(vec![]).unwrap_err(), PlanError::Empty);
let deep = CausalPlan::new(vec![conv(3, 1, u32::MAX); 3]).unwrap();
assert!(matches!(deep.total_stride(), Err(PlanError::Overflow(_))));
}
#[test]
fn budget_check_mints_or_rejects() {
let rate = SampleRate::try_from(48_000).unwrap();
let plan = CausalPlan::new(vec![conv(3, 1, 8), conv(3, 1, 8), conv(3, 1, 8)]).unwrap();
let checked = plan
.clone()
.check(&LatencyBudget::morph(rate, Frames(3)))
.unwrap();
assert_eq!(checked.spec().time_base().stride().get(), 512);
assert!(checked.spec().latency_seconds() < MORPH_BAND_MAX_SECONDS);
let err = plan
.check(&LatencyBudget::morph(rate, Frames(8)))
.unwrap_err();
assert!(matches!(
err,
PlanError::BudgetExceeded {
block: Frames(8),
..
}
));
}
#[test]
fn plans_serialize_proofs_do_not() {
let plan = CausalPlan::new(vec![conv(3, 2, 2)]).unwrap();
let json = serde_json::to_string(&plan).unwrap();
assert_eq!(serde_json::from_str::<CausalPlan>(&json).unwrap(), plan);
}
}