カテゴリー
JUCE

Projucer build & distribution

JUCEのビルドシステム、Projucerの設定項目例 (Code-Signing: Automatic, Sandbox options: ユーザー選択ファイルのread/writeの例)

Xcode (macOS)

  • Use App Sandbox: Enabled
  • App Sandbox Options
    • File Access:: User Selected File (Read/Write)
    • Network: Outgoing Connections (Client)
  • Development Team ID: Your team ID (10 characters)

Debug and Release

  • Name: Debug or Lelease
  • Binary Name: Your plug-in name
  • Custom Xcode Flags
    • CODE_SIGN_STYLE=”Automatic”
  • Cod-Signing Identity: Apple Development

Plugin FormatsにStandaloneが含まれていないとconsole outされない&AllをビルドしないとUI関連のUpdateが反映されないという現象があります。

Plugin AU Main TypeはMidi Pluginの場合、”kAudioUnitType_MIDIProcessor”を選択、

Plugin CharacteristicsはMidi Pluginの場合、”Plugin MIDI Input”と”Plugin MIDI Output”を選択します。”MIDI Effect Plugin”にチェックを入れるとAbleton Liveではうまく表示されませんでした。

Distributionには公式Tutorialにもあるように Packagesを使います。

参考Youtube

Packagesを立ち上げ、Nextをクリック
Project Nameを記入し、この`Packages Project`のフォルダをどこに作成するか、を指定。Desktopでいいと思います。
このような画面が表示されます。
今回はAUとVST版のインストーラーを作るので、Packagesの下の「+」ボタンを押して追加、名前をそれぞれ「AU」「VST」とします。名前の変更は長押しすると可能になります。
JUCEプロジェクトのbuildファイルはエイリアスなので、Show Originalで元ファイルを探し
Packagesプロジェクトフォルダにコピーします。

インストールディレクトリのPathは Macintosh HD/Library/Audio/Plug-Ins/ で
AUの場合はComponents
VST3の場合はVST3  を指定します。

Packagesには、デフォルトでは上記のディレクトリが表示されていないため、Library下に作成します。ディレクトリを右クリックするとAdd Folderを選択出来ます。

Componentsフォルダを右クリックし、Add Filesで
先ほどPackagesプロジェクトフォルダ内にコピーしたcomponentファイルを選択します。
AU
VST
プロジェクトをSaveし
Buildします。
問題がなければこのように表示されます。

Packagesでインストーラーに問題がある場合は、pkgbuildで手動で作る方法があります。参考

カテゴリー
C++ JUCE

JUCE AudioProcessorValueTreeState

JUCEの初歩tutorialシリーズではあまり取り上げられない内容ですが、Plug-Inの色々なパラメータを管理するためにAudioProcessorValueTreeState(APVTS)はとても重要です。

APVTSを理解するにはまず、APVTSを使用しない場合どのようにパラメータを登録し、取り出して使うかを理解する必要があります。登録するにはAudioProcessorのconstructorでイニシャライズし、使う時はgetParameters( )でパラメータのポインタのリストをゲットします。

#define LOWER_MIDI_BOUND_ID "lowerMidiBound"

{
  addParameter(new juce::AudioParameterInt(juce::ParameterID{LOWER_MIDI_BOUND_ID, 1}, LOWER_MIDI_BOUND, 21, 108, 21)); 

}

auto& parameters = getParameters();
float gain = parameters[0]->getValue();

この方法では数個のパラメータでは管理出来るかも知れませんが、パラメータ数が膨大になると管理が難しくなります。そこでAPVTSの出番です。

APVTSはPluginProcessorのprivate変数として宣言し、constructorでイニシャライズします。

// initializer
AudioProcessorValueTreeState (
  AudioProcessor& processorToConnectTo,
  UndoManager* undoManagerToUse,
  const juce::Identifier& valueTreeType,
  ParameterLayout parameterLayout);
// PluginProcessor.h
class ChordGeniusAudioProcessor: public juce::AudioProcessor 
{
public:

  // 省略

  juce::AudioProcessorValueTreeState& getValueTreeState() { return valueTreeState; }

private:
  juce::AudioProcessorValueTreeState valueTreeState;
}

// PluginProcessor.cpp
// constructor
ChordGeniusAudioProcessor::ChordGeniusAudioProcessor()
  : // 省略
    valueTreeState(*this, nullptr, ProjectInfo::projectName, ParameterHelper::createParameterLayout())
{}

ParameterLayoutを供給するHelperクラスを作っておけば便利です。

#define LOWER_CONTROL_ID "LOWER_CONTROL_ID"

class ParameterHelper
{
public:
  ParameterHelper() = delete;

  static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout()
  {
    juce::AudioProcessorValueTreeState::ParameterLayout layout;
   // ParameterIDs can't contain spaces
    
    auto lowerId = std::make_unique<juce::AudioParameterInt>(juce::ParameterID{LOWER_CONTROL_ID, 1}, LOWER_CONTROL_ID, 21, 108, 21);
    
    layout.add(std::move(lowerId));
    return layout;
  }
};

公式Tutorialによると、APVTSはひとつのAudioProcessorにのみattach出来る、とあります。また、APVTSにparameterを追加すると自動的にAudioProcessorに追加したことになる、とあります。

また公式Tutorial動画によると、AudioParameterとAPVTSのAttachmentクラスを使用することで、Audio ThreadとUI Thread間のThread Safeなプログラムを書くことが出来る、とあります。SliderインスタンスはSliderAttachmentインスタンスよりもあとにdeconstructされる必要があるため、宣言の順番は重要です。

// AudioProcessorEditor.h

private:
  // 省略
  juce::AudioProcessorValueTreeState& valueTreeState;
  juce::Slider slider;

std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> sliderAttachment;

// AudioProcessorEditor.cpp
// constructor 
 : valueTreeState(p.getValueTreeState())
{
  sliderAttachment = std::make_unique<juce::AudioProcessorValueState::SliderAttachment>(valueTreeState, LOWER_CONTROL_ID, slider);
}

ちなみに、公式TutorialではJUCEでは全てのAudioParameterは[0, 1]のレンジで保存される、とあります。JUCE stores all of the parameter values in the range [0, 1] as this is a limitation of some of the target plug-in APIs. 

Host(DAW)がプロジェクトをsave/loadする時は、Plug-Inの状態(state)もsave/loadします。その際、Plug-Inのstateのload時はHostが管理するメモリ領域へserialize、save時はメモリ領域からdeserializeされます。

プロジェクトをsaveする時、Hostが getStateInformation (juce::MemoryBlock& destData)をコールし、Plug-Inのstateをゲットします。

void ChordGeniusAudioProcessor::getStateInformation(juce::MemoryBlock& destData) {
  // copying State from apvts, then create XML from it.
  if (auto xmlState = valueTreeState.copyState().createXml()) 
  {
    // dereference from xmlState and copying it to MemoryBlock
    // human readable xml will allow debugging...
    copyXmlToBinary(*xmlState, destData);
  }
}

プロジェクトをloadする時、Hostが setStateInformation (const void* data, int sizeInByte)をコールしします。

void ChordGeniusAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
  if(auto xmlState = getXmlFromBinary (data, sizeInBytes)
  {
    if (xmlState->hasTagName(valueTreeState.state.getType())) {
    const auto newTree = juce::ValueTree::fromXml(*xmlState);
    valueTreeState.replaceState(newTree);
    }
  }
}

APVTSからパラメータをゲットする時は

// returns juce::Value type object
auto param = valueTreeState.getParameterAsValue("PARAMETER_ID");

パラメータに値をセットするときは

// need to cast to juce::var type in order for Host application to save and persist the plut-in state.

param.setValue(juce::var(newValue));
カテゴリー
C++ JUCE

JUCEのPopupMenu

JUCEのComboBoxのPopupMenuがGUIの後ろに隠れてしまい、on topに表示されない問題がある場合、この情報をもとにLookAndFeelのメソッドをoverrideすると、うまくon topに表示出来て、またドラッグ動作の際 parent componentと一緒に動く挙動を実装出来ます。

Component* getParentComponentForMenuOptions (const PopupMenu::Options& options) override
{
    if (auto* comp = options.getParentComponent())
        return comp;

    if (auto* targetComp = options.getTargetComponent())
    {
        if (auto* editor = targetComp->findParentComponentOfClass<AudioProcessorEditor>())
            return editor;
    }

    return LookAndFeel_V4::getParentComponentForMenuOptions (options);
}

AudioProcessorEditorをparentとすることで、ComboBoxのpopupが全てのWindowよりもfrontに来るように出来ます。

カテゴリー
iOS Swift

iOS & Swift 102 Threadについて

これまで見てきたコードは、全体がひとつの流れになっていて、初めから終わりまでが順番に処理されていくものになっていました。

このひとつの流れのことをThread(スレッド)といいます。

以下の例はボタンをタップすると1秒ごとにコンソールに変数iの値がプリントされるプログラムです。

import UIKit

class ViewController: UIViewController {
  
  lazy var button: UIButton = {
    let button = UIButton()
    button.setTitle("Tap", for: .normal)
    button.setTitleColor(.white, for: .normal)
    button.backgroundColor = .red
    button.addTarget(self, action: #selector(onButtonTapped), for: .touchUpInside)
    return button
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.addSubview(button)
  }
  
  override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    button.frame = CGRect(x: view.bounds.width/2 - 50, y: view.bounds.height/2 - 50, width: 100, height: 100)
  }
  
  private func loop () {
    for i in 0..<100 {
      Thread.sleep(forTimeInterval: 1)
      print(Thread.current)
      print(i)
    }
  }
  
  @objc private func onButtonTapped() {
    loop()
    print("button tapped")
  }
}

これをビルドし、ボタンをタップすると、コンソールに1秒ごとに変数iのカウントがプリントされます。

[1] 現在のThreadオブジェクトをコンソールにプリントしています。<_NSMainThread: ….>とプリントされているのがメイン・スレッドです。

Swiftだけではなく、他の色々なプログラミング言語・OSにおいても同じですが、UI(ユーザーインターフェース)はmain thread(メイン・スレッド)で動作しています。

カウント中にボタンをタップしても、コンソールには”button tapped”とプリントされないことに気が付くと思います。

これはThread.sleepがメイン・スレッドにおいて行われているため、UIがブロック(フリーズ)しているからです。

プログラミングでは、UIが動作するメインスレッドでは時間のかかるタスクを処理させてはいけないという大前提があります。理論上プログラムはクラッシュせずに正しく動いているのですが、UIが固まって見えるようでは、ユーザー満足度はとても低くなるからです。時間のかかるタスクはUIとは別のスレッド(バックグラウンド・スレッド)で処理する必要があります。

実際のアプリではひとつのThreadだけで動作していることはあまりなく、複数のThreadが起動しています。これをMulti Thread(マルチ・スレッド)といいます。

iOSではこのマルチ・スレッドを簡単に取り扱えるGrand Central Dispatch(GCD)という仕組みがあります。

loop( )関数を以下のように書き換えてみましょう。

private func loop () {
  // バックグラウンド・キュー(Thread)を作成
  DispatchQueue.global().async {
    for i in 0..<10 {
      Thread.sleep(forTimeInterval: 1)
    // NS Thread number = 5 などとプリントされる
      print(Thread.current)
      print(i)        
    }
  }
}

ボタンをタップするとバックグラウンド・スレッドが起動され、そのすぐ後に”button tapped”とプリントされ、カウントが始まるのがわかると思います。

もう一度ボタンをタップすると、また”button tapped”とプリントされ、先ほどとは違うカウントが「並列(concurrent)に」実行されているのがわかると思います。これは、もうひとつ別のバックグラウンド・スレッドを起動した、ということです。

次に、ひとつラベルを用意して、変数iの値をスクリーンに表示するようにしてみましょう。太字部分が先ほどのコードからの変更点です。

class ViewController: UIViewController {
  
  // labelを用意
  lazy var label: UILabel = {
    let label = UILabel()
    label.backgroundColor = .blue
    label.textColor = .white
    label.textAlignment = .center
    return label
  }()
  
  lazy var button: UIButton = {
    let button = UIButton()
    button.setTitle("On / Off", for: .normal)
    button.setTitleColor(.white, for: .normal)
    button.backgroundColor = .red
    button.addTarget(self, action: #selector(onButtonTapped), for: .touchUpInside)
    return button
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.addSubview(button)
  // labelをviewに追加
    view.addSubview(label)
  }
  
  override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    button.frame = CGRect(x: view.bounds.width/2 - 50, y: view.bounds.height/2 - 50, width: 100, height: 100)
  // labelをレイアウト
    label.frame = CGRect(x: view.bounds.width/2 - 50, y: view.bounds.height/4 - 25, width: 100, height: 50)
  }
  
  
  private func loop () {
    DispatchQueue.global().async {
      for i in 0..<10 {
        Thread.sleep(forTimeInterval: 1)
        print(Thread.current)
     // labelテキストにiを代入
        self.label.text = "\(i)"
      }
    }
  }
  
  @objc private func onButtonTapped() {
    loop()
    print("button tapped")
  }
}

ビルドし、ボタンをタップしてみましょう。

実はこのコードはうまく動作しません。

UI関連のアップデートはバックグラウンド・スレッドから行ってはいけないというルールがあるからです。

loop( )関数を以下のように変えるとうまく動作します。

private func loop () {
  DispatchQueue.global().async {
    for i in 0..<10 {
      Thread.sleep(forTimeInterval: 1)
      print(Thread.current)
      // バックグラウンド・スレッドから
    // メイン・スレッドに処理を戻している
      DispatchQueue.main.async {
        self.label.text = "\(i)"          
      }
    }
  }
}
カテゴリー
iOS Swift

iOS & Swift 102 callbackとdelegate

処理の中には、一定の時間がかかるものがあり(インターネット越しにあるAPIサーバーからレスポンスを待つ、など)、その処理が終わった後にプログラムに「これをやっといてね」と「頼み」たいことがよくあります。一般的にはそれらの処理を行う関数をcallback関数と呼びますが、Swiftではclosureを使ったパターン、delegateを使ったパターンがよく見られます。

まず closureで実装した例を説明します。

// PlaySound.swift

class PlaySound {

