AVFoundation is Apple's framework for all audio and video work. It handles MP4-to-audio extraction natively — no FFmpeg, no shell processes, no extra dependencies. The main gotcha: AVFoundation exports M4A (AAC in MPEG-4 container) natively but not raw MP3. For true MP3 output, the ChangeThisFile API or a process call to FFmpeg is required. This guide covers all three paths clearly.
Method 1: AVFoundation AVAssetExportSession (native, outputs M4A/AAC)
AVFoundation exports AAC audio in an M4A container natively. M4A is smaller than MP3 at equivalent quality and plays in all Apple devices, modern browsers, and most media players.
import AVFoundation
import Foundation
func mp4ToM4A(inputURL: URL, outputURL: URL) async throws {
let asset = AVAsset(url: inputURL)
// Verify asset has an audio track
let audioTracks = try await asset.loadTracks(withMediaType: .audio)
guard !audioTracks.isEmpty else {
throw ExportError.noAudioTrack
}
guard let exportSession = AVAssetExportSession(
asset: asset,
presetName: AVAssetExportPresetAppleM4A
) else {
throw ExportError.sessionCreationFailed
}
// Remove existing output file
try? FileManager.default.removeItem(at: outputURL)
exportSession.outputURL = outputURL
exportSession.outputFileType = .m4a
try await exportSession.export(to: outputURL, as: .m4a)
switch exportSession.status {
case .completed:
print("Saved: \(outputURL.path)")
case .failed:
throw exportSession.error ?? ExportError.unknown
case .cancelled:
throw ExportError.cancelled
default:
throw ExportError.unknown
}
}
enum ExportError: Error {
case noAudioTrack
case sessionCreationFailed
case cancelled
case unknown
}
// Usage
Task {
let input = URL(fileURLWithPath: "/tmp/video.mp4")
let output = URL(fileURLWithPath: "/tmp/audio.m4a")
try await mp4ToM4A(inputURL: input, outputURL: output)
}
Why M4A and not MP3? Apple deprecated MP3 encoding in AVFoundation — AVFileTypeMPEGLayer3 was removed from export presets in recent macOS/iOS versions. M4A (AAC) is the Apple-native output format and is universally compatible. If you specifically need MP3, see Method 2 or 3.
Method 2: FFmpeg via Process (true MP3 output on macOS)
For actual MP3 output on macOS, call FFmpeg via Swift's Process API. Requires FFmpeg installed (brew install ffmpeg).
import Foundation
func mp4ToMp3FFmpeg(
inputPath: String,
outputPath: String,
bitrate: String = "192k"
) throws -> Int32 {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/local/bin/ffmpeg")
process.arguments = [
"-i", inputPath,
"-vn",
"-acodec", "libmp3lame",
"-ab", bitrate,
"-y",
outputPath
]
let pipe = Pipe()
process.standardError = pipe
process.standardOutput = pipe
try process.run()
process.waitUntilExit()
let output = String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
if process.terminationStatus != 0 {
fputs("FFmpeg error:\n\(output)\n", stderr)
} else {
print("Saved: \(outputPath)")
}
return process.terminationStatus
}
// Usage
try mp4ToMp3FFmpeg(inputPath: "/tmp/video.mp4", outputPath: "/tmp/audio.mp3", bitrate: "192k")
FFmpeg location varies: /usr/local/bin/ffmpeg on Intel Macs, /opt/homebrew/bin/ffmpeg on Apple Silicon. Use which ffmpeg or make the path configurable. On iOS, Process is not available — use Method 1 (M4A) or Method 3 (API) instead.
Method 3: ChangeThisFile API via URLSession (MP3 on any platform)
True MP3 output, no AVFoundation restrictions, no FFmpeg install. Works on macOS, iOS, Linux Swift, and CI runners. Free tier: 1,000 conversions/month.
# curl reference
curl -X POST https://changethisfile.com/v1/convert \
-H "Authorization: Bearer ctf_sk_your_key_here" \
-F "file=@video.mp4" \
-F "target=mp3" \
--output audio.mp3
import Foundation
func mp4ToMp3API(
inputURL: URL,
outputURL: URL,
apiKey: String
) async throws {
let endpoint = URL(string: "https://changethisfile.com/v1/convert")!
let boundary = UUID().uuidString
let fileData = try Data(contentsOf: inputURL)
var request = URLRequest(url: endpoint)
request.httpMethod = "POST"
request.timeoutInterval = 300 // 5 min for long videos
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
var body = Data()
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(inputURL.lastPathComponent)\"\r\n".data(using: .utf8)!)
body.append("Content-Type: video/mp4\r\n\r\n".data(using: .utf8)!)
body.append(fileData)
body.append("\r\n".data(using: .utf8)!)
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"target\"\r\n\r\n".data(using: .utf8)!)
body.append("mp3\r\n".data(using: .utf8)!)
body.append("--\(boundary)--\r\n".data(using: .utf8)!)
request.httpBody = body
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
throw URLError(.badServerResponse)
}
try data.write(to: outputURL)
print("Saved: \(outputURL.path)")
}
// Usage
Task {
try await mp4ToMp3API(
inputURL: URL(fileURLWithPath: "/tmp/video.mp4"),
outputURL: URL(fileURLWithPath: "/tmp/audio.mp3"),
apiKey: "ctf_sk_your_key_here"
)
}
When to use each
| Approach | Best for | Tradeoff |
|---|---|---|
| AVFoundation (M4A) | iOS apps, macOS, no system deps, AAC output is fine | Outputs M4A (AAC), not MP3 — MP3 encoding removed |
| Process + FFmpeg | macOS CLI tools, scripts, true MP3 output | Requires FFmpeg install; not available on iOS |
| ChangeThisFile API (URLSession) | True MP3, iOS-compatible, no FFmpeg install | Network latency, 25MB free-tier upload limit |
Production tips
- M4A vs MP3: prefer M4A unless the receiver explicitly requires MP3. AAC (M4A) has better quality-per-bit than MP3 and plays in all modern players. MP3 is the compatibility format for legacy systems.
- Check for audio tracks before exporting. Not all MP4 files contain audio (screen recordings, dashcam footage). AVAsset.loadTracks(withMediaType: .audio) returning empty should surface a clear error, not a silent zero-byte output.
- Handle the export on a background Task. AVAssetExportSession.export() is async but can be slow for large files. Always run it off the main actor to avoid blocking UI.
- FFmpeg path differs on Apple Silicon. Homebrew installs to /opt/homebrew on Apple Silicon, /usr/local on Intel. Either use the output of
which ffmpegat runtime or make the path an environment variable. - Increase URLSession timeout for large files. The default URLSession timeout is 60 seconds. A 500MB MP4 upload will exceed this. Set
request.timeoutInterval = 300(5 minutes) or create a custom URLSessionConfiguration.
If M4A is acceptable (and it usually is), AVFoundation is the cleanest native path. For MP3 specifically on macOS, FFmpeg via Process. For iOS or zero-dependency pipelines, the API. Free tier covers 1,000 conversions/month.