annotate src/app/waveform/waveform.component.ts @ 56:0ecfbef9d942

Update layers after extracting features.
author Lucas Thompson <dev@lucas.im>
date Thu, 08 Dec 2016 15:29:18 +0000
parents 214e41418460
children bb2bbb192e2b
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@6 15
dev@6 16 @Component({
dev@6 17 selector: 'app-waveform',
dev@6 18 templateUrl: './waveform.component.html',
dev@6 19 styleUrls: ['./waveform.component.css']
dev@6 20 })
dev@51 21 export class WaveformComponent implements OnInit, AfterViewInit, OnDestroy {
dev@20 22
dev@8 23 @ViewChild('track') trackDiv: ElementRef;
dev@6 24
dev@54 25 private _audioBuffer: AudioBuffer;
dev@54 26 private timeline: Timeline;
dev@54 27 private cursorLayer: any;
dev@54 28 private disposableLayers: Layer[];
dev@16 29
dev@16 30 @Input()
dev@16 31 set audioBuffer(buffer: AudioBuffer) {
dev@16 32 this._audioBuffer = buffer || undefined;
dev@20 33 if (this.audioBuffer)
dev@20 34 this.renderWaveform(this.audioBuffer);
dev@16 35 }
dev@16 36
dev@16 37 get audioBuffer(): AudioBuffer {
dev@16 38 return this._audioBuffer;
dev@16 39 }
dev@16 40
dev@51 41 private featureExtractionSubscription: Subscription;
dev@53 42 private playingStateSubscription: Subscription;
dev@53 43 private seekedSubscription: Subscription;
dev@53 44 private isPlaying: boolean;
dev@51 45
dev@31 46 constructor(private audioService: AudioPlayerService,
dev@51 47 private piperService: FeatureExtractionService,
dev@51 48 public ngZone: NgZone) {
dev@54 49 this.disposableLayers = [];
dev@54 50 this._audioBuffer = undefined;
dev@54 51 this.timeline = undefined;
dev@54 52 this.cursorLayer = undefined;
dev@53 53 this.isPlaying = false;
dev@51 54 this.featureExtractionSubscription = piperService.featuresExtracted$.subscribe(
dev@51 55 features => {
dev@51 56 this.renderFeatures(features);
dev@51 57 });
dev@53 58 this.playingStateSubscription = audioService.playingStateChange$.subscribe(
dev@53 59 isPlaying => {
dev@53 60 this.isPlaying = isPlaying;
dev@53 61 if (this.isPlaying)
dev@53 62 this.animate();
dev@53 63 });
dev@53 64 this.seekedSubscription = audioService.seeked$.subscribe(() => {
dev@53 65 if (!this.isPlaying)
dev@53 66 this.animate();
dev@53 67 });
dev@51 68 }
dev@51 69
dev@53 70 ngOnInit() {
dev@53 71 }
dev@10 72
dev@10 73 ngAfterViewInit(): void {
dev@51 74 this.timeline = this.renderTimeline();
dev@20 75 }
dev@20 76
dev@20 77 renderTimeline(duration: number = 1.0): Timeline {
dev@18 78 const track: HTMLElement = this.trackDiv.nativeElement;
dev@20 79 track.innerHTML = "";
dev@18 80 const height: number = track.getBoundingClientRect().height;
dev@18 81 const width: number = track.getBoundingClientRect().width;
dev@18 82 const pixelsPerSecond = width / duration;
dev@18 83 const timeline = new wavesUI.core.Timeline(pixelsPerSecond, width);
dev@33 84 timeline.timeContext.offset = 0.5 * timeline.timeContext.visibleDuration;
dev@18 85 timeline.createTrack(track, height, 'main');
dev@54 86 return timeline;
dev@54 87 }
dev@18 88
dev@54 89 renderWaveform(buffer: AudioBuffer): void {
dev@54 90 const height: number = this.trackDiv.nativeElement.getBoundingClientRect().height;
dev@54 91 const mainTrack = this.timeline.getTrackById('main');
dev@54 92 if (this.timeline) {
dev@54 93 // resize
dev@54 94 const width = this.trackDiv.nativeElement.getBoundingClientRect().width;
dev@55 95
dev@54 96 // loop through layers and remove them, waves-ui provides methods for this but it seems to not work properly
dev@55 97 const timeContextChildren = this.timeline.timeContext._children;
dev@55 98
dev@54 99 for (let i = 0; i < this.disposableLayers.length; ++i) {
dev@54 100 let layer = this.disposableLayers.pop();
dev@54 101 mainTrack.remove(layer);
dev@55 102
dev@55 103 const index = timeContextChildren.indexOf(layer.timeContext);
dev@55 104 if (index >= 0)
dev@55 105 timeContextChildren.splice(index, 1);
dev@54 106 layer.destroy();
dev@54 107 }
dev@54 108 this.timeline.visibleWidth = width;
dev@54 109 this.timeline.pixelsPerSecond = width / buffer.duration;
dev@54 110 mainTrack.height = height;
dev@54 111 } else {
dev@54 112 this.timeline = this.renderTimeline(buffer.duration)
dev@54 113 }
dev@18 114 // time axis
dev@18 115 const timeAxis = new wavesUI.helpers.TimeAxisLayer({
dev@18 116 height: height,
dev@18 117 color: 'gray'
dev@18 118 });
dev@54 119 this.addLayer(timeAxis, mainTrack, this.timeline.timeContext, true);
dev@18 120
dev@20 121 const waveformLayer = new wavesUI.helpers.WaveformLayer(buffer, {
dev@10 122 top: 10,
dev@20 123 height: height * 0.9,
dev@16 124 color: 'darkblue'
dev@16 125 });
dev@54 126 this.addLayer(waveformLayer, mainTrack, this.timeline.timeContext);
dev@31 127
dev@53 128 this.cursorLayer = new wavesUI.helpers.CursorLayer({
dev@31 129 height: height
dev@31 130 });
dev@54 131 this.addLayer(this.cursorLayer, mainTrack, this.timeline.timeContext);
dev@51 132 this.timeline.state = new wavesUI.states.CenteredZoomState(this.timeline);
dev@54 133 mainTrack.render();
dev@54 134 mainTrack.update();
dev@53 135 this.animate();
dev@53 136 }
dev@53 137
dev@53 138 // TODO refactor - this doesn't belong here
dev@53 139 private renderFeatures(features: FeatureList): void {
dev@53 140 const plotData = features.map(feature => {
dev@53 141 return {
dev@53 142 cx: toSeconds(feature.timestamp),
dev@53 143 cy: feature.featureValues[0]
dev@53 144 };
dev@53 145 });
dev@54 146 this.addLayer(
dev@53 147 new wavesUI.helpers.BreakpointLayer(plotData, {color: 'green'}),
dev@54 148 this.timeline.getTrackById('main'),
dev@54 149 this.timeline.timeContext
dev@53 150 );
dev@56 151 this.timeline.tracks.update();
dev@53 152 }
dev@53 153
dev@53 154 private animate(): void {
dev@31 155 this.ngZone.runOutsideAngular(() => {
dev@31 156 // listen for time passing...
dev@31 157 const updateSeekingCursor = () => {
dev@53 158 const currentTime = this.audioService.getCurrentTime();
dev@53 159 this.cursorLayer.currentPosition = currentTime;
dev@53 160 this.cursorLayer.update();
dev@53 161
dev@53 162 const currentOffset = this.timeline.timeContext.offset;
dev@53 163 const offsetTimestamp = currentOffset
dev@53 164 + currentTime;
dev@53 165
dev@53 166 const visibleDuration = this.timeline.timeContext.visibleDuration;
dev@53 167 // TODO reduce duplication between directions and make more declarative
dev@53 168 // this kinda logic should also be tested
dev@53 169 const mustPageForward = offsetTimestamp > visibleDuration;
dev@53 170 const mustPageBackward = currentTime < -currentOffset;
dev@53 171
dev@53 172 if (mustPageForward) {
dev@53 173 const hasSkippedMultiplePages = offsetTimestamp - visibleDuration > visibleDuration;
dev@53 174
dev@53 175 this.timeline.timeContext.offset = hasSkippedMultiplePages
dev@53 176 ? -currentTime + 0.5 * visibleDuration
dev@53 177 : currentOffset - visibleDuration;
dev@51 178 this.timeline.tracks.update();
dev@34 179 }
dev@53 180
dev@53 181 if (mustPageBackward) {
dev@53 182 const hasSkippedMultiplePages = currentTime + visibleDuration < -currentOffset;
dev@53 183 this.timeline.timeContext.offset = hasSkippedMultiplePages
dev@53 184 ? -currentTime + 0.5 * visibleDuration
dev@53 185 : currentOffset + visibleDuration;
dev@51 186 this.timeline.tracks.update();
dev@34 187 }
dev@53 188
dev@53 189 if (this.isPlaying)
dev@53 190 requestAnimationFrame(updateSeekingCursor);
dev@31 191 };
dev@31 192 updateSeekingCursor();
dev@31 193 });
dev@6 194 }
dev@16 195
dev@54 196 private addLayer(layer: Layer, track: Track, timeContext: any, isAxis: boolean = false): void {
dev@54 197 timeContext.zoom = 1.0;
dev@54 198 if (!layer.timeContext) {
dev@54 199 layer.setTimeContext(isAxis ?
dev@54 200 timeContext : new wavesUI.core.LayerTimeContext(timeContext));
dev@54 201 }
dev@54 202 this.disposableLayers.push(layer);
dev@54 203 track.add(layer);
dev@54 204 layer.render();
dev@54 205 layer.update();
dev@54 206 }
dev@54 207
dev@51 208 ngOnDestroy(): void {
dev@51 209 this.featureExtractionSubscription.unsubscribe();
dev@53 210 this.playingStateSubscription.unsubscribe();
dev@53 211 this.seekedSubscription.unsubscribe();
dev@51 212 }
dev@6 213 }