Comparing version 0.3.1 to 0.4.0
@@ -0,1 +1,2 @@ | ||
'use strict' | ||
@@ -7,56 +8,54 @@ /** | ||
var Readable = require('stream').Readable; | ||
var Speaker = require('../'); | ||
const Readable = require('stream').Readable | ||
const bufferAlloc = require('buffer-alloc') | ||
const Speaker = require('../') | ||
// node v0.8.x compat | ||
if (!Readable) Readable = require('readable-stream/readable'); | ||
// the frequency to play | ||
var freq = parseFloat(process.argv[2], 10) || 440.0; // Concert A, default tone | ||
const freq = parseFloat(process.argv[2], 10) || 440.0 // Concert A, default tone | ||
// seconds worth of audio data to generate before emitting "end" | ||
var duration = parseFloat(process.argv[3], 10) || 2.0; | ||
const duration = parseFloat(process.argv[3], 10) || 2.0 | ||
console.log('generating a %dhz sine wave for %d seconds', freq, duration); | ||
console.log('generating a %dhz sine wave for %d seconds', freq, duration) | ||
// A SineWaveGenerator readable stream | ||
var sine = new Readable(); | ||
sine.bitDepth = 16; | ||
sine.channels = 2; | ||
sine.sampleRate = 44100; | ||
sine.samplesGenerated = 0; | ||
sine._read = read; | ||
const sine = new Readable() | ||
sine.bitDepth = 16 | ||
sine.channels = 2 | ||
sine.sampleRate = 44100 | ||
sine.samplesGenerated = 0 | ||
sine._read = read | ||
// create a SineWaveGenerator instance and pipe it to the speaker | ||
sine.pipe(new Speaker()); | ||
sine.pipe(new Speaker()) | ||
// the Readable "_read()" callback function | ||
function read (n) { | ||
var sampleSize = this.bitDepth / 8; | ||
var blockAlign = sampleSize * this.channels; | ||
var numSamples = n / blockAlign | 0; | ||
var buf = new Buffer(numSamples * blockAlign); | ||
var amplitude = 32760; // Max amplitude for 16-bit audio | ||
const sampleSize = this.bitDepth / 8 | ||
const blockAlign = sampleSize * this.channels | ||
const numSamples = n / blockAlign | 0 | ||
const buf = bufferAlloc(numSamples * blockAlign) | ||
const amplitude = 32760 // Max amplitude for 16-bit audio | ||
// the "angle" used in the function, adjusted for the number of | ||
// channels and sample rate. This value is like the period of the wave. | ||
var t = (Math.PI * 2 * freq) / this.sampleRate; | ||
const t = (Math.PI * 2 * freq) / this.sampleRate | ||
for (var i = 0; i < numSamples; i++) { | ||
for (let i = 0; i < numSamples; i++) { | ||
// fill with a simple sine wave at max amplitude | ||
for (var channel = 0; channel < this.channels; channel++) { | ||
var s = this.samplesGenerated + i; | ||
var val = Math.round(amplitude * Math.sin(t * s)); // sine wave | ||
var offset = (i * sampleSize * this.channels) + (channel * sampleSize); | ||
buf['writeInt' + this.bitDepth + 'LE'](val, offset); | ||
for (let channel = 0; channel < this.channels; channel++) { | ||
const s = this.samplesGenerated + i | ||
const val = Math.round(amplitude * Math.sin(t * s)) // sine wave | ||
const offset = (i * sampleSize * this.channels) + (channel * sampleSize) | ||
buf[`writeInt${this.bitDepth}LE`](val, offset) | ||
} | ||
} | ||
this.push(buf); | ||
this.push(buf) | ||
this.samplesGenerated += numSamples; | ||
this.samplesGenerated += numSamples | ||
if (this.samplesGenerated >= this.sampleRate * duration) { | ||
// after generating "duration" second of audio, emit "end" | ||
this.push(null); | ||
this.push(null) | ||
} | ||
} |
@@ -0,1 +1,2 @@ | ||
'use strict' | ||
@@ -6,5 +7,5 @@ /** | ||
var Speaker = require('../'); | ||
const Speaker = require('../') | ||
var speaker = new Speaker(); | ||
process.stdin.pipe(speaker); | ||
const speaker = new Speaker() | ||
process.stdin.pipe(speaker) |
571
index.js
@@ -0,1 +1,2 @@ | ||
'use strict' | ||
@@ -6,348 +7,344 @@ /** | ||
var os = require('os'); | ||
var debug = require('debug')('speaker'); | ||
var binding = require('bindings')('binding'); | ||
var inherits = require('util').inherits; | ||
var Writable = require('readable-stream/writable'); | ||
const os = require('os') | ||
const debug = require('debug')('speaker') | ||
const binding = require('bindings')('binding') | ||
const bufferAlloc = require('buffer-alloc') | ||
const Writable = require('readable-stream/writable') | ||
// determine the native host endianness, the only supported playback endianness | ||
var endianness = 'function' == os.endianness ? | ||
os.endianness() : | ||
'LE'; // assume little-endian for older versions of node.js | ||
const endianness = os.endianness() | ||
/** | ||
* Module exports. | ||
* The `Speaker` class accepts raw PCM data written to it, and then sends that data | ||
* to the default output device of the OS. | ||
* | ||
* @param {Object} opts options object | ||
* @api public | ||
*/ | ||
exports = module.exports = Speaker; | ||
class Speaker extends Writable { | ||
constructor (opts) { | ||
// default lwm and hwm to 0 | ||
if (!opts) opts = {} | ||
if (opts.lowWaterMark == null) opts.lowWaterMark = 0 | ||
if (opts.highWaterMark == null) opts.highWaterMark = 0 | ||
/** | ||
* Export information about the `mpg123_module_t` being used. | ||
*/ | ||
super(opts) | ||
exports.api_version = binding.api_version; | ||
exports.description = binding.description; | ||
exports.module_name = binding.name; | ||
// chunks are sent over to the backend in "samplesPerFrame * blockAlign" size. | ||
// this is necessary because if we send too big of chunks at once, then there | ||
// won't be any data ready when the audio callback comes (experienced with the | ||
// CoreAudio backend) | ||
this.samplesPerFrame = 1024 | ||
/** | ||
* Returns the `MPG123_ENC_*` constant that corresponds to the given "format" | ||
* object, or `null` if the format is invalid. | ||
* | ||
* @param {Object} format - format object with `channels`, `sampleRate`, `bitDepth`, etc. | ||
* @return {Number} MPG123_ENC_* constant, or `null` | ||
* @api public | ||
*/ | ||
// the `audio_output_t` struct pointer Buffer instance | ||
this.audio_handle = null | ||
exports.getFormat = function getFormat (format) { | ||
var f = null; | ||
if (format.bitDepth == 32 && format.float && format.signed) { | ||
f = binding.MPG123_ENC_FLOAT_32; | ||
} else if (format.bitDepth == 64 && format.float && format.signed) { | ||
f = binding.MPG123_ENC_FLOAT_64; | ||
} else if (format.bitDepth == 8 && format.signed) { | ||
f = binding.MPG123_ENC_SIGNED_8; | ||
} else if (format.bitDepth == 8 && !format.signed) { | ||
f = binding.MPG123_ENC_UNSIGNED_8; | ||
} else if (format.bitDepth == 16 && format.signed) { | ||
f = binding.MPG123_ENC_SIGNED_16; | ||
} else if (format.bitDepth == 16 && !format.signed) { | ||
f = binding.MPG123_ENC_UNSIGNED_16; | ||
} else if (format.bitDepth == 24 && format.signed) { | ||
f = binding.MPG123_ENC_SIGNED_24; | ||
} else if (format.bitDepth == 24 && !format.signed) { | ||
f = binding.MPG123_ENC_UNSIGNED_24; | ||
} else if (format.bitDepth == 32 && format.signed) { | ||
f = binding.MPG123_ENC_SIGNED_32; | ||
} else if (format.bitDepth == 32 && !format.signed) { | ||
f = binding.MPG123_ENC_UNSIGNED_32; | ||
// flipped after close() is called, no write() calls allowed after | ||
this._closed = false | ||
// set PCM format | ||
this._format(opts) | ||
// bind event listeners | ||
this._format = this._format.bind(this) | ||
this.on('finish', this._flush) | ||
this.on('pipe', this._pipe) | ||
this.on('unpipe', this._unpipe) | ||
} | ||
return f; | ||
} | ||
/** | ||
* Returns `true` if the given "format" is playable via the "output module" | ||
* that was selected during compilation, or `false` if not playable. | ||
* | ||
* @param {Number} format - MPG123_ENC_* format constant | ||
* @return {Boolean} true if the format is playable, false otherwise | ||
* @api public | ||
*/ | ||
/** | ||
* Calls the audio backend's `open()` function, and then emits an "open" event. | ||
* | ||
* @api private | ||
*/ | ||
exports.isSupported = function isSupported (format) { | ||
if ('number' !== typeof format) format = exports.getFormat(format); | ||
return (binding.formats & format) === format; | ||
} | ||
_open () { | ||
debug('open()') | ||
if (this.audio_handle) { | ||
throw new Error('_open() called more than once!') | ||
} | ||
// set default options, if not set | ||
if (this.channels == null) { | ||
debug('setting default %o: %o', 'channels', 2) | ||
this.channels = 2 | ||
} | ||
if (this.bitDepth == null) { | ||
const depth = this.float ? 32 : 16 | ||
debug('setting default %o: %o', 'bitDepth', depth) | ||
this.bitDepth = depth | ||
} | ||
if (this.sampleRate == null) { | ||
debug('setting default %o: %o', 'sampleRate', 44100) | ||
this.sampleRate = 44100 | ||
} | ||
if (this.signed == null) { | ||
debug('setting default %o: %o', 'signed', this.bitDepth !== 8) | ||
this.signed = this.bitDepth !== 8 | ||
} | ||
/** | ||
* The `Speaker` class accepts raw PCM data written to it, and then sends that data | ||
* to the default output device of the OS. | ||
* | ||
* @param {Object} opts options object | ||
* @api public | ||
*/ | ||
const format = Speaker.getFormat(this) | ||
if (format == null) { | ||
throw new Error('invalid PCM format specified') | ||
} | ||
function Speaker (opts) { | ||
if (!(this instanceof Speaker)) return new Speaker(opts); | ||
if (!Speaker.isSupported(format)) { | ||
throw new Error(`specified PCM format is not supported by "${binding.name}" backend`) | ||
} | ||
// default lwm and hwm to 0 | ||
if (!opts) opts = {}; | ||
if (null == opts.lowWaterMark) opts.lowWaterMark = 0; | ||
if (null == opts.highWaterMark) opts.highWaterMark = 0; | ||
// calculate the "block align" | ||
this.blockAlign = this.bitDepth / 8 * this.channels | ||
Writable.call(this, opts); | ||
// initialize the audio handle | ||
// TODO: open async? | ||
this.audio_handle = bufferAlloc(binding.sizeof_audio_output_t) | ||
const r = binding.open(this.audio_handle, this.channels, this.sampleRate, format) | ||
if (r !== 0) { | ||
throw new Error(`open() failed: ${r}`) | ||
} | ||
// chunks are sent over to the backend in "samplesPerFrame * blockAlign" size. | ||
// this is necessary because if we send too big of chunks at once, then there | ||
// won't be any data ready when the audio callback comes (experienced with the | ||
// CoreAudio backend) | ||
this.samplesPerFrame = 1024; | ||
this.emit('open') | ||
return this.audio_handle | ||
} | ||
// the `audio_output_t` struct pointer Buffer instance | ||
this.audio_handle = null; | ||
/** | ||
* Set given PCM formatting options. Called during instantiation on the passed in | ||
* options object, on the stream given to the "pipe" event, and a final time if | ||
* that stream emits a "format" event. | ||
* | ||
* @param {Object} opts | ||
* @api private | ||
*/ | ||
// flipped after close() is called, no write() calls allowed after | ||
this._closed = false; | ||
_format (opts) { | ||
debug('format(object keys = %o)', Object.keys(opts)) | ||
if (opts.channels != null) { | ||
debug('setting %o: %o', 'channels', opts.channels) | ||
this.channels = opts.channels | ||
} | ||
if (opts.bitDepth != null) { | ||
debug('setting %o: %o', 'bitDepth', opts.bitDepth) | ||
this.bitDepth = opts.bitDepth | ||
} | ||
if (opts.sampleRate != null) { | ||
debug('setting %o: %o', 'sampleRate', opts.sampleRate) | ||
this.sampleRate = opts.sampleRate | ||
} | ||
if (opts.float != null) { | ||
debug('setting %o: %o', 'float', opts.float) | ||
this.float = opts.float | ||
} | ||
if (opts.signed != null) { | ||
debug('setting %o: %o', 'signed', opts.signed) | ||
this.signed = opts.signed | ||
} | ||
if (opts.samplesPerFrame != null) { | ||
debug('setting %o: %o', 'samplesPerFrame', opts.samplesPerFrame) | ||
this.samplesPerFrame = opts.samplesPerFrame | ||
} | ||
if (opts.endianness == null || endianness === opts.endianness) { | ||
// no "endianness" specified or explicit native endianness | ||
this.endianness = endianness | ||
} else { | ||
// only native endianness is supported... | ||
this.emit('error', new Error(`only native endianness ("${endianness}") is supported, got "${opts.endianness}"`)) | ||
} | ||
} | ||
// set PCM format | ||
this._format(opts); | ||
/** | ||
* `_write()` callback for the Writable base class. | ||
* | ||
* @param {Buffer} chunk | ||
* @param {String} encoding | ||
* @param {Function} done | ||
* @api private | ||
*/ | ||
// bind event listeners | ||
this._format = this._format.bind(this); | ||
this.on('finish', this._flush); | ||
this.on('pipe', this._pipe); | ||
this.on('unpipe', this._unpipe); | ||
} | ||
inherits(Speaker, Writable); | ||
_write (chunk, encoding, done) { | ||
debug('_write() (%o bytes)', chunk.length) | ||
/** | ||
* Calls the audio backend's `open()` function, and then emits an "open" event. | ||
* | ||
* @api private | ||
*/ | ||
if (this._closed) { | ||
// close() has already been called. this should not be called | ||
return done(new Error('write() call after close() call')) | ||
} | ||
let b | ||
let left = chunk | ||
let handle = this.audio_handle | ||
if (!handle) { | ||
// this is the first time write() is being called; need to _open() | ||
try { | ||
handle = this._open() | ||
} catch (e) { | ||
return done(e) | ||
} | ||
} | ||
const chunkSize = this.blockAlign * this.samplesPerFrame | ||
Speaker.prototype._open = function () { | ||
debug('open()'); | ||
if (this.audio_handle) { | ||
throw new Error('_open() called more than once!'); | ||
} | ||
// set default options, if not set | ||
if (null == this.channels) { | ||
debug('setting default %o: %o', 'channels', 2); | ||
this.channels = 2; | ||
} | ||
if (null == this.bitDepth) { | ||
var depth = this.float ? 32 : 16; | ||
debug('setting default %o: %o', 'bitDepth', depth); | ||
this.bitDepth = depth; | ||
} | ||
if (null == this.sampleRate) { | ||
debug('setting default %o: %o', 'sampleRate', 44100); | ||
this.sampleRate = 44100; | ||
} | ||
if (null == this.signed) { | ||
debug('setting default %o: %o', 'signed', this.bitDepth != 8); | ||
this.signed = this.bitDepth != 8; | ||
} | ||
const write = () => { | ||
if (this._closed) { | ||
debug('aborting remainder of write() call (%o bytes), since speaker is `_closed`', left.length) | ||
return done() | ||
} | ||
b = left | ||
if (b.length > chunkSize) { | ||
const t = b | ||
b = t.slice(0, chunkSize) | ||
left = t.slice(chunkSize) | ||
} else { | ||
left = null | ||
} | ||
debug('writing %o byte chunk', b.length) | ||
binding.write(handle, b, b.length, onwrite) | ||
} | ||
var format = exports.getFormat(this); | ||
if (null == format) { | ||
throw new Error('invalid PCM format specified'); | ||
const onwrite = (r) => { | ||
debug('wrote %o bytes', r) | ||
if (r !== b.length) { | ||
done(new Error(`write() failed: ${r}`)) | ||
} else if (left) { | ||
debug('still %o bytes left in this chunk', left.length) | ||
write() | ||
} else { | ||
debug('done with this chunk') | ||
done() | ||
} | ||
} | ||
write() | ||
} | ||
if (!exports.isSupported(format)) { | ||
throw new Error('specified PCM format is not supported by "' + binding.name + '" backend'); | ||
/** | ||
* Called when this stream is pipe()d to from another readable stream. | ||
* If the "sampleRate", "channels", "bitDepth", and "signed" properties are | ||
* set, then they will be used over the currently set values. | ||
* | ||
* @api private | ||
*/ | ||
_pipe (source) { | ||
debug('_pipe()') | ||
this._format(source) | ||
source.once('format', this._format) | ||
} | ||
// calculate the "block align" | ||
this.blockAlign = this.bitDepth / 8 * this.channels; | ||
/** | ||
* Called when this stream is pipe()d to from another readable stream. | ||
* If the "sampleRate", "channels", "bitDepth", and "signed" properties are | ||
* set, then they will be used over the currently set values. | ||
* | ||
* @api private | ||
*/ | ||
// initialize the audio handle | ||
// TODO: open async? | ||
this.audio_handle = new Buffer(binding.sizeof_audio_output_t); | ||
var r = binding.open(this.audio_handle, this.channels, this.sampleRate, format); | ||
if (0 !== r) { | ||
throw new Error('open() failed: ' + r); | ||
_unpipe (source) { | ||
debug('_unpipe()') | ||
source.removeListener('format', this._format) | ||
} | ||
this.emit('open'); | ||
return this.audio_handle; | ||
}; | ||
/** | ||
* Emits a "flush" event and then calls the `.close()` function on | ||
* this Speaker instance. | ||
* | ||
* @api private | ||
*/ | ||
/** | ||
* Set given PCM formatting options. Called during instantiation on the passed in | ||
* options object, on the stream given to the "pipe" event, and a final time if | ||
* that stream emits a "format" event. | ||
* | ||
* @param {Object} opts | ||
* @api private | ||
*/ | ||
Speaker.prototype._format = function (opts) { | ||
debug('format(object keys = %o)', Object.keys(opts)); | ||
if (null != opts.channels) { | ||
debug('setting %o: %o', 'channels', opts.channels); | ||
this.channels = opts.channels; | ||
_flush () { | ||
debug('_flush()') | ||
this.emit('flush') | ||
this.close(false) | ||
} | ||
if (null != opts.bitDepth) { | ||
debug('setting %o: %o', "bitDepth", opts.bitDepth); | ||
this.bitDepth = opts.bitDepth; | ||
} | ||
if (null != opts.sampleRate) { | ||
debug('setting %o: %o', "sampleRate", opts.sampleRate); | ||
this.sampleRate = opts.sampleRate; | ||
} | ||
if (null != opts.float) { | ||
debug('setting %o: %o', "float", opts.float); | ||
this.float = opts.float; | ||
} | ||
if (null != opts.signed) { | ||
debug('setting %o: %o', "signed", opts.signed); | ||
this.signed = opts.signed; | ||
} | ||
if (null != opts.samplesPerFrame) { | ||
debug('setting %o: %o', "samplesPerFrame", opts.samplesPerFrame); | ||
this.samplesPerFrame = opts.samplesPerFrame; | ||
} | ||
if (null == opts.endianness || endianness == opts.endianness) { | ||
// no "endianness" specified or explicit native endianness | ||
this.endianness = endianness; | ||
} else { | ||
// only native endianness is supported... | ||
this.emit('error', new Error('only native endianness ("' + endianness + '") is supported, got "' + opts.endianness + '"')); | ||
} | ||
}; | ||
/** | ||
* `_write()` callback for the Writable base class. | ||
* | ||
* @param {Buffer} chunk | ||
* @param {String} encoding | ||
* @param {Function} done | ||
* @api private | ||
*/ | ||
/** | ||
* Closes the audio backend. Normally this function will be called automatically | ||
* after the audio backend has finished playing the audio buffer through the | ||
* speakers. | ||
* | ||
* @param {Boolean} flush - if `false`, then don't call the `flush()` native binding call. Defaults to `true`. | ||
* @api public | ||
*/ | ||
Speaker.prototype._write = function (chunk, encoding, done) { | ||
debug('_write() (%o bytes)', chunk.length); | ||
close (flush) { | ||
debug('close(%o)', flush) | ||
if (this._closed) return debug('already closed...') | ||
if (this._closed) { | ||
// close() has already been called. this should not be called | ||
return done(new Error('write() call after close() call')); | ||
} | ||
var b; | ||
var self = this; | ||
var left = chunk; | ||
var handle = this.audio_handle; | ||
if (!handle) { | ||
// this is the first time write() is being called; need to _open() | ||
try { | ||
handle = this._open(); | ||
} catch (e) { | ||
return done(e); | ||
} | ||
} | ||
var chunkSize = this.blockAlign * this.samplesPerFrame; | ||
if (this.audio_handle) { | ||
if (flush !== false) { | ||
// TODO: async most likely… | ||
debug('invoking flush() native binding') | ||
binding.flush(this.audio_handle) | ||
} | ||
function write () { | ||
if (self._closed) { | ||
debug('aborting remainder of write() call (%o bytes), since speaker is `_closed`', left.length); | ||
return done(); | ||
} | ||
b = left; | ||
if (b.length > chunkSize) { | ||
var t = b; | ||
b = t.slice(0, chunkSize); | ||
left = t.slice(chunkSize); | ||
// TODO: async maybe? | ||
debug('invoking close() native binding') | ||
binding.close(this.audio_handle) | ||
this.audio_handle = null | ||
} else { | ||
left = null; | ||
debug('not invoking flush() or close() bindings since no `audio_handle`') | ||
} | ||
debug('writing %o byte chunk', b.length); | ||
binding.write(handle, b, b.length, onwrite); | ||
} | ||
function onwrite (r) { | ||
debug('wrote %o bytes', r); | ||
if (r != b.length) { | ||
done(new Error('write() failed: ' + r)); | ||
} else if (left) { | ||
debug('still %o bytes left in this chunk', left.length); | ||
write(); | ||
} else { | ||
debug('done with this chunk'); | ||
done(); | ||
} | ||
this._closed = true | ||
this.emit('close') | ||
} | ||
} | ||
write(); | ||
}; | ||
/** | ||
* Called when this stream is pipe()d to from another readable stream. | ||
* If the "sampleRate", "channels", "bitDepth", and "signed" properties are | ||
* set, then they will be used over the currently set values. | ||
* | ||
* @api private | ||
* Export information about the `mpg123_module_t` being used. | ||
*/ | ||
Speaker.prototype._pipe = function (source) { | ||
debug('_pipe()'); | ||
this._format(source); | ||
source.once('format', this._format); | ||
}; | ||
Speaker.api_version = binding.api_version | ||
Speaker.description = binding.description | ||
Speaker.module_name = binding.name | ||
/** | ||
* Called when this stream is pipe()d to from another readable stream. | ||
* If the "sampleRate", "channels", "bitDepth", and "signed" properties are | ||
* set, then they will be used over the currently set values. | ||
* Returns the `MPG123_ENC_*` constant that corresponds to the given "format" | ||
* object, or `null` if the format is invalid. | ||
* | ||
* @api private | ||
* @param {Object} format - format object with `channels`, `sampleRate`, `bitDepth`, etc. | ||
* @return {Number} MPG123_ENC_* constant, or `null` | ||
* @api public | ||
*/ | ||
Speaker.prototype._unpipe = function (source) { | ||
debug('_unpipe()'); | ||
source.removeListener('format', this._format); | ||
}; | ||
Speaker.getFormat = function getFormat (format) { | ||
if (Number(format.bitDepth) === 32 && format.float && format.signed) { | ||
return binding.MPG123_ENC_FLOAT_32 | ||
} else if (Number(format.bitDepth) === 64 && format.float && format.signed) { | ||
return binding.MPG123_ENC_FLOAT_64 | ||
} else if (Number(format.bitDepth) === 8 && format.signed) { | ||
return binding.MPG123_ENC_SIGNED_8 | ||
} else if (Number(format.bitDepth) === 8 && !format.signed) { | ||
return binding.MPG123_ENC_UNSIGNED_8 | ||
} else if (Number(format.bitDepth) === 16 && format.signed) { | ||
return binding.MPG123_ENC_SIGNED_16 | ||
} else if (Number(format.bitDepth) === 16 && !format.signed) { | ||
return binding.MPG123_ENC_UNSIGNED_16 | ||
} else if (Number(format.bitDepth) === 24 && format.signed) { | ||
return binding.MPG123_ENC_SIGNED_24 | ||
} else if (Number(format.bitDepth) === 24 && !format.signed) { | ||
return binding.MPG123_ENC_UNSIGNED_24 | ||
} else if (Number(format.bitDepth) === 32 && format.signed) { | ||
return binding.MPG123_ENC_SIGNED_32 | ||
} else if (Number(format.bitDepth) === 32 && !format.signed) { | ||
return binding.MPG123_ENC_UNSIGNED_32 | ||
} else { | ||
return null | ||
} | ||
} | ||
/** | ||
* Emits a "flush" event and then calls the `.close()` function on | ||
* this Speaker instance. | ||
* Returns `true` if the given "format" is playable via the "output module" | ||
* that was selected during compilation, or `false` if not playable. | ||
* | ||
* @api private | ||
* @param {Number} format - MPG123_ENC_* format constant | ||
* @return {Boolean} true if the format is playable, false otherwise | ||
* @api public | ||
*/ | ||
Speaker.prototype._flush = function () { | ||
debug('_flush()'); | ||
this.emit('flush'); | ||
this.close(false); | ||
}; | ||
Speaker.isSupported = function isSupported (format) { | ||
if (typeof format !== 'number') format = Speaker.getFormat(format) | ||
return (binding.formats & format) === format | ||
} | ||
/** | ||
* Closes the audio backend. Normally this function will be called automatically | ||
* after the audio backend has finished playing the audio buffer through the | ||
* speakers. | ||
* | ||
* @param {Boolean} flush - if `false`, then don't call the `flush()` native binding call. Defaults to `true`. | ||
* @api public | ||
* Module exports. | ||
*/ | ||
Speaker.prototype.close = function (flush) { | ||
debug('close(%o)', flush); | ||
if (this._closed) return debug('already closed...'); | ||
if (this.audio_handle) { | ||
if (false !== flush) { | ||
// TODO: async most likely… | ||
debug('invoking flush() native binding'); | ||
binding.flush(this.audio_handle); | ||
} | ||
// TODO: async maybe? | ||
debug('invoking close() native binding'); | ||
binding.close(this.audio_handle); | ||
this.audio_handle = null; | ||
} else { | ||
debug('not invoking flush() or close() bindings since no `audio_handle`'); | ||
} | ||
this._closed = true; | ||
this.emit('close'); | ||
}; | ||
exports = module.exports = Speaker |
{ | ||
"name": "speaker", | ||
"version": "0.4.0", | ||
"license": "MIT", | ||
"description": "Output PCM audio data to the speakers", | ||
"author": "Nathan Rajlich <nathan@tootallnate.net> (http://tootallnate.net)", | ||
"repository": "TooTallNate/node-speaker", | ||
"scripts": { | ||
"test": "standard && node-gyp rebuild --mpg123-backend=dummy && mocha --reporter spec" | ||
}, | ||
"dependencies": { | ||
"bindings": "^1.3.0", | ||
"buffer-alloc": "^1.1.0", | ||
"debug": "^3.0.1", | ||
"nan": "^2.6.2", | ||
"readable-stream": "^2.3.3" | ||
}, | ||
"devDependencies": { | ||
"mocha": "^3.5.0", | ||
"standard": "^10.0.3" | ||
}, | ||
"engines": { | ||
"node": ">4" | ||
}, | ||
"keywords": [ | ||
@@ -21,27 +42,3 @@ "pcm", | ||
"mpg123" | ||
], | ||
"license": "MIT", | ||
"version": "0.3.1", | ||
"author": "Nathan Rajlich <nathan@tootallnate.net> (http://tootallnate.net)", | ||
"repository": { | ||
"type": "git", | ||
"url": "git://github.com/TooTallNate/node-speaker.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/TooTallNate/node-speaker/issues" | ||
}, | ||
"homepage": "https://github.com/TooTallNate/node-speaker", | ||
"main": "./index.js", | ||
"scripts": { | ||
"test": "node-gyp rebuild --mpg123-backend=dummy && mocha --reporter spec" | ||
}, | ||
"dependencies": { | ||
"bindings": "^1.2.1", | ||
"debug": "^2.2.0", | ||
"nan": "^2.2.0", | ||
"readable-stream": "^2.0.5" | ||
}, | ||
"devDependencies": { | ||
"mocha": "^2.1.0" | ||
} | ||
] | ||
} |
@@ -1,8 +0,8 @@ | ||
node-speaker | ||
============ | ||
### Output [PCM audio][pcm] data to the speakers | ||
# node-speaker | ||
## Output [PCM audio][pcm] data to the speakers | ||
[![Build Status](https://secure.travis-ci.org/TooTallNate/node-speaker.svg)](https://travis-ci.org/TooTallNate/node-speaker) | ||
[![Build Status](https://ci.appveyor.com/api/projects/status/wix7wml3v55670kw?svg=true)](https://ci.appveyor.com/project/TooTallNate/node-speaker) | ||
A Writable stream instance that accepts [PCM audio][pcm] data and outputs it | ||
@@ -13,10 +13,8 @@ to the speakers. The output is backed by `mpg123`'s audio output modules, which | ||
## Installation | ||
Installation | ||
------------ | ||
Simply compile and install `node-speaker` using `npm`: | ||
``` bash | ||
$ npm install speaker | ||
```sh | ||
npm install speaker | ||
``` | ||
@@ -27,18 +25,16 @@ | ||
``` bash | ||
$ sudo apt-get install libasound2-dev | ||
```sh | ||
sudo apt-get install libasound2-dev | ||
``` | ||
## Example | ||
Example | ||
------- | ||
Here's an example of piping `stdin` to the speaker, which should be 2 channel, | ||
16-bit audio at 44,100 samples per second (a.k.a CD quality audio). | ||
``` javascript | ||
var Speaker = require('speaker'); | ||
```javascript | ||
const Speaker = require('speaker'); | ||
// Create the Speaker instance | ||
var speaker = new Speaker({ | ||
const speaker = new Speaker({ | ||
channels: 2, // 2 channels | ||
@@ -53,10 +49,8 @@ bitDepth: 16, // 16-bit samples | ||
## API | ||
API | ||
--- | ||
`require('speaker')` directly returns the `Speaker` constructor. It is the only | ||
interface exported by `node-speaker`. | ||
### new Speaker([ format ]) -> Speaker instance; | ||
### new Speaker([ format ]) -> Speaker instance | ||
@@ -67,8 +61,8 @@ Creates a new `Speaker` instance, which is a writable stream that you can pipe | ||
* `channels` - The number of audio channels. PCM data must be interleaved. Defaults to `2`. | ||
* `bitDepth` - The number of bits per sample. Defaults to `16` (16-bit). | ||
* `sampleRate` - The number of samples per second per channel. Defaults to `44100`. | ||
* `signed` - Boolean specifying if the samples are signed or unsigned. Defaults to `true` when bit depth is 8-bit, `false` otherwise. | ||
* `float` - Boolean specifying if the samples are floating-point values. Defaults to `false`. | ||
* `samplesPerFrame` - The number of samples to send to the audio backend at a time. You likely don't need to mess with this value. Defaults to `1024`. | ||
* `channels` - The number of audio channels. PCM data must be interleaved. Defaults to `2`. | ||
* `bitDepth` - The number of bits per sample. Defaults to `16` (16-bit). | ||
* `sampleRate` - The number of samples per second per channel. Defaults to `44100`. | ||
* `signed` - Boolean specifying if the samples are signed or unsigned. Defaults to `true` when bit depth is 8-bit, `false` otherwise. | ||
* `float` - Boolean specifying if the samples are floating-point values. Defaults to `false`. | ||
* `samplesPerFrame` - The number of samples to send to the audio backend at a time. You likely don't need to mess with this value. Defaults to `1024`. | ||
@@ -90,6 +84,4 @@ #### "open" event | ||
## Audio Backend Selection | ||
Audio Backend Selection | ||
----------------------- | ||
`node-speaker` is backed by `mpg123`'s "output modules", which in turn use one of | ||
@@ -109,4 +101,4 @@ many popular audio backends like ALSA, OSS, SDL, and lots more. The default | ||
``` bash | ||
$ npm install speaker --mpg123-backend=openal | ||
```sh | ||
npm install speaker --mpg123-backend=openal | ||
``` | ||
@@ -113,0 +105,0 @@ |
161
test/test.js
@@ -0,2 +1,5 @@ | ||
/* eslint-env mocha */ | ||
'use strict' | ||
/** | ||
@@ -6,116 +9,116 @@ * Module dependencies. | ||
var os = require('os'); | ||
var assert = require('assert'); | ||
var Speaker = require('../'); | ||
var endianness = 'function' == os.endianness ? os.endianness() : 'LE'; | ||
var opposite = endianness == 'LE' ? 'BE' : 'LE'; | ||
const os = require('os') | ||
const assert = require('assert') | ||
const bufferAlloc = require('buffer-alloc') | ||
const Speaker = require('../') | ||
const endianness = os.endianness() | ||
const opposite = endianness === 'LE' ? 'BE' : 'LE' | ||
describe('exports', function () { | ||
it('should export a Function', function () { | ||
assert.equal('function', typeof Speaker); | ||
}); | ||
assert.equal('function', typeof Speaker) | ||
}) | ||
it('should have an "api_version" property', function () { | ||
assert(Speaker.hasOwnProperty('api_version')); | ||
assert('number', typeof Speaker.api_version); | ||
}); | ||
assert(Speaker.hasOwnProperty('api_version')) | ||
assert('number', typeof Speaker.api_version) | ||
}) | ||
it('should have a "description" property', function () { | ||
assert(Speaker.hasOwnProperty('description')); | ||
assert('string', typeof Speaker.description); | ||
}); | ||
assert(Speaker.hasOwnProperty('description')) | ||
assert('string', typeof Speaker.description) | ||
}) | ||
it('should have a "module_name" property', function () { | ||
assert(Speaker.hasOwnProperty('module_name')); | ||
assert('string', typeof Speaker.module_name); | ||
}); | ||
assert(Speaker.hasOwnProperty('module_name')) | ||
assert('string', typeof Speaker.module_name) | ||
}) | ||
}) | ||
}); | ||
describe('Speaker', function () { | ||
it('should return a Speaker instance', function () { | ||
var s = new Speaker(); | ||
assert(s instanceof Speaker); | ||
}); | ||
const s = new Speaker() | ||
assert(s instanceof Speaker) | ||
}) | ||
it('should be a writable stream', function () { | ||
var s = new Speaker(); | ||
assert.equal(s.writable, true); | ||
assert.notEqual(s.readable, true); | ||
}); | ||
const s = new Speaker() | ||
assert.equal(s.writable, true) | ||
assert.notEqual(s.readable, true) | ||
}) | ||
it('should emit an "open" event after the first write()', function (done) { | ||
var s = new Speaker(); | ||
var called = false; | ||
const s = new Speaker() | ||
let called = false | ||
s.on('open', function () { | ||
called = true; | ||
done(); | ||
}); | ||
assert.equal(called, false); | ||
s.write(Buffer(0)); | ||
}); | ||
called = true | ||
done() | ||
}) | ||
assert.equal(called, false) | ||
s.write(bufferAlloc(0)) | ||
}) | ||
it('should emit a "flush" event after end()', function (done) { | ||
var s = new Speaker(); | ||
var called = false; | ||
const s = new Speaker() | ||
let called = false | ||
s.on('flush', function () { | ||
called = true; | ||
done(); | ||
}); | ||
assert.equal(called, false); | ||
s.end(Buffer(0)); | ||
}); | ||
called = true | ||
done() | ||
}) | ||
assert.equal(called, false) | ||
s.end(bufferAlloc(0)) | ||
}) | ||
it('should emit a "close" event after end()', function (done) { | ||
this.slow(1000); | ||
var s = new Speaker(); | ||
var called = false; | ||
this.slow(1000) | ||
const s = new Speaker() | ||
let called = false | ||
s.on('close', function () { | ||
called = true; | ||
done(); | ||
}); | ||
assert.equal(called, false); | ||
s.end(Buffer(0)); | ||
}); | ||
called = true | ||
done() | ||
}) | ||
assert.equal(called, false) | ||
s.end(bufferAlloc(0)) | ||
}) | ||
it('should only emit one "close" event', function (done) { | ||
var s = new Speaker(); | ||
var count = 0; | ||
const s = new Speaker() | ||
let count = 0 | ||
s.on('close', function () { | ||
count++; | ||
}); | ||
assert.equal(0, count); | ||
s.close(); | ||
assert.equal(1, count); | ||
s.close(); | ||
assert.equal(1, count); | ||
done(); | ||
}); | ||
count++ | ||
}) | ||
assert.equal(0, count) | ||
s.close() | ||
assert.equal(1, count) | ||
s.close() | ||
assert.equal(1, count) | ||
done() | ||
}) | ||
it('should not throw an Error if native "endianness" is specified', function () { | ||
assert.doesNotThrow(function () { | ||
new Speaker({ endianness: endianness }); | ||
}); | ||
}); | ||
// eslint-disable-next-line no-new | ||
new Speaker({ endianness: endianness }) | ||
}) | ||
}) | ||
it('should throw an Error if non-native "endianness" is specified', function () { | ||
assert.throws(function () { | ||
new Speaker({ endianness: opposite }); | ||
}); | ||
}); | ||
// eslint-disable-next-line no-new | ||
new Speaker({ endianness: opposite }) | ||
}) | ||
}) | ||
it('should throw an Error if a non-supported "format" is specified', function (done) { | ||
var speaker = new Speaker({ | ||
const speaker = new Speaker({ | ||
bitDepth: 31, | ||
signed: true | ||
}); | ||
speaker.once('error', function (err) { | ||
assert.equal('invalid PCM format specified', err.message); | ||
done(); | ||
}); | ||
speaker.write('a'); | ||
}); | ||
}); | ||
}) | ||
speaker.once('error', (err) => { | ||
assert.equal('invalid PCM format specified', err.message) | ||
done() | ||
}) | ||
speaker.write('a') | ||
}) | ||
}) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
525
3511053
5
2
1
2
102
1
+ Addedbuffer-alloc@^1.1.0
+ Addedbuffer-alloc@1.2.0(transitive)
+ Addedbuffer-alloc-unsafe@1.1.0(transitive)
+ Addedbuffer-fill@1.0.0(transitive)
+ Addeddebug@3.2.7(transitive)
+ Addedms@2.1.3(transitive)
- Removeddebug@2.6.9(transitive)
- Removedms@2.0.0(transitive)
Updatedbindings@^1.3.0
Updateddebug@^3.0.1
Updatednan@^2.6.2
Updatedreadable-stream@^2.3.3