use std::ops::Add;
use std::time::Duration;
use sim_lib_pitch_core::Pitch;
use thiserror::Error;
#[derive(Debug, Error, Clone, PartialEq)]
pub enum SoundCoreError {
#[error("frequency must be positive")]
InvalidFrequency,
#[error("amplitude must be non-negative")]
InvalidAmplitude,
#[error("phase must be finite")]
InvalidPhase,
#[error("partial tag is invalid")]
InvalidPartialTag,
#[error("envelope sustain must be between 0.0 and 1.0")]
InvalidSustain,
#[error("tone duration must be positive")]
InvalidDuration,
#[error("tone must contain at least one partial")]
EmptyPartials,
#[error("time-stretch factor must be positive")]
InvalidStretch,
}
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub struct Frequency(pub f64);
impl Frequency {
pub fn new(hz: f64) -> Result<Self, SoundCoreError> {
if hz.is_finite() && hz > 0.0 {
Ok(Self(hz))
} else {
Err(SoundCoreError::InvalidFrequency)
}
}
pub fn ratio(self, other: Frequency) -> f64 {
self.0 / other.0
}
pub fn cents_above(self, other: Frequency) -> f64 {
1200.0 * self.ratio(other).log2()
}
pub fn shift_cents(self, cents: f64) -> Frequency {
Frequency(self.0 * 2.0_f64.powf(cents / 1200.0))
}
}
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub struct Amplitude(pub f64);
impl Amplitude {
pub fn new(linear: f64) -> Result<Self, SoundCoreError> {
if linear.is_finite() && linear >= 0.0 {
Ok(Self(linear))
} else {
Err(SoundCoreError::InvalidAmplitude)
}
}
pub fn from_db(db: f64) -> Self {
Self(10f64.powf(db / 20.0))
}
pub fn to_db(self) -> f64 {
20.0 * self.0.log10()
}
}
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub struct Phase(pub f64);
impl Phase {
pub fn new(radians: f64) -> Result<Self, SoundCoreError> {
if radians.is_finite() {
Ok(Self(radians).normalized())
} else {
Err(SoundCoreError::InvalidPhase)
}
}
pub fn normalized(self) -> Self {
let tau = std::f64::consts::TAU;
Self(self.0.rem_euclid(tau))
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum PartialTag {
Source,
Harmonic(u32),
Undertone(u32),
}
impl PartialTag {
pub fn harmonic(index: u32) -> Result<Self, SoundCoreError> {
if index > 0 {
Ok(Self::Harmonic(index))
} else {
Err(SoundCoreError::InvalidPartialTag)
}
}
pub fn undertone(index: u32) -> Result<Self, SoundCoreError> {
if index > 0 {
Ok(Self::Undertone(index))
} else {
Err(SoundCoreError::InvalidPartialTag)
}
}
fn validate(self) -> Result<Self, SoundCoreError> {
match self {
Self::Source => Ok(self),
Self::Harmonic(index) | Self::Undertone(index) if index > 0 => Ok(self),
Self::Harmonic(_) | Self::Undertone(_) => Err(SoundCoreError::InvalidPartialTag),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Partial {
pub frequency: Frequency,
pub amplitude: Amplitude,
pub phase: Phase,
pub tag: PartialTag,
}
impl Partial {
pub fn new(
frequency: Frequency,
amplitude: Amplitude,
phase: Phase,
) -> Result<Self, SoundCoreError> {
Self::tagged(frequency, amplitude, phase, PartialTag::Source)
}
pub fn tagged(
frequency: Frequency,
amplitude: Amplitude,
phase: Phase,
tag: PartialTag,
) -> Result<Self, SoundCoreError> {
let _ = Frequency::new(frequency.0)?;
let _ = Amplitude::new(amplitude.0)?;
let phase = Phase::new(phase.0)?;
let tag = tag.validate()?;
Ok(Self {
frequency,
amplitude,
phase,
tag,
})
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum EnvelopeShape {
Linear,
Exponential(f64),
Custom(String),
}
#[derive(Clone, Debug, PartialEq)]
pub struct Envelope {
pub attack: Duration,
pub decay: Duration,
pub sustain: f64,
pub release: Duration,
pub shape: EnvelopeShape,
}
impl Envelope {
pub fn new(
attack: Duration,
decay: Duration,
sustain: f64,
release: Duration,
shape: EnvelopeShape,
) -> Result<Self, SoundCoreError> {
if !sustain.is_finite() || !(0.0..=1.0).contains(&sustain) {
return Err(SoundCoreError::InvalidSustain);
}
Ok(Self {
attack,
decay,
sustain,
release,
shape,
})
}
pub fn sample_level(&self, t: Duration, total: Duration) -> f64 {
let elapsed = t.as_secs_f64();
let attack = self.attack.as_secs_f64();
let decay = self.decay.as_secs_f64();
let release = self.release.as_secs_f64();
let total_secs = total.as_secs_f64();
let release_start = (total_secs - release).max(0.0);
match &self.shape {
EnvelopeShape::Linear | EnvelopeShape::Custom(_) => {
if attack > 0.0 && elapsed < attack {
elapsed / attack
} else if decay > 0.0 && elapsed < attack + decay {
let progress = (elapsed - attack) / decay;
1.0 + (self.sustain - 1.0) * progress
} else if elapsed < release_start {
self.sustain
} else if release > 0.0 && elapsed <= total_secs {
let progress = ((elapsed - release_start) / release).clamp(0.0, 1.0);
self.sustain * (1.0 - progress)
} else {
0.0
}
}
EnvelopeShape::Exponential(curve) => {
let base = self.clone().with_shape(EnvelopeShape::Linear);
base.sample_level(t, total).powf((*curve).max(0.01))
}
}
}
fn with_shape(mut self, shape: EnvelopeShape) -> Self {
self.shape = shape;
self
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Tone {
pub partials: Vec<Partial>,
pub envelope: Envelope,
pub duration: Duration,
}
impl Tone {
pub fn sine(frequency: Frequency, duration: Duration) -> Self {
Self::from_partials(
vec![Partial {
frequency,
amplitude: Amplitude(1.0),
phase: Phase(0.0),
tag: PartialTag::Source,
}],
default_envelope(),
duration,
)
.expect("sine tone is valid")
}
pub fn sawtooth(frequency: Frequency, duration: Duration, partials: usize) -> Self {
Self::harmonic_series(frequency, duration, partials, |n| 1.0 / n as f64, |_| true)
}
pub fn square(frequency: Frequency, duration: Duration, partials: usize) -> Self {
Self::harmonic_series(
frequency,
duration,
partials,
|n| 1.0 / n as f64,
|n| n % 2 == 1,
)
}
pub fn triangle(frequency: Frequency, duration: Duration, partials: usize) -> Self {
Self::harmonic_series(
frequency,
duration,
partials,
|n| 1.0 / ((n * n) as f64),
|n| n % 2 == 1,
)
}
pub fn from_partials(
partials: Vec<Partial>,
envelope: Envelope,
duration: Duration,
) -> Result<Self, SoundCoreError> {
if duration.is_zero() {
return Err(SoundCoreError::InvalidDuration);
}
if partials.is_empty() {
return Err(SoundCoreError::EmptyPartials);
}
let partials = partials
.into_iter()
.map(|partial| {
Partial::tagged(
partial.frequency,
partial.amplitude,
partial.phase,
partial.tag,
)
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Self {
partials,
envelope,
duration,
})
}
pub fn transpose_cents(mut self, cents: f64) -> Self {
for partial in &mut self.partials {
partial.frequency = partial.frequency.shift_cents(cents);
}
self
}
pub fn amplify(mut self, gain: f64) -> Self {
for partial in &mut self.partials {
partial.amplitude = Amplitude(partial.amplitude.0 * gain);
}
self
}
pub fn time_stretch(mut self, factor: f64) -> Result<Self, SoundCoreError> {
if !factor.is_finite() || factor <= 0.0 {
return Err(SoundCoreError::InvalidStretch);
}
self.duration = Duration::from_secs_f64(self.duration.as_secs_f64() * factor);
self.envelope.attack = Duration::from_secs_f64(self.envelope.attack.as_secs_f64() * factor);
self.envelope.decay = Duration::from_secs_f64(self.envelope.decay.as_secs_f64() * factor);
self.envelope.release =
Duration::from_secs_f64(self.envelope.release.as_secs_f64() * factor);
Ok(self)
}
fn harmonic_series(
frequency: Frequency,
duration: Duration,
partial_count: usize,
amp: impl Fn(usize) -> f64,
include: impl Fn(usize) -> bool,
) -> Self {
let partials = (1..=partial_count.max(1))
.filter(|n| include(*n))
.map(|n| Partial {
frequency: Frequency(frequency.0 * n as f64),
amplitude: Amplitude(amp(n)),
phase: Phase(0.0),
tag: PartialTag::Harmonic(n as u32),
})
.collect();
Self::from_partials(partials, default_envelope(), duration)
.expect("harmonic-series tone is valid")
}
}
impl Add for Tone {
type Output = Self;
fn add(mut self, other: Self) -> Self::Output {
self.partials.extend(other.partials);
self.duration = self.duration.max(other.duration);
self
}
}
pub fn default_envelope() -> Envelope {
Envelope::new(
Duration::from_millis(10),
Duration::from_millis(50),
0.8,
Duration::from_millis(100),
EnvelopeShape::Linear,
)
.expect("default envelope is valid")
}
pub fn equal_temperament_frequency(pitch: Pitch) -> Frequency {
let semitones = pitch.semitone() - 69;
Frequency(440.0 * 2.0_f64.powf(semitones as f64 / 12.0))
}