Skip to content

Commit

Permalink
audio overhaul
Browse files Browse the repository at this point in the history
  • Loading branch information
mikeder committed Jun 27, 2023
1 parent c6232f2 commit ebe86ce
Show file tree
Hide file tree
Showing 12 changed files with 619 additions and 471 deletions.
674 changes: 339 additions & 335 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "turtle_time"
version = "0.9.0"
version = "0.10.0"
publish = false
authors = ["Mike Eder <[email protected]>"]
edition = "2021"
Expand Down
Binary file added assets/audio/fireball1.ogg
Binary file not shown.
Binary file added assets/audio/pickup.ogg
Binary file not shown.
Binary file added assets/audio/sprinting.ogg
Binary file not shown.
File renamed without changes.
35 changes: 0 additions & 35 deletions src/actions/game_control.rs

This file was deleted.

36 changes: 0 additions & 36 deletions src/actions/mod.rs

This file was deleted.

200 changes: 163 additions & 37 deletions src/audio.rs
Original file line number Diff line number Diff line change
@@ -1,60 +1,186 @@
use crate::actions::{set_movement_actions, Actions};
use crate::loading::AudioAssets;
use crate::AppState;
use std::time::Duration;

use crate::player::components::Expired;
use crate::{AppState, FPS};
use bevy::core::FrameCount;
use bevy::prelude::*;
use bevy::utils::{HashMap, HashSet};
use bevy_ggrs::Rollback;
use bevy_kira_audio::prelude::*;
use bevy_kira_audio::{Audio, AudioPlugin};

const MAX_SOUND_DELAY_FRAMES: u32 = 10;

pub struct InternalAudioPlugin;

// This plugin is responsible to control the game audio
impl Plugin for InternalAudioPlugin {
fn build(&self, app: &mut App) {
app.add_plugin(AudioPlugin)
.add_system(start_audio.in_schedule(OnEnter(AppState::MenuMain)))
.add_system(
control_flying_sound
.after(set_movement_actions)
.in_set(OnUpdate(AppState::RoundOnline)),
)
.add_system(
control_flying_sound
.after(set_movement_actions)
.in_set(OnUpdate(AppState::RoundLocal)),
);
.init_resource::<PlaybackStates>()
.add_system(init_audio.in_schedule(OnExit(AppState::Loading)))
.add_system(sync_rollback_sounds)
.add_system(remove_finished_sounds)
.add_system(update_looped_sounds);
}
}

#[derive(Resource)]
struct FlyingAudio(Handle<AudioInstance>);
/// Rollback Audio
/// https://johanhelsing.studio/posts/cargo-space-devlog-4
#[derive(Component)]
pub struct RollbackSound {
/// the actual sound effect to play
pub clip: Handle<AudioSource>,
/// when the sound effect should have started playing
pub start_frame: u32,
/// differentiates several unique instances of the same sound playing at once.
/// for example, two players shooting at the same time
pub sub_key: u32,
}

impl RollbackSound {
pub fn key(&self) -> (Handle<AudioSource>, usize) {
(self.clip.clone(), self.sub_key as usize)
}
}

#[derive(Bundle)]
pub struct RollbackSoundBundle {
pub sound: RollbackSound,
/// an id to make sure that the entity will be removed in case of rollbacks
pub rollback: Rollback,
}

