From 487f30d320af6c57f2abd564c29454e6025c60e1 Mon Sep 17 00:00:00 2001 From: Daniel Naab <dnaab@usgs.gov> Date: Wed, 14 Mar 2018 10:22:16 -0500 Subject: [PATCH] Play first current and compare series (like the tooltips) --- .../scripts/components/hydrograph/audible.js | 115 ++++++++---------- .../components/hydrograph/audible.spec.js | 67 +++++++++- .../scripts/components/hydrograph/index.js | 2 +- assets/src/styles/components/_hydrograph.scss | 4 + 4 files changed, 119 insertions(+), 69 deletions(-) diff --git a/assets/src/scripts/components/hydrograph/audible.js b/assets/src/scripts/components/hydrograph/audible.js index 31a5521a1..309b66e8e 100644 --- a/assets/src/scripts/components/hydrograph/audible.js +++ b/assets/src/scripts/components/hydrograph/audible.js @@ -1,10 +1,9 @@ -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 { yScaleSelector } = require('./scales'); const { tsDatumSelector } = require('./tooltip'); const { dispatch, link } = require('../../lib/redux'); @@ -14,49 +13,25 @@ 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; + return new AudioContext(); }); -const getOscillator = function(audioCtx, value) { +export const createSound = memoize(/* eslint-disable no-unused-vars */ tsKey => { + const audioCtx = getAudioContext(); 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; -}; + const compressor = audioCtx.createDynamicsCompressor(); -export const createSound = function (value) { - const audioCtx = getAudioContext(); - const oscillator = getOscillator(audioCtx, value); - const gainNode = getGainNode(audioCtx, value); - const compressor = getCompressor(audioCtx); + compressor.threshold.setValueAtTime(-50, audioCtx.currentTime); + compressor.knee.setValueAtTime(40, audioCtx.currentTime); + compressor.ratio.setValueAtTime(12, audioCtx.currentTime); + compressor.attack.setValueAtTime(0, audioCtx.currentTime); + compressor.release.setValueAtTime(0.25, audioCtx.currentTime); // Connect the oscillator to the gainNode to modulate volume + oscillator.type = 'sine'; oscillator.connect(gainNode); // Connect the gainNode to the compressor to address clipping @@ -68,23 +43,35 @@ export const createSound = function (value) { // Start the oscillator oscillator.start(); - // This rapidly ramps sound down - gainNode.gain.setTargetAtTime(0, audioCtx.currentTime, .2); -}; + return {oscillator, gainNode, compressor}; +}); -const audibleScale = function (domain) { - return scaleLinear().domain(domain).range([80, 1500]); +export const updateSound = function ({enabled, points}) { + 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 audibleScaleSelector = memoize(tsKey => createSelector( - flatPointsSelector(tsKey), - (points) => { - return audibleScale([ - min(points.map((datum) => datum.value)), - max(points.map((datum) => datum.value)) - ]); + yScaleSelector, + (scale) => { + const audibleScale = scale.copy(); + audibleScale.range([80, 1500]); + return audibleScale; } )); @@ -94,15 +81,6 @@ export const audibleUI = function (elem) { 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') @@ -121,18 +99,21 @@ export const audibleUI = function (elem) { // 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)); + // TODO: Handle more than just the first time series of each tsKey. This can + // piggyback on work to support multiple tooltip selections. + elem.call(link(function (elem, {enabled, datumCurrent, datumCompare, yScaleCurrent, yScaleCompare}) { + updateSound({ + points: { + current: datumCurrent ? yScaleCurrent(datumCurrent.value) : null, + compare: datumCompare ? yScaleCompare(datumCompare.value) : null + }, + enabled + }); }, createStructuredSelector({ - datum: tsDatumSelector('current'), enabled: audibleInterfaceOnSelector, - scale: audibleScaleSelector('current') + datumCurrent: tsDatumSelector('current'), + datumCompare: tsDatumSelector('compare'), + yScaleCurrent: audibleScaleSelector('current'), + yScaleCompare: audibleScaleSelector('compare') }))); }; diff --git a/assets/src/scripts/components/hydrograph/audible.spec.js b/assets/src/scripts/components/hydrograph/audible.spec.js index 8b053701e..0eb8f213d 100644 --- a/assets/src/scripts/components/hydrograph/audible.spec.js +++ b/assets/src/scripts/components/hydrograph/audible.spec.js @@ -2,14 +2,79 @@ const { select } = require('d3-selection'); 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', () => { + let container; beforeEach(() => { + container = select('body').append('div'); + container + .call(provide(configureStore(TEST_STATE))) + .call(audibleUI); }); 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 + ); }); }); diff --git a/assets/src/scripts/components/hydrograph/index.js b/assets/src/scripts/components/hydrograph/index.js index 1663ae492..727a5bc60 100644 --- a/assets/src/scripts/components/hydrograph/index.js +++ b/assets/src/scripts/components/hydrograph/index.js @@ -358,9 +358,9 @@ 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) + .call(audibleUI) .select('.hydrograph-last-year-input') .on('change', dispatch(function () { return Actions.toggleTimeseries('compare', this.checked); diff --git a/assets/src/styles/components/_hydrograph.scss b/assets/src/styles/components/_hydrograph.scss index 3bd94d7ca..b5435447d 100644 --- a/assets/src/styles/components/_hydrograph.scss +++ b/assets/src/styles/components/_hydrograph.scss @@ -203,3 +203,7 @@ table#select-timeseries { } } + +#audible-label { + padding-left: 2em; +} -- GitLab