杰理 SDK 文档中心
首页
首页
  • 概述与快速开始

    • 仓库概览
    • 运行环境与 SDK 集成
    • 工程结构与目录导航
  • 核心 SDK 架构

    • SDK 库体系与模块划分
    • 蓝牙连接与 RCSP 协议
    • 广播包解析与设备认证
    • 日志助手与调试支持
  • 设备功能模块

    • OTA 固件升级
    • 表盘管理与自定义表盘
    • 图像转换工具
    • 资源打包
    • 音频编解码
    • 健康与运动数据同步
    • 消息通知与实用设备功能
  • 宜动健康示例应用

    • 应用架构与页面导航
    • 健康界面与数据可视化
    • 设备连接与数据同步
    • 登录注册与用户中心
    • AI 云服务与语音交互
    • 本地数据库与持久化
    • 多语言国际化
  • 测试与调试

    • SDKTestHelper 功能测试工具
    • 音频编解码示例工程
    • 调试技巧与问题排查
  • 文档与资源

    • 在线文档与版本历史
    • 第三方框架与依赖管理

音频编解码

杰理健康(JieliJianKang)iOS 应用中音频编解码能力的封装层,以 JLAudioToolBox 单例为核心,基于系统 AVAudioPlayer / AudioToolbox 框架完成音频文件的解码与播放,并提供解码错误回调等生命周期管理。

Purpose and Scope

本页介绍杰理健康 iOS 应用中的音频编解码能力,涵盖:

  • JLAudioToolBox 单例的职责与设计(封装系统解码器)
  • 音频解码播放的完整控制流(文件 URL → AVAudioPlayer → 系统编解码器 → 硬件输出)
  • 解码错误、播放完成等生命周期回调的语义
  • 同目录下 Swift 封装层 AudioManager.swift 的定位(该文件为 Swift 侧入口,本页仅作边界说明)

本页不涉及以下属于其他目录页的主题:

  • 表盘市场(DialMarket)相关的下载与支付流程,请参见「表盘市场」相关页面
  • 蓝牙设备连接与数据传输,请参见「设备连接」相关页面
  • HTTP 客户端与产品信息模型,请参见「HTTP 客户端」相关页面

说明:仓库中不包含自定义的软件编解码器实现(如 FFmpeg、自研 DSP 编解码算法)。本应用的"编解码"能力完全委托给 iOS 系统框架——AVAudioPlayer 内部调用系统解码器(AAC、MP3、WAV、ALAC、IMA4 等格式)。本文档如实反映这一架构现实,并重点阐述封装层自身的职责与行为。

Overview

在杰理健康 App 中,音频播放被用于提示音、交互反馈等场景。JLAudioToolbox 目录位于 Utilities 之下,提供两个文件:

文件语言职责
JLAudioToolBox.h / .mObjective-C核心播放器封装:单例 + AVAudioPlayer 生命周期管理
AudioManager.swiftSwiftSwift 侧的音频管理入口(同一工具箱目录内,具体 API 以源码为准)

设计上,JLAudioToolBox 采用单例模式(sharedInstance),原因在于:

  1. AVAudioPlayer 实例持有解码后的音频缓冲区,复用同一个播放器对象可避免频繁创建/销毁的系统开销;
  2. 全局只需一个音频输出会话,单例天然避免了多播放器并发抢占音频硬件的冲突;
  3. 集中管理 delegate 回调(播放完成、解码错误),便于统一处理音频会话中断等系统事件。

类声明中通过 NS_UNAVAILABLE 禁用了 init 与 new,强制调用方走单例入口,这是 iOS 单例类的标准防御性写法。

Architecture

flowchart TD
    subgraph sg_App["应用层 (JieliJianKang)"]
        VC["ViewControllers / 业务代码"]
    end

    subgraph sg_Toolbox["Utilities/JLAudioToolbox"]
        AM["AudioManager.swift (Swift 入口)"]
        JLB["JLAudioToolBox (单例, Objective-C)"]
    end

    subgraph sg_System["系统框架层"]
        AVP["AVAudioPlayer"]
        AT["AudioToolbox / AudioServices"]
        CODEC["系统音频编解码器<br/>(AAC/MP3/WAV/ALAC...)"]
        SESSION["Audio Session (硬件输出)"]
    end

    VC -->|"调用"| AM
    VC -->|"playAudioWithFileUrlString:"| JLB
    AM --> JLB
    JLB -->|"initWithContentsOfURL"| AVP
    JLB -.->|"备选方案(已注释)"| AT
    AVP -->|"内部解码"| CODEC
    CODEC -->|"PCM 输出"| SESSION
    AVP -->|"delegate 回调"| JLB

架构说明:

  • JLAudioToolBox(单例) 是本页的核心组件。它持有唯一的 AVAudioPlayer 实例(@property (nonatomic, strong)),对外只暴露一个播放接口 playAudioWithFileUrlString:。
  • AVAudioPlayer 是系统级播放器,负责读取音频文件、调用系统编解码器解码为 PCM、并驱动音频会话输出。解码细节(格式协商、采样率转换、缓冲调度)完全由系统接管,应用层无需关心。
  • AudioToolbox / AudioServices 在实现文件中作为被注释掉的备选方案存在(AudioServicesCreateSystemSoundID + AudioServicesPlaySystemSound),用于"短提示音"场景(SystemSound),但当前代码已弃用该路径,仅保留注释作为历史参考。
  • Delegate 回调 将播放完成与解码错误事件返回给 JLAudioToolBox,形成完整的生命周期闭环。

核心实现解析

单例的创建与防御性初始化

JLAudioToolBox 使用经典的 dispatch_once 单例模式,确保整个应用生命周期内只存在一个实例:

Source: JLAudioToolBox.m

+ (instancetype)sharedInstance {
    static JLAudioToolBox *_sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedInstance = [[self alloc] init];
    });
    return _sharedInstance;
}

同时,头文件通过 NS_UNAVAILABLE 显式禁止外部直接 init / new:

Source: JLAudioToolBox.h

+ (instancetype)sharedInstance;
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;

设计意图:音频硬件(扬声器/听筒)是互斥资源。单例 + 禁用的初始化方法从语言层面强制"全局唯一播放器",避免业务方各自创建播放器导致的声音叠加、会话抢占等难以排查的问题。

播放接口:从 URL 到出声

playAudioWithFileUrlString: 是唯一的对外播放入口,完整流程如下:

Source: JLAudioToolBox.m

- (void)playAudioWithFileUrlString:(NSString *)fileUrlString {
    
    NSURL *audioPathUrl = [[NSURL alloc] initFileURLWithPath:fileUrlString];
    NSError *err;
    _player = [[AVAudioPlayer alloc] initWithContentsOfURL:audioPathUrl error:&err];
    _player.volume = 1.0f;
    _player.delegate = self;
    _player.rate = 1.0;
    [_player prepareToPlay];
    [_player play];
    
//    NSURL *audioPath = [[NSURL alloc] initFileURLWithPath:fileUrlString];
//    //定义SystemSoundID,后面需要在completion中精确控制控制可交由外部进行定义
//    SystemSoundID soundId;
//    //注册服务
//    AudioServicesCreateSystemSoundID((__bridge CFURLRef)audioPath, &soundId);
//    //增添回调方法
//    AudioServicesAddSystemSoundCompletion(soundId, (__bridge CFRunLoopRef _Nullable)([NSRunLoop currentRunLoop]), (CFStringRef)@"NSRunLoopCommonModes", NULL, NULL);
//    //开始播放
//    AudioServicesPlayAlertSound(soundId);
//    AudioServicesPlaySystemSound(soundId);
}

