audio-buffer-list
Version:
Data structure for sequence of AudioBuffers
489 lines (381 loc) • 12.9 kB
JavaScript
/**
* AudioBufferList class
*
* @module audio-buffer/buffer
*
*/
'use strict'
var isAudioBuffer = require('is-audio-buffer')
var util = require('audio-buffer-utils')
var extend = require('object-assign')
var nidx = require('negative-index')
var isPlainObj = require('is-plain-obj')
var AudioBuffer = require('audio-buffer')
module.exports = AudioBufferList
// @constructor
function AudioBufferList(arg, options) {
if (!(this instanceof AudioBufferList)) return new AudioBufferList(arg, options)
if (isPlainObj(arg)) {
options = arg
arg = null
}
if (typeof options === 'number') {
options = {channels: options}
}
if (options && options.channels != null) options.numberOfChannels = options.channels
extend(this, options)
this.buffers = []
this.length = 0
this.duration = 0
this.append(arg)
}
//AudioBuffer interface
AudioBufferList.prototype.numberOfChannels = 1
AudioBufferList.prototype.sampleRate = null
//copy from channel into destination array
AudioBufferList.prototype.copyFromChannel = function (destination, channel, from, to) {
if (from == null) from = 0
if (to == null) to = this.length
from = nidx(from, this.length)
to = nidx(to, this.length)
var fromOffset = this.offset(from)
var bufOffset = from - fromOffset[1]
var initialOffset = from
var toOffset = this.offset(to)
if (fromOffset[0] === toOffset[0]) {
var buf = this.buffers[fromOffset[0]]
var data = buf.getChannelData(channel).subarray(fromOffset[1], toOffset[1])
destination.set(data)
return this
}
for (var i = fromOffset[0], l = toOffset[0]; i < l; i++) {
var buf = this.buffers[i]
var data = buf.getChannelData(channel)
if (from > bufOffset) data = data.subarray(from - bufOffset)
if (channel < buf.numberOfChannels) {
destination.set(data, Math.max(0, -initialOffset + bufOffset))
}
bufOffset += buf.length
}
var lastBuf = this.buffers[toOffset[0]]
if (toOffset[1]) {
destination.set(lastBuf.getChannelData(channel).subarray(0, toOffset[1]), Math.max(0, bufOffset - initialOffset))
}
return this
}
//put data from array to channel
AudioBufferList.prototype.copyToChannel = function (source, channel, from) {
if (from == null) from = 0
from = nidx(from, this.length)
var offsets = this.offset(from)
var bufOffset = from - offsets[1]
source = source.subarray(0, this.length - from)
for (var i = offsets[0], l = this.buffers.length; i < l; i++) {
var buf = this.buffers[i]
var channelData = buf.getChannelData(channel)
if (channel < buf.numberOfChannels) {
var chunk = source.subarray(Math.max(0, bufOffset - from), bufOffset - from + buf.length)
channelData.set(chunk, from > bufOffset ? from : 0);
}
bufOffset += buf.length
}
return this
}
//patch BufferList methods
AudioBufferList.prototype.append = function (buf) {
//FIXME: we may want to do resampling/channel mapping here or something
var i = 0
// unwrap argument into individual BufferLists
if (buf instanceof AudioBufferList) {
this.append(buf.buffers)
}
else if (isAudioBuffer(buf) && buf.length) {
this._appendBuffer(buf)
}
else if (Array.isArray(buf) && typeof buf[0] !== 'number') {
for (var l = buf.length; i < l; i++) {
this.append(buf[i])
}
}
//create AudioBuffer from (possibly num) arg
else if (buf) {
buf = util.create(buf, this.numberOfChannels, this.sampleRate)
this._appendBuffer(buf)
}
return this
}
AudioBufferList.prototype._appendBuffer = function (buf) {
if (!buf) return this
//update channels count
if (!this.buffers.length) {
this.numberOfChannels = buf.numberOfChannels
}
else {
this.numberOfChannels = Math.max(this.numberOfChannels, buf.numberOfChannels)
}
this.duration += buf.duration
//init sampleRate
if (!this.sampleRate) this.sampleRate = buf.sampleRate
//push buffer
this.buffers.push(buf)
this.length += buf.length
return this
}
AudioBufferList.prototype.offset = function _offset (offset) {
var tot = 0, i = 0, _t
if (offset === 0) return [ 0, 0 ]
for (; i < this.buffers.length; i++) {
_t = tot + this.buffers[i].length
if (offset < _t || i == this.buffers.length - 1)
return [ i, offset - tot ]
tot = _t
}
}
//copy data to destination audio buffer
AudioBufferList.prototype.copy = function copy (dst, dstStart, srcStart, srcEnd) {
if (typeof dst === 'number') {
srcEnd = srcStart;
srcStart = dstStart
dstStart = dst;
dst = null;
}
if (srcEnd == null && srcStart != null) {
srcEnd = srcStart
srcStart = dstStart
dstStart = 0
}
if (typeof srcStart != 'number' || srcStart < 0)
srcStart = 0
if (typeof srcEnd != 'number' || srcEnd > this.length)
srcEnd = this.length
if (srcStart >= this.length)
return dst || new AudioBuffer(null, {length: 0})
if (srcEnd <= 0)
return dst || new AudioBuffer(null, {length: 0})
var copy = !!dst
, off = this.offset(srcStart)
, len = srcEnd - srcStart
, bytes = len
, bufoff = (copy && dstStart) || 0
, start = off[1]
, l
, i
// copy/slice everything
if (srcStart === 0 && srcEnd == this.length) {
if (!copy) { // slice, but full concat if multiple buffers
return this.buffers.length === 1
? util.slice(this.buffers[0])
: util.concat(this.buffers)
}
// copy, need to copy individual buffers
for (i = 0; i < this.buffers.length; i++) {
util.copy(this.buffers[i], dst, bufoff)
bufoff += this.buffers[i].length
}
return dst
}
// easy, cheap case where it's a subset of one of the buffers
if (bytes <= this.buffers[off[0]].length - start) {
return copy
? util.copy(util.subbuffer(this.buffers[off[0]], start, start + bytes), dst, dstStart)
: util.slice(this.buffers[off[0]], start, start + bytes)
}
if (!copy) // a slice, we need something to copy in to
dst = util.create(len, this.numberOfChannels)
for (i = off[0]; i < this.buffers.length; i++) {
l = this.buffers[i].length - start
if (bytes > l) {
util.copy(util.subbuffer(this.buffers[i], start), dst, bufoff)
} else {
util.copy(util.subbuffer(this.buffers[i], start, start + bytes), dst, bufoff)
break
}
bufoff += l
bytes -= l
if (start)
start = 0
}
return dst
}
//create a new list with the same data
AudioBufferList.prototype.clone = function clone (start, end) {
var i = 0, copy = new AudioBufferList(0, this.numberOfChannels), sublist = this.slice(start, end)
for (; i < sublist.buffers.length; i++)
copy.append(util.clone(sublist.buffers[i]))
return copy
}
//do superficial handle
AudioBufferList.prototype.slice = function slice (start, end) {
start = start || 0
end = end == null ? this.length : end
start = nidx(start, this.length)
end = nidx(end, this.length)
if (start == end) {
return new AudioBufferList(0, this.numberOfChannels)
}
var startOffset = this.offset(start)
, endOffset = this.offset(end)
, buffers = this.buffers.slice(startOffset[0], endOffset[0] + 1)
if (endOffset[1] == 0) {
buffers.pop()
}
else {
buffers[buffers.length-1] = util.subbuffer(buffers[buffers.length-1], 0, endOffset[1])
}
if (startOffset[1] != 0) {
buffers[0] = util.subbuffer(buffers[0], startOffset[1])
}
return new AudioBufferList(buffers, this.numberOfChannels)
}
//clean up
AudioBufferList.prototype.destroy = function destroy () {
this.buffers.length = 0
this.length = 0
this.buffers = null
}
//repeat contents N times
AudioBufferList.prototype.repeat = function (times) {
times = Math.floor(times)
if (!times && times !== 0 || !Number.isFinite(times)) throw RangeError('Repeat count must be non-negative number.')
if (!times) {
this.remove(0, this.length)
return this
}
if (times === 1) return this
var data = this
for (var i = 1; i < times; i++) {
data = new AudioBufferList(data.copy())
this.append(data)
}
return this
}
//insert new buffer/buffers at the offset
AudioBufferList.prototype.insert = function (offset, source) {
if (source == null) {
source = offset
offset = 0
}
offset = nidx(offset, this.length)
this.split(offset)
offset = this.offset(offset)
//convert any type of source to audio buffer list
source = new AudioBufferList(source)
this.buffers.splice.apply(this.buffers, [offset[0] + (offset[1] ? 1 : 0), 0].concat(source.buffers))
//update params
this.length += source.length
this.duration += source.duration
this.numberOfChannels = Math.max(source.numberOfChannels, this.numberOfChannels)
return this
}
//delete N samples from any position
AudioBufferList.prototype.remove = function (offset, count) {
if (!this.length) return null
if (count == null) {
count = offset
offset = 0
}
if (!count) return this
if (count < 0) {
count = -count
offset -= count
}
offset = nidx(offset, this.length)
count = Math.min(this.length - offset, count)
this.split(offset, offset + count)
var offsetLeft = this.offset(offset)
var offsetRight = this.offset(offset + count)
if (offsetRight[1] === this.buffers[offsetRight[0]].length) {
offsetRight[0] += 1
}
let deleted = this.buffers.splice(offsetLeft[0], offsetRight[0] - offsetLeft[0])
deleted = new AudioBufferList(deleted, this.numberOfChannels)
this.length -= deleted.length
this.duration = this.length / this.sampleRate
return deleted
}
//return new list via applying fn to each buffer from the indicated range
AudioBufferList.prototype.map = function map (fn, from, to) {
let options = arguments[arguments.length - 1]
if (!isPlainObj(options)) options = {reversed: false}
if (typeof from != 'number') from = 0
if (typeof to != 'number') to = this.length
from = nidx(from, this.length)
to = nidx(to, this.length)
this.split(from, to)
let fromOffset = this.offset(from)
let toOffset = this.offset(to)
if (options.reversed) {
let offset = to - toOffset[1]
for (let i = toOffset[0], l = fromOffset[0]; i >= l; i--) {
let buf = this.buffers[i]
let res = fn.call(this, buf, i, offset, this.buffers, this)
if (res === false) break
if (res !== undefined) this.buffers[i] = res
offset -= buf.length
}
}
else {
if (toOffset[1]) {
toOffset[0] += 1
toOffset[1] = 0
}
let offset = from - fromOffset[1]
for (let i = fromOffset[0], l = toOffset[0]; i < l; i++) {
let buf = this.buffers[i]
let res = fn.call(this, buf, i, offset, this.buffers, this)
if (res === false) break
if (res !== undefined) {
this.buffers[i] = res
}
offset += buf.length
}
}
this.buffers = this.buffers.filter(buf => {
return buf ? !!buf.length : false
})
let l = 0
for (let i = 0; i < this.buffers.length; i++) {
this.numberOfChannels = Math.max(this.buffers[i].numberOfChannels, this.numberOfChannels)
l += this.buffers[i].length
}
this.length = l
this.duration = this.length / this.sampleRate
return this
}
//split at the indicated indexes
AudioBufferList.prototype.split = function split () {
let args = arguments;
for (let i = 0; i < args.length; i++ ) {
let arg = args[i]
if (Array.isArray(arg)) {
this.split.apply(this, arg)
}
else if (typeof arg === 'number') {
let offset = this.offset(arg)
let buf = this.buffers[offset[0]]
if (offset[1] > 0 && offset[1] < buf.length) {
let left = util.subbuffer(buf, 0, offset[1])
let right = util.subbuffer(buf, offset[1])
this.buffers.splice(offset[0], 1, left, right)
}
}
}
return this
}
//join buffers within the subrange
AudioBufferList.prototype.join = function join (from, to) {
if (from == null) from = 0
if (to == null) to = this.length
from = nidx(from, this.length)
to = nidx(to, this.length)
let fromOffset = this.offset(from)
let toOffset = this.offset(to)
if (toOffset[1]) {
toOffset[0] += 1
toOffset[1] = 0
}
let bufs = this.buffers.slice(fromOffset[0], toOffset[0])
let buf = util.concat(bufs)
this.buffers.splice.apply(this.buffers, [fromOffset[0], toOffset[0] - fromOffset[0] + (toOffset[1] ? 1 : 0)].concat(buf))
return buf
}