2020-01-08 19:12:11 +00:00
|
|
|
import { connect } from "../../core/context/ToneAudioNode";
|
2019-06-18 01:52:23 +00:00
|
|
|
import { Param } from "../../core/context/Param";
|
|
|
|
import { ToneAudioBuffer } from "../../core/context/ToneAudioBuffer";
|
2019-07-30 19:35:27 +00:00
|
|
|
import { GainFactor, Positive, Seconds, Time } from "../../core/type/Units";
|
2019-06-18 01:52:23 +00:00
|
|
|
import { defaultArg, optionsFromArguments } from "../../core/util/Defaults";
|
2019-07-11 13:57:06 +00:00
|
|
|
import { noOp } from "../../core/util/Interface";
|
|
|
|
import { isDefined } from "../../core/util/TypeCheck";
|
2019-12-16 20:58:31 +00:00
|
|
|
import { assert } from "../../core/util/Debug";
|
2019-07-22 20:17:49 +00:00
|
|
|
import { OneShotSource, OneShotSourceCurve, OneShotSourceOptions } from "../OneShotSource";
|
2019-12-22 05:38:21 +00:00
|
|
|
import { EQ, GTE, LT } from "../../core/util/Math";
|
2019-07-22 20:17:49 +00:00
|
|
|
|
|
|
|
export type ToneBufferSourceCurve = OneShotSourceCurve;
|
2019-06-18 01:52:23 +00:00
|
|
|
|
2019-09-04 22:34:42 +00:00
|
|
|
export interface ToneBufferSourceOptions extends OneShotSourceOptions {
|
2020-01-30 04:34:05 +00:00
|
|
|
url: string | AudioBuffer | ToneAudioBuffer;
|
2019-07-22 20:17:49 +00:00
|
|
|
curve: ToneBufferSourceCurve;
|
2019-06-18 01:52:23 +00:00
|
|
|
playbackRate: Positive;
|
|
|
|
fadeIn: Time;
|
|
|
|
fadeOut: Time;
|
|
|
|
loopStart: Time;
|
|
|
|
loopEnd: Time;
|
|
|
|
loop: boolean;
|
|
|
|
onload: () => void;
|
2020-01-30 04:34:05 +00:00
|
|
|
onerror: (error: Error) => void;
|
2019-06-18 01:52:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-09-14 20:39:18 +00:00
|
|
|
* Wrapper around the native BufferSourceNode.
|
2019-09-16 14:15:23 +00:00
|
|
|
* @category Source
|
2019-06-18 01:52:23 +00:00
|
|
|
*/
|
|
|
|
export class ToneBufferSource extends OneShotSource<ToneBufferSourceOptions> {
|
|
|
|
|
2019-09-04 23:18:44 +00:00
|
|
|
readonly name: string = "ToneBufferSource";
|
2019-06-18 01:52:23 +00:00
|
|
|
|
|
|
|
/**
|
2019-09-14 20:39:18 +00:00
|
|
|
* The oscillator
|
2019-06-18 01:52:23 +00:00
|
|
|
*/
|
|
|
|
private _source = this.context.createBufferSource();
|
2019-08-03 16:00:14 +00:00
|
|
|
protected _internalChannels = [this._source];
|
2019-06-18 01:52:23 +00:00
|
|
|
|
|
|
|
/**
|
2019-09-14 20:39:18 +00:00
|
|
|
* The frequency of the oscillator
|
2019-06-18 01:52:23 +00:00
|
|
|
*/
|
2019-10-28 15:37:53 +00:00
|
|
|
readonly playbackRate: Param<"positive">;
|
2019-06-18 01:52:23 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The private instance of the buffer object
|
|
|
|
*/
|
|
|
|
private _buffer: ToneAudioBuffer;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* indicators if the source has started/stopped
|
|
|
|
*/
|
2019-11-17 18:09:19 +00:00
|
|
|
private _sourceStarted = false;
|
|
|
|
private _sourceStopped = false;
|
2019-06-18 01:52:23 +00:00
|
|
|
|
2019-08-27 15:47:52 +00:00
|
|
|
/**
|
2020-01-30 04:34:05 +00:00
|
|
|
* @param url The buffer to play or url to load
|
2019-08-30 16:03:42 +00:00
|
|
|
* @param onload The callback to invoke when the buffer is done playing.
|
2019-08-27 15:47:52 +00:00
|
|
|
*/
|
2020-01-30 04:34:05 +00:00
|
|
|
constructor(url?: ToneAudioBuffer | AudioBuffer | string, onload?: () => void);
|
2019-07-22 20:17:49 +00:00
|
|
|
constructor(options?: Partial<ToneBufferSourceOptions>);
|
2019-06-18 01:52:23 +00:00
|
|
|
constructor() {
|
|
|
|
|
2020-01-30 04:34:05 +00:00
|
|
|
super(optionsFromArguments(ToneBufferSource.getDefaults(), arguments, ["url", "onload"]));
|
|
|
|
const options = optionsFromArguments(ToneBufferSource.getDefaults(), arguments, ["url", "onload"]);
|
2019-06-18 01:52:23 +00:00
|
|
|
|
|
|
|
connect(this._source, this._gainNode);
|
2019-07-23 15:47:32 +00:00
|
|
|
this._source.onended = () => this._stopSource();
|
2019-06-18 01:52:23 +00:00
|
|
|
|
|
|
|
/**
|
2019-09-14 20:39:18 +00:00
|
|
|
* The playbackRate of the buffer
|
2019-06-18 01:52:23 +00:00
|
|
|
*/
|
|
|
|
this.playbackRate = new Param({
|
|
|
|
context: this.context,
|
2019-09-16 03:32:40 +00:00
|
|
|
param: this._source.playbackRate,
|
|
|
|
units: "positive",
|
|
|
|
value: options.playbackRate,
|
2019-06-18 01:52:23 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// set some values initially
|
|
|
|
this.loop = options.loop;
|
|
|
|
this.loopStart = options.loopStart;
|
|
|
|
this.loopEnd = options.loopEnd;
|
2020-01-30 04:34:05 +00:00
|
|
|
this._buffer = new ToneAudioBuffer(options.url, options.onload, options.onerror);
|
2019-08-03 16:00:14 +00:00
|
|
|
|
|
|
|
this._internalChannels.push(this._source);
|
2019-06-18 01:52:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static getDefaults(): ToneBufferSourceOptions {
|
|
|
|
return Object.assign(OneShotSource.getDefaults(), {
|
2020-01-30 04:34:05 +00:00
|
|
|
url: new ToneAudioBuffer(),
|
2019-06-18 01:52:23 +00:00
|
|
|
loop: false,
|
2019-09-16 03:32:40 +00:00
|
|
|
loopEnd: 0,
|
|
|
|
loopStart: 0,
|
2019-06-18 01:52:23 +00:00
|
|
|
onload: noOp,
|
2020-01-30 04:34:05 +00:00
|
|
|
onerror: noOp,
|
2019-09-16 03:32:40 +00:00
|
|
|
playbackRate: 1,
|
2019-06-18 01:52:23 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-09-14 20:39:18 +00:00
|
|
|
* The fadeIn time of the amplitude envelope.
|
2019-06-18 01:52:23 +00:00
|
|
|
*/
|
|
|
|
get fadeIn(): Time {
|
|
|
|
return this._fadeIn;
|
|
|
|
}
|
|
|
|
set fadeIn(t: Time) {
|
|
|
|
this._fadeIn = t;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-09-14 20:39:18 +00:00
|
|
|
* The fadeOut time of the amplitude envelope.
|
2019-06-18 01:52:23 +00:00
|
|
|
*/
|
|
|
|
get fadeOut(): Time {
|
|
|
|
return this._fadeOut;
|
|
|
|
}
|
|
|
|
set fadeOut(t: Time) {
|
|
|
|
this._fadeOut = t;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The curve applied to the fades, either "linear" or "exponential"
|
|
|
|
*/
|
2019-07-22 20:17:49 +00:00
|
|
|
get curve(): ToneBufferSourceCurve {
|
2019-06-18 01:52:23 +00:00
|
|
|
return this._curve;
|
|
|
|
}
|
2019-07-22 20:17:49 +00:00
|
|
|
set curve(t) {
|
2019-06-18 01:52:23 +00:00
|
|
|
this._curve = t;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-09-14 20:39:18 +00:00
|
|
|
* Start the buffer
|
2019-08-30 16:06:38 +00:00
|
|
|
* @param time When the player should start.
|
|
|
|
* @param offset The offset from the beginning of the sample to start at.
|
2019-10-23 03:04:52 +00:00
|
|
|
* @param duration How long the sample should play. If no duration is given, it will default to the full length of the sample (minus any offset)
|
2019-08-30 16:06:38 +00:00
|
|
|
* @param gain The gain to play the buffer back at.
|
2019-06-18 01:52:23 +00:00
|
|
|
*/
|
2019-06-19 21:20:20 +00:00
|
|
|
start(time?: Time, offset?: Time, duration?: Time, gain: GainFactor = 1): this {
|
2019-12-16 20:58:31 +00:00
|
|
|
assert(this.buffer.loaded, "buffer is either not set or not loaded");
|
2019-07-30 19:35:27 +00:00
|
|
|
const computedTime = this.toSeconds(time);
|
2019-06-18 01:52:23 +00:00
|
|
|
|
|
|
|
// apply the gain envelope
|
2019-07-30 19:35:27 +00:00
|
|
|
this._startGain(computedTime, gain);
|
2019-06-18 01:52:23 +00:00
|
|
|
|
|
|
|
// if it's a loop the default offset is the loopstart point
|
|
|
|
if (this.loop) {
|
|
|
|
offset = defaultArg(offset, this.loopStart);
|
|
|
|
} else {
|
|
|
|
// otherwise the default offset is 0
|
|
|
|
offset = defaultArg(offset, 0);
|
|
|
|
}
|
|
|
|
// make sure the offset is not less than 0
|
2019-07-30 19:35:27 +00:00
|
|
|
let computedOffset = Math.max(this.toSeconds(offset), 0);
|
2019-06-18 01:52:23 +00:00
|
|
|
|
|
|
|
// start the buffer source
|
|
|
|
if (this.loop) {
|
|
|
|
// modify the offset if it's greater than the loop time
|
|
|
|
const loopEnd = this.toSeconds(this.loopEnd) || this.buffer.duration;
|
|
|
|
const loopStart = this.toSeconds(this.loopStart);
|
|
|
|
const loopDuration = loopEnd - loopStart;
|
|
|
|
// move the offset back
|
2019-09-20 21:45:22 +00:00
|
|
|
if (GTE(computedOffset, loopEnd)) {
|
2019-07-30 19:35:27 +00:00
|
|
|
computedOffset = ((computedOffset - loopStart) % loopDuration) + loopStart;
|
2019-06-18 01:52:23 +00:00
|
|
|
}
|
2019-12-22 05:38:21 +00:00
|
|
|
// when the offset is very close to the duration, set it to 0
|
|
|
|
if (EQ(computedOffset, this.buffer.duration)) {
|
|
|
|
computedOffset = 0;
|
|
|
|
}
|
2019-06-18 01:52:23 +00:00
|
|
|
}
|
2019-12-22 05:38:21 +00:00
|
|
|
|
2019-06-18 01:52:23 +00:00
|
|
|
// this.buffer.loaded would have return false if the AudioBuffer was undefined
|
|
|
|
this._source.buffer = this.buffer.get() as AudioBuffer;
|
|
|
|
this._source.loopEnd = this.toSeconds(this.loopEnd) || this.buffer.duration;
|
2019-09-20 21:45:22 +00:00
|
|
|
if (LT(computedOffset, this.buffer.duration)) {
|
2019-06-18 01:52:23 +00:00
|
|
|
this._sourceStarted = true;
|
2019-07-30 19:35:27 +00:00
|
|
|
this._source.start(computedTime, computedOffset);
|
2019-06-18 01:52:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// if a duration is given, schedule a stop
|
|
|
|
if (isDefined(duration)) {
|
|
|
|
let computedDur = this.toSeconds(duration);
|
|
|
|
// make sure it's never negative
|
|
|
|
computedDur = Math.max(computedDur, 0);
|
2019-07-30 19:35:27 +00:00
|
|
|
this.stop(computedTime + computedDur);
|
2019-06-18 01:52:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2019-07-23 15:47:32 +00:00
|
|
|
protected _stopSource(time?: Seconds): void {
|
2019-09-20 21:45:22 +00:00
|
|
|
if (!this._sourceStopped && this._sourceStarted) {
|
2019-06-18 01:52:23 +00:00
|
|
|
this._sourceStopped = true;
|
2019-07-23 15:47:32 +00:00
|
|
|
this._source.stop(this.toSeconds(time));
|
|
|
|
this._onended();
|
2019-06-18 01:52:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If loop is true, the loop will start at this position.
|
|
|
|
*/
|
|
|
|
get loopStart(): Time {
|
|
|
|
return this._source.loopStart;
|
|
|
|
}
|
|
|
|
set loopStart(loopStart: Time) {
|
|
|
|
this._source.loopStart = this.toSeconds(loopStart);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If loop is true, the loop will end at this position.
|
|
|
|
*/
|
|
|
|
get loopEnd(): Time {
|
|
|
|
return this._source.loopEnd;
|
|
|
|
}
|
|
|
|
set loopEnd(loopEnd: Time) {
|
|
|
|
this._source.loopEnd = this.toSeconds(loopEnd);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The audio buffer belonging to the player.
|
|
|
|
*/
|
|
|
|
get buffer(): ToneAudioBuffer {
|
|
|
|
return this._buffer;
|
|
|
|
}
|
|
|
|
set buffer(buffer: ToneAudioBuffer) {
|
|
|
|
this._buffer.set(buffer);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If the buffer should loop once it's over.
|
|
|
|
*/
|
|
|
|
get loop(): boolean {
|
|
|
|
return this._source.loop;
|
|
|
|
}
|
|
|
|
set loop(loop: boolean) {
|
|
|
|
this._source.loop = loop;
|
|
|
|
if (this._sourceStarted) {
|
|
|
|
this.cancelStop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-09-14 20:39:18 +00:00
|
|
|
* Clean up.
|
2019-06-18 01:52:23 +00:00
|
|
|
*/
|
|
|
|
dispose(): this {
|
|
|
|
super.dispose();
|
|
|
|
this._source.onended = null;
|
|
|
|
this._source.disconnect();
|
|
|
|
this._buffer.dispose();
|
|
|
|
this.playbackRate.dispose();
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
}
|