Skip to content

Commit a88553c

Browse files
Merge pull request #92 from make4all/vpotluri/issue87
feat(updated architecture): updated sonification architecture and partial bug fix for noise related demos
2 parents 6997b4c + c729022 commit a88553c

15 files changed

+468
-183
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ Runs the mocha test suite
2626

2727
Start a compile watch of the library build and tests for iterative development.
2828

29-
## yarn pretify
29+
## yarn prettify
3030

31-
Run Prietter and format source files.
31+
Run Prettier and format source files.
3232

3333
## bundling
3434

src/.vscode/launch.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"type": "pwa-chrome",
9+
"request": "launch",
10+
"name": "Launch Chrome against localhost",
11+
"url": "http://localhost:8080",
12+
"webRoot": "${workspaceFolder}"
13+
}
14+
]
15+
}

src/pages/Demo.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import { IDemoView } from '../views/demos/IDemoView'
1414
import { DemoSimple } from '../views/demos/DemoSimple'
1515
import { DemoHighlightRegion } from '../views/demos/DemoHighlightRegion'
1616
import { op } from 'arquero'
17+
import { ExperimentalDemoHighlightRegion } from '../views/demos/ExperimentalDemoHighlightRegion'
1718

1819
const DEMO_VIEW_MAP = {
1920
simple: { value: 'simple', label: 'Simple sonification', component: DemoSimple },
2021
highlightRegion: { value: 'highlightRegion', label: 'Highlight points for region', component: DemoHighlightRegion },
22+
experimentalHighlightRegion: { value: 'experimentalHighlightRegion', label: 'experimental implementation of highlight points for region', component: ExperimentalDemoHighlightRegion },
2123
}
2224

2325
let demoViewRef: React.RefObject<DemoSimple<DemoProps, DemoState> | DemoHighlightRegion> = React.createRef()

src/sonification/Sonifier.ts

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ export class Sonifier {
2626
/**
2727
* Every sonifier has an audio context used to play sounds
2828
*/
29-
private _audioCtx!: AudioContext
30-
public get audioCtx(): AudioContext {
31-
return this._audioCtx
29+
private static _audioCtx = new AudioContext();
30+
public static get audioCtx(): AudioContext {
31+
return Sonifier._audioCtx
3232
}
33-
private gainNode: GainNode
33+
public static gainNode: GainNode
3434

3535
/**
3636
* Whether or not audio is currently playing
@@ -140,14 +140,13 @@ export class Sonifier {
140140
*/
141141
private constructor() {
142142
// super()
143-
this._audioCtx = new AudioContext() // works without needing additional libraries. need to check this when we move away from react as it is currently pulling from react-dom.d.ts.
144-
this._audioCtx.resume()
143+
Sonifier._audioCtx.resume()
145144
//this.startTime = this.audioCtx.currentTime
146145
// Always begin in a "stopped" state since there is no data to play yet at construction time
147146
this._playbackState = PlaybackState.Stopped
148147
this.sources = new Map()
149148
this._displays = new Map()
150-
this.gainNode = this._audioCtx.createGain()
149+
Sonifier.gainNode = Sonifier._audioCtx.createGain()
151150
}
152151

153152
/**
@@ -168,7 +167,7 @@ export class Sonifier {
168167
// The answer is yes if we ever want to handl control to a new/different audio context
169168
// maybe have an option for "halt" instead that ends everything?
170169

171-
this.audioCtx.suspend()
170+
Sonifier.audioCtx.suspend()
172171
if (DEBUG) console.log('stopping. playback state is paused')
173172
this._playbackState = PlaybackState.Paused
174173
// this.audioCtx.close() -- gives everything up, should only be done at the very very end.
@@ -178,43 +177,41 @@ export class Sonifier {
178177
public onPlay() {
179178
// @todo do I need to do anything differently if was stopped instead of paused?
180179
// The answer is yes if we ever want to handl control to a new/different audio context
181-
if (this.playbackState == PlaybackState.Playing && this.audioCtx.state == 'running') {
180+
if (this.playbackState == PlaybackState.Playing && Sonifier.audioCtx.state == 'running') {
182181
if (DEBUG) console.log('playing')
183182
} else {
184183
if (DEBUG) console.log('setting up for playing')
185-
this.audioCtx.resume()
186-
this.gainNode.connect(this._audioCtx.destination)
187-
this.startSources()
184+
Sonifier.audioCtx.resume()
185+
Sonifier.gainNode.connect(Sonifier.audioCtx.destination)
186+
// this.startSources()
188187
this._playbackState = PlaybackState.Playing
189188
}
190189
}
191190

192-
/**
193-
* Triggers all existing audio nodes to play.
194-
*
195-
* @todo if a new source is added after onPlay it won't get connected
196-
* @todo what about visually displaying things
197-
*/
198-
public startSources() {
199-
if (DEBUG) console.log(`starting sources ${this.sources.size}`)
200-
this.sources.forEach((source: DataSource, key: number) => {
201-
source.displays().map((display) => {
202-
if (DEBUG) console.log(`Source: ${source} Display: ${display.toString()}`)
203-
let sonify = display as Sonify
204-
let audioNode = sonify.getAudioNode(this)
205-
if (audioNode != undefined) {
206-
audioNode.connect(this.gainNode)
207-
}
208-
})
209-
})
210-
this.gainNode.connect(this._audioCtx.destination)
211-
}
191+
192+
// /**
193+
// * Triggers all existing audio nodes to play.
194+
// *
195+
// * @todo if a new source is added after onPlay it won't get connected
196+
// * @todo what about visually displaying things
197+
// */
198+
// public startSources() {
199+
// if (DEBUG) console.log(`starting sources ${this.sources.size}`)
200+
// this.sources.forEach((source: DataSource, key: number) => {
201+
// source.displays().map((display) => {
202+
// if (DEBUG) console.log(`Source: ${source} Display: ${display.toString()}`)
203+
// display.show();
204+
// })
205+
// })
206+
// Sonifier.gainNode.connect(Sonifier._audioCtx.destination)
207+
// }
208+
212209

213210
//needs extensive testing.
214211
public onPause() {
215212
if (DEBUG) console.log('Pausing. Playback state is paused')
216-
this.audioCtx.suspend()
217-
this.gainNode.disconnect()
213+
Sonifier.audioCtx.suspend()
214+
Sonifier.gainNode.disconnect()
218215
this._playbackState = PlaybackState.Paused
219216
}
220217

@@ -262,7 +259,7 @@ export class Sonifier {
262259
else {
263260
setTimeout(() => {
264261
this.pushPoint(point, sourceId)
265-
}, time - d3.now())
262+
}, time - Sonifier.audioCtx.currentTime*1000);
266263
}
267264
}
268265
}

src/sonification/displays/DatumDisplay.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export class DatumDisplay {
2222
*/
2323
update(datum: Datum) {
2424
this.datum = datum
25-
console.log(datum)
2625
}
2726

2827
/**

src/sonification/displays/NoiseSonify.ts

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,63 @@
11
import { Datum } from '../Datum'
22
import { Sonifier } from '../Sonifier'
3-
import { Sonify } from './Sonify'
3+
import { SonifyFixedDuration } from './SonifyFixedDuration'
4+
5+
const DEBUG = false;
6+
47

58
/**
69
* Class for sonifying a data point as a pitch.
710
* @extends Sonify
811
* @todo only plays noise once. investigate further. probably have to create new noise nodes for each point.
912
*
1013
*/
11-
export class NoiseSonify extends Sonify {
14+
export class NoiseSonify extends SonifyFixedDuration {
15+
1216
/**
13-
* @todo what do we do when this is live data? unlike audio nodes, noise nodes need a duration to calculate the buffer length.
17+
* This is the node that knows how to play noise. It is connected to the this.audioNode
1418
*/
19+
private filter: BiquadFilterNode | undefined;
1520

16-
private noiseBufferSize = 20
17-
private duration = 0.1
18-
19-
public getAudioNode(sonifier?: Sonifier) {
20-
console.log('NoiseSonify:getAudioNode')
21-
if (super.getAudioNode()) return super.getAudioNode()
22-
if (sonifier) {
23-
console.log('executing code in getAudioNode')
24-
let sampleRate = sonifier.audioCtx.sampleRate
25-
this.noiseBufferSize = sampleRate * this.duration
26-
let buffer = sonifier.audioCtx.createBuffer(1, this.noiseBufferSize, sonifier.audioCtx.sampleRate)
27-
let noiseNode = sonifier.audioCtx.createBufferSource()
28-
noiseNode.buffer = buffer
29-
let bandPassFilterNode = sonifier.audioCtx.createBiquadFilter()
30-
bandPassFilterNode.type = 'bandpass'
31-
bandPassFilterNode.frequency.value = 440
32-
noiseNode.connect(bandPassFilterNode)
33-
this.setAudioNode(noiseNode)
34-
}
35-
return super.getAudioNode()
21+
protected extend(timeAdd: number) {
22+
let noiseNode = this.outputNode as AudioBufferSourceNode
23+
if (DEBUG) console.log('noiseSonify, getting noise node', noiseNode)
24+
if (noiseNode) noiseNode.buffer = this.fillBuffer(timeAdd);
3625
}
3726

38-
public update(datum: Datum, duration?: number) {
39-
console.log('NoiseSonify: updateDatum')
40-
super.update(datum)
41-
if (duration) this.duration = duration
42-
let noiseNode = this.getAudioNode() as AudioBufferSourceNode
43-
console.log('noiseSonify, getting noise node', noiseNode)
44-
let buffer = noiseNode.buffer
45-
if (buffer) {
46-
let bufferData = buffer.getChannelData(0)
47-
console.log('filling in buffer data')
48-
for (let i = 0; i < this.noiseBufferSize; i++) {
49-
bufferData[i] = Math.random() * 2 - 1
50-
}
51-
noiseNode.start()
27+
/**
28+
* The length of the buffer for making noise
29+
*/
30+
private noiseBufferSize = 20
31+
/**
32+
* create a buffer and fill it
33+
* @param time Time to fill it for in seconds
34+
*/
35+
private fillBuffer(length: number): AudioBuffer {
36+
let sampleRate = Sonifier.audioCtx.sampleRate
37+
let noiseBufferSize = sampleRate * length;
38+
let buffer = Sonifier.audioCtx.createBuffer(1, noiseBufferSize, Sonifier.audioCtx.sampleRate)
39+
let bufferData = buffer.getChannelData(0)
40+
console.log('filling in buffer data');
41+
for (let i = 0; i < noiseBufferSize; i++) {
42+
bufferData[i] = Math.random() * 2 - 1
5243
}
44+
return buffer;
5345
}
46+
47+
public create(datum: Datum): AudioScheduledSourceNode {
48+
let outputNode = Sonifier.audioCtx.createBufferSource()
49+
this.filter = Sonifier.audioCtx.createBiquadFilter()
50+
this.filter.type = 'bandpass'
51+
this.filter.frequency.value = 440
52+
this.filter.connect(Sonifier.gainNode);
5453

54+
outputNode.buffer = this.fillBuffer(this.duration);
55+
outputNode.connect(this.filter);
56+
this.outputNode = outputNode;
57+
outputNode.start();
58+
return outputNode;
59+
}
60+
5561
public toString(): string {
5662
return `NoiseSonify`
5763
}

src/sonification/displays/NoteSonify.ts

Lines changed: 18 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Datum } from '../Datum'
22
import { Sonify } from './Sonify'
33
import { Sonifier } from '../Sonifier'
44

5+
const DEBUG = false;
6+
57
/**
68
* Class for sonifying a data point as a pitch.
79
* @extends Sonify
@@ -12,27 +14,6 @@ import { Sonifier } from '../Sonifier'
1214
* [note implementation not complete. Needs to handle scheduleSound still]
1315
*/
1416
export class NoteSonify extends Sonify {
15-
/**
16-
* The start and end frequency for the note to play
17-
*/
18-
private frequency: number | undefined
19-
20-
/**
21-
* Returns an audio node that contains an oscillator
22-
*
23-
* @param sonifier
24-
* @returns audio node that contains an oscillator
25-
*/
26-
public getAudioNode(sonifier?: Sonifier) {
27-
console.log('get audio node called')
28-
if (super.getAudioNode()) return super.getAudioNode()
29-
if (sonifier) {
30-
console.log('created audio node')
31-
let oscillator = sonifier.audioCtx.createOscillator()
32-
super.setAudioNode(oscillator)
33-
}
34-
return super.getAudioNode()
35-
}
3617

3718
/**
3819
* Stores relevant information when a new datum arrives
@@ -43,33 +24,33 @@ export class NoteSonify extends Sonify {
4324
*/
4425
public update(datum: Datum, duration = 200, volume?: number, smooth?: boolean) {
4526
super.update(datum)
46-
console.log(`updating value ${this.frequency}`)
47-
let oscillator = this.getAudioNode() as OscillatorNode
48-
if (this.frequency == undefined) {
49-
// first data point
50-
oscillator.frequency.value = datum.adjustedValue
51-
this.frequency = datum.adjustedValue
52-
oscillator.start()
53-
} else {
54-
oscillator.frequency.value = datum.adjustedValue
55-
this.frequency = datum.adjustedValue
56-
}
27+
if (DEBUG) console.log(`updating value ${this.datum.adjustedValue}`)
28+
let oscillator = this.outputNode as OscillatorNode
29+
oscillator.frequency.value = datum.adjustedValue
5730
}
5831

32+
5933
/**
6034
* Generates a new note sonifier
61-
* @param duration The length of time the sound should play for
6235
* @param volume The volume the sound should play at
6336
* @param optionally include an audio node that can be played
64-
* @param smooth optionall specify if sounds should transition smoothly between data points
6537
* @returns Returns an instance of specific subclass of SonificationType.
6638
*/
6739
public constructor(volume?: number, audioNode?: AudioScheduledSourceNode) {
68-
super(volume, audioNode)
40+
41+
super( volume, Sonifier.audioCtx.createOscillator())
42+
43+
let oscillator = this.outputNode as OscillatorNode;
44+
if (oscillator == undefined) {
45+
// oscillator = Sonifier.audioCtx.createOscillator()
46+
this.outputNode = oscillator;
47+
}
48+
oscillator.start();
6949
}
7050

7151
public toString(): string {
72-
//let oscillator = this.getAudioNode() as OscillatorNode;
73-
return `NoteSonify`
52+
let oscillator = this.outputNode as OscillatorNode;
53+
if (oscillator) return `NoteSonify playing ${oscillator.frequency.value}`
54+
else return `NoteSonify not currently playing`
7455
}
7556
}

0 commit comments

Comments
 (0)