Extract audio on iOS & macOS with FFmpeg
Extracting a soundtrack is useful for voice notes, transcription, and audio editing. This guide
uses FFmpeg from Swift on macOS and AVFoundation for the same task in Apple apps, including iOS.
Both examples write an audio-only .m4a file and preserve an existing destination.
Introduction to FFmpeg on Apple platforms
FFmpeg can copy a compatible audio stream without re-encoding it. The macOS example launches a
local FFmpeg executable through Foundation’s Process API. Process is unavailable on iOS; the
AVFoundation example uses Apple’s built-in media framework and exports AAC audio instead.
Setting up audio extraction on Apple platforms
FFmpegKit has been retired. Its original repository is not an installable Swift package, so the previous instructions to add it through Swift Package Manager do not work. The author’s retirement notice and continuation information describe the distribution changes. This guide no longer depends on those binaries.
Prepare FFmpeg and Swift
No third-party Swift package is needed for these examples. For macOS, install a working FFmpeg executable using a distribution listed on the FFmpeg download page. Check its version and pass its absolute path to the example:
ffmpeg -version
command -v ffmpeg
The Swift code below targets macOS 13 or iOS 16 and later. Its macOS command-line path needs
Apple Command Line Tools with Swift 5.9 or later. Building the iOS app requires full Xcode and
an iOS SDK. The native verification for this guide covers macOS; iOS device builds, file-picker
access, and app lifecycle behavior still need validation in your app.
Extracting audio streams from video files
The FFmpeg path selects the first audio stream with -map 0:a:0. Stream copying into M4A requires
a compatible codec, such as AAC. The AVFoundation path selects the first audio track and exports
it with AVAssetExportPresetAppleM4A. It can re-encode the audio and is not a byte-for-byte copy.
Step-by-step guide for audio extraction
Save this as AudioExtraction.swift. Each operation writes inside a fresh directory beside the
destination, publishes the result only after success, and removes its temporary files on failure.
Pass local file URLs that your app can read and an output URL in an existing writable directory.
import AVFoundation
import Foundation
enum AudioExtractionError: Error {
case invalidPaths, outputExists, noAudio, exportUnavailable, exportFailed
}
private func extractionDirectory(inputURL: URL, outputURL: URL) throws -> URL {
let files = FileManager.default
guard inputURL.isFileURL, outputURL.isFileURL,
files.isReadableFile(atPath: inputURL.path),
outputURL.pathExtension.lowercased() == "m4a" else {
throw AudioExtractionError.invalidPaths
}
guard !files.fileExists(atPath: outputURL.path) else {
throw AudioExtractionError.outputExists
}
let directory = outputURL.deletingLastPathComponent()
.appendingPathComponent("audio-\(UUID().uuidString)", isDirectory: true)
try files.createDirectory(at: directory, withIntermediateDirectories: false)
return directory
}
#if os(macOS)
// This blocking function belongs on a worker queue when called from a macOS UI.
func extractAudioWithFFmpeg(from videoURL: URL, to outputURL: URL, ffmpegURL: URL) throws {
let directory = try extractionDirectory(inputURL: videoURL, outputURL: outputURL)
defer { try? FileManager.default.removeItem(at: directory) }
let temporaryOutput = directory.appendingPathComponent("audio.m4a")
let process = Process()
process.executableURL = ffmpegURL
process.environment = [:]
process.arguments = [
"-nostdin", "-hide_banner", "-loglevel", "error", "-n",
"-protocol_whitelist", "file,pipe", "-i", videoURL.path,
"-map", "0:a:0", "-vn", "-c:a", "copy", "-f", "ipod", temporaryOutput.path
]
// In this command-line example, diagnostics go to stderr without a pipe that could fill up.
process.standardError = FileHandle.standardError
try process.run()
process.waitUntilExit()
guard process.terminationReason == .exit, process.terminationStatus == 0 else {
throw AudioExtractionError.exportFailed
}
try FileManager.default.moveItem(at: temporaryOutput, to: outputURL)
}
#endif
func extractAudioWithAVFoundation(from videoURL: URL, to outputURL: URL) async throws {
let directory = try extractionDirectory(inputURL: videoURL, outputURL: outputURL)
defer { try? FileManager.default.removeItem(at: directory) }
let temporaryOutput = directory.appendingPathComponent("audio.m4a")
let asset = AVURLAsset(url: videoURL)
guard let sourceTrack = try await asset.loadTracks(withMediaType: .audio).first else {
throw AudioExtractionError.noAudio
}
let composition = AVMutableComposition()
guard let track = composition.addMutableTrack(
withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid
) else {
throw AudioExtractionError.exportUnavailable
}
let timeRange = try await sourceTrack.load(.timeRange)
try track.insertTimeRange(timeRange, of: sourceTrack, at: .zero)
guard let session = AVAssetExportSession(
asset: composition, presetName: AVAssetExportPresetAppleM4A
), session.supportedFileTypes.contains(.m4a) else {
throw AudioExtractionError.exportUnavailable
}
session.outputURL = temporaryOutput
session.outputFileType = .m4a
await session.export()
guard session.status == .completed else {
throw AudioExtractionError.exportFailed
}
try FileManager.default.moveItem(at: temporaryOutput, to: outputURL)
}
Usage example
For a macOS command-line program, save this entry point as ExtractAudio.swift alongside the
previous file. The executable waits for extraction to finish and returns a nonzero exit status
on failure.
import Foundation
@main
struct ExtractAudio {
static func main() async {
let arguments = CommandLine.arguments
guard arguments.count == 4 || arguments.count == 5 else {
FileHandle.standardError.write(Data(
"Usage: extract-audio avfoundation|ffmpeg input output.m4a [ffmpeg-path]\n".utf8
))
exit(1)
}
do {
let input = URL(fileURLWithPath: arguments[2])
let output = URL(fileURLWithPath: arguments[3])
switch arguments[1] {
case "ffmpeg" where arguments.count == 5:
try extractAudioWithFFmpeg(
from: input, to: output, ffmpegURL: URL(fileURLWithPath: arguments[4])
)
case "avfoundation" where arguments.count == 4:
try await extractAudioWithAVFoundation(from: input, to: output)
default:
throw AudioExtractionError.invalidPaths
}
print("Audio extraction successful: \(output.path)")
} catch {
FileHandle.standardError.write(Data("Audio extraction failed.\n".utf8))
exit(1)
}
}
}
swiftc -parse-as-library AudioExtraction.swift ExtractAudio.swift -o extract-audio
./extract-audio ffmpeg "sample video.mp4" copied.m4a "$(command -v ffmpeg)"
./extract-audio avfoundation "sample video.mp4" encoded.m4a
In an iOS app, add only AudioExtraction.swift and await extractAudioWithAVFoundation from your
existing task or view model. Keep any security-scoped file access open until the export finishes.
This example processes a file already selected by the user; it does not record from a microphone.
Handling common issues
Here are common issues and how to resolve them:
- File access: Use an app-owned directory or retain access granted by the system file picker. Sandboxed macOS apps also need an appropriately packaged executable and app sandbox configuration.
- Existing output: Choose a new destination. The examples never replace an existing file.
- No audio track: A silent video has nothing to extract and produces an error.
- Unsupported codecs: FFmpeg stream copying can fail when the audio codec cannot go into M4A. Re-encode with AAC instead, or use the AVFoundation path for inputs that Apple’s framework supports.
- Diagnostics: Inspect FFmpeg’s stderr locally. For AVFoundation debugging, inspect
session.errorat the failure check without displaying internal errors in your app’s UI.
Optimizing performance
To optimize performance:
- Copy compatible audio with
-c:a copywhen you need to preserve the encoded stream. - Keep blocking FFmpeg process waits off a macOS app’s main thread.
- Await AVFoundation exports without loading the entire video into memory. Video decoding acceleration is not useful for an operation that copies only the audio stream.
Additional resources
See the FFmpeg stream selection documentation and Apple’s media export guide for more detail on selecting streams and configuring exports.
At Transloadit, we use FFmpeg in our Audio Encoding and Video Encoding services, powering Robots like /audio/encode and /video/encode.
