Skip to content

Speed::try_seek incorrectly multiplies seek position by speed factor #876

@FrogSnot

Description

@FrogSnot

Hey, I found this while debugging a really frustrating issue in my music player (Sunder). When playing audio at non-1x speeds and seeking via the progress bar, tracks would immediately end as if the file was over.

After a lot of head-scratching, I traced it down to Speed::try_seek in src/source/speed.rs:

fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
        /* TODO: This might be wrong, I do not know how speed achieves its speedup
         * so I can not reason about the correctness.
         * <dvdsk noreply@davidsk.dev> */

        // even after 24 hours of playback f32 has enough precision
        let pos_accounting_for_speedup = pos.mul_f32(self.factor);
        self.input.try_seek(pos_accounting_for_speedup)
    }

The multiplication causes the inner decoder to receive a position that's factor times larger than intended. Since Speed achieves its speedup by modifying sample_rate() (not by resampling or skipping), the inner source still operates in source-time. Multiplying the position overshoots the actual file duration and the source immediately hits EOF.

Example:

215-second audio file, speed set to 2x
User seeks to the 150s mark: sink.try_seek(Duration::from_secs(150))
Speed::try_seek multiplies: 150 * 2.0 = 300s
The decoder receives try_seek(300s) on a 215s file, past EOF
sink.empty() returns true immediately, track skips
This happens for any seek past duration / speed. At 1x the bug is invisible since multiplying by 1.0 is a no-op, which is probably why it went unnoticed.

The TODO comment in the code already notes the uncertainty here, so I figured this was worth reporting.

Suggested fix:

Pass the position through unchanged, like Amplify, Stoppable, and all other sources that don't alter the time domain:

fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
    self.input.try_seek(pos)
}

Current workaround:

Pre-divide the seek position by the speed factor before calling try_seek:

let seek_pos = source_secs / speed as f64;
sink.try_seek(Duration::from_secs_f64(seek_pos));

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions