iTunes内のALACファイル→FLAC変換をPowerShellで並列実行できるようにする
前回
の失敗した点を改善していくお話。
背景・動機
前回のスクリプトでは、シングルスレッドとして稼働していたこともあり、FFmpegの処理がボトルネックになってしまった。
そのため、PowerShell側で並列処理を与えるようにすることで速度の改善を行いたい。
また、WAVの変換を混ぜたことでメタデータがないFLACができあがるミスもやってしまったので、併せて対処したい。
今回のゴール
- 前回のPowerShellのスクリプトに並列処理を盛り込む。
- WAVを変換する場合にFLACタグにメタデータを書き込めるようにする。
タイトル
環境
前回と同じ。
- Windows 11 / Desktop
- ディスク容量は十分に大きなものを用意しておくこと
- FFmpeg 6
- PowerShell V7.3.8 or Later.
- 今回V7以降に入っている並列処理の機能を使うため、Windows内蔵版では動かないと思います。
調査
PowerShellで並列処理ってどうやって動かすの?
$maxParallelJobs = 4
echo $maxParallelJobs "parallel runs."
$Message = "Output:"
1..100 | ForEach-Object -Parallel {
"$using:Message $_"
Start-Sleep 1
} -ThrottleLimit $maxParallelJobs
こういうスクリプトで動く模様。
実際に走らせると、同時に4つに制限が掛かった状態になっていることがわかる。
CPUの物理コアの最大値ってどうやって知るの?
# CPUコア数を取得。
$cpuCores = (Get-CimInstance -ClassName Win32_Processor).NumberOfCores
Intel12世代以降では、Pコア、Eコアの種類が存在するが、この処理ではコア数だけを取り出してくるため、そのあたりを区別しない。注意。
また、コア数をフルで使おうとすると、システム側で何か処理が起きたときに割り振るリソースがなくなって宜しくないので、「物理コア数 - 1」くらいにとどめておくのが良いかもしれない。
一例としてCore i9-12900Kは、8コア16スレッドのPコア+8コア8スレッドのEコア=計16コア24スレッドを実現したCPUとなるのだが、そのあたりの実行最大値を決める計算をPowerShell上でさせるのは地味にしんどそう。
なので、タスクマネージャーとかから直接自身のマシンのコア数見て、手で最適な最大値に書き換えても良いと思う。
一応、今回のサンプルでは、プログラムらしく、スクリプト上からマシンの値を参照するようにはしておく。
これらを合体すると
# CPUコア数を取得。
$cpuCores = (Get-CimInstance -ClassName Win32_Processor).NumberOfCores
# 並列処理の上限を設定(コア数-1)
$maxParallelJobs = $cpuCores - 1
echo $maxParallelJobs "parallel runs."
$Message = "Output:"
1..100 | ForEach-Object -Parallel {
"$using:Message $_"
Start-Sleep 1
} -ThrottleLimit $maxParallelJobs
FLACのタグってどうやって編集するの
FFmpeg の引数にタグを編集するオプションがあるので、変換時に一緒に引き渡すのがよさそう。
# 前回のスクリプト
$ffmpegCommand = "ffmpeg -i `"$($file.FullName)`" -acodec flac -vcodec copy `"$dstFilePath`""
# タグを混ぜるオプションを入れた場合(一例)
$ffmpegCommand = "ffmpeg -i `"$($file.FullName)`" -acodec flac -vcodec copy -metadata album="Album" `"$dstFilePath`""
これを wav のパターンのelseifを用意して、そこで実行してあげれば問題なさそう。
作業
前回のスクリプトを書き直す
先ほどのマルチスレッドの処理とWavの分岐を付け足したコードが以下。
# アーティスト名のリスト
$allowedArtists = @(
"@Plesio_'s Mastery Puppetz",
"BOaT"
)
# 元のフォルダと格納先のフォルダを指定
$srcDir = "E:\MUSIC\iTunes Media\Music"
$dstDir = "E:\TO_XPERIA_FLAC_TEST"
# ソースディレクトリ内のファイルを再帰的に取得
$files = Get-ChildItem -Path $srcDir -File -Recurse
# CPUコア数を取得。
$cpuCores = (Get-CimInstance -ClassName Win32_Processor).NumberOfCores
# 並列処理の上限を設定(コア数-1)
$maxParallelJobs = $cpuCores - 1
$d = Get-Date
Write-Output $d | Out-File -FilePath .\result.log
$filteredFiles = @() # 結果を格納するための配列を初期化
#
foreach ($artist in $allowedArtists){
# アーティストのフォルダパスを組み立てる
$artistFolder = Join-Path $srcDir $artist
if (Test-Path $artistFolder -PathType Container) {
$files = Get-ChildItem -Path $artistFolder -File -Recurse
foreach ($fileTmp in $files){
$filteredFiles += $fileTmp
}
}
}
Write-Host "$maxParallelJobs parallel runs."
Write-Output "$maxParallelJobs parallel runs." | Out-File -Append -FilePath .\result.log
Write-Output "${filteredFiles.Count} files target." | Out-File -Append -FilePath .\result.log
# ファイルごとに処理
$job = $filteredFiles | ForEach-Object -Parallel {
$file = $_
# JOBで分離するためループ内部で再定義する。
$srcDir = "E:\MUSIC\iTunes Media\Music"
$dstDir = "E:\TO_XPERIA_FLAC_TEST"
# Title
$title = [System.IO.Path]::GetFileNameWithoutExtension($file.Name)
$trackNum = 1
if ($file.Name -match '^(\d{2}\s)(.*)') {
$trackNum = [int]$matches[1]
$title = $matches[2]
} else {
# Nothing.
}
# タイトルの前の余分なスペースをトリム
$title = $title.Trim()
# ファイルの拡張子を取得
$extension = $file.Extension.ToLower()
# ファイルの親ディレクトリを取得
$fileParentDir = [System.IO.Path]::GetDirectoryName($file.FullName)
$album = (Split-Path -Path $fileParentDir -Leaf)
# アルバムの親ディレクトリを取得
$albumParentDir = [System.IO.Path]::GetDirectoryName($fileParentDir)
# アルバムの親ディレクトリからアーティスト名を抽出
$artist = (Split-Path -Path $albumParentDir -Leaf)
$rtn = ""
if ($extension -eq ".m4a") {
# 拡張子が.m4aまたは.wavの場合、変換処理を実行
$dstFilePath = $file.FullName.Replace($srcDir, $dstDir) -replace [regex]::Escape($extension), ".flac"
$dstDirPath = [System.IO.Path]::GetDirectoryName($dstFilePath)
# 出力ディレクトリが存在しない場合、作成
if (-not (Test-Path -Path $dstDirPath)) {
New-Item -Path $dstDirPath -ItemType Directory
}
$startTime = Get-Date
# ffmpegコマンドで変換:強制上書きを有効にした。
$ffmpegCommand = "ffmpeg -y -i `"$($file.FullName)`" -acodec flac -vcodec copy `"$dstFilePath`" "
Invoke-Expression $ffmpegCommand
$endTime = Get-Date
$executionTime = $endTime - $startTime
#$rtn = "FullName=" + $file.FullName + ", srcDir=" + $srcDir + ", dstDir=" + $dstDir + " , dstFilePath="+ ${dstFilePath}+ "\n"
$rtn = "END: $endTime, exec_time: $executionTime - $artist > $album > $title"
} elseif ( $extension -eq ".wav") {
# 拡張子が.m4aまたは.wavの場合、変換処理を実行
$dstFilePath = $file.FullName.Replace($srcDir, $dstDir) -replace [regex]::Escape($extension), ".flac"
$dstDirPath = [System.IO.Path]::GetDirectoryName($dstFilePath)
# 出力ディレクトリが存在しない場合、作成
if (-not (Test-Path -Path $dstDirPath)) {
New-Item -Path $dstDirPath -ItemType Directory
}
$startTime = Get-Date
# ffmpegコマンドで変換:強制上書きを有効にした。
$ffmpegCommand = "ffmpeg -y -i `"$($file.FullName)`" -acodec flac -vcodec copy -metadata album=`"$album`" -metadata title=`"$title`" -metadata artist=`"$artist`" `"$dstFilePath`" "
Invoke-Expression $ffmpegCommand
$endTime = Get-Date
$executionTime = $endTime - $startTime
#$rtn = "FullName=" + $file.FullName + ", srcDir=" + $srcDir + ", dstDir=" + $dstDir + " , dstFilePath="+ ${dstFilePath}+ "\n"
$rtn = "END: $endTime, exec_time: $executionTime - $artist > $album > $title"
} else {
# それ以外の場合、単にコピー
$dstFilePath = $file.FullName.Replace($srcDir, $dstDir)
$dstDirPath = [System.IO.Path]::GetDirectoryName($dstFilePath)
# 出力ディレクトリが存在しない場合、作成
if (-not (Test-Path -Path $dstDirPath)) {
New-Item -Path $dstDirPath -ItemType Directory
}
# 強制上書きを有効にした。
Copy-Item -Force -Path $file.FullName -Destination $dstFilePath
$endTime = Get-Date
#$rtn = "FullName=" + $file.FullName + ", srcDir=" + $srcDir + ", dstDir=" + $dstDir + " , dstFilePath="+ ${dstFilePath}+ "\n"
$rtn = "END: $endTime, exec_time: no(Copy-Only) - $artist > $album > $title"
}
$rtn
} -ThrottleLimit $maxParallelJobs -AsJob
$job | Receive-Job -Wait >> result.log
Write-Host "処理が完了しました。"
よく見ると、ディレクトリの作成のIF文は切り出せるとおもうからもっと短くできるね。面倒だからそのままにしとくけど。
実行テストも兼ねていろいろ触った結果、こうなった。
やはり、単純な合体では動作しないものだ。
手頃ないくつかのアーティストに絞って試験
目的は「ちゃんとFFmpegが並列実行されている?」という疑問を解消すること。
仕事で開発をしていると、「おまえ、同時に1処理しか動かないUIスレッド様か!!??」みたいなことがある。
今回はさすがにないと思うが、CPUリソースの割り当てが頭悪くて1個のCPUに待機列を生成する可能性が0ではない...
スパイクがかかっている箇所が ALAC->FLAC変換が数曲発生した箇所となる。一応..
数を絞りすぎて、わかりにくいので、アーティスト数を増やして、100曲くらい流せるようにして再検証。
良い感じだ。成功っぽい。
じゃあ本番実行だ!おやすみ... あれ?
機は熟した。あとは実行して寝るだけだ。
やり直しとなったの変換作業だが効率がマシになるはず。
これから300GBくらいぶん回して深夜中に終わってほしい、おやすみ。
あ、"""モニターは切って、ログオン画面にしておこう"""
...
おはよう。さすがに終わったかな・・・ってあれ?
(全然進んでない!!!)
なんと、PowerShell君、ログオン画面にまで戻されると処理が止まる仕様になっていた・・・
うわめんどくせ
正しく動作させるにはもうちょっと考えないといけないようだ。
自宅のマシンで自分以外触ることもないので、ひとまず、一時的に自動ロックの設定を完全にOFFにして、夜間バッチとして実行し、終わったら設定を戻すことで解決することにした。
所感
PowerShell で並列処理できるけど、動かせるようになるまで相当しんどい。
結局、良い感じの実装に最後まで落とし込むことができなかったが、とりあえず目的を達成できたのでヨシ
Discussion