カスタムノードの実装

カスタムノードとは

カスタムノードとは, GPDSP ライブラリにあらかじめ用意されたノード以外の, 開発者が独自に定義した具象ノードを指します. いくつかの規則に従ってカスタムノードを実装することにより, あらかじめ用意されたノードと同じ操作性を得ることができます.

新しいクラスの実装

カスタムノードを作成するには, 新しいクラスを定義し, 必要な仮想関数をオーバーライドします.

データの入力を受け付けるときには GPDSPInputtableNode クラスを継承し, データの出力を行うときには GPDSPOutputtableNode クラスを継承します. GPDSPInputtableNode クラスと GPDSPOutputtableNode クラスは, それぞれ, 入力ターミナルの個数と出力ターミナルの個数がノードの生成時に決定される場合に利用します. ノードの生成後に入力ターミナルの個数と出力ターミナルの個数を自由に変更できるようにする場合は, GPDSPFlexInputtableNode クラスと GPDSPFlexOutputtableNode クラスを利用します. これらのクラスは public 継承して利用します.

また, カスタムノードが何らかの巻き戻しの機能を持つ場合には GPDSPRewindableNode クラスを継承し, カスタムノードが何らかの再初期化の機能を持つ場合には GPDSPRefreshableNode クラスを継承します. これらのクラスは public 仮想継承して利用します.

新しいクラスで実装しなければいけない仮想関数の種類は継承したクラスに応じて決定されるため, 必要となる仮想関数をそれぞれ適切に実装します.

次にカスタムノードのプログラム例を示します.

新しいクラスのヘッダファイルの例

#include "GPDSP.hpp"

using namespace ir;

class myClickerNode : public GPDSPInputtableNode,
public GPDSPOutputtableNode, public virtual GPDSPRefreshableNode {
    private:

                // 左チャンネルと右チャンネルを同期するかどうか
                bool            _interlock;

                // オーバーフローとなる限界値
                GPDSPFloat      _overflow;

                // 左チャンネルの積算値
                GPDSPFloat      _lload;

                // 右チャンネルの積算値
                GPDSPFloat      _rload;

    public:

        // コンストラクタとデストラクタ
        explicit                myClickerNode       (void) noexcept;
        virtual                 ~myClickerNode      (void) noexcept;

        // デフォルト値を取得するための関数
        static  bool            defaultInterlock    (void) noexcept;
        static  GPDSPFloat      defaultOverflow     (void) noexcept;

        // 左チャンネルと右チャンネルの同期に関連する関数
                void            setInterlock        (bool interlock) noexcept;
                bool            getInterlock        (void) const noexcept;

        // オーバーフローとなる限界値に関連する関数
                void            setOverflow         (GPDSPFloat overflow) noexcept;
                GPDSPFloat      getOverflow         (void) const noexcept;

        // 実装しなければいけない仮想関数
        virtual GPDSPError      fixate              (void) noexcept;
        virtual void            invalidate          (void) noexcept;
        virtual GPDSPError      prepare             (void) noexcept;
        virtual GPDSPError      process             (void) noexcept;

        // GPDSPRefreshableNode クラスを継承した場合に実装しなければいけない仮想関数
        virtual void            refresh             (void) noexcept;
    private:

        // インスタンスのコピーと代入を禁止するための関数宣言
                                myClickerNode       (myClickerNode const&);
                myClickerNode&  operator=           (myClickerNode const&);
};
  • fixate(), prepare(), process() 関数は GPDSPInputtableNode クラスや GPDSPOutputtableNode クラスを継承する場合に実装が必要になります
  • invalidate() 関数は GPDSPInputtableNode クラスと GPDSPOutputtableNode クラスなど, どちらも invalidate() 関数を持つクラスを同時に継承する場合に, 明示的な実装が必要になります
  • refresh() 関数は GPDSPRefreshableNode クラスを継承する場合に実装が必要になります
  • ノードに固有の開発者が操作可能な変数が存在する場合は, セッター, ゲッター, デフォルト値のゲッターの3種類の関数をそれぞれの変数について実装します
  • ノードのインスタンスのコピーと代入を禁止するために, コピーコンストラクタと代入演算子を private で宣言します

新しいクラスのソースファイルの例

#include "myClickerNode.hpp"

myClickerNode::myClickerNode(void) noexcept :

    // 左チャンネルと右チャンネルの同期をデフォルト値に初期化
    _interlock(defaultInterlock()),

    // オーバーフローとなる限界値をデフォルト値に初期化
    _overflow(defaultOverflow())
{
    // 左チャンネルと右チャンネルの積算値を初期化
    _lload = GPDSPFV(0.0);
    _rload = GPDSPFV(0.0);
}

myClickerNode::~myClickerNode(void) noexcept
{
    // 何もしない
}

bool myClickerNode::defaultInterlock(void) noexcept
{
    return false;
}

GPDSPFloat myClickerNode::defaultOverflow(void) noexcept
{
    return GPDSPFV(500.0);
}

void myClickerNode::setInterlock(bool interlock) noexcept
{
    // 左チャンネルと右チャンネルの同期の状態が変更される場合は,
    // invalidate() 関数を呼び出して演算結果を無効化し再演算を要求
    if (interlock != _interlock) {
        _interlock = interlock;
        invalidate();
    }
    return;
}

bool myClickerNode::getInterlock(void) const noexcept
{
    return _interlock;
}

void myClickerNode::setOverflow(GPDSPFloat overflow) noexcept
{
    // オーバーフローとなる限界値の状態が変更される場合は,
    // invalidate() 関数を呼び出して演算結果を無効化し再演算を要求
    if (overflow != _overflow) {
        _overflow = overflow;
        invalidate();
    }
    return;
}

GPDSPFloat myClickerNode::getOverflow(void) const noexcept
{
    return _overflow;
}

GPDSPError myClickerNode::fixate(void) noexcept
{
    GPDSPError error(GPDSPERROR_OK);

    // 初めに入力ターミナルと出力ターミナルをすべて破棄
    clearO();
    clearI();

    // 入力ターミナルを作成
    if ((error = appendI("Lch-in")) == GPDSPERROR_OK) {
        if ((error = appendI("Rch-in")) == GPDSPERROR_OK) {

            // 出力ターミナルを作成
            if ((error = appendO("Lch-out")) == GPDSPERROR_OK) {
                error = appendO("Rch-out");
            }
        }
    }

    // エラーが発生した場合は, 入力ターミナルと出力ターミナルをすべて破棄
    if (error != GPDSPERROR_OK) {
        clearO();
        clearI();
    }
    return error;
}

void myClickerNode::invalidate(void) noexcept
{
    // GPDSPInputtableNode クラスの invalidate() 関数と GPDSPOutputtableNode クラスの
    // invalidate() 関数は暗黙には区別がつかないので, 明示的に両方の関数を呼び出す
    GPDSPInputtableNode::invalidate();
    GPDSPOutputtableNode::invalidate();
    return;
}

GPDSPError myClickerNode::prepare(void) noexcept
{
    // 内部バッファを持たないため何もしない
    return GPDSPERROR_OK;
}

GPDSPError myClickerNode::process(void) noexcept
{
    GPDSPFloat lch;
    GPDSPFloat rch;
    bool lov;
    bool rov;
    GPDSPError error(GPDSPERROR_OK);

    // 入力ターミナルの値を取得
    if ((error = getValueI(0, &lch)) == GPDSPERROR_OK) {
        if ((error = getValueI(1, &rch)) == GPDSPERROR_OK) {

            // 左チャンネルと右チャンネルのそれぞれの振幅の絶対値を積算値に加算
            _lload += fabs(lch);
            _rload += fabs(rch);

            // 積算値がオーバーフローとなる限界値を超えたかどうかを検査
            lov = (_lload >= _overflow);
            rov = (_rload >= _overflow);

            // 左チャンネルと右チャンネルが同期されるとき,
            // どちらかのチャンネルが限界値を超えた場合にどちらも限界値を超えたことにする
            if (_interlock) {
                lov |= rov;
                rov |= lov;
            }

            // 左チャンネルが限界値を超えた場合
            if (lov) {

                // 左チャンネルの積算値を再初期化
                _lload = GPDSPFV(0.0);

                // 左チャンネルのクリック音を演算
                if (lch > GPDSPFV(0.0)) {
                    lch = +sqrt(+lch * GPDSPFV(1000.0)) / GPDSPFV(10.0);
                    lch = std::min(lch, GPDSPFV(+1.0));
                }
                else if (lch < GPDSPFV(0.0)) {
                    lch = -sqrt(-lch * GPDSPFV(1000.0)) / GPDSPFV(10.0);
                    lch = std::max(lch, GPDSPFV(-1.0));
                }
            }

            // 右チャンネルが限界値を超えた場合
            if (rov) {

                // 右チャンネルの積算値を再初期化
                _rload = GPDSPFV(0.0);

                // 右チャンネルのクリック音を演算
                if (rch > GPDSPFV(0.0)) {
                    rch = +sqrt(+rch * GPDSPFV(1000.0)) / GPDSPFV(10.0);
                    rch = std::min(rch, GPDSPFV(+1.0));
                }
                else if (rch < GPDSPFV(0.0)) {
                    rch = -sqrt(-rch * GPDSPFV(1000.0)) / GPDSPFV(10.0);
                    rch = std::max(rch, GPDSPFV(-1.0));
                }
            }

            // 出力ターミナルに値を設定
            if ((error = setValueO(0, lch)) == GPDSPERROR_OK) {
                error = setValueO(1, rch);
            }
        }
    }
    return error;
}

void myClickerNode::refresh(void) noexcept
{
    // 左チャンネルと右チャンネルの積算値を再初期化
    _lload = GPDSPFV(0.0);
    _rload = GPDSPFV(0.0);
    return;
}
  • ノードに固有の開発者が操作可能な変数が更新され, 入力と出力の関係性が変化する場合は, invalidate() 関数を呼び出して再演算を要求します
  • fixate() 関数は複数回呼び出される可能性があるために, 入力ターミナルと出力ターミナルを新しく作成する前にそれぞれのターミナルをすべて破棄します
  • invalidate() 関数の明示的な実装が必要なときは, 親クラスの invalidate() 関数を明示的に呼び出します
  • 内部バッファを持たないノードは prepare() 関数で行うべき処理がないため, 常に GPDSPERROR_OK を返却します
  • デジタル信号処理の具体的な演算は process() 関数で実装します

最後に, 上記のようにして作成した新しいクラスは, 次のようにして生成して利用することができます.

カスタムノードの生成と利用

using namespace ir;

GPDSPNodeRenderer dsp;

dsp.appendNode("clicker", std::make_shared<myClickerNode>());

内部バッファを持つノードの prepare() 関数と process() 関数の実装例

GPDSPError GPDSPDelayNode::prepare(void) noexcept
{
    return setValueO(0, _queue);
}

GPDSPError GPDSPDelayNode::process(void) noexcept
{
    GPDSPFloat value;
    GPDSPError error(GPDSPERROR_OK);

    if ((error = getValueI(0, &value)) == GPDSPERROR_OK) {
        _queue = value;
    }
    return error;
}

保存と復元への対応

新しいクラスを実装することによりカスタムノードを利用することができるようになりますが, 保存と復元への対応ができていない場合は, カスタムノードを含んだノード構成を GPDSPNodeRenderer::load() 関数を利用して復元したり, GPDSPNodeRenderer::save() 関数を利用して保存しようとすると GPDSPERROR_NO_SUPPORT となり失敗します.

カスタムノードを保存と復元に対応させるには, GPDSPSerializable クラスを継承したクラスを作成し, GPDSPSerializable::load() 関数と GPDSPSerializable::save() 関数をオーバーライドして実装します.

アプリケーションを表すクラスのヘッダファイルの例

#include "GPDSP.hpp"

using namespace ir;

class myApp : public GPDSPSerializable {
    ...
    public:
                                myApp   (void);
                                ~myApp  (void);
                void            doCopy  (void);
        ...

        // 復元を行うための関数
        virtual GPDSPError      load    (GPDSPNodeRenderer* renderer,
                                            std::string const& type,
                                            std::string const& name,
                                            int format,
                                            tinyxml2::XMLElement const* element) noexcept;

        // 保存を行うための関数
        virtual GPDSPError      save    (GPDSPNodeRenderer const& renderer,
                                            std::shared_ptr<GPDSPNode const> const& node,
                                            std::string const& name,
                                            tinyxml2::XMLElement* element) noexcept;
        ...
}

アプリケーションを表すクラスのソースファイルの例

using namespace ir;

// 復元を行うための関数
GPDSPError myApp::load(GPDSPNodeRenderer* renderer,
                          std::string const& type,
                          std::string const& name,
                          int format,
                          tinyxml2::XMLElement const* element) noexcept
{
    std::shared_ptr<myClickerNode> clicker;
    tinyxml2::XMLElement const* param;
    int interlock;
    GPDSPFloat overflow;
    GPDSPError error(GPDSPERROR_OK);

    // ノードの種類が実装したいノードであるかを検証
    if (type == "myClickerNode") {

        // gpdsp ファイルの記述内で値が指定されていない場合のためにデフォルト値を設定
        interlock = myClickerNode::defaultInterlock();
        overflow = myClickerNode::defaultOverflow();

        // tinyxml2 を利用してノードに固有の値を gpdsp ファイルから復元
        if ((param = element->FirstChildElement("param")) != NULL) {
            if ((error = GPDSPNodeRenderer::readTag(param, "interlock",
                             true, &interlock)) == GPDSPERROR_OK) {
                error = GPDSPNodeRenderer::readTag(param, "overflow",
                            true, format, &overflow);
            }
        }
        if (error == GPDSPERROR_OK) {

            // 例外を利用しない設計なので try ~ catch 構文で例外を捕捉しエラーに変換
            try {

                // カスタムノードのインスタンスを生成
                clicker = std::make_shared<myClickerNode>();
            }
            catch (std::bad_alloc const&) {
                error = GPDSPERROR_NO_MEMORY;
            }
            if (error == GPDSPERROR_OK) {

                // gpdsp ファイルから復元した値を設定
                clicker->setInterlock(interlock);
                clicker->setOverflow(overflow);

                // カスタムノードのインスタンスを GPDSPNodeRenderer クラスのインスタンスに登録
                error = renderer->appendNode(name, clicker);
            }
        }
    }
    else {

        // ノードの種類が一致しないときは GPDSPERROR_NO_SUPPORT を必ず返却
        error = GPDSPERROR_NO_SUPPORT;
    }
    return error;
}

// 保存を行うための関数
GPDSPError myApp::save(GPDSPNodeRenderer const& renderer,
                          std::shared_ptr<GPDSPNode const> const& node,
                          std::string const& name,
                          tinyxml2::XMLElement* element) noexcept
{
    std::shared_ptr<myClickerNode const> clicker;
    tinyxml2::XMLElement* param;
    GPDSPError error(GPDSPERROR_OK);

    // ノードの種類が実装したいノードであるかを検証
    if ((clicker = std::dynamic_pointer_cast<myClickerNode const>(node)) != NULL) {

        // tinyxml2 を利用してノードの種類をタグ名として設定
        element->SetName("myClickerNode");

        // tinyxml2 を利用してノードに固有の値を gpdsp ファイルに保存
        if ((error = GPDSPNodeRenderer::addTag(element, "param",
                         &param)) == GPDSPERROR_OK) {
            if ((error = GPDSPNodeRenderer::writeTag(param, "interlock",
                             clicker->getInterlock())) == GPDSPERROR_OK) {
                error = GPDSPNodeRenderer::writeTag(param, "overflow",
                            clicker->getOverflow());
            }
        }
    }
    else {

        // ノードの種類が一致しないときは GPDSPERROR_NO_SUPPORT を必ず返却
        error = GPDSPERROR_NO_SUPPORT;
    }
    return error;
}

最後に GPDSPSerializable クラスを継承したクラスのインスタンスへのポインタを GPDSPNodeRenderer::load() 関数や GPDSPNodeRenderer::save() 関数の第2引数に設定して呼び出します.

GPDSPNodeRenderer::load() 関数と GPDSPNodeRenderer::save() 関数の呼び出し方

using namespace ir;

void myApp::doCopy(void)
{
    GPDSPNodeRenderer dsp;

    dsp.load("custom.gpdsp", this);
    dsp.save("custom_copy.gpdsp", this);
    return;
}

gpdsp ファイルでの記述

カスタムノードの gpdsp ファイルでの記述例

<myClickerNode>
    <name>ノード名</name>
    <param>
        <interlock>左チャンネルと右チャンネルを同期するかどうか</interlock>
        <overflow>オーバーフローとなる限界値</overflow>
    </param>
    <input>
        <::0>
            <node>Lch-in に対する入力元のノード名</node>
            <output>::Lch-in に対する入力元のターミナル番号</output>
        </::0>
        <::1>
            <node>Rch-in に対する入力元のノード名</node>
            <output>::Rch-in に対する入力元のターミナル番号</output>
        </::1>
    </input>
</myClickerNode>