Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved Winamp Visualizer by @x2nie #1260

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0073056
Improved Winamp Visualizer by @x2nie
0x5066 Aug 15, 2024
09fc229
Fixed analyzer responding to data immediately
0x5066 Aug 15, 2024
9c80ef5
Added support for windowshade mode (colors are broken)
0x5066 Aug 16, 2024
7f3f1e1
Fixed analyzer exceeding bounds in unwindowshaded mode
0x5066 Aug 16, 2024
85dd5f1
Removed paintFrameThin() since it's now no longer needed (processing …
0x5066 Aug 16, 2024
f4424ce
Add proper windowshade mode visualizer support (adapts correctly to d…
0x5066 Aug 17, 2024
aaf4702
Visualizer is now pushed down by 2 pixels if not in double size mode
0x5066 Aug 17, 2024
b9e77c0
Fixed accidentally setting oscStyle to "dots" for testing (oops)
0x5066 Aug 17, 2024
7e3e565
New (non-working) parameter: "sa", dictates vis mode
0x5066 Aug 17, 2024
4f8c379
Maybe fix deploy issues?
0x5066 Aug 18, 2024
2d1457f
Replace rangeByAmplitude with a colorIndex function
0x5066 Aug 20, 2024
dc59c28
Missed a few variables that weren't in camelCase
0x5066 Aug 24, 2024
792831b
Move FFT stuff into VisPainter.ts
0x5066 Aug 24, 2024
99c4b21
Moved the FFT to be part of BarPaintHandler, instead of being global
0x5066 Aug 26, 2024
63e14b2
Missed implementing the solid mode drawing a pixel instead of a fille…
0x5066 Aug 27, 2024
45487bb
Readded drawing the visualizer background
0x5066 Aug 27, 2024
4356be8
Missed accounting for the Playlist Editor Visualizer w.r.t to the bac…
0x5066 Aug 28, 2024
8f91c87
Addressing comments of the recent review
0x5066 Aug 29, 2024
920ee7a
Make canvas required
0x5066 Aug 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 206 additions & 0 deletions packages/webamp/js/components/FFTNullsoft.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// 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 | null;
private equalize: Float32Array | null;
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 =
envelopePower > 0
? this.initEnvelopeTable(samplesIn, envelopePower)
: null;
this.equalize = bEqualize ? this.initEqualizeTable(NFREQ, mode) : null;

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);
}
}
}
7 changes: 2 additions & 5 deletions packages/webamp/js/components/MainWindow/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -106,10 +106,7 @@ const MainWindow = React.memo(({ analyser, filePickers }: Props) => {
/>
<Time />
</div>
<Visualizer
// @ts-ignore Visualizer is not typed yet
analyser={analyser}
/>
<Vis analyser={analyser} />
<div className="media-info">
<Marquee />
<Kbps />
Expand Down
7 changes: 2 additions & 5 deletions packages/webamp/js/components/PlaylistWindow/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
0x5066 marked this conversation as resolved.
Show resolved Hide resolved
import PlaylistShade from "./PlaylistShade";
import AddMenu from "./AddMenu";
import RemoveMenu from "./RemoveMenu";
Expand Down Expand Up @@ -140,10 +140,7 @@ function PlaylistWindow({ analyser }: Props) {
<div className="playlist-visualizer">
{activateVisualizer && (
<div className="visualizer-wrapper">
<Visualizer
// @ts-ignore Visualizer is not yet typed
analyser={analyser}
/>
<Vis analyser={analyser} />
</div>
)}
</div>
Expand Down
Loading
Loading