Integrate Volcengine realtime voice + Live2D mouth driving
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2
.gitignore
vendored
@ -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
|
||||
|
||||
|
||||
BIN
33b906454de60431cd68612cfc3b8080.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
aa40cd4a-1bd6-4699-af37-73f0bca77308.jpeg
Normal file
|
After Width: | Height: | Size: 128 KiB |
3
avatar_flutter_app/.gitignore
vendored
@ -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
|
||||
|
||||
BIN
avatar_flutter_app/assets/voice/aec.model
Normal file
12
avatar_flutter_app/demo/SpeechDemoIOS/Podfile
Normal file
@ -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
|
||||
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:SpeechDemo.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,137 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1330"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "EC1FD742239A041000A4BE46"
|
||||
BuildableName = "SpeechDemo.app"
|
||||
BlueprintName = "SpeechDemo"
|
||||
ReferencedContainer = "container:SpeechDemo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "[BD-build-infer]"
|
||||
scriptText = "export BD_ENTRY_SCRIPT="$0:A";
|
||||
if [ -f ~/Library/Caches/com.bytedance.buildinfra/common/pre_build_hook_sync.sh ];then
|
||||
bash ~/Library/Caches/com.bytedance.buildinfra/common/pre_build_hook_sync.sh 0</dev/null >&- 2>&- || echo "skip";
|
||||
fi;
|
||||
[ ! -d ~/Library/Caches/com.bytedance.buildinfra/common ] && mkdir -p ~/Library/Caches/com.bytedance.buildinfra/common;
|
||||
curl -s -S -f https://ios.bytedance.net/wlapi/tosDownload/iosbinary/indexstore/pre_build_hook_async.sh -o ~/Library/Caches/com.bytedance.buildinfra/common/pre_build_hook_async.sh 0</dev/null >&- 2>&- &
|
||||
if [ -f ~/Library/Caches/com.bytedance.buildinfra/common/pre_build_hook_async.sh ];then
|
||||
bash ~/Library/Caches/com.bytedance.buildinfra/common/pre_build_hook_async.sh 0</dev/null >&- 2>&- &;
|
||||
fi;
|
||||
"
|
||||
shellToInvoke = "/bin/zsh">
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<PostActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "[BD-build-infer]"
|
||||
scriptText = "export BD_ENTRY_SCRIPT="$0:A";
|
||||
if [ -f ~/Library/Caches/com.bytedance.buildinfra/common/post_build_hook_sync.sh ];then
|
||||
bash ~/Library/Caches/com.bytedance.buildinfra/common/post_build_hook_sync.sh 0</dev/null >&- 2>&- || echo "skip";
|
||||
fi;
|
||||
[ ! -d ~/Library/Caches/com.bytedance.buildinfra/common ] && mkdir -p ~/Library/Caches/com.bytedance.buildinfra/common;
|
||||
curl -s -S -f https://ios.bytedance.net/wlapi/tosDownload/iosbinary/indexstore/post_build_hook_async.sh -o ~/Library/Caches/com.bytedance.buildinfra/common/post_build_hook_async.sh 0</dev/null >&- 2>&- &;
|
||||
if [ -f ~/Library/Caches/com.bytedance.buildinfra/common/post_build_hook_async.sh ];then
|
||||
bash ~/Library/Caches/com.bytedance.buildinfra/common/post_build_hook_async.sh 0</dev/null >&- 2>&- &;
|
||||
fi;
|
||||
"
|
||||
shellToInvoke = "/bin/zsh">
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PostActions>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "EC1FD75D239A041300A4BE46"
|
||||
BuildableName = "SpeechDemoTests.xctest"
|
||||
BlueprintName = "SpeechDemoTests"
|
||||
ReferencedContainer = "container:SpeechDemo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "EC1FD768239A041300A4BE46"
|
||||
BuildableName = "SpeechDemoUITests.xctest"
|
||||
BlueprintName = "SpeechDemoUITests"
|
||||
ReferencedContainer = "container:SpeechDemo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(PODS_ROOT)/LLDBInitFile"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "EC1FD742239A041000A4BE46"
|
||||
BuildableName = "SpeechDemo.app"
|
||||
BlueprintName = "SpeechDemo"
|
||||
ReferencedContainer = "container:SpeechDemo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "EC1FD742239A041000A4BE46"
|
||||
BuildableName = "SpeechDemo.app"
|
||||
BlueprintName = "SpeechDemo"
|
||||
ReferencedContainer = "container:SpeechDemo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@ -0,0 +1,17 @@
|
||||
//
|
||||
// AfpViewController.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by fangweiwei on 2021/8/31.
|
||||
// Copyright © 2021 fangweiwei. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AfpViewController : UIViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@ -0,0 +1,340 @@
|
||||
//
|
||||
// AfpViewController.m
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by fangweiwei on 2021/8/31.
|
||||
// Copyright © 2021 fangweiwei. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AfpViewController.h"
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "FileUtils.h"
|
||||
#import "SettingsHelper.h"
|
||||
#import "ViewController.h"
|
||||
|
||||
@interface AfpViewController () <SpeechEngineDelegate, UITextViewDelegate>
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
//
|
||||
// AsrOfflineViewController.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2020/9/8.
|
||||
// Copyright © 2020 fengkai.0518. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface AsrOfflineViewController : UIViewController
|
||||
|
||||
@end
|
||||
@ -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 <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "FileUtils.h"
|
||||
#import "SettingsHelper.h"
|
||||
#import "ViewController.h"
|
||||
#import "SensitiveDefines.h"
|
||||
|
||||
@interface AsrOfflineViewController () <SpeechEngineDelegate, UITextViewDelegate>
|
||||
|
||||
@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
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
//
|
||||
// AsrViewController.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2020/9/8.
|
||||
// Copyright © 2020 fengkai.0518. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface AsrViewController : UIViewController
|
||||
|
||||
@end
|
||||
@ -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 <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "FileUtils.h"
|
||||
#import "SettingsHelper.h"
|
||||
#import "ViewController.h"
|
||||
#import "SensitiveDefines.h"
|
||||
|
||||
@interface AsrViewController () <SpeechEngineDelegate, UITextViewDelegate>
|
||||
|
||||
// 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
|
||||
23
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/Assets.xcassets/vad.imageset/Contents.json
vendored
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
BIN
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/Assets.xcassets/vad.imageset/vad_120_120.png
vendored
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/Assets.xcassets/vad.imageset/vad_180_180.png
vendored
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/Assets.xcassets/vad.imageset/vad_60_60.png
vendored
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
@ -0,0 +1,13 @@
|
||||
//
|
||||
// AuViewController.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2023/5/16.
|
||||
// Copyright © 2023 chengzihao.ds. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface AuViewController : UIViewController
|
||||
|
||||
@end
|
||||
@ -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 <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "FileUtils.h"
|
||||
#import "SettingsHelper.h"
|
||||
#import "ViewController.h"
|
||||
#import "SensitiveDefines.h"
|
||||
|
||||
@interface AuViewController () <SpeechEngineDelegate, UITextViewDelegate>
|
||||
|
||||
@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
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
//
|
||||
// BiTTSViewController.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by ByteDance on 2025/7/3.
|
||||
// Copyright © 2025 fangweiwei. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface BiTTSViewController : UIViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@ -0,0 +1,778 @@
|
||||
//
|
||||
// BiTTSViewController.m
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by ByteDance on 2025/7/3.
|
||||
// Copyright © 2025 fangweiwei. All rights reserved.
|
||||
//
|
||||
|
||||
#import "BiTTSViewController.h"
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <objc/objc.h>
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "FileUtils.h"
|
||||
#import "SettingsHelper.h"
|
||||
#import "ViewController.h"
|
||||
#import "SensitiveDefines.h"
|
||||
|
||||
static int TTS_MAX_RETRY_COUNT = 3;
|
||||
|
||||
@interface BiTTSViewController () <SpeechEngineDelegate, UITextViewDelegate>
|
||||
|
||||
@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<NSTextCheckingResult *> *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<NSTextCheckingResult *> *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
|
||||
@ -0,0 +1,13 @@
|
||||
//
|
||||
// BigAsrViewController.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2024/9/9.
|
||||
// Copyright © 2024 chengzihao.ds. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface BigAsrViewController : UIViewController
|
||||
|
||||
@end
|
||||
@ -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 <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "FileUtils.h"
|
||||
#import "SettingsHelper.h"
|
||||
#import "ViewController.h"
|
||||
#import "SensitiveDefines.h"
|
||||
|
||||
@interface BigAsrViewController () <SpeechEngineDelegate, UITextViewDelegate>
|
||||
|
||||
// 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
|
||||
@ -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})
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
//
|
||||
// CaptViewController.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2020/9/8.
|
||||
// Copyright © 2020 fengkai.0518. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface CaptViewController : UIViewController
|
||||
|
||||
@end
|
||||
@ -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 <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "FileUtils.h"
|
||||
#import "SettingsHelper.h"
|
||||
#import "ViewController.h"
|
||||
#import "SensitiveDefines.h"
|
||||
|
||||
@interface CaptViewController () <SpeechEngineDelegate, UITextViewDelegate>
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
//
|
||||
// DialogDelegateViewController.m
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2025/3/27.
|
||||
// Copyright © 2025 bytedance. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface DialogDelegateViewController : UIViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@ -0,0 +1,549 @@
|
||||
//
|
||||
// DialogDelegateViewController.m
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2025/3/27.
|
||||
// Copyright © 2025 bytedance. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DialogDelegateViewController.h"
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "FileUtils.h"
|
||||
#import "SettingsHelper.h"
|
||||
#import "ViewController.h"
|
||||
#import "SensitiveDefines.h"
|
||||
#import "utils/DialogMessage.h"
|
||||
|
||||
#pragma mark - DialogDelegateViewController
|
||||
@interface DialogDelegateViewController () <SpeechEngineDelegate, UITextViewDelegate>
|
||||
|
||||
// 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
|
||||
@ -0,0 +1,17 @@
|
||||
//
|
||||
// DialogViewController.m
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2025/3/27.
|
||||
// Copyright © 2025 bytedance. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface DialogViewController : UIViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@ -0,0 +1,512 @@
|
||||
//
|
||||
// DialogViewController.m
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2025/3/27.
|
||||
// Copyright © 2025 bytedance. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DialogViewController.h"
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "FileUtils.h"
|
||||
#import "SettingsHelper.h"
|
||||
#import "ViewController.h"
|
||||
#import "SensitiveDefines.h"
|
||||
#import "utils/DialogMessage.h"
|
||||
|
||||
#pragma mark - DialogViewController
|
||||
@interface DialogViewController () <SpeechEngineDelegate, UITextViewDelegate>
|
||||
|
||||
// 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
|
||||
@ -0,0 +1,13 @@
|
||||
//
|
||||
// FulllinkViewController.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2020/9/8.
|
||||
// Copyright © 2020 fengkai.0518. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FulllinkViewController : UIViewController
|
||||
|
||||
@end
|
||||
@ -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 <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "FileUtils.h"
|
||||
#import "SettingsHelper.h"
|
||||
#import "ViewController.h"
|
||||
#import "SensitiveDefines.h"
|
||||
|
||||
@interface FulllinkViewController () <SpeechEngineDelegate, UITextViewDelegate>
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
//
|
||||
// AsrOfflineViewController.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2020/9/8.
|
||||
// Copyright © 2020 fengkai.0518. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface KwsViewController : UIViewController
|
||||
|
||||
@end
|
||||
@ -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 <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "FileUtils.h"
|
||||
#import "SettingsHelper.h"
|
||||
#import "ViewController.h"
|
||||
#import "SpeechResourceManager.h"
|
||||
#import "SensitiveDefines.h"
|
||||
|
||||
@interface KwsViewController () <SpeechEngineDelegate, UITextViewDelegate>
|
||||
|
||||
@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
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Speech demo would like to access your recorder data.</string>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>Default Configuration</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>SceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,13 @@
|
||||
//
|
||||
// TtsNormalViewController.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2020/9/8.
|
||||
// Copyright © 2020 fengkai.0518. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface TtsNormalViewController : UIViewController
|
||||
|
||||
@end
|
||||
@ -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 <Foundation/Foundation.h>
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "FileUtils.h"
|
||||
#import "SettingsHelper.h"
|
||||
#import "ViewController.h"
|
||||
#import "SensitiveDefines.h"
|
||||
|
||||
@interface TtsNormalViewController () <SpeechEngineDelegate, UITextViewDelegate>
|
||||
|
||||
@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
|
||||
@ -0,0 +1,13 @@
|
||||
//
|
||||
// TtsNovelViewController.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2020/9/8.
|
||||
// Copyright © 2020 fengkai.0518. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface TtsNovelViewController : UIViewController
|
||||
|
||||
@end
|
||||
@ -0,0 +1,17 @@
|
||||
//
|
||||
// UniTTSViewController.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by ByteDance on 2025/7/3.
|
||||
// Copyright © 2025 ByteDance. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface UniTTSViewController : UIViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@ -0,0 +1,476 @@
|
||||
//
|
||||
// UniTTSViewController.m
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by ByteDance on 2025/7/3.
|
||||
// Copyright © 2025 ByteDance. All rights reserved.
|
||||
//
|
||||
|
||||
#import "UniTTSViewController.h"
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <objc/objc.h>
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "FileUtils.h"
|
||||
#import "SettingsHelper.h"
|
||||
#import "ViewController.h"
|
||||
#import "SensitiveDefines.h"
|
||||
|
||||
@interface UniTTSViewController () <SpeechEngineDelegate, UITextViewDelegate>
|
||||
|
||||
@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
|
||||
@ -0,0 +1,18 @@
|
||||
//
|
||||
// VadViewController.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2023/1/30.
|
||||
// Copyright © 2023 tinalei.richard. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface VadViewController : UIViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@ -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 <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "FileUtils.h"
|
||||
#import "SettingsHelper.h"
|
||||
#import "ViewController.h"
|
||||
#import "SensitiveDefines.h"
|
||||
|
||||
@interface VadViewController () <SpeechEngineDelegate, UITextViewDelegate>
|
||||
|
||||
@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
|
||||
@ -0,0 +1,14 @@
|
||||
//
|
||||
// VoiceCloneViewController.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2021/2/20.
|
||||
// Copyright © 2021 chengzihao.ds. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface VoiceCloneViewController : UIViewController
|
||||
|
||||
@end
|
||||
|
||||
@ -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 <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "FileUtils.h"
|
||||
#import "SettingsHelper.h"
|
||||
#import "ViewController.h"
|
||||
|
||||
@interface VoiceCloneViewController () <SpeechEngineDelegate, UITextViewDelegate>
|
||||
|
||||
@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
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
//
|
||||
// VoiceConvViewController.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by chengzihao.ds on 2021/4/1.
|
||||
// Copyright © 2021 chengzihao.ds. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface VoiceConvViewController : UIViewController
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@ -0,0 +1,432 @@
|
||||
//
|
||||
// VoiceConvViewController.m
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by chengzihao.ds on 2021/4/1.
|
||||
// Copyright © 2021 chengzihao.ds. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "VoiceConvViewController.h"
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "FileUtils.h"
|
||||
#import "SettingsHelper.h"
|
||||
#import "ViewController.h"
|
||||
#import "SensitiveDefines.h"
|
||||
|
||||
@interface VoiceConvViewController () <SpeechEngineDelegate, UITextViewDelegate>
|
||||
|
||||
@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
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
//
|
||||
// AppDelegate.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by fangweiwei on 2019/12/6.
|
||||
// Copyright © 2019 fangweiwei. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "SpeechEngineToB/SpeechEngineToB-umbrella.h"
|
||||
|
||||
@interface AppDelegate : UIResponder <UIApplicationDelegate>
|
||||
|
||||
@property(nonatomic, strong) UIWindow *window;
|
||||
@property(nonatomic, readonly, strong) NSString *deviceID;
|
||||
|
||||
@end
|
||||
@ -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<UISceneSession *> *)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
|
||||
@ -0,0 +1,16 @@
|
||||
//
|
||||
// SceneDelegate.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by fangweiwei on 2019/12/6.
|
||||
// Copyright © 2019 fangweiwei. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface SceneDelegate : UIResponder <UIWindowSceneDelegate>
|
||||
|
||||
@property (strong, nonatomic) UIWindow * window;
|
||||
|
||||
@end
|
||||
|
||||
@ -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
|
||||
@ -0,0 +1,67 @@
|
||||
//
|
||||
// SettingViewDelegate.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2021/3/26.
|
||||
// Copyright © 2021 chengzihao.ds. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "Settings.h"
|
||||
|
||||
#pragma mark - SettingItemCell
|
||||
@interface SettingItemCell : UITableViewCell<UITextFieldDelegate>
|
||||
@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<UIPickerViewDelegate, UIPickerViewDataSource>
|
||||
|
||||
@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<UITableViewDelegate, UITableViewDataSource>
|
||||
@property (strong, nonatomic) Settings* settings;
|
||||
|
||||
+ (instancetype)build:(Settings*)settings;
|
||||
|
||||
@end
|
||||
|
||||
@ -0,0 +1,280 @@
|
||||
//
|
||||
// SettingViewDelegate.m
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2021/3/26.
|
||||
// Copyright © 2021 chengzihao.ds. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#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
|
||||
@ -0,0 +1,83 @@
|
||||
//
|
||||
// Settings.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2021/3/26.
|
||||
// Copyright © 2021 chengzihao.ds. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
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
|
||||
|
||||
@ -0,0 +1,260 @@
|
||||
//
|
||||
// Settings.m
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2021/3/26.
|
||||
// Copyright © 2021 chengzihao.ds. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#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
|
||||
@ -0,0 +1,207 @@
|
||||
//
|
||||
// SettingsHelper.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2020/9/9.
|
||||
// Copyright © 2020 fengkai.0518. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#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
|
||||
@ -0,0 +1,804 @@
|
||||
//
|
||||
// SettingsHelper.m
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2020/9/9.
|
||||
// Copyright © 2020 fengkai.0518. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#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
|
||||
@ -0,0 +1,19 @@
|
||||
//
|
||||
// SettingsViewController.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by fangweiwei on 2020/2/28.
|
||||
// Copyright © 2020 fangweiwei. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SettingsViewController : UITableViewController
|
||||
|
||||
@property (strong, nonatomic) NSString *viewId;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@ -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 () <UITextFieldDelegate>
|
||||
|
||||
@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
|
||||
@ -0,0 +1,17 @@
|
||||
//
|
||||
// TestAfpViewController.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2024/7/10.
|
||||
// Copyright © 2024 chengzihao.ds. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TestAfpViewController : UIViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@ -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 <AVFoundation/AVFoundation.h>
|
||||
|
||||
#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 () <SpeechEngineDelegate, UITextViewDelegate>
|
||||
|
||||
@property (weak, nonatomic) IBOutlet UITextView *resultTextView;
|
||||
@property (weak, nonatomic) IBOutlet UITextField *statusTextView;
|
||||
|
||||
@property (strong, nonatomic) NSMutableArray<AfpPressureTask *> *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
|
||||
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
//
|
||||
// TestAsrOfflineRtfViewController.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2023/6/6.
|
||||
// Copyright © 2023 chengzihao.ds. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface TestAsrOfflineRtfViewController : UIViewController
|
||||
|
||||
@end
|
||||
@ -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 <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "FileUtils.h"
|
||||
#import "SettingsHelper.h"
|
||||
#import "ViewController.h"
|
||||
|
||||
@interface TestAsrOfflineRtfViewController () <SpeechEngineDelegate, UITextViewDelegate>
|
||||
|
||||
@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
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
//
|
||||
// AsrViewController.h
|
||||
// SpeechDemo
|
||||
//
|
||||
// Created by bytedance on 2020/9/8.
|
||||
// Copyright © 2020 fengkai.0518. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface TestAsrViewController : UIViewController
|
||||
|
||||
@end
|
||||
@ -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 <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "FileUtils.h"
|
||||
#import "SettingsHelper.h"
|
||||
#import "ViewController.h"
|
||||
#import "SensitiveDefines.h"
|
||||
|
||||
@interface TestAsrViewController () <SpeechEngineDelegate, UITextViewDelegate>
|
||||
|
||||
// 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
|
||||
BIN
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/testdata/aec.model
vendored
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
23
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/asr.imageset/Contents.json
vendored
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
BIN
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/asr.imageset/asr_120_120.png
vendored
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/asr.imageset/asr_180_180.png
vendored
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/asr.imageset/asr_60_60.png
vendored
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
23
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/capt.imageset/Contents.json
vendored
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
BIN
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/capt.imageset/capt_120_120.png
vendored
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/capt.imageset/capt_180_180.png
vendored
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/capt.imageset/capt_60_60.png
vendored
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
@ -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
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
@ -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
|
||||
}
|
||||
}
|
||||
BIN
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/settings.imageset/settings-1.png
vendored
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/settings.imageset/settings-2.png
vendored
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/settings.imageset/settings.png
vendored
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
23
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/tts.imageset/Contents.json
vendored
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
BIN
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/tts.imageset/tts_120_120.png
vendored
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/tts.imageset/tts_180_180.png
vendored
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
avatar_flutter_app/demo/SpeechDemoIOS/SpeechDemo/ui/Assets.xcassets/tts.imageset/tts_60_60.png
vendored
Normal file
|
After Width: | Height: | Size: 4.2 KiB |