Skip to content
Snippets Groups Projects
Commit 59060ecf authored by Naab, Daniel James's avatar Naab, Daniel James
Browse files

WDFN-31 - Initial sonification - will play on mouse over.

parent 015c13b3
No related branches found
No related tags found
No related merge requests found
const { max, min } = require('d3-array');
const { scaleLinear } = require('d3-scale');
const { select } = require('d3-selection');
const memoize = require('fast-memoize');
const { createSelector, createStructuredSelector } = require('reselect');
const { flatPointsSelector } = require('./timeseries');
const { tsDatumSelector } = require('./tooltip');
const { dispatch, link } = require('../../lib/redux');
const { Actions } = require('../../store');
// Higher tones get lower volume
const volumeScale = scaleLinear().range([2, .3]);
const AudioContext = window.AudioContext || window.webkitAudioContext;
const getAudioContext = memoize(function () {
return AudioContext ? new AudioContext() : null;
});
// Create a compressor node, to prevent clipping noises
const getCompressor = memoize(audioCtx => {
const compressor = audioCtx.createDynamicsCompressor();
compressor.threshold.setValueAtTime(-50, getAudioContext().currentTime);
compressor.knee.setValueAtTime(40, getAudioContext().currentTime);
compressor.ratio.setValueAtTime(12, getAudioContext().currentTime);
compressor.attack.setValueAtTime(0, getAudioContext().currentTime);
compressor.release.setValueAtTime(0.25, getAudioContext().currentTime);
return compressor;
});
const getOscillator = function(audioCtx, value) {
const oscillator = audioCtx.createOscillator();
//oscillator.type = 'triangle';
oscillator.type = 'sine';
// Set the frequency, in hertz
oscillator.frequency.value = value;
return oscillator;
};
const getGainNode = function(audioCtx, value) {
const gainNode = audioCtx.createGain();
gainNode.gain.value = volumeScale(value);
gainNode.gain.value = 1;
return gainNode;
};
export const createSound = function (value) {
const audioCtx = getAudioContext();
const oscillator = getOscillator(audioCtx, value);
const gainNode = getGainNode(audioCtx, value);
const compressor = getCompressor(audioCtx);
// Connect the oscillator to the gainNode to modulate volume
oscillator.connect(gainNode);
// Connect the gainNode to the compressor to address clipping
gainNode.connect(compressor);
// Connect the compressor to the output context
compressor.connect(audioCtx.destination);
// Start the oscillator
oscillator.start();
// This rapidly ramps sound down
gainNode.gain.setTargetAtTime(0, audioCtx.currentTime, .2);
};
const audibleScale = function (domain) {
return scaleLinear().domain(domain).range([80, 1500]);
};
export const audibleInterfaceOnSelector = state => state.audibleInterfaceOn;
export const audibleScaleSelector = memoize(tsKey => createSelector(
flatPointsSelector(tsKey),
(points) => {
return audibleScale([
min(points.map((datum) => datum.value)),
max(points.map((datum) => datum.value))
]);
}
));
export const audibleUI = function (elem) {
if (!AudioContext) {
console.warn('AudioContext not available');
return;
}
elem.append('audio')
.attr('id', 'audible-controls')
.attr('controls', true)
.attr('muted', true)
.style('width', '100%')
.on('change', function () {
console.log(arguments);
});
elem.append('input')
.attr('type', 'checkbox')
.attr('id', 'audible-checkbox')
.attr('aria-labelledby', 'audible-label')
.on('click', dispatch(function () {
return Actions.toggleAudibleInterface(this.checked);
}))
.call(link(function (checked) {
select(this).attr('checked', checked);
}, audibleInterfaceOnSelector));
elem.append('label')
.attr('id', 'audible-label')
.attr('for', 'audible-checkbox')
.text('Audible Interface');
// Listen for focus changes, and play back the audio representation of
// the selected points.
// FIXME: Handle more than just the first current time series.
elem.call(link(function (elem, {datum, enabled, scale}) {
if (!enabled) {
return;
}
if (!datum) {
return;
}
createSound(scale(datum.value));
}, createStructuredSelector({
datum: tsDatumSelector('current'),
enabled: audibleInterfaceOnSelector,
scale: audibleScaleSelector('current')
})));
};
const { select } = require('d3-selection');
const { audibleUI } = require('./audible');
describe('Audible interface', () => {
beforeEach(() => {
});
afterEach(() => {
});
it('checkbox created successfully', () => {
});
});
......@@ -9,6 +9,7 @@ const { createStructuredSelector } = require('reselect');
const { addSVGAccessibility, addSROnlyTable } = require('../../accessibility');
const { dispatch, link, provide } = require('../../lib/redux');
const { audibleUI } = require('./audible');
const { appendAxes, axesSelector } = require('./axes');
const { MARGIN, CIRCLE_RADIUS, CIRCLE_RADIUS_SINGLE_PT, SPARK_LINE_DIM, layoutSelector } = require('./layout');
const { drawSimpleLegend, legendMarkerRowsSelector } = require('./legend');
......@@ -357,6 +358,7 @@ const attachToNode = function (store, node, {siteno} = {}) {
store.dispatch(Actions.resizeUI(window.innerWidth, node.offsetWidth));
select(node)
.call(provide(store))
.call(audibleUI)
.call(timeSeriesGraph)
.call(timeSeriesLegend)
.select('.hydrograph-last-year-input')
......
......@@ -88,7 +88,7 @@ const tooltipFocusTimeSelector = memoize(tsKey => createSelector(
* @param String} tsKey - Timeseries key
* @return {Object}
*/
const tsDatumSelector = memoize(tsKey => createSelector(
export const tsDatumSelector = memoize(tsKey => createSelector(
pointsSelector(tsKey),
tooltipFocusTimeSelector(tsKey),
(points, tooltipFocusTime) => {
......
......@@ -133,6 +133,12 @@ export const Actions = {
type: 'SET_GAGE_HEIGHT',
gageHeightIndex
};
},
toggleAudibleInterface(audibleInterfaceOn) {
return {
type: 'AUDIBLE_INTERFACE_TOGGLE',
audibleInterfaceOn
};
}
};
......@@ -230,6 +236,12 @@ export const timeSeriesReducer = function (state={}, action) {
gageHeight: state.floodStages[action.gageHeightIndex]
};
case 'AUDIBLE_INTERFACE_TOGGLE':
return {
...state,
audibleInterfaceOn: action.audibleInterfaceOn
};
default:
return state;
}
......@@ -262,6 +274,7 @@ export const configureStore = function (initialState) {
floodStages: [],
floodExtent: {},
gageHeight: null,
audibleInterfaceOn: false,
...initialState
};
......
......@@ -69,6 +69,13 @@ describe('Redux store', () => {
compareTime: new Date('2017-01-03')
});
});
it('should create an action to toggle audible interface', () => {
expect(Actions.toggleAudibleInterface(true)).toEqual({
type: 'AUDIBLE_INTERFACE_TOGGLE',
audibleInterfaceOn: true
});
});
});
describe('reducers', () => {
......@@ -187,5 +194,14 @@ describe('Redux store', () => {
gageHeight: 10
});
});
it('should handle AUDIBLE_INTERFACE_TOGGLE', () => {
expect(timeSeriesReducer({audibleInterfaceOn: false}, {
type: 'AUDIBLE_INTERFACE_TOGGLE',
audibleInterfaceOn: true
})).toEqual({
audibleInterfaceOn: true
});
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment