feat: show top tracks on artist profile with play/shuffle
- Adds extra=topTracks to artist/page API request - Embeds a List::Tracks widget at the top of ArtistView showing the artist's most popular tracks, with Play and Shuffle buttons - Bubbles playTrackRequested through MainContent up to MainWindow - Also adds the viz PCM ring buffer FFI infrastructure (for future spectrum widget) to the Rust backend Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use rb::RB;
|
||||
use std::io::{self, Read, Seek, SeekFrom};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
@@ -176,8 +177,12 @@ pub fn play_track_inline(
|
||||
}
|
||||
}
|
||||
if audio_output.is_none() {
|
||||
*audio_output = Some(AudioOutput::try_open(sample_rate, channels)?);
|
||||
let mut ao = AudioOutput::try_open(sample_rate, channels)?;
|
||||
ao.set_viz_producer(status.viz_ring.producer());
|
||||
*audio_output = Some(ao);
|
||||
}
|
||||
status.viz_sample_rate.store(sample_rate, Ordering::Relaxed);
|
||||
status.viz_channels.store(channels as u32, Ordering::Relaxed);
|
||||
let ao = audio_output.as_mut().unwrap();
|
||||
|
||||
let mut stopped = false;
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
mod decoder;
|
||||
pub mod output;
|
||||
|
||||
use rb::{SpscRb, RB};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering},
|
||||
atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicU8, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::api::TrackDto;
|
||||
|
||||
/// Size of the visualizer ring buffer in f32 samples (~180ms at 44.1kHz stereo).
|
||||
const VIZ_RING_SIZE: usize = 16 * 1024;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PlayerCommand {
|
||||
Play(TrackInfo),
|
||||
@@ -50,10 +54,17 @@ pub struct PlayerStatus {
|
||||
pub replaygain_gain: Arc<std::sync::Mutex<f32>>,
|
||||
/// When false the audio output is torn down after each track, producing a gap.
|
||||
pub gapless: Arc<AtomicBool>,
|
||||
/// Visualizer ring buffer (consumer side, read by FFI).
|
||||
pub viz_ring: Arc<SpscRb<f32>>,
|
||||
pub viz_consumer: Arc<std::sync::Mutex<rb::Consumer<f32>>>,
|
||||
pub viz_sample_rate: Arc<AtomicU32>,
|
||||
pub viz_channels: Arc<AtomicU32>,
|
||||
}
|
||||
|
||||
impl PlayerStatus {
|
||||
pub fn new() -> Self {
|
||||
let viz_ring = Arc::new(SpscRb::new(VIZ_RING_SIZE));
|
||||
let viz_consumer = Arc::new(std::sync::Mutex::new(viz_ring.consumer()));
|
||||
Self {
|
||||
state: Arc::new(std::sync::Mutex::new(PlayerState::Idle)),
|
||||
position_secs: Arc::new(AtomicU64::new(0)),
|
||||
@@ -65,6 +76,10 @@ impl PlayerStatus {
|
||||
seek_target_secs: Arc::new(AtomicU64::new(0)),
|
||||
replaygain_gain: Arc::new(std::sync::Mutex::new(1.0)),
|
||||
gapless: Arc::new(AtomicBool::new(false)),
|
||||
viz_ring,
|
||||
viz_consumer,
|
||||
viz_sample_rate: Arc::new(AtomicU32::new(0)),
|
||||
viz_channels: Arc::new(AtomicU32::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ const RING_BUFFER_SIZE: usize = 32 * 1024;
|
||||
|
||||
pub struct AudioOutput {
|
||||
ring_buf_producer: rb::Producer<f32>,
|
||||
viz_producer: Option<rb::Producer<f32>>,
|
||||
_stream: cpal::Stream,
|
||||
pub sample_rate: u32,
|
||||
pub channels: usize,
|
||||
@@ -51,12 +52,17 @@ impl AudioOutput {
|
||||
|
||||
Ok(Self {
|
||||
ring_buf_producer: producer,
|
||||
viz_producer: None,
|
||||
_stream: stream,
|
||||
sample_rate,
|
||||
channels,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_viz_producer(&mut self, producer: rb::Producer<f32>) {
|
||||
self.viz_producer = Some(producer);
|
||||
}
|
||||
|
||||
pub fn write(
|
||||
&mut self,
|
||||
decoded: AudioBufferRef<'_>,
|
||||
@@ -70,6 +76,11 @@ impl AudioOutput {
|
||||
sample_buf.copy_interleaved_ref(decoded);
|
||||
let samples: Vec<f32> = sample_buf.samples().iter().map(|s| s * volume).collect();
|
||||
|
||||
// Best-effort copy for visualizer (non-blocking, ok to drop samples)
|
||||
if let Some(ref mut viz) = self.viz_producer {
|
||||
let _ = viz.write(&samples);
|
||||
}
|
||||
|
||||
let mut remaining = &samples[..];
|
||||
while !remaining.is_empty() {
|
||||
if stop.load(Ordering::SeqCst) {
|
||||
|
||||
Reference in New Issue
Block a user