Rework website with bulma instead of bootstrap #6
|
@ -2,15 +2,11 @@ import React from "react";
|
||||||
|
|
||||||
import Controls from "../controls/Controls";
|
import Controls from "../controls/Controls";
|
||||||
import Visualizer from "../visualizer/Visualizer";
|
import Visualizer from "../visualizer/Visualizer";
|
||||||
import { musicPlayer } from "./musicPlayerSlice";
|
|
||||||
|
|
||||||
function MusicPlayer() {
|
function MusicPlayer() {
|
||||||
return (
|
return (
|
||||||
<div className="is-flex-grow-1 is-flex is-flex-direction-column">
|
<div className="is-flex-grow-1 is-flex is-flex-direction-column">
|
||||||
<Visualizer
|
<Visualizer />
|
||||||
audioContext={musicPlayer.audioContext}
|
|
||||||
audioNode={musicPlayer.audioNode}
|
|
||||||
/>
|
|
||||||
<div className="is-flex-grow-0">
|
<div className="is-flex-grow-0">
|
||||||
<Controls />
|
<Controls />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -30,27 +30,18 @@ enum PlayState {
|
||||||
//*********************
|
//*********************
|
||||||
|
|
||||||
class MusicPlayer {
|
class MusicPlayer {
|
||||||
private context: AudioContext;
|
private context?: AudioContext;
|
||||||
private source: HTMLAudioElement;
|
private source: HTMLAudioElement;
|
||||||
private sourceNode: MediaElementAudioSourceNode;
|
private sourceNode?: MediaElementAudioSourceNode;
|
||||||
private volume: GainNode;
|
private volume?: GainNode;
|
||||||
|
private analyser?: AnalyserNode;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.context = new AudioContext();
|
|
||||||
this.source = new Audio();
|
this.source = new Audio();
|
||||||
this.sourceNode = this.context.createMediaElementSource(this.source);
|
|
||||||
this.volume = this.context.createGain();
|
|
||||||
|
|
||||||
this.sourceNode.connect(this.volume);
|
|
||||||
this.volume.connect(this.context.destination);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get audioContext() {
|
get audioAnalyser() {
|
||||||
return this.context;
|
return this.analyser;
|
||||||
}
|
|
||||||
|
|
||||||
get audioNode() {
|
|
||||||
return this.sourceNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set src(source: string) {
|
set src(source: string) {
|
||||||
|
@ -61,6 +52,22 @@ class MusicPlayer {
|
||||||
_: null,
|
_: null,
|
||||||
{ getState }: { getState: () => RootState }
|
{ getState }: { getState: () => RootState }
|
||||||
): Promise<PlayState> => {
|
): Promise<PlayState> => {
|
||||||
|
if (this.context === undefined) {
|
||||||
|
this.context = new AudioContext();
|
||||||
|
this.sourceNode = this.context.createMediaElementSource(
|
||||||
|
this.source
|
||||||
|
);
|
||||||
|
this.volume = this.context.createGain();
|
||||||
|
this.analyser = this.context.createAnalyser();
|
||||||
|
|
||||||
|
this.analyser.fftSize = 2048;
|
||||||
|
this.analyser.smoothingTimeConstant = 0.8;
|
||||||
|
|
||||||
|
this.sourceNode.connect(this.analyser);
|
||||||
|
this.sourceNode.connect(this.volume);
|
||||||
|
this.volume.connect(this.context.destination);
|
||||||
|
}
|
||||||
|
|
||||||
const playing = getState().musicPlayer.playing;
|
const playing = getState().musicPlayer.playing;
|
||||||
|
|
||||||
switch (playing) {
|
switch (playing) {
|
||||||
|
|
|
@ -33,16 +33,10 @@ class Renderer {
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
context: AudioContext,
|
analyser: AnalyserNode,
|
||||||
node: AudioNode,
|
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
overlay: HTMLSpanElement
|
overlay: HTMLSpanElement
|
||||||
) {
|
) {
|
||||||
const analyser = context.createAnalyser();
|
|
||||||
analyser.fftSize = 2048;
|
|
||||||
analyser.smoothingTimeConstant = 0.8;
|
|
||||||
node.connect(analyser);
|
|
||||||
|
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
this.overlay = overlay;
|
this.overlay = overlay;
|
||||||
this.analyser = analyser;
|
this.analyser = analyser;
|
||||||
|
|
|
@ -2,17 +2,41 @@ import React, { useCallback, useState } from "react";
|
||||||
import { Renderer, RendererError } from "./Renderer";
|
import { Renderer, RendererError } from "./Renderer";
|
||||||
import { ShaderError } from "./Shader";
|
import { ShaderError } from "./Shader";
|
||||||
|
|
||||||
function Visualizer({
|
import { useAppSelector } from "../../hooks";
|
||||||
audioContext,
|
import { PlayState, musicPlayer } from "../musicplayer/musicPlayerSlice";
|
||||||
audioNode,
|
|
||||||
}: {
|
function Visualizer() {
|
||||||
audioContext: AudioContext;
|
const playing = useAppSelector((state) => state.musicPlayer.playing);
|
||||||
audioNode: AudioNode;
|
const rendererState = useState<Renderer | null>(null);
|
||||||
}) {
|
let renderer = rendererState[0];
|
||||||
|
const setRenderer = rendererState[1];
|
||||||
const [renderError, setRenderError] = useState<JSX.Element | null>(null);
|
const [renderError, setRenderError] = useState<JSX.Element | null>(null);
|
||||||
|
|
||||||
const visualizer = useCallback(
|
const visualizer = useCallback(
|
||||||
(visualizer: HTMLDivElement | null) => {
|
(visualizer: HTMLDivElement | null) => {
|
||||||
|
// TODO(tlater): Clean up state management. This is all
|
||||||
|
// but trivial; there's seemingly no good place to keep
|
||||||
|
// these big api objects (WebGLRenderingcontext or
|
||||||
|
// AudioContext).
|
||||||
|
//
|
||||||
|
// It's tricky, too, because obviously react expects to be
|
||||||
|
// in control of the DOM, and be allowed to delete our
|
||||||
|
// canvas and create a new one.
|
||||||
|
//
|
||||||
|
// For the moment, this works, but it's a definite hack.
|
||||||
|
if (renderer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Until we start playing music, there is nothing to render.
|
||||||
|
if (playing !== PlayState.Playing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (musicPlayer.audioAnalyser === undefined) {
|
||||||
|
throw new Error("MusicPlayer analyser was not set up on time");
|
||||||
|
}
|
||||||
|
|
||||||
// If we're rendering an error message, we won't be
|
// If we're rendering an error message, we won't be
|
||||||
// setting up the visualizer.
|
// setting up the visualizer.
|
||||||
//
|
//
|
||||||
|
@ -35,12 +59,14 @@ function Visualizer({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderer = new Renderer(
|
if (renderer === null) {
|
||||||
audioContext,
|
renderer = new Renderer(
|
||||||
audioNode,
|
musicPlayer.audioAnalyser,
|
||||||
canvas,
|
canvas,
|
||||||
overlay
|
overlay
|
||||||
);
|
);
|
||||||
|
setRenderer(renderer);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
renderer.initializeScene();
|
renderer.initializeScene();
|
||||||
|
@ -92,7 +118,7 @@ function Visualizer({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[audioContext, audioNode]
|
[playing, renderer, musicPlayer.audioAnalyser]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (renderError === null) {
|
if (renderError === null) {
|
||||||
|
|
Reference in a new issue