diff --git a/.gitignore b/.gitignore index 15aa9f9..d2a9b22 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,8 @@ __pycache__/ # ===== Project-specific large/intermediate files ===== # Per-frame PNG dumps generated during GIF recording (intermediate only) gif-export/clips/*_frames/ +# Archive snapshots of clips — the GIFs themselves are already in clips/ +gif-export/clips*.zip # Texture backup created by black_to_transparent.py **/texture_*.backup.png diff --git a/33b906454de60431cd68612cfc3b8080.png b/33b906454de60431cd68612cfc3b8080.png new file mode 100644 index 0000000..3f09fba Binary files /dev/null and b/33b906454de60431cd68612cfc3b8080.png differ diff --git a/aa40cd4a-1bd6-4699-af37-73f0bca77308.jpeg b/aa40cd4a-1bd6-4699-af37-73f0bca77308.jpeg new file mode 100644 index 0000000..aba12c4 Binary files /dev/null and b/aa40cd4a-1bd6-4699-af37-73f0bca77308.jpeg differ diff --git a/avatar_flutter_app/.gitignore b/avatar_flutter_app/.gitignore index 3820a95..2d76bd5 100644 --- a/avatar_flutter_app/.gitignore +++ b/avatar_flutter_app/.gitignore @@ -43,3 +43,6 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +# Voice chat credentials (never commit) +lib/voice_chat/secrets.dart diff --git a/avatar_flutter_app/assets/voice/aec.model b/avatar_flutter_app/assets/voice/aec.model new file mode 100644 index 0000000..1c045ce Binary files /dev/null and b/avatar_flutter_app/assets/voice/aec.model differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/Podfile b/avatar_flutter_app/demo/SpeechDemoIOS/Podfile new file mode 100644 index 0000000..744e658 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/Podfile @@ -0,0 +1,12 @@ + +source 'https://github.com/CocoaPods/Specs.git' +source 'https://github.com/volcengine/volcengine-specs.git' + +platform :ios, '12.0' + +inhibit_all_warnings! + +target 'SpeechDemo' do + # Add cocoapods dependencyies + pod 'SPEECH_MODULE_NAME', 'SPEECH_MODULE_VERSION' +end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo.xcodeproj/project.pbxproj b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a4ab49a --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo.xcodeproj/project.pbxproj @@ -0,0 +1,1134 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 190DFD6BF614F40529F09E06 /* libPods-SpeechDemo-SpeechDemoUITests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1333EC06F468D2E52DF15EBB /* libPods-SpeechDemo-SpeechDemoUITests.a */; }; + 2B1F58FC2E166B85004F15B2 /* BiTTSViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B1F58FB2E166B85004F15B2 /* BiTTSViewController.m */; }; + 2B2296AC2E9DFBCE001B7D92 /* UniTTSViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B2296AB2E9DFBCE001B7D92 /* UniTTSViewController.m */; }; + 2B2296D92E9E4553001B7D92 /* aec.model in Resources */ = {isa = PBXBuildFile; fileRef = 2B2296D72E9E4553001B7D92 /* aec.model */; }; + 2B23098A2CB5796300535D5C /* TestAsrOfflineRtfViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B2309862CB5796300535D5C /* TestAsrOfflineRtfViewController.m */; }; + 2B23098B2CB5796300535D5C /* TestAsrViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B2309882CB5796300535D5C /* TestAsrViewController.m */; }; + 2B23098C2CB5796300535D5C /* TestAfpViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B2309842CB5796300535D5C /* TestAfpViewController.m */; }; + 2B24705C2A6A75D9003202D2 /* TtsNovelViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B24705A2A6A75D9003202D2 /* TtsNovelViewController.m */; }; + 2B59F5CA294317B500273846 /* SensitiveDefines.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B59F5C9294317B500273846 /* SensitiveDefines.m */; }; + 2B5FA6DA2CD90078007DE6C7 /* BigAsrViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B5FA6D92CD90078007DE6C7 /* BigAsrViewController.m */; }; + 2B6246862C50F668005B967D /* TtsNormalViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B6246852C50F668005B967D /* TtsNormalViewController.m */; }; + 2B6A762A2987AC0300A75DBA /* VadViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B6A76292987AC0300A75DBA /* VadViewController.m */; }; + 2B8084742DD5EBE70053DA63 /* DialogDelegateViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B8084732DD5EBE70053DA63 /* DialogDelegateViewController.m */; }; + 2B8084772DD60FA60053DA63 /* DialogMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B8084762DD60FA60053DA63 /* DialogMessage.m */; }; + 2B9A93F6272661FA00F48B85 /* VoiceCloneViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B9A93F5272661FA00F48B85 /* VoiceCloneViewController.m */; }; + 2B9A94382726807C00F48B85 /* VoiceConvViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B9A94372726807C00F48B85 /* VoiceConvViewController.m */; }; + 2B9A948D2726A16900F48B85 /* SettingViewDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B9A94892726A16900F48B85 /* SettingViewDelegate.m */; }; + 2B9A948E2726A16900F48B85 /* Settings.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B9A948B2726A16900F48B85 /* Settings.m */; }; + 2BACC1BB25078103009D850F /* AsrViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2BACC1BA25078103009D850F /* AsrViewController.m */; }; + 2BACC1BE250786C9009D850F /* CaptViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2BACC1BD250786C9009D850F /* CaptViewController.m */; }; + 2BB855D929F126A200B81519 /* AfpViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2BB855D629F126A200B81519 /* AfpViewController.m */; }; + 2BB855DA29F126A200B81519 /* AsrOfflineViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2BB855D829F126A200B81519 /* AsrOfflineViewController.m */; }; + 2BB855E02A0E5BF800B81519 /* AuViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2BB855DE2A0E5BF800B81519 /* AuViewController.m */; }; + 2BDC592029D2F3B800C90488 /* SettingsHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 2BDC591C29D2F3B700C90488 /* SettingsHelper.m */; }; + 2BDC592129D2F3B800C90488 /* SettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2BDC591D29D2F3B700C90488 /* SettingsViewController.m */; }; + 2BDC592329D31A0300C90488 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 2BDC592229D31A0300C90488 /* main.m */; }; + 2BDC592629D31A2100C90488 /* SpeechSettingItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2BDC592429D31A2100C90488 /* SpeechSettingItem.xib */; }; + 2BDC592E29D31A6800C90488 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2BDC592829D31A6800C90488 /* Assets.xcassets */; }; + 2BDC594229D31B6800C90488 /* FileUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 2BDC593E29D31B6700C90488 /* FileUtils.m */; }; + 2BDC594329D31B6800C90488 /* StreamRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 2BDC594029D31B6700C90488 /* StreamRecorder.m */; }; + 2BDC595129D3E3E100C90488 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2BDC594D29D3E3E100C90488 /* LaunchScreen.storyboard */; }; + 2BDC595229D3E3E100C90488 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2BDC594F29D3E3E100C90488 /* Main.storyboard */; }; + 2D5645BD6F9BABD19C6E8C07 /* libPods-SpeechDemo-SpeechDemoTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4FBCB4456D746604330A4433 /* libPods-SpeechDemo-SpeechDemoTests.a */; }; + 902248662A9606F1009B3646 /* KwsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 902248652A9606F1009B3646 /* KwsViewController.m */; }; + 90C447DB274CEC0A00C96ED6 /* FulllinkViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2BF772B326BBBB6B009FBB86 /* FulllinkViewController.m */; }; + 9CF40464ADBF4D8B1632B429 /* libPods-SpeechDemo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 30536C8DA34AD5C254CDEAD8 /* libPods-SpeechDemo.a */; }; + EC1FD748239A041000A4BE46 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = EC1FD747239A041000A4BE46 /* AppDelegate.m */; }; + EC1FD74B239A041000A4BE46 /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = EC1FD74A239A041000A4BE46 /* SceneDelegate.m */; }; + EC1FD74E239A041000A4BE46 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = EC1FD74D239A041000A4BE46 /* ViewController.m */; }; + EC1FD763239A041300A4BE46 /* SpeechDemoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EC1FD762239A041300A4BE46 /* SpeechDemoTests.m */; }; + EC1FD76E239A041300A4BE46 /* SpeechDemoUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = EC1FD76D239A041300A4BE46 /* SpeechDemoUITests.m */; }; + EC5D2CAC239DFACE00CF8EDA /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EC5D2CAB239DFACE00CF8EDA /* AVFoundation.framework */; }; + EC5D2CAE239DFAE100CF8EDA /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EC5D2CAD239DFAE000CF8EDA /* AudioToolbox.framework */; }; + EC5D2CB0239DFB1E00CF8EDA /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = EC5D2CAF239DFB1E00CF8EDA /* libc++.tbd */; }; + EC733AFB29D6B0AD0046BE0F /* SpeechMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = EC733AFA29D6B0AD0046BE0F /* SpeechMonitor.m */; }; + ECD14B2729B5BF310034B790 /* DialogViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = ECD14B2629B5BC5C0034B790 /* DialogViewController.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + EC1FD75F239A041300A4BE46 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = EC1FD73B239A041000A4BE46 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EC1FD742239A041000A4BE46; + remoteInfo = SpeechDemo; + }; + EC1FD76A239A041300A4BE46 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = EC1FD73B239A041000A4BE46 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EC1FD742239A041000A4BE46; + remoteInfo = SpeechDemo; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 1333EC06F468D2E52DF15EBB /* libPods-SpeechDemo-SpeechDemoUITests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SpeechDemo-SpeechDemoUITests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 20831F6486FF75C0CD2A744B /* Pods-SpeechDemoUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SpeechDemoUITests.debug.xcconfig"; path = "Target Support Files/Pods-SpeechDemoUITests/Pods-SpeechDemoUITests.debug.xcconfig"; sourceTree = ""; }; + 24AE1344BBCAC151F8B47D54 /* Pods-SpeechDemoUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SpeechDemoUITests.release.xcconfig"; path = "Target Support Files/Pods-SpeechDemoUITests/Pods-SpeechDemoUITests.release.xcconfig"; sourceTree = ""; }; + 2B1F58FB2E166B85004F15B2 /* BiTTSViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BiTTSViewController.m; sourceTree = ""; }; + 2B1F58FD2E166BD5004F15B2 /* BiTTSViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BiTTSViewController.h; sourceTree = ""; }; + 2B2296AA2E9DFBCE001B7D92 /* UniTTSViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UniTTSViewController.h; sourceTree = ""; }; + 2B2296AB2E9DFBCE001B7D92 /* UniTTSViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UniTTSViewController.m; sourceTree = ""; }; + 2B2296D72E9E4553001B7D92 /* aec.model */ = {isa = PBXFileReference; lastKnownFileType = file; path = aec.model; sourceTree = ""; }; + 2B2309832CB5796300535D5C /* TestAfpViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestAfpViewController.h; sourceTree = ""; }; + 2B2309842CB5796300535D5C /* TestAfpViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestAfpViewController.m; sourceTree = ""; }; + 2B2309852CB5796300535D5C /* TestAsrOfflineRtfViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestAsrOfflineRtfViewController.h; sourceTree = ""; }; + 2B2309862CB5796300535D5C /* TestAsrOfflineRtfViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestAsrOfflineRtfViewController.m; sourceTree = ""; }; + 2B2309872CB5796300535D5C /* TestAsrViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestAsrViewController.h; sourceTree = ""; }; + 2B2309882CB5796300535D5C /* TestAsrViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestAsrViewController.m; sourceTree = ""; }; + 2B24705A2A6A75D9003202D2 /* TtsNovelViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TtsNovelViewController.m; sourceTree = ""; }; + 2B24705F2A6A7D2B003202D2 /* TtsNovelViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TtsNovelViewController.h; sourceTree = ""; }; + 2B3265CD2CD8FD6600FAB976 /* BigAsrViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BigAsrViewController.h; sourceTree = ""; }; + 2B59F5C9294317B500273846 /* SensitiveDefines.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SensitiveDefines.m; sourceTree = ""; }; + 2B59F5CB294317D700273846 /* SensitiveDefines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SensitiveDefines.h; sourceTree = ""; }; + 2B5FA6D92CD90078007DE6C7 /* BigAsrViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BigAsrViewController.m; sourceTree = ""; }; + 2B6246842C50F639005B967D /* TtsNormalViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TtsNormalViewController.h; sourceTree = ""; }; + 2B6246852C50F668005B967D /* TtsNormalViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TtsNormalViewController.m; sourceTree = ""; }; + 2B6A76282987AC0300A75DBA /* VadViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VadViewController.h; sourceTree = ""; }; + 2B6A76292987AC0300A75DBA /* VadViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VadViewController.m; sourceTree = ""; }; + 2B8084722DD5EBE70053DA63 /* DialogDelegateViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DialogDelegateViewController.h; sourceTree = ""; }; + 2B8084732DD5EBE70053DA63 /* DialogDelegateViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DialogDelegateViewController.m; sourceTree = ""; }; + 2B8084752DD60FA60053DA63 /* DialogMessage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DialogMessage.h; sourceTree = ""; }; + 2B8084762DD60FA60053DA63 /* DialogMessage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DialogMessage.m; sourceTree = ""; }; + 2B9A93F4272661FA00F48B85 /* VoiceCloneViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VoiceCloneViewController.h; sourceTree = ""; }; + 2B9A93F5272661FA00F48B85 /* VoiceCloneViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VoiceCloneViewController.m; sourceTree = ""; }; + 2B9A94362726807C00F48B85 /* VoiceConvViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VoiceConvViewController.h; sourceTree = ""; }; + 2B9A94372726807C00F48B85 /* VoiceConvViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VoiceConvViewController.m; sourceTree = ""; }; + 2B9A94892726A16900F48B85 /* SettingViewDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingViewDelegate.m; sourceTree = ""; }; + 2B9A948A2726A16900F48B85 /* SettingViewDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingViewDelegate.h; sourceTree = ""; }; + 2B9A948B2726A16900F48B85 /* Settings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Settings.m; sourceTree = ""; }; + 2B9A948C2726A16900F48B85 /* Settings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Settings.h; sourceTree = ""; }; + 2BACC1B9250780EB009D850F /* AsrViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AsrViewController.h; sourceTree = ""; }; + 2BACC1BA25078103009D850F /* AsrViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AsrViewController.m; sourceTree = ""; }; + 2BACC1BC250786B4009D850F /* CaptViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CaptViewController.h; sourceTree = ""; }; + 2BACC1BD250786C9009D850F /* CaptViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CaptViewController.m; sourceTree = ""; }; + 2BB855D529F126A200B81519 /* AfpViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AfpViewController.h; sourceTree = ""; }; + 2BB855D629F126A200B81519 /* AfpViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AfpViewController.m; sourceTree = ""; }; + 2BB855D729F126A200B81519 /* AsrOfflineViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AsrOfflineViewController.h; sourceTree = ""; }; + 2BB855D829F126A200B81519 /* AsrOfflineViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AsrOfflineViewController.m; sourceTree = ""; }; + 2BB855DE2A0E5BF800B81519 /* AuViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AuViewController.m; sourceTree = ""; }; + 2BB855DF2A0E5BF800B81519 /* AuViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AuViewController.h; sourceTree = ""; }; + 2BDC591C29D2F3B700C90488 /* SettingsHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsHelper.m; sourceTree = ""; }; + 2BDC591D29D2F3B700C90488 /* SettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsViewController.m; sourceTree = ""; }; + 2BDC591E29D2F3B700C90488 /* SettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsViewController.h; sourceTree = ""; }; + 2BDC591F29D2F3B800C90488 /* SettingsHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsHelper.h; sourceTree = ""; }; + 2BDC592229D31A0300C90488 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 2BDC592429D31A2100C90488 /* SpeechSettingItem.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SpeechSettingItem.xib; sourceTree = ""; }; + 2BDC592529D31A2100C90488 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = SpeechDemo/ui/Info.plist; sourceTree = SOURCE_ROOT; }; + 2BDC592829D31A6800C90488 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 2BDC593E29D31B6700C90488 /* FileUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileUtils.m; sourceTree = ""; }; + 2BDC593F29D31B6700C90488 /* FileUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FileUtils.h; sourceTree = ""; }; + 2BDC594029D31B6700C90488 /* StreamRecorder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StreamRecorder.m; sourceTree = ""; }; + 2BDC594129D31B6700C90488 /* StreamRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StreamRecorder.h; sourceTree = ""; }; + 2BDC594E29D3E3E100C90488 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 2BDC595029D3E3E100C90488 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 2BF772B326BBBB6B009FBB86 /* FulllinkViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FulllinkViewController.m; sourceTree = ""; }; + 2BF772B426BBBB6B009FBB86 /* FulllinkViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FulllinkViewController.h; sourceTree = ""; }; + 30536C8DA34AD5C254CDEAD8 /* libPods-SpeechDemo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SpeechDemo.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33949311DCB2D6D13B65D7E3 /* Pods-SpeechDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SpeechDemo.debug.xcconfig"; path = "Target Support Files/Pods-SpeechDemo/Pods-SpeechDemo.debug.xcconfig"; sourceTree = ""; }; + 3632A3E25AFC110B26E7036F /* Pods-SpeechDemo-SpeechDemoUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SpeechDemo-SpeechDemoUITests.release.xcconfig"; path = "Target Support Files/Pods-SpeechDemo-SpeechDemoUITests/Pods-SpeechDemo-SpeechDemoUITests.release.xcconfig"; sourceTree = ""; }; + 4CE74A10663D66157749BD78 /* Pods-SpeechDemo-SpeechDemoTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SpeechDemo-SpeechDemoTests.debug.xcconfig"; path = "Target Support Files/Pods-SpeechDemo-SpeechDemoTests/Pods-SpeechDemo-SpeechDemoTests.debug.xcconfig"; sourceTree = ""; }; + 4E6809A675520FF019A8D9EA /* Pods-SpeechDemo-SpeechDemoUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SpeechDemo-SpeechDemoUITests.debug.xcconfig"; path = "Target Support Files/Pods-SpeechDemo-SpeechDemoUITests/Pods-SpeechDemo-SpeechDemoUITests.debug.xcconfig"; sourceTree = ""; }; + 4FBCB4456D746604330A4433 /* libPods-SpeechDemo-SpeechDemoTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SpeechDemo-SpeechDemoTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 902248642A9606F1009B3646 /* KwsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KwsViewController.h; sourceTree = ""; }; + 902248652A9606F1009B3646 /* KwsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KwsViewController.m; sourceTree = ""; }; + A8F84D623472936E4E083753 /* Pods-SpeechDemoTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SpeechDemoTests.release.xcconfig"; path = "Target Support Files/Pods-SpeechDemoTests/Pods-SpeechDemoTests.release.xcconfig"; sourceTree = ""; }; + ACC30EA44855C3801A9920DD /* Pods_SpeechDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SpeechDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AEED0457693B63B8128623CD /* Pods-SpeechDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SpeechDemo.release.xcconfig"; path = "Target Support Files/Pods-SpeechDemo/Pods-SpeechDemo.release.xcconfig"; sourceTree = ""; }; + C07A2F18D64AB43D6A8F57FB /* Pods-SpeechDemo-SpeechDemoTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SpeechDemo-SpeechDemoTests.release.xcconfig"; path = "Target Support Files/Pods-SpeechDemo-SpeechDemoTests/Pods-SpeechDemo-SpeechDemoTests.release.xcconfig"; sourceTree = ""; }; + D4AD4C28F6E6377336B8058E /* Pods-SpeechDemoTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SpeechDemoTests.debug.xcconfig"; path = "Target Support Files/Pods-SpeechDemoTests/Pods-SpeechDemoTests.debug.xcconfig"; sourceTree = ""; }; + EC1FD743239A041000A4BE46 /* SpeechDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SpeechDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + EC1FD746239A041000A4BE46 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + EC1FD747239A041000A4BE46 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + EC1FD749239A041000A4BE46 /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; + EC1FD74A239A041000A4BE46 /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = ""; }; + EC1FD74C239A041000A4BE46 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + EC1FD74D239A041000A4BE46 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + EC1FD75E239A041300A4BE46 /* SpeechDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SpeechDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + EC1FD762239A041300A4BE46 /* SpeechDemoTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SpeechDemoTests.m; sourceTree = ""; }; + EC1FD764239A041300A4BE46 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + EC1FD769239A041300A4BE46 /* SpeechDemoUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SpeechDemoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + EC1FD76D239A041300A4BE46 /* SpeechDemoUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SpeechDemoUITests.m; sourceTree = ""; }; + EC1FD76F239A041300A4BE46 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + EC5D2CAB239DFACE00CF8EDA /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; + EC5D2CAD239DFAE000CF8EDA /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; + EC5D2CAF239DFB1E00CF8EDA /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; }; + EC733AF929D6B0AD0046BE0F /* SpeechMonitor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SpeechMonitor.h; sourceTree = ""; }; + EC733AFA29D6B0AD0046BE0F /* SpeechMonitor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SpeechMonitor.m; sourceTree = ""; }; + ECD14B2529B5BC5C0034B790 /* DialogViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DialogViewController.h; sourceTree = ""; }; + ECD14B2629B5BC5C0034B790 /* DialogViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DialogViewController.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + EC1FD740239A041000A4BE46 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + EC5D2CB0239DFB1E00CF8EDA /* libc++.tbd in Frameworks */, + EC5D2CAE239DFAE100CF8EDA /* AudioToolbox.framework in Frameworks */, + EC5D2CAC239DFACE00CF8EDA /* AVFoundation.framework in Frameworks */, + 9CF40464ADBF4D8B1632B429 /* libPods-SpeechDemo.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EC1FD75B239A041300A4BE46 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2D5645BD6F9BABD19C6E8C07 /* libPods-SpeechDemo-SpeechDemoTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EC1FD766239A041300A4BE46 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 190DFD6BF614F40529F09E06 /* libPods-SpeechDemo-SpeechDemoUITests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2B2296D82E9E4553001B7D92 /* testdata */ = { + isa = PBXGroup; + children = ( + 2B2296D72E9E4553001B7D92 /* aec.model */, + ); + path = testdata; + sourceTree = ""; + }; + 2B2309892CB5796300535D5C /* test */ = { + isa = PBXGroup; + children = ( + 2B2309832CB5796300535D5C /* TestAfpViewController.h */, + 2B2309842CB5796300535D5C /* TestAfpViewController.m */, + 2B2309852CB5796300535D5C /* TestAsrOfflineRtfViewController.h */, + 2B2309862CB5796300535D5C /* TestAsrOfflineRtfViewController.m */, + 2B2309872CB5796300535D5C /* TestAsrViewController.h */, + 2B2309882CB5796300535D5C /* TestAsrViewController.m */, + ); + path = test; + sourceTree = ""; + }; + 2B9A94882726A14F00F48B85 /* settings */ = { + isa = PBXGroup; + children = ( + 2BDC591F29D2F3B800C90488 /* SettingsHelper.h */, + 2BDC591C29D2F3B700C90488 /* SettingsHelper.m */, + 2BDC591E29D2F3B700C90488 /* SettingsViewController.h */, + 2BDC591D29D2F3B700C90488 /* SettingsViewController.m */, + 2B9A948C2726A16900F48B85 /* Settings.h */, + 2B9A948B2726A16900F48B85 /* Settings.m */, + 2B9A948A2726A16900F48B85 /* SettingViewDelegate.h */, + 2B9A94892726A16900F48B85 /* SettingViewDelegate.m */, + ); + path = settings; + sourceTree = ""; + }; + D5D57D9C733374DB527D376F /* Pods */ = { + isa = PBXGroup; + children = ( + D4AD4C28F6E6377336B8058E /* Pods-SpeechDemoTests.debug.xcconfig */, + A8F84D623472936E4E083753 /* Pods-SpeechDemoTests.release.xcconfig */, + 20831F6486FF75C0CD2A744B /* Pods-SpeechDemoUITests.debug.xcconfig */, + 24AE1344BBCAC151F8B47D54 /* Pods-SpeechDemoUITests.release.xcconfig */, + 33949311DCB2D6D13B65D7E3 /* Pods-SpeechDemo.debug.xcconfig */, + AEED0457693B63B8128623CD /* Pods-SpeechDemo.release.xcconfig */, + 4CE74A10663D66157749BD78 /* Pods-SpeechDemo-SpeechDemoTests.debug.xcconfig */, + C07A2F18D64AB43D6A8F57FB /* Pods-SpeechDemo-SpeechDemoTests.release.xcconfig */, + 4E6809A675520FF019A8D9EA /* Pods-SpeechDemo-SpeechDemoUITests.debug.xcconfig */, + 3632A3E25AFC110B26E7036F /* Pods-SpeechDemo-SpeechDemoUITests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + EC1FD73A239A041000A4BE46 = { + isa = PBXGroup; + children = ( + EC1FD745239A041000A4BE46 /* SpeechDemo */, + EC1FD761239A041300A4BE46 /* SpeechDemoTests */, + EC1FD76C239A041300A4BE46 /* SpeechDemoUITests */, + EC1FD744239A041000A4BE46 /* Products */, + EC5D2CA9239DF23500CF8EDA /* Frameworks */, + D5D57D9C733374DB527D376F /* Pods */, + ); + sourceTree = ""; + }; + EC1FD744239A041000A4BE46 /* Products */ = { + isa = PBXGroup; + children = ( + EC1FD743239A041000A4BE46 /* SpeechDemo.app */, + EC1FD75E239A041300A4BE46 /* SpeechDemoTests.xctest */, + EC1FD769239A041300A4BE46 /* SpeechDemoUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + EC1FD745239A041000A4BE46 /* SpeechDemo */ = { + isa = PBXGroup; + children = ( + 2B2296D82E9E4553001B7D92 /* testdata */, + 902248642A9606F1009B3646 /* KwsViewController.h */, + 902248652A9606F1009B3646 /* KwsViewController.m */, + 2B6A76282987AC0300A75DBA /* VadViewController.h */, + 2B6A76292987AC0300A75DBA /* VadViewController.m */, + 2BACC1B9250780EB009D850F /* AsrViewController.h */, + 2BACC1BA25078103009D850F /* AsrViewController.m */, + 2B3265CD2CD8FD6600FAB976 /* BigAsrViewController.h */, + 2B5FA6D92CD90078007DE6C7 /* BigAsrViewController.m */, + 2BB855D729F126A200B81519 /* AsrOfflineViewController.h */, + 2BB855D829F126A200B81519 /* AsrOfflineViewController.m */, + 2BB855D529F126A200B81519 /* AfpViewController.h */, + 2BB855D629F126A200B81519 /* AfpViewController.m */, + 2BB855DF2A0E5BF800B81519 /* AuViewController.h */, + 2BB855DE2A0E5BF800B81519 /* AuViewController.m */, + 2B6246842C50F639005B967D /* TtsNormalViewController.h */, + 2B6246852C50F668005B967D /* TtsNormalViewController.m */, + 2B24705F2A6A7D2B003202D2 /* TtsNovelViewController.h */, + 2B24705A2A6A75D9003202D2 /* TtsNovelViewController.m */, + 2B9A94362726807C00F48B85 /* VoiceConvViewController.h */, + 2B9A94372726807C00F48B85 /* VoiceConvViewController.m */, + 2B9A93F4272661FA00F48B85 /* VoiceCloneViewController.h */, + 2B9A93F5272661FA00F48B85 /* VoiceCloneViewController.m */, + 2BF772B426BBBB6B009FBB86 /* FulllinkViewController.h */, + 2BF772B326BBBB6B009FBB86 /* FulllinkViewController.m */, + ECD14B2529B5BC5C0034B790 /* DialogViewController.h */, + ECD14B2629B5BC5C0034B790 /* DialogViewController.m */, + 2B8084722DD5EBE70053DA63 /* DialogDelegateViewController.h */, + 2B8084732DD5EBE70053DA63 /* DialogDelegateViewController.m */, + 2BACC1BC250786B4009D850F /* CaptViewController.h */, + 2BACC1BD250786C9009D850F /* CaptViewController.m */, + 2B1F58FD2E166BD5004F15B2 /* BiTTSViewController.h */, + 2B1F58FB2E166B85004F15B2 /* BiTTSViewController.m */, + 2B2296AA2E9DFBCE001B7D92 /* UniTTSViewController.h */, + 2B2296AB2E9DFBCE001B7D92 /* UniTTSViewController.m */, + EC733AF629D2D3790046BE0F /* utils */, + 2B9A94882726A14F00F48B85 /* settings */, + EC733AF829D2D9400046BE0F /* delegate */, + EC733AF729D2D7E30046BE0F /* ui */, + 2B2309892CB5796300535D5C /* test */, + ); + path = SpeechDemo; + sourceTree = ""; + }; + EC1FD761239A041300A4BE46 /* SpeechDemoTests */ = { + isa = PBXGroup; + children = ( + EC1FD762239A041300A4BE46 /* SpeechDemoTests.m */, + EC1FD764239A041300A4BE46 /* Info.plist */, + ); + path = SpeechDemoTests; + sourceTree = ""; + }; + EC1FD76C239A041300A4BE46 /* SpeechDemoUITests */ = { + isa = PBXGroup; + children = ( + EC1FD76D239A041300A4BE46 /* SpeechDemoUITests.m */, + EC1FD76F239A041300A4BE46 /* Info.plist */, + ); + path = SpeechDemoUITests; + sourceTree = ""; + }; + EC5D2CA9239DF23500CF8EDA /* Frameworks */ = { + isa = PBXGroup; + children = ( + EC5D2CAF239DFB1E00CF8EDA /* libc++.tbd */, + EC5D2CAD239DFAE000CF8EDA /* AudioToolbox.framework */, + EC5D2CAB239DFACE00CF8EDA /* AVFoundation.framework */, + ACC30EA44855C3801A9920DD /* Pods_SpeechDemo.framework */, + 30536C8DA34AD5C254CDEAD8 /* libPods-SpeechDemo.a */, + 4FBCB4456D746604330A4433 /* libPods-SpeechDemo-SpeechDemoTests.a */, + 1333EC06F468D2E52DF15EBB /* libPods-SpeechDemo-SpeechDemoUITests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + EC733AF629D2D3790046BE0F /* utils */ = { + isa = PBXGroup; + children = ( + 2B8084752DD60FA60053DA63 /* DialogMessage.h */, + 2B8084762DD60FA60053DA63 /* DialogMessage.m */, + 2BDC593F29D31B6700C90488 /* FileUtils.h */, + 2BDC593E29D31B6700C90488 /* FileUtils.m */, + 2BDC594129D31B6700C90488 /* StreamRecorder.h */, + 2BDC594029D31B6700C90488 /* StreamRecorder.m */, + EC733AF929D6B0AD0046BE0F /* SpeechMonitor.h */, + EC733AFA29D6B0AD0046BE0F /* SpeechMonitor.m */, + 2B59F5CB294317D700273846 /* SensitiveDefines.h */, + 2B59F5C9294317B500273846 /* SensitiveDefines.m */, + ); + path = utils; + sourceTree = ""; + }; + EC733AF729D2D7E30046BE0F /* ui */ = { + isa = PBXGroup; + children = ( + 2BDC594D29D3E3E100C90488 /* LaunchScreen.storyboard */, + 2BDC594F29D3E3E100C90488 /* Main.storyboard */, + 2BDC592829D31A6800C90488 /* Assets.xcassets */, + 2BDC592529D31A2100C90488 /* Info.plist */, + 2BDC592429D31A2100C90488 /* SpeechSettingItem.xib */, + 2BDC592229D31A0300C90488 /* main.m */, + EC1FD74C239A041000A4BE46 /* ViewController.h */, + EC1FD74D239A041000A4BE46 /* ViewController.m */, + ); + path = ui; + sourceTree = ""; + }; + EC733AF829D2D9400046BE0F /* delegate */ = { + isa = PBXGroup; + children = ( + EC1FD746239A041000A4BE46 /* AppDelegate.h */, + EC1FD747239A041000A4BE46 /* AppDelegate.m */, + EC1FD749239A041000A4BE46 /* SceneDelegate.h */, + EC1FD74A239A041000A4BE46 /* SceneDelegate.m */, + ); + path = delegate; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + EC1FD742239A041000A4BE46 /* SpeechDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = EC1FD772239A041300A4BE46 /* Build configuration list for PBXNativeTarget "SpeechDemo" */; + buildPhases = ( + AB467A3BE59EA58294FA1E6A /* [CP] Check Pods Manifest.lock */, + 7F409EEB0D604B5CF7107034 /* [CP-User] Hummer-before-hooks */, + EC1FD73F239A041000A4BE46 /* Sources */, + EC1FD740239A041000A4BE46 /* Frameworks */, + EC1FD741239A041000A4BE46 /* Resources */, + F3CE3275F7BCC49541894934 /* [CP] Embed Pods Frameworks */, + 3509D01F4C8B47E28D493507 /* [CP-User] Hummer-after-hooks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SpeechDemo; + productName = SpeechDemo; + productReference = EC1FD743239A041000A4BE46 /* SpeechDemo.app */; + productType = "com.apple.product-type.application"; + }; + EC1FD75D239A041300A4BE46 /* SpeechDemoTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = EC1FD775239A041300A4BE46 /* Build configuration list for PBXNativeTarget "SpeechDemoTests" */; + buildPhases = ( + A3A7445AA83C166AEFC2F42A /* [CP] Check Pods Manifest.lock */, + B9235AEDFABBC50F1EE379E0 /* [CP-User] Hummer-before-hooks */, + EC1FD75A239A041300A4BE46 /* Sources */, + EC1FD75B239A041300A4BE46 /* Frameworks */, + EC1FD75C239A041300A4BE46 /* Resources */, + D091270C3D490BCAE68CFEED /* [CP] Embed Pods Frameworks */, + 233B4C611F0CEE2EB36887D3 /* [CP-User] Hummer-after-hooks */, + ); + buildRules = ( + ); + dependencies = ( + EC1FD760239A041300A4BE46 /* PBXTargetDependency */, + ); + name = SpeechDemoTests; + productName = SpeechDemoTests; + productReference = EC1FD75E239A041300A4BE46 /* SpeechDemoTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + EC1FD768239A041300A4BE46 /* SpeechDemoUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = EC1FD778239A041300A4BE46 /* Build configuration list for PBXNativeTarget "SpeechDemoUITests" */; + buildPhases = ( + A2071DEC4B21722B9ABEE72F /* [CP] Check Pods Manifest.lock */, + A6F1017653C1572A63C0EA3E /* [CP-User] Hummer-before-hooks */, + EC1FD765239A041300A4BE46 /* Sources */, + EC1FD766239A041300A4BE46 /* Frameworks */, + EC1FD767239A041300A4BE46 /* Resources */, + 735FC38D6FB47958A68FC961 /* [CP] Embed Pods Frameworks */, + 3CAC2B0DCB331788DE820889 /* [CP-User] Hummer-after-hooks */, + ); + buildRules = ( + ); + dependencies = ( + EC1FD76B239A041300A4BE46 /* PBXTargetDependency */, + ); + name = SpeechDemoUITests; + productName = SpeechDemoUITests; + productReference = EC1FD769239A041300A4BE46 /* SpeechDemoUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + EC1FD73B239A041000A4BE46 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1330; + ORGANIZATIONNAME = fangweiwei; + TargetAttributes = { + EC1FD742239A041000A4BE46 = { + CreatedOnToolsVersion = 11.2.1; + }; + EC1FD75D239A041300A4BE46 = { + CreatedOnToolsVersion = 11.2.1; + TestTargetID = EC1FD742239A041000A4BE46; + }; + EC1FD768239A041300A4BE46 = { + CreatedOnToolsVersion = 11.2.1; + TestTargetID = EC1FD742239A041000A4BE46; + }; + }; + }; + buildConfigurationList = EC1FD73E239A041000A4BE46 /* Build configuration list for PBXProject "SpeechDemo" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = EC1FD73A239A041000A4BE46; + productRefGroup = EC1FD744239A041000A4BE46 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + EC1FD742239A041000A4BE46 /* SpeechDemo */, + EC1FD75D239A041300A4BE46 /* SpeechDemoTests */, + EC1FD768239A041300A4BE46 /* SpeechDemoUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + EC1FD741239A041000A4BE46 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2BDC592E29D31A6800C90488 /* Assets.xcassets in Resources */, + 2B2296D92E9E4553001B7D92 /* aec.model in Resources */, + 2BDC595229D3E3E100C90488 /* Main.storyboard in Resources */, + 2BDC595129D3E3E100C90488 /* LaunchScreen.storyboard in Resources */, + 2BDC592629D31A2100C90488 /* SpeechSettingItem.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EC1FD75C239A041300A4BE46 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EC1FD767239A041300A4BE46 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 233B4C611F0CEE2EB36887D3 /* [CP-User] Hummer-after-hooks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + name = "[CP-User] Hummer-after-hooks"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ -f \"${PODS_ROOT}/hummer_after_hooks.sh\" ];then\n bash \"${PODS_ROOT}/hummer_after_hooks.sh\"\nfi\n"; + }; + 3509D01F4C8B47E28D493507 /* [CP-User] Hummer-after-hooks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + name = "[CP-User] Hummer-after-hooks"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ -f \"${PODS_ROOT}/hummer_after_hooks.sh\" ];then\n bash \"${PODS_ROOT}/hummer_after_hooks.sh\"\nfi\n"; + }; + 3CAC2B0DCB331788DE820889 /* [CP-User] Hummer-after-hooks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + name = "[CP-User] Hummer-after-hooks"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ -f \"${PODS_ROOT}/hummer_after_hooks.sh\" ];then\n bash \"${PODS_ROOT}/hummer_after_hooks.sh\"\nfi\n"; + }; + 735FC38D6FB47958A68FC961 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SpeechDemo-SpeechDemoUITests/Pods-SpeechDemo-SpeechDemoUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SpeechDemo-SpeechDemoUITests/Pods-SpeechDemo-SpeechDemoUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SpeechDemo-SpeechDemoUITests/Pods-SpeechDemo-SpeechDemoUITests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 7F409EEB0D604B5CF7107034 /* [CP-User] Hummer-before-hooks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + name = "[CP-User] Hummer-before-hooks"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ -f \"${PODS_ROOT}/hummer_before_hooks.sh\" ];then\n bash \"${PODS_ROOT}/hummer_before_hooks.sh\"\nfi\n"; + }; + A2071DEC4B21722B9ABEE72F /* [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-SpeechDemo-SpeechDemoUITests-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; + }; + A3A7445AA83C166AEFC2F42A /* [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-SpeechDemo-SpeechDemoTests-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; + }; + A6F1017653C1572A63C0EA3E /* [CP-User] Hummer-before-hooks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + name = "[CP-User] Hummer-before-hooks"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ -f \"${PODS_ROOT}/hummer_before_hooks.sh\" ];then\n bash \"${PODS_ROOT}/hummer_before_hooks.sh\"\nfi\n"; + }; + AB467A3BE59EA58294FA1E6A /* [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-SpeechDemo-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; + }; + B9235AEDFABBC50F1EE379E0 /* [CP-User] Hummer-before-hooks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + name = "[CP-User] Hummer-before-hooks"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ -f \"${PODS_ROOT}/hummer_before_hooks.sh\" ];then\n bash \"${PODS_ROOT}/hummer_before_hooks.sh\"\nfi\n"; + }; + D091270C3D490BCAE68CFEED /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SpeechDemo-SpeechDemoTests/Pods-SpeechDemo-SpeechDemoTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SpeechDemo-SpeechDemoTests/Pods-SpeechDemo-SpeechDemoTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SpeechDemo-SpeechDemoTests/Pods-SpeechDemo-SpeechDemoTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + F3CE3275F7BCC49541894934 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SpeechDemo/Pods-SpeechDemo-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SpeechDemo/Pods-SpeechDemo-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SpeechDemo/Pods-SpeechDemo-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + EC1FD73F239A041000A4BE46 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 902248662A9606F1009B3646 /* KwsViewController.m in Sources */, + 2BB855DA29F126A200B81519 /* AsrOfflineViewController.m in Sources */, + 2B23098A2CB5796300535D5C /* TestAsrOfflineRtfViewController.m in Sources */, + 2B23098B2CB5796300535D5C /* TestAsrViewController.m in Sources */, + 2B23098C2CB5796300535D5C /* TestAfpViewController.m in Sources */, + EC1FD74E239A041000A4BE46 /* ViewController.m in Sources */, + 2B1F58FC2E166B85004F15B2 /* BiTTSViewController.m in Sources */, + 2B2296AC2E9DFBCE001B7D92 /* UniTTSViewController.m in Sources */, + 2BB855D929F126A200B81519 /* AfpViewController.m in Sources */, + 2B8084772DD60FA60053DA63 /* DialogMessage.m in Sources */, + EC1FD748239A041000A4BE46 /* AppDelegate.m in Sources */, + 2B59F5CA294317B500273846 /* SensitiveDefines.m in Sources */, + 2B9A948E2726A16900F48B85 /* Settings.m in Sources */, + 2BDC594229D31B6800C90488 /* FileUtils.m in Sources */, + 2BDC592129D2F3B800C90488 /* SettingsViewController.m in Sources */, + EC733AFB29D6B0AD0046BE0F /* SpeechMonitor.m in Sources */, + 90C447DB274CEC0A00C96ED6 /* FulllinkViewController.m in Sources */, + 2BACC1BB25078103009D850F /* AsrViewController.m in Sources */, + 2BDC594329D31B6800C90488 /* StreamRecorder.m in Sources */, + 2B8084742DD5EBE70053DA63 /* DialogDelegateViewController.m in Sources */, + 2B6246862C50F668005B967D /* TtsNormalViewController.m in Sources */, + 2BB855E02A0E5BF800B81519 /* AuViewController.m in Sources */, + 2B6A762A2987AC0300A75DBA /* VadViewController.m in Sources */, + 2B9A948D2726A16900F48B85 /* SettingViewDelegate.m in Sources */, + ECD14B2729B5BF310034B790 /* DialogViewController.m in Sources */, + EC1FD74B239A041000A4BE46 /* SceneDelegate.m in Sources */, + 2B9A93F6272661FA00F48B85 /* VoiceCloneViewController.m in Sources */, + 2B24705C2A6A75D9003202D2 /* TtsNovelViewController.m in Sources */, + 2BDC592029D2F3B800C90488 /* SettingsHelper.m in Sources */, + 2B5FA6DA2CD90078007DE6C7 /* BigAsrViewController.m in Sources */, + 2BACC1BE250786C9009D850F /* CaptViewController.m in Sources */, + 2BDC592329D31A0300C90488 /* main.m in Sources */, + 2B9A94382726807C00F48B85 /* VoiceConvViewController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EC1FD75A239A041300A4BE46 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EC1FD763239A041300A4BE46 /* SpeechDemoTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EC1FD765239A041300A4BE46 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EC1FD76E239A041300A4BE46 /* SpeechDemoUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + EC1FD760239A041300A4BE46 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = EC1FD742239A041000A4BE46 /* SpeechDemo */; + targetProxy = EC1FD75F239A041300A4BE46 /* PBXContainerItemProxy */; + }; + EC1FD76B239A041300A4BE46 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = EC1FD742239A041000A4BE46 /* SpeechDemo */; + targetProxy = EC1FD76A239A041300A4BE46 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 2BDC594D29D3E3E100C90488 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 2BDC594E29D3E3E100C90488 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + 2BDC594F29D3E3E100C90488 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 2BDC595029D3E3E100C90488 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + EC1FD770239A041300A4BE46 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = 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_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_BITCODE = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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; + INFOPLIST_FILE = SpeechDemo/ui/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_GENERATE_MAP_FILE = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + EC1FD771239A041300A4BE46 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = 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_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_BITCODE = NO; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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; + INFOPLIST_FILE = SpeechDemo/ui/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_GENERATE_MAP_FILE = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + EC1FD773239A041300A4BE46 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33949311DCB2D6D13B65D7E3 /* Pods-SpeechDemo.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = XJMHV8DXNL; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = XXHND5J98K; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/TTNetworkManager\"", + ); + HEADER_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = SpeechDemo/ui/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_GENERATE_MAP_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/../../../pod/libs\"", + ); + OTHER_LDFLAGS = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = com.bytedance.ailab.speech.sdk; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Speech SDK All Dev"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = combytedanceailabspeechsdk; + SWIFT_OBJC_BRIDGING_HEADER = "SpeechDemo/utils/SpeechDemo-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_WORKSPACE = YES; + }; + name = Debug; + }; + EC1FD774239A041300A4BE46 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AEED0457693B63B8128623CD /* Pods-SpeechDemo.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = XXHND5J98K; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/TTNetworkManager\"", + ); + HEADER_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = SpeechDemo/ui/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_GENERATE_MAP_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/../../../pod/libs\"", + ); + OTHER_LDFLAGS = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = com.bytedance.ailab.speech.sdk; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = combytedanceailabspeechsdk; + SWIFT_OBJC_BRIDGING_HEADER = "SpeechDemo/utils/SpeechDemo-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_WORKSPACE = YES; + }; + name = Release; + }; + EC1FD776239A041300A4BE46 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4CE74A10663D66157749BD78 /* Pods-SpeechDemo-SpeechDemoTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = X38J52YLA2; + INFOPLIST_FILE = SpeechDemoTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + OTHER_LDFLAGS = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = com.bytedance.ailab.speech.sdk.SpeechDemoTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SpeechDemo.app/SpeechDemo"; + }; + name = Debug; + }; + EC1FD777239A041300A4BE46 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C07A2F18D64AB43D6A8F57FB /* Pods-SpeechDemo-SpeechDemoTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = X38J52YLA2; + INFOPLIST_FILE = SpeechDemoTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + OTHER_LDFLAGS = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = com.bytedance.ailab.speech.sdk.SpeechDemoTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SpeechDemo.app/SpeechDemo"; + }; + name = Release; + }; + EC1FD779239A041300A4BE46 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4E6809A675520FF019A8D9EA /* Pods-SpeechDemo-SpeechDemoUITests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = X38J52YLA2; + INFOPLIST_FILE = SpeechDemoUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + OTHER_LDFLAGS = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = com.bytedance.ailab.speech.sdk.SpeechDemoUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = SpeechDemo; + }; + name = Debug; + }; + EC1FD77A239A041300A4BE46 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3632A3E25AFC110B26E7036F /* Pods-SpeechDemo-SpeechDemoUITests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = X38J52YLA2; + INFOPLIST_FILE = SpeechDemoUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + OTHER_LDFLAGS = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = com.bytedance.ailab.speech.sdk.SpeechDemoUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = SpeechDemo; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + EC1FD73E239A041000A4BE46 /* Build configuration list for PBXProject "SpeechDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EC1FD770239A041300A4BE46 /* Debug */, + EC1FD771239A041300A4BE46 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EC1FD772239A041300A4BE46 /* Build configuration list for PBXNativeTarget "SpeechDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EC1FD773239A041300A4BE46 /* Debug */, + EC1FD774239A041300A4BE46 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EC1FD775239A041300A4BE46 /* Build configuration list for PBXNativeTarget "SpeechDemoTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EC1FD776239A041300A4BE46 /* Debug */, + EC1FD777239A041300A4BE46 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EC1FD778239A041300A4BE46 /* Build configuration list for PBXNativeTarget "SpeechDemoUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EC1FD779239A041300A4BE46 /* Debug */, + EC1FD77A239A041300A4BE46 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = EC1FD73B239A041000A4BE46 /* Project object */; +} diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..dbf7c35 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo.xcodeproj/xcshareddata/xcschemes/SpeechDemo.xcscheme b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo.xcodeproj/xcshareddata/xcschemes/SpeechDemo.xcscheme new file mode 100644 index 0000000..3cf6098 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo.xcodeproj/xcshareddata/xcschemes/SpeechDemo.xcscheme @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AfpViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AfpViewController.h new file mode 100644 index 0000000..a9b805f --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AfpViewController.h @@ -0,0 +1,17 @@ +// +// AfpViewController.h +// SpeechDemo +// +// Created by fangweiwei on 2021/8/31. +// Copyright © 2021 fangweiwei. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface AfpViewController : UIViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AfpViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AfpViewController.m new file mode 100644 index 0000000..0747296 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AfpViewController.m @@ -0,0 +1,340 @@ +// +// AfpViewController.m +// SpeechDemo +// +// Created by fangweiwei on 2021/8/31. +// Copyright © 2021 fangweiwei. All rights reserved. +// + +#import "AfpViewController.h" + +#import + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" + +@interface AfpViewController () + +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; +@property (weak, nonatomic) IBOutlet UIButton *engineInitButton; +@property (weak, nonatomic) IBOutlet UIButton *engineUninitButton; +@property (weak, nonatomic) IBOutlet UIButton *startEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *stopEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *fetchResultButton; + +@property (strong, nonatomic) SpeechEngine *curEngine; +@property (assign, nonatomic) BOOL engineStarted; + +@property (nonatomic, strong) NSString *deviceID; +@property (strong, nonatomic) NSString *debugPath; + +// Settings +@property (strong, nonatomic) Settings *settings; +@end + +@implementation AfpViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.fetchResultButton.enabled = FALSE; + [self.statusTextView setText:@"Waiting for init."]; + [self decorateTextView:self.resultTextView]; + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + self.curEngine = nil; + self.engineStarted = FALSE; + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_AFP]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [self uninitEngine]; + [super viewDidDisappear:animated]; +} + +- (void)decorateTextView:(UITextView *)textView { + textView.layer.cornerRadius = 5.0f; + textView.layer.borderWidth = .25f; + textView.layer.borderColor = [UIColor grayColor].CGColor; +} + +#pragma mark - SpeechEngineDelegate + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSLog(@"Message Type: %d.", type); +} + +#pragma mark - UI Actions + +- (IBAction)initEngine:(id)sender { + [self initEngine]; +} + +- (IBAction)uninitEngine:(id)sender { + if (self.engineStarted) { + [self.statusTextView setText:@"Engine is busy, stop it first!"]; + return; + } + [self uninitEngine]; + [self.resultTextView setTextColor:UIColor.grayColor]; + [self.resultTextView setText:@"点击或按住说话后,展示语音识别结果"]; +} + +- (IBAction)startEngine:(id)sender { + NSLog(@"Start engine."); + SEEngineErrorCode ret = [self.curEngine ResetEngine]; + NSString *result = [NSString stringWithFormat:@"Reset engine: %d.", ret]; + NSLog(@"%@", result); + + NSString *path = [self.debugPath stringByAppendingPathComponent:@"test_afp.pcm"]; + NSError *error; + NSData *data = [NSData dataWithContentsOfFile:path options:NSDataReadingMappedIfSafe error:&error]; + if (data) { + // Read success. + SEErrorCode ret = [self.curEngine ProcessAudio:(int16_t *)data.bytes length:(int32_t)(data.length / 2) isFinal:TRUE]; + if (ret != SENoError) { + [self setResultText:[NSString stringWithFormat:@"Feed audio data failed: %d, lenght: %lu.", ret, (unsigned long)(data.length / 2)]]; + } + } + + self.engineStarted = true; + [self.statusTextView setText:@"Engine Started!"]; + [self setResultText:result]; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = TRUE; + self.fetchResultButton.enabled = TRUE; +} + +- (IBAction)stopEngine:(id)sender { + NSLog(@"Stop engine."); + SEEngineErrorCode ret = [self.curEngine ResetEngine]; + NSString *result = [NSString stringWithFormat:@"Reset engine: %d.", ret]; + NSLog(@"%@", result); + + self.engineStarted = FALSE; + [self.statusTextView setText:@"Engine Stopped!"]; + [self setResultText:result]; + self.startEngineButton.enabled = TRUE; + self.stopEngineButton.enabled = FALSE; + self.fetchResultButton.enabled = FALSE; +} + +- (IBAction)fetchResult:(id)sender { + SEResultType resultType = [self getResultType]; + int resultTypeIdx = [self.settings getOptions:SETTING_AFP_RESULT_TYPE].chooseIdx; + + if (resultTypeIdx == 0) { + // Bytes result. + NSData *result; + SEEngineErrorCode ret = [self.curEngine FetchResult:resultType result:&result]; + if (ret != SENoError) { + [self setResultText:[NSString stringWithFormat:@"Fetch result failed! Err code: %d.", ret]]; + } else { + [self setResultText:@"Fetch result succeed!"]; + } + NSLog(@"Fetch result: %d.", ret); + + NSString *fileName = [NSString stringWithFormat:@"test_%@_out.bytes", [self getEngineName]]; + NSFileHandle *file = [FileUtils openFileForWriting:fileName inPath:self.debugPath]; + [FileUtils writeData:result toFileHandel:file]; + [FileUtils closeFile:file]; + } else { + // Json result. + NSString* result = [self.curEngine FetchStringResult:resultType]; + // 提取err_code的值 + NSError *error; + NSDictionary *jsonResult = [NSJSONSerialization JSONObjectWithData:[result dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&error]; + int errCode = [jsonResult[@"err_code"] intValue]; + if (errCode != SENoError) { + [self setResultText:[NSString stringWithFormat:@"Fetch result failed! Err code: %d.", errCode]]; + } else { + [self setResultText:@"Fetch result succeed!"]; + } + NSLog(@"Fetch result: %d.", errCode); + + NSString *fileName = [NSString stringWithFormat:@"test_%@_out.json", [self getEngineName]]; + NSFileHandle *file = [FileUtils openFileForWriting:fileName inPath:self.debugPath]; + [FileUtils writeString:result toFileHandel:file]; + [FileUtils closeFile:file]; + } +} + +#pragma mark - Init Methods + +- (void)initEngine { + AppDelegate *appDelegate = [ViewController getAppDelegate]; + if (appDelegate == nil) { + appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; + } + if (appDelegate.deviceID.length < 1) { + self.engineInitButton.enabled = FALSE; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:@"Waiting for get deviceID."]; + sleep(1); + [self initEngine]; + }); + return; + } + [ViewController setAppDelegate:appDelegate]; + self.deviceID = appDelegate.deviceID; + + + if (self.curEngine == nil) { + self.curEngine = [[SpeechEngine alloc] init]; + } + if (![self.curEngine createEngineWithDelegate:nil]) { + NSLog(@"Create speech engine failed."); + return; + } + [self.resultTextView setTextColor:UIColor.blackColor]; + NSLog(@"Engine version: %@", [self.curEngine getVersion]); + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"Debug path: %@", self.debugPath); + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + [self.curEngine setStringParam:SE_LOG_LEVEL_TRACE forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + [self.curEngine setStringParam:[self getEngineName] forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + + SEEngineErrorCode ret = [self.curEngine initEngine]; + if (ret != SENoError) { + NSLog(@"Init Engine failed: %d", ret); + } + if (ret == SENoError) { + [self speechEngineInitOk]; + } else { + [self speechEngineInitFailed]; + } +} + +- (void)uninitEngine { + [self.curEngine destroyEngine]; + self.curEngine = nil; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.fetchResultButton.enabled = FALSE; +} + +#pragma mark - Engine Callback + +- (void)speechEngineNoPermission { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"No permission!"]; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + }); +} + +- (void)speechEngineInitOk { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:@"Ready"]; + [self.resultTextView setText:[NSString stringWithFormat:@"DeviceID: %@", self.deviceID]]; + self.engineUninitButton.enabled = TRUE; + self.engineInitButton.enabled = FALSE; + self.startEngineButton.enabled = TRUE; + self.fetchResultButton.enabled = TRUE; + }); +} + +- (void)speechEngineInitFailed { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"Failed to init engine!"]; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + }); +} + +- (void)setResultText:(NSString *)result { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[result stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; + }); +} + +#pragma mark - Helper + +- (long)timeDelayFrom:(long)pastTimestamp { + return [[NSDate date] timeIntervalSince1970] * 1000 - pastTimestamp; +} + +- (NSString* )getEngineName { + SettingOptions* engineNameOptions = [self.settings getOptions:SETTING_MUSIC_ENGINE_NAME]; + switch (engineNameOptions.chooseIdx) { + case 1: + return SE_COVERSONG_ENGINE; + case 2: + return SE_HUMMING_ENGINE; + case 0: + default: + return SE_AFP_ENGINE; + } +} + +- (SEResultType)getResultType { + NSString* engineName = [self getEngineName]; + int resultTypeIdx = [self.settings getOptions:SETTING_AFP_RESULT_TYPE].chooseIdx; + + // AFP + if ([engineName isEqualToString:SE_AFP_ENGINE]) { + switch (resultTypeIdx) { + case 0: + return SEAfpResult; + case 1: + default: + return SEAfpSliceResult; + } + } + // CoverSong + if ([engineName isEqualToString:SE_COVERSONG_ENGINE]) { + switch (resultTypeIdx) { + case 0: + return SECoversongResult; + case 1: + default: + return SECoverSongSliceResult; + } + } + // Humming + if ([engineName isEqualToString:SE_HUMMING_ENGINE]) { + switch (resultTypeIdx) { + case 0: + return SEHummingResult; + case 1: + default: + return SEHummingSliceResult; + } + } + + // Return afp result as default. + return SEAfpResult; +} + +#pragma mark - UITextViewDelegate + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if([text isEqualToString:@"\n"]) { + [textView resignFirstResponder]; + return NO; + } + return YES; +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_AFP forKey:@"viewId"]; +} + +@end + + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AsrOfflineViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AsrOfflineViewController.h new file mode 100644 index 0000000..a421340 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AsrOfflineViewController.h @@ -0,0 +1,13 @@ +// +// AsrOfflineViewController.h +// SpeechDemo +// +// Created by bytedance on 2020/9/8. +// Copyright © 2020 fengkai.0518. All rights reserved. +// + +#import + +@interface AsrOfflineViewController : UIViewController + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AsrOfflineViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AsrOfflineViewController.m new file mode 100644 index 0000000..4bcf424 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AsrOfflineViewController.m @@ -0,0 +1,507 @@ +// +// AsrOfflineViewController.m +// SpeechDemo +// +// Created by bytedance on 2020/9/8. +// Copyright © 2020 fengkai.0518. All rights reserved. +// + +#import "AsrOfflineViewController.h" + +#import + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" +#import "SensitiveDefines.h" + +@interface AsrOfflineViewController () + +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; +@property (weak, nonatomic) IBOutlet UIButton *engineInitButton; +@property (weak, nonatomic) IBOutlet UIButton *engineUninitButton; +@property (weak, nonatomic) IBOutlet UIButton *startEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *stopEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *longPressButton; + +@property (strong, nonatomic) SpeechEngine *curEngine; +@property (assign, nonatomic) BOOL engineStarted; + +@property (nonatomic, strong) NSString *deviceID; +@property (nonatomic, assign) long talkingFinisheTimestamp; +@property (nonatomic, assign) long startEngineTimestamp; +@property (strong, nonatomic) NSString *debugPath; + +@property (weak, nonatomic) StreamRecorder *streamRecorder; + +// settings +@property (strong, nonatomic) Settings *settings; + +@end + +static NSString *SLARDAR_ASR_SERVICE_NAME = @"asr_statistics"; +static NSString *SLARDAR_ASR_EVENT_RESPONSE_DELAY = @"asr_response_delay"; + +@implementation AsrOfflineViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_ASR_OFFLINE]; + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"当前调试路径 %@", self.debugPath); + + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.longPressButton.enabled = FALSE; + [self.statusTextView setText:@"Waiting for init."]; + [self decorateTextView:self.resultTextView]; + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + UILongPressGestureRecognizer *longPgr = [[UILongPressGestureRecognizer alloc] initWithTarget:self + action:@selector(longPressTriggered:)]; + longPgr.minimumPressDuration = 0.5; + [self.longPressButton addGestureRecognizer:longPgr]; + self.streamRecorder = [ViewController getStreamRecorder]; + self.engineStarted = FALSE; +} + +- (void)viewDidDisappear:(BOOL)animated { + [self uninitEngine]; + [super viewDidDisappear:animated]; +} + +- (void)decorateTextView:(UITextView *)textView { + textView.layer.cornerRadius = 5.0f; + textView.layer.borderWidth = .25f; + textView.layer.borderColor = [UIColor grayColor].CGColor; +} + +#pragma mark - SpeechEngineDelegate + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSLog(@"Message Type: %d.", type); + switch (type) { + case SEEngineStart: + [self speechEngineStarted]; + break; + case SEEngineStop: + [self speechEngineStopped]; + break; + case SEEngineError: + [self speechEngineError:data]; + break; + case SEAsrPartialResult: + [self speechEngineResult:data isFinal:FALSE]; + break; + case SEFinalResult: + [self speechEngineResult:data isFinal:TRUE]; + break; + case SEVolumeLevel: + NSLog(@"volume level: %s", (char*)data.bytes); + break; + case SEEngineLog: + NSLog(@"engine log: %s", (char*)data.bytes); + break; + default: + break; + } +} + +#pragma mark - UI Actions + +- (IBAction)initEngine:(id)sender { + [self initEngine]; +} + +- (IBAction)uninitEngine:(id)sender { + if (self.engineStarted) { + [self.statusTextView setText:@"Engine is busy, stop it first!"]; + return; + } + [self uninitEngine]; + [self.resultTextView setTextColor:UIColor.grayColor]; + [self.resultTextView setText:@"点击或按住说话后,展示语音识别结果"]; +} + +- (IBAction)startEngine:(id)sender { + NSLog(@"Start engine."); + [self.curEngine setStringParam:[self.settings getString:SETTING_APPID] forKey:SE_PARAMS_KEY_APP_ID_STRING]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_ENABLE_ITN] forKey:SE_PARAMS_KEY_ASR_ENABLE_ITN_BOOL]; + [self.curEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_ASR_AUTO_STOP_BOOL]; + [self.curEngine setIntParam:[self.settings getInt:SETTING_VAD_MAX_SPEECH_DURATION] forKey:SE_PARAMS_KEY_VAD_MAX_SPEECH_DURATION_INT]; + //【可选配置】控制识别结果返回的形式,全量返回或增量返回,默认为全量 + [self.curEngine setStringParam:[self.settings getOptionsValue:SETTING_ASR_RESULT_TYPE] forKey:SE_PARAMS_KEY_ASR_RESULT_TYPE_STRING]; + + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.curEngine setIntParam:[self.streamRecorder getSampleRate] forKey:SE_PARAMS_KEY_CUSTOM_SAMPLE_RATE_INT]; + [self.curEngine setIntParam:[self.streamRecorder getChannel] forKey:SE_PARAMS_KEY_CUSTOM_CHANNEL_INT]; + if (![self.streamRecorder start]) { + [self speechEngineNoPermission]; + return; + } + } else if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_FILE]) { + NSString* file_path = [NSString stringWithFormat:@"%@/%@", self.debugPath, @"asr_rec_file.pcm"]; + NSLog(@"test file path: %@", file_path); + [self.curEngine setStringParam:file_path forKey:SE_PARAMS_KEY_RECORDER_FILE_STRING]; + } + [self.curEngine sendDirective:SEDirectiveSyncStopEngine]; + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + } +} + +- (IBAction)stopEngine:(id)sender { + NSLog(@"Stop engine."); + [self.curEngine sendDirective:SEDirectiveStopEngine]; +} + +- (void)longPressTriggered:(UILongPressGestureRecognizer *)longPgr { + if (longPgr.state == UIGestureRecognizerStateBegan) { + NSLog(@"Long press begin."); + + [self setResultText:@""]; + [self.curEngine setStringParam:[self.settings getString:SETTING_APPID] forKey:SE_PARAMS_KEY_APP_ID_STRING]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_ENABLE_ITN] forKey:SE_PARAMS_KEY_ASR_ENABLE_ITN_BOOL]; + [self.curEngine setBoolParam:FALSE forKey:SE_PARAMS_KEY_ASR_AUTO_STOP_BOOL]; + [self.curEngine setIntParam:[self.settings getInt:SETTING_VAD_MAX_SPEECH_DURATION] forKey:SE_PARAMS_KEY_VAD_MAX_SPEECH_DURATION_INT]; + + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.curEngine setIntParam:[self.streamRecorder getSampleRate] forKey:SE_PARAMS_KEY_CUSTOM_SAMPLE_RATE_INT]; + [self.curEngine setIntParam:[self.streamRecorder getChannel] forKey:SE_PARAMS_KEY_CUSTOM_CHANNEL_INT]; + if (![self.streamRecorder start]) { + [self speechEngineNoPermission]; + return; + } + } else if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_FILE]) { + NSString* file_path = [NSString stringWithFormat:@"%@/%@", self.debugPath, @"asr_rec_file.pcm"]; + NSLog(@"test file path: %@", file_path); + [self.curEngine setStringParam:file_path forKey:SE_PARAMS_KEY_RECORDER_FILE_STRING]; + } + + [self.curEngine sendDirective:SEDirectiveSyncStopEngine]; + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + } + } else if (longPgr.state == UIGestureRecognizerStateEnded) { + NSLog(@"Long press ended."); + self.talkingFinisheTimestamp = [[NSDate date] timeIntervalSince1970] * 1000; + [self.curEngine sendDirective:SEDirectiveFinishTalking]; + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.streamRecorder stop]; + } + } +} + +#pragma mark - Init Methods + +- (void)initEngine { + AppDelegate *appDelegate = [ViewController getAppDelegate]; + if (appDelegate == nil) { + appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; + } + if (appDelegate.deviceID.length < 1) { + self.engineInitButton.enabled = FALSE; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:@"Waiting for get deviceID."]; + sleep(1); + [self initEngine]; + }); + return; + } + [ViewController setAppDelegate:appDelegate]; + self.deviceID = appDelegate.deviceID; + + if (![self.settings getBool:SETTING_ASR_ENABLE_RESOURCE_DOWNLOAD]) { + [self initEngineInternal]; + } else { + SpeechResourceManager *resourceManager = [SpeechResourceManager shareInstance]; + if ([resourceManager checkModelExist:[self getAsrModelName]]) { + [resourceManager checkModelVersion:[self getAsrModelName] completion:^(SEResourceStatus status, BOOL needUpdate, NSData *data) { + if (status == kSERSuccess) { + if (needUpdate) { + [self fetchAsrResource]; + } else { + [self initEngineInternal]; + } + } else { + NSLog(@"Model check failed: %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); + [self.resultTextView setText:@"Failed to check asr resource"]; + [self speechEngineInitFailed]; + } + }]; + } else { + [self fetchAsrResource]; + } + } +} + +- (void)fetchAsrResource { + SpeechResourceManager *resourceManager = [SpeechResourceManager shareInstance]; + + [resourceManager fetchModelByName:[self getAsrModelName] completion:^(SEResourceStatus status, NSData* data) { + if (status == kSERSuccess) { + [self initEngineInternal]; + } else { + [self.resultTextView setText:@"Failed to fetch asr resource"]; + [self speechEngineInitFailed]; + } + }]; +} + +- (void)initEngineInternal { + if (self.curEngine == nil) { + self.curEngine = [[SpeechEngine alloc] init]; + } + if (![self.curEngine createEngineWithDelegate:self]) { + NSLog(@"Create speech engine failed."); + return; + } + + [self.resultTextView setTextColor:UIColor.blackColor]; + NSLog(@"Engine version: %@", [self.curEngine getVersion]); + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"Debug path: %@", self.debugPath); + NSString* modelsPath = [NSString stringWithFormat:@"%@/models", self.debugPath]; + if ([self.settings getBool:SETTING_ASR_ENABLE_RESOURCE_DOWNLOAD]) { + SpeechResourceManager *resourceManager = [SpeechResourceManager shareInstance]; + modelsPath = [resourceManager getModelPath:[self getAsrModelName]]; + } + NSLog(@"Asr model path: %@", modelsPath); + + //【必需配置】离线合成鉴权相关:证书文件存放路径 + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_LICENSE_DIRECTORY_STRING]; + NSString* authenticationType = [self getAuthenticationType]; + //【必需配置】Authenticate Type + [self.curEngine setStringParam:authenticationType forKey:SE_PARAMS_KEY_AUTHENTICATE_TYPE_STRING]; + if ([authenticationType isEqualToString:SE_AUTHENTICATE_TYPE_PRE_BIND]) { + // 按包名授权,获取到授权的 APP 可以不限次数、不限设备数的使用离线合成 + NSString *licenseName = [self.settings getString:SETTING_LICENSE_NAME]; + NSString *licenseBusiId = [self.settings getString:SETTING_LICENSE_BUSI_ID]; + // 证书名和业务 ID, 离线合成鉴权相关,使用火山提供的证书下发服务时为【必需配置】, 否则为【无需配置】 + // 证书名,用于下载按报名授权的证书文件 + [self.curEngine setStringParam:licenseName forKey:SE_PARAMS_KEY_LICENSE_NAME_STRING]; + // 业务 ID, 用于下载按报名授权的证书文件 + [self.curEngine setStringParam:licenseBusiId forKey:SE_PARAMS_KEY_LICENSE_BUSI_ID_STRING]; + } else if ([authenticationType isEqualToString:SE_AUTHENTICATE_TYPE_LATE_BIND]) { + // 按装机量授权,不限制 APP 的包名和使用次数,但是限制使用离线合成的设备数量 + //【必需配置】离线合成鉴权相关:Authenticate Address + [self.curEngine setStringParam:SDEF_AUTHENTICATE_ADDRESS forKey:SE_PARAMS_KEY_AUTHENTICATE_ADDRESS_STRING]; + //【必需配置】离线合成鉴权相关:Authenticate Uri + [self.curEngine setStringParam:SDEF_AUTHENTICATE_URI forKey:SE_PARAMS_KEY_AUTHENTICATE_URI_STRING]; + NSString* curBusinessKey = [self.settings getString:SETTING_BUSINESS_KEY]; + NSString* curAuthenticateSecret = [self.settings getString:SETTING_AUTHENTICATE_SECRET]; + //【必需配置】离线合成鉴权相关:Business Key + [self.curEngine setStringParam:curBusinessKey forKey:SE_PARAMS_KEY_BUSINESS_KEY_STRING]; + //【必需配置】离线合成鉴权相关:Authenticate Secret + [self.curEngine setStringParam:curAuthenticateSecret forKey:SE_PARAMS_KEY_AUTHENTICATE_SECRET_STRING]; + } + + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + [self.curEngine setStringParam:SE_LOG_LEVEL_TRACE forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + [self.curEngine setStringParam:[self.settings getString:SETTING_APPID] forKey:SE_PARAMS_KEY_APP_ID_STRING]; + [self.curEngine setStringParam:@"388808087185088" forKey:SE_PARAMS_KEY_UID_STRING]; + [self.curEngine setIntParam:1 forKey:SE_PARAMS_KEY_CHANNEL_NUM_INT]; + [self.curEngine setStringParam:@"" forKey:SE_PARAMS_KEY_ASR_REC_PATH_STRING]; + if ([self.settings getBool:SETTING_ASR_RECORDER_SAVE]) { + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_ASR_REC_PATH_STRING]; + } + [self.curEngine setStringParam:[self getRecorderType] forKey:SE_PARAMS_KEY_RECORDER_TYPE_STRING]; + [self.curEngine setStringParam:SE_ASR_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + [self.curEngine setIntParam:16000 forKey:SE_PARAMS_KEY_SAMPLE_RATE_INT]; + [self.curEngine setBoolParam:true forKey:SE_PARAMS_KEY_ASR_SHOW_UTTER_BOOL]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_SHOW_LANGUAGE] forKey:SE_PARAMS_KEY_ASR_SHOW_LANG_BOOL]; + [self.curEngine setBoolParam:true forKey:SE_PARAMS_KEY_ASR_SHOW_VOLUME_BOOL]; + [self.curEngine setIntParam:SEAsrWorkModeOfflineFlute forKey:SE_PARAMS_KEY_ASR_WORK_MODE_INT]; + [self.curEngine setStringParam:modelsPath forKey:SE_PARAMS_KEY_ASR_OFF_RESOURCE_PATH_STRING]; + + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + if ([self.streamRecorder getSampleRate] != 16000 || [self.streamRecorder getChannel] != 1) { + [self.curEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_ENABLE_RESAMPLER_BOOL]; + } + } + + SEEngineErrorCode ret = [self.curEngine initEngine]; + if (ret != SENoError) { + NSLog(@"Init Engine failed: %d", ret); + } + if (ret == SENoError) { + [self speechEngineInitOk]; + } else { + [self.resultTextView setText:[NSString stringWithFormat:@"Failed to init engine: %d", ret]]; + [self speechEngineInitFailed]; + } +} + +- (NSString *)getRecorderType { + SettingOptions* recorderTypeOptions = [self.settings getOptions:SETTING_RECORD_TYPE]; + switch (recorderTypeOptions.chooseIdx) { + case 0: + return SE_RECORDER_TYPE_RECORDER; + case 1: + return SE_RECORDER_TYPE_FILE; + case 2: + return SE_RECORDER_TYPE_STREAM; + default: + break; + } + return @""; +} + +- (void)uninitEngine { + if (self.curEngine != nil) { + NSLog(@"引擎析构"); + [self.curEngine destroyEngine]; + self.curEngine = nil; + NSLog(@"引擎析构完成"); + } + dispatch_async(dispatch_get_main_queue(), ^{ + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.longPressButton.enabled = FALSE; + }); +} + +- (void)setHotWords:(NSString*) hotWords { + [self.curEngine sendDirective:SEDirectiveUpdateAsrHotWords data: hotWords]; +} + +#pragma mark - Engine Callback + +- (void)speechEngineNoPermission { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"No permission!"]; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + }); +} + +- (void)speechEngineInitOk { + [self.streamRecorder setSpeechEngine:VIEW_ASR_OFFLINE engine:self.curEngine]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:@"Ready"]; + [self.resultTextView setText:[NSString stringWithFormat:@"DeviceID: %@", self.deviceID]]; + self.engineUninitButton.enabled = TRUE; + self.engineInitButton.enabled = FALSE; + self.startEngineButton.enabled = TRUE; + self.longPressButton.enabled = TRUE; + }); +} + +- (void)speechEngineInitFailed { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"Failed to init engine!"]; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + }); +} + +- (void)speechEngineStarted { + dispatch_async(dispatch_get_main_queue(), ^{ + self.startEngineTimestamp = [[NSDate date] timeIntervalSince1970] * 1000; + self.engineStarted = true; + [self.statusTextView setText:@"Engine Started!"]; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = TRUE; + self.longPressButton.enabled = FALSE; + }); +} + +- (void)speechEngineStopped { + dispatch_async(dispatch_get_main_queue(), ^{ + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.streamRecorder stop]; + } + self.engineStarted = FALSE; + [self.statusTextView setText:@"Engine Stopped!"]; + self.startEngineButton.enabled = TRUE; + self.stopEngineButton.enabled = FALSE; + self.longPressButton.enabled = TRUE; + }); +} + +- (void)speechEngineResult:(NSData *)data isFinal:(BOOL)isFinal { + dispatch_async(dispatch_get_main_queue(), ^{ + long response_delay = 0; + if (isFinal && self.talkingFinisheTimestamp > 0) { + response_delay = [self timeDelayFrom:self.talkingFinisheTimestamp]; + self.talkingFinisheTimestamp = 0; + } + + NSString* dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSMutableString *text = [[NSMutableString alloc] initWithString:@""]; + [text appendFormat:@"result: %@", dataStr]; + if (isFinal) { + [text appendFormat:@"\nresponse_delay: %ld", response_delay]; + } + if (text.length) { + [self.resultTextView setText:[text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; + } + }); +} + +- (void)speechEngineError:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]]; +// [self stopEngine:nil]; + }); +} + +- (void)setResultText:(NSString *)result { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[result stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; + }); +} + +- (NSString*)getAsrModelName { + return [self.settings getString:SETTING_ASR_MODEL_NAME]; +} + +#pragma mark - Helper + +- (NSString*)getAuthenticationType { + switch ([self.settings getOptions:SETTING_AUTHENTICATION_TYPE].chooseIdx) { + case 0: + return SE_AUTHENTICATE_TYPE_PRE_BIND; + case 1: + return SE_AUTHENTICATE_TYPE_LATE_BIND; + default: + break; + } + return SE_AUTHENTICATE_TYPE_PRE_BIND; +} + +- (long)timeDelayFrom:(long)pastTimestamp { + return [[NSDate date] timeIntervalSince1970] * 1000 - pastTimestamp; +} + +#pragma mark - UITextViewDelegate + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if([text isEqualToString:@"\n"]) { + [textView resignFirstResponder]; + return NO; + } + return YES; +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_ASR_OFFLINE forKey:@"viewId"]; +} + +@end + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AsrViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AsrViewController.h new file mode 100644 index 0000000..d1408d9 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AsrViewController.h @@ -0,0 +1,13 @@ +// +// AsrViewController.h +// SpeechDemo +// +// Created by bytedance on 2020/9/8. +// Copyright © 2020 fengkai.0518. All rights reserved. +// + +#import + +@interface AsrViewController : UIViewController + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AsrViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AsrViewController.m new file mode 100644 index 0000000..3590cd0 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AsrViewController.m @@ -0,0 +1,563 @@ +// +// AsrViewController.m +// SpeechDemo +// +// Created by bytedance on 2020/9/8. +// Copyright © 2020 fengkai.0518. All rights reserved. +// + +#import "AsrViewController.h" + +#import + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" +#import "SensitiveDefines.h" + +@interface AsrViewController () + +// UI +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; +@property (weak, nonatomic) IBOutlet UIButton *initialEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *uninitialEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *startEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *stopEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *recordButton; + +// Device ID: 用于定位线上问题 +@property (nonatomic, strong) NSString *deviceID; +// Debug Path: 用于存放一些 SDK 相关的文件,比如模型、日志等 +@property (strong, nonatomic) NSString *debugPath; + +// SpeechEngine +@property (strong, nonatomic) SpeechEngine *curEngine; +@property (assign, nonatomic) BOOL engineStarted; + +// Settings +@property (strong, nonatomic) Settings *settings; + +// APP 层自定义的录音机,在音频来源为 Stream 时使用 +@property (weak, nonatomic) StreamRecorder *streamRecorder; + +// 一些用于统计的字段 +@property (nonatomic, assign) long talkingFinishTimestamp; +@end + +@implementation AsrViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.initialEngineButton.enabled = TRUE; + self.uninitialEngineButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.recordButton.enabled = FALSE; + + [self.statusTextView setText:@"Waiting for init."]; + [self decorateTextView:self.resultTextView]; + + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + UILongPressGestureRecognizer *longPgr = [[UILongPressGestureRecognizer alloc] initWithTarget:self + action:@selector(recordTriggered:)]; + longPgr.minimumPressDuration = 0.5; + [self.recordButton addGestureRecognizer:longPgr]; + + self.streamRecorder = [ViewController getStreamRecorder]; + self.engineStarted = FALSE; + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_ASR]; + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"当前调试路径 %@", self.debugPath); +} + +- (void)viewDidDisappear:(BOOL)animated { + [self uninitEngine]; + [super viewDidDisappear:animated]; +} + +- (void)decorateTextView:(UITextView *)textView { + textView.layer.cornerRadius = 5.0f; + textView.layer.borderWidth = .25f; + textView.layer.borderColor = [UIColor grayColor].CGColor; +} + +#pragma mark - Config & Init & Uninit Methods + +-(void)configInitParams{ + //【必需配置】Engine Name + [self.curEngine setStringParam:SE_ASR_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + + //【可选配置】Debug & Log + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + [self.curEngine setStringParam:SE_LOG_LEVEL_DEBUG forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + + //【可选配置】UID & deviceID: 用于定位线上问题 + [self.curEngine setStringParam:SDEF_UID forKey:SE_PARAMS_KEY_UID_STRING]; + [self.curEngine setStringParam:self.deviceID forKey:SE_PARAMS_KEY_DEVICE_ID_STRING]; + + //【必需配置】配置音频来源 + [self.curEngine setStringParam:[self getRecorderType] forKey:SE_PARAMS_KEY_RECORDER_TYPE_STRING]; + + if ([self.settings getBool:SETTING_ASR_RECORDER_SAVE]) { + //【可选配置】录音文件保存路径,如配置,SDK会将录音保存到该路径下,文件格式为 .wav + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_ASR_REC_PATH_STRING]; + } + + // 当音频来源为 RECORDER_TYPE_STREAM 时,如输入音频采样率不等于 16K,需添加如下配置 + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + if ([self.streamRecorder getSampleRate] != 16000 || [self.streamRecorder getChannel] != 1) { + // 当音频来源为 RECORDER_TYPE_STREAM 时【必需配置】,否则【无需配置】 + // 启用 SDK 内部的重采样 + [self.curEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_ENABLE_RESAMPLER_BOOL]; + // 将重采样所需的输入采样率设置为 APP 层输入的音频的实际采样率 + [self.curEngine setIntParam:[self.streamRecorder getSampleRate] forKey:SE_PARAMS_KEY_CUSTOM_SAMPLE_RATE_INT]; + [self.curEngine setIntParam:[self.streamRecorder getChannel] forKey:SE_PARAMS_KEY_CUSTOM_CHANNEL_INT]; + } + } + + NSString *address = [self.settings getString:SETTING_ADDRESS]; + if (!address.length) { + address = SDEF_DEFAULT_ADDRESS; + } + NSLog(@"Current address: %@", address); + //【必需配置】识别服务域名 + [self.curEngine setStringParam:address forKey:SE_PARAMS_KEY_ASR_ADDRESS_STRING]; + + NSString *uri = [self.settings getString:SETTING_URI]; + if (!uri.length) { + uri = SDEF_ASR_DEFAULT_URI; + } + NSLog(@"Current uri: %@", uri); + //【必需配置】识别服务Uri + [self.curEngine setStringParam:uri forKey:SE_PARAMS_KEY_ASR_URI_STRING]; + + NSString* appID = [self.settings getString:SETTING_APPID]; + //【必需配置】鉴权相关:Appid + [self.curEngine setStringParam:appID.length <= 0 ? SDEF_APPID : appID forKey:SE_PARAMS_KEY_APP_ID_STRING]; + //【必需配置】鉴权相关:Token + NSString* token = [self.settings getString:SETTING_TOKEN]; + [self.curEngine setStringParam:token forKey:SE_PARAMS_KEY_APP_TOKEN_STRING]; + //【可需配置】自定义请求头部KV:Headers + [self.curEngine setStringParam:[self.settings getString:SETTING_REQUEST_HEADERS] forKey:SE_PARAMS_KEY_REQUEST_HEADERS_STRING]; + + NSString* cluster = [self.settings getString:SETTING_CLUSTER]; + NSLog(@"Current cluster: %@", cluster); + //【必需配置】识别服务所用集群 + [self.curEngine setStringParam:cluster.length <= 0 ? SDEF_ASR_DEFAULT_CLUSTER : cluster forKey:SE_PARAMS_KEY_ASR_CLUSTER_STRING]; + + //【可选配置】在线请求的建连与接收超时,一般不需配置使用默认值即可 + [self.curEngine setIntParam:3000 forKey:SE_PARAMS_KEY_ASR_CONN_TIMEOUT_INT]; + [self.curEngine setIntParam:5000 forKey:SE_PARAMS_KEY_ASR_RECV_TIMEOUT_INT]; + + //【可选配置】在线请求断连后,重连次数,默认值为0,如果需要开启需要设置大于0的次数 + [self.curEngine setIntParam:[self.settings getInt:SETTING_ASR_MAX_RETRY_TIMES] forKey:SE_PARAMS_KEY_ASR_MAX_RETRY_TIMES_INT]; + + //【可选配置】音频采样率,默认16000 + [self.curEngine setIntParam:[self.settings getInt:SETTING_SAMPLE_RATE] forKey:SE_PARAMS_KEY_SAMPLE_RATE_INT]; + //【可选配置】音频通道数,默认1,可选1或2 + [self.curEngine setIntParam:[self.settings getInt:SETTING_CHANNEL] forKey:SE_PARAMS_KEY_CHANNEL_NUM_INT]; + //【可选配置】上传给服务的音频通道数,默认1,可选1或2,一般与SE_PARAMS_KEY_SAMPLE_RATE_INT保持一致即可 + [self.curEngine setIntParam:[self.settings getInt:SETTING_CHANNEL] forKey:SE_PARAMS_KEY_UP_CHANNEL_NUM_INT]; + +} + +-(void)configStartAsrParams{ + //【可选配置】是否开启顺滑(DDC) + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_ENABLE_DDC] forKey:SE_PARAMS_KEY_ASR_ENABLE_DDC_BOOL]; + //【可选配置】是否开启文字转数字(ITN) + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_ENABLE_ITN] forKey:SE_PARAMS_KEY_ASR_ENABLE_ITN_BOOL]; + //【可选配置】是否开启标点 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_ENABLE_NLU_PUNC] forKey:SE_PARAMS_KEY_ASR_SHOW_NLU_PUNC_BOOL]; + //【可选配置】是否隐藏句尾标点 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_DISABLE_END_PUNC] forKey:SE_PARAMS_KEY_ASR_DISABLE_END_PUNC_BOOL]; + // 【可选配置】直接传递自定义的ASR请求JSON,若使用此参数需自行确保JSON格式正确 + [self.curEngine setStringParam:[self.settings getString:SETTING_ASR_REQ_PARAMS] forKey:SE_PARAMS_KEY_ASR_REQ_PARAMS_STRING]; + + //【可选配置】设置识别语种 + [self.curEngine setStringParam:[self.settings getString:SETTING_ASR_LANGUAGE] forKey:SE_PARAMS_KEY_ASR_LANGUAGE_STRING]; + //【可选配置】是否返回用户说话的语种 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_SHOW_LANGUAGE] forKey:SE_PARAMS_KEY_ASR_SHOW_LANG_BOOL]; + + //【可选配置】控制识别结果返回的形式,全量返回或增量返回,默认为全量 + [self.curEngine setStringParam:[self.settings getOptionsValue:SETTING_ASR_RESULT_TYPE] forKey:SE_PARAMS_KEY_ASR_RESULT_TYPE_STRING]; + + //【可选配置】设置VAD头部静音时长,用户多久没说话视为空音频,即静音检测时长 + [self.curEngine setIntParam:[self.settings getInt:SETTING_ASR_VAD_START_SILENCE_TIME] forKey:SE_PARAMS_KEY_ASR_VAD_START_SILENCE_TIME_INT]; + //【可选配置】设置VAD尾部静音时长,用户说话后停顿多久视为说话结束,即自动判停时长 + [self.curEngine setIntParam:[self.settings getInt:SETTING_ASR_VAD_END_SILENCE_TIME] forKey:SE_PARAMS_KEY_ASR_VAD_END_SILENCE_TIME_INT]; + //【可选配置】用户音频输入最大时长,仅一句话识别场景生效,单位毫秒,默认为 150000ms. + [self.curEngine setIntParam:[self.settings getInt:SETTING_VAD_MAX_SPEECH_DURATION] forKey:SE_PARAMS_KEY_VAD_MAX_SPEECH_DURATION_INT]; + + //【可选配置】控制是否返回录音音量,在 APP 需要显示音频波形时可以启用 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_GET_VOLUME] forKey:SE_PARAMS_KEY_ENABLE_GET_VOLUME_BOOL]; + + //【可选配置】更新 ASR 热词 + if ([self.settings getString:SETTING_ASR_HOTWORDS].length != 0) { + [self setHotWords:[self.settings getString:SETTING_ASR_HOTWORDS]]; + } + + //【可选配置】设置纠错词表,识别结果会根据设置的纠错词纠正结果,例如:"{\"古爱玲\":\"谷爱凌\"}",当识别结果中出现"古爱玲"时会替换为"谷爱凌" + [self.curEngine setStringParam:[self.settings getString:SETTING_ASR_CORRECTWORDS] forKey:SE_PARAMS_KEY_ASR_CORRECT_WORDS_STRING]; + + NSString* recorderType = [self getRecorderType]; + NSLog(@"录音模式: %@", recorderType); + + if ([recorderType isEqualToString:SE_RECORDER_TYPE_STREAM]) { + if (![self.streamRecorder start]) { + [self speechEngineNoPermission]; + } + } else if ([recorderType isEqualToString:SE_RECORDER_TYPE_FILE]) { + // 使用音频文件识别时,需要设置文件的绝对路径 + NSString* file_path = [NSString stringWithFormat:@"%@/%@", self.debugPath, @"asr_rec_file.pcm"]; + NSLog(@"输入的音频文件路径: %@", file_path); + // 使用音频文件识别时【必须配置】,否则【无需配置】 + [self.curEngine setStringParam:file_path forKey:SE_PARAMS_KEY_RECORDER_FILE_STRING]; + } +} + +- (void)setHotWords:(NSString*) hotWords { + // 更新 ASR 热词,例如:"{\"hotwords\":[{\"word\":\"过秦论\",\"scale\":2.0}]}" + // scale为float类型参数,其中叠词的范围为[1.0,2.0],非叠词的范围为[1.0,50.0],scale值越大,结果中出现热词的概率越大 + [self.curEngine sendDirective:SEDirectiveUpdateAsrHotWords data: hotWords]; +} + +- (void)initEngine { + NSLog(@"获取设备ID,调试使用"); + AppDelegate *appDelegate = [ViewController getAppDelegate]; + if (appDelegate == nil) { + appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; + } + [ViewController setAppDelegate:appDelegate]; + self.deviceID = appDelegate.deviceID; + NSLog(@"获取设备ID成功: %@", self.deviceID); + + + NSLog(@"创建引擎"); + if (self.curEngine == nil) { + self.curEngine = [[SpeechEngine alloc] init]; + if (![self.curEngine createEngineWithDelegate:self]) { + NSLog(@"引擎创建失败."); + return; + } + } + [self.resultTextView setTextColor:UIColor.blackColor]; + NSLog(@"SDK 版本号: %@", [self.curEngine getVersion]); + + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"当前调试路径: %@", self.debugPath); + + NSLog(@"配置初始化参数"); + [self configInitParams]; + + + NSLog(@"引擎初始化"); + SEEngineErrorCode ret = [self.curEngine initEngine]; + if (ret == SENoError) { + NSLog(@"初始化成功"); + [self speechEngineInitSucceeded]; + } else { + NSLog(@"初始化失败,返回值: %d", ret); + [self speechEngineInitFailed:ret]; + } +} + +- (void)uninitEngine { + if (self.curEngine != nil) { + NSLog(@"引擎析构"); + [self.curEngine destroyEngine]; + self.curEngine = nil; + NSLog(@"引擎析构完成"); + } + dispatch_async(dispatch_get_main_queue(), ^{ + self.initialEngineButton.enabled = TRUE; + self.uninitialEngineButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.recordButton.enabled = FALSE; + }); +} + +- (void)speechEngineInitSucceeded { + [self.streamRecorder setSpeechEngine:VIEW_ASR engine:self.curEngine]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:@"Ready"]; + [self setResultText:[NSString stringWithFormat:@"DeviceID: %@", self.deviceID]]; + self.uninitialEngineButton.enabled = TRUE; + self.initialEngineButton.enabled = FALSE; + self.startEngineButton.enabled = TRUE; + self.recordButton.enabled = TRUE; + }); +} + +- (void)speechEngineInitFailed:(int)initStatus { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:[[NSString alloc] initWithFormat:@"Failed to init engine, %d!", initStatus]]; + self.initialEngineButton.enabled = TRUE; + self.uninitialEngineButton.enabled = FALSE; + }); +} + +- (void)speechEngineNoPermission { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"No permission!"]; + self.initialEngineButton.enabled = TRUE; + self.uninitialEngineButton.enabled = FALSE; + }); +} + +#pragma mark - UI Actions + +- (IBAction)initEngineBtnClicked:(id)sender { + [self initEngine]; +} + +- (IBAction)uninitEngineBtnClicked:(id)sender { + if (self.engineStarted) { + [self.statusTextView setText:@"Engine is busy, stop it first!"]; + return; + } + [self uninitEngine]; + + [self.resultTextView setTextColor:UIColor.grayColor]; + [self setResultText:@"点击或按住说话后,展示语音识别结果"]; +} + +- (IBAction)startEngineBtnClicked:(id)sender { + self.talkingFinishTimestamp = 0; + [self setResultText:@""]; + + NSLog(@"配置启动参数"); + [self configStartAsrParams]; + + //【可选配置】该按钮为短按模式,预期是按下开始录音,自动判停结束,需要开启云端自动判停功能。 + NSLog(@"开启 ASR 云端自动判停"); + [self.curEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_ASR_AUTO_STOP_BOOL]; + + + // Directive:启动引擎前调用SYNC_STOP指令,保证前一次请求结束。 + NSLog(@"Directive: SEDirectiveSyncStopEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveSyncStopEngine]; + if (ret != SENoError) { + NSLog(@"Send directive syncstop failed: %d", ret); + } else { + NSLog(@"启用引擎."); + NSLog(@"Directive: SEDirectiveStartEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + } + } +} + +- (IBAction)stopEngineBtnClicked:(id)sender { + NSLog(@"关闭引擎"); + NSLog(@"Directive: SEDirectiveStopEngine"); + [self.curEngine sendDirective:SEDirectiveStopEngine]; +} + +- (void)recordTriggered:(UILongPressGestureRecognizer *)longPgr { + if (longPgr.state == UIGestureRecognizerStateBegan) { + self.talkingFinishTimestamp = 0; + [self setResultText:@""]; + + NSLog(@"配置启动参数"); + [self configStartAsrParams]; + + //【可选配置】是否启用云端自动判停,仅一句话识别场景生效 + NSLog(@"关闭 ASR 云端自动判停"); + [self.curEngine setBoolParam:FALSE forKey:SE_PARAMS_KEY_ASR_AUTO_STOP_BOOL]; + + + // Directive:启动引擎前调用SYNC_STOP指令,保证前一次请求结束。 + NSLog(@"Directive: SEDirectiveSyncStopEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveSyncStopEngine]; + if (ret != SENoError) { + NSLog(@"Send directive syncstop failed: %d", ret); + } else { + // Directive:启动引擎指令。 + NSLog(@"Directive: SEDirectiveStartEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + } + } + } else if (longPgr.state == UIGestureRecognizerStateEnded) { + self.talkingFinishTimestamp = [[NSDate date] timeIntervalSince1970] * 1000; + // Directive:结束音频输入。 + NSLog(@"Directive: SEDirectiveFinishTalking"); + [self.curEngine sendDirective:SEDirectiveFinishTalking]; + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.streamRecorder stop]; + } + } +} + +#pragma mark - Message Callback + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSLog(@"Message Type: %d.", type); + switch (type) { + case SEEngineStart: + // Callback: 引擎启动成功回调 + NSLog(@"Callback: 引擎启动成功"); + [self speechEngineStarted]; + break; + case SEEngineStop: + // Callback: 引擎关闭回调 + NSLog(@"Callback: 引擎关闭"); + [self speechEngineStopped]; + break; + case SEEngineError: + // Callback: 错误信息回调 + NSLog(@"Callback: 错误信息: %@", data); + [self speechEngineError:data]; + break; + case SEConnectionConnected: + NSLog(@"Callback: 建连成功"); + break; + case SEAsrPartialResult: + // Callback: ASR 当前请求的部分结果回调 + NSLog(@"Callback: ASR 当前请求的部分结果"); + [self speechEngineResult:data isFinal:FALSE]; + break; + case SEFinalResult: + // Callback: ASR 当前请求最终结果回调 + NSLog(@"Callback: ASR 当前请求最终结果"); + [self speechEngineResult:data isFinal:TRUE]; + break; + case SEVolumeLevel: + // Callback: 录音音量回调 + NSLog(@"Callback: 录音音量,%.3f", [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] floatValue]); + break; + default: + break; + } +} + +- (void)speechEngineStarted { + dispatch_async(dispatch_get_main_queue(), ^{ + self.engineStarted = true; + [self.statusTextView setText:@"Engine Started!"]; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = TRUE; + self.recordButton.enabled = FALSE; + }); +} + +- (void)speechEngineStopped { + dispatch_async(dispatch_get_main_queue(), ^{ + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.streamRecorder stop]; + } + self.engineStarted = FALSE; + [self.statusTextView setText:@"Engine Stopped!"]; + self.startEngineButton.enabled = TRUE; + self.stopEngineButton.enabled = FALSE; + self.recordButton.enabled = TRUE; + }); +} + +- (void)speechEngineResult:(NSData *)data isFinal:(BOOL)isFinal { + dispatch_async(dispatch_get_main_queue(), ^{ + // 计算由录音结束到 ASR 最终结果之间的延迟 + long response_delay = 0; + if (isFinal && self.talkingFinishTimestamp > 0) { + response_delay = [self timeDelayFrom:self.talkingFinishTimestamp]; + } + + // 从回调的 json 数据中解析 ASR 结果 + NSError *error; + NSDictionary *jsonResult = [NSJSONSerialization JSONObjectWithData:data + options:NSJSONReadingMutableContainers + error:&error]; + if (![jsonResult objectForKey:@"result"]) { + return; + } + + // 在 UI 显示 ASR 结果和延迟信息 + NSString *result = [[[jsonResult objectForKey:@"result"] firstObject] objectForKey:@"text"]; + if (result.length == 0) { + return; + } + NSMutableString *text = [[NSMutableString alloc] initWithString:@""]; + [text appendFormat:@"result: %@", result]; + [text appendFormat:@"\nreqid: %@", [jsonResult objectForKey:@"reqid"]]; + if (isFinal && response_delay > 0) { + [text appendFormat:@"\nresponse_delay: %ld", response_delay]; + } + [self setResultText:[text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; + }); +} + +- (void)speechEngineError:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + // 从回调的 json 数据中解析错误码和错误详细信息 + id error_json = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil]; + if ([error_json isKindOfClass:[NSDictionary class]]) { + NSDictionary *error_info = error_json; + + // 在 UI 显示错误信息 + if ([error_info objectForKey:@"name"] != nil) { + NSString* error_msg = [[error_json objectForKey:@"err_msg"] stringValue]; + NSString* reqid = [[error_json objectForKey:@"reqid"] stringValue]; + [self setResultText:[NSString stringWithFormat:@"reqid: %@, error: %@", reqid, error_msg]]; + } else { + [self setResultText:[NSString stringWithFormat:@"%@", error_info]]; + } + } + }); +} + +#pragma mark - Helper + +- (NSString *)getRecorderType { + SettingOptions* recorderTypeOptions = [self.settings getOptions:SETTING_RECORD_TYPE]; + switch (recorderTypeOptions.chooseIdx) { + case 0: + return SE_RECORDER_TYPE_RECORDER; + case 1: + return SE_RECORDER_TYPE_FILE; + case 2: + return SE_RECORDER_TYPE_STREAM; + default: + break; + } + return @""; +} + +- (void)setResultText:(NSString *)result { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[result stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; + }); +} + +- (long)timeDelayFrom:(long)pastTimestamp { + return [[NSDate date] timeIntervalSince1970] * 1000 - pastTimestamp; +} + +#pragma mark - UITextViewDelegate + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if([text isEqualToString:@"\n"]) { + [textView resignFirstResponder]; + return NO; + } + return YES; +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_ASR forKey:@"viewId"]; +} + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/Assets.xcassets/vad.imageset/Contents.json b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/Assets.xcassets/vad.imageset/Contents.json new file mode 100644 index 0000000..3a92327 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/Assets.xcassets/vad.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "vad_60_60.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "vad_120_120.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "vad_180_180.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/Assets.xcassets/vad.imageset/vad_120_120.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/Assets.xcassets/vad.imageset/vad_120_120.png new file mode 100644 index 0000000..ea7ea73 Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/Assets.xcassets/vad.imageset/vad_120_120.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/Assets.xcassets/vad.imageset/vad_180_180.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/Assets.xcassets/vad.imageset/vad_180_180.png new file mode 100644 index 0000000..cad3ed9 Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/Assets.xcassets/vad.imageset/vad_180_180.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/Assets.xcassets/vad.imageset/vad_60_60.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/Assets.xcassets/vad.imageset/vad_60_60.png new file mode 100644 index 0000000..1f8acbd Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/Assets.xcassets/vad.imageset/vad_60_60.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AuViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AuViewController.h new file mode 100644 index 0000000..9531f56 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AuViewController.h @@ -0,0 +1,13 @@ +// +// AuViewController.h +// SpeechDemo +// +// Created by bytedance on 2023/5/16. +// Copyright © 2023 chengzihao.ds. All rights reserved. +// + +#import + +@interface AuViewController : UIViewController + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AuViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AuViewController.m new file mode 100644 index 0000000..24d9902 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/AuViewController.m @@ -0,0 +1,467 @@ +// +// AuViewController.m +// SpeechDemo +// +// Created by bytedance on 2023/5/16. +// Copyright © 2023 chengzihao.ds. All rights reserved. +// + +#import "AuViewController.h" + +#import + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" +#import "SensitiveDefines.h" + +@interface AuViewController () + +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; +@property (weak, nonatomic) IBOutlet UIButton *engineInitButton; +@property (weak, nonatomic) IBOutlet UIButton *engineUninitButton; +@property (weak, nonatomic) IBOutlet UIButton *startEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *stopEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *longPressButton; + +@property (strong, nonatomic) SpeechEngine *curEngine; +@property (assign, nonatomic) BOOL engineStarted; + +@property (nonatomic, strong) NSString *deviceID; +@property (nonatomic, assign) long talkingFinisheTimestamp; +@property (nonatomic, assign) long startEngineTimestamp; +@property (strong, nonatomic) NSString *debugPath; + +@property (weak, nonatomic) StreamRecorder *streamRecorder; + +// settings +@property (strong, nonatomic) Settings *settings; + +@end + +static NSString *SLARDAR_AU_SERVICE_NAME = @"au_statistics"; +static NSString *SLARDAR_AU_EVENT_RESPONSE_DELAY = @"au_response_delay"; + +@implementation AuViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_AU]; + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"当前调试路径 %@", self.debugPath); + + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.longPressButton.enabled = FALSE; + [self.statusTextView setText:@"Waiting for init."]; + [self decorateTextView:self.resultTextView]; + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + UILongPressGestureRecognizer *longPgr = [[UILongPressGestureRecognizer alloc] initWithTarget:self + action:@selector(longPressTriggered:)]; + longPgr.minimumPressDuration = 0.5; + [self.longPressButton addGestureRecognizer:longPgr]; + self.streamRecorder = [ViewController getStreamRecorder]; + self.engineStarted = FALSE; +} + +- (void)viewDidDisappear:(BOOL)animated { + [self uninitEngine]; + [super viewDidDisappear:animated]; +} + +- (void)decorateTextView:(UITextView *)textView { + textView.layer.cornerRadius = 5.0f; + textView.layer.borderWidth = .25f; + textView.layer.borderColor = [UIColor grayColor].CGColor; +} + +- (void)configInitAuParams { + //【必需配置】Engine Name + [self.curEngine setStringParam:SE_AU_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + + //【可选配置】Debug & Log + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + [self.curEngine setStringParam:SE_LOG_LEVEL_DEBUG forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + //【可选配置】UID & deviceID: 用于定位线上问题 + [self.curEngine setStringParam:SDEF_UID forKey:SE_PARAMS_KEY_UID_STRING]; + //【必需配置】配置音频来源 + [self.curEngine setStringParam:[self getRecorderType] forKey:SE_PARAMS_KEY_RECORDER_TYPE_STRING]; + if ([self.settings getBool:SETTING_AU_RECORDER_SAVE]) { + //【可选配置】录音文件保存路径,如配置,SDK会将录音保存到该路径下,文件格式为 .wav + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_AU_REC_PATH_STRING]; + } + //【必需配置】鉴权相关:Appid + [self.curEngine setStringParam:[self.settings getString:SETTING_APPID] forKey:SE_PARAMS_KEY_APP_ID_STRING]; + //【必需配置】鉴权相关:Token + [self.curEngine setStringParam:[self.settings getString:SETTING_TOKEN] forKey:SE_PARAMS_KEY_APP_TOKEN_STRING]; + //【可选配置】使用的AU能力组合,默认只进行ASR识别 + [self.curEngine setIntParam:[self getAuAbility] forKey:SE_PARAMS_KEY_AU_ABILITY_INT]; + //【必需配置】识别服务域名 + [self.curEngine setStringParam:[self.settings getString:SETTING_ADDRESS] forKey:SE_PARAMS_KEY_AU_ADDRESS_STRING]; + //【必需配置】识别服务Uri + [self.curEngine setStringParam:[self.settings getString:SETTING_URI] forKey:SE_PARAMS_KEY_AU_URI_STRING]; + //【必需配置】识别服务所用集群 + [self.curEngine setStringParam:[self.settings getString:SETTING_CLUSTER] forKey:SE_PARAMS_KEY_AU_CLUSTER_STRING]; + //【可选配置】在线请求的建连与接收超时,一般不需配置使用默认值即可 + [self.curEngine setIntParam:3000 forKey:SE_PARAMS_KEY_AU_CONN_TIMEOUT_INT]; + [self.curEngine setIntParam:5000 forKey:SE_PARAMS_KEY_AU_RECV_TIMEOUT_INT]; + //【可选配置】AU处理超时,音乐流程需要额外处理时间,一般不需配置使用默认值即可 + [self.curEngine setIntParam:[self.settings getInt:SETTING_AU_PROCESS_TIMEOUT] forKey:SE_PARAMS_KEY_AU_PROCESS_TIMEOUT_INT]; + //【可选配置】AU音频包发送间隔,一般不需配置使用默认值即可 + [self.curEngine setIntParam:[self.settings getInt:SETTING_AU_AUDIO_PACKET_DURATION] forKey:SE_PARAMS_KEY_AU_AUDIO_PACKET_DURATION_INT]; + //【可选配置】AU轮询包发送间隔,一般不需配置使用默认值即可 + [self.curEngine setIntParam:[self.settings getInt:SETTING_AU_EMPTY_PACKET_INTERVAL] forKey:SE_PARAMS_KEY_AU_EMPTY_PACKET_INTERVAL_INT]; + + // 当音频来源为 RECORDER_TYPE_STREAM 时,如输入音频采样率不等于 16K,需添加如下配置 + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + if ([self.streamRecorder getSampleRate] != 16000 || [self.streamRecorder getChannel] != 1) { + // 当音频来源为 RECORDER_TYPE_STREAM 时【必需配置】,否则【无需配置】 + // 启用 SDK 内部的重采样 + [self.curEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_ENABLE_RESAMPLER_BOOL]; + // 将重采样所需的输入采样率设置为 APP 层输入的音频的实际采样率 + [self.curEngine setIntParam:[self.streamRecorder getSampleRate] forKey:SE_PARAMS_KEY_CUSTOM_SAMPLE_RATE_INT]; + [self.curEngine setIntParam:[self.streamRecorder getChannel] forKey:SE_PARAMS_KEY_CUSTOM_CHANNEL_INT]; + } + } +} + +- (void)configStartAuParams { + // Au 部分配置 + //【可选配置】用户说话最大时长,单位毫秒,默认为 150000ms. + [self.curEngine setIntParam:[self.settings getInt:SETTING_VAD_MAX_SPEECH_DURATION] forKey:SE_PARAMS_KEY_VAD_MAX_SPEECH_DURATION_INT]; + //【可选配置】用户歌唱最大时长,单位毫秒,默认为 12000ms. + [self.curEngine setIntParam:[self.settings getInt:SETTING_VAD_MAX_MUSIC_DURATION] forKey:SE_PARAMS_KEY_VAD_MAX_MUSIC_DURATION_INT]; + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + if (![self.streamRecorder start]) { + [self speechEngineNoPermission]; + return; + } + } else if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_FILE]) { + // 使用音频文件识别时,需要设置文件的绝对路径 + NSString* file_path = [NSString stringWithFormat:@"%@/%@", self.debugPath, @"au_rec_file.pcm"]; + NSLog(@"test file path: %@", file_path); + // 使用音频文件识别时【必须配置】,否则【无需配置】 + [self.curEngine setStringParam:file_path forKey:SE_PARAMS_KEY_RECORDER_FILE_STRING]; + } + + // Asr 部分配置 + //【可选配置】是否开启顺滑(DDC) + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_ENABLE_DDC] forKey:SE_PARAMS_KEY_ASR_ENABLE_DDC_BOOL]; + //【可选配置】是否开启文字转数字(ITN) + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_ENABLE_ITN] forKey:SE_PARAMS_KEY_ASR_ENABLE_ITN_BOOL]; + //【可选配置】是否开启标点 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_ENABLE_NLU_PUNC] forKey:SE_PARAMS_KEY_ASR_SHOW_NLU_PUNC_BOOL]; + //【可选配置】设置识别语种 + [self.curEngine setStringParam:[self.settings getString:SETTING_ASR_LANGUAGE] forKey:SE_PARAMS_KEY_ASR_LANGUAGE_STRING]; + //【可选配置】是否返回用户说话的语种 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_SHOW_LANGUAGE] forKey:SE_PARAMS_KEY_ASR_SHOW_LANG_BOOL]; + //【可选配置】控制识别结果返回的形式,全量返回或增量返回,默认为全量 + [self.curEngine setStringParam:[self.settings getOptionsValue:SETTING_ASR_RESULT_TYPE] forKey:SE_PARAMS_KEY_ASR_RESULT_TYPE_STRING]; + //【可选配置】设置VAD头部静音时长,用户多久没说话视为空音频,即静音检测时长 + [self.curEngine setIntParam:[self.settings getInt:SETTING_ASR_VAD_START_SILENCE_TIME] forKey:SE_PARAMS_KEY_ASR_VAD_START_SILENCE_TIME_INT]; + //【可选配置】设置VAD尾部静音时长,用户说话后停顿多久视为说话结束,即自动判停时长 + [self.curEngine setIntParam:[self.settings getInt:SETTING_ASR_VAD_END_SILENCE_TIME] forKey:SE_PARAMS_KEY_ASR_VAD_END_SILENCE_TIME_INT]; + //【可选配置】设置VAD模式,用于定制VAD场景,默认为空 + [self.curEngine setStringParam:[self.settings getString:SETTING_ASR_VAD_MODE] forKey:SE_PARAMS_KEY_ASR_VAD_MODE_STRING]; + //【可选配置】更新 ASR 热词 + if ([self.settings getString:SETTING_ASR_HOTWORDS].length != 0) { + [self setHotWords:[self.settings getString:SETTING_ASR_HOTWORDS]]; + } +} + +- (void)setHotWords:(NSString*) hotWords { + [self.curEngine sendDirective:SEDirectiveUpdateAsrHotWords data: hotWords]; +} + +#pragma mark - SpeechEngineDelegate + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSLog(@"Message Type: %d.", type); + switch (type) { + case SEEngineStart: + [self speechEngineStarted]; + break; + case SEEngineStop: + [self speechEngineStopped]; + break; + case SEEngineError: + [self speechEngineError:data]; + break; + case SEPartialResult: + [self speechEngineResult:data isFinal:FALSE]; + break; + case SEFinalResult: + [self speechEngineResult:data isFinal:TRUE]; + break; + case SEVolumeLevel: + NSLog(@"volume level: %s", (char*)data.bytes); + break; + case SEEngineLog: + NSLog(@"engine log: %s", (char*)data.bytes); + break; + default: + break; + } +} + +#pragma mark - UI Actions + +- (IBAction)initEngine:(id)sender { + [self initEngine]; +} + +- (IBAction)uninitEngine:(id)sender { + if (self.engineStarted) { + [self.statusTextView setText:@"Engine is busy, stop it first!"]; + return; + } + [self uninitEngine]; + [self.resultTextView setTextColor:UIColor.grayColor]; + [self.resultTextView setText:@"点击或按住说话后,展示语音理解结果"]; +} + +- (IBAction)startEngine:(id)sender { + NSLog(@"配置启动参数"); + [self configStartAuParams]; + [self.curEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_AU_AUTO_STOP_BOOL]; + + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + } +} + +- (IBAction)stopEngine:(id)sender { + NSLog(@"Stop engine."); + [self.curEngine sendDirective:SEDirectiveStopEngine]; +} + +- (void)longPressTriggered:(UILongPressGestureRecognizer *)longPgr { + if (longPgr.state == UIGestureRecognizerStateBegan) { + NSLog(@"Long press begin."); + + [self setResultText:@""]; + NSLog(@"配置启动参数"); + [self configStartAuParams]; + [self.curEngine setBoolParam:FALSE forKey:SE_PARAMS_KEY_AU_AUTO_STOP_BOOL]; + + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + } + } else if (longPgr.state == UIGestureRecognizerStateEnded) { + NSLog(@"Long press ended."); + self.talkingFinisheTimestamp = [[NSDate date] timeIntervalSince1970] * 1000; + [self.curEngine sendDirective:SEDirectiveFinishTalking]; + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.streamRecorder stop]; + } + } +} + +#pragma mark - Init Methods + +- (void)initEngine { + AppDelegate *appDelegate = [ViewController getAppDelegate]; + if (appDelegate == nil) { + appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; + } + if (appDelegate.deviceID.length < 1) { + self.engineInitButton.enabled = FALSE; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:@"Waiting for get deviceID."]; + sleep(1); + [self initEngine]; + }); + return; + } + [ViewController setAppDelegate:appDelegate]; + self.deviceID = appDelegate.deviceID; + + + if (self.curEngine == nil) { + self.curEngine = [[SpeechEngine alloc] init]; + } + if (![self.curEngine createEngineWithDelegate:self]) { + NSLog(@"Create speech engine failed."); + return; + } + + [self.resultTextView setTextColor:UIColor.blackColor]; + + NSLog(@"SDK 版本号: %@", [self.curEngine getVersion]); + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"当前调试路径: %@", self.debugPath); + + NSLog(@"配置初始化参数"); + [self configInitAuParams]; + + NSLog(@"引擎初始化"); + SEEngineErrorCode ret = [self.curEngine initEngine]; + if (ret != SENoError) { + NSLog(@"初始化失败,返回值: %ld", ret); + } + if (ret == SENoError) { + [self speechEngineInitOk]; + } else { + [self speechEngineInitFailed]; + } +} + +- (NSString *)getRecorderType { + SettingOptions* recorderTypeOptions = [self.settings getOptions:SETTING_RECORD_TYPE]; + switch (recorderTypeOptions.chooseIdx) { + case 0: + return SE_RECORDER_TYPE_RECORDER; + case 1: + return SE_RECORDER_TYPE_FILE; + case 2: + return SE_RECORDER_TYPE_STREAM; + default: + break; + } + return @""; +} + +- (int)getAuAbility { + SettingOptions* auAbilityOptions = [self.settings getOptions:SETTING_AU_ABILITY]; + switch (auAbilityOptions.chooseIdx) { + case 0: + return SEAuAbilityAsr; + case 1: + return SEAuAbilityMusic; + case 2: + return SEAuAbilityAsr | SEAuAbilityMusic; + default: + return SEAuAbilityAsr; + } +} + +- (void)uninitEngine { + [self.curEngine destroyEngine]; + self.curEngine = nil; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.longPressButton.enabled = FALSE; +} + +#pragma mark - Engine Callback + +- (void)speechEngineNoPermission { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"No permission!"]; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + }); +} + +- (void)speechEngineInitOk { + [self.streamRecorder setSpeechEngine:VIEW_AU engine:self.curEngine]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:@"Ready"]; + [self.resultTextView setText:[NSString stringWithFormat:@"DeviceID: %@", self.deviceID]]; + self.engineUninitButton.enabled = TRUE; + self.engineInitButton.enabled = FALSE; + self.startEngineButton.enabled = TRUE; + self.longPressButton.enabled = TRUE; + }); +} + +- (void)speechEngineInitFailed { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"Failed to init engine!"]; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + }); +} + +- (void)speechEngineStarted { + dispatch_async(dispatch_get_main_queue(), ^{ + self.startEngineTimestamp = [[NSDate date] timeIntervalSince1970] * 1000; + self.engineStarted = true; + [self.statusTextView setText:@"Engine Started!"]; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = TRUE; + self.longPressButton.enabled = FALSE; + }); +} + +- (void)speechEngineStopped { + dispatch_async(dispatch_get_main_queue(), ^{ + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.streamRecorder stop]; + } + self.engineStarted = FALSE; + [self.statusTextView setText:@"Engine Stopped!"]; + self.startEngineButton.enabled = TRUE; + self.stopEngineButton.enabled = FALSE; + self.longPressButton.enabled = TRUE; + }); +} + +- (void)speechEngineResult:(NSData *)data isFinal:(BOOL)isFinal { + dispatch_async(dispatch_get_main_queue(), ^{ + long response_delay = 0; + if (isFinal && self.talkingFinisheTimestamp > 0) { + response_delay = [self timeDelayFrom:self.talkingFinisheTimestamp]; + self.talkingFinisheTimestamp = 0; + } + + NSMutableString *text = [[NSMutableString alloc] initWithString:@""]; + [text appendFormat:@"result: %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]]; + if (isFinal) { + [text appendFormat:@"\nresponse_delay: %ld", response_delay]; + } + [self.resultTextView setText:[text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; + }); +} + +- (void)speechEngineError:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]]; +// [self stopEngine:nil]; + }); +} + +- (void)setResultText:(NSString *)result { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[result stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; + }); +} + +#pragma mark - Helper + +- (long)timeDelayFrom:(long)pastTimestamp { + return [[NSDate date] timeIntervalSince1970] * 1000 - pastTimestamp; +} + +#pragma mark - UITextViewDelegate + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if([text isEqualToString:@"\n"]) { + [textView resignFirstResponder]; + return NO; + } + return YES; +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_AU forKey:@"viewId"]; +} + +@end + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/BiTTSViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/BiTTSViewController.h new file mode 100644 index 0000000..560c635 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/BiTTSViewController.h @@ -0,0 +1,17 @@ +// +// BiTTSViewController.h +// SpeechDemo +// +// Created by ByteDance on 2025/7/3. +// Copyright © 2025 fangweiwei. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface BiTTSViewController : UIViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/BiTTSViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/BiTTSViewController.m new file mode 100644 index 0000000..cd8c8a8 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/BiTTSViewController.m @@ -0,0 +1,778 @@ +// +// BiTTSViewController.m +// SpeechDemo +// +// Created by ByteDance on 2025/7/3. +// Copyright © 2025 fangweiwei. All rights reserved. +// + +#import "BiTTSViewController.h" +#include +#include + +#import + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" +#import "SensitiveDefines.h" + +static int TTS_MAX_RETRY_COUNT = 3; + +@interface BiTTSViewController () + +@property (weak, nonatomic) IBOutlet UITextView *referTextView; +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; +@property (weak, nonatomic) IBOutlet UIButton *engineSwitchButton; +@property (weak, nonatomic) IBOutlet UIButton *startEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *stopEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *synthesisButton; +@property (weak, nonatomic) IBOutlet UIButton *pauseResumeButton; + +// Device ID: 用于定位线上问题 +@property (nonatomic, strong) NSString *deviceID; +// Debug Path: 用于存放一些 SDK 相关的文件,比如模型、日志等 +@property (strong, nonatomic) NSString *debugPath; + +// SpeechEngine +@property (strong, nonatomic) SpeechEngine *curEngine; + +// Engine State +@property (assign, nonatomic) BOOL engineInited; +@property (assign, nonatomic) BOOL engineStarted; +@property (assign, nonatomic) BOOL engineErrorOccurred; +@property (assign, nonatomic) BOOL playerPaused; +@property (assign, nonatomic) BOOL refTextChanged; +// Settings +@property (strong, nonatomic) Settings *settings; + +// 一些在线合成的配置 +@property (strong, nonatomic) NSString *ttsAppId; +@property (strong, nonatomic) NSString *ttsVoiceOnline; +@property (strong, nonatomic) NSString *ttsVoiceTypeOnline; + + +// tts合成文本相关 +@property (assign, nonatomic) int ttsSynthesisIndex; +@property (strong, nonatomic) NSMutableArray* ttsSynthesisText; +@property (strong, nonatomic) NSMutableDictionary* ttsSynthesisMap; + +@property (assign, nonatomic) int ttsRetryCount; + + +@end + +@implementation BiTTSViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_BITTS]; + + self.engineSwitchButton.enabled = TRUE; + [self decorateTextView:self.referTextView]; + [self decorateTextView:self.resultTextView]; + [self.referTextView setDelegate:self]; + self.referTextView.editable = TRUE; + self.engineInited = FALSE; + self.engineStarted = FALSE; + self.engineErrorOccurred = FALSE; + self.playerPaused = FALSE; + + // 初始化和tts有关的字段 + self.ttsSynthesisIndex = 0; + self.ttsSynthesisText = [[NSMutableArray alloc] init]; + self.ttsSynthesisMap = [[NSMutableDictionary alloc]init]; + + self.ttsRetryCount = TTS_MAX_RETRY_COUNT; + + [self.referTextView setText:@"愿中国青年都摆脱冷气,只是向上走,不必听自暴自弃者流的话。能做事的做事,能发声的发声。有一分热,发一分光。就令萤火一般,也可以在黑暗里发一点光,不必等候炬火。此后如竟没有炬火:我便是唯一的光。"]; + [self.statusTextView setText:@"Waiting for init."]; + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"当前调试路径 %@", self.debugPath); + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(audioInterruptionHandler:) + name:AVAudioSessionInterruptionNotification + object:nil]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; +} + +- (void)decorateTextView:(UITextView *)textView { + textView.layer.cornerRadius = 5.0f; + textView.layer.borderWidth = .25f; + textView.layer.borderColor = [UIColor grayColor].CGColor; +} + +#pragma mark - Notifications + +-(void)appWillTerminate:(NSNotification*)note { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:AVAudioSessionInterruptionNotification + object:nil]; +} + +- (void)audioInterruptionHandler:(NSNotification*)notification { + AVAudioSessionInterruptionType interruptionType = (AVAudioSessionInterruptionType)[[notification.userInfo objectForKey:AVAudioSessionInterruptionTypeKey] unsignedIntegerValue]; + AVAudioSessionInterruptionOptions intertuptionOptions = [[notification.userInfo objectForKey:AVAudioSessionInterruptionOptionKey] unsignedIntValue]; + NSLog(@"Receive audio interruption notification, type: %lu, options: %lu.", (unsigned long)interruptionType, (unsigned long)intertuptionOptions); + if (interruptionType == AVAudioSessionInterruptionTypeBegan) { + NSLog(@"Audio session interruption began"); + @synchronized (self) { + [self pausePlayback]; + } + } else if (interruptionType == AVAudioSessionInterruptionTypeEnded) { + @synchronized (self) { + NSLog(@"Audio session interruption ended"); + if (intertuptionOptions == AVAudioSessionInterruptionOptionShouldResume) { + AVAudioSession *session = [AVAudioSession sharedInstance]; + AVAudioSessionCategoryOptions cur_options = session.categoryOptions; + // AudioQueueStart() will return AVAudioSessionErrorCodeCannotInterruptOthers if options didn't contains AVAudioSessionCategoryOptionMixWithOthers + if (!(cur_options & AVAudioSessionCategoryOptionMixWithOthers)) { + AVAudioSessionCategoryOptions readyOptions = AVAudioSessionCategoryOptionMixWithOthers | cur_options; + [session setCategory:AVAudioSessionCategoryPlayback withOptions:readyOptions error:nil]; + } + [self resumePlayback]; + + cur_options = session.categoryOptions; + // Remove AVAudioSessionCategoryOptionMixWithOthers, or the playback will not be interrupted any more + if (cur_options & AVAudioSessionCategoryOptionMixWithOthers) { + [session setCategory:AVAudioSessionCategoryPlayback withOptions:((~AVAudioSessionCategoryOptionMixWithOthers) & cur_options) error:nil]; + } + } + } + } +} + +#pragma mark - Config & Init & Uninit Methods + +-(void)configInitParams { + //【必需配置】Engine Name + [self.curEngine setStringParam:SE_BITTS_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + + + //【可选配置】Debug & Log + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + [self.curEngine setStringParam:SE_LOG_LEVEL_DEBUG forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + + //【可选配置】User ID(用以辅助定位线上用户问题) + [self.curEngine setStringParam:SDEF_UID forKey:SE_PARAMS_KEY_UID_STRING]; + [self.curEngine setStringParam:self.deviceID forKey:SE_PARAMS_KEY_DEVICE_ID_STRING]; + + //【可选配置】是否将合成出的音频保存到设备上,为 true 时需要正确配置 PARAMS_KEY_TTS_AUDIO_PATH_STRING 才会生效 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_ENABLE_DUMP] + forKey:SE_PARAMS_KEY_TTS_ENABLE_DUMP_BOOL]; + // TTS 音频文件保存目录,必须在合成之前创建好且 APP 具有访问权限,保存的音频文件名格式为 tts_{reqid}.wav, {reqid} 是本次合成的请求 id + // PARAMS_KEY_TTS_ENABLE_DUMP_BOOL 配置为 true 的音频时为【必需配置】,否则为【可选配置】 + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_TTS_AUDIO_PATH_STRING]; + //【可选配置】自定义请求Header + NSString* headerStr = [self.settings getString:SETTING_REQUEST_HEADERS]; + if ([headerStr isEqualToString:@""]) { + headerStr = @"{}"; + } + [self.curEngine setStringParam:headerStr forKey:SE_PARAMS_KEY_REQUEST_HEADERS_STRING]; + + //【可选配置】是否使用 SDK 内置播放器播放合成出的音频,默认为 true + [self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_ENABLE_PLAYER] + forKey:SE_PARAMS_KEY_TTS_ENABLE_PLAYER_BOOL]; + //【可选配置】是否令 SDK 通过回调返回合成的音频数据,默认不返回。 + // 开启后,SDK 会流式返回音频,收到 SETtsAudioData 回调表示当次合成所有的音频已经全部返回 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ENABLE_PLAYER_AUDIO_CALL_BACK] ? SETtsDataCallbackModeAll : SETtsDataCallbackModeNone forKey:SE_PARAMS_KEY_ENABLE_PLAYER_AUDIO_CALLBACK_BOOL]; + + // ------------------------ 在线合成相关配置 ----------------------- + + NSString* appid = [self.settings getString:SETTING_APPID]; + self.ttsAppId = appid.length > 0 ? appid : SDEF_APPID; + //【必需配置】在线合成鉴权相关:Appid + [self.curEngine setStringParam:self.ttsAppId forKey:SE_PARAMS_KEY_APP_ID_STRING]; + + NSString* token = [self.settings getString:SETTING_TOKEN]; + NSString* ttsAppToken = token.length > 0 ? token : SDEF_TOKEN; + //【必需配置】在线合成鉴权相关:Token + [self.curEngine setStringParam:ttsAppToken forKey:SE_PARAMS_KEY_APP_TOKEN_STRING]; + + + NSString *address = [self.settings getString:SETTING_ADDRESS]; + NSString *ttsAddress = address.length > 0 ? address : SDEF_DEFAULT_ADDRESS; + //【必需配置】语音合成服务域名 + [self.curEngine setStringParam:ttsAddress forKey:SE_PARAMS_KEY_TTS_ADDRESS_STRING]; + + NSString *uri = [self.settings getString:SETTING_URI]; + NSString *ttsUri = uri.length > 0 ? uri : SDEF_BITTS_DEFAULT_URI; + //【必需配置】语音合成服务Uri + [self.curEngine setStringParam:ttsUri forKey:SE_PARAMS_KEY_TTS_URI_STRING]; + + NSString *resourceId = [self.settings getString:SETTING_RESOURCE_ID]; + //【必需配置】语音合成服务资源id + [self.curEngine setStringParam:resourceId forKey: SE_PARAMS_KEY_RESOURCE_ID_STRING]; + //【可选配置】TTS连接超时时间 + [self.curEngine setIntParam:10000 forKey:SE_PARAMS_KEY_TTS_CONN_TIMEOUT_INT]; + +} + +-(void)configStartTtsParams { + // 准备待合成的文本 + if(![self prepareBiTTSText]) { + char fake_error_info[] = "{err_code:3006, err_msg:\"Invalid input text.\"}"; + [self speechEngineError:[NSData dataWithBytes:fake_error_info length:sizeof(fake_error_info)]]; + return; + } +} + +- (NSString*)getSynthesisText { + if(self.refTextChanged){ //如果文本有变更重新追加进去 + self.refTextChanged = FALSE; + + __block NSString* refText = nil; + if (![NSThread isMainThread]) { + dispatch_sync(dispatch_get_main_queue(), ^{ + refText = self.referTextView.text; + }); + } else { + refText = self.referTextView.text; // 已经在主线程,直接赋值 + } + + if (refText.length > 0) { + NSError *error = nil; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[^;!?。!?;…]+[;!?。!?;…]?" options:0 error:&error]; + + if (!error) { + NSArray *matches = [regex matchesInString:refText options:0 range:NSMakeRange(0, refText.length)]; + + for (NSTextCheckingResult *match in matches) { + NSString *sentence = [refText substringWithRange:match.range]; + [self addSentence:sentence]; + } + } + } + } + + if(self.ttsSynthesisIndex == [self.ttsSynthesisText count]){ + NSLog(@"no more tts conttent to synthesis"); + [self updateTtsResultText:@"No More Text to Synthesis"]; + return nil; + } + + NSString* text = self.ttsSynthesisText[self.ttsSynthesisIndex]; + NSLog(@"Synthesis: %d, text: %@", self.ttsSynthesisIndex, text); + self.ttsSynthesisIndex = (self.ttsSynthesisIndex + 1); + return text; +} + +- (void)initEngine { + NSLog(@"获取设备ID,调试使用"); + AppDelegate *appDelegate = [ViewController getAppDelegate]; + if (appDelegate == nil) { + appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; + } + [ViewController setAppDelegate:appDelegate]; + self.deviceID = appDelegate.deviceID; + NSLog(@"获取设备ID成功: %@", self.deviceID); + + + NSLog(@"创建引擎"); + if (self.curEngine == nil) { + self.curEngine = [[SpeechEngine alloc] init]; + if (![self.curEngine createEngineWithDelegate:self]) { + NSLog(@"引擎创建失败."); + return; + } + } + NSLog(@"SDK 版本号: %@", [self.curEngine getVersion]); + [self initEngineInternal]; +} + + + +- (void)initEngineInternal { + NSLog(@"配置初始化参数"); + [self configInitParams]; + + NSLog(@"引擎初始化"); + SEEngineErrorCode ret = [self.curEngine initEngine]; + self.engineInited = (ret == SENoError); + if (self.engineInited) { + NSLog(@"初始化成功"); + [self speechEngineInitSucceeded]; + } else { + NSLog(@"初始化失败,返回值: %d", ret); + [self speechEngineInitFailed:ret]; + } +} + +- (void)uninitEngine { + if (self.curEngine != nil) { + NSLog(@"引擎析构"); + [self.curEngine destroyEngine]; + self.curEngine = nil; + NSLog(@"引擎析构完成"); + } +} + +#pragma mark - UI Actions + +- (IBAction)switchEngine:(id)sender { + if (self.engineStarted) { + [self.statusTextView setText:@"Engine is busy, stop it first!"]; + return; + } + [self clearResult:nil]; + self.startEngineButton.enabled = FALSE; + self.synthesisButton.enabled = FALSE; + self.pauseResumeButton.enabled = FALSE; + if (self.engineInited) { + self.referTextView.editable = TRUE; + [self uninitEngine]; + self.engineInited = FALSE; + [self.statusTextView setText:@"Waiting for init."]; + self.engineSwitchButton.enabled = TRUE; + [self.engineSwitchButton setTitle:@"Init Engine" forState:UIControlStateNormal]; + self.stopEngineButton.enabled = FALSE; + } else { + self.referTextView.editable = TRUE; + [self initEngine]; + } +} + +- (IBAction)Synthesis:(id)sender { + [self triggerSynthesis]; +} + +- (IBAction)startEngineBtnClicked:(id)sender { + NSLog(@"Start engine, current status: %d", self.engineStarted); + if (!self.engineStarted) { + [self clearResult:nil]; + self.engineErrorOccurred = FALSE; + + // Directive:启动引擎前调用SYNC_STOP指令,保证前一次请求结束。 + NSLog(@"关闭引擎(同步)"); + NSLog(@"Directive: SEDirectiveSyncStopEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveSyncStopEngine]; + if (ret != SENoError) { + NSLog(@"Send directive syncstop failed: %d", ret); + } else { + [self configStartTtsParams]; + + NSLog(@"启动引擎."); + NSLog(@"Directive: SEDirectiveStartEngine"); + NSString* startPayload = @"{\"req_params\":{\"speaker\":\"zh_female_roumeinvyou_emo_v2_mars_bigtts\",\"audio_params\":{\"emotion\":\"excited\",\"loudness_rate\":50}}}"; + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine data:startPayload]; + if (SENoError != ret) { + NSString* message = [NSString stringWithFormat:@"发送启动引擎指令失败: %d", ret]; + [self sendStartEngineDirectiveFailed:message]; + } + } + } +} + +- (IBAction)stopEngineBtnClicked:(id)sender { + NSLog(@"关闭引擎"); + NSLog(@"Directive: SEDirectiveStopEngine"); + [self.curEngine sendDirective:SEDirectiveStopEngine]; +} + +- (void)pausePlayback { + NSLog(@"暂停播放"); + NSLog(@"Directive: SEDirectivePausePlayer"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectivePausePlayer]; + if (ret == SENoError) { + self.playerPaused = TRUE; + [self.pauseResumeButton setTitle:@"Resume" forState:UIControlStateNormal]; + } + NSLog(@"Pause playback status: %d", ret); +} + +- (void)resumePlayback { + NSLog(@"继续播放"); + NSLog(@"Directive: SEDirectiveResumePlayer"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveResumePlayer]; + if (ret == SENoError) { + self.playerPaused = FALSE; + [self.pauseResumeButton setTitle:@"Pause" forState:UIControlStateNormal]; + } + NSLog(@"Resume playback status: %d", ret); +} + +- (IBAction)controlPlayingStatus:(id)sender { + NSLog(@"Pause or resume player, current player status: %hhd", self.playerPaused); + if (self.playerPaused) { + [self resumePlayback]; + } else { + [self pausePlayback]; + } +} + +- (IBAction)clearResult:(id)sender { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:@""]; + }); +} + +#pragma mark - Message Callback + + + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSLog(@"Message Type: %d.", type); + switch (type) { + case SEEngineStart: + NSLog(@"Callback: 引擎启动成功: data: %@", data); + [self speechEngineStarted]; + break; + case SEEngineStop: + NSLog(@"Callback: 引擎关闭: data: %@", data); + [self speechEngineStopped]; + break; + case SEEngineError: + NSLog(@"Callback: 错误信息: %@", data); + [self speechEngineError:data]; + break; + case SEEventConnectionStarted: + NSLog(@"Callback: SEEventConnectionStarted"); + break; + case SEEventConnectionFailed: + NSLog(@"Callback: SEEventConnectionFailed"); + break; + case SEEventConnectionFinished: + NSLog(@"Callback: SEEventConnectionFinished"); + break; + case SEEventSessionStarted: + NSLog(@"Callback: SEEventSessionStarted"); + break; + case SEEventSessionCanceled: + NSLog(@"Callback: SEEventSessionCanceled"); + break; + case SEEventSessionFinished: + NSLog(@"Callback: SEEventSessionFinished"); + break; + case SEEventSessionFailed: + NSLog(@"Callback: SEEventSessionFailed"); + break; + case SEEventTTSSentenceStart: + [self speechStartSynthesis:data]; + NSLog(@"Callback: 合成开始 SentenceStart: %@", data); + break; + case SEEventTTSSentenceEnd: + [self speechFinishSynthesis:data]; + NSLog(@"Callback: 合成结束 SentenceEnd: %@", data); + case SEEventTTSResponse: + NSLog(@"Callback: 收到合成音频 TTSResponse: %@", data); + case SEEventTTSEnded: + NSLog(@"Callback: TTSEnd: %@", data); + case SEPlayerAudioData: +// NSLog(@"Callback: 播放的pcm音频: %@", data); + break; + case SEPlayerStartPlayAudio: + NSLog(@"Callback: 开始播放TTS音频"); + [self speechStartPlaying:nil]; + break; + case SEPlayerFinishPlayAudio: + NSLog(@"Callback: 结束播放TTS音频"); + [self speechFinishPlaying:nil]; + break; + default: + break; + } +} + +- (void)speechEngineInitSucceeded { + dispatch_async(dispatch_get_main_queue(), ^{ + self.engineSwitchButton.enabled = TRUE; + [self.engineSwitchButton setTitle:@"UninitEngine" forState:UIControlStateNormal]; + [self.statusTextView setText:@"Ready"]; + [self.resultTextView setText:[NSString stringWithFormat:@"DeviceID: %@.", self.deviceID]]; + self.referTextView.editable = TRUE; + self.startEngineButton.enabled = TRUE; + }); +} + +- (void)speechEngineInitFailed:(int)initStatus { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:[[NSString alloc] initWithFormat:@"Failed to init engine, %d!", initStatus]]; + self.engineSwitchButton.enabled = TRUE; + }); +} + +- (void)sendSynthesisDirectiveFailed:(NSString*)tipText { + NSLog(@"%@", tipText); + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:tipText]; + [self.curEngine sendDirective:SEDirectiveStopEngine]; + }); +} + +- (void)sendStartEngineDirectiveFailed:(NSString*)tipText { + NSLog(@"%@", tipText); + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:tipText]; + self.engineStarted = FALSE; + }); +} + +- (void)speechEngineStarted { + self.ttsRetryCount = TTS_MAX_RETRY_COUNT; + dispatch_async(dispatch_get_main_queue(), ^{ + self.referTextView.editable = TRUE; + self.engineStarted = true; + [self.statusTextView setText:@"Engine Started!"]; + self.startEngineButton.enabled = FALSE; + self.synthesisButton.enabled = TRUE; + self.stopEngineButton.enabled = TRUE; + }); +} + +- (void)speechEngineStopped { + dispatch_async(dispatch_get_main_queue(), ^{ + self.referTextView.editable = TRUE; + self.engineStarted = FALSE; + [self.statusTextView setText:@"Engine Stopped!"]; + self.startEngineButton.enabled = TRUE; + self.synthesisButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + [self.pauseResumeButton setTitle:@"Pause" forState:UIControlStateNormal]; + self.pauseResumeButton.enabled = FALSE; + self.playerPaused = FALSE; + }); +} + +- (void)speechEngineError:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + + BOOL needStop = NO; + id json_obj = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil]; + if ([json_obj isKindOfClass:[NSDictionary class]]) { + NSDictionary *error_info = json_obj; + NSInteger code = [[error_info objectForKey:@"err_code"] intValue]; + switch (code) { + case SETTSLimitQps: + case SETTSLimitCount: + case SETTSServerBusy: + case SETTSLongText: + case SETTSInvalidText: + case SETTSSynthesisTimeout: + case SETTSSynthesisError: + case SETTSSynthesisWaitingTimeout: + case SETTSErrorUnknown: + NSLog(@"When meeting this kind of error, continue to synthesize."); + [self triggerSynthesis]; + break; + case SEConnectTimeout: + case SEReceiveTimeout: + case SENetLibError: + // 遇到网络错误时建议重试,重试次数不超过 3 次 + needStop = ![self retrySynthesis]; + if (needStop) { + self.engineErrorOccurred = TRUE; + } + break; + default: + needStop = YES; + self.engineErrorOccurred = TRUE; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView + setText:[[NSString alloc] + initWithData:data + encoding:NSUTF8StringEncoding]]; + }); + break; + } + } else { + needStop = YES; + } + if (needStop) { + [self.curEngine sendDirective:SEDirectiveStopEngine]; + } + }); +} + +- (void)scrollTextViewToBottom { + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.resultTextView.text.length > 0) { + NSRange bottomRange = NSMakeRange(self.resultTextView.text.length - 1, 1); + [self.resultTextView scrollRangeToVisible:bottomRange]; + [self.resultTextView layoutIfNeeded]; + } + }); +} + +-(void)updateTtsResultText:(NSString*) playingId { + if (self.engineErrorOccurred) { + NSLog(@"When a fatal error occurs, prevent the playback text from being displayed."); + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + NSString *currentText = self.resultTextView.text ?: @""; + NSString *newText = [NSString stringWithFormat:@"%@\n%@", currentText, playingId]; + [self.resultTextView setText:newText]; + }); + + [self scrollTextViewToBottom]; +} + +- (void)speechStartSynthesis:(NSData *)data { + if (self.ttsSynthesisIndex < [self.ttsSynthesisText count]) { + NSString* req_id = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + [self.ttsSynthesisMap setValue:[NSNumber numberWithInt:self.ttsSynthesisIndex] forKey:req_id]; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + self.synthesisButton.enabled = FALSE; + }); +} + +- (void)speechFinishSynthesis:(NSData *)data { + if (self.ttsRetryCount < TTS_MAX_RETRY_COUNT) { + self.ttsRetryCount = TTS_MAX_RETRY_COUNT; + } + dispatch_async(dispatch_get_main_queue(), ^{ + self.synthesisButton.enabled = TRUE; + }); +} + +- (void)speechStartPlaying:(NSData *)data { + NSLog(@"TTS start playing"); + dispatch_async(dispatch_get_main_queue(), ^{ + self.pauseResumeButton.enabled = TRUE; + }); +} + + +- (void)speechFinishPlaying :(NSData *)data { + NSLog(@"TTS finish playing"); + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateTtsResultText:@"playing finished"]; + }); + [self triggerSynthesis]; + +} + +- (void)speechTtsAudioData:(NSData *)data { +} + +- (BOOL)retrySynthesis { + BOOL ret = FALSE; + if (self.engineStarted && self.ttsRetryCount > 0) { + NSLog(@"Retry synthesis for text: %@", self.ttsSynthesisText[self.ttsSynthesisIndex]); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [self triggerSynthesis]; + }); + self.ttsRetryCount -= 1; + ret = TRUE; + } + return ret; +} + +-(void)triggerSynthesis { + NSString *text = [self getSynthesisText]; + if(text == nil){ + return; + } + + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveEventStartSession data:@""]; + if (ret != SENoError) { + NSLog(@"Start Session faile: %d", ret); + } + + NSString* taskRequestJson = [NSString stringWithFormat: @"{\"req_params\":{\"text\":\"%@\"}}", text]; + // SEDirectiveTaskRequest 是可以连续发送的,服务端会缓存每一句文本,然后按顺序通过sentence_start 、 task respnse... sentence_end返回当前处理句的结果 + ret = [self.curEngine sendDirective:SEDirectiveEventTaskRequest data:taskRequestJson]; + [self updateTtsResultText:text]; + NSLog(@"触发合成"); + NSLog(@"Directive: SEDirectiveTaskRequest"); + if (ret != SENoError) { + NSLog(@"Synthesis faile: %d", ret); + } + + ret = [self.curEngine sendDirective:SEDirectiveEventFinishSession data:@""]; + if (ret != SENoError) { + NSLog(@"Finish Session faile: %d", ret); + } +} + +-(void)addSentence:(NSString*) text { + NSCharacterSet* blankChar = [NSCharacterSet characterSetWithCharactersInString:@" "]; + NSString* tmp = [text stringByTrimmingCharactersInSet:blankChar]; + if (tmp.length > 0) { + [self.ttsSynthesisText addObject:tmp]; + } +} + +-(void)resetTtsContext { + self.ttsSynthesisIndex = 0; + [self.ttsSynthesisText removeAllObjects]; + [self.ttsSynthesisMap removeAllObjects]; +} + +-(BOOL)prepareBiTTSText { + [self resetTtsContext]; + __block NSString* text = nil; + if (![NSThread isMainThread]) { + dispatch_sync(dispatch_get_main_queue(), ^{ + text = self.referTextView.text; + }); + } else { + text = self.referTextView.text; // 已经在主线程,直接赋值 + } + if (text.length <= 0) { + text = @"愿中国青年都摆脱冷气,只是向上走,不必听自暴自弃者流的话。能做事的做事,能发声的发声。有一分热,发一分光。就令萤火一般,也可以在黑暗里发一点光,不必等候炬火。此后如竟没有炬火:我便是唯一的光。"; + } + if (self.ttsSynthesisText == nil || [self.ttsSynthesisText count] <= 0) { + // 使用下面几个标点符号来分句,会让通过 MESSAGE_TYPE_TTS_PLAYBACK_PROGRESS 返回的播放进度更加准确 + NSArray* temp = [text componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@";!?。!?;…"]]; + if (text.length > 0) { + NSError *error = nil; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[^;!?。!?;…]+[;!?。!?;…]?" options:0 error:&error]; + + if (!error) { + NSArray *matches = [regex matchesInString:text options:0 range:NSMakeRange(0, text.length)]; + + for (NSTextCheckingResult *match in matches) { + NSString *sentence = [text substringWithRange:match.range]; + [self addSentence:sentence]; + } + } + } + } + NSLog(@"Synthesis text item num: %ld.", [self.ttsSynthesisText count]); + return [self.ttsSynthesisText count] > 0; +} + +#pragma mark - Helper + +- (long)timeDelayFrom:(long)pastTimestamp { + return [[NSDate date] timeIntervalSince1970] * 1000 - pastTimestamp; +} + + +#pragma mark - UITextViewDelegate + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if([text isEqualToString:@"\n"]) { + [textView resignFirstResponder]; + return NO; + } + return YES; +} +- (void)textViewDidChange:(UITextView *)textView { + if(textView == _referTextView){ + self.refTextChanged = TRUE; + } +} +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_BITTS forKey:@"viewId"]; +} + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/BigAsrViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/BigAsrViewController.h new file mode 100644 index 0000000..7ff76f8 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/BigAsrViewController.h @@ -0,0 +1,13 @@ +// +// BigAsrViewController.h +// SpeechDemo +// +// Created by bytedance on 2024/9/9. +// Copyright © 2024 chengzihao.ds. All rights reserved. +// + +#import + +@interface BigAsrViewController : UIViewController + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/BigAsrViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/BigAsrViewController.m new file mode 100644 index 0000000..0256aca --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/BigAsrViewController.m @@ -0,0 +1,465 @@ +// +// BigAsrViewController.m +// SpeechDemo +// +// Created by bytedance on 2024/9/9. +// Copyright © 2024 chengzihao.ds. All rights reserved. +// + +#import "BigAsrViewController.h" + +#import + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" +#import "SensitiveDefines.h" + +@interface BigAsrViewController () + +// UI +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; +@property (weak, nonatomic) IBOutlet UIButton *initialEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *uninitialEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *startEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *stopEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *recordButton; + +// Device ID: 用于定位线上问题 +@property (nonatomic, strong) NSString *deviceID; +// Debug Path: 用于存放一些 SDK 相关的文件,比如模型、日志等 +@property (strong, nonatomic) NSString *debugPath; + +// SpeechEngine +@property (strong, nonatomic) SpeechEngine *curEngine; +@property (assign, nonatomic) BOOL engineStarted; + +// Settings +@property (strong, nonatomic) Settings *settings; + +// APP 层自定义的录音机,在音频来源为 Stream 时使用 +@property (weak, nonatomic) StreamRecorder *streamRecorder; + +@end + +@implementation BigAsrViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.initialEngineButton.enabled = TRUE; + self.uninitialEngineButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.recordButton.enabled = FALSE; + + [self.statusTextView setText:@"Waiting for init."]; + [self decorateTextView:self.resultTextView]; + + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + UILongPressGestureRecognizer *longPgr = [[UILongPressGestureRecognizer alloc] initWithTarget:self + action:@selector(recordTriggered:)]; + longPgr.minimumPressDuration = 0.5; + [self.recordButton addGestureRecognizer:longPgr]; + + self.streamRecorder = [ViewController getStreamRecorder]; + self.engineStarted = FALSE; + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_BIGASR]; + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"当前调试路径 %@", self.debugPath); +} + +- (void)viewDidDisappear:(BOOL)animated { + [self uninitEngine]; + [super viewDidDisappear:animated]; +} + +- (void)decorateTextView:(UITextView *)textView { + textView.layer.cornerRadius = 5.0f; + textView.layer.borderWidth = .25f; + textView.layer.borderColor = [UIColor grayColor].CGColor; +} + +#pragma mark - Config & Init & Uninit Methods + +-(void)configInitParams{ + //【必需配置】Engine Name + [self.curEngine setStringParam:SE_ASR_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + + //【可选配置】Debug & Log + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + [self.curEngine setStringParam:SE_LOG_LEVEL_DEBUG forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + + //【可选配置】UID & deviceID: 用于定位线上问题 + [self.curEngine setStringParam:SDEF_UID forKey:SE_PARAMS_KEY_UID_STRING]; + [self.curEngine setStringParam:self.deviceID forKey:SE_PARAMS_KEY_DEVICE_ID_STRING]; + + //【必需配置】配置音频来源 + [self.curEngine setStringParam:[self getRecorderType] forKey:SE_PARAMS_KEY_RECORDER_TYPE_STRING]; + + if ([self.settings getBool:SETTING_ASR_RECORDER_SAVE]) { + //【可选配置】录音文件保存路径,如配置,SDK会将录音保存到该路径下,文件格式为 .wav + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_ASR_REC_PATH_STRING]; + } + + // 当音频来源为 RECORDER_TYPE_STREAM 时,如输入音频采样率不等于 16K,需添加如下配置 + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + if ([self.streamRecorder getSampleRate] != 16000 || [self.streamRecorder getChannel] != 1) { + // 当音频来源为 RECORDER_TYPE_STREAM 时【必需配置】,否则【无需配置】 + // 启用 SDK 内部的重采样 + [self.curEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_ENABLE_RESAMPLER_BOOL]; + // 将重采样所需的输入采样率设置为 APP 层输入的音频的实际采样率 + [self.curEngine setIntParam:[self.streamRecorder getSampleRate] forKey:SE_PARAMS_KEY_CUSTOM_SAMPLE_RATE_INT]; + [self.curEngine setIntParam:[self.streamRecorder getChannel] forKey:SE_PARAMS_KEY_CUSTOM_CHANNEL_INT]; + } + } + //【必需配置】识别服务域名 + [self.curEngine setStringParam:[self.settings getString:SETTING_ADDRESS] forKey:SE_PARAMS_KEY_ASR_ADDRESS_STRING]; + //【必需配置】识别服务Uri + [self.curEngine setStringParam:[self.settings getString:SETTING_URI] forKey:SE_PARAMS_KEY_ASR_URI_STRING]; + + //【必需配置】鉴权相关:Appid + [self.curEngine setStringParam:[self.settings getString:SETTING_APPID] forKey:SE_PARAMS_KEY_APP_ID_STRING]; + //【必需配置】鉴权相关:Token + [self.curEngine setStringParam:[self.settings getString:SETTING_TOKEN] forKey:SE_PARAMS_KEY_APP_TOKEN_STRING]; + //【必需配置】识别服务资源信息ResourceId + [self.curEngine setStringParam:[self.settings getString:SETTING_RESOURCE_ID] forKey:SE_PARAMS_KEY_RESOURCE_ID_STRING]; + //【必需配置】协议类型,BigAsr协议需设置为Seed + [self.curEngine setIntParam:SEProtocolTypeSeed forKey:SE_PARAMS_KEY_PROTOCOL_TYPE_INT]; + + //【可选配置】在线请求的建连与接收超时,一般不需配置使用默认值即可 + [self.curEngine setIntParam:3000 forKey:SE_PARAMS_KEY_ASR_CONN_TIMEOUT_INT]; + [self.curEngine setIntParam:5000 forKey:SE_PARAMS_KEY_ASR_RECV_TIMEOUT_INT]; + + //【可选配置】在线请求断连后,重连次数,默认值为0,如果需要开启需要设置大于0的次数 + [self.curEngine setIntParam:[self.settings getInt:SETTING_ASR_MAX_RETRY_TIMES] forKey:SE_PARAMS_KEY_ASR_MAX_RETRY_TIMES_INT]; + + //【可选配置】音频采样率,默认16000 + [self.curEngine setIntParam:[self.settings getInt:SETTING_SAMPLE_RATE] forKey:SE_PARAMS_KEY_SAMPLE_RATE_INT]; + //【可选配置】音频通道数,默认1,可选1或2 + [self.curEngine setIntParam:[self.settings getInt:SETTING_CHANNEL] forKey:SE_PARAMS_KEY_CHANNEL_NUM_INT]; + //【可选配置】上传给服务的音频通道数,默认1,可选1或2,一般与SE_PARAMS_KEY_SAMPLE_RATE_INT保持一致即可 + [self.curEngine setIntParam:[self.settings getInt:SETTING_CHANNEL] forKey:SE_PARAMS_KEY_UP_CHANNEL_NUM_INT]; + +} + +-(void)configStartAsrParams{ + //【可选配置】是否开启顺滑(DDC) + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_ENABLE_DDC] forKey:SE_PARAMS_KEY_ASR_ENABLE_DDC_BOOL]; + //【可选配置】是否开启文字转数字(ITN) + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_ENABLE_ITN] forKey:SE_PARAMS_KEY_ASR_ENABLE_ITN_BOOL]; + //【可选配置】是否开启标点 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_ENABLE_NLU_PUNC] forKey:SE_PARAMS_KEY_ASR_SHOW_PUNC_BOOL]; + // 【可选配置】直接传递自定义的ASR请求JSON,若使用此参数需自行确保JSON格式正确 + [self.curEngine setStringParam:[self.settings getString:SETTING_ASR_REQ_PARAMS] forKey:SE_PARAMS_KEY_ASR_REQ_PARAMS_STRING]; + //【可选配置】用户音频输入最大时长,仅一句话识别场景生效,单位毫秒,默认为 150000ms. + [self.curEngine setIntParam:[self.settings getInt:SETTING_VAD_MAX_SPEECH_DURATION] forKey:SE_PARAMS_KEY_VAD_MAX_SPEECH_DURATION_INT]; + + //【可选配置】控制是否返回录音音量,在 APP 需要显示音频波形时可以启用 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_GET_VOLUME] forKey:SE_PARAMS_KEY_ENABLE_GET_VOLUME_BOOL]; + + NSString* recorderType = [self getRecorderType]; + NSLog(@"录音模式: %@", recorderType); + + if ([recorderType isEqualToString:SE_RECORDER_TYPE_STREAM]) { + if (![self.streamRecorder start]) { + [self speechEngineNoPermission]; + } + } else if ([recorderType isEqualToString:SE_RECORDER_TYPE_FILE]) { + // 使用音频文件识别时,需要设置文件的绝对路径 + NSString* file_path = [NSString stringWithFormat:@"%@/%@", self.debugPath, @"asr_rec_file.pcm"]; + NSLog(@"输入的音频文件路径: %@", file_path); + // 使用音频文件识别时【必须配置】,否则【无需配置】 + [self.curEngine setStringParam:file_path forKey:SE_PARAMS_KEY_RECORDER_FILE_STRING]; + } +} + +- (void)initEngine { + NSLog(@"获取设备ID,调试使用"); + AppDelegate *appDelegate = [ViewController getAppDelegate]; + if (appDelegate == nil) { + appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; + } + [ViewController setAppDelegate:appDelegate]; + self.deviceID = appDelegate.deviceID; + NSLog(@"获取设备ID成功: %@", self.deviceID); + + + NSLog(@"创建引擎"); + if (self.curEngine == nil) { + self.curEngine = [[SpeechEngine alloc] init]; + if (![self.curEngine createEngineWithDelegate:self]) { + NSLog(@"引擎创建失败."); + return; + } + } + [self.resultTextView setTextColor:UIColor.blackColor]; + NSLog(@"SDK 版本号: %@", [self.curEngine getVersion]); + + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"当前调试路径: %@", self.debugPath); + + NSLog(@"配置初始化参数"); + [self configInitParams]; + + + NSLog(@"引擎初始化"); + SEEngineErrorCode ret = [self.curEngine initEngine]; + if (ret == SENoError) { + NSLog(@"初始化成功"); + [self speechEngineInitSucceeded]; + } else { + NSLog(@"初始化失败,返回值: %d", ret); + [self speechEngineInitFailed:ret]; + } +} + +- (void)uninitEngine { + if (self.curEngine != nil) { + NSLog(@"引擎析构"); + [self.curEngine destroyEngine]; + self.curEngine = nil; + NSLog(@"引擎析构完成"); + } + dispatch_async(dispatch_get_main_queue(), ^{ + self.initialEngineButton.enabled = TRUE; + self.uninitialEngineButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.recordButton.enabled = FALSE; + }); +} + +- (void)speechEngineInitSucceeded { + [self.streamRecorder setSpeechEngine:VIEW_ASR engine:self.curEngine]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:@"Ready"]; + [self setResultText:[NSString stringWithFormat:@"DeviceID: %@", self.deviceID]]; + self.uninitialEngineButton.enabled = TRUE; + self.initialEngineButton.enabled = FALSE; + self.startEngineButton.enabled = TRUE; + self.recordButton.enabled = TRUE; + }); +} + +- (void)speechEngineInitFailed:(int)initStatus { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:[[NSString alloc] initWithFormat:@"Failed to init engine, %d!", initStatus]]; + self.initialEngineButton.enabled = TRUE; + self.uninitialEngineButton.enabled = FALSE; + }); +} + +- (void)speechEngineNoPermission { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"No permission!"]; + self.initialEngineButton.enabled = TRUE; + self.uninitialEngineButton.enabled = FALSE; + }); +} + +#pragma mark - UI Actions + +- (IBAction)initEngineBtnClicked:(id)sender { + [self initEngine]; +} + +- (IBAction)uninitEngineBtnClicked:(id)sender { + if (self.engineStarted) { + [self.statusTextView setText:@"Engine is busy, stop it first!"]; + return; + } + [self uninitEngine]; + + [self.resultTextView setTextColor:UIColor.grayColor]; + [self setResultText:@"点击或按住说话后,展示语音识别结果"]; +} + +- (IBAction)startEngineBtnClicked:(id)sender { + [self setResultText:@""]; + + NSLog(@"配置启动参数"); + [self configStartAsrParams]; + + + // Directive:启动引擎前调用SYNC_STOP指令,保证前一次请求结束。 + NSLog(@"Directive: SEDirectiveSyncStopEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveSyncStopEngine]; + if (ret != SENoError) { + NSLog(@"Send directive syncstop failed: %d", ret); + } else { + NSLog(@"启用引擎."); + NSLog(@"Directive: SEDirectiveStartEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + } + } +} + +- (IBAction)stopEngineBtnClicked:(id)sender { + NSLog(@"关闭引擎"); + NSLog(@"Directive: SEDirectiveStopEngine"); + [self.curEngine sendDirective:SEDirectiveStopEngine]; +} + +- (void)recordTriggered:(UILongPressGestureRecognizer *)longPgr { + if (longPgr.state == UIGestureRecognizerStateBegan) { + [self setResultText:@""]; + + NSLog(@"配置启动参数"); + [self configStartAsrParams]; + + + // Directive:启动引擎前调用SYNC_STOP指令,保证前一次请求结束。 + NSLog(@"Directive: SEDirectiveSyncStopEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveSyncStopEngine]; + if (ret != SENoError) { + NSLog(@"Send directive syncstop failed: %d", ret); + } else { + // Directive:启动引擎指令。 + NSLog(@"Directive: SEDirectiveStartEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + } + } + } else if (longPgr.state == UIGestureRecognizerStateEnded) { + // Directive:结束音频输入。 + NSLog(@"Directive: SEDirectiveFinishTalking"); + [self.curEngine sendDirective:SEDirectiveFinishTalking]; + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.streamRecorder stop]; + } + } +} + +#pragma mark - Message Callback + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSLog(@"Message Type: %d.", type); + switch (type) { + case SEEngineStart: + // Callback: 引擎启动成功回调 + NSLog(@"Callback: 引擎启动成功"); + [self speechEngineStarted]; + break; + case SEEngineStop: + // Callback: 引擎关闭回调 + NSLog(@"Callback: 引擎关闭"); + [self speechEngineStopped]; + break; + case SEEngineError: + // Callback: 错误信息回调 + NSLog(@"Callback: 错误信息: %@", data); + [self speechEngineError:data]; + break; + case SEConnectionConnected: + NSLog(@"Callback: 建连成功"); + break; + case SEAsrPartialResult: + // Callback: ASR 当前请求的部分结果回调 + NSLog(@"Callback: ASR 当前请求的部分结果"); + [self speechEngineResult:data isFinal:FALSE]; + break; + case SEFinalResult: + // Callback: ASR 当前请求最终结果回调 + NSLog(@"Callback: ASR 当前请求最终结果"); + [self speechEngineResult:data isFinal:TRUE]; + break; + case SEVolumeLevel: + // Callback: 录音音量回调 + NSLog(@"Callback: 录音音量,%.3f", [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] floatValue]); + break; + default: + break; + } +} + +- (void)speechEngineStarted { + dispatch_async(dispatch_get_main_queue(), ^{ + self.engineStarted = true; + [self.statusTextView setText:@"Engine Started!"]; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = TRUE; + self.recordButton.enabled = FALSE; + }); +} + +- (void)speechEngineStopped { + dispatch_async(dispatch_get_main_queue(), ^{ + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.streamRecorder stop]; + } + self.engineStarted = FALSE; + [self.statusTextView setText:@"Engine Stopped!"]; + self.startEngineButton.enabled = TRUE; + self.stopEngineButton.enabled = FALSE; + self.recordButton.enabled = TRUE; + }); +} + +- (void)speechEngineResult:(NSData *)data isFinal:(BOOL)isFinal { + dispatch_async(dispatch_get_main_queue(), ^{ + // 展示 ASR 结果 + NSString* dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + [self setResultText:[NSString stringWithFormat:@"result: %@", dataStr]]; + }); +} + +- (void)speechEngineError:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + // 展示 Error 结果 + NSString* errorStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + [self setResultText:[NSString stringWithFormat:@"error: %@", errorStr]]; + }); +} + +#pragma mark - Helper + +- (NSString *)getRecorderType { + SettingOptions* recorderTypeOptions = [self.settings getOptions:SETTING_RECORD_TYPE]; + switch (recorderTypeOptions.chooseIdx) { + case 0: + return SE_RECORDER_TYPE_RECORDER; + case 1: + return SE_RECORDER_TYPE_FILE; + case 2: + return SE_RECORDER_TYPE_STREAM; + default: + break; + } + return @""; +} + +- (void)setResultText:(NSString *)result { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[result stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; + }); +} + +#pragma mark - UITextViewDelegate + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if([text isEqualToString:@"\n"]) { + [textView resignFirstResponder]; + return NO; + } + return YES; +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_BIGASR forKey:@"viewId"]; +} + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/CMakeLists.txt b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/CMakeLists.txt new file mode 100644 index 0000000..8f957c2 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/CMakeLists.txt @@ -0,0 +1,84 @@ +cmake_minimum_required(VERSION 3.1) + +project(SpeechDemo) + +set(DEVELOPMENT_PROJECT_NAME ${CMAKE_PROJECT_NAME}) +set(APP_NAME ${CMAKE_PROJECT_NAME}) +set(APP_BUNDLE_IDENTIFIER "com.bytedance.ailab.speech.sdk") +set(DEPLOYMENT_TARGET 10.0) +set(APP_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") + +set(CMAKE_OSX_SYSROOT "iphoneos") +set(CMAKE_XCODE_EFFECTIVE_PLATFORMS "-iphoneos;-iphonesimulator") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14") +set(CMAKE_SUPPRESS_REGENERATION true) + +set(CMAKE_IPHONEOS_SYSROOT "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk") +set(CMAKE_IPHONESIMULATOR_SYSROOT "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk") +set(XCODE_STARTUP_TARGET "SpeechDemo") +macro(ADD_FRAMEWORK fwname appname) + find_library(FRAMEWORK_${fwname} + NAMES ${fwname} + PATHS ${CMAKE_IPHONESIMULATOR_SYSROOT}/System/Library ${CMAKE_IPHONEOS_SYSROOT}/System/Library + PATH_SUFFIXES Frameworks + NO_DEFAULT_PATH) + if( ${FRAMEWORK_${fwname}} STREQUAL FRAMEWORK_${fwname}-NOTFOUND) + MESSAGE(ERROR ": Framework ${fwname} not found") + else() + TARGET_LINK_LIBRARIES(${appname} ${FRAMEWORK_${fwname}}) + MESSAGE(STATUS "Framework ${fwname} found at ${FRAMEWORK_${fwname}}") + endif() +endmacro(ADD_FRAMEWORK) + +macro(SET_XCODE_PROPERTY TARGET XCODE_PROPERTY XCODE_VALUE XCODE_RELVERSION) + set(XCODE_RELVERSION_I "${XCODE_RELVERSION}") + if(XCODE_RELVERSION_I STREQUAL "All") + set_property(TARGET ${TARGET} PROPERTY XCODE_ATTRIBUTE_${XCODE_PROPERTY} "${XCODE_VALUE}") + else() + set_property(TARGET ${TARGET} PROPERTY XCODE_ATTRIBUTE_${XCODE_PROPERTY}[variant=${XCODE_RELVERSION_I}] "${XCODE_VALUE}") + endif() +endmacro(SET_XCODE_PROPERTY) + +file(GLOB_RECURSE APP_HEADER_FILES "${APP_SOURCE_DIR}/*.h") +file(GLOB_RECURSE APP_SOURCE_FILES "${APP_SOURCE_DIR}/*.m") +set(INTERFACE_BUILDER + ${APP_SOURCE_DIR}/SpeechSettingItem.xib + ${APP_SOURCE_DIR}/Base.lproj/Main.storyboard + ${APP_SOURCE_DIR}/Base.lproj/LaunchScreen.storyboard) + +add_executable( + ${APP_NAME} + MACOSX_BUNDLE + ${INTERFACE_BUILDER} + ${APP_SOURCE_DIR}/Assets.xcassets + ${APP_HEADER_FILES} + ${APP_SOURCE_FILES}) + +set_target_properties( + ${APP_NAME} + PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${APP_SOURCE_DIR}/Info.plist" + XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC YES + RESOURCE "${APP_SOURCE_DIR}/Assets.xcassets;${INTERFACE_BUILDER}") + +SET_XCODE_PROPERTY(${APP_NAME} PRODUCT_BUNDLE_IDENTIFIER ${APP_BUNDLE_IDENTIFIER} All) +SET_XCODE_PROPERTY(${APP_NAME} ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon" All) + +ADD_FRAMEWORK(UIKit ${APP_NAME}) +ADD_FRAMEWORK(Foundation ${APP_NAME}) +ADD_FRAMEWORK(CoreFoundation ${APP_NAME}) +ADD_FRAMEWORK(AVFoundation ${APP_NAME}) +ADD_FRAMEWORK(AudioToolBox ${APP_NAME}) +ADD_FRAMEWORK(Accelerate ${APP_NAME}) +ADD_FRAMEWORK(AdSupport ${APP_NAME}) +ADD_FRAMEWORK(CFNetwork ${APP_NAME}) +ADD_FRAMEWORK(CoreGraphics ${APP_NAME}) +ADD_FRAMEWORK(CoreImage ${APP_NAME}) +ADD_FRAMEWORK(CoreTelephony ${APP_NAME}) +ADD_FRAMEWORK(CoreText ${APP_NAME}) +ADD_FRAMEWORK(MobileCoreServices ${APP_NAME}) +ADD_FRAMEWORK(QuartzCore ${APP_NAME}) +ADD_FRAMEWORK(Security ${APP_NAME}) +ADD_FRAMEWORK(SystemConfiguration ${APP_NAME}) +ADD_FRAMEWORK(JavaScriptCore ${APP_NAME}) + \ No newline at end of file diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/CaptViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/CaptViewController.h new file mode 100644 index 0000000..2b7d12c --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/CaptViewController.h @@ -0,0 +1,13 @@ +// +// CaptViewController.h +// SpeechDemo +// +// Created by bytedance on 2020/9/8. +// Copyright © 2020 fengkai.0518. All rights reserved. +// + +#import + +@interface CaptViewController : UIViewController + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/CaptViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/CaptViewController.m new file mode 100644 index 0000000..b42c57a --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/CaptViewController.m @@ -0,0 +1,528 @@ +// +// CaptViewController.m +// SpeechDemo +// +// Created by bytedance on 2020/9/8. +// Copyright © 2020 fengkai.0518. All rights reserved. +// + +#import "CaptViewController.h" + +#import + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" +#import "SensitiveDefines.h" + +@interface CaptViewController () + +@property (weak, nonatomic) IBOutlet UITextView *referTextView; +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; +@property (weak, nonatomic) IBOutlet UIButton *engineInitButton; +@property (weak, nonatomic) IBOutlet UIButton *engineUninitButton; +@property (weak, nonatomic) IBOutlet UIButton *startEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *stopEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *longPressButton; + +@property (strong, nonatomic) SpeechEngine *curEngine; +@property (assign, nonatomic) BOOL engineStarted; + +// Device ID: 用于定位线上问题 +@property (nonatomic, strong) NSString *deviceID; +// 一些用于统计的字段 +@property (nonatomic, assign) long talkingFinisheTimestamp; +// Debug Path: 用于存放一些 SDK 相关的文件,比如模型、日志等 +@property (strong, nonatomic) NSString *debugPath; +// APP 层自定义的录音机,在音频来源为 Stream 时使用 +@property (weak, nonatomic) StreamRecorder *streamRecorder; + +// settings +@property (strong, nonatomic) Settings *settings; + +@end + +@implementation CaptViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.longPressButton.enabled = FALSE; + + [self decorateTextView:self.referTextView]; + [self decorateTextView:self.resultTextView]; + [self.referTextView setDelegate:self]; + self.referTextView.editable = TRUE; + [self.statusTextView setText:@"Waiting for init."]; + + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + UILongPressGestureRecognizer *longPgr = [[UILongPressGestureRecognizer alloc] initWithTarget:self + action:@selector(recordTriggered:)]; + longPgr.minimumPressDuration = 0.5; + [self.longPressButton addGestureRecognizer:longPgr]; + + self.streamRecorder = [ViewController getStreamRecorder]; + self.engineStarted = FALSE; + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_CAPT]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [self uninitEngine]; + [super viewDidDisappear:animated]; +} + +- (void)decorateTextView:(UITextView *)textView { + textView.layer.cornerRadius = 5.0f; + textView.layer.borderWidth = .25f; + textView.layer.borderColor = [UIColor grayColor].CGColor; +} + +#pragma mark - Config & Init & Uninit Methods + +-(void)configInitParams{ + //【必需配置】Engine Name + [self.curEngine setStringParam:SE_CAPT_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + //【可选配置】Debug & Log + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + [self.curEngine setStringParam:SE_LOG_LEVEL_DEBUG forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + //【可选配置】UID: 用于定位线上问题 + [self.curEngine setStringParam:SDEF_UID forKey:SE_PARAMS_KEY_UID_STRING]; + + //【必需配置】配置音频来源 + [self.curEngine setStringParam:[self getRecorderType] forKey:SE_PARAMS_KEY_RECORDER_TYPE_STRING]; + + if ([self.settings getBool:SETTING_CAPT_RECORDER_SAVE]) { + //【可选配置】录音文件保存路径,如配置,SDK会将录音保存到该路径下,文件格式为 .wav + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_CAPT_REC_PATH_STRING]; + } + + // 当音频来源为 RECORDER_TYPE_STREAM 时,如输入音频采样率不等于 16K,需添加如下配置 + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + if ([self.streamRecorder getSampleRate] != 16000) { + // 当音频来源为 RECORDER_TYPE_STREAM 时【必需配置】,否则【无需配置】 + // 启用 SDK 内部的重采样 + [self.curEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_ENABLE_RESAMPLER_BOOL]; + // 将重采样所需的输入采样率设置为 APP 层输入的音频的实际采样率 + [self.curEngine setIntParam:[self.streamRecorder getSampleRate] forKey:SE_PARAMS_KEY_CUSTOM_SAMPLE_RATE_INT]; + } + } + + //【必需配置】鉴权相关:Appid + NSString* appID = [self.settings getString:SETTING_APPID]; + [self.curEngine setStringParam:appID.length <= 0 ? SDEF_APPID : appID forKey:SE_PARAMS_KEY_APP_ID_STRING]; + //【必需配置】鉴权相关:Token + NSString* token = [self.settings getString:SETTING_TOKEN]; + [self.curEngine setStringParam:token.length <= 0 ? SDEF_TOKEN : token forKey:SE_PARAMS_KEY_APP_TOKEN_STRING]; + + NSString *address = [self.settings getString:SETTING_ADDRESS]; + if (!address.length) { + address = SDEF_DEFAULT_ADDRESS; + } + NSLog(@"Current address: %@", address); + //【必需配置】评测服务域名 + [self.curEngine setStringParam:address forKey:SE_PARAMS_KEY_CAPT_ADDRESS_STRING]; + NSString *uri = [self.settings getString:SETTING_URI]; + if (!uri.length) { + uri = SDEF_CAPT_DEFAULT_MDD_URI; + } + NSLog(@"Current uri: %@", uri); + //【必需配置】评测服务Uri + [self.curEngine setStringParam:uri forKey:SE_PARAMS_KEY_CAPT_URI_STRING]; + NSString* cluster = [self.settings getString:SETTING_CLUSTER]; + if (!cluster.length) { + cluster = SDEF_CAPT_DEFAULT_CLUSTER; + } + NSLog(@"Current cluster: %@", cluster); + //【必需配置】评测服务所用集群 + [self.curEngine setStringParam:cluster forKey:SE_PARAMS_KEY_CAPT_CLUSTER_STRING]; + + //【可选配置】在线请求的建连与接收超时,一般不需配置使用默认值即可 + [self.curEngine setIntParam:12000 forKey:SE_PARAMS_KEY_CAPT_CONN_TIMEOUT_INT]; + [self.curEngine setIntParam:8000 forKey:SE_PARAMS_KEY_CAPT_RECV_TIMEOUT_INT]; +} + +-(void)configStartParams{ + NSString* text = self.referTextView.text; + if (text.length < 1) { + text = @"Write down the reference text here"; + } + //【必需配置】评测参考文本 + [self.curEngine setStringParam:text forKey:SE_PARAMS_KEY_CAPT_REFER_TEXT_STRING]; + + + //【可选配置】默认为英文打分,如果需要中文需要设置为SE_CAPT_CORE_TYPE_CN_SENT_RAW + [self.curEngine setStringParam:[self getCoreType] forKey:SE_PARAMS_KEY_CAPT_CORE_TYPE_STRING]; + + NSInteger difficultyLevel = [self.settings getInt:SETTING_CAPT_DIFFICULTY_LEVEL]; + //【可选配置】评测难度,默认2,1:容易,2:中等,3:困难 + [self.curEngine setIntParam:difficultyLevel forKey:SE_PARAMS_KEY_CAPT_DIFFICULTY_INT]; + + NSString *responseMode = SE_CAPT_RESPONSE_MODE_ONCE; + if ([self.settings getBool:SETTING_CAPT_STREAMING_MODE]) { + responseMode = SE_CAPT_RESPONSE_MODE_STREAMING; + } + //【可选配置】控制评测结果返回形式,默认是SE_CAPT_RESPONSE_MODE_ONCE单次返回,如果需要流式返回请设置为SE_CAPT_RESPONSE_MODE_STREAMING + [self.curEngine setStringParam:responseMode forKey:SE_PARAMS_KEY_CAPT_RESPONSE_MODE_STRING]; + + //【可选配置】是否启用云端自动判停 + [self.curEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_CAPT_AUTO_STOP_BOOL]; + + //【可选配置】控制是否返回录音音量,在 APP 需要显示音频波形时可以启用 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_GET_VOLUME] + forKey:SE_PARAMS_KEY_ENABLE_GET_VOLUME_BOOL]; + //【可选配置】用户音频输入最大时长,单位毫秒,默认为 150000ms. + [self.curEngine setIntParam:[self.settings getInt:SETTING_VAD_MAX_SPEECH_DURATION] forKey:SE_PARAMS_KEY_VAD_MAX_SPEECH_DURATION_INT]; + + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + if (![self.streamRecorder start]) { + [self speechEngineNoPermission]; + } + } else if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_FILE]) { + // 使用音频文件评测时,需要设置文件的绝对路径 + NSString* file_path = [NSString stringWithFormat:@"%@/%@", self.debugPath, @"capt_rec_file.pcm"]; + NSLog(@"输入的音频文件路径: %@", file_path); + // 使用音频文件评测时【必须配置】,否则【无需配置】 + [self.curEngine setStringParam:file_path forKey:SE_PARAMS_KEY_RECORDER_FILE_STRING]; + } +} + +#pragma mark - Init Methods + +- (void)initEngine { + NSLog(@"获取设备ID,调试使用"); + AppDelegate *appDelegate = [ViewController getAppDelegate]; + if (appDelegate == nil) { + appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; + } + [ViewController setAppDelegate:appDelegate]; + self.deviceID = appDelegate.deviceID; + NSLog(@"获取设备ID成功: %@", self.deviceID); + + + NSLog(@"创建引擎"); + if (self.curEngine == nil) { + self.curEngine = [[SpeechEngine alloc] init]; + } + if (![self.curEngine createEngineWithDelegate:self]) { + NSLog(@"引擎创建失败."); + return; + } + + [self.resultTextView setTextColor:UIColor.blackColor]; + NSLog(@"SDK 版本号: %@", [self.curEngine getVersion]); + + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"当前调试路径: %@", self.debugPath); + + NSLog(@"配置初始化参数"); + [self configInitParams]; + + NSLog(@"引擎初始化"); + SEEngineErrorCode ret = [self.curEngine initEngine]; + if (ret == SENoError) { + NSLog(@"初始化成功"); + [self speechEngineInitSucceeded]; + } else { + NSLog(@"初始化失败,返回值: %d", ret); + [self speechEngineInitFailed]; + } +} + +- (NSString *)getRecorderType { + switch ([self.settings getOptions:SETTING_RECORD_TYPE].chooseIdx) { + case 0: + return SE_RECORDER_TYPE_RECORDER; + case 1: + return SE_RECORDER_TYPE_FILE; + case 2: + return SE_RECORDER_TYPE_STREAM; + default: + break; + } + return @""; +} + +- (NSString *)getCoreType { + switch ([self.settings getOptions:SETTING_CAPT_CORE_TYPE].chooseIdx) { + case 0: + return SE_CAPT_CORE_TYPE_EN_SENT_SCORE; + case 1: + return SE_CAPT_CORE_TYPE_EN_WORD_SCORE; + case 2: + return SE_CAPT_CORE_TYPE_EN_WORD_PRON; + case 3: + return SE_CAPT_CORE_TYPE_CN_SENT_RAW; + default: + break; + } + return @""; +} + +- (void)uninitEngine { + [self.curEngine destroyEngine]; + self.curEngine = nil; +} + +#pragma mark - UI Actions + +- (IBAction)initEngineBtnClicked:(id)sender { + [self initEngine]; +} + +- (IBAction)uninitEngineBtnClicked:(id)sender { + if (self.engineStarted) { + [self.statusTextView setText:@"Engine is busy, stop it first!"]; + return; + } + [self uninitEngine]; + [self.statusTextView setText:@"Waiting for init."]; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.longPressButton.enabled = FALSE; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton = FALSE; +} + +- (IBAction)startEngineBtnClicked:(id)sender { + [self setResultText:@""]; + + NSLog(@"配置启动参数"); + [self configStartParams]; + + //【可选配置】是否启用云端自动判停 + NSLog(@"开启 CAPT 云端自动判停"); + [self.curEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_CAPT_AUTO_STOP_BOOL]; + + // Directive:启动引擎前调用SYNC_STOP指令,保证前一次请求结束。 + NSLog(@"Directive: SEDirectiveSyncStopEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveSyncStopEngine]; + if (ret != SENoError) { + NSLog(@"Send directive syncstop failed: %d", ret); + } else { + NSLog(@"启用引擎."); + NSLog(@"Directive: SEDirectiveStartEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + } + } +} + +- (IBAction)stopEngineBtnClicked:(id)sender { + NSLog(@"关闭引擎"); + NSLog(@"Directive: SEDirectiveStopEngine"); + [self.curEngine sendDirective:SEDirectiveStopEngine]; +} + +- (void)recordTriggered:(UILongPressGestureRecognizer *)longPgr { + if (longPgr.state == UIGestureRecognizerStateBegan) { + [self setResultText:@""]; + + NSLog(@"配置启动参数"); + [self configStartParams]; + + //【可选配置】是否启用云端自动判停 + NSLog(@"关闭 CAPT 云端自动判停"); + [self.curEngine setBoolParam:FALSE forKey:SE_PARAMS_KEY_CAPT_AUTO_STOP_BOOL]; + + // Directive:启动引擎前调用SYNC_STOP指令,保证前一次请求结束。 + NSLog(@"Directive: SEDirectiveSyncStopEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveSyncStopEngine]; + if (ret != SENoError) { + NSLog(@"Send directive syncstop failed: %d", ret); + } else { + // Directive:启动引擎指令。 + NSLog(@"Directive: SEDirectiveStartEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + } + } + } else if (longPgr.state == UIGestureRecognizerStateEnded) { + self.talkingFinisheTimestamp = [[NSDate date] timeIntervalSince1970] * 1000; + // Directive:结束音频输入。 + NSLog(@"Directive: SEDirectiveFinishTalking"); + [self.curEngine sendDirective:SEDirectiveFinishTalking]; + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.streamRecorder stop]; + } + } +} + +#pragma mark - SpeechEngineDelegate + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSLog(@"Message Type: %d.", type); + switch (type) { + case SEEngineStart: + // Callback: 引擎启动成功回调 + NSLog(@"Callback: 引擎启动成功"); + [self speechEngineStarted]; + break; + case SEEngineStop: + // Callback: 引擎关闭回调 + NSLog(@"Callback: 引擎关闭"); + [self speechEngineStopped]; + break; + case SEEngineError: + // Callback: 错误信息回调 + NSLog(@"Callback: 错误信息: %@", data); + [self speechEngineError:data]; + break; + case SEAsrPartialResult: + // Callback: Capt 当前请求的部分结果回调,只有设置SE_PARAMS_KEY_CAPT_RESPONSE_MODE_STRING为SE_CAPT_RESPONSE_MODE_STREAMING时才会回调该消息 + NSLog(@"Callback: Capt 当前请求的部分结果"); + break; + case SEFinalResult: + // Callback: Capt 最终评测结果回调 + NSLog(@"Callback: Capt 最终评测结果"); + [self speechEngineResult:data]; + break; + case SEVolumeLevel: + // Callback: 录音音量回调 + NSLog(@"Callback: 录音音量"); + break; + default: + break; + } +} + +#pragma mark - Engine Callback + +- (void)speechEngineNoPermission { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"No permission!"]; + self.engineUninitButton.enabled = TRUE; + self.engineInitButton.enabled = FALSE; + self.startEngineButton.enabled = TRUE; + self.longPressButton.enabled = TRUE; + self.referTextView.editable = TRUE; + self.startEngineButton.enabled = TRUE; + }); +} + +- (void)speechEngineInitSucceeded { + [self.streamRecorder setSpeechEngine:self.curEngine]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:@"Ready"]; + [self.resultTextView setText:[NSString stringWithFormat:@"DeviceID: %@", self.deviceID]]; + self.engineUninitButton.enabled = TRUE; + self.engineInitButton.enabled = FALSE; + self.startEngineButton.enabled = TRUE; + self.longPressButton.enabled = TRUE; + self.referTextView.editable = TRUE; + self.startEngineButton.enabled = TRUE; + }); +} + +- (void)speechEngineInitFailed { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"Failed to init engine!"]; + self.engineInitButton.enabled = TRUE; + }); +} + +- (void)speechEngineStarted { + dispatch_async(dispatch_get_main_queue(), ^{ + self.engineStarted = true; + [self.statusTextView setText:@"Engine Started!"]; + self.referTextView.editable = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = TRUE; + self.longPressButton.enabled = FALSE; + }); +} + +- (void)speechEngineStopped { + dispatch_async(dispatch_get_main_queue(), ^{ + if ([self getRecorderType] == SE_RECORDER_TYPE_STREAM) { + [self.streamRecorder stop]; + } + self.engineStarted = FALSE; + [self.statusTextView setText:@"Engine Stopped!"]; + self.referTextView.editable = TRUE; + self.startEngineButton.enabled = TRUE; + self.stopEngineButton.enabled = FALSE; + self.longPressButton.enabled = TRUE; + }); +} + +- (void)speechEngineResult:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + long response_delay = 0; + if (self.talkingFinisheTimestamp > 0) { + response_delay = [self timeDelayFrom:self.talkingFinisheTimestamp]; + self.talkingFinisheTimestamp = 0; + } + NSError *error; + NSDictionary *jsonResult = [NSJSONSerialization JSONObjectWithData:data + options:NSJSONReadingMutableContainers + error:&error]; + NSMutableString *text = [[NSMutableString alloc] initWithString:@""]; + NSDictionary *scores = [jsonResult objectForKey:@"scores"]; + NSArray *integrity = [jsonResult objectForKey:@"integrity_details"]; + if (scores && scores != (id)[NSNull null] && integrity && integrity != (id)[NSNull null]) { + [text appendFormat:@"response_delay: %ld", response_delay]; + [text appendFormat:@"\nreqid: %@", [jsonResult objectForKey:@"reqid"]]; + [text appendFormat:@"\nscores: %@", [scores description]]; + [text appendFormat:@"\nintegrity_details: %@", [integrity description]]; + } + + NSString *res_text = [NSString stringWithCString:[text cStringUsingEncoding:NSUTF8StringEncoding] encoding:NSNonLossyASCIIStringEncoding]; + if (res_text.length) { + [self.resultTextView setText:[res_text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; + } + }); +} + +- (void)speechEngineError:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + // 从回调的 json 数据中解析错误码和错误详细信息 + [self.resultTextView setText:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]]; + [self stopEngineBtnClicked:nil]; + }); +} + +- (void)setResultText:(NSString *)result { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[result stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; + }); +} + +#pragma mark - Helper + +- (long)timeDelayFrom:(long)pastTimestamp { + return [[NSDate date] timeIntervalSince1970] * 1000 - pastTimestamp; +} + +#pragma mark - UITextViewDelegate + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if([text isEqualToString:@"\n"]) { + [textView resignFirstResponder]; + return NO; + } + return YES; +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_CAPT forKey:@"viewId"]; +} + +@end + + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/DialogDelegateViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/DialogDelegateViewController.h new file mode 100644 index 0000000..40f913a --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/DialogDelegateViewController.h @@ -0,0 +1,17 @@ +// +// DialogDelegateViewController.m +// SpeechDemo +// +// Created by bytedance on 2025/3/27. +// Copyright © 2025 bytedance. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface DialogDelegateViewController : UIViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/DialogDelegateViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/DialogDelegateViewController.m new file mode 100644 index 0000000..29cb198 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/DialogDelegateViewController.m @@ -0,0 +1,549 @@ +// +// DialogDelegateViewController.m +// SpeechDemo +// +// Created by bytedance on 2025/3/27. +// Copyright © 2025 bytedance. All rights reserved. +// + +#import "DialogDelegateViewController.h" + +#import + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" +#import "SensitiveDefines.h" +#import "utils/DialogMessage.h" + +#pragma mark - DialogDelegateViewController +@interface DialogDelegateViewController () + +// UI +@property (weak, nonatomic) IBOutlet UIButton *initialEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *uninitialEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *startEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *stopEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *chatTtsTextButton; +@property (weak, nonatomic) IBOutlet UIButton *useServerTriggerTtsButton; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextView *referTextView; +@property (strong, nonatomic) NSMutableArray *dialogMessages; + +// Debug +@property (nonatomic, strong) NSString *deviceID; +@property (strong, nonatomic) NSString *debugPath; + +// Speech Engine +@property (strong, nonatomic) SpeechEngine *speechEngine; +@property (assign, nonatomic) BOOL engineStarted; + +// Settings +@property (strong, nonatomic) Settings *settings; + +@end + +@implementation DialogDelegateViewController + +static const int MAX_DIALOG_MESSAGE_COUNT = 20; + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.initialEngineButton.enabled = TRUE; + self.uninitialEngineButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.chatTtsTextButton.enabled = FALSE; + self.useServerTriggerTtsButton.enabled = FALSE; + + [self.statusTextView setText:@"Waiting for init."]; + [self decorateTextView:self.resultTextView]; + + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + + self.engineStarted = FALSE; + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_DIALOG_DELEGATE]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [self uninitEngine]; + [super viewDidDisappear:animated]; +} + +- (void)decorateView:(UIView *)view { + view.layer.cornerRadius = 5.0f; + view.layer.borderWidth = .25f; + view.layer.borderColor = [UIColor grayColor].CGColor; +} + +- (void)decorateTextView:(UITextView *)textView { + textView.layer.cornerRadius = 5.0f; + textView.layer.borderWidth = .25f; + textView.layer.borderColor = [UIColor grayColor].CGColor; +} + +#pragma mark - Config & Init & Uninit Methods + +- (void)configInitParams { + //【必需配置】Engine Name + [self.speechEngine setStringParam:SE_DIALOG_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + + //【可选配置】Debug & Log + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"Debug path: %@", self.debugPath); + [self.speechEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + [self.speechEngine setStringParam:SE_LOG_LEVEL_TRACE forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + + //【必需配置】Authentication:AppId + [self.speechEngine setStringParam:[self.settings getString:SETTING_APPID] forKey:SE_PARAMS_KEY_APP_ID_STRING]; + //【必需配置】Authentication:AppKey + [self.speechEngine setStringParam:[self.settings getString:SETTING_APPKEY] forKey:SE_PARAMS_KEY_APP_KEY_STRING]; + //【必需配置】Authentication:Token + [self.speechEngine setStringParam:[self.settings getString:SETTING_TOKEN] forKey:SE_PARAMS_KEY_APP_TOKEN_STRING]; + //【必需配置】对话服务资源信息ResourceId + [self.speechEngine setStringParam:[self.settings getString:SETTING_RESOURCE_ID] forKey:SE_PARAMS_KEY_RESOURCE_ID_STRING]; + //【必需配置】User ID(用以辅助定位线上用户问题,如无法提供可提供固定字符串) + [self.speechEngine setStringParam:SDEF_UID forKey:SE_PARAMS_KEY_UID_STRING]; + //【必需配置】Dialog Address,对话服务域名 + [self.speechEngine setStringParam:[self.settings getString:SETTING_ADDRESS] forKey:SE_PARAMS_KEY_DIALOG_ADDRESS_STRING]; + //【必需配置】Dialog Uri,对话服务Uri + [self.speechEngine setStringParam:[self.settings getString:SETTING_URI] forKey:SE_PARAMS_KEY_DIALOG_URI_STRING]; + //【可选配置】是否开启AEC,默认不开启,同时启用设备录音和播放时必须开启 + [self.speechEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_ENABLE_AEC_BOOL]; + //【可选配置】AEC模型路径,开启AEC时必填 + NSString* aecModelPath = [NSString stringWithFormat:@"%@/aec.model", self.debugPath]; + [self.speechEngine setStringParam:aecModelPath forKey:SE_PARAMS_KEY_AEC_MODEL_PATH_STRING]; + //【可选配置】配置音频来源,默认使用设备麦克风录音(Dialog 仅支持 RECORDER 和 STREAM 模式,RECORDER表示设备麦克风录音,STREAM表示自定义音频输入) + [self.speechEngine setStringParam:SE_RECORDER_TYPE_RECORDER forKey:SE_PARAMS_KEY_RECORDER_TYPE_STRING]; + //【可选配置】是否开启播放器,默认开启 + [self.speechEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_DIALOG_ENABLE_PLAYER_BOOL]; + //【可选配置】启用录音机音频回调,默认不启用 + [self.speechEngine setBoolParam:FALSE forKey:SE_PARAMS_KEY_DIALOG_ENABLE_RECORDER_AUDIO_CALLBACK_BOOL]; + //【可选配置】启用播放器音频回调,默认不启用(为当前正在播放的音频数据,会随着播放进度回调) + [self.speechEngine setBoolParam:FALSE forKey:SE_PARAMS_KEY_DIALOG_ENABLE_PLAYER_AUDIO_CALLBACK_BOOL]; + //【可选配置】启用解码后原始音频回调,默认不启用(为解码后的需要播报的数据,会在解码完成后立刻回调,不等待播放进度) + [self.speechEngine setBoolParam:FALSE forKey:SE_PARAMS_KEY_DIALOG_ENABLE_DECODER_AUDIO_CALLBACK_BOOL]; + //【可选配置】自定义请求Header + [self.speechEngine setStringParam:[self.settings getString:SETTING_REQUEST_HEADERS] forKey:SE_PARAMS_KEY_REQUEST_HEADERS_STRING]; + //【可选配置】录音文件保存路径,如不为空,则SDK会将录音机音频保存到该路径下,文件格式为 .wav + if ([self.settings getBool:SETTING_DIALOG_ENABLE_RECORDER_DUMP]) { + [self.speechEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DIALOG_RECORDER_PATH_STRING]; + } else { + [self.speechEngine setStringParam:@"" forKey:SE_PARAMS_KEY_DIALOG_RECORDER_PATH_STRING]; + } + //【可选配置】播放文件保存路径,如不为空,则SDK会将播放器音频保存到该路径下,文件格式为 .wav + if ([self.settings getBool:SETTING_DIALOG_ENABLE_PLAYER_DUMP]) { + [self.speechEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DIALOG_PLAYER_PATH_STRING]; + } else { + [self.speechEngine setStringParam:@"" forKey:SE_PARAMS_KEY_DIALOG_PLAYER_PATH_STRING]; + } + // 【可选配置】启用TTS文本委托,默认不启用 + [self.speechEngine setIntParam:SEDialogWorkModeDelegateChatTtsText forKey:SE_PARAMS_KEY_DIALOG_WORK_MODE_INT]; +} + +- (void)initEngine { + NSLog(@"获取设备ID,调试使用"); + AppDelegate *appDelegate = [ViewController getAppDelegate]; + if (appDelegate == nil) { + appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; + } + [ViewController setAppDelegate:appDelegate]; + self.deviceID = appDelegate.deviceID; + NSLog(@"获取设备ID成功: %@", self.deviceID); + + NSLog(@"创建引擎"); + if (self.speechEngine == nil) { + self.speechEngine = [[SpeechEngine alloc] init]; + if (![self.speechEngine createEngineWithDelegate:self]) { + NSLog(@"Create speech engine failed."); + return; + } + } + NSLog(@"Engine version: %@", [self.speechEngine getVersion]); + + NSLog(@"配置初始化参数"); + [self configInitParams]; + + NSLog(@"引擎初始化"); + SEEngineErrorCode ret = [self.speechEngine initEngine]; + if (ret != SENoError) { + NSLog(@"Init Engine failed: %d", ret); + [self speechEngineInitFailed]; + return; + } + + [self speechEngineInitOk]; +} + +- (void)uninitEngine { + if (self.speechEngine) { + NSLog(@"引擎析构"); + [self.speechEngine destroyEngine]; + self.speechEngine = nil; + NSLog(@"引擎析构完成"); + } + [self.statusTextView setText:@"Engine uninited!"]; +} + +- (void)sayHello { + // Directive:发送say_hello指令以播放开场白。 + NSString* sayHelloJson = [NSString stringWithFormat: @"{\"content\": \"%@\"}", self.referTextView.text]; + SEEngineErrorCode ret = [self.speechEngine sendDirective:SEDirectiveEventSayHello data:sayHelloJson]; + if (ret != SENoError) { + [self.statusTextView setText:[NSString stringWithFormat: @"开场白触发失败: %d", ret]]; + } else { + // Directive:发送UseClientTriggerTts指令以播放客户端指定的TTS回复。 + ret = [self.speechEngine sendDirective:SEDirectiveDialogUseClientTriggerTts]; + if (ret != SENoError) { + [self.statusTextView setText:[NSString stringWithFormat: @"播放客户端指定的TTS回复失败: %d", ret]]; + } else { + [self showHelloMessage:self.referTextView.text]; + } + } +} + +#pragma mark - UI Actions + +- (IBAction)initEngineBtnClicked:(id)sender { + [self initEngine]; +} + +- (IBAction)uninitEngineBtnClicked:(id)sender { + if (self.engineStarted) { + [self.statusTextView setText:@"Engine is busy, stop it first!"]; + return; + } + [self uninitEngine]; + + self.initialEngineButton.enabled = TRUE; + self.uninitialEngineButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.chatTtsTextButton.enabled = FALSE; + self.useServerTriggerTtsButton.enabled = FALSE; +} + +- (IBAction)startEngineBtnClicked:(id)sender { + if (self.speechEngine == nil) { + [self.statusTextView setText:@"Engine is not initialized!"]; + return; + } + // Directive:启动引擎指令。 + NSLog(@"Directive: SEDirectiveStartEngine"); + NSString* startJson = [NSString stringWithFormat: @"{\"dialog\":{\"bot_name\":\"%@\"}}", [self.settings getString:SETTING_DIALOG_BOT_NAME]]; + SEEngineErrorCode ret = [self.speechEngine sendDirective:SEDirectiveStartEngine data:startJson]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + } else if (ret == SENoError) { + self.dialogMessages = [[NSMutableArray alloc] init]; + self.resultTextView.text = @""; + // 开场白 + if (self.referTextView.text.length != 0) { + [self sayHello]; + } + } else { + [self.statusTextView setText:[NSString stringWithFormat: @"Fail to start engine: %d", ret]]; + } +} + +- (IBAction)stopEngineBtnClicked:(id)sender { + if (self.speechEngine == nil) { + [self.statusTextView setText:@"Engine is not initialized!"]; + return; + } + // Directive:关闭引擎,停止对话功能。 + NSLog(@"Directive: SEDirectiveStopEngine"); + [self.speechEngine sendDirective:SEDirectiveStopEngine]; +} + +- (IBAction)chatTtsTextBtnClicked:(id)sender { + if (self.speechEngine == nil) { + [self.statusTextView setText:@"Engine is not initialized!"]; + return; + } + NSString* chatTtsText = self.referTextView.text; + // Directive:发送ChatTtsText指令以播放自定义回复文本,可以流式不断补充文本内容。首包需要包含start:true,end:false 。 + NSString* chatTtsTextJson = [NSString stringWithFormat: @"{\"start\": true, \"content\": \"%@\", \"end\": false}", chatTtsText]; + SEEngineErrorCode ret = [self.speechEngine sendDirective:SEDirectiveEventChatTtsText data:chatTtsTextJson]; + if (ret != SENoError) { + [self.statusTextView setText:[NSString stringWithFormat: @"自定义TTS回复失败: %d", ret]]; + return; + } + // Directive:发送ChatTtsText指令以播放自定义回复文本,可以流式不断补充文本内容。尾包需要包含start:false,end:true 。 + chatTtsTextJson = @"{\"start\": false, \"content\": \"\", \"end\": true}"; + ret = [self.speechEngine sendDirective:SEDirectiveEventChatTtsText data:chatTtsTextJson]; + if (ret != SENoError) { + [self.statusTextView setText:[NSString stringWithFormat: @"自定义TTS回复失败: %d", ret]]; + return; + } + // Directive:发送UseClientTriggerTts指令以播放客户端指定的TTS回复。 + ret = [self.speechEngine sendDirective:SEDirectiveDialogUseClientTriggerTts]; + if (ret != SENoError) { + [self.statusTextView setText:[NSString stringWithFormat: @"播放客户端指定的TTS回复失败: %d", ret]]; + return; + } + [self updateChatTtsTextMessage:chatTtsText]; +} + +- (IBAction)useServerTriggerTtsBtnClicked:(id)sender { + if (self.speechEngine == nil) { + [self.statusTextView setText:@"Engine is not initialized!"]; + return; + } + // Directive:发送UseServerTriggerTts指令以播放服务端自动生成的TTS回复。 + int ret = [self.speechEngine sendDirective:SEDirectiveDialogUseServerTriggerTts]; + if (ret != SENoError) { + [self.statusTextView setText:[NSString stringWithFormat: @"播放服务端自动生成的TTS回复失败: %d", ret]]; + return; + } +} + +#pragma mark - SpeechEngineDelegate + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSLog(@"Message Type: %d.", type); + NSString *strData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + switch (type) { + case SEEngineStart: + // Callback: 引擎启动成功回调 + NSLog(@"Callback: 引擎启动成功: %d", type); + [self speechEngineStarted:strData]; + break; + case SEEngineStop: + // Callback: 引擎关闭回调 + NSLog(@"Callback: 引擎关闭: %d", type); + [self speechEngineStopped:strData]; + break; + case SEEngineError: + // Callback: 错误信息回调 + NSLog(@"Callback: 错误信息: %d, data: %@", type, strData); + [self showLogMessage:strData]; + break; + case SEDialogASRResponse: + [self showUserMessage: data]; + break; + case SEDialogASREnded: + [self confirmUserMessage]; + break; + case SEDialogChatResponse: + [self showAssistantMessage: data]; + break; + case SEDialogASRInfo: + case SEDialogChatEnded: + [self confirmAssistantMessage]; + break; + default: + break; + } +} + +#pragma mark - SpeechEngine Callback + +- (void)speechEngineNoPermission { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"No permission!"]; + self.initialEngineButton.enabled = TRUE; + self.uninitialEngineButton.enabled = FALSE; + }); +} + +- (void)speechEngineInitOk { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:[NSString stringWithFormat:@"DeviceID: %@", self.deviceID]]; + self.initialEngineButton.enabled = FALSE; + self.uninitialEngineButton.enabled = TRUE; + self.startEngineButton.enabled = TRUE; + }); +} + +- (void)speechEngineInitFailed { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"Failed to init engine!"]; + self.initialEngineButton.enabled = TRUE; + self.uninitialEngineButton.enabled = FALSE; + }); +} + +- (void)speechEngineStarted:(NSString *)sessionId { + [self showLogMessage: [NSString stringWithFormat:@"Engine start: %@", sessionId]]; + dispatch_async(dispatch_get_main_queue(), ^{ + self.engineStarted = true; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = TRUE; + self.chatTtsTextButton.enabled = TRUE; + self.useServerTriggerTtsButton.enabled = TRUE; + [self.statusTextView setText:@"Engine Started!"]; + }); +} + +- (void)speechEngineStopped:(NSString *)sessionId { + [self showLogMessage: [NSString stringWithFormat:@"Engine stop: %@", sessionId]]; + dispatch_async(dispatch_get_main_queue(), ^{ + self.engineStarted = FALSE; + self.startEngineButton.enabled = TRUE; + self.stopEngineButton.enabled = FALSE; + self.chatTtsTextButton.enabled = FALSE; + self.useServerTriggerTtsButton.enabled = FALSE; + [self.statusTextView setText:@"Engine Stopped!"]; + }); +} + + +# pragma mark - Helper + +- (void)showLogMessage:(NSString *)text { + dispatch_async(dispatch_get_main_queue(), ^{ + DialogMessage* message = [[DialogMessage alloc] init]; + message.role = ROLE_LOG; + message.text = text; + message.confirmed = true; + [self.dialogMessages addObject:message]; + [self updateMessageUI]; + }); +} + +- (void)showHelloMessage:(NSString *)helloMessage { + dispatch_async(dispatch_get_main_queue(), ^{ + DialogMessage* message = [[DialogMessage alloc] init]; + message.role = ROLE_ASSISTANT; + message.text = helloMessage; + message.confirmed = true; + [self.dialogMessages addObject:message]; + [self updateMessageUI]; + }); +} + +- (void)showUserMessage:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + DialogMessage* message = [self lastUnconfirmedMessage:ROLE_USER]; + if (message == nil) { + message = [[DialogMessage alloc] init]; + message.role = ROLE_USER; + message.confirmed = false; + [self.dialogMessages addObject:message]; + } + // 从回调的 json 数据中解析用户说的消息内容 + NSError *error; + NSDictionary *jsonResult = [NSJSONSerialization JSONObjectWithData:data + options:NSJSONReadingMutableContainers + error:&error]; + message.text = [[[jsonResult objectForKey:@"results"] firstObject] objectForKey:@"text"]; + [self updateMessageUI]; + }); +} + +- (void)confirmUserMessage { + dispatch_async(dispatch_get_main_queue(), ^{ + DialogMessage* message = [self lastUnconfirmedMessage:ROLE_USER]; + if (message) { + message.confirmed = true; + } + }); +} + +- (void)showAssistantMessage:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + DialogMessage* message = [self lastUnconfirmedMessage:ROLE_ASSISTANT]; + if (message == nil) { + message = [[DialogMessage alloc] init]; + message.role = ROLE_ASSISTANT; + message.text = @""; + message.confirmed = false; + [self.dialogMessages addObject:message]; + } + // 从回调的 json 数据中解析用户说的消息内容 + NSError *error; + NSDictionary *jsonResult = [NSJSONSerialization JSONObjectWithData:data + options:NSJSONReadingMutableContainers + error:&error]; + message.text = [message.text stringByAppendingString:[jsonResult objectForKey:@"content"]]; + [self updateMessageUI]; + }); +} + +- (void)confirmAssistantMessage { + dispatch_async(dispatch_get_main_queue(), ^{ + DialogMessage* message = [self lastUnconfirmedMessage:ROLE_ASSISTANT]; + if (message) { + message.confirmed = true; + } + }); +} + +- (void)updateChatTtsTextMessage:(NSString *)text { + dispatch_async(dispatch_get_main_queue(), ^{ + DialogMessage* message = [self.dialogMessages lastObject]; + if (message == nil || message.role != ROLE_ASSISTANT) { + message = [[DialogMessage alloc] init]; + message.role = ROLE_ASSISTANT; + [self.dialogMessages addObject:message]; + } + message.text = text; + message.confirmed = true; + [self updateMessageUI]; + }); +} + +- (DialogMessage*)lastUnconfirmedMessage:(Role)role { + for (DialogMessage* message in [self.dialogMessages reverseObjectEnumerator]) { + if (message.role == role) { + if (!message.confirmed) { + return message; + } + break; + } + } + return nil; +} + +- (void)updateMessageUI { + if (self.dialogMessages.count > MAX_DIALOG_MESSAGE_COUNT) { + [self.dialogMessages removeObjectAtIndex:0]; + } + NSString* results = @""; + for (DialogMessage* message in self.dialogMessages) { + NSString* role = @""; + switch (message.role) { + case ROLE_USER: + role = @"[USER]:"; + break; + case ROLE_ASSISTANT: + role = @"[ASSISTANT]:"; + break; + case ROLE_LOG: + role = @"[LOG]:"; + break; + } + results = [results stringByAppendingFormat:@"%@%@\n", role, message.text]; + } + [self.resultTextView setText:results]; + NSRange bottom = NSMakeRange(self.resultTextView.text.length -1, 1); + [self.resultTextView scrollRangeToVisible:bottom]; +} + +#pragma mark - UITextViewDelegate + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if([text isEqualToString:@"\n"]) { + [textView resignFirstResponder]; + return NO; + } + return YES; +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_DIALOG_DELEGATE forKey:@"viewId"]; +} + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/DialogViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/DialogViewController.h new file mode 100644 index 0000000..a733c91 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/DialogViewController.h @@ -0,0 +1,17 @@ +// +// DialogViewController.m +// SpeechDemo +// +// Created by bytedance on 2025/3/27. +// Copyright © 2025 bytedance. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface DialogViewController : UIViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/DialogViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/DialogViewController.m new file mode 100644 index 0000000..4c85814 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/DialogViewController.m @@ -0,0 +1,512 @@ +// +// DialogViewController.m +// SpeechDemo +// +// Created by bytedance on 2025/3/27. +// Copyright © 2025 bytedance. All rights reserved. +// + +#import "DialogViewController.h" + +#import + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" +#import "SensitiveDefines.h" +#import "utils/DialogMessage.h" + +#pragma mark - DialogViewController +@interface DialogViewController () + +// UI +@property (weak, nonatomic) IBOutlet UIButton *initialEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *uninitialEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *startEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *stopEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *chatTextQueryButton; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextView *helloTextView; +@property (weak, nonatomic) IBOutlet UITextView *queryTextView; +@property (strong, nonatomic) NSMutableArray *dialogMessages; + +// Debug +@property (nonatomic, strong) NSString *deviceID; +@property (strong, nonatomic) NSString *debugPath; + +// Speech Engine +@property (strong, nonatomic) SpeechEngine *speechEngine; +@property (assign, nonatomic) BOOL engineStarted; + +// Settings +@property (strong, nonatomic) Settings *settings; + +@end + +@implementation DialogViewController + +NSString *const AEC_MODEL_NAME = @"aec.model"; +static const int MAX_DIALOG_MESSAGE_COUNT = 20; + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.initialEngineButton.enabled = TRUE; + self.uninitialEngineButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.chatTextQueryButton.enabled = FALSE; + + [self.statusTextView setText:@"Waiting for init."]; + [self decorateTextView:self.resultTextView]; + + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + + self.engineStarted = FALSE; + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_DIALOG]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [self uninitEngine]; + [super viewDidDisappear:animated]; +} + +- (void)decorateView:(UIView *)view { + view.layer.cornerRadius = 5.0f; + view.layer.borderWidth = .25f; + view.layer.borderColor = [UIColor grayColor].CGColor; +} + +- (void)decorateTextView:(UITextView *)textView { + textView.layer.cornerRadius = 5.0f; + textView.layer.borderWidth = .25f; + textView.layer.borderColor = [UIColor grayColor].CGColor; +} + +#pragma mark - Config & Init & Uninit Methods + +- (void)configInitParams { + //【必需配置】Engine Name + [self.speechEngine setStringParam:SE_DIALOG_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + + //【可选配置】Debug & Log + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"Debug path: %@", self.debugPath); + [self.speechEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + [self.speechEngine setStringParam:SE_LOG_LEVEL_TRACE forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + + //【必需配置】Authentication:AppId + [self.speechEngine setStringParam:[self.settings getString:SETTING_APPID] forKey:SE_PARAMS_KEY_APP_ID_STRING]; + //【必需配置】Authentication:AppKey + [self.speechEngine setStringParam:[self.settings getString:SETTING_APPKEY] forKey:SE_PARAMS_KEY_APP_KEY_STRING]; + //【必需配置】Authentication:Token + [self.speechEngine setStringParam:[self.settings getString:SETTING_TOKEN] forKey:SE_PARAMS_KEY_APP_TOKEN_STRING]; + //【必需配置】对话服务资源信息ResourceId + [self.speechEngine setStringParam:[self.settings getString:SETTING_RESOURCE_ID] forKey:SE_PARAMS_KEY_RESOURCE_ID_STRING]; + //【必需配置】User ID(用以辅助定位线上用户问题,如无法提供可提供固定字符串) + [self.speechEngine setStringParam:SDEF_UID forKey:SE_PARAMS_KEY_UID_STRING]; + //【必需配置】Dialog Address,对话服务域名 + [self.speechEngine setStringParam:[self.settings getString:SETTING_ADDRESS] forKey:SE_PARAMS_KEY_DIALOG_ADDRESS_STRING]; + //【必需配置】Dialog Uri,对话服务Uri + [self.speechEngine setStringParam:[self.settings getString:SETTING_URI] forKey:SE_PARAMS_KEY_DIALOG_URI_STRING]; + //【可选配置】是否开启AEC,默认不开启,同时启用设备录音和播放时必须开启 + [self.speechEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_ENABLE_AEC_BOOL]; + //【可选配置】AEC模型路径,开启AEC时必填 + [self.speechEngine setStringParam:[ViewController extractBundleToFilePath:AEC_MODEL_NAME] forKey:SE_PARAMS_KEY_AEC_MODEL_PATH_STRING]; + //【可选配置】配置音频来源,默认使用设备麦克风录音(Dialog 仅支持 RECORDER 和 STREAM 模式,RECORDER表示设备麦克风录音,STREAM表示自定义音频输入) + [self.speechEngine setStringParam:SE_RECORDER_TYPE_RECORDER forKey:SE_PARAMS_KEY_RECORDER_TYPE_STRING]; + //【可选配置】是否开启播放器,默认开启 + [self.speechEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_DIALOG_ENABLE_PLAYER_BOOL]; + //【可选配置】启用录音机音频回调,默认不启用 + [self.speechEngine setBoolParam:FALSE forKey:SE_PARAMS_KEY_DIALOG_ENABLE_RECORDER_AUDIO_CALLBACK_BOOL]; + //【可选配置】启用播放器音频回调,默认不启用(为当前正在播放的音频数据,会随着播放进度回调) + [self.speechEngine setBoolParam:FALSE forKey:SE_PARAMS_KEY_DIALOG_ENABLE_PLAYER_AUDIO_CALLBACK_BOOL]; + //【可选配置】启用解码后原始音频回调,默认不启用(为解码后的需要播报的数据,会在解码完成后立刻回调,不等待播放进度) + [self.speechEngine setBoolParam:FALSE forKey:SE_PARAMS_KEY_DIALOG_ENABLE_DECODER_AUDIO_CALLBACK_BOOL]; + //【可选配置】自定义请求Header + [self.speechEngine setStringParam:[self.settings getString:SETTING_REQUEST_HEADERS] forKey:SE_PARAMS_KEY_REQUEST_HEADERS_STRING]; + //【可选配置】录音文件保存路径,如不为空,则SDK会将录音机音频保存到该路径下,文件格式为 .wav + if ([self.settings getBool:SETTING_DIALOG_ENABLE_RECORDER_DUMP]) { + [self.speechEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DIALOG_RECORDER_PATH_STRING]; + } else { + [self.speechEngine setStringParam:@"" forKey:SE_PARAMS_KEY_DIALOG_RECORDER_PATH_STRING]; + } + //【可选配置】播放文件保存路径,如不为空,则SDK会将播放器音频保存到该路径下,文件格式为 .wav + if ([self.settings getBool:SETTING_DIALOG_ENABLE_PLAYER_DUMP]) { + [self.speechEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DIALOG_PLAYER_PATH_STRING]; + } else { + [self.speechEngine setStringParam:@"" forKey:SE_PARAMS_KEY_DIALOG_PLAYER_PATH_STRING]; + } +} + +- (void)initEngine { + NSLog(@"获取设备ID,调试使用"); + AppDelegate *appDelegate = [ViewController getAppDelegate]; + if (appDelegate == nil) { + appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; + } + [ViewController setAppDelegate:appDelegate]; + self.deviceID = appDelegate.deviceID; + NSLog(@"获取设备ID成功: %@", self.deviceID); + + NSLog(@"创建引擎"); + if (self.speechEngine == nil) { + self.speechEngine = [[SpeechEngine alloc] init]; + if (![self.speechEngine createEngineWithDelegate:self]) { + NSLog(@"Create speech engine failed."); + return; + } + } + NSLog(@"Engine version: %@", [self.speechEngine getVersion]); + + NSLog(@"配置初始化参数"); + [self configInitParams]; + + NSLog(@"引擎初始化"); + SEEngineErrorCode ret = [self.speechEngine initEngine]; + if (ret != SENoError) { + NSLog(@"Init Engine failed: %d", ret); + [self speechEngineInitFailed]; + return; + } + + [self speechEngineInitOk]; +} + +- (void)uninitEngine { + if (self.speechEngine) { + NSLog(@"引擎析构"); + [self.speechEngine destroyEngine]; + self.speechEngine = nil; + NSLog(@"引擎析构完成"); + } + [self.statusTextView setText:@"Engine uninited!"]; +} + +- (void)sayHello { + NSString* sayHelloJson = [NSString stringWithFormat: @"{\"content\": \"%@\"}", self.helloTextView.text]; + SEEngineErrorCode ret = [self.speechEngine sendDirective:SEDirectiveEventSayHello data:sayHelloJson]; + if (ret != SENoError) { + NSLog(@"Send directive say hello failed: %d", ret); + } else { + [self showHelloMessage:self.helloTextView.text]; + } +} + +- (void)chatTextQuery { + NSString* chatTextQueryJson = [NSString stringWithFormat: @"{\"content\": \"%@\"}", self.queryTextView.text]; + SEEngineErrorCode ret = [self.speechEngine sendDirective:SEDirectiveEventChatTextQuery data:chatTextQueryJson]; + if (ret != SENoError) { + NSLog(@"Send directive chat text query failed: %d", ret); + } else { + [self showChatTextQueryMessage:self.queryTextView.text]; + } +} + +#pragma mark - UI Actions + +- (IBAction)initEngineBtnClicked:(id)sender { + [self initEngine]; +} + +- (IBAction)uninitEngineBtnClicked:(id)sender { + if (self.engineStarted) { + [self.statusTextView setText:@"Engine is busy, stop it first!"]; + return; + } + [self uninitEngine]; + + self.initialEngineButton.enabled = TRUE; + self.uninitialEngineButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.chatTextQueryButton.enabled = FALSE; +} + +- (IBAction)startEngineBtnClicked:(id)sender { + if (self.speechEngine == nil) { + [self.statusTextView setText:@"Engine is not initialized!"]; + return; + } + // Directive:启动引擎指令。 + NSLog(@"Directive: SEDirectiveStartEngine"); + NSString* startJson = [NSString stringWithFormat: @"{\"dialog\":{\"bot_name\":\"%@\"}}", [self.settings getString:SETTING_DIALOG_BOT_NAME]]; + SEEngineErrorCode ret = [self.speechEngine sendDirective:SEDirectiveStartEngine data:startJson]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + } else if (ret == SENoError) { + self.dialogMessages = [[NSMutableArray alloc] init]; + self.resultTextView.text = @""; + // 开场白 + if (self.helloTextView.text.length != 0) { + [self sayHello]; + } + } else { + [self.statusTextView setText:[NSString stringWithFormat: @"Fail to start engine: %d", ret]]; + } +} + +- (IBAction)stopEngineBtnClicked:(id)sender { + if (self.speechEngine == nil) { + [self.statusTextView setText:@"Engine is not initialized!"]; + return; + } + NSLog(@"Stop engine."); + // Directive:关闭引擎,停止对话功能。 + NSLog(@"Directive: SEDirectiveStopEngine"); + [self.speechEngine sendDirective:SEDirectiveStopEngine]; +} + +- (IBAction)chatTextQueryBtnClicked:(id)sender { + if (self.speechEngine == nil) { + [self.statusTextView setText:@"Engine is not initialized!"]; + return; + } + [self chatTextQuery]; +} + + +#pragma mark - SpeechEngineDelegate + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSLog(@"Message Type: %d.", type); + NSString *strData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + switch (type) { + case SEEngineStart: + // Callback: 引擎启动成功回调 + NSLog(@"Callback: 引擎启动成功: %d", type); + [self speechEngineStarted:strData]; + break; + case SEEngineStop: + // Callback: 引擎关闭回调 + NSLog(@"Callback: 引擎关闭: %d", type); + [self speechEngineStopped:strData]; + break; + case SEEngineError: + // Callback: 错误信息回调 + NSLog(@"Callback: 错误信息: %d, data: %@", type, strData); + [self showLogMessage:strData]; + break; + case SEDialogASRResponse: + [self showUserMessage: data]; + break; + case SEDialogASREnded: + [self confirmUserMessage]; + break; + case SEDialogChatResponse: + [self showAssistantMessage: data]; + break; + case SEDialogASRInfo: + case SEDialogChatEnded: + [self confirmAssistantMessage]; + break; + default: + break; + } +} + +#pragma mark - SpeechEngine Callback + +- (void)speechEngineNoPermission { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"No permission!"]; + self.initialEngineButton.enabled = TRUE; + self.uninitialEngineButton.enabled = FALSE; + }); +} + +- (void)speechEngineInitOk { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:[NSString stringWithFormat:@"DeviceID: %@", self.deviceID]]; + self.initialEngineButton.enabled = FALSE; + self.uninitialEngineButton.enabled = TRUE; + self.startEngineButton.enabled = TRUE; + }); +} + +- (void)speechEngineInitFailed { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"Failed to init engine!"]; + self.initialEngineButton.enabled = TRUE; + self.uninitialEngineButton.enabled = FALSE; + }); +} + +- (void)speechEngineStarted:(NSString *)sessionId { + [self showLogMessage: [NSString stringWithFormat:@"Engine start: %@", sessionId]]; + dispatch_async(dispatch_get_main_queue(), ^{ + self.engineStarted = true; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = TRUE; + self.chatTextQueryButton.enabled = TRUE; + [self.statusTextView setText:@"Engine Started!"]; + }); +} + +- (void)speechEngineStopped:(NSString *)sessionId { + [self showLogMessage: [NSString stringWithFormat:@"Engine stop: %@", sessionId]]; + dispatch_async(dispatch_get_main_queue(), ^{ + self.engineStarted = FALSE; + self.startEngineButton.enabled = TRUE; + self.stopEngineButton.enabled = FALSE; + self.chatTextQueryButton.enabled = FALSE; + [self.statusTextView setText:@"Engine Stopped!"]; + }); +} + + +# pragma mark - Helper + +- (void)showLogMessage:(NSString *)text { + dispatch_async(dispatch_get_main_queue(), ^{ + DialogMessage* message = [[DialogMessage alloc] init]; + message.role = ROLE_LOG; + message.text = text; + message.confirmed = true; + [self.dialogMessages addObject:message]; + [self updateMessageUI]; + }); +} + +- (void)showHelloMessage:(NSString *)helloMessage { + dispatch_async(dispatch_get_main_queue(), ^{ + DialogMessage* message = [[DialogMessage alloc] init]; + message.role = ROLE_ASSISTANT; + message.text = helloMessage; + message.confirmed = true; + [self.dialogMessages addObject:message]; + [self updateMessageUI]; + }); +} + +- (void)showChatTextQueryMessage:(NSString *)queryMessage { + dispatch_async(dispatch_get_main_queue(), ^{ + [self confirmAssistantMessage]; + DialogMessage* message = [[DialogMessage alloc] init]; + message.role = ROLE_USER; + message.text = queryMessage; + message.confirmed = true; + [self.dialogMessages addObject:message]; + [self updateMessageUI]; + }); +} + +- (void)showUserMessage:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + DialogMessage* message = [self lastUnconfirmedMessage:ROLE_USER]; + if (message == nil) { + message = [[DialogMessage alloc] init]; + message.role = ROLE_USER; + message.confirmed = false; + [self.dialogMessages addObject:message]; + } + // 从回调的 json 数据中解析用户说的消息内容 + NSError *error; + NSDictionary *jsonResult = [NSJSONSerialization JSONObjectWithData:data + options:NSJSONReadingMutableContainers + error:&error]; + message.text = [[[jsonResult objectForKey:@"results"] firstObject] objectForKey:@"text"]; + [self updateMessageUI]; + }); +} + +- (void)confirmUserMessage { + dispatch_async(dispatch_get_main_queue(), ^{ + DialogMessage* message = [self lastUnconfirmedMessage:ROLE_USER]; + if (message) { + message.confirmed = true; + } + }); +} + +- (void)showAssistantMessage:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + DialogMessage* message = [self lastUnconfirmedMessage:ROLE_ASSISTANT]; + if (message == nil) { + message = [[DialogMessage alloc] init]; + message.role = ROLE_ASSISTANT; + message.text = @""; + message.confirmed = false; + [self.dialogMessages addObject:message]; + } + // 从回调的 json 数据中解析用户说的消息内容 + NSError *error; + NSDictionary *jsonResult = [NSJSONSerialization JSONObjectWithData:data + options:NSJSONReadingMutableContainers + error:&error]; + message.text = [message.text stringByAppendingString:[jsonResult objectForKey:@"content"]]; + [self updateMessageUI]; + }); +} + +- (void)confirmAssistantMessage { + dispatch_async(dispatch_get_main_queue(), ^{ + DialogMessage* message = [self lastUnconfirmedMessage:ROLE_ASSISTANT]; + if (message) { + message.confirmed = true; + } + }); +} + +- (DialogMessage*)lastUnconfirmedMessage:(Role)role { + for (DialogMessage* message in [self.dialogMessages reverseObjectEnumerator]) { + if (message.role == role) { + if (!message.confirmed) { + return message; + } + break; + } + } + return nil; +} + +- (void)updateMessageUI { + if (self.dialogMessages.count > MAX_DIALOG_MESSAGE_COUNT) { + [self.dialogMessages removeObjectAtIndex:0]; + } + NSString* results = @""; + for (DialogMessage* message in self.dialogMessages) { + NSString* role = @""; + switch (message.role) { + case ROLE_USER: + role = @"[USER]:"; + break; + case ROLE_ASSISTANT: + role = @"[ASSISTANT]:"; + break; + case ROLE_LOG: + role = @"[LOG]:"; + break; + } + results = [results stringByAppendingFormat:@"%@%@\n", role, message.text]; + } + [self.resultTextView setText:results]; + NSRange bottom = NSMakeRange(self.resultTextView.text.length -1, 1); + [self.resultTextView scrollRangeToVisible:bottom]; +} + +#pragma mark - UITextViewDelegate + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if([text isEqualToString:@"\n"]) { + [textView resignFirstResponder]; + return NO; + } + return YES; +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_DIALOG forKey:@"viewId"]; +} + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/FulllinkViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/FulllinkViewController.h new file mode 100644 index 0000000..57f1590 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/FulllinkViewController.h @@ -0,0 +1,13 @@ +// +// FulllinkViewController.h +// SpeechDemo +// +// Created by bytedance on 2020/9/8. +// Copyright © 2020 fengkai.0518. All rights reserved. +// + +#import + +@interface FulllinkViewController : UIViewController + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/FulllinkViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/FulllinkViewController.m new file mode 100644 index 0000000..a775bff --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/FulllinkViewController.m @@ -0,0 +1,484 @@ +// +// FulllinkViewController.m +// SpeechDemo +// +// Created by bytedance on 2020/9/8. +// Copyright © 2020 fengkai.0518. All rights reserved. +// + +#import "FulllinkViewController.h" + +#import + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" +#import "SensitiveDefines.h" + +@interface FulllinkViewController () + +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextView *referTextView; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; +@property (weak, nonatomic) IBOutlet UIButton *engineInitButton; +@property (weak, nonatomic) IBOutlet UIButton *engineUninitButton; +@property (weak, nonatomic) IBOutlet UIButton *startEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *stopEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *longPressButton; +@property (weak, nonatomic) IBOutlet UIButton *forceWakeupButton; +@property (weak, nonatomic) IBOutlet UIButton *cancelDialogButton; + +@property (strong, nonatomic) SpeechEngine *curEngine; +@property (assign, nonatomic) BOOL engineStarted; + +@property (nonatomic, strong) NSString *deviceID; +@property (nonatomic, assign) long talkingFinisheTimestamp; +@property (nonatomic, assign) long startEngineTimestamp; +@property (strong, nonatomic) NSString *debugPath; + +@property (strong, nonatomic) NSArray *engineNameArray; +@property (assign, nonatomic) NSInteger engineNameType; + +@property (weak, nonatomic) StreamRecorder *streamRecorder; + +// settings +@property (strong, nonatomic) Settings *settings; +@end + +@implementation FulllinkViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_FULLLINK]; + + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.longPressButton.enabled = FALSE; + self.forceWakeupButton.enabled = FALSE; + self.cancelDialogButton.enabled = FALSE; + [self.statusTextView setText:@"Waiting for init."]; + [self decorateTextView:self.resultTextView]; + [self decorateTextView:self.referTextView]; + [self.referTextView setDelegate:self]; + self.referTextView.editable = TRUE; + self.referTextView.text = @"高兴的反义词"; + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + UILongPressGestureRecognizer *longPgr = [[UILongPressGestureRecognizer alloc] initWithTarget:self + action:@selector(longPressTriggered:)]; + longPgr.minimumPressDuration = 0.5; + [self.longPressButton addGestureRecognizer:longPgr]; + self.streamRecorder = [ViewController getStreamRecorder]; + self.engineStarted = FALSE; + self.engineNameArray = @[SE_FULLLINK_LITE_ENGINE, SE_FULLLINK_ENGINE]; + self.engineNameType = 0; +} + +- (void)viewDidDisappear:(BOOL)animated { + [self uninitEngine]; + [super viewDidDisappear:animated]; +} + +- (void)decorateTextView:(UITextView *)textView { + textView.layer.cornerRadius = 5.0f; + textView.layer.borderWidth = .25f; + textView.layer.borderColor = [UIColor grayColor].CGColor; +} + +#pragma mark - SpeechEngineDelegate + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSLog(@"Message Type: %d.", type); + switch (type) { + case SEEngineStart: + [self speechEngineStarted]; + break; + case SEEngineStop: + [self speechEngineStopped]; + break; + case SEEngineError: + [self speechEngineError:data]; + break; + case SEWakeupResult: + [self speechEngineWakeUp:data]; + break; + case SEAsrPartialResult: + [self speechEngineResult:data isFinal:FALSE]; + break; + case SEFinalResult: + [self speechEngineResult:data isFinal:TRUE]; + break; + case SENluResult: + [self speechEngineNluResult:data]; + break; + case SEVolumeLevel: + NSLog(@"volume level: %s", (char*)data.bytes); + break; + case SEEngineLog: + NSLog(@"engine log: %s", (char*)data.bytes); + break; + default: + break; + } +} + +#pragma mark - UI Actions + +- (IBAction)InitEngine:(id)sender { + [self initEngine]; +} + +- (IBAction)uninitEngine:(id)sender { + if (self.engineStarted) { + [self.statusTextView setText:@"Engine is busy, stop it first!"]; + return; + } + [self uninitEngine]; + [self.resultTextView setTextColor:UIColor.grayColor]; + [self.resultTextView setText:@"点击或按住说话后,展示语音识别结果"]; +} + +- (IBAction)startEngine:(id)sender { + NSLog(@"Start engine."); + [self.curEngine setBoolParam:[self.settings getBool:SETTING_GET_VOLUME] + forKey:SE_PARAMS_KEY_ENABLE_GET_VOLUME_BOOL]; + NSString* engine_name = [self.engineNameArray objectAtIndex:self.engineNameType]; + if (engine_name == SE_FULLLINK_LITE_ENGINE) { + [self.curEngine setBoolParam:[self.settings getBool:SETTING_FULLLINK_ONLY_ASR] forKey:SE_PARAMS_KEY_FULLLINK_ASR_ONLY_BOOL]; + [self.curEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_FULLLINK_ASR_AUTO_STOP_BOOL]; + } + [self configUserParams]; + + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.curEngine setIntParam:[self.streamRecorder getSampleRate] forKey:SE_PARAMS_KEY_CUSTOM_SAMPLE_RATE_INT]; + if (![self.streamRecorder start]) { + [self speechEngineNoPermission]; + return; + } + } else if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_FILE]) { + NSString* file_path = [NSString stringWithFormat:@"%@/%@", self.debugPath, @"recorder.pcm"]; + NSLog(@"test file path: %@", file_path); + [self.curEngine setStringParam:file_path forKey:SE_PARAMS_KEY_RECORDER_FILE_STRING]; + } + + NSInteger ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + } +} + +- (IBAction)stopEngine:(id)sender { + NSLog(@"Stop engine."); + [self.curEngine sendDirective:SEDirectiveStopEngine]; +} + +- (IBAction)forceWakeup:(id)sender { + NSLog(@"Force wakeup."); + NSString* engine_name = [self.engineNameArray objectAtIndex:self.engineNameType]; + if (engine_name == SE_FULLLINK_ENGINE) { + [self.curEngine sendDirective:SEDirectiveTriggerWakeup]; + } +} + +- (IBAction)cancelDialog:(id)sender { + NSLog(@"Force wakeup."); + [self.curEngine sendDirective:SEDirectiveCancelCurrentDialog]; +} + +- (IBAction)longPressTriggered:(UILongPressGestureRecognizer *)longPgr { + [self configUserParams]; + if (longPgr.state == UIGestureRecognizerStateBegan) { + NSLog(@"Long press begin."); + [self setResultText:@""]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_GET_VOLUME] + forKey:SE_PARAMS_KEY_ENABLE_GET_VOLUME_BOOL]; + NSString* engine_name = [self.engineNameArray objectAtIndex:self.engineNameType]; + if (engine_name == SE_FULLLINK_LITE_ENGINE) { + [self.curEngine setBoolParam:[self.settings getBool:SETTING_FULLLINK_ONLY_ASR] forKey:SE_PARAMS_KEY_FULLLINK_ASR_ONLY_BOOL]; + } + + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.curEngine setIntParam:[self.streamRecorder getSampleRate] forKey:SE_PARAMS_KEY_CUSTOM_SAMPLE_RATE_INT]; + if (![self.streamRecorder start]) { + [self speechEngineNoPermission]; + return; + } + } else if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_FILE]) { + NSString* file_path = [NSString stringWithFormat:@"%@/%@", self.debugPath, @"recorder.pcm"]; + NSLog(@"test file path: %@", file_path); + [self.curEngine setStringParam:file_path forKey:SE_PARAMS_KEY_RECORDER_FILE_STRING]; + } + + NSInteger ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + } + } else if (longPgr.state == UIGestureRecognizerStateEnded) { + NSLog(@"Long press ended."); + self.talkingFinisheTimestamp = [[NSDate date] timeIntervalSince1970] * 1000; + [self.curEngine sendDirective:SEDirectiveFinishTalking]; + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.streamRecorder stop]; + } + } +} + +#pragma mark - Init Methods + +- (void)initEngine { + AppDelegate *appDelegate = [ViewController getAppDelegate]; + if (appDelegate == nil) { + appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; + } + if (appDelegate.deviceID.length < 1) { + self.engineInitButton.enabled = FALSE; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:@"Waiting for get deviceID."]; + sleep(1); + [self initEngine]; + }); + return; + } + [ViewController setAppDelegate:appDelegate]; + self.deviceID = appDelegate.deviceID; + + + if (self.curEngine == nil) { + self.curEngine = [[SpeechEngine alloc] init]; + } + if (![self.curEngine createEngineWithDelegate:self]) { + NSLog(@"Create speech engine failed."); + return; + } + [self.resultTextView setTextColor:UIColor.blackColor]; + NSLog(@"Engine version: %@", [self.curEngine getVersion]); + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"Debug path: %@", self.debugPath); + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + [self.curEngine setStringParam:[NSString stringWithFormat:@"%@/%@", self.debugPath, @"kws"] forKey:SE_PARAMS_KEY_KWS_ROOT_PATH_STRING]; + [self.curEngine setStringParam:[NSString stringWithFormat:@"%@/%@", self.debugPath, @"signal"] forKey:SE_PARAMS_KEY_SIGNAL_ROOT_PATH_STRING]; + + [self.curEngine setStringParam:SE_LOG_LEVEL_TRACE forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + [self.curEngine setStringParam:@"robot_ios" forKey:SE_PARAMS_KEY_APP_ID_STRING]; + [self.curEngine setStringParam:SDEF_UID forKey:SE_PARAMS_KEY_UID_STRING]; + [self.curEngine setIntParam:1 forKey:SE_PARAMS_KEY_CHANNEL_NUM_INT]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_GET_VOLUME] + forKey:SE_PARAMS_KEY_ENABLE_GET_VOLUME_BOOL]; + NSString *address = [self.settings getString:SETTING_ADDRESS]; + if (!address.length) { + address = SDEF_DEFAULT_ADDRESS; + } + NSLog(@"Current address: %@", address); + [self.curEngine setStringParam:address forKey:SE_PARAMS_KEY_FULLLINK_ADDRESS_STRING]; + NSString *uri = [self.settings getString:SETTING_URI]; + if (!uri.length) { + uri = SDEF_FULLLINK_DEFAULT_URI; + } + NSLog(@"Current uri: %@", uri); + [self.curEngine setStringParam:uri forKey:SE_PARAMS_KEY_FULLLINK_URI_STRING]; + [self.curEngine setStringParam:@"" forKey:SE_PARAMS_KEY_FULLLINK_QUERY_STRING_STRING]; + [self.curEngine setStringParam:[self getRecorderType] forKey:SE_PARAMS_KEY_RECORDER_TYPE_STRING]; + self.engineNameType = [self.settings getOptions:SETTING_FULLLINK_ENGINE_TYPE].chooseIdx; + [self.curEngine setStringParam:[self.engineNameArray objectAtIndex:self.engineNameType] forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_FULLLINK_ENABLE_RECORDER_DUMP] forKey:@"enable_dump"]; + + + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + if ([self.streamRecorder getSampleRate] != 16000) { + [self.curEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_ENABLE_RESAMPLER_BOOL]; + } + } + + NSInteger ret = [self.curEngine initEngine]; + if (ret != SENoError) { + NSLog(@"Init Engine failed: %ld", ret); + } + if (ret == SENoError) { + [self speechEngineInitOk]; + } else { + [self speechEngineInitFailed]; + } +} + +- (void)configUserParams { + NSString *param = @"{\"auth_key\":\"\",\"product_info\":{\"product_id\":2001},\"nlg_request\":{\"nlg_type\":\"\",\"nlg_content\":\""; + param = [param stringByAppendingString:self.referTextView.text]; + param = [param stringByAppendingString:@"\"},\"session_info\":{},\"client_info\":{\"client_user_id\":\"123456\",\"client_device_id\":\"123456\",\"client_app_id\":\"111\",\"client_device_unique_code\":\"111\",\"sdk_version\":0,\"client_app_version\":\"\"}}"]; + [self.curEngine setStringParam:param forKey:SE_PARAMS_KEY_FULLLINK_USER_PARAM_STRING]; +} + +- (NSString *)getRecorderType { + switch ([self.settings getOptions:SETTING_RECORD_TYPE].chooseIdx) { + case 0: + return SE_RECORDER_TYPE_RECORDER; + case 1: + return SE_RECORDER_TYPE_FILE; + case 2: + return SE_RECORDER_TYPE_STREAM; + default: + break; + } + return @""; +} + +- (void)uninitEngine { + [self.curEngine destroyEngine]; + self.curEngine = nil; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.longPressButton.enabled = FALSE; + self.forceWakeupButton.enabled = FALSE; + self.cancelDialogButton.enabled = FALSE; +} + +#pragma mark - Engine Callback + +- (void)speechEngineNoPermission { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"No permission!"]; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + }); +} + +- (void)speechEngineInitOk { + [self.streamRecorder setSpeechEngine:self.curEngine]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:@"Ready"]; + [self.resultTextView setText:[NSString stringWithFormat:@"DeviceID: %@", self.deviceID]]; + self.engineUninitButton.enabled = TRUE; + self.engineInitButton.enabled = FALSE; + self.startEngineButton.enabled = TRUE; + self.longPressButton.enabled = TRUE; + }); +} + +- (void)speechEngineInitFailed { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"Failed to init engine!"]; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + }); +} + +- (void)speechEngineStarted { + dispatch_async(dispatch_get_main_queue(), ^{ + self.startEngineTimestamp = [[NSDate date] timeIntervalSince1970] * 1000; + self.engineStarted = true; + [self.statusTextView setText:@"Engine Started!"]; + [self setResultText:@""]; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = TRUE; + self.longPressButton.enabled = FALSE; + self.forceWakeupButton.enabled = TRUE; + self.cancelDialogButton.enabled = TRUE; + }); +} + +- (void)speechEngineStopped { + dispatch_async(dispatch_get_main_queue(), ^{ + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.streamRecorder stop]; + } + self.engineStarted = FALSE; + [self.statusTextView setText:@"Engine Stopped!"]; + self.startEngineButton.enabled = TRUE; + self.stopEngineButton.enabled = FALSE; + self.longPressButton.enabled = TRUE; + self.forceWakeupButton.enabled = FALSE; + self.cancelDialogButton.enabled = FALSE; + }); +} + +- (void)speechEngineResult:(NSData *)data isFinal:(BOOL)isFinal { + long response_delay = 0; + if (isFinal && self.talkingFinisheTimestamp > 0) { + response_delay = [self timeDelayFrom:self.talkingFinisheTimestamp]; + self.talkingFinisheTimestamp = 0; + } + + NSError *error; + NSDictionary *jsonResult = [NSJSONSerialization JSONObjectWithData:data + options:NSJSONReadingMutableContainers + error:&error]; + NSMutableString *text = [[NSMutableString alloc] initWithString:@""]; + if (![jsonResult objectForKey:@"messageData"]) { + return; + } + [text appendFormat:@"%@", [[[jsonResult objectForKey:@"messageData"] objectForKey:@"inputText"] objectForKey:@"text"]]; + if (text.length) { + [self setResultText:text]; + } +} + +- (void)speechEngineNluResult:(NSData *)data { + NSError *error; + NSDictionary *jsonResult = [NSJSONSerialization JSONObjectWithData:data + options:NSJSONReadingMutableContainers + error:&error]; + NSMutableString *text = [[NSMutableString alloc] initWithString:@""]; + if (![jsonResult objectForKey:@"messageData"]) { + return; + } + [text appendFormat:@"%@\n%@", [[jsonResult objectForKey:@"messageData"] objectForKey:@"inputText"], [[jsonResult objectForKey:@"messageData"] objectForKey:@"outputText"]]; + if (text.length) { + [self setResultText:text]; + } + bool disableTts = [self.settings getBool:SETTING_FULLLINK_DISABLE_TTS]; + [self.curEngine sendDirective:SEDirectivePlayingDecision data:disableTts ? @"false" : @"true"]; +} + +- (void)speechEngineError:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]]; +// [self stopEngine:nil]; + }); +} + +- (void)speechEngineWakeUp:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]]; + }); +} + +- (void)setResultText:(NSString *)result { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[result stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; + }); +} + +#pragma mark - Helper + +- (long)timeDelayFrom:(long)pastTimestamp { + return [[NSDate date] timeIntervalSince1970] * 1000 - pastTimestamp; +} + +#pragma mark - UITextViewDelegate + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if([text isEqualToString:@"\n"]) { + [textView resignFirstResponder]; + return NO; + } + return YES; +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_FULLLINK forKey:@"viewId"]; +} + +@end + + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/KwsViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/KwsViewController.h new file mode 100644 index 0000000..15828f8 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/KwsViewController.h @@ -0,0 +1,13 @@ +// +// AsrOfflineViewController.h +// SpeechDemo +// +// Created by bytedance on 2020/9/8. +// Copyright © 2020 fengkai.0518. All rights reserved. +// + +#import + +@interface KwsViewController : UIViewController + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/KwsViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/KwsViewController.m new file mode 100644 index 0000000..2abc0a0 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/KwsViewController.m @@ -0,0 +1,482 @@ +// +// KwsViewController.m +// SpeechDemo +// +// Created by bytedance on 2020/9/8. +// Copyright © 2020 fengkai.0518. All rights reserved. +// + +#import "KwsViewController.h" + +#import + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" +#import "SpeechResourceManager.h" +#import "SensitiveDefines.h" + +@interface KwsViewController () + +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; +@property (weak, nonatomic) IBOutlet UIButton *engineInitButton; +@property (weak, nonatomic) IBOutlet UIButton *engineUninitButton; +@property (weak, nonatomic) IBOutlet UIButton *startEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *stopEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *longPressButton; + +@property (strong, nonatomic) SpeechEngine *curEngine; +@property (assign, nonatomic) BOOL engineStarted; + +// Device ID: 用于定位线上问题 +@property (nonatomic, strong) NSString *deviceID; +// Debug Path: 用于存放一些 SDK 相关的文件,比如模型、日志等 +@property (strong, nonatomic) NSString *debugPath; + +@property (weak, nonatomic) StreamRecorder *streamRecorder; + +// settings +@property (strong, nonatomic) Settings *settings; + +@end + +@implementation KwsViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_KWS]; + + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.longPressButton.enabled = FALSE; + [self.statusTextView setText:@"Waiting for init."]; + [self decorateTextView:self.resultTextView]; + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + UILongPressGestureRecognizer *longPgr = [[UILongPressGestureRecognizer alloc] initWithTarget:self + action:@selector(longPressTriggered:)]; + longPgr.minimumPressDuration = 0.5; + [self.longPressButton addGestureRecognizer:longPgr]; + self.streamRecorder = [ViewController getStreamRecorder]; + self.engineStarted = FALSE; +} + +- (void)viewDidDisappear:(BOOL)animated { + [self uninitEngine]; + [super viewDidDisappear:animated]; +} + +- (void)decorateTextView:(UITextView *)textView { + textView.layer.cornerRadius = 5.0f; + textView.layer.borderWidth = .25f; + textView.layer.borderColor = [UIColor grayColor].CGColor; +} + +#pragma mark - Config & Init & Uninit Methods + +-(void)configInitParams{ + //【必需配置】Engine Name + [self.curEngine setStringParam:SE_KWS_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + + //【可选配置】Debug & Log + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + [self.curEngine setStringParam:SE_LOG_LEVEL_DEBUG forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + + //【必需配置】配置音频来源 + [self.curEngine setStringParam:[self.settings getOptionsValue:SETTING_RECORD_TYPE] forKey:SE_PARAMS_KEY_RECORDER_TYPE_STRING]; + + // 当音频来源为 RECORDER_TYPE_STREAM 时,如输入音频采样率不等于 16K,需添加如下配置 + if ([self.settings getOptionsValue:SETTING_RECORD_TYPE] == SE_RECORDER_TYPE_STREAM) { + if ([self.streamRecorder getSampleRate] != 16000) { + // 当音频来源为 RECORDER_TYPE_STREAM 时【必需配置】,否则【无需配置】 + // 启用 SDK 内部的重采样 + [self.curEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_ENABLE_RESAMPLER_BOOL]; + // 将重采样所需的输入采样率设置为 APP 层输入的音频的实际采样率 + [self.curEngine setIntParam:[self.streamRecorder getSampleRate] forKey:SE_PARAMS_KEY_CUSTOM_SAMPLE_RATE_INT]; + } + } + + NSString* recPath = @""; + if ([self.settings getBool:SETTING_RECORD_SAVE]) { + recPath = self.debugPath; + } + [self.curEngine setStringParam:recPath forKey:SE_PARAMS_KEY_REC_PATH_STRING]; + [self.curEngine setIntParam:[self.settings getOptions:SETTING_RECORD_FILE_TYPE].chooseIdx forKey:SE_PARAMS_KEY_REC_FILE_TYPE_INT]; + + [self.curEngine setStringParam:@"{\"array_type\": 0,\"radius\":0.0,\"total_channels\":1,\"mic_offset\":0,\"mic_num\":1,\"ref_offset\":0,\"ref_num\":0,\"vad_speech_active_thresh\":0.5,\"vad_speech_deactive_thresh\":0.5}" forKey:SE_PARAMS_KEY_KWS_USER_PARAM_STRING]; + [self.curEngine setIntParam:600 forKey:SE_PARAMS_KEY_KWS_VAD_END_SILENCE_TIMEOUT_INT]; + [self.curEngine setBoolParam:false forKey:SE_PARAMS_KEY_KWS_ENABLE_VAD_BOOL]; + + SpeechResourceManager *resourceManager = [SpeechResourceManager shareInstance]; + NSString* modelsPath = [resourceManager getModelPath:[self getKwsModelName]]; + NSLog(@"Kws model path: %@", modelsPath); + [self.curEngine setStringParam:modelsPath forKey:SE_PARAMS_KEY_KWS_ROOT_PATH_STRING]; +} + +-(void)configStartParams{ + //【可选配置】控制是否返回录音音量,在 APP 需要显示音频波形时可以启用 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_GET_VOLUME] forKey:SE_PARAMS_KEY_ENABLE_GET_VOLUME_BOOL]; + + if ([self.settings getOptionsValue:SETTING_RECORD_TYPE] == SE_RECORDER_TYPE_STREAM) { + if (![self.streamRecorder start]) { + [self speechEngineNoPermission]; + } + } else if ([self.settings getOptionsValue:SETTING_RECORD_TYPE] == SE_RECORDER_TYPE_FILE) { + // 使用音频文件唤醒时,需要设置文件的绝对路径 + NSString* file_path = [NSString stringWithFormat:@"%@/%@", self.debugPath, @"kws_rec_file.pcm"]; + NSLog(@"输入的音频文件路径: %@", file_path); + // 使用音频文件唤醒时【必须配置】,否则【无需配置】 + [self.curEngine setStringParam:file_path forKey:SE_PARAMS_KEY_RECORDER_FILE_STRING]; + } +} + +#pragma mark - Init Methods + +- (void)initEngine { + NSLog(@"获取设备ID,调试使用"); + AppDelegate *appDelegate = [ViewController getAppDelegate]; + if (appDelegate == nil) { + appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; + } + [ViewController setAppDelegate:appDelegate]; + self.deviceID = appDelegate.deviceID; + NSLog(@"获取设备ID成功: %@", self.deviceID); + + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"当前调试路径: %@", self.debugPath); + + SpeechResourceManager *resourceManager = [SpeechResourceManager shareInstance]; + [resourceManager setAppId:SDEF_APPID]; + [resourceManager setAppVersion:@"1.0.0"]; + [resourceManager setDeviceId:self.deviceID]; + [resourceManager setRootPath:self.debugPath]; + [resourceManager setSpeechEngineName:SE_KWS_ENGINE]; + [resourceManager setAddress:@"https://sdk.bytespeech.com"]; + [resourceManager setOnlineModelEnable:NO]; + [resourceManager setup]; + + if ([resourceManager checkModelExist:[self getKwsModelName]]) { + [resourceManager checkModelVersion:[self getKwsModelName] completion:^(SEResourceStatus status, BOOL needUpdate, NSData *data) { + if (status == kSERSuccess) { + if (needUpdate) { + [self fetchResource]; + } else { + [self initEngineInternal]; + } + } else { + NSLog(@"Model check failed: %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); + [self.resultTextView setText:@"Failed to check asr resource"]; + [self speechEngineInitFailed:status]; + } + }]; + } else { + [self fetchResource]; + } +} + +- (void)fetchResource { + SpeechResourceManager *resourceManager = [SpeechResourceManager shareInstance]; + [resourceManager fetchModelByName:[self getKwsModelName] completion:^(SEResourceStatus status, NSData* data) { + if (status == kSERSuccess) { + [self initEngineInternal]; + } else { + [self.resultTextView setText:@"Failed to fetch asr resource"]; + [self speechEngineInitFailed:status]; + } + }]; +} + +- (void)initEngineInternal { + + NSLog(@"创建引擎"); + if (self.curEngine == nil) { + self.curEngine = [[SpeechEngine alloc] init]; + if (![self.curEngine createEngineWithDelegate:self]) { + NSLog(@"引擎创建失败."); + return; + } + } + [self.resultTextView setTextColor:UIColor.blackColor]; + NSLog(@"SDK 版本号: %@", [self.curEngine getVersion]); + + NSLog(@"配置初始化参数"); + [self configInitParams]; + + NSLog(@"引擎初始化"); + SEEngineErrorCode ret = [self.curEngine initEngine]; + if (ret == SENoError) { + NSLog(@"初始化成功"); + [self speechEngineInitSucceeded]; + } else { + NSLog(@"初始化失败,返回值: %d", ret); + [self speechEngineInitFailed:ret]; + } +} + +- (void)uninitEngine { + [self.curEngine destroyEngine]; + self.curEngine = nil; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.longPressButton.enabled = FALSE; +} + +- (void)speechEngineInitSucceeded { + [self.streamRecorder setSpeechEngine:self.curEngine]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:@"Ready"]; + self.engineUninitButton.enabled = TRUE; + self.engineInitButton.enabled = FALSE; + self.startEngineButton.enabled = TRUE; + self.longPressButton.enabled = TRUE; + }); +} + +- (void)speechEngineInitFailed:(int)initStatus { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:[[NSString alloc] initWithFormat:@"Failed to init engine, %d!", initStatus]]; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + }); +} + +- (void)speechEngineNoPermission { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"No permission!"]; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + }); +} + +#pragma mark - UI Actions + +- (IBAction)initEngineBtnClicked:(id)sender { + [self initEngine]; +} + +- (IBAction)uninitEngineBtnClicked:(id)sender { + if (self.engineStarted) { + [self.statusTextView setText:@"Engine is busy, stop it first!"]; + return; + } + [self uninitEngine]; + [self.resultTextView setTextColor:UIColor.grayColor]; + [self.resultTextView setText:@"点击或按住说话后,展示语音唤醒结果"]; +} + +- (IBAction)startEngineBtnClicked:(id)sender { + [self setResultText:@""]; + + NSLog(@"配置启动参数"); + [self configStartParams]; + + // Directive:启动引擎前调用SYNC_STOP指令,保证前一次请求结束。 + NSLog(@"Directive: SEDirectiveSyncStopEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveSyncStopEngine]; + if (ret != SENoError) { + NSLog(@"Send directive syncstop failed: %d", ret); + } else { + NSLog(@"启用引擎."); + NSLog(@"Directive: SEDirectiveStartEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + } + } +} + +- (IBAction)stopEngineBtnClicked:(id)sender { + NSLog(@"关闭引擎"); + NSLog(@"Directive: SEDirectiveStopEngine"); + [self.curEngine sendDirective:SEDirectiveStopEngine]; +} + +- (void)recordTriggered:(UILongPressGestureRecognizer *)longPgr { + if (longPgr.state == UIGestureRecognizerStateBegan) { + [self setResultText:@""]; + + NSLog(@"配置启动参数"); + [self configStartParams]; + + // Directive:启动引擎前调用SYNC_STOP指令,保证前一次请求结束。 + NSLog(@"Directive: SEDirectiveSyncStopEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveSyncStopEngine]; + if (ret != SENoError) { + NSLog(@"Send directive syncstop failed: %d", ret); + } else { + // Directive:启动引擎指令。 + NSLog(@"Directive: SEDirectiveStartEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + } + } + } else if (longPgr.state == UIGestureRecognizerStateEnded) { + // Directive:结束音频输入。 + NSLog(@"Directive: SEDirectiveFinishTalking"); + [self.curEngine sendDirective:SEDirectiveFinishTalking]; + if ([self.settings getOptionsValue:SETTING_RECORD_TYPE] == SE_RECORDER_TYPE_STREAM) { + [self.streamRecorder stop]; + } + } +} + +#pragma mark - SpeechEngineDelegate + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSLog(@"Message Type: %d.", type); + switch (type) { + case SEEngineStart: + // Callback: 引擎启动成功回调 + NSLog(@"Callback: 引擎启动成功"); + [self speechEngineStarted]; + break; + case SEEngineStop: + // Callback: 引擎关闭回调 + NSLog(@"Callback: 引擎关闭"); + [self speechEngineStopped]; + break; + case SEEngineError: + // Callback: 错误信息回调 + NSLog(@"Callback: 错误信息: %@", data); + [self speechEngineError:data]; + break; + case SEWakeupResult: + // Callback: 唤醒结果 + NSLog(@"Callback: 唤醒结果"); + [self speechEngineResult:data]; + break; + case SEFinalResult: + case SEVolumeLevel: + // Callback: 录音音量回调 + NSLog(@"Callback: 录音音量"); + break; + default: + break; + } +} + +#pragma mark - Engine Callback + +- (void)speechEngineStarted { + dispatch_async(dispatch_get_main_queue(), ^{ + self.engineStarted = true; + [self.statusTextView setText:@"Engine Started!"]; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = TRUE; + self.longPressButton.enabled = FALSE; + }); +} + +- (void)speechEngineStopped { + dispatch_async(dispatch_get_main_queue(), ^{ + if ([self getRecorderType] == SE_RECORDER_TYPE_STREAM) { + [self.streamRecorder stop]; + } + self.engineStarted = FALSE; + [self.statusTextView setText:@"Engine Stopped!"]; + self.startEngineButton.enabled = TRUE; + self.stopEngineButton.enabled = FALSE; + self.longPressButton.enabled = TRUE; + }); +} + +- (void)speechEngineResult:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]]; + }); +} + +- (void)speechEngineError:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + // 从回调的 json 数据中解析错误码和错误详细信息 + id error_json = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil]; + if ([error_json isKindOfClass:[NSDictionary class]]) { + NSDictionary *error_info = error_json; + NSInteger error_code = [[error_info objectForKey:@"err_code"] intValue]; + switch (error_code) { + case SEEncodingAudioError: + [self stopEngineBtnClicked:nil]; + break; + default: + break; + } + // 在 UI 显示错误信息 + if ([error_info objectForKey:@"name"] != nil) { + NSString* error_msg = [[error_json objectForKey:@"err_msg"] stringValue]; + NSString* reqid = [[error_json objectForKey:@"reqid"] stringValue]; + [self setResultText:[NSString stringWithFormat:@"reqid: %@, error: %@", reqid, error_msg]]; + } else { + [self setResultText:[NSString stringWithFormat:@"%@", error_info]]; + } + } + }); +} + +#pragma mark - Helper + +- (NSString *)getRecorderType { + SettingOptions* recorderTypeOptions = [self.settings getOptions:SETTING_RECORD_TYPE]; + switch (recorderTypeOptions.chooseIdx) { + case 0: + return SE_RECORDER_TYPE_RECORDER; + case 1: + return SE_RECORDER_TYPE_FILE; + case 2: + return SE_RECORDER_TYPE_STREAM; + default: + break; + } + return @""; +} + +- (void)setResultText:(NSString *)result { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[result stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; + }); +} + +- (void)updateWakeupWords { + if (self.curEngine == nil) { + return; + } + NSString* customWords = [self.settings getString:SETTING_KWS_CUSTOM_WORDS]; + if (customWords.length == 0) { + return; + } + [self.curEngine sendDirective:SEDirectiveUpdateWakeupWordsParams data:customWords]; +} + +- (NSString*)getKwsModelName { + return [self.settings getString:SETTING_KWS_MODEL_NAME]; +} + +#pragma mark - UITextViewDelegate + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if([text isEqualToString:@"\n"]) { + [textView resignFirstResponder]; + return NO; + } + return YES; +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_KWS forKey:@"viewId"]; +} + +@end + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/SpeechDemoToB-Info.plist b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/SpeechDemoToB-Info.plist new file mode 100644 index 0000000..3a74e09 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/SpeechDemoToB-Info.plist @@ -0,0 +1,70 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 2 + LSRequiresIPhoneOS + + NSMicrophoneUsageDescription + Speech demo would like to access your recorder data. + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + SceneDelegate + UISceneStoryboardFile + Main + + + + + UIBackgroundModes + + audio + + UIFileSharingEnabled + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/TtsNormalViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/TtsNormalViewController.h new file mode 100644 index 0000000..c448b53 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/TtsNormalViewController.h @@ -0,0 +1,13 @@ +// +// TtsNormalViewController.h +// SpeechDemo +// +// Created by bytedance on 2020/9/8. +// Copyright © 2020 fengkai.0518. All rights reserved. +// + +#import + +@interface TtsNormalViewController : UIViewController + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/TtsNormalViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/TtsNormalViewController.m new file mode 100644 index 0000000..7000b46 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/TtsNormalViewController.m @@ -0,0 +1,824 @@ +// +// TtsNormalViewController.m +// SpeechDemo +// +// Created by bytedance on 2020/9/8. +// Copyright © 2020 fengkai.0518. All rights reserved. +// + +#import "TtsNormalViewController.h" +#include + +#import + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" +#import "SensitiveDefines.h" + +@interface TtsNormalViewController () + +@property (weak, nonatomic) IBOutlet UITextView *referTextView; +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; +@property (weak, nonatomic) IBOutlet UIButton *engineSwitchButton; +@property (weak, nonatomic) IBOutlet UIButton *createConnectionButton; +@property (weak, nonatomic) IBOutlet UIButton *startEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *stopEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *pauseResumeButton; + +// Device ID: 用于定位线上问题 +@property (nonatomic, strong) NSString *deviceID; +// Debug Path: 用于存放一些 SDK 相关的文件,比如模型、日志等 +@property (strong, nonatomic) NSString *debugPath; + +// SpeechEngine +@property (strong, nonatomic) SpeechEngine *curEngine; + +// Engine State +@property (assign, nonatomic) BOOL engineInited; +@property (assign, nonatomic) BOOL connectionCreated; +@property (assign, nonatomic) BOOL engineStarted; +@property (assign, nonatomic) BOOL engineErrorOccurred; +@property (assign, nonatomic) BOOL playerPaused; + +// Settings +@property (strong, nonatomic) Settings *settings; + +// 一些在线合成的配置 +@property (strong, nonatomic) NSString *ttsAppId; +@property (strong, nonatomic) NSString *ttsVoiceOnline; +@property (strong, nonatomic) NSString *ttsVoiceTypeOnline; + +// 一些离线合成的配置 +@property (strong, nonatomic) NSString *ttsVoiceOffline; +@property (strong, nonatomic) NSString *ttsVoiceTypeOffline; + +// 用于合成的文本 +@property (strong, nonatomic) NSString *ttsText; + + +@end + +@implementation TtsNormalViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_TTS]; + + self.engineSwitchButton.enabled = TRUE; + [self decorateTextView:self.referTextView]; + [self decorateTextView:self.resultTextView]; + [self.referTextView setDelegate:self]; + self.referTextView.editable = TRUE; + self.engineInited = FALSE; + self.connectionCreated = FALSE; + self.engineStarted = FALSE; + self.engineErrorOccurred = FALSE; + self.playerPaused = FALSE; + + + [self.statusTextView setText:@"Waiting for init."]; + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"当前调试路径 %@", self.debugPath); + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(appDidEnterBackground:) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; +} + +- (void)decorateTextView:(UITextView *)textView { + textView.layer.cornerRadius = 5.0f; + textView.layer.borderWidth = .25f; + textView.layer.borderColor = [UIColor grayColor].CGColor; +} + +#pragma mark - Notifications + +- (void)appDidEnterBackground:(UIApplication *)application; { + NSLog(@"app enter background."); + [self.curEngine sendDirective:SEDirectiveStopEngine]; +} + +-(void)appWillTerminate:(NSNotification*)note { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIApplicationDidEnterBackgroundNotification + object:nil]; +} + +#pragma mark - Config & Init & Uninit Methods + +-(void)configInitParams { + //【必需配置】Engine Name + [self.curEngine setStringParam:SE_TTS_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + + //【必需配置】Work Mode, 可选值如下 + // SETtsWorkModeOnline, 只进行在线合成,不需要配置离线合成相关参数; + // SETtsWorkModeOffline, 只进行离线合成,不需要配置在线合成相关参数; + // SETtsWorkModeAlternate, 先发起在线合成,失败后(网络超时),启动离线合成引擎开始合成; + [self.curEngine setIntParam:[self getTtsWorkMode] forKey:SE_PARAMS_KEY_TTS_WORK_MODE_INT]; + + //【可选配置】Debug & Log + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + [self.curEngine setStringParam:SE_LOG_LEVEL_DEBUG forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + + //【可选配置】User ID(用以辅助定位线上用户问题) + [self.curEngine setStringParam:SDEF_UID forKey:SE_PARAMS_KEY_UID_STRING]; + [self.curEngine setStringParam:self.deviceID forKey:SE_PARAMS_KEY_DEVICE_ID_STRING]; + + //【可选配置】是否将合成出的音频保存到设备上,为 true 时需要正确配置 PARAMS_KEY_TTS_AUDIO_PATH_STRING 才会生效 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_ENABLE_DUMP] + forKey:SE_PARAMS_KEY_TTS_ENABLE_DUMP_BOOL]; + // TTS 音频文件保存目录,必须在合成之前创建好且 APP 具有访问权限,保存的音频文件名格式为 tts_{reqid}.wav, {reqid} 是本次合成的请求 id + // PARAMS_KEY_TTS_ENABLE_DUMP_BOOL 配置为 true 的音频时为【必需配置】,否则为【可选配置】 + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_TTS_AUDIO_PATH_STRING]; + + //【可选配置】合成出的音频的采样率,默认为 24000 + [self.curEngine setIntParam:[self.settings getInt:SETTING_TTS_SAMPLE_RATE] forKey:SE_PARAMS_KEY_TTS_SAMPLE_RATE_INT]; + //【可选配置】打断播放时使用多长时间淡出停止,单位:毫秒。默认值 0 表示不淡出 + [self.curEngine setIntParam:[self.settings getInt:SETTING_AUDIO_FADEOUT_DURATION] forKey:SE_PARAMS_KEY_AUDIO_FADEOUT_DURATION_INT]; + //【可选配置】是否禁止创建播放器对象,不使用 SDK 内置播放器时可开启,默认为 false. 开启后将 SE_PARAMS_KEY_TTS_ENABLE_PLAYER_BOOL 设置为 true 不起作用。 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_PREVENT_PLAYER_CREATION] forKey:SE_PARAMS_KEY_PREVENT_PLAYER_CREATION_BOOL]; + + // ------------------------ 在线合成相关配置 ----------------------- + + NSString* appid = [self.settings getString:SETTING_APPID]; + self.ttsAppId = appid.length > 0 ? appid : SDEF_APPID; + //【必需配置】在线合成鉴权相关:Appid + [self.curEngine setStringParam:self.ttsAppId forKey:SE_PARAMS_KEY_APP_ID_STRING]; + + NSString* token = [self.settings getString:SETTING_TOKEN]; + NSString* ttsAppToken = token.length > 0 ? token : SDEF_TOKEN; + //【必需配置】在线合成鉴权相关:Token + [self.curEngine setStringParam:ttsAppToken forKey:SE_PARAMS_KEY_APP_TOKEN_STRING]; + + //【必需配置】语音合成服务域名 + NSString *address = [self.settings getString:SETTING_ADDRESS]; + NSString *ttsAddress = address.length > 0 ? address : SDEF_DEFAULT_ADDRESS; + [self.curEngine setStringParam:ttsAddress forKey:SE_PARAMS_KEY_TTS_ADDRESS_STRING]; + + //【必需配置】语音合成服务Uri + NSString *uri = [self.settings getString:SETTING_URI]; + NSString *ttsUri = uri.length > 0 ? uri : SDEF_TTS_DEFAULT_URI; + [self.curEngine setStringParam:ttsUri forKey:SE_PARAMS_KEY_TTS_URI_STRING]; + + // 【可选配置】是否允许在 websocket 建连失败时自动重连 + [self.curEngine setBoolParam:![self.settings getBool:SETTING_DISABLE_WS_RECONNECT] forKey:SE_PARAMS_KEY_ENABLE_WS_RECONNECT_BOOL]; + + //【可选配置】在线合成下发的 opus-ogg 音频的压缩倍率 + [self.curEngine setIntParam:10 forKey:SE_PARAMS_KEY_TTS_COMPRESSION_RATE_INT]; + + + // ------------------------ 离线合成相关配置 ----------------------- + + if ([self getTtsWorkMode] != SETtsWorkModeOnline && [self getTtsWorkMode] != SETtsWorkModeFile) { + NSString* resourcePath = @""; + if ([[self.settings getOptionsValue:SETTING_TTS_OFFLINE_RESOURCE_FORMAT] isEqual: @"SingleVoice"]) { + resourcePath = [[SpeechResourceManager shareInstance] getModelPath]; + } else if ([[self.settings getOptionsValue:SETTING_TTS_OFFLINE_RESOURCE_FORMAT] isEqual: @"MultipleVoice"]) { + NSString *model_name = [self.settings getString:SETTING_TTS_MODEL_NAME]; + resourcePath = [[SpeechResourceManager shareInstance] getModelPath:model_name]; + } + NSLog(@"TTS resource root path: %@", resourcePath); + //【必需配置】离线合成所需资源存放路径 + [self.curEngine setStringParam:resourcePath forKey:SE_PARAMS_KEY_TTS_OFF_RESOURCE_PATH_STRING]; + } + + //【必需配置】离线合成鉴权相关:证书文件存放路径 + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_LICENSE_DIRECTORY_STRING]; + NSString* authenticationType = [self getAuthenticationType]; + //【必需配置】Authenticate Type + [self.curEngine setStringParam:authenticationType forKey:SE_PARAMS_KEY_AUTHENTICATE_TYPE_STRING]; + if ([authenticationType isEqualToString:SE_AUTHENTICATE_TYPE_PRE_BIND]) { + // 按包名授权,获取到授权的 APP 可以不限次数、不限设备数的使用离线合成 + NSString *licenseName = [self.settings getString:SETTING_LICENSE_NAME]; + NSString *licenseBusiId = [self.settings getString:SETTING_LICENSE_BUSI_ID]; + // 证书名和业务 ID, 离线合成鉴权相关,使用火山提供的证书下发服务时为【必需配置】, 否则为【无需配置】 + // 证书名,用于下载按报名授权的证书文件 + [self.curEngine setStringParam:licenseName forKey:SE_PARAMS_KEY_LICENSE_NAME_STRING]; + // 业务 ID, 用于下载按报名授权的证书文件 + [self.curEngine setStringParam:licenseBusiId forKey:SE_PARAMS_KEY_LICENSE_BUSI_ID_STRING]; + } else if ([authenticationType isEqualToString:SE_AUTHENTICATE_TYPE_LATE_BIND]) { + // 按装机量授权,不限制 APP 的包名和使用次数,但是限制使用离线合成的设备数量 + //【必需配置】离线合成鉴权相关:Authenticate Address + [self.curEngine setStringParam:SDEF_AUTHENTICATE_ADDRESS forKey:SE_PARAMS_KEY_AUTHENTICATE_ADDRESS_STRING]; + //【必需配置】离线合成鉴权相关:Authenticate Uri + [self.curEngine setStringParam:SDEF_AUTHENTICATE_URI forKey:SE_PARAMS_KEY_AUTHENTICATE_URI_STRING]; + NSString* curBusinessKey = [self.settings getString:SETTING_BUSINESS_KEY]; + NSString* curAuthenticateSecret = [self.settings getString:SETTING_AUTHENTICATE_SECRET]; + //【必需配置】离线合成鉴权相关:Business Key + [self.curEngine setStringParam:curBusinessKey forKey:SE_PARAMS_KEY_BUSINESS_KEY_STRING]; + //【必需配置】离线合成鉴权相关:Authenticate Secret + [self.curEngine setStringParam:curAuthenticateSecret forKey:SE_PARAMS_KEY_AUTHENTICATE_SECRET_STRING]; + } + + // ------------------------ 在离线切换相关配置 ----------------------- + if ([self getTtsWorkMode] == SETtsWorkModeAlternate) { + // 断点续播功能在断点处会发生由在线合成音频切换到离线合成音频,为了提升用户体验,SDK 支持 + // 淡出地停止播放在线音频然后再淡入地开始播放离线音频,下面两个参数可以控制淡出淡入的长度 + + //【可选配置】断点续播专用,切换到离线合成时淡入的音频长度,单位:毫秒 + [self.curEngine setIntParam:30 forKey:SE_PARAMS_KEY_TTS_FADEIN_DURATION_INT]; + //【可选配置】断点续播专用,在线合成停止播放时淡出的音频长度,单位:毫秒 + [self.curEngine setIntParam:30 forKey:SE_PARAMS_KEY_TTS_FADEOUT_DURATION_INT]; + } +} + +-(void)configStartTtsParams { + //【必需配置】TTS 使用场景 + [self.curEngine setStringParam:SE_TTS_SCENARIO_TYPE_NORMAL forKey:SE_PARAMS_KEY_TTS_SCENARIO_STRING]; + + NSString* curText = self.referTextView.text; + if (curText.length > 0) { + self.ttsText = curText; + } else { + self.ttsText = @"愿中国青年都摆脱冷气,只是向上走,不必听自暴自弃者流的话。能做事的做事,能发声的发声。有一分热,发一分光。就令萤火一般,也可以在黑暗里发一点光,不必等候炬火。此后如竟没有炬火:我便是唯一的光。"; + } + + //【必需配置】需合成的文本,不可超过 80 字 + [self.curEngine setStringParam:self.ttsText forKey:SE_PARAMS_KEY_TTS_TEXT_STRING]; + //【可选配置】需合成的文本的类型,支持直接传文本(TTS_TEXT_TYPE_PLAIN)和传 SSML 形式(TTS_TEXT_TYPE_SSML)的文本 + [self.curEngine setStringParam:[self getTtsTextType] forKey:SE_PARAMS_KEY_TTS_TEXT_TYPE_STRING]; + //【可选配置】用于控制 TTS 音频的语速,支持的配置范围参考火山官网 语音技术/语音合成/离在线语音合成SDK/参数说明 文档 + [self.curEngine setDoubleParam:[self.settings getDouble:SETTING_TTS_SPEAK_SPEED] forKey:SE_PARAMS_KEY_TTS_SPEED_RATIO_DOUBLE]; + //【可选配置】用于控制 TTS 音频的音量,支持的配置范围参考火山官网 语音技术/语音合成/离在线语音合成SDK/参数说明 文档 + [self.curEngine setDoubleParam:[self.settings getDouble:SETTING_TTS_AUDIO_VOLUME] forKey:SE_PARAMS_KEY_TTS_VOLUME_RATIO_DOUBLE]; + //【可选配置】用于控制 TTS 音频的音高,支持的配置范围参考火山官网 语音技术/语音合成/离在线语音合成SDK/参数说明 文档 + [self.curEngine setDoubleParam:[self.settings getDouble:SETTING_TTS_AUDIO_PITCH] forKey:SE_PARAMS_KEY_TTS_PITCH_RATIO_DOUBLE]; + //【可选配置】是否在文本的每句结尾处添加静音段,单位:毫秒,默认为 0ms + [self.curEngine setIntParam:[self.settings getInt:SETTING_TTS_SILENCE_DURATION] forKey:SE_PARAMS_KEY_TTS_SILENCE_DURATION_INT]; + + //【可选配置】是否使用 SDK 内置播放器播放合成出的音频,默认为 true + [self.curEngine setBoolParam:![self.settings getBool:SETTING_PREVENT_PLAYER_CREATION] && [self.settings getBool:SETTING_TTS_ENABLE_PLAYER] + forKey:SE_PARAMS_KEY_TTS_ENABLE_PLAYER_BOOL]; + //【可选配置】是否令 SDK 通过回调返回合成的音频数据,默认不返回。 + // 开启后,SDK 会流式返回音频,收到 SETtsAudioData 回调表示当次合成所有的音频已经全部返回 + [self.curEngine setIntParam:[self.settings getBool:SETTING_TTS_ENABLE_DATA_CALLBACK] ? SETtsDataCallbackModeAll : SETtsDataCallbackModeNone forKey:SE_PARAMS_KEY_TTS_DATA_CALLBACK_MODE_INT]; + + // SDK 支持使用传入的 reqid 作为合成的唯一标识 + NSString* ttsReqId = [self.settings getString:SETTING_TTS_REQUEST_ID]; + if (ttsReqId.length > 0) { + NSLog(@"Tts req id: %@", ttsReqId); + //【可选配置】唯一标识一次合成的 reqid, 不传则自动生成并伴随 MESSAGE_TYPE_TTS_SYNTHESIS_BEGIN 返回 + [self.curEngine setStringParam:ttsReqId forKey:SE_PARAMS_KEY_TTS_REQUEST_ID_STRING]; + } + + // ------------------------ 在线合成相关配置 ----------------------- + + //【必需配置】语音合成服务所用集群 + NSString *cluster = [self.settings getString:SETTING_CLUSTER]; + [self.curEngine setStringParam:cluster forKey:SE_PARAMS_KEY_TTS_CLUSTER_STRING]; + + NSString *voiceOnline = [self.settings getString:SETTING_ONLINE_VOICE]; + if (voiceOnline.length <= 0) { + voiceOnline = [self.settings getOptionsValue:SETTING_ONLINE_VOICE]; + } + self.ttsVoiceOnline = voiceOnline; + //【必需配置】在线合成使用的发音人代号 + [self.curEngine setStringParam:self.ttsVoiceOnline forKey:SE_PARAMS_KEY_TTS_VOICE_ONLINE_STRING]; + NSString *voiceTypeOnline = [self.settings getString:SETTING_ONLINE_VOICE_TYPE]; + if (voiceTypeOnline.length <= 0) { + voiceTypeOnline = [self.settings getOptionsValue:SETTING_ONLINE_VOICE_TYPE]; + } + self.ttsVoiceTypeOnline = voiceTypeOnline; + //【必需配置】在线合成使用的音色代号 + [self.curEngine setStringParam:self.ttsVoiceTypeOnline forKey:SE_PARAMS_KEY_TTS_VOICE_TYPE_ONLINE_STRING]; + + //【可选配置】是否打开在线合成的服务端缓存,默认关闭 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_ENABLE_CACHE] forKey:SE_PARAMS_KEY_TTS_ENABLE_CACHE_BOOL]; + //【可选配置】指定在线合成的语种,默认为空,即不指定 + [self.curEngine setStringParam:[self.settings getString:SETTING_TTS_ONLINE_LANGUAGE] forKey:SE_PARAMS_KEY_TTS_LANGUAGE_ONLINE_STRING]; + //【可选配置】是否启用在线合成的情感预测功能 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_WITH_INTENT] forKey:SE_PARAMS_KEY_TTS_WITH_INTENT_BOOL]; + //【可选配置】指定在线合成的情感,例如 happy, sad 等 + [self.curEngine setStringParam:[self.settings getString:SETTING_TTS_EMOTION] forKey:SE_PARAMS_KEY_TTS_EMOTION_STRING]; + //【可选配置】需要返回详细的播放进度或需要启用断点续播功能时应配置为 1, 否则配置为 0 或不配置 + [self.curEngine setIntParam:[self.settings getBool:SETTING_TTS_ENABLE_RESUME_FROM_BREAKPOINT] forKey:SE_PARAMS_KEY_TTS_WITH_FRONTEND_INT]; + //【可选配置】使用复刻音色 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_USE_VOICECLONE_VOICE] forKey:SE_PARAMS_KEY_TTS_USE_VOICECLONE_BOOL]; + //【可选配置】在开启前述使用复刻音色的开关后,制定复刻音色所用的后端集群 + [self.curEngine setStringParam:[self.settings getString:SETTING_TTS_BACKEND_CLUSTER] forKey:SE_PARAMS_KEY_TTS_BACKEND_CLUSTER_STRING]; + + //【可选配置】在线合成的请求参数,JSON 格式。当服务端新增参数但是 SDK 还未新增对应的配置项时,开发者可自行构造请求参数由此传入 + [self.curEngine setStringParam:[self.settings getString:SETTING_TTS_REQUEST_PARAMS] forKey:SE_PARAMS_KEY_TTS_REQ_PARAMS_STRING]; + + // ------------------------ 离线合成相关配置 ----------------------- + + NSString *voiceOffline = [self.settings getString:SETTING_OFFLINE_VOICE]; + if (voiceOffline.length <= 0) { + voiceOffline = [self.settings getOptionsValue:SETTING_OFFLINE_VOICE]; + } + self.ttsVoiceOffline = voiceOffline; + //【必需配置】离线合成使用的发音人代号 + [self.curEngine setStringParam:self.ttsVoiceOffline forKey:SE_PARAMS_KEY_TTS_VOICE_OFFLINE_STRING]; + NSString *voiceTypeOffline = [self.settings getString:SETTING_OFFLINE_VOICE_TYPE]; + if (voiceTypeOffline.length <= 0) { + voiceTypeOffline = [self.settings getOptionsValue:SETTING_OFFLINE_VOICE_TYPE]; + } + self.ttsVoiceTypeOffline = voiceTypeOffline; + //【必需配置】离线合成使用的音色代号 + [self.curEngine setStringParam:self.ttsVoiceTypeOffline forKey:SE_PARAMS_KEY_TTS_VOICE_TYPE_OFFLINE_STRING]; + + //【可选配置】是否降低离线合成的 CPU 利用率,默认关闭 + // 打开该配置会使离线合成的实时率变大,仅当必要(例如为避免系统主动杀死CPU占用持续过高的进程)时才应开启 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_LIMIT_CPU_USAGE] forKey:SE_PARAMS_KEY_TTS_LIMIT_CPU_USAGE_BOOL]; +} + +- (void)initEngine { + NSLog(@"获取设备ID,调试使用"); + AppDelegate *appDelegate = [ViewController getAppDelegate]; + if (appDelegate == nil) { + appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; + } + [ViewController setAppDelegate:appDelegate]; + self.deviceID = appDelegate.deviceID; + NSLog(@"获取设备ID成功: %@", self.deviceID); + + + NSLog(@"创建引擎"); + if (self.curEngine == nil) { + self.curEngine = [[SpeechEngine alloc] init]; + if (![self.curEngine createEngineWithDelegate:self]) { + NSLog(@"引擎创建失败."); + return; + } + } + NSLog(@"SDK 版本号: %@", [self.curEngine getVersion]); + + if ([self getTtsWorkMode] == SETtsWorkModeOnline || [self getTtsWorkMode] == SETtsWorkModeFile) { + // 当使用纯在线模式时,不需要下载离线合成所需资源 + [self initEngineInternal]; + } else { + [self.statusTextView setText:@"Waiting for loading model."]; + // 下载离线合成所需资源需要区分多音色资源和单音色资源,下载这两种资源所调用的方法略有不同 + if ([[self.settings getOptionsValue:SETTING_TTS_OFFLINE_RESOURCE_FORMAT] isEqual: @"MultipleVoice"]) { + // 多音色资源是指一个资源文件中包含了多个离线音色,这种资源一般是旧版(V2)离线合成所用资源 + NSLog(@"当前所用资源类别为多音色资源,开始准备多音色资源"); + [self prepareMultipleVoiceResource]; + } else if ([[self.settings getOptionsValue:SETTING_TTS_OFFLINE_RESOURCE_FORMAT] isEqual: @"SingleVoice"]) { + // 单音色资源是指一个资源文件仅包含一个离线音色,新版(V4 及以上)离线合成用的就是单音色资源 + NSLog(@"当前所用资源类别为单音色资源,开始准备单音色资源"); + [self prepareSingleVoiceResource]; + } + } +} + +- (void)prepareMultipleVoiceResource { + // 因为多音色资源的一个文件包含了多个音色,导致资源的名字和音色的名字无法一一对应 + // 所以下载资源需要显式指定资源名字 + NSString *model_name = [self.settings getString:SETTING_TTS_MODEL_NAME]; + SpeechResourceManager *speechResourceManager = [SpeechResourceManager shareInstance]; + NSLog(@"检查本地是否存在可用模型"); + if (![speechResourceManager checkModelExist:model_name]) { + NSLog(@"本地没有模型,开始下载"); + [self fetchMultipleVoiceResource:model_name]; + } else { + NSLog(@"模型存在,检查是否需要更新模型"); + [speechResourceManager checkModelVersion:model_name completion:^(SEResourceStatus status, BOOL needUpdate, NSData *data) { + if (status != kSERSuccess || needUpdate == NO) { + NSLog(@"无需更新,直接使用本地已有模型。"); + [self initEngineInternal]; + } else { + NSLog(@"存在更新,开始下载模型"); + [self fetchMultipleVoiceResource:model_name]; + } + }]; + } +} + +- (void)fetchMultipleVoiceResource:(NSString*)model_name { + NSLog(@"需要下载的模型名为 %@", model_name); + SpeechResourceManager *speechResourceManager = [SpeechResourceManager shareInstance]; + [speechResourceManager fetchModelByName:model_name completion:^(SEResourceStatus status, NSData* data) { + if (status == kSERSuccess) { + NSLog(@"下载成功"); + [self initEngineInternal]; + } else { + NSLog(@"下载失败,错误码: %d", status); + [self speechEngineInitFailed:kSERDownloadFailed]; + } + }]; +} + +- (void)prepareSingleVoiceResource { + SpeechResourceManager *speechResourceManager = [SpeechResourceManager shareInstance]; + NSString* offlineLanguage = [self.settings getString:SETTING_TTS_OFFLINE_LANGUAGE]; + if (offlineLanguage.length <= 0) { + offlineLanguage = SDEF_TTS_DEFAULT_OFFLINE_LANGUAGE; + } + NSArray* ttsLanguageArray = @[offlineLanguage]; + NSLog(@"需要下载的离线合成语种资源有: %@", ttsLanguageArray); + [speechResourceManager setTtsLanguage:ttsLanguageArray]; + NSArray* needDownloadVoiceType = (NSArray *)SDEF_TTS_DEFAULT_DOWNLOAD_OFFLINE_VOICES(); + NSArray* voiceTypeArray = [self.settings getOptions:SETTING_OFFLINE_VOICE_TYPE].optionsArray; + if (voiceTypeArray != nil && voiceTypeArray.count > 0) { + needDownloadVoiceType = voiceTypeArray; + } + NSLog(@"需要下载的离线合成音色资源有: %@", needDownloadVoiceType); + [speechResourceManager setTtsVoiceType:needDownloadVoiceType]; + + NSLog(@"检查本地是否存在可用模型"); + if ([speechResourceManager checkModelExist]) { + NSLog(@"本地没有模型,开始下载"); + [self fetchSingleVoiceResource]; + } else { + NSLog(@"模型存在,检查是否需要更新模型"); + [speechResourceManager checkModelVersion:^(SEResourceStatus status, BOOL needUpdate, NSData *data) { + if (status != kSERSuccess || needUpdate == NO) { + NSLog(@"无需更新,直接使用本地已有模型。"); + [self initEngineInternal]; + } else { + NSLog(@"存在更新,开始下载模型"); + [self fetchSingleVoiceResource]; + } + }]; + } +} + +- (void)fetchSingleVoiceResource { + SpeechResourceManager *speechResourceManager = [SpeechResourceManager shareInstance]; + [speechResourceManager fetchModel:^(SEResourceStatus status, NSData* data) { + if (status == kSERSuccess) { + NSLog(@"下载成功"); + [self initEngineInternal]; + } else { + NSLog(@"下载失败,错误码: %d", status); + [self speechEngineInitFailed:kSERDownloadFailed]; + } + }]; +} + +- (void)initEngineInternal { + NSLog(@"配置初始化参数"); + [self configInitParams]; + + NSLog(@"引擎初始化"); + SEEngineErrorCode ret = [self.curEngine initEngine]; + self.engineInited = (ret == SENoError); + if (self.engineInited) { + NSLog(@"初始化成功"); + [self speechEngineInitSucceeded]; + } else { + NSLog(@"初始化失败,返回值: %d", ret); + [self speechEngineInitFailed:ret]; + } +} + +- (void)uninitEngine { + if (self.curEngine != nil) { + NSLog(@"引擎析构"); + [self.curEngine destroyEngine]; + self.curEngine = nil; + NSLog(@"引擎析构完成"); + } +} + +#pragma mark - UI Actions + +- (IBAction)switchEngine:(id)sender { + if (self.engineStarted) { + [self.statusTextView setText:@"Engine is busy, stop it first!"]; + return; + } + [self clearResult:nil]; + self.startEngineButton.enabled = FALSE; + self.pauseResumeButton.enabled = FALSE; + if (self.engineInited) { + self.referTextView.editable = FALSE; + [self uninitEngine]; + self.engineInited = FALSE; + self.connectionCreated = FALSE; + [self.statusTextView setText:@"Waiting for init."]; + self.engineSwitchButton.enabled = TRUE; + [self.engineSwitchButton setTitle:@"Init Engine" forState:UIControlStateNormal]; + + self.stopEngineButton.enabled = FALSE; + self.createConnectionButton.enabled = FALSE; + } else { + self.referTextView.editable = TRUE; + [self initEngine]; + } +} + +- (IBAction)createConnection:(id)sender { + if (self.connectionCreated) { + NSLog(@"Connection is created."); + return; + } + + // SEDirectiveCreateConnection 指令,可减小在线合成的端到端播放延时,主要应用在能够提前预知要使用语音合成的情况下,例如语音交互场景 + // SEDirectiveCreateConnection 指令是一个同步指令,调用返回之后可以根据返回值判断连接是否建立成功 + // 如果不使用 SEDirectiveCreateConnection 指令,建连实际发生在调用 DIRECTIVE_START_ENGINE 后 + NSLog(@"触发提前建连"); + NSLog(@"Directive: SEDirectiveCreateConnection"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveCreateConnection]; + if(ret != SENoError) { + NSString* error_message = [NSString stringWithFormat:@"在线合成提前建连失败: %d", ret]; + NSLog(@"%@", error_message); + [self createConnectionFailed:error_message]; + } else { + NSString* message = [NSString stringWithFormat:@"在线合成提前建连成功: %d", ret]; + NSLog(@"%@", message); + [self createConnectionSucceeded:message]; + } +} + +- (IBAction)startEngineBtnClicked:(id)sender { + NSLog(@"Start engine, current status: %d", self.engineStarted); + if (!self.engineStarted) { + [self clearResult:nil]; + self.engineErrorOccurred = FALSE; + + // Directive:启动引擎前调用SYNC_STOP指令,保证前一次请求结束。 + NSLog(@"关闭引擎(同步)"); + NSLog(@"Directive: SEDirectiveSyncStopEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveSyncStopEngine]; + if (ret != SENoError) { + NSLog(@"Send directive syncstop failed: %d", ret); + } else { + [self configStartTtsParams]; + NSLog(@"启动引擎."); + NSLog(@"Directive: SEDirectiveStartEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (SENoError != ret) { + NSString* message = [NSString stringWithFormat:@"发送启动引擎指令失败: %d", ret]; + [self sendStartEngineDirectiveFailed:message]; + } + } + } +} + +- (IBAction)stopEngineBtnClicked:(id)sender { + NSLog(@"关闭引擎"); + NSLog(@"Directive: SEDirectiveStopEngine"); + [self.curEngine sendDirective:SEDirectiveStopEngine]; +} + +- (void) pausePlayback { + NSLog(@"暂停播放"); + NSLog(@"Directive: SEDirectivePausePlayer"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectivePausePlayer]; + if (ret == SENoError) { + self.playerPaused = TRUE; + [self.pauseResumeButton setTitle:@"Resume" forState:UIControlStateNormal]; + } + NSLog(@"Pause playback status: %d", ret); +} + +- (void)resumePlayback { + NSLog(@"继续播放"); + NSLog(@"Directive: SEDirectiveResumePlayer"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveResumePlayer]; + if (ret == SENoError) { + self.playerPaused = FALSE; + [self.pauseResumeButton setTitle:@"Pause" forState:UIControlStateNormal]; + } + NSLog(@"Resume playback status: %d", ret); +} + +- (IBAction)controlPlayingStatus:(id)sender { + NSLog(@"Pause or resume player, current player status: %hhd", self.playerPaused); + if (self.playerPaused) { + [self resumePlayback]; + } else { + [self pausePlayback]; + } +} + +- (IBAction)clearResult:(id)sender { + [self.resultTextView setText:@""]; +} + +#pragma mark - Message Callback + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSLog(@"Message Type: %d.", type); + switch (type) { + case SEEngineStart: + NSLog(@"Callback: 引擎启动成功: data: %@", data); + [self speechEngineStarted]; + break; + case SEEngineStop: + NSLog(@"Callback: 引擎关闭: data: %@", data); + [self speechEngineStopped]; + break; + case SEEngineError: + NSLog(@"Callback: 错误信息: %@", data); + [self speechEngineError:data]; + break; + case SETtsSynthesisBegin: + NSLog(@"Callback: 合成开始: %@", data); + [self speechStartSynthesis:data]; + break; + case SETtsSynthesisEnd: + NSLog(@"Callback: 合成结束: %@", data); + [self speechFinishSynthesis:data]; + break; + case SETtsStartPlaying: + NSLog(@"Callback: 播放开始: %@", data); + [self speechStartPlaying:data]; + break; + case SETtsPlaybackProgress: + NSLog(@"Callback: 播放进度"); + [self updatePlayingProgress:data]; + break; + case SETtsFinishPlaying: + NSLog(@"Callback: 播放结束: %@", data); + [self speechFinishPlaying:data]; + break; + case SETtsAudioData: + NSLog(@"Callback: 音频数据,长度 %lu 字节", (unsigned long)data.length); + [self speechTtsAudioData:data]; + break; + default: + break; + } +} + +- (void)speechEngineInitSucceeded { + dispatch_async(dispatch_get_main_queue(), ^{ + self.engineSwitchButton.enabled = TRUE; + [self.engineSwitchButton setTitle:@"UninitEngine" forState:UIControlStateNormal]; + [self.statusTextView setText:@"Ready"]; + [self.resultTextView setText:[NSString stringWithFormat:@"DeviceID: %@.", self.deviceID]]; + self.referTextView.editable = TRUE; + self.startEngineButton.enabled = TRUE; + self.createConnectionButton.enabled = [self getTtsWorkMode] != SETtsWorkModeOffline; }); +} + +- (void)speechEngineInitFailed:(int)initStatus { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:[[NSString alloc] initWithFormat:@"Failed to init engine, %d!", initStatus]]; + self.engineSwitchButton.enabled = TRUE; + }); +} + +- (void)createConnectionSucceeded:(NSString*)tipText { + dispatch_async(dispatch_get_main_queue(), ^{ + self.createConnectionButton.enabled = FALSE; + [self.resultTextView setText:tipText]; + self.connectionCreated = TRUE; + }); +} + +- (void)createConnectionFailed:(NSString*)tipText { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:tipText]; + self.connectionCreated = FALSE; + }); +} + +- (void)sendStartEngineDirectiveFailed:(NSString*)tipText { + NSLog(@"%@", tipText); + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:tipText]; + self.engineStarted = FALSE; + }); +} + +- (void)speechEngineStarted { + dispatch_async(dispatch_get_main_queue(), ^{ + self.referTextView.editable = FALSE; + self.engineStarted = true; + [self.statusTextView setText:@"Engine Started!"]; + [self.resultTextView setText:self.ttsText]; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = TRUE; + self.createConnectionButton.enabled = FALSE; + }); +} + +- (void)speechEngineStopped { + dispatch_async(dispatch_get_main_queue(), ^{ + self.referTextView.editable = TRUE; + self.engineStarted = FALSE; + self.connectionCreated = FALSE; + [self.statusTextView setText:@"Engine Stopped!"]; + self.startEngineButton.enabled = TRUE; + self.stopEngineButton.enabled = FALSE; + self.createConnectionButton.enabled = [self getTtsWorkMode] != SETtsWorkModeOffline; + [self.pauseResumeButton setTitle:@"Pause" forState:UIControlStateNormal]; + self.pauseResumeButton.enabled = FALSE; + self.playerPaused = FALSE; + }); +} + +- (void)speechEngineError:(NSData *)data { + self.engineErrorOccurred = TRUE; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setTextColor:[UIColor blackColor]]; + [self.resultTextView setText:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]]; + }); +} + +- (void)speechStartSynthesis:(NSData *)data { +} + +- (void)speechFinishSynthesis:(NSData *)data { +} + +- (void)speechStartPlaying:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + self.pauseResumeButton.enabled = TRUE; + }); +} + +- (void)updatePlayingProgress :(NSData *)data { + if (data != nil) { + NSError *error = nil; + id object = [NSJSONSerialization + JSONObjectWithData:data + options:0 + error:&error]; + if(error) { + NSLog(@"Parse data as json error!"); + return ; + } + if([object isKindOfClass:[NSDictionary class]]) { + NSDictionary *results = object; + float percentage = [[results valueForKey:@"progress"] floatValue]; + NSString *reqid = [results valueForKey:@"reqid"]; + NSLog(@"当前播放的文本对应的 reqid: %@,播放进度:%.3f", reqid, percentage); + } + } +} + +- (void)speechFinishPlaying :(NSData *)data { +} + +- (void)speechTtsAudioData:(NSData *)data { +} + +#pragma mark - Helper + +- (NSString*)getTtsTextType { + switch ([self.settings getOptions:SETTING_TTS_TEXT_TYPE].chooseIdx) { + case 0: + return SE_TTS_TEXT_TYPE_PLAIN; + case 1: + return SE_TTS_TEXT_TYPE_SSML; + default: + break; + } + return SE_TTS_TEXT_TYPE_PLAIN;; +} + +- (int)getTtsWorkMode { + switch ([self.settings getOptions:SETTING_TTS_WORK_MODE].chooseIdx) { + case 0: + return SETtsWorkModeOnline; + case 1: + return SETtsWorkModeOffline; + case 2: + return SETtsWorkModeAlternate; + default: + break; + } + return SETtsWorkModeOnline;; +} + +- (NSString*)getAuthenticationType { + switch ([self.settings getOptions:SETTING_AUTHENTICATION_TYPE].chooseIdx) { + case 0: + return SE_AUTHENTICATE_TYPE_PRE_BIND; + case 1: + return SE_AUTHENTICATE_TYPE_LATE_BIND; + default: + break; + } + return SE_AUTHENTICATE_TYPE_PRE_BIND; +} + +- (long)timeDelayFrom:(long)pastTimestamp { + return [[NSDate date] timeIntervalSince1970] * 1000 - pastTimestamp; +} + +#pragma mark - UITextViewDelegate + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if([text isEqualToString:@"\n"]) { + [textView resignFirstResponder]; + return NO; + } + return YES; +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_TTS forKey:@"viewId"]; +} + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/TtsNovelViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/TtsNovelViewController.h new file mode 100644 index 0000000..3e173b1 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/TtsNovelViewController.h @@ -0,0 +1,13 @@ +// +// TtsNovelViewController.h +// SpeechDemo +// +// Created by bytedance on 2020/9/8. +// Copyright © 2020 fengkai.0518. All rights reserved. +// + +#import + +@interface TtsNovelViewController : UIViewController + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/TtsNovelViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/TtsNovelViewController.m new file mode 100644 index 0000000..4ff9f05 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/TtsNovelViewController.m @@ -0,0 +1,1004 @@ +// +// TtsNovelViewController.m +// SpeechDemo +// +// Created by bytedance on 2020/9/8. +// Copyright © 2020 fengkai.0518. All rights reserved. +// + +#import "TtsNovelViewController.h" +#include +#include + +#import + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" +#import "SensitiveDefines.h" + +static int TTS_MAX_RETRY_COUNT = 3; + +@interface TtsNovelViewController () + +@property (weak, nonatomic) IBOutlet UITextView *referTextView; +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; +@property (weak, nonatomic) IBOutlet UIButton *engineSwitchButton; +@property (weak, nonatomic) IBOutlet UIButton *startEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *stopEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *synthesisButton; +@property (weak, nonatomic) IBOutlet UIButton *pauseResumeButton; + +// Device ID: 用于定位线上问题 +@property (nonatomic, strong) NSString *deviceID; +// Debug Path: 用于存放一些 SDK 相关的文件,比如模型、日志等 +@property (strong, nonatomic) NSString *debugPath; + +// SpeechEngine +@property (strong, nonatomic) SpeechEngine *curEngine; + +// Engine State +@property (assign, nonatomic) BOOL engineInited; +@property (assign, nonatomic) BOOL engineStarted; +@property (assign, nonatomic) BOOL engineErrorOccurred; +@property (assign, nonatomic) BOOL playerPaused; + +// Settings +@property (strong, nonatomic) Settings *settings; + +// 一些在线合成的配置 +@property (strong, nonatomic) NSString *ttsAppId; +@property (strong, nonatomic) NSString *ttsVoiceOnline; +@property (strong, nonatomic) NSString *ttsVoiceTypeOnline; + +// 一些离线合成的配置 +@property (strong, nonatomic) NSString *ttsVoiceOffline; +@property (strong, nonatomic) NSString *ttsVoiceTypeOffline; + +// 小说模式相关 +@property (assign, nonatomic) BOOL ttsSynthesisFromPlayer; +@property (assign, nonatomic) int ttsSynthesisIndex; +@property (assign, nonatomic) int ttsPlayingIndex; +@property (assign, nonatomic) double ttsPlayingProgress; +@property (strong, nonatomic) NSMutableArray* ttsSynthesisText; +@property (strong, nonatomic) NSMutableDictionary* ttsSynthesisMap; + +@property (assign, nonatomic) int ttsRetryCount; + + +@end + +@implementation TtsNovelViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_TTS]; + + self.engineSwitchButton.enabled = TRUE; + [self decorateTextView:self.referTextView]; + [self decorateTextView:self.resultTextView]; + [self.referTextView setDelegate:self]; + self.referTextView.editable = TRUE; + self.engineInited = FALSE; + self.engineStarted = FALSE; + self.engineErrorOccurred = FALSE; + self.playerPaused = FALSE; + + // 初始化和小说模式有关的字段 + self.ttsSynthesisFromPlayer = FALSE; + self.ttsSynthesisIndex = 0; + self.ttsPlayingIndex = -1; + self.ttsPlayingProgress = 0.0; + self.ttsSynthesisText = [[NSMutableArray alloc] init]; + self.ttsSynthesisMap = [[NSMutableDictionary alloc]init]; + + self.ttsRetryCount = TTS_MAX_RETRY_COUNT; + + + [self.statusTextView setText:@"Waiting for init."]; + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"当前调试路径 %@", self.debugPath); + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(audioInterruptionHandler:) + name:AVAudioSessionInterruptionNotification + object:nil]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; +} + +- (void)decorateTextView:(UITextView *)textView { + textView.layer.cornerRadius = 5.0f; + textView.layer.borderWidth = .25f; + textView.layer.borderColor = [UIColor grayColor].CGColor; +} + +#pragma mark - Notifications + +-(void)appWillTerminate:(NSNotification*)note { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:AVAudioSessionInterruptionNotification + object:nil]; +} + +- (void)audioInterruptionHandler:(NSNotification*)notification { + AVAudioSessionInterruptionType interruptionType = (AVAudioSessionInterruptionType)[[notification.userInfo objectForKey:AVAudioSessionInterruptionTypeKey] unsignedIntegerValue]; + AVAudioSessionInterruptionOptions intertuptionOptions = [[notification.userInfo objectForKey:AVAudioSessionInterruptionOptionKey] unsignedIntValue]; + NSLog(@"Receive audio interruption notification, type: %lu, options: %lu.", (unsigned long)interruptionType, (unsigned long)intertuptionOptions); + if (interruptionType == AVAudioSessionInterruptionTypeBegan) { + NSLog(@"Audio session interruption began"); + @synchronized (self) { + [self pausePlayback]; + } + } else if (interruptionType == AVAudioSessionInterruptionTypeEnded) { + @synchronized (self) { + NSLog(@"Audio session interruption ended"); + if (intertuptionOptions == AVAudioSessionInterruptionOptionShouldResume) { + AVAudioSession *session = [AVAudioSession sharedInstance]; + AVAudioSessionCategoryOptions cur_options = session.categoryOptions; + // AudioQueueStart() will return AVAudioSessionErrorCodeCannotInterruptOthers if options didn't contains AVAudioSessionCategoryOptionMixWithOthers + if (!(cur_options & AVAudioSessionCategoryOptionMixWithOthers)) { + AVAudioSessionCategoryOptions readyOptions = AVAudioSessionCategoryOptionMixWithOthers | cur_options; + [session setCategory:AVAudioSessionCategoryPlayback withOptions:readyOptions error:nil]; + } + [self resumePlayback]; + + cur_options = session.categoryOptions; + // Remove AVAudioSessionCategoryOptionMixWithOthers, or the playback will not be interrupted any more + if (cur_options & AVAudioSessionCategoryOptionMixWithOthers) { + [session setCategory:AVAudioSessionCategoryPlayback withOptions:((~AVAudioSessionCategoryOptionMixWithOthers) & cur_options) error:nil]; + } + } + } + } +} + +#pragma mark - Config & Init & Uninit Methods + +-(void)configInitParams { + //【必需配置】Engine Name + [self.curEngine setStringParam:SE_TTS_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + + //【必需配置】Work Mode, 可选值如下 + // SETtsWorkModeOnline, 只进行在线合成,不需要配置离线合成相关参数; + // SETtsWorkModeOffline, 只进行离线合成,不需要配置在线合成相关参数; + // SETtsWorkModeAlternate, 先发起在线合成,失败后(网络超时),启动离线合成引擎开始合成; + [self.curEngine setIntParam:[self getTtsWorkMode] forKey:SE_PARAMS_KEY_TTS_WORK_MODE_INT]; + + //【可选配置】Debug & Log + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + [self.curEngine setStringParam:SE_LOG_LEVEL_DEBUG forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + + //【可选配置】User ID(用以辅助定位线上用户问题) + [self.curEngine setStringParam:SDEF_UID forKey:SE_PARAMS_KEY_UID_STRING]; + [self.curEngine setStringParam:self.deviceID forKey:SE_PARAMS_KEY_DEVICE_ID_STRING]; + + //【可选配置】是否将合成出的音频保存到设备上,为 true 时需要正确配置 PARAMS_KEY_TTS_AUDIO_PATH_STRING 才会生效 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_ENABLE_DUMP] + forKey:SE_PARAMS_KEY_TTS_ENABLE_DUMP_BOOL]; + // TTS 音频文件保存目录,必须在合成之前创建好且 APP 具有访问权限,保存的音频文件名格式为 tts_{reqid}.wav, {reqid} 是本次合成的请求 id + // PARAMS_KEY_TTS_ENABLE_DUMP_BOOL 配置为 true 的音频时为【必需配置】,否则为【可选配置】 + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_TTS_AUDIO_PATH_STRING]; + + //【可选配置】合成出的音频的采样率,默认为 24000 + [self.curEngine setIntParam:[self.settings getInt:SETTING_TTS_SAMPLE_RATE] forKey:SE_PARAMS_KEY_TTS_SAMPLE_RATE_INT]; + //【可选配置】打断播放时使用多长时间淡出停止,单位:毫秒。默认值 0 表示不淡出 + [self.curEngine setIntParam:[self.settings getInt:SETTING_AUDIO_FADEOUT_DURATION] forKey:SE_PARAMS_KEY_AUDIO_FADEOUT_DURATION_INT]; + + // ------------------------ 在线合成相关配置 ----------------------- + + NSString* appid = [self.settings getString:SETTING_APPID]; + self.ttsAppId = appid.length > 0 ? appid : SDEF_APPID; + //【必需配置】在线合成鉴权相关:Appid + [self.curEngine setStringParam:self.ttsAppId forKey:SE_PARAMS_KEY_APP_ID_STRING]; + + NSString* token = [self.settings getString:SETTING_TOKEN]; + NSString* ttsAppToken = token.length > 0 ? token : SDEF_TOKEN; + //【必需配置】在线合成鉴权相关:Token + [self.curEngine setStringParam:ttsAppToken forKey:SE_PARAMS_KEY_APP_TOKEN_STRING]; + + //【必需配置】语音合成服务域名 + NSString *address = [self.settings getString:SETTING_ADDRESS]; + NSString *ttsAddress = address.length > 0 ? address : SDEF_DEFAULT_ADDRESS; + [self.curEngine setStringParam:ttsAddress forKey:SE_PARAMS_KEY_TTS_ADDRESS_STRING]; + + //【必需配置】语音合成服务Uri + NSString *uri = [self.settings getString:SETTING_URI]; + NSString *ttsUri = uri.length > 0 ? uri : SDEF_TTS_DEFAULT_URI; + [self.curEngine setStringParam:ttsUri forKey:SE_PARAMS_KEY_TTS_URI_STRING]; + + // 【可选配置】是否允许在 websocket 建连失败时自动重连 + [self.curEngine setBoolParam:![self.settings getBool:SETTING_DISABLE_WS_RECONNECT] forKey:SE_PARAMS_KEY_ENABLE_WS_RECONNECT_BOOL]; + + //【可选配置】在线合成下发的 opus-ogg 音频的压缩倍率 + [self.curEngine setIntParam:10 forKey:SE_PARAMS_KEY_TTS_COMPRESSION_RATE_INT]; + + + // ------------------------ 离线合成相关配置 ----------------------- + + if ([self getTtsWorkMode] != SETtsWorkModeOnline && [self getTtsWorkMode] != SETtsWorkModeFile) { + NSString* resourcePath = @""; + if ([[self.settings getOptionsValue:SETTING_TTS_OFFLINE_RESOURCE_FORMAT] isEqual: @"SingleVoice"]) { + resourcePath = [[SpeechResourceManager shareInstance] getModelPath]; + } else if ([[self.settings getOptionsValue:SETTING_TTS_OFFLINE_RESOURCE_FORMAT] isEqual: @"MultipleVoice"]) { + NSString *model_name = [self.settings getString:SETTING_TTS_MODEL_NAME]; + resourcePath = [[SpeechResourceManager shareInstance] getModelPath:model_name]; + } + NSLog(@"TTS resource root path: %@", resourcePath); + //【必需配置】离线合成所需资源存放路径 + [self.curEngine setStringParam:resourcePath forKey:SE_PARAMS_KEY_TTS_OFF_RESOURCE_PATH_STRING]; + } + + //【必需配置】离线合成鉴权相关:证书文件存放路径 + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_LICENSE_DIRECTORY_STRING]; + NSString* authenticationType = [self getAuthenticationType]; + //【必需配置】Authenticate Type + [self.curEngine setStringParam:authenticationType forKey:SE_PARAMS_KEY_AUTHENTICATE_TYPE_STRING]; + if ([authenticationType isEqualToString:SE_AUTHENTICATE_TYPE_PRE_BIND]) { + // 按包名授权,获取到授权的 APP 可以不限次数、不限设备数的使用离线合成 + NSString *licenseName = [self.settings getString:SETTING_LICENSE_NAME]; + NSString *licenseBusiId = [self.settings getString:SETTING_LICENSE_BUSI_ID]; + // 证书名和业务 ID, 离线合成鉴权相关,使用火山提供的证书下发服务时为【必需配置】, 否则为【无需配置】 + // 证书名,用于下载按报名授权的证书文件 + [self.curEngine setStringParam:licenseName forKey:SE_PARAMS_KEY_LICENSE_NAME_STRING]; + // 业务 ID, 用于下载按报名授权的证书文件 + [self.curEngine setStringParam:licenseBusiId forKey:SE_PARAMS_KEY_LICENSE_BUSI_ID_STRING]; + } else if ([authenticationType isEqualToString:SE_AUTHENTICATE_TYPE_LATE_BIND]) { + // 按装机量授权,不限制 APP 的包名和使用次数,但是限制使用离线合成的设备数量 + //【必需配置】离线合成鉴权相关:Authenticate Address + [self.curEngine setStringParam:SDEF_AUTHENTICATE_ADDRESS forKey:SE_PARAMS_KEY_AUTHENTICATE_ADDRESS_STRING]; + //【必需配置】离线合成鉴权相关:Authenticate Uri + [self.curEngine setStringParam:SDEF_AUTHENTICATE_URI forKey:SE_PARAMS_KEY_AUTHENTICATE_URI_STRING]; + NSString* curBusinessKey = [self.settings getString:SETTING_BUSINESS_KEY]; + NSString* curAuthenticateSecret = [self.settings getString:SETTING_AUTHENTICATE_SECRET]; + //【必需配置】离线合成鉴权相关:Business Key + [self.curEngine setStringParam:curBusinessKey forKey:SE_PARAMS_KEY_BUSINESS_KEY_STRING]; + //【必需配置】离线合成鉴权相关:Authenticate Secret + [self.curEngine setStringParam:curAuthenticateSecret forKey:SE_PARAMS_KEY_AUTHENTICATE_SECRET_STRING]; + } +} + +-(void)configStartTtsParams { + //【必需配置】TTS 使用场景 + [self.curEngine setStringParam:SE_TTS_SCENARIO_TYPE_NOVEL forKey:SE_PARAMS_KEY_TTS_SCENARIO_STRING]; + + // 准备待合成的小说文本 + if(![self prepareNovelText]) { + char fake_error_info[] = "{err_code:3006, err_msg:\"Invalid input text.\"}"; + [self speechEngineError:[NSData dataWithBytes:fake_error_info length:sizeof(fake_error_info)]]; + return; + } + + //【可选配置】是否使用 SDK 内置播放器播放合成出的音频,默认为 true + [self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_ENABLE_PLAYER] + forKey:SE_PARAMS_KEY_TTS_ENABLE_PLAYER_BOOL]; + //【可选配置】是否令 SDK 通过回调返回合成的音频数据,默认不返回。 + // 开启后,SDK 会流式返回音频,收到 SETtsAudioData 回调表示当次合成所有的音频已经全部返回 + [self.curEngine setIntParam:[self.settings getBool:SETTING_TTS_ENABLE_DATA_CALLBACK] ? SETtsDataCallbackModeAll : SETtsDataCallbackModeNone forKey:SE_PARAMS_KEY_TTS_DATA_CALLBACK_MODE_INT]; +} + +- (void)configSynthesisParams { + NSString* text = self.ttsSynthesisText[self.ttsSynthesisIndex]; + NSLog(@"Synthesis: %d, text: %@", self.ttsSynthesisIndex, text); + //【必需配置】需合成的文本,不可超过 80 字 + [self.curEngine setStringParam:text forKey:SE_PARAMS_KEY_TTS_TEXT_STRING]; + //【可选配置】需合成的文本的类型,支持直接传文本(TTS_TEXT_TYPE_PLAIN)和传 SSML 形式(TTS_TEXT_TYPE_SSML)的文本 + [self.curEngine setStringParam:[self getTtsTextType] forKey:SE_PARAMS_KEY_TTS_TEXT_TYPE_STRING]; + //【可选配置】用于控制 TTS 音频的语速,支持的配置范围参考火山官网 语音技术/语音合成/离在线语音合成SDK/参数说明 文档 + [self.curEngine setDoubleParam:[self.settings getDouble:SETTING_TTS_SPEAK_SPEED] forKey:SE_PARAMS_KEY_TTS_SPEED_RATIO_DOUBLE]; + //【可选配置】用于控制 TTS 音频的音量,支持的配置范围参考火山官网 语音技术/语音合成/离在线语音合成SDK/参数说明 文档 + [self.curEngine setDoubleParam:[self.settings getDouble:SETTING_TTS_AUDIO_VOLUME] forKey:SE_PARAMS_KEY_TTS_VOLUME_RATIO_DOUBLE]; + //【可选配置】用于控制 TTS 音频的音高,支持的配置范围参考火山官网 语音技术/语音合成/离在线语音合成SDK/参数说明 文档 + [self.curEngine setDoubleParam:[self.settings getDouble:SETTING_TTS_AUDIO_PITCH] forKey:SE_PARAMS_KEY_TTS_PITCH_RATIO_DOUBLE]; + //【可选配置】是否在文本的每句结尾处添加静音段,单位:毫秒,默认为 0ms + [self.curEngine setIntParam:[self.settings getInt:SETTING_TTS_SILENCE_DURATION] forKey:SE_PARAMS_KEY_TTS_SILENCE_DURATION_INT]; + + // ------------------------ 在线合成相关配置 ----------------------- + + //【必需配置】语音合成服务所用集群 + NSString *cluster = [self.settings getString:SETTING_CLUSTER]; + [self.curEngine setStringParam:cluster forKey:SE_PARAMS_KEY_TTS_CLUSTER_STRING]; + + NSString *voiceOnline = [self.settings getString:SETTING_ONLINE_VOICE]; + if (voiceOnline.length <= 0) { + voiceOnline = [self.settings getOptionsValue:SETTING_ONLINE_VOICE]; + } + self.ttsVoiceOnline = voiceOnline; + //【必需配置】在线合成使用的发音人代号 + [self.curEngine setStringParam:self.ttsVoiceOnline forKey:SE_PARAMS_KEY_TTS_VOICE_ONLINE_STRING]; + NSString *voiceTypeOnline = [self.settings getString:SETTING_ONLINE_VOICE_TYPE]; + if (voiceTypeOnline.length <= 0) { + voiceTypeOnline = [self.settings getOptionsValue:SETTING_ONLINE_VOICE_TYPE]; + } + self.ttsVoiceTypeOnline = voiceTypeOnline; + //【必需配置】在线合成使用的音色代号 + [self.curEngine setStringParam:self.ttsVoiceTypeOnline forKey:SE_PARAMS_KEY_TTS_VOICE_TYPE_ONLINE_STRING]; + //【可选配置】是否打开在线合成的服务端缓存,默认关闭 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_ENABLE_CACHE] forKey:SE_PARAMS_KEY_TTS_ENABLE_CACHE_BOOL]; + //【可选配置】指定在线合成的语种,默认为空,即不指定 + [self.curEngine setStringParam:[self.settings getString:SETTING_TTS_ONLINE_LANGUAGE] forKey:SE_PARAMS_KEY_TTS_LANGUAGE_ONLINE_STRING]; + //【可选配置】是否启用在线合成的情感预测功能 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_WITH_INTENT] forKey:SE_PARAMS_KEY_TTS_WITH_INTENT_BOOL]; + //【可选配置】指定在线合成的情感,例如 happy, sad 等 + [self.curEngine setStringParam:[self.settings getString:SETTING_TTS_EMOTION] forKey:SE_PARAMS_KEY_TTS_EMOTION_STRING]; + //【可选配置】需要返回详细的播放进度或需要启用断点续播功能时应配置为 1, 否则配置为 0 或不配置 + [self.curEngine setIntParam:1 forKey:SE_PARAMS_KEY_TTS_WITH_FRONTEND_INT]; + //【可选配置】需要返回字粒度的播放进度时应配置为 simple, 同时要求 PARAMS_KEY_TTS_WITH_FRONTEND_INT 也配置为 1; 默认为空 + [self.curEngine setStringParam:[self.settings getBool:SETTING_TTS_ENABLE_WORD_LEVEL_PROGRESS_UPDATE] ? @"simple" : @"" forKey:SE_PARAMS_KEY_TTS_FRONTEND_TYPE_STRING]; + //【可选配置】使用复刻音色 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_USE_VOICECLONE_VOICE] forKey:SE_PARAMS_KEY_TTS_USE_VOICECLONE_BOOL]; + //【可选配置】在开启前述使用复刻音色的开关后,制定复刻音色所用的后端集群 + [self.curEngine setStringParam:[self.settings getString:SETTING_TTS_BACKEND_CLUSTER] forKey:SE_PARAMS_KEY_TTS_BACKEND_CLUSTER_STRING]; + + //【可选配置】在线合成的请求参数,JSON 格式。当服务端新增参数但是 SDK 还未新增对应的配置项时,开发者可自行构造请求参数由此传入 + [self.curEngine setStringParam:[self.settings getString:SETTING_TTS_REQUEST_PARAMS] forKey:SE_PARAMS_KEY_TTS_REQ_PARAMS_STRING]; + + // ------------------------ 离线合成相关配置 ----------------------- + + NSString *voiceOffline = [self.settings getString:SETTING_OFFLINE_VOICE]; + if (voiceOffline.length <= 0) { + voiceOffline = [self.settings getOptionsValue:SETTING_OFFLINE_VOICE]; + } + self.ttsVoiceOffline = voiceOffline; + //【必需配置】离线合成使用的发音人代号 + [self.curEngine setStringParam:self.ttsVoiceOffline forKey:SE_PARAMS_KEY_TTS_VOICE_OFFLINE_STRING]; + NSString *voiceTypeOffline = [self.settings getString:SETTING_OFFLINE_VOICE_TYPE]; + if (voiceTypeOffline.length <= 0) { + voiceTypeOffline = [self.settings getOptionsValue:SETTING_OFFLINE_VOICE_TYPE]; + } + self.ttsVoiceTypeOffline = voiceTypeOffline; + //【必需配置】离线合成使用的音色代号 + [self.curEngine setStringParam:self.ttsVoiceTypeOffline forKey:SE_PARAMS_KEY_TTS_VOICE_TYPE_OFFLINE_STRING]; + + //【可选配置】是否降低离线合成的 CPU 利用率,默认关闭 + // 打开该配置会使离线合成的实时率变大,仅当必要(例如为避免系统主动杀死CPU占用持续过高的进程)时才应开启 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_LIMIT_CPU_USAGE] forKey:SE_PARAMS_KEY_TTS_LIMIT_CPU_USAGE_BOOL]; +} + +- (void)initEngine { + NSLog(@"获取设备ID,调试使用"); + AppDelegate *appDelegate = [ViewController getAppDelegate]; + if (appDelegate == nil) { + appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; + } + [ViewController setAppDelegate:appDelegate]; + self.deviceID = appDelegate.deviceID; + NSLog(@"获取设备ID成功: %@", self.deviceID); + + + NSLog(@"创建引擎"); + if (self.curEngine == nil) { + self.curEngine = [[SpeechEngine alloc] init]; + if (![self.curEngine createEngineWithDelegate:self]) { + NSLog(@"引擎创建失败."); + return; + } + } + NSLog(@"SDK 版本号: %@", [self.curEngine getVersion]); + + if ([self getTtsWorkMode] == SETtsWorkModeOnline || [self getTtsWorkMode] == SETtsWorkModeFile) { + // 当使用纯在线模式时,不需要下载离线合成所需资源 + [self initEngineInternal]; + } else { + [self.statusTextView setText:@"Waiting for loading model."]; + // 下载离线合成所需资源需要区分多音色资源和单音色资源,下载这两种资源所调用的方法略有不同 + if ([[self.settings getOptionsValue:SETTING_TTS_OFFLINE_RESOURCE_FORMAT] isEqual: @"MultipleVoice"]) { + // 多音色资源是指一个资源文件中包含了多个离线音色,这种资源一般是旧版(V2)离线合成所用资源 + NSLog(@"当前所用资源类别为多音色资源,开始准备多音色资源"); + [self prepareMultipleVoiceResource]; + } else if ([[self.settings getOptionsValue:SETTING_TTS_OFFLINE_RESOURCE_FORMAT] isEqual: @"SingleVoice"]) { + // 单音色资源是指一个资源文件仅包含一个离线音色,新版(V4 及以上)离线合成用的就是单音色资源 + NSLog(@"当前所用资源类别为单音色资源,开始准备单音色资源"); + [self prepareSingleVoiceResource]; + } + } +} + +- (void)prepareMultipleVoiceResource { + // 因为多音色资源的一个文件包含了多个音色,导致资源的名字和音色的名字无法一一对应 + // 所以下载资源需要显式指定资源名字 + NSString *model_name = [self.settings getString:SETTING_TTS_MODEL_NAME]; + SpeechResourceManager *speechResourceManager = [SpeechResourceManager shareInstance]; + NSLog(@"检查本地是否存在可用模型"); + if (![speechResourceManager checkModelExist:model_name]) { + NSLog(@"本地没有模型,开始下载"); + [self fetchMultipleVoiceResource:model_name]; + } else { + NSLog(@"模型存在,检查是否需要更新模型"); + [speechResourceManager checkModelVersion:model_name completion:^(SEResourceStatus status, BOOL needUpdate, NSData *data) { + if (status != kSERSuccess || needUpdate == NO) { + NSLog(@"无需更新,直接使用本地已有模型。"); + [self initEngineInternal]; + } else { + NSLog(@"存在更新,开始下载模型"); + [self fetchMultipleVoiceResource:model_name]; + } + }]; + } +} + +- (void) fetchMultipleVoiceResource:(NSString*)model_name { + NSLog(@"需要下载的模型名为 %@", model_name); + SpeechResourceManager *speechResourceManager = [SpeechResourceManager shareInstance]; + [speechResourceManager fetchModelByName:model_name completion:^(SEResourceStatus status, NSData* data) { + if (status == kSERSuccess) { + NSLog(@"下载成功"); + [self initEngineInternal]; + } else { + NSLog(@"下载失败,错误码: %d", status); + [self speechEngineInitFailed:kSERDownloadFailed]; + } + }]; +} + +- (void) prepareSingleVoiceResource { + SpeechResourceManager *speechResourceManager = [SpeechResourceManager shareInstance]; + NSString* offlineLanguage = [self.settings getString:SETTING_TTS_OFFLINE_LANGUAGE]; + if (offlineLanguage.length <= 0) { + offlineLanguage = SDEF_TTS_DEFAULT_OFFLINE_LANGUAGE; + } + NSArray* ttsLanguageArray = @[offlineLanguage]; + NSLog(@"需要下载的离线合成语种资源有: %@", ttsLanguageArray); + [speechResourceManager setTtsLanguage:ttsLanguageArray]; + NSArray* needDownloadVoiceType = (NSArray *)SDEF_TTS_DEFAULT_DOWNLOAD_OFFLINE_VOICES(); + NSArray* voiceTypeArray = [self.settings getOptions:SETTING_OFFLINE_VOICE_TYPE].optionsArray; + if (voiceTypeArray != nil && voiceTypeArray.count > 0) { + needDownloadVoiceType = voiceTypeArray; + } + NSLog(@"需要下载的离线合成音色资源有: %@", needDownloadVoiceType); + [speechResourceManager setTtsVoiceType:needDownloadVoiceType]; + + NSLog(@"检查本地是否存在可用模型"); + if ([speechResourceManager checkModelExist]) { + NSLog(@"本地没有模型,开始下载"); + [self fetchSingleVoiceResource]; + } else { + NSLog(@"模型存在,检查是否需要更新模型"); + [speechResourceManager checkModelVersion:^(SEResourceStatus status, BOOL needUpdate, NSData *data) { + if (status != kSERSuccess || needUpdate == NO) { + NSLog(@"无需更新,直接使用本地已有模型。"); + [self initEngineInternal]; + } else { + NSLog(@"存在更新,开始下载模型"); + [self fetchSingleVoiceResource]; + } + }]; + } +} + +- (void)fetchSingleVoiceResource { + SpeechResourceManager *speechResourceManager = [SpeechResourceManager shareInstance]; + [speechResourceManager fetchModel:^(SEResourceStatus status, NSData* data) { + if (status == kSERSuccess) { + NSLog(@"下载成功"); + [self initEngineInternal]; + } else { + NSLog(@"下载失败,错误码: %d", status); + [self speechEngineInitFailed:kSERDownloadFailed]; + } + }]; +} + +- (void)initEngineInternal { + NSLog(@"配置初始化参数"); + [self configInitParams]; + + NSLog(@"引擎初始化"); + SEEngineErrorCode ret = [self.curEngine initEngine]; + self.engineInited = (ret == SENoError); + if (self.engineInited) { + NSLog(@"初始化成功"); + [self speechEngineInitSucceeded]; + } else { + NSLog(@"初始化失败,返回值: %d", ret); + [self speechEngineInitFailed:ret]; + } +} + +- (void)uninitEngine { + if (self.curEngine != nil) { + NSLog(@"引擎析构"); + [self.curEngine destroyEngine]; + self.curEngine = nil; + NSLog(@"引擎析构完成"); + } +} + +#pragma mark - UI Actions + +- (IBAction)switchEngine:(id)sender { + if (self.engineStarted) { + [self.statusTextView setText:@"Engine is busy, stop it first!"]; + return; + } + [self clearResult:nil]; + self.startEngineButton.enabled = FALSE; + self.synthesisButton.enabled = FALSE; + self.pauseResumeButton.enabled = FALSE; + if (self.engineInited) { + self.referTextView.editable = FALSE; + [self uninitEngine]; + self.engineInited = FALSE; + [self.statusTextView setText:@"Waiting for init."]; + self.engineSwitchButton.enabled = TRUE; + [self.engineSwitchButton setTitle:@"Init Engine" forState:UIControlStateNormal]; + + self.stopEngineButton.enabled = FALSE; + } else { + self.referTextView.editable = TRUE; + [self initEngine]; + } +} + +- (IBAction)Synthesis:(id)sender { + [self triggerSynthesis]; +} + +- (IBAction)startEngineBtnClicked:(id)sender { + NSLog(@"Start engine, current status: %d", self.engineStarted); + if (!self.engineStarted) { + [self clearResult:nil]; + self.engineErrorOccurred = FALSE; + + // Directive:启动引擎前调用SYNC_STOP指令,保证前一次请求结束。 + NSLog(@"关闭引擎(同步)"); + NSLog(@"Directive: SEDirectiveSyncStopEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveSyncStopEngine]; + if (ret != SENoError) { + NSLog(@"Send directive syncstop failed: %d", ret); + } else { + [self configStartTtsParams]; + + NSLog(@"启动引擎."); + NSLog(@"Directive: SEDirectiveStartEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (SENoError != ret) { + NSString* message = [NSString stringWithFormat:@"发送启动引擎指令失败: %d", ret]; + [self sendStartEngineDirectiveFailed:message]; + } + } + } +} + +- (IBAction)stopEngineBtnClicked:(id)sender { + NSLog(@"关闭引擎"); + NSLog(@"Directive: SEDirectiveStopEngine"); + [self.curEngine sendDirective:SEDirectiveStopEngine]; +} + +- (void) pausePlayback { + NSLog(@"暂停播放"); + NSLog(@"Directive: SEDirectivePausePlayer"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectivePausePlayer]; + if (ret == SENoError) { + self.playerPaused = TRUE; + [self.pauseResumeButton setTitle:@"Resume" forState:UIControlStateNormal]; + } + NSLog(@"Pause playback status: %d", ret); +} + +- (void) resumePlayback { + NSLog(@"继续播放"); + NSLog(@"Directive: SEDirectiveResumePlayer"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveResumePlayer]; + if (ret == SENoError) { + self.playerPaused = FALSE; + [self.pauseResumeButton setTitle:@"Pause" forState:UIControlStateNormal]; + } + NSLog(@"Resume playback status: %d", ret); +} + +- (IBAction)controlPlayingStatus:(id)sender { + NSLog(@"Pause or resume player, current player status: %hhd", self.playerPaused); + if (self.playerPaused) { + [self resumePlayback]; + } else { + [self pausePlayback]; + } +} + +- (IBAction)clearResult:(id)sender { + [self.resultTextView setText:@""]; +} + +#pragma mark - Message Callback + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSLog(@"Message Type: %d.", type); + switch (type) { + case SEEngineStart: + NSLog(@"Callback: 引擎启动成功: data: %@", data); + [self speechEngineStarted]; + break; + case SEEngineStop: + NSLog(@"Callback: 引擎关闭: data: %@", data); + [self speechEngineStopped]; + break; + case SEEngineError: + NSLog(@"Callback: 错误信息: %@", data); + [self speechEngineError:data]; + break; + case SETtsSynthesisBegin: + NSLog(@"Callback: 合成开始: %@", data); + [self speechStartSynthesis:data]; + break; + case SETtsSynthesisEnd: + NSLog(@"Callback: 合成结束: %@", data); + [self speechFinishSynthesis:data]; + break; + case SETtsStartPlaying: + NSLog(@"Callback: 播放开始: %@", data); + [self speechStartPlaying:data]; + break; + case SETtsPlaybackProgress: + NSLog(@"Callback: 播放进度"); + [self updatePlayingProgress:data]; + break; + case SETtsFinishPlaying: + NSLog(@"Callback: 播放结束: %@", data); + [self speechFinishPlaying:data]; + break; + case SETtsAudioData: + NSLog(@"Callback: 音频数据,长度 %lu 字节", (unsigned long)data.length); + [self speechTtsAudioData:data]; + break; + default: + break; + } +} + +- (void)speechEngineInitSucceeded { + dispatch_async(dispatch_get_main_queue(), ^{ + self.engineSwitchButton.enabled = TRUE; + [self.engineSwitchButton setTitle:@"UninitEngine" forState:UIControlStateNormal]; + [self.statusTextView setText:@"Ready"]; + [self.resultTextView setText:[NSString stringWithFormat:@"DeviceID: %@.", self.deviceID]]; + self.referTextView.editable = TRUE; + self.startEngineButton.enabled = TRUE; + }); +} + +- (void)speechEngineInitFailed:(int)initStatus { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:[[NSString alloc] initWithFormat:@"Failed to init engine, %d!", initStatus]]; + self.engineSwitchButton.enabled = TRUE; + }); +} + +- (void)sendSynthesisDirectiveFailed:(NSString*)tipText { + NSLog(@"%@", tipText); + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:tipText]; + [self.curEngine sendDirective:SEDirectiveStopEngine]; + }); +} + +- (void)sendStartEngineDirectiveFailed:(NSString*)tipText { + NSLog(@"%@", tipText); + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:tipText]; + self.engineStarted = FALSE; + }); +} + +- (void)speechEngineStarted { + self.ttsRetryCount = TTS_MAX_RETRY_COUNT; + dispatch_async(dispatch_get_main_queue(), ^{ + self.referTextView.editable = FALSE; + self.engineStarted = true; + [self.statusTextView setText:@"Engine Started!"]; + self.startEngineButton.enabled = FALSE; + self.synthesisButton.enabled = TRUE; + self.stopEngineButton.enabled = TRUE; + }); +} + +- (void)speechEngineStopped { + dispatch_async(dispatch_get_main_queue(), ^{ + self.referTextView.editable = TRUE; + self.engineStarted = FALSE; + [self.statusTextView setText:@"Engine Stopped!"]; + self.startEngineButton.enabled = TRUE; + self.synthesisButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + [self.pauseResumeButton setTitle:@"Pause" forState:UIControlStateNormal]; + self.pauseResumeButton.enabled = FALSE; + self.playerPaused = FALSE; + }); +} + +- (void)speechEngineError:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + + BOOL needStop = NO; + id json_obj = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil]; + if ([json_obj isKindOfClass:[NSDictionary class]]) { + NSDictionary *error_info = json_obj; + NSInteger code = [[error_info objectForKey:@"err_code"] intValue]; + switch (code) { + case SETTSLimitQps: + case SETTSLimitCount: + case SETTSServerBusy: + case SETTSLongText: + case SETTSInvalidText: + case SETTSSynthesisTimeout: + case SETTSSynthesisError: + case SETTSSynthesisWaitingTimeout: + case SETTSErrorUnknown: + NSLog(@"When meeting this kind of error, continue to synthesize."); + [self synthesisNextSentence]; + break; + case SEConnectTimeout: + case SEReceiveTimeout: + case SENetLibError: + // 遇到网络错误时建议重试,重试次数不超过 3 次 + needStop = ![self retrySynthesis]; + if (needStop) { + self.engineErrorOccurred = TRUE; + } + break; + default: + needStop = YES; + self.engineErrorOccurred = TRUE; + [self.resultTextView + setText:[[NSString alloc] + initWithData:data + encoding:NSUTF8StringEncoding]]; + break; + } + } else { + needStop = YES; + } + if (needStop) { + [self.curEngine sendDirective:SEDirectiveStopEngine]; + } + }); +} + +// 根据 SDK 返回的播放进度高亮正在播放的文本,用红色表示 +// 根据 SDK 返回的合成开始和合成结束回调高亮正在合成的文本,用蓝色表示 +-(void)updateTtsResultText:(NSString*) playingId { + if (self.engineErrorOccurred) { + NSLog(@"When a fatal error occurs, prevent the playback text from being displayed."); + return; + } + + NSNumber* val = [self.ttsSynthesisMap objectForKey:playingId]; + if (val != nil) { + self.ttsPlayingIndex = [val intValue]; + } + + int beginIndex = MAX(self.ttsPlayingIndex, 0); + int maxSentencesDisplayed = MIN((int)[self.ttsSynthesisText count], 16); + NSMutableAttributedString *resultStr = [[NSMutableAttributedString alloc] initWithString:@""]; + for (int cnt = 0; cnt < maxSentencesDisplayed; ++cnt) { + int index = (beginIndex + cnt) % [self.ttsSynthesisText count]; + NSString* current_sentence = self.ttsSynthesisText[index]; + NSInteger playedPosition = 0; + if (index == self.ttsPlayingIndex) { + playedPosition = MIN(ceil((double)(self.ttsPlayingProgress) * (double)([current_sentence length])), [current_sentence length]); + NSLog(@"played position: %ld", (long)playedPosition); + NSString* playedString = [current_sentence substringToIndex:playedPosition]; + NSAttributedString* playedSpan = [[NSAttributedString alloc] initWithString:playedString attributes:[NSDictionary dictionaryWithObject:[UIColor redColor] forKey:NSForegroundColorAttributeName]]; + [resultStr appendAttributedString:playedSpan]; + } + NSString* remainString = [current_sentence substringFromIndex:playedPosition]; + NSAttributedString* span = [[NSAttributedString alloc] initWithString:remainString attributes:[NSDictionary dictionaryWithObject:[UIColor blackColor] forKey:NSForegroundColorAttributeName]]; + [resultStr appendAttributedString:span]; + } + [self.resultTextView setAttributedText:resultStr]; +} + +- (void)speechStartSynthesis:(NSData *)data { + if (self.ttsSynthesisIndex < [self.ttsSynthesisText count]) { + NSString* req_id = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + [self.ttsSynthesisMap setValue:[NSNumber numberWithInt:self.ttsSynthesisIndex] forKey:req_id]; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + self.synthesisButton.enabled = FALSE; + }); +} + +- (void)speechFinishSynthesis:(NSData *)data { + if (self.ttsRetryCount < TTS_MAX_RETRY_COUNT) { + self.ttsRetryCount = TTS_MAX_RETRY_COUNT; + } + [self synthesisNextSentence]; +} + +- (void)speechStartPlaying:(NSData *)data { + NSString* playingId = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSLog(@"TTS start playing: %@", playingId); + dispatch_async(dispatch_get_main_queue(), ^{ + self.pauseResumeButton.enabled = TRUE; + self.ttsPlayingProgress = 0.0; + [self updateTtsResultText:playingId]; + }); +} + +- (void)updatePlayingProgress :(NSData *)data { + if (data != nil) { + NSError *error = nil; + id object = [NSJSONSerialization + JSONObjectWithData:data + options:0 + error:&error]; + if(error) { + NSLog(@"Parse data as json error!"); + return ; + } + if([object isKindOfClass:[NSDictionary class]]) { + NSDictionary *results = object; + float percentage = [[results valueForKey:@"progress"] floatValue]; + NSString *reqid = [results valueForKey:@"reqid"]; + NSLog(@"playing id: %@, progress in percent: %.2f", reqid, percentage); + dispatch_async(dispatch_get_main_queue(), ^{ + self.ttsPlayingProgress = percentage; + [self updateTtsResultText:reqid]; + }); + } + } +} + +- (void)speechFinishPlaying :(NSData *)data { + NSString* playingId = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSLog(@"TTS finish playing: %@", playingId); + dispatch_async(dispatch_get_main_queue(), ^{ + self.ttsPlayingProgress = 1.0; + [self updateTtsResultText:playingId]; + }); + if (self.ttsSynthesisFromPlayer) { + [self triggerSynthesis]; + self.ttsSynthesisFromPlayer = FALSE; + } +} + +- (void)speechTtsAudioData:(NSData *)data { +} + +- (BOOL)retrySynthesis { + BOOL ret = FALSE; + if (self.engineStarted && self.ttsRetryCount > 0) { + NSLog(@"Retry synthesis for text: %@", self.ttsSynthesisText[self.ttsSynthesisIndex]); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [self triggerSynthesis]; + }); + self.ttsRetryCount -= 1; + ret = TRUE; + } + return ret; +} + +- (void)synthesisNextSentence { + self.ttsSynthesisIndex = (self.ttsSynthesisIndex + 1) % [self.ttsSynthesisText count]; + if (!self.ttsSynthesisFromPlayer) { + [self triggerSynthesis]; + } +} + +-(void)triggerSynthesis { + [self configSynthesisParams]; + // DIRECTIVE_SYNTHESIS 是连续合成必需的一个指令,在成功调用 DIRECTIVE_START_ENGINE 之后,每次合成新的文本需要再调用 DIRECTIVE_SYNTHESIS 指令 + // DIRECTIVE_SYNTHESIS 需要在当前没有正在合成的文本时才可以成功调用,否则就会报错 -901,可以在收到 MESSAGE_TYPE_TTS_SYNTHESIS_END 之后调用 + // 当使用 SDK 内置的播放器时,为了避免缓存过多的音频导致内存占用过高,SDK 内部限制缓存的音频数量不超过 5 次合成的结果, + // 如果 DIRECTIVE_SYNTHESIS 后返回 -902, 就需要在下一次收到 MESSAGE_TYPE_TTS_FINISH_PLAYING 再去调用 MESSAGE_TYPE_TTS_FINISH_PLAYING + NSLog(@"触发合成"); + NSLog(@"Directive: DIRECTIVE_SYNTHESIS"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveSynthesis]; + if (ret != SENoError) { + NSLog(@"Synthesis faile: %d", ret); + if (ret == SESynthesisPlayerIsBusy) { + self.ttsSynthesisFromPlayer = TRUE; + } else { + NSString* message = [NSString stringWithFormat:@"发送合成指令失败: %d", ret]; + [self sendSynthesisDirectiveFailed:message]; + } + } +} + +-(void)addSentence:(NSString*) text { + NSCharacterSet* blankChar = [NSCharacterSet characterSetWithCharactersInString:@" "]; + NSString* tmp = [text stringByTrimmingCharactersInSet:blankChar]; + if (tmp.length > 0) { + [self.ttsSynthesisText addObject:tmp]; + } +} + +-(void)resetTtsContext { + self.ttsSynthesisIndex = 0; + self.ttsPlayingIndex = -1; + self.ttsSynthesisFromPlayer = FALSE; + [self.ttsSynthesisText removeAllObjects]; + [self.ttsSynthesisMap removeAllObjects]; +} + +-(BOOL)prepareNovelText { + [self resetTtsContext]; + NSString* text = self.referTextView.text; + if (text.length <= 0) { + text = @"愿中国青年都摆脱冷气,只是向上走,不必听自暴自弃者流的话。能做事的做事,能发声的发声。有一分热,发一分光。就令萤火一般,也可以在黑暗里发一点光,不必等候炬火。此后如竟没有炬火:我便是唯一的光。"; + } + if (self.ttsSynthesisText == nil || [self.ttsSynthesisText count] <= 0) { + // 使用下面几个标点符号来分句,会让通过 MESSAGE_TYPE_TTS_PLAYBACK_PROGRESS 返回的播放进度更加准确 + NSArray* temp = [text componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@";!?。!?;…"]]; + for (int j = 0; j < temp.count; ++j) { + [self addSentence:temp[j]]; + } + } + NSLog(@"Synthesis text item num: %ld.", [self.ttsSynthesisText count]); + return [self.ttsSynthesisText count] > 0; +} + +#pragma mark - Helper + +- (NSString*)getTtsTextType { + switch ([self.settings getOptions:SETTING_TTS_TEXT_TYPE].chooseIdx) { + case 0: + return SE_TTS_TEXT_TYPE_PLAIN; + case 1: + return SE_TTS_TEXT_TYPE_SSML; + default: + break; + } + return SE_TTS_TEXT_TYPE_PLAIN;; +} + +- (int)getTtsWorkMode { + switch ([self.settings getOptions:SETTING_TTS_WORK_MODE].chooseIdx) { + case 0: + return SETtsWorkModeOnline; + case 1: + return SETtsWorkModeOffline; + case 2: + return SETtsWorkModeAlternate; + default: + break; + } + return SETtsWorkModeOnline;; +} + +- (NSString*)getAuthenticationType { + switch ([self.settings getOptions:SETTING_AUTHENTICATION_TYPE].chooseIdx) { + case 0: + return SE_AUTHENTICATE_TYPE_PRE_BIND; + case 1: + return SE_AUTHENTICATE_TYPE_LATE_BIND; + default: + break; + } + return SE_AUTHENTICATE_TYPE_PRE_BIND; +} + +- (long)timeDelayFrom:(long)pastTimestamp { + return [[NSDate date] timeIntervalSince1970] * 1000 - pastTimestamp; +} + + +#pragma mark - UITextViewDelegate + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if([text isEqualToString:@"\n"]) { + [textView resignFirstResponder]; + return NO; + } + return YES; +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_TTS forKey:@"viewId"]; +} + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/UniTTSViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/UniTTSViewController.h new file mode 100644 index 0000000..963066a --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/UniTTSViewController.h @@ -0,0 +1,17 @@ +// +// UniTTSViewController.h +// SpeechDemo +// +// Created by ByteDance on 2025/7/3. +// Copyright © 2025 ByteDance. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UniTTSViewController : UIViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/UniTTSViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/UniTTSViewController.m new file mode 100644 index 0000000..088b2dd --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/UniTTSViewController.m @@ -0,0 +1,476 @@ +// +// UniTTSViewController.m +// SpeechDemo +// +// Created by ByteDance on 2025/7/3. +// Copyright © 2025 ByteDance. All rights reserved. +// + +#import "UniTTSViewController.h" +#include +#include + +#import + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" +#import "SensitiveDefines.h" + +@interface UniTTSViewController () + +@property (weak, nonatomic) IBOutlet UITextView *referTextView; +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; +@property (weak, nonatomic) IBOutlet UIButton *engineSwitchButton; +@property (weak, nonatomic) IBOutlet UIButton *startEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *stopEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *pauseResumeButton; + +// Device ID: 用于定位线上问题 +@property (nonatomic, strong) NSString *deviceID; +// Debug Path: 用于存放一些 SDK 相关的文件,比如模型、日志等 +@property (strong, nonatomic) NSString *debugPath; + +// SpeechEngine +@property (strong, nonatomic) SpeechEngine *curEngine; + +// Engine State +@property (assign, nonatomic) BOOL engineInited; +@property (assign, nonatomic) BOOL engineStarted; +@property (assign, nonatomic) BOOL playerPaused; +// Settings +@property (strong, nonatomic) Settings *settings; + +@end + +@implementation UniTTSViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_UNITTS]; + + self.engineSwitchButton.enabled = TRUE; + self.referTextView.editable = TRUE; + self.engineInited = FALSE; + self.engineStarted = FALSE; + self.playerPaused = FALSE; + + [self.referTextView setText:@"愿中国青年都摆脱冷气,只是向上走,不必听自暴自弃者流的话。能做事的做事,能发声的发声。有一分热,发一分光。就令萤火一般,也可以在黑暗里发一点光,不必等候炬火。此后如竟没有炬火:我便是唯一的光。"]; + [self.statusTextView setText:@"Waiting for init."]; + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"当前调试路径 %@", self.debugPath); + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(audioInterruptionHandler:) + name:AVAudioSessionInterruptionNotification + object:nil]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; +} + +#pragma mark - Notifications + +-(void)appWillTerminate:(NSNotification*)note { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:AVAudioSessionInterruptionNotification + object:nil]; +} + +- (void)audioInterruptionHandler:(NSNotification*)notification { + AVAudioSessionInterruptionType interruptionType = (AVAudioSessionInterruptionType)[[notification.userInfo objectForKey:AVAudioSessionInterruptionTypeKey] unsignedIntegerValue]; + AVAudioSessionInterruptionOptions intertuptionOptions = [[notification.userInfo objectForKey:AVAudioSessionInterruptionOptionKey] unsignedIntValue]; + NSLog(@"Receive audio interruption notification, type: %lu, options: %lu.", (unsigned long)interruptionType, (unsigned long)intertuptionOptions); + if (interruptionType == AVAudioSessionInterruptionTypeBegan) { + NSLog(@"Audio session interruption began"); + @synchronized (self) { + [self pausePlayback]; + } + } else if (interruptionType == AVAudioSessionInterruptionTypeEnded) { + @synchronized (self) { + NSLog(@"Audio session interruption ended"); + if (intertuptionOptions == AVAudioSessionInterruptionOptionShouldResume) { + AVAudioSession *session = [AVAudioSession sharedInstance]; + AVAudioSessionCategoryOptions cur_options = session.categoryOptions; + // AudioQueueStart() will return AVAudioSessionErrorCodeCannotInterruptOthers if options didn't contains AVAudioSessionCategoryOptionMixWithOthers + if (!(cur_options & AVAudioSessionCategoryOptionMixWithOthers)) { + AVAudioSessionCategoryOptions readyOptions = AVAudioSessionCategoryOptionMixWithOthers | cur_options; + [session setCategory:AVAudioSessionCategoryPlayback withOptions:readyOptions error:nil]; + } + [self resumePlayback]; + + cur_options = session.categoryOptions; + // Remove AVAudioSessionCategoryOptionMixWithOthers, or the playback will not be interrupted any more + if (cur_options & AVAudioSessionCategoryOptionMixWithOthers) { + [session setCategory:AVAudioSessionCategoryPlayback withOptions:((~AVAudioSessionCategoryOptionMixWithOthers) & cur_options) error:nil]; + } + } + } + } +} + +#pragma mark - Config & Init & Uninit Methods + +-(void)configInitParams { + //【必需配置】Engine Name(双向流式TTS Engine 也支持 单向流式TTS 功能) + [self.curEngine setStringParam:SE_BITTS_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + + //【可选配置】Debug & Log + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + [self.curEngine setStringParam:SE_LOG_LEVEL_DEBUG forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + + //【可选配置】User ID(用以辅助定位线上用户问题) + [self.curEngine setStringParam:SDEF_UID forKey:SE_PARAMS_KEY_UID_STRING]; + [self.curEngine setStringParam:self.deviceID forKey:SE_PARAMS_KEY_DEVICE_ID_STRING]; + + //【可选配置】是否将合成出的音频保存到设备上,为 true 时需要正确配置 PARAMS_KEY_TTS_AUDIO_PATH_STRING 才会生效 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_ENABLE_DUMP] + forKey:SE_PARAMS_KEY_TTS_ENABLE_DUMP_BOOL]; + // TTS 音频文件保存目录,必须在合成之前创建好且 APP 具有访问权限,保存的音频文件名格式为 tts_{reqid}.wav, {reqid} 是本次合成的请求 id + // PARAMS_KEY_TTS_ENABLE_DUMP_BOOL 配置为 true 的音频时为【必需配置】,否则为【可选配置】 + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_TTS_AUDIO_PATH_STRING]; + //【可选配置】自定义请求Header + [self.curEngine setStringParam:[self.settings getString:SETTING_REQUEST_HEADERS] forKey:SE_PARAMS_KEY_REQUEST_HEADERS_STRING]; + //【可选配置】是否使用 SDK 内置播放器播放合成出的音频,默认为 true + [self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_ENABLE_PLAYER] + forKey:SE_PARAMS_KEY_TTS_ENABLE_PLAYER_BOOL]; + //【可选配置】是否令 SDK 通过回调返回合成的音频数据,默认不返回。 + // 开启后,SDK 会流式返回音频,收到 SETtsAudioData 回调表示当次合成所有的音频已经全部返回 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ENABLE_PLAYER_AUDIO_CALL_BACK] ? SETtsDataCallbackModeAll : SETtsDataCallbackModeNone forKey:SE_PARAMS_KEY_ENABLE_PLAYER_AUDIO_CALLBACK_BOOL]; + + // ------------------------ 在线合成相关配置 ----------------------- + //【必需配置】在线合成鉴权相关:Appid + [self.curEngine setStringParam:[self.settings getString:SETTING_APPID] forKey:SE_PARAMS_KEY_APP_ID_STRING]; + //【必需配置】在线合成鉴权相关:Token + [self.curEngine setStringParam:[self.settings getString:SETTING_TOKEN] forKey:SE_PARAMS_KEY_APP_TOKEN_STRING]; + //【必需配置】语音合成服务域名 + [self.curEngine setStringParam:[self.settings getString:SETTING_ADDRESS] forKey:SE_PARAMS_KEY_TTS_ADDRESS_STRING]; + //【必需配置】语音合成服务Uri + [self.curEngine setStringParam:[self.settings getString:SETTING_URI] forKey:SE_PARAMS_KEY_TTS_URI_STRING]; + //【必需配置】语音合成服务资源id + [self.curEngine setStringParam:[self.settings getString:SETTING_RESOURCE_ID] forKey: SE_PARAMS_KEY_RESOURCE_ID_STRING]; + //【可选配置】TTS连接超时时间 + [self.curEngine setIntParam:10000 forKey:SE_PARAMS_KEY_TTS_CONN_TIMEOUT_INT]; + +} + +- (void)initEngine { + NSLog(@"获取设备ID,调试使用"); + AppDelegate *appDelegate = [ViewController getAppDelegate]; + if (appDelegate == nil) { + appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; + } + [ViewController setAppDelegate:appDelegate]; + self.deviceID = appDelegate.deviceID; + NSLog(@"获取设备ID成功: %@", self.deviceID); + + + NSLog(@"创建引擎"); + if (self.curEngine == nil) { + self.curEngine = [[SpeechEngine alloc] init]; + if (![self.curEngine createEngineWithDelegate:self]) { + NSLog(@"引擎创建失败."); + return; + } + } + NSLog(@"SDK 版本号: %@", [self.curEngine getVersion]); + [self initEngineInternal]; +} + +- (void)initEngineInternal { + NSLog(@"配置初始化参数"); + [self configInitParams]; + + NSLog(@"引擎初始化"); + SEEngineErrorCode ret = [self.curEngine initEngine]; + self.engineInited = (ret == SENoError); + if (self.engineInited) { + NSLog(@"初始化成功"); + [self speechEngineInitSucceeded]; + } else { + NSLog(@"初始化失败,返回值: %d", ret); + [self speechEngineInitFailed:ret]; + } +} + +- (void)uninitEngine { + if (self.curEngine != nil) { + NSLog(@"引擎析构"); + [self.curEngine destroyEngine]; + self.curEngine = nil; + NSLog(@"引擎析构完成"); + } +} + +#pragma mark - UI Actions + +- (IBAction)switchEngine:(id)sender { + if (self.engineStarted) { + [self.statusTextView setText:@"Engine is busy, stop it first!"]; + return; + } + [self clearResult:nil]; + self.startEngineButton.enabled = FALSE; + self.pauseResumeButton.enabled = FALSE; + if (self.engineInited) { + self.referTextView.editable = TRUE; + [self uninitEngine]; + self.engineInited = FALSE; + [self.statusTextView setText:@"Waiting for init."]; + self.engineSwitchButton.enabled = TRUE; + [self.engineSwitchButton setTitle:@"Init Engine" forState:UIControlStateNormal]; + self.stopEngineButton.enabled = FALSE; + } else { + self.referTextView.editable = TRUE; + [self initEngine]; + } +} + +- (IBAction)startEngineBtnClicked:(id)sender { + NSLog(@"Start engine, current status: %d", self.engineStarted); + if (!self.engineStarted) { + [self clearResult:nil]; + + // Directive:启动引擎前调用SYNC_STOP指令,保证前一次请求结束。 + NSLog(@"关闭引擎(同步)"); + NSLog(@"Directive: SEDirectiveSyncStopEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveSyncStopEngine]; + if (ret != SENoError) { + NSLog(@"Send directive syncstop failed: %d", ret); + } else { + + NSLog(@"启动引擎."); + NSLog(@"Directive: SEDirectiveStartEngine"); + NSString* startPayload = [NSString stringWithFormat:@"{\"req_params\":{\"text\":\"%@\",\"speaker\":\"zh_female_roumeinvyou_emo_v2_mars_bigtts\",\"audio_params\":{\"emotion\":\"excited\",\"loudness_rate\":50}}}", self.referTextView.text]; + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine data:startPayload]; + if (SENoError != ret) { + NSString* message = [NSString stringWithFormat:@"发送启动引擎指令失败: %d", ret]; + [self sendStartEngineDirectiveFailed:message]; + return; + } + ret = [self.curEngine sendDirective:SEDirectiveEventStartSession data:@""]; + if (ret != SENoError) { + NSLog(@"Start Session faile: %d", ret); + } + } + } +} + +- (IBAction)stopEngineBtnClicked:(id)sender { + NSLog(@"关闭引擎"); + NSLog(@"Directive: SEDirectiveStopEngine"); + [self.curEngine sendDirective:SEDirectiveStopEngine]; +} + +- (void)pausePlayback { + NSLog(@"暂停播放"); + NSLog(@"Directive: SEDirectivePausePlayer"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectivePausePlayer]; + if (ret == SENoError) { + self.playerPaused = TRUE; + [self.pauseResumeButton setTitle:@"Resume" forState:UIControlStateNormal]; + } + NSLog(@"Pause playback status: %d", ret); +} + +- (void)resumePlayback { + NSLog(@"继续播放"); + NSLog(@"Directive: SEDirectiveResumePlayer"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveResumePlayer]; + if (ret == SENoError) { + self.playerPaused = FALSE; + [self.pauseResumeButton setTitle:@"Pause" forState:UIControlStateNormal]; + } + NSLog(@"Resume playback status: %d", ret); +} + +- (IBAction)controlPlayingStatus:(id)sender { + NSLog(@"Pause or resume player, current player status: %hhd", self.playerPaused); + if (self.playerPaused) { + [self resumePlayback]; + } else { + [self pausePlayback]; + } +} + +- (IBAction)clearResult:(id)sender { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:@""]; + }); +} + +#pragma mark - Message Callback + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSLog(@"Message Type: %d.", type); + switch (type) { + case SEEngineStart: + NSLog(@"Callback: 引擎启动成功: data: %@", data); + [self speechEngineStarted]; + break; + case SEEngineStop: + NSLog(@"Callback: 引擎关闭: data: %@", data); + [self speechEngineStopped]; + break; + case SEEngineError: + NSLog(@"Callback: 错误信息: %@", data); + [self speechEngineError:data]; + break; + case SEEventConnectionStarted: + NSLog(@"Callback: SEEventConnectionStarted"); + break; + case SEEventConnectionFailed: + NSLog(@"Callback: SEEventConnectionFailed"); + break; + case SEEventConnectionFinished: + NSLog(@"Callback: SEEventConnectionFinished"); + break; + case SEEventSessionStarted: + NSLog(@"Callback: SEEventSessionStarted"); + break; + case SEEventSessionCanceled: + NSLog(@"Callback: SEEventSessionCanceled"); + break; + case SEEventSessionFinished: + NSLog(@"Callback: SEEventSessionFinished"); + break; + case SEEventSessionFailed: + NSLog(@"Callback: SEEventSessionFailed"); + break; + case SEEventTTSSentenceStart: + NSLog(@"Callback: 合成开始 SentenceStart: %@", data); + break; + case SEEventTTSSentenceEnd: + NSLog(@"Callback: 合成结束 SentenceEnd: %@", data); + case SEEventTTSResponse: + NSLog(@"Callback: 收到合成音频 TTSResponse: %@", data); + case SEEventTTSEnded: + NSLog(@"Callback: TTSEnd: %@", data); + case SEPlayerAudioData: + NSLog(@"Callback: 接收到播放的pcm音频"); + break; + case SEPlayerStartPlayAudio: + NSLog(@"Callback: 开始播放TTS音频"); + [self speechStartPlaying:nil]; + break; + case SEPlayerFinishPlayAudio: + NSLog(@"Callback: 结束播放TTS音频"); + [self speechFinishPlaying:nil]; + break; + default: + break; + } +} + +- (void)speechEngineInitSucceeded { + dispatch_async(dispatch_get_main_queue(), ^{ + self.engineSwitchButton.enabled = TRUE; + [self.engineSwitchButton setTitle:@"UninitEngine" forState:UIControlStateNormal]; + [self.statusTextView setText:@"Ready"]; + [self.resultTextView setText:[NSString stringWithFormat:@"DeviceID: %@.", self.deviceID]]; + self.referTextView.editable = TRUE; + self.startEngineButton.enabled = TRUE; + }); +} + +- (void)speechEngineInitFailed:(int)initStatus { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:[[NSString alloc] initWithFormat:@"Failed to init engine, %d!", initStatus]]; + self.engineSwitchButton.enabled = TRUE; + }); +} + +- (void)sendSynthesisDirectiveFailed:(NSString*)tipText { + NSLog(@"%@", tipText); + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:tipText]; + [self.curEngine sendDirective:SEDirectiveStopEngine]; + }); +} + +- (void)sendStartEngineDirectiveFailed:(NSString*)tipText { + NSLog(@"%@", tipText); + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:tipText]; + self.engineStarted = FALSE; + }); +} + +- (void)speechEngineStarted { + dispatch_async(dispatch_get_main_queue(), ^{ + self.referTextView.editable = TRUE; + self.engineStarted = true; + [self.statusTextView setText:@"Engine Started!"]; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = TRUE; + }); +} + +- (void)speechEngineStopped { + dispatch_async(dispatch_get_main_queue(), ^{ + self.referTextView.editable = TRUE; + self.engineStarted = FALSE; + [self.statusTextView setText:@"Engine Stopped!"]; + self.startEngineButton.enabled = TRUE; + self.stopEngineButton.enabled = FALSE; + [self.pauseResumeButton setTitle:@"Pause" forState:UIControlStateNormal]; + self.pauseResumeButton.enabled = FALSE; + self.playerPaused = FALSE; + }); +} + +- (void)speechEngineError:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + id json_obj = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil]; + if ([json_obj isKindOfClass:[NSDictionary class]]) { + NSDictionary *error_info = json_obj; + [self setResultText:[NSString stringWithFormat:@"%@", error_info]]; + } + }); +} + +- (void)setResultText:(NSString *)result { + dispatch_async(dispatch_get_main_queue(), ^{ + NSString *currentText = self.resultTextView.text ?: @""; + NSString *newText = [NSString stringWithFormat:@"%@\n%@", currentText, result]; + [self.resultTextView setText:newText]; + if (self.resultTextView.text.length > 0) { + NSRange bottomRange = NSMakeRange(self.resultTextView.text.length - 1, 1); + [self.resultTextView scrollRangeToVisible:bottomRange]; + [self.resultTextView layoutIfNeeded]; + } + }); +} + +- (void)speechStartPlaying:(NSData *)data { + NSLog(@"TTS start playing"); + dispatch_async(dispatch_get_main_queue(), ^{ + self.pauseResumeButton.enabled = TRUE; + }); +} + +- (void)speechFinishPlaying :(NSData *)data { + NSLog(@"TTS finish playing"); + [self setResultText:@"playing finished"]; + dispatch_async(dispatch_get_main_queue(), ^{ + // 播放结束,停止引擎 + [self stopEngineBtnClicked:nil]; + }); +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_UNITTS forKey:@"viewId"]; +} + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/VadViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/VadViewController.h new file mode 100644 index 0000000..8465ef8 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/VadViewController.h @@ -0,0 +1,18 @@ +// +// VadViewController.h +// SpeechDemo +// +// Created by bytedance on 2023/1/30. +// Copyright © 2023 tinalei.richard. All rights reserved. +// + + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface VadViewController : UIViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/VadViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/VadViewController.m new file mode 100644 index 0000000..fe6859e --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/VadViewController.m @@ -0,0 +1,455 @@ +// +// VadViewController.m +// SpeechDemo +// +// Created by bytedance on 2023/1/30. +// Copyright © 2023 tianlei.richard. All rights reserved. +// + +#import "VadViewController.h" + +#import + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" +#import "SensitiveDefines.h" + +@interface VadViewController () + +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; +@property (weak, nonatomic) IBOutlet UIButton *engineInitButton; +@property (weak, nonatomic) IBOutlet UIButton *engineUninitButton; +@property (weak, nonatomic) IBOutlet UIButton *startEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *stopEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *longPressButton; + +@property (strong, nonatomic) SpeechEngine *curEngine; +@property (assign, nonatomic) BOOL engineStarted; + +@property (nonatomic, strong) NSString *deviceID; +@property (nonatomic, assign) long talkingFinisheTimestamp; +@property (nonatomic, assign) long vadDuration; +@property (nonatomic, assign) long vadBeginPosition; +@property (nonatomic, assign) long vadEndPosition; +@property (strong, nonatomic) NSString *debugPath; + +@property (weak, nonatomic) StreamRecorder *streamRecorder; + +// settings +@property (strong, nonatomic) Settings *settings; + +@end + +@implementation VadViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_VAD]; + + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.longPressButton.enabled = FALSE; + [self.statusTextView setText:@"Waiting for init."]; + [self decorateTextView:self.resultTextView]; + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + UILongPressGestureRecognizer *longPgr = [[UILongPressGestureRecognizer alloc] initWithTarget:self + action:@selector(longPressTriggered:)]; + longPgr.minimumPressDuration = 0.5; + [self.longPressButton addGestureRecognizer:longPgr]; + self.streamRecorder = [ViewController getStreamRecorder]; + self.engineStarted = FALSE; +} + +- (void)viewDidDisappear:(BOOL)animated { + [self uninitEngine]; + [super viewDidDisappear:animated]; +} + +- (void)decorateTextView:(UITextView *)textView { + textView.layer.cornerRadius = 5.0f; + textView.layer.borderWidth = .25f; + textView.layer.borderColor = [UIColor grayColor].CGColor; +} + +#pragma mark - SpeechEngineDelegate + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSLog(@"Message Type: %d.", type); + switch (type) { + case SEEngineStart: + [self speechEngineStarted]; + break; + case SEEngineStop: + [self speechEngineStopped]; + break; + case SEEngineError: + [self speechEngineError:data]; + break; + case SEVadSilence: + break; + case SEVadSil2Speech: + [self messageVadBegin:data]; + break; + case SEVadSpeech: + [self messasgeVadSpeech:data]; + break; + case SEVadSpeech2Sil: + [self messageVadEnd:data]; + break; + case SEVadAudioData: + self.vadDuration += data.length; + break; + case SEEngineLog: + NSLog(@"engine log: %s", (char*)data.bytes); + break; + default: + break; + } +} + +#pragma mark - UI Actions + +- (IBAction)initEngine:(id)sender { + [self initEngine]; +} + +- (IBAction)uninitEngine:(id)sender { + if (self.engineStarted) { + [self.statusTextView setText:@"Engine is busy, stop it first!"]; + return; + } + [self uninitEngine]; + [self.resultTextView setTextColor:UIColor.grayColor]; + [self.resultTextView setText:@"点击或按住说话后,展示音频活性检测结果"]; +} + +- (IBAction)startEngine:(id)sender { + NSLog(@"Start engine."); + NSString* appID = [self.settings getString:SETTING_APPID]; + [self.curEngine setStringParam:appID.length <= 0 ? SDEF_APPID : appID forKey:SE_PARAMS_KEY_APP_ID_STRING]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_RESTART_AUDIO_SESSION_ENABLE] + forKey:SE_PARAMS_KEY_RESTART_AUDIOSESSION_BOOL]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_RESUME_OTHERS_INTERRUPTED_PLAYBACK_ENABLE] + forKey:SE_PARAMS_KEY_RESUME_OTHERS_INTERRUPTED_PLAYBACK_BOOL]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_GET_VOLUME] forKey:SE_PARAMS_KEY_ENABLE_GET_VOLUME_BOOL]; + [self.curEngine setIntParam:[self.settings getInt:SETTING_VAD_HEAD_SILENCE_THRESHOLD] forKey:SE_PARAMS_KEY_VAD_HEAD_SILENCE_THRESHOLD_INT]; + [self.curEngine setIntParam:[self.settings getInt:SETTING_VAD_TAIL_SILENCE_THRESHOLD] forKey:SE_PARAMS_KEY_VAD_TAIL_SILENCE_THRESHOLD_INT]; + + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.curEngine setIntParam:[self.streamRecorder getSampleRate] forKey:SE_PARAMS_KEY_CUSTOM_SAMPLE_RATE_INT]; + if (![self.streamRecorder start]) { + [self speechEngineNoPermission]; + return; + } + } else if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_FILE]) { + NSString* file_path = [NSString stringWithFormat:@"%@/%@", self.debugPath, @"vad_rec_file.pcm"]; + NSLog(@"test file path: %@", file_path); + [self.curEngine setStringParam:file_path forKey:SE_PARAMS_KEY_RECORDER_FILE_STRING]; + } + + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + } +} + +- (IBAction)stopEngine:(id)sender { + NSLog(@"Stop engine."); + [self.curEngine sendDirective:SEDirectiveStopEngine]; +} + +- (void)longPressTriggered:(UILongPressGestureRecognizer *)longPgr { + if (longPgr.state == UIGestureRecognizerStateBegan) { + NSLog(@"Long press begin."); + + [self setResultText:@""]; + NSString* appID = [self.settings getString:SETTING_APPID]; + [self.curEngine setStringParam:appID.length <= 0 ? SDEF_APPID : appID forKey:SE_PARAMS_KEY_APP_ID_STRING]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_RESTART_AUDIO_SESSION_ENABLE] + forKey:SE_PARAMS_KEY_RESTART_AUDIOSESSION_BOOL]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_RESUME_OTHERS_INTERRUPTED_PLAYBACK_ENABLE] + forKey:SE_PARAMS_KEY_RESUME_OTHERS_INTERRUPTED_PLAYBACK_BOOL]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_GET_VOLUME] + forKey:SE_PARAMS_KEY_ENABLE_GET_VOLUME_BOOL]; + [self.curEngine setIntParam:[self.settings getInt:SETTING_VAD_HEAD_SILENCE_THRESHOLD] forKey:SE_PARAMS_KEY_VAD_HEAD_SILENCE_THRESHOLD_INT]; + [self.curEngine setIntParam:[self.settings getInt:SETTING_VAD_TAIL_SILENCE_THRESHOLD] forKey:SE_PARAMS_KEY_VAD_TAIL_SILENCE_THRESHOLD_INT]; + + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.curEngine setIntParam:[self.streamRecorder getSampleRate] forKey:SE_PARAMS_KEY_CUSTOM_SAMPLE_RATE_INT]; + if (![self.streamRecorder start]) { + [self speechEngineNoPermission]; + return; + } + } else if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_FILE]) { + NSString* file_path = [NSString stringWithFormat:@"%@/%@", self.debugPath, @"vad_rec_file.pcm"]; + NSLog(@"test file path: %@", file_path); + [self.curEngine setStringParam:file_path forKey:SE_PARAMS_KEY_RECORDER_FILE_STRING]; + } + + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + } + } else if (longPgr.state == UIGestureRecognizerStateEnded) { + NSLog(@"Long press ended."); + self.talkingFinisheTimestamp = [[NSDate date] timeIntervalSince1970] * 1000; + [self.curEngine sendDirective:SEDirectiveFinishTalking]; + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.streamRecorder stop]; + } + } +} + +#pragma mark - Init Methods + +- (void)initEngine { + AppDelegate *appDelegate = [ViewController getAppDelegate]; + if (appDelegate == nil) { + appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; + } + if (appDelegate.deviceID.length < 1) { + self.engineInitButton.enabled = FALSE; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:@"Waiting for get deviceID."]; + sleep(1); + [self initEngine]; + }); + return; + } + [ViewController setAppDelegate:appDelegate]; + self.deviceID = appDelegate.deviceID; + + if (self.curEngine == nil) { + self.curEngine = [[SpeechEngine alloc] init]; + } + if (![self.curEngine createEngineWithDelegate:self]) { + NSLog(@"Create speech engine failed."); + return; + } + + [self.resultTextView setTextColor:UIColor.blackColor]; + NSLog(@"Engine version: %@", [self.curEngine getVersion]); + [self initOfflineModel:^() { + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"Debug path: %@", self.debugPath); + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + [self.curEngine setStringParam:SE_LOG_LEVEL_TRACE forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + [self.curEngine setStringParam:SDEF_APPID forKey:SE_PARAMS_KEY_APP_ID_STRING]; + [self.curEngine setStringParam:self.deviceID forKey:SE_PARAMS_KEY_DEVICE_ID_STRING]; + [self.curEngine setIntParam:1 forKey:SE_PARAMS_KEY_CHANNEL_NUM_INT]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_GET_VOLUME] + forKey:SE_PARAMS_KEY_ENABLE_GET_VOLUME_BOOL]; + [self.curEngine setStringParam:@"" forKey:SE_PARAMS_KEY_VAD_REC_PATH_STRING]; + if ([self.settings getBool:SETTING_VAD_RECORDER_SAVE]) { + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_VAD_REC_PATH_STRING]; + } + [self.curEngine setIntParam:4000 forKey:SE_PARAMS_KEY_VAD_HEAD_SILENCE_THRESHOLD_INT]; + [self.curEngine setIntParam:2000 forKey:SE_PARAMS_KEY_VAD_TAIL_SILENCE_THRESHOLD_INT]; + [self.curEngine setStringParam:[self getRecorderType] forKey:SE_PARAMS_KEY_RECORDER_TYPE_STRING]; + [self.curEngine setStringParam:SE_VAD_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + [self.curEngine setIntParam:16000 forKey:SE_PARAMS_KEY_SAMPLE_RATE_INT]; + + NSString* aedResourcePath = [[SpeechResourceManager shareInstance] getModelPath:SE_AED_MODEL]; + NSLog(@"petrel aed resource path: %@", aedResourcePath); + [self.curEngine setStringParam:aedResourcePath forKey:SE_PARAMS_KEY_AED_RESOURCE_PATH_STRING]; + + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + if ([self.streamRecorder getSampleRate] != 16000) { + [self.curEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_ENABLE_RESAMPLER_BOOL]; + } + } + + SEEngineErrorCode ret = [self.curEngine initEngine]; + if (ret != SENoError) { + NSLog(@"Init Engine failed: %d", ret); + } + if (ret == SENoError) { + [self speechEngineInitOk]; + } else { + [self speechEngineInitFailed:ret]; + } + } fail:^{ + [self speechEngineInitFailed:kSERDownloadFailed]; + }]; +} + +- (NSString *)getRecorderType { + SettingOptions* recorderTypeOptions = [self.settings getOptions:SETTING_RECORD_TYPE]; + switch (recorderTypeOptions.chooseIdx) { + case 0: + return SE_RECORDER_TYPE_RECORDER; + case 1: + return SE_RECORDER_TYPE_FILE; + case 2: + return SE_RECORDER_TYPE_STREAM; + default: + break; + } + return @""; +} + +- (void)initOfflineModel:(void(^)(void))succ fail:(void(^)(void))fail { + NSString *model = SE_AED_MODEL; + SpeechResourceManager *speechResourceManager = [SpeechResourceManager shareInstance]; + [speechResourceManager checkModelVersion:model completion:^(SEResourceStatus status, BOOL needUpdate, NSData *data) { + NSLog(@"Need update: %@", needUpdate ? @"YES" : @"NO"); + if (status != kSERSuccess || needUpdate == NO) { + if ([speechResourceManager checkModelExist:model]) { + NSLog(@"Model exist!"); + succ(); + } else { + NSLog(@"Model not exist!"); + fail(); + } + return; + } + + // need to update model + [speechResourceManager fetchModelByName:model completion:^(SEResourceStatus status, NSData* data) { + NSLog(@"Completion: %@", status == kSERSuccess ? @"success" : @"fail"); + if (status == kSERSuccess) { + succ(); + } else { + NSLog(@"Failed: %d", status); + fail(); + } + }]; + }]; +} + +- (void)uninitEngine { + [self.curEngine destroyEngine]; + self.curEngine = nil; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + self.longPressButton.enabled = FALSE; +} + +#pragma mark - Engine Callback + +- (void)speechEngineNoPermission { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"No permission!"]; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + }); +} + +- (void)speechEngineInitOk { + [self.streamRecorder setSpeechEngine:self.curEngine]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:@"Ready"]; + [self.resultTextView setText:[NSString stringWithFormat:@"DeviceID: %@", self.deviceID]]; + self.engineUninitButton.enabled = TRUE; + self.engineInitButton.enabled = FALSE; + self.startEngineButton.enabled = TRUE; + self.longPressButton.enabled = TRUE; + }); +} + +- (void)speechEngineInitFailed:(int)initStatus { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:[[NSString alloc] initWithFormat:@"Failed to init engine, %d!", initStatus]]; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + }); +} + +- (void)speechEngineStarted { + dispatch_async(dispatch_get_main_queue(), ^{ + self.engineStarted = true; + self.vadDuration = 0; + self.vadBeginPosition = 0; + self.vadEndPosition = 0; + [self.statusTextView setText:@"Engine Started!"]; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = TRUE; + self.longPressButton.enabled = FALSE; + }); +} + +- (void)speechEngineStopped { + dispatch_async(dispatch_get_main_queue(), ^{ + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.streamRecorder stop]; + } + self.engineStarted = FALSE; + [self.statusTextView setText:@"Engine Stopped!"]; + self.startEngineButton.enabled = TRUE; + self.stopEngineButton.enabled = FALSE; + self.longPressButton.enabled = TRUE; + }); +} + +- (void)speechEngineError:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]]; + [self stopEngine:nil]; + }); +} + +- (void)messageVadBegin:(NSData *)data { + id json_obj = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil]; + if ([json_obj isKindOfClass:[NSDictionary class]]) { + NSDictionary *vad_info = json_obj; + self.vadBeginPosition = [[vad_info objectForKey:@"start"] doubleValue]; + } + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[NSString stringWithFormat:@"Vad begin, bos: %ld", self.vadBeginPosition]]; + }); +} + +- (void)messasgeVadSpeech:(NSData *)data { + id json_obj = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil]; + if ([json_obj isKindOfClass:[NSDictionary class]]) { + NSDictionary *vad_info = json_obj; + double currentSegEnd = [[vad_info objectForKey:@"end"] doubleValue]; + if (currentSegEnd > self.vadEndPosition) { + self.vadEndPosition = currentSegEnd; + } + } +} + +- (void)messageVadEnd:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[NSString stringWithFormat:@"Vad end, eos: %ld, speech duration: %ldms.", self.vadEndPosition, self.vadDuration/(2*16000/1000)]]; + [self stopEngine:nil]; + }); +} + +- (void)setResultText:(NSString *)result { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[result stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; + }); +} + +#pragma mark - UITextViewDelegate + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if([text isEqualToString:@"\n"]) { + [textView resignFirstResponder]; + return NO; + } + return YES; +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_VAD forKey:@"viewId"]; +} + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/VoiceCloneViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/VoiceCloneViewController.h new file mode 100644 index 0000000..2fe104a --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/VoiceCloneViewController.h @@ -0,0 +1,14 @@ +// +// VoiceCloneViewController.h +// SpeechDemo +// +// Created by bytedance on 2021/2/20. +// Copyright © 2021 chengzihao.ds. All rights reserved. +// + +#import + +@interface VoiceCloneViewController : UIViewController + +@end + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/VoiceCloneViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/VoiceCloneViewController.m new file mode 100644 index 0000000..a1d4450 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/VoiceCloneViewController.m @@ -0,0 +1,649 @@ +// +// VoiceCloneViewController.m +// SpeechDemo +// +// Created by bytedance on 2021/2/20. +// Copyright © 2021 chengzihao.ds. All rights reserved. +// + +#import "VoiceCloneViewController.h" + +#import + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" + +@interface VoiceCloneViewController () + +@property (weak, nonatomic) IBOutlet UITextView *referTextView; +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; +@property (weak, nonatomic) IBOutlet UIButton *engineSwitchButton; +@property (weak, nonatomic) IBOutlet UIButton *getTaskButton; +@property (weak, nonatomic) IBOutlet UIButton *checkEnvButton; +@property (weak, nonatomic) IBOutlet UIButton *voiceRecordButton; +@property (weak, nonatomic) IBOutlet UIButton *finishTalkingButton; +@property (weak, nonatomic) IBOutlet UIButton *getTrainStatusButton; +@property (weak, nonatomic) IBOutlet UIButton *submitTaskButton; +@property (weak, nonatomic) IBOutlet UIButton *delTrainDataButton; +@property (weak, nonatomic) IBOutlet UIButton *nextRecordTaskButton; + +@property (strong, nonatomic) SpeechEngine *curEngine; +@property (assign, nonatomic) BOOL engineInited; +@property (assign, nonatomic) BOOL engineStarted; + +@property (nonatomic, strong) NSString *deviceID; +@property (nonatomic, assign) long talkingFinisheTimestamp; +@property (nonatomic, assign) long startEngineTimestamp; +@property (strong, nonatomic) NSString *debugPath; + +@property (weak, nonatomic) StreamRecorder *streamRecorder; + +// voiceclone json param +@property (strong, nonatomic) NSString *voiceCloneCurText; +@property (assign, nonatomic) NSInteger voiceCloneCurTextSeq; +@property (assign, nonatomic) NSInteger voiceCloneCurTaskId; +// voiceclone last get-task-info result +@property (strong, nonatomic) NSMutableDictionary *voiceCloneTaskInfo; +// ViewStatus +typedef enum ViewStatus : NSUInteger { + BEFORE_INIT, + INITING, + BEFORE_GET_TASK, + BEFORE_CHECK_ENV, + BEFORE_RECORD, + RECORDING, + BEFORE_SUBMIT, + AFTER_SUBMIT, +} ViewStatus; +// prev ViewStatus +@property (assign, nonatomic) ViewStatus preViewStatus; +// current ViewStatus +@property (assign, nonatomic) ViewStatus curViewStatus; + +// settings +@property (strong, nonatomic) Settings *settings; + +@end + +@implementation VoiceCloneViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_VOICECLONE]; + + self.voiceCloneTaskInfo = NULL; + [self updateParamJsonObj]; + + [self switchViewStatus:BEFORE_INIT]; + [self decorateTextView:self.referTextView]; + [self decorateTextView:self.resultTextView]; + [self.referTextView setDelegate:self]; + self.referTextView.editable = FALSE; + self.engineInited = FALSE; + self.engineStarted = FALSE; + self.streamRecorder = [ViewController getStreamRecorder]; + [self.statusTextView setText:@"Waiting for init."]; + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(appDidEnterBackground:) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(appWillTerminate:) + name:UIApplicationWillTerminateNotification + object:nil]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [self uninitEngine]; + [super viewDidDisappear:animated]; +} + +- (void)updateParamJsonObj { + if (self.voiceCloneTaskInfo != NULL) { + [[self.voiceCloneTaskInfo objectForKey:@"progress"]intValue]; + } else { + [self updateParamJsonObj:0]; + } +} + +- (void)updateParamJsonObj:(int)text_seq { + if (self.voiceCloneTaskInfo == NULL) { + self.voiceCloneCurText = @""; + self.voiceCloneCurTextSeq = 0; + self.voiceCloneCurTaskId = [self.settings getInt:SETTING_VOICECLONE_TASKID]; + } else { + NSArray *text_arr = [self.voiceCloneTaskInfo objectForKey:@"texts"]; + self.voiceCloneCurText = [text_arr objectAtIndex:text_seq]; + self.voiceCloneCurTextSeq = text_seq; + self.voiceCloneCurTaskId =[[self.voiceCloneTaskInfo objectForKey:@"task_id"]intValue]; + } + // update refer text + [self.referTextView setText:[NSString stringWithFormat:@"Current task: %d\nCurrent task sequence: %d\nCurrent text: %@", (int)self.voiceCloneCurTaskId, (int)self.voiceCloneCurTextSeq, self.voiceCloneCurText]]; +} + +- (void)decorateTextView:(UITextView *)textView { + textView.layer.cornerRadius = 5.0f; + textView.layer.borderWidth = .25f; + textView.layer.borderColor = [UIColor grayColor].CGColor; +} + +- (void)switchViewStatus:(ViewStatus) status { + self.preViewStatus = self.curViewStatus; + self.curViewStatus = status; + switch (status) { + case BEFORE_INIT: + [self.engineSwitchButton setTitle:@"InitEngine" forState:UIControlStateNormal]; + self.engineSwitchButton.enabled = TRUE; + self.getTaskButton.enabled = FALSE; + self.checkEnvButton.enabled = FALSE; + self.voiceRecordButton.enabled = FALSE; + self.finishTalkingButton.enabled = FALSE; + self.getTrainStatusButton.enabled = FALSE; + self.submitTaskButton.enabled = FALSE; + self.delTrainDataButton.enabled = FALSE; + self.nextRecordTaskButton.enabled = FALSE; + break; + case INITING: + [self.engineSwitchButton setTitle:@"InitEngine" forState:UIControlStateNormal]; + self.engineSwitchButton.enabled = FALSE; + self.getTaskButton.enabled = FALSE; + self.checkEnvButton.enabled = FALSE; + self.voiceRecordButton.enabled = FALSE; + self.finishTalkingButton.enabled = FALSE; + self.getTrainStatusButton.enabled = FALSE; + self.submitTaskButton.enabled = FALSE; + self.delTrainDataButton.enabled = FALSE; + self.nextRecordTaskButton.enabled = FALSE; + break; + case RECORDING: + [self.engineSwitchButton setTitle:@"UninitEngine" forState:UIControlStateNormal]; + self.engineSwitchButton.enabled = FALSE; + self.getTaskButton.enabled = FALSE; + self.checkEnvButton.enabled = FALSE; + self.voiceRecordButton.enabled = FALSE; + self.finishTalkingButton.enabled = TRUE; + self.getTrainStatusButton.enabled = FALSE; + self.submitTaskButton.enabled = FALSE; + self.delTrainDataButton.enabled = FALSE; + self.nextRecordTaskButton.enabled = FALSE; + break; + case BEFORE_GET_TASK: + case BEFORE_CHECK_ENV: + case BEFORE_RECORD: + case BEFORE_SUBMIT: + case AFTER_SUBMIT: + [self.engineSwitchButton setTitle:@"UninitEngine" forState:UIControlStateNormal]; + self.engineSwitchButton.enabled = TRUE; + self.getTaskButton.enabled = TRUE; + self.checkEnvButton.enabled = TRUE; + self.voiceRecordButton.enabled = TRUE; + self.finishTalkingButton.enabled = FALSE; + self.getTrainStatusButton.enabled = TRUE; + self.submitTaskButton.enabled = TRUE; + self.delTrainDataButton.enabled = TRUE; + self.nextRecordTaskButton.enabled = TRUE; + break; + default: + break; + } +} + +#pragma mark - Notifications + +- (void)appDidEnterBackground:(UIApplication *)application; { +// if (self.engineStarted) { +// [self stopEngine:nil]; +// } +} + +-(void)appWillTerminate:(NSNotification*)note { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIApplicationWillResignActiveNotification + object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIApplicationWillTerminateNotification + object:nil]; +} + +#pragma mark - SpeechEngineDelegate + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSLog(@"Message Type: %d.", type); + switch (type) { + case SEEngineStart: + [self speechEngineStarted]; + break; + case SEEngineStop: + [self speechEngineStopped]; + break; + case SEEngineError: + [self speechEngineError:data]; + break; + case SEVoiceCloneGetTaskResult: + [self speechVoiceCloneGetTaskResult:data]; + break; + case SEVoiceCloneCheckEnvResult: + [self speechVoiceCloneCheckEnvResult:data]; + break; + case SEVoiceCloneRecordVoiceResult: + [self speechVoiceCloneRecordVoiceResult:data]; + break; + case SEVoiceCloneQueryStatusResult: + [self speechVoiceCloneQueryStatusResult:data]; + break; + case SEVoiceCloneSubmitTaskResult: + [self speechVoiceCloneSubmitTaskResult:data]; + break; + case SEVoiceCloneDeleteDataResult: + [self speechVoiceCloneDeleteDataResult:data]; + break; + case SEEngineLog: + NSLog(@"engine log: %s", data.bytes); + break; + default: + break; + } +} + +#pragma mark - UI Actions +- (IBAction)switchEngine:(id)sender { + if (self.engineStarted) { + [self.statusTextView setText:@"Engine is busy, stop it first!"]; + return; + } + [self.referTextView setText:@""]; + self.voiceCloneTaskInfo = NULL; + [self updateParamJsonObj]; + if (self.engineInited) { + [self uninitEngine]; + self.engineInited = FALSE; + [self.statusTextView setText:@"Waiting for init."]; + [self switchViewStatus:BEFORE_INIT]; + } else { + [self switchViewStatus:INITING]; + [self initEngine]; + } +} + +- (IBAction)stopEngine:(id)sender { + [self.curEngine sendDirective:SEDirectiveStopEngine]; +} + +- (IBAction)getTask:(id)sender { + [self.curEngine setStringParam:[self.settings getString:SETTING_VOICECLONE_UID] forKey:SE_PARAMS_KEY_UID_STRING]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_VOICECLONE_GENDER] forKey:SE_PARAMS_KEY_VOICECLONE_GENDER_BOOL]; + [self.curEngine sendDirective:SEDirectiveVoiceCloneGetTask]; +} + +- (IBAction)checkEnv:(id)sender { + if (![self checkRecorder]) { + return; + } + [self.curEngine setStringParam:[self.settings getString:SETTING_VOICECLONE_UID] forKey:SE_PARAMS_KEY_UID_STRING]; + [self.curEngine setIntParam:self.voiceCloneCurTaskId forKey:SE_PARAMS_KEY_VOICECLONE_TASKID_INT]; + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveVoiceCloneCheckEnv]; + if (ret != SENoError) { + [self.resultTextView setText:[NSString stringWithFormat:@"checkEnv failed: %d", ret]]; + } else { + [self switchViewStatus:RECORDING]; + } +} + +- (IBAction)voiceRecord:(id)sender { + if (![self checkRecorder]) { + return; + } + [self.curEngine setStringParam:[self.settings getString:SETTING_VOICECLONE_UID] forKey:SE_PARAMS_KEY_UID_STRING]; + [self.curEngine setIntParam:self.voiceCloneCurTaskId forKey:SE_PARAMS_KEY_VOICECLONE_TASKID_INT]; + [self.curEngine setStringParam:self.voiceCloneCurText forKey:SE_PARAMS_KEY_VOICECLONE_TEXT_STRING]; + [self.curEngine setIntParam:self.voiceCloneCurTextSeq forKey:SE_PARAMS_KEY_VOICECLONE_TEXT_SEQ_INT]; + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveVoiceCloneRecordVoice]; + if (ret != SENoError) { + [self.resultTextView setText:[NSString stringWithFormat:@"voiceRecord failed: %d", ret]]; + } else { + [self switchViewStatus:RECORDING]; + } +} + +- (IBAction)finishTalking:(id)sender { + [self.curEngine sendDirective:SEDirectiveFinishTalking]; + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.streamRecorder stop]; + } +} + +- (IBAction)getTrainStatus:(id)sender { + [self.curEngine setStringParam:[self.settings getString:SETTING_VOICECLONE_UID] forKey:SE_PARAMS_KEY_UID_STRING]; + [self.curEngine setStringParam:[self.settings getString:SETTING_VOICECLONE_QUERY_UIDS] forKey:SE_PARAMS_KEY_VOICECLONE_QUERY_UIDS_STRING]; + [self.curEngine sendDirective:SEDirectiveVoiceCloneQueryStatus]; +} + +- (IBAction)submitTask:(id)sender { + [self.curEngine setStringParam:[self.settings getString:SETTING_VOICECLONE_UID] forKey:SE_PARAMS_KEY_UID_STRING]; + [self.curEngine setIntParam:self.voiceCloneCurTaskId forKey:SE_PARAMS_KEY_VOICECLONE_TASKID_INT]; + [self.curEngine setStringParam:[self.settings getString:SETTING_VOICECLONE_VOICE_TYPE] forKey:SE_PARAMS_KEY_VOICECLONE_VOICE_TYPE_STRING]; + [self.curEngine sendDirective:SEDirectiveVoiceCloneSubmitTask]; +} + +- (IBAction)delTrainData:(id)sender { + [self.curEngine setStringParam:[self.settings getString:SETTING_VOICECLONE_UID] forKey:SE_PARAMS_KEY_UID_STRING]; + [self.curEngine setIntParam:self.voiceCloneCurTaskId forKey:SE_PARAMS_KEY_VOICECLONE_TASKID_INT]; + [self.curEngine sendDirective:SEDirectiveVoiceCloneDeleteData]; +} + +- (IBAction)goToNextTaskInfo:(id)sender { + if (self.voiceCloneTaskInfo != NULL) { + NSArray *text_arr = [self.voiceCloneTaskInfo objectForKey:@"texts"]; + if (text_arr.count != 0) { + [self updateParamJsonObj:((int)self.voiceCloneCurTextSeq + 1) % text_arr.count]; + [self.resultTextView setText:@""]; + return; + } + } + [self.resultTextView setText:@"You should get task info sucessfully first!"]; +} + +#pragma mark - Init Methods + +- (void)initEngine { + AppDelegate *appDelegate = [ViewController getAppDelegate]; + if (appDelegate == nil) { + appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; + } + if (appDelegate.deviceID.length < 1) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:@"Waiting for get deviceID."]; + sleep(1); + [self initEngine]; + }); + return; + } + [ViewController setAppDelegate:appDelegate]; + self.deviceID = appDelegate.deviceID; + + + if (self.curEngine == nil) { + self.curEngine = [[SpeechEngine alloc] init]; + } + if (![self.curEngine createEngineWithDelegate:self]) { + NSLog(@"Create speech engine failed."); + return; + } + + NSLog(@"Engine version: %@", [self.curEngine getVersion]); + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"Debug path: %@", self.debugPath); + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + [self.curEngine setStringParam:[self.settings getString:SETTING_APPID] forKey:SE_PARAMS_KEY_APP_ID_STRING]; + [self.curEngine setStringParam:[self.settings getString:SETTING_TOKEN] forKey:SE_PARAMS_KEY_APP_TOKEN_STRING]; + [self.curEngine setStringParam:SE_LOG_LEVEL_TRACE forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + [self.curEngine setIntParam:1 forKey:SE_PARAMS_KEY_CHANNEL_NUM_INT]; + [self.curEngine setStringParam:[self.settings getString:SETTING_VOICECLONE_ADDRESS] forKey:SE_PARAMS_KEY_VOICECLONE_ADDRESS_STRING]; + [self.curEngine setStringParam:[self.settings getString:SETTING_VOICECLONE_STREAM_ADDRESS] forKey:SE_PARAMS_KEY_VOICECLONE_STREAM_ADDRESS_STRING]; + [self.curEngine setStringParam:[self getRecorderType] forKey:SE_PARAMS_KEY_RECORDER_TYPE_STRING]; + [self.curEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_VOICECLONE_ENABLE_DUMP_BOOL]; + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_VOICECLONE_REC_PATH_STRING]; + [self.curEngine setStringParam:SE_VOICECLONE_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + [self.curEngine setIntParam:[self.settings getInt:SETTING_SAMPLE_RATE] forKey:SE_PARAMS_KEY_SAMPLE_RATE_INT]; + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + if ([self.streamRecorder getSampleRate] != [self.settings getInt:SETTING_SAMPLE_RATE]) { + [self.curEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_ENABLE_RESAMPLER_BOOL]; + } + } + + SEEngineErrorCode ret = [self.curEngine initEngine]; + if (ret != SENoError) { + NSLog(@"Init Engine failed: %d", ret); + } + self.engineInited = (ret == SENoError); + if (self.engineInited) { + [self speechEngineInitOk]; + } else { + [self speechEngineInitFailed]; + } +} + +- (void)uninitEngine { + [self.curEngine destroyEngine]; + self.curEngine = nil; +} + +- (bool)checkRecorder { + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.curEngine setIntParam:[self.streamRecorder getSampleRate] forKey:SE_PARAMS_KEY_CUSTOM_SAMPLE_RATE_INT]; + if (![self.streamRecorder start]) { + [self speechEngineNoPermission]; + return false; + } + } else if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_FILE]) { + NSString* file_path = [NSString stringWithFormat:@"%@/%@", self.debugPath, @"voiceclone_rec_file.pcm"]; + NSLog(@"test file path: %@", file_path); + [self.curEngine setStringParam:file_path forKey:SE_PARAMS_KEY_RECORDER_FILE_STRING]; + } + return true; +} + +- (NSString *)getRecorderType { + SettingOptions* recorderTypeOptions = [self.settings getOptions:SETTING_RECORD_TYPE]; + switch (recorderTypeOptions.chooseIdx) { + case 0: + return SE_RECORDER_TYPE_RECORDER; + case 1: + return SE_RECORDER_TYPE_FILE; + case 2: + return SE_RECORDER_TYPE_STREAM; + default: + break; + } + return @""; +} + +#pragma mark - Engine Callback + +- (void)speechEngineNoPermission { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"No permission!"]; + }); +} + +- (void)speechEngineInitOk { + [self.streamRecorder setSpeechEngine:self.curEngine]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self switchViewStatus:BEFORE_GET_TASK]; + [self.statusTextView setText:@"Ready"]; + [self.resultTextView setText:[NSString stringWithFormat:@"DeviceID: %@", self.deviceID]]; + }); +} + +- (void)speechEngineInitFailed { + dispatch_async(dispatch_get_main_queue(), ^{ + [self switchViewStatus:BEFORE_INIT]; + [self uninitEngine]; + [self.statusTextView setText:@"Failed to init engine!"]; + }); +} + +- (void)speechEngineStarted { + dispatch_async(dispatch_get_main_queue(), ^{ + self.startEngineTimestamp = [[NSDate date] timeIntervalSince1970] * 1000; + self.engineStarted = true; + [self.statusTextView setText:@"Engine Started!"]; + }); +} + +- (void)speechEngineStopped { + dispatch_async(dispatch_get_main_queue(), ^{ + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.streamRecorder stop]; + } + self.engineStarted = FALSE; + [self.statusTextView setText:@"Engine Stopped!"]; + if (self.startEngineTimestamp > 0) { + long response_delay = [self timeDelayFrom:self.startEngineTimestamp]; + self.startEngineTimestamp = 0; + [self.resultTextView setText:[NSString stringWithFormat:@"%@\nVoiceClone cost: %ld\n", self.resultTextView.text, response_delay]]; + } + }); +} + +- (void) +speechEngineError:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]]; + [self stopEngine:nil]; + if (self.curViewStatus == RECORDING) { + [self switchViewStatus:self.preViewStatus]; + } + }); +} + +- (void) +speechEngineResult:(NSString *)text { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:text]; + }); +} + +- (void)speechVoiceCloneGetTaskResult:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + NSError *error; + NSArray *task_arr = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error]; + if (task_arr.count <= 0) { + [self.resultTextView setText:@"No task data!"]; + return; + } + [self.resultTextView setText:@""]; + self.voiceCloneCurTaskId = [self.settings getInt:SETTING_VOICECLONE_TASKID]; + for (int i = 0; i < task_arr.count; ++i) { + NSDictionary *task_info = [task_arr objectAtIndex:i]; + int task_id = [[task_info objectForKey:@"task_id"] intValue]; + if ((int)self.voiceCloneCurTaskId == task_id) { + self.voiceCloneTaskInfo = [NSMutableDictionary dictionaryWithDictionary:task_info]; + break; + } + } + // no match task_id + if (self.voiceCloneTaskInfo == NULL) { + self.voiceCloneTaskInfo = [task_arr objectAtIndex:0]; + [self.resultTextView setText:[NSString stringWithFormat:@"Find task %d failed! Choose task: %d", (int)self.voiceCloneCurTaskId, [[self.voiceCloneTaskInfo objectForKey:@"task_id"] intValue]]]; + } + // update task progress + int prog = [[self.voiceCloneTaskInfo objectForKey:@"progress"] intValue]; + NSArray *texts_arr = [self.voiceCloneTaskInfo objectForKey:@"texts"]; + [self updateParamJsonObj:(prog % texts_arr.count)]; + if (prog < texts_arr.count && prog >= 0) { + [self.resultTextView setText:[NSString stringWithFormat:@"%@\nGet task info success!\nRecord task unfinished.\nGo to check environment", self.resultTextView.text]]; + [self switchViewStatus:BEFORE_CHECK_ENV]; + } else { + [self.resultTextView setText:[NSString stringWithFormat:@"%@\nGet task info success!\nRecord task finished!\nGo to submit task or query train status.", self.resultTextView.text]]; + [self switchViewStatus:AFTER_SUBMIT]; + } + }); +} + +- (void)speechVoiceCloneCheckEnvResult:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + NSError *error; + NSDictionary *env_info = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error]; + int status = [[env_info objectForKey:@"status"] intValue]; + if (status > 0) { + [self.resultTextView setText:[NSString stringWithFormat:@"Checking...\nCurrent status: %d\nCurrent noise: %lf", status, [[env_info objectForKey:@"noise"] doubleValue]]]; + } else if (status < 0) { + [self.resultTextView setText:[NSString stringWithFormat:@"Check failed! status: %d", status]]; + [self switchViewStatus:BEFORE_CHECK_ENV]; + } else { + [self.resultTextView setText:[NSString stringWithFormat:@"Check success! status: %d", status]]; + [self switchViewStatus:BEFORE_RECORD]; + } + + }); +} + +- (void)speechVoiceCloneRecordVoiceResult:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + NSError *error; + NSDictionary *record_res = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error]; + int status = [[record_res objectForKey:@"status"] intValue]; + if (status > 0) { + } else if (status < 0) { + [self.resultTextView setText:[NSString stringWithFormat:@"Check failed! status: %d, wrong_index: %@", status, [record_res objectForKey:@"wrong_index"]]]; + [self switchViewStatus:BEFORE_RECORD]; + } else { + [self.resultTextView setText:[NSString stringWithFormat:@"Check success! status: %d", status]]; + NSArray* text_arr = [self.voiceCloneTaskInfo objectForKey:@"texts"]; + if (text_arr.count > 0 && self.voiceCloneCurTextSeq + 1 < text_arr.count) { + [self.resultTextView setText:[NSString stringWithFormat:@"Text %d success! status: %d", (int)self.voiceCloneCurTextSeq, status]]; + [self switchViewStatus:BEFORE_RECORD]; + } else { + [self.resultTextView setText:[NSString stringWithFormat:@"Voice record task: %d finished!\nGo to submit task!", (int)self.voiceCloneCurTaskId]]; + [self switchViewStatus:BEFORE_SUBMIT]; + } + } + }); +} + +- (void)speechVoiceCloneQueryStatusResult:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + NSError *error; + NSArray *train_status_arr = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error]; + [self.resultTextView setText:@""]; + for (int i = 0; i < train_status_arr.count; ++i) { + NSDictionary *item = [train_status_arr objectAtIndex:i]; + NSString *uid = [item objectForKey:@"uid"]; + int status = [[item objectForKey:@"status"] intValue]; + int task_id = [[item objectForKey:@"task_id"] intValue]; + NSString *extra = [item objectForKey:@"extra"]; + [self.resultTextView setText:[NSString stringWithFormat:@"%@%@: status: %d; task_id: %d; extra: %@\n\n", self.resultTextView.text, uid, status, task_id, extra]]; + } + }); +} + +- (void)speechVoiceCloneSubmitTaskResult:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + NSError *error; + NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error]; + NSString *voiceType = [result objectForKey:@"voice_type"]; + [self.resultTextView setText:[NSString stringWithFormat:@"Task submit success, voice type: %@", voiceType]]; + [self switchViewStatus:AFTER_SUBMIT]; + }); +} + +- (void)speechVoiceCloneDeleteDataResult:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:@"Task data del success"]; + }); +} + +#pragma mark - Helper + +- (long)timeDelayFrom:(long)pastTimestamp { + return [[NSDate date] timeIntervalSince1970] * 1000 - pastTimestamp; +} + +#pragma mark - UITextViewDelegate + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if([text isEqualToString:@"\n"]) { + [textView resignFirstResponder]; + return NO; + } + return YES; +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_VOICECLONE forKey:@"viewId"]; +} + +@end + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/VoiceConvViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/VoiceConvViewController.h new file mode 100644 index 0000000..2f77be3 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/VoiceConvViewController.h @@ -0,0 +1,15 @@ +// +// VoiceConvViewController.h +// SpeechDemo +// +// Created by chengzihao.ds on 2021/4/1. +// Copyright © 2021 chengzihao.ds. All rights reserved. +// + +#import + +@interface VoiceConvViewController : UIViewController + +@end + + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/VoiceConvViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/VoiceConvViewController.m new file mode 100644 index 0000000..5c4894a --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/VoiceConvViewController.m @@ -0,0 +1,432 @@ +// +// VoiceConvViewController.m +// SpeechDemo +// +// Created by chengzihao.ds on 2021/4/1. +// Copyright © 2021 chengzihao.ds. All rights reserved. +// + +#import +#import "VoiceConvViewController.h" + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" +#import "SensitiveDefines.h" + +@interface VoiceConvViewController () + +@property (weak, nonatomic) IBOutlet UITextView *referTextView; +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; +@property (weak, nonatomic) IBOutlet UIButton *engineSwitchButton; +@property (weak, nonatomic) IBOutlet UIButton *startEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *finishTalkingButton; + +@property (strong, nonatomic) SpeechEngine *curEngine; +@property (assign, nonatomic) BOOL signalEnabled; +@property (assign, nonatomic) BOOL engineInited; +@property (assign, nonatomic) BOOL engineStarted; + +@property (nonatomic, strong) NSString *deviceID; +@property (nonatomic, assign) long talkingFinisheTimestamp; +@property (nonatomic, assign) long startEngineTimestamp; +@property (strong, nonatomic) NSString *debugPath; + +// ViewStatus +typedef enum ViewStatus : NSUInteger { + BEFORE_INIT, + INITING, + BEFORE_START, + RECORDING, + WAITING_RESULT, +} ViewStatus; + +// Recorder +@property (weak, nonatomic) StreamRecorder *streamRecorder; +@property (strong, nonatomic) NSString *recFilePath; +@property (nonatomic, assign) long curResLength; +@property (nonatomic, assign) long recFileLength; + +// settings +@property (strong, nonatomic) Settings *settings; + +@end + +@implementation VoiceConvViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_VOICECONV]; + + [self switchViewStatus:BEFORE_INIT]; + [self decorateTextView:self.referTextView]; + [self decorateTextView:self.resultTextView]; + [self.referTextView setDelegate:self]; + self.referTextView.editable = FALSE; + + self.engineInited = FALSE; + self.engineStarted = FALSE; + self.streamRecorder = [ViewController getStreamRecorder]; + [self.statusTextView setText:@"Waiting for init."]; + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(appDidEnterBackground:) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(appWillTerminate:) + name:UIApplicationWillTerminateNotification + object:nil]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [self uninitEngine]; + [super viewDidDisappear:animated]; +} + +- (void)decorateTextView:(UITextView *)textView { + textView.layer.cornerRadius = 5.0f; + textView.layer.borderWidth = .25f; + textView.layer.borderColor = [UIColor grayColor].CGColor; +} + +- (void)switchViewStatus:(ViewStatus) status { + switch (status) { + case BEFORE_INIT: + [self.engineSwitchButton setTitle:@"InitEngine" forState:UIControlStateNormal]; + self.engineSwitchButton.enabled = TRUE; + self.startEngineButton.enabled = FALSE; + self.finishTalkingButton.enabled = FALSE; + break; + case INITING: + [self.engineSwitchButton setTitle:@"InitEngine" forState:UIControlStateNormal]; + self.engineSwitchButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.finishTalkingButton.enabled = FALSE; + break; + case BEFORE_START: + [self.engineSwitchButton setTitle:@"UninitEngine" forState:UIControlStateNormal]; + self.engineSwitchButton.enabled = TRUE; + self.startEngineButton.enabled = TRUE; + self.finishTalkingButton.enabled = FALSE; + break; + case RECORDING: + [self.engineSwitchButton setTitle:@"UninitEngine" forState:UIControlStateNormal]; + self.engineSwitchButton.enabled = TRUE; + self.startEngineButton.enabled = FALSE; + self.finishTalkingButton.enabled = TRUE; + break; + case WAITING_RESULT: + [self.engineSwitchButton setTitle:@"UninitEngine" forState:UIControlStateNormal]; + self.engineSwitchButton.enabled = TRUE; + self.startEngineButton.enabled = FALSE; + self.finishTalkingButton.enabled = FALSE; + break; + default: + break; + } +} + +#pragma mark - Notifications + +- (void)appDidEnterBackground:(UIApplication *)application; { +// if (self.engineStarted) { +// [self stopEngine:nil]; +// } +} + +-(void)appWillTerminate:(NSNotification*)note { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIApplicationWillResignActiveNotification + object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIApplicationWillTerminateNotification + object:nil]; +} + +#pragma mark - SpeechEngineDelegate + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSLog(@"Message Type: %d.", type); + switch (type) { + case SEEngineStart: + [self speechEngineStarted]; + break; + case SEEngineStop: + [self speechEngineStopped:data]; + break; + case SEEngineError: + [self speechEngineError:data]; + break; + case SEVoiceConvResultAudio: + [self speechResultAudio:data]; + break; + case SEEngineLog: + NSLog(@"engine log: %s", data.bytes); + break; + default: + break; + } +} + +#pragma mark - UI Actions +- (IBAction)switchEngine:(id)sender { + if (self.engineStarted) { + [self.statusTextView setText:@"Engine is busy, stop it first!"]; + return; + } + + if (self.engineInited) { + [self switchViewStatus:BEFORE_INIT]; + [self uninitEngine]; + self.engineInited = FALSE; + [self.statusTextView setText:@"Waiting for init."]; + [self.engineSwitchButton setTitle:@"InitEngine" forState:UIControlStateNormal]; + } else { + [self switchViewStatus:INITING]; + [self initEngine]; + } +} + +- (IBAction)startEngine:(id)sender { + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.curEngine setIntParam:[self.streamRecorder getSampleRate] forKey:SE_PARAMS_KEY_CUSTOM_SAMPLE_RATE_INT]; + if (![self.streamRecorder start]) { + [self speechEngineNoPermission]; + return; + } + } else if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_FILE]) { + // get file length + NSFileManager* manager = [NSFileManager defaultManager]; + if ([manager fileExistsAtPath:self.recFilePath]){ + self.recFileLength = [[manager attributesOfItemAtPath:self.recFilePath error:nil] fileSize]; + } + NSLog(@"Open record file: %@, length: %ld", self.recFilePath, self.recFileLength); + self.curResLength = 0; + [self.curEngine setStringParam:self.recFilePath forKey:SE_PARAMS_KEY_RECORDER_FILE_STRING]; + } + + [self.curEngine setStringParam:SDEF_UID forKey:SE_PARAMS_KEY_UID_STRING]; + [self.curEngine setStringParam:[self.settings getString:SETTING_VOICE] forKey:SE_PARAMS_KEY_VOICECONV_VOICE_STRING]; + [self.curEngine setStringParam:[self.settings getString:SETTING_VOICE_TYPE] forKey:SE_PARAMS_KEY_VOICECONV_VOICE_TYPE_STRING]; + [self.curEngine setIntParam:[self.settings getInt:SETTING_VOICECONV_RESULT_SAMPLE_RATE] forKey:SE_PARAMS_KEY_VOICECONV_RESULT_SAMPLE_RATE_INT]; + [self.curEngine setIntParam:[self.settings getInt:SETTING_VOICECONV_REQUEST_INTERVAL] forKey:SE_PARAMS_KEY_VOICECONV_REQUEST_INTERVAL_INT]; + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + + if (ret == SENoError) { + [self switchViewStatus:RECORDING]; + } else { + [self.resultTextView setText:[NSString stringWithFormat:@"Start engine failed! ret: %d", ret]]; + } +} + +- (IBAction)stopEngine:(id)sender { + [self.curEngine sendDirective:SEDirectiveStopEngine]; + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.streamRecorder stop]; + } +} + +- (IBAction)finishTalking:(id)sender { + [self.curEngine sendDirective:SEDirectiveFinishTalking]; + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.streamRecorder stop]; + } + [self switchViewStatus:WAITING_RESULT]; +} + +#pragma mark - Init Methods + +- (void)initEngine { + AppDelegate *appDelegate = [ViewController getAppDelegate]; + if (appDelegate == nil) { + appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; + } + if (appDelegate.deviceID.length < 1) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:@"Waiting for get deviceID."]; + sleep(1); + [self initEngine]; + }); + return; + } + [ViewController setAppDelegate:appDelegate]; + self.deviceID = appDelegate.deviceID; + + + if (self.curEngine == nil) { + self.curEngine = [[SpeechEngine alloc] init]; + } + if (![self.curEngine createEngineWithDelegate:self]) { + NSLog(@"Create speech engine failed."); + return; + } + + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + self.recFilePath = [NSString stringWithFormat:@"%@/%@", self.debugPath, @"voiceconv_rec_file.pcm"]; + NSLog(@"Engine version: %@", [self.curEngine getVersion]); + NSLog(@"Debug path: %@", self.debugPath); + + // recorder + SettingOptions *options = [self.settings getOptions:SETTING_RECORD_TYPE]; + + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + [self.curEngine setStringParam:SE_LOG_LEVEL_TRACE forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + [self.curEngine setStringParam:@"test" forKey:SE_PARAMS_KEY_APP_ID_STRING]; + [self.curEngine setIntParam:1 forKey:SE_PARAMS_KEY_CHANNEL_NUM_INT]; + [self.curEngine setIntParam:16000 forKey:SE_PARAMS_KEY_SAMPLE_RATE_INT]; + [self.curEngine setStringParam:[self getRecorderType] forKey:SE_PARAMS_KEY_RECORDER_TYPE_STRING]; + [self.curEngine setStringParam:SE_VOICECONV_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + + [self.curEngine setStringParam:@"wss://speech-test.bytedance.com" forKey:SE_PARAMS_KEY_VOICECONV_ADDRESS_STRING]; + [self.curEngine setStringParam:@"/api/v1/voice_conv/ws" forKey:SE_PARAMS_KEY_VOICECONV_URI_STRING]; + [self.curEngine setStringParam:@"default" forKey:SE_PARAMS_KEY_VOICECONV_CLUSTER_STRING]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_VOICECONV_ENABLE_RECORD_DUMP] forKey:SE_PARAMS_KEY_VOICECONV_ENABLE_RECORD_DUMP_BOOL]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_VOICECONV_ENABLE_RESULT_DUMP] forKey:SE_PARAMS_KEY_VOICECONV_ENABLE_RESULT_DUMP_BOOL]; + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_VOICECONV_AUDIO_PATH_STRING]; + + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + if ([self.streamRecorder getSampleRate] != 16000) { + [self.curEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_ENABLE_RESAMPLER_BOOL]; + } + } + + SEEngineErrorCode ret = [self.curEngine initEngine]; + if (ret != SENoError) { + NSLog(@"Init Engine failed: %d", ret); + } + self.engineInited = (ret == SENoError); + if (self.engineInited) { + [self speechEngineInitOk]; + } else { + [self speechEngineInitFailed]; + } +} + +- (void)uninitEngine { + [self.curEngine destroyEngine]; + self.curEngine = nil; +} + +- (NSString *)getRecorderType { + SettingOptions* recorderTypeOptions = [self.settings getOptions:SETTING_RECORD_TYPE]; + switch (recorderTypeOptions.chooseIdx) { + case 0: + return SE_RECORDER_TYPE_RECORDER; + case 1: + return SE_RECORDER_TYPE_FILE; + case 2: + return SE_RECORDER_TYPE_STREAM; + default: + break; + } + return @""; +} + +#pragma mark - Engine Callback + +- (void)speechEngineNoPermission { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"No permission!"]; + }); +} + +- (void)speechEngineInitOk { + [self.streamRecorder setSpeechEngine:self.curEngine]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self switchViewStatus:BEFORE_START]; + [self.statusTextView setText:@"Ready"]; + [self.resultTextView setText:[NSString stringWithFormat:@"DeviceID: %@", self.deviceID]]; + }); +} + +- (void)speechEngineInitFailed { + dispatch_async(dispatch_get_main_queue(), ^{ + [self switchViewStatus:BEFORE_INIT]; + [self uninitEngine]; + [self.statusTextView setText:@"Failed to init engine!"]; + }); +} + +- (void)speechEngineStarted { + dispatch_async(dispatch_get_main_queue(), ^{ + self.startEngineTimestamp = [[NSDate date] timeIntervalSince1970] * 1000; + self.engineStarted = true; + [self.statusTextView setText:@"Engine Started!"]; + }); +} + +- (void)speechEngineStopped:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_STREAM]) { + [self.streamRecorder stop]; + } + // record result file + NSString* reqId = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]; + NSString* resultFile = [NSString stringWithFormat:@"voiceconv_%@.wav", reqId]; + + self.engineStarted = FALSE; + [self.statusTextView setText:@"Engine Stopped!"]; + if (self.startEngineTimestamp > 0) { + long response_delay = [self timeDelayFrom:self.startEngineTimestamp]; + self.startEngineTimestamp = 0; + [self.resultTextView setText:[NSString stringWithFormat:@"%@\nVoiceConv cost: %ld\nResult File: %@", self.resultTextView.text, response_delay, resultFile]]; + } + [self switchViewStatus:BEFORE_START]; + }); +} + +- (void)speechEngineError:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]]; + [self stopEngine:nil]; + }); +} + +- (void)speechResultAudio:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[NSString stringWithFormat:@"Get audio data, size: %ld", data.length]]; + + // calculate progress + if ([[self getRecorderType] isEqualToString:SE_RECORDER_TYPE_FILE] && self.recFileLength != 0) { + self.curResLength += data.length; + long inputSampleRate = 16000; + long outputSampleRate = [self.settings getInt:SETTING_VOICECONV_RESULT_SAMPLE_RATE]; + double progress = (double) self.curResLength / self.recFileLength * inputSampleRate / outputSampleRate; + [self.resultTextView setText:[NSString stringWithFormat:@"%@\nCurrent result length: %ld, total file length: %ld, progress: %lf", self.resultTextView.text, self.curResLength, self.recFileLength, progress]]; + + } + }); +} + +#pragma mark - Helper + +- (long)timeDelayFrom:(long)pastTimestamp { + return [[NSDate date] timeIntervalSince1970] * 1000 - pastTimestamp; +} + +#pragma mark - UITextViewDelegate + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if([text isEqualToString:@"\n"]) { + [textView resignFirstResponder]; + return NO; + } + return YES; +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_VOICECONV forKey:@"viewId"]; +} + +@end + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/delegate/AppDelegate.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/delegate/AppDelegate.h new file mode 100644 index 0000000..0b40e61 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/delegate/AppDelegate.h @@ -0,0 +1,17 @@ +// +// AppDelegate.h +// SpeechDemo +// +// Created by fangweiwei on 2019/12/6. +// Copyright © 2019 fangweiwei. All rights reserved. +// + +#import +#import "SpeechEngineToB/SpeechEngineToB-umbrella.h" + +@interface AppDelegate : UIResponder + +@property(nonatomic, strong) UIWindow *window; +@property(nonatomic, readonly, strong) NSString *deviceID; + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/delegate/AppDelegate.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/delegate/AppDelegate.m new file mode 100644 index 0000000..a162bf1 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/delegate/AppDelegate.m @@ -0,0 +1,64 @@ +// +// AppDelegate.m +// SpeechDemo +// +// Created by fangweiwei on 2019/12/6. +// Copyright © 2019 fangweiwei. All rights reserved. +// + +#import "AppDelegate.h" +#import "SensitiveDefines.h" + + + +@interface AppDelegate () + +@property(nonatomic, readwrite, strong) NSString *deviceID; + +@end + +@implementation AppDelegate + + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + UIDevice *device = [UIDevice currentDevice]; + self.deviceID = [[device identifierForVendor] UUIDString]; + + // 完成网络环境等相关依赖配置,只需要调用一次。 + BOOL status = [SpeechEngine prepareEnvironment]; + if (status) { + [self setupResourceManager]; + } + return status; +} + + +- (void) setupResourceManager { + NSLog(@"初始化模型资源管理器"); + SpeechResourceManager *speechResourceManager = [SpeechResourceManager shareInstance]; + [speechResourceManager setAppId:SDEF_APPID]; + [speechResourceManager setAppVersion:@"1.0.0"]; + [speechResourceManager setDeviceId:self.deviceID]; + [speechResourceManager setRootPath: [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"models"]]; + [speechResourceManager setOnlineModelEnable:YES]; + [speechResourceManager setup]; +} + +#pragma mark - UISceneSession lifecycle + + +- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options API_AVAILABLE(ios(13.0)){ + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; +} + + +- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions API_AVAILABLE(ios(13.0)){ + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. +} + + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/delegate/SceneDelegate.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/delegate/SceneDelegate.h new file mode 100644 index 0000000..8b12afd --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/delegate/SceneDelegate.h @@ -0,0 +1,16 @@ +// +// SceneDelegate.h +// SpeechDemo +// +// Created by fangweiwei on 2019/12/6. +// Copyright © 2019 fangweiwei. All rights reserved. +// + +#import + +@interface SceneDelegate : UIResponder + +@property (strong, nonatomic) UIWindow * window; + +@end + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/delegate/SceneDelegate.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/delegate/SceneDelegate.m new file mode 100644 index 0000000..bc0bc3e --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/delegate/SceneDelegate.m @@ -0,0 +1,50 @@ +#import "SceneDelegate.h" + +@interface SceneDelegate () + +@end + +@implementation SceneDelegate + + +- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions API_AVAILABLE(ios(13.0)){ + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). +} + + +- (void)sceneDidDisconnect:(UIScene *)scene API_AVAILABLE(ios(13.0)){ + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). +} + + +- (void)sceneDidBecomeActive:(UIScene *)scene API_AVAILABLE(ios(13.0)){ + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. +} + + +- (void)sceneWillResignActive:(UIScene *)scene API_AVAILABLE(ios(13.0)){ + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). +} + + +- (void)sceneWillEnterForeground:(UIScene *)scene API_AVAILABLE(ios(13.0)){ + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. +} + + +- (void)sceneDidEnterBackground:(UIScene *)scene API_AVAILABLE(ios(13.0)){ + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. +} + + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/SettingViewDelegate.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/SettingViewDelegate.h new file mode 100644 index 0000000..73905b3 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/SettingViewDelegate.h @@ -0,0 +1,67 @@ +// +// SettingViewDelegate.h +// SpeechDemo +// +// Created by bytedance on 2021/3/26. +// Copyright © 2021 chengzihao.ds. All rights reserved. +// + +#import +#import +#import "Settings.h" + +#pragma mark - SettingItemCell +@interface SettingItemCell : UITableViewCell +@property (weak, nonatomic) Settings *settings; +@property (weak, nonatomic) UITableView* parent; + +-(void)init:(Settings*)settings parent:(UITableView*)parent; +-(SettingItem*)getCorrespondItem; + +- (BOOL)textFieldShouldReturn:(UITextField *)textField; +@end + +#pragma mark - SettingBoolCell +@interface SettingBoolCell : SettingItemCell +@property (weak, nonatomic) IBOutlet UILabel *keyLabel; +@property (weak, nonatomic) IBOutlet UISwitch *valSwitch; + +-(void)switchAction:(UISwitch*)sender; +@end + +#pragma mark - SettingIntCell +@interface SettingNumberCell : SettingItemCell +@property (weak, nonatomic) IBOutlet UILabel *keyLabel; +@property (weak, nonatomic) IBOutlet UITextField *valTextField; + +-(void)textFieldDidChange :(UITextField *)textField; +@end + +#pragma mark - SettingStringCell +@interface SettingStringCell : SettingItemCell +@property (weak, nonatomic) IBOutlet UILabel *keyLabel; +@property (weak, nonatomic) IBOutlet UITextField *valTextField; + +-(void)textFieldDidChange :(UITextField *)textField; +@end + +#pragma mark - SettingOptionsCell +@interface SettingOptionsCell : SettingItemCell + +@property (weak, nonatomic) IBOutlet UILabel *keyLabel; +@property (weak, nonatomic) IBOutlet UITextField *valTextField; +@property (strong, nonatomic) UIPickerView *pickerView; +@property (strong, nonatomic) UIToolbar *pickerToolbar; + +-(void)pickerConfirm; +-(void)pickerCancel; +@end + +#pragma mark - SettingViewDelegate +@interface SettingViewDelegate : NSObject +@property (strong, nonatomic) Settings* settings; + ++ (instancetype)build:(Settings*)settings; + +@end + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/SettingViewDelegate.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/SettingViewDelegate.m new file mode 100644 index 0000000..9aaa002 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/SettingViewDelegate.m @@ -0,0 +1,280 @@ +// +// SettingViewDelegate.m +// SpeechDemo +// +// Created by bytedance on 2021/3/26. +// Copyright © 2021 chengzihao.ds. All rights reserved. +// + +#import +#import "SettingViewDelegate.h" + +#pragma mark - SettingItemCell +@implementation SettingItemCell + +-(void)init:(Settings*)settings parent:(UITableView*)parent { + self.settings = settings; + self.parent = parent; +} + +-(SettingItem*)getCorrespondItem { + NSIndexPath* path = [self.parent indexPathForCell:self]; + NSMutableArray* group = self.settings.configs[path.section]; + return group[path.row]; +} + +// auto close keyboard when click return +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + [textField resignFirstResponder]; + return NO; +} +@end + + +#pragma mark - SettingBoolCell +@implementation SettingBoolCell + +-(void)init:(Settings*)settings parent:(UITableView*)parent { + [super init:settings parent:parent]; + [self.valSwitch addTarget:self action:@selector(switchAction:) forControlEvents:UIControlEventValueChanged]; +} + +-(void)switchAction:(UISwitch*)sender { + SettingItem* item = [self getCorrespondItem]; + [self.settings setBool:item.key val:sender.isOn]; +} + +@end + +#pragma mark - SettingNumberCell +@implementation SettingNumberCell + +-(void)init:(Settings*)settings parent:(UITableView*)parent { + [super init:settings parent:parent]; + self.valTextField.delegate = self; + [self.valTextField addTarget:self action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged]; +} + +-(void)textFieldDidChange :(UITextField *)textField { + SettingItem* item = [self getCorrespondItem]; + // if input empty, do nothing + if ([textField.text length] > 0) { + [self.settings setDouble:item.key val:[textField.text doubleValue]]; + } +} + +@end + +#pragma mark - SettingStringCell +@implementation SettingStringCell + +-(void)init:(Settings*)settings parent:(UITableView*)parent { + [super init:settings parent:parent]; + self.valTextField.delegate = self; + [self.valTextField addTarget:self action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged]; +} + +-(void)textFieldDidChange :(UITextField *)textField { + SettingItem* item = [self getCorrespondItem]; + [self.settings setString:item.key val:textField.text]; +} + +@end + +#pragma mark - SettingOptionsCell +@implementation SettingOptionsCell + +-(void)init:(Settings*)settings parent:(UITableView*)parent { + [super init:settings parent:parent]; + + // init picker view + self.pickerView = [[UIPickerView alloc] init]; + self.pickerView.dataSource = self; + self.pickerView.delegate = self; + + // init picker toolbar + self.pickerToolbar = [[UIToolbar alloc]initWithFrame: + CGRectMake(0, self.frame.size.height- + self.pickerView.frame.size.height-20, self.bounds.size.width, 40)]; + UIBarButtonItem *confirmBtn = [[UIBarButtonItem alloc] initWithTitle:@"Confirm" style:UIBarButtonItemStyleDone target:self action:@selector(pickerConfirm)]; + UIBarButtonItem *flexibleBBI = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; + [self.pickerToolbar setBarStyle:UIBarStyleDefault]; + NSArray *toolbarItems = [NSArray arrayWithObjects:flexibleBBI, confirmBtn, nil]; + [self.pickerToolbar setItems:toolbarItems]; + + // show pickerView + self.valTextField.delegate = self; + self.valTextField.inputView = self.pickerView; + self.valTextField.inputAccessoryView = self.pickerToolbar; +} + +// PickerView width +- (CGFloat)pickerView:(UIPickerView *)pickerView widthForComponent:(NSInteger)component { + return self.bounds.size.width; +} + +// PickerView select event +- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component { + SettingItem* item = [self getCorrespondItem]; + SettingOptions* options = (SettingOptions*)item.value; + options.chooseIdx = (int)row; + [self.settings setOptions:item.key val:options]; + [self.valTextField setText:options.optionsArray[row]]; +} + +// PickerView show options +-(NSString*)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component { + SettingItem* item = [self getCorrespondItem]; + SettingOptions* options = (SettingOptions*)item.value; + return options.optionsArray[row]; +} + +// PickerView column number +- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView { + return 1; +} + +// PickerView row number for each column +- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component { + SettingItem* item = [self getCorrespondItem]; + return ((SettingOptions*)item.value).optionsArray.count; +} + +// PickerToolbar confirm button click event +-(void)pickerConfirm { + if ([self.valTextField isFirstResponder]) { + [self.valTextField resignFirstResponder]; + } +} +@end + +#pragma mark - SettingViewDelegate + +@implementation SettingViewDelegate + +static NSString* nibName = @"SpeechSettingItem"; +static CGFloat sectionHeaderHeight = 30; +static CGFloat sectionFooterHeight = 30; + ++ (instancetype)build:(Settings*)settings { + SettingViewDelegate* instance = [[self alloc] init]; + instance.settings = settings; + return instance; +} + +// row number for each section +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + NSMutableArray *curGroup = _settings.configs[section]; + NSLog(@"numberOfRowsInSection %ld", curGroup.count); + return curGroup.count; +} + +// section number +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + NSLog(@"numberOfSectionsInTableView %ld", _settings.groups.count); + return _settings.groups.count; +} + +// section title +- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { + SettingItem *groupItem = _settings.groups[section]; + return groupItem.key; +} + +// section header height +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { + return sectionHeaderHeight; +} + +// section footer height +- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section { + return sectionFooterHeight; +} + +// generate TableViewCell +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + UITableViewCell* cell; + NSMutableArray* group = _settings.configs[indexPath.section]; + SettingItem* item = group[indexPath.row]; + + NSString *CellTableIndentifier = NULL; + switch (item.type) { + case kSettingBool: + { + CellTableIndentifier = @"SettingBool"; + SettingBoolCell* bCell = [tableView dequeueReusableCellWithIdentifier:CellTableIndentifier]; + if(bCell == nil) { + NSArray * nib = [[NSBundle mainBundle] loadNibNamed:nibName owner:self options:nil]; + bCell = [nib objectAtIndex:item.type]; + [bCell init:_settings parent:tableView]; + } + [bCell.keyLabel setText:item.key]; + [bCell.valSwitch setOn:[(NSNumber*)item.value intValue] == 1]; + + cell = bCell; + } + break; + case kSettingNumber: + { + CellTableIndentifier = @"SettingNumber"; + SettingNumberCell *iCell = [tableView dequeueReusableCellWithIdentifier:CellTableIndentifier]; + if(iCell == nil) { + NSArray * nib = [[NSBundle mainBundle] loadNibNamed:nibName owner:self options:nil]; + iCell = [nib objectAtIndex:item.type]; + [iCell init:_settings parent:tableView]; + } + [iCell.keyLabel setText:item.key]; + [iCell.valTextField setText:[(NSNumber*)item.value stringValue]]; + [iCell.valTextField setPlaceholder:(NSString*)item.hint]; + + cell = iCell; + } + break; + case kSettingString: + { + CellTableIndentifier = @"SettingString"; + SettingStringCell *sCell = [tableView dequeueReusableCellWithIdentifier:CellTableIndentifier]; + if(sCell == nil) { + NSArray * nib = [[NSBundle mainBundle] loadNibNamed:nibName owner:self options:nil]; + sCell = [nib objectAtIndex:item.type]; + [sCell init:_settings parent:tableView]; + } + [sCell.keyLabel setText:item.key]; + [sCell.valTextField setText:(NSString*)item.value]; + [sCell.valTextField setPlaceholder:(NSString*)item.hint]; + + cell = sCell; + } + break; + case kSettingOptions: + { + CellTableIndentifier = @"SettingOptions"; + SettingOptionsCell *oCell = [tableView dequeueReusableCellWithIdentifier:CellTableIndentifier]; + if(oCell == nil) { + NSArray * nib = [[NSBundle mainBundle] loadNibNamed:nibName owner:self options:nil]; + oCell = [nib objectAtIndex:item.type]; + [oCell init:_settings parent:tableView]; + } + SettingOptions *options = (SettingOptions*)item.value; + [oCell.keyLabel setText:item.key]; + [oCell.valTextField setText:options.optionsArray[options.chooseIdx]]; + [oCell.valTextField setPlaceholder:item.hint]; + + cell = oCell; + } + break; + default: + NSLog(@"Unsupported SettingType: %ld", item.type); + break; + } + cell.accessibilityIdentifier = item.key; + return cell; +} + +@synthesize description; + +@synthesize hash; + +@synthesize superclass; + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/Settings.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/Settings.h new file mode 100644 index 0000000..da0b6b5 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/Settings.h @@ -0,0 +1,83 @@ +// +// Settings.h +// SpeechDemo +// +// Created by bytedance on 2021/3/26. +// Copyright © 2021 chengzihao.ds. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN +#pragma mark - SettingType +typedef NS_ENUM(NSInteger, SettingType) { + kSettingGroup = 0, + kSettingBool = 1, + kSettingNumber = 2, + kSettingString = 3, + kSettingOptions = 4, +}; + +#pragma mark - SettingOptions +@interface SettingOptions : NSObject +@property (strong, nonatomic) NSArray* optionsArray; +@property (assign, nonatomic) int chooseIdx; + ++ (instancetype)build:(NSArray*)ops choose:(int)idx; +@end + +#pragma mark - SettingItem +@interface SettingItem : NSObject +@property (assign, nonatomic) SettingType type; +@property (strong, nonatomic) NSString* key; +@property (strong, nonatomic) NSObject* value; +@property (strong, nonatomic) NSString* hint; + ++ (instancetype)build:(SettingType)type key:(NSString*)key val:(NSObject*)val hint:(NSString*)hint; + ++ (instancetype)buildGroup:(NSString*)key val:(NSString*)val hint:(NSString*)hint; ++ (instancetype)buildBool:(NSString*)key val:(BOOL)val hint:(NSString*)hint; ++ (instancetype)buildInt:(NSString *)key val:(int)val hint:(NSString *)hint; ++ (instancetype)buildDouble:(NSString *)key val:(double)val hint:(NSString *)hint; ++ (instancetype)buildString:(NSString*)key val:(NSString*)val hint:(NSString*)hint; ++ (instancetype)buildOptions:(NSString*)key val:(SettingOptions*)val hint:(NSString*)hint; +@end + +#pragma mark - Settings +@interface Settings : NSObject +// kSettingGroup type, use SettingItem 1 dimension array. +@property (strong, nonatomic) NSMutableArray* groups; +// other type, use SettingItem 2 dimension array. 1st dimension for each group, 2nd dimension for detail SettingItem in group. +@property (strong, nonatomic) NSMutableArray* configs; + ++ (instancetype)build; + +- (void)registerItems:(NSArray*)cfgs; + +- (void)setBool:(NSString*)key; +- (void)setBool:(NSString*)key val:(BOOL)val; +- (void)setInt:(NSString*)key; +- (void)setInt:(NSString*)key val:(int)val; +- (void)setDouble:(NSString*)key; +- (void)setDouble:(NSString*)key val:(double)val; +- (void)setString:(NSString*)key; +- (void)setString:(NSString*)key val:(NSString*)val; +- (void)setOptions:(NSString*)key; +- (void)setOptions:(NSString*)key val:(SettingOptions*)val; + +- (BOOL)getBool:(NSString*)key; +- (BOOL)getBool:(NSString*)key def:(BOOL)def; +- (int)getInt:(NSString*)key; +- (int)getInt:(NSString*)key def:(int)def; +- (double)getDouble:(NSString*)key; +- (double)getDouble:(NSString*)key def:(double)def; +- (NSString*)getString:(NSString*)key; +- (NSString*)getString:(NSString*)key def:(NSString*)def; +- (SettingOptions*)getOptions:(NSString*)key; +- (SettingOptions*)getOptions:(NSString*)key def:(SettingOptions*)def; +- (NSString*)getOptionsValue:(NSString*)key; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/Settings.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/Settings.m new file mode 100644 index 0000000..5e9f202 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/Settings.m @@ -0,0 +1,260 @@ +// +// Settings.m +// SpeechDemo +// +// Created by bytedance on 2021/3/26. +// Copyright © 2021 chengzihao.ds. All rights reserved. +// + +#import +#import "Settings.h" + +#pragma mark - SettingOptions +@implementation SettingOptions + ++ (instancetype)build:(NSArray*)ops choose:(int)idx { + SettingOptions* instance = [[self alloc] init]; + instance.optionsArray = ops; + instance.chooseIdx = idx; + return instance; +} + +@end + +#pragma mark - SettingItem +@implementation SettingItem + ++ (instancetype)build:(SettingType)type key:(NSString*)key val:(NSObject*)val hint:(NSString*)hint { + SettingItem* instance = [[self alloc] init]; + instance.key = key; + instance.type = type; + instance.value = val; + instance.hint = hint; + return instance; +} + ++ (instancetype)buildGroup:(NSString*)key val:(NSString*)val hint:(NSString*)hint { + return [SettingItem build:kSettingGroup key:key val:val hint:hint]; +} + ++ (instancetype)buildBool:(NSString*)key val:(BOOL)val hint:(NSString*)hint { + return [SettingItem build:kSettingBool key:key val:val ? @1 : @0 hint:hint]; +} + ++ (instancetype)buildInt:(NSString*)key val:(int)val hint:(NSString*)hint { + return [SettingItem build:kSettingNumber key:key val:[NSNumber numberWithInt:val] hint:hint]; +} + ++ (instancetype)buildDouble:(NSString*)key val:(double)val hint:(NSString*)hint { + return [SettingItem build:kSettingNumber key:key val:[NSNumber numberWithDouble:val] hint:hint]; +} + ++ (instancetype)buildString:(NSString*)key val:(NSString*)val hint:(NSString*)hint { + return [SettingItem build:kSettingString key:key val:val hint:hint]; +} + ++ (instancetype)buildOptions:(NSString*)key val:(SettingOptions*)val hint:(NSString*)hint { + return [SettingItem build:kSettingOptions key:key val:val hint:hint]; +} + +@end + +#pragma mark - Settings +@implementation Settings + ++ (instancetype)build { + Settings* instance = [[self alloc] init]; + instance.groups = [[NSMutableArray alloc]init]; + instance.configs = [[NSMutableArray alloc]init]; + // configs has at least one group + [instance.configs addObject:[[NSMutableArray alloc] init]]; + return instance; +} + +- (void)setItem:(SettingItem*)item { + if (item.type == kSettingGroup) { + // group type + for (int i = 0; i < _groups.count; ++i) { + SettingItem* groupItem = _groups[i]; + if ([groupItem.key isEqualToString:item.key]) { + // same group exist, do nothing + return; + } + } + + [_groups addObject:item]; + + NSMutableArray* group = _configs[0]; + if (_configs.count == 1 && group.count == 0) { + // first group has no items yet, means this is first group + return; + } + // append new group + [_configs addObject:[[NSMutableArray alloc] init]]; + return; + } else { + // other type + NSMutableArray* group = NULL; + for (int i = 0; i < _configs.count; ++i) { + group = _configs[i]; + for (int j = 0; j < group.count; ++j) { + SettingItem* cur = group[j]; + if ([cur.key isEqualToString:item.key]) { + _configs[i][j] = item; + return; + } + } + } + // default add to last group + [group addObject:item]; + } +} + +- (SettingItem*)getItem:(NSString*)key type:(SettingType)type { + if (type == kSettingGroup) { + // group type + for (int i = 0; i < _groups.count; ++i) { + SettingItem* cur = _groups[i]; + if ([cur.key isEqualToString:key]) { + return cur; + } + } + return NULL; + } else { + // other type + for (int i = 0; i < _configs.count; ++i) { + NSMutableArray* group = _configs[i]; + for (int j = 0; j < group.count; ++j) { + SettingItem* cur = group[j]; + if ([cur.key isEqualToString:key]) { + if (cur.type == type) { + return cur; + } + return NULL; + } + } + } + return NULL; + } + return NULL; +} + +- (void)registerItems:(NSArray*)cfgs { + for (int i = 0; i < cfgs.count; ++i) { + [self setItem:cfgs[i]]; + } +} + +- (void)setBool:(NSString*)key { + [self setBool:key val:false]; +} + +- (void)setBool:(NSString*)key val:(BOOL)val { + [self setItem:[SettingItem buildBool:key val:val hint:@""]]; +} + +- (void)setInt:(NSString*)key { + [self setInt:key val:0]; +} + +- (void)setInt:(NSString*)key val:(int)val { + [self setItem:[SettingItem buildInt:key val:val hint:@""]]; +} + +- (void)setDouble:(NSString*)key { + [self setDouble:key val:0.]; +} + +- (void)setDouble:(NSString*)key val:(double)val { + [self setItem:[SettingItem buildDouble:key val:val hint:@""]]; +} + +- (void)setString:(NSString*)key { + [self setString:key val:@""]; +} + +- (void)setString:(NSString*)key val:(NSString*)val { + [self setItem:[SettingItem buildString:key val:val hint:@""]]; +} + +- (void)setOptions:(NSString*)key { + [self setOptions:key val:[SettingOptions build:@[] choose:0]]; +} + +- (void)setOptions:(NSString*)key val:(SettingOptions*)val { + [self setItem:[SettingItem buildOptions:key val:val hint:@""]]; +} + +- (BOOL)getBool:(NSString*)key { + return [self getBool:key def:false]; +} + +- (BOOL)getBool:(NSString*)key def:(BOOL)def { + SettingItem* item = [self getItem:key type:kSettingBool]; + if (item != NULL) { + NSNumber* val = (NSNumber*)item.value; + return [val intValue] == 1; + } + return def; +} + +- (int)getInt:(NSString*)key { + return [self getInt:key def:0]; +} + +- (int)getInt:(NSString*)key def:(int)def { + SettingItem* item = [self getItem:key type:kSettingNumber]; + if (item != NULL) { + NSNumber* val = (NSNumber*)item.value; + return [val intValue]; + } + return def; +} + +- (double)getDouble:(NSString*)key { + return [self getDouble:key def:0]; +} + +- (double)getDouble:(NSString*)key def:(double)def { + SettingItem* item = [self getItem:key type:kSettingNumber]; + if (item != NULL) { + NSNumber* val = (NSNumber*)item.value; + return [val doubleValue]; + } + return def; +} + +- (NSString*)getString:(NSString*)key { + return [self getString:key def:@""]; +} + +- (NSString*)getString:(NSString*)key def:(NSString*)def { + SettingItem* item = [self getItem:key type:kSettingString]; + if (item != NULL) { + return (NSString*)item.value; + } + return def; +} + +- (SettingOptions*)getOptions:(NSString*)key { + return [self getOptions:key def:[SettingOptions build:@[] choose:0]]; +} + +- (SettingOptions*)getOptions:(NSString*)key def:(SettingOptions*)def { + SettingItem* item = [self getItem:key type:kSettingOptions]; + if (item != NULL) { + return (SettingOptions*)item.value; + } + return def; +} + +- (NSString*)getOptionsValue:(NSString*)key { + SettingItem* item = [self getItem:key type:kSettingOptions]; + if (item != NULL) { + SettingOptions *options = (SettingOptions*)item.value; + return options.optionsArray[options.chooseIdx]; + } + return @""; +} + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/SettingsHelper.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/SettingsHelper.h new file mode 100644 index 0000000..4b93f67 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/SettingsHelper.h @@ -0,0 +1,207 @@ +// +// SettingsHelper.h +// SpeechDemo +// +// Created by bytedance on 2020/9/9. +// Copyright © 2020 fengkai.0518. All rights reserved. +// + +#import +#import "Settings.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - SETTING_KEY + +// view id +extern NSString *const VIEW_ASR; +extern NSString *const VIEW_ASR_OFFLINE; +extern NSString *const VIEW_AU; +extern NSString *const VIEW_BIGASR; +extern NSString *const VIEW_AFP; +extern NSString *const VIEW_CAPT; +extern NSString *const VIEW_FULLLINK; +extern NSString *const VIEW_TTS; +extern NSString *const VIEW_BITTS; +extern NSString *const VIEW_UNITTS; +extern NSString *const VIEW_VOICECLONE; +extern NSString *const VIEW_VOICECONV; +extern NSString *const VIEW_DIALOG; +extern NSString *const VIEW_DIALOG_DELEGATE; +extern NSString *const VIEW_VAD; +extern NSString *const VIEW_KWS; +extern NSString *const VIEW_TEST_AFP; +extern NSString *const VIEW_TEST_ASR_OFFLINE_RTF; +extern NSString *const VIEW_TEST_ASR_STRESS; + +// common +extern NSString *const SETTING_COMMON; +extern NSString *const SETTING_APPID; +extern NSString *const SETTING_APPKEY; +extern NSString *const SETTING_TOKEN; +extern NSString *const SETTING_CLUSTER; +extern NSString *const SETTING_RESOURCE_ID; +extern NSString *const SETTING_ADDRESS; +extern NSString *const SETTING_URI; +extern NSString *const SETTING_VOICE; +extern NSString *const SETTING_VOICE_TYPE; +extern NSString *const SETTING_ONLINE_VOICE; +extern NSString *const SETTING_ONLINE_VOICE_TYPE; +extern NSString *const SETTING_OFFLINE_VOICE; +extern NSString *const SETTING_OFFLINE_VOICE_TYPE; +extern NSString *const SETTING_GET_VOLUME; +extern NSString *const SETTING_RECORD_TYPE; +extern NSString *const SETTING_RECORD_SAVE; +extern NSString *const SETTING_RECORD_FILE_TYPE; +extern NSString *const SETTING_SAMPLE_RATE; +extern NSString *const SETTING_CHANNEL; +extern NSString *const SETTING_ENABLE_AEC; +extern NSString *const SETTING_RESTART_AUDIO_SESSION_ENABLE; +extern NSString *const SETTING_RESUME_OTHERS_INTERRUPTED_PLAYBACK_ENABLE; +extern NSString *const SETTING_LICENSE_NAME; +extern NSString *const SETTING_LICENSE_BUSI_ID; +extern NSString *const SETTING_AUTHENTICATION_TYPE; +extern NSString *const SETTING_BUSINESS_KEY; +extern NSString *const SETTING_AUTHENTICATE_SECRET; +extern NSString *const SETTING_DISABLE_WS_RECONNECT; +extern NSString *const SETTING_AUDIO_FADEOUT_DURATION; +extern NSString *const SETTING_VAD_MAX_SPEECH_DURATION; +extern NSString *const SETTING_VAD_MAX_MUSIC_DURATION; +extern NSString *const SETTING_STREAM_PACKAGE_DURATION; +extern NSString *const SETTING_REQUEST_HEADERS; +extern NSString *const SETTING_ENABLE_PLAYER_AUDIO_CALL_BACK; + +// asr +extern NSString *const SETTING_ASR; +extern NSString *const SETTING_ASR_RECORDER_SAVE; +extern NSString *const SETTING_ASR_ENABLE_DDC; +extern NSString *const SETTING_ASR_ENABLE_ITN; +extern NSString *const SETTING_ASR_ENABLE_NLU_PUNC; +extern NSString *const SETTING_ASR_DISABLE_END_PUNC; +extern NSString *const SETTING_ASR_KEEP_RECORDING; +extern NSString *const SETTING_ASR_HOTWORDS; +extern NSString *const SETTING_ASR_CORRECTWORDS; +extern NSString *const SETTING_ASR_VAD_START_SILENCE_TIME; +extern NSString *const SETTING_ASR_VAD_END_SILENCE_TIME; +extern NSString *const SETTING_ASR_VAD_MODE; +extern NSString *const SETTING_ASR_RESULT_TYPE; +extern NSString *const SETTING_ASR_MAX_RETRY_TIMES; +extern NSString *const SETTING_ASR_SHOW_LANGUAGE; +extern NSString *const SETTING_ASR_LANGUAGE; +extern NSString *const SETTING_ASR_AUTO_STOP; +extern NSString *const SETTING_ASR_MODEL_NAME; +extern NSString *const SETTING_ASR_REQ_PARAMS; +extern NSString *const SETTING_ASR_ENABLE_RESOURCE_DOWNLOAD; + +// capt +extern NSString *const SETTING_CAPT; +extern NSString *const SETTING_CAPT_RECORDER_SAVE; +extern NSString *const SETTING_CAPT_STREAMING_MODE; +extern NSString *const SETTING_CAPT_CORE_TYPE; +extern NSString *const SETTING_CAPT_DIFFICULTY_LEVEL; + +// fulllink +extern NSString *const SETTING_FULLLINK; +extern NSString *const SETTING_FULLLINK_ENGINE_TYPE; +extern NSString *const SETTING_FULLLINK_WAKEUP_WORDS; +extern NSString *const SETTING_FULLLINK_ENABLE_RECORDER_DUMP; +extern NSString *const SETTING_FULLLINK_ENABLE_KWS_DUMP; +extern NSString *const SETTING_FULLLINK_WAKEUP_MODE; +extern NSString *const SETTING_FULLLINK_ONLY_ASR; +extern NSString *const SETTING_FULLLINK_DISABLE_TTS; +extern NSString *const SETTING_FULLLINK_DISABLE_SIGNAL; +extern NSString *const SETTING_FULLLINK_DISABLE_DEVICE_TYPE; +extern NSString *const SETTING_FULLLINK_DISABLE_SIGTHREAD_PRI; +extern NSString *const SETTING_FULLLINK_DISABLE_FILE_OR_DIRECTORY_NAME; + +// tts +extern NSString *const SETTING_TTS; +extern NSString *const SETTING_TTS_ONLINE_LANGUAGE; +extern NSString *const SETTING_TTS_OFFLINE_LANGUAGE; +extern NSString *const SETTING_PREVENT_PLAYER_CREATION; +extern NSString *const SETTING_TTS_ENABLE_RESUME_FROM_BREAKPOINT; +extern NSString *const SETTING_TTS_ENABLE_PLAYER; +extern NSString *const SETTING_TTS_ENABLE_DUMP; +extern NSString *const SETTING_TTS_ENABLE_DATA_CALLBACK; +extern NSString *const SETTING_TTS_ENABLE_WORD_LEVEL_PROGRESS_UPDATE; +extern NSString *const SETTING_TTS_ENABLE_CACHE; +extern NSString *const SETTING_TTS_WITH_INTENT; +extern NSString *const SETTING_TTS_LIMIT_CPU_USAGE; +extern NSString *const SETTING_TTS_TEXT_TYPE; +extern NSString *const SETTING_TTS_WORK_MODE; +extern NSString *const SETTING_TTS_SPEAK_SPEED; +extern NSString *const SETTING_TTS_AUDIO_VOLUME; +extern NSString *const SETTING_TTS_AUDIO_PITCH; +extern NSString *const SETTING_TTS_SAMPLE_RATE; +extern NSString *const SETTING_TTS_EMOTION; +extern NSString *const SETTING_TTS_MODEL_NAME; +extern NSString *const SETTING_TTS_SILENCE_DURATION; +extern NSString *const SETTING_TTS_OFFLINE_RESOURCE_FORMAT; +extern NSString *const SETTING_TTS_BACKEND_CLUSTER; +extern NSString *const SETTING_TTS_REQUEST_ID; +extern NSString *const SETTING_TTS_REQUEST_PARAMS; +extern NSString *const SETTING_TTS_SILENCE_DURATION; +extern NSString *const SETTING_TTS_USE_VOICECLONE_VOICE; + +// voiceclone +extern NSString *const SETTING_VOICECLONE; +extern NSString *const SETTING_VOICECLONE_ADDRESS; +extern NSString *const SETTING_VOICECLONE_STREAM_ADDRESS; +extern NSString *const SETTING_VOICECLONE_UID; +extern NSString *const SETTING_VOICECLONE_QUERY_UIDS; +extern NSString *const SETTING_VOICECLONE_VOICE_TYPE; +extern NSString *const SETTING_VOICECLONE_GENDER; +extern NSString *const SETTING_VOICECLONE_TASKID; + +// voiceconv +extern NSString *const SETTING_VOICECONV; +extern NSString *const SETTING_VOICECONV_RESULT_SAMPLE_RATE; +extern NSString *const SETTING_VOICECONV_ENABLE_RECORD_DUMP; +extern NSString *const SETTING_VOICECONV_ENABLE_RESULT_DUMP; +extern NSString *const SETTING_VOICECONV_REQUEST_INTERVAL; + +// dialog +extern NSString *const SETTING_DIALOG; +extern NSString *const SETTING_DIALOG_ENABLE_RECORDER_DUMP; +extern NSString *const SETTING_DIALOG_ENABLE_PLAYER_DUMP; +extern NSString *const SETTING_DIALOG_BOT_NAME; + +// vad +extern NSString *const SETTING_VAD; +extern NSString *const SETTING_VAD_RECORDER_SAVE; +extern NSString *const SETTING_VAD_HEAD_SILENCE_THRESHOLD; +extern NSString *const SETTING_VAD_TAIL_SILENCE_THRESHOLD; + +// au +extern NSString *const SETTING_AU; +extern NSString *const SETTING_AU_ABILITY; +extern NSString *const SETTING_AU_RECORDER_SAVE; +extern NSString *const SETTING_AU_PROCESS_TIMEOUT; +extern NSString *const SETTING_AU_AUDIO_PACKET_DURATION; +extern NSString *const SETTING_AU_EMPTY_PACKET_INTERVAL; + +// afp +extern NSString *const SETTING_AFP; +extern NSString *const SETTING_MUSIC_ENGINE_NAME; +extern NSString *const SETTING_AFP_RESULT_TYPE; +extern NSString *const SETTING_AFP_INSTANCE_NUMBER; + +// kws +extern NSString *const SETTING_KWS; +extern NSString *const SETTING_KWS_CUSTOM_WORDS; +extern NSString *const SETTING_KWS_MODEL_NAME; + +// asr test +extern NSString *const SETTING_ASR_STRESS; +extern NSString *const SETTING_ASR_STRESS_SCENEID; + +#pragma mark - SETTING_HELPER +@interface SettingsHelper : NSObject + ++ (instancetype)shareInstance; + +- (Settings*)getSettings:(NSString*)engine; + +@end + +NS_ASSUME_NONNULL_END diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/SettingsHelper.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/SettingsHelper.m new file mode 100644 index 0000000..00346c3 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/SettingsHelper.m @@ -0,0 +1,804 @@ +// +// SettingsHelper.m +// SpeechDemo +// +// Created by bytedance on 2020/9/9. +// Copyright © 2020 fengkai.0518. All rights reserved. +// + +#import + +#import "SettingsHelper.h" +#import "ViewController.h" +#import "SensitiveDefines.h" + + +@interface SettingsHelper() +@property (strong, nonatomic) Settings* asrSettings; +@property (strong, nonatomic) Settings* asrOfflineSettings; +@property (strong, nonatomic) Settings* bigAsrSettings; +@property (strong, nonatomic) Settings* captSettings; +@property (strong, nonatomic) Settings* fulllinkSettings; +@property (strong, nonatomic) Settings* ttsSettings; +@property (strong, nonatomic) Settings* voiceCloneSettings; +@property (strong, nonatomic) Settings* voiceConvSettings; +@property (strong, nonatomic) Settings* dialogSettings; +@property (strong, nonatomic) Settings* bittsSettings; +@property (strong, nonatomic) Settings* uniTtsSettings; +@property (strong, nonatomic) Settings* dialogDelegateSettings; +@property (strong, nonatomic) Settings* vadSettings; +@property (strong, nonatomic) Settings* auSettings; +@property (strong, nonatomic) Settings* afpSettings; +@property (strong, nonatomic) Settings* kwsSettings; +@property (strong, nonatomic) Settings* testAfpSettings; +@property (strong, nonatomic) Settings* testAsrOfflineRtfSettings; +@property (strong, nonatomic) Settings* testAsrStressSettings; +@end + +@implementation SettingsHelper + +#pragma mark - SETTING_KEY + +// view id +NSString *const VIEW_ASR = @"ASR"; +NSString *const VIEW_ASR_OFFLINE = @"ASR_OFFLINE"; +NSString *const VIEW_AU = @"AU"; +NSString *const VIEW_BIGASR = @"BIGASR"; +NSString *const VIEW_AFP = @"AFP"; +NSString *const VIEW_CAPT = @"CAPT"; +NSString *const VIEW_FULLLINK = @"FULLLINK"; +NSString *const VIEW_TTS = @"TTS"; +NSString *const VIEW_BITTS = @"BITTS"; +NSString *const VIEW_UNITTS = @"UNITTS"; +NSString *const VIEW_VOICECLONE = @"VOICECLONE"; +NSString *const VIEW_VOICECONV = @"VOICECONV"; +NSString *const VIEW_DIALOG = @"DIALOG"; +NSString *const VIEW_DIALOG_DELEGATE = @"DIALOG_DELEGATE"; +NSString *const VIEW_VAD = @"VAD"; +NSString *const VIEW_KWS = @"KWS"; +NSString *const VIEW_TEST_AFP = @"AFP_TEST"; +NSString *const VIEW_TEST_ASR_OFFLINE_RTF = @"TEST_ASR_OFFLINE_RTF"; +NSString *const VIEW_TEST_ASR_STRESS = @"TEST_ASR_STRESS"; + +// common +NSString *const SETTING_COMMON = @"Common"; +NSString *const SETTING_APPID = @"AppID"; +NSString *const SETTING_APPKEY = @"AppKey"; +NSString *const SETTING_TOKEN = @"Token"; +NSString *const SETTING_CLUSTER = @"Cluster"; +NSString *const SETTING_RESOURCE_ID = @"ResourceId"; +NSString *const SETTING_ADDRESS = @"Address"; +NSString *const SETTING_URI = @"Uri"; +NSString *const SETTING_VOICE = @"Voice"; +NSString *const SETTING_VOICE_TYPE = @"Voice Type"; +NSString *const SETTING_ONLINE_VOICE = @"Online Voice"; +NSString *const SETTING_ONLINE_VOICE_TYPE = @"Online Voice Type"; +NSString *const SETTING_OFFLINE_VOICE = @"Offline Voice"; +NSString *const SETTING_OFFLINE_VOICE_TYPE = @"Offline Voice Type"; +NSString *const SETTING_GET_VOLUME = @"Get Volume"; +NSString *const SETTING_RECORD_TYPE = @"Record Type"; +NSString *const SETTING_RECORD_SAVE = @"Record Save"; +NSString *const SETTING_RECORD_FILE_TYPE = @"Record File Type"; +NSString *const SETTING_SAMPLE_RATE = @"Sample Rate"; +NSString *const SETTING_CHANNEL = @"Channel"; +NSString *const SETTING_ENABLE_AEC = @"Enable Aec"; +NSString *const SETTING_RESTART_AUDIO_SESSION_ENABLE = @"Restart AudioSession"; +NSString *const SETTING_RESUME_OTHERS_INTERRUPTED_PLAYBACK_ENABLE = @"Resume Others Interrupted Playback"; +NSString *const SETTING_AUTHENTICATION_TYPE = @"Authentication Type"; +NSString *const SETTING_BUSINESS_KEY = @"Business Key"; +NSString *const SETTING_AUTHENTICATE_SECRET = @"Authenticate Secret"; +NSString *const SETTING_DISABLE_WS_RECONNECT = @"Disable Ws Reconnect"; +NSString *const SETTING_AUDIO_FADEOUT_DURATION = @"Audio Fade Out Duration"; +NSString *const SETTING_VAD_MAX_SPEECH_DURATION = @"Vad Max Speech Duration"; +NSString *const SETTING_VAD_MAX_MUSIC_DURATION = @"Vad Max Music Duration"; +NSString *const SETTING_STREAM_PACKAGE_DURATION = @"Stream Package Duration"; +NSString *const SETTING_REQUEST_HEADERS = @"Request headers"; +NSString *const SETTING_ENABLE_PLAYER_AUDIO_CALL_BACK = @"Enable player audio callback"; +// asr +NSString *const SETTING_ASR = @"Asr"; +NSString *const SETTING_ASR_RECORDER_SAVE = @"Asr Recorder Save"; +NSString *const SETTING_ASR_ENABLE_DDC = @"Asr Enable Ddc"; +NSString *const SETTING_ASR_ENABLE_ITN = @"Asr Enable Itn"; +NSString *const SETTING_ASR_ENABLE_NLU_PUNC = @"Asr Enable nlu punctuation"; +NSString *const SETTING_ASR_DISABLE_END_PUNC = @"asr_disable_end_punc"; +NSString *const SETTING_ASR_KEEP_RECORDING = @"Asr Enable Keep Recording"; +NSString *const SETTING_ASR_HOTWORDS = @"Asr HotWords"; +NSString *const SETTING_ASR_CORRECTWORDS = @"Asr CorrectWords"; +NSString *const SETTING_ASR_VAD_START_SILENCE_TIME = @"Asr Vad Start Silence Time"; +NSString *const SETTING_ASR_VAD_END_SILENCE_TIME = @"Asr Vad End Silence Time"; +NSString *const SETTING_ASR_VAD_MODE = @"Asr VAD Mode"; +NSString *const SETTING_ASR_RESULT_TYPE = @"Asr Result Type"; +NSString *const SETTING_ASR_MAX_RETRY_TIMES = @"Asr Max Retry Times"; +NSString *const SETTING_ASR_SHOW_LANGUAGE = @"Asr Show Language"; +NSString *const SETTING_ASR_LANGUAGE = @"Asr Language"; +NSString *const SETTING_ASR_AUTO_STOP = @"Asr Auto Stop"; +NSString *const SETTING_ASR_MODEL_NAME = @"Asr Model Name"; +NSString *const SETTING_ASR_REQ_PARAMS = @"Asr Request Params"; +NSString *const SETTING_ASR_ENABLE_RESOURCE_DOWNLOAD = @"Enable Asr Resource Download"; + +// capt +NSString *const SETTING_CAPT = @"Capt"; +NSString *const SETTING_CAPT_RECORDER_SAVE = @"Capt Recorder Save"; +NSString *const SETTING_CAPT_STREAMING_MODE = @"Capt Streaming Mode"; +NSString *const SETTING_CAPT_CORE_TYPE = @"Capt Core Type"; +NSString *const SETTING_CAPT_DIFFICULTY_LEVEL = @"Capt Difficulty Level"; + +// fulllink +NSString *const SETTING_FULLLINK = @"Fulllink"; +NSString *const SETTING_FULLLINK_ENGINE_TYPE = @"Fulllink Engine Type"; +NSString *const SETTING_FULLLINK_WAKEUP_WORDS = @"Fulllink Wakeup Words"; +NSString *const SETTING_FULLLINK_ENABLE_RECORDER_DUMP = @"Fulllink Enable Recorder Dump"; +NSString *const SETTING_FULLLINK_ENABLE_KWS_DUMP = @"Fulllink Enable Kws Dump"; +NSString *const SETTING_FULLLINK_WAKEUP_MODE = @"Fulllink Wakeup Mode"; +NSString *const SETTING_FULLLINK_ONLY_ASR = @"Fulllink Only Asr"; +NSString *const SETTING_FULLLINK_DISABLE_TTS = @"Fulllink Disable Tts"; +NSString *const SETTING_FULLLINK_DISABLE_SIGNAL = @"Fulllink Disable Signal"; +NSString *const SETTING_FULLLINK_DISABLE_DEVICE_TYPE = @"Fulllink Device Type"; +NSString *const SETTING_FULLLINK_DISABLE_SIGTHREAD_PRI = @"Fulllink Disable SigThread Pri"; +NSString *const SETTING_FULLLINK_DISABLE_FILE_OR_DIRECTORY_NAME = @"Fulllink Disable File Or Directory Name"; + +// tts +NSString *const SETTING_TTS = @"Tts"; +NSString *const SETTING_TTS_ONLINE_LANGUAGE = @"Tts online Language"; +NSString *const SETTING_TTS_EMOTION = @"Tts emotion"; +NSString *const SETTING_TTS_OFFLINE_LANGUAGE = @"Tts offline Language"; +NSString *const SETTING_PREVENT_PLAYER_CREATION = @"Prevent Player Creation"; +NSString *const SETTING_TTS_ENABLE_RESUME_FROM_BREAKPOINT = @"Tts Enable Resume From Breakpoint"; +NSString *const SETTING_TTS_ENABLE_PLAYER = @"Tts Enable Player"; +NSString *const SETTING_TTS_ENABLE_DUMP = @"Tts Enable Dump"; +NSString *const SETTING_TTS_ENABLE_DATA_CALLBACK = @"Tts Enable Data Callback"; +NSString *const SETTING_TTS_ENABLE_WORD_LEVEL_PROGRESS_UPDATE = @"Tts Enable Word level Progress Update"; +NSString *const SETTING_TTS_ENABLE_CACHE = @"Tts Enable Cache"; +NSString *const SETTING_TTS_WITH_INTENT = @"Tts With Intent Predication"; +NSString *const SETTING_TTS_LIMIT_CPU_USAGE = @"Tts Limit Cpu Usage"; +NSString *const SETTING_TTS_TEXT_TYPE = @"Tts Text Type"; +NSString *const SETTING_TTS_WORK_MODE = @"Tts Synthesis Mode"; +NSString *const SETTING_TTS_SPEAK_SPEED = @"Tts Speak Speed"; +NSString *const SETTING_TTS_AUDIO_VOLUME = @"Tts Audio Volume"; +NSString *const SETTING_TTS_AUDIO_PITCH = @"Tts Audio Pitch"; +NSString *const SETTING_TTS_SAMPLE_RATE = @"Tts Sample Rate"; +NSString *const SETTING_TTS_MODEL_NAME = @"Tts Model Name"; +NSString *const SETTING_LICENSE_NAME = @"License Name"; +NSString *const SETTING_LICENSE_BUSI_ID = @"Busi Id"; +NSString *const SETTING_TTS_BACKEND_CLUSTER = @"Backend Cluster"; +NSString *const SETTING_TTS_REQUEST_ID = @"Tts Request ID"; +NSString *const SETTING_TTS_REQUEST_PARAMS = @"Tts Request Parameters"; +NSString *const SETTING_TTS_USE_VOICECLONE_VOICE = @"Tts Use VoiceClone Voice"; +NSString *const SETTING_TTS_SILENCE_DURATION = @"Tts Silence Duration"; +NSString *const SETTING_TTS_OFFLINE_RESOURCE_FORMAT = @"TTS Offline Model Format"; + +// voiceclone +NSString *const SETTING_VOICECLONE = @"VoiceClone"; +NSString *const SETTING_VOICECLONE_ADDRESS = @"VoiceClone Address"; +NSString *const SETTING_VOICECLONE_STREAM_ADDRESS = @"VoiceClone Stream Address"; +NSString *const SETTING_VOICECLONE_UID = @"VoiceClone Uid"; +NSString *const SETTING_VOICECLONE_QUERY_UIDS = @"VoiceClone Query Uids"; +NSString *const SETTING_VOICECLONE_VOICE_TYPE = @"VoiceClone Voice Type"; +NSString *const SETTING_VOICECLONE_GENDER = @"VoiceClone Gender (Is Female)"; +NSString *const SETTING_VOICECLONE_TASKID = @"VoiceClone Taskid"; + +// voiceconv +NSString *const SETTING_VOICECONV = @"VoiceConv"; +NSString *const SETTING_VOICECONV_RESULT_SAMPLE_RATE = @"VoiceConv Result Sample Rate"; +NSString *const SETTING_VOICECONV_ENABLE_RECORD_DUMP = @"VoiceConv Enable Record Dump"; +NSString *const SETTING_VOICECONV_ENABLE_RESULT_DUMP = @"VoiceConv Enable Result Dump"; +NSString *const SETTING_VOICECONV_REQUEST_INTERVAL = @"VoiceConv Request Interval (Unit: ms)"; + +// dialog +NSString *const SETTING_DIALOG = @"Dialog"; +NSString *const SETTING_DIALOG_ENABLE_RECORDER_DUMP = @"Dialog Recorder Dump"; +NSString *const SETTING_DIALOG_ENABLE_PLAYER_DUMP = @"Dialog Player Dump"; +NSString *const SETTING_DIALOG_BOT_NAME = @"Dialog Bot Name"; + +// bitts +NSString *const SETTING_BITTS = @"BiTTS"; + +// unitts +NSString *const SETTING_UNITTS = @"UniTTS"; + +// vad +NSString *const SETTING_VAD = @"Vad"; +NSString *const SETTING_VAD_RECORDER_SAVE = @"Vad Recorder Save"; +NSString *const SETTING_VAD_HEAD_SILENCE_THRESHOLD = @"Vad Head silence Threshold"; +NSString *const SETTING_VAD_TAIL_SILENCE_THRESHOLD = @"Vad Tail silence Threshold"; + +// au +NSString *const SETTING_AU = @"Au"; +NSString *const SETTING_AU_ABILITY = @"Au Ability"; +NSString *const SETTING_AU_RECORDER_SAVE = @"Au Recorder Save"; +NSString *const SETTING_AU_PROCESS_TIMEOUT = @"Au Process Timeout"; +NSString *const SETTING_AU_AUDIO_PACKET_DURATION = @"Au Audio Packet Duration"; +NSString *const SETTING_AU_EMPTY_PACKET_INTERVAL = @"Au Empty Packet Interval"; + +// afp +NSString *const SETTING_AFP = @"Afp"; +NSString *const SETTING_MUSIC_ENGINE_NAME = @"Music engine name"; +NSString *const SETTING_AFP_RESULT_TYPE = @"Afp result type"; +NSString *const SETTING_AFP_INSTANCE_NUMBER = @"Afp instance number"; + +// kws +NSString *const SETTING_KWS = @"Kws"; +NSString *const SETTING_KWS_CUSTOM_WORDS = @"Kws Custom Words"; +NSString *const SETTING_KWS_MODEL_NAME = @"Kws Model Name"; + +// asr test +NSString *const SETTING_ASR_STRESS = @"Asr Stress"; +NSString *const SETTING_ASR_STRESS_SCENEID = @"Asr Stress Sceneid"; + +#pragma mark - SETTING_HINT +static NSString *const SETTING_HINT_CLUSTER = @"Cluster"; +static NSString *const SETTING_HINT_BACKEND_CLUSTER = @"Backend Cluster"; +static NSString *const SETTING_HINT_ADDRESS = @"Address"; +static NSString *const SETTING_HINT_URI = @"Uri"; +static NSString *const SETTING_HINT_VOICE = @"Voice"; +static NSString *const SETTING_HINT_VOICE_TYPE = @"Voice Type"; +static NSString *const SETTING_HINT_ONLINE_VOICE = @"Online Voice"; +static NSString *const SETTING_HINT_ONLINE_VOICE_TYPE = @"Online Voice Type"; +static NSString *const SETTING_HINT_OFFLINE_VOICE = @"Offline Voice"; +static NSString *const SETTING_HINT_OFFLINE_VOICE_TYPE = @"Offline Voice Type"; +static NSString *const SETTING_HINT_TTS_ONLINE_LANGUAGE = @"Tts online Language"; +static NSString *const SETTING_HINT_TTS_EMOTION = @"Tts emotion"; +static NSString *const SETTING_HINT_TTS_OFFLINE_LANGUAGE = @"Tts offline Language"; +static NSString *const SETTING_HINT_DIFFICULTY_LEVEL = @"Support 1,2,3"; +static NSString *const SETTING_HINT_VOICECLONE_UID = @"VoiceClone Uid"; +static NSString *const SETTING_HINT_VOICECLONE_QUERY_UIDS = @"VoiceClone Query Uids"; +static NSString *const SETTING_HINT_VOICECLONE_VOICE_TYPE = @"VoiceClone Voice Type"; +static NSString *const SETTING_HINT_BUSINESS_KEY = @"Business Key"; +static NSString *const SETTING_HINT_AUTHENTICATE_SECRET = @"Authenticate Secret"; +static NSString *const SETTING_HINT_LICENSE_NAME = @"Input license name"; +static NSString *const SETTING_HINT_LICENSE_BUSI_ID = @"Input busi id"; +static NSString *const SETTING_HINT_TTS_SAMPLE_RATE = @"Input tts sample rate"; +static NSString *const SETTING_HINT_TTS_MODEL_NAME = @"Input Tts Model Name"; +static NSString *const SETTING_HINT_MAX_CACHE_NUM = @"Input max number of client audio cache, such as 100."; +static NSString *const SETTING_HINT_TTS_CACHE_RENEWAL_DURATION = @"Input cache renewal duration, unit:ms"; + +#pragma mark - SETTING_OPTIONS +static NSArray* SETTING_OPTIONS_RECORD_TYPE(void) { + return @[@"Recorder", @"File", @"Stream"]; +} +static NSArray* SETTING_OPTIONS_RECORD_FILE_TYPE(void) { + return @[@"Wav", @"Aac"]; +} +static NSArray* SETTING_OPTIONS_ASR_RESULT_TYPE(void) { + return @[@"full", @"single"]; +} +static NSArray* SETTING_OPTIONS_CAPT_CORE_TYPE(void) { + return @[@"English sentence score", @"English word score", @"English word pronounce", @"Chinese sentence raw"]; +} +static NSArray* SETTING_OPTIONS_FULLLINK_ENGINE_TYPE(void) { + return @[@"FULLLINK LITE", @"FULLLINK"]; +} +static NSArray* SETTING_OPTIONS_FULLLINK_WAKEUP_WORDS(void) { + return @[@"大力大力(only)", @"大力大力(大力同学)", @"大力同学(大力大力)", @"大力同学(only)"]; +} +static NSArray* SETTING_OPTIONS_FULLLINK_WAKEUP_MODE(void) { + return @[@"normal", @"disable wakeup", @"night mode"]; +} +static NSArray* SETTING_OPTIONS_TTS_TEXT_TYPE(void) { + return @[@"Plain", @"SSML", @"Json"]; +} +static NSArray* SETTING_OPTIONS_TTS_WORK_MODE(void) { + return @[@"Online", @"Offline", @"Alternate"]; +} +static NSArray* SETTING_OPTIONS_TTS_OFFLINE_RESOURCE_FORMAT(void) { + return @[@"SingleVoice", @"MultipleVoice"]; +} +static NSArray* SETTING_OPTIONS_AUTHENTICATION_TYPE(void) { + return @[@"Pre Bind", @"Late Bind"]; +} +static NSArray* SETTING_OPTIONS_AU_ABILITY(void) { + return @[@"ASR", @"MUSIC", @"ASR+MUSIC"]; +} +static NSArray* SETTING_OPTIONS_MUSIC_ENGINE_NAME(void) { + return @[@"AFP", @"CoverSong", @"Humming"]; +} +static NSArray* SETTING_OPTIONS_AFP_RESULT_TYPE(void) { + return @[@"Bytes", @"Json"]; +} +static NSArray* SETTING_OPTIONS_ASR_STRESS_TYPE(void) { + return @[@"正常场景1", @"正常场景2", @"ERROR回调时析构", @"随机压测"]; +} + +#pragma mark - Setting Init ++ (instancetype)shareInstance { + static SettingsHelper* sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] init]; + + // Asr settings + sharedInstance.asrSettings = [Settings build]; + [sharedInstance.asrSettings registerItems:@[ + // common + [SettingItem buildGroup:SETTING_COMMON val:@"" hint:@""], + [SettingItem buildString:SETTING_APPID val:SDEF_APPID hint:@""], + [SettingItem buildString:SETTING_TOKEN val:SDEF_TOKEN hint:@""], + [SettingItem buildString:SETTING_CLUSTER val:SDEF_ASR_DEFAULT_CLUSTER hint:SETTING_HINT_CLUSTER], + [SettingItem buildString:SETTING_ADDRESS val:SDEF_DEFAULT_ADDRESS hint:SETTING_HINT_ADDRESS], + [SettingItem buildString:SETTING_URI val:SDEF_ASR_DEFAULT_URI hint:SETTING_HINT_URI], + [SettingItem buildBool:SETTING_GET_VOLUME val:false hint:@""], + [SettingItem buildInt:SETTING_SAMPLE_RATE val:16000 hint:@""], + [SettingItem buildInt:SETTING_CHANNEL val:1 hint:@""], + [SettingItem buildOptions:SETTING_RECORD_TYPE val:[SettingOptions build:SETTING_OPTIONS_RECORD_TYPE() choose:0] hint:@""], + [SettingItem buildBool:SETTING_RESTART_AUDIO_SESSION_ENABLE val:false hint:@""], + [SettingItem buildBool:SETTING_RESUME_OTHERS_INTERRUPTED_PLAYBACK_ENABLE val:false hint:@""], + [SettingItem buildInt:SETTING_VAD_MAX_SPEECH_DURATION val:60000 hint:@""], + [SettingItem buildString:SETTING_REQUEST_HEADERS val:@"{\"custom_header_key0\": \"custom_header_value0\",\"custom_header_key1\": \"custom_header_value1\"}" hint:@""], + + // asr + [SettingItem buildGroup:SETTING_ASR val:@"" hint:@""], + [SettingItem buildBool:SETTING_ASR_RECORDER_SAVE val:false hint:@""], + [SettingItem buildBool:SETTING_ASR_ENABLE_DDC val:false hint:@""], + [SettingItem buildBool:SETTING_ASR_ENABLE_ITN val:false hint:@""], + [SettingItem buildBool:SETTING_ASR_ENABLE_NLU_PUNC val:true hint:@""], + [SettingItem buildBool:SETTING_ASR_DISABLE_END_PUNC val:false hint:@""], + [SettingItem buildBool:SETTING_ASR_KEEP_RECORDING val:false hint:@""], + [SettingItem buildBool:SETTING_ASR_SHOW_LANGUAGE val:false hint:@""], + [SettingItem buildString:SETTING_ASR_LANGUAGE val:@"en-US" hint:@""], + [SettingItem buildString:SETTING_ASR_HOTWORDS val:@"{\"hotwords\":[{\"word\":\"过秦论\",\"scale\":\"2.0\"}]}" hint:@""], + [SettingItem buildString:SETTING_ASR_CORRECTWORDS val:@"{\"古爱玲\":\"谷爱凌\",\"古埃宁\":\"谷爱凌\",\"谷爱玲\":\"谷爱凌\",\"谷埃宁\":\"谷爱凌\"}" hint:@""], + [SettingItem buildInt:SETTING_ASR_VAD_START_SILENCE_TIME val:0 hint:@""], + [SettingItem buildInt:SETTING_ASR_VAD_END_SILENCE_TIME val:0 hint:@""], + [SettingItem buildString:SETTING_ASR_VAD_MODE val:@"" hint:@""], + [SettingItem buildOptions:SETTING_ASR_RESULT_TYPE val:[SettingOptions build:SETTING_OPTIONS_ASR_RESULT_TYPE() choose:0] hint:@""], + [SettingItem buildInt:SETTING_ASR_MAX_RETRY_TIMES val:0 hint:@""], + [SettingItem buildString:SETTING_ASR_REQ_PARAMS val:@"" hint:@""], + ]]; + + // BigAsr settings + sharedInstance.bigAsrSettings = [Settings build]; + [sharedInstance.bigAsrSettings registerItems:@[ + // common + [SettingItem buildGroup:SETTING_COMMON val:@"" hint:@""], + [SettingItem buildString:SETTING_APPID val:SDEF_BIGASR_DEFAULT_APPID hint:@""], + [SettingItem buildString:SETTING_TOKEN val:SDEF_BIGASR_DEFAULT_TOKEN hint:@""], + [SettingItem buildString:SETTING_RESOURCE_ID val:SDEF_BIGASR_DEFAULT_RESOURCE_ID hint:@""], + [SettingItem buildString:SETTING_ADDRESS val:SDEF_DEFAULT_ADDRESS hint:SETTING_HINT_ADDRESS], + [SettingItem buildString:SETTING_URI val:SDEF_BIGASR_DEFAULT_URI hint:SETTING_HINT_URI], + [SettingItem buildBool:SETTING_GET_VOLUME val:false hint:@""], + [SettingItem buildInt:SETTING_SAMPLE_RATE val:16000 hint:@""], + [SettingItem buildInt:SETTING_CHANNEL val:1 hint:@""], + [SettingItem buildOptions:SETTING_RECORD_TYPE val:[SettingOptions build:SETTING_OPTIONS_RECORD_TYPE() choose:0] hint:@""], + [SettingItem buildBool:SETTING_RESTART_AUDIO_SESSION_ENABLE val:false hint:@""], + [SettingItem buildBool:SETTING_RESUME_OTHERS_INTERRUPTED_PLAYBACK_ENABLE val:false hint:@""], + [SettingItem buildInt:SETTING_VAD_MAX_SPEECH_DURATION val:60000 hint:@""], + + // asr + [SettingItem buildGroup:SETTING_ASR val:@"" hint:@""], + [SettingItem buildBool:SETTING_ASR_RECORDER_SAVE val:false hint:@""], + [SettingItem buildBool:SETTING_ASR_ENABLE_DDC val:false hint:@""], + [SettingItem buildBool:SETTING_ASR_ENABLE_ITN val:false hint:@""], + [SettingItem buildBool:SETTING_ASR_ENABLE_NLU_PUNC val:true hint:@""], + [SettingItem buildString:SETTING_ASR_REQ_PARAMS val:@"" hint:@""], + ]]; + + // Asr offline settings + sharedInstance.asrOfflineSettings = [Settings build]; + [sharedInstance.asrOfflineSettings registerItems:@[ + // common + [SettingItem buildGroup:SETTING_COMMON val:@"" hint:@""], + [SettingItem buildString:SETTING_APPID val:SDEF_APPID hint:@""], + [SettingItem buildOptions:SETTING_RECORD_TYPE val:[SettingOptions build:SETTING_OPTIONS_RECORD_TYPE() choose:0] hint:@""], + [SettingItem buildInt:SETTING_VAD_MAX_SPEECH_DURATION val:15000 hint:@""], + [SettingItem buildOptions:SETTING_AUTHENTICATION_TYPE val:[SettingOptions build:SETTING_OPTIONS_AUTHENTICATION_TYPE() choose:1] hint:@""], + [SettingItem buildString:SETTING_LICENSE_NAME val:SDEF_LICENSE_NAME hint:SETTING_HINT_LICENSE_NAME], + [SettingItem buildString:SETTING_LICENSE_BUSI_ID val:SDEF_LICENSE_BUSI_ID hint:SETTING_HINT_LICENSE_BUSI_ID], + [SettingItem buildString:SETTING_BUSINESS_KEY val:SDEF_BUSINESS_KEY hint:SETTING_HINT_BUSINESS_KEY], + [SettingItem buildString:SETTING_AUTHENTICATE_SECRET val:SDEF_SECRET hint:SETTING_HINT_AUTHENTICATE_SECRET], + + // asr + [SettingItem buildGroup:SETTING_ASR val:@"" hint:@""], + [SettingItem buildBool:SETTING_ASR_ENABLE_RESOURCE_DOWNLOAD val:false hint:@""], + [SettingItem buildString:SETTING_ASR_MODEL_NAME val:SDEF_ASR_DEFAULT_MODEL_NAME hint:@""], + [SettingItem buildBool:SETTING_ASR_RECORDER_SAVE val:false hint:@""], + [SettingItem buildBool:SETTING_ASR_ENABLE_ITN val:false hint:@""], + [SettingItem buildBool:SETTING_ASR_SHOW_LANGUAGE val:false hint:@""], + [SettingItem buildOptions:SETTING_ASR_RESULT_TYPE val:[SettingOptions build:SETTING_OPTIONS_ASR_RESULT_TYPE() choose:0] hint:@""], + ]]; + + // Capt settings + sharedInstance.captSettings = [Settings build]; + [sharedInstance.captSettings registerItems:@[ + // common + [SettingItem buildGroup:SETTING_COMMON val:@"" hint:@""], + [SettingItem buildString:SETTING_CLUSTER val:SDEF_CAPT_DEFAULT_CLUSTER hint:SETTING_HINT_CLUSTER], + [SettingItem buildString:SETTING_ADDRESS val:SDEF_DEFAULT_ADDRESS hint:SETTING_HINT_ADDRESS], + [SettingItem buildString:SETTING_URI val:@"" hint:SETTING_HINT_URI], + [SettingItem buildString:SETTING_APPID val:SDEF_APPID hint:@""], + [SettingItem buildString:SETTING_TOKEN val:SDEF_APPID hint:@""], + [SettingItem buildBool:SETTING_GET_VOLUME val:false hint:@""], + [SettingItem buildOptions:SETTING_RECORD_TYPE val:[SettingOptions build:SETTING_OPTIONS_RECORD_TYPE() choose:0] hint:@""], + [SettingItem buildBool:SETTING_ENABLE_AEC val:false hint:@""], + [SettingItem buildBool:SETTING_RESTART_AUDIO_SESSION_ENABLE val:false hint:@""], + [SettingItem buildBool:SETTING_RESUME_OTHERS_INTERRUPTED_PLAYBACK_ENABLE val:false hint:@""], + [SettingItem buildInt:SETTING_VAD_MAX_SPEECH_DURATION val:15000 hint:@""], + + // capt + [SettingItem buildGroup:SETTING_CAPT val:@"" hint:@""], + [SettingItem buildBool:SETTING_CAPT_RECORDER_SAVE val:false hint:@""], + [SettingItem buildBool:SETTING_CAPT_STREAMING_MODE val:false hint:@""], + [SettingItem buildOptions:SETTING_CAPT_CORE_TYPE val:[SettingOptions build:SETTING_OPTIONS_CAPT_CORE_TYPE() choose:0] hint:@""], + [SettingItem buildInt:SETTING_CAPT_DIFFICULTY_LEVEL val:2 hint:SETTING_HINT_DIFFICULTY_LEVEL], + ]]; + + // Fulllink settings + sharedInstance.fulllinkSettings = [Settings build]; + [sharedInstance.fulllinkSettings registerItems:@[ + // common + [SettingItem buildGroup:SETTING_COMMON val:@"" hint:@""], + [SettingItem buildString:SETTING_CLUSTER val:@"" hint:SETTING_HINT_CLUSTER], + [SettingItem buildString:SETTING_ADDRESS val:SDEF_DEFAULT_ADDRESS hint:SETTING_HINT_ADDRESS], + [SettingItem buildBool:SETTING_GET_VOLUME val:false hint:@""], + [SettingItem buildInt:SETTING_SAMPLE_RATE val:16000 hint:@""], + [SettingItem buildOptions:SETTING_RECORD_TYPE val:[SettingOptions build:SETTING_OPTIONS_RECORD_TYPE() choose:0] hint:@""], + [SettingItem buildBool:SETTING_ENABLE_AEC val:false hint:@""], + [SettingItem buildBool:SETTING_RESTART_AUDIO_SESSION_ENABLE val:false hint:@""], + [SettingItem buildBool:SETTING_RESUME_OTHERS_INTERRUPTED_PLAYBACK_ENABLE val:false hint:@""], + + // fulllink + [SettingItem buildGroup:SETTING_FULLLINK val:@"" hint:@""], + [SettingItem buildOptions:SETTING_FULLLINK_ENGINE_TYPE val:[SettingOptions build:SETTING_OPTIONS_FULLLINK_ENGINE_TYPE() choose:0] hint:@""], + [SettingItem buildOptions:SETTING_FULLLINK_WAKEUP_WORDS val:[SettingOptions build:SETTING_OPTIONS_FULLLINK_WAKEUP_WORDS() choose:0] hint:@""], + [SettingItem buildBool:SETTING_FULLLINK_ENABLE_RECORDER_DUMP val:false hint:@""], + [SettingItem buildBool:SETTING_FULLLINK_ENABLE_KWS_DUMP val:false hint:@""], + [SettingItem buildOptions:SETTING_FULLLINK_WAKEUP_MODE val:[SettingOptions build:SETTING_OPTIONS_FULLLINK_WAKEUP_MODE() choose:0] hint:@""], + [SettingItem buildBool:SETTING_FULLLINK_ONLY_ASR val:false hint:@""], + [SettingItem buildBool:SETTING_FULLLINK_DISABLE_TTS val:false hint:@""], + [SettingItem buildBool:SETTING_FULLLINK_DISABLE_SIGNAL val:false hint:@""], + [SettingItem buildInt:SETTING_FULLLINK_DISABLE_SIGTHREAD_PRI val:-10 hint:@""], + [SettingItem buildString:SETTING_FULLLINK_DISABLE_FILE_OR_DIRECTORY_NAME val:@"" hint:@""], + ]]; + + // Tts settings + sharedInstance.ttsSettings = [Settings build]; + [sharedInstance.ttsSettings registerItems:@[ + // common + [SettingItem buildGroup:SETTING_COMMON val:@"" hint:@""], + [SettingItem buildString:SETTING_APPID val:SDEF_APPID hint:@""], + [SettingItem buildString:SETTING_TOKEN val:SDEF_TOKEN hint:@""], + [SettingItem buildString:SETTING_CLUSTER val:SDEF_TTS_DEFAULT_CLUSTER hint:SETTING_HINT_CLUSTER], + [SettingItem buildString:SETTING_ADDRESS val:SDEF_DEFAULT_ADDRESS hint:SETTING_HINT_ADDRESS], + [SettingItem buildString:SETTING_URI val:SDEF_TTS_DEFAULT_URI hint:SETTING_HINT_URI], + [SettingItem buildString:SETTING_ONLINE_VOICE val:SDEF_TTS_DEFAULT_ONLINE_VOICE hint:SETTING_HINT_ONLINE_VOICE], + [SettingItem buildString:SETTING_ONLINE_VOICE_TYPE val:SDEF_TTS_DEFAULT_ONLINE_VOICE_TYPE hint:SETTING_HINT_ONLINE_VOICE_TYPE], + [SettingItem buildString:SETTING_OFFLINE_VOICE val:SDEF_TTS_DEFAULT_OFFLINE_VOICE hint:SETTING_HINT_OFFLINE_VOICE], + [SettingItem buildString:SETTING_OFFLINE_VOICE_TYPE val:SDEF_TTS_DEFAULT_OFFLINE_VOICE_TYPE hint:SETTING_HINT_OFFLINE_VOICE_TYPE], + [SettingItem buildOptions:SETTING_AUTHENTICATION_TYPE val:[SettingOptions build:SETTING_OPTIONS_AUTHENTICATION_TYPE() choose:0] hint:@""], + [SettingItem buildString:SETTING_LICENSE_NAME val:SDEF_LICENSE_NAME hint:SETTING_HINT_LICENSE_NAME], + [SettingItem buildString:SETTING_LICENSE_BUSI_ID val:SDEF_LICENSE_BUSI_ID hint:SETTING_HINT_LICENSE_BUSI_ID], + [SettingItem buildString:SETTING_BUSINESS_KEY val:SDEF_BUSINESS_KEY hint:SETTING_HINT_BUSINESS_KEY], + [SettingItem buildString:SETTING_AUTHENTICATE_SECRET val:SDEF_SECRET hint:SETTING_HINT_AUTHENTICATE_SECRET], + [SettingItem buildBool:SETTING_DISABLE_WS_RECONNECT val:false hint:@""], + [SettingItem buildInt:SETTING_AUDIO_FADEOUT_DURATION val:0 hint:@""], + + // tts + [SettingItem buildGroup:SETTING_TTS val:@"" hint:@""], + [SettingItem buildOptions:SETTING_TTS_WORK_MODE val:[SettingOptions build:SETTING_OPTIONS_TTS_WORK_MODE() choose:0] hint:@""], + [SettingItem buildOptions:SETTING_TTS_TEXT_TYPE val:[SettingOptions build:SETTING_OPTIONS_TTS_TEXT_TYPE() choose:0] hint:@""], + [SettingItem buildBool:SETTING_PREVENT_PLAYER_CREATION val:false hint:@""], + [SettingItem buildBool:SETTING_TTS_ENABLE_RESUME_FROM_BREAKPOINT val:true hint:@""], + [SettingItem buildBool:SETTING_TTS_ENABLE_PLAYER val:true hint:@""], + [SettingItem buildBool:SETTING_TTS_ENABLE_DUMP val:false hint:@""], + [SettingItem buildBool:SETTING_TTS_ENABLE_DATA_CALLBACK val:false hint:@""], + [SettingItem buildBool:SETTING_TTS_ENABLE_WORD_LEVEL_PROGRESS_UPDATE val:true hint:@""], + [SettingItem buildInt:SETTING_TTS_SILENCE_DURATION val:0 hint:@""], + [SettingItem buildDouble:SETTING_TTS_SPEAK_SPEED val:1.0 hint:@""], + [SettingItem buildDouble:SETTING_TTS_AUDIO_VOLUME val:1.0 hint:@""], + [SettingItem buildDouble:SETTING_TTS_AUDIO_PITCH val:1.0 hint:@""], + [SettingItem buildInt:SETTING_TTS_SAMPLE_RATE val:16000 hint:SETTING_HINT_TTS_SAMPLE_RATE], + + [SettingItem buildBool:SETTING_TTS_ENABLE_CACHE val:false hint:@""], + [SettingItem buildBool:SETTING_TTS_WITH_INTENT val:false hint:@""], + [SettingItem buildString:SETTING_TTS_ONLINE_LANGUAGE val:SDEF_TTS_DEFAULT_ONLINE_LANGUAGE hint:SETTING_HINT_TTS_ONLINE_LANGUAGE], + [SettingItem buildString:SETTING_TTS_EMOTION val:@"" hint:SETTING_HINT_TTS_EMOTION], + [SettingItem buildBool:SETTING_TTS_USE_VOICECLONE_VOICE val:false hint:@""], + [SettingItem buildString:SETTING_TTS_BACKEND_CLUSTER val:@"demo_test" hint:SETTING_HINT_BACKEND_CLUSTER], + [SettingItem buildString:SETTING_TTS_REQUEST_ID val:@"" hint:@""], + [SettingItem buildString:SETTING_TTS_REQUEST_PARAMS val:@"" hint:@""], + + [SettingItem buildOptions:SETTING_TTS_OFFLINE_RESOURCE_FORMAT val:[SettingOptions build:SETTING_OPTIONS_TTS_OFFLINE_RESOURCE_FORMAT() choose:0] hint:@""], + [SettingItem buildString:SETTING_TTS_OFFLINE_LANGUAGE val:SDEF_TTS_DEFAULT_OFFLINE_LANGUAGE hint:SETTING_HINT_TTS_OFFLINE_LANGUAGE], + [SettingItem buildString:SETTING_TTS_MODEL_NAME val:@"aispeech_tts" hint:SETTING_HINT_TTS_MODEL_NAME], + [SettingItem buildBool:SETTING_TTS_LIMIT_CPU_USAGE val:false hint:@""], + ]]; + // VoiceClone settings + sharedInstance.voiceCloneSettings = [Settings build]; + [sharedInstance.voiceCloneSettings registerItems:@[ + // common + [SettingItem buildGroup:SETTING_COMMON val:@"" hint:@""], + [SettingItem buildInt:SETTING_SAMPLE_RATE val:44100 hint:@""], + [SettingItem buildString:SETTING_APPID val:SDEF_APPID hint:@""], + [SettingItem buildString:SETTING_TOKEN val:SDEF_APPID hint:@""], + [SettingItem buildOptions:SETTING_RECORD_TYPE val:[SettingOptions build:SETTING_OPTIONS_RECORD_TYPE() choose:0] hint:@""], + + // voiceclone + [SettingItem buildGroup:SETTING_VOICECLONE val:@"" hint:@""], + [SettingItem buildString:SETTING_VOICECLONE_ADDRESS val:SDEF_DEFAULT_HTTP_ADDRESS hint:@""], + [SettingItem buildString:SETTING_VOICECLONE_STREAM_ADDRESS val:SDEF_DEFAULT_ADDRESS hint:@""], + [SettingItem buildString:SETTING_VOICECLONE_UID val:SDEF_UID hint:SETTING_HINT_VOICECLONE_UID], + [SettingItem buildString:SETTING_VOICECLONE_QUERY_UIDS val:SDEF_VOICECLONE_DEFAULT_UIDS hint:SETTING_HINT_VOICECLONE_QUERY_UIDS], + [SettingItem buildString:SETTING_VOICECLONE_VOICE_TYPE val:@"" hint:SETTING_HINT_VOICECLONE_VOICE_TYPE], + [SettingItem buildBool:SETTING_VOICECLONE_GENDER val:false hint:@""], + [SettingItem buildInt:SETTING_VOICECLONE_TASKID val:SDEF_VOICECLONE_DEFAULT_TASK_ID hint:@""], + ]]; + + // VoiceConv settings + sharedInstance.voiceConvSettings = [Settings build]; + [sharedInstance.voiceConvSettings registerItems:@[ + // common + [SettingItem buildGroup:SETTING_COMMON val:@"" hint:@""], + [SettingItem buildString:SETTING_VOICE val:SDEF_VOICECONV_DEFAULT_VOICE hint:SETTING_HINT_VOICE], + [SettingItem buildString:SETTING_VOICE_TYPE val:SDEF_VOICECONV_DEFAULT_VOICE_TYPE hint:SETTING_HINT_VOICE_TYPE], + [SettingItem buildBool:SETTING_GET_VOLUME val:false hint:@""], + [SettingItem buildOptions:SETTING_RECORD_TYPE val:[SettingOptions build:SETTING_OPTIONS_RECORD_TYPE() choose:0] hint:@""], + + // voiceconv + [SettingItem buildGroup:SETTING_VOICECONV val:@"" hint:@""], + [SettingItem buildInt:SETTING_VOICECONV_RESULT_SAMPLE_RATE val:24000 hint:@""], + [SettingItem buildBool:SETTING_VOICECONV_ENABLE_RECORD_DUMP val:true hint:@""], + [SettingItem buildBool:SETTING_VOICECONV_ENABLE_RESULT_DUMP val:true hint:@""], + [SettingItem buildInt:SETTING_VOICECONV_REQUEST_INTERVAL val:200 hint:@""], + ]]; + + // Dialog settings + sharedInstance.dialogSettings = [Settings build]; + [sharedInstance.dialogSettings registerItems:@[ + // common + [SettingItem buildGroup:SETTING_COMMON val:@"" hint:@""], + [SettingItem buildString:SETTING_APPID val:SDEF_APPID hint:@""], + [SettingItem buildString:SETTING_APPKEY val:SDEF_APPKEY hint:@""], + [SettingItem buildString:SETTING_TOKEN val:SDEF_TOKEN hint:@""], + [SettingItem buildString:SETTING_RESOURCE_ID val:SDEF_DIALOG_DEFAULT_RESOURCE_ID hint:@""], + [SettingItem buildString:SETTING_ADDRESS val:SDEF_DEFAULT_ADDRESS hint:SETTING_HINT_ADDRESS], + [SettingItem buildString:SETTING_URI val:SDEF_DIALOG_DEFAULT_URI hint:SETTING_HINT_URI], + [SettingItem buildString:SETTING_REQUEST_HEADERS val:@"" hint:@""], + + // dialog + [SettingItem buildGroup:SETTING_DIALOG val:@"" hint:@""], + [SettingItem buildBool:SETTING_DIALOG_ENABLE_RECORDER_DUMP val:false hint:@""], + [SettingItem buildBool:SETTING_DIALOG_ENABLE_PLAYER_DUMP val:false hint:@""], + [SettingItem buildString:SETTING_DIALOG_BOT_NAME val:@"豆包" hint:@""], + ]]; + + // Dialog settings + sharedInstance.dialogDelegateSettings = [Settings build]; + [sharedInstance.dialogDelegateSettings registerItems:@[ + // common + [SettingItem buildGroup:SETTING_COMMON val:@"" hint:@""], + [SettingItem buildString:SETTING_APPID val:SDEF_APPID hint:@""], + [SettingItem buildString:SETTING_APPKEY val:SDEF_APPKEY hint:@""], + [SettingItem buildString:SETTING_TOKEN val:SDEF_TOKEN hint:@""], + [SettingItem buildString:SETTING_RESOURCE_ID val:SDEF_DIALOG_DEFAULT_RESOURCE_ID hint:@""], + [SettingItem buildString:SETTING_ADDRESS val:SDEF_DEFAULT_ADDRESS hint:SETTING_HINT_ADDRESS], + [SettingItem buildString:SETTING_URI val:SDEF_DIALOG_DEFAULT_URI hint:SETTING_HINT_URI], + [SettingItem buildString:SETTING_REQUEST_HEADERS val:@"" hint:@""], + + // dialog + [SettingItem buildGroup:SETTING_DIALOG val:@"" hint:@""], + [SettingItem buildBool:SETTING_DIALOG_ENABLE_RECORDER_DUMP val:false hint:@""], + [SettingItem buildBool:SETTING_DIALOG_ENABLE_PLAYER_DUMP val:false hint:@""], + [SettingItem buildString:SETTING_DIALOG_BOT_NAME val:@"豆包" hint:@""], + ]]; + + // BiTTS settings + sharedInstance.bittsSettings = [Settings build]; + [sharedInstance.bittsSettings registerItems:@[ + // common + [SettingItem buildGroup:SETTING_COMMON val:@"" hint:@""], + [SettingItem buildString:SETTING_APPID val:SDEF_BITTS_DEFAULT_APPID hint:@""], + [SettingItem buildString:SETTING_TOKEN val:SDEF_BITTS_DEFAULT_TOKEN hint:@""], + [SettingItem buildString:SETTING_RESOURCE_ID val:SDEF_BITTS_DEFAULT_RESOURCE_ID hint:@""], + [SettingItem buildString:SETTING_ADDRESS val:SDEF_DEFAULT_ADDRESS hint:SETTING_HINT_ADDRESS], + [SettingItem buildString:SETTING_URI val:SDEF_BITTS_DEFAULT_URI hint:SETTING_HINT_URI], + [SettingItem buildString:SETTING_REQUEST_HEADERS val:@"{}" hint:@""], + + // bitts + [SettingItem buildGroup:SETTING_BITTS val:@"" hint:@""], + [SettingItem buildBool:SETTING_TTS_ENABLE_PLAYER val:true hint:@""], + [SettingItem buildBool:SETTING_TTS_ENABLE_DUMP val:false hint:@""], + [SettingItem buildBool:SETTING_ENABLE_PLAYER_AUDIO_CALL_BACK val:true hint:@""], + ]]; + + // UniTTS settings + sharedInstance.uniTtsSettings = [Settings build]; + [sharedInstance.uniTtsSettings registerItems:@[ + // common + [SettingItem buildGroup:SETTING_COMMON val:@"" hint:@""], + [SettingItem buildString:SETTING_APPID val:SDEF_UNITTS_DEFAULT_APPID hint:@""], + [SettingItem buildString:SETTING_TOKEN val:SDEF_UNITTS_DEFAULT_TOKEN hint:@""], + [SettingItem buildString:SETTING_RESOURCE_ID val:SDEF_UNITTS_DEFAULT_RESOURCE_ID hint:@""], + [SettingItem buildString:SETTING_ADDRESS val:SDEF_DEFAULT_ADDRESS hint:SETTING_HINT_ADDRESS], + [SettingItem buildString:SETTING_URI val:SDEF_UNITTS_DEFAULT_URI hint:SETTING_HINT_URI], + [SettingItem buildString:SETTING_REQUEST_HEADERS val:@"{}" hint:@""], + + // unitts + [SettingItem buildGroup:SETTING_UNITTS val:@"" hint:@""], + [SettingItem buildBool:SETTING_TTS_ENABLE_PLAYER val:true hint:@""], + [SettingItem buildBool:SETTING_TTS_ENABLE_DUMP val:false hint:@""], + [SettingItem buildBool:SETTING_ENABLE_PLAYER_AUDIO_CALL_BACK val:true hint:@""], + ]]; + + // Vad settings + sharedInstance.vadSettings = [Settings build]; + [sharedInstance.vadSettings registerItems:@[ + // common + [SettingItem buildGroup:SETTING_COMMON val:@"" hint:@""], + [SettingItem buildBool:SETTING_GET_VOLUME val:false hint:@""], + [SettingItem buildOptions:SETTING_RECORD_TYPE val:[SettingOptions build:SETTING_OPTIONS_RECORD_TYPE() choose:0] hint:@""], + [SettingItem buildBool:SETTING_RESTART_AUDIO_SESSION_ENABLE val:false hint:@""], + [SettingItem buildBool:SETTING_RESUME_OTHERS_INTERRUPTED_PLAYBACK_ENABLE val:false hint:@""], + + // vad + [SettingItem buildGroup:SETTING_VAD val:@"" hint:@""], + [SettingItem buildBool:SETTING_VAD_RECORDER_SAVE val:false hint:@""], + [SettingItem buildInt:SETTING_VAD_HEAD_SILENCE_THRESHOLD val:4000 hint:@""], + [SettingItem buildInt:SETTING_VAD_TAIL_SILENCE_THRESHOLD val:2000 hint:@""], + ]]; + + // Au settings + sharedInstance.auSettings = [Settings build]; + [sharedInstance.auSettings registerItems:@[ + // common + [SettingItem buildGroup:SETTING_COMMON val:@"" hint:@""], + [SettingItem buildString:SETTING_APPID val:SDEF_AU_DEFAULT_APP_ID hint:@""], + [SettingItem buildString:SETTING_TOKEN val:SDEF_TOKEN hint:@""], + [SettingItem buildString:SETTING_CLUSTER val:SDEF_AU_DEFAULT_CLUSTER hint:SETTING_HINT_CLUSTER], + [SettingItem buildString:SETTING_ADDRESS val:SDEF_AU_DEFAULT_ADDRESS hint:SETTING_HINT_ADDRESS], + [SettingItem buildString:SETTING_URI val:SDEF_AU_DEFAULT_URI hint:SETTING_HINT_URI], + [SettingItem buildOptions:SETTING_RECORD_TYPE val:[SettingOptions build:SETTING_OPTIONS_RECORD_TYPE() choose:0] hint:@""], + + // au + [SettingItem buildGroup:SETTING_AU val:@"" hint:@""], + [SettingItem buildOptions:SETTING_AU_ABILITY val:[SettingOptions build:SETTING_OPTIONS_AU_ABILITY() choose:2] hint:@""], + [SettingItem buildBool:SETTING_AU_RECORDER_SAVE val:false hint:@""], + [SettingItem buildInt:SETTING_AU_PROCESS_TIMEOUT val:3000 hint:@""], + [SettingItem buildInt:SETTING_AU_AUDIO_PACKET_DURATION val:80 hint:@""], + [SettingItem buildInt:SETTING_AU_EMPTY_PACKET_INTERVAL val:500 hint:@""], + [SettingItem buildInt:SETTING_VAD_MAX_SPEECH_DURATION val:60000 hint:@""], + [SettingItem buildInt:SETTING_VAD_MAX_MUSIC_DURATION val:12000 hint:@""], + + // asr + [SettingItem buildGroup:SETTING_ASR val:@"" hint:@""], + [SettingItem buildBool:SETTING_ASR_ENABLE_DDC val:false hint:@""], + [SettingItem buildBool:SETTING_ASR_ENABLE_ITN val:false hint:@""], + [SettingItem buildBool:SETTING_ASR_ENABLE_NLU_PUNC val:false hint:@""], + [SettingItem buildBool:SETTING_ASR_SHOW_LANGUAGE val:false hint:@""], + [SettingItem buildString:SETTING_ASR_LANGUAGE val:@"en-US" hint:@""], + [SettingItem buildString:SETTING_ASR_HOTWORDS val:@"{\"hotwords\":[{\"word\":\"过秦论\",\"scale\":\"2.0\"}]}" hint:@""], + [SettingItem buildInt:SETTING_ASR_VAD_START_SILENCE_TIME val:0 hint:@""], + [SettingItem buildInt:SETTING_ASR_VAD_END_SILENCE_TIME val:0 hint:@""], + [SettingItem buildString:SETTING_ASR_VAD_MODE val:@"" hint:@""], + [SettingItem buildOptions:SETTING_ASR_RESULT_TYPE val:[SettingOptions build:SETTING_OPTIONS_ASR_RESULT_TYPE() choose:0] hint:@""], + ]]; + + // Afp + sharedInstance.afpSettings = [Settings build]; + [sharedInstance.afpSettings registerItems:@[ + // afp + [SettingItem buildGroup:SETTING_AFP val:@"" hint:@""], + [SettingItem buildOptions:SETTING_MUSIC_ENGINE_NAME val:[SettingOptions build:SETTING_OPTIONS_MUSIC_ENGINE_NAME() choose:0] hint:@""], + [SettingItem buildOptions:SETTING_AFP_RESULT_TYPE val:[SettingOptions build:SETTING_OPTIONS_AFP_RESULT_TYPE() choose:0] hint:@""], + ]]; + + // Kws settings + sharedInstance.kwsSettings = [Settings build]; + [sharedInstance.kwsSettings registerItems:@[ + // common + [SettingItem buildGroup:SETTING_COMMON val:@"" hint:@""], + [SettingItem buildOptions:SETTING_RECORD_TYPE val:[SettingOptions build:SETTING_OPTIONS_RECORD_TYPE() choose:0] hint:@""], + // kws + [SettingItem buildGroup:SETTING_KWS val:@"" hint:@""], + [SettingItem buildString:SETTING_KWS_MODEL_NAME val:@"aispeech_kws_douyin" hint:@""], + [SettingItem buildString:SETTING_KWS_CUSTOM_WORDS val:@"{\"word_list\":[{\"name\":\"大力大力\",\"keyword_type\":0,\"min_dur\":0.15,\"max_dur\":3,\"threshold\":-3.6}]}" hint:@""], + ]]; + + // Test Afp + sharedInstance.testAfpSettings = [Settings build]; + [sharedInstance.testAfpSettings registerItems:@[ + // afp + [SettingItem buildGroup:SETTING_AFP val:@"" hint:@""], + [SettingItem buildOptions:SETTING_AFP_RESULT_TYPE val:[SettingOptions build:SETTING_OPTIONS_AFP_RESULT_TYPE() choose:0] hint:@""], + [SettingItem buildInt:SETTING_AFP_INSTANCE_NUMBER val:10 hint:@""], + ]]; + + // Test: asr offline rtf settins + sharedInstance.testAsrOfflineRtfSettings = [Settings build]; + [sharedInstance.testAsrOfflineRtfSettings registerItems:@[ + // common + [SettingItem buildGroup:SETTING_COMMON val:@"" hint:@""], + [SettingItem buildInt:SETTING_STREAM_PACKAGE_DURATION val:20000 hint:@""], + + // asr + [SettingItem buildGroup:SETTING_ASR val:@"" hint:@""], + [SettingItem buildBool:SETTING_ASR_RECORDER_SAVE val:false hint:@""], + [SettingItem buildBool:SETTING_ASR_ENABLE_ITN val:false hint:@""], + [SettingItem buildBool:SETTING_ASR_SHOW_LANGUAGE val:false hint:@""], + ]]; + + // Test: asr stress settings + sharedInstance.testAsrStressSettings = [Settings build]; + [sharedInstance.testAsrStressSettings registerItems:@[ + // stress + [SettingItem buildGroup:SETTING_ASR_STRESS val:@"" hint:@""], + [SettingItem buildOptions:SETTING_ASR_STRESS_SCENEID val:[SettingOptions build:SETTING_OPTIONS_ASR_STRESS_TYPE() choose:0] hint:@""], + + // common + [SettingItem buildGroup:SETTING_COMMON val:@"" hint:@""], + [SettingItem buildString:SETTING_APPID val:SDEF_APPID hint:@""], + [SettingItem buildString:SETTING_TOKEN val:SDEF_TOKEN hint:@""], + [SettingItem buildString:SETTING_CLUSTER val:SDEF_ASR_DEFAULT_CLUSTER hint:SETTING_HINT_CLUSTER], + [SettingItem buildString:SETTING_ADDRESS val:SDEF_DEFAULT_ADDRESS hint:SETTING_HINT_ADDRESS], + [SettingItem buildString:SETTING_URI val:SDEF_ASR_DEFAULT_URI hint:SETTING_HINT_URI], + [SettingItem buildBool:SETTING_GET_VOLUME val:false hint:@""], + [SettingItem buildOptions:SETTING_RECORD_TYPE val:[SettingOptions build:SETTING_OPTIONS_RECORD_TYPE() choose:0] hint:@""], + [SettingItem buildBool:SETTING_RESTART_AUDIO_SESSION_ENABLE val:false hint:@""], + [SettingItem buildBool:SETTING_RESUME_OTHERS_INTERRUPTED_PLAYBACK_ENABLE val:false hint:@""], + [SettingItem buildInt:SETTING_VAD_MAX_SPEECH_DURATION val:15000 hint:@""], + [SettingItem buildInt:SETTING_STREAM_PACKAGE_DURATION val:20000 hint:@""], + + // asr + [SettingItem buildGroup:SETTING_ASR val:@"" hint:@""], + [SettingItem buildBool:SETTING_ASR_RECORDER_SAVE val:false hint:@""], + [SettingItem buildBool:SETTING_ASR_ENABLE_DDC val:false hint:@""], + [SettingItem buildBool:SETTING_ASR_ENABLE_ITN val:false hint:@""], + [SettingItem buildBool:SETTING_ASR_ENABLE_NLU_PUNC val:true hint:@""], + [SettingItem buildBool:SETTING_ASR_KEEP_RECORDING val:false hint:@""], + [SettingItem buildBool:SETTING_ASR_SHOW_LANGUAGE val:false hint:@""], + [SettingItem buildString:SETTING_ASR_LANGUAGE val:@"en-US" hint:@""], + [SettingItem buildString:SETTING_ASR_HOTWORDS val:@"{\"hotwords\":[{\"word\":\"过秦论\",\"scale\":\"2.0\"}]}" hint:@""], + [SettingItem buildInt:SETTING_ASR_VAD_START_SILENCE_TIME val:0 hint:@""], + [SettingItem buildInt:SETTING_ASR_VAD_END_SILENCE_TIME val:0 hint:@""], + [SettingItem buildString:SETTING_ASR_VAD_MODE val:@"" hint:@""], + [SettingItem buildOptions:SETTING_ASR_RESULT_TYPE val:[SettingOptions build:SETTING_OPTIONS_ASR_RESULT_TYPE() choose:0] hint:@""], + ]]; + + }); + return sharedInstance; +} + +- (Settings*)getSettings:(NSString*)viewId { + if ([viewId isEqualToString: VIEW_ASR]) { + return self.asrSettings; + } else if ([viewId isEqualToString: VIEW_BIGASR]) { + return self.bigAsrSettings; + } else if ([viewId isEqualToString: VIEW_ASR_OFFLINE]) { + return self.asrOfflineSettings; + } else if ([viewId isEqualToString: VIEW_CAPT]) { + return self.captSettings; + } else if ([viewId isEqualToString: VIEW_FULLLINK]) { + return self.fulllinkSettings; + } else if ([viewId isEqualToString: VIEW_TTS]) { + return self.ttsSettings; + } else if ([viewId isEqualToString: VIEW_VOICECLONE]) { + return self.voiceCloneSettings; + } else if ([viewId isEqualToString: VIEW_VOICECONV]) { + return self.voiceConvSettings; + } else if ([viewId isEqualToString: VIEW_DIALOG]) { + return self.dialogSettings; + } else if ([viewId isEqualToString: VIEW_DIALOG_DELEGATE]) { + return self.dialogDelegateSettings; + } else if ([viewId isEqualToString: VIEW_BITTS]) { + return self.bittsSettings; + } else if ([viewId isEqualToString: VIEW_UNITTS]) { + return self.uniTtsSettings; + } else if ([viewId isEqualToString: VIEW_VAD]) { + return self.vadSettings; + } else if ([viewId isEqualToString: VIEW_AU]) { + return self.auSettings; + } else if ([viewId isEqualToString: VIEW_AFP]) { + return self.afpSettings; + } else if ([viewId isEqualToString: VIEW_KWS]) { + return self.kwsSettings; + } else if ([viewId isEqualToString: VIEW_TEST_AFP]) { + return self.testAfpSettings; + } else if ([viewId isEqualToString: VIEW_TEST_ASR_OFFLINE_RTF]) { + return self.testAsrOfflineRtfSettings; + } else if ([viewId isEqualToString: VIEW_TEST_ASR_STRESS]) { + return self.testAsrStressSettings; + } else { + NSLog( @"View id %@ is not found yet!", viewId); + } + return [Settings build]; +} + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/SettingsViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/SettingsViewController.h new file mode 100644 index 0000000..fe2cb0c --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/SettingsViewController.h @@ -0,0 +1,19 @@ +// +// SettingsViewController.h +// SpeechDemo +// +// Created by fangweiwei on 2020/2/28. +// Copyright © 2020 fangweiwei. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SettingsViewController : UITableViewController + +@property (strong, nonatomic) NSString *viewId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/SettingsViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/SettingsViewController.m new file mode 100644 index 0000000..b62ab27 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/settings/SettingsViewController.m @@ -0,0 +1,42 @@ +// +// SettingsViewController.m +// SpeechDemo +// +// Created by fangweiwei on 2020/2/28. +// Copyright © 2020 fangweiwei. All rights reserved. +// + +#import "SettingsViewController.h" +#import "SettingsHelper.h" +#import "SettingViewDelegate.h" + +@interface SettingsViewController () + +@property (weak, nonatomic) IBOutlet UITableView *settingTableView; +@property (strong, nonatomic) SettingViewDelegate *settingViewDelegate; + +@end + +@implementation SettingsViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + SettingsHelper* settingsInstance = [SettingsHelper shareInstance]; + self.settingViewDelegate = [SettingViewDelegate build:[settingsInstance getSettings:self.viewId]]; + self.settingTableView.delegate = self.settingViewDelegate; + self.settingTableView.dataSource = self.settingViewDelegate; +} + + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + [segue destinationViewController]; +} + + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/test/TestAfpViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/test/TestAfpViewController.h new file mode 100644 index 0000000..107533c --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/test/TestAfpViewController.h @@ -0,0 +1,17 @@ +// +// TestAfpViewController.h +// SpeechDemo +// +// Created by bytedance on 2024/7/10. +// Copyright © 2024 chengzihao.ds. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface TestAfpViewController : UIViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/test/TestAfpViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/test/TestAfpViewController.m new file mode 100644 index 0000000..ad18848 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/test/TestAfpViewController.m @@ -0,0 +1,265 @@ +// +// TestAfpViewController.m +// SpeechDemo +// +// Created by chengzihao.ds on 2024/7/10. +// Copyright © 2024 chengzihao.ds. All rights reserved. +// + +#import "TestAfpViewController.h" + +#import + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" + +@interface AfpPressureTask : NSObject + +@property (nonatomic, strong) NSThread *thread; +@property (nonatomic, strong) SpeechEngine *engine; +@property (nonatomic, weak) NSString *debugPath; +@property (nonatomic, assign) int resultType; +@property (nonatomic, assign) int taskId; +@property (nonatomic, assign) BOOL running; + +- (instancetype)initWithTaskId:(int)taskId; +- (void)startTask; +- (void)stopTask; +- (void)taskLoop; +- (int)fetchResult; + +@end + +@implementation AfpPressureTask + +- (instancetype)initWithTaskId:(int)taskId { + self = [super init]; + if (self) { + _taskId = taskId; + _running = YES; + _engine = NULL; + _debugPath = NULL; + _thread = [[NSThread alloc] initWithTarget:self selector:@selector(taskLoop) object:nil]; + } + return self; +} + +- (void)startTask { + [self.thread start]; +} + +- (void)stopTask { + self.running = NO; +} + +- (void)taskLoop { + @autoreleasepool { + NSString *path = [self.debugPath stringByAppendingPathComponent:@"test_afp.pcm"]; + while (self.running) { + @autoreleasepool { + // Init + self.engine = [[SpeechEngine alloc] init]; + [self.engine createEngineWithDelegate:nil]; + [self.engine setStringParam:SE_AFP_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + SEEngineErrorCode ret = [self.engine initEngine]; + if (ret != SENoError) { + NSLog(@"Init Engine failed: %d", ret); + } + + // Process + NSError *error; + NSData *data = [NSData dataWithContentsOfFile:path options:NSDataReadingMappedIfSafe error:&error]; + if (data) { + // Read success. + ret = [self.engine ProcessAudio:(int16_t *)data.bytes length:(int32_t)(data.length / 2) isFinal:TRUE]; + if (ret != SENoError) { + NSLog(@"Process Audio failed: %d", ret); + } + } + + // FetchResult + ret = [self fetchResult]; + if (ret != SENoError) { + NSLog(@"Fetch Result failed: %d", ret); + } + + // Reset + ret = [self.engine ResetEngine]; + if (ret != SENoError) { + NSLog(@"Reset Engine failed: %d", ret); + } + + // Destroy + [self.engine destroyEngine]; + self.engine = nil; + } + } + NSLog(@"Task %d has stopped", self.taskId); + } +} + +- (int) fetchResult { + @autoreleasepool { + NSString *filename; + if (self.resultType == SEAfpResult) { + filename = [NSString stringWithFormat:@"test_afp_out_%d.bytes", self.taskId]; + } else { + filename = [NSString stringWithFormat:@"test_afp_out_%d.json", self.taskId]; + } + + if (self.resultType == SEAfpResult) { + NSData *result; + SEEngineErrorCode ret = [self.engine FetchResult:&result]; + if (ret != SENoError) { + NSLog(@"Fetch Result failed: %d", ret); + return ret; + } + + NSFileHandle *file = [FileUtils openFileForWriting:filename inPath:self.debugPath]; + [FileUtils writeData:result toFileHandel:file]; + [FileUtils closeFile:file]; + } else if (self.resultType == SEAfpSliceResult) { + NSString* result = [self.engine FetchStringResult:SEAfpSliceResult]; + // 提取err_code的值 + NSError *error; + NSDictionary *jsonResult = [NSJSONSerialization JSONObjectWithData:[result dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&error]; + int errCode = [jsonResult[@"err_code"] intValue]; + if (errCode != SENoError) { + NSLog(@"Fetch Result failed: %d", errCode); + return errCode; + } + + NSFileHandle *file = [FileUtils openFileForWriting:filename inPath:self.debugPath]; + [FileUtils writeString:result toFileHandel:file]; + [FileUtils closeFile:file]; + } + } + return 0; +} + +@end + +@interface TestAfpViewController () + +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; + +@property (strong, nonatomic) NSMutableArray *tasks; + +@property (nonatomic, strong) NSString *deviceID; +@property (strong, nonatomic) NSString *debugPath; + +// Settings +@property (strong, nonatomic) Settings *settings; +@end + +@implementation TestAfpViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [self.statusTextView setText:@"Waiting for init."]; + [self decorateTextView:self.resultTextView]; + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_TEST_AFP]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [self stopPressureTest]; + [super viewDidDisappear:animated]; +} + +- (void)decorateTextView:(UITextView *)textView { + textView.layer.cornerRadius = 5.0f; + textView.layer.borderWidth = .25f; + textView.layer.borderColor = [UIColor grayColor].CGColor; +} + + +#pragma mark - UI Actions + +- (IBAction)startStopPressureTest:(id)sender { + if (self.tasks == NULL) { + [self startPressureTest]; + [self.resultTextView setText: [NSString stringWithFormat:@"压测开始: %d个实例", [self.settings getInt: SETTING_AFP_INSTANCE_NUMBER]]]; + } else { + [self stopPressureTest]; + [self.resultTextView setText: @"压测结束"]; + } +} + +#pragma mark - Init Methods + +- (void)startPressureTest { + if (self.tasks != NULL) { + return; + } + + self.tasks = [NSMutableArray array]; + for (int i = 0; i < [self.settings getInt: SETTING_AFP_INSTANCE_NUMBER]; i++) { + AfpPressureTask *task = [[AfpPressureTask alloc] initWithTaskId:i]; + task.resultType = [self getResultType]; + task.debugPath = self.debugPath; + [self.tasks addObject:task]; + [task startTask]; + } +} + +- (void)stopPressureTest { + if (self.tasks == NULL) { + return; + } + + // 停止所有任务 + for (AfpPressureTask *task in self.tasks) { + [task stopTask]; + } + + // 保证全部任务执行完毕 + for (AfpPressureTask *task in self.tasks) { + while (![task.thread isFinished]) { + [NSThread sleepForTimeInterval:0.1]; + } + } + + self.tasks = NULL; +} + +#pragma mark - Helper +- (SEResultType)getResultType { + SettingOptions* resultTypeOptions = [self.settings getOptions:SETTING_AFP_RESULT_TYPE]; + switch (resultTypeOptions.chooseIdx) { + case 0: + return SEAfpResult; + case 1: + default: + return SEAfpSliceResult; + } +} + +#pragma mark - UITextViewDelegate + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if([text isEqualToString:@"\n"]) { + [textView resignFirstResponder]; + return NO; + } + return YES; +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_TEST_AFP forKey:@"viewId"]; +} + +@end + + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/test/TestAsrOfflineRtfViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/test/TestAsrOfflineRtfViewController.h new file mode 100644 index 0000000..d68cc19 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/test/TestAsrOfflineRtfViewController.h @@ -0,0 +1,13 @@ +// +// TestAsrOfflineRtfViewController.h +// SpeechDemo +// +// Created by bytedance on 2023/6/6. +// Copyright © 2023 chengzihao.ds. All rights reserved. +// + +#import + +@interface TestAsrOfflineRtfViewController : UIViewController + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/test/TestAsrOfflineRtfViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/test/TestAsrOfflineRtfViewController.m new file mode 100644 index 0000000..3de2f83 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/test/TestAsrOfflineRtfViewController.m @@ -0,0 +1,339 @@ +// +// TestAsrOfflineRtfViewController.m +// SpeechDemo +// +// Created by bytedance on 2023/6/6. +// Copyright © 2023 chengzihao.ds. All rights reserved. +// + +#import "TestAsrOfflineRtfViewController.h" + +#import + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" + +@interface TestAsrOfflineRtfViewController () + +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; +@property (weak, nonatomic) IBOutlet UIButton *engineInitButton; +@property (weak, nonatomic) IBOutlet UIButton *engineUninitButton; +@property (weak, nonatomic) IBOutlet UIButton *startEngineButton; +@property (weak, nonatomic) IBOutlet UIButton *stopEngineButton; + +@property (strong, nonatomic) SpeechEngine *curEngine; +@property (assign, nonatomic) BOOL engineStarted; + +@property (nonatomic, strong) NSString *deviceID; +@property (nonatomic, assign) long feedTimestamp; +@property (nonatomic, assign) int audioLength; +@property (strong, nonatomic) NSString *debugPath; + +// settings +@property (strong, nonatomic) Settings *settings; + +@end + +@implementation TestAsrOfflineRtfViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_TEST_ASR_OFFLINE_RTF]; + + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; + [self.statusTextView setText:@"Waiting for init."]; + [self decorateTextView:self.resultTextView]; + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + self.engineStarted = FALSE; +} + +- (void)viewDidDisappear:(BOOL)animated { + [self uninitEngine]; + [super viewDidDisappear:animated]; +} + +- (void)decorateTextView:(UITextView *)textView { + textView.layer.cornerRadius = 5.0f; + textView.layer.borderWidth = .25f; + textView.layer.borderColor = [UIColor grayColor].CGColor; +} + +#pragma mark - SpeechEngineDelegate + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSLog(@"Message Type: %d.", type); + switch (type) { + case SEEngineStart: + [self speechEngineStarted]; + break; + case SEEngineStop: + [self speechEngineStopped]; + break; + case SEEngineError: + [self speechEngineError:data]; + break; + case SEAsrPartialResult: + [self speechEngineResult:data isFinal:FALSE]; + break; + case SEFinalResult: + [self speechEngineResult:data isFinal:TRUE]; + break; + case SEVolumeLevel: + NSLog(@"volume level: %s", (char*)data.bytes); + break; + case SEEngineLog: + NSLog(@"engine log: %s", (char*)data.bytes); + break; + default: + break; + } +} + +#pragma mark - UI Actions + +- (IBAction)initEngine:(id)sender { + [self initEngine]; +} + +- (IBAction)uninitEngine:(id)sender { + if (self.engineStarted) { + [self.statusTextView setText:@"Engine is busy, stop it first!"]; + return; + } + [self uninitEngine]; + [self.resultTextView setTextColor:UIColor.grayColor]; + [self.resultTextView setText:@"点击或按住说话后,展示语音识别结果"]; +} + +- (IBAction)startEngine:(id)sender { + NSLog(@"Start engine."); + [self.curEngine setStringParam:[self.settings getString:SETTING_APPID] forKey:SE_PARAMS_KEY_APP_ID_STRING]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_ENABLE_ITN] forKey:SE_PARAMS_KEY_ASR_ENABLE_ITN_BOOL]; + [self.curEngine setBoolParam:FALSE forKey:SE_PARAMS_KEY_ASR_AUTO_STOP_BOOL]; + + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + return; + } +} + +- (IBAction)stopEngine:(id)sender { + NSLog(@"Stop engine."); + [self.curEngine sendDirective:SEDirectiveStopEngine]; +} + +#pragma mark - Init Methods + +- (void)initEngine { + AppDelegate *appDelegate = [ViewController getAppDelegate]; + if (appDelegate == nil) { + appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; + } + if (appDelegate.deviceID.length < 1) { + self.engineInitButton.enabled = FALSE; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:@"Waiting for get deviceID."]; + sleep(1); + [self initEngine]; + }); + return; + } + [ViewController setAppDelegate:appDelegate]; + self.deviceID = appDelegate.deviceID; + + + if (self.curEngine == nil) { + self.curEngine = [[SpeechEngine alloc] init]; + } + if (![self.curEngine createEngineWithDelegate:self]) { + NSLog(@"Create speech engine failed."); + return; + } + + [self.resultTextView setTextColor:UIColor.blackColor]; + NSLog(@"Engine version: %@", [self.curEngine getVersion]); + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"Debug path: %@", self.debugPath); + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + [self.curEngine setStringParam:SE_LOG_LEVEL_TRACE forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + [self.curEngine setStringParam:[self.settings getString:SETTING_APPID] forKey:SE_PARAMS_KEY_APP_ID_STRING]; + [self.curEngine setStringParam:@"388808087185088" forKey:SE_PARAMS_KEY_UID_STRING]; + [self.curEngine setIntParam:1 forKey:SE_PARAMS_KEY_CHANNEL_NUM_INT]; + [self.curEngine setBoolParam:FALSE forKey:SE_PARAMS_KEY_ASR_AUTO_STOP_BOOL]; + [self.curEngine setStringParam:@"" forKey:SE_PARAMS_KEY_ASR_REC_PATH_STRING]; + if ([self.settings getBool:SETTING_ASR_RECORDER_SAVE]) { + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_ASR_REC_PATH_STRING]; + } + [self.curEngine setStringParam:SE_RECORDER_TYPE_STREAM forKey:SE_PARAMS_KEY_RECORDER_TYPE_STRING]; + [self.curEngine setStringParam:SE_ASR_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + [self.curEngine setIntParam:16000 forKey:SE_PARAMS_KEY_SAMPLE_RATE_INT]; + [self.curEngine setBoolParam:true forKey:SE_PARAMS_KEY_ASR_SHOW_UTTER_BOOL]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_SHOW_LANGUAGE] forKey:SE_PARAMS_KEY_ASR_SHOW_LANG_BOOL]; + [self.curEngine setBoolParam:true forKey:SE_PARAMS_KEY_ASR_SHOW_VOLUME_BOOL]; + [self.curEngine setIntParam:SEAsrWorkModeOfflineFlute forKey:SE_PARAMS_KEY_ASR_WORK_MODE_INT]; + NSString* modelsPath = [NSString stringWithFormat:@"%@/models", self.debugPath]; + [self.curEngine setStringParam:modelsPath forKey:SE_PARAMS_KEY_ASR_OFF_RESOURCE_PATH_STRING]; + NSLog(@"Models path: %@", modelsPath); + + SEEngineErrorCode ret = [self.curEngine initEngine]; + if (ret != SENoError) { + NSLog(@"Init Engine failed: %ld", ret); + } + if (ret == SENoError) { + [self speechEngineInitOk]; + } else { + [self speechEngineInitFailed]; + } +} + +- (void)uninitEngine { + [self.curEngine destroyEngine]; + self.curEngine = nil; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = FALSE; +} + +- (void)setHotWords:(NSString*) hotWords { + [self.curEngine sendDirective:SEDirectiveUpdateAsrHotWords data: hotWords]; +} + +#pragma mark - Engine Callback + +- (void)speechEngineNoPermission { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"No permission!"]; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + }); +} + +- (void)speechEngineInitOk { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.statusTextView setText:@"Ready"]; + [self.resultTextView setText:[NSString stringWithFormat:@"DeviceID: %@", self.deviceID]]; + self.engineUninitButton.enabled = TRUE; + self.engineInitButton.enabled = FALSE; + self.startEngineButton.enabled = TRUE; + }); +} + +- (void)speechEngineInitFailed { + dispatch_async(dispatch_get_main_queue(), ^{ + [self uninitEngine]; + [self.statusTextView setText:@"Failed to init engine!"]; + self.engineInitButton.enabled = TRUE; + self.engineUninitButton.enabled = FALSE; + }); +} + +- (void)speechEngineStarted { + dispatch_async(dispatch_get_main_queue(), ^{ + self.engineStarted = true; + [self.statusTextView setText:@"Engine Started!"]; + self.startEngineButton.enabled = FALSE; + self.stopEngineButton.enabled = TRUE; + + // Read whole file into nsdata. + NSString* filePath = [NSString stringWithFormat:@"%@/%@", self.debugPath, @"asr_rec_file.pcm"]; + NSData *fileData = [NSData dataWithContentsOfFile:filePath]; + if (!fileData) { + [self.resultTextView setText:@"ERROR: File asr_rec_file.pcm not found!"]; + [self stopEngine:NULL]; + return; + } + + int maxLength = [self.settings getInt:SETTING_STREAM_PACKAGE_DURATION] * 16 * 2; + int16_t* feedData = (int16_t*) malloc(maxLength * sizeof(int16_t)); + self.audioLength = MIN(maxLength, [fileData length]); + memcpy(feedData, [fileData bytes], self.audioLength); + self.feedTimestamp = [[NSDate date] timeIntervalSince1970] * 1000; + [self.curEngine feedAudio:feedData length:self.audioLength / 2]; + [self.curEngine sendDirective:SEDirectiveFinishTalking]; + + free(feedData); + feedData = NULL; + }); +} + +- (void)speechEngineStopped { + dispatch_async(dispatch_get_main_queue(), ^{ + self.engineStarted = FALSE; + [self.statusTextView setText:@"Engine Stopped!"]; + self.startEngineButton.enabled = TRUE; + self.stopEngineButton.enabled = FALSE; + }); +} + +- (void)speechEngineResult:(NSData *)data isFinal:(BOOL)isFinal { + dispatch_async(dispatch_get_main_queue(), ^{ + long delay = 0; + if (isFinal) { + long current = [[NSDate date] timeIntervalSince1970] * 1000; + delay = current - self.feedTimestamp; + } + + NSError *error; + NSDictionary *jsonResult = [NSJSONSerialization JSONObjectWithData:data + options:NSJSONReadingMutableContainers + error:&error]; + NSMutableString *text = [[NSMutableString alloc] initWithString:@""]; + if (![jsonResult objectForKey:@"result"]) { + return; + } + [text appendFormat:@"result: %@", [[[jsonResult objectForKey:@"result"] firstObject] objectForKey:@"text"]]; + if (isFinal) { + [text appendFormat:@"\naudio_length: %ld", self.audioLength]; + [text appendFormat:@"\nresponse_delay: %ld", delay]; + [text appendFormat:@"\nrtf: %f", (float) delay / (self.audioLength / 16 / 2)]; + } + if (text.length) { + [self.resultTextView setText:[text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; + NSLog(@"asr test result: %@", text); + } + }); +} + +- (void)speechEngineError:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]]; + }); +} + +- (void)setResultText:(NSString *)result { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[result stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; + }); +} + +#pragma mark - UITextViewDelegate + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if([text isEqualToString:@"\n"]) { + [textView resignFirstResponder]; + return NO; + } + return YES; +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_TEST_ASR_OFFLINE_RTF forKey:@"viewId"]; +} + +@end + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/test/TestAsrViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/test/TestAsrViewController.h new file mode 100644 index 0000000..fd5512c --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/test/TestAsrViewController.h @@ -0,0 +1,13 @@ +// +// AsrViewController.h +// SpeechDemo +// +// Created by bytedance on 2020/9/8. +// Copyright © 2020 fengkai.0518. All rights reserved. +// + +#import + +@interface TestAsrViewController : UIViewController + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/test/TestAsrViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/test/TestAsrViewController.m new file mode 100644 index 0000000..20512f3 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/test/TestAsrViewController.m @@ -0,0 +1,519 @@ +// +// TestAsrViewController.m +// SpeechDemo +// +// Created by bytedance on 2020/9/8. +// Copyright © 2020 fengkai.0518. All rights reserved. +// + +#import "TestAsrViewController.h" + +#import + +#import "AppDelegate.h" +#import "FileUtils.h" +#import "SettingsHelper.h" +#import "ViewController.h" +#import "SensitiveDefines.h" + +@interface TestAsrViewController () + +// UI +@property (weak, nonatomic) IBOutlet UITextView *resultTextView; +@property (weak, nonatomic) IBOutlet UITextField *statusTextView; +@property (weak, nonatomic) IBOutlet UIButton *startButton; +@property (weak, nonatomic) IBOutlet UIButton *stopButton; + +// Device ID: 用于定位线上问题 +@property (nonatomic, strong) NSString *deviceID; +// Debug Path: 用于存放一些 SDK 相关的文件,比如模型、日志等 +@property (strong, nonatomic) NSString *debugPath; + +// SpeechEngine +@property (strong, nonatomic) SpeechEngine *curEngine; + +// Settings +@property (strong, nonatomic) Settings *settings; + +// APP 层自定义的录音机,在音频来源为 Stream 时使用 +@property (weak, nonatomic) StreamRecorder *streamRecorder; + +// 一些用于统计的字段 +@property (nonatomic, assign) long talkingFinisheTimestamp; + +// 压测相关 +@property (nonatomic, assign) NSInteger stressSceneId; +@property (assign, nonatomic) BOOL stressStarted; +@property (nonatomic, strong) NSThread *testThread; +@end + +@implementation TestAsrViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.startButton.enabled = TRUE; + self.stopButton.enabled = TRUE; + self.stressStarted = FALSE; + self.testThread = nil; + + [self decorateTextView:self.resultTextView]; + [self.resultTextView setTextColor:UIColor.blackColor]; + + [ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]]; + + self.streamRecorder = [ViewController getStreamRecorder]; + self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_TEST_ASR_STRESS]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [self uninitEngine]; + [super viewDidDisappear:animated]; +} + +- (void)decorateTextView:(UITextView *)textView { + textView.layer.cornerRadius = 5.0f; + textView.layer.borderWidth = .25f; + textView.layer.borderColor = [UIColor grayColor].CGColor; +} + +#pragma mark - Config & Init & Uninit Methods + +-(void)configInitParams{ + //【必需配置】Engine Name + [self.curEngine setStringParam:SE_ASR_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + + //【可选配置】Debug & Log + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + [self.curEngine setStringParam:SE_LOG_LEVEL_DEBUG forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + + //【可选配置】UID & deviceID: 用于定位线上问题 + [self.curEngine setStringParam:SDEF_UID forKey:SE_PARAMS_KEY_UID_STRING]; + [self.curEngine setStringParam:self.deviceID forKey:SE_PARAMS_KEY_DEVICE_ID_STRING]; + + //【必需配置】配置音频来源 + [self.curEngine setStringParam:[self.settings getOptionsValue:SETTING_RECORD_TYPE] forKey:SE_PARAMS_KEY_RECORDER_TYPE_STRING]; + + if ([self.settings getBool:SETTING_ASR_RECORDER_SAVE]) { + //【可选配置】录音文件保存路径,如配置,SDK会将录音保存到该路径下,文件格式为 .wav + [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_ASR_REC_PATH_STRING]; + } + + // 当音频来源为 RECORDER_TYPE_STREAM 时,如输入音频采样率不等于 16K,需添加如下配置 + if ([self.settings getOptionsValue:SETTING_RECORD_TYPE] == SE_RECORDER_TYPE_STREAM) { + if ([self.streamRecorder getSampleRate] != 16000) { + // 当音频来源为 RECORDER_TYPE_STREAM 时【必需配置】,否则【无需配置】 + // 启用 SDK 内部的重采样 + [self.curEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_ENABLE_RESAMPLER_BOOL]; + // 将重采样所需的输入采样率设置为 APP 层输入的音频的实际采样率 + [self.curEngine setIntParam:[self.streamRecorder getSampleRate] forKey:SE_PARAMS_KEY_CUSTOM_SAMPLE_RATE_INT]; + } + } + + NSString *address = [self.settings getString:SETTING_ADDRESS]; + if (!address.length) { + address = SDEF_DEFAULT_ADDRESS; + } + NSLog(@"Current address: %@", address); + //【必需配置】识别服务域名 + [self.curEngine setStringParam:address forKey:SE_PARAMS_KEY_ASR_ADDRESS_STRING]; + + NSString *uri = [self.settings getString:SETTING_URI]; + if (!uri.length) { + uri = SDEF_ASR_DEFAULT_URI; + } + NSLog(@"Current uri: %@", uri); + //【必需配置】识别服务Uri + [self.curEngine setStringParam:uri forKey:SE_PARAMS_KEY_ASR_URI_STRING]; + + NSString* appID = [self.settings getString:SETTING_APPID]; + //【必需配置】鉴权相关:Appid + [self.curEngine setStringParam:appID.length <= 0 ? SDEF_APPID : appID forKey:SE_PARAMS_KEY_APP_ID_STRING]; + //【必需配置】鉴权相关:Token + NSString* token = [self.settings getString:SETTING_TOKEN]; + [self.curEngine setStringParam:token.length <= 0 ? SDEF_TOKEN : token forKey:SE_PARAMS_KEY_APP_TOKEN_STRING]; + + NSString* cluster = [self.settings getString:SETTING_CLUSTER]; + NSLog(@"Current cluster: %@", cluster); + //【必需配置】识别服务所用集群 + [self.curEngine setStringParam:cluster.length <= 0 ? SDEF_ASR_DEFAULT_CLUSTER : cluster forKey:SE_PARAMS_KEY_ASR_CLUSTER_STRING]; + + //【可选配置】在线请求的建连与接收超时,一般不需配置使用默认值即可 + [self.curEngine setIntParam:3000 forKey:SE_PARAMS_KEY_ASR_CONN_TIMEOUT_INT]; + [self.curEngine setIntParam:5000 forKey:SE_PARAMS_KEY_ASR_RECV_TIMEOUT_INT]; + + NSString* recPath = @""; + if ([self.settings getBool:SETTING_RECORD_SAVE]) { + recPath = self.debugPath; + } + [self.curEngine setStringParam:recPath forKey:SE_PARAMS_KEY_REC_PATH_STRING]; + [self.curEngine setIntParam:[self.settings getOptions:SETTING_RECORD_FILE_TYPE].chooseIdx forKey:SE_PARAMS_KEY_REC_FILE_TYPE_INT]; +} + +-(void)configStartAsrParams{ + //【可选配置】控制识别结果的配置 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_ENABLE_DDC] forKey:SE_PARAMS_KEY_ASR_ENABLE_DDC_BOOL]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_ENABLE_ITN] forKey:SE_PARAMS_KEY_ASR_ENABLE_ITN_BOOL]; + [self.curEngine setBoolParam:[self.settings getBool:SETTING_ASR_ENABLE_NLU_PUNC] forKey:SE_PARAMS_KEY_ASR_SHOW_NLU_PUNC_BOOL]; + + //【可选配置】控制识别结果返回的形式,全量返回或增量返回,默认为全量 + [self.curEngine setStringParam:[self.settings getOptionsValue:SETTING_ASR_RESULT_TYPE] forKey:SE_PARAMS_KEY_ASR_RESULT_TYPE_STRING]; + + //【可选配置】控制 ASR 中的 VAD 模块的阈值的配置 + [self.curEngine setIntParam:[self.settings getInt:SETTING_ASR_VAD_START_SILENCE_TIME] forKey:SE_PARAMS_KEY_ASR_VAD_START_SILENCE_TIME_INT]; + [self.curEngine setIntParam:[self.settings getInt:SETTING_ASR_VAD_END_SILENCE_TIME] forKey:SE_PARAMS_KEY_ASR_VAD_END_SILENCE_TIME_INT]; + [self.curEngine setStringParam:[self.settings getString:SETTING_ASR_VAD_MODE] forKey:SE_PARAMS_KEY_ASR_VAD_MODE_STRING]; + //【可选配置】用户音频输入最大时长,仅一句话识别场景生效,单位毫秒,默认为 150000ms. + [self.curEngine setIntParam:[self.settings getInt:SETTING_VAD_MAX_SPEECH_DURATION] forKey:SE_PARAMS_KEY_VAD_MAX_SPEECH_DURATION_INT]; + + //【可选配置】控制是否返回录音音量,在 APP 需要显示音频波形时可以启用 + [self.curEngine setBoolParam:[self.settings getBool:SETTING_GET_VOLUME] forKey:SE_PARAMS_KEY_ENABLE_GET_VOLUME_BOOL]; + + //【可选配置】更新 ASR 热词 + if ([self.settings getString:SETTING_ASR_HOTWORDS].length != 0) { + [self setHotWords:[self.settings getString:SETTING_ASR_HOTWORDS]]; + } + + if ([self.settings getOptionsValue:SETTING_RECORD_TYPE] == SE_RECORDER_TYPE_STREAM) { + if (![self.streamRecorder start]) { + [self speechEngineNoPermission]; + } + } else if ([self.settings getOptionsValue:SETTING_RECORD_TYPE] == SE_RECORDER_TYPE_FILE) { + // 使用音频文件识别时,需要设置文件的绝对路径 + NSString* file_path = [NSString stringWithFormat:@"%@/%@", self.debugPath, @"asr_rec_file.pcm"]; + NSLog(@"输入的音频文件路径: %@", file_path); + // 使用音频文件识别时【必须配置】,否则【无需配置】 + [self.curEngine setStringParam:file_path forKey:SE_PARAMS_KEY_RECORDER_FILE_STRING]; + } +} + +- (void)setHotWords:(NSString*) hotWords { + [self.curEngine sendDirective:SEDirectiveUpdateAsrHotWords data: hotWords]; +} + +- (void)initEngine { + NSLog(@"获取设备ID,调试使用"); + AppDelegate *appDelegate = [ViewController getAppDelegate]; + if (appDelegate == nil) { + appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; + } + [ViewController setAppDelegate:appDelegate]; + self.deviceID = appDelegate.deviceID; + if ([self.deviceID isEqualToString:@""]) { + self.deviceID = @"speech_test_123"; + } + NSLog(@"获取设备ID成功: %@", self.deviceID); + + + NSLog(@"创建引擎"); + if (self.curEngine == nil) { + self.curEngine = [[SpeechEngine alloc] init]; + if (![self.curEngine createEngineWithDelegate:self]) { + NSLog(@"引擎创建失败."); + return; + } + } + NSLog(@"SDK 版本号: %@", [self.curEngine getVersion]); + + self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; + NSLog(@"当前调试路径: %@", self.debugPath); + + NSLog(@"配置初始化参数"); + [self configInitParams]; + + NSLog(@"引擎初始化"); + SEEngineErrorCode ret = [self.curEngine initEngine]; + if (ret == SENoError) { + NSLog(@"初始化成功"); + } else { + NSLog(@"初始化失败,返回值: %d", ret); + } +} + +- (void)uninitEngine { + if (self.curEngine != nil) { + NSLog(@"引擎析构"); + [self.curEngine destroyEngine]; + self.curEngine = nil; + NSLog(@"引擎析构完成"); + } +} + +- (void)startEngine { + NSLog(@"配置启动参数"); + [self configStartAsrParams]; + + //【可选配置】是否启用云端自动判停,仅一句话识别场景生效 + NSLog(@"开启 ASR 云端自动判停"); + [self.curEngine setBoolParam:TRUE forKey:SE_PARAMS_KEY_ASR_AUTO_STOP_BOOL]; + + NSLog(@"启用引擎."); + NSLog(@"Directive: SEDirectiveStartEngine"); + SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine]; + if (ret == SERecCheckEnvironmentFailed) { + [self speechEngineNoPermission]; + } +} + +- (void)stopEngine { + NSLog(@"异步关闭引擎"); + NSLog(@"Directive: SEDirectiveStopEngine"); + [self.curEngine sendDirective:SEDirectiveStopEngine]; +} + +- (void)syncStopEngine { + NSLog(@"同步关闭引擎"); + NSLog(@"Directive: SEDirectiveStopEngine"); + [self.curEngine sendDirective:SEDirectiveSyncStopEngine]; +} + +- (void)finishTalking { + NSLog(@"结束音频输入"); + NSLog(@"Directive: SEDirectiveFinishTalking"); + [self.curEngine sendDirective:SEDirectiveFinishTalking]; +} + +- (void)speechEngineNoPermission { + NSLog(@"没有音频权限"); +} + +- (void)sleep:(NSTimeInterval)ti { + [NSThread sleepForTimeInterval:ti]; +} + +- (int)getRandomValue:(int)a andData:(int)b { + return (arc4random() % (b + 1)) + a; +} + +- (void)testScene0 { + [self initEngine]; + while (self.stressStarted) { + [self syncStopEngine]; + [self startEngine]; + [self sleep:[self getRandomValue:0 andData:3]]; + if ([self getRandomValue:0 andData:1] == 0) { + [self finishTalking]; + } else { + [self stopEngine]; + } + [self sleep:[self getRandomValue:0 andData:3]]; + } + [self uninitEngine]; +} + +- (void)testScene1 { + while (self.stressStarted) { + [self initEngine]; + [self syncStopEngine]; + [self startEngine]; + [self sleep:[self getRandomValue:0 andData:3]]; + if ([self getRandomValue:0 andData:1] == 0) { + [self finishTalking]; + } else { + [self stopEngine]; + } + [self sleep:[self getRandomValue:0 andData:3]]; + [self uninitEngine]; + } +} + +- (void)testScene2 { + while (self.stressStarted) { + [self initEngine]; + [self syncStopEngine]; + [self startEngine]; + [self sleep:1]; + [self finishTalking]; + [self sleep:2]; + [self uninitEngine]; + } +} + +- (void)testScene3 { + [self initEngine]; + while (self.stressStarted) { + int method = [self getRandomValue:0 andData:4]; + switch (method) { + case 0: + [self startEngine]; + break; + case 1: + [self syncStopEngine]; + break; + case 2: + [self stopEngine]; + break; + case 3: + [self finishTalking]; + break; + case 4: + [self uninitEngine]; + [self initEngine]; + break; + } + [self sleep:[self getRandomValue:0 andData:5]]; + } + [self uninitEngine]; +} + +- (void)test { + NSString* sceneid = [self.settings getOptionsValue:SETTING_ASR_STRESS_SCENEID]; + NSArray* sceneType = @[@"正常场景1", @"正常场景2", @"ERROR回调时析构", @"随机压测"]; + self.stressSceneId = [sceneType indexOfObject:sceneid]; + dispatch_async(dispatch_get_main_queue(), ^{ + NSString* str = [@"开始压测 " stringByAppendingString:sceneType[self.stressSceneId]]; + [self setResultText:str]; + }); + switch (self.stressSceneId) { + case 0: + [self testScene0]; + break; + case 1: + [self testScene1]; + break; + case 2: + [self testScene2]; + break; + case 3: + [self testScene3]; + break; + default: + break; + } + dispatch_async(dispatch_get_main_queue(), ^{ + NSString* str = [@"压测结束 " stringByAppendingString:sceneType[self.stressSceneId]]; + [self setResultText:str]; + }); +} + +#pragma mark - UI Actions + +- (IBAction)startBtnClicked:(id)sender { + if (self.testThread) { + if (self.testThread.executing) { + NSLog(@"Already start!"); + return; + } + [self.testThread cancel]; + self.testThread = nil; + } + self.stressStarted = TRUE; + self.testThread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil]; + [self.testThread start]; + +} + +- (IBAction)stopBtnClicked:(id)sender { + if (!self.testThread) { + NSLog(@"Not start yet!"); + return; + } + self.stressStarted = FALSE; +} + +#pragma mark - Message Callback + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSLog(@"Message Type: %d.", type); + switch (type) { + case SEEngineStart: + // Callback: 引擎启动成功回调 + NSLog(@"Callback: 引擎启动成功"); + break; + case SEEngineStop: + // Callback: 引擎关闭回调 + NSLog(@"Callback: 引擎关闭"); + break; + case SEEngineError: + // Callback: 错误信息回调 + NSLog(@"Callback: 错误信息: %@", data); + [self speechEngineError:data]; + break; + case SEAsrPartialResult: + // Callback: ASR 当前请求的部分结果回调 + NSLog(@"Callback: ASR 当前请求的部分结果"); + break; + case SEFinalResult: + // Callback: ASR 当前请求最终结果回调 + NSLog(@"Callback: ASR 当前请求最终结果"); + [self speechEngineResult:data isFinal:true]; + break; + case SEVolumeLevel: + // Callback: 录音音量回调 + NSLog(@"Callback: 录音音量"); + break; + default: + break; + } +} + +- (void)speechEngineError:(NSData *)data { + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.stressSceneId == 2) { + if ([self getRandomValue:0 andData:1] == 0) { + [self stopEngine]; + } + [self uninitEngine]; + } + }); +} + +- (void)speechEngineResult:(NSData *)data isFinal:(BOOL)isFinal { + dispatch_async(dispatch_get_main_queue(), ^{ + if (isFinal && self.stressSceneId == 2) { + if ([self getRandomValue:0 andData:1] == 0) { + [self stopEngine]; + } + [self uninitEngine]; + } + }); +} + +#pragma mark - Helper + +- (NSString *)getRecorderType { + SettingOptions* recorderTypeOptions = [self.settings getOptions:SETTING_RECORD_TYPE]; + switch (recorderTypeOptions.chooseIdx) { + case 0: + return SE_RECORDER_TYPE_RECORDER; + case 1: + return SE_RECORDER_TYPE_FILE; + case 2: + return SE_RECORDER_TYPE_STREAM; + default: + break; + } + return @""; +} + +- (void)setResultText:(NSString *)result { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.resultTextView setText:[result stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]; + }); +} + +- (long)timeDelayFrom:(long)pastTimestamp { + return [[NSDate date] timeIntervalSince1970] * 1000 - pastTimestamp; +} + +#pragma mark - UITextViewDelegate + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if([text isEqualToString:@"\n"]) { + [textView resignFirstResponder]; + return NO; + } + return YES; +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + id nextPage = [segue destinationViewController]; + [nextPage setValue:VIEW_TEST_ASR_STRESS forKey:@"viewId"]; +} + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/testdata/aec.model b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/testdata/aec.model new file mode 100644 index 0000000..1c045ce Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/testdata/aec.model differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/Contents.json b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..365a264 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,107 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "asr_lark_bot_ico_40_40.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "asr_lark_bot_ico_60_60.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "asr_lark_bot_ico_58_58.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "asr_lark_bot_ico_87_87.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "asr_lark_bot_ico_80_80.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "asr_lark_bot_ico_120_120.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "asr_lark_bot_ico_120_120-1.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "asr_lark_bot_ico_180_180.png", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "asr_lark_bot_ico_1024_1024.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_1024_1024.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_1024_1024.png new file mode 100644 index 0000000..9ee1095 Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_1024_1024.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_120_120-1.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_120_120-1.png new file mode 100644 index 0000000..182806f Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_120_120-1.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_120_120.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_120_120.png new file mode 100644 index 0000000..182806f Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_120_120.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_180_180.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_180_180.png new file mode 100644 index 0000000..17dd4e0 Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_180_180.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_40_40.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_40_40.png new file mode 100644 index 0000000..d356f48 Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_40_40.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_58_58.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_58_58.png new file mode 100644 index 0000000..58b38fb Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_58_58.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_60_60.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_60_60.png new file mode 100644 index 0000000..3163bf1 Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_60_60.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_80_80.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_80_80.png new file mode 100644 index 0000000..eb742c7 Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_80_80.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_87_87.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_87_87.png new file mode 100644 index 0000000..a7ae6c9 Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/AppIcon.appiconset/asr_lark_bot_ico_87_87.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/Contents.json b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/asr.imageset/Contents.json b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/asr.imageset/Contents.json new file mode 100644 index 0000000..3a01961 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/asr.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "asr_60_60.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "asr_120_120.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "asr_180_180.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/asr.imageset/asr_120_120.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/asr.imageset/asr_120_120.png new file mode 100644 index 0000000..18d87c1 Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/asr.imageset/asr_120_120.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/asr.imageset/asr_180_180.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/asr.imageset/asr_180_180.png new file mode 100644 index 0000000..5f217a9 Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/asr.imageset/asr_180_180.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/asr.imageset/asr_60_60.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/asr.imageset/asr_60_60.png new file mode 100644 index 0000000..dc5f467 Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/asr.imageset/asr_60_60.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/capt.imageset/Contents.json b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/capt.imageset/Contents.json new file mode 100644 index 0000000..44ff208 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/capt.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "capt_60_60.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "capt_120_120.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "capt_180_180.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/capt.imageset/capt_120_120.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/capt.imageset/capt_120_120.png new file mode 100644 index 0000000..c1040b9 Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/capt.imageset/capt_120_120.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/capt.imageset/capt_180_180.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/capt.imageset/capt_180_180.png new file mode 100644 index 0000000..23fd298 Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/capt.imageset/capt_180_180.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/capt.imageset/capt_60_60.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/capt.imageset/capt_60_60.png new file mode 100644 index 0000000..7b16e4e Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/capt.imageset/capt_60_60.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/fulllink.imageset/Contents.json b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/fulllink.imageset/Contents.json new file mode 100644 index 0000000..3e6ee20 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/fulllink.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "fulllink_60_60.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "fulllink_120_120.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "fulllink_180_180.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/fulllink.imageset/fulllink_120_120.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/fulllink.imageset/fulllink_120_120.png new file mode 100644 index 0000000..8ac6a6e Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/fulllink.imageset/fulllink_120_120.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/fulllink.imageset/fulllink_180_180.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/fulllink.imageset/fulllink_180_180.png new file mode 100644 index 0000000..31119ea Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/fulllink.imageset/fulllink_180_180.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/fulllink.imageset/fulllink_60_60.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/fulllink.imageset/fulllink_60_60.png new file mode 100644 index 0000000..e0907c3 Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/fulllink.imageset/fulllink_60_60.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/settings.imageset/Contents.json b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/settings.imageset/Contents.json new file mode 100644 index 0000000..f177a03 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/settings.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "settings.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "settings-1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "settings-2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/settings.imageset/settings-1.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/settings.imageset/settings-1.png new file mode 100644 index 0000000..9eb4f9e Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/settings.imageset/settings-1.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/settings.imageset/settings-2.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/settings.imageset/settings-2.png new file mode 100644 index 0000000..9eb4f9e Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/settings.imageset/settings-2.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/settings.imageset/settings.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/settings.imageset/settings.png new file mode 100644 index 0000000..9eb4f9e Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/settings.imageset/settings.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/tts.imageset/Contents.json b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/tts.imageset/Contents.json new file mode 100644 index 0000000..e1fcfbf --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/tts.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "tts_60_60.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "tts_120_120.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "tts_180_180.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/tts.imageset/tts_120_120.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/tts.imageset/tts_120_120.png new file mode 100644 index 0000000..0d421b2 Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/tts.imageset/tts_120_120.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/tts.imageset/tts_180_180.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/tts.imageset/tts_180_180.png new file mode 100644 index 0000000..399cf32 Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/tts.imageset/tts_180_180.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/tts.imageset/tts_60_60.png b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/tts.imageset/tts_60_60.png new file mode 100644 index 0000000..5df018f Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/tts.imageset/tts_60_60.png differ diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Base.lproj/LaunchScreen.storyboard b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..925db12 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Base.lproj/Main.storyboard b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Base.lproj/Main.storyboard new file mode 100644 index 0000000..de5d43f --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Base.lproj/Main.storyboard @@ -0,0 +1,3810 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Info.plist b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Info.plist new file mode 100644 index 0000000..3a74e09 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Info.plist @@ -0,0 +1,70 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 2 + LSRequiresIPhoneOS + + NSMicrophoneUsageDescription + Speech demo would like to access your recorder data. + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + SceneDelegate + UISceneStoryboardFile + Main + + + + + UIBackgroundModes + + audio + + UIFileSharingEnabled + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/SpeechSettingItem.xib b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/SpeechSettingItem.xib new file mode 100644 index 0000000..16cc9af --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/SpeechSettingItem.xib @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/ViewController.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/ViewController.h new file mode 100644 index 0000000..a7c7692 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/ViewController.h @@ -0,0 +1,23 @@ +// +// ViewController.h +// SpeechDemo +// +// Created by fangweiwei on 2019/12/6. +// Copyright © 2019 fangweiwei. All rights reserved. +// + +#import + +#import "AppDelegate.h" +#import "StreamRecorder.h" + +@interface ViewController : UIViewController + ++(void)setAppDelegate : (AppDelegate*)appDelegate; ++(AppDelegate*)getAppDelegate; + ++(StreamRecorder*)getStreamRecorder; ++(NSString*)extractBundleToFilePath:(NSString*)filename; + +@end + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/ViewController.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/ViewController.m new file mode 100644 index 0000000..f0c5df7 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/ViewController.m @@ -0,0 +1,73 @@ +// +// ViewController.m +// SpeechDemo +// +// Created by fangweiwei on 2019/12/6. +// Copyright © 2019 fangweiwei. All rights reserved. +// + +#import "ViewController.h" +#import + +static AppDelegate* APP_DELEGATE = nil; +static StreamRecorder *STREAM_RECORDER = nil; + +@implementation ViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + STREAM_RECORDER = [[StreamRecorder alloc] init]; +} + ++(void)setAppDelegate:(AppDelegate *)appDelegate { + APP_DELEGATE = appDelegate; +} + ++(AppDelegate*)getAppDelegate { + return APP_DELEGATE; +} + ++(StreamRecorder*)getStreamRecorder { + return STREAM_RECORDER; +} + ++(NSString*)extractBundleToFilePath:(NSString*)filename { + NSString *bundlePath = [[NSBundle mainBundle] pathForResource:filename ofType:nil]; + if (bundlePath == nil) { + return nil; + } + + NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + if (documentsPath == nil) { + return nil; + } + + NSString *targetPath = [documentsPath stringByAppendingPathComponent:filename]; + NSError *error = nil; + if ([[NSFileManager defaultManager] fileExistsAtPath:targetPath]) { + // Delete if file exists + [[NSFileManager defaultManager] removeItemAtPath:targetPath error:&error]; + if (error) { + NSLog(@"删除文件失败: %@", error.localizedDescription); + return nil; + } + } + [[NSFileManager defaultManager] copyItemAtPath:bundlePath toPath:targetPath error:&error]; + if (error != nil) { + return nil; + } + + return targetPath; +} + +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + [segue destinationViewController]; +} + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/main.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/main.m new file mode 100644 index 0000000..7941892 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/main.m @@ -0,0 +1,19 @@ +// +// main.m +// SpeechDemo +// +// Created by fangweiwei on 2019/12/6. +// Copyright © 2019 fangweiwei. All rights reserved. +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + NSString * appDelegateClassName; + @autoreleasepool { + // Setup code that might create autoreleased objects goes here. + appDelegateClassName = NSStringFromClass([AppDelegate class]); + } + return UIApplicationMain(argc, argv, nil, appDelegateClassName); +} diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/DialogMessage.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/DialogMessage.h new file mode 100644 index 0000000..559c6b5 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/DialogMessage.h @@ -0,0 +1,28 @@ +// +// DialogMessage.h +// SpeechDemo +// +// Created by bytedance on 2025/5/15. +// Copyright © 2025 bytedance. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef enum { + // 用户提问内容 + ROLE_USER, + // 助手机器人回复内容 + ROLE_ASSISTANT, + // 日志信息 + ROLE_LOG, +} Role; + +@interface DialogMessage : NSObject +@property (assign, nonatomic) Role role; +@property (strong, nonatomic) NSString* text; +@property (assign, nonatomic) bool confirmed; +@end + +NS_ASSUME_NONNULL_END diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/DialogMessage.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/DialogMessage.m new file mode 100644 index 0000000..fc55452 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/DialogMessage.m @@ -0,0 +1,12 @@ +// +// DialogMessage.h +// SpeechDemo +// +// Created by bytedance on 2025/5/15. +// Copyright © 2025 bytedance. All rights reserved. +// + +#import "DialogMessage.h" + +@implementation DialogMessage +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/FileUtils.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/FileUtils.h new file mode 100644 index 0000000..bfec270 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/FileUtils.h @@ -0,0 +1,24 @@ +// +// FileUtils.h +// SpeechDemo +// +// Created by fangweiwei on 2020/6/16. +// Copyright © 2020 fangweiwei. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FileUtils : NSObject + ++ (NSFileHandle *)openFileForReading:(NSString *)filename inPath:(NSString *)path; ++ (NSFileHandle *)openFileForWriting:(NSString *)filename inPath:(NSString *)path; ++ (BOOL)writeData:(NSData *)data toFileHandel:(NSFileHandle *)fileHandle; ++ (BOOL)writeString:(NSString *)data toFileHandel:(NSFileHandle *)fileHandle; ++ (BOOL)readData:(NSData *_Nullable*_Nullable)data length:(NSUInteger)length fromFileHandel:(NSFileHandle *)fileHandle; ++ (void)closeFile:(NSFileHandle *)filehandle; + +@end + +NS_ASSUME_NONNULL_END diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/FileUtils.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/FileUtils.m new file mode 100644 index 0000000..913fdb3 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/FileUtils.m @@ -0,0 +1,71 @@ +// +// FileUtils.m +// SpeechDemo +// +// Created by fangweiwei on 2020/6/16. +// Copyright © 2020 fangweiwei. All rights reserved. +// + +#import "FileUtils.h" + +@implementation FileUtils + ++ (NSFileHandle *)openFileForReading:(NSString *)filename inPath:(NSString *)path { + NSString *filePath = [path stringByAppendingPathComponent:filename]; + return [NSFileHandle fileHandleForReadingAtPath:filePath]; +} + ++ (NSFileHandle *)openFileForWriting:(NSString *)filename inPath:(NSString *)path { + NSString *filePath = [path stringByAppendingPathComponent:filename]; + [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]; + return [NSFileHandle fileHandleForUpdatingAtPath:filePath]; +} + ++ (BOOL)writeData:(NSData *)data toFileHandel:(NSFileHandle *)fileHandle { + if (fileHandle && data) { + @try { + [fileHandle writeData:data]; + } @catch (NSException *exception) { + NSLog(@"FileUtils, write data failed: %@", exception.description); + return FALSE; + } + return TRUE; + } + return FALSE; +} + ++ (BOOL)writeString:(NSString *)data toFileHandel:(NSFileHandle *)fileHandle { + if (fileHandle && data) { + @try { + [fileHandle writeData:[data dataUsingEncoding:NSUTF8StringEncoding]]; + } @catch (NSException *exception) { + NSLog(@"FileUtils, write data failed: %@", exception.description); + return FALSE; + } + return TRUE; + } + return FALSE; +} + ++ (BOOL)readData:(NSData **)data length:(NSUInteger)length fromFileHandel:(NSFileHandle *)fileHandle { + if (fileHandle && data) { + NSError *err; + if (@available(iOS 13.0, *)) { + *data = [fileHandle readDataUpToLength:length error:&err]; + if (err) { + NSLog(@"FileUtils read data failed: %@.", err); + return FALSE; + } + } + return TRUE; + } + return FALSE; +} + ++ (void)closeFile:(NSFileHandle *)filehandle { + if (filehandle) { + [filehandle closeFile]; + } +} + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/SensitiveDefines.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/SensitiveDefines.h new file mode 100644 index 0000000..eb2774b --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/SensitiveDefines.h @@ -0,0 +1,98 @@ +// +// SensitiveDefines.h +// SpeechDemo +// +// Created by bytedance on 2022/12/9. +// Copyright © 2022 tianlei.richard. All rights reserved. +// + +#define SensitiveDefines_h + +/** + * SensitiveDefines + * Defines in this class should be different for different business, + * please contact with @Bytedance AILab about what value should be set before use it. + */ + +// User Info +extern NSString* SDEF_UID; + +// Online & Resource Authentication +extern NSString* SDEF_APPID; +extern NSString* SDEF_APPKEY; +extern NSString* SDEF_TOKEN; +extern NSString* SDEF_APP_VERSION; + +// Offline Authentication +extern NSString* SDEF_AUTHENTICATE_ADDRESS; +extern NSString* SDEF_AUTHENTICATE_URI; +extern NSString* SDEF_SECRET; +extern NSString* SDEF_BUSINESS_KEY; +extern NSString* SDEF_LICENSE_NAME; +extern NSString* SDEF_LICENSE_BUSI_ID; + +// Address +extern NSString* SDEF_DEFAULT_ADDRESS; +extern NSString* SDEF_DEFAULT_HTTP_ADDRESS; + +// ASR +extern NSString* SDEF_ASR_DEFAULT_CLUSTER; +extern NSString* SDEF_ASR_DEFAULT_URI; +extern NSString* SDEF_ASR_DEFAULT_MODEL_NAME; + +// BigASR +extern NSString* SDEF_BIGASR_DEFAULT_APPID; +extern NSString* SDEF_BIGASR_DEFAULT_TOKEN; +extern NSString* SDEF_BIGASR_DEFAULT_RESOURCE_ID; +extern NSString* SDEF_BIGASR_DEFAULT_URI; + +// AU +extern const NSString* SDEF_AU_DEFAULT_APP_ID; +extern const NSString* SDEF_AU_DEFAULT_ADDRESS; +extern const NSString* SDEF_AU_DEFAULT_URI; +extern const NSString* SDEF_AU_DEFAULT_CLUSTER; + +// TTS +extern NSString* SDEF_TTS_DEFAULT_URI; +extern NSString* SDEF_TTS_DEFAULT_CLUSTER; +extern NSString* SDEF_TTS_DEFAULT_BACKEND_CLUSTER; +extern NSString* SDEF_TTS_DEFAULT_ONLINE_VOICE; +extern NSString* SDEF_TTS_DEFAULT_ONLINE_VOICE_TYPE; +extern NSString* SDEF_TTS_DEFAULT_OFFLINE_VOICE; +extern NSString* SDEF_TTS_DEFAULT_OFFLINE_VOICE_TYPE; +extern NSString* SDEF_TTS_DEFAULT_ONLINE_LANGUAGE; +extern NSString* SDEF_TTS_DEFAULT_OFFLINE_LANGUAGE; +const NSArray* SDEF_TTS_DEFAULT_DOWNLOAD_OFFLINE_VOICES(); + +// BITTS +extern NSString* SDEF_BITTS_DEFAULT_APPID; +extern NSString* SDEF_BITTS_DEFAULT_TOKEN; +extern NSString* SDEF_BITTS_DEFAULT_RESOURCE_ID; +extern NSString* SDEF_BITTS_DEFAULT_URI; + +// UNITTS +extern NSString* SDEF_UNITTS_DEFAULT_APPID; +extern NSString* SDEF_UNITTS_DEFAULT_TOKEN; +extern NSString* SDEF_UNITTS_DEFAULT_RESOURCE_ID; +extern NSString* SDEF_UNITTS_DEFAULT_URI; + +// VoiceClone +extern NSString* SDEF_VOICECLONE_DEFAULT_UIDS; +extern int SDEF_VOICECLONE_DEFAULT_TASK_ID; + +// VoiceConv +extern NSString* SDEF_VOICECONV_DEFAULT_URI; +extern NSString* SDEF_VOICECONV_DEFAULT_CLUSTER; +extern NSString* SDEF_VOICECONV_DEFAULT_VOICE; +extern NSString* SDEF_VOICECONV_DEFAULT_VOICE_TYPE; + +// Fulllink +extern NSString* SDEF_FULLLINK_DEFAULT_URI; + +// Dialog +extern NSString* SDEF_DIALOG_DEFAULT_URI; +extern NSString* SDEF_DIALOG_DEFAULT_RESOURCE_ID; + +// CAPT +extern NSString* SDEF_CAPT_DEFAULT_MDD_URI; +extern NSString* SDEF_CAPT_DEFAULT_CLUSTER; diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/SensitiveDefines.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/SensitiveDefines.m new file mode 100644 index 0000000..06b8a04 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/SensitiveDefines.m @@ -0,0 +1,95 @@ +// +// SensitiveDefines.m +// SpeechDemo +// +// Created by bytedance on 2022/12/9. +// Copyright © 2022 tianlei.richard. All rights reserved. +// + +#import + +#import "SensitiveDefines.h" + + +// User Info +const NSString* SDEF_UID = @"YOUR UID"; + +// Online & Resource Authentication +const NSString* SDEF_APPID = @"YOUR APPID"; +const NSString* SDEF_APPKEY = @"YOUR APPKEY"; +const NSString* SDEF_TOKEN = @"YOUR TOKEN"; +const NSString* SDEF_APP_VERSION = @"YOUR APP VERSION"; + +// Offline Authentication +const NSString* SDEF_AUTHENTICATE_ADDRESS = @"AUTHENTICAT ADDRESS"; +const NSString* SDEF_AUTHENTICATE_URI = @"AUTHENTICATE URI"; +const NSString* SDEF_SECRET = @"YOUR SECRET"; +const NSString* SDEF_BUSINESS_KEY = @"YOUR BUSINESS KEY"; +const NSString* SDEF_LICENSE_NAME = @"YOUR LICENSE NAME"; +const NSString* SDEF_LICENSE_BUSI_ID = @"YOUR LICENSE BUSI_ID"; + +// Address +const NSString* SDEF_DEFAULT_ADDRESS = @"wss://openspeech.bytedance.com"; +const NSString* SDEF_DEFAULT_HTTP_ADDRESS = @"https://openspeech.bytedance.com"; + +// ASR +const NSString* SDEF_ASR_DEFAULT_CLUSTER = @"YOUR ASR CLUSTER"; +const NSString* SDEF_ASR_DEFAULT_URI = @"/api/v2/asr"; +const NSString* SDEF_ASR_DEFAULT_MODEL_NAME = @"YOUR ASR MODEL NAME"; + +// BigASR +const NSString* SDEF_BIGASR_DEFAULT_APPID = @"YOUR APPID"; +const NSString* SDEF_BIGASR_DEFAULT_TOKEN = @"YOUR TOKEN"; +const NSString* SDEF_BIGASR_DEFAULT_RESOURCE_ID = @"YOUR RESOURCE ID"; +const NSString* SDEF_BIGASR_DEFAULT_URI = @"/api/v3/sauc/bigmodel"; + +// AU +const NSString* SDEF_AU_DEFAULT_APP_ID = @"YOUR APPID"; +const NSString* SDEF_AU_DEFAULT_ADDRESS = @"wss://openspeech.bytedance.com"; +const NSString* SDEF_AU_DEFAULT_URI = @"/api/v1/sauc"; +const NSString* SDEF_AU_DEFAULT_CLUSTER = @"YOUR AU CLUSTER"; + +// TTS +const NSString* SDEF_TTS_DEFAULT_URI = @"/api/v1/tts/ws_binary"; +const NSString* SDEF_TTS_DEFAULT_CLUSTER = @"YOUR TTS CLUSTER"; +const NSString* SDEF_TTS_DEFAULT_BACKEND_CLUSTER = @"YOUR TTS BACKEND CLUSTER"; +const NSString* SDEF_TTS_DEFAULT_ONLINE_VOICE = @"YOUR TTS ONLINE VOICE"; +const NSString* SDEF_TTS_DEFAULT_ONLINE_VOICE_TYPE = @"YOUR TTS ONLINE VOICE TYPE"; +const NSString* SDEF_TTS_DEFAULT_OFFLINE_VOICE = @"YOUR TTS OFFLINE VOICE"; +const NSString* SDEF_TTS_DEFAULT_OFFLINE_VOICE_TYPE = @"YOUR TTS OFFLINE VOICE TYPE"; +const NSString* SDEF_TTS_DEFAULT_ONLINE_LANGUAGE = @"YOUT TTS ONLINE LANGUAGE"; +const NSString* SDEF_TTS_DEFAULT_OFFLINE_LANGUAGE = @"YOUT TTS OFFLINE LANGUAGE"; +const NSArray* SDEF_TTS_DEFAULT_DOWNLOAD_OFFLINE_VOICES() { return @[]; } + +// BITTS +const NSString* SDEF_BITTS_DEFAULT_APPID=@"YOUR BITTS APPID"; +const NSString* SDEF_BITTS_DEFAULT_TOKEN = @"YOUR BITTS TOKEN"; +const NSString* SDEF_BITTS_DEFAULT_RESOURCE_ID = @"YOUR BITTS RESOURCE ID"; +const NSString* SDEF_BITTS_DEFAULT_URI = @"/api/v3/tts/bidirection"; + +// UNITTS +const NSString* SDEF_UNITTS_DEFAULT_APPID=@"YOUR UNITTS APPID"; +const NSString* SDEF_UNITTS_DEFAULT_TOKEN = @"YOUR UNITTS TOKEN"; +const NSString* SDEF_UNITTS_DEFAULT_RESOURCE_ID = @"YOUR UNITTS RESOURCE ID"; +const NSString* SDEF_UNITTS_DEFAULT_URI = @"/api/v3/tts/unidirectional/stream"; + +// VoiceClone +const NSString* SDEF_VOICECLONE_DEFAULT_UIDS = @"uid_1;uid_2"; +int SDEF_VOICECLONE_DEFAULT_TASK_ID = -1; + +// VoiceConv +const NSString* SDEF_VOICECONV_DEFAULT_URI = @"/api/v1/voice_conv/ws"; +const NSString* SDEF_VOICECONV_DEFAULT_CLUSTER = @"YOUR VOICECONV CLUSTER"; +const NSString* SDEF_VOICECONV_DEFAULT_VOICE = @"VOICECONV VOICE"; +const NSString* SDEF_VOICECONV_DEFAULT_VOICE_TYPE = @"VOICECONV VOICE TYPE"; + +// Fulllink +const NSString* SDEF_FULLLINK_DEFAULT_URI = @"FULLLINK URI"; + +// Dialog +const NSString* SDEF_DIALOG_DEFAULT_URI = @"/api/v3/realtime/dialogue"; +const NSString* SDEF_DIALOG_DEFAULT_RESOURCE_ID = @"DIALOG RESOURCE ID"; + +// CAPT +const NSString* SDEF_CAPT_DEFAULT_MDD_URI = @"CAPT MDD URI"; +const NSString* SDEF_CAPT_DEFAULT_CLUSTER = @"YOUR CAPT CLUSTER"; diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/SpeechMonitor.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/SpeechMonitor.h new file mode 100644 index 0000000..353a968 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/SpeechMonitor.h @@ -0,0 +1,23 @@ +// +// SpeechMonitor.h +// SpeechDemo +// +// Created by fangweiwei on 2023/3/31. +// Copyright © 2023 fangweiwei. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SpeechMonitor : NSObject + ++ (instancetype)shareInstance; + +- (void)UploadDelay:(long)delay withEventName:(NSString *)eventName withServiceName:(NSString *)serviceName; + +- (void)UploadMetric:(NSDictionary *)metric withServiceName:(NSString *)serviceName; + +@end + +NS_ASSUME_NONNULL_END diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/SpeechMonitor.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/SpeechMonitor.m new file mode 100644 index 0000000..6771bc9 --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/SpeechMonitor.m @@ -0,0 +1,33 @@ +// +// SpeechMonitor.m +// SpeechDemo +// +// Created by fangweiwei on 2023/3/31. +// Copyright © 2023 fangweiwei. All rights reserved. +// + +#import "SpeechMonitor.h" + +@implementation SpeechMonitor + ++ (instancetype)shareInstance { + static SpeechMonitor* sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] init]; + }); + return sharedInstance; +} + +- (void)UploadDelay:(long)delay withEventName:(NSString *)eventName withServiceName:(NSString *)serviceName { + NSLog(@"Upload delay: %ld.", delay); + NSMutableDictionary *metric = [[NSMutableDictionary alloc] init]; + [metric setValue:[NSNumber numberWithLong:delay] forKey:eventName]; + [self UploadMetric:metric withServiceName:serviceName]; +} + +- (void)UploadMetric:(NSDictionary *)metric withServiceName:(NSString *)serviceName { + NSLog(@"%@", metric); +} + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/StreamRecorder.h b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/StreamRecorder.h new file mode 100644 index 0000000..9b7255a --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/StreamRecorder.h @@ -0,0 +1,28 @@ +// +// StreamRecorder.h +// SpeechDemo +// +// Created by fangweiwei on 2020/9/16. +// Copyright © 2020 fangweiwei. All rights reserved. +// + +#define StreamRecorder_H + +#import +#import "AppDelegate.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface StreamRecorder : NSObject + +- (int)getSampleRate; +- (int)getChannel; +- (void)setSpeechEngine:(SpeechEngine *)engine; +- (void)setSpeechEngine:(NSString *) viewId engine:(SpeechEngine *)engine; +- (BOOL)start; +- (void)stop; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/StreamRecorder.m b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/StreamRecorder.m new file mode 100644 index 0000000..8c44b3a --- /dev/null +++ b/avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/utils/StreamRecorder.m @@ -0,0 +1,265 @@ +// +// StreamRecorder.m +// SpeechDemo +// +// Created by fangweiwei on 2020/9/16. +// Copyright © 2020 fangweiwei. All rights reserved. +// + +#import "StreamRecorder.h" +#import "SettingsHelper.h" +#import + +#define INPUT_BUS 1 +#define OUTPUT_BUS 0 +#define REC_SAMPLE_RATE 44100 +#define REC_CHANNEL 1 +#define DEFAULT_PACKAGE_DURATION 100 + +@interface StreamRecorder () + +@property (assign, nonatomic) BOOL initted; +@property (weak, nonatomic) SpeechEngine *curEngine; +@property (weak, nonatomic) NSString *curViewId; +@property (assign, atomic) int16_t* packageData; +@property (assign, nonatomic) int packageSize; // Size in int16_t +@property (assign, nonatomic) int packageTotalSize; // Size in int16_t +@property (assign, nonatomic) int packageDuration; // Unit: milliseconds +@property (strong, nonatomic) dispatch_semaphore_t waitAudioPermission; + +@end + +AudioUnit audioUnit; + +@implementation StreamRecorder + +- (instancetype)init +{ + self = [super init]; + if (self) { + _initted = FALSE; + _waitAudioPermission = dispatch_semaphore_create(0); + } + return self; +} + +- (int)getSampleRate { + return REC_SAMPLE_RATE; +} + +- (int)getChannel { + return REC_CHANNEL; +} + +- (void)setSpeechEngine:(SpeechEngine *)engine { + [self setSpeechEngine: @"" engine: engine]; +} + +- (void)setSpeechEngine:(NSString *) viewId engine:(SpeechEngine *)engine { + self.curViewId = viewId; + self.curEngine = engine; +} + +- (BOOL)start { + if (![self initRemoteIO]) { + return FALSE; + } + @synchronized (self) { + Settings *settings = [[SettingsHelper shareInstance]getSettings:self.curViewId]; + self.packageDuration = [settings getInt:SETTING_STREAM_PACKAGE_DURATION def:DEFAULT_PACKAGE_DURATION]; + self.packageTotalSize = (int)((long) REC_SAMPLE_RATE * REC_CHANNEL * 16 / 8 * self.packageDuration / 1000 / sizeof(int16_t)); + self.packageData = (int16_t*) malloc(self.packageTotalSize * sizeof(int16_t)); + self.packageSize = 0; + } + AudioOutputUnitStart(audioUnit); + return TRUE; +} + +- (void)stop { + AudioOutputUnitStop(audioUnit); + + @synchronized (self) { + if (self.packageData != NULL) { + free(self.packageData); + self.packageData = NULL; + } + self.packageSize = 0; + } +} + +- (BOOL)initRemoteIO { + if (self.initted) { + return TRUE; + } + + if (!([self checkRecEnvironment] && + [self initAudioComponent] && + [self initFormat] && + [self initAudioProperty] && + [self initRecordCallback])) { + NSLog(@"Fail to initialize remoteIO!"); + AudioComponentInstanceDispose(audioUnit); + return FALSE; + } + AudioUnitInitialize(audioUnit); + self.initted = TRUE; + return TRUE; +} + +#pragma mark - callback function + +static OSStatus RecordCallback(void *inRefCon, + AudioUnitRenderActionFlags *ioActionFlags, + const AudioTimeStamp *inTimeStamp, + UInt32 inBusNumber, + UInt32 inNumberFrames, + AudioBufferList *ioData) { + AudioBufferList bufferList; + bufferList.mNumberBuffers = 1; + bufferList.mBuffers[0].mData = NULL; + bufferList.mBuffers[0].mDataByteSize = 0; + OSStatus status = AudioUnitRender(audioUnit, ioActionFlags, inTimeStamp, 1, inNumberFrames, &bufferList); + if (status != noErr) { + NSLog(@"RecordCallback AudioUnitRender error: %d", (int)status); + } else { + StreamRecorder *recorder = (__bridge StreamRecorder *)(inRefCon); + @synchronized (recorder) { + if (recorder.packageData == NULL) { + return noErr; + } + + int16_t *data = (int16_t *)bufferList.mBuffers[0].mData; + int32_t data_size = bufferList.mBuffers[0].mDataByteSize / 2; + while (data_size + recorder.packageSize >= recorder.packageTotalSize) { + // Package buffer is full, feed audio. + int cut_size = recorder.packageTotalSize - recorder.packageSize; + memcpy(recorder.packageData + recorder.packageSize, data, cut_size * sizeof(int16_t)); + + NSLog(@"Current package size: %d, total package size: %d", recorder.packageSize, recorder.packageTotalSize); + + if ([recorder.curEngine feedAudio:recorder.packageData length:recorder.packageTotalSize]) { + NSLog(@"Fail to feed data to engine!"); + return -1; + } + recorder.packageSize = 0; + + + data = data + cut_size; + data_size = data_size - cut_size; + } + memcpy(recorder.packageData + recorder.packageSize, data, data_size * sizeof(int16_t)); + recorder.packageSize += data_size; + NSLog(@"Current package size: %d, total package size: %d", recorder.packageSize, recorder.packageTotalSize); + } + } + + return noErr; +} + +#pragma mark - helper + +- (BOOL)checkRecEnvironment { + NSLog(@"check rec environment"); + BOOL ret = NO; + __block BOOL hasPermission = YES; + AVAudioSession *session = [AVAudioSession sharedInstance]; + if ([session respondsToSelector:@selector(requestRecordPermission:)]) { + [session requestRecordPermission:^(BOOL granted) { + hasPermission = granted; + dispatch_semaphore_signal(self.waitAudioPermission); + }]; + dispatch_semaphore_wait(self.waitAudioPermission, DISPATCH_TIME_FOREVER); + } + if (!hasPermission) { + NSLog(@"No permission for recorder."); + return ret; + } + + if (!session.isInputAvailable) { + NSLog(@"Input device is not available"); + return ret; + } + + [session setCategory:AVAudioSessionCategoryPlayAndRecord + withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionAllowBluetooth + error:nil]; + + ret = YES; + NSLog(@"rec environment is OK"); + + return ret; +} + +- (BOOL)initAudioComponent { + AudioComponentDescription audioDesc; + audioDesc.componentType = kAudioUnitType_Output; + audioDesc.componentSubType = kAudioUnitSubType_RemoteIO; + audioDesc.componentManufacturer = kAudioUnitManufacturer_Apple; + audioDesc.componentFlags = 0; + audioDesc.componentFlagsMask = 0; + + AudioComponent inputComponent = AudioComponentFindNext(NULL, &audioDesc); + AudioComponentInstanceNew(inputComponent, &audioUnit); + return TRUE; +} + +- (BOOL)initFormat { + AudioStreamBasicDescription audioFormat; + audioFormat.mSampleRate = REC_SAMPLE_RATE; + audioFormat.mFormatID = kAudioFormatLinearPCM; + audioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; + audioFormat.mFramesPerPacket = 1; + audioFormat.mChannelsPerFrame = 1; + audioFormat.mBitsPerChannel = 16; + audioFormat.mBytesPerPacket = 2; + audioFormat.mBytesPerFrame = 2; + + OSStatus status = noErr; + status = AudioUnitSetProperty(audioUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Output, + INPUT_BUS, + &audioFormat, + sizeof(audioFormat)); + if (status != noErr) { + NSLog(@"initFormat SetOutput Error with status: %d", (int)status); + return FALSE; + } + return TRUE; +} + +- (BOOL)initRecordCallback { + AURenderCallbackStruct recordCallback; + recordCallback.inputProc = RecordCallback; + recordCallback.inputProcRefCon = (__bridge void *)self; + OSStatus status = noErr; + status = AudioUnitSetProperty(audioUnit, + kAudioOutputUnitProperty_SetInputCallback, + kAudioUnitScope_Global, + INPUT_BUS, + &recordCallback, + sizeof(recordCallback)); + if (status != noErr) { + NSLog(@"initRecordCallback SetRenderCallBack error with status: %d", (int)status); + return FALSE; + } + return TRUE; +} + +- (BOOL)initAudioProperty { + UInt32 flag = 1; + OSStatus status = noErr; + status = AudioUnitSetProperty(audioUnit, + kAudioOutputUnitProperty_EnableIO, + kAudioUnitScope_Input, + INPUT_BUS, + &flag, + sizeof(flag)); + if (status != noErr) { + NSLog(@"initAudioProperty SetInputIO error with status: %d", (int)status); + return FALSE; + } + return TRUE; +} + +@end diff --git a/avatar_flutter_app/demo/SpeechDemoIOS/aec.model b/avatar_flutter_app/demo/SpeechDemoIOS/aec.model new file mode 100644 index 0000000..1c045ce Binary files /dev/null and b/avatar_flutter_app/demo/SpeechDemoIOS/aec.model differ diff --git a/avatar_flutter_app/ios/Podfile b/avatar_flutter_app/ios/Podfile index 620e46e..352b952 100644 --- a/avatar_flutter_app/ios/Podfile +++ b/avatar_flutter_app/ios/Podfile @@ -1,5 +1,7 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '13.0' +platform :ios, '13.0' + +source 'https://github.com/CocoaPods/Specs.git' +source 'https://github.com/volcengine/volcengine-specs.git' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -28,16 +30,42 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe flutter_ios_podfile_setup target 'Runner' do - use_frameworks! + use_frameworks! :linkage => :static flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + + # 火山实时语音 SDK 仅提供 iphoneos arm64 / x86_64-simulator 切片 + # Apple Silicon Mac 的 arm64 模拟器不被支持。 + # 模拟器构建前请执行: SPEECH_SDK_ENABLED=0 pod install + # 真机构建前执行: pod install (默认引入) + if ENV['SPEECH_SDK_ENABLED'] != '0' + # 与火山 SpeechDemoIOS demo 保持一致,用全量包 SpeechEngineToB(含 APP_KEY/RESOURCE_ID 等字段)。 + # SpeechEngineDialogToB 0.0.1-rc.0 是更新的 RTC 重构版,参数体系不同且服务端尚未完全支持。 + pod 'SpeechEngineToB', '0.0.9' + end + target 'RunnerTests' do inherit! :search_paths end end post_install do |installer| + speech_sdk_enabled = ENV['SPEECH_SDK_ENABLED'] != '0' + installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' + + if speech_sdk_enabled + # 火山 SDK 没有 arm64-simulator slice,需要全局排除以让真机/Intel 模拟器构建通过 + config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64' + else + # 模拟器开发模式:强制取消 arm64 排除(覆盖 flutter_inappwebview 自带的排除), + # 让 Apple Silicon Mac 的 arm64 模拟器能跑 + config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = '' + config.build_settings.delete('EXCLUDED_ARCHS') + end + end end end diff --git a/avatar_flutter_app/ios/Podfile.lock b/avatar_flutter_app/ios/Podfile.lock index 3def93f..3652bbe 100644 --- a/avatar_flutter_app/ios/Podfile.lock +++ b/avatar_flutter_app/ios/Podfile.lock @@ -8,12 +8,19 @@ PODS: - Flutter - OrderedSet (~> 6.0.3) - OrderedSet (6.0.3) + - SpeechEngineToB (0.0.9): + - TTNetworkManager + - TTNetworkManager (5.2.210.21) DEPENDENCIES: - Flutter (from `Flutter`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) + - SpeechEngineToB (= 0.0.9) SPEC REPOS: + https://github.com/volcengine/volcengine-specs.git: + - SpeechEngineToB + - TTNetworkManager trunk: - OrderedSet @@ -27,7 +34,9 @@ SPEC CHECKSUMS: Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 + SpeechEngineToB: 3f57352e03d7a125b9f088417698969c73a8bbf9 + TTNetworkManager: 307666aa6eb8d041c0e962518338c53e379b9088 -PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e +PODFILE CHECKSUM: bfb0d2f32e3cf865b593124b6d8dcda1da30379f COCOAPODS: 1.16.2 diff --git a/avatar_flutter_app/ios/Runner.xcodeproj/project.pbxproj b/avatar_flutter_app/ios/Runner.xcodeproj/project.pbxproj index 37bb82f..7df0c7b 100644 --- a/avatar_flutter_app/ios/Runner.xcodeproj/project.pbxproj +++ b/avatar_flutter_app/ios/Runner.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; E45F56E060F8C7FA3AB1ADCD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4EE7F6FA02E117D478D3D4C2 /* Pods_RunnerTests.framework */; }; + F7797DDE6A9111B0C538469A /* SpeechEnginePlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = 84185ACD457EBBFB04E672BB /* SpeechEnginePlugin.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -45,6 +46,7 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 31DB49ECEDD6EB52C2A6EBD6 /* SpeechEnginePlugin.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = SpeechEnginePlugin.h; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 381D1AD3E23B7C85CF338BD7 /* 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 = ""; }; @@ -55,6 +57,7 @@ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 84185ACD457EBBFB04E672BB /* SpeechEnginePlugin.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = SpeechEnginePlugin.m; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -164,6 +167,8 @@ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + 31DB49ECEDD6EB52C2A6EBD6 /* SpeechEnginePlugin.h */, + 84185ACD457EBBFB04E672BB /* SpeechEnginePlugin.m */, ); path = Runner; sourceTree = ""; @@ -201,7 +206,8 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - B95611DA8805AF4DAD4B8949 /* [CP] Embed Pods Frameworks */, + 8B89901EBF968580C6ED9021 /* [CP] Copy Pods Resources */, + C6A0283A36074C8F3564FD97 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -311,6 +317,23 @@ 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; }; + 8B89901EBF968580C6ED9021 /* [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; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -348,7 +371,7 @@ 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; }; - B95611DA8805AF4DAD4B8949 /* [CP] Embed Pods Frameworks */ = { + C6A0283A36074C8F3564FD97 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -383,6 +406,7 @@ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, + F7797DDE6A9111B0C538469A /* SpeechEnginePlugin.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/avatar_flutter_app/ios/Runner/AppDelegate.swift b/avatar_flutter_app/ios/Runner/AppDelegate.swift index c30b367..85d41b5 100644 --- a/avatar_flutter_app/ios/Runner/AppDelegate.swift +++ b/avatar_flutter_app/ios/Runner/AppDelegate.swift @@ -7,10 +7,17 @@ import UIKit _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + NSLog("[AppDelegate] didFinishLaunchingWithOptions") + SpeechEnginePlugin.prepareEnvironment() return super.application(application, didFinishLaunchingWithOptions: launchOptions) } func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + NSLog("[AppDelegate] registering plugins...") GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + if let registrar = engineBridge.pluginRegistry.registrar(forPlugin: "SpeechEnginePlugin") { + SpeechEnginePlugin.register(with: registrar) + } + NSLog("[AppDelegate] plugins registered") } } diff --git a/avatar_flutter_app/ios/Runner/Info.plist b/avatar_flutter_app/ios/Runner/Info.plist index b319cb1..ed67184 100644 --- a/avatar_flutter_app/ios/Runner/Info.plist +++ b/avatar_flutter_app/ios/Runner/Info.plist @@ -26,6 +26,8 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + NSMicrophoneUsageDescription + 需要麦克风以便与虚拟形象进行实时语音对话 UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/avatar_flutter_app/ios/Runner/Runner-Bridging-Header.h b/avatar_flutter_app/ios/Runner/Runner-Bridging-Header.h index 308a2a5..5dcfdb1 100644 --- a/avatar_flutter_app/ios/Runner/Runner-Bridging-Header.h +++ b/avatar_flutter_app/ios/Runner/Runner-Bridging-Header.h @@ -1 +1,2 @@ #import "GeneratedPluginRegistrant.h" +#import "SpeechEnginePlugin.h" diff --git a/avatar_flutter_app/ios/Runner/SpeechEnginePlugin.h b/avatar_flutter_app/ios/Runner/SpeechEnginePlugin.h new file mode 100644 index 0000000..d0b6b51 --- /dev/null +++ b/avatar_flutter_app/ios/Runner/SpeechEnginePlugin.h @@ -0,0 +1,27 @@ +#import + +/// 桥接 SpeechEngineDialogToB(火山端到端实时语音对话 SDK)到 Flutter。 +/// +/// MethodChannel: avatar_flutter_app/speech_engine +/// - init(params) 初始化引擎与配置参数 +/// - start() 按住说话:SyncStop + StartEngine +/// - finishTalking() 抬起按钮:通知 SDK 用户讲完 +/// - cancelCurrentDialog() 打断 AI 当前回复 +/// - sayHello(content) 让 AI 主动说一句开场白 +/// - stop() 关闭会话 +/// - destroy() 销毁引擎释放资源 +/// +/// EventChannel: avatar_flutter_app/speech_events +/// 推送统一字典 {"type": "...", ...payload}: +/// - engine_start / engine_stop / engine_error +/// - asr_partial / asr_end +/// - chat_partial / chat_end +/// - mouth_value (0.0-1.0,30Hz) +/// - raw (未识别的 SEMessageType,调试用) +@interface SpeechEnginePlugin : NSObject + +/// AppDelegate 早期调用,必须在 didFinishLaunchingWithOptions 中调一次。 +/// 模拟器构建(SDK 未链接)时退化为无操作。 ++ (void)prepareEnvironment; + +@end diff --git a/avatar_flutter_app/ios/Runner/SpeechEnginePlugin.m b/avatar_flutter_app/ios/Runner/SpeechEnginePlugin.m new file mode 100644 index 0000000..e361137 --- /dev/null +++ b/avatar_flutter_app/ios/Runner/SpeechEnginePlugin.m @@ -0,0 +1,535 @@ +#import "SpeechEnginePlugin.h" + +#import +#import + +// 仅当 SpeechEngineDialogToB pod 真实被链接时启用全部 SDK 调用。 +// 跑模拟器/桌面端时执行 SPEECH_SDK_ENABLED=0 pod install 跳过 SDK, +// Plugin 退化为 stub(所有方法返回 SIMULATOR_NOT_SUPPORTED)。 +#if __has_include() +#define SPEECH_SDK_AVAILABLE 1 +#import +#elif __has_include() +#define SPEECH_SDK_AVAILABLE 1 +#import +#else +#define SPEECH_SDK_AVAILABLE 0 +#endif + +static NSString *const kMethodChannel = @"avatar_flutter_app/speech_engine"; +static NSString *const kEventChannel = @"avatar_flutter_app/speech_events"; + +@interface SpeechEnginePlugin () +@property (nonatomic, strong) FlutterMethodChannel *methodChannel; +@property (nonatomic, strong) FlutterEventChannel *eventChannel; +@property (nonatomic, copy) FlutterEventSink eventSink; + +#if SPEECH_SDK_AVAILABLE +@property (nonatomic, strong) SpeechEngine *speechEngine; +@property (nonatomic, assign) BOOL engineStarted; +@property (nonatomic, copy) NSString *botName; +@property (nonatomic, copy) NSString *sdkLogPath; +@property (nonatomic, assign) unsigned long long lastLogOffset; +@property (nonatomic, strong) dispatch_source_t logPoller; +@property (nonatomic, assign) NSTimeInterval lastMouthEmitTs; +@property (nonatomic, assign) float smoothedMouth; +#endif +@end + +@implementation SpeechEnginePlugin + +#pragma mark - FlutterPlugin + ++ (void)prepareEnvironment { +#if SPEECH_SDK_AVAILABLE + NSLog(@"[SpeechPlugin] prepareEnvironment"); + [SpeechEngine prepareEnvironment]; +#else + NSLog(@"[SpeechPlugin] prepareEnvironment skipped (SDK not linked, simulator build)"); +#endif +} + ++ (void)registerWithRegistrar:(NSObject *)registrar { + SpeechEnginePlugin *instance = [[SpeechEnginePlugin alloc] init]; + + instance.methodChannel = [FlutterMethodChannel + methodChannelWithName:kMethodChannel + binaryMessenger:[registrar messenger]]; + [registrar addMethodCallDelegate:instance channel:instance.methodChannel]; + + instance.eventChannel = [FlutterEventChannel + eventChannelWithName:kEventChannel + binaryMessenger:[registrar messenger]]; + [instance.eventChannel setStreamHandler:instance]; +} + +#pragma mark - FlutterStreamHandler + +- (FlutterError *)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)events { + self.eventSink = events; + return nil; +} + +- (FlutterError *)onCancelWithArguments:(id)arguments { + self.eventSink = nil; + return nil; +} + +- (void)sendEvent:(NSDictionary *)payload { + if (!self.eventSink) return; + if ([NSThread isMainThread]) { + self.eventSink(payload); + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.eventSink) self.eventSink(payload); + }); + } +} + +#pragma mark - MethodCall + +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { +#if !SPEECH_SDK_AVAILABLE + result([FlutterError errorWithCode:@"SIMULATOR_NOT_SUPPORTED" + message:@"火山实时语音 SDK 在 Apple Silicon 模拟器上不可用。请用真机测试,或执行 'pod install' 启用 SDK 后再构建。" + details:nil]); + return; +#else + NSString *m = call.method; + + if ([m isEqualToString:@"init"]) { + [self initEngine:call.arguments result:result]; + } else if ([m isEqualToString:@"start"]) { + [self startEngine:result]; + } else if ([m isEqualToString:@"startTalking"]) { + [self sendDirective:SEDirectiveStartTalking data:nil result:result]; + } else if ([m isEqualToString:@"finishTalking"]) { + [self sendDirective:SEDirectiveFinishTalking data:nil result:result]; + } else if ([m isEqualToString:@"pauseTalking"]) { + [self sendDirective:SEDirectivePauseTalking data:nil result:result]; + } else if ([m isEqualToString:@"resumeTalking"]) { + [self sendDirective:SEDirectiveResumeTalking data:nil result:result]; + } else if ([m isEqualToString:@"cancelCurrentDialog"]) { + [self sendDirective:SEDirectiveCancelCurrentDialog data:nil result:result]; + } else if ([m isEqualToString:@"sayHello"]) { + // demo 用 SEDirectiveEventSayHello + NSString *content = call.arguments[@"content"] ?: @""; + NSString *escaped = [content stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]; + NSString *json = [NSString stringWithFormat:@"{\"content\":\"%@\"}", escaped]; + [self sendDirective:SEDirectiveEventSayHello data:json result:result]; + } else if ([m isEqualToString:@"stop"]) { + [self sendDirective:SEDirectiveStopEngine data:nil result:result]; + } else if ([m isEqualToString:@"destroy"]) { + [self destroyEngine:result]; + } else { + result(FlutterMethodNotImplemented); + } +#endif +} + +#if SPEECH_SDK_AVAILABLE + +#pragma mark - Engine lifecycle + +/// TTS 音频帧(int16 PCM)→ RMS → 平滑 → 30Hz 推送给 Dart 驱动 setMouthOpen +- (void)processTtsAudio:(NSData *)data { + if (data.length < 2) return; + const int16_t *samples = data.bytes; + NSUInteger count = data.length / sizeof(int16_t); + if (count == 0) return; + + double sumSq = 0.0; + for (NSUInteger i = 0; i < count; i++) { + double s = samples[i] / 32768.0; + sumSq += s * s; + } + double rms = sqrt(sumSq / (double)count); + + // 非线性映射 + 增益(让嘴型动作更明显) + double mouth = pow(MIN(1.0, rms * 4.0), 0.6); + if (mouth < 0.05) mouth = 0; // noise gate + + // 一阶低通平滑(attack 快、release 慢避免抖动) + float prev = self.smoothedMouth; + float target = (float)mouth; + float alpha = (target > prev) ? 0.5f : 0.2f; + float smoothed = prev + (target - prev) * alpha; + self.smoothedMouth = smoothed; + + // 限频 30Hz + NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; + if (now - self.lastMouthEmitTs < 0.033) return; + self.lastMouthEmitTs = now; + + [self sendEvent:@{@"type": @"mouth", @"value": @(smoothed)}]; +} + +- (void)pluginLog:(NSString *)msg { + // 仅 NSLog 自用,不再推到 UI(避免噪音) + NSLog(@"[SpeechPlugin] %@", msg); +} + +- (void)initEngine:(NSDictionary *)args result:(FlutterResult)result { + [self pluginLog:@"initEngine() called"]; + if (self.speechEngine) { + [self pluginLog:@"already inited, skip"]; + result(@{@"ok": @YES, @"alreadyInited": @YES}); + return; + } + + // 注意:不要在这里手动设置 AVAudioSession。 + // VolcEngineRTC 内部会管理 audio session(category/mode/route/interruption), + // 手动设置 AVAudioSessionModeVoiceChat 会与 RTC 冲突, + // 导致 SDK 误判 "app into background" 并触发 errcode=5000。 + + [self pluginLog:@"step1 alloc SpeechEngine"]; + self.speechEngine = [[SpeechEngine alloc] init]; + + [self pluginLog:@"step2 createEngineWithDelegate"]; + if (![self.speechEngine createEngineWithDelegate:self]) { + [self pluginLog:@"step2 FAILED: createEngineWithDelegate returned NO"]; + self.speechEngine = nil; + result([FlutterError errorWithCode:@"CREATE_FAILED" + message:@"createEngineWithDelegate returned NO" + details:nil]); + return; + } + [self pluginLog:[NSString stringWithFormat:@"step3 SDK version=%@", [self.speechEngine getVersion]]]; + + [self pluginLog:@"step4 setStringParam ENGINE_NAME/LOG_LEVEL/DEBUG_PATH"]; + [self.speechEngine setStringParam:SE_DIALOG_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING]; + [self.speechEngine setStringParam:SE_LOG_LEVEL_WARN forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING]; + NSString *docs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject ?: @""; + [self.speechEngine setStringParam:docs forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING]; + // 仅保存 docs 路径,真正的日志文件名 SDK 启动时才生成(含时间戳) + self.sdkLogPath = docs; + self.lastLogOffset = 0; + [self pluginLog:[NSString stringWithFormat:@"docs=%@ (sdk 日志文件名运行时探测)", docs]]; + + NSString *appId = args[@"appId"] ?: @""; + NSString *appKey = args[@"appKey"] ?: @""; + NSString *token = args[@"token"] ?: @""; + NSString *resourceId = args[@"dialogId"] ?: @""; + self.botName = args[@"botName"] ?: @"豆包"; + [self pluginLog:[NSString stringWithFormat:@"step5 auth: appId=%@ appKeyLen=%lu tokenLen=%lu", + appId, (unsigned long)appKey.length, (unsigned long)token.length]]; + [self pluginLog:[NSString stringWithFormat:@"step6 dialog: resourceId=%@", resourceId]]; + + [self.speechEngine setStringParam:appId forKey:SE_PARAMS_KEY_APP_ID_STRING]; + [self.speechEngine setStringParam:appKey forKey:SE_PARAMS_KEY_APP_KEY_STRING]; + [self.speechEngine setStringParam:token forKey:SE_PARAMS_KEY_APP_TOKEN_STRING]; + [self.speechEngine setStringParam:resourceId forKey:SE_PARAMS_KEY_RESOURCE_ID_STRING]; + [self.speechEngine setStringParam:(args[@"uid"] ?: @"avatar_flutter_app_user") forKey:SE_PARAMS_KEY_UID_STRING]; + + [self.speechEngine setStringParam:(args[@"address"] ?: @"wss://openspeech.bytedance.com") + forKey:SE_PARAMS_KEY_DIALOG_ADDRESS_STRING]; + [self.speechEngine setStringParam:(args[@"uri"] ?: @"/api/v3/realtime/dialogue") + forKey:SE_PARAMS_KEY_DIALOG_URI_STRING]; + + [self.speechEngine setStringParam:SE_RECORDER_TYPE_RECORDER forKey:SE_PARAMS_KEY_RECORDER_TYPE_STRING]; + // AEC:和 demo 一致默认开启;AEC 模型从 Flutter assets/voice/aec.model 自动定位 + [self.speechEngine setBoolParam:YES forKey:SE_PARAMS_KEY_ENABLE_AEC_BOOL]; + NSString *aecPath = args[@"aecModelPath"] ?: @""; + if (aecPath.length == 0) { + // Flutter assets 在 .app/Frameworks/App.framework/flutter_assets/ 下 + NSString *bundle = [[NSBundle mainBundle] pathForResource:@"App" ofType:@"framework"]; + if (bundle.length > 0) { + NSString *candidate = [bundle stringByAppendingPathComponent:@"flutter_assets/assets/voice/aec.model"]; + if ([[NSFileManager defaultManager] fileExistsAtPath:candidate]) { + aecPath = candidate; + } + } + if (aecPath.length == 0) { + // 回退:递归找 + NSString *root = [[NSBundle mainBundle] bundlePath]; + NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath:root]; + for (NSString *p in enumerator) { + if ([p hasSuffix:@"aec.model"]) { + aecPath = [root stringByAppendingPathComponent:p]; + break; + } + } + } + } + [self pluginLog:[NSString stringWithFormat:@"AEC model path=%@", aecPath]]; + if (aecPath.length > 0) { + [self.speechEngine setStringParam:aecPath forKey:SE_PARAMS_KEY_AEC_MODEL_PATH_STRING]; + } + // 启用播放器 + 启用播放器音频回调(用于嘴型驱动) + [self.speechEngine setBoolParam:YES forKey:SE_PARAMS_KEY_DIALOG_ENABLE_PLAYER_BOOL]; + [self.speechEngine setBoolParam:YES forKey:SE_PARAMS_KEY_DIALOG_ENABLE_PLAYER_AUDIO_CALLBACK_BOOL]; + + [self pluginLog:@"step7 calling initEngine ..."]; + SEEngineErrorCode ret = [self.speechEngine initEngine]; + [self pluginLog:[NSString stringWithFormat:@"step8 initEngine ret=%d (0=NoError)", (int)ret]]; + if (ret != SENoError) { + result([FlutterError errorWithCode:@"INIT_FAILED" + message:[NSString stringWithFormat:@"initEngine returned %d", (int)ret] + details:nil]); + return; + } + result(@{@"ok": @YES, @"version": [self.speechEngine getVersion] ?: @""}); +} + +- (void)startEngine:(FlutterResult)result { + if (!self.speechEngine) { + [self pluginLog:@"startEngine FAILED: not inited"]; + result([FlutterError errorWithCode:@"NOT_INITED" message:@"call init() first" details:nil]); + return; + } + [self pluginLog:@"step9 SyncStopEngine (清理上次会话)"]; + [self.speechEngine sendDirective:SEDirectiveSyncStopEngine]; + + // demo: StartEngine 必须带 data={"dialog":{"bot_name":"豆包"}} + NSString *botName = self.botName.length > 0 ? self.botName : @"豆包"; + NSString *startJson = [NSString stringWithFormat:@"{\"dialog\":{\"bot_name\":\"%@\"}}", botName]; + [self pluginLog:[NSString stringWithFormat:@"step10 sending StartEngine data=%@", startJson]]; + SEEngineErrorCode ret = [self.speechEngine sendDirective:SEDirectiveStartEngine data:startJson]; + [self pluginLog:[NSString stringWithFormat:@"step11 StartEngine ret=%d", (int)ret]]; + if (ret == SERecCheckEnvironmentFailed) { + [self pluginLog:@"NO MICROPHONE PERMISSION!"]; + result([FlutterError errorWithCode:@"NO_PERMISSION" message:@"microphone not granted" details:nil]); + } else if (ret != SENoError) { + result([FlutterError errorWithCode:@"START_FAILED" + message:[NSString stringWithFormat:@"%d", (int)ret] details:nil]); + } else { + [self pluginLog:@"StartEngine OK"]; + // SDK 日志轮询已停用(避免日志爆刷)。调试时取消注释打开: + // [self startSdkLogPoller]; + result(@{@"ok": @YES}); + } +} + +- (void)startSdkLogPoller { + if (self.logPoller) return; + if (self.sdkLogPath.length == 0) return; + dispatch_queue_t q = dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0); + dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q); + dispatch_source_set_timer(timer, + dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC), + NSEC_PER_SEC, // 每秒读一次 + 250 * NSEC_PER_MSEC); + __weak typeof(self) weakSelf = self; + dispatch_source_set_event_handler(timer, ^{ + [weakSelf drainSdkLog]; + }); + dispatch_resume(timer); + self.logPoller = timer; + [self pluginLog:@"sdk log poller started"]; +} + +- (NSString *)resolveLatestSdkLog { + NSString *docs = self.sdkLogPath; // 此字段实际保存 docs 路径 + if (docs.length == 0) return nil; + NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:docs error:nil]; + NSString *latest = nil; + for (NSString *name in files) { + if (![name hasPrefix:@"speech_sdk_"]) continue; + if (![name hasSuffix:@".log"]) continue; + if (!latest || [name compare:latest] == NSOrderedDescending) { + latest = name; + } + } + return latest ? [docs stringByAppendingPathComponent:latest] : nil; +} + +- (void)drainSdkLog { + NSString *file = [self resolveLatestSdkLog]; + if (!file) { + static BOOL diagnosed = NO; + if (!diagnosed) { + diagnosed = YES; + NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.sdkLogPath error:nil]; + [self sendEvent:@{ + @"type": @"sdk_log_line", + @"line": [NSString stringWithFormat:@"docs 目录内容:%@", + [files componentsJoinedByString:@", "]] + }]; + } + return; + } + NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:file error:nil]; + if (!attrs) return; + unsigned long long size = [attrs fileSize]; + if (size <= self.lastLogOffset) return; + + NSFileHandle *fh = [NSFileHandle fileHandleForReadingAtPath:file]; + if (!fh) return; + @try { + [fh seekToFileOffset:self.lastLogOffset]; + NSData *data = [fh readDataToEndOfFile]; + self.lastLogOffset = size; + NSString *chunk = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] ?: @""; + // 只把 WARN/ERROR/FAIL 行推到 UI,info/debug 留在文件,避免刷屏 + NSArray *lines = [chunk componentsSeparatedByString:@"\n"]; + NSInteger emitted = 0; + for (NSString *raw in lines) { + NSString *line = [raw stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + if (line.length == 0) continue; + BOOL important = + [line rangeOfString:@"WARN" options:NSCaseInsensitiveSearch].location != NSNotFound + || [line rangeOfString:@"ERROR" options:NSCaseInsensitiveSearch].location != NSNotFound + || [line rangeOfString:@"FAIL" options:NSCaseInsensitiveSearch].location != NSNotFound + || [line rangeOfString:@"err_code" options:NSCaseInsensitiveSearch].location != NSNotFound + || [line rangeOfString:@"err_msg" options:NSCaseInsensitiveSearch].location != NSNotFound; + if (!important) continue; + [self sendEvent:@{@"type": @"sdk_log_line", @"line": line}]; + if (++emitted >= 15) break; + } + } @finally { + [fh closeFile]; + } +} + +- (void)stopSdkLogPoller { + if (self.logPoller) { + dispatch_source_cancel(self.logPoller); + self.logPoller = nil; + } +} + +- (void)sendDirective:(SEDirective)directive data:(NSString *)data result:(FlutterResult)result { + if (!self.speechEngine) { + result([FlutterError errorWithCode:@"NOT_INITED" message:@"call init() first" details:nil]); + return; + } + SEEngineErrorCode ret = data + ? [self.speechEngine sendDirective:directive data:data] + : [self.speechEngine sendDirective:directive]; + if (ret != SENoError) { + result([FlutterError errorWithCode:@"DIRECTIVE_FAILED" + message:[NSString stringWithFormat:@"directive=%d ret=%d", (int)directive, (int)ret] + details:nil]); + } else { + result(@{@"ok": @YES}); + } +} + +- (void)destroyEngine:(FlutterResult)result { + [self stopSdkLogPoller]; + if (self.speechEngine) { + [self.speechEngine destroyEngine]; + self.speechEngine = nil; + } + self.engineStarted = NO; + result(@{@"ok": @YES}); +} + +#pragma mark - SpeechEngineDelegate + +- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data { + NSString *str = data ? [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] : nil; + + switch (type) { + case SEEngineStart: + self.engineStarted = YES; + [self sendEvent:@{@"type": @"engine_start", @"payload": str ?: @""}]; + break; + case SEEngineStop: + self.engineStarted = NO; + [self sendEvent:@{@"type": @"engine_stop", @"payload": str ?: @""}]; + break; + case SEEngineError: { + NSLog(@"[SpeechPlugin] SEEngineError: %@", str); + // 尝试解出 code + message;解析失败就整段塞 message + id parsed = nil; + if (data) parsed = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + NSDictionary *info = [parsed isKindOfClass:[NSDictionary class]] ? parsed : nil; + [self sendEvent:@{ + @"type": @"engine_error", + @"code": info[@"code"] ?: info[@"errcode"] ?: info[@"error_code"] ?: @0, + @"message": info[@"message"] ?: info[@"msg"] ?: info[@"reason"] ?: (str ?: @""), + @"raw": str ?: @"" + }]; + break; + } + // === demo 风格 dialog 事件 (SpeechEngineToB) === + case SEDialogConnectionStarted: + [self sendEvent:@{@"type": @"channel_joined", @"payload": str ?: @""}]; + break; + case SEDialogConnectionFailed: + [self sendEvent:@{@"type": @"engine_error", @"message": str ?: @"connection failed"}]; + break; + case SEDialogConnectionFinished: + [self sendEvent:@{@"type": @"engine_stop", @"payload": str ?: @""}]; + break; + case SEDialogSessionStarted: + [self sendEvent:@{@"type": @"dialog_begin", @"payload": str ?: @""}]; + break; + case SEDialogSessionCanceled: + [self sendEvent:@{@"type": @"dialog_cancelled"}]; + break; + case SEDialogSessionFinished: + [self sendEvent:@{@"type": @"dialog_end", @"payload": str ?: @""}]; + break; + case SEDialogASRResponse: + // 用户讲话流式 {"results":[{"text":"..."}]} + [self sendEvent:@{@"type": @"asr_partial", @"payload": str ?: @""}]; + break; + case SEDialogASREnded: + [self sendEvent:@{@"type": @"vad_end"}]; + break; + case SEDialogChatResponse: + // AI 回复流式 {"content":"..."} + [self sendEvent:@{@"type": @"subtitle_on", @"payload": str ?: @""}]; + break; + case SEDialogChatEnded: + [self sendEvent:@{@"type": @"subtitle_off"}]; + break; + case SEDialogTTSResponse: + // TTS 文本片段 + [self sendEvent:@{@"type": @"tts_text", @"payload": str ?: @""}]; + break; + case SEDialogTTSSentenceStart: + [self sendEvent:@{@"type": @"ai_voice_begin"}]; + break; + case SEDialogTTSSentenceEnd: + [self sendEvent:@{@"type": @"tts_finish"}]; + break; + case SEDialogTTSEnded: + [self sendEvent:@{@"type": @"ai_voice_end"}]; + break; + case SETtsStartPlaying: + [self sendEvent:@{@"type": @"tts_start"}]; + break; + case SETtsAudioData: + case SEDialogPlayerAudio: { + // TTS 播放音频帧 → 计算 RMS → 限频 30Hz 推 mouth_value 驱动嘴型 + [self processTtsAudio:data]; + break; + } + case SEDialogRecorderAudio: + // 麦克风音频不需要驱动嘴型,丢弃 + break; + case SETtsFinishPlaying: + [self sendEvent:@{@"type": @"tts_finish"}]; + break; + case SEVadBegin: + [self sendEvent:@{@"type": @"vad_begin"}]; + break; + case SEVadEnd: + [self sendEvent:@{@"type": @"vad_end"}]; + break; + case SEAsrPartialResult: + [self sendEvent:@{@"type": @"asr_partial", @"payload": str ?: @""}]; + break; + case SEFinalResult: + [self sendEvent:@{@"type": @"asr_final", @"payload": str ?: @""}]; + break; + case SEEngineLog: + [self sendEvent:@{@"type": @"sdk_log", @"payload": str ?: @""}]; + break; + default: + // 调试用,仅 NSLog 不推 UI + NSLog(@"[SpeechPlugin] unhandled messageType=%d dataLen=%lu", (int)type, (unsigned long)(data ? data.length : 0)); + break; + } +} + +#endif // SPEECH_SDK_AVAILABLE + +@end diff --git a/avatar_flutter_app/ios/scripts/add_speech_plugin.rb b/avatar_flutter_app/ios/scripts/add_speech_plugin.rb new file mode 100644 index 0000000..281bafc --- /dev/null +++ b/avatar_flutter_app/ios/scripts/add_speech_plugin.rb @@ -0,0 +1,35 @@ +#!/usr/bin/env ruby +# Add SpeechEnginePlugin.h/m to the Runner target if not already present. +# Run with: ruby ios/scripts/add_speech_plugin.rb + +require 'xcodeproj' + +project_path = File.expand_path('../Runner.xcodeproj', __dir__) +project = Xcodeproj::Project.open(project_path) + +runner_group = project.main_group['Runner'] +abort('Runner group not found in pbxproj') unless runner_group + +runner_target = project.targets.find { |t| t.name == 'Runner' } +abort('Runner target not found in pbxproj') unless runner_target + +%w[SpeechEnginePlugin.h SpeechEnginePlugin.m].each do |name| + abs_path = File.expand_path("../Runner/#{name}", __dir__) + unless File.exist?(abs_path) + warn "Skipping #{name} (file not found at #{abs_path})" + next + end + + existing_ref = runner_group.files.find { |f| f.path == name } + file_ref = existing_ref || runner_group.new_reference(name) + + if name.end_with?('.m') + already_in_build = runner_target.source_build_phase.files_references.include?(file_ref) + runner_target.add_file_references([file_ref]) unless already_in_build + end + + puts "✓ #{name} #{existing_ref ? 'already referenced' : 'added'} in Runner target" +end + +project.save +puts "✓ Xcode project saved" diff --git a/avatar_flutter_app/lib/main.dart b/avatar_flutter_app/lib/main.dart index 3831847..dee0363 100644 --- a/avatar_flutter_app/lib/main.dart +++ b/avatar_flutter_app/lib/main.dart @@ -5,6 +5,8 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'avatar_bridge.dart'; import 'control_panel_sheet.dart'; +import 'voice_chat/voice_chat_controller.dart'; +import 'voice_chat/voice_chat_sheet.dart'; int _serverPort = 8080; @@ -68,9 +70,73 @@ class AvatarHomePage extends StatefulWidget { State createState() => _AvatarHomePageState(); } -class _AvatarHomePageState extends State { +class _AvatarHomePageState extends State with WidgetsBindingObserver { AvatarBridge? _bridge; bool _ready = false; + VoiceChatController? _voiceController; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _voiceController?.dispose(); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + final controller = _voiceController; + if (controller == null) return; + if (state == AppLifecycleState.resumed) { + // 回到前台:重新建立会话 + controller.startSession(); + } else if (state == AppLifecycleState.paused) { + controller.stopSession(); + } + } + + void _ensureVoiceSessionStarted() { + if (_bridge == null) return; + _voiceController ??= VoiceChatController(bridge: _bridge!); + _voiceController!.startSession(); + } + + Widget _buildVoiceFab() { + // 第一次进入页面时立刻启动会话;同时监听 controller 让 FAB 颜色实时反映状态 + _ensureVoiceSessionStarted(); + return ListenableBuilder( + listenable: _voiceController!, + builder: (context, _) { + final c = _voiceController!; + final (label, bg) = c.connected + ? ('语音对话 · 已连接', const Color(0xFF1E9F4D)) // 绿色 + : c.connecting + ? ('语音对话 · 连接中', const Color(0xFFD08F2C)) // 黄色 + : ('语音对话 · 未连接', const Color(0xFF6B6B73)); // 灰色 + return FloatingActionButton.extended( + heroTag: 'voice', + onPressed: () { + // 单击只展开抽屉看消息/日志;会话本身在后台一直跑 + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => VoiceChatSheet(controller: c), + ); + }, + icon: Icon(c.connected ? Icons.graphic_eq : Icons.sync_problem), + label: Text(label), + backgroundColor: bg, + foregroundColor: Colors.white, + ); + }, + ); + } @override Widget build(BuildContext context) { @@ -122,20 +188,29 @@ class _AvatarHomePageState extends State { ), ), floatingActionButton: _ready - ? FloatingActionButton.extended( - onPressed: () { - if (_bridge == null) return; - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (_) => ControlPanelSheet(bridge: _bridge!), - ); - }, - icon: const Icon(Icons.tune), - label: const Text('控制面板'), - backgroundColor: const Color(0xFF2766B6), - foregroundColor: Colors.white, + ? Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + _buildVoiceFab(), + const SizedBox(height: 12), + FloatingActionButton.extended( + heroTag: 'panel', + onPressed: () { + if (_bridge == null) return; + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => ControlPanelSheet(bridge: _bridge!), + ); + }, + icon: const Icon(Icons.tune), + label: const Text('控制面板'), + backgroundColor: const Color(0xFF2766B6), + foregroundColor: Colors.white, + ), + ], ) : null, ); diff --git a/avatar_flutter_app/lib/voice_chat/chat_message.dart b/avatar_flutter_app/lib/voice_chat/chat_message.dart new file mode 100644 index 0000000..6acfbb1 --- /dev/null +++ b/avatar_flutter_app/lib/voice_chat/chat_message.dart @@ -0,0 +1,15 @@ +enum ChatRole { user, assistant } + +class ChatMessage { + ChatRole role; + String text; + bool isFinal; + final DateTime createdAt; + + ChatMessage({ + required this.role, + required this.text, + this.isFinal = false, + DateTime? createdAt, + }) : createdAt = createdAt ?? DateTime.now(); +} diff --git a/avatar_flutter_app/lib/voice_chat/speech_engine_plugin.dart b/avatar_flutter_app/lib/voice_chat/speech_engine_plugin.dart new file mode 100644 index 0000000..c4ccbb8 --- /dev/null +++ b/avatar_flutter_app/lib/voice_chat/speech_engine_plugin.dart @@ -0,0 +1,65 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; + +/// Dart 侧对 iOS SpeechEnginePlugin 的包装。 +/// +/// 所有方法都 forwards 到 MethodChannel;事件流通过 [events] 暴露原始字典, +/// 上层 VoiceChatController 负责解析为状态变化。 +class SpeechEnginePlugin { + static const MethodChannel _method = + MethodChannel('avatar_flutter_app/speech_engine'); + static const EventChannel _event = + EventChannel('avatar_flutter_app/speech_events'); + + Stream>? _events; + + /// 事件流:每个事件都是 `{type: ..., ...payload}` 形式的 Map。 + /// type 取值:engine_start / engine_stop / engine_error / + /// asr_partial / asr_end / chat_partial / chat_end / + /// mouth_value / raw + Stream> get events => + _events ??= _event.receiveBroadcastStream().map(_castMap); + + static Map _castMap(dynamic raw) { + if (raw is Map) { + return raw.map((k, v) => MapEntry(k.toString(), v)); + } + return {'type': 'raw', 'payload': raw?.toString() ?? ''}; + } + + Future init({ + required String appId, + required String appKey, + required String token, + required String dialogId, + required String uid, + required String address, + required String uri, + String botName = '豆包', + String aecModelPath = '', + }) async { + await _method.invokeMethod('init', { + 'appId': appId, + 'appKey': appKey, + 'token': token, + 'dialogId': dialogId, + 'uid': uid, + 'address': address, + 'uri': uri, + 'botName': botName, + 'aecModelPath': aecModelPath, + }); + } + + Future start() => _method.invokeMethod('start'); + Future startTalking() => _method.invokeMethod('startTalking'); + Future finishTalking() => _method.invokeMethod('finishTalking'); + Future pauseTalking() => _method.invokeMethod('pauseTalking'); + Future resumeTalking() => _method.invokeMethod('resumeTalking'); + Future cancelCurrentDialog() => _method.invokeMethod('cancelCurrentDialog'); + Future sayHello(String content) => + _method.invokeMethod('sayHello', {'content': content}); + Future stop() => _method.invokeMethod('stop'); + Future destroy() => _method.invokeMethod('destroy'); +} diff --git a/avatar_flutter_app/lib/voice_chat/voice_chat_controller.dart b/avatar_flutter_app/lib/voice_chat/voice_chat_controller.dart new file mode 100644 index 0000000..9aa5cbf --- /dev/null +++ b/avatar_flutter_app/lib/voice_chat/voice_chat_controller.dart @@ -0,0 +1,396 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import '../avatar_bridge.dart'; +import 'chat_message.dart'; +import 'secrets.dart'; +import 'speech_engine_plugin.dart'; + +enum VoiceChatState { idle, listening, thinking, speaking, error } + +/// 把火山实时语音 SDK 的事件流编排成 avatar 状态机 + +/// 把消息流转交给 UI。 +/// +/// 按住说话生命周期: +/// 按下: +/// - 若 state==speaking 先 cancelCurrentDialog (打断) +/// - startTalking + 进 listening +/// 抬起: +/// - finishTalking + 进 thinking(最少 600ms 守底) +/// 收到 ai_voice_begin 或 tts_start: 进 speaking +/// 收到 ai_voice_end 或 tts_finish: 回 idle +class VoiceChatController extends ChangeNotifier { + final SpeechEnginePlugin _plugin = SpeechEnginePlugin(); + final AvatarBridge bridge; + + VoiceChatController({required this.bridge}); + + StreamSubscription>? _eventSub; + + VoiceChatState _state = VoiceChatState.idle; + VoiceChatState get state => _state; + + final List _messages = []; + List get messages => List.unmodifiable(_messages); + + bool _engineReady = false; + bool get engineReady => _engineReady; + + bool _simulatorMode = false; + bool get simulatorMode => _simulatorMode; + + /// 已加入 RTC 房间(channel_joined 触发后才为 true) + bool _connected = false; + bool get connected => _connected; + + /// 正在试图建立会话(startSession 已调用但还没 channel_joined) + bool _connecting = false; + bool get connecting => _connecting; + + bool _helloFired = false; + + String? _lastError; + String? get lastError => _lastError; + + /// 最近 30 条原始 SDK 事件(用于调试 UI 显示) + final List _debugLog = []; + List get debugLog => List.unmodifiable(_debugLog); + + DateTime? _thinkingStartedAt; + Timer? _thinkingMinTimer; + Timer? _thinkingTimeoutTimer; + static const _thinkingMinDuration = Duration(milliseconds: 250); + static const _thinkingTimeout = Duration(seconds: 8); + + // ---------------------- 生命周期 ---------------------- + + Future initialize() async { + _pushDebug('Controller.initialize()'); + _eventSub ??= _plugin.events.listen(_onEvent, onError: _onEventStreamError); + try { + _pushDebug('→ plugin.init(resource=${VoiceSecrets.dialogId})'); + await _plugin.init( + appId: VoiceSecrets.appId, + appKey: VoiceSecrets.appKey, + token: VoiceSecrets.token, + dialogId: VoiceSecrets.dialogId, + uid: VoiceSecrets.uid, + address: VoiceSecrets.address, + uri: VoiceSecrets.uri, + botName: VoiceSecrets.role.isEmpty ? '豆包' : VoiceSecrets.role, + // aec 模型路径留空,让 Plugin 端自己从 Flutter assets 定位 + ); + _pushDebug('plugin.init 返回成功'); + _engineReady = true; + notifyListeners(); + } on PlatformException catch (e) { + if (e.code == 'SIMULATOR_NOT_SUPPORTED') { + _simulatorMode = true; + _pushDebug('运行于模拟器:SDK 已禁用,仅供 UI 调试'); + notifyListeners(); + } else { + _setError('引擎初始化失败:${e.code} ${e.message ?? ''}'); + } + } catch (e) { + _setError('引擎初始化失败:$e'); + } + } + + /// 进入聊天抽屉后调用:启动会话 + Future startSession() async { + if (_connecting || _connected) return; + _pushDebug('Controller.startSession()'); + if (!_engineReady && !_simulatorMode) await initialize(); + if (_simulatorMode) { + _setState(VoiceChatState.idle); + return; + } + if (!_engineReady) { + _pushDebug('startSession 中止:引擎未 ready'); + return; + } + _connecting = true; + notifyListeners(); + try { + await _plugin.start(); + _setState(VoiceChatState.idle); + } catch (e) { + _connecting = false; + notifyListeners(); + _setError('会话启动失败:$e'); + } + } + + /// 退出抽屉前调用 + Future stopSession() async { + _thinkingMinTimer?.cancel(); + _thinkingTimeoutTimer?.cancel(); + _helloFired = false; + _connected = false; + _connecting = false; + try { + await _plugin.stop(); + } catch (_) {} + _setState(VoiceChatState.idle); + notifyListeners(); + } + + /// 手动打断 AI 说话(外部可调用,如点击屏幕) + Future interruptAi() async { + if (_state != VoiceChatState.speaking) return; + try { await _plugin.cancelCurrentDialog(); } catch (_) {} + } + + // ---------------------- 事件处理 ---------------------- + + Future _maybeFireHello() async { + if (_helloFired) return; + if (VoiceSecrets.helloText.isEmpty) return; + _helloFired = true; + try { + _pushDebug('→ sayHello "${VoiceSecrets.helloText}"'); + await _plugin.sayHello(VoiceSecrets.helloText); + } catch (e) { + _pushDebug('sayHello 异常 $e'); + } + } + + void _pushDebug(String line) { + // 仅写入 buffer,不主动 notify。UI 由具体业务事件(消息/状态)触发刷新即可。 + final ts = DateTime.now().toString().substring(11, 19); + _debugLog.add('[$ts] $line'); + if (_debugLog.length > 120) _debugLog.removeAt(0); + } + + void _onEvent(Map evt) { + final type = (evt['type'] ?? '').toString(); + if (kDebugMode) debugPrint('[VoiceChat] ← $type ${evt.toString().substring(0, evt.toString().length.clamp(0, 200))}'); + // 所有事件都进 debug 缓冲,让 release 模式也能在 UI 上看 + if (type == 'sdk_log_line' || type == 'plugin_log') { + // SDK / Plugin 日志完整显示,不截断 + _pushDebug(evt['line']?.toString() ?? evt.toString()); + } else { + final preview = evt.entries + .where((e) => e.key != 'type') + .map((e) => '${e.key}=${e.value.toString().length > 80 ? "${e.value.toString().substring(0, 80)}…" : e.value}') + .join(' '); + _pushDebug('$type${preview.isEmpty ? '' : ' $preview'}'); + } + + switch (type) { + case 'engine_start': + case 'channel_joined': + case 'dialog_begin': + // SpeechEngineToB 不一定回 channel_joined;engine_start 已经表示 SDK 在工作 + if (!_connected) { + _connecting = false; + _connected = true; + notifyListeners(); + _maybeFireHello(); + } + break; + case 'engine_stop': + _setState(VoiceChatState.idle); + break; + case 'engine_error': + final code = evt['code']?.toString() ?? '?'; + final msg = evt['message']?.toString() ?? ''; + final raw = evt['raw']?.toString() ?? ''; + _pushDebug('engine_error code=$code\n msg=$msg\n raw=$raw'); + _setError('SDK 错误 [$code] $msg\n$raw'); + break; + case 'subtitle_on': + _handleSubtitle(evt['payload']?.toString(), isFinal: false); + break; + case 'subtitle_off': + _handleSubtitle(evt['payload']?.toString(), isFinal: true); + break; + case 'ai_voice_begin': + case 'tts_start': + _enterSpeaking(); + break; + case 'ai_voice_end': + case 'tts_finish': + _enterIdle(); + break; + case 'dialog_cancelled': + _enterIdle(); + break; + case 'asr_partial': + _handleSubtitle(evt['payload']?.toString(), isFinal: false, forceRole: ChatRole.user); + break; + case 'asr_final': + _handleSubtitle(evt['payload']?.toString(), isFinal: true, forceRole: ChatRole.user); + break; + case 'mouth': + final v = (evt['value'] as num?)?.toDouble() ?? 0; + bridge.setMouthOpen(v.clamp(0.0, 1.0)); + break; + case 'vad_begin': + if (_state == VoiceChatState.idle) _setState(VoiceChatState.listening); + break; + case 'vad_end': + // SDK 自动判停 → 服务端开始处理 → 等待 AI 回复 + if (_state == VoiceChatState.listening) _enterThinking(); + break; + case 'dialog_end': + case 'dialog_init': + case 'sdk_log': + case 'sdk_log_line': + case 'plugin_log': + case 'raw': + break; + default: + break; + } + } + + void _onEventStreamError(Object err) { + _setError('事件流异常:$err'); + } + + /// SDK 字幕 JSON 一般含 role + text (字段名要根据真机数据调整) + /// 兼容几种常见形态。 + void _handleSubtitle(String? rawJson, {required bool isFinal, ChatRole? forceRole}) { + if (rawJson == null || rawJson.isEmpty) return; + Map? data; + try { + data = jsonDecode(rawJson) as Map; + } catch (_) { + data = null; + } + final text = data?['text']?.toString() + ?? data?['content']?.toString() + ?? data?['subtitle']?.toString() + ?? ''; + if (text.isEmpty) return; + + final roleStr = (data?['role'] ?? data?['speaker'] ?? '').toString().toLowerCase(); + final role = forceRole ?? + (roleStr.contains('user') ? ChatRole.user : ChatRole.assistant); + + if (role == ChatRole.user) { + // 看到 user 字幕意味着用户正在/已经说话 + if (_state == VoiceChatState.idle) _setState(VoiceChatState.listening); + _appendOrUpdateUser(text, isFinal); + } else { + _appendOrUpdateAssistant(text, isFinal); + // 收到 AI 字幕意味着思考结束、即将开口 + if (_state == VoiceChatState.thinking) _enterSpeaking(); + } + } + + // ---------------------- 消息列表维护 ---------------------- + + void _appendOrUpdateUser(String text, bool isFinal) { + ChatMessage? target; + for (final m in _messages.reversed) { + if (m.role == ChatRole.user && !m.isFinal) { target = m; break; } + } + if (target == null) { + _messages.add(ChatMessage(role: ChatRole.user, text: text, isFinal: isFinal)); + } else { + target.text = text; + if (isFinal) target.isFinal = true; + } + notifyListeners(); + } + + void _appendOrUpdateAssistant(String text, bool isFinal) { + ChatMessage? target; + for (final m in _messages.reversed) { + if (m.role == ChatRole.assistant && !m.isFinal) { target = m; break; } + } + if (target == null) { + _messages.add(ChatMessage(role: ChatRole.assistant, text: text, isFinal: isFinal)); + } else { + target.text = text; + if (isFinal) target.isFinal = true; + } + notifyListeners(); + } + + // ---------------------- 状态转移 ---------------------- + + void _setState(VoiceChatState next) { + if (_state == next) return; + _state = next; + _syncAvatar(); + notifyListeners(); + } + + void _enterThinking() { + _thinkingStartedAt = DateTime.now(); + _thinkingMinTimer?.cancel(); + _thinkingTimeoutTimer?.cancel(); + _thinkingTimeoutTimer = Timer(_thinkingTimeout, () { + if (_state == VoiceChatState.thinking) { + _pushDebug('thinking timeout → idle (服务端无响应 ${_thinkingTimeout.inSeconds}s)'); + _setState(VoiceChatState.idle); + } + }); + _setState(VoiceChatState.thinking); + } + + void _enterSpeaking() { + _thinkingTimeoutTimer?.cancel(); + final waited = _thinkingStartedAt == null + ? Duration.zero + : DateTime.now().difference(_thinkingStartedAt!); + final remaining = _thinkingMinDuration - waited; + if (remaining > Duration.zero && _state == VoiceChatState.thinking) { + _thinkingMinTimer?.cancel(); + _thinkingMinTimer = Timer(remaining, () => _setState(VoiceChatState.speaking)); + } else { + _setState(VoiceChatState.speaking); + } + } + + void _enterIdle() { + _thinkingMinTimer?.cancel(); + _thinkingTimeoutTimer?.cancel(); + _setState(VoiceChatState.idle); + } + + void _syncAvatar() { + switch (_state) { + case VoiceChatState.idle: + bridge.setState('idle'); + bridge.setMouthOpen(0); + break; + case VoiceChatState.listening: + bridge.setState('listening'); + bridge.setMouthOpen(0); + break; + case VoiceChatState.thinking: + bridge.setState('thinking'); + bridge.setMouthOpen(0); + break; + case VoiceChatState.speaking: + bridge.setState('speaking'); + // 嘴型由 Phase 2 的 mouth_value 事件单独驱动 + break; + case VoiceChatState.error: + bridge.setState('idle'); + break; + } + } + + void _setError(String msg) { + _lastError = msg; + if (kDebugMode) debugPrint('[VoiceChat] ERROR: $msg'); + _setState(VoiceChatState.error); + } + + @override + void dispose() { + _thinkingMinTimer?.cancel(); + _thinkingTimeoutTimer?.cancel(); + _eventSub?.cancel(); + _plugin.destroy(); + super.dispose(); + } +} diff --git a/avatar_flutter_app/lib/voice_chat/voice_chat_sheet.dart b/avatar_flutter_app/lib/voice_chat/voice_chat_sheet.dart new file mode 100644 index 0000000..ec4120c --- /dev/null +++ b/avatar_flutter_app/lib/voice_chat/voice_chat_sheet.dart @@ -0,0 +1,267 @@ +import 'package:flutter/material.dart'; + +import 'chat_message.dart'; +import 'voice_chat_controller.dart'; + +class VoiceChatSheet extends StatefulWidget { + final VoiceChatController controller; + const VoiceChatSheet({super.key, required this.controller}); + + @override + State createState() => _VoiceChatSheetState(); +} + +class _VoiceChatSheetState extends State { + final ScrollController _listScroll = ScrollController(); + bool _showDebug = false; + + @override + void initState() { + super.initState(); + widget.controller.addListener(_onTick); + } + + @override + void dispose() { + widget.controller.removeListener(_onTick); + _listScroll.dispose(); + super.dispose(); + } + + void _onTick() { + if (!mounted) return; + setState(() {}); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_listScroll.hasClients) { + _listScroll.animateTo( + _listScroll.position.maxScrollExtent, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + } + }); + } + + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + initialChildSize: 0.7, + minChildSize: 0.4, + maxChildSize: 0.95, + builder: (context, scrollController) { + return Container( + decoration: const BoxDecoration( + color: Color(0xE60F0F14), + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + border: Border(top: BorderSide(color: Color(0xFF333333))), + ), + child: Column( + children: [ + const SizedBox(height: 8), + Container( + width: 36, height: 4, + decoration: BoxDecoration( + color: Colors.white24, + borderRadius: BorderRadius.circular(2), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('AI 语音对话', + style: TextStyle(color: Color(0xFF80C0FF), + fontSize: 16, fontWeight: FontWeight.w600)), + const SizedBox(width: 12), + _stateBadge(widget.controller.state), + ], + ), + ), + const Divider(height: 1, color: Color(0xFF2A2A2F)), + + Expanded(child: _messageList()), + + if (widget.controller.lastError != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + child: SelectableText(widget.controller.lastError!, + style: const TextStyle(color: Colors.redAccent, fontSize: 11)), + ), + + if (_showDebug) _debugPanel(), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + TextButton.icon( + onPressed: () => setState(() => _showDebug = !_showDebug), + icon: Icon(_showDebug ? Icons.visibility_off : Icons.bug_report, + size: 14, color: Colors.white54), + label: Text(_showDebug ? '隐藏调试' : '调试日志', + style: const TextStyle(color: Colors.white54, fontSize: 11)), + ), + ], + ), + ), + + SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: _bottomBar(), + ), + ), + ], + ), + ); + }, + ); + } + + Widget _messageList() { + final msgs = widget.controller.messages; + if (msgs.isEmpty) { + return Center( + child: Text( + widget.controller.engineReady + ? '按住下方按钮,开始说话' + : '正在初始化引擎…', + style: const TextStyle(color: Colors.white38, fontSize: 13), + ), + ); + } + return ListView.builder( + controller: _listScroll, + padding: const EdgeInsets.fromLTRB(12, 12, 12, 12), + itemCount: msgs.length, + itemBuilder: (_, i) => _bubble(msgs[i]), + ); + } + + Widget _bubble(ChatMessage m) { + final isUser = m.role == ChatRole.user; + return Align( + alignment: isUser ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.symmetric(vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.72, + ), + decoration: BoxDecoration( + color: isUser ? const Color(0xFF2766B6) : const Color(0xFF222228), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: isUser ? Colors.transparent : const Color(0xFF333339)), + ), + child: Text( + m.text.isEmpty ? '…' : m.text, + style: TextStyle( + color: isUser ? Colors.white : Colors.white.withValues(alpha: 0.92), + fontSize: 13, + height: 1.4, + ), + ), + ), + ); + } + + Widget _debugPanel() { + final log = widget.controller.debugLog; + return Container( + constraints: const BoxConstraints(maxHeight: 280), + margin: const EdgeInsets.fromLTRB(12, 4, 12, 4), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFF101015), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: const Color(0xFF333339)), + ), + child: log.isEmpty + ? const Text('调试日志(事件流将显示在这里)', + style: TextStyle(color: Colors.white38, fontSize: 10)) + : SingleChildScrollView( + reverse: true, + child: SelectableText( + log.join('\n'), + style: const TextStyle( + color: Colors.white70, + fontSize: 10, + fontFamily: 'Menlo', + height: 1.35, + ), + ), + ), + ); + } + + /// 底部状态栏:左边显示当前状态文案,右边一个"结束会话"按钮 + Widget _bottomBar() { + final c = widget.controller; + final stateText = !c.engineReady + ? '正在初始化引擎…' + : !c.connected && !c.connecting + ? '未连接' + : c.connecting + ? '连接中…' + : switch (c.state) { + VoiceChatState.listening => '正在听你说…', + VoiceChatState.thinking => '思考中…', + VoiceChatState.speaking => 'AI 正在说话', + VoiceChatState.error => '出错', + VoiceChatState.idle => '已连接,开口说话即可', + }; + + return Row( + children: [ + Container( + width: 8, height: 8, + decoration: BoxDecoration( + color: c.connected ? const Color(0xFF1E9F4D) : Colors.white24, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + stateText, + style: const TextStyle(color: Colors.white70, fontSize: 12), + ), + ), + if (c.connected || c.connecting) + TextButton.icon( + onPressed: () => c.stopSession(), + icon: const Icon(Icons.stop_circle_outlined, size: 16, color: Colors.redAccent), + label: const Text('结束会话', style: TextStyle(color: Colors.redAccent, fontSize: 12)), + ) + else + TextButton.icon( + onPressed: () => c.startSession(), + icon: const Icon(Icons.play_circle_outline, size: 16, color: Color(0xFF1E9F4D)), + label: const Text('开始会话', style: TextStyle(color: Color(0xFF1E9F4D), fontSize: 12)), + ), + ], + ); + } + + static Widget _stateBadge(VoiceChatState s) { + final (label, color) = switch (s) { + VoiceChatState.idle => ('待机', Color(0xFF888888)), + VoiceChatState.listening => ('倾听', Color(0xFFE25C5C)), + VoiceChatState.thinking => ('思考', Color(0xFFFFB957)), + VoiceChatState.speaking => ('说话', Color(0xFF4488DD)), + VoiceChatState.error => ('错误', Color(0xFFCC4444)), + }; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.18), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: color.withValues(alpha: 0.5)), + ), + child: Text(label, style: TextStyle(color: color, fontSize: 11, fontWeight: FontWeight.w600)), + ); + } +} diff --git a/avatar_flutter_app/pubspec.yaml b/avatar_flutter_app/pubspec.yaml index c15bdd5..bc188bc 100644 --- a/avatar_flutter_app/pubspec.yaml +++ b/avatar_flutter_app/pubspec.yaml @@ -77,6 +77,7 @@ flutter: - assets/web/Resources/Haru/motions/ - assets/web/Resources/Haru/expressions/ - assets/web/Resources/Haru/sounds/ + - assets/voice/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images diff --git a/vrm-viewer-ios-app b/vrm-viewer-ios-app new file mode 160000 index 0000000..0640427 --- /dev/null +++ b/vrm-viewer-ios-app @@ -0,0 +1 @@ +Subproject commit 0640427f053719b4c456d9c793c18a44e60ea87b