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

8 回复

开发者你好,SoundPool目前无法实现停顿操作,您是在什么场景下需要间隔播放音频,麻烦请简单描述一下,AvPlayer可以实现起播后暂停,暂停后重新播放的效果:示例代码,若是不能解决您的问题,请提供以下信息:

  1. 复现代码(如最小复现demo)您具体是怎么实现的使用SoundPool间隔播放的音频,是否方便提供下完整的demo,您代码片段中的SoundPoolManager以及你们的数据源看不到是怎么实现SoundPoolManager和使用数据源的,方便的话麻烦可以提供一下完整的demo;

  2. 版本信息(如:开发工具、手机系统版本信息);

更多关于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进行音频间隔播放确实会遇到精度和响应延迟问题,尤其是在短时间间隔场景下。你的代码问题主要在于:

  1. setTimeout精度不足:JS定时器在鸿蒙上最小间隔约4ms,且受事件循环影响,无法保证精确时序
  2. 暂停响应延迟: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);
  }
}

关键改进点:

  1. 使用AVPlayer替代SoundPool:AVPlayer提供更精确的播放控制,支持seek(0)实现立即停止
  2. requestAnimationFrame调度:16.7ms的刷新率适合250ms间隔,通过时间戳计算确保时序准确
  3. 预初始化播放器:避免播放时的初始化延迟
  4. 立即停止机制:pause()方法立即停止所有播放并重置位置

对于你的数据格式,需要先将note数组映射到具体的音频URL,每个数字对应一个AVPlayer实例。当time为0时自动循环播放。

这种方案解决了setTimeout的精度问题和暂停延迟,同时保持了代码的简洁性。

回到顶部