文件音频编辑

很长时间没有写过文章了,原因很多。。。。
对于音频编辑的事情,我最近经历了很多。相信大家都知道,音频相关的知识很深很多,而我也是之前从没有接触过音视频编辑的人,对音频流知道的就甚少了。。。网上也没有搜到有完整的文章。最近查文档看源码以AudioEngine的方式弄了一套简单的流方式的demo出来,供大家共同讨论使用,也方便后面新接触的朋友吧。
我喜欢开门见山,所以。。。走起!!!

音频编辑功能结构


一个音频编辑的基本功能是要包含上面图片中的这些功能的。
解释顺序如下:编辑操作、音频播放、音频保存

编辑操作

1.ActionModel

对于编辑,首先要记录每次编辑所对应的起始点和结束点即beginFrame和endFrame,还要记录一个播放的位置 seekFrame。创建一个model用来记录每次的操作

class WQAudioEditModel: NSObject {
    //开始
    var beginFrame:Int64! = 0
    //结束
    var endFrame:Int64! = 0
    //从哪个位置开始播放
    var seekFrame:Int64! = 0
    
    var fileUrl:URL!
    
    var audioFile:AVAudioFile!

    var editAction:AudioEditActionEnum! = AudioEditActionEnum.none

class func model(bgFrame:Int64,edFrame:Int64,editAction:AudioEditActionEnum,fileUrl:URL) -> WQAudioEditModel {
        let model : WQAudioEditModel = WQAudioEditModel.init()
        model.beginFrame    = bgFrame
        model.endFrame      = edFrame
        model.seekFrame     = bgFrame
        model.editAction    = editAction
        model.fileUrl       = fileUrl
        do {
            try model.audioFile     = AVAudioFile.init(forReading: fileUrl)
        } catch  {
            
        }
        return model
    }
}

beginFrame和endFrame以及seekFrame均为记录原文件音频流位置

2.Action

默认原音频为第一个存入数组

 private func config() {
        self.editModelArray = NSMutableArray.init()
        let firstModel = WQAudioEditModel.model(bgFrame: 0, edFrame: 0, editAction: .none,fileUrl: self.defaultUrl)
        firstModel.endFrame = firstModel.audioFile.length
        self.editModelArray.add(firstModel)
        
    }

拷贝
要创建一个copyArray用来存储拷贝的model,如果音频只有一段,则直接根据beginFrame和endFrame进行读取,多段的情况下要考虑不同情况,这里只说多片的情况,其他类似



这是拷贝之后,存储到copyArray中是三个model,即第1片的后半部分,完整的第2片,第3片的前半部分。

func copyAction(firstFrame:Int64,endFrame:Int64) {
        self.copyArray = NSMutableArray.init()
        let cpLength = endFrame - firstFrame
        
        var realLoc:Int64 = 0
        
        for model in self.editModelArray {
            let aeModel = model as! WQAudioEditModel
            let modelLength = aeModel.endFrame - aeModel.beginFrame
            
            if realLoc + modelLength > firstFrame{//firstFrame在此model中
                if realLoc + modelLength >= endFrame{//endFrame也在model中
                    let cpFirstFrame:Int64 = firstFrame - realLoc + aeModel.beginFrame
                    let cpEndFrame:Int64 = cpFirstFrame + cpLength
                    let cpModel = WQAudioEditModel.model(bgFrame: cpFirstFrame, edFrame: cpEndFrame, editAction: .copy,fileUrl: aeModel.fileUrl)
                    self.copyArray.add(cpModel.reinitModel())
                    break
                }else if(realLoc + modelLength < endFrame){//endframe在下一个model中
                    let cpFirstFrame:Int64 = firstFrame - realLoc + aeModel.beginFrame
                    //先添加firstFrame所在的model的部分
                    self.copyArray.add(WQAudioEditModel.model(bgFrame: cpFirstFrame, edFrame: aeModel.endFrame, editAction: .copy,fileUrl: aeModel.fileUrl))
                    //获取endframe
                    let index = self.editModelArray.index(of: model)
                    let lastArray = self.editModelArray.subarray(with: NSMakeRange(index + 1, self.editModelArray.count - index - 1))
                    
                    for lastModel in lastArray{
                        let lastM = lastModel as! WQAudioEditModel
                        let lastMLength = lastM.endFrame - lastM.beginFrame
                        //找endFrame的所在model
                        if realLoc + lastMLength > endFrame{
                            self.copyArray.add(WQAudioEditModel.model(bgFrame: lastM.beginFrame, edFrame: endFrame - realLoc + lastM.beginFrame, editAction: .copy,fileUrl: aeModel.fileUrl))
                            break;
                        }else{
                            realLoc = realLoc + lastMLength
                            self.copyArray.add(lastM.reinitModel())
                        }
                        
                        
                    }
                    break
                    
                }
                
            }else{
                realLoc = modelLength + realLoc
            }

            
        }
        
    }

粘贴
方式和复制类似,情况主要体现在其实位置在某一段中间,要将该段分为两段,然后插入到中间



即将片段1分为前段和后段两个model

func pasteAction(locFrame:Int64) {
        if self.copyArray.count > 0{
            var realLoc:Int64 = 0
            let forModel = NSArray.init(array: self.editModelArray)
            for model in forModel {
                let aeModel = model as! WQAudioEditModel
                let modelLength = aeModel.endFrame - aeModel.beginFrame
                
                if realLoc + modelLength > locFrame{//寻找位置,位置在model的中部,分割model
                    let index = self.editModelArray.index(of: model)
                    if realLoc == locFrame{//起始点相同,则直接插入到前面
                        let lastarray = self.editModelArray.subarray(with: NSMakeRange(index, self.editModelArray.count - index))
                        self.editModelArray.removeObjects(in: lastarray)
                        self.editModelArray.addObjects(from: self.copyArray as! [Any])
                        self.editModelArray.addObjects(from: lastarray)
                    }else{
                        let newModel1 = WQAudioEditModel.model(bgFrame: aeModel.beginFrame, edFrame: locFrame - realLoc + aeModel.beginFrame, editAction: .none,fileUrl: aeModel.fileUrl)
                        self.editModelArray.replaceObject(at: index, with: newModel1)
                        let lastarray = self.editModelArray.subarray(with: NSMakeRange(index + 1, self.editModelArray.count - index - 1))
                        self.editModelArray.removeObjects(in: lastarray)
                        let newModel2 = WQAudioEditModel.model(bgFrame: locFrame - realLoc + aeModel.beginFrame, edFrame: aeModel.endFrame, editAction: .none,fileUrl: aeModel.fileUrl)
                        self.editModelArray.addObjects(from: self.copyArray as! [Any])
                        self.editModelArray.add(newModel2)
                        self.editModelArray.addObjects(from: lastarray)
                    }
                    
                    
                    break
                }else if realLoc + modelLength == locFrame{//寻找位置,位置在model的尾部,从其后插入位置
                    let index = self.editModelArray.index(of: model)
                    let lastarray = self.editModelArray.subarray(with: NSMakeRange(index + 1, self.editModelArray.count - index - 1))
                    self.editModelArray.removeObjects(in:lastarray)
                    self.editModelArray.addObjects(from: self.copyArray as! [Any])
                    self.editModelArray.addObjects(from: lastarray)
                    
                    break
                }else{
                    realLoc = realLoc + modelLength
                }
                
            }
            
        }
    }

剪切
剪切就是将复制和粘贴的方式进行结合了,只不过是把选中的部分去掉了而已,直接贴代码了哦

func cutAction(firstFrame:Int64,endFrame:Int64) {
        self.copyAction(firstFrame: firstFrame, endFrame: endFrame)
        var realLoc:Int64 = 0
        let forModel = NSArray.init(array: self.editModelArray)
        for model in forModel {
            let aeModel = model as! WQAudioEditModel
            let modelLength = aeModel.endFrame - aeModel.beginFrame
            
            if realLoc + modelLength > firstFrame{//firstFrame在此model中
                let index = self.editModelArray.index(of: model)
                if realLoc + modelLength >= endFrame{//endFrame也在model中
                    if realLoc == firstFrame{//起点相同,直接剪切
                        if realLoc + modelLength != endFrame{
                            let newModel2 = WQAudioEditModel.model(bgFrame: endFrame - realLoc, edFrame: aeModel.endFrame, editAction: .none,fileUrl: aeModel.fileUrl)
                            self.editModelArray.replaceObject(at: index, with: newModel2)
                        }
                    }else{
                        let newModel1 = WQAudioEditModel.model(bgFrame: aeModel.beginFrame, edFrame: firstFrame - realLoc, editAction: .none,fileUrl: aeModel.fileUrl)
                        self.editModelArray.replaceObject(at: index, with: newModel1)
                        if realLoc + modelLength != endFrame{
                            let newModel2 = WQAudioEditModel.model(bgFrame: endFrame - realLoc, edFrame: aeModel.endFrame, editAction: .none,fileUrl: aeModel.fileUrl)
                            self.editModelArray.insert(newModel2, at: index + 1)
                        }
                    }
                    break
                }else if(realLoc + modelLength < endFrame){//endframe在下一个model中
                    let cutFirstFrame:Int64 = firstFrame - realLoc + aeModel.beginFrame
                    let newFirstModel = WQAudioEditModel.model(bgFrame: aeModel.beginFrame, edFrame: cutFirstFrame, editAction: .none,fileUrl: aeModel.fileUrl)
                    self.editModelArray.replaceObject(at: index, with: newFirstModel)
                
                    //获取endframe
                    let index = self.editModelArray.index(of: model)
                    let lastArray = self.editModelArray.subarray(with: NSMakeRange(index+1, self.editModelArray.count - index - 1))
                    for lastModel in lastArray{
                        let lastM = lastModel as! WQAudioEditModel
                        let lastMLength = lastM.endFrame - lastM.beginFrame
                        //找endFrame的所在model
                        if realLoc + lastMLength > endFrame{
                            //创建新的model,去掉截取的部分
                            let newEndModel = WQAudioEditModel.model(bgFrame: endFrame - realLoc + lastM.beginFrame, edFrame: lastM.endFrame, editAction: .none,fileUrl: aeModel.fileUrl)
                            let cutIndex = self.editModelArray.index(of: lastM)
                            self.editModelArray.replaceObject(at: cutIndex, with: newEndModel)
                            break;
                        }else{
                            realLoc = realLoc + lastMLength
                            self.editModelArray.remove(lastModel)
                        }
                        
                        
                    }
                    break
                    
                }
                
            }else{
                realLoc = modelLength + realLoc
            }

            
        }
        
    }

这个时候所有操作过的音频段均保存在editArray数组中,开始准备播放了。

播放

这个时候需要把audioEngine请出来了,引擎主要管理整个过程的流以及流的操作,具体播放我们使用AVAudioPlayerNode进行播放,文件的读取是使用AVAudioFile。
对于播放,我目前未找到能够体现播放进度的属性或者方法,所以选择了个折中的方法,每次播放1024的长度,然后就可以监听进度了(如果有小伙伴知道怎么获得播放进度,请告知我,谢谢)
playnode播放的位置和长度是如下方法

open func scheduleSegment(_ file: AVAudioFile, startingFrame startFrame: AVAudioFramePosition, frameCount numberFrames: AVAudioFrameCount, at when: AVAudioTime?, completionHandler: AVAudioNodeCompletionHandler? = nil)

当这一片播放完之后completionHandler会调用,这个时候就可以去读取下一片继续播放,此时需要判断要播放我们editArray数组中的哪个片段,当数组中的所有片段均播放完毕,则为播放结束

