1use std::ops::Add;
2use std::time::Duration;
3
4use sim_lib_pitch_core::Pitch;
5use thiserror::Error;
6
7#[derive(Debug, Error, Clone, PartialEq)]
9pub enum SoundCoreError {
10 #[error("frequency must be positive")]
12 InvalidFrequency,
13 #[error("amplitude must be non-negative")]
15 InvalidAmplitude,
16 #[error("phase must be finite")]
18 InvalidPhase,
19 #[error("partial tag is invalid")]
21 InvalidPartialTag,
22 #[error("envelope sustain must be between 0.0 and 1.0")]
24 InvalidSustain,
25 #[error("tone duration must be positive")]
27 InvalidDuration,
28 #[error("tone must contain at least one partial")]
30 EmptyPartials,
31 #[error("time-stretch factor must be positive")]
33 InvalidStretch,
34}
35
36#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
49pub struct Frequency(pub f64);
50
51impl Frequency {
52 pub fn new(hz: f64) -> Result<Self, SoundCoreError> {
55 if hz.is_finite() && hz > 0.0 {
56 Ok(Self(hz))
57 } else {
58 Err(SoundCoreError::InvalidFrequency)
59 }
60 }
61
62 pub fn ratio(self, other: Frequency) -> f64 {
64 self.0 / other.0
65 }
66
67 pub fn cents_above(self, other: Frequency) -> f64 {
69 1200.0 * self.ratio(other).log2()
70 }
71
72 pub fn shift_cents(self, cents: f64) -> Frequency {
75 Frequency(self.0 * 2.0_f64.powf(cents / 1200.0))
76 }
77}
78
79#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
91pub struct Amplitude(pub f64);
92
93impl Amplitude {
94 pub fn new(linear: f64) -> Result<Self, SoundCoreError> {
97 if linear.is_finite() && linear >= 0.0 {
98 Ok(Self(linear))
99 } else {
100 Err(SoundCoreError::InvalidAmplitude)
101 }
102 }
103
104 pub fn from_db(db: f64) -> Self {
106 Self(10f64.powf(db / 20.0))
107 }
108
109 pub fn to_db(self) -> f64 {
111 20.0 * self.0.log10()
112 }
113}
114
115#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
117pub struct Phase(pub f64);
118
119impl Phase {
120 pub fn new(radians: f64) -> Result<Self, SoundCoreError> {
123 if radians.is_finite() {
124 Ok(Self(radians).normalized())
125 } else {
126 Err(SoundCoreError::InvalidPhase)
127 }
128 }
129
130 pub fn normalized(self) -> Self {
132 let tau = std::f64::consts::TAU;
133 Self(self.0.rem_euclid(tau))
134 }
135}
136
137#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
139pub enum PartialTag {
140 Source,
142 Harmonic(u32),
145 Undertone(u32),
147}
148
149impl PartialTag {
150 pub fn harmonic(index: u32) -> Result<Self, SoundCoreError> {
152 if index > 0 {
153 Ok(Self::Harmonic(index))
154 } else {
155 Err(SoundCoreError::InvalidPartialTag)
156 }
157 }
158
159 pub fn undertone(index: u32) -> Result<Self, SoundCoreError> {
161 if index > 0 {
162 Ok(Self::Undertone(index))
163 } else {
164 Err(SoundCoreError::InvalidPartialTag)
165 }
166 }
167
168 fn validate(self) -> Result<Self, SoundCoreError> {
169 match self {
170 Self::Source => Ok(self),
171 Self::Harmonic(index) | Self::Undertone(index) if index > 0 => Ok(self),
172 Self::Harmonic(_) | Self::Undertone(_) => Err(SoundCoreError::InvalidPartialTag),
173 }
174 }
175}
176
177#[derive(Copy, Clone, Debug, PartialEq)]
179pub struct Partial {
180 pub frequency: Frequency,
182 pub amplitude: Amplitude,
184 pub phase: Phase,
186 pub tag: PartialTag,
188}
189
190impl Partial {
191 pub fn new(
194 frequency: Frequency,
195 amplitude: Amplitude,
196 phase: Phase,
197 ) -> Result<Self, SoundCoreError> {
198 Self::tagged(frequency, amplitude, phase, PartialTag::Source)
199 }
200
201 pub fn tagged(
204 frequency: Frequency,
205 amplitude: Amplitude,
206 phase: Phase,
207 tag: PartialTag,
208 ) -> Result<Self, SoundCoreError> {
209 let _ = Frequency::new(frequency.0)?;
210 let _ = Amplitude::new(amplitude.0)?;
211 let phase = Phase::new(phase.0)?;
212 let tag = tag.validate()?;
213 Ok(Self {
214 frequency,
215 amplitude,
216 phase,
217 tag,
218 })
219 }
220}
221
222#[derive(Clone, Debug, PartialEq)]
224pub enum EnvelopeShape {
225 Linear,
227 Exponential(f64),
229 Custom(String),
231}
232
233#[derive(Clone, Debug, PartialEq)]
235pub struct Envelope {
236 pub attack: Duration,
238 pub decay: Duration,
240 pub sustain: f64,
242 pub release: Duration,
244 pub shape: EnvelopeShape,
246}
247
248impl Envelope {
249 pub fn new(
252 attack: Duration,
253 decay: Duration,
254 sustain: f64,
255 release: Duration,
256 shape: EnvelopeShape,
257 ) -> Result<Self, SoundCoreError> {
258 if !sustain.is_finite() || !(0.0..=1.0).contains(&sustain) {
259 return Err(SoundCoreError::InvalidSustain);
260 }
261 Ok(Self {
262 attack,
263 decay,
264 sustain,
265 release,
266 shape,
267 })
268 }
269
270 pub fn sample_level(&self, t: Duration, total: Duration) -> f64 {
273 let elapsed = t.as_secs_f64();
274 let attack = self.attack.as_secs_f64();
275 let decay = self.decay.as_secs_f64();
276 let release = self.release.as_secs_f64();
277 let total_secs = total.as_secs_f64();
278 let release_start = (total_secs - release).max(0.0);
279 match &self.shape {
280 EnvelopeShape::Linear | EnvelopeShape::Custom(_) => {
281 if attack > 0.0 && elapsed < attack {
282 elapsed / attack
283 } else if decay > 0.0 && elapsed < attack + decay {
284 let progress = (elapsed - attack) / decay;
285 1.0 + (self.sustain - 1.0) * progress
286 } else if elapsed < release_start {
287 self.sustain
288 } else if release > 0.0 && elapsed <= total_secs {
289 let progress = ((elapsed - release_start) / release).clamp(0.0, 1.0);
290 self.sustain * (1.0 - progress)
291 } else {
292 0.0
293 }
294 }
295 EnvelopeShape::Exponential(curve) => {
296 let base = self.clone().with_shape(EnvelopeShape::Linear);
297 base.sample_level(t, total).powf((*curve).max(0.01))
298 }
299 }
300 }
301
302 fn with_shape(mut self, shape: EnvelopeShape) -> Self {
303 self.shape = shape;
304 self
305 }
306}
307
308#[derive(Clone, Debug, PartialEq)]
311pub struct Tone {
312 pub partials: Vec<Partial>,
314 pub envelope: Envelope,
316 pub duration: Duration,
318}
319
320impl Tone {
321 pub fn sine(frequency: Frequency, duration: Duration) -> Self {
323 Self::from_partials(
324 vec![Partial {
325 frequency,
326 amplitude: Amplitude(1.0),
327 phase: Phase(0.0),
328 tag: PartialTag::Source,
329 }],
330 default_envelope(),
331 duration,
332 )
333 .expect("sine tone is valid")
334 }
335
336 pub fn sawtooth(frequency: Frequency, duration: Duration, partials: usize) -> Self {
338 Self::harmonic_series(frequency, duration, partials, |n| 1.0 / n as f64, |_| true)
339 }
340
341 pub fn square(frequency: Frequency, duration: Duration, partials: usize) -> Self {
344 Self::harmonic_series(
345 frequency,
346 duration,
347 partials,
348 |n| 1.0 / n as f64,
349 |n| n % 2 == 1,
350 )
351 }
352
353 pub fn triangle(frequency: Frequency, duration: Duration, partials: usize) -> Self {
356 Self::harmonic_series(
357 frequency,
358 duration,
359 partials,
360 |n| 1.0 / ((n * n) as f64),
361 |n| n % 2 == 1,
362 )
363 }
364
365 pub fn from_partials(
368 partials: Vec<Partial>,
369 envelope: Envelope,
370 duration: Duration,
371 ) -> Result<Self, SoundCoreError> {
372 if duration.is_zero() {
373 return Err(SoundCoreError::InvalidDuration);
374 }
375 if partials.is_empty() {
376 return Err(SoundCoreError::EmptyPartials);
377 }
378 let partials = partials
379 .into_iter()
380 .map(|partial| {
381 Partial::tagged(
382 partial.frequency,
383 partial.amplitude,
384 partial.phase,
385 partial.tag,
386 )
387 })
388 .collect::<Result<Vec<_>, _>>()?;
389 Ok(Self {
390 partials,
391 envelope,
392 duration,
393 })
394 }
395
396 pub fn transpose_cents(mut self, cents: f64) -> Self {
398 for partial in &mut self.partials {
399 partial.frequency = partial.frequency.shift_cents(cents);
400 }
401 self
402 }
403
404 pub fn amplify(mut self, gain: f64) -> Self {
406 for partial in &mut self.partials {
407 partial.amplitude = Amplitude(partial.amplitude.0 * gain);
408 }
409 self
410 }
411
412 pub fn time_stretch(mut self, factor: f64) -> Result<Self, SoundCoreError> {
415 if !factor.is_finite() || factor <= 0.0 {
416 return Err(SoundCoreError::InvalidStretch);
417 }
418 self.duration = Duration::from_secs_f64(self.duration.as_secs_f64() * factor);
419 self.envelope.attack = Duration::from_secs_f64(self.envelope.attack.as_secs_f64() * factor);
420 self.envelope.decay = Duration::from_secs_f64(self.envelope.decay.as_secs_f64() * factor);
421 self.envelope.release =
422 Duration::from_secs_f64(self.envelope.release.as_secs_f64() * factor);
423 Ok(self)
424 }
425
426 fn harmonic_series(
427 frequency: Frequency,
428 duration: Duration,
429 partial_count: usize,
430 amp: impl Fn(usize) -> f64,
431 include: impl Fn(usize) -> bool,
432 ) -> Self {
433 let partials = (1..=partial_count.max(1))
434 .filter(|n| include(*n))
435 .map(|n| Partial {
436 frequency: Frequency(frequency.0 * n as f64),
437 amplitude: Amplitude(amp(n)),
438 phase: Phase(0.0),
439 tag: PartialTag::Harmonic(n as u32),
440 })
441 .collect();
442 Self::from_partials(partials, default_envelope(), duration)
443 .expect("harmonic-series tone is valid")
444 }
445}
446
447impl Add for Tone {
448 type Output = Self;
449
450 fn add(mut self, other: Self) -> Self::Output {
451 self.partials.extend(other.partials);
452 self.duration = self.duration.max(other.duration);
453 self
454 }
455}
456
457pub fn default_envelope() -> Envelope {
460 Envelope::new(
461 Duration::from_millis(10),
462 Duration::from_millis(50),
463 0.8,
464 Duration::from_millis(100),
465 EnvelopeShape::Linear,
466 )
467 .expect("default envelope is valid")
468}
469
470pub fn equal_temperament_frequency(pitch: Pitch) -> Frequency {
473 let semitones = pitch.semitone() - 69;
474 Frequency(440.0 * 2.0_f64.powf(semitones as f64 / 12.0))
475}