ffmpegaudiovideo-encoding

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

··10 min read
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.mp4 prints one word, and on an MP4 that word is almost always aac.
  • Copying AAC into an .mp3 file fails with Could 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.m4a finishes 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.mp3 costs you a second lossy generation.
  • Pin the track with -map 0:a:0 whenever 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 saysCopy out asRe-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.

  1. Probe it. -select_streams a -show_entries stream=index,codec_name,channels -of csv=p=0 returns 1,aac,1 and 2,aac,2. Two AAC tracks, and the stereo one is the mixed feed.
  2. Pick the track. The transcript needs both speakers, so stream index 2, which is 0:a:1 in 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.
  3. Copy it out. ffmpeg -i call.mp4 -vn -map 0:a:1 -c:a copy call.m4a finishes in about a second and writes 18.3 MB of untouched 128 kbps AAC.
  4. Compare the alternative. The same 1,144 seconds re-encoded at -q:a 2 is 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."
  5. Send it to transcription. OpenAI's speech-to-text endpoint accepts .m4a and caps uploads at 25 MB, so 18.3 MB goes through. Had step 3 written those AAC bytes to call.mp3, the request would have returned 400 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.

Software EngineeringVideo ProcessingFFmpegCloud ArchitectureAPI DesignAutomation

Ready to process videos at scale?

Start using FFmpeg Micro's simple API today. No infrastructure required.

Get Started Free