录音,就要用到麦克风了
iOS 设备中,每一个应用 app,都有一个音频会话 Audio Session.
app 调用音频相关,自然会用到 iOS 的硬件功能。
音频会话 Audio Session ,就是来管理音频操作的。
iOS 使用音频,管理粒度很细
你觉得: 后台播放的音乐,要不要与你 app 的音频,混杂在一起?
Audio Session 处理音频,通过他的分类 Audio Session Category 设置
默认的分类,
1, 允许播放,不允许录音。
2, 静音按钮开启后,你的应用就哑巴了,播放音频没声音。
3, 锁屏后,你的应用也哑巴了,播放音频没声音。
4, 如果后台有别的 app 播放音频,你 app 要开始播放音频的时候,别的 app 就哑巴了。
更多分类,如图:
首先要对音频操作,做一些配置。
一般操作音频,会用到 AVFoundation
框架,先引入 import AVFoundation
设置 Audio Session 的分类,AVAudioSession.CategoryOptions.defaultToSpeaker
, 允许我们的 app , 调用内置的麦克风来录音,又可以播放音频。
这里要做录音功能,就把分类的选项也改了。
分类的默认选项是,音频播放的是收听者,即上面的喇叭口,场景一般是你把手机拿到耳朵边,打电话。
现在把音频播放路径, 指向说话的人,即麦克风,下面的喇叭口。
// 这是一个全局变量,记录麦克风权限的
var appHasMicAccess = true
// ...
// 先获取一个 AVAudioSession 的实例
let session = AVAudioSession.sharedInstance()
do {
// 在这里,设置分类
try session.setCategory(AVAudioSession.Category.playAndRecord, options: AVAudioSession.CategoryOptions.defaultToSpeaker)
try session.setActive(true)
// 检查 app 有没有权限,使用该设备麦克风
session.requestRecordPermission({ (isGranted: Bool) in
if isGranted {
// 你的 app 想要录制音频,用户必须授予麦克风权限
appHasMicAccess = true
}
else{
appHasMicAccess = false
}
})
} catch let error as NSError {
print("AVAudioSession configuration error: \(error.localizedDescription)")
}
进入录音,
// 这是一个枚举变量,用来手动追踪录音的状态
var audioStatus: AudioStatus = AudioStatus.Stopped
var audioRecorder: AVAudioRecorder!
func setupRecorder() {
// getURLforMemo, 这个方法,拿到一个可以保存录音文件的,临时路径
// getURLforMemo , 具体见下面的 GitHub 链接
let fileURL = getURLforMemo()
// 设置录音采样的描述信息
/*
线性脉冲编码调制,非压缩的数据格式
采样频率, 44.1 千赫兹的,CD 级别的效果
单声道,就录制一个单音
*/
let recordSettings = [
AVFormatIDKey: Int(kAudioFormatLinearPCM),
AVSampleRateKey: 44100.0,
AVNumberOfChannelsKey: 1,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
] as [String : Any]
do {
// 实例化 audioRecorder
audioRecorder = try AVAudioRecorder(url: fileURL, settings: recordSettings)
audioRecorder.delegate = self
audioRecorder.prepareToRecord()
} catch {
print("Error creating audio Recorder.")
}
}
// 开始录音
func record() {
startUpdateLoop()
// 追踪,记录下当前 app 的录音状态
audioStatus = .recording
// 这一行,就是开始录音了
audioRecorder.record()
}
// 停止录音
func stopRecording() {
recordButton.setBackgroundImage(UIImage(named: "button-record"), for: UIControl.State.normal )
audioStatus = .stopped
audioRecorder.stop()
stopUpdateLoop()
}
录音结束,通过代理 AVAudioRecorderDelegate ,更新状态
func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
audioStatus = .stopped
// 因为这个场景,录制完了, 必须手动点击,
// 所以不需要在这里更新 UI
}
录音好了,做播放
播放录音
var audioPlayer: AVAudioPlayer!
// 开始播放
func play() {
// getURLforMemo, 这个方法,拿到一个可以保存录音文件的,临时路径
// getURLforMemo , 具体见下面的 GitHub 链接
let fileURL = getURLforMemo()
do {
// 实例化 audioPlayer
audioPlayer = try AVAudioPlayer(contentsOf: fileURL)
audioPlayer.delegate = self
// 检查音频文件不为空,才播放音频文件
if audioPlayer.duration > 0.0 {
setPlayButtonOn(flag: true)
audioPlayer.play()
audioStatus = .Playing
startUpdateLoop()
}
} catch {
print("Error loading audio Player")
}
}
// 停止播放
func stopPlayback() {
setPlayButtonOn(flag: false)
audioStatus = .stopped
audioPlayer.stop()
stopUpdateLoop()
}
播放结束,通过代理 AVAudioPlayerDelegate ,更新 UI
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
// 因为只有在这里,我们才知道,播放完了的时机
setPlayButtonOn(flag: false)
audioStatus = .stopped
stopUpdateLoop()
}
显示录音/ 播放进展的 UI
要显示显示录音/ 播放的进展,就要用到计时器了,
因为录音/ 播放,每时每刻,都在变化。
计时器三步走:
开启计时器,
var soundTimer: CFTimeInterval = 0.0
var updateTimer: CADisplayLink!
func startUpdateLoop(){
if updateTimer != nil{
updateTimer.invalidate()
}
// 计时器是非常轻量级的对象,使用前,先销毁
updateTimer = CADisplayLink(target: self, selector: #selector(ViewController.updateLoop))
updateTimer.preferredFramesPerSecond = 1
updateTimer.add(to: RunLoop.current, forMode: RunLoop.Mode.common)
}
定时,做事情
@objc func updateLoop(){
if audioStatus == .recording{
// 录音状态,定时刷新
if CFAbsoluteTimeGetCurrent() - soundTimer > 0.5 {
timeLabel.text = formattedCurrentTime(UInt(audioRecorder.currentTime))
soundTimer = CFAbsoluteTimeGetCurrent()
}
}
else if audioStatus == .playing{
// 播放状态,定时刷新
if CFAbsoluteTimeGetCurrent() - soundTimer > 0.5 {
timeLabel.text = formattedCurrentTime(UInt(audioPlayer.currentTime))
soundTimer = CFAbsoluteTimeGetCurrent()
}
}
}
销毁计时器
需要停止的时候,就调用这个方法,例如: 播放完成的代理方法中,再一次点击播放按钮...
func stopUpdateLoop(){
updateTimer.invalidate()
updateTimer = nil
// formattedCurrentTime,这个方法,时间转文字,具体见文尾的 GitHub 链接
timeLabel.text = formattedCurrentTime(UInt(0))
}
采样音量大小计量
AVAudioPlayer 有音频的计量功能,播放音频的时候,音频计量可以检测到,波形的平均能级等信息
AVAudioPlayer 的方法 averagePower(forChannel:)
,会返回当前的分贝值,取值范围是 -160 ~ 0 db, 0 是很吵, -160 是很安静
波形,长这样
做一个张口嘴巴的动画,就是一个简单的音量大小可视化,音量越大,张开嘴的幅度也越大,具体见文尾的 GitHub repo
// 自己创建一个结构体,计量表 MeterTable
// 音频计量返回的浮点数的范围 -160 ~ 0,先做分贝转振幅,转换为 0 ~ 1 之间
// 张口嘴巴的动画的图片有 5 张,分为 5 个级别,上面的取值范围,就要划分为对应的五个层级,
// MeterTable 就要把采集的声音,映射到对应的图片
let meterTable = MeterTable(tableSize: 100)
// ...
// 播放前,先要激活音量分贝值检测功能
audioPlayer.isMeteringEnabled = true
// ...
// 将采集到的音量大小,映射为图片编号
// 更新状态的方法,一定要用到计时器。
// 该方法,要在计时器方法中使用到,具体见文尾的 github repo
func meterLevelsToFrame() -> Int{
guard let player = audioPlayer else {
return 1
}
player.updateMeters()
// 之前设置了,播放器是单声道
let avgPower = player.averagePower(forChannel: 0)
let linearLevel = meterTable.valueForPower(power: avgPower)
// 继续处理数据,转换出一个能级,具体见文尾的 GitHub repo
let powerPercentage = Int(round(linearLevel * 100))
// 目前总共有 5 张图片
let totalFrames = 5
// 根据音量大小,决定呈现哪一张
// 图片命名是 01~05,所以要 + 1
let frame = ( powerPercentage / totalFrames ) + 1
return min(frame, totalFrames)
}
音频播放控制: 包含音量大小控制、左右声道切换、播放循环、播放速率控制等等
控制播放音量大小
音量的取值范围是 0 ~ 1, 0 是静音,1 是最大
func toSetVolumn(value: Float){
guard let player = audioPlayer else {
return
}
// 苹果都封装好了,设置 audioPlayer 的 volume
player.volume = value
}
设置左右声道
取值范围是 -1 到 1,
-1 是全左,1 是全右,0是均衡声道
func toSetPan(value: Float) {
guard let player = audioPlayer else {
return
}
// 苹果都封装好了,设置 audioPlayer 的 pan
player.pan = value
}
设置播放循环
循环的取值范围是 -1 到 Int.max,
numberOfLoops 取值 0 到 Int.max,则会多播放那个取值的次数
func toSetLoopPlayback(loop: Bool) {
guard let player = audioPlayer else {
return
}
// 苹果都封装好了,设置 audioPlayer 的 numberOfLoops
if loop == true{
// numberOfLoops 为 -1,无限循环,直到 audioPlayer 停止
player.numberOfLoops = -1
}
else{
// numberOfLoops 为 0,仅播放一次,不循环
player.numberOfLoops = 0
}
}
设置播放速率
audioPlayer 的播放速率范围是,0.5 ~ 2.0
0.5 是半速播放,1.0 是正常播放,2.0 是倍速播放
// 播放前,要点亮 audioPlayer 的播放速率控制,为可用
audioPlayer.enableRate = true
// ...
func toSetRate(value: Float) {
guard let player = audioPlayer else {
return
}
// 苹果都封装好了,设置 audioPlayer 的 rate
player.rate = value
}
github 链接
续集: