2017年3月17日金曜日

最新のPortAudioを試す

私が音響信号処理に興味を持った頃,パソコンで音の出るプログラム(単にオーディオファイルを再生したりするプログラムのことではなく,DSPなどでよくやるようなリアルタイムに音を入出力して1サンプルごと処理していくようなプログラムのことです)を作るのは,かなり高度なテクニックに入る部類だったように思います.今は簡単になったのかというとそうでもなくて,私自身が色々と知ってしまったのであまり難しいとは感じなくなっただけなのですが,とにかく当時は未知の世界でした.当時,パソコン上で音を扱うプログラムについて解説した本は,日本語のものでは1冊しかありませんでした.その本のサンプルが,やたら長いソースコードで,ものぐさな私にはとてもやる気になれる内容ではありませんでした.(今では数冊この分野の本があります.)

勿論,MATLABやScilabで信号処理をして音を出すことは,当時も簡単にできました.しかし,音楽・音響制作の分野に身を置いていた私は,とにかく,リアルタイム入出力のアプリケーションこそが正義.これができなければ信号処理を勉強している意味がないと思っていました.

そんな中,知人が「最近使い始めた」と教えてくれたのがPortAudioでした.

http://www.portaudio.com/

PortAudioは,オープンソースのオーディオ入出力APIで,以下の点が特徴です.

・オーディオの基本的な機能を使うソースコードが簡単になる

・既存のオーディオAPIの抽象化として存在する

・クロス・プラットフォームである

クロス・プラットフォームというのはかなり魅力的で,現在のバージョンでは以下に対応しています.

・Windows DirectSound
・Windows WASAPI
・ASIO
・Mac CoreAudio
・UNIX OSS/ALSA

OS依存のAPIを同時に使ったりしない限り,これらの扱いが,全く同じソースコードでできてしまいます.

歴史はかなり古く,コンピュータ音楽の作曲家・技術者の間ではお馴染みのICMCで,2001年に発表されています.


http://www.portaudio.com/docs/portaudio_icmc2001.pdf

調べれば調べるほど,多くの音関係のオープンソースプロジェクトがPortAudioを使っていることが分かります.

私は,これまでずっとv18.1を使ってきました.理由は,上記の知人もそのバージョンを使っていたのと,まだプログラミングに完全に慣れきっていなかった私にもしっくりくる書き方ができたからです.しかし,v18.1のリリースは2003年.さすがに古すぎます.(ことさらに古さを演出するために,ここで「2003年といえば,〇〇があった年ですよ」とか書こうと思い,2003年の出来事を調べたが,あまり古いと感じる出来事がなかったので書くのをやめた)

そこで最新バージョンv19.06を使ってみることにしました.使ってみると,私の知っているPortAudioとは少し勝手が違っていたので,ビルド方法を紹介します.ゆとり世代なので,初めから統合開発環境Visual Studioを使います.先ほど紹介したPortAudioの特徴の恩恵に預かることが目的なので,PortAudioそのものの深い詮索等はせずに,一旦音の出るアプリケーションがビルドできればOKとします.

対象とするオーディオ環境はDirectSound.ビルドまでの手順が一番簡単だからです.

1, 上記webからPortAudioをダウンロード,解凍して "portaudio" フォルダを適当な場所にコピー.

2, ASIO SDKをダウンロード(今現在の最新バージョンは 2.3)

https://www.steinberg.net/en/company/developers.html

3, 解凍した "ASIOSDK2.3" フォルダを "portaudio/src/hostapi/asio/" にコピー.更に, "ASIOSDK2.3" -> "ASIOSDK" にリネーム.

4, "portaudio/build/msvc/portaudio.vcproj"をVisual Studioで開く(この時,比較的新しいVisual Studioを使うと,プロジェクトが最新化の環境に合わせて変換される)

5, ビルド設定を好みの設定にしてビルド.(今回はx86 Releaseにした)

6, 成功すると,"portaudio/build/msvc/Win32/Release"(Win32以下はビルド設定によって変わる) に "portaudio_x86.lib" "portaudio_x86.dll" が出来ている.

7, 新しいプロジェクト(空のプロジェクト)を作って,PortAudioを使ったコードをコーディング.

8, "portaudio/include/" をインクルードパスに設定して同ディレクトリの "portaudio.h" "pa_win_ds.h" をプロジェクトに追加する.

9, "portaudio/build/msvc/Win32/Release" をリンカのライブラリパスに設定して,同ディレクトリの "portaudio_x86.lib","winmm.lib"と"dsound.lib"をライブラリに追加する.

10, ビルド."portaudio_x86.dll" をアプリケーションと同じディレクトリにコピーすれば動作する.

という流れになります.v18.1と違うのは,"portaudio_〇〇.lib" "portaudio_〇〇.dll" をビルドしないといけない点です.v18.1では,各オーディオ環境に応じたインクルードファイルとソースファイルを追加してビルドする方式でしたが,現在のバージョンでは,Windowsベースのオーディオ環境(恐らくDirectSoundとWASAPIとASIO)はlibとdllにまとめられていて,それをリンクすれば全ての環境が使えるようになるみたいです.例えば,同じアプリケーションで複数のオーディオ環境をサポートするようなアプリケーションは,これで作りやすくなったと言えますが,個人的には,事前準備なくソースを追加するだけで音を出せるv18.1の方が楽なので好きです.

さて,ソースコードですが,例えば,正弦波を出すようなアプリケーションは以下のコードで出来ます.特にちゃんとやる必要性がある訳ではないので,エラー処理を完全に省略しましたが,それにしても本当に簡単です.


#include<stdio.h>
#include<math.h>
#include"portaudio.h"
#define Fs 44100 //サンプリング周波数
#define FRAMES_PER_BUFFER 128 //バッファサイズ
#define pi 3.14159265358979323

/*ユーザ定義データ*/
typedef struct{
    float freq; //正弦波の周波数
    float index;
}padata;

/* オーディオ処理コールバック関数*/
static int dsp(const void *inputBuffer, //入力
               void *outputBuffer, //出力
               unsigned long framesPerBuffer,
               const PaStreamCallbackTimeInfo *timeInfo,
               PaStreamCallbackFlags statusFlags,
               void *userData //ユーザ定義データ 
               ){
    padata *data = (padata *)userData;
    float *out = (float *)outputBuffer;
    long i;

    for( i=0; i<framesPerBuffer; i++){
        *out++ = 0.7 * sin( 2 * pi * data->freq * data->index / Fs ); //チャンネル1(左)
        *out++ = 0.7 * sin( 2 * pi * data->freq * data->index / Fs ); //チャンネル2(右)
        data->index+=1.f;
    }
    return 0;
}
int main(void){
    PaStreamParameters outParam; //出力の定義
    PaStream *stream;
    PaError err;
    padata data; //ユーザ定義データ
    data.freq = 800.f;
    data.index = 0.f;

    //PortAudio初期化
    Pa_Initialize();

    //出力の設定
    outParam.device = Pa_GetDefaultOutputDevice(); //デフォルトのオーディオデバイス
    outParam.channelCount = 2;
    outParam.sampleFormat = paFloat32; //32bit floatで処理
    outParam.suggestedLatency = Pa_GetDeviceInfo( outParam.device )->defaultLowOutputLatency;
    outParam.hostApiSpecificStreamInfo = NULL;

    //PortAudioオープン
    Pa_OpenStream(
        &stream,
        NULL,
        &outParam,
        Fs,
        FRAMES_PER_BUFFER,
        paClipOff,
        dsp,
        &data);
    
    //PortAudioスタート
    Pa_StartStream(stream);

    //エンターキーが押されるまで待機
    getchar();

    //PortAudio終了
    Pa_StopStream(stream);
    Pa_CloseStream(stream);
    Pa_Terminate();
    return 0;
}


入力を使いたいときは,同じように PaStreamParameters を入力用に定義して,PaOpenStreamの引数にすればよいです.


0 件のコメント:

コメントを投稿