use thiserror::Error;
#[derive(Error, Debug)]
pub enum GeneratorError {
#[error("Invalid input: {0}")]
InvalidInput(String),
}
pub fn clicks(
times: Option<&[f32]>,
frames: Option<&[usize]>,
sr: Option<u32>,
hop_length: Option<usize>,
) -> Result<Vec<f32>, GeneratorError> {
let sample_rate = sr.unwrap_or(44100);
let hop = hop_length.unwrap_or(512);
let max_samples = if let Some(t) = times {
if t.is_empty() {
return Err(GeneratorError::InvalidInput("Times array cannot be empty".to_string()));
}
((t.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap()
* sample_rate as f32) as usize)
+ 1
} else if let Some(f) = frames {
if f.is_empty() {
return Err(GeneratorError::InvalidInput("Frames array cannot be empty".to_string()));
}
f.iter().max().unwrap() * hop + 1
} else {
44100
};
let mut signal = vec![0.0; max_samples];
if let Some(ts) = times {
for &t in ts {
let idx = (t * sample_rate as f32) as usize;
if idx < signal.len() {
signal[idx] = 1.0;
}
}
} else if let Some(fs) = frames {
for &f in fs {
let idx = f * hop;
if idx < signal.len() {
signal[idx] = 1.0;
}
}
}
Ok(signal)
}
pub fn tone(frequency: f32, sr: u32) -> ToneBuilder {
ToneBuilder {
frequency,
sr,
duration: 1.0,
phase: 0.0,
}
}
#[derive(Debug, Clone)]
pub struct ToneBuilder {
frequency: f32,
sr: u32,
duration: f32,
phase: f32,
}
impl ToneBuilder {
pub fn duration(mut self, duration: f32) -> Self {
self.duration = duration;
self
}
pub fn phase(mut self, phase: f32) -> Self {
self.phase = phase;
self
}
pub fn compute(self) -> Vec<f32> {
tone_impl(self.frequency, self.sr, self.duration, self.phase)
}
}
fn tone_impl(frequency: f32, sr: u32, duration: f32, phase: f32) -> Vec<f32> {
let len = (duration * sr as f32) as usize;
(0..len)
.map(|n| {
(2.0 * std::f32::consts::PI * frequency * n as f32 / sr as f32 + phase).cos()
})
.collect()
}
pub fn chirp(fmin: f32, fmax: f32, sr: u32) -> ChirpBuilder {
ChirpBuilder {
fmin,
fmax,
sr,
duration: 1.0,
}
}
#[derive(Debug, Clone)]
pub struct ChirpBuilder {
fmin: f32,
fmax: f32,
sr: u32,
duration: f32,
}
impl ChirpBuilder {
pub fn duration(mut self, duration: f32) -> Self {
self.duration = duration;
self
}
pub fn compute(self) -> Vec<f32> {
chirp_impl(self.fmin, self.fmax, self.sr, self.duration)
}
}
fn chirp_impl(fmin: f32, fmax: f32, sr: u32, duration: f32) -> Vec<f32> {
let len = (duration * sr as f32) as usize;
let sr_f = sr as f32;
let two_pi = 2.0 * std::f32::consts::PI;
(0..len)
.map(|n| {
let t = n as f32 / sr_f; let phase = two_pi * (fmin * t + (fmax - fmin) * t * t / (2.0 * duration));
phase.cos()
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f32, b: f32, tol: f32) -> bool {
(a - b).abs() <= tol
}
#[test]
fn clicks_from_times_and_frames() {
let signal = clicks(Some(&[0.0, 0.001]), None, Some(1000), None).unwrap();
assert_eq!(signal.len(), 2);
assert_eq!(signal, vec![1.0, 1.0]);
let frames = clicks(None, Some(&[0, 2]), Some(8000), Some(2)).unwrap();
assert_eq!(frames.len(), 5);
assert_eq!(frames[0], 1.0);
assert_eq!(frames[4], 1.0);
assert!(frames[1].abs() < f32::EPSILON);
assert!(frames[2].abs() < f32::EPSILON);
assert!(frames[3].abs() < f32::EPSILON);
}
#[test]
fn clicks_rejects_empty_inputs() {
assert!(clicks(Some(&[]), None, None, None).is_err());
assert!(clicks(None, Some(&[]), None, None).is_err());
}
#[test]
fn tone_builder_respects_duration_and_phase() {
let samples = tone(440.0, 44_100).duration(0.001).phase(std::f32::consts::FRAC_PI_2).compute();
assert_eq!(samples.len(), 44); assert!(approx_eq(samples[0], 0.0, 1e-6));
assert!(samples.iter().all(|s| s.abs() <= 1.0));
}
#[test]
fn chirp_builder_generates_expected_length() {
let samples = chirp(100.0, 200.0, 10_000).duration(0.01).compute();
assert_eq!(samples.len(), 100); assert!(samples.iter().all(|s| s.is_finite() && s.abs() <= 1.0));
}
}