diff --git a/package.json b/package.json index 38cc2ed..d51b426 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@types/node": "^16.11.6", "d3": "^7.1.1", "jacdac-ts": "^1.24.20", + "react-copy-to-clipboard": "^5.1.0", "react-jacdac": "^1.1.3", "rxjs": "^7.5.2", "rxjs-spy": "^8.0.2", diff --git a/src/index.jsx b/src/index.jsx index 53e8c1f..338585b 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -7,7 +7,8 @@ const MicrobitController = lazy(() => import('./pages/MicrobitController')) import { HashRouter, Routes, Route } from 'react-router-dom' import Index from './pages/Index' - +import { Keyboard } from './pages/Keyboard' +import { MouseDemo } from './pages/MouseDemo' if (module.hot) { module.hot.accept() } @@ -17,6 +18,7 @@ ReactDOM.render( } /> } /> + , } /> + + + + } + /> + + + + } + /> , document.getElementById('root'), diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 3591f07..6f766ec 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -10,7 +10,7 @@ import * as d3 from 'd3' import { bus } from '../bus' -import { JDRegister, JDService, REPORT_UPDATE, SoundLevelReg, TraceRecorder } from 'jacdac-ts' +import { REPORT_UPDATE, SoundLevelReg, TraceRecorder } from 'jacdac-ts' import { JacdacProvider, useServices, useChange, useBus } from 'react-jacdac' import { @@ -45,67 +45,13 @@ import { Close } from '@mui/icons-material' import DataHandlerItem from '../views/dashboard/DataHandlerItem' import JDServiceItem from '../views/dashboard/JDServiceItem' -import { DataHandler } from '../sonification/handler/DataHandler' -import { DatumOutput } from '../sonification/output/DatumOutput' - -import { NoteSonify } from '../sonification/output/NoteSonify' -import { NoiseSonify } from '../sonification/output/NoiseSonify' -import { Speech } from '../sonification/output/Speech' -import { NoteHandler } from '../sonification/handler/NoteHandler' import { OutputStateChange } from '../sonification/OutputConstants' import { Datum } from '../sonification/Datum' -import { FilterRangeHandler } from '../sonification/handler/FilterRangeHandler' -import { RunningExtremaHandler } from '../sonification/handler/RunningExtremaHandler' -import { SlopeParityHandler } from '../sonification/handler/SlopeParityHandler' -import { FileOutput } from '../sonification/output/FileOutput' -import { SimpleDataHandler } from '../sonification/handler/SimpleDataHandler' - -export interface JDServiceWrapper { - name: string - id: string - serviceObject?: JDService - values: JDValueWrapper[] -} -export interface JDValueWrapper { - name: string - id: string - index: number - sinkId: number - domain: [number, number] - units: string - format: (value: number) => string - register: JDRegister - dataHandlers: DataHandlerWrapper[] - unsubscribe?: () => void -} +import { AVAILABLE_DATA_HANDLER_TEMPLATES } from './templates/DataHandlerTemplates' +import { JDServiceWrapper } from './templates/JDInterfaces' -export interface DataHandlerWrapper { - name: string - id: string - description: string - dataOutputs: DataOutputWrapper[] - handlerObject?: DataHandler - createHandler: (domain: [number, number]) => DataHandler - unsubscribe?: () => void - parameters?: ParameterWrapper[] -} - -export interface DataOutputWrapper { - name: string - id: string - createOutput: () => DatumOutput - outputObject?: DatumOutput - parameters?: ParameterWrapper[] -} - -export interface ParameterWrapper { - name: string - type: string - default?: (obj?: DataHandler | DatumOutput) => number - values?: { display: string; value: number }[] - handleUpdate: (value: number, obj?: DataHandler | DatumOutput) => void -} +import { DataHandlerWrapper } from './templates/DataHandlerInterfaces' export enum PlaybackState { PlayingLive, @@ -126,237 +72,6 @@ const SRV_INFO_MAP = { [SRV_TEMPERATURE]: { values: [''], units: '°C', format: d3.format('.1f'), domain: [-20, 60] }, } -export const AVAILABLE_DATA_OUTPUT_TEMPLATES = { - note: { - name: 'Note', - id: `Note-${Math.floor(Math.random() * Date.now())}`, - createOutput: () => new NoteSonify(), - parameters: [ - { - name: 'Stereo Pan', - type: 'list', - default: (obj?: DataHandler | DatumOutput) => 0, - values: [ - { display: 'Both', value: 0 }, - { display: 'Left', value: -1 }, - { display: 'Right', value: 1 }, - ], - handleUpdate: (value: number, obj?: DataHandler | DatumOutput) => { - if (obj) { - const ns = obj as NoteSonify - ns.stereoPannerNode.pan.value = value - } - }, - }, - ], - }, - noise: { - name: 'White Noise', - id: `White Noise-${Math.floor(Math.random() * Date.now())}`, - createOutput: () => new NoiseSonify(), - }, - earcon: { - name: 'Earcon', - id: `Earcon-${Math.floor(Math.random() * Date.now())}`, - createOutput: () => { - const fo = new FileOutput() - // Use long beep as the default - fetch(`./assets/shortbeep.wav`) - .then((res) => res.arrayBuffer()) - .then((buffer: ArrayBuffer) => { - fo.buffer = buffer - }) - return fo - }, - parameters: [ - { - name: 'Earcon to Play', - type: 'list', - default: (obj?: DataHandler | DatumOutput) => 0, - values: [ - { display: 'Short Beep', value: 0 }, - { display: 'Long Beep', value: 1 }, - { display: 'Bell', value: 2 }, - { display: 'Whistle Up', value: 3 }, - { display: 'Whistle Down', value: 4 }, - ], - handleUpdate: (value: number, obj?: DataHandler | DatumOutput) => { - if (obj) { - const file_list = [ - 'shortbeep.wav', - 'beep.wav', - 'bell.mp3', - 'whistle%20up.wav', - 'whistle%20down.wav', - ] - const fo = obj as FileOutput - fetch(`./assets/${file_list[value]}`) - .then((res) => res.arrayBuffer()) - .then((buffer: ArrayBuffer) => { - fo.buffer = buffer - }) - } - }, - }, - ], - }, - speech: { - name: 'Speech', - id: `Speech-${Math.floor(Math.random() * Date.now())}`, - createOutput: () => new Speech(), - parameters: [ - { - name: 'Interrupt when new point arrives?', - type: 'list', - default: (obj?: DataHandler | DatumOutput) => 0, - values: [ - { display: 'Yes', value: 0 }, - { display: 'No', value: 1 }, - ], - handleUpdate: (value: number, obj?: DataHandler | DatumOutput) => { - if (obj) { - const sp = obj as Speech - sp.polite = value == 1 ? true : false - } - }, - } - ] - }, -} - -const initializeDataOutput = (output: DataOutputWrapper): DataOutputWrapper => { - return { ...output, outputObject: output.createOutput() } -} - -export const AVAILABLE_DATA_HANDLER_TEMPLATES: DataHandlerWrapper[] = [ - { - name: 'Note Handler', - id: `Note Handler-${Math.floor(Math.random() * Date.now())}`, - description: 'Converts data to an audible note range.', - dataOutputs: [initializeDataOutput(AVAILABLE_DATA_OUTPUT_TEMPLATES.note)], - createHandler: (domain: [number, number]) => new NoteHandler(domain), - }, - { - name: 'Filter Range Handler', - id: `Filter Range Handler-${Math.floor(Math.random() * Date.now())}`, - description: "Filters data within the provided range. If within range, sent to this handler's outputs.", - dataOutputs: [ - initializeDataOutput(AVAILABLE_DATA_OUTPUT_TEMPLATES.noise), - AVAILABLE_DATA_OUTPUT_TEMPLATES.earcon, - ], - createHandler: (domain: [number, number]) => - new FilterRangeHandler([ - (domain[1] - domain[0]) * 0.4 + domain[0], - (domain[1] - domain[0]) * 0.6 + domain[0], - ]), - parameters: [ - { - name: 'Min', - type: 'number', - default: (obj?: DataHandler | DatumOutput) => { - if (obj) { - const frh = obj as FilterRangeHandler - return frh.domain[0] - } else { - return 0.4 - } - }, - handleUpdate: (value: number, obj?: DataHandler | DatumOutput) => { - if (obj) { - const frh = obj as FilterRangeHandler - frh.domain = [value, frh.domain[1]] - } - }, - }, - { - name: 'Max', - type: 'number', - default: (obj?: DataHandler | DatumOutput) => { - if (obj) { - const frh = obj as FilterRangeHandler - return frh.domain[1] - } else { - return 0.6 - } - }, - handleUpdate: (value: number, obj?: DataHandler | DatumOutput) => { - if (obj) { - const frh = obj as FilterRangeHandler - frh.domain = [frh.domain[0], value] - } - }, - }, - ], - }, - { - name: 'Extrema Handler', - id: `Extrema Handler-${Math.floor(Math.random() * Date.now())}`, - description: 'Finds the new extrema value (maximum and/or minimum) in the data stream.', - dataOutputs: [ - AVAILABLE_DATA_OUTPUT_TEMPLATES.earcon, - initializeDataOutput(AVAILABLE_DATA_OUTPUT_TEMPLATES.speech), - ], - createHandler: (domain: [number, number]) => new RunningExtremaHandler(), - parameters: [ - { - name: 'Extrema to Find', - type: 'list', - default: (obj?: DataHandler | DatumOutput) => 0, - values: [ - { display: 'Maximum and Minimum', value: 0 }, - { display: 'Maximum Only', value: 1 }, - { display: 'Minimum Only', value: -1 }, - ], - handleUpdate: (value: number, obj?: DataHandler | DatumOutput) => { - if (obj) { - const reh = obj as RunningExtremaHandler - reh.direction = value - } - }, - }, - ], - }, - // { name: 'Outlier Detection Handler', description: 'Description of outlier detection handler' }, - // { name: 'Slope Handler', description: 'Description of slope handler', createHandler: () => new Slope() }, - { - name: 'Slope Change Handler', - id: `Slope Change Handler-${Math.floor(Math.random() * Date.now())}`, - description: - 'Finds direction of slope changes in the data stream. When the data goes from increasing to decreasing, and vise-versa.', - dataOutputs: [ - AVAILABLE_DATA_OUTPUT_TEMPLATES.earcon, - initializeDataOutput(AVAILABLE_DATA_OUTPUT_TEMPLATES.speech), - ], - createHandler: (domain: [number, number]) => new SlopeParityHandler(), - parameters: [ - { - name: 'Direction to Find', - type: 'list', - default: (obj?: DataHandler | DatumOutput) => 0, - values: [ - { display: 'Postive and Negative', value: 0 }, - { display: 'Positive Only', value: 1 }, - { display: 'Negative Only', value: -1 }, - ], - handleUpdate: (value: number, obj?: DataHandler | DatumOutput) => { - if (obj) { - const sph = obj as SlopeParityHandler - sph.direction = value - } - }, - }, - ], - }, - - { - name: 'Simple Handler', - id: `Simple Handler-${Math.floor(Math.random() * Date.now())}`, - description: 'Outputs the raw data stream without processing.', - dataOutputs: [initializeDataOutput(AVAILABLE_DATA_OUTPUT_TEMPLATES.speech)], - createHandler: (domain: [number, number]) => new SimpleDataHandler(), - }, -] - function saveText(name: string, data: string, mimeType?: string) { if (!mimeType) { if (/\.(csv|txt)/i.test(name)) mimeType = 'text/plain' @@ -546,6 +261,7 @@ export function DashboardView() { template: DataHandlerWrapper, ) => { console.log(add, serviceId, valueId, template) + const servicesCopy = services.map((service) => { if (serviceId === service.id) { const values = service.values.map((value) => { @@ -730,7 +446,6 @@ export function DashboardView() { type: 'number', }} /> */} - diff --git a/src/pages/Demo.tsx b/src/pages/Demo.tsx index 31ad9b7..24243f0 100644 --- a/src/pages/Demo.tsx +++ b/src/pages/Demo.tsx @@ -18,17 +18,23 @@ import { DemoFileOutput } from '../views/demos/DemoFileOutput' import { op } from 'arquero' import { DemoSlopeParityV1 } from '../views/demos/DemoSlopeParityV1' import { DemoSlopeParityV2 } from '../views/demos/DemoSlopeParityV2' +import { DemoSlope } from '../views/demos/DemoSlope' import { DemoRunningExtrema } from '../views/demos/DemoRunningExtrema' import Table from 'arquero/dist/types/table/table' const DEMO_VIEW_MAP = { simple: { value: 'simple', label: 'Simple sonification', component: DemoSimple }, highlightRegion: { value: 'highlightRegion', label: 'Highlight points for region', component: DemoHighlightRegion }, - speechHighlight: { value: 'speechHighlight', label: 'Speak points in range', component: DemoSpeakRange}, - fileOutput: {value: 'fileOutput', label: 'Point of interest notification', component: DemoFileOutput}, - slopeParityV1: {value: 'slopeParityV1', label: 'Slope direction change notification', component: DemoSlopeParityV1}, - slopeParityV2: {value: 'slopeParityV2', label: 'Slope parity notification', component: DemoSlopeParityV2}, - runningExtrema: {value: 'runningExtrema', label: 'Running extrema notification', component: DemoRunningExtrema} + speechHighlight: { value: 'speechHighlight', label: 'Speak points in range', component: DemoSpeakRange }, + fileOutput: { value: 'fileOutput', label: 'Point of interest notification', component: DemoFileOutput }, + slopeParityV1: { + value: 'slopeParityV1', + label: 'Slope direction change notification', + component: DemoSlopeParityV1, + }, + slopeParityV2: { value: 'slopeParityV2', label: 'Slope parity notification', component: DemoSlopeParityV2 }, + slope: { value: 'slope', label: 'Slope display', component: DemoSlope }, + runningExtrema: { value: 'runningExtrema', label: 'Running extrema notification', component: DemoRunningExtrema }, } let demoViewRef: React.RefObject | DemoHighlightRegion> = React.createRef() diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index c6391e2..55c8b0e 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -7,6 +7,8 @@ const Index: FC = (props) => { { name: 'Jacdac', url: '/jacdac' }, { name: 'Jacdac and microbit', url: '/jacdacmicrobit' }, { name: 'Dashboard', url: '/dashboard' }, + { name: 'Keyboard', url: '/keyboard' }, + { name: 'MouseDemo', url: '/mousedemo' }, ] return ( diff --git a/src/pages/Key.tsx b/src/pages/Key.tsx new file mode 100644 index 0000000..fe34e01 --- /dev/null +++ b/src/pages/Key.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import '../styles/keyboard.css' +export interface KeyProps { + keyb : string, + black : boolean, + leftAdjust : number +} + +export interface KeyState { + class : string +} + + +export class Key extends React.Component { + constructor(props : KeyProps) { + super(props); + this.state = { + class : this.props.black ? "key black" : "key white" + } + } + + public render() { + const left = { + left : this.props.leftAdjust + } as const; + return ( +
+
+ ) + } +} \ No newline at end of file diff --git a/src/pages/Keyboard.tsx b/src/pages/Keyboard.tsx new file mode 100644 index 0000000..c198a8d --- /dev/null +++ b/src/pages/Keyboard.tsx @@ -0,0 +1,128 @@ +import React from 'react' +import { NoteSonify } from '../sonification/output/NoteSonify' +import { SimpleDataHandler } from '../sonification/handler/SimpleDataHandler' +import '../styles/keyboard.css' +import { OutputStateChange, getPianoKeys } from '../sonification/OutputConstants' +import { DataSink } from '../sonification/DataSink' +import { filter, Observable, OperatorFunction, pipe, Subject, UnaryFunction } from 'rxjs' +import { Datum } from '../sonification/Datum' +import { OutputEngine } from '../sonification/OutputEngine' +import { Key } from '../pages/Key' + +const DEBUG = false + +export interface KeyboardProps {} + +export interface KeyboardState { + musicHandler: SimpleDataHandler + pianoKeys : Map + validKeys: string[] +} + + +export class Keyboard extends React.Component { + + private streaming : boolean + private sink : DataSink | undefined + private stream : Subject | undefined + private currKey : string | undefined + + constructor(props : KeyboardProps) { + super(props) + this.state = { + musicHandler : new SimpleDataHandler(new NoteSonify()), + pianoKeys : getPianoKeys(), + validKeys : Array.from(getPianoKeys().keys()) + } + this.streaming = false + this.currKey = undefined + } + + /** + * helper function to filter out null values from subjects, and create an observable for the sink to subscribe. + * Source: https://stackoverflow.com/questions/57999777/filter-undefined-from-rxjs-observable + * @returns observable + */ + filterNullish(): UnaryFunction, Observable> { + return pipe(filter((x) => x != null) as OperatorFunction) + } + + handleStartStreaming = () => { + console.log('entering handle stream') + let sinkID: number = 0 + let src = this.sink + if (!this.streaming) { + if (DEBUG) console.log('streaming was false') + /** + * check if a sink exists to stream data to. else create one + */ + if (!src) { + src = OutputEngine.getInstance().addSink('jacdac accelerometer X axis') + if (DEBUG) console.log(`added sink to stream x axis data ${this.sink}`) + src.addDataHandler(this.state.musicHandler) + sinkID = src.id + this.sink = src + } + /** + * check if a observable exists for the data. + * If not, create an RXJS Subject, filter out null values and change it to be typed as observable, and then set this as a stream for the source. + */ + + let sourceX = this.stream + if (!sourceX) { + sourceX = new Subject() + this.stream = sourceX + OutputEngine.getInstance().setStream(sinkID, sourceX.pipe(this.filterNullish())) + } + OutputEngine.getInstance().next(OutputStateChange.Play) + } else { + OutputEngine.getInstance().next(OutputStateChange.Stop) + } + + this.streaming = !this.streaming + } + + handleKeyDown = (event: React.KeyboardEvent) => { + if (!this.streaming) this.handleStartStreaming() + let keyDown = event.code; + if (DEBUG) console.log("key down!", keyDown) + if (this.sink && this.stream && this.state.validKeys.includes(keyDown)) { + document.getElementById(keyDown)!.classList.add("selected") + OutputEngine.getInstance().next(OutputStateChange.Play) + this.currKey = keyDown + this.stream.next(new Datum(this.sink.id, this.state.pianoKeys.get(keyDown)![0])) + } + } + + handleKeyUp = (event: React.KeyboardEvent) => { + let keyUp = event.code + if (this.state.validKeys.includes(keyUp)) document.getElementById(keyUp)!.classList.remove("selected") + if (keyUp == this.currKey) { + OutputEngine.getInstance().next(OutputStateChange.Pause) + this.currKey = undefined + } + } + + public render() { + var white = 0; + return ( +
+ +
+ { + this.state.validKeys.map((id) => { + // if the key is black + if (this.state.pianoKeys.get(id)![1]) { + return + } else { // if the key is white + white++; + return + } + }) + } +
+
+ ) + } + +} \ No newline at end of file diff --git a/src/pages/MouseDemo.tsx b/src/pages/MouseDemo.tsx new file mode 100644 index 0000000..a09a1a5 --- /dev/null +++ b/src/pages/MouseDemo.tsx @@ -0,0 +1,134 @@ +import React from 'react' +import { NoteSonify } from '../sonification/output/NoteSonify' +import { SimpleDataHandler } from '../sonification/handler/SimpleDataHandler' +import '../styles/keyboard.css' +import { OutputStateChange, getPianoKeys } from '../sonification/OutputConstants' +import { DataSink } from '../sonification/DataSink' +import { filter, Observable, OperatorFunction, pipe, Subject, UnaryFunction } from 'rxjs' +import { Datum } from '../sonification/Datum' +import { OutputEngine } from '../sonification/OutputEngine' +import { NoteHandler } from '../sonification/handler/NoteHandler' +import { MousePositionPianoHandler } from '../sonification/handler/MousePositionPianoHandler' + +const DEBUG = false + +export interface MouseDemoProps {} + +export interface MouseDemoState { + height : number + width : number +} + + +export class MouseDemo extends React.Component { + + private streaming : boolean + private xSink : DataSink | undefined + private ySink : DataSink | undefined + private xStream : Subject | undefined + private yStream : Subject | undefined + + constructor(props : MouseDemoProps) { + super(props) + this.state = { + height: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight, + width: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth + } + this.streaming = false + console.log("height", this.state.height) + console.log("width", this.state.width) + } + + /** + * helper function to filter out null values from subjects, and create an observable for the sink to subscribe. + * Source: https://stackoverflow.com/questions/57999777/filter-undefined-from-rxjs-observable + * @returns observable + */ + filterNullish(): UnaryFunction, Observable> { + return pipe(filter((x) => x != null) as OperatorFunction) + } + + handleStartStreaming = () => { + let xSinkID: number = 0 + let ySinkID: number = 1 + let srcX = this.xSink + let srcY = this.ySink + if (!this.streaming) { + if (DEBUG) console.log('streaming was false') + /** + * check if a sink exists to stream X axis data to. else create one. + */ + if (!srcX) { + srcX = OutputEngine.getInstance().addSink('jacdac accelerometer X axis') + if (DEBUG) console.log(`added sink to stream x axis data ${this.xSink}`) + srcX.addDataHandler(new MousePositionPianoHandler ([0, this.state.width], new NoteSonify(-1))) + xSinkID = srcX.id + this.xSink = srcX + } + + /** + * check if a sink exists to stream Y axis data to. else create one. + */ + if (!srcY) { + srcY = OutputEngine.getInstance().addSink('jacdac accelerometer Y axis') + if (DEBUG) console.log(`added sink to stream y axis data ${this.ySink}`) + srcY.addDataHandler(new NoteHandler([0, this.state.height], new NoteSonify(1))) + ySinkID = srcY.id + this.ySink = srcY + } + /** + * check if a observable exists for each of the axes. + * If not, create an RXJS Subject, filter out null values and change it to be typed as observable, and then set this as a stream for the source. + */ + + let sourceX = this.xStream + if (!sourceX) { + sourceX = new Subject() + this.xStream = sourceX + OutputEngine.getInstance().setStream(xSinkID, sourceX.pipe(this.filterNullish())) + } + + let sourceY = this.yStream + if (!sourceY) { + sourceY = new Subject() + this.yStream = sourceY + OutputEngine.getInstance().setStream(ySinkID, sourceY.pipe(this.filterNullish())) + } + + OutputEngine.getInstance().next(OutputStateChange.Play) + } else { + OutputEngine.getInstance().next(OutputStateChange.Stop) + } + + this.streaming = !this.streaming + } + + startDemo = () => { + if (!this.streaming) { + this.handleStartStreaming() + document.addEventListener("mousemove", this.sonifyPosition) + } + } + + sonifyPosition = (event: MouseEvent): void => { + let x = event.offsetX; // should use -1 for pan + if (this.xSink && this.xStream) { + OutputEngine.getInstance().next(OutputStateChange.Play) + this.xStream.next(new Datum(this.xSink.id, x)) + } + let y = event.offsetY; // should use 1 for pan + if (this.ySink && this.yStream) { + OutputEngine.getInstance().next(OutputStateChange.Play) + this.yStream.next(new Datum(this.ySink.id, y)) + } + } + + public render() { + return ( +
+ +
+ ) + } + +} \ No newline at end of file diff --git a/src/pages/templates/DataHandlerInterfaces.tsx b/src/pages/templates/DataHandlerInterfaces.tsx new file mode 100644 index 0000000..6504428 --- /dev/null +++ b/src/pages/templates/DataHandlerInterfaces.tsx @@ -0,0 +1,22 @@ +import { DataHandler } from '../../sonification/handler/DataHandler' +import { DatumOutput } from '../../sonification/output/DatumOutput' +import { ParameterWrapper } from '../templates/ParameterInterface' + +export interface DataHandlerWrapper { + name: string + id: string + description: string + dataOutputs: DataOutputWrapper[] + handlerObject?: DataHandler + createHandler: (domain: [number, number]) => DataHandler + unsubscribe?: () => void + parameters?: ParameterWrapper[] +} + +export interface DataOutputWrapper { + name: string + id: string + createOutput: () => DatumOutput + outputObject?: DatumOutput + parameters?: ParameterWrapper[] +} diff --git a/src/pages/templates/DataHandlerTemplates.tsx b/src/pages/templates/DataHandlerTemplates.tsx new file mode 100644 index 0000000..8cfd198 --- /dev/null +++ b/src/pages/templates/DataHandlerTemplates.tsx @@ -0,0 +1,157 @@ +import { DataHandler } from '../../sonification/handler/DataHandler' +import { DatumOutput } from '../../sonification/output/DatumOutput' + +import { NoteHandler } from '../../sonification/handler/NoteHandler' + +import { FilterRangeHandler } from '../../sonification/handler/FilterRangeHandler' +import { RunningExtremaHandler } from '../../sonification/handler/RunningExtremaHandler' +import { SlopeParityHandler } from '../../sonification/handler/SlopeParityHandler' +import { SimpleDataHandler } from '../../sonification/handler/SimpleDataHandler' + +import { DataOutputWrapper } from './DataHandlerInterfaces' +import { AVAILABLE_DATA_OUTPUT_TEMPLATES } from './DataOutputTemplates' +import { ParameterWrapper } from '../templates/ParameterInterface' + +export interface DataHandlerWrapper { + name: string + id: string + description: string + dataOutputs: DataOutputWrapper[] + handlerObject?: DataHandler + createHandler: (domain: [number, number]) => DataHandler + unsubscribe?: () => void + parameters?: ParameterWrapper[] +} + +export const initializeDataOutput = (output: DataOutputWrapper): DataOutputWrapper => { + return { ...output, outputObject: output.createOutput() } +} + +export const AVAILABLE_DATA_HANDLER_TEMPLATES: DataHandlerWrapper[] = [ + { + name: 'Note Handler', + id: `Note Handler-${Math.floor(Math.random() * Date.now())}`, + description: 'Converts data to an audible note range.', + dataOutputs: [initializeDataOutput(AVAILABLE_DATA_OUTPUT_TEMPLATES.note)], + createHandler: (domain: [number, number]) => new NoteHandler(domain), + }, + { + name: 'Filter Range Handler', + id: `Filter Range Handler-${Math.floor(Math.random() * Date.now())}`, + description: "Filters data within the provided range. If within range, sent to this handler's outputs.", + dataOutputs: [ + initializeDataOutput(AVAILABLE_DATA_OUTPUT_TEMPLATES.noise), + AVAILABLE_DATA_OUTPUT_TEMPLATES.earcon, + ], + createHandler: (domain: [number, number]) => + new FilterRangeHandler([ + (domain[1] - domain[0]) * 0.4 + domain[0], + (domain[1] - domain[0]) * 0.6 + domain[0], + ]), + parameters: [ + { + name: 'Min', + type: 'number', + default: (obj?: DataHandler | DatumOutput) => { + if (obj) { + const frh = obj as FilterRangeHandler + return frh.domain[0] + } else { + return 0.4 + } + }, + handleUpdate: (value: number, obj?: DataHandler | DatumOutput) => { + if (obj) { + const frh = obj as FilterRangeHandler + frh.domain = [value, frh.domain[1]] + } + }, + }, + { + name: 'Max', + type: 'number', + default: (obj?: DataHandler | DatumOutput) => { + if (obj) { + const frh = obj as FilterRangeHandler + return frh.domain[1] + } else { + return 0.6 + } + }, + handleUpdate: (value: number, obj?: DataHandler | DatumOutput) => { + if (obj) { + const frh = obj as FilterRangeHandler + frh.domain = [frh.domain[0], value] + } + }, + }, + ], + }, + { + name: 'Extrema Handler', + id: `Extrema Handler-${Math.floor(Math.random() * Date.now())}`, + description: 'Finds the new extrema value (maximum and/or minimum) in the data stream.', + dataOutputs: [ + AVAILABLE_DATA_OUTPUT_TEMPLATES.earcon, + initializeDataOutput(AVAILABLE_DATA_OUTPUT_TEMPLATES.speech), + ], + createHandler: (domain: [number, number]) => new RunningExtremaHandler(), + parameters: [ + { + name: 'Extrema to Find', + type: 'list', + default: (obj?: DataHandler | DatumOutput) => 0, + values: [ + { display: 'Maximum and Minimum', value: 0 }, + { display: 'Maximum Only', value: 1 }, + { display: 'Minimum Only', value: -1 }, + ], + handleUpdate: (value: number, obj?: DataHandler | DatumOutput) => { + if (obj) { + const reh = obj as RunningExtremaHandler + reh.direction = value + } + }, + }, + ], + }, + // { name: 'Outlier Detection Handler', description: 'Description of outlier detection handler' }, + // { name: 'Slope Handler', description: 'Description of slope handler', createHandler: () => new Slope() }, + { + name: 'Slope Change Handler', + id: `Slope Change Handler-${Math.floor(Math.random() * Date.now())}`, + description: + 'Finds direction of slope changes in the data stream. When the data goes from increasing to decreasing, and vise-versa.', + dataOutputs: [ + AVAILABLE_DATA_OUTPUT_TEMPLATES.earcon, + initializeDataOutput(AVAILABLE_DATA_OUTPUT_TEMPLATES.speech), + ], + createHandler: (domain: [number, number]) => new SlopeParityHandler(), + parameters: [ + { + name: 'Direction to Find', + type: 'list', + default: (obj?: DataHandler | DatumOutput) => 0, + values: [ + { display: 'Postive and Negative', value: 0 }, + { display: 'Positive Only', value: 1 }, + { display: 'Negative Only', value: -1 }, + ], + handleUpdate: (value: number, obj?: DataHandler | DatumOutput) => { + if (obj) { + const sph = obj as SlopeParityHandler + sph.direction = value + } + }, + }, + ], + }, + + { + name: 'Simple Handler', + id: `Simple Handler-${Math.floor(Math.random() * Date.now())}`, + description: 'Outputs the raw data stream without processing.', + dataOutputs: [initializeDataOutput(AVAILABLE_DATA_OUTPUT_TEMPLATES.speech)], + createHandler: (domain: [number, number]) => new SimpleDataHandler(), + }, +] diff --git a/src/pages/templates/DataOutputTemplates.tsx b/src/pages/templates/DataOutputTemplates.tsx new file mode 100644 index 0000000..fa57cda --- /dev/null +++ b/src/pages/templates/DataOutputTemplates.tsx @@ -0,0 +1,105 @@ +import { DataHandler } from '../../sonification/handler/DataHandler' +import { DatumOutput } from '../../sonification/output/DatumOutput' + +import { NoteSonify } from '../../sonification/output/NoteSonify' +import { NoiseSonify } from '../../sonification/output/NoiseSonify' +import { Speech } from '../../sonification/output/Speech' +import { FileOutput } from '../../sonification/output/FileOutput' + +export const AVAILABLE_DATA_OUTPUT_TEMPLATES = { + note: { + name: 'Note', + id: `Note-${Math.floor(Math.random() * Date.now())}`, + createOutput: () => new NoteSonify(), + parameters: [ + { + name: 'Stereo Pan', + type: 'list', + default: (obj?: DataHandler | DatumOutput) => 0, + values: [ + { display: 'Both', value: 0 }, + { display: 'Left', value: -1 }, + { display: 'Right', value: 1 }, + ], + handleUpdate: (value: number, obj?: DataHandler | DatumOutput) => { + if (obj) { + const ns = obj as NoteSonify + ns.stereoPannerNode.pan.value = value + } + }, + }, + ], + }, + noise: { + name: 'White Noise', + id: `White Noise-${Math.floor(Math.random() * Date.now())}`, + createOutput: () => new NoiseSonify(), + }, + earcon: { + name: 'Earcon', + id: `Earcon-${Math.floor(Math.random() * Date.now())}`, + createOutput: () => { + const fo = new FileOutput() + // Use long beep as the default + fetch(`./assets/shortbeep.wav`) + .then((res) => res.arrayBuffer()) + .then((buffer: ArrayBuffer) => { + fo.buffer = buffer + }) + return fo + }, + parameters: [ + { + name: 'Earcon to Play', + type: 'list', + default: (obj?: DataHandler | DatumOutput) => 0, + values: [ + { display: 'Short Beep', value: 0 }, + { display: 'Long Beep', value: 1 }, + { display: 'Bell', value: 2 }, + { display: 'Whistle Up', value: 3 }, + { display: 'Whistle Down', value: 4 }, + ], + handleUpdate: (value: number, obj?: DataHandler | DatumOutput) => { + if (obj) { + const file_list = [ + 'shortbeep.wav', + 'beep.wav', + 'bell.mp3', + 'whistle%20up.wav', + 'whistle%20down.wav', + ] + const fo = obj as FileOutput + fetch(`./assets/${file_list[value]}`) + .then((res) => res.arrayBuffer()) + .then((buffer: ArrayBuffer) => { + fo.buffer = buffer + }) + } + }, + }, + ], + }, + speech: { + name: 'Speech', + id: `Speech-${Math.floor(Math.random() * Date.now())}`, + createOutput: () => new Speech(), + parameters: [ + { + name: 'Interrupt when new point arrives?', + type: 'list', + default: (obj?: DataHandler | DatumOutput) => 0, + values: [ + { display: 'Yes', value: 0 }, + { display: 'No', value: 1 }, + ], + handleUpdate: (value: number, obj?: DataHandler | DatumOutput) => { + if (obj) { + const sp = obj as Speech + sp.polite = value == 1 ? true : false + } + }, + }, + ], + }, +} diff --git a/src/pages/templates/JDInterfaces.tsx b/src/pages/templates/JDInterfaces.tsx new file mode 100644 index 0000000..5455f22 --- /dev/null +++ b/src/pages/templates/JDInterfaces.tsx @@ -0,0 +1,23 @@ +import JDServiceItem from '../../views/dashboard/JDServiceItem' +import { JDRegister, JDService } from 'jacdac-ts' +import { DataHandlerWrapper } from './DataHandlerInterfaces' + +export interface JDServiceWrapper { + name: string + id: string + serviceObject?: JDService + values: JDValueWrapper[] +} + +export interface JDValueWrapper { + name: string + id: string + index: number + sinkId: number + domain: [number, number] + units: string + format: (value: number) => string + register: JDRegister + dataHandlers: DataHandlerWrapper[] + unsubscribe?: () => void +} diff --git a/src/pages/templates/ParameterInterface.tsx b/src/pages/templates/ParameterInterface.tsx new file mode 100644 index 0000000..87b6ed2 --- /dev/null +++ b/src/pages/templates/ParameterInterface.tsx @@ -0,0 +1,10 @@ +import { DataHandler } from '../../sonification/handler/DataHandler' +import { DatumOutput } from '../../sonification/output/DatumOutput' + +export interface ParameterWrapper { + name: string + type: string + default?: (obj?: DataHandler | DatumOutput) => number + values?: { display: string; value: number }[] + handleUpdate: (value: number, obj?: DataHandler | DatumOutput) => void +} diff --git a/src/sonification/DataSink.ts b/src/sonification/DataSink.ts index dea97f2..364d4cd 100644 --- a/src/sonification/DataSink.ts +++ b/src/sonification/DataSink.ts @@ -2,10 +2,8 @@ import { map, Observable, tap, Subject } from 'rxjs' import { getSonificationLoggingLevel, OutputStateChange, SonificationLoggingLevel } from './OutputConstants' import { DataHandler } from './handler/DataHandler' - const DEBUG = false - /** * The DataSink for a stream of data */ @@ -33,15 +31,17 @@ export class DataSink extends Subject { this._dataHandlers = this._dataHandlers.filter((dataHandler) => dataHandler !== dataHandler) } public addDataHandler(dataHandler: DataHandler) { - debugStatic(SonificationLoggingLevel.DEBUG,`Adding data handeler: ${dataHandler}. length of _handelers of sink is ${this._dataHandlers.length}`) + debugStatic( + SonificationLoggingLevel.DEBUG, + `Adding data handeler: ${dataHandler}. length of _handelers of sink is ${this._dataHandlers.length}`, + ) let observable = this as Observable - if(this.overrideDatum) { + if (this.overrideDatum) { if (this._dataHandlers.length > 0) observable = this._dataHandlers[this._dataHandlers.length - 1] as Observable } - debugStatic(SonificationLoggingLevel.DEBUG, `printing ${observable}`) dataHandler.setupSubscription(observable) this._dataHandlers.push(dataHandler) @@ -55,15 +55,13 @@ export class DataSink extends Subject { * @param description A description for the DataSink */ - constructor(id: number, description: String,overrideDatum:boolean = false) { - + constructor(id: number, description: String, overrideDatum: boolean = false) { super() this.id = id this._description = description this._dataHandlers = new Array() this.overrideDatum = overrideDatum - } //////////////////////////////// HELPER METHODS /////////////////////////////////// @@ -82,7 +80,6 @@ export class DataSink extends Subject { }), debug(SonificationLoggingLevel.DEBUG, `dataSink`, DEBUG), - ) .subscribe(this) } @@ -110,11 +107,9 @@ const debug = (level: number, message: string, watch: boolean) => (source: Obser } const debugStatic = (level: number, message: string) => { - if (DEBUG) { if (level >= getSonificationLoggingLevel()) { console.log(message) } //else console.log('debug message dumped') } - } diff --git a/src/sonification/Datum.ts b/src/sonification/Datum.ts index 47d75c7..9ba2a11 100644 --- a/src/sonification/Datum.ts +++ b/src/sonification/Datum.ts @@ -6,10 +6,9 @@ import * as d3 from 'd3' * * All data points have certain properties and abilities, however * @field value The raw data value associated with this point - * @field adjustedValue An adjusted value that may be assigned to a point for output. - * @field previous The previous point in the sequence for this sink * @method toString() Returns a string describing this data point * @field sink The data sink this point is associated with [not sure if we need this pointer, but for completeness...] + * @field time The time this data point was created. Defaults to d3.now() */ export class Datum { diff --git a/src/sonification/OutputConstants.ts b/src/sonification/OutputConstants.ts index 2c6c4e3..d75fd12 100644 --- a/src/sonification/OutputConstants.ts +++ b/src/sonification/OutputConstants.ts @@ -58,3 +58,36 @@ export function getSonificationLoggingLevel(): SonificationLoggingLevel { export function setSonificationsLoggingLevel(level: SonificationLoggingLevel) { sonificationLoggingLevel = level } + +// maps from key -> frequency and if note is black +// currently uses the 3rd and 4th octave +const pianoKeys = new Map([ + ['KeyQ', [130.81, false]], // C3 + ['Digit2', [138.59, true]], // C3# + ['KeyW', [146.83, false]], // D3 + ['Digit3', [155.56, true]], // D3# + ['KeyE', [164.81, false]], // E3 + ['KeyR', [174.61, false]], // F3 + ['Digit5', [185.0, true]], // F3# + ['KeyT', [196.0, false]], // G3 + ['Digit6', [207.65, true]], // G3# + ['KeyY', [220.0, false]], // A3 + ['Digit7', [233.08, true]], // A3# + ['KeyU', [246.94, false]], // B3 + ['KeyZ', [261.63, false]], // C4 + ['KeyS', [277.18, true]], // C4# + ['KeyX', [293.66, false]], // D4 + ['KeyD', [311.13, true]], // D4# + ['KeyC', [329.63, false]], // E4 + ['KeyV', [349.23, false]], // F4 + ['KeyG', [369.99, true]], // F4# + ['KeyB', [392.0, false]], // G4 + ['KeyH', [415.3, true]], // G4# + ['KeyN', [440.0, false]], // A4 + ['KeyJ', [466.16, true]], // A4# + ['KeyM', [493.88, false]], // B4 +]) + +export function getPianoKeys() { + return pianoKeys +} diff --git a/src/sonification/OutputEngine.ts b/src/sonification/OutputEngine.ts index 217fa55..eaa2945 100644 --- a/src/sonification/OutputEngine.ts +++ b/src/sonification/OutputEngine.ts @@ -88,21 +88,6 @@ export class OutputEngine extends BehaviorSubject { if (!sink && !sinkId) throw Error('Must specify sink or ID') } - /** - * @deprecated - * - * pushPoint is sort of legacy. It feeds data in to the sink by calling - * sink.next(). However this should not be used as things like automatic - * calls to complete() when the stream ends break when you use this approach. - * - * @param x A number - * @param sinkId Which sink it should go to - */ - pushPoint(x: number, sinkId: number) { - let sink = this.getSink(sinkId) - sink.next(new Datum(sinkId, x)) - } - /////////////////// STREAM SUPPORT ///////////////////////////////// /** diff --git a/src/sonification/handler/DataHandler.ts b/src/sonification/handler/DataHandler.ts index 0ba1ba7..5665648 100644 --- a/src/sonification/handler/DataHandler.ts +++ b/src/sonification/handler/DataHandler.ts @@ -8,7 +8,6 @@ const DEBUG = false * A DataHandler class is used to decide how to output each data point. */ export abstract class DataHandler extends Subject { - private subscription?: Subscription private _outputs: Array @@ -23,8 +22,8 @@ export abstract class DataHandler extends Subject { } /** - * - * @param output + * + * @param output */ public removeOutput(output: DatumOutput) { this._outputs = this._outputs.filter((o) => o !== output) @@ -70,7 +69,9 @@ export abstract class DataHandler extends Subject { */ constructor(output?: DatumOutput) { super() + this._outputs = new Array() + if (output) this.addOutput(output) } } diff --git a/src/sonification/handler/MousePositionPianoHandler.ts b/src/sonification/handler/MousePositionPianoHandler.ts new file mode 100644 index 0000000..52e03bf --- /dev/null +++ b/src/sonification/handler/MousePositionPianoHandler.ts @@ -0,0 +1,53 @@ +import { DatumOutput } from '../output/DatumOutput' +import { getPianoKeys } from '../OutputConstants' +import { ScaleHandler } from './ScaleHandler' +/** + * A DataHandler that outputs a Datum as a note in the audible range. + * Assumes a note should be played in the general range of 80 to 500 Hz to sound nice + */ +export class MousePositionPianoHandler extends ScaleHandler { + /** + * Sets up a default target range that is audible. Uses the Mel Scale (https://www.wikiwand.com/en/Mel_scale) + * @param sink. DataSink that is providing data to this Handler. + * @param targetRange The audible range the note should be in + * @param volume How loudly to play the note. + */ + + constructor(domain?: [number, number], output?: DatumOutput) { + super(MousePositionPianoHandler.pianoConversion, domain, [80, 450], output) + } + + public static pianoConversion(num, domain, range): number { + console.log(`domain is${domain}, number is ${num}` ) + var pianoKeys = getPianoKeys() + var numberOfKeys = pianoKeys.size; + // console.log(`number of keys is ${numberOfKeys}`) + var step:number = domain[1]-domain[0]/numberOfKeys + console.log(`step for mapping mouse to piano is${step} `) + var buckets:number[] = []; + var notes:number[] = []; + for(var i=0;i) { + debugStatic(SonificationLoggingLevel.DEBUG, `setting up subscription for ${this} ${sink$}`) + let runningAverage$ = new RunningAverage(sink$) + + super.setupSubscription( + sink$.pipe( + debug(SonificationLoggingLevel.DEBUG, 'runningAverageOutput val', true), + withLatestFrom(runningAverage$), + map((vals) => { + debugStatic(SonificationLoggingLevel.DEBUG, `vals ${vals} ${vals[1]}: vals`) + let datum = vals[0] + try { + datum.value = vals[1] + } catch (e: unknown) { + datum = vals[0] + } + return datum + }), + debug(SonificationLoggingLevel.DEBUG, 'runningAverageOutput val', true), + ), + ) + } + + /** + * @returns A string describing this class including its range. + */ + public toString(): string { + return `RunningAverageHandler` + } +} + +//////////// DEBUGGING ////////////////// +import { tag } from 'rxjs-spy/operators/tag' +import { Datum } from '../Datum' +import { RunningAverage } from '../stat/RunningAverage' +const debug = (level: number, message: string, watch: boolean) => (source: Observable) => { + if (watch) { + return source.pipe( + tap((val) => { + debugStatic(level, message + ': ' + val) + }), + tag(message), + ) + } else { + return source.pipe( + tap((val) => { + debugStatic(level, message + ': ' + val) + }), + ) + } +} + +const debugStatic = (level: number, message: string) => { + if (DEBUG) { + if (level >= getSonificationLoggingLevel()) { + console.log(message) + } else console.log('debug message dumped') + } +} diff --git a/src/sonification/handler/ScaleHandler.ts b/src/sonification/handler/ScaleHandler.ts index 41c2aa4..900ff28 100644 --- a/src/sonification/handler/ScaleHandler.ts +++ b/src/sonification/handler/ScaleHandler.ts @@ -12,11 +12,8 @@ import { import { RangeEndExpander } from '../stat/RangeEndExpander' import assert from 'assert' - const DEBUG = false - - /** * A DataHandler that scales the given value based on a specified min and max * @@ -118,7 +115,6 @@ export class ScaleHandler extends DataHandler { }), debug(SonificationLoggingLevel.DEBUG, 'scaled', DEBUG), - ), ) } @@ -148,11 +144,9 @@ const debug = (level: number, message: string, watch: boolean) => (source: Obser } const debugStatic = (level: number, message: string) => { - if (DEBUG) { if (level >= getSonificationLoggingLevel()) { console.log(message) } //else console.log('debug message dumped') } - } diff --git a/src/sonification/handler/SimpleDataHandler.ts b/src/sonification/handler/SimpleDataHandler.ts index 23fecc5..115b2c5 100644 --- a/src/sonification/handler/SimpleDataHandler.ts +++ b/src/sonification/handler/SimpleDataHandler.ts @@ -3,13 +3,18 @@ import { Datum } from '../Datum' import { DatumOutput } from '../output/DatumOutput' import { DataHandler } from './DataHandler' import { bufferCount, filter, map, Observable, tap } from 'rxjs' -import { getSonificationLoggingLevel, OutputStateChange, SonificationLevel, SonificationLoggingLevel } from '../OutputConstants' +import { + getSonificationLoggingLevel, + OutputStateChange, + SonificationLevel, + SonificationLoggingLevel, +} from '../OutputConstants' const DEBUG = true /** * A DataHandler that notifies if a set of point/s are seen - */ + */ export class SimpleDataHandler extends DataHandler { /** * a value denoting the number of points that the user should be notified after. defaults to 1 if not specified in the constructor. The user is then notified for every point. @@ -41,15 +46,14 @@ export class SimpleDataHandler extends DataHandler { * * @param sink The sink that is producing data for us */ - public setupSubscription(sink$: Observable) { + public setupSubscription(sink$: Observable) { super.setupSubscription( - sink$.pipe(bufferCount(this.threshold), -map((frames) => { - return frames[frames.length-1] -} - - ), - filter((val) => { + sink$.pipe( + bufferCount(this.threshold), + map((frames) => { + return frames[frames.length - 1] + }), + filter((val) => { return true }), ), @@ -90,4 +94,4 @@ const debugStatic = (level: number, message: string) => { console.log(message) } else console.log('debug message dumped') } -} \ No newline at end of file +} diff --git a/src/sonification/handler/SlopeHandler.ts b/src/sonification/handler/SlopeHandler.ts new file mode 100644 index 0000000..638adbb --- /dev/null +++ b/src/sonification/handler/SlopeHandler.ts @@ -0,0 +1,88 @@ +import { filter, map, Observable, tap, withLatestFrom } from 'rxjs' +import { DatumOutput } from '../output/DatumOutput' +import { getSonificationLoggingLevel, OutputStateChange, SonificationLoggingLevel } from '../OutputConstants' +import { DataHandler } from './DataHandler' +import { Slope } from '../stat/Slope' + +const DEBUG = true + +/** + * A DataHandler that tracks the slope of the data + * @todo change this to take a function that decides how to filter? + */ +export class SlopeHandler extends DataHandler { + /** + * Constructor + * + * @param sink. DataSink that is providing data to this Handler. + * @param output. Optional output for this data + * @param direction. -1 for decreasing, 1 for increasing. Defaults to 0 if not provided. + */ + constructor(output?: DatumOutput) { + super(output) + } + + /** + * Set up a subscription so we are notified about events + * Override this if the data needs to be modified in some way + * + * @param sink The sink that is producing data for us + */ + public setupSubscription(sink$: Observable) { + debugStatic(SonificationLoggingLevel.DEBUG, `setting up subscription for ${this} ${sink$}`) + let slope$ = new Slope(sink$) + + super.setupSubscription( + sink$.pipe( + debug(SonificationLoggingLevel.DEBUG, 'slopeOutput val', true), + withLatestFrom(slope$), + map((vals) => { + debugStatic(SonificationLoggingLevel.DEBUG, `vals ${vals} ${vals[1]}: vals`) + let datum = vals[0] + try { + datum.value = vals[1] + } catch (e: unknown) { + datum = vals[0] + } + return datum + }), + debug(SonificationLoggingLevel.DEBUG, 'slopeOutput val', true), + ), + ) + } + + /** + * @returns A string describing this class including its range. + */ + public toString(): string { + return `SlopeParityHandler` + } +} + +//////////// DEBUGGING ////////////////// +import { tag } from 'rxjs-spy/operators/tag' +import { Datum } from '../Datum' +const debug = (level: number, message: string, watch: boolean) => (source: Observable) => { + if (watch) { + return source.pipe( + tap((val) => { + debugStatic(level, message + ': ' + val) + }), + tag(message), + ) + } else { + return source.pipe( + tap((val) => { + debugStatic(level, message + ': ' + val) + }), + ) + } +} + +const debugStatic = (level: number, message: string) => { + if (DEBUG) { + if (level >= getSonificationLoggingLevel()) { + console.log(message) + } else console.log('debug message dumped') + } +} diff --git a/src/sonification/handler/SlopeParityHandler.ts b/src/sonification/handler/SlopeParityHandler.ts index 3b3e6a3..683a0b4 100644 --- a/src/sonification/handler/SlopeParityHandler.ts +++ b/src/sonification/handler/SlopeParityHandler.ts @@ -1,7 +1,8 @@ -import { filter, Observable, tap } from 'rxjs' +import { filter, map, Observable, tap, withLatestFrom } from 'rxjs' import { DatumOutput } from '../output/DatumOutput' import { getSonificationLoggingLevel, OutputStateChange, SonificationLoggingLevel } from '../OutputConstants' import { DataHandler } from './DataHandler' +import { SlopeChange } from '../stat/SlopeChange' const DEBUG = true @@ -26,7 +27,7 @@ export class SlopeParityHandler extends DataHandler { */ private _prevPoint: number public get prevPoint(): number { - return this._prevPoint; + return this._prevPoint } public set prevPoint(value: number) { this._prevPoint = value @@ -67,16 +68,18 @@ export class SlopeParityHandler extends DataHandler { * * @param sink The sink that is producing data for us */ - public setupSubscription(sink$: Observable) { - debugStatic (SonificationLoggingLevel.DEBUG, `setting up subscription for ${this} ${sink$}`) + public setupSubscription(sink$: Observable) { + debugStatic(SonificationLoggingLevel.DEBUG, `setting up subscription for ${this} ${sink$}`) + let slope$ = new SlopeChange(sink$) + super.setupSubscription( sink$.pipe( filter((val) => { - if (val instanceof Datum){ + if (val instanceof Datum) { let slope = val.value - this.prevPoint this.prevPoint = val.value // no matter what, we'll need the prev point to calculate the slope if (this.direction == 0) { - console.log("direction 0") + console.log('direction 0') if (Math.sign(slope) != Math.sign(this.prevSlope)) { if (DEBUG) console.log('direction of slope changed') this.prevSlope = slope @@ -88,7 +91,8 @@ export class SlopeParityHandler extends DataHandler { if (Math.sign(slope) != Math.sign(this.prevSlope)) { this.prevSlope = slope return true - } else { // slope did not change direction + } else { + // slope did not change direction this.prevSlope = slope return false } @@ -96,8 +100,7 @@ export class SlopeParityHandler extends DataHandler { this.prevSlope = slope return false } - } - else return true + } else return true }), ), ) @@ -114,6 +117,7 @@ export class SlopeParityHandler extends DataHandler { //////////// DEBUGGING ////////////////// import { tag } from 'rxjs-spy/operators/tag' import { Datum } from '../Datum' + const debug = (level: number, message: string, watch: boolean) => (source: Observable) => { if (watch) { return source.pipe( diff --git a/src/sonification/output/NoteSonify.ts b/src/sonification/output/NoteSonify.ts index b9a296d..ecdbfcf 100644 --- a/src/sonification/output/NoteSonify.ts +++ b/src/sonification/output/NoteSonify.ts @@ -50,10 +50,12 @@ export class NoteSonify extends Sonify { protected output(datum: Datum) { debugStatic(SonificationLoggingLevel.DEBUG, `outputing ${datum.value} to oscillator`) let oscillator = this.outputNode as OscillatorNode - + if (!this.isAudioPlaying) { + oscillator.start() + this.isAudioPlaying = true + } oscillator.frequency.value = datum.value } - /** * Generates a new note sonifier */ @@ -101,7 +103,7 @@ const debug = (level: number, message: string, watch: boolean) => (source: Obser const debugStatic = (level: number, message: string) => { if (DEBUG) { if (level >= getSonificationLoggingLevel()) { - console.log(message) + console.log('slope' + message) } //else console.log('debug message dumped') } } diff --git a/src/sonification/output/Speech.ts b/src/sonification/output/Speech.ts index 34f8e5a..047974f 100644 --- a/src/sonification/output/Speech.ts +++ b/src/sonification/output/Speech.ts @@ -1,20 +1,20 @@ import { Datum } from '../Datum' -import { getSonificationLoggingLevel, OutputStateChange, SonificationLoggingLevel } from '../OutputConstants'; +import { getSonificationLoggingLevel, OutputStateChange, SonificationLoggingLevel } from '../OutputConstants' import { DatumOutput } from './DatumOutput' -const DEBUG = true; +const DEBUG = true /** * Class for sonifying data point as speech. */ export class Speech extends DatumOutput { - private _speechSynthesis : SpeechSynthesis - private _utterance : SpeechSynthesisUtterance - private _volume: number; - private playing: boolean; - - private _polite: boolean; - public get polite(): boolean { + private _speechSynthesis: SpeechSynthesis + private _utterance: SpeechSynthesisUtterance + private _volume: number + private playing: boolean + + private _polite: boolean + public get polite(): boolean { return this._polite } @@ -23,85 +23,93 @@ export class Speech extends DatumOutput { } // construct the utterance and set its properties - public constructor(lang?: string, volume?: number, rate?: number, voice?: SpeechSynthesisVoice, polite: boolean = false) { + public constructor( + lang?: string, + volume?: number, + rate?: number, + voice?: SpeechSynthesisVoice, + polite: boolean = false, + ) { super() - this._speechSynthesis = window.speechSynthesis; + this._speechSynthesis = window.speechSynthesis this._utterance = new SpeechSynthesisUtterance() - this._utterance.rate = rate? rate : 10 // rate is 0.1-10 - this._volume = volume? volume : 1 // volume is 0-1 + this._utterance.rate = rate ? rate : 10 // rate is 0.1-10 + this._volume = volume ? volume : 1 // volume is 0-1 if (lang) this._utterance.lang = lang if (voice) this._utterance.voice = voice else { - this._utterance.voice = this._speechSynthesis.getVoices()[0]; + this._utterance.voice = this._speechSynthesis.getVoices()[0] } - this._polite = polite + this._polite = polite - this.playing = false; - debugStatic (SonificationLoggingLevel.DEBUG, "initialized") + this.playing = false + debugStatic(SonificationLoggingLevel.DEBUG, 'initialized') } /** * Output the datum as speech */ protected output(datum: Datum) { - console.log("enter speech output") + console.log('enter speech output') if (!this.playing) return super.output(datum) this._utterance.text = datum.value.toString() - if((this._speechSynthesis.pending || this._speechSynthesis.speaking) && !this.polite) { + if ((this._speechSynthesis.pending || this._speechSynthesis.speaking) && !this.polite) { this._speechSynthesis.cancel() - console.log("bout to interrupt") + console.log('bout to interrupt') } this._speechSynthesis.speak(this._utterance) - console.log("spoken!") + console.log('spoken!') } // Start speaking public start(): void { - this._utterance.volume = this._volume; - this._speechSynthesis.resume(); // always resume before speaking. There is a bug on Web Speech that if you pause for more than 15 seconds, speech fails quietly. + this._utterance.volume = this._volume + this._speechSynthesis.resume() // always resume before speaking. There is a bug on Web Speech that if you pause for more than 15 seconds, speech fails quietly. // stop any future utterances if (this._speechSynthesis.pending) { - this._speechSynthesis.cancel(); + this._speechSynthesis.cancel() } // start current utterance this.playing = true - this._utterance.onend = () => { this._utterance.text = ""} // natural end - super.start(); + this._utterance.onend = () => { + this._utterance.text = '' + } // natural end + super.start() } // Pause speech if playing. public pause(): void { - this._utterance.volume = 0; - this._speechSynthesis.cancel(); + this._utterance.volume = 0 + this._speechSynthesis.cancel() this.playing = false - super.pause(); + super.pause() } // Resume speech if paused. public resume(): void { this.playing = true - this._utterance.volume = this._volume; - this._speechSynthesis.resume(); + this._utterance.volume = this._volume + this._speechSynthesis.resume() } // Stop speech. public stop(): void { if (this._speechSynthesis.pending) { - this._speechSynthesis.cancel(); + this._speechSynthesis.cancel() } this.playing = false } - public toString() : string { + public toString(): string { return `Speech` } } //////////// DEBUGGING ////////////////// import { tag } from 'rxjs-spy/operators/tag' -import { Observable, tap } from 'rxjs'; +import { Observable, tap } from 'rxjs' const debug = (level: number, message: string, watch: boolean) => (source: Observable) => { if (watch) { @@ -126,4 +134,4 @@ const debugStatic = (level: number, message: string) => { console.log(message) } else console.log('debug message dumped') } -} \ No newline at end of file +} diff --git a/src/sonification/stat/RangeEndExpander.ts b/src/sonification/stat/RangeEndExpander.ts index 4ff64c2..323972e 100644 --- a/src/sonification/stat/RangeEndExpander.ts +++ b/src/sonification/stat/RangeEndExpander.ts @@ -29,7 +29,7 @@ export class RangeEndExpander extends Statistic { * @param stream$ the stream of data * @returns a subscription */ - public setupSubscription(stream$: Observable): Subscription { + protected setupSubscription(stream$: Observable): Subscription { return super.setupSubscription( stream$.pipe( reduce((acc, curr) => { diff --git a/src/sonification/stat/RunningAverage.ts b/src/sonification/stat/RunningAverage.ts index 37270bb..c17d6f5 100644 --- a/src/sonification/stat/RunningAverage.ts +++ b/src/sonification/stat/RunningAverage.ts @@ -1,39 +1,71 @@ import { bufferCount, map, Observable, Subscription } from 'rxjs' import { Datum } from '../Datum' -import { OutputStateChange } from '../OutputConstants' +import { getSonificationLoggingLevel, OutputStateChange, SonificationLoggingLevel } from '../OutputConstants' import { Statistic } from './Statistic' - +const DEBUG:boolean = true /** * Calculates a running average based on the last n values seen. */ -class RunningAverage extends Statistic { +export class RunningAverage extends Statistic { /** * What buffer should the average be calculated over? */ - buffer = 3 + buffer:number = 3 /** * * @param stream$ The stream of data over which to calculate the statistic * @param len The number of data points to calculate the running average over */ - constructor(len: number, stream$: Observable) { + constructor(stream$: Observable,len?: number) { super(0, stream$) this.buffer = len ? len : this.buffer + debugStatic(SonificationLoggingLevel.DEBUG, `initializing RunningAverage with ${this.buffer}` ) } - public setupSubscription(stream$: Observable): Subscription { + protected setupSubscription(stream$: Observable): Subscription { return super.setupSubscription( stream$.pipe( - bufferCount(this.buffer), - map((frames) => { - const total = frames.reduce((acc, curr) => { + bufferCount(this.buffer,1), + map((nums) => { + const total = nums.reduce((acc, curr) => { acc += curr return acc - }, 0) - return 1 / (total / frames.length) + },0 ) + debugStatic(SonificationLoggingLevel.DEBUG,`returning ${1 / (total / nums.length)}`) + return 1 / (total / nums.length) }), ), ) } } + + +//////////// DEBUGGING ////////////////// +import { tag } from 'rxjs-spy/operators/tag' +import {tap } from 'rxjs' + +const debug = (level: number, message: string, watch: boolean) => (source: Observable) => { + if (watch) { + return source.pipe( + tap((val) => { + debugStatic(level, message + ': ' + val) + }), + tag(message), + ) + } else { + return source.pipe( + tap((val) => { + debugStatic(level, message + ': ' + val) + }), + ) + } +} + +const debugStatic = (level: number, message: string) => { + if (DEBUG) { + if (level >= getSonificationLoggingLevel()) { + console.log(message) + } else console.log('debug message dumped') + } +} \ No newline at end of file diff --git a/src/sonification/stat/Slope.ts b/src/sonification/stat/Slope.ts new file mode 100644 index 0000000..fa35e68 --- /dev/null +++ b/src/sonification/stat/Slope.ts @@ -0,0 +1,73 @@ +import { bufferCount, map, Subscription } from 'rxjs' +import { Datum } from '../Datum' +import { getSonificationLoggingLevel, OutputStateChange, SonificationLoggingLevel } from '../OutputConstants' +import { Statistic } from './Statistic' + +const DEBUG = false // true + +/** + * Returns 0 if the slopes have the same parity + * -1 if the slopes switched from positive to negative + * 1 if the slopes switched from negative to positive + */ +export class Slope extends Statistic { + /** + * What buffer should the slope average be calculated over? + */ + private slopeWindow = 3 + + /** + * + * @param stream$ The stream of data over which to calculate the statistic + * @param len The number of data points to calculate the running average over + */ + constructor(stream$: Observable) { + super(0, stream$) + } + + protected setupSubscription(stream$: Observable): Subscription { + if (!this.slopeWindow) this.slopeWindow = 3 + // TODO: figure out why typescript thinks slopeWindow is undefined + // TODO: and consider how to make window length possible to change without editing the cod + + return super.setupSubscription( + stream$.pipe( + bufferCount(this.slopeWindow, 1), + map((nums) => { + const max = Math.max(...nums) + const min = Math.min(...nums) + return max - min + }), + ), + ) + } +} + +//////////// DEBUGGING ////////////////// +import { tag } from 'rxjs-spy/operators/tag' +import { Observable, tap } from 'rxjs' + +const debug = (level: number, message: string, watch: boolean) => (source: Observable) => { + if (watch) { + return source.pipe( + tap((val) => { + debugStatic(level, message + ': ' + val) + }), + tag(message), + ) + } else { + return source.pipe( + tap((val) => { + debugStatic(level, message + ': ' + val) + }), + ) + } +} + +const debugStatic = (level: number, message: string) => { + if (DEBUG) { + if (level >= getSonificationLoggingLevel()) { + console.log(message) + } else console.log('debug message dumped') + } +} diff --git a/src/sonification/stat/SlopeChange.ts b/src/sonification/stat/SlopeChange.ts new file mode 100644 index 0000000..a7f003c --- /dev/null +++ b/src/sonification/stat/SlopeChange.ts @@ -0,0 +1,82 @@ +import { buffer, bufferCount, map, Subscription } from 'rxjs' +import { Datum } from '../Datum' +import { getSonificationLoggingLevel, OutputStateChange, SonificationLoggingLevel } from '../OutputConstants' +import { Statistic } from './Statistic' + +const DEBUG = false // true + +/** + * Returns 0 if the slopes have the same parity + * -1 if the slopes switched from positive to negative + * 1 if the slopes switched from negative to positive + */ +export class SlopeChange extends Statistic { + /** + * What buffer should the slope average be calculated over? + */ + private slopeWindow = 3 + + /** + * + * @param stream$ The stream of data over which to calculate the statistic + * @param len The number of data points to calculate the running average over + */ + constructor(stream$: Observable, slopeWindow:number = 3) { + super(0, stream$) + this.slopeWindow = slopeWindow; + } + + protected setupSubscription(stream$: Observable): Subscription { + // if (!this.slopeWindow) this.slopeWindow = 3 + // TODO: figure out why typescript thinks slopeWindow is undefined + // TODO: and consider how to make window length possible to change without editing the code + return super.setupSubscription( + stream$.pipe( + bufferCount(this.slopeWindow, 1), + map((nums) => { + const max = Math.max(...nums) + const min = Math.min(...nums) + return max - min + }), + bufferCount(2, 0), + map((slopes) => { + slopes[0] = slopes[0] >= 0 ? 1 : -1 + slopes[1] = slopes[1] >= 0 ? 1 : -1 + if (slopes[0] == slopes[1]) return 0 + if (slopes[0] > slopes[1]) return -1 + return 1 + }), + debug(SonificationLoggingLevel.DEBUG, 'result', DEBUG), + ), + ) + } +} + +//////////// DEBUGGING ////////////////// +import { tag } from 'rxjs-spy/operators/tag' +import { Observable, tap } from 'rxjs' + +const debug = (level: number, message: string, watch: boolean) => (source: Observable) => { + if (watch) { + return source.pipe( + tap((val) => { + debugStatic(level, message + ': ' + val) + }), + tag(message), + ) + } else { + return source.pipe( + tap((val) => { + debugStatic(level, message + ': ' + val) + }), + ) + } +} + +const debugStatic = (level: number, message: string) => { + if (DEBUG) { + if (level >= getSonificationLoggingLevel()) { + console.log(message) + } else console.log('debug message dumped') + } +} diff --git a/src/sonification/stat/Statistic.ts b/src/sonification/stat/Statistic.ts index c0715b9..035cb2e 100644 --- a/src/sonification/stat/Statistic.ts +++ b/src/sonification/stat/Statistic.ts @@ -37,7 +37,7 @@ export class Statistic extends BehaviorSubject { * * @param stream$ The data being streamed */ - public setupSubscription(stream$: Observable): Subscription { + protected setupSubscription(stream$: Observable): Subscription { this.subscription?.unsubscribe() this.subscription = stream$.subscribe(this) return this.subscription diff --git a/src/styles/keyboard.css b/src/styles/keyboard.css new file mode 100644 index 0000000..83ac869 --- /dev/null +++ b/src/styles/keyboard.css @@ -0,0 +1,38 @@ +/*#piano { + height: 200px; + width: 500px; + border: 2px +} +*/ + +:focus{ + outline: none; +} + +.black{ + background-color: rgb(0, 0, 0); + width: 40px; + height: 200px; + z-index: 10; +} + +.black.selected{ + background-color: rgb(49, 49, 49); +} + +.white{ + background-color: #f5f3ed; + box-sizing: border-box; + border: 2px solid black; + height: 300px; + width: 70px; + display: inline-block; +} + +.white.selected{ + background-color: #cfcfcf; +} + +.white, .black { + position: absolute; +} \ No newline at end of file diff --git a/src/views/dashboard/DataHandlerItem.tsx b/src/views/dashboard/DataHandlerItem.tsx index 0ebbb7d..5fb980c 100644 --- a/src/views/dashboard/DataHandlerItem.tsx +++ b/src/views/dashboard/DataHandlerItem.tsx @@ -19,7 +19,9 @@ import { import { ArrowDropDown } from '@mui/icons-material' import { grey } from '@mui/material/colors' -import { DataHandlerWrapper, DataOutputWrapper, JDServiceWrapper, ParameterWrapper } from '../../pages/Dashboard' +import { DataHandlerWrapper, DataOutputWrapper } from '../../pages/templates/DataHandlerInterfaces' +import { JDServiceWrapper } from '../../pages/templates/JDInterfaces' +import { ParameterWrapper } from '../../pages/templates/ParameterInterface' import { DataHandler } from '../../sonification/handler/DataHandler' import DataOutputItem from './DataOutputItem' @@ -148,13 +150,21 @@ export default function DataHandlerItem(props: React.Attributes & DataHandlerIte {props.description}
{props.parameters?.map((parameter) => { - return + return ( + + ) })}
{dataOutputs?.map((output) => { return output.parameters?.map((parameter) => { - return + return ( + + ) }) })}
diff --git a/src/views/dashboard/DataOutputItem.tsx b/src/views/dashboard/DataOutputItem.tsx index 2bf036f..818b85a 100644 --- a/src/views/dashboard/DataOutputItem.tsx +++ b/src/views/dashboard/DataOutputItem.tsx @@ -1,6 +1,5 @@ import { FormControlLabel, Switch } from '@mui/material' -import { DataOutputWrapper } from '../../pages/Dashboard' import { DatumOutput } from '../../sonification/output/DatumOutput' export interface DataOutputProps { diff --git a/src/views/dashboard/JDServiceItem.tsx b/src/views/dashboard/JDServiceItem.tsx index bde0dc3..bb50f3b 100644 --- a/src/views/dashboard/JDServiceItem.tsx +++ b/src/views/dashboard/JDServiceItem.tsx @@ -1,7 +1,8 @@ import { Card, CardContent, CardHeader, Grid, Typography } from '@mui/material' import { blueGrey } from '@mui/material/colors' -import { DataHandlerWrapper, JDValueWrapper } from '../../pages/Dashboard' +import { DataHandlerWrapper } from '../../pages/templates/DataHandlerInterfaces' +import { JDValueWrapper } from '../../pages/templates/JDInterfaces' import JDValueItem from './JDValueItem' export interface JDServiceItemProps { diff --git a/src/views/dashboard/JDValueItem.tsx b/src/views/dashboard/JDValueItem.tsx index 5bb605c..db56edb 100644 --- a/src/views/dashboard/JDValueItem.tsx +++ b/src/views/dashboard/JDValueItem.tsx @@ -2,7 +2,8 @@ import { useState, useEffect } from 'react' import { Box, Button, Card, CardContent, CardHeader, Grid, Menu, MenuItem, Typography } from '@mui/material' import { ArrowDropDown } from '@mui/icons-material' -import { AVAILABLE_DATA_OUTPUT_TEMPLATES, DataHandlerWrapper, DataOutputWrapper } from '../../pages/Dashboard' + +import { DataHandlerWrapper } from '../../pages/templates/DataHandlerInterfaces' import DataHandlerItem from './DataHandlerItem' diff --git a/src/views/demos/DemoFileOutput.tsx b/src/views/demos/DemoFileOutput.tsx index 41c0ed0..6d8f552 100644 --- a/src/views/demos/DemoFileOutput.tsx +++ b/src/views/demos/DemoFileOutput.tsx @@ -12,16 +12,13 @@ import { Box, Button, Input } from '@mui/material' const DEBUG = true export interface DemoFileOutputState extends DemoSimpleState { - targetValues : number[] + targetValues: number[] } export interface DemoFileOutputProps extends DemoSimpleProps { dataSummary: any } -export class DemoFileOutput - extends DemoSimple - implements IDemoView -{ +export class DemoFileOutput extends DemoSimple implements IDemoView { notifier: NotificationHandler | undefined private _inputFile: React.RefObject private _buffer: ArrayBuffer | undefined @@ -30,7 +27,7 @@ export class DemoFileOutput super(props) this.state = { // currently just chooses max as the default - targetValues: [this.props.dataSummary.max] + targetValues: [this.props.dataSummary.max], } this._inputFile = React.createRef() } @@ -83,37 +80,39 @@ export class DemoFileOutput private _handleValueChange = (value: string) => { let values = value.split(',') - let targets : number[] = [] + let targets: number[] = [] for (let val of values) { let numb = parseFloat(val) if (!isNaN(numb)) { targets.push(numb) } } - this.setState({ targetValues: targets}) + this.setState({ targetValues: targets }) } private _handleFileChange = (event: React.FormEvent) => { - if (DEBUG) console.log("file changed!") - let target: any = event.target - if (target && target.files && target.files.length === 1) { - console.log(event) - let file: File = target.files[0] - // process file - file.arrayBuffer().then((buffer) => { - // if (DEBUG) console.log(buffer.byteLength) - // byte length is not 0 from console.log statements - this._buffer = buffer - if (DEBUG) console.log("buffer updated!") - }).catch(console.error) - } + if (DEBUG) console.log('file changed!') + let target: any = event.target + if (target && target.files && target.files.length === 1) { + console.log(event) + let file: File = target.files[0] + // process file + file.arrayBuffer() + .then((buffer) => { + // if (DEBUG) console.log(buffer.byteLength) + // byte length is not 0 from console.log statements + this._buffer = buffer + if (DEBUG) console.log('buffer updated!') + }) + .catch(console.error) + } } ////////// HELPER METHODS /////////////// public initializeSink() { this.sink = OutputEngine.getInstance().addSink('FileOutputDemo') this.notifier = new NotificationHandler(new FileOutput(this._buffer), this.state.targetValues) - if (DEBUG) console.log("sink initialized") + if (DEBUG) console.log('sink initialized') //this.sink.addDataHandler(new NoteHandler()) this.sink.addDataHandler(this.notifier) return this.sink diff --git a/src/views/demos/DemoHighlightRegion.tsx b/src/views/demos/DemoHighlightRegion.tsx index a64e98c..fd292cb 100644 --- a/src/views/demos/DemoHighlightRegion.tsx +++ b/src/views/demos/DemoHighlightRegion.tsx @@ -98,12 +98,14 @@ export class DemoHighlightRegion /** * @todo vpotluri to understand: where is the update datum method for this being called? */ - this.filter = new FilterRangeHandler([this.state.minValue, this.state.maxValue], new NoiseSonify(undefined,undefined,-1)) - - + this.filter = new FilterRangeHandler( + [this.state.minValue, this.state.maxValue], + new NoiseSonify(undefined, undefined, -1), + ) + this.sink.addDataHandler(this.filter) this.sink.addDataHandler(new NoteHandler(undefined, new NoteSonify())) - + return this.sink } } diff --git a/src/views/demos/DemoRunningExtrema.tsx b/src/views/demos/DemoRunningExtrema.tsx index 32b23b7..630135b 100644 --- a/src/views/demos/DemoRunningExtrema.tsx +++ b/src/views/demos/DemoRunningExtrema.tsx @@ -16,10 +16,7 @@ export interface DemoRunningExtremaProps extends DemoSimpleProps { dataSummary: any } -export class DemoRunningExtrema - extends DemoSimple - implements IDemoView -{ +export class DemoRunningExtrema extends DemoSimple implements IDemoView { minimumTracker: RunningExtremaHandler | undefined maximumTracker: RunningExtremaHandler | undefined @@ -32,7 +29,7 @@ export class DemoRunningExtrema this.sink = OutputEngine.getInstance().addSink('DemoSlopeParity') this.maximumTracker = new RunningExtremaHandler(1, new Speech()) this.minimumTracker = new RunningExtremaHandler(-1, new Speech()) - if (DEBUG) console.log("sink initialized") + if (DEBUG) console.log('sink initialized') this.sink.addDataHandler(this.maximumTracker) this.sink.addDataHandler(this.minimumTracker) this.sink.addDataHandler(new NoteHandler(undefined, new NoteSonify())) diff --git a/src/views/demos/DemoSimple.tsx b/src/views/demos/DemoSimple.tsx index 2e31538..b88a592 100644 --- a/src/views/demos/DemoSimple.tsx +++ b/src/views/demos/DemoSimple.tsx @@ -95,11 +95,11 @@ export class DemoSimple if (this.sink == undefined) this.sink = this.initializeSink() - //if(this.delaySink == undefined) this.delaySink = this.initializeDelaySink() + if (this.delaySink == undefined) this.delaySink = this.initializeDelaySink() let id = this.sink ? this.sink.id : 0 - //let delayID = this.delaySink ? this.delaySink.id : 1 + let delayID = this.delaySink ? this.delaySink.id : 1 let dataCopy = Object.assign([], data) let data$ = of(...data) //.slice(0, 8)) @@ -136,22 +136,17 @@ export class DemoSimple /*let delayTimer$ = timer(0, 250).pipe(debug(SonificationLoggingLevel.DEBUG, 'point number')) - let delaySource$ = zip(delayData$, delayTimer$, (num, time) => new Datum(delayID, num)).pipe(delay(1000)).pipe( - debug(SonificationLoggingLevel.DEBUG, 'delayPoint'), - ) - OutputEngine.getInstance().setStream(delayID, delaySource$) - - */ + /// Make sure to delete the sink when the source is - /*delaySource$.subscribe({ + // delaySource$.subscribe({ /// Make sure to delete the sink when the source is - complete: () => { - this.delaySink = undefined + // complete: () => { + // this.delaySink = undefined //Demo.setState({ playbackLabel: "Play" }) - }, - }) + // }, + // }) debugStatic(SonificationLoggingLevel.DEBUG, `adding Handler to ${this.delaySink}`) diff --git a/src/views/demos/DemoSlope.tsx b/src/views/demos/DemoSlope.tsx new file mode 100644 index 0000000..7d65fb2 --- /dev/null +++ b/src/views/demos/DemoSlope.tsx @@ -0,0 +1,90 @@ +import React from 'react' + +import { fabClasses, TextField } from '@mui/material' +import { IDemoView } from './IDemoView' +import { SlopeHandler } from '../../sonification/handler/SlopeHandler' +import { FileOutput } from '../../sonification/output/FileOutput' +import { DemoSimple, DemoSimpleProps, DemoSimpleState } from './DemoSimple' +import { NoteHandler } from '../../sonification/handler/NoteHandler' +import { OutputEngine } from '../../sonification/OutputEngine' +import { Box, Button, Input } from '@mui/material' +import { NoteSonify } from '../../sonification/output/NoteSonify' +import { Speech } from '../../sonification/output/Speech' +import { RunningAverageHandler } from '../../sonification/handler/RunningAverageHandler' + +const DEBUG = true + +export interface DemoSlopeState extends DemoSimpleState { + targetValues: number[] +} +export interface DemoSlopeProps extends DemoSimpleProps { + dataSummary: any +} + +export class DemoSlope extends DemoSimple implements IDemoView { + filter: SlopeHandler | undefined + private _inputFile: React.RefObject + private _buffer: ArrayBuffer | undefined + + constructor(props: DemoSlopeProps) { + super(props) + this.state = { + // currently just chooses max as the default + targetValues: [this.props.dataSummary.max], + } + this._inputFile = React.createRef() + } + + public render() { + const { targetValues } = this.state + + return ( +
+ +
+ ) + } + + private _handleFileChange = (event: React.FormEvent) => { + if (DEBUG) console.log('file changed!') + let target: any = event.target + if (target && target.files && target.files.length === 1) { + console.log(event) + let file: File = target.files[0] + // process file + file.arrayBuffer() + .then((buffer) => { + // if (DEBUG) console.log(buffer.byteLength) + // byte length is not 0 from console.log statements + this._buffer = buffer + if (DEBUG) console.log('buffer updated!') + }) + .catch(console.error) + } + } + + ////////// HELPER METHODS /////////////// + public initializeSink() { + console.log('setting up note sonify for slope') + this.sink = OutputEngine.getInstance().addSink('DemoSlope') + // this.sink.addDataHandler(new SlopeHandler(new NoteSonify(-1)), false) + this.sink.addDataHandler(new RunningAverageHandler(new Speech())) + if (DEBUG) console.log('sink initialized') + this.sink.addDataHandler(new NoteHandler(undefined)) + return this.sink + } +} diff --git a/src/views/demos/DemoSpeakRange.tsx b/src/views/demos/DemoSpeakRange.tsx index 1067933..a5ca024 100644 --- a/src/views/demos/DemoSpeakRange.tsx +++ b/src/views/demos/DemoSpeakRange.tsx @@ -85,7 +85,10 @@ export class DemoSpeakRange extends DemoSimple