diff src/app/waveform/waveform.component.ts @ 319:64dee0c156b1

Some slight refactoring of the list deduction guff and rig up notes layer.
author Lucas Thompson <dev@lucas.im>
date Mon, 15 May 2017 17:57:42 +0100
parents 98490d0ceb77
children 802fbba26afe
line wrap: on
line diff
--- a/src/app/waveform/waveform.component.ts	Mon May 15 14:57:38 2017 +0100
+++ b/src/app/waveform/waveform.component.ts	Mon May 15 17:57:42 2017 +0100
@@ -25,7 +25,7 @@
   MatrixFeature,
   TracksFeature
 } from 'piper/HigherLevelUtilities';
-import {toSeconds} from 'piper';
+import {toSeconds, OutputDescriptor} from 'piper';
 import {FeatureList, Feature} from 'piper/Feature';
 import * as Hammer from 'hammerjs';
 import {WavesSpectrogramLayer} from '../spectrogram/Spectrogram';
@@ -53,6 +53,15 @@
   '#034748' // "deep jungle green"
 ]);
 
+type HigherLevelFeatureShape = 'regions' | 'instants' | 'notes';
+type NoteLikeUnit = 'midi' | 'hz' ;
+interface Note {
+  time: number;
+  duration: number;
+  pitch: number;
+  velocity?: number;
+}
+
 @Component({
   selector: 'ugly-waveform',
   templateUrl: './waveform.component.html',
@@ -754,100 +763,55 @@
         if (featureData.length === 0) {
           return;
         }
-        // TODO look at output descriptor instead of directly inspecting features
-        const hasDuration = outputDescriptor.configured.hasDuration;
-        const isMarker = !hasDuration
-          && outputDescriptor.configured.binCount === 0
-          && featureData[0].featureValues == null;
-        const isRegion = hasDuration
-          && featureData[0].timestamp != null;
-        console.log('Have list features: length ' + featureData.length +
-          ', isMarker ' + isMarker + ', isRegion ' + isRegion +
-          ', hasDuration ' + hasDuration);
+
         // TODO refactor, this is incomprehensible
-        if (isMarker) {
-          const plotData = featureData.map(feature => ({
-            time: toSeconds(feature.timestamp),
-            label: feature.label
-          }));
-          const featureLayer = new wavesUI.helpers.TickLayer(plotData, {
-            height: height,
-            color: colour,
-            labelPosition: 'bottom',
-            shadeSegments: true
-          });
-          this.addLayer(
-            featureLayer,
-            waveTrack,
-            this.timeline.timeContext
+        try {
+          const featureShape = deduceHigherLevelFeatureShape(
+            featureData,
+            outputDescriptor
           );
-        } else if (isRegion) {
-          console.log('Output is of region type');
-          const binCount = outputDescriptor.configured.binCount || 0;
-          const isBarRegion = featureData[0].featureValues.length >= 1 || binCount >= 1 ;
-          const getSegmentArgs = () => {
-            if (isBarRegion) {
-
-              // TODO refactor - this is messy
-              interface FoldsToNumber<T> {
-                reduce(fn: (previousValue: number,
-                            currentValue: T,
-                            currentIndex: number,
-                            array: ArrayLike<T>) => number,
-                       initialValue?: number): number;
-              }
-
-              // TODO potentially change impl., i.e avoid reduce
-              const findMin = <T>(arr: FoldsToNumber<T>, getElement: (x: T) => number): number => {
-                return arr.reduce((min, val) => Math.min(min, getElement(val)), Infinity);
-              };
-
-              const findMax = <T>(arr: FoldsToNumber<T>, getElement: (x: T) => number): number => {
-                return arr.reduce((min, val) => Math.max(min, getElement(val)), -Infinity);
-              };
-
-              const min = findMin<Feature>(featureData, (x: Feature) => {
-                return findMin<number>(x.featureValues, y => y);
+          switch (featureShape) {
+            case 'instants':
+              const plotData = featureData.map(feature => ({
+                time: toSeconds(feature.timestamp),
+                label: feature.label
+              }));
+              const featureLayer = new wavesUI.helpers.TickLayer(plotData, {
+                height: height,
+                color: colour,
+                labelPosition: 'bottom',
+                shadeSegments: true
               });
-
-              const max = findMax<Feature>(featureData, (x: Feature) => {
-                return findMax<number>(x.featureValues, y => y);
-              });
-
-              const barHeight = 1.0 / height;
-              return [
-                featureData.reduce((bars, feature) => {
-                  const staticProperties = {
-                    x: toSeconds(feature.timestamp),
-                    width: toSeconds(feature.duration),
-                    height: min + barHeight,
-                    color: colour,
-                    opacity: 0.8
-                  };
-                  // TODO avoid copying Float32Array to an array - map is problematic here
-                  return bars.concat([...feature.featureValues]
-                    .map(val => Object.assign({}, staticProperties, {y: val})));
-                }, []),
-                {yDomain: [min, max + barHeight], height: height} as any
-              ];
-            } else {
-              return [featureData.map(feature => ({
-                x: toSeconds(feature.timestamp),
-                width: toSeconds(feature.duration),
-                color: colour,
-                opacity: 0.8
-              })), {height: height}];
-            }
-          };
-
-          const segmentLayer = new wavesUI.helpers.SegmentLayer(
-            ...getSegmentArgs()
-          );
-          this.addLayer(
-            segmentLayer,
-            waveTrack,
-            this.timeline.timeContext
-          );
+              this.addLayer(
+                featureLayer,
+                waveTrack,
+                this.timeline.timeContext
+              );
+              break;
+            case 'regions':
+              this.renderRegions(
+                featureData,
+                outputDescriptor,
+                waveTrack,
+                height,
+                colour
+              );
+              break;
+            case 'notes':
+              const pianoRollLayer = new wavesUI.helpers.PianoRollLayer(
+                mapFeaturesToNotes(featureData, outputDescriptor),
+                {height: height, color: colour}
+              );
+              this.addLayer(
+                pianoRollLayer,
+                waveTrack,
+                this.timeline.timeContext
+              );
+              break;
+          }
+        } catch (e) {
+          console.warn(e); // TODO display
+          break;
         }
         break;
       }
@@ -947,6 +911,89 @@
     });
   }
 
+  // TODO not sure how much of the logic in here is actually sensible w.r.t
+  // what it functionally produces
+  private renderRegions(featureData: FeatureList,
+                        outputDescriptor: OutputDescriptor,
+                        waveTrack: any,
+                        height: number,
+                        colour: Colour) {
+    console.log('Output is of region type');
+    const binCount = outputDescriptor.configured.binCount || 0;
+    const isBarRegion = featureData[0].featureValues.length >= 1 || binCount >= 1 ;
+    const getSegmentArgs = () => {
+      if (isBarRegion) {
+
+        // TODO refactor - this is messy
+        interface FoldsToNumber<T> {
+          reduce(fn: (previousValue: number,
+                      currentValue: T,
+                      currentIndex: number,
+                      array: ArrayLike<T>) => number,
+                 initialValue?: number): number;
+        }
+
+        // TODO potentially change impl., i.e avoid reduce
+        const findMin = <T>(arr: FoldsToNumber<T>,
+                            getElement: (x: T) => number): number => {
+          return arr.reduce(
+            (min, val) => Math.min(min, getElement(val)),
+            Infinity
+          );
+        };
+
+        const findMax = <T>(arr: FoldsToNumber<T>,
+                            getElement: (x: T) => number): number => {
+          return arr.reduce(
+            (min, val) => Math.max(min, getElement(val)),
+            -Infinity
+          );
+        };
+
+        const min = findMin<Feature>(featureData, (x: Feature) => {
+          return findMin<number>(x.featureValues, y => y);
+        });
+
+        const max = findMax<Feature>(featureData, (x: Feature) => {
+          return findMax<number>(x.featureValues, y => y);
+        });
+
+        const barHeight = 1.0 / height;
+        return [
+          featureData.reduce((bars, feature) => {
+            const staticProperties = {
+              x: toSeconds(feature.timestamp),
+              width: toSeconds(feature.duration),
+              height: min + barHeight,
+              color: colour,
+              opacity: 0.8
+            };
+            // TODO avoid copying Float32Array to an array - map is problematic here
+            return bars.concat([...feature.featureValues]
+              .map(val => Object.assign({}, staticProperties, {y: val})));
+          }, []),
+          {yDomain: [min, max + barHeight], height: height} as any
+        ];
+      } else {
+        return [featureData.map(feature => ({
+          x: toSeconds(feature.timestamp),
+          width: toSeconds(feature.duration),
+          color: colour,
+          opacity: 0.8
+        })), {height: height}];
+      }
+    };
+
+    const segmentLayer = new wavesUI.helpers.SegmentLayer(
+      ...getSegmentArgs()
+    );
+    this.addLayer(
+      segmentLayer,
+      waveTrack,
+      this.timeline.timeContext
+    );
+  }
+
   private addLayer(layer: Layer, track: Track, timeContext: any, isAxis: boolean = false): void {
     timeContext.zoom = 1.0;
     if (!layer.timeContext) {
@@ -1003,3 +1050,56 @@
     }
   }
 }
+
+function deduceHigherLevelFeatureShape(featureData: FeatureList,
+                                       descriptor: OutputDescriptor)
+: HigherLevelFeatureShape {
+  // TODO look at output descriptor instead of directly inspecting features
+  const hasDuration = descriptor.configured.hasDuration;
+  const binCount = descriptor.configured.binCount;
+  const isMarker = !hasDuration
+    && binCount === 0
+    && featureData[0].featureValues == null;
+
+  const isMaybeNote = getCanonicalNoteLikeUnit(descriptor.configured.unit)
+    && [1, 2].find(nBins => nBins === binCount);
+
+  const isRegionLike = hasDuration && featureData[0].timestamp != null;
+
+  const isNote = isMaybeNote && isRegionLike;
+  const isRegion = !isMaybeNote && isRegionLike;
+  if (isMarker) {
+    return 'instants';
+  }
+  if (isNote) {
+    return 'notes';
+  }
+  if (isRegion) {
+    return 'regions';
+  }
+  throw 'No shape could be deduced';
+}
+
+function getCanonicalNoteLikeUnit(unit: string): NoteLikeUnit | null {
+  const canonicalUnits: NoteLikeUnit[] = ['midi', 'hz'];
+  return canonicalUnits.find(canonicalUnit => {
+    return unit.toLowerCase().indexOf(canonicalUnit) >= 0
+  });
+}
+
+function mapFeaturesToNotes(featureData: FeatureList,
+                            descriptor: OutputDescriptor): Note[] {
+  const canonicalUnit = getCanonicalNoteLikeUnit(descriptor.configured.unit);
+  const isHz = canonicalUnit === 'hz';
+  return featureData.map(feature => ({
+    time: toSeconds(feature.timestamp),
+    duration: toSeconds(feature.duration),
+    pitch: isHz ?
+      frequencyToMidiNote(feature.featureValues[0]) : feature.featureValues[0]
+  }));
+}
+
+function frequencyToMidiNote(frequency: number,
+                             concertA: number = 440.0): number {
+  return 69 + 12 * Math.log2(frequency / concertA);
+}