linx-simulator2/node_modules/speaker/index.js
2019-09-18 11:11:16 +03:00

358 lines
10 KiB
JavaScript

'use strict'
/**
* Module dependencies.
*/
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
const endianness = os.endianness()
/**
* 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
*/
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
super(opts)
// 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
// the `audio_output_t` struct pointer Buffer instance
this.audio_handle = null
// 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)
}
/**
* Calls the audio backend's `open()` function, and then emits an "open" event.
*
* @api private
*/
_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
}
if (this.device == null) {
debug('setting default %o: %o', 'device', null)
this.device = null
}
const format = Speaker.getFormat(this)
if (format == null) {
throw new Error('invalid PCM format specified')
}
if (!Speaker.isSupported(format)) {
throw new Error(`specified PCM format is not supported by "${binding.name}" backend`)
}
// calculate the "block align"
this.blockAlign = this.bitDepth / 8 * this.channels
// 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, this.device)
if (r !== 0) {
throw new Error(`open() failed: ${r}`)
}
this.emit('open')
return this.audio_handle
}
/**
* 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
*/
_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.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
} 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
*/
_write (chunk, encoding, done) {
debug('_write() (%o bytes)', chunk.length)
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
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)
}
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()
}
/**
* 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)
}
/**
* 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
*/
_unpipe (source) {
debug('_unpipe()')
source.removeListener('format', this._format)
}
/**
* Emits a "flush" event and then calls the `.close()` function on
* this Speaker instance.
*
* @api private
*/
_flush () {
debug('_flush()')
this.emit('flush')
this.close(false)
}
/**
* 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
*/
close (flush) {
debug('close(%o)', flush)
if (this._closed) return debug('already closed...')
if (this.audio_handle) {
if (flush !== false) {
// 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')
}
}
/**
* Export information about the `mpg123_module_t` being used.
*/
Speaker.api_version = binding.api_version
Speaker.description = binding.description
Speaker.module_name = binding.name
/**
* 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
*/
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
}
}
/**
* 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
*/
Speaker.isSupported = function isSupported (format) {
if (typeof format !== 'number') format = Speaker.getFormat(format)
return (binding.formats & format) === format
}
/**
* Module exports.
*/
exports = module.exports = Speaker