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