src/manipulations.js
/**
* Extend audio with manipulations functionality
*
* @module audio/src/manipulations
*/
'use strict'
const clamp = require('clamp')
const convert = require('pcm-convert')
const bufferFrom = require('audio-buffer-from')
const isPlainObj = require('is-plain-obj')
const aFormat = require('audio-format')
const AudioBufferList = require('audio-buffer-list')
let { parseArgs } = require('./util')
let Audio = require('../')
// return channels data distributed in array
Audio.prototype.read = function (t, d, options) {
let {start, end, from, to, duration, format, channels, destination, channel, length} = parseArgs(this, t, d, options)
//transfer data for indicated channels
let data = []
for (let c = 0; c < channels.length; c++) {
let arr = new Float32Array(length)
this.buffer.copyFromChannel(arr, channels[c], start, end)
data.push(arr)
}
if (format === 'audiobuffer') {
data = bufferFrom(data, {sampleRate: this.sampleRate})
return data
}
if (format || destination) {
//pre-convert data to float32 array
let len = data[0].length
let arr = new Float32Array(data.length * len)
for (let c = 0; c < data.length; c++) {
arr.set(data[c], c*len)
}
data = convert(arr, 'float32', format, destination)
}
else if (ArrayBuffer.isView(data[0])) {
//make sure data items are arrays
data = data.map(ch => Array.from(ch))
if (typeof channel == 'number') {
if (channel >= this.channels) throw Error('Bad channel number `' + channel + '`')
data = data[0]
}
}
return data
}
// put data by the offset
Audio.prototype.write = function write (value, time, duration, options) {
let {start, end, length, channels, format} = parseArgs(this, time, duration, options)
// fill with value
if (typeof value === 'number') {
this.buffer.map((buf, idx, offset) => {
for (let c = 0, l = buf.length; c < channels.length; c++) {
let channel = channels[c]
let data = buf.getChannelData(channel)
for (let i = 0; i < l; i++) {
data[i] = value
}
}
}, start, end)
}
// fill with function
else if (typeof value === 'function') {
this.buffer.map((buf, idx, offset) => {
for (let c = 0, l = buf.length; c < channels.length; c++) {
let channel = channels[c]
let data = buf.getChannelData(channel)
for (let i = 0; i < l; i++) {
data[i] = value.call(this, data[i], i + offset, c, this)
}
}
}, start, end)
}
// write any other argument
else {
let buf = bufferFrom(value instanceof Audio ? value.buffer : value, { format })
for (let c = 0, l = Math.min(channels.length, buf.numberOfChannels); c < l; c++ ) {
let channel = channels[c]
let data = buf.getChannelData(c).subarray(0, length)
this.buffer.copyToChannel(data, channel, start)
}
}
return this
}
// insert new data at the offset
Audio.prototype.insert = function (value, time, duration, options) {
let toEnd = false
if (time == null || isPlainObj(time)) {
toEnd = true
}
let {start, end, length, channels, format} = parseArgs(this, time, duration, options)
//make sure audio is padded till the indicated time
if (time > this.duration) {
this.pad(time, {right: true})
}
let buffer, isFn = typeof value === 'function'
// fill with value/function
if (typeof value === 'number' || isFn) {
buffer = bufferFrom(length, {channels: this.channels})
for (let c = 0, l = buffer.length; c < channels.length; c++) {
let channel = channels[c]
let data = buffer.getChannelData(channel)
for (let i = 0; i < l; i++) {
data[i] = isFn ? value.call(this, data[i], i + offset, c, this) : value
}
}
}
// write any other argument
else {
let src = bufferFrom(value instanceof Audio ? value.buffer : value, { format })
buffer = bufferFrom(src.length, {channels: this.channels})
for (let c = 0, l = Math.min(channels.length, src.numberOfChannels); c < l; c++ ) {
let channel = channels[c]
let data = src.getChannelData(c)
if (length) data.subarray(0, length)
buffer.getChannelData(channel).set(data)
}
}
// TODO: refactor to use updated audio-buffer-list notation
this.buffer.insert(toEnd ? end : start, buffer)
return this
}
// remove data at the offset
Audio.prototype.remove = function remove (time, duration, options) {
let o = parseArgs(this, time, duration, options)
let fragment = this.buffer.remove(o.start, o.length)
if (!o.keep) return this
return Audio(fragment)
}
// return sliced [copy] of audio
Audio.prototype.slice = function slice (time, duration, options) {
let {start, end, channels, copy} = parseArgs(this, time, duration, options)
if (copy == null) copy = true
if (copy) {
let src = this.buffer.slice(start, end)
let copy = new AudioBufferList(0, channels.length)
for (let i = 0; i < src.buffers.length; i++) {
let buffer = src.buffers[i]
let buf = bufferFrom(buffer.length, {
channels: channels.length,
sampleRate: buffer.sampleRate
});
// FIXME: channels are unnecessary extension here
for (let c = 0; c < channels.length; c++) {
let channel = channels[c]
let data = buf.getChannelData(c)
data.set(buffer.getChannelData(channel))
}
// FIXME: use insert
copy.append(buf)
}
return new Audio(copy)
}
this.buffer = this.buffer.slice(start, end)
return this
}
// apply processing function
Audio.prototype.through = function (fn, time, duration, options) {
if(typeof fn !== 'function') throw Error('First argument should be a function')
options = parseArgs(this, time, duration, options)
//make sure we split at proper positions
this.buffer.split(options.start)
this.buffer.split(options.end)
//apply processor
this.buffer.map((buf, idx, offset) => {
return fn(buf) || buf
}, options.start, options.end)
return this
}
// normalize contents by the offset
Audio.prototype.normalize = function normalize (time, duration, options) {
options = parseArgs(this, time, duration, options)
//find max amplitude for the channels set
let range = this.limits(options)
let max = Math.max(Math.abs(range[0]), Math.abs(range[1]))
let amp = Math.max(1 / max, 1)
//amp values
this.buffer.map((buf, idx, offset) => {
for (let c = 0, l = buf.length; c < options.channels.length; c++) {
let channel = options.channels[c]
let data = buf.getChannelData(channel)
for (let i = 0; i < l; i++) {
data[i] = clamp(data[i] * amp, -1, 1)
}
}
}, options.start, options.end)
return this
}
// fade in/out by db range
Audio.prototype.fade = function (time, duration, options) {
//first arg goes duration by default
if (typeof duration != 'number' || duration == null) {
duration = time;
time = 0;
}
options = parseArgs(this, time, duration, options)
let easing = typeof options.easing === 'function' ? options.easing : t => t
let step = options.duration > 0 ? 1 : -1
let halfStep = step*.5
let len = options.length
let gain
if (options.level != null) {
gain = Audio.db(options.level)
}
else {
gain = options.gain == null ? -40 : options.gain
}
this.buffer.map((buf, idx, offset) => {
for (let c = 0, l = buf.length; c < options.channels.length; c++) {
let channel = options.channels[c]
let data = buf.getChannelData(channel)
for (let i = Math.max(options.start - offset, 0); i != options.end; i+= step) {
let idx = Math.floor(i + halfStep)
let t = (i + halfStep - options.start) / len
//volume is mapped by easing and 0..-40db
data[idx] *= Audio.gain(-easing(t) * gain + gain)
}
}
}, options.start, options.end)
return this
}
// trim start/end silence
Audio.prototype.trim = function trim (options) {
let {threshold, left, right} = parseArgs(this, options)
if (threshold == null) threshold = -40
if (left && right == null) right = false
else if (right && left == null) left = false
if (left == null) left = true
if (right == null) right = true
let tlr = Audio.gain(threshold), first = 0, last = this.length;
//trim left
if (left) {
this.buffer.map((buf, idx, offset) => {
for (let c = 0; c < buf.numberOfChannels; c++) {
let data = buf.getChannelData(c)
for (let i = 0; i < buf.length; i++) {
if (Math.abs(data[i]) > tlr) {
first = offset + i
return false
}
}
}
})
}
//trim right
if (right) {
this.buffer.map((buf, idx, offset) => {
for (let c = 0; c < buf.numberOfChannels; c++) {
let data = buf.getChannelData(c)
for (let i = buf.length; i--;) {
if (Math.abs(data[i]) > tlr) {
last = offset + i + 1
return false
}
}
}
}, {reversed: true})
}
this.buffer = this.buffer.slice(first, last)
return this
}
// return audio padded to the duration
Audio.prototype.pad = function pad (duration, options) {
if (typeof options === 'number') {
options = {value: options}
}
if (typeof duration !== 'number') throw Error('First argument should be a number')
let length = this.offset(duration)
let {value, left} = parseArgs(this, options)
if (value == null) value = 0
//ignore already lengthy audio
if (length <= this.length) return this
let buf = bufferFrom(length - this.length, {channels: this.channels, rate: this.sampleRate})
if (value) {
let v = value
let channels = this.channels
for (let c = 0; c < channels; c++) {
let data = buf.getChannelData(c)
for (let i = 0, l = buf.length; i < l; i++) {
data[i] = v
}
}
}
//pad left
if (left) {
this.buffer.insert(0, buf)
}
//pad right
else {
this.buffer.append(buf)
}
return this
}
// move samples within audio by amount
Audio.prototype.shift = function shift (amount, options) {
let {rotate, channels} = parseArgs(this, options)
if (typeof amount !== 'number') throw Error('First argument should be a number')
let length = this.offset(Math.abs(amount))
let offset = amount < 0 ? -length : length;
// short way
if (channels.length === this.channels) {
let remains = Math.max(this.length - length, 0)
let remBuffer, fillBuffer
if (!rotate) {
fillBuffer = bufferFrom(this.length - remains, {
channels: this.channels,
sampleRate: this.sampleRate
})
}
if (offset > 0) {
remBuffer = this.buffer.slice(0, remains)
if (rotate) fillBuffer = this.buffer.slice(-this.length + remains)
this.buffer = new AudioBufferList([fillBuffer, remBuffer], this.channels)
}
else {
remBuffer = this.buffer.slice(-remains)
if (rotate) fillBuffer = this.buffer.slice(0, this.length - remains)
this.buffer = new AudioBufferList([remBuffer, fillBuffer], this.channels)
}
}
else {
// TODO: impl channel shift
throw Error('Unimplemented: channel shift')
}
return this
}
// regain audio
Audio.prototype.gain = function (gain=0, time, duration, options) {
if (!gain) return this
options = parseArgs(this, time, duration, options)
let level = Audio.gain(gain)
this.buffer.map((buf, idx, offset) => {
for (let c = 0, cnum = options.channels.length; c < cnum; c++) {
let channel = options.channels[c]
let data = buf.getChannelData(channel)
for (let i = 0, l = buf.length; i < l; i++) {
data[i] *= level
}
}
}, options.start, options.end)
return this
}
// reverse sequence of samples
Audio.prototype.reverse = function (start, duration, options) {
options = parseArgs(this, start, duration, options)
this.buffer.join(options.start, options.end)
this.buffer.map((buf, idx, offset) => {
// console.log(idx)
for (let c = 0, cnum = options.channels.length; c < cnum; c++) {
let channel = options.channels[c]
let data = buf.getChannelData(channel)
Array.prototype.reverse.call(data)
}
}, options.start, options.end)
return this
}
// invert sequence of samples
Audio.prototype.invert = function (time, duration, options) {
options = parseArgs(this, time, duration, options)
this.buffer.map((buf, idx, offset) => {
for (let c = 0, l = buf.length; c < options.channels.length; c++) {
let channel = options.channels[c]
let data = buf.getChannelData(channel)
for (let i = 0; i < l; i++) {
data[i] = -data[i]
}
}
}, options.start, options.end)
return this
}
// regulate rate of playback/output/read etc
Audio.prototype.rate = function rate () {
return this
}
Audio.prototype.mix = function mix () {
return this
}