Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
103b31b
Update cpal requirement from 0.15 to 0.16
dependabot[bot] Jul 7, 2025
098d436
Bumps rodio to 0.21
mnmaita Jul 28, 2025
4ee875b
Removes coreaudio-sys patch requirement
mnmaita Jul 28, 2025
9a43d20
Updates Android docs
mnmaita Jul 28, 2025
201bcb2
Updates doc comment pointing to older version
mnmaita Jul 28, 2025
84dce68
Bumps referenced bevy version in examples doc template
mnmaita Jul 28, 2025
ea265e2
Replaces aarch64-apple-ios-sim target with arm64-apple-ios-simulator
mnmaita Jul 28, 2025
0bdb9f9
Reworks audio feature flags
mnmaita Jul 28, 2025
07be887
Migrates code for rodio 0.21 compatibility
mnmaita Jul 28, 2025
38f49f8
Reworks features to make symphonia the default
mnmaita Jul 29, 2025
1749fa4
Fixes internal feature flags
mnmaita Jul 29, 2025
20cabfc
Fixes feature gated extensions
mnmaita Jul 29, 2025
5d44c1c
Updates AudioLoader docs
mnmaita Jul 29, 2025
94cec69
Temporarily makes lewton the default ogg backend
mnmaita Oct 22, 2025
99ba88c
Fixes disable-audio.patch
mnmaita Oct 22, 2025
62fc2bc
Fixes licence exceptions for symphonia
mnmaita Oct 27, 2025
ae86ae5
Removes minimp3 usage and fixes fallback features
mnmaita Oct 29, 2025
e765702
Updates cargo_features.md
mnmaita Nov 23, 2025
22c06db
Removes obsolete android_shared_stdcxx feature
mnmaita Nov 23, 2025
b71dc5e
TEMPORARY: cpal patch to fix Send + Sync issue
mnmaita Nov 23, 2025
eb9280b
Reexports rodio's ChannelCount and SampleRate aliases
mnmaita Dec 11, 2025
79e9dcc
TEMPORARY: rodio patch for latest cpal compatibility
mnmaita Dec 11, 2025
f3b18f7
Fixes decodable example
mnmaita Dec 11, 2025
14c4894
Enables lewton when using audio-all feature
mnmaita Dec 11, 2025
210a0bb
Adds migration guide
mnmaita Dec 11, 2025
fa00c43
Fix markdown lints
mnmaita Dec 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/update-caches.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ jobs:
target: aarch64-linux-android
- os: macos-14
toolchain: stable
target: aarch64-apple-ios-sim
target: arm64-apple-ios-simulator

steps:
# prepare the date - used to rebuild the cache daily to update the cache for rust nightly, even if no change on Bevy dependencies
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/validation-jobs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ jobs:
- uses: actions/cache/restore@v4
with:
# key won't match, will rely on restore-keys
key: ${{ runner.os }}-stable-aarch64-apple-ios-sim-${{ hashFiles('**/Cargo.toml') }}-
key: ${{ runner.os }}-stable-arm64-apple-ios-simulator-${{ hashFiles('**/Cargo.toml') }}-
# See .github/workflows/validation-jobs.yml for how keys are generated
restore-keys: |
${{ runner.os }}-stable-aarch64-apple-ios-sim-${{ hashFiles('**/Cargo.toml') }}-
${{ runner.os }}-stable-aarch64-apple-ios-sim-
${{ runner.os }}-stable-arm64-apple-ios-simulator-${{ hashFiles('**/Cargo.toml') }}-
${{ runner.os }}-stable-arm64-apple-ios-simulator-
path: |
~/.cargo/bin/
~/.cargo/registry/index/
Expand All @@ -49,7 +49,7 @@ jobs:
target/

- name: Add iOS targets
run: rustup target add aarch64-apple-ios-sim
run: rustup target add arm64-apple-ios-simulator

- name: Build and install iOS app in iOS Simulator.
run: cd examples/mobile && make install
Expand Down
48 changes: 24 additions & 24 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@ default_app = [
default_platform = [
"std",
"android-game-activity",
"android_shared_stdcxx",
"bevy_gilrs",
"bevy_winit",
"default_font",
Expand Down Expand Up @@ -488,35 +487,35 @@ zstd_rust = ["bevy_internal/zstd_rust"]
# For KTX2 Zstandard decompression using [zstd](https://crates.io/crates/zstd). This is a faster backend, but uses unsafe C bindings. For the safe option, stick to the default backend with "zstd_rust".
zstd_c = ["bevy_internal/zstd_c"]

# FLAC audio format support
flac = ["bevy_internal/flac"]
# FLAC audio format support (through `claxon`)
fallback-flac = ["bevy_internal/fallback-flac"]

# MP3 audio format support
mp3 = ["bevy_internal/mp3"]
# OGG/VORBIS audio format support (through `symphonia`)
fallback-vorbis = ["bevy_internal/fallback-vorbis"]

# OGG/VORBIS audio format support
vorbis = ["bevy_internal/vorbis"]
# WAV audio format support (through `hound`)
fallback-wav = ["bevy_internal/fallback-wav"]

# WAV audio format support
wav = ["bevy_internal/wav"]
# AAC audio format support (through `symphonia`)
aac = ["bevy_internal/aac"]

# AAC audio format support (through symphonia)
symphonia-aac = ["bevy_internal/symphonia-aac"]
# AAC, FLAC, MP3, MP4, WAV (through `symphonia`) and OGG/VORBIS (through `lewton`) audio formats support
audio-all = ["bevy_internal/audio-all"]

# AAC, FLAC, MP3, MP4, OGG/VORBIS, and WAV audio formats support (through symphonia)
symphonia-all = ["bevy_internal/symphonia-all"]
# FLAC audio format support (through `symphonia`)
flac = ["bevy_internal/flac"]

# FLAC audio format support (through symphonia)
symphonia-flac = ["bevy_internal/symphonia-flac"]
# MP4 audio format support (through `symphonia`). It also enables AAC support.
mp4 = ["bevy_internal/mp4"]

# MP4 audio format support (through symphonia)
symphonia-isomp4 = ["bevy_internal/symphonia-isomp4"]
# MP3 audio format support (through `symphonia`)
mp3 = ["bevy_internal/mp3"]

# OGG/VORBIS audio format support (through symphonia)
symphonia-vorbis = ["bevy_internal/symphonia-vorbis"]
# OGG/VORBIS audio format support (through `lewton`)
vorbis = ["bevy_internal/vorbis"]

# WAV audio format support (through symphonia)
symphonia-wav = ["bevy_internal/symphonia-wav"]
# WAV audio format support (through `symphonia`)
wav = ["bevy_internal/wav"]

# Enable serialization support through serde
serialize = ["bevy_internal/serialize"]
Expand Down Expand Up @@ -551,9 +550,6 @@ morph = ["bevy_internal/morph"]
# Enables bevy_mesh and bevy_animation morph weight support
morph_animation = ["bevy_internal/morph_animation"]

# Enable using a shared stdlib for cxx on Android
android_shared_stdcxx = ["bevy_internal/android_shared_stdcxx"]

# Enable detailed trace event logging. These trace events are expensive even when off, thus they require compile time opt-in
detailed_trace = ["bevy_internal/detailed_trace"]

Expand Down Expand Up @@ -4923,3 +4919,7 @@ name = "Mirror"
description = "Demonstrates how to create a mirror with a second camera"
category = "3D Rendering"
wasm = true

[patch.crates-io]
cpal = { git = "https://github.com/RustAudio/cpal.git", rev = "e32ee65a5335301553cc9650b949b868f922d748" }
rodio = { git = "https://github.com/philpax/rodio.git", rev = "1a66725ce9ecfd7a89ba780e249477cdd8c644e0" }
37 changes: 19 additions & 18 deletions crates/bevy_audio/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,17 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.18.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.18.0-dev" }

# other
# TODO: Remove `coreaudio-sys` dep below when updating `cpal`.
rodio = { version = "0.20", default-features = false }
rodio = { version = "0.21", default-features = false, features = ["playback"] }
tracing = { version = "0.1", default-features = false, features = ["std"] }

[target.'cfg(target_os = "android")'.dependencies]
cpal = { version = "0.15", optional = true }

[target.'cfg(target_vendor = "apple")'.dependencies]
# NOTE: Explicitly depend on this patch version to fix:
# https://github.com/bevyengine/bevy/issues/18893
coreaudio-sys = { version = "0.2.17", default-features = false }
cpal = { version = "0.16", optional = true }

[target.'cfg(target_arch = "wasm32")'.dependencies]
# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption.
rodio = { version = "0.20", default-features = false, features = [
rodio = { version = "0.21", default-features = false, features = [
"wasm-bindgen",
"playback",
] }
bevy_app = { path = "../bevy_app", version = "0.18.0-dev", default-features = false, features = [
"web",
Expand All @@ -43,18 +38,24 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.18.0-dev", default-featu
] }

[features]
fallback-flac = ["rodio/claxon"]
fallback-wav = ["rodio/hound"]
# TODO: Swap fallback-vorbis and vorbis features
# once https://github.com/RustAudio/rodio/issues/775 is resolved
fallback-vorbis = ["rodio/vorbis"]
mp3 = ["rodio/mp3"]
mp4 = ["rodio/mp4"]
flac = ["rodio/flac"]
wav = ["rodio/wav"]
vorbis = ["rodio/vorbis"]
symphonia-aac = ["rodio/symphonia-aac"]
symphonia-all = ["rodio/symphonia-all"]
symphonia-flac = ["rodio/symphonia-flac"]
symphonia-isomp4 = ["rodio/symphonia-isomp4"]
symphonia-vorbis = ["rodio/symphonia-vorbis"]
symphonia-wav = ["rodio/symphonia-wav"]
# Enable using a shared stdlib for cxx on Android.
android_shared_stdcxx = ["cpal/oboe-shared-stdcxx"]
vorbis = ["rodio/lewton"]
aac = ["rodio/symphonia-aac"]
audio-all = [
"rodio/flac",
"rodio/mp3",
"rodio/mp4",
"rodio/lewton",
"rodio/wav",
]

[lints]
workspace = true
Expand Down
61 changes: 16 additions & 45 deletions crates/bevy_audio/src/audio_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,25 @@ use bevy_asset::{Asset, Assets};
use bevy_ecs::{prelude::*, system::SystemParam};
use bevy_math::Vec3;
use bevy_transform::prelude::GlobalTransform;
use rodio::{OutputStream, OutputStreamHandle, Sink, Source, SpatialSink};
use rodio::{OutputStream, OutputStreamBuilder, Sink, Source, SpatialSink};
use tracing::warn;

use crate::{AudioSink, AudioSinkPlayback};

/// Used internally to play audio on the current "audio device"
///
/// ## Note
///
/// Initializing this resource will leak [`OutputStream`]
/// using [`std::mem::forget`].
/// This is done to avoid storing this in the struct (and making this `!Send`)
/// while preventing it from dropping (to avoid halting of audio).
///
/// This is fine when initializing this once (as is default when adding this plugin),
/// since the memory cost will be the same.
/// However, repeatedly inserting this resource into the app will **leak more memory**.
#[derive(Resource)]
pub(crate) struct AudioOutput {
stream_handle: Option<OutputStreamHandle>,
stream: Option<OutputStream>,
}

impl Default for AudioOutput {
fn default() -> Self {
if let Ok((stream, stream_handle)) = OutputStream::try_default() {
// We leak `OutputStream` to prevent the audio from stopping.
core::mem::forget(stream);
Self {
stream_handle: Some(stream_handle),
}
} else {
warn!("No audio device found.");
Self {
stream_handle: None,
}
}
let stream = OutputStreamBuilder::open_default_stream()
.inspect_err(|_err| {
warn!("No audio device found.");
})
.ok();
Self { stream }
}
}

Expand Down Expand Up @@ -111,12 +94,13 @@ pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
default_spatial_scale: Res<DefaultSpatialScale>,
mut commands: Commands,
) where
f32: rodio::cpal::FromSample<Source::DecoderItem>,
f32: rodio::cpal::FromSample<rodio::Sample>,
{
let Some(stream_handle) = audio_output.stream_handle.as_ref() else {
let Some(stream) = audio_output.stream.as_ref() else {
// audio output unavailable; cannot play sound
return;
};
let mixer = stream.mixer();

for (entity, source_handle, settings, maybe_emitter_transform) in &query_nonplaying {
let Some(audio_source) = audio_sources.get(&source_handle.0) else {
Expand Down Expand Up @@ -144,18 +128,12 @@ pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
Vec3::ZERO.into()
};

let sink = match SpatialSink::try_new(
stream_handle,
let sink = SpatialSink::connect_new(
mixer,
emitter_translation,
(left_ear * scale).into(),
(right_ear * scale).into(),
) {
Ok(sink) => sink,
Err(err) => {
warn!("Error creating spatial sink: {err:?}");
continue;
}
};
);

let decoder = audio_source.decoder();

Expand Down Expand Up @@ -226,14 +204,7 @@ pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
.insert((sink, PlaybackRemoveMarker)),
};
} else {
let sink = match Sink::try_new(stream_handle) {
Ok(sink) => sink,
Err(err) => {
warn!("Error creating sink: {err:?}");
continue;
}
};

let sink = Sink::connect_new(mixer);
let decoder = audio_source.decoder();

match settings.mode {
Expand Down Expand Up @@ -359,7 +330,7 @@ pub(crate) fn cleanup_finished_audio<T: Decodable + Asset>(

/// Run Condition to only play audio if the audio output is available
pub(crate) fn audio_output_available(audio_output: Res<AudioOutput>) -> bool {
audio_output.stream_handle.is_some()
audio_output.stream.is_some()
}

/// Updates spatial audio sinks when emitter positions change.
Expand Down
31 changes: 13 additions & 18 deletions crates/bevy_audio/src/audio_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ impl AsRef<[u8]> for AudioSource {
///
/// This asset loader supports different audio formats based on the enable Bevy features.
/// The feature `bevy/vorbis` enables loading from `.ogg` files and is enabled by default.
/// Other file endings can be loaded from with additional features:
/// Other file extensions can be loaded from with additional features:
/// `.mp3` with `bevy/mp3`
/// `.flac` with `bevy/flac`
/// `.wav` with `bevy/wav`
/// `.flac` with `bevy/flac` or `bevy/fallback-flac`
/// `.wav` with `bevy/wav` or `bevy/fallback-wav`
/// The `bevy/audio-all` feature will enable all file extensions.
#[derive(Default)]
pub struct AudioLoader;

Expand All @@ -57,17 +58,17 @@ impl AssetLoader for AudioLoader {

fn extensions(&self) -> &[&str] {
&[
#[cfg(feature = "mp3")]
#[cfg(any(feature = "mp3", feature = "audio-all"))]
"mp3",
#[cfg(feature = "flac")]
#[cfg(any(feature = "flac", feature = "fallback-flac", feature = "audio-all"))]
"flac",
#[cfg(feature = "wav")]
#[cfg(any(feature = "wav", feature = "fallback-wav", feature = "audio-all"))]
"wav",
#[cfg(feature = "vorbis")]
#[cfg(any(feature = "vorbis", feature = "fallback-vorbis", feature = "audio-all"))]
"oga",
#[cfg(feature = "vorbis")]
#[cfg(any(feature = "vorbis", feature = "fallback-vorbis", feature = "audio-all"))]
"ogg",
#[cfg(feature = "vorbis")]
#[cfg(any(feature = "vorbis", feature = "fallback-vorbis", feature = "audio-all"))]
"spx",
]
}
Expand All @@ -80,22 +81,16 @@ impl AssetLoader for AudioLoader {
/// This trait is implemented for [`AudioSource`].
/// Check the example [`decodable`](https://github.com/bevyengine/bevy/blob/latest/examples/audio/decodable.rs) for how to implement this trait on a custom type.
pub trait Decodable: Send + Sync + 'static {
/// The type of the audio samples.
/// Usually a [`u16`], [`i16`] or [`f32`], as those implement [`rodio::Sample`].
/// Other types can implement the [`rodio::Sample`] trait as well.
type DecoderItem: rodio::Sample + Send + Sync;

/// The type of the iterator of the audio samples,
/// which iterates over samples of type [`Self::DecoderItem`].
/// which iterates over samples of type [`rodio::Sample`].
/// Must be a [`rodio::Source`] so that it can provide information on the audio it is iterating over.
type Decoder: rodio::Source + Send + Iterator<Item = Self::DecoderItem>;
type Decoder: rodio::Source + Send + Iterator<Item = rodio::Sample>;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rodio::Source has Iterator<Item = rodio::Sample> as a bound as far as I can see, should we drop this redundant bound or should we keep it?


/// Build and return a [`Self::Decoder`] of the implementing type
fn decoder(&self) -> Self::Decoder;
}

impl Decodable for AudioSource {
type DecoderItem = <rodio::Decoder<Cursor<AudioSource>> as Iterator>::Item;
type Decoder = rodio::Decoder<Cursor<AudioSource>>;

fn decoder(&self) -> Self::Decoder {
Expand All @@ -115,5 +110,5 @@ pub trait AddAudioSource {
fn add_audio_source<T>(&mut self) -> &mut Self
where
T: Decodable + Asset,
f32: rodio::cpal::FromSample<T::DecoderItem>;
f32: rodio::cpal::FromSample<rodio::Sample>;
}
4 changes: 2 additions & 2 deletions crates/bevy_audio/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pub use audio_source::*;
pub use pitch::*;
pub use volume::*;

pub use rodio::{cpal::Sample as CpalSample, source::Source, Sample};
pub use rodio::{cpal::Sample as CpalSample, source::Source, ChannelCount, Sample, SampleRate};
pub use sinks::*;

use bevy_app::prelude::*;
Expand Down Expand Up @@ -108,7 +108,7 @@ impl AddAudioSource for App {
fn add_audio_source<T>(&mut self) -> &mut Self
where
T: Decodable + Asset,
f32: rodio::cpal::FromSample<T::DecoderItem>,
f32: rodio::cpal::FromSample<Sample>,
{
self.init_asset::<T>().add_systems(
PostUpdate,
Expand Down
1 change: 0 additions & 1 deletion crates/bevy_audio/src/pitch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ impl Pitch {
}

impl Decodable for Pitch {
type DecoderItem = <SineWave as Iterator>::Item;
type Decoder = TakeDuration<SineWave>;

fn decoder(&self) -> Self::Decoder {
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_audio/src/sinks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ mod tests {

#[test]
fn test_audio_sink() {
let (sink, _queue_rx) = Sink::new_idle();
let (sink, _queue_rx) = Sink::new();
let audio_sink = AudioSink::new(sink);
test_audio_sink_playback(audio_sink);
}
Expand Down
Loading
Loading