FFmpeg can extract audio without re-encoding, just not to .mp3

FFmpeg extract audio without re-encoding is a one-flag job, -c:a copy, but the filename you type decides whether it works at all, because the extension picks the muxer.
- Check the codec first:
ffprobe -v error -select_streams a:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 input.mp4prints one word, and on an MP4 that word is almost alwaysaac. - Copying AAC into an
.mp3file fails withCould not find tag for codec aac in stream #0, codec not currently supported in container, because the MP3 muxer writes a raw MP3 bitstream and nothing else. - Copy the stream and name the file what it actually is:
ffmpeg -i input.mp4 -vn -map 0:a:0 -c:a copy output.m4afinishes in under a second with zero quality loss. - Re-encode only if you genuinely need MP3 bytes:
ffmpeg -i input.mp4 -vn -map 0:a:0 -c:a libmp3lame -q:a 2 output.mp3costs you a second lossy generation. - Pin the track with
-map 0:a:0whenever the file has more than one audio stream, because FFmpeg's automatic pick is the stream with the most channels, not the first one.
Why the copy command every guide publishes fails on your MP4
ffmpeg -i input.mp4 -vn -acodec copy output.mp3 is the most-copied audio extraction command on the internet, and it is wrong for the file most people run it against. -c:a copy (the modern spelling of -acodec copy) tells FFmpeg to move the audio packets through untouched. The .mp3 extension tells FFmpeg to hand those packets to the MP3 muxer. Per FFmpeg's own format documentation, that muxer produces an MPEG audio bitstream, not a general-purpose container: there is no box, atom, or tag in which to park an AAC frame. The two instructions contradict each other, so the command dies before it writes a byte.
An MP4 normally holds H.264 video and an AAC audio track. The container also permits MPEG-1 Layer 3, ALAC, and Vorbis, which is the reason the bad command occasionally appears to work: if someone muxed MP3 audio into that MP4, copy into .mp3 is legal and instant. One file out of a batch behaving differently from the rest is the usual way people discover this.
The worse outcome is the silent one. Some tools don't error, they write the bytes and keep the name you gave them. A CLI doing exactly that was filed as a bug for writing AAC-in-MP4 to a .mp3 filename, because everything downstream reads the extension and routes to the wrong decoder. Audacious has the same class of report: a mislabeled stream picks the wrong decoder and simply doesn't play.
The one-second check that decides which command you type
Probe the audio stream before choosing a command, because the codec in the file is the only input to that decision:
ffprobe -v error -select_streams a:0 \
-show_entries stream=codec_name \
-of default=noprint_wrappers=1:nokey=1 input.mp4
That prints aac, mp3, opus, alac, or pcm_s16le and nothing else, which is the documented behavior of -show_entries with the nokey output format (ffprobe manual). For a multi-track file, swap a:0 for a and add fields: -select_streams a -show_entries stream=index,codec_name,channels -of csv=p=0 gives you one line per audio track.
What the answer maps to:
| Probe says | Copy out as | Re-encode needed for .mp3? |
|---|---|---|
| `aac` | `.m4a` | Yes, libmp3lame |
| `mp3` | `.mp3` | No, copy works |
| `opus` | `.opus` or `.ogg` | Yes |
| `alac` | `.m4a` | Yes |
| `pcm_s16le` | `.wav` | Yes |
If you're doing this from application code rather than a shell, the probe step is the part that usually isn't available to you, and there are routes around a missing binary: get video duration and codec in PHP without ffprobe installed covers the three of them.
.m4a beats raw .aac for a copied stream
Name a copied AAC stream .m4a, not .aac. Both are honest about the codec, but a raw ADTS .aac file carries no index and nothing that states its own duration, so players report the wrong length and seeking either jumps to the wrong place or does nothing. That exact symptom shows up in a project issue where the recorded .aac reported a bogus duration, and remuxing the same stream into .m4a with -c copy fixed it without touching a single audio frame.
.m4a is an MP4 container with no video track. It stores the sample table, so duration and seeking work, and it holds tags. The audio is byte-identical to what was in your MP4. That operation is a remux, not an encode, which is the distinction worth internalizing before you reach for a bitrate flag: transcode vs encode vs remux vs compress sorts out which one you're actually asking for.
When re-encoding to MP3 is worth the generation loss
Re-encode when something downstream can only accept MP3: an old CMS field, a podcast host that validates the codec, a legacy player. Everything else should take .m4a. The cost of converting is real, because transcoding between two lossy formats stacks generation loss: the first encoder's artifacts are treated as signal by the second one, so AAC to MP3 compounds what's already been thrown away.
ffmpeg -i input.mp4 -vn -map 0:a:0 -c:a libmp3lame -q:a 2 output.mp3
-q:a 2 is VBR around 190 kbps, which is where MP3 stops being obviously worse than a 128 kbps AAC source. A bitrate flag is required here and -b:a copy is not a valid one: libmp3lame rejects it with Error setting option b to value copy, a separate failure from the container error and one people routinely confuse with it.
The -map 0:a:0 isn't decoration. FFmpeg's stream selection rules say that without an explicit map, it picks the audio stream with the most channels, favoring streams flagged as default. A screen recording with a mono mic track and a stereo system-audio track hands you the system audio by default, which is how you end up with 19 minutes of keyboard clicks and no voice.
A 19-minute sales call, two audio tracks, and a 400 from the transcription step
Here is the whole decision on one real file: a 19:04 screen recording, 420 MB, exported from a meeting tool with the mic and the mixed room audio as separate tracks.
- Probe it.
-select_streams a -show_entries stream=index,codec_name,channels -of csv=p=0returns1,aac,1and2,aac,2. Two AAC tracks, and the stereo one is the mixed feed. - Pick the track. The transcript needs both speakers, so stream index 2, which is
0:a:1in map terms. Without the map flag FFmpeg would have chosen it anyway, by channel count, and that accident is exactly why the flag belongs in the command. - Copy it out.
ffmpeg -i call.mp4 -vn -map 0:a:1 -c:a copy call.m4afinishes in about a second and writes 18.3 MB of untouched 128 kbps AAC. - Compare the alternative. The same 1,144 seconds re-encoded at
-q:a 2is roughly 27 MB of MP3: a bigger file than the source audio, with audible quality lost. Paying 50% more bytes for a worse signal is the actual trade behind "just make it an .mp3." - Send it to transcription. OpenAI's speech-to-text endpoint accepts
.m4aand caps uploads at 25 MB, so 18.3 MB goes through. Had step 3 written those AAC bytes tocall.mp3, the request would have returned400 Unrecognized file format, because that endpoint infers type from the filename and the filename would have been lying. Developers hit this constantly, three services downstream of the command that caused it.
The API version, where the format is a parameter instead of a guess
On FFmpeg Micro the output container is an explicit outputFormat field next to the codec, so a mismatch between the two can't be expressed in the first place:
curl -X POST https://api.ffmpeg-micro.com/v1/transcodes \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"inputs": [{ "url": "gs://your-bucket/call.mp4" }],
"outputFormat": "mp3",
"options": [
{ "option": "-vn" },
{ "option": "-c:a", "argument": "mp3" },
{ "option": "-b:a", "argument": "192k" }
]
}'
You get a job id back, poll /v1/transcodes/{id} until status is completed, then call /download for a signed URL. Set outputFormat to aac for the AAC path. Nothing in the request infers a container from a string you typed, and a failed job tells you why in the status payload rather than in a shell you've already closed (what to check in the poll loop). That 19-minute call costs about 115 tokens, out of the 200 a new account starts with.
FAQ about extracting audio from MP4 without re-encoding
Why does FFmpeg say "Could not find tag for codec aac" when I extract audio?
The MP3 muxer selected by your .mp3 output filename has no tag for an AAC stream, so FFmpeg refuses to copy it in. Either change the output name to output.m4a and keep -c:a copy, or keep the .mp3 name and replace the copy with -c:a libmp3lame -q:a 2.
Can I rename a .m4a file to .mp3 so my tool accepts it?
Renaming an .m4a to .mp3 does not convert anything and breaks more than it fixes. Players and APIs that sniff the bytes will play it fine, and everything that trusts the extension, including the OpenAI transcription endpoint, will reject it or route it to an MP3 decoder that can't read AAC frames.
Is -b:a copy the same error as the container error?
No, -b:a copy is libmp3lame rejecting copy as a bitrate value, and it reports Error setting option b to value copy. The container error comes from the muxer and mentions a codec tag, so the two failures point at different flags.
My extracted .aac has the wrong duration and won't seek. Can I fix it without re-encoding?
Remux the raw ADTS stream into an MP4-based container with ffmpeg -i audio.aac -c copy audio.m4a. The audio frames are copied byte for byte, and the new file gains the sample table that gives it a correct duration and working seek.
If I extract audio from a 30-second section of a one-hour recording, am I billed for the hour?
FFmpeg Micro bills the duration of video actually processed, metered per second with no rounding up to the minute, so trimming 30 seconds out of a one-hour file costs 30 seconds. One token covers 10 seconds of input, failed jobs are never billed, and checking status or downloading the output costs nothing.
The two honest commands up top are in the extract-audio recipe alongside the API request that replaces them, with the full format and bitrate table. Sign up free and you get a key plus 200 tokens, about 33 minutes of video, with no card: no servers to run and no FFmpeg to install, called from your own code, from n8n, Make, and Zapier, or from your AI agents.
About Javid Jamae
Founder & CEO at FFmpeg Micro
Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.
You might also like

What transcode means: transcode vs encode vs remux vs compress
Transcode means decoding compressed video and encoding it again. See how transcode differs from encode, remux, and compress, and when to skip it entirely.

Why your transcode job failed (and what to do in the poll loop)
A transcode failed almost always means bad input, not a broken service. The real causes, an ffprobe pre-check, and the poll loop that knows when to retry.

Etsy Listing Video Requirements: 15 Seconds, No Audio, One Encode
Etsy video requirements in full: 5-15 seconds, 100 MB, no audio, 2:1 to 1:2 ratio. Cut two legal 15-second clips from one master with FFmpeg or one API call.
Ready to process videos at scale?
Start using FFmpeg Micro's simple API today. No infrastructure required.
Get Started Free