
# Objective Fixes #12402 ## Solution Use `despawn_recursive` instead of `despawn` for despawning `PlaybackMode::Despawn` audio. ## Migration Guide `PlaybackSettings::DESPAWN` (`PlaybackMode::Despawn`) now despawns the audio entity's children as well. If you were relying on the previous behavior, you may be able to use `PlaybackMode::Remove`, or you may need to use `PlaybackMode::Once` and manage your audio component lifecycle manually.
331 lines
12 KiB
Rust
331 lines
12 KiB
Rust
use crate::{
|
|
AudioSourceBundle, Decodable, DefaultSpatialScale, GlobalVolume, PlaybackMode,
|
|
PlaybackSettings, SpatialAudioSink, SpatialListener,
|
|
};
|
|
use bevy_asset::{Asset, Assets, Handle};
|
|
use bevy_ecs::{prelude::*, system::SystemParam};
|
|
use bevy_hierarchy::DespawnRecursiveExt;
|
|
use bevy_math::Vec3;
|
|
use bevy_transform::prelude::GlobalTransform;
|
|
use bevy_utils::tracing::warn;
|
|
use rodio::{OutputStream, OutputStreamHandle, Sink, Source, SpatialSink};
|
|
|
|
use crate::AudioSink;
|
|
|
|
/// 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>,
|
|
}
|
|
|
|
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.
|
|
std::mem::forget(stream);
|
|
Self {
|
|
stream_handle: Some(stream_handle),
|
|
}
|
|
} else {
|
|
warn!("No audio device found.");
|
|
Self {
|
|
stream_handle: None,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Marker for internal use, to despawn entities when playback finishes.
|
|
#[derive(Component)]
|
|
pub struct PlaybackDespawnMarker;
|
|
|
|
/// Marker for internal use, to remove audio components when playback finishes.
|
|
#[derive(Component)]
|
|
pub struct PlaybackRemoveMarker;
|
|
|
|
#[derive(SystemParam)]
|
|
pub(crate) struct EarPositions<'w, 's> {
|
|
pub(crate) query: Query<'w, 's, (Entity, &'static GlobalTransform, &'static SpatialListener)>,
|
|
}
|
|
impl<'w, 's> EarPositions<'w, 's> {
|
|
/// Gets a set of transformed ear positions.
|
|
///
|
|
/// If there are no listeners, use the default values. If a user has added multiple
|
|
/// listeners for whatever reason, we will return the first value.
|
|
pub(crate) fn get(&self) -> (Vec3, Vec3) {
|
|
let (left_ear, right_ear) = self
|
|
.query
|
|
.iter()
|
|
.next()
|
|
.map(|(_, transform, settings)| {
|
|
(
|
|
transform.transform_point(settings.left_ear_offset),
|
|
transform.transform_point(settings.right_ear_offset),
|
|
)
|
|
})
|
|
.unwrap_or_else(|| {
|
|
let settings = SpatialListener::default();
|
|
(settings.left_ear_offset, settings.right_ear_offset)
|
|
});
|
|
|
|
(left_ear, right_ear)
|
|
}
|
|
|
|
pub(crate) fn multiple_listeners(&self) -> bool {
|
|
self.query.iter().len() > 1
|
|
}
|
|
}
|
|
|
|
/// Plays "queued" audio through the [`AudioOutput`] resource.
|
|
///
|
|
/// "Queued" audio is any audio entity (with the components from
|
|
/// [`AudioBundle`][crate::AudioBundle] that does not have an
|
|
/// [`AudioSink`]/[`SpatialAudioSink`] component.
|
|
///
|
|
/// This system detects such entities, checks if their source asset
|
|
/// data is available, and creates/inserts the sink.
|
|
pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
|
|
audio_output: Res<AudioOutput>,
|
|
audio_sources: Res<Assets<Source>>,
|
|
global_volume: Res<GlobalVolume>,
|
|
query_nonplaying: Query<
|
|
(
|
|
Entity,
|
|
&Handle<Source>,
|
|
&PlaybackSettings,
|
|
Option<&GlobalTransform>,
|
|
),
|
|
(Without<AudioSink>, Without<SpatialAudioSink>),
|
|
>,
|
|
ear_positions: EarPositions,
|
|
default_spatial_scale: Res<DefaultSpatialScale>,
|
|
mut commands: Commands,
|
|
) where
|
|
f32: rodio::cpal::FromSample<Source::DecoderItem>,
|
|
{
|
|
let Some(stream_handle) = audio_output.stream_handle.as_ref() else {
|
|
// audio output unavailable; cannot play sound
|
|
return;
|
|
};
|
|
|
|
for (entity, source_handle, settings, maybe_emitter_transform) in &query_nonplaying {
|
|
let Some(audio_source) = audio_sources.get(source_handle) else {
|
|
continue;
|
|
};
|
|
// audio data is available (has loaded), begin playback and insert sink component
|
|
if settings.spatial {
|
|
let (left_ear, right_ear) = ear_positions.get();
|
|
|
|
// We can only use one `SpatialListener`. If there are more than that, then
|
|
// the user may have made a mistake.
|
|
if ear_positions.multiple_listeners() {
|
|
warn!(
|
|
"Multiple SpatialListeners found. Using {:?}.",
|
|
ear_positions.query.iter().next().unwrap().0
|
|
);
|
|
}
|
|
|
|
let scale = settings.spatial_scale.unwrap_or(default_spatial_scale.0).0;
|
|
|
|
let emitter_translation = if let Some(emitter_transform) = maybe_emitter_transform {
|
|
(emitter_transform.translation() * scale).into()
|
|
} else {
|
|
warn!("Spatial AudioBundle with no GlobalTransform component. Using zero.");
|
|
Vec3::ZERO.into()
|
|
};
|
|
|
|
let sink = match SpatialSink::try_new(
|
|
stream_handle,
|
|
emitter_translation,
|
|
(left_ear * scale).into(),
|
|
(right_ear * scale).into(),
|
|
) {
|
|
Ok(sink) => sink,
|
|
Err(err) => {
|
|
warn!("Error creating spatial sink: {err:?}");
|
|
continue;
|
|
}
|
|
};
|
|
|
|
sink.set_speed(settings.speed);
|
|
sink.set_volume(settings.volume.0 * global_volume.volume.0);
|
|
|
|
if settings.paused {
|
|
sink.pause();
|
|
}
|
|
|
|
match settings.mode {
|
|
PlaybackMode::Loop => {
|
|
sink.append(audio_source.decoder().repeat_infinite());
|
|
commands.entity(entity).insert(SpatialAudioSink { sink });
|
|
}
|
|
PlaybackMode::Once => {
|
|
sink.append(audio_source.decoder());
|
|
commands.entity(entity).insert(SpatialAudioSink { sink });
|
|
}
|
|
PlaybackMode::Despawn => {
|
|
sink.append(audio_source.decoder());
|
|
commands
|
|
.entity(entity)
|
|
// PERF: insert as bundle to reduce archetype moves
|
|
.insert((SpatialAudioSink { sink }, PlaybackDespawnMarker));
|
|
}
|
|
PlaybackMode::Remove => {
|
|
sink.append(audio_source.decoder());
|
|
commands
|
|
.entity(entity)
|
|
// PERF: insert as bundle to reduce archetype moves
|
|
.insert((SpatialAudioSink { sink }, PlaybackRemoveMarker));
|
|
}
|
|
};
|
|
} else {
|
|
let sink = match Sink::try_new(stream_handle) {
|
|
Ok(sink) => sink,
|
|
Err(err) => {
|
|
warn!("Error creating sink: {err:?}");
|
|
continue;
|
|
}
|
|
};
|
|
|
|
sink.set_speed(settings.speed);
|
|
sink.set_volume(settings.volume.0 * global_volume.volume.0);
|
|
|
|
if settings.paused {
|
|
sink.pause();
|
|
}
|
|
|
|
match settings.mode {
|
|
PlaybackMode::Loop => {
|
|
sink.append(audio_source.decoder().repeat_infinite());
|
|
commands.entity(entity).insert(AudioSink { sink });
|
|
}
|
|
PlaybackMode::Once => {
|
|
sink.append(audio_source.decoder());
|
|
commands.entity(entity).insert(AudioSink { sink });
|
|
}
|
|
PlaybackMode::Despawn => {
|
|
sink.append(audio_source.decoder());
|
|
commands
|
|
.entity(entity)
|
|
// PERF: insert as bundle to reduce archetype moves
|
|
.insert((AudioSink { sink }, PlaybackDespawnMarker));
|
|
}
|
|
PlaybackMode::Remove => {
|
|
sink.append(audio_source.decoder());
|
|
commands
|
|
.entity(entity)
|
|
// PERF: insert as bundle to reduce archetype moves
|
|
.insert((AudioSink { sink }, PlaybackRemoveMarker));
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn cleanup_finished_audio<T: Decodable + Asset>(
|
|
mut commands: Commands,
|
|
query_nonspatial_despawn: Query<
|
|
(Entity, &AudioSink),
|
|
(With<PlaybackDespawnMarker>, With<Handle<T>>),
|
|
>,
|
|
query_spatial_despawn: Query<
|
|
(Entity, &SpatialAudioSink),
|
|
(With<PlaybackDespawnMarker>, With<Handle<T>>),
|
|
>,
|
|
query_nonspatial_remove: Query<
|
|
(Entity, &AudioSink),
|
|
(With<PlaybackRemoveMarker>, With<Handle<T>>),
|
|
>,
|
|
query_spatial_remove: Query<
|
|
(Entity, &SpatialAudioSink),
|
|
(With<PlaybackRemoveMarker>, With<Handle<T>>),
|
|
>,
|
|
) {
|
|
for (entity, sink) in &query_nonspatial_despawn {
|
|
if sink.sink.empty() {
|
|
commands.entity(entity).despawn_recursive();
|
|
}
|
|
}
|
|
for (entity, sink) in &query_spatial_despawn {
|
|
if sink.sink.empty() {
|
|
commands.entity(entity).despawn_recursive();
|
|
}
|
|
}
|
|
for (entity, sink) in &query_nonspatial_remove {
|
|
if sink.sink.empty() {
|
|
commands
|
|
.entity(entity)
|
|
.remove::<(AudioSourceBundle<T>, AudioSink, PlaybackRemoveMarker)>();
|
|
}
|
|
}
|
|
for (entity, sink) in &query_spatial_remove {
|
|
if sink.sink.empty() {
|
|
commands
|
|
.entity(entity)
|
|
.remove::<(AudioSourceBundle<T>, SpatialAudioSink, PlaybackRemoveMarker)>();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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()
|
|
}
|
|
|
|
/// Updates spatial audio sinks when emitter positions change.
|
|
pub(crate) fn update_emitter_positions(
|
|
mut emitters: Query<
|
|
(&GlobalTransform, &SpatialAudioSink, &PlaybackSettings),
|
|
Or<(Changed<GlobalTransform>, Changed<PlaybackSettings>)>,
|
|
>,
|
|
default_spatial_scale: Res<DefaultSpatialScale>,
|
|
) {
|
|
for (transform, sink, settings) in emitters.iter_mut() {
|
|
let scale = settings.spatial_scale.unwrap_or(default_spatial_scale.0).0;
|
|
|
|
let translation = transform.translation() * scale;
|
|
sink.set_emitter_position(translation);
|
|
}
|
|
}
|
|
|
|
/// Updates spatial audio sink ear positions when spatial listeners change.
|
|
pub(crate) fn update_listener_positions(
|
|
mut emitters: Query<(&SpatialAudioSink, &PlaybackSettings)>,
|
|
changed_listener: Query<
|
|
(),
|
|
(
|
|
Or<(
|
|
Changed<SpatialListener>,
|
|
Changed<GlobalTransform>,
|
|
Changed<PlaybackSettings>,
|
|
)>,
|
|
With<SpatialListener>,
|
|
),
|
|
>,
|
|
ear_positions: EarPositions,
|
|
default_spatial_scale: Res<DefaultSpatialScale>,
|
|
) {
|
|
if !default_spatial_scale.is_changed() && changed_listener.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let (left_ear, right_ear) = ear_positions.get();
|
|
|
|
for (sink, settings) in emitters.iter_mut() {
|
|
let scale = settings.spatial_scale.unwrap_or(default_spatial_scale.0).0;
|
|
|
|
sink.set_ears_position(left_ear * scale, right_ear * scale);
|
|
}
|
|
}
|