AVAudio is truncating at the end when trying to Overwrite from 10 to 15 seconds for a recording of 20 Seconds

Bonjour!
Hey guys, we are developing an Application that contains the Recordings with a functionality of Insert and Overwrite..
When I try to Overwrite what we are doing is::

  • Say, "Recoding.m4a" is the recording
  • When clicked on Overwrite we are separating the recording as "Recording_Start.m4a" and "Recording_End.m4a" depending on the point of Where OverWrite has to be done.
  • Then Start a new Recording for 5 Seconds and naming it as "Recording_new.m4a" (viz, from 10 secs to 15 secs).
  • Creating an Export Session and Exporting all the these files to the Documents Directory.
  • Now we are removing these 5 seconds from "Recording_End.m4a" and Renaming as "Recording_End_OverWrite.m4a"
  • Finally we are adding all these in an Array and Creating a track out of that. (viz, ["Recording_Start.m4a","Recording_new.m4a","Recording_End_OverWrite.m4a"]).

Developer Specs:

  • We are using XCode 10.o and iOS12.o
  • Using Apple's AVFoundation Kit and AudioToolBox.
  • Recording audio in "MONO" format.


Will be very happie to hear if some one can raise a concern that why 'am I doing all this, Just to Overwrite a Recording and Comeup with few magical words.
No worries if developers can use this code if it Works for you with my legacy 😝
Here you go I'm sharing a Small Snippet like what we doing exactly.!

class func mergeAudioFiles(originalURL: URL, replacingURL:URL, startTime:CMTime, folderName:String, caseNumber:String) {
        
        let options = [AVURLAssetPreferPreciseDurationAndTimingKey:true]
        let originalAsset = AVURLAsset(url: originalURL, options: options)
        let replacingAsset = AVURLAsset(url: replacingURL, options: options)
        print("####### Original Asset Duration",originalAsset.duration.scaled)
        print("####### Replacing Asset Duration",replacingAsset.duration.scaled)
        
        if let replacingTrack = replacingAsset.tracks.first, let originalTrack = originalAsset.tracks.first {
            print("####### Original Track Duration",originalTrack.timeRange.duration.scaled)
            print("####### Replacing Track Duration",replacingTrack.timeRange.duration.scaled)
            
            let duration = replacingTrack.timeRange.duration.scaled
            let replacingRange = CMTimeRange(start: startTime, duration: duration)
            
           let urlV2 = mergeAudioV2(audioFileUrl: originalURL, folderNumber: folderName, caseNumber: caseNumber)
           // let jugaadAsset = AVURLAsset(url: urlV2, options: options)
            let jugaadAsset = AVURLAsset(url: urlV2)
            trimOriginalAssetFor(asset: jugaadAsset, replacingRange: replacingRange, replacingURL: replacingURL, folderName: folderName,  caseNumber:caseNumber) { (finalURLs) in
                exportFinalOutput(from: finalURLs, folderName: folderName,  caseNumber:caseNumber, completionHandler: { (isCompleted) in
                    if isCompleted {
                        print("Merge Successful")
                    } else {
                        print("Merge Failed")
                    }
                })
            }
        }
    }
}

//MARK:- Private
extension RecordingManager {
    private class func trimOriginalAssetFor(asset:AVAsset, replacingRange:CMTimeRange, replacingURL:URL, folderName:String, caseNumber:String, completionHandler handler: @escaping (_ finalURLs:[URL]) -> Void) {
        
        var finalURLs = [URL]()
        
        let startURL = getCurrentDirectory(with: folderName, caseNumber: caseNumber).appendingPathComponent("StartTrim.m4a")
        let endURL = getCurrentDirectory(with: folderName, caseNumber: caseNumber).appendingPathComponent("EndTrim.m4a")
        FileManager.removeFileIfAlreadyExists(at: startURL)
        FileManager.removeFileIfAlreadyExists(at: endURL)
        
        
        if let originalTrack = asset.tracks(withMediaType: AVMediaType.audio).first {
            
            //Range for first file
            let rangeStart = CMTimeRange(start: kCMTimeZero, duration: replacingRange.start.scaled)
            let endFileDuration = originalTrack.timeRange.duration.scaled - (replacingRange.start.scaled+replacingRange.duration.scaled)
            print("####### End Trimming Expected Duration",endFileDuration)
            let endFileStartTime = replacingRange.start.scaled+replacingRange.duration.scaled
            print("####### End file Start Time", endFileStartTime)
            let rangeEnd = CMTimeRange(start: endFileStartTime.scaled, duration: endFileDuration.scaled)
            
            //Overwrite
            if replacingRange.duration < originalTrack.timeRange.duration-replacingRange.start && replacingRange.start.value != 0 {
                //Divide the original file into 2 files.
                
                //Range for second file
                
                //Export the first file and after completion start exporting the second file
                asset.export(to: startURL, timeRange: rangeStart) { (isCompleted) in
                    if isCompleted {
                        //Second file export
                        asset.export(to: endURL, timeRange: rangeEnd) { (isCompleted) in
                            if isCompleted {
                                finalURLs.append(contentsOf:[startURL,replacingURL,endURL])
                                handler(finalURLs)
                            }
                        }
                    }
                }
                //Insert
            } else if replacingRange.start.value == 0 {
                asset.export(to: startURL, timeRange: rangeEnd) { (isCompleted) in
                    if isCompleted {
                        //Second file export
                        finalURLs.append(contentsOf:[replacingURL,startURL])
                        handler(finalURLs)
                    }
                }
            } else {
                asset.export(to: startURL, timeRange: rangeStart) { (isCompleted) in
                    if isCompleted {
                        //Second file export
                        finalURLs.append(contentsOf:[startURL,replacingURL])
                        handler(finalURLs)
                    }
                }
            }
        }
        
    }
    
    private class func exportFinalOutput(from urls:[URL] , folderName:String, caseNumber:String, completionHandler handler: @escaping (Bool) -> Void) {
        let options = [AVURLAssetPreferPreciseDurationAndTimingKey:true]
        let composition = AVMutableComposition(urlAssetInitializationOptions: options)
        let compositionAudioTrack = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: kCMPersistentTrackID_Invalid)
        
        //Export Trimmed Audio Files
        do {
            try compositionAudioTrack?.append(urls: urls)
        } catch {
            print("Error Occured while apending", error.localizedDescription)
        }
        
        //Export Final Audio //Dictation_10162018095338_20424047
        let finalAudioURL = getCurrentDirectory(with: folderName, caseNumber: caseNumber).appendingPathComponent("Dictation_\(folderName)_\(caseNumber).m4a")
        FileManager.removeFileIfAlreadyExists(at: finalAudioURL)
        FileManager.removeAllFilesExceptNewRecording(withFolderName: folderName, caseNumber: caseNumber)
        composition.export(to: finalAudioURL, completionHandler: handler)
    }
    
}

func getDocumentsDirectory() -> URL {
    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    let documentsDirectory = paths[0]
    return documentsDirectory
}

func getCurrentDirectory(with folderName:String, caseNumber:String)  -> URL {
    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    let documentsDirectory = paths[0]
    let appendedURL = documentsDirectory.appendingPathComponent(caseNumber).appendingPathComponent(folderName)
    return appendedURL
}

extension AVMutableCompositionTrack {
    func append(urls: [URL]) throws {
        for url in urls {
            let newAsset = AVURLAsset(url: url)
            let range = CMTimeRange(start:kCMTimeZero, duration:newAsset.duration)
            let end = timeRange.end
            if let track = newAsset.tracks(withMediaType: AVMediaType.audio).first {
                try insertTimeRange(range, of: track, at: end)
            }
        }
    }
}

extension AVAsset {
    func export(to url:URL, timeRange: CMTimeRange? = nil ,completionHandler handler: @escaping (Bool) -> Void) {
        if let assetExportSession = AVAssetExportSession(asset: self, presetName: AVAssetExportPresetAppleM4A) {
            assetExportSession.outputFileType = AVFileType.m4a
            assetExportSession.audioTimePitchAlgorithm = .timeDomain
            assetExportSession.outputURL = url
            if let range = timeRange {
                assetExportSession.timeRange = range
                print("Exporting Range from",range.start,"Duration",range.duration,url.lastPathComponent)
            }
            assetExportSession.exportAsynchronously(completionHandler: {
                if assetExportSession.status == .completed {
                    handler(true)
                } else if let error = assetExportSession.error {
                    print("STATUS:",assetExportSession.status,"ERROR:",error.localizedDescription,"URL",url)
                    handler(false)
                } else {
                    handler(false)
                }
            })
        } else {
            print("Export failed")
        }
    }
    
}

extension FileManager {
    
    class func removeFileIfAlreadyExists(at url:URL) {
        do {
            try FileManager.default.removeItem(at: url)
        } catch {
            
        }
        
    }
    
    class func removeAllFilesExceptNewRecording(withFolderName : String, caseNumber : String)
    {
        let newRecording = "Dictation_New_\(withFolderName).m4a"
        let newOverwrite = "Dictation_\(withFolderName)_\(caseNumber)_newOverWrite.m4a"
        
        let startTrim = getCurrentDirectory(with: withFolderName, caseNumber: caseNumber).appendingPathComponent("StartTrim.m4a")
        let endTrim = getCurrentDirectory(with: withFolderName, caseNumber: caseNumber).appendingPathComponent("EndTrim.m4a")
        let newRec = getCurrentDirectory(with: withFolderName, caseNumber: caseNumber).appendingPathComponent(newRecording)
        let newOver = getCurrentDirectory(with: withFolderName, caseNumber: caseNumber).appendingPathComponent(newOverwrite)
        
        do
        {
            try FileManager.default.removeItem(at: startTrim)
        }
        catch
        {
            print("File not Found in Directory..!!")
        }
        do
        {
            try FileManager.default.removeItem(at: endTrim)
        }
        catch
        {
            print("File not Found in Directory..!!")
        }
        do
        {
             try FileManager.default.removeItem(at: newRec)
        }
        catch
        {
            print("File not Found in Directory..!!")
        }
        do
        {
             try FileManager.default.removeItem(at: newOver)
        }
        catch
        {
            print("File not Found in Directory..!!")
        }
    }
}

extension CMTime {
    var scaled : CMTime {
        return self.convertScale(60000, method: CMTimeRoundingMethod.roundAwayFromZero)
    }
}