iOS SDK

更新时间:
复制 MD 格式

使用 iOS NUI SDK 将文本合成为语音,并通过回调获取音频数据。本文介绍 SDK 集成、调用步骤、代码示例、关键接口和常见问题。

前提条件

  • 已开通智能语音交互服务,并获取项目 Appkey。项目配置请参见管理项目。

  • 已获取 NLS Token,获取方法请参见获取 Token。

重要

AccessKey ID 和 AccessKey Secret 应保存在服务端,不得写入 App 代码或存储在移动端。云端语音合成使用 Appkey 和 NLS Token 鉴权;由服务端获取 Token 后下发给 App,并在 Token 过期前更新。

语音合成参数和支持的音色请参见移动端语音合成接口说明。

下载和集成 SDK

从移动端 SDK 选择与下载下载支持语音合成的 iOS NUI SDK 包。

以下集成说明和示例使用 V2.7.4-01B-20260904_iOS_TLS 包。2.5.14 版本后的 iOS 接口使用 Objective-C,不再需要通过 C++ 混合接口调用。

  1. 解压 SDK 包,选择与运行平台匹配的产物。

    产物

    用途

    Release/nuisdk.xcframework

    包含 iOS arm64 和 iOS Simulator arm64 分支,可用于真机和 Apple Silicon Mac 模拟器。

    Release/nuisdk.framework

    包含真机 arm64 和 Intel Mac 模拟器 x86_64。用于真机发布时,需要移除模拟器架构。

    Release-iphoneos/nuisdk.xcframework

    仅包含真机 arm64。

  2. 将选定的 Framework 或 XCFramework 添加到 Xcode 工程。在 Target 的 Build Phases > Link Binary With Libraries 中添加该库,并在 General > Frameworks, Libraries, and Embedded Content 中设置为 Embed & Sign。

  3. 导入语音合成头文件:

        #import <nuisdk/NeoNuiTts.h>

SDK 包内的 Demo 工程提供播放器和文件操作工具类。语音合成示例为 TTSViewController,播放器为 AudioController;替换 Appkey 和 Token 后可运行示例。当前纯云端合成不需要设置资源目录 workspace。

调用步骤

  1. 创建 SDK 和播放器实例,设置代理并初始化 SDK。

  2. 调用 nui_tts_set_param 设置音色、音频格式、采样率等参数。

  3. 调用 nui_tts_play 启动语音合成。

  4. 在 onNuiTtsUserdataCallback 中获取音频数据,写入播放器进行流式播放,或依次追加保存到同一个文件。

  5. 收到 TTS_EVENT_END 后,等待播放器播放完缓冲区中的数据。该事件表示合成结束,不表示播放结束。

  6. 不再使用 SDK 时,调用 nui_tts_release 释放资源。

代码示例

以下示例使用 Demo 中的 AudioController.h、AudioController.m 及其依赖 NLSRingBuffer.h、NLSRingBuffer.mm,播放格式为 16 kHz、16-bit、单声道 PCM。将这些辅助类加入工程,并链接 AudioToolbox.framework、AVFoundation.framework 和 libc++.tbd。

SDK 可通过 [NeoNuiTts get_instance] 获取默认单例,也可通过 [[NeoNuiTts alloc] init] 创建实例。初始化可能阻塞,应在非 UI 线程执行;同一实例的控制接口按顺序调用,建议同一时间只运行一个合成任务。

配置网关

NSString *gateway = @"wss://nls-gateway.cn-shanghai.aliyuncs.com/ws/v1";

初始化、合成和播放

#import <Foundation/Foundation.h>
#import <nuisdk/NeoNuiTts.h>
#import "AudioController.h"

@interface SpeechSynthesizer : NSObject <NeoNuiTtsDelegate, ConvVoiceRecorderDelegate>
@property(nonatomic, strong) NeoNuiTts *nui;
@property(nonatomic, strong) AudioController *player;
@property(atomic) BOOL finished;
@property(nonatomic, copy) void (^onFinished)(void);
- (int)configureWithAppkey:(NSString *)appkey token:(NSString *)token
                  gateway:(NSString *)gateway;
- (int)startWithText:(NSString *)text;
- (int)cancel;
- (int)releaseSdk;
@end

@implementation SpeechSynthesizer
- (int)configureWithAppkey:(NSString *)appkey token:(NSString *)token
                  gateway:(NSString *)gateway {
    self.finished = YES;
    self.nui = [[NeoNuiTts alloc] init];
    self.nui.delegate = self;
    self.player = [[AudioController alloc] init:only_player];
    self.player.delegate = self;
    [self.player setPlayerSampleRate:16000];

    NSDictionary *params = @{
        @"app_key": appkey,
        @"token": token,
        @"url": gateway,
        @"mode_type": @"2",
        @"device_id": [[NSUUID UUID] UUIDString]
    };
    NSData *data = [NSJSONSerialization dataWithJSONObject:params options:0 error:nil];
    NSString *json = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    int code = [self.nui nui_tts_initialize:json.UTF8String
                                  logLevel:NUI_LOG_LEVEL_NONE saveLog:NO];
    if (code != 0) return code;
    code = [self.nui nui_tts_set_param:"font_name" value:"xiaoyun"];
    if (code != 0) return code;
    code = [self.nui nui_tts_set_param:"encode_type" value:"pcm"];
    if (code != 0) return code;
    return [self.nui nui_tts_set_param:"sample_rate" value:"16000"];
}

- (int)startWithText:(NSString *)text {
    self.finished = NO;
    int code = [self.nui nui_tts_play:"1" taskId:"" text:text.UTF8String];
    if (code != 0) self.finished = YES;
    return code;
}

- (int)cancel {
    return [self.nui nui_tts_cancel:NULL];
}

- (int)releaseSdk {
    [self.player stopPlayer];
    int code = [self.nui nui_tts_release];
    self.nui.delegate = nil;
    self.player.delegate = nil;
    return code;
}

- (void)notifyFinished {
    @synchronized(self) {
        if (self.finished) return;
        self.finished = YES;
    }
    if (self.onFinished) self.onFinished();
}

- (void)onNuiTtsEventCallback:(NuiSdkTtsEvent)event taskId:(char *)taskid code:(int)code {
    if (event == TTS_EVENT_START) {
        [self.player startPlayer];
    } else if (event == TTS_EVENT_END) {
        [self.player drain];
    } else if (event == TTS_EVENT_CANCEL || event == TTS_EVENT_ERROR) {
        [self.player stopPlayer];
        if (event == TTS_EVENT_ERROR) {
            const char *message = [self.nui nui_tts_get_param:"error_msg"];
            NSLog(@"TTS error: %d, %s", code, message ?: "");
        }
        [self notifyFinished];
    }
}

- (void)onNuiTtsUserdataCallback:(char *)info infoLen:(int)infoLength
                        buffer:(char *)buffer len:(int)length taskId:(char *)taskid {
    if (length > 0) [self.player write:buffer Length:length];
}

- (void)playerDrainDataFinish {
    [self notifyFinished];
}
- (void)playerDidStart {}
- (void)playerDidFinish {}
- (void)playSoundLevel:(int)level {}
- (void)playData:(unsigned char *)buffer Length:(int)length {}
- (void)recorderDidStart {}
- (void)recorderDidStop {}
- (void)voiceRecorded:(unsigned char *)buffer Length:(int)length {}
- (void)voiceDidFail:(NSError *)error {}
@end

在业务对象中保留 SpeechSynthesizer 实例,并通过串行队列调用控制接口。将 appkey 和 token 替换为服务端下发的值,gateway 使用前面的网关配置。

// Declare these properties in the business object.
@property(nonatomic, strong) SpeechSynthesizer *synthesizer;
@property(nonatomic, strong) dispatch_queue_t ttsQueue;

// Start synthesis in a business method.
self.ttsQueue = dispatch_queue_create("tts.control", DISPATCH_QUEUE_SERIAL);
self.synthesizer = [SpeechSynthesizer new];
SpeechSynthesizer *synthesizer = self.synthesizer;
dispatch_queue_t queue = self.ttsQueue;
__weak typeof(self) weakSelf = self;
synthesizer.onFinished = ^{
    dispatch_async(queue, ^{
        int code = [synthesizer releaseSdk];
        if (code != 0) NSLog(@"Release failed: %d", code);
        synthesizer.onFinished = nil;
        dispatch_async(dispatch_get_main_queue(), ^{
            weakSelf.synthesizer = nil;
        });
    });
};
dispatch_async(queue, ^{
    int code = [synthesizer configureWithAppkey:appkey token:token gateway:gateway];
    if (code == 0) code = [synthesizer startWithText:@"Hello, welcome to the speech synthesis example."];
    if (code != 0) {
        NSLog(@"Start failed: %d", code);
        [synthesizer releaseSdk];
        synthesizer.onFinished = nil;
        dispatch_async(dispatch_get_main_queue(), ^{
            weakSelf.synthesizer = nil;
        });
    }
});

需要提前取消时,在同一串行队列中调用 cancel;该方法传入 NULL,取消实例中的所有合成任务。将资源释放异步安排到控制队列,不在 SDK 事件回调内直接执行。

关键接口与回调

除 nui_tts_get_param 返回参数值外,下列接口均返回整数状态码,0 表示调用成功。异步合成错误通过 TTS_EVENT_ERROR 通知。

错误码及处理方法请参见错误码查询。

控制接口

初始化和参数配置

nui_tts_initialize:初始化 SDK。再次初始化前,应先释放实例中的 SDK 资源。该调用可能阻塞,应在非 UI 线程执行。

- (int)nui_tts_initialize:(const char *)parameters
                 logLevel:(NuiSdkLogLevel)level
                  saveLog:(BOOL)save_log;
  • parameters:JSON 字符串,包含 app_key、token、url、mode_type 和 device_id 等初始化参数。纯在线合成必须将 mode_type 设置为 "2",并设置必填的 device_id;推荐使用具有唯一性的设备标识,便于定位问题。

  • level:日志级别,枚举值越小,日志越详细。例如 NUI_LOG_LEVEL_VERBOSE 或 NUI_LOG_LEVEL_NONE。

  • save_log:是否保存日志文件。启用时通过初始化参数 debug_path 设置目录,并关注日志的存储占用。当前 Demo 可通过 max_log_file_size 配置日志文件大小。

nui_tts_set_param:以键值对设置合成参数。

- (int)nui_tts_set_param:(const char *)param value:(const char *)value;

param 为参数名,value 为参数值。可设置音色、语调、语速等合成参数。例如 font_name 设置音色,encode_type 设置音频格式。

合成任务控制

nui_tts_play:启动合成。合成音频通过数据回调交给 App,由 App 播放或保存。

- (int)nui_tts_play:(const char *)priority
             taskId:(const char *)taskid
              text:(const char *)text;
  • priority:任务优先级,使用 "1"。

  • taskid:任务 ID,可传入 32 字节的 UUID,或传入空字符串由 SDK 自动生成。

  • text:待合成的文本。

nui_tts_cancel:取消合成。taskid 为待取消的任务 ID,传入 NULL 取消所有任务。

- (int)nui_tts_cancel:(const char *)taskid;

nui_tts_pause / nui_tts_resume:暂停或恢复合成任务。播放器中的缓冲音频由 App 的播放器控制。

- (int)nui_tts_pause;
- (int)nui_tts_resume;

参数查询和资源释放

nui_tts_get_param:查询参数。param 为参数名,返回值为参数值。例如查询 error_msg 获取错误信息。

- (const char *)nui_tts_get_param:(const char *)param;

nui_tts_release:释放 SDK 资源。

- (int)nui_tts_release;

音频数据回调

NeoNuiTtsDelegate 的 onNuiTtsUserdataCallback 向 App 输出合成音频和可用的时间戳信息。

- (void)onNuiTtsUserdataCallback:(char *)info infoLen:(int)info_len
                        buffer:(char *)buffer len:(int)len taskId:(char *)task_id;
  • info:启用时间戳且音色支持该功能时,返回 JSON 格式的时间戳信息。

  • info_len:info 的数据长度。

  • buffer:合成音频数据。

  • len:音频数据长度,单位为字节。

  • task_id:当前合成任务的 ID。

事件回调

onNuiTtsEventCallback 通知合成状态,App 可根据事件控制播放器。

- (void)onNuiTtsEventCallback:(NuiSdkTtsEvent)event taskId:(char *)taskid code:(int)code;

event 为事件类型,taskid 为当前合成任务 ID;code 为错误码,仅在 TTS_EVENT_ERROR 时有效。

事件

说明

TTS_EVENT_START

语音合成开始,准备播放。

TTS_EVENT_END

语音合成结束,音频已全部通过回调输出,但播放器可能仍有未播放的缓冲数据。

TTS_EVENT_CANCEL

合成任务已取消。

TTS_EVENT_PAUSE

合成任务已暂停。

TTS_EVENT_RESUME

合成任务已恢复。

TTS_EVENT_ERROR

合成发生错误。

常见问题

保存的音频播放时有杂音,如何处理?

检查文件的实际编码格式与播放器支持的格式是否一致。例如,MP3 数据应使用支持 MP3 的播放器播放。音频尾部有杂音时,检查保存的文件是否混入日志文本等非音频数据。

高频触发播放时出现页面终止,如何处理?

在线合成需要连接网络,网络状况会影响接口响应时间。业务需要快速停止当前任务并开始下一条时,可根据网络状况和业务需求调整网络超时时间。

如何将合成音频保存为文件?

通过 nui_tts_set_param 设置 encode_type,可选择 PCM、WAV 或 MP3。在 onNuiTtsUserdataCallback 中将每次回调的音频数据按顺序追加写入同一文件,收到 TTS_EVENT_END 后关闭文件。

本页示例播放器用于 PCM 流式播放;保存的 MP3 文件可使用支持 MP3 的软件播放。保存后出现少字时,检查每次回调的数据是否完整写入,尤其是音频数据产生较快时。使用 fwrite 保存时,可在写入后调用 fflush 刷新文件缓冲区,并在合成结束后调用 fclose 关闭文件。

如何获取时间戳信息?

默认不返回时间戳。选择支持时间戳的音色,并在启动合成前调用 nui_tts_set_param:"enable_subtitle" value:"1",然后在 onNuiTtsUserdataCallback 的 info 中获取结果。

参数说明请参见移动端语音合成接口说明。

移动端 SDK 能否用于专有云?

移动端 SDK 可调用公共云 ASR、TTS 服务,也可用于专有云环境。专有云安装包默认不提供移动端 SDK,可从帮助中心对应服务文档下载,例如实时语音识别的Android SDK和iOS SDK。

是否支持后台播放?

SDK 本身不限制前后台,App 需要配置 iOS 后台音频能力。在 Info.plist 中添加 Required background modes,并添加 App plays audio or streams audio/video using AirPlay,对应 UIBackgroundModes 的 audio 值。播放器还需要配置并激活音频会话。

真机运行提示 no suitable image found,如何处理?

确认所集成的库包含 iOS 真机架构,并检查 Embed & Sign 和 App 签名配置。修正后清理构建产物,删除设备上的旧 App,再重新安装。

导入语音合成头文件失败,如何处理?

确认 SDK 已加入工程的 Link Binary With Libraries,并设置 Embed & Sign。语音合成使用 #import <nuisdk/NeoNuiTts.h>;使用 Demo 播放器时,还需加入辅助类及其依赖库。

旧版 Framework 提示同时包含 iOS 和 iOS Simulator,如何处理?

优先选择与运行平台匹配的 XCFramework 分支。旧版 Xcode 集成混合平台 Framework 时,可在 Build Settings 中切换为 All > Combined,找到 Build Options > Validate Workspace 并设置为 Yes,然后重新编译。

旧工程需要切换 Legacy Build System 才能运行

旧版 Xcode 工程出现此类构建问题时,可检查 Validate Workspace 并将其设置为 Yes 后重新编译。

Flutter 工程出现 std::__1::mutex 等未定义符号,如何处理?

在 iOS Target 的 Build Phases > Link Binary With Libraries 中添加 libc++.tbd,链接 C++ 运行库。报错涉及插件库时,也应检查对应插件 Target 的链接配置。

App Store 提示 Unsupported Architectures,如何处理?

真机发布包中不应包含模拟器架构。使用 XCFramework 时选择 iOS 真机分支;旧版混合 Framework 可用 lipo -info nuisdk.framework/nuisdk 检查实际二进制,移除其中的模拟器架构后重新签名、打包。