1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
use bevy_asset::Asset;
use bevy_reflect::TypePath;
use firewheel::{collector::ArcGc, sample_resource::SampleResource};
use std::{num::NonZeroU32, sync::Arc};
/// A type-erased audio sample.
///
/// Decoding for PCM WAV, Ogg Vorbis, and a number of other
/// formats is supported through `symphonia` and the associated
/// `bevy_seedling` features.
///
/// You can also disable `symphonia` entirely and provide a custom
/// asset loader.
#[derive(Asset, TypePath, Clone)]
pub struct AudioSample {
sample: ArcGc<dyn SampleResource + Send + Sync>,
original_sample_rate: NonZeroU32,
}
impl AudioSample {
/// Create a new [`AudioSample`] from a [`SampleResource`] loaded into memory.
///
/// If the sample resource has been resampled, `original_sample_rate` should represent
/// the sample rate prior to resampling.
pub fn new<S: SampleResource + Send + Sync + 'static>(
sample: S,
original_sample_rate: NonZeroU32,
) -> Self {
Self {
sample: ArcGc::new_unsized(|| Arc::new(sample) as _),
original_sample_rate,
}
}
/// Share the inner value.
pub fn get(&self) -> ArcGc<dyn SampleResource + Send + Sync> {
self.sample.clone()
}
/// Return the sample resource's original sample rate.
///
/// If the resource has been resampled, this may return
/// a different value than [`SampleResourceInfo::sample_rate`].
///
/// [`SampleResourceInfo::sample_rate`]: firewheel::sample_resource::SampleResourceInfo::sample_rate
pub fn original_sample_rate(&self) -> NonZeroU32 {
self.original_sample_rate
}
}
#[cfg(feature = "symphonia")]
impl From<firewheel::SymphoniumAudioF32> for AudioSample {
fn from(source: firewheel::SymphoniumAudioF32) -> Self {
Self {
original_sample_rate: source.original_sample_rate(),
sample: ArcGc::new_unsized(|| Arc::new(source) as _),
}
}
}
#[cfg(feature = "symphonia")]
impl From<firewheel::SymphoniumAudio> for AudioSample {
fn from(source: firewheel::SymphoniumAudio) -> Self {
Self {
original_sample_rate: source.original_sample_rate(),
sample: ArcGc::new_unsized(|| Arc::new(source) as _),
}
}
}
impl core::fmt::Debug for AudioSample {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("AudioSample")
.field(&self.original_sample_rate)
.finish_non_exhaustive()
}
}
#[cfg(feature = "symphonia")]
pub mod loader {
use super::AudioSample;
use bevy_app::prelude::*;
use bevy_asset::{AssetLoader, AssetServer};
use bevy_ecs::prelude::*;
use bevy_reflect::TypePath;
use symphonia::core::{codecs::registry::CodecRegistry, formats::probe::Probe};
use symphonium::{DecodeConfig, cache::SymphoniumCache};
pub struct SymphoniumLoaderPlugin;
impl Plugin for SymphoniumLoaderPlugin {
fn build(&self, app: &mut App) {
let world = app.world_mut();
world.init_resource::<AudioLoaderConfig>();
world.resource_scope::<AudioLoaderConfig, _>(|world, config| {
world
.resource_mut::<AssetServer>()
.preregister_loader::<SampleLoader>(config.extensions());
});
app.add_observer(init_loader);
}
}
/// A [`Resource`] containing the configuration for [`SampleLoader`].
///
/// New formats and codecs (besides those enabled through this crate's feature flags) can be
/// added to the [symphonia]'s codec registry by inserting this resource before adding the
/// plugin.
///
/// For example:
/// ```no_run
/// use bevy::prelude::*;
/// use bevy_seedling::{prelude::*, sample::AudioLoaderConfig};
/// use symphonia::{
/// core::{codecs::registry::CodecRegistry, formats::probe::Probe},
/// default::{codecs::PcmDecoder, formats::WavReader},
/// };
///
/// fn main() {
/// let mut config = AudioLoaderConfig::default();
/// config.register_codec(["wav"], |registry, probe| {
/// registry.register_audio_decoder::<PcmDecoder>();
/// probe.register_format::<WavReader>();
/// });
///
/// App::new()
/// .insert_resource(config)
/// .add_plugins((DefaultPlugins, SeedlingPlugins));
/// }
/// ```
///
/// Adding the plugin will pre-register [`SampleLoader`] with the extensions in this config.
/// If the custom codecs are only available for insertion after adding the plugin,
/// then [`AssetApp::preregister_asset_loader`] can be called to manually pre-register
/// the new extensions.
///
/// [`AssetApp::preregister_asset_loader`]: bevy_asset::AssetApp::preregister_asset_loader
///
/// This resource will be removed when the loader is registered
/// following the [`StreamStartEvent`][crate::context::StreamStartEvent].
#[derive(Resource)]
pub struct AudioLoaderConfig {
/// The registry with codecs to be used for decoding.
codec_registry: CodecRegistry,
/// The format probe to be used for probing.
probe: Probe,
/// The extensions supported by the formats.
extensions: Vec<&'static str>,
}
impl AudioLoaderConfig {
/// Constructs a new, empty config.
///
/// This will not include `bevy_seedling`'s feature-gated codecs.
pub fn empty() -> Self {
Self {
codec_registry: CodecRegistry::new(),
probe: Probe::default(),
extensions: Vec::new(),
}
}
/// Register a new codec along with its associated extensions.
pub fn register_codec<I, F>(&mut self, extensions: I, f: F)
where
I: IntoIterator<Item = &'static str>,
F: FnOnce(&mut CodecRegistry, &mut Probe),
{
f(&mut self.codec_registry, &mut self.probe);
self.extensions.extend(extensions);
}
/// Returns this config's registered extensions.
pub fn extensions(&self) -> &[&'static str] {
&self.extensions
}
const fn default_extensions() -> &'static [&'static str] {
&[
#[cfg(feature = "wav")]
"wav",
#[cfg(feature = "ogg")]
"ogg",
#[cfg(feature = "mp3")]
"mp3",
#[cfg(feature = "flac")]
"flac",
#[cfg(feature = "mkv")]
"mkv",
]
}
}
impl Default for AudioLoaderConfig {
fn default() -> Self {
let mut config = Self::empty();
symphonia::default::register_enabled_codecs(&mut config.codec_registry);
symphonia::default::register_enabled_formats(&mut config.probe);
config
.extensions
.extend_from_slice(Self::default_extensions());
config
}
}
impl std::fmt::Debug for AudioLoaderConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AudioLoaderConfig")
.field("extensions", &self.extensions)
.finish_non_exhaustive()
}
}
/// A simple loader for audio samples.
///
/// Samples are loaded via [`symphonia`] and resampled eagerly.
/// As a result, you may notice some latency when loading longer
/// samples with low optimization levels.
///
/// The available containers and formats can be configured with
/// this crate's feature flags and [`AudioLoaderConfig`].
#[derive(TypePath, Debug)]
pub struct SampleLoader {
sample_rate: crate::context::SampleRate,
config: &'static AudioLoaderConfig,
}
impl SampleLoader {
/// Create a new sample loader.
///
/// `sample_rate` should be cloned directly from the resource
/// that lives in the same world.
pub fn new(sample_rate: crate::context::SampleRate, config: AudioLoaderConfig) -> Self {
Self {
sample_rate,
// we leak the config here to satisfy symphoium's `&'static` requirements
// NOTE: remove this when symphonium relaxes its lifetimes
config: Box::leak(Box::new(config)),
}
}
}
/// Errors produced while loading samples.
#[derive(Debug)]
pub enum SampleLoaderError {
/// An I/O error, such as missing files.
StdIo(std::io::Error),
/// An error directly from `symphonium`.
Symphonium(String),
}
impl From<std::io::Error> for SampleLoaderError {
fn from(value: std::io::Error) -> Self {
Self::StdIo(value)
}
}
impl From<symphonium::error::LoadError> for SampleLoaderError {
fn from(value: symphonium::error::LoadError) -> Self {
Self::Symphonium(value.to_string())
}
}
impl std::error::Error for SampleLoaderError {}
impl std::fmt::Display for SampleLoaderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::StdIo(stdio) => stdio.fmt(f),
Self::Symphonium(sy) => f.write_str(sy),
}
}
}
impl AssetLoader for SampleLoader {
type Asset = AudioSample;
type Settings = ();
type Error = SampleLoaderError;
async fn load(
&self,
reader: &mut dyn bevy_asset::io::Reader,
_settings: &Self::Settings,
load_context: &mut bevy_asset::LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
thread_local! {
static CACHE: SymphoniumCache = SymphoniumCache::new();
}
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let mut hint = symphonia::core::formats::probe::Hint::new();
hint.with_extension(&load_context.path().to_string());
let probed = symphonium::probe_from_source(
Box::new(std::io::Cursor::new(bytes)),
Some(hint),
Some(&self.config.probe),
)?;
let source = CACHE.with(|cache| {
symphonium::decode_f32(
probed,
&DecodeConfig::default(),
Some(self.sample_rate.get()),
Some(cache),
Some(&self.config.codec_registry),
)
})?;
Ok(firewheel::SymphoniumAudioF32(source).into())
}
fn extensions(&self) -> &[&str] {
self.config.extensions()
}
}
fn init_loader(_: On<crate::context::StreamStartEvent>, mut commands: Commands) {
commands.queue(|world: &mut World| -> Result {
let sample_rate = world
.get_resource::<crate::context::SampleRate>()
.ok_or("expected `SampleRate` resource")?
.clone();
let config = world
.remove_resource::<AudioLoaderConfig>()
.ok_or("expected `AudioLoaderConfig` resource")?;
world
.resource::<AssetServer>()
.register_loader(SampleLoader::new(sample_rate.clone(), config));
Ok(())
});
}
}