use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
use crate::time::{SampleRate, Samples};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChannelLayout {
Mono,
Stereo,
}
impl ChannelLayout {
pub const fn channels(self) -> usize {
match self {
ChannelLayout::Mono => 1,
ChannelLayout::Stereo => 2,
}
}
pub fn from_channels(channels: usize) -> Result<Self> {
match channels {
1 => Ok(ChannelLayout::Mono),
2 => Ok(ChannelLayout::Stereo),
other => Err(Error::validation(format!(
"unsupported channel count {other}: helena audio is mono or stereo (FR-014)"
))),
}
}
}
impl std::fmt::Display for ChannelLayout {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ChannelLayout::Mono => write!(f, "mono"),
ChannelLayout::Stereo => write!(f, "stereo"),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "PcmRepr")]
pub struct Pcm {
rate: SampleRate,
layout: ChannelLayout,
samples: Vec<f32>,
}
impl Pcm {
pub fn new(rate: SampleRate, layout: ChannelLayout, samples: Vec<f32>) -> Result<Self> {
if !samples.len().is_multiple_of(layout.channels()) {
return Err(Error::validation(format!(
"interleaved sample count {} is not a whole number of {layout} frames",
samples.len()
)));
}
if !samples.iter().all(|s| s.is_finite()) {
return Err(Error::NonFinite {
context: "pcm samples",
});
}
Ok(Self {
rate,
layout,
samples,
})
}
pub fn silence(rate: SampleRate, layout: ChannelLayout, frames: usize) -> Result<Self> {
let len = frames.checked_mul(layout.channels()).ok_or_else(|| {
Error::validation(format!(
"silent clip of {frames} frames over {layout} overflows usize"
))
})?;
Ok(Self {
rate,
layout,
samples: vec![0.0; len],
})
}
pub fn rate(&self) -> SampleRate {
self.rate
}
pub fn layout(&self) -> ChannelLayout {
self.layout
}
pub fn channels(&self) -> usize {
self.layout.channels()
}
pub fn samples(&self) -> &[f32] {
&self.samples
}
pub fn into_samples(self) -> Vec<f32> {
self.samples
}
pub fn frames(&self) -> usize {
self.samples.len() / self.layout.channels()
}
pub fn len_samples(&self) -> Samples {
Samples(self.frames() as u64)
}
pub fn duration_seconds(&self) -> f64 {
self.frames() as f64 / f64::from(self.rate.get())
}
}
#[derive(Deserialize)]
struct PcmRepr {
rate: SampleRate,
layout: ChannelLayout,
samples: Vec<f32>,
}
impl TryFrom<PcmRepr> for Pcm {
type Error = Error;
fn try_from(repr: PcmRepr) -> Result<Self> {
Pcm::new(repr.rate, repr.layout, repr.samples)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct PcmBlock(Pcm);
impl PcmBlock {
pub fn new(pcm: Pcm) -> Self {
Self(pcm)
}
pub fn pcm(&self) -> &Pcm {
&self.0
}
pub fn into_pcm(self) -> Pcm {
self.0
}
pub fn concat(blocks: impl IntoIterator<Item = PcmBlock>) -> Result<Pcm> {
let mut blocks = blocks.into_iter();
let Some(first) = blocks.next() else {
return Err(Error::validation("cannot concatenate zero pcm blocks"));
};
let (rate, layout) = (first.0.rate, first.0.layout);
let mut samples = first.0.samples;
for block in blocks {
if block.0.rate != rate || block.0.layout != layout {
return Err(Error::validation(format!(
"pcm block clock/layout mismatch: {} {} vs {} {}",
rate, layout, block.0.rate, block.0.layout
)));
}
samples.extend_from_slice(&block.0.samples);
}
Pcm::new(rate, layout, samples)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rate(hz: u32) -> SampleRate {
SampleRate::try_from(hz).unwrap()
}
#[test]
fn frames_and_duration() {
let p = Pcm::silence(rate(8_000), ChannelLayout::Stereo, 4_000).unwrap();
assert_eq!(p.samples().len(), 8_000);
assert_eq!(p.frames(), 4_000);
assert!((p.duration_seconds() - 0.5).abs() < 1e-12);
}
#[test]
fn new_rejects_partial_frames_and_non_finite() {
assert!(Pcm::new(rate(8_000), ChannelLayout::Stereo, vec![0.0; 3]).is_err());
assert!(matches!(
Pcm::new(rate(8_000), ChannelLayout::Mono, vec![0.0, f32::NAN]),
Err(Error::NonFinite { .. })
));
assert!(Pcm::new(rate(8_000), ChannelLayout::Stereo, vec![0.0; 4]).is_ok());
assert!(Pcm::silence(rate(8_000), ChannelLayout::Stereo, usize::MAX).is_err());
}
#[test]
fn layout_is_closed_and_checked() {
assert_eq!(
ChannelLayout::from_channels(1).unwrap(),
ChannelLayout::Mono
);
assert_eq!(
ChannelLayout::from_channels(2).unwrap(),
ChannelLayout::Stereo
);
assert!(ChannelLayout::from_channels(0).is_err());
assert!(ChannelLayout::from_channels(6).is_err());
}
#[test]
fn deserialize_enforces_the_invariant() {
let good = r#"{"rate":8000,"layout":"stereo","samples":[0.0,1.0,0.5,0.25]}"#;
let p: Pcm = serde_json::from_str(good).unwrap();
assert_eq!(p.frames(), 2);
let partial = r#"{"rate":8000,"layout":"stereo","samples":[0.0,1.0,0.5]}"#;
assert!(serde_json::from_str::<Pcm>(partial).is_err());
let zero_rate = r#"{"rate":0,"layout":"mono","samples":[0.0]}"#;
assert!(serde_json::from_str::<Pcm>(zero_rate).is_err());
}
#[test]
fn block_concat_joins_and_checks() {
let a = PcmBlock::new(Pcm::new(rate(8_000), ChannelLayout::Mono, vec![1.0, 2.0]).unwrap());
let b = PcmBlock::new(Pcm::new(rate(8_000), ChannelLayout::Mono, vec![3.0]).unwrap());
let joined = PcmBlock::concat([a.clone(), b]).unwrap();
assert_eq!(joined.samples(), &[1.0, 2.0, 3.0]);
let other_rate =
PcmBlock::new(Pcm::new(rate(16_000), ChannelLayout::Mono, vec![0.0]).unwrap());
assert!(PcmBlock::concat([a, other_rate]).is_err());
assert!(PcmBlock::concat(std::iter::empty::<PcmBlock>()).is_err());
}
}