refactor(player): improve audio stop handling and clean up test code

- Clear buffer and pause audio before stopping ASIO stream to prevent noise
- Remove test functions from
- Move stream URL generation responsibility to player
This commit is contained in:
Mercurio 2025-06-21 12:36:20 +02:00
parent 9012235efa
commit df1df2a294
4 changed files with 58 additions and 148 deletions

View file

@ -9,7 +9,7 @@ use symphonia::core::formats::{FormatOptions, FormatReader};
use symphonia::core::io::MediaSourceStream; use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions; use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint; use symphonia::core::probe::Hint;
use tracing::{info, debug, error, warn}; use tracing::{//info!, debug, error, warn};
pub struct AsioPlayer { pub struct AsioPlayer {
device: Device, device: Device,
@ -35,11 +35,11 @@ struct StreamBuffer {
impl AsioPlayer { impl AsioPlayer {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
let host = cpal::host_from_id(cpal::HostId::Asio)?; let host = cpal::host_from_id(cpal::HostId::Asio)?;
let device = host.default_output_device() let mut devices = host.output_devices()?;
let device = devices.next()
.ok_or_else(|| anyhow::anyhow!("No ASIO output device found"))?; .ok_or_else(|| anyhow::anyhow!("No ASIO output device found"))?;
//info!!("Using ASIO device: {}", device.name()?);
info!("Using ASIO device: {}", device.name()?);
Ok(Self { Ok(Self {
device, device,
stream: None, stream: None,
@ -75,17 +75,17 @@ impl AsioPlayer {
} }
self.forced_sample_rate = Some(sample_rate); self.forced_sample_rate = Some(sample_rate);
info!("Forced sample rate set to {} Hz", sample_rate); //info!!("Forced sample rate set to {} Hz", sample_rate);
Ok(()) Ok(())
} }
pub fn clear_sample_rate(&mut self) { pub fn clear_sample_rate(&mut self) {
self.forced_sample_rate = None; self.forced_sample_rate = None;
info!("Sample rate forcing cleared - will use automatic selection"); ////info!!("Sample rate forcing cleared - will use automatic selection");
} }
pub async fn play_url(&mut self, url: &str) -> Result<()> { pub async fn play_url(&mut self, url: &str) -> Result<()> {
info!("Starting playback of: {}", url); ////info!!("Starting playback of: {}", url);
// Stop any existing playback // Stop any existing playback
self.stop()?; self.stop()?;
@ -104,7 +104,7 @@ impl AsioPlayer {
stream.play()?; stream.play()?;
self.start_time = Some(Instant::now()); self.start_time = Some(Instant::now());
self.paused_position = None; self.paused_position = None;
info!("Playback started"); //info!!("Playback started");
} }
Ok(()) Ok(())
@ -127,7 +127,7 @@ impl AsioPlayer {
buffer.paused = true; buffer.paused = true;
} }
info!("Playback paused"); //info!!("Playback paused");
Ok(()) Ok(())
} else { } else {
Err(anyhow::anyhow!("No active stream to pause")) Err(anyhow::anyhow!("No active stream to pause"))
@ -150,7 +150,7 @@ impl AsioPlayer {
} }
stream.play()?; stream.play()?;
info!("Playback resumed"); //info!!("Playback resumed");
Ok(()) Ok(())
} else { } else {
Err(anyhow::anyhow!("No active stream to resume")) Err(anyhow::anyhow!("No active stream to resume"))
@ -159,15 +159,26 @@ impl AsioPlayer {
pub fn stop(&mut self) -> Result<()> { pub fn stop(&mut self) -> Result<()> {
if let Some(stream) = &self.stream { if let Some(stream) = &self.stream {
// First, clear the buffer to prevent noise
if let Some(buffer) = &self.stream_buffer {
let mut buffer = buffer.lock().unwrap();
buffer.paused = true; // Stop audio immediately
buffer.position = 0; // Reset position
buffer.samples.clear(); // Clear the buffer completely
}
std::thread::sleep(std::time::Duration::from_millis(50));
stream.pause()?; stream.pause()?;
self.stream = None; self.stream = None;
self.stream_buffer = None; self.stream_buffer = None;
self.start_time = None; self.start_time = None;
self.paused_position = None; self.paused_position = None;
info!("Playback stopped");
////info!!("Playback stopped and buffer cleared");
Ok(()) Ok(())
} else { } else {
// Not an error if already stopped
Ok(()) Ok(())
} }
} }
@ -195,7 +206,7 @@ impl AsioPlayer {
} }
} }
info!("Seeked to position: {:.1}%", position * 100.0); ////info!!("Seeked to position: {:.1}%", position * 100.0);
Ok(()) Ok(())
} else { } else {
Err(anyhow::anyhow!("No active stream to seek")) Err(anyhow::anyhow!("No active stream to seek"))
@ -209,7 +220,7 @@ impl AsioPlayer {
if let Some(buffer) = &self.stream_buffer { if let Some(buffer) = &self.stream_buffer {
let mut buffer = buffer.lock().unwrap(); let mut buffer = buffer.lock().unwrap();
buffer.volume = volume; buffer.volume = volume;
info!("Volume set to: {:.0}%", volume * 100.0); ////info!!("Volume set to: {:.0}%", volume * 100.0);
} }
Ok(()) Ok(())
@ -265,7 +276,7 @@ impl AsioPlayer {
let mut decoder = symphonia::default::get_codecs() let mut decoder = symphonia::default::get_codecs()
.make(&track.codec_params, &decoder_opts)?; .make(&track.codec_params, &decoder_opts)?;
info!("Decoding: {} channels, {} Hz", channels, sample_rate); ////info!!!("Decoding: {} channels, {} Hz", channels, sample_rate);
let mut samples = Vec::new(); let mut samples = Vec::new();
@ -355,7 +366,7 @@ impl AsioPlayer {
let num_frames = samples.len() / channels as usize; let num_frames = samples.len() / channels as usize;
let duration_ms = (num_frames as f64 / sample_rate as f64 * 1000.0) as u64; let duration_ms = (num_frames as f64 / sample_rate as f64 * 1000.0) as u64;
info!("Decoded {} samples, duration: {}ms", samples.len(), duration_ms); //info!!("Decoded {} samples, duration: {}ms", samples.len(), duration_ms);
Ok((samples, sample_rate, channels, duration_ms)) Ok((samples, sample_rate, channels, duration_ms))
} }
@ -375,7 +386,7 @@ impl AsioPlayer {
config.channels() == source_channels { config.channels() == source_channels {
selected_config = Some(config.with_sample_rate(SampleRate(forced_rate))); selected_config = Some(config.with_sample_rate(SampleRate(forced_rate)));
target_sample_rate = forced_rate; target_sample_rate = forced_rate;
info!("Using forced sample rate: {} Hz", forced_rate); //info!!("Using forced sample rate: {} Hz", forced_rate);
break; break;
} }
} }
@ -435,8 +446,8 @@ impl AsioPlayer {
self.device.default_output_config()? self.device.default_output_config()?
}; };
info!("Selected config: {} channels, {} Hz, format: {:?}", //info!!("Selected config: {} channels, {} Hz, format: {:?}",
config.channels(), config.sample_rate().0, config.sample_format()); // config.channels(), config.sample_rate().0, config.sample_format());
// Resample if necessary // Resample if necessary
let final_samples = if target_sample_rate != source_sample_rate { let final_samples = if target_sample_rate != source_sample_rate {
@ -510,7 +521,7 @@ impl AsioPlayer {
fn build_f32_stream(&mut self, config: StreamConfig, audio_buffer: Arc<Mutex<StreamBuffer>>) -> Result<()> { fn build_f32_stream(&mut self, config: StreamConfig, audio_buffer: Arc<Mutex<StreamBuffer>>) -> Result<()> {
let stream = self.device.build_output_stream( let stream = self.device.build_output_stream(
&config, &config,
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { move |data: &mut [f32], _: &cpal::OutputCallback//info!| {
let mut buffer = audio_buffer.lock().unwrap(); let mut buffer = audio_buffer.lock().unwrap();
// If paused, just output silence // If paused, just output silence
@ -555,7 +566,7 @@ impl AsioPlayer {
fn build_i32_stream(&mut self, config: StreamConfig, audio_buffer: Arc<Mutex<StreamBuffer>>) -> Result<()> { fn build_i32_stream(&mut self, config: StreamConfig, audio_buffer: Arc<Mutex<StreamBuffer>>) -> Result<()> {
let stream = self.device.build_output_stream( let stream = self.device.build_output_stream(
&config, &config,
move |data: &mut [i32], _: &cpal::OutputCallbackInfo| { move |data: &mut [i32], _: &cpal::OutputCallback//info!| {
let mut buffer = audio_buffer.lock().unwrap(); let mut buffer = audio_buffer.lock().unwrap();
// If paused, just output silence // If paused, just output silence
@ -590,7 +601,7 @@ impl AsioPlayer {
fn build_i16_stream(&mut self, config: StreamConfig, audio_buffer: Arc<Mutex<StreamBuffer>>) -> Result<()> { fn build_i16_stream(&mut self, config: StreamConfig, audio_buffer: Arc<Mutex<StreamBuffer>>) -> Result<()> {
let stream = self.device.build_output_stream( let stream = self.device.build_output_stream(
&config, &config,
move |data: &mut [i16], _: &cpal::OutputCallbackInfo| { move |data: &mut [i16], _: &cpal::OutputCallback//info!| {
let mut buffer = audio_buffer.lock().unwrap(); let mut buffer = audio_buffer.lock().unwrap();
// If paused, just output silence // If paused, just output silence

View file

@ -130,15 +130,15 @@ impl JellyfinClient {
pub fn get_stream_url(&self, item_id: &str) -> Result<String> { pub fn get_stream_url(&self, item_id: &str) -> Result<String> {
let user_id = self.config.user_id.as_ref().ok_or_else(|| anyhow::anyhow!("Not authenticated"))?; let user_id = self.config.user_id.as_ref().ok_or_else(|| anyhow::anyhow!("Not authenticated"))?;
let token = self.config.access_token.as_ref().ok_or_else(|| anyhow::anyhow!("Not authenticated"))?; let token = self.config.access_token.as_ref().ok_or_else(|| anyhow::anyhow!("Not authenticated"))?;
let url = format!(
"{}/Audio/{}/stream?UserId={}&api_key={}&Static=true",
self.config.server_url, item_id, user_id, token
);
let url = format!( Ok(url)
"{}/Audio/{}/stream?UserId={}&api_key={}&Static=true", }
self.config.server_url, item_id, user_id, token
);
Ok(url)
}
pub fn is_authenticated(&self) -> bool { pub fn is_authenticated(&self) -> bool {
self.config.is_authenticated() self.config.is_authenticated()

View file

@ -51,110 +51,10 @@ async fn main() -> Result<()> {
info!("Using saved session for {}", config.username.unwrap_or_else(|| "Unknown user".to_string())); info!("Using saved session for {}", config.username.unwrap_or_else(|| "Unknown user".to_string()));
} }
// List ASIO devices
list_asio_devices()?;
// Create player
let player = player::Player::new(jellyfin.clone())?; let player = player::Player::new(jellyfin.clone())?;
// Create and run TUI
let mut tui = tui::Tui::new(jellyfin, player)?; let mut tui = tui::Tui::new(jellyfin, player)?;
tui.run().await?; tui.run().await?;
Ok(()) Ok(())
} }
async fn test_jellyfin_connection() -> Result<()> {
info!("Testing Jellyfin connection...");
let config = config::Config {
server_url: "https://jfin.mercurio.moe".to_string(),
..config::Config::default()
};
let mut client = jellyfin::JellyfinClient::new(config);
println!("Enter your Jellyfin username:");
let mut username = String::new();
std::io::stdin().read_line(&mut username)?;
username = username.trim().to_string();
println!("Enter your Jellyfin password:");
let password = rpassword::read_password()?;
client.authenticate(&username, &password).await?;
let items = client.get_music_library().await?;
info!("Your music library (first few items):");
for item in items.iter().take(10) {
let artist = item.artists.as_ref()
.and_then(|a| a.first())
.map(|s| s.as_str())
.unwrap_or("Unknown Artist");
let album = item.album.as_ref()
.map(|s| s.as_str())
.unwrap_or("Unknown Album");
info!(" {} - {} - {}", artist, album, item.name);
if items.iter().position(|i| i.id == item.id) == Some(0) {
let stream_url = client.get_stream_url(&item.id)?;
info!(" Stream URL: {}", stream_url);
println!("\nDo you want to play this track? (y/n):");
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if input.trim().to_lowercase() == "y" {
let mut player = audio::AsioPlayer::new()?;
println!("Do you want to set a specific sample rate? (y/n):");
let mut rate_input = String::new();
std::io::stdin().read_line(&mut rate_input)?;
if rate_input.trim().to_lowercase() == "y" {
println!("Enter sample rate (e.g., 44100, 48000, 96000, 192000):");
let mut rate_str = String::new();
std::io::stdin().read_line(&mut rate_str)?;
if let Ok(sample_rate) = rate_str.trim().parse::<u32>() {
match player.set_sample_rate(sample_rate) {
Ok(_) => println!("Sample rate set to {} Hz", sample_rate),
Err(e) => println!("Failed to set sample rate: {}", e),
}
} else {
println!("Invalid sample rate, using automatic selection");
}
}
player.play_url(&stream_url).await?;
}
}
}
Ok(())
}
fn list_asio_devices() -> Result<()> {
info!("Enumerating ASIO devices...");
let host = cpal::host_from_id(cpal::HostId::Asio)?;
let devices = host.output_devices()?;
for (i, device) in devices.enumerate() {
let name = device.name()?;
info!("ASIO Device {}: {}", i, name);
let supported_configs = device.supported_output_configs()?;
for config in supported_configs {
info!(" Channels: {}, Sample Rate: {}-{} Hz, Format: {:?}",
config.channels(),
config.min_sample_rate().0,
config.max_sample_rate().0,
config.sample_format());
}
}
Ok(())
}

View file

@ -12,6 +12,7 @@ use std::io::{self, Stdout};
use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::mpsc::{channel, Receiver, Sender};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::thread; use std::thread;
use::tracing::{debug, info};
use crate::jellyfin::{JellyfinClient, LibraryItem}; use crate::jellyfin::{JellyfinClient, LibraryItem};
use crate::player::{Player, PlayerCommand, PlaybackState}; use crate::player::{Player, PlayerCommand, PlaybackState};
@ -312,7 +313,7 @@ impl Tui {
// Player controls (only if not showing help and not in search mode) // Player controls (only if not showing help and not in search mode)
if !self.show_help && self.active_tab != TAB_SEARCH { if !self.show_help && self.active_tab != TAB_SEARCH {
match key.code { match key.code {
KeyCode::Char(' ') => { KeyCode::Char('o') => {
self.player.send_command(PlayerCommand::PlayPause).await?; self.player.send_command(PlayerCommand::PlayPause).await?;
return Ok(false); return Ok(false);
}, },
@ -448,9 +449,9 @@ impl Tui {
if let Some(selected) = self.songs_state.selected() { if let Some(selected) = self.songs_state.selected() {
if selected < self.songs.len() { if selected < self.songs.len() {
let song = &self.songs[selected]; let song = &self.songs[selected];
let url = self.jellyfin.get_stream_url(&song.id)?; debug!("Song ID: {}", song.id);
self.player.send_command(PlayerCommand::Play(url)).await?; self.player.send_command(PlayerCommand::Play(song.id.clone())).await?;
if !self.queue.iter().any(|item| item.id == song.id) { if !self.queue.iter().any(|item| item.id == song.id) {
self.queue.push(song.clone()); self.queue.push(song.clone());
} }
@ -492,8 +493,8 @@ impl Tui {
if let Some(selected) = self.queue_state.selected() { if let Some(selected) = self.queue_state.selected() {
if selected < self.queue.len() { if selected < self.queue.len() {
let song = &self.queue[selected]; let song = &self.queue[selected];
let url = self.jellyfin.get_stream_url(&song.id)?; self.player.send_command(PlayerCommand::Play(song.id.clone())).await?;
self.player.send_command(PlayerCommand::Play(url)).await?;
} }
} }
} }
@ -543,16 +544,14 @@ impl Tui {
} }
} }
KeyCode::Enter => { KeyCode::Enter => {
if let Some(selected) = self.search_state.selected() { if let Some(selected) = self.songs_state.selected() {
if selected < self.search_results.len() { if selected < self.songs.len() {
let song = &self.search_results[selected]; let song = &self.songs[selected];
if song.item_type == "Audio" { debug!("Song ID: {}", song.id);
let url = self.jellyfin.get_stream_url(&song.id)?; self.player.send_command(PlayerCommand::Play(song.id.clone())).await?;
self.player.send_command(PlayerCommand::Play(url)).await?;
if !self.queue.iter().any(|item| item.id == song.id) {
if !self.queue.iter().any(|item| item.id == song.id) { self.queue.push(song.clone());
self.queue.push(song.clone());
}
} }
} }
} }
@ -860,7 +859,7 @@ impl Tui {
f.render_widget(playing_text, chunks[0]); f.render_widget(playing_text, chunks[0]);
// Controls help // Controls help
let controls = "Space: Play/Pause | s: Stop | n/p: Next/Prev"; let controls = "o: Play/Pause | s: Stop | n/p: Next/Prev";
let controls_text = Paragraph::new(controls) let controls_text = Paragraph::new(controls)
.block(Block::default().borders(Borders::ALL).title("Controls")); .block(Block::default().borders(Borders::ALL).title("Controls"));
f.render_widget(controls_text, chunks[1]); f.render_widget(controls_text, chunks[1]);