鸿蒙PCM录音功能开发
前言
和之前写的安卓PCM音频录制一样的需求,这里需要用鸿蒙next来实现,代码写的比较早,有些API被标记废弃了,但是不影响使用,这里简单抽时间记录下。
录音
录音配置
通过PCM录音,需要配置采样率、通道、采样格式、编码格式等:
/** 采样配置 **/
private audioStreamInfo: audio.AudioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000, // 采样率
channels: audio.AudioChannel.CHANNEL_1, // 通道
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式
}
/** 声源配置 **/
private audioCapturerInfo: audio.AudioCapturerInfo = {
source: audio.SourceType.SOURCE_TYPE_MIC,
capturerFlags: 0
};
/** 录音配置 **/
private audioCapturerOptions: audio.AudioCapturerOptions = {
streamInfo: this.audioStreamInfo,
capturerInfo: this.audioCapturerInfo
};
写好配置后,就可以获取录音器了:
// 录音
private audioCapturer: audio.AudioCapturer = null!
private isRecording: Boolean = false
/** 录音初始化 **/
private isInitRecord: Boolean = false
private async initRecord(): Promise<void> {
return new Promise((resolve, reject) => {
audio.createAudioCapturer(this.audioCapturerOptions, (err, data) => {
// 发生异常
if (err) {
LogUtil.e("createAudioCapturer fail")
reject()
}else {
// 拿到录音器
this.audioCapturer = data;
this.isInitRecord = true;
LogUtil.e("createAudioCapturer success!")
resolve()
}
});
});
}
这里代码写的不咋样,凑合看下呗。
开始录音
拿到录音器AudioCapturer后就可以录音了,记得提前申请下录音权限,前面写了工具类了,这里就不说了:
/**
* 开始录音
*
* @param context 上下文
* @param key 关键key
* @param resolve 成功回调
* @param reject 失败回调
*/
async startPcmRecord(context: Context, key: string, resolve: Function, reject: Function){
try {
// 等待初始化
if (!this.isInitRecord) {
await this.initRecord()
}
// 当且仅当状态为STATE_PREPARED、STATE_PAUSED和STATE_STOPPED之一时才能启动采集
let stateGroup = [
audio.AudioState.STATE_PREPARED,
audio.AudioState.STATE_PAUSED,
audio.AudioState.STATE_STOPPED
];
if (stateGroup.indexOf(this.audioCapturer.state) === -1) {
LogUtil.d("start record failed: state error")
reject("start record failed: state error")
return;
}
// 启动采集
await this.audioCapturer.start()
// 创建音频缓存目录
let dir = context.cacheDir + '/audio'
if (!fs.accessSync(dir)) {
fs.mkdirSync(dir)
}
let path = context.cacheDir + `/audio/${key}.wav`;
// 删除原来的文件
if (fs.accessSync(path)) {
fs.unlinkSync(dir)
}
let file: fs.File = fs.openSync(path, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
let fd = file.fd;
// option类
class Options {
offset?: number;
length?: number;
}
// 读取数据
let count = 0;
this.isRecording = true;
while (this.isRecording) {
let bufferSize = await this.audioCapturer.getBufferSize();
bufferSize = Math.max(this.PCM_PACKET_SIZE, bufferSize);
let buffer = await this.audioCapturer.read(bufferSize, true);
let options: Options = {
offset: count * bufferSize,
length: bufferSize
};
if (buffer === undefined) {
// reject("read buffer error!")
} else {
// 向文件写入
fs.writeSync(fd, buffer, options);
}
count++;
}
} catch (e) {
LogUtil.e("startPcmRecord: " + (e as BusinessError).message)
reject(e)
}
}
代码稍微有点长,不过逻辑按顺序下来还是比较简单的,最后pcm录音数据保存再我们创建的临时文件内。
这里因为要借讯飞的语音识别,我还得把数据切成一段一段,再每隔一段时间发出去,其实也好写,我这先定义了一个队列来保存,每次将数据通过enqueueData保存,就能做成分段,取的时候从队列取就可以了:
/** 数据包大小 **/
private PCM_PACKET_SIZE: number = 1280
/** 数据传输间隔 **/
private PCM_SEND_DELAY = 40
/** 数据队列,保存1280字节一个的packet **/
private mDataQueue: LinkedList<Uint8Array> = new LinkedList();
/** 上次未满 **/
private mRemainingData: Uint8Array = new Uint8Array();
// 将数据保存未 1280 字节的packet放到队列中
private enqueueData(data: Uint8Array): void {
let combinedData = new Uint8Array(this.mRemainingData.length + data.length);
combinedData.set(this.mRemainingData);
combinedData.set(data, this.mRemainingData.length);
let offset = 0;
while (offset < combinedData.length) {
const packetSize = Math.min(this.PCM_PACKET_SIZE, combinedData.length - offset);
const packet = new Uint8Array(packetSize);
packet.set(combinedData.subarray(offset, offset + packetSize));
this.mDataQueue.add(packet);
offset += packetSize;
}
this.mRemainingData = combinedData.subarray(offset);
}
数据来源也简单,在上面写入文件的地方,同事保存一份到队列:
if (buffer === undefined) {
// reject("read buffer error!")
} else {
// 向文件写入
fs.writeSync(fd, buffer, options);
// 传入队列保存
this.enqueueData(new Uint8Array(buffer))
}
启动定时也好做,在启动录音的时候加个定时器就可以了:
// 启动采集
await this.audioCapturer.start()
this.mDataQueue.clear();
// 启动定时回调
let base64 = new util.Base64Helper();
let timer = setInterval(()=>{
// 从队列取数据
if (this.mDataQueue.length > 0) {
let packet = this.mDataQueue.removeFirst();
resolve(base64.encodeToStringSync(packet));
LogUtil.d("setInterval get packet:" + JSON.stringify(packet));
}else if (!this.isRecording) {
// 队列为空且结束录音了,结束packet传输
LogUtil.d("setInterval finish");
resolve("end");
this.mDataQueue.clear();
clearInterval(timer)
}
}, this.PCM_SEND_DELAY);
这里加了个base64转换传输出去,看需要吧,最后通过resolve回调的。
结束录音
录音的代码比较多,结束录音就简单了,没什么好说的,甚至可以不管异常一行代码解决:
/**
* 关闭录音
*/
stopPcmRecord(){
// 只有采集器状态为STATE_RUNNING或STATE_PAUSED的时候才可以停止
if (this.audioCapturer.state.valueOf() !== audio.AudioState.STATE_RUNNING &&
this.audioCapturer.state.valueOf() !== audio.AudioState.STATE_PAUSED) {
LogUtil.d("stop record fail: state not right")
return;
}
this.audioCapturer.stop((err: BusinessError) => {
if (err) {
LogUtil.d("stop record fail: " + err.message)
} else {
this.isRecording = false
}
});
}
播放
音频播放配置
要播放PCM音频也简单,和录音差不多,先配置下:
/** 播放音频配置 **/
private audioRendererInfo: audio.AudioRendererInfo = {
usage: audio.StreamUsage.STREAM_USAGE_MUSIC, // 音频流使用类型
rendererFlags: 0 // 音频渲染器标志
}
/** 播放配置 **/
private audioRendererOptions: audio.AudioRendererOptions = {
streamInfo: this.audioStreamInfo,
rendererInfo: this.audioRendererInfo
}
// 播放器
private audioRenderer: audio.AudioRenderer = null!
/** 播放器初始化 **/
private isInitAudio: Boolean = false
private async initAudio(): Promise<void> {
return new Promise((resolve, reject) => {
audio.createAudioRenderer(this.audioRendererOptions, (err, renderer) => {
// 发生异常
if (err) {
reject()
}else {
// 拿到录音器
this.audioRenderer = renderer;
this.isInitAudio = true;
resolve()
}
});
});
}
播放PCM音频
拿到录音器audioRenderer后,就可以播放音频了:
private currentPlayingCount = 0;
/**
* 播放PCM音频
*/
async playPcmRecord(context: Context, key: string, resolve: Function, reject: Function) {
try {
// 创建音频缓存路径,文件不存在直接返回
let path = context.cacheDir + `/audio/${key}.wav`;
if (!fs.accessSync(path)) {
reject("file not exit")
return
}
if (!this.isInitAudio) {
await this.initAudio()
}
// 只有渲染器状态为running或paused的时候才可以停止
if (this.audioRenderer.state == audio.AudioState.STATE_RUNNING ||
this.audioRenderer.state == audio.AudioState.STATE_PAUSED) {
await this.audioRenderer.stop();
}
// 自旋等待
this.currentPlayingCount++;
while (this.currentPlayingCount > 1) {
}
// 当且仅当状态为prepared、paused和stopped之一时才能启动渲染
let stateGroup = [
audio.AudioState.STATE_PREPARED,
audio.AudioState.STATE_PAUSED,
audio.AudioState.STATE_STOPPED
];
if (stateGroup.indexOf(this.audioRenderer.state.valueOf()) === -1) {
LogUtil.e("play pcm failed: state error")
reject("play pcm failed: state error")
return;
}
// 启动渲染
await this.audioRenderer.start();
// option类
class Options {
offset?: number;
length?: number;
}
// 开始向音频播放器写入数据
let bufferSize = await this.audioRenderer.getBufferSize();
let file = fs.openSync(path, fs.OpenMode.READ_ONLY);
let stat = await fs.stat(path);
let buf = new ArrayBuffer(bufferSize);
// 读取次数
let len = stat.size % bufferSize === 0 ? Math.floor(stat.size / bufferSize) : Math.floor(stat.size / bufferSize + 1);
for (let i = 0; i < len && this.currentPlayingCount <= 1; i++) {
// 按缓存长度读取
let options: Options = {
offset: i * bufferSize,
length: bufferSize
};
// 读取文件
await fs.read(file.fd, buf, options);
// buf是要写入缓冲区的音频数据,在调用AudioRenderer.write()方法前可以进行音频数据的预处理,实现个性化的音频播放功能,
// AudioRenderer会读出写入缓冲区的音频数据进行渲染
await this.audioRenderer.write(buf);
// 如果渲染器状态为released,停止渲染
if (this.audioRenderer.state === audio.AudioState.STATE_RELEASED) {
fs.close(file);
await this.audioRenderer.stop();
}
// 如果音频文件已经被读取完,停止渲染
if(this.audioRenderer.state === audio.AudioState.STATE_RUNNING) {
if (i === len - 1) {
fs.close(file);
await this.audioRenderer.stop();
}
}
}
this.currentPlayingCount--;
resolve("");
}catch (e) {
LogUtil.e("" + e);
reject(e);
}
}
和录制音频差不多,不过这里我修复了一个问题,就是如果同时播放的话音频会重叠,虽然已经调用了stop但是还是会有问题,用了个计数解决。
(代码比较赶,很多是cv的,能用就不想改了,凑合用吧)
停止播放音频
停止播放的代码也非常简单:
/**
* 停止播放PCM音频
*/
stopPlayPcmRecord(){
// 只有渲染器状态为running或paused的时候才可以停止
if (this.audioRenderer.state !== audio.AudioState.STATE_RUNNING &&
this.audioRenderer.state !== audio.AudioState.STATE_PAUSED) {
LogUtil.e("stop pcm failed: state error")
return;
}
// 停止渲染
this.audioRenderer.stop();
}
释放资源
很多时候写代码都会忘记释放资源,这里记得在不用的时候调用下:
/**
* 释放资源
*/
release() {
// 采集器状态不是STATE_RELEASED或STATE_NEW状态,才能release
if (this.audioCapturer && this.audioCapturer.state.valueOf() != audio.AudioState.STATE_RELEASED &&
this.audioCapturer.state.valueOf() != audio.AudioState.STATE_NEW) {
this.audioCapturer.release();
}
// 渲染器状态不是released状态,才能release
if (this.audioRenderer && this.audioRenderer.state != audio.AudioState.STATE_RELEASED) {
this.audioRenderer.release();
}
}
小结
简单用鸿蒙的API写了下PCM录音功能,保存到临时文件,并且支持播放音频,记录下。