最近工作的时候发现了这样的一个问题,amr格式的文件audio标签不能解码播放。
查了下mdn相关资料,发现确实是不支持amr后缀的文件
一般情况下是不会有这样的需求的,问题是当后端不愿意去做这个事的时候,就得想办法了。
上网查资料发现了这么一个解码amr的库
那我们要做的就是在此基础上提供的api来封装一个audio播放器,忽略UI因素,要满足如下几个场景
- 支持播放暂停
- 支持进度条显示
- 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代码实现的,所以问题就是
- 需要判断当前文件是amrwb还是amrnb,使用对应的解码方式
- 对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上做了兼容,可以放心使用。