Skip to content

Conversation

@orionmiz
Copy link
Contributor

@orionmiz orionmiz commented Dec 20, 2025

Short description

This PR fixes a bug where currentTime returns an incorrect value when playbackRate is modified, causing the progress tracking to jump abnormally.

Implementation details

  • Buffer-based playbackPosition: Renamed playedDuration to playbackPosition and refactored the logic to store the absolute offset in the audio buffer (in seconds) rather than cumulative wall-clock time.
  • Improved currentTime Logic: Updated the currentTime getter to calculate the position as playbackPosition + (elapsed_time * playbackRate). This ensures that the tracked progress is independent of past rate changes.
  • Accurate Rate Transitions: The playbackRate setter now snapshots the current playbackPosition by momentarily pausing and resuming. This maintains a seamless progress flow even during rapid rate changes, such as DJ deck adjustments.
  • Direct Seek Assignment: The currentTime setter now assigns the target value directly to playbackPosition, eliminating incorrect rate-based scaling during seek operations.

How to test it

  1. Initialize WaveSurfer with the WebAudio backend.
  2. Change the playbackRate while the audio is playing (example: call wavesurfer.setPlaybackRate(1.2)).
  3. Verify: The progress tracking does not jump forward or backward and stays perfectly in sync with the audio.
  4. Perform a seek operation and verify it lands on the correct second regardless of the current playbackRate.

Screenshots

Here are videos changing playback rate dynamically:

Before:

2025-12-21.062929.mp4

After:

2025-12-21.062805.mp4

Checklist

  • This PR is covered by e2e tests
  • It introduces no breaking API changes

private bufferNode: AudioBufferSourceNode | null = null
private playStartTime = 0
private playedDuration = 0
private playbackPosition = 0
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed playedDuration to playbackPosition to clarify that this variable represents a coordinate (offset) within the audio buffer, rather than elapsed wall-clock time.

this.bufferNode.connect(this.gainNode)

let currentPos = this.playedDuration * this._playbackRate
let currentPos = this.playbackPosition
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since playbackPosition now denotes the absolute position in the buffer, it is used directly as the starting offset without any rate-based scaling.

this.paused = true
this.bufferNode?.stop()
this.playedDuration += this.audioContext.currentTime - this.playStartTime
this.playbackPosition += (this.audioContext.currentTime - this.playStartTime) * this._playbackRate
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upon pausing, the wall-clock time elapsed since the last play start is multiplied by the playbackRate to calculate the actual "distance" covered in the track, which is then accumulated into playbackPosition.

Comment on lines +174 to +178
const wasPlaying = !this.paused
if (wasPlaying) this._pause()
this._playbackRate = value
if (wasPlaying) this._play()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By calling _pause() before a rate change, we "snapshot" the current position as a buffer offset. Resuming with _play() ensures continuity in time calculation, preventing cursor jumps.

Comment on lines +185 to +187
return this.paused
? this.playbackPosition
: this.playbackPosition + (this.audioContext.currentTime - this.playStartTime) * this._playbackRate
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During playback, it returns the fixed starting point (playbackPosition) plus the real-time distance (elapsed time * rate). This prevents past accumulated data from being corrupted by the current rate setting.


if (wasPlaying) this._pause()
this.playedDuration = value / this._playbackRate
this.playbackPosition = value
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user-defined seek target (value) is an absolute value in seconds, so it is assigned directly to playbackPosition regardless of the playback rate.

@orionmiz orionmiz marked this pull request as ready for review December 20, 2025 21:33
@orionmiz
Copy link
Contributor Author

@katspaugh Could you review this PR?

Copy link
Owner

@katspaugh katspaugh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one, thank you!

@katspaugh katspaugh merged commit a3a7e4b into katspaugh:main Jan 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants