PCMのデータを自分でいじってデジタルオーディオを理解する

私はいままで音を扱ったことはなかったので、PCMのことは聞いたことがあるという程度でした。仕事でオーディオのデバイスドライバを触ることになったので、その前提知識としてPCMのことを調べていたのですが、実際にいろいろといじってみたらとてもよく理解できたので紹介します。

PCMとは

高校の物理で習ったとおり、音は空気の振動の波です。マイクの音はアナログの波形として入力されますが、AUDIO CODECと呼ばれるチップでデジタルの波形に変換されてコンピュータに入ってきます。逆に音を出すときにはデジタルの波形をAUDIO CODECに流すと、そこでアナログの波形に変換されてスピーカーを鳴らします。
このAUDIO CODECとやりとりするデジタルの波形のフォーマットがPCMです。

PCM(Pluse Code Modulation)についてはこちらの記事がわかりやすいです。
‚o‚b‚l‚ÌŠî‘b’mŽ¯

ここでは主に量子化ビット数: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データがどんなものなのかを体で理解することができました。