Skip to main content

firewheel_core/
sample_resource.rs

1use audioadapter::Adapter;
2use audioadapter_buffers::{
3    adapter_to_float::ConvertNumbers,
4    direct::{InterleavedSlice, SequentialSlice},
5};
6use audioadapter_sample::sample::RawSample;
7use core::{
8    num::{NonZeroU32, NonZeroUsize},
9    ops::Range,
10};
11
12#[cfg(not(feature = "std"))]
13use bevy_platform::prelude::Vec;
14
15use crate::collector::ArcGc;
16
17/// Trait returning information about a resource of audio samples
18pub trait SampleResourceInfo {
19    /// The number of channels in this resource.
20    fn num_channels(&self) -> NonZeroUsize;
21
22    /// The length of this resource in samples (of a single channel of audio).
23    ///
24    /// Not to be confused with video frames.
25    fn len_frames(&self) -> u64;
26
27    /// The sample rate of this resource.
28    ///
29    /// Returns `None` if the sample rate is unknown.
30    fn sample_rate(&self) -> Option<NonZeroU32> {
31        None
32    }
33}
34
35/// A resource of audio samples.
36pub trait SampleResource: SampleResourceInfo {
37    /// Fill the given buffers with audio data starting from the given
38    /// starting frame in the resource.
39    ///
40    /// * `out_buffer` - The buffers to fill with data. If the length of `buffers`
41    ///   is greater than the number of channels in this resource, then ignore
42    ///   the extra buffers.
43    /// * `out_buffer_range` - The range inside each buffer slice in which to
44    ///   fill with data. Do not fill any data outside of this range.
45    /// * `start_frame` - The sample (of a single channel of audio) in the
46    ///   resource at which to start copying from. Not to be confused with video
47    ///   frames.
48    ///
49    /// If the length of `out_buffer_range` is all or partly out of bounds of
50    /// the resource, then the frames which are out of bounds will be left
51    /// untouched.
52    ///
53    /// Returns the number of frames that were successfully filled. This may
54    /// be less than the length of `out_buffer_range` if the range is all or
55    /// partly out of bounds of the resource
56    fn fill_buffers(
57        &self,
58        out_buffer: &mut [&mut [f32]],
59        out_buffer_range: Range<usize>,
60        start_frame: u64,
61    ) -> usize;
62}
63
64/// A resource of audio samples stored as de-interleaved f32 values.
65pub trait SampleResourceF32: SampleResourceInfo {
66    /// Get the the buffer for a given channel.
67    fn channel(&self, i: usize) -> Option<&[f32]>;
68}
69
70impl<T: SampleResource + Send + Sync + 'static> From<T>
71    for ArcGc<dyn SampleResource + Send + Sync + 'static>
72{
73    fn from(value: T) -> Self {
74        ArcGc::new_unsized(|| {
75            bevy_platform::sync::Arc::new(value)
76                as bevy_platform::sync::Arc<dyn SampleResource + Send + Sync + 'static>
77        })
78    }
79}
80
81impl<T: SampleResourceF32 + Send + Sync + 'static> From<T>
82    for ArcGc<dyn SampleResourceF32 + Send + Sync + 'static>
83{
84    fn from(value: T) -> Self {
85        ArcGc::new_unsized(|| {
86            bevy_platform::sync::Arc::new(value)
87                as bevy_platform::sync::Arc<dyn SampleResourceF32 + Send + Sync + 'static>
88        })
89    }
90}
91
92#[derive(Clone)]
93pub struct InterleavedResourceF32 {
94    pub data: Vec<f32>,
95    pub channels: NonZeroUsize,
96    pub sample_rate: Option<NonZeroU32>,
97}
98
99impl InterleavedResourceF32 {
100    pub fn into_dyn_resource(self) -> ArcGc<dyn SampleResource + Send + Sync + 'static> {
101        ArcGc::new_unsized(|| {
102            bevy_platform::sync::Arc::new(self)
103                as bevy_platform::sync::Arc<dyn SampleResource + Send + Sync + 'static>
104        })
105    }
106}
107
108impl SampleResourceInfo for InterleavedResourceF32 {
109    fn num_channels(&self) -> NonZeroUsize {
110        self.channels
111    }
112
113    fn len_frames(&self) -> u64 {
114        (self.data.len() / self.channels.get()) as u64
115    }
116
117    fn sample_rate(&self) -> Option<NonZeroU32> {
118        self.sample_rate
119    }
120}
121
122impl SampleResource for InterleavedResourceF32 {
123    fn fill_buffers(
124        &self,
125        out_buffer: &mut [&mut [f32]],
126        out_buffer_range: Range<usize>,
127        start_frame: u64,
128    ) -> usize {
129        fill_buffers_interleaved(
130            out_buffer,
131            out_buffer_range,
132            start_frame,
133            self.channels,
134            &self.data,
135            self.len_frames() as usize,
136        )
137    }
138}
139
140impl core::fmt::Debug for InterleavedResourceF32 {
141    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
142        write!(
143            f,
144            "InterleavedResourceF32 {{ channels: {}, frames: {} }}",
145            self.channels.get(),
146            self.data.len() / self.channels.get(),
147        )
148    }
149}
150
151impl SampleResourceInfo for Vec<Vec<f32>> {
152    fn num_channels(&self) -> NonZeroUsize {
153        NonZeroUsize::new(self.len()).unwrap()
154    }
155
156    fn len_frames(&self) -> u64 {
157        self[0].len() as u64
158    }
159}
160
161impl SampleResource for Vec<Vec<f32>> {
162    fn fill_buffers(
163        &self,
164        out_buffer: &mut [&mut [f32]],
165        out_buffer_range: Range<usize>,
166        start_frame: u64,
167    ) -> usize {
168        fill_buffers_deinterleaved_f32(
169            out_buffer,
170            out_buffer_range,
171            start_frame,
172            self,
173            self[0].len(),
174        )
175    }
176}
177
178impl SampleResourceF32 for Vec<Vec<f32>> {
179    fn channel(&self, i: usize) -> Option<&[f32]> {
180        self.get(i).map(|data| data.as_slice())
181    }
182}
183
184/// A helper method to fill buffers from a resource of interleaved samples.
185///
186/// Returns the number of frames that were successfully filled. This may
187/// be less than the length of `out_buffer_range` if the range is all or
188/// partly out of bounds of the resource
189pub fn fill_buffers_interleaved<T: RawSample + Clone>(
190    out_buffer: &mut [&mut [f32]],
191    out_buffer_range: Range<usize>,
192    start_frame: u64,
193    channels: NonZeroUsize,
194    resource: &[T],
195    resource_len_frames: usize,
196) -> usize {
197    let channels = channels.get();
198
199    let Some((frames, start_frame)) = constrain_frames(
200        out_buffer_range.end - out_buffer_range.start,
201        start_frame,
202        resource_len_frames,
203    ) else {
204        return 0;
205    };
206
207    // Provide an optimized loop for stereo.
208    if channels == 2 && out_buffer.len() >= 2 {
209        let (buf0, buf1) = out_buffer.split_first_mut().unwrap();
210        let buf0 = &mut buf0[out_buffer_range.start..out_buffer_range.start + frames];
211        let buf1 = &mut buf1[0][out_buffer_range.start..out_buffer_range.start + frames];
212
213        let src_slice = &resource[start_frame * 2..(start_frame + frames) * 2];
214
215        for (src_chunk, (buf0_s, buf1_s)) in src_slice
216            .chunks_exact(2)
217            .zip(buf0.iter_mut().zip(buf1.iter_mut()))
218        {
219            *buf0_s = src_chunk[0].to_scaled_float();
220            *buf1_s = src_chunk[1].to_scaled_float();
221        }
222
223        return frames;
224    }
225
226    let src_slice = &resource[start_frame * channels..(start_frame + frames) * channels];
227
228    let adapter = InterleavedSlice::new(src_slice, channels, frames).unwrap();
229    let convert = ConvertNumbers::<&dyn Adapter<T>, f32>::new(&adapter as &dyn Adapter<T>);
230
231    for (ch_i, out_ch) in out_buffer.iter_mut().enumerate().take(channels) {
232        convert.copy_from_channel_to_slice(
233            ch_i,
234            start_frame,
235            &mut out_ch[out_buffer_range.start..out_buffer_range.start + frames],
236        );
237    }
238
239    frames
240}
241
242/// A helper method to fill buffers from a resource of deinterleaved samples.
243///
244/// Returns the number of frames that were successfully filled. This may
245/// be less than the length of `out_buffer_range` if the range is all or
246/// partly out of bounds of the resource
247pub fn fill_channel_deinterleaved<T: RawSample + Clone>(
248    out_buffer_channel: &mut [f32],
249    out_buffer_range: Range<usize>,
250    start_frame: u64,
251    resource_channel: &[T],
252) -> usize {
253    let Some((frames, start_frame)) = constrain_frames(
254        out_buffer_range.end - out_buffer_range.start,
255        start_frame,
256        resource_channel.len(),
257    ) else {
258        return 0;
259    };
260
261    let adapter = SequentialSlice::new(resource_channel, 1, frames).unwrap();
262    let convert = ConvertNumbers::<&dyn Adapter<T>, f32>::new(&adapter as &dyn Adapter<T>);
263
264    convert.copy_from_channel_to_slice(
265        0,
266        start_frame,
267        &mut out_buffer_channel[out_buffer_range.start..out_buffer_range.start + frames],
268    );
269
270    frames
271}
272
273/// A helper method to fill buffers from a resource of deinterleaved `f32` samples.
274///
275/// Returns the number of frames that were successfully filled. This may
276/// be less than the length of `out_buffer_range` if the range is all or
277/// partly out of bounds of the resource
278pub fn fill_buffers_deinterleaved_f32<V: AsRef<[f32]>>(
279    out_buffer: &mut [&mut [f32]],
280    out_buffer_range: Range<usize>,
281    start_frame: u64,
282    resource_channels: &[V],
283    resource_len_frames: usize,
284) -> usize {
285    let Some((frames, start_frame)) = constrain_frames(
286        out_buffer_range.end - out_buffer_range.start,
287        start_frame,
288        resource_len_frames,
289    ) else {
290        return 0;
291    };
292
293    for (out_ch, in_ch) in out_buffer.iter_mut().zip(resource_channels.iter()) {
294        out_ch[out_buffer_range.start..out_buffer_range.start + frames]
295            .copy_from_slice(&in_ch.as_ref()[start_frame..start_frame + frames]);
296    }
297
298    frames
299}
300
301/// A helper to constrain the requested number of frames to the available frames
302/// in the sample resource.
303///
304/// Returns `Some((available_frames, start_frame as usize))` if the range is all
305/// or partly contained in the resource, or `None` if the range is fully outside
306/// the resource (`available_frames == 0`).
307pub fn constrain_frames(
308    requested_frames: usize,
309    start_frame: u64,
310    resource_len_frames: usize,
311) -> Option<(usize, usize)> {
312    let frames = (requested_frames as u64)
313        .min((resource_len_frames as u64).saturating_sub(start_frame)) as usize;
314    if frames == 0 {
315        None
316    } else {
317        Some((frames, start_frame as usize))
318    }
319}