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

Pocket
reddit にシェア

前回前々回に引き続き、JUCEからASIOドライバ対応のアプリケーションを作る手順を紹介します。本記事では、Projucerの”Audio Application”テンプレートから作る際の手順を紹介していきます。JUCEライブラリが、どのようにしてオーディオデバイスと連携し、音声入出力を行っているのかについても触れていきますので、上級者向けの内容となっております。

■本記事の概要

1. ASIO SDKをダウンロードする
2. ASIO SDKのzipファイルを解凍する
3. Projucerで “Audio Application”のテンプレートから新規プロジェクトを作成する
4. [Modules] → [juce_audio_devices] → [JUCE_ASIO]を “Enabled”にする
5. [Exporters] → [Header Search paths]にASIO SDKのパスを入力する
6. プロジェクトを保存し、IDE (Visual Studio)でプロジェクトを開く
7. コーディングの前に:プロジェクトの初期構成について
8. コーディングの前に:”AudioDeviceManager”クラスについて
9. コーディングの前に:”AudioDeviceSelectorComponent”クラスについて
10. コーディング:”AudioDeviceSelectorComponent”を表示する
11. コーディング:サイン波を出力する処理を実装する
12. ビルドしたアプリを起動し、オーディオ設定パネルからASIOを有効にする
補足説明:AudioAppComponentに実装された仕組みについて

1.ASIO SDKをダウンロードする

Steinbergの開発者向けページからダウンロードすることができます。
URL:https://www.steinberg.net/en/company/developers.html

 

 

2.zipファイルを解凍する

ASIO SDKをダウンロードしたら、zipファイルを解凍しておきましょう。
本記事では、”C:/SDKs/Steinberg SDK/ASIOSDK2.3″に解凍しておきます。
ASIO SDKにはソースコード、サンプルプロジェクト、ドキュメントが含まれています。

 

 

3.Projucerで新規プロジェクトを作成する

本記事では、Projucerの”Audio Application”テンプレートを元に作成したアプリケーションをASIOに対応させていきます。
“Audio Application”テンプレートを利用することで、オーディオデバイスからの音声入力・出力やMIDI入出力を行うためのコンポーネント群に簡単にアクセスすることができるようになります。
※他のテンプレートを利用した場合でもオーディオデバイスとの連携を実装することもできますが、特定のコンポーネント群を追加、継承する必要があるので、JUCEライブラリに慣れてから挑戦することをお勧めします。

 

 

4.JUCEモジュール設定でASIO対応を有効にする

①プロジェクト設定から[Modules]→[juce_audio_devices]を選択します。
②[JUCE_ASIO]を “Enabled”に変更します。

 

 

5.エクスポート設定でASIO SDKのヘッダーファイルパスを追加する

①[Exporters]から、ビルドを実行するIDEのビルド設定アイテムを選択します。
②[Header Search Paths]欄に、上記(2)の作業で配置したASIO SDKのソースコードへのパスを入力します。
本記事では “C:/SDKs/Steinberg SDK/ASIOSDK2.3/common” を入力しました。
※画像ではReleaseビルドの設定項目にパスを追加していますが、DebugビルドでもASIO対応させたい場合は、同様にDebugビルドの設定項目にパスを追加してください。

 

 

6.プロジェクトを保存したら、IDEでプロジェクトを開く

Projucerでの作業が完了したら、プロジェクトを保存し、IDE(Visual Studio)でプロジェクトを開きます。
“AppConfig.h”内の”JUCE_ASIO”定義が有効になっていること、プロジェクトからヘッダーファイル”asio.h”が参照できていることを確認しましょう。

 

 

7.コーディングの前に:プロジェクトの初期構成について

コーディングを始める前に、”Audio Application”テンプレートについて解説します。
本テンプレートを利用すると、”Main.cpp”と”MainComponent.cpp”のソースファイルが自動で作成されます。

◆ “Main.cpp ”
– クラス xxxApplication – 継承元 JUCEApplication (xxxはプロジェクト名)
アプリケーションのエントリー部となっており、JUCEライブラリにおけるmain()関数を持つ。このクラスがアプリケーションの初期化と終了を担当する。
また、内部にはMainWindowクラスが定義されており、新規作成の時点で既に、initialise関数内でMainWindowのインスタンスを生成する処理が実装されている。

class xxxApplication : public JUCEApplication
{
public:
  xxxApplication() {}
~~~中略~~~
void initialise (const String& commandLine) override
{
   mainWindow = new MainWindow (getApplicationName());
}
~~~中略~~~
  • クラス  MainWindow – 継承元 DocumentWindow
    アプリケーションのウインドウ(外枠)を描画するDocuementWindowコンポーネントを継承している。このウインドウ内に各種コンポーネントを配置することで、GUIを構築する。
    また、新規作成の時点で既にMainContentComponentクラスのインスタンスを生成し、ウインドウ内の主要な部品として配置する処理が実装されている。
    ※setContentOwned (createMainContentComponent(), true);がその処理に当たる。
class MainWindow : public DocumentWindow
{
public:
    MainWindow (String name) : DocumentWindow (~~~)
    {
        setUsingNativeTitleBar (true);
        setContentOwned (createMainContentComponent(), true);
        setResizable (true, true);

        centreWithSize (getWidth(), getHeight());
        setVisible (true);
    }
~~~中略~~~

◆ "MainComponent.cpp"
– クラス MainContentComponent – 継承元 AudioAppComponent
ウインドウ内に配置されるコンテンツの基礎となります。GUI部品の機能とオーディオデバイスと連携するインターフェースが定義されたAudioAppComponentクラスを継承しており、新規作成時点で既に、コンストラクタ内において大きさを決定する関数(初期=800pixel × 600pixel)と、オーディオ入出力のチャンネル数を設定する関数(初期=2入力:2出力)が実装されています。

<pre>
class MainContentComponent : public AudioAppComponent
{
public:
MainContentComponent()
{
setSize (800, 600);
setAudioChannels (2, 2);
}
~~~中略~~~
}

 
 

8.コーディングの前に:”AudioDeviceManager”クラスについて

MainContentComponentの継承元であるAudioAppComponent 内において、AudioDeviceManagerクラスのインスタンスdeviceManagerが予め定義されています。このAudioDeviceManagerクラスが、アプリケーションとオーディオデバイスとの橋渡し役として機能してくれます。
具体的には、AudioDeviceManagerクラスを介してオーディオデバイス・MIDIデバイスからのコールバックを受けて音声処理やMIDIデータ処理の関数を実行し、その結果をオーディオデバイス・MIDIデバイスに返します。詳細については、公式ドキュメントからも確認することができます。
また、このAudioDeviceManagerクラスは、オーディオデバイス・MIDIデバイスを設定する役割も担っています。つまり、AudioDeviceManagerクラスが持つオーディオドライバの設定項目をASIOに変更することによって、アプリケーションからASIOを介してオーディオデータをデバイスと送受信することができるようになります。

class JUCE_API AudioAppComponent : public Component, public AudioSource
{
public:
    AudioAppComponent();
    ~AudioAppComponent();
~~~中略~~~
  virtual void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) = 0;
  void shutdownAudio();

  AudioDeviceManager deviceManager;
~~~中略~~~

 

 

9.コーディングの前に:”AudioDeviceSelectorComponent”について

(8)で述べた通り、JUCEライブラリではAudioDeviceManagerクラスを介して、オーディオデバイスとのオーディオデータ・MIDIデータを送受信する仕組みとなっています。また、AudioDeviceManagerクラス自身も様々な設定項目を持っており、使用可能なオーディオドライバの一覧や、送受信可能なMIDIデバイスの一覧など様々な情報を扱います。そのため、各項目の状態を取得、操作するための処理や、そのためのGUIを毎回実装するのはかなりの手間となります。
そこで活躍するのが、”AudioDeviceSelectorComponent”クラスです。このクラスは、AudioDeviceManagerクラスを操作するために特化したGUIコンポーネントで、各種項目を取得、操作するための関数群やGUI部品が予め実装されており、このクラスを介してオーディオデバイス・MIDIデバイスの設定を行うことができます。

 

 

 

10.コーディング:”AudioDeviceSelectorComponent”を表示する

MainContentComponentクラスに、AudioDeviceSelectorComponent(以下、セレクター)を表示する関数”showDeviceSetting()”を追加実装します。
また、”showDeviceSetting()”の実行を、MainContentComponentクラスのコンストラクタ内に記述することで、アプリケーションの起動時にオーディオデバイス設定画面が立ち上がるようにしておきます。
“showDeviceSetting()”関数では、ダイアログウインドウが生成され、ウインドウ内にセレクターを配置する処理を実装します。この時、セレクターにはMainContentComponentクラスが持つAudioDeviceManager(以下、マネージャー)の参照を渡しておきます。参照を渡すことで、セレクターからマネージャーの持つ各関数を実行することができるようになります。

class MainContentComponent : public AudioAppComponent
{
public:
    MainContentComponent()
    {
        setSize (800, 600);
        setAudioChannels (2, 2);
        showDeviceSetting();
    }
~~~中略~~~
private:
    void showDeviceSetting() 
    {
        AudioDeviceSelectorComponent selector(deviceManager, 
                                              0, 256, 
                                              0, 256, 
                                              true, true, 
                                              true, false);

        selector.setSize(400, 600);

        DialogWindow::LaunchOptions dialog;
        dialog.content.setNonOwned(&selector);
        dialog.dialogTitle = "Audio/MIDI Device Settings";
        dialog.componentToCentreAround = this;
        dialog.dialogBackgroundColour = getLookAndFeel().findColour(ResizableWindow::backgroundColourId);
        dialog.escapeKeyTriggersCloseButton = true;
        dialog.useNativeTitleBar = false;
        dialog.resizable = false;
        dialog.useBottomRightCornerResizer = false;

        dialog.runModal();
    }
~~~中略~~~
}

 

11.コーディング:サイン波を出力する処理

目的のオーディオデバイスにオーディオデータが送られているかを確認するため、サイン波を出力する処理を実装してみましょう。以下のコードでは、全てのチャンネルに対して、毎回のオーディオバッファに1周分のサイン波を出力する処理が実行されます。
実際にスピーカーから流れる周波数は、オーディオバッファやサンプリングレートの設定値によって異なります。

class MainContentComponent : public AudioAppComponent 
{
~~~中略~~~
    void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
    {
        bufferToFill.clearActiveBufferRegion();

        #define M_PI 3.14159265358979323846
        const float level = 0.5f;  //音量はこの値で変更する

        auto buffer = bufferToFill.buffer;
        for (int channel = 0; channel < buffer->getNumChannels(); ++channel)
        {
            float* channelData = buffer->getWritePointer(channel);

            for (int sample = 0; sample < buffer->getNumSamples(); ++sample)
            {
                channelData[sample] = sinf(M_PI * 2 * sample / buffer->getNumSamples() * 2) * level;
            }
        }
    }
~~~中略~~~

 

12.プロジェクトをビルドして動作確認

▼ビルドしたアプリを起動する
アプリケーションを起動すると、MainContentComponentのコンストラクタ内に記述された”showDeviceSetting()”関数が実行されるため、ダイアログウインドウが表示されます。
また、初期状態でオーディオデバイスの出力チャンネルが有効になっている場合は、オーディオデバイスからサイン波が鳴ります。

 

▼オーディオドライバをASIOに切り替える
[Audio device type]のコンボボックスから”ASIO”を選択します。
[Device]のコンボボックス、[Control Panel]ボタン、[Reset Device]ボタンが追加されます。
[Device]のコンボボックスにASIO対応のデバイス一覧が表示されるので、使用するオーディオデバイスを選択します。
[Control Panel]ボタンをクリックすると、ASIOデバイス毎に設けられたコントロールパネルが開きます。

 

▼出力チャンネル数を変更してみる
以下のように、[Active output channels]を追加して、複数のチャンネルからサイン波を出力していることが確認できます。

 

▼ダイアログウインドウを閉じるとメインウインドウが開く
本記事紹介の実例では、ダイアログウインドウはモーダルウインドウとして実行しているため、ダイアログウインドウを閉じてからメインウインドウが表示される挙動となります。
また、説明を簡単にするため、ダイアログウインドウを表示する関数をコンストラクタ内で実行する実装としていましたが、決してこのような実装方針には限られません。メインウインドウ内に配置したボタンを押すことでダイアログウインドウを表示するという実装も可能ですし、ウインドウメニューからダイアログウインドウを表示するという実装も可能です。

 

補足説明:AudioAppComponentに実装された仕組みについて

本記事では、Projucerの”AudioApplication”テンプレートから自動生成されたMainContentComponentクラスに機能を追加することで、ASIO対応のアプリケーションを作成しました。ここでは、MainContentComponentクラスに関する解説をしていくことで、JUCEライブラリが提供する仕組みについてより深く理解することを目指します。
まず、(7)にある通り、MainContentComponentクラスは、AudioAppComponentクラスを継承してます。このAudioAppComponentクラスは、ComponentクラスとAudioSourceクラスを継承したクラスです。

  • クラス AudioAppComponent – 継承元 Component, AudioSource
class JUCE_API AudioAppComponent : public Component, public AudioSource
{
public:
    AudioAppComponent();
    ~AudioAppComponent();
~~~中略~~~
    virtual void prepareToPlay (int samplesPerBlockExpected, double sampleRate) = 0;
    virtual void releaseResources() = 0;
    virtual void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) = 0;
~~~中略~~~

この点について詳しく説明すると、先ず、ComponentクラスはGUI部品の機能を持つクラスです。GUIの部品として配置、描画、操作させられるため関数群を持っており、また、このクラスを継承することで、MainWindowクラス内に配置することができるようになります。

次に、AudioSourceクラスはオーディオデバイスに送るオーディオデータを生成する機能(音源の機能)を実装することができるクラスです。AudioSourceクラス自身は純粋仮想関数を持つインターフェースであり、継承先のクラスで実装を記述することで音源の機能としての役割を果たすことができます。
このAudioSourceクラスで定義された純粋仮想関数 getNextAudioBlock 他は、継承先のAudioAppComponentクラスにおいても実装がされていないため、更なる継承先のMainContentComponentクラスに実装コードを記述することになります。
また、AudioAppComponentの実装コードである”juce_AudioAppComponent.cpp”を見ると、以下の実装が既に行われていることが確認できます。

"juce_AudioAppComponent.cpp"

~~~中略~~~
void AudioAppComponent::setAudioChannels (int numInputChannels, int numOutputChannels, const XmlElement* const xml)
{
    String audioError = deviceManager.initialise (numInputChannels, numOutputChannels, xml, true);
    jassert (audioError.isEmpty());

    deviceManager.addAudioCallback (&audioSourcePlayer);
    audioSourcePlayer.setSource (this);
}

void AudioAppComponent::shutdownAudio()
{
    audioSourcePlayer.setSource (nullptr);
    deviceManager.removeAudioCallback (&audioSourcePlayer);
    deviceManager.closeAudioDevice();
}
~~~中略~~~
AudioDeviceManager deviceManager;
AudioSourcePlayer audioSourcePlayer;

 

上記実装のうち、setAudioChannels関数内に注目すると、AudioDeviceManagerクラスのインスタンスであるdeviceManagerに対して、AudioSourcePlayerクラスのインスタンスであるaudioSourcePlayerを引数としてAudioDeviceManager::addAudioCallback 関数を実行します。このaddAudioCallback (AudioIODeviceCallback *newCallback)関数は、AudioIODeviceCallback クラスを継承したオブジェクトのポインタを引数に渡すことができます。AudioSourcePlayerクラスはAudioIODeviceCallbackを継承しているため、引数として渡すことができるのです。

続いて、AudioSourcePlayerクラスのインスタンスであるaudioSourcePlayerに対してAudioAppComponent自身”this”を引数として、AudioSourcePlayer::setSource 関数を実行しています。このsetSource (AudioSource *newSource)関数は、AudioSourceクラスを継承したオブジェクトのポインタを引数に渡すことができます。AudioAppComponentクラスはAudioSourceクラスを継承しているため、自身”this”を引数として渡すことができるのです。

公式ドキュメント:AudioDeviceManager
公式ドキュメント:AudioSourcePlayer
公式ドキュメント:AudioSource

さて、ここでAudioSourcePlayerの実装を覗いてみます。注目すべきは、”audioDeviceIOCallback”関数です。この関数は、AudioDeviceManagerからのコールバック関数として用意されています。つまり、オーディオデバイスの都合に合わせて、このコールバック関数が実行させられます。
また更に、このコールバック関数内には、AudioSourcePlayerに登録されたAudioSourceクラスのオブジェクトに対して”getNextAudioBlock”関数を実行するという、コールバック処理も実装されています。

"juce_AudioSourcePlayer.cpp"

void AudioSourcePlayer::audioDeviceIOCallback (const float** inputChannelData,                                               
                                               int totalNumInputChannels,                                               
                                               float** outputChannelData,                                               
                                               int totalNumOutputChannels,                                               
                                               int numSamples)
{ 
        ~~~中略~~~        
        AudioSampleBuffer buffer (channels, numActiveChans, numSamples);
        AudioSourceChannelInfo info (&buffer, 0, numSamples);        
        
        source->getNextAudioBlock (info);     //AudioSource* source
        ~~~中略~~~
}

これらの実装によって、以下の一連の処理が実現されます。
1. AudioDeviceManagerからAudioSourcePlayer::audioDeviceIOCallbackが実行される
2. “audioDeviceIOCallback”内でAudioAppComponent::getNextAudioBlock が実行される
3. AudioAppComponent::getNextAudioBlock に実装した音声信号処理が実行される