annotate 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
rev   line source
dev@347 1 import {
dev@347 2 Component,
dev@347 3 ViewChild,
dev@347 4 ElementRef,
dev@347 5 Input,
dev@347 6 ChangeDetectorRef
dev@347 7 } from '@angular/core';
dev@347 8 import wavesUI from 'waves-ui-piper';
dev@347 9 import {attachTouchHandlerBodges} from '../WavesJunk';
dev@347 10 import {OnSeekHandler} from '../../playhead/PlayHeadHelpers';
dev@347 11
dev@347 12 type Layer = any;
dev@347 13 type Track = any;
dev@347 14
dev@347 15 @Component({
dev@347 16 selector: 'ugly-waveform',
dev@347 17 templateUrl: './waveform.component.html',
dev@347 18 styleUrls: ['./waveform.component.css']
dev@347 19 })
dev@347 20 export class WaveformComponent {
dev@347 21
dev@347 22 @ViewChild('track') trackDiv: ElementRef;
dev@347 23 @Input() set width(width: number) {
dev@347 24 if (this.timeline) {
dev@347 25 requestAnimationFrame(() => {
dev@347 26 this.timeline.timeContext.visibleWidth = width;
dev@347 27 this.timeline.tracks.update();
dev@347 28 });
dev@347 29 }
dev@347 30 }
dev@347 31 @Input() timeline: Timeline;
dev@347 32 @Input() trackIdPrefix: string;
dev@347 33 @Input() onSeek: OnSeekHandler;
dev@347 34
dev@347 35 @Input() set audioBuffer(buffer: AudioBuffer) {
dev@347 36 this._audioBuffer = buffer || undefined;
dev@347 37 if (this.audioBuffer) {
dev@347 38 this.renderWaveform(this.audioBuffer);
dev@347 39 }
dev@347 40 }
dev@347 41
dev@347 42 get audioBuffer(): AudioBuffer {
dev@347 43 return this._audioBuffer;
dev@347 44 }
dev@347 45
dev@347 46 private _audioBuffer: AudioBuffer;
dev@347 47 private layers: Layer[];
dev@347 48 private zoomOnMouseDown: number;
dev@347 49 private offsetOnMouseDown: number;
dev@347 50 private waveTrack: Track;
dev@347 51
dev@347 52 constructor(private ref: ChangeDetectorRef) {
dev@347 53 this.layers = [];
dev@347 54 }
dev@347 55
dev@347 56 renderTimeline(duration: number = 1.0): Timeline {
dev@347 57 const track: HTMLElement = this.trackDiv.nativeElement;
dev@347 58 track.innerHTML = '';
dev@347 59 const height: number = track.getBoundingClientRect().height;
dev@347 60 const width: number = track.getBoundingClientRect().width;
dev@347 61 this.timeline.pixelsPerSecond = width / duration;
dev@347 62 this.timeline.visibleWidth = width;
dev@347 63 this.waveTrack = this.timeline.createTrack(
dev@347 64 track,
dev@347 65 height,
dev@347 66 `wave-${this.trackIdPrefix || 'default'}`
dev@347 67 );
dev@347 68
dev@347 69 if ('ontouchstart' in window) {
dev@347 70 attachTouchHandlerBodges(this.trackDiv.nativeElement, this.timeline);
dev@347 71 }
dev@347 72 }
dev@347 73
dev@347 74 // TODO can likely be removed, or use waves-ui methods
dev@347 75 clearTimeline(): void {
dev@347 76 // loop through layers and remove them, waves-ui provides methods for this but it seems to not work properly
dev@347 77 const timeContextChildren = this.timeline.timeContext._children;
dev@347 78 for (const track of this.timeline.tracks) {
dev@347 79 if (track.layers.length === 0) { continue; }
dev@347 80 const trackLayers = Array.from(track.layers);
dev@347 81 while (trackLayers.length) {
dev@347 82 const layer: Layer = trackLayers.pop();
dev@347 83 if (this.layers.includes(layer)) {
dev@347 84 track.remove(layer);
dev@347 85 this.layers.splice(this.layers.indexOf(layer), 1);
dev@347 86 const index = timeContextChildren.indexOf(layer.timeContext);
dev@347 87 if (index >= 0) {
dev@347 88 timeContextChildren.splice(index, 1);
dev@347 89 }
dev@347 90 layer.destroy();
dev@347 91 }
dev@347 92 }
dev@347 93 }
dev@347 94 }
dev@347 95
dev@347 96 renderWaveform(buffer: AudioBuffer): void {
dev@347 97 const height = this.trackDiv.nativeElement.getBoundingClientRect().height;
dev@347 98 if (this.timeline && this.waveTrack) {
dev@347 99 // resize
dev@347 100 const width = this.trackDiv.nativeElement.getBoundingClientRect().width;
dev@347 101
dev@347 102 this.clearTimeline();
dev@347 103 this.timeline.visibleWidth = width;
dev@347 104 this.timeline.pixelsPerSecond = width / buffer.duration;
dev@347 105 this.waveTrack.height = height;
dev@347 106 } else {
dev@347 107 this.renderTimeline(buffer.duration);
dev@347 108 }
dev@347 109 this.timeline.timeContext.offset = 0.5 * this.timeline.timeContext.visibleDuration;
dev@347 110
dev@347 111 // time axis
dev@347 112 const timeAxis = new wavesUI.helpers.TimeAxisLayer({
dev@347 113 height: height,
dev@347 114 color: '#b0b0b0'
dev@347 115 });
dev@347 116 this.addLayer(timeAxis, this.waveTrack, this.timeline.timeContext, true);
dev@347 117
dev@347 118 const nchannels = buffer.numberOfChannels;
dev@347 119 const totalWaveHeight = height * 0.9;
dev@347 120 const waveHeight = totalWaveHeight / nchannels;
dev@347 121
dev@347 122 for (let ch = 0; ch < nchannels; ++ch) {
dev@347 123 const waveformLayer = new wavesUI.helpers.WaveformLayer(buffer, {
dev@347 124 top: (height - totalWaveHeight) / 2 + waveHeight * ch,
dev@347 125 height: waveHeight,
dev@347 126 color: '#0868ac',
dev@347 127 channel: ch
dev@347 128 });
dev@347 129 this.addLayer(waveformLayer, this.waveTrack, this.timeline.timeContext);
dev@347 130 }
dev@347 131
dev@347 132 this.timeline.state = new wavesUI.states.CenteredZoomState(this.timeline);
dev@347 133 this.waveTrack.render();
dev@347 134 this.waveTrack.update();
dev@347 135 this.ref.markForCheck();
dev@347 136 }
dev@347 137
dev@347 138 // TODO can likely use methods in waves-ui directly
dev@347 139 private addLayer(layer: Layer,
dev@347 140 track: Track,
dev@347 141 timeContext: any,
dev@347 142 isAxis: boolean = false): void {
dev@347 143 timeContext.zoom = 1.0;
dev@347 144 if (!layer.timeContext) {
dev@347 145 layer.setTimeContext(isAxis ?
dev@347 146 timeContext : new wavesUI.core.LayerTimeContext(timeContext));
dev@347 147 }
dev@347 148 track.add(layer);
dev@347 149 this.layers.push(layer);
dev@347 150 layer.render();
dev@347 151 layer.update();
dev@347 152 }
dev@347 153
dev@347 154 seekStart(): void {
dev@347 155 this.zoomOnMouseDown = this.timeline.timeContext.zoom;
dev@347 156 this.offsetOnMouseDown = this.timeline.timeContext.offset;
dev@347 157 }
dev@347 158
dev@347 159 seekEnd(x: number): void {
dev@347 160 const hasSameZoom: boolean = this.zoomOnMouseDown ===
dev@347 161 this.timeline.timeContext.zoom;
dev@347 162 const hasSameOffset: boolean = this.offsetOnMouseDown ===
dev@347 163 this.timeline.timeContext.offset;
dev@347 164 if (hasSameZoom && hasSameOffset) {
dev@347 165 this.seek(x);
dev@347 166 }
dev@347 167 }
dev@347 168
dev@347 169 seek(x: number): void {
dev@347 170 if (this.timeline) {
dev@347 171 const timeContext: any = this.timeline.timeContext;
dev@347 172 if (this.onSeek) {
dev@347 173 this.onSeek(timeContext.timeToPixel.invert(x) - timeContext.offset);
dev@347 174 }
dev@347 175 }
dev@347 176 }
dev@347 177 }