Mercurial > hg > ugly-duckling
diff src/app/visualisations/waveform/waveform.component.ts @ 347:82d476b976e0
Move waveform.component and remove all logic not concerned with rendering an audio buffer as a waveform. Wire app in app.component.
author | Lucas Thompson <dev@lucas.im> |
---|---|
date | Thu, 25 May 2017 17:55:29 +0100 |
parents | |
children | a9ce5516c17d |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/app/visualisations/waveform/waveform.component.ts Thu May 25 17:55:29 2017 +0100 @@ -0,0 +1,177 @@ +import { + Component, + ViewChild, + ElementRef, + Input, + ChangeDetectorRef +} from '@angular/core'; +import wavesUI from 'waves-ui-piper'; +import {attachTouchHandlerBodges} from '../WavesJunk'; +import {OnSeekHandler} from '../../playhead/PlayHeadHelpers'; + +type Layer = any; +type Track = any; + +@Component({ + selector: 'ugly-waveform', + templateUrl: './waveform.component.html', + styleUrls: ['./waveform.component.css'] +}) +export class WaveformComponent { + + @ViewChild('track') trackDiv: ElementRef; + @Input() set width(width: number) { + if (this.timeline) { + requestAnimationFrame(() => { + this.timeline.timeContext.visibleWidth = width; + this.timeline.tracks.update(); + }); + } + } + @Input() timeline: Timeline; + @Input() trackIdPrefix: string; + @Input() onSeek: OnSeekHandler; + + @Input() set audioBuffer(buffer: AudioBuffer) { + this._audioBuffer = buffer || undefined; + if (this.audioBuffer) { + this.renderWaveform(this.audioBuffer); + } + } + + get audioBuffer(): AudioBuffer { + return this._audioBuffer; + } + + private _audioBuffer: AudioBuffer; + private layers: Layer[]; + private zoomOnMouseDown: number; + private offsetOnMouseDown: number; + private waveTrack: Track; + + constructor(private ref: ChangeDetectorRef) { + this.layers = []; + } + + renderTimeline(duration: number = 1.0): Timeline { + const track: HTMLElement = this.trackDiv.nativeElement; + track.innerHTML = ''; + const height: number = track.getBoundingClientRect().height; + const width: number = track.getBoundingClientRect().width; + this.timeline.pixelsPerSecond = width / duration; + this.timeline.visibleWidth = width; + this.waveTrack = this.timeline.createTrack( + track, + height, + `wave-${this.trackIdPrefix || 'default'}` + ); + + if ('ontouchstart' in window) { + attachTouchHandlerBodges(this.trackDiv.nativeElement, this.timeline); + } + } + + // TODO can likely be removed, or use waves-ui methods + clearTimeline(): void { + // loop through layers and remove them, waves-ui provides methods for this but it seems to not work properly + const timeContextChildren = this.timeline.timeContext._children; + for (const track of this.timeline.tracks) { + if (track.layers.length === 0) { continue; } + const trackLayers = Array.from(track.layers); + while (trackLayers.length) { + const layer: Layer = trackLayers.pop(); + if (this.layers.includes(layer)) { + track.remove(layer); + this.layers.splice(this.layers.indexOf(layer), 1); + const index = timeContextChildren.indexOf(layer.timeContext); + if (index >= 0) { + timeContextChildren.splice(index, 1); + } + layer.destroy(); + } + } + } + } + + renderWaveform(buffer: AudioBuffer): void { + const height = this.trackDiv.nativeElement.getBoundingClientRect().height; + if (this.timeline && this.waveTrack) { + // resize + const width = this.trackDiv.nativeElement.getBoundingClientRect().width; + + this.clearTimeline(); + this.timeline.visibleWidth = width; + this.timeline.pixelsPerSecond = width / buffer.duration; + this.waveTrack.height = height; + } else { + this.renderTimeline(buffer.duration); + } + this.timeline.timeContext.offset = 0.5 * this.timeline.timeContext.visibleDuration; + + // time axis + const timeAxis = new wavesUI.helpers.TimeAxisLayer({ + height: height, + color: '#b0b0b0' + }); + this.addLayer(timeAxis, this.waveTrack, this.timeline.timeContext, true); + + const nchannels = buffer.numberOfChannels; + const totalWaveHeight = height * 0.9; + const waveHeight = totalWaveHeight / nchannels; + + for (let ch = 0; ch < nchannels; ++ch) { + const waveformLayer = new wavesUI.helpers.WaveformLayer(buffer, { + top: (height - totalWaveHeight) / 2 + waveHeight * ch, + height: waveHeight, + color: '#0868ac', + channel: ch + }); + this.addLayer(waveformLayer, this.waveTrack, this.timeline.timeContext); + } + + this.timeline.state = new wavesUI.states.CenteredZoomState(this.timeline); + this.waveTrack.render(); + this.waveTrack.update(); + this.ref.markForCheck(); + } + + // TODO can likely use methods in waves-ui directly + private addLayer(layer: Layer, + track: Track, + timeContext: any, + isAxis: boolean = false): void { + timeContext.zoom = 1.0; + if (!layer.timeContext) { + layer.setTimeContext(isAxis ? + timeContext : new wavesUI.core.LayerTimeContext(timeContext)); + } + track.add(layer); + this.layers.push(layer); + layer.render(); + layer.update(); + } + + seekStart(): void { + this.zoomOnMouseDown = this.timeline.timeContext.zoom; + this.offsetOnMouseDown = this.timeline.timeContext.offset; + } + + seekEnd(x: number): void { + const hasSameZoom: boolean = this.zoomOnMouseDown === + this.timeline.timeContext.zoom; + const hasSameOffset: boolean = this.offsetOnMouseDown === + this.timeline.timeContext.offset; + if (hasSameZoom && hasSameOffset) { + this.seek(x); + } + } + + seek(x: number): void { + if (this.timeline) { + const timeContext: any = this.timeline.timeContext; + if (this.onSeek) { + this.onSeek(timeContext.timeToPixel.invert(x) - timeContext.offset); + } + } + } +}