wolfplus 2 years ago
parent
commit
1d6d69016c
  1. 2
      .metadata
  2. 21
      azure/LICENSE
  3. 66
      azure/README.md
  4. 710
      azure/ios/Classes/AzureAsrHelper.swift
  5. 18
      azure/ios/Classes/AzureSpeechRecognitionPlugin.swift
  6. 378
      azure/ios/Classes/AzureTtsHelper.swift
  7. 263
      azure/ios/Classes/SwiftAzureSpeechRecognitionPlugin.swift
  8. 24
      azure/ios/azure_speech_recognition.podspec
  9. 6
      azure/lib/azure_speech_recognition.dart
  10. 23
      azure/pubspec.yaml
  11. 6
      ios/Podfile
  12. 67
      ios/Podfile.lock
  13. 148
      ios/Runner.xcodeproj/project.pbxproj
  14. 109
      ios/Runner/AppDelegate.swift
  15. 13
      ios/Runner/Base.lproj/Main.storyboard
  16. 523
      ios/Runner/ClassicBluetoothHelper.swift
  17. 41
      ios/Runner/Info.plist
  18. 6
      ios/Runner/Runner-Bridging-Header.h
  19. 2
      lib/data/services/chat_history_service.dart
  20. 53
      lib/data/services/classic_bluetooth_service.dart
  21. 3
      lib/data/services/flutter_tts_service.dart
  22. 4
      lib/data/services/speech_factory.dart
  23. 239
      lib/data/services/speech_impl/azure_tts_service.dart
  24. 54
      lib/data/services/speech_impl/flutter_asr_service.dart
  25. 3
      lib/data/services/voice_interaction_service.dart
  26. 2
      lib/main.dart
  27. 14
      lib/modules/home/views/home_view.dart
  28. 2
      lib/routes/app_pages.dart
  29. 9
      pubspec.yaml
  30. 30
      test/widget_test.dart

2
.metadata

@ -15,7 +15,7 @@ migration:
- platform: root - platform: root
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: android - platform: ios
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1

21
azure/LICENSE

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Your Company
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

66
azure/README.md

@ -0,0 +1,66 @@
# Azure Speech Recognition
A Flutter plugin for Microsoft Azure Speech services, providing both speech recognition (ASR) and text-to-speech (TTS) capabilities.
## Features
- Speech-to-text (Azure Speech Recognition)
- Text-to-speech (Azure Speech Synthesis)
- Support for multiple languages
- Language detection
- Continuous recognition
- Streaming synthesis
## Getting Started
### Prerequisites
- Azure Speech service subscription key
- Azure Speech service region
### Installation
Add this to your package's `pubspec.yaml` file:
```yaml
dependencies:
azure_speech_recognition:
path: ./azure
```
### Usage
```dart
import 'package:azure_speech_recognition/azure_speech_recognition.dart';
// Initialize the service
await AzureSpeechRecognition.initialize(
subscriptionKey: 'your_subscription_key',
region: 'your_region',
supportedLanguages: ['zh-CN', 'en-US'],
);
// Start continuous recognition
await AzureSpeechRecognition.startContinuousRecognition();
// Listen for recognition events
AzureSpeechRecognition.onRecognitionEvent.listen((event) {
if (event['type'] == 'result') {
print('Recognized: ${event['text']}');
print('Detected language: ${event['detectedLanguage']}');
}
});
// Stop recognition when done
await AzureSpeechRecognition.stopContinuousRecognition();
// Speak text
await AzureSpeechRecognition.speakText('Hello, world!');
// Clean up
await AzureSpeechRecognition.dispose();
```
## License
This project is licensed under the MIT License - see the LICENSE file for details.

710
azure/ios/Classes/AzureAsrHelper.swift

@ -0,0 +1,710 @@
import Foundation
import MicrosoftCognitiveServicesSpeech
import AVFoundation
import AudioToolbox
/// 自定义麦克风流实现,优化ASR语音输入捕获
class AzureMicrophoneStream: NSObject {
var ioUnit: AudioUnit?
var audioFormat: AudioStreamBasicDescription
var audioBufferList: AudioBufferList
var audioList: [Float] = []
let audioListQueue = DispatchQueue(label: "azureAudioListQueue")
private var isActive = false
override init() {
// 音频会话配置
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(.playAndRecord,
mode: .voiceChat,
options: [.allowBluetooth, .defaultToSpeaker, .mixWithOthers])
try audioSession.setActive(true)
print("[AzureMicrophoneStream] 音频会话配置成功")
} catch {
print("[AzureMicrophoneStream] 配置音频会话失败: \(error.localizedDescription)")
}
// 设置音频格式为16KHz 16位单声道PCM
audioFormat = AudioStreamBasicDescription(
mSampleRate: 16000.0,
mFormatID: kAudioFormatLinearPCM,
mFormatFlags: kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked,
mBytesPerPacket: 2,
mFramesPerPacket: 1,
mBytesPerFrame: 2,
mChannelsPerFrame: 1,
mBitsPerChannel: 16,
mReserved: 0
)
audioBufferList = AudioBufferList(
mNumberBuffers: 1,
mBuffers: AudioBuffer(
mNumberChannels: audioFormat.mChannelsPerFrame,
mDataByteSize: 4096,
mData: malloc(4096)
)
)
super.init()
}
func start() -> Bool {
if isActive {
return true // 已经在运行
}
if setupAudioUnit() {
isActive = startAudioUnit()
return isActive
}
return false
}
private func setupAudioUnit() -> Bool {
print("[AzureMicrophoneStream] 设置音频单元")
var ioUnitDescription = AudioComponentDescription(
componentType: kAudioUnitType_Output,
componentSubType: kAudioUnitSubType_VoiceProcessingIO,
componentManufacturer: kAudioUnitManufacturer_Apple,
componentFlags: 0,
componentFlagsMask: 0
)
guard let ioUnitRef = AudioComponentFindNext(nil, &ioUnitDescription) else {
print("[AzureMicrophoneStream] 无法找到音频组件")
return false
}
if CheckHasError(AudioComponentInstanceNew(ioUnitRef, &ioUnit), "创建IO单元") {
ioUnit = nil
return false
}
var enableInput: UInt32 = 1
let kInputBus: AudioUnitElement = 1
let kOutputBus: AudioUnitElement = 0
if CheckHasError(AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Input, kInputBus, &enableInput,
UInt32(MemoryLayout<UInt32>.size)), "设置输入总线的EnableIO属性") {
return false
}
var enableOutput: UInt32 = 0
if CheckHasError(AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output, kOutputBus,
&enableOutput, UInt32(MemoryLayout<UInt32>.size)), "设置输出总线的EnableIO属性") {
return false
}
var flag: UInt32 = 0
if CheckHasError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_ShouldAllocateBuffer,
kAudioUnitScope_Output, kInputBus, &flag, UInt32(MemoryLayout<UInt32>.size)), "设置ShouldAllocateBuffer属性") {
return false
}
let size = UInt32(MemoryLayout<AudioStreamBasicDescription>.size)
if CheckHasError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output, kInputBus, &audioFormat, size), "设置输入总线的StreamFormat属性") {
return false
}
if CheckHasError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input, kOutputBus, &audioFormat, size), "设置输出总线的StreamFormat属性") {
return false
}
var inputCallback = AURenderCallbackStruct(
inputProc: AzureMicrophoneStream.OnRecordedDataIsAvailable,
inputProcRefCon: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
)
if CheckHasError(AudioUnitSetProperty(ioUnit!,
kAudioOutputUnitProperty_SetInputCallback,
kAudioUnitScope_Global, kInputBus,
&inputCallback, UInt32(MemoryLayout<AURenderCallbackStruct>.size)), "设置输入回调") {
return false
}
var hasError = CheckHasError(AudioUnitInitialize(ioUnit!), "初始化IO单元")
if hasError {
// 如果初始化失败,重试一次
Thread.sleep(forTimeInterval: 0.5)
hasError = CheckHasError(AudioUnitInitialize(ioUnit!), "重试初始化IO单元")
}
print("[AzureMicrophoneStream] 音频单元设置\(hasError ? "失败" : "成功")")
return !hasError
}
private func startAudioUnit() -> Bool {
print("[AzureMicrophoneStream] 启动音频单元")
guard let ioUnit = ioUnit else {
print("[AzureMicrophoneStream] IO单元未初始化")
return false
}
return !CheckHasError(AudioOutputUnitStart(ioUnit), "启动IO单元")
}
func stop() {
print("[AzureMicrophoneStream] 停止音频单元")
guard isActive, let ioUnit = ioUnit else { return }
_ = CheckHasError(AudioOutputUnitStop(ioUnit), "停止IO单元")
isActive = false
}
func dispose() {
stop()
if let ioUnit = ioUnit {
_ = CheckHasError(AudioUnitUninitialize(ioUnit), "反初始化IO单元")
_ = CheckHasError(AudioComponentInstanceDispose(ioUnit), "释放IO单元")
}
// 释放缓冲区
if let buffer = audioBufferList.mBuffers.mData {
free(buffer)
}
self.ioUnit = nil
// 清空音频数据
audioListQueue.sync {
audioList.removeAll()
}
}
static let OnRecordedDataIsAvailable: AURenderCallback = { inRefCon, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData in
let wrapper = Unmanaged<AzureMicrophoneStream>.fromOpaque(inRefCon).takeUnretainedValue()
let expectedDataByteSize = inNumberFrames * wrapper.audioFormat.mBytesPerFrame
if wrapper.audioBufferList.mBuffers.mDataByteSize < expectedDataByteSize {
wrapper.audioBufferList.mBuffers.mData = realloc(wrapper.audioBufferList.mBuffers.mData, Int(expectedDataByteSize))
wrapper.audioBufferList.mBuffers.mDataByteSize = expectedDataByteSize
}
let status = wrapper.CheckErrorStatus(AudioUnitRender(wrapper.ioUnit!, ioActionFlags, inTimeStamp,
inBusNumber, inNumberFrames, &wrapper.audioBufferList),
"AudioUnitRender调用")
var audioDataFloat = [Float](repeating: 0.0, count: Int(inNumberFrames))
let buffer = wrapper.audioBufferList.mBuffers
let bufferData = buffer.mData!.assumingMemoryBound(to: Int16.self)
for j in 0..<Int(buffer.mDataByteSize / UInt32(MemoryLayout<Int16>.size)) {
audioDataFloat[j] = Float(bufferData[j]) / 32768.0 // 归一化到[-1.0, 1.0]范围
}
if status == noErr {
wrapper.audioListQueue.async {
wrapper.audioList.append(contentsOf: audioDataFloat)
}
}
return status
}
private func CheckHasError(_ status: OSStatus, _ operation: String) -> Bool {
if status != noErr {
print("[AzureMicrophoneStream] \(operation)失败: \(status)")
return true
}
return false
}
private func CheckErrorStatus(_ status: OSStatus, _ operation: String) -> OSStatus {
if status != noErr {
print("[AzureMicrophoneStream] \(operation)失败: \(status)")
}
return status
}
// 读取音频数据,适配Azure SDK
func read(bytes: inout [UInt8]) -> Int {
return audioListQueue.sync {
if audioList.isEmpty {
return 0
}
// 确保有足够的数据
let minFrames = 1280
if audioList.count < minFrames {
return 0
}
let frameLength = minFrames
let buffer = Array(audioList.prefix(frameLength))
audioList.removeFirst(frameLength)
// 转换为Int16数据
var int16Data = buffer.map { Int16($0 * 32767) }
let data = Data(buffer: UnsafeBufferPointer(start: &int16Data, count: int16Data.count))
bytes = [UInt8](data)
return frameLength * 2 // 每个样本2字节(16位PCM)
}
}
}
/// Azure ASR工具类,负责实现语音识别服务接口
@available(iOS 13.0, *)
class AzureAsrHelper: NSObject {
// MARK: - 属性
/// 事件处理回调
private var eventHandler: (String, [String: Any]) -> Void
/// 语音配置信息
private var speechSubscriptionKey: String = ""
private var serviceRegion: String = ""
/// 语音识别相关
private var speechConfig: SPXSpeechConfiguration?
private var recognizer: SPXSpeechRecognizer?
private var audioConfig: SPXAudioConfiguration?
/// 麦克风流
private var microphoneStream: AzureMicrophoneStream?
private var pushStreamConfig: SPXPushAudioInputStream?
/// 状态标志
private var isInitialized = false
private var _isContinuousRecognitionActive = false
/// 当前语言和支持的语言
private var currentLanguage = "zh-CN"
private var supportedLanguages: [String] = ["zh-CN", "en-US"]
private var isAutoDetectLanguage = false
// MARK: - 初始化
init(eventHandler: @escaping (String, [String: Any]) -> Void) {
self.eventHandler = eventHandler
super.init()
}
deinit {
dispose()
}
// MARK: - ASR Service 接口实现
/// 初始化语音识别服务
/// - Parameters:
/// - speechSubscriptionKey: Azure 语音服务订阅密钥
/// - serviceRegion: Azure 服务区域 (如 eastasia)
/// - supportedLanguages: 支持的语言代码数组 (可选)
/// - Returns: 初始化是否成功
func initialize(speechSubscriptionKey: String, serviceRegion: String, supportedLanguages: [String]? = nil) -> Bool {
print("[AzureAsrHelper] 初始化 Azure 语音服务")
// 检查配置是否为空
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureAsrHelper] 错误: Azure 配置信息不完整")
eventHandler("error", ["message": "Azure 配置信息不完整"])
return false
}
// 释放之前的资源
dispose()
// 记录配置信息
self.speechSubscriptionKey = speechSubscriptionKey
self.serviceRegion = serviceRegion
// 设置语言
if let languages = supportedLanguages, !languages.isEmpty {
self.supportedLanguages = languages
}
// 根据支持的语言数量决定是否启用自动语言检测
isAutoDetectLanguage = self.supportedLanguages.count >= 2
// 如果只有一种语言,设置为当前语言
if !isAutoDetectLanguage && !self.supportedLanguages.isEmpty {
currentLanguage = self.supportedLanguages[0]
}
// 创建麦克风流
microphoneStream = AzureMicrophoneStream()
print("[AzureAsrHelper] Azure 语音服务初始化成功")
isInitialized = true
return true
}
/// 重置 recognizer
private func resetRecognizer() -> Bool {
// 释放之前的 recognizer
recognizer = nil
audioConfig = nil
do {
// 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: speechSubscriptionKey, region: serviceRegion)
// 创建推送流
pushStreamConfig = try SPXPushAudioInputStream()
// 创建音频配置,使用推送流
audioConfig = try SPXAudioConfiguration(streamInput: pushStreamConfig!)
// 设置语言配置
if isAutoDetectLanguage {
// 设置自动语言检测
speechConfig?.setPropertyTo("Continuous", by: SPXPropertyId.speechServiceConnectionLanguageIdMode)
// 创建自动语言检测配置
let autoDetectSourceLanguageConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages)
// 创建识别器
recognizer = try SPXSpeechRecognizer(
speechConfiguration: speechConfig!,
autoDetectSourceLanguageConfiguration: autoDetectSourceLanguageConfig,
audioConfiguration: audioConfig!
)
} else {
// 设置指定的识别语言
speechConfig?.speechRecognitionLanguage = currentLanguage
// 创建识别器
recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig!)
}
return true
} catch {
print("[AzureAsrHelper] 错误: 重置识别器失败: \(error.localizedDescription)")
eventHandler("error", ["message": "重置识别器失败: \(error.localizedDescription)"])
return false
}
}
/// 启动音频捕获和数据推送
private func startAudioStream() -> Bool {
guard let micStream = microphoneStream else {
print("[AzureAsrHelper] 错误: 麦克风流未初始化")
return false
}
// 启动麦克风
if !micStream.start() {
print("[AzureAsrHelper] 错误: 启动麦克风流失败")
return false
}
// 创建并启动音频推送线程
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self, let pushStream = self.pushStreamConfig else { return }
var isRunning = true
var audioBuffer = [UInt8](repeating: 0, count: 16000)
while isRunning {
autoreleasepool {
// 读取麦克风数据
let bytesRead = micStream.read(bytes: &audioBuffer)
if bytesRead > 0 {
do {
// 推送音频数据到Azure识别流
let data = Data(bytes: audioBuffer, count: bytesRead)
try pushStream.write(data)
} catch {
print("[AzureAsrHelper] 推送音频数据失败: \(error.localizedDescription)")
isRunning = false
}
}
// 检查是否应该继续捕获
if !self._isContinuousRecognitionActive {
isRunning = false
}
// 添加适当的休眠以避免过度消耗CPU
if bytesRead == 0 {
Thread.sleep(forTimeInterval: 0.01)
}
}
}
print("[AzureAsrHelper] 音频推送线程已停止")
}
return true
}
/// 执行一次性语音识别
/// - Returns: 是否成功启动识别
func recognizeOnce() -> Bool {
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler("error", ["message": "语音服务未初始化"])
return false
}
// 如果正在连续识别,先停止
if _isContinuousRecognitionActive {
stopContinuousRecognition()
}
// 重置 recognizer
if !resetRecognizer() {
return false
}
// 启动音频流
if !startAudioStream() {
return false
}
do {
// 设置回调
recognizer?.addRecognizedEventHandler { [weak self] _, event in
guard let self = self else { return }
if event.result.reason == SPXResultReason.recognizedSpeech {
let detectedLanguage = self.getDetectedLanguage(from: event.result)
self.eventHandler("result", [
"text": event.result.text ?? "",
"detectedLanguage": detectedLanguage
])
}
}
recognizer?.addCanceledEventHandler { [weak self] _, event in
guard let self = self else { return }
let errorDetails = event.errorDetails ?? "未知错误"
self.eventHandler("error", ["message": "识别异常: \(errorDetails)"])
}
// 通知会话开始
eventHandler("sessionStarted", [:])
// 执行识别
try recognizer?.recognizeOnceAsync { [weak self] result in
guard let self = self else { return }
// 停止麦克风流
self.microphoneStream?.stop()
if result.reason == SPXResultReason.recognizedSpeech {
let detectedLanguage = self.getDetectedLanguage(from: result)
self.eventHandler("result", [
"text": result.text ?? "",
"detectedLanguage": detectedLanguage
])
} else if result.reason == SPXResultReason.canceled {
do {
let details = try SPXCancellationDetails(fromCanceledRecognitionResult: result)
let errorDetails = details.errorDetails ?? "未知错误"
self.eventHandler("error", ["message": "识别取消: \(errorDetails)"])
} catch {
print("[AzureAsrHelper] 错误: 获取取消详情失败: \(error.localizedDescription)")
self.eventHandler("error", ["message": "识别取消,无法获取详细原因"])
}
}
}
return true
} catch {
print("[AzureAsrHelper] 错误: 识别异常: \(error.localizedDescription)")
eventHandler("error", ["message": "识别异常: \(error.localizedDescription)"])
microphoneStream?.stop()
return false
}
}
/// 开始连续语音识别
/// - Returns: 是否成功启动识别
func startContinuousRecognition() -> Bool {
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler("error", ["message": "语音服务未初始化"])
return false
}
// 如果已经在进行连续识别,先停止
if _isContinuousRecognitionActive {
stopContinuousRecognition()
}
// 重置 recognizer
if !resetRecognizer() {
return false
}
do {
// 设置识别事件处理
setupContinuousRecognitionCallbacks()
// 启动连续识别
try recognizer?.startContinuousRecognition()
_isContinuousRecognitionActive = true
// 启动音频流
if !startAudioStream() {
try recognizer?.stopContinuousRecognition()
_isContinuousRecognitionActive = false
return false
}
// 通知会话开始
eventHandler("sessionStarted", [:])
print("[AzureAsrHelper] 连续识别开始")
return true
} catch {
print("[AzureAsrHelper] 错误: 开始连续识别失败: \(error.localizedDescription)")
eventHandler("error", ["message": "开始连续识别失败: \(error.localizedDescription)"])
_isContinuousRecognitionActive = false
microphoneStream?.stop()
return false
}
}
/// 停止连续语音识别
/// - Returns: 是否成功停止识别
func stopContinuousRecognition() -> Bool {
if !_isContinuousRecognitionActive || recognizer == nil {
return true
}
// 停止麦克风流
microphoneStream?.stop()
do {
try recognizer?.stopContinuousRecognition()
_isContinuousRecognitionActive = false
eventHandler("sessionStopped", [:])
print("[AzureAsrHelper] 连续识别已停止")
return true
} catch {
print("[AzureAsrHelper] 错误: 停止连续识别失败: \(error.localizedDescription)")
eventHandler("error", ["message": "停止连续识别失败: \(error.localizedDescription)"])
_isContinuousRecognitionActive = false
return false
}
}
/// 检查连续识别是否活跃
/// - Returns: 连续识别是否处于活跃状态
func isContinuousRecognitionActive() -> Bool {
return _isContinuousRecognitionActive
}
/// 释放资源
func dispose() {
print("[AzureAsrHelper] 释放资源")
// 停止连续识别
if _isContinuousRecognitionActive {
stopContinuousRecognition()
}
// 关闭麦克风流
microphoneStream?.dispose()
microphoneStream = nil
// 关闭推送流
if let pushStream = pushStreamConfig {
do {
try pushStream.close()
} catch {
print("[AzureAsrHelper] 关闭推送流失败: \(error.localizedDescription)")
}
}
// 释放资源
recognizer = nil
speechConfig = nil
audioConfig = nil
pushStreamConfig = nil
// 重置状态
_isContinuousRecognitionActive = false
isInitialized = false
}
// MARK: - 私有辅助方法
/// 设置连续识别回调
private func setupContinuousRecognitionCallbacks() {
// 最终识别结果
recognizer?.addRecognizedEventHandler { [weak self] _, event in
guard let self = self else { return }
if event.result.reason == SPXResultReason.recognizedSpeech {
let detectedLanguage = self.getDetectedLanguage(from: event.result)
print("[AzureAsrHelper] 识别结果: \(event.result.text ?? ""), 语言: \(detectedLanguage)")
self.eventHandler("result", [
"text": event.result.text ?? "",
"detectedLanguage": detectedLanguage
])
}
}
// 识别中事件
recognizer?.addRecognizingEventHandler { [weak self] _, event in
guard let self = self else { return }
if event.result.reason == SPXResultReason.recognizingSpeech {
let detectedLanguage = self.getDetectedLanguage(from: event.result)
print("[AzureAsrHelper] 识别中: \(event.result.text ?? ""), 语言: \(detectedLanguage)")
self.eventHandler("recognizing", [
"text": event.result.text ?? "",
"detectedLanguage": detectedLanguage
])
}
}
// 会话事件
recognizer?.addSessionStartedEventHandler { [weak self] _, _ in
guard let self = self else { return }
self._isContinuousRecognitionActive = true
self.eventHandler("sessionStarted", [:])
}
recognizer?.addSessionStoppedEventHandler { [weak self] _, _ in
guard let self = self else { return }
self._isContinuousRecognitionActive = false
self.eventHandler("sessionStopped", [:])
}
// 取消事件
recognizer?.addCanceledEventHandler { [weak self] _, event in
guard let self = self else { return }
let reason = event.reason.rawValue
let errorDetails = event.errorDetails ?? ""
self.eventHandler("canceled", [
"reason": reason,
"errorDetails": errorDetails
])
self._isContinuousRecognitionActive = false
self.microphoneStream?.stop()
}
}
/// 从结果中获取检测到的语言
private func getDetectedLanguage(from result: SPXSpeechRecognitionResult) -> String {
if isAutoDetectLanguage {
do {
let langResult = try SPXAutoDetectSourceLanguageResult(result)
return langResult.language ?? currentLanguage
} catch {
print("[AzureAsrHelper] 错误: 获取检测到的语言失败: \(error.localizedDescription)")
return currentLanguage
}
} else {
return currentLanguage
}
}
}

18
azure/ios/Classes/AzureSpeechRecognitionPlugin.swift

@ -0,0 +1,18 @@
import Flutter
import UIKit
public class AzureSpeechRecognitionPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
if #available(iOS 13.0, *) {
SwiftAzureSpeechRecognitionPlugin.register(with: registrar)
} else {
// 如果低于iOS 13.0,返回不支持的错误
let channel = FlutterMethodChannel(name: "com.deep_voice.azure_asr", binaryMessenger: registrar.messenger())
channel.setMethodCallHandler { (call, result) in
result(FlutterError(code: "UNSUPPORTED",
message: "需要iOS 13.0及以上系统",
details: nil))
}
}
}
}

378
azure/ios/Classes/AzureTtsHelper.swift

