C++
audio
JUCE
JUCEDay 3

JUCEでのオーディオデバイスの扱い方

More than 1 year has passed since last update.

はじめに

この記事はJUCE Advent Calender 2017 3日目の記事です。
本記事はJUCEにおいてオーディオデバイスを扱う上で必要となる事柄について解説していきます。

この記事で解説する事

  • JUCEを用いたオーディオデバイス周りの解説と扱い方

解説しない事

  • JUCEアプリケーションの開発手法
  • DSPやその周辺の基礎
  • 各オーディオAPI毎の要求する環境設定方法

あくまでもJUCEのオーディオデバイス回りの解説となりますので、
各環境の導入方法や、踏み込んだ解説は本記事では行いません。

ASIO SDKについてはCOx2さんが詳解していますので、そちらを参照すると良いでしょう
(内容的にも結構被ってて焦りました)

JUCEで作るASIO対応アプリ -上級編-

用意するJUCEプロジェクトについての注意点

Projucerを用いて生成されたCUIプロジェクトを想定しており、サンプルコードもその環境で構築されています。
また、記事が複雑にならないよう、一般的なjuce::ApplicationComponentを基幹とした構成は避けており、
直接JUCEのオーディオデバイスモジュールを叩くような構成をとっています。

実はオーディオデバイスに関してはjuce::AudioDeviceSelectorComponentというコントロールパネルが実装されており、
JUCEの定めるアプリケーション機構に沿う場合はこれを使えばいいだけなので、本記事で解説する内容の大部分は直接触ることはないかもしれません。
オーディオデバイス周りは自分でハンドリングしたいという方や、デバッグの際にお役に立てばと思います。

オーディオデバイスについて

とりあえずデバイスに音を流してみよう

まずは基本となるコンポーネントにどのようなものがあるかを解説するため、簡単なサンプルコードを示します。
以下のコードは、正弦波をデフォルトのオーディオデバイスに対し、5秒間流し続けるコードです。
オーディオデバイスはステレオ環境を想定し、初期化を行っています。
(実際に動作させると一般的な環境では音が再生されますので注意して下さい)

#include <thread>
#include <chrono>
#include <Windows.h> // Windowsだとデフォルト設定の場合はWASAPIが選択されるため必要
#include "../JuceLibraryCode/JuceHeader.h"

int main (int argc, char* argv[])
{
    CoInitialize(nullptr); // Windowsだとデフォルト設定の場合はWASAPIが選択されるためCOMの初期化が必要

    juce::AudioDeviceManager adm;
    juce::ToneGeneratorAudioSource tone_generator;
    juce::AudioSourcePlayer player;
    audio_source_player.setSource(&tone_generator);
    adm.addAudioCallback(&audio_source_player);

    adm.initialiseWithDefaultDevices(0, 2); // 第一引数はinput, 第二引数はoutput

    std::this_thread::sleep_for(std::chrono::seconds(5));

    adm.closeAudioDevice();
    return 0;
}

オーディオデバイスを扱うに当たってJUCEで核を為すのはjuce::AudioDeviceManagerというクラスで、
オーディオデバイスの列挙や初期化といったデバイスの管理に必要な機能を提供します。
このコード中ではinitialiseWithDefaultDevices()により、デフォルトのデバイスタイプおよびデフォルトのデバイスを用いて初期化を行っています。
初期化した瞬間にデバイスは駆動を始めますので、その点は注意してください。

続いて、実際に音を流す部分ですが、

    juce::ToneGeneratorAudioSource tone_generator;
    juce::AudioSourcePlayer player;
    audio_source_player.setSource(&tone_generator);
    adm.addAudioCallback(&audio_source_player);

今回はすでに用意されているjuce::ToneGeneratorAudioSourceという指定したパラメータに基づき、デバイスの要求毎に正弦波をストリーミングするクラスを用います。
juce::ToneGeneratorAudioSourceは、オーディオストリーミングを簡易的に処理するためのjuce::AudioSourceを継承することで実装されています。
ただし、juce::AudioSourceはデバイスからの要求を受ける口がないため、juce::AudioSourcePlayerというデバイスとストリーミングクラスの橋渡しをする仕組みを介しています。

juce::AudioDeviceManageraddAudioCallback(juce::AudioIODeviceCallback)によって設定されたコールバックに対し、デバイスからの要求ごとにそのコールバックを呼び出す機構をもっています。juce::AudioSourcePlayerjuce::AudioIODeviceCallbackを継承していますので、
そのままaddAudioCallback(juce::AudioIODeviceCallback)に対して登録できるわけですね。

なんとなくJUCEでのオーディオデバイス周りの勝手が分かってきたでしょうか?
とりあえず
1. juce::AudioSourceを継承し、音を流す仕組みを実装する
2. 1をjuce::AudioSourcePlayerに登録
3. 2をjuce::AudioDeviceManagerのコールバックに追加
4. juce::AudioDeviceManagerをデフォルトで初期化

すればなんとか再生できそうです。

しかし、この記事を読んでいる方はDAWのようなホストアプリケーションを作りたいと思っているはずです!

DAWによってまちまちですが、自由にオーディオデバイスやその入力チャンネル/出力チャンネル、サンプリングレート、ブロックサイズ...を指定できるはずです。
この後のセクションではそれらを実現すべくオーディオデバイスを扱う各機能を紹介していきます。

オーディオデバイス詳解

このセクションではオーディオデバイスを扱うに当たって必要となる各クラス及び構造体に含まれる各機能を、
実際に扱う事例に倣って解説していきます。

必要となるクラス/構造体一覧

JUCEのオーディオデバイス周りの機能は各クラスに分散しているので、頭の片隅に置いておくと便利です。

juce::AudioDeviceManager

オーディオデバイスタイプの列挙やオーディオデバイスの初期化/読み込み、デバイスからの要求を適切にコールバックするための全般的な管理クラスです。今回は解説しませんが、MIDIデバイスを扱う機能もあります。

また、再生スレッド中のCPU負荷率を計測するgetCpuUsage()のようなものも実装されています。

juce::AudioDeviceManager::AudioDeviceSetup

オーディオデバイスの初期化に必要な設定を格納するための構造体です。

デバイス名と入出力チャンネル数、サンプリングレート、バッファサイズを格納することができます。
useDefualtInputChannelsuseDefaultOutputChannelsをtrueにすると、デバイスの初期化時に
JUCEが定めるデフォルトのチャンネル数が優先されます。

juce::AudioIODeviceType

JUCEでいうオーディオデバイスタイプとは、オーディオデバイスドライバを仲介するAPIの総称です。
よく使うものとしては、WindowsだとWASAPIやASIO、MacOSだとCoreAudioでしょうか。
JUCEではそれらはもちろんDirectSoundや、Linux用のJACKやALSAにも対応しています。

このjuce::AudioIODeviceTypeはそれらのAPIを抽象化して扱うために実装されています。
juce::AudioDeviceManagerを通して取得することができ、有効なオーディオデバイスの列挙、
入出力デバイス名をパラメータとしたデバイス作成、有効なデバイスリストが更新された時のリスナー登録などを担当しています。

また、オーディオデバイスタイプによってデバイスの扱い方も変わってきますので、hasSeparateInputsAndOutputs()のような
入力デバイスと出力デバイスを別々に扱っているか、のような情報も取得することができます。

juce::AudioIODevice

各オーディオデバイスタイプにおけるデバイスの扱いを抽象化したクラスです。

主にデバイスに対して有効な設定の取得と、デバイスの開閉/駆動停止を行うことができます。
チャンネルの扱いについてはちょっと癖があるので気をつけて下さい(後で解説します)。

このクラスを直接構築しても再生を行うことはできるんですが、状態管理も面倒臭いのでそこらへんはjuce::AudioDeviceManagerにお任せすることになるかと思います。

なお、オーディオデバイスタイプがASIOの場合のみhasControlPanel()showControlPanel()によって専用のコントロールパネルの有無の確認と開閉を行う事ができます。

juce::AudioIODeviceCallback

オーディオデバイスからの要求に合わせ呼ばれるコールバッククラスです。
* 1ブロックごとに呼ばれるaudioDeviceIOCallback()
* 再生開始時に呼ばれる audioDeviceAboutToStart()
* 再生停止時に呼ばれる audioDeviceStopped()
* エラー発生時にエラー内容を文字列クラスで受け取る audioDeviceError()

があります。適宜オーバーライドして登録しましょう。
このクラスはjuce::AudioDeviceManager::addAudioCallback()またはjuce::AudioIODevice::start()に渡すことで有効になります。

juce::AudioSource

オーディオデータを生成/加工し、ストリーミングするためのクラスです。
再生開始前に1ブロックあたりのサンプル数の期待値と、サンプリングレートを受け取れるprepareToPlay()と、
再生停止時に呼ばれるreleaseResources()、1ブロックごとに呼ばれるgetNextAudioBlock()によって構成されます。
getNextAudioBlock()で受け取れるAudioSourceChannelInfoは、
入力デバイスが有効な場合、最初に入力データがバッファに格納されてきますので注意してください。

このクラスはあくまでも再生系を楽に扱うためのものなので、juce::AudioIODeviceCallbackaudioDeviceIOCallback()
直接データを生成/加工してしまう、または独自のオーディオデータ管理の仕組みを組み込んでしまう場合は扱う必要はありません。

juce::AudioSourcePlayer

juce::AudioIODeviceCallbackを継承しており、juce::AudioSourcejuce::AudioDeviceManagerの仲介を行います。
setSource()にてjuce::AudioSourceを登録すると、juce::AudioDeviceManagerを通してjuce::AudioIODeviceCallbackにてオーバーライドした関数が呼ばれます。
また、ここで流れてくるオーディオデータのゲイン値の制御も行うことが出来ます。

このクラスはjuce::AudioSourceと同様に、juce::AudioIODeviceCallbac::audioDeviceIOCallback()で全て受けてしまう場合は必要ありません。

要件ごとの実装方法

柔軟にオーディオデバイスを扱う場合、どんな機能が必要となるでしょうか?

  • オーディオデバイスドライバを扱うAPIの選択
  • オーディオデバイスの列挙
  • オーディオデバイスの読み込み
  • オーディオデバイス情報の取得
  • オーディオデバイスからの再生要求コールバック

ひとまずこれらができれば一般的なDAWが提供しているオーディオデバイス設定に対応できそうです。


オーディオデバイスドライバを扱うAPIの選択

上記でも説明しましたが、オーディオデバイスドライバを扱うAPI群はJUCEではjuce::AudioIODeviceTypeという名前で扱っています。
juce::AudioIODeviceTypejuce::AudioDeviceManagerから取得することができます。

juce::AudioDeviceManager adm;
// 有効なjuce::AudioIODeviceTypeの列挙
const auto& device_types = adm.getAvailableDeviceTypes();
for (auto& type : device_types) {
   std::cout << type->getTypeName().toStdString() << std::endl;
}

assert(!device_type.isEmpty());

// オーディオデバイスタイプ名(APIの名前)を指定して有効にする
adm->setCurrentAudioDeviceType(
    device_types[0]->getTypeName(), false
);

// 現在有効なデバイスタイプを得る場合
auto current_device_type = adm.getCurrentDeviceTypeObject();

このコードの例では、juce::AudioDeviceManager::getAvailableDeviceTypes()により列挙を行い、
そこで得られたconst juce::OwnedArray<juce::AudioIODeviceType>&の先頭の値を用いてオーディオデバイスタイプを登録しています。

オーディオデバイスタイプをjuce::AudioDeviceManagerに登録するためにはjuce::AudioDeviceManager::setCurrentAudioDeviceType()
に渡すのですが、この関数の第二引数であるtreatAsChosenDeviceというbool値は、
JUCEで管理している設定値をXMLに登録する機能のものですので、その機構を使わない場合は常にfalseでも大丈夫です。

なお、有効なデバイスタイプは予めAppConfig.hより有効にする必要があります。

...
//==============================================================================
// juce_audio_devices flags:

#ifndef    JUCE_ASIO
// #define JUCE_ASIO 1
#endif

#ifndef    JUCE_WASAPI
 #define JUCE_WASAPI 1
#endif

#ifndef    JUCE_WASAPI_EXCLUSIVE
 #define JUCE_WASAPI_EXCLUSIVE 1
#endif

#ifndef    JUCE_DIRECTSOUND
 #define JUCE_DIRECTSOUND 1
#endif
...

オーディオデバイスを列挙する

サンプルコードではJUCE側でいいかんじに決定してくれるデフォルトのデバイスを選択しました。
適当に音を流したいだけの場合はそれでも良いのですが、ちゃんとしたオーディオアプリケーションを作る場合はユーザに対して選択権を与えないといけません。

デバイスを列挙するためにはjuce::AudioIODeviceTypeから行います。

auto current_type = adm.getCurrentDeviceTypeObject();

// 有効なデバイスの列挙(内部に蓄積する)
current_type->scanForDecices();

// 出力デバイス一覧の取得
for (auto name : current_type->getDeviceNames()) {
    std::cout << "[out] " << name.toStdString() << std::endl;
}

// 入力デバイス一覧の取得
for (auto name : current_type->getDeviceNames(true)) {
    std::cout << "[in"] << name.toStdString() << std::endl;
}

まず、scanForDevices()によってデバイスを列挙することによって、内部にデバイスリストを蓄積します。
そして、実際に一覧を取得するときはgetDeviceNames()を使います。

この関数はデフォルトでは出力デバイスのみを返しますが、引数としてtrue値を渡すことによって、
入力デバイスを得ることができるようになっています。


