diff --git a/src/audio-nodes/audio-buffer-source-node.ts b/src/audio-nodes/audio-buffer-source-node.ts index c827f6ab4..35c068bdd 100644 --- a/src/audio-nodes/audio-buffer-source-node.ts +++ b/src/audio-nodes/audio-buffer-source-node.ts @@ -38,9 +38,6 @@ export class AudioBufferSourceNode extends NoneAudioDestinationNode ): 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(). diff --git a/src/helpers/create-native-audio-buffer-source-node.ts b/src/helpers/create-native-audio-buffer-source-node.ts index 9c5c9ddf5..fd5f1b411 100644 --- a/src/helpers/create-native-audio-buffer-source-node.ts +++ b/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); +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, @@ -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; diff --git a/src/helpers/create-native-constant-source-node.ts b/src/helpers/create-native-constant-source-node.ts index ab1daace8..413a172a5 100644 --- a/src/helpers/create-native-constant-source-node.ts +++ b/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, @@ -22,16 +56,7 @@ export const createNativeConstantSourceNode = ( // Bug #62: Edge & Safari do not support ConstantSourceNodes. // @todo TypeScript doesn't know yet about createConstantSource(). if (( 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 = ( nativeContext).createConstantSource(); @@ -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; }; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 904b3b81a..4b8e72f9a 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -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'; diff --git a/src/interfaces/native-audio-worklet-node-faker.ts b/src/interfaces/native-audio-worklet-node-faker.ts index 02956a9f9..27b52dea2 100644 --- a/src/interfaces/native-audio-worklet-node-faker.ts +++ b/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; diff --git a/src/interfaces/native-constant-source-node-faker.ts b/src/interfaces/native-constant-source-node-faker.ts index a66ca6632..8d5f6b7a0 100644 --- a/src/interfaces/native-constant-source-node-faker.ts +++ b/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; diff --git a/src/interfaces/native-constant-source-node-map.ts b/src/interfaces/native-constant-source-node-map.ts new file mode 100644 index 000000000..01fff6d82 --- /dev/null +++ b/src/interfaces/native-constant-source-node-map.ts @@ -0,0 +1,5 @@ +export interface INativeConstantSourceNodeMap { + + ended: Event; + +} diff --git a/src/interfaces/native-constant-source-node.ts b/src/interfaces/native-constant-source-node.ts index bb02e006a..e28a76c24 100644 --- a/src/interfaces/native-constant-source-node.ts +++ b/src/interfaces/native-constant-source-node.ts @@ -1,3 +1,5 @@ +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 { @@ -5,6 +7,20 @@ export interface INativeConstantSourceNode extends AudioNode { onended: ((this: INativeConstantSourceNode, event: Event) => any) | null; + addEventListener ( + type: K, + listener: (this: OscillatorNode, ev: INativeConstantSourceNodeMap[K]) => any, + options?: boolean | AddEventListenerOptions + ): void; + addEventListener (type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + + removeEventListener ( + 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; diff --git a/src/interfaces/native-iir-filter-node-faker.ts b/src/interfaces/native-iir-filter-node-faker.ts index b8cb6b249..12e9f2b5e 100644 --- a/src/interfaces/native-iir-filter-node-faker.ts +++ b/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; diff --git a/src/support-testers/audio-scheduled-source-node-start-methods-consecutive-calls.ts b/src/support-testers/audio-scheduled-source-node-start-methods-consecutive-calls.ts new file mode 100644 index 000000000..2712dc9a9 --- /dev/null +++ b/src/support-testers/audio-scheduled-source-node-start-methods-consecutive-calls.ts @@ -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 +}; diff --git a/src/support-testers/audio-scheduled-source-node-start-methods-negative-parameters.ts b/src/support-testers/audio-scheduled-source-node-start-methods-negative-parameters.ts new file mode 100644 index 000000000..a3710d801 --- /dev/null +++ b/src/support-testers/audio-scheduled-source-node-start-methods-negative-parameters.ts @@ -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 +}; diff --git a/src/support-testers/stop-stopped.ts b/src/support-testers/audio-scheduled-source-node-stop-methods-consecutive-calls.ts similarity index 68% rename from src/support-testers/stop-stopped.ts rename to src/support-testers/audio-scheduled-source-node-stop-methods-consecutive-calls.ts index bbec11f8a..c06458b56 100644 --- a/src/support-testers/stop-stopped.ts +++ b/src/support-testers/audio-scheduled-source-node-stop-methods-consecutive-calls.ts @@ -1,6 +1,6 @@ import { TUnpatchedAudioContext, TUnpatchedOfflineAudioContext } from '../types'; -export class StopStoppedSupportTester { +export class AudioScheduledSourceNodeStopMethodConsecutiveCallsSupportTester { public test (audioContext: TUnpatchedAudioContext | TUnpatchedOfflineAudioContext) { const audioBuffer = audioContext.createBuffer(1, 1, 44100); @@ -22,4 +22,7 @@ export class StopStoppedSupportTester { } -export const STOP_STOPPED_SUPPORT_TESTER_PROVIDER = { deps: [ ], provide: StopStoppedSupportTester }; +export const AUDIO_SCHEDULED_SOURCE_NODE_STOP_METHOD_CONSECUTIVE_CALLS_SUPPORT_TESTER_PROVIDER = { + deps: [ ], + provide: AudioScheduledSourceNodeStopMethodConsecutiveCallsSupportTester +}; diff --git a/src/support-testers/audio-scheduled-source-node-stop-methods-negative-parameters.ts b/src/support-testers/audio-scheduled-source-node-stop-methods-negative-parameters.ts new file mode 100644 index 000000000..c36e297ea --- /dev/null +++ b/src/support-testers/audio-scheduled-source-node-stop-methods-negative-parameters.ts @@ -0,0 +1,22 @@ +import { TUnpatchedAudioContext, TUnpatchedOfflineAudioContext } from '../types'; + +export class AudioScheduledSourceNodeStopMethodNegativeParametersSupportTester { + + public test (audioContext: TUnpatchedAudioContext | TUnpatchedOfflineAudioContext) { + const audioBuffer = audioContext.createBufferSource(); + + try { + audioBuffer.stop(-1); + } catch (err) { + return (err instanceof RangeError); + } + + return false; + } + +} + +export const AUDIO_SCHEDULED_SOURCE_NODE_STOP_METHOD_NEGATIVE_PARAMETERS_SUPPORT_TESTER_PROVIDER = { + deps: [ ], + provide: AudioScheduledSourceNodeStopMethodNegativeParametersSupportTester +}; diff --git a/src/support-testers/connecting.ts b/src/support-testers/connecting.ts index 2b2927e8e..0859adef7 100644 --- a/src/support-testers/connecting.ts +++ b/src/support-testers/connecting.ts @@ -23,6 +23,8 @@ export class ConnectingSupportTester { analyserNode.connect(anotherAudioContext.destination); } catch (err) { return err.code === 15; + } finally { + anotherAudioContext.startRendering(); } return false; diff --git a/src/support-testers/constant-source-node-accurate-scheduling.ts b/src/support-testers/constant-source-node-accurate-scheduling.ts new file mode 100644 index 000000000..c525374d4 --- /dev/null +++ b/src/support-testers/constant-source-node-accurate-scheduling.ts @@ -0,0 +1,21 @@ +import { TUnpatchedAudioContext, TUnpatchedOfflineAudioContext } from '../types'; + +export class ConstantSourceNodeAccurateSchedulingSupportTester { + + public test (audioContext: TUnpatchedAudioContext | TUnpatchedOfflineAudioContext) { + // @todo TypeScript doesn't know yet about createConstantSource(). + const constantSourceNode = ( audioContext).createConstantSource(); + + /* + * @todo This is using bug #67 to detect bug #70. That works because both bugs are unique to the implementation of Firefox right + * now, but it could probably be done in a better way. + */ + return (constantSourceNode.channelCount !== 1); + } + +} + +export const CONSTANT_SOURCE_NODE_ACCURATE_SCHEDULING_SUPPORT_TESTER_PROVIDER = { + deps: [ ], + provide: ConstantSourceNodeAccurateSchedulingSupportTester +}; diff --git a/src/support-testers/decode-audio-data-type-error.ts b/src/support-testers/decode-audio-data-type-error.ts index ea2e52b93..cc88422c7 100644 --- a/src/support-testers/decode-audio-data-type-error.ts +++ b/src/support-testers/decode-audio-data-type-error.ts @@ -18,20 +18,16 @@ export class DecodeAudioDataTypeErrorSupportTester { return Promise.resolve(false); } - const audioContext = new this._unpatchedOfflineAudioContextConstructor(1, 1, 44100); + const offlineAudioContext = new this._unpatchedOfflineAudioContextConstructor(1, 1, 44100); // Bug #21: Safari does not support promises yet. return new Promise((resolve) => { - audioContext + offlineAudioContext // Bug #1: Safari requires a successCallback. .decodeAudioData( null, () => { // Ignore the success callback. }, (err) => { - audioContext - .close() - .catch(() => { - // Ignore errors. - }); + offlineAudioContext.startRendering(); resolve(err instanceof TypeError); }) diff --git a/src/wrappers/audio-buffer-source-node-stop-method.ts b/src/wrappers/audio-buffer-source-node-stop-method.ts deleted file mode 100644 index c25896370..000000000 --- a/src/wrappers/audio-buffer-source-node-stop-method.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { createNativeGainNode } from '../helpers/create-native-gain-node'; -import { - TNativeAudioBufferSourceNode, - TNativeAudioNode, - TNativeAudioParam, - TUnpatchedAudioContext, - TUnpatchedOfflineAudioContext -} from '../types'; - -export class AudioBufferSourceNodeStopMethodWrapper { - - public wrap ( - audioBufferSourceNode: TNativeAudioBufferSourceNode, audioContext: TUnpatchedAudioContext | TUnpatchedOfflineAudioContext - ) { - const gainNode = createNativeGainNode(audioContext); - - audioBufferSourceNode.connect(gainNode); - - const disconnectGainNode = () => { - audioBufferSourceNode.disconnect(gainNode); - audioBufferSourceNode.removeEventListener('ended', disconnectGainNode); - }; - - audioBufferSourceNode.addEventListener('ended', disconnectGainNode); - - audioBufferSourceNode.connect = ((destination: TNativeAudioNode | TNativeAudioParam, output = 0, input = 0) => { - if (destination instanceof AudioNode) { - gainNode.connect.call(gainNode, destination, output, input); - - // Bug #11: Safari does not support chaining yet. - return destination; - } - - // @todo This return statement is necessary to satisfy TypeScript. - return gainNode.connect.call(gainNode, destination, output); - }); - - audioBufferSourceNode.disconnect = function () { - gainNode.disconnect.apply(gainNode, arguments); - }; - - audioBufferSourceNode.stop = ((stop) => { - let isStopped = false; - - return (when = 0) => { - if (isStopped) { - try { - stop.call(audioBufferSourceNode, when); - } catch (err) { - gainNode.gain.setValueAtTime(0, when); - } - } else { - stop.call(audioBufferSourceNode, when); - - isStopped = true; - } - }; - })(audioBufferSourceNode.stop); - } - -} - -export const AUDIO_BUFFER_SOURCE_NODE_STOP_METHOD_WRAPPER_PROVIDER = { deps: [ ], provide: AudioBufferSourceNodeStopMethodWrapper }; diff --git a/src/wrappers/audio-scheduled-source-node-start-method-consecutive-calls.ts b/src/wrappers/audio-scheduled-source-node-start-method-consecutive-calls.ts new file mode 100644 index 000000000..308dac332 --- /dev/null +++ b/src/wrappers/audio-scheduled-source-node-start-method-consecutive-calls.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { InvalidStateErrorFactory } from '../factories/invalid-state-error'; +import { INativeConstantSourceNode } from '../interfaces'; +import { TNativeAudioBufferSourceNode, TNativeOscillatorNode } from '../types'; + +@Injectable() +export class AudioScheduledSourceNodeStartMethodConsecutiveCallsWrapper { + + constructor (private _invalidStateErrorFactory: InvalidStateErrorFactory) { } + + public wrap (audioScheduledSourceNode: TNativeAudioBufferSourceNode | INativeConstantSourceNode | TNativeOscillatorNode) { + audioScheduledSourceNode.start = ((start) => { + let isScheduled = false; + + return (when = 0, offset = 0, duration?: number) => { + if (isScheduled) { + throw this._invalidStateErrorFactory.create(); + } + + start.call(audioScheduledSourceNode, when, offset, duration); + + isScheduled = true; + }; + })(audioScheduledSourceNode.start); + } + +} + +export const AUDIO_SCHEDULED_SOURCE_NODE_START_METHOD_CONSECUTIVE_CALLS_WRAPPER_PROVIDER = { + deps: [ InvalidStateErrorFactory ], + provide: AudioScheduledSourceNodeStartMethodConsecutiveCallsWrapper +}; diff --git a/src/wrappers/audio-scheduled-source-node-start-method-negative-parameters.ts b/src/wrappers/audio-scheduled-source-node-start-method-negative-parameters.ts new file mode 100644 index 000000000..4a7713366 --- /dev/null +++ b/src/wrappers/audio-scheduled-source-node-start-method-negative-parameters.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { INativeConstantSourceNode } from '../interfaces'; +import { TNativeAudioBufferSourceNode, TNativeOscillatorNode } from '../types'; + +@Injectable() +export class AudioScheduledSourceNodeStartMethodNegativeParametersWrapper { + + public wrap (audioScheduledSourceNode: TNativeAudioBufferSourceNode | INativeConstantSourceNode | TNativeOscillatorNode) { + audioScheduledSourceNode.start = ((start) => { + return (when = 0, offset = 0, duration?: number) => { + if ((typeof duration === 'number' && duration < 0) || offset < 0 || when < 0) { + throw new RangeError("The parameters can't be negative."); + } + + start.call(audioScheduledSourceNode, when, offset, duration); + }; + })(audioScheduledSourceNode.start); + } + +} + +export const AUDIO_SCHEDULED_SOURCE_NODE_START_METHOD_NEGATIVE_PARAMETERS_WRAPPER_PROVIDER = { + deps: [ ], + provide: AudioScheduledSourceNodeStartMethodNegativeParametersWrapper +}; diff --git a/src/wrappers/audio-scheduled-source-node-stop-method-consecutive-calls.ts b/src/wrappers/audio-scheduled-source-node-stop-method-consecutive-calls.ts new file mode 100644 index 000000000..8836cc489 --- /dev/null +++ b/src/wrappers/audio-scheduled-source-node-stop-method-consecutive-calls.ts @@ -0,0 +1,71 @@ +import { createNativeGainNode } from '../helpers/create-native-gain-node'; +import { INativeConstantSourceNode } from '../interfaces'; +import { + TNativeAudioBufferSourceNode, + TNativeAudioNode, + TNativeAudioParam, + TNativeOscillatorNode, + TUnpatchedAudioContext, + TUnpatchedOfflineAudioContext +} from '../types'; + +export class AudioScheduledSourceNodeStopMethodConsecutiveCallsWrapper { + + public wrap ( + audioScheduledSourceNode: TNativeAudioBufferSourceNode | INativeConstantSourceNode | TNativeOscillatorNode, + audioContext: TUnpatchedAudioContext | TUnpatchedOfflineAudioContext + ) { + const gainNode = createNativeGainNode(audioContext); + + audioScheduledSourceNode.connect(gainNode); + + const disconnectGainNode = ((disconnect) => { + return () => { + disconnect.call(audioScheduledSourceNode, gainNode); + audioScheduledSourceNode.removeEventListener('ended', disconnectGainNode); + }; + })(audioScheduledSourceNode.disconnect); + + audioScheduledSourceNode.addEventListener('ended', disconnectGainNode); + + audioScheduledSourceNode.connect = ((destination: TNativeAudioNode | TNativeAudioParam, output = 0, input = 0) => { + if (destination instanceof AudioNode) { + gainNode.connect.call(gainNode, destination, output, input); + + // Bug #11: Safari does not support chaining yet. + return destination; + } + + // @todo This return statement is necessary to satisfy TypeScript. + return gainNode.connect.call(gainNode, destination, output); + }); + + audioScheduledSourceNode.disconnect = function () { + gainNode.disconnect.apply(gainNode, arguments); + }; + + audioScheduledSourceNode.stop = ((stop) => { + let isStopped = false; + + return (when = 0) => { + if (isStopped) { + try { + stop.call(audioScheduledSourceNode, when); + } catch (err) { + gainNode.gain.setValueAtTime(0, when); + } + } else { + stop.call(audioScheduledSourceNode, when); + + isStopped = true; + } + }; + })(audioScheduledSourceNode.stop); + } + +} + +export const AUDIO_SCHEDULED_SOURCE_NODE_STOP_METHOD_CONSECUTIVE_CALLS_WRAPPER_PROVIDER = { + deps: [ ], + provide: AudioScheduledSourceNodeStopMethodConsecutiveCallsWrapper +}; diff --git a/src/wrappers/audio-scheduled-source-node-stop-method-negative-parameters.ts b/src/wrappers/audio-scheduled-source-node-stop-method-negative-parameters.ts new file mode 100644 index 000000000..75f9de520 --- /dev/null +++ b/src/wrappers/audio-scheduled-source-node-stop-method-negative-parameters.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { INativeConstantSourceNode } from '../interfaces'; +import { TNativeAudioBufferSourceNode, TNativeOscillatorNode } from '../types'; + +@Injectable() +export class AudioScheduledSourceNodeStopMethodNegativeParametersWrapper { + + public wrap (audioScheduledSourceNode: TNativeAudioBufferSourceNode | INativeConstantSourceNode | TNativeOscillatorNode) { + audioScheduledSourceNode.stop = ((stop) => { + return (when = 0) => { + if (when < 0) { + throw new RangeError("The parameter can't be negative."); + } + + stop.call(audioScheduledSourceNode, when); + }; + })(audioScheduledSourceNode.stop); + } + +} + +export const AUDIO_SCHEDULED_SOURCE_NODE_STOP_METHOD_NEGATIVE_PARAMETERS_WRAPPER_PROVIDER = { + deps: [ ], + provide: AudioScheduledSourceNodeStopMethodNegativeParametersWrapper +}; diff --git a/src/wrappers/channel-merger-node.ts b/src/wrappers/channel-merger-node.ts index d98c19f58..53fa17478 100644 --- a/src/wrappers/channel-merger-node.ts +++ b/src/wrappers/channel-merger-node.ts @@ -1,6 +1,5 @@ import { Injectable } from '@angular/core'; import { InvalidStateErrorFactory } from '../factories/invalid-state-error'; -import { createNativeAudioBufferSourceNode } from '../helpers/create-native-audio-buffer-source-node'; import { TUnpatchedAudioContext, TUnpatchedOfflineAudioContext } from '../types'; @Injectable() @@ -9,7 +8,7 @@ export class ChannelMergerNodeWrapper { constructor (private _invalidStateErrorFactory: InvalidStateErrorFactory) { } public wrap (audioContext: TUnpatchedAudioContext | TUnpatchedOfflineAudioContext, channelMergerNode: ChannelMergerNode) { - const audioBufferSourceNode = createNativeAudioBufferSourceNode(audioContext); + const audioBufferSourceNode = audioContext.createBufferSource(); channelMergerNode.channelCount = 1; channelMergerNode.channelCountMode = 'explicit'; diff --git a/src/wrappers/channel-splitter-node.ts b/src/wrappers/channel-splitter-node.ts index 612350a02..f48b97b84 100644 --- a/src/wrappers/channel-splitter-node.ts +++ b/src/wrappers/channel-splitter-node.ts @@ -26,7 +26,7 @@ export class ChannelSplitterNodeWrapper { } }); - // Bug #31: Only Chrome has the correct channelInterpretation. + // Bug #31: Only Chrome & Opera have the correct channelInterpretation. Object.defineProperty(channelSplitterNode, 'channelInterpretation', { get: () => 'discrete', set: () => { diff --git a/src/wrappers/constant-source-node-accurate-scheduling.ts b/src/wrappers/constant-source-node-accurate-scheduling.ts new file mode 100644 index 000000000..dc3e189e8 --- /dev/null +++ b/src/wrappers/constant-source-node-accurate-scheduling.ts @@ -0,0 +1,78 @@ +import { createNativeGainNode } from '../helpers/create-native-gain-node'; +import { INativeConstantSourceNode } from '../interfaces'; +import { TNativeAudioNode, TNativeAudioParam, TUnpatchedAudioContext, TUnpatchedOfflineAudioContext } from '../types'; + +export class ConstantSourceNodeAccurateSchedulingWrapper { + + public wrap ( + constantSourceNode: INativeConstantSourceNode, + audioContext: TUnpatchedAudioContext | TUnpatchedOfflineAudioContext + ) { + const gainNode = createNativeGainNode(audioContext); + + constantSourceNode.connect(gainNode); + + const disconnectGainNode = ((disconnect) => { + return () => { + disconnect.call(constantSourceNode, gainNode); + constantSourceNode.removeEventListener('ended', disconnectGainNode); + }; + })(constantSourceNode.disconnect); + + constantSourceNode.addEventListener('ended', disconnectGainNode); + + constantSourceNode.connect = ((destination: TNativeAudioNode | TNativeAudioParam, output = 0, input = 0) => { + if (destination instanceof AudioNode) { + // Bug #11: Safari does not support chaining yet, but that wrapper should not be used in Safari. + return gainNode.connect.call(gainNode, destination, output, input); + } + + // @todo This return statement is necessary to satisfy TypeScript. + return gainNode.connect.call(gainNode, destination, output); + }); + + constantSourceNode.disconnect = function () { + gainNode.disconnect.apply(gainNode, arguments); + }; + + let startTime = 0; + let stopTime: null | number = null; + + const scheduleEnvelope = () => { + gainNode.gain.cancelScheduledValues(0); + gainNode.gain.setValueAtTime(0, 0); + + if (stopTime === null || startTime < stopTime) { + gainNode.gain.setValueAtTime(1, startTime); + } + + if (stopTime !== null && startTime < stopTime) { + gainNode.gain.setValueAtTime(0, stopTime); + } + }; + + constantSourceNode.start = ((start) => { + return (when = 0) => { + start.call(constantSourceNode, when); + startTime = when; + + scheduleEnvelope(); + }; + })(constantSourceNode.start); + + constantSourceNode.stop = ((stop) => { + return (when = 0) => { + stop.call(constantSourceNode, when); + stopTime = when; + + scheduleEnvelope(); + }; + })(constantSourceNode.stop); + } + +} + +export const CONSTANT_SOURCE_NODE_ACCURATE_SCHEDULING_WRAPPER_PROVIDER = { + deps: [ ], + provide: ConstantSourceNodeAccurateSchedulingWrapper +}; diff --git a/test/expectation/edge/audio-context-constructor.js b/test/expectation/edge/audio-context-constructor.js index 36bfe44dc..e86b33339 100644 --- a/test/expectation/edge/audio-context-constructor.js +++ b/test/expectation/edge/audio-context-constructor.js @@ -130,6 +130,20 @@ describe('audioContextConstructor', () => { describe('createBufferSource()', () => { + describe('buffer', () => { + + // bug #71 + + it('should throw a DOMException', () => { + const bufferSourceNode = audioContext.createBufferSource(); + + expect(() => { + bufferSourceNode.buffer = null; + }).to.throw('TypeMismatchError'); + }); + + }); + describe('playbackRate', () => { // bug #45 diff --git a/test/expectation/firefox/any/audio-context-constructor.js b/test/expectation/firefox/any/audio-context-constructor.js index 0386e7c9c..d45da0a8d 100644 --- a/test/expectation/firefox/any/audio-context-constructor.js +++ b/test/expectation/firefox/any/audio-context-constructor.js @@ -225,12 +225,40 @@ describe('audioContextConstructor', () => { describe('createConstantSource()', () => { - // bug #67 + describe('channelCount()', () => { - it('should have a channelCount of 1', () => { - const constantSourceNode = audioContext.createConstantSource(); + // bug #67 + + it('should have a channelCount of 1', () => { + const constantSourceNode = audioContext.createConstantSource(); + + expect(constantSourceNode.channelCount).to.equal(1); + }); + + }); + + describe('start()', () => { + + // bug #44 + + it('should throw a DOMException', () => { + const constantSourceNode = audioContext.createConstantSource(); + + expect(() => constantSourceNode.start(-1)).to.throw(DOMException); + }); + + }); + + describe('stop()', () => { + + // bug #44 + + it('should throw a DOMException', () => { + const constantSourceNode = audioContext.createConstantSource(); + + expect(() => constantSourceNode.stop(-1)).to.throw(DOMException); + }); - expect(constantSourceNode.channelCount).to.equal(1); }); }); diff --git a/test/expectation/firefox/any/offline-audio-context-constructor.js b/test/expectation/firefox/any/offline-audio-context-constructor.js index 37bbd617b..45a02ef11 100644 --- a/test/expectation/firefox/any/offline-audio-context-constructor.js +++ b/test/expectation/firefox/any/offline-audio-context-constructor.js @@ -142,12 +142,79 @@ describe('offlineAudioContextConstructor', () => { describe('createConstantSource()', () => { - // bug #67 + describe('channelCount', () => { - it('should have a channelCount of 1', () => { - const constantSourceNode = offlineAudioContext.createConstantSource(); + // bug #67 + + it('should have a channelCount of 1', () => { + const constantSourceNode = offlineAudioContext.createConstantSource(); + + expect(constantSourceNode.channelCount).to.equal(1); + }); + + }); + + describe('start()', () => { + + // bug #44 + + it('should throw a DOMException', () => { + const constantSourceNode = offlineAudioContext.createConstantSource(); + + expect(() => constantSourceNode.start(-1)).to.throw(DOMException); + }); + + // bug #70 + + it('should start it with a maximum accurary of 128 samples', () => { + const constantSourceNode = offlineAudioContext.createConstantSource(); + + constantSourceNode.connect(offlineAudioContext.destination); + constantSourceNode.start(127 / offlineAudioContext.sampleRate); + + return offlineAudioContext + .startRendering() + .then((buffer) => { + const channelData = new Float32Array(5); + + buffer.copyFromChannel(channelData, 0, 0); + + expect(Array.from(channelData)).to.deep.equal([ 1, 1, 1, 1, 1 ]); + }); + }); + + }); + + describe('stop()', () => { + + // bug #44 + + it('should throw a DOMException', () => { + const constantSourceNode = offlineAudioContext.createConstantSource(); + + expect(() => constantSourceNode.stop(-1)).to.throw(DOMException); + }); + + // bug #70 + + it('should stop it with a maximum accurary of 128 samples', () => { + const constantSourceNode = offlineAudioContext.createConstantSource(); + + constantSourceNode.connect(offlineAudioContext.destination); + constantSourceNode.start(); + constantSourceNode.stop(1 / offlineAudioContext.sampleRate); + + return offlineAudioContext + .startRendering() + .then((buffer) => { + const channelData = new Float32Array(5); + + buffer.copyFromChannel(channelData, 0, 0); + + expect(Array.from(channelData)).to.deep.equal([ 1, 1, 1, 1, 1 ]); + }); + }); - expect(constantSourceNode.channelCount).to.equal(1); }); }); diff --git a/test/expectation/safari/audio-context-constructor.js b/test/expectation/safari/audio-context-constructor.js index b7cb47297..23b7f6f05 100644 --- a/test/expectation/safari/audio-context-constructor.js +++ b/test/expectation/safari/audio-context-constructor.js @@ -132,48 +132,6 @@ describe('audioContextConstructor', () => { }); - describe('createBufferSource()', () => { - - describe('playbackRate', () => { - - // bug #45 - - it('should not throw any exception', () => { - const bufferSourceNode = audioContext.createBufferSource(); - - bufferSourceNode.playbackRate.exponentialRampToValueAtTime(0, 1); - }); - - }); - - describe('start()', () => { - - // bug #44 - - it('should throw a DOMException', () => { - const bufferSourceNode = audioContext.createBufferSource(); - - expect(() => bufferSourceNode.start(-1)).to.throw(DOMException); - expect(() => bufferSourceNode.start(0, -1)).to.throw(DOMException); - expect(() => bufferSourceNode.start(0, 0, -1)).to.throw(DOMException); - }); - - }); - - describe('stop()', () => { - - // bug #44 - - it('should throw a DOMException', () => { - const bufferSourceNode = audioContext.createBufferSource(); - - expect(() => bufferSourceNode.stop(-1)).to.throw(DOMException); - }); - - }); - - }); - describe('createBiquadFilter()', () => { // bug #11 @@ -250,6 +208,53 @@ describe('audioContextConstructor', () => { audioBufferSourceNode.stop(); }); + describe('playbackRate', () => { + + // bug #45 + + it('should not throw any exception', () => { + const bufferSourceNode = audioContext.createBufferSource(); + + bufferSourceNode.playbackRate.exponentialRampToValueAtTime(0, 1); + }); + + }); + + describe('start()', () => { + + // bug #44 + + it('should throw a DOMException', () => { + const bufferSourceNode = audioContext.createBufferSource(); + + expect(() => bufferSourceNode.start(-1)).to.throw(DOMException); + expect(() => bufferSourceNode.start(0, -1)).to.throw(DOMException); + expect(() => bufferSourceNode.start(0, 0, -1)).to.throw(DOMException); + }); + + // bug #69 + + it('should not ignore calls repeated calls to stop()', () => { + const audioBufferSourceNode = audioContext.createBufferSource(); + + audioBufferSourceNode.start(); + audioBufferSourceNode.start(); + }); + + }); + + describe('stop()', () => { + + // bug #44 + + it('should throw a DOMException', () => { + const bufferSourceNode = audioContext.createBufferSource(); + + expect(() => bufferSourceNode.stop(-1)).to.throw(DOMException); + }); + + }); + }); describe('createChannelMerger()', () => { diff --git a/test/expectation/safari/offline-audio-context-constructor.js b/test/expectation/safari/offline-audio-context-constructor.js index c41cdc38f..18faa3117 100644 --- a/test/expectation/safari/offline-audio-context-constructor.js +++ b/test/expectation/safari/offline-audio-context-constructor.js @@ -286,6 +286,15 @@ describe('offlineAudioContextConstructor', () => { expect(() => bufferSourceNode.stop(-1)).to.throw(DOMException); }); + // bug #69 + + it('should not ignore calls repeated calls to stop()', () => { + const audioBufferSourceNode = offlineAudioContext.createBufferSource(); + + audioBufferSourceNode.start(); + audioBufferSourceNode.start(); + }); + }); }); diff --git a/test/unit/audio-contexts/audio-context.js b/test/unit/audio-contexts/audio-context.js index bdffcdc91..22b46f83d 100644 --- a/test/unit/audio-contexts/audio-context.js +++ b/test/unit/audio-contexts/audio-context.js @@ -546,215 +546,8 @@ describe('AudioContext', () => { describe('createBufferSource()', () => { - it('should return an instance of the AudioBufferSourceNode interface', () => { - const audioBufferSourceNode = audioContext.createBufferSource(); - - expect(audioBufferSourceNode.buffer).to.be.null; - - expect(audioBufferSourceNode.channelCount).to.equal(2); - expect(audioBufferSourceNode.channelCountMode).to.equal('max'); - expect(audioBufferSourceNode.channelInterpretation).to.equal('speakers'); - - /* - * expect(audioBufferSourceNode.detune.cancelScheduledValues).to.be.a('function'); - * expect(audioBufferSourceNode.detune.defaultValue).to.equal(0); - * expect(audioBufferSourceNode.detune.exponentialRampToValueAtTime).to.be.a('function'); - * expect(audioBufferSourceNode.detune.linearRampToValueAtTime).to.be.a('function'); - * expect(audioBufferSourceNode.detune.setTargetAtTime).to.be.a('function'); - * expect(audioBufferSourceNode.detune.setValueCurveAtTime).to.be.a('function'); - * expect(audioBufferSourceNode.detune.value).to.equal(0); - */ - - expect(audioBufferSourceNode.loop).to.be.false; - expect(audioBufferSourceNode.loopEnd).to.equal(0); - expect(audioBufferSourceNode.loopStart).to.equal(0); - expect(audioBufferSourceNode.numberOfInputs).to.equal(0); - expect(audioBufferSourceNode.numberOfOutputs).to.equal(1); - expect(audioBufferSourceNode.onended).to.be.null; - - expect(audioBufferSourceNode.playbackRate.cancelScheduledValues).to.be.a('function'); - expect(audioBufferSourceNode.playbackRate.defaultValue).to.equal(1); - expect(audioBufferSourceNode.playbackRate.exponentialRampToValueAtTime).to.be.a('function'); - expect(audioBufferSourceNode.playbackRate.linearRampToValueAtTime).to.be.a('function'); - expect(audioBufferSourceNode.playbackRate.setTargetAtTime).to.be.a('function'); - expect(audioBufferSourceNode.playbackRate.setValueCurveAtTime).to.be.a('function'); - expect(audioBufferSourceNode.playbackRate.value).to.equal(1); - - expect(audioBufferSourceNode.start).to.be.a('function'); - expect(audioBufferSourceNode.stop).to.be.a('function'); - }); - - it('should throw an error if the AudioContext is closed', (done) => { - audioContext - .close() - .then(() => { - audioContext.createBufferSource(); - }) - .catch((err) => { - expect(err.code).to.equal(11); - expect(err.name).to.equal('InvalidStateError'); - - audioContext = new AudioContext(); - - done(); - }); - }); - - it('should be chainable', () => { - const audioBufferSourceNode = audioContext.createBufferSource(); - const gainNode = audioContext.createGain(); - - expect(audioBufferSourceNode.connect(gainNode)).to.equal(gainNode); - }); - - it('should not be connectable to a node of another AudioContext', (done) => { - const anotherAudioContext = new AudioContext(); - const audioBufferSourceNode = audioContext.createBufferSource(); - - try { - audioBufferSourceNode.connect(anotherAudioContext.destination); - } catch (err) { - expect(err.code).to.equal(15); - expect(err.name).to.equal('InvalidAccessError'); - - done(); - } finally { - anotherAudioContext.close(); - } - }); - - it('should not allow to stop an AudioBufferSourceNode which has not been started', (done) => { - const audioBufferSourceNode = audioContext.createBufferSource(); - - try { - audioBufferSourceNode.stop(); - } catch (err) { - expect(err.code).to.equal(11); - expect(err.name).to.equal('InvalidStateError'); - - done(); - } - }); - - it('should stop an AudioBufferSourceNode scheduled for stopping in the future', (done) => { - const audioBuffer = audioContext.createBuffer(1, 44100, 44100); - const audioBufferSourceNode = audioContext.createBufferSource(); - const buffer = new Float32Array(44100); - const scriptProcessorNode = createScriptProcessor(audioContext, 256, 1, 1); - - buffer.fill(1); - - audioBuffer.copyToChannel(buffer, 0, 0); - - audioBufferSourceNode.buffer = audioBuffer; - - audioBufferSourceNode - .connect(scriptProcessorNode) - .connect(audioContext.destination); - - const currentTime = audioContext.currentTime; - - audioBufferSourceNode.start(); - audioBufferSourceNode.stop(currentTime + 1); - audioBufferSourceNode.stop(currentTime); - - scriptProcessorNode.onaudioprocess = (event) => { - const channelData = event.inputBuffer.getChannelData(0); - - expect(Array.from(channelData)).to.not.contain(1); - - if (event.playbackTime > currentTime + 1) { - /* - * @todo Disconnecting the nodes causes a strange error in Firefox version 52 and above. - * @todo scriptProcessorNode.disconnect(audioContext.destination); - * @todo audioBufferSourceNode.disconnect(scriptProcessorNode); - */ - - done(); - } - }; - }); - - it('should ignore calls to stop() of an already stopped AudioBufferSourceNode', (done) => { - const audioBuffer = audioContext.createBuffer(1, 100, 44100); - const audioBufferSourceNode = audioContext.createBufferSource(); - - audioBufferSourceNode.onended = () => { - audioBufferSourceNode.stop(); - - done(); - }; - - audioBufferSourceNode.buffer = audioBuffer; - audioBufferSourceNode.connect(audioContext.destination); - audioBufferSourceNode.start(); - audioBufferSourceNode.stop(); - }); - - describe('onended', () => { - - it('should fire an assigned ended event listener', (done) => { - const audioBufferSourceNode = audioContext.createBufferSource(); - const audioBuffer = audioContext.createBuffer(2, 10, 44100); - - audioBufferSourceNode.buffer = audioBuffer; - audioBufferSourceNode.onended = (event) => { - expect(event).to.be.an.instanceOf(Event); - expect(event.type).to.equal('ended'); - - done(); - }; - - audioBufferSourceNode.connect(audioContext.destination); - - audioBufferSourceNode.start(); - }); - - }); - - describe('addEventListener()', () => { - - it('should fire a registered ended event listener', (done) => { - const audioBufferSourceNode = audioContext.createBufferSource(); - const audioBuffer = audioContext.createBuffer(2, 10, 44100); - - audioBufferSourceNode.buffer = audioBuffer; - audioBufferSourceNode.addEventListener('ended', (event) => { - expect(event).to.be.an.instanceOf(Event); - expect(event.type).to.equal('ended'); - - done(); - }); - - audioBufferSourceNode.connect(audioContext.destination); - - audioBufferSourceNode.start(); - }); - - }); - - describe('removeEventListener()', () => { - - it('should not fire a removed ended event listener', (done) => { - const audioBufferSourceNode = audioContext.createBufferSource(); - const audioBuffer = audioContext.createBuffer(2, 10, 44100); - const listener = spy(); - - audioBufferSourceNode.buffer = audioBuffer; - audioBufferSourceNode.addEventListener('ended', listener); - audioBufferSourceNode.removeEventListener('ended', listener); - - audioBufferSourceNode.connect(audioContext.destination); - - audioBufferSourceNode.start(); - - setTimeout(() => { - expect(listener).to.have.not.been.called; - - done(); - }, 500); - }); - + it('should be a function', () => { + expect(audioContext.createBufferSource).to.be.a('function'); }); }); diff --git a/test/unit/audio-contexts/offline-audio-context.js b/test/unit/audio-contexts/offline-audio-context.js index 242ee96ab..a460459f2 100644 --- a/test/unit/audio-contexts/offline-audio-context.js +++ b/test/unit/audio-contexts/offline-audio-context.js @@ -534,6 +534,20 @@ describe('OfflineAudioContext', () => { }); + describe('createBufferSource()', () => { + + let offlineAudioContext; + + beforeEach(() => { + offlineAudioContext = new OfflineAudioContext({ length: 1, sampleRate: 44100 }); + }); + + it('should be a function', () => { + expect(offlineAudioContext.createBufferSource).to.be.a('function'); + }); + + }); + describe('createConstantSource()', () => { let offlineAudioContext; diff --git a/test/unit/audio-nodes/audio-buffer-source-node.js b/test/unit/audio-nodes/audio-buffer-source-node.js new file mode 100644 index 000000000..7395f530d --- /dev/null +++ b/test/unit/audio-nodes/audio-buffer-source-node.js @@ -0,0 +1,1010 @@ +import { AudioBuffer } from '../../../src/audio-buffer'; +import { AudioBufferSourceNode } from '../../../src/audio-nodes/audio-buffer-source-node'; +import { AudioContext } from '../../../src/audio-contexts/audio-context'; +import { GainNode } from '../../../src/audio-nodes/gain-node'; +import { MinimalAudioContext } from '../../../src/audio-contexts/minimal-audio-context'; +import { MinimalOfflineAudioContext } from '../../../src/audio-contexts/minimal-offline-audio-context'; +import { OfflineAudioContext } from '../../../src/audio-contexts/offline-audio-context'; +import { createRenderer } from '../../helper/create-renderer'; +import { spy } from 'sinon'; + +describe('AudioBufferSourceNode', () => { + + // @todo leche seems to need a unique string as identifier as first argument. + leche.withData([ + [ + 'constructor with AudioContext', + () => new AudioContext(), + (context, options = null) => { + if (options === null) { + return new AudioBufferSourceNode(context); + } + + return new AudioBufferSourceNode(context, options); + } + ], [ + 'constructor with MinimalAudioContext', + () => new MinimalAudioContext(), + (context, options = null) => { + if (options === null) { + return new AudioBufferSourceNode(context); + } + + return new AudioBufferSourceNode(context, options); + } + ], [ + 'constructor with OfflineAudioContext', + () => new OfflineAudioContext({ length: 5, sampleRate: 44100 }), + (context, options = null) => { + if (options === null) { + return new AudioBufferSourceNode(context); + } + + return new AudioBufferSourceNode(context, options); + } + ], [ + 'constructor with MinimalOfflineAudioContext', + () => new MinimalOfflineAudioContext({ length: 5, sampleRate: 44100 }), + (context, options = null) => { + if (options === null) { + return new AudioBufferSourceNode(context); + } + + return new AudioBufferSourceNode(context, options); + } + ], [ + 'factory function of AudioContext', + () => new AudioContext(), + (context, options = null) => { + const audioBufferSourceNode = context.createBufferSource(); + + if (options !== null && options.channelCount !== undefined) { + audioBufferSourceNode.channelCount = options.channelCount; + } + + if (options !== null && options.channelCountMode !== undefined) { + audioBufferSourceNode.channelCountMode = options.channelCountMode; + } + + if (options !== null && options.channelInterpretation !== undefined) { + audioBufferSourceNode.channelInterpretation = options.channelInterpretation; + } + + if (options !== null && options.buffer !== undefined) { + audioBufferSourceNode.buffer = options.buffer; + } + + /* + * @todo if (options !== null && options.detune !== undefined) { + * @todo audioBufferSourceNode.detune.value = options.detune; + * @todo } + */ + + if (options !== null && options.loop !== undefined) { + audioBufferSourceNode.loop = options.loop; + } + + if (options !== null && options.loopEnd !== undefined) { + audioBufferSourceNode.loopEnd = options.loopEnd; + } + + if (options !== null && options.loopStart !== undefined) { + audioBufferSourceNode.loopStart = options.loopStart; + } + + if (options !== null && options.playbackRate !== undefined) { + audioBufferSourceNode.playbackRate.value = options.playbackRate; + } + + return audioBufferSourceNode; + } + ], [ + 'factory function of OfflineAudioContext', + () => new OfflineAudioContext({ length: 5, sampleRate: 44100 }), + (context, options = null) => { + const audioBufferSourceNode = context.createBufferSource(); + + if (options !== null && options.channelCount !== undefined) { + audioBufferSourceNode.channelCount = options.channelCount; + } + + if (options !== null && options.channelCountMode !== undefined) { + audioBufferSourceNode.channelCountMode = options.channelCountMode; + } + + if (options !== null && options.channelInterpretation !== undefined) { + audioBufferSourceNode.channelInterpretation = options.channelInterpretation; + } + + if (options !== null && options.buffer !== undefined) { + audioBufferSourceNode.buffer = options.buffer; + } + + /* + * @todo if (options !== null && options.detune !== undefined) { + * @todo audioBufferSourceNode.detune.value = options.detune; + * @todo } + */ + + if (options !== null && options.loop !== undefined) { + audioBufferSourceNode.loop = options.loop; + } + + if (options !== null && options.loopEnd !== undefined) { + audioBufferSourceNode.loopEnd = options.loopEnd; + } + + if (options !== null && options.loopStart !== undefined) { + audioBufferSourceNode.loopStart = options.loopStart; + } + + if (options !== null && options.playbackRate !== undefined) { + audioBufferSourceNode.playbackRate.value = options.playbackRate; + } + + return audioBufferSourceNode; + } + ] + ], (_, createContext, createAudioBufferSourceNode) => { + + let context; + + afterEach(() => { + if (context.close !== undefined) { + return context.close(); + } + }); + + beforeEach(() => context = createContext()); + + describe('constructor()', () => { + + describe('without any options', () => { + + let audioBufferSourceNode; + + beforeEach(() => { + audioBufferSourceNode = createAudioBufferSourceNode(context); + }); + + it('should be an instance of the EventTarget interface', () => { + expect(audioBufferSourceNode.addEventListener).to.be.a('function'); + expect(audioBufferSourceNode.dispatchEvent).to.be.a('function'); + expect(audioBufferSourceNode.removeEventListener).to.be.a('function'); + }); + + it('should be an instance of the AudioNode interface', () => { + expect(audioBufferSourceNode.channelCount).to.equal(2); + expect(audioBufferSourceNode.channelCountMode).to.equal('max'); + expect(audioBufferSourceNode.channelInterpretation).to.equal('speakers'); + expect(audioBufferSourceNode.connect).to.be.a('function'); + expect(audioBufferSourceNode.context).to.be.an.instanceOf(context.constructor); + expect(audioBufferSourceNode.disconnect).to.be.a('function'); + expect(audioBufferSourceNode.numberOfInputs).to.equal(0); + expect(audioBufferSourceNode.numberOfOutputs).to.equal(1); + }); + + it('should return an instance of the AudioScheduledSourceNode interface', () => { + expect(audioBufferSourceNode.onended).to.be.null; + expect(audioBufferSourceNode.start).to.be.a('function'); + expect(audioBufferSourceNode.stop).to.be.a('function'); + }); + + it('should return an instance of the AudioBufferSourceNode interface', () => { + expect(audioBufferSourceNode.buffer).to.be.null; + // expect(audioBufferSourceNode.detune).not.to.be.undefined; + expect(audioBufferSourceNode.loop).to.be.false; + expect(audioBufferSourceNode.loopEnd).to.equal(0); + expect(audioBufferSourceNode.loopStart).to.equal(0); + expect(audioBufferSourceNode.playbackRate).not.to.be.undefined; + }); + + it('should throw an error if the AudioContext is closed', (done) => { + ((context.close === undefined) ? context.startRendering() : context.close()) + .then(() => createAudioBufferSourceNode(context)) + .catch((err) => { + expect(err.code).to.equal(11); + expect(err.name).to.equal('InvalidStateError'); + + context.close = undefined; + + done(); + }); + }); + + }); + + describe('with valid options', () => { + + it('should return an instance with the given channelCount', () => { + const channelCount = 4; + const audioBufferSourceNode = createAudioBufferSourceNode(context, { channelCount }); + + expect(audioBufferSourceNode.channelCount).to.equal(channelCount); + }); + + it('should return an instance with the given channelCountMode', () => { + const channelCountMode = 'explicit'; + const audioBufferSourceNode = createAudioBufferSourceNode(context, { channelCountMode }); + + expect(audioBufferSourceNode.channelCountMode).to.equal(channelCountMode); + }); + + it('should return an instance with the given channelInterpretation', () => { + const channelInterpretation = 'discrete'; + const audioBufferSourceNode = createAudioBufferSourceNode(context, { channelInterpretation }); + + expect(audioBufferSourceNode.channelInterpretation).to.equal(channelInterpretation); + }); + + it('should return an instance with the given buffer', () => { + const audioBuffer = new AudioBuffer({ length: 1, sampleRate: context.sampleRate }); + const audioBufferSourceNode = createAudioBufferSourceNode(context, { buffer: audioBuffer }); + + expect(audioBufferSourceNode.buffer).to.equal(audioBuffer); + }); + + /* + * @todo it('should return an instance with the given initial value for detune', () => { + * @todo const detune = 0.5; + * @todo const audioBufferSourceNode = createAudioBufferSourceNode(context, { detune }); + * @todo + * @todo expect(audioBufferSourceNode.detune.value).to.equal(detune); + * @todo }); + */ + + it('should return an instance with the given loop', () => { + const loop = true; + const audioBufferSourceNode = createAudioBufferSourceNode(context, { loop }); + + expect(audioBufferSourceNode.loop).to.equal(loop); + }); + + it('should return an instance with the given loopEnd', () => { + const loopEnd = 10; + const audioBufferSourceNode = createAudioBufferSourceNode(context, { loopEnd }); + + expect(audioBufferSourceNode.loopEnd).to.equal(loopEnd); + }); + + it('should return an instance with the given loopStart', () => { + const loopStart = 2; + const audioBufferSourceNode = createAudioBufferSourceNode(context, { loopStart }); + + expect(audioBufferSourceNode.loopStart).to.equal(loopStart); + }); + + it('should return an instance with the given initial value for playbackRate', () => { + const playbackRate = 2; + const audioBufferSourceNode = createAudioBufferSourceNode(context, { playbackRate }); + + expect(audioBufferSourceNode.playbackRate.value).to.equal(playbackRate); + }); + + }); + + }); + + describe('buffer', () => { + + // @todo + + }); + + describe('channelCount', () => { + + let audioBufferSourceNode; + + beforeEach(() => { + audioBufferSourceNode = createAudioBufferSourceNode(context); + }); + + it('should be assignable to another value', () => { + const channelCount = 4; + + audioBufferSourceNode.channelCount = channelCount; + + expect(audioBufferSourceNode.channelCount).to.equal(channelCount); + }); + + }); + + describe('channelCountMode', () => { + + let audioBufferSourceNode; + + beforeEach(() => { + audioBufferSourceNode = createAudioBufferSourceNode(context); + }); + + it('should be assignable to another value', () => { + const channelCountMode = 'explicit'; + + audioBufferSourceNode.channelCountMode = channelCountMode; + + expect(audioBufferSourceNode.channelCountMode).to.equal(channelCountMode); + }); + + }); + + describe('channelInterpretation', () => { + + let audioBufferSourceNode; + + beforeEach(() => { + audioBufferSourceNode = createAudioBufferSourceNode(context); + }); + + it('should be assignable to another value', () => { + const channelInterpretation = 'discrete'; + + audioBufferSourceNode.channelInterpretation = channelInterpretation; + + expect(audioBufferSourceNode.channelInterpretation).to.equal(channelInterpretation); + }); + + }); + + describe('detune', () => { + + // @todo let audioBufferSourceNode; + // @todo + // @todo beforeEach(() => { + // @todo audioBufferSourceNode = createAudioBufferSourceNode(context); + // @todo }); + // @todo + // @todo it('should return an instance of the AudioParam interface', () => { + // @todo // @todo cancelAndHoldAtTime + // @todo expect(audioBufferSourceNode.detune.cancelScheduledValues).to.be.a('function'); + // @todo expect(audioBufferSourceNode.detune.defaultValue).to.equal(0); + // @todo expect(audioBufferSourceNode.detune.exponentialRampToValueAtTime).to.be.a('function'); + // @todo expect(audioBufferSourceNode.detune.linearRampToValueAtTime).to.be.a('function'); + // @todo /* + // @todo * @todo maxValue + // @todo * @todo minValue + // @todo */ + // @todo expect(audioBufferSourceNode.detune.setTargetAtTime).to.be.a('function'); + // @todo // @todo setValueAtTime + // @todo expect(audioBufferSourceNode.detune.setValueCurveAtTime).to.be.a('function'); + // @todo expect(audioBufferSourceNode.detune.value).to.equal(0); + // @todo }); + // @todo + // @todo it('should be readonly', () => { + // @todo expect(() => { + // @todo audioBufferSourceNode.detune = 'anything'; + // @todo }).to.throw(TypeError); + // @todo }); + // @todo + // @todo describe('automation', () => { + // @todo + // @todo let renderer; + // @todo + // @todo beforeEach(() => { + // @todo renderer = createRenderer({ + // @todo context, + // @todo length: (context.length === undefined) ? 5 : undefined, + // @todo prepare (destination) { + // @todo const audioBufferSourceNode = createAudioBufferSourceNode(context); + // @todo + // @todo audioBufferSourceNode + // @todo .connect(destination); + // @todo + // @todo return { audioBufferSourceNode }; + // @todo } + // @todo }); + // @todo }); + // @todo + // @todo describe('without any automation', () => { + // @todo + // @todo it('should not modify the signal', function () { + // @todo this.timeout(5000); + // @todo + // @todo return renderer({ + // @todo start (startTime, { audioBufferSourceNode }) { + // @todo audioBufferSourceNode.start(startTime); + // @todo } + // @todo }) + // @todo .then((channelData) => { + // @todo expect(Array.from(channelData)).to.deep.equal([ 1, 1, 1, 1, 1 ]); + // @todo }); + // @todo }); + // @todo + // @todo }); + // @todo + // @todo describe('with a modified value', () => { + // @todo + // @todo it('should modify the signal', function () { + // @todo this.timeout(5000); + // @todo + // @todo return renderer({ + // @todo prepare ({ audioBufferSourceNode }) { + // @todo audioBufferSourceNode.offset.value = 0.5; + // @todo }, + // @todo start (startTime, { audioBufferSourceNode }) { + // @todo audioBufferSourceNode.start(startTime); + // @todo } + // @todo }) + // @todo .then((channelData) => { + // @todo expect(Array.from(channelData)).to.deep.equal([ 0.5, 0.5, 0.5, 0.5, 0.5 ]); + // @todo }); + // @todo }); + // @todo + // @todo }); + // @todo + // @todo describe('with a call to setValueAtTime()', () => { + // @todo + // @todo it('should modify the signal', function () { + // @todo this.timeout(5000); + // @todo + // @todo return renderer({ + // @todo start (startTime, { audioBufferSourceNode }) { + // @todo audioBufferSourceNode.offset.setValueAtTime(0.5, startTime + (2 / context.sampleRate)); + // @todo + // @todo audioBufferSourceNode.start(startTime); + // @todo } + // @todo }) + // @todo .then((channelData) => { + // @todo expect(Array.from(channelData)).to.deep.equal([ 1, 1, 0.5, 0.5, 0.5 ]); + // @todo }); + // @todo }); + // @todo + // @todo }); + // @todo + // @todo describe('with a call to setValueCurveAtTime()', () => { + // @todo + // @todo it('should modify the signal', function () { + // @todo this.timeout(5000); + // @todo + // @todo return renderer({ + // @todo start (startTime, { audioBufferSourceNode }) { + // @todo audioBufferSourceNode.offset.setValueCurveAtTime(new Float32Array([ 0, 0.25, 0.5, 0.75, 1 ]), startTime, startTime + (5 / context.sampleRate)); + // @todo + // @todo audioBufferSourceNode.start(startTime); + // @todo } + // @todo }) + // @todo .then((channelData) => { + // @todo // @todo The implementation of Safari is different. Therefore this test only checks if the values have changed. + // @todo expect(Array.from(channelData)).to.not.deep.equal([ 1, 1, 1, 1, 1 ]); + // @todo }); + // @todo }); + // @todo + // @todo }); + // @todo + // @todo describe('with another AudioNode connected to the AudioParam', () => { + // @todo + // @todo it('should modify the signal', function () { + // @todo this.timeout(5000); + // @todo + // @todo return renderer({ + // @todo prepare ({ audioBufferSourceNode }) { + // @todo const audioBuffer = new AudioBuffer({ length: 5, sampleRate: context.sampleRate }); + // @todo const audioBufferSourceNodeForAudioParam = new AudioBufferSourceNode(context); + // @todo + // @todo audioBuffer.copyToChannel(new Float32Array([ 0.5, 0.25, 0, -0.25, -0.5 ]), 0); + // @todo + // @todo audioBufferSourceNodeForAudioParam.buffer = audioBuffer; + // @todo + // @todo audioBufferSourceNode.offset.value = 0; + // @todo + // @todo audioBufferSourceNodeForAudioParam.connect(audioBufferSourceNode.offset); + // @todo + // @todo return { audioBufferSourceNodeForAudioParam }; + // @todo }, + // @todo start (startTime, { audioBufferSourceNodeForAudioParam, audioBufferSourceNode }) { + // @todo audioBufferSourceNodeForAudioParam.start(startTime); + // @todo audioBufferSourceNode.start(startTime); + // @todo } + // @todo }) + // @todo .then((channelData) => { + // @todo expect(Array.from(channelData)).to.deep.equal([ 0.5, 0.25, 0, -0.25, -0.5 ]); + // @todo }); + // @todo }); + // @todo + // @todo }); + // @todo + // @todo // @todo Test other automations as well. + // @todo + // @todo }); + + }); + + describe('loop', () => { + + // @todo + + }); + + describe('loopEnd', () => { + + // @todo + + }); + + describe('loopStart', () => { + + // @todo + + }); + + describe('onended', () => { + + let audioBufferSourceNode; + + beforeEach(() => { + audioBufferSourceNode = createAudioBufferSourceNode(context, { + buffer: new AudioBuffer({ length: 5, sampleRate: context.sampleRate }) + }); + }); + + it('should fire an assigned ended event listener', (done) => { + audioBufferSourceNode.onended = (event) => { + expect(event).to.be.an.instanceOf(Event); + expect(event.type).to.equal('ended'); + + done(); + }; + + audioBufferSourceNode.connect(context.destination); + + audioBufferSourceNode.start(); + + if (context.startRendering !== undefined) { + context.startRendering(); + } + }); + + }); + + describe('playbackRate', () => { + + let audioBufferSourceNode; + + beforeEach(() => { + audioBufferSourceNode = createAudioBufferSourceNode(context); + }); + + it('should return an instance of the AudioParam interface', () => { + // @todo cancelAndHoldAtTime + expect(audioBufferSourceNode.playbackRate.cancelScheduledValues).to.be.a('function'); + expect(audioBufferSourceNode.playbackRate.defaultValue).to.equal(1); + expect(audioBufferSourceNode.playbackRate.exponentialRampToValueAtTime).to.be.a('function'); + expect(audioBufferSourceNode.playbackRate.linearRampToValueAtTime).to.be.a('function'); + /* + * @todo maxValue + * @todo minValue + */ + expect(audioBufferSourceNode.playbackRate.setTargetAtTime).to.be.a('function'); + // @todo setValueAtTime + expect(audioBufferSourceNode.playbackRate.setValueCurveAtTime).to.be.a('function'); + expect(audioBufferSourceNode.playbackRate.value).to.equal(1); + }); + + it('should be readonly', () => { + expect(() => { + audioBufferSourceNode.playbackRate = 'anything'; + }).to.throw(TypeError); + }); + + // @todo animation + + }); + + describe('addEventListener()', () => { + + let audioBufferSourceNode; + + beforeEach(() => { + audioBufferSourceNode = createAudioBufferSourceNode(context, { + buffer: new AudioBuffer({ length: 5, sampleRate: context.sampleRate }) + }); + }); + + it('should fire a registered ended event listener', (done) => { + audioBufferSourceNode.addEventListener('ended', (event) => { + expect(event).to.be.an.instanceOf(Event); + expect(event.type).to.equal('ended'); + + done(); + }); + + audioBufferSourceNode.connect(context.destination); + + audioBufferSourceNode.start(); + + if (context.startRendering !== undefined) { + context.startRendering(); + } + }); + + }); + + describe('connect()', () => { + + let audioBufferSourceNode; + + beforeEach(() => { + audioBufferSourceNode = createAudioBufferSourceNode(context); + }); + + it('should be chainable', () => { + const gainNode = new GainNode(context); + + expect(audioBufferSourceNode.connect(gainNode)).to.equal(gainNode); + }); + + it('should not be connectable to an AudioNode of another AudioContext', (done) => { + const anotherContext = createContext(); + + try { + audioBufferSourceNode.connect(anotherContext.destination); + } catch (err) { + expect(err.code).to.equal(15); + expect(err.name).to.equal('InvalidAccessError'); + + done(); + } finally { + if (anotherContext.close !== undefined) { + anotherContext.close(); + } + } + }); + + it('should not be connectable to an AudioParam of another AudioContext', (done) => { + const anotherContext = createContext(); + const gainNode = new GainNode(anotherContext); + + try { + audioBufferSourceNode.connect(gainNode.gain); + } catch (err) { + expect(err.code).to.equal(15); + expect(err.name).to.equal('InvalidAccessError'); + + done(); + } finally { + if (anotherContext.close !== undefined) { + anotherContext.close(); + } + } + }); + + it('should throw an IndexSizeError if the output is out-of-bound', (done) => { + const gainNode = new GainNode(context); + + try { + audioBufferSourceNode.connect(gainNode.gain, -1); + } catch (err) { + expect(err.code).to.equal(1); + expect(err.name).to.equal('IndexSizeError'); + + done(); + } + }); + + }); + + describe('disconnect()', () => { + + let renderer; + let values; + + beforeEach(() => { + values = [ 1, 1, 1, 1, 1 ]; + + renderer = createRenderer({ + context, + length: (context.length === undefined) ? 5 : undefined, + prepare (destination) { + const audioBuffer = new AudioBuffer({ length: 5, sampleRate: context.sampleRate }); + const audioBufferSourceNode = createAudioBufferSourceNode(context); + const firstDummyGainNode = new GainNode(context); + const secondDummyGainNode = new GainNode(context); + + audioBuffer.copyToChannel(new Float32Array(values), 0); + + audioBufferSourceNode.buffer = audioBuffer; + + audioBufferSourceNode + .connect(firstDummyGainNode) + .connect(destination); + + audioBufferSourceNode.connect(secondDummyGainNode); + + return { audioBufferSourceNode, firstDummyGainNode, secondDummyGainNode }; + } + }); + }); + + it('should be possible to disconnect a destination', function () { + this.timeout(5000); + + return renderer({ + prepare ({ audioBufferSourceNode, firstDummyGainNode }) { + audioBufferSourceNode.disconnect(firstDummyGainNode); + }, + start (startTime, { audioBufferSourceNode }) { + audioBufferSourceNode.start(startTime); + } + }) + .then((channelData) => { + expect(Array.from(channelData)).to.deep.equal([ 0, 0, 0, 0, 0 ]); + }); + }); + + it('should be possible to disconnect another destination in isolation', function () { + this.timeout(5000); + + return renderer({ + prepare ({ audioBufferSourceNode, secondDummyGainNode }) { + audioBufferSourceNode.disconnect(secondDummyGainNode); + }, + start (startTime, { audioBufferSourceNode }) { + audioBufferSourceNode.start(startTime); + } + }) + .then((channelData) => { + expect(Array.from(channelData)).to.deep.equal(values); + }); + }); + + it('should be possible to disconnect all destinations', function () { + this.timeout(5000); + + return renderer({ + prepare ({ audioBufferSourceNode }) { + audioBufferSourceNode.disconnect(); + }, + start (startTime, { audioBufferSourceNode }) { + audioBufferSourceNode.start(startTime); + } + }) + .then((channelData) => { + expect(Array.from(channelData)).to.deep.equal([ 0, 0, 0, 0, 0 ]); + }); + }); + + }); + + describe('removeEventListener()', () => { + + let audioBufferSourceNode; + + beforeEach(() => { + audioBufferSourceNode = createAudioBufferSourceNode(context, { + buffer: new AudioBuffer({ length: 5, sampleRate: context.sampleRate }) + }); + }); + + it('should not fire a removed ended event listener', (done) => { + const listener = spy(); + + audioBufferSourceNode.addEventListener('ended', listener); + audioBufferSourceNode.removeEventListener('ended', listener); + + audioBufferSourceNode.connect(context.destination); + + audioBufferSourceNode.start(); + + setTimeout(() => { + expect(listener).to.have.not.been.called; + + done(); + }, 500); + + if (context.startRendering !== undefined) { + context.startRendering(); + } + }); + + }); + + describe('start()', () => { + + let audioBufferSourceNode; + + beforeEach(() => { + audioBufferSourceNode = createAudioBufferSourceNode(context); + }); + + describe('with a previous call to start()', () => { + + beforeEach(() => { + audioBufferSourceNode.start(); + }); + + it('should throw an InvalidStateError', (done) => { + try { + audioBufferSourceNode.start(); + } catch (err) { + expect(err.code).to.equal(11); + expect(err.name).to.equal('InvalidStateError'); + + done(); + } + }); + + }); + + describe('with a previous call to stop()', () => { + + beforeEach(() => { + // ... Safari needs a buffer + audioBufferSourceNode.buffer = new AudioBuffer({ length: 5, sampleRate: context.sampleRate }); + audioBufferSourceNode.start(); + audioBufferSourceNode.stop(); + }); + + it('should throw an InvalidStateError', (done) => { + try { + audioBufferSourceNode.start(); + } catch (err) { + expect(err.code).to.equal(11); + expect(err.name).to.equal('InvalidStateError'); + + done(); + } + }); + + }); + + describe('with a negative value as first parameter', () => { + + it('should throw an RangeError', () => { + expect(() => { + audioBufferSourceNode.start(-1); + }).to.throw(RangeError); + }); + + }); + + }); + + describe('stop()', () => { + + describe('without a previous call to start()', () => { + + let audioBufferSourceNode; + + beforeEach(() => { + audioBufferSourceNode = createAudioBufferSourceNode(context); + }); + + it('should throw an InvalidStateError', (done) => { + try { + audioBufferSourceNode.stop(); + } catch (err) { + expect(err.code).to.equal(11); + expect(err.name).to.equal('InvalidStateError'); + + done(); + } + }); + + }); + + describe('with a previous call to stop()', () => { + + let renderer; + + beforeEach(() => { + renderer = createRenderer({ + context, + length: (context.length === undefined) ? 5 : undefined, + prepare (destination) { + const audioBuffer = new AudioBuffer({ length: 5, sampleRate: context.sampleRate }); + + audioBuffer.copyToChannel(new Float32Array([ 1, 1, 1, 1, 1 ]), 0); + + const audioBufferSourceNode = createAudioBufferSourceNode(context, { buffer: audioBuffer }); + + audioBufferSourceNode.connect(destination); + + return { audioBufferSourceNode }; + } + }); + }); + + it('should apply the values from the last invocation', function () { + this.timeout(5000); + + return renderer({ + start (startTime, { audioBufferSourceNode }) { + audioBufferSourceNode.start(startTime); + audioBufferSourceNode.stop(startTime + (5 / context.sampleRate)); + audioBufferSourceNode.stop(startTime + (3 / context.sampleRate)); + } + }) + .then((channelData) => { + expect(Array.from(channelData)).to.deep.equal([ 1, 1, 1, 0, 0 ]); + }); + }); + + }); + + describe('with a stop time reached prior to the start time', () => { + + let renderer; + + beforeEach(() => { + renderer = createRenderer({ + context, + length: (context.length === undefined) ? 5 : undefined, + prepare (destination) { + const audioBuffer = new AudioBuffer({ length: 5, sampleRate: context.sampleRate }); + + audioBuffer.copyToChannel(new Float32Array([ 1, 1, 1, 1, 1 ]), 0); + + const audioBufferSourceNode = createAudioBufferSourceNode(context, { buffer: audioBuffer }); + + audioBufferSourceNode.connect(destination); + + return { audioBufferSourceNode }; + } + }); + }); + + it('should not play anything', function () { + this.timeout(5000); + + return renderer({ + start (startTime, { audioBufferSourceNode }) { + audioBufferSourceNode.start(startTime + (3 / context.sampleRate)); + audioBufferSourceNode.stop(startTime + (1 / context.sampleRate)); + } + }) + .then((channelData) => { + expect(Array.from(channelData)).to.deep.equal([ 0, 0, 0, 0, 0 ]); + }); + }); + + }); + + describe('with an emitted ended event', () => { + + let audioBufferSourceNode; + + beforeEach((done) => { + const audioBuffer = new AudioBuffer({ length: 1, sampleRate: context.sampleRate }); + + audioBufferSourceNode = createAudioBufferSourceNode(context, { buffer: audioBuffer }); + + audioBufferSourceNode.onended = () => done(); + + audioBufferSourceNode.connect(context.destination); + + audioBufferSourceNode.start(); + audioBufferSourceNode.stop(); + + if (context.startRendering !== undefined) { + context.startRendering(); + } + }); + + it('should ignore calls to stop()', () => { + audioBufferSourceNode.stop(); + }); + + }); + + describe('with a negative value as first parameter', () => { + + let audioBufferSourceNode; + + beforeEach(() => { + audioBufferSourceNode = createAudioBufferSourceNode(context); + + audioBufferSourceNode.start(); + }); + + it('should throw an RangeError', () => { + expect(() => { + audioBufferSourceNode.stop(-1); + }).to.throw(RangeError); + }); + + }); + + }); + + }); + +}); diff --git a/test/unit/audio-nodes/constant-source-node.js b/test/unit/audio-nodes/constant-source-node.js index 94e15a6bf..949108d74 100644 --- a/test/unit/audio-nodes/constant-source-node.js +++ b/test/unit/audio-nodes/constant-source-node.js @@ -7,6 +7,7 @@ import { MinimalAudioContext } from '../../../src/audio-contexts/minimal-audio-c import { MinimalOfflineAudioContext } from '../../../src/audio-contexts/minimal-offline-audio-context'; import { OfflineAudioContext } from '../../../src/audio-contexts/offline-audio-context'; import { createRenderer } from '../../helper/create-renderer'; +import { spy } from 'sinon'; describe('ConstantSourceNode', () => { @@ -140,6 +141,12 @@ describe('ConstantSourceNode', () => { expect(constantSourceNode.numberOfOutputs).to.equal(1); }); + it('should return an instance of the AudioScheduledSourceNode interface', () => { + expect(constantSourceNode.onended).to.be.null; + expect(constantSourceNode.start).to.be.a('function'); + expect(constantSourceNode.stop).to.be.a('function'); + }); + it('should return an instance of the ConstantSourceNode interface', () => { expect(constantSourceNode.offset).not.to.be.undefined; }); @@ -412,7 +419,57 @@ describe('ConstantSourceNode', () => { describe('onended', () => { - // @todo + let constantSourceNode; + + beforeEach(() => { + constantSourceNode = createConstantSourceNode(context, { offset: 0 }); + }); + + it('should fire an assigned ended event listener', (done) => { + constantSourceNode.onended = (event) => { + expect(event).to.be.an.instanceOf(Event); + expect(event.type).to.equal('ended'); + + done(); + }; + + constantSourceNode.connect(context.destination); + + constantSourceNode.start(); + constantSourceNode.stop(); + + if (context.startRendering !== undefined) { + context.startRendering(); + } + }); + + }); + + describe('addEventListener()', () => { + + let constantSourceNode; + + beforeEach(() => { + constantSourceNode = createConstantSourceNode(context, { offset: 0 }); + }); + + it('should fire a registered ended event listener', (done) => { + constantSourceNode.addEventListener('ended', (event) => { + expect(event).to.be.an.instanceOf(Event); + expect(event.type).to.equal('ended'); + + done(); + }); + + constantSourceNode.connect(context.destination); + + constantSourceNode.start(); + constantSourceNode.stop(); + + if (context.startRendering !== undefined) { + context.startRendering(); + } + }); }); @@ -554,15 +611,231 @@ describe('ConstantSourceNode', () => { }); + describe('removeEventListener()', () => { + + let constantSourceNode; + + beforeEach(() => { + constantSourceNode = createConstantSourceNode(context, { offset: 0 }); + }); + + it('should not fire a removed ended event listener', (done) => { + const listener = spy(); + + constantSourceNode.addEventListener('ended', listener); + constantSourceNode.removeEventListener('ended', listener); + + constantSourceNode.connect(context.destination); + + constantSourceNode.start(); + constantSourceNode.stop(); + + setTimeout(() => { + expect(listener).to.have.not.been.called; + + done(); + }, 500); + + if (context.startRendering !== undefined) { + context.startRendering(); + } + }); + + }); + describe('start()', () => { - // @todo + let constantSourceNode; + + beforeEach(() => { + constantSourceNode = createConstantSourceNode(context); + }); + + describe('with a previous call to start()', () => { + + beforeEach(() => { + constantSourceNode.start(); + }); + + it('should throw an InvalidStateError', (done) => { + try { + constantSourceNode.start(); + } catch (err) { + expect(err.code).to.equal(11); + expect(err.name).to.equal('InvalidStateError'); + + done(); + } + }); + + }); + + describe('with a previous call to stop()', () => { + + beforeEach(() => { + constantSourceNode.start(); + constantSourceNode.stop(); + }); + + it('should throw an InvalidStateError', (done) => { + try { + constantSourceNode.start(); + } catch (err) { + expect(err.code).to.equal(11); + expect(err.name).to.equal('InvalidStateError'); + + done(); + } + }); + + }); + + describe('with a negative value as first parameter', () => { + + it('should throw an RangeError', () => { + expect(() => { + constantSourceNode.start(-1); + }).to.throw(RangeError); + }); + + }); }); describe('stop()', () => { - // @todo + describe('without a previous call to start()', () => { + + let constantSourceNode; + + beforeEach(() => { + constantSourceNode = createConstantSourceNode(context); + }); + + it('should throw an InvalidStateError', (done) => { + try { + constantSourceNode.stop(); + } catch (err) { + expect(err.code).to.equal(11); + expect(err.name).to.equal('InvalidStateError'); + + done(); + } + }); + + }); + + describe('with a previous call to stop()', () => { + + let renderer; + + beforeEach(() => { + renderer = createRenderer({ + context, + length: (context.length === undefined) ? 5 : undefined, + prepare (destination) { + const constantSourceNode = createConstantSourceNode(context); + + constantSourceNode.connect(destination); + + return { constantSourceNode }; + } + }); + }); + + it('should apply the values from the last invocation', function () { + this.timeout(5000); + + return renderer({ + start (startTime, { constantSourceNode }) { + constantSourceNode.start(startTime); + constantSourceNode.stop(startTime + (5 / context.sampleRate)); + constantSourceNode.stop(startTime + (3 / context.sampleRate)); + } + }) + .then((channelData) => { + expect(Array.from(channelData)).to.deep.equal([ 1, 1, 1, 0, 0 ]); + }); + }); + + }); + + describe('with a stop time reached prior to the start time', () => { + + let renderer; + + beforeEach(() => { + renderer = createRenderer({ + context, + length: (context.length === undefined) ? 5 : undefined, + prepare (destination) { + const constantSourceNode = createConstantSourceNode(context); + + constantSourceNode.connect(destination); + + return { constantSourceNode }; + } + }); + }); + + it('should not play anything', function () { + this.timeout(5000); + + return renderer({ + start (startTime, { constantSourceNode }) { + constantSourceNode.start(startTime + (3 / context.sampleRate)); + constantSourceNode.stop(startTime + (1 / context.sampleRate)); + } + }) + .then((channelData) => { + expect(Array.from(channelData)).to.deep.equal([ 0, 0, 0, 0, 0 ]); + }); + }); + + }); + + describe('with an emitted ended event', () => { + + let constantSourceNode; + + beforeEach((done) => { + constantSourceNode = createConstantSourceNode(context); + + constantSourceNode.onended = () => done(); + + constantSourceNode.connect(context.destination); + + constantSourceNode.start(); + constantSourceNode.stop(); + + if (context.startRendering !== undefined) { + context.startRendering(); + } + }); + + it('should ignore calls to stop()', () => { + constantSourceNode.stop(); + }); + + }); + + describe('with a negative value as first parameter', () => { + + let constantSourceNode; + + beforeEach(() => { + constantSourceNode = createConstantSourceNode(context); + + constantSourceNode.start(); + }); + + it('should throw an RangeError', () => { + expect(() => { + constantSourceNode.stop(-1); + }).to.throw(RangeError); + }); + + }); });