音频之旅(一)

1,459 阅读5分钟

由于业务需求要做一个web录音并实时上传到服务器的功能。以前从未接触过音频相关的东西,只能慢慢一步一步的摸索。

业务需求:web采集 采样频率1.6khz 采样深度16位 单声道道的 pcm 文件流,并实时通过websocket 上传

  1. 本人作为小白不禁要问: 采样频率是啥?采样位数是啥?这都是尼玛啥?

在此要科普下音频的几个基本概念:

采样频率: 采样设备(麦克风)每秒进行的采样次数(每台电脑的麦克风都有固定的采样频率,可以通过 AudioContext 对象进行查看,这个对象后续介绍。

采样位数: 简单理解为每次采样所占的位数。后续要加深理解

采样声道: 一般分为单声道和双声道 可以理解为在声音采集时从一个点进行收集,双声道从两个点进行收集

  1. 基本概念有了一个了解了,那web该如何采集录音呢?

先看如下模型图:

首先要获取设备的权限: navigator.getUserMedia

// 第一个参数为 要获取的权限类型 audio为录音设备权限 video为摄像头权限
// 第二个参数为 设备录音过程中产生的音频流(此音频流js无法直接操作)
// 第三个参数为 错误的回调
navigator.getUserMedia({audio:true},(stream)=> {},(err)=>{})
// 成功执行此代码后设备就开始录音了

到此,音频流stream是获取到了,我们该如何操作呢?AudioContext登场,这个对象是专门处理音频数据的。从模型图上我们还可以更形象的理解这个对象是一个工厂,这个工厂进口的原材料就是stream,这个工厂有多个部门:有负责接受stream的部门;有控制音量的部门creatGain;有获取频谱的部门createAnalyser;..等等

首先,stream进来就要被接受,stream的来源不同,负责接受的部门也不同。

// 1. 关联HTMLMediaElement. 这可以用来播放和处理来自<video>或<audio> 元素的音频.
audioInput = audioCtx.createMediaElementSource(stream) 
// 2.关联可能来自本地计算机麦克风或其他来源的音频流
audioInput = audioCtx.createMediaStreamSource(stream)

stream已被相应的部门接受了,js又该如何处理这些数据,然后得到我们想要的信息呢?不过可惜的是js无法直接操作audioInput,还需要这个工厂另一个部门createScriptProcessor的帮助

// 这个部门负责创建一个js可以操作的对象scriptProcessor
// 参数: bufferSize: 缓冲区的大小  后两个参数表示单声道
scriptProcessor = audioCtx.createScriptProcessor(bufferSize, 1, 1)
// 对象创建好了,但音频流audioInput并不在这个部门
// audioInput音频流要 送到 这个 scriptProcessor部门 部门之间协作用 connect
audioInput.connect(scriptProcessor)
// 到此 可以在scriptProcessor的对象中用js来操作音频了
// scriptProcessor在创建的时有一个缓存区大小参数,当音频文件达到这个大小时会触发onaudioprocess
scriptProcessor.onaudioprocess = (e) => {
    let pcm = e.inputBuffer.getChannelData(0) // 在此我们得到音频pcm的流
}

到此,我们已经拿到了可以让js来操作的音频pcm流了。但是不是我们需要的采样频率16khz,采样位数16bit的pcm呢?

不一定,我们知道每个设备的采样频率是固定的,并且在采样时是无法修改的。可以在

audioCtx.sampleRate // 查看采样频率 48000(每个设备的不同)

但获取的是采样频率为48k的pcm,那如何转变为16k的呢?

// 采样频率只能降级处理
// 参数:buffer:为我们获取的pcm流 rate为要转成的采样频率
export function downsampleBuffer(buffer, rate) {
  let audioContext = new AudioContext()
  let sampleRate = audioContext.sampleRate; // 当前设备采样频率
  if (rate == sampleRate) {
    return buffer;
  }
  if (rate > sampleRate) {
    throw "downsampling rate show be smaller than original sample rate";
  }
  var sampleRateRatio = sampleRate / rate;
  var newLength = Math.round(buffer.length / sampleRateRatio);
  var result = new Float32Array(newLength);
  var offsetResult = 0;
  var offsetBuffer = 0;
  while (offsetResult < result.length) {
    var nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
    var accum = 0, count = 0;
    for (var i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
      accum += buffer[i];
      count++;
    }
    result[offsetResult] = accum / count;
    offsetResult++;
    offsetBuffer = nextOffsetBuffer;
  }
  return result;
}

**注:对采样频率的降级处理原理目前尚不清楚,只贴出转换代码

16k的采样频率也搞定了,那16bit呢?

// 设置采样位数 设置采样位数为16
// 参数 samples 为pcm流
export function floatTo16BitPCM(samples) {
  let offset = 0
  var buffer = new ArrayBuffer(samples.length * 2)
  var view = new DataView(buffer)

  for (var i = 0; i < samples.length; i++ , offset += 2) {
    var s = Math.max(-1, Math.min(1, samples[i]));
    view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
  }

  return view
}

**注:对此原理也不是很清楚,目前只贴代码。

功夫不负有心人,16k 16bit 单声道音频数据获取到了。可以向websocket发送了?天真了。

scriptProcessor.onaudioprocess = (e) => {
    let pcm = e.inputBuffer.getChannelData(0) // 在此我们得到音频pcm的流
    let pcm_16kh = downsampleBuffer(pcm,16000) // 转成16kh采样频率
    let pcm_16kh_16bit = floatTo16BitPCM(pcm_16kh) // 转成16kh_16bit
}

看看这个函数多久触发一次就知道了,这样向socket发送就jj了。 这里就需要先存起来,每隔固定的一段时间向sockt发送

// pcm 其实就是二进制buffer
var pcms=[], pcmLength=0;
scriptProcessor.onaudioprocess = (e) => {
    let pcm = e.inputBuffer.getChannelData(0) // 在此我们得到音频pcm的流
    pcms.push(pcm)
    pcmLength += pcm.length
    // let pcm_16kh = downsampleBuffer(pcm,16000) // 转成16kh采样频率
    // let pcm_16kh_16bit = floatTo16BitPCM(pcm_16kh) // 转成16kh_16bit
}

setInterval(() => {
    let subPcms = mergeBuffers(pcms,pcmLength)
    let sub_pcm_16kh = downsampleBuffer(subPcms,16000) // 转成16kh采样频率
    let sub_pcm_16kh_16bit = floatTo16BitPCM(sub_pcm_16kh) // 转成16kh_16bit
    pcms = [];pcmLength = 0; // 别忘记重置
    websocket.send(sub_pcm_16kh_16bit) // 可以愉快的发送了
},1000)

// 合并buffer
export function mergeBuffers(recBuffers, recLength) {
  var result = new Float32Array(recLength);
  var offset = 0;
  for (var i = 0; i < recBuffers.length; i++) {
    result.set(recBuffers[i], offset);
    offset += recBuffers[i].length;
  }
  return result;
}

作为一个刚接触web音频处理的小白,还有许多东西需要了解。