1 | import _assign from "lodash.assign";
|
2 | import _forOwn from "lodash.forown";
|
3 | import { v4 as uuidv4 } from "uuid";
|
4 | import h from "virtual-dom/h";
|
5 | import extractPeaks from "webaudio-peaks";
|
6 | import { FADEIN, FADEOUT } from "fade-maker";
|
7 | import { secondsToPixels, secondsToSamples } from "./utils/conversions";
|
8 | import stateClasses from "./track/states";
|
9 | import CanvasHook from "./render/CanvasHook";
|
10 | import FadeCanvasHook from "./render/FadeCanvasHook";
|
11 | import VolumeSliderHook from "./render/VolumeSliderHook";
|
12 | import StereoPanSliderHook from "./render/StereoPanSliderHook";
|
13 | const MAX_CANVAS_WIDTH = 1000;
|
14 | export default class {
|
15 | constructor() {
|
16 | this.name = "Untitled";
|
17 | this.customClass = undefined;
|
18 | this.waveOutlineColor = undefined;
|
19 | this.gain = 1;
|
20 | this.fades = {};
|
21 | this.peakData = {
|
22 | type: "WebAudio",
|
23 | mono: false
|
24 | };
|
25 | this.cueIn = 0;
|
26 | this.cueOut = 0;
|
27 | this.duration = 0;
|
28 | this.startTime = 0;
|
29 | this.endTime = 0;
|
30 | this.stereoPan = 0;
|
31 | }
|
32 |
|
33 | setEventEmitter(ee) {
|
34 | this.ee = ee;
|
35 | }
|
36 |
|
37 | setName(name) {
|
38 | this.name = name;
|
39 | }
|
40 |
|
41 | setCustomClass(className) {
|
42 | this.customClass = className;
|
43 | }
|
44 |
|
45 | setWaveOutlineColor(color) {
|
46 | this.waveOutlineColor = color;
|
47 | }
|
48 |
|
49 | setCues(cueIn, cueOut) {
|
50 | if (cueOut < cueIn) {
|
51 | throw new Error("cue out cannot be less than cue in");
|
52 | }
|
53 |
|
54 | this.cueIn = cueIn;
|
55 | this.cueOut = cueOut;
|
56 | this.duration = this.cueOut - this.cueIn;
|
57 | this.endTime = this.startTime + this.duration;
|
58 | }
|
59 | |
60 |
|
61 |
|
62 |
|
63 |
|
64 | trim(start, end) {
|
65 | const trackStart = this.getStartTime();
|
66 | const trackEnd = this.getEndTime();
|
67 | const offset = this.cueIn - trackStart;
|
68 |
|
69 | if (trackStart <= start && trackEnd >= start || trackStart <= end && trackEnd >= end) {
|
70 | const cueIn = start < trackStart ? trackStart : start;
|
71 | const cueOut = end > trackEnd ? trackEnd : end;
|
72 | this.setCues(cueIn + offset, cueOut + offset);
|
73 |
|
74 | if (start > trackStart) {
|
75 | this.setStartTime(start);
|
76 | }
|
77 | }
|
78 | }
|
79 |
|
80 | setStartTime(start) {
|
81 | this.startTime = start;
|
82 | this.endTime = start + this.duration;
|
83 | }
|
84 |
|
85 | setPlayout(playout) {
|
86 | this.playout = playout;
|
87 | }
|
88 |
|
89 | setOfflinePlayout(playout) {
|
90 | this.offlinePlayout = playout;
|
91 | }
|
92 |
|
93 | setEnabledStates(enabledStates = {}) {
|
94 | const defaultStatesEnabled = {
|
95 | cursor: true,
|
96 | fadein: true,
|
97 | fadeout: true,
|
98 | select: true,
|
99 | shift: true
|
100 | };
|
101 | this.enabledStates = _assign({}, defaultStatesEnabled, enabledStates);
|
102 | }
|
103 |
|
104 | setFadeIn(duration, shape = "logarithmic") {
|
105 | if (duration > this.duration) {
|
106 | throw new Error("Invalid Fade In");
|
107 | }
|
108 |
|
109 | const fade = {
|
110 | shape,
|
111 | start: 0,
|
112 | end: duration
|
113 | };
|
114 |
|
115 | if (this.fadeIn) {
|
116 | this.removeFade(this.fadeIn);
|
117 | this.fadeIn = undefined;
|
118 | }
|
119 |
|
120 | this.fadeIn = this.saveFade(FADEIN, fade.shape, fade.start, fade.end);
|
121 | }
|
122 |
|
123 | setFadeOut(duration, shape = "logarithmic") {
|
124 | if (duration > this.duration) {
|
125 | throw new Error("Invalid Fade Out");
|
126 | }
|
127 |
|
128 | const fade = {
|
129 | shape,
|
130 | start: this.duration - duration,
|
131 | end: this.duration
|
132 | };
|
133 |
|
134 | if (this.fadeOut) {
|
135 | this.removeFade(this.fadeOut);
|
136 | this.fadeOut = undefined;
|
137 | }
|
138 |
|
139 | this.fadeOut = this.saveFade(FADEOUT, fade.shape, fade.start, fade.end);
|
140 | }
|
141 |
|
142 | saveFade(type, shape, start, end) {
|
143 | const id = uuidv4();
|
144 | this.fades[id] = {
|
145 | type,
|
146 | shape,
|
147 | start,
|
148 | end
|
149 | };
|
150 | return id;
|
151 | }
|
152 |
|
153 | removeFade(id) {
|
154 | delete this.fades[id];
|
155 | }
|
156 |
|
157 | setBuffer(buffer) {
|
158 | this.buffer = buffer;
|
159 | }
|
160 |
|
161 | setPeakData(data) {
|
162 | this.peakData = data;
|
163 | }
|
164 |
|
165 | calculatePeaks(samplesPerPixel, sampleRate) {
|
166 | const cueIn = secondsToSamples(this.cueIn, sampleRate);
|
167 | const cueOut = secondsToSamples(this.cueOut, sampleRate);
|
168 | this.setPeaks(extractPeaks(this.buffer, samplesPerPixel, this.peakData.mono, cueIn, cueOut));
|
169 | }
|
170 |
|
171 | setPeaks(peaks) {
|
172 | this.peaks = peaks;
|
173 | }
|
174 |
|
175 | setState(state) {
|
176 | this.state = state;
|
177 |
|
178 | if (this.state && this.enabledStates[this.state]) {
|
179 | const StateClass = stateClasses[this.state];
|
180 | this.stateObj = new StateClass(this);
|
181 | } else {
|
182 | this.stateObj = undefined;
|
183 | }
|
184 | }
|
185 |
|
186 | getStartTime() {
|
187 | return this.startTime;
|
188 | }
|
189 |
|
190 | getEndTime() {
|
191 | return this.endTime;
|
192 | }
|
193 |
|
194 | getDuration() {
|
195 | return this.duration;
|
196 | }
|
197 |
|
198 | isPlaying() {
|
199 | return this.playout.isPlaying();
|
200 | }
|
201 |
|
202 | setShouldPlay(bool) {
|
203 | this.playout.setShouldPlay(bool);
|
204 | }
|
205 |
|
206 | setGainLevel(level) {
|
207 | this.gain = level;
|
208 | this.playout.setVolumeGainLevel(level);
|
209 | }
|
210 |
|
211 | setMasterGainLevel(level) {
|
212 | this.playout.setMasterGainLevel(level);
|
213 | }
|
214 |
|
215 | setStereoPanValue(value) {
|
216 | this.stereoPan = value;
|
217 | this.playout.setStereoPanValue(value);
|
218 | }
|
219 | |
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 | schedulePlay(now, startTime, endTime, config) {
|
228 | let start;
|
229 | let duration;
|
230 | let when = now;
|
231 | let segment = endTime ? endTime - startTime : undefined;
|
232 | const defaultOptions = {
|
233 | shouldPlay: true,
|
234 | masterGain: 1,
|
235 | isOffline: false
|
236 | };
|
237 |
|
238 | const options = _assign({}, defaultOptions, config);
|
239 |
|
240 | const playoutSystem = options.isOffline ? this.offlinePlayout : this.playout;
|
241 |
|
242 |
|
243 | if (this.endTime <= startTime || segment && startTime + segment < this.startTime) {
|
244 |
|
245 | return Promise.resolve();
|
246 | }
|
247 |
|
248 |
|
249 |
|
250 | if (this.startTime >= startTime) {
|
251 | start = 0;
|
252 |
|
253 | when += this.startTime - startTime;
|
254 |
|
255 | if (endTime) {
|
256 | segment -= this.startTime - startTime;
|
257 | duration = Math.min(segment, this.duration);
|
258 | } else {
|
259 | duration = this.duration;
|
260 | }
|
261 | } else {
|
262 | start = startTime - this.startTime;
|
263 |
|
264 | if (endTime) {
|
265 | duration = Math.min(segment, this.duration - start);
|
266 | } else {
|
267 | duration = this.duration - start;
|
268 | }
|
269 | }
|
270 |
|
271 | start += this.cueIn;
|
272 | const relPos = startTime - this.startTime;
|
273 | const sourcePromise = playoutSystem.setUpSource();
|
274 |
|
275 |
|
276 | _forOwn(this.fades, fade => {
|
277 | let fadeStart;
|
278 | let fadeDuration;
|
279 |
|
280 | if (relPos < fade.end) {
|
281 | if (relPos <= fade.start) {
|
282 | fadeStart = now + (fade.start - relPos);
|
283 | fadeDuration = fade.end - fade.start;
|
284 | } else if (relPos > fade.start && relPos < fade.end) {
|
285 | fadeStart = now - (relPos - fade.start);
|
286 | fadeDuration = fade.end - fade.start;
|
287 | }
|
288 |
|
289 | switch (fade.type) {
|
290 | case FADEIN:
|
291 | {
|
292 | playoutSystem.applyFadeIn(fadeStart, fadeDuration, fade.shape);
|
293 | break;
|
294 | }
|
295 |
|
296 | case FADEOUT:
|
297 | {
|
298 | playoutSystem.applyFadeOut(fadeStart, fadeDuration, fade.shape);
|
299 | break;
|
300 | }
|
301 |
|
302 | default:
|
303 | {
|
304 | throw new Error("Invalid fade type saved on track.");
|
305 | }
|
306 | }
|
307 | }
|
308 | });
|
309 |
|
310 | playoutSystem.setVolumeGainLevel(this.gain);
|
311 | playoutSystem.setShouldPlay(options.shouldPlay);
|
312 | playoutSystem.setMasterGainLevel(options.masterGain);
|
313 | playoutSystem.setStereoPanValue(this.stereoPan);
|
314 | playoutSystem.play(when, start, duration);
|
315 | return sourcePromise;
|
316 | }
|
317 |
|
318 | scheduleStop(when = 0) {
|
319 | this.playout.stop(when);
|
320 | }
|
321 |
|
322 | renderOverlay(data) {
|
323 | const channelPixels = secondsToPixels(data.playlistLength, data.resolution, data.sampleRate);
|
324 | const config = {
|
325 | attributes: {
|
326 | style: `position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: ${channelPixels}px; z-index: 9;`
|
327 | }
|
328 | };
|
329 | let overlayClass = "";
|
330 |
|
331 | if (this.stateObj) {
|
332 | this.stateObj.setup(data.resolution, data.sampleRate);
|
333 | const StateClass = stateClasses[this.state];
|
334 | const events = StateClass.getEvents();
|
335 | events.forEach(event => {
|
336 | config[`on${event}`] = this.stateObj[event].bind(this.stateObj);
|
337 | });
|
338 | overlayClass = StateClass.getClass();
|
339 | }
|
340 |
|
341 |
|
342 | return h(`div.playlist-overlay${overlayClass}`, config);
|
343 | }
|
344 |
|
345 | renderControls(data) {
|
346 | const muteClass = data.muted ? ".active" : "";
|
347 | const soloClass = data.soloed ? ".active" : "";
|
348 | const isCollapsed = data.collapsed;
|
349 | const numChan = this.peaks.data.length;
|
350 | const widgets = data.controls.widgets;
|
351 | const removeTrack = h("button.btn.btn-danger.btn-xs.track-remove", {
|
352 | attributes: {
|
353 | type: "button",
|
354 | title: "Remove track"
|
355 | },
|
356 | onclick: () => {
|
357 | this.ee.emit("removeTrack", this);
|
358 | }
|
359 | }, [h("i.fas.fa-times")]);
|
360 | const trackName = h("span", [this.name]);
|
361 | const collapseTrack = h("button.btn.btn-info.btn-xs.track-collapse", {
|
362 | attributes: {
|
363 | type: "button",
|
364 | title: isCollapsed ? "Expand track" : "Collapse track"
|
365 | },
|
366 | onclick: () => {
|
367 | this.ee.emit("changeTrackView", this, {
|
368 | collapsed: !isCollapsed
|
369 | });
|
370 | }
|
371 | }, [h(`i.fas.${isCollapsed ? "fa-caret-down" : "fa-caret-up"}`)]);
|
372 | const headerChildren = [];
|
373 |
|
374 | if (widgets.remove) {
|
375 | headerChildren.push(removeTrack);
|
376 | }
|
377 |
|
378 | headerChildren.push(trackName);
|
379 |
|
380 | if (widgets.collapse) {
|
381 | headerChildren.push(collapseTrack);
|
382 | }
|
383 |
|
384 | const controls = [h("div.track-header", headerChildren)];
|
385 |
|
386 | if (!isCollapsed) {
|
387 | if (widgets.muteOrSolo) {
|
388 | controls.push(h("div.btn-group", [h(`button.btn.btn-outline-dark.btn-xs.btn-mute${muteClass}`, {
|
389 | attributes: {
|
390 | type: "button"
|
391 | },
|
392 | onclick: () => {
|
393 | this.ee.emit("mute", this);
|
394 | }
|
395 | }, ["Mute"]), h(`button.btn.btn-outline-dark.btn-xs.btn-solo${soloClass}`, {
|
396 | onclick: () => {
|
397 | this.ee.emit("solo", this);
|
398 | }
|
399 | }, ["Solo"])]));
|
400 | }
|
401 |
|
402 | if (widgets.volume) {
|
403 | controls.push(h("label.volume", [h("input.volume-slider", {
|
404 | attributes: {
|
405 | "aria-label": "Track volume control",
|
406 | type: "range",
|
407 | min: 0,
|
408 | max: 100,
|
409 | value: 100
|
410 | },
|
411 | hook: new VolumeSliderHook(this.gain),
|
412 | oninput: e => {
|
413 | this.ee.emit("volumechange", e.target.value, this);
|
414 | }
|
415 | })]));
|
416 | }
|
417 |
|
418 | if (widgets.stereoPan) {
|
419 | controls.push(h("label.stereopan", [h("input.stereopan-slider", {
|
420 | attributes: {
|
421 | "aria-label": "Track stereo pan control",
|
422 | type: "range",
|
423 | min: -100,
|
424 | max: 100,
|
425 | value: 100
|
426 | },
|
427 | hook: new StereoPanSliderHook(this.stereoPan),
|
428 | oninput: e => {
|
429 | this.ee.emit("stereopan", e.target.value / 100, this);
|
430 | }
|
431 | })]));
|
432 | }
|
433 | }
|
434 |
|
435 | return h("div.controls", {
|
436 | attributes: {
|
437 | style: `height: ${numChan * data.height}px; width: ${data.controls.width}px; position: absolute; left: 0; z-index: 10;`
|
438 | }
|
439 | }, controls);
|
440 | }
|
441 |
|
442 | render(data) {
|
443 | const width = this.peaks.length;
|
444 | const playbackX = secondsToPixels(data.playbackSeconds, data.resolution, data.sampleRate);
|
445 | const startX = secondsToPixels(this.startTime, data.resolution, data.sampleRate);
|
446 | const endX = secondsToPixels(this.endTime, data.resolution, data.sampleRate);
|
447 | let progressWidth = 0;
|
448 | const numChan = this.peaks.data.length;
|
449 | const scale = Math.floor(window.devicePixelRatio);
|
450 |
|
451 | if (playbackX > 0 && playbackX > startX) {
|
452 | if (playbackX < endX) {
|
453 | progressWidth = playbackX - startX;
|
454 | } else {
|
455 | progressWidth = width;
|
456 | }
|
457 | }
|
458 |
|
459 | const waveformChildren = [h("div.cursor", {
|
460 | attributes: {
|
461 | style: `position: absolute; width: 1px; margin: 0; padding: 0; top: 0; left: ${playbackX}px; bottom: 0; z-index: 5;`
|
462 | }
|
463 | })];
|
464 | const channels = Object.keys(this.peaks.data).map(channelNum => {
|
465 | const channelChildren = [h("div.channel-progress", {
|
466 | attributes: {
|
467 | style: `position: absolute; width: ${progressWidth}px; height: ${data.height}px; z-index: 2;`
|
468 | }
|
469 | })];
|
470 | let offset = 0;
|
471 | let totalWidth = width;
|
472 | const peaks = this.peaks.data[channelNum];
|
473 |
|
474 | while (totalWidth > 0) {
|
475 | const currentWidth = Math.min(totalWidth, MAX_CANVAS_WIDTH);
|
476 | const canvasColor = this.waveOutlineColor ? this.waveOutlineColor : data.colors.waveOutlineColor;
|
477 | channelChildren.push(h("canvas", {
|
478 | attributes: {
|
479 | width: currentWidth * scale,
|
480 | height: data.height * scale,
|
481 | style: `float: left; position: relative; margin: 0; padding: 0; z-index: 3; width: ${currentWidth}px; height: ${data.height}px;`
|
482 | },
|
483 | hook: new CanvasHook(peaks, offset, this.peaks.bits, canvasColor, scale, data.height, data.barWidth, data.barGap)
|
484 | }));
|
485 | totalWidth -= currentWidth;
|
486 | offset += MAX_CANVAS_WIDTH;
|
487 | }
|
488 |
|
489 |
|
490 | if (this.fadeIn) {
|
491 | const fadeIn = this.fades[this.fadeIn];
|
492 | const fadeWidth = secondsToPixels(fadeIn.end - fadeIn.start, data.resolution, data.sampleRate);
|
493 | channelChildren.push(h("div.wp-fade.wp-fadein", {
|
494 | attributes: {
|
495 | style: `position: absolute; height: ${data.height}px; width: ${fadeWidth}px; top: 0; left: 0; z-index: 4;`
|
496 | }
|
497 | }, [h("canvas", {
|
498 | attributes: {
|
499 | width: fadeWidth,
|
500 | height: data.height
|
501 | },
|
502 | hook: new FadeCanvasHook(fadeIn.type, fadeIn.shape, fadeIn.end - fadeIn.start, data.resolution)
|
503 | })]));
|
504 | }
|
505 |
|
506 | if (this.fadeOut) {
|
507 | const fadeOut = this.fades[this.fadeOut];
|
508 | const fadeWidth = secondsToPixels(fadeOut.end - fadeOut.start, data.resolution, data.sampleRate);
|
509 | channelChildren.push(h("div.wp-fade.wp-fadeout", {
|
510 | attributes: {
|
511 | style: `position: absolute; height: ${data.height}px; width: ${fadeWidth}px; top: 0; right: 0; z-index: 4;`
|
512 | }
|
513 | }, [h("canvas", {
|
514 | attributes: {
|
515 | width: fadeWidth,
|
516 | height: data.height
|
517 | },
|
518 | hook: new FadeCanvasHook(fadeOut.type, fadeOut.shape, fadeOut.end - fadeOut.start, data.resolution)
|
519 | })]));
|
520 | }
|
521 |
|
522 | return h(`div.channel.channel-${channelNum}`, {
|
523 | attributes: {
|
524 | style: `height: ${data.height}px; width: ${width}px; top: ${channelNum * data.height}px; left: ${startX}px; position: absolute; margin: 0; padding: 0; z-index: 1;`
|
525 | }
|
526 | }, channelChildren);
|
527 | });
|
528 | waveformChildren.push(channels);
|
529 | waveformChildren.push(this.renderOverlay(data));
|
530 |
|
531 | if (data.isActive === true) {
|
532 | const cStartX = secondsToPixels(data.timeSelection.start, data.resolution, data.sampleRate);
|
533 | const cEndX = secondsToPixels(data.timeSelection.end, data.resolution, data.sampleRate);
|
534 | const cWidth = cEndX - cStartX + 1;
|
535 | const cClassName = cWidth > 1 ? ".segment" : ".point";
|
536 | waveformChildren.push(h(`div.selection${cClassName}`, {
|
537 | attributes: {
|
538 | style: `position: absolute; width: ${cWidth}px; bottom: 0; top: 0; left: ${cStartX}px; z-index: 4;`
|
539 | }
|
540 | }));
|
541 | }
|
542 |
|
543 | const waveform = h("div.waveform", {
|
544 | attributes: {
|
545 | style: `height: ${numChan * data.height}px; position: relative;`
|
546 | }
|
547 | }, waveformChildren);
|
548 | const channelChildren = [];
|
549 | let channelMargin = 0;
|
550 |
|
551 | if (data.controls.show) {
|
552 | channelChildren.push(this.renderControls(data));
|
553 | channelMargin = data.controls.width;
|
554 | }
|
555 |
|
556 | channelChildren.push(waveform);
|
557 | const audibleClass = data.shouldPlay ? "" : ".silent";
|
558 | const customClass = this.customClass === undefined ? "" : `.${this.customClass}`;
|
559 | return h(`div.channel-wrapper${audibleClass}${customClass}`, {
|
560 | attributes: {
|
561 | style: `margin-left: ${channelMargin}px; height: ${data.height * numChan}px;`
|
562 | }
|
563 | }, channelChildren);
|
564 | }
|
565 |
|
566 | getTrackDetails() {
|
567 | const info = {
|
568 | src: this.src,
|
569 | start: this.startTime,
|
570 | end: this.endTime,
|
571 | name: this.name,
|
572 | customClass: this.customClass,
|
573 | cuein: this.cueIn,
|
574 | cueout: this.cueOut,
|
575 | stereoPan: this.stereoPan,
|
576 | gain: this.gain
|
577 | };
|
578 |
|
579 | if (this.fadeIn) {
|
580 | const fadeIn = this.fades[this.fadeIn];
|
581 | info.fadeIn = {
|
582 | shape: fadeIn.shape,
|
583 | duration: fadeIn.end - fadeIn.start
|
584 | };
|
585 | }
|
586 |
|
587 | if (this.fadeOut) {
|
588 | const fadeOut = this.fades[this.fadeOut];
|
589 | info.fadeOut = {
|
590 | shape: fadeOut.shape,
|
591 | duration: fadeOut.end - fadeOut.start
|
592 | };
|
593 | }
|
594 |
|
595 | return info;
|
596 | }
|
597 |
|
598 | } |
\ | No newline at end of file |