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

Play first current and compare series (like the tooltips)

parent 59060ecf
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 { scaleLinear } = require('d3-scale');
const { select } = require('d3-selection'); const { select } = require('d3-selection');
const memoize = require('fast-memoize'); const memoize = require('fast-memoize');
const { createSelector, createStructuredSelector } = require('reselect'); const { createSelector, createStructuredSelector } = require('reselect');
const { flatPointsSelector } = require('./timeseries'); const { yScaleSelector } = require('./scales');
const { tsDatumSelector } = require('./tooltip'); const { tsDatumSelector } = require('./tooltip');
const { dispatch, link } = require('../../lib/redux'); const { dispatch, link } = require('../../lib/redux');
...@@ -14,49 +13,25 @@ const { Actions } = require('../../store'); ...@@ -14,49 +13,25 @@ const { Actions } = require('../../store');
// Higher tones get lower volume // Higher tones get lower volume
const volumeScale = scaleLinear().range([2, .3]); const volumeScale = scaleLinear().range([2, .3]);
const AudioContext = window.AudioContext || window.webkitAudioContext; const AudioContext = window.AudioContext || window.webkitAudioContext;
const getAudioContext = memoize(function () { const getAudioContext = memoize(function () {
return AudioContext ? new AudioContext() : null; return new AudioContext();
});
// 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) { export const createSound = memoize(/* eslint-disable no-unused-vars */ tsKey => {
const audioCtx = getAudioContext();
const oscillator = audioCtx.createOscillator(); 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(); const gainNode = audioCtx.createGain();
gainNode.gain.value = volumeScale(value); const compressor = audioCtx.createDynamicsCompressor();
gainNode.gain.value = 1;
return gainNode;
};
export const createSound = function (value) { compressor.threshold.setValueAtTime(-50, audioCtx.currentTime);
const audioCtx = getAudioContext(); compressor.knee.setValueAtTime(40, audioCtx.currentTime);
const oscillator = getOscillator(audioCtx, value); compressor.ratio.setValueAtTime(12, audioCtx.currentTime);
const gainNode = getGainNode(audioCtx, value); compressor.attack.setValueAtTime(0, audioCtx.currentTime);
const compressor = getCompressor(audioCtx); compressor.release.setValueAtTime(0.25, audioCtx.currentTime);
// Connect the oscillator to the gainNode to modulate volume // Connect the oscillator to the gainNode to modulate volume
oscillator.type = 'sine';
oscillator.connect(gainNode); oscillator.connect(gainNode);
// Connect the gainNode to the compressor to address clipping // Connect the gainNode to the compressor to address clipping
...@@ -68,23 +43,35 @@ export const createSound = function (value) { ...@@ -68,23 +43,35 @@ export const createSound = function (value) {
// Start the oscillator // Start the oscillator
oscillator.start(); oscillator.start();
// This rapidly ramps sound down return {oscillator, gainNode, compressor};
gainNode.gain.setTargetAtTime(0, audioCtx.currentTime, .2); });
};
const audibleScale = function (domain) { export const updateSound = function ({enabled, points}) {
return scaleLinear().domain(domain).range([80, 1500]); const audioCtx = getAudioContext();
for (const tsKey of Object.keys(points)) {
const point = points[tsKey];
const {oscillator, gainNode} = createSound(tsKey);
oscillator.frequency.setTargetAtTime(
enabled && point ? point : null,
audioCtx.currentTime,
.2
);
gainNode.gain.setTargetAtTime(
enabled && point ? volumeScale(point) : null,
audioCtx.currentTime,
.2
);
}
}; };
export const audibleInterfaceOnSelector = state => state.audibleInterfaceOn; export const audibleInterfaceOnSelector = state => state.audibleInterfaceOn;
export const audibleScaleSelector = memoize(tsKey => createSelector( export const audibleScaleSelector = memoize(tsKey => createSelector(
flatPointsSelector(tsKey), yScaleSelector,
(points) => { (scale) => {
return audibleScale([ const audibleScale = scale.copy();
min(points.map((datum) => datum.value)), audibleScale.range([80, 1500]);
max(points.map((datum) => datum.value)) return audibleScale;
]);
} }
)); ));
...@@ -94,15 +81,6 @@ export const audibleUI = function (elem) { ...@@ -94,15 +81,6 @@ export const audibleUI = function (elem) {
return; 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') elem.append('input')
.attr('type', 'checkbox') .attr('type', 'checkbox')
.attr('id', 'audible-checkbox') .attr('id', 'audible-checkbox')
...@@ -121,18 +99,21 @@ export const audibleUI = function (elem) { ...@@ -121,18 +99,21 @@ export const audibleUI = function (elem) {
// Listen for focus changes, and play back the audio representation of // Listen for focus changes, and play back the audio representation of
// the selected points. // the selected points.
// FIXME: Handle more than just the first current time series. // TODO: Handle more than just the first time series of each tsKey. This can
elem.call(link(function (elem, {datum, enabled, scale}) { // piggyback on work to support multiple tooltip selections.
if (!enabled) { elem.call(link(function (elem, {enabled, datumCurrent, datumCompare, yScaleCurrent, yScaleCompare}) {
return; updateSound({
} points: {
if (!datum) { current: datumCurrent ? yScaleCurrent(datumCurrent.value) : null,
return; compare: datumCompare ? yScaleCompare(datumCompare.value) : null
} },
createSound(scale(datum.value)); enabled
});
}, createStructuredSelector({ }, createStructuredSelector({
datum: tsDatumSelector('current'),
enabled: audibleInterfaceOnSelector, enabled: audibleInterfaceOnSelector,
scale: audibleScaleSelector('current') datumCurrent: tsDatumSelector('current'),
datumCompare: tsDatumSelector('compare'),
yScaleCurrent: audibleScaleSelector('current'),
yScaleCompare: audibleScaleSelector('compare')
}))); })));
}; };
...@@ -2,14 +2,79 @@ const { select } = require('d3-selection'); ...@@ -2,14 +2,79 @@ const { select } = require('d3-selection');
const { audibleUI } = require('./audible'); const { audibleUI } = require('./audible');
const { provide } = require('../../lib/redux');
const { Actions, configureStore } = require('../../store');
const TEST_STATE = {
series: {
timeSeries: {
'00060:current': {
startTime: new Date('2018-01-02T15:00:00.000-06:00'),
endTime: new Date('2018-01-02T15:00:00.000-06:00'),
points: [{
dateTime: new Date('2018-01-02T15:00:00.000-06:00'),
value: 10,
qualifiers: ['P']
}],
method: 'method1',
tsKey: 'current',
variable: 45807197
},
'00060:compare': {
startTime: new Date('2018-01-02T15:00:00.000-06:00'),
endTime: new Date('2018-01-02T15:00:00.000-06:00'),
points: [{
dateTime: new Date('2018-01-02T15:00:00.000-06:00'),
value: 10,
qualifiers: ['P']
}],
method: 'method1',
tsKey: 'compare',
variable: 45807197
}
},
variables: {
'45807197': {
variableCode: '00060',
oid: 45807197,
unit: {
unitCode: 'unitCode'
}
}
}
},
currentVariableID: '45807197',
showSeries: {
current: true,
compare: true
}
};
describe('Audible interface', () => { describe('Audible interface', () => {
let container;
beforeEach(() => { beforeEach(() => {
container = select('body').append('div');
container
.call(provide(configureStore(TEST_STATE)))
.call(audibleUI);
}); });
afterEach(() => { afterEach(() => {
container.remove();
});
it('renders', () => {
const checkbox = select('#audible-checkbox');
expect(checkbox).toBeTruthy();
}); });
it('checkbox created successfully', () => { it('does nothing unexpected when playing a sound', () => {
Actions.toggleAudibleInterface(true);
Actions.setTooltipTime(
TEST_STATE.series.timeSeries['00060:current'].points[0].dateTime,
TEST_STATE.series.timeSeries['00060:compare'].points[0].dateTime
);
}); });
}); });
...@@ -358,9 +358,9 @@ const attachToNode = function (store, node, {siteno} = {}) { ...@@ -358,9 +358,9 @@ const attachToNode = function (store, node, {siteno} = {}) {
store.dispatch(Actions.resizeUI(window.innerWidth, node.offsetWidth)); store.dispatch(Actions.resizeUI(window.innerWidth, node.offsetWidth));
select(node) select(node)
.call(provide(store)) .call(provide(store))
.call(audibleUI)
.call(timeSeriesGraph) .call(timeSeriesGraph)
.call(timeSeriesLegend) .call(timeSeriesLegend)
.call(audibleUI)
.select('.hydrograph-last-year-input') .select('.hydrograph-last-year-input')
.on('change', dispatch(function () { .on('change', dispatch(function () {
return Actions.toggleTimeseries('compare', this.checked); return Actions.toggleTimeseries('compare', this.checked);
......
...@@ -203,3 +203,7 @@ table#select-timeseries { ...@@ -203,3 +203,7 @@ table#select-timeseries {
} }
} }
#audible-label {
padding-left: 2em;
}
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