A command-line Windows Media Audio encoder for Windows — a Rust port of
lvqcl's WMAEncode, extended
to also emit xWMA (the RIFF format used by XAudio2 in games, à la Microsoft's
xWMAEncode). It turns WAV / raw PCM into fully-working .wma or .xwma using the
built-in Windows Media Format runtime (wmvcore.dll) — the same encoder Windows
itself uses — so output is maximally compatible and DRM-free. It is bidirectional:
a .wma input is decoded back to WAV.
WAV/PCM -> WMA via the built-in Windows Media Format SDK (wmvcore).
Supports WMA Standard, Professional, Lossless, and Voice in CBR / VBR / two-pass
modes, with tagging, raw-PCM input, stdin input, stdout piping, and .xwma
output.
- Memory-safe at the boundary. All
unsafeis confined tosrc/encoder.rsandsrc/xwma.rs(the COM/FFI boundary); the crate root sets#![deny(unsafe_code)], so every other module is compiler-guaranteed unsafe-free. (Calling the Windows Media SDK requiresunsafe; it can only be isolated and audited, not removed.) - Bug fixes vs. the original C++: tag strings include the UTF-16 NUL
terminator (
WMT_TYPE_STRINGlength); 64-bit WAV offset/seek (two-pass on files > 2 GB); broken-pipe-safe stdout (--print-formats | headwon't panic). - xWMA output with an accurate
dpdsseek table (total length exact; seek points sub-millisecond), validated by round-tripping through Microsoft'sxwmaencode.exe. - Unicode-safe. Non-ASCII filenames and tags (incl. CJK) are handled without panic or mojibake; the console output code page is set to UTF-8 at startup.
- Bidirectional. Encodes WAV→WMA/xWMA and decodes WMA→WAV and xWMA→WAV.
Requires the Rust toolchain (x86_64-pc-windows-msvc) and a normal Windows install
(the Windows Media runtime). No external libraries to link — the windows crate
binds wmvcore.dll directly.
cargo build --release
# -> target/release/WMAEncodeRS.exePrebuilt x86-64 binaries are published on the Releases page.
WMAEncodeRS <input> [output] [options]
<input>— WAV PCM file, or-for stdin[output]— output file;.xwmaextension (or--xwma) selects xWMA;-streams the result to stdout; omitted derives<input>.wma
WMAEncodeRS song.wav # -> song.wma, WMA Standard CBR 128k
WMAEncodeRS song.wav out.wma -q 75 # VBR quality 75
WMAEncodeRS song.wav out.wma -c pro -b 192
WMAEncodeRS song.wav out.wma -c lossless
WMAEncodeRS song.wav out.wma -m cbr2pass -b 96
WMAEncodeRS song.wav sfx.xwma -b 48 # xWMA for XAudio2 (48 kbps)
WMAEncodeRS song.wma song.wav # decode WMA -> WAV (bidirectional)
WMAEncodeRS song.wav out.wma -v # verbose: print format detailsA .wma or .xwma input is decoded to a PCM WAV (output .wav, or derived if
omitted):
WMAEncodeRS music.wma music.wav
WMAEncodeRS sfx.xwma sfx.wav.wma decodes directly via the Windows Media reader.
.xwma is decoded by reconstructing an ASF from its packets, then decoding that — output is ~99.9%
identical to Microsoft's xwmaencode decoder (sub-LSB boundary differences).
Also, decoded length may exceed the original — WMA adds encoder-delay padding.
See more in Caveats below.
.xwma output (or --xwma) produces a RIFF/XWMA file: WMA2 (Standard) CBR with a
dpds seek table — the format XAudio2 plays directly. It is always WMA2/CBR;
supported bitrates are 20, 32, 48, 64, 96, 160, 192 kbps (default 48). This
reimplements both directions of Microsoft's xWMAEncode natively in Rust
(no temp tools, no DirectX SDK): PCM→xWMA, and xWMA→WAV (see Decoding above).
WMA/ASF can't truly stream to a pipe (the muxer must seek back to patch header
sizes), so when the output is - the result is encoded to a temp file and then
streamed to stdout. WAV-on-stdin is fully supported — the model Foobar2000's
converter uses:
cat song.wav | WMAEncodeRS -s - out.wma # WAV in via stdin
cat song.wav | WMAEncodeRS -s - - > out.wma # WAV in, WMA out via stdout--quality <n>, -q <n> VBR quality (10,25,50,75,90,98)
--bitrate <n>, -b <n> bitrate in kbps (default 128; xWMA default 48)
--codec, -c standard|std (default), professional|pro, lossless|lsl, voice
--mode, -m cbr (default), cbr2pass, vbr, vbr2pass
--xwma emit xWMA (RIFF/XWMA for XAudio2); also auto-on for .xwma
--out-samplerate n target sample rate
--out-channels n target channel count
--out-bitdepth n target bit depth
--ignorelength, -i ignore the length in the WAV header
--bufferstdin buffer stdin for two-pass
--raw headerless raw PCM input
--raw-samplerate / --raw-channels / --raw-bitdepth
--priority, --nice run at idle priority
--verbose, -v print format details to stderr
--silent, -s suppress progress
--print-formats list every codec/format the system supports
--help, -h full help; --longhelp extended help with examples
Tagging: --title --tracknumber --artist --album --year --genre
--composer --albumartist --discnumber --comment --tag name=value
Encoding goes through the Microsoft encoder, so output behaves exactly like Windows-produced WMA. A few inherent codec/container traits are by design and not controllable by any front-end:
- CBR leaky-bucket bandwidth limiting (use VBR/2-pass to avoid it)
- WMA Pro being undecodable on very old players (Standard is the default).
- Fixed-size ASF packet padding, and ASF's fixed header overhead on very short files.
This tool produces unprotected (DRM-free) files, handles the codec's bitrate↔sample-rate format matrix gracefully, and writes correct UTF-16 tags.
| Format | Container | Codec | Typical use | This tool |
|---|---|---|---|---|
.wma |
ASF | WMA Std/Pro/Lossless/Voice | Windows desktop / media players / general audio | ✅ encode + decode |
.xwma |
RIFF | WMA2 only | XAudio2 game audio (Windows, Xbox One/Series, Xbox 360) | ✅ encode + decode |
.xma |
RIFF | XMA (a different codec) | Xbox 360 only (decoded by dedicated 360 hardware) | ❌ not supported |
The common trap: .xma is not WMA. XMA is a separate, Xbox-360-specific codec (a
relative of WMA Pro) that has nothing to do with wmvcore or the WMA encoder. It can
only be produced with xma2encode from the Xbox 360 XDK (a restricted SDK); there
is no public Windows API/DLL that encodes XMA, so it is out of scope here. .wma is for
general Windows audio; .xwma is for XAudio2 game audio.
A player such as foobar2000 may warn that an encoded .wma's reported length is
slightly shorter than the decoded length (e.g. 5:26.008000 vs 5:26.008163). The
audio is complete — this is a metadata rounding artifact, not missing samples.
ASF stores durations as integers in 100-nanosecond units, and WMA decodes in fixed
~2048-sample frames, so the true length rarely lands on an exact tick. When wmvcore
writes the ASF header it rounds the Play Duration down, losing the sub-tick
remainder (here ~163 µs, about 7 samples at 44.1 kHz — inaudible). This is wmvcore's
behavior, not the front-end's: the original wmaencode.exe (and Windows Media Player)
produce the same warning, because we all hand PCM to the same IWMWriter muxer, which
computes and writes that field. We don't get to override it through the high-level API.
Note: do not try to "fix" this by padding the file with silence — WMA can only add whole ~46 ms frames (vastly larger than the 0.163 ms discrepancy), and any appended silence becomes real audio that defeats WMA's gapless trim metadata, manufacturing an audible gap. The only safe fix would be to post-process the header's duration field (left undone here as a cosmetic, risky change).
The only WMA decoder available on a normal PC is the Windows Media runtime, reached
through IWMSyncReader — which only opens ASF, not RIFF/XWMA. An .xwma is WMA2
audio (decodable) in a RIFF wrapper (not openable). So to decode it we must get the
WMA2 packets into a container the reader accepts.
We can't feed raw packets to IWMSyncReader (no such entry point), and driving the WMA
decoder DMO (WMADMOD.DLL) directly requires the WMA2 codec private data (samples-
per-block, encode options, …) that the bare xWMA fmt chunk (cbSize = 0)
deliberately omits. So instead we ask IWMCodecInfo3 for the WMA2 stream config
matching the xWMA's rate/channels/bitrate — that config carries the missing codec
private data — and write the (already-compressed) packets into a fresh ASF with
IWMWriterAdvanced::WriteStreamSample. That ASF is then decoded by the normal
.wma → WAV path. The reconstruction is the mechanism that supplies the codec metadata
the container left out, and it reuses a single decoder instead of a second
implementation.
Because the reconstructed-ASF decode routes packets through IWMSyncReader rather than
xWMA's native decoder, the decoded PCM is ~99.9 % byte-identical to Microsoft's
xwmaencode (in testing, 162 of 182,318 bytes differed; ±1-LSB, ~−90 dB).
The differences sit at packet boundaries because WMA is a lapped-transform codec:
consecutive frames overlap and cross-fade (overlap-add / inverse MDCT), so edge samples
are reconstructed from two packets at once. Two things make the boundary samples round
differently between the two decode paths: (1) decoder priming/preroll — our
reconstructed stream's warm-up isn't bit-identical to the native xWMA decoder's; and
(2) floating-point rounding order in the inverse transform, which tips the
final quantize-to-int by ±1 on occasional edge samples. Long identical interiors,
tiny ±1 differences at the seams — the expected fingerprint. Lossy decoders are only
required to be correct within tolerance, not bit-exact across implementations (the same
is true of decoding one MP3 with two decoders). It is inaudible and irrelevant to
playback; it would only matter for byte-exact reproducibility (e.g. hashing decoded
output), which would require driving WMADMOD.DLL directly instead.
Encoder (full path to the binary): \path\to\your\WMAEncodeRS.exe
Extension — what the output file gets:
wma
(use xwma if you're targeting XAudio2 instead — see the variants below)
Parameters — foobar pipes WAV to stdin (-) and substitutes the output path for %d:
- %d -s -b 192
Format is → lossy (WMA Standard/Pro are lossy). Choose lossless only for the WMA-lossless preset below.
Highest BPS mode supported → 16 (WMA Standard/xWMA decode/encode at 16-bit).
| Token | Meaning |
|---|---|
- |
read WAV from stdin (foobar pipes it in) |
%d |
foobar replaces this with the output file path |
-s |
silent (suppress progress on stderr) |
-b 192 |
bitrate 192 kbps (or -q 75 for VBR, etc.) |
Order matters only in that the input (
-) comes before the output (%d) — they're positional, exactly likeWMAEncodeRS <input> [output].
| Goal | Extension | Parameters | Format is |
|---|---|---|---|
| WMA Standard CBR 192k | wma |
- %d -s -b 192 |
lossy |
| WMA Standard VBR q75 | wma |
- %d -s -q 75 |
lossy |
| WMA Professional 192k | wma |
- %d -s -c pro -b 192 |
lossy |
| WMA Lossless | wma |
- %d -s -c lossless |
lossless |
| WMA Voice 20k (mono) | wma |
- %d -s -c voice -b 20 |
lossy |
| xWMA 48k (XAudio2) | xwma |
- %d -s -b 48 --xwma |
lossy |
| xWMA 192k (XAudio2) | xwma |
- %d -s -b 192 --xwma |
lossy |
| Goal | Parameters |
|---|---|
| WMA Std 2-pass ABR 128k | - %d -s -m vbr2pass -b 128 --bufferstdin |
| WMA Std 2-pass ABR 192k | - %d -s -m vbr2pass -b 192 --bufferstdin |
| WMA Pro 2-pass ABR 192k | - %d -s -m vbr2pass -b 192 -c pro --bufferstdin |
| CBR 2-pass 192k (alt) | - %d -s -m cbr2pass -b 192 --bufferstdin |
If you'd rather not buffer the whole track in RAM, drop
--bufferstdin— WMAEncodeRS will use a temp file for the second pass instead. RAM buffering is faster; temp-file is lighter on memory.
Note: there is no quality-based 2-pass in WMA — 2-pass modes are always bitrate-targeted. If you want quality-based, that's 1-pass vbr with -q (e.g. - %d -s -q 90).
(For xWMA the --xwma flag is optional since the xwma extension auto-selects it, but it's harmless to include.)
-
Don't pipe the output (
-as output) in foobar. Use%dso foobar gives a real file path — WMA/ASF can't be written to a non-seekable pipe (the muxer must seek back to patch the header).%dis the correct, file-based path and works fine. -
Tags: leave foobar's converter tagging on as usual — but note WMAEncodeRS can also write tags itself if you pass e.g.
--title "%title%" --artist "%artist%". You generally don't need to; foobar tags the output WMA after encoding. If you do pass them, foobar substitutes the%field%values.
cargo test runs unit/regression tests. The untrusted-input parsers (WAV header,
xWMA, CLI) are fuzzed with cargo-fuzz (libFuzzer) — see fuzz/. Fuzzing
found and fixed two robustness bugs in the WAV header parser (a divide-by-zero on a
zero-channel fmt chunk, and a multi-GB allocation from an absurd fmt size), both
now guarded by regression tests. Parsers are unsafe-free, so these are
clean error-returns rather than memory-safety issues.
MIT. Original work © 2011 lvqcl; Rust port retains the same license — see
LICENSE.