錄音,就要用到麥克風了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
}
複製代碼
續集: