mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'shandley/docs-audio' into 'master'
Add documentation for audio module. See merge request veloren/veloren!1060
This commit is contained in:
commit
4901dde20d
@ -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>,
|
||||
|
@ -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;
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user