PortAudioを使ってD言語で波形を鳴らす

[D言語 Advent Calendar](http://qiita.com/advent-calendar/2014/dlang) 8日目の記事です。

D言語でサウンドを扱った記事ってあまり見かけないような…と思って書きました。

#はじめに
まずD言語の標準ライブラリに音を鳴らす機能はありません。
今回は外部のライブラリにPortAudioを使用します。

#PortAudioとは何やら
オープンソースのシンプルなマルチプラットフォーム音声入出力ライブラリです。
シンプルな機能しかないのでゲームなどで沢山の音を鳴らしたいときは、
XAudio2とかOpenALのような多機能なオーディオライブラリを使った方が
いいでしょう。

PortAudio自体はC言語で記述されているため、ポーティングすれば様々な言語で
使用することができます。

#準備
##本家PortAudioをビルドする。

[PortAudio 公式サイト](http://www.portaudio.com)
Download から Stable Releases のアーカイブ ( tgz ) をダウンロードしてきます。

###Windows環境の場合 (Visual Studio)
アーカイブを展開すると build/msvc/ に portaudio.sln があると思います。
これをVisual Studioで開いてビルドするんですが、大体の環境でビルドエラーが発生すると思います。

```
1>  asiolist.cpp
1>c1xx : fatal error C1083: ソース ファイルを開けません。'..\..\src\hostapi\asio\ASIOSDK\host\pc\asiolist.cpp': No such file or directory
1>  asiodrivers.cpp
1>c1xx : fatal error C1083: ソース ファイルを開けません。'..\..\src\hostapi\asio\ASIOSDK\host\asiodrivers.cpp': No such file or directory
1>  asio.cpp
1>c1xx : fatal error C1083: ソース ファイルを開けません。'..\..\src\hostapi\asio\ASIOSDK\common\asio.cpp': No such file or directory
```

ASIO 関連でエラーが出ているようです。

PortAudio は WMME, WASAPI, DirectSound, ASIO など様々な Audio API に
対応してますが、ASIO を使用するには Steinberg SDK が必要です。

Steinberg SDK は 手に入れるのにデベロッパ登録とかしないといけないらしく、
今回は ASIO 関連の機能をOFFにすることで対処したいと思います。

1. プロジェクトツリーのhostapiのASIOを丸ごと削除する。
2. defファイルからASIO関連の下記関数を削除する。

```
PaAsio_GetAvailableBufferSizes      @50
PaAsio_ShowControlPanel             @51
PaAsio_GetInputChannelName          @53
PaAsio_GetOutputChannelName         @54
```

これでDLLのビルドが上手く行くと思います。

次にdmdでリンクできるインポートライブラリ( lib )を作成します。
Visual Studio が出力する lib は残念ながら dmd で使用することができません。
アーカイブに含まれている portaudio.def を改造してこんな感じにします。

```:portaudio_x86.def
LIBRARY 'portaudio_x86.dll'
EXPORTS
	_Pa_GetVersion								= Pa_GetVersion
	_Pa_GetVersionText                       	= Pa_GetVersionText
	_Pa_GetErrorText                         	= Pa_GetErrorText
	_Pa_Initialize                           	= Pa_Initialize
	_Pa_Terminate                            	= Pa_Terminate
	_Pa_GetHostApiCount                      	= Pa_GetHostApiCount
	_Pa_GetDefaultHostApi                    	= Pa_GetDefaultHostApi
	_Pa_GetHostApiInfo                       	= Pa_GetHostApiInfo
	_Pa_HostApiTypeIdToHostApiIndex          	= Pa_HostApiTypeIdToHostApiIndex
	_Pa_HostApiDeviceIndexToDeviceIndex      	= Pa_HostApiDeviceIndexToDeviceIndex
	_Pa_GetLastHostErrorInfo                 	= Pa_GetLastHostErrorInfo
	_Pa_GetDeviceCount                       	= Pa_GetDeviceCount
	_Pa_GetDefaultInputDevice                	= Pa_GetDefaultInputDevice
	_Pa_GetDefaultOutputDevice               	= Pa_GetDefaultOutputDevice
	_Pa_GetDeviceInfo                        	= Pa_GetDeviceInfo
	_Pa_IsFormatSupported                    	= Pa_IsFormatSupported
	_Pa_OpenStream                           	= Pa_OpenStream
	_Pa_OpenDefaultStream                    	= Pa_OpenDefaultStream
	_Pa_CloseStream                          	= Pa_CloseStream
	_Pa_SetStreamFinishedCallback            	= Pa_SetStreamFinishedCallback
	_Pa_StartStream                          	= Pa_StartStream
	_Pa_StopStream                           	= Pa_StopStream
	_Pa_AbortStream                          	= Pa_AbortStream
	_Pa_IsStreamStopped                      	= Pa_IsStreamStopped
	_Pa_IsStreamActive                       	= Pa_IsStreamActive
	_Pa_GetStreamInfo                        	= Pa_GetStreamInfo
	_Pa_GetStreamTime                        	= Pa_GetStreamTime
	_Pa_GetStreamCpuLoad                     	= Pa_GetStreamCpuLoad
	_Pa_ReadStream                           	= Pa_ReadStream
	_Pa_WriteStream                          	= Pa_WriteStream
	_Pa_GetStreamReadAvailable               	= Pa_GetStreamReadAvailable
	_Pa_GetStreamWriteAvailable              	= Pa_GetStreamWriteAvailable
	_Pa_GetSampleSize                        	= Pa_GetSampleSize
	_Pa_Sleep                                	= Pa_Sleep
	_PaUtil_InitializeX86PlainConverters     	= PaUtil_InitializeX86PlainConverters
	_PaUtil_SetDebugPrintFunction            	= PaUtil_SetDebugPrintFunction
	_PaWasapi_GetDeviceDefaultFormat         	= PaWasapi_GetDeviceDefaultFormat
	_PaWasapi_GetDeviceRole                  	= PaWasapi_GetDeviceRole
	_PaWasapi_ThreadPriorityBoost            	= PaWasapi_ThreadPriorityBoost
	_PaWasapi_ThreadPriorityRevert           	= PaWasapi_ThreadPriorityRevert
	_PaWasapi_GetFramesPerHostBuffer         	= PaWasapi_GetFramesPerHostBuffer
	_PaWasapi_GetJackDescription             	= PaWasapi_GetJackDescription
	_PaWasapi_GetJackCount                   	= PaWasapi_GetJackCount
```



```:console
implib portaudio_x86.lib portaudio_x86.def
```

###Mac環境の場合
アーカイブを展開。コンソールに以下コマンドを打ち込んでインストール完了です。簡単！

```:console
./configure
make install
```

##PoitAudioのD言語バインディングを手に入れる
なんとウォルター先生がポーティングしてくれていました。
ありがたく使わせてもらいましょう。

[PortAudio D bindings](https://github.com/D-Programming-Deimos/portaudio)

deimos 以下の portaudio.di をダウンロードします。

##サイン波を鳴らしてみる
とりあえず動作確認として、シンプルなサイン波形を鳴らしてみましょう。

```d:testaudio.d
import core.thread;
import std.stdio, std.conv, std.math;
import deimos.portaudio;

const int sampleRate   = 44100;
const float frequency  = 440;	// 波形の周波数[Hz]
const float phaseShift = 2 * PI * frequency / sampleRate;

struct AudioState
{
	float phase = 0;
}

extern(C)
int audioCallback(const(void)* inputBuffer, 
	void* outputBuffer, size_t framesPerBuffer,
	const(PaStreamCallbackTimeInfo)* timeInfo,
	PaStreamCallbackFlags statusFlags, void *userData)
{
	if (Thread.getThis() is null) {
		thread_attachThis();
	}

	auto state = cast(AudioState*)userData;
	auto pout = cast(float*)outputBuffer;

	foreach(i; 0 .. framesPerBuffer) {
		float s = sin(state.phase);
		*pout++ = s;
		*pout++ = s;

		state.phase += phaseShift;
	}
	return 0;
}

void main()
{
	PaError err;
	try {
		err = Pa_Initialize();
		if (err != paNoError) {
			throw new Exception(to!string(Pa_GetErrorText(err)));
		}

		AudioState state;

		PaStream* stream;
		err = Pa_OpenDefaultStream(&stream,
			0, 2, paFloat32, sampleRate,
			paFramesPerBufferUnspecified,
			&audioCallback, &state);
		if (err != paNoError) {
			throw new Exception(to!string(Pa_GetErrorText(err)));
		}

		Pa_StartStream(stream);

		Pa_Sleep(3_000);

		Pa_StopStream(stream);
		Pa_CloseStream(stream);
		Pa_Terminate();
	
	} catch (Exception e) {
		stderr.writefln("error %s", e);
	}
}
```

###Windows環境でビルド
testaudio.d と同じディレクトリに
portaudio.diが入っているディレクトリの deimos と
portaudio_x86.lib を置いておきます。

```:console
dmd testaudio.d portaudio_x86.lib
```

###Mac環境でビルド
testaudio.d と同じディレクトリに
portaudio.diが入っているディレクトリの deimos を置いておきます。

```:console
dmd testaudio.d -L-L/usr/local/lib -L-lportaudio
```

###実行！
440Hzのサイン波形が3秒くらい鳴ったでしょうか…？
frequency を変えると音の高さが変わると思います。

**＊補足**
PortAudio は音データが必要になったタイミングで audioCallback を呼んできます。
Cレイヤーで作成されたスレッドから呼ばれているため、こんな風にDレイヤーの
スレッドマネージャにatatchしておくと良いらしいです。

```d:
if (Thread.getThis() is null) {
	thread_attachThis();
}
```

#まとめ
いかがでしたか？
ちょっとチュートリアルみたいな記事になってしまいました。

サイン波形を鳴らすだけだと詰まらないので、次はMMLで曲を再生してみたいと思います。
（モノはだいたい出来ているのであとは記事を書くだけ）
D言語AC枠にも空きがあるので近いうちに続編を書ければ…と思います。

それでは。