web如何实现录制音频,满满干货(下篇)

news2024/9/17 7:47:19

上篇中讲了,web如何实现录制音频,这一篇中,介绍如何播放录制好的音频,以及如何下载和上传音频。

播放

播放,其实就有很多种方法了,可以先上传到云服务器,然后生成链接,使用audio标签进行播放;当然录制完成之后,没有上传之前,也是可以播放的。

获取录制数据

录制中的时候,数据全部存储为this.lBuffer和this.rBuffer,现在就可以使用,不过,当初存储一个怎样的数据呢?先来回顾一下

// 左声道数据
      // getChannelData返回Float32Array类型的pcm数据
      let lData = e.inputBuffer.getChannelData(0),
        rData = null,
        vol = 0; // 音量百分比
      // console.log(lData)
      this.lBuffer.push(new Float32Array(lData));

      this.size += lData.length;

      // 判断是否有右声道数据
      if (this.config.numChannels === 2) {
        rData = e.inputBuffer.getChannelData(1);
        this.rBuffer.push(new Float32Array(rData));

        this.size += rData.length;
      }

Float32Array,是使用数组来存一个一个Float32Array数组的,所以,现在获取所有的Float32Array数据,需要先把二维数组,转换为一维数组。

/**
   * 将二维数组转一维
   *
   * @private
   * @returns  {float32array}     音频pcm二进制数据
   * @memberof Recorder
   */
  flat() {
    let lData = null,
      rData = new Float32Array(0); // 右声道默认为0

    // 创建存放数据的容器
    if (this.config.numChannels === 1) {
      lData = new Float32Array(this.size);
    } else {
      lData = new Float32Array(this.size / 2);
      rData = new Float32Array(this.size / 2);
    }
    // 合并
    let offset = 0; // 偏移量计算
    // 将二维数据,转成一维数据
    // 左声道
    this.lBuffer.forEach(buffer => {
      lData.set(buffer, offset);
      offset += buffer.length;
    });

    // 右声道
    offset = 0;
    this.rBuffer.forEach(buffer => {
      rData.set(buffer, offset);
      offset += buffer.length;
    });

    return {
      left: lData,
      right: rData
    };
  }
// 获取录音数据
  getData() {
    return this.flat();
  }

数据合并压缩

根据输入和输出的采样率压缩数据,比如输入的采样率是48k的,我们需要的是(输出)的是16k的,由于48k与16k是3倍关系,所以输入数据中每隔3取1位

/**
 * 数据合并压缩
 * 根据输入和输出的采样率压缩数据,
 * 比如输入的采样率是48k的,我们需要的是(输出)的是16k的,由于48k与16k是3倍关系,
 * 所以输入数据中每隔3取1位
 *
 * @param {float32array} data       [-1, 1]的pcm数据
 * @param {number} inputSampleRate  输入采样率
 * @param {number} outputSampleRate 输出采样率
 * @returns  {float32array}         压缩处理后的二进制数据
 */
export function compress(data, inputSampleRate, outputSampleRate) {
  // 压缩,根据采样率进行压缩
  let rate = inputSampleRate / outputSampleRate,
    compression = Math.max(rate, 1),
    lData = data.left,
    rData = data.right,
    length = Math.floor((lData.length + rData.length) / rate),
    result = new Float32Array(length),
    index = 0,
    j = 0;

  // 循环间隔 compression 位取一位数据
  while (index < length) {
    // 取整是因为存在比例compression不是整数的情况
    let temp = Math.floor(j);

    result[index] = lData[temp];
    index++;

    if (rData.length) {
      /*
       * 双声道处理
       * e.inputBuffer.getChannelData(0)得到了左声道4096个样本数据,1是右声道的数据,
       * 此处需要组和成LRLRLR这种格式,才能正常播放,所以要处理下
       */
      result[index] = rData[temp];
      index++;
    }

    j += compression;
  }
  // 返回压缩后的一维数据
  return result;
}

如果是双声道,那就需要特殊处理,e.inputBuffer.getChannelData(0)得到了左声道4096个样本数据,1是右声道的数据,此处需要组和成LRLRLR这种格式,才能正常播放。

