Initial biome specific music and ambient sound channel

This commit is contained in:
jiminycrick 2020-10-16 16:52:09 -07:00
parent 26957d4fd4
commit 46d3f6f6d2
9 changed files with 424 additions and 25 deletions

View File

@ -0,0 +1,22 @@
// TODO: Add an ambient-soundtrack that runs independently from the musical soundtrack
(
tracks: [
(
title: "Forest Day", // Ambience Track
path: "voxygen.audio.ambient.forest_day",
length: 629.0,
timing: Some(Night),
biome: Some(Forest),
artist: "https://www.youtube.com/watch?v=FwVTkB-BIvM",
),
(
title: "Forest Morning", // Ambience Track
path: "voxygen.audio.ambient.forest_morning",
length: 600.0,
timing: Some(Day),
biome: Some(Forest),
artist: "https://www.youtube.com/watch?v=eq4nfIdK6C4",
),
]
)

View File

@ -3,18 +3,12 @@
(
tracks: [
(
title: "Forest Day", // Ambience Track
path: "voxygen.audio.ambient.forest_day",
length: 629.0,
timing: Some(Day),
artist: "https://www.youtube.com/watch?v=FwVTkB-BIvM",
),
(
title: "A Solemn Quest",
path: "voxygen.audio.soundtrack.a_solemn_quest",
length: 206.0,
timing: Some(Night),
biome: Some(Grassland),
artist: "Eden",
),
(
@ -22,6 +16,7 @@
path: "voxygen.audio.soundtrack.into_the_dark_forest",
length: 184.0,
timing: Some(Night),
biome: Some(Snowlands),
artist: "Aeronic",
),
(
@ -29,6 +24,7 @@
path: "voxygen.audio.soundtrack.field_grazing",
length: 154.0,
timing: Some(Day),
biome: Some(Grassland),
artist: "Aeronic",
),
(
@ -36,20 +32,23 @@
path: "voxygen.audio.soundtrack.wandering_voices",
length: 137.0,
timing: Some(Night),
biome: Some(Snowlands),
artist: "Aeronic",
),
/*(
(
title: "Snowtop Volume", //Snow Region
path: "voxygen.audio.soundtrack.snowtop_volume",
length: 89.0,
timing: Some(Day),
biome: Some(Snowlands),
artist: "Aeronic",
),*/
),
(
title: "Mineral Deposits",
path: "voxygen.audio.soundtrack.mineral_deposits",
length: 148.0,
timing: Some(Day),
biome: Some(Snowlands),
artist: "Aeronic",
),
(
@ -57,6 +56,7 @@
path: "voxygen.audio.soundtrack.moonbeams",
length: 158.0,
timing: Some(Night),
biome: Some(Snowlands),
artist: "Aeronic",
),
(
@ -64,6 +64,7 @@
path: "voxygen.audio.soundtrack.serene_meadows",
length: 173.0,
timing: Some(Night),
biome: Some(Snowlands),
artist: "Aeronic",
),
/*(
@ -71,6 +72,7 @@
path: "voxygen.audio.soundtrack.rest_assured",
length: 185.0,
timing: Some(Day),
biome: Some(Snowlands),
artist: "badbbad",
),*/
(
@ -78,6 +80,7 @@
path: "voxygen.audio.soundtrack.just_the_beginning",
length: 188.0,
timing: Some(Day),
biome: Some(Snowlands),
artist: "badbbad",
),
(
@ -85,6 +88,7 @@
path: "voxygen.audio.soundtrack.campfire_stories",
length: 100.0,
timing: Some(Night),
biome: Some(Snowlands),
artist: "badbbad",
),
(
@ -92,6 +96,7 @@
path: "voxygen.audio.soundtrack.limits",
length: 203.0,
timing: Some(Night),
biome: Some(Snowlands),
artist: "badbbad",
),
/*( // Dungeon
@ -99,6 +104,7 @@
path: "voxygen.audio.soundtrack.down_the_rabbit_hole",
length: 244.0,
timing: Some(Night),
biome: Some(Snowlands),
artist: "badbbad",
),*/
(
@ -106,14 +112,8 @@
path: "voxygen.audio.soundtrack.between_the_fairies",
length: 175.0,
timing: Some(Night),
biome: Some(Snowlands),
artist: "badbbad",
),
(
title: "Forest Morning", // Ambience Track
path: "voxygen.audio.ambient.forest_morning",
length: 600.0,
timing: Some(Day),
artist: "https://www.youtube.com/watch?v=eq4nfIdK6C4",
),
]
)
)

View File

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
pub enum BiomeKind {
Void,
Grassland,

View File

@ -0,0 +1,183 @@
//! Handles ambient sound playback and transitions
//!
//! Game ambient sound is controlled though a configuration file found in the
//! source at `/assets/voxygen/audio/ambient.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 ambient sound
//!
//! 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),
//! biome: Some(Forest),
//! 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 client::Client;
use common::{assets, state::State, terrain::BiomeKind};
use rand::{seq::IteratorRandom, thread_rng};
use serde::Deserialize;
use std::time::Instant;
use tracing::warn;
const DAY_START_SECONDS: u32 = 28800; // 8:00
const DAY_END_SECONDS: u32 = 70200; // 19:30
#[derive(Debug, Default, Deserialize)]
struct AmbientSoundtrackCollection {
tracks: Vec<AmbientSoundtrackItem>,
}
/// Configuration for a single music track in the soundtrack
#[derive(Debug, Deserialize)]
pub struct AmbientSoundtrackItem {
title: String,
path: String,
/// Length of the track in seconds
length: f64,
/// Whether this track should play during day or night
timing: Option<DayPeriod>,
biome: Option<BiomeKind>,
}
/// Allows control over when a track should play based on in-game time of day
#[derive(Debug, Deserialize, PartialEq)]
enum DayPeriod {
/// 8:00 AM to 7:30 PM
Day,
/// 7:31 PM to 6:59 AM
Night,
}
/// Provides methods to control music playback
pub struct AmbientMgr {
ambient_soundtrack: AmbientSoundtrackCollection,
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,
}
impl AmbientMgr {
#[allow(clippy::new_without_default)] // TODO: Pending review in #587
pub fn new() -> Self {
Self {
ambient_soundtrack: Self::load_ambient_soundtrack_items(),
began_playing: Instant::now(),
next_track_change: 0.0,
last_track: String::from("None"),
}
}
/// 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, client: &Client) {
if audio.music_enabled()
&& !self.ambient_soundtrack.tracks.is_empty()
&& self.began_playing.elapsed().as_secs_f64() > self.next_track_change
{
self.play_random_track(audio, state, client);
}
}
fn play_random_track(&mut self, audio: &mut AudioFrontend, state: &State, client: &Client) {
const SILENCE_BETWEEN_TRACKS_SECONDS: f64 = 45.0;
let game_time = (state.get_time_of_day() as u64 % 86400) as u32;
let current_period_of_day = Self::get_current_day_period(game_time);
let current_biome = Self::get_current_biome(client);
println!("Ambient Current biome: {:?}", current_biome);
let mut rng = thread_rng();
let maybe_track = self
.ambient_soundtrack
.tracks
.iter()
.filter(|track| {
!track.title.eq(&self.last_track)
&& match &track.timing {
Some(period_of_day) => period_of_day == &current_period_of_day,
None => true,
}
})
.filter(|track| match &track.biome {
Some(biome) => biome == &current_biome,
None => true,
})
.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_ambient(&track.path);
}
}
fn get_current_day_period(game_time: u32) -> DayPeriod {
if game_time > DAY_START_SECONDS && game_time < DAY_END_SECONDS {
DayPeriod::Day
} else {
DayPeriod::Night
}
}
fn get_current_biome(client: &Client) -> BiomeKind {
match client.current_chunk() {
Some(chunk) => chunk.meta().biome(),
_ => BiomeKind::Void,
}
}
fn load_ambient_soundtrack_items() -> AmbientSoundtrackCollection {
match assets::load_file("voxygen.audio.ambient", &["ron"]) {
Ok(file) => match ron::de::from_reader(file) {
Ok(config) => config,
Err(error) => {
warn!(
"Error parsing music config file, music will not be available: {}",
format!("{:#?}", error)
);
AmbientSoundtrackCollection::default()
},
},
Err(error) => {
warn!(
"Error reading music config file, music will not be available: {}",
format!("{:#?}", error)
);
AmbientSoundtrackCollection::default()
},
}
}
}

View File

@ -134,6 +134,111 @@ impl MusicChannel {
}
}
/// Each `AmbientChannel` has a `AmbientChannelTag` 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 AmbientChannelTag {
TitleMusic,
Exploration,
}
/// An AmbientChannel uses a non-positional audio sink designed to play ambient
/// sound which 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 AmbientChannel {
tag: AmbientChannelTag,
sink: Sink,
state: ChannelState,
fader: Fader,
}
impl AmbientChannel {
pub fn new(device: &Device) -> Self {
Self {
sink: Sink::new(device),
tag: AmbientChannelTag::TitleMusic,
state: ChannelState::Stopped,
fader: Fader::default(),
}
}
// Play an ambient track item on this channel. If the channel has an existing
// track playing, the new sounds will be appended and played once they
// complete. Otherwise it will begin playing immediately.
pub fn play<S>(&mut self, source: S, tag: AmbientChannelTag)
where
S: Source + Send + 'static,
S::Item: Sample,
S::Item: Send,
<S as std::iter::Iterator>::Item: std::fmt::Debug,
{
self.tag = tag;
if !self.sink.len() > 0 {
self.sink.append(source);
}
self.state = if !self.fader.is_finished() {
ChannelState::Fading
} else {
ChannelState::Playing
};
}
/// Set the volume of the current channel. If the channel is currently
/// fading, the volume of the fader is updated to this value.
pub fn set_volume(&mut self, volume: f32) {
if !self.fader.is_finished() {
self.fader.update_target_volume(volume);
} else {
self.sink.set_volume(volume);
}
}
/// Set a fader for the channel. If a fader exists already, it is replaced.
/// If the channel has not begun playing, and the fader is set to fade in,
/// we set the volume of the channel to the initial volume of the fader so
/// that the volumes match when playing begins.
pub fn set_fader(&mut self, fader: Fader) {
self.fader = fader;
self.state = ChannelState::Fading;
if self.state == ChannelState::Stopped && fader.direction() == FadeDirection::In {
self.sink.set_volume(fader.get_volume());
}
}
/// Returns true if either the channels sink reports itself as empty (no
/// more sounds in the queue) or we have forcibly set the channels state to
/// the 'Stopped' state
pub fn is_done(&self) -> bool { self.sink.empty() || self.state == ChannelState::Stopped }
pub fn get_tag(&self) -> AmbientChannelTag { self.tag }
/// Maintain the fader attached to this channel. If the channel is not
/// fading, no action is taken.
pub fn maintain(&mut self, dt: f32) {
if self.state == ChannelState::Fading {
self.fader.update(dt);
self.sink.set_volume(self.fader.get_volume());
if self.fader.is_finished() {
match self.fader.direction() {
FadeDirection::Out => {
self.state = ChannelState::Stopped;
self.sink.stop();
},
FadeDirection::In => {
self.state = ChannelState::Playing;
},
}
}
}
}
}
/// 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

View File

