dev@347
|
1 import {
|
dev@347
|
2 Component,
|
dev@347
|
3 ViewChild,
|
dev@347
|
4 ElementRef,
|
dev@347
|
5 Input,
|
dev@347
|
6 ChangeDetectorRef
|
dev@347
|
7 } from '@angular/core';
|
dev@347
|
8 import wavesUI from 'waves-ui-piper';
|
dev@347
|
9 import {attachTouchHandlerBodges} from '../WavesJunk';
|
dev@347
|
10 import {OnSeekHandler} from '../../playhead/PlayHeadHelpers';
|
dev@347
|
11
|
dev@347
|
12 type Layer = any;
|
dev@347
|
13 type Track = any;
|
dev@347
|
14
|
dev@347
|
15 @Component({
|
dev@347
|
16 selector: 'ugly-waveform',
|
dev@347
|
17 templateUrl: './waveform.component.html',
|
dev@347
|
18 styleUrls: ['./waveform.component.css']
|
dev@347
|
19 })
|
dev@347
|
20 export class WaveformComponent {
|
dev@347
|
21
|
dev@347
|
22 @ViewChild('track') trackDiv: ElementRef;
|
dev@347
|
23 @Input() set width(width: number) {
|
dev@347
|
24 if (this.timeline) {
|
dev@347
|
25 requestAnimationFrame(() => {
|
dev@347
|
26 this.timeline.timeContext.visibleWidth = width;
|
dev@347
|
27 this.timeline.tracks.update();
|
dev@347
|
28 });
|
dev@347
|
29 }
|
dev@347
|
30 }
|
dev@347
|
31 @Input() timeline: Timeline;
|
dev@347
|
32 @Input() trackIdPrefix: string;
|
dev@347
|
33 @Input() onSeek: OnSeekHandler;
|
dev@347
|
34
|
dev@347
|
35 @Input() set audioBuffer(buffer: AudioBuffer) {
|
dev@347
|
36 this._audioBuffer = buffer || undefined;
|
dev@347
|
37 if (this.audioBuffer) {
|
dev@347
|
38 this.renderWaveform(this.audioBuffer);
|
dev@347
|
39 }
|
dev@347
|
40 }
|
dev@347
|
41
|
dev@347
|
42 get audioBuffer(): AudioBuffer {
|
dev@347
|
43 return this._audioBuffer;
|
dev@347
|
44 }
|
dev@347
|
45
|
dev@347
|
46 private _audioBuffer: AudioBuffer;
|
dev@347
|
47 private layers: Layer[];
|
dev@347
|
48 private zoomOnMouseDown: number;
|
dev@347
|
49 private offsetOnMouseDown: number;
|
dev@347
|
50 private waveTrack: Track;
|
dev@347
|
51
|
dev@347
|
52 constructor(private ref: ChangeDetectorRef) {
|
dev@347
|
53 this.layers = [];
|
dev@347
|
54 }
|
dev@347
|
55
|
dev@347
|
56 renderTimeline(duration: number = 1.0): Timeline {
|
dev@347
|
57 const track: HTMLElement = this.trackDiv.nativeElement;
|
dev@347
|
58 track.innerHTML = '';
|
dev@347
|
59 const height: number = track.getBoundingClientRect().height;
|
dev@347
|
60 const width: number = track.getBoundingClientRect().width;
|
dev@347
|
61 this.timeline.pixelsPerSecond = width / duration;
|
dev@347
|
62 this.timeline.visibleWidth = width;
|
dev@347
|
63 this.waveTrack = this.timeline.createTrack(
|
dev@347
|
64 track,
|
dev@347
|
65 height,
|
dev@347
|
66 `wave-${this.trackIdPrefix || 'default'}`
|
dev@347
|
67 );
|
dev@347
|
68
|
dev@347
|
69 if ('ontouchstart' in window) {
|
dev@347
|
70 attachTouchHandlerBodges(this.trackDiv.nativeElement, this.timeline);
|
dev@347
|
71 }
|
dev@347
|
72 }
|
dev@347
|
73
|
dev@347
|
74 // TODO can likely be removed, or use waves-ui methods
|
dev@347
|
75 clearTimeline(): void {
|
dev@347
|
76 // loop through layers and remove them, waves-ui provides methods for this but it seems to not work properly
|
dev@347
|
77 const timeContextChildren = this.timeline.timeContext._children;
|
dev@347
|
78 for (const track of this.timeline.tracks) {
|
dev@347
|
79 if (track.layers.length === 0) { continue; }
|
dev@347
|
80 const trackLayers = Array.from(track.layers);
|
dev@347
|
81 while (trackLayers.length) {
|
dev@347
|
82 const layer: Layer = trackLayers.pop();
|
dev@347
|
83 if (this.layers.includes(layer)) {
|
dev@347
|
84 track.remove(layer);
|
dev@347
|
85 this.layers.splice(this.layers.indexOf(layer), 1);
|
dev@347
|
86 const index = timeContextChildren.indexOf(layer.timeContext);
|
dev@347
|
87 if (index >= 0) {
|
dev@347
|
88 timeContextChildren.splice(index, 1);
|
dev@347
|
89 }
|
dev@347
|
90 layer.destroy();
|
dev@347
|
91 }
|
dev@347
|
92 }
|
dev@347
|
93 }
|
dev@347
|
94 }
|
dev@347
|
95
|
dev@347
|
96 renderWaveform(buffer: AudioBuffer): void {
|
dev@347
|
97 const height = this.trackDiv.nativeElement.getBoundingClientRect().height;
|
dev@347
|
98 if (this.timeline && this.waveTrack) {
|
dev@347
|
99 // resize
|
dev@347
|
100 const width = this.trackDiv.nativeElement.getBoundingClientRect().width;
|
dev@347
|
101
|
dev@347
|
102 this.clearTimeline();
|
dev@347
|
103 this.timeline.visibleWidth = width;
|
dev@347
|
104 this.timeline.pixelsPerSecond = width / buffer.duration;
|
dev@347
|
105 this.waveTrack.height = height;
|
dev@347
|
106 } else {
|
dev@347
|
107 this.renderTimeline(buffer.duration);
|
dev@347
|
108 }
|
dev@347
|
109 this.timeline.timeContext.offset = 0.5 * this.timeline.timeContext.visibleDuration;
|
dev@347
|
110
|
dev@347
|
111 // time axis
|
dev@347
|
112 const timeAxis = new wavesUI.helpers.TimeAxisLayer({
|
dev@347
|
113 height: height,
|
dev@347
|
114 color: '#b0b0b0'
|
dev@347
|
115 });
|
dev@347
|
116 this.addLayer(timeAxis, this.waveTrack, this.timeline.timeContext, true);
|
dev@347
|
117
|
dev@347
|
118 const nchannels = buffer.numberOfChannels;
|
dev@347
|
119 const totalWaveHeight = height * 0.9;
|
dev@347
|
120 const waveHeight = totalWaveHeight / nchannels;
|
dev@347
|
121
|
dev@347
|
122 for (let ch = 0; ch < nchannels; ++ch) {
|
dev@347
|
123 const waveformLayer = new wavesUI.helpers.WaveformLayer(buffer, {
|
dev@347
|
124 top: (height - totalWaveHeight) / 2 + waveHeight * ch,
|
dev@347
|
125 height: waveHeight,
|
dev@347
|
126 color: '#0868ac',
|
dev@347
|
127 channel: ch
|
dev@347
|
128 });
|
dev@347
|
129 this.addLayer(waveformLayer, this.waveTrack, this.timeline.timeContext);
|
dev@347
|
130 }
|
dev@347
|
131
|
dev@347
|
132 this.timeline.state = new wavesUI.states.CenteredZoomState(this.timeline);
|
dev@347
|
133 this.waveTrack.render();
|
dev@347
|
134 this.waveTrack.update();
|
dev@347
|
135 this.ref.markForCheck();
|
dev@347
|
136 }
|
dev@347
|
137
|
dev@347
|
138 // TODO can likely use methods in waves-ui directly
|
dev@347
|
139 private addLayer(layer: Layer,
|
dev@347
|
140 track: Track,
|
dev@347
|
141 timeContext: any,
|
dev@347
|
142 isAxis: boolean = false): void {
|
dev@347
|
143 timeContext.zoom = 1.0;
|
dev@347
|
144 if (!layer.timeContext) {
|
dev@347
|
145 layer.setTimeContext(isAxis ?
|
dev@347
|
146 timeContext : new wavesUI.core.LayerTimeContext(timeContext));
|
dev@347
|
147 }
|
dev@347
|
148 track.add(layer);
|
dev@347
|
149 this.layers.push(layer);
|
dev@347
|
150 layer.render();
|
dev@347
|
151 layer.update();
|
dev@347
|
152 }
|
dev@347
|
153
|
dev@347
|
154 seekStart(): void {
|
dev@347
|
155 this.zoomOnMouseDown = this.timeline.timeContext.zoom;
|
dev@347
|
156 this.offsetOnMouseDown = this.timeline.timeContext.offset;
|
dev@347
|
157 }
|
dev@347
|
158
|
dev@347
|
159 seekEnd(x: number): void {
|
dev@347
|
160 const hasSameZoom: boolean = this.zoomOnMouseDown ===
|
dev@347
|
161 this.timeline.timeContext.zoom;
|
dev@347
|
162 const hasSameOffset: boolean = this.offsetOnMouseDown ===
|
dev@347
|
163 this.timeline.timeContext.offset;
|
dev@347
|
164 if (hasSameZoom && hasSameOffset) {
|
dev@347
|
165 this.seek(x);
|
dev@347
|
166 }
|
dev@347
|
167 }
|
dev@347
|
168
|
dev@347
|
169 seek(x: number): void {
|
dev@347
|
170 if (this.timeline) {
|
dev@347
|
171 const timeContext: any = this.timeline.timeContext;
|
dev@347
|
172 if (this.onSeek) {
|
dev@347
|
173 this.onSeek(timeContext.timeToPixel.invert(x) - timeContext.offset);
|
dev@347
|
174 }
|
dev@347
|
175 }
|
dev@347
|
176 }
|
dev@347
|
177 }
|