UNPKG

@seydx/sound-detection

Version:

Library to detect sudden noises above ambient from a network stream such as an IP Webcam or network microphone.

190 lines (167 loc) 6.31 kB
var _ = require('underscore'), inherits = require('util').inherits, Transform = require('stream').Transform, chai = require('chai'), expect = chai.expect; chai.config.includeStack = true; // This creates a read/write stream, which takes buffers of data as input, // and outputs decoded data as arrays. // The data is decoded using a `BufferDecoder`, and `format` is the same as for `BufferDecoder`. var StreamDecoder = (module.exports.StreamDecoder = function (format, options) { // eslint-disable-next-line unicorn/no-this-assignment var self = this; Transform.call(this, {}); this.opts = _.defaults(options || {}, { pad: false, }); this.format = format; this._decoder = new BufferDecoder(format); this._bytesPerFrame = Math.round(this.format.bitDepth / 8) * this.format.numberOfChannels; this.once('end', function () { self._hasEnded = true; }); }); inherits(StreamDecoder, Transform); _.extend(StreamDecoder.prototype, Transform.prototype, { // Reads a block read: function (frameCount) { var buffer, size; if (frameCount !== undefined) size = frameCount * this._bytesPerFrame; buffer = Transform.prototype.read.call(this, size); if (buffer && buffer.length < size && this.opts.pad) { var block = this._decoder(buffer); return concatBlocks(block, makeBlock(this.format.numberOfChannels, frameCount - block[0].length)); } else return buffer ? this._decoder(buffer) : buffer; }, _transform: function (buffer, encoding, done) { this.push(buffer); done(); }, }); // This creates a read/write stream, which takes arrays as input, and outputs // buffers of encoded data. // The data is encoded using a `BufferEncoder`, and `format` is the same as for `BufferEncoder`. // eslint-disable-next-line no-unused-vars var StreamEncoder = (module.exports.StreamEncoder = function (format, options) { Transform.call(this, {}); this.format = format; this._encoder = new BufferEncoder(format); }); inherits(StreamEncoder, Transform); _.extend(StreamEncoder.prototype, Transform.prototype, { write: function (block) { return Transform.prototype.write.call(this, this._encoder(block)); }, _transform: function (buffer, encoding, done) { this.push(buffer); done(); }, }); // Creates and returns a function which decodes node `Buffer` // to an array of `Float32Array`, each corresponding to one channel. // `format` configures the decoder, and should contain `bitDepth` and `numberOfChannels`. // !!! If the data contains some incomplete samples they will be dropped // TODO : format.signed, pcmMax is different if unsigned var BufferDecoder = (module.exports.BufferDecoder = function (format) { format = validateFormat(format); var byteDepth = Math.round(format.bitDepth / 8), numberOfChannels = format.numberOfChannels, pcmMax = Math.pow(2, format.bitDepth) / 2 - 1, decodeFunction = 'readInt' + (format.signed ? '' : 'U') + format.bitDepth + format.endianness, index, ch, chArray, array, frameCount; return function (data) { frameCount = Math.round(data.length / (byteDepth * numberOfChannels)); array = []; // Push samples to each channel for (ch = 0; ch < numberOfChannels; ch++) { chArray = new Float32Array(frameCount); array.push(chArray); for (index = 0; index < frameCount; index++) chArray[index] = data[decodeFunction](byteDepth * (index * numberOfChannels + ch)) / pcmMax; } return array; }; }); // Creates and returns a function which encodes an array of Float32Array - each of them // a separate channel - to a node `Buffer`. // `format` configures the encoder, and should contain `bitDepth` and `numberOfChannels`. // !!! This does not check that the data received matches the specified 'format'. // TODO : format.signed, pcmMax is different if unsigned var BufferEncoder = (module.exports.BufferEncoder = function (format) { format = validateFormat(format); var byteDepth = Math.round(format.bitDepth / 8), numberOfChannels = format.numberOfChannels, pcmMult = Math.pow(2, format.bitDepth) / 2, pcmMax = pcmMult - 1, pcmMin = -pcmMult, encodeFunction = 'writeInt' + (format.signed ? '' : 'U') + format.bitDepth + format.endianness, index, ch, chArray, buffer, frameCount; return function (array) { frameCount = array[0].length; buffer = Buffer.from(frameCount * byteDepth * numberOfChannels); for (ch = 0; ch < numberOfChannels; ch++) { chArray = array[ch]; for (index = 0; index < frameCount; index++) buffer[encodeFunction]( Math.max(Math.min(Math.round(chArray[index] * pcmMult), pcmMax), pcmMin), byteDepth * (index * numberOfChannels + ch) ); } return buffer; }; }); var makeBlock = (module.exports.makeBlock = function (numberOfChannels, length) { var block = [], ch; for (ch = 0; ch < numberOfChannels; ch++) block.push(new Float32Array(length)); return block; }); var concatBlocks = (module.exports.concatBlocks = function (block1, block2) { var numberOfChannels = block1.length, block1Length = block1[0].length, newBlock = makeBlock(numberOfChannels, block1Length + block2[0].length), ch, chArray; for (ch = 0; ch < numberOfChannels; ch++) { chArray = newBlock[ch]; chArray.set(block1[ch]); chArray.set(block2[ch], block1Length); } return newBlock; }); // eslint-disable-next-line no-unused-vars var sliceBlock = (module.exports.sliceBlock = function (block) { var sliceArguments = _.toArray(arguments).slice(1); return block.map(function (chArray) { return chArray.slice.apply(chArray, sliceArguments); }); }); var validateFormat = (module.exports.validateFormat = function (format) { _.defaults(format, { bitDepth: 16, endianness: 'LE', signed: true, }); expect(format.bitDepth) .to.be.a('number') .and.to.satisfy(_oneOf([8, 16, 32])); expect(format.numberOfChannels).to.be.a('number').and.to.be.above(0); expect(format.endianness) .to.be.a('string') .and.to.satisfy(_oneOf(['LE', 'BE'])); expect(format.signed).to.be.a('boolean'); return format; }); var _oneOf = function (values) { return function (value) { return _.contains(values, value); }; };