From 1389f043d825d5dce24c56488ce1df21d2948810 Mon Sep 17 00:00:00 2001 From: adstep Date: Wed, 3 Jun 2026 03:09:03 -0700 Subject: [PATCH 1/2] feat(evaluation): add optional results preview music on clear Add an EvalPreviewMusic option (Sound submenu, default off) that plays the song's preview music once on the results screen after a clear (silent on fail). --- assets/languages/en.ini | 2 ++ src/config/load/options.rs | 4 ++++ src/config/mod.rs | 2 ++ src/config/store/defaults.rs | 1 + src/config/store/save.rs | 1 + src/config/update/audio.rs | 11 ++++++++++ src/screens/evaluation.rs | 31 +++++++++++++++++++++++++++ src/screens/options/input.rs | 3 +++ src/screens/options/item.rs | 1 + src/screens/options/row.rs | 1 + src/screens/options/state.rs | 6 ++++++ src/screens/options/submenus/sound.rs | 17 +++++++++++++++ src/screens/select_music.rs | 2 +- 13 files changed, 81 insertions(+), 1 deletion(-) diff --git a/assets/languages/en.ini b/assets/languages/en.ini index 74ba99183..6c96199d8 100644 --- a/assets/languages/en.ini +++ b/assets/languages/en.ini @@ -242,6 +242,7 @@ ExclusiveMode=Exclusive Mode AudioSampleRate=Audio Sample Rate DefaultSuffix= (Default) MineSounds=Mine Sounds +EvalPreviewMusic=Results Preview Music GlobalOffsetMs=Global Offset (ms) GlobalOffset=Global Offset (ms) AlsaExclusive=Exclusive Mode @@ -1503,6 +1504,7 @@ SfxVolumeHelp=Set the sound-effect volume before master volume is applied. AssistTickVolumeHelp=Set the gameplay Assist Tick volume before master volume is applied. MusicVolumeHelp=Set the music volume before master volume is applied. MineSoundsHelp=Play a sound when mines are hit. +EvalPreviewMusicHelp=Play the song's preview music once on the results screen after clearing a song (silent on fail). GlobalOffsetHelp=Apply a global audio timing offset in 1 ms steps. RateModPreservesPitchHelp=Keep pitch constant when rate mods are active. ReplayGainHelp=Experimental: normalize playback loudness across songs using ReplayGain 2.0 / EBU R 128. Loudness is computed in the background on first play and cached on disk; gain is applied to subsequent playback or live once analysis completes. diff --git a/src/config/load/options.rs b/src/config/load/options.rs index 7a5c090eb..d5399f8bb 100644 --- a/src/config/load/options.rs +++ b/src/config/load/options.rs @@ -298,6 +298,10 @@ fn load_audio_opts(conf: &SimpleIni, default: Config, cfg: &mut Config) { .get("Options", "MenuMusic") .and_then(|v| v.parse::().ok()) .map_or(default.menu_music, |v| v != 0); + cfg.eval_preview_music = conf + .get("Options", "EvalPreviewMusic") + .and_then(|v| v.parse::().ok()) + .map_or(default.eval_preview_music, |v| v != 0); cfg.custom_sounds_enabled = conf .get("Options", "CustomSoundsEnabled") .and_then(|v| v.parse::().ok()) diff --git a/src/config/mod.rs b/src/config/mod.rs index a01491f2e..cdd1954c1 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -344,6 +344,7 @@ pub struct Config { pub visual_delay_seconds: f32, pub master_volume: u8, pub menu_music: bool, + pub eval_preview_music: bool, pub custom_sounds_enabled: bool, pub music_volume: u8, // ITGmania PrefsManager "MusicWheelSwitchSpeed" (default 15). @@ -529,6 +530,7 @@ impl Default for Config { visual_delay_seconds: 0.0, master_volume: 90, menu_music: true, + eval_preview_music: false, custom_sounds_enabled: true, music_volume: 100, music_wheel_switch_speed: 15, diff --git a/src/config/store/defaults.rs b/src/config/store/defaults.rs index 0d5a33e7e..30c881ab7 100644 --- a/src/config/store/defaults.rs +++ b/src/config/store/defaults.rs @@ -140,6 +140,7 @@ fn push_default_options(content: &mut String, default: &Config) { push_line(content, "VisualDelaySeconds", default.visual_delay_seconds); push_line(content, "MasterVolume", default.master_volume); push_bool(content, "MenuMusic", default.menu_music); + push_bool(content, "EvalPreviewMusic", default.eval_preview_music); push_bool( content, "CustomSoundsEnabled", diff --git a/src/config/store/save.rs b/src/config/store/save.rs index ef79fab6e..1e7ee2750 100644 --- a/src/config/store/save.rs +++ b/src/config/store/save.rs @@ -195,6 +195,7 @@ fn push_saved_options( push_line(content, "VisualDelaySeconds", cfg.visual_delay_seconds); push_line(content, "MasterVolume", cfg.master_volume); push_bool(content, "MenuMusic", cfg.menu_music); + push_bool(content, "EvalPreviewMusic", cfg.eval_preview_music); push_bool(content, "CustomSoundsEnabled", cfg.custom_sounds_enabled); push_bool(content, "MineHitSound", cfg.mine_hit_sound); push_line(content, "MusicVolume", cfg.music_volume); diff --git a/src/config/update/audio.rs b/src/config/update/audio.rs index e39ece6ba..16b287e6d 100644 --- a/src/config/update/audio.rs +++ b/src/config/update/audio.rs @@ -37,6 +37,17 @@ pub fn update_menu_music(enabled: bool) { save_without_keymaps(); } +pub fn update_eval_preview_music(enabled: bool) { + { + let mut cfg = lock_config(); + if cfg.eval_preview_music == enabled { + return; + } + cfg.eval_preview_music = enabled; + } + save_without_keymaps(); +} + pub fn update_software_renderer_threads(threads: u8) { { let mut cfg = lock_config(); diff --git a/src/screens/evaluation.rs b/src/screens/evaluation.rs index dd0bf7829..7f8e4436f 100644 --- a/src/screens/evaluation.rs +++ b/src/screens/evaluation.rs @@ -1969,6 +1969,7 @@ pub struct State { menu_lr_undo: [i8; MAX_PLAYERS], favorite_code: crate::screens::favorite_code::FavoriteCodeTracker, test_input_state: test_input::State, + preview_music_started: bool, } impl Clone for State { @@ -2014,6 +2015,7 @@ impl Clone for State { menu_lr_undo: self.menu_lr_undo, favorite_code: self.favorite_code.clone(), test_input_state: self.test_input_state.clone(), + preview_music_started: self.preview_music_started, } } } @@ -2607,6 +2609,7 @@ pub fn init(gameplay_results: Option) -> State { menu_lr_undo: [0; MAX_PLAYERS], favorite_code: Default::default(), test_input_state: test_input::State::default(), + preview_music_started: false, } } @@ -2759,6 +2762,7 @@ pub fn init_from_score_info( menu_lr_undo: [0; MAX_PLAYERS], favorite_code: Default::default(), test_input_state: test_input::State::default(), + preview_music_started: false, } } @@ -2936,6 +2940,8 @@ pub fn update(state: &mut State, dt: f32) { state.screen_elapsed += dt; } + maybe_start_clear_preview_music(state); + online::lobbies::poll_reconnect(); online::lobbies::update_machine_state_sides_with_stats( "ScreenEvaluationStage", @@ -3212,6 +3218,31 @@ pub(crate) fn all_joined_players_failed(state: &State) -> bool { any } +fn maybe_start_clear_preview_music(state: &mut State) { + if state.preview_music_started { + return; + } + state.preview_music_started = true; + + if !crate::config::get().eval_preview_music { + return; + } + if all_joined_players_failed(state) { + return; + } + let Some(si) = state.score_info.iter().flatten().next() else { + return; + }; + if let Some((path, cut)) = crate::screens::select_music::compute_preview_cut(&si.song) { + let rate = if si.music_rate.is_finite() && si.music_rate > 0.0 { + si.music_rate + } else { + 1.0 + }; + crate::engine::audio::play_music(path, cut, false, rate); + } +} + #[inline(always)] const fn stage_in_stinger_texture_key(failed: bool, disqualified: bool) -> Option<&'static str> { if failed || disqualified { diff --git a/src/screens/options/input.rs b/src/screens/options/input.rs index 78a469070..066d99a4f 100644 --- a/src/screens/options/input.rs +++ b/src/screens/options/input.rs @@ -566,6 +566,9 @@ pub(super) fn apply_submenu_choice_delta( SubRowId::MineSounds => { config::update_mine_hit_sound(new_index == 1); } + SubRowId::EvalPreviewMusic => { + config::update_eval_preview_music(new_index == 1); + } SubRowId::RateModPreservesPitch => { config::update_rate_mod_preserves_pitch(new_index == 1); } diff --git a/src/screens/options/item.rs b/src/screens/options/item.rs index 9c9646d2f..d712ada1b 100644 --- a/src/screens/options/item.rs +++ b/src/screens/options/item.rs @@ -125,6 +125,7 @@ pub enum ItemId { SndAssistTickVolume, SndMusicVolume, SndMineSounds, + SndEvalPreviewMusic, SndGlobalOffset, SndRateModPitch, SndReplayGain, diff --git a/src/screens/options/row.rs b/src/screens/options/row.rs index a4f8aae42..205d5c3a7 100644 --- a/src/screens/options/row.rs +++ b/src/screens/options/row.rs @@ -36,6 +36,7 @@ pub enum SubRowId { AssistTickVolume, MusicVolume, MineSounds, + EvalPreviewMusic, GlobalOffset, RateModPreservesPitch, ReplayGain, diff --git a/src/screens/options/state.rs b/src/screens/options/state.rs index 87fd46773..2e12d558e 100644 --- a/src/screens/options/state.rs +++ b/src/screens/options/state.rs @@ -882,6 +882,12 @@ pub fn init() -> State { SubRowId::MineSounds, usize::from(cfg.mine_hit_sound), ); + set_choice_by_id( + &mut state.sub[SubmenuKind::Sound].choice_indices, + SOUND_OPTIONS_ROWS, + SubRowId::EvalPreviewMusic, + usize::from(cfg.eval_preview_music), + ); set_choice_by_id( &mut state.sub[SubmenuKind::Sound].choice_indices, SOUND_OPTIONS_ROWS, diff --git a/src/screens/options/submenus/sound.rs b/src/screens/options/submenus/sound.rs index 6d17deae2..fe0e3b347 100644 --- a/src/screens/options/submenus/sound.rs +++ b/src/screens/options/submenus/sound.rs @@ -72,6 +72,15 @@ pub(in crate::screens::options) const SOUND_OPTIONS_ROWS: &[SubRow] = &[ ], inline: true, }, + SubRow { + id: SubRowId::EvalPreviewMusic, + label: lookup_key("OptionsSound", "EvalPreviewMusic"), + choices: &[ + localized_choice("Common", "Off"), + localized_choice("Common", "On"), + ], + inline: true, + }, SubRow { id: SubRowId::GlobalOffset, label: lookup_key("OptionsSound", "GlobalOffset"), @@ -181,6 +190,14 @@ pub(in crate::screens::options) const SOUND_OPTIONS_ITEMS: &[Item] = &[ "MineSoundsHelp", ))], }, + Item { + id: ItemId::SndEvalPreviewMusic, + name: lookup_key("OptionsSound", "EvalPreviewMusic"), + help: &[HelpEntry::Paragraph(lookup_key( + "OptionsSoundHelp", + "EvalPreviewMusicHelp", + ))], + }, Item { id: ItemId::SndGlobalOffset, name: lookup_key("OptionsSound", "GlobalOffset"), diff --git a/src/screens/select_music.rs b/src/screens/select_music.rs index aa3d1a283..0f94d3995 100644 --- a/src/screens/select_music.rs +++ b/src/screens/select_music.rs @@ -690,7 +690,7 @@ fn default_preview_start(song: &SongData, total_len: f64) -> f64 { sec_at_beat(song, i_beat) } -fn compute_preview_cut(song: &SongData) -> Option<(std::path::PathBuf, audio::Cut)> { +pub(crate) fn compute_preview_cut(song: &SongData) -> Option<(std::path::PathBuf, audio::Cut)> { let path = song.music_path.clone()?; let mut start = song.sample_start.unwrap_or(0.0) as f64; let mut length = song.sample_length.unwrap_or(0.0) as f64; From 3e89e2f641a924aa8e3b631f8052a4569b52124e Mon Sep 17 00:00:00 2001 From: adstep Date: Thu, 4 Jun 2026 20:17:27 -0700 Subject: [PATCH 2/2] feat(evaluation): play results preview music on pass or fail Drop the clear-only guard so the preview plays once on the results screen regardless of pass/fail. Update help text and rename the helper accordingly. --- assets/languages/en.ini | 2 +- src/screens/evaluation.rs | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/assets/languages/en.ini b/assets/languages/en.ini index 6c96199d8..3cdfa32e5 100644 --- a/assets/languages/en.ini +++ b/assets/languages/en.ini @@ -1504,7 +1504,7 @@ SfxVolumeHelp=Set the sound-effect volume before master volume is applied. AssistTickVolumeHelp=Set the gameplay Assist Tick volume before master volume is applied. MusicVolumeHelp=Set the music volume before master volume is applied. MineSoundsHelp=Play a sound when mines are hit. -EvalPreviewMusicHelp=Play the song's preview music once on the results screen after clearing a song (silent on fail). +EvalPreviewMusicHelp=Play the song's preview music once on the results screen, regardless of pass or fail. GlobalOffsetHelp=Apply a global audio timing offset in 1 ms steps. RateModPreservesPitchHelp=Keep pitch constant when rate mods are active. ReplayGainHelp=Experimental: normalize playback loudness across songs using ReplayGain 2.0 / EBU R 128. Loudness is computed in the background on first play and cached on disk; gain is applied to subsequent playback or live once analysis completes. diff --git a/src/screens/evaluation.rs b/src/screens/evaluation.rs index 7f8e4436f..0f35bd94a 100644 --- a/src/screens/evaluation.rs +++ b/src/screens/evaluation.rs @@ -2940,7 +2940,7 @@ pub fn update(state: &mut State, dt: f32) { state.screen_elapsed += dt; } - maybe_start_clear_preview_music(state); + maybe_start_preview_music(state); online::lobbies::poll_reconnect(); online::lobbies::update_machine_state_sides_with_stats( @@ -3218,7 +3218,7 @@ pub(crate) fn all_joined_players_failed(state: &State) -> bool { any } -fn maybe_start_clear_preview_music(state: &mut State) { +fn maybe_start_preview_music(state: &mut State) { if state.preview_music_started { return; } @@ -3227,9 +3227,6 @@ fn maybe_start_clear_preview_music(state: &mut State) { if !crate::config::get().eval_preview_music { return; } - if all_joined_players_failed(state) { - return; - } let Some(si) = state.score_info.iter().flatten().next() else { return; };