カテゴリー
C++ JUCE

JUCE ComboBoxのSelectorID

ComboBoxをAudioProcessorValueTreeState::ComboBoxAttachmentを使わずにAPVTSのパラメターと連動させたい場合は以下のようにします。

こうすることで、SelectorIDのmax値を2000(でも2000000でも可)などにし、後々の拡張にも対応出来ます。setValue()の時、型をjuce::var()にキャストすることでホスト(DAW)のパラメータとして保存されるようです。

void comboBoxChanged(juce::ComboBox *comboBoxThatHasChanged) override 
{
  if (comboBoxThatHasChanged == &voicingSelector) 
  {
    controls.clear();
    int comboBoxSelectedIndex = comboBoxThatHasChanged->getSelectedId();
    if(comboBoxSelectedIndex != 0) 
    {
      DBG("comboBoxSelectedIndex " << comboBoxSelectedIndex);
      auto selectorIdParam = valueTreeState.getParameterAsValue(selectorIdString);
// need to cast to juce::var type in order to persist the value in Host...

selectorIdParam.setValue(juce::var(comboBoxSelectedIndex));
    }
  }
}

復習ですが、ComboBoxの基本的な使い方は以下。。。

// add the parent Component as a listener
// you should remove the listener in the deconstructor
comboBox.addListener(this);

// sets selector id
comboBox.setSelectedId(id, juce::dontSendNotification);
カテゴリー
C++

C++ でstd::vectorをsortする

C++でstd::vectorを特定の順番でソートするsample code

例1 strings

void sortGuitarVoicingTypes() {
  std::vector<juce::String> order = {"Drop 2", "Drop 3", "Drop 2 & 3", "Drop 2 & 4", "Drop x2 & 3", "Closed Voicing"};
  std::vector<juce::String> result;
  for (const auto& str : order) {
    auto it = std::find(guitarVoicingTypes.begin(), guitarVoicingTypes.end(), str);
    if (it != guitarVoicingTypes.end()) {
      result.push_back(str);
      guitarVoicingTypes.erase(it);
    }
  }
  std::sort(guitarVoicingTypes.begin(), guitarVoicingTypes.end());
  result.insert(result.end(), guitarVoicingTypes.begin(), guitarVoicingTypes.end());
  guitarVoicingTypes = result;
}

例2 Object

struct Voicing {
  Voicing() {}
  Voicing(int i, juce::StringRef name, juce::StringRef quality, std::vector<int> voices)
  : index(i), name(name), quality(quality), voices(voices) {}
  int index;
  juce::String name;
  juce::String quality;
  std::vector<int> voices;
 
};
  
struct compare {
  inline bool operator() (const Voicing& v1, const Voicing& v2)
  {
    // Define the ordering of the quality values
      const std::array<juce::String, 4> qualityOrder = {MINOR_NAME, MAJOR_NAME, MINOR_MAJOR_NAME, DOMINANT_NAME};

      // Compare the quality values
      auto lhsQualityIndex = std::distance(qualityOrder.begin(), std::find(qualityOrder.begin(), qualityOrder.end(), v1.quality));
      auto rhsQualityIndex = std::distance(qualityOrder.begin(), std::find(qualityOrder.begin(), qualityOrder.end(), v2.quality));
      
    if (lhsQualityIndex != qualityOrder.size() && rhsQualityIndex != qualityOrder.size()) {
      // If both qualities are in the qualityOrder array, compare their indices
      return lhsQualityIndex < rhsQualityIndex;
    } else if (lhsQualityIndex == qualityOrder.size() && rhsQualityIndex == qualityOrder.size()) {
      // If both qualities are not in the qualityOrder array, compare them alphabetically
      return v1.quality < v2.quality;
    } else if (lhsQualityIndex != rhsQualityIndex) {
      // If only one quality is in the qualityOrder array, it is considered greater than the other
      return lhsQualityIndex < rhsQualityIndex;
    } else {
      // If the quality values are the same, compare the indices
      return v1.index < v2.index;
    }
  }
};

このように使います。

// std::vector<Voicing> voicings {};
std::sort(voicings.begin(), voicings.end(), compare());
カテゴリー
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"]));
}
カテゴリー
Mac

MacでLogicの音声と画面、マイクの音声入力を収録

M1以前はSoundFlowerが使えたようですが、M1以降はBlackHoleというAudio Driverを使うと良いようです。設定方法を記録しておきます。

Blackholeはここで入手
2chもしくは16chをインストールします。

Multi-Output Deviceを作成 To OBSと名前をつけます。
BlackHole 2chとAudio Interface(ここではScarlett)を選択し、Master DeviceをBlackHole 2chに指定します。
Logic ProのAudio Outputを先ほど作成したTo OBSに指定します。
OBSのDesktop Audio DeviceとしてBlackHole 2chを指定します。

ScarlettをMaster DeviceとするとOBSの音声収録が不安定でしたが、MasterをBlackHoleにすることで安定しました。

カテゴリー
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++

C++ Observer Pattern

C++でObserver, Subjectパターンを使ってCustom Listenerを作るサンプルコード

class CustomSubjectInterface {
public:
  class Listener {
  public:
    virtual ~Listener() {}
    virtual void onUpdated(CustomSubjectInterface* subject) = 0;
  };
  virtual ~CustomSubjectInterface() {}
  virtual void addListener(Listener* listener) = 0;
  virtual void removeListener(Listener* listener) = 0;
  virtual void notifyListeners(CustomSubjectInterface* subject) = 0;
};

class BoolSubject: public CustomSubjectInterface {
public:
  
  enum StatusCode {
    Success, Error
  };
  
  void addListener(CustomSubjectInterface::Listener* listener) override
  {
    listeners_.push_back(listener);
  }

  void removeListener(CustomSubjectInterface::Listener* listener) override
  {
    listeners_.erase(std::remove(listeners_.begin(), listeners_.end(), listener),listeners_.end());
  }

  void notifyListeners(CustomSubjectInterface* subject) override
  {
    for (CustomSubjectInterface::Listener* listener : listeners_) {
      listener->onUpdated(subject);
    }
  }
  
  void toggle() {
    if(isTrue_) {
      isTrue_ = false;
    } else {
      isTrue_ = true;
    }
    notifyListeners(this);
  }
  
  bool& getIsTrue() {
    return isTrue_;
  }
  
  void addMessage(juce::StringRef m, StatusCode code) {
    message = m;
    code_ = code;
  }
  
  std::string getMessage() {
    return message;
  }
  
  StatusCode getStatusCode() {
    return code_;
  }
  
  void setNewVoicingId(int newId) {
    newVoicingId = newId;
  }
  
  int getNewVoicingId() {
    return newVoicingId;
  }
  
private:
  int newVoicingId = 0;
  StatusCode code_ = Success;
  bool isTrue_ = false;
  std::string message;
  std::vector<CustomSubjectInterface::Listener*> listeners_;
};

Listener側では以下のコードで変更を受け取れます。

void onUpdated(CustomSubjectInterface* subject) override {
  BoolSubject* boolSubject = dynamic_cast<BoolSubject*>(subject);
  if (boolSubject != nullptr) {
    // Do something with the boolSubject
    bool isTrue = boolSubject->getIsTrue();
    // ...
  }
}
カテゴリー
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);