annotate src/app/waveform/waveform.component.ts @ 118:5a4cd7add25a

Update colour mappers to slightly changed API; add green & sunset
author Chris Cannam <cannam@all-day-breakfast.com>
date Tue, 14 Mar 2017 11:57:21 +0000
parents b8627a18c72d
children 7170e6ca2206
rev   line source
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;
cannam@117 43 if (this.audioBuffer) {
dev@20 44 this.renderWaveform(this.audioBuffer);
cannam@117 45 this.renderSpectrogram(this.audioBuffer);
cannam@117 46 }
dev@16 47 }
dev@16 48
dev@16 49 get audioBuffer(): AudioBuffer {
dev@16 50 return this._audioBuffer;
dev@16 51 }
dev@16 52
dev@51 53 private featureExtractionSubscription: Subscription;
dev@53 54 private playingStateSubscription: Subscription;
dev@53 55 private seekedSubscription: Subscription;
dev@53 56 private isPlaying: boolean;
dev@110 57 private offsetAtPanStart: number;
dev@110 58 private initialZoom: number;
dev@110 59 private initialDistance: number;
dev@51 60
dev@31 61 constructor(private audioService: AudioPlayerService,
dev@51 62 private piperService: FeatureExtractionService,
dev@51 63 public ngZone: NgZone) {
dev@59 64 this.colouredLayers = new Map();
dev@54 65 this.disposableLayers = [];
dev@54 66 this._audioBuffer = undefined;
dev@54 67 this.timeline = undefined;
dev@54 68 this.cursorLayer = undefined;
dev@53 69 this.isPlaying = false;
dev@59 70 const colours = function* () {
dev@59 71 const circularColours = [
dev@59 72 'black',
dev@59 73 'red',
dev@59 74 'green',
dev@59 75 'purple',
dev@59 76 'orange'
dev@59 77 ];
dev@59 78 let index = 0;
dev@59 79 const nColours = circularColours.length;
dev@59 80 while (true) {
dev@59 81 yield circularColours[index = ++index % nColours];
dev@59 82 }
dev@59 83 }();
dev@59 84
dev@51 85 this.featureExtractionSubscription = piperService.featuresExtracted$.subscribe(
dev@51 86 features => {
dev@59 87 this.renderFeatures(features, colours.next().value);
dev@51 88 });
dev@53 89 this.playingStateSubscription = audioService.playingStateChange$.subscribe(
dev@53 90 isPlaying => {
dev@53 91 this.isPlaying = isPlaying;
dev@53 92 if (this.isPlaying)
dev@53 93 this.animate();
dev@53 94 });
dev@53 95 this.seekedSubscription = audioService.seeked$.subscribe(() => {
dev@53 96 if (!this.isPlaying)
dev@53 97 this.animate();
dev@53 98 });
dev@51 99 }
dev@51 100
dev@53 101 ngOnInit() {
dev@53 102 }
dev@10 103
dev@10 104 ngAfterViewInit(): void {
dev@51 105 this.timeline = this.renderTimeline();
dev@20 106 }
dev@20 107
dev@20 108 renderTimeline(duration: number = 1.0): Timeline {
dev@18 109 const track: HTMLElement = this.trackDiv.nativeElement;
dev@20 110 track.innerHTML = "";
dev@18 111 const height: number = track.getBoundingClientRect().height;
dev@18 112 const width: number = track.getBoundingClientRect().width;
dev@18 113 const pixelsPerSecond = width / duration;
dev@18 114 const timeline = new wavesUI.core.Timeline(pixelsPerSecond, width);
cannam@117 115 timeline.createTrack(track, height/2, 'wave');
cannam@117 116 timeline.createTrack(track, height/2, 'grid');
dev@54 117 return timeline;
dev@54 118 }
dev@18 119
cannam@108 120 estimatePercentile(matrix, percentile) {
cannam@108 121 // our sample is not evenly distributed across the whole data set:
cannam@108 122 // it is guaranteed to include at least one sample from every
cannam@108 123 // column, and could sample some values more than once. But it
cannam@108 124 // should be good enough in most cases (todo: show this)
cannam@109 125 if (matrix.length === 0) {
cannam@109 126 return 0.0;
cannam@109 127 }
cannam@108 128 const w = matrix.length;
cannam@108 129 const h = matrix[0].length;
cannam@108 130 const n = w * h;
cannam@109 131 const m = (n > 50000 ? 50000 : n); // should base that on the %ile
cannam@108 132 let m_per = Math.floor(m / w);
cannam@108 133 if (m_per < 1) m_per = 1;
cannam@108 134 let sample = [];
cannam@108 135 for (let x = 0; x < w; ++x) {
cannam@108 136 for (let i = 0; i < m_per; ++i) {
cannam@108 137 const y = Math.floor(Math.random() * h);
cannam@109 138 const value = matrix[x][y];
cannam@109 139 if (!isNaN(value) && value !== Infinity) {
cannam@109 140 sample.push(value);
cannam@109 141 }
cannam@108 142 }
cannam@108 143 }
cannam@109 144 if (sample.length === 0) {
cannam@109 145 console.log("WARNING: No samples gathered, even though we hoped for " +
cannam@109 146 (m_per * w) + " of them");
cannam@109 147 return 0.0;
cannam@109 148 }
cannam@108 149 sample.sort((a,b) => { return a - b; });
cannam@108 150 const ix = Math.floor((sample.length * percentile) / 100);
cannam@108 151 console.log("Estimating " + percentile + "-%ile of " +
cannam@108 152 n + "-sample dataset (" + w + " x " + h + ") as value " + ix +
cannam@108 153 " of sorted " + sample.length + "-sample subset");
cannam@108 154 const estimate = sample[ix];
cannam@108 155 console.log("Estimate is: " + estimate + " (where min sampled value = " +
cannam@108 156 sample[0] + " and max = " + sample[sample.length-1] + ")");
cannam@108 157 return estimate;
cannam@108 158 }
cannam@108 159
cannam@108 160 interpolatingMapper(hexColours) {
cannam@108 161 const colours = hexColours.map(n => {
cannam@108 162 const i = parseInt(n, 16);
cannam@118 163 return [ ((i >> 16) & 255) / 255.0,
cannam@118 164 ((i >> 8) & 255) / 255.0,
cannam@118 165 ((i) & 255) / 255.0 ];
cannam@108 166 });
cannam@108 167 const last = colours.length - 1;
cannam@108 168 return (value => {
cannam@108 169 const m = value * last;
cannam@108 170 if (m >= last) {
cannam@108 171 return colours[last];
cannam@108 172 }
cannam@108 173 if (m <= 0) {
cannam@108 174 return colours[0];
cannam@108 175 }
cannam@108 176 const base = Math.floor(m);
cannam@108 177 const prop0 = base + 1.0 - m;
cannam@108 178 const prop1 = m - base;
cannam@108 179 const c0 = colours[base];
cannam@108 180 const c1 = colours[base+1];
cannam@118 181 return [ c0[0] * prop0 + c1[0] * prop1,
cannam@118 182 c0[1] * prop0 + c1[1] * prop1,
cannam@118 183 c0[2] * prop0 + c1[2] * prop1 ];
cannam@108 184 });
cannam@108 185 }
dev@110 186
cannam@108 187 iceMapper() {
dev@110 188 let hexColours = [
cannam@108 189 // Based on ColorBrewer ylGnBu
cannam@108 190 "ffffff", "ffff00", "f7fcf0", "e0f3db", "ccebc5", "a8ddb5",
cannam@108 191 "7bccc4", "4eb3d3", "2b8cbe", "0868ac", "084081", "042040"
cannam@108 192 ];
cannam@108 193 hexColours.reverse();
cannam@108 194 return this.interpolatingMapper(hexColours);
cannam@108 195 }
dev@110 196
cannam@118 197 hsv2rgb(h, s, v) { // all values in range [0, 1]
cannam@118 198 const i = Math.floor(h * 6);
cannam@118 199 const f = h * 6 - i;
cannam@118 200 const p = v * (1 - s);
cannam@118 201 const q = v * (1 - f * s);
cannam@118 202 const t = v * (1 - (1 - f) * s);
cannam@118 203 let r = 0, g = 0, b = 0;
cannam@118 204 switch (i % 6) {
cannam@118 205 case 0: r = v, g = t, b = p; break;
cannam@118 206 case 1: r = q, g = v, b = p; break;
cannam@118 207 case 2: r = p, g = v, b = t; break;
cannam@118 208 case 3: r = p, g = q, b = v; break;
cannam@118 209 case 4: r = t, g = p, b = v; break;
cannam@118 210 case 5: r = v, g = p, b = q; break;
cannam@118 211 }
cannam@118 212 return [ r, g, b ];
cannam@118 213 }
cannam@118 214
cannam@118 215 greenMapper() {
cannam@118 216 const blue = 0.6666;
cannam@118 217 const pieslice = 0.3333;
cannam@118 218 return (value => {
cannam@118 219 const h = blue - value * 2.0 * pieslice;
cannam@118 220 const s = 0.5 + value / 2.0;
cannam@118 221 const v = value;
cannam@118 222 return this.hsv2rgb(h, s, v);
cannam@118 223 });
cannam@118 224 }
cannam@118 225
cannam@118 226 sunsetMapper() {
cannam@118 227 return (value => {
cannam@118 228 let r = (value - 0.24) * 2.38;
cannam@118 229 let g = (value - 0.64) * 2.777;
cannam@118 230 let b = (3.6 * value);
cannam@118 231 if (value > 0.277) b = 2.0 - b;
cannam@118 232 return [ r, g, b ];
cannam@118 233 });
cannam@118 234 }
cannam@118 235
dev@54 236 renderWaveform(buffer: AudioBuffer): void {
cannam@117 237 const height: number = this.trackDiv.nativeElement.getBoundingClientRect().height / 2;
cannam@117 238 const waveTrack = this.timeline.getTrackById('wave');
dev@54 239 if (this.timeline) {
dev@54 240 // resize
dev@54 241 const width = this.trackDiv.nativeElement.getBoundingClientRect().width;
dev@55 242
dev@54 243 // loop through layers and remove them, waves-ui provides methods for this but it seems to not work properly
dev@55 244 const timeContextChildren = this.timeline.timeContext._children;
dev@55 245
dev@60 246 for (let i = 0, length = this.disposableLayers.length; i < length; ++i) {
dev@54 247 let layer = this.disposableLayers.pop();
cannam@117 248 // if (waveTrack.hasElement(layer)) {
cannam@117 249 // waveTrack.remove(layer);
cannam@117 250 // }
dev@55 251
dev@55 252 const index = timeContextChildren.indexOf(layer.timeContext);
cannam@117 253 if (index >= 0) {
dev@55 254 timeContextChildren.splice(index, 1);
cannam@117 255 }
dev@54 256 layer.destroy();
dev@54 257 }
dev@59 258 this.colouredLayers.clear();
dev@59 259
dev@54 260 this.timeline.visibleWidth = width;
dev@54 261 this.timeline.pixelsPerSecond = width / buffer.duration;
cannam@117 262 waveTrack.height = height;
dev@54 263 } else {
dev@54 264 this.timeline = this.renderTimeline(buffer.duration)
dev@54 265 }
dev@83 266 this.timeline.timeContext.offset = 0.5 * this.timeline.timeContext.visibleDuration;
cannam@106 267
dev@18 268 // time axis
dev@18 269 const timeAxis = new wavesUI.helpers.TimeAxisLayer({
dev@18 270 height: height,
cannam@106 271 color: '#b0b0b0'
dev@18 272 });
cannam@117 273 this.addLayer(timeAxis, waveTrack, this.timeline.timeContext, true);
dev@18 274
dev@20 275 const waveformLayer = new wavesUI.helpers.WaveformLayer(buffer, {
dev@10 276 top: 10,
dev@20 277 height: height * 0.9,
dev@16 278 color: 'darkblue'
dev@16 279 });
cannam@117 280 this.addLayer(waveformLayer, waveTrack, this.timeline.timeContext);
cannam@117 281
dev@53 282 this.cursorLayer = new wavesUI.helpers.CursorLayer({
dev@31 283 height: height
dev@31 284 });
cannam@117 285 this.addLayer(this.cursorLayer, waveTrack, this.timeline.timeContext);
dev@51 286 this.timeline.state = new wavesUI.states.CenteredZoomState(this.timeline);
cannam@117 287 waveTrack.render();
cannam@117 288 waveTrack.update();
dev@81 289
dev@81 290
dev@81 291 if ('ontouchstart' in window) {
dev@110 292 interface Point {
dev@110 293 x: number;
dev@110 294 y: number;
dev@110 295 }
dev@110 296
dev@113 297 let zoomGestureJustEnded: boolean = false;
dev@113 298
dev@110 299 const pixelToExponent: Function = wavesUI.utils.scales.linear()
dev@110 300 .domain([0, 100]) // 100px => factor 2
dev@110 301 .range([0, 1]);
dev@110 302
dev@110 303 const calculateDistance: (p1: Point, p2: Point) => number = (p1, p2) => {
dev@110 304 return Math.pow(
dev@110 305 Math.pow(p2.x - p1.x, 2) +
dev@110 306 Math.pow(p2.y - p1.y, 2), 0.5);
dev@110 307 };
dev@110 308
dev@84 309 const hammertime = new Hammer(this.trackDiv.nativeElement);
dev@81 310 const scroll = (ev) => {
dev@113 311 if (zoomGestureJustEnded) {
dev@113 312 zoomGestureJustEnded = false;
dev@113 313 console.log("Skip this event: likely a single touch dangling from pinch");
dev@113 314 return;
dev@113 315 }
dev@110 316 this.timeline.timeContext.offset = this.offsetAtPanStart +
dev@110 317 this.timeline.timeContext.timeToPixel.invert(ev.deltaX);
dev@81 318 this.timeline.tracks.update();
dev@81 319 };
dev@84 320
dev@81 321 const zoom = (ev) => {
dev@81 322 const minZoom = this.timeline.state.minZoom;
dev@81 323 const maxZoom = this.timeline.state.maxZoom;
dev@110 324 const distance = calculateDistance({
dev@110 325 x: ev.pointers[0].clientX,
dev@110 326 y: ev.pointers[0].clientY
dev@110 327 }, {
dev@110 328 x: ev.pointers[1].clientX,
dev@110 329 y: ev.pointers[1].clientY
dev@110 330 });
dev@110 331
dev@110 332 const lastCenterTime =
dev@110 333 this.timeline.timeContext.timeToPixel.invert(ev.center.x);
dev@110 334
dev@110 335 const exponent = pixelToExponent(distance - this.initialDistance);
dev@110 336 const targetZoom = this.initialZoom * Math.pow(2, exponent);
dev@110 337
dev@110 338 this.timeline.timeContext.zoom =
dev@110 339 Math.min(Math.max(targetZoom, minZoom), maxZoom);
dev@110 340
dev@110 341 const newCenterTime =
dev@110 342 this.timeline.timeContext.timeToPixel.invert(ev.center.x);
dev@110 343
dev@96 344 this.timeline.timeContext.offset += newCenterTime - lastCenterTime;
dev@81 345 this.timeline.tracks.update();
dev@81 346 };
dev@84 347 const seek = (ev) => {
dev@84 348 this.audioService.seekTo(
dev@84 349 this.timeline.timeContext.timeToPixel.invert(ev.center.x) - this.timeline.timeContext.offset
dev@84 350 );
dev@84 351 };
dev@81 352 hammertime.get('pinch').set({ enable: true });
dev@110 353 hammertime.on('panstart', () => {
dev@110 354 this.offsetAtPanStart = this.timeline.timeContext.offset;
dev@110 355 });
dev@81 356 hammertime.on('panleft', scroll);
dev@81 357 hammertime.on('panright', scroll);
dev@110 358 hammertime.on('pinchstart', (e) => {
dev@110 359 this.initialZoom = this.timeline.timeContext.zoom;
dev@110 360
dev@110 361 this.initialDistance = calculateDistance({
dev@110 362 x: e.pointers[0].clientX,
dev@110 363 y: e.pointers[0].clientY
dev@110 364 }, {
dev@110 365 x: e.pointers[1].clientX,
dev@110 366 y: e.pointers[1].clientY
dev@110 367 });
dev@110 368 });
dev@81 369 hammertime.on('pinch', zoom);
dev@113 370 hammertime.on('pinchend', () => {
dev@113 371 zoomGestureJustEnded = true;
dev@113 372 });
dev@84 373 hammertime.on('tap', seek);
dev@81 374 }
dev@81 375
dev@53 376 this.animate();
dev@53 377 }
dev@53 378
cannam@117 379 renderSpectrogram(buffer: AudioBuffer): void {
cannam@117 380 const height: number = this.trackDiv.nativeElement.getBoundingClientRect().height / 2;
cannam@117 381 const gridTrack = this.timeline.getTrackById('grid');
cannam@117 382
cannam@117 383 const spectrogramLayer = new wavesUI.helpers.SpectrogramLayer(buffer, {
cannam@118 384 top: height * 0.05,
cannam@117 385 height: height * 0.9,
cannam@117 386 stepSize: 512,
cannam@118 387 fftSize: 1024,
cannam@118 388 normalise: 'none',
cannam@118 389 mapper: this.sunsetMapper()
cannam@117 390 });
cannam@117 391 this.addLayer(spectrogramLayer, gridTrack, this.timeline.timeContext);
cannam@117 392
cannam@117 393 this.timeline.tracks.update();
cannam@117 394 }
cannam@117 395
dev@53 396 // TODO refactor - this doesn't belong here
dev@64 397 private renderFeatures(extracted: SimpleResponse, colour: Colour): void {
dev@64 398 if (!extracted.hasOwnProperty('features') || !extracted.hasOwnProperty('outputDescriptor')) return;
dev@64 399 if (!extracted.features.hasOwnProperty('shape') || !extracted.features.hasOwnProperty('data')) return;
dev@64 400 const features: FeatureCollection = (extracted.features as FeatureCollection);
dev@64 401 const outputDescriptor = extracted.outputDescriptor;
cannam@118 402 const height = this.trackDiv.nativeElement.getBoundingClientRect().height / 2;
cannam@118 403 const waveTrack = this.timeline.getTrackById('wave');
dev@64 404
dev@64 405 // TODO refactor all of this
dev@63 406 switch (features.shape) {
dev@64 407 case 'vector': {
dev@63 408 const stepDuration = (features as FixedSpacedFeatures).stepDuration;
dev@63 409 const featureData = (features.data as Float32Array);
dev@68 410 if (featureData.length === 0) return;
dev@63 411 const normalisationFactor = 1.0 /
dev@63 412 featureData.reduce(
dev@63 413 (currentMax, feature) => Math.max(currentMax, feature),
dev@63 414 -Infinity
dev@63 415 );
dev@67 416
dev@63 417 const plotData = [...featureData].map((feature, i) => {
dev@63 418 return {
dev@63 419 cx: i * stepDuration,
dev@63 420 cy: feature * normalisationFactor
dev@63 421 };
dev@63 422 });
dev@67 423
dev@105 424 let lineLayer = new wavesUI.helpers.LineLayer(plotData, {
dev@63 425 color: colour,
dev@64 426 height: height
dev@63 427 });
dev@63 428 this.colouredLayers.set(this.addLayer(
dev@105 429 lineLayer,
cannam@117 430 waveTrack,
dev@63 431 this.timeline.timeContext
dev@63 432 ), colour);
dev@63 433 break;
dev@64 434 }
dev@64 435 case 'list': {
dev@64 436 const featureData = (features.data as FeatureList);
dev@68 437 if (featureData.length === 0) return;
dev@64 438 // TODO look at output descriptor instead of directly inspecting features
dev@64 439 const hasDuration = outputDescriptor.configured.hasDuration;
dev@64 440 const isMarker = !hasDuration
dev@64 441 && outputDescriptor.configured.binCount === 0
dev@64 442 && featureData[0].featureValues == null;
dev@64 443 const isRegion = hasDuration
dev@64 444 && featureData[0].timestamp != null;
dev@64 445 // TODO refactor, this is incomprehensible
dev@64 446 if (isMarker) {
dev@64 447 const plotData = featureData.map(feature => {
dev@64 448 return {x: toSeconds(feature.timestamp)}
dev@64 449 });
dev@64 450 let markerLayer = new wavesUI.helpers.MarkerLayer(plotData, {
dev@64 451 height: height,
dev@64 452 color: colour,
dev@64 453 });
dev@64 454 this.colouredLayers.set(this.addLayer(
dev@64 455 markerLayer,
cannam@117 456 waveTrack,
dev@64 457 this.timeline.timeContext
dev@64 458 ), colour);
dev@64 459 } else if (isRegion) {
dev@67 460 const binCount = outputDescriptor.configured.binCount || 0;
dev@67 461 const isBarRegion = featureData[0].featureValues.length >= 1 || binCount >= 1 ;
dev@64 462 const getSegmentArgs = () => {
dev@64 463 if (isBarRegion) {
dev@64 464
dev@67 465 // TODO refactor - this is messy
dev@67 466 interface FoldsToNumber<T> {
dev@67 467 reduce(fn: (previousValue: number,
dev@67 468 currentValue: T,
dev@67 469 currentIndex: number,
dev@67 470 array: ArrayLike<T>) => number,
dev@67 471 initialValue?: number): number;
dev@67 472 }
dev@64 473
dev@67 474 // TODO potentially change impl., i.e avoid reduce
dev@67 475 const findMin = <T>(arr: FoldsToNumber<T>, getElement: (x: T) => number): number => {
dev@67 476 return arr.reduce((min, val) => Math.min(min, getElement(val)), Infinity);
dev@67 477 };
dev@67 478
dev@67 479 const findMax = <T>(arr: FoldsToNumber<T>, getElement: (x: T) => number): number => {
dev@67 480 return arr.reduce((min, val) => Math.max(min, getElement(val)), -Infinity);
dev@67 481 };
dev@67 482
dev@67 483 const min = findMin<Feature>(featureData, (x: Feature) => {
dev@67 484 return findMin<number>(x.featureValues, y => y);
dev@67 485 });
dev@67 486
dev@67 487 const max = findMax<Feature>(featureData, (x: Feature) => {
dev@67 488 return findMax<number>(x.featureValues, y => y);
dev@67 489 });
dev@67 490
dev@67 491 const barHeight = 1.0 / height;
dev@64 492 return [
dev@67 493 featureData.reduce((bars, feature) => {
dev@67 494 const staticProperties = {
dev@64 495 x: toSeconds(feature.timestamp),
dev@64 496 width: toSeconds(feature.duration),
dev@67 497 height: min + barHeight,
dev@64 498 color: colour,
dev@64 499 opacity: 0.8
dev@67 500 };
dev@67 501 // TODO avoid copying Float32Array to an array - map is problematic here
dev@67 502 return bars.concat([...feature.featureValues]
dev@67 503 .map(val => Object.assign({}, staticProperties, {y: val})))
dev@67 504 }, []),
dev@67 505 {yDomain: [min, max + barHeight], height: height} as any
dev@67 506 ];
dev@64 507 } else {
dev@64 508 return [featureData.map(feature => {
dev@64 509 return {
dev@64 510 x: toSeconds(feature.timestamp),
dev@64 511 width: toSeconds(feature.duration),
dev@64 512 color: colour,
dev@64 513 opacity: 0.8
dev@64 514 }
dev@64 515 }), {height: height}];
dev@64 516 }
dev@64 517 };
dev@64 518
dev@64 519 let segmentLayer = new wavesUI.helpers.SegmentLayer(
dev@64 520 ...getSegmentArgs()
dev@64 521 );
dev@64 522 this.colouredLayers.set(this.addLayer(
dev@64 523 segmentLayer,
cannam@117 524 waveTrack,
dev@64 525 this.timeline.timeContext
dev@64 526 ), colour);
dev@64 527 }
dev@64 528 break;
dev@64 529 }
cannam@106 530 case 'matrix': {
cannam@108 531 const stepDuration = (features as FixedSpacedFeatures).stepDuration;
cannam@108 532 const matrixData = (features.data as Float32Array[]);
cannam@108 533 if (matrixData.length === 0) return;
cannam@109 534 console.log("matrix data length = " + matrixData.length);
cannam@109 535 console.log("height of first column = " + matrixData[0].length);
cannam@109 536 const targetValue = this.estimatePercentile(matrixData, 95);
cannam@108 537 const gain = (targetValue > 0.0 ? (1.0 / targetValue) : 1.0);
cannam@108 538 console.log("setting gain to " + gain);
cannam@108 539 const matrixEntity = new wavesUI.utils.PrefilledMatrixEntity(matrixData);
cannam@108 540 let matrixLayer = new wavesUI.helpers.MatrixLayer(matrixEntity, {
cannam@108 541 gain,
cannam@118 542 height: height * 0.9,
cannam@118 543 top: height * 0.05,
cannam@109 544 normalise: 'none',
cannam@108 545 mapper: this.iceMapper()
cannam@108 546 });
cannam@108 547 this.colouredLayers.set(this.addLayer(
cannam@108 548 matrixLayer,
cannam@117 549 waveTrack,
cannam@108 550 this.timeline.timeContext
cannam@108 551 ), colour);
cannam@108 552 break;
cannam@106 553 }
dev@67 554 default:
cannam@106 555 console.log("Cannot render an appropriate layer for feature shape '" +
cannam@106 556 features.shape + "'");
dev@63 557 }
dev@59 558
dev@56 559 this.timeline.tracks.update();
dev@53 560 }
dev@53 561
dev@53 562 private animate(): void {
dev@31 563 this.ngZone.runOutsideAngular(() => {
dev@31 564 // listen for time passing...
dev@31 565 const updateSeekingCursor = () => {
dev@53 566 const currentTime = this.audioService.getCurrentTime();
dev@53 567 this.cursorLayer.currentPosition = currentTime;
dev@53 568 this.cursorLayer.update();
dev@53 569
dev@53 570 const currentOffset = this.timeline.timeContext.offset;
dev@53 571 const offsetTimestamp = currentOffset
dev@53 572 + currentTime;
dev@53 573
dev@53 574 const visibleDuration = this.timeline.timeContext.visibleDuration;
dev@53 575 // TODO reduce duplication between directions and make more declarative
dev@53 576 // this kinda logic should also be tested
dev@53 577 const mustPageForward = offsetTimestamp > visibleDuration;
dev@53 578 const mustPageBackward = currentTime < -currentOffset;
dev@53 579
dev@53 580 if (mustPageForward) {
dev@53 581 const hasSkippedMultiplePages = offsetTimestamp - visibleDuration > visibleDuration;
dev@53 582
cannam@106 583 this.timeline.timeContext.offset = hasSkippedMultiplePages ?
cannam@106 584 -currentTime + 0.5 * visibleDuration :
cannam@106 585 currentOffset - visibleDuration;
dev@51 586 this.timeline.tracks.update();
dev@34 587 }
dev@53 588
dev@53 589 if (mustPageBackward) {
dev@53 590 const hasSkippedMultiplePages = currentTime + visibleDuration < -currentOffset;
cannam@106 591 this.timeline.timeContext.offset = hasSkippedMultiplePages ?
cannam@106 592 -currentTime + 0.5 * visibleDuration :
cannam@106 593 currentOffset + visibleDuration;
dev@51 594 this.timeline.tracks.update();
dev@34 595 }
dev@53 596
dev@53 597 if (this.isPlaying)
dev@53 598 requestAnimationFrame(updateSeekingCursor);
dev@31 599 };
dev@31 600 updateSeekingCursor();
dev@31 601 });
dev@6 602 }
dev@16 603
dev@59 604 private addLayer(layer: Layer, track: Track, timeContext: any, isAxis: boolean = false): DisposableIndex {
dev@54 605 timeContext.zoom = 1.0;
dev@54 606 if (!layer.timeContext) {
dev@54 607 layer.setTimeContext(isAxis ?
dev@54 608 timeContext : new wavesUI.core.LayerTimeContext(timeContext));
dev@54 609 }
dev@54 610 track.add(layer);
dev@54 611 layer.render();
dev@54 612 layer.update();
dev@112 613 if (this.cursorLayer) {
dev@112 614 track.$layout.appendChild(this.cursorLayer.$el);
dev@112 615 }
dev@59 616 return this.disposableLayers.push(layer) - 1;
dev@59 617 }
dev@59 618
dev@59 619 private static changeColour(layer: Layer, colour: string): void {
dev@59 620 const butcherShapes = (shape) => {
dev@59 621 shape.install({color: () => colour});
dev@59 622 shape.params.color = colour;
dev@59 623 shape.update(layer._renderingContext, layer.data);
dev@59 624 };
dev@59 625
dev@59 626 layer._$itemCommonShapeMap.forEach(butcherShapes);
dev@59 627 layer._$itemShapeMap.forEach(butcherShapes);
dev@59 628 layer.render();
dev@59 629 layer.update();
dev@54 630 }
dev@54 631
dev@51 632 ngOnDestroy(): void {
dev@51 633 this.featureExtractionSubscription.unsubscribe();
dev@53 634 this.playingStateSubscription.unsubscribe();
dev@53 635 this.seekedSubscription.unsubscribe();
dev@51 636 }
dev@6 637 }