annotate src/app/waveform/waveform.component.ts @ 60:2c3d502507b4

Store the original array length before iterating, as popping off the array is causing the length to change dynamically on each iteration. Rookie mistake.
author Lucas Thompson <dev@lucas.im>
date Thu, 08 Dec 2016 23:28:54 +0000
parents 010871a771d3
children 8b8f794942d1
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@51 7 import {FeatureList} from "piper/Feature";
dev@51 8 import {FeatureExtractionService} from "../services/feature-extraction/feature-extraction.service";
dev@51 9 import {Subscription} from "rxjs";
dev@53 10 import {toSeconds} from "piper";
dev@8 11
dev@20 12 type Timeline = any; // TODO what type actually is it.. start a .d.ts for waves-ui?
dev@54 13 type Layer = any;
dev@54 14 type Track = any;
dev@59 15 type DisposableIndex = number;
dev@59 16 type Colour = string;
dev@6 17
dev@6 18 @Component({
dev@6 19 selector: 'app-waveform',
dev@6 20 templateUrl: './waveform.component.html',
dev@6 21 styleUrls: ['./waveform.component.css']
dev@6 22 })
dev@51 23 export class WaveformComponent implements OnInit, AfterViewInit, OnDestroy {
dev@20 24
dev@8 25 @ViewChild('track') trackDiv: ElementRef;
dev@6 26
dev@54 27 private _audioBuffer: AudioBuffer;
dev@54 28 private timeline: Timeline;
dev@54 29 private cursorLayer: any;
dev@54 30 private disposableLayers: Layer[];
dev@59 31 private colouredLayers: Map<DisposableIndex, Colour>;
dev@16 32
dev@16 33 @Input()
dev@16 34 set audioBuffer(buffer: AudioBuffer) {
dev@16 35 this._audioBuffer = buffer || undefined;
dev@20 36 if (this.audioBuffer)
dev@20 37 this.renderWaveform(this.audioBuffer);
dev@16 38 }
dev@16 39
dev@16 40 get audioBuffer(): AudioBuffer {
dev@16 41 return this._audioBuffer;
dev@16 42 }
dev@16 43
dev@51 44 private featureExtractionSubscription: Subscription;
dev@53 45 private playingStateSubscription: Subscription;
dev@53 46 private seekedSubscription: Subscription;
dev@53 47 private isPlaying: boolean;
dev@51 48
dev@31 49 constructor(private audioService: AudioPlayerService,
dev@51 50 private piperService: FeatureExtractionService,
dev@51 51 public ngZone: NgZone) {
dev@59 52 this.colouredLayers = new Map();
dev@54 53 this.disposableLayers = [];
dev@54 54 this._audioBuffer = undefined;
dev@54 55 this.timeline = undefined;
dev@54 56 this.cursorLayer = undefined;
dev@53 57 this.isPlaying = false;
dev@59 58 const colours = function* () {
dev@59 59 const circularColours = [
dev@59 60 'black',
dev@59 61 'red',
dev@59 62 'green',
dev@59 63 'purple',
dev@59 64 'orange'
dev@59 65 ];
dev@59 66 let index = 0;
dev@59 67 const nColours = circularColours.length;
dev@59 68 while (true) {
dev@59 69 yield circularColours[index = ++index % nColours];
dev@59 70 }
dev@59 71 }();
dev@59 72
dev@51 73 this.featureExtractionSubscription = piperService.featuresExtracted$.subscribe(
dev@51 74 features => {
dev@59 75 this.renderFeatures(features, colours.next().value);
dev@51 76 });
dev@53 77 this.playingStateSubscription = audioService.playingStateChange$.subscribe(
dev@53 78 isPlaying => {
dev@53 79 this.isPlaying = isPlaying;
dev@53 80 if (this.isPlaying)
dev@53 81 this.animate();
dev@53 82 });
dev@53 83 this.seekedSubscription = audioService.seeked$.subscribe(() => {
dev@53 84 if (!this.isPlaying)
dev@53 85 this.animate();
dev@53 86 });
dev@51 87 }
dev@51 88
dev@53 89 ngOnInit() {
dev@53 90 }
dev@10 91
dev@10 92 ngAfterViewInit(): void {
dev@51 93 this.timeline = this.renderTimeline();
dev@20 94 }
dev@20 95
dev@20 96 renderTimeline(duration: number = 1.0): Timeline {
dev@18 97 const track: HTMLElement = this.trackDiv.nativeElement;
dev@20 98 track.innerHTML = "";
dev@18 99 const height: number = track.getBoundingClientRect().height;
dev@18 100 const width: number = track.getBoundingClientRect().width;
dev@18 101 const pixelsPerSecond = width / duration;
dev@18 102 const timeline = new wavesUI.core.Timeline(pixelsPerSecond, width);
dev@33 103 timeline.timeContext.offset = 0.5 * timeline.timeContext.visibleDuration;
dev@18 104 timeline.createTrack(track, height, 'main');
dev@54 105 return timeline;
dev@54 106 }
dev@18 107
dev@54 108 renderWaveform(buffer: AudioBuffer): void {
dev@54 109 const height: number = this.trackDiv.nativeElement.getBoundingClientRect().height;
dev@54 110 const mainTrack = this.timeline.getTrackById('main');
dev@54 111 if (this.timeline) {
dev@54 112 // resize
dev@54 113 const width = this.trackDiv.nativeElement.getBoundingClientRect().width;
dev@55 114
dev@54 115 // loop through layers and remove them, waves-ui provides methods for this but it seems to not work properly
dev@55 116 const timeContextChildren = this.timeline.timeContext._children;
dev@55 117
dev@60 118 for (let i = 0, length = this.disposableLayers.length; i < length; ++i) {
dev@54 119 let layer = this.disposableLayers.pop();
dev@54 120 mainTrack.remove(layer);
dev@55 121
dev@55 122 const index = timeContextChildren.indexOf(layer.timeContext);
dev@55 123 if (index >= 0)
dev@55 124 timeContextChildren.splice(index, 1);
dev@54 125 layer.destroy();
dev@54 126 }
dev@59 127 this.colouredLayers.clear();
dev@59 128
dev@54 129 this.timeline.visibleWidth = width;
dev@54 130 this.timeline.pixelsPerSecond = width / buffer.duration;
dev@54 131 mainTrack.height = height;
dev@54 132 } else {
dev@54 133 this.timeline = this.renderTimeline(buffer.duration)
dev@54 134 }
dev@18 135 // time axis
dev@18 136 const timeAxis = new wavesUI.helpers.TimeAxisLayer({
dev@18 137 height: height,
dev@18 138 color: 'gray'
dev@18 139 });
dev@54 140 this.addLayer(timeAxis, mainTrack, this.timeline.timeContext, true);
dev@18 141
dev@20 142 const waveformLayer = new wavesUI.helpers.WaveformLayer(buffer, {
dev@10 143 top: 10,
dev@20 144 height: height * 0.9,
dev@16 145 color: 'darkblue'
dev@16 146 });
dev@54 147 this.addLayer(waveformLayer, mainTrack, this.timeline.timeContext);
dev@31 148
dev@53 149 this.cursorLayer = new wavesUI.helpers.CursorLayer({
dev@31 150 height: height
dev@31 151 });
dev@54 152 this.addLayer(this.cursorLayer, mainTrack, this.timeline.timeContext);
dev@51 153 this.timeline.state = new wavesUI.states.CenteredZoomState(this.timeline);
dev@54 154 mainTrack.render();
dev@54 155 mainTrack.update();
dev@53 156 this.animate();
dev@53 157 }
dev@53 158
dev@53 159 // TODO refactor - this doesn't belong here
dev@59 160 private renderFeatures(features: FeatureList, colour: Colour): void {
dev@58 161 const normalisationFactor = 1.0 /
dev@58 162 features.reduce((currentMax, feature) => {
dev@58 163 return (feature.featureValues)
dev@58 164 ? Math.max(currentMax, feature.featureValues[0])
dev@58 165 : currentMax;
dev@58 166 }, -Infinity);
dev@53 167 const plotData = features.map(feature => {
dev@53 168 return {
dev@53 169 cx: toSeconds(feature.timestamp),
dev@58 170 cy: feature.featureValues[0] * normalisationFactor
dev@53 171 };
dev@53 172 });
dev@59 173 let breakpointLayer = new wavesUI.helpers.BreakpointLayer(plotData, {
dev@59 174 color: colour
dev@59 175 });
dev@59 176 this.colouredLayers.set(this.addLayer(
dev@59 177 breakpointLayer,
dev@54 178 this.timeline.getTrackById('main'),
dev@54 179 this.timeline.timeContext
dev@59 180 ), colour);
dev@59 181
dev@56 182 this.timeline.tracks.update();
dev@53 183 }
dev@53 184
dev@53 185 private animate(): void {
dev@31 186 this.ngZone.runOutsideAngular(() => {
dev@31 187 // listen for time passing...
dev@31 188 const updateSeekingCursor = () => {
dev@53 189 const currentTime = this.audioService.getCurrentTime();
dev@53 190 this.cursorLayer.currentPosition = currentTime;
dev@53 191 this.cursorLayer.update();
dev@53 192
dev@53 193 const currentOffset = this.timeline.timeContext.offset;
dev@53 194 const offsetTimestamp = currentOffset
dev@53 195 + currentTime;
dev@53 196
dev@53 197 const visibleDuration = this.timeline.timeContext.visibleDuration;
dev@53 198 // TODO reduce duplication between directions and make more declarative
dev@53 199 // this kinda logic should also be tested
dev@53 200 const mustPageForward = offsetTimestamp > visibleDuration;
dev@53 201 const mustPageBackward = currentTime < -currentOffset;
dev@53 202
dev@53 203 if (mustPageForward) {
dev@53 204 const hasSkippedMultiplePages = offsetTimestamp - visibleDuration > visibleDuration;
dev@53 205
dev@53 206 this.timeline.timeContext.offset = hasSkippedMultiplePages
dev@53 207 ? -currentTime + 0.5 * visibleDuration
dev@53 208 : currentOffset - visibleDuration;
dev@51 209 this.timeline.tracks.update();
dev@34 210 }
dev@53 211
dev@53 212 if (mustPageBackward) {
dev@53 213 const hasSkippedMultiplePages = currentTime + visibleDuration < -currentOffset;
dev@53 214 this.timeline.timeContext.offset = hasSkippedMultiplePages
dev@53 215 ? -currentTime + 0.5 * visibleDuration
dev@53 216 : currentOffset + visibleDuration;
dev@51 217 this.timeline.tracks.update();
dev@34 218 }
dev@53 219
dev@53 220 if (this.isPlaying)
dev@53 221 requestAnimationFrame(updateSeekingCursor);
dev@31 222 };
dev@31 223 updateSeekingCursor();
dev@31 224 });
dev@6 225 }
dev@16 226
dev@59 227 private addLayer(layer: Layer, track: Track, timeContext: any, isAxis: boolean = false): DisposableIndex {
dev@54 228 timeContext.zoom = 1.0;
dev@54 229 if (!layer.timeContext) {
dev@54 230 layer.setTimeContext(isAxis ?
dev@54 231 timeContext : new wavesUI.core.LayerTimeContext(timeContext));
dev@54 232 }
dev@54 233 track.add(layer);
dev@54 234 layer.render();
dev@54 235 layer.update();
dev@59 236 return this.disposableLayers.push(layer) - 1;
dev@59 237 }
dev@59 238
dev@59 239 private static changeColour(layer: Layer, colour: string): void {
dev@59 240 const butcherShapes = (shape) => {
dev@59 241 shape.install({color: () => colour});
dev@59 242 shape.params.color = colour;
dev@59 243 shape.update(layer._renderingContext, layer.data);
dev@59 244 };
dev@59 245
dev@59 246 layer._$itemCommonShapeMap.forEach(butcherShapes);
dev@59 247 layer._$itemShapeMap.forEach(butcherShapes);
dev@59 248 layer.render();
dev@59 249 layer.update();
dev@54 250 }
dev@54 251
dev@51 252 ngOnDestroy(): void {
dev@51 253 this.featureExtractionSubscription.unsubscribe();
dev@53 254 this.playingStateSubscription.unsubscribe();
dev@53 255 this.seekedSubscription.unsubscribe();
dev@51 256 }
dev@6 257 }