音频编辑功能结构
一个音频编辑的基本功能是要包含上面图片中的这些功能的。
解释顺序如下:编辑操作、音频播放、音频保存
编辑操作
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编写了一半又跑来搞了下这个,学习过程还是很有趣