|
|
|
import {
|
|
|
|
FlutterPlugin,
|
|
|
|
FlutterPluginBinding,
|
|
|
|
MethodCall,
|
|
|
|
MethodCallHandler,
|
|
|
|
MethodChannel,
|
|
|
|
MethodResult,
|
|
|
|
Log,
|
|
|
|
} from '@ohos/flutter_ohos';
|
|
|
|
|
|
|
|
import { textToSpeech } from '@kit.CoreSpeechKit';
|
|
|
|
import { util } from '@kit.ArkTS';
|
|
|
|
import json from '@ohos.util.json';
|
|
|
|
|
|
|
|
enum Languages {
|
|
|
|
ZH_CN = 'zh-CN',
|
|
|
|
EN_US = 'en-US',
|
|
|
|
}
|
|
|
|
|
|
|
|
const SPOKEN_PREFIX = 'SPL_';
|
|
|
|
const SYNTHESIZE_TO_FILE_PREFIX = 'STF_';
|
|
|
|
|
|
|
|
const Tag = 'FlutterTtsOhosPlugin';
|
|
|
|
|
|
|
|
/** FlutterTtsOhosPlugin **/
|
|
|
|
export default class FlutterTtsOhosPlugin implements FlutterPlugin, MethodCallHandler {
|
|
|
|
private methodChannel: MethodChannel | null = null;
|
|
|
|
private awaitSpeakCompletion: boolean = false;
|
|
|
|
private awaitSynthCompletion: boolean = false;
|
|
|
|
private language: Languages = Languages.ZH_CN;
|
|
|
|
private volume: number = 1.0;
|
|
|
|
private pitch: number = 1.0;
|
|
|
|
private rate: number = 1.0;
|
|
|
|
private tts: textToSpeech.TextToSpeechEngine | null = null;
|
|
|
|
private queueMode: 0 | 1 = 0;
|
|
|
|
private utterances: Map<string, MethodResult> = new Map();
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
}
|
|
|
|
|
|
|
|
getUniqueClassName(): string {
|
|
|
|
return "FlutterTtsOhosPlugin"
|
|
|
|
}
|
|
|
|
|
|
|
|
onAttachedToEngine(binding: FlutterPluginBinding): void {
|
|
|
|
this.methodChannel = new MethodChannel(binding.getBinaryMessenger(), "flutter_tts");
|
|
|
|
this.methodChannel.setMethodCallHandler(this)
|
|
|
|
}
|
|
|
|
|
|
|
|
onDetachedFromEngine(binding: FlutterPluginBinding): void {
|
|
|
|
this.stop();
|
|
|
|
this.tts?.shutdown();
|
|
|
|
this.tts = null;
|
|
|
|
this.methodChannel?.setMethodCallHandler(null);
|
|
|
|
this.methodChannel = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
async onMethodCall(call: MethodCall, result: MethodResult): Promise<void> {
|
|
|
|
try {
|
|
|
|
switch (call.method) {
|
|
|
|
case "speak":
|
|
|
|
const speakId = await this.speak(call.args);
|
|
|
|
if (this.awaitSpeakCompletion) {
|
|
|
|
this.utterances.set(speakId, result);
|
|
|
|
} else {
|
|
|
|
result.success(1)
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case "awaitSpeakCompletion":
|
|
|
|
this.awaitSpeakCompletion = call.args;
|
|
|
|
result.success(1)
|
|
|
|
break;
|
|
|
|
case "awaitSynthCompletion":
|
|
|
|
this.awaitSynthCompletion = call.args;
|
|
|
|
result.success(1)
|
|
|
|
break;
|
|
|
|
case "synthesizeToFile":
|
|
|
|
const fileText: string = call.argument("text");
|
|
|
|
const synthId = await this.speak(fileText, 0);
|
|
|
|
if (this.awaitSynthCompletion) {
|
|
|
|
this.utterances.set(synthId, result);
|
|
|
|
} else {
|
|
|
|
result.success(1)
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case "stop":
|
|
|
|
this.stop();
|
|
|
|
result.success(1)
|
|
|
|
break;
|
|
|
|
case "setLanguage":
|
|
|
|
this.language = call.args;
|
|
|
|
result.success(1);
|
|
|
|
break;
|
|
|
|
case "setSpeechRate":
|
|
|
|
this.rate = call.args
|
|
|
|
result.success(1);
|
|
|
|
break;
|
|
|
|
case "setVolume":
|
|
|
|
this.volume = call.args;
|
|
|
|
result.success(1);
|
|
|
|
break;
|
|
|
|
case "setPitch":
|
|
|
|
this.pitch = call.args
|
|
|
|
result.success(1);
|
|
|
|
break;
|
|
|
|
case "getLanguages":
|
|
|
|
this.getLanguages(result)
|
|
|
|
break;
|
|
|
|
case "getSpeechRateValidRange":
|
|
|
|
this.getSpeechRateValidRange(result)
|
|
|
|
break;
|
|
|
|
case "isLanguageAvailable":
|
|
|
|
let language1: string = call.argument("language")
|
|
|
|
this.isLanguageAvailable(language1, result)
|
|
|
|
break;
|
|
|
|
case "getVoices":
|
|
|
|
this.getVoices(result)
|
|
|
|
break;
|
|
|
|
case "setVoice":
|
|
|
|
const voice: string = call.argument("voice")
|
|
|
|
this.setVoice(voice, result)
|
|
|
|
break;
|
|
|
|
case "clearVoice":
|
|
|
|
this.clearVoice(result)
|
|
|
|
break;
|
|
|
|
case "setSharedInstance":
|
|
|
|
result.success(1)
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
result.notImplemented()
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
result.error('1111', Tag, JSON.stringify(call))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
createEngineParams(): textToSpeech.CreateEngineParams {
|
|
|
|
return {
|
|
|
|
language: this.language,
|
|
|
|
online: 1,
|
|
|
|
person: 0,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async createEngine(): Promise<textToSpeech.TextToSpeechEngine> {
|
|
|
|
const tts: textToSpeech.TextToSpeechEngine = await textToSpeech.createEngine(this.createEngineParams());
|
|
|
|
tts.setListener({
|
|
|
|
// 开始播报回调
|
|
|
|
onStart: (requestId: string, response: textToSpeech.StartResponse) => {
|
|
|
|
if (requestId.startsWith(SYNTHESIZE_TO_FILE_PREFIX)) {
|
|
|
|
this.methodChannel?.invokeMethod("synth.onStart", true);
|
|
|
|
} else {
|
|
|
|
this.methodChannel?.invokeMethod("speak.onStart", true)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
// 合成完成及播报完成回调
|
|
|
|
onComplete: (requestId: string, response: textToSpeech.CompleteResponse) => {
|
|
|
|
if (requestId.startsWith(SYNTHESIZE_TO_FILE_PREFIX)) {
|
|
|
|
if (this.awaitSynthCompletion) {
|
|
|
|
this.completion(requestId, 1);
|
|
|
|
}
|
|
|
|
this.methodChannel?.invokeMethod("synth.onComplete", true)
|
|
|
|
} else {
|
|
|
|
if (this.awaitSpeakCompletion && response.type == 1) {
|
|
|
|
this.completion(requestId, 1);
|
|
|
|
}
|
|
|
|
this.methodChannel?.invokeMethod("speak.onComplete", true)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
// 停止播报回调
|
|
|
|
onStop: (requestId: string, response: textToSpeech.StopResponse) => {
|
|
|
|
this.methodChannel?.invokeMethod("speak.onCancel", true)
|
|
|
|
},
|
|
|
|
// 返回音频流
|
|
|
|
onData: (requestId: string, audio: ArrayBuffer, response: textToSpeech.SynthesisResponse) => {
|
|
|
|
|
|
|
|
},
|
|
|
|
// 错误回调
|
|
|
|
onError: (requestId: string, errorCode: number, errorMessage: string) => {
|
|
|
|
if (requestId.startsWith(SYNTHESIZE_TO_FILE_PREFIX)) {
|
|
|
|
if (this.awaitSynthCompletion) {
|
|
|
|
this.onError(requestId, errorCode, errorMessage);
|
|
|
|
}
|
|
|
|
this.methodChannel?.invokeMethod("synth.onError", errorMessage)
|
|
|
|
} else {
|
|
|
|
if (this.awaitSpeakCompletion) {
|
|
|
|
this.onError(requestId, errorCode, errorMessage);
|
|
|
|
}
|
|
|
|
this.methodChannel?.invokeMethod("speak.onError", errorMessage)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return tts;
|
|
|
|
}
|
|
|
|
|
|
|
|
createSpakParams(playType: 0 | 1 = 1): textToSpeech.SpeakParams {
|
|
|
|
return {
|
|
|
|
requestId: `${playType == 0 ? SYNTHESIZE_TO_FILE_PREFIX : SPOKEN_PREFIX}_${util.generateRandomBinaryUUID()}`,
|
|
|
|
extraParams: {
|
|
|
|
speed: this.rate,
|
|
|
|
pitch: this.pitch,
|
|
|
|
volume: this.volume,
|
|
|
|
languageContext: this.language,
|
|
|
|
playType: playType,
|
|
|
|
queueMode: this.queueMode,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
completion(id: string, success: number) {
|
|
|
|
this.utterances.get(id)?.success(success);
|
|
|
|
this.utterances.delete(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
onError(id: string, errorCode: number, errorMessage: string) {
|
|
|
|
this.utterances.get(id)?.error(`${errorCode}`, errorMessage, null);
|
|
|
|
this.utterances.delete(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
async speak(text: string, playType: 0 | 1 = 1): Promise<string> {
|
|
|
|
if (this.tts == null) {
|
|
|
|
this.tts = await this.createEngine();
|
|
|
|
}
|
|
|
|
let param = this.createSpakParams(playType)
|
|
|
|
this.tts.speak(text, param);
|
|
|
|
return param.requestId;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async stop() {
|
|
|
|
this.tts?.stop();
|
|
|
|
}
|
|
|
|
|
|
|
|
async setLanguage(language: string, result: MethodResult) {
|
|
|
|
// todo
|
|
|
|
}
|
|
|
|
|
|
|
|
async setRate(rate: number, result: MethodResult) {
|
|
|
|
// todo
|
|
|
|
}
|
|
|
|
|
|
|
|
async setVolume(volume: number, result: MethodResult) {
|
|
|
|
// todo
|
|
|
|
}
|
|
|
|
|
|
|
|
async setPitch(pitch: number, result: MethodResult) {
|
|
|
|
// todo
|
|
|
|
}
|
|
|
|
|
|
|
|
async getLanguages(result: MethodResult) {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
async getSpeechRateValidRange(result: MethodResult) {
|
|
|
|
// todo
|
|
|
|
}
|
|
|
|
|
|
|
|
async isLanguageAvailable(language: string, result: MethodResult) {
|
|
|
|
// todo
|
|
|
|
}
|
|
|
|
|
|
|
|
async getVoices(result: MethodResult) {
|
|
|
|
// todo
|
|
|
|
}
|
|
|
|
|
|
|
|
async setVoice(voice: string, result: MethodResult) {
|
|
|
|
// todo
|
|
|
|
}
|
|
|
|
|
|
|
|
async clearVoice(result: MethodResult) {
|
|
|
|
// todo
|
|
|
|
}
|
|
|
|
|
|
|
|
} |
|
|
\ No newline at end of file |
...
|
...
|
|