-
-
-
- {this.props.title.name} - {this.props.title.album}
-
+ render() {
+ return (
+
+
+
+
+ {this.props.title.name} - {this.props.title.album}
+
- {this.props.title.name === "Journey" &&
- this.props.title.artist === "Mseq" ? (
-
-
Journey
+ {this.props.title.name === "Journey" &&
+ this.props.title.artist === "Mseq" ? (
+
+ ) : null}
+
- ) : null}
-
-
- );
- }
+ );
+ }
}
function mapStateToProps(state: State): ControlProps {
- return {
- title: state.musicState.title,
- };
+ return {
+ title: state.musicState.title,
+ };
}
export default connect(mapStateToProps)(Controls);
diff --git a/src/music/components/indicator.tsx b/src/music/components/indicator.tsx
index 544d221..f4c2d7e 100644
--- a/src/music/components/indicator.tsx
+++ b/src/music/components/indicator.tsx
@@ -2,59 +2,59 @@ import React from "react";
import { connect } from "react-redux";
import classNames from "classnames";
-import { State } from "../store";
-import { MusicState, togglePlay } from "../store/music/types";
+import { State, Dispatch } from "../store";
+import { togglePlay } from "../store/music/types";
type IndicatorProps = {
- muted: boolean;
- playing: boolean;
+ muted: boolean;
+ playing: boolean;
};
type IndicatorDispatch = {
- play: () => void;
+ play: () => void;
};
type Props = IndicatorProps & IndicatorDispatch;
class Indicator extends React.Component
{
- click() {
- this.props.play();
- }
+ click() {
+ this.props.play();
+ }
- render() {
- let classes = classNames({
- btn: true,
- "col-auto": true,
- fas: true,
- "fa-muted": this.props.muted,
- "fa-play": this.props.playing,
- "fa-pause": !this.props.playing,
- });
+ render() {
+ let classes = classNames({
+ btn: true,
+ "col-auto": true,
+ fas: true,
+ "fa-muted": this.props.muted,
+ "fa-play": this.props.playing,
+ "fa-pause": !this.props.playing,
+ });
- return (
-
- );
- }
+ return (
+
+ );
+ }
}
function mapStateToProps(state: State): IndicatorProps {
- return {
- muted: state.musicState.muted,
- playing: state.musicState.playing,
- };
+ return {
+ muted: state.musicState.muted,
+ playing: state.musicState.playing,
+ };
}
-function mapDispatchToProps(dispatch: Redux.Dispatch): IndicatorDispatch {
- return {
- play: () => {
- dispatch(togglePlay());
- },
- };
+function mapDispatchToProps(dispatch: Dispatch): IndicatorDispatch {
+ return {
+ play: () => {
+ dispatch(togglePlay());
+ },
+ };
}
export default connect(mapStateToProps, mapDispatchToProps)(Indicator);
diff --git a/src/music/components/visualizer.tsx b/src/music/components/visualizer.tsx
index ebb7bfe..113fff9 100644
--- a/src/music/components/visualizer.tsx
+++ b/src/music/components/visualizer.tsx
@@ -2,217 +2,217 @@ import React from "react";
import * as three from "three";
type VisualizerProps = {
- audioContext: AudioContext;
- audioSource: AudioNode;
+ audioContext: AudioContext;
+ audioSource: AudioNode;
};
class CanvasDrawer {
- private analyser: AnalyserNode;
- private canvas: HTMLCanvasElement;
+ private analyser: AnalyserNode;
+ private canvas: HTMLCanvasElement;
- private analyserData: Float32Array;
+ private analyserData: Float32Array;
- private boxes: Array;
- private camera: three.Camera;
- private renderer: three.Renderer;
- private scene: three.Scene;
+ private boxes: Array;
+ private camera: three.PerspectiveCamera;
+ private renderer: three.WebGLRenderer;
+ private scene: three.Scene;
- private angle: number;
+ private angle: number;
- private animationFrame: number;
- private lastTime: number;
+ private animationFrame: number;
+ private lastTime: number;
- constructor(analyser: AnalyserNode, canvas: HTMLCanvasElement) {
- this.analyser = analyser;
- this.canvas = canvas;
+ constructor(analyser: AnalyserNode, canvas: HTMLCanvasElement) {
+ this.analyser = analyser;
+ this.canvas = canvas;
- // Set up analyser data storage
- this.analyserData = new Float32Array(analyser.frequencyBinCount);
+ // Set up analyser data storage
+ this.analyserData = new Float32Array(analyser.frequencyBinCount);
- // Initialize the scene
- this.scene = new three.Scene();
+ // Initialize the scene
+ this.scene = new three.Scene();
- // Make a bunch of boxes to represent the bars
- this.boxes = Array(analyser.frequencyBinCount);
- let width = 2 / analyser.frequencyBinCount;
- for (let freq = 0; freq < analyser.frequencyBinCount; freq++) {
- let geometry = new three.BoxGeometry(1, 1, 1);
- let material = new three.MeshLambertMaterial({
- color: new three.Color(0x99d1ce),
- });
- let cube = new three.Mesh(geometry, material);
+ // Make a bunch of boxes to represent the bars
+ this.boxes = Array(analyser.frequencyBinCount);
+ let width = 2 / analyser.frequencyBinCount;
+ for (let freq = 0; freq < analyser.frequencyBinCount; freq++) {
+ let geometry = new three.BoxGeometry(1, 1, 1);
+ let material = new three.MeshLambertMaterial({
+ color: new three.Color(0x99d1ce),
+ });
+ let cube = new three.Mesh(geometry, material);
- cube.scale.set(width, 1e-6, width);
- cube.position.set(-1 + freq * width, 0, 0);
+ cube.scale.set(width, 1e-6, width);
+ cube.position.set(-1 + freq * width, 0, 0);
- this.scene.add(cube);
- this.boxes[freq] = cube;
+ this.scene.add(cube);
+ this.boxes[freq] = cube;
+ }
+
+ // Add lights for shadowing
+ let ambientLight = new three.AmbientLight(0xffffff, 0.4);
+ this.scene.add(ambientLight);
+
+ let directionalLight = new three.DirectionalLight(0xffffff, 1);
+ directionalLight.position.set(-1, 0.3, -1);
+ directionalLight.castShadow = true;
+ this.scene.add(directionalLight);
+
+ // Add a camera
+ this.angle = 3;
+ this.camera = new three.PerspectiveCamera(
+ 70,
+ canvas.width / canvas.height,
+ 0.01,
+ 10
+ );
+ this.camera.lookAt(0, 0, 0);
+ this.scene.add(this.camera);
+ this.rotateCamera(1);
+
+ // Add a renderer
+ this.renderer = new three.WebGLRenderer({
+ antialias: true,
+ canvas: canvas,
+ powerPreference: "low-power",
+ });
+
+ this.renderer.setClearColor(new three.Color(0x0f0f0f), 1.0);
+ this.renderer.setSize(canvas.width, canvas.height);
+
+ // Set up canvas resizing
+ window.addEventListener("resize", this.resize.bind(this));
+
+ // Run the first, set the first animation frame time and start requesting
+ // animation frames
+ this.resize();
+ this.lastTime = 0;
+ this.animationFrame = requestAnimationFrame(this.render.bind(this));
}
- // Add lights for shadowing
- let ambientLight = new three.AmbientLight(0xffffff, 0.4);
- this.scene.add(ambientLight);
+ render(time: number) {
+ // Set our animation frame to 0, so that if we stop, we don't try to cancel a past animation frame
+ this.animationFrame = 0;
+ // Update elapsed time
+ let elapsed = time - this.lastTime;
+ this.lastTime = time;
- let directionalLight = new three.DirectionalLight(0xffffff, 1);
- directionalLight.position.set(-1, 0.3, -1);
- directionalLight.castShadow = true;
- this.scene.add(directionalLight);
+ let camera = this.camera;
+ let renderer = this.renderer;
+ let scene = this.scene;
- // Add a camera
- this.angle = 3;
- this.camera = new three.PerspectiveCamera(
- 70,
- canvas.width / canvas.height,
- 0.01,
- 10
- );
- this.camera.lookAt(0, 0, 0);
- this.scene.add(this.camera);
- this.rotateCamera(1);
+ this.scaleBoxes();
+ this.rotateCamera(elapsed);
- // Add a renderer
- this.renderer = new three.WebGLRenderer({
- antialias: true,
- canvas: canvas,
- powerPreference: "low-power",
- });
-
- this.renderer.setClearColor(new three.Color(0x0f0f0f));
- this.renderer.setSize(canvas.width, canvas.height);
-
- // Set up canvas resizing
- window.addEventListener("resize", this.resize.bind(this));
-
- // Run the first, set the first animation frame time and start requesting
- // animation frames
- this.resize();
- this.lastTime = 0;
- this.animationFrame = requestAnimationFrame(this.render.bind(this));
- }
-
- render(time: number) {
- // Set our animation frame to 0, so that if we stop, we don't try to cancel a past animation frame
- this.animationFrame = 0;
- // Update elapsed time
- let elapsed = time - this.lastTime;
- this.lastTime = time;
-
- let camera = this.camera;
- let renderer = this.renderer;
- let scene = this.scene;
-
- this.scaleBoxes();
- this.rotateCamera(elapsed);
-
- renderer.render(scene, camera);
- this.animationFrame = requestAnimationFrame(this.render.bind(this));
- }
-
- scaleBoxes() {
- let analyser = this.analyser;
-
- analyser.getFloatFrequencyData(this.analyserData);
-
- for (let freq = 0; freq < analyser.frequencyBinCount; freq++) {
- let height = analyser.maxDecibels / this.analyserData[freq];
-
- if (height > 0.3) {
- height -= 0.3;
- } else {
- height = 1e-6;
- }
-
- this.boxes[freq].scale.y = height;
- }
- }
-
- rotateCamera(elapsed: number) {
- if (this.angle >= Math.PI * 2) {
- this.angle = 0;
- } else {
- this.angle += 0.1 * (elapsed / 1000);
+ renderer.render(scene, camera);
+ this.animationFrame = requestAnimationFrame(this.render.bind(this));
}
- let camera = this.camera;
- let angle = this.angle;
+ scaleBoxes() {
+ let analyser = this.analyser;
- camera.position.x = 1.01 * Math.sin(angle);
- camera.position.z = 1.01 * Math.cos(angle);
+ analyser.getFloatFrequencyData(this.analyserData);
- /* camera.position.y = (1 - Math.abs(angle - 0.5) / 0.5); */
- camera.lookAt(0, 0, 0);
- }
+ for (let freq = 0; freq < analyser.frequencyBinCount; freq++) {
+ let height = analyser.maxDecibels / this.analyserData[freq];
- resize() {
- let canvas = this.canvas;
- if (canvas.parentElement === null) {
- throw Error("Could not access canvas parent for size calculation");
+ if (height > 0.3) {
+ height -= 0.3;
+ } else {
+ height = 1e-6;
+ }
+
+ this.boxes[freq].scale.y = height;
+ }
}
- // Compute the height of all our siblings
- let combinedHeight = 0;
- for (let i = 0; i < canvas.parentElement.children.length; i++) {
- const child = canvas.parentElement.children[i];
+ rotateCamera(elapsed: number) {
+ if (this.angle >= Math.PI * 2) {
+ this.angle = 0;
+ } else {
+ this.angle += 0.1 * (elapsed / 1000);
+ }
- if (child != canvas) {
- combinedHeight += child.clientHeight;
- }
+ let camera = this.camera;
+ let angle = this.angle;
+
+ camera.position.x = 1.01 * Math.sin(angle);
+ camera.position.z = 1.01 * Math.cos(angle);
+
+ /* camera.position.y = (1 - Math.abs(angle - 0.5) / 0.5); */
+ camera.lookAt(0, 0, 0);
}
- // The remaining space we want to fill
- let remainingHeight = canvas.parentElement.clientHeight - combinedHeight;
- canvas.height = remainingHeight;
- canvas.width = canvas.parentElement.clientWidth;
+ resize() {
+ let canvas = this.canvas;
+ if (canvas.parentElement === null) {
+ throw Error("Could not access canvas parent for size calculation");
+ }
- this.camera.aspect = canvas.width / remainingHeight;
- this.camera.updateProjectionMatrix();
- this.renderer.setSize(canvas.width, remainingHeight);
- }
+ // Compute the height of all our siblings
+ let combinedHeight = 0;
+ for (let i = 0; i < canvas.parentElement.children.length; i++) {
+ const child = canvas.parentElement.children[i];
- stop() {
- if (this.animationFrame != 0) {
- cancelAnimationFrame(this.animationFrame);
+ if (child != canvas) {
+ combinedHeight += child.clientHeight;
+ }
+ }
+
+ // The remaining space we want to fill
+ let remainingHeight = canvas.parentElement.clientHeight - combinedHeight;
+ canvas.height = remainingHeight;
+ canvas.width = canvas.parentElement.clientWidth;
+
+ this.camera.aspect = canvas.width / remainingHeight;
+ this.camera.updateProjectionMatrix();
+ this.renderer.setSize(canvas.width, remainingHeight);
+ }
+
+ stop() {
+ if (this.animationFrame != 0) {
+ cancelAnimationFrame(this.animationFrame);
+ }
}
- }
}
class Visualizer extends React.Component {
- private analyser: AnalyserNode;
- private canvas: React.RefObject;
- private drawer: CanvasDrawer;
+ private analyser: AnalyserNode;
+ private canvas: React.RefObject;
+ private drawer: CanvasDrawer;
- constructor(props: VisualizerProps) {
- super(props);
- this.canvas = React.createRef();
- }
-
- render() {
- return (
-
- );
- }
-
- componentDidMount() {
- if (this.canvas.current === null) {
- throw Error("Failed to create canvas; aborting");
+ constructor(props: VisualizerProps) {
+ super(props);
+ this.canvas = React.createRef();
}
- this.analyser = this.props.audioContext.createAnalyser();
- this.analyser.fftSize = 2048;
- this.analyser.smoothingTimeConstant = 0.8;
- this.props.audioSource.connect(this.analyser);
+ render() {
+ return (
+
+ );
+ }
- this.drawer = new CanvasDrawer(this.analyser, this.canvas.current);
- }
+ componentDidMount() {
+ if (this.canvas.current === null) {
+ throw Error("Failed to create canvas; aborting");
+ }
- componentWillUnmount() {
- this.drawer.stop();
- this.props.audioSource.disconnect(this.analyser);
- }
+ this.analyser = this.props.audioContext.createAnalyser();
+ this.analyser.fftSize = 2048;
+ this.analyser.smoothingTimeConstant = 0.8;
+ this.props.audioSource.connect(this.analyser);
+
+ this.drawer = new CanvasDrawer(this.analyser, this.canvas.current);
+ }
+
+ componentWillUnmount() {
+ this.drawer.stop();
+ this.props.audioSource.disconnect(this.analyser);
+ }
}
export default Visualizer;
diff --git a/src/music/index.tsx b/src/music/index.tsx
index a5c2917..2a053cf 100644
--- a/src/music/index.tsx
+++ b/src/music/index.tsx
@@ -5,23 +5,24 @@ import { Provider } from "react-redux";
import { store } from "./store";
import MusicPlayer from "./MusicPlayer";
import { setSource, setTitle } from "./store/music/types";
+// @ts-ignore Can't find module - this is an mp3 file, so...
import mseq from "./Mseq_-_Journey.mp3";
const rootElement = document.getElementById("playerUI");
ReactDOM.render(
-
-
- ,
- rootElement
+
+
+ ,
+ rootElement
);
store.dispatch(setSource(mseq));
store.dispatch(
- setTitle({
- name: "Journey",
- artist: "Mseq",
- album: "Unknown album",
- length: 192052244,
- })
+ setTitle({
+ name: "Journey",
+ artist: "Mseq",
+ album: "Unknown album",
+ length: 192052244,
+ })
);
diff --git a/src/music/store/index.ts b/src/music/store/index.ts
index f523735..a994939 100644
--- a/src/music/store/index.ts
+++ b/src/music/store/index.ts
@@ -4,14 +4,17 @@ import { MusicState } from "./music/types";
import { musicStateReducer } from "./music/reducers";
export interface State {
- musicState: MusicState;
+ musicState: MusicState;
}
const rootReducer = combineReducers({
- musicState: musicStateReducer,
+ musicState: musicStateReducer,
});
export const store = createStore(
- rootReducer,
- window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
+ rootReducer,
+ // @ts-ignore Missing property - this will only exist if the devtools extension is installed, and is actually what we're checking for
+ window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
+
+export type Dispatch = typeof store.dispatch;
diff --git a/src/music/store/music/reducers.ts b/src/music/store/music/reducers.ts
index 83883d1..f351d60 100644
--- a/src/music/store/music/reducers.ts
+++ b/src/music/store/music/reducers.ts
@@ -2,44 +2,44 @@ import { createReducer } from "redux-act";
import update from "immutability-helper";
import {
- Title,
- MusicState,
- setTitle,
- toggleMute,
- togglePlay,
- setSource,
+ Title,
+ MusicState,
+ setTitle,
+ toggleMute,
+ togglePlay,
+ setSource,
} from "./types";
const defaultTitle: Title = {
- name: "Untitled",
- artist: "Unknown Artist",
- album: "Unknown Album",
- length: 0,
+ name: "Untitled",
+ artist: "Unknown Artist",
+ album: "Unknown Album",
+ length: 0,
};
const initialState: MusicState = {
- muted: false,
- playing: false,
- title: defaultTitle,
- playTime: 0,
+ muted: false,
+ playing: false,
+ title: defaultTitle,
+ playTime: 0,
};
export const musicStateReducer = createReducer(
- {
- [setTitle]: (state: MusicState, title: Title): MusicState => {
- return update(state, {
- title: { $set: title },
- });
+ {
+ [setTitle]: (state: MusicState, title: Title): MusicState => {
+ return update(state, {
+ title: { $set: title },
+ });
+ },
+ [togglePlay]: (state: MusicState): MusicState => {
+ return update(state, { $toggle: ["playing"] });
+ },
+ [toggleMute]: (state: MusicState): MusicState => {
+ return update(state, { $toggle: ["muted"] });
+ },
+ [setSource]: (state: MusicState, source: string): MusicState => {
+ return update(state, { source: { $set: source } });
+ },
},
- [togglePlay]: (state: MusicState): MusicState => {
- return update(state, { $toggle: ["playing"] });
- },
- [toggleMute]: (state: MusicState): MusicState => {
- return update(state, { $toggle: ["muted"] });
- },
- [setSource]: (state: MusicState, source: string): MusicState => {
- return update(state, { source: { $set: source } });
- },
- },
- initialState
+ initialState
);
diff --git a/src/music/store/music/types.ts b/src/music/store/music/types.ts
index 39555fc..ca05cbd 100644
--- a/src/music/store/music/types.ts
+++ b/src/music/store/music/types.ts
@@ -1,29 +1,29 @@
import { Action, createAction } from "redux-act";
export interface Title {
- name: string;
- artist: string;
- album: string;
- /**
- * The length of the title in nanoseconds.
- */
- length: number;
+ name: string;
+ artist: string;
+ album: string;
+ /**
+ * The length of the title in nanoseconds.
+ */
+ length: number;
}
export interface MusicState {
- muted: boolean;
- playing: boolean;
- title: Title;
- playTime: number;
- source?: string;
+ muted: boolean;
+ playing: boolean;
+ title: Title;
+ playTime: number;
+ source?: string;
}
export const setTitle: (title: Title) => Action = createAction(
- "set currently playing title"
+ "set currently playing title"
);
export const setPlayTime: (time: number) => Action = createAction(
- "set the play time"
+ "set the play time"
);
export const toggleMute: () => Action = createAction("toggle mute");
@@ -31,5 +31,5 @@ export const toggleMute: () => Action = createAction("toggle mute");
export const togglePlay: () => Action = createAction("toggle play");
export const setSource: (source: string) => Action = createAction(
- "set the title"
+ "set the title"
);