firewheel_core/
sample_resource.rs

1use core::{num::NonZeroUsize, ops::Range};
2
3#[cfg(not(feature = "std"))]
4use bevy_platform::prelude::Vec;
5
6/// A resource of audio samples.
7pub trait SampleResource: Send + Sync + 'static {
8    /// The number of channels in this resource.
9    fn num_channels(&self) -> NonZeroUsize;
10
11    /// The length of this resource in samples (of a single channel of audio).
12    ///
13    /// Not to be confused with video frames.
14    fn len_frames(&self) -> u64;
15
16    /// Fill the given buffers with audio data starting from the given
17    /// starting frame in the resource.
18    ///
19    /// * `buffers` - The buffers to fill with data. If the length of `buffers`
20    /// is greater than the number of channels in this resource, then ignore
21    /// the extra buffers.
22    /// * `buffer_range` - The range inside each buffer slice in which to
23    /// fill with data. Do not fill any data outside of this range.
24    /// * `start_frame` - The sample (of a single channel of audio) in the
25    /// resource at which to start copying from. Not to be confused with video
26    /// frames.
27    fn fill_buffers(
28        &self,
29        buffers: &mut [&mut [f32]],
30        buffer_range: Range<usize>,
31        start_frame: u64,
32    );
33}
34
35pub struct InterleavedResourceI16 {
36    pub data: Vec<i16>,
37    pub channels: NonZeroUsize,
38}
39
40impl SampleResource for InterleavedResourceI16 {
41    fn num_channels(&self) -> NonZeroUsize {
42        self.channels
43    }
44
45    fn len_frames(&self) -> u64 {
46        (self.data.len() / self.channels.get()) as u64
47    }
48
49    fn fill_buffers(
50        &self,
51        buffers: &mut [&mut [f32]],
52        buffer_range: Range<usize>,
53        start_frame: u64,
54    ) {
55        fill_buffers_interleaved(
56            buffers,
57            buffer_range,
58            start_frame as usize,
59            self.channels,
60            &self.data,
61            pcm_i16_to_f32,
62        );
63    }
64}
65
66pub struct InterleavedResourceU16 {
67    pub data: Vec<u16>,
68    pub channels: NonZeroUsize,
69}
70
71impl SampleResource for InterleavedResourceU16 {
72    fn num_channels(&self) -> NonZeroUsize {
73        self.channels
74    }
75
76    fn len_frames(&self) -> u64 {
77        (self.data.len() / self.channels.get()) as u64
78    }
79
80    fn fill_buffers(
81        &self,
82        buffers: &mut [&mut [f32]],
83        buffer_range: Range<usize>,
84        start_frame: u64,
85    ) {
86        fill_buffers_interleaved(
87            buffers,
88            buffer_range,
89            start_frame as usize,
90            self.channels,
91            &self.data,
92            pcm_u16_to_f32,
93        );
94    }
95}
96
97pub struct InterleavedResourceF32 {
98    pub data: Vec<f32>,
99    pub channels: NonZeroUsize,
100}
101
102impl SampleResource for InterleavedResourceF32 {
103    fn num_channels(&self) -> NonZeroUsize {
104        self.channels
105    }
106
107    fn len_frames(&self) -> u64 {
108        (self.data.len() / self.channels.get()) as u64
109    }
110
111    fn fill_buffers(
112        &self,
113        buffers: &mut [&mut [f32]],
114        buffer_range: Range<usize>,
115        start_frame: u64,
116    ) {
117        fill_buffers_interleaved(
118            buffers,
119            buffer_range,
120            start_frame as usize,
121            self.channels,
122            &self.data,
123            |s| s,
124        );
125    }
126}
127
128impl SampleResource for Vec<Vec<i16>> {
129    fn num_channels(&self) -> NonZeroUsize {
130        NonZeroUsize::new(self.len()).unwrap()
131    }
132
133    fn len_frames(&self) -> u64 {
134        self[0].len() as u64
135    }
136
137    fn fill_buffers(
138        &self,
139        buffers: &mut [&mut [f32]],
140        buffer_range: Range<usize>,
141        start_frame: u64,
142    ) {
143        fill_buffers_deinterleaved(
144            buffers,
145            buffer_range,
146            start_frame as usize,
147            self.as_slice(),
148            pcm_i16_to_f32,
149        );
150    }
151}
152
153impl SampleResource for Vec<Vec<u16>> {
154    fn num_channels(&self) -> NonZeroUsize {
155        NonZeroUsize::new(self.len()).unwrap()
156    }
157
158    fn len_frames(&self) -> u64 {
159        self[0].len() as u64
160    }
161
162    fn fill_buffers(
163        &self,
164        buffers: &mut [&mut [f32]],
165        buffer_range: Range<usize>,
166        start_frame: u64,
167    ) {
168        fill_buffers_deinterleaved(
169            buffers,
170            buffer_range,
171            start_frame as usize,
172            self.as_slice(),
173            pcm_u16_to_f32,
174        );
175    }
176}
177
178impl SampleResource for Vec<Vec<f32>> {
179    fn num_channels(&self) -> NonZeroUsize {
180        NonZeroUsize::new(self.len()).unwrap()
181    }
182
183    fn len_frames(&self) -> u64 {
184        self[0].len() as u64
185    }
186
187    fn fill_buffers(
188        &self,
189        buffers: &mut [&mut [f32]],
190        buffer_range: Range<usize>,
191        start_frame: u64,
192    ) {
193        fill_buffers_deinterleaved_f32(buffers, buffer_range, start_frame as usize, self);
194    }
195}
196
197#[inline]
198pub fn pcm_i16_to_f32(s: i16) -> f32 {
199    f32::from(s) * (1.0 / core::i16::MAX as f32)
200}
201
202#[inline]
203pub fn pcm_u16_to_f32(s: u16) -> f32 {
204    ((f32::from(s)) * (2.0 / core::u16::MAX as f32)) - 1.0
205}
206
207/// A helper method to fill buffers from a resource of interleaved samples.
208pub fn fill_buffers_interleaved<T: Clone + Copy>(
209    buffers: &mut [&mut [f32]],
210    buffer_range: Range<usize>,
211    start_frame: usize,
212    channels: NonZeroUsize,
213    data: &[T],
214    convert: impl Fn(T) -> f32,
215) {
216    let start_frame = start_frame as usize;
217    let channels = channels.get();
218
219    let frames = buffer_range.end - buffer_range.start;
220
221    if channels == 1 {
222        // Mono, no need to deinterleave.
223        for (buf_s, &src_s) in buffers[0][buffer_range.clone()]
224            .iter_mut()
225            .zip(&data[start_frame..start_frame + frames])
226        {
227            *buf_s = convert(src_s);
228        }
229        return;
230    }
231
232    if channels == 2 && buffers.len() >= 2 {
233        // Provide an optimized loop for stereo.
234        let (buf0, buf1) = buffers.split_first_mut().unwrap();
235        let buf0 = &mut buf0[buffer_range.clone()];
236        let buf1 = &mut buf1[0][buffer_range.clone()];
237
238        let src_slice = &data[start_frame * 2..(start_frame + frames) * 2];
239
240        for (src_chunk, (buf0_s, buf1_s)) in src_slice
241            .chunks_exact(2)
242            .zip(buf0.iter_mut().zip(buf1.iter_mut()))
243        {
244            *buf0_s = convert(src_chunk[0]);
245            *buf1_s = convert(src_chunk[1]);
246        }
247
248        return;
249    }
250
251    let src_slice = &data[start_frame * channels..(start_frame + frames) * channels];
252    for (ch_i, buf_ch) in (0..channels).zip(buffers.iter_mut()) {
253        for (src_chunk, buf_s) in src_slice
254            .chunks_exact(channels)
255            .zip(buf_ch[buffer_range.clone()].iter_mut())
256        {
257            *buf_s = convert(src_chunk[ch_i]);
258        }
259    }
260}
261
262/// A helper method to fill buffers from a resource of deinterleaved samples.
263pub fn fill_buffers_deinterleaved<T: Clone + Copy, V: AsRef<[T]>>(
264    buffers: &mut [&mut [f32]],
265    buffer_range: Range<usize>,
266    start_frame: usize,
267    data: &[V],
268    convert: impl Fn(T) -> f32,
269) {
270    let start_frame = start_frame as usize;
271    let frames = buffer_range.end - buffer_range.start;
272
273    if data.len() == 2 && buffers.len() >= 2 {
274        // Provide an optimized loop for stereo.
275        let (buf0, buf1) = buffers.split_first_mut().unwrap();
276        let buf0 = &mut buf0[buffer_range.clone()];
277        let buf1 = &mut buf1[0][buffer_range.clone()];
278        let s0 = &data[0].as_ref()[start_frame..start_frame + frames];
279        let s1 = &data[1].as_ref()[start_frame..start_frame + frames];
280
281        for i in 0..frames {
282            buf0[i] = convert(s0[i]);
283            buf1[i] = convert(s1[i]);
284        }
285
286        return;
287    }
288
289    for (buf, ch) in buffers.iter_mut().zip(data.iter()) {
290        for (buf_s, &ch_s) in buf[buffer_range.clone()]
291            .iter_mut()
292            .zip(ch.as_ref()[start_frame..start_frame + frames].iter())
293        {
294            *buf_s = convert(ch_s);
295        }
296    }
297}
298
299/// A helper method to fill buffers from a resource of deinterleaved `f32` samples.
300pub fn fill_buffers_deinterleaved_f32<V: AsRef<[f32]>>(
301    buffers: &mut [&mut [f32]],
302    buffer_range: Range<usize>,
303    start_frame: usize,
304    data: &[V],
305) {
306    let start_frame = start_frame as usize;
307
308    for (buf, ch) in buffers.iter_mut().zip(data.iter()) {
309        buf[buffer_range.clone()].copy_from_slice(
310            &ch.as_ref()[start_frame..start_frame + buffer_range.end - buffer_range.start],
311        );
312    }
313}
314
315#[cfg(feature = "symphonium")]
316/// A wrapper around [`symphonium::DecodedAudio`] which implements the
317/// [`SampleResource`] trait.
318pub struct DecodedAudio(pub symphonium::DecodedAudio);
319
320#[cfg(feature = "symphonium")]
321impl DecodedAudio {
322    pub fn duration_seconds(&self) -> f64 {
323        self.0.frames() as f64 / self.0.sample_rate() as f64
324    }
325
326    pub fn into_dyn_resource(self) -> crate::collector::ArcGc<dyn SampleResource> {
327        crate::collector::ArcGc::new_unsized(|| {
328            bevy_platform::sync::Arc::new(self) as bevy_platform::sync::Arc<dyn SampleResource>
329        })
330    }
331}
332
333#[cfg(feature = "symphonium")]
334impl From<DecodedAudio> for crate::collector::ArcGc<dyn SampleResource> {
335    fn from(value: DecodedAudio) -> Self {
336        value.into_dyn_resource()
337    }
338}
339
340#[cfg(feature = "symphonium")]
341impl SampleResource for DecodedAudio {
342    fn num_channels(&self) -> NonZeroUsize {
343        NonZeroUsize::new(self.0.channels()).unwrap()
344    }
345
346    fn len_frames(&self) -> u64 {
347        self.0.frames() as u64
348    }
349
350    fn fill_buffers(
351        &self,
352        buffers: &mut [&mut [f32]],
353        buffer_range: Range<usize>,
354        start_frame: u64,
355    ) {
356        let channels = self.0.channels().min(buffers.len());
357
358        if channels == 2 {
359            let (b1, b2) = buffers.split_first_mut().unwrap();
360
361            self.0.fill_stereo(
362                start_frame as usize,
363                &mut b1[buffer_range.clone()],
364                &mut b2[0][buffer_range.clone()],
365            );
366        } else {
367            for (ch_i, b) in buffers[0..channels].iter_mut().enumerate() {
368                self.0
369                    .fill_channel(ch_i, start_frame as usize, &mut b[buffer_range.clone()])
370                    .unwrap();
371            }
372        }
373    }
374}
375
376#[cfg(feature = "symphonium")]
377impl From<symphonium::DecodedAudio> for DecodedAudio {
378    fn from(data: symphonium::DecodedAudio) -> Self {
379        Self(data)
380    }
381}
382
383#[cfg(feature = "symphonium")]
384/// A wrapper around [`symphonium::DecodedAudioF32`] which implements the
385/// [`SampleResource`] trait.
386pub struct DecodedAudioF32(pub symphonium::DecodedAudioF32);
387
388#[cfg(feature = "symphonium")]
389impl DecodedAudioF32 {
390    pub fn duration_seconds(&self, sample_rate: u32) -> f64 {
391        self.0.frames() as f64 / sample_rate as f64
392    }
393}
394
395#[cfg(feature = "symphonium")]
396impl SampleResource for DecodedAudioF32 {
397    fn num_channels(&self) -> NonZeroUsize {
398        NonZeroUsize::new(self.0.channels()).unwrap()
399    }
400
401    fn len_frames(&self) -> u64 {
402        self.0.frames() as u64
403    }
404
405    fn fill_buffers(
406        &self,
407        buffers: &mut [&mut [f32]],
408        buffer_range: Range<usize>,
409        start_frame: u64,
410    ) {
411        fill_buffers_deinterleaved_f32(buffers, buffer_range, start_frame as usize, &self.0.data);
412    }
413}
414
415#[cfg(feature = "symphonium")]
416impl From<symphonium::DecodedAudioF32> for DecodedAudioF32 {
417    fn from(data: symphonium::DecodedAudioF32) -> Self {
418        Self(data)
419    }
420}
421
422/// A helper method to load an audio file from a path using Symphonium.
423///
424/// * `loader` - The symphonium loader.
425/// * `path`` - The path to the audio file stored on disk.
426/// * `sample_rate` - The sample rate of the audio stream.
427/// * `resample_quality` - The quality of the resampler to use.
428#[cfg(feature = "symphonium")]
429pub fn load_audio_file<P: AsRef<std::path::Path>>(
430    loader: &mut symphonium::SymphoniumLoader,
431    path: P,
432    #[cfg(feature = "symphonium_resample")] sample_rate: core::num::NonZeroU32,
433    #[cfg(feature = "symphonium_resample")] resample_quality: symphonium::ResampleQuality,
434) -> Result<DecodedAudio, symphonium::error::LoadError> {
435    loader
436        .load(
437            path,
438            #[cfg(feature = "symphonium_resample")]
439            Some(sample_rate.get()),
440            #[cfg(feature = "symphonium_resample")]
441            resample_quality,
442            None,
443        )
444        .map(|d| DecodedAudio(d))
445}
446
447/// A helper method to load an audio file from a custom source using Symphonium.
448///
449/// * `loader` - The symphonium loader.
450/// * `source` - The audio source which implements the [`MediaSource`] trait.
451/// * `hint` -  An optional hint to help the format registry guess what format reader is appropriate.
452/// * `sample_rate` - The sample rate of the audio stream.
453/// * `resample_quality` - The quality of the resampler to use.
454///
455/// [`MediaSource`]: symphonium::symphonia::core::io::MediaSource
456#[cfg(feature = "symphonium")]
457pub fn load_audio_file_from_source(
458    loader: &mut symphonium::SymphoniumLoader,
459    source: Box<dyn symphonium::symphonia::core::io::MediaSource>,
460    hint: Option<symphonium::symphonia::core::probe::Hint>,
461    #[cfg(feature = "symphonium_resample")] sample_rate: core::num::NonZeroU32,
462    #[cfg(feature = "symphonium_resample")] resample_quality: symphonium::ResampleQuality,
463) -> Result<DecodedAudio, symphonium::error::LoadError> {
464    loader
465        .load_from_source(
466            source,
467            hint,
468            #[cfg(feature = "symphonium_resample")]
469            Some(sample_rate.get()),
470            #[cfg(feature = "symphonium_resample")]
471            resample_quality,
472            None,
473        )
474        .map(|d| DecodedAudio(d))
475}
476
477/// A helper method to load an audio file from a path using Symphonium. This
478/// also stretches (pitch shifts) the sample by the given amount.
479///
480/// * `loader` - The symphonium loader.
481/// * `path`` - The path to the audio file stored on disk.
482/// * `sample_rate` - The sample rate of the audio stream.
483/// * `stretch` - The amount of stretching (`new_length / old_length`). A value of `1.0` is no
484/// change, a value less than `1.0` will increase the pitch & decrease the length, and a value
485/// greater than `1.0` will decrease the pitch & increase the length. If a `target_sample_rate`
486/// is given, then the final amount will automatically be adjusted to account for that.
487#[cfg(feature = "symphonium_stretch")]
488pub fn load_audio_file_stretched<P: AsRef<std::path::Path>>(
489    loader: &mut symphonium::SymphoniumLoader,
490    path: P,
491    sample_rate: core::num::NonZeroU32,
492    stretch: f64,
493) -> Result<DecodedAudio, symphonium::error::LoadError> {
494    loader
495        .load_f32_stretched(path, stretch, Some(sample_rate.get()), None)
496        .map(|d| DecodedAudio(d.into()))
497}
498
499/// A helper method to load an audio file from a custom source using Symphonium. This
500/// also stretches (pitch shifts) the sample by the given amount.
501///
502/// * `loader` - The symphonium loader.
503/// * `source` - The audio source which implements the [`symphonium::symphonia::core::io::MediaSource`]
504/// trait.
505/// * `hint` -  An optional hint to help the format registry guess what format reader is appropriate.
506/// * `sample_rate` - The sample rate of the audio stream.
507/// * `stretch` - The amount of stretching (`new_length / old_length`). A value of `1.0` is no
508/// change, a value less than `1.0` will increase the pitch & decrease the length, and a value
509/// greater than `1.0` will decrease the pitch & increase the length. If a `target_sample_rate`
510/// is given, then the final amount will automatically be adjusted to account for that.
511#[cfg(feature = "symphonium_stretch")]
512pub fn load_audio_file_from_source_stretched(
513    loader: &mut symphonium::SymphoniumLoader,
514    source: Box<dyn symphonium::symphonia::core::io::MediaSource>,
515    hint: Option<symphonium::symphonia::core::probe::Hint>,
516    sample_rate: core::num::NonZeroU32,
517    stretch: f64,
518) -> Result<DecodedAudio, symphonium::error::LoadError> {
519    loader
520        .load_f32_from_source_stretched(source, hint, stretch, Some(sample_rate.get()), None)
521        .map(|d| DecodedAudio(d.into()))
522}
523
524#[cfg(feature = "symphonium")]
525/// A helper method to convert a [`symphonium::DecodedAudio`] resource into
526/// a [`SampleResource`].
527pub fn decoded_to_resource(
528    data: symphonium::DecodedAudio,
529) -> bevy_platform::sync::Arc<dyn SampleResource> {
530    bevy_platform::sync::Arc::new(DecodedAudio(data))
531}
532
533#[cfg(feature = "symphonium")]
534/// A helper method to convert a [`symphonium::DecodedAudioF32`] resource into
535/// a [`SampleResource`].
536pub fn decoded_f32_to_resource(
537    data: symphonium::DecodedAudioF32,
538) -> bevy_platform::sync::Arc<dyn SampleResource> {
539    bevy_platform::sync::Arc::new(DecodedAudioF32(data))
540}