warbler 0.0.1

procedural bird chirp synthesizer using sdl3
Documentation
use sdl3::audio::{AudioCallback, AudioFormat, AudioSpec, AudioStream};
use std::f32::consts::PI;

#[derive(Debug, Clone)]
pub struct ChirpParams {
    pub start_freq_hz: f32,
    pub end_freq_hz: f32,
    pub duration_ms: u32,
    pub volume: f32,
    pub attack_ratio: f32,
    pub decay_ratio: f32,
    pub harmonics: Vec<f32>,
    pub vibrato_rate_hz: f32,
    pub vibrato_depth: f32,
}

impl Default for ChirpParams {
    fn default() -> Self {
        Self {
            start_freq_hz: 2000.0,
            end_freq_hz: 4000.0,
            duration_ms: 200,
            volume: 0.3,
            attack_ratio: 0.1,
            decay_ratio: 0.3,
            harmonics: vec![1.0, 0.3, 0.1],
            vibrato_rate_hz: 0.0,
            vibrato_depth: 0.0,
        }
    }
}

impl ChirpParams {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn start_freq(mut self, hz: f32) -> Self {
        self.start_freq_hz = hz;
        self
    }

    pub fn end_freq(mut self, hz: f32) -> Self {
        self.end_freq_hz = hz;
        self
    }

    pub fn duration(mut self, ms: u32) -> Self {
        self.duration_ms = ms;
        self
    }

    pub fn volume(mut self, v: f32) -> Self {
        self.volume = v.clamp(0.0, 1.0);
        self
    }

    pub fn attack(mut self, ratio: f32) -> Self {
        self.attack_ratio = ratio.clamp(0.0, 1.0);
        self
    }

    pub fn decay(mut self, ratio: f32) -> Self {
        self.decay_ratio = ratio.clamp(0.0, 1.0);
        self
    }

    pub fn harmonics(mut self, harms: Vec<f32>) -> Self {
        self.harmonics = harms;
        self
    }

    pub fn vibrato(mut self, rate_hz: f32, depth: f32) -> Self {
        self.vibrato_rate_hz = rate_hz;
        self.vibrato_depth = depth;
        self
    }
}

pub fn sparrow() -> ChirpParams {
    ChirpParams::new()
        .start_freq(2500.0)
        .end_freq(5000.0)
        .duration(150)
        .attack(0.05)
        .decay(0.4)
        .harmonics(vec![1.0, 0.4, 0.15, 0.05])
}

pub fn robin() -> ChirpParams {
    ChirpParams::new()
        .start_freq(2000.0)
        .end_freq(3500.0)
        .duration(300)
        .attack(0.15)
        .decay(0.25)
        .harmonics(vec![1.0, 0.5, 0.2])
        .vibrato(8.0, 0.05)
}

pub fn warbler() -> ChirpParams {
    ChirpParams::new()
        .start_freq(3000.0)
        .end_freq(6000.0)
        .duration(180)
        .attack(0.08)
        .decay(0.35)
        .harmonics(vec![1.0, 0.6, 0.25, 0.1, 0.03])
        .vibrato(12.0, 0.08)
}

pub fn finch() -> ChirpParams {
    ChirpParams::new()
        .start_freq(3500.0)
        .end_freq(7000.0)
        .duration(120)
        .attack(0.03)
        .decay(0.5)
        .harmonics(vec![1.0, 0.3, 0.1])
}

struct BirdChirp {
    params: ChirpParams,
    sample_rate: f32,
    phase: f32,
    sample_index: usize,
    total_samples: usize,
    buffer: Vec<f32>,
}

impl BirdChirp {
    fn new(params: ChirpParams, sample_rate: f32) -> Self {
        let total_samples = (params.duration_ms as f32 * sample_rate / 1000.0) as usize;
        Self {
            params,
            sample_rate,
            phase: 0.0,
            sample_index: 0,
            total_samples,
            buffer: Vec::new(),
        }
    }

    fn get_frequency(&self) -> f32 {
        let progress = self.sample_index as f32 / self.total_samples as f32;
        let freq_range = self.params.end_freq_hz - self.params.start_freq_hz;
        let base_freq = self.params.start_freq_hz + freq_range * progress;

        if self.params.vibrato_rate_hz > 0.0 {
            let vibrato_phase = 2.0 * PI * self.params.vibrato_rate_hz * self.sample_index as f32
                / self.sample_rate;
            let vibrato = vibrato_phase.sin() * self.params.vibrato_depth;
            base_freq * (1.0 + vibrato)
        } else {
            base_freq
        }
    }

    fn get_envelope(&self) -> f32 {
        let progress = self.sample_index as f32 / self.total_samples as f32;
        let attack_samples = (self.params.attack_ratio * self.total_samples as f32) as usize;
        let decay_start = 1.0 - self.params.decay_ratio;

        if self.sample_index < attack_samples {
            self.sample_index as f32 / attack_samples as f32
        } else if progress > decay_start {
            let decay_progress = (progress - decay_start) / self.params.decay_ratio;
            1.0 - decay_progress
        } else {
            1.0
        }
    }

    fn generate_sample(&mut self) -> f32 {
        if self.sample_index >= self.total_samples {
            return 0.0;
        }

        let freq = self.get_frequency();
        let envelope = self.get_envelope();

        let mut sample = 0.0;
        for (i, &harmonic_amp) in self.params.harmonics.iter().enumerate() {
            let harmonic_phase = self.phase * (i + 1) as f32;
            sample += (2.0 * PI * harmonic_phase).sin() * harmonic_amp;
        }

        let normalizer = self.params.harmonics.iter().sum::<f32>();
        sample = sample / normalizer * envelope * self.params.volume;

        self.phase += freq / self.sample_rate;
        self.phase %= 1.0;
        self.sample_index += 1;

        sample
    }
}

impl AudioCallback<f32> for BirdChirp {
    fn callback(&mut self, stream: &mut AudioStream, requested: i32) {
        self.buffer.resize(requested as usize, 0.0);

        for i in 0..requested as usize {
            self.buffer[i] = self.generate_sample();
        }

        stream.put_data_f32(&self.buffer).unwrap();
    }
}

pub struct Synthesizer {
    sdl_context: sdl3::Sdl,
}

impl Synthesizer {
    pub fn new() -> Result<Self, String> {
        let sdl_context = sdl3::init().map_err(|e| e.to_string())?;
        Ok(Self { sdl_context })
    }

    pub fn play_chirp(&self, params: ChirpParams) -> Result<(), Box<dyn std::error::Error>> {
        let audio_subsystem = self.sdl_context.audio()?;

        let sample_rate = 48000;
        let desired_spec = AudioSpec {
            freq: Some(sample_rate),
            channels: Some(1),
            format: Some(AudioFormat::f32_sys()),
        };

        let chirp = BirdChirp::new(params.clone(), sample_rate as f32);
        let device = audio_subsystem.open_playback_stream(&desired_spec, chirp)?;

        device.resume()?;

        let duration = std::time::Duration::from_millis(params.duration_ms as u64);
        std::thread::sleep(duration);

        Ok(())
    }
}