逐步分析:

  1. 构造 NSURL:initFileURLWithPath: 将本地文件路径转换为 file:// URL(区别于 URLWithString:,不会对路径做网络 URL 语义解析)。
  2. 创建播放器:initWithContentsOfURL:error: 同步加载音频文件头部并初始化解码器。此时 AVAudioPlayer 已确认文件格式可解码;若文件损坏或格式不支持,err 会被填充(但当前实现未检查该错误,属于一个已知薄弱点,见「失败模式」一节)。
  3. 参数配置:volume = 1.0f 全音量;delegate = self 挂接生命周期回调;rate = 1.0 指定正常播放速率(注意:rate 仅在 enableRate 为 YES 时生效,此处未显式开启,实际取默认行为)。
  4. prepareToPlay:预解码并分配音频缓冲区,减少 play 时的启动延迟(首帧解码耗时被提前)。
  5. play:异步启动解码与播放,立即返回,不阻塞调用线程。

被注释的备选方案:AudioServices 提示音

playAudioWithFileUrlString: 的尾部保留了一段被注释的 AudioServices 实现。它使用 AudioServicesCreateSystemSoundID 注册 SystemSound、AudioServicesPlayAlertSound / AudioServicesPlaySystemSound 播放。该路径适合极短提示音(如按键音、通知音),延迟极低且不需要完整播放器状态机,但功能受限(无法控制音量/速率、无精细的生命周期回调),因此当前版本弃用,仅作为历史参考保留在源码中。

生命周期回调:播放完成与解码错误

JLAudioToolBox 遵循 AVAudioPlayerDelegate 协议,实现两个回调:

Source: JLAudioToolBox.m

#pragma mark - AVAudioPlayerDelegate

/**
 完成播放, 但是在打断播放和暂停、停止不会调用
 */
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
    
}

/**
 播放过程中解码错误时会调用
 */
- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError * __nullable)error {
    
}

语义要点(来自源码注释与 Apple 文档行为):

  • audioPlayerDidFinishPlaying:successfully: 仅在自然播放完毕时回调;被电话打断、pause、stop 均不会触发。flag 表示是否完整播完。
  • audioPlayerDecodeErrorDidOccur:error: 在播放过程中发生解码错误时回调(例如音频数据流中途损坏、格式中途不合法)。它区别于 initWithContentsOfURL:error: 的初始化期错误——后者是加载失败,前者是播放期解码失败。
  • 当前两个回调方法体为空,说明业务层尚未消费这些事件;这为后续扩展(如播放完成通知、错误上报)预留了明确的挂接点。

核心控制流

sequenceDiagram
    participant VC as 业务代码 (ViewControllers)
    participant JLB as JLAudioToolBox (单例)
    participant AVP as AVAudioPlayer
    participant CODEC as 系统音频编解码器
    participant HW as 音频硬件 (扬声器)

    VC->>JLB: playAudioWithFileUrlString:(本地文件路径)
    activate JLB
    JLB->>JLB: 构造 file:// NSURL
    JLB->>AVP: initWithContentsOfURL:error:
    AVP->>CODEC: 解析文件头, 初始化解码器<br/>(AAC/MP3/WAV/ALAC...)
    CODEC-->>AVP: 解码器就绪 / error
    JLB->>AVP: 设置 volume/delegate/rate
    JLB->>AVP: prepareToPlay (预解码缓冲)
    JLB->>AVP: play (异步)
    AVP->>CODEC: 持续解码为 PCM
    CODEC->>HW: 输出音频
    Note over AVP,HW: 播放过程中若数据损坏...
    CODEC-->>AVP: 解码错误
    AVP-->>JLB: audioPlayerDecodeErrorDidOccur:error:
    Note over AVP,HW: 正常播完后...
    HW-->>AVP: 播放结束
    AVP-->>JLB: audioPlayerDidFinishPlaying:successfully:
    deactivate JLB

控制流关键点:

  • 整个调用链是单向异步的:play 返回后,解码在系统音频线程(Audio Queue / Audio Unit 后台线程)持续进行,业务线程不被阻塞。
  • 错误分两个阶段到达:初始化期(initWithContentsOfURL:error: 的 err,当前未检查)与播放期(delegate 回调)。
  • 单例持有 _player 强引用,旧播放器对象会在下次播放时被新实例替换释放(ARC 管理),因此连续多次调用播放不同文件是安全的。

使用示例

获取单例并播放本地音频文件

播放接口接收本地文件路径字符串(非网络 URL、非 file:// 前缀形式),由内部转换为 NSURL:

Source: JLAudioToolBox.h

#import "JLAudioToolBox.h"

// 获取全局唯一播放器实例
JLAudioToolBox *audioBox = [JLAudioToolBox sharedInstance];

// 传入本地音频文件的完整路径,例如沙盒 Documents/audio/notice.wav
[audioBox playAudioWithFileUrlString:@"/var/mobile/.../Documents/notice.wav"];

调用注意事项:

  • 路径必须指向本地存在的文件,且格式需为系统解码器支持的格式(AAC、MP3、WAV、ALAC、IMA4 等)。
  • 每次调用会重新创建 AVAudioPlayer 并替换单例持有的旧实例;连续调用时旧播放立即被释放,新音频接管输出。
  • 该方法不返回播放是否成功——初始化错误目前被忽略(见「失败模式」)。

Swift 侧入口

同一工具箱目录下存在 Swift 封装 AudioManager.swift,作为 Swift 业务代码的音频管理入口(具体公开 API 与内部实现请直接查阅该源文件):

Source: AudioManager.swift

// 该文件为 JLAudioToolbox 目录中的 Swift 侧音频管理器,
// 具体方法签名以源文件为准,本文档不代述未核实的 API。

API 参考

+ (instancetype)sharedInstance

获取全局唯一的 JLAudioToolBox 实例。

返回值: JLAudioToolBox * —— 共享单例对象(dispatch_once 保证线程安全)。

说明: 由于 init / new 被 NS_UNAVAILABLE 禁用,这是获取实例的唯一途径。

- (void)playAudioWithFileUrlString:(NSString *)fileUrlString

播放指定本地音频文件。

参数:

  • fileUrlString(NSString *):本地音频文件的路径字符串,非空。内部通过 initFileURLWithPath: 转为 NSURL。

返回值: 无(void)。

抛出/错误:

  • 初始化错误:initWithContentsOfURL:error: 的 err 被接收但当前未检查,文件不存在或格式不支持时静默失败。
  • 播放期解码错误:通过 delegate 回调 audioPlayerDecodeErrorDidOccur:error: 上报(当前方法体为空)。

- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag(Delegate)

自然播放完成时回调;被打断、暂停、停止时不会调用。

参数:

  • player(AVAudioPlayer *):完成播放的播放器实例。
  • flag(BOOL):是否成功完整播放。

- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError * _Nullable)error(Delegate)

播放过程中发生解码错误时回调(区别于初始化期错误)。

参数:

  • player(AVAudioPlayer *):发生错误的播放器实例。
  • error(NSError *):解码错误信息,可为 nil。

配置说明

JLAudioToolBox 没有外部配置文件;播放参数在代码中以硬编码方式设置:

配置项取值说明
_player.volume1.0f播放音量,固定为最大
_player.rate1.0播放速率(需 enableRate 开启才生效,当前未显式开启)
_player.delegateself生命周期回调挂接
prepareToPlay调用预解码缓冲,降低首播延迟
播放方式[AVAudioPlayer play]异步播放,不阻塞调用线程

失败模式、边界情况与并发

初始化错误被静默吞掉

playAudioWithFileUrlString: 中 initWithContentsOfURL:error: 的 err 被创建但从未检查:

Source: JLAudioToolBox.m

NSURL *audioPathUrl = [[NSURL alloc] initFileURLWithPath:fileUrlString];
NSError *err;
_player = [[AVAudioPlayer alloc] initWithContentsOfURL:audioPathUrl error:&err];

后果: 文件不存在、路径非法、格式不支持时,_player 为 nil,后续 prepareToPlay / play 为 no-op 消息,播放静默失败,调用方无法感知。这是当前实现最值得改进的点——建议在 err != nil 时打日志或回调业务层。

解码错误的双阶段语义

阶段触发点当前处理
初始化期initWithContentsOfURL:error:err 未检查,静默
播放期audioPlayerDecodeErrorDidOccur:error:回调存在但方法体为空

播放中途的音频数据损坏(如截断的录音文件)会触发第二阶段回调;方法体为空意味着错误被"吞掉",不会上报、不会重试、不会降级。

并发与资源抢占

  • 单例互斥:dispatch_once 保证单例创建线程安全;播放调用本身不涉及共享可变状态(每次调用重建 _player),因此多线程同时调用 playAudioWithFileUrlString: 不会产生数据竞争,但最后一次调用会覆盖前一次(后到先得)。
  • 音频会话抢占:单例设计避免了应用内多播放器冲突,但系统级打断(来电、Siri、其他 App 播放)仍可能暂停本播放器;由于未实现 AVAudioSession 的 interruption 处理与 audioPlayerBeginInterruption 回调,中断后不会自动恢复播放。
  • 旧播放器释放:强引用 _player 被新实例替换后,旧播放器由 ARC 释放;若旧音频仍在播放,其解码线程随之终止,不会出现两个音频同时输出的情况。

边界情况

  • 空字符串/不存在的路径:initFileURLWithPath: 仍会生成 URL,但 AVAudioPlayer 初始化失败 → 静默失败。
  • 播放时长极短的文件:prepareToPlay + play 的正常路径即可覆盖,无需特判。
  • 格式不支持:AVAudioPlayer 仅支持系统解码器格式;不支持格式同样走静默失败路径。

性能与运维要点

  • 首播延迟:prepareToPlay 将解码器初始化与缓冲分配提前到 play 之前,显著降低首帧延迟,这是 Apple 推荐的标准做法。
  • 内存:AVAudioPlayer 一次性解码(对短提示音而言内存占用可接受);若未来播放长音频(如录音回放),应考虑 AVAudioEngine 流式解码方案。
  • CPU:解码完全发生在系统音频线程,不占用主线程;应用主线程仅承担文件读取与初始化开销(同步 I/O 可能带来毫秒级阻塞,长文件时建议异步化)。

扩展点

  1. 消费 Delegate 回调:在 audioPlayerDidFinishPlaying:successfully: 与 audioPlayerDecodeErrorDidOccur:error: 中实现通知(NSNotificationCenter 广播)、错误上报或日志,即可为上层业务提供"播放完成/失败"事件,无需改动对外接口。
  2. 检查初始化错误:在 initWithContentsOfURL:error: 后判断 err,转换为可观察的失败反馈(回调/异常/返回值)。
  3. 恢复 AudioServices 提示音路径:对系统提示音级延迟要求(如按键反馈),可启用被注释的 AudioServicesPlaySystemSound 代码路径,与 AVAudioPlayer 长音频播放并存。
  4. Swift 封装增强:AudioManager.swift 可作为面向 Swift 业务层的门面(Facade),在其内部封装 JLAudioToolBox 与未来的会话管理逻辑。
  5. 引入 AVAudioSession 管理:补充 setCategory / setActive 与中断处理,以支持后台播放、静音键语义与来电恢复等运营场景。

测试情况

仓库中的单元测试入口位于 UnitTester/DataTester.swift(属于数据测试工具,与音频编解码无直接关联);当前源码中未发现针对 JLAudioToolBox 的单元测试或 UI 测试用例。由于该类依赖真实音频文件与系统解码器,建议采用:

  • 集成测试:准备 WAV/MP3/AAC 样本文件,验证 play 后 delegate 回调按预期触发;
  • 负向测试:传入损坏文件路径,验证解码错误回调(需先补齐错误上报逻辑)。

相关链接

  • JLAudioToolBox.h(头文件:单例与播放接口声明)
  • JLAudioToolBox.m(实现:单例、播放流程与 delegate 回调)
  • AudioManager.swift(Swift 侧音频管理入口)
  • 表盘市场下载/播放相关流程,请参见「表盘市场」目录页
  • 设备交互与数据测试工具,请参见「设备数据测试」相关页面
Prev
资源打包
Next
健康与运动数据同步