カスタムノードの実装
Contents
カスタムノードとは
カスタムノードとは, 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::
カスタムノードを保存と復元に対応させるには, GPDSPSerializable クラスを継承したクラスを作成し, GPDSPSerializable::
アプリケーションを表すクラスのヘッダファイルの例
#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", ¶m)) == 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::
GPDSPNodeRenderer::
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>