【StartNode】工作站 --Vioce IO Function

前言:很早之前我就想过用程序将声音转换为文字,这样就可以离开键盘不打字了。

1.Vioce to words

首要解决的问题就是语音转文字了,简单瞄了一眼讯飞的语音识别,要钱,算了,直接看web audio,好像有可以用的api。

简单百度一下后,可以使用web speech api来做语音转换,这是ai给的例子:

// 创建 SpeechRecognition 对象
const speechRecognition = new SpeechRecognition();

// 监听语音识别事件
speechRecognition.addEventListener('start', () => {
  console.log('语音识别开始');
});

speechRecognition.addEventListener('result', (event) => {
  const { results } = event;
  for (let i = 0; i < results.length; i++) {
    const { transcript, confidence } = results[i];
    console.log(`转录结果: ${transcript}, 置信度: ${confidence}`);
  }
});

speechRecognition.addEventListener('error', (event) => {
  console.error('发生错误:', event);
});

// 开始语音识别
speechRecognition.start();

ok,用这个例子放入StartNode里尝试一下,创建一个myVoicejs文件,写入上述代码,然后试运行:

ok,能用。

但是这个api在语音录入的时候,没有什么反馈之类的,我想做一个语音可视化的界面来提供这种反馈效果。

2.Voice graphics render

现在开始尝试做一下语音的图形化。

首先上github上找一下现成的语音可视化的库,能白嫖就白嫖。看了一些现成的,都是那种折线图的动画,但是我想做环形的动画...

所以还是试着做一个简单的例子吧。多方百度后,发现MDN官网给了一个现成的例子:
https://mdn.github.io/voice-change-o-matic/

这个例子的核心:

export function setAudioCtx() {
  // const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  // this.analyser = this.audioCtx.createAnalyser();
  // nalyserNode.minDecibels属性用于设置快速傅立叶变换(FFT)数据的最小值,取值范围为-100到0,默认值为-100。该属性可以与AnalyserNode.maxDecibels属性一起使用,用于指定FFT数据的缩放范围。
  this.analyser.minDecibels = -90;
  this.analyser.maxDecibels = -10;
  this.analyser.smoothingTimeConstant = 0.85;
  // 失真效果 createWaveShaper()是AudioContext对象的一个方法,用于创建一个WaveShaperNode对象。该对象通常被用来给音频添加失真效果,也可以用于实现音频的变形处理,如改变音频的音高或音色等。
  const distortion = this.audioCtx.createWaveShaper();
  // createGain是Web Audio API提供的方法之一,用于创建增益节点( Gain Node ),也称为音量节点。增益节点允许你控制音频信号的音量,即音频的相对强度或音量级别。这在音频处理和音频效果中非常有用,因为它可以让你动态调整音频的音量,创建渐入渐出效果,以及实现音频混音等任务。
  const gainNode = this.audioCtx.createGain();
  // createBiquadFilter()是Web Audio API中的一个方法,用于创建一个双二阶滤波器(Biquad Filter)节点。双二阶滤波器是一种音频滤波器,可以对音频信号进行滤波处理,改变其频率响应。
  const biquadFilter = this.audioCtx.createBiquadFilter();
  // createConvolver是AudioContext对象的一个方法,用于创建一个卷积器节点ConvolverNode。该节点可以对输入的音频信号应用卷积运算,以实现音频处理效果,如滤波、混响等。卷积器节点在音频处理中常用于创建音频效果,例如通过应用滤波器来改变音频的频率响应,或者通过应用混响效果来模拟不同的声学环境。
  const convolver = this.audioCtx.createConvolver();

  // 用于创建回声延迟效果节点(Echo Delay Effect Node)。回声延迟效果节点可以对音频信号应用回声效果,通过调整延迟时间、反馈量等参数来控制回声的特征,实现音频的混响、回声等效果。
  const echoDelay = createEchoDelayEffect(this.audioCtx);
  // 支持getUserMedia 进行以下操作
  if (navigator.mediaDevices.getUserMedia) {
    console.log("getUserMedia supported.");
    const constraints = { audio: true };
    const that = this;
    let source;
    navigator.mediaDevices.getUserMedia(constraints).then(function (stream) {
      // console.log(stream);
      source = that.audioCtx.createMediaStreamSource(stream);
      source.connect(distortion);
      distortion.connect(biquadFilter);
      biquadFilter.connect(gainNode);
      // // convolver.connect(gainNode);
      echoDelay.placeBetween(gainNode, that.analyser);
      // analyser.connect(audioCtx.destination);
      visualize(that.analyser);
    });
  } else {
    console.log("getUserMedia not supported on your browser!");
  }
}

// 绘制图像渲染函数
function visualize(analyser) {
  let canvas = document.querySelector(".visualizer");
  if (!canvas) {
    canvas = document.createElement("canvas");
    canvas.className = "visualizer";
    canvas.width = 80;
    canvas.height = 100;
    canvas.style = "border-radius: 4px;";
    const dom = document.querySelector(".cavbox");
    dom.appendChild(canvas);
  }
  const canvasCtx = canvas.getContext("2d");
  const WIDTH = canvas.width;
  const HEIGHT = canvas.height;
  sessionStorage.setItem("voicerender", true);

  //   const visualSetting = visualSelect.value;
  //   console.log(visualSetting);

  //   if (visualSetting === "sinewave") {
  if (1) {
    analyser.fftSize = 2048;
    const bufferLength = analyser.fftSize;
    // We can use Float32Array instead of Uint8Array if we want higher precision
    // const dataArray = new Float32Array(bufferLength);
    const dataArray = new Uint8Array(bufferLength);

    canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);

    const draw = function () {
      // console.log(sessionStorage.getItem("voicerender") == "false");

      const drawVisual = requestAnimationFrame(draw);
      if (sessionStorage.getItem("voicerender") == "false") {
        // console.log(sessionStorage.getItem("voicerender"));
        // console.log(1);
        cancelAnimationFrame(drawVisual);
        return;
      } else {
        // console.log(sessionStorage.getItem("voicerender"));
      }

      analyser.getByteTimeDomainData(dataArray);

      canvasCtx.fillStyle = "rgb(200, 200, 200)";
      canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);

      canvasCtx.lineWidth = 2;
      canvasCtx.strokeStyle = "rgb(0, 0, 0)";

      canvasCtx.beginPath();

      const sliceWidth = (WIDTH * 1.0) / bufferLength;
      let x = 0;

      for (let i = 0; i < bufferLength; i++) {
        let v = dataArray[i] / 128.0;
        let y = (v * HEIGHT) / 2;

        if (i === 0) {
          // canvasCtx.moveTo(x, y);
          // canvasCtx.arc(40, 50, y / 2, 0, 360);
        } else {
          // canvasCtx.lineTo(x, y);
          // canvasCtx.arc(x, y, r)
          canvasCtx.arc(40, 50, y / 4, 0, 360);
          // canvasCtx.arc(40, 50, x / 2, 0, 360);
        }

        x += sliceWidth;
      }

      // canvasCtx.lineTo(canvas.width, canvas.height / 2);
      canvasCtx.stroke();

      // if (!sessionStorage.getItem("voicerender")) {
      //   console.log(sessionStorage.getItem("voicerender"));
      //   return 0;
      // }
    };

    draw();
  } else if (visualSetting == "frequencybars") {
    analyser.fftSize = 256;
    const bufferLengthAlt = analyser.frequencyBinCount;

    // See comment above for Float32Array()
    const dataArrayAlt = new Uint8Array(bufferLengthAlt);

    canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);

    const drawAlt = function () {
      drawVisual = requestAnimationFrame(drawAlt);

      analyser.getByteFrequencyData(dataArrayAlt);

      canvasCtx.fillStyle = "rgb(0, 0, 0)";
      canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);

      const barWidth = (WIDTH / bufferLengthAlt) * 2.5;
      let barHeight;
      let x = 0;

      for (let i = 0; i < bufferLengthAlt; i++) {
        barHeight = dataArrayAlt[i];

        canvasCtx.fillStyle = "rgb(" + (barHeight + 100) + ",50,50)";
        canvasCtx.fillRect(x, HEIGHT - barHeight / 2, barWidth, barHeight / 2);

        x += barWidth + 1;
      }
    };

    drawAlt();
  } else if (visualSetting == "off") {
    canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
    canvasCtx.fillStyle = "red";
    canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
  }
}

上述代码是来自voice-change-o-matic,我改动了绘制函数的部分,他这个例子绘制的是折线图,所以我直接在绘制函数里进行了小小的改动,这样就可以达到圆环的效果!

最后,将这些内容整合起来,就完成了。

由于这是本地项目,所以没有挂到博客上;演示视频:

仓库地址:stillwarter/startnode: How did I start nodejs as an noob (github.com)

3.后续

这个声音转文字倒是做好了,不过可惜的是这个web speech api的准确度不够高,感觉还是不够用。拓展方面我只做了关键词,当我说出关键词的时候,会自动跳转到对应路由页面。

后面最多把这个加到日志里,日志的东西口水话一点没什么。或者引入ai?让ai帮我处理转换不准确的问题。

最后祝大家春节愉快!🎉 🎉 🎉