Integrate visualizer with audio thread

- Add VisualizerTap to Player for sample capture
- Create wrap_source helper in audio thread
- Wrap all audio sources with TappedSource at 4 append points
- Player now captures samples for visualization when enabled
- Zero overhead when disabled (fast atomic check)

Audio samples flow: Source -> TappedSource -> RingBuffer -> FFT Thread -> Frontend
This commit is contained in:
vicrodh committed 2026-02-01 15:10:51 -06:00
1 parent b8cfcfb34a
commit 7bc429744b
2 files changed
+35 -6

No files matched your search

+6 -2
View File
@@ -90,16 +90,20 @@ impl AppState {
Arc::new(AudioCache::default())
};
// Create visualizer first to get the tap for the player
let visualizer = Visualizer::new();
let viz_tap = visualizer.get_tap();
Self {
client: Arc::new(Mutex::new(QobuzClient::default())),
player: Player::new(device_name, audio_settings),
player: Player::new(device_name, audio_settings, Some(viz_tap)),
queue: QueueManager::new(),
context: ContextManager::new(),
media_controls: MediaControlsManager::new(),
audio_cache,
lastfm: Arc::new(Mutex::new(LastFmClient::default())),
songlink: SongLinkClient::new(),
visualizer: Visualizer::new(),
visualizer,
}
}
}
+29 -4
View File
@@ -39,6 +39,7 @@ use symphonia::default::{get_codecs, get_probe};
use crate::api::{client::QobuzClient, models::Quality};
use crate::audio::{AudioBackendType, BackendConfig, BackendManager};
use crate::config::audio_settings::AudioSettings;
use crate::visualizer::{VisualizerTap, TappedSource};
use playback_engine::PlaybackEngine;
/// Commands sent to the audio thread
@@ -637,18 +638,21 @@ pub struct Player {
pub state: SharedState,
/// Audio settings (exclusive mode, DAC passthrough, etc.)
audio_settings: Arc<Mutex<AudioSettings>>,
/// Visualizer tap for audio sample capture (optional)
visualizer_tap: Option<VisualizerTap>,
}
impl Default for Player {
fn default() -> Self {
Self::new(None, AudioSettings::default())
Self::new(None, AudioSettings::default(), None)
}
}
impl Player {
/// Create a new player with an optional specific output device and audio settings
/// If device_name is None, uses the system default device
pub fn new(device_name: Option<String>, audio_settings: AudioSettings) -> Self {
/// visualizer_tap is optional - if provided, audio samples are captured for visualization
pub fn new(device_name: Option<String>, audio_settings: AudioSettings, visualizer_tap: Option<VisualizerTap>) -> Self {
let (tx, rx) = mpsc::channel::<AudioCommand>();
let state = SharedState::new();
let thread_state = state.clone();
@@ -657,10 +661,23 @@ impl Player {
let settings = Arc::new(Mutex::new(audio_settings.clone()));
let thread_settings = settings.clone();
// Clone visualizer tap for audio thread
let thread_viz_tap = visualizer_tap.clone();
// Spawn dedicated audio thread
thread::spawn(move || {
log::info!("Audio thread starting...");
// Helper to wrap source with visualizer tap (if enabled)
// This captures audio samples for visualization without affecting playback
let wrap_source = |source: Box<dyn Source<Item = i16> + Send>| -> Box<dyn Source<Item = i16> + Send> {
if let Some(ref tap) = thread_viz_tap {
Box::new(TappedSource::new(source, tap.ring_buffer.clone(), tap.enabled.clone()))
} else {
source
}
};
// Get the audio host
let host = rodio::cpal::default_host();
@@ -1092,6 +1109,8 @@ impl Player {
.unwrap_or(duration_secs);
thread_state.duration.store(actual_duration, Ordering::SeqCst);
// Wrap source for visualization (if enabled)
let source = wrap_source(source);
if let Err(e) = engine.append(source) {
log::error!("Failed to append source to engine: {}", e);
return;
@@ -1316,6 +1335,8 @@ impl Player {
// Box the incremental source to match the expected type
let source_to_play: Box<dyn Source<Item = i16> + Send> = Box::new(incremental_source);
// Wrap source for visualization (if enabled)
let source_to_play = wrap_source(source_to_play);
if let Err(e) = engine.append(source_to_play) {
log::error!("Failed to append streaming source to engine: {}", e);
return;
@@ -1428,6 +1449,8 @@ impl Player {
source
};
// Wrap source for visualization (if enabled)
let skipped_source = wrap_source(skipped_source);
if let Err(e) = engine.append(skipped_source) {
log::error!("Failed to append source for resume: {}", e);
return;
@@ -1523,8 +1546,10 @@ impl Player {
};
let skip_duration = Duration::from_secs(position_secs);
let skipped_source = source.skip_duration(skip_duration);
let skipped_source: Box<dyn Source<Item = i16> + Send> = Box::new(source.skip_duration(skip_duration));
// Wrap source for visualization (if enabled)
let skipped_source = wrap_source(skipped_source);
if let Err(e) = engine.append(skipped_source) {
log::error!("Failed to append source for seek: {}", e);
return;
@@ -1684,7 +1709,7 @@ impl Player {
}
});
Self { tx, state, audio_settings: settings }
Self { tx, state, audio_settings: settings, visualizer_tap }
}
/// Play a track by ID (downloads audio)