shinobi-sound-detection
Version:
Library to detect sudden noises above ambient from a network stream such as an IP Webcam or network microphone. This library is a modified version of Barry's to work with ShinobiCCTV.
179 lines (152 loc) • 5.94 kB
JavaScript
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, opts) {
var self = this
Transform.call(this, {})
this.opts = _.defaults(opts || {}, {
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`.
var StreamEncoder = module.exports.StreamEncoder = function(format, opts) {
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
, decodeFunc = 'readInt' + (format.signed ? '' : 'U') + format.bitDepth + format.endianness
, i, 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 (i = 0; i < frameCount; i++)
chArray[i] = data[decodeFunc](byteDepth * (i * 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
, encodeFunc = 'writeInt' + (format.signed ? '' : 'U') + format.bitDepth + format.endianness
, i, ch, chArray, buffer, frameCount
return function(array) {
frameCount = array[0].length
buffer = new Buffer(frameCount * byteDepth * numberOfChannels)
for (ch = 0; ch < numberOfChannels; ch++) {
chArray = array[ch]
for (i = 0; i < frameCount; i++)
buffer[encodeFunc](
Math.max(Math.min(Math.round(chArray[i] * pcmMult), pcmMax), pcmMin),
byteDepth * (i * 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
}
var sliceBlock = module.exports.sliceBlock = function(block) {
var sliceArgs = _.toArray(arguments).slice(1)
return block.map(function(chArray) {
return chArray.slice.apply(chArray, sliceArgs)
})
}
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(val) {
return _.contains(values, val)
}
}