🎤

ZOOM H4e の 32-bit float WAV ファイルを ffmpeg で mp3 に

2024/08/07に公開

ZOOM のハンディレコーダー H4essential は 32-bit float で録音できて最高なんだけど、そのままではファイルサイズが巨大かつ再生できる環境を選ぶので、ffmpeg を使ってノーマライズと mp3 への変換を行う。

基本的には 任意のラウドネス値に音量を調整する loudnorm に書かれた手順を Ruby スクリプトで実行しているだけ。

デフォルトのパラメータは オーディオコンテンツのラウドネス値の正規化 - Alexa Skills Kit を参考にした。

スクリプト

#!/usr/bin/env ruby
require "open3"
require "shellwords"
require "json"
require "optparse"

def sh(*args)
  puts args.shelljoin

  out = ""
  err = ""
  status = nil

  Open3.popen3(*args) do |stdin, stdout, stderr, wait_thread|
    # スレッドを使って標準エラー出力をリアルタイムで処理
    err_thread = Thread.new do
      stderr.each_char do |char|
        $stderr.print char
        err << char
      end
    end

    # 標準出力をキャプチャ
    stdout.each_char do |char|
      out << char
    end

    # 標準エラー出力のスレッドが完了するのを待機
    err_thread.join

    # プロセスの終了を待つ
    status = wait_thread.value
  end

  # ステータスコードを確認
  unless status.success?
    puts err
    # exit 1
  end

  [out, err]
end

def loudnorm(array)
  "loudnorm=" + array.map {|e|
    case e
    when Hash
      e.map {|k, v| "#{k}=#{v}"}.join(":")
    else
      e
    end
  }.join(",")
end

options = {
  i: -14, # integrated loudness target
  tp: -3, # maximum true peak
  lra: 11, # loudness range target
}

OptionParser.new do |o|
  o.banner = "Usage: #{File.basename($0).gsub(".rb", "")} [options] INPUT_FILE OUTPUT_FILE"
  o.on("-i", "--integrated-loudness NUMBER", Float, "Integrated loudness target") {|v| options[:i] = v}
  o.on("-tp", "--true-peak NUMBER", Float, "Maximum true peak") {|v| options[:tp] = v}
  o.on("-lra", "--loudness-range NUMBER", Float, "Loudness range target") {|v| options[:lra] = v}
  o.parse!(ARGV)
end

input, output = ARGV

unless input
  warn "Input file required."
  exit 1
end

unless output
  warn "Output file required."
  exit 1
end

i = options.fetch(:i)
tp = options.fetch(:tp)
lra = options.fetch(:lra)

out, err = sh(
  "ffmpeg",
  "-i", input,
  "-af", loudnorm([{I: i, TP: tp, LRA: lra, print_format: "json"}]),
  "-f", "null",
  "-",
)

result = JSON.parse(err.match(/Parsed_loudnorm.+?\n(.+)/m)[1])

sh(
  "ffmpeg",
  "-y", # Overwrite output files without asking
  "-i", input,
  "-af", loudnorm(
    [
      {
        I: i, TP: tp, LRA: lra, print_format: "json",
        measured_I: result.fetch("input_i"),
        measured_TP: result.fetch("input_tp"),
        measured_LRA: result.fetch("input_lra"),
        measured_thresh: result.fetch("input_thresh"),
        offset: result.fetch("target_offset"),
      },
      "channelmap=channel_layout=stereo",
      "aresample=44100:resampler=soxr",
    ]
  ),
  "-c:v", "copy",
  "-c:a", "mp3",
  output,
)

使い方

ruby wav2mp3.rb 入力ファイル 出力ファイル

Discussion