Skip to main content

firewheel_symphonium/
lib.rs

1use std::{
2    num::{NonZeroU32, NonZeroUsize},
3    ops::Range,
4};
5
6use firewheel_core::{
7    collector::ArcGc,
8    sample_resource::{SampleResource, SampleResourceInfo},
9};
10
11/// A wrapper around [`symphonium::DecodedAudio`] which implements the
12/// [`SampleResource`] trait.
13#[derive(Debug, Clone)]
14pub struct DecodedAudio(pub symphonium::DecodedAudio);
15
16impl DecodedAudio {
17    pub fn duration_seconds(&self) -> f64 {
18        self.0.frames() as f64 / self.0.sample_rate().get() as f64
19    }
20
21    pub fn into_dyn_resource(self) -> ArcGc<dyn SampleResource> {
22        ArcGc::new_unsized(|| {
23            bevy_platform::sync::Arc::new(self) as bevy_platform::sync::Arc<dyn SampleResource>
24        })
25    }
26
27    /// The sample rate of this resource.
28    pub fn sample_rate(&self) -> NonZeroU32 {
29        self.0.sample_rate()
30    }
31
32    /// The sample rate of the audio resource before it was resampled (if it was resampled).
33    pub fn original_sample_rate(&self) -> NonZeroU32 {
34        self.0.original_sample_rate()
35    }
36}
37
38impl From<DecodedAudio> for ArcGc<dyn SampleResource> {
39    fn from(value: DecodedAudio) -> Self {
40        value.into_dyn_resource()
41    }
42}
43
44impl SampleResourceInfo for DecodedAudio {
45    fn num_channels(&self) -> NonZeroUsize {
46        NonZeroUsize::new(self.0.channels()).unwrap()
47    }
48
49    fn len_frames(&self) -> u64 {
50        self.0.frames() as u64
51    }
52
53    fn sample_rate(&self) -> Option<NonZeroU32> {
54        Some(self.0.sample_rate())
55    }
56}
57
58impl SampleResource for DecodedAudio {
59    fn fill_buffers(
60        &self,
61        buffers: &mut [&mut [f32]],
62        buffer_range: Range<usize>,
63        start_frame: u64,
64    ) {
65        let channels = self.0.channels().min(buffers.len());
66
67        if channels == 2 {
68            let (b1, b2) = buffers.split_first_mut().unwrap();
69
70            self.0.fill_stereo(
71                start_frame as usize,
72                &mut b1[buffer_range.clone()],
73                &mut b2[0][buffer_range.clone()],
74            );
75        } else {
76            for (ch_i, b) in buffers[0..channels].iter_mut().enumerate() {
77                self.0
78                    .fill_channel(ch_i, start_frame as usize, &mut b[buffer_range.clone()])
79                    .unwrap();
80            }
81        }
82    }
83}
84
85impl From<symphonium::DecodedAudio> for DecodedAudio {
86    fn from(data: symphonium::DecodedAudio) -> Self {
87        Self(data)
88    }
89}
90
91/// A wrapper around [`symphonium::DecodedAudioF32`] which implements the
92/// [`SampleResource`] trait.
93#[derive(Debug, Clone)]
94pub struct DecodedAudioF32(pub symphonium::DecodedAudioF32);
95
96impl DecodedAudioF32 {
97    pub fn duration_seconds(&self, sample_rate: NonZeroU32) -> f64 {
98        self.0.frames() as f64 / sample_rate.get() as f64
99    }
100
101    /// The sample rate of this resource.
102    pub fn sample_rate(&self) -> NonZeroU32 {
103        self.0.sample_rate
104    }
105
106    /// The sample rate of the audio resource before it was resampled (if it was resampled).
107    pub fn original_sample_rate(&self) -> NonZeroU32 {
108        self.0.original_sample_rate
109    }
110}
111
112impl SampleResourceInfo for DecodedAudioF32 {
113    fn num_channels(&self) -> NonZeroUsize {
114        NonZeroUsize::new(self.0.channels()).unwrap()
115    }
116
117    fn len_frames(&self) -> u64 {
118        self.0.frames() as u64
119    }
120
121    fn sample_rate(&self) -> Option<NonZeroU32> {
122        Some(self.0.sample_rate)
123    }
124}
125
126impl SampleResource for DecodedAudioF32 {
127    fn fill_buffers(
128        &self,
129        buffers: &mut [&mut [f32]],
130        buffer_range: Range<usize>,
131        start_frame: u64,
132    ) {
133        firewheel_core::sample_resource::fill_buffers_deinterleaved_f32(
134            buffers,
135            buffer_range,
136            start_frame as usize,
137            &self.0.data,
138        );
139    }
140}
141
142impl From<symphonium::DecodedAudioF32> for DecodedAudioF32 {
143    fn from(data: symphonium::DecodedAudioF32) -> Self {
144        Self(data)
145    }
146}
147
148/// A helper method to load an audio file from a path using Symphonium.
149///
150/// * `loader` - The symphonium loader.
151/// * `path`` - The path to the audio file stored on disk.
152/// * `target_sample_rate` - If this is `Some`, then the file will be resampled to match
153/// the given target sample rate. (No resampling will occur if the audio file's sample rate
154/// is already the target sample rate). If this is `None`, then the file will not be
155/// resampled and stay its original sample rate.
156/// * `resample_quality` - The quality of the resampler to use if the sample rate of the
157/// audio file doesn't match the `target_sample_rate`. This has no effect if
158/// `target_sample_rate` is `None`.
159pub fn load_audio_file<P: AsRef<std::path::Path>>(
160    loader: &mut symphonium::SymphoniumLoader,
161    path: P,
162    #[cfg(feature = "resample")] target_sample_rate: Option<core::num::NonZeroU32>,
163    #[cfg(feature = "resample")] resample_quality: symphonium::ResampleQuality,
164) -> Result<DecodedAudio, symphonium::error::LoadError> {
165    loader
166        .load(
167            path,
168            #[cfg(feature = "resample")]
169            target_sample_rate,
170            #[cfg(feature = "resample")]
171            resample_quality,
172            None,
173        )
174        .map(|d| DecodedAudio(d))
175}
176
177/// A helper method to load an audio file from a custom source using Symphonium.
178///
179/// * `loader` - The symphonium loader.
180/// * `source` - The audio source which implements the [`MediaSource`] trait.
181/// * `hint` -  An optional hint to help the format registry guess what format reader is appropriate.
182/// * `target_sample_rate` - If this is `Some`, then the file will be resampled to match
183/// the given target sample rate. (No resampling will occur if the audio file's sample rate
184/// is already the target sample rate). If this is `None`, then the file will not be
185/// resampled and stay its original sample rate.
186/// * `resample_quality` - The quality of the resampler to use if the sample rate of the
187/// audio file doesn't match the `target_sample_rate`. This has no effect if
188/// `target_sample_rate` is `None`.
189///
190/// [`MediaSource`]: symphonium::symphonia::core::io::MediaSource
191pub fn load_audio_file_from_source(
192    loader: &mut symphonium::SymphoniumLoader,
193    source: Box<dyn symphonium::symphonia::core::io::MediaSource>,
194    hint: Option<symphonium::symphonia::core::probe::Hint>,
195    #[cfg(feature = "resample")] target_sample_rate: Option<core::num::NonZeroU32>,
196    #[cfg(feature = "resample")] resample_quality: symphonium::ResampleQuality,
197) -> Result<DecodedAudio, symphonium::error::LoadError> {
198    loader
199        .load_from_source(
200            source,
201            hint,
202            #[cfg(feature = "resample")]
203            target_sample_rate,
204            #[cfg(feature = "resample")]
205            resample_quality,
206            None,
207        )
208        .map(|d| DecodedAudio(d))
209}
210
211/// A helper method to load an audio file from a path using Symphonium. This
212/// also stretches (pitch shifts) the sample by the given amount.
213///
214/// * `loader` - The symphonium loader.
215/// * `path`` - The path to the audio file stored on disk.
216/// * `target_sample_rate` - If this is `Some`, then the file will be resampled to match
217/// the given target sample rate. If this is `None`, then the file will stay its original sample
218/// rate.
219/// * `stretch` - The amount of stretching (`new_length / old_length`). A value of `1.0` is no
220/// change, a value less than `1.0` will increase the pitch & decrease the length, and a value
221/// greater than `1.0` will decrease the pitch & increase the length. If a `target_sample_rate`
222/// is given, then the final amount will automatically be adjusted to account for that.
223#[cfg(feature = "stretch")]
224pub fn load_audio_file_stretched<P: AsRef<std::path::Path>>(
225    loader: &mut symphonium::SymphoniumLoader,
226    path: P,
227    target_sample_rate: Option<core::num::NonZeroU32>,
228    stretch: f64,
229) -> Result<DecodedAudio, symphonium::error::LoadError> {
230    loader
231        .load_stretched(path, stretch, target_sample_rate, None)
232        .map(|d| DecodedAudio(d.into()))
233}
234
235/// A helper method to load an audio file from a custom source using Symphonium. This
236/// also stretches (pitch shifts) the sample by the given amount.
237///
238/// * `loader` - The symphonium loader.
239/// * `source` - The audio source which implements the [`symphonium::symphonia::core::io::MediaSource`]
240/// trait.
241/// * `hint` -  An optional hint to help the format registry guess what format reader is appropriate.
242/// * `target_sample_rate` - If this is `Some`, then the file will be resampled to match
243/// the given target sample rate. If this is `None`, then the file will stay its original sample
244/// rate.
245/// * `stretch` - The amount of stretching (`new_length / old_length`). A value of `1.0` is no
246/// change, a value less than `1.0` will increase the pitch & decrease the length, and a value
247/// greater than `1.0` will decrease the pitch & increase the length. If a `target_sample_rate`
248/// is given, then the final amount will automatically be adjusted to account for that.
249#[cfg(feature = "stretch")]
250pub fn load_audio_file_from_source_stretched(
251    loader: &mut symphonium::SymphoniumLoader,
252    source: Box<dyn symphonium::symphonia::core::io::MediaSource>,
253    hint: Option<symphonium::symphonia::core::probe::Hint>,
254    target_sample_rate: Option<core::num::NonZeroU32>,
255    stretch: f64,
256) -> Result<DecodedAudio, symphonium::error::LoadError> {
257    loader
258        .load_from_source_stretched(source, hint, stretch, target_sample_rate, None)
259        .map(|d| DecodedAudio(d.into()))
260}
261
262/// A helper method to convert a [`symphonium::DecodedAudio`] resource into
263/// a [`SampleResource`].
264pub fn decoded_to_resource(
265    data: symphonium::DecodedAudio,
266) -> bevy_platform::sync::Arc<dyn SampleResource> {
267    bevy_platform::sync::Arc::new(DecodedAudio(data))
268}
269
270/// A helper method to convert a [`symphonium::DecodedAudioF32`] resource into
271/// a [`SampleResource`].
272pub fn decoded_f32_to_resource(
273    data: symphonium::DecodedAudioF32,
274) -> bevy_platform::sync::Arc<dyn SampleResource> {
275    bevy_platform::sync::Arc::new(DecodedAudioF32(data))
276}