PCMのデータを自分でいじってデジタルオーディオを理解する
私はいままで音を扱ったことはなかったので、PCMのことは聞いたことがあるという程度でした。仕事でオーディオのデバイスドライバを触ることになったので、その前提知識としてPCMのことを調べていたのですが、実際にいろいろといじってみたらとてもよく理解できたので紹介します。
PCMとは
高校の物理で習ったとおり、音は空気の振動の波です。マイクの音はアナログの波形として入力されますが、AUDIO CODECと呼ばれるチップでデジタルの波形に変換されてコンピュータに入ってきます。逆に音を出すときにはデジタルの波形をAUDIO CODECに流すと、そこでアナログの波形に変換されてスピーカーを鳴らします。
このAUDIO CODECとやりとりするデジタルの波形のフォーマットがPCMです。
PCM(Pluse Code Modulation)についてはこちらの記事がわかりやすいです。
oblÌîbm¯
ここでは主に量子化ビット数:16, サンプリングレート48kHzのPCMデータを扱います。
ffmpegでPCMデータを扱う
本題はPCMデータの加工なので、その前後の面倒なところはffmpegにやってもらいます。
音声ファイルをPCMデータに変換する
$ ffmpeg -i src.ogg -f s16le -ar 48k -ac 2 d.pcm
ar はサンプリングレート。ac はチャネル数。ステレオなら2, モノラルなら1
PCMデータを音声ファイルに変換する
$ ffmpeg -f s16le -ar 48k -ac 2 -i d.pcm d.wav
PCMデータを再生する
$ ffplay -f s16le -ar 48k -ac 2 d.pcm
あるいは、alsaのutilityコマンドを使って
$ aplay -f dat d.pcm
-f dat は -f S16_LE -c2 -r48000 の省略形。
PCMデータのサンプリングレートを変換する
$ ffmpeg -f s16le -ar 48k -ac 2 -i d.pcm -f s16le -ar 44.1k -ac 2 d_441.pcm
PCMデータの中を見てみる
まずは手元にあるPCMのデータをダンプして中をのぞいてみます。これは 16bitのモノラル、サンプリングレート48kHzのデータです。
$ od -x a.pcm |head 0000000 0003 0003 0003 0004 0003 0002 0004 0001 0000020 0004 0004 0002 0002 0002 0001 0005 0003 0000040 0002 0002 0000 0003 0002 0005 0006 0002 0000060 0005 0004 0000 0005 0003 0001 0001 0003 0000100 fff5 001b ffe0 0040 ff87 07b5 037e 0292 0000120 02be 0256 0273 022f 01c2 012a 011d 0164 0000140 0157 00e6 007c 0070 00e2 01a8 0214 01cc 0000160 0167 014d 016f 01a0 019b 0133 00a4 0027 0000200 fffc 0029 0075 0092 004b ffe7 ffc6 ffc3 0000220 ffd3 ffee ffdf ff97 ff40 fef0 fed4 fef1
このデータのファイルサイズは約3MB。
$ ls -l a.pcm -rwxr-xr-x 1 koba koba 3105280 Sep 30 10:09 a.pcm
再生してみます。
$ ffplay -f s16le -ar 48k -ac 1 a.pcm ... [s16le @ 0x7fc9440008c0] Estimating duration from bitrate, this may be inaccurate Input #0, s16le, from 'a.pcm': Duration: 00:00:32.35, bitrate: 767 kb/s Stream #0:0: Audio: pcm_s16le, 48000 Hz, 1 channels, s16, 768 kb/s 35.37 M-A: -0.000 fd= 0 aq= 0KB vq= 0KB sq= 0B f=0/0
正しく再生できました。
演奏時間は約32秒。16bitは2バイトなので
2 * 48000 * 32 = 3,072,000
ファイルサイズとのつじつまも合います。
モノラル(1ch)のPCMデータをステレオに変換する
モノラルのPCMデータは符号付き16bitのデータが単純に並んでいるだけです。そしてステレオのデータはそれが16bit x2のペアが並びます。
以下のようなCの関数で、元のデータにひとつづつ0を挿入してみます。
int mono2left(char *inbuf, char *outbuf, int len) { int16_t *inp; int16_t *outp; int cnt = len / sizeof(int16_t); inp = (int16_t*)inbuf; outp = (int16_t*)outbuf; while (cnt-- > 0) { *outp++ = *inp; *outp++ = 0; inp++; } return (char*)outp - outbuf; }
mono2left.c
そしてでき上がったデータがこちら。
$ od -x a_left.pcm |head 0000000 0003 0000 0003 0000 0003 0000 0004 0000 0000020 0003 0000 0002 0000 0004 0000 0001 0000 0000040 0004 0000 0004 0000 0002 0000 0002 0000 0000060 0002 0000 0001 0000 0005 0000 0003 0000 0000100 0002 0000 0002 0000 0000 0000 0003 0000 0000120 0002 0000 0005 0000 0006 0000 0002 0000 0000140 0005 0000 0004 0000 0000 0000 0005 0000 0000160 0003 0000 0001 0000 0001 0000 0003 0000 0000200 fff5 0000 001b 0000 ffe0 0000 0040 0000 0000220 ff87 0000 07b5 0000 037e 0000 0292 0000
これを -ac 1 を 2 に変えて再生してみます。
$ ffplay -f s16le -ar 48k -ac 2 a_left.pcm
みごとに元の音が左チャネルからでるステレオのPCMデータになりました。
以下のようにすれば、右チャネルからでるようになります。
while (cnt-- > 0) { *outp++ = 0; *outp++ = *inp; inp++; }
mono2right.c
以下のようにすれば左右両方から同じ音が出ます。
while (cnt-- > 0) { *outp++ = *inp; *outp++ = *inp; inp++; }
mono2stereo.c
なるほど。
PCMデータの音量を変更する
高校の物理で習ったとおり、音の大きさは波形の振幅で決まります。振幅を変更するにはかけ算でできます。
以下の関数は左と右で独立にボリュームを変更できるようにしてみました。
int changeVol(char *inbuf, char *outbuf, int len, float vol_l, float vol_r) { int16_t *inp; int16_t *outp; int cnt = len / sizeof(int16_t) / 2; inp = (int16_t*)inbuf; outp = (int16_t*)outbuf; while (cnt-- > 0) { *outp++ = *inp++ * vol_l; *outp++ = *inp++ * vol_r; } return (char*)outp - outbuf; }
vol.c
これで音の大きさが変わりました。
2つのPCMデータをミックスする
高校の物理で習ったとおり、2つの音の合成は足し算です。
実際には16bitで表現できる範囲を超えてしまうとまずいので、ボリュームを絞ってから足し算するようにします。
ただし2つのPCMデータを合成するには、そのフォーマット(量子化ビット数、サンプリングレート)が同じであることが前提になります。(その変換はffmpegなどのツールでやっておきます。)
int mix(char *inbuf1, char *inbuf2, char *outbuf, int len, float vol1, float vol2) { int16_t *inp1, *inp2; int16_t *outp; int cnt = len / sizeof(int16_t); inp1 = (int16_t*)inbuf1; inp2 = (int16_t*)inbuf2; outp = (int16_t*)outbuf; while (cnt-- > 0) { *outp++ = (*inp1++ * vol1) + (*inp2++ * vol2); } return (char*)outp - outbuf; }
mix.c
確かにこれで2つのPCMデータを混ぜることができました。単純なミキサーの原理がわかりました。
PCMデータの部分切り出し
PCMデータはモノラルなら2バイト、ステレオなら4バイトのデータの羅列です。なので、head, tailコマンドをバイト単位で指定することで任意の部分を切り出すことができます。
ステレオ、48kHzなら1秒間のデータ長は2 * 2 * 48000 = 192000 になります。
先頭の1秒間だけ切り出す
$ head --byte=192000 a2.pcm > a2_first1sec.pcm
最後の1秒間を捨てる
$ head --byte=-192000 a2.pcm > a2_all_but_last1sec.pcm
最後の1秒間だけ切り出す
$ tail --byte=192000 a2.pcm > a2_last1sec.pcm
先頭の1秒間を捨てる
$ tail --byte=+192000 a2.pcm > a2_all_but_first1sec.pcm
PCMデータの連結
連結は単にcatコマンドでつなぐだけです。
$ cat a2.pcm b2.pcm > c2.pcm
回数を指定してリピートするには以下のようにループを回します。
$ rm -f a2_10times.pcm $ for i in `seq 10`; do cat a2.pcm >> a2_10times.pcm; done
まとめ
PCMデータの基本的な加工はこのように簡単な演算でできることがわかりました。これらを組み合わせれば、自在にテストデータを作れそうです。
実際にデータを加工して、その音を聞いて確認することを繰り返すうちにPCMデータがどんなものなのかを体で理解することができました。