js录音原始数据降采样升采样,生成PCM文件(特指用于流失识别)

1 篇文章 0 订阅
1 篇文章 0 订阅
  1. 初始化录音
// 文件名称: recorder.js

import recordWorker from './transcode.worker'

const URL = window.URL || window.webkitURL // 获取浏览器API

// 加载并启动 record worker
let workerString = recordWorker.toString()
// 移除函数包裹
workerString = workerString.substr(workerString.indexOf("{") + 1)
workerString = workerString.substr(0, workerString.lastIndexOf("}"))
const workerBlob = new Blob([workerString])
const workerURL = URL.createObjectURL(workerBlob)
const worker = new Worker(workerURL)

/**
 * class IatRecorder 语音听写类
 * @param {Object} config 参数
 */
class IatRecorder {
  constructor(config) {
    this.status = "null" // 当前录音的状态 null为开始 ing为录音中 end为结束

    // 记录音频数据
    this.audioData = []
    this.buffer = []
    // 记录听写结果
    worker.onmessage = (event) => {
      this.audioData.push(...event.data)
      console.log('处理后的数据长度', this.audioData.length)
    }
  }
  // 修改录音听写状态
  setStatus (status) {
    this.onWillStatusChange &&
      this.status !== status &&
      this.onWillStatusChange(this.status, status)
    this.status = status
  }
  // 初始化浏览器录音
  recorderInit () {
    navigator.getUserMedia =
      navigator.getUserMedia ||
      navigator.webkitGetUserMedia ||
      navigator.mozGetUserMedia ||
      navigator.msGetUserMedia

    // 创建音频环境
    try {
      this.audioContext = new (window.AudioContext ||
          window.webkitAudioContext)()
      console.log('audioContext sampleRate:', this.audioContext.sampleRate)
      console.log('UA', navigator.userAgent)
      if (!this.audioContext) {
        alert("浏览器不支持webAudioApi相关接口,请前往Chrome,FireFox浏览器体验")
        return
      }
    } catch (e) {
      if (!this.audioContext) {
        alert("浏览器不支持webAudioApi相关接口,请前往Chrome,FireFox浏览器体验")
        return
      }
    }

    // 获取浏览器录音权限
    if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
      navigator.mediaDevices
        .getUserMedia({
          audio: true,
          video: false,
        })
        .then((stream) => {
          getMediaSuccess(stream)
        })
        .catch((e) => {
          getMediaFail(e)
        })
    } else if (navigator.getUserMedia) {
      navigator.getUserMedia(
        {
          audio: true,
          video: false,
        },
        (stream) => {
          getMediaSuccess(stream)
        },
        function (e) {
          getMediaFail(e)
        }
      )
    } else {
      if (
        navigator.userAgent.toLowerCase().match(/chrome/) &&
        location.origin.indexOf("https://") < 0
      ) {
        alert(
          "chrome下获取浏览器录音功能,因为安全性问题,需要在localhost或127.0.0.1或https下才能获取权限"
        )
      } else {
        alert("无法获取浏览器录音功能,请升级浏览器或使用chrome")
      }
      this.audioContext && this.audioContext.close()
      return
    }
    // 获取浏览器录音权限成功的回调
    let getMediaSuccess = (stream) => {
      // 创建一个用于通过JavaScript直接处理音频
      this.scriptProcessor = this.audioContext.createScriptProcessor(
        4096,
        1,
        1
      )
      this.scriptProcessor.onaudioprocess = (e) => {
        // 去处理音频数据
        this.buffer.push(...e.inputBuffer.getChannelData(0))
        console.log('录音原始数据长度,类型为 Float32Array', e.inputBuffer.getChannelData(0).length)
        worker.postMessage({
            command: "transform",
            buffer: e.inputBuffer.getChannelData(0),
            is16K: this.audioContext.sampleRate,
        })
      }
      // 创建一个新的MediaStreamAudioSourceNode 对象,使来自MediaStream的音频可以被播放和操作
      this.mediaSource = this.audioContext.createMediaStreamSource(stream)
      // 连接
      this.mediaSource.connect(this.scriptProcessor)
      this.scriptProcessor.connect(this.audioContext.destination)
    };

