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

341 lines
11 KiB
Objective-C

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