32 changed files with 3116 additions and 2039 deletions
@ -0,0 +1,804 @@ |
|||
// !$*UTF8*$! |
|||
{ |
|||
archiveVersion = 1; |
|||
classes = { |
|||
}; |
|||
objectVersion = 54; |
|||
objects = { |
|||
|
|||
/* Begin PBXBuildFile section */ |
|||
0AD3C3772DDE122500FD4F41 /* Opus in Frameworks */ = {isa = PBXBuildFile; productRef = 0AD3C3762DDE122500FD4F41 /* Opus */; }; |
|||
0AD3C37C2DDE142500FD4F41 /* Opus in Frameworks */ = {isa = PBXBuildFile; productRef = 0AD3C37B2DDE142500FD4F41 /* Opus */; }; |
|||
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 */; }; |
|||
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 */; }; |
|||
B11F80E69458BE0D0F2DA890 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DB4CB5146DA70CF43D67A5B /* Pods_Runner.framework */; }; |
|||
B3D1A46E14BD3B07986EF8CD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F9C3583A880BAF5E80CA0405 /* Pods_RunnerTests.framework */; }; |
|||
/* 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 */ |
|||
0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; }; |
|||
0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AccessorySetupKit.framework; path = System/Library/Frameworks/AccessorySetupKit.framework; sourceTree = SDKROOT; }; |
|||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; |
|||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; |
|||
1DE8AF4A64ABDD75043084FC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; |
|||
209BB2477C918E1C69F5F1E0 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; }; |
|||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; }; |
|||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; |
|||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; |
|||
3CFCAD0427BE38C23B922B35 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; }; |
|||
6DB4CB5146DA70CF43D67A5B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; |
|||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; |
|||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; |
|||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; |
|||
85F42A75E97670A970955821 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; }; |
|||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; |
|||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; |
|||
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 = "<group>"; }; |
|||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; |
|||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; |
|||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; |
|||
D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; }; |
|||
E75F8D421A56891E8C81AD82 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; }; |
|||
F9C3583A880BAF5E80CA0405 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; |
|||
FD4B933141D4D3B35A169527 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; }; |
|||
/* End PBXFileReference section */ |
|||
|
|||
/* Begin PBXFrameworksBuildPhase section */ |
|||
97C146EB1CF9000F007C117D /* Frameworks */ = { |
|||
isa = PBXFrameworksBuildPhase; |
|||
buildActionMask = 2147483647; |
|||
files = ( |
|||
0AD3C3772DDE122500FD4F41 /* Opus in Frameworks */, |
|||
0AD3C37C2DDE142500FD4F41 /* Opus in Frameworks */, |
|||
B11F80E69458BE0D0F2DA890 /* Pods_Runner.framework in Frameworks */, |
|||
); |
|||
runOnlyForDeploymentPostprocessing = 0; |
|||
}; |
|||
B3515CD9628E3332AFC88CD4 /* Frameworks */ = { |
|||
isa = PBXFrameworksBuildPhase; |
|||
buildActionMask = 2147483647; |
|||
files = ( |
|||
B3D1A46E14BD3B07986EF8CD /* Pods_RunnerTests.framework in Frameworks */, |
|||
); |
|||
runOnlyForDeploymentPostprocessing = 0; |
|||
}; |
|||
/* End PBXFrameworksBuildPhase section */ |
|||
|
|||
/* Begin PBXGroup section */ |
|||
1ACB686EC7DC36C6E33D372F /* Pods */ = { |
|||
isa = PBXGroup; |
|||
children = ( |
|||
3CFCAD0427BE38C23B922B35 /* Pods-Runner.debug.xcconfig */, |
|||
85F42A75E97670A970955821 /* Pods-Runner.release.xcconfig */, |
|||
1DE8AF4A64ABDD75043084FC /* Pods-Runner.profile.xcconfig */, |
|||
209BB2477C918E1C69F5F1E0 /* Pods-RunnerTests.debug.xcconfig */, |
|||
E75F8D421A56891E8C81AD82 /* Pods-RunnerTests.release.xcconfig */, |
|||
FD4B933141D4D3B35A169527 /* Pods-RunnerTests.profile.xcconfig */, |
|||
); |
|||
path = Pods; |
|||
sourceTree = "<group>"; |
|||
}; |
|||
1E5DDD8701F6FB27AA502080 /* Frameworks */ = { |
|||
isa = PBXGroup; |
|||
children = ( |
|||
0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */, |
|||
6DB4CB5146DA70CF43D67A5B /* Pods_Runner.framework */, |
|||
F9C3583A880BAF5E80CA0405 /* Pods_RunnerTests.framework */, |
|||
); |
|||
name = Frameworks; |
|||
sourceTree = "<group>"; |
|||
}; |
|||
331C8082294A63A400263BE5 /* RunnerTests */ = { |
|||
isa = PBXGroup; |
|||
children = ( |
|||
331C807B294A618700263BE5 /* RunnerTests.swift */, |
|||
); |
|||
path = RunnerTests; |
|||
sourceTree = "<group>"; |
|||
}; |
|||
9740EEB11CF90186004384FC /* Flutter */ = { |
|||
isa = PBXGroup; |
|||
children = ( |
|||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, |
|||
9740EEB21CF90195004384FC /* Debug.xcconfig */, |
|||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */, |
|||
9740EEB31CF90195004384FC /* Generated.xcconfig */, |
|||
); |
|||
name = Flutter; |
|||
sourceTree = "<group>"; |
|||
}; |
|||
97C146E51CF9000F007C117D = { |
|||
isa = PBXGroup; |
|||
children = ( |
|||
9740EEB11CF90186004384FC /* Flutter */, |
|||
97C146F01CF9000F007C117D /* Runner */, |
|||
97C146EF1CF9000F007C117D /* Products */, |
|||
331C8082294A63A400263BE5 /* RunnerTests */, |
|||
1ACB686EC7DC36C6E33D372F /* Pods */, |
|||
1E5DDD8701F6FB27AA502080 /* Frameworks */, |
|||
); |
|||
sourceTree = "<group>"; |
|||
}; |
|||
97C146EF1CF9000F007C117D /* Products */ = { |
|||
isa = PBXGroup; |
|||
children = ( |
|||
97C146EE1CF9000F007C117D /* Runner.app */, |
|||
331C8081294A63A400263BE5 /* RunnerTests.xctest */, |
|||
); |
|||
name = Products; |
|||
sourceTree = "<group>"; |
|||
}; |
|||
97C146F01CF9000F007C117D /* Runner */ = { |
|||
isa = PBXGroup; |
|||
children = ( |
|||
0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */, |
|||
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 */, |
|||
D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */, |
|||
); |
|||
path = Runner; |
|||
sourceTree = "<group>"; |
|||
}; |
|||
/* End PBXGroup section */ |
|||
|
|||
/* Begin PBXNativeTarget section */ |
|||
331C8080294A63A400263BE5 /* RunnerTests */ = { |
|||
isa = PBXNativeTarget; |
|||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; |
|||
buildPhases = ( |
|||
B6E8833064FA9BB82F279A0E /* [CP] Check Pods Manifest.lock */, |
|||
331C807D294A63A400263BE5 /* Sources */, |
|||
331C807F294A63A400263BE5 /* Resources */, |
|||
B3515CD9628E3332AFC88CD4 /* Frameworks */, |
|||
); |
|||
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 = ( |
|||
B77B7F8E420C3F7B653C6205 /* [CP] Check Pods Manifest.lock */, |
|||
9740EEB61CF901F6004384FC /* Run Script */, |
|||
97C146EA1CF9000F007C117D /* Sources */, |
|||
97C146EB1CF9000F007C117D /* Frameworks */, |
|||
97C146EC1CF9000F007C117D /* Resources */, |
|||
9705A1C41CF9048500538489 /* Embed Frameworks */, |
|||
3B06AD1E1E4923F5004D2608 /* Thin Binary */, |
|||
1D3D5881CA405E420076D1F6 /* [CP] Embed Pods Frameworks */, |
|||
C482126191919ADDA94A189A /* [CP] Copy Pods Resources */, |
|||
); |
|||
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 */, |
|||
0AD944122DD9DB5C00557D21 /* GoogleService-Info.plist in Resources */, |
|||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, |
|||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, |
|||
); |
|||
runOnlyForDeploymentPostprocessing = 0; |
|||
}; |
|||
/* End PBXResourcesBuildPhase section */ |
|||
|
|||
/* Begin PBXShellScriptBuildPhase section */ |
|||
1D3D5881CA405E420076D1F6 /* [CP] Embed Pods Frameworks */ = { |
|||
isa = PBXShellScriptBuildPhase; |
|||
buildActionMask = 2147483647; |
|||
files = ( |
|||
); |
|||
inputFileListPaths = ( |
|||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", |
|||
); |
|||
name = "[CP] Embed Pods Frameworks"; |
|||
outputFileListPaths = ( |
|||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", |
|||
); |
|||
runOnlyForDeploymentPostprocessing = 0; |
|||
shellPath = /bin/sh; |
|||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; |
|||
showEnvVarsInLog = 0; |
|||
}; |
|||
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"; |
|||
}; |
|||
B6E8833064FA9BB82F279A0E /* [CP] Check Pods Manifest.lock */ = { |
|||
isa = PBXShellScriptBuildPhase; |
|||
buildActionMask = 2147483647; |
|||
files = ( |
|||
); |
|||
inputFileListPaths = ( |
|||
); |
|||
inputPaths = ( |
|||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock", |
|||
"${PODS_ROOT}/Manifest.lock", |
|||
); |
|||
name = "[CP] Check Pods Manifest.lock"; |
|||
outputFileListPaths = ( |
|||
); |
|||
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; |
|||
}; |
|||
B77B7F8E420C3F7B653C6205 /* [CP] Check Pods Manifest.lock */ = { |
|||
isa = PBXShellScriptBuildPhase; |
|||
buildActionMask = 2147483647; |
|||
files = ( |
|||
); |
|||
inputFileListPaths = ( |
|||
); |
|||
inputPaths = ( |
|||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock", |
|||
"${PODS_ROOT}/Manifest.lock", |
|||
); |
|||
name = "[CP] Check Pods Manifest.lock"; |
|||
outputFileListPaths = ( |
|||
); |
|||
outputPaths = ( |
|||
"$(DERIVED_FILE_DIR)/Pods-Runner-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; |
|||
}; |
|||
C482126191919ADDA94A189A /* [CP] Copy Pods Resources */ = { |
|||
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"; |
|||
outputFileListPaths = ( |
|||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", |
|||
); |
|||
runOnlyForDeploymentPostprocessing = 0; |
|||
shellPath = /bin/sh; |
|||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; |
|||
showEnvVarsInLog = 0; |
|||
}; |
|||
/* 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 = "<group>"; |
|||
}; |
|||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { |
|||
isa = PBXVariantGroup; |
|||
children = ( |
|||
97C147001CF9000F007C117D /* Base */, |
|||
); |
|||
name = LaunchScreen.storyboard; |
|||
sourceTree = "<group>"; |
|||
}; |
|||
/* 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; |
|||
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 = 18.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; |
|||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; |
|||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; |
|||
DEVELOPMENT_TEAM = 42C82SZUVX; |
|||
ENABLE_BITCODE = NO; |
|||
INFOPLIST_FILE = Runner/Info.plist; |
|||
IPHONEOS_DEPLOYMENT_TARGET = 18.0; |
|||
LD_RUNPATH_SEARCH_PATHS = ( |
|||
"$(inherited)", |
|||
"@executable_path/Frameworks", |
|||
); |
|||
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound; |
|||
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; |
|||
baseConfigurationReference = 209BB2477C918E1C69F5F1E0 /* Pods-RunnerTests.debug.xcconfig */; |
|||
buildSettings = { |
|||
BUNDLE_LOADER = "$(TEST_HOST)"; |
|||
CODE_SIGN_STYLE = Automatic; |
|||
CURRENT_PROJECT_VERSION = 1; |
|||
GENERATE_INFOPLIST_FILE = YES; |
|||
IPHONEOS_DEPLOYMENT_TARGET = 18.0; |
|||
MARKETING_VERSION = 1.0; |
|||
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.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; |
|||
baseConfigurationReference = E75F8D421A56891E8C81AD82 /* Pods-RunnerTests.release.xcconfig */; |
|||
buildSettings = { |
|||
BUNDLE_LOADER = "$(TEST_HOST)"; |
|||
CODE_SIGN_STYLE = Automatic; |
|||
CURRENT_PROJECT_VERSION = 1; |
|||
GENERATE_INFOPLIST_FILE = YES; |
|||
IPHONEOS_DEPLOYMENT_TARGET = 18.0; |
|||
MARKETING_VERSION = 1.0; |
|||
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.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; |
|||
baseConfigurationReference = FD4B933141D4D3B35A169527 /* Pods-RunnerTests.profile.xcconfig */; |
|||
buildSettings = { |
|||
BUNDLE_LOADER = "$(TEST_HOST)"; |
|||
CODE_SIGN_STYLE = Automatic; |
|||
CURRENT_PROJECT_VERSION = 1; |
|||
GENERATE_INFOPLIST_FILE = YES; |
|||
IPHONEOS_DEPLOYMENT_TARGET = 18.0; |
|||
MARKETING_VERSION = 1.0; |
|||
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; |
|||
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 = 18.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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; |
|||
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 = 18.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; |
|||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; |
|||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; |
|||
DEVELOPMENT_TEAM = 42C82SZUVX; |
|||
ENABLE_BITCODE = NO; |
|||
INFOPLIST_FILE = Runner/Info.plist; |
|||
IPHONEOS_DEPLOYMENT_TARGET = 18.0; |
|||
LD_RUNPATH_SEARCH_PATHS = ( |
|||
"$(inherited)", |
|||
"@executable_path/Frameworks", |
|||
); |
|||
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound; |
|||
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; |
|||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; |
|||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; |
|||
DEVELOPMENT_TEAM = 42C82SZUVX; |
|||
ENABLE_BITCODE = NO; |
|||
INFOPLIST_FILE = Runner/Info.plist; |
|||
IPHONEOS_DEPLOYMENT_TARGET = 18.0; |
|||
LD_RUNPATH_SEARCH_PATHS = ( |
|||
"$(inherited)", |
|||
"@executable_path/Frameworks", |
|||
); |
|||
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound; |
|||
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 */ |
|||
|
|||
/* Begin XCRemoteSwiftPackageReference section */ |
|||
0AD3C3752DDE122500FD4F41 /* XCRemoteSwiftPackageReference "swift-opus" */ = { |
|||
isa = XCRemoteSwiftPackageReference; |
|||
repositoryURL = "https://github.com/alta/swift-opus"; |
|||
requirement = { |
|||
kind = upToNextMajorVersion; |
|||
minimumVersion = 0.0.2; |
|||
}; |
|||
}; |
|||
0AD3C37A2DDE139600FD4F41 /* XCRemoteSwiftPackageReference "swift-opus" */ = { |
|||
isa = XCRemoteSwiftPackageReference; |
|||
repositoryURL = "https://github.com/alta/swift-opus"; |
|||
requirement = { |
|||
kind = upToNextMajorVersion; |
|||
minimumVersion = 0.0.2; |
|||
}; |
|||
}; |
|||
/* End XCRemoteSwiftPackageReference section */ |
|||
|
|||
/* Begin XCSwiftPackageProductDependency section */ |
|||
0AD3C3762DDE122500FD4F41 /* Opus */ = { |
|||
isa = XCSwiftPackageProductDependency; |
|||
package = 0AD3C3752DDE122500FD4F41 /* XCRemoteSwiftPackageReference "swift-opus" */; |
|||
productName = Opus; |
|||
}; |
|||
0AD3C37B2DDE142500FD4F41 /* Opus */ = { |
|||
isa = XCSwiftPackageProductDependency; |
|||
package = 0AD3C37A2DDE139600FD4F41 /* XCRemoteSwiftPackageReference "swift-opus" */; |
|||
productName = Opus; |
|||
}; |
|||
/* End XCSwiftPackageProductDependency section */ |
|||
}; |
|||
rootObject = 97C146E61CF9000F007C117D /* Project object */; |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
{ |
|||
"originHash" : "bd9b3a14091d57af541f2718cbec3c63a0b618a9a7c0674743c7cf43998a352d", |
|||
"pins" : [ |
|||
{ |
|||
"identity" : "swift-opus", |
|||
"kind" : "remoteSourceControl", |
|||
"location" : "https://github.com/alta/swift-opus", |
|||
"state" : { |
|||
"revision" : "5f0164b8915fb9001265c54413aa0f0efbd3df06", |
|||
"version" : "0.0.2" |
|||
} |
|||
} |
|||
], |
|||
"version" : 3 |
|||
} |
|||
@ -1,994 +0,0 @@ |
|||
import Foundation |
|||
import MicrosoftCognitiveServicesSpeech |
|||
import open_ai_service |
|||
import chat_storage |
|||
|
|||
/// 代理服务 - 处理语音识别、TTS和AI对话相关逻辑 |
|||
/// |
|||
/// 负责集成Azure语音服务、OpenAI服务和本地存储服务, |
|||
/// 提供语音识别、语音合成、AI对话等功能 |
|||
class AgentService { |
|||
// 常量定义 |
|||
private let TAG = "AgentService" |
|||
|
|||
// 上下文和监听器 |
|||
private var listeners = NSHashTable<AnyObject>.weakObjects() |
|||
|
|||
// 配置参数 |
|||
private var azureSpeechKey: String = "" |
|||
private var azureSpeechRegion: String = "" |
|||
private var openaiApiKey: String = "" |
|||
private var openaiBaseUrl: String = "" |
|||
private var openaiModel: String = "gpt-3.5-turbo" |
|||
private var mcpServer: String = "" |
|||
|
|||
// Azure服务 |
|||
private var azureAsrHelper: AzureAsrHelper? |
|||
private var azureTtsHelper: AzureTtsHelper? |
|||
|
|||
// OpenAI服务 |
|||
private var openAIService: OpenAIServiceBridge? |
|||
|
|||
// 聊天存储服务 |
|||
private var chatStorageHelper: ChatStorageHelperBridge? |
|||
|
|||
// 会话ID,用于区分不同聊天上下文 |
|||
private let sessionId = "default_agent" |
|||
|
|||
// 历史聊天消息缓存 |
|||
private var historyMessages = [[String: Any]]() |
|||
|
|||
// 系统提示词 |
|||
private var systemPrompt = "" |
|||
|
|||
// 状态 |
|||
private var isInitialized = false |
|||
private var isRecognitionActive = false |
|||
private var isTtsSpeaking = false |
|||
private var hasSpeechDetected = false |
|||
private var isAiStreaming = false |
|||
|
|||
// AI流生成相关 |
|||
private var currentAiTask: DispatchWorkItem? |
|||
|
|||
// 空闲检测相关 |
|||
private var idleCheckTimer: Timer? |
|||
private let maxIdleSeconds: TimeInterval = 10 // 最大空闲秒数 |
|||
|
|||
/// 初始化 |
|||
init() { |
|||
// 创建OpenAI服务桥接器 |
|||
openAIService = OpenAIServiceBridge() |
|||
|
|||
// 创建聊天存储桥接器 |
|||
chatStorageHelper = ChatStorageHelperBridge() |
|||
} |
|||
|
|||
/** |
|||
* 初始化系统提示词 |
|||
*/ |
|||
private func initSystemPrompt() { |
|||
systemPrompt = """ |
|||
你是一个友好、专业的语音助手,名叫"小语"。你的目标是通过对话为用户提供帮助、解答问题和完成任务。 |
|||
遵循以下指导原则: |
|||
1. 保持简短精炼的回答,因为用户是通过语音与你交流 |
|||
2. 优先使用中文回复,除非用户明确要求使用其他语言 |
|||
3. 当用户问题不明确时,礼貌地请求更多信息 |
|||
4. 避免过长的列表,尽量将信息分成小段 |
|||
5. 不要使用需要视觉展示的元素(如表格、图表或代码块) |
|||
6. 记住用户之前的对话内容,保持对话连贯 |
|||
7. 如果用户发送了图片,请根据图片内容和文字要求回答问题 |
|||
你不仅可以回答知识性问题,还可以帮助用户设置提醒、提供建议,或进行轻松愉快的对话。 |
|||
无论遇到什么问题,都要尽力以温暖、贴心的语气提供最佳帮助。 |
|||
""" |
|||
} |
|||
|
|||
/** |
|||
* 初始化 |
|||
* |
|||
* - Parameter config: 配置参数,包含所需的所有API密钥和设置 |
|||
* - Returns: 是否初始化成功 |
|||
*/ |
|||
func initialize(config: [String: Any]) -> Bool { |
|||
if isInitialized { return true } |
|||
|
|||
do { |
|||
// 从配置中获取参数 |
|||
if let azureKey = config["azureSpeechKey"] as? String { |
|||
azureSpeechKey = azureKey |
|||
} |
|||
|
|||
if let azureRegion = config["azureSpeechRegion"] as? String { |
|||
azureSpeechRegion = azureRegion |
|||
} |
|||
|
|||
if let openaiKey = config["openaiApiKey"] as? String { |
|||
openaiApiKey = openaiKey |
|||
} |
|||
|
|||
if let baseUrl = config["openaiBaseUrl"] as? String { |
|||
openaiBaseUrl = baseUrl |
|||
} |
|||
|
|||
if let model = config["openaiModel"] as? String { |
|||
openaiModel = model |
|||
} |
|||
|
|||
if let server = config["mcpServer"] as? String { |
|||
mcpServer = server |
|||
} |
|||
|
|||
// 自定义系统提示词 |
|||
if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty { |
|||
systemPrompt = customSystemPrompt |
|||
} else { |
|||
// 使用默认系统提示词 |
|||
initSystemPrompt() |
|||
} |
|||
|
|||
// 检查必要参数 |
|||
if azureSpeechKey.isEmpty || azureSpeechRegion.isEmpty || openaiApiKey.isEmpty { |
|||
print("\(TAG): 初始化失败:关键配置参数缺失") |
|||
return false |
|||
} |
|||
|
|||
// 初始化OpenAI服务 |
|||
openAIService?.initialize(apiKey: openaiApiKey, baseUrl: openaiBaseUrl, model: openaiModel, mcpServer: mcpServer) |
|||
|
|||
// 初始化Azure ASR |
|||
azureAsrHelper = AzureAsrHelper(self) |
|||
let asrInitResult = azureAsrHelper?.initialize( |
|||
subscriptionKey: azureSpeechKey, |
|||
region: azureSpeechRegion, |
|||
audioSourceType: .microphone |
|||
) ?? false |
|||
|
|||
if !asrInitResult { |
|||
print("\(TAG): Azure ASR初始化失败") |
|||
return false |
|||
} |
|||
|
|||
// 初始化Azure TTS |
|||
azureTtsHelper = AzureTtsHelper(self) |
|||
let ttsInitResult = azureTtsHelper?.initialize( |
|||
subscriptionKey: azureSpeechKey, |
|||
region: azureSpeechRegion, |
|||
callback: self |
|||
) ?? false |
|||
|
|||
if !ttsInitResult { |
|||
print("\(TAG): Azure TTS初始化失败") |
|||
return false |
|||
} |
|||
|
|||
// 加载最近的聊天记录 |
|||
loadChatHistory() |
|||
|
|||
isInitialized = true |
|||
print("\(TAG): 代理服务初始化成功") |
|||
return true |
|||
} catch { |
|||
print("\(TAG): 初始化失败:\(error.localizedDescription)") |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 添加事件监听器 |
|||
* |
|||
* - Parameter listener: 要添加的监听器 |
|||
*/ |
|||
func addListener(_ listener: AgentServiceListener) { |
|||
listeners.add(listener as AnyObject) |
|||
} |
|||
|
|||
/** |
|||
* 设置事件监听器(替换现有监听器) |
|||
* |
|||
* - Parameter listener: 要设置的监听器 |
|||
*/ |
|||
func setListener(_ listener: AgentServiceListener) { |
|||
listeners.removeAllObjects() |
|||
listeners.add(listener as AnyObject) |
|||
} |
|||
|
|||
/** |
|||
* 移除事件监听器 |
|||
* |
|||
* - Parameter listener: 要移除的监听器 |
|||
*/ |
|||
func removeListener(_ listener: AgentServiceListener) { |
|||
listeners.remove(listener as AnyObject) |
|||
} |
|||
|
|||
/** |
|||
* 移除所有事件监听器 |
|||
*/ |
|||
func clearListeners() { |
|||
listeners.removeAllObjects() |
|||
} |
|||
|
|||
/** |
|||
* 启动空闲检测 |
|||
*/ |
|||
private func startIdleCheck() { |
|||
stopIdleCheck() // 先停止现有的检查 |
|||
|
|||
if !isRecognitionActive { return } |
|||
|
|||
// 创建定时器 |
|||
idleCheckTimer = Timer.scheduledTimer(withTimeInterval: maxIdleSeconds, repeats: false) { [weak self] _ in |
|||
guard let self = self else { return } |
|||
|
|||
// 如果状态仍然是空闲的,则停止识别 |
|||
if self.isRecognitionActive && !self.hasSpeechDetected && !self.isTtsSpeaking && !self.isAiStreaming { |
|||
self.stopRecognition() |
|||
self.sendEvent("auto_stop", ["reason": "idle_timeout", "seconds": self.maxIdleSeconds]) |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 停止空闲检测 |
|||
*/ |
|||
private func stopIdleCheck() { |
|||
idleCheckTimer?.invalidate() |
|||
idleCheckTimer = nil |
|||
} |
|||
|
|||
/** |
|||
* 重启空闲检测 |
|||
* 当状态发生变化时调用 |
|||
*/ |
|||
private func restartIdleCheck() { |
|||
if isRecognitionActive { |
|||
startIdleCheck() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 开始语音识别 |
|||
* |
|||
* - Returns: 是否成功开始识别 |
|||
*/ |
|||
func startRecognition() -> Bool { |
|||
if !isInitialized { |
|||
print("\(TAG): 服务未初始化") |
|||
return false |
|||
} |
|||
|
|||
if isRecognitionActive { return true } |
|||
|
|||
isRecognitionActive = true |
|||
hasSpeechDetected = false |
|||
|
|||
do { |
|||
// 创建连续识别回调 |
|||
class ContinuousRecognizeCallbackImpl: AzureAsrHelper.ContinuousRecognizeCallback { |
|||
private weak var service: AgentService? |
|||
|
|||
init(_ service: AgentService) { |
|||
self.service = service |
|||
} |
|||
|
|||
func onRecognizing(recognizing: String, detectedLanguage: String) { |
|||
guard let service = service else { return } |
|||
|
|||
if !recognizing.isEmpty { |
|||
// 检测到语音,更新状态 |
|||
let previousHasSpeech = service.hasSpeechDetected |
|||
service.hasSpeechDetected = true |
|||
|
|||
// 状态发生变化时重启空闲检测 |
|||
if !previousHasSpeech { |
|||
service.restartIdleCheck() |
|||
} |
|||
|
|||
service.sendEvent("recognizing", [ |
|||
"text": recognizing, |
|||
"language": detectedLanguage |
|||
]) |
|||
|
|||
// 如果TTS正在播放或AI正在生成,则触发打断 |
|||
if service.isTtsSpeaking || service.isAiStreaming { |
|||
service.interruptCurrentResponse() |
|||
} |
|||
} |
|||
} |
|||
|
|||
func onResult(text: String, detectedLanguage: String) { |
|||
guard let service = service else { return } |
|||
|
|||
if !text.isEmpty { |
|||
service.sendEvent("recognition_result", [ |
|||
"text": text, |
|||
"language": detectedLanguage |
|||
]) |
|||
|
|||
service.processWithOpenAI(text: text) |
|||
} |
|||
|
|||
// 重置状态,继续识别 |
|||
let previousHasSpeech = service.hasSpeechDetected |
|||
service.hasSpeechDetected = false |
|||
|
|||
// 状态发生变化时重启空闲检测 |
|||
if previousHasSpeech { |
|||
service.restartIdleCheck() |
|||
} |
|||
} |
|||
|
|||
func onSessionStarted() { |
|||
guard let service = service else { return } |
|||
|
|||
service.sendEvent("recognition_started", ["status": "started"]) |
|||
// 启动空闲检测 |
|||
service.startIdleCheck() |
|||
} |
|||
|
|||
func onSessionStopped() { |
|||
guard let service = service else { return } |
|||
|
|||
service.isRecognitionActive = false |
|||
service.stopIdleCheck() |
|||
service.sendEvent("recognition_stopped", ["status": "stopped"]) |
|||
} |
|||
|
|||
func onCanceled(reason: String, errorDetails: String) { |
|||
guard let service = service else { return } |
|||
|
|||
service.isRecognitionActive = false |
|||
service.stopIdleCheck() |
|||
service.sendEvent("recognition_canceled", [ |
|||
"reason": reason, |
|||
"details": errorDetails |
|||
]) |
|||
} |
|||
|
|||
func onError(error: String) { |
|||
guard let service = service else { return } |
|||
|
|||
service.isRecognitionActive = false |
|||
service.stopIdleCheck() |
|||
print("\(service.TAG): 语音识别出错: \(error)") |
|||
service.sendEvent("error", [ |
|||
"code": "RECOGNITION_ERROR", |
|||
"message": error |
|||
]) |
|||
} |
|||
} |
|||
|
|||
let callback = ContinuousRecognizeCallbackImpl(self) |
|||
let result = azureAsrHelper?.startContinuousRecognition(callback) ?? false |
|||
|
|||
if !result { |
|||
isRecognitionActive = false |
|||
print("\(TAG): 启动语音识别失败") |
|||
sendEvent("error", [ |
|||
"code": "RECOGNITION_START_ERROR", |
|||
"message": "启动语音识别失败" |
|||
]) |
|||
} |
|||
|
|||
return result |
|||
} catch { |
|||
isRecognitionActive = false |
|||
print("\(TAG): 启动语音识别失败: \(error.localizedDescription)") |
|||
sendEvent("error", [ |
|||
"code": "RECOGNITION_START_ERROR", |
|||
"message": error.localizedDescription |
|||
]) |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 停止语音识别 |
|||
*/ |
|||
func stopRecognition() { |
|||
if !isRecognitionActive { return } |
|||
|
|||
print("\(TAG): 停止语音识别") |
|||
|
|||
// 停止识别 |
|||
let _ = azureAsrHelper?.stopContinuousRecognition() |
|||
isRecognitionActive = false |
|||
stopIdleCheck() |
|||
print("\(TAG): 语音识别已停止") |
|||
} |
|||
|
|||
/** |
|||
* 打断当前响应 |
|||
* 停止TTS播放和AI流输出 |
|||
*/ |
|||
func interruptCurrentResponse() { |
|||
if isAiStreaming || isTtsSpeaking { |
|||
// 停止TTS播放 |
|||
stopTts() |
|||
|
|||
// 停止AI流输出 |
|||
stopAiStream() |
|||
|
|||
// 发送打断事件 |
|||
sendEvent("response_interrupted", ["status": "interrupted"]) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 停止AI流输出 |
|||
*/ |
|||
private func stopAiStream() { |
|||
if isAiStreaming { |
|||
// 取消当前AI生成任务 |
|||
currentAiTask?.cancel() |
|||
currentAiTask = nil |
|||
|
|||
// 通知OpenAI服务终止当前流式请求 |
|||
openAIService?.cancelCurrentStream() |
|||
|
|||
// 更新状态 |
|||
isAiStreaming = false |
|||
|
|||
// 记录日志 |
|||
print("\(TAG): AI流输出已停止") |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 处理文本输入 |
|||
* 作为语音输入的补充,直接处理文本并通过事件返回结果 |
|||
* |
|||
* - Parameters: |
|||
* - text: 用户输入文本 |
|||
* - speakResponse: 是否朗读回复,默认为false |
|||
* - Returns: 是否成功开始处理 |
|||
*/ |
|||
func processTextInput(text: String, speakResponse: Bool = false) -> Bool { |
|||
if !isInitialized { |
|||
print("\(TAG): 服务未初始化") |
|||
sendEvent("error", ["code": "NOT_INITIALIZED", "message": "服务未初始化"]) |
|||
return false |
|||
} |
|||
|
|||
if text.isEmpty { |
|||
print("\(TAG): 文本输入不能为空") |
|||
sendEvent("error", ["code": "EMPTY_TEXT", "message": "文本输入不能为空"]) |
|||
return false |
|||
} |
|||
|
|||
// 使用OpenAI处理文本 |
|||
processWithOpenAI(text: text, speakResponse: speakResponse) |
|||
return true |
|||
} |
|||
|
|||
/** |
|||
* 使用OpenAI处理语音识别结果 |
|||
* |
|||
* - Parameter text: 用户输入文本 |
|||
*/ |
|||
private func processWithOpenAI(text: String) { |
|||
processWithOpenAI(text: text, speakResponse: true) |
|||
} |
|||
|
|||
/** |
|||
* 使用OpenAI处理文本消息 |
|||
* |
|||
* - Parameters: |
|||
* - text: 用户输入文本 |
|||
* - speakResponse: 是否使用TTS朗读回复 |
|||
*/ |
|||
private func processWithOpenAI(text: String, speakResponse: Bool = true) { |
|||
print("\(TAG): 用户问题: \(text)") |
|||
|
|||
// 创建用户文本消息并处理 |
|||
guard let userMessage = openAIService?.createUserMessage(text: text) else { |
|||
print("\(TAG): 创建用户消息失败") |
|||
return |
|||
} |
|||
|
|||
processWithOpenAIInternal(userMessage: userMessage, displayText: text, speakResponse: speakResponse) |
|||
} |
|||
|
|||
/** |
|||
* 使用OpenAI处理图片 |
|||
* |
|||
* - Parameters: |
|||
* - imagePath: 图片文件路径 |
|||
* - text: 可选的文本描述或问题 |
|||
* - speakResponse: 是否朗读回复 |
|||
* - Returns: 是否成功开始处理 |
|||
*/ |
|||
func processImageInput(imagePath: String, text: String = "", speakResponse: Bool = false) -> Bool { |
|||
if !isInitialized { |
|||
print("\(TAG): 服务未初始化") |
|||
sendEvent("error", ["code": "NOT_INITIALIZED", "message": "服务未初始化"]) |
|||
return false |
|||
} |
|||
|
|||
if imagePath.isEmpty { |
|||
print("\(TAG): 图片路径不能为空") |
|||
sendEvent("error", ["code": "EMPTY_IMAGE_PATH", "message": "图片路径不能为空"]) |
|||
return false |
|||
} |
|||
|
|||
// 通知开始处理图片 |
|||
sendEvent("image_processing", [ |
|||
"status": "processing", |
|||
"imagePath": imagePath |
|||
]) |
|||
|
|||
// 异步处理图片 |
|||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in |
|||
guard let self = self else { return } |
|||
|
|||
// 将图片转换为Base64格式 |
|||
guard let imageBase64 = self.openAIService?.fileToBase64(filePath: imagePath) else { |
|||
DispatchQueue.main.async { |
|||
print("\(self.TAG): 图片转换失败: \(imagePath)") |
|||
self.sendEvent("error", [ |
|||
"code": "IMAGE_CONVERSION_FAILED", |
|||
"message": "图片转换失败" |
|||
]) |
|||
} |
|||
return |
|||
} |
|||
|
|||
// 通知图片准备完成 |
|||
DispatchQueue.main.async { |
|||
self.sendEvent("image_ready", [ |
|||
"status": "ready", |
|||
"imagePath": imagePath |
|||
]) |
|||
|
|||
// 处理包含图片的消息 |
|||
self.processImageWithOpenAI(imageBase64: imageBase64, text: text, speakResponse: speakResponse) |
|||
} |
|||
} |
|||
|
|||
return true |
|||
} |
|||
|
|||
/** |
|||
* 使用OpenAI处理图片 |
|||
* |
|||
* - Parameters: |
|||
* - imageBase64: Base64编码的图片数据 |
|||
* - text: 可选的文本描述或问题 |
|||
* - speakResponse: 是否朗读回复 |
|||
*/ |
|||
private func processImageWithOpenAI(imageBase64: String, text: String = "", speakResponse: Bool = false) { |
|||
print("\(TAG): 处理图片输入: \(text.isEmpty ? "无附加文本" : "附带文本: \(text)")") |
|||
|
|||
// 创建带图片的用户消息并处理 |
|||
guard let userMessage = openAIService?.createUserMessageWithImage(text: text, imageBase64: imageBase64) else { |
|||
print("\(TAG): 创建带图片的用户消息失败") |
|||
return |
|||
} |
|||
|
|||
// 图片描述用于存储 |
|||
let displayText = text.isEmpty ? "[图片]" : text |
|||
processWithOpenAIInternal(userMessage: userMessage, displayText: displayText, speakResponse: speakResponse, hasImage: true) |
|||
} |
|||
|
|||
/** |
|||
* 内部方法:通用的OpenAI处理逻辑 |
|||
* |
|||
* - Parameters: |
|||
* - userMessage: 用户消息(可以是文本或图片格式) |
|||
* - displayText: 用于显示和存储的文本 |
|||
* - speakResponse: 是否朗读回复 |
|||
* - hasImage: 是否包含图片 |
|||
*/ |
|||
private func processWithOpenAIInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false) { |
|||
// 如果有正在进行的AI流式输出,先停止它 |
|||
stopAiStream() |
|||
|
|||
// 创建AI任务 |
|||
let workItem = DispatchWorkItem { [weak self] in |
|||
guard let self = self else { return } |
|||
|
|||
// 设置状态为正在流式输出 |
|||
self.isAiStreaming = true |
|||
|
|||
// 使用历史记录作为上下文发送到OpenAI |
|||
var responseBuilder = "" |
|||
|
|||
// 添加系统提示词到历史消息的副本中 |
|||
var messagesWithSystemPrompt: [[String: Any]] = [] |
|||
|
|||
// 先添加系统提示词 |
|||
if !self.systemPrompt.isEmpty { |
|||
if let systemMessage = self.openAIService?.createSystemMessage(text: self.systemPrompt) { |
|||
messagesWithSystemPrompt.append(systemMessage) |
|||
} |
|||
} |
|||
|
|||
// 再添加历史消息 |
|||
messagesWithSystemPrompt.append(contentsOf: self.historyMessages) |
|||
messagesWithSystemPrompt.append(userMessage) |
|||
|
|||
// 将用户消息添加到历史记录 |
|||
self.addToHistoryMessages(userMessage) |
|||
|
|||
// 流式回调 |
|||
class StreamCallbackBridge: NSObject, OpenAIStreamCallback { |
|||
private weak var service: AgentService? |
|||
private var responseBuilder: String |
|||
private let speakResponse: Bool |
|||
private let displayText: String |
|||
private let hasImage: Bool |
|||
|
|||
init(_ service: AgentService, responseBuilder: String = "", speakResponse: Bool, displayText: String, hasImage: Bool) { |
|||
self.service = service |
|||
self.responseBuilder = responseBuilder |
|||
self.speakResponse = speakResponse |
|||
self.displayText = displayText |
|||
self.hasImage = hasImage |
|||
super.init() |
|||
} |
|||
|
|||
func onToken(token: String) { |
|||
guard let service = service else { return } |
|||
|
|||
responseBuilder.append(token) |
|||
if speakResponse { |
|||
_ = service.azureTtsHelper?.speakStream(token) |
|||
} |
|||
// 发送流式回复token |
|||
service.sendEvent("assistant_token", ["token": token]) |
|||
} |
|||
|
|||
func onComplete() { |
|||
guard let service = service else { return } |
|||
|
|||
// 视情况决定是否朗读回复 |
|||
if speakResponse { |
|||
_ = service.azureTtsHelper?.flushStream() |
|||
} |
|||
|
|||
if !responseBuilder.isEmpty { |
|||
// 发送完整回复,包含是否有图片的标记 |
|||
var responseData: [String: Any] = [ |
|||
"text": responseBuilder, |
|||
"userInput": displayText |
|||
] |
|||
|
|||
if hasImage { |
|||
responseData["hasImage"] = true |
|||
} |
|||
|
|||
service.sendEvent("assistant_response", responseData) |
|||
|
|||
// 添加AI回复到历史记录 |
|||
if let assistantMessage = service.openAIService?.createAssistantMessage(text: responseBuilder) { |
|||
service.addToHistoryMessages(assistantMessage) |
|||
} |
|||
|
|||
// 保存聊天记录 |
|||
service.saveChatMessage(userMessage: displayText, assistantMessage: responseBuilder) |
|||
} |
|||
|
|||
// 标记AI流式输出已完成 |
|||
service.isAiStreaming = false |
|||
service.currentAiTask = nil |
|||
} |
|||
|
|||
func onError(error: Error) { |
|||
guard let service = service else { return } |
|||
|
|||
print("\(service.TAG): AI处理出错: \(error.localizedDescription)") |
|||
service.sendEvent("error", [ |
|||
"code": "AI_ERROR", |
|||
"message": error.localizedDescription |
|||
]) |
|||
|
|||
// 标记AI流式输出已完成 |
|||
service.isAiStreaming = false |
|||
service.currentAiTask = nil |
|||
} |
|||
|
|||
func onFunctionCall(call: [String: Any]) { |
|||
guard let service = service else { return } |
|||
|
|||
if let name = call["name"] as? String { |
|||
service.sendEvent("function_call", [ |
|||
"name": name, |
|||
"arguments": call |
|||
]) |
|||
|
|||
if name == "exit_interaction" { |
|||
service.stopRecognition() |
|||
} |
|||
} |
|||
} |
|||
|
|||
func onFunctionCallResult(functionCall: [String: Any], functionCallResult: [String: Any]) { |
|||
guard let service = service else { return } |
|||
|
|||
service.sendEvent("function_call_result", [ |
|||
"function_call": functionCall, |
|||
"result": functionCallResult, |
|||
]) |
|||
} |
|||
} |
|||
|
|||
let callback = StreamCallbackBridge(self, responseBuilder: responseBuilder, speakResponse: speakResponse, displayText: displayText, hasImage: hasImage) |
|||
|
|||
self.openAIService?.sendMessageStream(messages: messagesWithSystemPrompt, callback: callback) |
|||
} |
|||
|
|||
// 保存任务引用并在全局队列中执行 |
|||
currentAiTask = workItem |
|||
DispatchQueue.global(qos: .userInitiated).async(execute: workItem) |
|||
} |
|||
|
|||
/** |
|||
* 加载最近的聊天历史记录 |
|||
*/ |
|||
private func loadChatHistory() { |
|||
// 清空当前历史记录 |
|||
historyMessages.removeAll() |
|||
|
|||
// 使用ChatStorageHelper直接获取最近消息 |
|||
guard let recentMessages = chatStorageHelper?.getRecentMessages(sessionId: sessionId, count: 10) else { |
|||
print("\(TAG): 没有找到历史记录") |
|||
return |
|||
} |
|||
|
|||
// 将消息添加到历史记录 |
|||
for message in recentMessages { |
|||
if let sender = message["sender"] as? String, |
|||
let content = message["message"] as? String { |
|||
|
|||
if sender == "user" { |
|||
if let userMessage = openAIService?.createUserMessage(text: content) { |
|||
addToHistoryMessages(userMessage) |
|||
} |
|||
} else if sender == "assistant" { |
|||
if let assistantMessage = openAIService?.createAssistantMessage(text: content) { |
|||
addToHistoryMessages(assistantMessage) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
print("\(TAG): 已加载\(recentMessages.count)条历史记录") |
|||
} |
|||
|
|||
/** |
|||
* 添加消息到历史记录,保持最近10条 |
|||
*/ |
|||
private func addToHistoryMessages(_ message: [String: Any]) { |
|||
// 添加新消息 |
|||
historyMessages.append(message) |
|||
|
|||
// 如果超过10条,删除最早的消息 |
|||
while historyMessages.count > 10 { |
|||
historyMessages.removeFirst() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* TTS播放函数 |
|||
* |
|||
* - Parameter text: 要播放的文本 |
|||
*/ |
|||
func speakText(text: String) { |
|||
if text.isEmpty { return } |
|||
|
|||
// 更新状态 |
|||
isTtsSpeaking = true |
|||
restartIdleCheck() // 状态变化,重启检测 |
|||
|
|||
// 播放文本 |
|||
_ = azureTtsHelper?.speakText(text) |
|||
} |
|||
|
|||
/** |
|||
* 停止TTS播放 |
|||
*/ |
|||
func stopTts() { |
|||
if isTtsSpeaking { |
|||
_ = azureTtsHelper?.stopSpeaking() |
|||
isTtsSpeaking = false |
|||
restartIdleCheck() // 状态变化,重启检测 |
|||
sendEvent("tts_stopped", ["status": "stopped"]) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 保存聊天消息 |
|||
*/ |
|||
private func saveChatMessage(userMessage: String, assistantMessage: String) { |
|||
DispatchQueue.global(qos: .background).async { [weak self] in |
|||
guard let self = self else { return } |
|||
|
|||
// 保存用户消息 |
|||
let userMessageId = self.chatStorageHelper?.saveMessage( |
|||
sessionId: self.sessionId, |
|||
message: userMessage, |
|||
sender: "user" |
|||
) ?? -1 |
|||
|
|||
if userMessageId != -1 { |
|||
// 保存AI回复 |
|||
let assistantMessageId = self.chatStorageHelper?.saveMessage( |
|||
sessionId: self.sessionId, |
|||
message: assistantMessage, |
|||
sender: "assistant" |
|||
) ?? -1 |
|||
|
|||
if assistantMessageId == -1 { |
|||
print("\(self.TAG): 保存助手消息失败") |
|||
} |
|||
} else { |
|||
print("\(self.TAG): 保存用户消息失败") |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 清除聊天历史 |
|||
*/ |
|||
func clearChatHistory(completion: @escaping (Bool) -> Void) { |
|||
DispatchQueue.global(qos: .background).async { [weak self] in |
|||
guard let self = self else { |
|||
DispatchQueue.main.async { |
|||
completion(false) |
|||
} |
|||
return |
|||
} |
|||
|
|||
// 清除指定会话的所有消息 |
|||
let success = self.chatStorageHelper?.deleteMessages(sessionId: self.sessionId) ?? false |
|||
|
|||
if success { |
|||
// 清空内存中的历史记录 |
|||
self.historyMessages.removeAll() |
|||
print("\(self.TAG): 聊天历史已清除") |
|||
} else { |
|||
print("\(self.TAG): 清除聊天历史失败") |
|||
} |
|||
|
|||
DispatchQueue.main.async { |
|||
completion(success) |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 发送事件 |
|||
*/ |
|||
private func sendEvent(_ eventName: String, _ data: [String: Any]) { |
|||
// 向所有监听器发送事件 |
|||
let allListeners = listeners.allObjects |
|||
for case let listener as AgentServiceListener in allListeners { |
|||
listener.onEvent(eventName: eventName, data: data) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 释放资源 |
|||
*/ |
|||
func dispose() { |
|||
stopRecognition() |
|||
stopTts() |
|||
stopAiStream() |
|||
stopIdleCheck() |
|||
|
|||
azureAsrHelper?.dispose() |
|||
azureTtsHelper?.dispose() |
|||
|
|||
// 清除所有监听器 |
|||
clearListeners() |
|||
|
|||
// 重置单例状态,以便下次使用时可以重新初始化 |
|||
isInitialized = false |
|||
|
|||
print("\(TAG): 代理服务资源已释放") |
|||
} |
|||
} |
|||
|
|||
// MARK: - AzureTtsHelper.TtsCallback |
|||
extension AgentService: AzureTtsHelper.TtsCallback { |
|||
func onSynthesisStarted() { |
|||
isTtsSpeaking = true |
|||
// 状态变化,重置空闲检测 |
|||
restartIdleCheck() |
|||
sendEvent("tts_started", ["status": "started"]) |
|||
} |
|||
|
|||
func onSynthesizing() { |
|||
// 可以在此添加TTS合成中的处理逻辑 |
|||
} |
|||
|
|||
func onSynthesisCompleted() { |
|||
isTtsSpeaking = false |
|||
// 状态变化,重启空闲检测 |
|||
restartIdleCheck() |
|||
sendEvent("tts_completed", ["status": "completed"]) |
|||
} |
|||
|
|||
func onSynthesisCanceled() { |
|||
isTtsSpeaking = false |
|||
// 状态变化,重启空闲检测 |
|||
restartIdleCheck() |
|||
sendEvent("tts_canceled", ["status": "canceled"]) |
|||
} |
|||
} |
|||
|
|||
// MARK: - 外部插件桥接器 |
|||
|
|||
/// 桥接OpenAIService插件 |
|||
class OpenAIServiceBridge { |
|||
private let service = OpenAIService() |
|||
|
|||
/// 初始化服务 |
|||
func initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String) { |
|||
service.initialize(apiKey: apiKey, baseUrl: baseUrl, model: model, mcpServer: mcpServer) |
|||
} |
|||
|
|||
/// 创建系统消息 |
|||
func createSystemMessage(text: String) -> [String: Any] { |
|||
return service.createSystemMessage(text) |
|||
} |
|||
|
|||
/// 创建用户消息 |
|||
func createUserMessage(text: String) -> [String: Any] { |
|||
return service.createUserMessage(text) |
|||
} |
|||
|
|||
/// 创建助手消息 |
|||
func createAssistantMessage(text: String) -> [String: Any] { |
|||
return service.createAssistantMessage(text) |
|||
} |
|||
|
|||
/// 创建带图片的用户消息 |
|||
func createUserMessageWithImage(text: String, imageBase64: String) -> [String: Any]? { |
|||
return service.createUserMessageWithImage(text: text, imageBase64: imageBase64) |
|||
} |
|||
|
|||
/// 文件转Base64 |
|||
func fileToBase64(filePath: String) -> String? { |
|||
return service.fileToBase64(filePath: filePath) |
|||
} |
|||
|
|||
/// 发送流式消息 |
|||
func sendMessageStream(messages: [[String: Any]], callback: OpenAIStreamCallback) { |
|||
service.sendMessageStream(messages: messages, callback: callback) |
|||
} |
|||
|
|||
/// 取消当前流 |
|||
func cancelCurrentStream() { |
|||
service.cancelCurrentStream() |
|||
} |
|||
} |
|||
|
|||
/// OpenAI流式回调协议 |
|||
@objc protocol OpenAIStreamCallback { |
|||
func onToken(token: String) |
|||
func onComplete() |
|||
func onError(error: Error) |
|||
func onFunctionCall(call: [String: Any]) |
|||
func onFunctionCallResult(functionCall: [String: Any], functionCallResult: [String: Any]) |
|||
} |
|||
|
|||
/// 桥接ChatStorageHelper插件 |
|||
class ChatStorageHelperBridge { |
|||
private let helper = ChatStorageHelper() |
|||
|
|||
/// 保存消息 |
|||
func saveMessage(sessionId: String, message: String, sender: String) -> Int64 { |
|||
return helper.saveMessage(sessionId: sessionId, message: message, sender: sender) |
|||
} |
|||
|
|||
/// 获取最近消息 |
|||
func getRecentMessages(sessionId: String, count: Int) -> [[String: Any]]? { |
|||
return helper.getRecentMessages(sessionId: sessionId, count: count) |
|||
} |
|||
|
|||
/// 删除消息 |
|||
func deleteMessages(sessionId: String) -> Bool { |
|||
return helper.deleteMessages(sessionId: sessionId) |
|||
} |
|||
} |
|||
@ -0,0 +1,797 @@ |
|||
import Foundation |
|||
import AVFoundation |
|||
import ble_service |
|||
import speech |
|||
|
|||
/// 代理服务事件监听器接口 |
|||
protocol AgentServiceListener { |
|||
/// 当事件发生时调用 |
|||
/// - Parameters: |
|||
/// - eventName: 事件名称 |
|||
/// - data: 事件数据 |
|||
func onEvent(eventName: String, data: [String: Any]) |
|||
} |
|||
|
|||
/// 代理服务实现类,负责语音识别、OpenAI对话和语音合成 |
|||
class AgentServiceImpl: NSObject { |
|||
// MARK: - 常量 |
|||
private let TAG = "AgentServiceImpl" |
|||
|
|||
// MARK: - 单例实现 |
|||
static let shared = AgentServiceImpl() |
|||
|
|||
// MARK: - 属性 |
|||
|
|||
// 监听器集合 |
|||
private var listeners = Set<AgentServiceListener>() |
|||
private let listenersLock = NSLock() |
|||
|
|||
// Azure 语音相关 |
|||
private var azureSpeechKey: String = "" |
|||
private var azureSpeechRegion: String = "" |
|||
private var isRecognizing: Bool = false |
|||
private var isSpeaking: Bool = false |
|||
|
|||
// 语音识别和合成服务 |
|||
private var azureAsrHelper: AzureAsrHelper? |
|||
private var azureTtsHelper: AzureTtsHelper? |
|||
|
|||
// OpenAI相关 |
|||
private var apiKey: String = "" |
|||
private var baseUrl: String = "https://api.openai.com" |
|||
private var model: String = "gpt-3.5-turbo" |
|||
private var systemPrompt: String = "" |
|||
private var chatHistory: [[String: Any]] = [] |
|||
|
|||
// MCP服务相关 |
|||
private var mcpServer: String = "" |
|||
|
|||
// 状态标志 |
|||
private var isInitialized: Bool = false |
|||
private var hasSpeechDetected: Bool = false |
|||
private var isAiStreaming: Bool = false |
|||
|
|||
// 空闲检测相关 |
|||
private var idleCheckTimer: Timer? |
|||
private let maxIdleSeconds: TimeInterval = 10 // 最大空闲秒数 |
|||
|
|||
// 音频播放器 |
|||
private var audioPlayer: AudioPlayer? |
|||
|
|||
// MARK: - 初始化 |
|||
|
|||
/// 私有初始化方法 |
|||
private override init() { |
|||
super.init() |
|||
|
|||
// 设置音频会话 |
|||
setupAudioSession() |
|||
|
|||
// 初始化音频播放器 |
|||
audioPlayer = AudioPlayer() |
|||
|
|||
// 初始化语音助手服务 |
|||
azureAsrHelper = AzureAsrHelper() |
|||
azureTtsHelper = AzureTtsHelper() |
|||
|
|||
// 设置BleService回调 |
|||
BleService.shared.setDelegate(self) |
|||
} |
|||
|
|||
// MARK: - 监听器管理 |
|||
|
|||
/// 添加事件监听器 |
|||
/// - Parameter listener: 要添加的监听器 |
|||
func addListener(_ listener: AgentServiceListener) { |
|||
listenersLock.lock() |
|||
listeners.insert(listener) |
|||
listenersLock.unlock() |
|||
} |
|||
|
|||
/// 移除事件监听器 |
|||
/// - Parameter listener: 要移除的监听器 |
|||
func removeListener(_ listener: AgentServiceListener) { |
|||
listenersLock.lock() |
|||
listeners.remove(listener) |
|||
listenersLock.unlock() |
|||
} |
|||
|
|||
/// 清除所有监听器 |
|||
func clearListeners() { |
|||
listenersLock.lock() |
|||
listeners.removeAll() |
|||
listenersLock.unlock() |
|||
} |
|||
|
|||
/// 向所有监听器发送事件 |
|||
/// - Parameters: |
|||
/// - eventName: 事件名称 |
|||
/// - data: 事件数据 |
|||
private func sendEvent(name eventName: String, data: [String: Any]) { |
|||
listenersLock.lock() |
|||
let currentListeners = self.listeners |
|||
listenersLock.unlock() |
|||
|
|||
for listener in currentListeners { |
|||
listener.onEvent(eventName: eventName, data: data) |
|||
} |
|||
} |
|||
|
|||
/// 发送错误事件 |
|||
/// - Parameter message: 错误信息 |
|||
private func sendError(_ message: String, code: String = "ERROR") { |
|||
sendEvent(name: "error", data: ["code": code, "message": message]) |
|||
} |
|||
|
|||
// MARK: - 初始化配置 |
|||
|
|||
/// 初始化系统提示词 |
|||
private func initSystemPrompt() { |
|||
systemPrompt = """ |
|||
你是小言,一个具备专业能力的智能助手,需根据用户场景灵活切换回答模式,确保服务精准高效。 |
|||
[系统提示词内容省略...] |
|||
""" |
|||
} |
|||
|
|||
/// 初始化配置 |
|||
/// - Parameter config: 配置参数 |
|||
/// - Returns: 是否初始化成功 |
|||
func initialize(config: [String: Any]) -> Bool { |
|||
if isInitialized { return true } |
|||
|
|||
// 设置OpenAI配置 |
|||
if let openaiApiKey = config["openaiApiKey"] as? String { |
|||
self.apiKey = openaiApiKey |
|||
} else { |
|||
sendError("OpenAI API密钥缺失", code: "CONFIG_ERROR") |
|||
return false |
|||
} |
|||
|
|||
if let openaiBaseUrl = config["openaiBaseUrl"] as? String { |
|||
self.baseUrl = openaiBaseUrl |
|||
} |
|||
|
|||
if let openaiModel = config["openaiModel"] as? String { |
|||
self.model = openaiModel |
|||
} |
|||
|
|||
// 设置系统提示词 |
|||
if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty { |
|||
self.systemPrompt = customSystemPrompt |
|||
} else { |
|||
initSystemPrompt() |
|||
} |
|||
|
|||
// 设置Azure语音配置 |
|||
if let azureSpeechKey = config["azureSpeechKey"] as? String { |
|||
self.azureSpeechKey = azureSpeechKey |
|||
} else { |
|||
sendError("Azure语音密钥缺失", code: "CONFIG_ERROR") |
|||
return false |
|||
} |
|||
|
|||
if let azureSpeechRegion = config["azureSpeechRegion"] as? String { |
|||
self.azureSpeechRegion = azureSpeechRegion |
|||
} else { |
|||
sendError("Azure语音区域缺失", code: "CONFIG_ERROR") |
|||
return false |
|||
} |
|||
|
|||
// 设置MCP服务 |
|||
if let mcpServer = config["mcpServer"] as? String { |
|||
self.mcpServer = mcpServer |
|||
} |
|||
|
|||
// 初始化Azure语音服务 |
|||
let asrInitSuccess = initializeAzureSpeech() |
|||
|
|||
isInitialized = asrInitSuccess |
|||
return asrInitSuccess |
|||
} |
|||
|
|||
// MARK: - 音频会话设置 |
|||
|
|||
/// 设置音频会话 |
|||
private func setupAudioSession() { |
|||
let audioSession = AVAudioSession.sharedInstance() |
|||
do { |
|||
try audioSession.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth]) |
|||
try audioSession.setActive(true, options: .notifyOthersOnDeactivation) |
|||
} catch { |
|||
sendError("设置音频会话失败: \(error.localizedDescription)") |
|||
} |
|||
} |
|||
|
|||
// MARK: - Azure 语音服务 |
|||
|
|||
/// 初始化Azure语音服务 |
|||
private func initializeAzureSpeech() -> Bool { |
|||
// 添加事件监听器 |
|||
azureAsrHelper?.addListener(self) |
|||
azureTtsHelper?.addListener(self) |
|||
|
|||
// 初始化ASR服务 |
|||
guard let asrSuccess = azureAsrHelper?.initialize( |
|||
subscriptionKey: azureSpeechKey, |
|||
region: azureSpeechRegion, |
|||
supportedLanguages: ["zh-CN"] |
|||
), asrSuccess else { |
|||
sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR") |
|||
return false |
|||
} |
|||
|
|||
// 初始化TTS服务 |
|||
guard let ttsSuccess = azureTtsHelper?.initialize( |
|||
ttsAppId: "", // Azure TTS不需要appId |
|||
ttsAppToken: azureSpeechKey, |
|||
ttsResource: azureSpeechRegion, |
|||
language: "zh-CN" |
|||
), ttsSuccess else { |
|||
sendError("初始化语音合成服务失败", code: "TTS_INIT_ERROR") |
|||
return false |
|||
} |
|||
|
|||
// 设置TTS语音 |
|||
_ = azureTtsHelper?.setVoice("zh-CN-XiaoxiaoNeural") |
|||
|
|||
return true |
|||
} |
|||
|
|||
// MARK: - 空闲检测 |
|||
|
|||
/// 启动空闲检测 |
|||
private func startIdleCheck() { |
|||
stopIdleCheck() // 先停止现有的检查 |
|||
|
|||
if !isRecognizing { return } |
|||
|
|||
// 创建一个新的定时器 |
|||
idleCheckTimer = Timer.scheduledTimer(withTimeInterval: maxIdleSeconds, repeats: false) { [weak self] _ in |
|||
guard let self = self else { return } |
|||
|
|||
if self.isRecognizing && !self.hasSpeechDetected && !self.isSpeaking && !self.isAiStreaming { |
|||
self.stopRecognition() |
|||
self.sendEvent(name: "auto_stop", data: [ |
|||
"reason": "idle_timeout", |
|||
"seconds": self.maxIdleSeconds |
|||
]) |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// 停止空闲检测 |
|||
private func stopIdleCheck() { |
|||
idleCheckTimer?.invalidate() |
|||
idleCheckTimer = nil |
|||
} |
|||
|
|||
/// 重启空闲检测 |
|||
private func restartIdleCheck() { |
|||
if isRecognizing { |
|||
startIdleCheck() |
|||
} |
|||
} |
|||
|
|||
// MARK: - 语音识别 |
|||
|
|||
/// 开始语音识别 |
|||
/// - Returns: 是否成功启动识别 |
|||
func startRecognition() -> Bool { |
|||
if !isInitialized { |
|||
sendError("服务未初始化", code: "NOT_INITIALIZED") |
|||
return false |
|||
} |
|||
|
|||
// 防止重复启动 |
|||
if isRecognizing { |
|||
stopRecognition() |
|||
} |
|||
|
|||
// 启动语音识别 |
|||
guard let success = azureAsrHelper?.startContinuousRecognition(), success else { |
|||
sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR") |
|||
return false |
|||
} |
|||
|
|||
return true |
|||
} |
|||
|
|||
/// 停止语音识别 |
|||
/// - Returns: 是否成功停止识别 |
|||
func stopRecognition() -> Bool { |
|||
if !isRecognizing { |
|||
return true |
|||
} |
|||
|
|||
// 停止语音识别 |
|||
guard let success = azureAsrHelper?.stopContinuousRecognition(), success else { |
|||
return false |
|||
} |
|||
|
|||
return true |
|||
} |
|||
|
|||
/// 推送音频数据 |
|||
/// - Parameter audioData: 音频数据 |
|||
/// - Returns: 是否成功推送 |
|||
func pushAudioData(_ audioData: Data) -> Bool { |
|||
if !isRecognizing || !isInitialized { |
|||
return false |
|||
} |
|||
|
|||
// 将音频数据推送到语音识别引擎 |
|||
return azureAsrHelper?.pushAudioData(audioData: audioData) ?? false |
|||
} |
|||
|
|||
// MARK: - 语音合成 |
|||
|
|||
/// 语音合成播放文本 |
|||
/// - Parameter text: 要播放的文本 |
|||
/// - Returns: 是否成功开始播放 |
|||
func speakText(_ text: String) -> Bool { |
|||
if !isInitialized { |
|||
sendError("服务未初始化", code: "NOT_INITIALIZED") |
|||
return false |
|||
} |
|||
|
|||
if text.isEmpty { |
|||
return false |
|||
} |
|||
|
|||
// 使用TTS服务进行语音合成 |
|||
return azureTtsHelper?.speakOnce(text) ?? false |
|||
} |
|||
|
|||
/// 停止语音合成 |
|||
/// - Returns: 是否成功停止合成 |
|||
func stopTts() -> Bool { |
|||
if !isSpeaking { |
|||
return true |
|||
} |
|||
|
|||
// 停止TTS服务 |
|||
return azureTtsHelper?.stop() ?? false |
|||
} |
|||
|
|||
/// 打断当前响应 |
|||
/// - Returns: 是否成功打断 |
|||
func interruptCurrentResponse() -> Bool { |
|||
var interrupted = false |
|||
|
|||
// 停止TTS播放 |
|||
if isSpeaking { |
|||
interrupted = stopTts() || interrupted |
|||
} |
|||
|
|||
// 停止AI流输出 |
|||
if isAiStreaming { |
|||
isAiStreaming = false |
|||
interrupted = true |
|||
} |
|||
|
|||
// 发送打断事件 |
|||
if interrupted { |
|||
sendEvent(name: "response_interrupted", data: ["status": "interrupted"]) |
|||
} |
|||
|
|||
return interrupted |
|||
} |
|||
|
|||
// MARK: - 文本处理 |
|||
|
|||
/// 处理文本输入 |
|||
/// - Parameters: |
|||
/// - text: 文本内容 |
|||
/// - speakResponse: 是否朗读响应 |
|||
/// - Returns: 是否成功处理 |
|||
func processTextInput(_ text: String, speakResponse: Bool) -> Bool { |
|||
if !isInitialized { |
|||
sendError("服务未初始化", code: "NOT_INITIALIZED") |
|||
return false |
|||
} |
|||
|
|||
if text.isEmpty { |
|||
sendError("文本输入不能为空", code: "EMPTY_TEXT") |
|||
return false |
|||
} |
|||
|
|||
// TODO: 实现与OpenAI的通信 |
|||
// 这里只是模拟处理 |
|||
|
|||
// 将用户消息添加到聊天历史 |
|||
let userMessage: [String: Any] = [ |
|||
"role": "user", |
|||
"content": text |
|||
] |
|||
chatHistory.append(userMessage) |
|||
|
|||
// 更新状态 |
|||
isAiStreaming = true |
|||
|
|||
// 模拟助手响应 |
|||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in |
|||
guard let self = self else { return } |
|||
|
|||
// 模拟流式输出 |
|||
let response = "这是对"\(text)"的回复。我是AI助手,很高兴为您服务。" |
|||
|
|||
// 逐词输出模拟 |
|||
let words = response.split(separator: " ") |
|||
for (index, word) in words.enumerated() { |
|||
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) { [weak self] in |
|||
guard let self = self, self.isAiStreaming else { return } |
|||
self.sendEvent(name: "assistant_token", data: ["token": String(word) + " "]) |
|||
|
|||
// 如果需要语音播报,则合成语音 |
|||
if speakResponse { |
|||
self.azureTtsHelper?.speakStream(String(word) + " ") |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 完整响应 |
|||
DispatchQueue.main.asyncAfter(deadline: .now() + Double(words.count) * 0.1 + 0.2) { [weak self] in |
|||
guard let self = self else { return } |
|||
|
|||
// 结束AI流式输出 |
|||
self.isAiStreaming = false |
|||
|
|||
// 发送完整响应 |
|||
self.sendEvent(name: "assistant_response", data: ["text": response]) |
|||
|
|||
// 将助手消息添加到聊天历史 |
|||
let assistantMessage: [String: Any] = [ |
|||
"role": "assistant", |
|||
"content": response |
|||
] |
|||
self.chatHistory.append(assistantMessage) |
|||
|
|||
// 如果需要语音播报,则完成语音流 |
|||
if speakResponse { |
|||
self.azureTtsHelper?.flushStream() |
|||
} |
|||
} |
|||
} |
|||
|
|||
return true |
|||
} |
|||
|
|||
// MARK: - 图片处理 |
|||
|
|||
/// 处理图片输入 |
|||
/// - Parameters: |
|||
/// - imagePath: 图片路径 |
|||
/// - text: 文本描述 |
|||
/// - speakResponse: 是否朗读响应 |
|||
/// - Returns: 是否成功处理 |
|||
func processImageInput(imagePath: String, text: String, speakResponse: Bool) -> Bool { |
|||
if !isInitialized { |
|||
sendError("服务未初始化", code: "NOT_INITIALIZED") |
|||
return false |
|||
} |
|||
|
|||
if imagePath.isEmpty { |
|||
sendError("图片路径不能为空", code: "EMPTY_IMAGE_PATH") |
|||
return false |
|||
} |
|||
|
|||
// 发送图片处理中事件 |
|||
sendEvent(name: "image_processing", data: [:]) |
|||
|
|||
// TODO: 实现与OpenAI Vision API的通信 |
|||
// 这里只是模拟处理 |
|||
|
|||
// 模拟图片处理 |
|||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in |
|||
guard let self = self else { return } |
|||
|
|||
// 图片就绪事件 |
|||
self.sendEvent(name: "image_ready", data: [:]) |
|||
|
|||
// 更新状态 |
|||
self.isAiStreaming = true |
|||
|
|||
// 模拟AI分析结果 |
|||
let imageDescription = "这是一张图片分析结果。" |
|||
let additionalText = text.isEmpty ? "" : "关于您提问的"\(text)"," |
|||
let response = "\(imageDescription)\(additionalText)我可以看到这是一张图片。" |
|||
|
|||
// 发送AI助手token流 |
|||
let words = response.split(separator: " ") |
|||
for (index, word) in words.enumerated() { |
|||
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) { [weak self] in |
|||
guard let self = self, self.isAiStreaming else { return } |
|||
|
|||
let token = String(word) + " " |
|||
self.sendEvent(name: "assistant_token", data: ["token": token]) |
|||
|
|||
// 如果需要语音播报,则合成语音 |
|||
if speakResponse { |
|||
self.azureTtsHelper?.speakStream(token) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 发送完整响应 |
|||
DispatchQueue.main.asyncAfter(deadline: .now() + Double(words.count) * 0.1 + 0.2) { [weak self] in |
|||
guard let self = self else { return } |
|||
|
|||
// 结束AI流式输出 |
|||
self.isAiStreaming = false |
|||
|
|||
self.sendEvent(name: "assistant_response", data: [ |
|||
"text": response, |
|||
"userInput": text.isEmpty ? "[图片]" : text, |
|||
"hasImage": true |
|||
]) |
|||
|
|||
// 如果需要语音播报,则完成语音 |
|||
if speakResponse { |
|||
self.azureTtsHelper?.flushStream() |
|||
} |
|||
} |
|||
} |
|||
|
|||
return true |
|||
} |
|||
|
|||
// MARK: - 聊天历史管理 |
|||
|
|||
/// 清除聊天历史 |
|||
/// - Returns: 是否成功清除 |
|||
func clearChatHistory() -> Bool { |
|||
chatHistory.removeAll() |
|||
return true |
|||
} |
|||
|
|||
// MARK: - 资源释放 |
|||
|
|||
/// 释放资源 |
|||
/// - Returns: 是否成功释放 |
|||
func dispose() -> Bool { |
|||
// 停止语音识别 |
|||
if isRecognizing { |
|||
stopRecognition() |
|||
} |
|||
|
|||
// 停止语音合成 |
|||
if isSpeaking { |
|||
stopTts() |
|||
} |
|||
|
|||
// 停止空闲检测 |
|||
stopIdleCheck() |
|||
|
|||
// 移除BleService代理 |
|||
BleService.shared.setDelegate(nil) |
|||
|
|||
// 释放Azure Speech资源 |
|||
azureAsrHelper?.removeListener(self) |
|||
azureTtsHelper?.removeListener(self) |
|||
azureAsrHelper?.dispose() |
|||
azureTtsHelper?.dispose() |
|||
|
|||
// 释放音频播放器 |
|||
audioPlayer = nil |
|||
|
|||
// 清除监听器 |
|||
clearListeners() |
|||
|
|||
isInitialized = false |
|||
|
|||
return true |
|||
} |
|||
} |
|||
|
|||
// MARK: - AsrEventListener 实现 |
|||
extension AgentServiceImpl: AsrEventListener { |
|||
func onEvent(_ event: AsrEvent) { |
|||
switch event.type { |
|||
case .sessionStarted: |
|||
sendEvent(name: "recognition_started", data: [:]) |
|||
isRecognizing = true |
|||
startIdleCheck() |
|||
audioPlayer?.playStartSound() |
|||
|
|||
case .sessionStopped: |
|||
sendEvent(name: "recognition_stopped", data: [:]) |
|||
isRecognizing = false |
|||
stopIdleCheck() |
|||
audioPlayer?.playStopSound() |
|||
|
|||
case .recognizing: |
|||
if let text = event.params["text"] as? String { |
|||
// 检测到语音,更新状态 |
|||
let previousHasSpeech = hasSpeechDetected |
|||
hasSpeechDetected = true |
|||
|
|||
if !previousHasSpeech { |
|||
restartIdleCheck() |
|||
} |
|||
|
|||
var data: [String: Any] = ["text": text] |
|||
if let language = event.params["language"] as? String, !language.isEmpty { |
|||
data["language"] = language |
|||
} |
|||
|
|||
sendEvent(name: "recognizing", data: data) |
|||
|
|||
// 如果TTS正在播放或AI正在生成,则触发打断 |
|||
if isSpeaking || isAiStreaming { |
|||
interruptCurrentResponse() |
|||
} |
|||
} |
|||
|
|||
case .recognized: |
|||
if let text = event.params["text"] as? String, !text.isEmpty { |
|||
var data: [String: Any] = ["text": text] |
|||
if let language = event.params["language"] as? String, !language.isEmpty { |
|||
data["language"] = language |
|||
} |
|||
|
|||
sendEvent(name: "recognition_result", data: data) |
|||
|
|||
// 处理识别结果,发送到OpenAI |
|||
processTextInput(text, speakResponse: true) |
|||
} |
|||
|
|||
// 重置状态,继续识别 |
|||
let previousHasSpeech = hasSpeechDetected |
|||
hasSpeechDetected = false |
|||
|
|||
if previousHasSpeech { |
|||
restartIdleCheck() |
|||
} |
|||
|
|||
case .canceled: |
|||
var data: [String: Any] = [:] |
|||
if let reason = event.params["reason"] as? String { |
|||
data["reason"] = reason |
|||
} |
|||
if let errorDetails = event.params["errorDetails"] as? String { |
|||
data["details"] = errorDetails |
|||
} |
|||
|
|||
sendEvent(name: "recognition_canceled", data: data) |
|||
isRecognizing = false |
|||
stopIdleCheck() |
|||
|
|||
case .error: |
|||
var data: [String: Any] = [:] |
|||
if let errorCode = event.params["errorCode"] as? String { |
|||
data["errorCode"] = errorCode |
|||
} |
|||
if let message = event.params["message"] as? String { |
|||
data["message"] = message |
|||
} else if let error = event.params["error"] as? String { |
|||
data["message"] = error |
|||
} else { |
|||
data["message"] = "未知错误" |
|||
} |
|||
|
|||
sendEvent(name: "recognition_error", data: data) |
|||
isRecognizing = false |
|||
stopIdleCheck() |
|||
|
|||
default: |
|||
break |
|||
} |
|||
} |
|||
} |
|||
|
|||
// MARK: - TtsEventListener 实现 |
|||
extension AgentServiceImpl: TtsEventListener { |
|||
func onEvent(_ event: TtsEvent) { |
|||
switch event.type { |
|||
case .synthesisStarted: |
|||
isSpeaking = true |
|||
restartIdleCheck() |
|||
sendEvent(name: "tts_started", data: [:]) |
|||
|
|||
case .synthesisCompleted: |
|||
isSpeaking = false |
|||
restartIdleCheck() |
|||
sendEvent(name: "tts_completed", data: [:]) |
|||
|
|||
case .synthesisCanceled: |
|||
isSpeaking = false |
|||
restartIdleCheck() |
|||
|
|||
var data: [String: Any] = [:] |
|||
if let reason = event.params["reason"] as? String { |
|||
data["reason"] = reason |
|||
} |
|||
if let errorDetails = event.params["errorDetails"] as? String { |
|||
data["details"] = errorDetails |
|||
} |
|||
|
|||
sendEvent(name: "tts_canceled", data: data) |
|||
|
|||
case .error: |
|||
isSpeaking = false |
|||
restartIdleCheck() |
|||
|
|||
var data: [String: Any] = [:] |
|||
if let errorCode = event.params["errorCode"] as? String { |
|||
data["errorCode"] = errorCode |
|||
} |
|||
if let errorMessage = event.params["errorMessage"] as? String { |
|||
data["message"] = errorMessage |
|||
} else { |
|||
data["message"] = "TTS错误" |
|||
} |
|||
|
|||
sendEvent(name: "error", data: data) |
|||
|
|||
default: |
|||
break |
|||
} |
|||
} |
|||
} |
|||
|
|||
// MARK: - BleService.Callback 实现 |
|||
extension AgentServiceImpl: BleService.Callback { |
|||
func onScanResult(devices: [[String: Any]]) { |
|||
// 不处理扫描结果 |
|||
} |
|||
|
|||
func onConnectionStateChanged(state: Int) { |
|||
// 可以在这里处理连接状态变化,例如通知UI更新 |
|||
} |
|||
|
|||
func onAudioDataReceived(data: Data) { |
|||
// 直接将音频数据推送到Azure Speech进行识别 |
|||
pushAudioData(data) |
|||
} |
|||
|
|||
func onWakeupSignalReceived() { |
|||
NSLog("\(TAG): 收到唤醒信号") |
|||
|
|||
// 停止当前TTS,避免冲突 |
|||
stopTts() |
|||
|
|||
// 如果已经在识别,则重新启动 |
|||
stopRecognition() |
|||
startRecognition() |
|||
} |
|||
|
|||
func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) { |
|||
// 可以处理设备信息,例如电量、设备名称等 |
|||
} |
|||
} |
|||
|
|||
// MARK: - 音频播放器 |
|||
|
|||
/// 简单的音频播放器,用于播放提示音 |
|||
class AudioPlayer { |
|||
private var player: AVAudioPlayer? |
|||
|
|||
/// 播放开始识别提示音 |
|||
func playStartSound() { |
|||
playSound(named: "start") |
|||
} |
|||
|
|||
/// 播放停止识别提示音 |
|||
func playStopSound() { |
|||
playSound(named: "stop") |
|||
} |
|||
|
|||
/// 播放音频文件 |
|||
/// - Parameter named: 音频文件名 |
|||
private func playSound(named: String) { |
|||
guard let path = Bundle.main.path(forResource: named, ofType: "wav") else { |
|||
print("找不到音频文件: \(named).wav") |
|||
return |
|||
} |
|||
|
|||
let url = URL(fileURLWithPath: path) |
|||
|
|||
do { |
|||
player = try AVAudioPlayer(contentsOf: url) |
|||
player?.prepareToPlay() |
|||
player?.play() |
|||
} catch { |
|||
print("播放音频失败: \(error.localizedDescription)") |
|||
} |
|||
} |
|||
} |
|||
@ -1,11 +0,0 @@ |
|||
import Foundation |
|||
|
|||
/// 代理服务事件监听器协议 |
|||
/// 用于接收代理服务发送的各类事件 |
|||
protocol AgentServiceListener: AnyObject { |
|||
/// 事件回调 |
|||
/// - Parameters: |
|||
/// - eventName: 事件名称 |
|||
/// - data: 事件数据 |
|||
func onEvent(eventName: String, data: [String: Any]) |
|||
} |
|||
@ -0,0 +1,227 @@ |
|||
import Flutter |
|||
import UIKit |
|||
import AVFoundation |
|||
|
|||
/// 代理服务插件,负责连接Flutter端与原生服务 |
|||
public class AgentServicePlugin: NSObject, FlutterPlugin { |
|||
private let TAG = "AgentServicePlugin" |
|||
private var eventSink: FlutterEventSink? |
|||
|
|||
// MARK: - 插件注册 |
|||
|
|||
/// 插件注册方法 |
|||
public static func register(with registrar: FlutterPluginRegistrar) { |
|||
let methodChannel = FlutterMethodChannel(name: "com.yunqiinnovation.agent_service", binaryMessenger: registrar.messenger()) |
|||
let eventChannel = FlutterEventChannel(name: "com.yunqiinnovation.agent_service/events", binaryMessenger: registrar.messenger()) |
|||
|
|||
let instance = AgentServicePlugin() |
|||
|
|||
registrar.addMethodCallDelegate(instance, channel: methodChannel) |
|||
eventChannel.setStreamHandler(instance) |
|||
|
|||
// 添加监听器 |
|||
AgentServiceImpl.shared.addListener(instance) |
|||
|
|||
NSLog("\(instance.TAG): 插件已加载") |
|||
} |
|||
|
|||
// MARK: - 方法处理 |
|||
|
|||
/// 处理来自Flutter的方法调用 |
|||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
|||
NSLog("\(TAG): 收到方法调用 - \(call.method)") |
|||
|
|||
switch call.method { |
|||
case "initialize": |
|||
handleInitialize(call, result: result) |
|||
case "startAgentService": |
|||
handleStartAgentService(call, result: result) |
|||
case "stopAgentService": |
|||
handleStopAgentService(result: result) |
|||
case "startConversation": |
|||
handleStartConversation(result: result) |
|||
case "stopConversation": |
|||
handleStopConversation(result: result) |
|||
case "processTextInput": |
|||
handleProcessTextInput(call, result: result) |
|||
case "speakText": |
|||
handleSpeakText(call, result: result) |
|||
case "stopTts": |
|||
handleStopTts(result: result) |
|||
case "clearChatHistory": |
|||
handleClearChatHistory(result: result) |
|||
case "interruptCurrentResponse": |
|||
handleInterruptCurrentResponse(result: result) |
|||
case "processImageInput": |
|||
handleProcessImageInput(call, result: result) |
|||
case "dispose": |
|||
handleDispose(result: result) |
|||
default: |
|||
result(FlutterMethodNotImplemented) |
|||
} |
|||
} |
|||
|
|||
// MARK: - 方法实现 |
|||
|
|||
private func handleInitialize(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
|||
guard let args = call.arguments as? [String: Any], |
|||
let config = args["config"] as? [String: Any] else { |
|||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "参数无效", details: nil)) |
|||
return |
|||
} |
|||
|
|||
// 初始化AgentServiceImpl |
|||
let success = AgentServiceImpl.shared.initialize(config: config) |
|||
result(success) |
|||
} |
|||
|
|||
private func handleStartAgentService(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
|||
guard let args = call.arguments as? [String: Any], |
|||
let config = args["config"] as? [String: Any] else { |
|||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "参数无效", details: nil)) |
|||
return |
|||
} |
|||
|
|||
NSLog("\(TAG): 启动AgentService,配置参数: \(config)") |
|||
|
|||
// 初始化AgentServiceImpl |
|||
let success = AgentServiceImpl.shared.initialize(config: config) |
|||
|
|||
if success { |
|||
result(true) |
|||
} else { |
|||
result(FlutterError(code: "SERVICE_START_ERROR", message: "AgentService启动失败", details: nil)) |
|||
} |
|||
} |
|||
|
|||
private func handleStopAgentService(result: @escaping FlutterResult) { |
|||
NSLog("\(TAG): 停止AgentService") |
|||
let success = AgentServiceImpl.shared.dispose() |
|||
result(success) |
|||
} |
|||
|
|||
private func handleStartConversation(result: @escaping FlutterResult) { |
|||
NSLog("\(TAG): 开始对话") |
|||
let success = AgentServiceImpl.shared.startRecognition() |
|||
result(success) |
|||
} |
|||
|
|||
private func handleStopConversation(result: @escaping FlutterResult) { |
|||
NSLog("\(TAG): 停止对话") |
|||
let success = AgentServiceImpl.shared.stopRecognition() |
|||
result(success) |
|||
} |
|||
|
|||
private func handleProcessTextInput(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
|||
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 speakResponse = args["speakResponse"] as? Bool ?? false |
|||
|
|||
NSLog("\(TAG): 处理文本输入 - \(text)") |
|||
let success = AgentServiceImpl.shared.processTextInput(text, speakResponse: speakResponse) |
|||
result(success) |
|||
} |
|||
|
|||
private func handleSpeakText(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
|||
guard let args = call.arguments as? [String: Any], |
|||
let text = args["text"] as? String else { |
|||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil)) |
|||
return |
|||
} |
|||
|
|||
NSLog("\(TAG): 语音合成 - \(text)") |
|||
let success = AgentServiceImpl.shared.speakText(text) |
|||
result(success) |
|||
} |
|||
|
|||
private func handleStopTts(result: @escaping FlutterResult) { |
|||
NSLog("\(TAG): 停止语音合成") |
|||
let success = AgentServiceImpl.shared.stopTts() |
|||
result(success) |
|||
} |
|||
|
|||
private func handleClearChatHistory(result: @escaping FlutterResult) { |
|||
NSLog("\(TAG): 清除聊天历史") |
|||
let success = AgentServiceImpl.shared.clearChatHistory() |
|||
result(success) |
|||
} |
|||
|
|||
private func handleInterruptCurrentResponse(result: @escaping FlutterResult) { |
|||
NSLog("\(TAG): 中断当前响应") |
|||
let success = AgentServiceImpl.shared.interruptCurrentResponse() |
|||
result(success) |
|||
} |
|||
|
|||
private func handleProcessImageInput(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
|||
guard let args = call.arguments as? [String: Any], |
|||
let imagePath = args["imagePath"] as? String else { |
|||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "图片路径不能为空", details: nil)) |
|||
return |
|||
} |
|||
let text = args["text"] as? String ?? "" |
|||
let speakResponse = args["speakResponse"] as? Bool ?? false |
|||
|
|||
NSLog("\(TAG): 处理图片输入 - \(imagePath)") |
|||
let success = AgentServiceImpl.shared.processImageInput(imagePath: imagePath, text: text, speakResponse: speakResponse) |
|||
result(success) |
|||
} |
|||
|
|||
private func handleDispose(result: @escaping FlutterResult) { |
|||
NSLog("\(TAG): 释放资源") |
|||
|
|||
// 移除监听器并释放资源 |
|||
AgentServiceImpl.shared.removeListener(self) |
|||
let success = AgentServiceImpl.shared.dispose() |
|||
|
|||
result(success) |
|||
} |
|||
|
|||
// MARK: - 实例析构 |
|||
|
|||
deinit { |
|||
// 确保在实例销毁时移除监听器 |
|||
AgentServiceImpl.shared.removeListener(self) |
|||
NSLog("\(TAG): 插件已卸载") |
|||
} |
|||
} |
|||
|
|||
// MARK: - AgentServiceListener 实现 |
|||
extension AgentServicePlugin: AgentServiceListener { |
|||
public func onEvent(eventName: String, data: [String: Any]) { |
|||
guard let eventSink = eventSink else { return } |
|||
|
|||
// 将事件转换为JSON字符串 |
|||
let eventData: [String: Any] = [ |
|||
"event": eventName, |
|||
"data": data |
|||
] |
|||
|
|||
do { |
|||
let jsonData = try JSONSerialization.data(withJSONObject: eventData, options: []) |
|||
if let jsonString = String(data: jsonData, encoding: .utf8) { |
|||
// 确保在主线程发送事件到Flutter |
|||
DispatchQueue.main.async { |
|||
eventSink(jsonString) |
|||
} |
|||
} |
|||
} catch { |
|||
NSLog("\(TAG): 错误 - 事件数据序列化失败: \(error.localizedDescription)") |
|||
} |
|||
} |
|||
} |
|||
|
|||
// MARK: - FlutterStreamHandler 实现 |
|||
extension AgentServicePlugin: FlutterStreamHandler { |
|||
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { |
|||
self.eventSink = events |
|||
return nil |
|||
} |
|||
|
|||
public func onCancel(withArguments arguments: Any?) -> FlutterError? { |
|||
self.eventSink = nil |
|||
return nil |
|||
} |
|||
} |
|||
@ -1,259 +0,0 @@ |
|||
import Flutter |
|||
import UIKit |
|||
import open_ai_service |
|||
import chat_storage |
|||
|
|||
/// Swift版Agent Service插件 |
|||
/// 处理Flutter与原生层的通信 |
|||
public class SwiftAgentServicePlugin: NSObject, FlutterPlugin { |
|||
// Flutter方法通道 |
|||
private var methodChannel: FlutterMethodChannel? |
|||
|
|||
// Flutter事件通道 |
|||
private var eventChannel: FlutterEventChannel? |
|||
|
|||
// 事件sink,用于向Flutter发送事件 |
|||
private var eventSink: FlutterEventSink? |
|||
|
|||
// 核心服务实例 |
|||
private var agentService: AgentService? |
|||
|
|||
/// 注册插件 |
|||
public static func register(with registrar: FlutterPluginRegistrar) { |
|||
let instance = SwiftAgentServicePlugin() |
|||
|
|||
// 初始化方法通道 |
|||
let methodChannel = FlutterMethodChannel(name: "com.yunqiinnovation.agent_service", binaryMessenger: registrar.messenger()) |
|||
registrar.addMethodCallDelegate(instance, channel: methodChannel) |
|||
instance.methodChannel = methodChannel |
|||
|
|||
// 初始化事件通道 |
|||
let eventChannel = FlutterEventChannel(name: "com.yunqiinnovation.agent_service/events", binaryMessenger: registrar.messenger()) |
|||
eventChannel.setStreamHandler(instance) |
|||
instance.eventChannel = eventChannel |
|||
} |
|||
|
|||
/// 处理方法调用 |
|||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
|||
switch call.method { |
|||
case "initialize": |
|||
handleInitialize(call, result) |
|||
|
|||
case "startConversation": |
|||
handleStartConversation(result) |
|||
|
|||
case "stopConversation": |
|||
handleStopConversation(result) |
|||
|
|||
case "processTextInput": |
|||
handleProcessTextInput(call, result) |
|||
|
|||
case "speakText": |
|||
handleSpeakText(call, result) |
|||
|
|||
case "stopTts": |
|||
handleStopTts(result) |
|||
|
|||
case "clearChatHistory": |
|||
handleClearChatHistory(result) |
|||
|
|||
case "interruptCurrentResponse": |
|||
handleInterruptCurrentResponse(result) |
|||
|
|||
case "processImageInput": |
|||
handleProcessImageInput(call, result) |
|||
|
|||
case "dispose": |
|||
handleDispose(result) |
|||
|
|||
default: |
|||
result(FlutterMethodNotImplemented) |
|||
} |
|||
} |
|||
|
|||
// MARK: - 方法处理 |
|||
|
|||
/// 初始化 |
|||
private func handleInitialize(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
|||
guard let args = call.arguments as? [String: Any], |
|||
let config = args["config"] as? [String: Any] else { |
|||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "缺少配置参数", details: nil)) |
|||
return |
|||
} |
|||
|
|||
// 初始化代理服务 |
|||
if agentService == nil { |
|||
agentService = AgentService() |
|||
|
|||
// 设置事件监听器 |
|||
agentService?.setListener(self) |
|||
} |
|||
|
|||
// 执行初始化 |
|||
let success = agentService?.initialize(config: config) ?? false |
|||
result(success) |
|||
} |
|||
|
|||
/// 开始对话 |
|||
private func handleStartConversation(_ result: @escaping FlutterResult) { |
|||
guard let service = agentService else { |
|||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|||
return |
|||
} |
|||
|
|||
let success = service.startRecognition() |
|||
result(success) |
|||
} |
|||
|
|||
/// 停止对话 |
|||
private func handleStopConversation(_ result: @escaping FlutterResult) { |
|||
guard let service = agentService else { |
|||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|||
return |
|||
} |
|||
|
|||
service.stopRecognition() |
|||
result(true) |
|||
} |
|||
|
|||
/// 处理文本输入 |
|||
private func handleProcessTextInput(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
|||
guard let service = agentService else { |
|||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|||
return |
|||
} |
|||
|
|||
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 speakResponse = (args["speakResponse"] as? Bool) ?? false |
|||
let success = service.processTextInput(text: text, speakResponse: speakResponse) |
|||
result(success) |
|||
} |
|||
|
|||
/// 朗读文本 |
|||
private func handleSpeakText(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
|||
guard let service = agentService else { |
|||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|||
return |
|||
} |
|||
|
|||
guard let args = call.arguments as? [String: Any], |
|||
let text = args["text"] as? String else { |
|||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的文本参数", details: nil)) |
|||
return |
|||
} |
|||
|
|||
service.speakText(text: text) |
|||
result(true) |
|||
} |
|||
|
|||
/// 停止语音合成 |
|||
private func handleStopTts(_ result: @escaping FlutterResult) { |
|||
guard let service = agentService else { |
|||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|||
return |
|||
} |
|||
|
|||
service.stopTts() |
|||
result(true) |
|||
} |
|||
|
|||
/// 清除聊天历史 |
|||
private func handleClearChatHistory(_ result: @escaping FlutterResult) { |
|||
guard let service = agentService else { |
|||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|||
return |
|||
} |
|||
|
|||
service.clearChatHistory { success in |
|||
result(success) |
|||
} |
|||
} |
|||
|
|||
/// 中断当前响应 |
|||
private func handleInterruptCurrentResponse(_ result: @escaping FlutterResult) { |
|||
guard let service = agentService else { |
|||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|||
return |
|||
} |
|||
|
|||
service.interruptCurrentResponse() |
|||
result(true) |
|||
} |
|||
|
|||
/// 处理图片输入 |
|||
private func handleProcessImageInput(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
|||
guard let service = agentService else { |
|||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|||
return |
|||
} |
|||
|
|||
guard let args = call.arguments as? [String: Any], |
|||
let imagePath = args["imagePath"] as? String else { |
|||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的图片路径参数", details: nil)) |
|||
return |
|||
} |
|||
|
|||
let text = (args["text"] as? String) ?? "" |
|||
let speakResponse = (args["speakResponse"] as? Bool) ?? false |
|||
|
|||
let success = service.processImageInput(imagePath: imagePath, text: text, speakResponse: speakResponse) |
|||
result(success) |
|||
} |
|||
|
|||
/// 释放资源 |
|||
private func handleDispose(_ result: @escaping FlutterResult) { |
|||
guard let service = agentService else { |
|||
result(true) // 已经是nil,直接返回成功 |
|||
return |
|||
} |
|||
|
|||
service.dispose() |
|||
agentService = nil |
|||
|
|||
result(true) |
|||
} |
|||
} |
|||
|
|||
// MARK: - FlutterStreamHandler |
|||
extension SwiftAgentServicePlugin: FlutterStreamHandler { |
|||
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { |
|||
self.eventSink = events |
|||
return nil |
|||
} |
|||
|
|||
public func onCancel(withArguments arguments: Any?) -> FlutterError? { |
|||
self.eventSink = nil |
|||
return nil |
|||
} |
|||
} |
|||
|
|||
// MARK: - AgentServiceListener |
|||
extension SwiftAgentServicePlugin: AgentServiceListener { |
|||
func onEvent(eventName: String, data: [String: Any]) { |
|||
guard let sink = eventSink else { return } |
|||
|
|||
do { |
|||
// 构建事件JSON |
|||
let eventMap: [String: Any] = [ |
|||
"event": eventName, |
|||
"data": data |
|||
] |
|||
|
|||
// 将事件转换为JSON字符串 |
|||
let jsonData = try JSONSerialization.data(withJSONObject: eventMap, options: []) |
|||
if let jsonString = String(data: jsonData, encoding: .utf8) { |
|||
// 发送到Flutter |
|||
DispatchQueue.main.async { |
|||
sink(jsonString) |
|||
} |
|||
} |
|||
} catch { |
|||
print("事件序列化失败: \(error.localizedDescription)") |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
# |
|||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. |
|||
# Run `pod lib lint agent_service.podspec` to validate before publishing. |
|||
# |
|||
Pod::Spec.new do |s| |
|||
s.name = 'agent_service' |
|||
s.version = '0.0.1' |
|||
s.summary = 'A Flutter plugin for agent service with voice interaction capabilities.' |
|||
s.description = <<-DESC |
|||
A Flutter plugin that provides voice interaction, AI conversation and text-to-speech functionalities. |
|||
DESC |
|||
s.homepage = 'http://example.com' |
|||
s.license = { :file => '../LICENSE' } |
|||
s.author = { 'Your Company' => 'email@example.com' } |
|||
s.source = { :path => '.' } |
|||
s.source_files = 'Classes/**/*' |
|||
s.dependency 'Flutter' |
|||
s.dependency 'azure_speech' |
|||
s.dependency 'ble_service' |
|||
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' |
|||
end |
|||
@ -1,4 +1,17 @@ |
|||
//
|
|||
// AzureSpeechPlugin.h
|
|||
// azure_speech
|
|||
//
|
|||
// Created for azure_speech plugin compatibility.
|
|||
//
|
|||
|
|||
#import <Flutter/Flutter.h> |
|||
|
|||
@interface AzureSpeechPlugin : NSObject<FlutterPlugin> |
|||
/**
|
|||
* Azure Speech Plugin 头文件 |
|||
* |
|||
* 注:实际实现使用 Swift,此头文件仅用于 Flutter 框架兼容 |
|||
*/ |
|||
@interface AzureSpeechPlugin : NSObject <FlutterPlugin> |
|||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar; |
|||
@end |
|||
@ -1,12 +0,0 @@ |
|||
#import "AzureSpeechPlugin.h" |
|||
#if __has_include(<azure_speech/azure_speech-Swift.h>) |
|||
#import <azure_speech/azure_speech-Swift.h> |
|||
#else |
|||
#import "azure_speech-Swift.h" |
|||
#endif |
|||
|
|||
@implementation AzureSpeechPlugin |
|||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { |
|||
[SwiftAzureSpeechPlugin registerWithRegistrar:registrar]; |
|||
} |
|||
@end |
|||
@ -1,6 +0,0 @@ |
|||
#ifndef azure_speech_Bridging_Header_h |
|||
#define azure_speech_Bridging_Header_h |
|||
|
|||
#import <Foundation/Foundation.h> |
|||
|
|||
#endif /* azure_speech_Bridging_Header_h */ |
|||
@ -1,5 +0,0 @@ |
|||
#import <Foundation/Foundation.h> |
|||
|
|||
#import "AzureSpeechPlugin.h" |
|||
|
|||
// 添加其他公共头文件导出
|
|||
@ -0,0 +1,128 @@ |
|||
import Foundation |
|||
import SpeexKit |
|||
import os.log |
|||
|
|||
/// 基于杰理SpeexKit的Opus音频处理器 |
|||
@available(iOS 13.0, *) |
|||
class OpusAudioProcessor: NSObject { |
|||
|
|||
// MARK: - 回调代理协议 |
|||
protocol AudioDataCallback: AnyObject { |
|||
/// 接收到解码后的PCM音频数据 |
|||
func onAudioDataReceived(data: Data) |
|||
} |
|||
|
|||
// MARK: - 属性 |
|||
private var isDecodingActive = false |
|||
private weak var callback: AudioDataCallback? |
|||
private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "OpusAudioProcessor") |
|||
|
|||
// MARK: - 初始化 |
|||
override init() { |
|||
super.init() |
|||
|
|||
// 注册通知 - 这是SDK设计要求的 |
|||
NotificationCenter.default.addObserver( |
|||
self, |
|||
selector: #selector(onOpusDecodedData(_:)), |
|||
name: NSNotification.Name(rawValue: kOPUS_DECODE_DATA), |
|||
object: nil |
|||
) |
|||
|
|||
// 启用日志 |
|||
OpusUnit.opusIsLog(true) |
|||
} |
|||
|
|||
deinit { |
|||
stopDecoding() |
|||
NotificationCenter.default.removeObserver(self) |
|||
} |
|||
|
|||
// MARK: - 通知回调 |
|||
@objc private func onOpusDecodedData(_ notification: Notification) { |
|||
guard let pcmData = notification.object as? Data else { |
|||
os_log("解码回调收到无效数据", log: logger, type: .error) |
|||
return |
|||
} |
|||
|
|||
// 传递给回调 |
|||
DispatchQueue.main.async { [weak self] in |
|||
self?.callback?.onAudioDataReceived(data: pcmData) |
|||
} |
|||
} |
|||
|
|||
// MARK: - 公共接口 |
|||
|
|||
/// 设置回调 |
|||
func setCallback(_ callback: AudioDataCallback?) { |
|||
self.callback = callback |
|||
} |
|||
|
|||
/// 开始Opus解码 |
|||
func startDecoding(sampleRate: Int32 = 16000, channels: Int32 = 1, frameSize: Int32 = 640) -> Bool { |
|||
// 如果已经在解码,直接返回成功 |
|||
if isDecodingActive { |
|||
os_log("Opus解码器已经在运行", log: logger, type: .debug) |
|||
return true |
|||
} |
|||
|
|||
// 设置音频参数 |
|||
OpusUnit.opusSetSampleRate(Int32(sampleRate), kbps: 16000, channels: Int32(channels)) |
|||
|
|||
// 必须在后台线程启动解码器(SDK要求) |
|||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in |
|||
guard let self = self else { return } |
|||
|
|||
// 启动解码器 |
|||
let result = OpusUnit.opusDecoderRun() |
|||
if result == 0 { |
|||
os_log("Opus解码器启动成功: 采样率=%d Hz", log: self.logger, type: .info, sampleRate) |
|||
self.isDecodingActive = true |
|||
} else { |
|||
os_log("Opus解码器启动失败: 错误=%d", log: self.logger, type: .error, result) |
|||
} |
|||
} |
|||
|
|||
return true |
|||
} |
|||
|
|||
/// 停止Opus解码 |
|||
func stopDecoding() -> Bool { |
|||
if !isDecodingActive { |
|||
return false |
|||
} |
|||
|
|||
// 必须在后台线程停止解码器 |
|||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in |
|||
guard let self = self else { return } |
|||
|
|||
let result = OpusUnit.opusDecoderStop() |
|||
os_log("Opus解码器停止: %d", log: self.logger, type: .info, result) |
|||
self.isDecodingActive = false |
|||
} |
|||
|
|||
return true |
|||
} |
|||
|
|||
/// 处理Opus编码数据 |
|||
func processAudioData(_ data: Data) { |
|||
if !isDecodingActive { |
|||
os_log("Opus解码器未启动", log: logger, type: .error) |
|||
return |
|||
} |
|||
|
|||
// 将Opus数据送入解码器 |
|||
OpusUnit.opusWrite(data) |
|||
// 解码结果将通过通知回调给onOpusDecodedData方法 |
|||
} |
|||
|
|||
/// 判断解码器是否运行中 |
|||
func isDecoding() -> Bool { |
|||
return isDecodingActive |
|||
} |
|||
|
|||
/// 清理资源 |
|||
func cleanup() { |
|||
stopDecoding() |
|||
} |
|||
} |
|||
@ -0,0 +1,92 @@ |
|||
//
|
|||
// OpusUnit.h
|
|||
// QCY_Demo
|
|||
//
|
|||
// Created by 杰理科技 on 2021/7/5.
|
|||
// Copyright © 2021 杰理科技. All rights reserved.
|
|||
//
|
|||
|
|||
#import <Foundation/Foundation.h> |
|||
|
|||
NS_ASSUME_NONNULL_BEGIN |
|||
|
|||
/**
|
|||
* 通知:流式编码回调Opus数据 |
|||
* 通知数据类型:NSData(Opus) |
|||
*/ |
|||
extern NSString *kOPUS_ENCODE_DATA; |
|||
/**
|
|||
* 通知:流式解码回调PCM数据 |
|||
* 通知数据类型:NSData(PCM) |
|||
*/ |
|||
extern NSString *kOPUS_DECODE_DATA; |
|||
|
|||
|
|||
@interface OpusUnit : NSObject |
|||
|
|||
+ (void)opusIsLog:(BOOL)log; |
|||
|
|||
#pragma mark - Opus参数 |
|||
/**
|
|||
* 设置参数 |
|||
* @param rate 采样率 例如:48000、24000、16000、12000、8000(默认 16000) |
|||
* @param kbps 比特率 例如:16000、32000、64000(默认 16000) |
|||
* @param channel 声道 例如:1、2 (默认 1) |
|||
*/ |
|||
+(void)opusSetSampleRate:(int)rate Kbps:(int)kbps Channels:(int)channel; |
|||
|
|||
#pragma mark - Opus解码 |
|||
|
|||
/**
|
|||
* 直接【opus文件】转换成【pcm文件】 |
|||
* @param path_opus opus文件路径 |
|||
* @param path_pcm pcm文件路径 |
|||
*/ |
|||
+ (int)opusDecodeOPUS:(NSString *)path_opus PCM:(NSString *)path_pcm; |
|||
|
|||
/**
|
|||
* 流式解码【开启】 |
|||
*/ |
|||
+ (int)opusDecoderRun; |
|||
|
|||
/**
|
|||
* 输入Opus的数据 |
|||
* 通知监听“kOPUS_DECODE_DATA”获得解码后数据 |
|||
* 通知数据类型:NSNotification.object => NSData(PCM格式) |
|||
* @param data opus数据流(长度:1024) |
|||
*/ |
|||
+ (void)opusWriteData:(NSData*)data; |
|||
|
|||
/**
|
|||
* 流式解码【关闭】 |
|||
*/ |
|||
+ (int)opusDecoderStop; |
|||
|
|||
#pragma mark - Opus编码 |
|||
|
|||
/**
|
|||
* 【pcm文件】转换成【opus文件】 |
|||
*/ |
|||
+ (int)opusEncodePCM:(NSString *)path_pcm OPUS:(NSString *)path_opus; |
|||
|
|||
/**
|
|||
* 流式编码【开启】 |
|||
*/ |
|||
+ (int)opusEecoderRun; |
|||
|
|||
/**
|
|||
* 输入pcm的数据 |
|||
* 通知监听“kOPUS_DECODE_DATA”获得解码后数据 |
|||
* 通知数据类型:NSNotification.object => NSData(opus格式) |
|||
* @param data opus数据流(长度:1024) |
|||
*/ |
|||
+ (void)pcmWriteData:(NSData*)data; |
|||
|
|||
/**
|
|||
* 流式编码【关闭】 |
|||
*/ |
|||
+ (int)opusEncoderStop; |
|||
|
|||
@end |
|||
|
|||
NS_ASSUME_NONNULL_END |
|||
@ -0,0 +1,20 @@ |
|||
//
|
|||
// SpeexKit.h
|
|||
// SpeexKit
|
|||
//
|
|||
// Created by zhihui liang on 2018/8/20.
|
|||
// Copyright © 2018年 www.zh-jieli.com. All rights reserved.
|
|||
//
|
|||
|
|||
#import <UIKit/UIKit.h> |
|||
|
|||
//! Project version number for SpeexKit.
|
|||
FOUNDATION_EXPORT double SpeexKitVersionNumber; |
|||
|
|||
//! Project version string for SpeexKit.
|
|||
FOUNDATION_EXPORT const unsigned char SpeexKitVersionString[]; |
|||
|
|||
// In this header, you should import all the public headers of your framework using statements like #import <SpeexKit/PublicHeader.h>
|
|||
|
|||
#import <SpeexKit/SpeexUnit.h> |
|||
#import <SpeexKit/OpusUnit.h> |
|||
@ -0,0 +1,40 @@ |
|||
//
|
|||
// SpeexUnit.h
|
|||
// SpeexKit
|
|||
//
|
|||
// Created by zhihui liang on 2018/8/20.
|
|||
// Copyright © 2018年 www.zh-jieli.com. All rights reserved.
|
|||
//
|
|||
|
|||
#import <Foundation/Foundation.h> |
|||
|
|||
/**
|
|||
* 流式解码回调PCM数据 |
|||
*/ |
|||
extern NSString *kSPEEX_DECODE_DATA; //speex decode
|
|||
|
|||
@interface SpeexUnit : NSObject |
|||
|
|||
+(void)speexIsLog:(BOOL)log; |
|||
|
|||
/**
|
|||
* 直接【speex文件】转换成【pcm文件】 |
|||
*/ |
|||
+(int)speexDecodeSPX:(NSString *)path_spx PCM:(NSString *)path_pcm; |
|||
|
|||
/**
|
|||
* 流式解码【开启】 |
|||
*/ |
|||
+(int)speexDecoderRun; |
|||
|
|||
/**
|
|||
* 输入Speex的数据 |
|||
*/ |
|||
+(void)speexWriteData:(NSData*)data; |
|||
|
|||
/**
|
|||
* 流式解码【关闭】 |
|||
*/ |
|||
+(int)speexDecoderStop; |
|||
|
|||
@end |
|||
Binary file not shown.
@ -0,0 +1,6 @@ |
|||
framework module SpeexKit { |
|||
umbrella header "SpeexKit.h" |
|||
|
|||
export * |
|||
module * { export * } |
|||
} |
|||
Binary file not shown.
Loading…
Reference in new issue