@ -0,0 +1,378 @@
import Foundation
import MicrosoftCognitiveServicesSpeech
import AVFoundation
/// Azure TTS工具类,负责实现TTS服务接口
@available(iOS 13.0, *)
class AzureTtsHelper: NSObject {
// MARK: - 属性
/// 事件处理回调
private var eventHandler: (String, [String: Any]) -> Void
/// 语音配置信息
private var speechSubscriptionKey: String = ""
private var serviceRegion: String = ""
/// 语音合成配置
private var speechConfig: SPXSpeechConfiguration?
/// 语音合成器
private var synthesizer: SPXSpeechSynthesizer?
/// 是否初始化成功
private var isInitialized = false
/// 当前是否正在播放
private var _isSpeaking = false
// MARK: - 语音设置
/// 当前语音
private var currentVoice = "zh-CN-XiaoxiaoNeural"
/// 支持的语音映射
private var voiceMap: [String: String] = [
"zh-CN": "zh-CN-XiaoxiaoNeural",
"en-US": "en-US-JennyNeural",
"ja-JP": "ja-JP-NanamiNeural",
"ko-KR": "ko-KR-SunHiNeural",
"zh-TW": "zh-TW-HsiaoChenNeural",
"zh-HK": "zh-HK-HiuMaanNeural"
]
/// 当前语音合成参数
private var currentSpeechRate = "0%"
private var currentPitch = "0%"
private var currentVolume = "100%"
// MARK: - 初始化
init(eventHandler: @escaping (String, [String: Any]) -> Void) {
self.eventHandler = eventHandler
super.init()
}
deinit {
dispose()
}
// MARK: - TTS 接口实现
/// 初始化语音合成服务
/// - Parameters:
/// - speechSubscriptionKey: Azure 语音服务订阅密钥
/// - serviceRegion: Azure 服务区域 (如 eastasia)
/// - language: 语言代码 (默认 zh-CN)
/// - Returns: 初始化是否成功
func initialize(speechSubscriptionKey: String, serviceRegion: String, language: String = "zh-CN") -> Bool {
print("[AzureTtsHelper] 初始化语音合成服务")
// 检查配置是否为空
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
eventHandler("error", ["error": "Azure 配置信息不完整"])
return false
}
// 释放之前的资源
dispose()
// 记录配置信息
self.speechSubscriptionKey = speechSubscriptionKey
self.serviceRegion = serviceRegion
do {
// 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: speechSubscriptionKey, region: serviceRegion)
// 设置语音合成输出格式
// speechConfig?.setSpeechSynthesisOutputFormat(.audio24Khz48KBitRateMonoMp3)
// 设置默认语音
let defaultVoice = getDefaultVoiceForLanguage(language)
currentVoice = defaultVoice
speechConfig?.speechSynthesisVoiceName = defaultVoice
// 创建语音合成器
synthesizer = try SPXSpeechSynthesizer(speechConfig!)
// 设置事件处理器
setupSynthesizerEvents()
isInitialized = true
print("[AzureTtsHelper] TTS 引擎初始化成功")
return true
} catch {
print("[AzureTtsHelper] 错误: 初始化语音合成服务失败: \(error.localizedDescription)")
eventHandler("error", ["error": "初始化语音合成服务失败: \(error.localizedDescription)"])
return false
}
}
/// 设置语音
/// - Parameter voiceName: 语音名称 (如 "zh-CN-XiaoxiaoNeural")
/// - Returns: 设置是否成功
func setVoice(voiceName: String) -> Bool {
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler("error", ["error": "TTS 引擎尚未初始化"])
return false
}
if voiceName.isEmpty {
print("[AzureTtsHelper] 错误: 声音名称为空")
eventHandler("error", ["error": "声音名称不能为空"])
return false
}
if voiceName == currentVoice {
print("[AzureTtsHelper] 已设置语音: \(voiceName)")
return true
}
print("[AzureTtsHelper] 设置声音: \(voiceName)")
currentVoice = voiceName
// 更新语音配置
if let speechConfig = speechConfig {
speechConfig.speechSynthesisVoiceName = voiceName
return true
}
return false
}
/// 设置语音合成参数
/// - Parameters:
/// - rate: 语速,范围 -100 到 100,默认为 0
/// - pitch: 音调,范围 -100 到 100,默认为 0
/// - volume: 音量,范围 0 到 100,默认为 100
/// - Returns: 是否设置成功
func setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100) -> Bool {
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler("error", ["error": "TTS 引擎尚未初始化"])
return false
}
// 转换参数格式
currentSpeechRate = formatRateParam(rate)
currentPitch = formatPitchParam(pitch)
currentVolume = formatVolumeParam(volume)
print("[AzureTtsHelper] 已设置语音参数: 语速=\(currentSpeechRate), 音调=\(currentPitch), 音量=\(currentVolume)")
return true
}
/// 合成文本为语音并播放
/// - Parameter text: 要合成的文本
/// - Returns: 操作是否成功启动
func speakText(text: String) -> Bool {
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler("error", ["error": "TTS 引擎尚未初始化"])
return false
}
if text.isEmpty {
print("[AzureTtsHelper] 警告: 要播放的文本为空")
return true
}
print("[AzureTtsHelper] 开始语音合成: \(text.prefix(50))...")
// 生成SSML
let ssml = generateSsml(text: text)
// 直接进行SSML合成
return speakSsmlInternal(text: ssml)
}
/// 内部SSML合成和播放
private func speakSsmlInternal(text: String) -> Bool {
guard let synthesizer = synthesizer else {
print("[AzureTtsHelper] 错误: 合成器未初始化")
eventHandler("error", ["error": "合成器未初始化"])
return false
}
_isSpeaking = true
eventHandler("started", [:])
Task {
do {
print("[AzureTtsHelper] 开始语音合成")
// 使用异步方法进行合成并直接播放
_ = try await synthesizer.startSpeakingSsml(text)
} catch {
print("[AzureTtsHelper] 错误: 语音合成失败: \(error.localizedDescription)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("error", ["error": "语音合成失败: \(error.localizedDescription)"])
}
}
}
return true
}
/// 停止当前语音合成
/// - Returns: 操作是否成功
func stopSpeaking() -> Bool {
if !isInitialized || !_isSpeaking {
return true
}
// 停止合成
do {
try synthesizer?.stopSpeaking()
_isSpeaking = false
eventHandler("canceled", [:])
print("[AzureTtsHelper] 已停止语音合成")
return true
} catch {
print("[AzureTtsHelper] 错误: 停止语音合成失败: \(error.localizedDescription)")
eventHandler("error", ["error": "停止语音合成失败: \(error.localizedDescription)"])
return false
}
}
/// 检查是否正在播放
/// - Returns: 当前是否正在播放语音
func isSpeaking() -> Bool {
return _isSpeaking
}
/// 释放资源
func dispose() {
try? stopSpeaking()
// 释放合成器和配置
synthesizer = nil
speechConfig = nil
isInitialized = false
_isSpeaking = false
print("[AzureTtsHelper] TTS 引擎已释放")
}
// MARK: - 私有辅助方法
/// 设置合成器事件处理
private func setupSynthesizerEvents() {
guard let synthesizer = synthesizer else { return }
// 添加书签到达事件处理
synthesizer.addBookmarkReachedEventHandler { _, e in
print("[AzureTtsHelper] 书签事件: 音频偏移: \((e.audioOffset + 5000) / 10000)ms, 文本: \"\(e.text)\"")
}
// 合成完成事件
synthesizer.addSynthesisCompletedEventHandler { [weak self] _, e in
guard let self = self else { return }
print("[AzureTtsHelper] 语音合成完成: 音频持续时间: \(e.result.audioDuration)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("completed", [:])
}
}
// 合成取消事件
synthesizer.addSynthesisCanceledEventHandler { [weak self] _, e in
guard let self = self else { return }
let result = e.result
do {
let cancellationDetails = try SPXSpeechSynthesisCancellationDetails(fromCanceledSynthesisResult: result)
print("[AzureTtsHelper] 语音合成取消: 原因: \(cancellationDetails.reason)")
if cancellationDetails.reason == SPXCancellationReason.error {
print("[AzureTtsHelper] 错误代码: \(cancellationDetails.errorCode)")
print("[AzureTtsHelper] 错误详情: \(cancellationDetails.errorDetails ?? "未知")")
}
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("error", ["error": "语音合成取消: \(cancellationDetails.errorDetails ?? "未知错误")"])
}
} catch {
print("[AzureTtsHelper] 获取取消详情时出错: \(error)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("error", ["error": "语音合成被取消"])
}
}
}
// 合成开始事件
synthesizer.addSynthesisStartedEventHandler { _, _ in
print("[AzureTtsHelper] 语音合成开始")
}
// 合成中事件
synthesizer.addSynthesizingEventHandler { _, _ in
print("[AzureTtsHelper] 语音合成中")
}
}
/// 生成 SSML 文本
private func generateSsml(text: String) -> String {
return """
<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='zh-CN'>
<voice name='\(currentVoice)'>
<prosody rate='\(currentSpeechRate)' pitch='\(currentPitch)' volume='\(currentVolume)'>
\(text)
</prosody>
</voice>
</speak>
"""
}
/// 格式化语速参数
private func formatRateParam(_ rate: Int) -> String {
let clampedRate = rate.clamp(min: -100, max: 100)
if clampedRate == 0 {
return "0%"
} else if clampedRate < 0 {
return "\(Int(Double(clampedRate) * 0.9))%"
} else {
return "+\(clampedRate)%"
}
}
/// 格式化音调参数
private func formatPitchParam(_ pitch: Int) -> String {
let clampedPitch = pitch.clamp(min: -100, max: 100)
if clampedPitch == 0 {
return "0%"
} else {
return "\(Int(Double(clampedPitch) * 0.5))%"
}
}
/// 格式化音量参数
private func formatVolumeParam(_ volume: Int) -> String {
let clampedVolume = volume.clamp(min: 0, max: 100)
return "\(clampedVolume)%"
}
/// 获取指定语言的默认语音
private func getDefaultVoiceForLanguage(_ language: String) -> String {
return voiceMap[language] ?? "zh-CN-XiaoxiaoNeural"
}
}
// MARK: - 扩展
extension Int {
func clamp(min: Int, max: Int) -> Int {
if self < min { return min }
if self > max { return max }
return self
}
}

263
azure/ios/Classes/SwiftAzureSpeechRecognitionPlugin.swift

@ -0,0 +1,263 @@
import Flutter
import UIKit
import MicrosoftCognitiveServicesSpeech
import AVFoundation
@available(iOS 13.0, *)
public class SwiftAzureSpeechRecognitionPlugin: NSObject, FlutterPlugin {
private var azureChannel: FlutterMethodChannel
private var ttsChannel: FlutterMethodChannel
private var asrHelper: AzureAsrHelper
private var ttsHelper: AzureTtsHelper
private static var eventStreamHandler: AzureEventStreamHandler?
// 创建方法到通道的映射
private static var ttsMethodHandlers = [String: FlutterMethodCallHandler]()
private static var asrMethodHandlers = [String: FlutterMethodCallHandler]()
public static func register(with registrar: FlutterPluginRegistrar) {
// ASR通道
let channel = FlutterMethodChannel(name: "com.deep_voice.azure_asr", binaryMessenger: registrar.messenger())
// TTS通道
let ttsChannel = FlutterMethodChannel(name: "com.deep_voice.azure_tts", binaryMessenger: registrar.messenger())
// 设置ASR事件通道
let eventChannel = FlutterEventChannel(name: "com.deep_voice.azure_asr_events", binaryMessenger: registrar.messenger())
eventStreamHandler = AzureEventStreamHandler()
eventChannel.setStreamHandler(eventStreamHandler)
let instance = SwiftAzureSpeechRecognitionPlugin(
azureChannel: channel,
ttsChannel: ttsChannel,
eventStreamHandler: eventStreamHandler!
)
// 直接设置各自通道的处理器
channel.setMethodCallHandler(instance.handleAsrMethodCalls)
ttsChannel.setMethodCallHandler(instance.handleTtsMethodCalls)
}
// 新增直接处理方法调用的函数
private func handleTtsMethodCalls(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
print("[AzurePlugin] 处理TTS方法调用: \(call.method)")
handleTtsMethod(call, result)
}
private func handleAsrMethodCalls(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
print("[AzurePlugin] 处理ASR方法调用: \(call.method)")
handleAsrMethod(call, result)
}
init(azureChannel: FlutterMethodChannel, ttsChannel: FlutterMethodChannel, eventStreamHandler: AzureEventStreamHandler) {
self.azureChannel = azureChannel
self.ttsChannel = ttsChannel
// 创建辅助类实例,使用自定义事件回调处理器
let eventHandler: (String, [String: Any]) -> Void = { eventName, arguments in
DispatchQueue.main.async {
if let eventSink = SwiftAzureSpeechRecognitionPlugin.eventStreamHandler?.eventSink {
var eventData = arguments
eventData["type"] = eventName
eventSink(eventData)
}
}
}
asrHelper = AzureAsrHelper(eventHandler: eventHandler)
ttsHelper = AzureTtsHelper(eventHandler: eventHandler)
super.init()
}
private func handleAsrMethod(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
print("[AzurePlugin] 处理ASR方法: \(call.method)")
let args = call.arguments as? Dictionary<String, Any>
switch call.method {
case "initialize":
// 仅在初始化时读取必要参数
guard let speechSubscriptionKey = args?["subscriptionKey"] as? String, !speechSubscriptionKey.isEmpty else {
let errorMsg = "语音订阅密钥不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_SUBSCRIPTION_KEY", message: errorMsg, details: nil))
return
}
guard let serviceRegion = args?["region"] as? String, !serviceRegion.isEmpty else {
let errorMsg = "服务区域不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_REGION", message: errorMsg, details: nil))
return
}
let supportedLanguages = args?["supportedLanguages"] as? [String] ?? []
let success = asrHelper.initialize(
speechSubscriptionKey: speechSubscriptionKey,
serviceRegion: serviceRegion,
supportedLanguages: supportedLanguages.isEmpty ? nil : supportedLanguages
)
result(success)
case "startContinuousRecognition":
// 只有使用参数时才验证
let success = asrHelper.startContinuousRecognition()
result(success)
case "stopContinuousRecognition":
// 不需要额外参数
let success = asrHelper.stopContinuousRecognition()
result(success)
case "recognizeOnce":
// 只有使用参数时才验证
let success = asrHelper.recognizeOnce()
result(success)
case "isContinuousRecognitionActive":
// 不需要额外参数
result(asrHelper.isContinuousRecognitionActive())
case "dispose":
// 不需要额外参数
print("[AzurePlugin] 释放ASR资源")
asrHelper.dispose()
result(true)
default:
print("[AzurePlugin] 错误: 未知ASR方法: \(call.method)")
result(FlutterMethodNotImplemented)
}
}
private func handleTtsMethod(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
print("[AzurePlugin] 处理TTS方法: \(call.method)")
let args = call.arguments as? Dictionary<String, Any>
switch call.method {
case "initialize":
// 仅在初始化时验证参数
guard let speechSubscriptionKey = args?["subscriptionKey"] as? String, !speechSubscriptionKey.isEmpty else {
let errorMsg = "语音订阅密钥不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_SUBSCRIPTION_KEY", message: errorMsg, details: nil))
return
}
guard let serviceRegion = args?["region"] as? String, !serviceRegion.isEmpty else {
let errorMsg = "服务区域不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_REGION", message: errorMsg, details: nil))
return
}
let language = args?["language"] as? String ?? "zh-CN"
print("[AzurePlugin] 初始化TTS,语言: \(language)")
let success = ttsHelper.initialize(speechSubscriptionKey: speechSubscriptionKey, serviceRegion: serviceRegion, language: language)
result(success)
case "setVoice":
// 仅获取voice参数
guard let voiceName = args?["voiceName"] as? String, !voiceName.isEmpty else {
let errorMsg = "声音名称不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_VOICE", message: errorMsg, details: nil))
return
}
print("[AzurePlugin] 设置声音: \(voiceName)")
let success = ttsHelper.setVoice(voiceName: voiceName)
result(success)
case "speakText":
// 仅获取text参数
let text = args?["text"] as? String ?? ""
if text.isEmpty {
print("[AzurePlugin] 警告: 要播放的文本为空")
result("OK")
return
}
print("[AzurePlugin] 播放文本: \(text.prefix(50))...")
let success = ttsHelper.speakText(text: text)
result(success ? "OK" : "ERROR")
case "speakSsml":
// 仅获取ssml参数
guard let ssml = args?["ssml"] as? String, !ssml.isEmpty else {
let errorMsg = "SSML内容不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_SSML", message: errorMsg, details: nil))
return
}
print("[AzurePlugin] 播放SSML: \(ssml.prefix(100))...")
// 由于我们移除了speakSsml方法,这里改用speakText方法
// Azure SDK内部会自动检测是普通文本还是SSML
let success = ttsHelper.speakText(text: ssml)
result(success)
case "stopSpeaking":
// 不需要参数
print("[AzurePlugin] 停止播放")
let success = ttsHelper.stopSpeaking()
result(success)
case "isSpeaking":
// 不需要参数
result(ttsHelper.isSpeaking())
case "setSpeechParams":
// 仅获取语音参数
let rate = args?["rate"] as? Int ?? 0
let pitch = args?["pitch"] as? Int ?? 0
let volume = args?["volume"] as? Int ?? 100
print("[AzurePlugin] 设置语音参数: rate=\(rate), pitch=\(pitch), volume=\(volume)")
let success = ttsHelper.setSpeechParams(rate: rate, pitch: pitch, volume: volume)
result(success)
case "dispose":
// 释放TTS资源
print("[AzurePlugin] 释放TTS资源")
ttsHelper.dispose()
result(true)
default:
print("[AzurePlugin] 错误: 未知TTS方法: \(call.method)")
result(FlutterMethodNotImplemented)
}
}
}
// 用于处理事件流的辅助类
@available(iOS 13.0, *)
class AzureEventStreamHandler: NSObject, FlutterStreamHandler {
var eventSink: FlutterEventSink?
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
// 通知Flutter端事件通道已准备好
DispatchQueue.main.async {
events(["type": "channelReady"])
}
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
self.eventSink = nil
return nil
}
}

