waveform-playlist
Version:
Multiple track web audio editor and player with waveform preview
225 lines (203 loc) • 7.64 kB
JavaScript
import h from "virtual-dom/h";
import inputAeneas from "./input/aeneas";
import outputAeneas from "./output/aeneas";
import { secondsToPixels } from "../utils/conversions";
import DragInteraction from "../interaction/DragInteraction";
import ScrollTopHook from "./render/ScrollTopHook";
import timeformat from "../utils/timeformat";
class AnnotationList {
constructor(playlist, annotations, controls = [], editable = false, linkEndpoints = false, isContinuousPlay = false, marginLeft = 0) {
this.playlist = playlist;
this.marginLeft = marginLeft;
this.resizeHandlers = [];
this.editable = editable;
this.annotations = annotations.map(a => // TODO support different formats later on.
inputAeneas(a));
this.setupInteractions();
this.controls = controls;
this.setupEE(playlist.ee); // TODO actually make a real plugin system that's not terrible.
this.playlist.isContinuousPlay = isContinuousPlay;
this.playlist.linkEndpoints = linkEndpoints;
this.length = this.annotations.length;
}
setupInteractions() {
this.annotations.forEach((a, i) => {
const leftShift = new DragInteraction(this.playlist, {
direction: "left",
index: i
});
const rightShift = new DragInteraction(this.playlist, {
direction: "right",
index: i
});
this.resizeHandlers.push(leftShift);
this.resizeHandlers.push(rightShift);
});
}
setupEE(ee) {
ee.on("dragged", (deltaTime, data) => {
const annotationIndex = data.index;
const annotations = this.annotations;
const note = annotations[annotationIndex]; // resizing to the left
if (data.direction === "left") {
const originalVal = note.start;
note.start += deltaTime;
if (note.start < 0) {
note.start = 0;
}
if (annotationIndex && annotations[annotationIndex - 1].end > note.start) {
annotations[annotationIndex - 1].end = note.start;
}
if (this.playlist.linkEndpoints && annotationIndex && annotations[annotationIndex - 1].end === originalVal) {
annotations[annotationIndex - 1].end = note.start;
}
} else {
// resizing to the right
const originalVal = note.end;
note.end += deltaTime;
if (note.end > this.playlist.duration) {
note.end = this.playlist.duration;
}
if (annotationIndex < annotations.length - 1 && annotations[annotationIndex + 1].start < note.end) {
annotations[annotationIndex + 1].start = note.end;
}
if (this.playlist.linkEndpoints && annotationIndex < annotations.length - 1 && annotations[annotationIndex + 1].start === originalVal) {
annotations[annotationIndex + 1].start = note.end;
}
}
this.playlist.drawRequest();
});
ee.on("continuousplay", val => {
this.playlist.isContinuousPlay = val;
});
ee.on("linkendpoints", val => {
this.playlist.linkEndpoints = val;
});
ee.on("annotationsrequest", () => {
this.export();
});
return ee;
}
export() {
const output = this.annotations.map(a => outputAeneas(a));
const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(output))}`;
const a = document.createElement("a");
document.body.appendChild(a);
a.href = dataStr;
a.download = "annotations.json";
a.click();
document.body.removeChild(a);
}
renderResizeLeft(i) {
const events = DragInteraction.getEvents();
const config = {
attributes: {
style: "position: absolute; height: 30px; width: 10px; top: 0; left: -2px",
draggable: true
}
};
const handler = this.resizeHandlers[i * 2];
events.forEach(event => {
config[`on${event}`] = handler[event].bind(handler);
});
return h("div.resize-handle.resize-w", config);
}
renderResizeRight(i) {
const events = DragInteraction.getEvents();
const config = {
attributes: {
style: "position: absolute; height: 30px; width: 10px; top: 0; right: -2px",
draggable: true
}
};
const handler = this.resizeHandlers[i * 2 + 1];
events.forEach(event => {
config[`on${event}`] = handler[event].bind(handler);
});
return h("div.resize-handle.resize-e", config);
}
renderControls(note, i) {
// seems to be a bug with references, or I'm missing something.
const that = this;
return this.controls.map(ctrl => h(`i.${ctrl.class}`, {
attributes: {
title: ctrl.title
},
onclick: () => {
ctrl.action(note, i, that.annotations, {
linkEndpoints: that.playlist.linkEndpoints
});
this.setupInteractions();
that.playlist.drawRequest();
}
}));
}
render() {
const boxes = h("div.annotations-boxes", {
attributes: {
style: `height: 30px; position: relative; margin-left: ${this.marginLeft}px;`
}
}, this.annotations.map((note, i) => {
const samplesPerPixel = this.playlist.samplesPerPixel;
const sampleRate = this.playlist.sampleRate;
const pixPerSec = sampleRate / samplesPerPixel;
const pixOffset = secondsToPixels(this.playlist.scrollLeft, samplesPerPixel, sampleRate);
const left = Math.floor(note.start * pixPerSec - pixOffset);
const width = Math.ceil(note.end * pixPerSec - note.start * pixPerSec);
return h("div.annotation-box", {
attributes: {
style: `position: absolute; height: 30px; width: ${width}px; left: ${left}px`,
"data-id": note.id
}
}, [this.renderResizeLeft(i), h("span.id", {
onclick: () => {
const start = this.annotations[i].start;
const end = this.annotations[i].end;
if (this.playlist.isContinuousPlay) {
this.playlist.seek(start, start);
this.playlist.ee.emit("play", start);
} else {
this.playlist.seek(start, end);
this.playlist.ee.emit("play", start, end);
}
}
}, [note.id]), this.renderResizeRight(i)]);
}));
const boxesWrapper = h("div.annotations-boxes-wrapper", {
attributes: {
style: "overflow: hidden;"
}
}, [boxes]);
const text = h("div.annotations-text", {
hook: new ScrollTopHook()
}, this.annotations.map((note, i) => {
const format = timeformat(this.playlist.durationFormat);
const start = format(note.start);
const end = format(note.end);
let segmentClass = "";
if (this.playlist.isPlaying() && this.playlist.playbackSeconds >= note.start && this.playlist.playbackSeconds <= note.end) {
segmentClass = ".current";
}
const editableConfig = {
attributes: {
contenteditable: true
},
oninput: e => {
// needed currently for references
// eslint-disable-next-line no-param-reassign
note.lines = [e.target.innerText];
},
onkeypress: e => {
if (e.which === 13 || e.keyCode === 13) {
e.target.blur();
e.preventDefault();
}
}
};
const linesConfig = this.editable ? editableConfig : {};
return h(`div.annotation${segmentClass}`, [h("span.annotation-id", [note.id]), h("span.annotation-start", [start]), h("span.annotation-end", [end]), h("span.annotation-lines", linesConfig, [note.lines]), h("span.annotation-actions", this.renderControls(note, i))]);
}));
return h("div.annotations", [boxesWrapper, text]);
}
}
export default AnnotationList;