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

效果图

条状波形图

线状波形图

配置AvAudioSession

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

相关属性

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

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

soundMeter和soundMeterCount用于保存音量表数组。

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进行录音操作,而需要播放操作的话是可以设置成AVAudioSessionCategoryPlayAndRecord或AVAudioSessionCategoryPlayBlack,两者区别一个是可以录音和播放,另一个是可以在后台播放(即静音后仍然可以播放语音)。

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方法,在这里我们可以进行绘制波形图的操作。

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

波形图的绘制使用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()

}

}

柱状波形图的绘制

根据maxVolume和noVoice计算出每一条柱状的高度,并移动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的耦合比较高,二是绘制线状波形图的效果并不是太理性,希望各位如果有更好的方法可以与我交流。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容