Merge branch 'shandley/docs-audio' into 'master'

Add documentation for audio module.

See merge request veloren/veloren!1060
This commit is contained in:
Imbris 2020-06-14 16:56:32 +00:00
commit 4901dde20d
5 changed files with 157 additions and 34 deletions

View File

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

View File

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

View File

@ -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<String>,
@ -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<f32>, vol: Option<f32>) {
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(

View File

@ -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<SoundtrackItem>,
}
/// 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<DayPeriod>,
}
/// 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,15 +122,16 @@ impl MusicMgr {
None => true,
}
})
.choose(&mut rng)
.expect("Failed to select a random track");
.choose(&mut rng);
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);
}
}
fn get_current_day_period(game_time: u32) -> DayPeriod {
if game_time > DAY_START_SECONDS && game_time < DAY_END_SECONDS {
@ -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()
},
}
}
}

View File

@ -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 ron::de::from_reader(file) {
match assets::load_file("voxygen.audio.sfx", &["ron"]) {
Ok(file) => match ron::de::from_reader(file) {
Ok(config) => config,
Err(e) => {
Err(error) => {
log::warn!(
"Error parsing sfx config file, sfx will not be available: {}",
format!("{:#?}", e)
format!("{:#?}", error)
);
SfxTriggers::default()
},
},
Err(error) => {
log::warn!(
"Error reading sfx config file, sfx will not be available: {}",
format!("{:#?}", error)
);
SfxTriggers::default()