annotate src/app/visualisations/FeatureUtilities.ts @ 509:041468f553e1 tip master

Merge pull request #57 from LucasThompson/fix/session-stack-max-call-stack Fix accidental recursion in PersistentStack
author Lucas Thompson <LucasThompson@users.noreply.github.com>
date Mon, 27 Nov 2017 11:04:30 +0000
parents abbc096e0335
children
rev   line source
dev@349 1 /**
dev@349 2 * Created by lucast on 24/05/2017.
dev@349 3 */
dev@349 4
dev@497 5 import {FeatureList, OutputDescriptor} from 'piper-js/core';
dev@497 6 import {toSeconds} from 'piper-js/time';
dev@349 7 import {
dev@497 8 OneShotExtractionResponse as SimpleResponse,
dev@349 9 TracksFeature,
dev@349 10 VectorFeature
dev@497 11 } from 'piper-js/one-shot';
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
cannam@480 22 export interface Region {
cannam@480 23 time: number;
cannam@480 24 duration: number;
cannam@480 25 value: number;
cannam@480 26 }
cannam@480 27
dev@349 28 export function getCanonicalNoteLikeUnit(unit: string): NoteLikeUnit | null {
dev@349 29 const canonicalUnits: NoteLikeUnit[] = ['midi', 'hz'];
dev@349 30 return canonicalUnits.find(canonicalUnit => {
dev@349 31 return unit.toLowerCase().indexOf(canonicalUnit) >= 0;
dev@349 32 });
dev@349 33 }
dev@349 34
dev@349 35 export function mapFeaturesToNotes(featureData: FeatureList,
dev@349 36 descriptor: OutputDescriptor): Note[] {
dev@349 37 const canonicalUnit = getCanonicalNoteLikeUnit(descriptor.configured.unit);
dev@349 38 const isHz = canonicalUnit === 'hz';
dev@349 39 return featureData.map(feature => ({
dev@349 40 time: toSeconds(feature.timestamp),
dev@349 41 duration: toSeconds(feature.duration),
dev@349 42 pitch: isHz ?
dev@349 43 frequencyToMidiNote(feature.featureValues[0]) : feature.featureValues[0]
dev@349 44 }));
dev@349 45 }
dev@349 46
cannam@480 47 export function mapFeaturesToRegions(featureData: FeatureList,
cannam@480 48 descriptor: OutputDescriptor): Region[] {
cannam@480 49 return featureData.map(feature => ({
cannam@480 50 time: toSeconds(feature.timestamp),
cannam@480 51 duration: toSeconds(feature.duration),
cannam@480 52 value: feature.featureValues.length > 0 ? feature.featureValues[0] : null,
cannam@480 53 label: feature.label
cannam@480 54 }));
cannam@480 55 }
cannam@480 56
dev@349 57 export function frequencyToMidiNote(frequency: number,
dev@349 58 concertA: number = 440.0): number {
dev@349 59 return 69 + 12 * Math.log2(frequency / concertA);
dev@349 60 }
dev@349 61
dev@349 62 export function* createColourGenerator(colours) {
dev@349 63 let index = 0;
dev@349 64 const nColours = colours.length;
dev@349 65 while (true) {
dev@349 66 yield colours[index = ++index % nColours];
dev@349 67 }
dev@349 68 }
dev@349 69
dev@349 70 export const defaultColourGenerator = createColourGenerator([
dev@349 71 '#0868ac', // "sapphire blue", our waveform / header colour
dev@349 72 '#c33c54', // "brick red"
dev@349 73 '#17bebb', // "tiffany blue"
dev@349 74 '#001021', // "rich black"
dev@349 75 '#fa8334', // "mango tango"
dev@349 76 '#034748' // "deep jungle green"
dev@349 77 ]);
dev@349 78
dev@349 79 // TODO this might belong somewhere else, or perhaps the stuff above ^^ does
dev@349 80
dev@349 81 export interface Instant {
dev@349 82 time: number;
dev@349 83 label: string;
dev@349 84 }
dev@349 85
dev@349 86 type CollectedShape = 'vector' | 'matrix' | 'tracks';
dev@349 87
cannam@480 88 type ShapeDeducedFromList = 'notes' | 'regions' | 'instants';
dev@349 89 export type HigherLevelFeatureShape = CollectedShape | ShapeDeducedFromList;
dev@349 90
dev@477 91 export abstract class Grid {
cannam@472 92 binNames: string[];
dev@477 93 startTime: number;
dev@477 94 stepDuration: number;
dev@477 95 data: Float32Array[];
cannam@472 96 }
cannam@472 97
dev@396 98 export type ShapedFeatureData =
dev@394 99 VectorFeature
dev@477 100 | Grid
dev@349 101 | TracksFeature
dev@349 102 | Note[]
cannam@480 103 | Region[]
dev@396 104 | Instant[];
dev@349 105
dev@349 106 // These needn't be classes (could just be interfaces), just experimenting
dev@349 107 export abstract class ShapedFeature<Shape extends HigherLevelFeatureShape,
dev@396 108 Data extends ShapedFeatureData> {
dev@349 109 shape: Shape;
dev@396 110 collected: Data;
dev@349 111 }
dev@349 112
dev@349 113 export class Vector extends ShapedFeature<'vector', VectorFeature> {}
dev@477 114 export class Matrix extends ShapedFeature<'matrix', Grid> {}
dev@349 115 export class Tracks extends ShapedFeature<'tracks', TracksFeature> {}
dev@349 116 export class Notes extends ShapedFeature<'notes', Note[]> {}
cannam@480 117 export class Regions extends ShapedFeature<'regions', Region[]> {}
dev@349 118 export class Instants extends ShapedFeature<'instants', Instant[]> {}
dev@349 119 export type KnownShapedFeature = Vector
dev@349 120 | Matrix
dev@349 121 | Tracks
dev@349 122 | Notes
cannam@480 123 | Regions
dev@349 124 | Instants;
dev@349 125
dev@349 126 function hasKnownShapeOtherThanList(shape: string): shape is CollectedShape {
dev@349 127 return ['vector', 'matrix', 'tracks'].includes(shape);
dev@349 128 }
dev@349 129
dev@496 130 function throwShapeError(compileAssertion?: never) {
dev@496 131 throw new Error('No shape could be deduced');
dev@496 132 }
dev@496 133
cannam@480 134 const rdfTypes = {
cannam@480 135 'http://purl.org/ontology/af/Note': 'notes',
dev@482 136 // 'http://purl.org/ontology/af/StructuralSegment': 'segments' // TODO segments
dev@482 137 };
cannam@480 138
dev@349 139 function deduceHigherLevelFeatureShape(response: SimpleResponse)
dev@349 140 : HigherLevelFeatureShape {
dev@349 141 const collection = response.features;
dev@349 142 const descriptor = response.outputDescriptor;
dev@349 143 if (hasKnownShapeOtherThanList(collection.shape)) {
dev@349 144 return collection.shape;
dev@349 145 }
dev@349 146
dev@477 147
dev@497 148 // TODO it's a shame that the types in piper-js don't make this easy for the
dev@349 149 // compiler to deduce
dev@349 150 if (collection.shape !== 'list' && collection.collected instanceof Array) {
dev@349 151 throwShapeError();
dev@349 152 }
dev@349 153
dev@349 154 const featureData = collection.collected as FeatureList;
dev@349 155 const hasDuration = descriptor.configured.hasDuration;
dev@349 156 const binCount = descriptor.configured.binCount;
dev@349 157 const isMarker = !hasDuration
dev@349 158 && binCount === 0
cannam@480 159 && (featureData.length === 0 || featureData[0].featureValues == null);
dev@349 160
dev@349 161 if (isMarker) {
dev@349 162 return 'instants';
dev@349 163 }
cannam@480 164
cannam@480 165 if (descriptor.static) {
cannam@480 166 const typeURI = descriptor.static.typeURI;
dev@482 167 if (typeof typeURI === 'string' && typeof rdfTypes[typeURI] === 'string') {
cannam@480 168 return rdfTypes[typeURI];
cannam@480 169 }
dev@349 170 }
cannam@480 171
cannam@480 172 const isRegionLike = hasDuration &&
cannam@480 173 (featureData.length > 0 && featureData[0].timestamp != null);
dev@482 174
dev@498 175 const hasUnit = descriptor.configured && descriptor.configured.unit;
dev@477 176
dev@498 177 const isMaybeNote = hasUnit
dev@498 178 && getCanonicalNoteLikeUnit(descriptor.configured.unit)
cannam@480 179 && [1, 2].find(nBins => nBins === binCount);
dev@482 180
cannam@480 181 if (isRegionLike) {
cannam@480 182 if (isMaybeNote) {
cannam@480 183 return 'notes';
cannam@480 184 } else {
cannam@480 185 return 'regions';
cannam@480 186 }
cannam@480 187 }
dev@482 188
dev@349 189 throwShapeError();
dev@349 190 }
dev@349 191
dev@349 192 export function toKnownShape(response: SimpleResponse): KnownShapedFeature {
dev@349 193 const deducedShape = deduceHigherLevelFeatureShape(response);
dev@396 194 switch (deducedShape) {
dev@396 195 case 'vector':
dev@396 196 return response.features as Vector;
dev@396 197 case 'matrix':
cannam@472 198 return {
cannam@472 199 shape: deducedShape,
cannam@472 200 collected: Object.assign(response.features.collected, {
cannam@472 201 binNames: response.outputDescriptor.configured.binNames || []
cannam@472 202 })
cannam@472 203 } as Matrix;
dev@396 204 case 'tracks':
dev@396 205 return response.features as Tracks;
dev@396 206 case 'notes':
dev@396 207 return {
dev@396 208 shape: deducedShape,
dev@396 209 collected: mapFeaturesToNotes(
dev@349 210 response.features.collected as FeatureList,
dev@349 211 response.outputDescriptor
dev@396 212 )
dev@396 213 };
cannam@480 214 case 'regions':
cannam@480 215 return {
cannam@480 216 shape: deducedShape,
cannam@480 217 collected: mapFeaturesToRegions(
cannam@480 218 response.features.collected as FeatureList,
cannam@480 219 response.outputDescriptor
cannam@480 220 )
cannam@480 221 };
dev@396 222 case 'instants':
dev@396 223 const featureData = response.features.collected as FeatureList;
dev@396 224 return {
dev@396 225 shape: deducedShape,
dev@396 226 collected: featureData.map(feature => ({
dev@396 227 time: toSeconds(feature.timestamp),
dev@396 228 label: feature.label
dev@396 229 }))
dev@396 230 };
dev@349 231 }
dev@496 232 throwShapeError(deducedShape);
dev@349 233 }
dev@368 234
dev@404 235 export interface PlotDataPoint {
dev@368 236 cx: number;
dev@368 237 cy: number;
dev@368 238 }
dev@368 239
dev@404 240 export interface PlotData {
dev@404 241 points: PlotDataPoint[];
dev@404 242 startTime: number;
dev@404 243 duration;
dev@404 244 }
dev@404 245
dev@368 246 export interface PlotLayerData {
dev@368 247 data: PlotData[];
dev@368 248 yDomain: [number, number];
dev@368 249 }
dev@368 250
dev@404 251 export function generatePlotData(features: VectorFeature[]): PlotLayerData {
dev@368 252
dev@368 253 const winnowed = features.filter(feature => feature.data.length > 0);
dev@368 254
dev@368 255 // First establish a [min,max] range across all of the features
dev@368 256 let [min, max] = winnowed.reduce((acc, feature) => {
dev@368 257 return feature.data.reduce((acc, val) => {
dev@368 258 const [min, max] = acc;
dev@368 259 return [Math.min(min, val), Math.max(max, val)];
dev@368 260 }, acc);
dev@368 261 }, [Infinity, -Infinity]);
dev@368 262
dev@368 263 if (min === Infinity) {
dev@368 264 min = 0;
dev@368 265 max = 1;
dev@368 266 }
dev@368 267
dev@368 268 if (min !== min || max !== max) {
dev@368 269 console.warn('WARNING: min or max is NaN');
dev@368 270 min = 0;
dev@368 271 max = 1;
dev@368 272 }
dev@368 273
dev@404 274 return {
dev@404 275 data: winnowed.map(feature => {
dev@404 276 let duration = 0;
dev@368 277
dev@404 278 // Give the plot items positions relative to the start of the
dev@404 279 // line, rather than relative to absolute time 0. This is
dev@404 280 // because we'll be setting the layer timeline start property
dev@404 281 // later on and these will be positioned relative to that
dev@368 282
dev@404 283 const plotData = [...feature.data].map((val, i) => {
dev@404 284 const t = i * feature.stepDuration;
dev@404 285 duration = t + feature.stepDuration;
dev@404 286 return {
dev@404 287 cx: t,
dev@404 288 cy: val
dev@404 289 };
dev@404 290 });
dev@404 291
dev@368 292 return {
dev@404 293 points: plotData,
dev@404 294 startTime: feature.startTime,
dev@404 295 duration: duration
dev@368 296 };
dev@404 297 }),
dev@404 298 yDomain: [min, max]
dev@404 299 };
dev@368 300 }