ffmpegでPCM音源をWAVE形式に変換するときにハマったこと

こんにちは。
ffmpegでPCM音源を変換するときに、期待したとおりに変換されなくてハマったので備忘録を残します。

渡しの場合、音声を扱うプログラムも経験がなく、そもそもサンプリングレートとかの用語からして分からないという感じでした。
同じ悩みを抱える方がいればと思い備忘録を残します。

用語の説明

音声ファイルについての用語を調べてまとめて書こうと思ったのですが、
この記事とWikipedia読めばだいたい必要な情報は揃ってしまったので、ご紹介にとどめます。

サンプリングレート・ビット深度・ビットレートの意味と関係性

PCMってなに

PCMは音声波形を電子化したもので、「音」そのものと捉えるとしっくり来ました。

PCM(ぴーしーえむ)とは - コトバンク

PCMの基礎知識

PCMとWAVEファイルの違い

WAVE(.wav)ファイルはよく見る形式だと思うのでファイル自体の説明は割愛します。ここでは違いについてだけ。
.wavとPCMの大きな違いは、メタデータの有無です。

PCMはただの音声波形にすぎず、「サンプリングレート」や「ビットレート」などの情報がファイル自体に含まれていません。
一方WAVEファイルの中にはそれらのメタデータ+PCMが含まれています。
この差が何を起こすか、コードベースで説明します。

問題のコード

ffmpegをナマで扱うのはつらいので、fluent-ffmpegというnpmパッケージを利用させていただきました。
バイナリのパスの指定なども柔軟にできるので、Lambdaとかクセのあるランタイム下でも扱いやすいです。

で、以下が問題のあるコードです。
変換元の音声ファイルは16000Hz, Signed, 16bit, LPCMです。

const ffmpeg = require('fluent-ffmpeg')

const pcmPath = './demo' # pcm
const destPath = './demo.wav'

ffmpeg()
  .input(pcmPath)
  .inputFormat('s16be')
  .output(destPath)
  .run()

ffmpegは拡張子を見てinput, outputをよしなにしてくれるらしいと噂を聞いたので、シンプルに実装してみました。
s16beというのは、Signedで16bitでBig endianなPCMのフォーマットを指します。
変換した結果が、これです。

なんかキュルキュル言ってる。
本来であれば、以下ような音声が再生されるのが期待する変換処理です。

PCMにはメタデータがない

ここでWAVEファイルのバイナリ構成を整理してみます。

開始byte 終了byte byte データ内容
1 4 4 'RIFF'の4文字
5 8 4 総ファイルサイズ-8(byte)
9 12 4 'WAVE'の4文字
13 16 4 'fmt 'の4文字 フォーマットチャンク
17 20 4 フォーマットサイズ デフォルト値16
21 22 2 フォーマットコード 非圧縮のPCMフォーマットは1
23 24 2 チャンネル数 モノラルは1、ステレオは2
25 28 4 サンプリングレート 44.1kHzの場合なら44100
29 32 4 バイト/秒 1秒間の録音に必要なバイト数
33 34 2 ブロック境界 ステレオ16bitなら、16bit*2 = 32bit = 4byte
35 36 2 ビット/サンプル 1サンプルに必要なビット数
37 40 4 'data'の4文字 フォーマットチャンク
41 44 4 総ファイルサイズ-126

WAVEファイルの構造

見ての通り、ファイルの中に「チャンネル数」「サンプリングレート」などの情報が保持されています。
なのでffmpegなどにWAVEファイルのパスを与えれば、ファイルの中身から音声の情報を読み取り変換処理が実行できます。

一方、PCMにはこれらの情報が含まれていないので、ファイルパスと音声ファイルのフォーマット(s16be)だけを与えても情報が足りません
なのでPCMだけをffmpegに与えても、音声プレイヤーやコンバータなどは与えられた音声波形をどう扱えば良いかが分からず、期待した通りに再生/変換されないなどの現象が起こります。

解決策:inputOptions

上記の問題に気づけずに、ひたすらオプションを組み替えて試していたら、偶然うまくいった例がありました。

const ffmpeg = require('fluent-ffmpeg')

const pcmPath = './demo.pcm'
const destPath = './demo.wav'

ffmpeg()
  .input(pcmPath)
  .inputOptions(['-ac 1', '-ar 16000'])
  .inputFormat('s16be')
  .output(destPath)
  .run()

inputOptionsが増えました。それ以外は同じです。
変換するための情報としてサンプリングレート(-ar)が欠けていたようです。

ということで、PCM音源をffmpegで変換する際には、符号化方式やサンプリングレートなどのPCMファイルには含まれない情報を明示的に指定する必要があります。

さいごに

変換処理のデモに使わさせていただいた音声は、docomoの音声合成APIを利用しています。
後日公開ですが、こちらの記事も見ていただけると幸いです。

(2017/08/08ごろ公開予定)docomoの音声合成APIを利用して無料でVOICEROIDっぽい声を生成してみる