@ -1,12 +1,13 @@
//! Handles audio device detection and playback of sound effects and music
pub mod ambient;
pub mod channel;
pub mod fader;
pub mod music;
pub mod sfx;
pub mod soundcache;
use channel::{MusicChannel, MusicChannelTag, SfxChannel};
use channel::{AmbientChannel, AmbientChannelTag, MusicChannel, MusicChannelTag, SfxChannel};
use fader::Fader;
use soundcache::SoundCache;
use std::time::Duration;
@ -38,9 +39,11 @@ pub struct AudioFrontend {
music_channels: Vec<MusicChannel>,
sfx_channels: Vec<SfxChannel>,
ambient_channels: Vec<AmbientChannel>,
sfx_volume: f32,
music_volume: f32,
ambient_volume: f32,
listener: Listener,
}
@ -61,9 +64,11 @@ impl AudioFrontend {
audio_device,
sound_cache: SoundCache::default(),
music_channels: Vec::new(),
ambient_channels: Vec::new(),
sfx_channels,
sfx_volume: 1.0,
music_volume: 1.0,
ambient_volume: 1.0,
listener: Listener::default(),
}
@ -77,9 +82,11 @@ impl AudioFrontend {
audio_device: None,
sound_cache: SoundCache::default(),
music_channels: Vec::new(),
ambient_channels: Vec::new(),
sfx_channels: Vec::new(),
sfx_volume: 1.0,
music_volume: 1.0,
ambient_volume: 1.0,
listener: Listener::default(),
}
}
@ -87,10 +94,15 @@ impl AudioFrontend {
/// Drop any unused music channels, and update their faders
pub fn maintain(&mut self, dt: Duration) {
self.music_channels.retain(|c| !c.is_done());
self.ambient_channels.retain(|c| !c.is_done());
for channel in self.music_channels.iter_mut() {
channel.maintain(dt);
}
for channel in self.ambient_channels.iter_mut() {
channel.maintain(dt);
}
}
fn get_sfx_channel(&mut self) -> Option<&mut SfxChannel> {
@ -141,6 +153,35 @@ impl AudioFrontend {
self.music_channels.last_mut()
}
fn get_ambient_channel(
&mut self,
next_channel_tag: AmbientChannelTag,
) -> Option<&mut AmbientChannel> {
if let Some(audio_device) = &self.audio_device {
if self.ambient_channels.is_empty() {
let mut next_ambient_channel = AmbientChannel::new(&audio_device);
next_ambient_channel.set_volume(self.ambient_volume);
self.ambient_channels.push(next_ambient_channel);
} else {
let existing_channel = self.ambient_channels.last_mut()?;
if existing_channel.get_tag() != next_channel_tag {
// Fade the existing channel out. It will be removed when the fade completes.
existing_channel.set_fader(Fader::fade_out(2.0, self.ambient_volume));
let mut next_ambient_channel = AmbientChannel::new(&audio_device);
next_ambient_channel.set_fader(Fader::fade_in(12.0, self.ambient_volume));
self.ambient_channels.push(next_ambient_channel);
}
}
}
self.ambient_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() {
@ -167,6 +208,15 @@ impl AudioFrontend {
}
}
fn play_ambient(&mut self, sound: &str, channel_tag: AmbientChannelTag) {
if let Some(channel) = self.get_ambient_channel(channel_tag) {
let file = assets::load_file(&sound, &["ogg"]).expect("Failed to load sound");
let sound = Decoder::new(file).expect("Failed to decode sound");
channel.play(sound, channel_tag);
}
}
pub fn set_listener_pos(&mut self, pos: Vec3<f32>, ori: Vec3<f32>) {
self.listener.pos = pos;
self.listener.ori = ori.normalized();
@ -199,14 +249,24 @@ impl AudioFrontend {
}
}
pub fn play_exploration_ambient(&mut self, item: &str) {
if self.ambient_enabled() {
self.play_ambient(item, AmbientChannelTag::Exploration)
}
}
pub fn get_sfx_volume(&self) -> f32 { self.sfx_volume }
pub fn get_music_volume(&self) -> f32 { self.music_volume }
pub fn get_ambient_volume(&self) -> f32 { self.ambient_volume }
pub fn sfx_enabled(&self) -> bool { self.sfx_volume > 0.0 }
pub fn music_enabled(&self) -> bool { self.music_volume > 0.0 }
pub fn ambient_enabled(&self) -> bool { self.ambient_volume > 0.0 }
pub fn set_sfx_volume(&mut self, sfx_volume: f32) {
self.sfx_volume = sfx_volume;
@ -223,6 +283,14 @@ impl AudioFrontend {
}
}
pub fn set_ambient_volume(&mut self, ambient_volume: f32) {
self.ambient_volume = ambient_volume;
for channel in self.ambient_channels.iter_mut() {
channel.set_volume(ambient_volume);
}
}
// TODO: figure out how badly this will break things when it is called
pub fn set_device(&mut self, name: String) {
self.device = name.clone();

View File

@ -26,6 +26,7 @@
//! path: "voxygen.audio.soundtrack.sleepy",
//! length: 400.0,
//! timing: Some(Night),
//! biome: Some(Forest),
//! artist: "Elvis",
//! ),
//! ```
@ -38,7 +39,8 @@
//! - 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 client::Client;
use common::{assets, state::State, terrain::BiomeKind};
use rand::{seq::IteratorRandom, thread_rng};
use serde::Deserialize;
use std::time::Instant;
@ -61,6 +63,7 @@ pub struct SoundtrackItem {
length: f64,
/// Whether this track should play during day or night
timing: Option<DayPeriod>,
biome: Option<BiomeKind>,
}
/// Allows control over when a track should play based on in-game time of day
@ -95,20 +98,22 @@ 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) {
pub fn maintain(&mut self, audio: &mut AudioFrontend, state: &State, client: &Client) {
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);
self.play_random_track(audio, state, client);
}
}
fn play_random_track(&mut self, audio: &mut AudioFrontend, state: &State) {
fn play_random_track(&mut self, audio: &mut AudioFrontend, state: &State, client: &Client) {
const SILENCE_BETWEEN_TRACKS_SECONDS: f64 = 45.0;
let game_time = (state.get_time_of_day() as u64 % 86400) as u32;
let current_period_of_day = Self::get_current_day_period(game_time);
let current_biome = Self::get_current_biome(client);
println!("======> Music Current biome: {:?}", current_biome);
let mut rng = thread_rng();
let maybe_track = self
@ -122,6 +127,10 @@ impl MusicMgr {
None => true,
}
})
.filter(|track| match &track.biome {
Some(biome) => biome == &current_biome,
None => true,
})
.choose(&mut rng);
if let Some(track) = maybe_track {
@ -141,6 +150,13 @@ impl MusicMgr {
}
}
fn get_current_biome(client: &Client) -> BiomeKind {
match client.current_chunk() {
Some(chunk) => chunk.meta().biome(),
_ => BiomeKind::Void,
}
}
fn load_soundtrack_items() -> SoundtrackCollection {
match assets::load_file("voxygen.audio.soundtrack", &["ron"]) {
Ok(file) => match ron::de::from_reader(file) {

View File

@ -14,7 +14,7 @@ pub use self::{
terrain::Terrain,
};
use crate::{
audio::{music::MusicMgr, sfx::SfxMgr, AudioFrontend},
audio::{ambient::AmbientMgr, music::MusicMgr, sfx::SfxMgr, AudioFrontend},
render::{
create_clouds_mesh, create_pp_mesh, create_skybox_mesh, CloudsLocals, CloudsPipeline,
Consts, GlobalModel, Globals, Light, LodData, Model, PostProcessLocals,
@ -103,6 +103,7 @@ pub struct Scene {
figure_mgr: FigureMgr,
sfx_mgr: SfxMgr,
music_mgr: MusicMgr,
ambient_mgr: AmbientMgr,
}
pub struct SceneData<'a> {
@ -308,6 +309,7 @@ impl Scene {
figure_mgr: FigureMgr::new(renderer),
sfx_mgr: SfxMgr::new(),
music_mgr: MusicMgr::new(),
ambient_mgr: AmbientMgr::new(),
}
}
@ -441,6 +443,7 @@ impl Scene {
renderer: &mut Renderer,
audio: &mut AudioFrontend,
scene_data: &SceneData,
client: &Client,
) {
span!(_guard, "maintain", "Scene::maintain");
// Get player position.
@ -994,7 +997,8 @@ impl Scene {
scene_data.player_entity,
&self.camera,
);
self.music_mgr.maintain(audio, scene_data.state);
self.music_mgr.maintain(audio, scene_data.state, client);
self.ambient_mgr.maintain(audio, scene_data.state, client);
}
/// Render the scene using the provided `Renderer`.

View File

@ -1077,6 +1077,7 @@ impl PlayState for SessionState {
global_state.window.renderer_mut(),
&mut global_state.audio,
&scene_data,
&client,
);
// Process outcomes from client