diff --git a/js/MainWindow.jsx b/js/MainWindow.jsx
index 0b5abb32..d6197ed1 100644
--- a/js/MainWindow.jsx
+++ b/js/MainWindow.jsx
@@ -67,7 +67,7 @@ const MainWindow = (props) => {
diff --git a/js/Visualizer.jsx b/js/Visualizer.jsx
index 7453dafe..229c2b01 100644
--- a/js/Visualizer.jsx
+++ b/js/Visualizer.jsx
@@ -1,23 +1,201 @@
import React from 'react';
import {connect} from 'react-redux';
+const OSCILLOSCOPE = 1;
+const BAR = 2;
+
+// Return the average value in a slice of dataArray
+function sliceAverage(dataArray, sliceWidth, sliceNumber) {
+ var start = sliceWidth * sliceNumber;
+ var end = start + sliceWidth;
+ var sum = 0;
+ for (var i = start; i < end; i++) {
+ sum += dataArray[i];
+ }
+ return sum / sliceWidth;
+}
+
class Visualizer extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
- handleClick() {
- this.props.dispatch({type: 'TOGGLE_VISUALIZER'});
+ componentDidMount() {
+ this.canvasCtx = this.refs.canvas.getContext('2d');
+ this.canvasCtx.imageSmoothingEnabled = false;
+ this.width = this.refs.canvas.width * 1; // Cast to int
+ this.height = this.refs.canvas.height * 1; // Cast to int
+
+ // Off-screen canvas for pre-rendering the background
+ this.bgCanvas = document.createElement('canvas');
+ this.bgCanvas.width = this.width;
+ this.bgCanvas.height = this.height;
+
+ // Off-screen canvas for pre-rendering a single bar gradient
+ this.barCanvas = document.createElement('canvas');
+ this.barCanvas.width = 6;
+ this.barCanvas.height = 32;
+
+ this.setStyle();
+
+ // Kick off the animation loop
+ const loop = () => {
+ if (this.props.status === 'PLAYING') {
+ this.paintFrame();
+ }
+ window.requestAnimationFrame(loop);
+ };
+ loop();
}
+
+ componentDidUpdate() {
+ this.setStyle();
+ }
+
+ setStyle() {
+ // TODO: Split this into to methods. One for skin update, one for style
+ // update.
+ this.preRenderBg();
+ this.preRenderBar();
+ if (this.props.style === OSCILLOSCOPE) {
+ this.props.analyser.fftSize = 2048;
+ this.bufferLength = this.props.analyser.fftSize;
+ this.dataArray = new Uint8Array(this.bufferLength);
+ } else if (this.props.style === BAR) {
+ this.props.analyser.fftSize = 64; // Must be a power of two
+ // Number of bins/bars we get
+ this.bufferLength = this.props.analyser.frequencyBinCount;
+ this.dataArray = new Uint8Array(this.bufferLength);
+ }
+ // If we are paused when the skin changes, we will keep the vis colors
+ // until we paint again. For now we can just clear the current frame so
+ // we don't end up with a clashing visual.
+
+ // TODO: Once this is split, alwasy clear on skin change
+ if (this.props.status === 'PLAYING') {
+ this.clear();
+ }
+ }
+
+ clear() {
+ this.canvasCtx.drawImage(this.bgCanvas, 0, 0);
+ }
+
+ // Pre-render the background grid
+ preRenderBg() {
+ var bgCanvasCtx = this.bgCanvas.getContext('2d');
+ bgCanvasCtx.fillStyle = this.props.colors[0];
+ bgCanvasCtx.fillRect(0, 0, this.width, this.height);
+ bgCanvasCtx.fillStyle = this.props.colors[1];
+ for (var x = 0; x < this.width; x += 4) {
+ for (var y = 2; y < this.height; y += 4) {
+ bgCanvasCtx.fillRect(x, y, 2, 2);
+ }
+ }
+ }
+
+ // Pre-render the bar gradient
+ preRenderBar() {
+ var barCanvasCtx = this.barCanvas.getContext('2d');
+ barCanvasCtx.fillStyle = this.props.colors[23];
+ barCanvasCtx.fillRect(0, 0, 6, 2);
+ for (var i = 0; i <= 15; i++) {
+ var colorNumber = 17 - i;
+ barCanvasCtx.fillStyle = this.props.colors[colorNumber];
+ var y = 32 - (i * 2);
+ barCanvasCtx.fillRect(0, y, 6, 2);
+ }
+ }
+
+ paintFrame() {
+ this.clear();
+ if (this.props.style === OSCILLOSCOPE) {
+ this._paintOscilloscopeFrame();
+ } else if (this.props.style === BAR) {
+ this._paintBarFrame();
+ }
+ }
+
+ _paintOscilloscopeFrame() {
+
+ this.props.analyser.getByteTimeDomainData(this.dataArray);
+
+ // 2 because we're shrinking the canvas by 2
+ this.canvasCtx.lineWidth = 2;
+
+ // Just use one of the viscolors for now
+ this.canvasCtx.strokeStyle = this.props.colors[18];
+
+ // Since dataArray has more values than we have pixels to display, we
+ // have to average several dataArray values per pixel. We call these
+ // groups slices.
+ //
+ // We use the 2x scale here since we only want to plot values for
+ // "real" pixels.
+ var sliceWidth = Math.floor(this.bufferLength / this.width) * 2;
+
+ // The max amplitude is half the height
+ var h = this.height / 2;
+
+ this.canvasCtx.beginPath();
+
+ // Iterate over the width of the canvas in "real" pixels.
+ for (var j = 0; j <= this.width / 2; j++) {
+ var amplitude = sliceAverage(this.dataArray, sliceWidth, j);
+ var percentAmplitude = amplitude / 128; // dataArray gives us bytes
+ var y = percentAmplitude * h;
+ var x = j * 2;
+
+ // Canvas coordinates are in the middle of the pixel by default.
+ // When we want to draw pixel perfect lines, we will need to
+ // account for that here
+ if (x === 0) {
+ this.canvasCtx.moveTo(x, y);
+ } else {
+ this.canvasCtx.lineTo(x, y);
+ }
+ }
+ this.canvasCtx.stroke();
+ }
+
+ _printBar(x, height) {
+ height = Math.round(height) * 2;
+ if (height > 0) {
+ var y = 32 - height;
+ // Draw the gray peak line
+ this.canvasCtx.drawImage(this.barCanvas, 0, 0, 6, 2, x, y - 2, 6, 2);
+ // Draw the gradient
+ this.canvasCtx.drawImage(this.barCanvas, 0, y, 6, height, x, y, 6, height);
+ }
+ }
+
+ _paintBarFrame() {
+ this.props.analyser.getByteFrequencyData(this.dataArray);
+ for (var j = 0; j < this.bufferLength; j++) {
+ var height = this.dataArray[j] * (14 / 256);
+ this._printBar(j * 8, height);
+ }
+ }
+
+ handleClick() {
+ this.props.dispatch({type: 'TOGGLE_VISUALIZER_STYLE'});
+ }
+
render() {
+ // TODO: Don't rerender DOM on style updates
return ;
}
}
-module.exports = connect((state) => state)(Visualizer);
+const mapStateToProps = (state) => {
+ return {...state.visualizer, status: state.media.status};
+};
+
+module.exports = connect(mapStateToProps)(Visualizer);
diff --git a/js/actionCreators.js b/js/actionCreators.js
index 0b91659a..bad12260 100644
--- a/js/actionCreators.js
+++ b/js/actionCreators.js
@@ -4,10 +4,10 @@ export function play(mediaPlayer) {
return (dispatch, getState) => {
if (getState().media.status === 'PLAYING') {
mediaPlayer.stop();
- dispatch({type: 'MEDIA_IS_STOPPED'});
+ dispatch({type: 'SET_MEDIA_STATUS', status: 'STOPPED'});
} else {
mediaPlayer.play();
- dispatch({type: 'MEDIA_IS_PLAYING'});
+ dispatch({type: 'SET_MEDIA_STATUS', status: 'PLAYING'});
}
};
}
@@ -18,11 +18,11 @@ export function pause(mediaPlayer) {
switch (status) {
case 'PAUSED':
mediaPlayer.play();
- dispatch({type: 'MEDIA_IS_PLAYING'});
+ dispatch({type: 'SET_MEDIA_STATUS', status: 'PLAYING'});
break;
case 'PLAYING':
mediaPlayer.pause();
- dispatch({type: 'MEDIA_IS_PAUSED'});
+ dispatch({type: 'SET_MEDIA_STATUS', status: 'PAUSED'});
break;
}
};
@@ -31,7 +31,7 @@ export function pause(mediaPlayer) {
export function stop(mediaPlayer) {
return (dispatch) => {
mediaPlayer.stop();
- dispatch({type: 'MEDIA_IS_STOPPED'});
+ dispatch({type: 'SET_MEDIA_STATUS', status: 'STOPPED'});
};
}
diff --git a/js/media.js b/js/media.js
index 9c17e33e..a7403ef0 100644
--- a/js/media.js
+++ b/js/media.js
@@ -232,11 +232,6 @@ module.exports = {
if (this._playing) {
this._updatePosition();
this._callbacks.timeupdate();
-
- // _updatePosition might have stopped the playing
- if (this._playing) {
- this._callbacks.visualizerupdate(this._analyser);
- }
}
window.requestAnimationFrame(this._draw.bind(this));
},
diff --git a/js/reducers.js b/js/reducers.js
index 9627caf3..7aa2df07 100644
--- a/js/reducers.js
+++ b/js/reducers.js
@@ -19,6 +19,23 @@ const userInput = (state, action) => {
}
};
+const visualizer = (state, action) => {
+ if (!state) {
+ return {
+ colors: [],
+ style: 2
+ };
+ }
+ switch (action.type) {
+ case 'SET_VISUALIZATION_COLORS':
+ return {...state, colors: action.colors};
+ case 'TOGGLE_VISUALIZER_STYLE':
+ return {...state, style: (state.style + 1) % 3};
+ default:
+ return state;
+ }
+};
+
const display = (state, action) => {
if (!state) {
return {
@@ -114,12 +131,8 @@ const media = (state, action) => {
return {...state, repeat: !state.repeat};
case 'TOGGLE_SHUFFLE':
return {...state, shuffle: !state.shuffle};
- case 'MEDIA_IS_PLAYING':
- return {...state, status: 'PLAYING'};
- case 'MEDIA_IS_PAUSED':
- return {...state, status: 'PAUSED'};
- case 'MEDIA_IS_STOPPED':
- return {...state, status: 'STOPPED'};
+ case 'SET_MEDIA_STATUS':
+ return {...state, status: action.status};
default:
return state;
}
@@ -130,6 +143,7 @@ const createReducer = (winamp) => {
const reducer = combineReducers({
userInput,
display,
+ visualizer,
contextMenu,
media
});
@@ -154,9 +168,6 @@ const createReducer = (winamp) => {
case 'TOGGLE_SHUFFLE':
winamp.toggleShuffle();
return state;
- case 'TOGGLE_VISUALIZER':
- winamp.toggleVisualizer();
- return state;
default:
return state;
}
diff --git a/js/skin.js b/js/skin.js
index 9cc62f03..5791d522 100644
--- a/js/skin.js
+++ b/js/skin.js
@@ -1,12 +1,10 @@
// Dynamically set the css background images for all the sprites
import SKIN_SPRITES from './skin-sprites';
-import Visualizer from './visualizer';
import JSZip from '../node_modules/jszip/dist/jszip'; // Hack
module.exports = {
- init: function(visualizerNode, analyser) {
+ init: function() {
this._createNewStyleNode();
- this.visualizer = Visualizer.init(visualizerNode, analyser);
return this;
},
@@ -38,7 +36,7 @@ module.exports = {
this.styleNode.appendChild(document.createTextNode(cssRules));
this._parseVisColors(zip);
if (this.completedCallback !== void 0) {
- this.completedCallback();
+ this.completedCallback(this.colors);
}
}.bind(this));
},
@@ -58,7 +56,7 @@ module.exports = {
console.error('Error in VISCOLOR.TXT on line', i);
}
}
- this.visualizer.setColors(colors);
+ this.colors = colors;
},
_findFileInZip: function(name, zip) {
diff --git a/js/visualizer.js b/js/visualizer.js
deleted file mode 100644
index 2416c4c5..00000000
--- a/js/visualizer.js
+++ /dev/null
@@ -1,163 +0,0 @@
-/* Use Canvas to recreate the simple Winamp visualizer */
-module.exports = {
- init: function(canvasNode, analyser) {
- this.canvas = canvasNode;
- this.analyser = analyser;
- this.canvasCtx = this.canvas.getContext('2d');
- this.canvasCtx.imageSmoothingEnabled = false;
- this.width = this.canvas.width * 1; // Cast to int
- this.height = this.canvas.height * 1; // Cast to int
- this.colors = []; // skin.js fills this from viscolors.txt
- this.NONE = 0;
- this.OSCILLOSCOPE = 1;
- this.BAR = 2;
- this.bufferLength = null;
- this.dataArray = null;
- this.setStyle(this.BAR);
-
- // Off-screen canvas for pre-rendering the background
- this.bgCanvas = document.createElement('canvas');
- this.bgCanvas.width = this.width;
- this.bgCanvas.height = this.height;
- this.bgCanvasCtx = this.bgCanvas.getContext('2d');
-
- // Off-screen canvas for pre-rendering a single bar gradient
- this.barCanvas = document.createElement('canvas');
- this.barCanvas.width = 6;
- this.barCanvas.height = 32;
- this.barCanvasCtx = this.barCanvas.getContext('2d');
- return this;
- },
-
- clear: function() {
- this.canvasCtx.drawImage(this.bgCanvas, 0, 0);
- },
-
- setColors: function(colors) {
- this.colors = colors;
- this.preRenderBg();
- this.preRenderBar();
- },
-
- // Pre-render the background grid
- preRenderBg: function() {
- this.bgCanvasCtx.fillStyle = this.colors[0];
- this.bgCanvasCtx.fillRect(0, 0, this.width, this.height);
- this.bgCanvasCtx.fillStyle = this.colors[1];
- for (var x = 0; x < this.width; x += 4) {
- for (var y = 2; y < this.height; y += 4) {
- this.bgCanvasCtx.fillRect(x, y, 2, 2);
- }
- }
- },
-
- // Pre-render the bar gradient
- preRenderBar: function() {
- this.barCanvasCtx.fillStyle = this.colors[23];
- this.barCanvasCtx.fillRect(0, 0, 6, 2);
- for (var i = 0; i <= 15; i++) {
- var colorNumber = 17 - i;
- this.barCanvasCtx.fillStyle = this.colors[colorNumber];
- var y = 32 - (i * 2);
- this.barCanvasCtx.fillRect(0, y, 6, 2);
- }
- // If we are paused when the skin changes, we will keep the vis colors
- // until we paint again. For now we can just clear the current frame so
- // we don't end up with a clashing visual.
- this.clear();
- },
-
- setStyle: function(style) {
- this.style = style;
- if (this.style === this.OSCILLOSCOPE) {
- this.analyser.fftSize = 2048;
- this.bufferLength = this.analyser.fftSize;
- this.dataArray = new Uint8Array(this.bufferLength);
- } else if (this.style === this.BAR) {
- this.analyser.fftSize = 64; // Must be a power of two
- // Number of bins/bars we get
- this.bufferLength = this.analyser.frequencyBinCount;
- this.dataArray = new Uint8Array(this.bufferLength);
- }
- },
-
- paintFrame: function() {
- this.clear();
- if (this.style === this.OSCILLOSCOPE) {
- return this._paintOscilloscopeFrame();
- } else if (this.style === this.BAR) {
- return this._paintBarFrame();
- }
- },
-
- _paintOscilloscopeFrame: function() {
- // Return the average value in a slice of dataArray
- function sliceAverage(dataArray, sliceWidth, sliceNumber) {
- var start = sliceWidth * sliceNumber;
- var end = start + sliceWidth;
- var sum = 0;
- for (var i = start; i < end; i++) {
- sum += dataArray[i];
- }
- return sum / sliceWidth;
- }
-
- this.analyser.getByteTimeDomainData(this.dataArray);
-
- // 2 because we're shrinking the canvas by 2
- this.canvasCtx.lineWidth = 2;
-
- // Just use one of the viscolors for now
- this.canvasCtx.strokeStyle = this.colors[18];
-
- // Since dataArray has more values than we have pixels to display, we
- // have to average several dataArray values per pixel. We call these
- // groups slices.
- //
- // We use the 2x scale here since we only want to plot values for
- // "real" pixels.
- var sliceWidth = Math.floor(this.bufferLength / this.width) * 2;
-
- // The max amplitude is half the height
- var h = this.height / 2;
-
- this.canvasCtx.beginPath();
-
- // Iterate over the width of the canvas in "real" pixels.
- for (var j = 0; j <= this.width / 2; j++) {
- var amplitude = sliceAverage(this.dataArray, sliceWidth, j);
- var percentAmplitude = amplitude / 128; // dataArray gives us bytes
- var y = percentAmplitude * h;
- var x = j * 2;
-
- // Canvas coordinates are in the middle of the pixel by default.
- // When we want to draw pixel perfect lines, we will need to
- // account for that here
- if (x === 0) {
- this.canvasCtx.moveTo(x, y);
- } else {
- this.canvasCtx.lineTo(x, y);
- }
- }
- this.canvasCtx.stroke();
- },
-
- _paintBarFrame: function() {
- var printBar = function(x, height) {
- height = Math.round(height) * 2;
- if (height > 0) {
- var y = 32 - height;
- // Draw the gray peak line
- this.canvasCtx.drawImage(this.barCanvas, 0, 0, 6, 2, x, y - 2, 6, 2);
- // Draw the gradient
- this.canvasCtx.drawImage(this.barCanvas, 0, y, 6, height, x, y, 6, height);
- }
- }.bind(this);
-
- this.analyser.getByteFrequencyData(this.dataArray);
- for (var j = 0; j < this.bufferLength; j++) {
- var height = this.dataArray[j] * (14 / 256);
- printBar(j * 8, height);
- }
- }
-};
diff --git a/js/winamp.js b/js/winamp.js
index 8f509d1c..100df1c6 100755
--- a/js/winamp.js
+++ b/js/winamp.js
@@ -12,8 +12,7 @@ module.exports = {
this.fileInput.type = 'file';
this.fileInput.style.display = 'none';
- this.skin = Skin.init(document.getElementById('visualizer'), this.media._analyser);
- this.state = '';
+ this.skin = Skin.init(this.media._analyser);
this.events = {
timeUpdated: new Event('timeUpdated')
@@ -37,13 +36,8 @@ module.exports = {
window.dispatchEvent(this.events.timeUpdated);
});
- this.media.addEventListener('visualizerupdate', (analyser) => {
- this.skin.visualizer.paintFrame(this.visualizerStyle, analyser);
- });
-
this.media.addEventListener('ended', () => {
- this.skin.visualizer.clear();
- this.dispatch({type: 'MEDIA_IS_STOPPED'});
+ this.dispatch({type: 'SET_MEDIA_STATUS', status: 'STOPPED'});
});
this.media.addEventListener('waiting', () => {
@@ -54,10 +48,6 @@ module.exports = {
this.dispatch({type: 'STOP_WORKING'});
});
- this.media.addEventListener('playing', () => {
- this.dispatch({type: 'MEDIA_IS_PLAYING'});
- });
-
this.fileInput.onchange = (e) => {
this.loadFromFileReference(e.target.files[0]);
};
@@ -97,7 +87,6 @@ module.exports = {
close: function() {
this.media.stop();
- this.dispatch({type: 'MEDIA_IS_STOPPED'});
},
openFileDialog: function() {
@@ -108,7 +97,9 @@ module.exports = {
const file = new MyFile();
file.setFileReference(fileReference);
if (new RegExp('(wsz|zip)$', 'i').test(fileReference.name)) {
- this.skin.setSkinByFile(file);
+ this.skin.setSkinByFile(file, (colors) => {
+ this.dispatch({type: 'SET_VISUALIZATION_COLORS', colors});
+ });
} else {
this.media.autoPlay = true;
this.fileName = fileReference.name;
@@ -130,18 +121,10 @@ module.exports = {
setSkin: function(file) {
this.dispatch({type: 'START_LOADING'});
- this.skin.setSkinByFile(file, () => this.dispatch({type: 'STOP_LOADING'}));
- },
-
- toggleVisualizer: function() {
- if (this.skin.visualizer.style === this.skin.visualizer.NONE) {
- this.skin.visualizer.setStyle(this.skin.visualizer.BAR);
- } else if (this.skin.visualizer.style === this.skin.visualizer.BAR) {
- this.skin.visualizer.setStyle(this.skin.visualizer.OSCILLOSCOPE);
- } else if (this.skin.visualizer.style === this.skin.visualizer.OSCILLOSCOPE) {
- this.skin.visualizer.setStyle(this.skin.visualizer.NONE);
- }
- this.skin.visualizer.clear();
+ this.skin.setSkinByFile(file, (colors) => {
+ this.dispatch({type: 'STOP_LOADING'});
+ this.dispatch({type: 'SET_VISUALIZATION_COLORS', colors});
+ });
},
/* Listeners */