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 }
|