HarmonyOS 鸿蒙Next中关于 AudioRenderer 写入数据方法 writeData 的疑问

HarmonyOS 鸿蒙Next中关于 AudioRenderer 写入数据方法 writeData 的疑问 如下面所示,我生成了一个持续时间为 0.1s 的音效,使用旧方法 write 播放正常,但是用新的 writeData 却会一直重复。

  1. debug 之后确认是会自动监听反复调用,但是每次调用之间还是会有重复的声音。

  2. 加上变量开关之后确保之后不会继续 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

7 回复

旧方法 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),不知这样的方法是否妥当,

  1. 播放音频时会出现部分杂音、卡顿,有时甚至会大段出现。

    • 分析原因
      AudioRenderer会调用AudioRendererWriteDataCallback方法来写入音频数据,如果数据未能填满回调buffer的长度就放入队列里播放,则会导致杂音、卡顿等现象。因为部分buffer未填满导致存在脏数据影响播放。

    • 解决方案
      在无法填满回调所需长度数据的情况下,需要返回audio.AudioDataCallbackResult.INVALID,此时系统不会处理该段音频数据,然后会再次向应用请求数据,在确认数据填满后再返回audio.AudioDataCallbackResult.VALID进行播放,参考如下代码,可以判断写入数据是否已填满buffer,如果未填满就可以返回INVALID:

      // 获取音频数据长度readLen,判断能否填满buffer
      if (readLen < buffer.byteLength) {
        return audio.AudioDataCallbackResult.INVALID;
      }
      
  2. 停止写入音频数据后,播放没有停止,还会一直输出杂音。

    • 分析原因
      在没有写入新的音频数据也没有停止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)
  }
}

根据您的问题描述,问题可能源于多次注册回调函数、未正确管理音频数据偏移量或未处理回调返回值。

问题分析

  1. 回调函数重复注册:您的代码在每次调用 playSound 时都重新设置 on('writeData') 回调。这会导致多个回调函数被注册,系统会多次触发回调,从而引起音效重复播放。
  2. 数据偏移量管理:您的回调函数每次都将整个音效数据复制到缓冲区,而没有维护播放偏移量。这导致每次回调都从音效开头播放,而不是继续播放剩余部分。
  3. 回调返回值处理:从 API version 12 开始,回调函数可以返回 AudioDataCallbackResult 来控制数据有效性。如果未返回正确值,系统可能一直请求数据或播放无效数据。

解决方案

  1. 仅设置一次回调函数:在音频渲染器初始化时设置 on('writeData') 回调,而不是在每次播放时设置。
  2. 管理播放状态和偏移量:在回调函数中维护当前音效数据和偏移量,确保每次回调只复制未播放的数据。
  3. 处理数据不足情况:当音效数据播放完毕时,返回 AudioDataCallbackResult.INVALID(API version 12+)或用静音数据填充缓冲区(API version 11)。
  4. 使用开关变量控制播放:确保播放完成后重置状态,防止重复。

代码示例

以下是一个修改后的示例代码,假设您使用 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 中每次设置回调,这会导致多个回调实例。应仅在初始化时设置一次。
  • 数据填充:务必确保回调缓冲区被完全填充(即使用静音),否则播放杂音。
  • 资源管理:播放完成后,调用 stoprelease 来释放资源。
  • 错误处理:添加适当的错误处理,如初始化失败或播放异常。

如果问题仍然存在,请检查音频渲染器的状态管理,确保没有意外多次调用 playSound 或回调函数。

AudioRenderer的writeData方法用于向音频渲染器写入PCM数据。该方法接受字节数组、写入偏移量和数据大小作为参数,返回实际写入的字节数。写入数据前需确保AudioRenderer处于运行状态,数据格式需与配置的音频参数一致。若返回字节数小于请求大小,可能因缓冲区已满或设备未就绪。写入失败会返回错误码,需根据具体错误进行处理。

在HarmonyOS Next中,writeData方法通过事件监听机制实现音频数据写入,与旧版write的直接写入方式不同。你的代码中,on('writeData')事件会持续触发,导致重复写入同一段音频数据,从而出现声音重复播放的问题。

正确做法是:在事件回调中,每次写入新的数据片段或明确控制写入次数,避免重复set相同数据。可以考虑使用计数器或状态管理来确保音频数据只写入一次,或者在完成所需播放后及时取消事件监听。

回到顶部