Mercurial > hg > ugly-duckling
comparison src/app/waveform/waveform.component.ts @ 196:aa1c92c553cb
A few different @Input flags allowing for using component for just a waveform or features or both, turning off seeking and allowing more than one feature to be extracted to the component. Very messy, desperately needs refactoring.
author | Lucas Thompson <dev@lucas.im> |
---|---|
date | Fri, 24 Mar 2017 11:00:54 +0000 |
parents | a50feba0d7f0 |
children | bdfd4b4b7130 |
comparison
equal
deleted
inserted
replaced
195:3ba03d9f0059 | 196:aa1c92c553cb |
---|---|
1 import { | 1 import { |
2 Component, OnInit, ViewChild, ElementRef, Input, AfterViewInit, NgZone, | 2 Component, OnInit, ViewChild, ElementRef, Input, AfterViewInit, NgZone, |
3 OnDestroy | 3 OnDestroy |
4 } from '@angular/core'; | 4 } from '@angular/core'; |
5 import {AudioPlayerService} from "../services/audio-player/audio-player.service"; | 5 import { |
6 AudioPlayerService, AudioResource, | |
7 AudioResourceError | |
8 } from "../services/audio-player/audio-player.service"; | |
6 import wavesUI from 'waves-ui'; | 9 import wavesUI from 'waves-ui'; |
7 import { | 10 import { |
8 FeatureExtractionService | 11 FeatureExtractionService |
9 } from "../services/feature-extraction/feature-extraction.service"; | 12 } from "../services/feature-extraction/feature-extraction.service"; |
10 import {Subscription} from "rxjs"; | 13 import {Subscription} from "rxjs"; |
30 | 33 |
31 @ViewChild('track') trackDiv: ElementRef; | 34 @ViewChild('track') trackDiv: ElementRef; |
32 | 35 |
33 @Input() timeline: Timeline; | 36 @Input() timeline: Timeline; |
34 @Input() trackIdPrefix: string; | 37 @Input() trackIdPrefix: string; |
35 private _audioBuffer: AudioBuffer; | 38 @Input() set isSubscribedToExtractionService(isSubscribed: boolean) { |
36 private cursorLayer: any; | 39 if (isSubscribed) { |
37 private layers: Layer[]; | 40 if (this.featureExtractionSubscription) { |
38 | 41 return; |
39 @Input() | 42 } |
43 | |
44 const colours = function* () { | |
45 const circularColours = [ | |
46 'black', | |
47 'red', | |
48 'green', | |
49 'purple', | |
50 'orange' | |
51 ]; | |
52 let index = 0; | |
53 const nColours = circularColours.length; | |
54 while (true) { | |
55 yield circularColours[index = ++index % nColours]; | |
56 } | |
57 }(); | |
58 | |
59 this.featureExtractionSubscription = | |
60 this.piperService.featuresExtracted$.subscribe( | |
61 features => { | |
62 this.renderFeatures(features, colours.next().value); | |
63 }); | |
64 } else { | |
65 if (this.featureExtractionSubscription) { | |
66 this.featureExtractionSubscription.unsubscribe(); | |
67 } | |
68 } | |
69 } | |
70 @Input() set isSubscribedToAudioService(isSubscribed: boolean) { | |
71 this._isSubscribedToAudioService = isSubscribed; | |
72 if (isSubscribed) { | |
73 if (this.onAudioDataSubscription) { | |
74 return; | |
75 } | |
76 | |
77 this.onAudioDataSubscription = | |
78 this.audioService.audioLoaded$.subscribe(res => { | |
79 const wasError = (res as AudioResourceError).message != null; | |
80 | |
81 if (wasError) { | |
82 console.warn('No audio, display error?'); | |
83 } else { | |
84 this.audioBuffer = (res as AudioResource).samples; | |
85 } | |
86 }); | |
87 } else { | |
88 if (this.onAudioDataSubscription) { | |
89 this.onAudioDataSubscription.unsubscribe(); | |
90 } | |
91 } | |
92 } | |
93 | |
94 get isSubscribedToAudioService(): boolean { | |
95 return this._isSubscribedToAudioService; | |
96 } | |
97 | |
98 @Input() set isOneShotExtractor(isOneShot: boolean) { | |
99 this._isOneShotExtractor = isOneShot; | |
100 } | |
101 | |
102 get isOneShotExtractor(): boolean { | |
103 return this._isOneShotExtractor; | |
104 } | |
105 | |
106 @Input() set isSeeking(isSeeking: boolean) { | |
107 this._isSeeking = isSeeking; | |
108 if (isSeeking) { | |
109 if (this.seekedSubscription) { | |
110 return; | |
111 } | |
112 if(this.playingStateSubscription) { | |
113 return; | |
114 } | |
115 | |
116 this.seekedSubscription = this.audioService.seeked$.subscribe(() => { | |
117 if (!this.isPlaying) | |
118 this.animate(); | |
119 }); | |
120 this.playingStateSubscription = | |
121 this.audioService.playingStateChange$.subscribe( | |
122 isPlaying => { | |
123 this.isPlaying = isPlaying; | |
124 if (this.isPlaying) | |
125 this.animate(); | |
126 }); | |
127 } else { | |
128 if (this.isPlaying) { | |
129 this.isPlaying = false; | |
130 } | |
131 if (this.playingStateSubscription) { | |
132 this.playingStateSubscription.unsubscribe(); | |
133 } | |
134 if (this.seekedSubscription) { | |
135 this.seekedSubscription.unsubscribe(); | |
136 } | |
137 } | |
138 } | |
139 | |
140 get isSeeking(): boolean { | |
141 return this._isSeeking; | |
142 } | |
143 | |
40 set audioBuffer(buffer: AudioBuffer) { | 144 set audioBuffer(buffer: AudioBuffer) { |
41 this._audioBuffer = buffer || undefined; | 145 this._audioBuffer = buffer || undefined; |
42 if (this.audioBuffer) { | 146 if (this.audioBuffer) { |
43 this.renderWaveform(this.audioBuffer); | 147 this.renderWaveform(this.audioBuffer); |
44 // this.renderSpectrogram(this.audioBuffer); | 148 // this.renderSpectrogram(this.audioBuffer); |
47 | 151 |
48 get audioBuffer(): AudioBuffer { | 152 get audioBuffer(): AudioBuffer { |
49 return this._audioBuffer; | 153 return this._audioBuffer; |
50 } | 154 } |
51 | 155 |
156 private _audioBuffer: AudioBuffer; | |
157 private _isSubscribedToAudioService: boolean; | |
158 private _isOneShotExtractor: boolean; | |
159 private _isSeeking: boolean; | |
160 private cursorLayer: any; | |
161 private layers: Layer[]; | |
52 private featureExtractionSubscription: Subscription; | 162 private featureExtractionSubscription: Subscription; |
53 private playingStateSubscription: Subscription; | 163 private playingStateSubscription: Subscription; |
54 private seekedSubscription: Subscription; | 164 private seekedSubscription: Subscription; |
165 private onAudioDataSubscription: Subscription; | |
55 private isPlaying: boolean; | 166 private isPlaying: boolean; |
56 private offsetAtPanStart: number; | 167 private offsetAtPanStart: number; |
57 private initialZoom: number; | 168 private initialZoom: number; |
58 private initialDistance: number; | 169 private initialDistance: number; |
59 private zoomOnMouseDown: number; | 170 private zoomOnMouseDown: number; |
60 private offsetOnMouseDown: number; | 171 private offsetOnMouseDown: number; |
172 private hasShot: boolean; | |
173 private isLoading: boolean; | |
61 | 174 |
62 constructor(private audioService: AudioPlayerService, | 175 constructor(private audioService: AudioPlayerService, |
63 private piperService: FeatureExtractionService, | 176 private piperService: FeatureExtractionService, |
64 public ngZone: NgZone) { | 177 public ngZone: NgZone) { |
178 this.isSubscribedToAudioService = true; | |
179 this.isSeeking = true; | |
65 this.layers = []; | 180 this.layers = []; |
66 this._audioBuffer = undefined; | 181 this.audioBuffer = undefined; |
67 this.timeline = undefined; | 182 this.timeline = undefined; |
68 this.cursorLayer = undefined; | 183 this.cursorLayer = undefined; |
69 this.isPlaying = false; | 184 this.isPlaying = false; |
70 const colours = function* () { | 185 this.isLoading = true; |
71 const circularColours = [ | |
72 'black', | |
73 'red', | |
74 'green', | |
75 'purple', | |
76 'orange' | |
77 ]; | |
78 let index = 0; | |
79 const nColours = circularColours.length; | |
80 while (true) { | |
81 yield circularColours[index = ++index % nColours]; | |
82 } | |
83 }(); | |
84 | |
85 this.featureExtractionSubscription = piperService.featuresExtracted$.subscribe( | |
86 features => { | |
87 this.renderFeatures(features, colours.next().value); | |
88 }); | |
89 this.playingStateSubscription = audioService.playingStateChange$.subscribe( | |
90 isPlaying => { | |
91 this.isPlaying = isPlaying; | |
92 if (this.isPlaying) | |
93 this.animate(); | |
94 }); | |
95 this.seekedSubscription = audioService.seeked$.subscribe(() => { | |
96 if (!this.isPlaying) | |
97 this.animate(); | |
98 }); | |
99 } | 186 } |
100 | 187 |
101 ngOnInit() { | 188 ngOnInit() { |
102 } | 189 } |
103 | 190 |
104 ngAfterViewInit(): void { | 191 ngAfterViewInit(): void { |
105 this.trackIdPrefix = this.trackIdPrefix || "default"; | 192 this.trackIdPrefix = this.trackIdPrefix || "default"; |
106 this.renderTimeline(); | 193 if (this.timeline) { |
107 } | 194 this.renderTimeline(null, true, true); |
108 | 195 } else { |
109 renderTimeline(duration: number = 1.0): Timeline { | 196 this.renderTimeline(); |
197 } | |
198 } | |
199 | |
200 renderTimeline(duration: number = 1.0, | |
201 useExistingDuration: boolean = false, | |
202 isInitialRender: boolean = false): Timeline { | |
110 const track: HTMLElement = this.trackDiv.nativeElement; | 203 const track: HTMLElement = this.trackDiv.nativeElement; |
111 track.innerHTML = ""; | 204 track.innerHTML = ""; |
112 const height: number = track.getBoundingClientRect().height; | 205 const height: number = track.getBoundingClientRect().height; |
113 const width: number = track.getBoundingClientRect().width; | 206 const width: number = track.getBoundingClientRect().width; |
114 const pixelsPerSecond = width / duration; | 207 const pixelsPerSecond = width / duration; |
115 if (this.timeline instanceof wavesUI.core.Timeline) { | 208 const hasExistingTimeline = this.timeline instanceof wavesUI.core.Timeline; |
116 this.timeline.pixelsPerSecond = pixelsPerSecond; | 209 |
117 this.timeline.visibleWidth = width; | 210 if (hasExistingTimeline) { |
211 if (!useExistingDuration) { | |
212 this.timeline.pixelsPerSecond = pixelsPerSecond; | |
213 this.timeline.visibleWidth = width; | |
214 } | |
118 } else { | 215 } else { |
119 this.timeline = new wavesUI.core.Timeline(pixelsPerSecond, width); | 216 this.timeline = new wavesUI.core.Timeline(pixelsPerSecond, width); |
120 } | 217 } |
121 this.timeline.createTrack(track, height, `wave-${this.trackIdPrefix}`); | 218 const waveTrack = this.timeline.createTrack( |
219 track, | |
220 height, | |
221 `wave-${this.trackIdPrefix}` | |
222 ); | |
223 if (isInitialRender && hasExistingTimeline) { | |
224 // time axis | |
225 const timeAxis = new wavesUI.helpers.TimeAxisLayer({ | |
226 height: height, | |
227 color: '#b0b0b0' | |
228 }); | |
229 this.addLayer(timeAxis, waveTrack, this.timeline.timeContext, true); | |
230 this.cursorLayer = new wavesUI.helpers.CursorLayer({ | |
231 height: height | |
232 }); | |
233 this.addLayer(this.cursorLayer, waveTrack, this.timeline.timeContext); | |
234 } | |
235 if ('ontouchstart' in window) { | |
236 interface Point { | |
237 x: number; | |
238 y: number; | |
239 } | |
240 | |
241 let zoomGestureJustEnded: boolean = false; | |
242 | |
243 const pixelToExponent: Function = wavesUI.utils.scales.linear() | |
244 .domain([0, 100]) // 100px => factor 2 | |
245 .range([0, 1]); | |
246 | |
247 const calculateDistance: (p1: Point, p2: Point) => number = (p1, p2) => { | |
248 return Math.pow( | |
249 Math.pow(p2.x - p1.x, 2) + | |
250 Math.pow(p2.y - p1.y, 2), 0.5); | |
251 }; | |
252 | |
253 const hammertime = new Hammer(this.trackDiv.nativeElement); | |
254 const scroll = (ev) => { | |
255 if (zoomGestureJustEnded) { | |
256 zoomGestureJustEnded = false; | |
257 console.log("Skip this event: likely a single touch dangling from pinch"); | |
258 return; | |
259 } | |
260 this.timeline.timeContext.offset = this.offsetAtPanStart + | |
261 this.timeline.timeContext.timeToPixel.invert(ev.deltaX); | |
262 this.timeline.tracks.update(); | |
263 }; | |
264 | |
265 const zoom = (ev) => { | |
266 const minZoom = this.timeline.state.minZoom; | |
267 const maxZoom = this.timeline.state.maxZoom; | |
268 const distance = calculateDistance({ | |
269 x: ev.pointers[0].clientX, | |
270 y: ev.pointers[0].clientY | |
271 }, { | |
272 x: ev.pointers[1].clientX, | |
273 y: ev.pointers[1].clientY | |
274 }); | |
275 | |
276 const lastCenterTime = | |
277 this.timeline.timeContext.timeToPixel.invert(ev.center.x); | |
278 | |
279 const exponent = pixelToExponent(distance - this.initialDistance); | |
280 const targetZoom = this.initialZoom * Math.pow(2, exponent); | |
281 | |
282 this.timeline.timeContext.zoom = | |
283 Math.min(Math.max(targetZoom, minZoom), maxZoom); | |
284 | |
285 const newCenterTime = | |
286 this.timeline.timeContext.timeToPixel.invert(ev.center.x); | |
287 | |
288 this.timeline.timeContext.offset += newCenterTime - lastCenterTime; | |
289 this.timeline.tracks.update(); | |
290 }; | |
291 hammertime.get('pinch').set({ enable: true }); | |
292 hammertime.on('panstart', () => { | |
293 this.offsetAtPanStart = this.timeline.timeContext.offset; | |
294 }); | |
295 hammertime.on('panleft', scroll); | |
296 hammertime.on('panright', scroll); | |
297 hammertime.on('pinchstart', (e) => { | |
298 this.initialZoom = this.timeline.timeContext.zoom; | |
299 | |
300 this.initialDistance = calculateDistance({ | |
301 x: e.pointers[0].clientX, | |
302 y: e.pointers[0].clientY | |
303 }, { | |
304 x: e.pointers[1].clientX, | |
305 y: e.pointers[1].clientY | |
306 }); | |
307 }); | |
308 hammertime.on('pinch', zoom); | |
309 hammertime.on('pinchend', () => { | |
310 zoomGestureJustEnded = true; | |
311 }); | |
312 } | |
122 // this.timeline.createTrack(track, height/2, `wave-${this.trackIdPrefix}`); | 313 // this.timeline.createTrack(track, height/2, `wave-${this.trackIdPrefix}`); |
123 // this.timeline.createTrack(track, height/2, `grid-${this.trackIdPrefix}`); | 314 // this.timeline.createTrack(track, height/2, `grid-${this.trackIdPrefix}`); |
124 } | 315 } |
125 | 316 |
126 estimatePercentile(matrix, percentile) { | 317 estimatePercentile(matrix, percentile) { |
306 this.addLayer(this.cursorLayer, waveTrack, this.timeline.timeContext); | 497 this.addLayer(this.cursorLayer, waveTrack, this.timeline.timeContext); |
307 this.timeline.state = new wavesUI.states.CenteredZoomState(this.timeline); | 498 this.timeline.state = new wavesUI.states.CenteredZoomState(this.timeline); |
308 waveTrack.render(); | 499 waveTrack.render(); |
309 waveTrack.update(); | 500 waveTrack.update(); |
310 | 501 |
311 | 502 this.isLoading = false; |
312 if ('ontouchstart' in window) { | |
313 interface Point { | |
314 x: number; | |
315 y: number; | |
316 } | |
317 | |
318 let zoomGestureJustEnded: boolean = false; | |
319 | |
320 const pixelToExponent: Function = wavesUI.utils.scales.linear() | |
321 .domain([0, 100]) // 100px => factor 2 | |
322 .range([0, 1]); | |
323 | |
324 const calculateDistance: (p1: Point, p2: Point) => number = (p1, p2) => { | |
325 return Math.pow( | |
326 Math.pow(p2.x - p1.x, 2) + | |
327 Math.pow(p2.y - p1.y, 2), 0.5); | |
328 }; | |
329 | |
330 const hammertime = new Hammer(this.trackDiv.nativeElement); | |
331 const scroll = (ev) => { | |
332 if (zoomGestureJustEnded) { | |
333 zoomGestureJustEnded = false; | |
334 console.log("Skip this event: likely a single touch dangling from pinch"); | |
335 return; | |
336 } | |
337 this.timeline.timeContext.offset = this.offsetAtPanStart + | |
338 this.timeline.timeContext.timeToPixel.invert(ev.deltaX); | |
339 this.timeline.tracks.update(); | |
340 }; | |
341 | |
342 const zoom = (ev) => { | |
343 const minZoom = this.timeline.state.minZoom; | |
344 const maxZoom = this.timeline.state.maxZoom; | |
345 const distance = calculateDistance({ | |
346 x: ev.pointers[0].clientX, | |
347 y: ev.pointers[0].clientY | |
348 }, { | |
349 x: ev.pointers[1].clientX, | |
350 y: ev.pointers[1].clientY | |
351 }); | |
352 | |
353 const lastCenterTime = | |
354 this.timeline.timeContext.timeToPixel.invert(ev.center.x); | |
355 | |
356 const exponent = pixelToExponent(distance - this.initialDistance); | |
357 const targetZoom = this.initialZoom * Math.pow(2, exponent); | |
358 | |
359 this.timeline.timeContext.zoom = | |
360 Math.min(Math.max(targetZoom, minZoom), maxZoom); | |
361 | |
362 const newCenterTime = | |
363 this.timeline.timeContext.timeToPixel.invert(ev.center.x); | |
364 | |
365 this.timeline.timeContext.offset += newCenterTime - lastCenterTime; | |
366 this.timeline.tracks.update(); | |
367 }; | |
368 hammertime.get('pinch').set({ enable: true }); | |
369 hammertime.on('panstart', () => { | |
370 this.offsetAtPanStart = this.timeline.timeContext.offset; | |
371 }); | |
372 hammertime.on('panleft', scroll); | |
373 hammertime.on('panright', scroll); | |
374 hammertime.on('pinchstart', (e) => { | |
375 this.initialZoom = this.timeline.timeContext.zoom; | |
376 | |
377 this.initialDistance = calculateDistance({ | |
378 x: e.pointers[0].clientX, | |
379 y: e.pointers[0].clientY | |
380 }, { | |
381 x: e.pointers[1].clientX, | |
382 y: e.pointers[1].clientY | |
383 }); | |
384 }); | |
385 hammertime.on('pinch', zoom); | |
386 hammertime.on('pinchend', () => { | |
387 zoomGestureJustEnded = true; | |
388 }); | |
389 } | |
390 | |
391 this.animate(); | 503 this.animate(); |
392 } | 504 } |
393 | 505 |
394 renderSpectrogram(buffer: AudioBuffer): void { | 506 renderSpectrogram(buffer: AudioBuffer): void { |
395 const height: number = this.trackDiv.nativeElement.getBoundingClientRect().height / 2; | 507 const height: number = this.trackDiv.nativeElement.getBoundingClientRect().height / 2; |
408 this.timeline.tracks.update(); | 520 this.timeline.tracks.update(); |
409 } | 521 } |
410 | 522 |
411 // TODO refactor - this doesn't belong here | 523 // TODO refactor - this doesn't belong here |
412 private renderFeatures(extracted: SimpleResponse, colour: Colour): void { | 524 private renderFeatures(extracted: SimpleResponse, colour: Colour): void { |
525 if (this.isOneShotExtractor && !this.hasShot) { | |
526 this.featureExtractionSubscription.unsubscribe(); | |
527 this.hasShot = true; | |
528 } | |
529 | |
413 if (!extracted.hasOwnProperty('features') || !extracted.hasOwnProperty('outputDescriptor')) return; | 530 if (!extracted.hasOwnProperty('features') || !extracted.hasOwnProperty('outputDescriptor')) return; |
414 if (!extracted.features.hasOwnProperty('shape') || !extracted.features.hasOwnProperty('data')) return; | 531 if (!extracted.features.hasOwnProperty('shape') || !extracted.features.hasOwnProperty('data')) return; |
415 const features: FeatureCollection = (extracted.features as FeatureCollection); | 532 const features: FeatureCollection = (extracted.features as FeatureCollection); |
416 const outputDescriptor = extracted.outputDescriptor; | 533 const outputDescriptor = extracted.outputDescriptor; |
417 const height = this.trackDiv.nativeElement.getBoundingClientRect().height / 2; | 534 // const height = this.trackDiv.nativeElement.getBoundingClientRect().height / 2; |
535 const height = this.trackDiv.nativeElement.getBoundingClientRect().height; | |
418 const waveTrack = this.timeline.getTrackById(`wave-${this.trackIdPrefix}`); | 536 const waveTrack = this.timeline.getTrackById(`wave-${this.trackIdPrefix}`); |
419 | 537 |
420 // TODO refactor all of this | 538 // TODO refactor all of this |
421 switch (features.shape) { | 539 switch (features.shape) { |
422 case 'vector': { | 540 case 'vector': { |
582 default: | 700 default: |
583 console.log("Cannot render an appropriate layer for feature shape '" + | 701 console.log("Cannot render an appropriate layer for feature shape '" + |
584 features.shape + "'"); | 702 features.shape + "'"); |
585 } | 703 } |
586 | 704 |
705 this.isLoading = false; | |
587 this.timeline.tracks.update(); | 706 this.timeline.tracks.update(); |
588 } | 707 } |
589 | 708 |
590 private animate(): void { | 709 private animate(): void { |
710 if (!this.isSeeking) return; | |
711 | |
591 this.ngZone.runOutsideAngular(() => { | 712 this.ngZone.runOutsideAngular(() => { |
592 // listen for time passing... | 713 // listen for time passing... |
593 const updateSeekingCursor = () => { | 714 const updateSeekingCursor = () => { |
594 const currentTime = this.audioService.getCurrentTime(); | 715 const currentTime = this.audioService.getCurrentTime(); |
595 this.cursorLayer.currentPosition = currentTime; | 716 this.cursorLayer.currentPosition = currentTime; |
656 layer.render(); | 777 layer.render(); |
657 layer.update(); | 778 layer.update(); |
658 } | 779 } |
659 | 780 |
660 ngOnDestroy(): void { | 781 ngOnDestroy(): void { |
661 this.featureExtractionSubscription.unsubscribe(); | 782 if (this.featureExtractionSubscription) |
662 this.playingStateSubscription.unsubscribe(); | 783 this.featureExtractionSubscription.unsubscribe(); |
663 this.seekedSubscription.unsubscribe(); | 784 if (this.playingStateSubscription) |
785 this.playingStateSubscription.unsubscribe(); | |
786 if (this.seekedSubscription) | |
787 this.seekedSubscription.unsubscribe(); | |
788 if (this.onAudioDataSubscription) | |
789 this.onAudioDataSubscription.unsubscribe(); | |
664 } | 790 } |
665 | 791 |
666 seekStart(): void { | 792 seekStart(): void { |
667 this.zoomOnMouseDown = this.timeline.timeContext.zoom; | 793 this.zoomOnMouseDown = this.timeline.timeContext.zoom; |
668 this.offsetOnMouseDown = this.timeline.timeContext.offset; | 794 this.offsetOnMouseDown = this.timeline.timeContext.offset; |
679 } | 805 } |
680 | 806 |
681 seek(x: number): void { | 807 seek(x: number): void { |
682 if (this.timeline) { | 808 if (this.timeline) { |
683 const timeContext: any = this.timeline.timeContext; | 809 const timeContext: any = this.timeline.timeContext; |
684 this.audioService.seekTo( | 810 if (this.isSeeking) { |
685 timeContext.timeToPixel.invert(x)- timeContext.offset | 811 this.audioService.seekTo( |
686 ); | 812 timeContext.timeToPixel.invert(x)- timeContext.offset |
813 ); | |
814 } | |
687 } | 815 } |
688 } | 816 } |
689 } | 817 } |