dev@10
|
1 import {
|
dev@51
|
2 Component, OnInit, ViewChild, ElementRef, Input, AfterViewInit, NgZone,
|
dev@51
|
3 OnDestroy
|
dev@10
|
4 } from '@angular/core';
|
dev@39
|
5 import {AudioPlayerService} from "../services/audio-player/audio-player.service";
|
dev@36
|
6 import wavesUI from 'waves-ui';
|
dev@51
|
7 import {FeatureList} from "piper/Feature";
|
dev@51
|
8 import {FeatureExtractionService} from "../services/feature-extraction/feature-extraction.service";
|
dev@51
|
9 import {Subscription} from "rxjs";
|
dev@53
|
10 import {toSeconds} from "piper";
|
dev@8
|
11
|
dev@20
|
12 type Timeline = any; // TODO what type actually is it.. start a .d.ts for waves-ui?
|
dev@54
|
13 type Layer = any;
|
dev@54
|
14 type Track = any;
|
dev@6
|
15
|
dev@6
|
16 @Component({
|
dev@6
|
17 selector: 'app-waveform',
|
dev@6
|
18 templateUrl: './waveform.component.html',
|
dev@6
|
19 styleUrls: ['./waveform.component.css']
|
dev@6
|
20 })
|
dev@51
|
21 export class WaveformComponent implements OnInit, AfterViewInit, OnDestroy {
|
dev@20
|
22
|
dev@8
|
23 @ViewChild('track') trackDiv: ElementRef;
|
dev@6
|
24
|
dev@54
|
25 private _audioBuffer: AudioBuffer;
|
dev@54
|
26 private timeline: Timeline;
|
dev@54
|
27 private cursorLayer: any;
|
dev@54
|
28 private disposableLayers: Layer[];
|
dev@16
|
29
|
dev@16
|
30 @Input()
|
dev@16
|
31 set audioBuffer(buffer: AudioBuffer) {
|
dev@16
|
32 this._audioBuffer = buffer || undefined;
|
dev@20
|
33 if (this.audioBuffer)
|
dev@20
|
34 this.renderWaveform(this.audioBuffer);
|
dev@16
|
35 }
|
dev@16
|
36
|
dev@16
|
37 get audioBuffer(): AudioBuffer {
|
dev@16
|
38 return this._audioBuffer;
|
dev@16
|
39 }
|
dev@16
|
40
|
dev@51
|
41 private featureExtractionSubscription: Subscription;
|
dev@53
|
42 private playingStateSubscription: Subscription;
|
dev@53
|
43 private seekedSubscription: Subscription;
|
dev@53
|
44 private isPlaying: boolean;
|
dev@51
|
45
|
dev@31
|
46 constructor(private audioService: AudioPlayerService,
|
dev@51
|
47 private piperService: FeatureExtractionService,
|
dev@51
|
48 public ngZone: NgZone) {
|
dev@54
|
49 this.disposableLayers = [];
|
dev@54
|
50 this._audioBuffer = undefined;
|
dev@54
|
51 this.timeline = undefined;
|
dev@54
|
52 this.cursorLayer = undefined;
|
dev@53
|
53 this.isPlaying = false;
|
dev@51
|
54 this.featureExtractionSubscription = piperService.featuresExtracted$.subscribe(
|
dev@51
|
55 features => {
|
dev@51
|
56 this.renderFeatures(features);
|
dev@51
|
57 });
|
dev@53
|
58 this.playingStateSubscription = audioService.playingStateChange$.subscribe(
|
dev@53
|
59 isPlaying => {
|
dev@53
|
60 this.isPlaying = isPlaying;
|
dev@53
|
61 if (this.isPlaying)
|
dev@53
|
62 this.animate();
|
dev@53
|
63 });
|
dev@53
|
64 this.seekedSubscription = audioService.seeked$.subscribe(() => {
|
dev@53
|
65 if (!this.isPlaying)
|
dev@53
|
66 this.animate();
|
dev@53
|
67 });
|
dev@51
|
68 }
|
dev@51
|
69
|
dev@53
|
70 ngOnInit() {
|
dev@53
|
71 }
|
dev@10
|
72
|
dev@10
|
73 ngAfterViewInit(): void {
|
dev@51
|
74 this.timeline = this.renderTimeline();
|
dev@20
|
75 }
|
dev@20
|
76
|
dev@20
|
77 renderTimeline(duration: number = 1.0): Timeline {
|
dev@18
|
78 const track: HTMLElement = this.trackDiv.nativeElement;
|
dev@20
|
79 track.innerHTML = "";
|
dev@18
|
80 const height: number = track.getBoundingClientRect().height;
|
dev@18
|
81 const width: number = track.getBoundingClientRect().width;
|
dev@18
|
82 const pixelsPerSecond = width / duration;
|
dev@18
|
83 const timeline = new wavesUI.core.Timeline(pixelsPerSecond, width);
|
dev@33
|
84 timeline.timeContext.offset = 0.5 * timeline.timeContext.visibleDuration;
|
dev@18
|
85 timeline.createTrack(track, height, 'main');
|
dev@54
|
86 return timeline;
|
dev@54
|
87 }
|
dev@18
|
88
|
dev@54
|
89 renderWaveform(buffer: AudioBuffer): void {
|
dev@54
|
90 const height: number = this.trackDiv.nativeElement.getBoundingClientRect().height;
|
dev@54
|
91 const mainTrack = this.timeline.getTrackById('main');
|
dev@54
|
92 if (this.timeline) {
|
dev@54
|
93 // resize
|
dev@54
|
94 const width = this.trackDiv.nativeElement.getBoundingClientRect().width;
|
dev@54
|
95 // loop through layers and remove them, waves-ui provides methods for this but it seems to not work properly
|
dev@54
|
96 for (let i = 0; i < this.disposableLayers.length; ++i) {
|
dev@54
|
97 let layer = this.disposableLayers.pop();
|
dev@54
|
98 mainTrack.remove(layer);
|
dev@54
|
99 layer.destroy();
|
dev@54
|
100 }
|
dev@54
|
101 this.timeline.visibleWidth = width;
|
dev@54
|
102 this.timeline.pixelsPerSecond = width / buffer.duration;
|
dev@54
|
103 mainTrack.height = height;
|
dev@54
|
104 } else {
|
dev@54
|
105 this.timeline = this.renderTimeline(buffer.duration)
|
dev@54
|
106 }
|
dev@18
|
107 // time axis
|
dev@18
|
108 const timeAxis = new wavesUI.helpers.TimeAxisLayer({
|
dev@18
|
109 height: height,
|
dev@18
|
110 color: 'gray'
|
dev@18
|
111 });
|
dev@54
|
112 this.addLayer(timeAxis, mainTrack, this.timeline.timeContext, true);
|
dev@18
|
113
|
dev@20
|
114 const waveformLayer = new wavesUI.helpers.WaveformLayer(buffer, {
|
dev@10
|
115 top: 10,
|
dev@20
|
116 height: height * 0.9,
|
dev@16
|
117 color: 'darkblue'
|
dev@16
|
118 });
|
dev@54
|
119 this.addLayer(waveformLayer, mainTrack, this.timeline.timeContext);
|
dev@31
|
120
|
dev@53
|
121 this.cursorLayer = new wavesUI.helpers.CursorLayer({
|
dev@31
|
122 height: height
|
dev@31
|
123 });
|
dev@54
|
124 this.addLayer(this.cursorLayer, mainTrack, this.timeline.timeContext);
|
dev@51
|
125 this.timeline.state = new wavesUI.states.CenteredZoomState(this.timeline);
|
dev@54
|
126 mainTrack.render();
|
dev@54
|
127 mainTrack.update();
|
dev@53
|
128 this.animate();
|
dev@53
|
129 }
|
dev@53
|
130
|
dev@53
|
131 // TODO refactor - this doesn't belong here
|
dev@53
|
132 private renderFeatures(features: FeatureList): void {
|
dev@53
|
133 const plotData = features.map(feature => {
|
dev@53
|
134 return {
|
dev@53
|
135 cx: toSeconds(feature.timestamp),
|
dev@53
|
136 cy: feature.featureValues[0]
|
dev@53
|
137 };
|
dev@53
|
138 });
|
dev@54
|
139 this.addLayer(
|
dev@53
|
140 new wavesUI.helpers.BreakpointLayer(plotData, {color: 'green'}),
|
dev@54
|
141 this.timeline.getTrackById('main'),
|
dev@54
|
142 this.timeline.timeContext
|
dev@53
|
143 );
|
dev@53
|
144 }
|
dev@53
|
145
|
dev@53
|
146 private animate(): void {
|
dev@31
|
147 this.ngZone.runOutsideAngular(() => {
|
dev@31
|
148 // listen for time passing...
|
dev@31
|
149 const updateSeekingCursor = () => {
|
dev@53
|
150 const currentTime = this.audioService.getCurrentTime();
|
dev@53
|
151 this.cursorLayer.currentPosition = currentTime;
|
dev@53
|
152 this.cursorLayer.update();
|
dev@53
|
153
|
dev@53
|
154 const currentOffset = this.timeline.timeContext.offset;
|
dev@53
|
155 const offsetTimestamp = currentOffset
|
dev@53
|
156 + currentTime;
|
dev@53
|
157
|
dev@53
|
158 const visibleDuration = this.timeline.timeContext.visibleDuration;
|
dev@53
|
159 // TODO reduce duplication between directions and make more declarative
|
dev@53
|
160 // this kinda logic should also be tested
|
dev@53
|
161 const mustPageForward = offsetTimestamp > visibleDuration;
|
dev@53
|
162 const mustPageBackward = currentTime < -currentOffset;
|
dev@53
|
163
|
dev@53
|
164 if (mustPageForward) {
|
dev@53
|
165 const hasSkippedMultiplePages = offsetTimestamp - visibleDuration > visibleDuration;
|
dev@53
|
166
|
dev@53
|
167 this.timeline.timeContext.offset = hasSkippedMultiplePages
|
dev@53
|
168 ? -currentTime + 0.5 * visibleDuration
|
dev@53
|
169 : currentOffset - visibleDuration;
|
dev@51
|
170 this.timeline.tracks.update();
|
dev@34
|
171 }
|
dev@53
|
172
|
dev@53
|
173 if (mustPageBackward) {
|
dev@53
|
174 const hasSkippedMultiplePages = currentTime + visibleDuration < -currentOffset;
|
dev@53
|
175 this.timeline.timeContext.offset = hasSkippedMultiplePages
|
dev@53
|
176 ? -currentTime + 0.5 * visibleDuration
|
dev@53
|
177 : currentOffset + visibleDuration;
|
dev@51
|
178 this.timeline.tracks.update();
|
dev@34
|
179 }
|
dev@53
|
180
|
dev@53
|
181 if (this.isPlaying)
|
dev@53
|
182 requestAnimationFrame(updateSeekingCursor);
|
dev@31
|
183 };
|
dev@31
|
184 updateSeekingCursor();
|
dev@31
|
185 });
|
dev@6
|
186 }
|
dev@16
|
187
|
dev@54
|
188 private addLayer(layer: Layer, track: Track, timeContext: any, isAxis: boolean = false): void {
|
dev@54
|
189 timeContext.zoom = 1.0;
|
dev@54
|
190 if (!layer.timeContext) {
|
dev@54
|
191 layer.setTimeContext(isAxis ?
|
dev@54
|
192 timeContext : new wavesUI.core.LayerTimeContext(timeContext));
|
dev@54
|
193 }
|
dev@54
|
194 this.disposableLayers.push(layer);
|
dev@54
|
195 track.add(layer);
|
dev@54
|
196 layer.render();
|
dev@54
|
197 layer.update();
|
dev@54
|
198 }
|
dev@54
|
199
|
dev@51
|
200 ngOnDestroy(): void {
|
dev@51
|
201 this.featureExtractionSubscription.unsubscribe();
|
dev@53
|
202 this.playingStateSubscription.unsubscribe();
|
dev@53
|
203 this.seekedSubscription.unsubscribe();
|
dev@51
|
204 }
|
dev@6
|
205 }
|