iOS开发基础62-音频开发:音效、音乐播放、录音、AVAudioSession 与后台播放

iOS 音频开发深度解析:音效、音乐播放、录音、AVAudioSession 与后台播放

本文系统梳理 iOS 音频开发:音效(System Sound Services)与音乐(AVAudioPlayer)的区别与播放、录音(AVAudioRecorder)的完整配置与权限、音频会话 AVAudioSession 的类别管理与中断处理、后台播放与远程控制、以及音频工具类封装。着重补充现代音频开发的核心细节。


一、音频简介

iOS 中音频按用途分为两类:

类型 别称 时长 作用 推荐框架
音效 短音频 1~2 秒 点缀效果(点击声、提示音、爆炸声) System Sound Services / AVAudioPlayer
音乐 长音频 较长(背景音乐) 持续播放 AVAudioPlayer / AVPlayer

播放音频主要使用 AVFoundation.framework。

音效为什么不用 AVAudioPlayer:System Sound Services 更轻量、启动延迟更低(毫秒级),适合频繁触发的短音效;AVAudioPlayer 初始化有开销,适合较长的音乐。但 System Sound Services 有格式和时长限制(见下文)。


二、音效播放

1. 基本使用

音效播放使用 System Sound Services(基于 AudioToolbox 框架):

#import <AudioToolbox/AudioToolbox.h>

// 1. 获得音效文件路径
NSURL *url = [[NSBundle mainBundle] URLForResource:@"m_03" withExtension:@"wav"];

// 2. 加载音效文件,得到音效 ID
SystemSoundID soundID = 0;
AudioServicesCreateSystemSoundID((__bridge CFURLRef)url, &soundID);

// 3. 播放音效
// 仅播放音效
AudioServicesPlaySystemSound(soundID);
// 播放音效同时震动(仅真机生效,模拟器无震动)
// AudioServicesPlayAlertSound(soundID);

音效文件只需加载一次:加载后 soundID 可重复使用,避免每次播放都重新加载(有性能开销)。

2. 常用函数

函数 作用
AudioServicesCreateSystemSoundID(CFURLRef, SystemSoundID *) 加载音效文件,生成 soundID
AudioServicesDisposeSystemSoundID(SystemSoundID) 释放音效资源
AudioServicesPlaySystemSound(SystemSoundID) 播放音效
AudioServicesPlayAlertSound(SystemSoundID) 播放音效 + 震动

3. 注意事项

  • 支持格式:仅支持 CAF、WAV、aif/aifc 等线性 PCM 或 IMA4 格式,不支持 MP3。
  • 时长限制:音效时长不超过 30 秒,超过会播放失败。
  • 震动:AudioServicesPlayAlertSound 的震动仅在真机生效,模拟器无震动硬件。
  • 音量:音效音量跟随系统铃声音量,不受 App 内 AVAudioPlayer 音量控制。
  • iOS 9 废弃:AudioServicesPlaySystemSound 和 AudioServicesPlayAlertSound 在 iOS 9 被标记为 deprecated,推荐使用带 completion 的版本或 AVAudioPlayer:
// iOS 9+ 推荐:带完成回调
AudioServicesPlaySystemSoundWithCompletion(soundID, ^{
    NSLog(@"音效播放完成");
});

// 或直接用 AVAudioPlayer 播放短音效(更灵活,支持 MP3)

三、音乐播放

1. AVAudioPlayer 简介

较长的音乐(背景音乐)使用 AVAudioPlayer,它是 AVFoundation 提供的音频播放器,支持本地音频文件的播放、暂停、循环、音量、速率等控制。

重要限制:AVAudioPlayer 只支持本地文件(NSURL 或 NSData),不支持远程流媒体 URL。远程音频流播放请使用 AVPlayer(AVFoundation 中更底层的播放器,支持本地和远程)。

2. 常用方法

#import <AVFoundation/AVFoundation.h>

// 加载音乐文件(本地 URL)
NSURL *url = [[NSBundle mainBundle] URLForResource:@"bgm" withExtension:@"mp3"];
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];

// 或从 NSData 加载
// AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithData:data error:nil];

// 准备播放(预缓冲,提高播放流畅性,建议调用)
[player prepareToPlay];

// 播放(异步)
[player play];

// 暂停
[player pause];

// 停止(停止后 currentTime 归零,再次 play 从头开始)
[player stop];

3. 常用属性

属性 类型 说明
isPlaying BOOL(只读) 是否正在播放
duration NSTimeInterval(只读) 总时长(秒)
currentTime NSTimeInterval 当前播放位置(可设置,用于拖拽进度)
numberOfLoops NSInteger 循环次数:-1 无限循环,0 播放 1 次,n 播放 n+1 次
volume float 音量(0.0 ~ 1.0)
enableRate BOOL 是否允许变速(必须先设 YES 才能设置 rate)
rate float 播放速率(1.0 正常,0.5 半速,2.0 双倍)
numberOfChannels NSUInteger(只读) 声道数
pan float 声道平衡(-1.0 左声道,0 中间,1.0 右声道)
meteringEnabled BOOL 是否启用音量测量
delegate id 代理(播放完成、解码错误、中断回调)

4. 音量测量

player.meteringEnabled = YES; // 启用测量

// 播放过程中定时调用
[player updateMeters]; // 更新测量值
float avgPower = [player averagePowerForChannel:0]; // 平均音量(分贝,-160 ~ 0)
float peakPower = [player peakPowerForChannel:0];   // 峰值音量

音量单位是分贝(dB),范围 -160(静音)~ 0(最大),需要转换为线性值才能用于 UI 显示。

5. 代理方法

@interface ViewController () <AVAudioPlayerDelegate>
@end

// 播放完成
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
    NSLog(@"播放完成");
}

// 解码错误
- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError *)error {
    NSLog(@"解码错误: %@", error);
}

// 播放中断(电话、闹钟等)
- (void)audioPlayerBeginInterruption:(AVAudioPlayer *)player {
    NSLog(@"开始中断,暂停播放");
}

- (void)audioPlayerEndInterruption:(AVAudioPlayer *)player withOptions:(NSUInteger)flags {
    if (flags == AVAudioSessionInterruptionOptionShouldResume) {
        [player play]; // 中断结束,恢复播放
    }
}

6. AVAudioPlayer vs AVPlayer

对比项 AVAudioPlayer AVPlayer
本地文件 支持 支持
远程流媒体 不支持 支持
播放控制 简单(play/pause/stop) 更底层(需自己管理 rate、timeControlStatus)
进度监听 currentTime 轮询 addPeriodicTimeObserver(Block 回调)
适用场景 本地背景音乐、短音效 网络音频、视频播放

四、录音

1. 录音权限(iOS 10+ 必须)

iOS 10 起,访问麦克风必须在 Info.plist 中添加 NSMicrophoneUsageDescription(麦克风使用说明),否则调用录音会直接崩溃:

<key>NSMicrophoneUsageDescription</key>
<string>需要访问麦克风进行录音</string>

2. 音频会话设置

录音前需要配置 AVAudioSession 类别为 Record 或 PlayAndRecord,否则可能录音失败或无声:

#import <AVFoundation/AVFoundation.h>

AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *error = nil;
// 设置类别为录音(只录音不播放)
[session setCategory:AVAudioSessionCategoryRecord error:&error];
// 或 PlayAndRecord(同时播放和录音,如 K 歌、语音通话)
// [session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
[session setActive:YES error:&error];

3. AVAudioRecorder 完整实现

@interface ViewController ()
@property (nonatomic, strong) AVAudioRecorder *recorder;
@end

@implementation ViewController

#pragma mark - 录音控制

- (IBAction)startRecord {
    // 请求麦克风权限(iOS 8+)
    [[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) {
        if (granted) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [self.recorder record];
            });
        } else {
            NSLog(@"用户拒绝了麦克风权限");
        }
    }];
}

- (IBAction)stopRecord {
    [self.recorder stop];
}

#pragma mark - 懒加载

- (AVAudioRecorder *)recorder {
    if (!_recorder) {
        // 1. 录音文件存放路径(Documents 目录)
        NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
        NSString *filePath = [docPath stringByAppendingPathComponent:@"record.caf"];
        NSURL *url = [NSURL fileURLWithPath:filePath];
        
        // 2. 录音设置(格式、采样率、声道、质量)
        NSDictionary *settings = @{
            AVFormatIDKey        : @(kAudioFormatMPEG4AAC),  // 格式:AAC(压缩,体积小)
            // AVFormatIDKey : @(kAudioFormatLinearPCM),     // 或 PCM(无损,体积大)
            AVSampleRateKey      : @(44100),                   // 采样率:44100(CD 音质)
            AVNumberOfChannelsKey: @(1),                        // 声道数:1(单声道)
            AVEncoderAudioQualityKey: @(AVAudioQualityHigh)    // 编码质量:高
        };
        
        // 3. 创建录音对象
        NSError *error = nil;
        _recorder = [[AVAudioRecorder alloc] initWithURL:url settings:settings error:&error];
        _recorder.meteringEnabled = YES; // 启用音量测量(可选)
        
        if (error) {
            NSLog(@"录音对象创建失败: %@", error);
        }
    }
    return _recorder;
}

@end

格式选择:

  • kAudioFormatMPEG4AAC:AAC 压缩格式,体积小,适合语音备忘录、聊天语音。
  • kAudioFormatLinearPCM:无损 PCM,体积大,适合后期音频处理。
  • 文件扩展名应与格式匹配:AAC 用 .m4a 或 .caf,PCM 用 .caf 或 .wav。

4. 录音音量监控

// 启用测量后,定时调用
[self.recorder updateMeters];
float avgPower = [self.recorder averagePowerForChannel:0]; // 平均音量
float peakPower = [self.recorder peakPowerForChannel:0];   // 峰值音量

五、音频会话 AVAudioSession(现代音频核心)

1. 为什么需要 AVAudioSession

AVAudioSession 是 iOS 音频的"总开关",管理 App 与系统音频的交互:

  • 决定 App 音频是否与其他 App 音频混音。
  • 决定音频被中断(电话、闹钟、Siri)时的行为。
  • 决定音频路由(扬声器、耳机、蓝牙)。
  • 决定是否支持后台播放。

每个 App 只有一个 AVAudioSession 单例:[AVAudioSession sharedInstance]。

2. 常用类别(Category)

类别 作用 混音 后台播放 典型场景
AVAudioSessionCategoryAmbient 静音键和锁屏时静音 是 否 游戏背景音、音效
AVAudioSessionCategorySoloAmbient 默认类别,静音键和锁屏时静音 否(独占) 否 普通音乐播放
AVAudioSessionCategoryPlayback 静音键和锁屏不静音 可选 是(需配置) 音乐播放器、视频音频
AVAudioSessionCategoryRecord 只录音 - 是(需配置) 录音、语音识别
AVAudioSessionCategoryPlayAndRecord 同时播放和录音 可选 是 K 歌、语音通话、VoIP
AVAudioSessionCategoryMultiRoute 多路由输出 - - 复杂音频设备
AVAudioSession *session = [AVAudioSession sharedInstance];
// 音乐播放器:Playback 类别,支持后台播放
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
[session setActive:YES error:nil];

3. 中断处理

音频被电话、闹钟、Siri 等中断时,系统发送 AVAudioSessionInterruptionNotification:

// 注册中断通知
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(handleInterruption:)
                                             name:AVAudioSessionInterruptionNotification
                                           object:nil];

