美文网首页
文件音频编辑

文件音频编辑

作者: 沐浴阳光的鱼 | 来源:发表于2020-12-25 17:07 被阅读0次

    很长时间没有写过文章了,原因很多。。。。
    对于音频编辑的事情,我最近经历了很多。相信大家都知道,音频相关的知识很深很多,而我也是之前从没有接触过音视频编辑的人,对音频流知道的就甚少了。。。网上也没有搜到有完整的文章。最近查文档看源码以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

    相关文章

      网友评论

          本文标题:文件音频编辑

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