カテゴリー
C++ JUCE

JUCEでJSONを読み込む

Compile timeにJSONファイルを読みたい場合は以下のようにします。

Resourcesというグループを作り、その中にjsonファイルをおきます。
// BinaryDataというオブジェクトが自動的(?)に作成されるようです。
const char* jsonData = BinaryData::jazz_chords_json;
juce::String jsonString = juce::String::createStringFromData(jsonData, BinaryData::jazz_chords_jsonSize);
juce::var parsedJson = juce::JSON::parse(jsonString);
juce::var jazzChords = parsedJson["jazz_chords"];
for(int i = 0; i < jazzChords.size(); ++i) {
  DBG(juce::String(jazzChords[i]["notes"]));
}
カテゴリー
C++ JUCE

JUCE PropertiesFile

それほどサイズの大きくないデータ(ユーザー設定などに関する情報)はPropertiesFileに保存するのが良いようです。

JUCEフォーラム参照

カテゴリー
JUCE

Pluginを配布するためのNotarizationについて

SignされていないPkgファイルを配布すると、「身元不明の開発者」というアラートが出ます。これはPkgファイルがnotarizeされていないためです。

これにどう対処するかのメモです。

参考にした情報
Notarizing Installers For macOS Catalina
Apple Audio Plugin Notarization
Macでプラグインのインストーラーを作成する

まずは、ビルドの対象マシンをAny Mac(Apple Silicon, Intel)とします。

CertificateはDeveloper ID InstallerとDeveloper ID Applicationのふたつが必要です。

Keychain AccessからSigning requestを発行します。*このプロセスは有効期限内のCertificationがある場合はスキップ出来ます。むしろ、同じ名前のCertificationが二つ以上あると認証が上手く出来ません。(Updated 28 Feb 2024)

Keychain Access -> Certificate Assistant -> Request a Certificate From a Certificate Authority…
Emailと、入手したいCertificateに適当な名前をつけます。Saved to diskを選択すると、選択したフォルダにrequestが作成されます。
Show in Finderをクリックするとどこに保存されたのかを示してくれます。
Developer ID Installerを選択 -> Continue
G2 Sub-CAを選択し、Choose Fileで先ほど作成したRequestを選択、-> Continue

Certificateをダウンロードし、ダブルクリックするとKeychainに保存されます。

上記のプロセスをDeveloper ID Applicationにおいても繰り返します。

以下はビルド手順です。

Allを選択
Build ConfigurationはReleaseを選択
ビルドをすると/Users/<your_user_name>/Library/Audio/Plug-InsのVST3とComponentsにファイルが作成されます。

以下のコマンドでDeveloper ID Application 証書で codesignをします。

codesign --force -s "Developer ID Application: Your Team (XXXXXXXXXX)" ./ChordGenius.vst3 --timestamp --options runtime

codesign --force -s "Developer ID Application: Your Team (XXXXXXXXXX)" ./ChordGenius.component --timestamp --options runtime

sign済のファイルに対して 以下のpkgbuildコマンドでDeveloper ID Installer 証書を使って pkgファイルを作成します。

pkgbuild --root /Users/<your_user_name>/<path>/ChordGenius.vst3 --identifier app.kobito.ChordGenius --install-location /Library/Audio/Plug-Ins/VST3/ChordGenius.vst3 --version 1.0.0 --sign "Developer ID Installer: Your Team (XXXXXXXXXX)" --timestamp /Users/<your_user_name>/<path>/ChordGeniusVST3.pkg


pkgbuild --root /Users/<your_user_name>/<path>/ChordGenius.component --identifier app.kobito.ChordGenius.ChordGeniusAUv3 --install-location /Library/Audio/Plug-Ins/Components/ChordGenius.component --version 1.0.0 --sign "Developer ID Installer: Your Team (XXXXXXXXXX)" --timestamp /Users/<your_user_name>/<path>/ChordGeniusAUv3.pkg

ここでApp-Specific Passwordを作成します。

パスワードが登録されていると、以下のコマンドでTeam ID情報を確認出来ます。

xcrun altool --list-providers -u "<your_email_address"
// paste the app-specific password
// you'll get the Team ID information

パスワードをKeychainに登録します。

xcrun notarytool store-credentials --apple-id "<your_email_address>" --password "xxxx-xxxx-xxxx-xxxx" --team-id "XXXXXXXXXX"

// prompts to enter a profile name. can be any name.

最後に、notarytoolでpkgファイルをAppleに提出します。自動的にスキャンされ、問題がなければApproveされます。

xcrun notarytool submit /Users/<your_user_name>/<path>/ChordGeniusVST3.pkg --keychain-profile "<your_profile_name>" --wait
カテゴリー
C++ JUCE

JUCEでXmlファイルを扱う

juce::Fileクラスのインスタンスと、

const juce::File PresetManager::colorsFile { juce::File::getSpecialLocation(
  juce::File::SpecialLocationType::userApplicationDataDirectory)
  .getChildFile(ProjectInfo::companyName)
  .getChildFile(ProjectInfo::projectName)
  .getChildFile("UserSettings")
  .getChildFile("Colors.preset")
};

以下のような色の情報をhexで記録しているXmlファイルがあるとします。

<Colors>
  <Color quality="Major" hex="ffbe5050"/>
  <Color quality="Minor" hex="ffbe5050"/>
  <Color quality="minorMajor" hex="ffbe5050"/>
  <Color quality="Dominant" hex="ffbe5050"/>
</Colors>

このファイルをreadする場合は、以下のようにします。一番外側のタグ<Colors>は特に指定しなくても勝手にparseしてくれるようです。

std::unique_ptr<juce::XmlElement> colorsData;

void readXml() {
  colorsData = juce::XmlDocument::parse(colorsFile);
  for(auto* c: colorsData->getChildIterator()) {
    DBG(c->getStringAttribute("quality");
  }
}

// prints
// Major
// Minor
// minorMajor
// Dominant

二段構造のXmlの作成と読み込みは以下のコード例

if(!colorsFile.existsAsFile()) {
  // parent
  juce::XmlElement parentXml("DATA");
    
  // controls
  auto controlsXml = std::make_unique<juce::XmlElement ("Controls");
    
  auto childXmlSlider = std::make_unique<juce::XmlElement>("Color");
  childXmlSlider->setAttribute("control", SLIDER_COLOR);
  std::string trackColor = "fff28d11";
  childXmlSlider->setAttribute("hex", trackColor);

  // update with a method
  colorHandler.updateHex(SLIDER_COLOR, trackColor);
  controlsXml->addChildElement(childXmlSlider.release());
    
  // chords
  auto chordsXml = std::make_unique<juce::XmlElement>("Chords");
  auto childXmlMinor = std::make_unique<juce::XmlElement>("Color");
  childXmlMinor->setAttribute("quality", MINOR_NAME);
  std::string minorColor = "ff3488c3";
  childXmlMinor->setAttribute("hex", minorColor);
  colorHandler.updateHex(MINOR_NAME, minorColor);
  auto childXmlMinorMajor = std::make_unique<juce::XmlElement>("Color");
  childXmlMinorMajor->setAttribute("quality", MINOR_MAJOR_NAME);
  std::string minMajorColor = "ff4e7e4f";
  childXmlMinorMajor->setAttribute("hex", minMajorColor); // greenyellow
  colorHandler.updateHex(MINOR_MAJOR_NAME, minMajorColor);
  auto childXmlMajor = std::make_unique<juce::XmlElement>("Color");
  childXmlMajor->setAttribute("quality", MAJOR_NAME);
  std::string majorColor = "ffeeb42b";
  childXmlMajor->setAttribute("hex", majorColor); // orange
  colorHandler.updateHex(MAJOR_NAME, majorColor);
  auto childXmlDominant = std::make_unique<juce::XmlElement>("Color");
  std::string dominantColor = "ff7fa080";
  childXmlDominant->setAttribute("quality", DOMINANT_NAME);
  childXmlDominant->setAttribute("hex", dominantColor); // purple
  colorHandler.updateHex(DOMINANT_NAME, dominantColor);
  chordsXml->addChildElement(childXmlMinor.release());
  chordsXml->addChildElement(childXmlMinorMajor.release());
  chordsXml->addChildElement(childXmlMajor.release());
  chordsXml->addChildElement(childXmlDominant.release());
    
  parentXml.addChildElement(controlsXml.release());
  parentXml.addChildElement(chordsXml.release());
  if(!parentXml.writeTo(colorsFile)) {
    DBG("Failed to create Colors xml file");
  } 
} else {
  // if exists, read and update hex with it.
  // returns the outmost XmlElement*
  auto xml = juce::XmlDocument::parse(colorsFile);
  if(xml != nullptr) {
    for(auto child : xml->getChildIterator()) {
      if(child->hasTagName("Controls")) {
        for(auto control: child->getChildIterator()) {
          const auto name = control->getStringAttribute("control");
          const auto hex = control->getStringAttribute("hex");
          colorHandler.updateHex(name, hex);
        }
      }
      else if (child->hasTagName("Chords")) {
        for(auto chords: child->getChildIterator()) {
          const auto name = chords->getStringAttribute("quality");
          const auto hex = chords->getStringAttribute("hex");
          colorHandler.updateHex(name, hex);
        }
      }
    }
  }
}

voicingsFileというxmlファイル内に以下のデータがあるとします。

<DATA>
  <Voicings>
    <Voicing index="1" name="minor7 11 9top" quality="Minor">
      <Voice value="1"/>
      <Voice value="11"/>
      <Voice value="16"/>
      <Voice value="18"/>
      <Voice value="27"/>
    </Voicing>
    <Voicing index="2" name="minor7 11 b3top" quality="Minor">
      <Voice value="1"/>
      <Voice value="11"/>
      <Voice value="18"/>
      <Voice value="20"/>
      <Voice value="28"/>
    </Voicing>
    <Voicing index="3" name="minor7 11 11top" quality="Minor">
      <Voice value="1"/>
      <Voice value="11"/>
      <Voice value="16"/>
      <Voice value="27"/>
      <Voice value="30"/>
    </Voicing>
  </Voicings>
</DATA>
// auto xmlはTagName "DATA"のxmlElementのunique_ptrです。
auto xml = juce::XmlDocument::parse(voicingsFile);

// auto voicingsXmlはTagName "Voicings"のxmlElementのunique_ptrです。
auto voicingsXml = xml->getChildByName("Voicings");

// child elements からattributeで検索することができます。
auto voicingInInterest = voicingsXml->getChildByAttribute("index", std::to_string(index));

// 削除する場合はremoveChildElementを使います。
voicingsXml->removeChildElement(voicingInInterest, true);
カテゴリー
C++ JUCE

JUCEでRectangleをdrawする

JUCEで四角いシェイプを描画する方法として、カスタムComponentクラスを用意し、paintメソッドに記述します。

AudioProcessorEditorクラスにCustomComponentをレイアウトする場合、以下のpattern 1 〜 3は同じ結果をもたらします。

// CustomComponent.h

class CustomComponent: public juce::Component {
public:

  void paint(juce::Graphics& g) override
  {
    // pattern 1
    g.fillAll(juce::Colurs::orange);
    
    // pattern 2
    auto rect = getLocalBounds();
    g.setColour(juce::Colours::orange);
    g.fillRect(rect);

    // pattern 3
    g.setColour(juce::Colours::orange);
    juce::Path path;
    path.startNewSubPath(rect.getX(), rect.getY());
    path.lineTo(rect.getX(), rect.getHeight());
    path.lineTo(rect.getWidth(), rect.getHeight());
    path.lineTo(rect.getWidth(), rect.getY());
    path.closeSubPath();
    g.fillPath(path); 

    // pattern 4
    g.setColour(juce::Colours::orange);
    juce::Path path;
    // directly adding rect to the path
    auto rect = getLocalBounds();
    path.addRectangle(rect);
    g.fillPath(path);

    // pattern 5
    g.setColour(juce::Colours::orange);
    juce::Path path;
    auto rect = getLocalBounds();
    // draw rounded corner
    float cornerRadius = 10.f;
    path.addRoundedRectangle(rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight(), cornerRadius);
    g.fillPath(path);

    // pattern 6
    // draw rounded rect using Rectangle<float>
    const float borderWidth = 2.f;
    const float cornerRadius = 10.f;
    const juce::Rectangle<float> rect = getLocalBounds().toFloat().reduced(borderWidth / 2.f);
    
    g.setColour(juce::Colours::grey);
    g.drawRoundedRectangle(rect, cornerRadius, borderWidth);

    // pattern 7
    auto rect = getLocalBounds();
    g.setColour(juce::Colours::black);
    juce::Path path;
    path.addRoundedRectangle(rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight(), rect.getWidth/2);
    g.fillPath(path);
  }

  
}
// AudioProcessorEditor.cpp

void AudioProcessorEditor::resized()
{
  auto area = getLocalBounds();
  auto width = area.getWidth();
  auto height = area.getHeight();
  float componentWidth = 100.f;
  float componentHeight = 100.f;
  customComponent.setBounds(width/2 - componentWidth/2, height/2 - componentHeight/2, componentWidth, componentHeight);
}
カテゴリー
C++ JUCE

JUCE SVG Button

参考にした情報

SVGアイコンをボタンに使うには以下のようにします。

addAndMakeVisible(velocityButton);
juce::Path velocityPath;
velocityPath.loadPathFromData(velocityPathData, sizeof(velocityPathData));

// if you want any transformation, use AffineTransform  velocityPath.applyTransform(juce::AffineTransform::rotation(juce::MathConstants<float>::halfPi));
    
velocityButton.setShape(velocityPath, true, true, false);
// set it to behave as a toggle button
velocityButton.setClickingTogglesState(true);
velocityButton.shouldUseOnColours(true);

AudioProcessorValueTreeState (APVTS)のButtonAttachmentを使うことでAPVTSパラメータとボタンが紐付けられます。

velocityButtonAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment>(valueTreeState, isVelocityRandomIdString, velocityButton);
カテゴリー
C++ JUCE

Message Thread

他のプログラミング言語、フレームワークと同様に、UIに関わる変更は、message thread (main thread)で行われる必要があります。

特に、AudioParameterの変更(background threadで処理されるようです)をトリガーとしてUIに変更を加える場合は、juce::CallbackMessageクラスを使って処理をmessage threadにdispatchしないと上手く動作しなかったり、Pluginがクラッシュします。

class ModulesContainerComponent
{
public:
  ModulesContainerComponent()
  {} 
  // 省略
  
  void parameterChanged(const juce::String &parameterID, float newValue) override {
    // dispatching to the message thread
    (new ResizeByAudioParameterCallback(*this))->post();
  }
  
  void updateControlBound() {
    lowerBound = (int)*valueTreeState.getRawParameterValue(LOWER_CONTROL_BOUND_ID);
    upperBound = (int)*valueTreeState.getRawParameterValue(UPPER_CONTROL_BOUND_ID);
  }
  
  void rebuild() {
    updateControlBound();
    int moduleCount = upperBound - lowerBound;
    // clear modules and rebuild
    modules.clear();
    if(moduleCount > 0) {
      for (int i = lowerBound; i <= upperBound; i++) {
        addAndMakeVisible(modules.add(std::make_unique<PerKeyModuleComponent>(i, valueTreeState, presetManager, lookAndFeel, voicings, dirtyVoices, colorHandler, showVoicingConfig)));
      }
    }
    repaint();
    resized();
  }
  
  // for dispatching to the message thread
  class ResizeByAudioParameterCallback : public juce::CallbackMessage
  {
  public:
    ResizeByAudioParameterCallback(ModulesContainerComponent& o)
      : owner(o)
    {
    
    }
    ModulesContainerComponent& owner;
    
    void messageCallback() override {
      owner.rebuild();
    }
  };
  
private:
  // 省略

  JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ModulesContainerComponent)
};
カテゴリー
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に来るように出来ます。