annotate src/app/waveform/waveform.component.ts @ 236:53ea6406d601

Generate new project with latest @angular/cli, including Angular 4.
author Lucas Thompson <dev@lucas.im>
date Tue, 25 Apr 2017 20:01:09 +0100
parents 0833ddde6a83
children ff71da632fbb
rev   line source
dev@10 1 import {
dev@236 2 Component,
dev@236 3 OnInit,
dev@236 4 ViewChild,
dev@236 5 ElementRef,
dev@236 6 Input,
dev@236 7 AfterViewInit,
dev@236 8 NgZone,
dev@236 9 OnDestroy,
dev@236 10 ChangeDetectorRef
dev@10 11 } from '@angular/core';
dev@196 12 import {
dev@196 13 AudioPlayerService, AudioResource,
dev@196 14 AudioResourceError
dev@236 15 } from '../services/audio-player/audio-player.service';
dev@36 16 import wavesUI from 'waves-ui';
dev@63 17 import {
dev@64 18 FeatureExtractionService
dev@236 19 } from '../services/feature-extraction/feature-extraction.service';
dev@236 20 import {Subscription} from 'rxjs/Subscription';
dev@63 21 import {
dev@63 22 FeatureCollection,
dev@236 23 FixedSpacedFeatures,
dev@236 24 SimpleResponse
dev@236 25 } from 'piper/HigherLevelUtilities';
dev@236 26 import {toSeconds} from 'piper';
dev@236 27 import {FeatureList, Feature} from 'piper/Feature';
dev@81 28 import * as Hammer from 'hammerjs';
dev@236 29 import {WavesSpectrogramLayer} from '../spectrogram/Spectrogram';
dev@8 30
dev@54 31 type Layer = any;
dev@54 32 type Track = any;
dev@59 33 type Colour = string;
dev@6 34
dev@6 35 @Component({
dev@236 36 selector: 'ugly-waveform',
dev@6 37 templateUrl: './waveform.component.html',
dev@6 38 styleUrls: ['./waveform.component.css']
dev@6 39 })
dev@51 40 export class WaveformComponent implements OnInit, AfterViewInit, OnDestroy {
dev@20 41
dev@8 42 @ViewChild('track') trackDiv: ElementRef;
dev@6 43
dev@189 44 @Input() timeline: Timeline;
dev@189 45 @Input() trackIdPrefix: string;
dev@196 46 @Input() set isSubscribedToExtractionService(isSubscribed: boolean) {
dev@196 47 if (isSubscribed) {
dev@196 48 if (this.featureExtractionSubscription) {
dev@196 49 return;
dev@196 50 }
dev@16 51
dev@196 52 const colours = function* () {
dev@196 53 const circularColours = [
dev@196 54 'black',
dev@196 55 'red',
dev@196 56 'green',
dev@196 57 'purple',
dev@196 58 'orange'
dev@196 59 ];
dev@196 60 let index = 0;
dev@196 61 const nColours = circularColours.length;
dev@196 62 while (true) {
dev@196 63 yield circularColours[index = ++index % nColours];
dev@196 64 }
dev@196 65 }();
dev@196 66
dev@196 67 this.featureExtractionSubscription =
dev@196 68 this.piperService.featuresExtracted$.subscribe(
dev@196 69 features => {
dev@196 70 this.renderFeatures(features, colours.next().value);
dev@196 71 });
dev@196 72 } else {
dev@196 73 if (this.featureExtractionSubscription) {
dev@196 74 this.featureExtractionSubscription.unsubscribe();
dev@196 75 }
dev@196 76 }
dev@196 77 }
dev@196 78 @Input() set isSubscribedToAudioService(isSubscribed: boolean) {
dev@196 79 this._isSubscribedToAudioService = isSubscribed;
dev@196 80 if (isSubscribed) {
dev@196 81 if (this.onAudioDataSubscription) {
dev@196 82 return;
dev@196 83 }
dev@196 84
dev@196 85 this.onAudioDataSubscription =
dev@196 86 this.audioService.audioLoaded$.subscribe(res => {
dev@196 87 const wasError = (res as AudioResourceError).message != null;
dev@196 88
dev@196 89 if (wasError) {
dev@196 90 console.warn('No audio, display error?');
dev@196 91 } else {
dev@196 92 this.audioBuffer = (res as AudioResource).samples;
dev@196 93 }
dev@196 94 });
dev@196 95 } else {
dev@196 96 if (this.onAudioDataSubscription) {
dev@196 97 this.onAudioDataSubscription.unsubscribe();
dev@196 98 }
dev@196 99 }
dev@196 100 }
dev@196 101
dev@196 102 get isSubscribedToAudioService(): boolean {
dev@196 103 return this._isSubscribedToAudioService;
dev@196 104 }
dev@196 105
dev@196 106 @Input() set isOneShotExtractor(isOneShot: boolean) {
dev@196 107 this._isOneShotExtractor = isOneShot;
dev@196 108 }
dev@196 109
dev@196 110 get isOneShotExtractor(): boolean {
dev@196 111 return this._isOneShotExtractor;
dev@196 112 }
dev@196 113
dev@196 114 @Input() set isSeeking(isSeeking: boolean) {
dev@196 115 this._isSeeking = isSeeking;
dev@196 116 if (isSeeking) {
dev@196 117 if (this.seekedSubscription) {
dev@196 118 return;
dev@196 119 }
dev@236 120 if (this.playingStateSubscription) {
dev@196 121 return;
dev@196 122 }
dev@196 123
dev@196 124 this.seekedSubscription = this.audioService.seeked$.subscribe(() => {
dev@236 125 if (!this.isPlaying) {
dev@196 126 this.animate();
dev@236 127 }
dev@196 128 });
dev@196 129 this.playingStateSubscription =
dev@196 130 this.audioService.playingStateChange$.subscribe(
dev@196 131 isPlaying => {
dev@196 132 this.isPlaying = isPlaying;
dev@236 133 if (this.isPlaying) {
dev@196 134 this.animate();
dev@236 135 }
dev@196 136 });
dev@196 137 } else {
dev@196 138 if (this.isPlaying) {
dev@196 139 this.isPlaying = false;
dev@196 140 }
dev@196 141 if (this.playingStateSubscription) {
dev@196 142 this.playingStateSubscription.unsubscribe();
dev@196 143 }
dev@196 144 if (this.seekedSubscription) {
dev@196 145 this.seekedSubscription.unsubscribe();
dev@196 146 }
dev@196 147 }
dev@196 148 }
dev@196 149
dev@196 150 get isSeeking(): boolean {
dev@196 151 return this._isSeeking;
dev@196 152 }
dev@196 153
dev@16 154 set audioBuffer(buffer: AudioBuffer) {
dev@16 155 this._audioBuffer = buffer || undefined;
cannam@117 156 if (this.audioBuffer) {
dev@20 157 this.renderWaveform(this.audioBuffer);
dev@180 158 // this.renderSpectrogram(this.audioBuffer);
cannam@117 159 }
dev@16 160 }
dev@16 161
dev@16 162 get audioBuffer(): AudioBuffer {
dev@16 163 return this._audioBuffer;
dev@16 164 }
dev@16 165
dev@196 166 private _audioBuffer: AudioBuffer;
dev@196 167 private _isSubscribedToAudioService: boolean;
dev@196 168 private _isOneShotExtractor: boolean;
dev@196 169 private _isSeeking: boolean;
dev@196 170 private cursorLayer: any;
dev@196 171 private layers: Layer[];
dev@51 172 private featureExtractionSubscription: Subscription;
dev@53 173 private playingStateSubscription: Subscription;
dev@53 174 private seekedSubscription: Subscription;
dev@196 175 private onAudioDataSubscription: Subscription;
dev@53 176 private isPlaying: boolean;
dev@155 177 private zoomOnMouseDown: number;
dev@157 178 private offsetOnMouseDown: number;
dev@196 179 private hasShot: boolean;
dev@196 180 private isLoading: boolean;
dev@51 181
dev@236 182 private static changeColour(layer: Layer, colour: string): void {
dev@236 183 const butcherShapes = (shape) => {
dev@236 184 shape.install({color: () => colour});
dev@236 185 shape.params.color = colour;
dev@236 186 shape.update(layer._renderingContext, layer.data);
dev@236 187 };
dev@236 188
dev@236 189 layer._$itemCommonShapeMap.forEach(butcherShapes);
dev@236 190 layer._$itemShapeMap.forEach(butcherShapes);
dev@236 191 layer.render();
dev@236 192 layer.update();
dev@236 193 }
dev@236 194
dev@31 195 constructor(private audioService: AudioPlayerService,
dev@51 196 private piperService: FeatureExtractionService,
dev@234 197 private ngZone: NgZone,
dev@234 198 private ref: ChangeDetectorRef) {
dev@196 199 this.isSubscribedToAudioService = true;
dev@196 200 this.isSeeking = true;
dev@185 201 this.layers = [];
dev@196 202 this.audioBuffer = undefined;
dev@54 203 this.timeline = undefined;
dev@54 204 this.cursorLayer = undefined;
dev@53 205 this.isPlaying = false;
dev@196 206 this.isLoading = true;
dev@51 207 }
dev@51 208
dev@53 209 ngOnInit() {
dev@53 210 }
dev@10 211
dev@10 212 ngAfterViewInit(): void {
dev@236 213 this.trackIdPrefix = this.trackIdPrefix || 'default';
dev@196 214 if (this.timeline) {
dev@196 215 this.renderTimeline(null, true, true);
dev@196 216 } else {
dev@196 217 this.renderTimeline();
dev@196 218 }
dev@20 219 }
dev@20 220
dev@196 221 renderTimeline(duration: number = 1.0,
dev@196 222 useExistingDuration: boolean = false,
dev@196 223 isInitialRender: boolean = false): Timeline {
dev@18 224 const track: HTMLElement = this.trackDiv.nativeElement;
dev@236 225 track.innerHTML = '';
dev@18 226 const height: number = track.getBoundingClientRect().height;
dev@18 227 const width: number = track.getBoundingClientRect().width;
dev@18 228 const pixelsPerSecond = width / duration;
dev@196 229 const hasExistingTimeline = this.timeline instanceof wavesUI.core.Timeline;
dev@196 230
dev@196 231 if (hasExistingTimeline) {
dev@196 232 if (!useExistingDuration) {
dev@196 233 this.timeline.pixelsPerSecond = pixelsPerSecond;
dev@196 234 this.timeline.visibleWidth = width;
dev@196 235 }
dev@180 236 } else {
dev@180 237 this.timeline = new wavesUI.core.Timeline(pixelsPerSecond, width);
dev@180 238 }
dev@196 239 const waveTrack = this.timeline.createTrack(
dev@196 240 track,
dev@196 241 height,
dev@196 242 `wave-${this.trackIdPrefix}`
dev@196 243 );
dev@196 244 if (isInitialRender && hasExistingTimeline) {
dev@196 245 // time axis
dev@196 246 const timeAxis = new wavesUI.helpers.TimeAxisLayer({
dev@196 247 height: height,
dev@196 248 color: '#b0b0b0'
dev@196 249 });
dev@196 250 this.addLayer(timeAxis, waveTrack, this.timeline.timeContext, true);
dev@196 251 this.cursorLayer = new wavesUI.helpers.CursorLayer({
dev@196 252 height: height
dev@196 253 });
dev@196 254 this.addLayer(this.cursorLayer, waveTrack, this.timeline.timeContext);
dev@196 255 }
dev@196 256 if ('ontouchstart' in window) {
dev@196 257 interface Point {
dev@196 258 x: number;
dev@196 259 y: number;
dev@196 260 }
dev@196 261
dev@236 262 let zoomGestureJustEnded = false;
dev@196 263
dev@196 264 const pixelToExponent: Function = wavesUI.utils.scales.linear()
dev@196 265 .domain([0, 100]) // 100px => factor 2
dev@196 266 .range([0, 1]);
dev@196 267
dev@196 268 const calculateDistance: (p1: Point, p2: Point) => number = (p1, p2) => {
dev@196 269 return Math.pow(
dev@196 270 Math.pow(p2.x - p1.x, 2) +
dev@196 271 Math.pow(p2.y - p1.y, 2), 0.5);
dev@196 272 };
dev@196 273
dev@205 274 const calculateMidPoint: (p1: Point, p2: Point) => Point = (p1, p2) => {
dev@205 275 return {
dev@205 276 x: 0.5 * (p1.x + p2.x),
dev@205 277 y: 0.5 * (p1.y + p2.y)
dev@205 278 };
dev@205 279 };
dev@205 280
dev@205 281 const hammertime = new Hammer.Manager(this.trackDiv.nativeElement, {
dev@205 282 recognizers: [
dev@205 283 [Hammer.Pan, { direction: Hammer.DIRECTION_HORIZONTAL }]
dev@205 284 ]
dev@205 285 });
dev@204 286
dev@204 287 // it seems HammerJs binds the event to the window?
dev@204 288 // causing these events to propagate to other components?
dev@204 289 const componentTimeline = this.timeline;
dev@204 290 let initialZoom;
dev@204 291 let initialDistance;
dev@204 292 let offsetAtPanStart;
dev@205 293 let startX;
dev@205 294 let isZooming;
dev@204 295
dev@196 296 const scroll = (ev) => {
dev@236 297 if (ev.center.x - startX === 0) {
dev@236 298 return;
dev@236 299 }
dev@236 300
dev@196 301 if (zoomGestureJustEnded) {
dev@196 302 zoomGestureJustEnded = false;
dev@236 303 console.log('Skip this event: likely a single touch dangling from pinch');
dev@196 304 return;
dev@196 305 }
dev@204 306 componentTimeline.timeContext.offset = offsetAtPanStart +
dev@204 307 componentTimeline.timeContext.timeToPixel.invert(ev.deltaX);
dev@204 308 componentTimeline.tracks.update();
dev@196 309 };
dev@196 310
dev@196 311 const zoom = (ev) => {
dev@236 312 if (ev.touches.length < 2) {
dev@236 313 return;
dev@236 314 }
dev@236 315
dev@214 316 ev.preventDefault();
dev@204 317 const minZoom = componentTimeline.state.minZoom;
dev@204 318 const maxZoom = componentTimeline.state.maxZoom;
dev@205 319 const p1: Point = {
dev@218 320 x: ev.touches[0].clientX,
dev@218 321 y: ev.touches[0].clientY
dev@205 322 };
dev@205 323 const p2: Point = {
dev@218 324 x: ev.touches[1].clientX,
dev@218 325 y: ev.touches[1].clientY
dev@205 326 };
dev@205 327 const distance = calculateDistance(p1, p2);
dev@205 328 const midPoint = calculateMidPoint(p1, p2);
dev@196 329
dev@196 330 const lastCenterTime =
dev@205 331 componentTimeline.timeContext.timeToPixel.invert(midPoint.x);
dev@196 332
dev@204 333 const exponent = pixelToExponent(distance - initialDistance);
dev@204 334 const targetZoom = initialZoom * Math.pow(2, exponent);
dev@196 335
dev@204 336 componentTimeline.timeContext.zoom =
dev@196 337 Math.min(Math.max(targetZoom, minZoom), maxZoom);
dev@196 338
dev@196 339 const newCenterTime =
dev@205 340 componentTimeline.timeContext.timeToPixel.invert(midPoint.x);
dev@196 341
dev@204 342 componentTimeline.timeContext.offset += newCenterTime - lastCenterTime;
dev@204 343 componentTimeline.tracks.update();
dev@196 344 };
dev@205 345 hammertime.on('panstart', (ev) => {
dev@204 346 offsetAtPanStart = componentTimeline.timeContext.offset;
dev@205 347 startX = ev.center.x;
dev@196 348 });
dev@196 349 hammertime.on('panleft', scroll);
dev@196 350 hammertime.on('panright', scroll);
dev@205 351
dev@205 352
dev@205 353 const element: HTMLElement = this.trackDiv.nativeElement;
dev@205 354 element.addEventListener('touchstart', (e) => {
dev@236 355 if (e.touches.length < 2) {
dev@236 356 return;
dev@236 357 }
dev@236 358
dev@205 359 isZooming = true;
dev@204 360 initialZoom = componentTimeline.timeContext.zoom;
dev@196 361
dev@204 362 initialDistance = calculateDistance({
dev@218 363 x: e.touches[0].clientX,
dev@218 364 y: e.touches[0].clientY
dev@196 365 }, {
dev@218 366 x: e.touches[1].clientX,
dev@218 367 y: e.touches[1].clientY
dev@196 368 });
dev@196 369 });
dev@205 370 element.addEventListener('touchend', () => {
dev@205 371 if (isZooming) {
dev@205 372 isZooming = false;
dev@205 373 zoomGestureJustEnded = true;
dev@205 374 }
dev@205 375 });
dev@205 376 element.addEventListener('touchmove', zoom);
dev@196 377 }
dev@189 378 // this.timeline.createTrack(track, height/2, `wave-${this.trackIdPrefix}`);
dev@189 379 // this.timeline.createTrack(track, height/2, `grid-${this.trackIdPrefix}`);
dev@54 380 }
dev@18 381
cannam@108 382 estimatePercentile(matrix, percentile) {
cannam@108 383 // our sample is not evenly distributed across the whole data set:
cannam@108 384 // it is guaranteed to include at least one sample from every
cannam@108 385 // column, and could sample some values more than once. But it
cannam@108 386 // should be good enough in most cases (todo: show this)
cannam@109 387 if (matrix.length === 0) {
cannam@109 388 return 0.0;
cannam@109 389 }
cannam@108 390 const w = matrix.length;
cannam@108 391 const h = matrix[0].length;
cannam@108 392 const n = w * h;
cannam@109 393 const m = (n > 50000 ? 50000 : n); // should base that on the %ile
cannam@108 394 let m_per = Math.floor(m / w);
dev@236 395 if (m_per < 1) {
dev@236 396 m_per = 1;
dev@236 397 }
dev@236 398
dev@236 399 const sample = [];
cannam@108 400 for (let x = 0; x < w; ++x) {
cannam@108 401 for (let i = 0; i < m_per; ++i) {
cannam@108 402 const y = Math.floor(Math.random() * h);
cannam@109 403 const value = matrix[x][y];
cannam@109 404 if (!isNaN(value) && value !== Infinity) {
cannam@109 405 sample.push(value);
cannam@109 406 }
cannam@108 407 }
cannam@108 408 }
cannam@109 409 if (sample.length === 0) {
dev@236 410 console.log('WARNING: No samples gathered, even though we hoped for ' +
dev@236 411 (m_per * w) + ' of them');
cannam@109 412 return 0.0;
cannam@109 413 }
dev@236 414 sample.sort((a, b) => { return a - b; });
cannam@108 415 const ix = Math.floor((sample.length * percentile) / 100);
dev@236 416 console.log('Estimating ' + percentile + '-%ile of ' +
dev@236 417 n + '-sample dataset (' + w + ' x ' + h + ') as value ' + ix +
dev@236 418 ' of sorted ' + sample.length + '-sample subset');
cannam@108 419 const estimate = sample[ix];
dev@236 420 console.log('Estimate is: ' + estimate + ' (where min sampled value = ' +
dev@236 421 sample[0] + ' and max = ' + sample[sample.length - 1] + ')');
cannam@108 422 return estimate;
cannam@108 423 }
cannam@108 424
cannam@108 425 interpolatingMapper(hexColours) {
cannam@108 426 const colours = hexColours.map(n => {
cannam@108 427 const i = parseInt(n, 16);
cannam@118 428 return [ ((i >> 16) & 255) / 255.0,
cannam@118 429 ((i >> 8) & 255) / 255.0,
cannam@118 430 ((i) & 255) / 255.0 ];
cannam@108 431 });
cannam@108 432 const last = colours.length - 1;
cannam@108 433 return (value => {
cannam@108 434 const m = value * last;
cannam@108 435 if (m >= last) {
cannam@108 436 return colours[last];
cannam@108 437 }
cannam@108 438 if (m <= 0) {
cannam@108 439 return colours[0];
cannam@108 440 }
cannam@108 441 const base = Math.floor(m);
cannam@108 442 const prop0 = base + 1.0 - m;
cannam@108 443 const prop1 = m - base;
cannam@108 444 const c0 = colours[base];
dev@236 445 const c1 = colours[base + 1];
cannam@118 446 return [ c0[0] * prop0 + c1[0] * prop1,
cannam@118 447 c0[1] * prop0 + c1[1] * prop1,
cannam@118 448 c0[2] * prop0 + c1[2] * prop1 ];
cannam@108 449 });
cannam@108 450 }
dev@110 451
cannam@108 452 iceMapper() {
dev@236 453 const hexColours = [
cannam@108 454 // Based on ColorBrewer ylGnBu
dev@236 455 'ffffff', 'ffff00', 'f7fcf0', 'e0f3db', 'ccebc5', 'a8ddb5',
dev@236 456 '7bccc4', '4eb3d3', '2b8cbe', '0868ac', '084081', '042040'
cannam@108 457 ];
cannam@108 458 hexColours.reverse();
cannam@108 459 return this.interpolatingMapper(hexColours);
cannam@108 460 }
dev@110 461
cannam@118 462 hsv2rgb(h, s, v) { // all values in range [0, 1]
cannam@118 463 const i = Math.floor(h * 6);
cannam@118 464 const f = h * 6 - i;
cannam@118 465 const p = v * (1 - s);
cannam@118 466 const q = v * (1 - f * s);
cannam@118 467 const t = v * (1 - (1 - f) * s);
cannam@118 468 let r = 0, g = 0, b = 0;
cannam@118 469 switch (i % 6) {
dev@236 470 case 0: r = v; g = t; b = p; break;
dev@236 471 case 1: r = q; g = v; b = p; break;
dev@236 472 case 2: r = p; g = v; b = t; break;
dev@236 473 case 3: r = p; g = q; b = v; break;
dev@236 474 case 4: r = t; g = p; b = v; break;
dev@236 475 case 5: r = v; g = p; b = q; break;
cannam@118 476 }
cannam@118 477 return [ r, g, b ];
cannam@118 478 }
dev@122 479
cannam@118 480 greenMapper() {
cannam@118 481 const blue = 0.6666;
cannam@118 482 const pieslice = 0.3333;
cannam@118 483 return (value => {
cannam@118 484 const h = blue - value * 2.0 * pieslice;
cannam@118 485 const s = 0.5 + value / 2.0;
cannam@118 486 const v = value;
cannam@118 487 return this.hsv2rgb(h, s, v);
cannam@118 488 });
cannam@118 489 }
cannam@118 490
cannam@118 491 sunsetMapper() {
cannam@118 492 return (value => {
dev@236 493 const r = (value - 0.24) * 2.38;
dev@236 494 const g = (value - 0.64) * 2.777;
cannam@118 495 let b = (3.6 * value);
dev@236 496 if (value > 0.277) {
dev@236 497 b = 2.0 - b;
dev@236 498 }
cannam@118 499 return [ r, g, b ];
cannam@118 500 });
cannam@118 501 }
cannam@118 502
dev@122 503 clearTimeline(): void {
dev@122 504 // loop through layers and remove them, waves-ui provides methods for this but it seems to not work properly
dev@122 505 const timeContextChildren = this.timeline.timeContext._children;
dev@236 506 for (const track of this.timeline.tracks) {
dev@122 507 if (track.layers.length === 0) { continue; }
dev@122 508 const trackLayers = Array.from(track.layers);
dev@122 509 while (trackLayers.length) {
dev@236 510 const layer: Layer = trackLayers.pop();
dev@185 511 if (this.layers.includes(layer)) {
dev@185 512 track.remove(layer);
dev@185 513 this.layers.splice(this.layers.indexOf(layer), 1);
dev@185 514 const index = timeContextChildren.indexOf(layer.timeContext);
dev@185 515 if (index >= 0) {
dev@185 516 timeContextChildren.splice(index, 1);
dev@185 517 }
dev@185 518 layer.destroy();
dev@122 519 }
dev@122 520 }
dev@122 521 }
dev@122 522 }
dev@122 523
dev@54 524 renderWaveform(buffer: AudioBuffer): void {
dev@180 525 // const height: number = this.trackDiv.nativeElement.getBoundingClientRect().height / 2;
dev@180 526 const height: number = this.trackDiv.nativeElement.getBoundingClientRect().height;
dev@189 527 const waveTrack = this.timeline.getTrackById(`wave-${this.trackIdPrefix}`);
dev@54 528 if (this.timeline) {
dev@54 529 // resize
dev@54 530 const width = this.trackDiv.nativeElement.getBoundingClientRect().width;
dev@55 531
dev@122 532 this.clearTimeline();
dev@59 533
dev@54 534 this.timeline.visibleWidth = width;
dev@54 535 this.timeline.pixelsPerSecond = width / buffer.duration;
cannam@117 536 waveTrack.height = height;
dev@54 537 } else {
dev@236 538 this.renderTimeline(buffer.duration);
dev@54 539 }
dev@83 540 this.timeline.timeContext.offset = 0.5 * this.timeline.timeContext.visibleDuration;
cannam@106 541
dev@18 542 // time axis
dev@18 543 const timeAxis = new wavesUI.helpers.TimeAxisLayer({
dev@18 544 height: height,
cannam@106 545 color: '#b0b0b0'
dev@18 546 });
cannam@117 547 this.addLayer(timeAxis, waveTrack, this.timeline.timeContext, true);
dev@18 548
cannam@161 549 const nchannels = buffer.numberOfChannels;
cannam@161 550 const totalWaveHeight = height * 0.9;
cannam@161 551 const waveHeight = totalWaveHeight / nchannels;
dev@189 552
cannam@161 553 for (let ch = 0; ch < nchannels; ++ch) {
dev@236 554 console.log('about to construct a waveform layer for channel ' + ch);
cannam@161 555 const waveformLayer = new wavesUI.helpers.WaveformLayer(buffer, {
dev@236 556 top: (height - totalWaveHeight) / 2 + waveHeight * ch,
dev@236 557 height: waveHeight,
dev@236 558 color: 'darkblue',
dev@236 559 channel: ch
cannam@161 560 });
cannam@161 561 this.addLayer(waveformLayer, waveTrack, this.timeline.timeContext);
cannam@161 562 }
cannam@117 563
dev@53 564 this.cursorLayer = new wavesUI.helpers.CursorLayer({
dev@31 565 height: height
dev@31 566 });
cannam@117 567 this.addLayer(this.cursorLayer, waveTrack, this.timeline.timeContext);
dev@51 568 this.timeline.state = new wavesUI.states.CenteredZoomState(this.timeline);
cannam@117 569 waveTrack.render();
cannam@117 570 waveTrack.update();
dev@81 571
dev@196 572 this.isLoading = false;
dev@234 573 this.ref.markForCheck();
dev@53 574 this.animate();
dev@53 575 }
dev@53 576
cannam@117 577 renderSpectrogram(buffer: AudioBuffer): void {
cannam@117 578 const height: number = this.trackDiv.nativeElement.getBoundingClientRect().height / 2;
dev@189 579 const gridTrack = this.timeline.getTrackById(`grid-${this.trackIdPrefix}`);
cannam@117 580
dev@129 581 const spectrogramLayer = new WavesSpectrogramLayer(buffer, {
cannam@221 582 top: 0,
cannam@221 583 height: height,
cannam@117 584 stepSize: 512,
dev@129 585 blockSize: 1024,
cannam@118 586 normalise: 'none',
cannam@118 587 mapper: this.sunsetMapper()
cannam@117 588 });
cannam@117 589 this.addLayer(spectrogramLayer, gridTrack, this.timeline.timeContext);
cannam@117 590
cannam@117 591 this.timeline.tracks.update();
cannam@117 592 }
cannam@117 593
dev@53 594 // TODO refactor - this doesn't belong here
dev@64 595 private renderFeatures(extracted: SimpleResponse, colour: Colour): void {
dev@196 596 if (this.isOneShotExtractor && !this.hasShot) {
dev@196 597 this.featureExtractionSubscription.unsubscribe();
dev@196 598 this.hasShot = true;
dev@196 599 }
dev@196 600
dev@236 601 if (!extracted.hasOwnProperty('features')
dev@236 602 || !extracted.hasOwnProperty('outputDescriptor')) {
dev@236 603 return;
dev@236 604 }
dev@236 605 if (!extracted.features.hasOwnProperty('shape')
dev@236 606 || !extracted.features.hasOwnProperty('data')) {
dev@236 607 return;
dev@236 608 }
dev@64 609 const features: FeatureCollection = (extracted.features as FeatureCollection);
dev@64 610 const outputDescriptor = extracted.outputDescriptor;
dev@196 611 // const height = this.trackDiv.nativeElement.getBoundingClientRect().height / 2;
dev@196 612 const height = this.trackDiv.nativeElement.getBoundingClientRect().height;
dev@189 613 const waveTrack = this.timeline.getTrackById(`wave-${this.trackIdPrefix}`);
dev@64 614
dev@64 615 // TODO refactor all of this
dev@63 616 switch (features.shape) {
dev@64 617 case 'vector': {
dev@63 618 const stepDuration = (features as FixedSpacedFeatures).stepDuration;
dev@63 619 const featureData = (features.data as Float32Array);
dev@236 620 if (featureData.length === 0) {
dev@236 621 return;
dev@236 622 }
dev@63 623 const normalisationFactor = 1.0 /
dev@63 624 featureData.reduce(
dev@63 625 (currentMax, feature) => Math.max(currentMax, feature),
dev@63 626 -Infinity
dev@63 627 );
dev@67 628
dev@63 629 const plotData = [...featureData].map((feature, i) => {
dev@63 630 return {
dev@63 631 cx: i * stepDuration,
dev@63 632 cy: feature * normalisationFactor
dev@63 633 };
dev@63 634 });
dev@67 635
dev@236 636 const lineLayer = new wavesUI.helpers.LineLayer(plotData, {
dev@63 637 color: colour,
dev@64 638 height: height
dev@63 639 });
dev@122 640 this.addLayer(
dev@105 641 lineLayer,
cannam@117 642 waveTrack,
dev@63 643 this.timeline.timeContext
dev@122 644 );
dev@63 645 break;
dev@64 646 }
dev@64 647 case 'list': {
dev@64 648 const featureData = (features.data as FeatureList);
dev@236 649 if (featureData.length === 0) {
dev@236 650 return;
dev@236 651 }
dev@64 652 // TODO look at output descriptor instead of directly inspecting features
dev@64 653 const hasDuration = outputDescriptor.configured.hasDuration;
dev@64 654 const isMarker = !hasDuration
dev@64 655 && outputDescriptor.configured.binCount === 0
dev@64 656 && featureData[0].featureValues == null;
dev@64 657 const isRegion = hasDuration
dev@64 658 && featureData[0].timestamp != null;
dev@236 659 console.log('Have list features: length ' + featureData.length +
dev@236 660 ', isMarker ' + isMarker + ', isRegion ' + isRegion +
dev@236 661 ', hasDuration ' + hasDuration);
dev@64 662 // TODO refactor, this is incomprehensible
dev@64 663 if (isMarker) {
dev@236 664 const plotData = featureData.map(feature => ({
dev@236 665 time: toSeconds(feature.timestamp),
dev@236 666 label: feature.label
dev@236 667 }));
dev@236 668 const featureLayer = new wavesUI.helpers.TickLayer(plotData, {
dev@64 669 height: height,
dev@64 670 color: colour,
cannam@152 671 labelPosition: 'bottom',
cannam@152 672 shadeSegments: true
dev@64 673 });
dev@122 674 this.addLayer(
cannam@149 675 featureLayer,
cannam@117 676 waveTrack,
dev@64 677 this.timeline.timeContext
dev@122 678 );
dev@64 679 } else if (isRegion) {
dev@236 680 console.log('Output is of region type');
dev@67 681 const binCount = outputDescriptor.configured.binCount || 0;
dev@67 682 const isBarRegion = featureData[0].featureValues.length >= 1 || binCount >= 1 ;
dev@64 683 const getSegmentArgs = () => {
dev@64 684 if (isBarRegion) {
dev@64 685
dev@67 686 // TODO refactor - this is messy
dev@67 687 interface FoldsToNumber<T> {
dev@67 688 reduce(fn: (previousValue: number,
dev@67 689 currentValue: T,
dev@67 690 currentIndex: number,
dev@67 691 array: ArrayLike<T>) => number,
dev@67 692 initialValue?: number): number;
dev@67 693 }
dev@64 694
dev@67 695 // TODO potentially change impl., i.e avoid reduce
dev@67 696 const findMin = <T>(arr: FoldsToNumber<T>, getElement: (x: T) => number): number => {
dev@67 697 return arr.reduce((min, val) => Math.min(min, getElement(val)), Infinity);
dev@67 698 };
dev@67 699
dev@67 700 const findMax = <T>(arr: FoldsToNumber<T>, getElement: (x: T) => number): number => {
dev@67 701 return arr.reduce((min, val) => Math.max(min, getElement(val)), -Infinity);
dev@67 702 };
dev@67 703
dev@67 704 const min = findMin<Feature>(featureData, (x: Feature) => {
dev@67 705 return findMin<number>(x.featureValues, y => y);
dev@67 706 });
dev@67 707
dev@67 708 const max = findMax<Feature>(featureData, (x: Feature) => {
dev@67 709 return findMax<number>(x.featureValues, y => y);
dev@67 710 });
dev@67 711
dev@67 712 const barHeight = 1.0 / height;
dev@64 713 return [
dev@67 714 featureData.reduce((bars, feature) => {
dev@67 715 const staticProperties = {
dev@64 716 x: toSeconds(feature.timestamp),
dev@64 717 width: toSeconds(feature.duration),
dev@67 718 height: min + barHeight,
dev@64 719 color: colour,
dev@64 720 opacity: 0.8
dev@67 721 };
dev@67 722 // TODO avoid copying Float32Array to an array - map is problematic here
dev@67 723 return bars.concat([...feature.featureValues]
dev@236 724 .map(val => Object.assign({}, staticProperties, {y: val})));
dev@67 725 }, []),
dev@67 726 {yDomain: [min, max + barHeight], height: height} as any
dev@67 727 ];
dev@64 728 } else {
dev@236 729 return [featureData.map(feature => ({
dev@236 730 x: toSeconds(feature.timestamp),
dev@236 731 width: toSeconds(feature.duration),
dev@236 732 color: colour,
dev@236 733 opacity: 0.8
dev@236 734 })), {height: height}];
dev@64 735 }
dev@64 736 };
dev@64 737
dev@236 738 const segmentLayer = new wavesUI.helpers.SegmentLayer(
dev@64 739 ...getSegmentArgs()
dev@64 740 );
dev@122 741 this.addLayer(
dev@64 742 segmentLayer,
cannam@117 743 waveTrack,
dev@64 744 this.timeline.timeContext
dev@122 745 );
dev@64 746 }
dev@64 747 break;
dev@64 748 }
cannam@106 749 case 'matrix': {
cannam@108 750 const stepDuration = (features as FixedSpacedFeatures).stepDuration;
dev@236 751 // !!! + start time
cannam@108 752 const matrixData = (features.data as Float32Array[]);
dev@236 753
dev@236 754 if (matrixData.length === 0) {
dev@236 755 return;
dev@236 756 }
dev@236 757
dev@236 758 console.log('matrix data length = ' + matrixData.length);
dev@236 759 console.log('height of first column = ' + matrixData[0].length);
cannam@109 760 const targetValue = this.estimatePercentile(matrixData, 95);
cannam@108 761 const gain = (targetValue > 0.0 ? (1.0 / targetValue) : 1.0);
dev@236 762 console.log('setting gain to ' + gain);
cannam@120 763 const matrixEntity =
cannam@120 764 new wavesUI.utils.PrefilledMatrixEntity(matrixData,
cannam@120 765 0, // startTime
cannam@120 766 stepDuration);
dev@236 767 const matrixLayer = new wavesUI.helpers.MatrixLayer(matrixEntity, {
cannam@108 768 gain,
cannam@221 769 top: 0,
cannam@221 770 height: height,
cannam@109 771 normalise: 'none',
cannam@108 772 mapper: this.iceMapper()
cannam@108 773 });
dev@122 774 this.addLayer(
cannam@108 775 matrixLayer,
cannam@117 776 waveTrack,
cannam@108 777 this.timeline.timeContext
dev@122 778 );
cannam@108 779 break;
cannam@106 780 }
dev@67 781 default:
dev@236 782 console.log(
dev@236 783 `Cannot render an appropriate layer for feature shape '${features.shape}'`
dev@236 784 );
dev@63 785 }
dev@59 786
dev@196 787 this.isLoading = false;
dev@234 788 this.ref.markForCheck();
dev@56 789 this.timeline.tracks.update();
dev@53 790 }
dev@53 791
dev@53 792 private animate(): void {
dev@236 793 if (!this.isSeeking) {
dev@236 794 return;
dev@236 795 }
dev@196 796
dev@31 797 this.ngZone.runOutsideAngular(() => {
dev@31 798 // listen for time passing...
dev@31 799 const updateSeekingCursor = () => {
dev@53 800 const currentTime = this.audioService.getCurrentTime();
dev@53 801 this.cursorLayer.currentPosition = currentTime;
dev@53 802 this.cursorLayer.update();
dev@53 803
dev@53 804 const currentOffset = this.timeline.timeContext.offset;
dev@53 805 const offsetTimestamp = currentOffset
dev@53 806 + currentTime;
dev@53 807
dev@53 808 const visibleDuration = this.timeline.timeContext.visibleDuration;
dev@53 809 // TODO reduce duplication between directions and make more declarative
dev@53 810 // this kinda logic should also be tested
dev@53 811 const mustPageForward = offsetTimestamp > visibleDuration;
dev@53 812 const mustPageBackward = currentTime < -currentOffset;
dev@53 813
dev@53 814 if (mustPageForward) {
dev@53 815 const hasSkippedMultiplePages = offsetTimestamp - visibleDuration > visibleDuration;
dev@53 816
cannam@106 817 this.timeline.timeContext.offset = hasSkippedMultiplePages ?
cannam@106 818 -currentTime + 0.5 * visibleDuration :
cannam@106 819 currentOffset - visibleDuration;
dev@51 820 this.timeline.tracks.update();
dev@34 821 }
dev@53 822
dev@53 823 if (mustPageBackward) {
dev@53 824 const hasSkippedMultiplePages = currentTime + visibleDuration < -currentOffset;
cannam@106 825 this.timeline.timeContext.offset = hasSkippedMultiplePages ?
cannam@106 826 -currentTime + 0.5 * visibleDuration :
cannam@106 827 currentOffset + visibleDuration;
dev@51 828 this.timeline.tracks.update();
dev@34 829 }
dev@53 830
dev@236 831 if (this.isPlaying) {
dev@53 832 requestAnimationFrame(updateSeekingCursor);
dev@236 833 }
dev@31 834 };
dev@31 835 updateSeekingCursor();
dev@31 836 });
dev@6 837 }
dev@16 838
dev@122 839 private addLayer(layer: Layer, track: Track, timeContext: any, isAxis: boolean = false): void {
dev@54 840 timeContext.zoom = 1.0;
dev@54 841 if (!layer.timeContext) {
dev@54 842 layer.setTimeContext(isAxis ?
dev@54 843 timeContext : new wavesUI.core.LayerTimeContext(timeContext));
dev@54 844 }
dev@54 845 track.add(layer);
dev@185 846 this.layers.push(layer);
dev@54 847 layer.render();
dev@54 848 layer.update();
dev@122 849 if (this.cursorLayer && track.$layout.contains(this.cursorLayer.$el)) {
dev@112 850 track.$layout.appendChild(this.cursorLayer.$el);
dev@112 851 }
dev@59 852 }
dev@59 853
dev@51 854 ngOnDestroy(): void {
dev@236 855 if (this.featureExtractionSubscription) {
dev@196 856 this.featureExtractionSubscription.unsubscribe();
dev@236 857 }
dev@236 858 if (this.playingStateSubscription) {
dev@196 859 this.playingStateSubscription.unsubscribe();
dev@236 860 }
dev@236 861 if (this.seekedSubscription) {
dev@196 862 this.seekedSubscription.unsubscribe();
dev@236 863 }
dev@236 864 if (this.onAudioDataSubscription) {
dev@196 865 this.onAudioDataSubscription.unsubscribe();
dev@236 866 }
dev@51 867 }
dev@154 868
dev@155 869 seekStart(): void {
dev@155 870 this.zoomOnMouseDown = this.timeline.timeContext.zoom;
dev@157 871 this.offsetOnMouseDown = this.timeline.timeContext.offset;
dev@155 872 }
dev@155 873
dev@155 874 seekEnd(x: number): void {
dev@157 875 const hasSameZoom: boolean = this.zoomOnMouseDown ===
dev@157 876 this.timeline.timeContext.zoom;
dev@157 877 const hasSameOffset: boolean = this.offsetOnMouseDown ===
dev@157 878 this.timeline.timeContext.offset;
dev@157 879 if (hasSameZoom && hasSameOffset) {
dev@155 880 this.seek(x);
dev@155 881 }
dev@155 882 }
dev@155 883
dev@154 884 seek(x: number): void {
dev@154 885 if (this.timeline) {
dev@154 886 const timeContext: any = this.timeline.timeContext;
dev@196 887 if (this.isSeeking) {
dev@196 888 this.audioService.seekTo(
dev@236 889 timeContext.timeToPixel.invert(x) - timeContext.offset
dev@196 890 );
dev@196 891 }
dev@154 892 }
dev@154 893 }
dev@6 894 }