├── cli.js ├── index.js ├── .travis.yml ├── browser-direct.js ├── pull.js ├── browser-stream.js ├── todo.md ├── stream.js ├── .eslintrc.json ├── package.json ├── .gitignore ├── direct.js ├── readme.md └── test.js /cli.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** @module audio-speaker */ 2 | 'use strict'; 3 | module.exports = require('./direct'); 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "6" 5 | - "5" 6 | - "4" 7 | - "0.12" 8 | - "0.10" 9 | matrix: 10 | fast_finish: true 11 | allow_failures: 12 | - node_js: "0.10" 13 | - node_js: "0.12" 14 | -------------------------------------------------------------------------------- /browser-direct.js: -------------------------------------------------------------------------------- 1 | /** @module audio-speaker/browser */ 2 | 3 | 'use strict'; 4 | 5 | var Writer = require('web-audio-stream/index'); 6 | var context = require('audio-context'); 7 | 8 | module.exports = Speaker; 9 | 10 | 11 | function Speaker(options) { 12 | let ctx = options && options.context || context(); 13 | 14 | return Writer(ctx.destination, options); 15 | } 16 | -------------------------------------------------------------------------------- /pull.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module audio-speaker/pull 3 | * 4 | */ 5 | 'use strict'; 6 | 7 | const Writer = require('./direct'); 8 | const drain = require('pull-stream/sinks/drain'); 9 | const asyncMap = require('pull-stream/throughs/async-map'); 10 | const pull = require('pull-stream/pull'); 11 | 12 | 13 | module.exports = function PullSpeaker (opts) { 14 | let sinkFn = Writer(opts); 15 | let d = drain(); 16 | 17 | let sink = pull(asyncMap(sinkFn), d); 18 | 19 | sink.abort = d.abort; 20 | 21 | return sink; 22 | } 23 | -------------------------------------------------------------------------------- /browser-stream.js: -------------------------------------------------------------------------------- 1 | /** @module audio-speaker/browser */ 2 | 3 | 'use strict'; 4 | 5 | var inherits = require('inherits'); 6 | var WAAStream = require('web-audio-stream/writable'); 7 | var context = require('audio-context'); 8 | 9 | 10 | module.exports = Speaker; 11 | 12 | 13 | inherits(Speaker, WAAStream); 14 | 15 | 16 | function Speaker(options) { 17 | if (!(this instanceof Speaker)) return new Speaker(options); 18 | 19 | let ctx = options && options.context || context(); 20 | 21 | WAAStream.call(this, ctx.destination, options); 22 | } 23 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | * Restrain buffers piping to node-speaker, bind to RT. Do not generate more than needed. 2 | * node-speaker restrains pressure, but with a 3s buffer - it should be able to be regulated. 3 | * replace node-speaker with good implementation. It fails on estimating big buffers 4 | * Decrease buffer, significantly. Make gain work realtime 5 | * Flash fallback 6 | * Create scriptProcessorNode mode. As an alternative. 7 | * For old browsers generate sound like t='data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgA',new Audio(t+btoa(t+S)).play() 8 | * http://www.p01.org/JS1K_Speech_Synthesizer/ 9 | 10 | * Detect audioBufferSize based on some performance measure, to avoid GC glitches 11 | * Test in Firefox, Opera, Safari, iOS Safari, IE, others. 12 | 13 | * Test variety of channels 14 | * Test different sample rates 15 | 16 | * CLI -------------------------------------------------------------------------------- /stream.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module audio-speaker 3 | * 4 | * Wraps node-speaker to ensure format. 5 | * 6 | */ 7 | 'use strict'; 8 | 9 | var inherits = require('inherits'); 10 | var Through = require('audio-through'); 11 | 12 | var NodeSpeaker; 13 | try { 14 | NodeSpeaker = require('speaker'); 15 | } catch (e) { 16 | console.warn('`speaker` package was not found. Using `audio-sink` instead.'); 17 | NodeSpeaker = require('audio-sink'); 18 | } 19 | 20 | /** 21 | * Speaker is just a format wrapper for node-speaker, 22 | * as node-speaker doesn’t support any input format in some platforms, like windows. 23 | * So we need to force the most safe format. 24 | * 25 | * @constructor 26 | */ 27 | function AudioSpeaker (opts) { 28 | if (!(this instanceof AudioSpeaker)) { 29 | return new AudioSpeaker(opts); 30 | } 31 | 32 | Through.call(this, opts); 33 | 34 | //create node-speaker with default options - the most cross-platform case 35 | this.speaker = new NodeSpeaker({ 36 | channels: this.channels 37 | }); 38 | 39 | this.pipe(this.speaker); 40 | } 41 | 42 | inherits(AudioSpeaker, Through); 43 | 44 | 45 | /** 46 | * Predefined format for node-speaker 47 | */ 48 | Object.assign(AudioSpeaker.prototype, { 49 | float: false, 50 | interleaved: true, 51 | bitDepth: 16, 52 | signed: true 53 | }); 54 | 55 | 56 | module.exports = AudioSpeaker; 57 | -------------------------------------------------------------------------------- /.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": "audio-speaker", 3 | "version": "1.5.1", 4 | "description": "Output audio data to speaker in browser/node", 5 | "main": "stream.js", 6 | "scripts": { 7 | "lint": "eslint *.js --ignore-pattern test*", 8 | "test:browser": "budo test.js", 9 | "test": "node test" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/audiojs/audio-speaker" 14 | }, 15 | "keywords": [ 16 | "audio", 17 | "audiojs", 18 | "pcm", 19 | "speaker", 20 | "web-audio", 21 | "sound", 22 | "sink" 23 | ], 24 | "browser": { 25 | "./direct.js": "./browser-direct.js", 26 | "./stream.js": "./browser-stream.js" 27 | }, 28 | "author": "Dima Yv ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/audiojs/audio-speaker/issues" 32 | }, 33 | "homepage": "https://github.com/audiojs/audio-speaker", 34 | "devDependencies": { 35 | "audio-buffer-utils": "^4.0.0", 36 | "audio-context": "^1.0.0", 37 | "audio-generator": "^2.0.1", 38 | "eslint": "^3.4.0", 39 | "insert-styles": "^1.2.1", 40 | "pcm-util": "^2.0.2", 41 | "pcm-volume": "^1.0.0", 42 | "tape": "^4.6.3", 43 | "tst": "^1.1.8", 44 | "wavefont": "^1.2.1" 45 | }, 46 | "optionalDependencies": { 47 | "audio-sink": "^1.0.2", 48 | "speaker": "^0.5.3" 49 | }, 50 | "dependencies": { 51 | "audio-through": "^2.2.3", 52 | "inherits": "^2.0.1", 53 | "is-audio-buffer": "^1.0.1", 54 | "pcm-util": "^2.1.0", 55 | "pull-stream": "^3.4.5", 56 | "web-audio-stream": "^3.0.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | //this will affect all the git repos 2 | git config --global core.excludesfile ~/.gitignore 3 | 4 | *.otf 5 | 6 | //update files since .ignore won't if already tracked 7 | git rm --cached 8 | 9 | # Compiled source # 10 | ################### 11 | *.com 12 | *.class 13 | *.dll 14 | *.exe 15 | *.o 16 | *.so 17 | 18 | # Packages # 19 | ############ 20 | # it's better to unpack these files and commit the raw source 21 | # git has its own built in compression methods 22 | *.7z 23 | *.dmg 24 | *.gz 25 | *.iso 26 | *.jar 27 | *.rar 28 | *.tar 29 | *.zip 30 | 31 | # Logs and databases # 32 | ###################### 33 | *.log 34 | *.sql 35 | *.sqlite 36 | 37 | # OS generated files # 38 | ###################### 39 | .DS_Store 40 | .DS_Store? 41 | ._* 42 | .Spotlight-V100 43 | .Trashes 44 | # Icon? 45 | ehthumbs.db 46 | Thumbs.db 47 | .cache 48 | .project 49 | .settings 50 | .tmproj 51 | *.esproj 52 | nbproject 53 | 54 | # Numerous always-ignore extensions # 55 | ##################################### 56 | *.diff 57 | *.err 58 | *.orig 59 | *.rej 60 | *.swn 61 | *.swo 62 | *.swp 63 | *.vi 64 | *~ 65 | *.sass-cache 66 | *.grunt 67 | *.tmp 68 | 69 | # Dreamweaver added files # 70 | ########################### 71 | _notes 72 | dwsync.xml 73 | 74 | # Komodo # 75 | ########################### 76 | *.komodoproject 77 | .komodotools 78 | 79 | # Node # 80 | ##################### 81 | node_modules 82 | 83 | # Bower # 84 | ##################### 85 | bower_components 86 | 87 | # Folders to ignore # 88 | ##################### 89 | .hg 90 | .svn 91 | .CVS 92 | intermediate 93 | publish 94 | .idea 95 | .graphics 96 | _test 97 | _archive 98 | uploads 99 | tmp 100 | 101 | # Vim files to ignore # 102 | ####################### 103 | .VimballRecord 104 | .netrwhist 105 | 106 | bundle.* 107 | build 108 | _demo -------------------------------------------------------------------------------- /direct.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module audio-speaker/direct 3 | * 4 | * Outputs chunk of data to audio output in node 5 | * 6 | */ 7 | 'use strict'; 8 | 9 | const pcm = require('pcm-util'); 10 | const isAudioBuffer = require('is-audio-buffer'); 11 | 12 | const format = { 13 | float: false, 14 | interleaved: true, 15 | bitDepth: 16, 16 | signed: true 17 | }; 18 | 19 | 20 | let NodeSpeaker, Sink; 21 | 22 | try { 23 | NodeSpeaker = require('speaker'); 24 | } 25 | catch (e) { 26 | console.warn('`speaker` package was not found. Using `audio-sink` instead.'); 27 | Sink = require('audio-sink/direct'); 28 | } 29 | 30 | 31 | module.exports = function (opts) { 32 | opts = Object.assign({}, format, opts); 33 | 34 | return opts.sink || !NodeSpeaker ? createSink(opts) : createSpeaker(opts); 35 | } 36 | 37 | 38 | /** 39 | * Speaker is just a format wrapper for node-speaker, 40 | * as node-speaker doesn’t support any input format in some platforms, like windows. 41 | * So we need to force the most safe format. 42 | * 43 | * @constructor 44 | */ 45 | function createSpeaker (opts) { 46 | //create node-speaker with default options - the most cross-platform case 47 | let speaker = new NodeSpeaker(opts); 48 | let ended = false; 49 | 50 | //FIXME: sometimes this lil fckr does not end stream hanging tests 51 | write.end = () => { 52 | ended = true; 53 | write(true); 54 | } 55 | 56 | return write; 57 | 58 | function write (chunk, cb) { 59 | if (chunk == null || chunk === true || ended) { 60 | ended = true; 61 | cb && cb(true); 62 | return; 63 | } 64 | 65 | let buf = isAudioBuffer(chunk) ? pcm.toBuffer(chunk, format) : chunk; 66 | speaker.write(buf, () => { 67 | if (ended) { 68 | speaker.close(); 69 | speaker.end(); 70 | return cb && cb(true); 71 | } 72 | cb && cb(null, chunk); 73 | }); 74 | } 75 | } 76 | 77 | function createSink (opts) { 78 | let ended = false; 79 | 80 | let sampleRate = opts.sampleRate || 44100; 81 | let samplesPerFrame = opts.samplesPerFrame || 1024; 82 | 83 | let sink = Sink((data, cb) => { 84 | if (ended || data == null || data == true) return cb && cb(true); 85 | cb && setTimeout(cb, samplesPerFrame / sampleRate); 86 | }); 87 | 88 | sink.end = () => { 89 | ended = true; 90 | sink(true); 91 | } 92 | 93 | return sink; 94 | } 95 | 96 | 97 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # audio-speaker [![Build Status](https://travis-ci.org/audiojs/audio-speaker.svg?branch=master)](https://travis-ci.org/audiojs/audio-speaker) [![stable](https://img.shields.io/badge/stability-stable-brightgreen.svg)](http://github.com/badges/stability-badges) [![Greenkeeper badge](https://badges.greenkeeper.io/audiojs/audio-speaker.svg)](https://greenkeeper.io/) 2 | 3 | Output audio stream to speaker in node or browser. 4 | 5 | [![npm install audio-speaker](https://nodei.co/npm/audio-speaker.png?mini=true)](https://npmjs.org/package/audio-speaker/) 6 | 7 | 8 | ### Use as a stream 9 | 10 | ```js 11 | var Speaker = require('audio-speaker/stream'); 12 | var Generator = require('audio-generator/stream'); 13 | 14 | Generator(function (time) { 15 | //panned unisson effect 16 | var τ = Math.PI * 2; 17 | return [Math.sin(τ * time * 441), Math.sin(τ * time * 439)]; 18 | }) 19 | .pipe(Speaker({ 20 | //PCM input format defaults, optional. 21 | //channels: 2, 22 | //sampleRate: 44100, 23 | //byteOrder: 'LE', 24 | //bitDepth: 16, 25 | //signed: true, 26 | //float: false, 27 | //interleaved: true, 28 | })); 29 | ``` 30 | 31 | ### Use as a pull-stream 32 | 33 | ```js 34 | const pull = require('pull-stream/pull'); 35 | const speaker = require('audio-speaker/pull'); 36 | const osc = require('audio-oscillator/pull'); 37 | 38 | pull(osc({frequency: 440}), speaker()); 39 | ``` 40 | 41 | ### Use directly 42 | 43 | Speaker is [async-sink](https://github.com/audiojs/contributing/wiki/Streams-convention) with `fn(data, cb)` notation. 44 | 45 | ```js 46 | const createSpeaker = require('audio-speaker'); 47 | const createGenerator = require('audio-generator'); 48 | 49 | let output = createSpeaker(); 50 | let generate = createGenerator(t => Math.sin(t * Math.PI * 2 * 440)); 51 | 52 | (function loop (err, buf) { 53 | let buffer = generate(); 54 | output(buffer, loop); 55 | })(); 56 | ``` 57 | 58 | #### Related 59 | 60 | > [web-audio-stream](https://github.com/audiojs/web-audio-stream) — stream data to web-audio.
61 | > [audio-through](http://npmjs.org/package/audio-through) — universal stream for processing audio.
62 | > [node-speaker](http://npmjs.org/package/speaker) — output pcm stream to speaker in node.
63 | > [audio-feeder](https://github.com/brion/audio-feeder) — cross-browser speaker for pcm data.
64 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Speaker = require('./stream'); 4 | var Generator = require('audio-generator/stream'); 5 | var Generate = require('audio-generator/index'); 6 | var Readable = require('stream').Readable; 7 | var util = require('audio-buffer-utils'); 8 | var pcm = require('pcm-util'); 9 | var Through = require('audio-through'); 10 | Through.log = true; 11 | var Volume = require('pcm-volume'); 12 | var test = require('tape') 13 | var SpeakerWriter = require('./direct'); 14 | var pull = require('pull-stream'); 15 | var PullSpeaker = require('./pull'); 16 | var pullGenerator = require('audio-generator/pull'); 17 | 18 | 19 | require('insert-styles')(` 20 | @font-face { 21 | font-family: wavefont; 22 | src: url(./wavefont.otf) format("opentype"); 23 | } 24 | `); 25 | 26 | test('Pure function', function (t) { 27 | let generate = Generate(t => { 28 | return Math.sin(t * Math.PI * 2 * 440); 29 | }, 1); 30 | 31 | let write = SpeakerWriter(); 32 | 33 | (function loop (err, buf) { 34 | if (err) return write(null); 35 | write(generate(buf), loop) 36 | })(); 37 | 38 | setTimeout(() => { 39 | write(null); 40 | t.end(); 41 | }, 200); 42 | }); 43 | 44 | test('Pull stream', function (t) { 45 | let out = PullSpeaker(); 46 | 47 | pull( 48 | pullGenerator(time => 2 * time * 440 - 1, {frequency: 440}), 49 | out 50 | ); 51 | 52 | setTimeout(() => { 53 | out.abort(); 54 | t.end(); 55 | }, 500); 56 | }); 57 | 58 | test('Cleanness of wave', function (t) { 59 | Through(function (buffer) { 60 | var self = this; 61 | util.fill(buffer, function (sample, idx, channel) { 62 | return Math.sin(Math.PI * 2 * (self.count + idx) * 440 / 44100); 63 | }); 64 | 65 | if (this.time > 2) return this.end(); 66 | 67 | return buffer; 68 | }) 69 | .on('end', function () { 70 | t.end() 71 | }) 72 | .pipe(Speaker()) 73 | // .pipe(WAASteam(context.destination)); 74 | }); 75 | 76 | test('Feed audio-through', function (t) { 77 | Generator({ 78 | generate: function (time) { 79 | return [ 80 | Math.sin(Math.PI * 2 * time * 538 ) / 5, 81 | Math.sin(Math.PI * 2 * time * 542 ) / 5 82 | // Math.random() 83 | ] 84 | }, 85 | duration: .4 86 | }) 87 | .on('end', function () {t.end()}) 88 | .pipe(Speaker()) 89 | }); 90 | 91 | test('Feed raw pcm', function (t) { 92 | var count = 0; 93 | Readable({ 94 | read: function (size) { 95 | var abuf = util.create(1024, 2, 44100); 96 | 97 | //EGG: swap ch & i and hear wonderful sfx 98 | util.fill(abuf, function (v, i, ch) { 99 | v = Math.sin(Math.PI * 2 * ((count + i)/44100) * (738 + ch*2) ) / 5; 100 | return v; 101 | }); 102 | 103 | count += 1024; 104 | 105 | if (count > 1e4 ) return this.push(null); 106 | 107 | let buf = pcm.toBuffer(abuf); 108 | 109 | this.push(buf); 110 | } 111 | }) 112 | .on('end', function () {t.end()}) 113 | .pipe(Speaker({ 114 | channels: 2 115 | })) 116 | }); 117 | 118 | //FIXME: use transform stream here to send floats data to speaker 119 | test.skip('Feed custom pcm', function (t) { 120 | var count = 0; 121 | Readable({ 122 | // objectMode: 1, 123 | read: function (size) { 124 | var abuf = util.create(1024, 2, 44100); 125 | 126 | util.fill(abuf, function (v, i, ch) { 127 | return Math.sin(Math.PI * 2 * ((count + i)/44100) * (938 + ch*2) ); 128 | }); 129 | 130 | count += 1024; 131 | 132 | if (count > 1e4 ) return this.push(null); 133 | 134 | let buf = pcm.toBuffer(abuf, { 135 | float: true 136 | }); 137 | 138 | this.push(buf); 139 | } 140 | }) 141 | .on('end', function () {t.end()}) 142 | .pipe(Speaker({ 143 | channels: 2, 144 | 145 | //EGG: comment this and hear wonderful sfx 146 | float: true 147 | })) 148 | }); 149 | 150 | test.skip('Feed random buffer size'); 151 | 152 | test('Volume case', function (t) { 153 | //TODO: fix the case! 154 | Generator(function (time) { 155 | return [ 156 | Math.sin(Math.PI * 2 * time * 1038 ) / 5, 157 | Math.sin(Math.PI * 2 * time * 1042 ) / 5 158 | ]; 159 | }, { 160 | duration: 1 161 | }) 162 | .on('end', function () {t.end()}) 163 | .pipe(Volume(5)) 164 | .pipe(Speaker()) 165 | }); 166 | 167 | 168 | 169 | 170 | //little debigger 171 | if (typeof document !== 'undefined') { 172 | var el = document.body.appendChild(document.createElement('div')); 173 | el.style.cssText = ` 174 | font-family: wavefont; 175 | max-width: 100vw; 176 | word-break: break-all; 177 | white-space: pre-wrap; 178 | font-size: 32px; 179 | `; 180 | 181 | function draw (arr) { 182 | let str = ''; 183 | 184 | for (let i = 0; i < arr.length; i++) { 185 | str += String.fromCharCode(0x200 + Math.floor(arr[i] * 128 * 5)); 186 | } 187 | 188 | el.innerHTML += '\n' + str; 189 | } 190 | } 191 | --------------------------------------------------------------------------------