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@396: export type ShapedFeatureData = dev@394: VectorFeature dev@349: | MatrixFeature dev@349: | TracksFeature dev@349: | Note[] dev@396: | 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@396: 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@396: switch (deducedShape) { dev@396: case 'vector': dev@396: return response.features as Vector; dev@396: case 'matrix': dev@396: return response.features as Matrix; dev@396: case 'tracks': dev@396: return response.features as Tracks; dev@396: case 'notes': dev@396: return { dev@396: shape: deducedShape, dev@396: collected: mapFeaturesToNotes( dev@349: response.features.collected as FeatureList, dev@349: response.outputDescriptor dev@396: ) dev@396: }; dev@396: case 'instants': dev@396: const featureData = response.features.collected as FeatureList; dev@396: return { dev@396: shape: deducedShape, dev@396: collected: featureData.map(feature => ({ dev@396: time: toSeconds(feature.timestamp), dev@396: label: feature.label dev@396: })) dev@396: }; dev@349: } dev@396: throwShapeError(); dev@349: } dev@368: dev@368: export interface PlotData { dev@368: cx: number; dev@368: cy: number; dev@368: } dev@368: dev@368: export interface PlotLayerData { dev@368: data: PlotData[]; dev@368: yDomain: [number, number]; dev@368: startTime: number; dev@368: duration: number; dev@368: } dev@368: dev@368: export function generatePlotData(features: VectorFeature[]): PlotLayerData[] { dev@368: dev@368: const winnowed = features.filter(feature => feature.data.length > 0); dev@368: dev@368: // First establish a [min,max] range across all of the features dev@368: let [min, max] = winnowed.reduce((acc, feature) => { dev@368: return feature.data.reduce((acc, val) => { dev@368: const [min, max] = acc; dev@368: return [Math.min(min, val), Math.max(max, val)]; dev@368: }, acc); dev@368: }, [Infinity, -Infinity]); dev@368: dev@368: if (min === Infinity) { dev@368: min = 0; dev@368: max = 1; dev@368: } dev@368: dev@368: if (min !== min || max !== max) { dev@368: console.warn('WARNING: min or max is NaN'); dev@368: min = 0; dev@368: max = 1; dev@368: } dev@368: dev@368: return winnowed.map(feature => { dev@368: let duration = 0; dev@368: dev@368: // Give the plot items positions relative to the start of the dev@368: // line, rather than relative to absolute time 0. This is dev@368: // because we'll be setting the layer timeline start property dev@368: // later on and these will be positioned relative to that dev@368: dev@368: const plotData = [...feature.data].map((val, i) => { dev@368: const t = i * feature.stepDuration; dev@368: duration = t + feature.stepDuration; dev@368: return { dev@368: cx: t, dev@368: cy: val dev@368: }; dev@368: }); dev@368: dev@368: return { dev@368: data: plotData, dev@368: yDomain: [min, max] as [number, number], dev@368: startTime: feature.startTime, dev@368: duration: duration dev@368: }; dev@368: }); dev@368: }