Mercurial > hg > ugly-duckling
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 } |