AVMutableComposition has slightly different duration than saved m4a file when using AVAssetExportSession

199 Views Asked by At

I'm adding an an m4a audio file from the file system, loaded via an AVURLAsset, into an AVMutableComposition. If the loaded asset has a duration of 1s, adding it to the AVMutableComposition results in the composition also having a duration of 1s. This makes sense. However, after exporting the composition to a new file using AVAssetExportSession, the resulting m4a file has a duration about 0.05s less than the duration of the composition (0.95s vs. 1s).

This bug only happens when working with m4a files. If I work with caf, there's no difference in the duration of the exported file vs. the composition.

I originally discovered this bug when I was combining an existing audio file with a new audio file. Despite the AVMutableComposition reporting the correct duration, the file exported by AVAssetExportSession was ~0.05s shorter in duration. To simplify this question, I've removed the code that combines 2 existing audio files together, and made it so that we simply insert a new audio file into an empty mutable composition. The bug still occurs even for this simple case.

Here's my mutable composition code:

    let composition = AVMutableComposition()
    guard
      let compositionTrack = composition.addMutableTrack(
        withMediaType: .audio,
        preferredTrackID: kCMPersistentTrackID_Invalid)
    else
    {
      fatalError("Could not create an AVMutableCompositionTrack.")
    }

    let newAudioAsset = AVURLAsset(
      url: newAudioFileURL, 
      options: [AVURLAssetPreferPreciseDurationAndTimingKey: true])

    guard let newAudioTrack = newAudioAsset.tracks(withMediaType: .audio).first else {
      fatalError("Could not get an audio track for the new audio to be inserted.")
    }

    // Insert the new audio.
    try compositionTrack.insertTimeRange(
      CMTimeRangeMake(start: .zero, duration: newAudioAsset.duration),
      of: newAudioTrack,
      at: .zero)
    }

Here's my export code:

    guard
      let exportSession = AVAssetExportSession(
        asset: composition,
        presetName: AVAssetExportPresetAppleM4A)
    else
    {
      fatalError("Could not create an AVAssetExploreSession.")
    }

    self.exportSession = exportSession

    exportSession.outputURL = audioFileURL
    exportSession.outputFileType = .m4a
    exportSession.timeRange = CMTimeRange(start: .zero, duration: composition.duration)
    exportSession.exportAsynchronously(completionHandler: { [weak self, weak exportSession] in
      guard let self = self, let exportSession = exportSession else {
        fatalError()
      }

      switch exportSession.status {
      case .completed:
        // Roughly 0.05s less
        let savedFileAsset = AVURLAsset(
          url: audioFileURL, 
          options: [AVURLAssetPreferPreciseDurationAndTimingKey: true])

        assert(savedFileAsset.duration == composition.duration) // fails; saved file asset is about 0.05s less

        self.exportSession = nil

      default:
        if let error = exportSession.error {
          self.exportSession = nil
          print(error.localizedDescription)
        }
      }
    })
0

There are 0 best solutions below