HarmonyOS鸿蒙Next中请教下如何间隔播放音频
HarmonyOS鸿蒙Next中请教下如何间隔播放音频 我想请教用什么方法,来间隔播放不同的音频。我现在用的是setTimeout的方式,但是有好多问题。时间间隔比较短的时候,按暂定要过几秒才停下来。有时停下来 音频还能响一会。
数据源是类似这样,到0再循环播放
[{"time": 250,"note": [1,2]},{"time": 250,"note": [1]},{"time": 250,"note": [2]},{ "time": 250,"note": [2] }, {"time": 250,"note": [1]},{"time": 250,"note": [1]{"time":250,"note": [2]},{"time": 250,"note": [12] }, {"time": 0,"note": [0] }]
下面是我的写法,不过有问题。是不是应该换成worker来实现? 有没有什么好的思路。
音频播放我用的SoundPool实现的。
private timerId: number | null = null;
private nextDelay: number = 0
private currentIndex = 0
private startDynamicTimer() {
this.stopDynamicTimer(); // 防重启
this.currentIndex = 0
this.isPlaying = true
this.nextDelay = 0
this.scheduleNext()
this.currentTabID = 1
}
private scheduleNext() {
if (!this.isPlaying) return
this.timerId = setTimeout(() => {
this.performTask();
this.scheduleNext();
}, this.nextDelay);
}
private performTask() {
if (!this.currentMusicBean) return
const step = this.currentMusicBean.notes[this.currentIndex]
if (!step || step.note[0] === 0) {
this.currentIndex = 0;
return;
}
step.note?.forEach(async n => {
SoundPoolManager.getInstance().playMusicNote(n);
})
this.nextDelay = step.time ;
this.currentIndex++;
}
private stopDynamicTimer() {
if (this.timerId !== null) {
clearTimeout(this.timerId)
this.timerId = null
}
this.isPlaying = false
SoundPoolManager.getInstance().playStopAll()
}
更多关于HarmonyOS鸿蒙Next中请教下如何间隔播放音频的实战教程也可以访问 https://www.itying.com/category-93-b0.html
开发者你好,SoundPool目前无法实现停顿操作,您是在什么场景下需要间隔播放音频,麻烦请简单描述一下,AvPlayer可以实现起播后暂停,暂停后重新播放的效果:示例代码,若是不能解决您的问题,请提供以下信息:
-
复现代码(如最小复现demo)您具体是怎么实现的使用SoundPool间隔播放的音频,是否方便提供下完整的demo,您代码片段中的SoundPoolManager以及你们的数据源看不到是怎么实现SoundPoolManager和使用数据源的,方便的话麻烦可以提供一下完整的demo;
-
版本信息(如:开发工具、手机系统版本信息);
更多关于HarmonyOS鸿蒙Next中请教下如何间隔播放音频的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
[{"time": 250,"note": [1,2]},{"time": 250,"note": [1]},{"time": 250,"note": [2]},{ "time": 250,"note": [2] }, {"time": 250,"note": [1]},{"time": 250,"note": [1]{"time":250,"note": [2]},{"time": 250,"note": [12] }, {"time": 0,"note": [0] }]
按照这个数据播放音频,time是间隔时间。SoundPoolManager就是播放某个音频,很简单的封装,都是简短的音频。
开发者你好,麻烦您提供下SoundPoolManager的实现,这边方便定位问题,
export class SoundPoolManager {
private static instance: SoundPoolManager
private soundPool?: media.SoundPool
private soundMap: HashMap<number, number> = new HashMap()
playParameters: media.PlayParameters = { loop: 0, rate: audio.AudioRendererRate.RENDER_RATE_NORMAL,
leftVolume: 1, rightVolume: 1, priority: 0 }
public static getInstance(): SoundPoolManager {
if (!SoundPoolManager.instance) {
SoundPoolManager.instance = new SoundPoolManager()
}
return SoundPoolManager.instance;
}
async createSound() { // 初始化SoundPool
this.soundPool = await media.createSoundPool(5, {
usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
rendererFlags: 0,
})
if (this.soundPool) {
this.soundPool.on('loadComplete', (soundId) => {
console.info('jiejie', 'loadComplete soundId:' + soundId)
})
this.loadSoundRwaFiles(1, 'kick.mp3')
this.loadSoundRwaFiles(2, 'bell.mp3')
this.loadSoundRwaFiles(3, 'snare.mp3')
this.loadSoundRwaFiles(4, 'tom2.mp3')
this.loadSoundRwaFiles(5, 'tom1.mp3')
this.loadSoundRwaFiles(6, 'tom3.mp3')
this.loadSoundRwaFiles(7, 'floor.mp3')
this.loadSoundRwaFiles(8, 'openhh.mp3')
this.loadSoundRwaFiles(9, 'closehh.mp3')
this.loadSoundRwaFiles(10, 'floor.mp3')
this.loadSoundRwaFiles(11, 'crashr.mp3')
this.loadSoundRwaFiles(12, 'crashm.mp3')
this.loadSoundRwaFiles(13, 'crashl.mp3')
}
}
async loadSoundRwaFiles(id: number, fileName_: string) { // 加载音频
if (!this.soundMap.hasKey(id)) {
getContext().resourceManager.getRawFd(fileName_).then((fileDescriptor: resourceManager.RawFileDescriptor) => {
this.soundPool?.load(fileDescriptor.fd, fileDescriptor.offset,
fileDescriptor.length).then((soundId: number) => {
this.soundMap.set(id, soundId)
})
})
}
}
getSoundId(id: number): number {
let soundId = this.soundMap.get(id)
if (soundId != undefined) {
return soundId as number
} else {
return 1
}
}
public async playMusicNote(note: number) {
switch (note) {
case 1:
case 14:
this.playSoundById(1)
case 2:
this.playSoundById(3)
case 3:
this.playSoundById(5)
case 4:
this.playSoundById(4)
case 5:
this.playSoundById(6)
case 6:
this.playSoundById(7)
case 7:
this.playSoundById(2)
case 8:
this.playSoundById(11)
case 9:
this.playSoundById(12)
case 10:
this.playSoundById(10)
case 11:
this.playSoundById(8)
case 12:
this.playSoundById(9)
}
}
async playSoundById(soundId: number) { // 根据soundId播放音频
this.soundPool?.play(soundId, this.playParameters, (error, streamID: number) => {
if (error) {
console.log('jiejie', '播放报错' + error.code + " " + error.message + " " + soundId)
}
})
}
async playStopAll(){
this.soundMap.forEach((value: number, key: number) => {
this.soundPool?.stop(value)
})
}
public release() {
if (this.soundPool) {
this.soundMap.forEach((value: number, key: number) => {
this.soundPool?.unload(value)
})
this.soundMap.clear()
this.soundPool.release()
this.soundPool = undefined
}
}
}
import { AudioContext, OscillatorNode, GainNode } from '@ohos/audio'; // 鸿蒙AudioContext API(若为前端则直接使用浏览器原生API)
class AudioScheduler {
private audioContext: AudioContext | null = null;
private currentIndex = 0;
private isPlaying = false;
private totalDuration = 0; // 所有音频段总时长(ms)
private scheduledNodes: (OscillatorNode | GainNode)[] = []; // 存储已调度的音频节点,用于暂停时销毁
private notes: Array<{ time: number; note: number[] }> = []; // 数据源
constructor(notes: Array<{ time: number; note: number[] }>) {
this.notes = notes.filter(step => step.time > 0); // 过滤time=0的循环标记
// 计算总时长(用于循环)
this.totalDuration = this.notes.reduce((sum, step) => sum + step.time, 0);
}
// 初始化AudioContext(需用户交互后调用,如点击按钮)
async init() {
if (!this.audioContext) {
this.audioContext = new AudioContext();
await this.audioContext.resume(); // 激活AudioContext(浏览器/鸿蒙均需)
}
}
// 开始播放(核心调度逻辑)
start() {
if (!this.audioContext || this.isPlaying) return;
this.isPlaying = true;
this.currentIndex = 0;
this.scheduleAll(this.audioContext.currentTime); // 基于当前绝对时间开始调度
}
// 递归调度所有音频段
private scheduleAll(startTime: number) {
if (!this.isPlaying || !this.audioContext) return;
const step = this.notes[this.currentIndex];
if (!step) {
// 播放完一轮,无缝循环
this.currentIndex = 0;
this.scheduleAll(startTime + this.totalDuration / 1000); // 总时长转秒
return;
}
// 计算当前段的播放时间(绝对时间,无延迟)
const playTime = startTime + (this.currentIndex > 0
? this.notes.slice(0, this.currentIndex).reduce((sum, s) => sum + s.time, 0) / 1000
: 0);
// 播放当前段的所有音符
step.note.forEach(note => {
this.playNote(note, playTime, step.time / 1000); // time转秒
});
// 调度下一段
this.currentIndex++;
// 用requestAnimationFrame确保调度不阻塞主线程(比setTimeout更精准)
requestAnimationFrame(() => this.scheduleAll(startTime));
}
// 播放单个音符(通过AudioContext生成音频,替代SoundPool)
private playNote(note: number, playTime: number, duration: number) {
if (!this.audioContext) return;
// 创建音频节点(OscillatorNode生成音调,GainNode控制音量)
const oscillator = this.audioContext.createOscillator();
const gainNode = this.audioContext.createGain();
// 设置音调(根据note映射频率,示例:note=1→220Hz,可自定义映射表)
oscillator.frequency.value = 220 * Math.pow(2, (note - 1) / 12); // 十二平均律
// 设置音量
gainNode.gain.value = 0.3;
// 连接节点
oscillator.connect(gainNode);
gainNode.connect(this.audioContext.destination);
// 调度播放和停止时间(绝对时间,精确到毫秒)
oscillator.start(playTime);
oscillator.stop(playTime + duration);
// 存储节点,用于暂停时销毁
this.scheduledNodes.push(oscillator, gainNode);
// 播放结束后移除节点(避免内存泄漏)
oscillator.onended = () => {
const index = this.scheduledNodes.indexOf(oscillator);
if (index !== -1) {
this.scheduledNodes.splice(index, 2);
}
};
}
// 暂停播放(立即停止所有音频)
pause() {
this.isPlaying = false;
// 销毁所有已调度的音频节点,直接停止播放
this.scheduledNodes.forEach(node => {
if (node instanceof OscillatorNode) {
node.stop();
}
node.disconnect();
});
this.scheduledNodes = [];
}
// 停止播放(重置状态)
stop() {
this.pause();
this.currentIndex = 0;
if (this.audioContext) {
this.audioContext.suspend(); // 暂停AudioContext,节省资源
}
}
}
// 使用示例
const notesData = [
{"time": 250,"note": [1,2]}, {"time": 250,"note": [1]}, {"time": 250,"note": [2]},
{"time": 250,"note": [2]}, {"time": 250,"note": [1]}, {"time": 250,"note": [1]},
{"time": 250,"note": [2]}, {"time": 250,"note": [12]}, {"time": 0,"note": [0]}
];
// 初始化调度器
const audioScheduler = new AudioScheduler(notesData);
// 需用户交互触发(如按钮点击)
async function onStartClick() {
await audioScheduler.init();
audioScheduler.start();
}
// 暂停按钮点击
function onPauseClick() {
audioScheduler.pause();
}
// 停止按钮点击
function onStopClick() {
audioScheduler.stop();
}
AudioContext找不到这个类,也没有requestAnimationFrame这个方法。
在HarmonyOS Next中,间隔播放音频可通过media模块实现。使用AVPlayer创建播放器,监听播放完成事件,在on('finish')回调中设置定时器,通过setTimeout控制间隔时间,再次调用play()方法。需注意管理播放器状态,避免资源泄漏。
在HarmonyOS Next中,使用setTimeout进行音频间隔播放确实会遇到精度和响应延迟问题,尤其是在短时间间隔场景下。你的代码问题主要在于:
- setTimeout精度不足:JS定时器在鸿蒙上最小间隔约4ms,且受事件循环影响,无法保证精确时序
- 暂停响应延迟:clearTimeout只能取消未执行的定时器,已触发的音频播放无法立即停止
推荐使用AVPlayer + 时间戳调度方案:
import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
class AudioScheduler {
private avPlayers: media.AVPlayer[] = [];
private startTime: number = 0;
private schedule: Array<{time: number, notes: number[]}> = [];
private currentIndex: number = 0;
private animationFrameId: number = 0;
private isPlaying: boolean = false;
// 初始化多个AVPlayer实例
async initAudioPlayers(urls: string[]) {
for (const url of urls) {
const avPlayer = await media.createAVPlayer();
avPlayer.url = url;
await avPlayer.prepare();
this.avPlayers.push(avPlayer);
}
}
// 基于requestAnimationFrame的精确调度
startSchedule(scheduleData: any[]) {
this.schedule = this.parseSchedule(scheduleData);
this.startTime = performance.now();
this.currentIndex = 0;
this.isPlaying = true;
this.scheduleLoop();
}
private scheduleLoop() {
if (!this.isPlaying) return;
const currentTime = performance.now() - this.startTime;
// 检查当前时间点需要播放的音符
while (this.currentIndex < this.schedule.length &&
this.schedule[this.currentIndex].time <= currentTime) {
const { notes } = this.schedule[this.currentIndex];
notes.forEach(note => {
if (this.avPlayers[note]) {
this.avPlayers[note].seek(0); // 重置播放位置
this.avPlayers[note].play();
}
});
this.currentIndex++;
}
// 循环检测
if (this.currentIndex >= this.schedule.length) {
this.currentIndex = 0;
this.startTime = performance.now();
}
this.animationFrameId = requestAnimationFrame(() => this.scheduleLoop());
}
// 立即暂停所有音频
pause() {
this.isPlaying = false;
cancelAnimationFrame(this.animationFrameId);
this.avPlayers.forEach(player => {
player.pause();
player.seek(0);
});
}
// 解析数据格式
private parseSchedule(data: any[]) {
let accumulatedTime = 0;
return data.map(item => {
if (item.time === 0) return null;
accumulatedTime += item.time;
return {
time: accumulatedTime,
notes: item.note.filter((n: number) => n !== 0)
};
}).filter(Boolean);
}
}
关键改进点:
- 使用AVPlayer替代SoundPool:AVPlayer提供更精确的播放控制,支持seek(0)实现立即停止
- requestAnimationFrame调度:16.7ms的刷新率适合250ms间隔,通过时间戳计算确保时序准确
- 预初始化播放器:避免播放时的初始化延迟
- 立即停止机制:pause()方法立即停止所有播放并重置位置
对于你的数据格式,需要先将note数组映射到具体的音频URL,每个数字对应一个AVPlayer实例。当time为0时自动循环播放。
这种方案解决了setTimeout的精度问题和暂停延迟,同时保持了代码的简洁性。

