mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-28 20:40:39 +00:00
update frequency bars and add falling peaks (#616)
* update frequency bars * add falling peak bars * tweak bar math to drop exponentially * plot frequencies in logspace instead of linearly * use default audio analysis settings * plot frequencies by octave
This commit is contained in:
parent
3173e7eb98
commit
4b7560dda0
2 changed files with 83 additions and 23 deletions
|
|
@ -6,7 +6,8 @@ import { getWindowShade, getVisualizerStyle } from "../selectors";
|
|||
import { VISUALIZERS } from "../constants";
|
||||
|
||||
const PIXEL_DENSITY = 2;
|
||||
const BAR_WIDTH = 6;
|
||||
const NUM_BARS = 20;
|
||||
const BAR_PEAK_DROP_RATE = 0.01;
|
||||
const GRADIENT_COLOR_COUNT = 16;
|
||||
const PEAK_COLOR_INDEX = 23;
|
||||
|
||||
|
|
@ -23,6 +24,8 @@ function sliceAverage(dataArray, sliceWidth, sliceNumber) {
|
|||
|
||||
class Visualizer extends React.Component {
|
||||
componentDidMount() {
|
||||
this.barPeaks = new Array(NUM_BARS).fill(0);
|
||||
this.barPeakFrames = new Array(NUM_BARS).fill(0);
|
||||
this.canvasCtx = this.canvas.getContext("2d");
|
||||
this.canvasCtx.imageSmoothingEnabled = false;
|
||||
|
||||
|
|
@ -72,6 +75,38 @@ class Visualizer extends React.Component {
|
|||
return this.props.width * PIXEL_DENSITY;
|
||||
}
|
||||
|
||||
_barWidth() {
|
||||
const barWidth = Math.floor(this._width() / NUM_BARS);
|
||||
if (barWidth % 2 === 0) {
|
||||
return barWidth;
|
||||
}
|
||||
|
||||
return barWidth - 1;
|
||||
}
|
||||
|
||||
_generateOctaveBuckets() {
|
||||
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) * this.bufferLength
|
||||
);
|
||||
octaveBuckets[i] = octaveIdx;
|
||||
}
|
||||
|
||||
return octaveBuckets;
|
||||
}
|
||||
|
||||
setStyle() {
|
||||
if (!this.props.colors) {
|
||||
return;
|
||||
|
|
@ -80,15 +115,17 @@ class Visualizer extends React.Component {
|
|||
// update.
|
||||
this.preRenderBg();
|
||||
this.preRenderBar();
|
||||
this.props.analyser.fftSize = 2048;
|
||||
if (this.props.style === VISUALIZERS.OSCILLOSCOPE) {
|
||||
this.props.analyser.fftSize = 1024;
|
||||
this.bufferLength = this.props.analyser.fftSize;
|
||||
this.dataArray = new Uint8Array(this.bufferLength);
|
||||
} else if (this.props.style === VISUALIZERS.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 (!this.octaveBuckets) {
|
||||
this.octaveBuckets = this._generateOctaveBuckets();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -127,8 +164,9 @@ class Visualizer extends React.Component {
|
|||
*/
|
||||
|
||||
// Off-screen canvas for pre-rendering a single bar gradient
|
||||
const barWidth = this._barWidth();
|
||||
this.barCanvas = document.createElement("canvas");
|
||||
this.barCanvas.width = BAR_WIDTH;
|
||||
this.barCanvas.width = barWidth;
|
||||
this.barCanvas.height = this._height();
|
||||
|
||||
const offset = 2; // The first two colors are for the background;
|
||||
|
|
@ -151,7 +189,7 @@ class Visualizer extends React.Component {
|
|||
const colorIndex = GRADIENT_COLOR_COUNT - 1 - Math.floor(i * multiplier);
|
||||
barCanvasCtx.fillStyle = gradientColors[colorIndex];
|
||||
const y = this._height() - i * PIXEL_DENSITY;
|
||||
barCanvasCtx.fillRect(0, y, BAR_WIDTH, PIXEL_DENSITY);
|
||||
barCanvasCtx.fillRect(0, y, barWidth, PIXEL_DENSITY);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -204,31 +242,56 @@ class Visualizer extends React.Component {
|
|||
this.canvasCtx.stroke();
|
||||
}
|
||||
|
||||
_printBar(x, height) {
|
||||
_printBar(x, height, peakHeight) {
|
||||
height = Math.ceil(height) * PIXEL_DENSITY;
|
||||
if (height > 0) {
|
||||
peakHeight = Math.ceil(peakHeight) * PIXEL_DENSITY;
|
||||
if (height > 0 || peakHeight > 0) {
|
||||
const y = this._height() - height;
|
||||
const ctx = this.canvasCtx;
|
||||
// Draw the gradient
|
||||
const b = BAR_WIDTH;
|
||||
ctx.drawImage(this.barCanvas, 0, y, b, height, x, y, b, height);
|
||||
const b = this._barWidth();
|
||||
if (height > 0) {
|
||||
ctx.drawImage(this.barCanvas, 0, y, b, height, x, y, b, height);
|
||||
}
|
||||
|
||||
// Draw the gray peak line
|
||||
// TODO: Rather than sitting on top of the bar, these
|
||||
// are expected to be behind the top pixel, and fall more slowly.
|
||||
// Currently these overwrite the top pixel.
|
||||
const peakY = this._height() - peakHeight;
|
||||
ctx.fillStyle = this.props.colors[PEAK_COLOR_INDEX];
|
||||
ctx.fillRect(x, y, BAR_WIDTH, PIXEL_DENSITY);
|
||||
ctx.fillRect(x, peakY, b, PIXEL_DENSITY);
|
||||
}
|
||||
}
|
||||
|
||||
_paintBarFrame() {
|
||||
this.props.analyser.getByteFrequencyData(this.dataArray);
|
||||
// We are printing bars off the right of the canvas :(
|
||||
const xOffset = BAR_WIDTH + PIXEL_DENSITY; // Bar width, plus a pixel of spacing to the right.
|
||||
const heightMultiplier = this._renderHeight() / 256;
|
||||
for (let j = 0; j < this.bufferLength; j++) {
|
||||
this._printBar(j * xOffset, this.dataArray[j] * heightMultiplier);
|
||||
const barWidth = this._barWidth();
|
||||
const xOffset = barWidth + PIXEL_DENSITY; // Bar width, plus a pixel of spacing to the right.
|
||||
for (let j = 0; j < NUM_BARS - 1; j++) {
|
||||
const start = this.octaveBuckets[j];
|
||||
const end = this.octaveBuckets[j + 1];
|
||||
let amplitude = 0;
|
||||
for (let k = start; k < end; k++) {
|
||||
amplitude += this.dataArray[k];
|
||||
}
|
||||
amplitude /= end - start;
|
||||
|
||||
// The drop rate should probably be normalized to the rendering FPS, for now assume 60 FPS
|
||||
let barPeak =
|
||||
this.barPeaks[j] -
|
||||
BAR_PEAK_DROP_RATE * Math.pow(this.barPeakFrames[j], 2);
|
||||
if (barPeak < amplitude) {
|
||||
barPeak = amplitude;
|
||||
this.barPeakFrames[j] = 0;
|
||||
} else {
|
||||
this.barPeakFrames[j] += 1;
|
||||
}
|
||||
this.barPeaks[j] = barPeak;
|
||||
|
||||
this._printBar(
|
||||
j * xOffset,
|
||||
amplitude * heightMultiplier,
|
||||
barPeak * heightMultiplier
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,11 +55,8 @@ export default class Media {
|
|||
// Create the analyser node for the visualizer
|
||||
this._analyser = this._context.createAnalyser();
|
||||
this._analyser.fftSize = 2048;
|
||||
// Tuned by @The1Freeman to resemble Winamp 2.9x.
|
||||
// https://github.com/captbaritone/webamp/pull/613
|
||||
this._analyser.minDecibels = -64;
|
||||
this._analyser.maxDecibels = 0;
|
||||
this._analyser.smoothingTimeConstant = 0.25;
|
||||
// don't smooth audio analysis
|
||||
this._analyser.smoothingTimeConstant = 0.0;
|
||||
|
||||
// Create the gain node for the volume control
|
||||
this._gainNode = this._context.createGain();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue