react native 集成腾讯语音合成TTS(iOS)

本文档详细介绍了如何在React Native项目中集成iOS版腾讯语音合成(TTS)服务。从下载SDK、集成frameworks到创建原生模块,再到前端使用和解决arm64报错问题,提供了一步一步的指导。适用于希望在iOS应用中实现TTS功能的开发者。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、前言

从我去年集成好安卓的代码,已经过去了大半年了,sdk的版本也从1.5升级到了2.0,近期终于完成了ios的集成,希望可以帮助到大家。本人)Objective C写的不好,代码可能不是那么大的完备,仅作参考学习。

二、下载sdk

需要登陆腾讯云,找到语音技术,下载sdk
文档链接 腾讯tts文档

三、 iOS 原生模块

1. 集成frameworks

从sdk文件夹里面找到QCloudTTS.xcframework,引入到项目中。

2. TTSModule.h

注意1:我这里用了MediaPlayerDemo,也可以使用sdk内置播放器

//
//  TTSModule.h
//
//  Created by cauyyl on 2023/5/24.
//
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
//#import <React/RCTLog.h>

#import "MediaPlayerDemo.h"
//@interface TTSModule : RCTEventEmitter <RCTBridgeModule>
@interface TTSModule : NSObject <RCTBridgeModule>
@property(strong) MediaPlayerDemo *player;
@property (nonatomic)int cout; //统计合成句子数,当作utteranceId用于标记句子用
@property (atomic)NSMutableArray*  textArr;
@end

3.TTSModule.m

注意1:setOnlineAuthParam的参数需要改成你的账户信息
注意2:我简化了很多参数设置,参数都被我写死了。参数的详情可以参考demo里面LongTextViewController.m我的代码也是主要参考的LongTextViewController.m
注意3:我去掉了离线模式

//
//  TTSModule.m
//
//  Created by cauyyl on 2023/5/24.
//

#import "TTSModule.h"
#import <QCloudTTS/QCloudTTSEngine.h>


@implementation TTSModule

RCTResponseSenderBlock ttsCallback;

- (void)dealloc{

    _player = nil;
    [QCloudTTSEngine instanceRelease];

}

RCT_EXPORT_MODULE(TTS);

RCT_EXPORT_METHOD(pause){
  if(_player){
    [_player PausePlay];
  }
}

RCT_EXPORT_METHOD(destroy){
  [_player StopPlay];
  [_textArr removeAllObjects];
  QCloudTTSEngine *tts = [QCloudTTSEngine getShareInstance];
  [tts cancel];
  _player = nil;
  [QCloudTTSEngine instanceRelease];
}

-(void) stop{
  [_player StopPlay];
  [_textArr removeAllObjects];
  QCloudTTSEngine *tts = [QCloudTTSEngine getShareInstance];
  [tts cancel];
}

RCT_EXPORT_METHOD(playLongText: (NSString *)title content:(NSString *)content forceStart:(BOOL *)forceStart callback:(RCTResponseSenderBlock)callback) {
  ttsCallback=callback;
  NSLog(@"showWithMessage");
  NSLog(@"title=%@",title);
  NSLog(@"content=%@",content);
//  if(forceStart){
//    NSLog(@"forceStart");
//  }
//
  QCloudTTSEngine *tts = [QCloudTTSEngine getShareInstance];
  if(_player){
    if(forceStart){
      NSLog(@"forceStart");
      
      _cout=0;
      [self stop];
        _textArr = [self breakIntoSentencesFromString:content];
//      NSLog(@"_textArr38=%@",_textArr);
      NSLog(@"_cout63=%d",_cout);
        for (int i = 0; i < 10 - [_player getAudioQueueSize]; i++) {
            if (_textArr.count > 0) {
                [tts synthesize:_textArr[0] UtteranceId:[NSString stringWithFormat:@"%d",_cout++]];
                [_textArr removeObjectAtIndex:0];
            }
        }
      }else {
        [_player ResumePlay];
        ttsCallback(@[@(1)]);
//        [self sendEventWithName:@"ttsEvents" body:@{@"code":@"1",}];
      }
    
    
  }else{
    _cout = 0;
    _textArr = [NSMutableArray array];
     //如果使用STS临时证书鉴权时需要设置Token
    NSLog(@"鉴权");
    [tts setOnlineAuthParam:123 SecretId:@"AK****Hp7" SecretKey:@"D9***c2" Token:nil];

    [tts engineInit:0 Delegate:self];
//    [tts setEnableDebugLog:YES];

    //SDK 内置播放器
//    _player = [[QCloudMediaPlayer alloc]init];
    // demo 层播放器,如SDK播放器不能满足需求 可以自定义播放器 如下只是一个例子 源码在MediaPlayerDemo.m文件 可自行修改
    _player = [[MediaPlayerDemo alloc]init];
    _player.playerDelegate = self;
    
    [self setOnlineParam:tts];
    
    _textArr = [self breakIntoSentencesFromString:content];
//    NSLog(@"_textArr68=%@",_textArr);
    for (int i = 0; i < 10 - [_player getAudioQueueSize]; i++) {
        if (_textArr.count > 0) {
            [tts synthesize:_textArr[0] UtteranceId:[NSString stringWithFormat:@"%d",_cout++]];
            [_textArr removeObjectAtIndex:0];
        }
    }

  }
 

 }


-(void)setOnlineParam:(QCloudTTSEngine*)ttsEngine{


    //设置合成声音速度
    [ttsEngine setOnlineVoiceSpeed:0];
    //设置合成声音类型
    [ttsEngine setOnlineVoiceType:101001];
    //设置合成语种
    [ttsEngine setOnlineVoiceLanguage:1];
    //设置合成声音大小默认值为0
    [ttsEngine setOnlineVoiceVolume:0];
    //设置响应超时时间
    [ttsEngine setTimeoutIntervalForResource :15*1000];
    //设置请求超时时间
    [ttsEngine setTimeoutIntervalForRequest: 30 * 1000];
    //设置检测网络间隔时间
    [ttsEngine setCheckNetworkIntervalTime:5*60];

}


//合成结果返回
//---------QCloudTTSEngineDelegate---------
/// 合成回调
/// @param data 语音数据
/// @param utteranceId 语句id
/// @param text 文本
/// @param type 引擎类型 0:在线 1:离线
///
-(void) onSynthesizeData:(NSData *_Nullable)data UtteranceId:(NSString *_Nullable)utteranceId Text:(NSString *_Nullable)text EngineType:(NSInteger)type RequestId:(NSString*)requestId{

    NSLog(@"text====%@,utteranceId ==%@,type==%ld,requestId=%@",text,utteranceId,(long)type,requestId);
    [_player enqueueWithData:data Text:text UtteranceId:utteranceId];

    //通过保存音频的URL播放
    //NSString *str = [self filePathWithName:@"tmp.mp3"];
    //NSURL * url = [NSURL URLWithString:str];
    //[_player enqueueWithFile:url Text:text UtteranceId:utteranceId];


    NSLog(@"onSynthesizeData %@",@(data.length));

}

/// 错误回调
/// @param error 错误信息
/// @param utteranceId utteranceId
/// @param text text
-(void) onError:(TtsError *_Nullable)error UtteranceId:(NSString *_Nullable)utteranceId Text:(NSString *_Nullable)text{




    NSLog(@"onError 273");


}



//---------QCloudPlayerDelegate---------
//播放开始
-(void) onTTSPlayStart{

    NSLog(@"onTTSPlayStart 开始播放");
}

//队列所有音频播放完成,音频等待中
-(void) onTTSPlayWait{
//    [self sendEventWithName:@"ttsEvents" body:@{@"code":@"3",}];
    ttsCallback(@[@(3)]);
    NSLog(@"onTTSPlayWait 播放完成,等待音频数据");
}

//恢复播放
-(void) onTTSPlayResume{

    NSLog(@"onTTSPlayResume");
}

//暂停播放
-(void) onTTSPlayPause{

    NSLog(@"onTTSPlayPause");
}

//播放中止
-(void)onTTSPlayStop{
  _cout = 0;
  
    NSLog(@"onTTSPlayStop");

}


//播放器异常
-(void)onTTSPlayError:(QCPlayerError* _Nullable)playError{

    NSLog(@"playError.code==%@,playError.massage==%@",@(playError.mCode),playError.message);

}

//即将播放播放下一句,即将播放音频对应的句子,以及这句话utteranceId
/// 即将播放播放下一句,即将播放音频对应的句子,以及这句话utteranceId
/// @param text 当前播放句子的文本
/// @param utteranceId 当前播放音频对应的ID
-(void) onTTSPlayNextWithText:(NSString* _Nullable)text UtteranceId:(NSString* _Nullable)utteranceId{


    NSLog(@"text==%@,utteranceId==%@",text,utteranceId);
    QCloudTTSEngine *tts = [QCloudTTSEngine getShareInstance];
  
    if ([_player getAudioQueueSize] < 10) {
      
        if (_textArr.count > 0) {
            NSLog(@"AudioQueueSize=%@",@([_player getAudioQueueSize]));
            [tts synthesize:_textArr[0] UtteranceId:[NSString stringWithFormat:@"%d",_cout++]];
            [_textArr removeObjectAtIndex:0];
        }
    }
}

/// 当前播放的字符,当前播放的字符在所在的句子中的下标.
/// @param currentWord 当前读到的单个字,是一个估算值不一定准确
/// @param currentIdex 当前播放句子中读到文字的下标
-(void)onTTSPlayProgressWithCurrentWord:(NSString*_Nullable)currentWord CurrentIndex:(NSInteger)currentIdex{

    NSLog(@"CurrentWord==%@,currentIdex==%@",currentWord,@(currentIdex));

}



/*===============================使用自定义分割逻辑,以下为句子分割示例=====================*/
#define Sentence_Count_Max 50   //单次最大请求字数,建议首句不要设置太长
/*
 API最大请求字数详见官网文档https://cloud.tencent.com/document/product/1073/37995
 
 */

- (NSMutableArray *)breakIntoSentencesFromString:(NSString *)string
{
    NSMutableArray *stringsArr = [NSMutableArray array];
    
    NSString *splitString = @",;,;";
    //先按句子分割
    [string enumerateSubstringsInRange:NSMakeRange(0, string.length) options:NSStringEnumerationBySentences usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) {
        //后台接收字符不能太长
        const char *commitChar = [substring UTF8String];
        if (strlen(commitChar) > Sentence_Count_Max * 3) { //UTF-8中文的字符长度一般是3个字节,英文的长度为1
            //再按分号逗号分割
            NSArray *textArr = [self separatedString:substring split:splitString];
            for (NSString *object in textArr) {
                //再按字数分割
                const char *objectChar = [object UTF8String];
                if (strlen(objectChar) > Sentence_Count_Max * 3) {
                    NSString *remainStr = object;
                    while (remainStr.length > Sentence_Count_Max) {
                        NSString *sentence = [remainStr substringToIndex:Sentence_Count_Max];
                        if (![self isAllSplitString:sentence split:splitString]
                            &&![self isPunct:sentence]) {
                            [stringsArr addObject:sentence];
                        }
                        remainStr = [remainStr substringFromIndex:Sentence_Count_Max];
                    }
                    if (![self isAllSplitString:remainStr split:splitString]
                        &&![self isPunct:remainStr]) {
                        [stringsArr addObject:remainStr];
                    }
                }
                else {
                    if (![self isAllSplitString:object split:splitString]
                        &&![self isPunct:object]) {
                        [stringsArr addObject:object];
                    }
                }
            }
        }else{
            if (![self isAllSplitString:substring split:splitString]
                &&![self isPunct:substring]) {
                [stringsArr addObject:substring];
            }
        }
    }];
//  NSLog(@"stringsArr=%@",stringsArr);
    return stringsArr;
}


- (NSArray *)separatedString:(NSString *)text split:(NSString *)split {
    NSMutableArray *mArray = [NSMutableArray array];
    NSInteger loc = 0;
    for (NSInteger i = 0; i < [text length]; i++) {
        if ([split rangeOfString:[text substringWithRange:NSMakeRange(i, 1)]].location != NSNotFound) {
            NSString *subString = [text substringWithRange:NSMakeRange(loc, i-loc+1)];
            [mArray addObject:subString];
            loc = i + 1;
        }
        if (i + 1 == [text length] && [split rangeOfString:[text substringWithRange:NSMakeRange(i, 1)]].location == NSNotFound) { //判断最后一个字符是否包含分隔符,如果不包含把最后一句添加到数组
            NSString *subString = [text substringWithRange:NSMakeRange(loc, i-loc+1)];
            [mArray addObject:subString];
        }
    }
    return [mArray copy];
}
- (BOOL)isAllSplitString:(NSString *)text split:(NSString *)split
{
    split = @",;,;。";
    if ([text length] && [split length]) {
        BOOL all = YES;
        for (NSInteger i = 0; i < [text length]; i++) {
            if ([split rangeOfString:[text substringWithRange:NSMakeRange(i, 1)]].location == NSNotFound) {
                all = NO;
                break;
            }
        }
        return all;
    }
    else {
        return NO;
    }
}


-(BOOL)isPunct:(NSString *)string //校验是否全为符号
{
    NSError *error = nil;
    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[\\p{P}~^<>]" options:NSRegularExpressionCaseInsensitive error:&error];

    NSString *modifiedString = [regex stringByReplacingMatchesInString:string options:0 range:NSMakeRange(0, [string length]) withTemplate:@""];
    
    NSCharacterSet *set = [NSCharacterSet whitespaceAndNewlineCharacterSet];
    NSString *trimmedStr = [modifiedString stringByTrimmingCharactersInSet:set];
    if (!trimmedStr.length) {
        return YES;
    }
    return NO;
}



/*=======================================使用自定义分割逻辑====================================================*/

/*
#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.
}
*/

@end


四、前端使用

NativeModules.TTS.playLongText(
   '测试文章',
     '三月的春风是彩色的,染红了迎春花,描青了山峰,绘绿了秧苗,吹红了我们胸前的红领巾,同时也掀起了我们学习雷锋的热潮。说起雷锋,我们并不陌生,自然会想起一段话:“如果你是一滴水,你是否滋润了一方土地?如果你是一缕阳光,你是否照亮了一分黑暗?如果你是一粒粮食,你是否哺育了生命?如果你是一颗最小的螺丝钉,你是否坚守着你生活的岗位?”。。。。。。',
     false,
     () => {
         navigation.navigate('BishenHuiYuan', {
             source: 'correct-tts',
             courseType: 'correct-tts',
         })
     },
 )

五、调试和报错

1. arm64报错

  1. 在built setting里找到 Architectures
  2. 在Excluded Architectures添加arm64
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值