├── .eslintignore ├── src ├── __version__.js ├── utils │ ├── getAPIVersion.js │ ├── defaults.js │ ├── toSeconds.js │ ├── toNodeName.js │ ├── appendIfNotExists.js │ ├── removeIfExists.js │ ├── inLaws.js │ ├── format.js │ ├── toJSON.js │ ├── toS.js │ ├── toMicroseconds.js │ ├── Immigration.js │ ├── api.js │ ├── Configuration.js │ └── Junction.js ├── validators │ ├── isNumber.js │ ├── isInteger.js │ ├── isString.js │ ├── isBoolean.js │ ├── isFunction.js │ ├── isPositiveNumber.js │ ├── isPositiveInteger.js │ ├── isInstanceOf.js │ ├── isAudioSource.js │ ├── isNullOrInstanceOf.js │ └── index.js ├── index.js ├── dom │ ├── Element.js │ ├── HTMLElement.js │ ├── MediaStream.js │ ├── HTMLMediaElement.js │ ├── Event.js │ └── EventTarget.js ├── MediaStreamAudioDestinationNode.js ├── GainNode.js ├── StereoPannerNode.js ├── AudioProcessingEvent.js ├── AudioDestinationNode.js ├── OfflineAudioCompletionEvent.js ├── WaveShaperNode.js ├── ChannelMergerNode.js ├── ChannelSplitterNode.js ├── ConvolverNode.js ├── MediaStreamAudioSourceNode.js ├── MediaElementAudioSourceNode.js ├── DynamicsCompressorNode.js ├── DelayNode.js ├── BiquadFilterNode.js ├── WebAudioAPI.js ├── AudioListener.js ├── PeriodicWave.js ├── AnalyserNode.js ├── PannerNode.js ├── ScriptProcessorNode.js ├── OscillatorNode.js ├── AudioBufferSourceNode.js ├── OfflineAudioContext.js ├── decorators │ ├── props.js │ └── methods.js ├── AudioBuffer.js ├── WebAudioTestAPI.js ├── AudioParam.js └── AudioNode.js ├── index.js ├── test ├── utils │ ├── .eslintrc │ ├── getAPIVersion.js │ ├── toNodeName.js │ ├── defaults.js │ ├── toSeconds.js │ ├── removeIfExists.js │ ├── appendIfNotExists.js │ ├── toMicroseconds.js │ ├── inLaws.js │ ├── toS.js │ ├── Immigration.js │ └── Configuration.js ├── decorators │ ├── .eslintrc │ ├── props │ │ ├── readonly.js │ │ ├── on.js │ │ ├── enums.js │ │ ├── audioparam.js │ │ └── typed.js │ └── methods │ │ ├── contract.js │ │ └── param.js ├── validators │ ├── .eslintrc │ ├── isInstanceOf.js │ ├── isNullOrInstanceOf.js │ ├── isNumber.js │ ├── isString.js │ ├── isBoolean.js │ ├── isInteger.js │ ├── isFunction.js │ ├── isPositiveNumber.js │ └── isPositiveInteger.js ├── mocha.opts ├── .eslintrc ├── bootstrap │ └── bootstrap.js ├── dom │ ├── Element.js │ ├── Event.js │ ├── HTMLElement.js │ ├── MediaStream.js │ ├── HTMLMediaElement.js │ └── EventTarget.js ├── AudioProcessingEvent.js ├── OfflineAudioCompletionEvent.js ├── MediaStreamAudioDestinationNode.js ├── ChannelMergerNode.js ├── ChannelSplitterNode.js ├── MediaStreamAudioSourceNode.js ├── AudioDestinationNode.js ├── MediaElementAudioSourceNode.js ├── GainNode.js ├── WebAudioTestAPI.js ├── StereoPannerNode.js ├── PeriodicWave.js ├── DelayNode.js ├── ConvolverNode.js ├── WaveShaperNode.js ├── DynamicsCompressorNode.js ├── AudioListener.js ├── BiquadFilterNode.js ├── ScriptProcessorNode.js ├── AnalyserNode.js ├── PannerNode.js └── OscillatorNode.js ├── .gitignore ├── .travis.yml ├── .eslintrc ├── .babelrc ├── package.json ├── index.html └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | test/bootstrap/assert.min.js 2 | -------------------------------------------------------------------------------- /src/__version__.js: -------------------------------------------------------------------------------- 1 | export default "0.5.2"; 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./lib").default; 2 | -------------------------------------------------------------------------------- /test/utils/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint" 3 | } 4 | -------------------------------------------------------------------------------- /test/decorators/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint" 3 | } 4 | -------------------------------------------------------------------------------- /test/validators/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | npm-debug.log 3 | node_modules/ 4 | coverage/ 5 | lib/ 6 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --recursive 3 | --require test/bootstrap/bootstrap.js 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "4.2" 5 | script: 6 | - npm run travis 7 | -------------------------------------------------------------------------------- /src/utils/getAPIVersion.js: -------------------------------------------------------------------------------- 1 | import VERSION from "../__version__"; 2 | 3 | export default function getAPIVersion() { 4 | return VERSION; 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/defaults.js: -------------------------------------------------------------------------------- 1 | export default function defaults(value, defaultValue) { 2 | return typeof value !== "undefined" ? value : defaultValue; 3 | } 4 | -------------------------------------------------------------------------------- /src/validators/isNumber.js: -------------------------------------------------------------------------------- 1 | export default { 2 | description: "number", 3 | typeName: "number", 4 | test: (value) => value === +value 5 | }; 6 | -------------------------------------------------------------------------------- /src/validators/isInteger.js: -------------------------------------------------------------------------------- 1 | export default { 2 | description: "integer", 3 | typeName: "number", 4 | test: (value) => value === (value|0) 5 | }; 6 | -------------------------------------------------------------------------------- /src/validators/isString.js: -------------------------------------------------------------------------------- 1 | export default { 2 | description: "string", 3 | typeName: "string", 4 | test: (value) => typeof value === "string" 5 | }; 6 | -------------------------------------------------------------------------------- /src/validators/isBoolean.js: -------------------------------------------------------------------------------- 1 | export default { 2 | description: "boolean", 3 | typeName: "boolean", 4 | test: (value) => typeof value === "boolean" 5 | }; 6 | -------------------------------------------------------------------------------- /src/validators/isFunction.js: -------------------------------------------------------------------------------- 1 | export default { 2 | description: "function", 3 | typeName: "function", 4 | test: (value) => typeof value === "function" 5 | }; 6 | -------------------------------------------------------------------------------- /src/utils/toSeconds.js: -------------------------------------------------------------------------------- 1 | import toMicroseconds from "./toMicroseconds"; 2 | 3 | export default function toSeconds(time) { 4 | return toMicroseconds(time) / (1000 * 1000); 5 | } 6 | -------------------------------------------------------------------------------- /src/validators/isPositiveNumber.js: -------------------------------------------------------------------------------- 1 | export default { 2 | description: "positive number", 3 | typeName: "number", 4 | test: (value) => value === +value && 0 <= value 5 | }; 6 | -------------------------------------------------------------------------------- /src/utils/toNodeName.js: -------------------------------------------------------------------------------- 1 | export default function toNodeName(obj) { 2 | if (obj.hasOwnProperty("$id")) { 3 | return `${obj.$name}#${obj.$id}`; 4 | } 5 | return obj.$name; 6 | } 7 | -------------------------------------------------------------------------------- /src/validators/isPositiveInteger.js: -------------------------------------------------------------------------------- 1 | export default { 2 | description: "positive integer", 3 | typeName: "number", 4 | test: (value) => value === (value|0) && 0 <= value 5 | }; 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import WebAudioTestAPI from "./WebAudioTestAPI"; 2 | 3 | if (!global.WEB_AUDIO_TEST_API_IGNORE) { 4 | WebAudioTestAPI.use(); 5 | } 6 | 7 | export default WebAudioTestAPI; 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "node": true 5 | }, 6 | "rules": { 7 | "comma-dangle": [ 2, "never" ] 8 | }, 9 | "extends": "mohayonao" 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/appendIfNotExists.js: -------------------------------------------------------------------------------- 1 | export default function appendIfNotExists(list, value) { 2 | let index = list.indexOf(value); 3 | 4 | if (index === -1) { 5 | list.push(value); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0" 5 | ], 6 | "plugins": [ 7 | "transform-es2015-modules-commonjs", 8 | "transform-decorators-legacy" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/validators/isInstanceOf.js: -------------------------------------------------------------------------------- 1 | export default function isInstanceOf(klass) { 2 | return { 3 | description: klass.name, 4 | typeName: klass.name, 5 | test: (value) => value instanceof klass 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/validators/isAudioSource.js: -------------------------------------------------------------------------------- 1 | export default { 2 | description: "AudioNode or an AudioParam", 3 | typeName: "AudioNode|AudioParam", 4 | test: (value) => value instanceof global.AudioNode || value instanceof global.AudioParam 5 | }; 6 | -------------------------------------------------------------------------------- /src/utils/removeIfExists.js: -------------------------------------------------------------------------------- 1 | export default function removeIfExists(list, value) { 2 | let index = list.indexOf(value); 3 | 4 | if (index !== -1) { 5 | return list.splice(index, 1)[0]; 6 | } 7 | 8 | return null; 9 | } 10 | -------------------------------------------------------------------------------- /src/validators/isNullOrInstanceOf.js: -------------------------------------------------------------------------------- 1 | export default function isNullOrInstanceOf(klass) { 2 | return { 3 | description: klass.name, 4 | typeName: `${klass.name}|null`, 5 | test: (value) => value === null || value instanceof klass 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/inLaws.js: -------------------------------------------------------------------------------- 1 | export default function inLaws(superClass) { 2 | function ctor() { 3 | } 4 | 5 | ctor.prototype = Object.create(superClass.prototype, { 6 | constructor: { value: ctor, enumerable: false, writable: true, configurable: true } 7 | }); 8 | 9 | return ctor; 10 | } 11 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "", 3 | "env": { 4 | "mocha": true 5 | }, 6 | "rules": { 7 | "no-new": 0, 8 | "no-undefined": 0, 9 | "no-var": 0 10 | }, 11 | "globals": { 12 | "assert": true, 13 | "sinon": true, 14 | "_": true, 15 | "closeTo": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/utils/getAPIVersion.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import getAPIVersion from "../../src/utils/getAPIVersion"; 3 | 4 | describe("utils/getAPIVersion(): string", () => { 5 | it("works", () => { 6 | const pkg = require("../../package.json"); 7 | 8 | assert(getAPIVersion() === pkg.version); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/utils/format.js: -------------------------------------------------------------------------------- 1 | export default function format(text) { 2 | text = text.trim(); 3 | text = text.replace(/\$a (\w)/g, (_, a) => { 4 | if (/[aiueo]/i.test(a)) { 5 | return "an " + a; 6 | } 7 | return "a " + a; 8 | }); 9 | text = text.replace(/{{(\w+)}}/g, "$1"); 10 | text = text.replace(/^ +/gm, ""); 11 | return text; 12 | } 13 | -------------------------------------------------------------------------------- /test/utils/toNodeName.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import toNodeName from "../../src/utils/toNodeName"; 3 | 4 | describe("utils/toNodeName(obj: object): string", () => { 5 | it("works", () => { 6 | assert(toNodeName({ $name: "name" }) === "name"); 7 | assert(toNodeName({ $name: "name", $id: "id" }) === "name#id"); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/dom/Element.js: -------------------------------------------------------------------------------- 1 | import inLaws from "../utils/inLaws"; 2 | import EventTarget from "./EventTarget"; 3 | 4 | global.Element = global.Element || class Element extends EventTarget { 5 | constructor() { 6 | super(); 7 | throw new TypeError("Illegal constructor"); 8 | } 9 | }; 10 | 11 | export default class Element extends inLaws(global.Element) {} 12 | -------------------------------------------------------------------------------- /src/utils/toJSON.js: -------------------------------------------------------------------------------- 1 | import toNodeName from "./toNodeName"; 2 | 3 | export default function toJSON(node, func, memo = []) { 4 | let result; 5 | 6 | if (memo.indexOf(node) !== -1) { 7 | return ``; 8 | } 9 | memo.push(node); 10 | 11 | result = func(node, memo); 12 | 13 | memo.pop(); 14 | 15 | return result; 16 | } 17 | -------------------------------------------------------------------------------- /src/dom/HTMLElement.js: -------------------------------------------------------------------------------- 1 | import inLaws from "../utils/inLaws"; 2 | import Element from "./Element"; 3 | 4 | global.HTMLElement = global.HTMLElement || class HTMLElement extends Element { 5 | constructor() { 6 | super(); 7 | throw new TypeError("Illegal constructor"); 8 | } 9 | }; 10 | 11 | export default class HTMLElement extends inLaws(global.HTMLElement) {} 12 | -------------------------------------------------------------------------------- /src/dom/MediaStream.js: -------------------------------------------------------------------------------- 1 | import inLaws from "../utils/inLaws"; 2 | import EventTarget from "./EventTarget"; 3 | 4 | global.MediaStream = global.MediaStream || class MediaStream extends EventTarget { 5 | constructor() { 6 | super(); 7 | throw new TypeError("Illegal constructor"); 8 | } 9 | }; 10 | 11 | export default class MediaStream extends inLaws(global.MediaStream) {} 12 | -------------------------------------------------------------------------------- /test/bootstrap/bootstrap.js: -------------------------------------------------------------------------------- 1 | global.window = global; 2 | global.window.location = global.window.location || {}; 3 | 4 | global.assert = require("power-assert"); 5 | global.sinon = require("sinon"); 6 | global._ = require("lodash"); 7 | 8 | global.closeTo = function(actual, expected, delta) { 9 | return Math.abs(actual - expected) <= delta; 10 | }; 11 | 12 | require("../../src"); 13 | -------------------------------------------------------------------------------- /test/utils/defaults.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import defaults from "../../src/utils/defaults"; 3 | 4 | describe("utils/defaults(value: any, defaultValue: any): any", () => { 5 | it("works", () => { 6 | assert(defaults(0, 1) === 0); 7 | assert(defaults(1, 2) === 1); 8 | assert(defaults(undefined, 0) === 0); 9 | assert(defaults(undefined, 1) === 1); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/dom/HTMLMediaElement.js: -------------------------------------------------------------------------------- 1 | import inLaws from "../utils/inLaws"; 2 | import HTMLElement from "./HTMLElement"; 3 | 4 | global.HTMLMediaElement = global.HTMLMediaElement || class HTMLMediaElement extends HTMLElement { 5 | constructor() { 6 | super(); 7 | throw new TypeError("Illegal constructor"); 8 | } 9 | }; 10 | 11 | export default class HTMLMediaElement extends inLaws(global.HTMLMediaElement) {} 12 | -------------------------------------------------------------------------------- /test/dom/Element.js: -------------------------------------------------------------------------------- 1 | describe("Element", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | 4 | describe("constructor()", function() { 5 | it("works", function() { 6 | var element = new WebAudioTestAPI.Element(); 7 | 8 | assert(element instanceof global.window.Element); 9 | }); 10 | it("not work when 'new' directly", function() { 11 | assert.throws(function() { new global.Element(); }, TypeError); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/validators/index.js: -------------------------------------------------------------------------------- 1 | export isAudioSource from "./isAudioSource"; 2 | export isBoolean from "./isBoolean"; 3 | export isFunction from "./isFunction"; 4 | export isInstanceOf from "./isInstanceOf"; 5 | export isInteger from "./isInteger"; 6 | export isNullOrInstanceOf from "./isNullOrInstanceOf"; 7 | export isNumber from "./isNumber"; 8 | export isPositiveInteger from "./isPositiveInteger"; 9 | export isPositiveNumber from "./isPositiveNumber"; 10 | export isString from "./isString"; 11 | -------------------------------------------------------------------------------- /test/utils/toSeconds.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import toSeconds from "../../src/utils/toSeconds"; 3 | 4 | describe("utils/toSeconds(time: number|string): number", () => { 5 | it("works", () => { 6 | assert(toSeconds(1.500) === 1.500); 7 | assert(toSeconds(NaN) === 0); 8 | assert(toSeconds("00:00.250") === 0.250); 9 | assert(toSeconds("00:01.250") === 1.250); 10 | assert(toSeconds("01:01.250") === 61.250); 11 | assert(toSeconds(null) === 0); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/utils/removeIfExists.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import removeIfExists from "../../src/utils/removeIfExists"; 3 | 4 | describe("utils/removeIfExists(list: any[], value: any): any[]", () => { 5 | it("works", () => { 6 | let list = [ 1, 2, 3, 4, 5 ]; 7 | 8 | let result1 = removeIfExists(list, 3); 9 | let result2 = removeIfExists(list, 0); 10 | 11 | assert.deepEqual(list, [ 1, 2, 4, 5 ]); 12 | assert(result1 === 3); 13 | assert(result2 === null); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/dom/Event.js: -------------------------------------------------------------------------------- 1 | describe("Event", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | 4 | function Foo() {} 5 | 6 | describe("constructor()", function() { 7 | it("works", function() { 8 | var foo = new Foo(); 9 | var e = new WebAudioTestAPI.Event("name", foo); 10 | 11 | assert(e instanceof global.window.Event); 12 | assert(e.type === "name"); 13 | assert(e.target === foo); 14 | assert(typeof e.timestamp === "number"); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/utils/appendIfNotExists.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import appendIfNotExists from "../../src/utils/appendIfNotExists"; 3 | 4 | describe("utils/appendIfNotExists(list: any[], value: any): void", () => { 5 | it("works", () => { 6 | let list = []; 7 | 8 | appendIfNotExists(list, 1); 9 | appendIfNotExists(list, 2); 10 | appendIfNotExists(list, 1); 11 | appendIfNotExists(list, 2); 12 | appendIfNotExists(list, 3); 13 | 14 | assert.deepEqual(list, [ 1, 2, 3 ]); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/MediaStreamAudioDestinationNode.js: -------------------------------------------------------------------------------- 1 | import AudioNode from "./AudioNode"; 2 | 3 | export default class MediaStreamAudioDestinationNode extends AudioNode { 4 | static $JSONKeys = []; 5 | 6 | constructor(admission, context) { 7 | super(admission, { 8 | name: "MediaStreamAudioDestinationNode", 9 | context: context, 10 | numberOfInputs: 1, 11 | numberOfOutputs: 0, 12 | channelCount: 2, 13 | channelCountMode: "explicit", 14 | channelInterpretation: "speakers" 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/utils/toMicroseconds.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import toMicroseconds from "../../src/utils/toMicroseconds"; 3 | 4 | describe("utils/toMicroseconds(time: number|string): number", () => { 5 | it("works", () => { 6 | assert(toMicroseconds(1.5) === 1500000); 7 | assert(toMicroseconds(NaN) === 0); 8 | assert(toMicroseconds("00:00.250") === 250000); 9 | assert(toMicroseconds("00:01.250") === 1250000); 10 | assert(toMicroseconds("01:01.250") === 61250000); 11 | assert(toMicroseconds(null) === 0); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/dom/HTMLElement.js: -------------------------------------------------------------------------------- 1 | describe("HTMLElement", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | 4 | describe("constructor()", function() { 5 | it("works", function() { 6 | var element = new WebAudioTestAPI.HTMLElement(); 7 | 8 | assert(element instanceof global.window.HTMLElement); 9 | assert(element instanceof global.window.Element); 10 | }); 11 | it("not work when 'new' directly", function() { 12 | assert.throws(function() { new global.HTMLElement(); }, TypeError); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/dom/MediaStream.js: -------------------------------------------------------------------------------- 1 | describe("MediaStream", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | 4 | describe("constructor()", function() { 5 | it("works", function() { 6 | var stream = new WebAudioTestAPI.MediaStream(); 7 | 8 | assert(stream instanceof global.window.MediaStream); 9 | assert(stream instanceof global.window.EventTarget); 10 | }); 11 | it("not work when 'new' directly", function() { 12 | assert.throws(function() { new global.MediaStream(); }, TypeError); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/decorators/props/readonly.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import * as props from "../../../src/decorators/props"; 3 | 4 | describe("@props.readonly()", () => { 5 | it("defines a readonly property", () => { 6 | class Foo { 7 | constructor() { 8 | this._ = {}; 9 | } 10 | 11 | @props.readonly() 12 | bar() { 13 | return "bar"; 14 | } 15 | } 16 | 17 | const foo = new Foo(); 18 | 19 | assert(foo.bar === "bar"); 20 | assert.throws(() => { foo.bar = "baz"; }, TypeError); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/GainNode.js: -------------------------------------------------------------------------------- 1 | import AudioNode from "./AudioNode"; 2 | import * as props from "./decorators/props"; 3 | 4 | export default class GainNode extends AudioNode { 5 | static $JSONKeys = [ "gain" ]; 6 | 7 | constructor(admission, context) { 8 | super(admission, { 9 | name: "GainNode", 10 | context: context, 11 | numberOfInputs: 1, 12 | numberOfOutputs: 1, 13 | channelCount: 2, 14 | channelCountMode: "max", 15 | channelInterpretation: "speakers" 16 | }); 17 | } 18 | 19 | @props.audioparam(1) 20 | gain() {} 21 | } 22 | -------------------------------------------------------------------------------- /test/dom/HTMLMediaElement.js: -------------------------------------------------------------------------------- 1 | describe("HTMLMediaElement", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | 4 | describe("constructor()", function() { 5 | it("works", function() { 6 | var element = new WebAudioTestAPI.HTMLMediaElement(); 7 | 8 | assert(element instanceof global.window.HTMLMediaElement); 9 | assert(element instanceof global.window.HTMLElement); 10 | }); 11 | it("not work when 'new' directly", function() { 12 | assert.throws(function() { new global.HTMLMediaElement(); }, TypeError); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/StereoPannerNode.js: -------------------------------------------------------------------------------- 1 | import AudioNode from "./AudioNode"; 2 | import * as props from "./decorators/props"; 3 | 4 | export default class StereoPannerNode extends AudioNode { 5 | static $JSONKeys = [ "pan" ]; 6 | 7 | constructor(admission, context) { 8 | super(admission, { 9 | name: "StereoPannerNode", 10 | context: context, 11 | numberOfInputs: 1, 12 | numberOfOutputs: 1, 13 | channelCount: 2, 14 | channelCountMode: "clamped-max", 15 | channelInterpretation: "speakers" 16 | }); 17 | } 18 | 19 | @props.audioparam(0) 20 | pan() {} 21 | } 22 | -------------------------------------------------------------------------------- /src/AudioProcessingEvent.js: -------------------------------------------------------------------------------- 1 | import Immigration from "./utils/Immigration"; 2 | import Event from "./dom/Event"; 3 | 4 | let immigration = Immigration.getInstance(); 5 | 6 | export default class AudioProcessingEvent extends Event { 7 | constructor(admission, node) { 8 | immigration.check(admission, () => { 9 | throw new TypeError("Illegal constructor"); 10 | }); 11 | super("audioprocess", node); 12 | 13 | this._.node = node; 14 | } 15 | 16 | get $name() { 17 | return "AudioProcessingEvent"; 18 | } 19 | 20 | get $node() { 21 | return this._.node; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AudioDestinationNode.js: -------------------------------------------------------------------------------- 1 | import AudioNode from "./AudioNode"; 2 | import * as props from "./decorators/props"; 3 | 4 | export default class AudioDestinationNode extends AudioNode { 5 | static $JSONKeys = []; 6 | 7 | constructor(admission, context) { 8 | super(admission, { 9 | name: "AudioDestinationNode", 10 | context: context, 11 | numberOfInputs: 1, 12 | numberOfOutputs: 0, 13 | channelCount: 2, 14 | channelCountMode: "explicit", 15 | channelInterpretation: "speakers" 16 | }); 17 | } 18 | 19 | @props.readonly(2) 20 | maxChannelCount() {} 21 | } 22 | -------------------------------------------------------------------------------- /src/OfflineAudioCompletionEvent.js: -------------------------------------------------------------------------------- 1 | import Immigration from "./utils/Immigration"; 2 | import Event from "./dom/Event"; 3 | 4 | let immigration = Immigration.getInstance(); 5 | 6 | export default class OfflineAudioCompletionEvent extends Event { 7 | constructor(admission, node) { 8 | immigration.check(admission, () => { 9 | throw new TypeError("Illegal constructor"); 10 | }); 11 | super("complete", node); 12 | 13 | this._.node = node; 14 | } 15 | 16 | get $name() { 17 | return "OfflineAudioCompletionEvent"; 18 | } 19 | 20 | get $node() { 21 | return this._.node; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/decorators/props/on.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import * as props from "../../../src/decorators/props"; 3 | 4 | describe("@props.on()", () => { 5 | it("defines a callback property", () => { 6 | class Foo { 7 | constructor() { 8 | this._ = {}; 9 | } 10 | 11 | @props.on("end") 12 | onend() {} 13 | } 14 | 15 | const foo = new Foo(); 16 | 17 | assert(foo.onend === null); 18 | assert.doesNotThrow(() => { foo.onend = it; }); 19 | assert(foo.onend === it); 20 | assert.throws(() => { foo.onend = "not a function"; }, TypeError); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/utils/inLaws.js: -------------------------------------------------------------------------------- 1 | import inLaws from "../../src/utils/inLaws"; 2 | 3 | describe("inLaws(superClass: Class): Class", () => { 4 | it("works", () => { 5 | class Foo { 6 | constructor() { 7 | throw new Error("Illegal constructor"); 8 | } 9 | } 10 | 11 | class Bar1 extends Foo {} 12 | 13 | class Bar2 extends inLaws(Foo) {} 14 | 15 | assert.throws(() => { 16 | return new Bar1(); 17 | }, (e) => { 18 | return e instanceof Error && e.message === "Illegal constructor"; 19 | }); 20 | 21 | assert.doesNotThrow(() => { 22 | return new Bar2(); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/decorators/props/enums.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import * as props from "../../../src/decorators/props"; 3 | 4 | describe("@props.enums(values: any[], [ defaultValue: any ])", () => { 5 | it("defines an enum property", () => { 6 | class Foo { 7 | constructor() { 8 | this._ = {}; 9 | } 10 | 11 | @props.enums([ "a", "b", "c" ]) 12 | bar() {} 13 | } 14 | 15 | const foo = new Foo(); 16 | 17 | assert(foo.bar === "a"); 18 | assert.doesNotThrow(() => { foo.bar = "c"; }); 19 | assert(foo.bar === "c"); 20 | assert.throws(() => { foo.bar = "d"; }, TypeError); }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/decorators/props/audioparam.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import AudioParam from "../../../src/AudioParam"; 3 | import * as props from "../../../src/decorators/props"; 4 | 5 | describe("@props.audioparam(defaultValue: number)", () => { 6 | it("defines an AudioParam property", () => { 7 | class Foo { 8 | constructor() { 9 | this._ = {}; 10 | } 11 | 12 | @props.audioparam(100) 13 | bar() {} 14 | } 15 | 16 | const foo = new Foo(); 17 | 18 | assert(foo.bar instanceof AudioParam); 19 | assert(foo.bar.defaultValue === 100); 20 | assert.throws(() => { foo.bar = 0; }, TypeError); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/validators/isInstanceOf.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import isInstanceOf from "../../src/validators/isInstanceOf"; 3 | 4 | describe("validators.isInstanceOf(klass: function): object", () => { 5 | describe(".typeName: string", () => { 6 | it("works", () => { 7 | assert(isInstanceOf(Float32Array).typeName === "Float32Array"); 8 | }); 9 | }); 10 | describe(".test(value: any): boolean", () => { 11 | it("works", () => { 12 | assert(isInstanceOf(Float32Array).test(null) === false); 13 | assert(isInstanceOf(Float32Array).test(new Float32Array()) === true); 14 | assert(isInstanceOf(Float32Array).test(new Uint8Array()) === false); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/decorators/props/typed.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import * as props from "../../../src/decorators/props"; 3 | 4 | describe("@props.typed(validator: object, defaultValue: any)", () => { 5 | it("defines a callback property", () => { 6 | const isNumber = { typeName: "number", test: value => typeof value === "number" }; 7 | 8 | class Foo { 9 | constructor() { 10 | this._ = {}; 11 | } 12 | 13 | @props.typed(isNumber, 0) 14 | bar() {} 15 | } 16 | 17 | const foo = new Foo(); 18 | 19 | assert(foo.bar === 0); 20 | assert.doesNotThrow(() => { foo.bar = 10; }); 21 | assert(foo.bar === 10); 22 | assert.throws(() => { foo.bar = "not a number"; }, TypeError); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/WaveShaperNode.js: -------------------------------------------------------------------------------- 1 | import AudioNode from "./AudioNode"; 2 | import * as props from "./decorators/props"; 3 | import * as validators from "./validators"; 4 | 5 | export default class WaveShaperNode extends AudioNode { 6 | static $JSONKeys = [ "oversample" ]; 7 | 8 | constructor(admission, context) { 9 | super(admission, { 10 | name: "WaveShaperNode", 11 | context: context, 12 | numberOfInputs: 1, 13 | numberOfOutputs: 1, 14 | channelCount: 2, 15 | channelCountMode: "max", 16 | channelInterpretation: "speakers" 17 | }); 18 | } 19 | 20 | @props.typed(validators.isNullOrInstanceOf(Float32Array), null) 21 | curve() {} 22 | 23 | @props.enums([ "none", "2x", "4x" ]) 24 | oversample() {} 25 | } 26 | -------------------------------------------------------------------------------- /src/dom/Event.js: -------------------------------------------------------------------------------- 1 | import inLaws from "../utils/inLaws"; 2 | import defaults from "../utils/defaults"; 3 | 4 | global.Event = global.Event || class Event { 5 | constructor() { 6 | throw new TypeError("Illegal constructor"); 7 | } 8 | }; 9 | 10 | export default class Event extends inLaws(global.Event) { 11 | constructor(name, target) { 12 | super(); 13 | 14 | Object.defineProperty(this, "_", { value: {} }); 15 | 16 | this._.type = name; 17 | this._.target = defaults(target, null); 18 | this._.timestamp = Date.now(); 19 | } 20 | 21 | get type() { 22 | return this._.type; 23 | } 24 | 25 | get target() { 26 | return this._.target; 27 | } 28 | 29 | get timestamp() { 30 | return this._.timestamp; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/validators/isNullOrInstanceOf.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import isNullOrInstanceOf from "../../src/validators/isNullOrInstanceOf"; 3 | 4 | describe("validators.isNullOrInstanceOf(klass: function): object", () => { 5 | describe(".typeName: string", () => { 6 | it("works", () => { 7 | assert(isNullOrInstanceOf(Float32Array).typeName === "Float32Array|null"); 8 | }); 9 | }); 10 | describe(".test(value: any): boolean", () => { 11 | it("works", () => { 12 | assert(isNullOrInstanceOf(Float32Array).test(null) === true); 13 | assert(isNullOrInstanceOf(Float32Array).test(new Float32Array()) === true); 14 | assert(isNullOrInstanceOf(Float32Array).test(new Uint8Array()) === false); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/ChannelMergerNode.js: -------------------------------------------------------------------------------- 1 | import AudioNode from "./AudioNode"; 2 | import * as methods from "./decorators/methods"; 3 | import * as validators from "./validators"; 4 | 5 | export default class ChannelMergerNode extends AudioNode { 6 | static $JSONKeys = []; 7 | 8 | constructor(admission, context, numberOfInputs) { 9 | super(admission, { 10 | name: "ChannelMergerNode", 11 | context: context, 12 | numberOfInputs: numberOfInputs, 13 | numberOfOutputs: 1, 14 | channelCount: 2, 15 | channelCountMode: "max", 16 | channelInterpretation: "speakers" 17 | }); 18 | this.__createChannelMerger(numberOfInputs); 19 | } 20 | 21 | @methods.param("numberOfInputs", validators.isPositiveInteger) 22 | __createChannelMerger() {} 23 | } 24 | -------------------------------------------------------------------------------- /src/ChannelSplitterNode.js: -------------------------------------------------------------------------------- 1 | import AudioNode from "./AudioNode"; 2 | import * as methods from "./decorators/methods"; 3 | import * as validators from "./validators"; 4 | 5 | export default class ChannelSplitterNode extends AudioNode { 6 | static $JSONKeys = []; 7 | 8 | constructor(admission, context, numberOfOutputs) { 9 | super(admission, { 10 | name: "ChannelSplitterNode", 11 | context: context, 12 | numberOfInputs: 1, 13 | numberOfOutputs: numberOfOutputs, 14 | channelCount: 2, 15 | channelCountMode: "max", 16 | channelInterpretation: "speakers" 17 | }); 18 | this.__createChannelSplitter(numberOfOutputs); 19 | } 20 | 21 | @methods.param("numberOfOutputs", validators.isPositiveInteger) 22 | __createChannelSplitter() {} 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/toS.js: -------------------------------------------------------------------------------- 1 | export default function toS(value) { 2 | if (value === null || typeof value === "undefined") { 3 | return String(value); 4 | } 5 | let type = typeof value; 6 | 7 | if (type === "number" || type === "boolean") { 8 | return String(value); 9 | } 10 | 11 | if (type === "string") { 12 | return `'${value}'`; 13 | } 14 | 15 | if (Array.isArray(value)) { 16 | return `[ ${value.map(toS).join(", ")} ]`; 17 | } 18 | 19 | if (value.constructor === {}.constructor) { 20 | return "{ " + Object.keys(value).map((key) => { 21 | return key + ": " + toS(value[key]); 22 | }).join(", ") + "}"; 23 | } 24 | 25 | let name = value.constructor.name || Object.prototype.toString.call(value).slice(8, -1); 26 | 27 | return `a ${name}`; 28 | } 29 | -------------------------------------------------------------------------------- /src/ConvolverNode.js: -------------------------------------------------------------------------------- 1 | import AudioNode from "./AudioNode"; 2 | import AudioBuffer from "./AudioBuffer"; 3 | import * as props from "./decorators/props"; 4 | import * as validators from "./validators"; 5 | 6 | export default class ConvolverNode extends AudioNode { 7 | static $JSONKeys = [ "normalize" ]; 8 | 9 | constructor(admission, context) { 10 | super(admission, { 11 | name: "ConvolverNode", 12 | context: context, 13 | numberOfInputs: 1, 14 | numberOfOutputs: 1, 15 | channelCount: 2, 16 | channelCountMode: "clamped-max", 17 | channelInterpretation: "speakers" 18 | }); 19 | } 20 | 21 | @props.typed(validators.isNullOrInstanceOf(AudioBuffer), null) 22 | buffer() {} 23 | 24 | @props.typed(validators.isBoolean, true) 25 | normalize() {} 26 | } 27 | -------------------------------------------------------------------------------- /src/MediaStreamAudioSourceNode.js: -------------------------------------------------------------------------------- 1 | import AudioNode from "./AudioNode"; 2 | import MediaStream from "./dom/MediaStream"; 3 | import * as methods from "./decorators/methods"; 4 | import * as validators from "./validators"; 5 | 6 | export default class MediaStreamAudioSourceNode extends AudioNode { 7 | static $JSONKeys = []; 8 | 9 | constructor(admission, context, mediaStream) { 10 | super(admission, { 11 | name: "MediaStreamAudioSourceNode", 12 | context: context, 13 | numberOfInputs: 0, 14 | numberOfOutputs: 1, 15 | channelCount: 2, 16 | channelCountMode: "max", 17 | channelInterpretation: "speakers" 18 | }); 19 | this.__createMediaStreamSource(mediaStream); 20 | } 21 | 22 | @methods.param("mediaStream", validators.isInstanceOf(MediaStream)) 23 | __createMediaStreamSource() {} 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/toMicroseconds.js: -------------------------------------------------------------------------------- 1 | const MIN_MICRO_SECONDS = 0; 2 | const MAX_MICRO_SECONDS = 24 * 60 * 60 * 1000 * 1000; 3 | 4 | export default function toMicroseconds(time) { 5 | let value = 0; 6 | 7 | if (typeof time === "number") { 8 | // seconds -> microseconds 9 | value = Math.floor(time * 1000 * 1000) || 0; 10 | return Math.max(MIN_MICRO_SECONDS, Math.min(value, MAX_MICRO_SECONDS)); 11 | } 12 | 13 | let matches = /^([0-5]\d):([0-5]\d)\.(\d\d\d)$/.exec(time); 14 | 15 | if (matches) { 16 | // minutes 17 | value += +matches[1]; 18 | value *= 60; 19 | // seconds 20 | value += +matches[2]; 21 | value *= 1000; 22 | // milliseconds 23 | value += +matches[3]; 24 | value *= 1000; 25 | return Math.max(MIN_MICRO_SECONDS, Math.min(value, MAX_MICRO_SECONDS)); 26 | } 27 | 28 | return value; 29 | } 30 | -------------------------------------------------------------------------------- /src/MediaElementAudioSourceNode.js: -------------------------------------------------------------------------------- 1 | import AudioNode from "./AudioNode"; 2 | import HTMLMediaElement from "./dom/HTMLMediaElement"; 3 | import * as methods from "./decorators/methods"; 4 | import * as validators from "./validators"; 5 | 6 | export default class MediaElementAudioSourceNode extends AudioNode { 7 | static $JSONKeys = []; 8 | 9 | constructor(admission, context, mediaElement) { 10 | super(admission, { 11 | name: "MediaElementAudioSourceNode", 12 | context: context, 13 | numberOfInputs: 0, 14 | numberOfOutputs: 1, 15 | channelCount: 2, 16 | channelCountMode: "max", 17 | channelInterpretation: "speakers" 18 | }); 19 | this.__createMediaElementSource(mediaElement); 20 | } 21 | 22 | @methods.param("mediaElement", validators.isInstanceOf(HTMLMediaElement)) 23 | __createMediaElementSource() {} 24 | } 25 | -------------------------------------------------------------------------------- /test/utils/toS.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import toS from "../../src/utils/toS"; 3 | 4 | describe("utils/toS(value: any): string", () => { 5 | it("works", () => { 6 | let f32 = new Float32Array(100); 7 | let i16 = new Int16Array(100); 8 | 9 | assert(toS(null) === "null"); 10 | assert(toS(undefined) === "undefined"); 11 | assert(toS(true) === "true"); 12 | assert(toS(false) === "false"); 13 | assert(toS(10000) === "10000"); 14 | assert(toS(100.5) === "100.5"); 15 | assert(toS("abc") === "'abc'"); 16 | assert(toS([ 1, [ 2, 3 ] ]) === "[ 1, [ 2, 3 ] ]"); 17 | assert(toS({ a: 1, b: { c: [ 2, 3 ] } }), "{ a: 1, b: { c: [ 2, 3 ] } }"); 18 | assert(toS(f32) === "a Float32Array"); 19 | assert(toS(i16) === "a Int16Array"); 20 | assert(toS({ constructor: { name: "" } }) === "a Object"); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/decorators/methods/contract.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import * as methods from "../../../src/decorators/methods"; 3 | 4 | describe("@methods.contract({ precondition, postcondition })", () => { 5 | it("defines precondition/postcondition", () => { 6 | class Foo { 7 | @methods.contract({ 8 | precondition(x) { 9 | if (x === 0) { 10 | throw new TypeError("x is 0"); 11 | } 12 | }, 13 | postcondition(y) { 14 | if (y % 2 === 1) { 15 | throw new TypeError("y is odd"); 16 | } 17 | } 18 | }) 19 | bar(x) { 20 | return x; 21 | } 22 | } 23 | 24 | const foo = new Foo(); 25 | 26 | assert.doesNotThrow(() => { foo.bar(10); }); 27 | assert.throws(() => { foo.bar(0); }, TypeError); 28 | assert.throws(() => { foo.bar(5); }, TypeError); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/DynamicsCompressorNode.js: -------------------------------------------------------------------------------- 1 | import AudioNode from "./AudioNode"; 2 | import * as props from "./decorators/props"; 3 | 4 | export default class DynamicsCompressorNode extends AudioNode { 5 | static $JSONKeys = [ "threshold", "knee", "ratio", "reduction", "attack", "release" ]; 6 | 7 | constructor(admission, context) { 8 | super(admission, { 9 | name: "DynamicsCompressorNode", 10 | context: context, 11 | numberOfInputs: 1, 12 | numberOfOutputs: 1, 13 | channelCount: 2, 14 | channelCountMode: "explicit", 15 | channelInterpretation: "speakers" 16 | }); 17 | } 18 | 19 | @props.audioparam(-24) 20 | threshold() {} 21 | 22 | @props.audioparam(30) 23 | knee() {} 24 | 25 | @props.audioparam(12) 26 | ratio() {} 27 | 28 | @props.audioparam(0) 29 | reduction() {} 30 | 31 | @props.audioparam(0.003) 32 | attack() {} 33 | 34 | @props.audioparam(0.25) 35 | release() {} 36 | } 37 | -------------------------------------------------------------------------------- /test/validators/isNumber.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import isNumber from "../../src/validators/isNumber"; 3 | 4 | describe("validators.isNumber", () => { 5 | describe(".typeName: string", () => { 6 | it("works", () => { 7 | assert(isNumber.typeName === "number"); 8 | }); 9 | }); 10 | describe(".test(value: any): boolean", () => { 11 | it("works", () => { 12 | assert(isNumber.test(-1.5) === true); 13 | assert(isNumber.test(-1) === true); 14 | assert(isNumber.test(0) === true); 15 | assert(isNumber.test(1) === true); 16 | assert(isNumber.test(1.5) === true); 17 | assert(isNumber.test(true) === false); 18 | assert(isNumber.test(false) === false); 19 | assert(isNumber.test("0") === false); 20 | assert(isNumber.test(it) === false); 21 | assert(isNumber.test(NaN) === false); 22 | assert(isNumber.test(null) === false); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/validators/isString.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import isString from "../../src/validators/isString"; 3 | 4 | describe("validators.isString", () => { 5 | describe(".typeName: string", () => { 6 | it("works", () => { 7 | assert(isString.typeName === "string"); 8 | }); 9 | }); 10 | describe(".test(value: any): boolean", () => { 11 | it("works", () => { 12 | assert(isString.test(-1.5) === false); 13 | assert(isString.test(-1) === false); 14 | assert(isString.test(0) === false); 15 | assert(isString.test(1) === false); 16 | assert(isString.test(1.5) === false); 17 | assert(isString.test(true) === false); 18 | assert(isString.test(false) === false); 19 | assert(isString.test("0") === true); 20 | assert(isString.test(it) === false); 21 | assert(isString.test(NaN) === false); 22 | assert(isString.test(null) === false); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/validators/isBoolean.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import isBoolean from "../../src/validators/isBoolean"; 3 | 4 | describe("validators.isBoolean", () => { 5 | describe(".typeName: string", () => { 6 | it("works", () => { 7 | assert(isBoolean.typeName === "boolean"); 8 | }); 9 | }); 10 | describe(".test(value: any): boolean", () => { 11 | it("works", () => { 12 | assert(isBoolean.test(-1.5) === false); 13 | assert(isBoolean.test(-1) === false); 14 | assert(isBoolean.test(0) === false); 15 | assert(isBoolean.test(1) === false); 16 | assert(isBoolean.test(1.5) === false); 17 | assert(isBoolean.test(true) === true); 18 | assert(isBoolean.test(false) === true); 19 | assert(isBoolean.test("0") === false); 20 | assert(isBoolean.test(it) === false); 21 | assert(isBoolean.test(NaN) === false); 22 | assert(isBoolean.test(null) === false); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/validators/isInteger.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import isInteger from "../../src/validators/isInteger"; 3 | 4 | describe("validators.isInteger", () => { 5 | describe(".typeName: string", () => { 6 | it("works", () => { 7 | assert(isInteger.typeName === "number"); 8 | }); 9 | }); 10 | describe(".test(value: any): boolean", () => { 11 | it("works", () => { 12 | assert(isInteger.test(-1.5) === false); 13 | assert(isInteger.test(-1) === true); 14 | assert(isInteger.test(0) === true); 15 | assert(isInteger.test(1) === true); 16 | assert(isInteger.test(1.5) === false); 17 | assert(isInteger.test(true) === false); 18 | assert(isInteger.test(false) === false); 19 | assert(isInteger.test("0") === false); 20 | assert(isInteger.test(it) === false); 21 | assert(isInteger.test(NaN) === false); 22 | assert(isInteger.test(null) === false); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/DelayNode.js: -------------------------------------------------------------------------------- 1 | import AudioNode from "./AudioNode"; 2 | import * as props from "./decorators/props"; 3 | import * as methods from "./decorators/methods"; 4 | import * as validators from "./validators"; 5 | 6 | export default class DelayNode extends AudioNode { 7 | static $JSONKeys = [ "delayTime" ] 8 | 9 | constructor(admission, context, maxDelayTime) { 10 | super(admission, { 11 | name: "DelayNode", 12 | context: context, 13 | numberOfInputs: 1, 14 | numberOfOutputs: 1, 15 | channelCount: 2, 16 | channelCountMode: "max", 17 | channelInterpretation: "speakers" 18 | }); 19 | this.__createDelay(maxDelayTime); 20 | } 21 | 22 | @methods.param("maxDelayTime", validators.isPositiveNumber) 23 | __createDelay(maxDelayTime) { 24 | this._.maxDelayTime = maxDelayTime; 25 | } 26 | 27 | @props.audioparam(0) 28 | delayTime() {} 29 | 30 | get $maxDelayTime() { 31 | return this._.maxDelayTime; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/validators/isFunction.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import isFunction from "../../src/validators/isFunction"; 3 | 4 | describe("validators.isFunction", () => { 5 | describe(".typeName: string", () => { 6 | it("works", () => { 7 | assert(isFunction.typeName === "function"); 8 | }); 9 | }); 10 | describe(".test(value: any): boolean", () => { 11 | it("works", () => { 12 | assert(isFunction.test(-1.5) === false); 13 | assert(isFunction.test(-1) === false); 14 | assert(isFunction.test(0) === false); 15 | assert(isFunction.test(1) === false); 16 | assert(isFunction.test(1.5) === false); 17 | assert(isFunction.test(true) === false); 18 | assert(isFunction.test(false) === false); 19 | assert(isFunction.test("0") === false); 20 | assert(isFunction.test(it) === true); 21 | assert(isFunction.test(NaN) === false); 22 | assert(isFunction.test(null) === false); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/utils/Immigration.js: -------------------------------------------------------------------------------- 1 | let instance = null; 2 | 3 | export default class Immigration { 4 | constructor() { 5 | this._admissions = []; 6 | } 7 | 8 | static getInstance() { 9 | if (instance === null) { 10 | instance = new Immigration(); 11 | } 12 | return instance; 13 | } 14 | 15 | apply(fn) { 16 | let admission1 = { token: {}, count: 0 }; 17 | 18 | this._admissions.push(admission1); 19 | 20 | let result = fn(admission1.token); 21 | 22 | let admission2 = this._admissions.pop(); 23 | 24 | if (admission1 !== admission2 || admission2.count !== 1) { 25 | throw new Error("invalid admission"); 26 | } 27 | 28 | return result; 29 | } 30 | 31 | check(token, errorCallback) { 32 | let lastAdmission = this._admissions.pop(); 33 | 34 | if (!lastAdmission || lastAdmission.token !== token || lastAdmission.count++ !== 0) { 35 | errorCallback(); 36 | } 37 | 38 | this._admissions.push(lastAdmission); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/api.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "AnalyserNode#getFloatTimeDomainData": { 3 | states: [ "disabled", "enabled" ] 4 | }, 5 | "AudioBuffer#copyToChannel": { 6 | states: [ "disabled", "enabled" ] 7 | }, 8 | "AudioBuffer#copyFromChannel": { 9 | states: [ "disabled", "enabled" ] 10 | }, 11 | "AudioContext#createAudioWorker": { 12 | states: [ "disabled", "enabled" ] 13 | }, 14 | "AudioContext#createStereoPanner": { 15 | states: [ "disabled", "enabled" ] 16 | }, 17 | "AudioContext#decodeAudioData": { 18 | states: [ "void", "promise" ] 19 | }, 20 | "AudioContext#close": { 21 | states: [ "disabled", "enabled" ] 22 | }, 23 | "AudioContext#suspend": { 24 | states: [ "disabled", "enabled" ] 25 | }, 26 | "AudioContext#resume": { 27 | states: [ "disabled", "enabled" ] 28 | }, 29 | "OfflineAudioContext#startRendering": { 30 | states: [ "void", "promise" ] 31 | }, 32 | "AudioNode#disconnect": { 33 | states: [ "channel", "selective" ] 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /test/decorators/methods/param.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import * as methods from "../../../src/decorators/methods"; 3 | 4 | describe("@methods.param(name: string, validator: object)", () => { 5 | it("defines function parameters", () => { 6 | const isNumber = { typeName: "number", test: value => typeof value === "number" }; 7 | const isString = { typeName: "string", test: value => typeof value === "string" }; 8 | 9 | class Foo { 10 | constructor() { 11 | this._ = {}; 12 | } 13 | 14 | @methods.param("x", isNumber) 15 | @methods.param("y", isNumber) 16 | @methods.param("[ z ]", isString) 17 | bar() {} 18 | } 19 | 20 | const foo = new Foo(); 21 | 22 | assert.doesNotThrow(() => { foo.bar(10, 20, "30"); }); 23 | assert.doesNotThrow(() => { foo.bar(10, 20); }); 24 | assert.throws(() => { foo.bar(10); }, TypeError); 25 | assert.throws(() => { foo.bar(10, "20"); }, TypeError); 26 | assert.throws(() => { foo.bar(10, 20, 30); }, TypeError); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/validators/isPositiveNumber.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import isPositiveNumber from "../../src/validators/isPositiveNumber"; 3 | 4 | describe("validators.isPositiveNumber", () => { 5 | describe(".typeName: string", () => { 6 | it("works", () => { 7 | assert(isPositiveNumber.typeName === "number"); 8 | }); 9 | }); 10 | describe(".test(value: any): boolean", () => { 11 | it("works", () => { 12 | assert(isPositiveNumber.test(-1.5) === false); 13 | assert(isPositiveNumber.test(-1) === false); 14 | assert(isPositiveNumber.test(0) === true); 15 | assert(isPositiveNumber.test(1) === true); 16 | assert(isPositiveNumber.test(1.5) === true); 17 | assert(isPositiveNumber.test(true) === false); 18 | assert(isPositiveNumber.test(false) === false); 19 | assert(isPositiveNumber.test("0") === false); 20 | assert(isPositiveNumber.test(it) === false); 21 | assert(isPositiveNumber.test(NaN) === false); 22 | assert(isPositiveNumber.test(null) === false); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/validators/isPositiveInteger.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import isPositiveInteger from "../../src/validators/isPositiveInteger"; 3 | 4 | describe("validators.isPositiveInteger", () => { 5 | describe(".typeName: string", () => { 6 | it("works", () => { 7 | assert(isPositiveInteger.typeName === "number"); 8 | }); 9 | }); 10 | describe(".test(value: any): boolean", () => { 11 | it("works", () => { 12 | assert(isPositiveInteger.test(-1.5) === false); 13 | assert(isPositiveInteger.test(-1) === false); 14 | assert(isPositiveInteger.test(0) === true); 15 | assert(isPositiveInteger.test(1) === true); 16 | assert(isPositiveInteger.test(1.5) === false); 17 | assert(isPositiveInteger.test(true) === false); 18 | assert(isPositiveInteger.test(false) === false); 19 | assert(isPositiveInteger.test("0") === false); 20 | assert(isPositiveInteger.test(it) === false); 21 | assert(isPositiveInteger.test(NaN) === false); 22 | assert(isPositiveInteger.test(null) === false); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/AudioProcessingEvent.js: -------------------------------------------------------------------------------- 1 | describe("AudioProcessingEvent", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var utils = WebAudioTestAPI.utils; 4 | var immigration = utils.Immigration.getInstance(); 5 | var audioContext; 6 | 7 | beforeEach(function() { 8 | audioContext = new WebAudioTestAPI.AudioContext(); 9 | }); 10 | 11 | describe("constructor()", function() { 12 | it("works", function() { 13 | var node = immigration.apply(function(admission) { 14 | return new WebAudioTestAPI.AudioNode(admission, { context: audioContext }); 15 | }); 16 | var event = immigration.apply(function(admission) { 17 | return new WebAudioTestAPI.AudioProcessingEvent(admission, node); 18 | }); 19 | 20 | assert(event instanceof global.AudioProcessingEvent); 21 | assert(event instanceof global.Event); 22 | 23 | // test api 24 | assert(event.$name === "AudioProcessingEvent"); 25 | assert(event.$node === node); 26 | }); 27 | it("not work when 'new' directly", function() { 28 | assert.throws(function() { new global.AudioProcessingEvent(); }, TypeError); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/utils/Configuration.js: -------------------------------------------------------------------------------- 1 | import api from "./api"; 2 | 3 | let instance = null; 4 | 5 | export default class Configuration { 6 | constructor() { 7 | this._states = {}; 8 | Object.keys(api).forEach((key) => { 9 | this._states[key] = api[key].states[0]; 10 | }); 11 | } 12 | 13 | static getInstance() { 14 | if (instance === null) { 15 | instance = new Configuration(); 16 | } 17 | return instance; 18 | } 19 | 20 | getState(name) { 21 | if (!this._states.hasOwnProperty(name)) { 22 | throw new TypeError(`invalid state name ${name}`); 23 | } 24 | return this._states[name]; 25 | } 26 | 27 | setState(name, value) { 28 | if (name && typeof name === "object") { 29 | let dict = name; 30 | 31 | Object.keys(dict).forEach((name) => { 32 | this.setState(name, dict[name]); 33 | }); 34 | return; 35 | } 36 | if (!this._states.hasOwnProperty(name)) { 37 | throw new TypeError(`invalid state name ${name}`); 38 | } 39 | if (api[name].states.indexOf(value) === -1) { 40 | throw new TypeError(`invalid state value ${value} on ${name}`); 41 | } 42 | this._states[name] = value; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/OfflineAudioCompletionEvent.js: -------------------------------------------------------------------------------- 1 | describe("OfflineAudioCompletionEvent", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var utils = WebAudioTestAPI.utils; 4 | var immigration = utils.Immigration.getInstance(); 5 | var audioContext; 6 | 7 | beforeEach(function() { 8 | audioContext = new WebAudioTestAPI.AudioContext(); 9 | }); 10 | 11 | describe("constructor()", function() { 12 | it("works", function() { 13 | var node = immigration.apply(function(admission) { 14 | return new WebAudioTestAPI.AudioNode(admission, { context: audioContext }); 15 | }); 16 | var event = immigration.apply(function(admission) { 17 | return new WebAudioTestAPI.OfflineAudioCompletionEvent(admission, node); 18 | }); 19 | 20 | assert(event instanceof global.OfflineAudioCompletionEvent); 21 | assert(event instanceof global.Event); 22 | 23 | // test api 24 | assert(event.$name === "OfflineAudioCompletionEvent"); 25 | assert(event.$node === node); 26 | }); 27 | it("not work when 'new' directly", function() { 28 | assert.throws(function() { new global.OfflineAudioCompletionEvent(); }, TypeError); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/BiquadFilterNode.js: -------------------------------------------------------------------------------- 1 | import AudioNode from "./AudioNode"; 2 | import * as props from "./decorators/props"; 3 | import * as methods from "./decorators/methods"; 4 | import * as validators from "./validators"; 5 | 6 | export default class BiquadFilterNode extends AudioNode { 7 | static $JSONKeys = [ "type", "frequency", "detune", "Q", "gain" ]; 8 | 9 | constructor(admission, context) { 10 | super(admission, { 11 | name: "BiquadFilterNode", 12 | context: context, 13 | numberOfInputs: 1, 14 | numberOfOutputs: 1, 15 | channelCount: 2, 16 | channelCountMode: "max", 17 | channelInterpretation: "speakers" 18 | }); 19 | } 20 | 21 | @props.enums([ "lowpass", "highpass", "bandpass", "lowshelf", "highshelf", "peaking", "notch", "allpass" ]) 22 | type() {} 23 | 24 | @props.audioparam(350) 25 | frequency() {} 26 | 27 | @props.audioparam(0) 28 | detune() {} 29 | 30 | @props.audioparam(1) 31 | Q() {} 32 | 33 | @props.audioparam(0) 34 | gain() {} 35 | 36 | @methods.param("frequencyHz", validators.isInstanceOf(Float32Array)) 37 | @methods.param("magResponse", validators.isInstanceOf(Float32Array)) 38 | @methods.param("phaseResponse", validators.isInstanceOf(Float32Array)) 39 | getFrequencyResponse() {} 40 | } 41 | -------------------------------------------------------------------------------- /src/WebAudioAPI.js: -------------------------------------------------------------------------------- 1 | export default { 2 | AnalyserNode: global.AnalyserNode, 3 | AudioBuffer: global.AudioBuffer, 4 | AudioBufferSourceNode: global.AudioBufferSourceNode, 5 | AudioContext: global.AudioContext || global.webkitAudioContext, 6 | AudioDestinationNode: global.AudioDestinationNode, 7 | AudioListener: global.AudioListener, 8 | AudioNode: global.AudioNode, 9 | AudioParam: global.AudioParam, 10 | AudioProcessingEvent: global.AudioProcessingEvent, 11 | BiquadFilterNode: global.BiquadFilterNode, 12 | ChannelMergerNode: global.ChannelMergerNode, 13 | ChannelSplitterNode: global.ChannelSplitterNode, 14 | ConvolverNode: global.ConvolverNode, 15 | DelayNode: global.DelayNode, 16 | DynamicsCompressorNode: global.DynamicsCompressorNode, 17 | GainNode: global.GainNode, 18 | MediaElementAudioSourceNode: global.MediaElementAudioSourceNode, 19 | MediaStreamAudioDestinationNode: global.MediaStreamAudioDestinationNode, 20 | MediaStreamAudioSourceNode: global.MediaStreamAudioSourceNode, 21 | OfflineAudioCompletionEvent: global.OfflineAudioCompletionEvent, 22 | OfflineAudioContext: global.OfflineAudioContext || global.webkitOfflineAudioContext, 23 | OscillatorNode: global.OscillatorNode, 24 | PannerNode: global.PannerNode, 25 | PeriodicWave: global.PeriodicWave, 26 | ScriptProcessorNode: global.ScriptProcessorNode, 27 | StereoPannerNode: global.StereoPannerNode, 28 | WaveShaperNode: global.WaveShaperNode 29 | }; 30 | -------------------------------------------------------------------------------- /src/AudioListener.js: -------------------------------------------------------------------------------- 1 | import Immigration from "./utils/Immigration"; 2 | import * as props from "./decorators/props"; 3 | import * as methods from "./decorators/methods"; 4 | import * as validators from "./validators"; 5 | 6 | let immigration = Immigration.getInstance(); 7 | 8 | export default class AudioListener { 9 | constructor(admission, context) { 10 | immigration.check(admission, () => { 11 | throw new TypeError("Illegal constructor"); 12 | }); 13 | Object.defineProperty(this, "_", { value: {} }); 14 | 15 | this._.context = context; 16 | } 17 | 18 | @props.typed(validators.isNumber, 1) 19 | dopplerFactor() {} 20 | 21 | @props.typed(validators.isNumber, 343.3) 22 | speedOfSound() {} 23 | 24 | @methods.param("x", validators.isNumber) 25 | @methods.param("y", validators.isNumber) 26 | @methods.param("z", validators.isNumber) 27 | setPosition() {} 28 | 29 | @methods.param("x", validators.isNumber) 30 | @methods.param("y", validators.isNumber) 31 | @methods.param("z", validators.isNumber) 32 | @methods.param("xUp", validators.isNumber) 33 | @methods.param("yUp", validators.isNumber) 34 | @methods.param("zUp", validators.isNumber) 35 | setOrientation() {} 36 | 37 | @methods.param("x", validators.isNumber) 38 | @methods.param("y", validators.isNumber) 39 | @methods.param("z", validators.isNumber) 40 | setVelocity() {} 41 | 42 | get $name() { 43 | return "AudioListener"; 44 | } 45 | 46 | get $context() { 47 | return this._.context; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/MediaStreamAudioDestinationNode.js: -------------------------------------------------------------------------------- 1 | describe("MediaStreamAudioDestinationNode", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var audioContext; 4 | 5 | beforeEach(function() { 6 | audioContext = new WebAudioTestAPI.AudioContext(); 7 | }); 8 | 9 | describe("constructor()", function() { 10 | it("works", function() { 11 | var node = audioContext.createMediaStreamDestination(); 12 | 13 | assert(node instanceof global.MediaStreamAudioDestinationNode); 14 | assert(node instanceof global.AudioNode); 15 | }); 16 | it("not work when 'new' directly", function() { 17 | assert.throws(function() { new global.MediaStreamAudioDestinationNode(); }, TypeError); 18 | }); 19 | }); 20 | 21 | describe("#toJSON(): object", function() { 22 | it("works", function() { 23 | var node = audioContext.createMediaStreamDestination(); 24 | 25 | assert.deepEqual(node.toJSON(), { 26 | name: "MediaStreamAudioDestinationNode", 27 | inputs: [] 28 | }); 29 | }); 30 | }); 31 | 32 | describe("$name: string", function() { 33 | it("works", function() { 34 | var node = audioContext.createMediaStreamDestination(); 35 | 36 | assert(node.$name === "MediaStreamAudioDestinationNode"); 37 | }); 38 | }); 39 | 40 | describe("$context: AudioContext", function() { 41 | it("works", function() { 42 | var node = audioContext.createMediaStreamDestination(); 43 | 44 | assert(node.$context === audioContext); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/ChannelMergerNode.js: -------------------------------------------------------------------------------- 1 | describe("ChannelMergerNode", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var audioContext; 4 | 5 | beforeEach(function() { 6 | audioContext = new WebAudioTestAPI.AudioContext(); 7 | }); 8 | 9 | describe("constructor()", function() { 10 | it("works", function() { 11 | var node = audioContext.createChannelMerger(); 12 | 13 | assert(node instanceof global.ChannelMergerNode); 14 | assert(node instanceof global.AudioNode); 15 | 16 | assert.throws(function() { 17 | audioContext.createChannelMerger(5.1); 18 | }, TypeError); 19 | }); 20 | it("not work when 'new' directly", function() { 21 | assert.throws(function() { new global.ChannelMergerNode(); }, TypeError); 22 | }); 23 | }); 24 | 25 | describe("#toJSON(): object", function() { 26 | it("works", function() { 27 | var node = audioContext.createChannelMerger(); 28 | 29 | assert.deepEqual(node.toJSON(), { 30 | name: "ChannelMergerNode", 31 | inputs: [ [], [], [], [], [], [] ] 32 | }); 33 | }); 34 | }); 35 | 36 | describe("$name: string", function() { 37 | it("works", function() { 38 | var node = audioContext.createChannelMerger(); 39 | 40 | assert(node.$name === "ChannelMergerNode"); 41 | }); 42 | }); 43 | 44 | describe("$context: AudioContext", function() { 45 | it("works", function() { 46 | var node = audioContext.createChannelMerger(); 47 | 48 | assert(node.$context === audioContext); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/ChannelSplitterNode.js: -------------------------------------------------------------------------------- 1 | describe("ChannelSplitterNode", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var audioContext; 4 | 5 | beforeEach(function() { 6 | audioContext = new WebAudioTestAPI.AudioContext(); 7 | }); 8 | 9 | describe("constructor()", function() { 10 | it("works", function() { 11 | var node = audioContext.createChannelSplitter(); 12 | 13 | assert(node instanceof global.ChannelSplitterNode); 14 | assert(node instanceof global.AudioNode); 15 | 16 | assert.throws(function() { 17 | audioContext.createChannelSplitter(5.1); 18 | }, TypeError); 19 | }); 20 | it("not work when 'new' directly", function() { 21 | assert.throws(function() { new global.ChannelSplitterNode(); }, TypeError); 22 | }); 23 | }); 24 | 25 | describe("#toJSON(): object", function() { 26 | it("works", function() { 27 | var node = audioContext.createChannelSplitter(); 28 | 29 | assert.deepEqual(node.toJSON(), { 30 | name: "ChannelSplitterNode", 31 | inputs: [] 32 | }); 33 | }); 34 | }); 35 | 36 | describe("$name: string", function() { 37 | it("works", function() { 38 | var node = audioContext.createChannelSplitter(); 39 | 40 | assert(node.$name === "ChannelSplitterNode"); 41 | }); 42 | }); 43 | 44 | describe("$context: AudioContext", function() { 45 | it("works", function() { 46 | var node = audioContext.createChannelSplitter(); 47 | 48 | assert(node.$context === audioContext); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/utils/Junction.js: -------------------------------------------------------------------------------- 1 | import appendIfNotExists from "./appendIfNotExists"; 2 | import removeIfExists from "./removeIfExists"; 3 | 4 | export default class Junction { 5 | constructor(node, index) { 6 | this.node = node; 7 | this.index = index; 8 | this.inputs = []; 9 | this.outputs = []; 10 | } 11 | 12 | connect(destination) { 13 | appendIfNotExists(this.outputs, destination); 14 | appendIfNotExists(destination.inputs, this); 15 | } 16 | 17 | disconnectAll() { 18 | this.outputs.splice(0).forEach((destination) => { 19 | removeIfExists(destination.inputs, this); 20 | }); 21 | } 22 | 23 | disconnectNode(node) { 24 | for (let i = this.outputs.length - 1; i >= 0; i--) { 25 | let destination = this.outputs[i]; 26 | 27 | if (destination.node === node) { 28 | this.outputs.splice(i, 1); 29 | removeIfExists(destination.inputs, this); 30 | } 31 | } 32 | } 33 | 34 | disconnectChannel(node, input) { 35 | for (let i = this.outputs.length - 1; i >= 0; i--) { 36 | let destination = this.outputs[i]; 37 | 38 | if (destination.node === node && destination.index === input) { 39 | this.outputs.splice(i, 1); 40 | removeIfExists(destination.inputs, this); 41 | } 42 | } 43 | } 44 | 45 | isConnected(destination) { 46 | return this.outputs.some(junction => junction.node === destination); 47 | } 48 | 49 | process(inNumSamples, tick) { 50 | this.inputs.forEach((junction) => { 51 | junction.node.$process(inNumSamples, tick); 52 | }); 53 | } 54 | 55 | toJSON(memo) { 56 | return this.inputs.map(junction => junction.node.toJSON(memo)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/PeriodicWave.js: -------------------------------------------------------------------------------- 1 | import Immigration from "./utils/Immigration"; 2 | import * as methods from "./decorators/methods"; 3 | import * as validators from "./validators"; 4 | 5 | let immigration = Immigration.getInstance(); 6 | 7 | export default class PeriodicWave { 8 | constructor(admission, context, real, imag) { 9 | immigration.check(admission, () => { 10 | throw new TypeError("Illegal constructor"); 11 | }); 12 | Object.defineProperty(this, "_", { value: {} }); 13 | 14 | this._.context = context; 15 | this.__createPeriodicWave(real, imag); 16 | } 17 | 18 | @methods.param("real", validators.isInstanceOf(Float32Array)) 19 | @methods.param("imag", validators.isInstanceOf(Float32Array)) 20 | @methods.contract({ 21 | precondition(real, imag) { 22 | if (4096 < real.length) { 23 | throw new TypeError(`The length of "{{real}}" array (${real.length}) exceeds allow maximum of 4096.`); 24 | } 25 | if (4096 < imag.length) { 26 | throw new TypeError(`The length of "{{imag}}" array (${imag.length}) exceeds allow maximum of 4096.`); 27 | } 28 | if (real.length !== imag.length) { 29 | throw new TypeError(`The length of "{{real}}" array (${real.length}) and length of "imag" array (${imag.length}) must match.`); 30 | } 31 | } 32 | }) 33 | __createPeriodicWave(real, imag) { 34 | this._.real = real; 35 | this._.imag = imag; 36 | } 37 | 38 | get $name() { 39 | return "PeriodicWave"; 40 | } 41 | 42 | get $context() { 43 | return this._.context; 44 | } 45 | 46 | get $real() { 47 | return this._.real; 48 | } 49 | 50 | get $imag() { 51 | return this._.imag; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/MediaStreamAudioSourceNode.js: -------------------------------------------------------------------------------- 1 | describe("MediaStreamAudioSourceNode", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var audioContext, mediaStream; 4 | 5 | beforeEach(function() { 6 | audioContext = new WebAudioTestAPI.AudioContext(); 7 | mediaStream = new WebAudioTestAPI.MediaStream(); 8 | }); 9 | 10 | describe("constructor()", function() { 11 | it("works", function() { 12 | var node = audioContext.createMediaStreamSource(mediaStream); 13 | 14 | assert(node instanceof global.MediaStreamAudioSourceNode); 15 | assert(node instanceof global.AudioNode); 16 | 17 | assert.throws(function() { 18 | audioContext.createMediaStreamSource("INVALID"); 19 | }, TypeError); 20 | }); 21 | it("not work when 'new' directly", function() { 22 | assert.throws(function() { new global.MediaStreamAudioSourceNode(); }, TypeError); 23 | }); 24 | }); 25 | 26 | describe("#toJSON(): object", function() { 27 | it("works", function() { 28 | var node = audioContext.createMediaStreamSource(mediaStream); 29 | 30 | assert.deepEqual(node.toJSON(), { 31 | name: "MediaStreamAudioSourceNode", 32 | inputs: [] 33 | }); 34 | }); 35 | }); 36 | 37 | describe("$name: string", function() { 38 | it("works", function() { 39 | var node = audioContext.createMediaStreamSource(mediaStream); 40 | 41 | assert(node.$name === "MediaStreamAudioSourceNode"); 42 | }); 43 | }); 44 | 45 | describe("$context: AudioContext", function() { 46 | it("works", function() { 47 | var node = audioContext.createMediaStreamSource(mediaStream); 48 | 49 | assert(node.$context === audioContext); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/AudioDestinationNode.js: -------------------------------------------------------------------------------- 1 | describe("AudioDestinationNode", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var audioContext; 4 | 5 | beforeEach(function() { 6 | audioContext = new WebAudioTestAPI.AudioContext(); 7 | }); 8 | 9 | describe("constructor()", function() { 10 | it("works", function() { 11 | var node = audioContext.destination; 12 | 13 | assert(node instanceof global.AudioDestinationNode); 14 | assert(node instanceof global.AudioNode); 15 | }); 16 | it("not work when 'new' directly", function() { 17 | assert.throws(function() { new global.AudioDestinationNode(); }, TypeError); 18 | }); 19 | }); 20 | 21 | describe("#maxChannelCount: number", function() { 22 | it("works", function() { 23 | var node = audioContext.destination; 24 | 25 | assert(typeof node.maxChannelCount === "number"); 26 | 27 | assert.throws(function() { 28 | node.maxChannelCount = 256; 29 | }, TypeError); 30 | }); 31 | }); 32 | 33 | describe("#toJSON(): object", function() { 34 | it("works", function() { 35 | var node = audioContext.destination; 36 | 37 | assert.deepEqual(node.toJSON(), { 38 | name: "AudioDestinationNode", 39 | inputs: [] 40 | }); 41 | }); 42 | }); 43 | 44 | describe("$name: string", function() { 45 | it("works", function() { 46 | var node = audioContext.destination; 47 | 48 | assert(node.$name === "AudioDestinationNode"); 49 | }); 50 | }); 51 | 52 | describe("$context: AudioContext", function() { 53 | it("works", function() { 54 | var node = audioContext.destination; 55 | 56 | assert(node.$context === audioContext); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/MediaElementAudioSourceNode.js: -------------------------------------------------------------------------------- 1 | describe("MediaElementAudioSourceNode", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var audioContext, mediaElement; 4 | 5 | beforeEach(function() { 6 | audioContext = new WebAudioTestAPI.AudioContext(); 7 | mediaElement = new WebAudioTestAPI.HTMLMediaElement(); 8 | }); 9 | 10 | describe("constructor()", function() { 11 | it("works", function() { 12 | var node = audioContext.createMediaElementSource(mediaElement); 13 | 14 | assert(node instanceof global.MediaElementAudioSourceNode); 15 | assert(node instanceof global.AudioNode); 16 | 17 | assert.throws(function() { 18 | audioContext.createMediaElementSource("INVALID"); 19 | }, TypeError); 20 | }); 21 | it("not work when 'new' directly", function() { 22 | assert.throws(function() { new global.MediaElementAudioSourceNode(); }, TypeError); 23 | }); 24 | }); 25 | 26 | describe("#toJSON(): object", function() { 27 | it("works", function() { 28 | var node = audioContext.createMediaElementSource(mediaElement); 29 | 30 | assert.deepEqual(node.toJSON(), { 31 | name: "MediaElementAudioSourceNode", 32 | inputs: [] 33 | }); 34 | }); 35 | }); 36 | 37 | describe("$name: string", function() { 38 | it("works", function() { 39 | var node = audioContext.createMediaElementSource(mediaElement); 40 | 41 | assert(node.$name === "MediaElementAudioSourceNode"); 42 | }); 43 | }); 44 | 45 | describe("$context: AudioContext", function() { 46 | it("works", function() { 47 | var node = audioContext.createMediaElementSource(mediaElement); 48 | 49 | assert(node.$context === audioContext); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/GainNode.js: -------------------------------------------------------------------------------- 1 | describe("GainNode", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var audioContext; 4 | 5 | beforeEach(function() { 6 | audioContext = new WebAudioTestAPI.AudioContext(); 7 | }); 8 | 9 | describe("constructor()", function() { 10 | it("works", function() { 11 | var node = audioContext.createGain(); 12 | 13 | assert(node instanceof global.GainNode); 14 | assert(node instanceof global.AudioNode); 15 | }); 16 | it("not work when 'new' directly", function() { 17 | assert.throws(function() { new global.GainNode(); }, TypeError); 18 | }); 19 | }); 20 | 21 | describe("#gain: AudioParam", function() { 22 | it("works", function() { 23 | var node = audioContext.createGain(); 24 | 25 | assert(node.gain instanceof WebAudioTestAPI.AudioParam); 26 | 27 | assert.throws(function() { 28 | node.gain = 0; 29 | }, TypeError); 30 | }); 31 | }); 32 | 33 | describe("#toJSON(): object", function() { 34 | it("works", function() { 35 | var node = audioContext.createGain(); 36 | 37 | assert.deepEqual(node.toJSON(), { 38 | name: "GainNode", 39 | gain: { 40 | value: 1, 41 | inputs: [] 42 | }, 43 | inputs: [] 44 | }); 45 | }); 46 | }); 47 | 48 | describe("$name: string", function() { 49 | it("works", function() { 50 | var node = audioContext.createGain(); 51 | 52 | assert(node.$name === "GainNode"); 53 | }); 54 | }); 55 | 56 | describe("$context: AudioContext", function() { 57 | it("works", function() { 58 | var node = audioContext.createDynamicsCompressor(); 59 | 60 | assert(node.$context === audioContext); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/AnalyserNode.js: -------------------------------------------------------------------------------- 1 | import Configuration from "./utils/Configuration"; 2 | import AudioNode from "./AudioNode"; 3 | import * as props from "./decorators/props"; 4 | import * as methods from "./decorators/methods"; 5 | import * as validators from "./validators"; 6 | 7 | let configuration = Configuration.getInstance(); 8 | 9 | export default class AnalyserNode extends AudioNode { 10 | static $JSONKeys = [ "fftSize", "minDecibels", "maxDecibels", "smoothingTimeConstant" ]; 11 | 12 | constructor(admission, context) { 13 | super(admission, { 14 | name: "AnalyserNode", 15 | context: context, 16 | numberOfInputs: 1, 17 | numberOfOutputs: 1, 18 | channelCount: 1, 19 | channelCountMode: "explicit", 20 | channelInterpretation: "speakers" 21 | }); 22 | this._.fftSize = 2048; 23 | } 24 | 25 | @props.enums([ 32, 64, 128, 256, 512, 1024, 2048 ]) 26 | fftSize() {} 27 | 28 | @props.readonly() 29 | frequencyBinCount() { 30 | return this.fftSize >> 1; 31 | } 32 | 33 | @props.typed(validators.isNumber, -100) 34 | minDecibels() {} 35 | 36 | @props.typed(validators.isNumber, 30) 37 | maxDecibels() {} 38 | 39 | @props.typed(validators.isNumber, 0.8) 40 | smoothingTimeConstant() {} 41 | 42 | @methods.param("array", validators.isInstanceOf(Float32Array)) 43 | getFloatFrequencyData() {} 44 | 45 | @methods.param("array", validators.isInstanceOf(Uint8Array)) 46 | getByteFrequencyData() {} 47 | 48 | @methods.param("array", validators.isInstanceOf(Float32Array)) 49 | @methods.contract({ 50 | precondition() { 51 | if (configuration.getState("AnalyserNode#getFloatTimeDomainData") !== "enabled") { 52 | throw new TypeError("not enabled"); 53 | } 54 | } 55 | }) 56 | getFloatTimeDomainData() {} 57 | 58 | @methods.param("array", validators.isInstanceOf(Uint8Array)) 59 | getByteTimeDomainData() {} 60 | } 61 | -------------------------------------------------------------------------------- /src/PannerNode.js: -------------------------------------------------------------------------------- 1 | import AudioNode from "./AudioNode"; 2 | import * as props from "./decorators/props"; 3 | import * as methods from "./decorators/methods"; 4 | import * as validators from "./validators"; 5 | 6 | export default class PannerNode extends AudioNode { 7 | static $JSONKeys = [ 8 | "panningModel", "distanceModel", "refDistance", "maxDistance", 9 | "rolloffFactor", "coneInnerAngle", "coneOuterAngle", "coneOuterGain" 10 | ]; 11 | 12 | constructor(admission, context) { 13 | super(admission, { 14 | name: "PannerNode", 15 | context: context, 16 | numberOfInputs: 1, 17 | numberOfOutputs: 1, 18 | channelCount: 2, 19 | channelCountMode: "clamped-max", 20 | channelInterpretation: "speakers" 21 | }); 22 | } 23 | 24 | @props.enums([ "HRTF", "equalpower" ]) 25 | panningModel() {} 26 | 27 | @props.enums([ "inverse", "linear", "exponential" ]) 28 | distanceModel() {} 29 | 30 | @props.typed(validators.isNumber, 1) 31 | refDistance() {} 32 | 33 | @props.typed(validators.isNumber, 10000) 34 | maxDistance() {} 35 | 36 | @props.typed(validators.isNumber, 1) 37 | rolloffFactor() {} 38 | 39 | @props.typed(validators.isNumber, 360) 40 | coneInnerAngle() {} 41 | 42 | @props.typed(validators.isNumber, 360) 43 | coneOuterAngle() {} 44 | 45 | @props.typed(validators.isNumber, 0) 46 | coneOuterGain() {} 47 | 48 | @methods.param("x", validators.isNumber) 49 | @methods.param("y", validators.isNumber) 50 | @methods.param("z", validators.isNumber) 51 | setPosition() {} 52 | 53 | @methods.param("x", validators.isNumber) 54 | @methods.param("y", validators.isNumber) 55 | @methods.param("z", validators.isNumber) 56 | setOrientation() {} 57 | 58 | @methods.param("x", validators.isNumber) 59 | @methods.param("y", validators.isNumber) 60 | @methods.param("z", validators.isNumber) 61 | setVelocity() {} 62 | } 63 | -------------------------------------------------------------------------------- /test/WebAudioTestAPI.js: -------------------------------------------------------------------------------- 1 | describe("WebAudioTestAPI", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | 4 | after(function() { 5 | WebAudioTestAPI.use(); 6 | }); 7 | 8 | describe("VERSION: string", function() { 9 | it("works", function() { 10 | assert(typeof WebAudioTestAPI.VERSION === "string"); 11 | if (typeof global.WEB_AUDIO_TEST_API_VERSION === "string") { 12 | assert(WebAudioTestAPI.VERSION === global.WEB_AUDIO_TEST_API_VERSION); 13 | } 14 | }); 15 | }); 16 | 17 | describe("sampleRate: number", function() { 18 | it("works", function() { 19 | assert(typeof WebAudioTestAPI.sampleRate === "number"); 20 | }); 21 | }); 22 | 23 | describe("getState(name: string): string", function() { 24 | it("works", function() { 25 | assert(WebAudioTestAPI.getState("AnalyserNode#getFloatTimeDomainData") === "disabled"); 26 | }); 27 | }); 28 | 29 | describe("setState(name: string, value: string): string", function() { 30 | it("works", function() { 31 | WebAudioTestAPI.setState({ 32 | "AnalyserNode#getFloatTimeDomainData": "enabled" 33 | }); 34 | 35 | assert(WebAudioTestAPI.getState("AnalyserNode#getFloatTimeDomainData") === "enabled"); 36 | 37 | WebAudioTestAPI.setState({ 38 | "AnalyserNode#getFloatTimeDomainData": "disabled" 39 | }); 40 | }); 41 | }); 42 | 43 | describe("unuse(): void", function() { 44 | it("works", function() { 45 | WebAudioTestAPI.unuse(); 46 | 47 | assert(global.AudioContext !== WebAudioTestAPI.AudioContext); 48 | assert(global.OfflineAudioContext !== WebAudioTestAPI.OfflineAudioContext); 49 | }); 50 | }); 51 | 52 | describe("use(): void", function() { 53 | it("works", function() { 54 | WebAudioTestAPI.use(); 55 | 56 | assert(global.AudioContext === WebAudioTestAPI.AudioContext); 57 | assert(global.OfflineAudioContext === WebAudioTestAPI.OfflineAudioContext); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-audio-test-api", 3 | "description": "Web Audio API test library for CI", 4 | "version": "0.5.2", 5 | "author": "Nao Yonamine ", 6 | "bugs": { 7 | "url": "https://github.com/mohayonao/web-audio-test-api/issues" 8 | }, 9 | "dependencies": {}, 10 | "devDependencies": { 11 | "babel-cli": "^6.3.15", 12 | "babel-eslint": "^4.1.6", 13 | "babel-plugin-transform-decorators-legacy": "^1.2.0", 14 | "babel-plugin-transform-es2015-modules-commonjs": "^6.3.16", 15 | "babel-preset-es2015": "^6.3.13", 16 | "babel-preset-stage-0": "^6.3.13", 17 | "babel-register": "^6.3.13", 18 | "babelify": "^7.2.0", 19 | "browserify": "^12.0.1", 20 | "eslint": "^1.10.3", 21 | "eslint-config-mohayonao": "^0.1.0", 22 | "espower-babel": "^4.0.0", 23 | "isparta": "^4.0.0", 24 | "lodash": "^3.10.1", 25 | "mocha": "^2.3.4", 26 | "power-assert": "^1.2.0", 27 | "sinon": "^1.17.2" 28 | }, 29 | "files": [ 30 | "package.json", 31 | "README.md", 32 | "index.js", 33 | "lib", 34 | "build" 35 | ], 36 | "homepage": "https://github.com/mohayonao/web-audio-test-api/", 37 | "keywords": [ 38 | "test", 39 | "web audio api" 40 | ], 41 | "license": "MIT", 42 | "main": "index.js", 43 | "repository": { 44 | "type": "git", 45 | "url": "http://github.com/mohayonao/web-audio-test-api.git" 46 | }, 47 | "scripts": { 48 | "build": "npm run build:to5 && npm run build:browser", 49 | "build:browser": "browserify index.js --standalone WebAudioTestAPI -o build/web-audio-test-api.js", 50 | "build:to5": "babel src --out-dir lib", 51 | "cover": "babel-node $(npm bin)/isparta cover --report text --report html --report lcov _mocha", 52 | "lint": "eslint src test", 53 | "prepublish": "npm run lint && npm run test && npm run build", 54 | "test": "mocha --compilers js:babel-register", 55 | "test:pow": "mocha --compilers js:espower-babel/guess", 56 | "travis": "npm run lint && npm run test" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/StereoPannerNode.js: -------------------------------------------------------------------------------- 1 | describe("StereoPannerNode", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var audioContext; 4 | 5 | before(function() { 6 | WebAudioTestAPI.setState("AudioContext#createStereoPanner", "enabled"); 7 | }); 8 | 9 | beforeEach(function() { 10 | audioContext = new WebAudioTestAPI.AudioContext(); 11 | }); 12 | 13 | after(function() { 14 | WebAudioTestAPI.setState("AudioContext#createStereoPanner", "disabled"); 15 | }); 16 | 17 | describe("constructor()", function() { 18 | it("works", function() { 19 | var node = audioContext.createStereoPanner(); 20 | 21 | assert(node instanceof global.StereoPannerNode); 22 | assert(node instanceof global.AudioNode); 23 | }); 24 | it("not work when 'new' directly", function() { 25 | assert.throws(function() { new global.StereoPannerNode(); }, TypeError); 26 | }); 27 | }); 28 | 29 | describe("#pan: AudioParam", function() { 30 | it("works", function() { 31 | var node = audioContext.createStereoPanner(); 32 | 33 | assert(node.pan instanceof WebAudioTestAPI.AudioParam); 34 | 35 | assert.throws(function() { 36 | node.pan = 0; 37 | }, TypeError); 38 | }); 39 | }); 40 | 41 | describe("#toJSON(): object", function() { 42 | it("works", function() { 43 | var node = audioContext.createStereoPanner(); 44 | 45 | assert.deepEqual(node.toJSON(), { 46 | name: "StereoPannerNode", 47 | pan: { 48 | value: 0, 49 | inputs: [] 50 | }, 51 | inputs: [] 52 | }); 53 | }); 54 | }); 55 | 56 | describe("$name: string", function() { 57 | it("works", function() { 58 | var node = audioContext.createStereoPanner(); 59 | 60 | assert(node.$name === "StereoPannerNode"); 61 | }); 62 | }); 63 | 64 | describe("$context: AudioContext", function() { 65 | it("works", function() { 66 | var node = audioContext.createStereoPanner(); 67 | 68 | assert(node.$context === audioContext); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/PeriodicWave.js: -------------------------------------------------------------------------------- 1 | describe("PeriodicWave", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var audioContext, real, imag; 4 | 5 | beforeEach(function() { 6 | audioContext = new WebAudioTestAPI.AudioContext(); 7 | real = new Float32Array(1024); 8 | imag = new Float32Array(1024); 9 | }); 10 | 11 | describe("constructor()", function() { 12 | it("works", function() { 13 | var wave = audioContext.createPeriodicWave(real, imag); 14 | var f128 = new Float32Array(128); 15 | var f256 = new Float32Array(256); 16 | var f8192 = new Float32Array(8192); 17 | 18 | assert(wave instanceof global.PeriodicWave); 19 | 20 | assert.throws(function() { 21 | audioContext.createPeriodicWave("INVALID", imag); 22 | }, TypeError); 23 | 24 | assert.throws(function() { 25 | audioContext.createPeriodicWave(real, "INVALID"); 26 | }, TypeError); 27 | 28 | assert.throws(function() { 29 | audioContext.createPeriodicWave(f128, f256); 30 | }, TypeError); 31 | 32 | assert.throws(function() { 33 | audioContext.createPeriodicWave(f8192, f128); 34 | }, TypeError); 35 | 36 | assert.throws(function() { 37 | audioContext.createPeriodicWave(f128, f8192); 38 | }, TypeError); 39 | }); 40 | it("not work when 'new' directly", function() { 41 | assert.throws(function() { new global.PeriodicWave(); }, TypeError); 42 | }); 43 | }); 44 | 45 | describe("$name: string", function() { 46 | it("works", function() { 47 | var wave = audioContext.createPeriodicWave(real, imag); 48 | 49 | assert(wave.$name === "PeriodicWave"); 50 | }); 51 | }); 52 | 53 | describe("$context: AudioContext", function() { 54 | it("works", function() { 55 | var wave = audioContext.createPeriodicWave(real, imag); 56 | 57 | assert(wave.$context === audioContext); 58 | }); 59 | }); 60 | 61 | describe("$real: Float32Array", function() { 62 | it("works", function() { 63 | var wave = audioContext.createPeriodicWave(real, imag); 64 | 65 | assert(wave.$real === real); 66 | }); 67 | }); 68 | 69 | describe("$imag: Float32Array", function() { 70 | it("works", function() { 71 | var wave = audioContext.createPeriodicWave(real, imag); 72 | 73 | assert(wave.$imag === imag); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/DelayNode.js: -------------------------------------------------------------------------------- 1 | describe("DelayNode", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var audioContext; 4 | 5 | beforeEach(function() { 6 | audioContext = new WebAudioTestAPI.AudioContext(); 7 | }); 8 | 9 | describe("constructor()", function() { 10 | it("works", function() { 11 | var node = audioContext.createDelay(); 12 | 13 | assert(node instanceof global.DelayNode); 14 | assert(node instanceof global.AudioNode); 15 | 16 | assert.throws(function() { 17 | audioContext.createDelay("INVALID"); 18 | }, TypeError); 19 | 20 | assert.doesNotThrow(function() { 21 | audioContext.createDelay(); 22 | }); 23 | 24 | assert.throws(function() { 25 | audioContext.createDelay(undefined); 26 | }, TypeError); 27 | }); 28 | it("not work when 'new' directly", function() { 29 | assert.throws(function() { new global.DelayNode(); }, TypeError); 30 | }); 31 | }); 32 | 33 | describe("#delayTime: AudioParam", function() { 34 | it("works", function() { 35 | var node = audioContext.createDelay(); 36 | 37 | assert(node.delayTime instanceof WebAudioTestAPI.AudioParam); 38 | 39 | assert.throws(function() { 40 | node.delayTime = 0; 41 | }, TypeError); 42 | }); 43 | }); 44 | 45 | describe("#toJSON(): object", function() { 46 | it("works", function() { 47 | var node = audioContext.createDelay(); 48 | 49 | assert.deepEqual(node.toJSON(), { 50 | name: "DelayNode", 51 | delayTime: { 52 | value: 0, 53 | inputs: [] 54 | }, 55 | inputs: [] 56 | }); 57 | }); 58 | }); 59 | 60 | describe("$name: string", function() { 61 | it("works", function() { 62 | var node = audioContext.createDelay(); 63 | 64 | assert(node.$name === "DelayNode"); 65 | }); 66 | }); 67 | 68 | describe("$context: AudioContext", function() { 69 | it("works", function() { 70 | var node = audioContext.createDelay(); 71 | 72 | assert(node.$context === audioContext); 73 | }); 74 | }); 75 | 76 | describe("$maxDelayTime: number", function() { 77 | it("works", function() { 78 | var node = audioContext.createDelay(10); 79 | 80 | assert(typeof node.$maxDelayTime === "number"); 81 | assert(node.$maxDelayTime === 10); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/dom/EventTarget.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | import Event from "./Event"; 3 | import inLaws from "../utils/inLaws"; 4 | import * as methods from "../decorators/methods"; 5 | import * as validators from "../validators"; 6 | 7 | const EMITTER = Symbol("emitter"); 8 | 9 | global.EventTarget = global.EventTarget || class EventTarget { 10 | constructor() { 11 | throw new TypeError("Illegal constructor"); 12 | } 13 | }; 14 | 15 | export default class EventTarget extends inLaws(global.EventTarget) { 16 | constructor() { 17 | super(); 18 | 19 | this[EMITTER] = new EventEmitter(); 20 | } 21 | 22 | @methods.param("type", validators.isString) 23 | @methods.param("listener", validators.isFunction) 24 | addEventListener(type, listener) { 25 | this[EMITTER].addListener(type, listener); 26 | } 27 | 28 | @methods.param("type", validators.isString) 29 | @methods.param("listener", validators.isFunction) 30 | removeEventListener(type, listener) { 31 | this[EMITTER].removeListener(type, listener); 32 | } 33 | 34 | @methods.param("event", validators.isInstanceOf(Event)) 35 | dispatchEvent(event) { 36 | const type = event.type; 37 | const callback = this["on" + type]; 38 | 39 | if (typeof callback === "function") { 40 | this::callback(event); 41 | } 42 | 43 | this[EMITTER].listeners(type).forEach((listener) => { 44 | this::listener(event); 45 | }); 46 | 47 | return true; 48 | } 49 | 50 | $addListener(event, listener) { 51 | this[EMITTER].addListener(event, listener); 52 | return this; 53 | } 54 | 55 | $emit(...args) { 56 | this[EMITTER].emit(...args); 57 | return this; 58 | } 59 | 60 | $getMaxListeners() { 61 | return this[EMITTER].getMaxListeners(); 62 | } 63 | 64 | $listenerCount(type) { 65 | return this[EMITTER].listenerCount(type); 66 | } 67 | 68 | $listeners(event) { 69 | return this[EMITTER].listeners(event); 70 | } 71 | 72 | $on(event, listener) { 73 | this[EMITTER].on(event, listener); 74 | return this; 75 | } 76 | 77 | $once(event, listener) { 78 | this[EMITTER].on(event, listener); 79 | return this; 80 | } 81 | 82 | $removeAllListeners(event) { 83 | this[EMITTER].removeAllListeners(event); 84 | return this; 85 | } 86 | 87 | $removeListener(event, listener) { 88 | this[EMITTER].removeAllListeners(event, listener); 89 | return this; 90 | } 91 | 92 | $setMaxListeners(event, listener) { 93 | this[EMITTER].setMaxListeners(event, listener); 94 | return this; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test/ConvolverNode.js: -------------------------------------------------------------------------------- 1 | describe("ConvolverNode", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var audioContext; 4 | 5 | beforeEach(function() { 6 | audioContext = new WebAudioTestAPI.AudioContext(); 7 | }); 8 | 9 | describe("constructor()", function() { 10 | it("works", function() { 11 | var node = audioContext.createConvolver(); 12 | 13 | assert(node instanceof global.ConvolverNode); 14 | assert(node instanceof global.AudioNode); 15 | }); 16 | it("not work when 'new' directly", function() { 17 | assert.throws(function() { new global.ConvolverNode(); }, TypeError); 18 | }); 19 | }); 20 | 21 | describe("#buffer: AudioBuffer", function() { 22 | it("works", function() { 23 | var node = audioContext.createConvolver(); 24 | var buf1 = audioContext.createBuffer(1, 16, 44100); 25 | var buf2 = audioContext.createBuffer(2, 32, 44100); 26 | 27 | assert(node.buffer === null); 28 | 29 | node.buffer = buf1; 30 | assert(node.buffer === buf1); 31 | 32 | node.buffer = buf2; 33 | assert(node.buffer === buf2); 34 | 35 | node.buffer = null; 36 | assert(node.buffer === null); 37 | 38 | assert.throws(function() { 39 | node.buffer = "INVALID"; 40 | }, TypeError); 41 | }); 42 | }); 43 | 44 | describe("#normalize: boolean", function() { 45 | it("works", function() { 46 | var node = audioContext.createConvolver(); 47 | 48 | assert(typeof node.normalize === "boolean"); 49 | 50 | node.normalize = true; 51 | assert(node.normalize === true); 52 | 53 | node.normalize = false; 54 | assert(node.normalize === false); 55 | 56 | assert.throws(function() { 57 | node.normalize = "INVALID"; 58 | }, TypeError); 59 | }); 60 | }); 61 | 62 | describe("#toJSON(): object", function() { 63 | it("works", function() { 64 | var node = audioContext.createConvolver(); 65 | 66 | assert.deepEqual(node.toJSON(), { 67 | name: "ConvolverNode", 68 | normalize: true, 69 | inputs: [] 70 | }); 71 | }); 72 | }); 73 | 74 | describe("$name: string", function() { 75 | it("works", function() { 76 | var node = audioContext.createConvolver(); 77 | 78 | assert(node.$name === "ConvolverNode"); 79 | }); 80 | }); 81 | 82 | describe("$context: AudioContext", function() { 83 | it("works", function() { 84 | var node = audioContext.createConvolver(); 85 | 86 | assert(node.$context === audioContext); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/WaveShaperNode.js: -------------------------------------------------------------------------------- 1 | describe("WaveShaperNode", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var audioContext; 4 | 5 | beforeEach(function() { 6 | audioContext = new WebAudioTestAPI.AudioContext(); 7 | }); 8 | 9 | describe("constructor()", function() { 10 | it("works", function() { 11 | var node = audioContext.createWaveShaper(); 12 | 13 | assert(node instanceof global.WaveShaperNode); 14 | assert(node instanceof global.AudioNode); 15 | 16 | assert.throws(function() { 17 | return new global.WaveShaperNode(); 18 | }, TypeError); 19 | }); 20 | }); 21 | 22 | describe("#curve: Float32Array", function() { 23 | it("works", function() { 24 | var node = audioContext.createWaveShaper(); 25 | var f32a = new Float32Array(128); 26 | var f32b = new Float32Array(128); 27 | 28 | assert(node.curve === null); 29 | 30 | node.curve = f32a; 31 | assert(node.curve === f32a); 32 | 33 | node.curve = f32b; 34 | assert(node.curve === f32b); 35 | 36 | node.curve = null; 37 | assert(node.curve === null); 38 | 39 | assert.throws(function() { 40 | node.curve = "INVALID"; 41 | }, TypeError); 42 | }); 43 | }); 44 | 45 | describe("#oversample: string", function() { 46 | it("works", function() { 47 | var node = audioContext.createWaveShaper(); 48 | 49 | assert(typeof node.oversample === "string"); 50 | 51 | node.oversample = "none"; 52 | assert(node.oversample === "none"); 53 | 54 | node.oversample = "2x"; 55 | assert(node.oversample === "2x"); 56 | 57 | node.oversample = "4x"; 58 | assert(node.oversample === "4x"); 59 | 60 | assert.throws(function() { 61 | node.oversample = "custom"; 62 | }, TypeError); 63 | }); 64 | }); 65 | 66 | describe("#toJSON(): object", function() { 67 | it("works", function() { 68 | var node = audioContext.createWaveShaper(); 69 | 70 | assert.deepEqual(node.toJSON(), { 71 | name: "WaveShaperNode", 72 | oversample: "none", 73 | inputs: [] 74 | }); 75 | }); 76 | }); 77 | 78 | describe("$name: string", function() { 79 | it("works", function() { 80 | var node = audioContext.createWaveShaper(); 81 | 82 | assert(node.$name === "WaveShaperNode"); 83 | }); 84 | }); 85 | 86 | describe("$context: AudioContext", function() { 87 | it("works", function() { 88 | var node = audioContext.createWaveShaper(); 89 | 90 | assert(node.$context === audioContext); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/ScriptProcessorNode.js: -------------------------------------------------------------------------------- 1 | import Immigration from "./utils/Immigration"; 2 | import AudioNode from "./AudioNode"; 3 | import AudioBuffer from "./AudioBuffer"; 4 | import AudioProcessingEvent from "./AudioProcessingEvent"; 5 | import * as props from "./decorators/props"; 6 | import * as methods from "./decorators/methods"; 7 | import * as validators from "./validators"; 8 | 9 | let immigration = Immigration.getInstance(); 10 | 11 | export default class ScriptProcessorNode extends AudioNode { 12 | static $JSONKeys = []; 13 | 14 | constructor(admission, context, bufferSize, numberOfInputChannels, numberOfOutputChannels) { 15 | super(admission, { 16 | name: "ScriptProcessorNode", 17 | context: context, 18 | numberOfInputs: 1, 19 | numberOfOutputs: 1, 20 | channelCount: numberOfInputChannels, 21 | channelCountMode: "max", 22 | channelInterpretation: "speakers" 23 | }); 24 | this.__createScriptProcessor(bufferSize, numberOfInputChannels, numberOfOutputChannels); 25 | } 26 | 27 | @methods.param("bufferSize", validators.isPositiveInteger) 28 | @methods.param("numberOfInputChannels", validators.isPositiveInteger) 29 | @methods.param("numberOfOutputChannels", validators.isPositiveInteger) 30 | @methods.contract({ 31 | precondition(bufferSize) { 32 | if ([ 256, 512, 1024, 2048, 4096, 8192, 16384 ].indexOf(bufferSize) === -1) { 33 | throw new TypeError(`The {{bufferSize}} should be one of [ 256, 512, 1024, 2048, 4096, 8192, 16384 ], but got ${bufferSize}.`); 34 | } 35 | } 36 | }) 37 | __createScriptProcessor(bufferSize, numberOfInputChannels, numberOfOutputChannels) { 38 | this._.bufferSize = bufferSize; 39 | this._.numberOfInputChannels = numberOfInputChannels; 40 | this._.numberOfOutputChannels = numberOfOutputChannels; 41 | this._.numSamples = 0; 42 | } 43 | 44 | @props.readonly() 45 | bufferSize() { 46 | return this._.bufferSize; 47 | } 48 | 49 | @props.on("audioprocess"); 50 | onaudioprocess() {} 51 | 52 | __process(inNumSamples) { 53 | this._.numSamples -= inNumSamples; 54 | 55 | if (this._.numSamples <= 0) { 56 | this._.numSamples += this.bufferSize; 57 | 58 | let event = immigration.apply(admission => 59 | new AudioProcessingEvent(admission, this) 60 | ); 61 | 62 | event.playbackTime = this.context.currentTime + this.bufferSize / this.context.sampleRate; 63 | event.inputBuffer = immigration.apply(admission => 64 | new AudioBuffer(admission, this.context, this._.numberOfInputChannels, this.bufferSize, this.context.sampleRate) 65 | ); 66 | event.outputBuffer = immigration.apply(admission => 67 | new AudioBuffer(admission, this.context, this._.numberOfOutputChannels, this.bufferSize, this.context.sampleRate) 68 | ); 69 | 70 | this.dispatchEvent(event); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/OscillatorNode.js: -------------------------------------------------------------------------------- 1 | import AudioNode from "./AudioNode"; 2 | import PeriodicWave from "./PeriodicWave"; 3 | import Event from "./dom/Event"; 4 | import toSeconds from "./utils/toSeconds"; 5 | import * as props from "./decorators/props"; 6 | import * as methods from "./decorators/methods"; 7 | import * as validators from "./validators"; 8 | 9 | export default class OscillatorNode extends AudioNode { 10 | static $JSONKeys = [ "type", "frequency", "detune" ]; 11 | 12 | constructor(admission, context) { 13 | super(admission, { 14 | name: "OscillatorNode", 15 | context: context, 16 | numberOfInputs: 0, 17 | numberOfOutputs: 1, 18 | channelCount: 2, 19 | channelCountMode: "max", 20 | channelInterpretation: "speakers" 21 | }); 22 | this._.custom = null; 23 | this._.startTime = Infinity; 24 | this._.stopTime = Infinity; 25 | this._.firedOnEnded = false; 26 | } 27 | 28 | @props.enums([ "sine", "square", "sawtooth", "triangle" ]) 29 | type() {} 30 | 31 | @props.audioparam(440) 32 | frequency() {} 33 | 34 | @props.audioparam(0) 35 | detune() {} 36 | 37 | @props.on("ended") 38 | onended() {} 39 | 40 | @methods.param("[ when ]", validators.isPositiveNumber) 41 | @methods.contract({ 42 | precondition() { 43 | if (this._.startTime !== Infinity) { 44 | throw new Error("Cannot start more than once."); 45 | } 46 | } 47 | }) 48 | start(when = 0) { 49 | this._.startTime = when; 50 | } 51 | 52 | @methods.param("[ when ]", validators.isPositiveNumber) 53 | @methods.contract({ 54 | precondition() { 55 | if (this._.startTime === Infinity) { 56 | throw new Error("Cannot call stop without calling start first."); 57 | } 58 | if (this._.stopTime !== Infinity) { 59 | throw new Error("Cannot stop more than once."); 60 | } 61 | } 62 | }) 63 | stop(when = 0) { 64 | this._.stopTime = when; 65 | } 66 | 67 | @methods.param("periodicWave", validators.isInstanceOf(PeriodicWave)) 68 | setPeriodicWave(periodicWave) { 69 | this._.type = "custom"; 70 | this._.custom = periodicWave; 71 | } 72 | 73 | get $state() { 74 | return this.$stateAtTime(this.context.currentTime); 75 | } 76 | 77 | get $custom() { 78 | return this._.custom; 79 | } 80 | 81 | get $startTime() { 82 | return this._.startTime; 83 | } 84 | 85 | get $stopTime() { 86 | return this._.stopTime; 87 | } 88 | 89 | $stateAtTime(when) { 90 | const playbackTime = toSeconds(when); 91 | 92 | if (this._.startTime === Infinity) { 93 | return "UNSCHEDULED"; 94 | } 95 | if (playbackTime < this._.startTime) { 96 | return "SCHEDULED"; 97 | } 98 | if (playbackTime < this._.stopTime) { 99 | return "PLAYING"; 100 | } 101 | 102 | return "FINISHED"; 103 | } 104 | 105 | __process() { 106 | if (!this._.firedOnEnded && this.$stateAtTime(this.context.currentTime) === "FINISHED") { 107 | this.dispatchEvent(new Event("ended", this)); 108 | this._.firedOnEnded = true; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | web-audio-test-api.js 6 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 |

web-audio-test-api.js

67 |
68 | Web Audio API for CI | GitHub | Documents 69 |
70 |
71 | 72 | 73 | -------------------------------------------------------------------------------- /src/AudioBufferSourceNode.js: -------------------------------------------------------------------------------- 1 | import AudioNode from "./AudioNode"; 2 | import AudioBuffer from "./AudioBuffer"; 3 | import Event from "./dom/Event"; 4 | import toSeconds from "./utils/toSeconds"; 5 | import * as props from "./decorators/props"; 6 | import * as methods from "./decorators/methods"; 7 | import * as validators from "./validators"; 8 | 9 | export default class AudioBufferSourceNode extends AudioNode { 10 | static $JSONKeys = [ "buffer", "playbackRate", "loop", "loopStart", "loopEnd" ]; 11 | 12 | constructor(admission, context) { 13 | super(admission, { 14 | name: "AudioBufferSourceNode", 15 | context: context, 16 | numberOfInputs: 0, 17 | numberOfOutputs: 1, 18 | channelCount: 2, 19 | channelCountMode: "max", 20 | channelInterpretation: "speakers" 21 | }); 22 | 23 | this._.startTime = Infinity; 24 | this._.stopTime = Infinity; 25 | this._.firedOnEnded = false; 26 | } 27 | 28 | @props.typed(validators.isNullOrInstanceOf(AudioBuffer), null) 29 | buffer() {} 30 | 31 | @props.audioparam(1) 32 | playbackRate() {} 33 | 34 | @props.audioparam(0) 35 | detune() {} 36 | 37 | @props.typed(validators.isBoolean, false) 38 | loop() {} 39 | 40 | @props.typed(validators.isPositiveNumber, 0) 41 | loopStart() {} 42 | 43 | @props.typed(validators.isPositiveNumber, 0) 44 | loopEnd() {} 45 | 46 | @props.on("ended") 47 | onended() {} 48 | 49 | @methods.param("[ when ]", validators.isPositiveNumber) 50 | @methods.param("[ offset ]", validators.isPositiveNumber) 51 | @methods.param("[ duration ]", validators.isPositiveNumber) 52 | @methods.contract({ 53 | precondition() { 54 | if (this._.startTime !== Infinity) { 55 | throw new TypeError("Cannot start more than once."); 56 | } 57 | } 58 | }) 59 | start(when = 0, offset = 0, duration = 0) { 60 | this._.startTime = when; 61 | this._.offset = offset; 62 | this._.duration = duration; 63 | } 64 | 65 | @methods.param("[ when ]", validators.isPositiveNumber) 66 | @methods.contract({ 67 | precondition() { 68 | if (this._.startTime === Infinity) { 69 | throw new TypeError("Cannot call stop without calling start first."); 70 | } 71 | if (this._.stopTime !== Infinity) { 72 | throw new TypeError("Cannot stop more than once."); 73 | } 74 | } 75 | }) 76 | stop(when = 0) { 77 | this._.stopTime = when; 78 | } 79 | 80 | get $state() { 81 | return this.$stateAtTime(this.context.currentTime); 82 | } 83 | 84 | get $startTime() { 85 | return this._.startTime; 86 | } 87 | 88 | get $stopTime() { 89 | return this._.stopTime; 90 | } 91 | 92 | $stateAtTime(when) { 93 | const playbackTime = toSeconds(when); 94 | 95 | if (this._.startTime === Infinity) { 96 | return "UNSCHEDULED"; 97 | } 98 | if (playbackTime < this._.startTime) { 99 | return "SCHEDULED"; 100 | } 101 | 102 | let stopTime = this._.stopTime; 103 | 104 | if (!this.loop && this.buffer) { 105 | stopTime = Math.min(stopTime, this._.startTime + this.buffer.duration); 106 | } 107 | 108 | if (playbackTime < stopTime) { 109 | return "PLAYING"; 110 | } 111 | 112 | return "FINISHED"; 113 | } 114 | 115 | __process() { 116 | if (!this._.firedOnEnded && this.$stateAtTime(this.context.currentTime) === "FINISHED") { 117 | this.dispatchEvent(new Event("ended", this)); 118 | this._.firedOnEnded = true; 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /test/dom/EventTarget.js: -------------------------------------------------------------------------------- 1 | describe("EventTarget", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | 4 | describe("constructor()", function() { 5 | it("works", function() { 6 | var target = new WebAudioTestAPI.EventTarget(); 7 | 8 | assert(target instanceof global.window.EventTarget); 9 | }); 10 | it("not work when 'new' directly", function() { 11 | assert.throws(function() { new global.EventTarget(); }, TypeError); 12 | }); 13 | }); 14 | 15 | describe("#addEventListener(type: string, listener: function): void", function() { 16 | it("works", function() { 17 | var target = new WebAudioTestAPI.EventTarget(); 18 | var listener1 = sinon.spy(); 19 | var listener2 = sinon.spy(); 20 | var listener3 = sinon.spy(); 21 | 22 | target.addEventListener("foo", listener1); 23 | target.addEventListener("foo", listener2); 24 | target.addEventListener("bar", listener3); 25 | 26 | assert.deepEqual(target.$listeners("foo"), [ listener1, listener2 ]); 27 | assert.deepEqual(target.$listeners("bar"), [ listener3 ]); 28 | 29 | assert.throws(function() { 30 | target.addEventListener(null, sinon.spy()); 31 | }, TypeError); 32 | 33 | assert.throws(function() { 34 | target.addEventListener("baz", "INVALID"); 35 | }, TypeError); 36 | }); 37 | }); 38 | 39 | describe("#removeEventListener(type: string, listener: function): void", function() { 40 | it("works", function() { 41 | var target = new WebAudioTestAPI.EventTarget(); 42 | var listener1 = sinon.spy(); 43 | var listener2 = sinon.spy(); 44 | var listener3 = sinon.spy(); 45 | 46 | target.addEventListener("foo", listener1); 47 | target.addEventListener("foo", listener2); 48 | target.addEventListener("bar", listener3); 49 | 50 | target.removeEventListener("foo", listener1); 51 | target.removeEventListener("bar", listener2); 52 | target.removeEventListener("baz", listener3); 53 | 54 | assert.deepEqual(target.$listeners("foo"), [ listener2 ]); 55 | assert.deepEqual(target.$listeners("bar"), [ listener3 ]); 56 | 57 | assert.throws(function() { 58 | target.removeEventListener(null, sinon.spy()); 59 | }, TypeError); 60 | 61 | assert.throws(function() { 62 | target.removeEventListener("baz", "INVALID"); 63 | }, TypeError); 64 | }); 65 | }); 66 | 67 | describe("#dispatchEvent(event: Event): void", function() { 68 | it("works", function() { 69 | var target = new WebAudioTestAPI.EventTarget(); 70 | var listener1 = sinon.spy(); 71 | var listener2 = sinon.spy(); 72 | var listener3 = sinon.spy(); 73 | var listener4 = sinon.spy(); 74 | var event = new WebAudioTestAPI.Event("foo"); 75 | 76 | target.addEventListener("foo", listener1); 77 | target.addEventListener("foo", listener2); 78 | target.addEventListener("bar", listener3); 79 | 80 | target.onfoo = listener4; 81 | 82 | target.dispatchEvent(event); 83 | 84 | assert(listener1.callCount === 1); 85 | assert(listener2.callCount === 1); 86 | assert(listener3.callCount === 0); 87 | assert(listener4.callCount === 1); 88 | assert(listener1.calledOn(target)); 89 | assert(listener2.calledOn(target)); 90 | assert(listener4.calledOn(target)); 91 | assert(listener1.args[0][0] === event); 92 | assert(listener2.args[0][0] === event); 93 | assert(listener4.args[0][0] === event); 94 | 95 | assert.throws(function() { 96 | target.dispatchEvent({}); 97 | }, TypeError); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /test/utils/Immigration.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import Immigration from "../../src/utils/Immigration"; 3 | 4 | describe("Immigration", () => { 5 | describe("constructor()", () => { 6 | it("works", () => { 7 | let immigration = new Immigration(); 8 | 9 | assert(immigration instanceof Immigration); 10 | }); 11 | }); 12 | describe(".getInstance(): Immigration", () => { 13 | it("works", () => { 14 | let immigration1 = Immigration.getInstance(); 15 | let immigration2 = Immigration.getInstance(); 16 | 17 | assert(immigration1 instanceof Immigration); 18 | assert(immigration2 instanceof Immigration); 19 | assert(immigration1 === immigration2); 20 | }); 21 | }); 22 | describe("workflow", () => { 23 | describe("apply -> check", () => { 24 | it("ok", () => { 25 | let immigration = new Immigration(); 26 | 27 | let result = immigration.apply((admission) => { 28 | immigration.check(admission); 29 | 30 | return 1000; 31 | }); 32 | 33 | assert(result === 1000); 34 | }); 35 | }); 36 | describe("apply1 -> check1 -> apply2 -> check2", () => { 37 | it("ok", () => { 38 | let immigration = new Immigration(); 39 | 40 | let result = immigration.apply((admission1) => { 41 | immigration.check(admission1); 42 | 43 | let result2 = immigration.apply((admission2) => { 44 | immigration.check(admission2); 45 | 46 | return 1000; 47 | }); 48 | 49 | return result2; 50 | }); 51 | 52 | assert(result === 1000); 53 | }); 54 | }); 55 | describe("apply1 -> apply2 -> check2 -> check1", () => { 56 | it("ok", () => { 57 | let immigration = new Immigration(); 58 | 59 | let result = immigration.apply((admission1) => { 60 | let result2 = immigration.apply((admission2) => { 61 | immigration.check(admission2); 62 | 63 | return 1000; 64 | }); 65 | 66 | immigration.check(admission1); 67 | 68 | return result2; 69 | }); 70 | 71 | assert(result === 1000); 72 | }); 73 | }); 74 | describe("apply -> check with an invalid admission", () => { 75 | it("call failed callback", () => { 76 | let passed = false; 77 | let immigration = new Immigration(); 78 | 79 | assert.throws(() => { 80 | immigration.apply((admission1) => { 81 | let result2 = immigration.apply(() => { 82 | immigration.check(admission1, () => { 83 | passed = true; 84 | }); 85 | 86 | return 1000; 87 | }); 88 | 89 | immigration.check(admission1); 90 | 91 | return result2; 92 | }); 93 | }, Error); 94 | 95 | assert(passed); 96 | }); 97 | }); 98 | describe("apply -> NOT check", () => { 99 | it("failed", () => { 100 | let immigration = new Immigration(); 101 | 102 | assert.throws(() => { 103 | /* admission */ 104 | immigration.apply(() => { 105 | // immigration.check(admission); 106 | }); 107 | }, Error); 108 | }); 109 | }); 110 | describe("apply -> check -> checl", () => { 111 | it("failed", () => { 112 | let immigration = new Immigration(); 113 | 114 | assert.throws(() => { 115 | immigration.apply((admission) => { 116 | immigration.check(admission); 117 | immigration.check(admission); 118 | }); 119 | }, Error); 120 | }); 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /test/DynamicsCompressorNode.js: -------------------------------------------------------------------------------- 1 | describe("DynamicsCompressorNode", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var audioContext; 4 | 5 | beforeEach(function() { 6 | audioContext = new WebAudioTestAPI.AudioContext(); 7 | }); 8 | 9 | describe("constructor()", function() { 10 | it("works", function() { 11 | var node = audioContext.createDynamicsCompressor(); 12 | 13 | assert(node instanceof global.DynamicsCompressorNode); 14 | assert(node instanceof global.AudioNode); 15 | }); 16 | it("not work when 'new' directly", function() { 17 | assert.throws(function() { new global.DynamicsCompressorNode(); }, TypeError); 18 | }); 19 | }); 20 | 21 | describe("#threshold: AudioParam", function() { 22 | it("works", function() { 23 | var node = audioContext.createDynamicsCompressor(); 24 | 25 | assert(node.threshold instanceof WebAudioTestAPI.AudioParam); 26 | 27 | assert.throws(function() { 28 | node.threshold = 0; 29 | }, TypeError); 30 | }); 31 | }); 32 | 33 | describe("#knee: AudioParam", function() { 34 | it("works", function() { 35 | var node = audioContext.createDynamicsCompressor(); 36 | 37 | assert(node.knee instanceof WebAudioTestAPI.AudioParam); 38 | 39 | assert.throws(function() { 40 | node.knee = 0; 41 | }, TypeError); 42 | }); 43 | }); 44 | 45 | describe("#ratio: AudioParam", function() { 46 | it("works", function() { 47 | var node = audioContext.createDynamicsCompressor(); 48 | 49 | assert(node.ratio instanceof WebAudioTestAPI.AudioParam); 50 | 51 | assert.throws(function() { 52 | node.ratio = 0; 53 | }, TypeError); 54 | }); 55 | }); 56 | 57 | describe("#reduction: AudioParam", function() { 58 | it("works", function() { 59 | var node = audioContext.createDynamicsCompressor(); 60 | 61 | assert(node.reduction instanceof WebAudioTestAPI.AudioParam); 62 | 63 | assert.throws(function() { 64 | node.reduction = 0; 65 | }, TypeError); 66 | }); 67 | }); 68 | 69 | describe("#attack: AudioParam", function() { 70 | it("works", function() { 71 | var node = audioContext.createDynamicsCompressor(); 72 | 73 | assert(node.attack instanceof WebAudioTestAPI.AudioParam); 74 | 75 | assert.throws(function() { 76 | node.attack = 0; 77 | }, TypeError); 78 | }); 79 | }); 80 | 81 | describe("#release: AudioParam", function() { 82 | it("works", function() { 83 | var node = audioContext.createDynamicsCompressor(); 84 | 85 | assert(node.release instanceof WebAudioTestAPI.AudioParam); 86 | 87 | assert.throws(function() { 88 | node.release = 0; 89 | }, TypeError); 90 | }); 91 | }); 92 | 93 | describe("#toJSON(): object", function() { 94 | it("works", function() { 95 | var node = audioContext.createDynamicsCompressor(); 96 | 97 | assert.deepEqual(node.toJSON(), { 98 | name: "DynamicsCompressorNode", 99 | threshold: { 100 | value: -24, 101 | inputs: [] 102 | }, 103 | knee: { 104 | value: 30, 105 | inputs: [] 106 | }, 107 | ratio: { 108 | value: 12, 109 | inputs: [] 110 | }, 111 | reduction: { 112 | value: 0, 113 | inputs: [] 114 | }, 115 | attack: { 116 | value: 0.003, 117 | inputs: [] 118 | }, 119 | release: { 120 | value: 0.25, 121 | inputs: [] 122 | }, 123 | inputs: [] 124 | }); 125 | }); 126 | }); 127 | 128 | describe("$name: string", function() { 129 | it("works", function() { 130 | var node = audioContext.createDynamicsCompressor(); 131 | 132 | assert(node.$name === "DynamicsCompressorNode"); 133 | }); 134 | }); 135 | 136 | describe("$context: AudioContext", function() { 137 | it("works", function() { 138 | var node = audioContext.createDynamicsCompressor(); 139 | 140 | assert(node.$context === audioContext); 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /test/AudioListener.js: -------------------------------------------------------------------------------- 1 | describe("AudioListener", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var audioContext; 4 | 5 | beforeEach(function() { 6 | audioContext = new WebAudioTestAPI.AudioContext(); 7 | }); 8 | 9 | describe("constructor()", function() { 10 | it("works", function() { 11 | var listener = audioContext.listener; 12 | 13 | assert(listener instanceof global.AudioListener); 14 | }); 15 | it("not work when 'new' directly", function() { 16 | assert.throws(function() { new global.AudioListener(); }, TypeError); 17 | }); 18 | }); 19 | 20 | describe("#dopplerFactor: number", function() { 21 | it("works", function() { 22 | var listener = audioContext.listener; 23 | 24 | assert(typeof listener.dopplerFactor === "number"); 25 | 26 | listener.dopplerFactor = 1; 27 | assert(listener.dopplerFactor === 1); 28 | 29 | listener.dopplerFactor = 2; 30 | assert(listener.dopplerFactor === 2); 31 | 32 | assert.throws(function() { 33 | listener.dopplerFactor = "INVALID"; 34 | }, TypeError); 35 | }); 36 | }); 37 | 38 | describe("#speedOfSound: number", function() { 39 | it("works", function() { 40 | var listener = audioContext.listener; 41 | 42 | assert(typeof listener.speedOfSound === "number"); 43 | 44 | listener.speedOfSound = 686.6; 45 | assert(listener.speedOfSound === 686.6); 46 | 47 | listener.speedOfSound = 1373.2; 48 | assert(listener.speedOfSound === 1373.2); 49 | 50 | assert.throws(function() { 51 | listener.speedOfSound = "INVALID"; 52 | }, TypeError); 53 | }); 54 | }); 55 | 56 | describe("#setPosition(x: number, y: number, z: number): void", function() { 57 | it("works", function() { 58 | var listener = audioContext.listener; 59 | 60 | listener.setPosition(0, 0, 0); 61 | 62 | assert.throws(function() { 63 | listener.setPosition("INVALID", 0, 0); 64 | }, TypeError); 65 | 66 | assert.throws(function() { 67 | listener.setPosition(0, "INVALID"); 68 | }, TypeError); 69 | 70 | assert.throws(function() { 71 | listener.setPosition(0, 0, "INVALID"); 72 | }, TypeError); 73 | }); 74 | }); 75 | 76 | describe("#setOrientation(x: number, y: number, z: number, xUp: number, yUp: number, zUp: number): void", function() { 77 | it("works", function() { 78 | var listener = audioContext.listener; 79 | 80 | listener.setOrientation(0, 0, 0, 0, 0, 0); 81 | 82 | assert.throws(function() { 83 | listener.setOrientation("INVALID", 0, 0, 0, 0, 0); 84 | }, TypeError); 85 | 86 | assert.throws(function() { 87 | listener.setOrientation(0, "INVALID", 0, 0, 0, 0); 88 | }, TypeError); 89 | 90 | assert.throws(function() { 91 | listener.setOrientation(0, 0, "INVALID", 0, 0, 0); 92 | }, TypeError); 93 | 94 | assert.throws(function() { 95 | listener.setOrientation(0, 0, 0, "INVALID", 0, 0); 96 | }, TypeError); 97 | 98 | assert.throws(function() { 99 | listener.setOrientation(0, 0, 0, 0, "INVALID", 0); 100 | }, TypeError); 101 | 102 | assert.throws(function() { 103 | listener.setOrientation(0, 0, 0, 0, 0, "INVALID"); 104 | }, TypeError); 105 | }); 106 | }); 107 | 108 | describe("#setVelocity(x: number, y: number, z: number): void", function() { 109 | it("works", function() { 110 | var listener = audioContext.listener; 111 | 112 | listener.setVelocity(0, 0, 0); 113 | 114 | assert.throws(function() { 115 | listener.setVelocity("INVALID", 0, 0); 116 | }, TypeError); 117 | 118 | assert.throws(function() { 119 | listener.setVelocity(0, "INVALID"); 120 | }, TypeError); 121 | 122 | assert.throws(function() { 123 | listener.setVelocity(0, 0, "INVALID"); 124 | }, TypeError); 125 | }); 126 | }); 127 | 128 | describe("$name: string", function() { 129 | it("works", function() { 130 | var listener = audioContext.listener; 131 | 132 | assert(listener.$name === "AudioListener"); 133 | }); 134 | }); 135 | 136 | describe("$context: AudioContext", function() { 137 | it("works", function() { 138 | var listener = audioContext.listener; 139 | 140 | assert(listener.$context === audioContext); 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /src/OfflineAudioContext.js: -------------------------------------------------------------------------------- 1 | import Configuration from "./utils/Configuration"; 2 | import Immigration from "./utils/Immigration"; 3 | import Event from "./dom/Event"; 4 | import AudioContext from "./AudioContext"; 5 | import AudioBuffer from "./AudioBuffer"; 6 | import OfflineAudioCompletionEvent from "./OfflineAudioCompletionEvent"; 7 | import * as props from "./decorators/props"; 8 | import * as methods from "./decorators/methods"; 9 | import * as validators from "./validators"; 10 | 11 | let configuration = Configuration.getInstance(); 12 | let immigration = Immigration.getInstance(); 13 | 14 | export default class OfflineAudioContext extends AudioContext { 15 | constructor(numberOfChannels, length, sampleRate) { 16 | super(); 17 | this.__OfflineAudioContext(numberOfChannels, length, sampleRate); 18 | } 19 | 20 | @methods.param("numberOfChannels", validators.isPositiveInteger) 21 | @methods.param("length", validators.isPositiveInteger) 22 | @methods.param("sampleRate", validators.isPositiveInteger) 23 | __OfflineAudioContext(numberOfChannels, length, sampleRate) { 24 | this._.sampleRate = sampleRate; 25 | this._.numberOfChannels = numberOfChannels; 26 | this._.length = length; 27 | this._.rendering = false; 28 | this._.resolve = null; 29 | this._.state = "suspended"; 30 | } 31 | 32 | @props.on("complete") 33 | oncomplete() {} 34 | 35 | suspend() { 36 | return this.__transitionToState("suspend"); 37 | } 38 | 39 | resume() { 40 | return this.__transitionToState("resume"); 41 | } 42 | 43 | close() { 44 | return this.__transitionToState("close"); 45 | } 46 | 47 | @methods.contract({ 48 | precondition() { 49 | if (this._.rendering) { 50 | throw new TypeError("Cannot call startRendering more than once."); 51 | } 52 | } 53 | }) 54 | startRendering() { 55 | let isPromiseBased = configuration.getState("OfflineAudioContext#startRendering") === "promise"; 56 | 57 | this._.rendering = true; 58 | 59 | if (isPromiseBased) { 60 | return new Promise((resolve) => { 61 | this._.resolve = resolve; 62 | this._.state = "running"; 63 | this.dispatchEvent(new Event("statechange", this)); 64 | }); 65 | } 66 | 67 | this._.state = "running"; 68 | this.dispatchEvent(new Event("statechange", this)); 69 | } 70 | 71 | @methods.contract({ 72 | precondition(methodName) { 73 | if (configuration.getState(`AudioContext#${methodName}`) !== "enabled") { 74 | throw new TypeError("not enabled"); 75 | } 76 | } 77 | }) 78 | __transitionToState(methodName) { 79 | return new Promise(() => { 80 | throw new TypeError(`Cannot ${methodName} on an OfflineAudioContext.`); 81 | }); 82 | } 83 | 84 | get $name() { 85 | return "OfflineAudioContext"; 86 | } 87 | 88 | __process(microseconds) { 89 | if (!this._.rendering || this._.length <= this._.processedSamples) { 90 | return; 91 | } 92 | 93 | let nextMicroCurrentTime = this._.microCurrentTime + microseconds; 94 | 95 | while (this._.microCurrentTime < nextMicroCurrentTime) { 96 | let microCurrentTime = Math.min(this._.microCurrentTime + 1000, nextMicroCurrentTime); 97 | let processedSamples = Math.floor(microCurrentTime / (1000 * 1000) * this.sampleRate); 98 | let inNumSamples = processedSamples - this._.processedSamples; 99 | 100 | this.destination.$process(inNumSamples, ++this._.tick); 101 | 102 | this._.microCurrentTime = microCurrentTime; 103 | this._.processedSamples = processedSamples; 104 | 105 | if (this._.length <= this._.processedSamples) { 106 | break; 107 | } 108 | } 109 | 110 | if (this._.length <= this._.processedSamples) { 111 | let renderedBuffer = immigration.apply(admission => 112 | new AudioBuffer(admission, this, this._.numberOfChannels, this._.length, this.sampleRate) 113 | ); 114 | let event = immigration.apply(admission => 115 | new OfflineAudioCompletionEvent(admission, this) 116 | ); 117 | 118 | event.renderedBuffer = renderedBuffer; 119 | 120 | this._.state = "closed"; 121 | 122 | this.dispatchEvent(event); 123 | if (this._.resolve !== null) { 124 | this._.resolve(renderedBuffer); 125 | this._.resolve = null; 126 | } 127 | 128 | this.dispatchEvent(new Event("statechange", this)); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/decorators/props.js: -------------------------------------------------------------------------------- 1 | import Immigration from "../utils/Immigration"; 2 | import format from "../utils/format"; 3 | import toS from "../utils/toS"; 4 | 5 | const immigration = Immigration.getInstance(); 6 | 7 | function createSetterError(klassName, propName, message) { 8 | return new TypeError(format(` 9 | Failed to set the '${propName}' property on '${klassName}' 10 | ${message} 11 | `) + "\n"); 12 | } 13 | 14 | export function audioparam(defaultValue) { 15 | return (target, propName, descriptor) => { 16 | descriptor.get = function get() { 17 | if (!this._.hasOwnProperty(propName)) { 18 | this._[propName] = immigration.apply(admission => 19 | new global.WebAudioTestAPI.AudioParam(admission, this, propName, defaultValue) 20 | ); 21 | } 22 | return this._[propName]; 23 | }; 24 | 25 | descriptor.set = function set(value) { 26 | throw createSetterError(this.constructor.name, propName, ` 27 | \tAttempt to assign to readonly property. Do you mean this? 28 | 29 | \t\t\t${propName}.value = ${toS(value)}; 30 | `); 31 | }; 32 | 33 | return { 34 | get: descriptor.get, 35 | set: descriptor.set, 36 | enumerable: descriptor.enumerable, 37 | configurable: descriptor.configurable 38 | }; 39 | }; 40 | } 41 | 42 | export function enums(values) { 43 | return (target, propName, descriptor) => { 44 | if (typeof descriptor.get !== "function") { 45 | descriptor.get = function get() { 46 | if (!this._.hasOwnProperty(propName)) { 47 | this._[propName] = values[0]; 48 | } 49 | return this._[propName]; 50 | }; 51 | } 52 | 53 | descriptor.set = function set(value) { 54 | if (values.indexOf(value) === -1) { 55 | throw createSetterError(this.constructor.name, propName, ` 56 | \tThis property should be one of [ ${values.map(toS).join(", ")} ], but got ${toS(value)}. 57 | `); 58 | } 59 | this._[propName] = value; 60 | }; 61 | 62 | return { 63 | get: descriptor.get, 64 | set: descriptor.set, 65 | enumerable: descriptor.enumerable, 66 | configurable: descriptor.configurable 67 | }; 68 | }; 69 | } 70 | 71 | export function on() { 72 | return (target, propName, descriptor) => { 73 | descriptor.get = function get() { 74 | if (!this._.hasOwnProperty(propName)) { 75 | this._[propName] = null; 76 | } 77 | return this._[propName]; 78 | }; 79 | descriptor.set = function set(value) { 80 | if (value !== null && typeof value !== "function") { 81 | throw createSetterError(this.constructor.name, propName, ` 82 | \tA callback should be a function or null, but got ${toS(value)}. 83 | `); 84 | } 85 | this._[propName] = value; 86 | }; 87 | 88 | return { 89 | get: descriptor.get, 90 | set: descriptor.set, 91 | enumerable: descriptor.enumerable, 92 | configurable: descriptor.configurable 93 | }; 94 | }; 95 | } 96 | 97 | export function readonly(value) { 98 | return (target, propName, descriptor) => { 99 | const getter = descriptor.get || descriptor.value; 100 | 101 | if (typeof descriptor.get !== "function") { 102 | descriptor.get = function get() { 103 | if (typeof value !== "undefined") { 104 | return value; 105 | } 106 | if (typeof getter === "function") { 107 | return this::getter(); 108 | } 109 | }; 110 | } 111 | 112 | descriptor.set = function set() { 113 | throw createSetterError(this.constructor.name, propName, ` 114 | \tAttempt to assign to readonly property. 115 | `); 116 | }; 117 | 118 | return { 119 | get: descriptor.get, 120 | set: descriptor.set, 121 | enumerable: descriptor.enumerable, 122 | configurable: descriptor.configurable 123 | }; 124 | }; 125 | } 126 | 127 | export function typed(validator, defaultValue) { 128 | return (target, propName, descriptor) => { 129 | if (typeof descriptor.get !== "function") { 130 | descriptor.get = function get() { 131 | if (!this._.hasOwnProperty(propName)) { 132 | this._[propName] = defaultValue; 133 | } 134 | return this._[propName]; 135 | }; 136 | } 137 | 138 | if (typeof descriptor.set !== "function") { 139 | descriptor.set = function set(value) { 140 | if (!validator.test(value)) { 141 | throw createSetterError(this.constructor.name, propName, ` 142 | \tThis property should be $a ${validator.description}, but got ${toS(value)}. 143 | `); 144 | } 145 | this._[propName] = value; 146 | }; 147 | } 148 | 149 | return { 150 | get: descriptor.get, 151 | set: descriptor.set, 152 | enumerable: descriptor.enumerable, 153 | configurable: descriptor.configurable 154 | }; 155 | }; 156 | } 157 | -------------------------------------------------------------------------------- /test/BiquadFilterNode.js: -------------------------------------------------------------------------------- 1 | describe("BiquadFilterNode", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var audioContext; 4 | 5 | beforeEach(function() { 6 | audioContext = new WebAudioTestAPI.AudioContext(); 7 | }); 8 | 9 | describe("constructor()", function() { 10 | it("works", function() { 11 | var node = audioContext.createBiquadFilter(); 12 | 13 | assert(node instanceof global.BiquadFilterNode); 14 | assert(node instanceof global.AudioNode); 15 | }); 16 | it("not work when 'new' directly", function() { 17 | assert.throws(function() { new global.BiquadFilterNode(); }, TypeError); 18 | }); 19 | }); 20 | 21 | describe("#type: string", function() { 22 | it("works", function() { 23 | var node = audioContext.createBiquadFilter(); 24 | 25 | assert(typeof node.type === "string"); 26 | 27 | node.type = "lowpass"; 28 | assert(node.type === "lowpass"); 29 | 30 | node.type = "highpass"; 31 | assert(node.type === "highpass"); 32 | 33 | node.type = "bandpass"; 34 | assert(node.type === "bandpass"); 35 | 36 | node.type = "lowshelf"; 37 | assert(node.type === "lowshelf"); 38 | 39 | node.type = "highshelf"; 40 | assert(node.type === "highshelf"); 41 | 42 | node.type = "peaking"; 43 | assert(node.type === "peaking"); 44 | 45 | node.type = "notch"; 46 | assert(node.type === "notch"); 47 | 48 | node.type = "allpass"; 49 | assert(node.type === "allpass"); 50 | 51 | assert.throws(function() { 52 | node.type = "custom"; 53 | }, TypeError); 54 | }); 55 | }); 56 | 57 | describe("#frequency: AudioParam", function() { 58 | it("works", function() { 59 | var node = audioContext.createBiquadFilter(); 60 | 61 | assert(node.frequency instanceof WebAudioTestAPI.AudioParam); 62 | 63 | assert.throws(function() { 64 | node.frequency = 0; 65 | }, TypeError); 66 | }); 67 | }); 68 | 69 | describe("#detune: AudioParam", function() { 70 | it("works", function() { 71 | var node = audioContext.createBiquadFilter(); 72 | 73 | assert(node.detune instanceof WebAudioTestAPI.AudioParam); 74 | 75 | assert.throws(function() { 76 | node.detune = 0; 77 | }, TypeError); 78 | }); 79 | }); 80 | 81 | describe("#Q: AudioParam", function() { 82 | it("works", function() { 83 | var node = audioContext.createBiquadFilter(); 84 | 85 | assert(node.Q instanceof WebAudioTestAPI.AudioParam); 86 | 87 | assert.throws(function() { 88 | node.Q = 0; 89 | }, TypeError); 90 | }); 91 | }); 92 | 93 | describe("#gain: AudioParam", function() { 94 | it("works", function() { 95 | var node = audioContext.createBiquadFilter(); 96 | 97 | assert(node.gain instanceof WebAudioTestAPI.AudioParam); 98 | assert.throws(function() { 99 | node.gain = 0; 100 | }, TypeError); 101 | }); 102 | }); 103 | 104 | describe("#getFrequencyResponse(frequencyHz: Float32Array, magResponse: Float32Array, phaseResponse: Float32Array): void", function() { 105 | it("works", function() { 106 | var node = audioContext.createBiquadFilter(); 107 | var f32f = new Float32Array(128); 108 | var f32m = new Float32Array(128); 109 | var f32p = new Float32Array(128); 110 | 111 | node.getFrequencyResponse(f32f, f32p, f32m); 112 | 113 | assert.throws(function() { 114 | node.getFrequencyResponse("INVALID", f32p, f32m); 115 | }, TypeError); 116 | 117 | assert.throws(function() { 118 | node.getFrequencyResponse(f32f, "INVALID", f32m); 119 | }, TypeError); 120 | 121 | assert.throws(function() { 122 | node.getFrequencyResponse(f32f, f32p, "INVALID"); 123 | }, TypeError); 124 | }); 125 | }); 126 | 127 | describe("#toJSON(): object", function() { 128 | it("works", function() { 129 | var node = audioContext.createBiquadFilter(); 130 | 131 | assert.deepEqual(node.toJSON(), { 132 | name: "BiquadFilterNode", 133 | type: "lowpass", 134 | frequency: { 135 | value: 350, 136 | inputs: [] 137 | }, 138 | detune: { 139 | value: 0, 140 | inputs: [] 141 | }, 142 | Q: { 143 | value: 1, 144 | inputs: [] 145 | }, 146 | gain: { 147 | value: 0, 148 | inputs: [] 149 | }, 150 | inputs: [] 151 | }); 152 | }); 153 | }); 154 | 155 | describe("$name: string", function() { 156 | it("works", function() { 157 | var node = audioContext.createBiquadFilter(); 158 | 159 | assert(node.$name === "BiquadFilterNode"); 160 | }); 161 | }); 162 | 163 | describe("$context: AudioContext", function() { 164 | it("works", function() { 165 | var node = audioContext.createBiquadFilter(); 166 | 167 | assert(node.$context === audioContext); 168 | }); 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /src/AudioBuffer.js: -------------------------------------------------------------------------------- 1 | import Configuration from "./utils/Configuration"; 2 | import Immigration from "./utils/Immigration"; 3 | import * as props from "./decorators/props"; 4 | import * as methods from "./decorators/methods"; 5 | import * as validators from "./validators"; 6 | 7 | let configuration = Configuration.getInstance(); 8 | let immigration = Immigration.getInstance(); 9 | 10 | export default class AudioBuffer { 11 | constructor(admission, context, numberOfChannels, length, sampleRate) { 12 | immigration.check(admission, () => { 13 | throw new TypeError("Illegal constructor"); 14 | }); 15 | Object.defineProperty(this, "_", { value: {} }); 16 | 17 | this._.context = context; 18 | this.__createAudioBuffer(numberOfChannels, length, sampleRate); 19 | } 20 | 21 | @methods.param("numberOfChannels", validators.isPositiveInteger) 22 | @methods.param("length", validators.isPositiveInteger) 23 | @methods.param("sampleRate", validators.isPositiveInteger) 24 | __createAudioBuffer(numberOfChannels, length, sampleRate) { 25 | this._.numberOfChannels = numberOfChannels; 26 | this._.length = length; 27 | this._.sampleRate = sampleRate; 28 | this._.data = new Array(numberOfChannels); 29 | 30 | for (let i = 0; i < numberOfChannels; i++) { 31 | this._.data[i] = new Float32Array(length); 32 | } 33 | } 34 | 35 | @props.readonly() 36 | sampleRate() { 37 | return this._.sampleRate; 38 | } 39 | 40 | @props.readonly() 41 | length() { 42 | return this._.length; 43 | } 44 | 45 | @props.readonly() 46 | duration() { 47 | return this.length / this.sampleRate; 48 | } 49 | 50 | @props.readonly() 51 | numberOfChannels() { 52 | return this._.numberOfChannels; 53 | } 54 | 55 | @methods.param("channel", validators.isPositiveInteger) 56 | @methods.contract({ 57 | precondition(channel) { 58 | if (this._.data.length <= channel) { 59 | throw new TypeError(`The {{channel}} index (${channel}) exceeds number of channels (${this._.data.length}).`); 60 | } 61 | } 62 | }) 63 | @methods.returns(validators.isInstanceOf(Float32Array)) 64 | getChannelData(channel) { 65 | return this._.data[channel]; 66 | } 67 | 68 | @methods.param("destination", validators.isInstanceOf(Float32Array)) 69 | @methods.param("channelNumber", validators.isPositiveInteger) 70 | @methods.param("[ startInChannel ]", validators.isPositiveInteger) 71 | @methods.contract({ 72 | precondition(destination, channelNumber, startInChannel = 0) { 73 | if (this._.data.length <= channelNumber) { 74 | throw new TypeError(`The {{channelNumber}} provided (${channelNumber}) is outside the range [0, ${this._.data.length}).`); 75 | } 76 | if (this._.length <= startInChannel) { 77 | throw new TypeError(`The {{startInChannel}} provided (${startInChannel}) is outside the range [0, ${this._.length}).`); 78 | } 79 | if (configuration.getState("AudioBuffer#copyFromChannel") !== "enabled") { 80 | throw new TypeError("not enabled"); 81 | } 82 | } 83 | }) 84 | copyFromChannel(destination, channelNumber, startInChannel = 0) { 85 | let source = this._.data[channelNumber].subarray(startInChannel); 86 | 87 | destination.set(source.subarray(0, Math.min(source.length, destination.length))); 88 | } 89 | 90 | @methods.param("source", validators.isInstanceOf(Float32Array)) 91 | @methods.param("channelNumber", validators.isPositiveInteger) 92 | @methods.param("[ startInChannel ]", validators.isPositiveInteger) 93 | @methods.contract({ 94 | precondition(source, channelNumber, startInChannel = 0) { 95 | if (this._.data.length <= channelNumber) { 96 | throw new TypeError(`The {{channelNumber}} provided (${channelNumber}) is outside the range [0, ${this._.data.length}).`); 97 | } 98 | if (this._.length <= startInChannel) { 99 | throw new TypeError(`The {{startInChannel}} provided (${startInChannel}) is outside the range [0, ${this._.length}).`); 100 | } 101 | if (configuration.getState("AudioBuffer#copyToChannel") !== "enabled") { 102 | throw new TypeError("not enabled"); 103 | } 104 | } 105 | }) 106 | copyToChannel(source, channelNumber, startInChannel = 0) { 107 | let clipped = source.subarray(0, Math.min(source.length, this._.length - startInChannel)); 108 | 109 | this._.data[channelNumber].set(clipped, startInChannel); 110 | } 111 | 112 | toJSON() { 113 | let json = { 114 | name: this.$name, 115 | sampleRate: this.sampleRate, 116 | length: this.length, 117 | duration: this.duration, 118 | numberOfChannels: this.numberOfChannels 119 | }; 120 | 121 | if (this.$context.VERBOSE_JSON) { 122 | json.data = this._.data.map((data) => { 123 | return Array.prototype.slice.call(data); 124 | }); 125 | } 126 | 127 | return json; 128 | } 129 | 130 | get $name() { 131 | return "AudioBuffer"; 132 | } 133 | 134 | get $context() { 135 | return this._.context; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/decorators/methods.js: -------------------------------------------------------------------------------- 1 | import format from "../utils/format"; 2 | import toS from "../utils/toS"; 3 | 4 | const repository = new WeakMap(); 5 | 6 | function createMethodForm(methodName, parameters, returnValue, errParamName) { 7 | const retType = returnValue ? returnValue.typeName : "void"; 8 | let result = methodName + "("; 9 | let optional = false; 10 | let errArgIndex = -1; 11 | 12 | for (let i = 0; i < parameters.length; i++) { 13 | if (!optional && parameters[i].optional) { 14 | optional = true; 15 | result += "[ "; 16 | } 17 | if (parameters[i].paramName === errParamName) { 18 | errArgIndex = result.length; 19 | } 20 | result += parameters[i].paramName; 21 | // result += ": " + parameters[i].validator.typeName; 22 | if (i < parameters.length - 1) { 23 | result += ", "; 24 | } 25 | } 26 | 27 | if (optional) { 28 | result += " ]"; 29 | } 30 | 31 | result += "): " + retType; 32 | 33 | return [ errArgIndex, result ]; 34 | } 35 | 36 | function repeat(ch, n) { 37 | let str = ""; 38 | 39 | while (n--) { 40 | str += ch; 41 | } 42 | 43 | return str; 44 | } 45 | 46 | function createExecuteError(klassName, methodName, parameters, returnValue, message) { 47 | const matches = /{{(\w+)}}/.exec(message); 48 | 49 | if (matches) { 50 | const [ errArgIndex, methodForm ] = createMethodForm(methodName, parameters, returnValue, matches[1]); 51 | 52 | if (errArgIndex !== -1) { 53 | message = [ 54 | "\t" + methodForm, 55 | "\t" + repeat(" ", errArgIndex) + "|", 56 | "\t" + repeat(" ", errArgIndex) + message 57 | ].join("\n"); 58 | } 59 | } 60 | 61 | return new TypeError(format(` 62 | Failed to execute the '${methodName}' on '${klassName}' 63 | 64 | ${message} 65 | `) + "\n"); 66 | } 67 | 68 | function createValidator(methodName) { 69 | const config = {}; 70 | 71 | function validate(...args) { 72 | const methodName = config.methodName; 73 | const parameters = config.parameters; 74 | const returnValue = config.returnValue; 75 | const errArgIndex = validateArguments(args, parameters); 76 | 77 | if (errArgIndex !== -1) { 78 | const errParamName = parameters[errArgIndex].paramName; 79 | const expectedType = parameters[errArgIndex].validator.description; 80 | const actualValue = toS(args[errArgIndex]); 81 | const errMessage = `'{{${errParamName}}}' require $a ${expectedType}, but got ${actualValue}.`; 82 | 83 | throw createExecuteError(this.constructor.name, methodName, parameters, returnValue, errMessage); 84 | } 85 | 86 | if (typeof config.precondition === "function") { 87 | try { 88 | this::config.precondition(...args); 89 | } catch (e) { 90 | throw createExecuteError(this.constructor.name, methodName, parameters, returnValue, e.message.trim()); 91 | } 92 | } 93 | 94 | const res = this::config.methodBody(...args); 95 | 96 | if (typeof config.postcondition === "function") { 97 | try { 98 | this::config.postcondition(res); 99 | } catch (e) { 100 | throw createExecuteError(this.constructor.name, methodName, parameters, returnValue, e.message.trim()); 101 | } 102 | } 103 | 104 | return res; 105 | } 106 | 107 | config.methodName = /(?:__)?(\w+)/.exec(methodName)[1]; 108 | config.parameters = []; 109 | config.descriptor = { 110 | value: validate, enumerable: true, configurable: true 111 | }; 112 | 113 | return config; 114 | } 115 | 116 | function validateArguments(values, validators) { 117 | for (let i = 0; i < validators.length; i++) { 118 | if (validators[i].optional === true && values.length <= i) { 119 | break; 120 | } 121 | if (!validators[i].validator.test(values[i])) { 122 | return i; 123 | } 124 | } 125 | return -1; 126 | } 127 | 128 | function getMethodConfig(target, methodName) { 129 | let classConfig = repository.get(target); 130 | 131 | if (!classConfig) { 132 | repository.set(target, (classConfig = {})); 133 | } 134 | 135 | if (!classConfig[methodName]) { 136 | classConfig[methodName] = createValidator(methodName); 137 | } 138 | 139 | return classConfig[methodName]; 140 | } 141 | 142 | export function param(paramName, validator) { 143 | return (target, methodName, descriptor) => { 144 | const methodConfig = getMethodConfig(target, methodName); 145 | const optional = /^\[\s*\w+?\s*\]$/.test(paramName); 146 | 147 | if (optional) { 148 | paramName = paramName.replace(/^\[|\]$/g, "").trim(); 149 | } 150 | 151 | methodConfig.parameters.unshift({ paramName, validator, optional }); 152 | methodConfig.methodBody = methodConfig.methodBody || descriptor.value; 153 | 154 | return methodConfig.descriptor; 155 | }; 156 | } 157 | 158 | export function returns(validator) { 159 | return (target, methodName, descriptor) => { 160 | const methodConfig = getMethodConfig(target, methodName); 161 | 162 | methodConfig.returnValue = validator; 163 | methodConfig.methodBody = methodConfig.methodBody || descriptor.value; 164 | 165 | return methodConfig.descriptor; 166 | }; 167 | } 168 | 169 | export function contract({ precondition, postcondition }) { 170 | return (target, methodName, descriptor) => { 171 | const methodConfig = getMethodConfig(target, methodName); 172 | 173 | methodConfig.precondition = precondition; 174 | methodConfig.postcondition = postcondition; 175 | methodConfig.methodBody = methodConfig.methodBody || descriptor.value; 176 | 177 | return methodConfig.descriptor; 178 | }; 179 | } 180 | -------------------------------------------------------------------------------- /test/ScriptProcessorNode.js: -------------------------------------------------------------------------------- 1 | describe("ScriptProcessorNode", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var audioContext; 4 | 5 | beforeEach(function() { 6 | audioContext = new WebAudioTestAPI.AudioContext(); 7 | }); 8 | 9 | describe("constructor()", function() { 10 | it("works", function() { 11 | var node = audioContext.createScriptProcessor(256, 1, 1); 12 | 13 | assert(node instanceof global.ScriptProcessorNode); 14 | assert(node instanceof global.AudioNode); 15 | 16 | assert.throws(function() { 17 | audioContext.createScriptProcessor(0, 1, 1); 18 | }, TypeError); 19 | 20 | assert.throws(function() { 21 | audioContext.createScriptProcessor(1024, 1.5, 1); 22 | }, TypeError); 23 | 24 | assert.throws(function() { 25 | audioContext.createScriptProcessor(1024, 1, 1.5); 26 | }, TypeError); 27 | 28 | assert.doesNotThrow(function() { 29 | audioContext.createScriptProcessor(1024); 30 | }); 31 | 32 | assert.throws(function() { 33 | audioContext.createScriptProcessor(1024, undefined); 34 | }, TypeError); 35 | 36 | assert.throws(function() { 37 | audioContext.createScriptProcessor(1024, 1, undefined); 38 | }, TypeError); 39 | }); 40 | it("not work when 'new' directly", function() { 41 | assert.throws(function() { new global.ScriptProcessorNode(256, 1, 1); }, TypeError); 42 | }); 43 | }); 44 | 45 | describe("#bufferSize: number", function() { 46 | it("works", function() { 47 | var node1 = audioContext.createScriptProcessor(1024, 0, 1); 48 | var node2 = audioContext.createScriptProcessor(2048, 0, 2); 49 | 50 | assert(node1.bufferSize === 1024); 51 | assert(node2.bufferSize === 2048); 52 | 53 | assert.throws(function() { 54 | node1.bufferSize = 2048; 55 | }, TypeError); 56 | }); 57 | }); 58 | 59 | describe("#onaudioprocess: function", function() { 60 | it("works", function() { 61 | var node = audioContext.createScriptProcessor(1024, 0, 1); 62 | 63 | function fn1() {} 64 | function fn2() {} 65 | 66 | assert(node.onaudioprocess === null); 67 | 68 | node.onaudioprocess = fn1; 69 | assert(node.onaudioprocess === fn1); 70 | 71 | node.onaudioprocess = fn2; 72 | assert(node.onaudioprocess === fn2); 73 | 74 | node.onaudioprocess = null; 75 | assert(node.onaudioprocess === null); 76 | 77 | assert.throws(function() { 78 | node.onaudioprocess = "INVALID"; 79 | }, TypeError); 80 | }); 81 | }); 82 | 83 | describe("#toJSON(): object", function() { 84 | it("works", function() { 85 | var node = audioContext.createScriptProcessor(1024, 0, 1); 86 | 87 | assert.deepEqual(node.toJSON(), { 88 | name: "ScriptProcessorNode", 89 | inputs: [] 90 | }); 91 | }); 92 | }); 93 | 94 | describe("$name: string", function() { 95 | it("works", function() { 96 | var node = audioContext.createScriptProcessor(256, 1, 1); 97 | 98 | assert(node.$name === "ScriptProcessorNode"); 99 | }); 100 | }); 101 | 102 | describe("$context: AudioContext", function() { 103 | it("works", function() { 104 | var node = audioContext.createScriptProcessor(256, 1, 1); 105 | 106 | assert(node.$context === audioContext); 107 | }); 108 | }); 109 | 110 | describe("works", function() { 111 | it("onaudioprocess", function() { 112 | // 256 / 44100 = 5.805msec -> 11.610msec -> 17.415msec 113 | var node = audioContext.createScriptProcessor(256, 1, 1); 114 | var onaudioprocess = sinon.spy(); 115 | var event; 116 | 117 | node.onaudioprocess = onaudioprocess; 118 | 119 | node.connect(audioContext.destination); 120 | 121 | audioContext.$processTo("00:00.000"); 122 | assert(onaudioprocess.callCount === 0, "00:00.000"); 123 | 124 | audioContext.$processTo("00:00.001"); 125 | assert(onaudioprocess.callCount === 1, "00:00.001"); 126 | assert(onaudioprocess.calledOn(node), "00:00.001"); 127 | event = onaudioprocess.args[0][0]; 128 | assert(audioContext.currentTime < event.playbackTime); 129 | 130 | audioContext.$processTo("00:00.005"); 131 | assert(onaudioprocess.callCount === 1, "00:00.005"); 132 | 133 | audioContext.$processTo("00:00.006"); 134 | assert(onaudioprocess.callCount === 2, "00:00.006"); 135 | assert(onaudioprocess.calledOn(node), "00:00.006"); 136 | event = onaudioprocess.args[1][0]; 137 | assert(audioContext.currentTime < event.playbackTime); 138 | 139 | audioContext.$processTo("00:00.011"); 140 | assert(onaudioprocess.callCount === 2, "00:00.011"); 141 | 142 | audioContext.$processTo("00:00.012"); 143 | assert(onaudioprocess.callCount === 3, "00:00.012"); 144 | assert(onaudioprocess.calledOn(node), "00:00.012"); 145 | event = onaudioprocess.args[2][0]; 146 | assert(audioContext.currentTime < event.playbackTime); 147 | 148 | audioContext.$processTo("00:00.017"); 149 | assert(onaudioprocess.callCount === 3, "00:00.017"); 150 | 151 | audioContext.$processTo("00:00.018"); 152 | assert(onaudioprocess.callCount === 4, "00:00.018"); 153 | assert(onaudioprocess.calledOn(node), "00:00.018"); 154 | event = onaudioprocess.args[3][0]; 155 | assert(audioContext.currentTime < event.playbackTime); 156 | 157 | event = onaudioprocess.args[0][0]; 158 | 159 | assert(event instanceof WebAudioTestAPI.AudioProcessingEvent); 160 | assert(event.inputBuffer instanceof WebAudioTestAPI.AudioBuffer); 161 | assert(event.outputBuffer instanceof WebAudioTestAPI.AudioBuffer); 162 | assert(event.type === "audioprocess"); 163 | assert(event.target === node); 164 | assert(typeof event.playbackTime === "number"); 165 | 166 | assert(onaudioprocess.args[0][0] !== onaudioprocess.args[1][0]); 167 | }); 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /test/utils/Configuration.js: -------------------------------------------------------------------------------- 1 | import assert from "power-assert"; 2 | import Configuration from "../../src/utils/Configuration"; 3 | 4 | describe("Configuration", () => { 5 | describe("constructor()", () => { 6 | it("works", () => { 7 | let configuration = new Configuration(); 8 | 9 | assert(configuration instanceof Configuration); 10 | }); 11 | }); 12 | describe(".getInstance(): Configuration", () => { 13 | it("works", () => { 14 | let configuration1 = Configuration.getInstance(); 15 | let configuration2 = Configuration.getInstance(); 16 | 17 | assert(configuration1 instanceof Configuration); 18 | assert(configuration2 instanceof Configuration); 19 | assert(configuration1 === configuration2); 20 | }); 21 | }); 22 | describe("#getState(name: string): string", () => { 23 | it("works", () => { 24 | let configuration = new Configuration(); 25 | 26 | assert(configuration.getState("AnalyserNode#getFloatTimeDomainData") === "disabled"); 27 | assert(configuration.getState("AudioBuffer#copyFromChannel") === "disabled"); 28 | assert(configuration.getState("AudioBuffer#copyToChannel") === "disabled"); 29 | assert(configuration.getState("AudioContext#createAudioWorker") === "disabled"); 30 | assert(configuration.getState("AudioContext#createStereoPanner") === "disabled"); 31 | assert(configuration.getState("AudioContext#decodeAudioData") === "void"); 32 | assert(configuration.getState("AudioContext#close") === "disabled"); 33 | assert(configuration.getState("AudioContext#resume") === "disabled"); 34 | assert(configuration.getState("AudioContext#suspend") === "disabled"); 35 | assert(configuration.getState("OfflineAudioContext#startRendering") === "void"); 36 | assert(configuration.getState("AudioNode#disconnect") === "channel"); 37 | 38 | assert.throws(() => { 39 | configuration.getState("AudioNode#valueOf"); 40 | }, (e) => { 41 | return e instanceof TypeError && /invalid state name/.test(e.message); 42 | }); 43 | }); 44 | }); 45 | describe("#setState(name: string, value: string): void", () => { 46 | it("works", () => { 47 | let configuration = new Configuration(); 48 | 49 | configuration.setState("AnalyserNode#getFloatTimeDomainData", "enabled"); 50 | configuration.setState("AudioBuffer#copyFromChannel", "enabled"); 51 | configuration.setState("AudioBuffer#copyToChannel", "enabled"); 52 | configuration.setState("AudioContext#createAudioWorker", "enabled"); 53 | configuration.setState("AudioContext#createStereoPanner", "enabled"); 54 | configuration.setState("AudioContext#decodeAudioData", "promise"); 55 | configuration.setState("AudioContext#close", "enabled"); 56 | configuration.setState("AudioContext#resume", "enabled"); 57 | configuration.setState("AudioContext#suspend", "enabled"); 58 | configuration.setState("OfflineAudioContext#startRendering", "promise"); 59 | configuration.setState("AudioNode#disconnect", "selective"); 60 | 61 | assert(configuration.getState("AnalyserNode#getFloatTimeDomainData") === "enabled"); 62 | assert(configuration.getState("AudioBuffer#copyFromChannel") === "enabled"); 63 | assert(configuration.getState("AudioBuffer#copyToChannel") === "enabled"); 64 | assert(configuration.getState("AudioContext#createAudioWorker") === "enabled"); 65 | assert(configuration.getState("AudioContext#createStereoPanner") === "enabled"); 66 | assert(configuration.getState("AudioContext#decodeAudioData") === "promise"); 67 | assert(configuration.getState("AudioContext#close") === "enabled"); 68 | assert(configuration.getState("AudioContext#resume") === "enabled"); 69 | assert(configuration.getState("AudioContext#suspend") === "enabled"); 70 | assert(configuration.getState("OfflineAudioContext#startRendering") === "promise"); 71 | assert(configuration.getState("AudioNode#disconnect") === "selective"); 72 | 73 | configuration.setState({ 74 | "AnalyserNode#getFloatTimeDomainData": "disabled", 75 | "AudioBuffer#copyFromChannel": "disabled", 76 | "AudioBuffer#copyToChannel": "disabled", 77 | "AudioContext#createAudioWorker": "disabled", 78 | "AudioContext#createStereoPanner": "disabled", 79 | "AudioContext#decodeAudioData": "void", 80 | "AudioContext#close": "disabled", 81 | "AudioContext#resume": "disabled", 82 | "AudioContext#suspend": "disabled", 83 | "OfflineAudioContext#startRendering": "void", 84 | "AudioNode#disconnect": "channel" 85 | }); 86 | 87 | assert(configuration.getState("AnalyserNode#getFloatTimeDomainData") === "disabled"); 88 | assert(configuration.getState("AudioBuffer#copyFromChannel") === "disabled"); 89 | assert(configuration.getState("AudioBuffer#copyToChannel") === "disabled"); 90 | assert(configuration.getState("AudioContext#createAudioWorker") === "disabled"); 91 | assert(configuration.getState("AudioContext#createStereoPanner") === "disabled"); 92 | assert(configuration.getState("AudioContext#decodeAudioData") === "void"); 93 | assert(configuration.getState("AudioContext#close") === "disabled"); 94 | assert(configuration.getState("AudioContext#resume") === "disabled"); 95 | assert(configuration.getState("AudioContext#suspend") === "disabled"); 96 | assert(configuration.getState("OfflineAudioContext#startRendering") === "void"); 97 | assert(configuration.getState("AudioNode#disconnect") === "channel"); 98 | 99 | assert.throws(() => { 100 | configuration.setState("AudioNode#valueOf", "enabled"); 101 | }, (e) => { 102 | return e instanceof TypeError && /invalid state name/.test(e.message); 103 | }); 104 | 105 | assert.throws(() => { 106 | configuration.setState("AudioNode#disconnect", "enabled"); 107 | }, (e) => { 108 | return e instanceof TypeError && /invalid state value/.test(e.message); 109 | }); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /test/AnalyserNode.js: -------------------------------------------------------------------------------- 1 | describe("AnalyserNode", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var audioContext; 4 | 5 | beforeEach(function() { 6 | audioContext = new WebAudioTestAPI.AudioContext(); 7 | }); 8 | 9 | describe("constructor()", function() { 10 | it("works", function() { 11 | var node = audioContext.createAnalyser(); 12 | 13 | assert(node instanceof global.AnalyserNode); 14 | assert(node instanceof global.AudioNode); 15 | }); 16 | it("not work when 'new' directly", function() { 17 | assert.throws(function() { new global.AnalyserNode(); }, TypeError); 18 | }); 19 | }); 20 | 21 | describe("#fftSize: number", function() { 22 | it("works", function() { 23 | var node = audioContext.createAnalyser(); 24 | 25 | assert(typeof node.fftSize === "number"); 26 | 27 | node.fftSize = 32; 28 | assert(node.fftSize === 32); 29 | 30 | node.fftSize = 64; 31 | assert(node.fftSize === 64); 32 | 33 | node.fftSize = 128; 34 | assert(node.fftSize === 128); 35 | 36 | node.fftSize = 256; 37 | assert(node.fftSize === 256); 38 | 39 | node.fftSize = 512; 40 | assert(node.fftSize === 512); 41 | 42 | node.fftSize = 512; 43 | assert(node.fftSize === 512); 44 | 45 | node.fftSize = 1024; 46 | assert(node.fftSize === 1024); 47 | 48 | node.fftSize = 2048; 49 | assert(node.fftSize === 2048); 50 | 51 | assert.throws(function() { 52 | node.fftSize = 4096; 53 | }, TypeError); 54 | }); 55 | }); 56 | 57 | describe("#frequencyBinCount: number", function() { 58 | it("works", function() { 59 | var node = audioContext.createAnalyser(); 60 | 61 | assert(typeof node.frequencyBinCount === "number"); 62 | 63 | node.fftSize = 2048; 64 | assert(node.frequencyBinCount === 1024); 65 | 66 | node.fftSize = 1024; 67 | assert(node.frequencyBinCount === 512); 68 | 69 | assert.throws(function() { 70 | node.frequencyBinCount = 256; 71 | }, TypeError); 72 | }); 73 | }); 74 | 75 | describe("#minDecibels: number", function() { 76 | it("works", function() { 77 | var node = audioContext.createAnalyser(); 78 | 79 | assert(typeof node.minDecibels === "number"); 80 | 81 | node.minDecibels = -50; 82 | assert(node.minDecibels === -50); 83 | 84 | node.minDecibels = -25; 85 | assert(node.minDecibels === -25); 86 | 87 | assert.throws(function() { 88 | node.minDecibels = "INVALID"; 89 | }, TypeError); 90 | }); 91 | }); 92 | 93 | describe("#maxDecibels: number", function() { 94 | it("works", function() { 95 | var node = audioContext.createAnalyser(); 96 | 97 | assert(typeof node.maxDecibels === "number"); 98 | 99 | node.maxDecibels = 15; 100 | assert(node.maxDecibels === 15); 101 | 102 | node.maxDecibels = 7.5; 103 | assert(node.maxDecibels === 7.5); 104 | 105 | assert.throws(function() { 106 | node.maxDecibels = "INVALID"; 107 | }, TypeError); 108 | }); 109 | }); 110 | 111 | describe("#smoothingTimeConstant: number", function() { 112 | it("works", function() { 113 | var node = audioContext.createAnalyser(); 114 | 115 | assert(typeof node.smoothingTimeConstant === "number"); 116 | 117 | node.smoothingTimeConstant = 0.4; 118 | assert(node.smoothingTimeConstant === 0.4); 119 | 120 | node.smoothingTimeConstant = 0.2; 121 | assert(node.smoothingTimeConstant === 0.2); 122 | 123 | assert.throws(function() { 124 | node.smoothingTimeConstant = "INVALID"; 125 | }, TypeError); 126 | }); 127 | }); 128 | 129 | describe("#getFloatFrequencyData(array: Float32Array): void", function() { 130 | it("works", function() { 131 | var node = audioContext.createAnalyser(); 132 | var f32 = new Float32Array(128); 133 | var i16 = new Int16Array(128); 134 | 135 | node.getFloatFrequencyData(f32); 136 | 137 | assert.throws(function() { 138 | node.getFloatFrequencyData(i16); 139 | }, TypeError); 140 | }); 141 | }); 142 | 143 | describe("#getByteFrequencyData(array: Uint8Array): void", function() { 144 | it("works", function() { 145 | var node = audioContext.createAnalyser(); 146 | var ui8 = new Uint8Array(128); 147 | var i16 = new Int16Array(128); 148 | 149 | node.getByteFrequencyData(ui8); 150 | 151 | assert.throws(function() { 152 | node.getByteFrequencyData(i16); 153 | }, TypeError); 154 | }); 155 | }); 156 | 157 | describe("#getFloatTimeDomainData", function() { 158 | it("(array: Float32Array): void", function() { 159 | var node = audioContext.createAnalyser(); 160 | var f32 = new Float32Array(128); 161 | var i16 = new Int16Array(128); 162 | 163 | assert.throws(function() { 164 | node.getFloatTimeDomainData(f32); 165 | }, TypeError); 166 | 167 | WebAudioTestAPI.setState("AnalyserNode#getFloatTimeDomainData", "enabled"); 168 | 169 | assert.doesNotThrow(function() { 170 | node.getFloatTimeDomainData(f32); 171 | }); 172 | 173 | assert.throws(function() { 174 | node.getFloatTimeDomainData(i16); 175 | }, TypeError); 176 | 177 | WebAudioTestAPI.setState("AnalyserNode#getFloatTimeDomainData", "disabled"); 178 | }); 179 | }); 180 | 181 | describe("#getByteTimeDomainData", function() { 182 | it("(array: Uint8Array): void", function() { 183 | var node = audioContext.createAnalyser(); 184 | var ui8 = new Uint8Array(128); 185 | var i16 = new Int16Array(128); 186 | 187 | node.getByteTimeDomainData(ui8); 188 | 189 | assert.throws(function() { 190 | node.getByteTimeDomainData(i16); 191 | }, TypeError); 192 | }); 193 | }); 194 | 195 | describe("#toJSON", function() { 196 | it("(): object", function() { 197 | var node = audioContext.createAnalyser(); 198 | 199 | assert.deepEqual(node.toJSON(), { 200 | name: "AnalyserNode", 201 | fftSize: 2048, 202 | minDecibels: -100, 203 | maxDecibels: 30, 204 | smoothingTimeConstant: 0.8, 205 | inputs: [] 206 | }); 207 | }); 208 | }); 209 | 210 | describe("$name", function() { 211 | it("get: string", function() { 212 | var node = audioContext.createAnalyser(); 213 | 214 | assert(node.$name === "AnalyserNode"); 215 | }); 216 | }); 217 | 218 | describe("$context", function() { 219 | it("get: AudioContext", function() { 220 | var node = audioContext.createAnalyser(); 221 | 222 | assert(node.$context === audioContext); 223 | }); 224 | }); 225 | }); 226 | -------------------------------------------------------------------------------- /src/WebAudioTestAPI.js: -------------------------------------------------------------------------------- 1 | import Configuration from "./utils/Configuration"; 2 | import Immigration from "./utils/Immigration"; 3 | import WebAudioAPI from "./WebAudioAPI"; 4 | import Element from "./dom/Element"; 5 | import Event from "./dom/Event"; 6 | import EventTarget from "./dom/EventTarget"; 7 | import HTMLElement from "./dom/HTMLElement"; 8 | import HTMLMediaElement from "./dom/HTMLMediaElement"; 9 | import MediaStream from "./dom/MediaStream"; 10 | import AnalyserNode from "./AnalyserNode"; 11 | import AudioBuffer from "./AudioBuffer"; 12 | import AudioBufferSourceNode from "./AudioBufferSourceNode"; 13 | import AudioContext from "./AudioContext"; 14 | import AudioDestinationNode from "./AudioDestinationNode"; 15 | import AudioListener from "./AudioListener"; 16 | import AudioNode from "./AudioNode"; 17 | import AudioParam from "./AudioParam"; 18 | import AudioProcessingEvent from "./AudioProcessingEvent"; 19 | import BiquadFilterNode from "./BiquadFilterNode"; 20 | import ChannelMergerNode from "./ChannelMergerNode"; 21 | import ChannelSplitterNode from "./ChannelSplitterNode"; 22 | import ConvolverNode from "./ConvolverNode"; 23 | import DelayNode from "./DelayNode"; 24 | import DynamicsCompressorNode from "./DynamicsCompressorNode"; 25 | import GainNode from "./GainNode"; 26 | import MediaElementAudioSourceNode from "./MediaElementAudioSourceNode"; 27 | import MediaStreamAudioDestinationNode from "./MediaStreamAudioDestinationNode"; 28 | import MediaStreamAudioSourceNode from "./MediaStreamAudioSourceNode"; 29 | import OfflineAudioCompletionEvent from "./OfflineAudioCompletionEvent"; 30 | import OfflineAudioContext from "./OfflineAudioContext"; 31 | import OscillatorNode from "./OscillatorNode"; 32 | import PannerNode from "./PannerNode"; 33 | import PeriodicWave from "./PeriodicWave"; 34 | import ScriptProcessorNode from "./ScriptProcessorNode"; 35 | import StereoPannerNode from "./StereoPannerNode"; 36 | import WaveShaperNode from "./WaveShaperNode"; 37 | import getAPIVersion from "./utils/getAPIVersion"; 38 | 39 | let sampleRate = 44100; 40 | let configuration = Configuration.getInstance(); 41 | 42 | let WebAudioTestAPI = { 43 | VERSION: getAPIVersion(), 44 | utils: { Configuration, Immigration }, 45 | sampleRate, 46 | AnalyserNode, 47 | AudioBuffer, 48 | AudioBufferSourceNode, 49 | AudioContext, 50 | AudioDestinationNode, 51 | AudioListener, 52 | AudioNode, 53 | AudioParam, 54 | AudioProcessingEvent, 55 | BiquadFilterNode, 56 | ChannelMergerNode, 57 | ChannelSplitterNode, 58 | ConvolverNode, 59 | DelayNode, 60 | DynamicsCompressorNode, 61 | Element, 62 | Event, 63 | EventTarget, 64 | GainNode, 65 | HTMLElement, 66 | HTMLMediaElement, 67 | MediaElementAudioSourceNode, 68 | MediaStream, 69 | MediaStreamAudioDestinationNode, 70 | MediaStreamAudioSourceNode, 71 | OfflineAudioCompletionEvent, 72 | OfflineAudioContext, 73 | OscillatorNode, 74 | PannerNode, 75 | PeriodicWave, 76 | ScriptProcessorNode, 77 | StereoPannerNode, 78 | WaveShaperNode, 79 | getState(name) { 80 | return configuration.getState(name); 81 | }, 82 | setState(name, value) { 83 | configuration.setState(name, value); 84 | }, 85 | use() { 86 | global.AnalyserNode = WebAudioTestAPI.AnalyserNode; 87 | global.AudioBuffer = WebAudioTestAPI.AudioBuffer; 88 | global.AudioBufferSourceNode = WebAudioTestAPI.AudioBufferSourceNode; 89 | global.AudioContext = WebAudioTestAPI.AudioContext; 90 | global.AudioDestinationNode = WebAudioTestAPI.AudioDestinationNode; 91 | global.AudioListener = WebAudioTestAPI.AudioListener; 92 | global.AudioNode = WebAudioTestAPI.AudioNode; 93 | global.AudioParam = WebAudioTestAPI.AudioParam; 94 | global.AudioProcessingEvent = WebAudioTestAPI.AudioProcessingEvent; 95 | global.BiquadFilterNode = WebAudioTestAPI.BiquadFilterNode; 96 | global.ChannelMergerNode = WebAudioTestAPI.ChannelMergerNode; 97 | global.ChannelSplitterNode = WebAudioTestAPI.ChannelSplitterNode; 98 | global.ConvolverNode = WebAudioTestAPI.ConvolverNode; 99 | global.DelayNode = WebAudioTestAPI.DelayNode; 100 | global.DynamicsCompressorNode = WebAudioTestAPI.DynamicsCompressorNode; 101 | global.GainNode = WebAudioTestAPI.GainNode; 102 | global.MediaElementAudioSourceNode = WebAudioTestAPI.MediaElementAudioSourceNode; 103 | global.MediaStreamAudioDestinationNode = WebAudioTestAPI.MediaStreamAudioDestinationNode; 104 | global.MediaStreamAudioSourceNode = WebAudioTestAPI.MediaStreamAudioSourceNode; 105 | global.OfflineAudioCompletionEvent = WebAudioTestAPI.OfflineAudioCompletionEvent; 106 | global.OfflineAudioContext = WebAudioTestAPI.OfflineAudioContext; 107 | global.OscillatorNode = WebAudioTestAPI.OscillatorNode; 108 | global.PannerNode = WebAudioTestAPI.PannerNode; 109 | global.PeriodicWave = WebAudioTestAPI.PeriodicWave; 110 | global.ScriptProcessorNode = WebAudioTestAPI.ScriptProcessorNode; 111 | global.StereoPannerNode = WebAudioTestAPI.StereoPannerNode; 112 | global.WaveShaperNode = WebAudioTestAPI.WaveShaperNode; 113 | global.WebAudioTestAPI = WebAudioTestAPI; 114 | }, 115 | unuse() { 116 | global.AnalyserNode = WebAudioAPI.AnalyserNode; 117 | global.AudioBuffer = WebAudioAPI.AudioBuffer; 118 | global.AudioBufferSourceNode = WebAudioAPI.AudioBufferSourceNode; 119 | global.AudioContext = WebAudioAPI.AudioContext; 120 | global.AudioDestinationNode = WebAudioAPI.AudioDestinationNode; 121 | global.AudioListener = WebAudioAPI.AudioListener; 122 | global.AudioNode = WebAudioAPI.AudioNode; 123 | global.AudioParam = WebAudioAPI.AudioParam; 124 | global.AudioProcessingEvent = WebAudioAPI.AudioProcessingEvent; 125 | global.BiquadFilterNode = WebAudioAPI.BiquadFilterNode; 126 | global.ChannelMergerNode = WebAudioAPI.ChannelMergerNode; 127 | global.ChannelSplitterNode = WebAudioAPI.ChannelSplitterNode; 128 | global.ConvolverNode = WebAudioAPI.ConvolverNode; 129 | global.DelayNode = WebAudioAPI.DelayNode; 130 | global.DynamicsCompressorNode = WebAudioAPI.DynamicsCompressorNode; 131 | global.GainNode = WebAudioAPI.GainNode; 132 | global.MediaElementAudioSourceNode = WebAudioAPI.MediaElementAudioSourceNode; 133 | global.MediaStreamAudioDestinationNode = WebAudioAPI.MediaStreamAudioDestinationNode; 134 | global.MediaStreamAudioSourceNode = WebAudioAPI.MediaStreamAudioSourceNode; 135 | global.OfflineAudioCompletionEvent = WebAudioAPI.OfflineAudioCompletionEvent; 136 | global.OfflineAudioContext = WebAudioAPI.OfflineAudioContext; 137 | global.OscillatorNode = WebAudioAPI.OscillatorNode; 138 | global.PannerNode = WebAudioAPI.PannerNode; 139 | global.PeriodicWave = WebAudioAPI.PeriodicWave; 140 | global.ScriptProcessorNode = WebAudioAPI.ScriptProcessorNode; 141 | global.StereoPannerNode = WebAudioAPI.StereoPannerNode; 142 | global.WaveShaperNode = WebAudioAPI.WaveShaperNode; 143 | } 144 | }; 145 | 146 | export default WebAudioTestAPI; 147 | -------------------------------------------------------------------------------- /src/AudioParam.js: -------------------------------------------------------------------------------- 1 | import Immigration from "./utils/Immigration"; 2 | import Junction from "./utils/Junction"; 3 | import defaults from "./utils/defaults"; 4 | import toJSON from "./utils/toJSON"; 5 | import toSeconds from "./utils/toSeconds"; 6 | import * as props from "./decorators/props"; 7 | import * as methods from "./decorators/methods"; 8 | import * as validators from "./validators"; 9 | 10 | let immigration = Immigration.getInstance(); 11 | 12 | export default class AudioParam { 13 | constructor(admission, node, name, defaultValue) { 14 | immigration.check(admission, () => { 15 | throw new TypeError("Illegal constructor"); 16 | }); 17 | Object.defineProperty(this, "_", { value: {} }); 18 | 19 | this._.value = defaultValue; 20 | this._.name = name; 21 | this._.defaultValue = defaultValue; 22 | this._.context = node.context; 23 | this._.node = node; 24 | this._.inputs = [ new Junction(this, 0) ]; 25 | this._.events = []; 26 | this._.tick = -1; 27 | } 28 | 29 | @props.typed(validators.isNumber, 0) 30 | get value() { 31 | this._.value = this.$valueAtTime(this.$context.currentTime); 32 | return this._.value; 33 | } 34 | 35 | @props.readonly() 36 | name() { 37 | return this._.name; 38 | } 39 | 40 | @props.readonly() 41 | defaultValue() { 42 | return this._.defaultValue; 43 | } 44 | 45 | @methods.param("value", validators.isNumber) 46 | @methods.param("startTime", validators.isNumber) 47 | setValueAtTime(value, startTime) { 48 | this.__insertEvent({ type: "SetValue", value: value, time: startTime }); 49 | } 50 | 51 | @methods.param("value", validators.isNumber) 52 | @methods.param("endTime", validators.isNumber) 53 | linearRampToValueAtTime(value, endTime) { 54 | this.__insertEvent({ type: "LinearRampToValue", value: value, time: endTime }); 55 | } 56 | 57 | @methods.param("value", validators.isNumber) 58 | @methods.param("endTime", validators.isNumber) 59 | exponentialRampToValueAtTime(value, endTime) { 60 | this.__insertEvent({ type: "ExponentialRampToValue", value: value, time: endTime }); 61 | } 62 | 63 | @methods.param("value", validators.isNumber) 64 | @methods.param("endTime", validators.isNumber) 65 | @methods.param("timeConstant", validators.isNumber) 66 | setTargetAtTime(target, startTime, timeConstant) { 67 | this.__insertEvent({ type: "SetTarget", value: target, time: startTime, timeConstant: timeConstant }); 68 | } 69 | 70 | @methods.param("values", validators.isInstanceOf(Float32Array)) 71 | @methods.param("startTime", validators.isNumber) 72 | @methods.param("duration", validators.isNumber) 73 | setValueCurveAtTime(values, startTime, duration) { 74 | this.__insertEvent({ type: "SetValueCurve", time: startTime, duration: duration, curve: values }); 75 | } 76 | 77 | @methods.param("startTime", validators.isNumber) 78 | cancelScheduledValues(startTime) { 79 | let events = this.$events; 80 | 81 | for (let i = 0, imax = events.length; i < imax; ++i) { 82 | if (events[i].time >= startTime) { 83 | return events.splice(i); 84 | } 85 | } 86 | } 87 | 88 | toJSON(memo) { 89 | return toJSON(this, (node, memo) => { 90 | let json = {}; 91 | 92 | json.value = node.value; 93 | json.inputs = node.$inputs[0].toJSON(memo); 94 | 95 | return json; 96 | }, memo); 97 | } 98 | 99 | get $name() { 100 | return "AudioParam"; 101 | } 102 | 103 | get $context() { 104 | return this._.context; 105 | } 106 | 107 | get $node() { 108 | return this._.node; 109 | } 110 | 111 | get $inputs() { 112 | return this._.inputs; 113 | } 114 | 115 | get $events() { 116 | return this._.events; 117 | } 118 | 119 | $valueAtTime(when) { 120 | let time = toSeconds(when); 121 | let value = this._.value; 122 | let events = this.$events; 123 | let t0; 124 | 125 | for (let i = 0; i < events.length; i++) { 126 | let e0 = events[i]; 127 | let e1 = events[i + 1]; 128 | 129 | if (time < e0.time) { 130 | break; 131 | } 132 | t0 = Math.min(time, e1 ? e1.time : time); 133 | 134 | if (e1 && e1.type === "LinearRampToValue") { 135 | value = AudioParam.$linearRampToValueAtTime(value, e0.value, e1.value, t0, e0.time, e1.time); 136 | } else if (e1 && e1.type === "ExponentialRampToValue") { 137 | value = AudioParam.$exponentialRampToValueAtTime(value, e0.value, e1.value, t0, e0.time, e1.time); 138 | } else { 139 | switch (e0.type) { 140 | case "SetValue": 141 | case "LinearRampToValue": 142 | case "ExponentialRampToValue": 143 | value = e0.value; 144 | break; 145 | case "SetTarget": 146 | value = AudioParam.$setTargetAtTime(value, e0.value, t0, e0.time, e0.timeConstant); 147 | break; 148 | case "SetValueCurve": 149 | value = AudioParam.$setValueCurveAtTime(value, t0, e0.time, e0.time + e0.duration, e0.curve); 150 | break; 151 | default: 152 | // no default 153 | } 154 | } 155 | } 156 | 157 | return value; 158 | } 159 | 160 | $process(inNumSamples, tick) { 161 | if (this._.tick !== tick) { 162 | this._.tick = tick; 163 | this.$inputs[0].process(inNumSamples, tick); 164 | } 165 | } 166 | 167 | $isConnectedFrom(destination, output = 0) { 168 | if (!(destination instanceof global.AudioNode)) { 169 | return false; 170 | } 171 | 172 | let outputJunction = destination._.outputs[output]; 173 | let inputJunction = this._.inputs[0]; 174 | 175 | if (!outputJunction || !inputJunction) { 176 | return false; 177 | } 178 | 179 | return inputJunction.inputs.some(junction => junction === outputJunction); 180 | } 181 | 182 | __insertEvent(event) { 183 | let time = event.time; 184 | let events = this.$events; 185 | let replace = 0; 186 | let i, imax = events.length; 187 | 188 | for (i = 0; i < imax; ++i) { 189 | if (events[i].time === time && events[i].type === event.type) { 190 | replace = 1; 191 | break; 192 | } 193 | 194 | if (events[i].time > time) { 195 | break; 196 | } 197 | } 198 | 199 | events.splice(i, replace, event); 200 | } 201 | 202 | static $linearRampToValueAtTime(v, v0, v1, t, t0, t1) { 203 | if (t <= t0) { 204 | return v0; 205 | } 206 | if (t1 <= t) { 207 | return v1; 208 | } 209 | let dt = (t - t0) / (t1 - t0); 210 | 211 | return (1 - dt) * v0 + dt * v1; 212 | } 213 | 214 | static $exponentialRampToValueAtTime(v, v0, v1, t, t0, t1) { 215 | if (t <= t0) { 216 | return v0; 217 | } 218 | if (t1 <= t) { 219 | return v1; 220 | } 221 | if (v0 === v1) { 222 | return v0; 223 | } 224 | 225 | let dt = (t - t0) / (t1 - t0); 226 | 227 | if ((0 < v0 && 0 < v1) || (v0 < 0 && v1 < 0)) { 228 | return v0 * Math.pow(v1 / v0, dt); 229 | } 230 | 231 | return v; 232 | } 233 | 234 | static $setTargetAtTime(v0, v1, t, t0, tau) { 235 | if (t <= t0) { 236 | return v0; 237 | } 238 | return v1 + (v0 - v1) * Math.exp((t0 - t) / tau); 239 | } 240 | 241 | static $setValueCurveAtTime(v, t, t0, t1, curve) { 242 | let dt = (t - t0) / (t1 - t0); 243 | 244 | if (dt <= 0) { 245 | return defaults(curve[0], v); 246 | } 247 | 248 | if (1 <= dt) { 249 | return defaults(curve[curve.length - 1], v); 250 | } 251 | 252 | return defaults(curve[(curve.length * dt)|0], v); 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /test/PannerNode.js: -------------------------------------------------------------------------------- 1 | describe("PannerNode", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var audioContext; 4 | 5 | beforeEach(function() { 6 | audioContext = new WebAudioTestAPI.AudioContext(); 7 | }); 8 | 9 | describe("constructor()", function() { 10 | it("works", function() { 11 | var node = audioContext.createPanner(); 12 | 13 | assert(node instanceof global.PannerNode); 14 | assert(node instanceof global.AudioNode); 15 | }); 16 | it("not work when 'new' directly", function() { 17 | assert.throws(function() { new global.PannerNode(); }, TypeError); 18 | }); 19 | }); 20 | 21 | describe("#panningModel: string", function() { 22 | it("works", function() { 23 | var node = audioContext.createPanner(); 24 | 25 | assert(typeof node.panningModel === "string"); 26 | 27 | node.panningModel = "equalpower"; 28 | assert(node.panningModel === "equalpower"); 29 | 30 | node.panningModel = "HRTF"; 31 | assert(node.panningModel === "HRTF"); 32 | 33 | assert.throws(function() { 34 | node.panningModel = "custom"; 35 | }, TypeError); 36 | }); 37 | }); 38 | 39 | describe("#distanceModel: string", function() { 40 | it("works", function() { 41 | var node = audioContext.createPanner(); 42 | 43 | assert(typeof node.distanceModel === "string"); 44 | 45 | node.distanceModel = "linear"; 46 | assert(node.distanceModel === "linear"); 47 | 48 | node.distanceModel = "inverse"; 49 | assert(node.distanceModel === "inverse"); 50 | 51 | node.distanceModel = "exponential"; 52 | assert(node.distanceModel === "exponential"); 53 | 54 | assert.throws(function() { 55 | node.distanceModel = "custom"; 56 | }, TypeError); 57 | }); 58 | }); 59 | 60 | describe("#refDistance: number", function() { 61 | it("works", function() { 62 | var node = audioContext.createPanner(); 63 | 64 | assert(typeof node.refDistance === "number"); 65 | 66 | node.refDistance = 0.5; 67 | assert(node.refDistance === 0.5); 68 | 69 | node.refDistance = 0.25; 70 | assert(node.refDistance === 0.25); 71 | 72 | assert.throws(function() { 73 | node.refDistance = "INVALID"; 74 | }, TypeError); 75 | }); 76 | }); 77 | 78 | describe("#maxDistance: number", function() { 79 | it("works", function() { 80 | var node = audioContext.createPanner(); 81 | 82 | assert(typeof node.maxDistance === "number"); 83 | 84 | node.maxDistance = 5000; 85 | assert(node.maxDistance === 5000); 86 | 87 | node.maxDistance = 2500; 88 | assert(node.maxDistance === 2500); 89 | 90 | assert.throws(function() { 91 | node.maxDistance = "INVALID"; 92 | }, TypeError); 93 | }); 94 | }); 95 | 96 | describe("#rolloffFactor: number", function() { 97 | it("works", function() { 98 | var node = audioContext.createPanner(); 99 | 100 | assert(typeof node.rolloffFactor === "number"); 101 | 102 | node.rolloffFactor = 0.5; 103 | assert(node.rolloffFactor === 0.5); 104 | 105 | node.rolloffFactor = 0.25; 106 | assert(node.rolloffFactor === 0.25); 107 | 108 | assert.throws(function() { 109 | node.rolloffFactor = "INVALID"; 110 | }, TypeError); 111 | }); 112 | }); 113 | 114 | describe("#coneInnerAngle: number", function() { 115 | it("works", function() { 116 | var node = audioContext.createPanner(); 117 | 118 | assert(typeof node.coneInnerAngle === "number"); 119 | 120 | node.coneInnerAngle = 180; 121 | assert(node.coneInnerAngle === 180); 122 | 123 | node.coneInnerAngle = 90; 124 | assert(node.coneInnerAngle === 90); 125 | 126 | assert.throws(function() { 127 | node.coneInnerAngle = "INVALID"; 128 | }, TypeError); 129 | }); 130 | }); 131 | 132 | describe("#coneOuterAngle: number", function() { 133 | it("works", function() { 134 | var node = audioContext.createPanner(); 135 | 136 | assert(typeof node.coneOuterAngle === "number"); 137 | 138 | node.coneOuterAngle = 180; 139 | assert(node.coneOuterAngle === 180); 140 | 141 | node.coneOuterAngle = 90; 142 | assert(node.coneOuterAngle === 90); 143 | 144 | assert.throws(function() { 145 | node.coneOuterAngle = "INVALID"; 146 | }, TypeError); 147 | }); 148 | }); 149 | 150 | describe("#coneOuterGain: number", function() { 151 | it("works", function() { 152 | var node = audioContext.createPanner(); 153 | 154 | assert(typeof node.coneOuterGain === "number"); 155 | 156 | node.coneOuterGain = 1; 157 | assert(node.coneOuterGain === 1); 158 | 159 | node.coneOuterGain = 2; 160 | assert(node.coneOuterGain === 2); 161 | 162 | assert.throws(function() { 163 | node.coneOuterGain = "INVALID"; 164 | }, TypeError); 165 | }); 166 | }); 167 | 168 | describe("#setPosition(x: number, y: number, z: number): void", function() { 169 | it("works", function() { 170 | var node = audioContext.createPanner(); 171 | 172 | node.setPosition(0, 0, 0); 173 | 174 | assert.throws(function() { 175 | node.setPosition("INVALID", 0, 0); 176 | }, TypeError); 177 | 178 | assert.throws(function() { 179 | node.setPosition(0, "INVALID", 0); 180 | }, TypeError); 181 | 182 | assert.throws(function() { 183 | node.setPosition(0, 0, "INVALID"); 184 | }, TypeError); 185 | }); 186 | }); 187 | 188 | describe("#setOrientation(x: number, y: number, z: number): void", function() { 189 | it("works", function() { 190 | var node = audioContext.createPanner(); 191 | 192 | node.setOrientation(0, 0, 0); 193 | 194 | assert.throws(function() { 195 | node.setOrientation("INVALID", 0, 0); 196 | }, TypeError); 197 | 198 | assert.throws(function() { 199 | node.setOrientation(0, "INVALID", 0); 200 | }, TypeError); 201 | 202 | assert.throws(function() { 203 | node.setOrientation(0, 0, "INVALID"); 204 | }, TypeError); 205 | }); 206 | }); 207 | 208 | describe("#setVelocity(x: number, y: number, z: number): void", function() { 209 | it("works", function() { 210 | var node = audioContext.createPanner(); 211 | 212 | node.setVelocity(0, 0, 0); 213 | 214 | assert.throws(function() { 215 | node.setVelocity("INVALID", 0, 0); 216 | }, TypeError); 217 | 218 | assert.throws(function() { 219 | node.setVelocity(0, "INVALID", 0); 220 | }, TypeError); 221 | 222 | assert.throws(function() { 223 | node.setVelocity(0, 0, "INVALID"); 224 | }, TypeError); 225 | }); 226 | }); 227 | 228 | describe("#toJSON(): object", function() { 229 | it("works", function() { 230 | var node = audioContext.createPanner(); 231 | 232 | assert.deepEqual(node.toJSON(), { 233 | name: "PannerNode", 234 | panningModel: "HRTF", 235 | distanceModel: "inverse", 236 | refDistance: 1, 237 | maxDistance: 10000, 238 | rolloffFactor: 1, 239 | coneInnerAngle: 360, 240 | coneOuterAngle: 360, 241 | coneOuterGain: 0, 242 | inputs: [] 243 | }); 244 | }); 245 | }); 246 | 247 | describe("$name: string", function() { 248 | it("works", function() { 249 | var node = audioContext.createPanner(); 250 | 251 | assert(node.$name === "PannerNode"); 252 | }); 253 | }); 254 | 255 | describe("$context: AudioContext", function() { 256 | it("works", function() { 257 | var node = audioContext.createPanner(); 258 | 259 | assert(node.$context === audioContext); 260 | }); 261 | }); 262 | }); 263 | -------------------------------------------------------------------------------- /test/OscillatorNode.js: -------------------------------------------------------------------------------- 1 | describe("OscillatorNode", function() { 2 | var WebAudioTestAPI = global.WebAudioTestAPI; 3 | var audioContext; 4 | 5 | beforeEach(function() { 6 | audioContext = new WebAudioTestAPI.AudioContext(); 7 | }); 8 | 9 | describe("constructor()", function() { 10 | it("works", function() { 11 | var node = audioContext.createOscillator(); 12 | 13 | assert(node instanceof global.OscillatorNode); 14 | assert(node instanceof global.AudioNode); 15 | }); 16 | it("not work when 'new' directly", function() { 17 | assert.throws(function() { new global.OscillatorNode(); }, TypeError); 18 | }); 19 | }); 20 | 21 | describe("#type: string", function() { 22 | it("works", function() { 23 | var node = audioContext.createOscillator(); 24 | 25 | assert(typeof node.type === "string"); 26 | 27 | node.type = "sine"; 28 | assert(node.type === "sine"); 29 | 30 | node.type = "square"; 31 | assert(node.type === "square"); 32 | 33 | node.type = "sawtooth"; 34 | assert(node.type === "sawtooth"); 35 | 36 | node.type = "triangle"; 37 | assert(node.type === "triangle"); 38 | 39 | assert.throws(function() { 40 | node.type = "custom"; 41 | }, TypeError); 42 | }); 43 | }); 44 | 45 | describe("#frequency: AudioParam", function() { 46 | it("works", function() { 47 | var node = audioContext.createOscillator(); 48 | 49 | assert(node.frequency instanceof WebAudioTestAPI.AudioParam); 50 | 51 | assert.throws(function() { 52 | node.frequency = 0; 53 | }, TypeError); 54 | }); 55 | }); 56 | 57 | describe("#detune: AudioParam", function() { 58 | it("works", function() { 59 | var node = audioContext.createOscillator(); 60 | 61 | assert(node.detune instanceof WebAudioTestAPI.AudioParam); 62 | 63 | assert.throws(function() { 64 | node.detune = 0; 65 | }, TypeError); 66 | }); 67 | }); 68 | 69 | describe("#onended: function", function() { 70 | it("works", function() { 71 | var node = audioContext.createOscillator(); 72 | 73 | function fn1() {} 74 | function fn2() {} 75 | 76 | assert(node.onended === null); 77 | 78 | node.onended = fn1; 79 | assert(node.onended === fn1); 80 | 81 | node.onended = fn2; 82 | assert(node.onended === fn2); 83 | 84 | node.onended = null; 85 | assert(node.onended === null); 86 | 87 | assert.throws(function() { 88 | node.onended = "INVALID"; 89 | }, TypeError); 90 | }); 91 | }); 92 | 93 | describe("#start([ when: number ]): void", function() { 94 | it("works", function() { 95 | var node = audioContext.createOscillator(); 96 | 97 | node.start(); 98 | 99 | assert.throws(function() { 100 | node.start(); 101 | }, Error, "call twice"); 102 | }); 103 | it("works with when", function() { 104 | var node = audioContext.createOscillator(); 105 | 106 | assert.throws(function() { 107 | node.start(-0.5); 108 | }, TypeError); 109 | 110 | assert.throws(function() { 111 | node.start(undefined); 112 | }, TypeError); 113 | 114 | node.start(0); 115 | 116 | assert.throws(function() { 117 | node.start(0); 118 | }, Error, "call twice"); 119 | }); 120 | }); 121 | 122 | describe("#stop([ when: number ]): void", function() { 123 | it("works", function() { 124 | var node = audioContext.createOscillator(); 125 | 126 | assert.throws(function() { 127 | node.stop(); 128 | }, Error, "not start yet"); 129 | 130 | node.start(); 131 | 132 | assert.throws(function() { 133 | node.stop(-0.5); 134 | }, TypeError); 135 | 136 | assert.throws(function() { 137 | node.stop(undefined); 138 | }, TypeError); 139 | 140 | node.stop(); 141 | 142 | assert.throws(function() { 143 | node.stop(); 144 | }, Error, "call twice"); 145 | 146 | assert.throws(function() { 147 | node.start(); 148 | }, Error); 149 | }); 150 | it("works with", function() { 151 | var node = audioContext.createOscillator(); 152 | 153 | assert.throws(function() { 154 | node.stop(0); 155 | }, Error, "not start yet"); 156 | 157 | node.start(0); 158 | 159 | assert.throws(function() { 160 | node.stop(-0.5); 161 | }, TypeError); 162 | 163 | node.stop(0); 164 | 165 | assert.throws(function() { 166 | node.stop(0); 167 | }, Error, "call twice"); 168 | 169 | assert.throws(function() { 170 | node.start(0); 171 | }, Error); 172 | }); 173 | }); 174 | 175 | describe("#setPeriodicWave(periodicWave: PeriodicWave): void", function() { 176 | it("works", function() { 177 | var node = audioContext.createOscillator(); 178 | var periodicWave = audioContext.createPeriodicWave( 179 | new Float32Array(128), new Float32Array(128) 180 | ); 181 | 182 | node.setPeriodicWave(periodicWave); 183 | 184 | assert(node.type === "custom"); 185 | assert(node.$custom === periodicWave); 186 | 187 | assert.throws(function() { 188 | node.setPeriodicWave("INVALID"); 189 | }, TypeError); 190 | }); 191 | }); 192 | 193 | describe("#toJSON", function() { 194 | it("(): object", function() { 195 | var node = audioContext.createOscillator(); 196 | 197 | assert.deepEqual(node.toJSON(), { 198 | name: "OscillatorNode", 199 | type: "sine", 200 | frequency: { 201 | value: 440, 202 | inputs: [] 203 | }, 204 | detune: { 205 | value: 0, 206 | inputs: [] 207 | }, 208 | inputs: [] 209 | }); 210 | }); 211 | }); 212 | 213 | describe("$name: string", function() { 214 | it("works", function() { 215 | var node = audioContext.createOscillator(); 216 | 217 | assert(node.$name === "OscillatorNode"); 218 | }); 219 | }); 220 | 221 | describe("$context: AudioContext", function() { 222 | it("works", function() { 223 | var node = audioContext.createOscillator(); 224 | 225 | assert(node.$context === audioContext); 226 | }); 227 | }); 228 | 229 | describe("$state: string", function() { 230 | it("works", function() { 231 | var node = audioContext.createOscillator(); 232 | 233 | assert(node.$state === "UNSCHEDULED"); 234 | 235 | node.start(0); 236 | 237 | assert(node.$state === "PLAYING"); 238 | }); 239 | }); 240 | 241 | describe("$stateAtTime(time: number|string): string", function() { 242 | it("works", function() { 243 | var node = audioContext.createOscillator(); 244 | 245 | assert(node.$stateAtTime("00:00.000") === "UNSCHEDULED"); 246 | 247 | node.start(0); 248 | 249 | assert(node.$stateAtTime("00:00.000") === "PLAYING"); 250 | }); 251 | }); 252 | 253 | describe("$startTime: number", function() { 254 | it("works", function() { 255 | var node = audioContext.createOscillator(); 256 | 257 | assert(node.$startTime === Infinity); 258 | 259 | node.start(2); 260 | 261 | assert(node.$startTime === 2); 262 | }); 263 | }); 264 | 265 | describe("$stopTime: number", function() { 266 | it("works", function() { 267 | var node = audioContext.createOscillator(); 268 | 269 | assert(node.$stopTime === Infinity); 270 | 271 | node.start(2); 272 | node.stop(3); 273 | 274 | assert(node.$stopTime === 3); 275 | }); 276 | }); 277 | 278 | describe("works", function() { 279 | it("onended", function() { 280 | var node = audioContext.createOscillator(); 281 | var onended = sinon.spy(); 282 | var event; 283 | 284 | node.onended = onended; 285 | 286 | node.connect(audioContext.destination); 287 | 288 | assert(node.$state === "UNSCHEDULED"); 289 | 290 | node.start(0.100); 291 | node.stop(0.150); 292 | 293 | audioContext.$processTo("00:00.000"); 294 | assert(node.$state === "SCHEDULED", "00:00.000"); 295 | assert(onended.callCount === 0, "00:00.000"); 296 | 297 | audioContext.$processTo("00:00.099"); 298 | assert(node.$state === "SCHEDULED", "00:00.099"); 299 | assert(onended.callCount === 0, "00:00.099"); 300 | 301 | audioContext.$processTo("00:00.100"); 302 | assert(node.$state === "PLAYING", "00:00.100"); 303 | assert(onended.callCount === 0, "00:00.100"); 304 | 305 | audioContext.$processTo("00:00.149"); 306 | assert(node.$state === "PLAYING", "00:00.149"); 307 | assert(onended.callCount === 0, "00:00.149"); 308 | 309 | audioContext.$processTo("00:00.150"); 310 | assert(node.$state === "FINISHED", "00:00.150"); 311 | assert(onended.callCount === 1, "00:00.150"); 312 | assert(onended.calledOn(node), "00:00.150"); 313 | 314 | audioContext.$processTo("00:00.200"); 315 | assert(node.$state === "FINISHED", "00:00.200"); 316 | assert(onended.callCount === 1, "00:00.200"); 317 | 318 | event = onended.args[0][0]; 319 | 320 | assert(event instanceof WebAudioTestAPI.Event); 321 | assert(event.type === "ended"); 322 | assert(event.target === node); 323 | 324 | assert(node.$stateAtTime("00:00.000") === "SCHEDULED"); 325 | assert(node.$stateAtTime("00:00.099") === "SCHEDULED"); 326 | assert(node.$stateAtTime("00:00.100") === "PLAYING"); 327 | assert(node.$stateAtTime("00:00.149") === "PLAYING"); 328 | assert(node.$stateAtTime("00:00.150") === "FINISHED"); 329 | assert(node.$stateAtTime("00:00.200") === "FINISHED"); 330 | }); 331 | }); 332 | }); 333 | -------------------------------------------------------------------------------- /src/AudioNode.js: -------------------------------------------------------------------------------- 1 | import Configuration from "./utils/Configuration"; 2 | import Immigration from "./utils/Immigration"; 3 | import Junction from "./utils/Junction"; 4 | import EventTarget from "./dom/EventTarget"; 5 | import defaults from "./utils/defaults"; 6 | import toJSON from "./utils/toJSON"; 7 | import toNodeName from "./utils/toNodeName"; 8 | import * as props from "./decorators/props"; 9 | import * as methods from "./decorators/methods"; 10 | import * as validators from "./validators"; 11 | 12 | let configuration = Configuration.getInstance(); 13 | let immigration = Immigration.getInstance(); 14 | 15 | export default class AudioNode extends EventTarget { 16 | static $JSONKeys = []; 17 | 18 | constructor(admission, spec) { 19 | immigration.check(admission, () => { 20 | throw new TypeError("Illegal constructor"); 21 | }); 22 | super(); 23 | 24 | Object.defineProperty(this, "_", { value: {} }); 25 | 26 | this._.context = spec.context; 27 | this.__createAudioNode(spec); 28 | } 29 | 30 | @methods.contract({ 31 | precondition() { 32 | if (this._.context.state === "closed") { 33 | throw new TypeError(`AudioContext has been closed`); 34 | } 35 | } 36 | }) 37 | __createAudioNode(spec) { 38 | this._.name = defaults(spec.name, "AudioNode"); 39 | this._.numberOfInputs = defaults(spec.numberOfInputs, 1); 40 | this._.numberOfOutputs = defaults(spec.numberOfOutputs, 1); 41 | this._.channelCount = defaults(spec.channelCount, 2); 42 | this._.channelCountMode = defaults(spec.channelCountMode, "max"); 43 | this._.channelInterpretation = defaults(spec.channelInterpretation, "speakers"); 44 | this._.inputs = new Array(this._.numberOfInputs).fill().map(i => new Junction(this, i)); 45 | this._.outputs = new Array(this._.numberOfOutputs).fill().map(i => new Junction(this, i)); 46 | this._.tick = -1; 47 | } 48 | 49 | @props.readonly() 50 | context() { 51 | return this._.context; 52 | } 53 | 54 | @props.readonly() 55 | numberOfInputs() { 56 | return this._.numberOfInputs; 57 | } 58 | 59 | @props.readonly() 60 | numberOfOutputs() { 61 | return this._.numberOfOutputs; 62 | } 63 | 64 | @props.typed(validators.isPositiveInteger, 2) 65 | channelCount() {} 66 | 67 | @props.enums([ "max", "clamped-max", "explicit" ]) 68 | channelCountMode() {} 69 | 70 | @props.enums([ "speakers", "discrete" ]) 71 | channelInterpretation() {} 72 | 73 | @methods.param("destination", validators.isAudioSource); 74 | @methods.param("[ output ]", validators.isPositiveInteger); 75 | @methods.param("[ input ]", validators.isPositiveInteger); 76 | @methods.contract({ 77 | precondition(destination, output = 0, input = 0) { 78 | if (this.$context !== destination.$context) { 79 | throw new TypeError("Cannot connect to a destination belonging to a different AudioContext."); 80 | } 81 | if (this.numberOfOutputs <= output) { 82 | throw new TypeError(`The {{output}} index (${output}) exceeds number of outputs (${this.numberOfOutputs}).`); 83 | } 84 | if ((destination.numberOfInputs || 1) <= input) { 85 | throw new TypeError(`The {{input}} index (${input}) exceeds number of inputs (${destination.numberOfInputs}).`); 86 | } 87 | } 88 | }) 89 | connect(destination, output = 0, input = 0) { 90 | this._.outputs[output].connect(destination.$inputs[input]); 91 | } 92 | 93 | disconnect(destination, output, input) { 94 | if (configuration.getState("AudioNode#disconnect") !== "selective") { 95 | return this.__disconnect$$Channel(defaults(destination, 0)); 96 | } 97 | 98 | switch (arguments.length) { 99 | case 0: 100 | return this.__disconnect$$All(); 101 | case 1: 102 | if (typeof destination === "number") { 103 | return this.__disconnect$$Channel(destination); 104 | } 105 | return this.__disconnect$$Selective1(destination); 106 | case 2: 107 | return this.__disconnect$$Selective2(destination, output); 108 | case 3: 109 | return this.__disconnect$$Selective3(destination, output, input); 110 | default: 111 | // no default 112 | } 113 | } 114 | 115 | __disconnect$$All() { 116 | this._.outputs.forEach((junction) => { 117 | junction.disconnectAll(); 118 | }); 119 | } 120 | 121 | @methods.param("output", validators.isPositiveInteger) 122 | @methods.contract({ 123 | precondition(output) { 124 | if (this.numberOfOutputs <= output) { 125 | throw new TypeError(`The {{output}} index (${output}) exceeds number of outputs (${this.numberOfOutputs}).`); 126 | } 127 | } 128 | }) 129 | __disconnect$$Channel(output) { 130 | this._.outputs[output].disconnectAll(); 131 | } 132 | 133 | @methods.param("destination", validators.isAudioSource) 134 | @methods.contract({ 135 | precondition(destination) { 136 | if (!this._.outputs.some(junction => junction.isConnected(destination))) { 137 | throw new TypeError("The given {{destination}} is not connected."); 138 | } 139 | } 140 | }) 141 | __disconnect$$Selective1(destination) { 142 | this._.outputs.forEach((junction) => { 143 | junction.disconnectNode(destination); 144 | }); 145 | } 146 | 147 | @methods.param("destination", validators.isAudioSource) 148 | @methods.param("output", validators.isPositiveInteger) 149 | @methods.contract({ 150 | precondition(destination, output) { 151 | if (!this._.outputs.some(junction => junction.isConnected(destination))) { 152 | throw new TypeError("The given {{destination}} is not connected."); 153 | } 154 | if (this.numberOfOutputs <= output) { 155 | throw new TypeError(`The {{output}} provided (${output}) is outside the range [0, ${this.numberOfOutputs}).`); 156 | } 157 | } 158 | }) 159 | __disconnect$$Selective2(destination, output) { 160 | this._.outputs[output].disconnectNode(destination); 161 | } 162 | 163 | @methods.param("destination", validators.isAudioSource) 164 | @methods.param("output", validators.isPositiveInteger) 165 | @methods.param("input", validators.isPositiveInteger) 166 | @methods.contract({ 167 | precondition(destination, output, input) { 168 | if (!this._.outputs.some(junction => junction.isConnected(destination))) { 169 | throw new TypeError("The given {{destination}} is not connected."); 170 | } 171 | if (output < 0 || this.numberOfOutputs <= output) { 172 | throw new TypeError(`The {{output}} provided (${output}) is outside the range [0, ${this.numberOfOutputs}).`); 173 | } 174 | if (input < 0 || destination.numberOfInputs <= input) { 175 | throw new TypeError(`The {{input}} provided (${input}) is outside the range [0, ${this.numberOfInputs}).`); 176 | } 177 | } 178 | }) 179 | __disconnect$$Selective3(destination, output, input) { 180 | this._.outputs[output].disconnectChannel(destination, input); 181 | } 182 | 183 | toJSON(memo) { 184 | function __toJSON(obj, memo) { 185 | if (obj && typeof obj.toJSON === "function") { 186 | return obj.toJSON(memo); 187 | } 188 | return obj; 189 | } 190 | 191 | return toJSON(this, (node, memo) => { 192 | let json = {}; 193 | 194 | json.name = toNodeName(node); 195 | 196 | node.constructor.$JSONKeys.forEach((key) => { 197 | json[key] = __toJSON(node[key], memo); 198 | }); 199 | 200 | if (node.$context.VERBOSE_JSON) { 201 | json.numberOfInputs = node.numberOfInputs; 202 | json.numberOfOutputs = node.numberOfOutputs; 203 | json.channelCount = node.channelCount; 204 | json.channelCountMode = node.channelCountMode; 205 | json.channelInterpretation = node.channelInterpretation; 206 | } 207 | 208 | if (node.$inputs.length === 1) { 209 | json.inputs = node.$inputs[0].toJSON(memo); 210 | } else { 211 | json.inputs = node.$inputs.map(junction => junction.toJSON(memo)); 212 | } 213 | 214 | return json; 215 | }, memo); 216 | } 217 | 218 | get $name() { 219 | return this._.name; 220 | } 221 | 222 | get $context() { 223 | return this._.context; 224 | } 225 | 226 | get $inputs() { 227 | // TODO: remove v0.4.0 228 | if (this._.inputs.length === 0) { 229 | return [ new Junction(this, 0) ]; 230 | } 231 | return this._.inputs; 232 | } 233 | 234 | $process(inNumSamples, tick) { 235 | if (this._.tick !== tick) { 236 | this._.tick = tick; 237 | this.$inputs.forEach((junction) => { 238 | junction.process(inNumSamples, tick); 239 | }); 240 | Object.keys(this._).forEach((key) => { 241 | if (this[key] instanceof global.AudioParam) { 242 | this[key].$process(inNumSamples, tick); 243 | } 244 | }); 245 | if (this.__process) { 246 | this.__process(inNumSamples); 247 | } 248 | } 249 | } 250 | 251 | $isConnectedTo(destination, output = 0, input = 0) { 252 | if (!(destination instanceof global.AudioNode) && !(destination instanceof global.AudioParam)) { 253 | return false; 254 | } 255 | 256 | let outputJunction = this._.outputs[output]; 257 | let inputJunction = destination._.inputs[input]; 258 | 259 | if (!outputJunction || !inputJunction) { 260 | return false; 261 | } 262 | 263 | return outputJunction.outputs.some(junction => junction === inputJunction); 264 | } 265 | 266 | $isConnectedFrom(destination, output = 0, input = 0) { 267 | if (!(destination instanceof global.AudioNode)) { 268 | return false; 269 | } 270 | 271 | let outputJunction = destination._.outputs[output]; 272 | let inputJunction = this._.inputs[input]; 273 | 274 | if (!outputJunction || !inputJunction) { 275 | return false; 276 | } 277 | 278 | return inputJunction.inputs.some(junction => junction === outputJunction); 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web-audio-test-api 2 | [![Build Status](http://img.shields.io/travis/mohayonao/web-audio-test-api.svg?style=flat-square)](https://travis-ci.org/mohayonao/web-audio-test-api) 3 | [![NPM Version](http://img.shields.io/npm/v/web-audio-test-api.svg?style=flat-square)](https://www.npmjs.org/package/web-audio-test-api) 4 | [![License](http://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](http://mohayonao.mit-license.org/) 5 | 6 | > Web Audio API test library for CI 7 | 8 | ## Installation 9 | 10 | ### node.js 11 | 12 | ``` 13 | $ npm install --save-dev web-audio-test-api 14 | ``` 15 | 16 | Install Web Audio API interfaces to global scope 17 | 18 | ```javascript 19 | import "web-audio-test-api"; 20 | ``` 21 | 22 | ### browser 23 | 24 | - [web-audio-test-api.js](http://mohayonao.github.io/web-audio-test-api/build/web-audio-test-api.js) 25 | 26 | Replace existing Web Audio API with web-audio-test-api 27 | 28 | ```html 29 | 30 | ``` 31 | 32 | if you won't use web-audio-test-api 33 | 34 | ```javascript 35 | WebAudioTestAPI.unuse(); 36 | ``` 37 | 38 | ## Online Test Suite 39 | 40 | - [web-audio-test-api.js - online test suite](http://mohayonao.github.io/web-audio-test-api/) 41 | 42 | ## Documents 43 | 44 | - [Web Audio API Specification](http://www.w3.org/TR/webaudio/) 45 | - [Test API Reference](https://github.com/mohayonao/web-audio-test-api/wiki) 46 | 47 | ## Features 48 | 49 | - Strict type check more than original Web Audio API 50 | 51 | ```javascript 52 | var audioContext = new AudioContext(); 53 | var osc = audioContext.createOsillator(); 54 | 55 | // correct 56 | osc.frequency.value = 880; 57 | 58 | // wrong 59 | assert.throws(function() { 60 | osc.frequency = 880; 61 | }, function(e) { 62 | return e instanceof TypeError && 63 | e.message === "OscillatorNode#frequency is readonly"; 64 | }); 65 | 66 | assert.throws(function() { 67 | osc.type = 2; 68 | }, function(e) { 69 | return e instanceof TypeError && 70 | e.message === "OscillatorNode#type should be an enum { sine, square, sawtooth, triangle }, but got: 2"; 71 | }); 72 | }); 73 | ``` 74 | 75 | - Convert to JSON from audio graph 76 | 77 | ```javascript 78 | var audioContext = new AudioContext(); 79 | var osc = audioContext.createOscillator(); 80 | var lfo = audioContext.createOscillator(); 81 | var amp = audioContext.createGain(); 82 | 83 | lfo.$id = "LFO"; // name for debugging 84 | 85 | osc.type = "sawtooth"; 86 | osc.frequency.value = 880; 87 | 88 | lfo.frequency.value = 2; 89 | 90 | lfo.connect(amp.gain); 91 | osc.connect(amp); 92 | amp.connect(audioContext.destination); 93 | 94 | assert.deepEqual(audioContext.toJSON(), { 95 | name: "AudioDestinationNode" // +------------------+ 96 | inputs: [ // | OscillatorNode | 97 | { // | - type: sawtooth | 98 | name: "GainNode", // | - frequency: 220 | 99 | gain: { // | - detune: 0 | 100 | value: 1, // +------------------+ 101 | inputs: [ // | 102 | { // +-----------+ +--------------------+ 103 | name: "OscillatorNode#LFO", // | GainNode | | OscillatorNode#LFO | 104 | type: "sine", // | - gain: 1 |--| - frequency: 2 | 105 | frequency: { // +-----------+ | - detune: 0 | 106 | value: 2, // | +--------------------+ 107 | inputs: [] // | 108 | }, // +----------------------+ 109 | detune: { // | AudioDestinationNode | 110 | value: 0, // +----------------------+ 111 | inputs: [] 112 | }, 113 | inputs: [] 114 | } 115 | ] 116 | }, 117 | inputs: [ 118 | { 119 | name: "OscillatorNode", 120 | type: "sawtooth", 121 | frequency: { 122 | value: 880, 123 | inputs: [] 124 | }, 125 | detune: { 126 | value: 0, 127 | inputs: [] 128 | }, 129 | inputs: [] 130 | } 131 | ] 132 | } 133 | ] 134 | }); 135 | ``` 136 | 137 | - OscillatorNode/BufferSourceNode state 138 | 139 | ```javascript 140 | var audioContext = new AudioContext(); 141 | var node = audioContext.createOscillator(); 142 | 143 | assert(node.$state === "UNSCHEDULED"); 144 | 145 | node.start(0.100); 146 | node.stop(0.150); 147 | node.connect(audioContext.destination); 148 | 149 | audioContext.$processTo("00:00.000"); 150 | assert(node.$state === "SCHEDULED", "00:00.000"); 151 | 152 | audioContext.$processTo("00:00.099"); 153 | assert(node.$state === "SCHEDULED", "00:00.099"); 154 | 155 | audioContext.$processTo("00:00.100"); 156 | assert(node.$state === "PLAYING", "00:00.100"); 157 | 158 | audioContext.$processTo("00:00.149"); 159 | assert(node.$state === "PLAYING", "00:00.149"); 160 | 161 | audioContext.$processTo("00:00.150"); 162 | assert(node.$state === "FINISHED", "00:00.150"); 163 | 164 | // other way 165 | assert(node.$stateAtTime("00:00.000") === "SCHEDULED"); 166 | assert(node.$stateAtTime("00:00.099") === "SCHEDULED"); 167 | assert(node.$stateAtTime("00:00.100") === "PLAYING"); 168 | assert(node.$stateAtTime("00:00.149") === "PLAYING"); 169 | assert(node.$stateAtTime("00:00.150") === "FINISHED"); 170 | ``` 171 | 172 | - AudioParam simulation 173 | 174 | ```javascript 175 | var audioContext = new AudioContext(); 176 | var node = audioContext.createOscillator(); 177 | 178 | node.frequency.setValueAtTime(880, 0.500); 179 | node.frequency.linearRampToValueAtTime(440, 1.500); 180 | node.connect(audioContext.destination); 181 | 182 | audioContext.$processTo("00:00.000"); 183 | assert(node.frequency.value === 440, "00:00.000"); 184 | 185 | audioContext.$processTo("00:00.250"); 186 | assert(node.frequency.value === 440, "00:00.250"); 187 | 188 | audioContext.$processTo("00:00.500"); 189 | assert(node.frequency.value === 880, "00:00.500"); // <- setValueAtTime 190 | // ^ 191 | audioContext.$processTo("00:00.750"); // | 192 | assert(node.frequency.value === 770, "00:00.750"); // | 193 | // | 194 | audioContext.$processTo("00:01.000"); // | 195 | assert(node.frequency.value === 660, "00:01.000"); // | linearRampToValueAtTime 196 | // | 197 | audioContext.$processTo("00:01.250"); // | 198 | assert(node.frequency.value === 550, "00:01.250"); // | 199 | // | 200 | audioContext.$processTo("00:01.500"); // v 201 | assert(node.frequency.value === 440, "00:01.500"); // 202 | 203 | audioContext.$processTo("00:01.750"); 204 | assert(node.frequency.value === 440, "00:01.750"); 205 | 206 | // other way 207 | assert(node.frequency.$valueAtTime("00:00.000" === 440); 208 | assert(node.frequency.$valueAtTime("00:00.250" === 440); 209 | assert(node.frequency.$valueAtTime("00:00.500" === 880); // <- setValueAtTime 210 | assert(node.frequency.$valueAtTime("00:00.750" === 770); // ^ 211 | assert(node.frequency.$valueAtTime("00:01.000" === 660); // | linearRampToValueAtTime 212 | assert(node.frequency.$valueAtTime("00:01.250" === 550); // v 213 | assert(node.frequency.$valueAtTime("00:01.500" === 440); // 214 | assert(node.frequency.$valueAtTime("00:01.750" === 440); 215 | ``` 216 | 217 | - ScriptProcessing simulation 218 | 219 | ```javascript 220 | var audioContext = new AudioContext(); 221 | var node = audioContext.createScriptProcessor(1024, 2, 2); 222 | 223 | node.onaudioprocess = sinon.spy(); 224 | node.connect(audioContext.destination); 225 | 226 | audioContext.$processTo("00:00.500"); 227 | assert(node.onaudioprocess.callCount === 22); 228 | // 22times call (0.5 / (1024 / 44100) = 21.5332) 229 | ``` 230 | 231 | - DecodeAudioData simulation 232 | 233 | ```javascript 234 | var audioContext = new AudioContext(); 235 | 236 | // audioContext.DECODE_AUDIO_DATA_RESULT = customResult; 237 | // audioContext.DECODE_AUDIO_DATA_FAILED = true; 238 | 239 | audioContext.decodeAudioData(audioData, function(result) { 240 | // successCallback 241 | assert(result instanceof AudioBuffer); 242 | }, function() { 243 | // errorCallback 244 | throw new ERROR("NOT REACHED"); 245 | }); 246 | ``` 247 | 248 | - New API support 249 | 250 | ```javascript 251 | WebAudioTestAPI.setState({ 252 | "AudioContext#createStereoPanner": "enabled", 253 | }); 254 | 255 | var audioContext = new AudioContext(); 256 | 257 | var node = audioContext.createStereoPanner(); 258 | 259 | console.log(WebAudioTestAPI.getState("AudioContext#createStereoPanner")); // "enabled" 260 | ``` 261 | 262 | | API Name | states | 263 | |---------------------------------------|------------------------------| 264 | | `AnalyserNode#getFloatTimeDomainData` | "enabled" or **"disabled"** | 265 | | `AudioBuffer#copyToChannel` | "enabled" or **"disabled"** | 266 | | `AudioBuffer#copyFromChannel` | "enabled" or **"disabled"** | 267 | | `AudioContext#createAudioWorker` | **"disabled"** | 268 | | `AudioContext#createStereoPanner` | "enabled" or **"disabled"** | 269 | | `AudioContext#close` | "enabled" or **"disabled"** | 270 | | `AudioContext#suspend` | "enabled" or **"disabled"** | 271 | | `AudioContext#resume` | "enabled" or **"disabled"** | 272 | | `AudioContext#decodeAudioData` | "promise" or **"void"** | 273 | | `OfflineAudioContext#startRendering` | "promise" or **"void"** | 274 | | `AudioNode#disconnect` | "selective" or **"channel"** | 275 | 276 | ## License 277 | 278 | web-audio-test-api.js is available under the The MIT License. 279 | --------------------------------------------------------------------------------