+ )
+ }
+}
\ 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}