diff --git a/CHANGELOG.md b/CHANGELOG.md index bf4f1c44bc..d449224398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - "Poise" renamed to "Stun resilience" - Stun resilience stat display - Villagers and guards now spawn with potions, and know how to use them. +- Combat music in dungeons when within range of enemies. ### Changed diff --git a/assets/voxygen/audio/soundtrack.ron b/assets/voxygen/audio/soundtrack.ron index 950b735643..135618fe4a 100644 --- a/assets/voxygen/audio/soundtrack.ron +++ b/assets/voxygen/audio/soundtrack.ron @@ -6,17 +6,17 @@ ( tracks: [ - ( + Individual(( title: "Dank Dungeon", path: "voxygen.audio.soundtrack.dungeon.dank_dungeon", length: 130.0, timing: None, biomes: [], site: Some(Dungeon), - activity: Explore, + activity: State(Explore), artist: "Aeronic", - ), - ( + )), + Individual(( title: "Calming Hills", path: "voxygen.audio.soundtrack.overworld.calming_hills", length: 101.0, @@ -25,10 +25,10 @@ (Mountain, 1) ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "Ultimafounding; mixed by Robotnik", - ), - ( + )), + Individual(( title: "Fiesta Del Pueblo", path: "voxygen.audio.soundtrack.overworld.fiesta_del_pueblo", length: 183.0, @@ -37,50 +37,50 @@ (Desert, 1) ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "Aeronic; mixed by Robotnik", - ), - ( + )), + Individual(( title: "Ruination", path: "voxygen.audio.soundtrack.dungeon.ruination", length: 135.0, timing: None, biomes: [], site: Some(Dungeon), - activity: Explore, + activity: State(Explore), artist: "Aeronic", - ), - ( + )), + Individual(( title: "Dank Hallows", path: "voxygen.audio.soundtrack.cave.dank_hallows", length: 227.0, timing: None, biomes: [], site: Some(Cave), - activity: Explore, + activity: State(Explore), artist: "Flashbang", - ), - ( + )), + Individual(( title: "Vast Onslaught", path: "voxygen.audio.soundtrack.dungeon.vast_onslaught", length: 237.0, timing: None, biomes: [], site: Some(Dungeon), - activity: Explore, + activity: State(Explore), artist: "Aeronic", - ), - ( + )), + Individual(( title: "Sacred Temple", path: "voxygen.audio.soundtrack.dungeon.sacred_temple", length: 75.0, timing: None, biomes: [], site: Some(Dungeon), - activity: Explore, + activity: State(Explore), artist: "Aeronic", - ), - ( + )), + Individual(( title: "True Nature", path: "voxygen.audio.soundtrack.overworld.true_nature", length: 169.0, @@ -89,10 +89,10 @@ (Forest, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "DaforLynx", - ), - ( + )), + Individual(( title: "Jungle Ambient", path: "voxygen.audio.soundtrack.overworld.jungle_ambient", length: 218.0, @@ -101,10 +101,10 @@ (Jungle, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "badbbad", - ), - ( + )), + Individual(( title: "Ethereal Bonds", path: "voxygen.audio.soundtrack.overworld.ethereal_bonds", length: 59.0, @@ -113,10 +113,10 @@ (Mountain, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "Aeronic", - ), - ( + )), + Individual(( title: "Leap of Faith", path: "voxygen.audio.soundtrack.overworld.leap_of_faith", length: 269.0, @@ -126,10 +126,10 @@ (Lake, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "Aeronic", - ), - ( + )), + Individual(( title: "Highland of the Hawk", path: "voxygen.audio.soundtrack.overworld.highland_of_the_hawk", length: 283.0, @@ -139,10 +139,10 @@ (Mountain, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "badbbad", - ), - ( + )), + Individual(( title: "Verdant Glades", path: "voxygen.audio.soundtrack.overworld.verdant_glades", length: 97.0, @@ -151,10 +151,10 @@ (Grassland, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "Aeronic", - ), - ( + )), + Individual(( title: "Calling Wild", path: "voxygen.audio.soundtrack.overworld.calling_wild", length: 160.0, @@ -163,10 +163,10 @@ (Grassland, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "Ultimafounding", - ), - ( + )), + Individual(( title: "Drifting Along", path: "voxygen.audio.soundtrack.overworld.drifting_along", length: 164.0, @@ -176,35 +176,35 @@ (Ocean, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "DaforLynx", - ), - ( - title: "Winter Falls", - path: "voxygen.audio.soundtrack.overworld.winter_falls", - length: 215.0, - timing: Some(Day), - biomes: [ - (Snowland, 1), - ], - site: Some(Void), - activity: Explore, - artist: "DaforLynx", - ), - ( - title: "Short Meandering", - path: "voxygen.audio.soundtrack.overworld.short_meandering", - length: 147.0, - timing: Some(Night), - biomes: [ + )), + Individual(( + title: "Winter Falls", + path: "voxygen.audio.soundtrack.overworld.winter_falls", + length: 215.0, + timing: Some(Day), + biomes: [ + (Snowland, 1), + ], + site: Some(Void), + activity: State(Explore), + artist: "DaforLynx", + )), + Individual(( + title: "Short Meandering", + path: "voxygen.audio.soundtrack.overworld.short_meandering", + length: 147.0, + timing: Some(Night), + biomes: [ (Desert, 1), (Mountain, 1), ], - site: Some(Void), - activity: Explore, - artist: "Ap1evideogame", - ), - ( + site: Some(Void), + activity: State(Explore), + artist: "Ap1evideogame", + )), + Individual(( title: "Oceania", path: "voxygen.audio.soundtrack.overworld.oceania", length: 135.0, @@ -214,10 +214,10 @@ (Ocean, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "Eden", - ), - ( + )), + Individual(( title: "A Solemn Quest", path: "voxygen.audio.soundtrack.overworld.a_solemn_quest", length: 206.0, @@ -227,10 +227,10 @@ (Mountain, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "Eden", - ), - ( + )), + Individual(( title: "Into The Dark Forest", path: "voxygen.audio.soundtrack.overworld.into_the_dark_forest", length: 184.0, @@ -240,23 +240,23 @@ (Jungle, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "Aeronic", - ), - ( + )), + Individual(( title: "Field Grazing", path: "voxygen.audio.soundtrack.overworld.field_grazing", length: 154.0, timing: Some(Day), biomes: [ (Grassland, 1), - (Forest, 1), + (Forest, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "Aeronic", - ), - ( + )), + Individual(( title: "Wandering Voices", path: "voxygen.audio.soundtrack.overworld.wandering_voices", length: 137.0, @@ -265,10 +265,10 @@ (Grassland, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "Aeronic", - ), - ( + )), + Individual(( title: "Snowtop Volume", path: "voxygen.audio.soundtrack.overworld.snowtop_volume", length: 89.0, @@ -277,20 +277,20 @@ (Snowland, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "Aeronic", - ), - ( + )), + Individual(( title: "Mineral Deposits", path: "voxygen.audio.soundtrack.cave.mineral_deposits", length: 148.0, timing: None, biomes: [], site: Some(Cave), - activity: Explore, + activity: State(Explore), artist: "Aeronic", - ), - ( + )), + Individual(( title: "Moonbeams", path: "voxygen.audio.soundtrack.overworld.moonbeams", length: 158.0, @@ -299,23 +299,23 @@ (Snowland, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "Aeronic", - ), - ( + )), + Individual(( title: "Serene Meadows", path: "voxygen.audio.soundtrack.overworld.serene_meadows", length: 173.0, timing: Some(Night), biomes: [ (Grassland, 1), - (Desert, 1), + (Desert, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "Aeronic", - ), - ( + )), + Individual(( title: "Just The Beginning", path: "voxygen.audio.soundtrack.overworld.just_the_beginning", length: 188.0, @@ -324,10 +324,10 @@ (Grassland, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "badbbad", - ), - ( + )), + Individual(( title: "Campfire Stories", path: "voxygen.audio.soundtrack.overworld.campfire_stories", length: 100.0, @@ -336,10 +336,10 @@ (Forest, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "badbbad", - ), - ( + )), + Individual(( title: "Limits", path: "voxygen.audio.soundtrack.overworld.limits", length: 203.0, @@ -348,20 +348,20 @@ (Mountain, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "badbbad", - ), - ( + )), + Individual(( title: "Down The Rabbit Hole", path: "voxygen.audio.soundtrack.dungeon.down_the_rabbit_hole", length: 244.0, timing: None, biomes: [], site: Some(Dungeon), - activity: Explore, + activity: State(Explore), artist: "badbbad", - ), - ( + )), + Individual(( title: "Between The Fairies", path: "voxygen.audio.soundtrack.overworld.between_the_fairies", length: 175.0, @@ -370,8 +370,26 @@ (Forest, 1), ], site: Some(Void), - activity: Explore, + activity: State(Explore), artist: "badbbad", - ) + )), + Segmented( + title: "Combat1", + author: "DaforLynx", + timing: None, + biomes: [], + site: Some(Dungeon), + segments: [ + ("voxygen.audio.soundtrack.combat1.combat1-hi-loop", 54.0, State(Combat(High))), + ("voxygen.audio.soundtrack.combat1.combat1-hi-start", 55.0, Transition(Explore, Combat(High))), + ("voxygen.audio.soundtrack.combat1.combat1-lo-loop", 7.0, State(Combat(Low))), + ("voxygen.audio.soundtrack.combat1.combat1-lo-start", 10.0, Transition(Explore, Combat(Low))), + ("voxygen.audio.soundtrack.combat1.combat1-trans-hi-lo", 10.0, Transition(Combat(High), Combat(Low))), + ("voxygen.audio.soundtrack.combat1.combat1-trans-lo-hi", 7.0, Transition(Combat(Low), Combat(High))), + // temporary until more assets exist: + ("voxygen.audio.soundtrack.combat1.combat1-trans-hi-lo", 10.0, Transition(Combat(High), Explore)), + ("voxygen.audio.soundtrack.combat1.combat1-lo-loop", 7.0, Transition(Combat(Low), Explore)), + ], + ), ] ) diff --git a/assets/voxygen/audio/soundtrack/combat1/combat1-hi-loop.ogg b/assets/voxygen/audio/soundtrack/combat1/combat1-hi-loop.ogg new file mode 100644 index 0000000000..741a92a16f --- /dev/null +++ b/assets/voxygen/audio/soundtrack/combat1/combat1-hi-loop.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07e54f39b02b34678c4b480ea3a6326aada8735d4d033f0ed684a278f5565d02 +size 984725 diff --git a/assets/voxygen/audio/soundtrack/combat1/combat1-hi-start.ogg b/assets/voxygen/audio/soundtrack/combat1/combat1-hi-start.ogg new file mode 100644 index 0000000000..3c081342f1 --- /dev/null +++ b/assets/voxygen/audio/soundtrack/combat1/combat1-hi-start.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15e848a16f57d11e6137c75488ef0d3f1113dedd6ff647354e93ab9aada120ef +size 970856 diff --git a/assets/voxygen/audio/soundtrack/combat1/combat1-lo-loop.ogg b/assets/voxygen/audio/soundtrack/combat1/combat1-lo-loop.ogg new file mode 100644 index 0000000000..e737f9e8b0 --- /dev/null +++ b/assets/voxygen/audio/soundtrack/combat1/combat1-lo-loop.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3f5ccad9ee64bc473bd94519b662a7da8e8852bafce5f227b4e6d0ffadba292 +size 138018 diff --git a/assets/voxygen/audio/soundtrack/combat1/combat1-lo-start.ogg b/assets/voxygen/audio/soundtrack/combat1/combat1-lo-start.ogg new file mode 100644 index 0000000000..cae1ce0c9e --- /dev/null +++ b/assets/voxygen/audio/soundtrack/combat1/combat1-lo-start.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbef1f3c47a22e93f9723c4e1c40b4ccdf2078b8b9116ec30cb1d5f93efd0916 +size 194587 diff --git a/assets/voxygen/audio/soundtrack/combat1/combat1-trans-hi-lo.ogg b/assets/voxygen/audio/soundtrack/combat1/combat1-trans-hi-lo.ogg new file mode 100644 index 0000000000..55405138ff --- /dev/null +++ b/assets/voxygen/audio/soundtrack/combat1/combat1-trans-hi-lo.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7002b4c2de92c1e6792c8e41d0367cda49996243309e8afb43a84d626a65f944 +size 203394 diff --git a/assets/voxygen/audio/soundtrack/combat1/combat1-trans-lo-hi.ogg b/assets/voxygen/audio/soundtrack/combat1/combat1-trans-lo-hi.ogg new file mode 100644 index 0000000000..40b6814251 --- /dev/null +++ b/assets/voxygen/audio/soundtrack/combat1/combat1-trans-lo-hi.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0de994318a3b6c973498b14a1e32a1f39e4589f2c236674fa62274c158d33013 +size 138016 diff --git a/voxygen/src/audio/channel.rs b/voxygen/src/audio/channel.rs index 6d325d72d1..0cbf0b2fac 100644 --- a/voxygen/src/audio/channel.rs +++ b/voxygen/src/audio/channel.rs @@ -41,6 +41,7 @@ enum ChannelState { pub enum MusicChannelTag { TitleMusic, Exploration, + Combat, } /// A MusicChannel uses a non-positional audio sink designed to play music which diff --git a/voxygen/src/audio/music.rs b/voxygen/src/audio/music.rs index 9f4433b955..2ad19624cf 100644 --- a/voxygen/src/audio/music.rs +++ b/voxygen/src/audio/music.rs @@ -53,17 +53,21 @@ use common_sys::state::State; use rand::{prelude::SliceRandom, thread_rng, Rng}; use serde::Deserialize; use std::time::Instant; -use tracing::warn; +use tracing::{debug, trace, warn}; /// Collection of all the tracks -#[derive(Debug, Default, Deserialize)] -struct SoundtrackCollection { +#[derive(Debug, Deserialize)] +struct SoundtrackCollection { /// List of tracks - tracks: Vec, + tracks: Vec, +} + +impl Default for SoundtrackCollection { + fn default() -> Self { Self { tracks: Vec::new() } } } /// Configuration for a single music track in the soundtrack -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct SoundtrackItem { /// Song title title: String, @@ -82,14 +86,38 @@ pub struct SoundtrackItem { activity: MusicActivity, } -#[derive(Debug, Deserialize, PartialEq)] -enum MusicActivity { +#[derive(Clone, Debug, Deserialize)] +enum RawSoundtrackItem { + Individual(SoundtrackItem), + Segmented { + title: String, + timing: Option, + biomes: Vec<(BiomeKind, u8)>, + site: Option, + segments: Vec<(String, f32, MusicActivity)>, + }, +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +enum CombatIntensity { + Low, + High, +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +enum MusicActivityState { Explore, - Combat, + Combat(CombatIntensity), +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +enum MusicActivity { + State(MusicActivityState), + Transition(MusicActivityState, MusicActivityState), } /// Allows control over when a track should play based on in-game time of day -#[derive(Debug, Deserialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, PartialEq)] enum DayPeriod { /// 8:00 AM to 7:30 PM Day, @@ -109,7 +137,7 @@ enum PlayState { /// Provides methods to control music playback pub struct MusicMgr { /// Collection of all the tracks - soundtrack: AssetHandle, + soundtrack: AssetHandle>, /// Instant at which the current track began playing began_playing: Instant, /// Time until the next track should be played @@ -117,6 +145,8 @@ pub struct MusicMgr { /// The title of the last track played. Used to prevent a track /// being played twice in a row last_track: String, + /// The previous track's activity kind, for transitions + last_activity: MusicActivityState, } impl Default for MusicMgr { @@ -126,6 +156,7 @@ impl Default for MusicMgr { began_playing: Instant::now(), next_track_change: 0.0, last_track: String::from("None"), + last_activity: MusicActivityState::Explore, } } } @@ -144,41 +175,97 @@ impl MusicMgr { // player_alt = position.0.z; //} + use common::comp::{Health, Pos}; + use specs::{Join, WorldExt}; + use MusicActivityState::*; + let mut activity_state = Explore; + let player = client.entity(); + let ecs = state.ecs(); + let entities = ecs.entities(); + let positions = ecs.read_component::(); + let healths = ecs.read_component::(); + if let Some(player_pos) = positions.get(player) { + const NEARBY_RADIUS: f32 = 50.0; + // TODO: consider filtering by Alignment::Enemy (requires sync changes) + let num_nearby_entities: u32 = (&entities, &positions, &healths) + .join() + .map(|(entity, pos, _)| { + if entity != player + && (player_pos.0 - pos.0).magnitude_squared() < NEARBY_RADIUS.powf(2.0) + { + 1 + } else { + 0 + } + }) + .sum(); + + if num_nearby_entities > 2 { + activity_state = Combat(CombatIntensity::High); + } else if num_nearby_entities >= 1 { + activity_state = Combat(CombatIntensity::Low); + } + } + + let activity = if self.last_activity != activity_state { + MusicActivity::Transition(self.last_activity, activity_state) + } else { + MusicActivity::State(activity_state) + }; + + let interrupt = matches!(activity, MusicActivity::Transition(_, _)); + if audio.music_enabled() && !self.soundtrack.read().tracks.is_empty() - && self.began_playing.elapsed().as_secs_f32() > self.next_track_change + && (self.began_playing.elapsed().as_secs_f32() > self.next_track_change || interrupt) { - self.play_random_track(audio, state, client); + trace!("in audio maintain: {:?} {:?}", self.last_activity, activity); + if let Ok(()) = self.play_random_track(audio, state, client, &activity) { + self.last_activity = activity_state; + } } } - fn play_random_track(&mut self, audio: &mut AudioFrontend, state: &State, client: &Client) { + fn play_random_track( + &mut self, + audio: &mut AudioFrontend, + state: &State, + client: &Client, + activity: &MusicActivity, + ) -> Result<(), ()> { let mut rng = thread_rng(); // Adds a bit of randomness between plays - let silence_between_tracks_seconds: f32 = rng.gen_range(60.0..120.0); + let silence_between_tracks_seconds: f32 = + if matches!(activity, MusicActivity::State(MusicActivityState::Explore)) { + rng.gen_range(60.0..120.0) + } else { + 0.0 + }; let is_dark = (state.get_day_period().is_dark()) as bool; let current_period_of_day = Self::get_current_day_period(is_dark); let current_biome = client.current_biome(); let current_site = client.current_site(); - // Filters out tracks not matching the timing, site, and biome + // Filter the soundtrack in stages, so that we don't overprune it if there are + // too many constraints. Returning Err(()) signals that we couldn't find + // an appropriate track for the current state, and hence the state + // machine for the activity shouldn't be updated. + let mut res = Ok(()); let soundtrack = self.soundtrack.read(); - let maybe_tracks = soundtrack + // First, filter out tracks not matching the timing, site, and biome + let mut maybe_tracks = soundtrack .tracks .iter() - .filter(|track| track.activity == MusicActivity::Explore) .filter(|track| { - !track.title.eq(&self.last_track) - && match &track.timing { - Some(period_of_day) => period_of_day == ¤t_period_of_day, - None => true, - } - && match &track.site { - Some(site) => site == ¤t_site, - None => true, - } + (match &track.timing { + Some(period_of_day) => period_of_day == ¤t_period_of_day, + None => true, + }) && match &track.site { + Some(site) => site == ¤t_site, + None => true, + } }) .filter(|track| { let mut result = false; @@ -194,6 +281,37 @@ impl MusicMgr { result }) .collect::>(); + // Second, filter out any tracks that don't match the current activity. + let filtered_tracks: Vec<_> = maybe_tracks + .iter() + .filter(|track| &track.activity == activity) + .cloned() + .collect(); + trace!( + "maybe_len: {}, filtered len: {}", + maybe_tracks.len(), + filtered_tracks.len() + ); + if !filtered_tracks.is_empty() { + maybe_tracks = filtered_tracks; + } else { + res = Err(()); + } + // Third, prevent playing the last track if possible (though don't return Err + // here, since the combat music is intended to loop) + let filtered_tracks: Vec<_> = maybe_tracks + .iter() + .filter(|track| !track.title.eq(&self.last_track)) + .cloned() + .collect(); + trace!( + "maybe_len: {}, filtered len: {}", + maybe_tracks.len(), + filtered_tracks.len() + ); + if !filtered_tracks.is_empty() { + maybe_tracks = filtered_tracks; + } // Randomly selects a track from the remaining tracks weighted based // on the biome @@ -213,6 +331,10 @@ impl MusicMgr { } chance }); + debug!( + "selecting new track for {:?}: {:?}", + activity, new_maybe_track + ); if let Ok(track) = new_maybe_track { //println!("Now playing {:?}", track.title); @@ -220,7 +342,19 @@ impl MusicMgr { self.began_playing = Instant::now(); self.next_track_change = track.length + silence_between_tracks_seconds; - audio.play_music(&track.path, MusicChannelTag::Exploration); + let tag = if matches!( + activity, + MusicActivity::State(MusicActivityState::Explore) + | MusicActivity::Transition(_, MusicActivityState::Explore) + ) { + MusicChannelTag::Exploration + } else { + MusicChannelTag::Combat + }; + audio.play_music(&track.path, tag); + res + } else { + Err(()) } } @@ -232,23 +366,58 @@ impl MusicMgr { } } - fn load_soundtrack_items() -> AssetHandle { + fn load_soundtrack_items() -> AssetHandle> { // Cannot fail: A default value is always provided SoundtrackCollection::load_expect("voxygen.audio.soundtrack") } } - -impl assets::Asset for SoundtrackCollection { +impl assets::Asset for SoundtrackCollection { type Loader = assets::RonLoader; const EXTENSION: &'static str = "ron"; +} - fn default_value(_: &str, error: assets::Error) -> Result { - warn!( - "Error reading music config file, music will not be available: {:#?}", - error - ); - - Ok(SoundtrackCollection::default()) +impl assets::Compound for SoundtrackCollection { + fn load( + _: &assets::AssetCache, + id: &str, + ) -> Result { + let inner = || -> Result<_, assets::Error> { + let manifest: AssetHandle>> = + AssetExt::load(id)?; + let mut soundtracks = SoundtrackCollection::default(); + for item in manifest.read().0.tracks.iter().cloned() { + match item { + RawSoundtrackItem::Individual(track) => soundtracks.tracks.push(track), + RawSoundtrackItem::Segmented { + title, + timing, + biomes, + site, + segments, + } => { + for (path, length, activity) in segments.into_iter() { + soundtracks.tracks.push(SoundtrackItem { + title: title.clone(), + path, + length, + timing: timing.clone(), + biomes: biomes.clone(), + site, + activity, + }); + } + }, + } + } + Ok(soundtracks) + }; + match inner() { + Ok(soundtracks) => Ok(soundtracks), + Err(e) => { + warn!("Error loading soundtracks: {:?}", e); + Ok(SoundtrackCollection::default()) + }, + } } }