Skip to content

Instantly share code, notes, and snippets.

@Solessfir
Last active December 30, 2025 18:04
Show Gist options
  • Select an option

  • Save Solessfir/cf0a22b30c6459f9d7436f6678bb8ad4 to your computer and use it in GitHub Desktop.

Select an option

Save Solessfir/cf0a22b30c6459f9d7436f6678bb8ad4 to your computer and use it in GitHub Desktop.
PowerShell script to batch convert FLAC to MP3 using FFmpeg
<#
.SYNOPSIS
Recursively converts FLAC tracks to MP3 320 kbps using FFmpeg.
Usage:
.\convert.ps1 | Converts all FLACs in the current directory to 320k MP3 in a "_Converted" subfolder.
.\convert.ps1 -DestinationRoot "D:\Music\MP3" -Bitrate 256k | Converts files to the specified path at 256kbps.
#>
[CmdletBinding()]
param(
[Alias("h", "help")]
[switch]$ShowHelp,
[Parameter(Position=0)]
[string]$DestinationRoot,
[Parameter(Position=1)]
[ValidatePattern("^\d+k$")]
[string]$Bitrate = "320k"
)
if ($ShowHelp) {
Get-Help $PSCommandPath
exit
}
if (!(Get-Command ffmpeg -ErrorAction SilentlyContinue)) {
Write-Host "Error: FFmpeg was not found in your system path." -ForegroundColor Red
Write-Host "Please install it using the following command (Admin PowerShell):" -ForegroundColor Yellow
Write-Host "`n winget install --id Gyan.FFmpeg`n" -ForegroundColor Cyan
return
}
$sourceRoot = (Get-Location).Path
if ([string]::IsNullOrWhiteSpace($DestinationRoot)) {
$DestinationRoot = Join-Path $sourceRoot "_Converted"
}
if (!(Test-Path $DestinationRoot)) {
New-Item -ItemType Directory -Path $DestinationRoot -Force | Out-Null
}
Write-Host "--- Audio Conversion Started ---" -ForegroundColor Cyan
Write-Host "Source: $sourceRoot"
Write-Host "Destination: $DestinationRoot"
Write-Host "Bitrate: $Bitrate" -ForegroundColor Cyan
# Get all unique folders containing FLAC files
$albumDirs = Get-ChildItem -Path $sourceRoot -Recurse -File -Filter *.flac |
Select-Object -ExpandProperty DirectoryName -Unique |
Where-Object { $_ -notlike "*$DestinationRoot*" }
foreach ($dir in $albumDirs) {
# 1. Path Calculation and Folder Renaming
$relativeDir = ""
if ($dir.Length -gt $sourceRoot.Length) {
$relativeDir = $dir.Substring($sourceRoot.Length).TrimStart([IO.Path]::DirectorySeparatorChar)
}
# "Year Album" -> "Year - Album"
$pathSegments = $relativeDir.Split([IO.Path]::DirectorySeparatorChar)
$renamedSegments = $pathSegments | ForEach-Object {
$_ -replace '^(\d{4})\s+(?!-\s)', '$1 - '
}
$newRelativeDir = $renamedSegments -join [IO.Path]::DirectorySeparatorChar
$targetDir = Join-Path $DestinationRoot $newRelativeDir
if (!(Test-Path $targetDir)) { New-Item -ItemType Directory -Path $targetDir -Force | Out-Null }
Write-Host "`nFolder: $relativeDir" -ForegroundColor Magenta
# 2. Cover Art Handling (.jpg, .jpeg, .png)
$artFile = Get-ChildItem -Path $dir -File | Where-Object {
$_.BaseName -eq "Cover" -and ($_.Extension -match '^\.(jpg|jpeg|png)$')
} | Select-Object -First 1
if ($artFile) {
Copy-Item -Path $artFile.FullName -Destination (Join-Path $targetDir $artFile.Name) -Force
Write-Host " [OK] Cover art: $($artFile.Name)" -ForegroundColor Yellow
}
# 3. Process FLAC files
$flacsInDir = Get-ChildItem -Path $dir -Filter *.flac
foreach ($flac in $flacsInDir) {
$matchingCue = Join-Path $dir ($flac.BaseName + ".cue")
if (Test-Path $matchingCue) {
# --- SCENARIO: MONOLITHIC FLAC ---
Write-Host " [CUE] Splitting image: $($flac.Name)" -ForegroundColor Cyan
$globalMetadata = @{ PERFORMER = "Unknown Artist"; TITLE = $flac.BaseName; DATE = "" }
$tracks = @()
$currentTrack = $null
Get-Content $matchingCue -Encoding UTF8 | ForEach-Object {
$line = $_.Trim()
if ($line -match '^REM DATE (\d+)') { $globalMetadata.DATE = $matches[1] }
elseif ($line -match '^PERFORMER "(.*)"') { if ($currentTrack) { $currentTrack.Artist = $matches[1] } else { $globalMetadata.PERFORMER = $matches[1] } }
elseif ($line -match '^TITLE "(.*)"') { if ($currentTrack) { $currentTrack.Title = $matches[1] } else { $globalMetadata.TITLE = $matches[1] } }
elseif ($line -match '^TRACK (\d+) AUDIO') {
if ($currentTrack) { $tracks += $currentTrack }
$currentTrack = [ordered]@{ Number = $matches[1]; Title = "Track $($matches[1])"; Artist = $globalMetadata.PERFORMER; Start = 0 }
}
elseif ($line -match 'INDEX 01 (\d+):(\d+):(\d+)') {
$currentTrack.Start = ([int]$matches[1] * 60) + [int]$matches[2] + ([int]$matches[3] / 75.0)
}
}
if ($currentTrack) { $tracks += $currentTrack }
for ($i = 0; $i -lt $tracks.Count; $i++) {
$t = $tracks[$i]
$startStr = $t.Start.ToString("0.000", [System.Globalization.CultureInfo]::InvariantCulture)
$duration = if ($i -lt $tracks.Count - 1) { $tracks[$i+1].Start - $t.Start } else { $null }
$outName = "$($t.Number). $($t.Title -replace '[\\/:*?"<>|]', '_').mp3"
$outPath = Join-Path $targetDir $outName
$args = @("-ss", $startStr, "-i", "`"$($flac.FullName)`"")
if ($artFile) { $args += @("-i", "`"$($artFile.FullName)`"") }
$args += @("-map", "0:a")
if ($artFile) { $args += @("-map", "1:v", "-c:v", "copy", "-disposition:v", "attached_pic") }
$args += @("-ab", $Bitrate, "-id3v2_version", "3", "-metadata", "title=$($t.Title)", "-metadata", "artist=$($t.Artist)", "-metadata", "album=$($globalMetadata.TITLE)", "-metadata", "date=$($globalMetadata.DATE)", "-metadata", "track=$($t.Number)/$($tracks.Count)")
if ($duration) { $args += @("-t", $duration.ToString("0.000", [System.Globalization.CultureInfo]::InvariantCulture)) }
$args += "`"$outPath`""
cmd /c "ffmpeg -hide_banner -loglevel error -y $args"
}
}
else {
# --- SCENARIO: SEPARATE TRACKS ---
Write-Host " [FILE] Converting: $($flac.Name)" -ForegroundColor Green
$outPath = Join-Path $targetDir ($flac.BaseName + ".mp3")
$args = @("-i", "`"$($flac.FullName)`"")
if ($artFile) { $args += @("-i", "`"$($artFile.FullName)`"") }
$args += @("-map", "0:a")
if ($artFile) { $args += @("-map", "1:v", "-c:v", "copy", "-disposition:v", "attached_pic") }
$args += @("-ab", $Bitrate, "-map_metadata", "0", "-id3v2_version", "3", "-y", "`"$outPath`"")
cmd /c "ffmpeg -hide_banner -loglevel error $args"
}
}
}
Write-Host "`nConversion Complete!" -ForegroundColor Green
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment