C++/Sound

[JUCE] Juce Framework로 사운드 프로그래밍 #4 Processor와 Value Tree

Kareus 2022. 6. 24. 02:53

가상악기는 Standalone으로 쓰기보다는 DAW 프로그램에서 불러와서 쓰기 마련입니다.

그래서 DAW 프로그램과 연동시킬 수 있도록 프로그래밍하는 것이 중요합니다.

바로 그 부분을 담당하는 곳이 Processor와 ProcessorEditor입니다.

 

이전 포스팅에서 이야기했듯이, Processor가 사운드 프로세싱을 한다면, ProcessorEditor는 GUI를 그립니다.

Processor는 그 외에도 프로그램에 관련된 데이터를 저장하고 불러오는 기능도 하는데,

함수 getStateInformation과 setStateInformation을 사용합니다.

 

우리는 이 함수에다 무슨 데이터를 저장하고 불러올지만 작성해주면 됩니다.

FL Studio의 Automation

데이터는 XML로 저장되고, JUCE에서도 juce::XmlElement 등의 클래스를 지원합니다.

다만 단순히 XML로 저장해서 되는 건 아닙니다.

위 사진에서처럼, DAW 프로그램에서 value를 연동해서 값을 바꾸거나 할 필요가 있기 때문에

이러한 기능을 지원하려면 juce::AudioProcessorValueTreeState라는 클래스를 사용해야 합니다. 이름이 참 길군요.

 

AudioProcessorValueTreeState (이하 ValueTree)는 프로그램에서 사용할 변수 (parameter)를 등록하고 외부 DAW에서 인식하고 연동할 수 있게 해줍니다. Processor에서 선언한 뒤에, 사용할 parameter를 constructor에서 등록해주면 됩니다.

 

class AudioProcessor
{
    //...
private:
    juce::AudioProcessorValueTreeState parameters;
    //...
}

AudioProcessor::AudioProcessor()
    : parameters(*this, nullptr, juce::Identifier("Params"),
      {
          std::make_unique<juce::AudioParameterFloat>("volume", "Volume", 0.0f, 1.0f, 0.5f),
          std::make_unique<juce::AudioParameterBool>("id", "Name", false),
      }
{
   //...
}

ValueTree의 constructor가 좀 복잡합니다.

순서대로 등록할 Processor, undoManager, ValueTree의 Identifier, parameter list입니다.

parameter list에는 사용할 parameter를 넣어주면 됩니다.

종류에는 AudioParameterFloat, AudioParameterBool, AudioParameterChoice (string list), AudioParameterInt가 있습니다.

각각의 constructor가 다르긴 한데, Float을 기준으로 id, name, min value, max value, default value입니다.

뭐 이런 건 코드 힌트나 document를 보면 되니까요.

 

알아둘 점은, ValueTree에 등록하는 Parameter는 그 후로 Range (min/max value)를 변경할 수 없습니다.

외부에서 automation 등을 적용할 필요가 없는 값이라면 그냥 값을 별도로 저장하면 되지만,

그렇지 않다면 Normalized Range로 저장하고, 사용할 때는 다시 range에 맞게 변환해줘야 됩니다.

 

사실 등록하는 Parameter는 모두 따로 Range 관련 클래스를 생성해서 사용하는 게 아닌 한, 자동으로 NormalisedRange를 생성해서 사용합니다.

그러므로 저장 및 불러오기를 할 때 값이 이상하다고 생각된다면, Normalized value를 그대로 불러온 것은 아닌지 확인하고 알맞은 값으로 변환해주시기 바랍니다.

 

이렇게 등록한 parameter는 ValueTree의 데이터를 저장하고 불러오면 연동됩니다.

void getStateInformation(juce::MemoryBlock& destData) override
{
    auto state = parameters.copyState();
    std::unique_ptr<juce::XmlElement> xml (state.createXml());
    copyXmlToBinary (*xml, destData);
} //save

void setStateInformation(const void* data, int sizeInBytes) override
{
    std::unique_ptr<juce::XmlElement> xmlState (getXmlFromBinary (data, sizeInBytes));

    if (xmlState.get() != nullptr)
        if (xmlState->hasTagName (parameters.state.getType()))
            parameters.replaceState (juce::ValueTree::fromXml (*xmlState));
} //load

튜토리얼의 코드 그대로입니다.

유의할 점은, GUI와 parameter의 연동도 따로 해줘야한다는 것입니다.

Attachment를 이용하면 되는데, 이건 GUI를 할 때 다시 이야기합시다.

 

경우에 따라서 Parameter 이외의 값을 저장할 필요가 있습니다.

 

외부에서 값을 수정할 수 없게 하고 싶을 때나, parameter로 표현하기 어려운 값은 이렇게 저장해야 합니다.

예를 들면 파일을 불러와서 사용하는 Plugin같은 경우에는 그 파일의 경로는 단일 string 값이기 때문에 별도로 저장하고 불러오는 게 좋겠습니다.

 

이런 경우에는 XML 파일을 작성하듯이 태그를 추가해서 저장해주면 됩니다.

void getStateInformation(juce::MemoryBlock& destData) override
{
    auto state = parameters.copyState();
    juce::XmlElement* root = new juce::XmlElement("root");

    std::unique_ptr<juce::XmlElement> xml(state.createXml());

    root->addChildElement(xml.get());
    xml.release();

    //custom
    juce::XmlElement* preset = new juce::XmlElement("presetID");
    preset->addTextElement(juce::String(presetID));
    root->addChildElement(preset);

    std::unique_ptr<juce::XmlElement> to_save(root);
    copyXmlToBinary(*to_save, destData);
}

void setStateInformation (const void* data, int sizeInBytes) override
{
    std::unique_ptr<juce::XmlElement> xml(getXmlFromBinary(data, sizeInBytes));

    if (xml.get() != nullptr)
    {
        if (xml->hasTagName("root"))
        {
            juce::XmlElement* valueTree = xml->getChildByName(parameters.state.getType());

            if (valueTree == nullptr)
                return;

            parameters.replaceState(juce::ValueTree::fromXml(*valueTree));

            //custom
            juce::XmlElement* preset = xml->getChildByName("presetID");
            if (preset != nullptr)
            {
                juce::XmlElement* id = preset->getFirstChildElement();
                if (id != nullptr && id->isTextElement())
                    presetID = id->getText().getIntValue();
            }
        }
    }
}

 

ValueTree의 parameter는 어지간하면 Attachment로 연결된 GUI에서 값을 수정할 때 연동되지만,

직접 값을 저장하고 싶을 때가 있습니다.

저같은 경우에는 버그인지 코드가 꼬였는지 Attachment에서 연동이 잘 안 되더군요.

 

void saveStateForce()
{
    parameters.getParameter("volume")->setValueNotifyingHost(volume);
}

valueTreeState에서 해당 id의 parameter를 가져온 뒤에 setValueNotifyingHost를 호출해주면 됩니다.

parameter로 저장할 값을 넣어주세요.

참고로, setValue가 있고 setValueNotifyingHost가 있는데 JUCE에서는 후자를 사용하라고 하네요.

전자는 시스템 내부에서 사용하는 함수고, user level에서는 후자로 요청을 해야되나 봅니다.

 

이제 GUI 파트가 남았네요. 이건 다음 포스팅을 넘기겠습니다.

GUI를 그리는 것보다는, GUI와 Processor 연동을 주로 다룰 것 같습니다.

그 뒤에 잡다한 팁들 정리하고, 얼른 본업으로 돌아와야겠습니다.