NTS-1からNTS-1 mk2への移植ガイド(OSC編)

NTS-1で動作するエフェクタ類のNTS-1 mkIIへの移植について、以前記事を書きましたが、今回はオシレータ編です。

上の記事を見ながら、Waves2をNTS-1 mkIIに移植してみましたので、本記事はその経験に基づいています。基本的にはエフェクタの場合とあまり大きくは変わらないのですが、APIの違いがありますので別記事にしておきます。

ファイル構成

新旧を比較すると、主な構成ファイルは以下のように異なります。

内容 NTS-1 NTS-1 mk2
メイン 任意(.c, .cpp) 任意(.c, .cc)
ビルドオプション project.mk config.mk
パラメータ設定等 manifest.json header.c
ビルドスクリプト Makefile Makefile
リンカスクリプト ld/ 不要
スタブ tpl/_unit.c 不要

いずれも記述内容に共通性があるというだけで、ファイルそのものには互換性が無いので、すべて作り直しが必要です。
そんなわけで、まず移植の初手で以下を実行します。C++についてはMakefileの中で指定されている拡張子が異なる(旧SDKはcpp、新SDKはcc)ので、すべてリネームしておきます。

git switch -c nts1mkii
git rm -r Makefile project.mk ld tpl
cp ../dummy-osc/Makefile ../dummy-osc/config.mk ../dummy-osc/header.c .
git mv your-main-src-code.cpp your-main-src-code.cc

ちなみにmanifest.jsonも不要ですが、header.cを作るときに有用ですので上記では削除していません。

config.mkの修正

config.mkはプロジェクト名を変更します。現状は

PROJECT := dummy_osc

となっているはずです。

また、

UCXXSRC = unit.cc

となっていますが、unit.ccを自分のメインのソースコードのファイル名に変更します。

header.cの修正

これは旧SDKよりも記述内容が増えていますので、新規のつもりで作成します。必須の変更点は

.dev_id(デベロッパID)
.unit_id(ユニットID)
.name(ユニット名)
.num_params(パラメータ数)
パラメータの定義

です。デベロッパIDは、自分のプラグインを配布するつもりなら、あったほうがいいです。旧SDKではプラグインを識別する方法がなかったのですが、新SDKではデベロッパIDとユニットIDの組でプラグインを識別できるからです。デベロッパIDを登録する場合はこちらのファイルに対してプルリクエストで要求してください。

パラメータの個数は、SDK1.1と異なりshapeとshiftshapeを含みますので、最小2個最大8個です。パラメータの定義のうち最初の2つのパラメータの記述は、shapeとshiftshapeに割り当てられますので、とりあえずデフォルトのまま(0~1023)にしておきます。

        // A knob
        {0, 1023, 0, 0, k_unit_param_type_none, 0, 0, 0, {"SHPE"}},
        // B knob
        {0, 1023, 0, 0, k_unit_param_type_none, 0, 0, 0, {"ALT"}},

3番目以降のパラメータ(最大6個)の内容はmanifest.jsonを見ながら作成します。ちなみに旧APIでは、表示が「パラメータの実際の値+1」になります(例えば値が0のとき、ディスプレイには1と表示されます)ので、同じように表示させたい場合はパラメータの形式としてk_unit_param_type_enumを選んでください。(なおNTS-1には、パラメータの最小値が0以外の場合は、表示の際に数値に+1されず、そのままの値が表示されてしまうバグがあります。)
header.cが作成出来たら、manifest.jsonは削除してOKです。

ソースコードの変更

ソースコードについてはAPIの変更に対応して何か所か変更する必要があります。以下、主なものを挙げていきます。

インクルードファイルのファイル名の変更

旧:userosc.h
新:unit_osc.h
旧:biquad.hpp  delayline.hpp  simplelfo.hpp
新:dsp/biquad.hpp  dsp/delayline.hpp  dsp/simplelfo.hpp
旧:buffer_ops.h  cortexm.h  fixed_math.h  float_math.h  int_math.h
新:utils/buffer_ops.h  utils/cortexm.h  utils/fixed_math.h  utils/float_math.h  utils/int_math.h

fast_inlineマクロ

旧SDKと新SDKでマクロの名称が違っています。名称を直すか、旧APIの名称で宣言しなおすことが必要です。

旧:
#define __fast_inline static inline __attribute__((always_inline, optimize("Ofast")))
新:
#define fast_inline inline __attribute__((always_inline, optimize("Ofast")))
対処(例):
#define __fast_inline fast_inline

・パラメータ用定数
旧APIでは、パラメータのIDが事前に以下のように定義されていました。

  typedef enum {
    k_user_osc_param_id1 = 0,
    k_user_osc_param_id2,
    k_user_osc_param_id3,
    k_user_osc_param_id4,
    k_user_osc_param_id5,
    k_user_osc_param_id6,
    /** Shape parameter */
    k_user_osc_param_shape,
    /** Alternative Shape parameter: generally available via a shift function */
    k_user_osc_param_shiftshape,
    k_num_user_osc_param_id
  } user_osc_param_id_t;

NTS-1 mkIIではSDKのヘッダファイルでは、2つのパラメータだけが以下のように定義されています。

  enum {
    k_unit_osc_fixed_param_shape = 0,
    k_unit_osc_fixed_param_altshape,
    k_num_unit_osc_fixed_param_id
  };  

従って、NTS-1用のソースコードで使われている名称を流用するなら、以下のような定義をソースコードに含めることが必要です。

typedef enum {
    k_user_osc_param_shape = 0,
    k_user_osc_param_shiftshape,
    k_user_osc_param_id1,
    k_user_osc_param_id2,
    k_user_osc_param_id3,
    k_user_osc_param_id4,
    k_user_osc_param_id5,
    k_user_osc_param_id6,
    k_num_user_osc_param_id
} user_osc_param_id_t;

・ランタイムデスクリプション用変数
ランタイムデスクリプションは初期化関数で引数として渡されるので、この構造体をスタティック変数に保持します。オシレータのピッチはこのスタティック変数を通じて取得するのが標準的な方法ですので、この変数は必ず必要です。

static unit_runtime_desc_t s_desc;

初期化関数(OSC_INIT)

関数名、パラメータが変わっているので変更します。渡されたパラメータdescは、上で宣言したスタティック変数にコピーしておきます。

旧:
void OSC_INIT(uint32_t platform, uint32_t api)
新:
__unit_callback int8_t unit_init(const unit_runtime_desc_t * desc)

サンプルで提供されているダミーのコードでは初期化関数の中で環境チェックを行っていますが、これはダミーのコードをそのまま使うのが良いです。
また、この初期化関数はSDK1と異なり、戻り値がありますので、最後に

    return k_unit_err_none;

しておく必要があります。まとめるとこんな感じです。もちろんこのほかに、ユニット固有の変数も初期化しておく必要があります。

__unit_callback int8_t unit_init(const unit_runtime_desc_t * desc)
{
    if (!desc)
      return k_unit_err_undef;

    if (desc->target != unit_header.target)
      return k_unit_err_target;

    if (!UNIT_API_IS_COMPAT(desc->api))
      return k_unit_err_api_version;

    if (desc->samplerate != 48000)
      return k_unit_err_samplerate;

    if (desc->input_channels != 2 || desc->output_channels != 1) 
      return k_unit_err_geometry;
               
    s_desc = *desc;
    return k_unit_err_none;
}

パラメータ値の変更(OSC_PARAM)

パラメータが変更された際に呼ばれる関数は、以下のように関数名だけが変わっています。

旧:void OSC_PARAM(uint8_t index, int32_t value)
新:__unit_callback void unit_set_param_value(uint8_t id, int32_t value)

ShapeおよびShift Shapeの値域は0~1023で変更はありません。浮動小数点数への変換も同一の名称param_val_to_f32()で用意されています。

信号処理(OSC_CYCLE)

メインとなる信号処理の関数は、新APIでは、エフェクトの種類に関わらず同一の関数名となっています。旧APIで渡されていた引数paramsは無くなりました。代わりに、初期化関数の中で渡されたランタイムデスクリプションを使用します。このパラメータにはhooks.runtime_contextというメンバ変数があり、これが旧APIにおけるparamsと同等の情報を提供します。

旧:
void OSC_CYCLE(const user_osc_param_t * const params,
               int32_t *yn,
               const uint32_t frames)

