HarmonyOS 鸿蒙Next中关于 AudioRenderer 写入数据方法 writeData 的疑问
HarmonyOS 鸿蒙Next中关于 AudioRenderer 写入数据方法 writeData 的疑问 如下面所示,我生成了一个持续时间为 0.1s 的音效,使用旧方法 write 播放正常,但是用新的 writeData 却会一直重复。
-
debug 之后确认是会自动监听反复调用,但是每次调用之间还是会有重复的声音。
-
加上变量开关之后确保之后不会继续 set ,还是一直有声音
async playSound(buffer: ArrayBuffer): Promise<void> {
if (!this.renderer || !this.isRunning) {
return;
}
try {
// await this.renderer.write(buffer);
// await this.renderer.drain(); // 确保数据写入完成
this.renderer.on('writeData', (rendererBuffer: ArrayBuffer) => {
const buffer1 = new Int16Array(rendererBuffer);
const buffer2 = new Int16Array(buffer.slice(0, rendererBuffer.byteLength));
buffer1.set((buffer2))
})
} catch (err) {
console.error('播放音效失败:', err);
}
}
更多关于HarmonyOS 鸿蒙Next中关于 AudioRenderer 写入数据方法 writeData 的疑问的实战教程也可以访问 https://www.itying.com/category-93-b0.html
旧方法 write主动写入数据后,播放完成即停止;新方法 writeData基于订阅回调机制,音频服务会持续请求数据填充缓冲区,若未正确终止数据流,会导致循环调用
解决方案
1/在数据写入完成后,通过 bufferSize 或偏移量判断是否结束,返回 INVALID 终止后续回调:
let bufferSize: number = 0;
let totalSize: number = buffer.byteLength; // 总数据长度
const writeDataCallback = (rendererBuffer: ArrayBuffer): audio.AudioDataCallbackResult => {
if (bufferSize >= totalSize) {
// 数据已全部写入,返回 INVALID 停止请求
return audio.AudioDataCallbackResult.INVALID;
}
const remaining = totalSize - bufferSize;
const chunkSize = Math.min(remaining, rendererBuffer.byteLength);
const srcBuffer = new Uint8Array(buffer, bufferSize, chunkSize);
const dstBuffer = new Uint8Array(rendererBuffer);
dstBuffer.set(srcBuffer);
bufferSize += chunkSize;
// 填满后返回 VALID,否则填充静音数据
if (chunkSize === rendererBuffer.byteLength) {
return audio.AudioDataCallbackResult.VALID;
} else {
// 填充剩余空间为静音(0)
dstBuffer.fill(0, chunkSize);
return audio.AudioDataCallbackResult.VALID;
}
};
// 注册回调(仅一次!)
this.renderer.on('writeData', writeDataCallback);
2/在 playSound 方法中,确保回调只绑定一次,而非每次调用都重新注册:
async playSound(buffer: ArrayBuffer): Promise<void> {
if (!this.renderer || !this.isRunning) {
return;
}
// 重置偏移量
bufferSize = 0;
totalSize = buffer.byteLength;
// 启动播放
this.renderer.start((err) => {
if (err) {
console.error('启动失败:', err);
}
});
}
3/停止播放时重置状态:
stopPlayback() {
if (this.renderer) {
this.renderer.off('writeData'); // 取消回调订阅
this.renderer.stop();
this.renderer.release();
}
}
更多关于HarmonyOS 鸿蒙Next中关于 AudioRenderer 写入数据方法 writeData 的疑问的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
感谢回答,原理我看明白了,但还是不太行,我这边是生成了一个0.1s的很短的音频,然后每秒调用一次,即使我现在去除定时方法,而且执行完第一次set之后后续全部返回 return audio.AudioDataCallbackResult.INVALID,也还是有急促的声音,大致生成方法如下
// ========== 音频初始化与预生成 ==========
async createAudioBuffer(frequency: number, duration: number, amplitude: number): Promise<ArrayBuffer> {
const samples = Math.floor(SAMPLE_RATE * duration);
const buffer = new ArrayBuffer(samples * 2);
const view = new Int16Array(buffer);
const S = 2 * Math.PI * frequency / SAMPLE_RATE;
const R = 100 / SAMPLE_RATE; // 快速衰减
const P = 200 / SAMPLE_RATE;
const O = 500 / SAMPLE_RATE;
for (let i = 0; i < samples; i++) {
view[i] = amplitude * (
0.09 * Math.exp(-i * R) * Math.sin(S * i) +
0.34 * Math.exp(-i * P) * Math.sin(2 * S * i) +
0.57 * Math.exp(-i * O) * Math.sin(6 * S * i)
) * 0x7FFF;
}
return buffer;
}
this.createAudioBuffer(440, BEAT_DURATION, 0.7)
目前的处理是生成的声音结束全部播放完成之后,后续都执行 dstBuffer.fill(0),不知这样的方法是否妥当,
-
播放音频时会出现部分杂音、卡顿,有时甚至会大段出现。
-
分析原因:
AudioRenderer会调用AudioRendererWriteDataCallback方法来写入音频数据,如果数据未能填满回调buffer的长度就放入队列里播放,则会导致杂音、卡顿等现象。因为部分buffer未填满导致存在脏数据影响播放。 -
解决方案:
在无法填满回调所需长度数据的情况下,需要返回audio.AudioDataCallbackResult.INVALID,此时系统不会处理该段音频数据,然后会再次向应用请求数据,在确认数据填满后再返回audio.AudioDataCallbackResult.VALID进行播放,参考如下代码,可以判断写入数据是否已填满buffer,如果未填满就可以返回INVALID:// 获取音频数据长度readLen,判断能否填满buffer if (readLen < buffer.byteLength) { return audio.AudioDataCallbackResult.INVALID; }
-
-
停止写入音频数据后,播放没有停止,还会一直输出杂音。
-
分析原因:
在没有写入新的音频数据也没有停止AudioRenderer的情况下,则会循环播放缓冲区的历史数据,导致不断输出杂音,影响播放效果。 -
解决方案:
对于最后一帧,如果数据不够填满缓冲长度,开发者需要使用剩余数据拼接空数据的方式,将缓冲填满,不够的时候可以用0将数据填满,没有音频数据写入时返回INVALID,或停止AudioRenderer。参考示例代码如下:if (chunkSize < rendererBuffer.byteLength) { const view = new DataView(rendererBuffer); for (let i = chunkSize; i < rendererBuffer.byteLength; i++) { view.setUint8(i, 0); } }
-
【解决方案】
主要在于音频的回调处理中,下面是AudioRenderere使用writeData写入生成正弦波脉冲音频的Demo代码,具体可参考使用AudioRenderer渲染音频文件的示例代码。
import { BusinessError } from '@ohos.base';
import { audio } from '@kit.AudioKit';
// 常量定义
const SAMPLE_RATE = 48000;
const BEAT_DURATION = 0.1; // 0.1秒音频
const PLAY_INTERVAL = 1000; // 每秒播放一次
@Entry
@Component
struct AudioPlayerPage {
// 音频渲染器
private audioRenderer: audio.AudioRenderer | null = null;
private isRendererInitialized: boolean = false;
// 音频数据
private audioBuffer: ArrayBuffer | null = null;
private bufferSize: number = 0;
private totalSize: number = 0;
// 定时器
private playTimer: number | null = null;
// 播放状态
@State isPlaying: boolean = false;
@State statusText: string = "准备中";
@State playCount: number = 0;
aboutToAppear() {
this.initAudioRenderer();
this.prepareAudioBuffer();
}
aboutToDisappear() {
this.stopPlayback();
this.releaseRenderer();
this.clearTimer();
}
// 初始化音频渲染器
async initAudioRenderer() {
try {
// 释放已有的渲染器
if (this.audioRenderer) {
this.audioRenderer.release();
}
// 配置音频参数
let audioParams: audio.AudioRendererOptions = {
streamInfo: {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000,
channels: audio.AudioChannel.CHANNEL_1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
},
rendererInfo: {
usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
rendererFlags: 0
}
};
// 创建音频渲染器
this.audioRenderer = await audio.createAudioRenderer(audioParams);
this.isRendererInitialized = true;
this.statusText = "准备就绪";
console.info("音频渲染器初始化成功");
} catch (err) {
const businessError = err as BusinessError;
console.error(`音频渲染器初始化失败: ${businessError.message}`);
this.statusText = "初始化失败";
}
}
// 预生成音频缓冲区
async prepareAudioBuffer() {
try {
this.audioBuffer = await this.createAudioBuffer(440, BEAT_DURATION, 0.7);
this.totalSize = this.audioBuffer.byteLength;
this.statusText = "音频就绪,点击开始";
console.info("音频缓冲区生成成功");
} catch (err) {
const businessError = err as BusinessError;
console.error(`音频缓冲区生成失败: ${businessError.message}`);
this.statusText = "音频生成失败";
}
}
// 生成音频数据
async createAudioBuffer(frequency: number, duration: number, amplitude: number): Promise<ArrayBuffer> {
const samples = Math.floor(SAMPLE_RATE * duration);
const buffer = new ArrayBuffer(samples * 2);
const view = new Int16Array(buffer);
const S = 2 * Math.PI * frequency / SAMPLE_RATE;
const R = 100 / SAMPLE_RATE;
const P = 200 / SAMPLE_RATE;
const O = 500 / SAMPLE_RATE;
for (let i = 0; i < samples; i++) {
view[i] = amplitude * (
0.09 * Math.exp(-i * R) * Math.sin(S * i) +
0.34 * Math.exp(-i * P) * Math.sin(2 * S * i) +
0.57 * Math.exp(-i * O) * Math.sin(6 * S * i)
) * 0x7FFF;
}
return buffer;
}
// 音频数据写入回调
writeDataCallback = (rendererBuffer: ArrayBuffer): audio.AudioDataCallbackResult => {
if (!this.audioBuffer || this.bufferSize >= this.totalSize) {
// 数据已全部写入或没有数据,返回INVALID停止请求
return audio.AudioDataCallbackResult.INVALID;
}
const remaining = this.totalSize - this.bufferSize;
const chunkSize = Math.min(remaining, rendererBuffer.byteLength);
console.info(`写入数据: 剩余=${remaining}, 块大小=${chunkSize}, 缓冲区大小=${rendererBuffer.byteLength}`);
// 复制音频数据到渲染缓冲区
if (chunkSize > 0) {
const srcBuffer = new Uint8Array(this.audioBuffer, this.bufferSize, chunkSize);
const dstBuffer = new Uint8Array(rendererBuffer);
dstBuffer.set(srcBuffer);
this.bufferSize += chunkSize;
}
// 如果数据不足填满缓冲区,使用DataView填充静音
if (chunkSize < rendererBuffer.byteLength) {
const view = new DataView(rendererBuffer);
for (let i = chunkSize; i < rendererBuffer.byteLength; i++) {
view.setUint8(i, 0);
}
console.info(`填充静音: 从位置 ${chunkSize} 到 ${rendererBuffer.byteLength}`);
}
return audio.AudioDataCallbackResult.VALID;
}
// 播放单次音频
async playSingleSound(): Promise<boolean> {
if (!this.audioRenderer || !this.isRendererInitialized || !this.audioBuffer) {
console.error("音频渲染器或缓冲区未就绪");
return false;
}
try {
// 重置偏移量
this.bufferSize = 0;
// 注册回调
this.audioRenderer.off('writeData');
this.audioRenderer.on('writeData', this.writeDataCallback);
// 启动播放
await this.audioRenderer.start();
// 监听播放完成事件
return new Promise<boolean>((resolve) => {
const checkComplete = () => {
if (this.bufferSize >= this.totalSize) {
this.audioRenderer?.stop();
resolve(true);
} else {
setTimeout(checkComplete, 10);
}
};
setTimeout(checkComplete, 10);
});
} catch (err) {
const businessError = err as BusinessError;
console.error(`播放失败: ${businessError.message}`);
return false;
}
}
// 启动定时播放
async startTimedPlayback() {
if (this.isPlaying) {
return;
}
if (!this.audioRenderer || !this.isRendererInitialized || !this.audioBuffer) {
console.error("音频渲染器或缓冲区未就绪");
this.statusText = "播放器未就绪";
return;
}
this.isPlaying = true;
this.playCount = 0;
this.statusText = "定时播放中...";
// 启动定时器
this.playTimer = setInterval(async () => {
const success = await this.playSingleSound();
if (success) {
this.playCount++;
this.statusText = `播放中 (${this.playCount}次)`;
}
}, PLAY_INTERVAL);
// 立即播放第一次
const success = await this.playSingleSound();
if (success) {
this.playCount++;
this.statusText = `播放中 (${this.playCount}次)`;
}
}
// 停止播放
async stopPlayback() {
if (!this.audioRenderer) {
return;
}
try {
// 停止并移除回调
await this.audioRenderer.stop();
this.audioRenderer.off('writeData');
this.isPlaying = false;
this.statusText = "已停止";
this.clearTimer();
console.info("音频播放停止");
} catch (err) {
const businessError = err as BusinessError;
console.error(`停止播放失败: ${businessError.message}`);
}
}
// 清除定时器
private clearTimer() {
if (this.playTimer !== null) {
clearInterval(this.playTimer);
this.playTimer = null;
}
}
// 释放渲染器资源
async releaseRenderer() {
if (this.audioRenderer) {
try {
this.clearTimer();
this.audioRenderer.off('writeData');
await this.audioRenderer.stop();
await this.audioRenderer.release();
this.isRendererInitialized = false;
console.info("音频渲染器已释放");
} catch (err) {
const businessError = err as BusinessError;
console.error(`释放渲染器失败: ${businessError.message}`);
}
}
}
build() {
Column() {
Text('定时音频播放器')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 40 })
Text(this.statusText)
.fontSize(18)
.margin({ bottom: 20 })
Text(`播放次数: ${this.playCount}`)
.fontSize(16)
.margin({ bottom: 30 })
Row() {
Button(this.isPlaying ? '停止定时播放' : '开始定时播放')
.width(160)
.height(40)
.backgroundColor(this.isPlaying ? '#ff0000' : '#007dff')
.onClick(() => {
if (this.isPlaying) {
this.stopPlayback();
} else {
this.startTimedPlayback();
}
})
.margin({ right: 20 })
Button('单次播放')
.width(120)
.height(40)
.backgroundColor('#10a0a0')
.onClick(async () => {
if (!this.isPlaying) {
const success = await this.playSingleSound();
if (success) {
this.playCount++;
this.statusText = `单次播放完成 (${this.playCount}次)`;
}
}
})
}
.margin({ bottom: 30 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
根据您的问题描述,问题可能源于多次注册回调函数、未正确管理音频数据偏移量或未处理回调返回值。
问题分析
- 回调函数重复注册:您的代码在每次调用
playSound
时都重新设置on('writeData')
回调。这会导致多个回调函数被注册,系统会多次触发回调,从而引起音效重复播放。 - 数据偏移量管理:您的回调函数每次都将整个音效数据复制到缓冲区,而没有维护播放偏移量。这导致每次回调都从音效开头播放,而不是继续播放剩余部分。
- 回调返回值处理:从 API version 12 开始,回调函数可以返回
AudioDataCallbackResult
来控制数据有效性。如果未返回正确值,系统可能一直请求数据或播放无效数据。
解决方案
- 仅设置一次回调函数:在音频渲染器初始化时设置
on('writeData')
回调,而不是在每次播放时设置。 - 管理播放状态和偏移量:在回调函数中维护当前音效数据和偏移量,确保每次回调只复制未播放的数据。
- 处理数据不足情况:当音效数据播放完毕时,返回
AudioDataCallbackResult.INVALID
(API version 12+)或用静音数据填充缓冲区(API version 11)。 - 使用开关变量控制播放:确保播放完成后重置状态,防止重复。
代码示例
以下是一个修改后的示例代码,假设您使用 API version 12 或更高版本(支持返回 AudioDataCallbackResult
)。
import { audio } from '@kit.AudioKit';
import { BusinessError } from '@kit.BasicServicesKit';
class AudioPlayer {
private renderer: audio.AudioRenderer | null = null;
private isRunning: boolean = false;
private currentSound: ArrayBuffer | null = null; // 当前音效数据
private soundOffset: number = 0; // 当前音效的播放偏移量(字节)
// 初始化音频渲染器
async initRenderer() {
try {
// 创建音频渲染器参数(根据您的实际配置调整)
let audioStreamInfo: audio.AudioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000,
channels: audio.AudioChannel.CHANNEL_2,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
};
let audioRendererInfo: audio.AudioRendererInfo = {
usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
rendererFlags: 0
};
let audioRendererOptions: audio.AudioRendererOptions = {
streamInfo: audioStreamInfo,
rendererInfo: audioRendererInfo
};
this.renderer = await audio.createAudioRenderer(audioRendererOptions);
this.setupWriteCallback(); // 设置一次回调函数
await this.renderer.start();
this.isRunning = true;
} catch (err) {
console.error('初始化音频渲染器失败:', err);
}
}
// 设置 writeData 回调函数(仅一次)
private setupWriteCallback() {
if (!this.renderer) return;
this.renderer.on('writeData', (rendererBuffer: ArrayBuffer) => {
if (!this.currentSound) {
// 没有音效数据时,返回 INVALID,系统不会播放
return audio.AudioDataCallbackResult.INVALID;
}
const sourceData = new Uint8Array(this.currentSound);
const targetData = new Uint8Array(rendererBuffer);
const bytesToCopy = Math.min(sourceData.length - this.soundOffset, targetData.length);
// 复制数据到回调缓冲区
if (bytesToCopy > 0) {
targetData.set(sourceData.subarray(this.soundOffset, this.soundOffset + bytesToCopy));
this.soundOffset += bytesToCopy;
}
// 检查是否复制完整
if (bytesToCopy < targetData.length) {
// 数据不足,用静音填充剩余部分(对于 S16LE,静音为 0)
targetData.fill(0, bytesToCopy);
// 重置当前音效状态
this.currentSound = null;
this.soundOffset = 0;
// 返回 VALID,播放数据(包括静音)
return audio.AudioDataCallbackResult.VALID;
}
// 数据足够,返回 VALID
return audio.AudioDataCallbackResult.VALID;
});
}
// 播放音效
async playSound(buffer: ArrayBuffer): Promise<void> {
if (!this.renderer || !this.isRunning) {
return;
}
// 设置当前音效和偏移量
this.currentSound = buffer;
this.soundOffset = 0;
// 注意:不需要在此设置回调,回调已在初始化时设置
// 系统会自动触发 writeData 回调来请求数据
}
// 停止播放并释放资源
async stop() {
if (this.renderer) {
try {
await this.renderer.stop();
await this.renderer.release();
} catch (err) {
console.error('停止音频渲染器失败:', err);
}
this.renderer = null;
}
this.isRunning = false;
this.currentSound = null;
this.soundOffset = 0;
}
}
针对 API version 11 的注意事项
如果您使用 API version 11(不支持回调返回值),则必须确保回调函数总是填充有效数据(即使用静音填充),否则会导致杂音。修改回调函数如下:
private setupWriteCallback() {
if (!this.renderer) return;
this.renderer.on('writeData', (rendererBuffer: ArrayBuffer) => {
if (!this.currentSound) {
// 没有音效数据时,用静音填充整个缓冲区
const targetData = new Uint8Array(rendererBuffer);
targetData.fill(0); // 静音填充
return; // 无返回值
}
const sourceData = new Uint8Array(this.currentSound);
const targetData = new Uint8Array(rendererBuffer);
const bytesToCopy = Math.min(sourceData.length - this.soundOffset, targetData.length);
if (bytesToCopy > 0) {
targetData.set(sourceData.subarray(this.soundOffset, this.soundOffset + bytesToCopy));
this.soundOffset += bytesToCopy;
}
if (bytesToCopy < targetData.length) {
// 数据不足,静音填充剩余部分
targetData.fill(0, bytesToCopy);
this.currentSound = null;
this.soundOffset = 0;
}
});
}
重要提醒
- 避免多次注册回调:您的原始代码在
playSound
中每次设置回调,这会导致多个回调实例。应仅在初始化时设置一次。 - 数据填充:务必确保回调缓冲区被完全填充(即使用静音),否则播放杂音。
- 资源管理:播放完成后,调用
stop
和release
来释放资源。 - 错误处理:添加适当的错误处理,如初始化失败或播放异常。
如果问题仍然存在,请检查音频渲染器的状态管理,确保没有意外多次调用 playSound
或回调函数。
AudioRenderer的writeData方法用于向音频渲染器写入PCM数据。该方法接受字节数组、写入偏移量和数据大小作为参数,返回实际写入的字节数。写入数据前需确保AudioRenderer处于运行状态,数据格式需与配置的音频参数一致。若返回字节数小于请求大小,可能因缓冲区已满或设备未就绪。写入失败会返回错误码,需根据具体错误进行处理。
在HarmonyOS Next中,writeData
方法通过事件监听机制实现音频数据写入,与旧版write
的直接写入方式不同。你的代码中,on('writeData')
事件会持续触发,导致重复写入同一段音频数据,从而出现声音重复播放的问题。
正确做法是:在事件回调中,每次写入新的数据片段或明确控制写入次数,避免重复set相同数据。可以考虑使用计数器或状态管理来确保音频数据只写入一次,或者在完成所需播放后及时取消事件监听。