Skip to content

Commit

Permalink
feat: implement OscillatorNode for OfflineAudioContext
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisguttandin committed Mar 28, 2018
1 parent 251116b commit 09dd6e6
Show file tree
Hide file tree
Showing 11 changed files with 1,050 additions and 202 deletions.
7 changes: 1 addition & 6 deletions src/audio-contexts/audio-context.ts
Expand Up @@ -4,10 +4,9 @@ import { ChannelMergerNode } from '../audio-nodes/channel-merger-node';
import { ChannelSplitterNode } from '../audio-nodes/channel-splitter-node';
import { MediaElementAudioSourceNode } from '../audio-nodes/media-element-audio-source-node';
import { MediaStreamAudioSourceNode } from '../audio-nodes/media-stream-audio-source-node';
import { OscillatorNode } from '../audio-nodes/oscillator-node';
import { INVALID_STATE_ERROR_FACTORY_PROVIDER, InvalidStateErrorFactory } from '../factories/invalid-state-error';
import { isValidLatencyHint } from '../helpers/is-valid-latency-hint';
import { IAnalyserNode, IAudioContext, IAudioContextOptions, IOscillatorNode } from '../interfaces';
import { IAnalyserNode, IAudioContext, IAudioContextOptions } from '../interfaces';
import {
UNPATCHED_AUDIO_CONTEXT_CONSTRUCTOR_PROVIDER,
unpatchedAudioContextConstructor as nptchdDCntxtCnstrctr
Expand Down Expand Up @@ -96,10 +95,6 @@ export class AudioContext extends BaseAudioContext implements IAudioContext {
return new MediaStreamAudioSourceNode(this, { mediaStream });
}

public createOscillator (): IOscillatorNode {
return new OscillatorNode(this);
}

public close () {
// Bug #35: Firefox does not throw an error if the AudioContext was closed before.
if (this.state === 'closed') {
Expand Down
6 changes: 6 additions & 0 deletions src/audio-contexts/base-audio-context.ts
Expand Up @@ -5,6 +5,7 @@ import { BiquadFilterNode } from '../audio-nodes/biquad-filter-node';
import { ConstantSourceNode } from '../audio-nodes/constant-source-node';
import { GainNode } from '../audio-nodes/gain-node';
import { IIRFilterNode } from '../audio-nodes/iir-filter-node';
import { OscillatorNode } from '../audio-nodes/oscillator-node';
import { decodeAudioData } from '../decode-audio-data';
import {
IAudioBuffer,
Expand All @@ -14,6 +15,7 @@ import {
IBiquadFilterNode,
IGainNode,
IIIRFilterNode,
IOscillatorNode,
IWorkletOptions
} from '../interfaces';
import {
Expand Down Expand Up @@ -65,6 +67,10 @@ export class BaseAudioContext extends MinimalBaseAudioContext implements IBaseAu
return new IIRFilterNode(this, { feedback, feedforward });
}

public createOscillator (): IOscillatorNode {
return new OscillatorNode(this);
}

public decodeAudioData (
audioData: ArrayBuffer, successCallback?: TDecodeSuccessCallback, errorCallback?: TDecodeErrorCallback
): Promise<IAudioBuffer> {
Expand Down
39 changes: 19 additions & 20 deletions src/audio-nodes/oscillator-node.ts
@@ -1,25 +1,23 @@
import { Injector } from '@angular/core';
import { INVALID_STATE_ERROR_FACTORY_PROVIDER, InvalidStateErrorFactory } from '../factories/invalid-state-error';
import { AUDIO_NODE_RENDERER_STORE } from '../globals';
import { createNativeOscillatorNode } from '../helpers/create-native-oscillator-node';
import { getNativeContext } from '../helpers/get-native-context';
import { isOfflineAudioContext } from '../helpers/is-offline-audio-context';
import { IAudioParam, IMinimalBaseAudioContext, IOscillatorNode, IOscillatorOptions } from '../interfaces';
import {
TChannelCountMode,
TChannelInterpretation,
TEndedEventHandler,
TNativeOscillatorNode,
TOscillatorType,
TUnpatchedAudioContext,
TUnpatchedOfflineAudioContext
} from '../types';
import { OscillatorNodeRenderer } from '../renderers/oscillator-node';
import { TChannelCountMode, TChannelInterpretation, TEndedEventHandler, TNativeOscillatorNode, TOscillatorType } from '../types';
import { AUDIO_PARAM_WRAPPER_PROVIDER, AudioParamWrapper } from '../wrappers/audio-param';
import { NoneAudioDestinationNode } from './none-audio-destination-node';

const injector = Injector.create({
providers: [
AUDIO_PARAM_WRAPPER_PROVIDER,
INVALID_STATE_ERROR_FACTORY_PROVIDER
]
});

const audioParamWrapper = injector.get(AudioParamWrapper);
const invalidStateErrorFactory = injector.get(InvalidStateErrorFactory);

// The DEFAULT_OPTIONS are only of type Partial<IOscillatorOptions> because there is no default value for periodicWave.
Expand All @@ -32,22 +30,23 @@ const DEFAULT_OPTIONS: Partial<IOscillatorOptions> = {
type: <TOscillatorType> 'sine'
};

const createNativeNode = (nativeContext: TUnpatchedAudioContext | TUnpatchedOfflineAudioContext) => {
if (isOfflineAudioContext(nativeContext)) {
throw new Error('This is not yet supported.');
}

return nativeContext.createOscillator();
};

export class OscillatorNode extends NoneAudioDestinationNode<TNativeOscillatorNode> implements IOscillatorNode {

constructor (context: IMinimalBaseAudioContext, options: Partial<IOscillatorOptions> = DEFAULT_OPTIONS) {
const nativeContext = getNativeContext(context);
const { channelCount } = <IOscillatorOptions> { ...DEFAULT_OPTIONS, ...options };
const nativeNode = createNativeNode(nativeContext);
const mergedOptions = <IOscillatorOptions> { ...DEFAULT_OPTIONS, ...options };
const nativeNode = createNativeOscillatorNode(nativeContext, mergedOptions);

super(context, nativeNode, channelCount);
super(context, nativeNode, mergedOptions.channelCount);

if (isOfflineAudioContext(nativeContext)) {
const oscillatorNodeRenderer = new OscillatorNodeRenderer(this);

AUDIO_NODE_RENDERER_STORE.set(this, oscillatorNodeRenderer);

audioParamWrapper.wrap(nativeNode, context, 'detune');
audioParamWrapper.wrap(nativeNode, context, 'frequency');
}
}

public get detune () {
Expand Down
96 changes: 96 additions & 0 deletions src/helpers/create-native-oscillator-node.ts
@@ -0,0 +1,96 @@
import { Injector } from '@angular/core';
import { assignNativeAudioNodeOptions } from '../helpers/assign-native-audio-node-options';
import { cacheTestResult } from '../helpers/cache-test-result';
import { IOscillatorOptions } from '../interfaces';
import {
AUDIO_SCHEDULED_SOURCE_NODE_START_METHOD_NEGATIVE_PARAMETERS_SUPPORT_TESTER_PROVIDER,
AudioScheduledSourceNodeStartMethodNegativeParametersSupportTester
} from '../support-testers/audio-scheduled-source-node-start-methods-negative-parameters';
import {
AUDIO_SCHEDULED_SOURCE_NODE_STOP_METHOD_CONSECUTIVE_CALLS_SUPPORT_TESTER_PROVIDER,
AudioScheduledSourceNodeStopMethodConsecutiveCallsSupportTester
} from '../support-testers/audio-scheduled-source-node-stop-methods-consecutive-calls';
import {
AUDIO_SCHEDULED_SOURCE_NODE_STOP_METHOD_NEGATIVE_PARAMETERS_SUPPORT_TESTER_PROVIDER,
AudioScheduledSourceNodeStopMethodNegativeParametersSupportTester
} from '../support-testers/audio-scheduled-source-node-stop-methods-negative-parameters';
import { TNativeOscillatorNode, TUnpatchedAudioContext, TUnpatchedOfflineAudioContext } from '../types';
import {
AUDIO_SCHEDULED_SOURCE_NODE_START_METHOD_NEGATIVE_PARAMETERS_WRAPPER_PROVIDER,
AudioScheduledSourceNodeStartMethodNegativeParametersWrapper
} from '../wrappers/audio-scheduled-source-node-start-method-negative-parameters';
import {
AUDIO_SCHEDULED_SOURCE_NODE_STOP_METHOD_CONSECUTIVE_CALLS_WRAPPER_PROVIDER,
AudioScheduledSourceNodeStopMethodConsecutiveCallsWrapper
} from '../wrappers/audio-scheduled-source-node-stop-method-consecutive-calls';
import {
AUDIO_SCHEDULED_SOURCE_NODE_STOP_METHOD_NEGATIVE_PARAMETERS_WRAPPER_PROVIDER,
AudioScheduledSourceNodeStopMethodNegativeParametersWrapper
} from '../wrappers/audio-scheduled-source-node-stop-method-negative-parameters';

const injector = Injector.create({
providers: [
AUDIO_SCHEDULED_SOURCE_NODE_START_METHOD_NEGATIVE_PARAMETERS_SUPPORT_TESTER_PROVIDER,
AUDIO_SCHEDULED_SOURCE_NODE_START_METHOD_NEGATIVE_PARAMETERS_WRAPPER_PROVIDER,
AUDIO_SCHEDULED_SOURCE_NODE_STOP_METHOD_CONSECUTIVE_CALLS_SUPPORT_TESTER_PROVIDER,
AUDIO_SCHEDULED_SOURCE_NODE_STOP_METHOD_CONSECUTIVE_CALLS_WRAPPER_PROVIDER,
AUDIO_SCHEDULED_SOURCE_NODE_STOP_METHOD_NEGATIVE_PARAMETERS_SUPPORT_TESTER_PROVIDER,
AUDIO_SCHEDULED_SOURCE_NODE_STOP_METHOD_NEGATIVE_PARAMETERS_WRAPPER_PROVIDER
]
});

const startMethodNegativeParametersSupportTester = injector.get(AudioScheduledSourceNodeStartMethodNegativeParametersSupportTester);
const startMethodNegativeParametersWrapper = injector.get(AudioScheduledSourceNodeStartMethodNegativeParametersWrapper);
const stopMethodConsecutiveCallsSupportTester = injector.get(AudioScheduledSourceNodeStopMethodConsecutiveCallsSupportTester);
const stopMethodConsecutiveCallsWrapper = injector.get(AudioScheduledSourceNodeStopMethodConsecutiveCallsWrapper);
const stopMethodNegativeParametersSupportTester = injector.get(AudioScheduledSourceNodeStopMethodNegativeParametersSupportTester);
const stopMethodNegativeParametersWrapper = injector.get(AudioScheduledSourceNodeStopMethodNegativeParametersWrapper);

export const createNativeOscillatorNode = (
nativeContext: TUnpatchedAudioContext | TUnpatchedOfflineAudioContext,
options: Partial<IOscillatorOptions> = { }
): TNativeOscillatorNode => {
const nativeNode = nativeContext.createOscillator();

assignNativeAudioNodeOptions(nativeNode, options);

if (options.detune !== undefined) {
nativeNode.detune.value = options.detune;
}

if (options.frequency !== undefined) {
nativeNode.frequency.value = options.frequency;
}

// @todo periodicWave

if (options.type !== undefined) {
nativeNode.type = options.type;
}

// Bug #44: Only Chrome & Opera throw a RangeError yet.
if (!cacheTestResult(
AudioScheduledSourceNodeStartMethodNegativeParametersSupportTester,
() => startMethodNegativeParametersSupportTester.test(nativeContext)
)) {
startMethodNegativeParametersWrapper.wrap(nativeNode);
}

// Bug #19: Safari does not ignore calls to stop() of an already stopped AudioBufferSourceNode.
if (!cacheTestResult(
AudioScheduledSourceNodeStopMethodConsecutiveCallsSupportTester,
() => stopMethodConsecutiveCallsSupportTester.test(nativeContext)
)) {
stopMethodConsecutiveCallsWrapper.wrap(nativeNode, nativeContext);
}

// Bug #44: No browser does throw a RangeError yet.
if (!cacheTestResult(
AudioScheduledSourceNodeStopMethodNegativeParametersSupportTester,
() => stopMethodNegativeParametersSupportTester.test(nativeContext)
)) {
stopMethodNegativeParametersWrapper.wrap(nativeNode);
}

return nativeNode;
};
4 changes: 0 additions & 4 deletions src/interfaces/audio-context.ts
Expand Up @@ -4,7 +4,6 @@ import { IBaseAudioContext } from './base-audio-context';
import { IMediaElementAudioSourceNode } from './media-element-audio-source-node';
import { IMediaStreamAudioSourceNode } from './media-stream-audio-source-node';
import { IMinimalAudioContext } from './minimal-audio-context';
import { IOscillatorNode } from './oscillator-node';

export interface IAudioContext extends IBaseAudioContext, IMinimalAudioContext {

Expand All @@ -21,7 +20,4 @@ export interface IAudioContext extends IBaseAudioContext, IMinimalAudioContext {

createMediaStreamSource (mediaStream: MediaStream): IMediaStreamAudioSourceNode;

// @todo This should move into the IBaseAudioContext interface.
createOscillator (): IOscillatorNode;

}
3 changes: 3 additions & 0 deletions src/interfaces/base-audio-context.ts
Expand Up @@ -6,6 +6,7 @@ import { IConstantSourceNode } from './constant-source-node';
import { IGainNode } from './gain-node';
import { IIIRFilterNode } from './iir-filter-node';
import { IMinimalBaseAudioContext } from './minimal-base-audio-context';
import { IOscillatorNode } from './oscillator-node';

export interface IBaseAudioContext extends IMinimalBaseAudioContext {

Expand All @@ -25,6 +26,8 @@ export interface IBaseAudioContext extends IMinimalBaseAudioContext {

createIIRFilter (feedforward: number[], feedback: number[]): IIIRFilterNode;

createOscillator (): IOscillatorNode;

decodeAudioData (
audioData: ArrayBuffer,
successCallback?: TDecodeSuccessCallback,
Expand Down
53 changes: 53 additions & 0 deletions src/renderers/oscillator-node.ts
@@ -0,0 +1,53 @@
import { AUDIO_PARAM_STORE } from '../globals';
import { connectAudioParam } from '../helpers/connect-audio-param';
import { createNativeOscillatorNode } from '../helpers/create-native-oscillator-node';
import { getNativeNode } from '../helpers/get-native-node';
import { isOwnedByContext } from '../helpers/is-owned-by-context';
import { renderAutomation } from '../helpers/render-automation';
import { IAudioParam, IOscillatorNode } from '../interfaces';
import { TNativeAudioNode, TNativeAudioParam, TNativeOscillatorNode, TUnpatchedOfflineAudioContext } from '../types';
import { AudioNodeRenderer } from './audio-node';

export class OscillatorNodeRenderer extends AudioNodeRenderer {

private _nativeNode: null | TNativeOscillatorNode;

private _proxy: IOscillatorNode;

constructor (proxy: IOscillatorNode) {
super();

this._nativeNode = null;
this._proxy = proxy;
}

public async render (offlineAudioContext: TUnpatchedOfflineAudioContext): Promise<TNativeAudioNode> {
if (this._nativeNode !== null) {
return this._nativeNode;
}

this._nativeNode = <TNativeOscillatorNode> getNativeNode(this._proxy);

// If the initially used nativeNode was not constructed on the same OfflineAudioContext it needs to be created again.
if (!isOwnedByContext(this._nativeNode, offlineAudioContext)) {
const detuneAudioParam = <IAudioParam> (<any> this._nativeNode.detune);
const frequencyAudioParam = <IAudioParam> (<any> this._nativeNode.frequency);

this._nativeNode = createNativeOscillatorNode(offlineAudioContext);

await renderAutomation(offlineAudioContext, detuneAudioParam, this._nativeNode.detune);
await renderAutomation(offlineAudioContext, frequencyAudioParam, this._nativeNode.frequency);
} else {
const detuneNativeAudioParam = <TNativeAudioParam> AUDIO_PARAM_STORE.get(this._proxy.detune);
const frequencyNativeAudioParam = <TNativeAudioParam> AUDIO_PARAM_STORE.get(this._proxy.frequency);

await connectAudioParam(offlineAudioContext, <IAudioParam> (<any> this._nativeNode.detune), detuneNativeAudioParam);
await connectAudioParam(offlineAudioContext, <IAudioParam> (<any> this._nativeNode.frequency), frequencyNativeAudioParam);
}

await this._connectSources(offlineAudioContext, <TNativeAudioNode> this._nativeNode);

return <TNativeAudioNode> this._nativeNode;
}

}

0 comments on commit 09dd6e6

Please sign in to comment.