見出し画像

ALSAライブラリのAPIを使ってWAVファイルを再生

ALSAライブラのAPIを使ったWAVファイルの音声データを再生するためのプログラムについて説明します。

CQ出版のインターフェース誌2014年9月号「ハイレゾLinuxオーディオ」の「第1部第3章 Linux 用超定番サウンドI/OライブラリALSA入門:大津 秀紀」の記事を参考にさせていただきました。

下記は、WAVファイルをALSAライブラリのAPIを使って再生するためのC/C++のサンプルコードです。

#include <stdio.h>
#include <string.h>

// (1)
#include <alsa/asoundlib.h>

#define	BUFFER_SAMPLES	588

int main(int argc, char *argv[])
{
	if(argc == 1){
		printf("Usage: %s <wav> [dev]\n", argv[0]);
		return -1;
	}

	// WAVファイルをオープン
	FILE *fp = fopen(argv[1], "rb");
	if(fp == NULL){
		printf("Can't open wav: %s\n", argv[1]);
		return -1;
	}

	// WAVファイルからパラメータのチャンクを読み取る
	char cid[5] = "CHID";
	uint32_t clen;

	printf("---- WAV FILE ----\n");
	fread(cid, 4, 1, fp); printf("'%s'", cid);		// 'RIFF'
	fread(&clen, sizeof(clen), 1, fp);
	printf(", len = %d\n", clen);

	fread(cid, 4, 1, fp); printf("'%s'\n", cid);	// 'WAVE'
	fread(cid, 4, 1, fp); printf("'%s'", cid);		// 'fmt '
	fread(&clen, sizeof(clen), 1, fp);
	printf(", len = %d\n", clen);

	uint8_t *cbuf = new uint8_t[clen];
	fread(cbuf, clen, 1, fp);

	uint16_t formatTag		= *(uint16_t *)&cbuf[0];
	uint16_t channels 		= *(uint16_t *)&cbuf[2];
	uint32_t samplesPerSec	= *(uint32_t *)&cbuf[4];
	uint32_t avgBytesPerSec	= *(uint32_t *)&cbuf[8];
	uint16_t blockAlign 	= *(uint16_t *)&cbuf[12];
	uint16_t bitsPerSample	= *(uint16_t *)&cbuf[14];
	delete[] cbuf;
	
	printf("  formatTag = %d\n", formatTag);
	printf("  channels = %d\n", channels);
	printf("  samplesPerSec = %d\n", samplesPerSec);
	printf("  avgBytesPerSec = %d\n", avgBytesPerSec);
	printf("  blockAlign = %d\n", blockAlign);
	printf("  bitsPerSample = %d\n", bitsPerSample);

	fread(cid, 4, 1, fp); printf("'%s'", cid);		// 'data'
	fread(&clen, sizeof(clen), 1, fp);
	printf(", len = %d\n", clen);

	if(strcmp(cid, "data") != 0){
		printf("WAV file format error.\n");
		fclose(fp);
		return -1;
	}
	int data_len = clen;

	// 音楽データの読み取り用バッファの領域を確保する
	uint8_t *buffer = new uint8_t[blockAlign * BUFFER_SAMPLES];

	// (2)
	snd_pcm_t *hndl = NULL;
	snd_output_t *output = NULL;
	const char *dev = "default";
	if(argc > 2){
		dev = argv[2];
	}

	int soft_resample = 1;
	unsigned int latency = 50000;

	// (3)
	snd_pcm_format_t format = SND_PCM_FORMAT_S16_LE;
	if((bitsPerSample == 24) && (blockAlign == 6)){
		format = SND_PCM_FORMAT_S24_3LE;
	}

	// (4)
	int ret = snd_pcm_open(&hndl, dev, SND_PCM_STREAM_PLAYBACK, 0);
	if(ret != 0){
		printf("snd_pcm_open: error %d\n", ret);
		goto End;
	}

	// (5)
	ret = snd_pcm_set_params(hndl, format, SND_PCM_ACCESS_RW_INTERLEAVED, channels, samplesPerSec, soft_resample, latency);
	if(ret != 0){
		printf("snd_pcm_set_params: error %d\n", ret);
		goto End;
	}

	// (6)
	snd_output_stdio_attach(&output, stdout, 0);
	printf("---- PCM DUMP ----\n");
	snd_pcm_dump(hndl, output);

	for(int n = 0; n < data_len; n += (blockAlign * BUFFER_SAMPLES)){
		// 音楽データの読み取り
		ret = fread(buffer, blockAlign, BUFFER_SAMPLES, fp);
		if(ret > 0){
			// (7)
			snd_pcm_writei(hndl, (const void *)buffer, ret);
		}
	}
	// (8)
	snd_pcm_drain(hndl);

End:
	// (9)
	if(hndl != NULL) snd_pcm_close(hndl);
	if(fp != NULL) fclose(fp);
	if(buffer != NULL) delete[] buffer;

	return 0;
}

以下にALSAに関係する部分の簡単な説明を書いておきます。

出力先のデバイス名などに関しては、「Linuxオーディオ:ALSAとPulseAudioの関係」を参照してください。

(1) ALSAのライブラリを利用するためalsa/asoundlib.hをインクルードします。
(2) ALSAで音声再生で必要なハンドルなどを初期化します。
デバイス名(dev)は、コマンドラインの第2引数で指定されていれば("hw:2,0", "plughw:2,0", "sysdefault:0"など)それを使いますが、指定が無い場合はけ"default"としています。
soft_resampleはリサンプルをするかどうかの指定で、0とした場合は、例えば24bit音声を16bitに変換するなどは行わなくなります。なので、例えばラズパイ5でsysdefault:0で24bit音声ファイルをHDMIへ出力したい場合にはエラーになります。
(3) formatは入力音声のフォーマットを指定します。とりあえず、S16_LE(2byte/16bit/little endian)と、S24_3LE(3byte/24bit/little endian)だけテスト用に簡易的に対応させていますが、色々な音声のフォーマットに対応するには、もっときちんとした変更をする必要があります。
(4) snd_pcm_openでPCMをオープンします。
(5) snd_pcm_set_paramsで入力ファイルのパラメータを指定します。
下記のようにsnd_pcm_hw_paramas_tの変数に個々のパラメータをセットしてしてsnd_pcm_hw_paramsで指定する方法もあるようです。

    snd_pcm_hw_params_t *params;
    snd_pcm_hw_params_alloca(&params);
    snd_pcm_hw_params_any(hndl, params);
    snd_pcm_hw_params_set_format(hndl, params, format);
    ・・・・必要なパラメータをセット・・・・
    ret = snd_pcm_hw_params(hndl, params);
    if(ret < 0){
        printf("snd_pcm_hw_params: error %d\n", ret);
        goto End;
    }

(6) snd_pcm_dumpでPCMの情報を表示させています。確認用なので、この3行はなくても問題ありません。
(7) 音声データをsnd_pcm_writeiで書き込みます。書き込みデータの長さはフレーム数の単位です。
(8) 再生が終了したらsnd_pcm_drainですべてを出力させて終了します。
(9) 最後にオープンしたハンドルをsnd_pcm_openでクローズします。

上記のソースコードをalsa_simple.cppなどのファイル名で保存し、下記のようにコンパイルすればOKでしょう。

$ g++ -o alsa_simle alsa_simple.cpp -lasound

コンパイルしたプログラムをWAVの音声ファイル名を指定して実行すれば再生が開始します。第2引数として出力先デバイスを指定すれば、そちらのオーディカードに出力されます。

例えば、自分のラズパイ5の場合だと、JVCハイレゾリューションサイトに置いてある24bit/192kHzのハイレゾ音源のwavファイルを、PiDAC+で再生させる場合には、下記のように実行します。

$ ./alsa_simple Keep_your_side_24bit192kHz_short.wav plughw:2,0

---- WAV FILE ----
'RIFF', len = 51806244
'WAVE'
'fmt ', len = 16
  formatTag = 1
  channels = 2
  samplesPerSec = 192000
  avgBytesPerSec = 1152000
  blockAlign = 6
  bitsPerSample = 24
'data', len = 51806208
---- PCM DUMP ----
Plug PCM: Linear conversion PCM (S24_LE)
Its setup is:
  stream       : PLAYBACK
  access       : RW_INTERLEAVED
  format       : S24_3LE
  subformat    : STD
  channels     : 2
  rate         : 192000
  exact rate   : 192000 (192000/1)
  msbits       : 24
  buffer_size  : 9600
  period_size  : 480
  period_time  : 2500
  tstamp_mode  : NONE
  tstamp_type  : MONOTONIC
  period_step  : 1
  avail_min    : 480
  period_event : 0
  start_threshold  : 9600
  stop_threshold   : 9600
  silence_threshold: 0
  silence_size : 0
  boundary     : 5404319552844595200
Slave: Hardware PCM card 2 'RPi DAC+' device 0 subdevice 0
Its setup is:
  stream       : PLAYBACK
  access       : MMAP_INTERLEAVED
  format       : S24_LE
  subformat    : STD
  channels     : 2
  rate         : 192000
  exact rate   : 192000 (192000/1)
  msbits       : 32
  buffer_size  : 9600
  period_size  : 480
  period_time  : 2500
  tstamp_mode  : NONE
  tstamp_type  : MONOTONIC
  period_step  : 1
  avail_min    : 480
  period_event : 0
  start_threshold  : 9600
  stop_threshold   : 9600
  silence_threshold: 0
  silence_size : 0
  boundary     : 5404319552844595200
  appl_ptr     : 0
  hw_ptr       : 0

S24_3LE(3byte/24bit/Little Endian)から、PiDAC+の対応するS24_LE(4byte/24bit/Little Endian)に変換されて音声が再生されます。

HDMIに出力させたい場合には、"sysdefault:0"にすれば同様にHDMIに対応したフォーマットに変換されて音声再生がされます。

以上、基本的には、snd_pcm_open, snd_pcm_set_params, snd_pcm_writeiの3つのAPIを使えば音声を再生できます。

音楽の再生とともにタイムコードを表示させたい場合は、snd_pcm_delay()関数で、内部のバッファ(リングバッファ)で再生出力待ちのフレーム数(delay)が取得できるので、snd_pcm_writeiで書き込んだタイムコードからディレイ分を引くことで、再生中のタイムコードが取得すれば良いようです。

また、リングバッファの空き状態に合わせてデータを書き込みしたい場合は、snd_pcm_avail()関数でバッファの空き容量(avail)がわかるので、それを見ながら書き込めば良さそうです。snd_pcm_avail_delay()関数を使えば、delayとavailの値を同時に取得もできます。

より詳しい使い方を知りたい方は、ALSA project - the C library referenceにその他の多くのAPIの説明があるので、そちらも参考になると思います。また、pcm.cのサンプルコードでは、コールバックによる再生など、他の再生方法などのサンプルコードもあり参考になるように思います。

これまでに、音楽CDからの音声データの取得(リッピング)方法CDDB(音楽データベース)からの楽曲データの取得方法ALSAとPulseAudioによる音楽再生と説明してきました。今回のALSAライブラのAPIを使ってWAVファイルの音声データを再生できるようになりました。これで、音楽CDを含むPCM音声を再生するための基本的なプログラムは作成できるようになったかなと思います。


いいなと思ったら応援しよう!