1use crate::pages::audio;
2#[cfg(target_os = "windows")]
3use crate::pages::audio::tasks::AdpcmSource;
4use crate::pages::audio::types::{MusicPageState, SoundMode, TargetSampleRate};
5use crate::tasks::{TaskEvent, spawn_task};
6use eframe::egui;
7use midas_audio::types::Channels;
8#[cfg(target_os = "windows")]
9use rodio::Player;
10use std::fs::File;
11use std::io::Read;
12use std::path::Path;
13use supreme_config::models::SoundConfig;
14
15#[cfg(target_os = "windows")]
17fn player_active(sink: Option<&Player>) -> bool {
18 !sink.is_none_or(rodio::Player::empty)
19}
20
21#[cfg(not(target_os = "windows"))]
23fn player_active(_sink: Option<&crate::pages::audio::types::PlayerHandle>) -> bool {
24 false
25}
26
27pub fn music_page(
28 ui: &mut egui::Ui,
29 state: &mut MusicPageState,
30 sound_config: &mut SoundConfig,
31 music_configs: &mut Vec<supreme_music::MusicConfig>,
32 storage: &supreme_fs::GameStorage,
33) {
34 let mut task_finished = false;
36 if let Some(handle) = &state.task_handle {
37 while let Ok(event) = handle.try_recv() {
38 match event {
39 TaskEvent::SetStatus(status) => state.import_status = status,
40 TaskEvent::Finished(res) => {
41 task_finished = true;
42 match res {
43 Ok(new_config) => {
44 music_configs.push(new_config);
45 state.import_status = "Import successful".to_string();
46 state.is_scanned = false; }
48 Err(e) => {
49 state.import_status = format!("Error: {e}");
50 }
51 }
52 }
53 _ => {}
54 }
55 }
56 }
57 if task_finished {
58 state.task_handle = None;
59 }
60
61 #[cfg(target_os = "windows")]
64 if state._stream.is_none()
65 && let Ok(stream) = rodio::DeviceSinkBuilder::open_default_sink()
66 {
67 state._stream = Some(stream);
68 }
69
70 let game_path = storage.game_path();
71 let music_dir = game_path.join("data/music");
72
73 if !state.is_scanned {
75 let start = std::time::Instant::now();
76
77 let recon_start = std::time::Instant::now();
79 *music_configs = supreme_music::load_and_reconcile_music_configs(&music_dir, game_path);
80 let recon_ms = recon_start.elapsed().as_secs_f64() * 1000.0;
81
82 let scan_start = std::time::Instant::now();
84 state.status_cache.clear();
85 state.metadata_cache.clear();
86
87 let mut buffer = [0u8; 64];
88 for config in music_configs.iter() {
89 let abs_path = game_path.join(&config.filename);
90
91 let status = if let Ok(mut file) = File::open(&abs_path) {
93 if file.read_exact(&mut buffer).is_ok()
95 && let Ok(metadata) = midas_audio::get_metadata(&buffer)
96 {
97 state
98 .metadata_cache
99 .insert(config.filename.clone(), metadata);
100 }
101 supreme_music::MusicFileStatus::Ok
102 } else {
103 let filename = supreme_music::get_music_filename_only(config);
105 if music_dir.join(&filename).exists() {
106 supreme_music::MusicFileStatus::Fixed
107 } else {
108 supreme_music::MusicFileStatus::Missing
109 }
110 };
111 state.status_cache.insert(config.filename.clone(), status);
112 }
113 let scan_ms = scan_start.elapsed().as_secs_f64() * 1000.0;
114 let total_ms = start.elapsed().as_secs_f64() * 1000.0;
115
116 state.is_scanned = true;
117
118 let timestamp = std::time::SystemTime::now()
120 .duration_since(std::time::UNIX_EPOCH)
121 .unwrap_or_default()
122 .as_secs();
123
124 let perf_data = serde_json::json!({
125 "total_ms": total_ms,
126 "reconciliation_ms": recon_ms,
127 "metadata_scan_ms": scan_ms,
128 "file_count": music_configs.len(),
129 "timestamp": timestamp,
130 });
131
132 if let Err(e) = storage.write_report("music_scan_perf", &perf_data) {
133 eprintln!("Failed to write music_scan_perf report: {e}");
134 }
135 }
136 ui.horizontal(|ui| {
138 ui.heading("Audio Settings");
139 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
140 let current_mode = SoundMode::from_config(sound_config);
141 let mut mode_val = f64::from(current_mode as u32);
142
143 let response = ui.add(
144 egui::Slider::new(&mut mode_val, 0.0..=2.0)
145 .step_by(1.0)
146 .text("Sound and music")
147 .show_value(true)
148 .custom_formatter(|n, _| match n as u32 {
149 0 => "🔇 ".to_string(),
150 1 => "🔊 ".to_string(),
151 2 => "🔊🎵".to_string(),
152 _ => String::new(),
153 }),
154 );
155
156 if mode_val < 2.0 {
157 ui.colored_label(ui.visuals().warn_fg_color, egui::RichText::new("⚠").size(16.0))
158 .on_hover_text("Suggestion: Enable the music here, and mute it using the in-game setting if you need it off.");
159 }
160
161 if response.changed() {
162 let new_mode = match mode_val as u32 {
163 0 => SoundMode::Disabled,
164 1 => SoundMode::WithoutMusic,
165 _ => SoundMode::Full,
166 };
167 new_mode.apply_to_config(sound_config);
168 }
169 if response.drag_stopped()
170 && let Err(e) = sound_config.save(storage, Path::new("Data/Sound_Config.txt"))
171 {
172 eprintln!("Failed to save sound config: {e}");
173 }
174 });
175 });
176
177 ui.add_space(10.0);
178
179 egui::ScrollArea::vertical()
180 .auto_shrink([false, false])
181 .show(ui, |ui| {
182 let music_dir = game_path.join("data/music");
184
185 if !music_dir.exists() {
187 ui.colored_label(egui::Color32::RED, "Music directory not found!");
188 return;
189 }
190
191 let mut needs_music_save = false;
192
193 ui.add_space(10.0);
194
195 if music_configs.is_empty() {
197 ui.label("No music configurations found.");
198 } else {
199 egui::Grid::new("music_table").striped(true).show(ui, |ui| {
201 ui.label(""); ui.label("Filename");
204 ui.label("Info");
205 ui.label("Status");
206 ui.label("Halfpipe");
207 ui.label("Tracks");
208 ui.label("Band or artist name");
209 ui.label("Track name");
210 ui.label("Volume");
211 ui.end_row();
212
213 for config in music_configs.iter_mut() {
215 let filename = supreme_music::get_music_filename_only(config);
217
218 let status = state
220 .status_cache
221 .get(&config.filename)
222 .cloned()
223 .unwrap_or(supreme_music::MusicFileStatus::Missing);
224 let file_exists = matches!(
225 status,
226 supreme_music::MusicFileStatus::Ok
227 | supreme_music::MusicFileStatus::Fixed
228 );
229
230 let is_playing = state.current_playing_file.as_ref()
232 == Some(&config.filename)
233 && player_active(state.current_sink.as_ref());
234
235 if ui.button(if is_playing { "⏸" } else { "▶" }).clicked() {
236 if is_playing {
237 #[cfg(target_os = "windows")]
239 if let Some(sink) = &state.current_sink {
240 sink.stop();
241 }
242 state.current_sink = None;
243 state.current_playing_file = None;
244 } else {
245 #[cfg(not(target_os = "windows"))]
247 {
248 let _ = file_exists;
249 eprintln!("Audio preview is only available on Windows builds.");
250 }
251
252 #[cfg(target_os = "windows")]
253 if file_exists {
254 if let Some(sink) = &state.current_sink {
256 sink.stop();
257 }
258 state.current_sink = None;
259
260 if let Some(stream) = &state._stream {
261 let player = Player::connect_new(stream.mixer());
262 let abs_path = game_path.join(&config.filename);
263
264 match File::open(&abs_path) {
266 Ok(file) => match AdpcmSource::new(
267 file,
268 state.target_sample_rate.to_u32(None),
269 ) {
270 Ok(source) => {
271 player.append(source);
272 player.play();
273 state.current_sink = Some(player);
274 state.current_playing_file =
275 Some(config.filename.clone());
276 }
277 Err(e) => {
278 eprintln!("Failed to create ADPCM source: {e}")
279 }
280 },
281 Err(e) => eprintln!("Failed to open file: {e}"),
282 }
283 }
284 }
285 }
286 }
287
288 ui.label(&filename);
290
291 let info_response = ui.label("ℹ");
293 if let Some(metadata) = state.metadata_cache.get(&config.filename) {
294 info_response.on_hover_ui(|ui| {
295 ui.heading("Audio Metadata");
296 ui.label(format!("Channels: {}", metadata.channels));
297 ui.label(format!("Format: {:?}", metadata.sample_type));
298 ui.label(format!("Frame Length: {}", metadata.frame_length));
299 });
300 }
301
302 match status {
304 supreme_music::MusicFileStatus::Ok => {
305 ui.label("✅")
306 .on_hover_text("File exists at configured path");
307 }
308 supreme_music::MusicFileStatus::Fixed => {
309 ui.colored_label(egui::Color32::ORANGE, "⚠")
310 .on_hover_text("Path was automatically fixed - file exists");
311 }
312 supreme_music::MusicFileStatus::Missing => {
313 ui.colored_label(egui::Color32::RED, "❌")
314 .on_hover_text("File not found");
315 }
316 }
317
318 if ui.checkbox(&mut config.halfpipe, "").changed() {
320 needs_music_save = true;
321 }
322
323 if ui.checkbox(&mut config.tracks, "").changed() {
325 needs_music_save = true;
326 }
327
328 if ui.text_edit_singleline(&mut config.author).lost_focus() {
330 needs_music_save = true;
331 }
332
333 if ui.text_edit_singleline(&mut config.track).lost_focus() {
335 needs_music_save = true;
336 }
337
338 let res = ui.add(egui::Slider::new(&mut config.volume, 0.0..=1.0));
340 if res.drag_stopped() {
341 needs_music_save = true;
342 }
343
344 ui.end_row();
345 }
346 });
347 }
348
349 ui.add_space(5.0);
350
351 ui.group(|ui| {
353 ui.label("Import Settings");
354
355 ui.horizontal(|ui| {
356 ui.label("Channels:");
357 egui::ComboBox::from_id_salt("channels_combo")
358 .selected_text(match state.target_channels {
359 Channels::Mono => "Mono",
360 Channels::Stereo => "Stereo",
361 })
362 .show_ui(ui, |ui| {
363 ui.selectable_value(&mut state.target_channels, Channels::Mono, "Mono");
364 ui.selectable_value(
365 &mut state.target_channels,
366 Channels::Stereo,
367 "Stereo",
368 );
369 });
370
371 ui.add_space(10.0);
372
373 ui.label("Bit Depth:");
374 let target_bit_depth = &mut state.target_bit_depth;
375 use crate::pages::audio::types::TargetBitDepth;
376 egui::ComboBox::from_id_salt("bit_depth_combo")
377 .selected_text(match target_bit_depth {
378 TargetBitDepth::Adpcm => "ADPCM",
379 TargetBitDepth::EightBit => "8-bit PCM",
380 TargetBitDepth::SixteenBit => "16-bit PCM",
381 })
382 .show_ui(ui, |ui| {
383 ui.selectable_value(target_bit_depth, TargetBitDepth::Adpcm, "ADPCM");
384 ui.selectable_value(
385 target_bit_depth,
386 TargetBitDepth::EightBit,
387 "8-bit PCM",
388 );
389 ui.selectable_value(
390 target_bit_depth,
391 TargetBitDepth::SixteenBit,
392 "16-bit PCM",
393 );
394 });
395
396 ui.add_space(10.0);
397
398 ui.label("Sample Rate:");
399 egui::ComboBox::from_id_salt("sample_rate_combo")
400 .selected_text(match state.target_sample_rate {
401 TargetSampleRate::Original => "Original",
402 TargetSampleRate::Hz11025 => "11025 Hz",
403 TargetSampleRate::Hz22050 => "22050 Hz",
404 TargetSampleRate::Hz44100 => "44100 Hz",
405 })
406 .show_ui(ui, |ui| {
407 ui.selectable_value(
408 &mut state.target_sample_rate,
409 TargetSampleRate::Original,
410 "Original",
411 );
412 ui.selectable_value(
413 &mut state.target_sample_rate,
414 TargetSampleRate::Hz11025,
415 "11025 Hz",
416 );
417 ui.selectable_value(
418 &mut state.target_sample_rate,
419 TargetSampleRate::Hz22050,
420 "22050 Hz",
421 );
422 ui.selectable_value(
423 &mut state.target_sample_rate,
424 TargetSampleRate::Hz44100,
425 "44100 Hz",
426 );
427 });
428 });
429
430 if state.task_handle.is_some() {
431 ui.add(egui::Spinner::new());
432 ui.label(&state.import_status);
433 } else if ui
434 .button("Select Music File (WAV/MP3/Vorbis/FLAC) to add")
435 .clicked()
436 && let Some(file_path) = rfd::FileDialog::new()
437 .add_filter("WAV", &["wav"])
438 .add_filter("MP3", &["mp3"])
439 .add_filter("Vorbis", &["ogg"])
440 .add_filter("FLAC", &["flac"])
441 .pick_file()
442 {
443 let music_dir = music_dir.clone();
444 let target_channels = state.target_channels;
445 let target_sample_rate = state.target_sample_rate;
446 let target_bit_depth = state.target_bit_depth;
447 let storage = storage.clone();
448
449 state.task_handle = Some(spawn_task(ui.ctx().clone(), move |tx| {
450 let _ = tx.send(TaskEvent::SetStatus("Converting audio...".to_string()));
451
452 audio::tasks::import_music_file(
453 &file_path,
454 &music_dir,
455 &MusicPageState {
456 target_channels,
457 target_sample_rate,
458 target_bit_depth,
459 ..Default::default()
460 },
461 &storage,
462 )
463 }));
464 state.import_status = "Starting import...".to_string();
465 }
466 });
467
468 if needs_music_save
469 && let Err(e) = supreme_music::save_music_config(&music_dir, music_configs)
470 {
471 eprintln!("Failed to save music config: {e}");
472 }
473 });
474}