diff --git a/src/api.rs b/src/api.rs index 6707211..76a819e 100644 --- a/src/api.rs +++ b/src/api.rs @@ -17,11 +17,17 @@ type Aes128CbcDec = cbc::Decryptor; pub enum TrackStream { DirectUrl { url: String, + sampling_rate_hz: Option, + bit_depth: Option, + channels: Option, }, Segmented { url_template: String, n_segments: u32, encryption_key_hex: Option, + sampling_rate_hz: Option, + bit_depth: Option, + channels: Option, }, } @@ -482,7 +488,12 @@ impl QobuzApi { } if let Some(url) = file.url { - return Ok(TrackStream::DirectUrl { url }); + return Ok(TrackStream::DirectUrl { + url, + sampling_rate_hz: file.sampling_rate.map(|v| v.round() as u32), + bit_depth: file.bit_depth.map(|v| v as u32), + channels: Some(2), + }); } if let (Some(url_template), Some(n_segments)) = (file.url_template, file.n_segments) { @@ -490,6 +501,9 @@ impl QobuzApi { url_template, n_segments, encryption_key_hex: file.key, + sampling_rate_hz: file.sampling_rate.map(|v| v.round() as u32), + bit_depth: file.bit_depth.map(|v| v as u32), + channels: Some(2), }); } @@ -566,7 +580,12 @@ impl QobuzApi { let url = self .get_track_url_legacy(access_token, track_id, format_id) .await?; - Ok(TrackStream::DirectUrl { url }) + Ok(TrackStream::DirectUrl { + url, + sampling_rate_hz: None, + bit_depth: None, + channels: None, + }) } } } @@ -581,7 +600,7 @@ impl QobuzApi { .get_track_stream(access_token, track_id, format_id) .await? { - TrackStream::DirectUrl { url } => Ok(url), + TrackStream::DirectUrl { url, .. } => Ok(url), TrackStream::Segmented { .. } => Err(QobuzError::ApiError( "Track uses segmented stream; use get_track_stream instead".to_string(), )), diff --git a/src/main.rs b/src/main.rs index c339e78..152f881 100644 --- a/src/main.rs +++ b/src/main.rs @@ -179,11 +179,14 @@ async fn main() -> Result<()> { drop(guard); let api = QobuzApi::new(&config); match api.get_track_stream(&token, &track_id, format_id).await { - Ok(TrackStream::DirectUrl { url }) => println!("Stream URL: {}", url), + Ok(TrackStream::DirectUrl { url, .. }) => println!("Stream URL: {}", url), Ok(TrackStream::Segmented { url_template, n_segments, encryption_key_hex, + sampling_rate_hz, + bit_depth, + .. }) => { println!("Segmented stream template: {}", url_template); println!("Segments: {}", n_segments); @@ -195,6 +198,12 @@ async fn main() -> Result<()> { "no" } ); + if let Some(sr) = sampling_rate_hz { + println!("Sampling rate: {} Hz", sr); + } + if let Some(bits) = bit_depth { + println!("Bit depth: {}", bits); + } } Err(e) => { error!("Failed: {}", e); diff --git a/src/qconnect.rs b/src/qconnect.rs index 7e40861..78abd1a 100644 --- a/src/qconnect.rs +++ b/src/qconnect.rs @@ -392,6 +392,30 @@ fn msg_max_audio_quality_changed(quality: u64) -> Vec { build_qconnect_message(28, &payload) } +fn msg_file_audio_quality_changed( + sampling_rate_hz: u64, + bit_depth: u64, + channels: u64, + audio_quality: u64, +) -> Vec { + let mut payload = encode_field_varint(1, sampling_rate_hz); + payload.extend(encode_field_varint(2, bit_depth)); + payload.extend(encode_field_varint(3, channels)); + payload.extend(encode_field_varint(4, audio_quality)); + build_qconnect_message(26, &payload) +} + +fn msg_device_audio_quality_changed( + sampling_rate_hz: u64, + bit_depth: u64, + channels: u64, +) -> Vec { + let mut payload = encode_field_varint(1, sampling_rate_hz); + payload.extend(encode_field_varint(2, bit_depth)); + payload.extend(encode_field_varint(3, channels)); + build_qconnect_message(27, &payload) +} + /// RNDR_SRVR_VOLUME_MUTED (29): renderer confirms mute state. fn msg_volume_muted(muted: bool) -> Vec { let payload = encode_field_varint(1, if muted { 1 } else { 0 }); @@ -1045,25 +1069,43 @@ async fn run_connection( current_duration_ms = duration_ms; match api.get_track_stream(auth_token, &track_id_str, format_id).await { Ok(stream) => { - let player_stream = match stream { - TrackStream::DirectUrl { url } => { + let (player_stream, stream_sr, stream_bits, stream_ch) = match stream { + TrackStream::DirectUrl { + url, + sampling_rate_hz, + bit_depth, + channels, + } => { info!("[STATE] Got direct stream URL (duration={}ms)", duration_ms); - StreamSource::DirectUrl(url) + ( + StreamSource::DirectUrl(url), + sampling_rate_hz, + bit_depth, + channels, + ) } TrackStream::Segmented { url_template, n_segments, encryption_key_hex, + sampling_rate_hz, + bit_depth, + channels, } => { info!( "[STATE] Got segmented stream (segments={}, duration={}ms)", n_segments, duration_ms ); - StreamSource::Segmented { - url_template, - n_segments, - encryption_key_hex, - } + ( + StreamSource::Segmented { + url_template, + n_segments, + encryption_key_hex, + }, + sampling_rate_hz, + bit_depth, + channels, + ) } }; player.send(PlayerCommand::Play { @@ -1073,6 +1115,24 @@ async fn run_connection( duration_ms, start_position_ms: requested_pos.unwrap_or(0), }); + if let (Some(sr), Some(bits)) = (stream_sr, stream_bits) { + let ch = stream_ch.unwrap_or(2).max(1); + let file_msg = msg_file_audio_quality_changed( + sr as u64, + bits as u64, + ch as u64, + max_audio_quality as u64, + ); + ws_tx.send(Message::Binary(build_payload_frame(msg_id, &file_msg).into())).await?; + msg_id += 1; + let dev_msg = msg_device_audio_quality_changed( + sr as u64, + bits as u64, + ch as u64, + ); + ws_tx.send(Message::Binary(build_payload_frame(msg_id, &dev_msg).into())).await?; + msg_id += 1; + } current_buffer_state = 2; // OK } Err(e) => { @@ -1271,17 +1331,39 @@ async fn run_connection( current_duration_ms = duration_ms; match api.get_track_stream(auth_token, &track_id_str, format_id).await { Ok(stream) => { - let player_stream = match stream { - TrackStream::DirectUrl { url } => StreamSource::DirectUrl(url), + let (player_stream, stream_sr, stream_bits, stream_ch) = match stream { + TrackStream::DirectUrl { + url, + sampling_rate_hz, + bit_depth, + channels, + } => { + ( + StreamSource::DirectUrl(url), + sampling_rate_hz, + bit_depth, + channels, + ) + } TrackStream::Segmented { url_template, n_segments, encryption_key_hex, - } => StreamSource::Segmented { - url_template, - n_segments, - encryption_key_hex, - }, + sampling_rate_hz, + bit_depth, + channels, + } => { + ( + StreamSource::Segmented { + url_template, + n_segments, + encryption_key_hex, + }, + sampling_rate_hz, + bit_depth, + channels, + ) + } }; player.send(PlayerCommand::Play { stream: player_stream, @@ -1290,6 +1372,24 @@ async fn run_connection( duration_ms, start_position_ms: 0, }); + if let (Some(sr), Some(bits)) = (stream_sr, stream_bits) { + let ch = stream_ch.unwrap_or(2).max(1); + let file_msg = msg_file_audio_quality_changed( + sr as u64, + bits as u64, + ch as u64, + *quality as u64, + ); + ws_tx.send(Message::Binary(build_payload_frame(msg_id, &file_msg).into())).await?; + msg_id += 1; + let dev_msg = msg_device_audio_quality_changed( + sr as u64, + bits as u64, + ch as u64, + ); + ws_tx.send(Message::Binary(build_payload_frame(msg_id, &dev_msg).into())).await?; + msg_id += 1; + } current_buffer_state = 2; // OK(2) info!("Restarted at format_id={}", format_id); }