    let getMediaFail = () => {
      alert("请求麦克风失败")
      this.audioContext && this.audioContext.close()
      this.audioContext = undefined
    }
  }

  /**
   * 开始录音
   */
  recorderStart () {
    this.setStatus("ing")
    this.recorderStop()
    this.recorderInit()
  }
  /**
   * 暂停录音
   * @param e
   */
  recorderStop (e) {
    // this.download();
    this.setStatus("end")
    // 关闭录音
    this.audioContext && this.audioContext.close()
    this.audioContext = undefined
  }
  
  getBuffer () {
    let output = this.to16BitPCM(this.buffer)
    return output
  }
  to16BitPCM (input) {
    var dataLength = input.length * (16 / 8)
    var dataBuffer = new ArrayBuffer(dataLength)
    var dataView = new DataView(dataBuffer)
    var offset = 0
    for (var i = 0; i < input.length; i++, offset += 2) {
      var s = Math.max(-1, Math.min(1, input[i]))
      dataView.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true)
    }
    return dataView
  }
  download () {
    const blob = new Blob([this.getBuffer()]) //处理文档流
    if ("msSaveOrOpenBlob" in navigator) {
      window.navigator.msSaveOrOpenBlob(blob, "test.pcm")
      return
    }
    const elink = document.createElement("a")
    elink.download = "test.pcm"
    elink.style.display = "none"
    elink.href = URL.createObjectURL(blob)
    document.body.appendChild(elink)
    elink.click()
    URL.revokeObjectURL(elink.href) // 释放URL 对象
    document.body.removeChild(elink)
  }
}

export default IatRecorder

  1. 利用 worker 处理数据
