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