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@63
|
7 import {
|
dev@64
|
8 FeatureExtractionService
|
dev@63
|
9 } from "../services/feature-extraction/feature-extraction.service";
|
dev@51
|
10 import {Subscription} from "rxjs";
|
dev@63
|
11 import {
|
dev@63
|
12 FeatureCollection,
|
dev@64
|
13 FixedSpacedFeatures, SimpleResponse
|
dev@63
|
14 } from "piper/HigherLevelUtilities";
|
dev@53
|
15 import {toSeconds} from "piper";
|
dev@67
|
16 import {FeatureList, Feature} from "piper/Feature";
|
dev@81
|
17 import * as Hammer from 'hammerjs';
|
dev@8
|
18
|
dev@20
|
19 type Timeline = any; // TODO what type actually is it.. start a .d.ts for waves-ui?
|
dev@54
|
20 type Layer = any;
|
dev@54
|
21 type Track = any;
|
dev@59
|
22 type DisposableIndex = number;
|
dev@59
|
23 type Colour = string;
|
dev@6
|
24
|
dev@6
|
25 @Component({
|
dev@6
|
26 selector: 'app-waveform',
|
dev@6
|
27 templateUrl: './waveform.component.html',
|
dev@6
|
28 styleUrls: ['./waveform.component.css']
|
dev@6
|
29 })
|
dev@51
|
30 export class WaveformComponent implements OnInit, AfterViewInit, OnDestroy {
|
dev@20
|
31
|
dev@8
|
32 @ViewChild('track') trackDiv: ElementRef;
|
dev@6
|
33
|
dev@54
|
34 private _audioBuffer: AudioBuffer;
|
dev@54
|
35 private timeline: Timeline;
|
dev@54
|
36 private cursorLayer: any;
|
dev@54
|
37 private disposableLayers: Layer[];
|
dev@59
|
38 private colouredLayers: Map<DisposableIndex, Colour>;
|
dev@16
|
39
|
dev@16
|
40 @Input()
|
dev@16
|
41 set audioBuffer(buffer: AudioBuffer) {
|
dev@16
|
42 this._audioBuffer = buffer || undefined;
|
dev@20
|
43 if (this.audioBuffer)
|
dev@20
|
44 this.renderWaveform(this.audioBuffer);
|
dev@16
|
45 }
|
dev@16
|
46
|
dev@16
|
47 get audioBuffer(): AudioBuffer {
|
dev@16
|
48 return this._audioBuffer;
|
dev@16
|
49 }
|
dev@16
|
50
|
dev@51
|
51 private featureExtractionSubscription: Subscription;
|
dev@53
|
52 private playingStateSubscription: Subscription;
|
dev@53
|
53 private seekedSubscription: Subscription;
|
dev@53
|
54 private isPlaying: boolean;
|
dev@51
|
55
|
dev@31
|
56 constructor(private audioService: AudioPlayerService,
|
dev@51
|
57 private piperService: FeatureExtractionService,
|
dev@51
|
58 public ngZone: NgZone) {
|
dev@59
|
59 this.colouredLayers = new Map();
|
dev@54
|
60 this.disposableLayers = [];
|
dev@54
|
61 this._audioBuffer = undefined;
|
dev@54
|
62 this.timeline = undefined;
|
dev@54
|
63 this.cursorLayer = undefined;
|
dev@53
|
64 this.isPlaying = false;
|
dev@59
|
65 const colours = function* () {
|
dev@59
|
66 const circularColours = [
|
dev@59
|
67 'black',
|
dev@59
|
68 'red',
|
dev@59
|
69 'green',
|
dev@59
|
70 'purple',
|
dev@59
|
71 'orange'
|
dev@59
|
72 ];
|
dev@59
|
73 let index = 0;
|
dev@59
|
74 const nColours = circularColours.length;
|
dev@59
|
75 while (true) {
|
dev@59
|
76 yield circularColours[index = ++index % nColours];
|
dev@59
|
77 }
|
dev@59
|
78 }();
|
dev@59
|
79
|
dev@51
|
80 this.featureExtractionSubscription = piperService.featuresExtracted$.subscribe(
|
dev@51
|
81 features => {
|
dev@59
|
82 this.renderFeatures(features, colours.next().value);
|
dev@51
|
83 });
|
dev@53
|
84 this.playingStateSubscription = audioService.playingStateChange$.subscribe(
|
dev@53
|
85 isPlaying => {
|
dev@53
|
86 this.isPlaying = isPlaying;
|
dev@53
|
87 if (this.isPlaying)
|
dev@53
|
88 this.animate();
|
dev@53
|
89 });
|
dev@53
|
90 this.seekedSubscription = audioService.seeked$.subscribe(() => {
|
dev@53
|
91 if (!this.isPlaying)
|
dev@53
|
92 this.animate();
|
dev@53
|
93 });
|
dev@51
|
94 }
|
dev@51
|
95
|
dev@53
|
96 ngOnInit() {
|
dev@53
|
97 }
|
dev@10
|
98
|
dev@10
|
99 ngAfterViewInit(): void {
|
dev@51
|
100 this.timeline = this.renderTimeline();
|
dev@20
|
101 }
|
dev@20
|
102
|
dev@20
|
103 renderTimeline(duration: number = 1.0): Timeline {
|
dev@18
|
104 const track: HTMLElement = this.trackDiv.nativeElement;
|
dev@20
|
105 track.innerHTML = "";
|
dev@18
|
106 const height: number = track.getBoundingClientRect().height;
|
dev@18
|
107 const width: number = track.getBoundingClientRect().width;
|
dev@18
|
108 const pixelsPerSecond = width / duration;
|
dev@18
|
109 const timeline = new wavesUI.core.Timeline(pixelsPerSecond, width);
|
dev@18
|
110 timeline.createTrack(track, height, 'main');
|
dev@54
|
111 return timeline;
|
dev@54
|
112 }
|
dev@18
|
113
|
cannam@108
|
114 estimatePercentile(matrix, percentile) {
|
cannam@108
|
115 // our sample is not evenly distributed across the whole data set:
|
cannam@108
|
116 // it is guaranteed to include at least one sample from every
|
cannam@108
|
117 // column, and could sample some values more than once. But it
|
cannam@108
|
118 // should be good enough in most cases (todo: show this)
|
cannam@108
|
119 if (matrix.length === 0) return 0.0;
|
cannam@108
|
120 const w = matrix.length;
|
cannam@108
|
121 const h = matrix[0].length;
|
cannam@108
|
122 const n = w * h;
|
cannam@108
|
123 const m = (n > 10000 ? 10000 : n); // should base that on the %ile
|
cannam@108
|
124 let m_per = Math.floor(m / w);
|
cannam@108
|
125 if (m_per < 1) m_per = 1;
|
cannam@108
|
126 let sample = [];
|
cannam@108
|
127 for (let x = 0; x < w; ++x) {
|
cannam@108
|
128 for (let i = 0; i < m_per; ++i) {
|
cannam@108
|
129 const y = Math.floor(Math.random() * h);
|
cannam@108
|
130 sample.push(matrix[x][y]);
|
cannam@108
|
131 }
|
cannam@108
|
132 }
|
cannam@108
|
133 sample.sort((a,b) => { return a - b; });
|
cannam@108
|
134 const ix = Math.floor((sample.length * percentile) / 100);
|
cannam@108
|
135 console.log("Estimating " + percentile + "-%ile of " +
|
cannam@108
|
136 n + "-sample dataset (" + w + " x " + h + ") as value " + ix +
|
cannam@108
|
137 " of sorted " + sample.length + "-sample subset");
|
cannam@108
|
138 const estimate = sample[ix];
|
cannam@108
|
139 console.log("Estimate is: " + estimate + " (where min sampled value = " +
|
cannam@108
|
140 sample[0] + " and max = " + sample[sample.length-1] + ")");
|
cannam@108
|
141 return estimate;
|
cannam@108
|
142 }
|
cannam@108
|
143
|
cannam@108
|
144 interpolatingMapper(hexColours) {
|
cannam@108
|
145 const colours = hexColours.map(n => {
|
cannam@108
|
146 const i = parseInt(n, 16);
|
cannam@108
|
147 return [ (i >> 16) & 255, (i >> 8) & 255, i & 255, 255 ];
|
cannam@108
|
148 });
|
cannam@108
|
149 const last = colours.length - 1;
|
cannam@108
|
150 return (value => {
|
cannam@108
|
151 // value must be in the range [0,1]. We quantize to 256 levels,
|
cannam@108
|
152 // as the PNG encoder deep inside uses a limited palette for
|
cannam@108
|
153 // simplicity. Should document this for the mapper. Also that
|
cannam@108
|
154 // individual colour values should be integers
|
cannam@108
|
155 value = Math.round(value * 255) / 255;
|
cannam@108
|
156 const m = value * last;
|
cannam@108
|
157 if (m >= last) {
|
cannam@108
|
158 return colours[last];
|
cannam@108
|
159 }
|
cannam@108
|
160 if (m <= 0) {
|
cannam@108
|
161 return colours[0];
|
cannam@108
|
162 }
|
cannam@108
|
163 const base = Math.floor(m);
|
cannam@108
|
164 const prop0 = base + 1.0 - m;
|
cannam@108
|
165 const prop1 = m - base;
|
cannam@108
|
166 const c0 = colours[base];
|
cannam@108
|
167 const c1 = colours[base+1];
|
cannam@108
|
168 return [ Math.round(c0[0] * prop0 + c1[0] * prop1),
|
cannam@108
|
169 Math.round(c0[1] * prop0 + c1[1] * prop1),
|
cannam@108
|
170 Math.round(c0[2] * prop0 + c1[2] * prop1),
|
cannam@108
|
171 255 ];
|
cannam@108
|
172 });
|
cannam@108
|
173 }
|
cannam@108
|
174
|
cannam@108
|
175 iceMapper() {
|
cannam@108
|
176 let hexColours = [
|
cannam@108
|
177 // Based on ColorBrewer ylGnBu
|
cannam@108
|
178 "ffffff", "ffff00", "f7fcf0", "e0f3db", "ccebc5", "a8ddb5",
|
cannam@108
|
179 "7bccc4", "4eb3d3", "2b8cbe", "0868ac", "084081", "042040"
|
cannam@108
|
180 ];
|
cannam@108
|
181 hexColours.reverse();
|
cannam@108
|
182 return this.interpolatingMapper(hexColours);
|
cannam@108
|
183 }
|
cannam@108
|
184
|
dev@54
|
185 renderWaveform(buffer: AudioBuffer): void {
|
dev@54
|
186 const height: number = this.trackDiv.nativeElement.getBoundingClientRect().height;
|
dev@54
|
187 const mainTrack = this.timeline.getTrackById('main');
|
dev@54
|
188 if (this.timeline) {
|
dev@54
|
189 // resize
|
dev@54
|
190 const width = this.trackDiv.nativeElement.getBoundingClientRect().width;
|
dev@55
|
191
|
dev@54
|
192 // loop through layers and remove them, waves-ui provides methods for this but it seems to not work properly
|
dev@55
|
193 const timeContextChildren = this.timeline.timeContext._children;
|
dev@55
|
194
|
dev@60
|
195 for (let i = 0, length = this.disposableLayers.length; i < length; ++i) {
|
dev@54
|
196 let layer = this.disposableLayers.pop();
|
dev@54
|
197 mainTrack.remove(layer);
|
dev@55
|
198
|
dev@55
|
199 const index = timeContextChildren.indexOf(layer.timeContext);
|
dev@55
|
200 if (index >= 0)
|
dev@55
|
201 timeContextChildren.splice(index, 1);
|
dev@54
|
202 layer.destroy();
|
dev@54
|
203 }
|
dev@59
|
204 this.colouredLayers.clear();
|
dev@59
|
205
|
dev@54
|
206 this.timeline.visibleWidth = width;
|
dev@54
|
207 this.timeline.pixelsPerSecond = width / buffer.duration;
|
dev@54
|
208 mainTrack.height = height;
|
dev@54
|
209 } else {
|
dev@54
|
210 this.timeline = this.renderTimeline(buffer.duration)
|
dev@54
|
211 }
|
dev@83
|
212 this.timeline.timeContext.offset = 0.5 * this.timeline.timeContext.visibleDuration;
|
cannam@106
|
213
|
dev@18
|
214 // time axis
|
dev@18
|
215 const timeAxis = new wavesUI.helpers.TimeAxisLayer({
|
dev@18
|
216 height: height,
|
cannam@106
|
217 color: '#b0b0b0'
|
dev@18
|
218 });
|
dev@54
|
219 this.addLayer(timeAxis, mainTrack, this.timeline.timeContext, true);
|
dev@18
|
220
|
dev@20
|
221 const waveformLayer = new wavesUI.helpers.WaveformLayer(buffer, {
|
dev@10
|
222 top: 10,
|
dev@20
|
223 height: height * 0.9,
|
dev@16
|
224 color: 'darkblue'
|
dev@16
|
225 });
|
dev@54
|
226 this.addLayer(waveformLayer, mainTrack, this.timeline.timeContext);
|
cannam@106
|
227 /*
|
cannam@106
|
228 const spectrogramLayer = new wavesUI.helpers.SpectrogramLayer(buffer, {
|
cannam@106
|
229 top: 10,
|
cannam@106
|
230 height: height * 0.9,
|
cannam@106
|
231 stepSize: 512,
|
cannam@106
|
232 fftSize: 1024
|
cannam@106
|
233 });
|
cannam@106
|
234 this.addLayer(spectrogramLayer, mainTrack, this.timeline.timeContext);
|
cannam@106
|
235 */
|
dev@53
|
236 this.cursorLayer = new wavesUI.helpers.CursorLayer({
|
dev@31
|
237 height: height
|
dev@31
|
238 });
|
dev@54
|
239 this.addLayer(this.cursorLayer, mainTrack, this.timeline.timeContext);
|
dev@51
|
240 this.timeline.state = new wavesUI.states.CenteredZoomState(this.timeline);
|
dev@54
|
241 mainTrack.render();
|
dev@54
|
242 mainTrack.update();
|
dev@81
|
243
|
dev@81
|
244
|
dev@81
|
245 if ('ontouchstart' in window) {
|
dev@84
|
246 const hammertime = new Hammer(this.trackDiv.nativeElement);
|
dev@81
|
247 const scroll = (ev) => {
|
dev@81
|
248 const sign = ev.direction === Hammer.DIRECTION_LEFT ? -1 : 1;
|
dev@81
|
249 let delta = this.timeline.timeContext.timeToPixel.invert(sign * ev.distance);
|
dev@96
|
250 const speed: number = Math.abs(ev.velocityX);
|
dev@96
|
251 delta *= (speed > 0.075 ? 0.075 : speed); // this is completely made up to limit the max speed, TODO something sensible
|
dev@81
|
252 this.timeline.timeContext.offset += delta;
|
dev@81
|
253 this.timeline.tracks.update();
|
dev@81
|
254 };
|
dev@84
|
255
|
dev@81
|
256 const zoom = (ev) => {
|
dev@81
|
257 const minZoom = this.timeline.state.minZoom;
|
dev@81
|
258 const maxZoom = this.timeline.state.maxZoom;
|
dev@81
|
259 const initialZoom = this.timeline.timeContext.zoom;
|
dev@81
|
260 const targetZoom = initialZoom * ev.scale;
|
dev@96
|
261 const lastCenterTime = this.timeline.timeContext.timeToPixel.invert(ev.center.x);
|
dev@81
|
262 this.timeline.timeContext.zoom = Math.min(Math.max(targetZoom, minZoom), maxZoom);
|
dev@96
|
263 const newCenterTime = this.timeline.timeContext.timeToPixel.invert(ev.center.x);
|
dev@96
|
264 this.timeline.timeContext.offset += newCenterTime - lastCenterTime;
|
dev@81
|
265 this.timeline.tracks.update();
|
dev@81
|
266 };
|
dev@84
|
267 const seek = (ev) => {
|
dev@84
|
268 this.audioService.seekTo(
|
dev@84
|
269 this.timeline.timeContext.timeToPixel.invert(ev.center.x) - this.timeline.timeContext.offset
|
dev@84
|
270 );
|
dev@84
|
271 };
|
dev@81
|
272 hammertime.get('pinch').set({ enable: true });
|
dev@81
|
273 hammertime.on('panleft', scroll);
|
dev@81
|
274 hammertime.on('panright', scroll);
|
dev@81
|
275 hammertime.on('pinch', zoom);
|
dev@84
|
276 hammertime.on('tap', seek);
|
dev@81
|
277 }
|
dev@81
|
278
|
dev@53
|
279 this.animate();
|
dev@53
|
280 }
|
dev@53
|
281
|
dev@53
|
282 // TODO refactor - this doesn't belong here
|
dev@64
|
283 private renderFeatures(extracted: SimpleResponse, colour: Colour): void {
|
dev@64
|
284 if (!extracted.hasOwnProperty('features') || !extracted.hasOwnProperty('outputDescriptor')) return;
|
dev@64
|
285 if (!extracted.features.hasOwnProperty('shape') || !extracted.features.hasOwnProperty('data')) return;
|
dev@64
|
286 const features: FeatureCollection = (extracted.features as FeatureCollection);
|
dev@64
|
287 const outputDescriptor = extracted.outputDescriptor;
|
dev@64
|
288 const height = this.trackDiv.nativeElement.getBoundingClientRect().height;
|
dev@64
|
289 const mainTrack = this.timeline.getTrackById('main');
|
dev@64
|
290
|
dev@64
|
291 // TODO refactor all of this
|
dev@63
|
292 switch (features.shape) {
|
dev@64
|
293 case 'vector': {
|
dev@63
|
294 const stepDuration = (features as FixedSpacedFeatures).stepDuration;
|
dev@63
|
295 const featureData = (features.data as Float32Array);
|
dev@68
|
296 if (featureData.length === 0) return;
|
dev@63
|
297 const normalisationFactor = 1.0 /
|
dev@63
|
298 featureData.reduce(
|
dev@63
|
299 (currentMax, feature) => Math.max(currentMax, feature),
|
dev@63
|
300 -Infinity
|
dev@63
|
301 );
|
dev@67
|
302
|
dev@63
|
303 const plotData = [...featureData].map((feature, i) => {
|
dev@63
|
304 return {
|
dev@63
|
305 cx: i * stepDuration,
|
dev@63
|
306 cy: feature * normalisationFactor
|
dev@63
|
307 };
|
dev@63
|
308 });
|
dev@67
|
309
|
dev@105
|
310 let lineLayer = new wavesUI.helpers.LineLayer(plotData, {
|
dev@63
|
311 color: colour,
|
dev@64
|
312 height: height
|
dev@63
|
313 });
|
dev@63
|
314 this.colouredLayers.set(this.addLayer(
|
dev@105
|
315 lineLayer,
|
dev@64
|
316 mainTrack,
|
dev@63
|
317 this.timeline.timeContext
|
dev@63
|
318 ), colour);
|
dev@63
|
319 break;
|
dev@64
|
320 }
|
dev@64
|
321 case 'list': {
|
dev@64
|
322 const featureData = (features.data as FeatureList);
|
dev@68
|
323 if (featureData.length === 0) return;
|
dev@64
|
324 // TODO look at output descriptor instead of directly inspecting features
|
dev@64
|
325 const hasDuration = outputDescriptor.configured.hasDuration;
|
dev@64
|
326 const isMarker = !hasDuration
|
dev@64
|
327 && outputDescriptor.configured.binCount === 0
|
dev@64
|
328 && featureData[0].featureValues == null;
|
dev@64
|
329 const isRegion = hasDuration
|
dev@64
|
330 && featureData[0].timestamp != null;
|
dev@64
|
331 // TODO refactor, this is incomprehensible
|
dev@64
|
332 if (isMarker) {
|
dev@64
|
333 const plotData = featureData.map(feature => {
|
dev@64
|
334 return {x: toSeconds(feature.timestamp)}
|
dev@64
|
335 });
|
dev@64
|
336 let markerLayer = new wavesUI.helpers.MarkerLayer(plotData, {
|
dev@64
|
337 height: height,
|
dev@64
|
338 color: colour,
|
dev@64
|
339 });
|
dev@64
|
340 this.colouredLayers.set(this.addLayer(
|
dev@64
|
341 markerLayer,
|
dev@64
|
342 mainTrack,
|
dev@64
|
343 this.timeline.timeContext
|
dev@64
|
344 ), colour);
|
dev@64
|
345 } else if (isRegion) {
|
dev@67
|
346 const binCount = outputDescriptor.configured.binCount || 0;
|
dev@67
|
347 const isBarRegion = featureData[0].featureValues.length >= 1 || binCount >= 1 ;
|
dev@64
|
348 const getSegmentArgs = () => {
|
dev@64
|
349 if (isBarRegion) {
|
dev@64
|
350
|
dev@67
|
351 // TODO refactor - this is messy
|
dev@67
|
352 interface FoldsToNumber<T> {
|
dev@67
|
353 reduce(fn: (previousValue: number,
|
dev@67
|
354 currentValue: T,
|
dev@67
|
355 currentIndex: number,
|
dev@67
|
356 array: ArrayLike<T>) => number,
|
dev@67
|
357 initialValue?: number): number;
|
dev@67
|
358 }
|
dev@64
|
359
|
dev@67
|
360 // TODO potentially change impl., i.e avoid reduce
|
dev@67
|
361 const findMin = <T>(arr: FoldsToNumber<T>, getElement: (x: T) => number): number => {
|
dev@67
|
362 return arr.reduce((min, val) => Math.min(min, getElement(val)), Infinity);
|
dev@67
|
363 };
|
dev@67
|
364
|
dev@67
|
365 const findMax = <T>(arr: FoldsToNumber<T>, getElement: (x: T) => number): number => {
|
dev@67
|
366 return arr.reduce((min, val) => Math.max(min, getElement(val)), -Infinity);
|
dev@67
|
367 };
|
dev@67
|
368
|
dev@67
|
369 const min = findMin<Feature>(featureData, (x: Feature) => {
|
dev@67
|
370 return findMin<number>(x.featureValues, y => y);
|
dev@67
|
371 });
|
dev@67
|
372
|
dev@67
|
373 const max = findMax<Feature>(featureData, (x: Feature) => {
|
dev@67
|
374 return findMax<number>(x.featureValues, y => y);
|
dev@67
|
375 });
|
dev@67
|
376
|
dev@67
|
377 const barHeight = 1.0 / height;
|
dev@64
|
378 return [
|
dev@67
|
379 featureData.reduce((bars, feature) => {
|
dev@67
|
380 const staticProperties = {
|
dev@64
|
381 x: toSeconds(feature.timestamp),
|
dev@64
|
382 width: toSeconds(feature.duration),
|
dev@67
|
383 height: min + barHeight,
|
dev@64
|
384 color: colour,
|
dev@64
|
385 opacity: 0.8
|
dev@67
|
386 };
|
dev@67
|
387 // TODO avoid copying Float32Array to an array - map is problematic here
|
dev@67
|
388 return bars.concat([...feature.featureValues]
|
dev@67
|
389 .map(val => Object.assign({}, staticProperties, {y: val})))
|
dev@67
|
390 }, []),
|
dev@67
|
391 {yDomain: [min, max + barHeight], height: height} as any
|
dev@67
|
392 ];
|
dev@64
|
393 } else {
|
dev@64
|
394 return [featureData.map(feature => {
|
dev@64
|
395 return {
|
dev@64
|
396 x: toSeconds(feature.timestamp),
|
dev@64
|
397 width: toSeconds(feature.duration),
|
dev@64
|
398 color: colour,
|
dev@64
|
399 opacity: 0.8
|
dev@64
|
400 }
|
dev@64
|
401 }), {height: height}];
|
dev@64
|
402 }
|
dev@64
|
403 };
|
dev@64
|
404
|
dev@64
|
405 let segmentLayer = new wavesUI.helpers.SegmentLayer(
|
dev@64
|
406 ...getSegmentArgs()
|
dev@64
|
407 );
|
dev@64
|
408 this.colouredLayers.set(this.addLayer(
|
dev@64
|
409 segmentLayer,
|
dev@64
|
410 mainTrack,
|
dev@64
|
411 this.timeline.timeContext
|
dev@64
|
412 ), colour);
|
dev@64
|
413 }
|
dev@64
|
414 break;
|
dev@64
|
415 }
|
cannam@106
|
416 case 'matrix': {
|
cannam@108
|
417 const stepDuration = (features as FixedSpacedFeatures).stepDuration;
|
cannam@108
|
418 const matrixData = (features.data as Float32Array[]);
|
cannam@108
|
419 if (matrixData.length === 0) return;
|
cannam@108
|
420 const targetValue = this.estimatePercentile(matrixData, 97);
|
cannam@108
|
421 const gain = (targetValue > 0.0 ? (1.0 / targetValue) : 1.0);
|
cannam@108
|
422 console.log("setting gain to " + gain);
|
cannam@108
|
423 const matrixEntity = new wavesUI.utils.PrefilledMatrixEntity(matrixData);
|
cannam@108
|
424 let matrixLayer = new wavesUI.helpers.MatrixLayer(matrixEntity, {
|
cannam@108
|
425 gain,
|
cannam@108
|
426 height,
|
cannam@108
|
427 normalise: 'hybrid',
|
cannam@108
|
428 mapper: this.iceMapper()
|
cannam@108
|
429 });
|
cannam@108
|
430 this.colouredLayers.set(this.addLayer(
|
cannam@108
|
431 matrixLayer,
|
cannam@108
|
432 mainTrack,
|
cannam@108
|
433 this.timeline.timeContext
|
cannam@108
|
434 ), colour);
|
cannam@108
|
435 break;
|
cannam@106
|
436 }
|
dev@67
|
437 default:
|
cannam@106
|
438 console.log("Cannot render an appropriate layer for feature shape '" +
|
cannam@106
|
439 features.shape + "'");
|
dev@63
|
440 }
|
dev@59
|
441
|
dev@56
|
442 this.timeline.tracks.update();
|
dev@53
|
443 }
|
dev@53
|
444
|
dev@53
|
445 private animate(): void {
|
dev@31
|
446 this.ngZone.runOutsideAngular(() => {
|
dev@31
|
447 // listen for time passing...
|
dev@31
|
448 const updateSeekingCursor = () => {
|
dev@53
|
449 const currentTime = this.audioService.getCurrentTime();
|
dev@53
|
450 this.cursorLayer.currentPosition = currentTime;
|
dev@53
|
451 this.cursorLayer.update();
|
dev@53
|
452
|
dev@53
|
453 const currentOffset = this.timeline.timeContext.offset;
|
dev@53
|
454 const offsetTimestamp = currentOffset
|
dev@53
|
455 + currentTime;
|
dev@53
|
456
|
dev@53
|
457 const visibleDuration = this.timeline.timeContext.visibleDuration;
|
dev@53
|
458 // TODO reduce duplication between directions and make more declarative
|
dev@53
|
459 // this kinda logic should also be tested
|
dev@53
|
460 const mustPageForward = offsetTimestamp > visibleDuration;
|
dev@53
|
461 const mustPageBackward = currentTime < -currentOffset;
|
dev@53
|
462
|
dev@53
|
463 if (mustPageForward) {
|
dev@53
|
464 const hasSkippedMultiplePages = offsetTimestamp - visibleDuration > visibleDuration;
|
dev@53
|
465
|
cannam@106
|
466 this.timeline.timeContext.offset = hasSkippedMultiplePages ?
|
cannam@106
|
467 -currentTime + 0.5 * visibleDuration :
|
cannam@106
|
468 currentOffset - visibleDuration;
|
dev@51
|
469 this.timeline.tracks.update();
|
dev@34
|
470 }
|
dev@53
|
471
|
dev@53
|
472 if (mustPageBackward) {
|
dev@53
|
473 const hasSkippedMultiplePages = currentTime + visibleDuration < -currentOffset;
|
cannam@106
|
474 this.timeline.timeContext.offset = hasSkippedMultiplePages ?
|
cannam@106
|
475 -currentTime + 0.5 * visibleDuration :
|
cannam@106
|
476 currentOffset + visibleDuration;
|
dev@51
|
477 this.timeline.tracks.update();
|
dev@34
|
478 }
|
dev@53
|
479
|
dev@53
|
480 if (this.isPlaying)
|
dev@53
|
481 requestAnimationFrame(updateSeekingCursor);
|
dev@31
|
482 };
|
dev@31
|
483 updateSeekingCursor();
|
dev@31
|
484 });
|
dev@6
|
485 }
|
dev@16
|
486
|
dev@59
|
487 private addLayer(layer: Layer, track: Track, timeContext: any, isAxis: boolean = false): DisposableIndex {
|
dev@54
|
488 timeContext.zoom = 1.0;
|
dev@54
|
489 if (!layer.timeContext) {
|
dev@54
|
490 layer.setTimeContext(isAxis ?
|
dev@54
|
491 timeContext : new wavesUI.core.LayerTimeContext(timeContext));
|
dev@54
|
492 }
|
dev@54
|
493 track.add(layer);
|
dev@54
|
494 layer.render();
|
dev@54
|
495 layer.update();
|
dev@59
|
496 return this.disposableLayers.push(layer) - 1;
|
dev@59
|
497 }
|
dev@59
|
498
|
dev@59
|
499 private static changeColour(layer: Layer, colour: string): void {
|
dev@59
|
500 const butcherShapes = (shape) => {
|
dev@59
|
501 shape.install({color: () => colour});
|
dev@59
|
502 shape.params.color = colour;
|
dev@59
|
503 shape.update(layer._renderingContext, layer.data);
|
dev@59
|
504 };
|
dev@59
|
505
|
dev@59
|
506 layer._$itemCommonShapeMap.forEach(butcherShapes);
|
dev@59
|
507 layer._$itemShapeMap.forEach(butcherShapes);
|
dev@59
|
508 layer.render();
|
dev@59
|
509 layer.update();
|
dev@54
|
510 }
|
dev@54
|
511
|
dev@51
|
512 ngOnDestroy(): void {
|
dev@51
|
513 this.featureExtractionSubscription.unsubscribe();
|
dev@53
|
514 this.playingStateSubscription.unsubscribe();
|
dev@53
|
515 this.seekedSubscription.unsubscribe();
|
dev@51
|
516 }
|
dev@6
|
517 }
|