waveform-data
Version:
Audio Waveform Data Manipulation API – resample, offset and segment waveform data in JavaScript
796 lines (741 loc) • 33.3 kB
JavaScript
/**
* Provides access to the waveform data for a single audio channel.
*/
function WaveformDataChannel(waveformData, channelIndex) {
this._waveformData = waveformData;
this._channelIndex = channelIndex;
}
/**
* Returns the waveform minimum at the given index position.
*/
WaveformDataChannel.prototype.min_sample = function (index) {
var offset = (index * this._waveformData.channels + this._channelIndex) * 2;
return this._waveformData._at(offset);
};
/**
* Returns the waveform maximum at the given index position.
*/
WaveformDataChannel.prototype.max_sample = function (index) {
var offset = (index * this._waveformData.channels + this._channelIndex) * 2 + 1;
return this._waveformData._at(offset);
};
/**
* Sets the waveform minimum at the given index position.
*/
WaveformDataChannel.prototype.set_min_sample = function (index, sample) {
var offset = (index * this._waveformData.channels + this._channelIndex) * 2;
return this._waveformData._set_at(offset, sample);
};
/**
* Sets the waveform maximum at the given index position.
*/
WaveformDataChannel.prototype.set_max_sample = function (index, sample) {
var offset = (index * this._waveformData.channels + this._channelIndex) * 2 + 1;
return this._waveformData._set_at(offset, sample);
};
/**
* Returns all the waveform minimum values as an array.
*/
WaveformDataChannel.prototype.min_array = function () {
var length = this._waveformData.length;
var values = [];
for (var i = 0; i < length; i++) {
values.push(this.min_sample(i));
}
return values;
};
/**
* Returns all the waveform maximum values as an array.
*/
WaveformDataChannel.prototype.max_array = function () {
var length = this._waveformData.length;
var values = [];
for (var i = 0; i < length; i++) {
values.push(this.max_sample(i));
}
return values;
};
/**
* AudioBuffer-based WaveformData generator
*
* Adapted from BlockFile::CalcSummary in Audacity, with permission.
* See https://github.com/audacity/audacity/blob/
* 1108c1376c09166162335fab4743008cba57c4ee/src/BlockFile.cpp#L198
*/
var INT8_MAX = 127;
var INT8_MIN = -128;
var INT16_MAX = 32767;
var INT16_MIN = -32768;
function calculateWaveformDataLength(audio_sample_count, scale) {
var data_length = Math.floor(audio_sample_count / scale);
var samples_remaining = audio_sample_count - data_length * scale;
if (samples_remaining > 0) {
data_length++;
}
return data_length;
}
function generateWaveformData(options) {
var scale = options.scale;
var amplitude_scale = options.amplitude_scale;
var split_channels = options.split_channels;
var length = options.length;
var sample_rate = options.sample_rate;
var channels = options.channels.map(function (channel) {
return new Float32Array(channel);
});
var output_channels = split_channels ? channels.length : 1;
var header_size = 24;
var data_length = calculateWaveformDataLength(length, scale);
var bytes_per_sample = options.bits === 8 ? 1 : 2;
var total_size = header_size + data_length * 2 * bytes_per_sample * output_channels;
var buffer = new ArrayBuffer(total_size);
var data_view = new DataView(buffer);
var scale_counter = 0;
var offset = header_size;
var min_value = new Array(output_channels);
var max_value = new Array(output_channels);
for (var channel = 0; channel < output_channels; channel++) {
min_value[channel] = Infinity;
max_value[channel] = -Infinity;
}
var range_min = options.bits === 8 ? INT8_MIN : INT16_MIN;
var range_max = options.bits === 8 ? INT8_MAX : INT16_MAX;
data_view.setInt32(0, 2, true); // Version
data_view.setUint32(4, options.bits === 8, true); // Is 8 bit?
data_view.setInt32(8, sample_rate, true); // Sample rate
data_view.setInt32(12, scale, true); // Scale
data_view.setInt32(16, data_length, true); // Length
data_view.setInt32(20, output_channels, true);
for (var i = 0; i < length; i++) {
var sample = 0;
if (output_channels === 1) {
for (var _channel = 0; _channel < channels.length; ++_channel) {
sample += channels[_channel][i];
}
sample = Math.floor(range_max * sample * amplitude_scale / channels.length);
if (sample < min_value[0]) {
min_value[0] = sample;
if (min_value[0] < range_min) {
min_value[0] = range_min;
}
}
if (sample > max_value[0]) {
max_value[0] = sample;
if (max_value[0] > range_max) {
max_value[0] = range_max;
}
}
} else {
for (var _channel2 = 0; _channel2 < output_channels; ++_channel2) {
sample = Math.floor(range_max * channels[_channel2][i] * amplitude_scale);
if (sample < min_value[_channel2]) {
min_value[_channel2] = sample;
if (min_value[_channel2] < range_min) {
min_value[_channel2] = range_min;
}
}
if (sample > max_value[_channel2]) {
max_value[_channel2] = sample;
if (max_value[_channel2] > range_max) {
max_value[_channel2] = range_max;
}
}
}
}
if (++scale_counter === scale) {
for (var _channel3 = 0; _channel3 < output_channels; _channel3++) {
if (options.bits === 8) {
data_view.setInt8(offset++, min_value[_channel3]);
data_view.setInt8(offset++, max_value[_channel3]);
} else {
data_view.setInt16(offset, min_value[_channel3], true);
data_view.setInt16(offset + 2, max_value[_channel3], true);
offset += 4;
}
min_value[_channel3] = Infinity;
max_value[_channel3] = -Infinity;
}
scale_counter = 0;
}
}
if (scale_counter > 0) {
for (var _channel4 = 0; _channel4 < output_channels; _channel4++) {
if (options.bits === 8) {
data_view.setInt8(offset++, min_value[_channel4]);
data_view.setInt8(offset++, max_value[_channel4]);
} else {
data_view.setInt16(offset, min_value[_channel4], true);
data_view.setInt16(offset + 2, max_value[_channel4], true);
}
}
}
return buffer;
}
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
function isJsonWaveformData(data) {
return data && _typeof(data) === 'object' && 'sample_rate' in data && 'samples_per_pixel' in data && 'bits' in data && 'length' in data && 'data' in data;
}
function isBinaryWaveformData(data) {
var isCompatible = data && _typeof(data) === 'object' && 'byteLength' in data;
if (isCompatible) {
var view = new DataView(data);
var version = view.getInt32(0, true);
if (version !== 1 && version !== 2) {
throw new TypeError('WaveformData.create(): This waveform data version not supported');
}
}
return isCompatible;
}
function convertJsonToBinary(data) {
var waveformData = data.data;
var channels = data.channels || 1;
var header_size = 24; // version 2
var bytes_per_sample = data.bits === 8 ? 1 : 2;
var expected_length = data.length * 2 * channels;
if (waveformData.length !== expected_length) {
throw new Error('WaveformData.create(): Length mismatch in JSON waveform data');
}
var total_size = header_size + waveformData.length * bytes_per_sample;
var array_buffer = new ArrayBuffer(total_size);
var data_object = new DataView(array_buffer);
data_object.setInt32(0, 2, true); // Version
data_object.setUint32(4, data.bits === 8, true);
data_object.setInt32(8, data.sample_rate, true);
data_object.setInt32(12, data.samples_per_pixel, true);
data_object.setInt32(16, data.length, true);
data_object.setInt32(20, channels, true);
var index = header_size;
if (data.bits === 8) {
for (var i = 0; i < waveformData.length; i++) {
data_object.setInt8(index++, waveformData[i], true);
}
} else {
for (var _i = 0; _i < waveformData.length; _i++) {
data_object.setInt16(index, waveformData[_i], true);
index += 2;
}
}
return array_buffer;
}
function isNullOrUndefined(value) {
return value === undefined || value === null;
}
function decodeBase64(base64, enableUnicode) {
var binaryString = atob(base64);
if (enableUnicode) {
var binaryView = new Uint8Array(binaryString.length);
for (var i = 0, n = binaryString.length; i < n; ++i) {
binaryView[i] = binaryString.charCodeAt(i);
}
return String.fromCharCode.apply(null, new Uint16Array(binaryView.buffer));
}
return binaryString;
}
function createURL(base64, sourcemapArg, enableUnicodeArg) {
var sourcemap = sourcemapArg === undefined ? null : sourcemapArg;
var enableUnicode = enableUnicodeArg === undefined ? false : enableUnicodeArg;
var source = decodeBase64(base64, enableUnicode);
var start = source.indexOf('\n', 10) + 1;
var body = source.substring(start) + (sourcemap ? '\/\/# sourceMappingURL=' + sourcemap : '');
var blob = new Blob([body], { type: 'application/javascript' });
return URL.createObjectURL(blob);
}
function createBase64WorkerFactory(base64, sourcemapArg, enableUnicodeArg) {
var url;
return function WorkerFactory(options) {
url = url || createURL(base64, sourcemapArg, enableUnicodeArg);
return new Worker(url, options);
};
}
var WorkerFactory = /*#__PURE__*/createBase64WorkerFactory('Lyogcm9sbHVwLXBsdWdpbi13ZWItd29ya2VyLWxvYWRlciAqLwooZnVuY3Rpb24gKCkgewogICd1c2Ugc3RyaWN0JzsKCiAgLyoqCiAgICogQXVkaW9CdWZmZXItYmFzZWQgV2F2ZWZvcm1EYXRhIGdlbmVyYXRvcgogICAqCiAgICogQWRhcHRlZCBmcm9tIEJsb2NrRmlsZTo6Q2FsY1N1bW1hcnkgaW4gQXVkYWNpdHksIHdpdGggcGVybWlzc2lvbi4KICAgKiBTZWUgaHR0cHM6Ly9naXRodWIuY29tL2F1ZGFjaXR5L2F1ZGFjaXR5L2Jsb2IvCiAgICogICAxMTA4YzEzNzZjMDkxNjYxNjIzMzVmYWI0NzQzMDA4Y2JhNTdjNGVlL3NyYy9CbG9ja0ZpbGUuY3BwI0wxOTgKICAgKi8KCiAgdmFyIElOVDhfTUFYID0gMTI3OwogIHZhciBJTlQ4X01JTiA9IC0xMjg7CiAgdmFyIElOVDE2X01BWCA9IDMyNzY3OwogIHZhciBJTlQxNl9NSU4gPSAtMzI3Njg7CiAgZnVuY3Rpb24gY2FsY3VsYXRlV2F2ZWZvcm1EYXRhTGVuZ3RoKGF1ZGlvX3NhbXBsZV9jb3VudCwgc2NhbGUpIHsKICAgIHZhciBkYXRhX2xlbmd0aCA9IE1hdGguZmxvb3IoYXVkaW9fc2FtcGxlX2NvdW50IC8gc2NhbGUpOwogICAgdmFyIHNhbXBsZXNfcmVtYWluaW5nID0gYXVkaW9fc2FtcGxlX2NvdW50IC0gZGF0YV9sZW5ndGggKiBzY2FsZTsKICAgIGlmIChzYW1wbGVzX3JlbWFpbmluZyA+IDApIHsKICAgICAgZGF0YV9sZW5ndGgrKzsKICAgIH0KICAgIHJldHVybiBkYXRhX2xlbmd0aDsKICB9CiAgZnVuY3Rpb24gZ2VuZXJhdGVXYXZlZm9ybURhdGEob3B0aW9ucykgewogICAgdmFyIHNjYWxlID0gb3B0aW9ucy5zY2FsZTsKICAgIHZhciBhbXBsaXR1ZGVfc2NhbGUgPSBvcHRpb25zLmFtcGxpdHVkZV9zY2FsZTsKICAgIHZhciBzcGxpdF9jaGFubmVscyA9IG9wdGlvbnMuc3BsaXRfY2hhbm5lbHM7CiAgICB2YXIgbGVuZ3RoID0gb3B0aW9ucy5sZW5ndGg7CiAgICB2YXIgc2FtcGxlX3JhdGUgPSBvcHRpb25zLnNhbXBsZV9yYXRlOwogICAgdmFyIGNoYW5uZWxzID0gb3B0aW9ucy5jaGFubmVscy5tYXAoZnVuY3Rpb24gKGNoYW5uZWwpIHsKICAgICAgcmV0dXJuIG5ldyBGbG9hdDMyQXJyYXkoY2hhbm5lbCk7CiAgICB9KTsKICAgIHZhciBvdXRwdXRfY2hhbm5lbHMgPSBzcGxpdF9jaGFubmVscyA/IGNoYW5uZWxzLmxlbmd0aCA6IDE7CiAgICB2YXIgaGVhZGVyX3NpemUgPSAyNDsKICAgIHZhciBkYXRhX2xlbmd0aCA9IGNhbGN1bGF0ZVdhdmVmb3JtRGF0YUxlbmd0aChsZW5ndGgsIHNjYWxlKTsKICAgIHZhciBieXRlc19wZXJfc2FtcGxlID0gb3B0aW9ucy5iaXRzID09PSA4ID8gMSA6IDI7CiAgICB2YXIgdG90YWxfc2l6ZSA9IGhlYWRlcl9zaXplICsgZGF0YV9sZW5ndGggKiAyICogYnl0ZXNfcGVyX3NhbXBsZSAqIG91dHB1dF9jaGFubmVsczsKICAgIHZhciBidWZmZXIgPSBuZXcgQXJyYXlCdWZmZXIodG90YWxfc2l6ZSk7CiAgICB2YXIgZGF0YV92aWV3ID0gbmV3IERhdGFWaWV3KGJ1ZmZlcik7CiAgICB2YXIgc2NhbGVfY291bnRlciA9IDA7CiAgICB2YXIgb2Zmc2V0ID0gaGVhZGVyX3NpemU7CiAgICB2YXIgbWluX3ZhbHVlID0gbmV3IEFycmF5KG91dHB1dF9jaGFubmVscyk7CiAgICB2YXIgbWF4X3ZhbHVlID0gbmV3IEFycmF5KG91dHB1dF9jaGFubmVscyk7CiAgICBmb3IgKHZhciBjaGFubmVsID0gMDsgY2hhbm5lbCA8IG91dHB1dF9jaGFubmVsczsgY2hhbm5lbCsrKSB7CiAgICAgIG1pbl92YWx1ZVtjaGFubmVsXSA9IEluZmluaXR5OwogICAgICBtYXhfdmFsdWVbY2hhbm5lbF0gPSAtSW5maW5pdHk7CiAgICB9CiAgICB2YXIgcmFuZ2VfbWluID0gb3B0aW9ucy5iaXRzID09PSA4ID8gSU5UOF9NSU4gOiBJTlQxNl9NSU47CiAgICB2YXIgcmFuZ2VfbWF4ID0gb3B0aW9ucy5iaXRzID09PSA4ID8gSU5UOF9NQVggOiBJTlQxNl9NQVg7CiAgICBkYXRhX3ZpZXcuc2V0SW50MzIoMCwgMiwgdHJ1ZSk7IC8vIFZlcnNpb24KICAgIGRhdGFfdmlldy5zZXRVaW50MzIoNCwgb3B0aW9ucy5iaXRzID09PSA4LCB0cnVlKTsgLy8gSXMgOCBiaXQ/CiAgICBkYXRhX3ZpZXcuc2V0SW50MzIoOCwgc2FtcGxlX3JhdGUsIHRydWUpOyAvLyBTYW1wbGUgcmF0ZQogICAgZGF0YV92aWV3LnNldEludDMyKDEyLCBzY2FsZSwgdHJ1ZSk7IC8vIFNjYWxlCiAgICBkYXRhX3ZpZXcuc2V0SW50MzIoMTYsIGRhdGFfbGVuZ3RoLCB0cnVlKTsgLy8gTGVuZ3RoCiAgICBkYXRhX3ZpZXcuc2V0SW50MzIoMjAsIG91dHB1dF9jaGFubmVscywgdHJ1ZSk7CiAgICBmb3IgKHZhciBpID0gMDsgaSA8IGxlbmd0aDsgaSsrKSB7CiAgICAgIHZhciBzYW1wbGUgPSAwOwogICAgICBpZiAob3V0cHV0X2NoYW5uZWxzID09PSAxKSB7CiAgICAgICAgZm9yICh2YXIgX2NoYW5uZWwgPSAwOyBfY2hhbm5lbCA8IGNoYW5uZWxzLmxlbmd0aDsgKytfY2hhbm5lbCkgewogICAgICAgICAgc2FtcGxlICs9IGNoYW5uZWxzW19jaGFubmVsXVtpXTsKICAgICAgICB9CiAgICAgICAgc2FtcGxlID0gTWF0aC5mbG9vcihyYW5nZV9tYXggKiBzYW1wbGUgKiBhbXBsaXR1ZGVfc2NhbGUgLyBjaGFubmVscy5sZW5ndGgpOwogICAgICAgIGlmIChzYW1wbGUgPCBtaW5fdmFsdWVbMF0pIHsKICAgICAgICAgIG1pbl92YWx1ZVswXSA9IHNhbXBsZTsKICAgICAgICAgIGlmIChtaW5fdmFsdWVbMF0gPCByYW5nZV9taW4pIHsKICAgICAgICAgICAgbWluX3ZhbHVlWzBdID0gcmFuZ2VfbWluOwogICAgICAgICAgfQogICAgICAgIH0KICAgICAgICBpZiAoc2FtcGxlID4gbWF4X3ZhbHVlWzBdKSB7CiAgICAgICAgICBtYXhfdmFsdWVbMF0gPSBzYW1wbGU7CiAgICAgICAgICBpZiAobWF4X3ZhbHVlWzBdID4gcmFuZ2VfbWF4KSB7CiAgICAgICAgICAgIG1heF92YWx1ZVswXSA9IHJhbmdlX21heDsKICAgICAgICAgIH0KICAgICAgICB9CiAgICAgIH0gZWxzZSB7CiAgICAgICAgZm9yICh2YXIgX2NoYW5uZWwyID0gMDsgX2NoYW5uZWwyIDwgb3V0cHV0X2NoYW5uZWxzOyArK19jaGFubmVsMikgewogICAgICAgICAgc2FtcGxlID0gTWF0aC5mbG9vcihyYW5nZV9tYXggKiBjaGFubmVsc1tfY2hhbm5lbDJdW2ldICogYW1wbGl0dWRlX3NjYWxlKTsKICAgICAgICAgIGlmIChzYW1wbGUgPCBtaW5fdmFsdWVbX2NoYW5uZWwyXSkgewogICAgICAgICAgICBtaW5fdmFsdWVbX2NoYW5uZWwyXSA9IHNhbXBsZTsKICAgICAgICAgICAgaWYgKG1pbl92YWx1ZVtfY2hhbm5lbDJdIDwgcmFuZ2VfbWluKSB7CiAgICAgICAgICAgICAgbWluX3ZhbHVlW19jaGFubmVsMl0gPSByYW5nZV9taW47CiAgICAgICAgICAgIH0KICAgICAgICAgIH0KICAgICAgICAgIGlmIChzYW1wbGUgPiBtYXhfdmFsdWVbX2NoYW5uZWwyXSkgewogICAgICAgICAgICBtYXhfdmFsdWVbX2NoYW5uZWwyXSA9IHNhbXBsZTsKICAgICAgICAgICAgaWYgKG1heF92YWx1ZVtfY2hhbm5lbDJdID4gcmFuZ2VfbWF4KSB7CiAgICAgICAgICAgICAgbWF4X3ZhbHVlW19jaGFubmVsMl0gPSByYW5nZV9tYXg7CiAgICAgICAgICAgIH0KICAgICAgICAgIH0KICAgICAgICB9CiAgICAgIH0KICAgICAgaWYgKCsrc2NhbGVfY291bnRlciA9PT0gc2NhbGUpIHsKICAgICAgICBmb3IgKHZhciBfY2hhbm5lbDMgPSAwOyBfY2hhbm5lbDMgPCBvdXRwdXRfY2hhbm5lbHM7IF9jaGFubmVsMysrKSB7CiAgICAgICAgICBpZiAob3B0aW9ucy5iaXRzID09PSA4KSB7CiAgICAgICAgICAgIGRhdGFfdmlldy5zZXRJbnQ4KG9mZnNldCsrLCBtaW5fdmFsdWVbX2NoYW5uZWwzXSk7CiAgICAgICAgICAgIGRhdGFfdmlldy5zZXRJbnQ4KG9mZnNldCsrLCBtYXhfdmFsdWVbX2NoYW5uZWwzXSk7CiAgICAgICAgICB9IGVsc2UgewogICAgICAgICAgICBkYXRhX3ZpZXcuc2V0SW50MTYob2Zmc2V0LCBtaW5fdmFsdWVbX2NoYW5uZWwzXSwgdHJ1ZSk7CiAgICAgICAgICAgIGRhdGFfdmlldy5zZXRJbnQxNihvZmZzZXQgKyAyLCBtYXhfdmFsdWVbX2NoYW5uZWwzXSwgdHJ1ZSk7CiAgICAgICAgICAgIG9mZnNldCArPSA0OwogICAgICAgICAgfQogICAgICAgICAgbWluX3ZhbHVlW19jaGFubmVsM10gPSBJbmZpbml0eTsKICAgICAgICAgIG1heF92YWx1ZVtfY2hhbm5lbDNdID0gLUluZmluaXR5OwogICAgICAgIH0KICAgICAgICBzY2FsZV9jb3VudGVyID0gMDsKICAgICAgfQogICAgfQogICAgaWYgKHNjYWxlX2NvdW50ZXIgPiAwKSB7CiAgICAgIGZvciAodmFyIF9jaGFubmVsNCA9IDA7IF9jaGFubmVsNCA8IG91dHB1dF9jaGFubmVsczsgX2NoYW5uZWw0KyspIHsKICAgICAgICBpZiAob3B0aW9ucy5iaXRzID09PSA4KSB7CiAgICAgICAgICBkYXRhX3ZpZXcuc2V0SW50OChvZmZzZXQrKywgbWluX3ZhbHVlW19jaGFubmVsNF0pOwogICAgICAgICAgZGF0YV92aWV3LnNldEludDgob2Zmc2V0KyssIG1heF92YWx1ZVtfY2hhbm5lbDRdKTsKICAgICAgICB9IGVsc2UgewogICAgICAgICAgZGF0YV92aWV3LnNldEludDE2KG9mZnNldCwgbWluX3ZhbHVlW19jaGFubmVsNF0sIHRydWUpOwogICAgICAgICAgZGF0YV92aWV3LnNldEludDE2KG9mZnNldCArIDIsIG1heF92YWx1ZVtfY2hhbm5lbDRdLCB0cnVlKTsKICAgICAgICB9CiAgICAgIH0KICAgIH0KICAgIHJldHVybiBidWZmZXI7CiAgfQoKICBvbm1lc3NhZ2UgPSBmdW5jdGlvbiBvbm1lc3NhZ2UoZXZ0KSB7CiAgICB2YXIgYnVmZmVyID0gZ2VuZXJhdGVXYXZlZm9ybURhdGEoZXZ0LmRhdGEpOwoKICAgIC8vIFRyYW5zZmVyIGJ1ZmZlciB0byB0aGUgY2FsbGluZyB0aHJlYWQKICAgIHRoaXMucG9zdE1lc3NhZ2UoYnVmZmVyLCBbYnVmZmVyXSk7CiAgICB0aGlzLmNsb3NlKCk7CiAgfTsKCn0pKCk7Ci8vIyBzb3VyY2VNYXBwaW5nVVJMPXdhdmVmb3JtLWRhdGEtd29ya2VyLmpzLm1hcAoK', null, false);
/* eslint-enable */
/**
* Provides access to waveform data.
*/
function WaveformData(data) {
if (isJsonWaveformData(data)) {
data = convertJsonToBinary(data);
}
if (isBinaryWaveformData(data)) {
this._data = new DataView(data);
this._offset = this._version() === 2 ? 24 : 20;
this._channels = [];
for (var channel = 0; channel < this.channels; channel++) {
this._channels[channel] = new WaveformDataChannel(this, channel);
}
} else {
throw new TypeError('WaveformData.create(): Unknown data format');
}
}
var defaultOptions = {
scale: 512,
bits: 8,
amplitude_scale: 1.0,
split_channels: false,
disable_worker: false
};
function getOptions(options) {
var opts = {
scale: options.scale || defaultOptions.scale,
bits: options.bits || defaultOptions.bits,
amplitude_scale: options.amplitude_scale || defaultOptions.amplitude_scale,
split_channels: options.split_channels || defaultOptions.split_channels,
disable_worker: options.disable_worker || defaultOptions.disable_worker
};
return opts;
}
function getChannelData(audio_buffer) {
var channels = [];
for (var i = 0; i < audio_buffer.numberOfChannels; ++i) {
channels.push(audio_buffer.getChannelData(i).buffer);
}
return channels;
}
function createFromAudioBuffer(audio_buffer, options, callback) {
var channels = getChannelData(audio_buffer);
if (options.disable_worker) {
var buffer = generateWaveformData({
scale: options.scale,
bits: options.bits,
amplitude_scale: options.amplitude_scale,
split_channels: options.split_channels,
length: audio_buffer.length,
sample_rate: audio_buffer.sampleRate,
channels: channels
});
callback(undefined, new WaveformData(buffer), audio_buffer);
} else {
var worker = new WorkerFactory();
worker.onmessage = function (evt) {
callback(undefined, new WaveformData(evt.data), audio_buffer);
};
worker.postMessage({
scale: options.scale,
bits: options.bits,
amplitude_scale: options.amplitude_scale,
split_channels: options.split_channels,
length: audio_buffer.length,
sample_rate: audio_buffer.sampleRate,
channels: channels
}, channels);
}
}
function createFromArrayBuffer(audioContext, audioData, options, callback) {
// The following function is a workaround for a Webkit bug where decodeAudioData
// invokes the errorCallback with null instead of a DOMException.
// See https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-decodeaudiodata
// and http://stackoverflow.com/q/10365335/103396
function errorCallback(error) {
if (!error) {
error = new DOMException('EncodingError');
}
callback(error);
// prevent double-calling the callback on errors:
callback = function callback() {};
}
var promise = audioContext.decodeAudioData(audioData, function (audio_buffer) {
createFromAudioBuffer(audio_buffer, options, callback);
}, errorCallback);
if (promise) {
promise.catch(errorCallback);
}
}
/**
* Creates and returns a WaveformData instance from the given waveform data.
*/
WaveformData.create = function create(data) {
return new WaveformData(data);
};
/**
* Creates a WaveformData instance from audio.
*/
WaveformData.createFromAudio = function (options, callback) {
var opts = getOptions(options);
if (options.audio_context && options.array_buffer) {
return createFromArrayBuffer(options.audio_context, options.array_buffer, opts, callback);
} else if (options.audio_buffer) {
return createFromAudioBuffer(options.audio_buffer, opts, callback);
} else {
throw new TypeError(
// eslint-disable-next-line
'WaveformData.createFromAudio(): Pass either an AudioContext and ArrayBuffer, or an AudioBuffer object');
}
};
function WaveformResampler(options) {
this._inputData = options.waveformData;
// Scale we want to reach
this._output_samples_per_pixel = options.scale;
this._scale = this._inputData.scale; // scale we are coming from
// The amount of data we want to resample i.e. final zoom want to resample
// all data but for intermediate zoom we want to resample subset
this._input_buffer_size = this._inputData.length;
var input_buffer_length_samples = this._input_buffer_size * this._inputData.scale;
var output_buffer_length_samples = Math.ceil(input_buffer_length_samples / this._output_samples_per_pixel);
var output_header_size = 24; // version 2
var bytes_per_sample = this._inputData.bits === 8 ? 1 : 2;
var total_size = output_header_size + output_buffer_length_samples * 2 * this._inputData.channels * bytes_per_sample;
this._output_data = new ArrayBuffer(total_size);
this.output_dataview = new DataView(this._output_data);
this.output_dataview.setInt32(0, 2, true); // Version
this.output_dataview.setUint32(4, this._inputData.bits === 8, true); // Is 8 bit?
this.output_dataview.setInt32(8, this._inputData.sample_rate, true);
this.output_dataview.setInt32(12, this._output_samples_per_pixel, true);
this.output_dataview.setInt32(16, output_buffer_length_samples, true);
this.output_dataview.setInt32(20, this._inputData.channels, true);
this._outputWaveformData = new WaveformData(this._output_data);
this._input_index = 0;
this._output_index = 0;
var channels = this._inputData.channels;
this._min = new Array(channels);
this._max = new Array(channels);
for (var channel = 0; channel < channels; ++channel) {
if (this._input_buffer_size > 0) {
this._min[channel] = this._inputData.channel(channel).min_sample(this._input_index);
this._max[channel] = this._inputData.channel(channel).max_sample(this._input_index);
} else {
this._min[channel] = 0;
this._max[channel] = 0;
}
}
this._min_value = this._inputData.bits === 8 ? -128 : -32768;
this._max_value = this._inputData.bits === 8 ? 127 : 32767;
this._where = 0;
this._prev_where = 0;
this._stop = 0;
this._last_input_index = 0;
}
WaveformResampler.prototype.sample_at_pixel = function (x) {
return Math.floor(x * this._output_samples_per_pixel);
};
WaveformResampler.prototype.next = function () {
var count = 0;
var total = 1000;
var channels = this._inputData.channels;
var channel;
while (this._input_index < this._input_buffer_size && count < total) {
while (Math.floor(this.sample_at_pixel(this._output_index) / this._scale) === this._input_index) {
if (this._output_index > 0) {
for (var i = 0; i < channels; ++i) {
channel = this._outputWaveformData.channel(i);
channel.set_min_sample(this._output_index - 1, this._min[i]);
channel.set_max_sample(this._output_index - 1, this._max[i]);
}
}
this._last_input_index = this._input_index;
this._output_index++;
this._where = this.sample_at_pixel(this._output_index);
this._prev_where = this.sample_at_pixel(this._output_index - 1);
if (this._where !== this._prev_where) {
for (var _i = 0; _i < channels; ++_i) {
this._min[_i] = this._max_value;
this._max[_i] = this._min_value;
}
}
}
this._where = this.sample_at_pixel(this._output_index);
this._stop = Math.floor(this._where / this._scale);
if (this._stop > this._input_buffer_size) {
this._stop = this._input_buffer_size;
}
while (this._input_index < this._stop) {
for (var _i2 = 0; _i2 < channels; ++_i2) {
channel = this._inputData.channel(_i2);
var value = channel.min_sample(this._input_index);
if (value < this._min[_i2]) {
this._min[_i2] = value;
}
value = channel.max_sample(this._input_index);
if (value > this._max[_i2]) {
this._max[_i2] = value;
}
}
this._input_index++;
}
count++;
}
if (this._input_index < this._input_buffer_size) {
// More to do
return false;
} else {
// Done
if (this._input_index !== this._last_input_index) {
for (var _i3 = 0; _i3 < channels; ++_i3) {
channel = this._outputWaveformData.channel(_i3);
channel.set_min_sample(this._output_index - 1, this._min[_i3]);
channel.set_max_sample(this._output_index - 1, this._max[_i3]);
}
}
return true;
}
};
WaveformResampler.prototype.getOutputData = function () {
return this._output_data;
};
WaveformData.prototype = {
_getResampleOptions: function _getResampleOptions(options) {
var opts = {};
opts.scale = options.scale;
opts.width = options.width;
if (!isNullOrUndefined(opts.width) && (typeof opts.width !== 'number' || opts.width <= 0)) {
throw new RangeError('WaveformData.resample(): width should be a positive integer value');
}
if (!isNullOrUndefined(opts.scale) && (typeof opts.scale !== 'number' || opts.scale <= 0)) {
throw new RangeError('WaveformData.resample(): scale should be a positive integer value');
}
if (!opts.scale && !opts.width) {
throw new Error('WaveformData.resample(): Missing scale or width option');
}
if (opts.width) {
// Calculate the target scale for the resampled waveform
opts.scale = Math.floor(this.duration * this.sample_rate / opts.width);
}
if (opts.scale < this.scale) {
throw new Error('WaveformData.resample(): Zoom level ' + opts.scale + ' too low, minimum: ' + this.scale);
}
opts.abortSignal = options.abortSignal;
return opts;
},
resample: function resample(options) {
options = this._getResampleOptions(options);
options.waveformData = this;
var resampler = new WaveformResampler(options);
while (!resampler.next()) {
// nothing
}
return new WaveformData(resampler.getOutputData());
},
/**
* Concatenates with one or more other waveforms, returning a new WaveformData object.
*/
concat: function concat() {
var self = this;
var otherWaveforms = Array.prototype.slice.call(arguments);
// Check that all the supplied waveforms are compatible
otherWaveforms.forEach(function (otherWaveform) {
if (self.channels !== otherWaveform.channels || self.sample_rate !== otherWaveform.sample_rate || self.bits !== otherWaveform.bits || self.scale !== otherWaveform.scale) {
throw new Error('WaveformData.concat(): Waveforms are incompatible');
}
});
var combinedBuffer = this._concatBuffers.apply(this, otherWaveforms);
return WaveformData.create(combinedBuffer);
},
/**
* Returns a new ArrayBuffer with the concatenated waveform.
* All waveforms must have identical metadata (version, channels, etc)
*/
_concatBuffers: function _concatBuffers() {
var otherWaveforms = Array.prototype.slice.call(arguments);
var headerSize = this._offset;
var totalSize = headerSize;
var totalDataLength = 0;
var bufferCollection = [this].concat(otherWaveforms).map(function (w) {
return w._data.buffer;
});
for (var i = 0; i < bufferCollection.length; i++) {
var buffer = bufferCollection[i];
var dataSize = new DataView(buffer).getInt32(16, true);
totalSize += buffer.byteLength - headerSize;
totalDataLength += dataSize;
}
var totalBuffer = new ArrayBuffer(totalSize);
var sourceHeader = new DataView(bufferCollection[0]);
var totalBufferView = new DataView(totalBuffer);
// Copy the header from the first chunk
for (var _i4 = 0; _i4 < headerSize; _i4++) {
totalBufferView.setUint8(_i4, sourceHeader.getUint8(_i4));
}
// Rewrite the data-length header item to reflect all of the samples concatenated together
totalBufferView.setInt32(16, totalDataLength, true);
var offset = 0;
var dataOfTotalBuffer = new Uint8Array(totalBuffer, headerSize);
for (var _i5 = 0; _i5 < bufferCollection.length; _i5++) {
var _buffer = bufferCollection[_i5];
dataOfTotalBuffer.set(new Uint8Array(_buffer, headerSize), offset);
offset += _buffer.byteLength - headerSize;
}
return totalBuffer;
},
slice: function slice(options) {
var startIndex = 0;
var endIndex = 0;
if (!isNullOrUndefined(options.startIndex) && !isNullOrUndefined(options.endIndex)) {
startIndex = options.startIndex;
endIndex = options.endIndex;
} else if (!isNullOrUndefined(options.startTime) && !isNullOrUndefined(options.endTime)) {
startIndex = this.at_time(options.startTime);
endIndex = this.at_time(options.endTime);
}
if (startIndex < 0) {
throw new RangeError('startIndex or startTime must not be negative');
}
if (endIndex < 0) {
throw new RangeError('endIndex or endTime must not be negative');
}
if (startIndex > this.length) {
startIndex = this.length;
}
if (endIndex > this.length) {
endIndex = this.length;
}
if (startIndex > endIndex) {
startIndex = endIndex;
}
var length = endIndex - startIndex;
var header_size = 24; // Version 2
var bytes_per_sample = this.bits === 8 ? 1 : 2;
var total_size = header_size + length * 2 * this.channels * bytes_per_sample;
var output_data = new ArrayBuffer(total_size);
var output_dataview = new DataView(output_data);
output_dataview.setInt32(0, 2, true); // Version
output_dataview.setUint32(4, this.bits === 8, true); // Is 8 bit?
output_dataview.setInt32(8, this.sample_rate, true);
output_dataview.setInt32(12, this.scale, true);
output_dataview.setInt32(16, length, true);
output_dataview.setInt32(20, this.channels, true);
for (var i = 0; i < length * this.channels * 2; i++) {
var sample = this._at(startIndex * this.channels * 2 + i);
if (this.bits === 8) {
output_dataview.setInt8(header_size + i, sample);
} else {
output_dataview.setInt16(header_size + i * 2, sample, true);
}
}
return new WaveformData(output_data);
},
/**
* Returns the data format version number.
*/
_version: function _version() {
return this._data.getInt32(0, true);
},
/**
* Returns the length of the waveform, in pixels.
*/
get length() {
return this._data.getUint32(16, true);
},
/**
* Returns the number of bits per sample, either 8 or 16.
*/
get bits() {
var bits = Boolean(this._data.getUint32(4, true));
return bits ? 8 : 16;
},
/**
* Returns the (approximate) duration of the audio file, in seconds.
*/
get duration() {
return this.length * this.scale / this.sample_rate;
},
/**
* Returns the number of pixels per second.
*/
get pixels_per_second() {
return this.sample_rate / this.scale;
},
/**
* Returns the amount of time represented by a single pixel, in seconds.
*/
get seconds_per_pixel() {
return this.scale / this.sample_rate;
},
/**
* Returns the number of waveform channels.
*/
get channels() {
if (this._version() === 2) {
return this._data.getInt32(20, true);
} else {
return 1;
}
},
/**
* Returns a waveform channel.
*/
channel: function channel(index) {
if (index >= 0 && index < this._channels.length) {
return this._channels[index];
} else {
throw new RangeError('Invalid channel: ' + index);
}
},
/**
* Returns the number of audio samples per second.
*/
get sample_rate() {
return this._data.getInt32(8, true);
},
/**
* Returns the number of audio samples per pixel.
*/
get scale() {
return this._data.getInt32(12, true);
},
/**
* Returns a waveform data value at a specific offset.
*/
_at: function at_sample(index) {
if (this.bits === 8) {
return this._data.getInt8(this._offset + index);
} else {
return this._data.getInt16(this._offset + index * 2, true);
}
},
/**
* Sets a waveform data value at a specific offset.
*/
_set_at: function set_at(index, sample) {
if (this.bits === 8) {
return this._data.setInt8(this._offset + index, sample);
} else {
return this._data.setInt16(this._offset + index * 2, sample, true);
}
},
/**
* Returns the waveform data index position for a given time.
*/
at_time: function at_time(time) {
return Math.floor(time * this.sample_rate / this.scale);
},
/**
* Returns the time in seconds for a given index.
*/
time: function time(index) {
return index * this.scale / this.sample_rate;
},
/**
* Returns an object containing the waveform data.
*/
toJSON: function toJSON() {
var waveform = {
version: 2,
channels: this.channels,
sample_rate: this.sample_rate,
samples_per_pixel: this.scale,
bits: this.bits,
length: this.length,
data: []
};
for (var i = 0; i < this.length; i++) {
for (var channel = 0; channel < this.channels; channel++) {
waveform.data.push(this.channel(channel).min_sample(i));
waveform.data.push(this.channel(channel).max_sample(i));
}
}
return waveform;
},
/**
* Returns the waveform data in binary format as an ArrayBuffer.
*/
toArrayBuffer: function toArrayBuffer() {
return this._data.buffer;
}
};
export { WaveformData as default };