IESM项目实训四
通过浏览器录制音频,生成可供百度语音识别api使用的pcm音频文件,然后将音频数据传至后端。根据百度语音识别api文档要求,音频文件为pcm格式,单音道,16k采样率,16位深。
1.三个基本概念
音频源,也就是音频输入,可以是直接从设备输入的音频,也可以是远程获取的音频文件。
处理节点,分析器和处理器,比如音调节点,音量节点,声音处理节点。
输出源,指音频渲染设备,一般情况下是用户设备的扬声器,即context.destination。其实,音频源和输出源也都可以视为节点,这三者的关系可以用这张图表示:
2.使用Web Audio进行录音
录音过程如下:
- 输入源
getUserMedia()
首先调用用户的麦克风设备,需要用到navigator.mediaDevices.getUserMedia()
这个方法,该方法会返回一个promise,成功回调的参数是一个MediaStream对象,该对象就是麦克风采集的语音。
然后该MediaStream流媒体对象需要传入createMediaStreamSource()方法中,用于创建一个新的MediaStreamAudioSourceNode对象,也就是创建了一个输入源节点,从而使得来自MediaStream的音频可以被播放和操作。
navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) { // 务必分开写then
var audioContext = new AudioContext()
var sourceNode = audioContext.createMediaStreamSource(stream) //sourceNode就是得到的输入源节点
})
2.处理节点createScriptProcessor()
createScriptProcessor()方法用于创建一个处理节点,该处理节点有三个参数:
- bufferSize,缓冲区大小,以样本帧为单位。一般有以下值 256,512,1024,2048,4096,8192,16384。当传0时,系统会取当前环境最合适的缓冲区大小。每当缓冲区满时,则会触发audioprocess事件,即bufferSize控制着回调事件的频率。注:mdn提示 chrome 31版本的不支持传0的方式。
- numberOfInputChannels,值为整数,用于指定输入node的声道的数量,默认值是2,最高能为32,且不能为0。
- numberOfOutputChannels,值为整数,用于指定输出node的声道的数量,默认值是2,最高能取32,不能为0。
由于百度语音识别api要求使用单音道,且采样率为16k,位深为16bit;而实际麦克风的录音的采样率为44.1k,双音道,故需要对输入源的音频进行处理,即通过监听onaudioprocess即可对音频信号进行处理。
var scriptNode = audioContext.createScriptProcessor(4096, 1, 1)
scriptNode.onaudioprocess = event => {
audioData.input(event.inputBuffer.getChannelData(0)) //取单音道信号
}
//对音频信号进行处理
var audioData = {
size: 0,
buffer: [],
input: function (data) {
this.buffer.push(new Float32Array((data)))
this.size += data.length
},
//得到格式为pcm,采样率为16k,位深为16bit的音频文件
getData: function () {
var sampleBits = 16
var inputSampleRate = 44100
var outputSampleRate = 16000
var bytes = this.decompress(this.buffer, this.size, inputSampleRate, outputSampleRate)
var dataLen = bytes.length * (sampleBits / 8)
var buffer = new ArrayBuffer(dataLen) // For PCM , 浏览器无法播放pcm格式音频
var data = new DataView(buffer)
var offset = 0
data = this.reshapeData(sampleBits, offset, bytes, data)
return new Blob([data], { type: 'audio/pcm' })
},
// 将收到的音频信号进行预处理,即将二维数组转成一维数组,并且对音频信号进行降采样
decompress: function (buffer, size, inputSampleRate, outputSampleRate) {
var data = new Float32Array(size)
var offset = 0
for (var i = 0; i < buffer.length; i++) {
data.set(buffer[i], offset)
offset += buffer[i].length
}
// 降采样,采取每interval长度取一个信号点的方式
var interval = parseInt(inputSampleRate / outputSampleRate)
var length = data.length / interval
var result = new Float32Array(length)
var index = 0; var j = 0
while (index < length) {
result[index] = data[j]
j += interval
index++
}
return result
},
//将音频信号转为16bit位深
reshapeData: function (sampleBits, offset, bytes, data) {
var s
for (var i = 0; i < bytes.length; i++, offset += (sampleBits / 8)) {
s = Math.max(-1, Math.min(1, bytes[i]))
data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true)
}
return data
}
3.输出源audioContext.destination
以上三个节点Source Node,scriptNode 和 audioContext.destination通过connect相连。
sourceNode.connect(scriptNode) //输入源节点连接到处理节点
scriptNode.connect(audioContext.destination) //处理节点连接到输出源节点
3.完整代码如下
recorder.js
import request from "request";
function Recorder (stream) {
var audioContext = new AudioContext()
var sourceNode = audioContext.createMediaStreamSource(stream)
var scriptNode = audioContext.createScriptProcessor(4096, 1, 1)
var audioData = {
size: 0,
buffer: [],
input: function (data) {
this.buffer.push(new Float32Array((data)))
this.size += data.length
},
getData: function () {
var sampleBits = 16
var inputSampleRate = 44100
var outputSampleRate = 16000
var bytes = this.decompress(this.buffer, this.size, inputSampleRate, outputSampleRate)
var dataLen = bytes.length * (sampleBits / 8)
var buffer = new ArrayBuffer(dataLen) // For PCM , 浏览器无法播放pcm格式音频
var data = new DataView(buffer)
var offset = 0
data = this.reshapeData(sampleBits, offset, bytes, data)
return new Blob([data], { type: 'audio/pcm' })
},
// 将二维数组转成一维数组
decompress: function (buffer, size, inputSampleRate, outputSampleRate) {
var data = new Float32Array(size)
var offset = 0
for (var i = 0; i < buffer.length; i++) {
data.set(buffer[i], offset)
offset += buffer[i].length
}
// 降采样
var interval = parseInt(inputSampleRate / outputSampleRate)
var length = data.length / interval
var result = new Float32Array(length)
var index = 0; var j = 0
while (index < length) {
result[index] = data[j]
j += interval
index++
}
return result
},
reshapeData: function (sampleBits, offset, bytes, data) {
var s
for (var i = 0; i < bytes.length; i++, offset += (sampleBits / 8)) {
s = Math.max(-1, Math.min(1, bytes[i]))
data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true)
}
return data
}
}
// 监听录音的过程
var buffers = []
scriptNode.onaudioprocess = event => {
audioData.input(event.inputBuffer.getChannelData(0))
}
this.start = function () {
sourceNode.connect(scriptNode)
scriptNode.connect(audioContext.destination)
}
this.stop = function () {
sourceNode.disconnect()
scriptNode.disconnect() }
this.getBlob = function () {
return audioData.getData()
}
this.clear = function() {
audioData.clear();
}
}
Recorder.get = function (callback) {
navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) { // 务必分开写then
var rec = new Recorder(stream)
callback(rec)
})
}
export { Recorder }
export function remoteControl(num,roId) {
return request({
url:'/ScoresInput/scores/voiceInputScores',
method: 'post',
params: {
num,
roId
}
})
}
ScoreInputList.vue
成绩录入界面使用到音频录制,后续需要进行修改
//开始录制方法,如需开始录制音频,可如下
startRecording: function () {
Recorder.get(function (rec) {
recorder = rec
recorder.start()
})
},
//停止音频录制
stopRecording: function () {
recorder.stop();
console.log('已暂停');
},
//前期测试录制的音频文件是否正确
getRecording: function () {
var voiceBlobFile = recorder.getBlob(); //得到需要的pcm文件
saveAs(voiceBlobFile, "test8.pcm");//存入本地
console.log(voiceBlobFile);
},
beforeDestroy:function () {
this.$once('hook:beforeDestroy', () => {
if (recorder !== null) {
recorder.stop();
recorder = null;
}
})
}
考虑将名字转换为拼音
因为名字不算日常用语,百度语音识别结果不能与真实的姓名一致,但是会发音一致,为了提高准确率,将识别的姓名转为拼音再进行对比。
需要添加依赖:
<!--拼音依赖-->
<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
<version>2.5.0</version>
</dependency>
工具类:
将字符串转为拼音
import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
import net.sourceforge.pinyin4j.format.HanyuPinyinVCharType;
import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;
public class pingyin {
public static String getPinyin(String src) {
char[] t1 = null;
t1 = src.toCharArray();
String[] t2 = new String[t1.length];
HanyuPinyinOutputFormat t3 = new HanyuPinyinOutputFormat();
t3.setCaseType(HanyuPinyinCaseType.LOWERCASE);// 小写格式
t3.setToneType(HanyuPinyinToneType.WITHOUT_TONE);// 有无音标
t3.setVCharType(HanyuPinyinVCharType.WITH_V);
String t4 = "";
try {
for (int i = 0; i < t1.length; i++) {
// 判断是否为汉字字符
// if(t1[i] >= 32 && t1[i] <= 125)//ASCII码表范围内直接返回
if (String.valueOf(t1[i]).matches("[\\u4E00-\\u9FA5]+")) {
t2 = PinyinHelper.toHanyuPinyinStringArray(t1[i], t3);// 转化为拼音
t4 += t2[0].substring(0, 1).toUpperCase() + t2[0].substring(1);// 首字母大写
} else {
t4 += String.valueOf(t1[i]);// 不是汉字不处理
}
}
} catch (BadHanyuPinyinOutputFormatCombination e1) {
e1.printStackTrace();
}
return t4;
}
}