79、iOS音频开发全解析

iOS音频开发全解析

1. 音频播放基础操作

在iOS开发中,处理音频播放时,有新旧两种方式。旧方式在释放声音时,需要先移除声音完成函数,代码示例如下:

AudioServicesRemoveSystemSoundCompletion(snd)
AudioServicesDisposeSystemSoundID(snd)
}, nil)
AudioServicesPlaySystemSound(snd)

新方式则使用 AudioServicesPlaySystemSoundWithCompletion ,它接收两个参数:一个 SystemSoundID 和一个完成函数。完成函数无参数,由于 SystemSoundID 在作用域内,仍可用于释放其内存。示例代码如下:

let sndurl = Bundle.main.url(forResource:"test", withExtension: "aif")!
var snd : SystemSoundID = 0
AudioServicesCreateSystemSoundID(sndurl as CFURL, &snd)
AudioServicesPlaySystemSoundWithCompletion(snd) {
    AudioServicesDisposeSystemSoundID(snd)
}
2. 音频会话(Audio Session)

设备上的所有音频,包括所有应用和进程的音频,都由媒体服务守护进程控制和协调。应用通过音频会话与该守护进程通信,音频会话是应用启动时自动创建的单例 AVAudioSession 实例,需导入 AVFoundation 框架,并通过 sharedInstance 类方法引用。

2.1 音频会话类别(Category)

如果应用要产生声音,需指定声音策略并告知媒体服务守护进程。这涉及回答以下问题:
- 屏幕锁定时,应用的声音是否应停止?
- 若有其他声音正在播放(如音乐应用在后台播放歌曲),应用应停止其他声音还是叠加播放?

通过调用 setCategory(_:mode:options:) 设置音频会话的类别( AVAudioSession.Category ), options 参数可省略,若没有特定模式,可使用 .default 模式。应用不必始终使用单一类别,不同活动或时刻可能需要更改类别。

基本的音频播放策略如下:
| 策略 | 描述 |
| ---- | ---- |
| Ambient (.ambient) | 应用的音频可与其他应用的音频同时播放,会被手机的静音开关和屏幕锁定停止。 |
| Solo Ambient (.soloAmbient,默认) | 应用会停止其他应用正在播放的音频,会被手机的静音开关和屏幕锁定停止。 |
| Playback (.playback) | 应用会停止其他应用正在播放的音频,不受静音开关影响。除非配置为在后台播放,否则会被屏幕锁定停止。 |

音频会话类别选项( AVAudioSession.CategoryOptions )可修改播放策略,例如:
- .mixWithOthers :可覆盖 Playback 策略,允许其他应用继续播放音频,此时应用的声音可混合。
- .interruptSpokenAudioAndMixWithOthers :与 .mixWithOthers 类似,但在与背景音乐混合时会停止语音音频。应用的音频通过将音频会话模式设置为 .spokenAudio 标记为语音。
- .duckOthers :可覆盖允许其他音频播放的策略,使其他音频音量降低,这是一种混合方式。

2.2 音频会话的激活与停用

音频会话策略只有在会话处于活动状态时才生效,默认情况下会话不活动。因此,需通过配置音频会话并激活它来应用策略。激活音频会话可调用 setActive(true)

何时调用 setActive(true) 取决于应用是否需要音频会话始终处于活动状态,还是仅在产生声音时活动。在许多情况下,最好在真正需要时才激活,即开始产生声音之前。例如,若应用的声音偶尔、间断且非必需,希望其他应用(如音乐应用)的声音在用户启动或切换到应用时继续播放,可采用 Ambient 策略,并在应用启动时设置类别并激活会话:

func application(_ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions:
    [UIApplication.LaunchOptionsKey : Any]?) -> Bool {
        let sess = AVAudioSession.sharedInstance()
        try? sess.setCategory(.ambient, mode:.default)
        try? sess.setActive(true)
        return true
}

也可调用 setActive(false) 停用音频会话,有多种原因可能需要停用(并可能重新激活)音频会话:
- 更改音频会话策略:某些音频会话类别和选项的更改只有在停用现有策略并激活新策略后才生效,例如“Ducking”。
- 停止播放声音:停止播放声音后,不再需要占用设备音频,可停用会话让其他应用恢复播放,还可发送消息通知其他应用:

let sess = AVAudioSession.sharedInstance()
try? sess.setActive(false, options: .notifyOthersOnDeactivation)

建议注册 AVAudioSession.mediaServicesWereResetNotification ,若收到此通知,说明媒体服务守护进程出现问题,此时应重新配置类别、激活音频会话,并重置和重新创建所有音频相关对象。

2.3 Ducking(音量降低)

以停用和激活音频会话为例,说明如何实现“Ducking”。假设已配置并激活 Ambient 类别音频会话,该类别允许其他音频继续播放。若应用偶尔播放短暂声音,且希望在播放时其他音频音量暂时降低,可按以下步骤操作:
1. 停用音频会话。
2. 重新配置音频会话类别,添加 .duckOthers 选项。
3. 激活音频会话。

播放声音前,添加 .duckOthers 选项降低其他声音音量:

let sess = AVAudioSession.sharedInstance()
try? sess.setActive(false)
let opts = sess.categoryOptions.union(.duckOthers)
try? sess.setCategory(sess.category, mode: sess.mode, options: opts)
try? sess.setActive(true)

声音播放完成后,移除 .duckOthers 选项恢复其他声音音量:

let sess = AVAudioSession.sharedInstance()
try? sess.setActive(false)
let opts = sess.categoryOptions.subtracting(.duckOthers)
try? sess.setCategory(sess.category, mode: sess.mode, options:opts)
try? sess.setActive(true)
2.4 中断处理(Interruptions)

音频会话可能会被中断,如来电、闹钟响起,或其他应用抢占音频会话。当音频会话被中断时,会话会被停用,因此需要知道中断何时结束,以便重新激活会话。可注册 AVAudioSession.interruptionNotification ,最好在应用启动时注册。

该通知可能表示中断开始或结束,通过检查通知的 userInfo 字典中的 AVAudioSessionInterruptionTypeKey 条目来确定,其值为 UInt 类型,对应 AVAudioSession.InterruptionType .began .ended 。示例代码如下:

NotificationCenter.default.addObserver(forName:
    AVAudioSession.interruptionNotification, object: nil, queue: nil) { n in
        let why = n.userInfo![AVAudioSessionInterruptionTypeKey] as! UInt
        let type = AVAudioSession.InterruptionType(rawValue: why)!
        switch type {
        case .began:
            // update interface if needed
        case .ended:
            try? AVAudioSession.sharedInstance().setActive(true)
            // update interface if needed
            // resume playback?
        }
}

中断开始时,音频已暂停,音频会话已停用,可能需要更新界面(如将暂停按钮改为播放按钮)。中断结束时,需重新激活音频会话并可能恢复音频播放。

通知中可能包含其他应用中断并停用音频会话时发送的消息,通过 userInfo 字典中的 AVAudioSessionInterruptionOptionKey 条目接收,其值为 UInt 类型,对应 AVAudioSession.InterruptionOptions ,可能为 .shouldResume ,若包含该选项,可恢复播放:

guard let opt = n.userInfo![AVAudioSessionInterruptionOptionKey] as? UInt
    else {return}
if AVAudioSession.InterruptionOptions(rawValue:opt).contains(.shouldResume) {
    // resume playback
}
2.5 次要音频(Secondary Audio)

当应用处于前台,用户调出控制中心并使用播放按钮恢复音乐应用的歌曲时,可能不会中断应用的音频会话,而是收到 AVAudioSession.silenceSecondaryAudioHintNotification 通知。该通知对应 AVAudioSession secondaryAudioShouldBeSilencedHint 布尔属性,区分了主音频和次音频。

例如,游戏应用中,间歇性音效为主音频,持续的背景音乐为次音频。用户开始播放音乐应用的歌曲时,应用应暂停次音频,继续产生主音频。

处理该通知时,检查通知的 userInfo 字典中的 AVAudioSessionSilenceSecondaryAudioHintTypeKey 条目,其值为 UInt 类型,对应 AVAudioSession.SilenceSecondaryAudioHintType .begin .end 。示例代码如下:

NotificationCenter.default.addObserver(forName:
    AVAudioSession.silenceSecondaryAudioHintNotification,
    object: nil, queue: nil) { n in
        let why = n.userInfo![AVAudioSessionSilenceSecondaryAudioHintTypeKey]
            as! UInt
        let type = AVAudioSession.SilenceSecondaryAudioHintType(rawValue:why)!
        switch type {
        case .begin:
            // pause secondary audio
        case .end:
            // resume secondary audio
        }
}
2.6 路由更改(Routing Changes)

音频通过特定的输出(和输入)进行路由,外部事件(如来电)或用户操作(如插入耳机)可能导致音频路由更改。可注册 AVAudioSession.routeChangeNotification 来监听路由更改并做出响应。

通知的 userInfo 字典包含有用信息,例如,当拔掉耳机时,字典内容如下:

AVAudioSessionRouteChangeReasonKey = 2;
AVAudioSessionRouteChangePreviousRouteKey =
    <AVAudioSessionRouteDescription: 0x174019ee0,
        inputs = (null);
        outputs = (
            <AVAudioSessionPortDescription: 0x174019f00,
                type = Headphones;
                name = Headphones;
                UID = Wired Headphones;
                selectedDataSource = (null)>
        )>;

收到通知后,可调用 AVAudioSession currentRoute 方法获取当前音频路由:

<AVAudioSessionRouteDescription: 0x174019fc0,
    inputs = (null);
    outputs = (
        <AVAudioSessionPortDescription: 0x17401a000,
            type = Speaker;
            name = Speaker;
            UID = Speaker;
            selectedDataSource = (null)>
    )>

AVAudioSessionRouteChangeReasonKey 对应 AVAudioSessionRouteChangeReason ,值为2表示 .oldDeviceUnavailable ,即停止使用耳机并开始使用扬声器。路由更改本身可能不会中断声音,但苹果建议在这种情况下主动停止音频,以免声音突然从扬声器在公共场所播放。

3. 音频播放器(Audio Player)

播放声音最简单的方法是使用音频播放器( AVAudioPlayer ),它是 AVFoundation 框架的一部分,需导入该框架。

音频播放器使用本地文件URL或 Data 初始化声音,可选择指定预期的声音文件格式。支持多种声音类型,包括MP3、AAC、ALAC、AIFF和WAV,从iOS 11开始,还支持FLAC和Opus。一个音频播放器只能播放一个声音,但可创建多个音频播放器,它们可单独或同时播放,还可同步。

创建并初始化音频播放器后,需保留它,通常将其赋值给实例属性。常见的初学者错误是将音频播放器赋值给局部变量并播放,却听不到声音,因为播放器在开始播放前就已销毁。

播放声音的步骤如下:
1. 确保音频会话配置正确。
2. 调用 prepareToPlay 方法,使播放器加载缓冲区并初始化硬件。
3. 调用 play 方法开始播放。

音频播放器的委托( AVAudioPlayerDelegate )会在声音播放完成时通过 audioPlayerDidFinishPlaying(_:successfully:) 方法得到通知,不要反复检查 isPlaying 属性来获取播放状态。其他有用方法包括 pause stop ,主要区别是 pause 不会释放 prepareToPlay 设置的缓冲区和硬件,而 stop 会,因此恢复播放前需再次调用 prepareToPlay pause stop 都不会改变播放头位置,可使用 currentTime 属性设置播放位置。

以下是一个自定义 Player 类的示例,用于播放不同的声音文件:

protocol PlayerDelegate : class {
    func soundFinished(_ sender: Any)
}

class Player : NSObject, AVAudioPlayerDelegate {
    var player : AVAudioPlayer!
    weak var delegate : PlayerDelegate?

    func playFile(atPath path:String) {
        self.player?.delegate = nil
        self.player?.stop()
        let fileURL = URL(fileURLWithPath: path)
        guard let p = try? AVAudioPlayer(contentsOf:fileURL) else {return}
        self.player = p
        self.player.prepareToPlay()
        self.player.delegate = self
        self.player.play()
    }

    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer,
        successfully flag: Bool) {
            self.delegate?.soundFinished(self)
    }
}

AVAudioPlayer 的一些有用属性如下:
| 属性 | 描述 |
| ---- | ---- |
| pan, volume | 分别为立体声定位和音量。 |
| numberOfLoops | 声音播放完成后应重复的次数,0(默认)表示不重复,负值表示无限重复(直到停止)。 |
| duration | 声音的长度(只读)。 |
| currentTime | 声音中的播放头位置,可设置该属性来“查找”播放位置。 |
| enableRate, rate | 允许声音以0.5到2.0倍的速度播放,在调用 prepareToPlay 前将 enableRate 设置为 true ,然后可设置 rate 。 |
| isMeteringEnabled | 若为 true (默认 false ),可调用 updateMeters ,然后定期调用 averagePower(forChannel:) 和/或 peakPower(forChannel:) 来跟踪声音的音量,可能用于在界面中提供图形表示。 |
| settings | 描述声音特征的只读字典,如比特率( AVEncoderBitRateKey )、采样率( AVSampleRateKey )和数据格式( AVFormatIDKey ),也可通过 format 属性获取声音的数据格式。 |

playAtTime(_:) 方法可安排在特定时间开始播放,时间应根据音频播放器的 deviceCurrentTime 属性描述。

音频播放器可无缝处理某些类型的中断,例如,当应用移到后台时声音被迫停止,应用回到前台时,音频播放器会重新激活音频会话并恢复播放,且不会收到中断通知。但并非所有中断都能自动恢复播放,因此可能仍需注册中断通知。

综上所述,iOS音频开发涉及多个方面,包括音频播放基础操作、音频会话管理、路由更改处理和音频播放器使用等。开发者需根据应用的具体需求,合理配置音频会话类别和选项,处理各种中断和路由更改情况,以提供良好的音频体验。

iOS音频开发全解析(续)

4. 音频开发综合流程梳理

为了更清晰地展示iOS音频开发的整体流程,我们可以通过一个mermaid流程图来呈现:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;

    A([开始]):::startend --> B(配置音频会话):::process
    B --> C{选择音频会话类别}:::decision
    C -->|Ambient| D(允许与其他音频混合,受静音和锁屏影响):::process
    C -->|Solo Ambient| E(停止其他音频,受静音和锁屏影响):::process
    C -->|Playback| F(停止其他音频,不受静音影响,可能受锁屏影响):::process
    D --> G(可添加类别选项):::process
    E --> G
    F --> G
    G --> H{是否需要Ducking}:::decision
    H -->|是| I(按Ducking步骤操作):::process
    H -->|否| J(无需Ducking):::process
    I --> K(停用会话,添加.duckOthers,激活会话):::process
    J --> L(创建音频播放器):::process
    K --> L
    L --> M(初始化音频播放器):::process
    M --> N(保留音频播放器实例):::process
    N --> O(配置音频播放器属性):::process
    O --> P(调用prepareToPlay):::process
    P --> Q(调用play开始播放):::process
    Q --> R{是否收到中断通知}:::decision
    R -->|是| S(处理中断,重新激活会话):::process
    R -->|否| T(正常播放):::process
    T --> U{是否收到路由更改通知}:::decision
    U -->|是| V(处理路由更改):::process
    U -->|否| W(继续播放直到结束):::process
    V --> W
    W --> X(释放音频资源):::process
    X --> Y([结束]):::startend

这个流程图涵盖了从配置音频会话到最后释放音频资源的整个过程,包括了前面提到的各种操作和决策点,帮助开发者更系统地理解和实施iOS音频开发。

5. 常见问题及解决方案

在iOS音频开发过程中,开发者可能会遇到一些常见问题,以下是一些问题及对应的解决方案:

问题 描述 解决方案
听不到声音 将音频播放器赋值给局部变量,播放时无声音 将音频播放器赋值给实例属性,确保其生命周期足够长
音频中断后无法恢复 音频会话被中断后,无法自动恢复播放 注册 AVAudioSession.interruptionNotification ,在中断结束时重新激活会话并恢复播放
声音突然从扬声器大声播放 拔掉耳机后,声音突然从扬声器大声播放 注册 AVAudioSession.routeChangeNotification ,在路由更改时主动停止音频
无法实现Ducking效果 希望在播放自己的声音时降低其他音频音量,但未实现 按照Ducking的步骤,停用会话,添加 .duckOthers 选项,然后激活会话
6. 高级应用场景及实现思路

除了基本的音频播放和管理,iOS音频开发还有一些高级应用场景,以下是一些示例及实现思路:

6.1 音频同步播放

在某些应用中,可能需要多个音频播放器同步播放,例如游戏中的音效和背景音乐同步。实现思路如下:
1. 使用 AVAudioPlayer playAtTime(_:) 方法,根据音频播放器的 deviceCurrentTime 属性来安排播放时间。
2. 确保所有音频播放器在同一时间开始播放,可通过设置相同的播放时间来实现。

示例代码:

let audioPlayer1 = // 初始化音频播放器1
let audioPlayer2 = // 初始化音频播放器2

let currentTime = audioPlayer1.deviceCurrentTime
let startTime = currentTime + 1.0 // 1秒后开始播放

audioPlayer1.prepareToPlay()
audioPlayer2.prepareToPlay()

audioPlayer1.playAtTime(startTime)
audioPlayer2.playAtTime(startTime)
6.2 音频实时处理

在一些音频应用中,可能需要对音频进行实时处理,例如添加音效、变声等。可使用 AVAudioEngine 来实现音频的实时处理。实现思路如下:
1. 创建 AVAudioEngine 实例。
2. 添加音频节点,如 AVAudioPlayerNode 用于播放音频, AVAudioUnitEffect 用于添加音效。
3. 连接音频节点,设置音频处理链。
4. 启动 AVAudioEngine

示例代码:

import AVFoundation

let audioEngine = AVAudioEngine()
let audioPlayerNode = AVAudioPlayerNode()
let audioUnitEffect = AVAudioUnitReverb()

audioEngine.attach(audioPlayerNode)
audioEngine.attach(audioUnitEffect)

audioEngine.connect(audioPlayerNode, to: audioUnitEffect, format: nil)
audioEngine.connect(audioUnitEffect, to: audioEngine.mainMixerNode, format: nil)

let audioFile = // 加载音频文件
let audioFormat = audioFile.processingFormat
let audioFrameCount = UInt32(audioFile.length)

let audioBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: audioFrameCount)!
try! audioFile.read(into: audioBuffer)

audioPlayerNode.scheduleBuffer(audioBuffer)

try! audioEngine.start()
audioPlayerNode.play()
7. 总结

iOS音频开发是一个涉及多个方面的复杂过程,从音频会话的配置、音频播放器的使用,到各种中断和路由更改的处理,都需要开发者仔细考虑。通过合理选择音频会话类别和选项,处理好各种异常情况,开发者可以为用户提供优质的音频体验。同时,对于高级应用场景,如音频同步播放和实时处理,也有相应的实现思路和方法可供参考。在实际开发中,开发者应根据应用的具体需求,灵活运用这些知识和技巧,不断优化音频功能。

希望本文能对iOS开发者在音频开发方面提供有价值的参考,帮助大家更好地完成音频相关的开发任务。

**高校专业实习管理平台设计与实现** 本设计项目旨在构建一个服务于高等院校专业实习环节的综合性管理平台。该系统采用当前主流的Web开发架构,基于Python编程语言,结合Django后端框架与Vue.js前端框架进行开发,实现了前后端逻辑的分离。数据存储层选用广泛应用的MySQL关系型数据库,确保了系统的稳定性和数据处理的效率。 平台设计了多角色协同工作的管理模型,具体包括系统管理员、院系负责人、指导教师、实习单位对接人以及参与实习的学生。各角色依据权限访问不同的功能模块,共同构成完整的实习管理流程。核心功能模块涵盖:基础信息管理(如院系、专业、人员信息)、实习过程管理(包括实习公告发布、实习内容规划、实习申请与安排)、双向反馈机制(单位评价与学生反馈)、实习支持与保障、以及贯穿始终的成绩评定与综合成绩管理。 在技术实现层面,后端服务依托Django框架的高效与安性构建业务逻辑;前端界面则利用Vue.js的组件化特性与LayUI的样式库,致力于提供清晰、友好的用户交互体验。数据库设计充分考虑了实习管理业务的实体关系与数据一致性要求,并保留了未来功能扩展的灵活性。 整个系统遵循规范的软件开发流程,从需求分析、系统设计、编码实现到测试验证,均进行了多轮迭代与优化,力求在功能完备性、系统性能及用户使用体验方面达到较高标准。 **核心术语**:实习管理平台;Django框架;MySQL数据库;Vue.js前端;Python语言。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值