原生js和electron实现一个音频监听渲染频率直方图

前言

功能实现需要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来绘制直方图;

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值