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

  • 3
    Like
  • 0
    Comment
More than 1 year has 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さんです.