Skip to content

Instantly share code, notes, and snippets.

@mayerwin
Created March 2, 2026 03:21
Show Gist options
  • Select an option

  • Save mayerwin/53a8dd6c51fb317426cb521fa45217b6 to your computer and use it in GitHub Desktop.

Select an option

Save mayerwin/53a8dd6c51fb317426cb521fa45217b6 to your computer and use it in GitHub Desktop.
PowerShell script to batch-convert MP3 files to Wwise .wem format using ffmpeg + WwiseConsole.exe, with two-pass EBU R128 loudness normalization

Convert-Mp3ToWem.ps1

Batch-convert MP3 files to Wwise Encoded Media (.wem) using ffmpeg and WwiseConsole.exe, with optional two-pass EBU R128 loudness normalization.

Built for modding games that use Wwise audio (e.g., Age of Empires 2 DE custom taunts), but works for any MP3-to-WEM workflow.

How It Works

  1. ffmpeg converts each MP3 to WAV (Wwise only accepts WAV input)
  2. If normalization is enabled (default), ffmpeg runs a two-pass loudnorm analysis for each file — pass 1 measures loudness, pass 2 applies precise linear gain correction
  3. A .wsources XML file is generated listing all WAV files
  4. WwiseConsole.exe convert-external-source encodes all WAVs into .wem files
  5. Output .wem files are moved to the target folder and temp files are cleaned up

Requirements

Requirement Details
PowerShell 5.1+ (included with Windows 10/11)
Wwise Audiokinetic Wwise Authoring installed with the WWISEROOT environment variable set. Download here (free for non-commercial use). The script uses WwiseConsole.exe located at %WWISEROOT%\Authoring\x64\Release\bin\WwiseConsole.exe.
ffmpeg Must be available in your PATH or specified via -FfmpegPath. Download here, or install via winget install ffmpeg.

Usage

# Basic usage — converts all MP3s in current directory with default settings
# (two-pass normalization to -16 LUFS, Vorbis Quality High encoding)
.\Convert-Mp3ToWem.ps1

# Specify input and output folders
.\Convert-Mp3ToWem.ps1 -InputFolder "C:\MyTaunts\mp3" -OutputFolder "C:\MyMod\Sound\taunt"

# Adjust loudness target (quieter)
.\Convert-Mp3ToWem.ps1 -LufsTarget -20

# Skip normalization entirely
.\Convert-Mp3ToWem.ps1 -SkipNormalize

# Lower encoding quality (smaller file size)
.\Convert-Mp3ToWem.ps1 -Conversion "Vorbis Quality Low"

# Manual volume adjustment without normalization
.\Convert-Mp3ToWem.ps1 -SkipNormalize -Volume "-6dB"

# Combine normalization with additional volume trim
.\Convert-Mp3ToWem.ps1 -LufsTarget -16 -Volume "0.8"

# Explicit tool paths
.\Convert-Mp3ToWem.ps1 -FfmpegPath "C:\tools\ffmpeg.exe" -WwiseConsolePath "C:\Wwise\Authoring\x64\Release\bin\WwiseConsole.exe"

# Recurse into subdirectories
.\Convert-Mp3ToWem.ps1 -InputFolder "C:\Audio" -Recurse

Parameters

Parameter Default Description
-InputFolder . (current dir) Folder containing MP3 files
-OutputFolder same as input Destination for .wem files
-LufsTarget -16 Target loudness in LUFS. Common values: -14 (YouTube/podcasts), -16 (broadcast, recommended), -20 (quieter), -23 (EBU R128 EU broadcast)
-TruePeak -1.5 Maximum true peak ceiling in dBTP
-LoudnessRange 11 Target loudness range in LU
-SkipNormalize off Skip loudness normalization entirely
-Volume none Additional volume adjustment applied after normalization. Accepts fractions (0.5 = 50%) or decibels (-6dB)
-Conversion Vorbis Quality High Wwise conversion preset (case-sensitive). Options: Vorbis Quality High, Vorbis Quality Medium, Vorbis Quality Low, PCM
-FfmpegPath auto (PATH) Override path to ffmpeg.exe
-WwiseConsolePath auto (WWISEROOT) Override path to WwiseConsole.exe
-Recurse off Search for MP3 files in subdirectories

Two-Pass Normalization

Single-pass loudnorm uses dynamic processing that can introduce pumping artifacts. This script uses two-pass normalization by default:

  • Pass 1: Analyzes the entire file and measures integrated loudness, true peak, loudness range, and threshold
  • Pass 2: Applies a single precise linear gain adjustment using the measurements from pass 1 (linear=true)

This produces cleaner, more accurate results — especially important for short audio clips like taunts where dynamic normalization can behave unpredictably.

License

MIT — do whatever you want with it.

#Requires -Version 5.1
<#
.SYNOPSIS
Converts all MP3 files in a folder to Wwise Encoded Media (.wem) files.
.DESCRIPTION
Uses ffmpeg to convert MP3 to WAV, then WwiseConsole.exe convert-external-source
to produce .wem files.
PREREQUISITES:
1. Wwise installed (with WWISEROOT env var set).
Download from https://www.audiokinetic.com/en/download
WwiseConsole.exe lives at: %WWISEROOT%\Authoring\x64\Release\bin\WwiseConsole.exe
2. ffmpeg available in PATH or specified via -FfmpegPath.
.PARAMETER InputFolder
Path to the folder containing MP3 files. Defaults to current directory.
.PARAMETER OutputFolder
Path for the output WEM files. Defaults to InputFolder.
.PARAMETER WwiseConsolePath
Override path to WwiseConsole.exe. If not specified, auto-detects from WWISEROOT.
.PARAMETER FfmpegPath
Override path to ffmpeg.exe. If not specified, expects ffmpeg in PATH.
.PARAMETER Conversion
Wwise conversion setting name (case-sensitive). Examples:
"Vorbis Quality High" (default)
"Vorbis Quality Medium"
"Vorbis Quality Low"
"PCM"
.PARAMETER LufsTarget
Target loudness in LUFS for two-pass EBU R128 normalization (default: -16).
Common values:
-16 = broadcast standard (default, recommended for game taunts)
-14 = slightly louder (podcasts, YouTube)
-20 = quieter, blends into background
-23 = EBU R128 European broadcast
Two-pass mode measures first, then applies precise linear normalization.
.PARAMETER TruePeak
Maximum true peak in dBTP (default: -1.5). Used with LufsTarget.
.PARAMETER LoudnessRange
Target loudness range in LU (default: 11). Used with LufsTarget.
.PARAMETER SkipNormalize
If set, skips loudness normalization entirely.
.PARAMETER Volume
Manual volume adjustment applied after normalization. Examples:
"0.5" = 50% volume (quieter)
"1.5" = 150% volume (louder)
"-6dB" = reduce by 6dB
"6dB" = increase by 6dB
.PARAMETER Recurse
If set, searches for MP3 files in subdirectories as well.
.EXAMPLE
.\Convert-Mp3ToWem.ps1 -InputFolder "C:\MyMusic"
.EXAMPLE
.\Convert-Mp3ToWem.ps1 -InputFolder "C:\MyMusic" -Conversion "Vorbis Quality Low"
.EXAMPLE
.\Convert-Mp3ToWem.ps1 -LufsTarget -16
.EXAMPLE
.\Convert-Mp3ToWem.ps1 -LufsTarget -14 -TruePeak -1.0 -Volume "0.8"
#>
[CmdletBinding()]
param(
[Parameter(Position = 0)]
[string]$InputFolder = ".",
[string]$OutputFolder,
[string]$WwiseConsolePath,
[string]$FfmpegPath,
[string]$Conversion = "Vorbis Quality High",
[double]$LufsTarget = -16,
[double]$TruePeak = -1.5,
[double]$LoudnessRange = 11,
[switch]$SkipNormalize,
[string]$Volume,
[switch]$Recurse
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Continue"
# -- Resolve input folder ------------------------------------------------------
$InputFolder = (Resolve-Path $InputFolder).Path
if (-not $OutputFolder) {
$OutputFolder = $InputFolder
}
if (-not (Test-Path $OutputFolder)) {
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
}
$OutputFolder = (Resolve-Path $OutputFolder).Path
# -- Locate ffmpeg -------------------------------------------------------------
function Find-Ffmpeg {
param([string]$OverridePath)
if ($OverridePath -and (Test-Path $OverridePath)) {
return $OverridePath
}
$inPath = Get-Command "ffmpeg.exe" -ErrorAction SilentlyContinue
if ($inPath) { return $inPath.Source }
return $null
}
$ffmpeg = Find-Ffmpeg -OverridePath $FfmpegPath
if (-not $ffmpeg) {
Write-Host ""
Write-Host "ERROR: ffmpeg.exe not found." -ForegroundColor Red
Write-Host ""
Write-Host "Please do ONE of the following:" -ForegroundColor Yellow
Write-Host " 1. Install ffmpeg and add it to your PATH."
Write-Host " 2. Pass -FfmpegPath pointing to ffmpeg.exe."
Write-Host " Download from: https://ffmpeg.org/download.html"
Write-Host ""
exit 1
}
Write-Host "Using ffmpeg: $ffmpeg" -ForegroundColor Cyan
# -- Locate WwiseConsole.exe ---------------------------------------------------
function Find-WwiseConsole {
param([string]$OverridePath)
if ($OverridePath -and (Test-Path $OverridePath)) {
return $OverridePath
}
# Check WWISEROOT environment variable
$wwiseRoot = $env:WWISEROOT
if ($wwiseRoot) {
$candidate = Join-Path $wwiseRoot "Authoring\x64\Release\bin\WwiseConsole.exe"
if (Test-Path $candidate) {
return $candidate
}
}
# Search common Audiokinetic install roots
$searchRoots = @(
"${env:ProgramFiles(x86)}\Audiokinetic",
"$env:ProgramFiles\Audiokinetic",
"C:\Program Files (x86)\Audiokinetic",
"C:\Program Files\Audiokinetic"
) | Select-Object -Unique | Where-Object { Test-Path $_ }
foreach ($root in $searchRoots) {
$cli = Get-ChildItem -Path $root -Recurse -Filter "WwiseConsole.exe" -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if ($cli) { return $cli.FullName }
}
# Check PATH
$inPath = Get-Command "WwiseConsole.exe" -ErrorAction SilentlyContinue
if ($inPath) { return $inPath.Source }
return $null
}
$wwiseConsole = Find-WwiseConsole -OverridePath $WwiseConsolePath
if (-not $wwiseConsole) {
Write-Host ""
Write-Host "ERROR: WwiseConsole.exe not found." -ForegroundColor Red
Write-Host ""
Write-Host "Please do ONE of the following:" -ForegroundColor Yellow
Write-Host " 1. Install Wwise Authoring from https://www.audiokinetic.com/en/download"
Write-Host " (Ensure WWISEROOT environment variable is set after installation.)"
Write-Host " 2. Pass -WwiseConsolePath pointing to WwiseConsole.exe."
Write-Host " Typical location: WWISEROOT\Authoring\x64\Release\bin\WwiseConsole.exe"
Write-Host ""
exit 1
}
Write-Host "Using WwiseConsole: $wwiseConsole" -ForegroundColor Cyan
# -- Collect MP3 files ---------------------------------------------------------
$searchParams = @{ Path = $InputFolder; Filter = "*.mp3" }
if ($Recurse) { $searchParams.Recurse = $true }
$mp3Files = @(Get-ChildItem @searchParams -File)
if ($mp3Files.Count -eq 0) {
Write-Host "No MP3 files found in '$InputFolder'." -ForegroundColor Yellow
exit 0
}
Write-Host ""
Write-Host "Found $($mp3Files.Count) MP3 file(s) to convert." -ForegroundColor Green
Write-Host " Conversion : $Conversion"
if (-not $SkipNormalize) {
Write-Host " Normalize : Two-pass loudnorm -> $LufsTarget LUFS, TP=$TruePeak dBTP, LRA=$LoudnessRange LU"
}
else {
Write-Host " Normalize : Skipped"
}
if ($Volume) { Write-Host " Volume : $Volume" }
Write-Host " Output : $OutputFolder"
Write-Host ""
# -- Create temp working area --------------------------------------------------
$tempDir = Join-Path $env:TEMP "wem$(Get-Random -Maximum 9999)"
$wavTempDir = Join-Path $tempDir "wav"
$projectDir = Join-Path $tempDir "ConvProj"
$projectFile = Join-Path $projectDir "ConvProj.wproj"
$wsourcesFile = Join-Path $tempDir "list.wsources"
New-Item -ItemType Directory -Path $wavTempDir -Force | Out-Null
# Do NOT pre-create $projectDir - WwiseConsole create-new-project creates it itself
# -- Step 1: Convert MP3 to WAV using ffmpeg -----------------------------------
Write-Host "Step 1: Converting MP3 to WAV via ffmpeg..." -ForegroundColor Cyan
$wavFiles = @()
foreach ($mp3 in $mp3Files) {
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($mp3.Name)
$wavOut = Join-Path $wavTempDir "$baseName.wav"
# Handle name collisions
if (Test-Path $wavOut) {
$counter = 1
do {
$wavOut = Join-Path $wavTempDir "${baseName}_${counter}.wav"
$counter++
} while (Test-Path $wavOut)
}
if (-not $SkipNormalize) {
Write-Host " $($mp3.Name) -> $(Split-Path $wavOut -Leaf) [2-pass] ... " -NoNewline
}
else {
Write-Host " $($mp3.Name) -> $(Split-Path $wavOut -Leaf) ... " -NoNewline
}
# Two-pass loudnorm normalization
if (-not $SkipNormalize) {
# Pass 1: Analyze loudness, capture JSON measurements
Write-Host "" -NoNewline
$loudnormFilter = "loudnorm=I=${LufsTarget}:TP=${TruePeak}:LRA=${LoudnessRange}:print_format=json"
$pass1Output = & $ffmpeg -hide_banner -y -i "$($mp3.FullName)" -af $loudnormFilter -f null NUL 2>&1
$pass1Text = $pass1Output | Out-String
# Extract measured values directly from ffmpeg output
# (avoids JSON parsing issues from binary id3v2 metadata containing { characters)
$gotAll = $true
$mI = $mTP = $mLRA = $mThresh = $mOffset = $null
if ($pass1Text -match '"input_i"\s*:\s*"([^"]+)"') { $mI = $matches[1] } else { $gotAll = $false }
if ($pass1Text -match '"input_tp"\s*:\s*"([^"]+)"') { $mTP = $matches[1] } else { $gotAll = $false }
if ($pass1Text -match '"input_lra"\s*:\s*"([^"]+)"') { $mLRA = $matches[1] } else { $gotAll = $false }
if ($pass1Text -match '"input_thresh"\s*:\s*"([^"]+)"') { $mThresh = $matches[1] } else { $gotAll = $false }
if ($pass1Text -match '"target_offset"\s*:\s*"([^"]+)"'){ $mOffset = $matches[1] } else { $gotAll = $false }
if (-not $gotAll) {
Write-Host "FAIL (loudnorm pass 1)" -ForegroundColor Red
Write-Host " Could not parse loudnorm measurements." -ForegroundColor DarkRed
continue
}
# Pass 2: Apply precise linear normalization using measured values
$pass2Filter = "loudnorm=I=${LufsTarget}:TP=${TruePeak}:LRA=${LoudnessRange}"
$pass2Filter += ":measured_I=${mI}:measured_TP=${mTP}:measured_LRA=${mLRA}"
$pass2Filter += ":measured_thresh=${mThresh}:offset=${mOffset}:linear=true"
# Append volume filter if specified
if ($Volume) {
$pass2Filter += ",volume=$Volume"
}
$ffmpegOutput = & $ffmpeg -hide_banner -loglevel warning -y -i "$($mp3.FullName)" -af $pass2Filter "$wavOut" 2>&1
}
else {
# No normalization - simple convert with optional volume
$audioFilters = @()
if ($Volume) {
$audioFilters += "volume=$Volume"
}
$filterArgs = @()
if ($audioFilters.Count -gt 0) {
$filterArgs = @("-af", ($audioFilters -join ","))
}
$ffmpegOutput = & $ffmpeg -hide_banner -loglevel warning -y -i "$($mp3.FullName)" @filterArgs "$wavOut" 2>&1
}
if ($LASTEXITCODE -eq 0 -and (Test-Path $wavOut)) {
Write-Host "OK" -ForegroundColor Green
$wavFiles += $wavOut
}
else {
Write-Host "FAIL" -ForegroundColor Red
if ($ffmpegOutput) {
$errText = $ffmpegOutput | Out-String
Write-Host " $errText" -ForegroundColor DarkRed
}
}
}
if ($wavFiles.Count -eq 0) {
Write-Host "No WAV files were produced. Aborting." -ForegroundColor Red
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
exit 1
}
# -- Step 2: Create Wwise project ---------------------------------------------
Write-Host ""
Write-Host "Step 2: Creating temporary Wwise project..." -ForegroundColor Cyan
$createOutput = & $wwiseConsole create-new-project "$projectFile" 2>&1
if (-not (Test-Path $projectFile)) {
Write-Host " First attempt failed. Trying with pre-created directory..." -ForegroundColor Yellow
if ($createOutput) {
Write-Host " Output: $($createOutput | Out-String)" -ForegroundColor DarkYellow
}
# Some versions need the parent dir to exist
New-Item -ItemType Directory -Path $projectDir -Force | Out-Null
$createOutput = & $wwiseConsole create-new-project "$projectFile" 2>&1
}
if (-not (Test-Path $projectFile)) {
Write-Host " ERROR: Could not create Wwise project." -ForegroundColor Red
if ($createOutput) {
Write-Host " Output: $($createOutput | Out-String)" -ForegroundColor DarkRed
}
Write-Host " Cleaning up and exiting." -ForegroundColor Red
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
exit 1
}
else {
Write-Host " Project created: $projectFile" -ForegroundColor Green
}
# -- Step 3: Build .wsources XML ----------------------------------------------
Write-Host ""
Write-Host "Step 3: Building .wsources file..." -ForegroundColor Cyan
$wsLines = @()
$wsLines += '<?xml version="1.0" encoding="UTF-8"?>'
$wsLines += "<ExternalSourcesList SchemaVersion=""1"" Root=""$wavTempDir"">"
foreach ($wav in $wavFiles) {
$wavName = Split-Path $wav -Leaf
$wsLines += " <Source Path=""$wavName"" Conversion=""$Conversion""/>"
}
$wsLines += "</ExternalSourcesList>"
$wsContent = $wsLines -join "`r`n"
[System.IO.File]::WriteAllText($wsourcesFile, $wsContent, [System.Text.Encoding]::UTF8)
Write-Host " Written: $wsourcesFile" -ForegroundColor Green
# -- Step 4: Run WwiseConsole convert-external-source --------------------------
Write-Host ""
Write-Host "Step 4: Running WwiseConsole convert-external-source..." -ForegroundColor Cyan
$convertOutput = & $wwiseConsole convert-external-source "$projectFile" --source-file "$wsourcesFile" --output "$OutputFolder" 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host " WwiseConsole exited with code $LASTEXITCODE." -ForegroundColor Yellow
if ($convertOutput) {
$convertOutput | Out-String | Write-Host -ForegroundColor DarkYellow
}
}
else {
Write-Host " Conversion command completed (exit 0)." -ForegroundColor Green
}
# -- Step 5: Move .wem files from Windows subfolder ----------------------------
# WwiseConsole outputs into a "Windows" subfolder under the output path.
$windowsSubDir = Join-Path $OutputFolder "Windows"
$wemCount = 0
if (Test-Path $windowsSubDir) {
$wemFiles = @(Get-ChildItem -Path $windowsSubDir -Filter "*.wem" -File -ErrorAction SilentlyContinue)
foreach ($wem in $wemFiles) {
$dest = Join-Path $OutputFolder $wem.Name
Move-Item -Path $wem.FullName -Destination $dest -Force
$wemCount++
}
# Clean up the empty Windows subfolder
Remove-Item -Path $windowsSubDir -Recurse -Force -ErrorAction SilentlyContinue
}
# Also check if any .wem landed directly in the output folder
$directWem = @(Get-ChildItem -Path $OutputFolder -Filter "*.wem" -File -ErrorAction SilentlyContinue)
if ($directWem.Count -gt $wemCount) {
$wemCount = $directWem.Count
}
# -- Summary -------------------------------------------------------------------
Write-Host ""
Write-Host "===========================================" -ForegroundColor Cyan
Write-Host " Conversion complete!" -ForegroundColor Cyan
Write-Host " WEM files produced : $wemCount" -ForegroundColor Green
Write-Host " Output folder : $OutputFolder" -ForegroundColor Cyan
Write-Host "===========================================" -ForegroundColor Cyan
Write-Host ""
if ($wemCount -eq 0) {
Write-Host "==========================================================" -ForegroundColor Yellow
Write-Host " TROUBLESHOOTING" -ForegroundColor Yellow
Write-Host "==========================================================" -ForegroundColor Yellow
Write-Host " No .wem files were found in the output." -ForegroundColor Yellow
Write-Host ""
Write-Host " Common causes:" -ForegroundColor Yellow
Write-Host " - The Conversion name is case-sensitive." -ForegroundColor Yellow
Write-Host " Try: 'Vorbis Quality High', 'Vorbis Quality Medium'," -ForegroundColor Yellow
Write-Host " 'Vorbis Quality Low', or 'PCM'." -ForegroundColor Yellow
Write-Host " - Wwise Authoring may not be fully installed." -ForegroundColor Yellow
Write-Host " Ensure you installed the Authoring component." -ForegroundColor Yellow
Write-Host ""
Write-Host " WwiseConsole output:" -ForegroundColor Yellow
if ($convertOutput) {
$convertOutput | Out-String | Write-Host -ForegroundColor DarkYellow
}
Write-Host ""
Write-Host " Check logs in: $tempDir" -ForegroundColor Yellow
Write-Host "==========================================================" -ForegroundColor Yellow
Write-Host ""
Write-Host "Temp directory preserved for debugging: $tempDir" -ForegroundColor DarkGray
}
else {
# Clean up temp files on success
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
Write-Host "Done." -ForegroundColor Gray
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment