"Convert M4A to MP3 to save space" is advice you see everywhere. I measured it with the encoders that ship in Windows, and on the same source the two files came out 0.6% apart. Here is what is actually going on, and the PowerShell that does the conversion without installing anything.
The setup
One 10-second, 44.1kHz, 16-bit stereo WAV — 1,764,044 bytes, which Windows reports as 1,411,200 bps (44100 × 16 × 2, exactly CD rate). Everything below went through Windows.Media.Transcoding.MediaTranscoder on Windows 11, with sizes from the filesystem and bitrates from MusicProperties.
Same preset, same bitrate, same size
| File | Bytes | Bitrate reported | Duration |
|---|---|---|---|
| source WAV | 1,764,044 | 1,411,200 bps | 10.00s |
M4A CreateM4a("High")
|
243,233 | 192,512 bps | 10.01s |
M4A CreateM4a("Medium")
|
162,994 | 128,448 bps | 10.01s |
M4A CreateM4a("Low")
|
122,926 | 96,392 bps | 10.01s |
MP3 CreateMp3("High")
|
241,775 | 192,000 bps | — |
The High presets are 192kbps for both. That is the whole explanation for the size result: AAC may be the better codec at a given bitrate, but Windows' presets never give it a lower bitrate to prove that with. You get the same bits either way, and the 1,458-byte gap is container overhead.
So if you are converting M4A to MP3 to save space, you are doing work for nothing. The only good reason is that something downstream only accepts MP3 — an upload validator, a car stereo, a hardware player.
The round trip is worse than it looks
Converting that M4A onward to MP3 gave 239,471 bytes. Similar size, but the audio inside has now been through two lossy encoders. And going back the other way:
source WAV 1,764,044 bytes
-> M4A (High) 243,233 bytes
-> WAV again 1,921,106 bytes
The recovered WAV is larger than the original WAV. A lossless container makes the file big again; it does not make the discarded audio reappear. If you ever need this spelled out for a non-technical colleague, that line usually does it.
There is a subtler one. Windows reports the source WAV as 10.00s, the M4A as 10.01s, and the MP3 as 9.96s. Encoder delay and padding are not free, and the container's idea of duration drifts with each hop. If you are stitching converted clips together, that drift is where the click at the seam comes from.
The conversion, with nothing installed
Windows ships the encoders; PowerShell can reach them through WinRT.
Add-Type -AssemblyName System.Runtime.WindowsRuntime
$ms = [System.WindowsRuntimeSystemExtensions].GetMethods()
$op = ($ms | Where-Object { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -like 'IAsyncOperation*' })[0]
$ap = ($ms | Where-Object { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -like 'IAsyncActionWithProgress*' })[0]
function Wait-Op($o, $t) { $x = $op.MakeGenericMethod($t).Invoke($null, @($o)); $x.Wait(-1) | Out-Null; $x.Result }
function Wait-Act($a) { $x = $ap.MakeGenericMethod([double]).Invoke($null, @($a)); $x.Wait(-1) | Out-Null }
[Windows.Storage.StorageFile, Windows.Storage, ContentType=WindowsRuntime] | Out-Null
[Windows.Storage.StorageFolder, Windows.Storage, ContentType=WindowsRuntime] | Out-Null
[Windows.Media.MediaProperties.MediaEncodingProfile, Windows.Media, ContentType=WindowsRuntime] | Out-Null
[Windows.Media.Transcoding.MediaTranscoder, Windows.Media, ContentType=WindowsRuntime] | Out-Null
$folderPath = "C:\Users\you\Music"
$folder = Wait-Op ([Windows.Storage.StorageFolder]::GetFolderFromPathAsync($folderPath)) ([Windows.Storage.StorageFolder])
$profile = [Windows.Media.MediaProperties.MediaEncodingProfile]::CreateMp3("High")
Get-ChildItem "$folderPath\*.m4a" | ForEach-Object {
$src = Wait-Op ([Windows.Storage.StorageFile]::GetFileFromPathAsync($_.FullName)) ([Windows.Storage.StorageFile])
$dst = Wait-Op ($folder.CreateFileAsync([IO.Path]::ChangeExtension($_.Name, ".mp3"), 1)) ([Windows.Storage.StorageFile])
$t = New-Object Windows.Media.Transcoding.MediaTranscoder
$prep = Wait-Op ($t.PrepareFileTranscodeAsync($src, $dst, $profile)) ([Windows.Media.Transcoding.PrepareTranscodeResult])
if ($prep.CanTranscode) { Wait-Act ($prep.TranscodeAsync()); "$($_.Name) -> $($dst.Name)" }
else { "$($_.Name): $($prep.FailureReason)" }
}
Three things worth knowing before you run it:
The Wait-Op / Wait-Act wrappers are the point, not boilerplate. Windows PowerShell 5.1 has no await, and these calls are asynchronous. Skip the wait and you measure a file that is still being written — that is how a ten-second clip first came out of this script as 4KB.
CanTranscode is where failures surface. It returns false for a missing codec and for DRM-protected files. Old iTunes purchases (.m4p) land here and cannot be converted this way.
Swap the profile, not the script. CreateM4a, CreateWav and CreateMp3 all take the same "High" | "Medium" | "Low", so the same loop covers every direction — which is how the table above was produced.
The part that surprises people
M4A is a container, not a codec. It is the audio-only sibling of .mp4, which is why renaming one to .mp4 often still plays — same internal structure, different extension. It is also why renaming solves nothing when something validates the format.
And on Windows specifically: there is no Store extension to install. Unlike HEIC, M4A plays out of the box. When a double-click does nothing, the format is not the problem — no program is associated with the extension.
The version written for people who just received a voice memo they cannot open: https://www.coding-now.com/en/guides/m4a-to-mp3?utm_source=devto
Curious whether other platforms' presets pin AAC and MP3 to the same bitrate the way Windows does — if ffmpeg defaults differ on your setup, I would like to see the numbers.
Top comments (0)