diff --git a/packages/webamp/js/components/FFTNullsoft.ts b/packages/webamp/js/components/FFTNullsoft.ts
new file mode 100644
index 0000000000..31ffdcc3dd
--- /dev/null
+++ b/packages/webamp/js/components/FFTNullsoft.ts
@@ -0,0 +1,203 @@
+// The Web Audio API's FFT is bad, so this exists now!
+// Taken from https://github.com/WACUP/vis_classic/tree/master/FFTNullsoft
+
+export class FFT {
+ private bitrevtable: number[];
+ private envelope: Float32Array;
+ private equalize: Float32Array;
+ private temp1: Float32Array;
+ private temp2: Float32Array;
+ private cossintable: Float32Array[];
+
+ // Constants
+ private static readonly TWO_PI = 6.2831853; // 2 * Math.PI
+ private static readonly HALF_PI = 1.5707963268; // Math.PI / 2
+
+ constructor() {
+ // Assuming these are your hardcoded values:
+ const samplesIn = 1024; // hardcoded value
+ const samplesOut = 512; // hardcoded value
+ const bEqualize = true; // hardcoded value
+ const envelopePower = 1.0; // hardcoded value
+ const mode = false; // hardcoded value
+
+ const NFREQ = samplesOut * 2;
+
+ // Initialize the tables and arrays with hardcoded values
+ this.bitrevtable = this.initBitRevTable(NFREQ);
+ this.cossintable = this.initCosSinTable(NFREQ);
+
+ this.envelope = this.initEnvelopeTable(samplesIn, envelopePower);
+ this.equalize = this.initEqualizeTable(NFREQ, mode);
+
+ this.temp1 = new Float32Array(NFREQ);
+ this.temp2 = new Float32Array(NFREQ);
+ }
+
+ private initEqualizeTable(NFREQ: number, mode: boolean): Float32Array {
+ const equalize = new Float32Array(NFREQ / 2);
+ let bias = 0.04; // FFT.INITIAL_BIAS
+
+ for (let i = 0; i < NFREQ / 2; i++) {
+ const inv_half_nfreq = (9.0 - bias) / (NFREQ / 2);
+ equalize[i] = Math.log10(1.0 + bias + (i + 1) * inv_half_nfreq);
+ bias /= 1.0025; // FFT.BIAS_DECAY_RATE
+ }
+
+ return equalize;
+ }
+
+ private initEnvelopeTable(samplesIn: number, power: number): Float32Array {
+ const mult = (1.0 / samplesIn) * FFT.TWO_PI;
+ const envelope = new Float32Array(samplesIn);
+
+ for (let i = 0; i < samplesIn; i++) {
+ envelope[i] = Math.pow(
+ 0.5 + 0.5 * Math.sin(i * mult - FFT.HALF_PI),
+ power
+ );
+ }
+
+ return envelope;
+ }
+
+ private initBitRevTable(NFREQ: number): number[] {
+ const bitrevtable = new Array(NFREQ);
+
+ for (let i = 0; i < NFREQ; i++) {
+ bitrevtable[i] = i;
+ }
+
+ for (let i = 0, j = 0; i < NFREQ; i++) {
+ if (j > i) {
+ const temp = bitrevtable[i];
+ bitrevtable[i] = bitrevtable[j];
+ bitrevtable[j] = temp;
+ }
+
+ let m = NFREQ >> 1;
+ while (m >= 1 && j >= m) {
+ j -= m;
+ m >>= 1;
+ }
+
+ j += m;
+ }
+
+ return bitrevtable;
+ }
+
+ private initCosSinTable(NFREQ: number): Float32Array[] {
+ const cossintable: Float32Array[] = [];
+ let dftsize = 2;
+
+ while (dftsize <= NFREQ) {
+ const theta = (-2.0 * Math.PI) / dftsize;
+ cossintable.push(new Float32Array([Math.cos(theta), Math.sin(theta)]));
+ dftsize <<= 1;
+ }
+
+ return cossintable;
+ }
+
+ public timeToFrequencyDomain(
+ inWavedata: Float32Array,
+ outSpectraldata: Float32Array
+ ): void {
+ if (!this.temp1 || !this.temp2 || !this.cossintable) return;
+ // Converts time-domain samples from inWavedata[]
+ // into frequency-domain samples in outSpectraldata[].
+ // The array lengths are the two parameters to Init().
+
+ // The last sample of the output data will represent the frequency
+ // that is 1/4th of the input sampling rate. For example,
+ // if the input wave data is sampled at 44,100 Hz, then the last
+ // sample of the spectral data output will represent the frequency
+ // 11,025 Hz. The first sample will be 0 Hz; the frequencies of
+ // the rest of the samples vary linearly in between.
+ // Note that since human hearing is limited to the range 200 - 20,000
+ // Hz. 200 is a low bass hum; 20,000 is an ear-piercing high shriek.
+ // Each time the frequency doubles, that sounds like going up an octave.
+ // That means that the difference between 200 and 300 Hz is FAR more
+ // than the difference between 5000 and 5100, for example!
+ // So, when trying to analyze bass, you'll want to look at (probably)
+ // the 200-800 Hz range; whereas for treble, you'll want the 1,400 -
+ // 11,025 Hz range.
+ // If you want to get 3 bands, try it this way:
+ // a) 11,025 / 200 = 55.125
+ // b) to get the number of octaves between 200 and 11,025 Hz, solve for n:
+ // 2^n = 55.125
+ // n = log 55.125 / log 2
+ // n = 5.785
+ // c) so each band should represent 5.785/3 = 1.928 octaves; the ranges are:
+ // 1) 200 - 200*2^1.928 or 200 - 761 Hz
+ // 2) 200*2^1.928 - 200*2^(1.928*2) or 761 - 2897 Hz
+ // 3) 200*2^(1.928*2) - 200*2^(1.928*3) or 2897 - 11025 Hz
+
+ // A simple sine-wave-based envelope is convolved with the waveform
+ // data before doing the FFT, to emeliorate the bad frequency response
+ // of a square (i.e. nonexistent) filter.
+
+ // You might want to slightly damp (blur) the input if your signal isn't
+ // of a very high quality, to reduce high-frequency noise that would
+ // otherwise show up in the output.
+
+ // code should be smart enough to call Init before this function
+ //if (!bitrevtable) return;
+ //if (!temp1) return;
+ //if (!temp2) return;
+ //if (!cossintable) return;
+
+ // 1. set up input to the fft
+ for (let i = 0; i < this.temp1.length; i++) {
+ const idx = this.bitrevtable[i];
+ if (idx < inWavedata.length) {
+ this.temp1[i] =
+ inWavedata[idx] * (this.envelope ? this.envelope[idx] : 1);
+ } else {
+ this.temp1[i] = 0;
+ }
+ }
+ this.temp2.fill(0);
+
+ // 2. Perform FFT
+ let real = this.temp1;
+ let imag = this.temp2;
+ let dftsize = 2;
+ let t = 0;
+
+ while (dftsize <= this.temp1.length) {
+ const wpr = this.cossintable[t][0];
+ const wpi = this.cossintable[t][1];
+ let wr = 1.0;
+ let wi = 0.0;
+ const hdftsize = dftsize >> 1;
+
+ for (let m = 0; m < hdftsize; m += 1) {
+ for (let i = m; i < this.temp1.length; i += dftsize) {
+ const j = i + hdftsize;
+ const tempr = wr * real[j] - wi * imag[j];
+ const tempi = wr * imag[j] + wi * real[j];
+ real[j] = real[i] - tempr;
+ imag[j] = imag[i] - tempi;
+ real[i] += tempr;
+ imag[i] += tempi;
+ }
+
+ const wtemp = wr;
+ wr = wr * wpr - wi * wpi;
+ wi = wi * wpr + wtemp * wpi;
+ }
+
+ dftsize <<= 1;
+ ++t;
+ }
+
+ // 3. take the magnitude & equalize it (on a log10 scale) for output
+ for (let i = 0; i < outSpectraldata.length; i++) {
+ outSpectraldata[i] =
+ Math.sqrt(real[i] * real[i] + imag[i] * imag[i]) *
+ (this.equalize ? this.equalize[i] : 1);
+ }
+ }
+}
diff --git a/packages/webamp/js/components/MainWindow/index.tsx b/packages/webamp/js/components/MainWindow/index.tsx
index 7324b3539b..0a401725e0 100644
--- a/packages/webamp/js/components/MainWindow/index.tsx
+++ b/packages/webamp/js/components/MainWindow/index.tsx
@@ -8,7 +8,7 @@ import MiniTime from "../MiniTime";
import ClickedDiv from "../ClickedDiv";
import ContextMenuTarget from "../ContextMenuTarget";
-import Visualizer from "../Visualizer";
+import Vis from "../Vis";
import ActionButtons from "./ActionButtons";
import MainBalance from "./MainBalance";
import Close from "./Close";
@@ -106,10 +106,7 @@ const MainWindow = React.memo(({ analyser, filePickers }: Props) => {
/>
-
+
diff --git a/packages/webamp/js/components/PlaylistWindow/index.tsx b/packages/webamp/js/components/PlaylistWindow/index.tsx
index 3f51567436..a9fb43ed99 100644
--- a/packages/webamp/js/components/PlaylistWindow/index.tsx
+++ b/packages/webamp/js/components/PlaylistWindow/index.tsx
@@ -7,7 +7,7 @@ import * as Selectors from "../../selectors";
import { clamp } from "../../utils";
import DropTarget from "../DropTarget";
-import Visualizer from "../Visualizer";
+import Vis from "../Vis";
import PlaylistShade from "./PlaylistShade";
import AddMenu from "./AddMenu";
import RemoveMenu from "./RemoveMenu";
@@ -140,10 +140,7 @@ function PlaylistWindow({ analyser }: Props) {
{activateVisualizer && (
-
+
)}
diff --git a/packages/webamp/js/components/Vis.tsx b/packages/webamp/js/components/Vis.tsx
new file mode 100644
index 0000000000..731b1ac5e3
--- /dev/null
+++ b/packages/webamp/js/components/Vis.tsx
@@ -0,0 +1,200 @@
+import React, { useMemo, useState, useLayoutEffect, useEffect } from "react";
+
+import * as Actions from "../actionCreators";
+import * as Selectors from "../selectors";
+import { useTypedSelector, useActionCreator } from "../hooks";
+import { VISUALIZERS, MEDIA_STATUS } from "../constants";
+
+import {
+ Vis as IVis,
+ BarPaintHandler,
+ WavePaintHandler,
+ NoVisualizerHandler,
+} from "./VisPainter";
+
+type Props = {
+ analyser: AnalyserNode;
+};
+
+// Pre-render the background grid
+function preRenderBg(
+ width: number,
+ height: number,
+ bgColor: string,
+ fgColor: string,
+ windowShade: boolean,
+ pixelDensity: number
+): HTMLCanvasElement {
+ // Off-screen canvas for pre-rendering the background
+ const bgCanvas = document.createElement("canvas");
+ bgCanvas.width = width;
+ bgCanvas.height = height;
+ const distance = 2 * pixelDensity;
+
+ const bgCanvasCtx = bgCanvas.getContext("2d");
+ if (bgCanvasCtx == null) {
+ throw new Error("Could not construct canvas context");
+ }
+ bgCanvasCtx.fillStyle = bgColor;
+ bgCanvasCtx.fillRect(0, 0, width, height);
+ if (!windowShade) {
+ bgCanvasCtx.fillStyle = fgColor;
+ for (let x = 0; x < width; x += distance) {
+ for (let y = pixelDensity; y < height; y += distance) {
+ bgCanvasCtx.fillRect(x, y, pixelDensity, pixelDensity);
+ }
+ }
+ }
+ return bgCanvas;
+}
+
+export default function Vis({ analyser }: Props) {
+ useLayoutEffect(() => {
+ analyser.fftSize = 1024;
+ }, [analyser, analyser.fftSize]);
+
+ const colors = useTypedSelector(Selectors.getSkinColors);
+ const mode = useTypedSelector(Selectors.getVisualizerStyle);
+ const audioStatus = useTypedSelector(Selectors.getMediaStatus);
+ const getWindowShade = useTypedSelector(Selectors.getWindowShade);
+ const getWindowOpen = useTypedSelector(Selectors.getWindowOpen);
+ const isMWOpen = getWindowOpen("main");
+ const doubled = useTypedSelector(Selectors.getDoubled);
+ const toggleVisualizerStyle = useActionCreator(Actions.toggleVisualizerStyle);
+ const windowShade = getWindowShade("main");
+
+ const smallVis = windowShade && isMWOpen;
+ const renderHeight = smallVis ? 5 : 16;
+ const renderWidth = 76;
+ const pixelDensity = doubled && smallVis ? 2 : 1;
+ const renderWidthBG = !isMWOpen
+ ? renderWidth
+ : windowShade
+ ? doubled
+ ? renderWidth
+ : 38
+ : renderWidth * pixelDensity;
+
+ const width = renderWidth * pixelDensity;
+ const height = renderHeight * pixelDensity;
+
+ const bgCanvas = useMemo(() => {
+ return preRenderBg(
+ renderWidthBG,
+ height,
+ colors[0],
+ colors[1],
+ Boolean(windowShade),
+ pixelDensity
+ );
+ }, [colors, height, renderWidthBG, windowShade, pixelDensity]);
+
+ const [canvas, setCanvas] = useState(null);
+
+ //? painter administration
+ const painter = useMemo(() => {
+ if (!canvas) return null;
+
+ const vis: IVis = {
+ canvas,
+ colors,
+ analyser,
+ oscStyle: "lines",
+ bandwidth: "wide",
+ coloring: "normal",
+ peaks: true,
+ saFalloff: "moderate",
+ saPeakFalloff: "slow",
+ sa: "analyzer", // unused, but hopefully will be used in the future for providing config options
+ renderHeight,
+ smallVis,
+ pixelDensity,
+ doubled,
+ isMWOpen,
+ };
+
+ switch (mode) {
+ case VISUALIZERS.OSCILLOSCOPE:
+ return new WavePaintHandler(vis);
+ case VISUALIZERS.BAR:
+ return new BarPaintHandler(vis);
+ case VISUALIZERS.NONE:
+ return new NoVisualizerHandler(vis);
+ default:
+ return new NoVisualizerHandler(vis);
+ }
+ }, [
+ analyser,
+ canvas,
+ mode,
+ colors,
+ renderHeight,
+ smallVis,
+ pixelDensity,
+ doubled,
+ isMWOpen,
+ ]);
+
+ // reacts to changes in doublesize mode
+ useEffect(() => {
+ if (canvas && painter) {
+ const canvasCtx = canvas.getContext("2d");
+ if (canvasCtx) {
+ painter.prepare();
+ // wipes the canvas clean if playback is paused and doubled is changing
+ if (audioStatus === MEDIA_STATUS.PAUSED) {
+ canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
+ }
+ }
+ }
+ }, [doubled, canvas, painter]);
+
+ useEffect(() => {
+ if (canvas == null || painter == null) {
+ return;
+ }
+
+ const canvasCtx = canvas.getContext("2d");
+ if (canvasCtx == null) {
+ return;
+ }
+ canvasCtx.imageSmoothingEnabled = false;
+
+ let animationRequest: number | null = null;
+
+ const loop = () => {
+ canvasCtx.drawImage(bgCanvas, 0, 0);
+ painter.paintFrame();
+ animationRequest = window.requestAnimationFrame(loop);
+ };
+
+ if (audioStatus === MEDIA_STATUS.PLAYING) {
+ if (mode === VISUALIZERS.NONE) {
+ canvasCtx.clearRect(0, 0, renderWidthBG, height);
+ } else {
+ loop();
+ }
+ }
+
+ return () => {
+ if (animationRequest !== null) {
+ window.cancelAnimationFrame(animationRequest);
+ }
+ };
+ }, [audioStatus, canvas, painter, bgCanvas, renderWidthBG, height, mode]);
+
+ if (audioStatus === MEDIA_STATUS.STOPPED) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/packages/webamp/js/components/VisPainter.ts b/packages/webamp/js/components/VisPainter.ts
new file mode 100644
index 0000000000..8530fe1d7e
--- /dev/null
+++ b/packages/webamp/js/components/VisPainter.ts
@@ -0,0 +1,787 @@
+export interface Vis {
+ canvas: HTMLCanvasElement;
+ colors: string[];
+ analyser?: AnalyserNode;
+ oscStyle?: "dots" | "solid" | "lines";
+ bandwidth?: "wide" | "thin";
+ coloring?: "fire" | "line" | "normal";
+ peaks?: boolean;
+ saFalloff?: "slower" | "slow" | "moderate" | "fast" | "faster";
+ saPeakFalloff?: "slower" | "slow" | "moderate" | "fast" | "faster";
+ sa?: "analyzer" | "oscilloscope" | "none";
+ renderHeight?: number;
+ smallVis?: boolean;
+ pixelDensity?: number;
+ doubled?: boolean;
+ isMWOpen?: boolean;
+}
+import { FFT } from "./FFTNullsoft";
+
+/**
+ * Base class of Visualizer (animation frame renderer engine)
+ */
+abstract class VisPaintHandler {
+ _vis: Vis;
+ _ctx: CanvasRenderingContext2D | null;
+
+ constructor(vis: Vis) {
+ this._vis = vis;
+ this._ctx = vis.canvas!.getContext("2d");
+ }
+
+ /**
+ * Attempt to build cached bitmaps for later use while rendering a frame.
+ * Purpose: fast rendering in animation loop
+ */
+ prepare() {}
+
+ /**
+ * Called once per frame rendering
+ */
+ paintFrame() {}
+
+ /**
+ * Attempt to cleanup cached bitmaps
+ */
+ dispose() {}
+}
+
+/**
+ * Feeds audio data to the FFT.
+ * @param analyser The AnalyserNode used to get the audio data.
+ * @param fft The FFTNullsoft instance from the PaintHandler.
+ */
+function processFFT(
+ analyser: AnalyserNode,
+ fft: FFT,
+ inWaveData: Float32Array,
+ outSpectralData: Float32Array
+): void {
+ const dataArray = new Uint8Array(1024);
+
+ analyser.getByteTimeDomainData(dataArray);
+ for (let i = 0; i < dataArray.length; i++) {
+ inWaveData[i] = (dataArray[i] - 128) / 24;
+ }
+ fft.timeToFrequencyDomain(inWaveData, outSpectralData);
+}
+
+//? =============================== BAR PAINTER ===============================
+type PaintFrameFunction = () => void;
+type PaintBarFunction = (
+ ctx: CanvasRenderingContext2D,
+ x1: number,
+ x2: number,
+ barHeight: number,
+ peakHeight: number
+) => void;
+
+export class BarPaintHandler extends VisPaintHandler {
+ private saPeaks: Int16Array;
+ private saData2: Float32Array;
+ private saData: Int16Array;
+ private saFalloff: Float32Array;
+ private sample: Float32Array;
+ private barPeak: Int16Array;
+ private chunk: number;
+ private uVar12: number;
+ private falloff: number;
+ private peakFalloff: number;
+ private pushDown: number;
+
+ private inWaveData = new Float32Array(1024);
+ private outSpectralData = new Float32Array(512);
+
+ _analyser: AnalyserNode;
+ _fft: FFT;
+ _color: string = "rgb(255,255,255)";
+ _colorPeak: string = "rgb(255,255,255)";
+ // Off-screen canvas for pre-rendering a single bar gradient
+ _bar: HTMLCanvasElement = document.createElement("canvas");
+ _peak: HTMLCanvasElement = document.createElement("canvas");
+ _16h: HTMLCanvasElement = document.createElement("canvas"); // non-stretched
+ _bufferLength: number;
+ _dataArray: Uint8Array;
+ colorssmall: string[];
+ colorssmall2: string[];
+ _renderHeight: number;
+ _smallVis: boolean;
+ _pixelDensity: number;
+ _doubled: boolean;
+ _isMWOpen: boolean;
+ paintBar: PaintBarFunction;
+ paintFrame: PaintFrameFunction;
+
+ constructor(vis: Vis) {
+ super(vis);
+ this._analyser = this._vis.analyser!;
+ this._fft = new FFT();
+ this._bufferLength = this._analyser.frequencyBinCount;
+ this._dataArray = new Uint8Array(this._bufferLength);
+
+ this._renderHeight = vis.renderHeight!;
+ this._smallVis = vis.smallVis!;
+ this._pixelDensity = vis.pixelDensity!;
+ this._doubled = vis.doubled!;
+ this._isMWOpen = vis.isMWOpen!;
+
+ this.colorssmall = [
+ vis.colors[17],
+ vis.colors[14],
+ vis.colors[11],
+ vis.colors[8],
+ vis.colors[4],
+ ];
+ this.colorssmall2 = [
+ vis.colors[17],
+ vis.colors[16],
+ vis.colors[14],
+ vis.colors[13],
+ vis.colors[11],
+ vis.colors[10],
+ vis.colors[8],
+ vis.colors[7],
+ vis.colors[5],
+ vis.colors[4],
+ ];
+
+ this._16h.width = 1;
+ this._16h.height = 16;
+ this._16h.setAttribute("width", "75");
+ this._16h.setAttribute("height", "16");
+
+ // draws the analyzer and handles changing the bandwidth correctly
+ this.paintFrame = this.paintAnalyzer.bind(this);
+
+ this.saPeaks = new Int16Array(76).fill(0);
+ this.saData2 = new Float32Array(76).fill(0);
+ this.saData = new Int16Array(76).fill(0);
+ this.saFalloff = new Float32Array(76).fill(0);
+ this.sample = new Float32Array(76).fill(0);
+ this.barPeak = new Int16Array(76).fill(0); // Needs to be specified as Int16 else the peaks don't behave as they should
+ this.chunk = 0;
+ this.uVar12 = 0;
+ this.pushDown = 0;
+
+ this.inWaveData;
+ this.outSpectralData;
+
+ switch (this._vis.coloring) {
+ case "fire":
+ this.paintBar = this.paintBarFire.bind(this);
+ break;
+ case "line":
+ this.paintBar = this.paintBarLine.bind(this);
+ break;
+ default:
+ this.paintBar = this.paintBarNormal.bind(this);
+ break;
+ }
+
+ switch (this._vis.saFalloff) {
+ case "slower":
+ this.falloff = 3;
+ break;
+ case "slow":
+ this.falloff = 6;
+ break;
+ case "moderate":
+ this.falloff = 12;
+ break;
+ case "fast":
+ this.falloff = 16;
+ break;
+ case "faster":
+ this.falloff = 32;
+ break;
+ default:
+ this.falloff = 12;
+ break;
+ }
+
+ switch (this._vis.saPeakFalloff) {
+ case "slower":
+ this.peakFalloff = 1.05;
+ break;
+ case "slow":
+ this.peakFalloff = 1.1;
+ break;
+ case "moderate":
+ this.peakFalloff = 1.2;
+ break;
+ case "fast":
+ this.peakFalloff = 1.4;
+ break;
+ case "faster":
+ this.peakFalloff = 1.6;
+ break;
+ default:
+ this.peakFalloff = 1.1;
+ break;
+ }
+ }
+
+ prepare() {
+ const vis = this._vis;
+
+ //? paint peak
+ this._peak.height = 1;
+ this._peak.width = 1;
+ let ctx = this._peak.getContext("2d")!;
+ ctx.fillStyle = vis.colors[23];
+ ctx.fillRect(0, 0, 1, 1);
+
+ if (this._vis.smallVis) {
+ this.pushDown = 0;
+ } else if (this._vis.doubled && !this._vis.isMWOpen) {
+ this.pushDown = 2;
+ } else if (this._vis.doubled) {
+ this.pushDown = 0;
+ } else {
+ this.pushDown = 2;
+ }
+
+ //? paint bar
+ this._bar.height = 16;
+ this._bar.width = 1;
+ this._bar.setAttribute("width", "1");
+ this._bar.setAttribute("height", "16");
+ ctx = this._bar.getContext("2d")!;
+ for (let y = 0; y < 16; y++) {
+ if (this._vis.pixelDensity === 2 && this._vis.smallVis) {
+ ctx.fillStyle = this.colorssmall2[-y + 9];
+ } else {
+ ctx.fillStyle = this._vis.smallVis
+ ? this.colorssmall[-y + 4]
+ : vis.colors[2 - this.pushDown - -y];
+ }
+ ctx.fillRect(0, y, 1, y + 1);
+ }
+ }
+
+ /**
+ * ⬜⬜⬜ ⬜⬜⬜
+ * 🟧🟧🟧
+ * 🟫🟫🟫 🟧🟧🟧
+ * 🟫🟫🟫 🟫🟫🟫
+ * 🟫🟫🟫 🟫🟫🟫 ⬜⬜⬜
+ * 🟫🟫🟫 🟫🟫🟫 🟧🟧🟧
+ * 🟫🟫🟫 🟫🟫🟫 🟫🟫🟫
+ * 1 bar = multiple pixels
+ */
+ /**
+ * ⬜⬜
+ * 🟧
+ * 🟫🟧
+ * 🟫🟫⬜⬜
+ * 🟫🟫🟧
+ * 🟫🟫🟫🟧⬜
+ * 🟫🟫🟫🟫🟧
+ * drawing 1pixel width bars
+ */
+ paintAnalyzer() {
+ if (!this._ctx) return;
+ const ctx = this._ctx;
+ const w = ctx.canvas.width;
+ const h = ctx.canvas.height;
+ ctx.fillStyle = this._color;
+
+ let maxFreqIndex = 512;
+ let logMaxFreqIndex = Math.log10(maxFreqIndex);
+ let logMinFreqIndex = 0;
+
+ let targetSize: number;
+ let maxHeight: number;
+ let maxWidth: number;
+ if (this._vis.pixelDensity === 2) {
+ targetSize = 75;
+ maxHeight = 10;
+ } else {
+ targetSize = this._vis.smallVis ? 40 : 75;
+ maxHeight = this._vis.smallVis ? 5 : 15;
+ }
+
+ processFFT(
+ this._analyser,
+ this._fft,
+ this.inWaveData,
+ this.outSpectralData
+ );
+
+ if (this._vis.smallVis) {
+ if (this._vis.pixelDensity === 2) {
+ maxWidth = 75; // this is not 37*2, but if this was 74, we'd be missing a pixel
+ // someone here at Nullsoft screwed up...? or thought 74 didn't look good, I don't know.
+ } else {
+ maxWidth = 37;
+ }
+ } else {
+ maxWidth = 75;
+ }
+
+ // This is to roughly emulate the Analyzer in more modern versions of Winamp.
+ // 2.x and early 5.x versions had a completely linear(?) FFT, if so desired the
+ // scale variable can be set to 0.0
+
+ // This factor controls the scaling from linear to logarithmic.
+ // scale = 0.0 -> fully linear scaling
+ // scale = 1.0 -> fully logarithmic scaling
+ let scale = 0.91; // Adjust this value between 0.0 and 1.0
+ for (let x = 0; x < targetSize; x++) {
+ // Linear interpolation between linear and log scaling
+ let linearIndex = (x / (targetSize - 1)) * (maxFreqIndex - 1);
+ let logScaledIndex =
+ logMinFreqIndex +
+ ((logMaxFreqIndex - logMinFreqIndex) * x) / (targetSize - 1);
+ let logIndex = Math.pow(10, logScaledIndex);
+
+ // Interpolating between linear and logarithmic scaling
+ let scaledIndex = (1.0 - scale) * linearIndex + scale * logIndex;
+
+ let index1 = Math.floor(scaledIndex);
+ let index2 = Math.ceil(scaledIndex);
+
+ if (index1 >= maxFreqIndex) {
+ index1 = maxFreqIndex - 1;
+ }
+ if (index2 >= maxFreqIndex) {
+ index2 = maxFreqIndex - 1;
+ }
+
+ if (index1 == index2) {
+ this.sample[x] = this.outSpectralData[index1];
+ } else {
+ let frac2 = scaledIndex - index1;
+ let frac1 = 1.0 - frac2;
+ this.sample[x] =
+ frac1 * this.outSpectralData[index1] +
+ frac2 * this.outSpectralData[index2];
+ }
+ }
+
+ for (let x = 0; x < maxWidth; x++) {
+ // Based on research of looking at Winamp 5.666 and 2.63 executables
+
+ // if our bandwidth is "wide", chunk every 5 instances of the bars,
+ // add them together and display them
+ if (this._vis.bandwidth === "wide") {
+ this.chunk = this.chunk = x & 0xfffffffc;
+ this.uVar12 =
+ (this.sample[this.chunk + 3] +
+ this.sample[this.chunk + 2] +
+ this.sample[this.chunk + 1] +
+ this.sample[this.chunk]) /
+ 4;
+ this.saData[x] = this.uVar12;
+ } else {
+ this.chunk = 0;
+ this.saData[x] = this.sample[x];
+ }
+
+ if (this.saData[x] >= maxHeight) {
+ this.saData[x] = maxHeight;
+ }
+
+ // prevents saPeaks going out of bounds when switching to windowShade mode
+ if (this.saPeaks[x] >= maxHeight * 256) {
+ this.saPeaks[x] = maxHeight * 256;
+ }
+
+ this.saFalloff[x] -= this.falloff / 16.0;
+ // Possible bar fall off values are
+ // 3, 6, 12, 16, 32
+ // Should there ever be some form of config options,
+ // these should be used
+ // 12 is the default of a fresh new Winamp installation
+
+ if (this.saFalloff[x] <= this.saData[x]) {
+ this.saFalloff[x] = this.saData[x];
+ }
+
+ if (this.saPeaks[x] <= Math.round(this.saFalloff[x] * 256)) {
+ this.saPeaks[x] = this.saFalloff[x] * 256;
+ this.saData2[x] = 3.0;
+ }
+
+ this.barPeak[x] = this.saPeaks[x] / 256;
+
+ this.saPeaks[x] -= Math.round(this.saData2[x]);
+ this.saData2[x] *= this.peakFalloff;
+ // Possible peak fall off values are
+ // 1.05f, 1.1f, 1.2f, 1.4f, 1.6f
+ // 1.1f is the default of a fresh new Winamp installation
+ if (this.saPeaks[x] <= 0) {
+ this.saPeaks[x] = 0;
+ }
+
+ if (this._vis.smallVis) {
+ // SORRY NOTHING
+ // ironically enough the peaks do appear at the bottom here
+ } else {
+ if (Math.round(this.barPeak[x]) < 1) {
+ this.barPeak[x] = -3; // Push peaks outside the viewable area, this isn't a Modern Skin!
+ }
+ }
+
+ // skip rendering if x is 4
+ if (!(x === this.chunk + 3 && this._vis.bandwidth === "wide")) {
+ this.paintBar(
+ ctx,
+ x,
+ x,
+ Math.round(this.saFalloff[x]) - this.pushDown,
+ this.barPeak[x] + 1 - this.pushDown
+ );
+ }
+ }
+ }
+
+ /**
+ * 🟥
+ * 🟧🟧
+ * 🟨🟨🟨
+ * 🟩🟩🟩🟩
+ */
+ paintBarNormal(
+ ctx: CanvasRenderingContext2D,
+ x: number,
+ x2: number,
+ barHeight: number,
+ peakHeight: number
+ ) {
+ const h = ctx.canvas.height;
+ const y = h - barHeight;
+
+ ctx.drawImage(this._bar, 0, y, 1, h - y, x, y, x2 - x + 1, h - y);
+
+ if (this._vis.peaks) {
+ const peakY = h - peakHeight;
+ ctx.drawImage(this._peak, 0, 0, 1, 1, x, peakY, x2 - x + 1, 1);
+ }
+ }
+
+ /**
+ * 🟥
+ * 🟧🟥
+ * 🟨🟧🟥
+ * 🟩🟨🟧🟥
+ */
+ paintBarFire(
+ ctx: CanvasRenderingContext2D,
+ x: number,
+ x2: number,
+ barHeight: number,
+ peakHeight: number
+ ) {
+ const h = ctx.canvas.height;
+ let y = h - barHeight;
+
+ ctx.drawImage(
+ this._bar,
+ 0,
+ 0,
+ this._bar.width,
+ h - y,
+ x,
+ y,
+ x2 - x + 1,
+ h - y
+ );
+
+ if (this._vis.peaks) {
+ const peakY = h - peakHeight;
+ ctx.drawImage(this._peak, 0, 0, 1, 1, x, peakY, x2 - x + 1, 1);
+ }
+ }
+
+ /**
+ * 🟥
+ * 🟥🟧
+ * 🟥🟧🟨
+ * 🟥🟧🟨🟩
+ */
+ paintBarLine(
+ ctx: CanvasRenderingContext2D,
+ x: number,
+ x2: number,
+ barHeight: number,
+ peakHeight: number
+ ) {
+ const h = ctx.canvas.height;
+ let y = h - barHeight;
+ // FIXME: Line drawing is currently Fire mode!
+
+ ctx.drawImage(
+ this._bar,
+ 0, // sx
+ 0, // sy
+ this._bar.width, // sw
+ h - y, // sh
+ x,
+ y, // dx,dy
+ x2 - x + 1, //dw
+ h - y //dh
+ );
+
+ if (this._vis.peaks) {
+ const peakY = h - peakHeight;
+ ctx.drawImage(this._peak, 0, 0, 1, 1, x, peakY, x2 - x + 1, 1);
+ }
+ }
+}
+
+//? =============================== OSCILLOSCOPE PAINTER ===============================
+
+type PaintWavFunction = (x: number, y: number) => void;
+
+function slice1st(
+ dataArray: Uint8Array,
+ sliceWidth: number,
+ sliceNumber: number
+): number {
+ const start = sliceWidth * sliceNumber;
+ return dataArray[start];
+}
+
+export class WavePaintHandler extends VisPaintHandler {
+ private pushDown: number;
+
+ _analyser: AnalyserNode;
+ _bufferLength: number;
+ _lastX: number = 0;
+ _lastY: number = 0;
+ _dataArray: Uint8Array;
+ _pixelRatio: number; // 1 or 2
+ // Off-screen canvas for drawing perfect pixel (no blurred lines)
+ _bar: HTMLCanvasElement = document.createElement("canvas");
+ _16h: HTMLCanvasElement = document.createElement("canvas"); // non-stretched
+ paintWav: PaintWavFunction;
+
+ constructor(vis: Vis) {
+ super(vis);
+ this._analyser = this._vis.analyser!;
+ this._bufferLength = this._analyser.fftSize;
+ this._dataArray = new Uint8Array(this._bufferLength);
+
+ this._16h.width = 1;
+ this._16h.height = 16;
+ this._16h.setAttribute("width", "75");
+ this._16h.setAttribute("height", "16");
+
+ //* see https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#monitoring_screen_resolution_or_zoom_level_changes
+ this._pixelRatio = window.devicePixelRatio || 1;
+
+ // draws the oscilloscope and handles overly complex operations
+ // in relation to oscilloscope style and main window states
+ this.paintWav = this.paintOscilloscope.bind(this);
+ this.pushDown = 0;
+ }
+
+ prepare() {
+ const vis = this._vis;
+
+ //? paint bar
+ this._bar.width = 1;
+ this._bar.height = 5;
+ this._bar.setAttribute("width", "1");
+ this._bar.setAttribute("height", "5");
+ const ctx = this._bar.getContext("2d");
+ if (ctx) {
+ for (let y = 0; y < 5; y++) {
+ ctx.fillStyle = vis.colors[18 + y];
+ ctx.fillRect(0, y, 1, y + 1);
+ }
+ }
+
+ // @ts-ignore
+ this._ctx.imageSmoothingEnabled = false;
+ // @ts-ignore
+ this._ctx.mozImageSmoothingEnabled = false;
+ // @ts-ignore
+ this._ctx.webkitImageSmoothingEnabled = false;
+ // @ts-ignore
+ this._ctx.msImageSmoothingEnabled = false;
+ }
+
+ paintFrame() {
+ if (!this._ctx) return;
+ this._analyser.getByteTimeDomainData(this._dataArray);
+ this._dataArray = this._dataArray.slice(0, 576);
+ const bandwidth = this._dataArray.length;
+
+ const width = this._ctx!.canvas.width;
+ const height = this._ctx!.canvas.height;
+
+ // width would technically be correct, but if the main window is
+ // in windowshade mode, it is set to 150, making sliceWidth look
+ // wrong in that mode, concerning the oscilloscope
+ const sliceWidth = Math.floor(bandwidth / 75);
+
+ // Iterate over the width of the canvas in fixed 75 pixels.
+ for (let j = 0; j <= 75; j++) {
+ const amplitude = slice1st(this._dataArray, sliceWidth, j);
+ this.paintWav(j, amplitude);
+ }
+ }
+
+ /**
+ *
+ * @param y 0..5
+ * @returns value in use for coloring stuff in
+ */
+ colorIndex(y: number): number {
+ if (this._vis.smallVis) {
+ return 0;
+ } else {
+ if (y >= 14) return 4;
+ if (y >= 12) return 3;
+ if (y >= 10) return 2;
+ if (y >= 8) return 1;
+ if (y >= 6) return 0;
+ if (y >= 4) return 1;
+ if (y >= 2) return 2;
+ if (y >= 0) return 3;
+ return 3;
+ }
+ }
+
+ paintOscilloscope(x: number, y: number) {
+ // we skip rendering of the oscilloscope if we are in windowShade mode
+ // previously the renderWidth variable in Vis.tsx scaled down the width
+ // of the canvas, but i didn't really like the idea since we squished
+ // down the result of y to fit within 35/75 pixels, winamp doesn't
+ // squish it's audio data down in the x axis, resulting in only
+ // getting a small portion of what we hear, they did it, so do we
+ if (this._vis.smallVis && this._vis.doubled) {
+ if (x >= 75) {
+ // SORRY NOTHING
+ return;
+ }
+ } else if (x >= (this._vis.smallVis ? 38 : 75)) {
+ // SORRY NOTHING
+ return;
+ }
+ // pushes vis down if not double size, winamp does this
+ if (this._vis.smallVis) {
+ this.pushDown = 0;
+ } else if (this._vis.doubled && !this._vis.isMWOpen) {
+ this.pushDown = 2;
+ } else if (this._vis.doubled) {
+ this.pushDown = 0;
+ } else {
+ this.pushDown = 2;
+ }
+
+ // rounds y down to the nearest int
+ // before that even happens, y is scaled down and then doubled again (could've done * 8
+ // but i feel this makes more sense to me)
+ // y is then adjusted downward to be in the center of the scope
+ y = Math.round((y / 16) * 2) - 9;
+
+ // adjusts the center point of y if we are in windowShade mode, and if pixelDensity is 2
+ // where it's adjusted further to give you the fullest view possible in that small window
+ // else we leave y as is
+ let yadjust: number;
+ if (this._vis.pixelDensity == 2) yadjust = 3;
+ else yadjust = 5;
+ y = this._vis.smallVis ? y - yadjust : y;
+
+ // scales down the already scaled down result of y to 0..10 or 0..5, depending on
+ // if pixelDensity returns 2, this serves the purpose of avoiding full sending
+ // y to that really tiny space we have there
+ if (this._vis.smallVis && this._vis.pixelDensity === 2) {
+ y = Math.round(((y + 11) / 16) * 10) - 5;
+ } else if (this._vis.smallVis) {
+ y = Math.round(((y + 11) / 16) * 5) - 2;
+ }
+
+ // clamp y to be within a certain range, here it would be 0..10 if both windowShade and pixelDensity apply
+ // else we clamp y to 0..15 or 0..3, depending on renderHeight
+ if (this._vis.smallVis && this._vis.pixelDensity === 2) {
+ y = y < 0 ? 0 : y > 10 - 1 ? 10 - 1 : y;
+ } else {
+ y =
+ y < 0
+ ? 0
+ : y > this._vis.renderHeight - 1
+ ? this._vis.renderHeight - 1
+ : y;
+ }
+ let v = y;
+ if (x === 0) this._lastY = y;
+
+ let top = y;
+ let bottom = this._lastY;
+ this._lastY = y;
+
+ if (this._vis.oscStyle === "solid") {
+ if (this._vis.pixelDensity === 2) {
+ if (y >= (this._vis.smallVis ? 5 : 8)) {
+ top = this._vis.smallVis ? 5 : 8;
+ bottom = y;
+ } else {
+ top = y;
+ bottom = this._vis.smallVis ? 5 : 7;
+ }
+ if (x === 0 && this._vis.smallVis) {
+ // why? i dont know!!
+ top = y;
+ bottom = y;
+ }
+ } else {
+ if (y >= (this._vis.smallVis ? 2 : 8)) {
+ top = this._vis.smallVis ? 2 : 8;
+ bottom = y;
+ } else {
+ top = y;
+ bottom = this._vis.smallVis ? 2 : 7;
+ }
+ if (x === 0 && this._vis.smallVis) {
+ // why? i dont know!!
+ top = y;
+ bottom = y;
+ }
+ }
+ } else if (this._vis.oscStyle === "dots") {
+ top = y;
+ bottom = y;
+ } else {
+ if (bottom < top) {
+ [bottom, top] = [top, bottom];
+ if (this._vis.smallVis) {
+ // SORRY NOTHING
+ // really just removes the smoother line descending thing that's present in the Main Window
+ } else {
+ top++; //top++, that emulates Winamp's/WACUP's OSC behavior correctly
+ }
+ }
+ }
+
+ for (y = top; y <= bottom; y++) {
+ this._ctx!.drawImage(
+ this._bar,
+ 0,
+ this.colorIndex(v), // sx,sy
+ 1,
+ 1, // sw,sh
+ x,
+ y + this.pushDown,
+ 1,
+ 1 //dw,dh
+ );
+ }
+ }
+}
+
+export class NoVisualizerHandler extends VisPaintHandler {
+ cleared: boolean = false;
+ prepare() {
+ this.cleared = false;
+ }
+
+ paintFrame() {
+ if (!this._ctx) return;
+ const ctx = this._ctx;
+ this.cleared = true;
+ }
+}
diff --git a/packages/webamp/js/components/Visualizer.tsx b/packages/webamp/js/components/Visualizer.tsx
deleted file mode 100644
index cb376845a8..0000000000
--- a/packages/webamp/js/components/Visualizer.tsx
+++ /dev/null
@@ -1,169 +0,0 @@
-import { useMemo, useCallback, useState, useLayoutEffect } from "react";
-
-import * as Actions from "../actionCreators";
-import * as Selectors from "../selectors";
-import { useTypedSelector, useActionCreator } from "../hooks";
-import { usePaintOscilloscopeFrame } from "./useOscilloscopeVisualizer";
-import { usePaintBarFrame, usePaintBar } from "./useBarVisualizer";
-import { VISUALIZERS, MEDIA_STATUS } from "../constants";
-
-const PIXEL_DENSITY = 2;
-
-type Props = {
- analyser: AnalyserNode;
-};
-
-// Pre-render the background grid
-function preRenderBg(
- width: number,
- height: number,
- bgColor: string,
- fgColor: string,
- windowShade: boolean
-): HTMLCanvasElement {
- // Off-screen canvas for pre-rendering the background
- const bgCanvas = document.createElement("canvas");
- bgCanvas.width = width;
- bgCanvas.height = height;
- const distance = 2 * PIXEL_DENSITY;
-
- const bgCanvasCtx = bgCanvas.getContext("2d");
- if (bgCanvasCtx == null) {
- throw new Error("Could not construct canvas context");
- }
- bgCanvasCtx.fillStyle = bgColor;
- bgCanvasCtx.fillRect(0, 0, width, height);
- if (!windowShade) {
- bgCanvasCtx.fillStyle = fgColor;
- for (let x = 0; x < width; x += distance) {
- for (let y = PIXEL_DENSITY; y < height; y += distance) {
- bgCanvasCtx.fillRect(x, y, PIXEL_DENSITY, PIXEL_DENSITY);
- }
- }
- }
- return bgCanvas;
-}
-
-function Visualizer({ analyser }: Props) {
- useLayoutEffect(() => {
- analyser.fftSize = 2048;
- }, [analyser, analyser.fftSize]);
- const colors = useTypedSelector(Selectors.getSkinColors);
- const style = useTypedSelector(Selectors.getVisualizerStyle);
- const status = useTypedSelector(Selectors.getMediaStatus);
- const getWindowShade = useTypedSelector(Selectors.getWindowShade);
- const dummyVizData = useTypedSelector(Selectors.getDummyVizData);
-
- const toggleVisualizerStyle = useActionCreator(Actions.toggleVisualizerStyle);
- const windowShade = getWindowShade("main");
- const renderWidth = windowShade ? 38 : 76;
- const renderHeight = windowShade ? 5 : 16;
-
- const width = renderWidth * PIXEL_DENSITY;
- const height = renderHeight * PIXEL_DENSITY;
-
- const bgCanvas = useMemo(() => {
- return preRenderBg(
- width,
- height,
- colors[0],
- colors[1],
- Boolean(windowShade)
- );
- }, [colors, height, width, windowShade]);
-
- const paintOscilloscopeFrame = usePaintOscilloscopeFrame({
- analyser,
- height,
- width,
- renderWidth,
- });
- const paintBarFrame = usePaintBarFrame({
- analyser,
- height,
- renderHeight,
- });
- const paintBar = usePaintBar({ height, renderHeight });
-
- const paintFrame = useCallback(
- (canvasCtx: CanvasRenderingContext2D) => {
- if (status !== MEDIA_STATUS.PLAYING) {
- return;
- }
- if (dummyVizData) {
- canvasCtx.drawImage(bgCanvas, 0, 0);
- Object.entries(dummyVizData).forEach(([i, value]) => {
- paintBar(canvasCtx, Number(i), value, -1);
- });
- return;
- }
- switch (style) {
- case VISUALIZERS.OSCILLOSCOPE:
- canvasCtx.drawImage(bgCanvas, 0, 0);
- paintOscilloscopeFrame(canvasCtx);
- break;
- case VISUALIZERS.BAR:
- canvasCtx.drawImage(bgCanvas, 0, 0);
- paintBarFrame(canvasCtx);
- break;
- default:
- canvasCtx.clearRect(0, 0, width, height);
- }
- },
- [
- bgCanvas,
- dummyVizData,
- height,
- paintBar,
- paintBarFrame,
- paintOscilloscopeFrame,
- status,
- style,
- width,
- ]
- );
-
- const [canvas, setCanvas] = useState(null);
-
- useLayoutEffect(() => {
- if (canvas == null) {
- return;
- }
- const canvasCtx = canvas.getContext("2d");
- if (canvasCtx == null) {
- return;
- }
- canvasCtx.imageSmoothingEnabled = false;
-
- let animationRequest: number | null = null;
- // Kick off the animation loop
- const loop = () => {
- paintFrame(canvasCtx);
- animationRequest = window.requestAnimationFrame(loop);
- };
- loop();
-
- return () => {
- if (animationRequest != null) {
- window.cancelAnimationFrame(animationRequest);
- }
- };
- }, [canvas, paintFrame]);
-
- if (status === MEDIA_STATUS.STOPPED) {
- return null;
- }
-
- return (
-
- );
-}
-
-export default Visualizer;
diff --git a/packages/webamp/js/components/useBarVisualizer.ts b/packages/webamp/js/components/useBarVisualizer.ts
deleted file mode 100644
index df340171c8..0000000000
--- a/packages/webamp/js/components/useBarVisualizer.ts
+++ /dev/null
@@ -1,186 +0,0 @@
-import { useMemo, useCallback, useState } from "react";
-
-import * as Selectors from "../selectors";
-import { useTypedSelector } from "../hooks";
-
-const PIXEL_DENSITY = 2;
-const BAR_WIDTH = 3 * PIXEL_DENSITY;
-const GRADIENT_COLOR_COUNT = 16;
-const PEAK_COLOR_INDEX = 23;
-const BAR_PEAK_DROP_RATE = 0.01;
-const NUM_BARS = 20;
-
-function octaveBucketsForBufferLength(bufferLength: number): number[] {
- const octaveBuckets = new Array(NUM_BARS).fill(0);
- const minHz = 200;
- const maxHz = 22050;
- const octaveStep = Math.pow(maxHz / minHz, 1 / NUM_BARS);
-
- octaveBuckets[0] = 0;
- octaveBuckets[1] = minHz;
- for (let i = 2; i < NUM_BARS - 1; i++) {
- octaveBuckets[i] = octaveBuckets[i - 1] * octaveStep;
- }
- octaveBuckets[NUM_BARS - 1] = maxHz;
-
- for (let i = 0; i < NUM_BARS; i++) {
- const octaveIdx = Math.floor((octaveBuckets[i] / maxHz) * bufferLength);
- octaveBuckets[i] = octaveIdx;
- }
-
- return octaveBuckets;
-}
-
-function preRenderBar(
- height: number,
- colors: string[],
- renderHeight: number
-): HTMLCanvasElement {
- /**
- * The order of the colours is commented in the file: the fist two colours
- * define the background and dots (check it to see what are the dots), the
- * next 16 colours are the analyzer's colours from top to bottom, the next
- * 5 colours are the oscilloscope's ones, from center to top/bottom, the
- * last colour is for the analyzer's peak markers.
- */
-
- // Off-screen canvas for pre-rendering a single bar gradient
- const barCanvas = document.createElement("canvas");
- barCanvas.width = BAR_WIDTH;
- barCanvas.height = height;
-
- const offset = 2; // The first two colors are for the background;
- const gradientColors = colors.slice(offset, offset + GRADIENT_COLOR_COUNT);
-
- const barCanvasCtx = barCanvas.getContext("2d");
- if (barCanvasCtx == null) {
- throw new Error("Could not construct canvas context");
- }
- const multiplier = GRADIENT_COLOR_COUNT / renderHeight;
- // In shade mode, the five colors are, from top to bottom:
- // 214, 102, 0 -- 3
- // 222, 165, 24 -- 6
- // 148, 222, 33 -- 9
- // 57, 181, 16 -- 12
- // 24, 132, 8 -- 15
- // TODO: This could probably be improved by iterating backwards
- for (let i = 0; i < renderHeight; i++) {
- const colorIndex = GRADIENT_COLOR_COUNT - 1 - Math.floor(i * multiplier);
- barCanvasCtx.fillStyle = gradientColors[colorIndex];
- const y = height - i * PIXEL_DENSITY;
- barCanvasCtx.fillRect(0, y, BAR_WIDTH, PIXEL_DENSITY);
- }
- return barCanvas;
-}
-
-export function usePaintBar({
- renderHeight,
- height,
-}: {
- renderHeight: number;
- height: number;
-}) {
- const colors = useTypedSelector(Selectors.getSkinColors);
- const getWindowShade = useTypedSelector(Selectors.getWindowShade);
- const windowShade = getWindowShade("main");
-
- const barCanvas = useMemo(() => {
- return preRenderBar(height, colors, renderHeight);
- }, [colors, height, renderHeight]);
-
- return useCallback(
- (
- ctx: CanvasRenderingContext2D,
- x: number,
- barHeight: number,
- peakHeight: number
- ) => {
- barHeight = Math.ceil(barHeight) * PIXEL_DENSITY;
- peakHeight = Math.ceil(peakHeight) * PIXEL_DENSITY;
- if (barHeight > 0 || peakHeight > 0) {
- const y = height - barHeight;
- // Draw the gradient
- const b = BAR_WIDTH;
- if (height > 0) {
- ctx.drawImage(barCanvas, 0, y, b, height, x, y, b, height);
- }
-
- // Draw the gray peak line
- if (!windowShade) {
- const peakY = height - peakHeight;
- ctx.fillStyle = colors[PEAK_COLOR_INDEX];
- ctx.fillRect(x, peakY, b, PIXEL_DENSITY);
- }
- }
- },
- [barCanvas, colors, height, windowShade]
- );
-}
-
-export function usePaintBarFrame({
- renderHeight,
- height,
- analyser,
-}: {
- renderHeight: number;
- height: number;
- analyser: AnalyserNode;
-}) {
- const [barPeaks] = useState(() => new Array(NUM_BARS).fill(0));
- const [barPeakFrames] = useState(() => new Array(NUM_BARS).fill(0));
- const bufferLength = analyser.frequencyBinCount;
-
- const octaveBuckets = useMemo(() => {
- return octaveBucketsForBufferLength(bufferLength);
- }, [bufferLength]);
-
- const dataArray = useMemo(() => {
- return new Uint8Array(bufferLength);
- }, [bufferLength]);
-
- const paintBar = usePaintBar({ height, renderHeight });
-
- return useCallback(
- (canvasCtx: CanvasRenderingContext2D) => {
- analyser.getByteFrequencyData(dataArray);
- const heightMultiplier = renderHeight / 256;
- const xOffset = BAR_WIDTH + PIXEL_DENSITY; // Bar width, plus a pixel of spacing to the right.
- for (let j = 0; j < NUM_BARS - 1; j++) {
- const start = octaveBuckets[j];
- const end = octaveBuckets[j + 1];
- let amplitude = 0;
- for (let k = start; k < end; k++) {
- amplitude += dataArray[k];
- }
- amplitude /= end - start;
-
- // The drop rate should probably be normalized to the rendering FPS, for now assume 60 FPS
- let barPeak =
- barPeaks[j] - BAR_PEAK_DROP_RATE * Math.pow(barPeakFrames[j], 2);
- if (barPeak < amplitude) {
- barPeak = amplitude;
- barPeakFrames[j] = 0;
- } else {
- barPeakFrames[j] += 1;
- }
- barPeaks[j] = barPeak;
-
- paintBar(
- canvasCtx,
- j * xOffset,
- amplitude * heightMultiplier,
- barPeak * heightMultiplier
- );
- }
- },
- [
- analyser,
- barPeakFrames,
- barPeaks,
- dataArray,
- octaveBuckets,
- paintBar,
- renderHeight,
- ]
- );
-}
diff --git a/packages/webamp/js/components/useOscilloscopeVisualizer.ts b/packages/webamp/js/components/useOscilloscopeVisualizer.ts
deleted file mode 100644
index 4b8870c504..0000000000
--- a/packages/webamp/js/components/useOscilloscopeVisualizer.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { useMemo, useCallback } from "react";
-
-import * as Selectors from "../selectors";
-import { useTypedSelector } from "../hooks";
-
-const PIXEL_DENSITY = 2;
-
-// Return the average value in a slice of dataArray
-function sliceAverage(
- dataArray: Uint8Array,
- sliceWidth: number,
- sliceNumber: number
-): number {
- const start = sliceWidth * sliceNumber;
- const end = start + sliceWidth;
- let sum = 0;
- for (let i = start; i < end; i++) {
- sum += dataArray[i];
- }
- return sum / sliceWidth;
-}
-
-export function usePaintOscilloscopeFrame({
- analyser,
- height,
- width,
- renderWidth,
-}: {
- analyser: AnalyserNode;
- height: number;
- width: number;
- renderWidth: number;
-}) {
- const colors = useTypedSelector(Selectors.getSkinColors);
-
- const bufferLength = analyser.fftSize;
-
- const dataArray = useMemo(() => {
- return new Uint8Array(bufferLength);
- }, [bufferLength]);
-
- return useCallback(
- (canvasCtx: CanvasRenderingContext2D) => {
- analyser.getByteTimeDomainData(dataArray);
-
- canvasCtx.lineWidth = PIXEL_DENSITY;
-
- // Just use one of the viscolors for now
- canvasCtx.strokeStyle = 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.
- const sliceWidth = Math.floor(bufferLength / width) * PIXEL_DENSITY;
-
- const h = height;
-
- canvasCtx.beginPath();
-
- // Iterate over the width of the canvas in "real" pixels.
- for (let j = 0; j <= renderWidth; j++) {
- const amplitude = sliceAverage(dataArray, sliceWidth, j);
- const percentAmplitude = amplitude / 255; // dataArray gives us bytes
- const y = (1 - percentAmplitude) * h; // flip y
- const x = j * PIXEL_DENSITY;
-
- // 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) {
- canvasCtx.moveTo(x, y);
- } else {
- canvasCtx.lineTo(x, y);
- }
- }
- canvasCtx.stroke();
- },
- [analyser, bufferLength, colors, dataArray, height, renderWidth, width]
- );
-}
diff --git a/packages/webamp/js/constants.ts b/packages/webamp/js/constants.ts
index 190ced9959..c3881b5828 100644
--- a/packages/webamp/js/constants.ts
+++ b/packages/webamp/js/constants.ts
@@ -55,7 +55,7 @@ export const VISUALIZERS = {
export const VISUALIZER_ORDER = [
VISUALIZERS.BAR,
- VISUALIZERS.OSCILLOSCOPE, // TODO: Verify the order
+ VISUALIZERS.OSCILLOSCOPE, // Order is correct
VISUALIZERS.NONE,
];