- (void)handleInterruption:(NSNotification *)note {
    NSDictionary *info = note.userInfo;
    AVAudioSessionInterruptionType type = [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
    
    if (type == AVAudioSessionInterruptionTypeBegan) {
        // 中断开始:暂停播放
        [self.player pause];
    } else if (type == AVAudioSessionInterruptionTypeEnded) {
        // 中断结束:判断是否应恢复
        AVAudioSessionInterruptionOptions options = [info[AVAudioSessionInterruptionOptionKey] unsignedIntegerValue];
        if (options == AVAudioSessionInterruptionOptionShouldResume) {
            [self.player play];
        }
    }
}

4. 路由变化(耳机插拔、蓝牙连接)

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(handleRouteChange:)
                                             name:AVAudioSessionRouteChangeNotification
                                           object:nil];

- (void)handleRouteChange:(NSNotification *)note {
    NSDictionary *info = note.userInfo;
    AVAudioSessionRouteChangeReason reason = [info[AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue];
    
    if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
        // 耳机拔出:暂停播放(苹果官方推荐行为)
        [self.player pause];
    }
}

六、后台播放与远程控制(现代重点)

1. 开启后台播放

  1. 在 Info.plist 中添加后台模式:
<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>

或在 Xcode → Target → Capabilities → Background Modes → 勾选 Audio, AirPlay, and Picture in Picture。

  1. 设置 AVAudioSession 类别为 Playback:
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
[[AVAudioSession sharedInstance] setActive:YES error:nil];

完成后,App 进入后台时音频继续播放。

2. 锁屏信息(Now Playing Info)

在锁屏和控制中心显示歌曲信息:

#import <MediaPlayer/MediaPlayer.h>

MPNowPlayingInfoCenter *center = [MPNowPlayingInfoCenter defaultCenter];
NSMutableDictionary *info = [NSMutableDictionary dictionary];
info[MPMediaItemPropertyTitle] = @"歌曲名";
info[MPMediaItemPropertyArtist] = @"艺术家";
info[MPMediaItemPropertyAlbumTitle] = @"专辑名";
info[MPMediaItemPropertyPlaybackDuration] = @(self.player.duration);
info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = @(self.player.currentTime);
info[MPNowPlayingInfoPropertyPlaybackRate] = @(self.player.rate);
// 专辑封面
MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithImage:[UIImage imageNamed:@"cover"]];
info[MPMediaItemPropertyArtwork] = artwork;
center.nowPlayingInfo = info;

3. 远程控制(锁屏按钮、耳机线控)

MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];

// 播放/暂停
[commandCenter.playCommand addTarget:self action:@selector(remotePlay)];
[commandCenter.pauseCommand addTarget:self action:@selector(remotePause)];

// 上一首/下一首
[commandCenter.nextTrackCommand addTarget:self action:@selector(remoteNext)];
[commandCenter.previousTrackCommand addTarget:self action:@selector(remotePrevious)];

// 耳机线控的播放/暂停切换
[commandCenter.togglePlayPauseCommand addTarget:self action:@selector(remoteToggle)];

实现远程控制后,锁屏界面的播放/暂停/上一首/下一首按钮、耳机线控、控制中心都能控制 App 音频。


七、音频工具类封装

1. 工具类接口

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

@interface CHGAudioTool : NSObject

/// 播放音效(根据文件名)
+ (void)playSoundWithName:(NSString *)soundName;

/// 播放音乐(根据文件名),返回播放器
+ (AVAudioPlayer *)playMusicWithName:(NSString *)musicName;

/// 暂停音乐
+ (void)pauseMusicWithName:(NSString *)musicName;

/// 停止音乐
+ (void)stopMusicWithName:(NSString *)musicName;

/// 停止所有音乐
+ (void)stopAllMusic;

@end

2. 工具类实现(改进版)

#import "CHGAudioTool.h"
#import <AudioToolbox/AudioToolbox.h>

static NSMutableDictionary *_soundIDs;  // 音效 ID 缓存
static NSMutableDictionary *_players;    // 音乐播放器缓存

@implementation CHGAudioTool

+ (void)initialize {
    _soundIDs = [NSMutableDictionary dictionary];
    _players = [NSMutableDictionary dictionary];
}

#pragma mark - 音效

+ (void)playSoundWithName:(NSString *)soundName {
    // 1. 从缓存取 soundID
    SystemSoundID soundID = [[_soundIDs objectForKey:soundName] unsignedIntValue];
    
    // 2. 未加载则加载
    if (soundID == 0) {
        NSURL *url = [[NSBundle mainBundle] URLForResource:soundName withExtension:nil];
        if (!url) {
            NSLog(@"音效文件不存在: %@", soundName);
            return;
        }
        AudioServicesCreateSystemSoundID((__bridge CFURLRef)url, &soundID);
        _soundIDs[soundName] = @(soundID);
    }
    
    // 3. 播放
    AudioServicesPlaySystemSound(soundID);
}

#pragma mark - 音乐

+ (AVAudioPlayer *)playMusicWithName:(NSString *)musicName {
    // 1. 从缓存取播放器
    AVAudioPlayer *player = _players[musicName];
    
    // 2. 未创建则创建
    if (!player) {
        NSURL *url = [[NSBundle mainBundle] URLForResource:musicName withExtension:nil];
        if (!url) {
            NSLog(@"音乐文件不存在: %@", musicName);
            return nil;
        }
        NSError *error = nil;
        player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
        if (error) {
            NSLog(@"音乐播放器创建失败: %@", error);
            return nil;
        }
        [player prepareToPlay]; // 预缓冲
        _players[musicName] = player;
    }
    
    // 3. 如果正在播放,先重置到开头再播放(或根据需求改为不操作)
    if (player.isPlaying) {
        player.currentTime = 0;
    }
    [player play];
    return player;
}

+ (void)pauseMusicWithName:(NSString *)musicName {
    AVAudioPlayer *player = _players[musicName];
    if (player.isPlaying) {
        [player pause];
    }
}

+ (void)stopMusicWithName:(NSString *)musicName {
    AVAudioPlayer *player = _players[musicName];
    if (player) {
        [player stop];
        [_players removeObjectForKey:musicName];
    }
}

+ (void)stopAllMusic {
    for (NSString *key in _players.allKeys) {
        AVAudioPlayer *player = _players[key];
        [player stop];
    }
    [_players removeAllObjects];
}

@end

3. 使用示例

#import "CHGAudioTool.h"

// 播放音效
[CHGAudioTool playSoundWithName:@"click.wav"];

// 播放背景音乐(无限循环)
AVAudioPlayer *bgmPlayer = [CHGAudioTool playMusicWithName:@"bgm.mp3"];
bgmPlayer.numberOfLoops = -1;
bgmPlayer.volume = 0.5;

// 暂停/停止
[CHGAudioTool pauseMusicWithName:@"bgm.mp3"];
[CHGAudioTool stopMusicWithName:@"bgm.mp3"];

八、Swift 版本对照

音效播放(Swift)

import AudioToolbox

var soundID: SystemSoundID = 0
let url = Bundle.main.url(forResource: "m_03", withExtension: "wav")!
AudioServicesCreateSystemSoundID(url as CFURL, &soundID)
AudioServicesPlaySystemSound(soundID)
// 带完成回调(iOS 9+)
AudioServicesPlaySystemSoundWithCompletion(soundID) {
    print("音效播放完成")
}

音乐播放(Swift)

import AVFoundation

let url = Bundle.main.url(forResource: "bgm", withExtension: "mp3")!
let player = try! AVAudioPlayer(contentsOf: url)
player.numberOfLoops = -1
player.volume = 0.5
player.prepareToPlay()
player.play()

录音(Swift)

import AVFoundation

let session = AVAudioSession.sharedInstance()
try session.setCategory(.record)
try session.setActive(true)

session.requestRecordPermission { granted in
    if granted {
        let docPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
        let filePath = (docPath as NSString).appendingPathComponent("record.m4a")
        let url = URL(fileURLWithPath: filePath)
        
        let settings: [String: Any] = [
            AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
            AVSampleRateKey: 44100,
            AVNumberOfChannelsKey: 1,
            AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
        ]
        
        let recorder = try! AVAudioRecorder(url: url, settings: settings)
        recorder.record()
    }
}

AVAudioSession 与后台播放(Swift)

let session = AVAudioSession.sharedInstance()
try session.setCategory(.playback)
try session.setActive(true)

// 中断通知
NotificationCenter.default.addObserver(forName: AVAudioSession.interruptionNotification, object: nil, queue: .main) { note in
    guard let info = note.userInfo,
          let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
          let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }
    if type == .began {
        // 暂停
    } else if type == .ended {
        // 恢复
    }
}

锁屏信息与远程控制(Swift)

import MediaPlayer

let center = MPNowPlayingInfoCenter.default()
center.nowPlayingInfo = [
    MPMediaItemPropertyTitle: "歌曲名",
    MPMediaItemPropertyArtist: "艺术家",
    MPMediaItemPropertyPlaybackDuration: player.duration,
    MPNowPlayingInfoPropertyElapsedPlaybackTime: player.currentTime
]

let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.addTarget { _ in
    player.play()
    return .success
}
commandCenter.pauseCommand.addTarget { _ in
    player.pause()
    return .success
}

九、总结

  • 音频分类:音效(短音频,1~2 秒,System Sound Services,轻量低延迟)和音乐(长音频,AVAudioPlayer,功能丰富)。
  • 音效播放:AudioServicesCreateSystemSoundID 加载 → AudioServicesPlaySystemSound 播放,只加载一次可重复使用;仅支持 CAF/WAV/aif,不支持 MP3,时长 ≤30 秒;震动仅真机生效;iOS 9 起 API 废弃,推荐带 completion 版本或 AVAudioPlayer。
  • 音乐播放:AVAudioPlayer 只支持本地文件(远程流媒体用 AVPlayer);常用 play/pause/stop、numberOfLoops(-1 无限循环)、volume、rate(需先 enableRate=YES)、pan、currentTime、meteringEnabled + updateMeters + averagePower/peakPower;delegate 监听播放完成、解码错误、中断。
  • 录音:AVAudioRecorder;iOS 10+ 必须在 Info.plist 添加 NSMicrophoneUsageDescription,否则崩溃;需配置 AVAudioSession 类别为 Record/PlayAndRecord 并 requestRecordPermission 请求权限;settings 包含格式(AAC/PCM)、采样率(44100)、声道数、编码质量。
  • AVAudioSession(现代核心):单例管理 App 与系统音频交互;类别 Ambient/SoloAmbient/Playback/Record/PlayAndRecord/MultiRoute;中断通知 AVAudioSessionInterruptionNotification(电话/闹钟/Siri);路由变化通知 AVAudioSessionRouteChangeNotification(耳机插拔/蓝牙)。
  • 后台播放:Info.plist 添加 UIBackgroundModes=audio + AVAudioSession 类别 Playback;MPNowPlayingInfoCenter 设置锁屏歌曲信息;MPRemoteCommandCenter 处理远程控制(锁屏按钮/耳机线控/控制中心)。
  • 工具类封装:静态字典缓存 soundID 和 player,避免重复加载;playSoundWithName、playMusicWithName、pause/stop/stopAll;prepareToPlay 预缓冲;stop 后从缓存移除。

posted @ 2015-10-10 09:52  Mr.陳  阅读(889)  评论(0)    收藏  举报