AudioRenderer怎么判断音频播放结束
=======================

#### 问题现象

AudioRenderer组件在播放PCM音频时如何监听到音频文件播放结束？

#### 背景知识

[AudioRenderer](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/using-audiorenderer-for-playback)是音频渲染器，用于播放PCM（Pulse Code Modulation）音频数据，相比AVPlayer而言，可以在输入前添加数据预处理，更适合有音频开发经验的开发者，以实现更灵活的播放功能。

[getAudioTimestampInfo](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-audio-audiorenderer#getaudiotimestampinfo19)获取输出音频流时间戳和位置信息。

#### 解决方案

AudioRenderer通过回调[on('writeData')](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-audio-audiorenderer#onwritedata11)方法不停的写入音频数据播放，未提供播放结束的API。开发者可通过如下方式判断音频播放是否结束：

1.  定义一个定时器[setInterval](https://developer.huawei.com/consumer/cn/doc/atomic-ascf/apis-timer#section1242317318140)，每隔250ms调用getAudioTimestampInfo获取播放的音频流时间戳，并记录时间戳值preTimestamp。
2.  当最新的时间戳值和上次获取的时间戳值preTimestamp相等，表示音频渲染结束，否则继续定时轮询。

示例代码如下：

import { audio } from '@kit.AudioKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';


let audioRenderer: audio.AudioRenderer;
let audioStreamInfo: audio.AudioStreamInfo = {
  samplingRate: audio.AudioSamplingRate.SAMPLE\_RATE\_8000, _// 采样率。_
  channels: audio.AudioChannel.CHANNEL\_1, _// 通道。_
  sampleFormat: audio.AudioSampleFormat.SAMPLE\_FORMAT\_S16LE, _// 采样格式。_
  encodingType: audio.AudioEncodingType.ENCODING\_TYPE\_RAW _// 编码格式。_
};
let audioRendererInfo: audio.AudioRendererInfo = {
  usage: audio.StreamUsage.STREAM\_USAGE\_MUSIC, _// 音频流使用类型：音乐。根据业务场景配置，参考StreamUsage。_
  rendererFlags: 0 _// 音频渲染器标志。_
};
let audioRendererOptions: audio.AudioRendererOptions = {
  streamInfo: audioStreamInfo,
  rendererInfo: audioRendererInfo
};


@Entry
@Component
export struct PlayPcmEndDemo {
  context = this.getUIContext().getHostContext() as common.UIAbilityContext;
  audioData: Uint8Array = generateTestPCM(); _// 测试PCM数据，按需替换为其他音频数据源_
  writeOffset = 0;
  preTimestamp = 0;
  intervalId = 0;
  @State isEnd: boolean = false;


  async aboutToAppear(): Promise<void> {
    audioRenderer = await audio.createAudioRenderer(audioRendererOptions);
    await this.init(); _//初始化_
  }


  async aboutToDisappear(): Promise<void> {
    await audioRenderer.release();
  }


  build() {
    Column({ space: 20 }) {
      Button('播放音频数据').width('100%')
        .onClick(async () => {
          this.writeOffset = 0;
          this.preTimestamp = 0;
          this.isEnd = false;
          await audioRenderer.start();


          this.checkPlayEnd(); _//定时获取音频流时间戳，当时间戳不变时，播放结束_
        });
      Button('停止播放').width('100%')
        .onClick(async () => {
          clearInterval(this.intervalId);
          this.stopAndFlush();
        });
      Text(\`播放是否结束：${this.isEnd}\`);
    }
    .padding(40)
    .justifyContent(FlexAlign.Start)
    .width('100%')
    .height('100%');
  }


 _ //定时获取音频流时间戳，当时间戳不变时，播放结束_
  checkPlayEnd() {
    this.intervalId = setInterval(() => {
      audioRenderer.getAudioTimestampInfo().then((audioTimestampInfo: audio.AudioTimestampInfo) => {
        console.info(\`Current timestamp: ${JSON.stringify(audioTimestampInfo)}\`);
        if (this.preTimestamp === audioTimestampInfo.timestamp) {
          console.info(\`Last timestamp: ${this.preTimestamp}\`);
          this.isEnd = true;
          clearInterval(this.intervalId);
        }
        this.preTimestamp = audioTimestampInfo.timestamp;
      }).catch((err: BusinessError) => {
        console.error(\`ERROR: ${err}\`);
      });
    }, 250);
  }


  async init() {
    audioRenderer.on('writeData', (buffer: ArrayBuffer) => {
      if (!this.audioData) {
        return audio.AudioDataCallbackResult.INVALID;
      }
      let bufferView = new Uint8Array(buffer);
      let writeLen = Math.min(buffer.byteLength, this.audioData.byteLength - this.writeOffset);
      if (writeLen <= 0) {
        console.info('Play Done');
        return audio.AudioDataCallbackResult.INVALID;
      }
      bufferView.set(this.audioData.slice(this.writeOffset, this.writeOffset + writeLen));
      this.writeOffset += writeLen;
      return audio.AudioDataCallbackResult.VALID;
    });
  }


  async stopAndFlush() {
    console.info('renderer status' + audioRenderer.state);
    audioRenderer.stop().then(() => {
      console.error('Renderer stop ok.');
    }).catch((err: BusinessError) => {
      console.error('Renderer stop failed. ', err);
    });
    audioRenderer.flush().then(() => {
      console.error('Renderer flush ok.');
    }).catch((err: BusinessError) => {
      console.error('renderer flush err. ' + err);
    });
  }
}


function generateTestPCM(): Uint8Array {
  const sampleRate = 8000;
  const noteDuration = 0.5;
  const amplitude = 0.35;


  const freqMap: Record<number, number> = {
    1: 523.25, _// C5_
    2: 587.33, _// D5_
    3: 659.25, _// E5_
    4: 698.46, _// F5_
    5: 783.99, _// G5_
    6: 880.00, _// A5_
  };


  const melody = \[
    1, 1, 5, 5, 6, 6, 5, 0,
    4, 4, 3, 3, 2, 2, 1, 0,
    5, 5, 4, 4, 3, 3, 2, 0
  \];


  const samplesPerNote = Math.floor(sampleRate \* noteDuration); _// 4000_
  const totalSamples = samplesPerNote \* melody.length; _// 96,000_
  const buffer = new ArrayBuffer(totalSamples \* 2); _// 192,000 bytes_
  const view = new DataView(buffer);


  let idx = 0;
  for (const note of melody) {
    const freq = note ? freqMap\[note\] : 0;
    for (let i = 0; i < samplesPerNote; i++) {
      const t = i / sampleRate;
      const wave = freq ? amplitude \* Math.sin(2 \* Math.PI \* freq \* t) : 0;
      const sample = Math.round(wave \* 32767);
      const clamped = Math.max(-32768, Math.min(32767, sample));
      view.setInt16(idx \* 2, clamped, true);
      idx++;
    }
  }
  return new Uint8Array(buffer);
}