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

在開發(fā)中我遇到有根據(jù)錄音來繪制波形圖的需求,所以這篇文章就教大家利用swift和AVAudioSession來繪制波形圖。

本文Demo

效果圖

條狀波形圖

線狀波形圖

配置AvAudioSession

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

相關(guān)屬性

  • recorderSetting用于設(shè)定錄音音質(zhì)等相關(guān)數(shù)據(jù)枯芬。
  • timer以及updateFequency用于定時更新波形圖。
  • soundMetersoundMeterCount用于保存音量表數(shù)組徘意。
  • recordTime用于記錄錄音時間,可以用于判斷錄音時間是否達(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ì)量
    /// 錄音計時器
    private var timer: Timer?
    /// 波形更新間隔
    private let updateFequency = 0.05
    /// 聲音數(shù)據(jù)數(shù)組
    private var soundMeters: [Float]!
    /// 聲音數(shù)據(jù)數(shù)組容量
    private let soundMeterCount = 10
    /// 錄音時間
    private var recordTime = 0.00

AvAudioSession相關(guān)配置

  • configAVAudioSession用于配置AVAudioSession椎咧,其中AVAudioSessionCategoryRecord是代表僅僅利用這個session進(jìn)行錄音操作,而需要播放操作的話是可以設(shè)置成AVAudioSessionCategoryPlayAndRecordAVAudioSessionCategoryPlayBlack,兩者區(qū)別一個是可以錄音和播放把介,另一個是可以在后臺播放(即靜音后仍然可以播放語音)勤讽。
  • configRecord是用于配置整個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ù)

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

  • updateMeters被定時器調(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ù)妹懒,可以開始繪制波形圖了。這時候讓我們轉(zhuǎn)到MCVolumeView.swift文件中双吆,在上一個步驟中眨唬,我們發(fā)送了一條叫做updateMeters的通知,目的就是為了通知MCVolumeView進(jìn)行波形圖的更新好乐。

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = UIColor.clear
        contentMode = .redraw   //內(nèi)容模式為重繪匾竿,因為需要多次重復(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)用之后,就會調(diào)用drawRect方法蔚万,在這里我們可以進(jìn)行繪制波形圖的操作岭妖。

  • noVoicemaxVolume是用于確保聲音的顯示范圍
  • 波形圖的繪制使用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ù)maxVolumenoVoice計算出每一條柱狀的高度昵慌,并移動context所在的點(diǎn)進(jìn)行繪制
  • 另外需要注意的是CGContext中坐標(biāo)點(diǎn)時反轉(zhuǎn)的假夺,所以在進(jìn)行計算時需要將坐標(biāo)軸進(jìn)行反轉(zhuǎn)來計算。
    case .bar:          
        context?.setLineWidth(3)
       for (index,item) in soundMeters.enumerated() {
        let barHeight = maxVolume - (Double(item) - noVoice)    //通過當(dāng)前聲音表計算應(yīng)該顯示的聲音表高度
            context?.move(to: CGPoint(x: index * 6 + 3, y: 40))
            context?.addLine(to: CGPoint(x: index * 6 + 3, y: Int(barHeight)))
       }

線狀波形圖的繪制

  • 線狀與條狀一樣使用同樣的方法計算“高度”斋攀,但是在繪制條狀波形圖時已卷,是先畫線,再移動淳蔼,而繪制條狀波形圖時是先移動再畫線侧蘸。
    case .line:
        context?.setLineWidth(1.5)
        for (index, item) in soundMeters.enumerated() {
            let position = maxVolume - (Double(item) - noVoice)     //計算對應(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)一步完善我們的波形圖

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

  • 使用UIBezierPath配合CAShapeLayer進(jìn)行繪制析桥。
  • maskPath是作為整個進(jìn)度路徑的蒙版,因為我們的錄音HUD不是規(guī)則的方形艰垂,所以需要使用蒙版進(jìn)度路徑進(jìn)行裁剪泡仗。
  • progressPath為進(jìn)度路徑,進(jìn)度的繪制方法為從左到右依次繪制猜憎。
  • animation是進(jìn)度路徑的繪制動畫娩怎。
    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 //最大錄音時長
        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ì)感柬讨,這些就略過不提了崩瓤。雖然如此,但是這個錄音HUD我覺得還是有一些缺陷的踩官,一來是和VC的耦合比較高却桶,二是繪制線狀波形圖的效果并不是太理性,希望各位如果有更好的方法可以與我交流蔗牡。


想了解更多內(nèi)容可以查看我的博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末颖系,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子辩越,更是在濱河造成了極大的恐慌嘁扼,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件黔攒,死亡現(xiàn)場離奇詭異趁啸,居然都是意外死亡强缘,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進(jìn)店門莲绰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來欺旧,“玉大人姑丑,你說我怎么就攤上這事蛤签。” “怎么了栅哀?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵震肮,是天一觀的道長。 經(jīng)常有香客問我留拾,道長戳晌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任痴柔,我火速辦了婚禮沦偎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘咳蔚。我一直安慰自己豪嚎,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布谈火。 她就那樣靜靜地躺著侈询,像睡著了一般。 火紅的嫁衣襯著肌膚如雪糯耍。 梳的紋絲不亂的頭發(fā)上扔字,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天,我揣著相機(jī)與錄音温技,去河邊找鬼革为。 笑死,一個胖子當(dāng)著我的面吹牛舵鳞,可吹牛的內(nèi)容都是我干的震檩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼系任,長吁一口氣:“原來是場噩夢啊……” “哼恳蹲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起俩滥,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤嘉蕾,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后霜旧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體错忱,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡儡率,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了以清。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片儿普。...
    茶點(diǎn)故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖掷倔,靈堂內(nèi)的尸體忽然破棺而出眉孩,到底是詐尸還是另有隱情,我是刑警寧澤勒葱,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布浪汪,位于F島的核電站,受9級特大地震影響凛虽,放射性物質(zhì)發(fā)生泄漏死遭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一凯旋、第九天 我趴在偏房一處隱蔽的房頂上張望呀潭。 院中可真熱鬧,春花似錦至非、人聲如沸钠署。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽踏幻。三九已至,卻和暖如春戳杀,著一層夾襖步出監(jiān)牢的瞬間该面,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工信卡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留隔缀,地道東北人。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓傍菇,卻偏偏與公主長得像猾瘸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子丢习,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評論 2 356

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