Tone.js/Tone/source/oscillator/Oscillator.ts

449 lines
12 KiB
TypeScript
Raw Normal View History

2019-10-31 19:43:16 +00:00
import { AudioRange, Degrees, Frequency, Radians, Time } from "../../core/type/Units";
import { deepEquals, optionsFromArguments } from "../../core/util/Defaults";
import { readOnly } from "../../core/util/Interface";
import { isDefined } from "../../core/util/TypeCheck";
2019-06-19 19:53:14 +00:00
import { Signal } from "../../signal/Signal";
import { Source } from "../Source";
import {
generateWaveform, ToneOscillatorConstructorOptions, ToneOscillatorInterface,
ToneOscillatorOptions, ToneOscillatorType
} from "./OscillatorInterface";
import { ToneOscillatorNode } from "./ToneOscillatorNode";
import { assertRange } from "../../core/util/Debug";
2019-12-16 21:50:07 +00:00
import { clamp } from "../../core/util/Math";
2019-09-04 23:18:44 +00:00
export { ToneOscillatorOptions, ToneOscillatorType } from "./OscillatorInterface";
2019-06-19 19:53:14 +00:00
/**
2019-09-14 20:39:18 +00:00
* Oscillator supports a number of features including
* phase rotation, multiple oscillator types (see Oscillator.type),
* and Transport syncing (see Oscillator.syncFrequency).
2019-06-19 19:53:14 +00:00
*
* @example
2019-10-25 20:54:33 +00:00
* // make and start a 440hz sine tone
* const osc = new Tone.Oscillator(440, "sine").toDestination().start();
2019-09-16 14:15:23 +00:00
* @category Source
2019-06-19 19:53:14 +00:00
*/
export class Oscillator extends Source<ToneOscillatorOptions> implements ToneOscillatorInterface {
2019-06-19 19:53:14 +00:00
2019-09-04 23:18:44 +00:00
readonly name: string = "Oscillator";
2019-06-19 19:53:14 +00:00
/**
2019-09-14 20:39:18 +00:00
* the main oscillator
2019-06-19 19:53:14 +00:00
*/
private _oscillator: ToneOscillatorNode | null = null;
/**
2019-09-14 20:39:18 +00:00
* The frequency control.
2019-06-19 19:53:14 +00:00
*/
frequency: Signal<"frequency">;
2019-06-19 19:53:14 +00:00
/**
2019-09-14 20:39:18 +00:00
* The detune control signal.
2019-06-19 19:53:14 +00:00
*/
detune: Signal<"cents">;
2019-06-19 19:53:14 +00:00
/**
2019-09-14 20:39:18 +00:00
* the periodic wave
2019-06-19 19:53:14 +00:00
*/
private _wave?: PeriodicWave;
/**
2019-09-14 20:39:18 +00:00
* The partials of the oscillator
2019-06-19 19:53:14 +00:00
*/
private _partials: number[];
/**
2019-09-14 20:39:18 +00:00
* The number of partials to limit or extend the periodic wave by
2019-06-19 19:53:14 +00:00
*/
private _partialCount: number;
/**
2019-09-14 20:39:18 +00:00
* the phase of the oscillator between 0 - 360
2019-06-19 19:53:14 +00:00
*/
private _phase!: Radians;
2019-06-19 19:53:14 +00:00
/**
2019-09-14 20:39:18 +00:00
* the type of the oscillator
2019-06-19 19:53:14 +00:00
*/
2019-08-27 15:47:52 +00:00
private _type: ToneOscillatorType;
2019-06-19 19:53:14 +00:00
2019-08-27 15:47:52 +00:00
/**
* @param frequency Starting frequency
* @param type The oscillator type. Read more about type below.
2019-08-27 15:47:52 +00:00
*/
2019-06-19 19:53:14 +00:00
constructor(frequency?: Frequency, type?: ToneOscillatorType);
2019-08-27 15:47:52 +00:00
constructor(options?: Partial<ToneOscillatorConstructorOptions>)
2019-06-19 19:53:14 +00:00
constructor() {
super(optionsFromArguments(Oscillator.getDefaults(), arguments, ["frequency", "type"]));
const options = optionsFromArguments(Oscillator.getDefaults(), arguments, ["frequency", "type"]);
this.frequency = new Signal<"frequency">({
2019-06-19 19:53:14 +00:00
context: this.context,
units: "frequency",
value: options.frequency,
});
readOnly(this, "frequency");
this.detune = new Signal<"cents">({
2019-06-19 19:53:14 +00:00
context: this.context,
units: "cents",
value: options.detune,
});
readOnly(this, "detune");
this._partials = options.partials;
this._partialCount = options.partialCount;
this._type = options.type;
if (options.partialCount && options.type !== "custom") {
2019-08-27 15:47:52 +00:00
this._type = this.baseType + options.partialCount.toString() as ToneOscillatorType;
2019-06-19 19:53:14 +00:00
}
this.phase = options.phase;
2019-06-19 19:53:14 +00:00
}
static getDefaults(): ToneOscillatorOptions {
2019-06-19 19:53:14 +00:00
return Object.assign(Source.getDefaults(), {
detune: 0,
frequency: 440,
partialCount: 0,
partials: [],
phase: 0,
2020-05-19 01:13:44 +00:00
type: "sine" as const,
});
2019-06-19 19:53:14 +00:00
}
/**
2019-09-14 20:39:18 +00:00
* start the oscillator
2019-06-19 19:53:14 +00:00
*/
protected _start(time?: Time): void {
const computedTime = this.toSeconds(time);
2019-06-19 19:53:14 +00:00
// new oscillator with previous values
const oscillator = new ToneOscillatorNode({
context: this.context,
2019-08-10 15:51:35 +00:00
onended: () => this.onstop(this),
2019-06-19 19:53:14 +00:00
});
this._oscillator = oscillator;
if (this._wave) {
this._oscillator.setPeriodicWave(this._wave);
} else {
2019-08-27 15:47:52 +00:00
this._oscillator.type = this._type as OscillatorType;
2019-06-19 19:53:14 +00:00
}
// connect the control signal to the oscillator frequency & detune
this._oscillator.connect(this.output);
this.frequency.connect(this._oscillator.frequency);
this.detune.connect(this._oscillator.detune);
// start the oscillator
this._oscillator.start(computedTime);
2019-06-19 19:53:14 +00:00
}
/**
2019-09-14 20:39:18 +00:00
* stop the oscillator
2019-06-19 19:53:14 +00:00
*/
protected _stop(time?: Time): void {
2019-08-13 22:35:07 +00:00
const computedTime = this.toSeconds(time);
2019-06-19 19:53:14 +00:00
if (this._oscillator) {
this._oscillator.stop(computedTime);
2019-06-19 19:53:14 +00:00
}
}
/**
* Restart the oscillator. Does not stop the oscillator, but instead
* just cancels any scheduled 'stop' from being invoked.
*/
protected _restart(time?: Time): this {
const computedTime = this.toSeconds(time);
this.log("restart", computedTime);
2019-06-19 19:53:14 +00:00
if (this._oscillator) {
this._oscillator.cancelStop();
}
this._state.cancel(computedTime);
2019-06-19 19:53:14 +00:00
return this;
}
/**
2019-09-14 20:39:18 +00:00
* Sync the signal to the Transport's bpm. Any changes to the transports bpm,
* will also affect the oscillators frequency.
* @example
* const osc = new Tone.Oscillator().toDestination().start();
2019-06-19 19:53:14 +00:00
* osc.frequency.value = 440;
2019-10-25 20:54:33 +00:00
* // the ratio between the bpm and the frequency will be maintained
2019-06-19 19:53:14 +00:00
* osc.syncFrequency();
2019-10-25 20:54:33 +00:00
* // double the tempo
* Tone.Transport.bpm.value *= 2;
2019-06-19 19:53:14 +00:00
* // the frequency of the oscillator is doubled to 880
*/
syncFrequency(): this {
2019-07-11 03:33:36 +00:00
this.context.transport.syncSignal(this.frequency);
2019-06-19 19:53:14 +00:00
return this;
}
/**
2019-09-14 20:39:18 +00:00
* Unsync the oscillator's frequency from the Transport.
* See Oscillator.syncFrequency
2019-06-19 19:53:14 +00:00
*/
unsyncFrequency(): this {
2019-07-11 03:33:36 +00:00
this.context.transport.unsyncSignal(this.frequency);
2019-06-19 19:53:14 +00:00
return this;
}
/**
* Cache the periodic waves to avoid having to redo computations
*/
private static _periodicWaveCache: Array<{
partials: number[];
phase: number;
type: string;
partialCount: number;
real: Float32Array;
imag: Float32Array;
wave: PeriodicWave;
}> = [];
/**
* Get a cached periodic wave. Avoids having to recompute
* the oscillator values when they have already been computed
* with the same values.
*/
private _getCachedPeriodicWave(): { real: Float32Array; imag: Float32Array; partials: number[]; wave: PeriodicWave } | undefined {
if (this._type === "custom") {
const oscProps = Oscillator._periodicWaveCache.find(description => {
return description.phase === this._phase &&
deepEquals(description.partials, this._partials);
});
return oscProps;
} else {
const oscProps = Oscillator._periodicWaveCache.find(description => {
return description.type === this._type &&
description.phase === this._phase;
});
this._partialCount = oscProps ? oscProps.partialCount : this._partialCount;
return oscProps;
}
}
2019-06-19 19:53:14 +00:00
get type(): ToneOscillatorType {
return this._type;
}
set type(type) {
this._type = type;
2019-06-19 19:53:14 +00:00
const isBasicType = ["sine", "square", "sawtooth", "triangle"].indexOf(type) !== -1;
if (this._phase === 0 && isBasicType) {
this._wave = undefined;
this._partialCount = 0;
// just go with the basic approach
if (this._oscillator !== null) {
// already tested that it's a basic type
this._oscillator.type = type as OscillatorType;
}
} else {
// first check if the value is cached
const cache = this._getCachedPeriodicWave();
if (isDefined(cache)) {
const { partials, wave } = cache;
this._wave = wave;
this._partials = partials;
if (this._oscillator !== null) {
this._oscillator.setPeriodicWave(this._wave);
}
} else {
const [real, imag] = this._getRealImaginary(type, this._phase);
const periodicWave = this.context.createPeriodicWave(real, imag);
this._wave = periodicWave;
if (this._oscillator !== null) {
this._oscillator.setPeriodicWave(this._wave);
}
// set the cache
Oscillator._periodicWaveCache.push({
imag,
partialCount: this._partialCount,
partials: this._partials,
phase: this._phase,
real,
type: this._type,
wave: this._wave,
});
if (Oscillator._periodicWaveCache.length > 100) {
Oscillator._periodicWaveCache.shift();
}
2019-06-19 19:53:14 +00:00
}
}
}
get baseType(): OscillatorType {
2019-08-27 15:47:52 +00:00
return (this._type as string).replace(this.partialCount.toString(), "") as OscillatorType;
2019-06-19 19:53:14 +00:00
}
set baseType(baseType) {
2019-06-19 19:53:14 +00:00
if (this.partialCount && this._type !== "custom" && baseType !== "custom") {
this.type = baseType + this.partialCount as ToneOscillatorType;
2019-06-19 19:53:14 +00:00
} else {
this.type = baseType;
}
}
get partialCount(): number {
return this._partialCount;
}
set partialCount(p) {
assertRange(p, 0);
2019-06-19 19:53:14 +00:00
let type = this._type;
const partial = /^(sine|triangle|square|sawtooth)(\d+)$/.exec(this._type);
if (partial) {
2019-08-27 15:47:52 +00:00
type = partial[1] as OscillatorType;
2019-06-19 19:53:14 +00:00
}
if (this._type !== "custom") {
if (p === 0) {
this.type = type;
} else {
this.type = type + p.toString() as ToneOscillatorType;
2019-06-19 19:53:14 +00:00
}
} else {
// extend or shorten the partials array
const fullPartials = new Float32Array(p);
// copy over the partials array
this._partials.forEach((v, i) => fullPartials[i] = v);
this._partials = Array.from(fullPartials);
this.type = this._type;
2019-06-19 19:53:14 +00:00
}
}
/**
* Returns the real and imaginary components based
* on the oscillator type.
* @returns [real: Float32Array, imaginary: Float32Array]
2019-06-19 19:53:14 +00:00
*/
private _getRealImaginary(type: ToneOscillatorType, phase: Radians): Float32Array[] {
const fftSize = 4096;
let periodicWaveSize = fftSize / 2;
const real = new Float32Array(periodicWaveSize);
const imag = new Float32Array(periodicWaveSize);
let partialCount = 1;
if (type === "custom") {
partialCount = this._partials.length + 1;
this._partialCount = this._partials.length;
periodicWaveSize = partialCount;
// if the partial count is 0, don't bother doing any computation
2019-09-16 03:32:40 +00:00
if (this._partials.length === 0) {
return [real, imag];
}
2019-06-19 19:53:14 +00:00
} else {
const partial = /^(sine|triangle|square|sawtooth)(\d+)$/.exec(type);
if (partial) {
partialCount = parseInt(partial[2], 10) + 1;
this._partialCount = parseInt(partial[2], 10);
type = partial[1] as ToneOscillatorType;
2019-06-19 19:53:14 +00:00
partialCount = Math.max(partialCount, 2);
periodicWaveSize = partialCount;
} else {
this._partialCount = 0;
}
this._partials = [];
}
for (let n = 1; n < periodicWaveSize; ++n) {
const piFactor = 2 / (n * Math.PI);
let b;
switch (type) {
case "sine":
b = (n <= partialCount) ? 1 : 0;
this._partials[n - 1] = b;
break;
case "square":
b = (n & 1) ? 2 * piFactor : 0;
this._partials[n - 1] = b;
break;
case "sawtooth":
2019-06-19 19:53:14 +00:00
b = piFactor * ((n & 1) ? 1 : -1);
this._partials[n - 1] = b;
break;
case "triangle":
if (n & 1) {
b = 2 * (piFactor * piFactor) * ((((n - 1) >> 1) & 1) ? -1 : 1);
} else {
b = 0;
}
this._partials[n - 1] = b;
break;
case "custom":
b = this._partials[n - 1];
break;
default:
throw new TypeError("Oscillator: invalid type: " + type);
}
if (b !== 0) {
real[n] = -b * Math.sin(phase * n);
imag[n] = b * Math.cos(phase * n);
} else {
real[n] = 0;
imag[n] = 0;
}
}
return [real, imag];
}
/**
2019-09-14 20:39:18 +00:00
* Compute the inverse FFT for a given phase.
2019-06-19 19:53:14 +00:00
*/
private _inverseFFT(real: Float32Array, imag: Float32Array, phase: Radians): number {
let sum = 0;
const len = real.length;
for (let i = 0; i < len; i++) {
sum += real[i] * Math.cos(i * phase) + imag[i] * Math.sin(i * phase);
}
return sum;
}
/**
* Returns the initial value of the oscillator when stopped.
* E.g. a "sine" oscillator with phase = 90 would return an initial value of -1.
2019-06-19 19:53:14 +00:00
*/
getInitialValue(): AudioRange {
2019-06-19 19:53:14 +00:00
const [real, imag] = this._getRealImaginary(this._type, 0);
let maxValue = 0;
const twoPi = Math.PI * 2;
const testPositions = 32;
// check for peaks in 16 places
for (let i = 0; i < testPositions; i++) {
maxValue = Math.max(this._inverseFFT(real, imag, (i / testPositions) * twoPi), maxValue);
2019-06-19 19:53:14 +00:00
}
2019-12-16 21:50:07 +00:00
return clamp(-this._inverseFFT(real, imag, this._phase) / maxValue, -1, 1);
2019-06-19 19:53:14 +00:00
}
get partials(): number[] {
return this._partials.slice(0, this.partialCount);
2019-06-19 19:53:14 +00:00
}
set partials(partials) {
2019-06-19 19:53:14 +00:00
this._partials = partials;
this._partialCount = this._partials.length;
2019-07-17 16:55:34 +00:00
if (partials.length) {
this.type = "custom";
}
2019-06-19 19:53:14 +00:00
}
get phase(): Degrees {
return this._phase * (180 / Math.PI);
}
set phase(phase) {
2019-06-19 19:53:14 +00:00
this._phase = phase * Math.PI / 180;
// reset the type
this.type = this._type;
}
2019-11-17 18:09:19 +00:00
async asArray(length = 1024): Promise<Float32Array> {
return generateWaveform(this, length);
}
2019-06-19 19:53:14 +00:00
dispose(): this {
super.dispose();
if (this._oscillator !== null) {
this._oscillator.dispose();
}
this._wave = undefined;
this.frequency.dispose();
this.detune.dispose();
return this;
}
}