D言語 Advent Calendar 8日目の記事です。
D言語でサウンドを扱った記事ってあまり見かけないような…と思って書きました。
#はじめに
まずD言語の標準ライブラリに音を鳴らす機能はありません。
今回は外部のライブラリにPortAudioを使用します。
#PortAudioとは何やら
オープンソースのシンプルなマルチプラットフォーム音声入出力ライブラリです。
シンプルな機能しかないのでゲームなどで沢山の音を鳴らしたいときは、
XAudio2とかOpenALのような多機能なオーディオライブラリを使った方が
いいでしょう。
PortAudio自体はC言語で記述されているため、ポーティングすれば様々な言語で
使用することができます。
#準備
##本家PortAudioをビルドする。
PortAudio 公式サイト
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にすることで対処したいと思います。
- プロジェクトツリーのhostapiのASIOを丸ごと削除する。
- defファイルからASIO関連の下記関数を削除する。
PaAsio_GetAvailableBufferSizes @50
PaAsio_ShowControlPanel @51
PaAsio_GetInputChannelName @53
PaAsio_GetOutputChannelName @54
これでDLLのビルドが上手く行くと思います。
次にdmdでリンクできるインポートライブラリ( lib )を作成します。
Visual Studio が出力する lib は残念ながら dmd で使用することができません。
アーカイブに含まれている portaudio.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
implib portaudio_x86.lib portaudio_x86.def
###Mac環境の場合
アーカイブを展開。コンソールに以下コマンドを打ち込んでインストール完了です。簡単!
./configure
make install
##PoitAudioのD言語バインディングを手に入れる
なんとウォルター先生がポーティングしてくれていました。
ありがたく使わせてもらいましょう。
deimos 以下の portaudio.di をダウンロードします。
##サイン波を鳴らしてみる
とりあえず動作確認として、シンプルなサイン波形を鳴らしてみましょう。
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 を置いておきます。
dmd testaudio.d portaudio_x86.lib
###Mac環境でビルド
testaudio.d と同じディレクトリに
portaudio.diが入っているディレクトリの deimos を置いておきます。
dmd testaudio.d -L-L/usr/local/lib -L-lportaudio
###実行!
440Hzのサイン波形が3秒くらい鳴ったでしょうか…?
frequency を変えると音の高さが変わると思います。
*補足
PortAudio は音データが必要になったタイミングで audioCallback を呼んできます。
Cレイヤーで作成されたスレッドから呼ばれているため、こんな風にDレイヤーの
スレッドマネージャにatatchしておくと良いらしいです。
if (Thread.getThis() is null) {
thread_attachThis();
}
#まとめ
いかがでしたか?
ちょっとチュートリアルみたいな記事になってしまいました。
サイン波形を鳴らすだけだと詰まらないので、次はMMLで曲を再生してみたいと思います。
(モノはだいたい出来ているのであとは記事を書くだけ)
D言語AC枠にも空きがあるので近いうちに続編を書ければ…と思います。
それでは。