在iOS中繪制錄音音頻波形圖

效果圖

條狀波形圖

線狀波形圖

配置AvAudioSession

繪制波形圖前首先需要配置好AVAudioSession,同時(shí)需要建立一個(gè)數(shù)組去保存音量數(shù)據(jù)。

相關(guān)屬性

recorderSetting用于設(shè)定錄音音質(zhì)等相關(guān)數(shù)據(jù)。

timer以及updateFequency用于定時(shí)更新波形圖。

soundMeter和soundMeterCount用于保存音量表數(shù)組。

recordTime用于記錄錄音時(shí)間,可以用于判斷錄音時(shí)間是否達(dá)到要求等進(jìn)一波需求。

/// 錄音器

private var recorder: AVAudioRecorder!? ? /// 錄音器設(shè)置

private let recorderSetting = [AVSampleRateKey : NSNumber(value: Float(44100.0)),//聲音采樣率

AVFormatIDKey : NSNumber(value: Int32(kAudioFormatMPEG4AAC)),//編碼格式

AVNumberOfChannelsKey : NSNumber(value: 1),//采集音軌

AVEncoderAudioQualityKey : NSNumber(value: Int32(AVAudioQuality.medium.rawValue))]//聲音質(zhì)量

/// 錄音計(jì)時(shí)器

private var timer: Timer?? ? /// 波形更新間隔

private let updateFequency = 0.05

/// 聲音數(shù)據(jù)數(shù)組

private var soundMeters: [Float]!? ? /// 聲音數(shù)據(jù)數(shù)組容量

private let soundMeterCount = 10

/// 錄音時(shí)間

private var recordTime = 0.00

AvAudioSession相關(guān)配置

configAVAudioSession用于配置AVAudioSession,其中AVAudioSessionCategoryRecord是代表僅僅利用這個(gè)session進(jìn)行錄音操作,而需要播放操作的話是可以設(shè)置成AVAudioSessionCategoryPlayAndRecord或AVAudioSessionCategoryPlayBlack,兩者區(qū)別一個(gè)是可以錄音和播放,另一個(gè)是可以在后臺(tái)播放(即靜音后仍然可以播放語音)。

configRecord是用于配置整個(gè)AVAudioRecoder,包括權(quán)限獲取、代理源設(shè)置、是否記錄音量表等。

directoryURL是用于配置文件保存地址。

private func configAVAudioSession() {? ? ? ? let session = AVAudioSession.sharedInstance()? ? ? ? do { try session.setCategory(AVAudioSessionCategoryPlayAndRecord, with: .defaultToSpeaker) }? ? ? ? catch { print("session config failed") }

}? ?

private func configRecord() {? ? ? ? AVAudioSession.sharedInstance().requestRecordPermission { (allowed) in

if !allowed {? ? ? ? ? ? ? ? return

}

}? ? ? ? let session = AVAudioSession.sharedInstance()? ? ? ? do { try session.setCategory(AVAudioSessionCategoryPlayAndRecord, with: .defaultToSpeaker) }? ? ? ? catch { print("session config failed") }? ? ? ? do {? ? ? ? ? ? self.recorder = try AVAudioRecorder(url: self.directoryURL()!, settings: self.recorderSetting)? ? ? ? ? ? self.recorder.delegate = self

self.recorder.prepareToRecord()? ? ? ? ? ? self.recorder.isMeteringEnabled = true

} catch {? ? ? ? ? ? print(error.localizedDescription)

}? ? ? ? do { try AVAudioSession.sharedInstance().setActive(true) }? ? ? ? catch { print("session active failed") }

}? ?

private func directoryURL() -> URL? {? ? ? ? // do something ...

return soundFileURL

}

記錄音頻數(shù)據(jù)

在開始錄音后,利用我們剛剛配置的定時(shí)器不斷獲取averagePower,并保存到數(shù)組之中。

updateMeters被定時(shí)器調(diào)用,不斷將recorder中記錄的音量數(shù)據(jù)保存到soundMeter數(shù)組中。

addSoundMeter用于完成添加數(shù)據(jù)的工作。

private func updateMeters() {

recorder.updateMeters()

recordTime += updateFequency

addSoundMeter(item: recorder.averagePower(forChannel: 0))

}? ?

private func addSoundMeter(item: Float) {? ? ? ? if soundMeters.count < soundMeterCount {

soundMeters.append(item)

} else {? ? ? ? ? ? for (index, _) in soundMeters.enumerated() {? ? ? ? ? ? ? ? if index < soundMeterCount - 1 {

soundMeters[index] = soundMeters[index + 1]

}

}? ? ? ? ? ? // 插入新數(shù)據(jù)

soundMeters[soundMeterCount - 1] = item? ? ? ? ? ? NotificationCenter.default.post(name: NSNotification.Name.init("updateMeters"), object: soundMeters)

}

}

開始繪制波形圖

現(xiàn)在我們已經(jīng)獲取了我們需要的所有數(shù)據(jù),可以開始繪制波形圖了。這時(shí)候讓我們轉(zhuǎn)到MCVolumeView.swift文件中,在上一個(gè)步驟中,我們發(fā)送了一條叫做updateMeters的通知,目的就是為了通知MCVolumeView進(jìn)行波形圖的更新。

override init(frame: CGRect) {? ? ? ? super.init(frame: frame)

backgroundColor = UIColor.clear

contentMode = .redraw? //內(nèi)容模式為重繪,因?yàn)樾枰啻沃貜?fù)繪制音量表

NotificationCenter.default.addObserver(self, selector: #selector(updateView(notice:)), name: NSNotification.Name.init("updateMeters"), object: nil)

}? ?

@objc private func updateView(notice: Notification) {

soundMeters = notice.object as! [Float]

setNeedsDisplay()

}

當(dāng)setNeedsDisplay被調(diào)用之后,就會(huì)調(diào)用drawRect方法,在這里我們可以進(jìn)行繪制波形圖的操作。

noVoice和maxVolume是用于確保聲音的顯示范圍

波形圖的繪制使用CGContext進(jìn)行繪制,當(dāng)然也可以使用UIBezierPath進(jìn)行繪制。

override func draw(_ rect: CGRect) {? ? ? ? if soundMeters != nil && soundMeters.count > 0 {? ? ? ? ? ? let context = UIGraphicsGetCurrentContext()

context?.setLineCap(.round)

context?.setLineJoin(.round)

context?.setStrokeColor(UIColor.white.cgColor)? ? ? ? ? ?

let noVoice = -46.0 // 該值代表低于-46.0的聲音都認(rèn)為無聲音

let maxVolume = 55.0 // 該值代表最高聲音為55.0

// draw the volume...? ? ? ? ? ?

context?.strokePath()

}

}

柱狀波形圖的繪制

根據(jù)maxVolume和noVoice計(jì)算出每一條柱狀的高度,并移動(dòng)context所在的點(diǎn)進(jìn)行繪制

另外需要注意的是CGContext中坐標(biāo)點(diǎn)時(shí)反轉(zhuǎn)的,所以在進(jìn)行計(jì)算時(shí)需要將坐標(biāo)軸進(jìn)行反轉(zhuǎn)來計(jì)算。

case .bar:? ? ? ? ?

context?.setLineWidth(3)? ? ? for (index,item) in soundMeters.enumerated() {? ? ? ? let barHeight = maxVolume - (Double(item) - noVoice)? ? //通過當(dāng)前聲音表計(jì)算應(yīng)該顯示的聲音表高度

context?.move(to: CGPoint(x: index * 6 + 3, y: 40))

context?.addLine(to: CGPoint(x: index * 6 + 3, y: Int(barHeight)))

}

線狀波形圖的繪制

線狀與條狀一樣使用同樣的方法計(jì)算“高度”,但是在繪制條狀波形圖時(shí),是先畫線,再移動(dòng),而繪制條狀波形圖時(shí)是先移動(dòng)再畫線。

case .line:

context?.setLineWidth(1.5)? ? ? ? for (index, item) in soundMeters.enumerated() {? ? ? ? ? ? let position = maxVolume - (Double(item) - noVoice)? ? //計(jì)算對(duì)應(yīng)線段高度

context?.addLine(to: CGPoint(x: Double(index * 6 + 3), y: position))

context?.move(to: CGPoint(x: Double(index * 6 + 3), y: position))

}

}

進(jìn)一步完善我們的波形圖

在很多時(shí)候,錄音不單止是需要顯示波形圖,還需要我們展示目前錄音的時(shí)間和進(jìn)度,所以我們可以在波形圖上添加錄音的進(jìn)度條,所以我們轉(zhuǎn)向MCProgressView.swift文件進(jìn)行操作。

使用UIBezierPath配合CAShapeLayer進(jìn)行繪制。

maskPath是作為整個(gè)進(jìn)度路徑的蒙版,因?yàn)槲覀兊匿浺鬑UD不是規(guī)則的方形,所以需要使用蒙版進(jìn)度路徑進(jìn)行裁剪。

progressPath為進(jìn)度路徑,進(jìn)度的繪制方法為從左到右依次繪制。

animation是進(jìn)度路徑的繪制動(dòng)畫。

private func configAnimate() {? ? ? ? let maskPath = UIBezierPath(roundedRect: CGRect.init(x: 0, y: 0, width: frame.width, height: frame.height), cornerRadius: HUDCornerRadius)? ? ? ? let maskLayer = CAShapeLayer()

maskLayer.backgroundColor = UIColor.clear.cgColor

maskLayer.path = maskPath.cgPath

maskLayer.frame = bounds? ? ? ?

// 進(jìn)度路徑

/*

路徑的中心為HUD的中心,寬度為HUD的高度,從左往右繪制

*/

let progressPath = CGMutablePath()

progressPath.move(to: CGPoint(x: 0, y: frame.height / 2))

progressPath.addLine(to: CGPoint(x: frame.width, y: frame.height / 2))

progressLayer = CAShapeLayer()

progressLayer.frame = bounds

progressLayer.fillColor = UIColor.clear.cgColor //圖層背景顏色

progressLayer.strokeColor = UIColor(red: 0.29, green: 0.29, blue: 0.29, alpha: 0.90).cgColor? //圖層繪制顏色

progressLayer.lineCap = kCALineCapButt

progressLayer.lineWidth = HUDHeight

progressLayer.path = progressPath

progressLayer.mask = maskLayer

animation = CABasicAnimation(keyPath: "strokeEnd")

animation.duration = 60 //最大錄音時(shí)長(zhǎng)

animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)? ? //勻速前進(jìn)

animation.fillMode = kCAFillModeForwards

animation.fromValue = 0.0

animation.toValue = 1.0

animation.autoreverses = false

animation.repeatCount = 1

}

結(jié)語

以上就是我在繪制錄音波形圖的一些心得和看法,在demo中我還為錄音HUD加入了高斯模糊和陰影,讓HUD在展示上更具質(zhì)感,這些就略過不提了。雖然如此,但是這個(gè)錄音HUD我覺得還是有一些缺陷的,一來是和VC的耦合比較高,二是繪制線狀波形圖的效果并不是太理性,希望各位如果有更好的方法可以與我交流。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評(píng)論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,441評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,211評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,475評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,834評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,009評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,559評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,516評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,728評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評(píng)論 1 295
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,249評(píng)論 3 399
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,484評(píng)論 2 379

推薦閱讀更多精彩內(nèi)容