|
#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 |