美文网首页iOS开发@IT·互联网iOS Developer
在iOS中绘制录音音频波形图

在iOS中绘制录音音频波形图

作者: Dywane | 来源:发表于2018-03-04 14:22 被阅读364次

    在开发中我遇到有根据录音来绘制波形图的需求,所以这篇文章就教大家利用swift和AVAudioSession来绘制波形图。

    本文Demo

    效果图

    条状波形图
    线状波形图

    配置AvAudioSession

    绘制波形图前首先需要配置好AVAudioSession,同时需要建立一个数组去保存音量数据。

    相关属性

    • recorderSetting用于设定录音音质等相关数据。
    • timer以及updateFequency用于定时更新波形图。
    • soundMetersoundMeterCount用于保存音量表数组。
    • recordTime用于记录录音时间,可以用于判断录音时间是否达到要求等进一波需求。
         /// 录音器
        private var recorder: AVAudioRecorder!
        /// 录音器设置
        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))]//声音质量
        /// 录音计时器
        private var timer: Timer?
        /// 波形更新间隔
        private let updateFequency = 0.05
        /// 声音数据数组
        private var soundMeters: [Float]!
        /// 声音数据数组容量
        private let soundMeterCount = 10
        /// 录音时间
        private var recordTime = 0.00
    
    

    AvAudioSession相关配置

    • configAVAudioSession用于配置AVAudioSession,其中AVAudioSessionCategoryRecord是代表仅仅利用这个session进行录音操作,而需要播放操作的话是可以设置成AVAudioSessionCategoryPlayAndRecordAVAudioSessionCategoryPlayBlack,两者区别一个是可以录音和播放,另一个是可以在后台播放(即静音后仍然可以播放语音)。
    • configRecord是用于配置整个AVAudioRecoder,包括权限获取、代理源设置、是否记录音量表等。
    • 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
        }
    

    记录音频数据

    在开始录音后,利用我们刚刚配置的定时器不断获取averagePower,并保存到数组之中。

    • updateMeters被定时器调用,不断将recorder中记录的音量数据保存到soundMeter数组中。
    • addSoundMeter用于完成添加数据的工作。
        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]
                    }
                }
                // 插入新数据
                soundMeters[soundMeterCount - 1] = item
                NotificationCenter.default.post(name: NSNotification.Name.init("updateMeters"), object: soundMeters)
            }
        }
    

    开始绘制波形图

    现在我们已经获取了我们需要的所有数据,可以开始绘制波形图了。这时候让我们转到MCVolumeView.swift文件中,在上一个步骤中,我们发送了一条叫做updateMeters的通知,目的就是为了通知MCVolumeView进行波形图的更新。

        override init(frame: CGRect) {
            super.init(frame: frame)
            backgroundColor = UIColor.clear
            contentMode = .redraw   //内容模式为重绘,因为需要多次重复绘制音量表
            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()
        }
    

    setNeedsDisplay被调用之后,就会调用drawRect方法,在这里我们可以进行绘制波形图的操作。

    • noVoicemaxVolume是用于确保声音的显示范围
    • 波形图的绘制使用CGContext进行绘制,当然也可以使用UIBezierPath进行绘制。
        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的声音都认为无声音
                let maxVolume = 55.0    // 该值代表最高声音为55.0
                
                  // draw the volume...            
                  
                context?.strokePath()
            }
        }
    

    柱状波形图的绘制

    • 根据maxVolumenoVoice计算出每一条柱状的高度,并移动context所在的点进行绘制
    • 另外需要注意的是CGContext中坐标点时反转的,所以在进行计算时需要将坐标轴进行反转来计算。
        case .bar:          
            context?.setLineWidth(3)
           for (index,item) in soundMeters.enumerated() {
            let barHeight = maxVolume - (Double(item) - noVoice)    //通过当前声音表计算应该显示的声音表高度
                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)     //计算对应线段高度
                context?.addLine(to: CGPoint(x: Double(index * 6 + 3), y: position))
                context?.move(to: CGPoint(x: Double(index * 6 + 3), y: position))
            }
        }
    

    进一步完善我们的波形图

    在很多时候,录音不单止是需要显示波形图,还需要我们展示目前录音的时间和进度,所以我们可以在波形图上添加录音的进度条,所以我们转向MCProgressView.swift文件进行操作。

    • 使用UIBezierPath配合CAShapeLayer进行绘制。
    • maskPath是作为整个进度路径的蒙版,因为我们的录音HUD不是规则的方形,所以需要使用蒙版进度路径进行裁剪。
    • progressPath为进度路径,进度的绘制方法为从左到右依次绘制。
    • animation是进度路径的绘制动画。
        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
            
            // 进度路径
            /*
             路径的中心为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)    //匀速前进
            animation.fillMode = kCAFillModeForwards
            animation.fromValue = 0.0
            animation.toValue = 1.0
            animation.autoreverses = false
            animation.repeatCount = 1
        }
    
    

    结语

    以上就是我在绘制录音波形图的一些心得和看法,在demo中我还为录音HUD加入了高斯模糊和阴影,让HUD在展示上更具质感,这些就略过不提了。虽然如此,但是这个录音HUD我觉得还是有一些缺陷的,一来是和VC的耦合比较高,二是绘制线状波形图的效果并不是太理性,希望各位如果有更好的方法可以与我交流。


    想了解更多内容可以查看我的博客

    相关文章

      网友评论

        本文标题:在iOS中绘制录音音频波形图

        本文链接:https://www.haomeiwen.com/subject/vhnffftx.html