文本转语音如何区分是否为手动停止播报
==================

#### 问题现象

文本转语音播报时如何区分是否手动停止？调用stop方法停止播报后，speak的回调顺序是先onComplete然后onStop。无论是正常结束停止还是手动停止都会执行onComplete，应该如何正确区分是用户手动停止还是已经正常结束停止。

#### 背景知识

*   [文本转语音](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/texttospeech-guide)：支持将一篇不超过10000字数的中英文文本（简体中文、繁体中文、数字、英文）合成为语音，并以选定音色进行播报。
*   [SpeakListener](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/hms-ai-texttospeech#speaklistener)：合成及播报的回调对象，通过此对象可返回合成及播报过程的相关状态，例如开始合成及播报、合成完成、播报完成、停止播报完成等。
*   <'queueMode', number>播报模式：0：排队模式播报；1：抢占模式播报。
*   onComplete回调触发有2种情况，回调结果[CompleteResponse](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/hms-ai-texttospeech#completeresponse)中type为0表示合成结束，type为1表示播报结束，两者的回调时序并不固定。而onStop回调必然会在播报结束（type为1）的onComplete回调之后触发，即当调用stop接口时，可能会出现两种回调时序：
    *   调用stop前语音已经合成结束：
        
        ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/c1/v3/OIHBjaYoQXuejiy-NBdJEA/zh-cn_image_0000002658914049.png?HW-CC-KV=V1&HW-CC-Date=20260709T134437Z&HW-CC-Expire=86400&HW-CC-Sign=C3E44C832C477CB29232BF6BA67489785568F26591C82C3629D785DF532EC088 "点击放大")
        
    *   调用stop前语音还未合成完毕：
        
        ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/cf/v3/nrS068aMRqu_FJ0b_Gj8lw/zh-cn_image_0000002658794099.png?HW-CC-KV=V1&HW-CC-Date=20260709T134437Z&HW-CC-Expire=86400&HW-CC-Sign=BA3A5EF2F74FB285C2F20016EED7224C9AF80D69F6841026BEB4E9A3F23B5718 "点击放大")
        

#### 解决方案

采用排队模式播报，在onComplete回调中判断播报完成（即回调结果[CompleteResponse](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/hms-ai-texttospeech#completeresponse)中type为1）时调用[speak](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/hms-ai-texttospeech#speak)方法加载下一个需要播报的文本。示例代码如下：

import { textToSpeech } from '@kit.CoreSpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { PromptAction } from '@kit.ArkUI';
import { UIContext } from '@kit.ArkUI';


@Entry
@Component
struct Index {
  textArr: string\[\] = \[
    '\\n\\t\\t古人学问无遗力，少壮工夫老始成；\\n\\t\\t' + '纸上得来终觉浅，绝知此事要躬行。\\n\\t\\t',
    '\\n\\t\\t清明时节雨纷纷，路上行人欲断魂；\\n\\t\\t' + '借问酒家何处有？牧童遥指杏花村。\\n\\t\\t'
  \];
  @State curIndex: number = 0;
  @State isManMadeStopped: boolean = false; _// 标识是否为手动停止_
  uiContext: UIContext = this.getUIContext();
  promptAction: PromptAction = this.uiContext.getPromptAction();
  ttsEngine: textToSpeech.TextToSpeechEngine | undefined = undefined;
  engineCreated: boolean = false;


  build() {
    Column() {
      Scroll() {
        Column({ space: 20 }) {
          Text(\`${this.textArr\[this.curIndex\]}\`)
            .fontSize(20)
            .fontColor(0x317AE7);


          Button() {
            Text('初始化文本转语音引擎')
              .fontColor(Color.White)
              .fontSize(20);
          }
          .type(ButtonType.Capsule)
          .backgroundColor('#0A59F7')
          .width('80%')
          .height(50)
          .margin(10)
          .onClick(() => {
            this.engineCreated = true;
            this.createByCallback();
            this.promptAction.showToast({
              message: '初始化文本转语音引擎成功!',
              duration: 2000
            });
          });


          Button() {
            Text('合成播报')
              .fontColor(Color.White)
              .fontSize(20);
          }
          .type(ButtonType.Capsule)
          .backgroundColor('#0A59F7')
          .width('80%')
          .height(50)
          .margin(10)
          .onClick(() => {
            if (this.engineCreated) {
              try {
                this.speak();
               _ // 重置手动停止状态_
                this.isManMadeStopped = false;
                this.promptAction.showToast({
                  message: '开始合成播报',
                  duration: 2000
                });
              } catch (err) {
                this.promptAction.showToast({
                  message: '合成播报失败',
                  duration: 2000
                });
              }
            } else {
              this.promptAction.showToast({
                message: 'The engine has not been created',
                duration: 2000
              });
            }
          });




          Button() {
            Text('停止播报')
              .fontColor(Color.White)
              .fontSize(20);
          }
          .type(ButtonType.Capsule)
          .backgroundColor('#0A59F7')
          .width('80%')
          .height(50)
          .margin(10)
          .onClick(() => {
            try {
              let isBusy: boolean = this.ttsEngine?.isBusy() || false;
              if (isBusy) {
                this.isManMadeStopped = true;
                this.ttsEngine?.stop();
              }
              this.promptAction.showToast({
                message: '停止播报!',
                duration: 2000
              });
            } catch (err) {
              this.promptAction.showToast({
                message: '停止播报失败',
                duration: 2000
              });
            }
          });


          Button() {
            Text('关闭引擎')
              .fontColor(Color.White)
              .fontSize(20);
          }
          .type(ButtonType.Capsule)
          .backgroundColor('#0A59F7')
          .width('80%')
          .height(50)
          .margin(10)
          .onClick(() => {
            try {
              this.ttsEngine?.shutdown();
              this.engineCreated = false;
              this.promptAction.showToast({
                message: '关闭引擎成功!',
                duration: 2000
              });
            } catch (err) {
              this.promptAction.showToast({
                message: '关闭引擎失败',
                duration: 2000
              });
            }
          });


        }
        .layoutWeight(1);
      }
      .width('100%')
      .height('100%');


    };
  }


 _ // 创建引擎，通过callback形式返回_
  _// 当引擎不存在、引擎资源不存在、初始化超时，返回错误码1002300005，引擎创建失败_
  private createByCallback() {
  **  // 设置创建引擎参数**
    let extraParam: Record<string, Object> = { 'style': 'interaction-broadcast', 'locate': 'CN', 'name': 'EngineName' };
    let initParamsInfo: textToSpeech.CreateEngineParams = {
      language: 'zh-CN',
      person: 0,
      online: 1,
      extraParams: extraParam
    };
    try {
     _ // 调用createEngine方法_
      textToSpeech.createEngine(initParamsInfo,
        (err: BusinessError, textToSpeechEngine: textToSpeech.TextToSpeechEngine) => {
          if (!err) {
            console.info('createEngine is success');
          _  // 接收创建引擎的实例_
            this.ttsEngine = textToSpeechEngine;
          } else {
            console.error(\`error code: ${err.code}, message: ${err.message}.\`);
          }
        });
    } catch (error) {
      let message = (error as BusinessError).message;
      let code = (error as BusinessError).code;
      console.error(\`createEngine failed, error code: ${code}, message: ${message}.\`);
    }
  }

  _// 调用speak播报方法_
  private speak() {
  _  // 设置播报相关参数_
    let extraParam: Record<string, Object> = {
      'queueMode': 0,
      'speed': 1,
      'volume': 2,
      'pitch': 1,
      'languageContext': 'zh-CN',
      'audioType': 'pcm',
      'soundChannel': 3,
      'playType': 1
    };
    let speakListener: textToSpeech.SpeakListener = {
    _  // 开始播报回调_
      onStart(requestId: string, response: textToSpeech.StartResponse) {
        console.info(\`onStart, requestId: ${requestId} response: ${JSON.stringify(response)}\`);
      },
     _ // 完成播报回调_
      onComplete: (requestId: string, response: textToSpeech.CompleteResponse) => {
        console.info(\`onComplete, requestId: ${requestId} response: ${JSON.stringify(response)}\`);
       _ // 播报完成并且非手动停止则自动播报下一个文本_
        if (response.type === 1 && !this.isManMadeStopped) {
        _  // 语音播放结束，加载下一个需要播报的文本_
          let speakParams: textToSpeech.SpeakParams = {
            requestId: '123456' + Date.now(), /_/ requestId在同一实例内仅能用一次，请勿重复设置_
            extraParams: extraParam
          };
          this.promptAction.showToast({
            message: '播报下一个',
            duration: 2000
          });
          this.curIndex = this.curIndex == this.textArr.length - 1 ? 0 : this.curIndex + 1;
          this.ttsEngine?.speak(this.textArr\[this.curIndex\], speakParams);
        }
      },
   _   // 停止播报完成回调，调用stop方法并完成时会触发此回调_
      onStop(requestId: string, response: textToSpeech.StopResponse) {
        console.info(\`onStop, requestId: ${requestId} response: ${JSON.stringify(response)}\`);
      },
  _    // 返回音频流_
      onData(requestId: string, audio: ArrayBuffer, response: textToSpeech.SynthesisResponse) {
        console.info(\`onData, requestId: ${requestId} sequence: ${JSON.stringify(response)} audio: ${JSON.stringify(audio)}\`);
      },
    _  // 错误回调，播报过程发生错误时触发此回调_
      onError(requestId: string, errorCode: number, errorMessage: string) {
        console.error(\`onError, requestId: ${requestId} errorCode: ${errorCode} errorMessage: ${errorMessage}\`);
      }
    };
    _// 设置回调_
    this.ttsEngine?.setListener(speakListener);
    let speakParams: textToSpeech.SpeakParams = {
      requestId: '123456' + Date.now(), // _requestId在同一实例内仅能用一次，请勿重复设置_
      extraParams: extraParam
    };
 _   // 调用speak播报方法_
    this.ttsEngine?.speak(this.textArr\[this.curIndex\], speakParams);
  };
}