annotate src/app/visualisations/FeatureUtilities.ts @ 396:3eab26a629e1

Revert changes relating to bodging unit onto the shaped features. Now return from the extraction service and add to the analysis item, and send into the cross-hair component with a prop.
author Lucas Thompson <dev@lucas.im>
date Thu, 01 Jun 2017 23:04:39 +0100
parents f45a916eb5b1
children 6672496ff32e
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@396 77 export type ShapedFeatureData =
dev@394 78 VectorFeature
dev@349 79 | MatrixFeature
dev@349 80 | TracksFeature
dev@349 81 | Note[]
dev@396 82 | Instant[];
dev@349 83
dev@349 84 // These needn't be classes (could just be interfaces), just experimenting
dev@349 85 export abstract class ShapedFeature<Shape extends HigherLevelFeatureShape,
dev@396 86 Data extends ShapedFeatureData> {
dev@349 87 shape: Shape;
dev@396 88 collected: Data;
dev@349 89 }
dev@349 90
dev@349 91 export class Vector extends ShapedFeature<'vector', VectorFeature> {}
dev@349 92 export class Matrix extends ShapedFeature<'matrix', MatrixFeature> {}
dev@349 93 export class Tracks extends ShapedFeature<'tracks', TracksFeature> {}
dev@349 94 export class Notes extends ShapedFeature<'notes', Note[]> {}
dev@349 95 export class Instants extends ShapedFeature<'instants', Instant[]> {}
dev@349 96 export type KnownShapedFeature = Vector
dev@349 97 | Matrix
dev@349 98 | Tracks
dev@349 99 | Notes
dev@349 100 | Instants;
dev@349 101
dev@349 102 function hasKnownShapeOtherThanList(shape: string): shape is CollectedShape {
dev@349 103 return ['vector', 'matrix', 'tracks'].includes(shape);
dev@349 104 }
dev@349 105
dev@349 106 const throwShapeError = () => { throw new Error('No shape could be deduced'); };
dev@349 107 function deduceHigherLevelFeatureShape(response: SimpleResponse)
dev@349 108 : HigherLevelFeatureShape {
dev@349 109 const collection = response.features;
dev@349 110 const descriptor = response.outputDescriptor;
dev@349 111 if (hasKnownShapeOtherThanList(collection.shape)) {
dev@349 112 return collection.shape;
dev@349 113 }
dev@349 114
dev@349 115
dev@349 116 // TODO it's a shame that the types in piper don't make this easy for the
dev@349 117 // compiler to deduce
dev@349 118 if (collection.shape !== 'list' && collection.collected instanceof Array) {
dev@349 119 throwShapeError();
dev@349 120 }
dev@349 121
dev@349 122 const featureData = collection.collected as FeatureList;
dev@349 123 const hasDuration = descriptor.configured.hasDuration;
dev@349 124 const binCount = descriptor.configured.binCount;
dev@349 125 const isMarker = !hasDuration
dev@349 126 && binCount === 0
dev@349 127 && featureData[0].featureValues == null;
dev@349 128
dev@349 129 const isMaybeNote = getCanonicalNoteLikeUnit(descriptor.configured.unit)
dev@349 130 && [1, 2].find(nBins => nBins === binCount);
dev@349 131
dev@349 132 // TODO any need to be directly inspecting features?
dev@349 133 const isRegionLike = hasDuration && featureData[0].timestamp != null;
dev@349 134
dev@349 135 const isNote = isMaybeNote && isRegionLike;
dev@349 136 if (isMarker) {
dev@349 137 return 'instants';
dev@349 138 }
dev@349 139 if (isNote) {
dev@349 140 return 'notes';
dev@349 141 }
dev@349 142 throwShapeError();
dev@349 143 }
dev@349 144
dev@349 145 export function toKnownShape(response: SimpleResponse): KnownShapedFeature {
dev@349 146 const deducedShape = deduceHigherLevelFeatureShape(response);
dev@396 147 switch (deducedShape) {
dev@396 148 case 'vector':
dev@396 149 return response.features as Vector;
dev@396 150 case 'matrix':
dev@396 151 return response.features as Matrix;
dev@396 152 case 'tracks':
dev@396 153 return response.features as Tracks;
dev@396 154 case 'notes':
dev@396 155 return {
dev@396 156 shape: deducedShape,
dev@396 157 collected: mapFeaturesToNotes(
dev@349 158 response.features.collected as FeatureList,
dev@349 159 response.outputDescriptor
dev@396 160 )
dev@396 161 };
dev@396 162 case 'instants':
dev@396 163 const featureData = response.features.collected as FeatureList;
dev@396 164 return {
dev@396 165 shape: deducedShape,
dev@396 166 collected: featureData.map(feature => ({
dev@396 167 time: toSeconds(feature.timestamp),
dev@396 168 label: feature.label
dev@396 169 }))
dev@396 170 };
dev@349 171 }
dev@396 172 throwShapeError();
dev@349 173 }
dev@368 174
dev@368 175 export interface PlotData {
dev@368 176 cx: number;
dev@368 177 cy: number;
dev@368 178 }
dev@368 179
dev@368 180 export interface PlotLayerData {
dev@368 181 data: PlotData[];
dev@368 182 yDomain: [number, number];
dev@368 183 startTime: number;
dev@368 184 duration: number;
dev@368 185 }
dev@368 186
dev@368 187 export function generatePlotData(features: VectorFeature[]): PlotLayerData[] {
dev@368 188
dev@368 189 const winnowed = features.filter(feature => feature.data.length > 0);
dev@368 190
dev@368 191 // First establish a [min,max] range across all of the features
dev@368 192 let [min, max] = winnowed.reduce((acc, feature) => {
dev@368 193 return feature.data.reduce((acc, val) => {
dev@368 194 const [min, max] = acc;
dev@368 195 return [Math.min(min, val), Math.max(max, val)];
dev@368 196 }, acc);
dev@368 197 }, [Infinity, -Infinity]);
dev@368 198
dev@368 199 if (min === Infinity) {
dev@368 200 min = 0;
dev@368 201 max = 1;
dev@368 202 }
dev@368 203
dev@368 204 if (min !== min || max !== max) {
dev@368 205 console.warn('WARNING: min or max is NaN');
dev@368 206 min = 0;
dev@368 207 max = 1;
dev@368 208 }
dev@368 209
dev@368 210 return winnowed.map(feature => {
dev@368 211 let duration = 0;
dev@368 212
dev@368 213 // Give the plot items positions relative to the start of the
dev@368 214 // line, rather than relative to absolute time 0. This is
dev@368 215 // because we'll be setting the layer timeline start property
dev@368 216 // later on and these will be positioned relative to that
dev@368 217
dev@368 218 const plotData = [...feature.data].map((val, i) => {
dev@368 219 const t = i * feature.stepDuration;
dev@368 220 duration = t + feature.stepDuration;
dev@368 221 return {
dev@368 222 cx: t,
dev@368 223 cy: val
dev@368 224 };
dev@368 225 });
dev@368 226
dev@368 227 return {
dev@368 228 data: plotData,
dev@368 229 yDomain: [min, max] as [number, number],
dev@368 230 startTime: feature.startTime,
dev@368 231 duration: duration
dev@368 232 };
dev@368 233 });
dev@368 234 }