- 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>
97 lines
2.7 KiB
Rust
97 lines
2.7 KiB
Rust
use anyhow::{anyhow, Result};
|
|
use cpal::{
|
|
traits::{DeviceTrait, HostTrait, StreamTrait},
|
|
StreamConfig,
|
|
};
|
|
use rb::{RbConsumer, RbProducer, SpscRb, RB};
|
|
use std::sync::{
|
|
atomic::{AtomicBool, Ordering},
|
|
Arc,
|
|
};
|
|
use symphonia::core::audio::AudioBufferRef;
|
|
|
|
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,
|
|
}
|
|
|
|
impl AudioOutput {
|
|
pub fn try_open(sample_rate: u32, channels: usize) -> Result<Self> {
|
|
let host = cpal::default_host();
|
|
let device = host
|
|
.default_output_device()
|
|
.ok_or_else(|| anyhow!("no output device"))?;
|
|
|
|
let config = StreamConfig {
|
|
channels: channels as u16,
|
|
sample_rate: cpal::SampleRate(sample_rate),
|
|
buffer_size: cpal::BufferSize::Default,
|
|
};
|
|
|
|
let ring = SpscRb::new(RING_BUFFER_SIZE);
|
|
let (producer, consumer) = (ring.producer(), ring.consumer());
|
|
|
|
let stream = device.build_output_stream(
|
|
&config,
|
|
move |data: &mut [f32], _| {
|
|
let n = consumer.read(data).unwrap_or(0);
|
|
for s in &mut data[n..] {
|
|
*s = 0.0;
|
|
}
|
|
},
|
|
|e| eprintln!("audio stream error: {e}"),
|
|
None,
|
|
)?;
|
|
|
|
stream.play()?;
|
|
|
|
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<'_>,
|
|
volume: f32,
|
|
stop: &Arc<AtomicBool>,
|
|
) -> Result<()> {
|
|
let mut sample_buf = symphonia::core::audio::SampleBuffer::<f32>::new(
|
|
decoded.capacity() as u64,
|
|
*decoded.spec(),
|
|
);
|
|
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) {
|
|
return Ok(());
|
|
}
|
|
match self.ring_buf_producer.write_blocking(remaining) {
|
|
Some(n) => remaining = &remaining[n..],
|
|
None => break,
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|