annotate 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
rev   line source
dev@349 1 /**
dev@349 2 * Created by lucast on 24/05/2017.
dev@349 3 */
dev@349 4
dev@349 5 import {FeatureList} from 'piper/Feature';
dev@349 6 import {OutputDescriptor, toSeconds} from 'piper';
dev@349 7 import {
dev@349 8 MatrixFeature,
dev@349 9 SimpleResponse,
dev@349 10 TracksFeature,
dev@349 11 VectorFeature
dev@349 12 } from 'piper/HigherLevelUtilities';
dev@349 13
dev@349 14
dev@349 15 export type NoteLikeUnit = 'midi' | 'hz' ;
dev@349 16 export interface Note {
dev@349 17 time: number;
dev@349 18 duration: number;
dev@349 19 pitch: number;
dev@349 20 velocity?: number;
dev@349 21 }
dev@349 22
dev@349 23 export function getCanonicalNoteLikeUnit(unit: string): NoteLikeUnit | null {
dev@349 24 const canonicalUnits: NoteLikeUnit[] = ['midi', 'hz'];
dev@349 25 return canonicalUnits.find(canonicalUnit => {
dev@349 26 return unit.toLowerCase().indexOf(canonicalUnit) >= 0;
dev@349 27 });
dev@349 28 }
dev@349 29
dev@349 30 export function mapFeaturesToNotes(featureData: FeatureList,
dev@349 31 descriptor: OutputDescriptor): Note[] {
dev@349 32 const canonicalUnit = getCanonicalNoteLikeUnit(descriptor.configured.unit);
dev@349 33 const isHz = canonicalUnit === 'hz';
dev@349 34 return featureData.map(feature => ({
dev@349 35 time: toSeconds(feature.timestamp),
dev@349 36 duration: toSeconds(feature.duration),
dev@349 37 pitch: isHz ?
dev@349 38 frequencyToMidiNote(feature.featureValues[0]) : feature.featureValues[0]
dev@349 39 }));
dev@349 40 }
dev@349 41
dev@349 42 export function frequencyToMidiNote(frequency: number,
dev@349 43 concertA: number = 440.0): number {
dev@349 44 return 69 + 12 * Math.log2(frequency / concertA);
dev@349 45 }
dev@349 46
dev@349 47 export function* createColourGenerator(colours) {
dev@349 48 let index = 0;
dev@349 49 const nColours = colours.length;
dev@349 50 while (true) {
dev@349 51 yield colours[index = ++index % nColours];
dev@349 52 }
dev@349 53 }
dev@349 54
dev@349 55 export const defaultColourGenerator = createColourGenerator([
dev@349 56 '#0868ac', // "sapphire blue", our waveform / header colour
dev@349 57 '#c33c54', // "brick red"
dev@349 58 '#17bebb', // "tiffany blue"
dev@349 59 '#001021', // "rich black"
dev@349 60 '#fa8334', // "mango tango"
dev@349 61 '#034748' // "deep jungle green"
dev@349 62 ]);
dev@349 63
dev@349 64 // TODO this might belong somewhere else, or perhaps the stuff above ^^ does
dev@349 65
dev@349 66 export interface Instant {
dev@349 67 time: number;
dev@349 68 label: string;
dev@349 69 }
dev@349 70
dev@349 71 type CollectedShape = 'vector' | 'matrix' | 'tracks';
dev@349 72
dev@349 73 // TODO regions
dev@349 74 type ShapeDeducedFromList = 'instants' | 'notes';
dev@349 75 export type HigherLevelFeatureShape = CollectedShape | ShapeDeducedFromList;
dev@349 76
dev@349 77 export type ShapedFeatureData = VectorFeature
dev@349 78 | MatrixFeature
dev@349 79 | TracksFeature
dev@349 80 | Note[]
dev@349 81 | Instant[];
dev@349 82
dev@349 83 // These needn't be classes (could just be interfaces), just experimenting
dev@349 84 export abstract class ShapedFeature<Shape extends HigherLevelFeatureShape,
dev@349 85 Data extends ShapedFeatureData> {
dev@349 86 shape: Shape;
dev@349 87 collected: Data;
dev@349 88 }
dev@349 89
dev@349 90 export class Vector extends ShapedFeature<'vector', VectorFeature> {}
dev@349 91 export class Matrix extends ShapedFeature<'matrix', MatrixFeature> {}
dev@349 92 export class Tracks extends ShapedFeature<'tracks', TracksFeature> {}
dev@349 93 export class Notes extends ShapedFeature<'notes', Note[]> {}
dev@349 94 export class Instants extends ShapedFeature<'instants', Instant[]> {}
dev@349 95 export type KnownShapedFeature = Vector
dev@349 96 | Matrix
dev@349 97 | Tracks
dev@349 98 | Notes
dev@349 99 | Instants;
dev@349 100
dev@349 101 function hasKnownShapeOtherThanList(shape: string): shape is CollectedShape {
dev@349 102 return ['vector', 'matrix', 'tracks'].includes(shape);
dev@349 103 }
dev@349 104
dev@349 105 const throwShapeError = () => { throw new Error('No shape could be deduced'); };
dev@349 106 function deduceHigherLevelFeatureShape(response: SimpleResponse)
dev@349 107 : HigherLevelFeatureShape {
dev@349 108 const collection = response.features;
dev@349 109 const descriptor = response.outputDescriptor;
dev@349 110 if (hasKnownShapeOtherThanList(collection.shape)) {
dev@349 111 return collection.shape;
dev@349 112 }
dev@349 113
dev@349 114
dev@349 115 // TODO it's a shame that the types in piper don't make this easy for the
dev@349 116 // compiler to deduce
dev@349 117 if (collection.shape !== 'list' && collection.collected instanceof Array) {
dev@349 118 throwShapeError();
dev@349 119 }
dev@349 120
dev@349 121 const featureData = collection.collected as FeatureList;
dev@349 122 const hasDuration = descriptor.configured.hasDuration;
dev@349 123 const binCount = descriptor.configured.binCount;
dev@349 124 const isMarker = !hasDuration
dev@349 125 && binCount === 0
dev@349 126 && featureData[0].featureValues == null;
dev@349 127
dev@349 128 const isMaybeNote = getCanonicalNoteLikeUnit(descriptor.configured.unit)
dev@349 129 && [1, 2].find(nBins => nBins === binCount);
dev@349 130
dev@349 131 // TODO any need to be directly inspecting features?
dev@349 132 const isRegionLike = hasDuration && featureData[0].timestamp != null;
dev@349 133
dev@349 134 const isNote = isMaybeNote && isRegionLike;
dev@349 135 if (isMarker) {
dev@349 136 return 'instants';
dev@349 137 }
dev@349 138 if (isNote) {
dev@349 139 return 'notes';
dev@349 140 }
dev@349 141 throwShapeError();
dev@349 142 }
dev@349 143
dev@349 144 export function toKnownShape(response: SimpleResponse): KnownShapedFeature {
dev@349 145 const deducedShape = deduceHigherLevelFeatureShape(response);
dev@349 146 switch (deducedShape) {
dev@349 147 case 'vector':
dev@349 148 return response.features as Vector;
dev@349 149 case 'matrix':
dev@349 150 return response.features as Matrix;
dev@349 151 case 'notes':
dev@349 152 return {
dev@349 153 shape: deducedShape,
dev@349 154 collected: mapFeaturesToNotes(
dev@349 155 response.features.collected as FeatureList,
dev@349 156 response.outputDescriptor
dev@349 157 )
dev@349 158 };
dev@349 159 case 'instants':
dev@349 160 const featureData = response.features.collected as FeatureList;
dev@349 161 return {
dev@349 162 shape: deducedShape,
dev@349 163 collected: featureData.map(feature => ({
dev@349 164 time: toSeconds(feature.timestamp),
dev@349 165 label: feature.label
dev@349 166 }))
dev@349 167 };
dev@349 168 }
dev@349 169 throwShapeError();
dev@349 170 }