annotate src/app/visualisations/FeatureUtilities.ts @ 489:ab43880f1cd5

Rename to something a bit more appropriate.
author Lucas Thompson <dev@lucas.im>
date Wed, 05 Jul 2017 18:52:12 +0100
parents e3cbf4c53e5e
children 0fbba61603b3 3c962af5342c
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 SimpleResponse,
dev@349 9 TracksFeature,
dev@349 10 VectorFeature
dev@349 11 } from 'piper/HigherLevelUtilities';
dev@349 12
dev@349 13
dev@349 14 export type NoteLikeUnit = 'midi' | 'hz' ;
dev@349 15 export interface Note {
dev@349 16 time: number;
dev@349 17 duration: number;
dev@349 18 pitch: number;
dev@349 19 velocity?: number;
dev@349 20 }
dev@349 21
dev@349 22 export function getCanonicalNoteLikeUnit(unit: string): NoteLikeUnit | null {
dev@349 23 const canonicalUnits: NoteLikeUnit[] = ['midi', 'hz'];
dev@349 24 return canonicalUnits.find(canonicalUnit => {
dev@349 25 return unit.toLowerCase().indexOf(canonicalUnit) >= 0;
dev@349 26 });
dev@349 27 }
dev@349 28
dev@349 29 export function mapFeaturesToNotes(featureData: FeatureList,
dev@349 30 descriptor: OutputDescriptor): Note[] {
dev@349 31 const canonicalUnit = getCanonicalNoteLikeUnit(descriptor.configured.unit);
dev@349 32 const isHz = canonicalUnit === 'hz';
dev@349 33 return featureData.map(feature => ({
dev@349 34 time: toSeconds(feature.timestamp),
dev@349 35 duration: toSeconds(feature.duration),
dev@349 36 pitch: isHz ?
dev@349 37 frequencyToMidiNote(feature.featureValues[0]) : feature.featureValues[0]
dev@349 38 }));
dev@349 39 }
dev@349 40
dev@349 41 export function frequencyToMidiNote(frequency: number,
dev@349 42 concertA: number = 440.0): number {
dev@349 43 return 69 + 12 * Math.log2(frequency / concertA);
dev@349 44 }
dev@349 45
dev@349 46 export function* createColourGenerator(colours) {
dev@349 47 let index = 0;
dev@349 48 const nColours = colours.length;
dev@349 49 while (true) {
dev@349 50 yield colours[index = ++index % nColours];
dev@349 51 }
dev@349 52 }
dev@349 53
dev@349 54 export const defaultColourGenerator = createColourGenerator([
dev@349 55 '#0868ac', // "sapphire blue", our waveform / header colour
dev@349 56 '#c33c54', // "brick red"
dev@349 57 '#17bebb', // "tiffany blue"
dev@349 58 '#001021', // "rich black"
dev@349 59 '#fa8334', // "mango tango"
dev@349 60 '#034748' // "deep jungle green"
dev@349 61 ]);
dev@349 62
dev@349 63 // TODO this might belong somewhere else, or perhaps the stuff above ^^ does
dev@349 64
dev@349 65 export interface Instant {
dev@349 66 time: number;
dev@349 67 label: string;
dev@349 68 }
dev@349 69
dev@349 70 type CollectedShape = 'vector' | 'matrix' | 'tracks';
dev@349 71
dev@349 72 // TODO regions
dev@349 73 type ShapeDeducedFromList = 'instants' | 'notes';
dev@349 74 export type HigherLevelFeatureShape = CollectedShape | ShapeDeducedFromList;
dev@349 75
dev@477 76 export abstract class Grid {
cannam@472 77 binNames: string[];
dev@477 78 startTime: number;
dev@477 79 stepDuration: number;
dev@477 80 data: Float32Array[];
cannam@472 81 }
cannam@472 82
dev@396 83 export type ShapedFeatureData =
dev@394 84 VectorFeature
dev@477 85 | Grid
dev@349 86 | TracksFeature
dev@349 87 | Note[]
dev@396 88 | Instant[];
dev@349 89
dev@349 90 // These needn't be classes (could just be interfaces), just experimenting
dev@349 91 export abstract class ShapedFeature<Shape extends HigherLevelFeatureShape,
dev@396 92 Data extends ShapedFeatureData> {
dev@349 93 shape: Shape;
dev@396 94 collected: Data;
dev@349 95 }
dev@349 96
dev@349 97 export class Vector extends ShapedFeature<'vector', VectorFeature> {}
dev@477 98 export class Matrix extends ShapedFeature<'matrix', Grid> {}
dev@349 99 export class Tracks extends ShapedFeature<'tracks', TracksFeature> {}
dev@349 100 export class Notes extends ShapedFeature<'notes', Note[]> {}
dev@349 101 export class Instants extends ShapedFeature<'instants', Instant[]> {}
dev@349 102 export type KnownShapedFeature = Vector
dev@349 103 | Matrix
dev@349 104 | Tracks
dev@349 105 | Notes
dev@349 106 | Instants;
dev@349 107
dev@349 108 function hasKnownShapeOtherThanList(shape: string): shape is CollectedShape {
dev@349 109 return ['vector', 'matrix', 'tracks'].includes(shape);
dev@349 110 }
dev@349 111
dev@349 112 const throwShapeError = () => { throw new Error('No shape could be deduced'); };
dev@349 113 function deduceHigherLevelFeatureShape(response: SimpleResponse)
dev@349 114 : HigherLevelFeatureShape {
dev@349 115 const collection = response.features;
dev@349 116 const descriptor = response.outputDescriptor;
dev@349 117 if (hasKnownShapeOtherThanList(collection.shape)) {
dev@349 118 return collection.shape;
dev@349 119 }
dev@349 120
dev@349 121
dev@349 122 // TODO it's a shame that the types in piper don't make this easy for the
dev@349 123 // compiler to deduce
dev@349 124 if (collection.shape !== 'list' && collection.collected instanceof Array) {
dev@349 125 throwShapeError();
dev@349 126 }
dev@349 127
dev@349 128 const featureData = collection.collected as FeatureList;
dev@349 129 const hasDuration = descriptor.configured.hasDuration;
dev@349 130 const binCount = descriptor.configured.binCount;
dev@349 131 const isMarker = !hasDuration
dev@349 132 && binCount === 0
dev@349 133 && featureData[0].featureValues == null;
dev@349 134
dev@349 135 const isMaybeNote = getCanonicalNoteLikeUnit(descriptor.configured.unit)
dev@349 136 && [1, 2].find(nBins => nBins === binCount);
dev@349 137
dev@349 138 // TODO any need to be directly inspecting features?
dev@349 139 const isRegionLike = hasDuration && featureData[0].timestamp != null;
dev@349 140
dev@349 141 const isNote = isMaybeNote && isRegionLike;
dev@349 142 if (isMarker) {
dev@349 143 return 'instants';
dev@349 144 }
dev@349 145 if (isNote) {
dev@349 146 return 'notes';
dev@349 147 }
dev@349 148 throwShapeError();
dev@349 149 }
dev@349 150
dev@349 151 export function toKnownShape(response: SimpleResponse): KnownShapedFeature {
dev@349 152 const deducedShape = deduceHigherLevelFeatureShape(response);
dev@396 153 switch (deducedShape) {
dev@396 154 case 'vector':
dev@396 155 return response.features as Vector;
dev@396 156 case 'matrix':
cannam@472 157 return {
cannam@472 158 shape: deducedShape,
cannam@472 159 collected: Object.assign(response.features.collected, {
cannam@472 160 binNames: response.outputDescriptor.configured.binNames || []
cannam@472 161 })
cannam@472 162 } as Matrix;
dev@396 163 case 'tracks':
dev@396 164 return response.features as Tracks;
dev@396 165 case 'notes':
dev@396 166 return {
dev@396 167 shape: deducedShape,
dev@396 168 collected: mapFeaturesToNotes(
dev@349 169 response.features.collected as FeatureList,
dev@349 170 response.outputDescriptor
dev@396 171 )
dev@396 172 };
dev@396 173 case 'instants':
dev@396 174 const featureData = response.features.collected as FeatureList;
dev@396 175 return {
dev@396 176 shape: deducedShape,
dev@396 177 collected: featureData.map(feature => ({
dev@396 178 time: toSeconds(feature.timestamp),
dev@396 179 label: feature.label
dev@396 180 }))
dev@396 181 };
dev@349 182 }
dev@396 183 throwShapeError();
dev@349 184 }
dev@368 185
dev@404 186 export interface PlotDataPoint {
dev@368 187 cx: number;
dev@368 188 cy: number;
dev@368 189 }
dev@368 190
dev@404 191 export interface PlotData {
dev@404 192 points: PlotDataPoint[];
dev@404 193 startTime: number;
dev@404 194 duration;
dev@404 195 }
dev@404 196
dev@368 197 export interface PlotLayerData {
dev@368 198 data: PlotData[];
dev@368 199 yDomain: [number, number];
dev@368 200 }
dev@368 201
dev@404 202 export function generatePlotData(features: VectorFeature[]): PlotLayerData {
dev@368 203
dev@368 204 const winnowed = features.filter(feature => feature.data.length > 0);
dev@368 205
dev@368 206 // First establish a [min,max] range across all of the features
dev@368 207 let [min, max] = winnowed.reduce((acc, feature) => {
dev@368 208 return feature.data.reduce((acc, val) => {
dev@368 209 const [min, max] = acc;
dev@368 210 return [Math.min(min, val), Math.max(max, val)];
dev@368 211 }, acc);
dev@368 212 }, [Infinity, -Infinity]);
dev@368 213
dev@368 214 if (min === Infinity) {
dev@368 215 min = 0;
dev@368 216 max = 1;
dev@368 217 }
dev@368 218
dev@368 219 if (min !== min || max !== max) {
dev@368 220 console.warn('WARNING: min or max is NaN');
dev@368 221 min = 0;
dev@368 222 max = 1;
dev@368 223 }
dev@368 224
dev@404 225 return {
dev@404 226 data: winnowed.map(feature => {
dev@404 227 let duration = 0;
dev@368 228
dev@404 229 // Give the plot items positions relative to the start of the
dev@404 230 // line, rather than relative to absolute time 0. This is
dev@404 231 // because we'll be setting the layer timeline start property
dev@404 232 // later on and these will be positioned relative to that
dev@368 233
dev@404 234 const plotData = [...feature.data].map((val, i) => {
dev@404 235 const t = i * feature.stepDuration;
dev@404 236 duration = t + feature.stepDuration;
dev@404 237 return {
dev@404 238 cx: t,
dev@404 239 cy: val
dev@404 240 };
dev@404 241 });
dev@404 242
dev@368 243 return {
dev@404 244 points: plotData,
dev@404 245 startTime: feature.startTime,
dev@404 246 duration: duration
dev@368 247 };
dev@404 248 }),
dev@404 249 yDomain: [min, max]
dev@404 250 };
dev@368 251 }