You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
422 lines
14 KiB
422 lines
14 KiB
import Flutter
|
|
import UIKit
|
|
import AVFoundation
|
|
import MicrosoftCognitiveServicesSpeech
|
|
// 自定义语音处理组件,提供音频流处理等功能
|
|
import speech
|
|
import os.log
|
|
|
|
/**
|
|
* Azure Speech Plugin
|
|
*
|
|
* 基于微软Azure语音服务的Flutter插件
|
|
* 提供语音识别(ASR)和语音合成(TTS)功能
|
|
*/
|
|
@objc public class AzureSpeechPlugin: NSObject, FlutterPlugin {
|
|
// 日志标签
|
|
private let tag = "AzureSpeechPlugin"
|
|
// 日志对象
|
|
private let log = OSLog(subsystem: "com.azure.speech", category: "AzureSpeechPlugin")
|
|
|
|
// ASR相关
|
|
private var asrChannel: FlutterMethodChannel?
|
|
private var asrEventChannel: FlutterEventChannel?
|
|
private var asrEventSink: FlutterEventSink?
|
|
private let azureAsrHelper = AzureAsrHelper()
|
|
|
|
// TTS相关
|
|
private var ttsChannel: FlutterMethodChannel?
|
|
private var ttsEventChannel: FlutterEventChannel?
|
|
private var ttsEventSink: FlutterEventSink?
|
|
private let azureTtsHelper = AzureTtsHelper()
|
|
|
|
// 是否已添加TTS事件监听器
|
|
private var isTtsListenerAdded = false
|
|
|
|
// 当前的连续识别回调
|
|
private var currentAsrCallback: AsrCallbackWrapper?
|
|
|
|
// 插件注册
|
|
public static func register(with registrar: FlutterPluginRegistrar) {
|
|
let instance = AzureSpeechPlugin()
|
|
|
|
// 初始化ASR通道
|
|
let asrChannel = FlutterMethodChannel(name: "azure_speech/asr", binaryMessenger: registrar.messenger())
|
|
registrar.addMethodCallDelegate(instance, channel: asrChannel)
|
|
instance.asrChannel = asrChannel
|
|
|
|
// 初始化TTS通道
|
|
let ttsChannel = FlutterMethodChannel(name: "azure_speech/tts", binaryMessenger: registrar.messenger())
|
|
registrar.addMethodCallDelegate(instance, channel: ttsChannel)
|
|
instance.ttsChannel = ttsChannel
|
|
|
|
// 初始化ASR事件通道
|
|
let asrEventChannel = FlutterEventChannel(name: "azure_speech/asr_events", binaryMessenger: registrar.messenger())
|
|
asrEventChannel.setStreamHandler(instance)
|
|
instance.asrEventChannel = asrEventChannel
|
|
|
|
// 初始化TTS事件通道
|
|
let ttsEventChannel = FlutterEventChannel(name: "azure_speech/tts_events", binaryMessenger: registrar.messenger())
|
|
ttsEventChannel.setStreamHandler(instance)
|
|
instance.ttsEventChannel = ttsEventChannel
|
|
}
|
|
|
|
// 发送ASR事件方法
|
|
internal func sendAsrEvent(_ event: [String: Any]) {
|
|
if asrEventSink == nil {
|
|
os_log("无法发送ASR事件:事件通道未准备好", log: log, type: .error)
|
|
return
|
|
}
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self = self else { return }
|
|
self.asrEventSink?(event)
|
|
}
|
|
}
|
|
|
|
// 发送TTS事件方法
|
|
private func sendTtsEvent(_ event: [String: Any]) {
|
|
if ttsEventSink == nil {
|
|
os_log("无法发送TTS事件:事件通道未准备好", log: log, type: .error)
|
|
return
|
|
}
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self = self else { return }
|
|
self.ttsEventSink?(event)
|
|
}
|
|
}
|
|
|
|
// 设置TTS事件监听器
|
|
private func setupTtsEventListener() {
|
|
if !isTtsListenerAdded {
|
|
azureTtsHelper.addListener(self)
|
|
isTtsListenerAdded = true
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 处理Flutter方法调用
|
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
if call.method.hasPrefix("tts_") {
|
|
handleTtsMethodCall(call, result)
|
|
} else {
|
|
handleAsrMethodCall(call, result)
|
|
}
|
|
}
|
|
|
|
// MARK: - ASR 方法处理
|
|
|
|
private func handleAsrMethodCall(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
|
|
switch call.method {
|
|
case "initialize":
|
|
guard let args = call.arguments as? [String: Any],
|
|
let subscriptionKey = args["subscriptionKey"] as? String,
|
|
let region = args["region"] as? String else {
|
|
result(FlutterError(code: "INVALID_ARGUMENTS", message: "必要的参数不能为空", details: nil))
|
|
return
|
|
}
|
|
|
|
let supportedLanguages = args["supportedLanguages"] as? [String] ?? ["zh-CN"]
|
|
|
|
// 初始化ASR引擎
|
|
let success = azureAsrHelper.initialize(
|
|
subscriptionKey: subscriptionKey,
|
|
region: region,
|
|
supportedLanguages: supportedLanguages,
|
|
audioSourceType: .microphone
|
|
)
|
|
|
|
result(success)
|
|
|
|
case "recognizeOnce":
|
|
// iOS版本不支持recognizeOnce
|
|
result(FlutterError(code: "NOT_SUPPORTED", message: "iOS版本不支持recognizeOnce", details: nil))
|
|
|
|
case "startContinuousRecognition":
|
|
// 确保事件通道已准备好
|
|
guard asrEventSink != nil else {
|
|
result(FlutterError(code: "EVENT_CHANNEL_NOT_READY", message: "事件通道未准备好,无法开始连续识别", details: nil))
|
|
return
|
|
}
|
|
|
|
// 解析音频源类型
|
|
let audioSourceType: AzureAsrHelper.AudioSourceType
|
|
if let args = call.arguments as? [String: Any],
|
|
let audioSourceString = args["audioSourceType"] as? String {
|
|
switch audioSourceString.lowercased() {
|
|
case "external":
|
|
audioSourceType = .external
|
|
default:
|
|
audioSourceType = .microphone
|
|
}
|
|
} else {
|
|
audioSourceType = .microphone
|
|
}
|
|
|
|
// 创建回调包装器
|
|
currentAsrCallback = AsrCallbackWrapper(plugin: self)
|
|
|
|
let success = azureAsrHelper.startContinuousRecognition(
|
|
callback: currentAsrCallback!,
|
|
audioSourceType: audioSourceType
|
|
)
|
|
result(success)
|
|
|
|
case "stopContinuousRecognition":
|
|
let success = azureAsrHelper.stopContinuousRecognition()
|
|
currentAsrCallback = nil
|
|
result(success)
|
|
|
|
case "isContinuousRecognitionActive":
|
|
result(azureAsrHelper.isContinuousRecognitionActive())
|
|
|
|
case "dispose":
|
|
azureAsrHelper.dispose()
|
|
currentAsrCallback = nil
|
|
result(true)
|
|
|
|
case "pushAudioData":
|
|
guard let args = call.arguments as? [String: Any],
|
|
let audioBytes = args["data"] as? FlutterStandardTypedData else {
|
|
result(FlutterError(code: "INVALID_ARGUMENTS", message: "音频数据不能为空", details: nil))
|
|
return
|
|
}
|
|
|
|
azureAsrHelper.pushAudioData(data: audioBytes.data)
|
|
result(true)
|
|
|
|
default:
|
|
result(FlutterMethodNotImplemented)
|
|
}
|
|
}
|
|
|
|
// MARK: - TTS 方法处理
|
|
|
|
private func handleTtsMethodCall(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
|
|
switch call.method {
|
|
case "tts_initialize":
|
|
guard let args = call.arguments as? [String: Any],
|
|
let subscriptionKey = args["subscriptionKey"] as? String,
|
|
let region = args["region"] as? String else {
|
|
result(FlutterError(code: "INVALID_ARGUMENTS", message: "必要的参数不能为空", details: nil))
|
|
return
|
|
}
|
|
|
|
// 初始化TTS引擎
|
|
let language = args["language"] as? String ?? "zh-CN"
|
|
let success = azureTtsHelper.initialize(ttsAppId: "", ttsAppToken: subscriptionKey, ttsResource: region, language: language)
|
|
|
|
// 设置TTS事件监听器
|
|
setupTtsEventListener()
|
|
|
|
result(success)
|
|
|
|
case "tts_set_voice":
|
|
guard let args = call.arguments as? [String: Any],
|
|
let voiceName = args["voiceName"] as? String else {
|
|
result(FlutterError(code: "INVALID_ARGUMENTS", message: "语音名称不能为空", details: nil))
|
|
return
|
|
}
|
|
|
|
let success = azureTtsHelper.setVoice(voiceName)
|
|
result(success)
|
|
|
|
case "tts_speak_once":
|
|
guard let args = call.arguments as? [String: Any],
|
|
let text = args["text"] as? String else {
|
|
result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil))
|
|
return
|
|
}
|
|
|
|
let success = azureTtsHelper.speakOnce(text)
|
|
result(success)
|
|
|
|
case "tts_speak_stream":
|
|
guard let args = call.arguments as? [String: Any],
|
|
let text = args["text"] as? String else {
|
|
result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil))
|
|
return
|
|
}
|
|
|
|
let success = azureTtsHelper.speakStream(text)
|
|
result(success)
|
|
|
|
case "tts_flush_stream":
|
|
let success = azureTtsHelper.flushStream()
|
|
result(success)
|
|
|
|
case "tts_stop":
|
|
let success = azureTtsHelper.stop()
|
|
result(success)
|
|
|
|
case "tts_isSpeaking":
|
|
result(azureTtsHelper.isSpeaking())
|
|
|
|
case "tts_release":
|
|
azureTtsHelper.dispose()
|
|
result(true)
|
|
|
|
default:
|
|
result(FlutterMethodNotImplemented)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - ASR回调包装器
|
|
private class AsrCallbackWrapper: AzureAsrHelper.ContinuousRecognizeCallback {
|
|
private weak var plugin: AzureSpeechPlugin?
|
|
|
|
init(plugin: AzureSpeechPlugin) {
|
|
self.plugin = plugin
|
|
}
|
|
|
|
func onResult(_ text: String, _ detectedLanguage: String) {
|
|
plugin?.sendAsrEvent([
|
|
"type": "result",
|
|
"text": text,
|
|
"language": detectedLanguage
|
|
])
|
|
}
|
|
|
|
func onRecognizing(_ recognizing: String, _ detectedLanguage: String) {
|
|
plugin?.sendAsrEvent([
|
|
"type": "recognizing",
|
|
"text": recognizing,
|
|
"language": detectedLanguage
|
|
])
|
|
}
|
|
|
|
func onSessionStarted() {
|
|
plugin?.sendAsrEvent([
|
|
"type": "sessionStarted"
|
|
])
|
|
}
|
|
|
|
func onSessionStopped() {
|
|
plugin?.sendAsrEvent([
|
|
"type": "sessionStopped"
|
|
])
|
|
}
|
|
|
|
func onCanceled(_ reason: String, _ errorDetails: String) {
|
|
plugin?.sendAsrEvent([
|
|
"type": "canceled",
|
|
"reason": reason,
|
|
"errorDetails": errorDetails
|
|
])
|
|
}
|
|
|
|
func onError(_ error: String) {
|
|
plugin?.sendAsrEvent([
|
|
"type": "error",
|
|
"error": error
|
|
])
|
|
}
|
|
}
|
|
|
|
// MARK: - 事件处理
|
|
extension AzureSpeechPlugin: FlutterStreamHandler {
|
|
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
|
|
// 根据通道类型设置事件接收器
|
|
if let args = arguments as? [String: Any],
|
|
let channel = args["channel"] as? String {
|
|
|
|
if channel == "asr" {
|
|
asrEventSink = events
|
|
} else if channel == "tts" {
|
|
ttsEventSink = events
|
|
setupTtsEventListener()
|
|
}
|
|
} else if arguments == nil {
|
|
// 如果没有指定通道,尝试弄清楚是哪个通道在监听
|
|
if asrEventSink == nil && ttsEventSink != nil {
|
|
asrEventSink = events
|
|
} else if asrEventSink != nil && ttsEventSink == nil {
|
|
ttsEventSink = events
|
|
setupTtsEventListener()
|
|
} else {
|
|
// 无法确定哪个通道,默认设置为ASR
|
|
asrEventSink = events
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
|
// 根据通道类型清除事件接收器
|
|
if let args = arguments as? [String: Any],
|
|
let channel = args["channel"] as? String {
|
|
|
|
if channel == "asr" {
|
|
asrEventSink = nil
|
|
} else if channel == "tts" {
|
|
ttsEventSink = nil
|
|
}
|
|
} else {
|
|
// 如果没有指定通道,清除所有通道
|
|
asrEventSink = nil
|
|
ttsEventSink = nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// MARK: - TTS 事件监听实现
|
|
extension AzureSpeechPlugin: TtsEventListener {
|
|
// 使用@objc特性为方法提供一个不同的Objective-C选择器名称
|
|
@objc(onTtsEvent:)
|
|
public func onEvent(_ event: TtsEvent) {
|
|
var eventMap: [String: Any] = [:]
|
|
|
|
// 根据事件类型转换
|
|
switch event.type {
|
|
case .synthesisStarted:
|
|
eventMap["type"] = "synthesis_started"
|
|
case .synthesisCompleted:
|
|
eventMap["type"] = "synthesis_completed"
|
|
case .synthesisCanceled:
|
|
eventMap["type"] = "synthesis_canceled"
|
|
if let reason = event.params["reason"] {
|
|
eventMap["reason"] = reason
|
|
}
|
|
if let errorDetails = event.params["errorDetails"] {
|
|
eventMap["errorDetails"] = errorDetails
|
|
}
|
|
case .error:
|
|
eventMap["type"] = "error"
|
|
if let errorCode = event.params["errorCode"] {
|
|
eventMap["errorCode"] = errorCode
|
|
}
|
|
if let errorMessage = event.params["errorMessage"] {
|
|
eventMap["errorMessage"] = errorMessage
|
|
}
|
|
@unknown default:
|
|
eventMap["type"] = "unknown"
|
|
for (key, value) in event.params {
|
|
eventMap[key] = value
|
|
}
|
|
}
|
|
|
|
sendTtsEvent(eventMap)
|
|
}
|
|
}
|
|
|
|
// MARK: - 音频数据监听实现
|
|
extension AzureSpeechPlugin: AudioDataListener {
|
|
public func onAudioData(_ data: Data) {
|
|
if ttsEventSink == nil { return }
|
|
|
|
let eventMap: [String: Any] = [
|
|
"type": "audio_data",
|
|
"data": FlutterStandardTypedData(bytes: data)
|
|
]
|
|
|
|
sendTtsEvent(eventMap)
|
|
}
|
|
}
|