dev@349: /** dev@349: * Created by lucast on 24/05/2017. dev@349: */ dev@349: dev@349: import {FeatureList} from 'piper/Feature'; dev@349: import {OutputDescriptor, toSeconds} from 'piper'; dev@349: import { dev@349: MatrixFeature, dev@349: SimpleResponse, dev@349: TracksFeature, dev@349: VectorFeature dev@349: } from 'piper/HigherLevelUtilities'; dev@349: dev@349: dev@349: export type NoteLikeUnit = 'midi' | 'hz' ; dev@349: export interface Note { dev@349: time: number; dev@349: duration: number; dev@349: pitch: number; dev@349: velocity?: number; dev@349: } dev@349: dev@349: export function getCanonicalNoteLikeUnit(unit: string): NoteLikeUnit | null { dev@349: const canonicalUnits: NoteLikeUnit[] = ['midi', 'hz']; dev@349: return canonicalUnits.find(canonicalUnit => { dev@349: return unit.toLowerCase().indexOf(canonicalUnit) >= 0; dev@349: }); dev@349: } dev@349: dev@349: export function mapFeaturesToNotes(featureData: FeatureList, dev@349: descriptor: OutputDescriptor): Note[] { dev@349: const canonicalUnit = getCanonicalNoteLikeUnit(descriptor.configured.unit); dev@349: const isHz = canonicalUnit === 'hz'; dev@349: return featureData.map(feature => ({ dev@349: time: toSeconds(feature.timestamp), dev@349: duration: toSeconds(feature.duration), dev@349: pitch: isHz ? dev@349: frequencyToMidiNote(feature.featureValues[0]) : feature.featureValues[0] dev@349: })); dev@349: } dev@349: dev@349: export function frequencyToMidiNote(frequency: number, dev@349: concertA: number = 440.0): number { dev@349: return 69 + 12 * Math.log2(frequency / concertA); dev@349: } dev@349: dev@349: export function* createColourGenerator(colours) { dev@349: let index = 0; dev@349: const nColours = colours.length; dev@349: while (true) { dev@349: yield colours[index = ++index % nColours]; dev@349: } dev@349: } dev@349: dev@349: export const defaultColourGenerator = createColourGenerator([ dev@349: '#0868ac', // "sapphire blue", our waveform / header colour dev@349: '#c33c54', // "brick red" dev@349: '#17bebb', // "tiffany blue" dev@349: '#001021', // "rich black" dev@349: '#fa8334', // "mango tango" dev@349: '#034748' // "deep jungle green" dev@349: ]); dev@349: dev@349: // TODO this might belong somewhere else, or perhaps the stuff above ^^ does dev@349: dev@349: export interface Instant { dev@349: time: number; dev@349: label: string; dev@349: } dev@349: dev@349: type CollectedShape = 'vector' | 'matrix' | 'tracks'; dev@349: dev@349: // TODO regions dev@349: type ShapeDeducedFromList = 'instants' | 'notes'; dev@349: export type HigherLevelFeatureShape = CollectedShape | ShapeDeducedFromList; dev@349: dev@349: export type ShapedFeatureData = VectorFeature dev@349: | MatrixFeature dev@349: | TracksFeature dev@349: | Note[] dev@349: | Instant[]; dev@349: dev@349: // These needn't be classes (could just be interfaces), just experimenting dev@349: export abstract class ShapedFeature { dev@349: shape: Shape; dev@349: collected: Data; dev@349: } dev@349: dev@349: export class Vector extends ShapedFeature<'vector', VectorFeature> {} dev@349: export class Matrix extends ShapedFeature<'matrix', MatrixFeature> {} dev@349: export class Tracks extends ShapedFeature<'tracks', TracksFeature> {} dev@349: export class Notes extends ShapedFeature<'notes', Note[]> {} dev@349: export class Instants extends ShapedFeature<'instants', Instant[]> {} dev@349: export type KnownShapedFeature = Vector dev@349: | Matrix dev@349: | Tracks dev@349: | Notes dev@349: | Instants; dev@349: dev@349: function hasKnownShapeOtherThanList(shape: string): shape is CollectedShape { dev@349: return ['vector', 'matrix', 'tracks'].includes(shape); dev@349: } dev@349: dev@349: const throwShapeError = () => { throw new Error('No shape could be deduced'); }; dev@349: function deduceHigherLevelFeatureShape(response: SimpleResponse) dev@349: : HigherLevelFeatureShape { dev@349: const collection = response.features; dev@349: const descriptor = response.outputDescriptor; dev@349: if (hasKnownShapeOtherThanList(collection.shape)) { dev@349: return collection.shape; dev@349: } dev@349: dev@349: dev@349: // TODO it's a shame that the types in piper don't make this easy for the dev@349: // compiler to deduce dev@349: if (collection.shape !== 'list' && collection.collected instanceof Array) { dev@349: throwShapeError(); dev@349: } dev@349: dev@349: const featureData = collection.collected as FeatureList; dev@349: const hasDuration = descriptor.configured.hasDuration; dev@349: const binCount = descriptor.configured.binCount; dev@349: const isMarker = !hasDuration dev@349: && binCount === 0 dev@349: && featureData[0].featureValues == null; dev@349: dev@349: const isMaybeNote = getCanonicalNoteLikeUnit(descriptor.configured.unit) dev@349: && [1, 2].find(nBins => nBins === binCount); dev@349: dev@349: // TODO any need to be directly inspecting features? dev@349: const isRegionLike = hasDuration && featureData[0].timestamp != null; dev@349: dev@349: const isNote = isMaybeNote && isRegionLike; dev@349: if (isMarker) { dev@349: return 'instants'; dev@349: } dev@349: if (isNote) { dev@349: return 'notes'; dev@349: } dev@349: throwShapeError(); dev@349: } dev@349: dev@349: export function toKnownShape(response: SimpleResponse): KnownShapedFeature { dev@349: const deducedShape = deduceHigherLevelFeatureShape(response); dev@349: switch (deducedShape) { dev@349: case 'vector': dev@349: return response.features as Vector; dev@349: case 'matrix': dev@349: return response.features as Matrix; dev@349: case 'notes': dev@349: return { dev@349: shape: deducedShape, dev@349: collected: mapFeaturesToNotes( dev@349: response.features.collected as FeatureList, dev@349: response.outputDescriptor dev@349: ) dev@349: }; dev@349: case 'instants': dev@349: const featureData = response.features.collected as FeatureList; dev@349: return { dev@349: shape: deducedShape, dev@349: collected: featureData.map(feature => ({ dev@349: time: toSeconds(feature.timestamp), dev@349: label: feature.label dev@349: })) dev@349: }; dev@349: } dev@349: throwShapeError(); dev@349: }