  // [1] 処理に1秒かかる作業を10回実行しているシミュレーション。1秒ごとにcallbackを呼ぶ。callbackはclosure型で指定
  public func play(callback: (Int) -> Void) {
    for i in 0..<10 {
      Thread.sleep(forTimeInterval: 1)
      callback(i)
    }
  }
}

// ViewController.swift

class ViewController: UIViewController {
 // PlaySoundクラスのインスタンス
  let playSound = PlaySound()

  override func viewDidLoad() {
    super.viewDidLoad()
    // [2] インスタンスメソッドをコールする時に関数を渡している
    playSound.play(callback: printNum)
  }
  
  // コールバック関数の実装をここで記述出来る
  private func printNum(num: Int) {
    print(num)
  }
}

[1] 関数 play() はclosure型の引数を取ります。

closureとは、名前の無い関数のことで、以下のように( ) -> ( )のような形で書くことができます。最初の( )にはそのclosureの引数を、->(矢印)の後の( )には返り値を記述します。

( ) はtuple(タプル)という型で、複数の異なる型を入れられます。

ここで面白いのは、play( )関数の引数として渡すclosureの型は指定しますが、closureでどのような処理をするのかは、play( )関数を定義する時には決めなくてもよく、その関数を呼ぶ時に決められる、ということです。

[2] 上記の例ではplay( )に渡すためのprintNum(num: Int)という関数をViewControllerクラスで定義し、play(callback: printNum)という形で渡しています。

(Int) -> Void というclosureの型と printNum(num: Int)は合っている必要があります。

func printNum(num: Int) -> Void { // 返り値がない場合 -> Voidは省略可

}
// closureの書き方の例

// 引数なし、返り値なし
() -> ()
() -> Void

// 引数はInt型、返り値はなし
(Int) -> ()
(Int) -> Void

// 引数はIntとFloat、返り値はIntとBool
(Int, Float) -> (Int, Bool)

次に、Delegateパターンの説明をしますが、その前にProtocolについて理解する必要があります。

Protocolとは、この処理を担当するなら、これらの関数(や変数)は備えといてね、という「規約」を決めるものです。

// PlaySound.swift

// [1] Protocolを定義
protocol PlaySoundDelegate {
  func printNum(num: Int)
}

class PlaySound {

 // [1] Protocol型の変数を宣言
  var playDelegate: PlaySoundDelegate!

  public func play() {
    for i in 0..<10 {
      Thread.sleep(forTimeInterval: 1)
   // ここでProtocolの関数を使う
      playSoundDelegate.printNum(num: i)
    }
  }
}

// ViewController.swift

class ViewController: UIViewController,
                        PlaySoundDelegate // [2]-b   Protocolに準拠
{

  let playSound = PlaySound()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    // [2]-a   delegateをself(ViewControllerクラス)に指定
    playSound.playSoundDelegate = self
    playSound.play()
  
  }
  // [2]-c   Protocolの関数を定義
  func printNum(num: Int) {
    print(num)
  }
}

[1] delegateは「任せる」という意味です。PlaySoundクラスの色々な処理が完了したら、その次に任せたい処理(関数)をProtocolに記述します。Protocolには関数の宣言だけをし、定義はProtocolに準拠するクラスに記述します。

[2] 上記の例ではViewControllerクラスをPlaySoundDelegate Protocolに準拠させることにします。

[2]-a playSound.playSoundDelegate = self はViewControllerクラスがdelegateを担当しますよ、と指定しています。

[2]-b その際、クラスがProtocolに準拠していないとエラーになります。

[2]-c また、Protocolの必須関数を定義していないとエラーになります。

どちらの実装方法でも同じようなことは実現出来るので、まずは一つのやり方にじっくり慣れると、より理解が深まると思います。

カテゴリー
iOS Swift

iOS & Swift 101(初歩)Part 3

ViewController内でUIエレメントの配置に慣れてきたら、次は簡単なclassを作ってみることをおすすめします。

プロジェクトナビゲーターのプロジェクトファイルを右クリック->New File->Swift Fileを選択、ファイル名(class名)を記入して新規ファイルを作成します。

PlaySoundという名前のclassを作ることにし、その中のmember variable(メンバ変数)として、拍子を表すmeterを定義してみます。

class PlaySound {
  
  public var meter: Int = 4
  
}

これをViewControllerで使うには、以下のようにします。

// ViewController.swift

class ViewController: UIViewController {
  
  let playSound = PlaySound()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    print(playSound.meter)
 
  }
}

class PlaySound内に関数を作り、それを介してmeterにアクセスすることも可能です。

変数や関数はAccess Controlを定義することが出来ます。publicは自身以外のclassからもアクセスが可能、privateはそのclass自身以外からのアクセスが不可になります。これは将来的により安全でわかりやすいコードを書く時に役に立ちますが、いまはそれほど気にしなくても良いと思います

// PlaySound.swift

class PlaySound {
  
  private var meter: Int = 4
  
  public func printMeter() {
    print(meter)
  } 

  public func getMeter() -> Int {
    return meter
  }
}
// ViewController.swift

class ViewController: UIViewController {
  
  let playSound = PlaySound()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    // playSoundで定義したprintする関数
    playSound.printMeter()
    // playSoundからInt型の返り値をprintする処理
    print(playSound.getMeter())
 
  }
}
カテゴリー
C++

C++ の文法

C++の文法について、個人的なメモ

Stack vs Heap

Objectをinstantiateする時、メモリをstackとheapどちらからallocateするかによって処理内容と文法が大きく違います。

struct Vector3 {
  float x, y, z;

  Vector3()
    : x(10), y(11), z(12) {}
};

// stack
// int
int value = 5;
// array. allocating 5 * int size in memory
int array[5];
array[0] = 1;
array[1] = 2;
array[2] = 3;
array[3] = 4;
array[4] = 5;
// object
Vector3 vector;

// heap
// int
int* hvalue = new int;
*int = 5;
// array
int* harray = new int[5];
harray[0] = 1;
harray[1] = 2;
harray[2] = 3;
harray[3] = 4;
harray[4] = 5;
// object, () is optional
Vector3* hvector = new Vector3();

// you need to manually call delete to deallocate
delete hvalue;
delete[] harray;
delete hvector;

stackにvariableをallocateする時は、”stack pointer”がメモリ上でvariableのサイズだけ移動させ、メモリを次々とallocateする(stack)ので、動作がとても早いです。variableのscopeが終わると自動的にdeallocateされます。

heapの場合はallocateの時newで「free list(メモリ領域にあるavailableなメモリのリスト)」からavailableなメモリブロックを探しだし、確保し、そのpointerをdereferenceした上で、値を代入します。また、明示的に delete(deallocate)する必要があります。もしApplicationに割り当てられた以上のメモリが必要になると、Applicationはsystemに要求します。つまりとても「コストが高い」オペレーションです。

smart pointers の場合は裏でこのnew、deleteが呼ばれています。

可能な限りstackにメモリをallocateする方が良い、ということです。

PointerReference

int a = 5;
int* ptr = &a;
std::cout << *a << std::endl; // prints 5 (value of a)

int& ref = a;  // ref is now an alias of a
ref = 2;
std::cout << a << std::endl; // prints 2

Constructor

このThe Chernoのtutorialで説明されているように、initializer listによるinitializationは、member variableを一度だけinstantiateするため効率が良いです。また、initializer listによるinitializationはinitializer listの順番ではなく、member variableが宣言されている順に実行されるため、dependencies(どのmemberが先にinitializeされなくてはならないか、など)に関して気をつけなければなりません。

class Entity {

public:
  // a constructor without parameter
  Entity {
  }
 // a constructor with initializer lists
  Entity(float x, float y)
    : X(x), Y(y) // it needs to be in the same order as you declared
  {
  }
  // a constructor with parameters
  Entity(float x, float y) {
    X = x;
    Y = y;
  }
  float X, Y;
}

class Log {

public:
  // by assigning delete, can disable default constructor
  Log() = delete;
}

Copy Constructor

// example of deep copy...
class Entity {
public:
  Entity() {} // default constructor
  // copy constructor
  Entity(const Entity& other) {
    m_X = other.m_X;
  }
  int m_X;
}

Smart Pointers

//unique pointer

// shared pointer

// weak pointer

Macro

// example
#define LOG(x) std::cout << x << std::endl

Namespace alias

// example...
using MidiKeyboardStateListener = MidiKeyboardState::Listener;

Const

const int MAX_AGE = 90;
int* a = new int; // a is a pointer
*a = 2; // you can dereference a and assign 2
a = (int*)&MAX_AGE // you can reassign pointer

const int* a = new int; // add "const" keyword to a pointer
// you can also write it as "int const* a"
*a = 2; // error, you can't modify the contents of the pointer
a = (int*)&MAX_AGE; // no error
std::cout << *a << std::endl; // reading a works

int* const a = new int;
*a = 2; // this works
a = (int*)&MAX_AGE; // error
a = nullptr; // error

class Entity {
private: 
  int m_X, m_Y;

public:
  int GetX() const { // it means the function can't modify the value
                     // read only method
                     // good for getters
    m_X = 2;  // can't do this
    return m_X;
  }

  void SetX(int x) {  // can't have const here...
    m_X = x;
  }
}

void PrintEntity(const Entity& e) {
  std::cout << e.GetX() << std::end;  // if I remove const from
                                      // the above GetX(), it won't work
}

const reference

non-const lvalue reference

lvalues and rvalues

lvalueはメモリ上にアドレスがある値(変数など)j

rvalueはメモリ上にアドレスがない値(記述されただけの値)