新:
__unit_callback void unit_render(const float * in, float * out, uint32_t frames)
{
  const unit_runtime_osc_context_t *ctxt = static_cast(s_desc.hooks.runtime_context);

また、大きな違いとしてバッファに入れるデータ型は、旧APIでは固定小数点(q31_t型)だったのですが、新APIではfloat型になります。従って、バッファ関連でfloatとq31_tの変換を行っていたコードはすべて削除する必要があります。(LFOなどのパラメータ関連は引き続き固定小数点です。)

旧:
q31_t * __restrict y = (q31_t *) yn;
const q31_t * y_e = y + frames;

        *(y++) = f32_to_q31(sig);

新:
float * __restrict y = out;
const float * y_e = y + frames;

        *(y++) = sig;

以下の記事も参考にしてください。

NTS-1 mk2でオシレータを作ってみた(追記あり)
NTS-1 mk IIではSDKがアップデートされています。SDKの概要は前回の記事に書きましたが、今回は試しにオシレータを作ってみました。 以前、旧NTS-1とdrumlogue用に作った、以下のシンプルなオシレータを、mk2に移植してみ...

ノートイベント関連(OSC_NOTEON, OSC_NOTEOFF)

これらは旧APIで行えるのは、イベントが起きたことを検知するだけだったのですが、新APIではノート番号とベロシティを取得できるようになりました。移植という観点では、上記信号処理同様、初期化時に渡されたランタイムデスクリプションを参照すれば、旧APIのパラメータと同様の情報を参照できます。

旧:
void OSC_NOTEON(const user_osc_param_t * const params) {}
void OSC_NOTEOFF(const user_osc_param_t * const params) {}

新:
__unit_callback void unit_note_on(uint8_t note, uint8_t velo) {}
__unit_callback void unit_note_off(uint8_t note) {}

新規に追加されたコールバック関数

新APIでは実装しなければならないコールバック関数が増えています。しかし、移植にあたってはほとんどの関数は中身がカラッポでも動作はするはずです。ただし、空でも関数自体は定義しないと、ビルドは通ってもKONTROL Editorにロードしようとした時点でKONTROL Editorが落ちます。

返り値がある関数はカラッポというわけにはいきませんが、そのような関数はunit_get_param_value(),unit_get_param_str_value()の2つだけです。後者はとりあえずnullptrを返せば問題ありません。前者は指定されたパラメータの値を返す必要がありますが、私は以下のようにしています。

// 値の保存用のスタティック変数の宣言
static int32_t params[k_num_user_osc_param_id];

// 値の保存
_unit_callback void unit_set_param_value(uint8_t id, int32_t value)
{
    params[id] = value;
    // your code here
}
// 保存した値の読み出し
__unit_callback int32_t unit_get_param_value(uint8_t id) {
    return params[id];
}

これらも含めた、定義が必要な関数は以下の通りです。

_unit_callback void unit_teardown() {
}

__unit_callback void unit_reset() {
}

__unit_callback void unit_resume() {
}

__unit_callback void unit_suspend() {
}

__unit_callback int32_t unit_get_param_value(uint8_t id) {
    return params[id];
}

__unit_callback const char * unit_get_param_str_value(uint8_t id, int32_t value\
) {
    return nullptr;
}

__unit_callback void unit_set_tempo(uint32_t tempo) {
}

__unit_callback void unit_tempo_4ppqn_tick(uint32_t counter) {
}

__unit_callback void unit_all_note_off() {
}

__unit_callback void unit_pitch_bend(uint16_t bend) {
}

__unit_callback void unit_channel_pressure(uint8_t press) {
}

__unit_callback void unit_aftertouch(uint8_t note, uint8_t press) {
}

以上が移植作業の概要です。

Waves2のNTS-1 mkII版はリポジトリのnts1mkiiブランチで公開しています。バイナリはリリースページにあります。私が実際に移植を行った際の差分が参考になるかもしれませんので、以下にその差分へのリンクを貼っておきます。

first commit of nts1mkii port · boochow/Waves2@f7fc453
a simple wavetable synthesizer for Korg logue SDK. runs on prologue, minilogue xd, NTS-1 - first commit of nts1mkii port...

コメント