Skip to content

Commit

Permalink
fix: align AudioBufferSourceNode & ConstantSourceNode implementations
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisguttandin committed Mar 28, 2018
1 parent 5d8b395 commit fc569e7
Show file tree
Hide file tree
Showing 34 changed files with 2,008 additions and 371 deletions.
3 changes: 0 additions & 3 deletions src/audio-nodes/audio-buffer-source-node.ts
Expand Up @@ -38,9 +38,6 @@ export class AudioBufferSourceNode extends NoneAudioDestinationNode<TNativeAudio

super(context, nativeNode, mergedOptions.channelCount);

// @todo Set all the other options.
// @todo this.buffer = options.buffer;

if (isOfflineAudioContext(nativeContext)) {
const audioBufferSourceNodeRenderer = new AudioBufferSourceNodeRenderer(this);

Expand Down
7 changes: 6 additions & 1 deletion src/fakers/constant-source-node.ts
Expand Up @@ -12,8 +12,13 @@ export class ConstantSourceNodeFaker {
{ offset, ...audioNodeOptions }: Partial<IConstantSourceOptions>
): INativeConstantSourceNodeFaker {
// @todo Safari does not play/loop 1 sample buffers. This should be covered by an expectation test.
const audioBuffer = unpatchedAudioContext.createBuffer(1, 2, unpatchedAudioContext.sampleRate);
const audioBufferSourceNode = createNativeAudioBufferSourceNode(unpatchedAudioContext);
/*
* @todo Edge will throw a NotSupportedError when calling createBuffer() on a closed context. That's why the audioBuffer is created
* after the audioBufferSourceNode in this case. If the context is closed createNativeAudioBufferSourceNode() will throw the
* expected error and createBuffer() never gets called.
*/
const audioBuffer = unpatchedAudioContext.createBuffer(1, 2, unpatchedAudioContext.sampleRate);
const gainNode = createNativeGainNode(unpatchedAudioContext, { ...audioNodeOptions, gain: offset });

// Bug #5: Safari does not support copyFromChannel() and copyToChannel().
Expand Down
114 changes: 104 additions & 10 deletions src/helpers/create-native-audio-buffer-source-node.ts
@@ -1,23 +1,65 @@
import { Injector } from '@angular/core';
import { INVALID_STATE_ERROR_FACTORY_PROVIDER } from '../factories/invalid-state-error';
import { assignNativeAudioNodeOptions } from '../helpers/assign-native-audio-node-options';
import { cacheTestResult } from '../helpers/cache-test-result';
import { IAudioBufferSourceOptions } from '../interfaces';
import { STOP_STOPPED_SUPPORT_TESTER_PROVIDER, StopStoppedSupportTester } from '../support-testers/stop-stopped';
import {
AUDIO_SCHEDULED_SOURCE_NODE_START_METHOD_CONSECUTIVE_CALLS_SUPPORT_TESTER_PROVIDER,
AudioScheduledSourceNodeStartMethodConsecutiveCallsSupportTester
} from '../support-testers/audio-scheduled-source-node-start-methods-consecutive-calls';
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 { TNativeAudioBufferSourceNode, TUnpatchedAudioContext, TUnpatchedOfflineAudioContext } from '../types';
import {
AUDIO_BUFFER_SOURCE_NODE_STOP_METHOD_WRAPPER_PROVIDER,
AudioBufferSourceNodeStopMethodWrapper
} from '../wrappers/audio-buffer-source-node-stop-method';
AUDIO_SCHEDULED_SOURCE_NODE_START_METHOD_CONSECUTIVE_CALLS_WRAPPER_PROVIDER,
AudioScheduledSourceNodeStartMethodConsecutiveCallsWrapper
} from '../wrappers/audio-scheduled-source-node-start-method-consecutive-calls';
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_BUFFER_SOURCE_NODE_STOP_METHOD_WRAPPER_PROVIDER,
STOP_STOPPED_SUPPORT_TESTER_PROVIDER
AUDIO_SCHEDULED_SOURCE_NODE_START_METHOD_CONSECUTIVE_CALLS_SUPPORT_TESTER_PROVIDER,
AUDIO_SCHEDULED_SOURCE_NODE_START_METHOD_CONSECUTIVE_CALLS_WRAPPER_PROVIDER,
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,
INVALID_STATE_ERROR_FACTORY_PROVIDER
]
});

