2020-06-14 16:56:32 +00:00
|
|
|
//! 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),
|
2020-10-16 23:52:09 +00:00
|
|
|
//! biome: Some(Forest),
|
2020-06-14 16:56:32 +00:00
|
|
|
//! 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
|
2020-02-15 21:30:44 +00:00
|
|
|
use crate::audio::AudioFrontend;
|
2020-10-16 23:52:09 +00:00
|
|
|
use client::Client;
|
2020-11-02 07:19:28 +00:00
|
|
|
use common::{
|
|
|
|
assets,
|
|
|
|
state::State,
|
|
|
|
terrain::{BiomeKind, SitesKind},
|
|
|
|
};
|
2020-11-04 06:41:54 +00:00
|
|
|
use rand::{prelude::SliceRandom, thread_rng};
|
2020-02-03 11:55:32 +00:00
|
|
|
use serde::Deserialize;
|
|
|
|
use std::time::Instant;
|
2020-06-21 10:22:26 +00:00
|
|
|
use tracing::warn;
|
2020-02-03 11:55:32 +00:00
|
|
|
|
|
|
|
const DAY_START_SECONDS: u32 = 28800; // 8:00
|
|
|
|
const DAY_END_SECONDS: u32 = 70200; // 19:30
|
|
|
|
|
2020-06-14 16:56:32 +00:00
|
|
|
#[derive(Debug, Default, Deserialize)]
|
2020-02-03 11:55:32 +00:00
|
|
|
struct SoundtrackCollection {
|
|
|
|
tracks: Vec<SoundtrackItem>,
|
|
|
|
}
|
|
|
|
|
2020-06-14 16:56:32 +00:00
|
|
|
/// Configuration for a single music track in the soundtrack
|
2020-02-03 11:55:32 +00:00
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
pub struct SoundtrackItem {
|
|
|
|
title: String,
|
|
|
|
path: String,
|
2020-06-14 16:56:32 +00:00
|
|
|
/// Length of the track in seconds
|
2020-02-03 11:55:32 +00:00
|
|
|
length: f64,
|
2020-06-14 16:56:32 +00:00
|
|
|
/// Whether this track should play during day or night
|
2020-02-03 11:55:32 +00:00
|
|
|
timing: Option<DayPeriod>,
|
2020-11-04 23:22:56 +00:00
|
|
|
biomes: Vec<(BiomeKind, u8)>,
|
2020-11-02 07:19:28 +00:00
|
|
|
site: Option<SitesKind>,
|
2020-02-03 11:55:32 +00:00
|
|
|
}
|
|
|
|
|
2020-06-14 16:56:32 +00:00
|
|
|
/// Allows control over when a track should play based on in-game time of day
|
2020-02-03 11:55:32 +00:00
|
|
|
#[derive(Debug, Deserialize, PartialEq)]
|
|
|
|
enum DayPeriod {
|
2020-06-14 16:56:32 +00:00
|
|
|
/// 8:00 AM to 7:30 PM
|
|
|
|
Day,
|
|
|
|
/// 7:31 PM to 6:59 AM
|
|
|
|
Night,
|
2020-02-03 11:55:32 +00:00
|
|
|
}
|
|
|
|
|
2020-10-21 22:10:32 +00:00
|
|
|
/// Determines whether the sound is stopped, playing, or fading
|
|
|
|
#[derive(Debug, Deserialize, PartialEq)]
|
|
|
|
enum PlayState {
|
|
|
|
Playing,
|
|
|
|
Stopped,
|
|
|
|
FadingOut,
|
|
|
|
FadingIn,
|
|
|
|
}
|
|
|
|
|
2020-06-14 16:56:32 +00:00
|
|
|
/// Provides methods to control music playback
|
2020-02-03 11:55:32 +00:00
|
|
|
pub struct MusicMgr {
|
|
|
|
soundtrack: SoundtrackCollection,
|
|
|
|
began_playing: Instant,
|
2020-11-04 23:22:56 +00:00
|
|
|
//began_fading: Instant,
|
2020-02-03 11:55:32 +00:00
|
|
|
next_track_change: f64,
|
2020-06-14 16:56:32 +00:00
|
|
|
/// The title of the last track played. Used to prevent a track
|
|
|
|
/// being played twice in a row
|
2020-02-03 11:55:32 +00:00
|
|
|
last_track: String,
|
2020-11-04 23:22:56 +00:00
|
|
|
/*last_biome: BiomeKind,
|
|
|
|
*playing: PlayState, */
|
2020-02-03 11:55:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl MusicMgr {
|
2020-06-10 19:47:36 +00:00
|
|
|
#[allow(clippy::new_without_default)] // TODO: Pending review in #587
|
2020-02-03 11:55:32 +00:00
|
|
|
pub fn new() -> Self {
|
|
|
|
Self {
|
|
|
|
soundtrack: Self::load_soundtrack_items(),
|
|
|
|
began_playing: Instant::now(),
|
2020-11-04 23:22:56 +00:00
|
|
|
//began_fading: Instant::now(),
|
2020-02-03 11:55:32 +00:00
|
|
|
next_track_change: 0.0,
|
|
|
|
last_track: String::from("None"),
|
2020-11-04 23:22:56 +00:00
|
|
|
/*last_biome: BiomeKind::Void,
|
|
|
|
*playing: PlayState::Stopped, */
|
2020-02-03 11:55:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-14 16:56:32 +00:00
|
|
|
/// Checks whether the previous track has completed. If so, sends a
|
|
|
|
/// request to play the next (random) track
|
2020-10-16 23:52:09 +00:00
|
|
|
pub fn maintain(&mut self, audio: &mut AudioFrontend, state: &State, client: &Client) {
|
2020-10-20 00:28:25 +00:00
|
|
|
// Gets the current player biome
|
2020-11-04 06:41:54 +00:00
|
|
|
//let current_biome: BiomeKind = match client.current_chunk() {
|
|
|
|
// Some(chunk) => chunk.meta().biome(),
|
|
|
|
// _ => self.last_biome,
|
|
|
|
//};
|
2020-10-20 00:28:25 +00:00
|
|
|
|
2020-11-04 23:22:56 +00:00
|
|
|
//if let Some(current_chunk) = client.current_chunk() {
|
|
|
|
// println!("biome: {:?}", current_chunk.meta().biome());
|
|
|
|
// println!("chaos: {}", current_chunk.meta().chaos());
|
|
|
|
// println!("alt: {}", current_chunk.meta().alt());
|
|
|
|
// println!("temp: {}", current_chunk.meta().temp());
|
|
|
|
// println!("tree_density: {}",
|
|
|
|
// current_chunk.meta().tree_density());
|
|
|
|
// println!("humidity: {}", current_chunk.meta().humidity());
|
|
|
|
// println!("cave_alt: {}", current_chunk.meta().cave_alt());
|
|
|
|
// if let Some(position) = client.current_position() {
|
|
|
|
// println!("player_alt: {}", position[2]);
|
|
|
|
// }
|
|
|
|
//}
|
2020-11-02 07:19:28 +00:00
|
|
|
|
2020-02-03 11:55:32 +00:00
|
|
|
if audio.music_enabled()
|
2020-06-14 16:56:32 +00:00
|
|
|
&& !self.soundtrack.tracks.is_empty()
|
2020-11-02 07:19:28 +00:00
|
|
|
&& self.began_playing.elapsed().as_secs_f64() > self.next_track_change
|
|
|
|
// || self.playing == PlayState::Stopped)
|
|
|
|
// && self.playing != PlayState::FadingOut
|
2020-02-03 11:55:32 +00:00
|
|
|
{
|
2020-10-16 23:52:09 +00:00
|
|
|
self.play_random_track(audio, state, client);
|
2020-11-02 07:19:28 +00:00
|
|
|
// self.playing = PlayState::Playing;
|
|
|
|
//} else if current_biome != self.last_biome && self.playing == PlayState::Playing {
|
|
|
|
// audio.fade_out_exploration_music();
|
|
|
|
// self.began_fading = Instant::now();
|
|
|
|
// self.playing = PlayState::FadingOut;
|
|
|
|
//} else if self.began_fading.elapsed().as_secs_f64() > 5.0
|
|
|
|
// && self.playing == PlayState::FadingOut
|
|
|
|
//{
|
|
|
|
// audio.stop_exploration_music();
|
|
|
|
// self.playing = PlayState::Stopped;
|
2020-02-03 11:55:32 +00:00
|
|
|
}
|
2020-11-02 07:19:28 +00:00
|
|
|
//self.last_biome = current_biome;
|
2020-02-03 11:55:32 +00:00
|
|
|
}
|
|
|
|
|
2020-10-16 23:52:09 +00:00
|
|
|
fn play_random_track(&mut self, audio: &mut AudioFrontend, state: &State, client: &Client) {
|
2020-11-04 06:41:54 +00:00
|
|
|
//const SILENCE_BETWEEN_TRACKS_SECONDS: f64 = 45.0;
|
|
|
|
const SILENCE_BETWEEN_TRACKS_SECONDS: f64 = 5.0;
|
2020-02-03 11:55:32 +00:00
|
|
|
|
2020-02-29 03:59:11 +00:00
|
|
|
let game_time = (state.get_time_of_day() as u64 % 86400) as u32;
|
2020-02-15 21:30:44 +00:00
|
|
|
let current_period_of_day = Self::get_current_day_period(game_time);
|
2020-10-16 23:52:09 +00:00
|
|
|
let current_biome = Self::get_current_biome(client);
|
2020-11-02 07:19:28 +00:00
|
|
|
let current_site = Self::get_current_site(client);
|
2020-02-03 11:55:32 +00:00
|
|
|
let mut rng = thread_rng();
|
|
|
|
|
2020-06-14 16:56:32 +00:00
|
|
|
let maybe_track = self
|
2020-02-03 11:55:32 +00:00
|
|
|
.soundtrack
|
|
|
|
.tracks
|
|
|
|
.iter()
|
|
|
|
.filter(|track| {
|
|
|
|
!track.title.eq(&self.last_track)
|
|
|
|
&& match &track.timing {
|
|
|
|
Some(period_of_day) => period_of_day == ¤t_period_of_day,
|
|
|
|
None => true,
|
|
|
|
}
|
|
|
|
})
|
2020-11-02 07:19:28 +00:00
|
|
|
.filter(|track| match &track.site {
|
|
|
|
Some(site) => site == ¤t_site,
|
|
|
|
None => true,
|
|
|
|
})
|
2020-11-04 23:22:56 +00:00
|
|
|
.filter(|track| {
|
|
|
|
let mut result = false;
|
|
|
|
if track.biomes.len() > 0 {
|
|
|
|
for biome in track.biomes.iter() {
|
|
|
|
if biome.0 == current_biome {
|
|
|
|
result = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
result = true;
|
|
|
|
}
|
|
|
|
result
|
2020-11-04 06:41:54 +00:00
|
|
|
})
|
|
|
|
.collect::<Vec<&SoundtrackItem>>();
|
|
|
|
|
2020-11-04 23:22:56 +00:00
|
|
|
let new_maybe_track = maybe_track.choose_weighted(&mut rng, |track| {
|
|
|
|
let mut chance = 0;
|
|
|
|
if track.biomes.len() > 0 {
|
|
|
|
for biome in track.biomes.iter() {
|
|
|
|
if biome.0 == current_biome {
|
|
|
|
chance = biome.1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
chance = 1;
|
|
|
|
}
|
|
|
|
chance
|
2020-11-04 06:41:54 +00:00
|
|
|
});
|
2020-02-03 11:55:32 +00:00
|
|
|
|
2020-11-04 06:41:54 +00:00
|
|
|
if let Ok(track) = new_maybe_track {
|
2020-06-14 16:56:32 +00:00
|
|
|
self.last_track = String::from(&track.title);
|
|
|
|
self.began_playing = Instant::now();
|
|
|
|
self.next_track_change = track.length + SILENCE_BETWEEN_TRACKS_SECONDS;
|
2020-02-03 11:55:32 +00:00
|
|
|
|
2020-06-14 16:56:32 +00:00
|
|
|
audio.play_exploration_music(&track.path);
|
|
|
|
}
|
2020-02-03 11:55:32 +00:00
|
|
|
}
|
|
|
|
|
2020-02-15 21:30:44 +00:00
|
|
|
fn get_current_day_period(game_time: u32) -> DayPeriod {
|
2020-02-03 11:55:32 +00:00
|
|
|
if game_time > DAY_START_SECONDS && game_time < DAY_END_SECONDS {
|
|
|
|
DayPeriod::Day
|
|
|
|
} else {
|
|
|
|
DayPeriod::Night
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-16 23:52:09 +00:00
|
|
|
fn get_current_biome(client: &Client) -> BiomeKind {
|
|
|
|
match client.current_chunk() {
|
|
|
|
Some(chunk) => chunk.meta().biome(),
|
|
|
|
_ => BiomeKind::Void,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-02 07:19:28 +00:00
|
|
|
fn get_current_site(client: &Client) -> SitesKind {
|
|
|
|
let mut player_alt = 0.0;
|
|
|
|
if let Some(position) = client.current_position() {
|
|
|
|
player_alt = position[2];
|
|
|
|
}
|
|
|
|
let mut cave_alt = 0.0;
|
|
|
|
let mut alt = 0.0;
|
|
|
|
if let Some(chunk) = client.current_chunk() {
|
|
|
|
alt = chunk.meta().alt();
|
|
|
|
cave_alt = chunk.meta().cave_alt();
|
|
|
|
}
|
2020-11-04 23:22:56 +00:00
|
|
|
if player_alt < (alt - 20.0) && cave_alt != 0.0 {
|
2020-11-02 07:19:28 +00:00
|
|
|
SitesKind::Cave
|
2020-11-04 23:22:56 +00:00
|
|
|
} else if player_alt < (alt - 20.0) {
|
2020-11-02 07:19:28 +00:00
|
|
|
SitesKind::Dungeon
|
|
|
|
} else {
|
|
|
|
SitesKind::None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-03 11:55:32 +00:00
|
|
|
fn load_soundtrack_items() -> SoundtrackCollection {
|
2020-06-14 16:56:32 +00:00
|
|
|
match assets::load_file("voxygen.audio.soundtrack", &["ron"]) {
|
|
|
|
Ok(file) => match ron::de::from_reader(file) {
|
|
|
|
Ok(config) => config,
|
|
|
|
Err(error) => {
|
2020-06-21 10:22:26 +00:00
|
|
|
warn!(
|
2020-06-14 16:56:32 +00:00
|
|
|
"Error parsing music config file, music will not be available: {}",
|
|
|
|
format!("{:#?}", error)
|
|
|
|
);
|
2020-02-03 11:55:32 +00:00
|
|
|
|
2020-06-14 16:56:32 +00:00
|
|
|
SoundtrackCollection::default()
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Err(error) => {
|
2020-06-21 10:22:26 +00:00
|
|
|
warn!(
|
2020-06-14 16:56:32 +00:00
|
|
|
"Error reading music config file, music will not be available: {}",
|
|
|
|
format!("{:#?}", error)
|
|
|
|
);
|
|
|
|
|
|
|
|
SoundtrackCollection::default()
|
|
|
|
},
|
|
|
|
}
|
2020-02-03 11:55:32 +00:00
|
|
|
}
|
|
|
|
}
|