C++
audio
MIDI
JUCE
ableton
JUCEDay 25

JUCEにAbleton Linkを組み合わせてみた

本記事はJUCE Advent Calendar 2017 の12月25日向けに投稿した記事です。

これを作りました

JUCEにAbleton Linkを組み込むことが出来ました。
Ableton Linkに対応したアプリであれば、互いにテンポを同期させることができます。

ソースコード

https://github.com/COx2/juce_meets_link

Prebuiltディレクトリにビルド済みのバイナリも同梱しています。

A. Ableton Linkとは?

Abletonの公式サイトの説明を引用すると、

Linkは、ローカル・ネットワーク経由でデバイス間のタイミングを合わせるテクノロジー。面倒な設定からユーザーを解放し、演奏に集中させます。LinkはLiveの一部となり、その他の音楽アプリケーションの内蔵機能としても提供されます。

同じネットワークに接続するだけで、Link対応のソフトウェアを実行する複数のデバイス間でジャム演奏。他のパートの演奏中に参加したり、途中で抜けたりできます。また、誰かがテンポを調整すると、その他のメンバーはその変更に従います。MIDIケーブルも設定も必要なく、スムーズな同期が可能です。

とあるように、同一のLAN内に接続されたLink対応アプリケーション間でテンポを同期させることができる仕組みです。
また、MIDIのようにマスター・スレーブの関係ではなく、互いにテンポを変更可能なので、まさにジャムセッションと呼べるプレイアビリティを実現することができます。

音楽作成にはさまざまなインストゥルメントが使用されるもの。だからLinkは、幅広いデバイスを使用した演奏にも対応します。Link機能を内蔵した音楽アプリケーションはますます増加中です。これらのアプリを使用すれば、同一ネットワーク上の誰でもLiveに合わせて演奏できます。LinkはLiveを使用しないセットアップでも使用できます。Link対応ソフトウェアは、複数のデバイス間、または同一デバイス上の複数のアプリケーション間で同期プレイできます。

上記の通り、Ableton Liveをはじめとした多くのオーディオアプリケーションに採用されます。
また、Ableton Link SDK自体もC++の汎用的な機構で構成されているため、オーディオ用途に限られない様々な用途のアプリケーションに組み込むことができます。

A-1. Ableton Link SDK

https://github.com/Ableton/link

Windows/macOS/Linuxのクロスプラットフォームに対応したライブラリがGitHubで公開されています。
ライセンスはGPLv2+と商用ライセンスのデュアルライセンスとなっています。
商用ライセンスで使用する場合はAbletonのサポートに問い合わせる必要があるようです。
また、iOSアプリに組み込む場合は、ReadMeで書かれているように、このSDKとは別のLinkKit SDK for iOSを使う必要があるようです。

iOS developers should not use this repo. See http://ableton.github.io/linkkit for information on the LinkKit SDK for iOS.

A-2. 必要な開発環境

上記リポジトリのReadMeから引用すると、各プラットフォームで以下のツール・コンパイラが必要になります。
C++11必須であったり、CMakeを採用するなど、モダンな開発思想であることが伺えます。
なお、JUCEにLinkを組み込む場合はCMakeの作業をProjucerで置き換えます。
本記事の範囲においてCMakeは不要です。

Platform Minimum Required Optional (only required for examples)
All CMake 3.0 Qt 5.5
Windows MSVC 2013 Steinberg ASIO SDK 2.3
Mac Xcode 7.0
Linux Clang 3.6 or GCC 5.2 libportaudio19-dev

Other compilers with good C++11 support should work, but are not verified.

A-3. サンプルアプリのビルド

上記リポジトリのReadMeにビルド手順が記載されています。
Ableton Link SDK付属のサンプルのビルドについては本記事では割愛させていただきます。
筆者が試したところ、いくつか躓く箇所もあったので、後日記事にしてみようかと思います。
ちなみにQLinkHutというサンプルを実行した例がこちらになります。


B. 実際にビルドしてみる

それでは、JUCEにAbleton Linkを組み込んだこちらのリポジトリを実際にビルドしてみましょう。
Gitによる作業をいくつか行います。その作業をGitコマンドで説明しますが、SourceTreeなどのGUIクライアントでも同じ作業をしてもらえれば大丈夫です。

B-1. リポジトリをクローンする

以下のGitコマンドからソースコードをクローンします。

$ git clone https://github.com/COx2/juce_meets_link

B-2. サブモジュール(JUCE, Link SDK)をクローンする

当リポジトリのDependenciesディレクトリにJUCEとLink SDKを、Gitサブモジュールとして置いています。
リポジトリのディレクトリに移動して、サブモジュールをクローンします。

$ cd juce_meets_link
$ git submodule update --init

> Submodule 'Dependencies/JUCE' (https://github.com/WeAreROLI/JUCE.git) registered for path 'Dependencies/JUCE'
> Submodule 'Dependencies/link' (https://github.com/Ableton/link.git) registered for path 'Dependencies/link'
...
> Submodule path 'Dependencies/JUCE': checked out '9f6126779c63cbe2e07719d9453c0d506b562a25'
> Submodule path 'Dependencies/link': checked out '3c7722bc116b812afeed9a0193a245b2992c06cd'

B-3. Link SDK内のサブモジュールをクローンする

Link SDKにもGitサブモジュールが置かれています。
Link SDKのディレクトリに移動して、サブモジュールのクローンを実行してください。
asio-standaloneというライブラリ(Asio C++ Library) がLink SDKのリポジトリにクローンされます。

$ cd Dependencies/link

$ git submodule update --init
> Submodule 'modules/asio-standalone' (https://github.com/chriskohlhoff/asio.git) registered for path 'modules/asio-standalone'
...
> Submodule path 'modules/asio-standalone': checked out '722f7e2be05a51c69644662ec514d6149b2b7ef8'

これでソースコードが揃いました。

B-4. jucerファイルを開く

当リポジトリ内のjucerファイルjuce_meets_link/JuceLink/JuceLink.jucerをProjucerで開きます。
JuceLink.PNG

B-5. ビルド

Projucerで保存を実行すると、各種IDE用のプロジェクトファイルが生成されます。
生成されたプロジェクトファイルを開き、IDEおよびmakeコマンドからビルドを実行します。
ビルドが成功したら、アプリケーションを実行してみましょう。

JuceLink_built.PNG

C. Link SDKの解説

ソースコードを例に挙げながら、Link SDKをアプリケーションに組み込む手順について解説します(筆者が理解している範囲で)。
なお、正確な情報はLink SDK内のドキュメントにありますので、疑問が生じた場合はReadMeを確認するようにしましょう。

C-1. コンパイル設定

実装コードを確認する前に、コンパイルに必要なフラグやプリプロセッサについて解説します。
Link SDKはクロスプラットフォームな設計であるが故に、ビルド環境毎に必要なフラグが設けられています。
JUCEフレームワークでは、Projucerにプリプロセッサ定義やコンパイルフラグを入力しておくことで、IDE用のプロジェクトファイルにその値を渡すことができます。

JuceLinkFlag.PNG

プリプロセッサ定義

ビルド環境のOSに合わせてプリプロセッサ定義を追加します。

  • Windows: LINK_PLATFORM_WINDOWS=1
  • macOS: LINK_PLATFORM_MACOSX=1
  • Linux: LINK_PLATFORM_LINUX=1

インクルードパス

Header Search PathsにLink SDK内の以下のディレクトリを追加します。

  • ../link/include
  • ../link/modules/asio-standalone/asio/include

コンパイルフラグ

Link SDKをビルドする際に大量のWarningが発生するので、それを抑えるフラグなどを追加します。

/D_SCL_SECURE_NO_WARNINGS /DLINK_BUILD_VLD=0 /MP /EHsc /wd4061 /wd4265 /wd4350 /wd4355 /wd4365 /wd4371 /wd4503 /wd4510 /wd4512 /wd4514 /wd4571 /wd4610 /wd4625 /wd4626 /wd4628 /wd4640 /wd4710 /wd4711 /wd4738 /wd4820 /wd4464 /wd4548 /wd4623 /wd4868 /wd5026 /wd5027 /wd4987 /wd4774 /wd5039 /wd4917

C-2. 実装コード

Link SDKを組み込む際に必要なコードについて紹介します。

ヘッダーインクルード

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

// include link library.
#include "ableton/Link.hpp"

変数定義

Linkのインスタンスや、Linkの接続状態、同期しているテンポ(BPM)などを保持する変数を定義します。

class MainContentComponent   : public AudioAppComponent,
{
~中略~
private:
    ScopedPointer<ableton::Link> link;  // Linkオブジェクトを保持するポインタ 
    double tempo = 120.0;               // 現在のテンポ(BPM)を保持する変数
    std::size_t numPeers = 0;           // 現在の接続アプリ数
    std::size_t quantum = 4;            // 現在の拍子の分母...(quantumの値)拍子。quantum = 4なら4拍子
}

Linkオブジェクトの生成と初期設定

以下にLinkオブジェクトの生成と初期化の例を示します。
Linkオブジェクトをnewする際に引数としてテンポ(BPM)の初期値を渡します。
また、以下のコールバック関数にコールバック時の処理を記述しておきます。

  • Link::setNumPeersCallback ...他のLinkアプリと接続したときに実行されるコールバック関数
  • Link::setTempoCallback...他のLinkアプリから同期テンポの変更を受けたときに実行されるコールバック関数
class MainContentComponent   : public AudioAppComponent
{
public:
    //コンストラクタ
    MainContentComponent()
    {

   ~中略~

        link = new ableton::Link(tempo);    // Linkオブジェクトの生成. 引数にテンポ(BPM)の初期値を渡す

        link->enable(false);                // 他のLinkアプリとの同期を無効にする

        // 他のLinkアプリと接続したときに実行されるコールバック関数
        link->setNumPeersCallback([this](std::size_t p) {
           numPeers = p;               // 現在の接続アプリ数を保持する変数に代入
        });

        // 他のLinkアプリから同期テンポの変更を受けたときに実行されるコールバック関数
        link->setTempoCallback([this](const double bpm) {
            tempo = bpm;                 // 現在のテンポ(BPM)を保持する変数に代入
        });

    ~中略~

    }

}

他のLinkアプリとの同期を有効にする

Linkオブジェクトは、インスタンスが生成された時点から独自のスレッドが生成されるとともに、その内部ではクロックカウンタが常に回っています。
ableton::Link::enable(const bool bEnable)関数に引数trueを渡して実行することで、当該オブジェクトが実行することで、同一LAN内の他のLinkアプリと自動で接続を始めます。

// ableton::Link::enable(const bool bEnable)
link->enable(true);   

Linkオブジェクト(スレッド)の情報を取得する

Linkオブジェクトから同期テンポや拍の値を取得するには、最初にableton::Link::captureAppTimeline()
を実行して、現在のタイムライン情報オブジェクトを取得します。
このタイムライン情報オブジェクトに対して、現在の時間と拍子の情報を渡すことで、1小節における拍数(Phase)や、合計の拍数(Beats)を取得することができます。

また、現在接続しているアプリの数を取得するには、ableton::Link::numPeers()関数から取得することができます。

// Linkオブジェクトからタイムライン情報オブジェクトを取得
auto timeline = link->captureAppTimeline();

// 現在の時間情報を取得
const auto time = link->clock().micros();

// Linkオブジェクト(スレッド)内でのテンポ情報を取得
auto tempo = timeline.tempo();

// タイムライン情報オブジェクトから合計の拍数を取得
const auto beats = timeline.beatAtTime(time, quantum);

// タイムライン情報オブジェクトから1小節における現在の拍数を取得
const auto phase = timeline.phaseAtTime(time, quantum);

// 現在接続しているアプリの数を取得
auto numpeers = link->numPeers();

他のアプリのテンポを変更する

自身のアプリで実行しているLinkオブジェクトに対してテンポ変更を要求することで、接続している他のLink対応アプリのテンポを変更することができます。
テンポ変更の要求は、以下の手続きを経て行います。

  1. Linkオブジェクトからタイムライン情報オブジェクトを取得 ...ableton::Link::captureAppTimeline()
  2. 現在の時間情報を取得 ...ableton::Link::clock()::micros()
  3. タイムライン情報オブジェクトのテンポ値を変更する ...ableton::Link::Timeline::setTempo()
  4. Linkオブジェクトに変更したタイムライン情報を投げる ...ableton::Link::commitAudioTimeline()
// Linkオブジェクトからタイムライン情報オブジェクトを取得
auto timeline = link->captureAppTimeline();

// 現在の時間情報を取得
const auto time = link->clock().micros();

// タイムライン情報オブジェクトに対してテンポ値を変更する
timeline.setTempo(newTempo, time);

// Linkオブジェクト(スレッド)に変更したタイムライン情報を投げる
link->commitAudioTimeline(timeline);

他のアプリにビート(現在の拍数)の変更を渡す

自身のアプリで実行しているLinkオブジェクトに対してテンポ変更を要求することで、接続している他のLink対応アプリのテンポを変更することができます。
ビート変更の処理は、タイムライン情報オブジェクトからableton::Link::Timeline::requestBeatAtTime()関数もしくは
ableton::Link::Timeline::forceBeatAtTime()を実行することで、引数に渡した時刻を基準時間として目的のビート値に同期する処理が適用されます。requestBeatAtTime()は他のアプリのタイムライン情報との調整を行うのに対して、forceBeatAtTime()は他のアプリのタイムライン情報に関わらず同期情報を設定します。

  1. Linkオブジェクトからタイムライン情報オブジェクトを取得 ...ableton::Link::captureAppTimeline()
  2. 現在の時間情報を取得 ...ableton::Link::clock()::micros()
  3. タイムライン情報オブジェクトのビート変更を実行 ...ableton::Link::Timeline::requestBeatAtTime()またはableton::Link::Timeline::forceBeatAtTime()
  4. Linkオブジェクトに変更したタイムライン情報を投げる ...ableton::Link::commitAudioTimeline()

ableton::Link::Timeline::requestBeatAtTime()の場合

// Linkオブジェクトからタイムライン情報オブジェクトを取得
auto timeline = link->captureAppTimeline();

// 現在の時間情報を取得
const auto time = link->clock().micros();

// タイムライン情報オブジェクトに対してテンポ値を変更する
// 引数に渡した時間を基準に、1小節4拍のビートとして、ビート値が0.0となるようにタイムライン情報を変更する
timeline.requestBeatAtTime(0.0, time, 4);

// Linkオブジェクト(スレッド)に変更したタイムライン情報を投げる
link->commitAudioTimeline(timeline);

ableton::Link::Timeline::requestBeatAtTime()の場合

// Linkオブジェクトからタイムライン情報オブジェクトを取得
auto timeline = link->captureAppTimeline();

// 現在の時間情報を取得
const auto time = link->clock().micros();

// タイムライン情報オブジェクトに対してテンポ値を変更する
// 引数に渡した時間から、1小節4拍のビートとして、ビート値が0.0となるようにリクエストを送る
timeline.forceBeatAtTime(0.0, time, 4);

// Linkオブジェクト(スレッド)に変更したタイムライン情報を投げる
link->commitAudioTimeline(timeline);