LoginSignup
10
7

More than 5 years have passed since last update.

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

Posted at

はじめに

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さんです.

10
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
10
7