waveform-playlist-nartj
Version:
Multiple track web audio editor and player with waveform preview
807 lines (716 loc) • 27.2 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _lodash = _interopRequireDefault(require("lodash.assign"));
var _lodash2 = _interopRequireDefault(require("lodash.forown"));
var _uuid = require("uuid");
var _lodash3 = _interopRequireDefault(require("lodash.clonedeep"));
var _h = _interopRequireDefault(require("virtual-dom/h"));
var _webaudioPeaks = _interopRequireDefault(require("webaudio-peaks"));
var _fadeMaker = require("fade-maker");
var _conversions = require("./utils/conversions");
var _states = _interopRequireDefault(require("./track/states"));
var _CanvasHook = _interopRequireDefault(require("./render/CanvasHook"));
var _FadeCanvasHook = _interopRequireDefault(require("./render/FadeCanvasHook"));
var _VolumeSliderHook = _interopRequireDefault(require("./render/VolumeSliderHook"));
var MAX_CANVAS_WIDTH = 1000;
var _default = /*#__PURE__*/function () {
function _default() {
(0, _classCallCheck2["default"])(this, _default);
this.name = 'Untitled';
this.id = 'i' + (0, _uuid.v4)(); // must start with a letter to be a valid css id
this.taggedName = this.name;
this.customClass = undefined;
this.waveOutlineColor = undefined;
this.gain = 1;
this.fades = {};
this.peakData = {
type: 'WebAudio',
mono: false
};
this.cueIn = 0;
this.cueOut = 0;
this.duration = 0;
this.startTime = 0;
this.endTime = 0;
this.stereoPan = 0;
this.src = undefined;
this.duplicationNumber = 0;
this.srcTrack = undefined;
this.scale = window.devicePixelRatio;
}
(0, _createClass2["default"])(_default, [{
key: "setSrc",
value: function setSrc(src) {
this.src = src;
}
}, {
key: "setSrcTrack",
value: function setSrcTrack(track) {
if (this.srcTrack === undefined) {
this.srcTrack = track;
}
}
}, {
key: "setTaggedName",
value: function setTaggedName() {
if (this.duplicationNumber !== undefined && this.duplicationNumber !== 0) {
this.taggedName = this.name + '#' + this.duplicationNumber;
} else {
this.taggedName = this.name;
}
}
}, {
key: "setEventEmitter",
value: function setEventEmitter(ee) {
this.ee = ee;
}
}, {
key: "setName",
value: function setName(name) {
this.name = name;
this.setTaggedName();
}
}, {
key: "setCustomClass",
value: function setCustomClass(className) {
this.customClass = className;
}
}, {
key: "setWaveOutlineColor",
value: function setWaveOutlineColor(color) {
this.waveOutlineColor = color;
}
}, {
key: "setCues",
value: function setCues(cueIn, cueOut) {
if (cueOut < cueIn) {
throw new Error('cue out cannot be less than cue in');
}
this.cueIn = cueIn;
this.cueOut = cueOut;
this.duration = this.cueOut - this.cueIn;
this.endTime = this.startTime + this.duration;
}
/*
* start, end in seconds relative to the entire playlist.
*/
}, {
key: "trim",
value: function () {
var _trim = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(start, end) {
var trackStart, trackEnd, offset, self, middleTrack, endTrack, trackOffset, cueIn, cueOut, peaks, undo;
return _regenerator["default"].wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
trackStart = this.getStartTime();
trackEnd = this.getEndTime();
offset = this.cueIn - trackStart;
self = this;
middleTrack = null;
endTrack = null;
trackOffset = 0;
if (!(trackStart <= start && trackEnd >= start || trackStart <= end && trackEnd >= end)) {
_context.next = 24;
break;
}
cueIn = trackStart;
cueOut = start < trackStart ? end : start;
this.setCues(cueIn + offset, cueOut + offset);
if (start > trackStart) {
this.setStartTime(trackStart);
}
if (!(start > trackStart)) {
_context.next = 18;
break;
}
cueIn = start < trackStart ? trackStart : start;
cueOut = end > trackEnd ? trackEnd : end;
_context.next = 17;
return this.duplicateTrack(this, start, cueIn + offset, cueOut + offset, ++trackOffset);
case 17:
middleTrack = _context.sent;
case 18:
if (!(end < trackEnd)) {
_context.next = 24;
break;
}
cueIn = end;
cueOut = trackEnd;
_context.next = 23;
return this.duplicateTrack(this, end, cueIn + offset, cueOut + offset, ++trackOffset);
case 23:
endTrack = _context.sent;
case 24:
peaks = (0, _lodash3["default"])(this.peaks);
undo = function undo() {
// todo replace with playlist.load and remove peaks
self.setCues(trackStart, trackEnd);
self.setPeaks(peaks);
playlist.removeTrack(middleTrack);
playlist.removeTrack(endTrack);
playlist.setTimeSelection(0, 0);
playlist.adjustDuration();
playlist.draw(playlist.render());
};
return _context.abrupt("return", undo);
case 27:
case "end":
return _context.stop();
}
}
}, _callee, this);
}));
function trim(_x, _x2) {
return _trim.apply(this, arguments);
}
return trim;
}()
}, {
key: "duplicateTrack",
value: function () {
var _duplicateTrack = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2(track, start, cueIn, cueOut, trackOffset) {
return _regenerator["default"].wrap(function _callee2$(_context2) {
while (1) {
switch (_context2.prev = _context2.next) {
case 0:
playlist.setActiveTrack(track); // it duplicates the current active track
_context2.next = 3;
return playlist.load([{
track: track,
src: track.src,
name: track.name,
start: start,
states: track.states,
cueIn: cueIn,
cueOut: cueOut,
gain: track.gain,
muted: track.muted,
soloed: track.soloed,
selection: track.selection,
peaks: track.peaks,
customClass: track.customClass,
waveOutlineColor: track.waveOutlineColor,
stereoPan: track.stereoPan,
duplicationNumber: track.duplicationNumber,
trackOffset: trackOffset
}]);
case 3:
return _context2.abrupt("return", _context2.sent[0]);
case 4:
case "end":
return _context2.stop();
}
}
}, _callee2);
}));
function duplicateTrack(_x3, _x4, _x5, _x6, _x7) {
return _duplicateTrack.apply(this, arguments);
}
return duplicateTrack;
}()
}, {
key: "setStartTime",
value: function setStartTime(start) {
this.startTime = start;
this.endTime = start + this.duration;
}
}, {
key: "setPlayout",
value: function setPlayout(playout) {
this.playout = playout;
}
}, {
key: "setOfflinePlayout",
value: function setOfflinePlayout(playout) {
this.offlinePlayout = playout;
}
}, {
key: "setEnabledStates",
value: function setEnabledStates() {
var enabledStates = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var defaultStatesEnabled = {
cursor: true,
fadein: true,
fadeout: true,
select: true,
shift: true
};
this.enabledStates = (0, _lodash["default"])({}, defaultStatesEnabled, enabledStates);
}
}, {
key: "setFadeIn",
value: function setFadeIn(duration) {
var shape = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'logarithmic';
if (duration > this.duration) {
throw new Error('Invalid Fade In');
}
var fade = {
shape: shape,
start: 0,
end: duration
};
if (this.fadeIn) {
this.removeFade(this.fadeIn);
this.fadeIn = undefined;
}
this.fadeIn = this.saveFade(_fadeMaker.FADEIN, fade.shape, fade.start, fade.end);
}
}, {
key: "setFadeOut",
value: function setFadeOut(duration) {
var shape = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'logarithmic';
if (duration > this.duration) {
throw new Error('Invalid Fade Out');
}
var fade = {
shape: shape,
start: this.duration - duration,
end: this.duration
};
if (this.fadeOut) {
this.removeFade(this.fadeOut);
this.fadeOut = undefined;
}
this.fadeOut = this.saveFade(_fadeMaker.FADEOUT, fade.shape, fade.start, fade.end);
}
}, {
key: "saveFade",
value: function saveFade(type, shape, start, end) {
var id = (0, _uuid.v4)();
this.fades[id] = {
type: type,
shape: shape,
start: start,
end: end
};
return id;
}
}, {
key: "removeFade",
value: function removeFade(id) {
delete this.fades[id];
}
}, {
key: "setBuffer",
value: function setBuffer(buffer) {
this.buffer = buffer;
}
}, {
key: "setPeakData",
value: function setPeakData(data) {
this.peakData = data;
}
}, {
key: "calculatePeaks",
value: function calculatePeaks(samplesPerPixel, sampleRate) {
var cueIn = (0, _conversions.secondsToSamples)(this.cueIn, sampleRate);
var cueOut = (0, _conversions.secondsToSamples)(this.cueOut, sampleRate);
this.setPeaks((0, _webaudioPeaks["default"])(this.buffer, samplesPerPixel, this.peakData.mono, cueIn, cueOut));
}
}, {
key: "setPeaks",
value: function setPeaks(peaks) {
this.peaks = peaks;
}
}, {
key: "setState",
value: function setState(state) {
this.state = state;
if (this.state && this.enabledStates[this.state]) {
var StateClass = _states["default"][this.state];
this.stateObj = new StateClass(this);
} else {
this.stateObj = undefined;
}
}
}, {
key: "getStartTime",
value: function getStartTime() {
return this.startTime;
}
}, {
key: "getEndTime",
value: function getEndTime() {
return this.endTime;
}
}, {
key: "getDuration",
value: function getDuration() {
return this.duration;
}
}, {
key: "isPlaying",
value: function isPlaying() {
return this.playout.isPlaying();
}
}, {
key: "setShouldPlay",
value: function setShouldPlay(bool) {
this.playout.setShouldPlay(bool);
}
}, {
key: "setGainLevel",
value: function setGainLevel(level) {
this.gain = level;
this.playout.setVolumeGainLevel(level);
}
}, {
key: "setMasterGainLevel",
value: function setMasterGainLevel(level) {
this.playout.setMasterGainLevel(level);
}
}, {
key: "setStereoPanValue",
value: function setStereoPanValue(value) {
this.stereoPan = value;
this.playout.setStereoPanValue(value);
}
/*
startTime, endTime in seconds (float).
segment is for a highlighted section in the UI.
returns a Promise that will resolve when the AudioBufferSource
is either stopped or plays out naturally.
*/
}, {
key: "schedulePlay",
value: function schedulePlay(now, startTime, endTime, config) {
var start;
var duration;
var when = now;
var segment = endTime ? endTime - startTime : undefined;
var defaultOptions = {
shouldPlay: true,
masterGain: 1,
isOffline: false
};
var options = (0, _lodash["default"])({}, defaultOptions, config);
var playoutSystem = options.isOffline ? this.offlinePlayout : this.playout; // 1) track has no content to play.
// 2) track does not play in this selection.
if (this.endTime <= startTime || segment && startTime + segment < this.startTime) {
// return a resolved promise since this track is technically "stopped".
return Promise.resolve();
} // track should have something to play if it gets here.
// the track starts in the future or on the cursor position
if (this.startTime >= startTime) {
start = 0; // schedule additional delay for this audio node.
when += this.startTime - startTime;
if (endTime) {
segment -= this.startTime - startTime;
duration = Math.min(segment, this.duration);
} else {
duration = this.duration;
}
} else {
start = startTime - this.startTime;
if (endTime) {
duration = Math.min(segment, this.duration - start);
} else {
duration = this.duration - start;
}
}
start += this.cueIn;
var relPos = startTime - this.startTime;
var sourcePromise = playoutSystem.setUpSource(); // param relPos: cursor position in seconds relative to this track.
// can be negative if the cursor is placed before the start of this track etc.
(0, _lodash2["default"])(this.fades, function (fade) {
var fadeStart;
var fadeDuration; // only apply fade if it's ahead of the cursor.
if (relPos < fade.end) {
if (relPos <= fade.start) {
fadeStart = now + (fade.start - relPos);
fadeDuration = fade.end - fade.start;
} else if (relPos > fade.start && relPos < fade.end) {
fadeStart = now - (relPos - fade.start);
fadeDuration = fade.end - fade.start;
}
switch (fade.type) {
case _fadeMaker.FADEIN:
{
playoutSystem.applyFadeIn(fadeStart, fadeDuration, fade.shape);
break;
}
case _fadeMaker.FADEOUT:
{
playoutSystem.applyFadeOut(fadeStart, fadeDuration, fade.shape);
break;
}
default:
{
throw new Error('Invalid fade type saved on track.');
}
}
}
});
playoutSystem.setVolumeGainLevel(this.gain);
playoutSystem.setShouldPlay(options.shouldPlay);
playoutSystem.setMasterGainLevel(options.masterGain);
playoutSystem.setStereoPanValue(this.stereoPan);
playoutSystem.play(when, start, duration);
return sourcePromise;
}
}, {
key: "scheduleStop",
value: function scheduleStop() {
var when = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
this.playout.stop(when);
}
}, {
key: "renderOverlay",
value: function renderOverlay(data) {
var _this = this;
var channelPixels = (0, _conversions.secondsToPixels)(data.playlistLength, data.resolution, data.sampleRate);
var config = {
attributes: {
style: "position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: ".concat(channelPixels, "px; z-index: 9;")
}
};
var overlayClass = '';
if (this.stateObj) {
this.stateObj.setup(data.resolution, data.sampleRate);
var StateClass = _states["default"][this.state];
var events = StateClass.getEvents();
events.forEach(function (event) {
config["on".concat(event)] = _this.stateObj[event].bind(_this.stateObj);
});
overlayClass = StateClass.getClass();
} // use this overlay for track event cursor position calculations.
return (0, _h["default"])("div.playlist-overlay".concat(overlayClass), config);
}
}, {
key: "renderControls",
value: function renderControls(data) {
var _this2 = this;
var muteClass = data.muted ? '.active' : '';
var soloClass = data.soloed ? '.active' : '';
var numChan = this.peaks.data.length;
return (0, _h["default"])('div.controls', {
attributes: {
style: "height: ".concat(numChan * data.height, "px; width: ").concat(data.controls.width, "px; position: absolute; left: 0; z-index: 10;"),
id: this.id + 'Controls'
}
}, [(0, _h["default"])('header', [this.taggedName]), (0, _h["default"])('div.btn-group', [(0, _h["default"])("span.btn.btn-default.i.fa.fa-volume-off", {
onclick: function onclick() {
_this2.ee.emit('mute', _this2);
}
}), (0, _h["default"])("span.btn.btn-default.i.fa.fa-crosshairs", {
onclick: function onclick() {
_this2.ee.emit('solo', _this2);
}
}), (0, _h["default"])("span.btn.btn-default.i.fa.fa-chevron-up", {
onclick: function onclick() {
_this2.ee.emit('moveUp', _this2);
}
}), (0, _h["default"])("span.btn.btn-default.i.fa.fa-chevron-down", {
onclick: function onclick() {
_this2.ee.emit('moveDown', _this2);
}
}), (0, _h["default"])("span.btn.btn-default.i.fa.fa-times", {
onclick: function onclick() {
_this2.ee.emit('delete', _this2);
}
}), (0, _h["default"])("span.btn.btn-default.i.fa.fa-files-o", {
onclick: function onclick() {
_this2.ee.emit('duplicate', _this2);
}
}), (0, _h["default"])("span.btn.btn-default.i.fa.fa-compress}", {
onclick: function onclick() {
document.querySelector("#" + _this2.id + 'Controls').style.display = "none";
document.querySelector("#" + _this2.id + "Card").style.display = "none";
document.querySelector("#" + _this2.id + "Header").style.display = "contents";
}
})]), (0, _h["default"])('label', [(0, _h["default"])('input.volume-slider', {
attributes: {
type: 'range',
min: 0,
max: 100,
value: 100
},
hook: new _VolumeSliderHook["default"](this.gain),
oninput: function oninput(e) {
_this2.ee.emit('volumechange', e.target.value, _this2);
}
})])]);
}
}, {
key: "render",
value: function render(data) {
var _this3 = this;
var width = this.peaks.length;
var playbackX = (0, _conversions.secondsToPixels)(data.playbackSeconds, data.resolution, data.sampleRate);
var startX = (0, _conversions.secondsToPixels)(this.startTime, data.resolution, data.sampleRate);
var endX = (0, _conversions.secondsToPixels)(this.endTime, data.resolution, data.sampleRate);
var progressWidth = 0;
var numChan = this.peaks.data.length;
var oldScale = this.scale;
this.scale = window.devicePixelRatio;
if (playbackX > 0 && playbackX > startX) {
if (playbackX < endX) {
progressWidth = playbackX - startX;
} else {
progressWidth = width;
}
}
var waveformChildren = [(0, _h["default"])('div.cursor', {
attributes: {
style: "position: absolute; width: 1px; margin: 0; padding: 0; top: 0; left: ".concat(playbackX, "px; bottom: 0; z-index: 5;")
}
})];
var channels = Object.keys(this.peaks.data).map(function (channelNum) {
var channelChildren = [(0, _h["default"])('div.channel-progress', {
attributes: {
style: "position: absolute; width: ".concat(progressWidth, "px; height: ").concat(data.height, "px; z-index: 2;")
}
})];
var offset = 0;
var totalWidth = width;
var peaks = _this3.peaks.data[channelNum];
while (totalWidth > 0) {
var currentWidth = Math.min(totalWidth, MAX_CANVAS_WIDTH);
var canvasColor = _this3.waveOutlineColor ? _this3.waveOutlineColor : data.colors.waveOutlineColor;
channelChildren.push((0, _h["default"])('canvas', {
attributes: {
width: currentWidth * _this3.scale,
height: data.height * _this3.scale,
style: "float: left; position: relative; margin: 0; padding: 0; z-index: 3; width: ".concat(currentWidth, "px; height: ").concat(data.height, "px;")
},
hook: new _CanvasHook["default"](peaks, offset, _this3.peaks.bits, canvasColor, _this3.scale, _this3.scale !== oldScale)
}));
totalWidth -= currentWidth;
offset += MAX_CANVAS_WIDTH;
} // if there are fades, display them.
if (_this3.fadeIn) {
var fadeIn = _this3.fades[_this3.fadeIn];
var fadeWidth = (0, _conversions.secondsToPixels)(fadeIn.end - fadeIn.start, data.resolution, data.sampleRate);
channelChildren.push((0, _h["default"])('div.wp-fade.wp-fadein', {
attributes: {
style: "position: absolute; height: ".concat(data.height, "px; width: ").concat(fadeWidth, "px; top: 0; left: 0; z-index: 4;")
}
}, [(0, _h["default"])('canvas', {
attributes: {
width: fadeWidth,
height: data.height
},
hook: new _FadeCanvasHook["default"](fadeIn.type, fadeIn.shape, fadeIn.end - fadeIn.start, data.resolution)
})]));
}
if (_this3.fadeOut) {
var fadeOut = _this3.fades[_this3.fadeOut];
var _fadeWidth = (0, _conversions.secondsToPixels)(fadeOut.end - fadeOut.start, data.resolution, data.sampleRate);
channelChildren.push((0, _h["default"])('div.wp-fade.wp-fadeout', {
attributes: {
style: "position: absolute; height: ".concat(data.height, "px; width: ").concat(_fadeWidth, "px; top: 0; right: 0; z-index: 4;")
}
}, [(0, _h["default"])('canvas', {
attributes: {
width: _fadeWidth,
height: data.height
},
hook: new _FadeCanvasHook["default"](fadeOut.type, fadeOut.shape, fadeOut.end - fadeOut.start, data.resolution)
})]));
}
return (0, _h["default"])("div.channel.channel-".concat(channelNum), {
attributes: {
style: "height: ".concat(data.height, "px; width: ").concat(width, "px; top: ").concat(channelNum * data.height, "px; left: ").concat(startX, "px; position: absolute; margin: 0; padding: 0; z-index: 1;")
}
}, channelChildren);
});
waveformChildren.push(channels);
waveformChildren.push(this.renderOverlay(data)); // draw cursor selection on active track.
if (data.isActive === true) {
var cStartX = (0, _conversions.secondsToPixels)(data.timeSelection.start, data.resolution, data.sampleRate);
var cEndX = (0, _conversions.secondsToPixels)(data.timeSelection.end, data.resolution, data.sampleRate);
var cWidth = cEndX - cStartX + 1;
var cClassName = cWidth > 1 ? '.segment' : '.point';
waveformChildren.push((0, _h["default"])("div.selection".concat(cClassName), {
attributes: {
style: "position: absolute; width: ".concat(cWidth, "px; bottom: 0; top: 0; left: ").concat(cStartX, "px; z-index: 4;")
}
}));
}
var waveform = (0, _h["default"])('div.waveform', {
attributes: {
style: "height: ".concat(numChan * data.height, "px; position: relative;")
}
}, waveformChildren);
var channelChildren = [];
var channelMargin = 0;
if (data.controls.show) {
channelChildren.push(this.renderControls(data));
channelMargin = data.controls.width;
}
channelChildren.push(waveform);
var audibleClass = data.shouldPlay ? '' : '.silent';
var customClass = this.customClass === undefined ? '' : ".".concat(this.customClass);
var full = (0, _h["default"])("div.channel-wrapper".concat(audibleClass).concat(customClass), {
attributes: {
style: "margin-left: ".concat(channelMargin, "px; height: ").concat(data.height * numChan, "px;"),
id: this.id + 'Card'
}
}, channelChildren);
var collapseButton = (0, _h["default"])("span.btn.btn-default.i.fa.fa-expand}", {
onclick: function onclick() {
document.querySelector("#" + _this3.id + 'Controls').style.display = "block";
document.querySelector("#" + _this3.id + "Card").style.display = "block";
document.querySelector("#" + _this3.id + "Header").style.display = "none";
}
});
var collapse = (0, _h["default"])("div.controls.collapsed", {
attributes: {
style: "display: none; text-align: left;",
id: this.id + 'Header'
}
}, [(0, _h["default"])('header', [this.taggedName, collapseButton])]);
return (0, _h["default"])('', [collapse, full]);
}
}, {
key: "getTrackDetails",
value: function getTrackDetails() {
var info = {
src: this.src,
start: this.startTime,
end: this.endTime,
name: this.name,
customClass: this.customClass,
cueIn: this.cueIn,
cueOut: this.cueOut
};
if (this.fadeIn) {
var fadeIn = this.fades[this.fadeIn];
info.fadeIn = {
shape: fadeIn.shape,
duration: fadeIn.end - fadeIn.start
};
}
if (this.fadeOut) {
var fadeOut = this.fades[this.fadeOut];
info.fadeOut = {
shape: fadeOut.shape,
duration: fadeOut.end - fadeOut.start
};
}
return info;
}
}, {
key: "setDuplicationNumber",
value: function setDuplicationNumber(duplicationNumber) {
if (duplicationNumber === undefined) {
duplicationNumber = 0;
}
if (this.srcTrack !== undefined) {
this.srcTrack.setDuplicationNumber(duplicationNumber);
}
this.duplicationNumber = duplicationNumber;
}
}]);
return _default;
}();
exports["default"] = _default;