LoginSignup
7
7

More than 5 years have passed since last update.

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

Posted at

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枠にも空きがあるので近いうちに続編を書ければ…と思います。

それでは。

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7