Initial implementation of qobuz-qt

- Rust backend (qobuz-backend static lib): Qobuz API client (reqwest/tokio),
  Symphonia audio decoder, CPAL audio output, extern "C" FFI bridge
- Qt 6 frontend mirroring spotify-qt layout: toolbar with playback controls,
  left library dock, central track list, right search panel
- Auth: email/password login with MD5-signed requests; session token persisted
  via QSettings
- Playback: double-click a track → Rust fetches stream URL → Symphonia decodes
  → CPAL outputs to default audio device
- Dark Fusion palette matching spotify-qt feel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
joren
2026-03-23 23:34:23 +01:00
commit 9402dca7ed
40 changed files with 3963 additions and 0 deletions

81
rust/src/player/output.rs Normal file
View File

@@ -0,0 +1,81 @@
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>,
_stream: cpal::Stream,
}
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,
_stream: stream,
})
}
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();
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(())
}
}