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

效果图

1.gif

条状波形图

2.gif


线状波形图

配置AvAudioSession

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

相关属性

  • recorderSetting用于设定录音音质等相关数据。

  • timer以及updateFequency用于定时更新波形图。

  • soundMetersoundMeterCount用于保存音量表数组。

  • recordTime用于记录录音时间,可以用于判断录音时间是否达到要求等进一波需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    /// 录音器
     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进行录音操作,而需要播放操作的话是可以设置成AVAudioSessionCategoryPlayAndRecord或AVAudioSessionCategoryPlayBlack,两者区别一个是可以录音和播放,另一个是可以在后台播放(即静音后仍然可以播放语音)。

  • configRecord是用于配置整个AVAudioRecoder,包括权限获取、代理源设置、是否记录音量表等。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  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用于完成添加数据的工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    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进行波形图的更新。

1
2
3
4
5
6
7
8
9
    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方法,在这里我们可以进行绘制波形图的操作。

  • noVoice和maxVolume是用于确保声音的显示范围

  • 波形图的绘制使用CGContext进行绘制,当然也可以使用UIBezierPath进行绘制。

1
2
3
4
5
6
7
8
9
10
11
12
    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()
         }
     }

柱状波形图的绘制

  • 根据maxVolume和noVoice计算出每一条柱状的高度,并移动context所在的点进行绘制

  • 另外需要注意的是CGContext中坐标点时反转的,所以在进行计算时需要将坐标轴进行反转来计算。

1
2
3
4
5
    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)))
        }

线状波形图的绘制

  • 线状与条状一样使用同样的方法计算“高度”,但是在绘制条状波形图时,是先画线,再移动,而绘制条状波形图时是先移动再画线。

1
2
3
4
5
6
    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是进度路径的绘制动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    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的耦合比较高,二是绘制线状波形图的效果并不是太理性,希望各位如果有更好的方法可以与我交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值