#import "ChivoxAienginePlugin.h" #include "CAIEngine/CAIEngine.h" // 经过实际测试,有以下结论: // 1. dart和oc代码运行在不同线程,oc代码中阻塞并不会导致dart阻塞 // 2. 每次dart调用oc代码,都会派发到oc主线程调用,并且是串行方式。 @implementation ChivoxAienginePlugin { NSMutableDictionary *engineDic; FlutterMethodChannel *callbackChannel; NSObject *registrar; } + (void)registerWithRegistrar:(NSObject*)registrar { FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"com.chivox.aiengine/call" binaryMessenger:[registrar messenger]]; ChivoxAienginePlugin* instance = [[ChivoxAienginePlugin alloc] initWithRegistrar:registrar]; [registrar addMethodCallDelegate:instance channel:channel]; } - (instancetype) init { if (self = [super init]) { self->engineDic = [NSMutableDictionary dictionary]; } return self; } - (instancetype) initWithRegistrar:(NSObject*)registrar { if (self = [self init]) { self->callbackChannel = [FlutterMethodChannel methodChannelWithName:@"com.chivox.aiengine/callback" binaryMessenger:[registrar messenger]]; self->registrar = registrar; } return self; } - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { if ([@"getPlatformVersion" isEqualToString:call.method]) { result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]); } else if ([@"aiengineNew" isEqualToString:call.method]) { [self handle_aiengineNew:call result:result]; } else if ([@"aiengineDelete" isEqualToString:call.method]) { [self handle_aiengineDelete:call result:result]; } else if ([@"aiengineStart" isEqualToString:call.method]) { [self handle_aiengineStart:call result:result]; } else if ([@"aiengineFeed" isEqualToString:call.method]) { [self handle_aiengineFeed:call result:result]; } else if ([@"aiengineStop" isEqualToString:call.method]) { [self handle_aiengineStop:call result:result]; } else if ([@"aiengineCancel" isEqualToString:call.method]) { [self handle_aiengineCancel:call result:result]; } else if ([@"getDeviceId" isEqualToString:call.method]) { [self handle_getDeviceId:call result:result]; } else if ([@"getSerialNumber" isEqualToString:call.method]) { [self handle_getSerialNumber:call result:result]; } else if ([@"clearSavedSerialNumber" isEqualToString:call.method]) { [self handle_clearSavedSerialNumber:call result:result]; } else if ([@"getProvision" isEqualToString:call.method]) { [self handle_getProvision:call result:result]; } else if ([@"extractRes" isEqualToString:call.method]) { [self handle_toolExtractRes:call result:result]; } else if ([@"loadNativeCfg" isEqualToString:call.method]) { [self handle_toolLoadNativeCfg:call result:result]; } else { result(FlutterMethodNotImplemented); } } static NSString * _GenerateEngineId() { static dispatch_once_t onceToken; static NSLock *_lock = nil; dispatch_once(&onceToken, ^{ _lock = [[NSLock alloc] init]; }); int cnt = 0; @synchronized (_lock) { static int _cnt = 0; cnt = _cnt; _cnt++; if (_cnt > 65535) { _cnt = 0; } } int64_t timestamp = [[NSDate date] timeIntervalSince1970]; NSString *engineId = [NSString stringWithFormat:@"%llx-%x", timestamp, cnt]; return engineId; } #define _ParseJsonString(str, err) \ [NSJSONSerialization JSONObjectWithData:[str dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers error:err] #define _CondSignal(cond) do {\ [cond lock]; \ [cond signal]; \ [cond unlock]; \ }while(0) #define _CondWait(cond) do {\ [cond lock]; \ [cond wait]; \ [cond unlock]; \ }while(0) /** handle_aiengineNew @param call call.arguments[0]: cfg string; @param result engineId / FlutterError */ - (void)handle_aiengineNew:(FlutterMethodCall*)call result:(FlutterResult)result { NSString *strCfg = (NSString *) call.arguments[0]; // 检查参数 NSError *jerr = nil; id cfg = _ParseJsonString(strCfg, &jerr); if (jerr != nil) { int eid = -1; id emsg = @"cfg is not valid json"; result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); return; } if (![cfg isKindOfClass:[NSMutableDictionary class]]) { int eid = -1; id emsg = @"cfg is not in jsonobject type"; result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); return; } NSCondition *cond = [[NSCondition alloc] init]; __block NSString *engineId = @""; __block int eid = 0; __block NSString *emsg = @""; // 创建引擎 [ChivoxAIEngine create:cfg cb:[ChivoxAIEngineCreateCallback onSuccess:^(ChivoxAIEngine * _Nonnull engine) { engineId = _GenerateEngineId(); NSLog(@"ChivoxAIEngine create success: engineId=%@, engine=%p", engineId, engine); if ([engineId length] == 0) { if (eid == 0) eid = -1; result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:@"generate engineid fail" details:nil]); return; } [self->engineDic setObject:engine forKey:engineId]; //_CondSignal(cond); result(engineId); } onFail:^(ChivoxAIRetValue * _Nonnull err) { NSLog(@"ChivoxAIEngine create fail: %@", err); eid = err.errId; emsg = err.error; if (eid == 0) eid = -1; //_CondSignal(cond); result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); }]]; //_CondWait(cond); //if ([engineId length] == 0) { // if (eid == 0) eid = -1; // result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); // return; //} //result(engineId); } /** handle_aiengineDelete @param call call.arguments[0]: engineId; @param result void */ - (void)handle_aiengineDelete:(FlutterMethodCall*)call result:(FlutterResult)result { NSString *engineId = (NSString *) call.arguments[0]; ChivoxAIEngine *engine = [self->engineDic objectForKey:engineId]; if (engine != nil) { [self->engineDic removeObjectForKey:engineId]; [engine destory]; } result(nil); } static id _CheckMakeAudioSrc(NSDictionary *mapAudioSrc, FlutterError **err) { *err = nil; id srcType = [mapAudioSrc objectForKey:@"srcType"]; if (srcType == nil || ![srcType isKindOfClass:[NSString class]]) { int eid = -1; id emsg = @"invalid param audioSrc: 'srcType' required, must be String"; *err = [FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]; return nil; } if (![srcType isEqualToString:@"innerRecorder"] && ![srcType isEqualToString:@"outerFeed"]) { int eid = -1; id emsg = [NSString stringWithFormat:@"invalid param audioSrc: 'srcType' not valid: %@", srcType]; *err = [FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]; return nil; } if ([srcType isEqualToString:@"outerFeed"]) { return [[ChivoxAIOuterFeed alloc] init]; } // else innerRecorder ChivoxAIInnerRecorder *inner = [[ChivoxAIInnerRecorder alloc] init]; id innerParam = [mapAudioSrc objectForKey:@"innerRecorderParam"]; if (innerParam == nil) { return inner; } if (![innerParam isKindOfClass:[NSDictionary class]]) { int eid = -1; id emsg = @"invalid param audioSrc: 'innerRecorderParam' must be Map or null"; *err = [FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]; return nil; } id duration = [innerParam objectForKey:@"duration"]; id channel = [innerParam objectForKey:@"channel"]; id sampleBytes = [innerParam objectForKey:@"sampleBytes"]; id sampleRate = [innerParam objectForKey:@"sampleRate"]; id saveFile = [innerParam objectForKey:@"saveFile"]; if (duration != nil && ![duration isKindOfClass:[NSNumber class]]) { int eid = -1; id emsg = @"invalid param audioSrc: 'innerRecorderParam.duration' must be number or null"; *err = [FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]; return nil; } if (channel != nil && ![channel isKindOfClass:[NSNumber class]]) { int eid = -1; id emsg = @"invalid param audioSrc: 'innerRecorderParam.channel' must be number or null"; *err = [FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]; return nil; } if (sampleBytes != nil && ![sampleBytes isKindOfClass:[NSNumber class]]) { int eid = -1; id emsg = @"invalid param audioSrc: 'innerRecorderParam.sampleBytes' must be number or nul"; *err = [FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]; return nil; } if (sampleRate != nil && ![sampleRate isKindOfClass:[NSNumber class]]) { int eid = -1; id emsg = @"invalid param audioSrc: 'innerRecorderParam.sampleRate' must be number or null"; *err = [FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]; return nil; } if (saveFile != nil && ![saveFile isKindOfClass:[NSString class]]) { int eid = -1; id emsg = @"invalid param audioSrc: 'innerRecorderParam.saveFile' must be String or null"; *err = [FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]; return nil; } if (duration != nil) { inner.recordParam.duration = [duration intValue]; } if (channel != nil) { inner.recordParam.channel = [channel intValue]; } if (sampleBytes != nil) { inner.recordParam.sampleBytes = [sampleBytes intValue]; } if (sampleRate != nil) { inner.recordParam.sampleRate = [sampleRate intValue]; } if (saveFile != nil) { inner.recordParam.saveFile = saveFile; } return inner; } static id _MakeAiengineCallbackArgs(NSString *callbackId, NSString *type, ChivoxAIEvalResult *result) { return @[ callbackId, type, @(result.isLast), result.tokenId ? result.tokenId : @"", result.text ? result.text : [NSNull null], result.data ? [FlutterStandardTypedData typedDataWithBytes:result.data] : [NSNull null], result.recFilePath ? result.recFilePath : [NSNull null], ]; }; /** handle_aiengineStart @param call call.arguments[0]: engineId; call.arguments[1]: audioSrc Map; call.arguments[2]: param string; @param result tokenId / FlutterError */ - (void)handle_aiengineStart:(FlutterMethodCall*)call result:(FlutterResult)result { NSString *engineId = (NSString *) call.arguments[0]; NSDictionary *mapAudioSrc = (NSDictionary *) call.arguments[1]; NSString *strParam = (NSString *) call.arguments[2]; NSString *callbackId = (NSString *) call.arguments[3]; FlutterError *flerr = nil; id audioSrc = _CheckMakeAudioSrc(mapAudioSrc, &flerr); if (flerr != nil) { result(flerr); return; } NSError *jerr = nil; id param = _ParseJsonString(strParam, &jerr); if (jerr != nil) { int eid = -1; id emsg = @"param is not valid json"; result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); return; } if (![param isKindOfClass:[NSMutableDictionary class]]) { int eid = -1; id emsg = @"param is not in jsonobject type"; result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); return; } ChivoxAIEngine *engine = [self->engineDic objectForKey:engineId]; if (engine == nil) { int eid = -1; id emsg = @"the engineId doesn't represent any engine"; result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); return; } NSMutableString *tokenId = [NSMutableString string]; ChivoxAIEvalResultListener *listener = [[ChivoxAIEvalResultListener alloc] init]; listener.onEvalResult = ^(NSString * _Nonnull tokenId, ChivoxAIEvalResult * _Nonnull result) { id args = _MakeAiengineCallbackArgs(callbackId, @"evalResult", result); [self->callbackChannel invokeMethod:@"aiengine_callback" arguments:args]; }; listener.onBinResult = ^(NSString * _Nonnull tokenId, ChivoxAIEvalResult * _Nonnull result) { id args = _MakeAiengineCallbackArgs(callbackId, @"binResult", result); [self->callbackChannel invokeMethod:@"aiengine_callback" arguments:args]; }; listener.onError = ^(NSString * _Nonnull tokenId, ChivoxAIEvalResult * _Nonnull result) { id args = _MakeAiengineCallbackArgs(callbackId, @"error", result); [self->callbackChannel invokeMethod:@"aiengine_callback" arguments:args]; }; listener.onVad = ^(NSString * _Nonnull tokenId, ChivoxAIEvalResult * _Nonnull result) { id args = _MakeAiengineCallbackArgs(callbackId, @"vad", result); [self->callbackChannel invokeMethod:@"aiengine_callback" arguments:args]; }; listener.onSoundIntensity = ^(NSString * _Nonnull tokenId, ChivoxAIEvalResult * _Nonnull result) { id args = _MakeAiengineCallbackArgs(callbackId, @"soundIntensity", result); [self->callbackChannel invokeMethod:@"aiengine_callback" arguments:args]; }; listener.onOther = ^(NSString * _Nonnull tokenId, ChivoxAIEvalResult * _Nonnull result) { id args = _MakeAiengineCallbackArgs(callbackId, @"other", result); [self->callbackChannel invokeMethod:@"aiengine_callback" arguments:args]; }; ChivoxAIRetValue * ret = [engine start:audioSrc tokenId:tokenId param:param listener:listener]; if (ret != nil && [ret errId] != 0) { result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", [ret errId]] message:[ret error] details:nil]); return; } result([NSString stringWithString:tokenId]); } /** handle_aiengineFeed @param call call.arguments[0]: engineId; call.arguments[1]: bytes Uint8List; call.arguments[2]: length int; @param result void / FlutterError */ - (void)handle_aiengineFeed:(FlutterMethodCall*)call result:(FlutterResult)result { NSString *engineId = (NSString *) call.arguments[0]; FlutterStandardTypedData *bytes = (FlutterStandardTypedData *) call.arguments[1]; NSNumber *length = (NSNumber *)call.arguments[2]; if ([length intValue] <= 0) { result(nil); return; } ChivoxAIEngine *engine = [self->engineDic objectForKey:engineId]; if (engine == nil) { int eid = -1; id emsg = @"the engineId doesn't represent any engine"; result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); return; }; NSData *data = [bytes data]; ChivoxAIRetValue *ret = [engine feed:[data bytes] length:[length intValue]]; if (ret != nil && [ret errId] != 0) { result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", [ret errId]] message:[ret error] details:nil]); return; } result(nil); } /** handle_aiengineStop @param call call.arguments[0]: engineId; @param result void / FlutterError */ - (void)handle_aiengineStop:(FlutterMethodCall*)call result:(FlutterResult)result { NSString *engineId = (NSString *) call.arguments[0]; ChivoxAIEngine *engine = [self->engineDic objectForKey:engineId]; if (engine == nil) { int eid = -1; id emsg = @"the engineId doesn't represent any engine"; result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); return; }; ChivoxAIRetValue *ret = [engine stop]; if (ret != nil && [ret errId] != 0) { result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", [ret errId]] message:[ret error] details:nil]); return; } result(nil); } /** handle_aiengineCancel @param call call.arguments[0]: engineId; @param result void / FlutterError */ - (void)handle_aiengineCancel:(FlutterMethodCall*)call result:(FlutterResult)result { NSString *engineId = (NSString *) call.arguments[0]; ChivoxAIEngine *engine = [self->engineDic objectForKey:engineId]; if (engine == nil) { int eid = -1; id emsg = @"the engineId doesn't represent any engine"; result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); return; }; [engine cancel]; result(nil); return; } /** handle_getDeviceId @param call no args @param result string / FlutterError */ - (void)handle_getDeviceId:(FlutterMethodCall*)call result:(FlutterResult)result { NSString *devid = [ChivoxAIEngine getDeviceId]; if (devid == nil) devid = @""; result(devid); } /** handle_getSerialNumber @param call call.arguments[0]: engineId; unused; call.arguments[1]: input Map; @param result string / FlutterError */ - (void)handle_getSerialNumber:(FlutterMethodCall*)call result:(FlutterResult)result { // unused call.arguments[0] id input = call.arguments[1]; NSDictionary *output = [ChivoxAIEngine getSerialNumber:input]; if (output == nil) { int eid = -1; id emsg = @"the platform interface return null"; result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); return; } NSError *jerr = nil; NSData *data = [NSJSONSerialization dataWithJSONObject:output options:0 error:&jerr]; if (jerr != nil) { int eid = -1; id emsg = @"the platform interface do json serialization failed"; result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); return; } result([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); } /** handle_clearSavedSerialNumber @param call call.arguments[0]: appKey; @param result bool / FlutterError */ - (void)handle_clearSavedSerialNumber:(FlutterMethodCall*)call result:(FlutterResult)result { NSString *appKey = call.arguments[0]; BOOL output = [ChivoxAIEngine clearSavedSerialNumber:appKey]; result(@(output)); } /** handle_getProvision @param call call.arguments[0]: engineId; unused; call.arguments[1]: input Map; @param result string / FlutterError */ - (void)handle_getProvision:(FlutterMethodCall*)call result:(FlutterResult)result { // unused call.arguments[0] id input = call.arguments[1]; NSDictionary *output = [ChivoxAIEngine getProvision:input]; if (output == nil) { int eid = -1; id emsg = @"the platform interface return null"; result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); return; } NSError *jerr = nil; NSData *data = [NSJSONSerialization dataWithJSONObject:output options:0 error:&jerr]; if (jerr != nil) { int eid = -1; id emsg = @"the platform interface do json serialization failed"; result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); return; } result([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); } - (void)handle_toolExtractRes:(FlutterMethodCall*)call result:(FlutterResult)result { NSString *assetPrefix = call.arguments[0]; NSArray *assetNames = call.arguments[1]; NSString *targetRoot = call.arguments[2]; NSString *callbackId = call.arguments[3]; NSString *key = [self->registrar lookupKeyForAsset:assetPrefix]; NSString *prefix = [[NSBundle mainBundle] pathForResource:key ofType:nil]; if (prefix == nil) { int errId = -1; id errMsg = [NSString stringWithFormat:@"extrace res fail: invalid assetPrefix"]; result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", errId] message:errMsg details:nil]); return; } ChivoxAIResTool *tool = [[ChivoxAIResTool alloc] init]; tool.onProgress = ^(float i) { id args = @[ callbackId, [NSNumber numberWithFloat:i], ]; [self->callbackChannel invokeMethod:@"aiengine_callback" arguments:args]; }; id err = [tool extract:prefix assets:[assetNames mutableCopy] target:targetRoot]; if (err != nil) { int errId = -1; id errMsg = [NSString stringWithFormat:@"extrace res fail: %@", err]; result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", errId] message:errMsg details:nil]); return; } result(nil); } - (void)handle_toolLoadNativeCfg:(FlutterMethodCall*)call result:(FlutterResult)result { NSString *resRoot = call.arguments[0]; NSArray *resNames = call.arguments[1]; ChivoxAIResTool *tool = [[ChivoxAIResTool alloc] init]; NSDictionary *dic = [tool loadNativeCfg:resRoot :[resNames mutableCopy]]; if (dic == nil) { result(nil); return; } NSError *jerr = nil; NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:0 error:&jerr]; if (jerr != nil) { int eid = -1; id emsg = @"the platform interface do json serialization failed"; result([FlutterError errorWithCode:[NSString stringWithFormat:@"%d", eid] message:emsg details:nil]); return; } result([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); } @end