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