问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

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

发布网友 发布时间:2022-11-02 19:14

我来回答

1个回答

热心网友 时间:2023-10-24 11:29

效果图

条状波形图

线状波形图

配置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.ration = 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的耦合比较高,二是绘制线状波形图的效果并不是太理性,希望各位如果有更好的方法可以与我交流。
声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
桃李芬芳的近义词是? 请会答正确。 急... 墨西哥很混乱吗 为什么我的OPPOR9手机连接到任何蓝牙设备放歌曲,都没有办法在蓝牙设备... OPPOA9如何连接酷狗与手机蓝牙? 华为荣耀3X 白色畅玩版的声音调至最大声仍很小声 荣耀3x刷机后卸载了一些系统软件,然后就无法开机,一直停留在开机界面... 平安富赢金生年金保险值得买吗?最全产品测评! 收音机音量旋钮音量最大还是小 德生pl_450收音机音量电位器声音惑大惑小,电位器的型号是什么_百度知 ... 浙江金融学院有什么专业 根据词语不同意思造句 名堂一花样名目二成就结果三道理内容 为什么每个人都要经历读书啊??? 马云经历书多失败为何始终没有放弃自己的梦想。 请问日文职务经历书中的&quot;役割等&quot;以及&quot;实绩その他&quot;是什麼意思呢? 求某位名人写的自己之前的大学生活和经历书籍 中颖电子2022目标价?中颖电子投资股票股吧?中颖电子 2021 分红派息? 最新中颖电子股票值得投资吗?中颖电子2021年二季度报?中颖电子股票是多少钱一股? 什么数据可以称为大数据 zara官网怎么打不开 为什么我的体脂秤蓝牙连接不上手机。? 老当益壮的主人公是谁 著名的马援将军老当益壮的意义 老当益壮指的是谁 老当益壮典故介绍 云商平台是什么东西 h6不系安全带车子不动怎么设置 帮我找找六年级800道的口算题 六年级下册口算题 带答案 你觉的那个名字好一点? 杨正琪拼音怎么写? 我叫胡正琪,谁能帮我取一下英文名? 早晚读只上语文英语合理吗? 根号2的立方等于多少 防冻液放了3年会过期吗 泌乳素16.68正常吗 力洛克 钢带重量 T41.1.483.33 长城的防冻液多久换一次? op1105手机截屏在哪里 我用的op1105使联通和移动那一个信号好些我我用联通的卡,信号相当不好,是这个手机不支持,还是联 opop1105天气冷了充电为什么充不满 opop1105怎么样格式化 op1105手机软件锁怎么解除 op价位最低的电信4g手机是多少钱 如何在网上报名自考计算机网络专业?广外自考学位证怎么办? 请问家具企业画册上的二维码有什么作用? 做行业内部的宣传杂志需要办什么手续? 一个手机号怎么申请两个 一个手机号如何申请两个 一个手机号码怎么申请两个 怎样用同一个手机号申请两个? 一个手机号码可以申请二个吗?如何申请? 一个手机号码怎么申请两个? 多美鲜全脂牛奶一盒7.9元,整箱多少钱