annotate src/app/visualisations/waves-base.component.ts @ 412:89674c064cda

Set duration on the LayerTimeContext to the length of the underlying audio file.
author Lucas Thompson <dev@lucas.im>
date Mon, 05 Jun 2017 14:25:54 +0100
parents 0554b1af47f6
children de23ea6bcd0d
rev   line source
dev@354 1 /**
dev@354 2 * Created by lucast on 26/05/2017.
dev@354 3 */
dev@384 4 import {AfterViewInit, ElementRef, Input, ViewChild} from '@angular/core';
dev@354 5 import {OnSeekHandler} from '../playhead/PlayHeadHelpers';
dev@354 6 import {attachTouchHandlerBodges} from './WavesJunk';
dev@354 7 import Waves from 'waves-ui-piper';
dev@376 8 import {countingIdProvider} from 'piper/client-stubs/WebWorkerStreamingClient';
dev@383 9 import {ShapedFeatureData} from './FeatureUtilities';
dev@376 10
dev@376 11 const trackIdGenerator = countingIdProvider(0);
dev@354 12
dev@389 13 // has to be an abstract class vs as interface for Angular's DI
dev@389 14 export abstract class VerticallyBounded {
dev@389 15 abstract get range(): [number, number];
dev@392 16 }
dev@392 17
dev@392 18 export abstract class VerticalScaleRenderer extends VerticallyBounded {
dev@389 19 abstract renderScale(range: [number, number]): void;
dev@389 20 }
dev@389 21
dev@392 22 export abstract class VerticalValueInspectorRenderer
dev@392 23 extends VerticalScaleRenderer {
dev@392 24 // TODO how do I know these layers are actually 'describable'?
dev@394 25 abstract renderInspector(range: [number, number], unit?: string): void;
dev@397 26 abstract get updatePosition(): OnSeekHandler;
dev@392 27 }
dev@392 28
dev@406 29 export type LayerRemover = () => void;
dev@406 30
dev@383 31 export abstract class WavesComponent<T extends ShapedFeatureData | AudioBuffer>
dev@383 32 implements AfterViewInit {
dev@384 33 @ViewChild('track') trackContainer: ElementRef;
dev@354 34 @Input() set width(width: number) {
dev@354 35 if (this.timeline) {
dev@354 36 requestAnimationFrame(() => {
dev@354 37 this.timeline.timeContext.visibleWidth = width;
dev@354 38 this.timeline.tracks.update();
dev@354 39 });
dev@354 40 }
dev@354 41 }
dev@354 42 @Input() timeline: Timeline;
dev@354 43 @Input() onSeek: OnSeekHandler;
dev@379 44 @Input() colour: string;
dev@412 45 @Input() duration: number;
dev@383 46 @Input() set feature(feature: T) {
dev@383 47 this.mFeature = feature;
dev@383 48 this.update();
dev@383 49 }
dev@383 50
dev@383 51 get feature(): T {
dev@383 52 return this.mFeature;
dev@383 53 }
dev@354 54
dev@387 55 private layers: Layer[];
dev@387 56 private zoomOnMouseDown: number;
dev@387 57 private offsetOnMouseDown: number;
dev@387 58 private waveTrack: Track;
dev@387 59 private mFeature: T;
dev@387 60 private id: string;
dev@383 61 protected abstract get featureLayers(): Layer[];
dev@392 62 protected cachedFeatureLayers: Layer[];
dev@383 63 protected postAddMap: (value: Layer, index: number, array: Layer[]) => void;
dev@406 64 height: number;
dev@354 65
dev@354 66 constructor() {
dev@354 67 this.layers = [];
dev@376 68 this.id = trackIdGenerator.next().value;
dev@354 69 }
dev@354 70
dev@383 71 ngAfterViewInit(): void {
dev@384 72 this.height =
dev@384 73 this.trackContainer.nativeElement.getBoundingClientRect().height;
dev@386 74 this.renderTimeline();
dev@383 75 this.update();
dev@383 76 }
dev@383 77
dev@386 78 private update(): void {
dev@383 79 if (!this.waveTrack || !this.mFeature) {
dev@383 80 return;
dev@383 81 }
dev@386 82 this.clearTimeline();
dev@392 83 this.cachedFeatureLayers = this.featureLayers;
dev@392 84 for (const layer of this.cachedFeatureLayers) {
dev@389 85 this.addLayer(layer);
dev@383 86 }
dev@383 87 if (this.postAddMap) {
dev@392 88 this.cachedFeatureLayers.forEach(this.postAddMap);
dev@383 89 }
dev@383 90 }
dev@383 91
dev@383 92
dev@386 93 private renderTimeline(): Timeline {
dev@386 94 const track: HTMLElement = this.trackContainer.nativeElement;
dev@354 95 track.innerHTML = '';
dev@383 96 if (this.duration >= 0) {
dev@364 97 const width: number = track.getBoundingClientRect().width;
dev@383 98 this.timeline.pixelsPerSecond = width / this.duration;
dev@364 99 this.timeline.visibleWidth = width;
dev@364 100 }
dev@354 101 this.waveTrack = this.timeline.createTrack(
dev@354 102 track,
dev@383 103 this.height,
dev@376 104 this.id
dev@354 105 );
dev@354 106
dev@354 107 if ('ontouchstart' in window) {
dev@354 108 attachTouchHandlerBodges(
dev@386 109 track,
dev@354 110 this.timeline
dev@354 111 );
dev@354 112 }
dev@383 113 this.resetTimelineState();
dev@354 114 }
dev@354 115
dev@354 116 // TODO can likely be removed, or use waves-ui methods
dev@386 117 private clearTimeline(): void {
dev@354 118 // loop through layers and remove them, waves-ui provides methods for this but it seems to not work properly
dev@406 119 const track = this.waveTrack;
dev@406 120 if (track.layers.length === 0) { return; }
dev@406 121 const trackLayers: Layer[] = Array.from(track.layers as Layer[]);
dev@406 122 while (trackLayers.length) {
dev@406 123 this.removeLayer(trackLayers.pop());
dev@354 124 }
dev@383 125 this.resetTimelineState();
dev@356 126 }
dev@356 127
dev@406 128 private removeLayer(layer: Layer) {
dev@406 129 if (this.layers.includes(layer) && this.waveTrack) {
dev@406 130 const timeContextChildren = this.timeline.timeContext._children;
dev@406 131 this.waveTrack.remove(layer);
dev@406 132 this.layers.splice(this.layers.indexOf(layer), 1);
dev@406 133 const index = timeContextChildren.indexOf(layer.timeContext);
dev@406 134 if (index >= 0) {
dev@406 135 timeContextChildren.splice(index, 1);
dev@406 136 }
dev@406 137 layer.destroy();
dev@406 138 }
dev@406 139 }
dev@406 140
dev@383 141 private resetTimelineState(): void {
dev@356 142 // time axis
dev@356 143 const timeAxis = new Waves.helpers.TimeAxisLayer({
dev@384 144 height: this.height,
dev@356 145 color: '#b0b0b0'
dev@356 146 });
dev@389 147 this.addLayer(timeAxis, true);
dev@356 148 this.timeline.state = new Waves.states.CenteredZoomState(this.timeline);
dev@364 149 this.timeline.tracks.update(); // TODO this is problematic, shared state across components
dev@354 150 }
dev@354 151
dev@354 152
dev@354 153 // TODO can likely use methods in waves-ui directly
dev@406 154 addLayer(layer: Layer,
dev@406 155 isAxis: boolean = false): LayerRemover {
dev@389 156 const timeContext = this.timeline.timeContext;
dev@354 157 if (!layer.timeContext) {
dev@412 158 if (isAxis) {
dev@412 159 layer.setTimeContext(timeContext);
dev@412 160 } else {
dev@412 161 const layerTimeContext = new Waves.core.LayerTimeContext(timeContext);
dev@412 162 if (this.duration) {
dev@412 163 layerTimeContext.duration = this.duration;
dev@412 164 }
dev@412 165 layer.setTimeContext(layerTimeContext);
dev@412 166 }
dev@354 167 }
dev@389 168 this.waveTrack.add(layer);
dev@354 169 this.layers.push(layer);
dev@354 170 layer.render();
dev@354 171 layer.update();
dev@406 172 return () => this.removeLayer(layer);
dev@354 173 }
dev@354 174
dev@354 175 seekStart(): void {
dev@354 176 this.zoomOnMouseDown = this.timeline.timeContext.zoom;
dev@354 177 this.offsetOnMouseDown = this.timeline.timeContext.offset;
dev@354 178 }
dev@354 179
dev@354 180 seekEnd(x: number): void {
dev@354 181 const hasSameZoom: boolean = this.zoomOnMouseDown ===
dev@354 182 this.timeline.timeContext.zoom;
dev@354 183 const hasSameOffset: boolean = this.offsetOnMouseDown ===
dev@354 184 this.timeline.timeContext.offset;
dev@354 185 if (hasSameZoom && hasSameOffset) {
dev@354 186 this.seek(x);
dev@354 187 }
dev@354 188 }
dev@354 189
dev@354 190 seek(x: number): void {
dev@354 191 if (this.timeline) {
dev@354 192 const timeContext: any = this.timeline.timeContext;
dev@354 193 if (this.onSeek) {
dev@354 194 this.onSeek(timeContext.timeToPixel.invert(x) - timeContext.offset);
dev@354 195 }
dev@354 196 }
dev@354 197 }
dev@354 198 }
dev@389 199
dev@389 200 export abstract class VerticallyBoundedWavesComponent
dev@389 201 <T extends ShapedFeatureData> extends WavesComponent<T>
dev@392 202 implements VerticalScaleRenderer {
dev@389 203 abstract range: [number, number];
dev@389 204
dev@389 205 renderScale(range: [number, number]): void {
dev@389 206 this.addLayer(new Waves.helpers.ScaleLayer({
dev@389 207 tickColor: this.colour,
dev@389 208 textColor: this.colour,
dev@389 209 height: this.height,
dev@389 210 yDomain: range
dev@389 211 }));
dev@389 212 }
dev@389 213 }
dev@392 214
dev@392 215 export abstract class InspectableVerticallyBoundedComponent
dev@392 216 <T extends ShapedFeatureData> extends VerticallyBoundedWavesComponent<T>
dev@392 217 implements VerticalValueInspectorRenderer {
dev@392 218
dev@392 219 private wrappedSeekHandler: OnSeekHandler;
dev@392 220 private highlight: HighlightLayer;
dev@392 221
dev@392 222 @Input() set onSeek(handler: OnSeekHandler) {
dev@392 223 this.wrappedSeekHandler = (x: number) => {
dev@392 224 handler(x);
dev@397 225 this.updatePosition(x);
dev@397 226 };
dev@397 227 }
dev@397 228
dev@397 229 get updatePosition() {
dev@397 230 return (currentTime: number): void => {
dev@394 231 if (this.highlight) {
dev@397 232 this.highlight.currentPosition = currentTime;
dev@394 233 this.highlight.update();
dev@394 234 }
dev@392 235 };
dev@392 236 }
dev@392 237
dev@392 238 get onSeek(): OnSeekHandler {
dev@392 239 return this.wrappedSeekHandler;
dev@392 240 }
dev@392 241
dev@392 242
dev@394 243 renderInspector(range: [number, number], unit?: string): void {
dev@394 244 if (range) {
dev@394 245 this.highlight = new Waves.helpers.HighlightLayer(
dev@394 246 this.cachedFeatureLayers,
dev@394 247 {
dev@394 248 opacity: 0.7,
dev@394 249 height: this.height,
dev@394 250 color: '#c33c54', // TODO pass in?
dev@394 251 labelOffset: 38,
dev@394 252 yDomain: range,
dev@396 253 unit: unit || ''
dev@394 254 }
dev@394 255 );
dev@394 256 this.addLayer(this.highlight);
dev@394 257 }
dev@392 258 }
dev@392 259 }