Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_audio/src/audio_output.rs
6601 views
1
use crate::{
2
AudioPlayer, Decodable, DefaultSpatialScale, GlobalVolume, PlaybackMode, PlaybackSettings,
3
SpatialAudioSink, SpatialListener,
4
};
5
use bevy_asset::{Asset, Assets};
6
use bevy_ecs::{prelude::*, system::SystemParam};
7
use bevy_math::Vec3;
8
use bevy_transform::prelude::GlobalTransform;
9
use rodio::{OutputStream, OutputStreamHandle, Sink, Source, SpatialSink};
10
use tracing::warn;
11
12
use crate::{AudioSink, AudioSinkPlayback};
13
14
/// Used internally to play audio on the current "audio device"
15
///
16
/// ## Note
17
///
18
/// Initializing this resource will leak [`OutputStream`]
19
/// using [`std::mem::forget`].
20
/// This is done to avoid storing this in the struct (and making this `!Send`)
21
/// while preventing it from dropping (to avoid halting of audio).
22
///
23
/// This is fine when initializing this once (as is default when adding this plugin),
24
/// since the memory cost will be the same.
25
/// However, repeatedly inserting this resource into the app will **leak more memory**.
26
#[derive(Resource)]
27
pub(crate) struct AudioOutput {
28
stream_handle: Option<OutputStreamHandle>,
29
}
30
31
impl Default for AudioOutput {
32
fn default() -> Self {
33
if let Ok((stream, stream_handle)) = OutputStream::try_default() {
34
// We leak `OutputStream` to prevent the audio from stopping.
35
core::mem::forget(stream);
36
Self {
37
stream_handle: Some(stream_handle),
38
}
39
} else {
40
warn!("No audio device found.");
41
Self {
42
stream_handle: None,
43
}
44
}
45
}
46
}
47
48
/// Marker for internal use, to despawn entities when playback finishes.
49
#[derive(Component, Default)]
50
pub struct PlaybackDespawnMarker;
51
52
/// Marker for internal use, to remove audio components when playback finishes.
53
#[derive(Component, Default)]
54
pub struct PlaybackRemoveMarker;
55
56
#[derive(SystemParam)]
57
pub(crate) struct EarPositions<'w, 's> {
58
pub(crate) query: Query<'w, 's, (Entity, &'static GlobalTransform, &'static SpatialListener)>,
59
}
60
61
impl<'w, 's> EarPositions<'w, 's> {
62
/// Gets a set of transformed ear positions.
63
///
64
/// If there are no listeners, use the default values. If a user has added multiple
65
/// listeners for whatever reason, we will return the first value.
66
pub(crate) fn get(&self) -> (Vec3, Vec3) {
67
let (left_ear, right_ear) = self
68
.query
69
.iter()
70
.next()
71
.map(|(_, transform, settings)| {
72
(
73
transform.transform_point(settings.left_ear_offset),
74
transform.transform_point(settings.right_ear_offset),
75
)
76
})
77
.unwrap_or_else(|| {
78
let settings = SpatialListener::default();
79
(settings.left_ear_offset, settings.right_ear_offset)
80
});
81
82
(left_ear, right_ear)
83
}
84
85
pub(crate) fn multiple_listeners(&self) -> bool {
86
self.query.iter().len() > 1
87
}
88
}
89
90
/// Plays "queued" audio through the [`AudioOutput`] resource.
91
///
92
/// "Queued" audio is any audio entity (with an [`AudioPlayer`] component) that does not have an
93
/// [`AudioSink`]/[`SpatialAudioSink`] component.
94
///
95
/// This system detects such entities, checks if their source asset
96
/// data is available, and creates/inserts the sink.
97
pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
98
audio_output: Res<AudioOutput>,
99
audio_sources: Res<Assets<Source>>,
100
global_volume: Res<GlobalVolume>,
101
query_nonplaying: Query<
102
(
103
Entity,
104
&AudioPlayer<Source>,
105
&PlaybackSettings,
106
Option<&GlobalTransform>,
107
),
108
(Without<AudioSink>, Without<SpatialAudioSink>),
109
>,
110
ear_positions: EarPositions,
111
default_spatial_scale: Res<DefaultSpatialScale>,
112
mut commands: Commands,
113
) where
114
f32: rodio::cpal::FromSample<Source::DecoderItem>,
115
{
116
let Some(stream_handle) = audio_output.stream_handle.as_ref() else {
117
// audio output unavailable; cannot play sound
118
return;
119
};
120
121
for (entity, source_handle, settings, maybe_emitter_transform) in &query_nonplaying {
122
let Some(audio_source) = audio_sources.get(&source_handle.0) else {
123
continue;
124
};
125
// audio data is available (has loaded), begin playback and insert sink component
126
if settings.spatial {
127
let (left_ear, right_ear) = ear_positions.get();
128
129
// We can only use one `SpatialListener`. If there are more than that, then
130
// the user may have made a mistake.
131
if ear_positions.multiple_listeners() {
132
warn!(
133
"Multiple SpatialListeners found. Using {}.",
134
ear_positions.query.iter().next().unwrap().0
135
);
136
}
137
138
let scale = settings.spatial_scale.unwrap_or(default_spatial_scale.0).0;
139
140
let emitter_translation = if let Some(emitter_transform) = maybe_emitter_transform {
141
(emitter_transform.translation() * scale).into()
142
} else {
143
warn!("Spatial AudioPlayer with no GlobalTransform component. Using zero.");
144
Vec3::ZERO.into()
145
};
146
147
let sink = match SpatialSink::try_new(
148
stream_handle,
149
emitter_translation,
150
(left_ear * scale).into(),
151
(right_ear * scale).into(),
152
) {
153
Ok(sink) => sink,
154
Err(err) => {
155
warn!("Error creating spatial sink: {err:?}");
156
continue;
157
}
158
};
159
160
let decoder = audio_source.decoder();
161
162
match settings.mode {
163
PlaybackMode::Loop => match (settings.start_position, settings.duration) {
164
// custom start position and duration
165
(Some(start_position), Some(duration)) => sink.append(
166
decoder
167
.skip_duration(start_position)
168
.take_duration(duration)
169
.repeat_infinite(),
170
),
171
172
// custom start position
173
(Some(start_position), None) => {
174
sink.append(decoder.skip_duration(start_position).repeat_infinite());
175
}
176
177
// custom duration
178
(None, Some(duration)) => {
179
sink.append(decoder.take_duration(duration).repeat_infinite());
180
}
181
182
// full clip
183
(None, None) => sink.append(decoder.repeat_infinite()),
184
},
185
PlaybackMode::Once | PlaybackMode::Despawn | PlaybackMode::Remove => {
186
match (settings.start_position, settings.duration) {
187
(Some(start_position), Some(duration)) => sink.append(
188
decoder
189
.skip_duration(start_position)
190
.take_duration(duration),
191
),
192
193
(Some(start_position), None) => {
194
sink.append(decoder.skip_duration(start_position));
195
}
196
197
(None, Some(duration)) => sink.append(decoder.take_duration(duration)),
198
199
(None, None) => sink.append(decoder),
200
}
201
}
202
}
203
204
let mut sink = SpatialAudioSink::new(sink);
205
206
if settings.muted {
207
sink.mute();
208
}
209
210
sink.set_speed(settings.speed);
211
sink.set_volume(settings.volume * global_volume.volume);
212
213
if settings.paused {
214
sink.pause();
215
}
216
217
match settings.mode {
218
PlaybackMode::Loop | PlaybackMode::Once => commands.entity(entity).insert(sink),
219
PlaybackMode::Despawn => commands
220
.entity(entity)
221
// PERF: insert as bundle to reduce archetype moves
222
.insert((sink, PlaybackDespawnMarker)),
223
PlaybackMode::Remove => commands
224
.entity(entity)
225
// PERF: insert as bundle to reduce archetype moves
226
.insert((sink, PlaybackRemoveMarker)),
227
};
228
} else {
229
let sink = match Sink::try_new(stream_handle) {
230
Ok(sink) => sink,
231
Err(err) => {
232
warn!("Error creating sink: {err:?}");
233
continue;
234
}
235
};
236
237
let decoder = audio_source.decoder();
238
239
match settings.mode {
240
PlaybackMode::Loop => match (settings.start_position, settings.duration) {
241
// custom start position and duration
242
(Some(start_position), Some(duration)) => sink.append(
243
decoder
244
.skip_duration(start_position)
245
.take_duration(duration)
246
.repeat_infinite(),
247
),
248
249
// custom start position
250
(Some(start_position), None) => {
251
sink.append(decoder.skip_duration(start_position).repeat_infinite());
252
}
253
254
// custom duration
255
(None, Some(duration)) => {
256
sink.append(decoder.take_duration(duration).repeat_infinite());
257
}
258
259
// full clip
260
(None, None) => sink.append(decoder.repeat_infinite()),
261
},
262
PlaybackMode::Once | PlaybackMode::Despawn | PlaybackMode::Remove => {
263
match (settings.start_position, settings.duration) {
264
(Some(start_position), Some(duration)) => sink.append(
265
decoder
266
.skip_duration(start_position)
267
.take_duration(duration),
268
),
269
270
(Some(start_position), None) => {
271
sink.append(decoder.skip_duration(start_position));
272
}
273
274
(None, Some(duration)) => sink.append(decoder.take_duration(duration)),
275
276
(None, None) => sink.append(decoder),
277
}
278
}
279
}
280
281
let mut sink = AudioSink::new(sink);
282
283
if settings.muted {
284
sink.mute();
285
}
286
287
sink.set_speed(settings.speed);
288
sink.set_volume(settings.volume * global_volume.volume);
289
290
if settings.paused {
291
sink.pause();
292
}
293
294
match settings.mode {
295
PlaybackMode::Loop | PlaybackMode::Once => commands.entity(entity).insert(sink),
296
PlaybackMode::Despawn => commands
297
.entity(entity)
298
// PERF: insert as bundle to reduce archetype moves
299
.insert((sink, PlaybackDespawnMarker)),
300
PlaybackMode::Remove => commands
301
.entity(entity)
302
// PERF: insert as bundle to reduce archetype moves
303
.insert((sink, PlaybackRemoveMarker)),
304
};
305
}
306
}
307
}
308
309
pub(crate) fn cleanup_finished_audio<T: Decodable + Asset>(
310
mut commands: Commands,
311
query_nonspatial_despawn: Query<
312
(Entity, &AudioSink),
313
(With<PlaybackDespawnMarker>, With<AudioPlayer<T>>),
314
>,
315
query_spatial_despawn: Query<
316
(Entity, &SpatialAudioSink),
317
(With<PlaybackDespawnMarker>, With<AudioPlayer<T>>),
318
>,
319
query_nonspatial_remove: Query<
320
(Entity, &AudioSink),
321
(With<PlaybackRemoveMarker>, With<AudioPlayer<T>>),
322
>,
323
query_spatial_remove: Query<
324
(Entity, &SpatialAudioSink),
325
(With<PlaybackRemoveMarker>, With<AudioPlayer<T>>),
326
>,
327
) {
328
for (entity, sink) in &query_nonspatial_despawn {
329
if sink.sink.empty() {
330
commands.entity(entity).despawn();
331
}
332
}
333
for (entity, sink) in &query_spatial_despawn {
334
if sink.sink.empty() {
335
commands.entity(entity).despawn();
336
}
337
}
338
for (entity, sink) in &query_nonspatial_remove {
339
if sink.sink.empty() {
340
commands.entity(entity).remove::<(
341
AudioPlayer<T>,
342
AudioSink,
343
PlaybackSettings,
344
PlaybackRemoveMarker,
345
)>();
346
}
347
}
348
for (entity, sink) in &query_spatial_remove {
349
if sink.sink.empty() {
350
commands.entity(entity).remove::<(
351
AudioPlayer<T>,
352
SpatialAudioSink,
353
PlaybackSettings,
354
PlaybackRemoveMarker,
355
)>();
356
}
357
}
358
}
359
360
/// Run Condition to only play audio if the audio output is available
361
pub(crate) fn audio_output_available(audio_output: Res<AudioOutput>) -> bool {
362
audio_output.stream_handle.is_some()
363
}
364
365
/// Updates spatial audio sinks when emitter positions change.
366
pub(crate) fn update_emitter_positions(
367
mut emitters: Query<
368
(&GlobalTransform, &SpatialAudioSink, &PlaybackSettings),
369
Or<(Changed<GlobalTransform>, Changed<PlaybackSettings>)>,
370
>,
371
default_spatial_scale: Res<DefaultSpatialScale>,
372
) {
373
for (transform, sink, settings) in emitters.iter_mut() {
374
let scale = settings.spatial_scale.unwrap_or(default_spatial_scale.0).0;
375
376
let translation = transform.translation() * scale;
377
sink.set_emitter_position(translation);
378
}
379
}
380
381
/// Updates spatial audio sink ear positions when spatial listeners change.
382
pub(crate) fn update_listener_positions(
383
mut emitters: Query<(&SpatialAudioSink, &PlaybackSettings)>,
384
changed_listener: Query<
385
(),
386
(
387
Or<(
388
Changed<SpatialListener>,
389
Changed<GlobalTransform>,
390
Changed<PlaybackSettings>,
391
)>,
392
With<SpatialListener>,
393
),
394
>,
395
ear_positions: EarPositions,
396
default_spatial_scale: Res<DefaultSpatialScale>,
397
) {
398
if !default_spatial_scale.is_changed() && changed_listener.is_empty() {
399
return;
400
}
401
402
let (left_ear, right_ear) = ear_positions.get();
403
404
for (sink, settings) in emitters.iter_mut() {
405
let scale = settings.spatial_scale.unwrap_or(default_spatial_scale.0).0;
406
407
sink.set_ears_position(left_ear * scale, right_ear * scale);
408
}
409
}
410
411