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:
François 2022-03-01 01:12:11 +00:00
parent 1ba9818a78
commit b21c69c60e
6 changed files with 258 additions and 15 deletions

View file

@ -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"

View file

@ -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()
}
}

View file

@ -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();
if repeat {
sink.append(audio_source.decoder().repeat_infinite());
} else {
sink.append(audio_source.decoder());
sink.detach();
}
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()
}
}

View file

@ -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,

View file

@ -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

View 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);
}
}
}