int i = 5;
int& a = 10;  // this does not work, because 10 is a rvalue
int& b = i; // this works, because i is a lvalue
const int& c = 10; // this works
// it creates a temporary behind the scenes as in the below pseudo code
int temp = 10;
int& c = temp;

Lambda — captureをthis(オブジェクト)でするか、&(オブジェクトのreference)でするか、ですが、referenceの場合はmember変数に変更を加えます。

関数の戻り値へconst keywordをつけると、戻り値はconst(変更不可)になります。

// example
juce::String getCurrentPreset() const;
カテゴリー
C++ JUCE

JUCE その2 UIの基礎

UIについては公式Tutorialを以下の順番にやるのが良いと思います。

Application Window

Component

Graphics

以下はJUCEApplicationを継承したApplication classの最小コードです。

class MainComponentTutorialApplication: public juce::JUCEApplication
{
public:
  MainComponentTutorialApplication() {}
  const juce::String getApplicationName() override
  {
    return ProjectInfo::projectName; 
  }
  const juce::String getApplicationVersion() override 
  { 
    return ProjectInfo::versionString;
  }
  bool moreThanOneInstanceAllowed() override { return true; }
  void initialize (const juce::String& commandLine) override
  {
    // [3] initializing the mainWindow instance.
    mainWindow.reset(new MainWindow(getApplicationName()));
  }
  void shutdown() override
  {
    // [4] nullifying the pointer 
    mainWindow = nullptr;
  }
  void systemRequestedQuit() override
  {
    quit();
  }

  // [1] nested class inside the application class
  class MainWindow: public juce::DocumentWindow {
  public:
    MainWindow(juce::String name): DocumentWindow(name,
                                          juce::Colours::lightgrey,
                                          DocumentWindow::allButtons)
    {
      // [1] setup
      setUsingNativeTitleBar(true);
      // [5] resize will work automatically when setResizable is set
     // you can write custom layout logic inside resized() 
     // virtual func of Component class
   setResizable(true, false);
      centreWithSize(300, 200);
      setVisible(true);
    }
    void closeButtonPressed() override
    {
      JUCEApplication::getInstance()->systemRequestedQuit();
    }
  private:
   JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainWindow)
  };
private:
  // [2] declare as a private member of the class. 
  // unique_ptr is a scoped pointer.
   // It gets deleted automatically when it becomes out of scope.
  std::unique_ptr<MainWindow> mainWindow;
};

この状態からComponentを追加していきます。

JUCEでは、全てのGUIエレメント(button, slider, textFieldなど)がComponentクラスを継承(derive, inherit)しています。

ApplicationにまずmainComponentを追加し、その他のGUIエレメントはmainComponentのchildrenとして追加されます。

ProjucerのFile Explorerの+ボタンをクリックし、Add New Component class(split between a CPP & header)を選択し、ファイル名をMainComponentとします。

// MainComponent.h

class MainComponent  : public juce::Component
{
public:
    MainComponent();
    ~MainComponent() override;

