zyc 689fa8936b Integrate Volcengine realtime voice + Live2D mouth driving
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:39:23 +08:00

266 lines
7.9 KiB
Objective-C

//
// 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