Extract audio on iOS & macOS with FFmpeg
Use Swift to save a video’s first audio track as an audio-only .m4a file. On macOS, FFmpeg can
copy compatible audio without re-encoding; on iOS, AVFoundation exports AAC audio inside your app.
The examples below preserve existing destinations and report failed exports.
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, which is unavailable on iOS. For iOS, use Apple’s built-in AVFoundation framework and its Apple M4A export preset.
Setting up audio extraction on Apple platforms
FFmpegKit has been retired. The author’s retirement notice and continuation information point to FFmpegKitNext, distributed as source. These examples need no FFmpegKit binaries or third-party Swift packages.
Prepare FFmpeg and Swift
Start with a Mac that already has Xcode and a working FFmpeg distribution, including ffprobe,
on PATH. Apple’s Xcode downloads and the
FFmpeg download page list installation options. This walkthrough
starts after those tools are installed; it does not install system software.
Run these checks in a fresh working directory before saving the Swift files:
command -v swiftc >/dev/null &&
command -v ffmpeg >/dev/null &&
command -v ffprobe >/dev/null &&
swiftc --version && ffmpeg -version && ffprobe -version
The tested environment is macOS 26.6.2, Xcode 27.0, Swift 6.4 in Swift 5 language mode, FFmpeg and ffprobe 9.0.2, and an iOS 27.0 simulator. Older OS versions and physical devices are outside this walkthrough’s tested scope. Full Xcode supplies the iOS SDK and simulator.
Use an intact local MP4 with an AAC audio track, named sample video.mp4 in the working directory.
Run one extraction at a time. The iOS example uses a bundled file and app-owned Documents storage;
it does not cover external file providers, file-picker permissions, or background exports.
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.
The temporary directory is cleaned up after an ordinary success or thrown error. Forced process
termination can leave it behind.
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
The command writes copied.m4a and encoded.m4a beside the source files. Open both in an audio
player and compare their soundtrack and duration with the input. Stream copying preserves the
encoded AAC stream; AVFoundation can re-encode it, so the files need not have identical bytes.
Run the iOS example
In Xcode, create an iOS App project with the SwiftUI interface. Use Swift 5 language mode and
select the installed iOS 27 simulator. Add AudioExtraction.swift to the app target, and add
sample video.mp4 to its Copy Bundle Resources build phase. Do not add the macOS
ExtractAudio.swift entry point to this target. Replace the generated app and content-view files
with this single AudioDemoApp.swift file:
import Foundation
import SwiftUI
@main
struct AudioDemoApp: App {
var body: some Scene {
WindowGroup { AudioDemoView() }
}
}
struct AudioDemoView: View {
@State private var started = false
@State private var message = "Extracting audio…"
@State private var output: URL?
var body: some View {
VStack(spacing: 16) {
Text(message)
if let output {
ShareLink("Share audio", item: output)
}
}
.padding()
.task {
guard !started else { return }
started = true
do {
guard let input = Bundle.main.url(
forResource: "sample video", withExtension: "mp4"
) else { throw AudioExtractionError.invalidPaths }
let documents = try FileManager.default.url(
for: .documentDirectory, in: .userDomainMask,
appropriateFor: nil, create: true
)
let destination = documents.appendingPathComponent("encoded.m4a")
try await extractAudioWithAVFoundation(from: input, to: destination)
output = destination
message = "Audio saved: encoded.m4a"
} catch {
message = "Extraction failed. Check the input and destination."
}
}
}
}
Build and run the app. It exports once when the view opens and displays
Audio saved: encoded.m4a after completion. Use
Share audio to open or save the result in another app.
The source stays in the bundle; encoded.m4a lives in the app’s Documents directory. On the next
launch, the existing output causes a refusal. For another export, choose a new destination filename
or remove that output deliberately. No microphone permission is needed because the app reads a file.
Handling common issues
- File access: Check that the input exists and the destination’s parent is writable. The CLI is a local terminal program; packaging FFmpeg inside a sandboxed macOS app requires separate work. For iOS, this example deliberately stays inside the app’s bundle and Documents directory.
- Existing output: Choose a new destination. The examples never replace an existing file.
- No audio track: A video without an audio track produces an error and no new output. A track containing silence is still an audio track and can be exported.
- 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. - Damaged recordings: A successful export is not an integrity check. Media tools can recover incomplete input; compare the output’s actual duration and sound before using it.
Optimizing 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, as the examples do, instead of reading the entire recording into a
Datavalue. The retainedexport()andstatusAPIs produce deprecation warnings on current SDKs; Apple’s export guide describes the newer export API for applications that target newer OS versions.
Additional resources
See the FFmpeg stream selection documentation and Apple’s media export guide for more detail on selecting streams and configuring exports.
For a server-side workflow, see Transloadit’s Audio Encoding service.
