5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JUCEAdvent Calendar 2020

Day 7

JUCE アプリ上で仮想オーディオデバイスを使用する

Last updated at Posted at 2020-12-28

はじめに

DAW のようなアプリケーションは、GUI の処理(画面の描画処理やユーザーの操作に反応する処理)とオーディオデバイスを利用した録音/再生処理が、さまざまなタイミングでお互いに影響を及ぼします。

そのようなアプリケーションでは、オーディオデバイスが健全に動いていることが重要で、もし使用しているオーディオデバイスがロストするなどしてオーディオデバイスからの録音/再生要求の処理が止まってしまうと、アプリケーションの処理が正しく進行できなくなってしまうことがあります。

JUCE を使ったアプリケーションであれば、 JUCE の仕組みの上で仮想オーディオデバイスを用意できるため、もとのオーディオデバイスが利用できなくなったとしても、仮想オーディオデバイスに切り替えることでこの問題に対処できます。(ここでいう仮想オーディオデバイスとは、あくまで JUCE のアプリケーション上に用意したもので、システム上に何らかのデバイスやドライバをインストールして利用可能にするようなものではありません)1

この記事では、この仮想オーディオデバイスを実装する方法について解説します。

大まかな設計について

JUCE を使ったアプリケーションでオーディオデバイスの列挙や切り替えを行うには AudioDeviceManager クラスを使用します。

AudioDeviceManager にはいくつかの AudioIODeviceType クラスを登録でき、これが CoreAudio, ASIO, WASAPI などのオーディオデバイスドライバに相当します。 JUCE ではこれをデバイスタイプと呼びます。

AudioIODeviceType は、 AudioIODevice というクラスのインスタンスを構築する仕組みを持っていて、これが個別のオーディオデバイスに相当します。(この辺りの仕組みについては、 @COx2 さんの JUCEのAudioIODeviceについての覚え書き の記事が参考になります)

AudioIODeviceType クラスや AudioIODevice クラスは抽象基底クラスになっているため、これを継承すれば、独自の仮想オーディオデバイスを実装できます。

具体的には、以下のような作業を行います。

  1. AudioIODeviceType を継承した VirtualAudioIODeviceType クラスを定義する。
  2. AudioIODevice を継承した VirtualAudioIODevice クラスを定義して、それを VirtualAudioIODeviceType から構築できるようにする。
  3. VirtualAudioIODevice の中に、(あたかも実際のオーディオデバイスが動作しているかのように)録音/再生データをアプリケーション側とやり取りする仕組みを実装する。
  4. 定義した VirtualAudioIODeviceType を AudioDeviceManager に登録し、 AudioDeviceManager から仮想オーディオデバイスを利用できるようにする。

実装について

上の設計方針に従って実装していきます。(完全なコードは、この記事の下部に掲載します。)

デバイスタイプ名とデバイス名の定義

まずは、デバイスタイプの名前と、そのデバイスタイプで利用できるデバイスの名前を決めます。

/** デバイスタイプ名
*/
String getVirtualAudioIODeviceTypeName() { return "Demo Virtual Audio"; }

/** 入力デバイス名
*/
String getVirtualAudioInputDeviceName() { return "Demo Virtual Audio Input"; }

/** 出力デバイス名
*/
String getVirtualAudioOutputDeviceName() { return "Demo Virtual Audio Output"; }

VirtualAudioIODeviceType の定義

次に VirtualAudioIODeviceType クラスを定義します。

/** デバイスが一つもない環境用に、代替のデバイスとして利用可能なデバイスを提供するデバイスタイプ。

    このデバイスタイプで作成されるデバイスは、実際には外部のオーディオデバイスに接続はせず、実時間と同じ速度でオーディオデータを生成/消費だけする。
 */
class VirtualAudioIODeviceType
:   public AudioIODeviceType // -- (A-1)
{
public:
    // -- (A-2)
    VirtualAudioIODeviceType()
    :   AudioIODeviceType(getVirtualAudioIODeviceTypeName())
    {}

    // ...
};

(A-1) VirtualAudioIODeviceType クラスは JUCE の AudioIODeviceType から派生させます。

(A-2) そして、親クラスである AudioIODeviceType のコンストラクタにデバイスタイプ名を渡して、 AudioIODeviceType を初期化します。

次に AudioIODeviceType クラスはいくつかの仮想関数を定義しているため、それをオーバーライドします。

class VirtualAudioIODeviceType
:   public AudioIODeviceType
{
    // -- (A-3)
    bool hasSeparateInputsAndOutputs () const override
    {
        return true;
    }

    // -- (A-4)
    void scanForDevices() override
    {}

    // -- (A-5)
    StringArray getDeviceNames (bool wantInputNames=false) const override
    {
        StringArray arr;
        if(wantInputNames) {
             arr.add(getVirtualAudioInputDeviceName());
        } else {
             arr.add(getVirtualAudioOutputDeviceName());
        }
        return arr;
    }

    // -- (A-6)
    int getDefaultDeviceIndex (bool forInput) const override
    {
        return 0;
    }

    // -- (A-7)
    AudioIODevice * createDevice (const String &outputDeviceName, const String &inputDeviceName) override
    {
        bool const has_input = (inputDeviceName == getVirtualAudioInputDeviceName());
        bool const has_output = (outputDeviceName == getVirtualAudioOutputDeviceName());

        if(has_input == false && has_output == false)
        {
            return nullptr;
        }

        String deviceName = outputDeviceName.isNotEmpty() ? outputDeviceName : inputDeviceName;
        return new VirtualAudioIODevice(deviceName, has_input, has_output);
    }

    // -- (A-8)
    int getIndexOfDevice (AudioIODevice *device, bool asInput) const override
    {
        if(device == nullptr) { return -1; }

        auto p = dynamic_cast<VirtualAudioIODevice*>(device);
        if(p == nullptr) {
            return -1;
        }

        if(asInput && p->hasInput()) { return 0; }
        if(asInput == false && p->hasOutput()) { return 0; }

        return -1;
    }
};

(A-3) の hasSeparateInputsAndOutputs() メンバ関数は、このデバイスタイプの特性として、入力と出力のデバイスを分離して扱えるかどうかを定義しています。 macOS の CoreAudio や Windows の WASAPI などのデバイスタイプではこのような仕組みがサポートされていて、入力と出力で異なるデバイスを指定できるようになっています。一方で ASIO や OpenSL などのデバイスタイプでは、入力と出力で異なるデバイスを指定することができません。

今回定義するデバイスタイプでは macOS の CoreAudio や Windows の WASAPI などと同じように、入力と出力のデバイスを分離して扱えるように実装するため、 true を返すようにこの仮想関数をオーバーライドします。

(A-4) の scanForDevices() メンバ関数では、現在の最新のデバイス状態をスキャンする処理を行います。このデバイスタイプは実際のオーディオデバイスは利用しないため、実際に何かをスキャンする処理は不要です。そのためこの関数では何も処理を行なっていません。

(A-5) の getDeviceNames() メンバ関数では、このデバイスタイプで利用可能なオーディオデバイスのリストを返します。今回は入力と出力でそれぞれ、"Demo Virtual Audio Input" と "Demo Virtual Audio Output" というデバイスを利用できるようにしているため、その名前を返しています。

(A-6) の getDefaultDeviceIndex() メンバ関数は、事前にスキャンしたデバイスのうち、デフォルトのデバイスのインデックスを返す処理を行います。このデバイスタイプでは、入力と出力で一つずつしかデバイスを定義しないため、ここでは wantInputNames フラグの値が true/false どちらであっても 0 を返しています。

(A-7) の createDevice() メンバ関数は、 VirtualAudioIODevice クラスのオブジェクトを構築して返します。ここで返したオブジェクトは AudioDeviceManager によって管理され、オーディオデバイスとして利用されます。

createDevice() メンバ関数の引数には入力デバイスと出力デバイスの名前が一緒に渡されます。今回の実装では入力デバイス名と出力デバイス名のそれぞれに有効なデバイス名が指定されたかどうかに合わせて VirtualAudioIODevice のコンストラクタに渡すフラグを切り替えています。例えば、入力デバイス名が空で出力デバイス名のみが指定された場合は、出力デバイスとしてだけ動作する VirtualAudioIODevice を構築します。 2

(A-8) の getIndexOfDevice() メンバ関数は、事前に作成されたデバイスが、 getDeviceNames() から返るデバイスのリストの何番目に位置するものなのかを返します。

今回実装するデバイスタイプでは入力と出力でそれぞれ一つずつしかデバイスを定義していないため、 asInput フラグとともに、それに合う入力デバイスか出力デバイスが渡されたときは 0 を返し、それ以外のケースでは -1 を返すようにしています。

VirtualAudioIODevice の定義

次に、デバイスとして動作する VirtualAudioIODevice を定義します。

/** 仮想的なオーディオ入出力デバイス
*/
class VirtualAudioIODevice
:   public AudioIODevice // -- (B-1)
,   Thread
{
public:
    // -- (B-2)
    VirtualAudioIODevice(String deviceName, bool hasInput, bool hasOutput)
    :   AudioIODevice(deviceName, getVirtualAudioIODeviceTypeName())
    ,   Thread("VirtualAudioIODeviceThread")
    {
        if(hasInput) {
            input_channel_names_ = Array<String> {
                "Virtual Input Left",
                "Virtual Input Right",
            };
        }
        if(hasOutput) {
            output_channel_names_ = Array<String> {
                "Virtual Output Left",
                "Virtual Output Right",
            };
        }
    }

    // -- (B-3)
    bool hasInput() const { return input_channel_names_.size() > 0; }
    bool hasOutput() const { return output_channel_names_.size() > 0; }

private:
    Array<String> input_channel_names_;
    Array<String> output_channel_names_;
};

