diff --git a/.gitignore b/.gitignore index 79bd6ed445..6b02cb7fd2 100644 --- a/.gitignore +++ b/.gitignore @@ -63,4 +63,6 @@ nuclear.json bundle.electron.js # Prettier configuration file -.prettierrc \ No newline at end of file +.prettierrc + +package-lock.json \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000..43c97e719a --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/app/App.js b/app/App.js index 73b170d45a..532c5c20fb 100644 --- a/app/App.js +++ b/app/App.js @@ -1,11 +1,12 @@ import React from 'react'; import FontAwesome from 'react-fontawesome'; -import Sound from 'react-sound'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { NavLink, withRouter } from 'react-router-dom'; import classnames from 'classnames'; import _ from 'lodash'; +import Sound from 'react-sound-html5'; + import * as Actions from './actions'; import * as PlayerActions from './actions/player'; import * as PlaylistsActions from './actions/playlists'; @@ -151,7 +152,8 @@ class App extends React.Component { {this.renderNavLink('plugins', 'flask', 'Plugins', settings)} {this.renderNavLink('search', 'search', 'Search Results', settings)} {this.renderNavLink('settings', 'cogs', 'Settings', settings)} - + {this.renderNavLink('equalizer', 'sliders', 'Equalizer', settings)} + Collection diff --git a/app/actions/equalizer.js b/app/actions/equalizer.js new file mode 100644 index 0000000000..d602a5b082 --- /dev/null +++ b/app/actions/equalizer.js @@ -0,0 +1,31 @@ +export const UPDATE_EQUALIZER = 'UPDATE_EQUALIZER'; +export const SET_EQUALIZER = 'SET_EQUALIZER'; +export const TOGGLE_VISUALIZATION = 'TOGGLE_VISUALIZATION'; +export const SET_VISUALIZATION_DATA = 'SET_VISUALIZATION_DATA'; + +export function updateEqualizer(payload) { + return { + type: UPDATE_EQUALIZER, + payload + }; +} + +export function setEqualizer(payload) { + return { + type: SET_EQUALIZER, + payload + }; +} + +export function toggleVisualization() { + return { + type: TOGGLE_VISUALIZATION + }; +} + +export function setVisualizationData(payload) { + return { + type: SET_VISUALIZATION_DATA, + payload + }; +} diff --git a/app/actions/player.js b/app/actions/player.js index 9ead678204..7311e58466 100644 --- a/app/actions/player.js +++ b/app/actions/player.js @@ -1,4 +1,4 @@ -import Sound from 'react-sound'; +import Sound from 'react-sound-html5'; import { sendPaused, sendPlay } from '../mpris'; export const START_PLAYBACK = 'START_PLAYBACK'; diff --git a/app/components/Equalizer/PreAmp/index.js b/app/components/Equalizer/PreAmp/index.js new file mode 100644 index 0000000000..3554885b13 --- /dev/null +++ b/app/components/Equalizer/PreAmp/index.js @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import styles from './index.scss'; + +const PreAmp = ({ + onChange, + value +}) => ( + onChange(Number(evt.target.value))} + value={value} + className={styles.preamp} + min={-10} + max={10} + step={1} + /> +); + +PreAmp.propTypes = { + onChange: PropTypes.func.isRequired, + value: PropTypes.number.isRequired +}; + +export default PreAmp; + diff --git a/app/components/Equalizer/PreAmp/index.scss b/app/components/Equalizer/PreAmp/index.scss new file mode 100644 index 0000000000..240c1909d3 --- /dev/null +++ b/app/components/Equalizer/PreAmp/index.scss @@ -0,0 +1,9 @@ +.preamp { + -webkit-appearance: slider-vertical !important; + height: 60%; + margin-top: 30px; + cursor: grab !important; + &:active { + cursor: grabbing !important; + } +} \ No newline at end of file diff --git a/app/components/Equalizer/chart.js b/app/components/Equalizer/chart.js new file mode 100644 index 0000000000..0a1618a02e --- /dev/null +++ b/app/components/Equalizer/chart.js @@ -0,0 +1,63 @@ +function getChartOptions(data) { + return { + type: 'line', + data, + options: { + events: ['mousemove', 'mousedown'], + onHover: (event, chartElement) => { + switch (event.type) { + case 'mousedown': + event.target.style.cursor = chartElement[0] ? 'grabbing' : 'default'; + break; + default: + event.target.style.cursor = chartElement[0] ? 'grab' : 'default'; + } + }, + responsive: true, + legend: { + display: false + }, + tooltips: { + enabled: false + }, + animation: { + duration: 1000 + }, + scales: { + xAxes: [{ + display: true, + gridLines: { + display: false + }, + scaleLabel: { + display: true + } + }], + yAxes: [{ + ticks: { + display: false, + min: 0, + max: 20 + }, + gridLines: { + display: false + }, + scaleLabel: { + display: false + } + }] + } + } + }; +} + +function formatLabels(frequencies) { + return frequencies.map(freq => { + return freq > 999 + ? `${Math.round(freq / 1000)}KHz` + : `${freq}Hz`; + }); +} + +export { getChartOptions, formatLabels }; + diff --git a/app/components/Equalizer/index.js b/app/components/Equalizer/index.js new file mode 100644 index 0000000000..e4854d38f6 --- /dev/null +++ b/app/components/Equalizer/index.js @@ -0,0 +1,227 @@ +/* eslint no-empty: 0 */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import Chart from 'chart.js'; +import { drag } from 'd3-drag'; +import * as d3 from 'd3-selection'; +import _ from 'lodash'; +import { Radio } from 'semantic-ui-react'; + +import PreAmp from './PreAmp'; +import { getChartOptions, formatLabels} from './chart'; + +import styles from './styles.scss'; + +export const filterFrequencies = [60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000]; + +class Equalizer extends React.Component { + canvas; + context; + chartInstance; + state = { viz: false } + + par = { + chart: undefined, + element: undefined, + scale: undefined, + datasetIndex: undefined, + index: undefined, + value: undefined, + grabOffsetY: undefined + }; + + constructor(props) { + super(props); + + this.attachCanvas = this.attachCanvas.bind(this); + this.onDragStart = this.onDragStart.bind(this); + this.updateChart = this.updateChart.bind(this); + this.handlePreampChange = this.handlePreampChange.bind(this); + } + + attachCanvas(element) { + this.canvas = element; + } + + getEventPoints(event) { + const retval = { + point: [], + type: event.type + }; + if (event.type.startsWith('mouse')) { + retval.point.push({ + x: event.layerX, + y: event.layerY + }); + } + return retval; + } + + onDragStart() { + try { + const e = d3.event.sourceEvent; + + this.par.scale = undefined; + + this.par.element = this.chartInstance.getElementAtEvent(e)[0]; + this.par.chart = this.par.element._chart; + this.par.scale = this.par.element._yScale; + + this.par.datasetIndex = this.par.element._datasetIndex; + this.par.index = this.par.element._index; + + this.par.grabOffsetY = this.par.scale.getPixelForValue( + this.par.chart.config.data.datasets[this.par.datasetIndex].data[this.par.index], + this.par.index, + this.par.datasetIndex, + false + ) - this.getEventPoints(e).point[0].y; + } catch (err) {} + } + + updateChart() { + try { + const e = d3.event.sourceEvent; + + if (this.par.datasetIndex === 1) { + return; + } + + this.par.value = Math.floor( + this.par.scale.getValueForPixel( + this.par.grabOffsetY + this.getEventPoints(e).point[0].y + ) + 0.5 + ); + this.par.value = Math.max(0, Math.min(20, this.par.value)); + this.par.chart.config.data.datasets[this.par.datasetIndex].data[this.par.index] = this.par.value; + this.chartInstance.update(0); + + this.props.onChange({ + values: this.par.chart.config.data.datasets[0].data.map(value => value - 10), + preAmp: this.props.preAmp + }); + } catch (err) {} + } + + handlePreampChange(preAmp) { + this.props.onChange({ + preAmp, + values: this.props.values + }); + this.chartInstance.config.data.datasets[1].data = + this.chartInstance.config.data.datasets[1].data.map(() => 10 - preAmp); + this.chartInstance.update(0); + } + + componentDidMount() { + this.context = this.canvas.getContext('2d'); + + const lineGradient = this.context.createLinearGradient(0, 0, 0, 400); + lineGradient.addColorStop(0, 'rgb(80, 250, 123, 0.3)'); + lineGradient.addColorStop(1, 'rgba(40, 42, 54, 0)'); + + const barGradient = this.context.createLinearGradient(0, 0, 0, 400); + barGradient.addColorStop(0, 'rgb(80, 250, 123, 0.1)'); + barGradient.addColorStop(1, 'rgba(40, 42, 54, 0)'); + + this.chartInstance = new Chart(this.context, getChartOptions({ + labels: formatLabels(filterFrequencies), + datasets: [{ + pointBorderColor: 'white', + pointBackgroundColor: 'white', + borderColor: 'rgb(80, 250, 123)', + backgroundColor: lineGradient, + data: this.props.values.map(val => val + 10), + pointHoverRadius: 5, + pointHitRadius: 10, + borderWidth: 2 + }, { + data: Array.from(Array(10).keys()).map(() => 10 - this.props.preAmp), + borderColor: 'rgb(255, 255, 255, 0.7)', + fill: false, + borderWidth: 0.5, + pointBorderWidth: 0, + pointHitRadius: 0, + pointBorderColor: 'transparent', + pointBackgroundColor: 'transparent' + }, { + backgroundColor: barGradient, + borderColor: 'transparent', + data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + pointBorderWidth: 0, + pointHitRadius: 0, + pointBorderColor: 'transparent', + pointBackgroundColor: 'transparent', + steppedLine: 'middle' + }] + })); + + d3.select(this.chartInstance.chart.canvas).call( + drag().container(this.chartInstance.chart.canvas) + .on('start', this.onDragStart) + .on('drag', this.updateChart) + ); + } + + componentDidUpdate(prevProps) { + let update = false; + + if (!_.isEqual(prevProps.values, this.props.values)) { + this.chartInstance.data.datasets[0].data = this.props.values.map(val => val + 10); + update = true; + } + if (prevProps.preAmp !== this.props.preAmp) { + this.chartInstance.data.datasets[1].data = + this.chartInstance.data.datasets[1].data.map(() => 10 - this.props.preAmp); + update = true; + } + + if (!_.isEqual(prevProps.dataViz, this.props.dataViz)) { + this.chartInstance.data.datasets[2].data = this.props.dataViz.map(b => b / 10 + (this.props.preAmp || 0)); + update = true; + } + + if (prevProps.viz && !this.props.viz) { + this.chartInstance.data.datasets[2].data = prevProps.dataViz.map(() => 0); + update = true; + } + + if (update) { + this.chartInstance.update(); + } + } + + render() { + return ( +
+

Equalizer

+
+
+ + +
+
+ +
+
+
+ ); + } +} + +Equalizer.propTypes = { + preAmp: PropTypes.number, + values: PropTypes.arrayOf(PropTypes.number), + onChange: PropTypes.func, + viz: PropTypes.bool, + onToggleViz: PropTypes.func, + dataViz: PropTypes.arrayOf(PropTypes.number) +}; + +export default Equalizer; diff --git a/app/components/Equalizer/styles.scss b/app/components/Equalizer/styles.scss new file mode 100644 index 0000000000..870ba5d2a5 --- /dev/null +++ b/app/components/Equalizer/styles.scss @@ -0,0 +1,27 @@ +.equalizer_wrapper { + width: 100%; + padding: 30px 50px; +} + +.chart_wrapper { + margin: auto; + width: 90%; + canvas { + width: 100% !important; + min-width: 442px; + } +} + +.flexbox { + display: flex; +} + +.vertical_flex { + display: flex; + flex-direction: column; +} + +.toggle_viz { + align-self: center; + margin-top: 20px; +} \ No newline at end of file diff --git a/app/components/EqualizerPresetList/index.js b/app/components/EqualizerPresetList/index.js new file mode 100644 index 0000000000..9960b19f53 --- /dev/null +++ b/app/components/EqualizerPresetList/index.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { List, Icon } from 'semantic-ui-react'; +import classNames from 'classnames'; + +import styles from './styles.scss'; + +const EqualizerPresetList = ({ presets, onClickItem, selected }) => ( + + {presets.map((preset, idx) => ( + preset !== selected && onClickItem(preset)} + className={classNames(styles.item, { + [styles.click_item]: preset !== selected + })} + > + + {preset === selected && } + + {preset} + + ))} + +); + +EqualizerPresetList.propTypes = { + presets: PropTypes.arrayOf(PropTypes.string), + onClickItem: PropTypes.func, + selected: PropTypes.string +}; + +export default EqualizerPresetList; diff --git a/app/components/EqualizerPresetList/styles.scss b/app/components/EqualizerPresetList/styles.scss new file mode 100644 index 0000000000..f64a0fadee --- /dev/null +++ b/app/components/EqualizerPresetList/styles.scss @@ -0,0 +1,17 @@ +.list { + margin: auto !important; + width: 50%; + height: 200px; + overflow: auto; +} + +.item { + padding: 5px !important; +} + +.click_item { + cursor: pointer; + :hover { + border-right: solid 2px white; + } +} diff --git a/app/components/Seekbar/index.js b/app/components/Seekbar/index.js index 9513713d53..6b96aeb320 100644 --- a/app/components/Seekbar/index.js +++ b/app/components/Seekbar/index.js @@ -6,9 +6,10 @@ class Seekbar extends React.Component { handleClick(seek, queue) { return event => { - let percent = (event.pageX - event.target.offsetLeft)/document.body.clientWidth; - let duration = queue.queueItems[queue.currentSong].streams[0].duration; - seek(percent * duration * 1000); + const percent = (event.pageX - event.target.offsetLeft)/document.body.clientWidth; + const duration = queue.queueItems[queue.currentSong].streams[0].duration; + + seek(percent * duration); }; } diff --git a/app/containers/EqualizerViewContainer/index.js b/app/containers/EqualizerViewContainer/index.js new file mode 100644 index 0000000000..d95166be79 --- /dev/null +++ b/app/containers/EqualizerViewContainer/index.js @@ -0,0 +1,46 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; + +import * as EqualizerActions from '../../actions/equalizer'; +import Equalizer from '../../components/Equalizer'; +import EqualizerPresetList from '../../components/EqualizerPresetList'; + +const EqualizerViewContainer = ({ actions, equalizer, presets, selected, viz, dataViz }) => ( + + + + +); + +function mapStateToProps({ equalizer }) { + return { + selected: equalizer.selected, + equalizer: equalizer.presets[equalizer.selected], + presets: Object.keys(equalizer.presets), + viz: equalizer.viz, + dataViz: equalizer.dataViz + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators(EqualizerActions, dispatch) + }; +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(EqualizerViewContainer); diff --git a/app/containers/IpcContainer/index.js b/app/containers/IpcContainer/index.js index 2ff68138b7..09a783ccc5 100644 --- a/app/containers/IpcContainer/index.js +++ b/app/containers/IpcContainer/index.js @@ -7,6 +7,7 @@ import * as PlayerActions from '../../actions/player'; import * as QueueActions from '../../actions/queue'; import * as SettingsActions from '../../actions/settings'; import * as PlaylistActions from '../../actions/playlists'; +import * as EqualizerActions from '../../actions/equalizer'; import { onNext, @@ -24,7 +25,9 @@ import { onMute, onEmptyQueue, onCreatePlaylist, - onRefreshPlaylists + onRefreshPlaylists, + onUpdateEqualizer, + onSetEqualizer } from '../../mpris'; class IpcContainer extends React.Component { @@ -45,6 +48,8 @@ class IpcContainer extends React.Component { ipcRenderer.on('queue', event => sendQueueItems(event, this.props.queue.queueItems)); ipcRenderer.on('create-playlist', (event, name) => onCreatePlaylist(event, { name, tracks: this.props.queue.queueItems }, this.props.actions)); ipcRenderer.on('refresh-playlists', (event) => onRefreshPlaylists(event, this.props.actions)); + ipcRenderer.on('update-equalizer', (event, data) => onUpdateEqualizer(event, this.props.actions, data)); + ipcRenderer.on('set-equalizer', (event, data) => onSetEqualizer(event, this.props.actions, data)); } componentWillReceiveProps(nextProps){ @@ -68,7 +73,17 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { - actions: bindActionCreators(Object.assign({}, PlayerActions, QueueActions, SettingsActions, PlaylistActions), dispatch) + actions: bindActionCreators( + Object.assign( + {}, + PlayerActions, + QueueActions, + SettingsActions, + PlaylistActions, + EqualizerActions + ), + dispatch + ) }; } diff --git a/app/containers/MainContentContainer/index.js b/app/containers/MainContentContainer/index.js index a9bf30c360..031aedc1bf 100644 --- a/app/containers/MainContentContainer/index.js +++ b/app/containers/MainContentContainer/index.js @@ -17,6 +17,7 @@ import PluginsContainer from '../PluginsContainer'; import SearchResultsContainer from '../SearchResultsContainer'; import SettingsContainer from '../SettingsContainer'; import TagViewContainer from '../TagViewContainer'; +import EqualizerViewContainer from '../EqualizerViewContainer'; import Downloads from '../../components/Downloads'; @@ -31,7 +32,7 @@ class MainContentContainer extends React.Component { render () { return ( - { + { return ( @@ -47,6 +48,7 @@ class MainContentContainer extends React.Component { + ); @@ -56,9 +58,8 @@ class MainContentContainer extends React.Component { } } -function mapStateToProps (state) { - return { - }; +function mapStateToProps () { + return {}; } function mapDispatchToProps (dispatch) { diff --git a/app/containers/ShortcutsContainer/index.js b/app/containers/ShortcutsContainer/index.js index 734679f72e..55d4c6588d 100644 --- a/app/containers/ShortcutsContainer/index.js +++ b/app/containers/ShortcutsContainer/index.js @@ -1,8 +1,8 @@ import React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import Sound from 'react-sound'; import * as Mousetrap from 'mousetrap'; +import Sound from 'react-sound-html5'; import * as PlayerActions from '../../actions/player'; import * as QueueActions from '../../actions/queue'; diff --git a/app/containers/SoundContainer/index.js b/app/containers/SoundContainer/index.js index e8ffd67d11..74f1682618 100644 --- a/app/containers/SoundContainer/index.js +++ b/app/containers/SoundContainer/index.js @@ -3,13 +3,15 @@ import { withRouter } from 'react-router-dom'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import _ from 'lodash'; +import Sound from 'react-sound-html5'; import * as Actions from '../../actions'; import * as PlayerActions from '../../actions/player'; +import * as EqualizerActions from '../../actions/equalizer'; import * as QueueActions from '../../actions/queue'; import * as ScrobblingActions from '../../actions/scrobbling'; import * as LyricsActions from '../../actions/lyrics'; -import Sound from 'react-sound'; +import { filterFrequencies } from '../../components/Equalizer'; import { getSelectedStream } from '../../utils'; import * as Autoradio from './autoradio'; import globals from '../../globals'; @@ -18,6 +20,10 @@ import core from 'nuclear-core'; let lastfm = new core.LastFmApi(globals.lastfmApiKey, globals.lastfmApiSecret); class SoundContainer extends React.Component { + constructor(props) { + super(props); + } + handlePlaying (update) { let seek = update.position; let progress = (update.position / update.duration) * 100; @@ -102,7 +108,7 @@ class SoundContainer extends React.Component { } getSimilarArtists (artistJson) { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { resolve(artistJson.similar.artist); }); } @@ -111,7 +117,7 @@ class SoundContainer extends React.Component { let devianceParameter = 0.2; // We will select one of the 20% most similar artists let randomElement = arr[Math.round(Math.random() * (devianceParameter * (arr.length - 1)))]; - return new Promise((resolve, reject) => resolve(randomElement)); + return new Promise((resolve) => resolve(randomElement)); } getArtistTopTracks (artist) { @@ -121,7 +127,7 @@ class SoundContainer extends React.Component { } addToQueue (artist, track) { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { let musicSources = this.props.plugins.plugins.musicSources; this.props.actions.addToQueue(musicSources, { artist: artist.name, @@ -144,11 +150,12 @@ class SoundContainer extends React.Component { } render () { - let { player, queue, plugins } = this.props; + let { player, queue, plugins, equalizer, actions, viz } = this.props; let streamUrl = ''; if (queue.queueItems.length > 0) { - let currentSong = queue.queueItems[queue.currentSong]; + const currentSong = queue.queueItems[queue.currentSong]; + streamUrl = ( getSelectedStream(currentSong.streams, plugins.defaultMusicSource) || {} ).stream; @@ -164,6 +171,12 @@ class SoundContainer extends React.Component { onLoad={this.handleLoaded.bind(this)} position={player.seek} volume={player.muted ? 0 : player.volume} + equalizer={filterFrequencies.reduce((acc, freq, idx) => ({ + ...acc, + [freq]: equalizer.values[idx] || 0 + }), {})} + preAmp={equalizer.preAmp} + onVisualizationChange={viz && actions.setVisualizationData} /> ); } @@ -175,7 +188,9 @@ function mapStateToProps (state) { plugins: state.plugin, player: state.player, scrobbling: state.scrobbling, - settings: state.settings + settings: state.settings, + equalizer: state.equalizer.presets[state.equalizer.selected], + viz: state.equalizer.viz }; } @@ -188,7 +203,8 @@ function mapDispatchToProps (dispatch) { PlayerActions, QueueActions, ScrobblingActions, - LyricsActions + LyricsActions, + EqualizerActions ), dispatch ) diff --git a/app/mpris.js b/app/mpris.js index 07b3479c17..fddaa1113b 100644 --- a/app/mpris.js +++ b/app/mpris.js @@ -70,6 +70,14 @@ export function onRefreshPlaylists(event, actions) { actions.loadPlaylists(); } +export function onSetEqualizer(event, actions, equalizer) { + actions.setEqualizer(equalizer); +} + +export function onUpdateEqualizer(event, actions, data) { + actions.updateEqualizer(data); +} + export function onSongChange(song) { ipcRenderer.send('songChange', song); } diff --git a/app/persistence/store.js b/app/persistence/store.js index 1ceffc96a7..2153bf0f58 100644 --- a/app/persistence/store.js +++ b/app/persistence/store.js @@ -26,6 +26,46 @@ function initStore () { albums: [] }); } + + if (!store.get('equalizer')) { + store.set('equalizer', { + selected: 'default', + presets: { + default: { + values: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + preAmp: 0 + }, + Classical: { + values: [0, 0, 0, 0, 0, 0, -4, -4, -4, -6], + preAmp: 0 + }, + Club: { + values: [0, 0, 2, 4, 4, 4, 2, 0, 0, 0], + preAmp: 0 + }, + Pop: { + values: [-1, 2, 3, 4, 3, 0, -1, -1, -1, -1], + preAmp: 0 + }, + Reggae: { + values: [0, 0, 0, -2, 0, 2, 2, 0, 0, 0], + preAmp: 0 + }, + Rock: { + values: [4, 3, -2, -3, -2, 2, 5, 6, 6, 6], + preAmp: 0 + }, + 'Full bass': { + values: [6, 6, 6, 4, 0, -2, -4, -6, -6, -6], + preAmp: 0 + }, + 'Full trebble': { + values: [-6, -6, -6, -2, 2, 6, 8, 8, 9, 9], + preAmp: 0 + } + } + }); + } } initStore(); diff --git a/app/reducers/equalizer.js b/app/reducers/equalizer.js new file mode 100644 index 0000000000..459a21918e --- /dev/null +++ b/app/reducers/equalizer.js @@ -0,0 +1,60 @@ +import { + UPDATE_EQUALIZER, + SET_EQUALIZER, + TOGGLE_VISUALIZATION, + SET_VISUALIZATION_DATA +} from '../actions/equalizer'; +import { store } from '../persistence/store'; + +const { presets, selected } = store.get('equalizer'); + +const initialState = { + presets, + selected, + viz: false, + dataViz: [] +}; + +export default function EqualizerReducer(state = initialState, action) { + let newState; + + switch (action.type) { + case UPDATE_EQUALIZER: + newState = { + selected: 'custom', + presets: { + ...state.presets, + custom: action.payload + } + }; + store.set('equalizer', newState); + + return { + ...state, + ...newState + }; + case SET_EQUALIZER: + newState = { + presets: state.presets, + selected: action.payload + }; + store.set('equalizer', newState); + + return { + ...state, + ...newState + }; + case TOGGLE_VISUALIZATION: + return { + ...state, + viz: !state.viz + }; + case SET_VISUALIZATION_DATA: + return { + ...state, + dataViz: action.payload + }; + default: + return state; + } +} diff --git a/app/reducers/index.js b/app/reducers/index.js index 3f07708708..ce520f9040 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -12,6 +12,7 @@ import TagReducer from './tag'; import ToastsReducer from './toasts'; import LyricsReducer from './lyrics'; import FavoritesReducer from './favorites'; +import EqualizerReducer from './equalizer'; const rootReducer = combineReducers({ search: SearchReducer, @@ -25,7 +26,8 @@ const rootReducer = combineReducers({ settings: SettingsReducer, toasts: ToastsReducer, lyrics: LyricsReducer, - favorites: FavoritesReducer + favorites: FavoritesReducer, + equalizer: EqualizerReducer }); export default rootReducer; diff --git a/app/reducers/player.js b/app/reducers/player.js index 4b65de89df..35a84a946b 100644 --- a/app/reducers/player.js +++ b/app/reducers/player.js @@ -1,4 +1,4 @@ -import Sound from 'react-sound'; +import Sound from 'react-sound-html5'; import { START_PLAYBACK, diff --git a/package.json b/package.json index 8e2f7ac351..213b30a940 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,11 @@ "billboard-top-100": "^2.0.8", "bluebird": "^3.5.3", "body-parser": "^1.18.3", + "chart.js": "^2.8.0", "cheerio": "^1.0.0-rc.2", "cors": "^2.8.5", + "d3-drag": "^1.2.3", + "d3-selection": "^1.4.0", "electron-platform": "^1.2.0", "electron-store": "^2.0.0", "electron-timber": "^0.5.1", @@ -69,7 +72,7 @@ "react-dom": "^16.3.2", "react-image-smooth-loading": "^2.0.0", "react-range-progress": "^4.0.3", - "react-sound": "^1.1.0", + "react-sound-html5": "^1.3.1", "react-toastify": "^4.5.2", "semantic-ui-react": "^0.82.1", "simple-get-lyrics": "0.0.4", diff --git a/server/http/api/equalizer.js b/server/http/api/equalizer.js new file mode 100644 index 0000000000..71b5071cad --- /dev/null +++ b/server/http/api/equalizer.js @@ -0,0 +1,50 @@ +import express from 'express'; +import { Validator } from 'express-json-validator-middleware'; +import swagger from 'swagger-spec-express'; + +import { store } from '../../store'; +import { onUpdateEqualizer, onSetEqualizer } from '../../mpris'; +import { updateEqualizerSchema } from '../schema'; +import { getStandardDescription } from '../lib/swagger'; + +const { validate } = new Validator({ allErrors: true }); + +export function equalizerRouter() { + const router = express.Router(); + + swagger.swaggerize(router); + + router.get('/', (req, res) => { + res.json(store.get('equalizer')); + }) + .describe(getStandardDescription({ + successDescription: 'the selected equalizer and the presets', + tags: ['Equalizer'] + })); + + router.post('/', validate(updateEqualizerSchema), (req, res) => { + onUpdateEqualizer(req.body.values); + res.send(); + }) + .describe(getStandardDescription({ + tags: ['Equalizer'], + body: ['eqValues'] + })); + + router.post('/:eqName/set', (req, res) => { + const equalizerNames = Object.keys(store.get('equalizer').presets); + + if (!equalizerNames.includes(req.params.eqName)) { + res.status('400').send(`name should be one of ${equalizerNames.toString()}`); + } else { + onSetEqualizer(req.params.eqName); + res.send(); + } + }) + .describe(getStandardDescription({ + tags: ['Equalizer'], + path: ['eqName'] + })); + + return router; +} diff --git a/server/http/api/index.js b/server/http/api/index.js index 10f310b143..df6f5e5a14 100644 --- a/server/http/api/index.js +++ b/server/http/api/index.js @@ -4,3 +4,4 @@ export * from './player'; export * from './swagger'; export * from './playlist'; export * from './queue'; +export * from './equalizer'; diff --git a/server/http/lib/swagger.js b/server/http/lib/swagger.js index 625fef9921..93bca666be 100644 --- a/server/http/lib/swagger.js +++ b/server/http/lib/swagger.js @@ -1,6 +1,15 @@ import swagger from 'swagger-spec-express'; -import { volumeSchema, seekSchema, updateSettingsSchema, getSettingsSchema, addPlaylistSchema, deletePlaylistSchema } from '../schema'; +import { + volumeSchema, + seekSchema, + updateSettingsSchema, + getSettingsSchema, + addPlaylistSchema, + deletePlaylistSchema, + updateEqualizerSchema, + setEqualizerSchema +} from '../schema'; export function getStandardDescription({ successDescription = 'Action successfull', @@ -53,6 +62,10 @@ export function initSwagger(app) { { name: 'Queue', description: 'Queue related endpoints' + }, + { + name: 'Equalizer', + description: 'Equalizer related endpoints' } ] }); @@ -98,5 +111,19 @@ export function initSwagger(app) { required: true, ...deletePlaylistSchema.params.properties.name }); + + swagger.common.parameters.addPath({ + name: 'eqName', + description: 'The name of the equalizer presets to set', + required: true, + ...setEqualizerSchema.params.properties.name + }); + + swagger.common.parameters.addBody({ + name: 'eqValues', + description: 'The values of the equalizer to set', + required: true, + schema: updateEqualizerSchema.body + }); } diff --git a/server/http/schema.js b/server/http/schema.js index 9ba07a610d..aa454fd6dc 100644 --- a/server/http/schema.js +++ b/server/http/schema.js @@ -94,3 +94,29 @@ export const deletePlaylistSchema = { } } }; + +export const setEqualizerSchema = { + params: { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string' + } + } + } +}; + +export const updateEqualizerSchema = { + body: { + required: ['values'], + properties: { + values: { + type: 'array', + minItems: 10, + maxItems: 10, + items: [{ type: 'number' }] + } + } + } +}; diff --git a/server/http/server.js b/server/http/server.js index 3e03be3cb5..5ff744b94f 100644 --- a/server/http/server.js +++ b/server/http/server.js @@ -10,7 +10,8 @@ import { settingsRouter, swaggerRouter, playlistRouter, - queueRouter + queueRouter, + equalizerRouter } from './api'; import { errorMiddleware, notFoundMiddleware } from './middlewares'; import { initSwagger } from './lib/swagger'; @@ -38,6 +39,7 @@ function runHttpServer({ .use(`${prefix}/docs`, swaggerRouter()) .use(`${prefix}/playlist`, playlistRouter()) .use(`${prefix}/queue`, queueRouter()) + .use(`${prefix}/equalizer`, equalizerRouter()) .use(notFoundMiddleware()) .use(errorMiddleware(logger)) .listen(port, host, err => { diff --git a/server/mpris.js b/server/mpris.js index d1df69119d..9fb7166c1d 100644 --- a/server/mpris.js +++ b/server/mpris.js @@ -63,6 +63,14 @@ function onRemovePlaylist() { rendererWindow.send('refresh-playlists'); } +function onUpdateEqualizer(data) { + rendererWindow.send('update-equalizer', data); +} + +function onSetEqualizer(equalizer) { + rendererWindow.send('set-equalizer', equalizer); +} + function getQueue() { return new Promise(resolve => { rendererWindow.send('queue'); @@ -96,5 +104,7 @@ module.exports = { getQueue, onCreatePlaylist, onRemovePlaylist, - getPlayingStatus + getPlayingStatus, + onUpdateEqualizer, + onSetEqualizer };