amr文件播放前端实现

1,605 阅读2分钟

最近工作的时候发现了这样的一个问题,amr格式的文件audio标签不能解码播放。

查了下mdn相关资料,发现确实是不支持amr后缀的文件

一般情况下是不会有这样的需求的,问题是当后端不愿意去做这个事的时候,就得想办法了。

上网查资料发现了这么一个解码amr的库

那我们要做的就是在此基础上提供的api来封装一个audio播放器,忽略UI因素,要满足如下几个场景

  1. 支持播放暂停
  2. 支持进度条显示
  3. amr和普通文件可以使用同一套api

于是基于原生audio的一些特性和解码库,封装了一个工具库

这里放一个基于vue二次封装使用的例子,有同样需求的朋友可以使用,欢迎提建议和star

<template>
  <div class="player" v-loading="loading">
    <i
      :class="state==='pause'?'el-icon-video-play btn':'el-icon-video-pause btn'"
      @click="playOrPause"
    ></i>
    <el-progress :percentage="percentage" :show-text="false"></el-progress>
  </div>
</template>

<script >
import { AudioPlayer } from "audio-amr-player";

export default {
  data() {
    return {
      percentage: 0,
      state: "pause",
      loading: true
    };
  },
  props: {
    url: {
      type: String
    },
    file: {
      type: File,
      validator(val) {
        return val instanceof File;
      }
    }
  },

  watch: {
    url() {
      this.init();
    },
    file() {
      this.init();
    }
  },

  mounted() {
    this.init();
  },

  methods: {
    playOrPause() {
      if (this.state === "pause") {
        this.state = "play";
        this.player.play();
      } else {
        this.state = "pause";
        this.player.pause();
      }
    },
    init() {
      let _this = this;
      this.player = new AudioPlayer({
        url: this.url,
        file: this.file,
        afterInit() {
          _this.loading = false;
        }
      });
      this.player.onTimeUpdate(time => {
        let rate =
          time > this.player.duration ? 1 : time / this.player.duration;
        this.percentage = rate * 100;
      });
      this.player.onEnd(() => {
        this.state = "pause";
      });
    }
  },
  beforeDestroy() {
    this.player.destroy();
  }
};
</script>

在后续的使用中,发现直接把amr转为wav更简单一些,对于amrnb有现成的方法

本来以为一般来说录音不会出现amrwb的情况,结果是还是有的,经过搜索,发现AMRWB也是有对应的解码的js代码实现的,所以问题就是

  1. 需要判断当前文件是amrwb还是amrnb,使用对应的解码方式
  2. 对amrwb做出一个可行的转wav的方案

对于第一个问题,同一种文件的文件信息头部分是一致的,这里可以很容易去判断

   const headerInfo = Array.from(U8Array.slice(0, 6)).toString()
    if (headerInfo === '35,33,65,77,82,10') {
            wavU8Array = AMR.toWAV(U8Array) //nb
    }else {
      //wb
    }

关于第二个问题,现在搜到的一些库大多是nodejs相关的库,和我们的要求不吻合,所以没办法,硬啃amrnb的towav方法吧

function samplesToWAV(samples: Uint8Array) {
  const out = new Uint8Array(samples.length + 44)
  let offset = 0
  const write_int16 = function (value: number) { const a = new Uint8Array(2); (new Int16Array(a.buffer))[0] = value; out.set(a, offset); offset += 2 }
  const write_int32 = function (value: number) { const a = new Uint8Array(4); (new Int32Array(a.buffer))[0] = value; out.set(a, offset); offset += 4 }
  const write_string = function (value: string) { const d = (new TextEncoder()).encode(value); out.set(d, offset); offset += d.length }
  write_string('RIFF')
  write_int32(36 + samples.length)
  write_string('WAVEfmt ')
  write_int32(16)
  const bits_per_sample = 8
  const sample_rate = 16000
  const channels = 1
  const bytes_per_frame = bits_per_sample / 8 * channels
  const bytes_per_sec = bytes_per_frame * sample_rate
  write_int16(1); write_int16(1); write_int32(sample_rate)
  write_int32(bytes_per_sec); write_int16(bytes_per_frame)
  write_int16(bits_per_sample); write_string('data')
  write_int32(samples.length)
  out.set(samples, offset)
  return out
}

这里一些参数调整下,注意wb的频率一般以16000来解析,然而还是不对,这不禁让我产生了怀疑。后面才发现是这个方法传入的Unit8Array的参数,而AMRWB.decode出来的是float32Array,所以还得做一次转换

function float32Array2Uint8Array(float32Array: any) {
  const len = float32Array.length
  const output = new Uint8Array(len)

  for (let i = 0; i < len; i++) {
    let tmp = Math.max(-1, Math.min(1, float32Array[i]))
    tmp = tmp < 0 ? tmp * 0x8000 : tmp * 0x7FFF
    tmp = tmp / 256
    output[i] = tmp + 128
  }

  return output
}

至此 amr两种频段都完成了兼容,目前也在npm上做了兼容,可以放心使用。