1use super::canvas::TerminalCanvas;
6use crate::{
7 AudioBlock, AudioProcessor, AudioSpec, BrickwallLimiter, Complex, Compressor, CompressorConfig,
8 DetectionMode, DitherMode, EqualizerProcessor, Fft, Gain, GainProcessor, LimiterProcessor,
9 Normalizer, ParametricEqualizer3Band, ProcessorChain, WavReader, WavSampleFormat,
10 WavWriteOptions, WavWriter, Window, WindowType,
11};
12use std::f32::consts::PI;
13use std::io::{stdin, stdout, Write};
14use std::path::Path;
15use std::thread::sleep;
16use std::time::{Duration, Instant};
17
18#[derive(Debug, Clone)]
23pub struct MenuItem {
24 pub key: char,
25 pub title: &'static str,
26 pub description: &'static str,
27}
28
29pub struct TuiMenu {
30 canvas: TerminalCanvas,
31 items: Vec<MenuItem>,
32}
33
34impl TuiMenu {
35 pub fn new() -> Self {
36 Self {
37 canvas: TerminalCanvas::new(100, 30),
38 items: vec![
39 MenuItem {
40 key: '1',
41 title: "🎵 Master / ReScale Audio File",
42 description:
43 "Headroom normalization, Dynamic Compressor, & Lookahead Brickwall Limiter",
44 },
45 MenuItem {
46 key: '2',
47 title: "📊 Realtime DSP & Spectrum Monitor",
48 description:
49 "Live 9-Band FFT Spectrogram, Stereo Level Meter, & Frequency Sweep DSP",
50 },
51 MenuItem {
52 key: '3',
53 title: "🔍 Inspect Audio File (WAV/MP3)",
54 description:
55 "RIFF metadata analysis, sample rate, bit depth, Peak dBFS, & RMS Energy",
56 },
57 MenuItem {
58 key: '4',
59 title: "🎛️ 3-Band Parametric EQ & Gain Tool",
60 description:
61 "Biquad IIR Filter (Low Shelf, Peaking Mid, High Shelf) & Gain Control",
62 },
63 MenuItem {
64 key: '5',
65 title: "❌ Quit (Exit)",
66 description: "Close the REScale Audio Engine",
67 },
68 ],
69 }
70 }
71
72 pub fn run(&self) {
74 loop {
75 self.draw_main_menu();
76 self.canvas.show_cursor();
79 print!("\n \x1b[1;33mSelect menu [1-5 / q]: \x1b[0m");
80 let _ = stdout().flush();
81
82 let mut input = String::new();
83 match stdin().read_line(&mut input) {
84 Ok(0) => break,
87 Ok(_) => {}
88 Err(_) => break,
89 }
90 let choice = input.trim().to_lowercase();
91
92 match choice.as_str() {
93 "1" => self.action_master_audio(),
94 "2" => self.action_realtime_monitor(),
95 "3" => self.action_inspect_audio(),
96 "4" => self.action_equalizer_tool(),
97 "5" | "q" | "exit" => {
98 println!(
99 "\n \x1b[1;36mThank you for using REScale DSP Audio Engine!\x1b[0m\n"
100 );
101 break;
102 }
103 _ => {
104 println!(" \x1b[1;31mInvalid choice. Press Enter to continue...\x1b[0m");
105 let _ = stdin().read_line(&mut String::new());
106 }
107 }
108 }
109
110 self.canvas.restore_cursor();
112 }
113
114 fn draw_main_menu(&self) {
119 self.canvas.clear_screen();
120
121 self.canvas
123 .draw_panel(1, 1, 98, 28, "⚡ RESCALE DSP ENGINE", Some("v0.1.0"));
124
125 self.canvas.move_to(3, 4);
127 print!(
128 "\x1b[90mARCHITECTURE\x1b[0m: Pure Rust Zero External Dependency │ \x1b[90mFORMAT\x1b[0m: f32 [-1..1] Planar Audio"
129 );
130
131 self.canvas.move_to(4, 4);
132 print!(
133 "\x1b[90mTEST SUITE\x1b[0m: 18 Unit Tests Passing │ \x1b[90mPERFORMANCE\x1b[0m: Full Chain < 0.2s (3.4min stereo)"
134 );
135
136 self.canvas.move_to(6, 4);
138 print!("\x1b[38;5;240m{}", "─".repeat(90));
139
140 let mut row = 8;
142 for item in &self.items {
143 let highlight_char = if item.key == '2' { '▶' } else { '▸' };
145 let color = if item.key == '2' {
146 "\x1b[1;36m"
147 } else {
148 "\x1b[1;33m"
149 };
150
151 self.canvas.move_to(row, 4);
152 print!("{color}[{highlight_char}]\x1b[0m ");
153 let item_color = if item.key == '5' || item.key == 'q' {
154 "\x1b[1;31m"
155 } else {
156 "\x1b[1;37m"
157 };
158 print!("{item_color}{:<38}", item.title);
159 print!(" \x1b[90m— {}\x1b[0m", item.description);
160 row += 2;
161 }
162
163 self.canvas.move_to(26, 4);
165 print!("\x1b[90mUse number keys [1 - 5] or 'q' to quit.\x1b[0m");
166 let _ = stdout().flush();
167 }
168
169 fn action_master_audio(&self) {
174 self.canvas.clear_screen();
175 self.canvas
176 .draw_panel(1, 1, 96, 28, " 🎵 AUDIO MASTERING PROCESSOR ", None);
177
178 print!("\n \x1b[1;37m[1] Enter audio file path (.wav / .mp3):\x1b[0m ");
180 let _ = stdout().flush();
181 let mut path_str = String::new();
182 if stdin().read_line(&mut path_str).is_err() {
183 return;
184 }
185 let input_path = path_str.trim().trim_matches('"').trim();
186 if input_path.is_empty() {
187 return;
188 }
189
190 let path = Path::new(input_path);
191 if !path.exists() {
192 println!(" \x1b[1;31m[ERROR] File not found: {}\x1b[0m", input_path);
193 self.wait_keypress();
194 return;
195 }
196
197 print!(" \x1b[1;37m[2] Target Output dBFS [-60.0 to 0.0, default: -1.0]:\x1b[0m ");
199 let _ = stdout().flush();
200 let mut target_str = String::new();
201 let _ = stdin().read_line(&mut target_str);
202 let target_dbfs: f32 = target_str.trim().parse().unwrap_or(-1.0);
203
204 print!(" \x1b[1;37m[3] Output Bit Depth [16 / 24 / 32f, default: 24]:\x1b[0m ");
206 let _ = stdout().flush();
207 let mut depth_str = String::new();
208 let _ = stdin().read_line(&mut depth_str);
209 let format = match depth_str.trim().to_lowercase().as_str() {
210 "16" => WavSampleFormat::Pcm16Bit,
211 "32" | "32f" => WavSampleFormat::Pcm32BitFloat,
212 _ => WavSampleFormat::Pcm24Bit,
213 };
214
215 let dither = if format == WavSampleFormat::Pcm32BitFloat {
216 DitherMode::None
217 } else {
218 DitherMode::Tpdf
219 };
220
221 println!("\n \x1b[1;32m[PROCESSING] Starting mastering pipeline...\x1b[0m");
223 self.canvas.draw_separator(18, 4, 80);
224
225 let start_time = Instant::now();
226
227 if let Err(err) = self.process_mastering_visual(path, target_dbfs, format, dither) {
228 println!(" \x1b[1;31m[FAILED] Processing failed: {err}\x1b[0m");
229 } else {
230 let elapsed = start_time.elapsed().as_secs_f32();
231 println!(
232 "\n \x1b[1;32m[DONE] Mastering complete in {:.2} seconds!\x1b[0m",
233 elapsed
234 );
235 }
236
237 self.wait_keypress();
238 }
239
240 fn process_mastering_visual(
241 &self,
242 input_path: &Path,
243 target_dbfs: f32,
244 output_format: WavSampleFormat,
245 dither: DitherMode,
246 ) -> Result<(), Box<dyn std::error::Error>> {
247 let (header, mut channels) = WavReader::read_from_file(input_path)?;
248 let sample_rate = header.sample_rate as f32;
249
250 let initial_peak = channels
252 .iter()
253 .map(|ch| Normalizer::find_peak(ch))
254 .fold(0.0_f32, f32::max);
255 let initial_rms = channels
256 .iter()
257 .map(|ch| Normalizer::calculate_rms(ch))
258 .fold(0.0_f32, f32::max);
259
260 println!(
261 " \x1b[90mANALYSIS INPUT: {} Hz | {} ch | Peak {:>6.2} dBFS | RMS {:>6.2} dBFS\x1b[0m",
262 header.sample_rate,
263 header.num_channels,
264 Gain::linear_to_db(initial_peak),
265 Gain::linear_to_db(initial_rms),
266 );
267
268 let target_linear = Gain::db_to_linear(target_dbfs);
269 Normalizer::normalize_channels_to_peak(&mut channels, target_linear);
270
271 let comp_config = CompressorConfig {
273 threshold_db: target_dbfs - 8.0,
274 ratio: 2.5,
275 knee_db: 3.0,
276 attack_ms: 8.0,
277 release_ms: 100.0,
278 makeup_gain_db: 0.8,
279 mode: DetectionMode::Rms,
280 };
281
282 let mut chain = ProcessorChain::new();
283 chain.push(crate::engine::CompressorProcessor::new(comp_config.clone()));
284 chain.push(LimiterProcessor::new(target_dbfs, 25.0, 2.0));
285 chain.prepare(AudioSpec::new(sample_rate, channels.len())?)?;
286
287 let mut channel_views: Vec<&mut [f32]> =
288 channels.iter_mut().map(Vec::as_mut_slice).collect();
289 let mut block = AudioBlock::new(&mut channel_views)?;
290 chain.process(&mut block)?;
291
292 let song_stem = input_path
294 .file_stem()
295 .and_then(|s| s.to_str())
296 .unwrap_or("audio");
297 let parent_dir = input_path.parent().unwrap_or_else(|| Path::new("."));
298 let output_path = parent_dir.join(format!("REScale-{}.wav", song_stem));
299
300 WavWriter::write_to_file_with_options(
301 &output_path,
302 &channels,
303 header.sample_rate,
304 WavWriteOptions::new(output_format, dither),
305 )?;
306
307 let final_peak = channels
308 .iter()
309 .map(|ch| Normalizer::find_peak(ch))
310 .fold(0.0_f32, f32::max);
311 let final_rms = channels
312 .iter()
313 .map(|ch| Normalizer::calculate_rms(ch))
314 .fold(0.0_f32, f32::max);
315
316 println!(
317 " \x1b[90mRESULT OUTPUT: Peak {:>6.2} dBFS | RMS {:>6.2} dBFS\x1b[0m",
318 Gain::linear_to_db(final_peak),
319 Gain::linear_to_db(final_rms),
320 );
321 println!(
322 " \x1b[1;32mSAVED TO: {}\x1b[0m",
323 output_path.display()
324 );
325 Ok(())
326 }
327
328 fn action_realtime_monitor(&self) {
333 let w = 100usize;
335 let h = 30usize;
336 let tui = TerminalCanvas::new(w, h);
337 tui.clear_screen();
338
339 let sample_rate = 44_100.0;
340 let total_frames = 300; let mut phase = 0.0_f32;
342
343 let mut eq = ParametricEqualizer3Band::new(sample_rate);
345 eq.set_low_band(200.0, 3.0);
346 eq.set_mid_band(1000.0, 1.2, -2.0);
347 eq.set_high_band(6000.0, 1.5);
348
349 let comp_config = CompressorConfig {
350 threshold_db: -14.0,
351 ratio: 4.0,
352 knee_db: 4.0,
353 attack_ms: 5.0,
354 release_ms: 80.0,
355 makeup_gain_db: 1.5,
356 mode: DetectionMode::Peak,
357 };
358 let mut compressor = Compressor::new(sample_rate, comp_config);
359 let mut limiter = BrickwallLimiter::new(sample_rate, -0.2, 20.0, 2.0);
360
361 let mut prev_in_peak_l = 0.0_f32;
363 let mut prev_in_peak_r = 0.0_f32;
364 let mut prev_out_peak_l = 0.0_f32;
365 let mut prev_out_peak_r = 0.0_f32;
366
367 for frame in 0..total_frames {
368 let sweep_freq = 180.0 + (frame as f32 * 0.08).sin().abs() * 3_200.0;
370 let block_size = 512;
371 let mut left = Vec::with_capacity(block_size);
372 let mut right = Vec::with_capacity(block_size);
373
374 for _ in 0..block_size {
375 let sample = (phase).sin() * 0.5;
376 phase += 2.0 * PI * sweep_freq / sample_rate;
377 if phase > 2.0 * PI {
378 phase -= 2.0 * PI;
379 }
380 left.push(sample);
381 right.push(sample * 0.85);
382 }
383
384 let in_peak_l = Normalizer::find_peak(&left);
386 let in_peak_r = Normalizer::find_peak(&right);
387 let _in_rms_l = Normalizer::calculate_rms(&left);
388 let _in_rms_r = Normalizer::calculate_rms(&right);
389
390 eq.process_buffer(&mut left);
392 compressor.process_buffer(&mut left);
393 limiter.process_buffer(&mut left);
394
395 eq.process_buffer(&mut right);
396 compressor.process_buffer(&mut right);
397 limiter.process_buffer(&mut right);
398
399 let out_peak_l = Normalizer::find_peak(&left);
401 let out_peak_r = Normalizer::find_peak(&right);
402 let _out_rms_l = Normalizer::calculate_rms(&left);
403 let _out_rms_r = Normalizer::calculate_rms(&right);
404
405 let mut fft_buf: Vec<Complex> = left.iter().map(|&s| Complex::new(s, 0.0)).collect();
407 let mut win = left.clone();
408 Window::apply(&mut win, WindowType::Hann);
409 for (c, &w) in fft_buf.iter_mut().zip(win.iter()) {
410 c.re = w;
411 }
412 Fft::forward(&mut fft_buf);
413
414 let spectrum_bands = [
415 (
416 fft_buf[1..3].iter().map(|c| c.magnitude()).sum::<f32>(),
417 "63",
418 ),
419 (
420 fft_buf[3..5].iter().map(|c| c.magnitude()).sum::<f32>(),
421 "125",
422 ),
423 (
424 fft_buf[5..9].iter().map(|c| c.magnitude()).sum::<f32>(),
425 "250",
426 ),
427 (
428 fft_buf[9..16].iter().map(|c| c.magnitude()).sum::<f32>(),
429 "500",
430 ),
431 (
432 fft_buf[16..31].iter().map(|c| c.magnitude()).sum::<f32>(),
433 "1k",
434 ),
435 (
436 fft_buf[31..61].iter().map(|c| c.magnitude()).sum::<f32>(),
437 "2k",
438 ),
439 (
440 fft_buf[61..121].iter().map(|c| c.magnitude()).sum::<f32>(),
441 "4k",
442 ),
443 (
444 fft_buf[121..221].iter().map(|c| c.magnitude()).sum::<f32>(),
445 "8k",
446 ),
447 (
448 fft_buf[221..255].iter().map(|c| c.magnitude()).sum::<f32>(),
449 "16k",
450 ),
451 ];
452
453 tui.clear_region(1, 1, w, h);
455
456 tui.draw_header(1, 2, "MONITOR — LIVE DSP ANALYZER", 44100, 2);
458
459 tui.draw_stat(3, 4, "MODE", "Dynamic Frequency Sweep");
461 tui.draw_stat(3, 36, "SWEEP FREQ", &format!("{:>6.1} Hz", sweep_freq));
462 tui.draw_stat(3, 72, "FRAME", &format!("{:03}/{total_frames}", frame + 1));
463
464 tui.draw_processing_status(5, 4, &["EQ", "COMPRESS", "LIMIT"]);
466
467 tui.draw_separator(6, 4, 90);
469
470 tui.draw_panel(7, 4, 42, 6, " ◉ INPUT SIGNAL", None);
472 tui.draw_vu_meter(
473 8,
474 6,
475 "L",
476 in_peak_l,
477 prev_in_peak_l,
478 Gain::linear_to_db(in_peak_l),
479 26,
480 );
481 tui.draw_vu_meter(
482 10,
483 6,
484 "R",
485 in_peak_r,
486 prev_in_peak_r,
487 Gain::linear_to_db(in_peak_r),
488 26,
489 );
490
491 tui.draw_panel(7, 50, 42, 6, " ◯ OUTPUT (POST MASTERING)", None);
493 tui.draw_vu_meter(
494 8,
495 52,
496 "L",
497 out_peak_l,
498 prev_out_peak_l,
499 Gain::linear_to_db(out_peak_l),
500 26,
501 );
502 tui.draw_vu_meter(
503 10,
504 52,
505 "R",
506 out_peak_r,
507 prev_out_peak_r,
508 Gain::linear_to_db(out_peak_r),
509 26,
510 );
511
512 prev_in_peak_l = in_peak_l.max(prev_in_peak_l);
514 prev_in_peak_r = in_peak_r.max(prev_in_peak_r);
515 prev_out_peak_l = out_peak_l.max(prev_out_peak_l);
516 prev_out_peak_r = out_peak_r.max(prev_out_peak_r);
517
518 if frame % 10 == 0 && (prev_in_peak_l > in_peak_l || prev_in_peak_r > in_peak_r) {
520 prev_in_peak_l *= 0.9;
521 prev_in_peak_r *= 0.9;
522 prev_out_peak_l *= 0.9;
523 prev_out_peak_r *= 0.9;
524 }
525
526 tui.draw_panel(
528 14,
529 4,
530 92,
531 12,
532 " ◆ SPECTRUM ANALYZER (FFT 512 / HANN WINDOW)",
533 None,
534 );
535 tui.draw_smooth_spectrum(16, 6, 8, &spectrum_bands);
536
537 tui.draw_separator(28, 4, 90);
539 tui.move_to(29, 4);
540 print!(
541 "\x1b[90mPlaying live simulation... Frame {:03}/{total_frames} | {} Hz Sweep | Block 512 samples\x1b[0m",
542 frame + 1,
543 sweep_freq
544 );
545
546 let _ = stdout().flush();
547 sleep(Duration::from_millis(30));
548 }
549
550 tui.show_cursor();
551 println!("\n\n \x1b[1;32mRealtime Monitor Complete!\x1b[0m");
552 self.wait_keypress();
553 }
554
555 fn action_inspect_audio(&self) {
560 self.canvas.clear_screen();
561 self.canvas
562 .draw_panel(1, 1, 94, 28, " 🔍 AUDIO FILE INSPECTOR ", None);
563
564 print!("\n \x1b[1;37mEnter audio file path (.wav):\x1b[0m ");
565 let _ = stdout().flush();
566 let mut path_str = String::new();
567 if stdin().read_line(&mut path_str).is_err() {
568 return;
569 }
570 let input_path = path_str.trim().trim_matches('"').trim();
571 if input_path.is_empty() {
572 return;
573 }
574
575 match WavReader::read_from_file(input_path) {
576 Ok((header, channels)) => {
577 let duration_sec =
578 header.num_samples_per_channel as f32 / header.sample_rate as f32;
579 println!("\n \x1b[1;32mHEADER INFORMATION:\x1b[0m");
580 println!(" ─────────────────────────────────────");
581 println!(
582 " Sample Rate : \x1b[1;37m{} Hz\x1b[0m",
583 header.sample_rate
584 );
585 println!(
586 " Channels : \x1b[1;37m{} ({})\x1b[0m",
587 header.num_channels,
588 if header.num_channels == 2 {
589 "Stereo"
590 } else {
591 "Mono"
592 }
593 );
594 println!(
595 " Bit Depth : \x1b[1;37m{}-bit ({:?})\x1b[0m",
596 header.bits_per_sample(),
597 header.format
598 );
599 println!(
600 " Duration : \x1b[1;37m{:.2} seconds\x1b[0m",
601 duration_sec
602 );
603
604 println!("\n \x1b[1;32mDYNAMICS ANALYSIS PER CHANNEL:\x1b[0m");
605 println!(" ─────────────────────────────────────");
606 for (i, ch) in channels.iter().enumerate() {
607 let peak = Normalizer::find_peak(ch);
608 let rms = Normalizer::calculate_rms(ch);
609 let peak_db = Gain::linear_to_db(peak);
610 let rms_db = Gain::linear_to_db(rms);
611 let clipping = if peak >= 1.0 {
612 "\x1b[1;31m[CLIPPING!]\x1b[0m"
613 } else {
614 "\x1b[1;32m[OK]\x1b[0m"
615 };
616 let level_bar_width = 30usize;
617 let filled = (peak * level_bar_width as f32).round() as usize;
618 let bar = format!(
619 "[{}{}]",
620 "\x1b[1;32m█\x1b[0m".repeat(filled),
621 "\x1b[90m░\x1b[0m".repeat(level_bar_width.saturating_sub(filled))
622 );
623
624 println!(
625 " Ch {}: Peak {:>7.2} dBFS | RMS {:>7.2} dBFS {} {}",
626 i + 1,
627 peak_db,
628 rms_db,
629 bar,
630 clipping
631 );
632 }
633 }
634 Err(e) => {
635 println!(" \x1b[1;31m[ERROR] Failed to read WAV: {}\x1b[0m", e);
636 }
637 }
638
639 self.wait_keypress();
640 }
641
642 fn action_equalizer_tool(&self) {
647 self.canvas.clear_screen();
648 self.canvas
649 .draw_panel(1, 1, 94, 28, " 🎛️ 3-BAND PARAMETRIC EQUALIZER ", None);
650
651 println!("\n \x1b[1;36m--- 3-Band Parametric EQ Settings ---\x1b[0m\n");
652
653 print!(" \x1b[1;37mEnter WAV file path:\x1b[0m ");
654 let _ = stdout().flush();
655 let mut path_str = String::new();
656 let _ = stdin().read_line(&mut path_str);
657 let input_path = path_str.trim().trim_matches('"').trim();
658 if input_path.is_empty() {
659 return;
660 }
661
662 print!(" Low Shelf Gain (dB) [-24..+24, default 0.0]: ");
663 let _ = stdout().flush();
664 let mut low_str = String::new();
665 let _ = stdin().read_line(&mut low_str);
666 let low_gain: f32 = low_str.trim().parse().unwrap_or(0.0);
667
668 print!(" Mid Peak Gain (dB) [-24..+24, default 0.0]: ");
669 let _ = stdout().flush();
670 let mut mid_str = String::new();
671 let _ = stdin().read_line(&mut mid_str);
672 let mid_gain: f32 = mid_str.trim().parse().unwrap_or(0.0);
673
674 print!(" High Shelf Gain (dB) [-24..+24, default 0.0]: ");
675 let _ = stdout().flush();
676 let mut high_str = String::new();
677 let _ = stdin().read_line(&mut high_str);
678 let high_gain: f32 = high_str.trim().parse().unwrap_or(0.0);
679
680 print!(" Overall Gain (dB) [-24..+24, default 0.0]: ");
681 let _ = stdout().flush();
682 let mut master_str = String::new();
683 let _ = stdin().read_line(&mut master_str);
684 let master_gain: f32 = master_str.trim().parse().unwrap_or(0.0);
685
686 println!("\n \x1b[1;36m--- Preview EQ Settings ---\x1b[0m");
688 println!(
689 " LOW SHELF (150 Hz): \x1b[1;33m{:>+5.1} dB\x1b[0m",
690 low_gain
691 );
692 println!(
693 " MID PEAK (1000 Hz): \x1b[1;33m{:>+5.1} dB\x1b[0m",
694 mid_gain
695 );
696 println!(
697 " HIGH SHELF (6000 Hz): \x1b[1;33m{:>+5.1} dB\x1b[0m",
698 high_gain
699 );
700 println!(
701 " MASTER GAIN: \x1b[1;33m{:>+5.1} dB\x1b[0m",
702 master_gain
703 );
704
705 match WavReader::read_from_file(input_path) {
706 Ok((header, mut channels)) => {
707 let sample_rate = header.sample_rate as f32;
708 let mut chain = ProcessorChain::new();
709 chain.push(EqualizerProcessor::new(
710 150.0, low_gain, 1000.0, 1.0, mid_gain, 6000.0, high_gain,
711 ));
712 if master_gain.abs() > 1e-4 {
713 chain.push(GainProcessor::new(master_gain));
714 }
715
716 if let Ok(spec) = AudioSpec::new(sample_rate, channels.len()) {
717 let _ = chain.prepare(spec);
718 let mut views: Vec<&mut [f32]> =
719 channels.iter_mut().map(Vec::as_mut_slice).collect();
720 if let Ok(mut block) = AudioBlock::new(&mut views) {
721 let _ = chain.process(&mut block);
722 }
723 }
724
725 let path_obj = Path::new(input_path);
726 let song_stem = path_obj
727 .file_stem()
728 .and_then(|s| s.to_str())
729 .unwrap_or("audio");
730 let parent = path_obj.parent().unwrap_or_else(|| Path::new("."));
731 let out_path = parent.join(format!("REScale-EQ-{}.wav", song_stem));
732
733 if WavWriter::write_to_file(&out_path, &channels, header.sample_rate, header.format)
734 .is_ok()
735 {
736 println!(
737 "\n \x1b[1;32m[SUCCESS] EQ saved to: {}\x1b[0m",
738 out_path.display()
739 );
740 } else {
741 println!("\n \x1b[1;31m[ERROR] Failed to save EQ result file.\x1b[0m");
742 }
743 }
744 Err(e) => {
745 println!(" \x1b[1;31m[ERROR] Failed to read WAV: {}\x1b[0m", e);
746 }
747 }
748
749 self.wait_keypress();
750 }
751
752 fn wait_keypress(&self) {
757 print!("\n \x1b[90mPress Enter to return to Main Menu...\x1b[0m");
758 let _ = stdout().flush();
759 let _ = stdin().read_line(&mut String::new());
760 }
761}
762
763impl Default for TuiMenu {
764 fn default() -> Self {
765 Self::new()
766 }
767}