カテゴリー
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);
カテゴリー
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)
};