Skip to content

Commit

Permalink
feat: support AudioWorkletNodes with multiple inputs and outputs
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisguttandin committed Mar 26, 2018
1 parent 5f6465f commit 812f36d
Show file tree
Hide file tree
Showing 31 changed files with 589 additions and 382 deletions.
39 changes: 6 additions & 33 deletions src/audio-nodes/audio-buffer-source-node.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,21 @@
import { Injector } from '@angular/core';
import { AUDIO_NODE_RENDERER_STORE } from '../globals';
import { cacheTestResult } from '../helpers/cache-test-result';
import { createNativeAudioBufferSourceNode } from '../helpers/create-native-audio-buffer-source-node';
import { getNativeContext } from '../helpers/get-native-context';
import { isOfflineAudioContext } from '../helpers/is-offline-audio-context';
import { IAudioBufferSourceNode, IAudioBufferSourceOptions, IAudioParam, IMinimalBaseAudioContext } from '../interfaces';
import { AudioBufferSourceNodeRenderer } from '../renderers/audio-buffer-source-node';
import { STOP_STOPPED_SUPPORT_TESTER_PROVIDER, StopStoppedSupportTester } from '../support-testers/stop-stopped';
import {
TChannelCountMode,
TChannelInterpretation,
TEndedEventHandler,
TNativeAudioBufferSourceNode,
TUnpatchedAudioContext,
TUnpatchedOfflineAudioContext
} from '../types';
import {
AUDIO_BUFFER_SOURCE_NODE_STOP_METHOD_WRAPPER_PROVIDER,
AudioBufferSourceNodeStopMethodWrapper
} from '../wrappers/audio-buffer-source-node-stop-method';
import { TChannelCountMode, TChannelInterpretation, TEndedEventHandler, TNativeAudioBufferSourceNode } 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_BUFFER_SOURCE_NODE_STOP_METHOD_WRAPPER_PROVIDER,
AUDIO_PARAM_WRAPPER_PROVIDER,
STOP_STOPPED_SUPPORT_TESTER_PROVIDER
AUDIO_PARAM_WRAPPER_PROVIDER
]
});

const audioBufferSourceNodeStopMethodWrapper = injector.get(AudioBufferSourceNodeStopMethodWrapper);
const audioParamWrapper = injector.get(AudioParamWrapper);
const stopStoppedSupportTester = injector.get(StopStoppedSupportTester);

const createNativeNode = (nativeContext: TUnpatchedAudioContext | TUnpatchedOfflineAudioContext) => {
const nativeNode = nativeContext.createBufferSource();

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

return nativeNode;
};

const DEFAULT_OPTIONS: IAudioBufferSourceOptions = {
buffer: null,
Expand All @@ -60,10 +33,10 @@ export class AudioBufferSourceNode extends NoneAudioDestinationNode<TNativeAudio

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

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

// @todo Set all the other options.
// @todo this.buffer = options.buffer;
Expand Down
14 changes: 10 additions & 4 deletions src/audio-nodes/audio-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,10 @@ export class AudioNode<T extends INativeAudioNodeFaker | TNativeAudioNode> exten
throw new Error('The associated nativeNode is missing.');
}

if ((<INativeAudioNodeFaker> nativeDestinationNode).input !== undefined) {
this._nativeNode.connect((<TNativeAudioNode> (<INativeAudioNodeFaker> nativeDestinationNode).input), output, input);
if ((<INativeAudioNodeFaker> nativeDestinationNode).inputs !== undefined) {
const nativeInputDestinationNode = (<TNativeAudioNode[]> (<INativeAudioNodeFaker> nativeDestinationNode).inputs)[input];

this._nativeNode.connect(nativeInputDestinationNode, output, input);
} else {
this._nativeNode.connect(nativeDestinationNode, output, input);
}
Expand Down Expand Up @@ -256,8 +258,12 @@ export class AudioNode<T extends INativeAudioNodeFaker | TNativeAudioNode> exten
throw new Error('The associated nativeNode is missing.');
}

if ((<INativeAudioNodeFaker> nativeDestinationNode).input !== undefined) {
return this._nativeNode.disconnect(<TNativeAudioNode> (<INativeAudioNodeFaker> nativeDestinationNode).input);
if ((<INativeAudioNodeFaker> nativeDestinationNode).inputs !== undefined) {
for (const input of (<TNativeAudioNode[]> (<INativeAudioNodeFaker> nativeDestinationNode).inputs)) {
this._nativeNode.disconnect(input);
}

return;
}

return this._nativeNode.disconnect(nativeDestinationNode);
Expand Down
22 changes: 12 additions & 10 deletions src/audio-nodes/audio-worklet-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,8 @@ const createChannelCount = (length: number): number[] => {
return channelCount;
};

const createNativeAudioWorkletNode = (
nativeContext: TUnpatchedAudioContext | TUnpatchedOfflineAudioContext,
name: string,
processorDefinition: undefined | IAudioWorkletProcessorConstructor,
options: IAudioWorkletNodeOptions
) => {
const sanitizedOptions: { outputChannelCount: number[] } & IAudioWorkletNodeOptions = {
const sanitizedOptions = (options: IAudioWorkletNodeOptions): { outputChannelCount: number[] } & IAudioWorkletNodeOptions => {
return {
...options,
outputChannelCount: (options.outputChannelCount !== undefined) ?
options.outputChannelCount :
Expand All @@ -98,10 +93,17 @@ const createNativeAudioWorkletNode = (
// Bug #66: The default value of processorOptions should be null, but Chrome Canary doesn't like it.
processorOptions: (options.processorOptions === null) ? { } : options.processorOptions
};
};

const createNativeAudioWorkletNode = (
nativeContext: TUnpatchedAudioContext | TUnpatchedOfflineAudioContext,
name: string,
processorDefinition: undefined | IAudioWorkletProcessorConstructor,
options: { outputChannelCount: number[] } & IAudioWorkletNodeOptions
) => {
if (nativeAudioWorkletNodeConstructor !== null) {
try {
const nativeNode = new nativeAudioWorkletNodeConstructor(nativeContext, name, sanitizedOptions);
const nativeNode = new nativeAudioWorkletNodeConstructor(nativeContext, name, options);

/*
* Bug #61: Overwriting the property accessors is necessary as long as some browsers have no native implementation to achieve a
Expand Down Expand Up @@ -138,7 +140,7 @@ const createNativeAudioWorkletNode = (
throw notSupportedErrorFactory.create();
}

return audioWorkletNodeFaker.fake(nativeContext, processorDefinition, sanitizedOptions);
return audioWorkletNodeFaker.fake(nativeContext, processorDefinition, options);
};

export class AudioWorkletNode extends NoneAudioDestinationNode<INativeAudioWorkletNode> implements IAudioWorkletNode {
Expand All @@ -147,7 +149,7 @@ export class AudioWorkletNode extends NoneAudioDestinationNode<INativeAudioWorkl

constructor (context: IMinimalBaseAudioContext, name: string, options: IAudioWorkletNodeOptions = DEFAULT_OPTIONS) {
const nativeContext = getNativeContext(context);
const mergedOptions = <IAudioWorkletNodeOptions> { ...DEFAULT_OPTIONS, ...options };
const mergedOptions = sanitizedOptions(<IAudioWorkletNodeOptions> { ...DEFAULT_OPTIONS, ...options });
const nodeNameToProcessorDefinitionMap = NODE_NAME_TO_PROCESSOR_DEFINITION_MAPS.get(nativeContext);
const processorDefinition = (nodeNameToProcessorDefinitionMap === undefined) ?
undefined :
Expand Down
30 changes: 2 additions & 28 deletions src/audio-nodes/channel-merger-node.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Injector } from '@angular/core';
import { INVALID_STATE_ERROR_FACTORY_PROVIDER } from '../factories/invalid-state-error';
import { createNativeChannelMergerNode } from '../helpers/create-native-channel-merger-node';
import { getNativeContext } from '../helpers/get-native-context';
import { isOfflineAudioContext } from '../helpers/is-offline-audio-context';
import { IChannelMergerOptions, IMinimalBaseAudioContext } from '../interfaces';
Expand All @@ -10,7 +9,6 @@ import {
TUnpatchedAudioContext,
TUnpatchedOfflineAudioContext
} from '../types';
import { CHANNEL_MERGER_NODE_WRAPPER_PROVIDER, ChannelMergerNodeWrapper } from '../wrappers/channel-merger-node';
import { NoneAudioDestinationNode } from './none-audio-destination-node';

const DEFAULT_OPTIONS: IChannelMergerOptions = {
Expand All @@ -20,37 +18,13 @@ const DEFAULT_OPTIONS: IChannelMergerOptions = {
numberOfInputs: 6
};

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

const channelMergerNodeWrapper = injector.get<ChannelMergerNodeWrapper>(ChannelMergerNodeWrapper);

const createNativeNode = (nativeContext: TUnpatchedAudioContext | TUnpatchedOfflineAudioContext, numberOfInputs: number) => {
// @todo Use this inside the AudioWorkletNodeFaker once it supports the OfflineAudioContext.
if (isOfflineAudioContext(nativeContext)) {
throw new Error('This is not yet supported.');
}

const nativeNode = nativeContext.createChannelMerger(numberOfInputs);

// Bug #15: Safari does not return the default properties.
if (nativeNode.channelCount !== 1 &&
nativeNode.channelCountMode !== 'explicit') {
channelMergerNodeWrapper.wrap(nativeContext, nativeNode);
}

// Bug #16: Firefox does not throw an error when setting a different channelCount or channelCountMode.
try {
nativeNode.channelCount = numberOfInputs;

channelMergerNodeWrapper.wrap(nativeContext, nativeNode);
} catch (err) {} // tslint:disable-line:no-empty

return nativeNode;
return createNativeChannelMergerNode(nativeContext, { numberOfInputs });
};

export class ChannelMergerNode extends NoneAudioDestinationNode<TNativeChannelMergerNode> {
Expand Down
20 changes: 2 additions & 18 deletions src/audio-nodes/channel-splitter-node.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Injector } from '@angular/core';
import { INVALID_STATE_ERROR_FACTORY_PROVIDER } from '../factories/invalid-state-error';
import { createNativeChannelSplitterNode } from '../helpers/create-native-channel-splitter-node';
import { getNativeContext } from '../helpers/get-native-context';
import { isOfflineAudioContext } from '../helpers/is-offline-audio-context';
import { IChannelSplitterOptions, IMinimalBaseAudioContext } from '../interfaces';
Expand All @@ -10,7 +9,6 @@ import {
TUnpatchedAudioContext,
TUnpatchedOfflineAudioContext
} from '../types';
import { CHANNEL_SPLITTER_NODE_WRAPPER_PROVIDER, ChannelSplitterNodeWrapper } from '../wrappers/channel-splitter-node';
import { NoneAudioDestinationNode } from './none-audio-destination-node';

const DEFAULT_OPTIONS: IChannelSplitterOptions = {
Expand All @@ -20,27 +18,13 @@ const DEFAULT_OPTIONS: IChannelSplitterOptions = {
numberOfOutputs: 6
};

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

const channelSplitterNodeWrapper = injector.get<ChannelSplitterNodeWrapper>(ChannelSplitterNodeWrapper);

const createNativeNode = (nativeContext: TUnpatchedAudioContext | TUnpatchedOfflineAudioContext, numberOfOutputs: number) => {
// @todo Use this inside the AudioWorkletNodeFaker once it supports the OfflineAudioContext.
if (isOfflineAudioContext(nativeContext)) {
throw new Error('This is not yet supported.');
}

const nativeNode = nativeContext.createChannelSplitter(numberOfOutputs);

// Bug #29 - #32: Only Chrome partially supports the spec yet.
channelSplitterNodeWrapper.wrap(nativeNode);

return nativeNode;
return createNativeChannelSplitterNode(nativeContext, { numberOfOutputs });
};

export class ChannelSplitterNode extends NoneAudioDestinationNode<TNativeChannelSplitterNode> {
Expand Down
31 changes: 2 additions & 29 deletions src/audio-nodes/gain-node.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { Injector } from '@angular/core';
import { AUDIO_NODE_RENDERER_STORE } from '../globals';
import { createNativeGainNode } from '../helpers/create-native-gain-node';
import { getNativeContext } from '../helpers/get-native-context';
import { isOfflineAudioContext } from '../helpers/is-offline-audio-context';
import { IAudioParam, IGainNode, IGainOptions, IMinimalBaseAudioContext } from '../interfaces';
import { GainNodeRenderer } from '../renderers/gain-node';
import {
TChannelCountMode,
TChannelInterpretation,
TNativeGainNode,
TUnpatchedAudioContext,
TUnpatchedOfflineAudioContext
} from '../types';
import { TChannelCountMode, TChannelInterpretation, TNativeGainNode } from '../types';
import { AUDIO_PARAM_WRAPPER_PROVIDER, AudioParamWrapper } from '../wrappers/audio-param';
import { NoneAudioDestinationNode } from './none-audio-destination-node';

Expand All @@ -29,28 +24,6 @@ const DEFAULT_OPTIONS: IGainOptions = {
gain: 1
};

const createNativeGainNode = (nativeContext: TUnpatchedAudioContext | TUnpatchedOfflineAudioContext, options: IGainOptions) => {
const nativeNode = nativeContext.createGain();

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

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

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

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

return nativeNode;
};

export class GainNode extends NoneAudioDestinationNode<TNativeGainNode> implements IGainNode {

constructor (context: IMinimalBaseAudioContext, options: Partial<IGainOptions> = DEFAULT_OPTIONS) {
Expand Down

0 comments on commit 812f36d

Please sign in to comment.