Skip to main content

sim_lib_sound_render/
model.rs

1use std::io::Write;
2use std::time::Duration;
3
4use sim_lib_sound_bridge::ScheduledTone;
5use sim_lib_sound_core::Tone;
6
7use crate::SoundRenderError;
8
9/// Configuration for a [`PcmRenderer`].
10#[derive(Copy, Clone, Debug, PartialEq, Eq)]
11pub struct RendererOptions {
12    /// Output sample rate, in hertz.
13    pub sample_rate: u32,
14    /// Output channel count (1 for mono, 2 for stereo).
15    pub channels: u8,
16}
17
18impl RendererOptions {
19    /// Builds options, rejecting a zero sample rate or a channel count outside
20    /// `1..=2`.
21    pub fn new(sample_rate: u32, channels: u8) -> Result<Self, SoundRenderError> {
22        if sample_rate == 0 {
23            return Err(SoundRenderError::InvalidSampleRate);
24        }
25        if !(1..=2).contains(&channels) {
26            return Err(SoundRenderError::InvalidChannelCount);
27        }
28        Ok(Self {
29            sample_rate,
30            channels,
31        })
32    }
33}
34
35impl Default for RendererOptions {
36    fn default() -> Self {
37        Self {
38            sample_rate: 44_100,
39            channels: 2,
40        }
41    }
42}
43
44/// A renderer that synthesizes tones into interleaved PCM `f32` samples and
45/// encodes them as WAV.
46#[derive(Copy, Clone, Debug, PartialEq, Eq)]
47pub struct PcmRenderer {
48    /// Output sample rate, in hertz.
49    pub sample_rate: u32,
50    /// Output channel count (1 for mono, 2 for stereo).
51    pub channels: u8,
52}
53
54impl PcmRenderer {
55    /// Builds a renderer from validated [`RendererOptions`].
56    pub fn new(options: RendererOptions) -> Result<Self, SoundRenderError> {
57        let _ = RendererOptions::new(options.sample_rate, options.channels)?;
58        Ok(Self {
59            sample_rate: options.sample_rate,
60            channels: options.channels,
61        })
62    }
63
64    /// Renders a single tone to interleaved PCM samples, centered in the
65    /// stereo field.
66    ///
67    /// # Examples
68    ///
69    /// ```
70    /// use std::time::Duration;
71    /// use sim_lib_sound_core::{Frequency, Tone};
72    /// use sim_lib_sound_render::{PcmRenderer, RendererOptions};
73    ///
74    /// let renderer = PcmRenderer::new(RendererOptions::new(8_000, 1).unwrap()).unwrap();
75    /// let tone = Tone::sine(Frequency::new(440.0).unwrap(), Duration::from_millis(10));
76    /// assert_eq!(renderer.render_tone(&tone).len(), 80);
77    /// ```
78    pub fn render_tone(&self, tone: &Tone) -> Vec<f32> {
79        self.render_tone_with_pan(tone, 0.0)
80    }
81
82    /// Renders and sums a set of scheduled tones into a single mixed PCM
83    /// buffer, honoring each tone's start time and pan.
84    pub fn render_mix(&self, tones: &[ScheduledTone]) -> Vec<f32> {
85        let frames = tones
86            .iter()
87            .map(|scheduled| {
88                start_frame(self.sample_rate, scheduled.start)
89                    + tone_frames(self.sample_rate, &scheduled.tone)
90            })
91            .max()
92            .unwrap_or(0);
93        let mut mix = vec![0.0_f32; frames * usize::from(self.channels)];
94        for scheduled in tones {
95            let rendered = self.render_tone_with_pan(&scheduled.tone, scheduled.pan);
96            let offset =
97                start_frame(self.sample_rate, scheduled.start) * usize::from(self.channels);
98            for (index, sample) in rendered.iter().enumerate() {
99                if let Some(slot) = mix.get_mut(offset + index) {
100                    *slot += *sample;
101                }
102            }
103        }
104        mix
105    }
106
107    /// Encodes `samples` as a 16-bit PCM WAV stream to `writer`, returning the
108    /// writer on success.
109    pub fn write_wav<W: Write>(
110        &self,
111        samples: &[f32],
112        mut writer: W,
113    ) -> Result<W, SoundRenderError> {
114        let sample_count =
115            u32::try_from(samples.len()).map_err(|_| SoundRenderError::BufferTooLarge)?;
116        let bytes_per_sample = 2_u16;
117        let block_align = self.channels as u16 * bytes_per_sample;
118        let byte_rate = self.sample_rate * u32::from(block_align);
119        let data_size = sample_count * u32::from(bytes_per_sample);
120        let riff_size = 36_u32 + data_size;
121        writer
122            .write_all(b"RIFF")
123            .map_err(|_| SoundRenderError::BufferTooLarge)?;
124        writer
125            .write_all(&riff_size.to_le_bytes())
126            .map_err(|_| SoundRenderError::BufferTooLarge)?;
127        writer
128            .write_all(b"WAVE")
129            .map_err(|_| SoundRenderError::BufferTooLarge)?;
130        writer
131            .write_all(b"fmt ")
132            .map_err(|_| SoundRenderError::BufferTooLarge)?;
133        writer
134            .write_all(&16_u32.to_le_bytes())
135            .map_err(|_| SoundRenderError::BufferTooLarge)?;
136        writer
137            .write_all(&1_u16.to_le_bytes())
138            .map_err(|_| SoundRenderError::BufferTooLarge)?;
139        writer
140            .write_all(&(self.channels as u16).to_le_bytes())
141            .map_err(|_| SoundRenderError::BufferTooLarge)?;
142        writer
143            .write_all(&self.sample_rate.to_le_bytes())
144            .map_err(|_| SoundRenderError::BufferTooLarge)?;
145        writer
146            .write_all(&byte_rate.to_le_bytes())
147            .map_err(|_| SoundRenderError::BufferTooLarge)?;
148        writer
149            .write_all(&block_align.to_le_bytes())
150            .map_err(|_| SoundRenderError::BufferTooLarge)?;
151        writer
152            .write_all(&(bytes_per_sample * 8).to_le_bytes())
153            .map_err(|_| SoundRenderError::BufferTooLarge)?;
154        writer
155            .write_all(b"data")
156            .map_err(|_| SoundRenderError::BufferTooLarge)?;
157        writer
158            .write_all(&data_size.to_le_bytes())
159            .map_err(|_| SoundRenderError::BufferTooLarge)?;
160        for sample in samples {
161            let pcm = (sample.clamp(-1.0, 1.0) * f32::from(i16::MAX)).round() as i16;
162            writer
163                .write_all(&pcm.to_le_bytes())
164                .map_err(|_| SoundRenderError::BufferTooLarge)?;
165        }
166        Ok(writer)
167    }
168
169    fn render_tone_with_pan(&self, tone: &Tone, pan: f32) -> Vec<f32> {
170        let frames = tone_frames(self.sample_rate, tone);
171        let mut out = vec![0.0_f32; frames * usize::from(self.channels)];
172        let (left_gain, right_gain) = pan_gains(pan);
173        for frame in 0..frames {
174            let time = Duration::from_secs_f64(frame as f64 / f64::from(self.sample_rate));
175            let env = tone.envelope.sample_level(time, tone.duration) as f32;
176            let mut mono = 0.0_f32;
177            for partial in &tone.partials {
178                let angle = std::f64::consts::TAU * partial.frequency.0 * time.as_secs_f64()
179                    + partial.phase.0;
180                mono += (angle.sin() * partial.amplitude.0) as f32;
181            }
182            let sample = mono * env;
183            match self.channels {
184                1 => out[frame] = sample,
185                2 => {
186                    let base = frame * 2;
187                    out[base] = sample * left_gain;
188                    out[base + 1] = sample * right_gain;
189                }
190                _ => unreachable!(),
191            }
192        }
193        out
194    }
195}
196
197fn tone_frames(sample_rate: u32, tone: &Tone) -> usize {
198    (tone.duration.as_secs_f64() * f64::from(sample_rate)).ceil() as usize
199}
200
201fn start_frame(sample_rate: u32, start: Duration) -> usize {
202    (start.as_secs_f64() * f64::from(sample_rate)).round() as usize
203}
204
205fn pan_gains(pan: f32) -> (f32, f32) {
206    let normalized = ((pan.clamp(-1.0, 1.0) + 1.0) * 0.5) * std::f32::consts::FRAC_PI_2;
207    (normalized.cos(), normalized.sin())
208}