@objc private func playNoti(noti:Notification) {//播放完毕再填充下一部分
        let model = noti.object as! WQAudioEditModel
        if self.playerNode.isPlaying == false{
            model.seekFrame = model.beginFrame
            NotificationCenter.default.removeObserver(self)
            if self.playFinishBlock != nil{
                self.playFinishBlock()
            }
            return
        }
        if(model.seekFrame >= model.endFrame){//结束
            //最后一片结束,查询下一个
            let index =  self.editActionTool.editModelArray.index(of: model)
            if index < self.editActionTool.editModelArray.count - 1{
                model.seekFrame = model.beginFrame
                let nextModel = self.editActionTool.editModelArray.object(at: index + 1) as! WQAudioEditModel
                self.realScheduleSegment(fromFrame: nextModel.seekFrame, length: self.segLength, model: nextModel)
            }else{
                print("nam")
                model.seekFrame = model.beginFrame
                NotificationCenter.default.removeObserver(self)
                if self.playFinishBlock != nil{
                    self.playFinishBlock()
                }
            }
        }else if (model.seekFrame + self.segLength  < model.endFrame){
            
        
            print("\(String(describing: model.seekFrame)),\(String(describing: model.endFrame))")
            self.realScheduleSegment(fromFrame: model.seekFrame, length: self.segLength, model: model)
            
        }else if (model.seekFrame + self.segLength >= model.endFrame ){//最后一片

            self.realScheduleSegment(fromFrame: model.seekFrame, length: model.endFrame - model.seekFrame, model: model)
        }
    }

存储

存储是使用AVAudioFile进行存储

func beginSave() {
        var writeFile:AVAudioFile! = nil
        do {
            writeFile = try AVAudioFile.init(forWriting: URL.init(string: self.fileUrl)!, settings: [AVFormatIDKey:NSNumber.init(value: kAudioFormatLinearPCM),AVNumberOfChannelsKey:NSNumber.init(value: 2),AVSampleRateKey:NSNumber.init(value: self.actionTool.getSampleRate())])
        } catch  {
            
        }
        if writeFile == nil{
            return
        }

        
//
        self.editPlay.stop()
        self.editPlay.audioEngine.pause()
        for model:WQAudioEditModel in self.actionTool.editModelArray as! [WQAudioEditModel]{
            let buffer:AVAudioPCMBuffer = AVAudioPCMBuffer.init(pcmFormat: model.audioFile.processingFormat, frameCapacity: AVAudioFrameCount(model.endFrame - model.beginFrame))!
            model.audioFile.framePosition = model.beginFrame
            do {
                try model.audioFile.read(into: buffer, frameCount: AVAudioFrameCount(model.endFrame - model.beginFrame))
            } catch  {

            }
            do {
                try writeFile.write(from: buffer)
            } catch  {

            }
        }
        print("\(String(describing: self.fileUrl))")
        do {
            try self.editPlay.audioEngine.start()
        } catch  {

        }
    }

至此,保存也结束了。是不是也挺简单的

结尾

对于AVAudioEngine,其底层是audiounit,我也是从audiounit看起,其较为c语言化,这一块的内容可以看下EZAudio的源码,内容很全面,存储这一块也可以看下AVAudioEngineOfflineRender,使用的exaudioUnit进行存储,比较简洁。 后来查到苹果推荐使用audioEngine,且功能上同audiounit相差无二,编写也少了很多代码,所以用audiounit编写了一半又跑来搞了下这个,学习过程还是很有趣

未来如果更多的朋友有兴趣,我们可以一起维护这个音频编辑的模块,增加更多的功能,不管是为了自己学习还是为了其他朋友使用。

参考资料
EZAudio : https://github.com/syedhali/EZAudio
AVAudioEngineOfflineRender:https://github.com/VladimirKravchenko/AVAudioEngineOfflineRender
develop:https://developer.apple.com/documentation/avfoundation/audio_playback_recording_and_processing/avaudioengine

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

推荐阅读更多精彩内容