diff --git a/voxygen/src/audio/channel.rs b/voxygen/src/audio/channel.rs index 8c65e6dc98..8a88923f95 100644 --- a/voxygen/src/audio/channel.rs +++ b/voxygen/src/audio/channel.rs @@ -1,3 +1,21 @@ +//! Distinct audio playback channels for music and sound effects +//! +//! Voxygen's audio system uses a limited number of channels to play multiple +//! sounds simultaneously. Each additional channel used decreases performance +//! in-game, so the amount of channels utilized should be kept to a minimum. +//! +//! When constructing a new [`AudioFrontend`](../struct.AudioFrontend.html), two +//! music channels are created internally (to achieve crossover fades) while the +//! number of sfx channels are determined by the `max_sfx_channels` value +//! defined in the client +//! [`AudioSettings`](../../settings/struct.AudioSettings.html) +//! +//! When the AudioFrontend's +//! [`play_sfx`](../struct.AudioFrontend.html#method.play_sfx) +//! methods is called, it attempts to retrieve an SfxChannel for playback. If +//! the channel capacity has been reached and all channels are occupied, a +//! warning is logged, and no sound is played. + use crate::audio::fader::{FadeDirection, Fader}; use rodio::{Device, Sample, Sink, Source, SpatialSink}; use vek::*; @@ -9,8 +27,11 @@ enum ChannelState { Stopped, } -/// Each MusicChannel has a MusicChannelTag which help us determine how we -/// should transition between music types +/// Each `MusicChannel` has a `MusicChannelTag` which help us determine when we +/// should transition between two types of in-game music. For example, we +/// transition between `TitleMusic` and `Exploration` when a player enters the +/// world by crossfading over a slow duration. In the future, transitions in the +/// world such as `Exploration` -> `BossBattle` would transition more rapidly. #[derive(PartialEq, Clone, Copy)] pub enum MusicChannelTag { TitleMusic, @@ -18,7 +39,9 @@ pub enum MusicChannelTag { } /// A MusicChannel uses a non-positional audio sink designed to play music which -/// is always heard at the player's position +/// is always heard at the player's position. +/// +/// See also: [`Rodio::Sink`](https://docs.rs/rodio/0.11.0/rodio/struct.Sink.html) pub struct MusicChannel { tag: MusicChannelTag, sink: Sink, @@ -111,6 +134,8 @@ impl MusicChannel { /// An SfxChannel uses a positional audio sink, and is designed for short-lived /// audio which can be spatially controlled, but does not need control over /// playback or fading/transitions +/// +/// See also: [`Rodio::SpatialSink`](https://docs.rs/rodio/0.11.0/rodio/struct.SpatialSink.html) pub struct SfxChannel { sink: SpatialSink, pub pos: Vec3, diff --git a/voxygen/src/audio/fader.rs b/voxygen/src/audio/fader.rs index a6f6cdad23..b2c6c8ca16 100644 --- a/voxygen/src/audio/fader.rs +++ b/voxygen/src/audio/fader.rs @@ -1,3 +1,7 @@ +//! Controls volume transitions for Audio Channels + +/// Faders are attached to channels with initial and target volumes as well as a +/// transition time. #[derive(PartialEq, Clone, Copy)] pub struct Fader { length: f32, @@ -6,6 +10,8 @@ pub struct Fader { volume_to: f32, is_running: bool, } +/// Enables quick lookup of whether a fader is increasing or decreasing the +/// channel volume #[derive(Debug, PartialEq, Clone, Copy)] pub enum FadeDirection { In, @@ -29,6 +35,11 @@ impl Fader { pub fn fade_out(time: f32, volume_from: f32) -> Self { Self::fade(time, volume_from, 0.0) } + /// Used to update the `target` volume of the fader when the max or min + /// volume changes. This occurs when the player changes their in-game + /// volume setting during a fade. Updating the target in this case prevents + /// the final fade volume from falling outside of the newly configured + /// volume range. pub fn update_target_volume(&mut self, volume: f32) { match self.direction() { FadeDirection::In => { @@ -50,10 +61,10 @@ impl Fader { } } - #[allow(clippy::assign_op_pattern)] // TODO: Pending review in #587 + /// Called each tick to update the volume and state pub fn update(&mut self, dt: f32) { if self.is_running { - self.running_time = self.running_time + dt; + self.running_time += dt; if self.running_time >= self.length { self.running_time = self.length; self.is_running = false; diff --git a/voxygen/src/audio/mod.rs b/voxygen/src/audio/mod.rs index 68a7cab77d..9617a8fea8 100644 --- a/voxygen/src/audio/mod.rs +++ b/voxygen/src/audio/mod.rs @@ -1,3 +1,5 @@ +//! Handles audio device detection and playback of sound effects and music + pub mod channel; pub mod fader; pub mod music; @@ -15,6 +17,10 @@ use vek::*; const FALLOFF: f32 = 0.13; +/// Holds information about the system audio devices and internal channels used +/// for sfx and music playback. An instance of `AudioFrontend` is used by +/// Voxygen's [`GlobalState`](../struct.GlobalState.html#structfield.audio) to +/// provide access to devices and playback control in-game pub struct AudioFrontend { pub device: String, pub device_list: Vec, @@ -136,6 +142,7 @@ impl AudioFrontend { self.music_channels.last_mut() } + /// Play (once) an sfx file by file path at the give position and volume pub fn play_sfx(&mut self, sound: &str, pos: Vec3, vol: Option) { if self.audio_device.is_some() { let calc_pos = ((pos - self.listener_pos) * FALLOFF).into_array(); @@ -192,6 +199,8 @@ impl AudioFrontend { } } + /// Switches the playing music to the title music, which is pinned to a + /// specific sound file (veloren_title_tune.ogg) pub fn play_title_music(&mut self) { if self.music_enabled() { self.play_music( diff --git a/voxygen/src/audio/music.rs b/voxygen/src/audio/music.rs index d3469782f8..6d86908666 100644 --- a/voxygen/src/audio/music.rs +++ b/voxygen/src/audio/music.rs @@ -1,3 +1,43 @@ +//! Handles music playback and transitions +//! +//! Game music is controlled though a configuration file found in the source at +//! `/assets/voxygen/audio/soundtrack.ron`. Each track enabled in game has a +//! configuration corresponding to the +//! [`SoundtrackItem`](struct.SoundtrackItem.html) format, as well as the +//! corresponding `.ogg` file in the `/assets/voxygen/audio/soundtrack/` +//! directory. +//! +//! If there are errors while reading or deserialising the configuration file, a +//! warning is logged and music will be disabled. +//! +//! ## Adding new music +//! +//! To add a new item, append the details to the audio configuration file, and +//! add the audio file (in `.ogg` format) to the assets directory. +//! +//! The `length` should be provided in seconds. This allows us to know when to +//! transition to another track, without having to spend time determining track +//! length programmatically. +//! +//! An example of a new night time track: +//! ```text +//! ( +//! title: "Sleepy Song", +//! path: "voxygen.audio.soundtrack.sleepy", +//! length: 400.0, +//! timing: Some(Night), +//! artist: "Elvis", +//! ), +//! ``` +//! +//! Before sending an MR for your new track item: +//! - Be conscious of the file size for your new track. Assets contribute to +//! download sizes +//! - Ensure that the track is mastered to a volume proportionate to other music +//! tracks +//! - If you are not the author of the track, ensure that the song's licensing +//! permits usage of the track for non-commercial use + use crate::audio::AudioFrontend; use common::{assets, state::State}; use rand::{seq::IteratorRandom, thread_rng}; @@ -7,29 +47,38 @@ use std::time::Instant; const DAY_START_SECONDS: u32 = 28800; // 8:00 const DAY_END_SECONDS: u32 = 70200; // 19:30 -#[derive(Debug, Deserialize)] +#[derive(Debug, Default, Deserialize)] struct SoundtrackCollection { tracks: Vec, } +/// Configuration for a single music track in the soundtrack #[derive(Debug, Deserialize)] pub struct SoundtrackItem { title: String, path: String, + /// Length of the track in seconds length: f64, + /// Whether this track should play during day or night timing: Option, } +/// Allows control over when a track should play based on in-game time of day #[derive(Debug, Deserialize, PartialEq)] enum DayPeriod { - Day, // 8:00 AM to 7:30 PM - Night, // 7:31 PM to 6:59 AM + /// 8:00 AM to 7:30 PM + Day, + /// 7:31 PM to 6:59 AM + Night, } +/// Provides methods to control music playback pub struct MusicMgr { soundtrack: SoundtrackCollection, began_playing: Instant, next_track_change: f64, + /// The title of the last track played. Used to prevent a track + /// being played twice in a row last_track: String, } @@ -44,8 +93,11 @@ impl MusicMgr { } } + /// Checks whether the previous track has completed. If so, sends a + /// request to play the next (random) track pub fn maintain(&mut self, audio: &mut AudioFrontend, state: &State) { if audio.music_enabled() + && !self.soundtrack.tracks.is_empty() && self.began_playing.elapsed().as_secs_f64() > self.next_track_change { self.play_random_track(audio, state); @@ -59,7 +111,7 @@ impl MusicMgr { let current_period_of_day = Self::get_current_day_period(game_time); let mut rng = thread_rng(); - let track = self + let maybe_track = self .soundtrack .tracks .iter() @@ -70,14 +122,15 @@ impl MusicMgr { None => true, } }) - .choose(&mut rng) - .expect("Failed to select a random track"); + .choose(&mut rng); - self.last_track = String::from(&track.title); - self.began_playing = Instant::now(); - self.next_track_change = track.length + SILENCE_BETWEEN_TRACKS_SECONDS; + if let Some(track) = maybe_track { + self.last_track = String::from(&track.title); + self.began_playing = Instant::now(); + self.next_track_change = track.length + SILENCE_BETWEEN_TRACKS_SECONDS; - audio.play_exploration_music(&track.path); + audio.play_exploration_music(&track.path); + } } fn get_current_day_period(game_time: u32) -> DayPeriod { @@ -89,9 +142,26 @@ impl MusicMgr { } fn load_soundtrack_items() -> SoundtrackCollection { - let file = assets::load_file("voxygen.audio.soundtrack", &["ron"]) - .expect("Failed to load the soundtrack config file"); + match assets::load_file("voxygen.audio.soundtrack", &["ron"]) { + Ok(file) => match ron::de::from_reader(file) { + Ok(config) => config, + Err(error) => { + log::warn!( + "Error parsing music config file, music will not be available: {}", + format!("{:#?}", error) + ); - ron::de::from_reader(file).expect("Error parsing soundtrack manifest") + SoundtrackCollection::default() + }, + }, + Err(error) => { + log::warn!( + "Error reading music config file, music will not be available: {}", + format!("{:#?}", error) + ); + + SoundtrackCollection::default() + }, + } } } diff --git a/voxygen/src/audio/sfx/mod.rs b/voxygen/src/audio/sfx/mod.rs index 60c2a0e163..38e50412db 100644 --- a/voxygen/src/audio/sfx/mod.rs +++ b/voxygen/src/audio/sfx/mod.rs @@ -4,6 +4,9 @@ //! Veloren's sfx are managed through a configuration which lives in the //! codebase under `/assets/voxygen/audio/sfx.ron`. //! +//! If there are errors while reading or deserialising the configuration file, a +//! warning is logged and sfx will be disabled. +//! //! Each entry in the configuration consists of an //! [SfxEvent](../../../veloren_common/event/enum.SfxEvent.html) item, with some //! additional information to allow playback: @@ -12,7 +15,9 @@ //! played each time, or a list of files from which one is chosen at random to //! be played. //! - `threshold` - the time that the system should wait between successive -//! plays. +//! plays. This avoids playing the sound with very fast successive repetition +//! when the character can maintain a state over a long period, such as +//! running or climbing. //! //! The following snippet details some entries in the configuration and how they //! map to the sound files: @@ -61,20 +66,16 @@ //! // An attack ability which depends on the weapon //! Attack(DashMelee, Sword): ( //! files: [ -//! "voxygen.audio.sfx.weapon.whoosh_normal_01", -//! "voxygen.audio.sfx.weapon.whoosh_normal_02", -//! "voxygen.audio.sfx.weapon.whoosh_normal_03", -//! "voxygen.audio.sfx.weapon.whoosh_normal_04", +//! "voxygen.audio.sfx.weapon.sword_dash_01", +//! "voxygen.audio.sfx.weapon.sword_dash_02", //! ], //! threshold: 1.2, //! ), //! // A multi-stage attack ability which depends on the weapon //! Attack(TripleStrike(First), Sword): ( //! files: [ -//! "voxygen.audio.sfx.weapon.whoosh_normal_01", -//! "voxygen.audio.sfx.weapon.whoosh_normal_02", -//! "voxygen.audio.sfx.weapon.whoosh_normal_03", -//! "voxygen.audio.sfx.weapon.whoosh_normal_04", +//! "voxygen.audio.sfx.weapon.sword_03", +//! "voxygen.audio.sfx.weapon.sword_04", //! ], //! threshold: 0.5, //! ), @@ -188,15 +189,22 @@ impl SfxMgr { } fn load_sfx_items() -> SfxTriggers { - let file = assets::load_file("voxygen.audio.sfx", &["ron"]) - .expect("Failed to load the sfx config file"); + match assets::load_file("voxygen.audio.sfx", &["ron"]) { + Ok(file) => match ron::de::from_reader(file) { + Ok(config) => config, + Err(error) => { + log::warn!( + "Error parsing sfx config file, sfx will not be available: {}", + format!("{:#?}", error) + ); - match ron::de::from_reader(file) { - Ok(config) => config, - Err(e) => { + SfxTriggers::default() + }, + }, + Err(error) => { log::warn!( - "Error parsing sfx config file, sfx will not be available: {}", - format!("{:#?}", e) + "Error reading sfx config file, sfx will not be available: {}", + format!("{:#?}", error) ); SfxTriggers::default()