Add seeking and notes about clock desync
This commit is contained in:
parent
9c752680ba
commit
ebb6f1b602
5 changed files with 143 additions and 17 deletions
|
@ -18,7 +18,42 @@ import { SimpleObservable } from "matrix-widget-api";
|
|||
import { IDestroyable } from "../utils/IDestroyable";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
// Because keeping track of time is sufficiently complicated...
|
||||
/**
|
||||
* Tracks accurate human-perceptible time for an audio clip, as informed
|
||||
* by managed playback. This clock is tightly coupled with the operation
|
||||
* of the Playback class, making assumptions about how the provided
|
||||
* AudioContext will be used (suspended/resumed to preserve time, etc).
|
||||
*
|
||||
* But why do we need a clock? The AudioContext exposes time information,
|
||||
* and so does the audio buffer, but not in a way that is useful for humans
|
||||
* to perceive. The audio buffer time is often lagged behind the context
|
||||
* time due to internal processing delays of the audio API. Additionally,
|
||||
* the context's time is tracked from when it was first initialized/started,
|
||||
* not related to positioning within the clip. However, the context time
|
||||
* is the most accurate time we can use to determine position within the
|
||||
* clip if we're fast enough to track the pauses and stops.
|
||||
*
|
||||
* As a result, we track every play, pause, stop, and seek event from the
|
||||
* Playback class (kinda: it calls us, which is close enough to the same
|
||||
* thing). These events are then tracked on the AudioContext time scale,
|
||||
* with assumptions that code execution will result in negligible desync
|
||||
* of the clock, or at least no perceptible difference in time. It's
|
||||
* extremely important that the calling code, and the clock's own code,
|
||||
* is extremely fast between the event happening and the clock time being
|
||||
* tracked - anything more than a dozen milliseconds is likely to stack up
|
||||
* poorly, leading to clock desync.
|
||||
*
|
||||
* Clock desync can be dangerous for the stability of the playback controls:
|
||||
* if the clock thinks the user is somewhere else in the clip, it could
|
||||
* inform the playback of the wrong place in time, leading to dead air in
|
||||
* the output or, if severe enough, a clock that won't stop running while
|
||||
* the audio is paused/stopped. Other examples include the clip stopping at
|
||||
* 90% time due to playback ending, the clip playing from the wrong spot
|
||||
* relative to the time, and negative clock time.
|
||||
*
|
||||
* Note that the clip duration is fed to the clock: this is to ensure that
|
||||
* we have the most accurate time possible to present.
|
||||
*/
|
||||
export class PlaybackClock implements IDestroyable {
|
||||
private clipStart = 0;
|
||||
private stopped = true;
|
||||
|
@ -41,6 +76,12 @@ export class PlaybackClock implements IDestroyable {
|
|||
}
|
||||
|
||||
public get timeSeconds(): number {
|
||||
// The modulo is to ensure that we're only looking at the most recent clip
|
||||
// time, as the context is long-running and multiple plays might not be
|
||||
// informed to us (if the control is looping, for example). By taking the
|
||||
// remainder of the division operation, we're assuming that playback is
|
||||
// incomplete or stopped, thus giving an accurate position within the active
|
||||
// clip segment.
|
||||
return (this.context.currentTime - this.clipStart) % this.clipDuration;
|
||||
}
|
||||
|
||||
|
@ -49,7 +90,7 @@ export class PlaybackClock implements IDestroyable {
|
|||
}
|
||||
|
||||
private checkTime = () => {
|
||||
const now = this.timeSeconds;
|
||||
const now = this.timeSeconds; // calculated dynamically
|
||||
if (this.lastCheck !== now) {
|
||||
this.observable.update([now, this.durationSeconds]);
|
||||
this.lastCheck = now;
|
||||
|
@ -82,8 +123,9 @@ export class PlaybackClock implements IDestroyable {
|
|||
}
|
||||
|
||||
if (!this.timerId) {
|
||||
// case to number because the types are wrong
|
||||
// 100ms interval to make sure the time is as accurate as possible
|
||||
// cast to number because the types are wrong
|
||||
// 100ms interval to make sure the time is as accurate as possible without
|
||||
// being overly insane
|
||||
this.timerId = <number><any>setInterval(this.checkTime, 100);
|
||||
}
|
||||
}
|
||||
|
@ -92,6 +134,12 @@ export class PlaybackClock implements IDestroyable {
|
|||
this.stopped = true;
|
||||
}
|
||||
|
||||
public syncTo(contextTime: number, clipTime: number) {
|
||||
this.clipStart = contextTime - clipTime;
|
||||
this.stopped = false; // count as a mid-stream pause (if we were stopped)
|
||||
this.checkTime();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.observable.close();
|
||||
if (this.timerId) clearInterval(this.timerId);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue