录音,就要用到麦克风了git
iOS 设备中,每个应用 app,都有一个音频会话 Audio Session.github
app 调用音频相关,天然会用到 iOS 的硬件功能。bash
音频会话 Audio Session ,就是来管理音频操做的。session
你以为: 后台播放的音乐,要不要与你 app 的音频,混杂在一块儿?app
Audio Session 处理音频,经过他的分类 Audio Session Category 设置框架
默认的分类,oop
1, 容许播放,不容许录音。post
2, 静音按钮开启后,你的应用就哑吧了,播放音频没声音。动画
3, 锁屏后,你的应用也哑吧了,播放音频没声音。ui
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()
}
复制代码
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()
}
复制代码
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
// 由于只有在这里,咱们才知道,播放完了的时机
setPlayButtonOn(flag: false)
audioStatus = .stopped
stopUpdateLoop()
}
复制代码
要显示显示录音/ 播放的进展,就要用到计时器了,
由于录音/ 播放,每时每刻,都在变化。
计时器三步走:
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
}
复制代码
续集: