Posted at
D言語Day 8

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

More than 3 years have passed since last update.

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にすることで対処したいと思います。


  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

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


サイン波を鳴らしてみる

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


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しておくと良いらしいです。

if (Thread.getThis() is null) {

thread_attachThis();
}


まとめ

いかがでしたか?

ちょっとチュートリアルみたいな記事になってしまいました。

サイン波形を鳴らすだけだと詰まらないので、次はMMLで曲を再生してみたいと思います。

(モノはだいたい出来ているのであとは記事を書くだけ)

D言語AC枠にも空きがあるので近いうちに続編を書ければ…と思います。

それでは。