录音播放功能实现

939 阅读5分钟

1. 录音播放功能实现

背景:业务中需要在前端页面上实现录音的播放功能,可能选择位置、速度进行播放,以及实时当前播放位置对应的语音转写文本。

录音文件通过 http 方式获取,服务器不进行格式转换。所以会出现 wav/flac/pcm 这些前端不能直接播放的声音格式,针对这些问题进行技术调研和验证。

1.1. 目前可用的开源方案调研

1.1.1. 音乐播放器 aplayer

6.2k star

成熟度高,功能完整、维护时间长。

项目地址: github.com/DIYgod/APla…

调研结果:

这是一个比较类似音乐播放器的项目,本身提供了一些音乐播放基本的API,例如歌词显示、播放、停止、选择位置等。如果格式情况满足要求的的话经过一些修改用到项目中。

声音格式支持为:

  • mp3
  • acc
  • wave pcm - 不直接支持 pcm
  • ogg
  • falc - 不支持 falc

转写文本(歌词)

  • lrc

使用示例

const ap = new APlayer({
  container: document.getElementById('aplayer'),
  audio: [{
    name: 'name',
    artist: 'artist',
    url: 'url.mp3',
    cover: 'cover.jpg'
  }]
});

1.1.2. 声音播放库 Howler

20.4k star

成熟度高,仅用于专门播放声音、维护时间长。

项目地址: github.com/goldfire/ho…

调研结果:

这个库用于专门播放声音,特别用于多声音文件合并播放、合并处理。可以自动缓存加载过的音乐。支持声音分段播放。

声音格式支持为:

  • mp3
  • acc
  • pcm - 不支持 pcm
  • ogg
  • falc
  • wav

转写文本(歌词)

  • 不支持

使用示例 基本示例:

import {Howl, Howler} from 'howler';

// 初始化一个音频类
const sound = new Howl({
  src: ['sound.webm', 'sound.mp3']
});

// 播放音频
sound.play();

// 改变全局音频声音大小
Howler.volume(0.5);

// 只想改变某个音频的大小可以在初始化的时候修改
const sound = new Howl({
  src: ['sound.webm', 'sound.mp3'],
  volume:0.5
});

定义和播放某一部分的音频:

var sound = new Howl({
  urls: ['sounds.mp3', 'sounds.ogg'],
  sprite: {
    blast: [0, 1000],
    laser: [2000, 3000],
    winner: [4000, 7500]
  }
});

// shoot the laser!
sound.play('laser');

1.2. 已知开源方案横向对比进行选择

如果是做一个类似 mp3 播放器列表的形式的话偏向于选择 aplayer, 因为它比较接近。同时还支持歌词。

但当前业务是对单个文件进行分段播放比较多,样式和交互与 aplayer 提供的有较大的差别,虽然 aplayer 提供了歌词功能,我们可以把自己的语音转写文本转换为符合规范的 lrc 文件。

但当前原型的交互也与 aplayer 提供的歌词显示情况很不一致,对于已经为播放器而生的 aplayer 封装来说,要修改这些交互和样式作出的成本很高。

howler 只提供播放声音和部分我们可以使用的声音操作 api,不提供声音与对应的文本播放功能,不提供播放界面。 但是它提供的 api 和支持的格式相比于 aplayer 来说对于我们的可用性更多一些。

综上所述:

要求aplayerhowler备注
常见格式 mp3/acc/ogg/wav[x][x]--
无损音频 flac[ ][x]--
声音源文件 pcm[ ][ ]--
分段播放[ ][x]--
转写(歌词)[x][ ]与原型差别较大
播放器界面[x][ ]与原型差别较大
播放速度控制[ ][x]

选择: aplayer 所提供的界面难以复用,并且不支持 flac,所以我们选择 howler。

1.3. 需要额外处理的

就仅上述要求而言,我们需要做以下事情:

  • 处理 pcm 的支持情况
  • 验证 howler 的 api 和格式
  • 根据原型和UI开发交互界面

1.3.1. 处理 pcm 的支持情况

根据研究, pcm 格式可以简单理解为是模板信息的数字形式存储,不属于完整的直接可播放的声音文件。一般可直接播放的声音文件中,包含了一部分信息,这部分信息用于描述自身的一些属性以让播放器知道如何播放自己。

有一些开源的库可以对 pcm 直接播放,但由于这里我们要兼容其他格式,要使用 howler 来进行播放,所以不能直接使用这些播放 pcm 的库。

了解到与 pcm 想近的格式是 wav,所以这里我们把它转换为 wav,一般情况下,wav数据实际上就是裸数据pcm外面包了一层文件头。

wav头如下图表示: wav头

这里附上可用的转换代码,若需要了解具体原理请参考相关文章。

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

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

在本地调试的时候,可以使用 audacity 进行播放验证。 Audacity 是一个易用、多轨音频录制和编辑的自由、开源、跨平台音乐软件。

1.3.2. 验证 howler 的 api 和格式

pcm、wav、m4a、flac、aiff、m4a、mp3、ogg 这些格式的声音文件以及 播放、时间跳转、停止、下载、速度 功能进行尝试,均可正常使用。

以下为测试代码, 相关的测试文件以附件方式与本文一起提供:

<template>
  <div class="test">
    <div v-for="item in audioHowlList" :key="item.link">
      <div>{{ item.link }}</div>
      <button @click="playItem(item)">播放</button>
      <button @click="item.howl.seek(5)">时间跳转</button>
      <button @click="item.howl.stop()">停止</button>
      <button @click="downItem(item)">下载</button>
      <button @click="item.howl.rate(0.5)">速度 0.5</button>
      <button @click="item.howl.rate(4.0)">速度 4.0</button>
      <button @click="item.howl.rate(1.0)">速度 1.0</button>
      <hr />
    </div>
  </div>
</template>
<script>
import { Howl, Howler } from 'howler'

export default {
  name: `Home`,
  data() {
    return {
      audioHowlList: [],
      audioList: [
        `/audio/2channels-16bit-8kHz.pcm`,
        `/audio/pcm1608s.wav`,
        `/audio/Sample_BeeMoved_48kHz16bit.m4a`,
        `/audio/Sample_BeeMoved_96kHz24bit.flac`,
        `/audio/AIFF-Testfile.aiff`,
        `/audio/ALAC-Testfile.m4a`,
        `/audio/MP3-Testfile.mp3`,
        `/audio/OGG-Testfile.ogg`,
      ],
    }
  },
  components: {},
  async mounted() {
    window.ctx = Howler.ctx
    let res = []
    for (let i = 0; i < this.audioList.length; i++) {
      let url = this.audioList[i]
      if (url.endsWith(`.pcm`)) {
        url = await window.pcmUrlToWavUrl({
          pcmUrl: `/audio/2channels-16bit-8kHz.pcm`,
          rate: 8000,
          bit: 16,
          channel: 2,
        })
      }
      console.log(`url`, url)
      res.push({
        link: url.slice(0, 100) + `...`,
        howl: new Howl({
          src: [url],
          loop: true,
        }),
      })
    }
    this.audioHowlList = res
  },
  computed: {},
  methods: {
    playItem(item) {
      Howler.stop()
      item.howl.play()
    },
    playSprite(item, spriteName) {
      Howler.stop()
      item.howl.play(spriteName)
    },
    downItem(item) {
      window.open(item.link)
    },
  },
}
</script>
<style lang="less" scoped>
.test {
}
</style>

如果需要其他格式的测试文件,可以到下面的网站上直接获取:

1.4. 参考