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