Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ process.stdin.pipe(speaker);
`require('speaker')` directly returns the `Speaker` constructor. It is the only
interface exported by `node-speaker`.

### new Speaker([ format ]) -> Speaker instance
### new Speaker([ options ]) -> Speaker instance

Creates a new `Speaker` instance, which is a writable stream that you can pipe
PCM audio data to. The optional `format` object may contain any of the `Writable`
PCM audio data to. The optional `options` object may contain any of the `Writable`
base class options, as well as any of these PCM formatting options:

* `channels` - The number of audio channels. PCM data must be interleaved. Defaults to `2`.
Expand All @@ -61,12 +61,21 @@ base class options, as well as any of these PCM formatting options:
* `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`.
* `device` - The name of the playback device. E.g. `'hw:0,0'` for first device of first sound card or `'hw:1,0'` for first device of second sound card. Defaults to `null` which will pick the default device.

#### "open" event

Fired when the backend `open()` call has completed. This happens once the first
`write()` call happens on the speaker instance.

#### "progress" event

Fired after each data write to the speaker instance. The parameter contains progress info:
* `numwr` - the cumulative number of writes (this is related to the number of frames)
* `wrlen` - the number of bytes written this time
* `wrtotal` - the total number of bytes written
* `buflen` - the number of bytes currently remaining in the buffer
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we try and name these something more human friendly?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it would be a kind of acknowledgement or ack .


#### "flush" event

Fired after the speaker instance has had `end()` called, and after the audio data
Expand Down
55 changes: 55 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Writable, WritableOptions } from 'stream';

export interface Options extends WritableOptions {
readonly channels?: number;
readonly bitDepth?: number;
readonly sampleRate?: number;
readonly lowWaterMark?: number;
readonly highWaterMark?: number;
}

export interface Format {
readonly float?: boolean;
readonly signed?: boolean;
readonly bitDepth?: number;
readonly channels?: number;
readonly sampleRate?: number;
readonly samplesPerFrame?: number;
}

/**
* The `Speaker` class accepts raw PCM data written to it, and then sends that data
* to the default output device of the OS.
*
* @param opts options.
*/
export default class Speaker extends Writable {
constructor(opts?: Options);

/**
* 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 flush Defaults to `true`.
*/
public close(flush: boolean): string;

/**
* Returns the `MPG123_ENC_*` constant that corresponds to the given "format"
* object, or `null` if the format is invalid.
*
* @param format format object with `channels`, `sampleRate`, `bitDepth`, etc.
* @return MPG123_ENC_* constant, or `null`
*/
public getFormat(format: Format): number | null;

/**
* Returns whether or not "format" is playable via the "output module"
* that was selected during compilation.
*
* @param format MPG123_ENC_* format constant
* @return whether or not is playable
*/
public isSupported(format: number): boolean;
}
72 changes: 70 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ class Speaker extends Writable {
debug('setting default %o: %o', 'signed', this.bitDepth !== 8)
this.signed = this.bitDepth !== 8
}
if (this.device == null) {
debug('setting default %o: %o', 'device', null)
this.device = null
}

const format = Speaker.getFormat(this)
if (format == null) {
Expand All @@ -97,7 +101,7 @@ class Speaker extends Writable {
// 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)
const r = binding.open(this.audio_handle, this.channels, this.sampleRate, format, this.device)
if (r !== 0) {
throw new Error(`open() failed: ${r}`)
}
Expand Down Expand Up @@ -141,6 +145,10 @@ class Speaker extends Writable {
debug('setting %o: %o', 'samplesPerFrame', opts.samplesPerFrame)
this.samplesPerFrame = opts.samplesPerFrame
}
if (opts.device != null) {
debug('setting %o: %o', 'device', opts.device)
this.device = opts.device
}
if (opts.endianness == null || endianness === opts.endianness) {
// no "endianness" specified or explicit native endianness
this.endianness = endianness
Expand Down Expand Up @@ -196,8 +204,13 @@ class Speaker extends Writable {
binding.write(handle, b, b.length, onwrite)
}

// var THIS = this; //preserve "this" for onwrite call-back; arrow functions don't have a "this"
const onwrite = (r) => {
debug('wrote %o bytes', r)
debug('wrote %o bytes', r);
// removed -dj 12/15/18; too much latency (and too unpredictable due to node event loop)
// if (isNaN(++THIS.numwr)) THIS.numwr = 1; //track #writes; is this == #frames?
// if (isNaN(THIS.wrtotal += r)) THIS.wrtotal = r; //track total data written
// THIS.emit('progress', {numwr: THIS.numwr, wrlen: r, wrtotal: THIS.wrtotal, buflen: (left || []).length}); //give caller some progress info
if (r !== b.length) {
done(new Error(`write() failed: ${r}`))
} else if (left) {
Expand All @@ -212,6 +225,61 @@ class Speaker extends Writable {
write()
}


/**
* get current playback progress. -dj 12/15/18
* Last couple of writes can be used to estimate current playback status.
* There will always be latency and hence uncertainty, but try to keep to minimum.
* Async event emit would introduce more (variable) latency due to uv event loop,
* so just let caller ask for progress data synchronously.
*
* @api public
*/
progress () {
debug('status()')
if (this._closed) return debug('already closed...')
if (!this.audio_handle) return debug('no handle, closed?');
debug('invoking progess() native binding')
const retval = binding.progess(this.audio_handle);
debug('got back %o from progess()', retval)
return retval
}


/**
* get current playback volume. -dj 12/15/18
* Not sure which value to use (base, actual, rva_db), so get them all.
*
* @api public
*/
get_vol () {
debug('get_vol()')
if (this._closed) return debug('already closed...')
if (!this.audio_handle) return debug('no handle, closed?');
debug('invoking get_vol() native binding')
const retval = binding.volume_get(this.audio_handle);
debug('got back %o from volume_get()', retval)
return retval
}


/**
* set current playback volume. -dj 12/17/18
*
* @api public
* @param {new_vol} float
*/
set_vol (new_vol) {
debug('set_vol() %o new volume', new_vol)
if (this._closed) return debug('already closed...')
if (!this.audio_handle) return debug('no handle, closed?');
debug('invoking set_vol() native binding')
const retval = binding.volume_set(this.audio_handle, new_vol);
debug('got back %o from volume_set()', retval)
return retval
}


/**
* Called when this stream is pipe()d to from another readable stream.
* If the "sampleRate", "channels", "bitDepth", and "signed" properties are
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{
"name": "speaker",
"version": "0.4.0",
"version": "0.4.2",
"license": "MIT",
"description": "Output PCM audio data to the speakers",
"author": "Nathan Rajlich <nathan@tootallnate.net> (http://tootallnate.net)",
"repository": "TooTallNate/node-speaker",
"main": "index.js",
"types": "index.d.ts",
"scripts": {
"test": "standard && node-gyp rebuild --mpg123-backend=dummy && mocha --reporter spec"
},
Expand All @@ -16,6 +18,7 @@
"readable-stream": "^2.3.3"
},
"devDependencies": {
"@types/node": "^10.11.4",
"mocha": "^3.5.0",
"standard": "^10.0.3"
},
Expand Down
Loading