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.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
};