annotate src/app/waveform/waveform.component.ts @ 108:7740f7fd7c3c

Some crazy work to try to get sensible default normalisation and colour map
author Chris Cannam <cannam@all-day-breakfast.com>
date Fri, 10 Mar 2017 14:46:18 +0000
parents fe6b0c525a7d
children 68fe21cfda2a
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;
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 }