(B-1) VirtualAudioIODevice クラスは JUCE の AudioIODevice から派生させます。
また、実際のオーディオデバイスが専用のスレッド上でオーディオ処理を行う挙動をこのクラスで再現するために、 juce::Thread クラスを継承して、スレッドを作成できるようにしています。

(B-2) VirtualAudioIODevice クラスのコンストラクタでは、親クラスである AudioIODevice のコンストラクタに自身のデバイス名とデバイスタイプ名を渡しておきます。
また、 VirtualAudioIODevice がオーディオ処理を行うためのスレッドを後で作成するのに先立ち、そのスレッドの名前を "VirtualAudioIODeviceThread" と設定しています。

さらにコンストラクタの中では、このデバイスがサポートするチャンネル情報を表す配列を準備しています。この配列は次に解説する getInputChannelNames()/getOutputChannelNames() メンバ関数で使用されます。

(B-3) また、 VirtualAudioIODevice クラスが入力チャンネルと出力チャンネルをサポートしているかどうかを返すメンバ関数も定義します。これは AudioIODevice クラスで定義された仮想関数ではありませんが、 VirtualAudioIODeviceType クラスの getIndexOfDevice() の実装でこの関数があると便利なため、このメンバ関数を定義しています。

続いて、 AudioIODevice が定義している仮想関数をオーバーライドします。

class VirtualAudioIODevice
:   public AudioIODevice
,   Thread
{
    // -- (B-4)
    StringArray getInputChannelNames() override
    {
        return input_channel_names_;
    }

    StringArray getOutputChannelNames() override
    {
        return output_channel_names_;
    }
    
    // -- (B-5)
    Array<double> getAvailableSampleRates () override
    {
        return { 44100.0, 48000.0, 88200.0, 96000.0 };
    }

    Array<int> getAvailableBufferSizes () override
    {
        return { 16, 32, 64, 128, 256, 512, 1024 };
    }

    int getDefaultBufferSize () override
    {
        return 128;
    }
};

(B-4) の getInputChannelNames()/getOutputChannelNames() メンバ関数では、コンストラクタで準備したチャンネル情報を返します。ここで返したチャンネル名と個数の情報が、このデバイスでサポートしているチャンネル情報になります。

(B-5) の getAvailableSampleRates()/getAvailableBufferSize()/getDefaultBufferSize() メンバ関数では、このデバイスがサポートするサンプリングレートやバッファサイズに関する情報を返しています。

class VirtualAudioIODevice
:   public AudioIODevice
,   Thread
{
    // -- (B-6)
    String open(BigInteger const &inputChannels,
                BigInteger const &outputChannels,
                double sampleRate,
                int bufferSizeSamples) override
    {
        input_channels_ = inputChannels;
        output_channels_ = outputChannels;
        current_sampling_rate_ = sampleRate;
        current_buffer_size_ = bufferSizeSamples;
        input_buffer_.setSize(inputChannels.countNumberOfSetBits(), bufferSizeSamples);
        input_buffer_.clear();
        output_buffer_.setSize(outputChannels.countNumberOfSetBits(), bufferSizeSamples);
        output_buffer_.clear();
        is_opened_ = true;

        return "";
    }

    void close () override
    {
        is_opened_ = false;
    }

    bool isOpen () override
    {
        return is_opened_;
    }

    BigInteger input_channels_;
    BigInteger output_channels_;
    double current_sampling_rate_ = 0.0;
    int current_buffer_size_ = 0;
    bool is_opened_ = false;
    AudioSampleBuffer input_buffer_;
    AudioSampleBuffer output_buffer_;

(B-6) では、指定されたパラメータに従ってデバイスをオープンする処理を行います。とはいってもこのデバイスは仮想的なものなので、実際のデバイスをオープンするような処理は不要なため、単に引数に指定されたサンプリングレートやバッファサイズなどのパラメータを自身のメンバ変数として保持したり、 VirtualAudioIODeviceThread の処理で必要になるバッファを確保したりする処理だけを行っています。

open() メンバ関数はデバイスのオープン時にエラーが発生した場合、そのエラーを表す文字列を返します。 今回の実装では、通常ここでエラーが起きることがないため、単に空文字列を返しています。

class VirtualAudioIODevice
:   public AudioIODevice
,   Thread
{
    // -- (B-7)
    void start (AudioIODeviceCallback *callback) override
    {
        jassert(isPlaying() == false);

        callback_ = callback;
        callback_->audioDeviceAboutToStart(this);

        // 別スレッドで、実時間に合わせて callback にオーディオ生成を要求する処理を開始する。
        startThread();
    }

    // -- (B-8)
    void stop () override
    {
        // スレッドの停止
        signalThreadShouldExit();
        waitForThreadToExit(-1);

        // コールバックへの通知
        if(callback_) {
            callback_->audioDeviceStopped();
            callback_ = nullptr;
        }
    }

    // -- (B-9)
    bool isPlaying () override
    {
        return isThreadRunning();
    }

    AudioIODeviceCallback *callback_ = nullptr;

(B-7) の start() メンバ関数では、オープンしたデバイスの録音/再生処理を開始します。録音/再生処理を開始した AudioIODevice は、 start() メンバ関数の引数として渡された AudioIODeviceCallback クラスのオブジェクトのメンバ関数を呼び出して、デバイスで録音したデータを渡したり、逆にデバイスで再生するためのデータを受け取ったりします。

VirtualAudioIODevice クラスの start() メンバ関数では、まず独自の AudioIODeviceCallback クラスのポインタをメンバ変数に保持して、 AudioIODeviceCallback クラスの audioDeviceAboutToStart() メンバ関数を呼び出しています。これによって、このあとオーディオデバイスの処理が開始することを AudioIODeviceCallback に伝えます。

その後、オーディオ処理を行うための専用のスレッド(VirtualAudioIODeviceThread)を作成して、関数から処理を戻します。このスレッドで行う処理が VirtualAudioIODevice の肝となる部分なので、これは後で詳しく解説します。

(B-8) の stop() メンバ関数では、作成したスレッドを停止して、録音/再生処理を停止させています。

(B-9) VirtualAudioIODevice クラスでは、このスレッドが動作中であることがそのままデバイスが動作中であることを表しています。そのため、デバイスが動作中かどうかを表す isPlaying() メンバ関数は、スレッドが動作中かどうかのフラグをそのまま返す形でオーバーライドしています。

次に、 VirtualAudioIODevice の処理の核となるスレッド処理を実装します。

class VirtualAudioIODevice
:   public AudioIODevice
,   Thread
{
    // -- (B-10)
    void run() override
    {
        using seconds_t = double;

        jassert(current_buffer_size_ != 0);
        jassert(current_sampling_rate_ != 0);

        // -- (B-11)
        // 現在のサンプリングレートとバッファサイズから計算した、1フレームの長さ
        seconds_t const frame_time(current_buffer_size_ / (double)current_sampling_rate_);

        // フレーム時間よりも小さい適当な長さを待機時間に設定
        seconds_t const wait_time(frame_time / 2);

        // ドロップアウト時間
        static seconds_t const dropout = 0.3;

        auto now_as_seconds = [] {
            using clock_type = std::chrono::high_resolution_clock;
            auto const dur_since_epoch = clock_type::now().time_since_epoch();
            return std::chrono::duration<double>(dur_since_epoch).count();
        };

        seconds_t last = now_as_seconds();

        // -- (B-12)
        // スレッドの終了フラグが立つまで無限にループしながら、
        // サンプリングレートとバッファサイズに合わせた頻度で audioDeviceIOCallback() を呼び出す。
        for( ; ; ) {
            // -- (B-13)
            if(threadShouldExit()) {
                break;
            }

            auto now = now_as_seconds();

            // -- (B-14)
            if(now - last > dropout) {
                last = now;
                continue;
            }

            // -- (B-15)
            if(now - last >= frame_time) { 
                callback_->audioDeviceIOCallback(input_buffer_.getArrayOfReadPointers(),
                                                 input_buffer_.getNumChannels(),
                                                 output_buffer_.getArrayOfWritePointers(),
                                                 output_buffer_.getNumChannels(),
                                                 output_buffer_.getNumSamples()
                                                 );
                last += frame_time;
            } else {
                std::this_thread::sleep_for(std::chrono::duration<double>(wait_time));
            }
        }
    }
};

(B-10) の run() メンバ関数は、 start() メンバ関数のなかでスレッドを作成したときに、そのスレッド上から呼び出されます。この関数では、実時間に合わせて AudioIODeviceCallback の audioDeviceIOCallback() メンバ関数を定期的に呼び出す処理を行います。

(B-11) の箇所では、 AudioIODeviceCallback のメンバ関数を呼び出す頻度を制御するための変数をいくつか準備しています。実際のオーディオデバイスでは、サンプリングレートとバッファサイズの設定によってどれくらいの頻度でデバイスから録音データを受け取ったり、デバイスに再生用のデータを渡したりするかかが変わります。ここでは実際にオーディオデバイスを利用する時と同じ頻度でこの処理を行うように設定値を計算しています。

変数名 説明
frame_time audioDeviceIOCallback() に渡すオーディオデータが、実時間でどれくらいの時間の長さに相当するかを表す
wait_time for 文のループ処理の頻度を調整するための待機時間
dropout for 文のループ処理に予期せず時間がかかったとき、ドロップアウトと判断して内部の時間情報をリセットするための時間

(B-12) の for 文がこの関数のメインの処理です。この for 文の処理を一言でいうと、無限ループのなかで経過時間を計測しながら、一定時間ごとに AudioIODeviceCallback の audioDeviceIOCallback() メンバ関数を呼び出す処理を行います。

(B-13) for 文の冒頭では、 threadShouldExit() メンバ関数を呼び出して、スレッドを停止するべきかどうかを判定しています。 VirtualAudioIODevice の stop() メンバ関数が呼ばれたときは、その中でスレッドの停止フラグをセットするため、 threadShouldExit() メンバ関数が true を返すようになります。従って、 stop() メンバ関数が呼び出された後は for 文を抜けて、スレッドの処理を終了します。

(B-14) のところでは、ドロップアウトに対する処理を行っています。

VirtualAudioIODevice はなるべく実時間と同じ分量のオーディオデータを生成/消費するように実装にしています。これによって、 もしこのあと呼び出す audioDeviceIOCallback() の中で少し処理時間がかかる事があっても、次回以降のループ処理で待機処理を行わずに audioDeviceIOCallback() を呼び出し続けることで、最終的に実時間に追いつくことができます。

ただしこの仕組みは、見た目上デバイスのオーディオ生成/消費処理が一時的に早送りされたように見えるため、 VirtualAudioIODevice が内部で持っている時間情報と実時間のずれが大きいと、長くこの挙動が続いてしまって、動作的に違和感が出ることがあります。

このため、もしデバッグ処理や突発的な処理負荷などによって前回のループ処理から長い時間が経過してしまっていることが検知された場合は、ドロップアウトが発生したものとして、その時刻から改めて実時間に追従できるように、内部の時間情報をリセットするようにしています。

(B-15) スレッドの停止フラグがセットされておらず、ドロップアウトも検知されなかった場合は、 (B-14) の位置に処理が到達します。ここでは直前の時刻情報と最新の時刻情報の差分から経過時間を計測し、 frame_time よりも長い時間が経過していたら AudioIODeviceCallback の audioDeviceIOCallback() を呼び出します。

audioDeviceIOCallback()には input_buffer_/output_buffer_ というバッファを渡していて、これがそれぞれオーディオデバイスの録音したオーディオデータ/オーディオデバイスで再生するオーディオデータになります。 VirtualAudioIODevice の実装では録音データ(input_buffer_)は必ず無音で、再生データ(output_buffer_)は何もせずそのまま捨てています。

直前の時刻情報と最新の時刻情報の差分が frame_time よりも小さい場合は、 wait_time だけ待機して for 文の先頭から処理を再開します。

その他のメンバ関数

juce::AudioIODevice にはこの他にもいくつかの仮想関数が定義されていますが、特に解説が必要なものではないため割愛します。詳細はこの記事の下に載せた実装や juce::AudioIODevice のドキュメントを参照してください。

AudioDeviceManager への登録

このようにして定義した VirtualAudioIODeviceType クラスを AudioDeviceManager に登録するには、 AudioDeviceManager クラスの addAudioDeviceType() メンバ関数を呼び出します。

AudioDeviceManager adm;
adm.initialise();
adm.addAudioDeviceType(std::make_unique<VirtualAudioIODeviceType>());

VirtualAudioIODevice への切り替えについて

今回解説した内容は VirtualAudioIODeviceType と VirtualAudioIODevice の実装方法だけで、記事の冒頭でユースケースとして触れていた、オーディオデバイスが利用できなくなった場合に仮想オーディオデバイスに切り替えるような仕組みは実装してしていません。

もしそのような仕組みが必要な場合は、一定時間ごとに juce::AudioDeviceManager の状態をチェックするような仕組みを自分で実装して、アプリケーションに組み込む必要があります。

サンプルプロジェクトについて

ここまで紹介した内容をもとに実装したサンプルコードを下に載せます。

このサンプルコードをビルドして実行すると、このような UI を持つウィンドウが表示されます。

スクリーンショット 2020-12-28 14.53.45.png

Audio device type コンボボックスには今回実装した Demo Virtual Audio デバイスタイプが追加されていて、コンボボックスでそれを選択すれば、デバイスを Demo Virtual Audio デバイスタイプのものに切り替えられます。

スクリーンショット 2020-12-28 14.53.55.png

ウィンドウ下部のテキストエリアは、デバイス側の処理が開始してからどれくらいのオーディオデータを生成/消費したかを計測して表示しています。使用するオーディオデバイスを Demo Virtual Audio のデバイスに切り替えた場合でも、実際のデバイスを使用したときと同じようにオーディオデータが生成/消費されていることが、これによって確認できます。

このコードは JUCE の PIP 形式になっているため、このコードを *.h ファイルとして保存し Projucer で開けば、このプログラムをビルドするためのプロジェクトファイルが生成できます。(JUCE 6.0.4 で動作確認しています)

VirtualAudioDeviceDemo.h
/*******************************************************************************
 The block below describes the properties of this PIP. A PIP is a short snippet
 of code that can be read by the Projucer and used to generate a JUCE project.

 BEGIN_JUCE_PIP_METADATA

 name:             VirtualAudioDeviceDemo
 version:          1.0.0
 vendor:           @hotwatermorning
 website:          https://diatonic.jp
 description:      Demo application of Virtual Audio Device.

 dependencies:     juce_core, juce_data_structures, juce_events, juce_graphics,
                   juce_gui_basics, juce_audio_basics, juce_audio_devices,
                   juce_audio_utils, juce_audio_formats, juce_audio_processors,
                   juce_gui_extra
 exporters:        xcode_mac, vs2019

 type:             Component
 mainClass:        MainComponent

 useLocalCopy:     0

 END_JUCE_PIP_METADATA

*******************************************************************************/

#pragma once

#include <thread>

static String getVirtualAudioIODeviceTypeName() { return "Demo Virtual Audio"; }
static String getVirtualAudioInputDeviceName() { return "Demo Virtual Audio Input"; }
static String getVirtualAudioOutputDeviceName() { return "Demo Virtual Audio Output"; }

/** 仮想的なオーディオ入出力デバイス
*/
class VirtualAudioIODevice
:   public AudioIODevice
,   Thread
{
public:
    VirtualAudioIODevice(String deviceName, bool hasInput, bool hasOutput)
    :   AudioIODevice(deviceName, getVirtualAudioIODeviceTypeName())
    ,   Thread("VirtualAudioIODeviceThread")
    {
        if(hasInput) {
            input_channel_names_ = Array<String> {
                "Virtual Input Left",
                "Virtual Input Right",
            };
        }
        if(hasOutput) {
            output_channel_names_ = Array<String> {
                "Virtual Output Left",
                "Virtual Output Right",
            };
        }
    }

    ~VirtualAudioIODevice()
    {
        stop();
        close();
    }

    bool hasInput() const { return input_channel_names_.size() > 0; }
    bool hasOutput() const { return output_channel_names_.size() > 0; }

private:
    StringArray getInputChannelNames () override
    {
        return input_channel_names_;
    }

    StringArray getOutputChannelNames() override
    {
        return output_channel_names_;
    }

    Array<double> getAvailableSampleRates () override
    {
        return { 44100.0, 48000.0, 88200.0, 96000.0 };
    }

    Array<int> getAvailableBufferSizes () override
    {
        return { 16, 32, 64, 128, 256, 512, 1024 };
    }

    int getDefaultBufferSize () override
    {
        return 128;
    }

    String open(BigInteger const &inputChannels,
                BigInteger const &outputChannels,
                double sampleRate,
                int bufferSizeSamples) override
    {
        input_channels_ = inputChannels;
        output_channels_ = outputChannels;
        current_sampling_rate_ = sampleRate;
        current_buffer_size_ = bufferSizeSamples;
        input_buffer_.setSize(inputChannels.countNumberOfSetBits(), bufferSizeSamples);
        input_buffer_.clear();
        output_buffer_.setSize(outputChannels.countNumberOfSetBits(), bufferSizeSamples);
        output_buffer_.clear();
        is_opened_ = true;

        return "";
    }

    void close () override
    {
        is_opened_ = false;
    }

    bool isOpen () override
    {
        return is_opened_;
    }

    void start (AudioIODeviceCallback *callback) override
    {
        jassert(isPlaying() == false);

        callback_ = callback;
        callback_->audioDeviceAboutToStart(this);

        // 別スレッドで、実時間に合わせて callback のメンバ関数を呼び出す処理を行う。
        startThread();
    }

    void stop () override
    {
        // スレッドの停止
        signalThreadShouldExit();
        waitForThreadToExit(-1);

        // コールバックへの通知
        if(callback_) {
            callback_->audioDeviceStopped();
            callback_ = nullptr;
        }
    }

    bool isPlaying () override
    {
        return isThreadRunning();
    }

    String getLastError () override
    {
        return L"";
    }

    int getCurrentBufferSizeSamples () override
    {
        return current_buffer_size_;
    }

    double getCurrentSampleRate () override
    {
        return current_sampling_rate_;
    }

    int getCurrentBitDepth () override
    {
        return 16;
    }

    BigInteger getActiveInputChannels () const override
    {
        return input_channels_;
    }

    BigInteger getActiveOutputChannels () const override
    {
        return output_channels_;
    }

    int     getOutputLatencyInSamples () override
    {
        return 0;
    }

    int     getInputLatencyInSamples () override
    {
        return 0;
    }

private:
    bool is_opened_ = false;
    AudioIODeviceCallback *callback_ = nullptr;
    double current_sampling_rate_ = 0;
    int current_buffer_size_ = 0;
    Array<String> input_channel_names_;
    Array<String> output_channel_names_;
    BigInteger input_channels_;
    BigInteger output_channels_;
    AudioSampleBuffer input_buffer_;
    AudioSampleBuffer output_buffer_;

    /** 実時間に合わせて callback_->audioDeviceIOCallback() を呼び出す。
     */
    void run() override
    {
        using seconds_t = double;

        jassert(current_buffer_size_ != 0);
        jassert(current_sampling_rate_ != 0);

        // 現在のサンプリングレートとバッファサイズから計算した、1フレームの長さ
        seconds_t const frame_time(current_buffer_size_ / (double)current_sampling_rate_);

        // フレーム時間よりも小さい適当な長さを待機時間に設定
        seconds_t const wait_time(frame_time / 2);

        // ドロップアウト時間
        // このデバイスクラスは、実時間と同じ速度でデータを消費するように作成されているが、
        // 突発的な処理負荷やデバッグによるブレークによって大きな遅延が発生したときには、
        // このデバイスクラスで想定している時間の進み方と実際に進んだ時刻が大きくずれる可能性がある。
        // このずれが小さいときは、本来の実時間で消費されたはずのデータ量に達するまで頻繁にループを回してずれを解消できるが、
        // ずれが大きい場合は、頻繁にループを回すことによって早送りされた状態が長く続いてしまうため、動作として好ましくない。
        // そのため、ここで設定した長さ以上の遅延が発生した場合には、ドロップアウトが発生したものとみなし、
        // このデバイスクラスが基準とする時刻を最新の実時間の時刻でリセットして、不自然な早送り状態が発生しないようにしている。
        static seconds_t const dropout = 0.3;

        auto now_as_seconds = [] {
            using clock_type = std::chrono::high_resolution_clock;
            auto const dur_since_epoch = clock_type::now().time_since_epoch();
            return std::chrono::duration<double>(dur_since_epoch).count();
        };

        seconds_t last = now_as_seconds();

        // スレッドの停止フラグがセットされるまで無限にループしながら、
        // サンプリングレートとバッファサイズに合わせた頻度で audioDeviceIOCallback() を呼び出す。
        for( ; ; ) {
            if(threadShouldExit()) {
                break;
            }

            auto const now = now_as_seconds();

            if(now - last >= dropout) {
                last = now;
                continue;
            }

            if(now - last >= frame_time) {
                callback_->audioDeviceIOCallback(input_buffer_.getArrayOfReadPointers(),
                                                 input_buffer_.getNumChannels(),
                                                 output_buffer_.getArrayOfWritePointers(),
                                                 output_buffer_.getNumChannels(),
                                                 output_buffer_.getNumSamples()
                                                 );
                last += frame_time;
            } else {
                std::this_thread::sleep_for(std::chrono::duration<double>(wait_time));
            }
        }
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(VirtualAudioIODevice);
};

/** デバイスが一つもない環境用に、代替のデバイスとして利用可能なデバイスを提供するデバイスタイプ。

    このデバイスタイプによって作成されるデバイスは、外部のオーディオデバイスに接続せず、実時間と同じ速度でデータを消費する。
 */
class VirtualAudioIODeviceType
:   public AudioIODeviceType
{
public:
    VirtualAudioIODeviceType()
    :   AudioIODeviceType(getVirtualAudioIODeviceTypeName())
    {}

private:
    void scanForDevices() override
    {}

    StringArray getDeviceNames (bool wantInputNames=false) const override
    {
        StringArray arr;
        if(wantInputNames) {
             arr.add(getVirtualAudioInputDeviceName());
        } else {
             arr.add(getVirtualAudioOutputDeviceName());
        }
        return arr;
    }

    int getDefaultDeviceIndex (bool forInput) const override
    {
        return 0;
    }

    int getIndexOfDevice (AudioIODevice *device, bool asInput) const override
    {
        if(device == nullptr) { return -1; }

        auto p = dynamic_cast<VirtualAudioIODevice*>(device);
        if(p == nullptr) {
            return -1;
        }

        if(asInput && p->hasInput()) { return 0; }
        if(asInput == false && p->hasOutput()) { return 0; }

        return -1;
    }

    bool hasSeparateInputsAndOutputs () const override
    {
        return true;
    }

    AudioIODevice * createDevice (const String &outputDeviceName, const String &inputDeviceName) override
    {
        bool const has_input = (inputDeviceName == getVirtualAudioInputDeviceName());
        bool const has_output = (outputDeviceName == getVirtualAudioOutputDeviceName());

        if(has_input == false && has_output == false)
        {
            return nullptr;
        }

        String deviceName = outputDeviceName.isNotEmpty() ? outputDeviceName : inputDeviceName;
        return new VirtualAudioIODevice(deviceName, has_input, has_output);
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(VirtualAudioIODeviceType);
};

//============================================================================
//
// 以下は、動作検証のサンプルプログラム用のクラス
//

//! デバイスが動いていることを確認するためのコールバック
class DemoIOCallback
:   public AudioIODeviceCallback
{
public:
    struct DeviceCallbackState
    {
        double sample_rate_ = 0;
        int64 frame_count_ = 0;
        int64 total_processed_samples_ = 0;
        double total_processed_time_ = 0;
        bool is_running_ = false;
        String last_error_;

        void reset()
        {
            *this = DeviceCallbackState();
        }
    };

    DeviceCallbackState getCurrentState() const
    {
        std::unique_lock<std::mutex> lock(mtx_);
        return state_;
    }

private:
    void audioDeviceIOCallback(float const **inputChannelData, int numInputChannels,
                               float **outputChannelData, int numOutputChannels,
                               int numSamples) override
    {
        std::unique_lock<std::mutex> lock(mtx_);
        state_.frame_count_ += 1;
        state_.total_processed_samples_ += numSamples;
        state_.total_processed_time_ += numSamples / state_.sample_rate_;

        for(int ch = 0; ch < numOutputChannels; ++ch) {
            FloatVectorOperations::fill(outputChannelData[ch], 0.0, numSamples);
        }
    }

    void audioDeviceAboutToStart(AudioIODevice *device) override
    {
        std::unique_lock<std::mutex> lock(mtx_);
        state_.reset();
        state_.sample_rate_ = device->getCurrentSampleRate();
        state_.is_running_ = true;
    }

    void audioDeviceStopped() override
    {
        std::unique_lock<std::mutex> lock(mtx_);
        state_.is_running_ = false;
    }

    void audioDeviceError(const String &errorMessage) override
    {
        std::unique_lock<std::mutex> lock(mtx_);
        state_.last_error_ = errorMessage;
    }

    std::mutex mutable mtx_;
    DeviceCallbackState state_;
};

class MainComponent
:   public Component
,   Timer
{
public:
    /** コンストラクタ
    */
    MainComponent()
    {
        adm_.initialise(256, 256, nullptr, true);
        adm_.addAudioDeviceType(std::make_unique<VirtualAudioIODeviceType>());

        selector_ = std::make_unique<AudioDeviceSelectorComponent>(adm_, 0, 256, 0, 256,
                                                                   false, false, true, false);

        addAndMakeVisible(*selector_);
        addAndMakeVisible(editor_);

        editor_.setReadOnly(true);
        editor_.setMultiLine(true);
        editor_.setFont(juce::Font(juce::Font::getDefaultMonospacedFontName(), 14.0, juce::Font::plain));

        startTimer(16);

        adm_.addAudioCallback(&callback_);

        setSize(600, 500);
    }

    ~MainComponent()
    {
        stopTimer();
        adm_.removeAudioCallback(&callback_);
    }

private:
    AudioDeviceManager adm_;
    std::unique_ptr<AudioDeviceSelectorComponent> selector_;
    TextEditor editor_;
    DemoIOCallback callback_;

    void timerCallback() override
    {
        auto const state = callback_.getCurrentState();
        String msg;
        String const nl = "\n";

        msg += String("Frame Count       : ") + String(state.frame_count_) + nl;
        msg += String("Processed Samples : ") + String(state.total_processed_samples_) + nl;
        msg += String("Processed Time    : ") + String(state.total_processed_time_, 4) + nl;
        msg += String("Is Running        : ") + (state.is_running_ ? "true" : "false");

        editor_.setText(msg);
    }

    void resized() override
    {
        auto b = getLocalBounds();

        auto top = b.removeFromTop(360);
        auto bottom = b;
        selector_->setBounds(top);
        editor_.setBounds(bottom);
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};
  1. もしシステムに複数のオーディオデバイスが接続されいれば、利用中のオーディオデバイスが突然利用できなくなったとしても、ユーザーに他のオーディオデバイスを選択させ、そのデバイスに切り替えることでアプリケーションの処理を続行できます。とはいえ、システムにオーディオデバイスが一つしかない場合にはこの方法は利用できませんし、ユーザーに他のオーディオデバイスを選択させるワークフローを設計するときに、少なくとも一つのオーディオデバイスがアプリケーションから自由に利用できると保証できれば便利な場合があります。

  2. 今回は簡易的に、 VirtualAudioIODevice 自体に渡すフラグによって VirtualAudioIODevice の入出力設定を切り替えられるようにしていますが、実際のオーディオデバイスを利用するようなデバイスタイプの場合は createDevice() メンバ関数でもう少し複雑な処理を行っています。例えば macOS の CoreAudio 用のデバイスタイプでは createDevice() に渡された入力/出力デバイス名をもとに入力/出力それぞれのオーディオデバイスを表すオブジェクトを生成し、それを結合させた AudioIODevice のオブジェクトを構築して返すような処理を行っていたりします(参照: https://github.com/juce-framework/JUCE/blob/b8206e3604ebaca64779bf19f1613c373b9adf4f/modules/juce_audio_devices/native/juce_mac_CoreAudio.cpp#L2169

5
4
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
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?