mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
Audio control - play, pause, volume, speed, loop (#3948)
# Objective - Add ways to control how audio is played ## Solution - playing a sound will return a (weak) handle to an asset that can be used to control playback - if the asset is dropped, it will detach the sink (same behaviour as now)
This commit is contained in:
parent
1ba9818a78
commit
b21c69c60e
6 changed files with 258 additions and 15 deletions
|
@ -304,6 +304,10 @@ path = "examples/async_tasks/external_source_external_thread.rs"
|
|||
name = "audio"
|
||||
path = "examples/audio/audio.rs"
|
||||
|
||||
[[example]]
|
||||
name = "audio_control"
|
||||
path = "examples/audio/audio_control.rs"
|
||||
|
||||
# Diagnostics
|
||||
[[example]]
|
||||
name = "log_diagnostics"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{AudioSource, Decodable};
|
||||
use bevy_asset::{Asset, Handle};
|
||||
use crate::{AudioSink, AudioSource, Decodable};
|
||||
use bevy_asset::{Asset, Handle, HandleId};
|
||||
use parking_lot::RwLock;
|
||||
use std::{collections::VecDeque, fmt};
|
||||
|
||||
|
@ -18,7 +18,7 @@ where
|
|||
Source: Asset + Decodable,
|
||||
{
|
||||
/// Queue for playing audio from asset handles
|
||||
pub queue: RwLock<VecDeque<Handle<Source>>>,
|
||||
pub(crate) queue: RwLock<VecDeque<AudioToPlay<Source>>>,
|
||||
}
|
||||
|
||||
impl<Source: Asset> fmt::Debug for Audio<Source>
|
||||
|
@ -55,7 +55,71 @@ where
|
|||
/// audio.play(asset_server.load("my_sound.ogg"));
|
||||
/// }
|
||||
/// ```
|
||||
pub fn play(&self, audio_source: Handle<Source>) {
|
||||
self.queue.write().push_front(audio_source);
|
||||
///
|
||||
/// Returns a weak [`Handle`] to the [`AudioSink`]. If this handle isn't changed to a
|
||||
/// strong one, the sink will be detached and the sound will continue playing. Changing it
|
||||
/// to a strong handle allows for control on the playback through the [`AudioSink`] asset.
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_ecs::system::Res;
|
||||
/// # use bevy_asset::{AssetServer, Assets};
|
||||
/// # use bevy_audio::{Audio, AudioSink};
|
||||
/// fn play_audio_system(
|
||||
/// asset_server: Res<AssetServer>,
|
||||
/// audio: Res<Audio>,
|
||||
/// audio_sinks: Res<Assets<AudioSink>>,
|
||||
/// ) {
|
||||
/// // This is a weak handle, and can't be used to control playback.
|
||||
/// let weak_handle = audio.play(asset_server.load("my_sound.ogg"));
|
||||
/// // This is now a strong handle, and can be used to control playback.
|
||||
/// let strong_handle = audio_sinks.get_handle(weak_handle);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn play(&self, audio_source: Handle<Source>) -> Handle<AudioSink> {
|
||||
let id = HandleId::random::<AudioSink>();
|
||||
let config = AudioToPlay {
|
||||
repeat: false,
|
||||
sink_handle: id,
|
||||
source_handle: audio_source,
|
||||
};
|
||||
self.queue.write().push_back(config);
|
||||
Handle::<AudioSink>::weak(id)
|
||||
}
|
||||
|
||||
/// Play audio from a [`Handle`] to the audio source in a loop
|
||||
///
|
||||
/// See [`Self::play`] on how to control playback.
|
||||
pub fn play_in_loop(&self, audio_source: Handle<Source>) -> Handle<AudioSink> {
|
||||
let id = HandleId::random::<AudioSink>();
|
||||
let config = AudioToPlay {
|
||||
repeat: true,
|
||||
sink_handle: id,
|
||||
source_handle: audio_source,
|
||||
};
|
||||
self.queue.write().push_back(config);
|
||||
Handle::<AudioSink>::weak(id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub(crate) struct AudioToPlay<Source>
|
||||
where
|
||||
Source: Asset + Decodable,
|
||||
{
|
||||
pub(crate) sink_handle: HandleId,
|
||||
pub(crate) source_handle: Handle<Source>,
|
||||
pub(crate) repeat: bool,
|
||||
}
|
||||
|
||||
impl<Source> fmt::Debug for AudioToPlay<Source>
|
||||
where
|
||||
Source: Asset + Decodable,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("AudioToPlay")
|
||||
.field("sink_handle", &self.sink_handle)
|
||||
.field("source_handle", &self.source_handle)
|
||||
.field("repeat", &self.repeat)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use crate::{Audio, AudioSource, Decodable};
|
||||
use bevy_asset::{Asset, Assets};
|
||||
use bevy_ecs::world::World;
|
||||
use bevy_reflect::TypeUuid;
|
||||
use bevy_utils::tracing::warn;
|
||||
use rodio::{OutputStream, OutputStreamHandle, Sink};
|
||||
use rodio::{OutputStream, OutputStreamHandle, Sink, Source};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// Used internally to play audio on the current "audio device"
|
||||
|
@ -41,25 +42,39 @@ impl<Source> AudioOutput<Source>
|
|||
where
|
||||
Source: Asset + Decodable,
|
||||
{
|
||||
fn play_source(&self, audio_source: &Source) {
|
||||
fn play_source(&self, audio_source: &Source, repeat: bool) -> Option<Sink> {
|
||||
if let Some(stream_handle) = &self.stream_handle {
|
||||
let sink = Sink::try_new(stream_handle).unwrap();
|
||||
sink.append(audio_source.decoder());
|
||||
sink.detach();
|
||||
if repeat {
|
||||
sink.append(audio_source.decoder().repeat_infinite());
|
||||
} else {
|
||||
sink.append(audio_source.decoder());
|
||||
}
|
||||
Some(sink)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn try_play_queued(&self, audio_sources: &Assets<Source>, audio: &mut Audio<Source>) {
|
||||
fn try_play_queued(
|
||||
&self,
|
||||
audio_sources: &Assets<Source>,
|
||||
audio: &mut Audio<Source>,
|
||||
sinks: &mut Assets<AudioSink>,
|
||||
) {
|
||||
let mut queue = audio.queue.write();
|
||||
let len = queue.len();
|
||||
let mut i = 0;
|
||||
while i < len {
|
||||
let audio_source_handle = queue.pop_back().unwrap();
|
||||
if let Some(audio_source) = audio_sources.get(&audio_source_handle) {
|
||||
self.play_source(audio_source);
|
||||
let config = queue.pop_front().unwrap();
|
||||
if let Some(audio_source) = audio_sources.get(&config.source_handle) {
|
||||
if let Some(sink) = self.play_source(audio_source, config.repeat) {
|
||||
// don't keep the strong handle. there is no way to return it to the user here as it is async
|
||||
let _ = sinks.set(config.sink_handle, AudioSink { sink: Some(sink) });
|
||||
}
|
||||
} else {
|
||||
// audio source hasn't loaded yet. add it back to the queue
|
||||
queue.push_front(audio_source_handle);
|
||||
queue.push_back(config);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
@ -74,8 +89,101 @@ where
|
|||
let world = world.cell();
|
||||
let audio_output = world.get_non_send::<AudioOutput<Source>>().unwrap();
|
||||
let mut audio = world.get_resource_mut::<Audio<Source>>().unwrap();
|
||||
let mut sinks = world.get_resource_mut::<Assets<AudioSink>>().unwrap();
|
||||
|
||||
if let Some(audio_sources) = world.get_resource::<Assets<Source>>() {
|
||||
audio_output.try_play_queued(&*audio_sources, &mut *audio);
|
||||
audio_output.try_play_queued(&*audio_sources, &mut *audio, &mut *sinks);
|
||||
};
|
||||
}
|
||||
|
||||
/// Asset controlling the playback of a sound
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_ecs::system::{Local, Res};
|
||||
/// # use bevy_asset::{Assets, Handle};
|
||||
/// # use bevy_audio::AudioSink;
|
||||
/// // Execution of this system should be controlled by a state or input,
|
||||
/// // otherwise it would just toggle between play and pause every frame.
|
||||
/// fn pause(
|
||||
/// audio_sinks: Res<Assets<AudioSink>>,
|
||||
/// music_controller: Local<Handle<AudioSink>>,
|
||||
/// ) {
|
||||
/// if let Some(sink) = audio_sinks.get(&*music_controller) {
|
||||
/// if sink.is_paused() {
|
||||
/// sink.play()
|
||||
/// } else {
|
||||
/// sink.pause()
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
#[derive(TypeUuid)]
|
||||
#[uuid = "8BEE570C-57C2-4FC0-8CFB-983A22F7D981"]
|
||||
pub struct AudioSink {
|
||||
// This field is an Option in order to allow us to have a safe drop that will detach the sink.
|
||||
// It will never be None during its life
|
||||
sink: Option<Sink>,
|
||||
}
|
||||
|
||||
impl Drop for AudioSink {
|
||||
fn drop(&mut self) {
|
||||
self.sink.take().unwrap().detach();
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioSink {
|
||||
/// Gets the volume of the sound.
|
||||
///
|
||||
/// The value `1.0` is the "normal" volume (unfiltered input). Any value other than `1.0`
|
||||
/// will multiply each sample by this value.
|
||||
pub fn volume(&self) -> f32 {
|
||||
self.sink.as_ref().unwrap().volume()
|
||||
}
|
||||
|
||||
/// Changes the volume of the sound.
|
||||
///
|
||||
/// The value `1.0` is the "normal" volume (unfiltered input). Any value other than `1.0`
|
||||
/// will multiply each sample by this value.
|
||||
pub fn set_volume(&self, volume: f32) {
|
||||
self.sink.as_ref().unwrap().set_volume(volume);
|
||||
}
|
||||
|
||||
/// Gets the speed of the sound.
|
||||
///
|
||||
/// The value `1.0` is the "normal" speed (unfiltered input). Any value other than `1.0`
|
||||
/// will change the play speed of the sound.
|
||||
pub fn speed(&self) -> f32 {
|
||||
self.sink.as_ref().unwrap().speed()
|
||||
}
|
||||
|
||||
/// Changes the speed of the sound.
|
||||
///
|
||||
/// The value `1.0` is the "normal" speed (unfiltered input). Any value other than `1.0`
|
||||
/// will change the play speed of the sound.
|
||||
pub fn set_speed(&self, speed: f32) {
|
||||
self.sink.as_ref().unwrap().set_speed(speed);
|
||||
}
|
||||
|
||||
/// Resumes playback of a paused sink.
|
||||
///
|
||||
/// No effect if not paused.
|
||||
pub fn play(&self) {
|
||||
self.sink.as_ref().unwrap().play();
|
||||
}
|
||||
|
||||
/// Pauses playback of this sink.
|
||||
///
|
||||
/// No effect if already paused.
|
||||
/// A paused sink can be resumed with [`play`](Self::play).
|
||||
pub fn pause(&self) {
|
||||
self.sink.as_ref().unwrap().pause();
|
||||
}
|
||||
|
||||
/// Is this sink paused?
|
||||
///
|
||||
/// Sinks can be paused and resumed using [`pause`](Self::pause) and [`play`](Self::play).
|
||||
pub fn is_paused(&self) -> bool {
|
||||
self.sink.as_ref().unwrap().is_paused()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@ impl Plugin for AudioPlugin {
|
|||
fn build(&self, app: &mut App) {
|
||||
app.init_non_send_resource::<AudioOutput<AudioSource>>()
|
||||
.add_asset::<AudioSource>()
|
||||
.add_asset::<AudioSink>()
|
||||
.init_resource::<Audio<AudioSource>>()
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
|
|
|
@ -153,6 +153,7 @@ Example | File | Description
|
|||
Example | File | Description
|
||||
--- | --- | ---
|
||||
`audio` | [`audio/audio.rs`](./audio/audio.rs) | Shows how to load and play an audio file
|
||||
`audio_control` | [`audio/audio_control.rs`](./audio/audio_control.rs) | Shows how to load and play an audio file, and control how it's played
|
||||
|
||||
## Diagnostics
|
||||
|
||||
|
|
65
examples/audio/audio_control.rs
Normal file
65
examples/audio/audio_control.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use bevy::{audio::AudioSink, prelude::*};
|
||||
|
||||
/// This example illustrates how to load and play an audio file, and control how it's played
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_startup_system(setup)
|
||||
.add_system(update_speed)
|
||||
.add_system(pause)
|
||||
.add_system(volume)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
audio: Res<Audio>,
|
||||
audio_sinks: Res<Assets<AudioSink>>,
|
||||
) {
|
||||
let music = asset_server.load("sounds/Windless Slopes.ogg");
|
||||
let handle = audio_sinks.get_handle(audio.play(music));
|
||||
commands.insert_resource(MusicController(handle));
|
||||
}
|
||||
|
||||
struct MusicController(Handle<AudioSink>);
|
||||
|
||||
fn update_speed(
|
||||
audio_sinks: Res<Assets<AudioSink>>,
|
||||
music_controller: Res<MusicController>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
if let Some(sink) = audio_sinks.get(&music_controller.0) {
|
||||
sink.set_speed(((time.seconds_since_startup() / 5.0).sin() as f32 + 1.0).max(0.1));
|
||||
}
|
||||
}
|
||||
|
||||
fn pause(
|
||||
keyboard_input: Res<Input<KeyCode>>,
|
||||
audio_sinks: Res<Assets<AudioSink>>,
|
||||
music_controller: Res<MusicController>,
|
||||
) {
|
||||
if keyboard_input.just_pressed(KeyCode::Space) {
|
||||
if let Some(sink) = audio_sinks.get(&music_controller.0) {
|
||||
if sink.is_paused() {
|
||||
sink.play()
|
||||
} else {
|
||||
sink.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn volume(
|
||||
keyboard_input: Res<Input<KeyCode>>,
|
||||
audio_sinks: Res<Assets<AudioSink>>,
|
||||
music_controller: Res<MusicController>,
|
||||
) {
|
||||
if let Some(sink) = audio_sinks.get(&music_controller.0) {
|
||||
if keyboard_input.just_pressed(KeyCode::Plus) {
|
||||
sink.set_volume(sink.volume() + 0.1);
|
||||
} else if keyboard_input.just_pressed(KeyCode::Minus) {
|
||||
sink.set_volume(sink.volume() - 0.1);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue