Music player in the sound settings: pick any calm track or automatic

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-06 08:49:54 +01:00
1 parent 51b7bb4804
commit 4ef7672008
22 files changed
+108 -4

No files matched your search

+44
View File
@@ -30,6 +30,36 @@ pub fn tracks_in(c: Category) -> Vec<usize> {
TRACKS.iter().enumerate().filter(|(_, t)| t.1 == c).map(|(i, _)| i).collect()
}
// the calm tracks the music player offers (no battle, victory or defeat music)
pub fn player_tracks() -> Vec<usize> {
use Category as C;
TRACKS.iter().enumerate().filter(|(_, t)| matches!(t.1, C::Title | C::Build | C::Surface | C::Torture | C::Temple)).map(|(i, _)| i).collect()
}
pub fn track_id(t: usize) -> &'static str {
TRACKS.get(t).map_or("", |t| t.0)
}
pub fn track_by_id(id: &str) -> Option<usize> {
if id.is_empty() {
return None;
}
TRACKS.iter().position(|t| t.0 == id)
}
// the song title stored in the XM module (without the "DungeonCraft - " prefix)
pub fn track_title(t: usize) -> String {
let Some(d) = TRACKS.get(t).map(|t| t.2) else { return String::new() };
let raw = d.get(17..37).unwrap_or(&[]);
let name: String = raw.iter().take_while(|&&b| b != 0).map(|&b| b as char).collect();
let name = name.trim().trim_start_matches("DungeonCraft - ").trim().to_string();
if name.is_empty() {
track_id(t).to_string()
} else {
name
}
}
const MUSIC_GAIN: f32 = 0.45; // our replayer is ~4x louder than libopenmpt; leaves headroom for effects
const SFX_GAIN: f32 = 0.55;
@@ -218,6 +248,20 @@ pub extern "C" fn dc_audio_render(frames: u32) -> *const f32 {
mod tests {
use super::*;
#[test]
fn the_music_player_offers_only_calm_tracks_with_titles() {
let l = player_tracks();
assert!(l.len() > 20);
for t in l {
let id = track_id(t);
assert!(!id.starts_with("battle") && !id.starts_with("victory") && id != "defeat" && !id.starts_with("assault"), "{}", id);
assert!(!track_title(t).is_empty());
assert_eq!(track_by_id(id), Some(t));
}
assert_eq!(track_by_id(""), None);
assert_eq!(track_title(track_by_id("build13").unwrap()), "Underground River");
}
#[test]
fn engine_crossfade_and_sfx() {
dc_audio_init(48000.0);
+15 -3
View File
@@ -481,7 +481,8 @@ impl MusicDirector {
t
}
fn update(&mut self, g: &Game, title: bool, cam: (f32, f32)) {
// `chosen`: a track picked in the music player; it replaces the calm music
fn update(&mut self, g: &Game, title: bool, cam: (f32, f32), chosen: Option<usize>) {
use audio::Category as C;
let alive = |f: &dyn Fn(&game::Ent) -> bool| g.ents.iter().any(|e| e.alive && f(e));
let near_room = |k: map::Kind| {
@@ -535,8 +536,19 @@ impl MusicDirector {
C::Build | C::Torture | C::Temple if above => C::Surface,
w => w,
};
let calm = matches!(want, C::Build | C::Title | C::Surface | C::Torture | C::Temple);
if let (true, Some(t)) = (calm, chosen) {
if audio::current_track() != Some(t) {
let fade = if self.current.map_or(false, |c| matches!(c, C::Victory | C::Defeat)) { 1.0 } else { 2.5 };
audio::play_music(t, fade);
}
self.current = Some(want);
return;
}
let rotate = matches!(want, C::Build | C::Title | C::Surface) && self.current == Some(want) && audio::music_loops() >= 2;
if self.current != Some(want) || rotate || audio::current_track().is_none() {
// after the chosen track was let go, the director picks again
let foreign = chosen.is_none() && audio::current_track().map_or(false, |t| crate::tracks::TRACKS.get(t).map_or(false, |x| x.1 != want)) && calm && self.current == Some(want);
if self.current != Some(want) || rotate || foreign || audio::current_track().is_none() {
let before = self.current;
self.current = Some(want);
let t = self.pick(want);
@@ -2005,7 +2017,7 @@ app.pending_briefing = briefing_of(&campaign, i);
| Some(Page::Match { from_title: true })
| Some(Page::Achievements { from_title: true })
);
music.update(&app.game, on_title, (app.cam.rx, app.cam.rz));
music.update(&app.game, on_title, (app.cam.rx, app.cam.rz), audio::track_by_id(&app.settings.music_track));
// ---------------- achievements: count the game, unlock, store and share the profile
{
let ctx = achievements::Context { coop: app.coop, editor_test: app.editor_testing };
+11 -1
View File
@@ -67,12 +67,13 @@ pub enum Item {
Events,
TouchUi,
Voice,
Track,
}
fn tab_items(tab: usize) -> &'static [Item] {
use Item::*;
match tab {
0 => &[Language, Volume, MusicVolume, Mute, Voice],
0 => &[Language, Volume, MusicVolume, Track, Mute, Voice],
1 => &[RotateSpeed, PanSpeed, ZoomSpeed, EdgeScroll, InvertMouse],
2 => &[Outlines, WallDetail, Lights, UiScale, RenderScale],
3 => &[GameSpeed, Hints, Events, TouchUi],
@@ -108,6 +109,7 @@ fn item_label(it: Item) -> String {
Item::Events => "set.events",
Item::TouchUi => "set.touch_ui",
Item::Voice => "set.voice",
Item::Track => "set.track",
})
}
@@ -126,6 +128,7 @@ fn item_value(it: Item, s: &Settings) -> String {
Item::MusicVolume => pct(s.music_volume),
Item::Mute => onoff(s.muted),
Item::Voice => onoff(s.voice),
Item::Track => crate::audio::track_by_id(&s.music_track).map_or_else(|| tr("val.track_auto"), crate::audio::track_title),
Item::RotateSpeed => pct(s.rotate_speed),
Item::PanSpeed => pct(s.pan_speed),
Item::ZoomSpeed => pct(s.zoom_speed),
@@ -184,6 +187,13 @@ fn item_change(it: Item, s: &mut Settings, d: i32) {
Item::MusicVolume => s.music_volume = step(s.music_volume, d, 0.0, 1.0, 0.1),
Item::Mute => s.muted = !s.muted,
Item::Voice => s.voice = !s.voice,
Item::Track => {
// automatic, then every calm track
let mut opts: Vec<String> = vec![String::new()];
opts.extend(crate::audio::player_tracks().into_iter().map(|t| crate::audio::track_id(t).to_string()));
let i = opts.iter().position(|o| *o == s.music_track).unwrap_or(0) as i32;
s.music_track = opts[(i + d).rem_euclid(opts.len() as i32) as usize].clone();
}
Item::RotateSpeed => s.rotate_speed = step(s.rotate_speed, d, 0.25, 2.5, 0.25),
Item::PanSpeed => s.pan_speed = step(s.pan_speed, d, 0.25, 2.5, 0.25),
Item::ZoomSpeed => s.zoom_speed = step(s.zoom_speed, d, 0.25, 2.5, 0.25),
+2
View File
@@ -137,6 +137,7 @@ pub struct Settings {
pub music_volume: f32,
pub muted: bool,
pub voice: bool, // Crookfinger's babbling voice
pub music_track: String, // music player: "" = automatic, else the id of a calm track that replaces the calm music
// camera
pub rotate_speed: f32,
pub pan_speed: f32,
@@ -177,6 +178,7 @@ impl Default for Settings {
music_volume: 0.6,
muted: false,
voice: true,
music_track: String::new(),
rotate_speed: 1.0,
pan_speed: 1.25,
zoom_speed: 1.0,