24
azure/ios/azure_speech_recognition.podspec

@ -0,0 +1,24 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint azure_speech_recognition.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'azure_speech_recognition'
s.version = '0.1.0'
s.summary = 'Azure Speech Recognition plugin for Flutter'
s.description = <<-DESC
A Flutter plugin for Microsoft Azure Speech services, providing both speech recognition (ASR) and text-to-speech (TTS) capabilities.
DESC
s.homepage = 'https://github.com/yourusername/azure_speech_recognition'
s.license = { :type => 'MIT', :file => '../LICENSE' }
s.author = { 'Your Company' => 'your-email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.dependency 'MicrosoftCognitiveServicesSpeech-iOS', '~> 1.34.0'
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
end

6
azure/lib/azure_speech_recognition.dart

@ -0,0 +1,6 @@
// This is a placeholder file that exports nothing.
// The actual implementation is in the app's services folder.
// This file exists just to satisfy the Flutter plugin structure requirements.
// Empty library to satisfy plugin structure
library azure_speech_recognition;

23
azure/pubspec.yaml

@ -0,0 +1,23 @@
name: azure_speech_recognition
description: Azure Speech Recognition and Text-to-Speech services Flutter plugin
version: 0.1.0
homepage: https://github.com/yourusername/azure_speech_recognition
environment:
sdk: '>=2.12.0 <3.0.0'
flutter: ">=2.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^1.0.0
flutter:
plugin:
platforms:
ios:
pluginClass: AzureSpeechRecognitionPlugin

6
ios/Podfile

@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project # Uncomment this line to define a global platform for your project
# platform :ios, '12.0' platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@ -31,6 +31,10 @@ target 'Runner' do
use_frameworks! use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
# 添加 Microsoft Cognitive Services Speech SDK
pod 'MicrosoftCognitiveServicesSpeech-iOS', '~> 1.34.0'
target 'RunnerTests' do target 'RunnerTests' do
inherit! :search_paths inherit! :search_paths
end end

67
ios/Podfile.lock

@ -1,21 +1,32 @@
PODS: PODS:
- app_settings (5.1.1):
- Flutter
- audio_service (0.0.1): - audio_service (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- audio_session (0.0.1): - audio_session (0.0.1):
- Flutter - Flutter
- azure_speech_recognition_null_safety (0.0.1): - audio_waveforms (0.0.1):
- Flutter
- azure_speech_recognition (0.1.0):
- Flutter - Flutter
- MicrosoftCognitiveServicesSpeech-iOS (~> 1.29.0) - MicrosoftCognitiveServicesSpeech-iOS (~> 1.34.0)
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
- Flutter - Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_blue_plus_darwin (0.0.2):
- Flutter
- FlutterMacOS
- flutter_local_notifications (0.0.1): - flutter_local_notifications (0.0.1):
- Flutter - Flutter
- flutter_tts (0.0.1):
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- just_audio (0.0.1): - just_audio (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- MicrosoftCognitiveServicesSpeech-iOS (1.29.0) - MicrosoftCognitiveServicesSpeech-iOS (1.34.0)
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@ -23,40 +34,66 @@ PODS:
- Flutter - Flutter
- record_darwin (1.0.0): - record_darwin (1.0.0):
- Flutter - Flutter
- share_plus (0.0.1):
- Flutter
- speech_to_text (0.0.1):
- Flutter
- FlutterMacOS
- Try
- sqflite_darwin (0.0.4): - sqflite_darwin (0.0.4):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- Try (2.1.1)
DEPENDENCIES: DEPENDENCIES:
- app_settings (from `.symlinks/plugins/app_settings/ios`)
- audio_service (from `.symlinks/plugins/audio_service/darwin`) - audio_service (from `.symlinks/plugins/audio_service/darwin`)
- audio_session (from `.symlinks/plugins/audio_session/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`)
- azure_speech_recognition_null_safety (from `.symlinks/plugins/azure_speech_recognition_null_safety/ios`) - audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`)
- azure_speech_recognition (from `.symlinks/plugins/azure_speech_recognition/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_tts (from `.symlinks/plugins/flutter_tts/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- just_audio (from `.symlinks/plugins/just_audio/darwin`) - just_audio (from `.symlinks/plugins/just_audio/darwin`)
- MicrosoftCognitiveServicesSpeech-iOS (~> 1.34.0)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- record_darwin (from `.symlinks/plugins/record_darwin/ios`) - record_darwin (from `.symlinks/plugins/record_darwin/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- speech_to_text (from `.symlinks/plugins/speech_to_text/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
- MicrosoftCognitiveServicesSpeech-iOS - MicrosoftCognitiveServicesSpeech-iOS
- Try
EXTERNAL SOURCES: EXTERNAL SOURCES:
app_settings:
:path: ".symlinks/plugins/app_settings/ios"
audio_service: audio_service:
:path: ".symlinks/plugins/audio_service/darwin" :path: ".symlinks/plugins/audio_service/darwin"
audio_session: audio_session:
:path: ".symlinks/plugins/audio_session/ios" :path: ".symlinks/plugins/audio_session/ios"
azure_speech_recognition_null_safety: audio_waveforms:
:path: ".symlinks/plugins/azure_speech_recognition_null_safety/ios" :path: ".symlinks/plugins/audio_waveforms/ios"
azure_speech_recognition:
:path: ".symlinks/plugins/azure_speech_recognition/ios"
device_info_plus: device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios" :path: ".symlinks/plugins/device_info_plus/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_blue_plus_darwin:
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
flutter_local_notifications: flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios" :path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_tts:
:path: ".symlinks/plugins/flutter_tts/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
just_audio: just_audio:
:path: ".symlinks/plugins/just_audio/darwin" :path: ".symlinks/plugins/just_audio/darwin"
path_provider_foundation: path_provider_foundation:
@ -65,23 +102,35 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/permission_handler_apple/ios" :path: ".symlinks/plugins/permission_handler_apple/ios"
record_darwin: record_darwin:
:path: ".symlinks/plugins/record_darwin/ios" :path: ".symlinks/plugins/record_darwin/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
speech_to_text:
:path: ".symlinks/plugins/speech_to_text/darwin"
sqflite_darwin: sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin" :path: ".symlinks/plugins/sqflite_darwin/darwin"
SPEC CHECKSUMS: SPEC CHECKSUMS:
app_settings: 5127ae0678de1dcc19f2293271c51d37c89428b2
audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
azure_speech_recognition_null_safety: aed1d7ededc73a1d0b7a342b1f4803ea166e2747 audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
azure_speech_recognition: 063bf4f294dbabaf2741f529f76132b2c38b744b
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_blue_plus_darwin: 09444a26fb6bdef523e55b68fc1c59af5a877ea6
flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4 flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4
flutter_tts: b88dbc8655d3dc961bc4a796e4e16a4cc1795833
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
MicrosoftCognitiveServicesSpeech-iOS: 0de20a8ebb6bbd87decaeaa9a707dd40830106a6 MicrosoftCognitiveServicesSpeech-iOS: 723eccb3e459bbb530df3d9bfe84a291074b09eb
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
speech_to_text: 9dc43a5df3cbc2813f8c7cc9bd0fbf94268ed7ac
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96
PODFILE CHECKSUM: 359b0564325fb2f8842a19a47bd7e63cbdcd9c04 PODFILE CHECKSUM: f35c1d63b4b8ce9c2eb88107f162ac59923c4839
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

148
ios/Runner.xcodeproj/project.pbxproj

@ -7,15 +7,16 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
0A5EDBFC2D89C4FD00215E26 /* ClassicBluetoothHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3406320CAE3314A73EF78043 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
993AA22300B2BA544C45E2B2 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D59A10400354CAD76662DBB3 /* Pods_RunnerTests.framework */; }; E1D2128FAB4726BEEED42F4C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4A9190D0838960F143BDC158 /* Pods_Runner.framework */; };
F3E628587CFA36508BAB3B56 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 735C476F36391FA72F7CA27A /* Pods_Runner.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -42,18 +43,22 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
02CF74744224D1C933681603 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
0906EF7A73CBA5EAFC64E05D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicBluetoothHelper.swift; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
478E46ED4F3AE11E50872120 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; 4A9190D0838960F143BDC158 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
5C3B0E63225CBCC97A48145A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; }; 5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
671AD9D18AB66E08CC945D07 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
735C476F36391FA72F7CA27A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
77D16D761891F29239C8DF02 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
8C1130E20389847CBD7B1AAC /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
930FE6D58BDB20688CE0A7CF /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -61,26 +66,23 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
BCC52C2C6E872DD19D86EA8B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; }; 9B84B894288BF76332D3F407 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
C0DB3B999FA4C3E6CFA90079 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
D59A10400354CAD76662DBB3 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
F83C5E570162235C4FE20CCB /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
6AE85031FDE82288FAE3C673 /* Frameworks */ = { 97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
993AA22300B2BA544C45E2B2 /* Pods_RunnerTests.framework in Frameworks */, E1D2128FAB4726BEEED42F4C /* Pods_Runner.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
97C146EB1CF9000F007C117D /* Frameworks */ = { B4459155A2535A0ED1D6C7D8 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
F3E628587CFA36508BAB3B56 /* Pods_Runner.framework in Frameworks */, 3406320CAE3314A73EF78043 /* Pods_RunnerTests.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -95,6 +97,19 @@
path = RunnerTests; path = RunnerTests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
776E5AB1CE0A62CBDBAB3996 /* Pods */ = {
isa = PBXGroup;
children = (
8C1130E20389847CBD7B1AAC /* Pods-Runner.debug.xcconfig */,
77D16D761891F29239C8DF02 /* Pods-Runner.release.xcconfig */,
0906EF7A73CBA5EAFC64E05D /* Pods-Runner.profile.xcconfig */,
930FE6D58BDB20688CE0A7CF /* Pods-RunnerTests.debug.xcconfig */,
02CF74744224D1C933681603 /* Pods-RunnerTests.release.xcconfig */,
9B84B894288BF76332D3F407 /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = { 9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -113,8 +128,8 @@
97C146F01CF9000F007C117D /* Runner */, 97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */, 97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */, 331C8082294A63A400263BE5 /* RunnerTests */,
9DDB11208866A014130CF5A9 /* Pods */, 776E5AB1CE0A62CBDBAB3996 /* Pods */,
D1DDBFE3838710CC012E9C02 /* Frameworks */, A5A9FDE94E78257E9A48E2E8 /* Frameworks */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -130,6 +145,7 @@
97C146F01CF9000F007C117D /* Runner */ = { 97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */,
97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@ -142,25 +158,11 @@
path = Runner; path = Runner;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
9DDB11208866A014130CF5A9 /* Pods */ = { A5A9FDE94E78257E9A48E2E8 /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
F83C5E570162235C4FE20CCB /* Pods-Runner.debug.xcconfig */, 4A9190D0838960F143BDC158 /* Pods_Runner.framework */,
C0DB3B999FA4C3E6CFA90079 /* Pods-Runner.release.xcconfig */, 5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */,
478E46ED4F3AE11E50872120 /* Pods-Runner.profile.xcconfig */,
5C3B0E63225CBCC97A48145A /* Pods-RunnerTests.debug.xcconfig */,
BCC52C2C6E872DD19D86EA8B /* Pods-RunnerTests.release.xcconfig */,
671AD9D18AB66E08CC945D07 /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
D1DDBFE3838710CC012E9C02 /* Frameworks */ = {
isa = PBXGroup;
children = (
735C476F36391FA72F7CA27A /* Pods_Runner.framework */,
D59A10400354CAD76662DBB3 /* Pods_RunnerTests.framework */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@ -172,10 +174,10 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = ( buildPhases = (
4CED93D53BF7D79C894B39B7 /* [CP] Check Pods Manifest.lock */, 1BD7F444CF127CDFCB7F9F47 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */, 331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */, 331C807F294A63A400263BE5 /* Resources */,
6AE85031FDE82288FAE3C673 /* Frameworks */, B4459155A2535A0ED1D6C7D8 /* Frameworks */,
); );
buildRules = ( buildRules = (
); );
@ -191,15 +193,15 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
CB1E1186EB3C828E55784697 /* [CP] Check Pods Manifest.lock */, 8A6DB356BF41E96BFD7DF109 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */, 9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */, 97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */, 97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */, 97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
D7B8455F87E542E882C75D57 /* [CP] Embed Pods Frameworks */, 4A5C19D622EF675DB4056F07 /* [CP] Embed Pods Frameworks */,
E839EB3BEF50366A68B2A46D /* [CP] Copy Pods Resources */, D49299AF1487DF3783287D24 /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@ -271,23 +273,7 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 1BD7F444CF127CDFCB7F9F47 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
4CED93D53BF7D79C894B39B7 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@ -309,22 +295,40 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
9740EEB61CF901F6004384FC /* Run Script */ = { 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1; alwaysOutOfDate = 1;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
inputPaths = ( inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
); );
name = "Run Script"; name = "Thin Binary";
outputPaths = ( outputPaths = (
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
}; };
CB1E1186EB3C828E55784697 /* [CP] Check Pods Manifest.lock */ = { 4A5C19D622EF675DB4056F07 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
8A6DB356BF41E96BFD7DF109 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@ -346,24 +350,22 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
D7B8455F87E542E882C75D57 /* [CP] Embed Pods Frameworks */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
inputFileListPaths = ( inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
name = "[CP] Embed Pods Frameworks"; name = "Run Script";
outputFileListPaths = ( outputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
showEnvVarsInLog = 0;
}; };
E839EB3BEF50366A68B2A46D /* [CP] Copy Pods Resources */ = { D49299AF1487DF3783287D24 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@ -395,6 +397,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
0A5EDBFC2D89C4FD00215E26 /* ClassicBluetoothHelper.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
); );
@ -505,7 +508,7 @@
}; };
331C8088294A63A400263BE5 /* Debug */ = { 331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 5C3B0E63225CBCC97A48145A /* Pods-RunnerTests.debug.xcconfig */; baseConfigurationReference = 930FE6D58BDB20688CE0A7CF /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@ -523,7 +526,7 @@
}; };
331C8089294A63A400263BE5 /* Release */ = { 331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = BCC52C2C6E872DD19D86EA8B /* Pods-RunnerTests.release.xcconfig */; baseConfigurationReference = 02CF74744224D1C933681603 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@ -539,7 +542,7 @@
}; };
331C808A294A63A400263BE5 /* Profile */ = { 331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 671AD9D18AB66E08CC945D07 /* Pods-RunnerTests.profile.xcconfig */; baseConfigurationReference = 9B84B894288BF76332D3F407 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@ -671,13 +674,14 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice; PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";

109
ios/Runner/AppDelegate.swift

@ -3,11 +3,120 @@ import UIKit
@main @main
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate {
private var classicBluetoothHelper: ClassicBluetoothHelper?
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
// 初始化ClassicBluetoothHelper
setupClassicBluetoothHelper()
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
private func setupClassicBluetoothHelper() {
let controller = window?.rootViewController as? FlutterViewController
if let controller = controller {
// 初始化ClassicBluetoothHelper
classicBluetoothHelper = ClassicBluetoothHelper()
// 启动蓝牙设备监控
classicBluetoothHelper?.startBluetoothDeviceMonitoring()
// 注册蓝牙通道
let channel = FlutterMethodChannel(name: "com.deep_voice.classic_bluetooth", binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler { [weak self] (call, result) in
guard let self = self else {
result(FlutterError(code: "UNAVAILABLE", message: "ClassicBluetoothHelper实例不可用", details: nil))
return
}
switch call.method {
case "isBluetoothEnabled":
result(self.classicBluetoothHelper?.isBluetoothEnabled())
case "getConnectedA2dpDevices":
self.classicBluetoothHelper?.getConnectedA2dpDevices { devices, error in
if let error = error {
result(FlutterError(code: "GET_A2DP_DEVICES_FAILED", message: error, details: nil))
} else {
result(devices)
}
}
case "getConnectedHeadsetDevices":
self.classicBluetoothHelper?.getConnectedHeadsetDevices { devices, error in
if let error = error {
result(FlutterError(code: "GET_HEADSET_DEVICES_FAILED", message: error, details: nil))
} else {
result(devices)
}
}
case "checkAndRequestNativePermission":
self.classicBluetoothHelper?.checkAndRequestNativePermission { permissionStatus in
result(permissionStatus)
}
case "startBluetoothDeviceMonitoring":
self.classicBluetoothHelper?.startBluetoothDeviceMonitoring()
result(true)
case "stopBluetoothDeviceMonitoring":
self.classicBluetoothHelper?.stopBluetoothDeviceMonitoring()
result(true)
case "openSettings":
result(self.classicBluetoothHelper?.openSettings())
case "openBluetoothSettings":
result(self.classicBluetoothHelper?.openBluetoothSettings())
default:
result(FlutterMethodNotImplemented)
}
}
// 注册蓝牙事件通道
let eventChannel = FlutterEventChannel(name: "com.deep_voice.classic_bluetooth_events", binaryMessenger: controller.binaryMessenger)
// 创建事件处理器
let eventHandler = BluetoothStreamHandler(bluetoothHelper: self.classicBluetoothHelper!)
eventChannel.setStreamHandler(eventHandler)
}
}
}
// 蓝牙事件流处理器
class BluetoothStreamHandler: NSObject, FlutterStreamHandler {
private let bluetoothHelper: ClassicBluetoothHelper
private var eventSink: FlutterEventSink?
init(bluetoothHelper: ClassicBluetoothHelper) {
self.bluetoothHelper = bluetoothHelper
super.init()
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
NSLog("BluetoothStreamHandler: 开始监听蓝牙事件")
self.eventSink = events
// 注册设备事件回调
bluetoothHelper.registerDeviceEventCallback { [weak self] event in
NSLog("BluetoothStreamHandler: 收到蓝牙事件 \(event)")
self?.eventSink?(event)
}
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
NSLog("BluetoothStreamHandler: 取消监听蓝牙事件")
// 不要在这里调用dispose,只需清除回调
eventSink = nil
return nil
}
} }

13
ios/Runner/Base.lproj/Main.storyboard

@ -1,8 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
<!--Flutter View Controller--> <!--Flutter View Controller-->
@ -14,13 +16,14 @@
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/> <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides> </layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC"> <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/> <rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/> <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view> </view>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="-26" y="-77"/>
</scene> </scene>
</scenes> </scenes>
</document> </document>

523
ios/Runner/ClassicBluetoothHelper.swift

@ -0,0 +1,523 @@
import Foundation
import CoreBluetooth
import AVFoundation
import UIKit
@objc class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate, CBPeripheralManagerDelegate {
private let TAG = "ClassicBluetoothHelper"
// AVAudioSession 用于获取已连接的蓝牙设备
private let audioSession = AVAudioSession.sharedInstance()
// 蓝牙管理器
private var centralManager: CBCentralManager?
private var peripheralManager: CBPeripheralManager?
// 设备列表缓存
private var bluetoothDevices: [[String: String]] = []
// 设备连接和断开回调
private var deviceEventCallback: (([String: Any]) -> Void)?
// 初始化
override init() {
super.init()
// 初始化中央管理器来检查蓝牙状态,并将自己设置为代理
centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: true])
// 初始化外设管理器
peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
// 请求权限
requestBluetoothPermissions()
}
// 请求蓝牙权限
private func requestBluetoothPermissions() {
// 在 iOS 13 及更高版本中,需要主动请求蓝牙权限
if #available(iOS 13.0, *) {
// 激活音频会话将触发系统蓝牙权限请求
do {
try audioSession.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .allowBluetoothA2DP])
try audioSession.setActive(true)
NSLog("\(TAG): 已请求音频会话蓝牙权限")
} catch {
NSLog("\(TAG): 激活音频会话失败: \(error.localizedDescription)")
}
}
}
// CBCentralManagerDelegate 方法
func centralManagerDidUpdateState(_ central: CBCentralManager) {
var stateString = "unknown"
switch central.state {
case .poweredOn:
stateString = "on"
NSLog("\(TAG): centralManagerDidUpdateState: 蓝牙已启用")
// 蓝牙已打开,可以开始扫描或其他操作
// 初始化设备列表 - 主要是为了触发 iOS 的权限请求
_ = getConnectedAudioDevices()
case .poweredOff:
stateString = "off"
NSLog("\(TAG): centralManagerDidUpdateState: 蓝牙已关闭")
// 清空设备列表
bluetoothDevices.removeAll()
case .resetting:
stateString = "resetting"
NSLog("\(TAG): centralManagerDidUpdateState: 蓝牙正在重置")
case .unauthorized:
stateString = "unauthorized"
NSLog("\(TAG): centralManagerDidUpdateState: 蓝牙使用未授权")
case .unsupported:
stateString = "unsupported"
NSLog("\(TAG): centralManagerDidUpdateState: 设备不支持蓝牙")
case .unknown:
stateString = "unknown"
NSLog("\(TAG): centralManagerDidUpdateState: 蓝牙状态未知")
@unknown default:
stateString = "unknown"
NSLog("\(TAG): centralManagerDidUpdateState: 蓝牙状态未知(default)")
}
NSLog("\(TAG): centralManagerDidUpdateState: 发送蓝牙状态变化事件: \(stateString)")
// 发送蓝牙状态变化事件
if let callback = deviceEventCallback {
let event: [String: Any] = [
"type": "bluetoothStateChanged",
"state": stateString,
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
callback(event)
} else {
NSLog("\(TAG): centralManagerDidUpdateState: 没有回调注册,无法发送事件")
}
}
// CBPeripheralManagerDelegate 方法
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
NSLog("\(TAG): 外设管理器状态变化: \(peripheral.state.rawValue)")
}
// 注册设备事件回调
@objc func registerDeviceEventCallback(_ callback: @escaping ([String: Any]) -> Void) {
deviceEventCallback = callback
}
// 检查蓝牙是否启用
@objc func isBluetoothEnabled() -> Bool {
guard let manager = centralManager else { return false }
return manager.state == .poweredOn
}
// 获取已连接的音频设备 (A2DP 设备)
@objc func getConnectedA2dpDevices(_ completion: @escaping ([[String: String]]?, String?) -> Void) {
// 检查蓝牙状态和权限
let (enabled, authorized) = checkBluetoothPermission()
if !enabled {
NSLog("\(TAG): 蓝牙未启用")
completion(nil, "蓝牙未启用,请在设置中开启蓝牙")
return
}
if !authorized {
NSLog("\(TAG): 蓝牙权限被拒绝")
completion(nil, "蓝牙权限被拒绝,请在设置中允许蓝牙访问")
return
}
// 尝试激活音频会话以获取设备信息
do {
try audioSession.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .allowBluetoothA2DP])
try audioSession.setActive(true)
// 获取输出设备
let devices = self.getConnectedAudioDevices()
// 记录找到的设备数量
NSLog("\(TAG): 找到 \(devices.count) 个A2DP设备")
completion(devices, nil)
} catch {
NSLog("\(TAG): 获取A2DP设备失败: \(error.localizedDescription)")
completion(nil, "获取A2DP设备失败: \(error.localizedDescription)")
}
}
// 获取已连接的耳机设备
@objc func getConnectedHeadsetDevices(_ completion: @escaping ([[String: String]]?, String?) -> Void) {
// 检查蓝牙状态和权限
let (enabled, authorized) = checkBluetoothPermission()
if !enabled {
NSLog("\(TAG): 蓝牙未启用")
completion(nil, "蓝牙未启用,请在设置中开启蓝牙")
return
}
if !authorized {
NSLog("\(TAG): 蓝牙权限被拒绝")
completion(nil, "蓝牙权限被拒绝,请在设置中允许蓝牙访问")
return
}
// 尝试激活音频会话以获取设备信息
do {
try audioSession.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth])
try audioSession.setActive(true)
// 获取输出设备
let devices = self.getConnectedAudioDevices()
// 记录找到的设备数量
NSLog("\(TAG): 找到 \(devices.count) 个耳机设备")
completion(devices, nil)
} catch {
NSLog("\(TAG): 获取耳机设备失败: \(error.localizedDescription)")
completion(nil, "获取耳机设备失败: \(error.localizedDescription)")
}
}
// 使用原生方式检查蓝牙权限
private func checkBluetoothPermission() -> (enabled: Bool, authorized: Bool) {
// 检查蓝牙是否启用
let isEnabled = centralManager?.state == .poweredOn
// 检查蓝牙权限
var isAuthorized = true
var authDescription = "unknown"
if #available(iOS 13.0, *) {
let authStatus = centralManager?.authorization
switch authStatus {
case .allowedAlways:
authDescription = "allowedAlways"
isAuthorized = true
case .denied:
authDescription = "denied"
isAuthorized = false
case .restricted:
authDescription = "restricted"
isAuthorized = false
case .notDetermined:
authDescription = "notDetermined"
// 未决定状态仍然当作授权, 因为系统会在实际使用时弹出请求
isAuthorized = true
default:
authDescription = "unknown"
isAuthorized = true
}
NSLog("\(TAG): 原生蓝牙权限状态: \(authDescription)")
} else {
// iOS 13 以下版本没有细粒度的权限控制
authDescription = "legacy_version"
NSLog("\(TAG): iOS版本低于13,使用旧版权限模型")
}
NSLog("\(TAG): 蓝牙状态: 启用=\(isEnabled), 已授权=\(isAuthorized), 权限描述=\(authDescription)")
return (isEnabled, isAuthorized)
}
// 原生方式检查蓝牙权限并返回给 Flutter
@objc func checkAndRequestNativePermission(_ completion: @escaping ([String: Any]) -> Void) {
// 确保 centralManager 已初始化
if centralManager == nil {
centralManager = CBCentralManager(delegate: self, queue: nil)
NSLog("\(self.TAG): 创建新的蓝牙管理器用于权限检查")
}
// 延迟执行检查,确保 centralManager 状态已更新
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// 再次检查蓝牙权限,以获取最新状态
let (enabled, authorized) = self.checkBluetoothPermission()
// 尝试主动请求蓝牙权限(如果尚未决定)
if #available(iOS 13.0, *) {
if self.centralManager?.authorization == .notDetermined {
NSLog("\(self.TAG): 蓝牙权限状态为未决定,尝试主动请求权限")
// 启动扫描会触发系统权限请求
self.centralManager?.scanForPeripherals(withServices: nil, options: nil)
// 立即停止扫描
self.centralManager?.stopScan()
}
}
let result: [String: Any] = [
"enabled": enabled,
"authorized": authorized,
"status": self.getPermissionStatusText(enabled: enabled, authorized: authorized)
]
NSLog("\(self.TAG): 返回权限检查结果: \(result)")
completion(result)
}
}
// 获取权限状态文本
private func getPermissionStatusText(enabled: Bool, authorized: Bool) -> String {
if !enabled {
return "disabled" // 蓝牙已禁用
}
if !authorized {
return "denied" // 权限被拒绝
}
return "granted" // 已授权
}
// 获取当前连接的音频设备
private func getConnectedAudioDevices() -> [[String: String]] {
var result: [[String: String]] = []
// 尝试初始化设备列表
if bluetoothDevices.isEmpty {
// 只在第一次使用时初始化
NSLog("\(TAG): 第一次获取蓝牙设备,初始化设备列表")
}
// 获取可用的音频输出设备
guard let outputs = audioSession.currentRoute.outputs as? [AVAudioSessionPortDescription] else {
NSLog("\(TAG): 无法获取当前音频输出设备")
return result
}
NSLog("\(TAG): 当前音频路由包含 \(outputs.count) 个输出设备")
// 过滤蓝牙相关设备
for output in outputs {
NSLog("\(TAG): 检查音频输出设备: \(output.portName), 类型: \(output.portType.rawValue)")
if output.portType == .bluetoothA2DP || output.portType == .bluetoothHFP || output.portType == .bluetoothLE {
let device: [String: String] = [
"name": output.portName,
"address": output.uid // iOS 使用 UID 作为设备标识符
]
result.append(device)
NSLog("\(TAG): 添加蓝牙设备: \(output.portName)")
}
}
return result
}
// 监听蓝牙设备连接/断开
@objc func startBluetoothDeviceMonitoring() {
// 注册音频会话通知
NotificationCenter.default.addObserver(
self,
selector: #selector(handleRouteChange(_:)),
name: AVAudioSession.routeChangeNotification,
object: nil
)
// 注册蓝牙状态变化通知
NotificationCenter.default.addObserver(
self,
selector: #selector(handleBluetoothStateChange(_:)),
name: NSNotification.Name(rawValue: "CBCentralManagerDidUpdateStateNotification"),
object: nil
)
}
// 处理音频路由变化
@objc private func handleRouteChange(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
return
}
// 获取当前设备
let currentDevices = getConnectedAudioDevices()
// 检查原因
switch reason {
case .newDeviceAvailable:
// 新设备已连接 - 查找新增的设备
for device in currentDevices {
if !deviceExistsInCache(device: device) {
// 找到新设备
NSLog("\(TAG): 新的音频设备已连接: \(device["name"] ?? "未知设备")")
// 添加到缓存
bluetoothDevices.append(device)
// 发送设备连接事件
sendDeviceConnectedEvent(device: device)
}
}
case .oldDeviceUnavailable:
// 设备已断开 - 查找从缓存中移除的设备
var devicesToRemove: [[String: String]] = []
for cachedDevice in bluetoothDevices {
if !deviceExistsInList(device: cachedDevice, list: currentDevices) {
// 设备已断开
NSLog("\(TAG): 音频设备已断开: \(cachedDevice["name"] ?? "未知设备")")
devicesToRemove.append(cachedDevice)
// 发送设备断开事件
sendDeviceDisconnectedEvent(device: cachedDevice)
}
}
// 从缓存中移除断开的设备
for device in devicesToRemove {
if let index = bluetoothDevices.firstIndex(where: { $0["address"] == device["address"] }) {
bluetoothDevices.remove(at: index)
}
}
default:
break
}
}
// 处理蓝牙状态变化
@objc private func handleBluetoothStateChange(_ notification: Notification) {
guard let manager = centralManager else { return }
var stateString = "unknown"
switch manager.state {
case .poweredOn:
stateString = "on"
NSLog("\(TAG): 蓝牙状态变化: 已启用")
case .poweredOff:
stateString = "off"
NSLog("\(TAG): 蓝牙状态变化: 已关闭")
// 清空设备列表
bluetoothDevices.removeAll()
case .resetting:
stateString = "resetting"
NSLog("\(TAG): 蓝牙状态变化: 正在重置")
case .unauthorized:
stateString = "unauthorized"
NSLog("\(TAG): 蓝牙状态变化: 未授权")
case .unsupported:
stateString = "unsupported"
NSLog("\(TAG): 蓝牙状态变化: 不支持")
case .unknown:
stateString = "unknown"
NSLog("\(TAG): 蓝牙状态变化: 未知")
@unknown default:
stateString = "unknown"
NSLog("\(TAG): 蓝牙状态变化: 未知(default)")
}
NSLog("\(TAG): 发送蓝牙状态变化事件: \(stateString)")
// 发送蓝牙状态变化事件
if let callback = deviceEventCallback {
let event: [String: Any] = [
"type": "bluetoothStateChanged",
"state": stateString,
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
callback(event)
}
}
// 发送设备连接事件
private func sendDeviceConnectedEvent(device: [String: String]) {
if let callback = deviceEventCallback,
let name = device["name"],
let address = device["address"] {
let deviceMap: [String: Any] = [
"name": name,
"address": address,
"type": "a2dp" // iOS 默认认为是 A2DP 设备
]
let event: [String: Any] = [
"type": "deviceConnected",
"device": deviceMap,
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
callback(event)
}
}
// 发送设备断开事件
private func sendDeviceDisconnectedEvent(device: [String: String]) {
if let callback = deviceEventCallback,
let name = device["name"],
let address = device["address"] {
let deviceMap: [String: Any] = [
"name": name,
"address": address,
"type": "a2dp" // iOS 默认认为是 A2DP 设备
]
let event: [String: Any] = [
"type": "deviceDisconnected",
"device": deviceMap,
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
callback(event)
}
}
// 检查设备是否存在于缓存中
private func deviceExistsInCache(device: [String: String]) -> Bool {
return deviceExistsInList(device: device, list: bluetoothDevices)
}
// 检查设备是否存在于列表中
private func deviceExistsInList(device: [String: String], list: [[String: String]]) -> Bool {
guard let address = device["address"] else { return false }
return list.contains { $0["address"] == address }
}
// 停止监听
@objc func stopBluetoothDeviceMonitoring() {
NotificationCenter.default.removeObserver(self, name: AVAudioSession.routeChangeNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: "CBCentralManagerDidUpdateStateNotification"), object: nil)
}
// 释放资源
@objc func dispose() {
stopBluetoothDeviceMonitoring()
deviceEventCallback = nil
}
// 打开系统设置
@objc func openSettings() -> Bool {
NSLog("\(TAG): 尝试打开应用设置")
if let url = URL(string: UIApplication.openSettingsURLString) {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
return true
}
}
return false
}
// 打开蓝牙设置(iOS 只能打开系统设置,无法直接跳转到蓝牙设置)
@objc func openBluetoothSettings() -> Bool {
// 在 iOS 10+ 上直接跳转到蓝牙设置页面
if #available(iOS 10.0, *) {
NSLog("\(TAG): 尝试使用 URL Scheme 打开蓝牙设置")
if let url = URL(string: "App-prefs:root=Bluetooth") {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
return true
}
}
}
// 如果无法直接跳转到蓝牙设置,则打开一般设置
return openSettings()
}
}

41
ios/Runner/Info.plist

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
@ -24,14 +26,8 @@
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSMicrophoneUsageDescription</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<string>需要使用麦克风来进行语音交互</string> <true/>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
@ -39,14 +35,27 @@
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限来连接和管理音频设备</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限来连接和管理音频设备</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要麦克风权限来进行语音识别</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>需要语音识别权限来提供语音交互服务</string>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
</array> </array>
<key>NSUserNotificationsUsageDescription</key>
<string>需要通知权限来保持与AI助手的实时对话</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict> </dict>
</plist> </plist>

6
ios/Runner/Runner-Bridging-Header.h

@ -1 +1,7 @@
#ifndef Runner_Bridging_Header_h
#define Runner_Bridging_Header_h
#import "GeneratedPluginRegistrant.h" #import "GeneratedPluginRegistrant.h"
#import <MicrosoftCognitiveServicesSpeech/SPXSpeechApi.h>
#endif /* Runner_Bridging_Header_h */

2
lib/data/services/chat_history_service.dart

@ -41,7 +41,7 @@ class ChatHistoryService extends GetxService {
// 保存到存储 // 保存到存储
await _storage.write(_getHistoryKey(agentId), jsonString); await _storage.write(_getHistoryKey(agentId), jsonString);
Logger.info('已保存聊天记录,代理: $agentId,消息数: ${messagesToSave.length}'); // Logger.info('已保存聊天记录,代理: $agentId,消息数: ${messagesToSave.length}');
} catch (e) { } catch (e) {
Logger.error('保存聊天记录失败', e); Logger.error('保存聊天记录失败', e);

53
lib/data/services/classic_bluetooth_service.dart

@ -162,7 +162,9 @@ class ClassicBluetoothService extends GetxService {
try { try {
Logger.info('正在初始化 ClassicBluetoothService...'); Logger.info('正在初始化 ClassicBluetoothService...');
// 开始监控蓝牙设备连接状态
await _channel.invokeMethod('startBluetoothDeviceMonitoring');
Logger.info('启动蓝牙设备监控');
// 设置事件监听器 // 设置事件监听器
_setupBluetoothStateListener(); _setupBluetoothStateListener();
@ -187,6 +189,32 @@ class ClassicBluetoothService extends GetxService {
return false; return false;
} }
// 根据平台选择不同的权限检查方式
if (GetPlatform.isIOS) {
// iOS: 使用原生权限检查
final nativeResult = await checkNativePermission();
if (nativeResult != null) {
Logger.info('iOS平台蓝牙权限检查结果: $nativeResult');
if (nativeResult['status'] == 'granted') {
return true;
} else if (nativeResult['status'] == 'denied') {
Logger.error('iOS蓝牙权限被拒绝');
errorMessage.value = '蓝牙权限被拒绝,请在设置中允许蓝牙访问';
return false;
} else if (nativeResult['status'] == 'disabled') {
Logger.error('iOS蓝牙未启用');
errorMessage.value = '蓝牙未启用,请开启蓝牙';
return false;
}
}
// 如果原生检查失败,继续使用Flutter权限包(作为备选)
Logger.info('iOS平台回退到Flutter权限检查');
}
// Android: 使用Flutter权限包进行检查(原有实现)
Logger.info('${GetPlatform.isAndroid ? "Android" : "通用"}平台使用Flutter权限检查');
// 检查蓝牙相关权限 // 检查蓝牙相关权限
Map<Permission, PermissionStatus> statuses = await [ Map<Permission, PermissionStatus> statuses = await [
Permission.bluetooth, Permission.bluetooth,
@ -211,6 +239,23 @@ class ClassicBluetoothService extends GetxService {
} }
} }
/// 使用原生方法检查蓝牙权限
Future<Map<dynamic, dynamic>?> checkNativePermission() async {
try {
final result = await _channel.invokeMethod('checkAndRequestNativePermission');
Logger.info('原生蓝牙权限检查原始结果: $result');
if (result is Map) {
return result;
}
return null;
} catch (e) {
Logger.error('原生蓝牙权限检查失败: $e');
return null;
}
}
/// 刷新已连接的蓝牙设备 /// 刷新已连接的蓝牙设备
Future<bool> refreshConnectedDevices() async { Future<bool> refreshConnectedDevices() async {
try { try {
@ -316,7 +361,7 @@ class ClassicBluetoothService extends GetxService {
if (event is Map) { if (event is Map) {
final type = event['type'] as String?; final type = event['type'] as String?;
if (type == 'stateChanged') { if (type == 'bluetoothStateChanged') {
final state = event['state'] as String?; final state = event['state'] as String?;
if (state != null) { if (state != null) {
_handleBluetoothStateChange(state); _handleBluetoothStateChange(state);
@ -421,8 +466,8 @@ class ClassicBluetoothService extends GetxService {
/// 打开蓝牙设置 /// 打开蓝牙设置
Future<bool> openBluetoothSettings() async { Future<bool> openBluetoothSettings() async {
try { try {
await AppSettings.openAppSettings(); final result = await _channel.invokeMethod('openBluetoothSettings');
return true; return result == true;
} catch (e) { } catch (e) {
Logger.error('打开蓝牙设置失败: $e'); Logger.error('打开蓝牙设置失败: $e');
return false; return false;

3
lib/data/services/flutter_tts_service.dart

@ -1,3 +0,0 @@
// 重新导出FlutterTtsService类
// 这个文件作为兼容层,将speech_impl/flutter_tts_service.dart中的服务导出
export 'speech_impl/flutter_tts_service.dart';

4
lib/data/services/speech_factory.dart

@ -1,3 +1,4 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'asr_service.dart'; import 'asr_service.dart';
import 'tts_service.dart'; import 'tts_service.dart';
@ -5,6 +6,7 @@ import 'speech_impl/azure_asr_service.dart';
import 'speech_impl/azure_tts_service.dart'; import 'speech_impl/azure_tts_service.dart';
import 'speech_impl/flutter_asr_service.dart'; import 'speech_impl/flutter_asr_service.dart';
import 'speech_impl/flutter_tts_service.dart'; import 'speech_impl/flutter_tts_service.dart';
import '../../core/utils/logger.dart'; import '../../core/utils/logger.dart';
/// 语音服务类型 /// 语音服务类型
@ -54,8 +56,10 @@ class SpeechFactory extends GetxService {
// 根据类型注册新服务 // 根据类型注册新服务
switch (_currentServiceType) { switch (_currentServiceType) {
case SpeechServiceType.azure: case SpeechServiceType.azure:
Get.lazyPut<AsrService>(() => AzureAsrService()); Get.lazyPut<AsrService>(() => AzureAsrService());
Get.lazyPut<TtsService>(() => AzureTtsService()); Get.lazyPut<TtsService>(() => AzureTtsService());
break; break;
case SpeechServiceType.flutter: case SpeechServiceType.flutter:

239
lib/data/services/speech_impl/azure_tts_service.dart

@ -1,25 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import '../tts_service.dart'; import '../tts_service.dart';
/// 音频输出设备类型
enum AudioOutputType {
/// 扬声器
speaker,
/// 听筒
earpiece,
/// 自动选择(有耳机用耳机,没有则用听筒)
auto
}
/// 微软 Text-to-Speech 服务 /// 微软 Text-to-Speech 服务
/// ///
/// 该服务通过平台通道与 Android 上的 Microsoft Speech SDK 交互, /// 该服务通过平台通道与原生 Microsoft Speech SDK 交互,
/// 提供文本转语音功能。 /// 提供文本转语音功能。
class AzureTtsService extends GetxService implements TtsService { class AzureTtsService extends GetxService implements TtsService {
static final AzureTtsService to = Get.put(AzureTtsService()); static final AzureTtsService to = Get.put(AzureTtsService());
@ -34,7 +23,6 @@ class AzureTtsService extends GetxService implements TtsService {
int _currentRate = 0; int _currentRate = 0;
int _currentPitch = 0; int _currentPitch = 0;
int _currentVolume = 100; int _currentVolume = 100;
AudioOutputType _currentAudioOutputType = AudioOutputType.auto;
// 支持的语音列表 // 支持的语音列表
final List<String> _supportedVoices = [ final List<String> _supportedVoices = [
@ -49,29 +37,40 @@ class AzureTtsService extends GetxService implements TtsService {
final List<_SpeechItem> _textQueue = []; final List<_SpeechItem> _textQueue = [];
bool _isProcessingQueue = false; bool _isProcessingQueue = false;
// 事件流控制器
final StreamController<TtsEvent> _eventController = StreamController<TtsEvent>.broadcast();
@override
Stream<TtsEvent> get onEvent => _eventController.stream;
// 可观察状态 // 可观察状态
final isEnabled = true.obs; final isEnabled = true.obs;
final _isSpeaking = false.obs;
final audioOutputType = AudioOutputType.auto.obs;
// 流式文本缓冲区 // 流式文本缓冲区
String _streamBuffer = ''; String _streamBuffer = '';
/// 缓存的播放状态
bool _speakingCache = false;
DateTime _lastSpeakingCheck = DateTime.now();
@override @override
String get currentVoice => _currentVoice; String get currentVoice => _currentVoice;
@override @override
bool get isSpeaking => _isSpeaking.value; bool get isSpeaking {
// 如果未初始化,直接返回false
if (!_isInitialized) return false;
// 如果距离上次检查不到50ms,则使用缓存值
if (DateTime.now().difference(_lastSpeakingCheck).inMilliseconds < 50) {
return _speakingCache;
}
// 否则异步更新缓存(但仍返回当前缓存值)
_checkSpeakingStatus();
return _speakingCache;
}
@override
Stream<TtsEvent> get onEvent => const Stream.empty();
@override @override
List<String> get supportedVoices => _supportedVoices; List<String> get supportedVoices => _supportedVoices;
AudioOutputType get currentAudioOutputType => _currentAudioOutputType;
AzureTtsService() { AzureTtsService() {
_loadConfig(); _loadConfig();
@ -112,15 +111,6 @@ class AzureTtsService extends GetxService implements TtsService {
}); });
_isInitialized = result; _isInitialized = result;
// 确保设置默认的音频输出设备类型
if (result) {
// 不需要显式调用 setAudioOutputType,因为原生层已经设置了默认值
// 只需要确保 Dart 层的状态与原生层一致
_currentAudioOutputType = AudioOutputType.auto;
audioOutputType.value = AudioOutputType.auto;
}
return result; return result;
} else { } else {
// 使用默认语言初始化 // 使用默认语言初始化
@ -131,21 +121,10 @@ class AzureTtsService extends GetxService implements TtsService {
}); });
_isInitialized = result; _isInitialized = result;
// 确保设置默认的音频输出设备类型
if (result) {
_currentAudioOutputType = AudioOutputType.auto;
audioOutputType.value = AudioOutputType.auto;
}
return result; return result;
} }
} catch (e) { } catch (e) {
Logger.error('初始化失败: $e'); Logger.error('初始化失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('TTS初始化失败: $e'));
return false; return false;
} }
} }
@ -164,36 +143,6 @@ class AzureTtsService extends GetxService implements TtsService {
return result; return result;
} catch (e) { } catch (e) {
Logger.error('设置语音失败: $e'); Logger.error('设置语音失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('设置语音失败: $e'));
return false;
}
}
/// 设置音频输出设备类型
Future<bool> setAudioOutputType(AudioOutputType outputType) async {
if (!_isInitialized) await initialize();
try {
final String outputTypeStr = outputType.toString().split('.').last;
final result = await _channel.invokeMethod('setAudioOutputType', {
'outputType': outputTypeStr,
});
if (result) {
_currentAudioOutputType = outputType;
audioOutputType.value = outputType;
}
return result;
} catch (e) {
Logger.error('设置音频输出设备失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('设置音频输出设备失败: $e'));
return false; return false;
} }
} }
@ -218,10 +167,6 @@ class AzureTtsService extends GetxService implements TtsService {
return result; return result;
} catch (e) { } catch (e) {
Logger.error('设置语音参数失败: $e'); Logger.error('设置语音参数失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('设置语音参数失败: $e'));
return false; return false;
} }
} }
@ -232,26 +177,19 @@ class AzureTtsService extends GetxService implements TtsService {
if (!isEnabled.value || text.isEmpty) return false; if (!isEnabled.value || text.isEmpty) return false;
try { try {
_isSpeaking.value = true; // 主动设置缓存状态为true
_speakingCache = true;
// 发送开始事件
_eventController.add(TtsEvent(type: TtsEventType.started));
final result = await _channel.invokeMethod('speakText', {'text': text}); final result = await _channel.invokeMethod('speakText', {'text': text});
_isSpeaking.value = false; // 调用完成后主动检查状态
_checkSpeakingStatus();
// 发送完成事件
_eventController.add(TtsEvent(type: TtsEventType.completed));
return result == "OK"; return result == "OK";
} catch (e) { } catch (e) {
_isSpeaking.value = false; // 发生错误时重置状态
_speakingCache = false;
Logger.error('语音合成失败: $e'); Logger.error('语音合成失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('语音合成失败: $e'));
return false; return false;
} }
} }
@ -262,27 +200,41 @@ class AzureTtsService extends GetxService implements TtsService {
if (!isEnabled.value || ssml.isEmpty) return false; if (!isEnabled.value || ssml.isEmpty) return false;
try { try {
_isSpeaking.value = true; // 主动设置缓存状态为true
_speakingCache = true;
// 发送开始事件
_eventController.add(TtsEvent(type: TtsEventType.started));
final result = await _channel.invokeMethod('speakSsml', {'ssml': ssml}); final result = await _channel.invokeMethod('speakSsml', {'ssml': ssml});
_isSpeaking.value = false; // 调用完成后主动检查状态
_checkSpeakingStatus();
// 发送完成事件
_eventController.add(TtsEvent(type: TtsEventType.completed));
return result == "OK"; return result == "OK";
} catch (e) { } catch (e) {
_isSpeaking.value = false; // 发生错误时重置状态
_speakingCache = false;
Logger.error('SSML语音合成失败: $e'); Logger.error('SSML语音合成失败: $e');
return false;
}
}
// 发送错误事件 /// 等待语音播放完成
_eventController.add(TtsEvent.error('SSML语音合成失败: $e')); Future<void> waitForSpeechCompletion({Duration timeout = const Duration(seconds: 30)}) async {
// 首先检查当前状态
_checkSpeakingStatus();
if (!_speakingCache) return;
return false; final stopwatch = Stopwatch()..start();
while (_speakingCache && stopwatch.elapsed < timeout) {
// 短暂等待后再次检查
await Future.delayed(const Duration(milliseconds: 100));
// 检查状态
await _channel.invokeMethod<bool>('isSpeaking').then((result) {
if (result != null) {
_speakingCache = result;
_lastSpeakingCheck = DateTime.now();
}
});
} }
} }
@ -343,10 +295,6 @@ class AzureTtsService extends GetxService implements TtsService {
return true; return true;
} catch (e) { } catch (e) {
Logger.error('处理流式文本失败: $e'); Logger.error('处理流式文本失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('处理流式文本失败: $e'));
return false; return false;
} }
} }
@ -380,18 +328,16 @@ class AzureTtsService extends GetxService implements TtsService {
Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条'); Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条');
// 等待队列处理完成 // 等待队列处理完成
while (_isProcessingQueue || _isSpeaking.value) { await _checkSpeakingStatus();
while (_isProcessingQueue || _speakingCache) {
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
await _checkSpeakingStatus();
} }
Logger.info('TTS队列播放完成'); Logger.info('TTS队列播放完成');
return true; return true;
} catch (e) { } catch (e) {
Logger.error('刷新TTS流失败: $e'); Logger.error('刷新TTS流失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('刷新TTS流失败: $e'));
return false; return false;
} }
} }
@ -421,12 +367,12 @@ class AzureTtsService extends GetxService implements TtsService {
// 使用speakOnce播放当前项 // 使用speakOnce播放当前项
await speakOnce(item.text); await speakOnce(item.text);
// 等待播放完成
await waitForSpeechCompletion();
} }
} catch (e) { } catch (e) {
Logger.error('处理语音队列出错: $e'); Logger.error('处理语音队列出错: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('处理语音队列出错: $e'));
} finally { } finally {
_isProcessingQueue = false; _isProcessingQueue = false;
} }
@ -437,23 +383,16 @@ class AzureTtsService extends GetxService implements TtsService {
try { try {
_textQueue.clear(); _textQueue.clear();
if (_isSpeaking.value) { if (_speakingCache) {
final result = await _channel.invokeMethod('stopSpeaking'); final result = await _channel.invokeMethod('stopSpeaking');
_isSpeaking.value = false; // 重置缓存状态
_speakingCache = false;
// 发送取消事件
_eventController.add(TtsEvent(type: TtsEventType.canceled));
return result; return result;
} }
return true; return true;
} catch (e) { } catch (e) {
Logger.error('停止语音合成出错: $e'); Logger.error('停止语音合成出错: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('停止语音合成出错: $e'));
return false; return false;
} }
} }
@ -492,25 +431,6 @@ class AzureTtsService extends GetxService implements TtsService {
if (!isEnabled.value) stop(); if (!isEnabled.value) stop();
} }
/// 切换音频输出设备类型
Future<bool> toggleAudioOutputType() async {
AudioOutputType newType;
switch (_currentAudioOutputType) {
case AudioOutputType.speaker:
newType = AudioOutputType.earpiece;
break;
case AudioOutputType.earpiece:
newType = AudioOutputType.auto;
break;
case AudioOutputType.auto:
newType = AudioOutputType.speaker;
break;
}
return await setAudioOutputType(newType);
}
@override @override
Future<void> dispose() async { Future<void> dispose() async {
if (!_isInitialized) return; if (!_isInitialized) return;
@ -518,40 +438,27 @@ class AzureTtsService extends GetxService implements TtsService {
try { try {
await stop(); await stop();
await _channel.invokeMethod('dispose'); await _channel.invokeMethod('dispose');
// 关闭事件流控制器
await _eventController.close();
_isInitialized = false; _isInitialized = false;
} catch (e) { } catch (e) {
Logger.error('释放资源失败: $e'); Logger.error('释放资源失败: $e');
} }
} }
/// 处理AI返回的文本流,将其转换为语音 /// 异步检查并更新播放状态缓存
/// Future<bool> _checkSpeakingStatus() async {
/// [textStream] 文本流 _lastSpeakingCheck = DateTime.now();
/// 返回一个Future,当流处理完成时完成
Future<bool> processAiTextStream(Stream<String> textStream) async {
if (!isEnabled.value) {
Logger.error('TTS服务未启用');
return false;
}
try { try {
await for (final text in textStream) { final result = await _channel.invokeMethod<bool>('isSpeaking');
if (text.isEmpty) continue; if (result != null) {
await speakStream(text); _speakingCache = result;
} }
return _speakingCache;
// 处理完流后,刷新缓冲区中的剩余文本
await flushStream();
return true;
} catch (e) { } catch (e) {
Logger.error('处理文本流时出错: $e'); Logger.error('获取播放状态失败: $e');
return false; return _speakingCache;
} }
} }
} }
/// 语音合成项目 /// 语音合成项目

54
lib/data/services/speech_impl/flutter_asr_service.dart

@ -165,12 +165,22 @@ class FlutterAsrService extends GetxService implements AsrService {
// 如果当前正在监听,先停止 // 如果当前正在监听,先停止
if (_speech.isListening) { if (_speech.isListening) {
await _speech.stop(); await _speech.stop();
// 短暂延迟确保资源释放 // 延长等待时间,确保资源完全释放
await Future.delayed(const Duration(milliseconds: 300)); await Future.delayed(const Duration(milliseconds: 800));
} }
// 在启动前再次检查状态
if (_speech.isListening) {
Logger.warning('停止识别后,仍处于监听状态,可能有系统冲突');
await _speech.stop();
await Future.delayed(const Duration(milliseconds: 500));
}
// 启动语音识别前确保状态正确
_asrState.value = AsrState.listening;
// 启动语音识别 // 启动语音识别
return await _speech.listen( final result = await _speech.listen(
onResult: onResult, onResult: onResult,
localeId: _currentLocale.value, localeId: _currentLocale.value,
listenFor: listenFor, listenFor: listenFor,
@ -178,10 +188,20 @@ class FlutterAsrService extends GetxService implements AsrService {
listenOptions: stt.SpeechListenOptions( listenOptions: stt.SpeechListenOptions(
partialResults: true, partialResults: true,
autoPunctuation: autoPunctuation, autoPunctuation: autoPunctuation,
// 使用独白模式,减少与系统交互冲突
listenMode: stt.ListenMode.dictation,
), ),
) ?? false; ) ?? false;
if (!result) {
Logger.error('语音识别启动失败');
_asrState.value = AsrState.error;
}
return result;
} catch (e) { } catch (e) {
Logger.error('启动语音识别失败: $e'); Logger.error('启动语音识别失败: $e');
_asrState.value = AsrState.error;
return false; return false;
} }
} }
@ -195,12 +215,29 @@ class FlutterAsrService extends GetxService implements AsrService {
return; return;
} }
// 短暂延迟后再重新启动 // 检查系统状态,避免频繁重启
await Future.delayed(const Duration(milliseconds: 300)); if (_speech.isListening) {
Logger.info('当前已在监听中,无需重启');
return;
}
// 延长等待时间,避免与系统资源冲突
await Future.delayed(const Duration(milliseconds: 1000));
// 再次检查状态
if (_continuousRecognitionController == null || _continuousRecognitionController!.isClosed) {
Logger.info('延迟期间控制器已关闭,跳过重启');
return;
}
Logger.info('尝试重新启动连续识别');
// 使用统一方法启动识别 // 使用统一方法启动识别
final success = await _startListening( final success = await _startListening(
onResult: _onSpeechResult, onResult: _onSpeechResult,
// 增加超时时间和暂停时间
listenFor: const Duration(seconds: 30),
pauseFor: const Duration(seconds: 4),
); );
if (success) { if (success) {
@ -208,11 +245,16 @@ class FlutterAsrService extends GetxService implements AsrService {
} else { } else {
Logger.error('连续识别重启失败'); Logger.error('连续识别重启失败');
_continuousRecognitionController?.add( _continuousRecognitionController?.add(
RecognitionEvent.error('重启语音识别失败') RecognitionEvent.error('重启语音识别失败,可能存在系统资源冲突')
); );
} }
} catch (e) { } catch (e) {
Logger.error('重启连续识别失败: $e'); Logger.error('重启连续识别失败: $e');
// 发送错误给控制器
_continuousRecognitionController?.add(
RecognitionEvent.error('重启失败: $e')
);
} }
} }

3
lib/data/services/voice_interaction_service.dart

@ -109,6 +109,7 @@ class VoiceInteractionService extends GetxService {
/// 初始化服务 /// 初始化服务
Future<VoiceInteractionService> initialize() async { Future<VoiceInteractionService> initialize() async {
return this;
try { try {
if (_isInitialized) { if (_isInitialized) {
return this; return this;
@ -146,6 +147,7 @@ class VoiceInteractionService extends GetxService {
/// 启动语音交互服务 /// 启动语音交互服务
Future<bool> startService() async { Future<bool> startService() async {
return true;
if (_isServiceRunning.value) { if (_isServiceRunning.value) {
Logger.info('语音交互服务已经在运行'); Logger.info('语音交互服务已经在运行');
return true; return true;
@ -198,6 +200,7 @@ class VoiceInteractionService extends GetxService {
/// 暂停语音交互(停止TTS和语音识别,但保持服务运行) /// 暂停语音交互(停止TTS和语音识别,但保持服务运行)
Future<bool> pauseVoiceInteraction() async { Future<bool> pauseVoiceInteraction() async {
return true;
if (!_isServiceRunning.value) { if (!_isServiceRunning.value) {
Logger.info('语音交互服务未运行,无法暂停'); Logger.info('语音交互服务未运行,无法暂停');
return false; return false;

2
lib/main.dart

@ -44,10 +44,12 @@ void main() async {
} }
} }
// 运行应用 // 运行应用
runApp(const MainApp()); runApp(const MainApp());
} }
class MainApp extends StatelessWidget { class MainApp extends StatelessWidget {
const MainApp({super.key}); const MainApp({super.key});

14
lib/modules/home/views/home_view.dart

@ -100,6 +100,12 @@ class HomeView extends GetView<HomeController> {
_buildSectionTitle(isDarkMode), _buildSectionTitle(isDarkMode),
SizedBox(height: 16.h), SizedBox(height: 16.h),
_buildPremiumAILayout(), _buildPremiumAILayout(),
// 在合适的位置添加测试按钮
ElevatedButton(
onPressed: () => Get.toNamed(Routes.ASR_TEST),
child: const Text('Azure语音识别测试'),
),
], ],
), ),
), ),
@ -640,7 +646,8 @@ class HomeView extends GetView<HomeController> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
SizedBox(height: 4.h), SizedBox(height: 4.h),
Text( Flexible(
child: Text(
description, description,
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
@ -649,6 +656,7 @@ class HomeView extends GetView<HomeController> {
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
),
], ],
), ),
) )
@ -671,7 +679,8 @@ class HomeView extends GetView<HomeController> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
SizedBox(height: 2.h), SizedBox(height: 2.h),
Text( Flexible(
child: Text(
description, description,
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
@ -680,6 +689,7 @@ class HomeView extends GetView<HomeController> {
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
),
], ],
), ),
), ),

2
lib/routes/app_pages.dart

@ -64,7 +64,6 @@ abstract class AppPages {
page: () => const ProfileView(), page: () => const ProfileView(),
binding: ProfileBinding(), binding: ProfileBinding(),
), ),
GetPage( GetPage(
name: Routes.translation, name: Routes.translation,
page: () => const TranslationView(), page: () => const TranslationView(),
@ -80,5 +79,6 @@ abstract class AppPages {
page: () => const SettingsView(), page: () => const SettingsView(),
binding: SettingsBinding(), binding: SettingsBinding(),
), ),
]; ];
} }

9
pubspec.yaml

@ -14,8 +14,6 @@ dependencies:
get_storage: ^2.1.1 get_storage: ^2.1.1
http: ^1.3.0 http: ^1.3.0
flutter_screenutil: ^5.9.0 flutter_screenutil: ^5.9.0
just_audio: ^0.9.36
flutter_azure_tts: ^0.2.3
path_provider: ^2.1.5 path_provider: ^2.1.5
audio_session: ^0.1.18 audio_session: ^0.1.18
flutter_markdown: ^0.6.18+2 flutter_markdown: ^0.6.18+2
@ -30,8 +28,11 @@ dependencies:
flutter_tts: ^4.2.2 flutter_tts: ^4.2.2
logger: ^2.0.2+1 logger: ^2.0.2+1
flutter_blue_plus: ^1.31.16 flutter_blue_plus: ^1.31.16
# shared_preferences: ^2.2.2 # 移除,使用 get_storage 代替 just_audio: ^0.9.36
# speech_to_text: ^7.0.0 # 移除,使用自定义的 ASR 服务 azure_speech_recognition:
path: ./azure
window_size: window_size:
git: git:
url: https://github.com/google/flutter-desktop-embedding.git url: https://github.com/google/flutter-desktop-embedding.git

30
test/widget_test.dart

@ -1,30 +0,0 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:deep_voice/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MainApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
Loading…
Cancel
Save