├── .travis.yml ├── .gitignore ├── pull.js ├── index.js ├── stream.js ├── sink.js ├── source.js ├── process.md ├── LICENSE ├── readable.js ├── .eslintrc.json ├── package.json ├── read.js ├── writable.js ├── test.js ├── write.js └── readme.md /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 'node' 5 | - '6' 6 | - '5' 7 | - '4' 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | pids 10 | logs 11 | results 12 | npm-debug.log 13 | node_modules 14 | .idea -------------------------------------------------------------------------------- /pull.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module web-audio-stream/pull 3 | */ 4 | 'use strict'; 5 | 6 | const sink = require('./sink'); 7 | const source = require('./source'); 8 | 9 | sink.sink = sink; 10 | sink.source = source; 11 | module.exports = sink; 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module web-audio-stream 3 | */ 4 | 'use strict'; 5 | 6 | const Writer = require('./write'); 7 | const Reader = require('./read'); 8 | Writer.Writer = Writer; 9 | Writer.Reader = Reader; 10 | 11 | module.exports = Writer; 12 | -------------------------------------------------------------------------------- /stream.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module web-audio-stream/stream 3 | */ 4 | 'use strict'; 5 | 6 | var Writable = require('./writable'); 7 | var Readable = require('./readable'); 8 | 9 | Writable.Writable = Writable; 10 | Writable.Readable = Readable; 11 | 12 | module.exports = Writable; 13 | -------------------------------------------------------------------------------- /sink.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module web-audio-stream/sink 3 | * 4 | * Sink pull-stream for web-audio 5 | */ 6 | 'use strict'; 7 | 8 | 9 | var pull = require('pull-stream/pull'); 10 | var asyncMap = require('pull-stream/throughs/async-map'); 11 | var drain = require('pull-stream/sinks/drain'); 12 | var createWriter = require('./write'); 13 | 14 | module.exports = sink; 15 | 16 | 17 | function sink (node, options) { 18 | let write = createWriter(node, options); 19 | let d = drain(); 20 | 21 | let stream = pull(asyncMap(write), d); 22 | 23 | stream.abort = () => { 24 | write.end(); 25 | d.abort(); 26 | }; 27 | 28 | return stream; 29 | } 30 | -------------------------------------------------------------------------------- /source.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module web-audio-stream/source 3 | * 4 | * Source pull-stream for web-audio node 5 | */ 6 | 'use strict'; 7 | 8 | var createReader = require('./read'); 9 | 10 | module.exports = source; 11 | 12 | 13 | function source (node, options) { 14 | let read = createReader(node, options); 15 | let ended = false; 16 | 17 | let stream = function (end, cb) { 18 | if (end || ended) { 19 | if (!ended) { 20 | read.end(); 21 | } 22 | 23 | ended = true; 24 | return cb && cb(true) 25 | } 26 | 27 | return read(cb); 28 | } 29 | 30 | stream.abort = () => { 31 | ended = true; 32 | read.end(); 33 | } 34 | 35 | return stream; 36 | } 37 | -------------------------------------------------------------------------------- /process.md: -------------------------------------------------------------------------------- 1 | ## Q: Should we make it a separate class or a part of audio-through? 2 | + Yes. Make writable stream implementation with input audio-buffer/ndsample/float32array/arraybuffer detector. No need for audio-through. 3 | 4 | ## Q: what is supposed API? 5 | * Plain data wrapper, like `inst.feed(data);` 6 | - What is the difference with AudioBuffer then? 7 | * Stream instance `pipe(inst).connect(dest)` 8 | - Optimized piping is able only with audio-through as it operates buffers 9 | + stream makes more sense, as basically if something wants more data - it is stream API 10 | + Writable stream has .write method, allowing for direct writing data to the stack. 11 | 12 | ## Q: how do we keep pipe and connect at the same time? 13 | * We send data for both. We just need to bufferize data for web-audio-stream, because real stream can be faster. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2018 DY 3 | 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /readable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module web-audio-stram/readable 3 | * 4 | * Pipe web-audio to stream 5 | */ 6 | 7 | 'use strict'; 8 | 9 | 10 | const inherits = require('inherits'); 11 | const Readable = require('stream').Readable; 12 | const createReader = require('./read'); 13 | 14 | module.exports = WAAReadable; 15 | 16 | 17 | inherits(WAAReadable, Readable); 18 | 19 | 20 | //@constructor 21 | function WAAReadable (node, options) { 22 | if (!(this instanceof WAAReadable)) return new WAAReadable(node, options); 23 | 24 | let read = createReader(node, options); 25 | 26 | Readable.call(this, { 27 | objectMode: true, 28 | 29 | //to keep processing delays very short, in case of RT binding. 30 | //otherwise each stream will hoard data and release only when it’s full. 31 | highWaterMark: 0, 32 | 33 | read: function (size) { 34 | if (size === null) read.end(); 35 | 36 | read((err, buffer) => { 37 | if (!err) this.push(buffer); 38 | }); 39 | } 40 | }); 41 | 42 | this.end = function () { 43 | read.end(); 44 | return this; 45 | } 46 | } 47 | 48 | // WAAReadable.WORKER_MODE = 2; 49 | // WAAReadable.ANALYZER_MODE = 0; 50 | WAAReadable.SCRIPT_MODE = 1; 51 | 52 | WAAReadable.prototype.mode = WAAReadable.prototype.SCRIPT_MODE; 53 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "commonjs": true, 6 | "es6": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "rules": { 10 | "strict": 2, 11 | "indent": 0, 12 | "linebreak-style": 0, 13 | "quotes": 0, 14 | "semi": 0, 15 | "no-cond-assign": 1, 16 | "no-console": 1, 17 | "no-constant-condition": 1, 18 | "no-duplicate-case": 1, 19 | "no-empty": 1, 20 | "no-ex-assign": 1, 21 | "no-extra-boolean-cast": 1, 22 | "no-extra-semi": 1, 23 | "no-fallthrough": 1, 24 | "no-func-assign": 1, 25 | "no-global-assign": 1, 26 | "no-implicit-globals": 2, 27 | "no-inner-declarations": ["error", "functions"], 28 | "no-irregular-whitespace": 2, 29 | "no-loop-func": 1, 30 | "no-multi-str": 1, 31 | "no-mixed-spaces-and-tabs": 1, 32 | "no-proto": 1, 33 | "no-sequences": 1, 34 | "no-throw-literal": 1, 35 | "no-unmodified-loop-condition": 1, 36 | "no-useless-call": 1, 37 | "no-void": 1, 38 | "no-with": 2, 39 | "wrap-iife": 1, 40 | "no-redeclare": 1, 41 | "no-unused-vars": ["error", { "vars": "all", "args": "none" }], 42 | "no-sparse-arrays": 1 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-audio-stream", 3 | "version": "3.0.1", 4 | "description": "Interface between Web Audio API and Streams", 5 | "main": "./index.js", 6 | "scripts": { 7 | "preversion": "npm run lint", 8 | "lint": "eslint *.js --ignore-pattern test*", 9 | "test": "echo No tests for node available", 10 | "test:browser": "budo test.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/audiojs/web-audio-stream.git" 15 | }, 16 | "keywords": [ 17 | "waa", 18 | "web-audio", 19 | "dsp", 20 | "stream", 21 | "pcm", 22 | "audio" 23 | ], 24 | "author": "DY ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/audiojs/web-audio-stream/issues" 28 | }, 29 | "homepage": "https://github.com/audiojs/web-audio-stream#readme", 30 | "devDependencies": { 31 | "audio-buffer": "^3.1.1", 32 | "audio-generator": "^2.0.3", 33 | "audio-speaker": "^1.2.4", 34 | "audio-through": "^2.1.0", 35 | "tape": "^4.6.3" 36 | }, 37 | "dependencies": { 38 | "audio-buffer-list": "^2.0.6", 39 | "audio-buffer-utils": "^4.3.0", 40 | "audio-context": "^1.0.0", 41 | "inherits": "^2.0.1", 42 | "is-audio-buffer": "^1.0.1", 43 | "is-plain-obj": "^1.1.0", 44 | "object-assign": "^4.1.1", 45 | "pcm-util": "^2.1.0", 46 | "pull-stream": "^3.4.4" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /read.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module web-audio-stream/reader 3 | * 4 | * Read data from web-audio 5 | */ 6 | 7 | 'use strict'; 8 | 9 | const extend = require('object-assign'); 10 | 11 | module.exports = WAAReader; 12 | 13 | 14 | 15 | //@constructor 16 | function WAAReader (sourceNode, options) { 17 | if (!sourceNode || !sourceNode.context) throw Error('Pass AudioNode instance first argument'); 18 | 19 | if (!options) { 20 | options = {}; 21 | } 22 | 23 | let context = sourceNode.context; 24 | 25 | options = extend({ 26 | //the only available option for now 27 | mode: WAAReader.SCRIPT_MODE, 28 | 29 | samplesPerFrame: 1024 30 | }, options); 31 | 32 | 33 | let release; 34 | 35 | //TODO: gate by SCRIPT_MODE 36 | let node = context.createScriptProcessor(options.samplesPerFrame, options.channels, options.channels); 37 | 38 | node.addEventListener('audioprocess', e => { 39 | let cb = release; 40 | release = null; 41 | cb && cb(null, e.inputBuffer); 42 | }); 43 | 44 | //scriptProcessor is active only being connected to output 45 | sourceNode.connect(node); 46 | node.connect(context.destination); 47 | 48 | read.end = function () { 49 | node.disconnect(); 50 | release = null; 51 | } 52 | 53 | return read; 54 | 55 | function read (cb) { 56 | if (cb === null) return read.end(); 57 | 58 | release = cb; 59 | } 60 | 61 | } 62 | 63 | // WAAReader.WORKER_MODE = 2; 64 | // WAAReader.ANALYZER_MODE = 0; 65 | WAAReader.SCRIPT_MODE = 1; 66 | -------------------------------------------------------------------------------- /writable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module web-audio-stream/writable 3 | * 4 | * Write stream data to web-audio. 5 | */ 6 | 'use strict'; 7 | 8 | 9 | var inherits = require('inherits'); 10 | var Writable = require('stream').Writable; 11 | var createWriter = require('./write'); 12 | 13 | module.exports = WAAWritable; 14 | 15 | 16 | /** 17 | * @constructor 18 | */ 19 | function WAAWritable (node, options) { 20 | if (!(this instanceof WAAWritable)) return new WAAWritable(node, options); 21 | 22 | let write = createWriter(node, options); 23 | 24 | Writable.call(this, { 25 | //we need object mode to recognize any type of input 26 | objectMode: true, 27 | 28 | //to keep processing delays very short, in case of RT binding. 29 | //otherwise each stream will hoard data and release only when it’s full. 30 | highWaterMark: 0, 31 | 32 | write: (chunk, enc, cb) => { 33 | return write(chunk, cb); 34 | } 35 | }); 36 | 37 | 38 | //manage input pipes number 39 | this.inputsCount = 0; 40 | this.on('pipe', (source) => { 41 | this.inputsCount++; 42 | 43 | //do autoend 44 | source.once('end', () => { 45 | this.end() 46 | }); 47 | 48 | }).on('unpipe', (source) => { 49 | this.inputsCount--; 50 | }) 51 | 52 | //end writer 53 | this.once('end', () => { 54 | write.end() 55 | }) 56 | } 57 | 58 | 59 | inherits(WAAWritable, Writable); 60 | 61 | 62 | /** 63 | * Rendering modes 64 | */ 65 | WAAWritable.WORKER_MODE = 2; 66 | WAAWritable.SCRIPT_MODE = 1; 67 | WAAWritable.BUFFER_MODE = 0; 68 | 69 | 70 | /** 71 | * There is an opinion that script mode is better. 72 | * https://github.com/brion/audio-feeder/issues/13 73 | * 74 | * But for me there are moments of glitch when it infinitely cycles sound. Very disappointing and makes feel desperate. 75 | * 76 | * But buffer mode also tend to create noisy clicks. Not sure why, cannot remove that. 77 | * With script mode I at least defer my responsibility. 78 | */ 79 | WAAWritable.prototype.mode = WAAWritable.SCRIPT_MODE; 80 | 81 | 82 | /** Count of inputs */ 83 | WAAWritable.prototype.inputsCount = 0; 84 | 85 | 86 | /** 87 | * Overrides stream’s end to ensure event. 88 | */ 89 | //FIXME: not sure why `end` is triggered here like 10 times. 90 | WAAWritable.prototype.end = function () { 91 | if (this.isEnded) return; 92 | 93 | this.isEnded = true; 94 | 95 | var triggered = false; 96 | this.once('end', () => { 97 | triggered = true; 98 | }); 99 | Writable.prototype.end.call(this); 100 | 101 | //timeout cb, because native end emits after a tick 102 | setTimeout(() => { 103 | if (!triggered) { 104 | this.emit('end'); 105 | } 106 | }); 107 | 108 | return this; 109 | }; 110 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var context = require('audio-context')(); 3 | var Writable = require('./writable'); 4 | var Readable = require('./readable'); 5 | var Writer = require('./write'); 6 | var Reader = require('./read'); 7 | var AudioBuffer = require('audio-buffer'); 8 | var util = require('audio-buffer-utils'); 9 | var Generator = require('audio-generator'); 10 | var Generate = require('audio-generator/index.js') 11 | var Speaker = require('audio-speaker'); 12 | var assert = require('assert'); 13 | var Sink = require('./sink'); 14 | var Source = require('./source'); 15 | var pull = require('pull-stream'); 16 | 17 | //TODO: make node tests 18 | 19 | test('Writer', function (t) { 20 | let frame = 1024; 21 | let write = Writer(context.destination, { 22 | samplesPerFrame: 1024 23 | }); 24 | let generate = Generate(t => Math.sin(440 * t * Math.PI * 2)); 25 | 26 | let isStopped = 0; 27 | setTimeout(() => { 28 | isStopped = 1; 29 | }, 500); 30 | function gen (err) { 31 | if (err) throw err; 32 | if (isStopped) { 33 | write(null); 34 | t.end() 35 | return; 36 | } 37 | let buf = generate(util.create(frame)); 38 | write(buf, gen); 39 | } 40 | gen(); 41 | }); 42 | 43 | test('Reader', function (t) { 44 | let oscNode = context.createOscillator(); 45 | oscNode.type = 'sawtooth'; 46 | oscNode.frequency.value = 440; 47 | oscNode.start(); 48 | 49 | let read = Reader(oscNode); 50 | 51 | let count = 0; 52 | 53 | read(function getData(err, buff) { 54 | assert.notEqual(buff.getChannelData(0)[1], 0); 55 | if (++count >= 5) { 56 | read(null); 57 | } 58 | else { 59 | read(getData); 60 | } 61 | }); 62 | 63 | setTimeout(() => { 64 | assert.equal(count, 5); 65 | t.end(); 66 | }, 200); 67 | }); 68 | 69 | 70 | test('Write AudioBuffer', function (t) { 71 | var stream = Writable(context.destination); 72 | // stream.connect(context.destination); 73 | 74 | var buf = new AudioBuffer(1024*8); 75 | util.noise(buf); 76 | stream.write(buf); 77 | 78 | setTimeout(function () { 79 | stream.end(); 80 | t.end(); 81 | }, 300); 82 | }); 83 | 84 | test('Write Float32Array', function (t) { 85 | var stream = Writable(context.destination); 86 | 87 | var buf = new AudioBuffer(1024*8); 88 | util.noise(buf); 89 | 90 | stream.write(buf.getChannelData(0)); 91 | 92 | setTimeout(function () { 93 | stream.end(); 94 | t.end(); 95 | }, 300); 96 | }); 97 | 98 | test('Write Array', function (t) { 99 | var stream = Writable(context.destination, {channels: 1}); 100 | 101 | var a = Array(1024).fill(0).map(function () {return Math.random()}); 102 | 103 | stream.write(a); 104 | 105 | setTimeout(function () { 106 | stream.end(); 107 | t.end(); 108 | }, 300); 109 | }); 110 | 111 | test('Write ArrayBuffer', function (t) { 112 | var stream = Writable(context.destination); 113 | 114 | var buf = new AudioBuffer(1024*8); 115 | util.noise(buf); 116 | 117 | stream.write(buf.getChannelData(0).buffer); 118 | 119 | setTimeout(function () { 120 | stream.end(); 121 | t.end(); 122 | }, 300); 123 | }); 124 | 125 | 126 | test('Write Buffer', function (t) { 127 | var stream = Writable(context.destination); 128 | 129 | var buf = new AudioBuffer(1024*8); 130 | util.noise(buf); 131 | 132 | buf = new Buffer(buf.getChannelData(0).buffer); 133 | 134 | stream.write(buf); 135 | 136 | setTimeout(function () { 137 | stream.end(); 138 | t.end(); 139 | }, 300); 140 | }); 141 | 142 | 143 | test('Writable stream', function (t) { 144 | Generator(function (time) { 145 | return Math.sin(Math.PI * 2 * 440 * time); 146 | }, {duration: 0.5}) 147 | .pipe(Writable(context.destination)) 148 | .on('end', t.end) 149 | }); 150 | 151 | test('Chain of sound processing', function (t) { 152 | var panner = context.createStereoPanner(); 153 | panner.pan.value = -1; 154 | 155 | var stream = Writable(panner); 156 | 157 | Generator(function (time) { 158 | return Math.sin(Math.PI * 2 * 220 * time); 159 | }, {duration: 1}) 160 | .pipe(stream) 161 | .on('end', t.end) 162 | 163 | // stream.connect(); 164 | 165 | panner.connect(context.destination); 166 | }); 167 | 168 | test('Delayed connection/start'); 169 | 170 | 171 | test('Readable stream', function (t) { 172 | let oscNode = context.createOscillator(); 173 | oscNode.type = 'sawtooth'; 174 | oscNode.frequency.value = 440; 175 | oscNode.start(); 176 | 177 | let count = 0; 178 | let stream = Readable(oscNode).on('data', x => { 179 | assert.notEqual(x.getChannelData(0)[1], 0); 180 | if (++count >= 5) stream.end(); 181 | }); 182 | 183 | setTimeout(() => { 184 | assert.equal(count, 5); 185 | t.end(); 186 | }, 200); 187 | }); 188 | 189 | 190 | test('Pull stream sink', function (t) { 191 | let generate = Generate(Math.random); 192 | let source = pull.infinite(generate); 193 | let sink = Sink(context.destination); 194 | 195 | pull( 196 | source, 197 | // pull.take(10), 198 | sink 199 | ); 200 | 201 | setTimeout(() => { 202 | sink.abort(); 203 | t.end(); 204 | }, 200); 205 | }); 206 | 207 | test('Pull stream source', function (t) { 208 | let oscNode = context.createOscillator(); 209 | oscNode.type = 'sawtooth'; 210 | oscNode.frequency.value = 440; 211 | oscNode.start(); 212 | 213 | let count = 0; 214 | let stream = Source(oscNode); 215 | 216 | pull( 217 | stream, 218 | pull.map(buf => { 219 | assert.notEqual(buf.getChannelData(0)[1], 0); 220 | if (++count >= 5) stream.abort(); 221 | return buf; 222 | }), 223 | pull.drain() 224 | ); 225 | 226 | setTimeout(() => { 227 | assert.equal(count, 5); 228 | t.end(); 229 | }, 200); 230 | }); 231 | 232 | 233 | // test('Readable stream processing', function () { 234 | // let AppAudio = require('app-audio'); 235 | // let appAudio = AppAudio({ 236 | // source: 'sine', 237 | // }).on('ready', () => { 238 | 239 | // }) 240 | // }); 241 | -------------------------------------------------------------------------------- /write.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module web-audio-stream/write 3 | * 4 | * Write data to web-audio. 5 | */ 6 | 'use strict'; 7 | 8 | 9 | const extend = require('object-assign') 10 | const pcm = require('pcm-util') 11 | const util = require('audio-buffer-utils') 12 | const isAudioBuffer = require('is-audio-buffer') 13 | const AudioBufferList = require('audio-buffer-list') 14 | 15 | module.exports = WAAWriter; 16 | 17 | 18 | /** 19 | * Rendering modes 20 | */ 21 | WAAWriter.WORKER_MODE = 2; 22 | WAAWriter.SCRIPT_MODE = 1; 23 | WAAWriter.BUFFER_MODE = 0; 24 | 25 | 26 | /** 27 | * @constructor 28 | */ 29 | function WAAWriter (target, options) { 30 | if (!target || !target.context) throw Error('Pass AudioNode instance first argument') 31 | 32 | if (!options) { 33 | options = {}; 34 | } 35 | 36 | options.context = target.context; 37 | 38 | options = extend({ 39 | /** 40 | * There is an opinion that script mode is better. 41 | * https://github.com/brion/audio-feeder/issues/13 42 | * 43 | * But for me there are moments of glitch when it infinitely cycles sound. Very disappointing and makes feel desperate. 44 | * 45 | * But buffer mode also tend to create noisy clicks. Not sure why, cannot remove that. 46 | * With script mode I at least defer my responsibility. 47 | */ 48 | mode: WAAWriter.SCRIPT_MODE, 49 | samplesPerFrame: pcm.defaults.samplesPerFrame, 50 | 51 | //FIXME: take this from input node 52 | channels: pcm.defaults.channels 53 | }, options) 54 | 55 | //ensure input format 56 | let format = pcm.format(options) 57 | pcm.normalize(format) 58 | 59 | let context = options.context; 60 | let channels = options.channels; 61 | let samplesPerFrame = options.samplesPerFrame; 62 | let sampleRate = context.sampleRate; 63 | let node, release, isStopped, isEmpty = false; 64 | 65 | //queued data to send to output 66 | let data = new AudioBufferList(0, channels) 67 | 68 | //init proper mode 69 | if (options.mode === WAAWriter.SCRIPT_MODE) { 70 | node = initScriptMode() 71 | } 72 | else if (options.mode === WAAWriter.BUFFER_MODE) { 73 | node = initBufferMode() 74 | } 75 | else { 76 | throw Error('Unknown mode. Choose from BUFFER_MODE or SCRIPT_MODE') 77 | } 78 | 79 | //connect node 80 | node.connect(target) 81 | 82 | write.end = () => { 83 | if (isStopped) return; 84 | node.disconnect() 85 | isStopped = true; 86 | } 87 | 88 | return write; 89 | 90 | //return writer function 91 | function write (buffer, cb) { 92 | if (isStopped) return; 93 | 94 | if (buffer == null) { 95 | return write.end() 96 | } 97 | else { 98 | push(buffer) 99 | } 100 | release = cb; 101 | } 102 | 103 | 104 | //push new data for the next WAA dinner 105 | function push (chunk) { 106 | if (!isAudioBuffer(chunk)) { 107 | chunk = util.create(chunk, channels) 108 | } 109 | 110 | data.append(chunk) 111 | 112 | isEmpty = false; 113 | } 114 | 115 | //get last ready data 116 | function shift (size) { 117 | size = size || samplesPerFrame; 118 | 119 | //if still empty - return existing buffer 120 | if (isEmpty) return data; 121 | 122 | let output = data.slice(0, size) 123 | 124 | data.consume(size) 125 | 126 | //if size is too small, fill with silence 127 | if (output.length < size) { 128 | output = util.pad(output, size) 129 | } 130 | 131 | return output; 132 | } 133 | 134 | /** 135 | * Init scriptProcessor-based rendering. 136 | * Each audioprocess event triggers tick, which releases pipe 137 | */ 138 | function initScriptMode () { 139 | //buffer source node 140 | let bufferNode = context.createBufferSource() 141 | bufferNode.loop = true; 142 | bufferNode.buffer = util.create(samplesPerFrame, channels, {context: context}) 143 | 144 | node = context.createScriptProcessor(samplesPerFrame) 145 | node.addEventListener('audioprocess', function (e) { 146 | //release causes synchronous pulling the pipeline 147 | //so that we get a new data chunk 148 | let cb = release; 149 | release = null; 150 | cb && cb() 151 | 152 | if (isStopped) return; 153 | 154 | util.copy(shift(e.inputBuffer.length), e.outputBuffer) 155 | }) 156 | 157 | //start should be done after the connection, or there is a chance it won’t 158 | bufferNode.connect(node) 159 | bufferNode.start() 160 | 161 | return node; 162 | } 163 | 164 | 165 | /** 166 | * Buffer-based rendering. 167 | * The schedule is triggered by setTimeout. 168 | */ 169 | function initBufferMode () { 170 | //how many times output buffer contains input one 171 | let FOLD = 2; 172 | 173 | //buffer source node 174 | node = context.createBufferSource() 175 | node.loop = true; 176 | node.buffer = util.create(samplesPerFrame * FOLD, channels, {context: node.context}) 177 | 178 | //output buffer 179 | let buffer = node.buffer; 180 | 181 | //audio buffer realtime ticked cycle 182 | //FIXME: find a way to receive target starving callback here instead of unguaranteed timeouts 183 | setTimeout(tick) 184 | 185 | node.start() 186 | 187 | //last played count, position from which there is no data filled up 188 | let lastCount = 0; 189 | 190 | //time of start 191 | //FIXME: find out why and how this magic coefficient affects buffer scheduling 192 | let initTime = context.currentTime; 193 | 194 | return node; 195 | 196 | //tick function - if the half-buffer is passed - emit the tick event, which will fill the buffer 197 | function tick (a) { 198 | if (isStopped) return; 199 | 200 | let playedTime = context.currentTime - initTime; 201 | let playedCount = playedTime * sampleRate; 202 | 203 | //if offset has changed - notify processor to provide a new piece of data 204 | if (lastCount - playedCount < samplesPerFrame) { 205 | //send queued data chunk to buffer 206 | util.copy(shift(samplesPerFrame), buffer, lastCount % buffer.length) 207 | 208 | //increase rendered count 209 | lastCount += samplesPerFrame; 210 | 211 | //if there is a holding pressure control - release it 212 | if (release) { 213 | let cb = release; 214 | release = null; 215 | cb() 216 | } 217 | 218 | //call tick extra-time in case if there is a room for buffer 219 | //it will plan timeout, if none 220 | tick() 221 | } 222 | //else plan tick for the expected time of starving 223 | else { 224 | //time of starving is when played time reaches (last count time) - half-duration 225 | let starvingTime = (lastCount - samplesPerFrame) / sampleRate; 226 | let remainingTime = starvingTime - playedTime; 227 | setTimeout(tick, remainingTime * 1000) 228 | } 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # web-audio-stream [![Build Status](https://travis-ci.org/audiojs/web-audio-stream.svg?branch=master)](https://travis-ci.org/audiojs/web-audio-stream) [![Greenkeeper badge](https://badges.greenkeeper.io/audiojs/web-audio-stream.svg)](https://greenkeeper.io/) [![stable](https://img.shields.io/badge/stability-unstable-orange.svg)](http://github.com/badges/stability-badges) 2 | 3 | Interface between Web Audio API and streams. Send AudioBuffer/Buffer/ArrayBuffer/FloatArray data to Web Audio API (writable mode) or connect any AudioNode to stream (readable mode). There are three types of connection available: as [plain functions](#API), as [streams](#Stream) or [pull-streams](#pull). 4 | 5 | ## Usage 6 | 7 | [![npm install web-audio-stream](https://nodei.co/npm/web-audio-stream.png?mini=true)](https://npmjs.org/package/web-audio-stream/) 8 | 9 | ```js 10 | const context = require('audio-context') 11 | const Generator = require('audio-generator') 12 | const {Readable, Writable} = require('web-audio-stream/stream') 13 | 14 | let oscillator = context.createOscillator() 15 | oscillator.type = 'sawtooth' 16 | oscillator.frequency.value = 440 17 | oscillator.start() 18 | 19 | //pipe oscillator audio data to stream 20 | Readable(oscillator).on('data', (audioBuffer) => { 21 | console.log(audioBuffer.getChannelData(0)) 22 | }) 23 | 24 | //pipe generator stream to audio destination 25 | Generator(time => Math.sin(Math.PI * 2 * time * 440)) 26 | .pipe(Writable(context.destination)) 27 | ``` 28 | 29 | ## API 30 | 31 | ### `const {Read, Write} = require('web-audio-stream')` 32 | 33 | Get Web-Audio-API reader or writer constructors. They can be required separately: 34 | 35 | ```js 36 | const createReader = require('web-audio-stream/read') 37 | const createWriter = require('web-audio-stream/write') 38 | ``` 39 | 40 | ### `let write = Write(destNode, options?)` 41 | 42 | Create function writing to web-audio-API AudioNode with the following signature: `write(audioBuffer, (err) => {})`. To end stream properly, call `write(null)`. 43 | 44 | `options` parameter is optional and may provide the following: 45 | 46 | * `mode` − 0 or 1, defines buffer or script mode of feeding data, may affect performance insignificantly. 47 | * `context` − audio context, defaults to [audio-context](https://npmjs.org/package/audio-context) module. 48 | * `samplesPerFrame` and `channels` define audio buffer params. 49 | 50 | ```js 51 | const Writer = require('web-audio-stream/write') 52 | const Generate = require('audio-generator') 53 | const util = require('audio-buffer-utils') 54 | 55 | let write = Writer(context.destination, { 56 | samplesPerFrame: 1024 57 | }) 58 | let generate = Generate(t => Math.sin(440 * t * Math.PI * 2)) 59 | 60 | //add stopper 61 | let isStopped = 0 62 | setTimeout(() => { 63 | isStopped = 1 64 | }, 500) 65 | 66 | function gen (err) { 67 | if (err) throw err 68 | if (isStopped) { 69 | write(null) 70 | return 71 | } 72 | //generate new audio buffer 73 | let aBuf = generate(util.create(frame)) 74 | 75 | //send audio buffer to audio node 76 | write(aBuf, gen) 77 | } 78 | gen() 79 | ``` 80 | 81 | Writer is smart enough to recognize any type of data placed into it: [AudioBuffer](https://github.com/audiojs/audio-buffer), [AudioBufferList](https://github.com/audiojs/audio-buffer-list), ArrayBuffer, FloatArray, Buffer, Array. Make sure only that passed buffer format complies with passed options, ie. `samplerPerFrame`, `channels` etc. 82 | 83 | Note on performance. Internally writer uses [audio-buffer-list](https://github.com/audiojs/audio-buffer-list) to manage memory efficiently, providing pretty low latency. 84 | 85 | ### `let read = Read(sourceNode, options?)` 86 | 87 | Create reader from web _AudioNode_ with signature `read((err, audioBuffer) => {})`, returning audio frames data. To end reading, pass `read(null)`. 88 | 89 | ```js 90 | const Reader = require('web-audio-stream/read') 91 | 92 | let oscNode = context.createOscillator() 93 | oscNode.type = 'sawtooth' 94 | oscNode.frequency.value = 440 95 | oscNode.start() 96 | 97 | let read = Reader(oscNode) 98 | 99 | let count = 0 100 | 101 | read(function getData(err, audioBuffer) { 102 | //output audioBuffer here or whatever 103 | 104 | if (count++ >= 5) { 105 | //end after 5th frame 106 | read(null) 107 | } 108 | else { 109 | read(getData) 110 | } 111 | }) 112 | ``` 113 | 114 | ## Stream API 115 | 116 | ### `const {Readable, Writable} = require('web-audio-stream/stream')` 117 | 118 | Get readable or writable stream to pipe data from stream to web-audio directly. 119 | 120 | ```js 121 | //web-audio → stream 122 | const Readable = require('web-audio-stream/readable') 123 | 124 | //stream → web-audio 125 | const Writable = require('web-audio-stream/writable') 126 | ``` 127 | 128 | ### `let writable = Writable(destNode, options?)` 129 | 130 | Create writer to web audio node, possibly based on options. Pipe any stream to writable, or write data directly to it, basically it implements _Writable_ stream class. 131 | 132 | ```js 133 | const Writable = require('web-audio-stream/writable') 134 | const context = require('audio-context') 135 | 136 | //options or single properties are optional 137 | let writable = Writable(context.destination, { 138 | context: context, 139 | channels: 2, 140 | sampleRate: context.sampleRate, 141 | 142 | //BUFFER_MODE, SCRIPT_MODE, WORKER_MODE (pending web-audio-workers) 143 | mode: Writable.BUFFER_MODE, 144 | 145 | //disconnect node if input stream ends 146 | autoend: true 147 | }) 148 | 149 | 150 | const Generator = require('audio-generator') 151 | let src = Generator(function (time) { 152 | return Math.sin(Math.PI * 2 * time * 440) 153 | }) 154 | src.pipe(writable) 155 | 156 | 157 | //or simply send data to web-audio 158 | let chunk = new Float32Array(1024) 159 | for (let i = 0; i < 1024; i++) { 160 | chunk[i] = Math.random() 161 | } 162 | writable.write(chunk) 163 | 164 | setTimeout(writable.end, 1000) 165 | ``` 166 | 167 | ### `let readable = Readable(audioNode, options?)` 168 | 169 | Readable stream to read data from web-audio-API. 170 | 171 | ```js 172 | const Readable = require('web-audio-stream/readable') 173 | 174 | let readable = Readable(myNode) 175 | readable.on('data', buffer => { 176 | console.log('Got audio buffer') 177 | }) 178 | ``` 179 | 180 | ## Pull-stream API 181 | 182 | Pull-stream interfaces internally use plain reader and writer and can be used in the same fashion. 183 | 184 | ### `const {sink, source} = require('web-audio-stream/pull')` 185 | 186 | ```js 187 | const Sink = require('web-audio-stream/sink') 188 | const Source = require('web-audio-stream/source') 189 | ``` 190 | 191 | ### `let sink = Sink(destNode, options?)` 192 | ### `let source = Source(srcNode, options?)` 193 | 194 | 195 | ## Related 196 | 197 | * [audio-speaker](https://github.com/audiojs/audio-speaker) — node/browser speaker stream. 198 | * [audio-through](https://github.com/audiojs/audio-through) — universal audio stream class. 199 | --------------------------------------------------------------------------------