我的电脑上,输入和输出的采样率是一样的,所以都是1

转换对应格式编码

按采样位数重新编码

/**
 * 转换到我们需要的对应格式的编码
 *
 * @param {Float32Array} bytes      pcm二进制数据
 * @param {number}  sampleBits      采样位数
 * @param {boolean} littleEdian     是否是小端字节序
 * @returns {dataview}              pcm二进制数据
 */
export function encodePCM(bytes, sampleBits, littleEdian = true) {
  let offset = 0,
    dataLength = bytes.length * (sampleBits / 8),
    buffer = new ArrayBuffer(dataLength),
    data = new DataView(buffer);

  // 写入采样数据
  if (sampleBits === 8) {
    for (let i = 0; i < bytes.length; i++, offset++) {
      // 范围[-1, 1]
      let s = Math.max(-1, Math.min(1, bytes[i]));
      // 8位采样位划分成2^8=256份,它的范围是0-255;
      // 对于8位的话,负数*128,正数*127,然后整体向上平移128(+128),即可得到[0,255]范围的数据。
      let val = s < 0 ? s * 128 : s * 127;
      val = +val + 128;
      data.setInt8(offset, val);
    }
  } else {
    for (let i = 0; i < bytes.length; i++, offset += 2) {
      let s = Math.max(-1, Math.min(1, bytes[i]));
      // 16位的划分的是2^16=65536份,范围是-32768到32767
      // 因为我们收集的数据范围在[-1,1],那么你想转换成16位的话,只需要对负数*32768,对正数*32767,即可得到范围在[-32768,32767]的数据。
      data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, littleEdian);
    }
  }

  return data;
}

这里有一个判断是否小端字节序

那什么是字节序,简单来说,就是超过一个字节的数据类型在内存中的存储顺序。目前有两种字节序,大端字节序和小端字节序。详细介绍可以看下面的文章:

https://blog.csdn.net/damanchen/article/details/112424874

阮一峰老师的:

https://www.ruanyifeng.com/blog/2016/11/byte-order.html

在windows平台上是小端字节序(Windos(x86,x64)和Linux(x86,x64)都是Little Endian操作系统,所以默认小端字节序为true。

PCM数据

获取到PCM数据,就是要经历上面的步骤,合并压缩,格式编码

getPCM() {
    // 先停止
    this.stop();
    // 获取pcm数据
    let data = this.getData();
    // 根据输入输出比例 压缩或扩展
    data = compress(data, this.inputSampleRate, this.outputSampleRate);
    // 按采样位数重新编码
    return encodePCM(data, this.oututSampleBits, this.littleEdian);
  }

WAV编码

编码wav,一般wav格式是在pcm文件前增加44个字节的文件头,所以,此处只需要在pcm数据前增加下就行了。

/**
 * 编码wav,一般wav格式是在pcm文件前增加44个字节的文件头,
 * 所以,此处只需要在pcm数据前增加下就行了。
 *
 * @param {DataView} bytes           pcm二进制数据
 * @param {number}  inputSampleRate  输入采样率
 * @param {number}  outputSampleRate 输出采样率
 * @param {number}  numChannels      声道数
 * @param {number}  oututSampleBits  输出采样位数
 * @param {boolean} littleEdian      是否是小端字节序
 * @returns {DataView}               wav二进制数据
 */
export function encodeWAV(bytes, inputSampleRate, outputSampleRate, numChannels, oututSampleBits, littleEdian = true) {
  let sampleRate = outputSampleRate > inputSampleRate ? inputSampleRate : outputSampleRate, // 输出采样率较大时,仍使用输入的值,
    sampleBits = oututSampleBits,
    buffer = new ArrayBuffer(44 + bytes.byteLength),
    data = new DataView(buffer),
    channelCount = numChannels, // 声道
    offset = 0;

  // 资源交换文件标识符
  writeString(data, offset, 'RIFF');
  offset += 4;
  // 下个地址开始到文件尾总字节数,即文件大小-8
  data.setUint32(offset, 36 + bytes.byteLength, littleEdian);
  offset += 4;
  // WAV文件标志
  writeString(data, offset, 'WAVE');
  offset += 4;
  // 波形格式标志
  writeString(data, offset, 'fmt ');
  offset += 4;
  // 过滤字节,一般为 0x10 = 16
  data.setUint32(offset, 16, littleEdian);
  offset += 4;
  // 格式类别 (PCM形式采样数据)
  data.setUint16(offset, 1, littleEdian);
  offset += 2;
  // 声道数
  data.setUint16(offset, channelCount, littleEdian);
  offset += 2;
  // 采样率,每秒样本数,表示每个通道的播放速度
  data.setUint32(offset, sampleRate, littleEdian);
  offset += 4;
  // 波形数据传输率 (每秒平均字节数) 声道数 × 采样频率 × 采样位数 / 8
  data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), littleEdian);
  offset += 4;
  // 快数据调整数 采样一次占用字节数 声道数 × 采样位数 / 8
  data.setUint16(offset, channelCount * (sampleBits / 8), littleEdian);
  offset += 2;
  // 采样位数
  data.setUint16(offset, sampleBits, littleEdian);
  offset += 2;
  // 数据标识符
  writeString(data, offset, 'data');
  offset += 4;
  // 采样数据总数,即数据总大小-44
  data.setUint32(offset, bytes.byteLength, littleEdian);
  offset += 4;

  // 给wav头增加pcm体
  for (let i = 0; i < bytes.byteLength;) {
    data.setUint8(offset, bytes.getUint8(i));
    offset++;
    i++;
  }

  return data;
}
/**
   * 获取WAV编码的二进制数据(dataview)
   *
   * @returns {dataview}  WAV编码的二进制数据
   * @memberof Recorder
   */
  getWAV() {
    let pcmTemp = this.getPCM();

    // PCM增加44字节的头就是WAV格式了
    return encodeWAV(pcmTemp, this.inputSampleRate,
      this.outputSampleRate, this.config.numChannels, this.oututSampleBits, this.littleEdian);;
  }

开始播放录音

上面拿到WAV数据之后,就可以进行播放了,播放使用window.AudioContext对象。

https://developer.mozilla.org/zh-CN/docs/Web/API/AudioContext

let audioData = this.getWAV();
let context = null;
let analyser = null;


/**
 * 初始化
 */
function init() {
  context = new(window.AudioContext || window.webkitAudioContext)();
  analyser = context.createAnalyser();
  analyser.fftSize = 2048; // 表示存储频域的大小
}

/**
 * play
 * @returns {Promise<{}>}
 */
function playAudio() {
  isPaused = false;

  return context.decodeAudioData(audioData.slice(0), buffer => {
    source = context.createBufferSource();

    // 播放结束的事件绑定
    source.onended = () => {
      if (!isPaused) { // 暂停的时候也会触发该事件
        // 计算音频总时长
        totalTime = context.currentTime - playStamp + playTime;
        endplayFn();
      }

    }

    // 设置数据
    source.buffer = buffer;
    // connect到分析器,还是用录音的,因为播放时不能录音的
    source.connect(analyser);
    analyser.connect(context.destination);
    source.start(0, playTime); // 开始播放

    // 记录当前的时间戳,以备暂停时使用
    playStamp = context.currentTime;
  }, function (e) {
    throwError(e);
  });
}

AudioContext接口的 decodeAudioData() 方法可用于异步解码音频文件中的 ArrayBuffer。ArrayBuffer 数据可以通过 XMLHttpRequest 和 FileReader 来获取。AudioBuffer 是通过 AudioContext 采样率进行解码的,然后通过回调返回结果。

暂停播放

点击暂停之后,又触发暂停,所以需要获取到最新一次暂停的时间戳

/**
   * 暂停播放录音
   * @memberof Player
   */
  function pausePlay() {
    destroySource();
    // 多次暂停需要累加
    playTime += context.currentTime - playStamp;
    isPaused = true;
  }

恢复播放

播放的时候,记录了播放的时间戳,就是为了恢复播放的时候使用

/**
   * 暂停播放录音
   * @memberof Player
   */
  function pausePlay() {
    destroySource();
    // 多次暂停需要累加
    playTime += context.currentTime - playStamp;
    isPaused = true;
  }

结束播放

/**
   * 停止播放
   * @memberof Player
   */
  function stopPlay() {
    playTime = 0;
    audioData = null;

    destroySource();
  }

// 销毁source, 由于 decodeAudioData 产生的source每次停止后就不能使用,所以暂停也意味着销毁,下次需重新启动。
function destroySource() {
  if (source) {
    source.stop();
    source = null;
  }
}

下载

其实上面已经拿到WAV数据了,就很好实现下载了。

下载就是创建一个a标签,实现下载功能,拿到Blob数据之后,就可以直接调用下面方法

通用下载方法

/**
 * 下载录音文件
 * @private
 * @param {*} blob      blob数据
 * @param {string} name 下载的文件名
 * @param {string} type 下载的文件后缀
 */
function _download(blob, name, type) {
  let oA = document.createElement('a');

  oA.href = window.URL.createObjectURL(blob);
  oA.download = `${ name }.${ type }`;
  oA.click();
}

mav&pcm下载

下载格式,可以是wav或者pcm

一般wav格式是在pcm文件前增加44个字节的文件头

/**
 * 下载录音的wav数据
 *
 * @param {blob}   需要下载的blob数据类型
 * @param {string} [name='recorder']    重命名的名字
 */
export function downloadWAV(wavblob, name = 'recorder') {
  _download(wavblob, name, 'wav');
}

/**
 * 下载录音pcm数据
 *
 * @param {blob}   需要下载的blob数据类型
 * @param {string} [name='recorder']    重命名的名字
 * @memberof Recorder
 */
export function downloadPCM(pcmBlob, name = 'recorder') {
  _download(pcmBlob, name, 'pcm');
}

mp3下载

如果需要下载mp3

在不使用第三方库的情况下,将PCM数据转换为MP3是一个复杂的任务,因为MP3是一种有损压缩音频格式,涉及到信号处理和编码技术,比如傅立叶变换、量化、哈夫曼编码等。一种方法是使用lamejs的纯JavaScript MP3编码器,它是LAME MP3编码器的JavaScript移植版本。

// 首先引入lamejs库
import { Mp3Encoder } from 'lamejs';


function convertToMp3 (wavDataView) {
  // 获取wav头信息
  const wav = lamejs.WavHeader.readHeader(wavDataView); // 此处其实可以不用去读wav头信息,毕竟有对应的config配置
  const { channels, sampleRate } = wav;
  // 设置一些音频参数
  let mp3Encoder = new Mp3Encoder(channels, sampleRate, 128); // 2表示立体声, 44100表示采样率, 128表示比特率
  
  // 获取左右通道数据
  const result = recorder.getChannelData()
  const buffer = [];

  const leftData = result.left && new Int16Array(result.left.buffer, 0, result.left.byteLength / 2);
  const rightData = result.right && new Int16Array(result.right.buffer, 0, result.right.byteLength / 2);
  const remaining = leftData.length + (rightData ? rightData.length : 0);

  const maxSamples = 1152;
  for (let i = 0; i < remaining; i += maxSamples) {
      const left = leftData.subarray(i, i + maxSamples);
      let right = null;
      let mp3buf = null;

      if (channels === 2) {
          right = rightData.subarray(i, i + maxSamples);
          mp3buf = mp3Encoder.encodeBuffer(left, right);
      } else {
          mp3buf = mp3Encoder.encodeBuffer(left);
      }

      if (mp3buf.length > 0) {
          buffer.push(mp3buf);
      }
  }

  const enc = mp3Encoder.flush();

  if (enc.length > 0) {
      buffer.push(enc);
  }

  return new Blob(buffer, { type: 'audio/mp3' });
}

上传

得到Blob数据,对于上传到云服务器,就是很简单的事情了

具体可以看腾讯云文档:

https://cloud.tencent.com/document/product/436/64960

async uploadRecorder(blobData) {
  const fileName = `recorder.wav`
  const ossDirPath = ''
  const cutImgFile = new File([blobData], fileName, {
    type: 'audio/wav',
  })
  const res = await uploadFileToCos(cutImgFile, ossDirPath)
  return res
}

录制的全部流程如下:

无标题-2023-12-12-2040.png

总结

好了,这就是录制+播放+下载+上传音频的正确方式,其实上面这些功能,就是第三方库js-audio-recorder的全部源码了

仓库:https://github.com/2fps/recorder

引入方式
  • npm方式:

安装:

npm i js-audio-recorder

调用:

import Recorder from 'js-audio-recorder';

let recorder = new Recorder();
  • script标签方式
<script type="text/javascript" src="./dist/recorder.js"></script>

let recorder = new Recorder();

具体的效果就是这样

好了,本次分享到这里就结束了~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1305458.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AMC8历年真题在线练习、解析全新按年份独立,更便捷练习和巩固

告诉大家一个好消息&#xff01; 根据家长朋友们的反馈&#xff0c;六分成长独家制作的AMC8美国数学竞赛的历年真题在练已全新架构和上线&#xff0c;改为了按年份独立一套试卷&#xff0c;这样在线练习加载更快&#xff0c;随需练习也更方便。 先来一睹为快&#xff0c;练习的…

什么是 AWS IAM?如何使用 IAM 数据库身份验证连接到 Amazon RDS(上)

驾驭云服务的安全环境可能很复杂&#xff0c;但 AWS IAM 为安全访问管理提供了强大的框架。在本文中&#xff0c;我们将探讨什么是 AWS Identity and Access Management (IAM) 以及它如何增强安全性。我们还将提供有关使用 IAM 连接到 Amazon Relational Database Service (RDS…

【Week P1】 MNIST手写数字识别

文章目录 一、环境配置1.1 安装环境1.2 设置环境&#xff0c;开始本文内容 二、准备数据三、搭建网络结构四、开始训练五、查看训练结果六、总结2.1 ⭐ torchvision.datasets.MNIST详解(Line4 & Line9)2.2 ⭐ torch.utils.data.DataLoader详解(Line4 & Line9)2.3 ⭐ sq…

《天天爱科学》期刊国家级知网投稿

《天天爱科学》国家级期刊知网收录&#xff0c;投稿方向&#xff1a;幼儿教育、基础教育文章&#xff0c;不收案例分析、教学设计、图表讲解、例题分析。 刊名&#xff1a;天天爱科学 主管单位&#xff1a;中国出版传媒股份有限公司 主办单位&#xff1a;人民文学出版社有限…

IM系统(即时通讯系统)初识

文章目录 IM系统概述即时通讯应用和即时通讯系统 现有系统添加IM功能早期即时通讯系统架构即时通讯系统的基本组成当代即时通讯系统常用架构 IM系统概述 IM是即时通讯的缩写&#xff0c;它指的是一种网络通讯技术&#xff0c;可以让用户在网络上进行实时的文字、语音、视频等多…

2023年第三季度全球SSD出货量环比增长24%,市场复苏!

根据Trendfocus发布的研究报告显示&#xff1a;2023年第三季度全球SSD出货量环比增长24%&#xff0c;达到9306万pcs&#xff0c;出货容量也增长了21%&#xff0c;达到7769EB。三星出货量市场TOP1&#xff0c;其次是WDC西部数据、金士顿、镁光Micron、海力士等。 由于PC OEM连续…

Leetcode—509.斐波那契数【简单】

2023每日刷题&#xff08;五十七&#xff09; Leetcode—509.斐波那契数 实现代码 int fib(int n){if(n 0) {return 0;}if(n 1) {return 1;}return fib(n-1) fib(n-2); }运行结果 之后我会持续更新&#xff0c;如果喜欢我的文章&#xff0c;请记得一键三连哦&#xff0c;点…

免费素材网站合集,设计师赶快收藏

设计师通常去哪里找设计素材&#xff1f; 寻找高质量、免费的设计素材&#xff0c;给大家总结了15个网站&#xff0c;平面、UI、电商、网页等都可以找到不错的设计素材&#xff0c;赶紧收藏一波~ 即时设计资源广场 即时设计资源广场拥有数万件来自优秀设计师的精美设计作品&a…

高中生应该及早接触职业性格测试

性格是我们成长过程中日渐形成的、固有的特征和行为习惯&#xff0c;性格跟我们的成长环境有很大的关系&#xff0c;比如父母的教养方式&#xff0c;父母的性格特征&#xff0c;以及我们的朋友关系&#xff0c;课堂学习&#xff0c;知识积累。这是一个无数层面的综合。 每个人…

ARM day7

题目1&#xff1a;按键中断代码编写 代码&#xff1a; main.c #include "key_it.h"#include "led.h"void delay(int ms){int i,j;for(i0;i<ms;i){for(j0;j<2000;j);}}int main(){myall_led_init();key1_it_config();key2_it_config();key3_it_conf…

PPT制作的几个注意事项

PPT制作的几个注意事项 字数不可过多字体大小字体颜色排版问题PPT篇末致谢什么是好的PPT关于演讲不要念PPT说话时面向观众。讲话的时候抖腿其他 事先声明&#xff1a; 以下展示的PPT就PPT制作技巧而言&#xff0c;与其内容无关。 字数不可过多 做PPT最忌讳的就是满篇全是文字&…

宝塔 Warning: require(): open_basedir restriction in effect

去掉网站目录下的勾选&#xff0c;防跨站攻击&#xff08;open_basedir&#xff09;,然后重启php服务。

Enabling Application Engine Tracing 启用应用程序引擎跟踪

Enabling Application Engine Tracing 启用应用程序引擎跟踪 By default, all Application Engine traces are turned off. To see a trace or a combination of traces, set trace options before you run a program. 默认情况下&#xff0c;所有应用程序引擎跟踪都处于关闭…

kernel(二):启动内核

本文主要探讨210内核启动过程。 主Makefile 定义kernel版本号(2.6.35.7) VERSION 2PATCHLEVEL 6SUBLEVEL 35EXTRAVERSION .7 指定编译文件生成目录 make O/tmp 定义交叉编译工具链 CROSS_COMPILE ? /root/arm-2009q3/bin/arm-none-linux-gnueabi- 指定架构 ARCH …

网络安全公司梳理,看F5如何实现安全基因扩增

应用无处不在的当下&#xff0c;从传统应用到现代应用再到边缘、多云、多中心的安全防护&#xff0c;安全已成为企业数字化转型中的首要挑战。根据IDC2023年《全球网络安全支出指南》&#xff0c;2022年度中国网络安全支出规模137.6亿美元&#xff0c;增速位列全球第一。有专家…

「PPT 下载」Google DevFest Keynote | 复杂的海外网络环境下,如何提升连接质量

&#xff08;全网都在找的《社交泛娱乐出海作战地图》&#xff0c;点击获取&#x1f446;&#xff09; 12 月 10 日&#xff0c;“Google DevFest 2023 上海站”大会如期在上海市东方万国宴会中心举办。延续过往的技术交流碰撞、前沿技术学习基调传统&#xff0c;本届大会聚焦行…

CS110L 系统编程安全 笔记

用户向程序输入数据&#xff0c;程序分析数据&#xff0c;但是当用户的输入大于缓冲区长度时&#xff0c;数据会溢出&#xff0c;覆盖掉内存中其他内容&#xff0c;比如函数返回地址&#xff0c;从而可能导致程序返回到错误的地址执行了不安全的程序&#xff08;远程代码执行&a…

每日一练2023.12.6——Left-pad【PTA】

题目链接&#xff1a;L1-032 Left-pad 题目要求&#xff1a; 根据新浪微博上的消息&#xff0c;有一位开发者不满NPM&#xff08;Node Package Manager&#xff09;的做法&#xff0c;收回了自己的开源代码&#xff0c;其中包括一个叫left-pad的模块&#xff0c;就是这个模块…

C++STL库的 deque、stack、queue、list、set/multiset、map/multimap

deque 容器 Vector 容器是单向开口的连续内存空间&#xff0c; deque 则是一种双向开口的连续线性空 间。所谓的双向开口&#xff0c;意思是可以在头尾两端分别做元素的插入和删除操作&#xff0c;当然&#xff0c; vector 容器也可以在头尾两端插入元素&#xff0c;但是在其…

bug-ku--计算器

F12 maxlength"1" 限制的是你能输入几位数 改成3就行 来那个数相相加就能输入了 flag{464f5f406e7e182014500fc49f7aedfc}