Skip to main content

launcher/pages/audio/
ui.rs

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/// True while the preview player still has queued/playing audio.
16#[cfg(target_os = "windows")]
17fn player_active(sink: Option<&Player>) -> bool {
18    !sink.is_none_or(rodio::Player::empty)
19}
20
21/// Non-Windows stub: the audio preview is compiled out (no rodio).
22#[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    // Handle background task
35    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; // Trigger re-scan
47                        }
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    // Initialize audio stream if needed (Windows-only; no audio device on
62    // other targets since rodio is gated out there)
63    #[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    // Perform one-time scan if needed
74    if !state.is_scanned {
75        let start = std::time::Instant::now();
76
77        // 1. Reconciliation (One-time expensive op)
78        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        // 2. Metadata and Status Scan
83        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            // Single pass: try opening the file.
92            let status = if let Ok(mut file) = File::open(&abs_path) {
93                // Read header if it exists
94                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                // Fallback check if it was moved/fixed
104                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        // Write performance log
119        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    // Display Sound Configuration Header
137    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            // Get the music directory path
183            let music_dir = game_path.join("data/music");
184
185            // Check if music directory exists
186            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            // Display music table
196            if music_configs.is_empty() {
197                ui.label("No music configurations found.");
198            } else {
199                // Create a table-like layout
200                egui::Grid::new("music_table").striped(true).show(ui, |ui| {
201                    // Header row
202                    ui.label(""); // Play/Pause
203                    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                    // Data rows
214                    for config in music_configs.iter_mut() {
215                        // Get filename only (without path)
216                        let filename = supreme_music::get_music_filename_only(config);
217
218                        // Get file status from cache
219                        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                        // Play/Pause Button
231                        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                                // Pause/Stop
238                                #[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                                // Play
246                                #[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                                    // Stop previous
255                                    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                                        // We need custom decoder
265                                        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                        // Filename (read-only)
289                        ui.label(&filename);
290
291                        // Info Button
292                        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                        // File status indicator with GUI-level styling
303                        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                        // Halfpipe checkbox
319                        if ui.checkbox(&mut config.halfpipe, "").changed() {
320                            needs_music_save = true;
321                        }
322
323                        // Tracks checkbox
324                        if ui.checkbox(&mut config.tracks, "").changed() {
325                            needs_music_save = true;
326                        }
327
328                        // Author text input
329                        if ui.text_edit_singleline(&mut config.author).lost_focus() {
330                            needs_music_save = true;
331                        }
332
333                        // Track name text input
334                        if ui.text_edit_singleline(&mut config.track).lost_focus() {
335                            needs_music_save = true;
336                        }
337
338                        // Volume slider
339                        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            // Import Settings
352            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}