オーディオデバイスを読み込む

このセクションではオーディオデバイスを初期化する方法を解説するのですが、
以前解説したように、初期化を行う方法はいくつかあります。
今回はjuce::AudioDeviceManager::setAudioDeviceSetup()を用いる方法を解説します。

juce::AudioDeviceManager::AudioDeviceSetup setup;
setup.inputDeviceName = device_type->getDeviceNames()[0];
setup.outputDevicename = device_type->getDeviceNames()[0];
setup.inputChannels.setRange(0, 2, true);
setup.outputChannels.setRange(0, 2, true);
setup.bufferSize = 512;
setup.sampleRate = 44100;
setup.useDefaultOutputChannels = false;
setup.useDefaultInputChannels = false;

auto error_message = adm.setAudioDeviceSetup(setup, false);
assert(error_message.isEmpty());

まず、前項で解説したjuce::AudioIODeviceTypeから取得したデバイス名と共に、
juce::AudioDeviceManager::AudioDeviceSetupを構築していきます。

この構造体にセットすべきパラメータは、

  • 入出力デバイス名
  • 入出力各チャンネルのon/off情報
  • バッファサイズ
  • サンプリングレート

となります。
それをjuce::AudioDeviceManager::setAudioDeviceSetup()に渡すことによってデバイスを初期化することができます。
ここで注意する必要があるのがチャンネル数で、実はデバイスが対応するチャンネル数というのは、
juce::AudioDeviceManager::setAudioDeviceSetup()で得られたjuce::AudioIODeviceからしか得る道がありません…

そのため、最初はjuce::AudioIODeviceType::scanForDevices()で得られた各デバイスをuseDefaultOutputChannelsをtrueにして初期化する必要があるかもしれません。
(よくDAWで起動した時に『デバイスの読み込み中です』のようなダイアログがポップアップして若干待たされるのは多分これ)。

もう一点チャンネルに関する捕捉として、JUCEではチャンネル設定を扱う場合juce::BigIntegerを使います。
これは多倍長整数を扱うためのクラスで、ビットごとの操作も行えるよう設計されています。
オーディオデバイスはこのjuce::BigIntegerの仕組みを利用し、チャンネル位置ごとのon/off状態を管理しています。
ここでは、

setup.inputChannels.setRange(0, 2, true);

のように設定してますので、0番目と1番目のチャンネルを有効にしていることになります(つまりステレオですね)。
チャンネルの仕組みについては後の項でもう少し詳しく解説していきます。


オーディオデバイス情報の取得

前項で説明したようにオーディオデバイスの情報はjuce::AudioIODeviceから得ます

auto device = adm->getCurrentAudioDevice();

// デバイス名とデバイスタイプ名
std::cout << device->getName() << std::endl;
std::cout << device->getTypeName() << std::endl;

// 有効な出力チャンネル名の取得
for (auto name : device->getOutputChannelNames()) {
    std::cout << name.toStdString() << std::endl;
}

// 有効な入力チャンネル名の取得
for (auto name : device->getInputChannelNames()) {
    std::cout << name.toStdString() << std::endl;
}

// 有効なサンプリングレートの取得(doubleに注意)
for (double sampling_rate : device->getAvailableSampleRates()) {
    std::cout << std::round(sampling_rate) << std::endl;
}

// 有効なバッファサイズの取得
for (auto buffer_size : device->getAvailableBufferSizes()) {
    std::cout << buffer_size << std::endl;
}

// デフォルトのバッファサイズの取得
auto default_buffer_size = device->getDefaultBufferSize();

// 現在デバイスに設定されているバッファサイズの取得
auto current_buffer_size = device->getCurrentBufferSizeSamples();

// 現在デバイスに設定されているサンプリングレートの取得
double current_sampling_rate = device->getCurrentSumpleRate();

// 現在デバイスに設定されているビット深度の取得
auto bit_depth = device->getCurrentBitDepth();

// "アクティブ"な出力/入力チャンネル情報の取得
juce::BigInteger active_out_ch = device->getActiveOutputChannels();
juce::BigInteger active_in_ch = device->getActiveInputChannels();

ざっと取得できる関数を列挙してみました。

取得できる情報も概ねソースコードのコメントに書いてある通りなんですが、
一つ注意して欲しいのがチャンネルの扱いです!

そのデバイスがサポートするチャンネル数を調べたい時は、getOutputChannelNames()または、getInputChannelNames()を使用してください。
getActiveOutputChannels()getActiveInputChannels()はあくまでもアクティブなチャンネル数であり、
juce::BigIntegerで表されています。たとえば、

Output Channel 1
Output Channel 2
Output Channel 3
Output Channel 4
Output Channel 5
Output Channel 6

というチャンネルが列挙された場合、ここから3番と4番のチャンネルをステレオで扱うとします。

そうすると各チャンネルの状態をjuce::BigIntegerで表すと、

juce::BigInteger active_out_ch = device->getActiveOutputChannels();
assert(
    active_out_ch[0] == false &&
    active_out_ch[1] == false &&
    active_out_ch[2] == true &&
    active_out_ch[3] == true &&
    active_out_ch[4] == false &&
    active_out_ch[5] == false
);

の様な状態になります(juce::BigIntegerbool operator[]を実装しているので各ビット位置の情報がとれます)。
無理にjuce::BigIntegerからチャンネル数を取ろうとしても、

juce::BigInteger active_out_ch = device->getActiveOutputChannels();
// 有効になっているビット数を得る
assert(active_out_ch->countNumerOfSetBits() == 2);
// これは有効な最上位ビットを得る
assert(active_out_ch->getHighestBit() == 3);
// インデックスを指定し、次の有効なビット位置を得る。得られない場合は-1を返す
assert(active_out_ch->findNextSetBit(0) == 2);
// インデックスを指定し、次の無効なビット位置を得る。得られない場合は-1を返す
assert(active_out_ch->findNextSetBit(2) == 4);

といった関数群しか用意されていないため、実際のデバイスのサポートする有効なチャンネル数である6を得ることは不可能となっています。
そのため、繰り返しになりますがgetActiveOutputChannels()getActiveInputChannels()によって取得できる情報は、"現状の"チャンネル設定情報であって、デバイスのサポートするチャンネル数情報ではないという事を留めておいてください。


オーディオデバイスからの再生要求コールバック

オーディオデバイスからの要求に対応するためには、juce::AudioIODeviceCallbackを用います。

class MyCallback
    : public juce::AudioIODeviceCallback
{
public:
    // デバイスの再生要求を受ける関数
    void audioDeviceIOCallback(
        const float** inputChannelData,
        int numInputChannels,
        const float** outputChannelData,
        int numOutputChannels,
        int numSamples) override
    {
    } 

    // デバイスの再生状態への移行を受けとる関数
    void audioDeviceAboutToStart(juce::AudioIODevice* device) override {}

    // デバイスが停止状態へ移行したことを受けとる関数
    void audioDeviceStopped() override {}

    // エラー発生時に検知するための関数
    void audioDeviceError(const juce::String& errorMessage) override {}
};

オーバーライドすべき関数はaudioDeviceIOCallback(), audioDeviceAboutToStart(),
audioDeviceStopped(), audioDeviceError()の4つです。

ここで重要なのはaudioDeviceIOCallback()です。

void MyCallback::audioDeviceIOCallback(
    const float** inputChannelData,
    int numInputChannels,
    const float** outputChannelData,
    int numOutputChannels,
    int numSamples)
{
    for (auto ch=0; ch<numOutputChannels; ++ch) {
        const auto data = outputChannelData[ch];
        memcpy(data, buffer_[ch], sizeof(float)*numSamples);
    }
} 

簡単に実装するとこんな感じになるでしょうか。
ここではメンバとして保持しているバッファをコピーしてるだけですが、
実際はこの中でオーディオデータを加工したり、バッファをリングバッファにしたりともう少し複雑になるかと思います。
numSamplesデバイスを初期化した時に指定したブロックサイズが確実に要求されるとは限らないため注意してください。

再生系共通の注意点として、この関数は再生スレッドから呼び出されるため、負荷の原因となる余計なメモリ確保は行わないよう細心の注意を払いましょう
(特殊なアロケータを実装していないスマートポインタの類はもちろん、自動で領域の再確保が行われるstd::vectorのようなコンテナも該当します)。
再生スレッドを扱うにあたって非常に参考になるCppConのセッションと記事を以下に紹介しておきます。

さいごに

JUCEが登場したことでオーディオアプリケーション開発の敷居も大分低くなったように思えます。
JUCEを携わっているプロジェクトに採用してから2年半ほど経過していますが、それ以前は碌にドキュメントも整備されていないプラットフォーム固有のAPIや、ASIO SDKのAPIなどを直で触ったり、portaudioを使ったりして精神を擦り減らしていました。

とはいえ、JUCE自体もドキュメントは他よりマシといえども網羅されているほど親切ではないので、結局はソースを見ることが多々出てくるのですが、このライブラリの良さはなんといってもC++11移行の規格をベースに開発されていることです!
とても追いやすいコードになっていますので、詰まったらどんどん読みましょう。