本記事は JUCE Advent Calendar 2017 の12月11日向けに投稿した記事です。
前日の記事にて、Lightpad Blockで電光掲示板を作った話について紹介しました。
本日はそれの続きとして、Lightpad BlockとJUCEライブラリと連携することで、Lightpad Blockで実行するプログラム(Littlefoot言語)を動的に変更した事例についても紹介します。
作ったもの
アドカレ用のネタの実験中。Ligthpad Blockに転送するLittlefootのソースコードをホストアプリ内でメタプログラミングで生成することで、電光掲示板の文字をアプリで変更してみる。#JUCE #ROLI #BLOCKS pic.twitter.com/7BmKdjEe1W
— COx2 ))))@ (@CO_CO_) 2017年11月19日
ROLI Blocksについて
英国ROLI社が開発・販売しているBlocksシリーズは、プログラマブルな楽器インターフェースです。
製品サイト: https://www.mi7.co.jp/products/roli/blocks/
- 5Dタッチセンシティブ・コントロール・サーフェス
- 15 x 15 LEDマトリクス
- 94 mm x 94 mm x 20 mm / 250g
- DNAコネクター8基搭載
- 充電式バッテリー内蔵
- USB-C端子(MIDI出力/電源供給)
- USBおよびBluetoothによるMIDI互換(DAWでも使用可能)
1. 新規プロジェクト作成
本記事では、Projucerの"GUI Application"テンプレートから新規プロジェクトを作成します。
2. juce::Blocksモジュールを追加する
Blocksと連携するAPIを利用するには、juce::Blocksモジュールを追加する必要があります。
Projucerのプロジェクト設定画面において、[Modules]の設定ボタン(歯車のアイコン)をクリックし、
[Add a module] → [Global JUCE modules path] → [juce_blocks_basics] をクリックしてモジュールを追加します。
3. GUIを作成する
ProjucerのGUIエディタを利用してGUIを定義するクラスを作成しました。
GUIエディタの使い方については、こちらの記事をご参考ください。
今回は以下のコンポーネントを配置しました。
- 文字列を入力するテキストエディタ―コンポーネント
- BlocksにLittlefootプログラムを送信するトリガーボタン
- Bluetooth接続ダイアログを表示するボタン(iOS/Android用)
- Blocksのステータスを表示するラベル
アプリケーションで表示した場合
4. Blocksと接続するクラスを継承する
TopologySource::Listenerクラスを継承することでBlocksからのコールバックを受け取ることが出来ます。
今回はGUIコンポーネントのクラスにTopologySource::Listenerクラスを継承しました。
内部変数としてBlocksの接続状態を監視するPhysicalTopologySourceクラスのインスタンスを追加します。
"SignBoardManager.h"
class SignBoardManager : public Component,
public TopologySource::Listener,
public Button::Listener
{
public:
//==============================================================================
SignBoardManager ();
~SignBoardManager();
//==============================================================================
//[UserMethods] -- You can add your own custom methods in this section.
void topologyChanged() override; // TopologySource::Listenerクラスに定義されたインターフェース
// Blocksの接続状態が変更されたことをトリガーとしてコールバック関数として実行される
void setSignBoardToBlocks(String text);
void setSignBoardProgram(Block& block, String text);
void detachActiveBlock();
//[/UserMethods]
~~中略~~
private:
//[UserVariables] -- You can add your own custom variables in this section.
PhysicalTopologySource topologySource; // Blocksの接続状態を監視するインスタンス
Block::Ptr activeBlock; // 現在接続状態にあるBlocksへのポインタ
//[/UserVariables]
~~中略~~
}
また、実装コードでは以下のようにtopologySource変数からコールバックを受け取れるよう、"addListener"関数からリスナーを登録します。
"SignBoardManager.cpp"
SignBoardManager::SignBoardManager ()
{
//[Constructor_pre] You can add your own custom stuff here..
topologySource.addListener (this); // PhysicalTopologySourceクラスからコールバックを受けるようにリスナーを登録
//[/Constructor_pre]
~~中略~~
}
5. BlocksにLittlefootプログラムを送信する処理
先ず、Blocksとの接続が確立したことをトリガーとしてデフォルトのプログラムを送る処理を実装します。
Blocksとの接続が確立したら、Block::setProgram(Program* newProgram)を実行することで、BlocksにLittlefootプログラムを送信します。
"SignBoardManager.cpp"
// Blocksとの接続状態に変化が生じた場合に実行されるコールバック関数
void SignBoardManager::topologyChanged()
{
// すでに接続があった場合にはactiveBlockにnullを代入して、接続解除したものとみなす
if (activeBlock != nullptr)
detachActiveBlock();
auto blocks = topologySource.getCurrentTopology().blocks;
for (auto b : blocks)
{
if (b->getType() == Block::Type::lightPadBlock)
{
activeBlock = b;
// 接続が確立された際にデフォルトのプログラムを送信
setSignBoardProgram (*activeBlock, "Hello Blocks");
// Statusラベルに接続したBlocksの状態を表示する
label->setText("Status: "
+ activeBlock->getDeviceDescription()
+ (String)activeBlock->getBatteryLevel(),
dontSendNotification);
break;
}
}
}
void SignBoardManager::detachActiveBlock()
{
activeBlock = nullptr;
}
void SignBoardManager::setSignBoardToBlocks(String text)
{
if(activeBlock == nullptr)
return;
setSignBoardProgram(*activeBlock, text);
}
void SignBoardManager::setSignBoardProgram(Block& block, String text)
{
// 接続されたBlocksに新しいプログラムを送信する
block.setProgram(new SignBoardProgram(block, text));
}
6. Blocksに送信するプログラムを定義する
Block::Programクラスを継承したクラスは、Blocksにプログラムを送信する関数"Block::setProgram(Program* newProgram)"の引数として渡すことができるようになります。
今回はBlock::Programクラスを継承する"SignBoardProgram"クラスを定義し、"Program::getLittleFootProgram()"関数をオーバーライドしてLittlefootプログラムを返す処理を実装します。
ちなみに、R"littlefoot(~~~)littlefoot"で囲まれたブロックは、C++11から導入された生文字列リテラルです。参考情報
"SignBoardProgram.h"
class SignBoardProgram : public Block::Program
{
public:
SignBoardProgram (Block& b, String t) : Program (b), sendingtext(t){}
String getLittleFootProgram() override
{
sendingtext = sendingtext.toUpperCase();
return // Littlefootプログラム本体(juce::String型の文字列)
R"littlefoot(
#heapsize: 256
//==============================================================================
int newTouchColor;
int touchColor0;
~~中略~~
void repaint()
{
clearDisplay();
drawPressureMap();
fadePressureMap();
updateBar();
if(moveSwitch)
{
basePostionX = updateBasePosition(basePostionX);
}
showCharactor(0x4a, touchColor0, basePostionX+0, basePostionY+0); //J
showCharactor(0x55, touchColor1, basePostionX+5, basePostionY+0); //U
showCharactor(0x43, touchColor2, basePostionX+10,basePostionY+0); //C
showCharactor(0x45, touchColor3, basePostionX+15,basePostionY+0); //E
showCharactor(0x4a, touchColor0, basePostionX+25,basePostionY+0); //J
showCharactor(0x41, touchColor4, basePostionX+30,basePostionY+0); //A
showCharactor(0x50, touchColor1, basePostionX+35,basePostionY+0); //P
showCharactor(0x41, touchColor4, basePostionX+40,basePostionY+0); //A
showCharactor(0x4e, touchColor2, basePostionX+45,basePostionY+0); //N
}
~~中略~~
)littlefoot"
) //String
);
}
~~中略~~
}
7. Littlefootプログラムを動的に変更する方法
"SignBoardProgram"クラスの関数"getLittleFootProgram()"は返りの型がString型であることからも解るように、Littlefoot言語のプログラムはString型の文字列そのものです。
ですので、文字列を変更してしまえば、Littlefootプログラムを動的に変更することが出来ます。
"juce::String"型は operator+ によって二つの文字列を接続することが出来ますので、動的に変更したい箇所の前後をString型で切り離し、変更後のString文字列を operator+ で接続しました。
"SignBoardProgram.h"
String getLittleFootProgram() override
{
sendingtext = sendingtext.toUpperCase();
return String(
String(
R"littlefoot(
#heapsize: 256
//==============================================================================
int newTouchColor;
~~中略~~
if(moveSwitch)
{
basePostionX = updateBasePosition(basePostionX);
}
)littlefoot"
)
+ generateCharacterInstruction(sendingtext) //動的に変更する箇所. juce::String型の文字列として挿入する
+ String(
R"littlefoot(
}
void initialise()
{
~~中略~~
)littlefoot"
) //String
);
}
~~中略~~
private:
String generateCharacterInstruction(String text){
String result = "";
for(int i=0; i<text.length(); i++){
result +=
String("showCharactor("
+ (String)getASCII(sendingtext.getCharPointer()[i])
+ ", touchColor" + (String)(i%5)
+ ", basePostionX + " + (String)(i*5)
+ ", basePostionY + 0);"
);
}
return result;
}
int getASCII(char ch){
if (ch == 'A')
return 0x41;
~~中略~~
}