diff src/app/visualisations/FeatureUtilities.ts @ 349:bf038a51f7e3

Restore some of the feature related logic from waveform.component into a utilities module. Introduce some additional types for representing reshaped features. A work in progress.
author Lucas Thompson <dev@lucas.im>
date Thu, 25 May 2017 17:57:03 +0100
parents
children a8a6e8a4ec70
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/visualisations/FeatureUtilities.ts	Thu May 25 17:57:03 2017 +0100
@@ -0,0 +1,170 @@
+/**
+ * Created by lucast on 24/05/2017.
+ */
+
+import {FeatureList} from 'piper/Feature';
+import {OutputDescriptor, toSeconds} from 'piper';
+import {
+  MatrixFeature,
+  SimpleResponse,
+  TracksFeature,
+  VectorFeature
+} from 'piper/HigherLevelUtilities';
+
+
+export type NoteLikeUnit = 'midi' | 'hz' ;
+export interface Note {
+  time: number;
+  duration: number;
+  pitch: number;
+  velocity?: number;
+}
+
+export function getCanonicalNoteLikeUnit(unit: string): NoteLikeUnit | null {
+  const canonicalUnits: NoteLikeUnit[] = ['midi', 'hz'];
+  return canonicalUnits.find(canonicalUnit => {
+    return unit.toLowerCase().indexOf(canonicalUnit) >= 0;
+  });
+}
+
+export 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]
+  }));
+}
+
+export function frequencyToMidiNote(frequency: number,
+                                    concertA: number = 440.0): number {
+  return 69 + 12 * Math.log2(frequency / concertA);
+}
+
+export function* createColourGenerator(colours) {
+  let index = 0;
+  const nColours = colours.length;
+  while (true) {
+    yield colours[index = ++index % nColours];
+  }
+}
+
+export const defaultColourGenerator = createColourGenerator([
+  '#0868ac', // "sapphire blue", our waveform / header colour
+  '#c33c54', // "brick red"
+  '#17bebb', // "tiffany blue"
+  '#001021', // "rich black"
+  '#fa8334', // "mango tango"
+  '#034748' // "deep jungle green"
+]);
+
+// TODO this might belong somewhere else, or perhaps the stuff above ^^ does
+
+export interface Instant {
+  time: number;
+  label: string;
+}
+
+type CollectedShape = 'vector' | 'matrix' | 'tracks';
+
+// TODO regions
+type ShapeDeducedFromList = 'instants' | 'notes';
+export type HigherLevelFeatureShape = CollectedShape | ShapeDeducedFromList;
+
+export type ShapedFeatureData = VectorFeature
+  | MatrixFeature
+  | TracksFeature
+  | Note[]
+  | Instant[];
+
+// These needn't be classes (could just be interfaces), just experimenting
+export abstract class ShapedFeature<Shape extends HigherLevelFeatureShape,
+  Data extends ShapedFeatureData> {
+  shape: Shape;
+  collected: Data;
+}
+
+export class Vector extends ShapedFeature<'vector', VectorFeature> {}
+export class Matrix extends ShapedFeature<'matrix', MatrixFeature> {}
+export class Tracks extends ShapedFeature<'tracks', TracksFeature> {}
+export class Notes extends ShapedFeature<'notes', Note[]> {}
+export class Instants extends ShapedFeature<'instants', Instant[]> {}
+export type KnownShapedFeature = Vector
+  | Matrix
+  | Tracks
+  | Notes
+  | Instants;
+
+function hasKnownShapeOtherThanList(shape: string): shape is CollectedShape {
+  return ['vector', 'matrix', 'tracks'].includes(shape);
+}
+
+const throwShapeError = () => { throw new Error('No shape could be deduced'); };
+function deduceHigherLevelFeatureShape(response: SimpleResponse)
+: HigherLevelFeatureShape {
+  const collection = response.features;
+  const descriptor = response.outputDescriptor;
+  if (hasKnownShapeOtherThanList(collection.shape)) {
+    return collection.shape;
+  }
+
+
+  // TODO it's a shame that the types in piper don't make this easy for the
+  // compiler to deduce
+  if (collection.shape !== 'list' && collection.collected instanceof Array) {
+    throwShapeError();
+  }
+
+  const featureData = collection.collected as FeatureList;
+  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);
+
+  // TODO any need to be directly inspecting features?
+  const isRegionLike = hasDuration && featureData[0].timestamp != null;
+
+  const isNote = isMaybeNote && isRegionLike;
+  if (isMarker) {
+    return 'instants';
+  }
+  if (isNote) {
+    return 'notes';
+  }
+  throwShapeError();
+}
+
+export function toKnownShape(response: SimpleResponse): KnownShapedFeature {
+  const deducedShape = deduceHigherLevelFeatureShape(response);
+  switch (deducedShape) {
+    case 'vector':
+      return response.features as Vector;
+    case 'matrix':
+      return response.features as Matrix;
+    case 'notes':
+      return {
+        shape: deducedShape,
+        collected: mapFeaturesToNotes(
+          response.features.collected as FeatureList,
+          response.outputDescriptor
+        )
+      };
+    case 'instants':
+      const featureData = response.features.collected as FeatureList;
+      return {
+        shape: deducedShape,
+        collected: featureData.map(feature => ({
+          time: toSeconds(feature.timestamp),
+          label: feature.label
+        }))
+      };
+  }
+  throwShapeError();
+}