1use crate::audio::{AudioFrontend, MusicChannelTag};
47use client::Client;
48use common::{
49 assets::{self, AssetExt, AssetHandle},
50 calendar::{Calendar, CalendarEvent},
51 terrain::{BiomeKind, SiteKindMeta},
52 weather::WeatherKind,
53};
54use common_state::State;
55use hashbrown::HashMap;
56use kira::clock::ClockTime;
57use rand::{Rng, prelude::SliceRandom, rngs::ThreadRng, thread_rng};
58use serde::Deserialize;
59use tracing::{debug, trace, warn};
60
61#[derive(Debug, Deserialize)]
63struct SoundtrackCollection<T> {
64 tracks: Vec<T>,
66}
67
68impl<T> Default for SoundtrackCollection<T> {
69 fn default() -> Self { Self { tracks: Vec::new() } }
70}
71
72#[derive(Clone, Debug, Deserialize)]
74pub struct SoundtrackItem {
75 title: String,
77 path: String,
79 length: f32,
81 loop_points: Option<(f32, f32)>,
82 timing: Option<DayPeriod>,
84 weather: Option<WeatherKind>,
86 biomes: Vec<(BiomeKind, u8)>,
88 sites: Vec<SiteKindMeta>,
90 music_state: MusicState,
93 #[serde(default)]
97 activity_override: Option<MusicActivity>,
98 artist: (String, Option<String>),
100}
101
102#[derive(Clone, Debug, Deserialize)]
103enum RawSoundtrackItem {
104 Individual(SoundtrackItem),
105 Segmented {
106 title: String,
107 timing: Option<DayPeriod>,
108 weather: Option<WeatherKind>,
109 biomes: Vec<(BiomeKind, u8)>,
110 sites: Vec<SiteKindMeta>,
111 segments: Vec<(String, f32, MusicState, Option<MusicActivity>)>,
112 loop_points: (f32, f32),
113 artist: (String, Option<String>),
114 },
115}
116
117#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
118enum CombatIntensity {
119 Low,
120 High,
121}
122
123#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
124enum MusicActivity {
125 Explore,
126 Combat(CombatIntensity),
127}
128
129#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
130enum MusicState {
131 Activity(MusicActivity),
132 Transition(MusicActivity, MusicActivity),
133}
134
135#[derive(Clone, Debug, Deserialize, PartialEq)]
137enum DayPeriod {
138 Day,
140 Night,
142}
143
144pub struct MusicMgr {
146 soundtrack: SoundtrackCollection<SoundtrackItem>,
148 began_playing: Option<ClockTime>,
150 song_end: Option<ClockTime>,
152 gap_length: f32,
154 gap_time: f64,
156 last_track: String,
159 last_combat_track: String,
160 last_interrupt_attempt: Option<ClockTime>,
162 last_activity: MusicState,
164 current_track: String,
166 current_artist: String,
167 track_length: f32,
168 loop_points: Option<(f32, f32)>,
169}
170
171#[derive(Deserialize)]
172pub struct MusicTransitionManifest {
173 combat_nearby_radius: f32,
175 combat_health_factor: f32,
178 combat_nearby_high_thresh: u32,
180 combat_nearby_low_thresh: u32,
182 pub fade_timings: HashMap<(MusicChannelTag, MusicChannelTag), (f32, f32)>,
184 pub interrupt_delay: f32,
186}
187
188impl Default for MusicTransitionManifest {
189 fn default() -> MusicTransitionManifest {
190 MusicTransitionManifest {
191 combat_nearby_radius: 40.0,
192 combat_health_factor: 100.0,
193 combat_nearby_high_thresh: 3,
194 combat_nearby_low_thresh: 1,
195 fade_timings: HashMap::new(),
196 interrupt_delay: 5.0,
197 }
198 }
199}
200
201impl assets::Asset for MusicTransitionManifest {
202 type Loader = assets::RonLoader;
203
204 const EXTENSION: &'static str = "ron";
205}
206
207fn time_f64(clock_time: ClockTime) -> f64 { clock_time.ticks as f64 + clock_time.fraction }
208
209impl MusicMgr {
210 pub fn new(calendar: &Calendar) -> Self {
211 Self {
212 soundtrack: Self::load_soundtrack_items(calendar),
213 began_playing: None,
214 song_end: None,
215 gap_length: 0.0,
216 gap_time: -1.0,
217 last_track: String::from("None"),
218 last_combat_track: String::from("None"),
219 last_interrupt_attempt: None,
220 last_activity: MusicState::Activity(MusicActivity::Explore),
221 current_track: String::from("None"),
222 current_artist: String::from("None"),
223 track_length: 0.0,
224 loop_points: None,
225 }
226 }
227
228 pub fn maintain(&mut self, audio: &mut AudioFrontend, state: &State, client: &Client) {
231 use common::comp::{Group, Health, Pos, group::ENEMY};
232 use specs::{Join, WorldExt};
233
234 if !audio.music_enabled() || audio.get_clock().is_none() || audio.get_clock_time().is_none()
235 {
236 return;
237 }
238
239 let mut activity_state = MusicActivity::Explore;
240
241 let player = client.entity();
242 let ecs = state.ecs();
243 let entities = ecs.entities();
244 let positions = ecs.read_component::<Pos>();
245 let healths = ecs.read_component::<Health>();
246 let groups = ecs.read_component::<Group>();
247 let mtm = audio.mtm.read();
248 let mut rng = thread_rng();
249
250 if audio.combat_music_enabled {
251 if let Some(player_pos) = positions.get(player) {
252 let num_nearby_entities: u32 = (&entities, &positions, &healths, &groups)
256 .join()
257 .map(|(entity, pos, health, group)| {
258 if entity != player
259 && group == &ENEMY
260 && (player_pos.0 - pos.0).magnitude_squared()
261 < mtm.combat_nearby_radius.powf(2.0)
262 {
263 (health.maximum() / mtm.combat_health_factor).ceil() as u32
264 } else {
265 0
266 }
267 })
268 .sum();
269
270 if num_nearby_entities >= mtm.combat_nearby_high_thresh {
271 activity_state = MusicActivity::Combat(CombatIntensity::High);
272 } else if num_nearby_entities >= mtm.combat_nearby_low_thresh {
273 activity_state = MusicActivity::Combat(CombatIntensity::Low);
274 }
275 }
276 }
277
278 if let Some(health) = healths.get(player) {
280 if health.is_dead {
281 activity_state = MusicActivity::Explore;
282 }
283 }
284
285 let mut music_state = match self.last_activity {
286 MusicState::Activity(prev) => {
287 if prev != activity_state {
288 MusicState::Transition(prev, activity_state)
289 } else {
290 MusicState::Activity(activity_state)
291 }
292 },
293 MusicState::Transition(_, next) => {
294 warn!("Transitioning: {:?}", self.last_activity);
295 MusicState::Activity(next)
296 },
297 };
298
299 let now = audio.get_clock_time().unwrap();
300
301 let began_playing = *self.began_playing.get_or_insert(now);
302 let last_interrupt_attempt = *self.last_interrupt_attempt.get_or_insert(now);
303 let song_end = *self.song_end.get_or_insert(now);
304 let mut time_since_began_playing = time_f64(now) - time_f64(began_playing);
305
306 let interrupt = matches!(music_state, MusicState::Transition(_, _))
310 && time_f64(now) - time_f64(last_interrupt_attempt) > mtm.interrupt_delay as f64;
311
312 if matches!(
315 music_state,
316 MusicState::Transition(
317 MusicActivity::Combat(CombatIntensity::High),
318 MusicActivity::Explore
319 )
320 ) {
321 music_state = MusicState::Activity(MusicActivity::Explore)
322 }
323
324 if audio.music_enabled()
325 && !self.soundtrack.tracks.is_empty()
326 && (time_since_began_playing
327 > time_f64(song_end) - time_f64(began_playing) || interrupt)
329 {
330 time_since_began_playing = time_f64(now) - time_f64(began_playing);
331 if time_since_began_playing > self.track_length as f64
332 && self.last_activity
333 != MusicState::Activity(MusicActivity::Combat(CombatIntensity::High))
334 {
335 self.current_track = String::from("None");
336 self.current_artist = String::from("None");
337 }
338
339 if interrupt {
340 self.last_interrupt_attempt = Some(now);
341 if let Ok(next_activity) =
342 self.play_random_track(audio, state, client, &music_state, &mut rng)
343 {
344 trace!(
345 "pre-play_random_track: {:?} {:?}",
346 self.last_activity, music_state
347 );
348 self.last_activity = next_activity;
349 }
350 } else if music_state == MusicState::Activity(MusicActivity::Explore)
351 || music_state
352 == MusicState::Transition(
353 MusicActivity::Explore,
354 MusicActivity::Combat(CombatIntensity::High),
355 )
356 {
357 if self.gap_time == 0.0 {
359 self.gap_length = self.generate_silence_between_tracks(
360 audio.music_spacing,
361 client,
362 &music_state,
363 &mut rng,
364 );
365 self.gap_time = self.gap_length as f64;
366 self.song_end = audio.get_clock_time();
367 } else if self.gap_time < 0.0 {
368 if music_state
371 == MusicState::Transition(
372 MusicActivity::Explore,
373 MusicActivity::Combat(CombatIntensity::High),
374 )
375 {
376 music_state = MusicState::Activity(MusicActivity::Explore)
377 }
378 if let Ok(next_activity) =
379 self.play_random_track(audio, state, client, &music_state, &mut rng)
380 {
381 self.last_activity = next_activity;
382 self.gap_time = 0.0;
383 self.gap_length = 0.0;
384 }
385 }
386 } else if music_state
387 == MusicState::Activity(MusicActivity::Combat(CombatIntensity::High))
388 {
389 self.began_playing = Some(now);
391 self.song_end = Some(ClockTime::from_ticks_f64(
392 audio.get_clock().unwrap().id(),
393 time_f64(now) + self.loop_points.unwrap_or((0.0, 0.0)).1 as f64
394 - self.loop_points.unwrap_or((0.0, 0.0)).0 as f64,
395 ));
396 } else {
397 trace!(
398 "pre-play_random_track: {:?} {:?}",
399 self.last_activity, music_state
400 );
401 }
402 } else {
403 if self.began_playing.is_none() {
404 self.began_playing = Some(now)
405 }
406 if self.soundtrack.tracks.is_empty() {
407 warn!("No tracks available to play")
408 }
409 }
410
411 if time_since_began_playing > self.track_length as f64 {
412 self.gap_time = (self.gap_length as f64) - (time_f64(now) - time_f64(song_end));
414 }
415 }
416
417 fn play_random_track(
418 &mut self,
419 audio: &mut AudioFrontend,
420 state: &State,
421 client: &Client,
422 music_state: &MusicState,
423 rng: &mut ThreadRng,
424 ) -> Result<MusicState, String> {
425 let is_dark = state.get_day_period().is_dark();
426 let current_period_of_day = Self::get_current_day_period(is_dark);
427 let current_weather = client.weather_at_player();
428 let current_biome = client.current_biome();
429 let current_site = client.current_site();
430
431 let mut maybe_tracks = self
438 .soundtrack
439 .tracks
440 .iter()
441 .filter(|track| {
442 (match &track.timing {
443 Some(period_of_day) => period_of_day == ¤t_period_of_day,
444 None => true,
445 }) && match &track.weather {
446 Some(weather) => weather == ¤t_weather.get_kind(),
447 None => true,
448 }
449 })
450 .filter(|track| track.sites.iter().any(|s| s == ¤t_site))
451 .filter(|track| {
452 track.biomes.is_empty() || track.biomes.iter().any(|b| b.0 == current_biome)
453 })
454 .filter(|track| &track.music_state == music_state)
455 .collect::<Vec<&SoundtrackItem>>();
456 if maybe_tracks.is_empty() {
457 let error_string = format!(
458 "No tracks for {:?}, {:?}, {:?}, {:?}, {:?}",
459 ¤t_period_of_day,
460 ¤t_weather,
461 ¤t_site,
462 ¤t_biome,
463 &music_state
464 );
465 return Err(error_string);
466 }
467 if matches!(
470 music_state,
471 &MusicState::Activity(MusicActivity::Combat(CombatIntensity::High))
472 | &MusicState::Transition(
473 MusicActivity::Combat(CombatIntensity::High),
474 MusicActivity::Explore
475 )
476 ) {
477 let filtered_tracks: Vec<_> = maybe_tracks
478 .iter()
479 .filter(|track| track.title.eq(&self.last_track))
480 .copied()
481 .collect();
482 if !filtered_tracks.is_empty() {
483 maybe_tracks = filtered_tracks;
484 }
485 } else {
486 let filtered_tracks: Vec<_> = maybe_tracks
487 .iter()
488 .filter(|track| !track.title.eq(&self.last_track))
489 .filter(|track| !track.title.eq(&self.last_combat_track))
490 .copied()
491 .collect();
492 if !filtered_tracks.is_empty() {
493 maybe_tracks = filtered_tracks;
494 }
495 }
496
497 let new_maybe_track = maybe_tracks.choose_weighted(rng, |track| {
500 track
504 .biomes
505 .iter()
506 .find(|b| b.0 == current_biome)
507 .map_or(1.0, |b| (1.0_f32 / (b.1 as f32)))
508 });
509 debug!(
510 "selecting new track for {:?}: {:?}",
511 music_state, new_maybe_track
512 );
513
514 if let Ok(track) = new_maybe_track {
515 let now = audio.get_clock_time().unwrap();
516 self.last_track = String::from(&track.title);
518 self.began_playing = Some(now);
519 self.song_end = Some(ClockTime::from_ticks_f64(
520 audio.get_clock().unwrap().id(),
521 time_f64(now) + track.length as f64,
522 ));
523 self.track_length = track.length;
524 self.gap_length = 0.0;
525 if audio.music_enabled() {
526 self.current_track = String::from(&track.title);
527 self.current_artist = String::from(&track.artist.0);
528 } else {
529 self.current_track = String::from("None");
530 self.current_artist = String::from("None");
531 }
532
533 let tag = if matches!(music_state, MusicState::Activity(MusicActivity::Explore)) {
534 MusicChannelTag::Exploration
535 } else {
536 self.last_combat_track = String::from(&track.title);
537 MusicChannelTag::Combat
538 };
539 audio.play_music(&track.path, tag, track.length);
540 if tag == MusicChannelTag::Combat {
541 audio.set_loop_points(
542 tag,
543 track.loop_points.unwrap_or((0.0, 0.0)).0,
544 track.loop_points.unwrap_or((0.0, 0.0)).1,
545 );
546 self.loop_points = track.loop_points
547 } else {
548 self.loop_points = None
549 };
550
551 if let Some(state) = track.activity_override {
552 Ok(MusicState::Activity(state))
553 } else {
554 Ok(*music_state)
555 }
556 } else {
557 Err(format!("{:?}", new_maybe_track))
558 }
559 }
560
561 fn generate_silence_between_tracks(
562 &self,
563 spacing_multiplier: f32,
564 client: &Client,
565 music_state: &MusicState,
566 rng: &mut ThreadRng,
567 ) -> f32 {
568 let mut silence_between_tracks_seconds: f32 = 0.0;
569 if spacing_multiplier > f32::EPSILON {
570 silence_between_tracks_seconds =
571 if matches!(
572 music_state,
573 MusicState::Activity(MusicActivity::Explore)
574 | MusicState::Transition(
575 MusicActivity::Explore,
576 MusicActivity::Combat(CombatIntensity::High)
577 )
578 ) && matches!(client.current_site(), SiteKindMeta::Settlement(_))
579 {
580 rng.gen_range(120.0 * spacing_multiplier..180.0 * spacing_multiplier)
581 } else if matches!(
582 music_state,
583 MusicState::Activity(MusicActivity::Explore)
584 | MusicState::Transition(
585 MusicActivity::Explore,
586 MusicActivity::Combat(CombatIntensity::High)
587 )
588 ) && matches!(client.current_site(), SiteKindMeta::Dungeon(_))
589 {
590 rng.gen_range(10.0 * spacing_multiplier..20.0 * spacing_multiplier)
591 } else if matches!(
592 music_state,
593 MusicState::Activity(MusicActivity::Explore)
594 | MusicState::Transition(
595 MusicActivity::Explore,
596 MusicActivity::Combat(CombatIntensity::High)
597 )
598 ) && matches!(client.current_site(), SiteKindMeta::Cave)
599 {
600 rng.gen_range(20.0 * spacing_multiplier..40.0 * spacing_multiplier)
601 } else if matches!(
602 music_state,
603 MusicState::Activity(MusicActivity::Explore)
604 | MusicState::Transition(
605 MusicActivity::Explore,
606 MusicActivity::Combat(CombatIntensity::High)
607 )
608 ) {
609 rng.gen_range(120.0 * spacing_multiplier..240.0 * spacing_multiplier)
610 } else if matches!(
611 music_state,
612 MusicState::Activity(MusicActivity::Combat(_)) | MusicState::Transition(_, _)
613 ) {
614 0.0
615 } else {
616 rng.gen_range(30.0 * spacing_multiplier..60.0 * spacing_multiplier)
617 };
618 }
619 silence_between_tracks_seconds
620 }
621
622 fn get_current_day_period(is_dark: bool) -> DayPeriod {
623 if is_dark {
624 DayPeriod::Night
625 } else {
626 DayPeriod::Day
627 }
628 }
629
630 pub fn current_track(&self) -> String { self.current_track.clone() }
631
632 pub fn current_artist(&self) -> String { self.current_artist.clone() }
633
634 pub fn reset_track(&mut self) {
635 self.current_artist = String::from("None");
636 self.current_track = String::from("None");
637 }
638
639 fn load_soundtrack_items(calendar: &Calendar) -> SoundtrackCollection<SoundtrackItem> {
643 let mut soundtrack = SoundtrackCollection::default();
644 if calendar.events().len() == 0 {
646 for track in SoundtrackCollection::load_expect("voxygen.audio.soundtrack")
647 .read()
648 .tracks
649 .clone()
650 {
651 soundtrack.tracks.push(track)
652 }
653 } else {
654 for event in calendar.events() {
656 match event {
657 CalendarEvent::Halloween => {
658 for track in SoundtrackCollection::load_expect(
659 "voxygen.audio.calendar.halloween.soundtrack",
660 )
661 .read()
662 .tracks
663 .clone()
664 {
665 soundtrack.tracks.push(track)
666 }
667 },
668 CalendarEvent::Christmas => {
669 for track in SoundtrackCollection::load_expect(
670 "voxygen.audio.calendar.christmas.soundtrack",
671 )
672 .read()
673 .tracks
674 .clone()
675 {
676 soundtrack.tracks.push(track)
677 }
678 },
679 _ => {
680 for track in SoundtrackCollection::load_expect("voxygen.audio.soundtrack")
681 .read()
682 .tracks
683 .clone()
684 {
685 soundtrack.tracks.push(track)
686 }
687 },
688 }
689 }
690 }
691 if soundtrack.tracks.is_empty() {
693 for track in SoundtrackCollection::load_expect("voxygen.audio.soundtrack")
694 .read()
695 .tracks
696 .clone()
697 {
698 soundtrack.tracks.push(track)
699 }
700 soundtrack
701 } else {
702 soundtrack
703 }
704 }
705}
706impl assets::Asset for SoundtrackCollection<RawSoundtrackItem> {
707 type Loader = assets::RonLoader;
708
709 const EXTENSION: &'static str = "ron";
710}
711
712impl assets::Compound for SoundtrackCollection<SoundtrackItem> {
713 fn load(_: assets::AnyCache, id: &assets::SharedString) -> Result<Self, assets::BoxedError> {
714 let manifest: AssetHandle<SoundtrackCollection<RawSoundtrackItem>> = AssetExt::load(id)?;
715 let mut soundtrack = SoundtrackCollection::default();
716 for item in manifest.read().tracks.iter().cloned() {
717 match item {
718 RawSoundtrackItem::Individual(track) => soundtrack.tracks.push(track),
719 RawSoundtrackItem::Segmented {
720 title,
721 timing,
722 weather,
723 biomes,
724 sites,
725 segments,
726 loop_points,
727 artist,
728 } => {
729 for (path, length, music_state, activity_override) in segments.into_iter() {
730 soundtrack.tracks.push(SoundtrackItem {
731 title: title.clone(),
732 path,
733 length,
734 loop_points: Some(loop_points),
735 timing: timing.clone(),
736 weather,
737 biomes: biomes.clone(),
738 sites: sites.clone(),
739 music_state,
740 activity_override,
741 artist: artist.clone(),
742 });
743 }
744 },
745 }
746 }
747 Ok(soundtrack)
748 }
749}
750
751#[cfg(test)]
752mod tests {
753 use super::*;
754 use strum::IntoEnumIterator;
755
756 #[test]
757 fn test_load_soundtracks() {
758 let _: AssetHandle<SoundtrackCollection<SoundtrackItem>> =
759 SoundtrackCollection::load_expect("voxygen.audio.soundtrack");
760 for event in CalendarEvent::iter() {
761 match event {
762 CalendarEvent::Halloween => {
763 let _: AssetHandle<SoundtrackCollection<SoundtrackItem>> =
764 SoundtrackCollection::load_expect(
765 "voxygen.audio.calendar.halloween.soundtrack",
766 );
767 },
768 CalendarEvent::Christmas => {
769 let _: AssetHandle<SoundtrackCollection<SoundtrackItem>> =
770 SoundtrackCollection::load_expect(
771 "voxygen.audio.calendar.christmas.soundtrack",
772 );
773 },
774 _ => {},
775 }
776 }
777 }
778}