diff --git a/ios/Flutter/Profile.xcconfig b/ios/Flutter/Profile.xcconfig new file mode 100644 index 000000000..709239950 --- /dev/null +++ b/ios/Flutter/Profile.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig" +#include "Generated.xcconfig" \ No newline at end of file diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index c4855bfe2..3648d51b1 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1,2 +1,3 @@ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile index a2084703d..4ca6f7005 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -39,5 +39,10 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + + # Ensure all pods use the same iOS deployment target + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '18.0' + end end end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 336d0ad28..57a277978 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -11,6 +11,10 @@ PODS: - Flutter - audio_waveforms (0.0.1): - Flutter + - azure_speech (0.0.1): + - Flutter + - MicrosoftCognitiveServicesSpeech-iOS (~> 1.30.0) + - speech - ble_service (0.0.1): - Flutter - chat_storage (0.0.1): @@ -179,6 +183,7 @@ PODS: - Mantle (2.2.0): - Mantle/extobjc (= 2.2.0) - Mantle/extobjc (2.2.0) + - MicrosoftCognitiveServicesSpeech-iOS (1.30.0) - music_service (0.0.1): - Flutter - open_ai_service (0.0.1): @@ -210,10 +215,8 @@ PODS: - SDWebImage/Core (~> 5.17) - share_plus (0.0.1): - Flutter - - speech_to_text (0.0.1): + - speech (0.0.1): - Flutter - - FlutterMacOS - - Try - spotify_sdk (0.0.1): - Flutter - sqflite_darwin (0.0.4): @@ -223,7 +226,6 @@ PODS: - tencentcloud_cos_sdk_plugin (1.2.3): - Flutter - QCloudCOSXML (= 6.4.7) - - Try (2.1.1) - webview_flutter_wkwebview (0.0.1): - Flutter - FlutterMacOS @@ -233,6 +235,7 @@ DEPENDENCIES: - app_settings (from `.symlinks/plugins/app_settings/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`) - audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`) + - azure_speech (from `.symlinks/plugins/azure_speech/ios`) - ble_service (from `.symlinks/plugins/ble_service/ios`) - chat_storage (from `.symlinks/plugins/chat_storage/ios`) - classic_bluetooth (from `.symlinks/plugins/classic_bluetooth/ios`) @@ -260,7 +263,7 @@ DEPENDENCIES: - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/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`) + - speech (from `.symlinks/plugins/speech/ios`) - spotify_sdk (from `.symlinks/plugins/spotify_sdk/ios`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - tencentcloud_cos_sdk_plugin (from `.symlinks/plugins/tencentcloud_cos_sdk_plugin/ios`) @@ -288,6 +291,7 @@ SPEC REPOS: - GTMSessionFetcher - libwebp - Mantle + - MicrosoftCognitiveServicesSpeech-iOS - QCloudCore - QCloudCOSXML - QCloudTrack @@ -295,7 +299,6 @@ SPEC REPOS: - SDWebImage - SDWebImageWebPCoder - SwiftyGif - - Try - WechatOpenSDK-XCFramework EXTERNAL SOURCES: @@ -305,6 +308,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/audio_session/ios" audio_waveforms: :path: ".symlinks/plugins/audio_waveforms/ios" + azure_speech: + :path: ".symlinks/plugins/azure_speech/ios" ble_service: :path: ".symlinks/plugins/ble_service/ios" chat_storage: @@ -359,8 +364,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/record_darwin/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" - speech_to_text: - :path: ".symlinks/plugins/speech_to_text/darwin" + speech: + :path: ".symlinks/plugins/speech/ios" spotify_sdk: :path: ".symlinks/plugins/spotify_sdk/ios" sqflite_darwin: @@ -375,7 +380,8 @@ SPEC CHECKSUMS: AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf - ble_service: 068ebe51800424f980282b16cbf757574eec4e08 + azure_speech: d65fa5c1f04be9cee7303abf4caea30680e814fd + ble_service: 3414a81261f88a6e673cbc71178d2353d96b2a1c chat_storage: 933a96144157a07a2fef7f6acb28c78ff18465d8 classic_bluetooth: c5e5c0c86acab8dcd6b8e9cef7fca654c3518034 device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 @@ -414,6 +420,7 @@ SPEC CHECKSUMS: just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d + MicrosoftCognitiveServicesSpeech-iOS: f53991647f295e18407619816a8064908a8700f4 music_service: 85abecc8fa21599a06907ad50d83f48a58e0dc0d open_ai_service: 82ea947b1a081a2a17bc0c8e5467dcbd7cf9b232 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 @@ -427,15 +434,14 @@ SPEC CHECKSUMS: SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - speech_to_text: 9dc43a5df3cbc2813f8c7cc9bd0fbf94268ed7ac + speech: 89e648ea83c83d32b25ad2247fdec80fdddd7fb6 spotify_sdk: a48400bb29f70c4fe251ebfdc9135c37097ac5ca sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 tencentcloud_cos_sdk_plugin: c3d78b2c3bae42b5a20009dc047f3f1621e6272b - Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96 webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2 WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f -PODFILE CHECKSUM: f62382080fcf32f2d4640dc419aa07d69eff0328 +PODFILE CHECKSUM: c27b71cd46016bc2643e657c88b59244c316e68a COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 70369f46d..f8125569b 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -7,16 +7,16 @@ objects = { /* Begin PBXBuildFile section */ + 094D08D02FDF5BCF4300904C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 89BDD8FCD8E4A95DFFAC950E /* Pods_Runner.framework */; }; 0AD944122DD9DB5C00557D21 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 6077538D78AAB34545D7F3DD /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E27AD0DBE6531F2655DB9986 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 8C1EC4B8888A2A7B8741B084 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 971EED197AE19E5EA8C93AB7 /* Pods_RunnerTests.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - A7986DFCFDA104638080424B /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F79ADB762804BBED65875C03 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -50,12 +50,13 @@ 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 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 = ""; }; - 6189535D5FBB81D8377BE874 /* 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 = ""; }; - 742A00629970110BA5008064 /* 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 = ""; }; + 4AC518688D83E399D06C65D9 /* 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 = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 762C8874AAE15CA0CB901347 /* 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 = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 89BDD8FCD8E4A95DFFAC950E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8AEFEF9B7F1F2853E3BE4328 /* 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 = ""; }; + 971EED197AE19E5EA8C93AB7 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -63,12 +64,11 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B3EC1304D6F5453D58E0BDB0 /* 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 = ""; }; - C15B3030DF1AED99D3829135 /* 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 = ""; }; - D1CF75E5BD49BB58812D5F36 /* 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 = ""; }; + AE1151546CDAD222B8569F48 /* 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 = ""; }; + CD485A3D7C665ACD79DE1A05 /* 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 = ""; }; D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; - E27AD0DBE6531F2655DB9986 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F79ADB762804BBED65875C03 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D45B01309FCD51D40429D22A /* 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 = ""; }; + D6F4721705FE7CF4B2C9A7C7 /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -76,7 +76,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6077538D78AAB34545D7F3DD /* Pods_Runner.framework in Frameworks */, + 094D08D02FDF5BCF4300904C /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -84,7 +84,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A7986DFCFDA104638080424B /* Pods_RunnerTests.framework in Frameworks */, + 8C1EC4B8888A2A7B8741B084 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -94,12 +94,12 @@ 1ACB686EC7DC36C6E33D372F /* Pods */ = { isa = PBXGroup; children = ( - 762C8874AAE15CA0CB901347 /* Pods-Runner.debug.xcconfig */, - B3EC1304D6F5453D58E0BDB0 /* Pods-Runner.release.xcconfig */, - 6189535D5FBB81D8377BE874 /* Pods-Runner.profile.xcconfig */, - C15B3030DF1AED99D3829135 /* Pods-RunnerTests.debug.xcconfig */, - 742A00629970110BA5008064 /* Pods-RunnerTests.release.xcconfig */, - D1CF75E5BD49BB58812D5F36 /* Pods-RunnerTests.profile.xcconfig */, + 4AC518688D83E399D06C65D9 /* Pods-Runner.debug.xcconfig */, + AE1151546CDAD222B8569F48 /* Pods-Runner.release.xcconfig */, + D6F4721705FE7CF4B2C9A7C7 /* Pods-Runner.profile.xcconfig */, + 8AEFEF9B7F1F2853E3BE4328 /* Pods-RunnerTests.debug.xcconfig */, + CD485A3D7C665ACD79DE1A05 /* Pods-RunnerTests.release.xcconfig */, + D45B01309FCD51D40429D22A /* Pods-RunnerTests.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -108,8 +108,8 @@ isa = PBXGroup; children = ( 0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */, - E27AD0DBE6531F2655DB9986 /* Pods_Runner.framework */, - F79ADB762804BBED65875C03 /* Pods_RunnerTests.framework */, + 89BDD8FCD8E4A95DFFAC950E /* Pods_Runner.framework */, + 971EED197AE19E5EA8C93AB7 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -178,7 +178,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - D21548F12B1535261EB87672 /* [CP] Check Pods Manifest.lock */, + 4555DFF031A57477C09E2572 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, B3515CD9628E3332AFC88CD4 /* Frameworks */, @@ -197,15 +197,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 90215E32C3610039A2575557 /* [CP] Check Pods Manifest.lock */, + 1A23E53C090955A193472A0D /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 510B02006C08E514BF31EFD9 /* [CP] Embed Pods Frameworks */, - 298589D956D542AE0EE16DAE /* [CP] Copy Pods Resources */, + 4F94027D1F57DF2EE72F6FDE /* [CP] Embed Pods Frameworks */, + 56059DFFB76EF409A9B11842 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -278,21 +278,26 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 298589D956D542AE0EE16DAE /* [CP] Copy Pods Resources */ = { + 1A23E53C090955A193472A0D /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Copy Pods Resources"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\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; }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { @@ -311,81 +316,76 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 510B02006C08E514BF31EFD9 /* [CP] Embed Pods Frameworks */ = { + 4555DFF031A57477C09E2572 /* [CP] Check Pods Manifest.lock */ = { 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"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\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; }; - 90215E32C3610039A2575557 /* [CP] Check Pods Manifest.lock */ = { + 4F94027D1F57DF2EE72F6FDE /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - 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 = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { + 56059DFFB76EF409A9B11842 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputPaths = ( + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - name = "Run Script"; - outputPaths = ( + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; }; - D21548F12B1535261EB87672 /* [CP] Check Pods Manifest.lock */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( ); + name = "Run Script"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - 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; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; /* End PBXShellScriptBuildPhase section */ @@ -516,7 +516,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C15B3030DF1AED99D3829135 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 8AEFEF9B7F1F2853E3BE4328 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -535,7 +535,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 742A00629970110BA5008064 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = CD485A3D7C665ACD79DE1A05 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -552,7 +552,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D1CF75E5BD49BB58812D5F36 /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = D45B01309FCD51D40429D22A /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index eb35dbbf9..1d2871f5d 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -117,6 +117,11 @@ NSAccessorySetupKitSupports Bluetooth + WiFi + + NSAccessorySetupBluetoothCompanyIdentifiers + + ABEF diff --git a/lib/data/services/speech_factory.dart b/lib/data/services/speech_factory.dart index dd9a79c68..c8ed0cbbf 100644 --- a/lib/data/services/speech_factory.dart +++ b/lib/data/services/speech_factory.dart @@ -3,8 +3,6 @@ import 'asr_service.dart'; import 'tts_service.dart'; import 'speech_impl/azure_asr_service.dart'; import 'speech_impl/azure_tts_service.dart'; -import 'speech_impl/flutter_asr_service.dart'; -import 'speech_impl/flutter_tts_service.dart'; import '../../core/utils/logger.dart'; @@ -53,27 +51,17 @@ class SpeechFactory extends GetxService { Get.delete(force: true); } - // 根据类型注册新服务 - switch (_currentServiceType) { - case SpeechServiceType.azure: + Get.lazyPut(() => AzureAsrService()); Get.lazyPut(() => AzureTtsService()); - break; - case SpeechServiceType.flutter: - Get.lazyPut(() => FlutterAsrService()); - Get.lazyPut(() => FlutterTtsService()); - break; - } + + } /// 初始化语音服务 Future initialize({SpeechServiceType? initialType}) async { try { - // 如果指定了初始类型,则设置 - if (initialType != null) { - _currentServiceType = initialType; - } // 注册服务 _registerServices(); diff --git a/lib/data/services/speech_impl/flutter_asr_service.dart b/lib/data/services/speech_impl/flutter_asr_service.dart deleted file mode 100644 index c8716ad8f..000000000 --- a/lib/data/services/speech_impl/flutter_asr_service.dart +++ /dev/null @@ -1,478 +0,0 @@ -import 'dart:async'; -import 'package:flutter/foundation.dart'; -import 'package:get/get.dart'; -import 'package:speech_to_text/speech_to_text.dart' as stt; -import 'package:speech_to_text/speech_recognition_result.dart'; -import 'package:speech_to_text/speech_recognition_error.dart'; -import '../../../core/utils/logger.dart'; -import '../asr_service.dart'; - -/// ASR状态枚举 -enum AsrState { - /// 未初始化 - notInitialized, - - /// 已初始化但未开始监听 - initialized, - - /// 正在监听 - listening, - - /// 正在处理识别结果 - processing, - - /// 识别完成 - done, - - /// 发生错误 - error, -} - -/// Flutter ASR服务,负责本地语音转文本功能 -/// 使用speech_to_text插件实现语音识别功能 -class FlutterAsrService extends GetxService implements AsrService { - static final FlutterAsrService to = Get.put(FlutterAsrService()); - - // speech_to_text 实例 - final stt.SpeechToText _speech = stt.SpeechToText(); - - // 可用的语音识别语言 - final List _supportedLanguages = []; - @override - List get supportedLanguages => _supportedLanguages; - - // 当前选择的语言 - final RxString _currentLocale = ''.obs; - String get currentLocale => _currentLocale.value; - - // 是否可用 - final RxBool _isAvailable = false.obs; - bool get isAvailable => _isAvailable.value; - - // 是否正在监听 - bool _isListening = false; - - // 当前ASR状态 - final Rx _asrState = AsrState.notInitialized.obs; - AsrState get asrState => _asrState.value; - - // 连续识别模式的结果流控制器 - StreamController? _continuousRecognitionController; - - FlutterAsrService() { - _initSpeech(); - } - - /// 初始化语音识别 - Future _initSpeech() async { - try { - _asrState.value = AsrState.notInitialized; - - // 初始化语音识别,使用较短的最终超时时间,提高响应速度 - _isAvailable.value = await _speech.initialize( - onStatus: _onSpeechStatus, - onError: _onSpeechError, - debugLogging: false, - finalTimeout: const Duration(milliseconds: 800), // 更短的最终超时时间,加速结果返回 - ); - - if (_isAvailable.value) { - // 获取支持的语言列表 - final systemLocales = await _speech.locales(); - - // 清空并重新填充支持的语言列表 - _supportedLanguages.clear(); - for (var locale in systemLocales) { - if (!_supportedLanguages.contains(locale.localeId)) { - _supportedLanguages.add(locale.localeId); - } - } - - // 设置默认语言(优先使用系统语言或中文) - var systemLocale = await _speech.systemLocale(); - if (systemLocale != null) { - _currentLocale.value = systemLocale.localeId; - } else if (_supportedLanguages.contains('zh-CN')) { - _currentLocale.value = 'zh-CN'; - } else if (_supportedLanguages.isNotEmpty) { - _currentLocale.value = _supportedLanguages.first; - } - - _asrState.value = AsrState.initialized; - Logger.info('语音识别初始化成功,支持 ${_supportedLanguages.length} 种语言'); - } else { - _asrState.value = AsrState.error; - Logger.error('语音识别不可用'); - } - } catch (e) { - _isAvailable.value = false; - _asrState.value = AsrState.error; - Logger.error('语音识别初始化失败: ${e.toString()}'); - } - } - - /// 处理语音识别状态变化 - void _onSpeechStatus(String status) { - Logger.info('语音识别状态: $status'); - - switch (status) { - case 'listening': - _asrState.value = AsrState.listening; - _isListening = true; - break; - case 'notListening': - if (_asrState.value == AsrState.listening) { - _asrState.value = AsrState.processing; - } - _isListening = false; - break; - case 'done': - _asrState.value = AsrState.done; - _isListening = false; - - // 在连续识别模式下,当一次识别完成后立即开始下一次识别 - if (_continuousRecognitionController != null && - !_continuousRecognitionController!.isClosed) { - _restartContinuousRecognition(); - } - break; - } - } - - /// 处理语音识别错误 - void _onSpeechError(SpeechRecognitionError error) { - Logger.error('语音识别错误: ${error.errorMsg} (永久性错误: ${error.permanent})'); - _asrState.value = AsrState.error; - - // 向连续识别控制器发送错误事件(如果存在) - _continuousRecognitionController - ?.add(RecognitionEvent.error(error.errorMsg)); - - // 在连续识别模式下,尝试从错误中恢复(非永久性错误) - if (!error.permanent && - _continuousRecognitionController != null && - !_continuousRecognitionController!.isClosed) { - _restartContinuousRecognition(); - } - } - - /// 启动语音识别的统一方法 - Future _startListening({ - required Function(SpeechRecognitionResult) onResult, - Duration listenFor = const Duration(seconds: 30), - Duration pauseFor = const Duration(seconds: 3), - bool autoPunctuation = true, - }) async { - try { - // 如果当前正在监听,先停止 - if (_speech.isListening) { - await _speech.stop(); - // 延长等待时间,确保资源完全释放 - 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; - - // 启动语音识别 - final result = await _speech.listen( - onResult: onResult, - localeId: _currentLocale.value, - listenFor: listenFor, - pauseFor: pauseFor, - listenOptions: stt.SpeechListenOptions( - partialResults: true, - autoPunctuation: autoPunctuation, - // 使用独白模式,减少与系统交互冲突 - listenMode: stt.ListenMode.dictation, - ), - ) ?? - false; - - if (!result) { - Logger.error('语音识别启动失败'); - _asrState.value = AsrState.error; - } - - return result; - } catch (e) { - Logger.error('启动语音识别失败: ${e.toString()}'); - _asrState.value = AsrState.error; - return false; - } - } - - /// 重新启动连续识别 - Future _restartContinuousRecognition() async { - try { - // 确保控制器有效 - if (_continuousRecognitionController == null || - _continuousRecognitionController!.isClosed) { - Logger.info('连续识别控制器已关闭,跳过重启'); - return; - } - - // 检查系统状态,避免频繁重启 - 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( - onResult: _onSpeechResult, - // 增加超时时间和暂停时间 - listenFor: const Duration(seconds: 30), - pauseFor: const Duration(seconds: 4), - ); - - if (success) { - Logger.info('连续识别重启成功'); - } else { - Logger.error('连续识别重启失败'); - _continuousRecognitionController - ?.add(RecognitionEvent.error('重启语音识别失败,可能存在系统资源冲突')); - } - } catch (e) { - Logger.error('重启连续识别失败: ${e.toString()}'); - - // 发送错误给控制器 - _continuousRecognitionController?.add(RecognitionEvent.error('重启失败: $e')); - } - } - - /// 处理语音识别结果 - void _onSpeechResult(SpeechRecognitionResult result) { - // 如果是连续识别,发送到连续识别控制器 - if (_continuousRecognitionController != null) { - if (result.finalResult) { - Logger.info('收到最终识别结果: "${result.recognizedWords}"'); - _continuousRecognitionController!.add(RecognitionEvent.finalResult( - text: result.recognizedWords, - detectedLanguage: currentLocale, - )); - } else { - _continuousRecognitionController!.add(RecognitionEvent( - type: RecognitionEventType.intermediateResult, - text: result.recognizedWords, - detectedLanguage: currentLocale, - )); - } - } - } - - /// 设置当前语言 - void setLocale(String localeId) { - if (_supportedLanguages.contains(localeId)) { - _currentLocale.value = localeId; - Logger.info('设置语音识别语言: $localeId'); - } else { - Logger.error('不支持的语言: $localeId'); - } - } - - @override - Future initialize({ - List? supportedLanguages, - }) async { - try { - if (supportedLanguages != null && supportedLanguages.isNotEmpty) { - _supportedLanguages.clear(); - _supportedLanguages.addAll(supportedLanguages); - } - - // 初始化语音识别 - await _initSpeech(); - return _isAvailable.value; - } catch (e) { - Logger.error('语音识别初始化失败: ${e.toString()}'); - return false; - } - } - - @override - Future recognizeOnce() async { - try { - if (!_isAvailable.value) { - return RecognitionEvent.error('语音识别不可用'); - } - - if (_isListening) { - await stopContinuousRecognition(); - } - - // 创建Completer来等待最终结果 - final completer = Completer(); - - // 开始监听 - _asrState.value = AsrState.listening; - - // 定义结果处理函数 - void resultListener(SpeechRecognitionResult result) { - if (result.finalResult) { - // 只有在最终结果时才完成completer - completer.complete(RecognitionEvent.finalResult( - text: result.recognizedWords, - detectedLanguage: currentLocale, - )); - } - } - - // 使用统一方法启动识别,单次识别使用更短的时间参数 - final success = await _startListening( - onResult: resultListener, - listenFor: const Duration(seconds: 15), - pauseFor: const Duration(seconds: 2), - ); - - if (!success) { - return RecognitionEvent.error('启动语音识别失败'); - } - - // 设置较短的超时时间 - return await completer.future.timeout( - const Duration(seconds: 20), // 略长于listenFor以确保获取到结果 - onTimeout: () { - _speech.stop(); - return RecognitionEvent.error('语音识别超时'); - }, - ); - } catch (e) { - Logger.error('一次性语音识别失败: ${e.toString()}'); - return RecognitionEvent.error('$e'); - } - } - - @override - Future> startContinuousRecognition( - bool audioSourceType) async { - try { - if (!_isAvailable.value) { - final controller = StreamController.broadcast(); - controller.add(RecognitionEvent.error('语音识别不可用')); - controller.close(); - return controller.stream; - } - - // 如果已经在监听,先停止 - if (_isListening) { - await stopContinuousRecognition(); - } - - // 创建新的流控制器 - _continuousRecognitionController = - StreamController.broadcast(); - - // 开始会话 - _continuousRecognitionController! - .add(RecognitionEvent(type: RecognitionEventType.sessionStarted)); - - // 开始连续监听 - _asrState.value = AsrState.listening; - - // 使用统一方法启动识别,连续识别使用较长的参数 - final success = await _startListening( - onResult: _onSpeechResult, - ); - - if (!success) { - // 添加错误事件,但保留流 - _continuousRecognitionController! - .add(RecognitionEvent.error('启动连续语音识别失败')); - - // 返回已有的流控制器 - return _continuousRecognitionController!.stream; - } - - _isListening = true; - return _continuousRecognitionController!.stream; - } catch (e) { - Logger.error('启动连续语音识别失败: ${e.toString()}'); - - // 如果已经创建了控制器,使用它 - if (_continuousRecognitionController != null) { - _continuousRecognitionController!.add(RecognitionEvent.error('$e')); - return _continuousRecognitionController!.stream; - } else { - // 否则创建新的控制器 - final controller = StreamController.broadcast(); - controller.add(RecognitionEvent.error('$e')); - return controller.stream; - } - } - } - - @override - Future stopContinuousRecognition() async { - try { - // 无论是否正在监听,都尝试停止 - if (_speech.isListening) { - await _speech.stop(); - } - - _isListening = false; - - // 发送会话结束事件 - _continuousRecognitionController - ?.add(RecognitionEvent(type: RecognitionEventType.sessionStopped)); - - // 关闭流控制器 - await _continuousRecognitionController?.close(); - _continuousRecognitionController = null; - - _asrState.value = AsrState.done; - return true; - } catch (e) { - Logger.error('停止连续语音识别失败: ${e.toString()}'); - - // 强制重置状态 - _isListening = false; - _asrState.value = AsrState.error; - - return false; - } - } - - @override - bool isContinuousRecognitionActive() { - return _isListening; - } - - @override - Future dispose() async { - try { - // 确保停止所有识别活动 - if (_speech.isListening) { - await _speech.stop(); - } - - // 关闭控制器 - await _continuousRecognitionController?.close(); - _continuousRecognitionController = null; - - _isListening = false; - _asrState.value = AsrState.notInitialized; - } catch (e) { - Logger.error('释放语音识别资源失败: ${e.toString()}'); - } - } -} \ No newline at end of file diff --git a/lib/data/services/speech_impl/flutter_tts_service.dart b/lib/data/services/speech_impl/flutter_tts_service.dart deleted file mode 100644 index aca6fca66..000000000 --- a/lib/data/services/speech_impl/flutter_tts_service.dart +++ /dev/null @@ -1,484 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; -import 'package:flutter/foundation.dart'; -import 'package:get/get.dart'; -import 'package:flutter_tts/flutter_tts.dart'; -import '../../../core/utils/logger.dart'; -import '../tts_service.dart'; - -/// Flutter TTS服务,负责基本的文本转语音功能 -/// -/// 使用flutter_tts插件实现TtsService接口 -class FlutterTtsService extends GetxService implements TtsService { - static final FlutterTtsService to = Get.put(FlutterTtsService()); - - // flutter_tts实例 - late final FlutterTts _flutterTts; - - // 支持的语音列表 - final List _supportedVoices = []; - List get supportedVoices => _supportedVoices; - - // 当前语音 - final RxString _currentVoice = ''.obs; - @override - String get currentVoice => _currentVoice.value; - - // 播放状态 - final RxBool _isSpeaking = false.obs; - @override - bool get isSpeaking => _isSpeaking.value; - - // 文本队列 - final Queue<_SpeechItem> _textQueue = Queue<_SpeechItem>(); - bool _isProcessingQueue = false; - - // 事件流控制器 - final StreamController _eventController = StreamController.broadcast(); - @override - Stream get onEvent => _eventController.stream; - - // 语言 - String _language = 'zh-CN'; - - // 流式文本缓冲区 - String _streamBuffer = ''; - - // 当前参数设置 - double _volume = 1.0; - double _rate = 0.5; - double _pitch = 1.0; - - FlutterTtsService() { - _flutterTts = FlutterTts(); - _initTts(); - } - - /// 初始化TTS - Future _initTts() async { - try { - // 设置完成回调 - _flutterTts.setCompletionHandler(() { - _isSpeaking.value = false; - _eventController.add(TtsEvent(type: TtsEventType.completed)); - }); - - // 设置开始回调 - _flutterTts.setStartHandler(() { - _isSpeaking.value = true; - _eventController.add(TtsEvent(type: TtsEventType.started)); - }); - - // 设置错误回调 - _flutterTts.setErrorHandler((msg) { - _isSpeaking.value = false; - _eventController.add(TtsEvent.error(msg)); - Logger.error('TTS错误: $msg'); - }); - - // 设置取消回调 - _flutterTts.setCancelHandler(() { - _isSpeaking.value = false; - _eventController.add(TtsEvent(type: TtsEventType.canceled)); - }); - - // 设置默认语言 - await _flutterTts.setLanguage(_language); - - // 设置音量 - await _flutterTts.setVolume(_volume); - - // 设置语速 - await _flutterTts.setSpeechRate(_rate); - - // 设置音调 - await _flutterTts.setPitch(_pitch); - - Logger.info('FlutterTtsService 已初始化'); - } catch (e) { - Logger.error('TTS初始化失败: ${e.toString()}'); - } - } - - @override - Future initialize({ - List? supportedLanguages, - }) async { - try { - // 获取可用的语音 - await _loadVoices(); - - // 更新支持的语言列表(如果提供) - if (supportedLanguages != null && supportedLanguages.isNotEmpty) { - _language = supportedLanguages.first; - await _flutterTts.setLanguage(_language); - } - - Logger.info('FlutterTtsService 初始化成功'); - Logger.info('支持的语音: ${_supportedVoices.join(", ")}'); - return true; - } catch (e) { - Logger.error('TTS初始化失败: ${e.toString()}'); - return false; - } - } - - /// 加载可用的语音 - Future _loadVoices() async { - try { - _supportedVoices.clear(); - - // 获取可用的语音 - final voices = await _flutterTts.getVoices; - - if (voices != null && voices is List) { - for (final voice in voices) { - if (voice is Map && voice.containsKey('name')) { - _supportedVoices.add(voice['name'].toString()); - } - } - } - - // 如果没有找到语音,添加默认语音 - if (_supportedVoices.isEmpty) { - _supportedVoices.add('default'); - } - - // 设置默认语音 - if (_currentVoice.value.isEmpty && _supportedVoices.isNotEmpty) { - _currentVoice.value = _supportedVoices.first; - } - } catch (e) { - Logger.error('加载TTS语音失败: ${e.toString()}'); - - // 添加默认语音 - if (_supportedVoices.isEmpty) { - _supportedVoices.add('default'); - _currentVoice.value = 'default'; - } - } - } - - @override - Future setVoice(String voiceName) async { - try { - // 检查语音是否存在 - if (!_supportedVoices.contains(voiceName) && voiceName != 'default') { - Logger.warning('语音不存在: $voiceName,使用默认语音'); - voiceName = _supportedVoices.isNotEmpty ? _supportedVoices.first : 'default'; - } - - // 设置语音 - await _flutterTts.setVoice({"name": voiceName}); - _currentVoice.value = voiceName; - - Logger.info('设置TTS语音: $voiceName'); - return true; - } catch (e) { - Logger.error('设置TTS语音失败: ${e.toString()}'); - return false; - } - } - - /// 设置语音参数 - Future setSpeechParams({double? rate, double? pitch, double? volume}) async { - try { - if (volume != null) { - await _flutterTts.setVolume(volume); - _volume = volume; - } - - if (rate != null) { - await _flutterTts.setSpeechRate(rate); - _rate = rate; - } - - if (pitch != null) { - await _flutterTts.setPitch(pitch); - _pitch = pitch; - } - - return true; - } catch (e) { - Logger.error('设置语音参数失败: ${e.toString()}'); - - // 发送错误事件 - _eventController.add(TtsEvent.error('设置语音参数失败: $e')); - - return false; - } - } - - @override - Future speakOnce(String text) async { - if (text.isEmpty) return false; - - try { - // 如果正在播放,先停止 - if (_isSpeaking.value) { - await stop(); - } - - // 播放文本 - await _flutterTts.speak(text); - - return true; - } catch (e) { - // 发送错误事件 - _eventController.add(TtsEvent.error(e.toString())); - - Logger.error('TTS播放失败: ${e.toString()}'); - return false; - } - } - - @override - Future speakStream(String text) async { - if (text.isEmpty) return false; - - try { - // 添加文本到缓冲区 - _streamBuffer += text; - - // 如果缓冲区为空,直接返回 - if (_streamBuffer.isEmpty) return true; - - // 使用正则表达式匹配句子,包括结束符号 - // 匹配任意字符,直到遇到句子结束符号 - final sentenceRegex = RegExp( - r'([^。.!!??;;:\n\r]+[。.!!??;;::\n\r]|[^。.!!??;;:\n\r]+(?:\.{3,}|…)|[^。.!!??;;:\n\r]+["」』])' - ); - - bool hasProcessed = false; - - // 查找所有完整句子 - final matches = sentenceRegex.allMatches(_streamBuffer); - final List sentences = []; - int lastMatchEnd = 0; - - for (final match in matches) { - // 提取完整句子(包含结束符号) - final sentence = match.group(1)?.trim(); - if (sentence != null && sentence.isNotEmpty) { - sentences.add(sentence); - lastMatchEnd = match.end; - } - } - - // 处理找到的句子 - for (final sentence in sentences) { - _textQueue.add(_SpeechItem( - text: sentence, - rate: _rate, - pitch: _pitch, - volume: _volume, - )); - hasProcessed = true; - } - - // 更新缓冲区,只保留未完成的部分 - if (lastMatchEnd > 0) { - _streamBuffer = _streamBuffer.substring(lastMatchEnd); - } - - // 如果处理了文本并且队列未在处理中,开始处理队列 - if (hasProcessed && !_isProcessingQueue) { - _processQueue(); - } - - return true; - } catch (e) { - Logger.error('处理流式文本失败: ${e.toString()}'); - - // 发送错误事件 - _eventController.add(TtsEvent.error(e.toString())); - - return false; - } - } - - @override - Future flushStream() async { - if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true; - - try { - // 处理缓冲区中可能的完整句子 - await speakStream(''); - - // 如果缓冲区仍有剩余文本,将其添加到播放队列 - if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) { - _textQueue.add(_SpeechItem( - text: _streamBuffer, - rate: _rate, - pitch: _pitch, - volume: _volume, - )); - - // 清空缓冲区 - _streamBuffer = ''; - - // 如果队列未在处理中,开始处理队列 - if (!_isProcessingQueue) { - _processQueue(); - } - } - - Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条'); - - // 等待队列处理完成 - while (_isProcessingQueue || _isSpeaking.value) { - await Future.delayed(const Duration(milliseconds: 100)); - } - - Logger.info('TTS队列播放完成'); - return true; - } catch (e) { - Logger.error('等待TTS队列播放完成失败: ${e.toString()}'); - - // 发送错误事件 - _eventController.add(TtsEvent.error(e.toString())); - - return false; - } - } - - /// 处理队列中的文本 - Future _processQueue() async { - if (_isProcessingQueue) return; - - _isProcessingQueue = true; - - try { - while (_textQueue.isNotEmpty) { - if (!_isSpeaking.value) { - final item = _textQueue.removeFirst(); - - // 如果参数有变化,更新参数 - if (item.rate != _rate || item.pitch != _pitch || item.volume != _volume) { - await setSpeechParams( - rate: item.rate, - pitch: item.pitch, - volume: item.volume, - ); - } - - // 播放文本 - await _flutterTts.speak(item.text); - - // 等待开始播放 - int attempts = 0; - while (!_isSpeaking.value && attempts < 10) { - await Future.delayed(const Duration(milliseconds: 50)); - attempts++; - } - - // 等待播放完成 - while (_isSpeaking.value) { - await Future.delayed(const Duration(milliseconds: 100)); - } - } else { - // 等待一小段时间再检查队列 - await Future.delayed(const Duration(milliseconds: 100)); - } - } - } catch (e) { - Logger.error('TTS处理队列失败: ${e.toString()}'); - - // 发送错误事件 - _eventController.add(TtsEvent.error(e.toString())); - } finally { - _isProcessingQueue = false; - } - } - - @override - Future stop() async { - try { - Logger.info('尝试停止TTS播放...'); - - // 清空队列 - _textQueue.clear(); - _streamBuffer = ''; - - // 如果正在播放,停止播放 - if (_isSpeaking.value) { - // 先重置状态标志,避免处理队列循环继续等待 - _isProcessingQueue = false; - - // 执行停止 - final result = await _flutterTts.stop(); - Logger.info('TTS停止结果: $result'); - - // 短暂延迟等待状态更新 - await Future.delayed(const Duration(milliseconds: 100)); - - // 强制更新状态 - _isSpeaking.value = false; - - // 发送取消事件 - _eventController.add(TtsEvent(type: TtsEventType.canceled)); - } - - Logger.info('TTS停止播放完成'); - return true; - } catch (e) { - Logger.error('TTS停止播放失败: ${e.toString()}'); - - // 即使出错也尝试强制重置状态 - _isSpeaking.value = false; - _isProcessingQueue = false; - - return false; - } - } - - @override - Future dispose() async { - try { - await stop(); - _textQueue.clear(); - - // 关闭事件流控制器 - await _eventController.close(); - - Logger.info('TTS资源已释放'); - } catch (e) { - Logger.error('释放TTS资源失败: ${e.toString()}'); - } - } - - /// 处理AI返回的文本流,将其转换为语音 - /// - /// [textStream] 文本流 - /// 返回一个Future,当流处理完成时完成 - Future processAiTextStream(Stream textStream) async { - try { - await for (final text in textStream) { - if (text.isEmpty) continue; - await speakStream(text); - } - - // 处理完流后,刷新缓冲区中的剩余文本 - await flushStream(); - return true; - } catch (e) { - Logger.error('处理文本流时出错: ${e.toString()}'); - return false; - } - } -} - -/// 语音合成项目 -class _SpeechItem { - final String text; - final double rate; - final double pitch; - final double volume; - - _SpeechItem({ - required this.text, - this.rate = 0.5, - this.pitch = 1.0, - this.volume = 1.0, - }); -} \ No newline at end of file diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart index 4eb9c327c..951885884 100644 --- a/lib/modules/login/controllers/login_controller.dart +++ b/lib/modules/login/controllers/login_controller.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:logger/web.dart'; import '../../../routes/app_routes.dart'; import 'dart:convert'; -import 'dart:ffi'; import 'facebookLogin.dart'; import 'googleLogin.dart'; import 'weixinLogin.dart'; diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 7f11158bf..25c1ce95d 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -85,10 +85,6 @@ class PairingController extends GetxController { isLoading.value = true; statusDescription.value = 'checkingPermissions'.tr; - // 在iOS上,延迟一下再请求权限,避免多权限冲突 - if (Platform.isIOS) { - await Future.delayed(Duration(milliseconds: 800)); - } // 检查并请求权限 if (!await _checkAndRequestPermissions()) { @@ -114,10 +110,22 @@ class PairingController extends GetxController { /// 检查蓝牙状态并刷新设备 Future _checkBluetoothAndRefresh() async { statusDescription.value = 'checkingBluetoothStatus'.tr; - final isEnabled = await _bluetoothService.isBluetoothEnabled(); - _bluetoothState.value = isEnabled ? 'on' : 'off'; + + // 无论是iOS还是Android,都正常检查蓝牙状态 + try { + final isEnabled = await _bluetoothService.isBluetoothEnabled(); + _bluetoothState.value = isEnabled ? 'on' : 'off'; + + if (isEnabled) { + // 如果蓝牙已开启,刷新设备连接 + await _bluetoothService.refreshDeviceConnection(); + } + } catch (e) { + Logger.e(_tag, '检查蓝牙状态失败: ${e.toString()}'); + _bluetoothState.value = 'off'; + } - if (!isEnabled) { + if (_bluetoothState.value != 'on') { statusDescription.value = 'bluetoothTurnedOff'.tr; actionButtonText.value = '去系统设置'; return; @@ -191,100 +199,47 @@ class PairingController extends GetxController { List permissionsToRequest = []; if (Platform.isAndroid) { + // Android权限处理保持不变 permissionsToRequest.add(Permission.locationWhenInUse); - // permissionsToRequest.add(Permission.microphone); if (await _getAndroidSdkVersion() >= 31) { permissionsToRequest.add(Permission.bluetoothScan); permissionsToRequest.add(Permission.bluetoothConnect); } - } else if (Platform.isIOS) { - // iOS需要请求的权限 - permissionsToRequest.add(Permission.bluetooth); - permissionsToRequest.add(Permission.location); - // 如果需要麦克风权限(例如语音功能) - // permissionsToRequest.add(Permission.microphone); - - try { - // 先尝试请求权限 - if (permissionsToRequest.isNotEmpty) { - statuses = await permissionsToRequest.request(); - bool allGranted = true; - statuses.forEach((permission, status) { - if (!status.isGranted) { - allGranted = false; - Logger.w(_tag, 'iOS权限 ${permission.toString()} 未被授予: $status'); - - // 特殊处理蓝牙权限 - if (permission == Permission.bluetooth) { - Logger.w(_tag, 'iOS蓝牙权限未授予,状态: $status'); - } - } - }); - - // 如果有权限未授予,但不是永久拒绝,继续尝试 - if (!allGranted) { - bool hasPermanentlyDenied = statuses.values.any((status) => status.isPermanentlyDenied); - if (hasPermanentlyDenied) { - // 有权限被永久拒绝,需要引导用户去设置 - _showPermissionSettingsDialog('部分权限被永久拒绝,请在设置中启用'); - return false; - } - } - } - - // 直接尝试使用蓝牙功能,这会触发iOS原生权限请求 - bool isEnabled = await _bluetoothService.isBluetoothEnabled(); - Logger.i(_tag, 'iOS蓝牙状态检查: ${isEnabled ? "已开启" : "未开启"}'); - - // 检测AccessorySetupKit支持情况(iOS 18+特性) - bool companionSupported = await _bleService.isCompanionSupported(); - Logger.i(_tag, 'iOS Companion支持检查: ${companionSupported ? "支持" : "不支持"}'); + // 处理Android权限 + if (permissionsToRequest.isNotEmpty) { + statuses = await permissionsToRequest.request(); - if (companionSupported) { - // 针对iOS 18+,尝试预热AccessorySetupKit框架 - try { - // 此调用会触发AccessorySetupKit相关权限请求 - final pairedMac = await _bleService.getPairedMacAddress(); - Logger.i(_tag, '获取已配对设备MAC: ${pairedMac ?? "无"}'); - } catch (e) { - Logger.w(_tag, 'iOS 18+ AccessorySetupKit初始化失败: ${e.toString()}'); + bool allGranted = true; + statuses.forEach((permission, status) { + if (!status.isGranted) { + allGranted = false; + Logger.w(_tag, '权限 ${permission.toString()} 未被授予: $status'); } - } - - // 在iOS上视为权限已授予 - _hasPermissions.value = true; - return true; - } catch (e) { - Logger.e(_tag, "iOS蓝牙初始化失败: ${e.toString()}"); + }); - // 显示自定义对话框引导用户 - _showPermissionSettingsDialog('需要开启蓝牙权限才能配对设备'); - return false; - } - } - - // 如果还有其他权限需要请求 - if (permissionsToRequest.isNotEmpty) { - statuses = await permissionsToRequest.request(); - - bool allGranted = true; - statuses.forEach((permission, status) { - if (!status.isGranted) { - allGranted = false; - Logger.w(_tag, '权限 ${permission.toString()} 未被授予: $status'); + if (!allGranted) { + return false; } - }); - - if (!allGranted) { - return false; } + } else if (Platform.isIOS) { + await _bluetoothService.refreshDeviceConnection(); + + // 获取iOS蓝牙权限状态 + final bluetoothState = await _bluetoothService.bluetoothState; + final permissionStatus = await Permission.bluetooth.status; + + Logger.i(_tag, 'iOS蓝牙状态: $bluetoothState'); + Logger.i(_tag, 'iOS蓝牙权限状态: $permissionStatus'); + // iOS上不需要主动请求权限,系统会在使用蓝牙时自动请求 + _hasPermissions.value = true; + return true; } _hasPermissions.value = true; return true; } - + /// 显示权限设置对话框 void _showPermissionSettingsDialog(String message) { Get.dialog( @@ -409,6 +364,49 @@ class PairingController extends GetxController { _updateStatusDescription(); try { + // iOS 环境也启用完整的关联流程 + if (Platform.isIOS) { + Logger.i(_tag, 'iOS: 启动完整关联流程,使用AccessorySetupKit'); + + // 检查是否支持Companion功能 + final isSupported = await _bleService.isCompanionSupported(); + if (!isSupported) { + Logger.i(_tag, 'iOS设备不支持Companion功能,跳过关联'); + _companionAssociated.value = true; + _autoNavigateToHome(); + return; + } + + // 检查设备名称是否符合要求 + if (!_isValidDeviceName(currentDevice!.name)) { + statusDescription.value = 'pleaseConnectDeepSoundDevice'.tr; + _isAssociating = false; + return; + } + + Logger.i(_tag, 'iOS开始Companion设备关联流程, 设备: ${currentDevice!.name}'); + + var success = await _bleService.startDeviceAssociation( + deviceNamePattern: currentDevice!.name, + macAddress: currentDevice!.address, + ); + + _isAssociating = false; + + if (success) { + Logger.i(_tag, 'iOS Companion设备关联成功'); + _companionAssociated.value = true; + _userRejectedAssociation.value = false; + _autoNavigateToHome(); + } else { + Logger.e(_tag, 'iOS Companion设备关联失败或被用户拒绝'); + _userRejectedAssociation.value = true; + _updateStatusDescription(); + } + return; + } + + // 下面是原有的Android关联流程 // 检查是否支持Companion功能 final isSupported = await _bleService.isCompanionSupported(); if (!isSupported) { @@ -538,50 +536,17 @@ class PairingController extends GetxController { isLoading.value = true; statusDescription.value = '正在重新初始化蓝牙...'; - // iOS上尝试重置权限状态 + // iOS上简化处理 if (Platform.isIOS) { - await Future.delayed(Duration(milliseconds: 500)); - - // 检查蓝牙权限 - PermissionStatus bluetoothStatus = await Permission.bluetooth.status; - Logger.i(_tag, "iOS蓝牙权限当前状态: $bluetoothStatus"); - - if (bluetoothStatus.isPermanentlyDenied) { - Logger.i(_tag, "iOS蓝牙权限处于永久拒绝状态,提示用户手动开启"); - _showPermissionSettingsDialog('蓝牙权限被拒绝,请在设置中手动开启'); - return; - } - - // 尝试重新请求权限 - if (!bluetoothStatus.isGranted) { - bluetoothStatus = await Permission.bluetooth.request(); - Logger.i(_tag, "iOS蓝牙权限请求结果: $bluetoothStatus"); - } - - // 尝试检查蓝牙状态,这会重新触发系统权限弹窗 - try { - bool isEnabled = await _bluetoothService.isBluetoothEnabled(); - Logger.i(_tag, "iOS蓝牙状态: ${isEnabled ? '开启' : '关闭'}"); - } catch (e) { - Logger.e(_tag, "iOS蓝牙状态检查失败: ${e.toString()}"); - } + Logger.i(_tag, "iOS重新初始化蓝牙: 简化流程"); - // 检查Companion功能(iOS 18+) - try { - bool isSupported = await _bleService.isCompanionSupported(); - if (isSupported) { - Logger.i(_tag, "iOS 18+ Companion功能可用,尝试初始化"); - // 尝试预热AccessorySetupKit - await _bleService.getPairedMacAddress(); - } - } catch (e) { - Logger.w(_tag, "iOS Companion功能检查失败: ${e.toString()}"); - } + // 直接检查蓝牙状态并刷新连接,系统会在需要时请求权限 + await _checkBluetoothAndRefresh(); + return; } - // 重新检查权限 + // Android上的正常流程 if (await _checkAndRequestPermissions()) { - // 重新检查蓝牙状态 await _checkBluetoothAndRefresh(); } } catch (e) { diff --git a/local_plugins/azure_speech/ios/.gitignore b/local_plugins/azure_speech/ios/.gitignore deleted file mode 100644 index 7a7f9873a..000000000 --- a/local_plugins/azure_speech/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/dgph -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Flutter.podspec -Flutter/Generated.xcconfig -Flutter/ephemeral/ -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/local_plugins/azure_speech/ios/Classes/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/Classes/AzureAsrHelper.swift index 7e01c0f8e..2cbe8d9d5 100644 --- a/local_plugins/azure_speech/ios/Classes/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/Classes/AzureAsrHelper.swift @@ -1,9 +1,16 @@ import Foundation import AVFoundation import MicrosoftCognitiveServicesSpeech +// 自定义语音处理组件,提供音频流处理等功能 +import speech -/// Azure语音识别辅助类,支持麦克风和外部音频源的单次和连续语音识别 -class AzureAsrHelper { +/** + * Azure ASR Helper + * + * 基于微软Azure语音服务的ASR实现 + * 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-recognize-speech + */ +class AzureAsrHelper: NSObject, IAsrService { private let tag = "AzureAsrHelper" // 核心组件 @@ -18,97 +25,37 @@ class AzureAsrHelper { private var currentLanguage = "zh-CN" private var supportedLanguages = ["zh-CN"] private var isAutoDetectLanguage = false - private var subscriptionKey = "" - private var region = "" // 音频源配置 - private var audioSourceType = AudioSourceType.microphone - - // 音频处理 - private var microphoneStream: MicrophoneStream? - private var externalAudioStream: ExternalAudioPullStream? - - // 连续识别回调 - private var continuousRecognizeCallback: ContinuousRecognizeCallback? - - /// 应用上下文 - private let context: Any - - /// 音频来源类型 - enum AudioSourceType { - /// 使用设备麦克风 + private enum AudioSourceType { + /** 使用设备麦克风 */ case microphone - /// 使用外部提供的音频数据 + /** 使用外部提供的音频数据 */ case external } - /// 一次性识别回调 - protocol RecognizeCallback { - /// 识别结果回调 - /// - Parameters: - /// - text: 识别文本 - /// - detectedLanguage: 检测到的语言 - func onResult(text: String, detectedLanguage: String) - - /// 识别错误回调 - /// - Parameter error: 错误信息 - func onError(error: String) - } + private var audioSourceType = AudioSourceType.microphone - /// 连续识别回调 - protocol ContinuousRecognizeCallback { - /// 识别中回调 - /// - Parameters: - /// - recognizing: 识别中的文本 - /// - detectedLanguage: 检测到的语言 - func onRecognizing(recognizing: String, detectedLanguage: String) - - /// 识别结果回调 - /// - Parameters: - /// - text: 识别文本 - /// - detectedLanguage: 检测到的语言 - func onResult(text: String, detectedLanguage: String) - - /// 会话开始回调 - func onSessionStarted() - - /// 会话结束回调 - func onSessionStopped() - - /// 识别取消回调 - /// - Parameters: - /// - reason: 取消原因 - /// - errorDetails: 错误详情 - func onCanceled(reason: String, errorDetails: String) - - /// 识别错误回调 - /// - Parameter error: 错误信息 - func onError(error: String) - } + // 音频处理 + private var externalAudioStream: ExternalAudioStream? - /// 初始化Azure语音识别辅助类 - /// - Parameter context: 应用上下文 - init(_ context: Any) { - self.context = context - } + // 事件监听处理 + private var eventListeners = NSHashTable.weakObjects() + + // 音频会话 + private let audioSession = AVAudioSession.sharedInstance() - /// 初始化Azure语音服务 - /// - Parameters: - /// - subscriptionKey: Azure 订阅密钥 - /// - region: Azure 区域 - /// - supportedLanguages: 支持的语言数组,默认为["zh-CN"] - /// - audioSourceType: 音频源类型,默认为麦克风 - /// - Returns: 初始化是否成功 - func initialize( - subscriptionKey: String, - region: String, - supportedLanguages: [String] = ["zh-CN"], - audioSourceType: AudioSourceType = .microphone - ) -> Bool { + /** + * 初始化语音识别服务 + * + * @param subscriptionKey 服务订阅密钥/访问令牌 + * @param region 服务区域/应用ID + * @param supportedLanguages 支持的语言列表,如["zh-CN", "en-US"] + * @return 是否初始化成功 + */ + func initialize(subscriptionKey: String, region: String, supportedLanguages: [String]?) -> Bool { do { - print("\(tag): 初始化 Azure 语音服务, 音频源类型: \(audioSourceType)") - // 检查配置是否为空 if subscriptionKey.isEmpty || region.isEmpty { print("\(tag): Azure 配置信息不完整") @@ -118,579 +65,423 @@ class AzureAsrHelper { // 释放之前的资源 dispose() - // 保存配置 - self.subscriptionKey = subscriptionKey - self.region = region - self.audioSourceType = audioSourceType - // 设置语言 - if !supportedLanguages.isEmpty { + if let supportedLanguages = supportedLanguages, !supportedLanguages.isEmpty { self.supportedLanguages = supportedLanguages } // 根据支持的语言数量决定是否启用自动语言检测 - self.isAutoDetectLanguage = supportedLanguages.count >= 2 + self.isAutoDetectLanguage = self.supportedLanguages.count >= 2 // 如果只有一种语言,设置为当前语言 - if !isAutoDetectLanguage && !supportedLanguages.isEmpty { - self.currentLanguage = supportedLanguages[0] + if !isAutoDetectLanguage && !self.supportedLanguages.isEmpty { + self.currentLanguage = self.supportedLanguages[0] } // 创建语音配置 speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region) if isAutoDetectLanguage { - // 启用语言检测模式 - try speechConfig?.setPropertyTo("Continuous", byId: SPXPropertyId.speechServiceConnection_languageIdMode) + // 直接启用语言检测模式 + speechConfig?.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode") + print("AzureAsrHelper: 启用语言检测模式") + } else { // 设置指定的识别语言 speechConfig?.speechRecognitionLanguage = currentLanguage } - // 设置音频配置 + // 创建识别器 return setupRecognizer() - } catch { print("\(tag): 初始化失败: \(error.localizedDescription)") return false } } - /// 设置识别器 - private func setupRecognizer() -> Bool { - do { - // 清理旧的识别器 - recognizer = nil - - // 设置音频配置 - switch audioSourceType { - case .microphone: - // 检查是否连接了耳机 - let isHeadsetConnected = isHeadphonesConnected() - - if !isHeadsetConnected { - print("\(tag): 未检测到耳机,使用麦克风输入") - // 使用拉流模式进行回音消除 - setupMicrophoneStream() - } else { - print("\(tag): 检测到耳机连接,使用默认麦克风输入") - // 使用默认麦克风输入 - audioConfig = SPXAudioConfiguration() - } - - case .external: - // 创建拉流 - setupExternalAudioStream() - print("\(tag): 使用外部音频源(拉流模式)") - } - - // 创建识别器 - if isAutoDetectLanguage { - let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(sourceLanguages: supportedLanguages) - recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig!, autoDetectSourceLanguageConfiguration: autoDetectConfig, audioConfiguration: audioConfig!) - } else { - recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig!) - } - - print("\(tag): Azure 语音服务初始化成功") + /** + * 单次识别(非流式) + * + * @param audioData 要识别的音频数据字节数组 + * @param callback 识别结果回调 + * @return 是否成功开始识别 + */ + func recognizeOnce(audioData: Data, callback: @escaping (String) -> Void) -> Bool { + return false + } + + /** + * 启动连续识别 + * + * @return 是否成功启动 + */ + func startContinuousRecognition() -> Bool { + if isContinuousRecognitionActive { return true - } catch { - print("\(tag): 创建识别器失败: \(error.localizedDescription)") - stopAudioProcessing() + } + + guard let recognizer = recognizer else { + print("\(tag): 启动连续识别失败: 识别器未初始化") return false } - } - - /// 设置麦克风流 - 使用拉流方式 - private func setupMicrophoneStream() { + do { - // 创建麦克风流 - microphoneStream = MicrophoneStream() - - // 创建音频配置 - audioConfig = try SPXAudioConfiguration(streamInput: microphoneStream!) - - print("\(tag): 已设置麦克风流(拉流模式)") + // 启动连续识别 + try recognizer.startContinuousRecognition() + isContinuousRecognitionActive = true + return true } catch { - print("\(tag): 设置麦克风流失败: \(error.localizedDescription)") + print("\(tag): 启动连续识别失败: \(error.localizedDescription)") + return false } } - /// 设置外部音频流 - 使用拉流方式 - private func setupExternalAudioStream() { + /** + * 停止连续识别 + * + * @return 是否成功停止 + */ + func stopContinuousRecognition() -> Bool { + guard let recognizer = recognizer, isContinuousRecognitionActive else { + return true + } + do { - // 创建外部音频拉流对象 - externalAudioStream = ExternalAudioPullStream() - - // 创建音频配置 - audioConfig = try SPXAudioConfiguration(streamInput: externalAudioStream!) - - print("\(tag): 已设置外部音频流(拉流模式)") + // 停止连续识别 + try recognizer.stopContinuousRecognition() + isContinuousRecognitionActive = false + return true } catch { - print("\(tag): 设置外部音频流失败: \(error.localizedDescription)") + print("\(tag): 停止连续识别失败: \(error.localizedDescription)") + return false } } - /// 向音频流写入音频数据 - /// 仅当音频源设置为external时有效 - /// - Parameter data: 音频数据字节数组 - func pushAudioData(_ data: Data) { + /** + * 推送音频数据 + * 在连续识别模式下推送新的音频数据 + * + * @param audioData 要识别的音频数据字节数组 + * @return 是否成功处理 + */ + func pushAudioData(audioData: Data) -> Bool { + if !isContinuousRecognitionActive { + return false + } + if audioSourceType != .external { - print("\(tag): 当前未使用外部音频源,忽略推送的音频数据") - return + notifyEvent(eventType: .error, params: ["error": "当前不是外部音频流模式"]) + return false } - // 使用拉流模式,将数据推入队列 - externalAudioStream?.pushAudio(data) + // 将音频数据推送到外部音频流 + externalAudioStream?.pushAudio(audioData) + return true } - /// 执行一次性语音识别 - /// - Parameter callback: 识别结果回调 - func recognizeOnce(callback: RecognizeCallback) { - if speechConfig == nil { - callback.onError(error: "语音服务未初始化") - return - } - - // 确保不在连续识别中 + /** + * 添加ASR事件监听器 + * + * @param listener 事件监听器 + */ + func addListener(_ listener: AsrEventListener) { + eventListeners.add(listener as AnyObject) + } + + /** + * 移除ASR事件监听器 + * + * @param listener 要移除的事件监听器 + */ + func removeListener(_ listener: AsrEventListener) { + eventListeners.remove(listener as AnyObject) + } + + /** + * 释放资源 + * 在不再需要服务时调用,释放底层资源 + */ + func dispose() { + // 停止识别 if isContinuousRecognitionActive { _ = stopContinuousRecognition() } - // 重置识别器 - if !setupRecognizer() { - callback.onError(error: "重置识别器失败") - return - } - - // 启动音频处理 - startAudioProcessing() + // 停止音频处理 + stopAudioProcessing() - // 执行同步识别 - do { - let result = try recognizer?.recognizeOnce() - - // 停止音频处理 - stopAudioProcessing() - - if let result = result, result.reason == .recognizedSpeech { - let resultText = result.text ?? "" - var detectedLanguage = supportedLanguages[0] - - // 尝试获取检测到的语言 - if isAutoDetectLanguage, let autoDetectResult = result.properties?.getPropertyByName("SourceLanguage") { - detectedLanguage = autoDetectResult - } - - callback.onResult(text: resultText, detectedLanguage: detectedLanguage) - } else { - callback.onError(error: "未能识别语音") - } - } catch { - stopAudioProcessing() - callback.onError(error: "识别异常: \(error.localizedDescription)") - } + // 释放资源 + recognizer = nil + speechConfig = nil + audioConfig = nil + externalAudioStream = nil } - /// 开始连续语音识别 - /// - Parameter callback: 连续识别结果回调 - /// - Returns: 是否成功开始识别 - func startContinuousRecognition(_ callback: ContinuousRecognizeCallback) -> Bool { - if speechConfig == nil { - callback.onError(error: "语音服务未初始化") - return false - } - - if isContinuousRecognitionActive { - return true - } - - // 重置识别器 - if !setupRecognizer() { - callback.onError(error: "重置识别器失败") + /** + * 设置识别参数 + * + * @param params 参数键值对 + * @return 是否设置成功 + */ + func setParameters(_ params: [String: Any]) -> Bool { + // 未实现自定义参数,后续可以根据需要添加 + return true + } + + /** + * 获取连续识别是否活跃 + */ + func isRecognitionActive() -> Bool { + return isContinuousRecognitionActive + } + + // MARK: - 辅助方法 + + /** + * 创建识别器 + * + * @return 是否创建成功 + */ + private func setupRecognizer() -> Bool { + guard let speechConfig = speechConfig else { + print("\(tag): 无法创建识别器:语音配置不存在") return false } - // 保存回调 - continuousRecognizeCallback = callback - do { - // 设置各种事件监听 - setupEventListeners(callback) + if audioSourceType == .external { + // 设置外部音频流 + setupExternalAudioStream() + } - // 启动音频处理 - startAudioProcessing() + // 根据是否启用自动识别语言创建不同的识别器 + if isAutoDetectLanguage && supportedLanguages.count > 0 { + // 创建语言自动检测配置 + let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages) + + if let audioConfig = self.audioConfig { + // 使用指定的音频源创建识别器 + recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig, + autoDetectSourceLanguageConfiguration: autoDetectConfig, + audioConfiguration: audioConfig) + } else { + // 使用默认音频源创建识别器 + // 创建一个默认的麦克风配置而不是传nil + let defaultAudioConfig = SPXAudioConfiguration() + recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig, + autoDetectSourceLanguageConfiguration: autoDetectConfig, + audioConfiguration: defaultAudioConfig) + } + } else { + // 使用单一语言配置创建识别器 + if let audioConfig = self.audioConfig { + // 使用指定的音频源创建识别器 + recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig, + audioConfiguration: audioConfig) + } else { + // 使用默认音频源创建识别器 + // 创建一个默认的麦克风配置 + let defaultAudioConfig = SPXAudioConfiguration() + recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig, + audioConfiguration: defaultAudioConfig) + } + } - // 开始连续识别 - try recognizer?.startContinuousRecognition() - isContinuousRecognitionActive = true + // 设置事件监听器 + setupEventListeners() return true } catch { - isContinuousRecognitionActive = false + print("\(tag): 创建识别器失败: \(error.localizedDescription)") stopAudioProcessing() - callback.onError(error: "启动连续识别失败: \(error.localizedDescription)") return false } } - /// 设置事件监听器 - private func setupEventListeners(_ callback: ContinuousRecognizeCallback) { + /** + * 停止音频处理 + */ + private func stopAudioProcessing() { + // 清理外部音频流 + externalAudioStream = nil + } + + /** + * 设置外部音频流 + */ + private func setupExternalAudioStream() { + externalAudioStream = ExternalAudioStream() + // 使用ExternalAudioStream实例中创建的pullStream + audioConfig = SPXAudioConfiguration(streamInput: externalAudioStream!.pullStream) + } + + /** + * 设置事件监听器 + */ + private func setupEventListeners() { guard let recognizer = recognizer else { return } - // 识别中事件 - recognizer.addRecognizing { [weak self] _, event in + // 修改为直接设置事件回调 + recognizer.addRecognizingEventHandler { [weak self] (sender, event) in guard let self = self else { return } var detectedLanguage = "" - if self.isAutoDetectLanguage, let lang = event.result.properties?.getPropertyByName("SourceLanguage") { - detectedLanguage = lang + // 修改event.result的处理,不使用可选链 + let result = event.result + if self.isAutoDetectLanguage { + // 处理自动语言检测结果 + if let properties = result.properties, + let language = properties.getPropertyByName("SpeechServiceResponse_RecognitionLanguage") { + detectedLanguage = language + } } - print("\(self.tag): 识别中: \(event.result.text ?? ""), 语言: \(detectedLanguage)") - callback.onRecognizing(recognizing: event.result.text ?? "", detectedLanguage: detectedLanguage) + self.notifyEvent(eventType: .recognizing, params: [ + "text": result.text, + "language": detectedLanguage + ]) } - // 识别完成事件 - recognizer.addRecognized { [weak self] _, event in + // 添加最终识别结果处理器 + recognizer.addRecognizedEventHandler { [weak self] (sender, event) in guard let self = self else { return } - if event.result.reason == .recognizedSpeech { - var detectedLanguage = "" - if self.isAutoDetectLanguage, let lang = event.result.properties?.getPropertyByName("SourceLanguage") { - detectedLanguage = lang + // 修改event.result的处理,不使用可选绑定 + let result = event.result + var detectedLanguage = "" + + if self.isAutoDetectLanguage { + // 处理自动语言检测结果 + if let properties = result.properties, + let language = properties.getPropertyByName("SpeechServiceResponse_RecognitionLanguage") { + detectedLanguage = language } - print("\(self.tag): 识别完成: \(event.result.text ?? ""), 语言: \(detectedLanguage)") - callback.onResult(text: event.result.text ?? "", detectedLanguage: detectedLanguage) + // 尝试另一种方式获取语言 + do { + let langResult = try SPXAutoDetectSourceLanguageResult(result) + if let lang = langResult.language, !lang.isEmpty { + detectedLanguage = lang + } + } catch { + // 忽略错误,继续使用之前获取的语言 + } } + + self.notifyEvent(eventType: .recognized, params: [ + "text": result.text, + "language": detectedLanguage + ]) } - // 会话开始事件 - recognizer.addSessionStarted { [weak self] _, _ in + // 添加会话开始事件处理器 + recognizer.addSessionStartedEventHandler { [weak self] (sender, event) in guard let self = self else { return } - - print("\(self.tag): 识别会话已开始") - callback.onSessionStarted() + self.notifyEvent(eventType: .sessionStarted) } - // 会话结束事件 - recognizer.addSessionStopped { [weak self] _, _ in + // 添加会话结束事件处理器 + recognizer.addSessionStoppedEventHandler { [weak self] (sender, event) in guard let self = self else { return } - - print("\(self.tag): 识别会话已结束") self.isContinuousRecognitionActive = false - self.stopAudioProcessing() - callback.onSessionStopped() + self.notifyEvent(eventType: .sessionStopped) } - // 取消事件 - recognizer.addCanceled { [weak self] _, event in + // 添加取消事件处理器 + recognizer.addCanceledEventHandler { [weak self] (sender, event) in guard let self = self else { return } - let errorDetails = event.errorDetails ?? "未知错误" - let reason = String(describing: event.reason) + print("\(self.tag): 识别取消: \(event.errorDetails ?? "未知错误")") - print("\(self.tag): 识别取消: \(errorDetails)") + // 标记连续识别已停止 self.isContinuousRecognitionActive = false - self.stopAudioProcessing() - callback.onCanceled(reason: reason, errorDetails: errorDetails) - callback.onError(error: "识别取消: \(errorDetails)") // 兼容旧接口 + // 通知错误 + self.notifyEvent(eventType: .canceled, params: [ + "error": "识别取消", + "errorDetails": event.errorDetails ?? "未知错误", + "reason": String(event.reason.rawValue) + ]) } } - /// 停止连续语音识别 - /// - Returns: 是否成功停止 - func stopContinuousRecognition() -> Bool { - if speechConfig == nil { - print("\(tag): 语音服务未初始化") - return false - } - - if !isContinuousRecognitionActive { - print("\(tag): 未进行连续识别,忽略停止请求") - return true - } - - do { - print("\(tag): 停止连续语音识别") - - if recognizer == nil { - print("\(tag): 识别器为空,重置状态") - isContinuousRecognitionActive = false - return true - } - - // 停止连续识别 - try recognizer?.stopContinuousRecognition() - - // 停止音频处理 - stopAudioProcessing() - - // 等待一段时间以确保资源正确释放 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - self?.isContinuousRecognitionActive = false + /** + * 触发事件通知 + */ + private func notifyEvent(eventType: AsrEventType, params: [String: Any] = [:]) { + let event = AsrEvent(type: eventType, params: params) + + // 在主线程通知事件 + DispatchQueue.main.async { + for case let listener as AsrEventListener in self.eventListeners.allObjects { + listener.onEvent(event) } - - print("\(tag): 连续识别停止指令已发送") - return true - } catch { - // 强制重置状态 - isContinuousRecognitionActive = false - print("\(tag): 停止连续识别失败: \(error.localizedDescription)") - - // 停止音频处理 - stopAudioProcessing() - - // 尝试强制关闭识别器 - recognizer = nil - - return false - } - } - - /// 检查连续识别是否活跃 - /// - Returns: 是否活跃 - func isContinuousRecognitionActive() -> Bool { - return isContinuousRecognitionActive - } - - /// 释放所有资源 - func dispose() { - // 如果正在进行连续识别,先停止 - if isContinuousRecognitionActive { - _ = stopContinuousRecognition() - } - - // 停止音频处理 - stopAudioProcessing() - - // 清理所有资源 - recognizer = nil - speechConfig = nil - audioConfig = nil - microphoneStream = nil - externalAudioStream = nil - - // 重置状态 - isContinuousRecognitionActive = false - continuousRecognizeCallback = nil - } - - /// 启动音频处理 - private func startAudioProcessing() { - switch audioSourceType { - case .microphone: - // 拉流模式不需要额外启动,SDK会自动拉取数据 - break - case .external: - // 外部音频数据模式下不需要启动处理,等待外部调用pushAudioData - break } } - /// 停止音频处理 - private func stopAudioProcessing() { - microphoneStream?.close() - microphoneStream = nil - - externalAudioStream?.close() - externalAudioStream = nil - } - - /// 检查是否连接了耳机 - private func isHeadphonesConnected() -> Bool { - let audioSession = AVAudioSession.sharedInstance() - let outputs = audioSession.currentRoute.outputs - - // 检查是否存在耳机类型的输出 - let headphoneTypes: [AVAudioSession.Port] = [ - .headphones, .bluetoothA2DP, .bluetoothHFP, .bluetoothLE - ] - - for output in outputs { - if headphoneTypes.contains(output.portType) { - return true - } - } - - return false - } - - /// 麦克风流 - 拉流模式 - /// 实现SPXPullAudioInputStreamDelegate,为Azure SDK提供音频数据 - class MicrophoneStream: NSObject, SPXPullAudioInputStreamDelegate { - private let tag = "MicrophoneStream" - private var audioEngine: AVAudioEngine? - private var audioBuffer = Data() - private let bufferLock = NSLock() + /** + * 外部音频流 - 使用拉流模式 + * 基于NSObject实现,将外部推送的音频数据转换为SDK可拉取的形式 + */ + class ExternalAudioStream: NSObject { + // 内部持有一个SPXPullAudioInputStream实例 + private(set) var pullStream: SPXPullAudioInputStream! + + // 音频数据缓冲区 + private var buffer = Data() + private let lock = NSLock() + + // 记录是否已关闭 + private var isClosed = false + /** + * 初始化方法 + */ override init() { super.init() - setupAudioEngine() - } - - private func setupAudioEngine() { - do { - print("\(tag): 初始化音频引擎") - - // 创建音频引擎 - audioEngine = AVAudioEngine() - - // 设置音频会话 - let audioSession = AVAudioSession.sharedInstance() - try audioSession.setCategory(.record, mode: .default) - try audioSession.setActive(true, options: .notifyOthersOnDeactivation) - - // 获取输入节点 - guard let inputNode = audioEngine?.inputNode else { - print("\(tag): 无法获取输入节点") - return - } - - // 创建16kHz, 16bit, 单声道格式 - let format = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 16000, channels: 1, interleaved: true) - - // 设置音频处理回调 - let bufferSize = 4096 - inputNode.installTap(onBus: 0, bufferSize: UInt32(bufferSize), format: format) { [weak self] (buffer, time) in - guard let self = self else { return } - - // 将音频数据转换为Data - let audioBuffer = buffer.int16ChannelData?[0] - let audioBufferSize = Int(buffer.frameLength * buffer.format.streamDescription.pointee.mBytesPerFrame) - - if let audioBuffer = audioBuffer { - let data = Data(bytes: audioBuffer, count: audioBufferSize) - self.bufferLock.lock() - self.audioBuffer.append(data) - self.bufferLock.unlock() - } - } - - // 启动音频引擎 - audioEngine?.prepare() - try audioEngine?.start() - - print("\(tag): 音频引擎启动成功") - } catch { - print("\(tag): 设置音频引擎失败: \(error.localizedDescription)") - } - } - - // SPXPullAudioInputStreamDelegate协议方法 - func read(_ buffer: UnsafeMutablePointer, size: UInt32) -> Int32 { - bufferLock.lock() - defer { bufferLock.unlock() } - - let maxSize = Int(size) - - if audioBuffer.count == 0 { - // 没有数据可读 - return 0 - } - - let readSize = min(audioBuffer.count, maxSize) - audioBuffer.copyBytes(to: buffer, from: 0.. Int in + guard let self = self else { return 0 } + self.lock.lock() + defer { self.lock.unlock() } + if self.buffer.isEmpty { return 0 } + let bytesToRead = min(Int(size), self.buffer.count) + data.append(self.buffer.prefix(bytesToRead)) + self.buffer.removeFirst(bytesToRead) + return bytesToRead + }, + closeHandler: { [weak self] in + self?.close() + } + ) } - // 关闭音频资源 - func close() { - print("\(tag): 关闭音频引擎") - - // 停止音频引擎 - if let inputNode = audioEngine?.inputNode { - inputNode.removeTap(onBus: 0) - } - - audioEngine?.stop() - audioEngine = nil - - // 关闭音频会话 - do { - try AVAudioSession.sharedInstance().setActive(false) - } catch { - print("\(tag): 关闭音频会话失败: \(error.localizedDescription)") - } - - // 清空缓冲区 - bufferLock.lock() - audioBuffer = Data() - bufferLock.unlock() - - print("\(tag): 音频资源已释放") - } - } - - /// 外部音频拉流 - /// 实现SPXPullAudioInputStreamDelegate,将外部推送的音频数据转换为SDK可拉取的形式 - class ExternalAudioPullStream: NSObject, SPXPullAudioInputStreamDelegate { - private let tag = "ExternalAudioPullStream" - private let audioQueue = DispatchQueue(label: "com.yunqiinnovation.azure_speech.externalAudio") - private var audioBuffer = Data() - private let bufferLock = NSLock() - private var isClosed = false - - // 外部调用:推送音频数据 + /** + * 推送音频数据到缓冲区 + * + * @param data 要推送的音频数据 + */ func pushAudio(_ data: Data) { if isClosed { return } - bufferLock.lock() - audioBuffer.append(data) - bufferLock.unlock() - } - - // SPXPullAudioInputStreamDelegate协议方法 - func read(_ buffer: UnsafeMutablePointer, size: UInt32) -> Int32 { - bufferLock.lock() - defer { bufferLock.unlock() } - - if isClosed { - return 0 - } - - let maxSize = Int(size) + lock.lock() + defer { lock.unlock() } - if audioBuffer.count == 0 { - // 没有数据可读,返回0表示需要等待 - return 0 - } - - let readSize = min(audioBuffer.count, maxSize) - audioBuffer.copyBytes(to: buffer, from: 0.. + +@interface AzureSpeechPlugin : NSObject +@end \ No newline at end of file diff --git a/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.m b/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.m new file mode 100644 index 000000000..ca89297ef --- /dev/null +++ b/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.m @@ -0,0 +1,12 @@ +#import "AzureSpeechPlugin.h" +#if __has_include() +#import +#else +#import "azure_speech-Swift.h" +#endif + +@implementation AzureSpeechPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftAzureSpeechPlugin registerWithRegistrar:registrar]; +} +@end \ No newline at end of file diff --git a/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.swift index dcb2d57b8..87ae07e43 100644 --- a/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.swift +++ b/local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.swift @@ -1,182 +1,388 @@ import Flutter import UIKit +import AVFoundation +import MicrosoftCognitiveServicesSpeech +// 自定义语音处理组件,提供音频流处理等功能 +import speech -public class AzureSpeechPlugin: NSObject, FlutterPlugin { - private var ttsHelper: AzureTtsHelper? - private var asrHelper: AzureAsrHelper? - private var eventSink: FlutterEventSink? - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "azure_speech", binaryMessenger: registrar.messenger()) - let instance = AzureSpeechPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) +/** + * Azure Speech Plugin + * + * 基于微软Azure语音服务的Flutter插件 + * 提供语音识别(ASR)和语音合成(TTS)功能 + */ +public class SwiftAzureSpeechPlugin: NSObject, FlutterPlugin { + // 日志标签 + private let tag = "AzureSpeechPlugin" - // 初始化事件通道 - let eventChannel = FlutterEventChannel(name: "azure_speech/asr_events", binaryMessenger: registrar.messenger()) - eventChannel.setStreamHandler(AsrStreamHandler(instance: instance)) - } - - override init() { - super.init() - ttsHelper = AzureTtsHelper() - asrHelper = AzureAsrHelper() + // ASR相关 + private var asrChannel: FlutterMethodChannel? + private var asrEventChannel: FlutterEventChannel? + private var asrEventSink: FlutterEventSink? + private let azureAsrHelper = AzureAsrHelper() - // 设置ASR事件处理 - asrHelper?.setEventHandler { [weak self] event in - self?.handleAsrEvent(event) + // TTS相关 + private var ttsChannel: FlutterMethodChannel? + private var ttsEventChannel: FlutterEventChannel? + private var ttsEventSink: FlutterEventSink? + private let azureTtsHelper = AzureTtsHelper() + + // 是否已添加TTS事件监听器 + private var isTtsListenerAdded = false + + // 插件注册 + public static func register(with registrar: FlutterPluginRegistrar) { + let instance = SwiftAzureSpeechPlugin() + + // 初始化ASR通道 + let asrChannel = FlutterMethodChannel(name: "azure_speech/asr", binaryMessenger: registrar.messenger()) + registrar.addMethodCallDelegate(instance, channel: asrChannel) + instance.asrChannel = asrChannel + + // 初始化TTS通道 + let ttsChannel = FlutterMethodChannel(name: "azure_speech/tts", binaryMessenger: registrar.messenger()) + registrar.addMethodCallDelegate(instance, channel: ttsChannel) + instance.ttsChannel = ttsChannel + + // 初始化ASR事件通道 + let asrEventChannel = FlutterEventChannel(name: "azure_speech/asr_events", binaryMessenger: registrar.messenger()) + asrEventChannel.setStreamHandler(instance) + instance.asrEventChannel = asrEventChannel + + // 初始化TTS事件通道 + let ttsEventChannel = FlutterEventChannel(name: "azure_speech/tts_events", binaryMessenger: registrar.messenger()) + ttsEventChannel.setStreamHandler(instance) + instance.ttsEventChannel = ttsEventChannel + } + + // 发送ASR事件方法 + private func sendAsrEvent(_ event: [String: Any]) { + if asrEventSink == nil { + print("\(tag): 无法发送ASR事件:事件通道未准备好") + return + } + + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.asrEventSink?(event) + } + } + + // 发送TTS事件方法 + private func sendTtsEvent(_ event: [String: Any]) { + if ttsEventSink == nil { + print("\(tag): 无法发送TTS事件:事件通道未准备好") + return + } + + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.ttsEventSink?(event) + } + } + + // 设置TTS事件监听器 + private func setupTtsEventListener() { + if !isTtsListenerAdded { + azureTtsHelper.addListener(self) + isTtsListenerAdded = true + } + } + + // 设置ASR事件监听器 + private func setupAsrEventListener() { + azureAsrHelper.addListener(self) + } + + // 处理Flutter方法调用 + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + if call.method.hasPrefix("tts_") { + handleTtsMethodCall(call, result) + } else { + handleAsrMethodCall(call, result) + } + } + + // MARK: - ASR 方法处理 + + private func handleAsrMethodCall(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + switch call.method { + case "initialize": + guard let args = call.arguments as? [String: Any], + let subscriptionKey = args["subscriptionKey"] as? String, + let region = args["region"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENTS", message: "必要的参数不能为空", details: nil)) + return + } + + let supportedLanguages = args["supportedLanguages"] as? [String] + + // 初始化ASR引擎并设置事件监听 + let success = azureAsrHelper.initialize(subscriptionKey: subscriptionKey, region: region, supportedLanguages: supportedLanguages) + setupAsrEventListener() + + result(success) + + case "recognizeOnce": + guard let args = call.arguments as? [String: Any], + let audioBytes = args["data"] as? FlutterStandardTypedData else { + result(FlutterError(code: "INVALID_ARGUMENTS", message: "音频数据不能为空", details: nil)) + return + } + + // 执行一次性识别 + let success = azureAsrHelper.recognizeOnce(audioData: audioBytes.data) { recognizedText in + result(["text": recognizedText, "detectedLanguage": ""]) + } + + if !success { + result(FlutterError(code: "RECOGNITION_ERROR", message: "启动识别失败", details: nil)) + } + + case "startContinuousRecognition": + // 确保事件通道已准备好 + if asrEventSink == nil { + result(FlutterError(code: "EVENT_CHANNEL_NOT_READY", message: "事件通道未准备好,无法开始连续识别", details: nil)) + return + } + + let success = azureAsrHelper.startContinuousRecognition() + result(success) + + case "stopContinuousRecognition": + let success = azureAsrHelper.stopContinuousRecognition() + result(success) + + case "isContinuousRecognitionActive": + result(azureAsrHelper.isRecognitionActive()) + + case "dispose": + azureAsrHelper.dispose() + result(true) + + case "pushAudioData": + guard let args = call.arguments as? [String: Any], + let audioBytes = args["data"] as? FlutterStandardTypedData else { + result(FlutterError(code: "INVALID_ARGUMENTS", message: "音频数据不能为空", details: nil)) + return + } + + let success = azureAsrHelper.pushAudioData(audioData: audioBytes.data) + result(success) + + default: + result(FlutterMethodNotImplemented) + } + } + + // MARK: - TTS 方法处理 + + private func handleTtsMethodCall(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + switch call.method { + case "tts_initialize": + guard let args = call.arguments as? [String: Any], + let subscriptionKey = args["subscriptionKey"] as? String, + let region = args["region"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENTS", message: "必要的参数不能为空", details: nil)) + return + } + + // 初始化TTS引擎 + let language = args["language"] as? String ?? "zh-CN" + let success = azureTtsHelper.initialize(ttsAppId: "", ttsAppToken: subscriptionKey, ttsResource: region, language: language) + + // 设置TTS事件监听器 + setupTtsEventListener() + + result(success) + + case "tts_set_voice": + guard let args = call.arguments as? [String: Any], + let voiceName = args["voiceName"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENTS", message: "语音名称不能为空", details: nil)) + return + } + + let success = azureTtsHelper.setVoice(voiceName) + result(success) + + case "tts_speak_once": + guard let args = call.arguments as? [String: Any], + let text = args["text"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil)) + return + } + + let success = azureTtsHelper.speakOnce(text) + result(success) + + case "tts_speak_stream": + guard let args = call.arguments as? [String: Any], + let text = args["text"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil)) + return + } + + let success = azureTtsHelper.speakStream(text) + result(success) + + case "tts_flush_stream": + let success = azureTtsHelper.flushStream() + result(success) + + case "tts_stop": + let success = azureTtsHelper.stop() + result(success) + + case "tts_isSpeaking": + result(azureTtsHelper.isSpeaking()) + + case "tts_release": + azureTtsHelper.dispose() + result(true) + + default: + result(FlutterMethodNotImplemented) + } } - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - // TTS相关方法 - case "initializeTts": - guard let args = call.arguments as? [String: Any], - let subscriptionKey = args["subscriptionKey"] as? String, - let serviceRegion = args["serviceRegion"] as? String else { - result(false) - return - } - - let language = args["language"] as? String ?? "zh-CN" - let success = ttsHelper?.initialize(speechSubscriptionKey: subscriptionKey, serviceRegion: serviceRegion, language: language) ?? false - result(success) - - case "setTtsVoice": - guard let args = call.arguments as? [String: Any], - let voiceName = args["voiceName"] as? String else { - result(false) - return - } - - let success = ttsHelper?.setVoice(voiceName: voiceName) ?? false - result(success) - - case "setTtsSpeechParams": - guard let args = call.arguments as? [String: Any], - let rate = args["rate"] as? Int, - let pitch = args["pitch"] as? Int, - let volume = args["volume"] as? Int else { - result(false) - return - } - - let success = ttsHelper?.setSpeechParams(rate: rate, pitch: pitch, volume: volume) ?? false - result(success) - - case "speakText": - guard let args = call.arguments as? [String: Any], - let text = args["text"] as? String else { - result(false) - return - } - - ttsHelper?.speakText(text: text) { success, _ in - result(success) - } - - case "stopSpeaking": - let success = ttsHelper?.stopSpeaking() ?? false - result(success) - - case "isSpeaking": - let speaking = ttsHelper?.isSpeaking() ?? false - result(speaking) - - case "setTtsAudioOutputType": - guard let args = call.arguments as? [String: Any], - let outputType = args["outputType"] as? String else { - result(false) - return - } - - var type: AzureTtsHelper.AudioOutputType = .auto - switch outputType.uppercased() { - case "SPEAKER": - type = .speaker - case "EARPIECE": - type = .earpiece - default: - type = .auto - } - - let success = ttsHelper?.setAudioOutputType(outputType: type) ?? false - result(success) - - // ASR相关方法 - case "initializeAsr": - guard let args = call.arguments as? [String: Any], - let subscriptionKey = args["subscriptionKey"] as? String, - let serviceRegion = args["serviceRegion"] as? String, - let supportedLanguages = args["supportedLanguages"] as? [String] else { - result(false) - return - } - - let success = asrHelper?.initialize(speechSubscriptionKey: subscriptionKey, serviceRegion: serviceRegion, supportedLanguages: supportedLanguages) ?? false - result(success) - - case "recognizeOnce": - asrHelper?.recognizeOnce { success, text, language, error in - var resultMap: [String: Any] = ["success": success] - if success { - resultMap["text"] = text - resultMap["language"] = language +} + +// MARK: - 事件处理 +extension SwiftAzureSpeechPlugin: FlutterStreamHandler { + public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + // 根据通道类型设置事件接收器 + if let args = arguments as? [String: Any], + let channel = args["channel"] as? String { + + if channel == "asr" { + asrEventSink = events + } else if channel == "tts" { + ttsEventSink = events + setupTtsEventListener() + } + } else if arguments == nil { + // 如果没有指定通道,尝试弄清楚是哪个通道在监听 + if asrEventSink == nil && ttsEventSink != nil { + asrEventSink = events + } else if asrEventSink != nil && ttsEventSink == nil { + ttsEventSink = events + setupTtsEventListener() + } else { + // 无法确定哪个通道,默认设置为ASR + asrEventSink = events + } + } + + return nil + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + // 根据通道类型清除事件接收器 + if let args = arguments as? [String: Any], + let channel = args["channel"] as? String { + + if channel == "asr" { + asrEventSink = nil + } else if channel == "tts" { + ttsEventSink = nil + } } else { - resultMap["error"] = error + // 如果没有指定通道,清除所有通道 + asrEventSink = nil + ttsEventSink = nil + } + + return nil + } +} + +// MARK: - ASR 事件监听实现 +extension SwiftAzureSpeechPlugin: AsrEventListener { + // 为ASR事件监听提供明确的选择器名称 + @objc(onAsrEvent:) + public func onEvent(_ event: AsrEvent) { + var eventMap: [String: Any] = [ + "type": getAsrEventTypeName(event.type) + ] + + // 添加参数 + for (key, value) in event.params { + eventMap[key] = value + } + + sendAsrEvent(eventMap) + } + + private func getAsrEventTypeName(_ type: AsrEventType) -> String { + switch type { + case .sessionStarted: + return "sessionStarted" + case .sessionStopped: + return "sessionStopped" + case .recognizing: + return "recognizing" + case .recognized: + return "result" + case .canceled: + return "canceled" + case .error: + return "error" + @unknown default: + return "unknown" } - result(resultMap) - } - - case "startContinuousRecognition": - let success = asrHelper?.startContinuousRecognition() ?? false - result(success) - - case "stopContinuousRecognition": - let success = asrHelper?.stopContinuousRecognition() ?? false - result(success) - - case "isContinuousRecognitionActive": - let isActive = asrHelper?.isContinuousRecognitionActive() ?? false - result(isActive) - - case "dispose": - ttsHelper?.dispose() - asrHelper?.dispose() - result(nil) - - default: - result(FlutterMethodNotImplemented) } - } - - // 设置事件接收器 - func setEventSink(_ sink: FlutterEventSink?) { - self.eventSink = sink - } - - // 处理ASR事件 - private func handleAsrEvent(_ event: [String: Any]) { - self.eventSink?(event) - } } -// ASR事件流处理器 -class AsrStreamHandler: NSObject, FlutterStreamHandler { - private weak var plugin: AzureSpeechPlugin? - - init(instance: AzureSpeechPlugin) { - self.plugin = instance - super.init() - } - - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - plugin?.setEventSink(events) - return nil - } - - func onCancel(withArguments arguments: Any?) -> FlutterError? { - plugin?.setEventSink(nil) - return nil - } +// MARK: - TTS 事件监听实现 +extension SwiftAzureSpeechPlugin: TtsEventListener { + // 使用@objc特性为方法提供一个不同的Objective-C选择器名称 + @objc(onTtsEvent:) + public func onEvent(_ event: TtsEvent) { + var eventMap: [String: Any] = [:] + + // 根据事件类型转换 + switch event.type { + case .synthesisStarted: + eventMap["type"] = "synthesis_started" + case .synthesisCompleted: + eventMap["type"] = "synthesis_completed" + case .synthesisCanceled: + eventMap["type"] = "synthesis_canceled" + if let reason = event.params["reason"] { + eventMap["reason"] = reason + } + if let errorDetails = event.params["errorDetails"] { + eventMap["errorDetails"] = errorDetails + } + case .error: + eventMap["type"] = "error" + if let errorCode = event.params["errorCode"] { + eventMap["errorCode"] = errorCode + } + if let errorMessage = event.params["errorMessage"] { + eventMap["errorMessage"] = errorMessage + } + @unknown default: + eventMap["type"] = "unknown" + for (key, value) in event.params { + eventMap[key] = value + } + } + + sendTtsEvent(eventMap) + } +} + +// MARK: - 音频数据监听实现 +extension SwiftAzureSpeechPlugin: AudioDataListener { + public func onAudioData(_ data: Data) { + if ttsEventSink == nil { return } + + let eventMap: [String: Any] = [ + "type": "audio_data", + "data": FlutterStandardTypedData(bytes: data) + ] + + sendTtsEvent(eventMap) + } } \ No newline at end of file diff --git a/local_plugins/azure_speech/ios/Classes/AzureTtsHelper.swift b/local_plugins/azure_speech/ios/Classes/AzureTtsHelper.swift index b846fef4a..f3eaa8d57 100644 --- a/local_plugins/azure_speech/ios/Classes/AzureTtsHelper.swift +++ b/local_plugins/azure_speech/ios/Classes/AzureTtsHelper.swift @@ -1,315 +1,175 @@ import Foundation import AVFoundation import MicrosoftCognitiveServicesSpeech +// 自定义语音处理组件,提供音频流处理等功能 +import speech -/// Azure 语音合成辅助类 -class AzureTtsHelper: NSObject { - // 常量定义 +/** + * Azure TTS Helper + * + * 基于微软Azure语音服务的TTS实现 + * 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis + */ +class AzureTtsHelper: NSObject, ITtsService { private let tag = "AzureTtsHelper" - private let DEFAULT_LANGUAGE = "zh-CN" - private let DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural" - // 核心组件 - private var synthesizer: SPXSpeechSynthesizer? + private static let DEFAULT_LANGUAGE = "zh-CN" + private static let DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural" + + // Azure语音服务配置 private var speechConfig: SPXSpeechConfiguration? - private var audioConfig: SPXAudioConfiguration? - private var initialized = false + private var synthesizer: SPXSpeechSynthesizer? + private var isInitialized = false private var speaking = false - // 当前设置 - private var currentVoice = "zh-CN-XiaoxiaoNeural" - private var currentRate = "0%" - private var currentPitch = "0%" + // 当前配置 + private var currentVoice = DEFAULT_VOICE + private var currentLanguage = DEFAULT_LANGUAGE + private var currentRate = "+0%" + private var currentPitch = "+0%" private var currentVolume = "100%" - // 音频会话管理 - private let audioSession = AVAudioSession.sharedInstance() - - // 自定义音频输出流 - private var customAudioOutputStream: SPXPushAudioOutputStream? + // 事件监听器列表 + private var eventListeners = NSHashTable.weakObjects() // 流式文本处理的缓冲区 private var streamBuffer = "" private var lastSpeakTime: TimeInterval = 0 - // 应用上下文 - private let context: Any - - // 全局回调 - private var ttsCallback: TtsCallback? - - /// TTS回调接口 - protocol TtsCallback { - /// 合成开始 - func onSynthesisStarted() - - /// 合成中 - func onSynthesizing() - - /// 合成完成 - func onSynthesisCompleted() - - /// 合成取消 - func onSynthesisCanceled() - } - - /// 初始化 - /// - Parameter context: 应用上下文 - init(_ context: Any) { - self.context = context - super.init() - } + // 音频会话 + private let audioSession = AVAudioSession.sharedInstance() - /// 初始化 TTS 引擎 - /// - Parameters: - /// - subscriptionKey: Azure 语音服务订阅密钥 - /// - region: Azure 语音服务区域 - /// - callback: 可选,全局TTS回调接口 - /// - Returns: 是否初始化成功 - func initialize( - subscriptionKey: String, - region: String, - callback: TtsCallback? = nil - ) -> Bool { - print("\(tag): 初始化 Azure 语音服务") - - // 检查配置是否为空 - if subscriptionKey.isEmpty || region.isEmpty { - print("\(tag): 错误: Azure 配置信息不完整") - return false - } - - // 释放之前的资源 - dispose() - - // 保存全局回调 - ttsCallback = callback - + /** + * 初始化语音合成服务 + * + * @param appId 服务应用ID + * @param token 服务访问令牌/订阅密钥 + * @param resource 服务资源ID/区域(可选) + * @param language 语言代码,如"zh-CN" + * @return 是否初始化成功 + */ + func initialize(ttsAppId: String, ttsAppToken: String, ttsResource: String, language: String) -> Bool { do { // 创建语音配置 - speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region) - - // 设置语音合成输出格式为高质量音频 - speechConfig?.setSpeechSynthesisOutputFormat(.riff24Khz16BitMonoPcm) - - // 设置默认语音 - speechConfig?.setSpeechSynthesisVoiceName(currentVoice) - - // 创建音频配置 - if let customOutput = customAudioOutputStream { - // 使用自定义音频输出流 - audioConfig = try SPXAudioConfiguration(streamOutput: customOutput) + if ttsResource.isEmpty { + speechConfig = try SPXSpeechConfiguration(subscription: ttsAppToken, region: "eastasia") } else { - // 使用默认扬声器 - audioConfig = SPXAudioConfiguration() + speechConfig = try SPXSpeechConfiguration(subscription: ttsAppToken, region: ttsResource) } - // 创建语音合成器 - synthesizer = try SPXSpeechSynthesizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig!) + // 设置语言 + speechConfig?.speechSynthesisLanguage = language + currentLanguage = language - // 设置事件监听 - setupEventListeners() + // 设置音频输出格式 - 使用16k、16位的PCM格式 + speechConfig?.setPropertyTo("Audio16Khz16BitMonoPcm", + byName: "SpeechServiceConnection_SynthOutputFormat") - initialized = true - print("\(tag): TTS 引擎初始化成功") - return true - } catch { - print("\(tag): TTS 引擎初始化失败: \(error.localizedDescription)") - return false - } - } - - /// 设置事件监听器 - private func setupEventListeners() { - guard let synthesizer = synthesizer else { return } - - // 合成开始事件 - synthesizer.addSynthesisStarted { [weak self] _, _ in - guard let self = self else { return } - self.ttsCallback?.onSynthesisStarted() - } - - // 合成中事件 - synthesizer.addSynthesizing { [weak self] _, _ in - guard let self = self else { return } - self.ttsCallback?.onSynthesizing() - } - - // 合成完成事件 - synthesizer.addSynthesisCompleted { [weak self] _, _ in - guard let self = self else { return } - self.speaking = false - self.ttsCallback?.onSynthesisCompleted() - } - - // 合成取消事件 - synthesizer.addSynthesisCanceled { [weak self] _, _ in - guard let self = self else { return } - print("\(self.tag): 语音合成取消") - self.speaking = false - self.ttsCallback?.onSynthesisCanceled() - } - } - - /// 设置TTS回调 - /// - Parameter callback: TTS回调接口 - func setTtsCallback(_ callback: TtsCallback?) { - ttsCallback = callback - } - - /// 设置自定义音频输出流 - /// - Parameter outputStream: 自定义音频输出流,如果为nil则使用默认音频输出 - /// - Returns: 是否设置成功 - func setCustomAudioOutputStream(_ outputStream: SPXPushAudioOutputStream?) -> Bool { - do { - // 保存引用 - customAudioOutputStream = outputStream + // 创建合成器,使用默认音频输出(扬声器) + synthesizer = try SPXSpeechSynthesizer(speechConfig!) - // 如果已初始化,需要重新创建合成器以应用新的音频输出流 - if initialized { - recreateSynthesizer() - } - - return true - } catch { - print("\(tag): 设置自定义音频输出流失败: \(error.localizedDescription)") - return false - } - } - - /// 重新创建合成器 - private func recreateSynthesizer() { - do { - // 关闭现有合成器 - synthesizer = nil + // 设置事件监听 + setupEventListeners() - // 创建音频配置 - if let customOutput = customAudioOutputStream { - // 使用自定义音频输出流 - audioConfig = try SPXAudioConfiguration(streamOutput: customOutput) + // 设置默认音色 - 中文默认使用晓晓,英文默认使用Jenny + if language.lowercased().starts(with: "zh") { + _ = setVoice("zh-CN-XiaoxiaoNeural") } else { - // 使用默认扬声器 - audioConfig = SPXAudioConfiguration() + _ = setVoice("en-US-JennyNeural") } - // 使用新的音频配置创建合成器 - synthesizer = try SPXSpeechSynthesizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig!) + // 设置初始化完成 + isInitialized = true - // 重新设置事件监听 - setupEventListeners() + return true } catch { - print("\(tag): 重新创建合成器失败: \(error.localizedDescription)") + print("\(tag): 语音合成服务初始化失败: \(error.localizedDescription)") + return false } } - /// 设置语音 - /// - Parameter voiceName: 语音名称,例如 "zh-CN-XiaoxiaoNeural" - /// - Returns: 是否设置成功 - func setVoice(voiceName: String) -> Bool { - if !initialized { - print("\(tag): TTS 引擎尚未初始化") - return false - } - - if voiceName == currentVoice { - print("\(tag): 已设置语音: \(voiceName)") - return true - } + /** + * 设置语音角色 + * + * @param voiceName 语音角色名称(不同服务的语音角色命名可能不同) + * @return 是否设置成功 + */ + func setVoice(_ voiceName: String) -> Bool { + if !isInitialized { return false } do { currentVoice = voiceName - speechConfig?.setSpeechSynthesisVoiceName(voiceName) + + // 更新语音名称 + if let config = speechConfig { + config.speechSynthesisVoiceName = voiceName + } // 重新创建合成器 recreateSynthesizer() - - print("\(tag): 已设置语音: \(voiceName)") return true } catch { print("\(tag): 设置语音失败: \(error.localizedDescription)") + notifyEvent(eventType: .error, params: [ + "errorCode": "VOICE_SET_FAILED", + "errorMessage": "设置语音失败: \(error.localizedDescription)" + ]) 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 !initialized { - print("\(tag): TTS 引擎尚未初始化") + /** + * 单次合成并播放 + * + * @param text 要合成的文本 + * @return 是否成功开始合成 + */ + func speakOnce(_ text: String) -> Bool { + if !isInitialized { + print("\(tag): 语音合成未初始化") return false } - do { - currentRate = formatPercentage(rate) - currentPitch = formatPercentage(pitch) - currentVolume = "\(min(max(volume, 0), 100))%" - - print("\(tag): 已设置语音参数: 语速=\(rate), 音调=\(pitch), 音量=\(volume)") - return true - } catch { - print("\(tag): 设置语音参数失败: \(error.localizedDescription)") - return false - } - } - - /// 格式化百分比值 - private func formatPercentage(_ value: Int) -> String { - return value >= 0 ? "+\(value)%" : "\(value)%" - } - - /// 生成 SSML 文本 - /// - Parameter text: 要转换的文本 - /// - Returns: SSML 格式的文本 - private func generateSsml(text: String) -> String { - return """ - - - - \(text) - - - - """ - } - - /// 合成文本为语音并播放 - /// - Parameter text: 要合成的文本 - /// - Returns: 是否成功开始合成 - func speakText(_ text: String) -> Bool { - if !initialized { - print("\(tag): TTS 引擎尚未初始化") - return false + // 已经在播放,先停止 + if speaking { + _ = stop() } + // 重置状态 + speaking = true + + // 生成SSML + let ssml = generateSsml(text) + do { - print("\(tag): 开始合成文本: \(text)") - - // 生成 SSML - let ssml = generateSsml(text: text) - - // 标记为正在播放 - speaking = true - - // 异步合成语音 - try synthesizer?.speakSsmlAsync(ssml) - + // 创建合成任务 + try synthesizer?.startSpeakingSsml(ssml) return true } catch { - print("\(tag): 语音合成异常: \(error.localizedDescription)") + print("\(tag): 语音合成失败: \(error.localizedDescription)") + notifyEvent(eventType: .error, params: [ + "errorCode": "SYNTHESIS_FAILED", + "errorMessage": error.localizedDescription + ]) speaking = false return false } } - /// 处理流式文本并在遇到标点符号时播放 - /// - Parameter text: 收到的文本流片段 - /// - Returns: 是否成功处理 + /** + * 流式合成文本 + * + * @param text 要合成的文本片段 + * @return 是否成功处理 + */ func speakStream(_ text: String) -> Bool { - if !initialized || text.isEmpty { + if !isInitialized || text.isEmpty { + if !isInitialized { + notifyEvent(eventType: .error, params: [ + "errorCode": "NOT_INITIALIZED", + "errorMessage": "TTS引擎未初始化" + ]) + } return false } @@ -319,7 +179,7 @@ class AzureTtsHelper: NSObject { // 增加500ms防抖逻辑 let currentTime = Date().timeIntervalSince1970 - if currentTime - lastSpeakTime < 0.5 { + if currentTime - lastSpeakTime < 0.6 { return true } lastSpeakTime = currentTime @@ -327,13 +187,13 @@ class AzureTtsHelper: NSObject { let currentText = streamBuffer // 定义标点符号列表 - let punctuationMarks: [Character] = [".", "。", "!", "!", "?", "?", ";", ";", ",", ",", ":", ":"] + let punctuationMarks: [Character] = [".", "。", "!", "!", "?", "?", ";", ";", ",", ",", ":", ":", "\n"] // 查找最后一个标点符号的位置 var lastPunctuationIndex = -1 - for i in currentText.indices.reversed() { - if punctuationMarks.contains(currentText[i]) { - lastPunctuationIndex = currentText.distance(from: currentText.startIndex, to: i) + for (i, char) in currentText.enumerated().reversed() { + if punctuationMarks.contains(char) { + lastPunctuationIndex = i break } } @@ -344,24 +204,36 @@ class AzureTtsHelper: NSObject { let textToSpeak = String(currentText.prefix(lastPunctuationIndex + 1)) // 剩余的文本保存在缓冲区中 - streamBuffer = String(currentText.dropFirst(lastPunctuationIndex + 1)) + let startIndex = currentText.index(currentText.startIndex, offsetBy: lastPunctuationIndex + 1) + streamBuffer = String(currentText[startIndex...]) // 播放提取的文本 - return speakText(textToSpeak) + return speakOnce(textToSpeak) } // 如果没有找到标点符号,则等待更多文本 return true } catch { print("\(tag): 流式语音合成失败: \(error.localizedDescription)") + notifyEvent(eventType: .error, params: [ + "errorCode": "STREAM_FAILED", + "errorMessage": "流式语音合成失败: \(error.localizedDescription)" + ]) return false } } - /// 播放剩余的流式文本 - /// - Returns: 是否成功播放剩余文本 + /** + * 刷新并播放流式文本缓冲区中的剩余内容 + * + * @return 是否成功处理 + */ func flushStream() -> Bool { - if !initialized { + if !isInitialized { + notifyEvent(eventType: .error, params: [ + "errorCode": "NOT_INITIALIZED", + "errorMessage": "TTS引擎未初始化" + ]) return false } @@ -378,69 +250,248 @@ class AzureTtsHelper: NSObject { } // 播放剩余文本 - return speakText(remainingText) + return speakOnce(remainingText) } catch { print("\(tag): 刷新流式文本失败: \(error.localizedDescription)") + notifyEvent(eventType: .error, params: [ + "errorCode": "FLUSH_FAILED", + "errorMessage": "刷新流式文本失败: \(error.localizedDescription)" + ]) return false } } - /// 停止当前语音合成 - /// - Returns: 是否停止成功 - func stopSpeaking() -> Bool { - if !initialized { - print("\(tag): TTS 引擎尚未初始化") - return false + /** + * 停止语音合成和播放 + * + * @return 是否成功停止 + */ + func stop() -> Bool { + if !speaking || synthesizer == nil { + return true } do { - // 清空流缓冲区 - streamBuffer = "" - - try synthesizer?.stopSpeakingAsync() + // 停止合成 + try synthesizer?.stopSpeaking() speaking = false - print("\(tag): 已停止语音合成") return true } catch { print("\(tag): 停止语音合成失败: \(error.localizedDescription)") + notifyEvent(eventType: .error, params: [ + "errorCode": "STOP_FAILED", + "errorMessage": error.localizedDescription + ]) return false } } - /// 释放资源 + /** + * 释放资源 + * 在不再需要服务时调用,释放底层资源 + */ func dispose() { do { - stopSpeaking() + // 停止播放 + _ = stop() - // 恢复音频会话 - try audioSession.setActive(false, options: .notifyOthersOnDeactivation) + // 释放合成器 + synthesizer = nil - // 关闭自定义音频输出流 - customAudioOutputStream = nil + // 释放配置 + speechConfig = nil // 清空流缓冲区 streamBuffer = "" - // 清理资源 - synthesizer = nil - speechConfig = nil - audioConfig = nil - ttsCallback = nil - - initialized = false + // 重置状态 + isInitialized = false speaking = false - print("\(tag): TTS 引擎已释放") } catch { - print("\(tag): 释放 TTS 引擎失败: \(error.localizedDescription)") - // 确保重置状态 - initialized = false + // 即使发生异常,也确保重置状态 + isInitialized = false speaking = false } } - /// 检查当前是否正在播放语音 - /// - Returns: 是否正在播放语音 + /** + * 添加TTS事件监听器 + * + * @param listener 事件监听器 + */ + func addListener(_ listener: TtsEventListener) { + eventListeners.add(listener as AnyObject) + } + + /** + * 移除TTS事件监听器 + * + * @param listener 要移除的事件监听器 + */ + func removeListener(_ listener: TtsEventListener) { + eventListeners.remove(listener as AnyObject) + } + + /** + * 添加音频数据监听器 + * 由于不再支持自定义音频流,此方法实际上不再有效 + * + * @param listener 音频数据监听器 + */ + func addAudioDataListener(_ listener: AudioDataListener) { + print("\(tag): 警告:不支持音频数据监听器功能") + } + + /** + * 移除音频数据监听器 + * 由于不再支持自定义音频流,此方法实际上不再有效 + * + * @param listener 要移除的音频数据监听器 + */ + func removeAudioDataListener(_ listener: AudioDataListener) { + // 不做任何操作 + } + + /** + * 当前是否正在播放/合成 + */ func isSpeaking() -> Bool { return speaking } -} \ No newline at end of file + + // MARK: - 辅助方法 + + /** + * 设置事件监听器 + */ + private func setupEventListeners() { + guard let synthesizer = synthesizer else { return } + + // 添加合成开始事件处理器 + synthesizer.addSynthesisStartedEventHandler { [weak self] _, _ in + guard let self = self else { return } + self.notifyEvent(eventType: .synthesisStarted) + } + + // 添加合成中事件处理器 + synthesizer.addSynthesizingEventHandler { [weak self] _, _ in + // 可以在这里处理合成中的事件,目前没有特别操作 + } + + // 添加合成完成事件处理器 + synthesizer.addSynthesisCompletedEventHandler { [weak self] _, e in + guard let self = self else { return } + self.speaking = false + self.notifyEvent(eventType: .synthesisCompleted) + } + + // 添加合成取消事件处理器 + synthesizer.addSynthesisCanceledEventHandler { [weak self] _, e in + guard let self = self else { return } + + print("\(self.tag): 语音合成取消") + self.speaking = false + + var params: [String: Any] = [:] + do { + let cancellationDetails = try SPXSpeechSynthesisCancellationDetails(fromCanceledSynthesisResult: e.result) + if cancellationDetails.reason == SPXCancellationReason.error { + params["reason"] = String(describing: cancellationDetails.reason.rawValue) + params["errorDetails"] = cancellationDetails.errorDetails ?? "未知错误" + } + } catch { + params["errorDetails"] = "获取取消详情失败: \(error.localizedDescription)" + } + + self.notifyEvent(eventType: .synthesisCanceled, params: params) + } + } + + /** + * 触发事件通知 + */ + private func notifyEvent(eventType: TtsEventType, params: [String: Any] = [:]) { + let event = TtsEvent(type: eventType, params: params) + + // 在主线程通知事件 + DispatchQueue.main.async { + for case let listener as TtsEventListener in self.eventListeners.allObjects { + listener.onEvent(event) + } + } + } + + /** + * 重新创建合成器 + */ + private func recreateSynthesizer() { + do { + // 使用默认音频输出配置创建合成器 + synthesizer = try SPXSpeechSynthesizer(speechConfig!) + + // 设置事件监听 + setupEventListeners() + } catch { + print("\(tag): 重新创建合成器失败: \(error.localizedDescription)") + notifyEvent(eventType: .error, params: [ + "errorCode": "RECREATE_FAILED", + "errorMessage": "重新创建合成器失败: \(error.localizedDescription)" + ]) + } + } + + /** + * 设置语音参数 + */ + private func setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100) -> Bool { + if !isInitialized { return false } + + do { + currentRate = formatPercentage(rate) + currentPitch = formatPercentage(pitch) + currentVolume = "\(min(max(volume, 0), 100))%" + return true + } catch { + print("\(tag): 设置语音参数失败: \(error.localizedDescription)") + notifyEvent(eventType: .error, params: [ + "errorCode": "PARAMS_SET_FAILED", + "errorMessage": "设置语音参数失败: \(error.localizedDescription)" + ]) + return false + } + } + + /** + * 格式化百分比值 + */ + private func formatPercentage(_ value: Int) -> String { + return value >= 0 ? "+\(value)%" : "\(value)%" + } + + /** + * 生成SSML + */ + private func generateSsml(_ rawText: String) -> String { + // 转义 XML 保留字符 + var escapedText = rawText + .replacingOccurrences(of: "&", with: "&") + .replacingOccurrences(of: "<", with: "<") + .replacingOccurrences(of: ">", with: ">") + + // 构造简化的SSML文档 + let ssml = """ + + + + \(escapedText) + + + + """ + + return ssml + } +} diff --git a/local_plugins/azure_speech/ios/Classes/azure_speech-Bridging-Header.h b/local_plugins/azure_speech/ios/Classes/azure_speech-Bridging-Header.h new file mode 100644 index 000000000..1d4245b2b --- /dev/null +++ b/local_plugins/azure_speech/ios/Classes/azure_speech-Bridging-Header.h @@ -0,0 +1,6 @@ +#ifndef azure_speech_Bridging_Header_h +#define azure_speech_Bridging_Header_h + +#import + +#endif /* azure_speech_Bridging_Header_h */ \ No newline at end of file diff --git a/local_plugins/azure_speech/ios/Classes/azure_speech.h b/local_plugins/azure_speech/ios/Classes/azure_speech.h new file mode 100644 index 000000000..7cb0fd7ab --- /dev/null +++ b/local_plugins/azure_speech/ios/Classes/azure_speech.h @@ -0,0 +1,5 @@ +#import + +#import "AzureSpeechPlugin.h" + +// 添加其他公共头文件导出 \ No newline at end of file diff --git a/local_plugins/azure_speech/ios/Flutter/AppFrameworkInfo.plist b/local_plugins/azure_speech/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 7c5696400..000000000 --- a/local_plugins/azure_speech/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 12.0 - - diff --git a/local_plugins/azure_speech/ios/Flutter/Debug.xcconfig b/local_plugins/azure_speech/ios/Flutter/Debug.xcconfig deleted file mode 100644 index 592ceee85..000000000 --- a/local_plugins/azure_speech/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/local_plugins/azure_speech/ios/Flutter/Release.xcconfig b/local_plugins/azure_speech/ios/Flutter/Release.xcconfig deleted file mode 100644 index 592ceee85..000000000 --- a/local_plugins/azure_speech/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/local_plugins/azure_speech/ios/Runner.xcodeproj/project.pbxproj b/local_plugins/azure_speech/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 8b82c852b..000000000 --- a/local_plugins/azure_speech/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,619 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 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 = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C8082294A63A400263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C807B294A618700263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 331C8082294A63A400263BE5 /* RunnerTests */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - 331C8081294A63A400263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C8080294A63A400263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C807D294A63A400263BE5 /* Sources */, - 331C807F294A63A400263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C8086294A63A400263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C8080294A63A400263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 1100; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - 331C8080294A63A400263BE5 /* RunnerTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C807F294A63A400263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - 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"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C807D294A63A400263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = PNNU77KTCP; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 331C8088294A63A400263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Debug; - }; - 331C8089294A63A400263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Release; - }; - 331C808A294A63A400263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = PNNU77KTCP; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = PNNU77KTCP; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C8088294A63A400263BE5 /* Debug */, - 331C8089294A63A400263BE5 /* Release */, - 331C808A294A63A400263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a62..000000000 --- a/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5e..000000000 --- a/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/local_plugins/azure_speech/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/local_plugins/azure_speech/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 15cada483..000000000 --- a/local_plugins/azure_speech/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/local_plugins/azure_speech/ios/Runner.xcworkspace/contents.xcworkspacedata b/local_plugins/azure_speech/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16e..000000000 --- a/local_plugins/azure_speech/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5e..000000000 --- a/local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/local_plugins/azure_speech/ios/Runner/AppDelegate.swift b/local_plugins/azure_speech/ios/Runner/AppDelegate.swift deleted file mode 100644 index 626664468..000000000 --- a/local_plugins/azure_speech/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Flutter -import UIKit - -@main -@objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } -} diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fab2..000000000 --- a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada472..000000000 Binary files a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 7353c41ec..000000000 Binary files a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 797d452e4..000000000 Binary files a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index 6ed2d933e..000000000 Binary files a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cd7b0099..000000000 Binary files a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index fe730945a..000000000 Binary files a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index 321773cd8..000000000 Binary files a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 797d452e4..000000000 Binary files a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index 502f463a9..000000000 Binary files a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index 0ec303439..000000000 Binary files a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index 0ec303439..000000000 Binary files a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index e9f5fea27..000000000 Binary files a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index 84ac32ae7..000000000 Binary files a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 8953cba09..000000000 Binary files a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index 0467bf12a..000000000 Binary files a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2fd..000000000 --- a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19eaca..000000000 Binary files a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19eaca..000000000 Binary files a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19eaca..000000000 Binary files a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725b7..000000000 --- a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/local_plugins/azure_speech/ios/Runner/Base.lproj/LaunchScreen.storyboard b/local_plugins/azure_speech/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c7c..000000000 --- a/local_plugins/azure_speech/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/local_plugins/azure_speech/ios/Runner/Base.lproj/Main.storyboard b/local_plugins/azure_speech/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516f..000000000 --- a/local_plugins/azure_speech/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/local_plugins/azure_speech/ios/Runner/Info.plist b/local_plugins/azure_speech/ios/Runner/Info.plist deleted file mode 100644 index 1d54d7cd7..000000000 --- a/local_plugins/azure_speech/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Azure Speech - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - azure_speech - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - diff --git a/local_plugins/azure_speech/ios/Runner/Runner-Bridging-Header.h b/local_plugins/azure_speech/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 308a2a560..000000000 --- a/local_plugins/azure_speech/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" diff --git a/local_plugins/azure_speech/ios/RunnerTests/RunnerTests.swift b/local_plugins/azure_speech/ios/RunnerTests/RunnerTests.swift deleted file mode 100644 index 86a7c3b1b..000000000 --- a/local_plugins/azure_speech/ios/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Flutter -import UIKit -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/local_plugins/azure_speech/ios/azure_speech.podspec b/local_plugins/azure_speech/ios/azure_speech.podspec index b6eed7b01..7b6cf275e 100644 --- a/local_plugins/azure_speech/ios/azure_speech.podspec +++ b/local_plugins/azure_speech/ios/azure_speech.podspec @@ -5,19 +5,20 @@ Pod::Spec.new do |s| s.name = 'azure_speech' s.version = '0.0.1' - s.summary = 'Azure Speech plugin' + s.summary = 'Azure Speech SDK Flutter plugin' s.description = <<-DESC -A Flutter plugin for Azure Speech services. +A Flutter plugin for Azure Speech Services providing speech recognition and text-to-speech capabilities. DESC - s.homepage = 'http://example.com' + s.homepage = 'https://github.com/yunqiinnovation/azure_speech' s.license = { :file => '../LICENSE' } s.author = { 'Your Company' => 'email@example.com' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.dependency 'MicrosoftCognitiveServicesSpeech-iOS' - s.platform = :ios, '13.0' - + s.dependency 'speech' + s.dependency 'MicrosoftCognitiveServicesSpeech-iOS', '~> 1.30.0' + s.platform = :ios, '11.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' diff --git a/local_plugins/azure_speech/lib/main.dart b/local_plugins/azure_speech/lib/main.dart deleted file mode 100644 index a7256585a..000000000 --- a/local_plugins/azure_speech/lib/main.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; - -void main() { - runApp(const MainApp()); -} - -class MainApp extends StatelessWidget { - const MainApp({super.key}); - - @override - Widget build(BuildContext context) { - return const MaterialApp( - home: Scaffold( - body: Center( - child: Text('Hello World!'), - ), - ), - ); - } -} diff --git a/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift b/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift index b4970c3ce..67ccf3772 100644 --- a/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift +++ b/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift @@ -22,8 +22,13 @@ class BleAccessorySetup: NSObject { class AccessorySessionWrapper { var session: ASAccessorySession - init() { - self.session = ASAccessorySession() + init() throws { + do { + self.session = try ASAccessorySession() + } catch { + print("ASAccessorySession 初始化错误: \(error.localizedDescription)") + throw error + } } } @@ -57,58 +62,66 @@ class BleAccessorySetup: NSObject { if #available(iOS 18.0, *) { os_log("初始化iOS 18 AccessorySetupKit会话", log: logger, type: .info) - // 创建会话 - accessorySessionWrapper = AccessorySessionWrapper() + // 检查Info.plist配置 + let plistCheck = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupKitSupports") + os_log("Info.plist NSAccessorySetupKitSupports: %{public}@", log: logger, type: .info, String(describing: plistCheck)) - // 激活会话并设置事件处理 - if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper { - wrapper.session.activate(on: .main) { [weak self] event in - guard let self = self else { return } - - switch event.eventType { - case .activated: - os_log("AccessorySetupKit会话已激活", log: self.logger, type: .info) - - case .accessoryAdded: - if let accessory = event.accessory, let uuid = accessory.bluetoothIdentifier { - os_log("发现新设备: %{public}@", log: self.logger, type: .info, accessory.displayName) - self.onDeviceDiscovered?(uuid) - } - - case .accessoryRemoved: - if let accessory = event.accessory { - os_log("设备已移除: %{public}@", log: self.logger, type: .info, accessory.displayName) - } - - // 根据WWDC24视频,正确的事件类型为accessoryChanged - case .accessoryChanged: - if let accessory = event.accessory { - os_log("设备已更新: %{public}@", log: self.logger, type: .info, accessory.displayName) - } + // 创建会话 (使用try-catch) + do { + accessorySessionWrapper = try AccessorySessionWrapper() + + // 激活会话并设置事件处理 + if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper { + wrapper.session.activate(on: .main) { [weak self] event in + guard let self = self else { return } - // 根据WWDC24视频,失败事件类型已变更 - case .pickerSetupFailed: - if let error = event.error { - os_log("会话出错: %{public}@", log: self.logger, type: .error, error.localizedDescription) + switch event.eventType { + case .activated: + os_log("AccessorySetupKit会话已激活", log: self.logger, type: .info) + + case .accessoryAdded: + if let accessory = event.accessory, let uuid = accessory.bluetoothIdentifier { + os_log("发现新设备: %{public}@", log: self.logger, type: .info, accessory.displayName) + self.onDeviceDiscovered?(uuid) + } + + case .accessoryRemoved: + if let accessory = event.accessory { + os_log("设备已移除: %{public}@", log: self.logger, type: .info, accessory.displayName) + } + + // 根据WWDC24视频,正确的事件类型为accessoryChanged + case .accessoryChanged: + if let accessory = event.accessory { + os_log("设备已更新: %{public}@", log: self.logger, type: .info, accessory.displayName) + } + + // 根据WWDC24视频,失败事件类型已变更 + case .pickerSetupFailed: + if let error = event.error { + os_log("会话出错: %{public}@", log: self.logger, type: .error, error.localizedDescription) + } + + default: + os_log("收到其他会话事件: %{public}d", log: self.logger, type: .debug, event.eventType.rawValue) } - - default: - os_log("收到其他会话事件: %{public}d", log: self.logger, type: .debug, event.eventType.rawValue) } - } - - // 检查现有授权设备 - if !wrapper.session.accessories.isEmpty { - os_log("发现%{public}d个已授权设备", log: logger, type: .info, wrapper.session.accessories.count) - // 如果有现有设备,触发回调 - for accessory in wrapper.session.accessories { - if let uuid = accessory.bluetoothIdentifier { - os_log("处理已授权设备: %{public}@", log: logger, type: .info, accessory.displayName) - self.onDeviceDiscovered?(uuid) + // 检查现有授权设备 + if !wrapper.session.accessories.isEmpty { + os_log("发现%{public}d个已授权设备", log: logger, type: .info, wrapper.session.accessories.count) + + // 如果有现有设备,触发回调 + for accessory in wrapper.session.accessories { + if let uuid = accessory.bluetoothIdentifier { + os_log("处理已授权设备: %{public}@", log: logger, type: .info, accessory.displayName) + self.onDeviceDiscovered?(uuid) + } } } } + } catch { + os_log("AccessorySetupKit初始化错误: %{public}@", log: logger, type: .error, error.localizedDescription) } } else { os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info) @@ -132,7 +145,7 @@ class BleAccessorySetup: NSObject { let descriptor = ASDiscoveryDescriptor() // 设置过滤条件 - // 使用ASBluetoothCompanyIdentifier类型 + // 使用UInt16格式的公司标识符 descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(UInt16(0xABEF)) // BleConst.MANUFACTURER_ID descriptor.bluetoothServiceUUID = CBUUID(string: "0000abc0-0000-1111-2222-123456789abc") // BleConst.PRIMARY_SERVICE_UUID diff --git a/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift b/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift index 388e3bd92..457118fea 100644 --- a/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift +++ b/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift @@ -18,7 +18,7 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { // 设置BLE服务回调 BleService.shared.setDelegate(self) - // 初始化AccessorySetup + // 初始化AccessorySetup - iOS 18相关代码 BleAccessorySetup.shared.setBleService(BleService.shared) BleAccessorySetup.shared.setDeviceDiscoveredCallback { [weak self] uuid in self?.handleDiscoveredDevice(uuid: uuid) @@ -56,90 +56,32 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { } case "isCompanionSupported": - // 检查设备是否支持iOS 18的AccessorySetupKit功能 - #if canImport(AccessorySetupKit) + // iOS 18+ 支持 AccessorySetupKit + #if canImport(AccessorySetupKit) && swift(>=5.9) if #available(iOS 18.0, *) { - // 设备运行iOS 18+,支持AccessorySetupKit result(true) } else { - // 设备运行iOS 17或更低版本 result(false) } #else - // 编译时没有AccessorySetupKit框架 result(false) #endif case "getPairedMacAddress": - // 获取已配对设备的标识符 - #if canImport(AccessorySetupKit) - if #available(iOS 18.0, *) { - if let wrapper = BleAccessorySetup.shared.accessorySessionWrapper as? BleAccessorySetup.AccessorySessionWrapper { - // 检查是否有配对设备 - if let firstAccessory = wrapper.session.accessories.first, - let btId = firstAccessory.bluetoothIdentifier { - // 在iOS中,我们返回UUID字符串作为"MAC地址"的等价物 - result(btId.uuidString) - } else { - // 无配对设备 - result(nil) - } - } else { - // 会话未初始化 - result(nil) - } - } else { - // iOS版本太低 - result(nil) - } - #else - // 编译环境不支持 + // iOS上禁用AccessorySetupKit,始终返回无配对设备 result(nil) - #endif case "registerBackgroundScan": - // iOS通过AccessorySetupKit自动处理后台扫描,不需要单独注册 - #if canImport(AccessorySetupKit) - if #available(iOS 18.0, *) { - // 在iOS 18+上,不需要额外操作 - result(true) - } else { - // iOS版本太低 - result(false) - } - #else - result(false) - #endif + // iOS上禁用,直接返回成功以避免Flutter端报错 + result(true) case "unregisterBackgroundScan": - // iOS通过AccessorySetupKit自动处理后台扫描,不需要单独注销 + // iOS上禁用,直接返回成功以避免Flutter端报错 result(true) case "clearAssociations": - // 清除设备关联 - #if canImport(AccessorySetupKit) - if #available(iOS 18.0, *) { - if let wrapper = BleAccessorySetup.shared.accessorySessionWrapper as? BleAccessorySetup.AccessorySessionWrapper { - // 遍历所有配对设备 - for accessory in wrapper.session.accessories { - if let btId = accessory.bluetoothIdentifier { - // 移除设备的系统处理已经集成在设置中,这里什么都不做 - os_log("设备需要在系统设置中手动移除: %{public}@", - log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), - type: .info, - btId.uuidString) - } - } - } - // 返回成功 - result(true) - } else { - // iOS版本太低 - result(false) - } - #else - result(false) - #endif + // iOS上禁用,直接返回成功以避免Flutter端报错 + result(true) case "getDeviceVersion": result(BleService.shared.getDeviceVersion()) @@ -167,19 +109,22 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { // iOS 18+ AccessorySetupKit相关方法 case "startDeviceAssociation": - #if canImport(AccessorySetupKit) + #if canImport(AccessorySetupKit) && swift(>=5.9) if #available(iOS 18.0, *) { + // 调用AccessorySetupKit的设备选择器 BleAccessorySetup.shared.showDevicePicker() result(true) } else { - result(FlutterError(code: "UNAVAILABLE", - message: "AccessorySetupKit需要iOS 18或更高版本", - details: nil)) + os_log("iOS版本低于18.0,不支持AccessorySetupKit", + log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), + type: .info) + result(true) } #else - result(FlutterError(code: "UNAVAILABLE", - message: "编译环境不支持AccessorySetupKit", - details: nil)) + os_log("环境不支持AccessorySetupKit,模拟成功以继续流程", + log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), + type: .info) + result(true) #endif default: @@ -188,7 +133,9 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { } // MARK: - 私有方法 + // AccessorySetupKit发现设备的处理 private func handleDiscoveredDevice(uuid: UUID) { + // 通过AccessorySetupKit处理发现的设备 BleAccessorySetup.shared.handleDiscoveredAccessory(uuid: uuid) } diff --git a/local_plugins/ble_service/ios/ble_service.podspec b/local_plugins/ble_service/ios/ble_service.podspec index cad3ddffb..2fc4d9c42 100644 --- a/local_plugins/ble_service/ios/ble_service.podspec +++ b/local_plugins/ble_service/ios/ble_service.podspec @@ -13,9 +13,6 @@ BLE服务插件,提供蓝牙扫描、连接和通信功能,支持iOS 18+ Acc s.author = { 'Your Company' => 'email@example.com' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' - s.info_plist = { - 'NSAccessorySetupKitSupports' => ['Bluetooth'] - } s.dependency 'Flutter' s.platform = :ios, '18.0' diff --git a/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift b/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift index f9a9c782c..c9f8f0cb9 100644 --- a/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift +++ b/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift @@ -24,47 +24,55 @@ class ClassicBluetoothHelper: NSObject { // 状态监控相关 private var isMonitoring = false private var stateCallback: ((BluetoothEventType, [String: Any]) -> Void)? - + // 用于监控蓝牙系统状态,延迟初始化 private var centralManager: CBCentralManager? - + // 音频路由变化观察者 private var routeChangeObserver: NSObjectProtocol? - + override init() { super.init() - // 初始化时不立即创建centralManager,延迟到实际需要时 + initializeCentralManagerIfNeeded() } - + deinit { stopBluetoothStateMonitoring() } - + /// 延迟初始化CBCentralManager,只在实际需要时创建 private func initializeCentralManagerIfNeeded() { if centralManager == nil { + // 移除 ShowPowerAlertKey 选项,避免不必要的系统弹窗 centralManager = CBCentralManager( delegate: self, - queue: DispatchQueue.main, - options: [CBCentralManagerOptionShowPowerAlertKey: true] + queue: DispatchQueue.main ) } } - + /// 检查蓝牙是否启用,此方法会触发权限请求 func isBluetoothEnabled() -> Bool { // 此方法调用会触发权限申请 initializeCentralManagerIfNeeded() - + guard let central = centralManager else { print("检查蓝牙状态失败:centralManager未初始化") return false } - + + // 如果状态是 .unknown,说明蓝牙管理器刚初始化,尚未收到系统回调 + // 此时应该返回一个中性值,避免错误地显示"蓝牙已关闭" + if central.state == .unknown || central.state == .resetting { + print("蓝牙状态检查结果: 初始化中,等待系统回调") + // 返回 true 避免误报蓝牙关闭 + return true + } + // 检查蓝牙状态并记录 let stateDescription: String let isEnabled: Bool - + switch central.state { case .poweredOn: stateDescription = "已开启" @@ -88,19 +96,19 @@ class ClassicBluetoothHelper: NSObject { stateDescription = "未定义状态" isEnabled = false } - + print("蓝牙状态检查结果: \(stateDescription), isEnabled: \(isEnabled)") return isEnabled } - + /// 获取已连接的蓝牙耳机,此方法会触发权限请求 func getConnectedDevices(completion: @escaping ([[String: String]], Error?) -> Void) { // 确保蓝牙已初始化 initializeCentralManagerIfNeeded() - + // 获取音频设备(主要是蓝牙耳机) let audioDevices = getConnectedAudioDevices() - + // 日志输出 if !audioDevices.isEmpty { print("已找到\(audioDevices.count)个蓝牙音频设备:") @@ -110,17 +118,17 @@ class ClassicBluetoothHelper: NSObject { } else { print("未找到蓝牙音频设备") } - + completion(audioDevices, nil) } - + // 获取连接的蓝牙音频设备 private func getConnectedAudioDevices() -> [[String: String]] { var result: [[String: String]] = [] - + // 获取当前音频路由 let outputs = AVAudioSession.sharedInstance().currentRoute.outputs - + // 过滤蓝牙相关设备 for output in outputs where isBluetoothOutput(output.portType) { let device: [String: String] = [ @@ -129,15 +137,15 @@ class ClassicBluetoothHelper: NSObject { ] result.append(device) } - + return result } - + // 检查是否为蓝牙输出 private func isBluetoothOutput(_ portType: AVAudioSession.Port) -> Bool { return portType == .bluetoothA2DP || portType == .bluetoothHFP || portType == .bluetoothLE } - + // 清理设备地址 private func cleanDeviceAddress(_ address: String) -> String { // 移除类似 "-tacl" 的后缀 @@ -146,17 +154,17 @@ class ClassicBluetoothHelper: NSObject { } return address } - + /// 开始监听蓝牙状态变化 func startBluetoothStateMonitoring(callback: @escaping (BluetoothEventType, [String: Any]) -> Void) { self.stateCallback = callback - + if !isMonitoring { isMonitoring = true - + // 初始化蓝牙管理器 initializeCentralManagerIfNeeded() - + // 观察音频路由变化 let notificationCenter = NotificationCenter.default routeChangeObserver = notificationCenter.addObserver( @@ -166,12 +174,12 @@ class ClassicBluetoothHelper: NSObject { ) { [weak self] notification in self?.handleAudioRouteChange(notification) } - + // 激活音频会话 activateAudioSession() } } - + // 处理音频路由变化 private func handleAudioRouteChange(_ notification: Notification) { guard let userInfo = notification.userInfo, @@ -179,7 +187,7 @@ class ClassicBluetoothHelper: NSObject { let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return } - + switch reason { case .newDeviceAvailable: // 新音频设备已连接 @@ -187,7 +195,7 @@ class ClassicBluetoothHelper: NSObject { for device in audioDevices { stateCallback?(.deviceConnected, ["device": device]) } - + case .oldDeviceUnavailable: // 获取断开之前的路由 if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription { @@ -199,12 +207,12 @@ class ClassicBluetoothHelper: NSObject { stateCallback?(.deviceDisconnected, ["device": device]) } } - + default: break } } - + // 激活音频会话 private func activateAudioSession() { do { @@ -215,26 +223,26 @@ class ClassicBluetoothHelper: NSObject { print("设置音频会话失败: \(error.localizedDescription)") } } - + /// 停止监听蓝牙状态变化 func stopBluetoothStateMonitoring() { if isMonitoring { isMonitoring = false - + // 移除音频路由观察者 if let observer = routeChangeObserver { NotificationCenter.default.removeObserver(observer) routeChangeObserver = nil } - + // 关闭音频会话 deactivateAudioSession() } - + // 清除回调 stateCallback = nil } - + // 关闭音频会话 private func deactivateAudioSession() { do { @@ -243,25 +251,38 @@ class ClassicBluetoothHelper: NSObject { print("关闭音频会话失败: \(error.localizedDescription)") } } - + /// 报告当前蓝牙状态 func reportCurrentBluetoothState() { // 如果centralManager未初始化,不触发状态报告 - guard let centralManager = centralManager else { return } - + guard let central = centralManager else { return } + + // 如果状态是 .unknown 或 .resetting,暂不向 Flutter 端报告 + if central.state == .unknown || central.state == .resetting { + print("蓝牙状态: 临时状态(.unknown/.resetting),暂不报告") + return + } + // 直接调用delegate方法保持状态处理一致性 - centralManagerDidUpdateState(centralManager) + centralManagerDidUpdateState(central) } } // MARK: - CBCentralManagerDelegate extension ClassicBluetoothHelper: CBCentralManagerDelegate { func centralManagerDidUpdateState(_ central: CBCentralManager) { - let state: BluetoothState - // 添加详细日志输出 print("iOS蓝牙状态原始值: \(central.state.rawValue)") - + + // 如果状态是 .unknown 或 .resetting,暂不向 Flutter 端报告 + // 这些是临时状态,等待系统确定真正的状态后再报告 + guard central.state != .unknown && central.state != .resetting else { + print("蓝牙状态: 临时状态(.unknown/.resetting),暂不报告") + return + } + + let state: BluetoothState + switch central.state { case .poweredOn: print("蓝牙状态: 已开启(.poweredOn)") @@ -285,9 +306,9 @@ extension ClassicBluetoothHelper: CBCentralManagerDelegate { print("蓝牙状态: 未处理的新状态(\(central.state.rawValue))") state = .unknown } - + print("向Flutter端报告的蓝牙状态: \(state.rawValue)") - + // 仅发送兼容的状态(为保持向后兼容,只发送on/off/unknown) let reportState: String switch state { @@ -299,7 +320,7 @@ extension ClassicBluetoothHelper: CBCentralManagerDelegate { default: reportState = BluetoothState.unknown.rawValue } - + stateCallback?(.stateChanged, ["state": reportState]) } -} \ No newline at end of file +} \ No newline at end of file diff --git a/local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift b/local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift index c3755d0d7..9257d4c16 100644 --- a/local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift +++ b/local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift @@ -41,11 +41,11 @@ public class SwiftClassicBluetoothPlugin: NSObject, FlutterPlugin { private func getConnectedDevices(result: @escaping FlutterResult) { // 先检查蓝牙状态 - if !bluetoothHelper.isBluetoothEnabled() { - print("蓝牙未启用,无法获取设备") - result([]) - return - } + // if !bluetoothHelper.isBluetoothEnabled() { + // print("蓝牙未启用,无法获取设备") + // result([]) + // return + // } bluetoothHelper.getConnectedDevices { devices, error in if let error = error { diff --git a/local_plugins/classic_bluetooth/ios/classic_bluetooth.podspec b/local_plugins/classic_bluetooth/ios/classic_bluetooth.podspec index 0ed858671..79fea0113 100644 --- a/local_plugins/classic_bluetooth/ios/classic_bluetooth.podspec +++ b/local_plugins/classic_bluetooth/ios/classic_bluetooth.podspec @@ -13,8 +13,7 @@ Pod::Spec.new do |s| s.author = { 'YunQi Innovation' => 'support@yunqiinnovation.com' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' - - s.ios.deployment_target = '13.0' + s.ios.deployment_target = '18.0' s.dependency 'Flutter' s.swift_version = '5.0' diff --git a/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock index 3e3066aed..0bf0a4b49 100644 Binary files a/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties b/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties index 9139b185a..ee30fae9a 100644 --- a/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties +++ b/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties @@ -1,2 +1,2 @@ -#Sun May 18 21:39:38 IST 2025 -gradle.version=8.10 +#Tue May 20 17:39:11 IST 2025 +gradle.version=8.9 diff --git a/local_plugins/azure_speech/ios/Classes/.gitkeep b/local_plugins/speech/LICENSE similarity index 100% rename from local_plugins/azure_speech/ios/Classes/.gitkeep rename to local_plugins/speech/LICENSE diff --git a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/IAsrService.kt b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/IAsrService.kt index 94da1b435..a1fe4e196 100644 --- a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/IAsrService.kt +++ b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/IAsrService.kt @@ -14,7 +14,7 @@ interface IAsrService { * @param supportedLanguages 支持的语言列表,如["zh-CN", "en-US"] * @return 是否初始化成功 */ - suspend fun initialize( + fun initialize( subscriptionKey: String, region: String, supportedLanguages: List? = null @@ -27,7 +27,7 @@ interface IAsrService { * @param callback 识别结果回调 * @return 是否成功开始识别 */ - suspend fun recognizeOnce(audioData: ByteArray, callback: (String) -> Unit): Boolean + fun recognizeOnce(audioData: ByteArray, callback: (String) -> Unit): Boolean /** * 启动连续识别 @@ -35,14 +35,14 @@ interface IAsrService { * * @return 是否成功启动 */ - suspend fun startContinuousRecognition(): Boolean + fun startContinuousRecognition(): Boolean /** * 停止连续识别 * * @return 是否成功停止 */ - suspend fun stopContinuousRecognition(): Boolean + fun stopContinuousRecognition(): Boolean /** * 推送音频数据 @@ -51,7 +51,7 @@ interface IAsrService { * @param audioData 要识别的音频数据字节数组 * @return 是否成功处理 */ - suspend fun pushAudioData(audioData: ByteArray): Boolean + fun pushAudioData(audioData: ByteArray): Boolean /** * 添加ASR事件监听器 @@ -73,13 +73,6 @@ interface IAsrService { */ fun release() - /** - * 判断当前是否正在识别 - * - * @return 是否正在进行语音识别 - */ - fun isRecognizing(): Boolean - /** * 设置识别参数 * diff --git a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/SpeechPlugin.kt b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/SpeechPlugin.kt new file mode 100644 index 000000000..0240742f8 --- /dev/null +++ b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/SpeechPlugin.kt @@ -0,0 +1,33 @@ +package com.deep_voice.speech + +import androidx.annotation.NonNull +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +/** + * SpeechPlugin + * + * 语音服务接口定义插件 + * 注意:此插件仅提供接口定义,不提供具体实现 + * 实际的语音识别和合成功能由其他插件实现 + */ +class SpeechPlugin: FlutterPlugin, MethodCallHandler { + private lateinit var channel: MethodChannel + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "speech") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + // 此插件仅提供接口定义,不处理任何方法调用 + result.notImplemented() + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } +} \ No newline at end of file diff --git a/local_plugins/speech/ios/Classes/IAsrService.swift b/local_plugins/speech/ios/Classes/IAsrService.swift new file mode 100644 index 000000000..ea81981ee --- /dev/null +++ b/local_plugins/speech/ios/Classes/IAsrService.swift @@ -0,0 +1,132 @@ +import Foundation + +/** + * ASR事件类型枚举 + */ +@objc public enum AsrEventType: Int { + /** 会话开始 */ + case sessionStarted + + /** 会话结束 */ + case sessionStopped + + /** 识别中(中间结果) */ + case recognizing + + /** 识别完成(最终结果) */ + case recognized + + /** 识别取消 */ + case canceled + + /** 发生错误 */ + case error +} + +/** + * ASR事件数据类 + */ +@objc public class AsrEvent: NSObject { + public let type: AsrEventType + public let params: [String: Any] + + public init(type: AsrEventType, params: [String: Any] = [:]) { + self.type = type + self.params = params + } +} + +/** + * ASR事件监听协议 + */ +@objc public protocol AsrEventListener { + /** + * 当发生ASR事件时调用 + * + * @param event ASR事件 + */ + @objc(onAsrEvent:) + func onEvent(_ event: AsrEvent) +} + +/** + * ASR服务通用协议 + * + * 定义了语音识别服务的标准接口,可用于不同提供商的实现,如火山引擎、Azure等 + */ +@objc public protocol IAsrService { + /** + * 初始化语音识别服务 + * + * @param subscriptionKey 服务订阅密钥/访问令牌 + * @param region 服务区域/应用ID + * @param supportedLanguages 支持的语言列表,如["zh-CN", "en-US"] + * @return 是否初始化成功 + */ + func initialize( + subscriptionKey: String, + region: String, + supportedLanguages: [String]? + ) -> Bool + + /** + * 单次识别(非流式) + * + * @param audioData 要识别的音频数据字节数组 + * @param callback 识别结果回调 + * @return 是否成功开始识别 + */ + func recognizeOnce(audioData: Data, callback: @escaping (String) -> Void) -> Bool + + /** + * 启动连续识别 + * 开始持续监听音频输入并识别 + * + * @return 是否成功启动 + */ + func startContinuousRecognition() -> Bool + + /** + * 停止连续识别 + * + * @return 是否成功停止 + */ + func stopContinuousRecognition() -> Bool + + /** + * 推送音频数据 + * 在连续识别模式下推送新的音频数据 + * + * @param audioData 要识别的音频数据字节数组 + * @return 是否成功处理 + */ + func pushAudioData(audioData: Data) -> Bool + + /** + * 添加ASR事件监听器 + * + * @param listener 事件监听器 + */ + func addListener(_ listener: AsrEventListener) + + /** + * 移除ASR事件监听器 + * + * @param listener 要移除的事件监听器 + */ + func removeListener(_ listener: AsrEventListener) + + /** + * 释放资源 + * 在不再需要服务时调用,释放底层资源 + */ + func dispose() + + /** + * 设置识别参数 + * + * @param params 参数键值对 + * @return 是否设置成功 + */ + func setParameters(_ params: [String: Any]) -> Bool +} \ No newline at end of file diff --git a/local_plugins/speech/ios/Classes/ITtsService.swift b/local_plugins/speech/ios/Classes/ITtsService.swift new file mode 100644 index 000000000..3f784bc20 --- /dev/null +++ b/local_plugins/speech/ios/Classes/ITtsService.swift @@ -0,0 +1,147 @@ +import Foundation + +/** + * TTS事件类型枚举 + */ +@objc public enum TtsEventType: Int { + /** 开始合成 */ + case synthesisStarted + + /** 合成完成 */ + case synthesisCompleted + + /** 合成取消 */ + case synthesisCanceled + + /** 发生错误 */ + case error +} + +/** + * TTS事件数据类 + */ +@objc public class TtsEvent: NSObject { + public let type: TtsEventType + public let params: [String: Any] + + public init(type: TtsEventType, params: [String: Any] = [:]) { + self.type = type + self.params = params + } +} + +/** + * TTS事件监听协议 + */ +@objc public protocol TtsEventListener { + /** + * 当发生TTS事件时调用 + * + * @param event TTS事件 + */ + @objc(onTtsEvent:) + func onEvent(_ event: TtsEvent) +} + +/** + * 音频数据监听协议 + */ +@objc public protocol AudioDataListener { + /** + * 当接收到音频数据时调用 + * + * @param data 音频数据字节数组 + */ + func onAudioData(_ data: Data) +} + +/** + * TTS服务通用协议 + * + * 定义了语音合成服务的标准接口,可用于不同提供商的实现,如微软Azure、火山语音等 + */ +@objc public protocol ITtsService { + /** + * 初始化语音合成服务 + * + * @param ttsAppId 服务应用ID + * @param ttsAppToken 服务访问令牌/订阅密钥 + * @param ttsResource 服务资源ID/区域(可选) + * @param language 语言代码,如"zh-CN" + * @return 是否初始化成功 + */ + func initialize(ttsAppId: String, ttsAppToken: String, ttsResource: String, language: String) -> Bool + + /** + * 设置语音角色 + * + * @param voiceName 语音角色名称(不同服务的语音角色命名可能不同) + * @return 是否设置成功 + */ + func setVoice(_ voiceName: String) -> Bool + + /** + * 单次播放文本(非流式) + * + * @param text 要合成的文本 + * @return 是否成功开始合成 + */ + func speakOnce(_ text: String) -> Bool + + /** + * 流式合成 + * 在流式场景下合成并播放文本,适用于连续的文本输入 + * + * @param text 要合成的文本片段 + * @return 是否成功处理 + */ + func speakStream(_ text: String) -> Bool + + /** + * 刷新并播放流式文本缓冲区中的剩余内容 + * + * @return 是否成功处理 + */ + func flushStream() -> Bool + + /** + * 停止当前合成和播放 + * + * @return 是否成功停止 + */ + func stop() -> Bool + + /** + * 释放资源 + * 在不再需要服务时调用,释放底层资源 + */ + func dispose() + + /** + * 添加TTS事件监听器 + * + * @param listener 事件监听器 + */ + func addListener(_ listener: TtsEventListener) + + /** + * 移除TTS事件监听器 + * + * @param listener 要移除的事件监听器 + */ + func removeListener(_ listener: TtsEventListener) + + /** + * 添加音频数据监听器 + * + * @param listener 音频数据监听器 + */ + func addAudioDataListener(_ listener: AudioDataListener) + + /** + * 移除音频数据监听器 + * + * @param listener 要移除的音频数据监听器 + */ + func removeAudioDataListener(_ listener: AudioDataListener) +} \ No newline at end of file diff --git a/local_plugins/speech/ios/Classes/SpeechPlugin.h b/local_plugins/speech/ios/Classes/SpeechPlugin.h new file mode 100644 index 000000000..f4f2f2d6b --- /dev/null +++ b/local_plugins/speech/ios/Classes/SpeechPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface SpeechPlugin : NSObject +@end \ No newline at end of file diff --git a/local_plugins/speech/ios/Classes/SpeechPlugin.m b/local_plugins/speech/ios/Classes/SpeechPlugin.m new file mode 100644 index 000000000..8087ac986 --- /dev/null +++ b/local_plugins/speech/ios/Classes/SpeechPlugin.m @@ -0,0 +1,13 @@ +#import "SpeechPlugin.h" +#if __has_include() +#import +#else +#import "speech-Swift.h" +#endif + +@implementation SpeechPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + // 正确调用 Swift 类的静态方法 + [SwiftSpeechPlugin registerWithRegistrar:registrar]; +} +@end \ No newline at end of file diff --git a/local_plugins/speech/ios/Classes/SpeechPlugin.swift b/local_plugins/speech/ios/Classes/SpeechPlugin.swift new file mode 100644 index 000000000..20dbb8ed3 --- /dev/null +++ b/local_plugins/speech/ios/Classes/SpeechPlugin.swift @@ -0,0 +1,22 @@ +import Flutter +import UIKit + +/** + * SpeechPlugin + * + * 语音服务接口定义插件 + * 注意:此插件仅提供接口定义,不提供具体实现 + * 实际的语音识别和合成功能由其他插件实现 + */ +public class SwiftSpeechPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "speech", binaryMessenger: registrar.messenger()) + let instance = SwiftSpeechPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + // 此插件仅提供接口定义,不处理任何方法调用 + result(FlutterMethodNotImplemented) + } +} \ No newline at end of file diff --git a/local_plugins/speech/ios/Classes/speech-Bridging-Header.h b/local_plugins/speech/ios/Classes/speech-Bridging-Header.h new file mode 100644 index 000000000..a12436481 --- /dev/null +++ b/local_plugins/speech/ios/Classes/speech-Bridging-Header.h @@ -0,0 +1,6 @@ +#ifndef speech_Bridging_Header_h +#define speech_Bridging_Header_h + +#import + +#endif /* speech_Bridging_Header_h */ \ No newline at end of file diff --git a/local_plugins/speech/ios/Classes/speech.h b/local_plugins/speech/ios/Classes/speech.h new file mode 100644 index 000000000..b574c3aee --- /dev/null +++ b/local_plugins/speech/ios/Classes/speech.h @@ -0,0 +1,5 @@ +#import + +#import "SpeechPlugin.h" + +// 添加其他公共头文件导出 \ No newline at end of file diff --git a/local_plugins/speech/ios/README.md b/local_plugins/speech/ios/README.md new file mode 100644 index 000000000..9fa57fadb --- /dev/null +++ b/local_plugins/speech/ios/README.md @@ -0,0 +1,29 @@ +# Speech 插件 - iOS 实现 + +## 概述 + +Speech 插件定义了语音识别 (ASR) 和语音合成 (TTS) 服务的标准接口协议,用于支持 DeepVoice 应用中的语音交互功能。 + +## 主要组件 + +- `IAsrService`: 语音识别服务协议,定义了语音识别的标准接口 +- `ITtsService`: 语音合成服务协议,定义了语音合成的标准接口 + +## 使用方式 + +此插件仅提供接口定义,实际使用时需要: + +1. 实现 `IAsrService` 和 `ITtsService` 协议 +2. 将实现注册到 Flutter 插件系统 + +## 文件结构 + +- `IAsrService.swift`: 语音识别服务接口定义 +- `ITtsService.swift`: 语音合成服务接口定义 +- `SpeechPlugin.swift`: Flutter 插件注册类 + +## 注意事项 + +- 此插件仅定义接口,不包含具体实现 +- 使用 Swift 5.0 编写 +- 支持 iOS 11.0 及以上版本 \ No newline at end of file diff --git a/local_plugins/speech/ios/speech.podspec b/local_plugins/speech/ios/speech.podspec new file mode 100644 index 000000000..9a6b22a74 --- /dev/null +++ b/local_plugins/speech/ios/speech.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# +Pod::Spec.new do |s| + s.name = 'speech' + s.version = '0.0.1' + s.summary = '语音识别和合成服务接口' + s.description = <<-DESC +语音识别和合成服务接口,定义了用于不同供应商实现的标准协议 + DESC + s.homepage = 'https://www.deep-voice.com' + s.license = { :file => '../LICENSE' } + s.author = { 'DeepVoice Team' => 'dev@deep-voice.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + s.platform = :ios, '17.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 \ No newline at end of file diff --git a/local_plugins/speech/lib/speech.dart b/local_plugins/speech/lib/speech.dart new file mode 100644 index 000000000..7cc102ac6 --- /dev/null +++ b/local_plugins/speech/lib/speech.dart @@ -0,0 +1,8 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +// 空实现,仅作为接口定义的共享库 +class Speech { + static const MethodChannel _channel = MethodChannel('speech'); +} \ No newline at end of file diff --git a/local_plugins/speech/pubspec.yaml b/local_plugins/speech/pubspec.yaml new file mode 100644 index 000000000..04ef35cb7 --- /dev/null +++ b/local_plugins/speech/pubspec.yaml @@ -0,0 +1,26 @@ +name: speech +description: 语音识别和合成服务接口,定义了用于不同供应商实现的标准协议 +version: 0.0.1 +publish_to: 'none' + +environment: + sdk: '>=3.3.0 <4.0.0' + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +flutter: + plugin: + platforms: + android: + package: com.deep_voice.speech + pluginClass: SpeechPlugin + ios: + pluginClass: SpeechPlugin \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 7a8bbb5c2..88bed3ce9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,7 +53,6 @@ dependencies: font_awesome_flutter: ^10.8.0 lottie: ^3.3.1 app_settings: 5.2.0 - speech_to_text: ^7.0.0 fluwx: ^5.4.1 firebase_core: ^3.12.1 firebase_auth: ^5.5.1 @@ -65,6 +64,8 @@ dependencies: dio: ^5.8.0+1 sqflite: ^2.4.2 path: ^1.9.1 + speech: + path: local_plugins/speech azure_speech: path: local_plugins/azure_speech open_ai_service: