Merge pull request #987 from marcelblum/context-options-bugfixes

fixes for Context options handling & micro timing bugs
This commit is contained in:
Yotam Mann 2022-01-15 09:51:35 -05:00 committed by GitHub
commit fb17cf564f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 42 additions and 43 deletions

View file

@ -16,7 +16,12 @@ export class Ticker {
/** /**
* The update interval of the worker * The update interval of the worker
*/ */
private _updateInterval: Seconds; private _updateInterval!: Seconds;
/**
* The lowest allowable interval, preferably calculated from context sampleRate
*/
private _minimumUpdateInterval: Seconds;
/** /**
* The callback to invoke at regular intervals * The callback to invoke at regular intervals
@ -33,11 +38,12 @@ export class Ticker {
*/ */
private _worker!: Worker; private _worker!: Worker;
constructor(callback: () => void, type: TickerClockSource, updateInterval: Seconds) { constructor(callback: () => void, type: TickerClockSource, updateInterval: Seconds, contextSampleRate?: number) {
this._callback = callback; this._callback = callback;
this._type = type; this._type = type;
this._updateInterval = updateInterval; this._minimumUpdateInterval = Math.max( 128/(contextSampleRate || 44100), .001 );
this.updateInterval = updateInterval;
// create the clock source for the first time // create the clock source for the first time
this._createClock(); this._createClock();
@ -121,9 +127,9 @@ export class Ticker {
return this._updateInterval; return this._updateInterval;
} }
set updateInterval(interval: Seconds) { set updateInterval(interval: Seconds) {
this._updateInterval = Math.max(interval, 128 / 44100); this._updateInterval = Math.max(interval, this._minimumUpdateInterval);
if (this._type === "worker") { if (this._type === "worker") {
this._worker.postMessage(Math.max(interval * 1000, 1)); this._worker?.postMessage(this._updateInterval * 1000);
} }
} }

View file

@ -75,8 +75,10 @@ describe("Context", () => {
clockSource: "timeout", clockSource: "timeout",
latencyHint: "playback", latencyHint: "playback",
lookAhead: 0.2, lookAhead: 0.2,
updateInterval: 0.1
}); });
expect(ctx.lookAhead).to.equal(0.2); expect(ctx.lookAhead).to.equal(0.2);
expect(ctx.updateInterval).to.equal(0.1);
expect(ctx.latencyHint).to.equal("playback"); expect(ctx.latencyHint).to.equal("playback");
expect(ctx.clockSource).to.equal("timeout"); expect(ctx.clockSource).to.equal("timeout");
ctx.dispose(); ctx.dispose();

View file

@ -3,7 +3,7 @@ import { Seconds } from "../type/Units";
import { isAudioContext } from "../util/AdvancedTypeCheck"; import { isAudioContext } from "../util/AdvancedTypeCheck";
import { optionsFromArguments } from "../util/Defaults"; import { optionsFromArguments } from "../util/Defaults";
import { Timeline } from "../util/Timeline"; import { Timeline } from "../util/Timeline";
import { isDefined, isString } from "../util/TypeCheck"; import { isDefined } from "../util/TypeCheck";
import { import {
AnyAudioContext, AnyAudioContext,
createAudioContext, createAudioContext,
@ -39,13 +39,6 @@ export interface ContextTimeoutEvent {
export class Context extends BaseContext { export class Context extends BaseContext {
readonly name: string = "Context"; readonly name: string = "Context";
/**
* The amount of time into the future events are scheduled. Giving Web Audio
* a short amount of time into the future to schedule events can reduce clicks and
* improve performance. This value can be set to 0 to get the lowest latency.
*/
lookAhead: Seconds;
/** /**
* private reference to the BaseAudioContext * private reference to the BaseAudioContext
*/ */
@ -116,16 +109,20 @@ export class Context extends BaseContext {
if (options.context) { if (options.context) {
this._context = options.context; this._context = options.context;
// custom context provided, latencyHint unknown (unless explicitly provided in options)
this._latencyHint = arguments[0]?.latencyHint || "";
} else { } else {
this._context = createAudioContext({ this._context = createAudioContext({
latencyHint: options.latencyHint, latencyHint: options.latencyHint,
}); });
this._latencyHint = options.latencyHint;
} }
this._ticker = new Ticker( this._ticker = new Ticker(
this.emit.bind(this, "tick"), this.emit.bind(this, "tick"),
options.clockSource, options.clockSource,
options.updateInterval options.updateInterval,
this._context.sampleRate
); );
this.on("tick", this._timeoutLoop.bind(this)); this.on("tick", this._timeoutLoop.bind(this));
@ -133,9 +130,9 @@ export class Context extends BaseContext {
this._context.onstatechange = () => { this._context.onstatechange = () => {
this.emit("statechange", this.state); this.emit("statechange", this.state);
}; };
this._setLatencyHint(options.latencyHint); // if no custom updateInterval provided, updateInterval will be derived by lookAhead setter
this.lookAhead = options.lookAhead; this[arguments[0]?.hasOwnProperty("updateInterval") ? "_lookAhead" : "lookAhead"] = options.lookAhead;
} }
static getDefaults(): ContextOptions { static getDefaults(): ContextOptions {
@ -391,8 +388,9 @@ export class Context extends BaseContext {
/** /**
* How often the interval callback is invoked. * How often the interval callback is invoked.
* This number corresponds to how responsive the scheduling * This number corresponds to how responsive the scheduling
* can be. context.updateInterval + context.lookAhead gives you the * can be. Setting to 0 will result in the lowest practial interval
* total latency between scheduling an event and hearing it. * based on context properties. context.updateInterval + context.lookAhead
* gives you the total latency between scheduling an event and hearing it.
*/ */
get updateInterval(): Seconds { get updateInterval(): Seconds {
return this._ticker.updateInterval; return this._ticker.updateInterval;
@ -412,6 +410,22 @@ export class Context extends BaseContext {
this._ticker.type = type; this._ticker.type = type;
} }
/**
* The amount of time into the future events are scheduled. Giving Web Audio
* a short amount of time into the future to schedule events can reduce clicks and
* improve performance. This value can be set to 0 to get the lowest latency.
* Adjusting this value also affects the [[updateInterval]].
*/
get lookAhead(): Seconds {
return this._lookAhead;
}
set lookAhead(time: Seconds) {
this._lookAhead = time;
// if lookAhead is 0, default to .01 updateInterval
this.updateInterval = time ? (time / 2) : .01;
}
private _lookAhead!: Seconds;
/** /**
* The type of playback, which affects tradeoffs between audio * The type of playback, which affects tradeoffs between audio
* output latency and responsiveness. * output latency and responsiveness.
@ -431,29 +445,6 @@ export class Context extends BaseContext {
return this._latencyHint; return this._latencyHint;
} }
/**
* Update the lookAhead and updateInterval based on the latencyHint
*/
private _setLatencyHint(hint: ContextLatencyHint | Seconds): void {
let lookAheadValue = 0;
this._latencyHint = hint;
if (isString(hint)) {
switch (hint) {
case "interactive":
lookAheadValue = 0.1;
break;
case "playback":
lookAheadValue = 0.5;
break;
case "balanced":
lookAheadValue = 0.25;
break;
}
}
this.lookAhead = lookAheadValue;
this.updateInterval = lookAheadValue / 2;
}
/** /**
* The unwrapped AudioContext or OfflineAudioContext * The unwrapped AudioContext or OfflineAudioContext
*/ */
@ -469,7 +460,7 @@ export class Context extends BaseContext {
* }, 100); * }, 100);
*/ */
now(): Seconds { now(): Seconds {
return this._context.currentTime + this.lookAhead; return this._context.currentTime + this._lookAhead;
} }
/** /**