Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Programmed soundtrack example #12774

Merged
merged 6 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
* FiraMono by The Mozilla Foundation and Telefonica S.A (SIL Open Font License, Version 1.1: assets/fonts/FiraMono-LICENSE)
* Barycentric from [mk_bary_gltf](https://github.com/komadori/mk_bary_gltf) (MIT OR Apache-2.0)
* `MorphStressTest.gltf`, [MorphStressTest] ([CC-BY 4.0] by Analytical Graphics, Inc, Model and textures by Ed Mackey)
* Mysterious accoustic guitar music sample from [florianreichelt](https://freesound.org/people/florianreichelt/sounds/412429/) (CC0 license)

Check warning on line 31 in CREDITS.md

View workflow job for this annotation

GitHub Actions / typos

"accoustic" should be "acoustic".
Copy link
Member

Choose a reason for hiding this comment

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

We can either fix this typo (which came from the link), or allow it in our typos config.

I think I'm in favor of fixing it: the link plus artist name should make this straightforward to discover the source anyways.

* Epic orchestra music sample, modified to loop, from [Migfus20](https://freesound.org/people/Migfus20/sounds/560449/) ([CC BY 4.0 DEED](https://creativecommons.org/licenses/by/4.0/))

[MorphStressTest]: https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/MorphStressTest
[fox]: https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/Fox
Expand Down
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1378,6 +1378,17 @@ description = "Shows how to create and register a custom audio source by impleme
category = "Audio"
wasm = true

[[example]]
name = "soundtrack"
path = "examples/audio/soundtrack.rs"
doc-scrape-examples = true

[package.metadata.example.soundtrack]
name = "Soundtrack"
description = "Shows how to play different soundtracks"
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
category = "Audio"
wasm = true

[[example]]
name = "spatial_audio_2d"
path = "examples/audio/spatial_audio_2d.rs"
Expand Down
Binary file added assets/sounds/Epic orchestra music.ogg
Binary file not shown.
Binary file added assets/sounds/Mysterious accoustic guitar.ogg
Binary file not shown.
154 changes: 154 additions & 0 deletions examples/audio/soundtrack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//! This example illustrates how to load and play different soundtracks
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved

use bevy::prelude::*;

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, (cycle_game_state, fade_in, fade_out))
.add_systems(Update, change_track.run_if(resource_changed::<GameState>))
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
.run();
}

// This resource simulates game states
#[derive(Resource, Default)]
enum GameState {
#[default]
Peaceful,
Battle,
}

// This timer simulates game state changes
#[derive(Resource)]
struct GameStateTimer(Timer);

// This resource will hold the track list for your soundtrack
#[derive(Resource)]
struct SoundtrackPlayer {
track_list: Vec<Handle<AudioSource>>,
}

impl SoundtrackPlayer {
fn new(track_list: Vec<Handle<AudioSource>>) -> Self {
Self {
track_list
}
}
}

// This component will be attached to an entity to fade the audio in
#[derive(Component)]
struct FadeIn;

// This component will be attached to an entity to fade the audio out
#[derive(Component)]
struct FadeOut;

fn setup(asset_server: Res<AssetServer>, mut commands: Commands) {
// Instantiate the game state resources
commands.insert_resource(GameState::default());
commands.insert_resource(GameStateTimer(Timer::from_seconds(10.0, TimerMode::Repeating)));

// Create the track list
let track_1 = asset_server.load::<AudioSource>("sounds/Mysterious accoustic guitar.ogg");

Check warning on line 54 in examples/audio/soundtrack.rs

View workflow job for this annotation

GitHub Actions / typos

"accoustic" should be "acoustic".
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, the way this propagates through is unfortunate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What's the best approach for me to fix this? Do I make a suggestion for every line of code that is affected, or do I "Request Changes" ? Still kind of new to contributing to projects, so unsure of how to proceed from here.

Copy link
Member

Choose a reason for hiding this comment

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

As the author, just push changes to your branch that fix the issue.

let track_2 = asset_server.load::<AudioSource>("sounds/Epic orchestra music.ogg");
let track_list = vec![track_1, track_2];
commands.insert_resource(SoundtrackPlayer::new(track_list));
}


// Every time the GameState resource changes, this system is run to trigger the song change.
fn change_track(
mut commands: Commands,
sountrack_player: Res<SoundtrackPlayer>,

Check warning on line 64 in examples/audio/soundtrack.rs

View workflow job for this annotation

GitHub Actions / typos

"sountrack" should be "soundtrack".
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
soundtrack: Query<Entity, With<AudioSink>>,
game_state: Res<GameState>,
) {
// Fade out all currently running tracks
for track in soundtrack.iter() {
commands.entity(track).insert(FadeOut);
}

// Spawn a new `AudioBundle` with the appropriate soundtrack based on
// the game state.
//
// Volume is set to start at zero and is then increased by the fade_in system.
match game_state.as_ref() {
GameState::Peaceful => {
commands.spawn((
AudioBundle {
source: sountrack_player.track_list.get(0).unwrap().clone(),

Check warning on line 81 in examples/audio/soundtrack.rs

View workflow job for this annotation

GitHub Actions / typos

"sountrack" should be "soundtrack".
settings: PlaybackSettings {
mode: bevy::audio::PlaybackMode::Loop,
volume: bevy::audio::Volume::ZERO,
..default()
}
},
FadeIn,
));
}
GameState::Battle => {
commands.spawn((
AudioBundle {
source: sountrack_player.track_list.get(1).unwrap().clone(),

Check warning on line 94 in examples/audio/soundtrack.rs

View workflow job for this annotation

GitHub Actions / typos

"sountrack" should be "soundtrack".
settings: PlaybackSettings {
mode: bevy::audio::PlaybackMode::Loop,
volume: bevy::audio::Volume::ZERO,
..default()
}
},
FadeIn,
));
}
}
}

// Fade effect duration
const FADE_TIME: f32 = 2.0;

// Fades in the audio of entities that has the FadeIn component. Removes the FadeIn component once
// full volume is reached.
fn fade_in(
mut commands: Commands,
mut audio_sink: Query<(&mut AudioSink, Entity), With<FadeIn>>,
time: Res<Time>,
) {
for (audio, entity) in audio_sink.iter_mut() {
audio.set_volume(audio.volume() + time.delta_seconds() / FADE_TIME);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nothing we can do at this stage for the example but this highlights the need for tweening in the audio engine -- this is going to result in sounding like stepwise motion, because it will only update as fast as the game renders (which is 60 or maybe even 120 Hz which is definitely not fast enough to perceive as gradual).

In this case because the fade is 2 seconds each increment is going to be gradual enough, but shorten the fade time and the problem manifests more clearly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is also something I considered while programming this. Also, I just did a linear fade on the volume setting, but according to the docs, to decrease the perceived volume by half, you should multiply by 0.316. I figured it was good enough at illustrating the concept while still keeping things simple.

if audio.volume() >= 1.0 {
audio.set_volume(1.0);
commands.entity(entity).remove::<FadeIn>();
}
}
}

// Fades out the audio of entities that has the FadeOut component. Despawns the entities once audio
// volume reaches zero.
fn fade_out(
mut commands: Commands,
mut audio_sink: Query<(&mut AudioSink, Entity), With<FadeOut>>,
time: Res<Time>,
) {
for (audio, entity) in audio_sink.iter_mut() {
audio.set_volume(audio.volume() - time.delta_seconds() / FADE_TIME);
if audio.volume() <= 0.0 {
commands.entity(entity).despawn_recursive();
}
}
}

// Every time the timer ends, switches between the "Peaceful" and "Battle" state.
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
fn cycle_game_state(
mut timer: ResMut<GameStateTimer>,
mut game_state: ResMut<GameState>,
time: Res<Time>,
) {
timer.0.tick(time.delta());
if timer.0.just_finished() {
match game_state.as_ref() {
GameState::Battle => *game_state = GameState::Peaceful,
GameState::Peaceful => *game_state = GameState::Battle,
}
}
}