├── .eslintrc ├── .gitignore ├── .travis.yml ├── README.md ├── audio-graph.png ├── index.js ├── package.json └── test ├── .eslintrc ├── index.js └── mocha.opts /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "rules": { 6 | "vars-on-top": 0 7 | }, 8 | "extends": "mohayonao/legacy" 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | npm-debug.log 3 | node_modules/ 4 | coverage/ 5 | lib/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "4.2" 5 | cache: 6 | directories: 7 | - node_modules 8 | script: 9 | - npm run travis 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AudioWorkerNode 2 | [![Build Status](http://img.shields.io/travis/mohayonao/audio-worker-node.svg?style=flat-square)](https://travis-ci.org/mohayonao/audio-worker-node) 3 | [![NPM Version](http://img.shields.io/npm/v/audio-worker-node.svg?style=flat-square)](https://www.npmjs.org/package/audio-worker-node) 4 | [![License](http://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](http://mohayonao.mit-license.org/) 5 | 6 | This module is a sub module for [audio-worker-shim](https://github.com/mohayonao/audio-worker-shim). 7 | 8 | ## Installation 9 | 10 | ``` 11 | $ npm install audio-worker-node 12 | ``` 13 | 14 | ## API 15 | ### AudioWorkerCompiler 16 | - `constructor(audioContext: AudioContext, audioprocess: function, opts: object): ScriptProcessorNode` 17 | 18 | ## Audio Graph 19 | 20 | ![audio-graph](./audio-graph.png) 21 | 22 | ## License 23 | 24 | MIT 25 | -------------------------------------------------------------------------------- /audio-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohayonao/audio-worker-node/9ed3932c512fdea6f30a4ab88af5bbf6fea7420a/audio-graph.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var _dc1 = null; 2 | 3 | function getDC1(audioContext) { 4 | if (_dc1 === null) { 5 | _dc1 = audioContext.createBuffer(1, 8, audioContext.sampleRate); 6 | _dc1.getChannelData(0).set([ 1, 1, 1, 1, 1, 1, 1, 1 ]); 7 | } 8 | return _dc1; 9 | } 10 | 11 | function AudioWorkerNode(audioContext, audioprocess, opts) { 12 | opts = opts || {}; 13 | 14 | var numberOfInputs = Math.max(1, Math.min(+opts.numberOfInputs|0, 32)); 15 | var numberOfOutputs = Math.max(1, Math.min(+opts.numberOfOutputs|0, 32)); 16 | var bufferLength = Math.max(256, Math.min(+opts.bufferLength|0, 16384)); 17 | var dspBufLength = Math.max(128, Math.min(+opts.dspBufLength|0, bufferLength)); 18 | var playbackTimeIncr = dspBufLength / audioContext.sampleRate; 19 | var numberOfParams = (opts.parameters && opts.parameters.length)|0; 20 | var paramKeys = [], paramBuffers = []; 21 | var processor = opts.processor || {}; 22 | var dc1, merger, capture; 23 | var node = audioContext.createScriptProcessor(bufferLength, numberOfInputs, numberOfOutputs); 24 | 25 | node._onmessage = null; 26 | 27 | Object.defineProperty(node, "onmessage", { 28 | get: function() { 29 | return this._onmessage; 30 | }, 31 | set: function(callback) { 32 | if (callback === null || typeof callback === "function") { 33 | this._onmessage = callback; 34 | } 35 | }, 36 | }); 37 | 38 | node.postMessage = function(message) { 39 | var _this = this; 40 | 41 | setTimeout(function() { 42 | if (_this.__target__ && typeof _this.__target__.onmessage === "function") { 43 | _this.__target__.onmessage({ data: message }); 44 | } 45 | }, 0); 46 | }; 47 | 48 | if (numberOfParams) { 49 | dc1 = audioContext.createBufferSource(); 50 | merger = audioContext.createChannelMerger(numberOfParams); 51 | capture = audioContext.createScriptProcessor(bufferLength, numberOfParams, 1); 52 | 53 | dc1.buffer = getDC1(audioContext); 54 | dc1.loop = true; 55 | dc1.start(0); 56 | 57 | opts.parameters.forEach(function(param, index) { 58 | var paramGain = audioContext.createGain(); 59 | 60 | paramGain.channelCount = 1; 61 | paramGain.channelCountMode = "explicit"; 62 | paramGain.gain.value = +param.defaultValue || 0; 63 | node[param.name] = paramGain.gain; 64 | 65 | paramKeys[index] = param.name; 66 | 67 | dc1.connect(paramGain); 68 | paramGain.connect(merger, 0, index); 69 | }); 70 | 71 | capture.onaudioprocess = function(e) { 72 | for (var i = 0; i < numberOfParams; i++) { 73 | paramBuffers[i] = e.inputBuffer.getChannelData(i); 74 | } 75 | }; 76 | 77 | merger.connect(capture); 78 | capture.connect(node); 79 | } 80 | 81 | node.onaudioprocess = function(e) { 82 | var inputs = new Array(numberOfInputs); 83 | var outputs = new Array(numberOfOutputs); 84 | var playbackTime = e.playbackTime; 85 | var parameters = {}; 86 | var bufferIndex = 0; 87 | var nextBufferIndex = 0; 88 | var i, imax; 89 | 90 | while (bufferIndex < bufferLength) { 91 | nextBufferIndex = bufferIndex + dspBufLength; 92 | 93 | for (i = 0, imax = numberOfInputs; i < imax; i++) { 94 | inputs[i] = e.inputBuffer.getChannelData(i).subarray(bufferIndex, nextBufferIndex); 95 | } 96 | 97 | for (i = 0, imax = numberOfOutputs; i < imax; i++) { 98 | outputs[i] = e.outputBuffer.getChannelData(i).subarray(bufferIndex, nextBufferIndex); 99 | } 100 | 101 | for (i = 0, imax = paramKeys.length; i < imax; i++) { 102 | parameters[paramKeys[i]] = paramBuffers[i].subarray(bufferIndex, nextBufferIndex); 103 | } 104 | 105 | audioprocess({ 106 | type: "audioprocess", 107 | playbackTime: playbackTime, 108 | node: processor, 109 | inputs: [ inputs ], 110 | outputs: [ outputs ], 111 | parameters: parameters, 112 | }); 113 | 114 | playbackTime += playbackTimeIncr; 115 | bufferIndex = nextBufferIndex; 116 | } 117 | }; 118 | 119 | return node; 120 | } 121 | 122 | module.exports = AudioWorkerNode; 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "audio-worker-node", 3 | "description": "AudioWorkerNode for audio-worker-shim", 4 | "version": "0.4.0", 5 | "author": "nao yonamine ", 6 | "bugs": { 7 | "url": "https://github.com/mohayonao/audio-worker-node/issues" 8 | }, 9 | "devDependencies": { 10 | "eslint": "^1.10.3", 11 | "eslint-config-mohayonao": "^0.1.0", 12 | "mocha": "^2.3.4", 13 | "sinon": "^1.17.2", 14 | "web-audio-test-api": "^0.5.2" 15 | }, 16 | "files": [ 17 | "package.json", 18 | "README.md", 19 | "index.js" 20 | ], 21 | "homepage": "http://mohayonao.github.io/audio-worker-node/", 22 | "keywords": [ 23 | "audioworker", 24 | "webaudioapi" 25 | ], 26 | "license": "MIT", 27 | "main": "index.js", 28 | "repository": { 29 | "type": "git", 30 | "url": "git://github.com/mohayonao/audio-worker-node.git" 31 | }, 32 | "scripts": { 33 | "lint": "eslint index.js test", 34 | "prepublish": "rm -rf lib && npm run lint && npm run test", 35 | "test": "mocha", 36 | "travis": "npm run lint && npm run test" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true, 4 | "es6": true 5 | }, 6 | "rules": { 7 | "comma-dangle": 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require("web-audio-test-api"); 2 | 3 | const assert = require("assert"); 4 | const sinon = require("sinon"); 5 | const AudioWorkerNode = require("../"); 6 | 7 | function noop() {} 8 | 9 | const DC1 = { 10 | name: "AudioBufferSourceNode", 11 | buffer: { 12 | name: "AudioBuffer", 13 | sampleRate: 44100, 14 | length: 8, 15 | duration: 8 / 44100, 16 | numberOfChannels: 1 17 | }, 18 | playbackRate: { 19 | value: 1, 20 | inputs: [] 21 | }, 22 | loop: true, 23 | loopStart: 0, 24 | loopEnd: 0, 25 | inputs: [] 26 | }; 27 | 28 | describe("AudioWorkerNode", () => { 29 | var audioContext; 30 | 31 | beforeEach(() => { 32 | audioContext = new AudioContext(); 33 | }); 34 | 35 | describe("constructor(audioContext: AudioContext, audioprocess: function, opts: object): ScriptProcessorNode", () => { 36 | it("works", () => { 37 | var node = new AudioWorkerNode(audioContext, noop, {}); 38 | 39 | assert(node instanceof ScriptProcessorNode); 40 | }); 41 | }); 42 | describe("audioprocess", () => { 43 | it("works", () => { 44 | var audioprocess = sinon.spy(); 45 | var processor = {}; 46 | var node = new AudioWorkerNode(audioContext, audioprocess, { 47 | parameters: [ 48 | { name: "frequency", defaultValue: 880 }, 49 | { name: "detune" }, 50 | ], 51 | processor: processor 52 | }); 53 | 54 | node.connect(audioContext.destination); 55 | 56 | audioContext.$processTo("00:00.500"); 57 | 58 | assert(0 < audioprocess.callCount); 59 | assert(audioprocess.args[0][0].type === "audioprocess"); 60 | assert(audioprocess.args[1][0].type === "audioprocess"); 61 | assert(audioprocess.args[0][0].playbackTime < audioprocess.args[1][0].playbackTime); 62 | assert(audioprocess.args[0][0].node === processor); 63 | assert(audioprocess.args[0][0].node === audioprocess.args[1][0].node); 64 | }); 65 | }); 66 | describe("audio-graph", () => { 67 | it("works", () => { 68 | var node = new AudioWorkerNode(audioContext, noop, {}); 69 | 70 | node.connect(audioContext.destination); 71 | 72 | assert.deepEqual(audioContext.destination.toJSON(), { 73 | name: "AudioDestinationNode", 74 | inputs: [ 75 | { 76 | name: "ScriptProcessorNode", 77 | inputs: [] 78 | } 79 | ] 80 | }); 81 | }); 82 | it("works with parameters", () => { 83 | var node = new AudioWorkerNode(audioContext, noop, { 84 | parameters: [ 85 | { name: "frequency", defaultValue: 880 }, 86 | { name: "detune" } 87 | ] 88 | }); 89 | 90 | node.connect(audioContext.destination); 91 | 92 | assert.deepEqual(audioContext.destination.toJSON(), { 93 | name: "AudioDestinationNode", 94 | inputs: [ 95 | { 96 | name: "ScriptProcessorNode", 97 | inputs: [ 98 | { 99 | name: "ScriptProcessorNode", 100 | inputs: [ 101 | { 102 | name: "ChannelMergerNode", 103 | inputs: [ 104 | [ 105 | { 106 | name: "GainNode", 107 | gain: { 108 | value: 880, 109 | inputs: [] 110 | }, 111 | inputs: [ DC1 ] 112 | } 113 | ], 114 | [ 115 | { 116 | name: "GainNode", 117 | gain: { 118 | value: 0, 119 | inputs: [] 120 | }, 121 | inputs: [ DC1 ] 122 | } 123 | ] 124 | ] 125 | } 126 | ] 127 | } 128 | ] 129 | } 130 | ] 131 | }); 132 | }); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --recursive 3 | --------------------------------------------------------------------------------