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:
jberg 2018-07-24 22:10:11 -07:00 committed by Jordan Eldredge
parent 3173e7eb98
commit 4b7560dda0
2 changed files with 83 additions and 23 deletions

View file

@ -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
);
}
}

View file

@ -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();