JUCEのAudioIODeviceについての覚え書き


オーディオアプリケーションフレームワークであるJUCEでは、様々なプラットフォームネイティブなオーディオAPI(オーディオドライバ)をサポートしています。これらの実装はjuce_audio_devicesモジュールに置かれています。
公式リファレンス: juce_audio_devices

サポートするオーディオAPI(一例)

  • Windows: DirectSound, WASAPI, ASIO
  • macOS: CoreAudio
  • Linux: ALSA, JACK

さて、現状でも十分なくらいのオーディオAPIを公式にサポートしているJUCEですが、何らかの事情で公式にサポートされていないオーディオAPIで動作するアプリケーションを実装したいとなった場合はどうしましょう?そのような状況にも対応できるように、JUCEでは開発者が独自にオーディオデバイスを追加できる仕組みが用意されています。

JUCEにおけるオーディオ処理の仕組み

まずは前提の知識として、JUCEのオーディオ処理の仕組みについて確認しておきましょう。
JUCEのオーディオに関する機能は複数のクラスに分散されており、それらが協調して動作することで、オーディオAPIとのコミュニケーションから信号処理までを分散して各クラスに分割して実装することができます。
JUCEにおけるオーディオデバイスに関連するクラス群の一覧についてはらぴすさんの記事で詳細が書かれているので、そちらを参照するとよいでしょう。JUCEでのオーディオデバイスの扱い方

JUCEでは、オーディオ処理を管理するAudioDeviceManagerというクラスが提供されます。このクラスはオーディオデバイスを抽象化したクラスAudioIODeviceとオーディオ処理(信号処理・オーディオバッファ操作)を抽象化したクラスAudioIODeviceCallbackの橋渡し役も担っています。

次の図は、AudioDeviceManagerクラス、AudioIODeviceクラス、AudioIODeviceCallbackクラスがどのようなやり取りを行っているかを図にしたものです。AudioDeviceManagerAudioIODeviceのデバイス作成やオーディオスレッドの開始を実行し、AudioIODevice内で走っているオーディオスレッドからのコールバックに基づいてAudioDeviceManager登録されたAudioIODeviceCallbackを継承したオブジェクトに対してコールバック関数が実行されるという仕組みになっています。

オーディオデバイスを定義するクラス

オーディオデバイスに関連するクラス群のうち、オーディオAPIの抽象化に関係するクラスは主に次のクラスになります。

  • AudioIODevice: プラットフォームネイティブなオーディオAPIの実装を定義する。公式リファレンス
  • AudioIODeviceType: AudioIODeviceの”型情報”みたいなもの。インスタンス生成やメタ情報を定義する。公式リファレンス

独自のオーディオデバイスをアプリケーションに追加したい場合、以下の作業を行うことで、JUCEのフレームワークに独自のオーディオデバイスを組み込んで使用することができます。

  1. AudioIODeviceAudioIODeviceTypeをそれぞれ継承したクラスを実装する。
  2. プログラム実行時、独自に追加したAudioIODeviceTypeの子クラスをAudioDeviceManagerに登録する。
  3. AudioDeviceManagerを介して独自に追加したオーディオデバイスを生成する

次の図は、AudioIODeviceクラス、AudioIODeviceTypeクラス、AudioDeviceManagerクラスがどのようなやり取りを行っているかを図にしたものです。AudioIODeviceTypeを継承したMyAudioIODeviceTypeクラスをAudioDeviceManagerに登録することで、AudioDeviceManagerからMyAudioIODeviceTypeに対してMyAudioIODeviceのインスタンス生成とオーディオスレッドの開始を実行することができます。MyAudioIODeviceが生成されたら引き続きオーディオスレッドの開始を行うことで、MyAudioIODevice内のオーディオスレッドからのコールバックに基づいてAudioDeviceManagerに対してコールバック関数が実行されるという仕組みになっています。

Sine波を入力するSineWave Audio Deviceの実装例

ここでは実例として、SineWave Audio Deviceを例に、JUCEに独自のオーディオデバイスを追加するまでのソースコードを載せておきます。
このSineWave Audio Deviceは、毎フレーム常に入力バッファへSine波を入力するといった処理を行いますが、AudioSourcePlayerクラスやAudioProcessorPlayerクラスのaudioDeviceIOCallback関数から戻される出力バッファの中身を受け取った後はプラットフォームネイティブなオーディオAPIに渡すことはしていません。そのため、このオーディオデバイスを選択した場合、スピーカーに音声は流れません。

■ SineWaveAudioIODevice.h

#pragma once

#include "../JuceLibraryCode/JuceHeader.h"

// AudioIODeviceクラスを継承する
// オーディオスレッドとしてTimerクラスを継承する
class SineWaveAudioIODevice : public AudioIODevice, private Timer
{
public:
    SineWaveAudioIODevice(const String& deviceName, const String& typeName, const String& outputDeviceID, const String& inputDeviceID);
    ~SineWaveAudioIODevice() override;

    // AudioIODeviceクラスで宣言された関数(インターフェース)をオーバーライドする
    virtual StringArray getOutputChannelNames() override { return { "Out1", "Out2", "Out3", "Out4" }; };
    virtual StringArray getInputChannelNames() override { return{ "In1", "In2", "In3", "In4" }; };
    virtual Array<double> getAvailableSampleRates() override;
    virtual Array<int> getAvailableBufferSizes() override;
    virtual int getDefaultBufferSize() override;
    virtual String open(const BigInteger& inputChannels, const BigInteger& outputChannels, double sampleRate, int bufferSizeSamples) override;
    virtual void close() override;
    virtual bool isOpen() override;
    virtual void start(AudioIODeviceCallback* callback) override;
    virtual void stop() override;
    virtual bool isPlaying() override;
    virtual String getLastError() override;
    virtual int getCurrentBufferSizeSamples() override { return bufferSize; };
    virtual double getCurrentSampleRate() override { return sampleRate; };
    virtual int getCurrentBitDepth() override { return bitDepth; };
    virtual BigInteger getActiveOutputChannels() const override { return channelsOut; };
    virtual BigInteger getActiveInputChannels() const override { return channelsIn; };
    virtual int getOutputLatencyInSamples() override { return 0; };
    virtual int getInputLatencyInSamples() override { return 0; };

private:
    // Timerクラスのメンバ関数(インターフェース)をオーバーライドする
    virtual void timerCallback() override;

    int bufferSize{ 512 };
    double sampleRate{ 44100 };
    int bitDepth{ 32 };
    BigInteger channelsIn, channelsOut;
    int numChannelsIn, numChannelsOut;
    CriticalSection audioDeviceLock;
    bool closed{ false };

    // AudioDeviceMangerで保持しているコールバック先のオブジェクト
    AudioIODeviceCallback* deviceManagerCallbackHandler{ nullptr };
};

// AudioIODeviceTypeクラスを継承する
class SineWaveAudioIODeviceType : public AudioIODeviceType
{
public:
    SineWaveAudioIODeviceType()
        : AudioIODeviceType("SineWave Audio Device")
    {};

    ~SineWaveAudioIODeviceType() override
    {};

    // AudioIODeviceTypeクラスのメンバ関数(インターフェース)をオーバーライドする
    virtual void scanForDevices() override {}
    virtual StringArray getDeviceNames(bool wantInputNames = false) const override;
    virtual int getDefaultDeviceIndex(bool forInput) const override;
    virtual int getIndexOfDevice(AudioIODevice* device, bool asInput) const override;
    virtual bool hasSeparateInputsAndOutputs() const override { return true; };
    virtual AudioIODevice* createDevice(const String& outputDeviceName, const String& inputDeviceName) override;

private:
    StringArray inputDeviceNames { "SineWave Input1", "SineWave Input2" };
    StringArray outputDeviceNames{ "SineWave Output1", "SineWave Output2" };
};

■ SineWaveAudioIODevice.cpp

#include "SineWaveAudioIODevice.h"

SineWaveAudioIODevice::SineWaveAudioIODevice(const String & deviceName, const String & typeName, const String & outputDeviceID, const String & inputDeviceID)
    : AudioIODevice(deviceName, typeName)
{
}

SineWaveAudioIODevice::~SineWaveAudioIODevice()
{
}

Array<double> SineWaveAudioIODevice::getAvailableSampleRates()
{
    Array<double> ret;
    ret.add(44100);
    ret.add(48000);
    ret.add(88200);
    ret.add(96000);
    return ret;
}

Array<int> SineWaveAudioIODevice::getAvailableBufferSizes()
{
    Array<int> ret;
    ret.add(256);
    ret.add(512);
    ret.add(1024);
    return ret;
}

int SineWaveAudioIODevice::getDefaultBufferSize()
{
    return bufferSize;
}

