Skip to content

Commit

Permalink
fix: ensure initial volume applies on reconnection
Browse files Browse the repository at this point in the history
Make initial volume setting work on all connections, not just the first one.
This fixes a bug where the initial volume would only be set once, even if
the controller reconnects multiple times.

Additionally optimizes Percentage methods to be const.
  • Loading branch information
roderickvd committed Dec 18, 2024
1 parent 0c801e7 commit 37dbd79
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 29 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).

### Fixed
- [remote] Prevent duplicate remotes appearing in older Deezer apps
- [remote] Initial volume not being set when controller reconnects
- [track] Infinite loop loading track that is not available yet or anymore

## [0.6.1] - 2024-12-13
Expand Down
12 changes: 10 additions & 2 deletions src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,14 +219,21 @@ pub struct Player {
}

impl Player {
/// Default volume level.
///
/// Constant value of 100% (1.0) used as initial volume setting.
const DEFAULT_VOLUME: Percentage = Percentage::from_ratio_f32(1.0);

/// Logarithmic volume scale factor for a dynamic range of 60 dB.
///
/// Equal to 10^(60/20) = 1000.0
/// Constant used in volume scaling calculations.
const LOG_VOLUME_SCALE_FACTOR: f32 = 1000.0;

/// Logarithmic volume growth rate for a dynamic range of 60 dB.
///
/// Equal to ln(1000) ≈ 6.907755279
/// Constant used in volume scaling calculations.
const LOG_VOLUME_GROWTH_RATE: f32 = 6.907_755_4;

/// Creates a new player instance.
Expand Down Expand Up @@ -275,7 +282,7 @@ impl Player {
repeat_mode: RepeatMode::default(),
normalization: config.normalization,
gain_target_db,
volume: Percentage::from_ratio_f32(1.0),
volume: Self::DEFAULT_VOLUME,
event_tx: None,
playing_since: Duration::ZERO,
deferred_seek: None,
Expand Down Expand Up @@ -436,7 +443,8 @@ impl Player {
let sink = rodio::Sink::try_new(&handle)?;

// Set the volume to the last known value.
sink.set_volume(self.volume.as_ratio_f32());
let target_volume = std::mem::replace(&mut self.volume, Self::DEFAULT_VOLUME);
sink.set_volume(target_volume.as_ratio_f32());

// The output source will output silence when the queue is empty.
// That will cause the sink to report as "playing", so we need to pause it.
Expand Down
107 changes: 83 additions & 24 deletions src/protocol/connect/contents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1229,23 +1229,34 @@ impl FromStr for AudioQuality {
/// Values are stored internally as `f64` ratios between 0.0 and 1.0, but the
/// type provides methods to work with both ratio and percentage formats.
///
/// # Constants
///
/// Provides common values as const:
/// ```rust
/// const ZERO: Percentage = Percentage::ZERO; // 0%
/// const MAX: Percentage = Percentage::ONE_HUNDRED; // 100%
/// ```
///
/// # Examples
///
/// Creating percentages:
/// ```rust
/// // Volume control
/// let max_volume = Percentage::ONE_HUNDRED; // 100% volume
/// let half_volume = Percentage::from_percent_f32(50.0); // 50% volume
/// let muted = Percentage::ZERO; // 0% volume
/// // Using const constructors
/// const HALF: Percentage = Percentage::from_ratio_f32(0.5);
/// const QUARTER: Percentage = Percentage::from_percent_f32(25.0);
///
/// // Using runtime constructors
/// let progress = Percentage::from_ratio_f64(0.75);
/// let volume = Percentage::from_percent_f64(80.0);
/// ```
///
/// Using in playback messages:
/// Using in messages:
/// ```rust
/// let body = Body::PlaybackProgress {
/// message_id: "msg123".to_string(),
/// // ... other fields ...
/// // ...
/// progress: Some(Percentage::from_ratio_f32(0.25)), // 25% through track
/// volume: Percentage::from_ratio_f32(0.8), // 80% volume
/// // ... other fields ...
/// // ...
/// };
/// ```
///
Expand All @@ -1269,60 +1280,102 @@ pub struct Percentage(f64);

impl Percentage {
/// Represents 0% (0.0)
///
/// # Examples
///
/// ```rust
/// const MUTED: Percentage = Percentage::ZERO;
/// assert_eq!(MUTED.as_percent_f32(), 0.0);
/// ```
pub const ZERO: Self = Self(0.0);

/// Represents 100% (1.0)
///
/// # Examples
///
/// ```rust
/// const MAX_VOLUME: Percentage = Percentage::ONE_HUNDRED;
/// assert_eq!(MAX_VOLUME.as_percent_f32(), 100.0);
/// ```
pub const ONE_HUNDRED: Self = Self(1.0);

/// Creates a new percentage from a 32-bit floating point ratio.
///
/// Can be used in const contexts.
///
/// # Examples
///
/// ```rust
/// // Const context
/// const HALF: Percentage = Percentage::from_ratio_f32(0.5);
/// assert_eq!(HALF.as_percent_f32(), 50.0);
///
/// // Runtime context
/// let p = Percentage::from_ratio_f32(0.75);
/// assert_eq!(p.as_percent_f32(), 75.0);
/// ```
#[must_use]
pub fn from_ratio_f32(ratio: f32) -> Self {
Self(ratio.into())
pub const fn from_ratio_f32(ratio: f32) -> Self {
Self(ratio as f64)
}

/// Creates a new percentage from a 64-bit floating point ratio.
///
/// Can be used in const contexts.
///
/// # Examples
///
/// ```rust
/// let p = Percentage::from_ratio_f64(0.333);
/// assert_eq!(p.as_percent_f64(), 33.3);
/// // Const context
/// const THIRD: Percentage = Percentage::from_ratio_f64(0.333);
/// assert_eq!(THIRD.as_percent_f64(), 33.3);
///
/// // Runtime context
/// let p = Percentage::from_ratio_f64(0.5);
/// assert_eq!(p.as_percent_f64(), 50.0);
/// ```
#[must_use]
pub fn from_ratio_f64(ratio: f64) -> Self {
pub const fn from_ratio_f64(ratio: f64) -> Self {
Self(ratio)
}

/// Creates a new percentage from a 32-bit floating point percentage value.
///
/// Can be used in const contexts.
///
/// # Examples
///
/// ```rust
/// // Const context
/// const HALF: Percentage = Percentage::from_percent_f32(50.0);
/// assert_eq!(HALF.as_ratio_f32(), 0.5);
///
/// // Runtime context
/// let p = Percentage::from_percent_f32(75.0);
/// assert_eq!(p.as_ratio_f32(), 0.75);
/// ```
#[must_use]
pub fn from_percent_f32(percent: f32) -> Self {
Self(f64::from(percent) / 100.0)
pub const fn from_percent_f32(percent: f32) -> Self {
Self(percent as f64 / 100.0)
}

/// Creates a new percentage from a 64-bit floating point percentage value.
///
/// Can be used in const contexts.
///
/// # Examples
///
/// ```rust
/// let p = Percentage::from_percent_f64(33.3);
/// assert_eq!(p.as_ratio_f64(), 0.333);
/// // Const context
/// const THIRD: Percentage = Percentage::from_percent_f64(33.3);
/// assert_eq!(THIRD.as_ratio_f64(), 0.333);
///
/// // Runtime context
/// let p = Percentage::from_percent_f64(75.0);
/// assert_eq!(p.as_ratio_f64(), 0.75);
/// ```
#[must_use]
pub fn from_percent_f64(percent: f64) -> Self {
pub const fn from_percent_f64(percent: f64) -> Self {
Self(percent / 100.0)
}

Expand All @@ -1344,14 +1397,17 @@ impl Percentage {

/// Returns the value as a 64-bit floating point ratio (0.0 to 1.0).
///
/// Can be used in const contexts.
///
/// # Examples
///
/// ```rust
/// let p = Percentage::from_ratio_f64(0.333);
/// assert_eq!(p.as_ratio_f64(), 0.333);
/// const P: Percentage = Percentage::from_ratio_f64(0.333);
/// const RATIO: f64 = P.as_ratio_f64();
/// assert_eq!(RATIO, 0.333);
/// ```
#[must_use]
pub fn as_ratio_f64(&self) -> f64 {
pub const fn as_ratio_f64(&self) -> f64 {
self.0
}

Expand All @@ -1373,14 +1429,17 @@ impl Percentage {

/// Returns the value as a 64-bit floating point percentage (0.0 to 100.0).
///
/// Can be used in const contexts.
///
/// # Examples
///
/// ```rust
/// let p = Percentage::from_ratio_f64(0.333);
/// assert_eq!(p.as_percent_f64(), 33.3);
/// const P: Percentage = Percentage::from_ratio_f64(0.333);
/// const PERCENT: f64 = P.as_percent_f64();
/// assert_eq!(PERCENT, 33.3);
/// ```
#[must_use]
pub fn as_percent_f64(&self) -> f64 {
pub const fn as_percent_f64(&self) -> f64 {
self.0 * 100.0
}
}
Expand Down
13 changes: 10 additions & 3 deletions src/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,11 +233,15 @@ enum ShuffleAction {
/// * Active - Set volume and remain active until client sets below maximum
/// * Inactive - Initial volume has been superseded by client control
/// * Disabled - No initial volume configured
///
/// The state transitions from Active to Inactive when the client takes control by setting
/// a volume below maximum. When a connection ends, it transitions back to Active to ensure
/// the initial volume is reapplied on reconnection.
#[derive(Copy, Clone, Debug, PartialEq)]
enum InitialVolume {
/// Initial volume is active and will be applied
/// Initial volume is active and will be applied on connection/reconnection
Active(Percentage),
/// Initial volume is stored but inactive
/// Initial volume is stored but inactive (client has taken control)
Inactive(Percentage),
/// No initial volume configured
Disabled,
Expand Down Expand Up @@ -1202,9 +1206,12 @@ impl Client {
/// * Clear controller association
/// * Reset connection state
/// * Reset discovery state
/// * Restore initial volume activation
/// * Restore initial volume for next connection
/// * Flush cached tokens
/// * Emit disconnect event
///
/// The initial volume is reactivated during reset to ensure it will be
/// applied again when a new controller connects.
fn reset_states(&mut self) {
if let Some(controller) = self.controller() {
info!("disconnected from {controller}");
Expand Down

0 comments on commit 37dbd79

Please sign in to comment.