HarmonyOS鸿蒙Next AudioCapturer 录制只收到底噪,CoreSpeechKit 正常
HarmonyOS鸿蒙Next AudioCapturer 录制只收到底噪,CoreSpeechKit 正常 录制音频只收到底噪(±7-20),不是人声。同一设备上 CoreSpeechKit 中文 ASR 正常。
AudioCapturer 配置:
const info: audio.AudioCapturerOptions = {
streamInfo: {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000,
channels: audio.AudioChannel.CHANNEL_1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
},
capturerInfo: {
source: audio.SourceType.SOURCE_TYPE_MIC,
capturerFlags: 0
}
};
const capturer = await audio.createAudioCapturer(info);
capturer.on('readData', (buffer: ArrayBuffer) => {
this.audioChunks.push(buffer);
});
await capturer.start();
数据读取和解析:
录音停止后,拼接所有 chunk → new Uint8Array(total) → 用 DataView.getInt16(i, true)(小端)读取每个 16bit 有符号样本。
Hex dump(前 40 字节):
07 00 09 00 08 00 08 00 07 00 03 00 02 00 01 00 02 00 05 00 06 00 05 00 09 00 0f 00 14 00 11 00 0e 00 0a 00 09 00 0d 00
解为 int16 = 7, 9, 8, 8, 7, 3, 2, 1, 2, 5, 6, 5, 9, 15, 20, 17, 14, 10, 9, 13。满量程 ±32768,实际值仅 ±20。
已排查的项目:
| 项目 | 状态 |
|---|---|
| source = SOURCE_TYPE_MIC | ✅ |
| 采样格式 S16LE + DataView 小端读取 | ✅ |
| 回调仅 push 数组,无耗时操作 | ✅ |
| module.json5 有 MICROPHONE 权限 | ✅ |
| 运行时权限已授权 | ✅ |
| 同一设备 CoreSpeechKit 中文 ASR 能正常识别 | ✅(硬件和通路正常) |
| 前台用户交互触发 | ✅ |
问题:以上全部通过,但 readData 收到的 16bit 样本值仅 ±20,明显是电路底噪而非麦克风信号。CoreSpeechKit 在同一设备上正常,说明不是硬件问题。这个 AudioCapturer 配置哪里不对?
更多关于HarmonyOS鸿蒙Next AudioCapturer 录制只收到底噪,CoreSpeechKit 正常的实战教程也可以访问 https://www.itying.com/category-93-b0.html
这组配置本身看起来没有明显的字节序问题:S16LE + DataView.getInt16(i, true) 是匹配的;采样值长期只有 ±20,更像是实际采集到的就是近似静音/底噪,而不是解析方式错了。
建议按这个顺序排查:
-
先把 source 从 SOURCE_TYPE_MIC 改成 SOURCE_TYPE_VOICE_RECOGNITION 试一下。CoreSpeechKit 正常通常走语音识别采集链路,和普通 MIC 源在路由、前处理、降噪策略上可能不同。
-
start 后确认 capturer 状态、getCapturerInfo() 返回的 source,以及当前实际输入设备。外接耳机/蓝牙、隐私开关、并发录音高优先级流,都可能让实际输入设备和预期不一致。
-
readData 回调里建议立即复制 buffer 后再保存,例如拷到新的 Uint8Array,避免底层缓冲被复用后影响后续拼接分析。
-
再交叉测试 48k 采样率、VOICE_RECOGNITION 源、系统录音或官方 AudioCapturer Demo。如果这些也只有底噪,重点看设备/系统版本的采集源路由问题;如果只有当前应用异常,再回到权限、前后台状态和采集生命周期排查。
更多关于HarmonyOS鸿蒙Next AudioCapturer 录制只收到底噪,CoreSpeechKit 正常的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
开发者您好,使用描述中相同配置录音存入沙箱并播放未复现只收到底噪问题,使用的demo如下。如未能解决问题,请说明使用DevEco Studio版本以及手机型号和版本信息,并提供可复现问题的demo。录音存入沙箱并播放的demo:
// Index.ets
import { audio } from '@kit.AudioKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { abilityAccessCtrl, common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct Index {
private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
// 音频配置参数(录音与播放需完全一致)
private sampleRate: audio.AudioSamplingRate = audio.AudioSamplingRate.SAMPLE_RATE_16000;
private channels: audio.AudioChannel = audio.AudioChannel.CHANNEL_1;
private sampleFormat: audio.AudioSampleFormat = audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE;
// 录音与播放实例
private audioCapturer: audio.AudioCapturer | null = null;
private audioRenderer: audio.AudioRenderer | null = null;
// 文件保存路径 (应用沙箱路径)
private filePath: string = '';
private file: fs.File | null = null;
@State statusText: string = '准备就绪';
@State isRecording: boolean = false;
@State isPlaying: boolean = false;
aboutToAppear() {
// 初始化音频文件路径
this.filePath = this.context.filesDir + '/test_record.pcm';
this.requestMicrophonePermission();
}
// 1. 动态申请麦克风权限
async requestMicrophonePermission() {
let atManager = abilityAccessCtrl.createAtManager();
try {
let data = await atManager.requestPermissionsFromUser(this.context, ['ohos.permission.MICROPHONE']);
if (data.authResults[0] === 0) {
this.statusText = '麦克风权限获取成功';
} else {
this.statusText = '麦克风权限被拒绝,无法录音';
}
} catch (err) {
this.statusText = '权限申请异常';
}
}
// 2. 开始录音 (AudioCapturer)
async startRecording() {
if (this.isRecording || this.isPlaying) {
return;
}
try {
this.statusText = '正在初始化录音组件...';
// 创建或打开文件(每次重新录音则覆盖原文件)
if (fs.accessSync(this.filePath)) {
fs.unlinkSync(this.filePath);
}
this.file = fs.openSync(this.filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
// 配置采集参数
let capturerInfo: audio.AudioCapturerInfo = {
source: audio.SourceType.SOURCE_TYPE_MIC,
capturerFlags: 0
};
let streamInfo: audio.AudioStreamInfo = {
samplingRate: this.sampleRate,
channels: this.channels,
sampleFormat: this.sampleFormat,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
};
let capturerOptions: audio.AudioCapturerOptions = {
streamInfo: streamInfo,
capturerInfo: capturerInfo
};
// 创建实例
this.audioCapturer = await audio.createAudioCapturer(capturerOptions);
// 订阅数据读入回调 (关键:持续向文件中写入采集到的裸数据)
this.audioCapturer.on('readData', (buffer: ArrayBuffer) => {
if (this.file) {
fs.writeSync(this.file.fd, buffer);
}
});
// 启动录音
await this.audioCapturer.start();
this.isRecording = true;
this.statusText = '正在录音中...';
} catch (error) {
let err = error as BusinessError;
this.statusText = `录音失败: ${err.message}`;
this.closeFile();
}
}
// 3. 停止录音
async stopRecording() {
if (!this.isRecording || !this.audioCapturer) {
return;
}
try {
await this.audioCapturer.stop();
await this.audioCapturer.release();
this.audioCapturer = null;
this.closeFile();
this.isRecording = false;
this.statusText = '录音已保存';
} catch (error) {
let err = error as BusinessError;
this.statusText = `停止录音失败: ${err.message}`;
}
}
// 4. 点击播放录音 (AudioRenderer)
async startPlayback() {
if (this.isRecording || this.isPlaying) {
return;
}
if (!fs.accessSync(this.filePath)) {
this.statusText = '无录音文件,请先录音';
return;
}
try {
this.statusText = '正在初始化播放组件...';
this.file = fs.openSync(this.filePath, fs.OpenMode.READ_ONLY);
let fileLen = fs.statSync(this.file.fd).size;
let totalReadSize = 0;
// 配置播放参数 (须与录音参数完全对齐)
let rendererInfo: audio.AudioRendererInfo = {
usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
rendererFlags: 0
};
let streamInfo: audio.AudioStreamInfo = {
samplingRate: this.sampleRate,
channels: this.channels,
sampleFormat: this.sampleFormat,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
};
let rendererOptions: audio.AudioRendererOptions = {
streamInfo: streamInfo,
rendererInfo: rendererInfo
};
// 创建播放实例
this.audioRenderer = await audio.createAudioRenderer(rendererOptions);
// 订阅数据写入回调(播放器向我们要数据时,我们从文件中读取塞给它)
this.audioRenderer.on('writeData', (buffer: ArrayBuffer) => {
if (this.file) {
let readLen = fs.readSync(this.file.fd, buffer, { offset: totalReadSize });
totalReadSize += readLen;
// 如果读取完毕,自动停止播放
if (totalReadSize >= fileLen) {
this.stopPlayback();
}
}
});
// 启动播放
await this.audioRenderer.start();
this.isPlaying = true;
this.statusText = '正在播放音频...';
} catch (error) {
let err = error as BusinessError;
this.statusText = `播放失败: ${err.message}`;
this.closeFile();
}
}
// 5. 停止播放
async stopPlayback() {
if (!this.isPlaying || !this.audioRenderer) {
return;
}
try {
await this.audioRenderer.stop();
await this.audioRenderer.release();
this.audioRenderer = null;
this.closeFile();
this.isPlaying = false;
this.statusText = '播放已结束';
} catch (error) {
let err = error as BusinessError;
this.statusText = `停止播放失败: ${err.message}`;
}
}
// 安全关闭文件句柄
private closeFile() {
if (this.file) {
fs.closeSync(this.file);
this.file = null;
}
}
// UI 渲染布局
build() {
Column({ space: 20 }) {
Text('HarmonyOS AudioCapturer Demo')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.margin({ top: 40 });
Text(`状态: ${this.statusText}`)
.fontSize(16)
.fontColor(Color.Gray)
.padding(10);
Divider().color('#E1E1E1').strokeWidth(1);
// 录音控制组
Row({ space: 20 }) {
Button(this.isRecording ? '正在录音...' : '开始录音')
.backgroundColor(this.isRecording ? Color.Red : Color.Blue)
.enabled(!this.isRecording && !this.isPlaying)
.onClick(() => this.startRecording());
Button('停止录音')
.backgroundColor(Color.Orange)
.enabled(this.isRecording)
.onClick(() => this.stopRecording());
};
// 播放控制组
Row({ space: 20 }) {
Button(this.isPlaying ? '正在播放...' : '播放录音')
.backgroundColor(this.isPlaying ? Color.Green : '#4CAF50')
.enabled(!this.isRecording && !this.isPlaying)
.onClick(() => this.startPlayback());
Button('停止播放')
.backgroundColor(Color.Gray)
.enabled(this.isPlaying)
.onClick(() => this.stopPlayback());
};
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
.padding(20);
}
}
在项目的 entry/src/main/module.json5 中声明麦克风权限:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:reason_microphone", // 请在 string.json 中配置具体原因
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
}
]
}
}
CoreSpeechKit 能识别人声,不等于当前 AudioCapturer 配置一定在采同一路输入。建议先按“输入源、权限、数据解释”三层拆。
可以这样排查:
- source 先用 SOURCE_TYPE_MIC 做最小录音 demo,确认已动态授予 MICROPHONE 权限,并在真机前台测试。
- 你现在看到 ±7~20,要确认 ArrayBuffer 是按 S16LE 小端解析,不要按 Uint8 或错误步长读样本。
- 保存原始 PCM 到沙箱,再用同样采样率、声道、格式播放或用桌面工具打开,判断是采集本身没声音,还是显示/统计代码读错。
- 16k 单声道通常可用,但某些设备的默认麦克风链路可能更偏 48k,建议也测 48k/mono/S16LE 对比。
- 如果 CoreSpeechKit 正常而 AudioCapturer 异常,重点看 sourceType、音频焦点、是否有蓝牙/耳机输入被选中,以及是否同时存在其他录音占用。
从您贴的 hex 看,S16LE 小端解析基本没问题,07 00、09 00 解成 7、9 是符合预期的;现在更像是采集链路只拿到了极低幅度输入,而不是 DataView 字节序错。CoreSpeechKit 正常也不能完全证明 AudioCapturer 的 MIC 源、路由和前处理链路一致。
我这边建议按这个顺序排:1)在 readData 回调里立即拷贝 buffer,避免后续底层复用导致数据异常;2)录制时实时打印 peak/RMS,边说话边看峰值是否能上千;3)对比 SOURCE_TYPE_MIC 与目标 API 支持的语音识别类 sourceType,并确认麦克风权限、系统麦克风开关、蓝牙/有线耳机输入路由;4)把 PCM 加 WAV 头保存,用 Audacity/ffplay 播放确认原始采集结果。
示例:
capturer.on('readData', (buffer: ArrayBuffer) => {
const copy = buffer.slice(0);
const pcm = new Int16Array(copy);
let peak = 0;
for (const s of pcm) { peak = Math.max(peak, Math.abs(s)); }
console.info(`pcm peak=${peak}, samples=${pcm.length}`);
this.audioChunks.push(copy);
});
如果 peak 一直在 20 左右,优先查输入源/路由/权限;如果回调内峰值正常、停止后变小,再查拼接和保存流程。参考:AudioCapturer 录制 https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/using-audiocapturer-for-recording ;音频流类型选择 https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/using-right-streamusage-and-sourcetype
AudioCapturer 录到底噪而 CoreSpeechKit 正常,说明麦克风硬件与权限均无问题。
常见原因:AudioCapturer 配置的采样率、声道数、位深与设备实际参数不匹配;未正确设置 AudioStreamInfo 的 usage 或 sourceType(例如误用 VOICE_COMMUNICATION 导致 AGC/降噪未生效);或未按 AudioCapturerStateChange 回调动态适配格式。
建议检查 StreamUsage、SourceType 及 encodingType,并确保使用系统推荐的采样率(如 48000Hz、16bit、单声道)。
问题大概率不是PCM解析或权限,而是采集通路不对。CoreSpeechKit走的是系统语音识别专用通路,而你的配置用的是普通MIC源(SOURCE_TYPE_MIC + capturerFlags:0),这在某些设备上会被路由到非语音识别通道,导致AGC/降噪把主麦信号压成近零底噪。
建议方向:
- 将
source改为audio.SourceType.SOURCE_TYPE_VOICE_RECOGNITION(若有该枚举),capturerFlags改为audio.AudioCapturerFlag.AUDIO_CAPTURER_FLAG_VOICE_RECOGNITION(值为1),和CoreSpeechKit对齐。 - start前调用
capturer.getAvailableCaptureDevices()并选一个内置麦克风,用capturer.setCaptureDevice(device)显式绑定。 - 打印
capturer.getAudioStreamInfo()确认实际sampleFormat确实是S16LE;若不是,按实际格式解析。 - 若仍异常,改用
capturer.read(buffer, true)同步读取,在start后延时100ms再读,排除on('readData')回调的启动数据问题。
