文章

iOS 音视频处理小记

iOS 音视频处理小记

本文记录了处理音频时候踩的坑, 主要是其中因不熟悉导致耗时较长的点

  1. 从视频中提取音频
  2. 音频文件的播放
  3. AVAudioPlayer 创建报错
  4. 系统中音频文件的格式
  5. iOS-音频-AVAudioSession

从视频中提取音频

视频由视轨和音轨组成, 从视频中提取音轨需要用到 AVFoundation 框架中 AVAsset & AVURLAsset.

主要分为 3 步:

  1. 给创建一个音轨对象
  2. 拿到视频按照音轨对象规格提取(指定格式 m4a/caf)
  3. 指定输出地址 URL 输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
fileprivate extension AVAsset {
    
    // 对外提供写入的方法
    func writeAudioTrack(to url: URL,
                         success: @escaping () -> (),
                         failure: @escaping (Error) -> ()) {
        do {
            let asset = try audioAsset()
            asset.write(to: url, success: success, failure: failure)
        } catch {
            failure(error)
        }
    }
    
    // 写入到指定 URL
    private func write(to url: URL, success: @escaping () -> (), failure: @escaping (Error) -> ()) {
        try? FileManager().removeItem(at: url)
        
        // Create an export session that will output an audio track (CAF file)
        if let exportSession = AVAssetExportSession(asset: self, presetName: AVAssetExportPresetPassthrough) {
            
            exportSession.outputFileType = .caf
            exportSession.outputURL = url
            
            exportSession.exportAsynchronously {
                switch exportSession.status {
                case .completed:
                    success()
                default:
                    let error = NSError(domain: "提取音频失败", code: 0, userInfo: nil)
                    failure(error)
                    break
                }
            }
        }else{
            let error = NSError(domain: "提取音频失败", code: 0, userInfo: nil)
            failure(error)
        }
    }
    
    // 生成要提取音频的音轨对象
    private func audioAsset() throws -> AVAsset {
        let composition = AVMutableComposition()
        let audioTracks = tracks(withMediaType: .audio)
        
        for track in audioTracks {
            let compositionTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
            try compositionTrack?.insertTimeRange(track.timeRange, of: track, at: track.timeRange.start)
            compositionTrack?.preferredTransform = track.preferredTransform
        }
        return composition
    }
}

使用方式如下:

1
2
3
4
5
6
7
8
9
10
chooseVideo { videoUrl in
    let asset = AVURLAsset(url: videoUrl)
    let audioUrl = XYFileManager.rootURL!.appendingPathComponent( UUID().uuidString + ".caf")
    
    asset.writeAudioTrack(to: audioUrl) {
        callback(audioUrl)
    } failure: { error in
        Toast.make(error.localizedDescription)
    }
}

音频文件的播放

音频文件播放很简单, 直接使用 AVAudioPlayer.

但是这里有个坑点: 就是 audioPlayer 必须作为当前控制器的属性被持有才能播放, 否则它会直接被释放而无法播放

1
2
3
4
5
6
7
8
// 先解析成 audioData. 并播放
static func playAudio(data: Data) {
    let audioData = data
    shared.audioPlayer = try? AVAudioPlayer(data: audioData)
    shared.audioPlayer?.volume = 1.0
    shared.audioPlayer?.prepareToPlay()
    shared.audioPlayer?.play()
}

AVAudioPlayer 创建报错

创建方式 try? AVAudioPlayer(data: audioData), 可能的错误如下:

1
The operation couldn’t be completed. (OSStatus error 1685348671.)

这个错误本质是初始化时候的音频格式不正确, 比如你名称是 mp3, 实际上文件格式是 caf. 这里需要正确对应

1
The operation couldn’t be completed. (OSStatus error 2003334207.)

这个错误是文件不存在,指定了一个空数据源.

系统中音频文件的格式

要获取导出音频支持的格式, 可参考

具体我们能指定的可用的输出格式可用下面方法得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 初始化一个导出会话, presetName 会直接影响后面可导出的文件格式
let exportSession = AVAssetExportSession(asset: self, presetName: AVAssetExportPresetPassthrough)

// 理论上支持的类型
let types = exportSession.supportedFileTypes
print(types)

// 打印数据
__C.AVFileType(_rawValue: com.apple.quicktime-movie), 
__C.AVFileType(_rawValue: com.apple.m4a-audio), 
__C.AVFileType(_rawValue: public.mpeg-4), 
__C.AVFileType(_rawValue: com.apple.m4v-video), 
__C.AVFileType(_rawValue: public.3gpp), 
__C.AVFileType(_rawValue: org.3gpp.adaptive-multi-rate-audio),
__C.AVFileType(_rawValue: com.microsoft.waveform-audio), 
__C.AVFileType(_rawValue: public.aiff-audio), 
__C.AVFileType(_rawValue: public.aifc-audio), 
__C.AVFileType(_rawValue: com.apple.coreaudio-format)


// 运行时真实能支持的类型, 所选择类型需要在这里面选取         
exportSession.determineCompatibleFileTypes { types in   
    print(types)
}

// 打印
__C.AVFileType(_rawValue: com.apple.coreaudio-format), 
__C.AVFileType(_rawValue: com.apple.m4a-audio), 
__C.AVFileType(_rawValue: com.apple.m4v-video), 
__C.AVFileType(_rawValue: com.apple.quicktime-movie), 
__C.AVFileType(_rawValue: public.3gpp), 
__C.AVFileType(_rawValue: public.mpeg-4)

iOS-音频-AVAudioSession

这里是网络上别人整理好文章, 下面列几个要点.

AVAudioSession的作用就是管理音频这一唯一硬件资源的分配,通过调优合适的AVAudioSession来适配我们的APP对于音频的功能需求。切换音频场景时候,需要相应的切换AVAudioSession。

  • AVAudioSession 就是用来管理多个 APP 对音频硬件设备(麦克风,扬声器)的资源使用

  • AVAudioSession Category 不同类型适用不同类型的 App, 共 7 种类

  • AVAudioSession Mode&&Options AVAudioSession Mode 定制我们的Category行为, 比如 VoIP, chat, game, 视频录制 等 AVAudioSession Options 微调Category行为, AVAudioSessionCategoryOptionMixWithOthers 支持和其他 App 混音

  • 调优我们的Category

比如我们的 App 启动就占用音频通道, 打断了其他音乐软件的播放任务. 经过了解之后就能通过 category / mode / option 来修改为不占用其他 App 的音频通道.

1
2
3
4
5
6
7
8
9
Before: - 直接占用, 打断了其他 App 得使用
	[[AVAudioSession sharedInstance] setActive: YES error: nil];
	[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
	
After: - 设置为可以播放,并允许和其他 App 混音
	[AVAudioSession.sharedInstance setCategory: AVAudioSessionCategoryAmbient mode:AVAudioSessionModeDefault options:AVAudioSessionCategoryOptionMixWithOthers error:NULL];
	
比如: 导航软件,需要后台音频, 并支持智能压低和混音其他 App. 
	BOOL isSuccess = [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionDuckOthers error:&setCategoryError];
  • 音频中断处理 其他APP或者电话会中断我们的APP音频,所以相应的我们要做出处理。 我们可以通过监听AVAudioSessionInterruptionNotification 这个 key 获取音频中断事件.

回调回来Userinfo有键值 AVAudioSessionInterruptionTypeKey: 取值AVAudioSessionInterruptionTypeBegan表示中断开始 取值AVAudioSessionInterruptionTypeEnded表示中断结束

参考

系统文档 系统文档 Audio Session Programming Guide

本文由作者按照 CC BY 4.0 进行授权