はじめに
DTMではオーディオ・インターフェースという音響機器を使って,リアルタイムにDTMソフトと楽器間の通信を行っています.通信はASIOという規格が使われ,ASIOを使うと音が遅れて聴こえるという現象がなくなります.
さて,このASIOですが,オーディオ・インターフェースを持っていないとできないのかと言うとそうでもなく,ASIO4ALLというドライバを入れれば使えるようになります.ただし,あくまで仮想的な振る舞いをするだけですので,そんなに速くならないかもしれません.あしからず.
TinyASIO
TinyASIOは,ASIOを操作するためのラッパーライブラリです.普通はCOMインターフェースを直接叩かないとASIOが使えないのですが,その辺りをカプセル化して使い易くしてあります.
最も簡単なサンプル
そういうわけで,雰囲気をつかむために簡単なサンプルを例示します.
#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を提供しています.
コントローラの仕組みは後ほど詳しく説明します.
オーディオ・インターフェースの一覧を取得
まずは,登録されているオーディオ・インターフェースの一覧を取得します.
#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の初期化を行います.
コントローラを自作する
コントローラの自作は少し難しいかもしれません.
#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さんです.