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