D言語で音楽再生

# はじめに
[D言語アドベントカレンダー2020](https://qiita.com/advent-calendar/2020/dlang) 5日目の記事となります。
アドベントカレンダーへの投稿は、はじめてですが、よろしくお願いします。

# 予備知識
D言語の記事を初めて読まれる方を想定して、私なりに説明します。
説明不要な方は、[本題](#本題)へお進みください。

## D言語とは
[D言語](https://ja.wikipedia.org/wiki/D%E8%A8%80%E8%AA%9E)はプログラミング言語です。（これは説明するまでもないですね？？）
D言語の特徴やいい点を一言で説明するのは難しいので、私のお気に入りのQiita記事を紹介します。
[D言語をご紹介したい](https://qiita.com/cedretaber/items/90b7b34ee710eb5cc965)
[ネタじゃないD言語](https://qiita.com/JJ1LIS/items/0c0117f2ac3c362b916d)
[D言語のUFCSが好きだ！](https://qiita.com/nak2yoshi/items/1bbd91fb23d7cc67109e)
[全ての開発者が学ぶべき1つの言語](https://qiita.com/outlandkarasu@github/items/a2243eb3adbe906ec30d)

## D言語のコンパイラについて
[D言語コンパイラ](https://dlang.org/download.html)は、`DMD`、`GDC`、`LDC`の３つほど存在します。
`DMD`は公式のD言語コンパイラの位置づけです。
[バージョンアップリリース](https://dlang.org/changelog/release-schedule.html)が2か月ごとのペースで続いており、新しい言語仕様を取り込みながら、活発な開発が続いています。
その他にも、`gcc`系の`GDC`、`llvm`系の`LDC`が存在しますので、状況に応じて選択可能です。

## D言語にはDUBというパッケージマネージャがあります

D言語では、標準ライブラリ[Phobos](https://dlang.org/phobos/index.html)の他に、[DUB](https://code.dlang.org/)で提供されるライブラリを利用可能です。
`DUB`に登録されたライブラリは、統一された方法で取り扱い可能で、ライブラリ間の依存性も解決されます。
活用することで、開発できるアプリケーションの幅が広がります。

## SDLとは
[SDL](https://www.libsdl.org/)は、クロスプラットフォームのマルチメディアライブラリです。グラフィックの描画やサウンドの再生などのAPIを提供しています。インタフェース部はPerl、Python、Ruby、Javaなどのプログラミング言語にも移植されています。（[Wikipedia](https://ja.wikipedia.org/wiki/SDL)より）
D言語のパッケージとしては、[bindbc-sdl](https://github.com/BindBC/bindbc-sdl)として、利用可能です。

## FFMpegとは
[FFmpeg](https://ffmpeg.org/)は、動画と音声を記録・変換・再生するためのフリーソフトウェアです。Unix系オペレーティングシステム (OS) 生まれですが、現在ではクロスプラットフォームです。対応コーデックが多く、多彩なオプションを使用可能なため、幅広く利用されています。 （[Wikipedia](https://ja.wikipedia.org/wiki/FFmpeg)より）
D言語のパッケージとしては、[ffmpeg4d](https://github.com/lesderid/ffmpeg4d)として、利用可能です。

# 本題
`DUB`パッケージで提供されている`bindbc-sdl`、`ffmpeg4d`を使って音楽再生を実装します。
64ビット版のアプリケーションを作成する前提です。

# 環境
* OS : Windows 10 64ビット版 (バージョン 1909)
* D言語コンパイラ : DMD v2.094.0
* 使用するライブラリ
  * SDL 2.0.12 (bindbc-sdl バージョン 0.19.2)
  * FFMpeg 4.3.1 (ffmpeg4d バージョン 4.1.0)

## SDLの入手
これから作成するアプリケーションでは、`SDL2.dll`が必要となります。
`SDL2.dll`は、[ダウンロードページ](https://www.libsdl.org/download-2.0.php)にある`SDL2-2.0.12-win32-x64.zip`から入手します。

## FFMpegの入手
コンパイル時に`lib`ファイル、アプリケーション実行時に`DLL`ファイルが必要となります。
各ファイルは、[ダウンロードページ](https://ffmpeg.org/download.html#build-windows)で紹介されているサイトから、ビルド済みバイナリを入手するのが簡単です。
この記事では、[gyan.dev](https://www.gyan.dev/ffmpeg/builds/)の`ffmpeg-release-full-shared.7z`（Version 4.3.1-2020-11-08）を使っています。

# ソースコード
作成したソースコードを紹介します。
`FFmpeg API`の使い方については、[こちらのサイト](https://proc-cpuinfo.fixstars.com/2017/08/ffmpeg-api-decode/)が概要を理解するのに役立ちます。
また、詳細な関数や構造体の使い方は、[FFmpeg Documentation](https://ffmpeg.org/doxygen/4.1/index.html)（英語サイト）で調べました。
`SDL API`については、[こちらのサイト](http://sdl2referencejp.osdn.jp/CategoryAudio.html)が参考になります。

```d:app.d
import std.stdio;
import std.utf;
import core.stdc.string;

import bindbc.sdl;
import ffmpeg;

__gshared ubyte[] audiobuf;
__gshared ubyte *audio_pos;
__gshared ulong audio_len;
__gshared int freq;
const CHANNEL = 2;

int main(string[] args)
{
	if ( args.length < 2 ){
	    writeln("Argument required : input file");
		return ( 1 );
	}
	readMediaFile(args[1]);
	playMedia();
	
	return ( 0 );
}

void readMediaFile(string fileName)
{
	SwrContext* swr = swr_alloc();
	scope(exit) swr_free(&swr);
	AVFrame* frame = av_frame_alloc();
	scope(exit) av_frame_free(&frame);
	
	void initSwr(){
		av_opt_set_int(swr, "in_channel_layout", frame.channel_layout, 0);
		av_opt_set_int(swr, "out_channel_layout", frame.channel_layout, 0);
		av_opt_set_int(swr, "in_sample_rate", frame.sample_rate, 0);
		av_opt_set_int(swr, "out_sample_rate", frame.sample_rate, 0);
		av_opt_set_sample_fmt(swr, "in_sample_fmt", cast(AVSampleFormat)frame.format, 0);
		av_opt_set_sample_fmt(swr, "out_sample_fmt", AVSampleFormat.AV_SAMPLE_FMT_S16, 0);
		int ret = swr_init(swr);
		if ( ret < 0 ){
			writefln("swr_init failed : %d", AVERROR(ret));
			writefln("channel_layout  : %d", frame.channel_layout);
			writefln("sample_rate     : %d", frame.sample_rate);
		}
		freq = frame.sample_rate;
	}
	void decodeFrame(AVFrame* frame){
		int swr_buf_len = frame.nb_samples*frame.channels * 2;
		audiobuf.length += swr_buf_len;
		ubyte* swr_bufp = audiobuf.ptr;
		swr_bufp += audiobuf.length - swr_buf_len;
		
		int ret = swr_convert(swr, &swr_bufp, frame.nb_samples, cast(const ubyte**)frame.extended_data, frame.nb_samples);
		if ( ret < 0 ){
			writefln("swr_convert failed : %d", AVERROR(ret));
		}
	}
	
	av_register_all();
	AVFormatContext* format_context = null;
	const char* input_path = toUTFz!(const(char)*)(fileName);
	if ( avformat_open_input(&format_context, input_path, null, null) != 0 ){
		writeln("avformat_open_input failed");
		writefln("%s", fileName);
	}
	scope(exit) avformat_close_input(&format_context);
	
	if ( avformat_find_stream_info(format_context, null) < 0 ){
		writeln("avformat_find_stream_info failed");
	}
	
	AVStream* audio_stream = null;
	for ( int i = 0; i < cast(int)format_context.nb_streams; ++i ){
		if ( format_context.streams[i].codecpar.codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO ){
			audio_stream = format_context.streams[i];
			break;
		}
	}
	if ( audio_stream == null ){
		writeln("No audio stream ...");
	}
	
	AVCodec* codec = avcodec_find_decoder(audio_stream.codecpar.codec_id);
	if ( codec == null ){
		writeln("No supported decoder ...");
	}
	
	AVCodecContext* codec_context = avcodec_alloc_context3(codec);
	if ( codec_context == null ){
		writeln("avcodec_alloc_context3 failed");
	}
	scope(exit) avcodec_free_context(&codec_context);
	
	if ( avcodec_parameters_to_context(codec_context, audio_stream.codecpar) < 0 ){
		writeln("avcodec_parameters_to_context failed");
	}
	
	if ( avcodec_open2(codec_context, codec, null) != 0 ){
		writeln("avcodec_open2 failed");
	}
	
	AVPacket packet = AVPacket();
	while ( av_read_frame(format_context, &packet) == 0 ){
		if ( packet.stream_index == audio_stream.index ){
			if ( avcodec_send_packet(codec_context, &packet) != 0 ){
				writeln("avcodec_send_packet failed");
			}
			while ( avcodec_receive_frame(codec_context, frame) == 0 ){
				if ( swr_is_initialized(swr) == 0 ){
					initSwr();
				}
				decodeFrame(frame);
			}
		}
		av_packet_unref(&packet);
	}
	// flush decoder
	if ( avcodec_send_packet(codec_context, null) != 0 ){
		writeln("avcodec_send_packet failed");
	}
	while ( avcodec_receive_frame(codec_context, frame) == 0 ){
		decodeFrame(frame);
	}
}

void playMedia()
{
	// Load SDL
	SDLSupport ret = loadSDL();
	if ( ret != sdlSupport ){
		if ( ret == SDLSupport.noLibrary ){
			writeln("SDLSupport.noLibrary");
		} else if ( SDLSupport.badLibrary ){
			writeln("SDLSupport.badLibrary");
		}
	}
	scope(exit) unloadSDL();
	// Initialize SDL
	if ( SDL_Init(SDL_INIT_AUDIO) < 0 ){
		writefln("SDL_Init failed : %s", SDL_GetError());
	}
	scope(exit) SDL_Quit();
	
	SDL_AudioSpec wav_spec;
	wav_spec.freq = freq;
	wav_spec.format = AUDIO_S16SYS;
	wav_spec.channels = CHANNEL;
	wav_spec.callback = &audioCallback;
	wav_spec.userdata = null;
	audio_pos = audiobuf.ptr;
	audio_len = audiobuf.length;
	if ( SDL_OpenAudio(&wav_spec, null) < 0 ){
		writefln("Couldn't open audio: %s", SDL_GetError());
	}
	scope(exit) SDL_CloseAudio();
	
	SDL_PauseAudio(0);
	while ( audio_len > 0 ){
		SDL_Delay(200); 
	}
}

extern(C) nothrow
void audioCallback(void *userdata, ubyte *stream, int len)
{
	len = cast(int)( len > audio_len ? audio_len : len );
	memcpy(stream, audio_pos, len);
	audio_pos += len;
	audio_len -= len;
}
```
```json:dub.json
{
	"name": "playaudio",
	"description": "audio file player",
	"authors": [
		"devmynote"
	],
	"license": "BSL-1.0",
	"dependencies": {
		"ffmpeg4d": "~>4.1.0",
		"bindbc-sdl": "~>0.19.2",
	},
	"versions": ["SDL_205","BindSDL_Image"],
	"lflags":["/libpath:./lib"],
}
```
# ソースコード補足説明
`main`関数では引数をチェックした後、本処理を呼び出します。
`readMediaFile`は、パラメータで指定したファイル名から音楽データを抽出し、メモリに格納します。
`playMedia`関数は、メモリに格納した音楽データを使って、音楽再生します。
ソースコードで使っている`FFmpeg API`と`SDL API`の一覧をまとめました。

|関数名|関数の簡単な説明|
|:---|:---|
|av_register_all|FFmpegのDLLを使用するための初期化|
|avformat_open_input|指定したファイルを読み、ヘッダ情報を`AVFormatContext`に格納する|
|avformat_close_input|`AVFormatContext`を閉じる|
|avformat_find_stream_info|`AVFormatContext`から、格納されているストリーム情報を取得する|
|avcodec_find_decoder|引数であるコーデックIDに対応したデコーダを取得する|
|avcodec_alloc_context3|`AVCodecContext`のメモリを割り当てる|
|avcodec_free_context|`AVCodecContext`のメモリを開放する|
|avcodec_parameters_to_context|引数であるコーデックパラメータをもとに`AVCodecContext`に必要な値をセットする|
|avcodec_open2|引数である`AVCodec`をもとに`AVCodecContext`を初期化する|
|av_read_frame|ストリームよりパケットデータを取得する|
|avcodec_send_packet|パケットデータをデコーダに渡す|
|avcodec_receive_frame|デコーダからフレーム情報を受け取る|
|av_packet_unref|パケットデータのメモリを開放する|
|av_frame_alloc|`AVFrame`のメモリを割り当てる|
|av_frame_free|`AVFrame`のメモリを開放する|
|av_opt_set_int|`SwrContext`にパラメータをセットする|
|av_opt_set_sample_fmt|`SwrContext`に入出力フォーマット情報をセットする|
|swr_alloc|`SwrContext`のメモリを割り当てる|
|swr_free|`SwrContext`のメモリを開放する|
|swr_is_initialized|`SwrContext`が初期化されてるかを調べる|
|swr_init|`SwrContext`の初期化|
|swr_convert|`SwrContext`の情報をもとに入出力フォーマット変換を行う|

|関数名|関数の簡単な説明|
|:---|:---|
|loadSDL|`bindbc-sdl`でSDLを使用する前の初期化|
|unloadSDL|`bindbc-sdl`でSDLを使用した後の終了処理|
|SDL_Init|SDLライブラリの初期化|
|SDL_Quit|SDLライブラリの終了処理|
|SDL_OpenAudio|オーディオデバイスを開く|
|SDL_CloseAudio|オーディオデバイスを閉じる|
|SDL_PauseAudio|オーディオの再生、停止を行う|
|SDL_Delay|指定のミリ秒間待つ|

# コンパイル
私の開発環境を例にとると、`D:\Dev`が開発用フォルダです。
アプリケーション作成用に`playaudio`フォルダを作成しています。

```text:コマンドプロンプト
D:\Dev> cd playaudio
```
`playaudio`内のフォルダ構成は`tree`コマンドの出力通りです。

```text:コマンドプロンプト
D:\Dev\playaudio> tree
フォルダー パスの一覧:  ボリューム ****
ボリューム シリアル番号は ****-**** です
D:.
├─lib
└─source
```
`D:\Dev\playaudio`フォルダに`dub.json`と`DLL`ファイルを配置します。
`DLL`ファイルは、ダウンロードした`SDL2-2.0.12-win32-x64.zip`と`ffmpeg-release-full-shared.7z`にあります。

```text:コマンドプロンプト
D:\Dev\playaudio> dir /B
avcodec-58.dll
avdevice-58.dll
avfilter-7.dll
avformat-58.dll
avutil-56.dll
dub.json
lib
postproc-55.dll
SDL2.dll
source
swresample-3.dll
swscale-5.dll
```
`D:\Dev\playaudio\lib`フォルダに`ffmpeg-release-full-shared.7z`のlibファイルを配置します。

```text:コマンドプロンプト
D:\Dev\playaudio> dir /B lib
avcodec.lib
avdevice.lib
avfilter.lib
avformat.lib
avutil.lib
postproc.lib
swresample.lib
swscale.lib
```
`D:\Dev\playaudio\source`フォルダに`app.d`を配置します。

```text:コマンドプロンプト
D:\Dev\playaudio> dir /B source
app.d
```
各ファイルの配置が完了したら、`DUB`コマンドでコンパイルを実行します。

```text:コマンドプロンプト
D:\Dev\playaudio> dub build --build=release --arch=x86_64
Fetching bindbc-sdl 0.19.2 (getting selected version)...
Fetching bindbc-loader 0.3.2 (getting selected version)...
Fetching ffmpeg4d 4.1.0 (getting selected version)...
Performing "release" build using D:\App\Dev\dmd2\windows\bin\dmd.exe for x86_64.
bindbc-loader 0.3.2: building configuration "noBC"...
bindbc-sdl 0.19.2: building configuration "dynamic"...
playaudio ~master: building configuration "application"...
Linking...
```

# 実行例
コンパイルが無事終了すると、`D:\Dev\playaudio`フォルダに`playaudio.exe`が生成されます。
再生したい音楽ファイル名を引数に指定して実行することで、音楽再生ができます。
`FFmpeg`が対応しているフォーマットであれば、再生可能です。
※`wave`や`mp3`ファイルの音楽再生はもちろんのこと、`mp4`や`mkv`ファイルの音声再生も可能です。

```text:
D:\Dev\playaudio> playaudio "再生したい音楽ファイル名"
```

最後まで読んでいただき、ありがとうございます。
次週に続編の「D言語で動画再生」の記事が掲載される予定です。
興味がありましたら、またお会いしましょう。

# おまけ

音楽再生に`SDL`を使わず、`Windows API`だけで処理したソースコードをおまけに紹介します。

```d:
import std.stdio;
import std.utf;
import core.sys.windows.windows;
import core.thread;

import ffmpeg;

pragma(lib, "Winmm.lib");

__gshared ubyte[] audiobuf;
__gshared PCMWAVEFORMAT waveformat;

int main(string[] args)
{
	if ( args.length < 2 ){
	    writeln("Argument required : input file");
		return ( 1 );
	}
	readMediaFile(args[1]);
	playMedia();
	
	return ( 0 );
}

void readMediaFile(string fileName)
{
	SwrContext* swr = swr_alloc();
	scope(exit) swr_free(&swr);
	AVFrame* frame = av_frame_alloc();
	scope(exit) av_frame_free(&frame);
	
	void initSwr(){
		av_opt_set_int(swr, "in_channel_layout", frame.channel_layout, 0);
		av_opt_set_int(swr, "out_channel_layout", frame.channel_layout, 0);
		av_opt_set_int(swr, "in_sample_rate", frame.sample_rate, 0);
		av_opt_set_int(swr, "out_sample_rate", frame.sample_rate, 0);
		av_opt_set_sample_fmt(swr, "in_sample_fmt", cast(AVSampleFormat)frame.format, 0);
		av_opt_set_sample_fmt(swr, "out_sample_fmt", AVSampleFormat.AV_SAMPLE_FMT_S16, 0);
		int ret = swr_init(swr);
		if ( ret < 0 ){
			writefln("swr_init failed : %d", AVERROR(ret));
			writefln("channel_layout  : %d", frame.channel_layout);
			writefln("sample_rate     : %d", frame.sample_rate);
		}
		waveformat.wf.wFormatTag = WAVE_FORMAT_PCM;
		waveformat.wf.nChannels = cast(ushort)frame.channels;
		waveformat.wf.nSamplesPerSec = frame.sample_rate;
		waveformat.wf.nAvgBytesPerSec = frame.sample_rate * 2;
		waveformat.wf.nBlockAlign = cast(ushort)(frame.channels * 2);
		waveformat.wBitsPerSample = 16;
	}
	void decodeFrame(AVFrame* frame){
		int swr_buf_len = frame.nb_samples*frame.channels * 2;
		audiobuf.length += swr_buf_len;
		ubyte* swr_bufp = audiobuf.ptr;
		swr_bufp += audiobuf.length - swr_buf_len;
		
		int ret = swr_convert(swr, &swr_bufp, frame.nb_samples, cast(const ubyte**)frame.extended_data, frame.nb_samples);
		if ( ret < 0 ){
			writefln("swr_convert failed : %d", AVERROR(ret));
		}
	}
	
	av_register_all();
	AVFormatContext* format_context = null;
	const char* input_path = toUTFz!(const(char)*)(fileName);
	if ( avformat_open_input(&format_context, input_path, null, null) != 0 ){
		writeln("avformat_open_input failed");
		writefln("%s", fileName);
	}
	scope(exit) avformat_close_input(&format_context);
	
	if ( avformat_find_stream_info(format_context, null) < 0 ){
		writeln("avformat_find_stream_info failed");
	}
	
	AVStream* audio_stream = null;
	for ( int i = 0; i < cast(int)format_context.nb_streams; ++i ){
		if ( format_context.streams[i].codecpar.codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO ){
			audio_stream = format_context.streams[i];
			break;
		}
	}
	if ( audio_stream == null ){
		writeln("No audio stream ...");
	}
	
	AVCodec* codec = avcodec_find_decoder(audio_stream.codecpar.codec_id);
	if ( codec == null ){
		writeln("No supported decoder ...");
	}
	
	AVCodecContext* codec_context = avcodec_alloc_context3(codec);
	if ( codec_context == null ){
		writeln("avcodec_alloc_context3 failed");
	}
	scope(exit) avcodec_free_context(&codec_context);
	
	if ( avcodec_parameters_to_context(codec_context, audio_stream.codecpar) < 0 ){
		writeln("avcodec_parameters_to_context failed");
	}
	
	if ( avcodec_open2(codec_context, codec, null) != 0 ){
		writeln("avcodec_open2 failed");
	}
	
	AVPacket packet = AVPacket();
	while ( av_read_frame(format_context, &packet) == 0 ){
		if ( packet.stream_index == audio_stream.index ){
			if ( avcodec_send_packet(codec_context, &packet) != 0 ){
				writeln("avcodec_send_packet failed");
			}
			while ( avcodec_receive_frame(codec_context, frame) == 0 ){
				if ( swr_is_initialized(swr) == 0 ){
					initSwr();
				}
				decodeFrame(frame);
			}
		}
		av_packet_unref(&packet);
	}
	// flush decoder
	if ( avcodec_send_packet(codec_context, null) != 0 ){
		writeln("avcodec_send_packet failed");
	}
	while ( avcodec_receive_frame(codec_context, frame) == 0 ){
		decodeFrame(frame);
	}
}

void playMedia()
{
	HWAVEOUT hwaveout;
	int ret;
	ret = waveOutOpen(&hwaveout, WAVE_MAPPER, cast(WAVEFORMATEX*)&waveformat, 0, 0, CALLBACK_NULL);
	if ( ret != 0 ){
		writefln("waveOutOpen failed : %d", ret);
		return;
	}
	scope(exit) waveOutClose(hwaveout);
	
	WAVEHDR waveheader;
	waveheader.lpData = cast(char*)audiobuf.ptr;
	waveheader.dwBufferLength = cast(uint)audiobuf.length;
//	waveheader.dwBytesRecorded;
//	waveheader.dwUser;
	waveheader.dwFlags = 0;
	waveheader.dwLoops = 0;
//	waveheader.lpNext;
//	waveheader.reserved;
	ret = waveOutPrepareHeader(hwaveout, &waveheader, WAVEHDR.sizeof);
	if ( ret != 0 ){
		writefln("waveOutPrepareHeader failed : %d", ret);
		return;
	}
	scope(exit) waveOutUnprepareHeader(hwaveout, &waveheader, WAVEHDR.sizeof);
	
	ret = waveOutWrite(hwaveout, &waveheader, WAVEHDR.sizeof);
	if ( ret != 0 ){
		writefln("waveOutWrite failed : %d", ret);
		return;
	}
	MMTIME mm;
	mm.wType = TIME_BYTES;
	do {
		waveOutGetPosition(hwaveout, &mm, mm.sizeof);
//		writefln("%d / %d", mm.cb, audiobuf.length);
		Thread.sleep(dur!("msecs")(200));
	} while ( mm.cb < audiobuf.length );
}
```