├── .eslintrc ├── .gitignore ├── .travis.yml ├── README.md ├── build └── fm-synth.js ├── index.js ├── package.json ├── src ├── FMSynth.js ├── FMSynthUtils.js ├── algorithms.js └── index.js └── test ├── .eslintrc ├── FMSynth.js ├── FMSynthUtils.js └── mocha.opts /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "node": true 5 | }, 6 | "rules": { 7 | // possible errors 8 | "comma-dangle": [ 2, "always-multiline" ], 9 | "no-cond-assign": [ 2, "except-parens" ], 10 | "no-console": 2, 11 | "no-constant-condition": 2, 12 | "no-control-regex": 2, 13 | "no-debugger": 2, 14 | "no-dupe-args": 2, 15 | "no-dupe-keys": 2, 16 | "no-duplicate-case": 2, 17 | "no-empty-character-class": 2, 18 | "no-empty": 2, 19 | "no-ex-assign": 2, 20 | "no-extra-boolean-cast": 2, 21 | "no-extra-parens": 0, 22 | "no-extra-semi": 2, 23 | "no-func-assign": 2, 24 | "no-inner-declarations": 2, 25 | "no-invalid-regexp": 2, 26 | "no-irregular-whitespace": 2, 27 | "no-negated-in-lhs": 2, 28 | "no-obj-calls": 2, 29 | "no-regex-spaces": 2, 30 | "no-reserved-keys": 2, 31 | "no-sparse-arrays": 2, 32 | "no-unreachable": 2, 33 | "use-isnan": 2, 34 | "valid-typeof": 2, 35 | "no-unexpected-multiline": 2, 36 | 37 | // best practice 38 | "accessor-pairs": [ 2, { "getWithoutSet": false, "setWithoutGet": true } ], 39 | "block-scoped-var": 0, 40 | "consistent-return": 2, 41 | "curly": [ 2, "all" ], 42 | "default-case": 2, 43 | "dot-notation": 0, 44 | "dot-location": 2, 45 | "eqeqeq": 2, 46 | "guard-for-in": 2, 47 | "no-alert": 2, 48 | "no-caller": 2, 49 | "no-div-regex": 2, 50 | "no-else-return": 2, 51 | "no-empty-label": 2, 52 | "no-eq-null": 2, 53 | "no-eval": 2, 54 | "no-extend-native": 2, 55 | "no-extra-bind": 2, 56 | "no-fallthrough": 2, 57 | "no-floating-decimal": 2, 58 | "no-implied-eval": 2, 59 | "no-iterator": 2, 60 | "no-labels": 2, 61 | "no-lone-blocks": 2, 62 | "no-loop-func": 2, 63 | "no-multi-spaces": 2, 64 | "no-multi-str": 2, 65 | "no-native-reassign": 2, 66 | "no-new-func": 2, 67 | "no-new-wrappers": 2, 68 | "no-new": 2, 69 | "no-octal-escape": 2, 70 | "no-octal": 2, 71 | "no-param-reassign": 0, 72 | "no-process-env": 2, 73 | "no-proto": 2, 74 | "no-redeclare": 2, 75 | "no-return-assign": 2, 76 | "no-script-url": 2, 77 | "no-self-compare": 2, 78 | "no-sequences": 2, 79 | "no-throw-literal": 2, 80 | "no-unused-expressions": 2, 81 | "no-void": 2, 82 | "no-warning-comments": 0, 83 | "no-with": 2, 84 | "radix": 2, 85 | "vars-on-top": 2, 86 | "wrap-iife": [ 2, "inside" ], 87 | "yoda": 0, 88 | 89 | // strict mode 90 | "strict": [ 2, "never" ], 91 | 92 | // variables 93 | "no-catch-shadow": 2, 94 | "no-delete-var": 2, 95 | "no-label-var": 2, 96 | "no-shadow-restricted-names": 2, 97 | "no-shadow": 0, 98 | "no-undef-init": 2, 99 | "no-undef": 2, 100 | "no-undefined": 2, 101 | "no-unused-vars": 2, 102 | "no-use-before-define": [ 2, "nofunc" ], 103 | 104 | // style 105 | "array-bracket-spacing": [ 2, "always" ], 106 | "camelcase": [ 2, { "properties": "always" } ], 107 | "comma-spacing": [ 2, { "before": false, "after": true } ], 108 | "comma-style": [ 2, "last" ], 109 | "computed-property-spacing": [ 2, "never" ], 110 | "consistent-this": [ 2, "_this" ], 111 | "eol-last": 2, 112 | "func-names": 0, 113 | "func-style": 0, 114 | "indent": [ 2, 2 ], 115 | "key-spacing": 2, 116 | "linebreak-style": [ 2, "unix" ], 117 | "max-nested-callbacks": 0, 118 | "new-cap": 2, 119 | "new-parens": 2, 120 | "newline-after-var": [ 2, "always" ], 121 | "no-array-constructor": 2, 122 | "no-continue": 0, 123 | "no-inline-comments": 2, 124 | "no-lonely-if": 2, 125 | "no-mixed-spaces-and-tabs": 2, 126 | "no-multiple-empty-lines": 2, 127 | "no-nested-ternary": 2, 128 | "no-new-object": 2, 129 | "no-spaced-func": 2, 130 | "no-ternary": 0, 131 | "no-trailing-spaces": 2, 132 | "no-underscore-dangle": 0, 133 | "no-unneeded-ternary": 2, 134 | "object-curly-spacing": [ 2, "always" ], 135 | "one-var": 0, 136 | "operator-assignment": [ 2, "always" ], 137 | "operator-linebreak": [ 2, "before" ], 138 | "padded-blocks": [ 2, "never" ], 139 | "quote-props": [ 2, "as-needed" ], 140 | "quotes": [ 2, "double", "avoid-escape" ], 141 | "semi-spacing": [ 2, { "before": false, "after": true } ], 142 | "semi": [ 2, "always" ], 143 | "sort-vars": 0, 144 | "space-after-keywords": [ 2, "always" ], 145 | "space-before-blocks": [ 2, "always" ], 146 | "space-before-function-paren": [ 2, "never" ], 147 | "space-in-parens": [ 2, "never" ], 148 | "space-infix-ops": [ 2, { "int32Hint": true } ], 149 | "space-return-throw-case": 2, 150 | "space-unary-ops": [ 2, { "words": true, "nonwords": false } ], 151 | "spaced-comment": 0, 152 | "wrap-regex": 0, 153 | 154 | // es6 155 | "constructor-super": 2, 156 | "no-this-before-super": 2, 157 | "no-var": 2, 158 | "object-shorthand": 0, 159 | "prefer-const": 0, 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | npm-debug.log 3 | node_modules/ 4 | lib/ 5 | coverage/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "0.12" 5 | - "iojs" 6 | script: 7 | - npm run travis 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FM SYNTH 2 | [![Build Status](http://img.shields.io/travis/mohayonao/fm-synth.svg?style=flat-square)](https://travis-ci.org/mohayonao/fm-synth) 3 | [![NPM Version](http://img.shields.io/npm/v/@mohayonao/fm-synth.svg?style=flat-square)](https://www.npmjs.org/package/@mohayonao/fm-synth) 4 | [![License](http://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](http://mohayonao.mit-license.org/) 5 | 6 | > simple frequency modulation synthesizer 7 | 8 | ## Installation 9 | 10 | Node.js 11 | 12 | ```sh 13 | npm install @mohayonao/fm-synth 14 | ``` 15 | 16 | Browser 17 | 18 | - [fm-synth.js](https://raw.githubusercontent.com/mohayonao/fm-synth/master/build/fm-synth.js) 19 | 20 | ## API 21 | ### Operator 22 | - `constructor(algorithm: number|string, operators: any[])` 23 | 24 | #### Instance attribute 25 | - `context: AudioContext` 26 | - `operators: any[]` 27 | - `algorithm: string` 28 | - `onended: function` 29 | 30 | #### Instance methods 31 | - `connect(destination: AudioNode): void` 32 | - `disconnect(): void` 33 | - `start(when: number): void` 34 | - `stop(when: number): void` 35 | 36 | ## Algorithm Notation 37 | ```js 38 | let A = audioContext.createDelay(); 39 | let B = new Operator(audioContext); 40 | let C = new Operator(audioContext); 41 | let D = new Operator(audioContext); 42 | let fm = new FMSynth("E-D-C->; B-A->", [ A, B, C, D, 0 ]); 43 | ``` 44 | 45 | - `A`, `B`, `C` ... `Z`: index of the operators 46 | - `-`: connection (connects to a node's frequency or node self if not has frequency) 47 | - `>`: connection to output 48 | - `;`: separator 49 | - `0` in operators mean OFF. Those are ignored in the audio graph. 50 | 51 | Now, the instance of FMSynth builds the below graph by the given algorithm `"D-C->; B-A->"`. 52 | 53 | ![fm-synth](https://raw.githubusercontent.com/wiki/mohayonao/fm-synth/images/fm-synth.png) 54 | 55 | ## Algorithm Presets 56 | ![4 operators algorithm](https://raw.githubusercontent.com/wiki/mohayonao/fm-synth/images/4-op-alg.png) 57 | 58 | ## See Also 59 | - [@mohayonao/operator](https://github.com/mohayonao/operator) - simple operator 60 | 61 | ## License 62 | MIT 63 | -------------------------------------------------------------------------------- /build/fm-synth.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.FMSynth = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o") { 176 | outlets.push(node); 177 | } else if (typeof nextNode.frequency === "object") { 178 | node.connect(nextNode.frequency); 179 | } else { 180 | node.connect(nextNode); 181 | } 182 | 183 | token = nextToken; 184 | node = nextNode; 185 | }); 186 | }); 187 | 188 | if (outlets.length === 0) { 189 | throw new TypeError("no output"); 190 | } 191 | 192 | return outlets; 193 | } 194 | 195 | function isValidAlgorithm(algorithm, numOfOperators) { 196 | var X = String.fromCharCode(64 + numOfOperators); 197 | var re = new RegExp("^[A-" + X + "]-(?:[A-" + X + "]-)*[>A-" + X + "]$", "i"); 198 | 199 | return algorithm.indexOf(">") !== -1 && algorithm.replace(/\s+/g, "").split(";").every(function (algorithm) { 200 | return re.test(algorithm); 201 | }); 202 | } 203 | 204 | exports["default"] = { 205 | build: build, 206 | isValidAlgorithm: isValidAlgorithm 207 | }; 208 | module.exports = exports["default"]; 209 | },{"./algorithms":4}],4:[function(require,module,exports){ 210 | "use strict"; 211 | 212 | Object.defineProperty(exports, "__esModule", { 213 | value: true 214 | }); 215 | exports["default"] = { 216 | 1: ["A->"], 217 | 2: ["B-A->", "B->; A->"], 218 | 3: ["C-B-A->", "C-A; B-A; A->", "C-B->; C-A->", "C-B->; A->", "C->; B->; A->"], 219 | 4: ["D-C-B-A->", "D-B; C-B; B-A->", "D-A->; C-B-A; A->", "D-C; D-B; C-A->; B-A->", "D-C; C-A->; C-B->", "D-C-B->; A->", "D-A; C-A; B-A; A->", "D-C->; B-A->", "D-C->; D-B->; D-A->", "D-C->; B->; A->", "D->; C->; B->; A->"] 220 | }; 221 | module.exports = exports["default"]; 222 | },{}],5:[function(require,module,exports){ 223 | "use strict"; 224 | 225 | Object.defineProperty(exports, "__esModule", { 226 | value: true 227 | }); 228 | 229 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 230 | 231 | var _FMSynth = require("./FMSynth"); 232 | 233 | var _FMSynth2 = _interopRequireDefault(_FMSynth); 234 | 235 | exports["default"] = _FMSynth2["default"]; 236 | module.exports = exports["default"]; 237 | },{"./FMSynth":2}]},{},[1])(1) 238 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./lib"); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mohayonao/fm-synth", 3 | "description": "simple frequency modulation synthesizer", 4 | "version": "0.1.3", 5 | "author": "mohayonao ", 6 | "bugs": { 7 | "url": "https://github.com/mohayonao/fm-synth/issues" 8 | }, 9 | "dependencies": {}, 10 | "devDependencies": { 11 | "babel": "^5.6.14", 12 | "babel-eslint": "^3.1.23", 13 | "browserify": "^10.2.4", 14 | "eslint": "^0.24.0", 15 | "espower-babel": "^3.2.0", 16 | "isparta": "^3.0.3", 17 | "mocha": "^2.2.5", 18 | "power-assert": "^0.11.0", 19 | "sinon": "^1.15.4", 20 | "web-audio-test-api": "^0.3.5" 21 | }, 22 | "files": [ 23 | "package.json", 24 | "README.md", 25 | "index.js", 26 | "lib" 27 | ], 28 | "homepage": "https://github.com/mohayonao/fm-synth/", 29 | "license": "MIT", 30 | "main": "index.js", 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/mohayonao/fm-synth.git" 34 | }, 35 | "scripts": { 36 | "build": "npm run build-to5 && npm run build-browser", 37 | "build-browser": "browserify index.js --standalone FMSynth -o build/fm-synth.js", 38 | "build-to5": "babel src --out-dir lib", 39 | "cover": "babel-node $(npm bin)/isparta cover --report text --report html _mocha", 40 | "lint": "eslint src test", 41 | "prepublish": "rm -rf lib && npm run lint && npm run test && npm run build-to5", 42 | "test": "mocha --compilers js:espower-babel/guess", 43 | "travis": "npm run lint && npm run test" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/FMSynth.js: -------------------------------------------------------------------------------- 1 | import FMSynthUtils from "./FMSynthUtils"; 2 | 3 | const OUTLETS = typeof Symbol !== "undefined" ? Symbol("OUTLETS") : "_@mohayonao/operator:OUTLETS"; 4 | const OPERATORS = typeof Symbol !== "undefined" ? Symbol("OPERATORS") : "_@mohayonao/operator:OPERATORS"; 5 | const ALGORITHM = typeof Symbol !== "undefined" ? Symbol("ALGORITHM") : "_@mohayonao/operator:ALGORITHM"; 6 | const ONENDED = typeof Symbol !== "undefined" ? Symbol("ONENDED") : "_@mohayonao/operator:ONENDED"; 7 | 8 | export default class FMSynth { 9 | constructor(algorithm, operators) { 10 | let outlets = FMSynthUtils.build(algorithm, operators); 11 | 12 | this[OUTLETS] = outlets; 13 | this[OPERATORS] = operators; 14 | this[ALGORITHM] = algorithm; 15 | this[ONENDED] = findOnEndedNode(operators); 16 | } 17 | 18 | get context() { 19 | return this[OUTLETS][0].context; 20 | } 21 | 22 | get operators() { 23 | return this[OPERATORS].slice(); 24 | } 25 | 26 | get algorithm() { 27 | return this[ALGORITHM]; 28 | } 29 | 30 | get onended() { 31 | return this[ONENDED].onended; 32 | } 33 | 34 | set onended(value) { 35 | this[ONENDED].onended = value; 36 | } 37 | 38 | connect(destination) { 39 | this[OUTLETS].forEach((outlet) => { 40 | outlet.connect(destination); 41 | }); 42 | } 43 | 44 | disconnect(...args) { 45 | this[OUTLETS].forEach((outlet) => { 46 | outlet.disconnect(...args); 47 | }); 48 | } 49 | 50 | start(when) { 51 | this[OPERATORS].forEach((op) => { 52 | if (op && typeof op.start === "function") { 53 | op.start(when); 54 | } 55 | }); 56 | } 57 | 58 | stop(when) { 59 | this[OPERATORS].forEach((op) => { 60 | if (op && typeof op.stop === "function") { 61 | op.stop(when); 62 | } 63 | }); 64 | } 65 | } 66 | 67 | function findOnEndedNode(operators) { 68 | for (let i = 0, imax = operators.length; i < imax; i++) { 69 | if (typeof operators[i].onended !== "undefined") { 70 | return operators[i]; 71 | } 72 | } 73 | 74 | return { onended: null }; 75 | } 76 | -------------------------------------------------------------------------------- /src/FMSynthUtils.js: -------------------------------------------------------------------------------- 1 | import ALGORITHMS from "./algorithms"; 2 | 3 | function build(pattern, operators) { 4 | let algorithm = null; 5 | 6 | if (typeof pattern === "number") { 7 | algorithm = (ALGORITHMS[operators.length] && ALGORITHMS[operators.length][pattern]) || null; 8 | } else { 9 | algorithm = "" + pattern; 10 | } 11 | 12 | if (26 < operators.length) { 13 | throw new TypeError("too many operator"); 14 | } 15 | if (algorithm === null) { 16 | throw new TypeError(`not found algorithm ${pattern} for ${operators.length} operators`); 17 | } 18 | if (!isValidAlgorithm(algorithm, operators.length)) { 19 | throw new TypeError(`invalid algorithm: ${algorithm}`); 20 | } 21 | 22 | function findOperatorByName(name) { 23 | return operators[name.toUpperCase().charCodeAt(0) - 65]; 24 | } 25 | 26 | let outlets = []; 27 | let graph = {}; 28 | 29 | algorithm.replace(/\s+|-/g, "").split(";").forEach((algorithm) => { 30 | let tokens = algorithm.split(""); 31 | let token, node; 32 | 33 | while (!node && tokens.length) { 34 | token = tokens.shift(); 35 | node = findOperatorByName(token); 36 | } 37 | 38 | tokens.forEach((nextToken) => { 39 | if (graph[token]) { 40 | if (graph[token].indexOf(nextToken) !== -1) { 41 | return; 42 | } 43 | graph[token].push(nextToken); 44 | } else { 45 | graph[token] = [ nextToken ]; 46 | } 47 | 48 | let nextNode = findOperatorByName(nextToken); 49 | 50 | if (nextToken === ">") { 51 | outlets.push(node); 52 | } else if (typeof nextNode.frequency === "object") { 53 | node.connect(nextNode.frequency); 54 | } else { 55 | node.connect(nextNode); 56 | } 57 | 58 | [ token, node ] = [ nextToken, nextNode ]; 59 | }); 60 | }); 61 | 62 | if (outlets.length === 0) { 63 | throw new TypeError(`no output`); 64 | } 65 | 66 | return outlets; 67 | } 68 | 69 | function isValidAlgorithm(algorithm, numOfOperators) { 70 | let X = String.fromCharCode(64 + numOfOperators); 71 | let re = new RegExp(`^[A-${X}]-(?:[A-${X}]-)*[>A-${X}]$`, "i"); 72 | 73 | return algorithm.indexOf(">") !== -1 74 | && algorithm.replace(/\s+/g, "").split(";").every(algorithm => re.test(algorithm)); 75 | } 76 | 77 | export default { 78 | build, 79 | isValidAlgorithm, 80 | }; 81 | -------------------------------------------------------------------------------- /src/algorithms.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 1: [ 3 | "A->", 4 | ], 5 | 2: [ 6 | "B-A->", 7 | "B->; A->", 8 | ], 9 | 3: [ 10 | "C-B-A->", 11 | "C-A; B-A; A->", 12 | "C-B->; C-A->", 13 | "C-B->; A->", 14 | "C->; B->; A->", 15 | ], 16 | 4: [ 17 | "D-C-B-A->", 18 | "D-B; C-B; B-A->", 19 | "D-A->; C-B-A; A->", 20 | "D-C; D-B; C-A->; B-A->", 21 | "D-C; C-A->; C-B->", 22 | "D-C-B->; A->", 23 | "D-A; C-A; B-A; A->", 24 | "D-C->; B-A->", 25 | "D-C->; D-B->; D-A->", 26 | "D-C->; B->; A->", 27 | "D->; C->; B->; A->", 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import FMSynth from "./FMSynth"; 2 | 3 | export default FMSynth; 4 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "describe": true, 4 | "it": true, 5 | "before": true, 6 | "beforeEach": true, 7 | "afterEach": true, 8 | "after": true, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/FMSynth.js: -------------------------------------------------------------------------------- 1 | import "web-audio-test-api"; 2 | import assert from "power-assert"; 3 | import sinon from "sinon"; 4 | import FMSynth from "../src/FMSynth"; 5 | 6 | describe("FMSynth", () => { 7 | let audioContext, A, B, C, D; 8 | 9 | beforeEach(() => { 10 | audioContext = new global.AudioContext(); 11 | A = audioContext.createGain(); 12 | B = audioContext.createOscillator(); 13 | C = audioContext.createGain(); 14 | D = audioContext.createOscillator(); 15 | }); 16 | 17 | describe("constructor(algorithm: string, operators: Operator[])", () => { 18 | it("works", () => { 19 | let fm = new FMSynth("D-C-B-A->", [ A, B, C, D ]); 20 | 21 | assert(fm instanceof FMSynth); 22 | }); 23 | }); 24 | describe("#context: AudioContext", () => { 25 | it("works", () => { 26 | let fm = new FMSynth("D-C-B-A->", [ A, B, C, D ]); 27 | 28 | assert(fm.context === audioContext); 29 | }); 30 | }); 31 | describe("#operators: Operator[]", () => { 32 | it("works", () => { 33 | let fm = new FMSynth("D-C-B-A->", [ A, B, C, D ]); 34 | 35 | assert.deepEqual(fm.operators, [ A, B, C, D ]); 36 | }); 37 | }); 38 | describe("#algorithm: string", () => { 39 | it("works", () => { 40 | let fm = new FMSynth("D-C-B-A->", [ A, B, C, D ]); 41 | 42 | assert(fm.algorithm === "D-C-B-A->"); 43 | }); 44 | }); 45 | describe("#onended: function", () => { 46 | it("works", () => { 47 | let fm = new FMSynth("D-C-B-A->", [ A, B, C, D ]); 48 | 49 | fm.onended = sinon.spy(); 50 | 51 | assert(fm.onended === B.onended); 52 | }); 53 | it("works when operators have no onended", () => { 54 | let fm = new FMSynth("A->", [ A ]); 55 | let spy = sinon.spy(); 56 | 57 | fm.onended = spy; 58 | 59 | assert(fm.onended === spy); 60 | assert(typeof A.onended === "undefined"); 61 | }); 62 | }); 63 | describe("#connect(destination: AudioNode): void", () => { 64 | it("works", () => { 65 | let fm = new FMSynth("D-C-B-A->", [ A, B, C, D ]); 66 | 67 | fm.connect(audioContext.destination); 68 | 69 | assert(audioContext.destination.$isConnectedFrom(A)); 70 | }); 71 | }); 72 | describe("#disconnect(destination: AudioNode): void", () => { 73 | it("works", () => { 74 | let fm = new FMSynth("D-C-B-A->", [ A, B, C, D ]); 75 | 76 | fm.connect(audioContext.destination); 77 | fm.disconnect(0); 78 | 79 | assert(!audioContext.destination.$isConnectedFrom(A)); 80 | }); 81 | }); 82 | describe("#start(when: number): void", () => { 83 | it("works", () => { 84 | let fm = new FMSynth("D-C-B-A->", [ A, B, C, D ]); 85 | 86 | fm.start(1); 87 | 88 | assert(B.$startTime === 1); 89 | assert(D.$startTime === 1); 90 | }); 91 | }); 92 | describe("#start(when: number): void", () => { 93 | it("works", () => { 94 | let fm = new FMSynth("D-C-B-A->", [ A, B, C, D ]); 95 | 96 | fm.start(1); 97 | fm.stop(2); 98 | 99 | assert(B.$stopTime === 2); 100 | assert(D.$stopTime === 2); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /test/FMSynthUtils.js: -------------------------------------------------------------------------------- 1 | import "web-audio-test-api"; 2 | import assert from "power-assert"; 3 | import FMSynthUtils from "../src/FMSynthUtils"; 4 | 5 | describe("FMSynthUtils", () => { 6 | let audioContext, A, B, C, D; 7 | 8 | beforeEach(() => { 9 | audioContext = new global.AudioContext(); 10 | A = audioContext.createOscillator(); 11 | B = audioContext.createOscillator(); 12 | C = audioContext.createOscillator(); 13 | D = audioContext.createOscillator(); 14 | }); 15 | 16 | describe("build(pattern: number|string, operators: Operator[]): any[]", () => { 17 | describe("algorithm notation", () => { 18 | it("works", () => { 19 | let outlet = FMSynthUtils.build("B-A; A-B; A->", [ A, B ]); 20 | 21 | assert.deepEqual(outlet, [ A ]); 22 | assert(B.$isConnectedTo(A.frequency)); 23 | assert(A.$isConnectedTo(B.frequency)); 24 | }); 25 | }); 26 | describe("off operator", () => { 27 | it("works", () => { 28 | let outlet = FMSynthUtils.build("D-C-B-A->", [ A, B, 0, 0 ]); 29 | 30 | assert.deepEqual(outlet, [ A ]); 31 | assert(B.$isConnectedTo(A.frequency)); 32 | }); 33 | }); 34 | describe("operators.length === 1", () => { 35 | describe("algorithim === 0", () => { 36 | it("works", () => { 37 | let outlet = FMSynthUtils.build(0, [ A ]); 38 | 39 | assert.deepEqual(outlet, [ A ]); 40 | }); 41 | }); 42 | }); 43 | describe("operators.length === 2", () => { 44 | describe("algorithim === 0", () => { 45 | it("works", () => { 46 | let outlet = FMSynthUtils.build(0, [ A, B ]); 47 | 48 | assert(B.$isConnectedTo(A.frequency)); 49 | assert.deepEqual(outlet, [ A ]); 50 | }); 51 | }); 52 | describe("algorithim === 1", () => { 53 | it("works", () => { 54 | let outlet = FMSynthUtils.build(1, [ A, B ]); 55 | 56 | assert.deepEqual(outlet, [ A, B ]); 57 | }); 58 | }); 59 | }); 60 | describe("operators.length === 3", () => { 61 | describe("algorithim === 0", () => { 62 | it("works", () => { 63 | let outlet = FMSynthUtils.build(0, [ A, B, C ]); 64 | 65 | assert(C.$isConnectedTo(B.frequency)); 66 | assert(B.$isConnectedTo(A.frequency)); 67 | assert.deepEqual(outlet, [ A ]); 68 | }); 69 | }); 70 | describe("algorithim === 1", () => { 71 | it("works", () => { 72 | let outlet = FMSynthUtils.build(1, [ A, B, C ]); 73 | 74 | assert(C.$isConnectedTo(A.frequency)); 75 | assert(B.$isConnectedTo(A.frequency)); 76 | assert.deepEqual(outlet, [ A ]); 77 | }); 78 | }); 79 | describe("algorithim === 2", () => { 80 | it("works", () => { 81 | let outlet = FMSynthUtils.build(2, [ A, B, C ]); 82 | 83 | assert(C.$isConnectedTo(B.frequency)); 84 | assert(C.$isConnectedTo(A.frequency)); 85 | assert.deepEqual(outlet, [ A, B ]); 86 | }); 87 | }); 88 | describe("algorithim === 3", () => { 89 | it("works", () => { 90 | let outlet = FMSynthUtils.build(3, [ A, B, C ]); 91 | 92 | assert(C.$isConnectedTo(B.frequency)); 93 | assert.deepEqual(outlet, [ A, B ]); 94 | }); 95 | }); 96 | describe("algorithim === 4", () => { 97 | it("works", () => { 98 | let outlet = FMSynthUtils.build(4, [ A, B, C ]); 99 | 100 | assert.deepEqual(outlet, [ A, B, C ]); 101 | }); 102 | }); 103 | }); 104 | describe("operators.length === 4", () => { 105 | describe("algorithim === 0", () => { 106 | it("works", () => { 107 | let outlet = FMSynthUtils.build(0, [ A, B, C, D ]); 108 | 109 | assert(D.$isConnectedTo(C.frequency)); 110 | assert(C.$isConnectedTo(B.frequency)); 111 | assert(B.$isConnectedTo(A.frequency)); 112 | assert.deepEqual(outlet, [ A ]); 113 | }); 114 | }); 115 | describe("algorithim === 1", () => { 116 | it("works", () => { 117 | let outlet = FMSynthUtils.build(1, [ A, B, C, D ]); 118 | 119 | assert(D.$isConnectedTo(B.frequency)); 120 | assert(C.$isConnectedTo(B.frequency)); 121 | assert(B.$isConnectedTo(A.frequency)); 122 | assert.deepEqual(outlet, [ A ]); 123 | }); 124 | }); 125 | describe("algorithim === 2", () => { 126 | it("works", () => { 127 | let outlet = FMSynthUtils.build(2, [ A, B, C, D ]); 128 | 129 | assert(D.$isConnectedTo(A.frequency)); 130 | assert(C.$isConnectedTo(B.frequency)); 131 | assert(B.$isConnectedTo(A.frequency)); 132 | assert.deepEqual(outlet, [ A ]); 133 | }); 134 | }); 135 | describe("algorithim === 3", () => { 136 | it("works", () => { 137 | let outlet = FMSynthUtils.build(3, [ A, B, C, D ]); 138 | 139 | assert(D.$isConnectedTo(C.frequency)); 140 | assert(D.$isConnectedTo(B.frequency)); 141 | assert(C.$isConnectedTo(A.frequency)); 142 | assert(B.$isConnectedTo(A.frequency)); 143 | assert.deepEqual(outlet, [ A ]); 144 | }); 145 | }); 146 | describe("algorithim === 4", () => { 147 | it("works", () => { 148 | let outlet = FMSynthUtils.build(4, [ A, B, C, D ]); 149 | 150 | assert(D.$isConnectedTo(C.frequency)); 151 | assert(C.$isConnectedTo(B.frequency)); 152 | assert(C.$isConnectedTo(A.frequency)); 153 | assert.deepEqual(outlet, [ A, B ]); 154 | }); 155 | }); 156 | describe("algorithim === 5", () => { 157 | it("works", () => { 158 | let outlet = FMSynthUtils.build(5, [ A, B, C, D ]); 159 | 160 | assert(D.$isConnectedTo(C.frequency)); 161 | assert(C.$isConnectedTo(B.frequency)); 162 | assert.deepEqual(outlet, [ A, B ]); 163 | }); 164 | }); 165 | describe("algorithim === 6", () => { 166 | it("works", () => { 167 | let outlet = FMSynthUtils.build(6, [ A, B, C, D ]); 168 | 169 | assert(D.$isConnectedTo(A.frequency)); 170 | assert(C.$isConnectedTo(A.frequency)); 171 | assert(B.$isConnectedTo(A.frequency)); 172 | assert.deepEqual(outlet, [ A ]); 173 | }); 174 | }); 175 | describe("algorithim === 7", () => { 176 | it("works", () => { 177 | let outlet = FMSynthUtils.build(7, [ A, B, C, D ]); 178 | 179 | assert(D.$isConnectedTo(C.frequency)); 180 | assert(B.$isConnectedTo(A.frequency)); 181 | assert.deepEqual(outlet, [ A, B ]); 182 | }); 183 | }); 184 | describe("algorithim === 8", () => { 185 | it("works", () => { 186 | let outlet = FMSynthUtils.build(8, [ A, B, C, D ]); 187 | 188 | assert(D.$isConnectedTo(C.frequency)); 189 | assert(D.$isConnectedTo(B.frequency)); 190 | assert(D.$isConnectedTo(A.frequency)); 191 | assert.deepEqual(outlet, [ A, B, C ]); 192 | }); 193 | }); 194 | describe("algorithim === 9", () => { 195 | it("works", () => { 196 | let outlet = FMSynthUtils.build(9, [ A, B, C, D ]); 197 | 198 | assert(D.$isConnectedTo(C.frequency)); 199 | assert.deepEqual(outlet, [ A, B, C ]); 200 | }); 201 | }); 202 | describe("algorithim === 10", () => { 203 | it("works", () => { 204 | let outlet = FMSynthUtils.build(10, [ A, B, C, D ]); 205 | 206 | assert.deepEqual(outlet, [ A, B, C, D ]); 207 | }); 208 | }); 209 | }); 210 | describe("given invalid parameter", () => { 211 | it("throws TypeError", () => { 212 | assert.throws(() => { 213 | FMSynthUtils.build(0, [ 214 | A, B, C, D, A, B, C, D, A, B, 215 | A, B, C, D, A, B, C, D, A, B, 216 | A, B, C, D, A, B, C, D, A, B, 217 | ]); 218 | }, (e) => { 219 | return e instanceof TypeError && /too many operator/i.test(e.message); 220 | }); 221 | assert.throws(() => { 222 | FMSynthUtils.build(99, [ A, B, C, D ]); 223 | }, (e) => { 224 | return e instanceof TypeError && /not found algorithm/i.test(e.message); 225 | }); 226 | assert.throws(() => { 227 | FMSynthUtils.build("@_@;", [ A, B, C, D ]); 228 | }, (e) => { 229 | return e instanceof TypeError && /invalid algorithm/i.test(e.message); 230 | }); 231 | assert.throws(() => { 232 | FMSynthUtils.build("D-C-B-A->", [ 0, 0, 0, 0 ]); 233 | }, (e) => { 234 | return e instanceof TypeError && /no output/i.test(e.message); 235 | }); 236 | }); 237 | }); 238 | }); 239 | describe("isValidAlgorithm(pattern: string, length: number): boolean", () => { 240 | it("works", () => { 241 | assert(FMSynthUtils.isValidAlgorithm("D-C-B-A->", 4)); 242 | assert(FMSynthUtils.isValidAlgorithm("D-B; C-B; B-A->", 4)); 243 | assert(FMSynthUtils.isValidAlgorithm("D-A->; C-B-A; A->", 4)); 244 | assert(!FMSynthUtils.isValidAlgorithm("D->", 3)); 245 | assert(!FMSynthUtils.isValidAlgorithm("B-A", 4)); 246 | assert(!FMSynthUtils.isValidAlgorithm("B->A", 4)); 247 | assert(!FMSynthUtils.isValidAlgorithm("-A->", 4)); 248 | assert(!FMSynthUtils.isValidAlgorithm("A-->", 4)); 249 | }); 250 | }); 251 | }); 252 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --recursive 3 | --------------------------------------------------------------------------------