DEV Community

코딩나우(하늘아래)
코딩나우(하늘아래)

Posted on Originally published at coding-now.com

Windows encodes M4A and MP3 at the same bitrate, so the file sizes land 0.6% apart

"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
Enter fullscreen mode Exit fullscreen mode

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)" }
}
Enter fullscreen mode Exit fullscreen mode

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)