Posted at
Siv3DDay 6

TinyASIOを使ったリアルタイム・オーディオ処理

More than 3 years have passed since last update.


はじめに

DTMではオーディオ・インターフェースという音響機器を使って,リアルタイムにDTMソフトと楽器間の通信を行っています.通信はASIOという規格が使われ,ASIOを使うと音が遅れて聴こえるという現象がなくなります.

さて,このASIOですが,オーディオ・インターフェースを持っていないとできないのかと言うとそうでもなく,ASIO4ALLというドライバを入れれば使えるようになります.ただし,あくまで仮想的な振る舞いをするだけですので,そんなに速くならないかもしれません.あしからず.


TinyASIO

TinyASIOは,ASIOを操作するためのラッパーライブラリです.普通はCOMインターフェースを直接叩かないとASIOが使えないのですが,その辺りをカプセル化して使い易くしてあります.


最も簡単なサンプル

そういうわけで,雰囲気をつかむために簡単なサンプルを例示します.


test.cpp

#include <Siv3D.hpp>

#include <TinyASIO.hpp>

void Main() {
// 入力音声を直接出力するだけのコントローラ
asio::InputBackController controller("AudioBox"); // 接続されているAudioBoxを使います

controller.Start(); // バッファリング開始,うっかり忘れずに

while (System::Update())
{
// StreamPtrはstd::shared_ptr<std::vector<float>>のtypedef
StreamPtr stream = controller.Fetch(); // ここでストリームに蓄積された音声を取得できる
}

controller.Stop(); // バッファリング停止

// 自動的に開放されます.Stopが呼ばれていない場合,勝手にStopします.
// 稀に解放した時にオーディオ・インターフェースが原因で落ちることがあります.
}


たったの4~5行程度でオーディオ・インターフェースが扱えるようになります.

TinyASIOで一番重要なクラスがコントローラです.コントローラとは,音声データを送ったり受け取ったりするためのクラスです.

TinyASIOでは,おまけでInputBackControllerとInputOnlyControllerを提供しています.

コントローラの仕組みは後ほど詳しく説明します.


オーディオ・インターフェースの一覧を取得

まずは,登録されているオーディオ・インターフェースの一覧を取得します.


drivers.cpp

#include <Siv3D.hpp>

#include "TinyASIO.hpp"

void Main() {

auto pathes = asio::Registory::GetAsioDriverPathes(); // ASIOドライバのレジストリ値を取得する

std::string driverName;
for (const auto& subkey : pathes.Items()) {
driverName = subkey.driverName; // ドライバの名前を適当に取得
}

asio::InputBackController controller(driverName); // ドライバ名で初期化する

// 中略
}


登録されているオーディオ・インターフェースは,asio::Registroy::GetAsioDriverPathes関数で取得することができます.

コントローラのコンストラクタにsubkey.driverNameを渡すと,自動的にそのドライバを取得して,ASIOの初期化を行います.


コントローラを自作する

コントローラの自作は少し難しいかもしれません.


original.cpp

#include "TinyASIO.hpp"

using namespace asio;

// ControllerBaseを継承することで,オリジナルのコントローラを作れます
class OriginalController : public ControllerBase
{
// コールバック関数から呼び出せるようにstaticで宣言する
static InputBuffer* input;
static OutputBuffer* output;

// バッファリング用のコールバック関数
// バッファが満杯になるとASIOから呼び出される
static void BufferSwitch(long index, long)
{
void* outBuf = output->GetBuffer(index); // ASIO側の出力バッファのアドレス
void* inBuf = input->GetBuffer(index); // ASIO側の入力バッファのアドレス

// 入力から出力に流す,BufferSize()でバッファの容量を返す
memcpy(outBuf, inBuf, BufferSize()); // 入力バッファ0 => 出力バッファ0

// 入力ストリームに蓄積する,BufferLength()でバッファの個数を返す
input->Store(inBuf, BufferLength()); // 入力バッファ0 => 入力ストリーム0

// note: 重い処理をかけると音が途切れるので注意
// note: フーリエなどの処理は一度ストリームに蓄積して,コールバック関数外で処理するのがベタです
}

public:
OriginalController(const std::string& driverName) : ControllerBase(driverName)
{
// CreateBufferでバッファの初期化とコールバック関数の登録をする
// 1番の入力チャンネルと0番の出力チャンネルからバッファを生成する
CreateBuffer({channelManager->Inputs(1), channelManager->Outputs(0)}, &BufferSwitch);
input = &bufferManager->Inputs(0); // 入力チャンネル1 => 入力バッファ0
output = &bufferManager->Outputs(0); // 出力チャンネル0 => 出力バッファ0
}

StreamPtr Fetch()
{
// 入力ストリームの内容を取り出す
return input->Fetch();
}
};

InputBuffer* OriginalController::input = nullptr;
OutputBuffer* OriginalController::output = nullptr;


とりあえず,ソースコード中のコメントに必要な情報は書いてあります.

InputBufferもしくはOutputBufferのポインタをstatic宣言することと,CreateBuffer関数でチャンネルとバッファを対応付けることを忘れなければ,オールオーケーじゃないかなと.

バッファとストリームという言葉が出てきますが,バッファというのはASIO側で確保されるメモリのことを指します.ストリームの方は,バッファから受け取ったデータを退避させるためのメモリです.

バッファのデータはリアルタイムにやり取りさせますが,ストリームの方は逐次的にデータをやり取りするためのものです.


おわりに

ちょっと急ぎ足になってしまいましたが,これでリアルタイムに音声処理ができるようになります.

音が遅れて聴こえなくなるだけなんですが,普通にOSのサウンドドライバを通すと,0.2~0.5秒ぐらい遅れて聞こえてきます.

楽器とコンピュータのインタラクションをするのに,この遅れはかなり致命的です.

ASIOはそこをミリ秒以下の遅れにしてくれる画期的な規格なのですが,いかんせん開発が難しいです.

開発を楽にしたいという理由でTinyASIOを作りましたが,今ではSiv3DとTinyASIOがないと研究にならないレベルで依存しています.

他の人も,ぜひぜひASIOで楽器とコンピュータのインタラクションを実現してみてください.

そういうわけで筆を置きます.

Siv3Dアドカレの7日担当は@Pctg-x8さんです.