Last active
August 15, 2024 18:07
-
-
Save milnak/7459b156915e64adaa07350393b171fa to your computer and use it in GitHub Desktop.
Extract audio files from CUE file: Uses ffmpeg.exe as a helper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # https://wiki.hydrogenaud.io/index.php?title=Cue_sheet | |
| Param( | |
| # Path to CUE file | |
| [Parameter(Mandatory = $true)][string]$CuePath | |
| ) | |
| function Parse-Cue { | |
| Param([Parameter(Mandatory = $true)][string]$CuePath) | |
| # Top level object. Includes File array. | |
| $albumInfo = @{ | |
| Files = @() | |
| } | |
| $cue = Get-Content -LiteralPath $CuePath | ForEach-Object { $_.Trim() } | |
| $parsingfile = $false | |
| foreach ($line in $cue) { | |
| $spaceIdx = $line.IndexOf(' ') | |
| $key = $line.Substring(0, $spaceIdx) | |
| $value = $line.Substring($spaceIdx + 1).Trim('"') | |
| if ($key -eq 'REM') { | |
| # REVIEW: Treat REM as if it wasn't a REM. | |
| $spaceIdx = $value.IndexOf(' ') | |
| $key = $value.Substring(0, $spaceIdx) | |
| $value = $value.Substring($spaceIdx + 1).Trim('"') | |
| } | |
| if ($key -eq 'FILE') { | |
| # REVIEW: Assume that once a FILE tag is seen, the rest is FILE metadata. | |
| $parsingFile = $true | |
| } | |
| if (-not $parsingFile) { | |
| # ALBUM metadata | |
| if ($key -in 'DATE', 'DISCID', 'COMMENT', 'CATALOG', 'COMPOSER', 'GENRE', 'PERFORMER', 'TITLE') { | |
| $albumInfo.Add($key, $value) | |
| } | |
| else { | |
| Write-Warning "Invalid ALBUM value: $key" | |
| } | |
| } | |
| else { | |
| # Parsing FILE objects | |
| if ($key -eq 'FILE') { | |
| # New file | |
| # FILE "Range.wav" WAVE | |
| $spaceIdx = $value.LastIndexOf(' ') | |
| $fileName = $value.Substring(0, $spaceIdx).Trim('"') | |
| $fileType = $value.Substring($spaceIdx + 1) | |
| $albumInfo.Files += @{ | |
| Filename = $fileName | |
| Type = $fileType | |
| Tracks = @() | |
| } | |
| } | |
| elseif ($key -eq 'TRACK') { | |
| # New track | |
| # TRACK 01 AUDIO | |
| $spaceIdx = $value.IndexOf(' ') | |
| $trackNumber = $value.Substring(0, $spaceIdx) | |
| $trackType = $value.Substring($spaceIdx + 1) | |
| $numFiles = $albumInfo.Files.Count | |
| $albumInfo.Files[$numFiles - 1].Tracks += @{ | |
| Number = [int]$trackNumber | |
| Type = $trackType | |
| Indices = @() | |
| } | |
| } | |
| elseif ($key -in 'COMPOSER', 'FLAGS', 'TITLE', 'PERFORMER', 'ISRC') { | |
| # TITLE "Red Rain" | |
| # PERFORMER "Peter Gabriel" | |
| $numFiles = $albumInfo.Files.Count | |
| $numTracks = $albumInfo.Files[$numFiles - 1].Tracks.Count | |
| $albumInfo.Files[$numFiles - 1].Tracks[$numTracks - 1] += @{ | |
| $key = $value | |
| } | |
| } | |
| elseif ($key -eq 'INDEX') { | |
| # INDEX 00 28:41:15 | |
| # INDEX 01 28:42:06 | |
| $spaceIdx = $value.IndexOf(' ') | |
| $indexTime = $value.Substring($spaceIdx + 1) | |
| $numFiles = $albumInfo.Files.Count | |
| $numTracks = $albumInfo.Files[$numFiles - 1].Tracks.Count | |
| $albumInfo.Files[$numFiles - 1].Tracks[$numTracks - 1].Indices += $indexTime | |
| } | |
| else { | |
| Write-Warning "Invalid FILE value: $key" | |
| } | |
| } | |
| } | |
| $albumInfo | |
| } | |
| function Convert-MSFToHMS { | |
| Param([string]$CueTime) | |
| $split = $CueTime -split ':' | |
| if ($split.Length -ne 3) { | |
| Write-Warning "Invalid CUE time: $CueTime" | |
| return | |
| } | |
| if ([int]$split[2] -ge 75) { | |
| Write-Warning "Invalid frames: $CueTime" | |
| return | |
| } | |
| # CUE time is in m:s:f. Frame is 1/75th of a second. Convert to milliseconds. | |
| $timespan = New-TimeSpan -Minutes ([int]$split[0]) -Seconds ([int]$split[1]) -Milliseconds ([int]($split[2] / 75 * 1000)) | |
| # ffmpeg time unit format is sexagesimal (HOURS:MM:SS.MILLISECONDS, as in 01:23:45.678) | |
| $timespan.ToString('hh\:mm\:ss\.fff') | |
| } | |
| function Make-LegalFilename { | |
| Param([Parameter(Mandatory = $true)][string]$Name) | |
| # Replace invalid characters with '_' | |
| $regex = '[{0}]' -f [RegEx]::Escape([IO.Path]::GetInvalidFileNameChars() -join '') | |
| $Name -replace $regex, '_' | |
| } | |
| function Run-FFMPEG { | |
| Param([Parameter(Mandatory = $true)]$Arguments) | |
| $psi = New-Object Diagnostics.ProcessStartInfo | |
| $psi.FileName = 'ffmpeg.exe' | |
| $psi.RedirectStandardError = $true | |
| $psi.RedirectStandardOutput = $false | |
| $psi.UseShellExecute = $false | |
| $psi.Arguments = $Arguments | |
| $psi.WorkingDirectory = ((Get-Location).Path) | |
| $proc = New-Object System.Diagnostics.Process | |
| $proc.StartInfo = $psi | |
| $proc.Start() | Out-Null | |
| # Important: ReadToEnd must come before WaitForExit! | |
| $err = $proc.StandardError.ReadToEnd().Trim() | |
| $proc.WaitForExit() | Out-Null | |
| $result = $proc.ExitCode | |
| if ($result -ne 0) { | |
| Write-Warning "Failed ($result):" | |
| $err | |
| } | |
| } | |
| function Extract-Cue { | |
| Param([PSCustomObject]$CueInfo) | |
| foreach ($file in $CueInfo.Files) { | |
| if ($file.Type -ne 'WAVE') { | |
| Write-Warning ('Unsupported type: {0}, skipping.' -f $file.Type) | |
| continue | |
| } | |
| $inputFile = $file.Filename | |
| for ($trackNum = 0; $trackNum -lt $file.Tracks.Count; $trackNum++) { | |
| $track = $file.Tracks[$trackNum] | |
| if ($track.Type -ne 'AUDIO') { | |
| Write-Warning ('Unsupported type: {0}, skipping.' -f $track.Type) | |
| continue | |
| } | |
| $outputFile = Make-LegalFilename -Name ('{0:d2} {1} - {2}.flac' -f $track.Number, $track.TITLE, $track.PERFORMER) | |
| # Print status | |
| '[{0}/{1}] {2}' -f ($trackNum + 1), $file.Tracks.Count, $outputFile | |
| $ssIndex = 0 | |
| if ($track.Indices.Count -ne 1) { | |
| Write-Warning ('Track {0} has {1} indexes. Using index {2} as start.' -f ($trackNum + 1), $track.Indices.Count, ($track.Indices.Count - 1)) | |
| $ssIndex = $track.Indices.Count - 1 | |
| } | |
| $ss = Convert-MSFToHMS -CueTime $track.Indices[$ssIndex] | |
| $arguments = @('-n', '-hide_banner', '-nostdin', '-ss', $ss) | |
| if ($trackNum + 1 -ne $file.Tracks.Count) { | |
| # Not last track. | |
| $to = Convert-MSFToHMS -CueTime $file.Tracks[$trackNum + 1].Indices[0] | |
| $arguments += @('-to', $to) | |
| } | |
| $arguments += @('-i', """$inputFile""", """$outputFile""") | |
| Run-FFMPEG -Arguments $arguments | |
| } | |
| } | |
| } | |
| if (-not (Get-Command 'ffmpeg.exe' -CommandType Application)) { | |
| Write-Warning 'ffmpeg.exe not found or not in path.' | |
| exit 1 | |
| } | |
| if ((Split-Path -Path $CuePath -Leaf).Split(".")[1] -ne 'cue') { | |
| Write-Warning "Need to provide a CUE file." | |
| exit 1 | |
| } | |
| if (-not (Test-Path -LiteralPath $CuePath -Type Leaf)) { | |
| Write-Warning ('File {0} not found.' -f $CuePath) | |
| exit 1 | |
| } | |
| $cue = Parse-Cue -CuePath $CuePath | |
| "Performer: $($cue.PERFORMER)" | |
| "Title: $($cue.TITLE)" | |
| '' | |
| # $cue | ConvertTo-Json -Depth 5 | |
| Extract-Cue -CueInfo $cue |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment