From b7ce91fead53aeedebc89e0b35939a37089f10d3 Mon Sep 17 00:00:00 2001 From: S Handley Date: Mon, 3 Feb 2020 11:55:32 +0000 Subject: [PATCH] Move music under audio Also add some blank time after each track so that we get some silence between tracks. --- CHANGELOG.md | 2 + assets/voxygen/audio/soundtrack.ron | 46 +++++++++ .../audio/soundtrack/field_grazing.ogg | 3 + .../audio/soundtrack/mineral_deposits.ogg | 3 + assets/voxygen/audio/soundtrack/moonbeams.ogg | 3 + .../audio/soundtrack/serene_meadows.ogg | 3 + .../audio/soundtrack/snowtop_volume.ogg | 3 + .../audio/soundtrack/wandering_voices.ogg | 3 + voxygen/src/audio/channel.rs | 44 +++------ voxygen/src/audio/fader.rs | 4 +- voxygen/src/audio/mod.rs | 49 +++++++-- voxygen/src/audio/music.rs | 99 +++++++++++++++++++ voxygen/src/audio/sfx/mod.rs | 4 + voxygen/src/menu/main/mod.rs | 9 +- voxygen/src/scene/mod.rs | 8 +- voxygen/src/session.rs | 3 + 16 files changed, 242 insertions(+), 44 deletions(-) create mode 100644 assets/voxygen/audio/soundtrack.ron create mode 100644 assets/voxygen/audio/soundtrack/field_grazing.ogg create mode 100644 assets/voxygen/audio/soundtrack/mineral_deposits.ogg create mode 100644 assets/voxygen/audio/soundtrack/moonbeams.ogg create mode 100644 assets/voxygen/audio/soundtrack/serene_meadows.ogg create mode 100644 assets/voxygen/audio/soundtrack/snowtop_volume.ogg create mode 100644 assets/voxygen/audio/soundtrack/wandering_voices.ogg create mode 100644 voxygen/src/audio/music.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 592334492b..7bc81b6ee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added music system + ### Changed ### Removed diff --git a/assets/voxygen/audio/soundtrack.ron b/assets/voxygen/audio/soundtrack.ron new file mode 100644 index 0000000000..0ac60d9bde --- /dev/null +++ b/assets/voxygen/audio/soundtrack.ron @@ -0,0 +1,46 @@ +( + tracks: [ + ( + title: "Field Grazing", + path: "voxygen.audio.soundtrack.field_grazing", + length: 154.0, + timing: Some(Day), + artist: "Aeronic", + ), + ( + title: "Wandering Voices", + path: "voxygen.audio.soundtrack.wandering_voices", + length: 137.0, + timing: Some(Night), + artist: "Aeronic", + ), + ( + title: "Snowtop Volume", + path: "voxygen.audio.soundtrack.snowtop_volume", + length: 89.0, + timing: Some(Day), + artist: "Aeronic", + ), + ( + title: "Mineral Deposits", + path: "voxygen.audio.soundtrack.mineral_deposits", + length: 148.0, + timing: Some(Day), + artist: "Aeronic", + ), + ( + title: "Moonbeams", + path: "voxygen.audio.soundtrack.moonbeams", + length: 158.0, + timing: Some(Night), + artist: "Aeronic", + ), + ( + title: "Serene Meadows", + path: "voxygen.audio.soundtrack.serene_meadows", + length: 173.0, + timing: Some(Night), + artist: "Aeronic", + ) + ] +) \ No newline at end of file diff --git a/assets/voxygen/audio/soundtrack/field_grazing.ogg b/assets/voxygen/audio/soundtrack/field_grazing.ogg new file mode 100644 index 0000000000..b3b1115318 --- /dev/null +++ b/assets/voxygen/audio/soundtrack/field_grazing.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31abe12210d7eaea3fbdd9e52102f63711b8227c31985dac9cf2da319e24fed0 +size 8645063 diff --git a/assets/voxygen/audio/soundtrack/mineral_deposits.ogg b/assets/voxygen/audio/soundtrack/mineral_deposits.ogg new file mode 100644 index 0000000000..42c1da0905 --- /dev/null +++ b/assets/voxygen/audio/soundtrack/mineral_deposits.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2261a1229682f1aecc9776eb8eee46c774bdff75a2a2a2e897bfa09c4217532 +size 8395603 diff --git a/assets/voxygen/audio/soundtrack/moonbeams.ogg b/assets/voxygen/audio/soundtrack/moonbeams.ogg new file mode 100644 index 0000000000..6674a865ac --- /dev/null +++ b/assets/voxygen/audio/soundtrack/moonbeams.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d33106ef431382e26151dc9072b7d2195c1ce6641ed7c156f53cb6a1129a4c44 +size 3131296 diff --git a/assets/voxygen/audio/soundtrack/serene_meadows.ogg b/assets/voxygen/audio/soundtrack/serene_meadows.ogg new file mode 100644 index 0000000000..4e58c3ba5e --- /dev/null +++ b/assets/voxygen/audio/soundtrack/serene_meadows.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddf39d22d6b6d4ebcd8102726c80624f9ba532ae277d13e51edba19cd4e5400e +size 3450218 diff --git a/assets/voxygen/audio/soundtrack/snowtop_volume.ogg b/assets/voxygen/audio/soundtrack/snowtop_volume.ogg new file mode 100644 index 0000000000..46e5fe08ac --- /dev/null +++ b/assets/voxygen/audio/soundtrack/snowtop_volume.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b581be43adaea478f2949811e2b0dc9852f0c311dd6f7423c201c6c025c144e4 +size 5039853 diff --git a/assets/voxygen/audio/soundtrack/wandering_voices.ogg b/assets/voxygen/audio/soundtrack/wandering_voices.ogg new file mode 100644 index 0000000000..1a053e0df0 --- /dev/null +++ b/assets/voxygen/audio/soundtrack/wandering_voices.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01460c772f4a03ad2762be220d18d6e73a563a1a1d2136f2e605aad952b4c3a7 +size 7795278 diff --git a/voxygen/src/audio/channel.rs b/voxygen/src/audio/channel.rs index 7624fe6cd3..00c36646c0 100644 --- a/voxygen/src/audio/channel.rs +++ b/voxygen/src/audio/channel.rs @@ -1,6 +1,5 @@ use crate::audio::fader::Fader; -use rodio::{Decoder, Device, Sample, Source, SpatialSink}; -use std::{fs::File, io::BufReader}; +use rodio::{Device, Sample, Source, SpatialSink}; use vek::*; #[derive(PartialEq, Clone, Copy)] @@ -20,12 +19,19 @@ enum ChannelState { Stopped, } +#[derive(PartialEq, Clone, Copy)] +pub enum ChannelTag { + TitleMusic, + Soundtrack, +} + pub struct Channel { id: usize, sink: SpatialSink, audio_type: AudioType, state: ChannelState, fader: Fader, + tag: Option, pub pos: Vec3, } @@ -35,41 +41,15 @@ impl Channel { pub fn new(device: &Device) -> Self { Self { id: 0, - sink: SpatialSink::new(device, [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]), + sink: SpatialSink::new(device, [0.0; 3], [1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]), audio_type: AudioType::None, state: ChannelState::Stopped, fader: Fader::fade_in(0.0), + tag: None, pos: Vec3::zero(), } } - pub fn music(id: usize, device: &Device, bufr: BufReader) -> Self { - let sink = SpatialSink::new(device, [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]); - let sound = Decoder::new(bufr).unwrap(); - - sink.append(sound); - - Self { - id, - sink, - audio_type: AudioType::Music, - state: ChannelState::Playing, - fader: Fader::fade_in(0.0), - pos: Vec3::zero(), - } - } - - pub fn sfx(id: usize, sink: SpatialSink, pos: Vec3) -> Self { - Self { - id, - sink, - audio_type: AudioType::Sfx, - state: ChannelState::Playing, - fader: Fader::fade_in(0.0), - pos, - } - } - pub fn play(&mut self, source: S) where S: Source + Send + 'static, @@ -83,6 +63,10 @@ impl Channel { pub fn is_done(&self) -> bool { self.sink.empty() || self.state == ChannelState::Stopped } + pub fn set_tag(&mut self, tag: Option) { self.tag = tag; } + + pub fn get_tag(&self) -> Option { self.tag } + pub fn stop(&mut self, fader: Fader) { self.state = ChannelState::Stopping; self.fader = fader; diff --git a/voxygen/src/audio/fader.rs b/voxygen/src/audio/fader.rs index a3bba56ed1..dd835a2045 100644 --- a/voxygen/src/audio/fader.rs +++ b/voxygen/src/audio/fader.rs @@ -30,11 +30,11 @@ impl Fader { } } - pub fn fade_out(time: f32) -> Self { + pub fn fade_out(time: f32, volume_from: f32) -> Self { Self { length: time, running_time: 0.0, - volume_from: 1.0, + volume_from, volume_to: 0.0, is_running: true, } diff --git a/voxygen/src/audio/mod.rs b/voxygen/src/audio/mod.rs index 77f19fe3ae..7e73d9a752 100644 --- a/voxygen/src/audio/mod.rs +++ b/voxygen/src/audio/mod.rs @@ -1,9 +1,10 @@ pub mod channel; pub mod fader; +pub mod music; pub mod sfx; pub mod soundcache; -use channel::{AudioType, Channel}; +use channel::{AudioType, Channel, ChannelTag}; use fader::Fader; use soundcache::SoundCache; @@ -84,7 +85,11 @@ impl AudioFrontend { } } - pub fn get_channel(&mut self, audio_type: AudioType) -> Option<&mut Channel> { + pub fn get_channel( + &mut self, + audio_type: AudioType, + channel_tag: Option, + ) -> Option<&mut Channel> { if let Some(channel) = self.channels.iter_mut().find(|c| c.is_done()) { let id = self.next_channel_id; self.next_channel_id += 1; @@ -95,6 +100,7 @@ impl AudioFrontend { }; channel.set_id(id); + channel.set_tag(channel_tag); channel.set_audio_type(audio_type); channel.set_volume(volume); @@ -114,7 +120,7 @@ impl AudioFrontend { let left_ear = self.listener_ear_left.into_array(); let right_ear = self.listener_ear_right.into_array(); - if let Some(channel) = self.get_channel(AudioType::Sfx) { + if let Some(channel) = self.get_channel(AudioType::Sfx, None) { channel.set_emitter_position(calc_pos); channel.set_left_ear_position(left_ear); channel.set_right_ear_position(right_ear); @@ -127,12 +133,13 @@ impl AudioFrontend { None } - pub fn play_music(&mut self, sound: &str) -> Option { + pub fn play_music(&mut self, sound: &str, channel_tag: Option) -> Option { if self.audio_device.is_some() { - if let Some(channel) = self.get_channel(AudioType::Music) { + if let Some(channel) = self.get_channel(AudioType::Music, 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.set_emitter_position([0.0; 3]); channel.play(sound); return Some(channel.get_id()); @@ -167,8 +174,30 @@ impl AudioFrontend { } } + pub fn play_title_music(&mut self) -> Option { + if self.music_enabled() { + self.play_music( + "voxygen.audio.soundtrack.veloren_title_tune", + Some(ChannelTag::TitleMusic), + ) + } else { + None + } + } + + pub fn stop_title_music(&mut self) { + let index = self.channels.iter().position(|c| { + !c.is_done() && c.get_tag().is_some() && c.get_tag().unwrap() == ChannelTag::TitleMusic + }); + + if let Some(index) = index { + self.channels[index].stop(Fader::fade_out(1.5, self.music_volume)); + } + } + pub fn stop_channel(&mut self, channel_id: usize, fader: Fader) { let index = self.channels.iter().position(|c| c.get_id() == channel_id); + if let Some(index) = index { self.channels[index].stop(fader); } @@ -178,6 +207,10 @@ impl AudioFrontend { pub fn get_music_volume(&self) -> f32 { self.music_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 set_sfx_volume(&mut self, sfx_volume: f32) { self.sfx_volume = sfx_volume; @@ -193,7 +226,11 @@ impl AudioFrontend { for channel in self.channels.iter_mut() { if channel.get_audio_type() == AudioType::Music { - channel.set_volume(music_volume); + if music_volume > 0.0 { + channel.set_volume(music_volume); + } else { + channel.stop(Fader::fade_out(0.0, 0.0)); + } } } } diff --git a/voxygen/src/audio/music.rs b/voxygen/src/audio/music.rs new file mode 100644 index 0000000000..8f6ecc0525 --- /dev/null +++ b/voxygen/src/audio/music.rs @@ -0,0 +1,99 @@ +use crate::audio::{channel::ChannelTag, AudioFrontend}; +use client::Client; +use common::assets; +use rand::{seq::IteratorRandom, thread_rng}; +use serde::Deserialize; +use std::time::Instant; + +const DAY_START_SECONDS: u32 = 28800; // 8:00 +const DAY_END_SECONDS: u32 = 70200; // 19:30 + +#[derive(Debug, Deserialize)] +struct SoundtrackCollection { + tracks: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct SoundtrackItem { + title: String, + path: String, + length: f64, + timing: Option, +} + +#[derive(Debug, Deserialize, PartialEq)] +enum DayPeriod { + Day, // 8:00 AM to 7:30 PM + Night, // 7:31 PM to 6:59 AM +} + +pub struct MusicMgr { + soundtrack: SoundtrackCollection, + began_playing: Instant, + next_track_change: f64, + current_music: Option, + last_track: String, +} + +impl MusicMgr { + pub fn new() -> Self { + Self { + soundtrack: Self::load_soundtrack_items(), + began_playing: Instant::now(), + next_track_change: 0.0, + current_music: None, + last_track: String::from("None"), + } + } + + pub fn maintain(&mut self, audio: &mut AudioFrontend, client: &Client) { + if audio.music_enabled() + && self.began_playing.elapsed().as_secs_f64() > self.next_track_change + { + self.current_music = self.play_random_track(audio, client); + } + } + + fn play_random_track(&mut self, audio: &mut AudioFrontend, client: &Client) -> Option { + const SILENCE_BETWEEN_TRACKS_SECONDS: f64 = 45.0; + + let game_time = (client.state().get_time_of_day() as u64 % 86400) as u32; + let current_period_of_day = self.get_current_day_period(game_time); + let mut rng = thread_rng(); + + let track = self + .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, + } + }) + .choose(&mut rng) + .expect("Failed to select a random 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_music(&track.path, Some(ChannelTag::Soundtrack)) + } + + fn get_current_day_period(&self, game_time: u32) -> DayPeriod { + if game_time > DAY_START_SECONDS && game_time < DAY_END_SECONDS { + DayPeriod::Day + } else { + DayPeriod::Night + } + } + + fn load_soundtrack_items() -> SoundtrackCollection { + let file = assets::load_file("voxygen.audio.soundtrack", &["ron"]) + .expect("Failed to load the soundtrack config file"); + + ron::de::from_reader(file).expect("Error parsing soundtrack manifest") + } +} diff --git a/voxygen/src/audio/sfx/mod.rs b/voxygen/src/audio/sfx/mod.rs index 919c97abbe..cbd6d50f5b 100644 --- a/voxygen/src/audio/sfx/mod.rs +++ b/voxygen/src/audio/sfx/mod.rs @@ -51,6 +51,10 @@ impl SfxMgr { } pub fn maintain(&mut self, audio: &mut AudioFrontend, client: &Client) { + if !audio.sfx_enabled() { + return; + } + self.event_mapper.maintain(client, &self.triggers); let ecs = client.state().ecs(); diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs index 1ee773f769..3c05d71eae 100644 --- a/voxygen/src/menu/main/mod.rs +++ b/voxygen/src/menu/main/mod.rs @@ -44,10 +44,11 @@ impl PlayState for MainMenuState { let mut client_init: Option = None; // Kick off title music - if self.title_music_channel.is_none() && global_state.settings.audio.audio_on { - self.title_music_channel = global_state - .audio - .play_music("voxygen.audio.soundtrack.veloren_title_tune"); + if self.title_music_channel.is_none() + && global_state.settings.audio.audio_on + && global_state.audio.music_enabled() + { + self.title_music_channel = global_state.audio.play_title_music(); } // Reset singleplayer server if it was running already diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index ad3ee41017..fd54ae6b62 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -5,11 +5,12 @@ pub mod terrain; use self::{ camera::{Camera, CameraMode}, figure::FigureMgr, + music::MusicMgr, terrain::Terrain, }; use crate::{ anim::character::SkeletonAttr, - audio::{sfx::SfxMgr, AudioFrontend}, + audio::{music, sfx::SfxMgr, AudioFrontend}, render::{ create_pp_mesh, create_skybox_mesh, Consts, Globals, Light, Model, PostProcessLocals, PostProcessPipeline, Renderer, Shadow, SkyboxLocals, SkyboxPipeline, @@ -58,6 +59,7 @@ pub struct Scene { figure_mgr: FigureMgr, sfx_mgr: SfxMgr, + music_mgr: MusicMgr, } impl Scene { @@ -91,6 +93,7 @@ impl Scene { figure_mgr: FigureMgr::new(), sfx_mgr: SfxMgr::new(), + music_mgr: MusicMgr::new(), } } @@ -312,8 +315,9 @@ impl Scene { // Remove unused figures. self.figure_mgr.clean(client.get_tick()); - // Maintain sfx + // Maintain audio self.sfx_mgr.maintain(audio, client); + self.music_mgr.maintain(audio, client); } /// Render the scene using the provided `Renderer`. diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index a3755e4080..b5a063d247 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -126,6 +126,9 @@ impl PlayState for SessionState { let mut clock = Clock::start(); self.client.borrow_mut().clear_terrain(); + // Kill the title music if it is still playing + global_state.audio.stop_title_music(); + // Send startup commands to the server if global_state.settings.send_logon_commands { for cmd in &global_state.settings.logon_commands {