Skip to content
Merged
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions crates/av/src/audio_loader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use bevy::asset::{io::Reader, AssetLoader, LoadContext};
use bevy_kira_audio::AudioSource;
use kira::sound::{static_sound::StaticSoundData, FromFileError};
use std::io::Cursor;
use thiserror::Error;

// Format-agnostic loader for scene-content audio assets served without a file
// extension on the wire. Kira's `StaticSoundData::from_cursor` internally uses
// symphonia's probe to sniff the container (mp3/ogg/wav/flac) from the byte
// stream, so we don't need to know the original extension up front.
#[derive(Default)]
pub struct AudioAssetLoader;

#[non_exhaustive]
#[derive(Debug, Error)]
pub enum AudioLoaderError {
#[error("Could not read audio asset: {0}")]
Io(#[from] std::io::Error),
#[error("Error decoding audio asset: {0}")]
FileError(#[from] FromFileError),
}

impl AssetLoader for AudioAssetLoader {
type Asset = AudioSource;
type Settings = ();
type Error = AudioLoaderError;

async fn load(
&self,
reader: &mut dyn Reader,
_settings: &(),
load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let byte_count = bytes.len();
let sound = StaticSoundData::from_cursor(Cursor::new(bytes))?;
bevy::log::debug!(
"loaded .audio asset {:?} ({} bytes, {} frames @ {} Hz)",
load_context.path(),
byte_count,
sound.frames.len(),
sound.sample_rate,
);
Ok(AudioSource { sound })
}

fn extensions(&self) -> &[&str] {
&["audio"]
}
}
3 changes: 3 additions & 0 deletions crates/av/src/audio_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ impl Plugin for AudioSourcePlugin {
fn build(&self, app: &mut App) {
// we use kira for audio source asset management, regardless of native / wasm
app.add_plugins(bevy_kira_audio::AudioPlugin);
// Custom `.audio` loader for scene-content clips fetched by content hash
// (no on-wire extension). Format is detected from the byte stream.
app.register_asset_loader(crate::audio_loader::AudioAssetLoader);
app.add_event::<SystemAudio>();
app.add_crdt_lww_component::<PbAudioSource, AudioSource>(
SceneComponentId::AUDIO_SOURCE,
Expand Down
10 changes: 9 additions & 1 deletion crates/av/src/audio_source_wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,15 @@ fn manage_audio_sources(
Option<&RenderLayers>,
Option<&RetryEmitter>,
),
Or<(Changed<AudioEmitter>, With<Playing>)>,
Or<(
Changed<AudioEmitter>,
With<Playing>,
// Include entities queued for retry: on wasm we can't start
// playback until the web AudioBuffer is decoded, so the first
// frame often fails and we insert RetryEmitter without a Playing
// marker. Without this term the retry never fires.
With<RetryEmitter>,
)>,
>,
mut audio: NonSendMut<HtmlAudioContext>,
containing_scene: ContainingScene,
Expand Down
1 change: 1 addition & 0 deletions crates/av/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub mod video_context;
pub mod video_stream;

// audio source (non-streaming audio)
pub mod audio_loader;
pub mod audio_source;
#[cfg(not(feature = "html"))]
pub mod audio_source_native;
Expand Down
Loading
Loading