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@349
|
77 export type ShapedFeatureData = VectorFeature
|
dev@349
|
78 | MatrixFeature
|
dev@349
|
79 | TracksFeature
|
dev@349
|
80 | Note[]
|
dev@349
|
81 | Instant[];
|
dev@349
|
82
|
dev@349
|
83 // These needn't be classes (could just be interfaces), just experimenting
|
dev@349
|
84 export abstract class ShapedFeature<Shape extends HigherLevelFeatureShape,
|
dev@349
|
85 Data extends ShapedFeatureData> {
|
dev@349
|
86 shape: Shape;
|
dev@349
|
87 collected: Data;
|
dev@349
|
88 }
|
dev@349
|
89
|
dev@349
|
90 export class Vector extends ShapedFeature<'vector', VectorFeature> {}
|
dev@349
|
91 export class Matrix extends ShapedFeature<'matrix', MatrixFeature> {}
|
dev@349
|
92 export class Tracks extends ShapedFeature<'tracks', TracksFeature> {}
|
dev@349
|
93 export class Notes extends ShapedFeature<'notes', Note[]> {}
|
dev@349
|
94 export class Instants extends ShapedFeature<'instants', Instant[]> {}
|
dev@349
|
95 export type KnownShapedFeature = Vector
|
dev@349
|
96 | Matrix
|
dev@349
|
97 | Tracks
|
dev@349
|
98 | Notes
|
dev@349
|
99 | Instants;
|
dev@349
|
100
|
dev@349
|
101 function hasKnownShapeOtherThanList(shape: string): shape is CollectedShape {
|
dev@349
|
102 return ['vector', 'matrix', 'tracks'].includes(shape);
|
dev@349
|
103 }
|
dev@349
|
104
|
dev@349
|
105 const throwShapeError = () => { throw new Error('No shape could be deduced'); };
|
dev@349
|
106 function deduceHigherLevelFeatureShape(response: SimpleResponse)
|
dev@349
|
107 : HigherLevelFeatureShape {
|
dev@349
|
108 const collection = response.features;
|
dev@349
|
109 const descriptor = response.outputDescriptor;
|
dev@349
|
110 if (hasKnownShapeOtherThanList(collection.shape)) {
|
dev@349
|
111 return collection.shape;
|
dev@349
|
112 }
|
dev@349
|
113
|
dev@349
|
114
|
dev@349
|
115 // TODO it's a shame that the types in piper don't make this easy for the
|
dev@349
|
116 // compiler to deduce
|
dev@349
|
117 if (collection.shape !== 'list' && collection.collected instanceof Array) {
|
dev@349
|
118 throwShapeError();
|
dev@349
|
119 }
|
dev@349
|
120
|
dev@349
|
121 const featureData = collection.collected as FeatureList;
|
dev@349
|
122 const hasDuration = descriptor.configured.hasDuration;
|
dev@349
|
123 const binCount = descriptor.configured.binCount;
|
dev@349
|
124 const isMarker = !hasDuration
|
dev@349
|
125 && binCount === 0
|
dev@349
|
126 && featureData[0].featureValues == null;
|
dev@349
|
127
|
dev@349
|
128 const isMaybeNote = getCanonicalNoteLikeUnit(descriptor.configured.unit)
|
dev@349
|
129 && [1, 2].find(nBins => nBins === binCount);
|
dev@349
|
130
|
dev@349
|
131 // TODO any need to be directly inspecting features?
|
dev@349
|
132 const isRegionLike = hasDuration && featureData[0].timestamp != null;
|
dev@349
|
133
|
dev@349
|
134 const isNote = isMaybeNote && isRegionLike;
|
dev@349
|
135 if (isMarker) {
|
dev@349
|
136 return 'instants';
|
dev@349
|
137 }
|
dev@349
|
138 if (isNote) {
|
dev@349
|
139 return 'notes';
|
dev@349
|
140 }
|
dev@349
|
141 throwShapeError();
|
dev@349
|
142 }
|
dev@349
|
143
|
dev@349
|
144 export function toKnownShape(response: SimpleResponse): KnownShapedFeature {
|
dev@349
|
145 const deducedShape = deduceHigherLevelFeatureShape(response);
|
dev@349
|
146 switch (deducedShape) {
|
dev@349
|
147 case 'vector':
|
dev@349
|
148 return response.features as Vector;
|
dev@349
|
149 case 'matrix':
|
dev@349
|
150 return response.features as Matrix;
|
dev@349
|
151 case 'notes':
|
dev@349
|
152 return {
|
dev@349
|
153 shape: deducedShape,
|
dev@349
|
154 collected: mapFeaturesToNotes(
|
dev@349
|
155 response.features.collected as FeatureList,
|
dev@349
|
156 response.outputDescriptor
|
dev@349
|
157 )
|
dev@349
|
158 };
|
dev@349
|
159 case 'instants':
|
dev@349
|
160 const featureData = response.features.collected as FeatureList;
|
dev@349
|
161 return {
|
dev@349
|
162 shape: deducedShape,
|
dev@349
|
163 collected: featureData.map(feature => ({
|
dev@349
|
164 time: toSeconds(feature.timestamp),
|
dev@349
|
165 label: feature.label
|
dev@349
|
166 }))
|
dev@349
|
167 };
|
dev@349
|
168 }
|
dev@349
|
169 throwShapeError();
|
dev@349
|
170 }
|