前言
功能实现需要FFT(快速傅立叶变换),canvas绘制
目录
1.FFT算法;
2.canvas绘制API;
3.性能优化;
(1)八分频
(2)加窗函数
正文
1.FFT算法,主要只有一条公式
Math.round(Math.log(bufferSize) / Math.log(2))
function FFT_Fn(bufferSize) {
//bufferSize只能取值2的n次方
FFT_N_LOG = Math.round(Math.log(bufferSize) / Math.log(2));
FFT_N = 1 << FFT_N_LOG;
MINY = (FFT_N << 2) * Math.sqrt(2);
real = [];
imag = [];
sintable = [0];
costable = [0];
bitReverse = [];
var i, j, k, reve;
for (i = 0; i < FFT_N; i++) {
k = i;
for (j = 0, reve = 0; j != FFT_N_LOG; j++) {
reve <<= 1;
reve |= k & 1;
k >>>= 1;
}
bitReverse[i] = reve;
}
var theta,
dt = (2 * Math.PI) / FFT_N;
for (i = (FFT_N >> 1) - 1; i > 0; i--) {
theta = i * dt;
costable[i] = Math.cos(theta);
sintable[i] = Math.sin(theta);
}
}
2、canvas绘制
draw: function (frequencyData, sampleRate) {
frequencyData.forEach((item, index) => {
if (index === 0) {
this.frequencyFirst = item;
} else if (index === 1) {
this.frequencySecond = item;
}
});
var This = this,
// set = This.set;
set = This.set;
var ctx = This.ctx;
var scale = set.scale;
var width = this.width * scale;
var height = this.height * scale;
var lineCount = set.lineCount;
var bufferSize = This.fft.bufferSize;
//计算高度位置
var position = set.position;
var posAbs = Math.abs(set.position);
// console.log(posAbs);
var originY = position == 1 ? 0 : height; //y轴原点
var heightY = height; //最高的一边高度
if (posAbs < 1) {
heightY = heightY / 2;
originY = heightY;
heightY = Math.floor(heightY * (1 + posAbs));
originY = Math.floor(
position > 0 ? originY * (1 - posAbs) : originY * (1 + posAbs)
);
}
var lastH = This.lastH;
var stripesH = This.stripesH;
var speed = Math.ceil(heightY / (set.fallDuration / (1000 / set.fps)));
var stripeSpeed = Math.ceil(
heightY / (set.stripeFallDuration / (1000 / set.fps))
);
var stripeMargin = set.stripeMargin * scale;
var Y0 = 1 << (Math.round(Math.log(bufferSize) / Math.log(2) + 3) << 1);
var logY0 = Math.log(Y0) / Math.log(10);
var dBmax = (20 * Math.log(0x7fff)) / Math.log(10);
var fftSize = bufferSize / 2;
var fftSize5k = Math.min(
fftSize,
Math.floor((fftSize * 5000) / (sampleRate / 2))
); //5khz所在位置,8000采样率及以下最高只有4khz
var fftSize5kIsAll = fftSize5k == fftSize;
var line80 = fftSize5kIsAll ? lineCount : Math.round(lineCount * 0.8); //80%的柱子位置
var fftSizeStep1 = fftSize5k / line80;
var fftSizeStep2 = fftSize5kIsAll
? 0
: (fftSize - fftSize5k) / (lineCount - line80);
var fftIdx = 0;
for (var i = 0; i < lineCount; i++) {
//不采用jmp123的非线性划分频段,录音语音并不适用于音乐的频率,应当弱化高频部分
//80%关注0-5khz主要人声部分 20%关注剩下的高频,这样不管什么采样率都能做到大部分频率显示一致。
var start = Math.ceil(fftIdx);
// console.log(start);
if (i < line80) {
//5khz以下
fftIdx += fftSizeStep1;
} else {
//5khz以上
fftIdx += fftSizeStep2;
}
// console.log(fftIdx);
var end = Math.min(Math.ceil(fftIdx), fftSize);
// console.log(end);
//参考AudioGUI.java .drawHistogram方法
//查找当前频段的最大"幅值"
var maxAmp = 0;
for (var j = start; j < end; j++) {
// maxAmp = Math.max(maxAmp, Math.abs(this.frequencyFirst[j]));
if (start < 107) {
// console.log(this.frequencySecond[j]);
maxAmp = Math.max(maxAmp, Math.abs(this.frequencyFirst[j]));
} else {
maxAmp = Math.max(maxAmp, Math.abs(this.frequencySecond[j]));
}
}
// console.log(i, maxAmp);
//计算音量
//解决中间没有频率问题
// var dB =
// maxAmp > Y0
// ? Math.floor((Math.log(maxAmp) / Math.log(10) - logY0) * 17)
// : 0;
var dB =
maxAmp === 0
? 0
: Math.floor((Math.log(maxAmp) / Math.log(10) - logY0) * 17);
//heightY * Math.min(dB / dBmax, 1)
// var h = heightY * Math.min(dB / dBmax, 1);
var h = Math.abs(heightY * Math.min(dB / dBmax, 1));
// console.log(h, 'hhhhhhhhhh');
//使柱子匀速下降
// console.log(lastH[i], 'rrrrrr');
lastH[i] = (lastH[i] || 0) - speed;
// console.log(lastH[i], 'ooooooo');
if (h < lastH[i]) {
h = lastH[i];
}
// console.log(h, lastH[i]);
if (h < 0) {
h = 0;
}
lastH[i] = h;
var shi = stripesH[i] || 0;
// console.log(shi);
if (h && h + stripeMargin > shi) {
stripesH[i] = h + stripeMargin;
} else {
//使峰值小横条匀速度下落
var sh = shi - stripeSpeed;
// console.log(shi, stripeSpeed);
if (sh < 0) {
sh = 0;
}
stripesH[i] = sh;
}
}
//开始绘制图形
ctx.clearRect(0, 0, width, height);
var linear1 = This.genLinear(ctx, set.linear, originY, originY - heightY); //上半部分的填充
var stripeLinear1 =
(set.stripeLinear &&
This.genLinear(ctx, set.stripeLinear, originY, originY - heightY)) ||
linear1; //上半部分的峰值小横条填充
var linear2 = This.genLinear(ctx, set.linear, originY, originY + heightY); //下半部分的填充
var stripeLinear2 =
(set.stripeLinear &&
This.genLinear(ctx, set.stripeLinear, originY, originY + heightY)) ||
linear2; //上半部分的峰值小横条填充
//计算柱子间距
ctx.shadowBlur = set.shadowBlur * scale;
ctx.shadowColor = set.shadowColor;
var mirrorEnable = set.mirrorEnable;
var mirrorCount = mirrorEnable ? lineCount * 2 - 1 : lineCount; //镜像柱子数量翻一倍-1根
var widthRatio = set.widthRatio;
var spaceWidth = set.spaceWidth * scale;
if (spaceWidth != 0) {
widthRatio = (width - spaceWidth * (mirrorCount + 1)) / width;
}
var lineWidth = Math.max(
1 * scale,
Math.floor((width * widthRatio) / mirrorCount)
); //柱子宽度至少1个单位
var spaceFloat = (width - mirrorCount * lineWidth) / (mirrorCount + 1); //均匀间隔,首尾都留空,可能为负数,柱子将发生重叠
//绘制柱子
var minHeight = set.minHeight * scale;
var mirrorSubX = spaceFloat + lineWidth / 2;
var XFloat = mirrorEnable ? width / 2 - mirrorSubX : 0; //镜像时,中间柱子位于正中心
for (var i = 0, xFloat = XFloat, x, y, h; i < lineCount; i++) {
xFloat += spaceFloat;
x = Math.floor(xFloat);
h = Math.max(lastH[i], minHeight);
//绘制上半部分
if (originY != 0) {
y = originY - h;
ctx.fillStyle = linear1;
ctx.fillRect(x, y, lineWidth, h);
}
//绘制下半部分
if (originY != height) {
ctx.fillStyle = linear2;
ctx.fillRect(x, originY, lineWidth, h);
}
xFloat += lineWidth;
}
//绘制柱子顶上峰值小横条
if (set.stripeEnable) {
var stripeShadowBlur = set.stripeShadowBlur;
ctx.shadowBlur =
(stripeShadowBlur == -1 ? set.shadowBlur : stripeShadowBlur) * scale;
ctx.shadowColor = set.stripeShadowColor || set.shadowColor;
var stripeHeight = set.stripeHeight * scale;
for (var i = 0, xFloat = XFloat, x, y, h; i < lineCount; i++) {
xFloat += spaceFloat;
x = Math.floor(xFloat);
h = stripesH[i];
//绘制上半部分
if (originY != 0) {
y = originY - h - stripeHeight;
if (y < 0) {
y = 0;
}
ctx.fillStyle = stripeLinear1;
// console.log(y, '2');
ctx.fillRect(x, y, lineWidth, stripeHeight);
}
//绘制下半部分
if (originY != height) {
y = originY + h;
if (y + stripeHeight > height) {
y = height - stripeHeight;
}
ctx.fillStyle = stripeLinear2;
ctx.fillRect(x, y, lineWidth, stripeHeight);
}
xFloat += lineWidth;
}
}
//镜像,从中间直接镜像即可
if (mirrorEnable) {
var srcW = Math.floor(width / 2);
ctx.save();
ctx.scale(-1, 1);
ctx.drawImage(
This.canvas,
Math.ceil(width / 2),
0,
srcW,
height,
-srcW,
0,
srcW,
height
);
ctx.restore();
}
// console.log(this.canvas.width, this.canvas.height);
set.onDraw(frequencyData, sampleRate);
},
3.性能优化;
(1)八分频
(2)加窗函数
//8分频取点 //生成汉宁窗,降低矩形窗边缘的信号泄露影响( 0.5 - 0.5 * Math.cos((2 * Math.PI * i) / (second_fft_real.length - 1));) var second_fft_real = new Int16Array(1024); var second_fft_real2 = new Int16Array(1024); // second_fft_imag[0] = 0; this.first_fft_real = arr.slice(0, 1024); for (var i = 0; i < arr.length; i++) { second_fft_real[i] = [ (arr[i * 8] + arr[i * 8 + 1] + arr[i * 8 + 2] + arr[i * 8 + 3] + arr[i * 8 + 4] + arr[i * 8 + 5] + arr[i * 8 + 6] + arr[i * 8 + 7]) / 8.0, ] * 0.5 - 0.5 * Math.cos((2 * Math.PI * i) / (i - 1)); }
总结
1、FFT通过Math.round(Math.log(bufferSize) / Math.log(2))来求出实部和虚部的音频段;
2、使用canvas中的API:clearRect,fillRect,addColorStop,createLinearGradient来绘制直方图;