UNPKG

23.5 kBJavaScriptView Raw
1import _defaults from "lodash.defaultsdeep";
2import h from "virtual-dom/h";
3import diff from "virtual-dom/diff";
4import patch from "virtual-dom/patch";
5import InlineWorker from "inline-worker";
6import { pixelsToSeconds } from "./utils/conversions";
7import LoaderFactory from "./track/loader/LoaderFactory";
8import ScrollHook from "./render/ScrollHook";
9import TimeScale from "./TimeScale";
10import Track from "./Track";
11import Playout from "./Playout";
12import AnnotationList from "./annotation/AnnotationList";
13import RecorderWorkerFunction from "./utils/recorderWorker";
14import ExportWavWorkerFunction from "./utils/exportWavWorker";
15export default class {
16 constructor() {
17 this.tracks = [];
18 this.soloedTracks = [];
19 this.mutedTracks = [];
20 this.collapsedTracks = [];
21 this.playoutPromises = [];
22 this.cursor = 0;
23 this.playbackSeconds = 0;
24 this.duration = 0;
25 this.scrollLeft = 0;
26 this.scrollTimer = undefined;
27 this.showTimescale = false; // whether a user is scrolling the waveform
28
29 this.isScrolling = false;
30 this.fadeType = "logarithmic";
31 this.masterGain = 1;
32 this.annotations = [];
33 this.durationFormat = "hh:mm:ss.uuu";
34 this.isAutomaticScroll = false;
35 this.resetDrawTimer = undefined;
36 } // TODO extract into a plugin
37
38
39 initExporter() {
40 this.exportWorker = new InlineWorker(ExportWavWorkerFunction);
41 } // TODO extract into a plugin
42
43
44 initRecorder(stream) {
45 this.mediaRecorder = new MediaRecorder(stream);
46
47 this.mediaRecorder.onstart = () => {
48 const track = new Track();
49 track.setName("Recording");
50 track.setEnabledStates();
51 track.setEventEmitter(this.ee);
52 this.recordingTrack = track;
53 this.tracks.push(track);
54 this.chunks = [];
55 this.working = false;
56 };
57
58 this.mediaRecorder.ondataavailable = e => {
59 this.chunks.push(e.data); // throttle peaks calculation
60
61 if (!this.working) {
62 const recording = new Blob(this.chunks, {
63 type: "audio/ogg; codecs=opus"
64 });
65 const loader = LoaderFactory.createLoader(recording, this.ac);
66 loader.load().then(audioBuffer => {
67 // ask web worker for peaks.
68 this.recorderWorker.postMessage({
69 samples: audioBuffer.getChannelData(0),
70 samplesPerPixel: this.samplesPerPixel
71 });
72 this.recordingTrack.setCues(0, audioBuffer.duration);
73 this.recordingTrack.setBuffer(audioBuffer);
74 this.recordingTrack.setPlayout(new Playout(this.ac, audioBuffer));
75 this.adjustDuration();
76 }).catch(() => {
77 this.working = false;
78 });
79 this.working = true;
80 }
81 };
82
83 this.mediaRecorder.onstop = () => {
84 this.chunks = [];
85 this.working = false;
86 };
87
88 this.recorderWorker = new InlineWorker(RecorderWorkerFunction); // use a worker for calculating recording peaks.
89
90 this.recorderWorker.onmessage = e => {
91 this.recordingTrack.setPeaks(e.data);
92 this.working = false;
93 this.drawRequest();
94 };
95 }
96
97 setShowTimeScale(show) {
98 this.showTimescale = show;
99 }
100
101 setMono(mono) {
102 this.mono = mono;
103 }
104
105 setExclSolo(exclSolo) {
106 this.exclSolo = exclSolo;
107 }
108
109 setSeekStyle(style) {
110 this.seekStyle = style;
111 }
112
113 getSeekStyle() {
114 return this.seekStyle;
115 }
116
117 setSampleRate(sampleRate) {
118 this.sampleRate = sampleRate;
119 }
120
121 setSamplesPerPixel(samplesPerPixel) {
122 this.samplesPerPixel = samplesPerPixel;
123 }
124
125 setAudioContext(ac) {
126 this.ac = ac;
127 }
128
129 setControlOptions(controlOptions) {
130 this.controls = controlOptions;
131 }
132
133 setWaveHeight(height) {
134 this.waveHeight = height;
135 }
136
137 setCollapsedWaveHeight(height) {
138 this.collapsedWaveHeight = height;
139 }
140
141 setColors(colors) {
142 this.colors = colors;
143 }
144
145 setBarWidth(width) {
146 this.barWidth = width;
147 }
148
149 setBarGap(width) {
150 this.barGap = width;
151 }
152
153 setAnnotations(config) {
154 const controlWidth = this.controls.show ? this.controls.width : 0;
155 this.annotationList = new AnnotationList(this, config.annotations, config.controls, config.editable, config.linkEndpoints, config.isContinuousPlay, controlWidth);
156 }
157
158 setEventEmitter(ee) {
159 this.ee = ee;
160 }
161
162 getEventEmitter() {
163 return this.ee;
164 }
165
166 setUpEventEmitter() {
167 const ee = this.ee;
168 ee.on("automaticscroll", val => {
169 this.isAutomaticScroll = val;
170 });
171 ee.on("durationformat", format => {
172 this.durationFormat = format;
173 this.drawRequest();
174 });
175 ee.on("select", (start, end, track) => {
176 if (this.isPlaying()) {
177 this.lastSeeked = start;
178 this.pausedAt = undefined;
179 this.restartPlayFrom(start);
180 } else {
181 // reset if it was paused.
182 this.seek(start, end, track);
183 this.ee.emit("timeupdate", start);
184 this.drawRequest();
185 }
186 });
187 ee.on("startaudiorendering", type => {
188 this.startOfflineRender(type);
189 });
190 ee.on("statechange", state => {
191 this.setState(state);
192 this.drawRequest();
193 });
194 ee.on("shift", (deltaTime, track) => {
195 track.setStartTime(track.getStartTime() + deltaTime);
196 this.adjustDuration();
197 this.drawRequest();
198 });
199 ee.on("record", () => {
200 this.record();
201 });
202 ee.on("play", (start, end) => {
203 this.play(start, end);
204 });
205 ee.on("pause", () => {
206 this.pause();
207 });
208 ee.on("stop", () => {
209 this.stop();
210 });
211 ee.on("rewind", () => {
212 this.rewind();
213 });
214 ee.on("fastforward", () => {
215 this.fastForward();
216 });
217 ee.on("clear", () => {
218 this.clear().then(() => {
219 this.drawRequest();
220 });
221 });
222 ee.on("solo", track => {
223 this.soloTrack(track);
224 this.adjustTrackPlayout();
225 this.drawRequest();
226 });
227 ee.on("mute", track => {
228 this.muteTrack(track);
229 this.adjustTrackPlayout();
230 this.drawRequest();
231 });
232 ee.on("removeTrack", track => {
233 this.removeTrack(track);
234 this.adjustTrackPlayout();
235 this.drawRequest();
236 });
237 ee.on("changeTrackView", (track, opts) => {
238 this.collapseTrack(track, opts);
239 this.drawRequest();
240 });
241 ee.on("volumechange", (volume, track) => {
242 track.setGainLevel(volume / 100);
243 this.drawRequest();
244 });
245 ee.on("mastervolumechange", volume => {
246 this.masterGain = volume / 100;
247 this.tracks.forEach(track => {
248 track.setMasterGainLevel(this.masterGain);
249 });
250 });
251 ee.on("fadein", (duration, track) => {
252 track.setFadeIn(duration, this.fadeType);
253 this.drawRequest();
254 });
255 ee.on("fadeout", (duration, track) => {
256 track.setFadeOut(duration, this.fadeType);
257 this.drawRequest();
258 });
259 ee.on("stereopan", (panvalue, track) => {
260 track.setStereoPanValue(panvalue);
261 this.drawRequest();
262 });
263 ee.on("fadetype", type => {
264 this.fadeType = type;
265 });
266 ee.on("newtrack", file => {
267 this.load([{
268 src: file,
269 name: file.name
270 }]);
271 });
272 ee.on("trim", () => {
273 const track = this.getActiveTrack();
274 const timeSelection = this.getTimeSelection();
275 track.trim(timeSelection.start, timeSelection.end);
276 track.calculatePeaks(this.samplesPerPixel, this.sampleRate);
277 this.setTimeSelection(0, 0);
278 this.drawRequest();
279 });
280 ee.on("zoomin", () => {
281 const zoomIndex = Math.max(0, this.zoomIndex - 1);
282 const zoom = this.zoomLevels[zoomIndex];
283
284 if (zoom !== this.samplesPerPixel) {
285 this.setZoom(zoom);
286 this.drawRequest();
287 }
288 });
289 ee.on("zoomout", () => {
290 const zoomIndex = Math.min(this.zoomLevels.length - 1, this.zoomIndex + 1);
291 const zoom = this.zoomLevels[zoomIndex];
292
293 if (zoom !== this.samplesPerPixel) {
294 this.setZoom(zoom);
295 this.drawRequest();
296 }
297 });
298 ee.on("scroll", () => {
299 this.isScrolling = true;
300 this.drawRequest();
301 clearTimeout(this.scrollTimer);
302 this.scrollTimer = setTimeout(() => {
303 this.isScrolling = false;
304 }, 200);
305 });
306 }
307
308 load(trackList) {
309 const loadPromises = trackList.map(trackInfo => {
310 const loader = LoaderFactory.createLoader(trackInfo.src, this.ac, this.ee);
311 return loader.load();
312 });
313 return Promise.all(loadPromises).then(audioBuffers => {
314 this.ee.emit("audiosourcesloaded");
315 const tracks = audioBuffers.map((audioBuffer, index) => {
316 const info = trackList[index];
317 const name = info.name || "Untitled";
318 const start = info.start || 0;
319 const states = info.states || {};
320 const fadeIn = info.fadeIn;
321 const fadeOut = info.fadeOut;
322 const cueIn = info.cuein || 0;
323 const cueOut = info.cueout || audioBuffer.duration;
324 const gain = info.gain || 1;
325 const muted = info.muted || false;
326 const soloed = info.soloed || false;
327 const selection = info.selected;
328 const peaks = info.peaks || {
329 type: "WebAudio",
330 mono: this.mono
331 };
332 const customClass = info.customClass || undefined;
333 const waveOutlineColor = info.waveOutlineColor || undefined;
334 const stereoPan = info.stereoPan || 0; // webaudio specific playout for now.
335
336 const playout = new Playout(this.ac, audioBuffer);
337 const track = new Track();
338 track.src = info.src;
339 track.setBuffer(audioBuffer);
340 track.setName(name);
341 track.setEventEmitter(this.ee);
342 track.setEnabledStates(states);
343 track.setCues(cueIn, cueOut);
344 track.setCustomClass(customClass);
345 track.setWaveOutlineColor(waveOutlineColor);
346
347 if (fadeIn !== undefined) {
348 track.setFadeIn(fadeIn.duration, fadeIn.shape);
349 }
350
351 if (fadeOut !== undefined) {
352 track.setFadeOut(fadeOut.duration, fadeOut.shape);
353 }
354
355 if (selection !== undefined) {
356 this.setActiveTrack(track);
357 this.setTimeSelection(selection.start, selection.end);
358 }
359
360 if (peaks !== undefined) {
361 track.setPeakData(peaks);
362 }
363
364 track.setState(this.getState());
365 track.setStartTime(start);
366 track.setPlayout(playout);
367 track.setGainLevel(gain);
368 track.setStereoPanValue(stereoPan);
369
370 if (muted) {
371 this.muteTrack(track);
372 }
373
374 if (soloed) {
375 this.soloTrack(track);
376 } // extract peaks with AudioContext for now.
377
378
379 track.calculatePeaks(this.samplesPerPixel, this.sampleRate);
380 return track;
381 });
382 this.tracks = this.tracks.concat(tracks);
383 this.adjustDuration();
384 this.draw(this.render());
385 this.ee.emit("audiosourcesrendered");
386 }).catch(e => {
387 this.ee.emit("audiosourceserror", e);
388 });
389 }
390 /*
391 track instance of Track.
392 */
393
394
395 setActiveTrack(track) {
396 this.activeTrack = track;
397 }
398
399 getActiveTrack() {
400 return this.activeTrack;
401 }
402
403 isSegmentSelection() {
404 return this.timeSelection.start !== this.timeSelection.end;
405 }
406 /*
407 start, end in seconds.
408 */
409
410
411 setTimeSelection(start = 0, end) {
412 this.timeSelection = {
413 start,
414 end: end === undefined ? start : end
415 };
416 this.cursor = start;
417 }
418
419 startOfflineRender(type) {
420 if (this.isRendering) {
421 return;
422 }
423
424 this.isRendering = true;
425 this.offlineAudioContext = new OfflineAudioContext(2, 44100 * this.duration, 44100);
426 const currentTime = this.offlineAudioContext.currentTime;
427 this.tracks.forEach(track => {
428 track.setOfflinePlayout(new Playout(this.offlineAudioContext, track.buffer));
429 track.schedulePlay(currentTime, 0, 0, {
430 shouldPlay: this.shouldTrackPlay(track),
431 masterGain: 1,
432 isOffline: true
433 });
434 });
435 /*
436 TODO cleanup of different audio playouts handling.
437 */
438
439 this.offlineAudioContext.startRendering().then(audioBuffer => {
440 if (type === "buffer") {
441 this.ee.emit("audiorenderingfinished", type, audioBuffer);
442 this.isRendering = false;
443 return;
444 }
445
446 if (type === "wav") {
447 this.exportWorker.postMessage({
448 command: "init",
449 config: {
450 sampleRate: 44100
451 }
452 }); // callback for `exportWAV`
453
454 this.exportWorker.onmessage = e => {
455 this.ee.emit("audiorenderingfinished", type, e.data);
456 this.isRendering = false; // clear out the buffer for next renderings.
457
458 this.exportWorker.postMessage({
459 command: "clear"
460 });
461 }; // send the channel data from our buffer to the worker
462
463
464 this.exportWorker.postMessage({
465 command: "record",
466 buffer: [audioBuffer.getChannelData(0), audioBuffer.getChannelData(1)]
467 }); // ask the worker for a WAV
468
469 this.exportWorker.postMessage({
470 command: "exportWAV",
471 type: "audio/wav"
472 });
473 }
474 }).catch(e => {
475 throw e;
476 });
477 }
478
479 getTimeSelection() {
480 return this.timeSelection;
481 }
482
483 setState(state) {
484 this.state = state;
485 this.tracks.forEach(track => {
486 track.setState(state);
487 });
488 }
489
490 getState() {
491 return this.state;
492 }
493
494 setZoomIndex(index) {
495 this.zoomIndex = index;
496 }
497
498 setZoomLevels(levels) {
499 this.zoomLevels = levels;
500 }
501
502 setZoom(zoom) {
503 this.samplesPerPixel = zoom;
504 this.zoomIndex = this.zoomLevels.indexOf(zoom);
505 this.tracks.forEach(track => {
506 track.calculatePeaks(zoom, this.sampleRate);
507 });
508 }
509
510 muteTrack(track) {
511 const index = this.mutedTracks.indexOf(track);
512
513 if (index > -1) {
514 this.mutedTracks.splice(index, 1);
515 } else {
516 this.mutedTracks.push(track);
517 }
518 }
519
520 soloTrack(track) {
521 const index = this.soloedTracks.indexOf(track);
522
523 if (index > -1) {
524 this.soloedTracks.splice(index, 1);
525 } else if (this.exclSolo) {
526 this.soloedTracks = [track];
527 } else {
528 this.soloedTracks.push(track);
529 }
530 }
531
532 collapseTrack(track, opts) {
533 if (opts.collapsed) {
534 this.collapsedTracks.push(track);
535 } else {
536 const index = this.collapsedTracks.indexOf(track);
537
538 if (index > -1) {
539 this.collapsedTracks.splice(index, 1);
540 }
541 }
542 }
543
544 removeTrack(track) {
545 if (track.isPlaying()) {
546 track.scheduleStop();
547 }
548
549 const trackLists = [this.mutedTracks, this.soloedTracks, this.collapsedTracks, this.tracks];
550 trackLists.forEach(list => {
551 const index = list.indexOf(track);
552
553 if (index > -1) {
554 list.splice(index, 1);
555 }
556 });
557 }
558
559 adjustTrackPlayout() {
560 this.tracks.forEach(track => {
561 track.setShouldPlay(this.shouldTrackPlay(track));
562 });
563 }
564
565 adjustDuration() {
566 this.duration = this.tracks.reduce((duration, track) => Math.max(duration, track.getEndTime()), 0);
567 }
568
569 shouldTrackPlay(track) {
570 let shouldPlay; // if there are solo tracks, only they should play.
571
572 if (this.soloedTracks.length > 0) {
573 shouldPlay = false;
574
575 if (this.soloedTracks.indexOf(track) > -1) {
576 shouldPlay = true;
577 }
578 } else {
579 // play all tracks except any muted tracks.
580 shouldPlay = true;
581
582 if (this.mutedTracks.indexOf(track) > -1) {
583 shouldPlay = false;
584 }
585 }
586
587 return shouldPlay;
588 }
589
590 isPlaying() {
591 return this.tracks.reduce((isPlaying, track) => isPlaying || track.isPlaying(), false);
592 }
593 /*
594 * returns the current point of time in the playlist in seconds.
595 */
596
597
598 getCurrentTime() {
599 const cursorPos = this.lastSeeked || this.pausedAt || this.cursor;
600 return cursorPos + this.getElapsedTime();
601 }
602
603 getElapsedTime() {
604 return this.ac.currentTime - this.lastPlay;
605 }
606
607 setMasterGain(gain) {
608 this.ee.emit("mastervolumechange", gain);
609 }
610
611 restartPlayFrom(start, end) {
612 this.stopAnimation();
613 this.tracks.forEach(editor => {
614 editor.scheduleStop();
615 });
616 return Promise.all(this.playoutPromises).then(this.play.bind(this, start, end));
617 }
618
619 play(startTime, endTime) {
620 clearTimeout(this.resetDrawTimer);
621 const currentTime = this.ac.currentTime;
622 const selected = this.getTimeSelection();
623 const playoutPromises = [];
624 const start = startTime || this.pausedAt || this.cursor;
625 let end = endTime;
626
627 if (!end && selected.end !== selected.start && selected.end > start) {
628 end = selected.end;
629 }
630
631 if (this.isPlaying()) {
632 return this.restartPlayFrom(start, end);
633 }
634
635 this.tracks.forEach(track => {
636 track.setState("cursor");
637 playoutPromises.push(track.schedulePlay(currentTime, start, end, {
638 shouldPlay: this.shouldTrackPlay(track),
639 masterGain: this.masterGain
640 }));
641 });
642 this.lastPlay = currentTime; // use these to track when the playlist has fully stopped.
643
644 this.playoutPromises = playoutPromises;
645 this.startAnimation(start);
646 return Promise.all(this.playoutPromises);
647 }
648
649 pause() {
650 if (!this.isPlaying()) {
651 return Promise.all(this.playoutPromises);
652 }
653
654 this.pausedAt = this.getCurrentTime();
655 return this.playbackReset();
656 }
657
658 stop() {
659 if (this.mediaRecorder && this.mediaRecorder.state === "recording") {
660 this.mediaRecorder.stop();
661 }
662
663 this.pausedAt = undefined;
664 this.playbackSeconds = 0;
665 return this.playbackReset();
666 }
667
668 playbackReset() {
669 this.lastSeeked = undefined;
670 this.stopAnimation();
671 this.tracks.forEach(track => {
672 track.scheduleStop();
673 track.setState(this.getState());
674 });
675 this.drawRequest();
676 return Promise.all(this.playoutPromises);
677 }
678
679 rewind() {
680 return this.stop().then(() => {
681 this.scrollLeft = 0;
682 this.ee.emit("select", 0, 0);
683 });
684 }
685
686 fastForward() {
687 return this.stop().then(() => {
688 if (this.viewDuration < this.duration) {
689 this.scrollLeft = this.duration - this.viewDuration;
690 } else {
691 this.scrollLeft = 0;
692 }
693
694 this.ee.emit("select", this.duration, this.duration);
695 });
696 }
697
698 clear() {
699 return this.stop().then(() => {
700 this.tracks = [];
701 this.soloedTracks = [];
702 this.mutedTracks = [];
703 this.playoutPromises = [];
704 this.cursor = 0;
705 this.playbackSeconds = 0;
706 this.duration = 0;
707 this.scrollLeft = 0;
708 this.seek(0, 0, undefined);
709 });
710 }
711
712 record() {
713 const playoutPromises = [];
714 this.mediaRecorder.start(300);
715 this.tracks.forEach(track => {
716 track.setState("none");
717 playoutPromises.push(track.schedulePlay(this.ac.currentTime, 0, undefined, {
718 shouldPlay: this.shouldTrackPlay(track)
719 }));
720 });
721 this.playoutPromises = playoutPromises;
722 }
723
724 startAnimation(startTime) {
725 this.lastDraw = this.ac.currentTime;
726 this.animationRequest = window.requestAnimationFrame(() => {
727 this.updateEditor(startTime);
728 });
729 }
730
731 stopAnimation() {
732 window.cancelAnimationFrame(this.animationRequest);
733 this.lastDraw = undefined;
734 }
735
736 seek(start, end, track) {
737 if (this.isPlaying()) {
738 this.lastSeeked = start;
739 this.pausedAt = undefined;
740 this.restartPlayFrom(start);
741 } else {
742 // reset if it was paused.
743 this.setActiveTrack(track || this.tracks[0]);
744 this.pausedAt = start;
745 this.setTimeSelection(start, end);
746
747 if (this.getSeekStyle() === "fill") {
748 this.playbackSeconds = start;
749 }
750 }
751 }
752 /*
753 * Animation function for the playlist.
754 * Keep under 16.7 milliseconds based on a typical screen refresh rate of 60fps.
755 */
756
757
758 updateEditor(cursor) {
759 const currentTime = this.ac.currentTime;
760 const selection = this.getTimeSelection();
761 const cursorPos = cursor || this.cursor;
762 const elapsed = currentTime - this.lastDraw;
763
764 if (this.isPlaying()) {
765 const playbackSeconds = cursorPos + elapsed;
766 this.ee.emit("timeupdate", playbackSeconds);
767 this.animationRequest = window.requestAnimationFrame(() => {
768 this.updateEditor(playbackSeconds);
769 });
770 this.playbackSeconds = playbackSeconds;
771 this.draw(this.render());
772 this.lastDraw = currentTime;
773 } else {
774 if (cursorPos + elapsed >= (this.isSegmentSelection() ? selection.end : this.duration)) {
775 this.ee.emit("finished");
776 }
777
778 this.stopAnimation();
779 this.resetDrawTimer = setTimeout(() => {
780 this.pausedAt = undefined;
781 this.lastSeeked = undefined;
782 this.setState(this.getState());
783 this.playbackSeconds = 0;
784 this.draw(this.render());
785 }, 0);
786 }
787 }
788
789 drawRequest() {
790 window.requestAnimationFrame(() => {
791 this.draw(this.render());
792 });
793 }
794
795 draw(newTree) {
796 const patches = diff(this.tree, newTree);
797 this.rootNode = patch(this.rootNode, patches);
798 this.tree = newTree; // use for fast forwarding.
799
800 this.viewDuration = pixelsToSeconds(this.rootNode.clientWidth - this.controls.width, this.samplesPerPixel, this.sampleRate);
801 }
802
803 getTrackRenderData(data = {}) {
804 const defaults = {
805 height: this.waveHeight,
806 resolution: this.samplesPerPixel,
807 sampleRate: this.sampleRate,
808 controls: this.controls,
809 isActive: false,
810 timeSelection: this.getTimeSelection(),
811 playlistLength: this.duration,
812 playbackSeconds: this.playbackSeconds,
813 colors: this.colors,
814 barWidth: this.barWidth,
815 barGap: this.barGap
816 };
817 return _defaults({}, data, defaults);
818 }
819
820 isActiveTrack(track) {
821 const activeTrack = this.getActiveTrack();
822
823 if (this.isSegmentSelection()) {
824 return activeTrack === track;
825 }
826
827 return true;
828 }
829
830 renderAnnotations() {
831 return this.annotationList.render();
832 }
833
834 renderTimeScale() {
835 const controlWidth = this.controls.show ? this.controls.width : 0;
836 const timeScale = new TimeScale(this.duration, this.scrollLeft, this.samplesPerPixel, this.sampleRate, controlWidth, this.colors);
837 return timeScale.render();
838 }
839
840 renderTrackSection() {
841 const trackElements = this.tracks.map(track => {
842 const collapsed = this.collapsedTracks.indexOf(track) > -1;
843 return track.render(this.getTrackRenderData({
844 isActive: this.isActiveTrack(track),
845 shouldPlay: this.shouldTrackPlay(track),
846 soloed: this.soloedTracks.indexOf(track) > -1,
847 muted: this.mutedTracks.indexOf(track) > -1,
848 collapsed,
849 height: collapsed ? this.collapsedWaveHeight : this.waveHeight,
850 barGap: this.barGap,
851 barWidth: this.barWidth
852 }));
853 });
854 return h("div.playlist-tracks", {
855 attributes: {
856 style: "overflow: auto;"
857 },
858 onscroll: e => {
859 this.scrollLeft = pixelsToSeconds(e.target.scrollLeft, this.samplesPerPixel, this.sampleRate);
860 this.ee.emit("scroll");
861 },
862 hook: new ScrollHook(this)
863 }, trackElements);
864 }
865
866 render() {
867 const containerChildren = [];
868
869 if (this.showTimescale) {
870 containerChildren.push(this.renderTimeScale());
871 }
872
873 containerChildren.push(this.renderTrackSection());
874
875 if (this.annotationList.length) {
876 containerChildren.push(this.renderAnnotations());
877 }
878
879 return h("div.playlist", {
880 attributes: {
881 style: "overflow: hidden; position: relative;"
882 }
883 }, containerChildren);
884 }
885
886 getInfo() {
887 const info = [];
888 this.tracks.forEach(track => {
889 info.push(track.getTrackDetails());
890 });
891 return info;
892 }
893
894}
\No newline at end of file