Skip to content

Commit e0e81a0

Browse files
committed
feat: add microphone input support with automatic device reconnection and improved error handling for audio streams.
1 parent 90227ac commit e0e81a0

5 files changed

Lines changed: 111 additions & 28 deletions

File tree

src/audio.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2019,6 +2019,7 @@ where
20192019
let mut last_current_row_string = String::new();
20202020
let mut was_paused = false;
20212021

2022+
let shared_state_err = shared_state.clone();
20222023
let stream = device.build_output_stream(
20232024
config,
20242025
move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
@@ -2135,7 +2136,12 @@ where
21352136

21362137
let _ = tx.try_send(msg);
21372138
},
2138-
|err| eprintln!("an error occurred on stream: {}", err),
2139+
move |err| {
2140+
eprintln!("an error occurred on stream: {}", err);
2141+
if let Ok(mut state) = shared_state_err.try_lock() {
2142+
state.audio_device_lost = true;
2143+
}
2144+
},
21392145
None,
21402146
)?;
21412147

@@ -2165,6 +2171,7 @@ where
21652171

21662172
let mut was_paused = false;
21672173

2174+
let shared_state_err = shared_state.clone();
21682175
let stream = device.build_input_stream(
21692176
config,
21702177
move |data: &[T], _: &cpal::InputCallbackInfo| {
@@ -2227,7 +2234,12 @@ where
22272234

22282235
let _ = tx.try_send(msg);
22292236
},
2230-
|err| eprintln!("an error occurred on input stream: {}", err),
2237+
move |err| {
2238+
eprintln!("an error occurred on input stream: {}", err);
2239+
if let Ok(mut state) = shared_state_err.try_lock() {
2240+
state.audio_device_lost = true;
2241+
}
2242+
},
22312243
None,
22322244
)?;
22332245

src/bitstream.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ pub fn start_bitstream_thread(
4343
let pw_rate = if codec_name == "truehd" || codec_name == "dts" { 192000 } else { 48000 };
4444
println!("[bitstream] Codec: {}, Decoder Rate: {}, Output Rate: {}", codec_name, decoder_sample_rate, pw_rate);
4545

46-
let pipe_path = format!("/tmp/rusttracker_bitstream_{}", std::process::id());
46+
let pipe_path = std::env::temp_dir()
47+
.join(format!("rusttracker_bitstream_{}", std::process::id()))
48+
.to_string_lossy()
49+
.into_owned();
4750
let _ = std::fs::remove_file(&pipe_path);
4851

4952
let mkfifo_out = std::process::Command::new("mkfifo")

src/engine.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,8 +412,7 @@ impl<'a> VulkanEngine<'a> {
412412
).await.unwrap();
413413

414414
device.on_uncaptured_error(std::sync::Arc::new(|e: wgpu::Error| {
415-
println!("WGPU VALIDATION ERROR: {:?}", e);
416-
std::process::exit(1);
415+
eprintln!("WGPU VALIDATION ERROR: {:?}", e);
417416
}));
418417

419418
let surface_caps = surface.get_capabilities(&adapter);

src/main.rs

Lines changed: 89 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -189,18 +189,18 @@ fn main() -> Result<(), Box<dyn Error>> {
189189
}));
190190

191191
let mut tui = Tui::new()?;
192-
if let Err(err) = run_tui(&mut tui.terminal, app_state, initial_stream) {
192+
if let Err(err) = run_tui(&mut tui.terminal, app_state, initial_stream, args.mic) {
193193
eprintln!("App error: {:?}", err);
194194
}
195195
} else {
196-
pollster::block_on(run_gui(app_state, initial_stream, args.fullscreen, args.gpu_fft, args.bench));
196+
pollster::block_on(run_gui(app_state, initial_stream, args.fullscreen, args.gpu_fft, args.bench, args.mic));
197197
}
198198

199199
Ok(())
200200
}
201201

202202
#[allow(unused_variables, unused_assignments)]
203-
async fn run_gui(app_state: Arc<Mutex<AppState>>, mut active_stream: Option<audio::PlaybackHandle>, is_fullscreen: bool, use_gpu_fft: bool, bench: Option<u32>) {
203+
async fn run_gui(app_state: Arc<Mutex<AppState>>, mut active_stream: Option<audio::PlaybackHandle>, is_fullscreen: bool, use_gpu_fft: bool, bench: Option<u32>, is_mic_launch: bool) {
204204
if use_gpu_fft {
205205
let mut state = app_state.lock().unwrap();
206206
state.gpu_fft = true;
@@ -566,6 +566,23 @@ async fn run_gui(app_state: Arc<Mutex<AppState>>, mut active_stream: Option<audi
566566
let load_path = {
567567
let mut state = app_state.lock().unwrap();
568568

569+
if state.audio_device_lost {
570+
state.audio_device_lost = false;
571+
let reload_path = if is_mic_launch {
572+
"".to_string()
573+
} else if state.playlist_index < state.playlist.len() {
574+
state.playlist[state.playlist_index].clone()
575+
} else {
576+
state.song_title.clone()
577+
};
578+
579+
if !reload_path.is_empty() || is_mic_launch {
580+
state.load_request = Some(reload_path);
581+
state.osd_text = Some("Audio Device Reconnected".to_string());
582+
state.osd_timer = 3.0;
583+
}
584+
}
585+
569586
if state.track_ended {
570587
state.track_ended = false;
571588
if state.load_request.is_none() {
@@ -580,8 +597,9 @@ async fn run_gui(app_state: Arc<Mutex<AppState>>, mut active_stream: Option<audi
580597
};
581598

582599
if let Some(path) = load_path {
600+
let is_mic = is_mic_launch && path.is_empty();
583601
// Check if the path exists first!
584-
let path_exists = if path.starts_with("http") {
602+
let path_exists = if path.starts_with("http") || is_mic {
585603
true
586604
} else {
587605
std::path::Path::new(&path).exists()
@@ -621,31 +639,44 @@ async fn run_gui(app_state: Arc<Mutex<AppState>>, mut active_stream: Option<audi
621639

622640
// We rely entirely on DSP thread messages to update tracker string state
623641
let mut loaded_stream = None;
642+
let mut last_err = None;
624643
for _ in 0..5 {
625-
if let Ok(stream) = audio::start_audio_thread(&path, false, Arc::clone(&app_state)) {
626-
loaded_stream = Some(stream);
627-
break;
644+
match audio::start_audio_thread(&path, is_mic, Arc::clone(&app_state)) {
645+
Ok(stream) => {
646+
loaded_stream = Some(stream);
647+
break;
648+
}
649+
Err(e) => {
650+
last_err = Some(e);
651+
}
628652
}
629653
std::thread::sleep(std::time::Duration::from_millis(50));
630654
}
631655

632656
if let Some(stream) = loaded_stream {
633657
let mut state = app_state.lock().unwrap();
634658
state.file_loaded = true;
635-
state.song_title = path.clone();
659+
state.song_title = if is_mic { "Microphone Input".to_string() } else { path.clone() };
636660
state.track_ended = false;
637661
active_stream = Some(stream);
638662
} else {
639663
let mut state = app_state.lock().unwrap();
640-
state.osd_text = Some(format!("Load Failed: {}", std::path::Path::new(&path).file_name().unwrap_or_default().to_string_lossy()));
641-
state.osd_timer = 3.0;
664+
let err_msg = last_err.map(|e| format!("{:?}", e)).unwrap_or_else(|| "Unknown error".to_string());
665+
let file_name = if is_mic { "Microphone".to_string() } else { std::path::Path::new(&path).file_name().unwrap_or_default().to_string_lossy().into_owned() };
666+
state.osd_text = Some(format!("Load Failed: {}\n{}", file_name, err_msg));
667+
state.osd_timer = 5.0;
642668

643-
state.playlist_index += 1;
644-
if state.playlist_index < state.playlist.len() {
645-
state.load_request = Some(state.playlist[state.playlist_index].clone());
669+
if !is_mic {
670+
state.playlist_index += 1;
671+
if state.playlist_index < state.playlist.len() {
672+
state.load_request = Some(state.playlist[state.playlist_index].clone());
673+
} else {
674+
state.file_loaded = false;
675+
state.artist = "Load Failed".to_string();
676+
}
646677
} else {
647678
state.file_loaded = false;
648-
state.artist = "Load Failed".to_string();
679+
state.artist = "Mic Failed".to_string();
649680
}
650681
}
651682
}
@@ -1374,6 +1405,7 @@ fn run_tui<B: Backend>(
13741405
terminal: &mut Terminal<B>,
13751406
app_state: Arc<Mutex<AppState>>,
13761407
mut active_stream: Option<audio::PlaybackHandle>,
1408+
is_mic_launch: bool,
13771409
) -> io::Result<()>
13781410
where std::io::Error: From<<B as Backend>::Error>
13791411
{
@@ -1390,6 +1422,23 @@ where std::io::Error: From<<B as Backend>::Error>
13901422
let load_path = {
13911423
let mut state = app_state.lock().unwrap();
13921424

1425+
if state.audio_device_lost {
1426+
state.audio_device_lost = false;
1427+
let reload_path = if is_mic_launch {
1428+
"".to_string()
1429+
} else if state.playlist_index < state.playlist.len() {
1430+
state.playlist[state.playlist_index].clone()
1431+
} else {
1432+
state.song_title.clone()
1433+
};
1434+
1435+
if !reload_path.is_empty() || is_mic_launch {
1436+
state.load_request = Some(reload_path);
1437+
state.osd_text = Some("Audio Device Reconnected".to_string());
1438+
state.osd_timer = 3.0;
1439+
}
1440+
}
1441+
13931442
if state.track_ended {
13941443
state.track_ended = false;
13951444
if state.load_request.is_none() {
@@ -1404,8 +1453,9 @@ where std::io::Error: From<<B as Backend>::Error>
14041453
};
14051454

14061455
if let Some(path) = load_path {
1456+
let is_mic = is_mic_launch && path.is_empty();
14071457
// Check if the path exists first!
1408-
let path_exists = if path.starts_with("http") {
1458+
let path_exists = if path.starts_with("http") || is_mic {
14091459
true
14101460
} else {
14111461
std::path::Path::new(&path).exists()
@@ -1432,28 +1482,44 @@ where std::io::Error: From<<B as Backend>::Error>
14321482
active_stream = None; // DROP OLD STREAM FIRST
14331483

14341484
let mut loaded_stream = None;
1485+
let mut last_err = None;
14351486
for _ in 0..5 {
1436-
if let Ok(stream) = audio::start_audio_thread(&path, false, Arc::clone(&app_state)) {
1437-
loaded_stream = Some(stream);
1438-
break;
1487+
match audio::start_audio_thread(&path, is_mic, Arc::clone(&app_state)) {
1488+
Ok(stream) => {
1489+
loaded_stream = Some(stream);
1490+
break;
1491+
}
1492+
Err(e) => {
1493+
last_err = Some(e);
1494+
}
14391495
}
14401496
std::thread::sleep(std::time::Duration::from_millis(50));
14411497
}
14421498

14431499
if let Some(stream) = loaded_stream {
14441500
let mut state = app_state.lock().unwrap();
14451501
state.file_loaded = true;
1446-
state.song_title = path.clone();
1502+
state.song_title = if is_mic { "Microphone Input".to_string() } else { path.clone() };
14471503
state.track_ended = false;
14481504
active_stream = Some(stream);
14491505
} else {
14501506
let mut state = app_state.lock().unwrap();
1451-
state.playlist_index += 1;
1452-
if state.playlist_index < state.playlist.len() {
1453-
state.load_request = Some(state.playlist[state.playlist_index].clone());
1507+
let err_msg = last_err.map(|e| format!("{:?}", e)).unwrap_or_else(|| "Unknown error".to_string());
1508+
let file_name = if is_mic { "Microphone".to_string() } else { std::path::Path::new(&path).file_name().unwrap_or_default().to_string_lossy().into_owned() };
1509+
state.osd_text = Some(format!("Load Failed: {}\n{}", file_name, err_msg));
1510+
state.osd_timer = 5.0;
1511+
1512+
if !is_mic {
1513+
state.playlist_index += 1;
1514+
if state.playlist_index < state.playlist.len() {
1515+
state.load_request = Some(state.playlist[state.playlist_index].clone());
1516+
} else {
1517+
state.file_loaded = false;
1518+
state.artist = format!("Load Failed: {}", err_msg);
1519+
}
14541520
} else {
14551521
state.file_loaded = false;
1456-
state.artist = "Load Failed".to_string();
1522+
state.artist = format!("Mic Failed: {}", err_msg);
14571523
}
14581524
}
14591525
}

src/state.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ pub struct AppState {
192192
pub osd_text: Option<String>,
193193
pub osd_timer: f32,
194194
pub cumulative_scrub: f64,
195+
pub audio_device_lost: bool,
195196
}
196197

197198
pub fn get_history_file_path() -> std::path::PathBuf {
@@ -321,6 +322,7 @@ impl AppState {
321322
osd_text: None,
322323
osd_timer: 0.0,
323324
cumulative_scrub: 0.0,
325+
audio_device_lost: false,
324326
}
325327
}
326328

@@ -409,6 +411,7 @@ impl AppState {
409411
osd_text: self.osd_text.clone(),
410412
osd_timer: self.osd_timer,
411413
cumulative_scrub: self.cumulative_scrub,
414+
audio_device_lost: self.audio_device_lost,
412415
}
413416
}
414417
}

0 commit comments

Comments
 (0)