dev@10: import { dev@51: Component, OnInit, ViewChild, ElementRef, Input, AfterViewInit, NgZone, dev@51: OnDestroy dev@10: } from '@angular/core'; dev@39: import {AudioPlayerService} from "../services/audio-player/audio-player.service"; dev@36: import wavesUI from 'waves-ui'; dev@63: import { dev@63: FeatureExtractionService, dev@63: Extracted dev@63: } from "../services/feature-extraction/feature-extraction.service"; dev@51: import {Subscription} from "rxjs"; dev@63: import { dev@63: FeatureCollection, dev@63: FixedSpacedFeatures dev@63: } from "piper/HigherLevelUtilities"; dev@53: import {toSeconds} from "piper"; dev@8: dev@20: type Timeline = any; // TODO what type actually is it.. start a .d.ts for waves-ui? dev@54: type Layer = any; dev@54: type Track = any; dev@59: type DisposableIndex = number; dev@59: type Colour = string; dev@6: dev@6: @Component({ dev@6: selector: 'app-waveform', dev@6: templateUrl: './waveform.component.html', dev@6: styleUrls: ['./waveform.component.css'] dev@6: }) dev@51: export class WaveformComponent implements OnInit, AfterViewInit, OnDestroy { dev@20: dev@8: @ViewChild('track') trackDiv: ElementRef; dev@6: dev@54: private _audioBuffer: AudioBuffer; dev@54: private timeline: Timeline; dev@54: private cursorLayer: any; dev@54: private disposableLayers: Layer[]; dev@59: private colouredLayers: Map; dev@16: dev@16: @Input() dev@16: set audioBuffer(buffer: AudioBuffer) { dev@16: this._audioBuffer = buffer || undefined; dev@20: if (this.audioBuffer) dev@20: this.renderWaveform(this.audioBuffer); dev@16: } dev@16: dev@16: get audioBuffer(): AudioBuffer { dev@16: return this._audioBuffer; dev@16: } dev@16: dev@51: private featureExtractionSubscription: Subscription; dev@53: private playingStateSubscription: Subscription; dev@53: private seekedSubscription: Subscription; dev@53: private isPlaying: boolean; dev@51: dev@31: constructor(private audioService: AudioPlayerService, dev@51: private piperService: FeatureExtractionService, dev@51: public ngZone: NgZone) { dev@59: this.colouredLayers = new Map(); dev@54: this.disposableLayers = []; dev@54: this._audioBuffer = undefined; dev@54: this.timeline = undefined; dev@54: this.cursorLayer = undefined; dev@53: this.isPlaying = false; dev@59: const colours = function* () { dev@59: const circularColours = [ dev@59: 'black', dev@59: 'red', dev@59: 'green', dev@59: 'purple', dev@59: 'orange' dev@59: ]; dev@59: let index = 0; dev@59: const nColours = circularColours.length; dev@59: while (true) { dev@59: yield circularColours[index = ++index % nColours]; dev@59: } dev@59: }(); dev@59: dev@51: this.featureExtractionSubscription = piperService.featuresExtracted$.subscribe( dev@51: features => { dev@59: this.renderFeatures(features, colours.next().value); dev@51: }); dev@53: this.playingStateSubscription = audioService.playingStateChange$.subscribe( dev@53: isPlaying => { dev@53: this.isPlaying = isPlaying; dev@53: if (this.isPlaying) dev@53: this.animate(); dev@53: }); dev@53: this.seekedSubscription = audioService.seeked$.subscribe(() => { dev@53: if (!this.isPlaying) dev@53: this.animate(); dev@53: }); dev@51: } dev@51: dev@53: ngOnInit() { dev@53: } dev@10: dev@10: ngAfterViewInit(): void { dev@51: this.timeline = this.renderTimeline(); dev@20: } dev@20: dev@20: renderTimeline(duration: number = 1.0): Timeline { dev@18: const track: HTMLElement = this.trackDiv.nativeElement; dev@20: track.innerHTML = ""; dev@18: const height: number = track.getBoundingClientRect().height; dev@18: const width: number = track.getBoundingClientRect().width; dev@18: const pixelsPerSecond = width / duration; dev@18: const timeline = new wavesUI.core.Timeline(pixelsPerSecond, width); dev@33: timeline.timeContext.offset = 0.5 * timeline.timeContext.visibleDuration; dev@18: timeline.createTrack(track, height, 'main'); dev@54: return timeline; dev@54: } dev@18: dev@54: renderWaveform(buffer: AudioBuffer): void { dev@54: const height: number = this.trackDiv.nativeElement.getBoundingClientRect().height; dev@54: const mainTrack = this.timeline.getTrackById('main'); dev@54: if (this.timeline) { dev@54: // resize dev@54: const width = this.trackDiv.nativeElement.getBoundingClientRect().width; dev@55: dev@54: // loop through layers and remove them, waves-ui provides methods for this but it seems to not work properly dev@55: const timeContextChildren = this.timeline.timeContext._children; dev@55: dev@60: for (let i = 0, length = this.disposableLayers.length; i < length; ++i) { dev@54: let layer = this.disposableLayers.pop(); dev@54: mainTrack.remove(layer); dev@55: dev@55: const index = timeContextChildren.indexOf(layer.timeContext); dev@55: if (index >= 0) dev@55: timeContextChildren.splice(index, 1); dev@54: layer.destroy(); dev@54: } dev@59: this.colouredLayers.clear(); dev@59: dev@54: this.timeline.visibleWidth = width; dev@54: this.timeline.pixelsPerSecond = width / buffer.duration; dev@54: mainTrack.height = height; dev@54: } else { dev@54: this.timeline = this.renderTimeline(buffer.duration) dev@54: } dev@18: // time axis dev@18: const timeAxis = new wavesUI.helpers.TimeAxisLayer({ dev@18: height: height, dev@18: color: 'gray' dev@18: }); dev@54: this.addLayer(timeAxis, mainTrack, this.timeline.timeContext, true); dev@18: dev@20: const waveformLayer = new wavesUI.helpers.WaveformLayer(buffer, { dev@10: top: 10, dev@20: height: height * 0.9, dev@16: color: 'darkblue' dev@16: }); dev@54: this.addLayer(waveformLayer, mainTrack, this.timeline.timeContext); dev@31: dev@53: this.cursorLayer = new wavesUI.helpers.CursorLayer({ dev@31: height: height dev@31: }); dev@54: this.addLayer(this.cursorLayer, mainTrack, this.timeline.timeContext); dev@51: this.timeline.state = new wavesUI.states.CenteredZoomState(this.timeline); dev@54: mainTrack.render(); dev@54: mainTrack.update(); dev@53: this.animate(); dev@53: } dev@53: dev@53: // TODO refactor - this doesn't belong here dev@63: private renderFeatures(extracted: Extracted, colour: Colour): void { dev@63: if (!extracted.hasOwnProperty('shape') || !extracted.hasOwnProperty('data')) return; dev@63: const features: FeatureCollection = (extracted as FeatureCollection); dev@63: switch (features.shape) { dev@63: case 'vector': dev@63: const stepDuration = (features as FixedSpacedFeatures).stepDuration; dev@63: const featureData = (features.data as Float32Array); dev@63: const normalisationFactor = 1.0 / dev@63: featureData.reduce( dev@63: (currentMax, feature) => Math.max(currentMax, feature), dev@63: -Infinity dev@63: ); dev@63: const plotData = [...featureData].map((feature, i) => { dev@63: return { dev@63: cx: i * stepDuration, dev@63: cy: feature * normalisationFactor dev@63: }; dev@63: }); dev@63: let breakpointLayer = new wavesUI.helpers.BreakpointLayer(plotData, { dev@63: color: colour, dev@63: height: this.trackDiv.nativeElement.getBoundingClientRect().height dev@63: }); dev@63: this.colouredLayers.set(this.addLayer( dev@63: breakpointLayer, dev@63: this.timeline.getTrackById('main'), dev@63: this.timeline.timeContext dev@63: ), colour); dev@63: break; dev@63: } dev@59: dev@56: this.timeline.tracks.update(); dev@53: } dev@53: dev@53: private animate(): void { dev@31: this.ngZone.runOutsideAngular(() => { dev@31: // listen for time passing... dev@31: const updateSeekingCursor = () => { dev@53: const currentTime = this.audioService.getCurrentTime(); dev@53: this.cursorLayer.currentPosition = currentTime; dev@53: this.cursorLayer.update(); dev@53: dev@53: const currentOffset = this.timeline.timeContext.offset; dev@53: const offsetTimestamp = currentOffset dev@53: + currentTime; dev@53: dev@53: const visibleDuration = this.timeline.timeContext.visibleDuration; dev@53: // TODO reduce duplication between directions and make more declarative dev@53: // this kinda logic should also be tested dev@53: const mustPageForward = offsetTimestamp > visibleDuration; dev@53: const mustPageBackward = currentTime < -currentOffset; dev@53: dev@53: if (mustPageForward) { dev@53: const hasSkippedMultiplePages = offsetTimestamp - visibleDuration > visibleDuration; dev@53: dev@53: this.timeline.timeContext.offset = hasSkippedMultiplePages dev@53: ? -currentTime + 0.5 * visibleDuration dev@53: : currentOffset - visibleDuration; dev@51: this.timeline.tracks.update(); dev@34: } dev@53: dev@53: if (mustPageBackward) { dev@53: const hasSkippedMultiplePages = currentTime + visibleDuration < -currentOffset; dev@53: this.timeline.timeContext.offset = hasSkippedMultiplePages dev@53: ? -currentTime + 0.5 * visibleDuration dev@53: : currentOffset + visibleDuration; dev@51: this.timeline.tracks.update(); dev@34: } dev@53: dev@53: if (this.isPlaying) dev@53: requestAnimationFrame(updateSeekingCursor); dev@31: }; dev@31: updateSeekingCursor(); dev@31: }); dev@6: } dev@16: dev@59: private addLayer(layer: Layer, track: Track, timeContext: any, isAxis: boolean = false): DisposableIndex { dev@54: timeContext.zoom = 1.0; dev@54: if (!layer.timeContext) { dev@54: layer.setTimeContext(isAxis ? dev@54: timeContext : new wavesUI.core.LayerTimeContext(timeContext)); dev@54: } dev@54: track.add(layer); dev@54: layer.render(); dev@54: layer.update(); dev@59: return this.disposableLayers.push(layer) - 1; dev@59: } dev@59: dev@59: private static changeColour(layer: Layer, colour: string): void { dev@59: const butcherShapes = (shape) => { dev@59: shape.install({color: () => colour}); dev@59: shape.params.color = colour; dev@59: shape.update(layer._renderingContext, layer.data); dev@59: }; dev@59: dev@59: layer._$itemCommonShapeMap.forEach(butcherShapes); dev@59: layer._$itemShapeMap.forEach(butcherShapes); dev@59: layer.render(); dev@59: layer.update(); dev@54: } dev@54: dev@51: ngOnDestroy(): void { dev@51: this.featureExtractionSubscription.unsubscribe(); dev@53: this.playingStateSubscription.unsubscribe(); dev@53: this.seekedSubscription.unsubscribe(); dev@51: } dev@6: }