firewheel_core/
sample_resource.rs

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