fn start_audio(mut commands: Commands, audio_assets: Res<AudioAssets>, audio: Res<Audio>) {
audio.pause();
let handle = audio
.play(audio_assets.flying.clone())
.looped()
.with_volume(0.3)
.handle();
commands.insert_resource(FlyingAudio(handle));
fn init_audio(audio: Res<Audio>) {
// todo: volume control in options
let volume = 0.3;
audio.set_volume(volume);
}

fn control_flying_sound(
actions: Res<Actions>,
audio: Res<FlyingAudio>,
/// Actual playback states, managed by sync_rollback_sounds system below.
#[derive(Resource, Reflect, Default)]
struct PlaybackStates {
playing: HashMap<(Handle<AudioSource>, usize), Handle<AudioInstance>>,
}

fn sync_rollback_sounds(
mut current_state: ResMut<PlaybackStates>,
mut audio_instances: ResMut<Assets<AudioInstance>>,
desired_query: Query<&RollbackSound>,
audio: Res<Audio>,
frame: Res<FrameCount>,
) {
if let Some(instance) = audio_instances.get_mut(&audio.0) {
match instance.state() {
PlaybackState::Paused { .. } => {
if actions.player_movement.is_some() {
instance.resume(AudioTween::default());
// remove any finished sound effects
current_state.playing.retain(|_, handle| {
!matches!(
audio_instances.state(handle),
PlaybackState::Stopped | PlaybackState::Stopping { .. }
)
});

let mut live = HashSet::new();

// start/update sound effects
for rollback_sound in desired_query.iter() {
let key = rollback_sound.key();
if current_state.playing.contains_key(&key) {
// already playing
// todo: compare frames and seek if time critical
} else {
let frames_late = frame.0 - rollback_sound.start_frame;
// ignore any sound effects that are *really* late
// todo: make configurable
if frames_late <= MAX_SOUND_DELAY_FRAMES {
if frames_late > 0 {
// todo: seek if time critical
info!(
"playing sound effect {} frames late",
frame.0 - rollback_sound.start_frame
);
}
let instance_handle = audio.play(rollback_sound.clip.clone()).handle();
current_state
.playing
.insert(key.to_owned(), instance_handle);
}
PlaybackState::Playing { .. } => {
if actions.player_movement.is_none() {
instance.pause(AudioTween::default());
}
}

// we keep track of `RollbackSound`s still existing,
// so we can remove any sound effects not present later
live.insert(rollback_sound.key().to_owned());
}

// stop interrupted sound effects
for (_, instance_handle) in current_state
.playing
.drain_filter(|key, _| !live.contains(key))
{
if let Some(instance) = audio_instances.get_mut(&instance_handle) {
// todo: add config to use linear tweening, stop or keep playing as appropriate
// instance.stop(default()); // immediate
instance.stop(AudioTween::linear(Duration::from_millis(100)));
} else {
error!("Audio instance not found");
}
}
}

fn remove_finished_sounds(
frame: Res<FrameCount>,
query: Query<(Entity, &RollbackSound)>,
mut commands: Commands,
audio_sources: Res<Assets<AudioSource>>,
) {
for (entity, sfx) in query.iter() {
// perf: cache frames_to_play instead of checking audio_sources every frame?
if let Some(audio_source) = audio_sources.get(&sfx.clip) {
let frames_played = frame.0 - sfx.start_frame;
let seconds_to_play = audio_source.sound.duration().as_secs_f64();
let frames_to_play = (seconds_to_play * FPS as f64) as u32;

if frames_played >= frames_to_play {
commands.entity(entity).insert(Expired);
}
_ => {}
}
}
}

#[derive(Component, Default, Reflect)]
#[reflect(Component)]
pub struct FadedLoopSound {
/// The actual sound playing, if any
pub audio_instance: Option<Handle<AudioInstance>>,
/// The sound to play
pub clip: Handle<AudioSource>,
/// number of seconds to fade in
pub fade_in: f32,
/// number of seconds to fade out
pub fade_out: f32,
/// whether the sound effect should be playing or not
pub should_play: bool,
}

fn update_looped_sounds(
mut sounds: Query<&mut FadedLoopSound>,
mut audio_instances: ResMut<Assets<AudioInstance>>,
audio: Res<Audio>,
) {
for mut sound in sounds.iter_mut() {
if sound.should_play {
if sound.audio_instance.is_none() {
sound.audio_instance = Some(
audio
.play(sound.clip.clone())
.looped()
.linear_fade_in(Duration::from_secs_f32(sound.fade_in))
.handle(),
);
}
} else if let Some(instance_handle) = sound.audio_instance.take() {
if let Some(instance) = audio_instances.get_mut(&instance_handle) {
instance.stop(AudioTween::linear(Duration::from_secs_f32(sound.fade_out)));
}
};
}
}
3 changes: 0 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
mod actions;
mod ascii;
mod audio;
pub mod debug;
Expand All @@ -9,7 +8,6 @@ mod menu;
pub mod npc;
pub mod player;

use crate::actions::ActionsPlugin;
use crate::audio::InternalAudioPlugin;
use crate::loading::LoadingPlugin;
use ascii::AsciiPlugin;
Expand Down Expand Up @@ -84,7 +82,6 @@ impl Plugin for GamePlugin {
.add_plugin(GraphicsPlugin)
.add_plugin(TileMapPlugin)
.add_plugin(MenuPlugin)
.add_plugin(ActionsPlugin)
.add_plugin(InternalAudioPlugin)
.add_plugin(PlayerPlugin)
.add_plugin(GoosePlugin)
Expand Down
14 changes: 12 additions & 2 deletions src/loading.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,18 @@ pub struct FontAssets {

#[derive(AssetCollection, Resource)]
pub struct AudioAssets {
#[asset(path = "audio/flying.ogg")]
pub flying: Handle<AudioSource>,
#[asset(path = "audio/fireball1.ogg")]
pub fireball_shot: Handle<AudioSource>,
#[asset(path = "audio/fireball1.ogg")]
pub fireball_hit: Handle<AudioSource>,
#[asset(path = "audio/fireball1.ogg")]
pub fireball_miss: Handle<AudioSource>,
#[asset(path = "audio/walking.ogg")]
pub walking: Handle<AudioSource>,
#[asset(path = "audio/sprinting.ogg")]
pub sprinting: Handle<AudioSource>,
#[asset(path = "audio/pickup.ogg")]
pub pickup: Handle<AudioSource>,
}

#[derive(AssetCollection, Resource)]
Expand Down
Loading

0 comments on commit ebe86ce

Please sign in to comment.