String SineWaveAudioIODevice::open(const BigInteger & inputChannels, const BigInteger & outputChannels, double sampleRate_, int bufferSizeSamples)
{
    ScopedLock lock(audioDeviceLock);
    sampleRate = sampleRate_;
    channelsIn = inputChannels;
    channelsOut = outputChannels;
    bufferSize = bufferSizeSamples;

    numChannelsIn = channelsIn.countNumberOfSetBits();
    numChannelsOut = channelsOut.countNumberOfSetBits();

    closed = false;

    return String();
}

void SineWaveAudioIODevice::close()
{
    stop();
    closed = true;
}

bool SineWaveAudioIODevice::isOpen()
{
    return ! closed;
}

void SineWaveAudioIODevice::start(AudioIODeviceCallback * callback)
{
    // AudioDeviceManagerのインスタンスに自身のオーディオデバイスを渡すとともに、オーディオスレッドが開始したことを通知する
    deviceManagerCallbackHandler = callback;
    deviceManagerCallbackHandler->audioDeviceAboutToStart(this);

    startTimerHz(30);
}

void SineWaveAudioIODevice::stop()
{
    // AudioDeviceManagerのインスタンスにオーディオスレッドが停止したことを通知する
    if (deviceManagerCallbackHandler != nullptr)
    {
        ScopedLock lock(audioDeviceLock);
        stopTimer();
        deviceManagerCallbackHandler->audioDeviceStopped();
        deviceManagerCallbackHandler = nullptr;
    }
}

bool SineWaveAudioIODevice::isPlaying()
{
    return deviceManagerCallbackHandler != nullptr;
}

String SineWaveAudioIODevice::getLastError()
{
    return String();
}

// このオーディオデバイスのオーディオスレッドループ
void SineWaveAudioIODevice::timerCallback()
{
    AudioBuffer<float> inputBuffer(numChannelsIn, bufferSize);
    AudioBuffer<float> outputBuffer(numChannelsOut, bufferSize);

    // audioDeviceIOCallback()に渡すSine波のサンプルデータを生成する
    for (int ch_index = 0; ch_index < inputBuffer.getNumChannels(); ++ch_index)
    {
        auto ch_data = inputBuffer.getWritePointer(ch_index);
        for (int sample = 0; sample < inputBuffer.getNumSamples(); ++sample)
        {
            ch_data[sample] = sinf(juce::MathConstants<float>::twoPi * sample / inputBuffer.getNumSamples()) * 0.5f;
        }
    }

    // AudioDeviceManagerを介してオーディオバッファの更新を実行する
    deviceManagerCallbackHandler->audioDeviceIOCallback(
        inputBuffer.getArrayOfReadPointers(), inputBuffer.getNumChannels(),
        outputBuffer.getArrayOfWritePointers(), outputBuffer.getNumChannels(),
        bufferSize
        );
}

//==============================================================================
StringArray SineWaveAudioIODeviceType::getDeviceNames(bool wantInputNames) const
{
    if(wantInputNames)
        return inputDeviceNames;
    else
        return outputDeviceNames;
}

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

int SineWaveAudioIODeviceType::getIndexOfDevice(AudioIODevice * device, bool asInput) const
{
    if (asInput) {
        switch (device->getActiveInputChannels().getHighestBit())
        {
        case 0:
        case 1:
            return 0;
            break;
        case 2:
        case 3:
            return 1;
            break;
        default:
            return -1;
            break;
        }
    } else {
        switch (device->getActiveOutputChannels().getHighestBit())
        {
        case 0:
        case 1:
            return 0;
            break;
        case 2:
        case 3:
            return 1;
            break;
        default:
            return -1;
            break;
        }
    }
}

// SineWaveAudioIODeviceのインスタンスを生成するメンバ関数
AudioIODevice * SineWaveAudioIODeviceType::createDevice(const String & outputDeviceName, const String & inputDeviceName)
{
    std::unique_ptr<SineWaveAudioIODevice> device;
    device.reset(new SineWaveAudioIODevice(outputDeviceName, getTypeName(), outputDeviceName, inputDeviceName));
    return device.release();
}

■ MainComponent.cpp

#include "MainComponent.h"
#include "SineWaveAudioIODevice.h"

MainComponent::MainComponent()
{
    // AudioDeviceManagerに独自のオーディオデバイスを追加登録する
    deviceManager.addAudioDeviceType(new SineWaveAudioIODeviceType());
    setSize(800, 600);
}

サンプル実装例を組み込んだアプリケーションの動作例

前述の通りSineWave Audio Deviceの出力バッファに値を渡してもスピーカーから音が聞こえてくることはないため、実際の信号を耳で確認することはできません。ここでは、オーディオビジュアライザを利用して視覚的に信号を確認することにしました。