動画や音声ファイルの音量(ゲイン)を ffmpeg で調整するときはコマンド1発でやりたい

## Abstract

[動画や音声ファイルの音量(ゲイン)を ffmpeg で調整するときのメモ](https://qiita.com/mml/items/c28d69ab889eb36dd522)をPowerShellで一つのコマンドにしたかったが、当時(約1年前)苦労したので備忘録。

https://qiita.com/mml/items/c28d69ab889eb36dd522

## 環境

- Windows 10
- PoweShell v7.4.0
- ffmpeg version 6.1-full_build-www.gyan.dev

## 目的

mp3とかの音楽ファイルの`max_volume`を0dB位になるように調整すること。
0以外にもできるように引数を生やしたりして、次のようにコマンドで変換できるようにする。

```powershell
Set-MaxVolume -InputFile hoge.mp3 -OutputFile fuga.mp3 -TargetVolume 0.0
```

## 方針

1. ffmepgのログ出力から、max_volumeを抽出
1. ゲインを上げたら`$TargetVolume`になるように、上げるゲイン`X`[dB]を計算
1. `ffmpeg -i input -af volume=XdB output`で調整

簡単ですね！

## 完成したもの

```powershell
function Select-ffmpegLog([parameter(mandatory=$true)][string]$File, [string]$Pattern = "max_volume:") {
    # ffmpegは標準エラーに出力されてしまうので、標準出力に渡しなおす
    ffmpeg -i $File -vn -af volumedetect -f null - 2>&1 | Select-String $Pattern;
}

function Find-MaxVolume([parameter(mandatory=$true)][string]$File) {
    <#
        .SYNOPSIS
            対象ファイルのmax_volumeを'10 dB'の形式で取得して返す
    #>
    $pattern = "max_volume:";
    $raw = Select-ffmpegLog $File $pattern;
    $raw.Tostring().Substring($raw.Matches.Index + $pattern.Length);
}

function Remove-dB([parameter(mandatory=$true)][string]$dBString) {
    <#
        .SYNOPSIS
            '10 dB' -> '10'のように取り出す
    #>
    $dBString = $dBString.Replace(' ', '');
    $dBString -replace "(\d+)dB", "`$1";
}

function Get-VolumeGain([parameter(mandatory=$true)][string]$File, [float]$TargetVolume = 0.0) {
    $max_volume = [float](Remove-dB (Find-MaxVolume $File));
    $gain = [Math]::Truncate($TargetVolume - $max_volume * 10) / 10;     # 小数点第1位で切り捨て
    "${gain}dB";
}

function Set-MaxVolume([parameter(mandatory=$true)][string]$InputFile, [parameter(mandatory=$true)][string]$OutputFile, [float]$TargetVolume = 0.0) {
    $gain = Get-VolumeGain $InputFile $TargetVolume;
    ffmpeg -i $InputFile -af volume=$gain $OutputFile;
}
```

## ハマりどころと解説

### ffmpegのログ出力を受け取れない

A. ffmpegのログ出力は**標準エラー**に出るから。

初心者殺し。当時沼った。
ということで、次のようにしてログを得て、max_volumeの行を`Select-String`で抽出します。

```powershell
function Select-ffmpegLog([parameter(mandatory=$true)][string]$file, [string]$pattern = "max_volume:") {
    # ffmpegのログ出力は標準エラーに出力されてしまうので、標準出力に渡しなおす
    ffmpeg -i $file -vn -af volumedetect -f null - 2>&1 | Select-String $pattern;
}
```

### 取得したログからmax_volumeを抽出

`Select-String`の返り値から、Matchオブジェクトを取り出して、ゲインを取り出す。

```powershell
function Find-MaxVolume([parameter(mandatory=$true)][string]$file) {
    <#
        .SYNOPSIS
            対象ファイルのmax_volumeを'10 dB'の形式で取得して返す
    #>
    $pattern = "max_volume:";
    $raw = Select-ffmpegLog $file $pattern;
    $raw.Tostring().Substring($raw.Matches.Index + $pattern.Length);
}
```

### 上げるゲインを計算する

得られたmax_volume`'10 dB'`から10を取り出して、上げるゲインを計算して、`'2dB'`みたいな形式で得る。
つまり文字列 -> float -> 計算 -> 文字列という処理が必要。
次のように`[float]`でキャストしたりして処理する。

ちなみに`Replace`メソッドでは正規表現による置換ができない。

```powershell
function Remove-dB([parameter(mandatory=$true)][string]$dBString) {
    <#
        .SYNOPSIS
            '10 dB' -> '10'のように取り出す
    #>
    $dBString = $dBString.Replace(' ', '');
    $dBString -replace "(\d+)dB", "`$1";
}

function Get-VolumeGain([parameter(mandatory=$true)][string]$File, [float]$TargetVolume = 0.0) {
    $max_volume = [float](Remove-dB (Find-MaxVolume $File));
    $gain = [Math]::Truncate($TargetVolume - $max_volume * 10) / 10;     # 小数点第1位で切り捨て
    "${gain}dB";
}
```