    // [5] two important virtual functions
    void paint (juce::Graphics&) override;
    void resized() override;

private:
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

Componentベースクラスには2つ重要なvirtual function(subclassでoverrideするfunction)があり、全てのcomponentで定義される必要があります。

Component::paint() はこのcomponentがスクリーン上にどのように描画されるかを定義し、

Component::resized() は画面リサイズ時の挙動を定義します。

// MainComponent.cpp
void MainComponent::paint (juce::Graphics& g)
{
    g.fillAll (juce::Colours::lightblue);
 
    g.setColour (juce::Colours::darkblue);
    g.setFont (20.0f);
    g.drawText ("Hello, World!",
                getLocalBounds(), 
                juce::Justification::centred, 
                true);
}
// Main.cpp
#include "MainComponent.h"

// MainWindow constructor
MainWindow(juce::String name): DocumentWindow(name,
                                    juce::Colours::lightgrey,
                                   DocumentWindow::allButtons)
{
  setUsingNativeTitleBar(true);

  // assigning MainComponent instance in setContentOwned()
  setContentOwned(new MainComponent(), true);

  // [6 ] in order for getWidth() and getHeight() to work, 
  // MainComponent needs to have setSize() set in it's constructor
  centreWithSize(getWidth(), getHeight());
  setVisible(true);
}
// MainComponent.cpp
MainComponent::MainComponent()
{
  setSize(400, 300);
}

[6] componentのサイズ設定し忘れはJUCEでよくあるbugの原因、、、とのことです。

これらvirtual functions はresized()、paint()の順で、componentのinitialize時に一度呼ばれます。これらは必要に応じてシステムがコールするので、プログラマーが自分でコールしてはいけません。

[5] MainWindowのconstructorでsetResizable()を設定すると、Component::resized()に何も記述しなくてもリサイズは動作します。child elementなどのレイアウトに関することをresized()に記述出来ます。

paint(juce::Graphics& g) にはGraphicsクラスのインスタンスのアドレス g が渡されています。この g を使って色々なUIエレメントを描画出来ます。ほぼ全ての場合、Graphicsクラスはpaint()内でのみ使用されます。

Fontの設定

g.setFont(20.0f); // set size of the font

juce::Font mainComponentFont ("Times New Roman", 20.0f, juce::Font::italic);
// font styles can be used as a bitmask
juce::Font mainComponentFont ("Times New Roman", 20.0f, juce::Font::bold | juce::Font::italic);
g.setFont(mainComponentFont); // can pass a Font object

// using getLocalBounds()
g.drawText(currentSizeAsString, getLocalBounds(), juce::Justification::centred, true);
// using Justification::Flags
g.drawText(currentSizeAsString, getLocalBounds(), juce::Justification::topLeft, true);
// using explicit size and position
g.drawText(currentSizeAsString, 20, 40, 200, 40, juce::Justification::centred, true);

// draws a line with 5 pixels width from (10, 300) to (590, 300), 
g.drawLine (10, 300, 590, 300, 5);
// draws a rectangle with origin (300, 120), width 200, height 170
g.drawRect (300, 120, 200, 170);
// draws a ellipse inside a rectangle with origin (530, 120), width and height 60 pixels
g.drawEllipse (530, 120, 60, 60, 3);
カテゴリー
C++ JUCE

JUCE その1、インストール

JUCE(Audio/Midi application/plug-inのためのC++フレームワーク)でVST3のMidiプラグインを作ろうと思い立ち、インストールの手順、など、忘れがちなステップを記録しておきます。公式Documentはここ

JUCEはgithubのdevelopブランチをクローンします。

git clone https://github.com/juce-framework/JUCE.git JUCE_dev
cd ./JUCE_dev
git checkout develop
// branch 'develop' set up to track 'origin/develop'.
// Switched to a new branch 'develop'

JUCE_dev -> extras -> Projucer -> Builds -> MacOSX 内のProjucer.xcodeprojを立ち上げてビルドするとProjucerアプリが立ち上がります。

ProjucerのGlobal pathsをこのJUCE_devに設定します。

  • ProjucerのメニューでNew Projectを選択
  • Plug-In -> Basicを選択、Project Nameに適当な名前を入力し、Create Project…をクリック。保存先を聞かれるので、disk内の適切な場所を選択し、Openをクリック。

VSTプラグインを作成する場合は、プロジェクト名の横のギア・アイコンをクリックし、Plugin Formatsで「VST3」「AU」「AUv3」「Standalone」、Plugin Characteristicsで「Plugin MIDI Input」「Plugin MIDI Output」にチェックを入れます。

*Standaloneにチェックを入れないとDAW上でテストが出来ません。(updated 26 Feb 2024)

Xcodeアイコンをクリックすると、設定がsaveされ、Xcodeが起動されます。Xcodeでプロジェクトをビルドすると、標準では/Users/[your_user_name]/Library/Audio/Plug-Ins/VST3にファイルが作成されます。

これをテストする時は、/Users/[your_user_name]/JUCE/extras/AudioPluginHostビルドし、メニューバーのOptions -> Edit the List of Available Pluginsを選択

左下コーナーのOptionsをクリックし、Scan for new or updated VST3 plug-insを選択

Select folders to scan に /Users/[your_user_name]/Library/Audio/Plug-Ins/VST3 が無ければ + ボタンでパスを選択し、Scanをクリック。Scan終了後は AudioPluginHostで右クリックをするとyourcompany -> TestPlugin と先ほどビルドしたPluginが選択可能になります。

TestPluginをダブルクリックすると、”Hello World!”と画面が表示されます。

開発中のPlug-Inをビルドする度にこのAudioPluginHostを立ち上げるには、Plug-InのXcodeのProduct -> Scheme -> Edit Schemeを選択、Runの中の ExecutableでOtherを選択し、ここで /Users/[your_user_name]/JUCE/extras/AudioPluginHost/Builds/MacOSX/build/Debug/AudioPluginHost を選択、Debug executableにチェックを入れます。

Logic Pro XのMidi Pluginとして動作させるにはAUv3でコンパイルする必要があります。

カテゴリー
iOS Swift

Core Data Stackを理解する

Cocoacastの良記事を勉強することでCore Dataについてより理解をしたので、忘れないように記録しておきます。

Core Dataはデータをdisk(デバイスのストレージ)に記録(persist)することが出来ますが、それはCore Dataのひとつの機能であって全てではなく、Core DataはあくまでObject Graphを管理するものです。

XcodeでUse Core Dataをチェックしてプロジェクトを作成すると、AppDelegateに最低限のコードテンプレートが用意されますが、それが逆にCore Dataを分かりにくくしている、とも言えます。

Core Data Stackの「構成部品」をひとつひとつ自分で組んでみると、全体像がより見えてくると言えます。

Core Data Stackは以下の三つから構成されます。

  • NSManagedObjectModel
  • NSPersistentStore (NSPersistentStoreCoordinator)
  • NSManagedObjectContext

これらを順序立てて組み上げて使うためのclass CoreDataManagerを用意し、必要な場面で使用します。

import UIKit
import CoreData

class CoreDataManager {
  
  private let modelName: String

  // [3]
  public typealias CoreDataManagerCompletion = () -> ()
  private let completion: CoreDataManagerCompletion
  
  // [4]
  public func privateChildManagedObjectContext() -> NSManagedObjectContext {
    let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
    managedObjectContext.parent = mainManagedObjectContext
    managedObjectContext.undoManager = UndoManager()
    return managedObjectContext
  }
  
  // [5]
  public func saveChanges() {
    mainManagedObjectContext.performAndWait{
      do {
        if self.mainManagedObjectContext.hasChanges {
          try self.mainManagedObjectContext.save()
        }
      } catch {
        print("Unable to save changes of Main Managed Object Context")
        print("\(error), \(error.localizedDescription)")
      }
    }
    
    privateManagedObjectContext.perform {
      do {
        if self.privateManagedObjectContext.hasChanges {
          try self.privateManagedObjectContext.save()
        }
      } catch {
        print("Unable to save changes of Private Managed Object Context")
        print("\(error), \(error.localizedDescription)")
      }
    }
  }
  
  public init(modelName: String, completion: @escaping CoreDataManagerCompletion) {
    self.modelName = modelName
    self.completion = completion
    setupCoreDataStack()
    setupNotification()
  }
  
  // [1]
  private lazy var managedObjectModel: NSManagedObjectModel = {
    guard let modelURL = Bundle.main.url(forResource: modelName, withExtension: "momd") else {
      fatalError("can't get momd url")
    }
    guard let mom = NSManagedObjectModel(contentsOf: modelURL) else {
      fatalError("failed to create model from file: \(modelURL)")
    }
    return mom
  }()
  
  private lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
    let psc = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
    // adding persistentStore in a background thread in addPersistentStore() below
    return psc
  }()

  // [3]
  private func addPersistentStore(to persistentStoreCoordinator: NSPersistentStoreCoordinator) {
    let dirURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last
    let fileURL = URL(string: "\(modelName).sqlite", relativeTo: dirURL)
    do {
      try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: fileURL, options: [
        NSMigratePersistentStoresAutomaticallyOption: true,
        NSInferMappingModelAutomaticallyOption: true
      ])
    } catch {
      fatalError("error configuring persistent store: \(error)")
    }
  }
  
  private func setupCoreDataStack() {
    guard let persistentStoreCoordinator = mainManagedObjectContext.persistentStoreCoordinator else {
      fatalError("unable to set up Core Data Stack")
    }
    // [3]
    DispatchQueue.global().async {
      self.addPersistentStore(to: persistentStoreCoordinator)
      DispatchQueue.main.async {
        self.completion()
      }
    }
  }
  
  private func setupNotification() {
    let notificationCenter = NotificationCenter.default
    notificationCenter.addObserver(self, selector: #selector(onNotifyBackground), name: UIApplication.willResignActiveNotification, object: nil)
    notificationCenter.addObserver(self, selector: #selector(onNotifyBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
  }
  
  @objc private func onNotifyBackground(_ notification: Notification) {
    saveChanges()
  }

  // [2]
  private lazy var privateManagedObjectContext: NSManagedObjectContext = {
    let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
    managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator
    return managedObjectContext
  }()
  
  // [2]
  public private(set) lazy var mainManagedObjectContext: NSManagedObjectContext = {
    let managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
    managedObjectContext.parent = privateManagedObjectContext
    return managedObjectContext
  }()
}

[1] extensionがmomdとなっていますが、これはxcdatamodeldを元にコンパイルされた、より小さな(効率の良い)ファイルです。

[2] NSManagedObjectContextは動作するスレッド(メインスレッドもしくはプライベート(バックグラウンド))を指定出来ます。stack上にcontextは複数あってもよく、例えばdiskに記録するなどの作業はメインスレッドで行われるとUIをブロックしてしまう可能性があるため、バックグラウンドで行うようにするなどの目的別に分けることが出来ます。

[3] persistentStoreCoordinatorにpersistentStoreを追加する際は、状況により少し時間がかかる場合があるため、処理をバックグラウンドで行い、処理完了後の作業をコールバック関数内で行えるようにしています。

[4] mainManagedObjectContextをparentとするprivateChileManagedObjectContextを用意し、UIViewControllerなで使用するためのもの。

[5] 各contextはsave( )する事に、自身とparentに保存内容をpushします。mainManagedObjectContextがsave( )し終わるのを待ち、その後privateManagedObjectContextがsave( )し、diskに記録されるという流れです。