const recordWorker = function () {
    let self = this;
    this.onmessage = function (e) {
        switch (e.data.command) {
            case "transform":
                transform.transaction(e.data, e.data.is16K);
                break;
        }
    };

    let transform = {
        transaction(audioData, is16K) {
            let output;
            if (is16K === 16000) {
                output = transform.to16BitPCM(audioData.buffer, is16K);
            } else if (is16K > 16000) {
                output = transform.to16kHz(audioData.buffer, is16K);
                output = transform.to16BitPCM(output);
            } else if (is16K < 16000) {
                output = transform.lessTo16kHz(audioData.buffer, is16K);
                output = transform.to16BitPCM(output);
            }
            output = Array.from(new Uint8Array(output.buffer));
            self.postMessage(output);
        },
        /**
         * 大于16kHz降采样到16kHz
         * @param audioData
         * @param originalSampleRate
         * @return {*|Float32Array}
         */
        to16kHz(audioData, originalSampleRate) {
            if (originalSampleRate <= 16000) {
                return audioData;
            }

            let data = new Float32Array(audioData);
            let fitCount = Math.round(data.length * (16000 / originalSampleRate));
            let newData = new Float32Array(fitCount);
            let springFactor = (data.length - 1) / (fitCount - 1);

            newData[0] = data[0];
            for (let i = 1; i < fitCount - 1; i++) {
                let tmp = i * springFactor;
                let before = Math.floor(tmp).toFixed();
                let after = Math.ceil(tmp).toFixed();
                let atPoint = tmp - before;
                newData[i] = data[before] + (data[after] - data[before]) * atPoint;
            }
            newData[fitCount - 1] = data[data.length - 1];

            return newData;
        },
        /**
         * 小于16kHz升采样到16kHz
         * @param audioData
         * @param originalSampleRate
         * @return {*|Float32Array}
         */
        lessTo16kHz(audioData, originalSampleRate) {
            // 如果原始采样率已经是16kHz或更高,直接返回原始数据
            if (originalSampleRate >= 16000) {
                return audioData;
            }
            // 计算新的采样率与原始采样率的比例
            const upsampleFactor = 16000 / originalSampleRate;
            // 计算新的数据长度
            const newLength = Math.ceil(audioData.length * upsampleFactor);
            // 创建一个新的 Float32Array 用于存储升采样后的数据
            const newData = new Float32Array(newLength);
            // 进行最近邻插值
            for (let i = 0; i < newLength; i++) {
                // 计算原始数据中对应的样本索引
                const originalIndex = Math.floor(i / upsampleFactor);
                // 将原始样本值复制到新数据中
                newData[i] = audioData[originalIndex];
            }
            return newData;
        },
        to16BitPCM(input) {
            let dataLength = input.length * (16 / 8); // 计算了将要创建的缓冲区的长度,这里使用了位操作来表示字节数,即 16 / 8 表示每个样本占用2个字节(16位PCM格式)。
            let dataBuffer = new ArrayBuffer(dataLength); // 是一个用于存储二进制数据的固定大小的缓冲区,其长度为 dataLength 字节。
            let dataView = new DataView(dataBuffer); // 则是一个用于读写 dataBuffer 中数据的视图,通过它可以以不同的数据格式(如整数、浮点数)访问缓冲区中的数据。

            let offset = 0; // offset 变量用于追踪当前写入到 dataView 中的位置。初始为0,每次迭代增加2,因为每个16位样本占用2个字节。
            for (let i = 0; i < input.length; i++, offset += 2) {
                let s = Math.max(-1, Math.min(1, input[i])); // Math.max(-1, Math.min(1, input[i])) 确保将输入值限制在 -1 到 1 之间,因为16位PCM格式只能表示在 -32768 到 32767 之间的整数。
                // dataView.setInt16(offset, value, littleEndian) 将处理过的音频数据写入到 dataView 中:
                //     offset 是数据在缓冲区中的偏移量。
                //     value 是通过将输入数据映射到16位有符号整数的方式得到的数值。
                //     littleEndian 参数设为 true,表示使用小端序(低位字节在前,高位字节在后)存储数据,这是常见的音频格式存储方式。
                dataView.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
            }
            return dataView;
        },
        to8BitPCM(input) {
            var dataLength = input.length;
            var dataBuffer = new ArrayBuffer(dataLength);
            var dataView = new DataView(dataBuffer);
            var offset = 0;
            for (var i = 0; i < input.length; i++, offset++) {
                var s = Math.max(-1, Math.min(1, input[i]));
                var val = s < 0 ? s * 0x80 : s * 0x7F;
                var intVal = Math.round(val); // Round to nearest integer
                dataView.setInt8(offset, intVal);
            }
            return dataView;
        },
    };
};

export default recordWorker;

  1. 调用示例
<script setup>

    import { onMounted } from 'vue'; // 导入 onMounted 钩子

    import recorder from './utils/recorder';
    let audioRecorder = null; // 用于存储录音对象的变量

    // 初始化录音对象
    const initializeRecorder = () => {
        audioRecorder = new recorder(); // 假设 recorder 是一个类,并且通过 new 创建实例
        console.log('Recorder initialized:', audioRecorder);
    }

    const startRecording = () => {
        console.log('开始录音');
        if (!audioRecorder) {
            console.error('Recorder not initialized!');
            return;
        }
        audioRecorder.recorderStart(); // 调用 recorder 中的 start 方法开始录音
    }

    const stopRecording = () => {
        console.log('结束录音');
        if (!audioRecorder) {
            console.error('Recorder not initialized!');
            return;
        }
        audioRecorder.recorderStop(); // 调用 recorder 中的 stop 方法结束录音
    }

    const download = () => {
        if (audioRecorder.status !== 'end') {
            alert('请完成录音后点击下载')
            return;
        }
        audioRecorder.download()
    }

    // 在组件加载时自动初始化录音对象
    onMounted(() => {
        initializeRecorder();
    });
</script>

<template>
    <div id="app">
        <button @click="startRecording">开始录音</button>
        <br/>
        <br/>
        <button @click="stopRecording">结束录音</button>

        <br/>
        <br/>
        <button @click="download">下载录音文件 PCM</button>
    </div>
</template>

<style scoped>

</style>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值