changeset 196:aa1c92c553cb

A few different @Input flags allowing for using component for just a waveform or features or both, turning off seeking and allowing more than one feature to be extracted to the component. Very messy, desperately needs refactoring.
author Lucas Thompson <dev@lucas.im>
date Fri, 24 Mar 2017 11:00:54 +0000
parents 3ba03d9f0059
children 7224d9f990cf
files src/app/waveform/waveform.component.ts
diffstat 1 files changed, 256 insertions(+), 128 deletions(-) [+]
line wrap: on
line diff
--- a/src/app/waveform/waveform.component.ts	Thu Mar 23 20:16:00 2017 +0000
+++ b/src/app/waveform/waveform.component.ts	Fri Mar 24 11:00:54 2017 +0000
@@ -2,7 +2,10 @@
   Component, OnInit, ViewChild, ElementRef, Input, AfterViewInit, NgZone,
   OnDestroy
 } from '@angular/core';
-import {AudioPlayerService} from "../services/audio-player/audio-player.service";
+import {
+  AudioPlayerService, AudioResource,
+  AudioResourceError
+} from "../services/audio-player/audio-player.service";
 import wavesUI from 'waves-ui';
 import {
   FeatureExtractionService
@@ -32,11 +35,112 @@
 
   @Input() timeline: Timeline;
   @Input() trackIdPrefix: string;
-  private _audioBuffer: AudioBuffer;
-  private cursorLayer: any;
-  private layers: Layer[];
+  @Input() set isSubscribedToExtractionService(isSubscribed: boolean) {
+    if (isSubscribed) {
+      if (this.featureExtractionSubscription) {
+        return;
+      }
 
-  @Input()
+      const colours = function* () {
+        const circularColours = [
+          'black',
+          'red',
+          'green',
+          'purple',
+          'orange'
+        ];
+        let index = 0;
+        const nColours = circularColours.length;
+        while (true) {
+          yield circularColours[index = ++index % nColours];
+        }
+      }();
+
+      this.featureExtractionSubscription =
+        this.piperService.featuresExtracted$.subscribe(
+          features => {
+            this.renderFeatures(features, colours.next().value);
+          });
+    } else {
+      if (this.featureExtractionSubscription) {
+        this.featureExtractionSubscription.unsubscribe();
+      }
+    }
+  }
+  @Input() set isSubscribedToAudioService(isSubscribed: boolean) {
+    this._isSubscribedToAudioService = isSubscribed;
+    if (isSubscribed) {
+      if (this.onAudioDataSubscription) {
+        return;
+      }
+
+      this.onAudioDataSubscription =
+        this.audioService.audioLoaded$.subscribe(res => {
+          const wasError = (res as AudioResourceError).message != null;
+
+          if (wasError) {
+            console.warn('No audio, display error?');
+          } else {
+            this.audioBuffer = (res as AudioResource).samples;
+          }
+        });
+    } else {
+      if (this.onAudioDataSubscription) {
+        this.onAudioDataSubscription.unsubscribe();
+      }
+    }
+  }
+
+  get isSubscribedToAudioService(): boolean {
+    return this._isSubscribedToAudioService;
+  }
+
+  @Input() set isOneShotExtractor(isOneShot: boolean) {
+    this._isOneShotExtractor = isOneShot;
+  }
+
+  get isOneShotExtractor(): boolean {
+    return this._isOneShotExtractor;
+  }
+
+  @Input() set isSeeking(isSeeking: boolean) {
+    this._isSeeking = isSeeking;
+    if (isSeeking) {
+      if (this.seekedSubscription) {
+        return;
+      }
+      if(this.playingStateSubscription) {
+        return;
+      }
+
+      this.seekedSubscription = this.audioService.seeked$.subscribe(() => {
+        if (!this.isPlaying)
+          this.animate();
+      });
+      this.playingStateSubscription =
+        this.audioService.playingStateChange$.subscribe(
+          isPlaying => {
+            this.isPlaying = isPlaying;
+            if (this.isPlaying)
+              this.animate();
+          });
+    } else {
+      if (this.isPlaying) {
+        this.isPlaying = false;
+      }
+      if (this.playingStateSubscription) {
+        this.playingStateSubscription.unsubscribe();
+      }
+      if (this.seekedSubscription) {
+        this.seekedSubscription.unsubscribe();
+      }
+    }
+  }
+
+  get isSeeking(): boolean {
+    return this._isSeeking;
+  }
+
   set audioBuffer(buffer: AudioBuffer) {
     this._audioBuffer = buffer || undefined;
     if (this.audioBuffer) {
@@ -49,53 +153,36 @@
     return this._audioBuffer;
   }
 
+  private _audioBuffer: AudioBuffer;
+  private _isSubscribedToAudioService: boolean;
+  private _isOneShotExtractor: boolean;
+  private _isSeeking: boolean;
+  private cursorLayer: any;
+  private layers: Layer[];
   private featureExtractionSubscription: Subscription;
   private playingStateSubscription: Subscription;
   private seekedSubscription: Subscription;
+  private onAudioDataSubscription: Subscription;
   private isPlaying: boolean;
   private offsetAtPanStart: number;
   private initialZoom: number;
   private initialDistance: number;
   private zoomOnMouseDown: number;
   private offsetOnMouseDown: number;
+  private hasShot: boolean;
+  private isLoading: boolean;
 
   constructor(private audioService: AudioPlayerService,
               private piperService: FeatureExtractionService,
               public ngZone: NgZone) {
+    this.isSubscribedToAudioService = true;
+    this.isSeeking = true;
     this.layers = [];
-    this._audioBuffer = undefined;
+    this.audioBuffer = undefined;
     this.timeline = undefined;
     this.cursorLayer = undefined;
     this.isPlaying = false;
-    const colours = function* () {
-      const circularColours = [
-        'black',
-        'red',
-        'green',
-        'purple',
-        'orange'
-      ];
-      let index = 0;
-      const nColours = circularColours.length;
-      while (true) {
-        yield circularColours[index = ++index % nColours];
-      }
-    }();
-
-    this.featureExtractionSubscription = piperService.featuresExtracted$.subscribe(
-      features => {
-        this.renderFeatures(features, colours.next().value);
-      });
-    this.playingStateSubscription = audioService.playingStateChange$.subscribe(
-      isPlaying => {
-        this.isPlaying = isPlaying;
-        if (this.isPlaying)
-          this.animate();
-      });
-    this.seekedSubscription = audioService.seeked$.subscribe(() => {
-      if (!this.isPlaying)
-        this.animate();
-    });
+    this.isLoading = true;
   }
 
   ngOnInit() {
@@ -103,22 +190,126 @@
 
   ngAfterViewInit(): void {
     this.trackIdPrefix = this.trackIdPrefix || "default";
-    this.renderTimeline();
+    if (this.timeline) {
+      this.renderTimeline(null, true, true);
+    } else {
+      this.renderTimeline();
+    }
   }
 
-  renderTimeline(duration: number = 1.0): Timeline {
+  renderTimeline(duration: number = 1.0,
+                 useExistingDuration: boolean = false,
+                 isInitialRender: boolean = false): Timeline {
     const track: HTMLElement = this.trackDiv.nativeElement;
     track.innerHTML = "";
     const height: number = track.getBoundingClientRect().height;
     const width: number = track.getBoundingClientRect().width;
     const pixelsPerSecond = width / duration;
-    if (this.timeline instanceof wavesUI.core.Timeline) {
-      this.timeline.pixelsPerSecond = pixelsPerSecond;
-      this.timeline.visibleWidth = width;
+    const hasExistingTimeline = this.timeline instanceof wavesUI.core.Timeline;
+
+    if (hasExistingTimeline) {
+      if (!useExistingDuration) {
+        this.timeline.pixelsPerSecond = pixelsPerSecond;
+        this.timeline.visibleWidth = width;
+      }
     } else {
       this.timeline = new wavesUI.core.Timeline(pixelsPerSecond, width);
     }
-    this.timeline.createTrack(track, height, `wave-${this.trackIdPrefix}`);
+    const waveTrack = this.timeline.createTrack(
+      track,
+      height,
+      `wave-${this.trackIdPrefix}`
+    );
+    if (isInitialRender && hasExistingTimeline) {
+      // time axis
+      const timeAxis = new wavesUI.helpers.TimeAxisLayer({
+        height: height,
+        color: '#b0b0b0'
+      });
+      this.addLayer(timeAxis, waveTrack, this.timeline.timeContext, true);
+      this.cursorLayer = new wavesUI.helpers.CursorLayer({
+        height: height
+      });
+      this.addLayer(this.cursorLayer, waveTrack, this.timeline.timeContext);
+    }
+    if ('ontouchstart' in window) {
+      interface Point {
+        x: number;
+        y: number;
+      }
+
+      let zoomGestureJustEnded: boolean = false;
+
+      const pixelToExponent: Function = wavesUI.utils.scales.linear()
+        .domain([0, 100]) // 100px => factor 2
+        .range([0, 1]);
+
+      const calculateDistance: (p1: Point, p2: Point) => number = (p1, p2) => {
+        return Math.pow(
+          Math.pow(p2.x - p1.x, 2) +
+          Math.pow(p2.y - p1.y, 2), 0.5);
+      };
+
+      const hammertime = new Hammer(this.trackDiv.nativeElement);
+      const scroll = (ev) => {
+        if (zoomGestureJustEnded) {
+          zoomGestureJustEnded = false;
+          console.log("Skip this event: likely a single touch dangling from pinch");
+          return;
+        }
+        this.timeline.timeContext.offset = this.offsetAtPanStart +
+          this.timeline.timeContext.timeToPixel.invert(ev.deltaX);
+        this.timeline.tracks.update();
+      };
+
+      const zoom = (ev) => {
+        const minZoom = this.timeline.state.minZoom;
+        const maxZoom = this.timeline.state.maxZoom;
+        const distance = calculateDistance({
+          x: ev.pointers[0].clientX,
+          y: ev.pointers[0].clientY
+        }, {
+          x: ev.pointers[1].clientX,
+          y: ev.pointers[1].clientY
+        });
+
+        const lastCenterTime =
+          this.timeline.timeContext.timeToPixel.invert(ev.center.x);
+
+        const exponent = pixelToExponent(distance - this.initialDistance);
+        const targetZoom = this.initialZoom * Math.pow(2, exponent);
+
+        this.timeline.timeContext.zoom =
+          Math.min(Math.max(targetZoom, minZoom), maxZoom);
+
+        const newCenterTime =
+          this.timeline.timeContext.timeToPixel.invert(ev.center.x);
+
+        this.timeline.timeContext.offset += newCenterTime - lastCenterTime;
+        this.timeline.tracks.update();
+      };
+      hammertime.get('pinch').set({ enable: true });
+      hammertime.on('panstart', () => {
+        this.offsetAtPanStart = this.timeline.timeContext.offset;
+      });
+      hammertime.on('panleft', scroll);
+      hammertime.on('panright', scroll);
+      hammertime.on('pinchstart', (e) => {
+        this.initialZoom = this.timeline.timeContext.zoom;
+
+        this.initialDistance = calculateDistance({
+          x: e.pointers[0].clientX,
+          y: e.pointers[0].clientY
+        }, {
+          x: e.pointers[1].clientX,
+          y: e.pointers[1].clientY
+        });
+      });
+      hammertime.on('pinch', zoom);
+      hammertime.on('pinchend', () => {
+        zoomGestureJustEnded = true;
+      });
+    }
     // this.timeline.createTrack(track, height/2, `wave-${this.trackIdPrefix}`);
     // this.timeline.createTrack(track, height/2, `grid-${this.trackIdPrefix}`);
   }
@@ -308,86 +499,7 @@
     waveTrack.render();
     waveTrack.update();
 
-
-    if ('ontouchstart' in window) {
-      interface Point {
-        x: number;
-        y: number;
-      }
-
-      let zoomGestureJustEnded: boolean = false;
-
-      const pixelToExponent: Function = wavesUI.utils.scales.linear()
-        .domain([0, 100]) // 100px => factor 2
-        .range([0, 1]);
-
-      const calculateDistance: (p1: Point, p2: Point) => number = (p1, p2) => {
-        return Math.pow(
-          Math.pow(p2.x - p1.x, 2) +
-          Math.pow(p2.y - p1.y, 2), 0.5);
-      };
-
-      const hammertime = new Hammer(this.trackDiv.nativeElement);
-      const scroll = (ev) => {
-        if (zoomGestureJustEnded) {
-          zoomGestureJustEnded = false;
-          console.log("Skip this event: likely a single touch dangling from pinch");
-          return;
-        }
-        this.timeline.timeContext.offset = this.offsetAtPanStart +
-          this.timeline.timeContext.timeToPixel.invert(ev.deltaX);
-        this.timeline.tracks.update();
-      };
-
-      const zoom = (ev) => {
-        const minZoom = this.timeline.state.minZoom;
-        const maxZoom = this.timeline.state.maxZoom;
-        const distance = calculateDistance({
-          x: ev.pointers[0].clientX,
-          y: ev.pointers[0].clientY
-        }, {
-          x: ev.pointers[1].clientX,
-          y: ev.pointers[1].clientY
-        });
-
-        const lastCenterTime =
-          this.timeline.timeContext.timeToPixel.invert(ev.center.x);
-
-        const exponent = pixelToExponent(distance - this.initialDistance);
-        const targetZoom = this.initialZoom * Math.pow(2, exponent);
-
-        this.timeline.timeContext.zoom =
-          Math.min(Math.max(targetZoom, minZoom), maxZoom);
-
-        const newCenterTime =
-          this.timeline.timeContext.timeToPixel.invert(ev.center.x);
-
-        this.timeline.timeContext.offset += newCenterTime - lastCenterTime;
-        this.timeline.tracks.update();
-      };
-      hammertime.get('pinch').set({ enable: true });
-      hammertime.on('panstart', () => {
-        this.offsetAtPanStart = this.timeline.timeContext.offset;
-      });
-      hammertime.on('panleft', scroll);
-      hammertime.on('panright', scroll);
-      hammertime.on('pinchstart', (e) => {
-        this.initialZoom = this.timeline.timeContext.zoom;
-
-        this.initialDistance = calculateDistance({
-          x: e.pointers[0].clientX,
-          y: e.pointers[0].clientY
-        }, {
-          x: e.pointers[1].clientX,
-          y: e.pointers[1].clientY
-        });
-      });
-      hammertime.on('pinch', zoom);
-      hammertime.on('pinchend', () => {
-        zoomGestureJustEnded = true;
-      });
-    }
-
+    this.isLoading = false;
     this.animate();
   }
 
@@ -410,11 +522,17 @@
 
   // TODO refactor - this doesn't belong here
   private renderFeatures(extracted: SimpleResponse, colour: Colour): void {
+    if (this.isOneShotExtractor && !this.hasShot) {
+      this.featureExtractionSubscription.unsubscribe();
+      this.hasShot = true;
+    }
+
     if (!extracted.hasOwnProperty('features') || !extracted.hasOwnProperty('outputDescriptor')) return;
     if (!extracted.features.hasOwnProperty('shape') || !extracted.features.hasOwnProperty('data')) return;
     const features: FeatureCollection = (extracted.features as FeatureCollection);
     const outputDescriptor = extracted.outputDescriptor;
-    const height = this.trackDiv.nativeElement.getBoundingClientRect().height / 2;
+    // const height = this.trackDiv.nativeElement.getBoundingClientRect().height / 2;
+    const height = this.trackDiv.nativeElement.getBoundingClientRect().height;
     const waveTrack = this.timeline.getTrackById(`wave-${this.trackIdPrefix}`);
 
     // TODO refactor all of this
@@ -584,10 +702,13 @@
                     features.shape + "'");
     }
 
+    this.isLoading = false;
     this.timeline.tracks.update();
   }
 
   private animate(): void {
+    if (!this.isSeeking) return;
+
     this.ngZone.runOutsideAngular(() => {
       // listen for time passing...
       const updateSeekingCursor = () => {
@@ -658,9 +779,14 @@
   }
 
   ngOnDestroy(): void {
-    this.featureExtractionSubscription.unsubscribe();
-    this.playingStateSubscription.unsubscribe();
-    this.seekedSubscription.unsubscribe();
+    if (this.featureExtractionSubscription)
+      this.featureExtractionSubscription.unsubscribe();
+    if (this.playingStateSubscription)
+      this.playingStateSubscription.unsubscribe();
+    if (this.seekedSubscription)
+      this.seekedSubscription.unsubscribe();
+    if (this.onAudioDataSubscription)
+      this.onAudioDataSubscription.unsubscribe();
   }
 
   seekStart(): void {
@@ -681,9 +807,11 @@
   seek(x: number): void {
     if (this.timeline) {
       const timeContext: any = this.timeline.timeContext;
-      this.audioService.seekTo(
-        timeContext.timeToPixel.invert(x)- timeContext.offset
-      );
+      if (this.isSeeking) {
+        this.audioService.seekTo(
+          timeContext.timeToPixel.invert(x)- timeContext.offset
+        );
+      }
     }
   }
 }