const audioBufferSourceNodeStopMethodWrapper = injector.get(AudioBufferSourceNodeStopMethodWrapper);
const stopStoppedSupportTester = injector.get(StopStoppedSupportTester);
const startMethodConsecutiveCallsSupportTester = injector.get(AudioScheduledSourceNodeStartMethodConsecutiveCallsSupportTester);
const startMethodConsecutiveCallsWrapper = injector
.get<AudioScheduledSourceNodeStartMethodConsecutiveCallsWrapper>(AudioScheduledSourceNodeStartMethodConsecutiveCallsWrapper);
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 createNativeAudioBufferSourceNode = (
nativeContext: TUnpatchedAudioContext | TUnpatchedOfflineAudioContext,
Expand All @@ -27,9 +69,61 @@ export const createNativeAudioBufferSourceNode = (

assignNativeAudioNodeOptions(nativeNode, options);

// Bug #71: Edge does not allow to set the buffer to null.
if (options.buffer !== undefined && options.buffer !== null) {
nativeNode.buffer = options.buffer;
}

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

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

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

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

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

// Bug #69: Safari does allow calls to start() of an already scheduled AudioBufferSourceNode.
if (!cacheTestResult(
AudioScheduledSourceNodeStartMethodConsecutiveCallsSupportTester,
() => startMethodConsecutiveCallsSupportTester.test(nativeContext)
)) {
startMethodConsecutiveCallsWrapper.wrap(nativeNode);
}

// 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(StopStoppedSupportTester, () => stopStoppedSupportTester.test(nativeContext))) {
audioBufferSourceNodeStopMethodWrapper.wrap(nativeNode, nativeContext);
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;
Expand Down
77 changes: 63 additions & 14 deletions src/helpers/create-native-constant-source-node.ts
@@ -1,19 +1,53 @@
import { Injector } from '@angular/core';
import { INVALID_STATE_ERROR_FACTORY_PROVIDER, InvalidStateErrorFactory } from '../factories/invalid-state-error';
import { CONSTANT_SOURCE_NODE_FAKER_PROVIDER, ConstantSourceNodeFaker } from '../fakers/constant-source-node';
import { assignNativeAudioNodeOptions } from '../helpers/assign-native-audio-node-options';
import { cacheTestResult } from '../helpers/cache-test-result';
import { IConstantSourceOptions, INativeConstantSourceNode } 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_NEGATIVE_PARAMETERS_SUPPORT_TESTER_PROVIDER,
AudioScheduledSourceNodeStopMethodNegativeParametersSupportTester
} from '../support-testers/audio-scheduled-source-node-stop-methods-negative-parameters';
import {
CONSTANT_SOURCE_NODE_ACCURATE_SCHEDULING_SUPPORT_TESTER_PROVIDER,
ConstantSourceNodeAccurateSchedulingSupportTester
} from '../support-testers/constant-source-node-accurate-scheduling';
import { 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_NEGATIVE_PARAMETERS_WRAPPER_PROVIDER,
AudioScheduledSourceNodeStopMethodNegativeParametersWrapper
} from '../wrappers/audio-scheduled-source-node-stop-method-negative-parameters';
import {
CONSTANT_SOURCE_NODE_ACCURATE_SCHEDULING_WRAPPER_PROVIDER,
ConstantSourceNodeAccurateSchedulingWrapper
} from '../wrappers/constant-source-node-accurate-scheduling';

const injector = Injector.create({
providers: [
CONSTANT_SOURCE_NODE_FAKER_PROVIDER,
INVALID_STATE_ERROR_FACTORY_PROVIDER
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_NEGATIVE_PARAMETERS_SUPPORT_TESTER_PROVIDER,
AUDIO_SCHEDULED_SOURCE_NODE_STOP_METHOD_NEGATIVE_PARAMETERS_WRAPPER_PROVIDER,
CONSTANT_SOURCE_NODE_ACCURATE_SCHEDULING_SUPPORT_TESTER_PROVIDER,
CONSTANT_SOURCE_NODE_ACCURATE_SCHEDULING_WRAPPER_PROVIDER,
CONSTANT_SOURCE_NODE_FAKER_PROVIDER
]
});

const constantSourceNodeFaker = injector.get(ConstantSourceNodeFaker);
const invalidStateErrorFactory = injector.get(InvalidStateErrorFactory);
const accurateSchedulingSupportTester = injector.get(ConstantSourceNodeAccurateSchedulingSupportTester);
const accurateSchedulingWrapper = injector.get(ConstantSourceNodeAccurateSchedulingWrapper);
const startMethodNegativeParametersSupportTester = injector.get(AudioScheduledSourceNodeStartMethodNegativeParametersSupportTester);
const startMethodNegativeParametersWrapper = injector.get(AudioScheduledSourceNodeStartMethodNegativeParametersWrapper);
const stopMethodNegativeParametersSupportTester = injector.get(AudioScheduledSourceNodeStopMethodNegativeParametersSupportTester);
const stopMethodNegativeParametersWrapper = injector.get(AudioScheduledSourceNodeStopMethodNegativeParametersWrapper);

export const createNativeConstantSourceNode = (
nativeContext: TUnpatchedAudioContext | TUnpatchedOfflineAudioContext,
Expand All @@ -22,16 +56,7 @@ export const createNativeConstantSourceNode = (
// Bug #62: Edge & Safari do not support ConstantSourceNodes.
// @todo TypeScript doesn't know yet about createConstantSource().
if ((<any> nativeContext).createConstantSource === undefined) {
try {
return constantSourceNodeFaker.fake(nativeContext, options);
} catch (err) {
// @todo Edge does throw a NotSupportedError if the context is closed.
if (err.code === 9) {
throw invalidStateErrorFactory.create();
}

throw err;
}
return constantSourceNodeFaker.fake(nativeContext, options);
}

const nativeNode = <INativeConstantSourceNode> (<any> nativeContext).createConstantSource();
Expand All @@ -47,5 +72,29 @@ export const createNativeConstantSourceNode = (
nativeNode.offset.value = options.offset;
}

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

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

// Bug #70: Firefox does not schedule ConstantSourceNodes accurately.
if (!cacheTestResult(
ConstantSourceNodeAccurateSchedulingSupportTester,
() => accurateSchedulingSupportTester.test(nativeContext)
)) {
accurateSchedulingWrapper.wrap(nativeNode, nativeContext);
}

return nativeNode;
};
1 change: 1 addition & 0 deletions src/interfaces/index.ts
Expand Up @@ -49,6 +49,7 @@ export * from './native-audio-worklet-node-constructor';
export * from './native-audio-worklet-node-faker';
export * from './native-constant-source-node';
export * from './native-constant-source-node-faker';
export * from './native-constant-source-node-map';
export * from './native-iir-filter-node-faker';
export * from './offline-audio-completion-event';
export * from './offline-audio-context';
Expand Down
4 changes: 2 additions & 2 deletions src/interfaces/native-audio-worklet-node-faker.ts
@@ -1,8 +1,8 @@
import { TNativeAudioNode } from '../types';
import { INativeAudioNodeFaker } from './native-audio-node-faker';
import { INativeAudioWorkletNode } from './native-audio-worklet-node';

export interface INativeAudioWorkletNodeFaker extends INativeAudioNodeFaker, INativeAudioWorkletNode {
// @todo This does kind of implement the INativeAudioNodeFaker interface.
export interface INativeAudioWorkletNodeFaker extends INativeAudioWorkletNode {

bufferSize: number;

Expand Down
4 changes: 2 additions & 2 deletions src/interfaces/native-constant-source-node-faker.ts
@@ -1,7 +1,7 @@
import { INativeAudioNodeFaker } from './native-audio-node-faker';
import { INativeConstantSourceNode } from './native-constant-source-node';

export interface INativeConstantSourceNodeFaker extends INativeAudioNodeFaker, INativeConstantSourceNode {
// @todo This does kind of implement the INativeAudioNodeFaker interface.
export interface INativeConstantSourceNodeFaker extends INativeConstantSourceNode {

bufferSize: undefined;

Expand Down
5 changes: 5 additions & 0 deletions src/interfaces/native-constant-source-node-map.ts
@@ -0,0 +1,5 @@
export interface INativeConstantSourceNodeMap {

ended: Event;

}
16 changes: 16 additions & 0 deletions src/interfaces/native-constant-source-node.ts
@@ -1,10 +1,26 @@
import { INativeConstantSourceNodeMap } from './native-constant-source-node-map';

// @todo Since there are no native types yet they need to be crafted.
export interface INativeConstantSourceNode extends AudioNode {

readonly offset: AudioParam;

onended: ((this: INativeConstantSourceNode, event: Event) => any) | null;

addEventListener <K extends keyof INativeConstantSourceNodeMap> (
type: K,
listener: (this: OscillatorNode, ev: INativeConstantSourceNodeMap[K]) => any,
options?: boolean | AddEventListenerOptions
): void;
addEventListener (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;

removeEventListener <K extends keyof INativeConstantSourceNodeMap> (
type: K,
listener: (this: OscillatorNode, ev: INativeConstantSourceNodeMap[K]) => any,
options?: boolean | EventListenerOptions
): void;
removeEventListener (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;

start (when?: number): void;

stop (when?: number): void;
Expand Down
4 changes: 2 additions & 2 deletions src/interfaces/native-iir-filter-node-faker.ts
@@ -1,7 +1,7 @@
import { TNativeAudioNode, TNativeIIRFilterNode } from '../types';
import { INativeAudioNodeFaker } from './native-audio-node-faker';

export interface INativeIIRFilterNodeFaker extends INativeAudioNodeFaker, TNativeIIRFilterNode {
// @todo This does kind of implement the INativeAudioNodeFaker interface.
export interface INativeIIRFilterNodeFaker extends TNativeIIRFilterNode {

bufferSize: number;

Expand Down
@@ -0,0 +1,24 @@
import { TUnpatchedAudioContext, TUnpatchedOfflineAudioContext } from '../types';

export class AudioScheduledSourceNodeStartMethodConsecutiveCallsSupportTester {

public test (audioContext: TUnpatchedAudioContext | TUnpatchedOfflineAudioContext) {
const audioBuffer = audioContext.createBufferSource();

audioBuffer.start();

try {
audioBuffer.start();
} catch (err) {
return true;
}

return false;
}

}

export const AUDIO_SCHEDULED_SOURCE_NODE_START_METHOD_CONSECUTIVE_CALLS_SUPPORT_TESTER_PROVIDER = {
deps: [ ],
provide: AudioScheduledSourceNodeStartMethodConsecutiveCallsSupportTester
};
@@ -0,0 +1,22 @@
import { TUnpatchedAudioContext, TUnpatchedOfflineAudioContext } from '../types';

export class AudioScheduledSourceNodeStartMethodNegativeParametersSupportTester {

public test (audioContext: TUnpatchedAudioContext | TUnpatchedOfflineAudioContext) {
const audioBuffer = audioContext.createBufferSource();

try {
audioBuffer.start(-1);
} catch (err) {
return (err instanceof RangeError);
}

return false;
}

}

export const AUDIO_SCHEDULED_SOURCE_NODE_START_METHOD_NEGATIVE_PARAMETERS_SUPPORT_TESTER_PROVIDER = {
deps: [ ],
provide: AudioScheduledSourceNodeStartMethodNegativeParametersSupportTester
};

0 comments on commit fc569e7

Please sign in to comment.