LV2プラグインの作り方 (1) 簡単なオーバードライブ

394
NO IMAGE

LV2とはLADSPA Version 2の略らしく[^1]、Linux上で音楽制作をする上で必要なDAWのプラグインは概ねこれで書かれているようだ。^2そして実際に作ってみたら案外簡単に作れたので、備忘録がてら書いておくことにする。詳細な情報は公式のドキュメントを読むべし。

LV2プラグインの構成

LV2プラグインは、プラグイン本体のバイナリファイル(Linuxならまあ大体.so)とマニフェストファイル(manifest.ttl)から成り立っている。マニフェストファイルはプラグインの仕様を記述するもので、プラグインの入出力ポート−−オーディオ入出力の他プラグインの動作の調整もポート経由−−の定義などを行うものになっている。

そしてこのマニフェストファイル、ファイル形式がなんとTurtleである。確かにプラグインの仕様をフォーマルに記述するとなるとRDF[^3]に行き着くのは妥当ではないとは言えないし、RDFを手書きするぐらいならTurtleを書くので確かに選択肢としてなくはない……気がしなくもない。^4

調べてみた限りでは、LV2プラグインの多くはmanifest.ttlにはファイル構成だけを書き、プラグインの実際の定義は別の.ttlファイルに記述するのが主流のようだ。つまり以下のようなファイル構成になる。

plugin-directory/
    manifest.ttl
    plugin.ttl
    plugin.so

とりあえずオーバードライブを作ってみる

私はギタリストなので、ギタリストがまず最初に作るエフェクターは歪み系と相場が決まっている。^5いや歪みと言ってもその意味する所は1つではないが、ここでの歪みはオーバードライブなので非線形歪み、即ち入力と出力が相似形にならない歪みを指す。もう少し詳細に書けば、ギターアンプやエフェクターにおける歪みというのは、概ね回路への過負荷で波形がクリップしたものと、原音を元にさらなる倍音が付加されたものになる。

screenshot_2020-03-17_04-48-38.png

上記の画像は元の波形と2種類の歪み系エフェクトによるサウンドの例だ。最初の波形が原音、2番目がオーバードライブで歪ませたもの、3番目がディストーションで歪ませたものとなっている。クリッピングにはソフトクリップと呼ばれるある程度なだらかに波形を頭打ちにして均すものと、ハードクリップと呼ばれるスレッショルドでスパッと切るように均すものがあるが、オーバードライブはソフトクリップに近く、ディストーションになるとハードクリップに近いと言えそうだ。^6

実機のエフェクターは過大に入力を上げた音をクリッピング素子に通して波形をクリップさせて歪みを得るなどしているが、ここではプラグインによる歪みなのでダイオードなど実際のクリッピング素子の挙動をシミュレーションして……というのは大変なので、サンプルプログラムだし適当にクリップさせることにする。

また多くのオーバードライブ製品は歪みの具合の他に音色をある程度変えられるようにToneなどというノブが付いているものが多いが、これは要するにイコライザーできちんと実装するのが少々大変というかQiitaにサンプルコードを丸々載せるのがタルすぎるので、今回作るオーバードライブは:

  • Gain: クリッピング回路への入力
  • Volume: 最終的な出力の調整。ここでは非線形な歪みは与えない。

以上2つのパートからのみのシンプル極まりないものにする。

マニフェストファイルの作成

ファイル構成のmanifest.ttlは以下のようになる。^7

@prefix lv2:  <http://lv2plug.in/ns/lv2core#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<http://return0.info/plugins/tube-fucker>
        a lv2:Plugin ;
        lv2:binary <tube-fucker.so>  ;
        rdfs:seeAlso <tube-fucker.ttl> .

@prefixはXMLの名前空間と同じ。それ以降のファイル内容は

<主語>
  述語 目的語 (; 述語 目的語)* .

という並びになる。自然言語で書き下せば

http://return0.info/plugins/tube-fuckerは
  lv2:pluginである
  tube-fucker.soがlv2:binaryである
  tube-fucker.ttlを参照せよ

ぐらいの意味合いに取れば良い。また上記のファイルは以下の記述と同様。

<http://return0.info/plugins/tube-fucker> a lv2:Plugin .
<http://return0.info/plugins/tube-fucker> lv2:binary <tube-fucker.so>  .
<http://return0.info/plugins/tube-fucker> rdfs:seeAlso <tube-fucker.ttl> .

つまり、セミコロンは同じ主語に対しての記述を続ける表記法という事。

続いてプラグインの定義本体のttlファイルだが、GainとVolumeしかないプラグインでもこの程度の記述になる。

@prefix doap:  <http://usefulinc.com/ns/doap#> .
@prefix lv2:   <http://lv2plug.in/ns/lv2core#> .
@prefix rdf:   <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs:  <http://www.w3.org/2000/01/rdf-schema#> .
@prefix units: <http://lv2plug.in/ns/extensions/units#> .
<http://return0.info/plugins/tube-fucker>
    a lv2:Plugin , lv2:DistortionPlugin ;
    doap:name "Tube Fucker";
    doap:license <http://opensource.org/licenses/isc> ;
    lv2:port [
        a lv2:InputPort , lv2:ControlPort ;
        lv2:index 0 ;
        lv2:symbol "gain" ;
        lv2:name "Gain"  ;
        lv2:default 3 ;
        lv2:minimum 0 ;
        lv2:maximum 6 ;
        units:unit units:db ;
        lv2:scalePoint [
            rdfs:label "0dB" ;
            rdf:value 0.0
        ] , 
        [
            rdfs:label "3dB" ;
            rdf:value 3
        ] , 
        [
            rdfs:label "6dB" ;
            rdf:value 6
        ] 
    ] , 
    [
        a lv2:ControlPort , lv2:InputPort  ;
        lv2:index 1 ;
        lv2:symbol "vol" ;
        lv2:name "Vol"  ;
        lv2:default 3.0 ;
        lv2:minimum 0.0 ;
        lv2:maximum 6.0 ;
        units:unit units:db ;
        lv2:scalePoint  [
            rdfs:label "0db" ;
            rdf:value 0.0
        ] , 
        [
            rdfs:label "3dB" ;
            rdf:value 3.0
        ] , 
        [
            rdfs:label "6dB" ;
            rdf:value 6.0
        ] 
    ] , 
    [
        a lv2:AudioPort , lv2:InputPort ;
        lv2:index 2 ;
        lv2:symbol "in" ;
        lv2:name "In"
    ] , 
    [
        a lv2:AudioPort , lv2:OutputPort ;
        lv2:index 3 ;
        lv2:symbol "out" ;
        lv2:name "Out"
    ] .

まず目を引くのはlv2:portの記述だが、この[]で囲む記法はTurtleにおける空白の主語と述語目的語リストの導入で、上記のファイルのlv2:portはさらにその空白の主語と述語目的語リストの組が目的語リストを形成している。無理やり自然言語に直すなら、

<http://return0.info/plugins/tube-fucker>
   のlv2:portは
     lv2:indexが0であり、lv2:symbolがgainであり(略)である入力及びコントロールポート
     lv2:indexが1であり、lv2:symbolがvolであり(略)である入力及びコントロールポート

といった意味合いになる。

プラグインの実装

実装言語はとりあえずCにする。公式のサンプルなんかがCで書かれていることだし。

インクルードファイル

#include <stdlib.h>
#include <math.h>
#include "lv2/lv2plug.in/ns/lv2core/lv2.h"

LV2のライブラリはモジュラ−アーキテクチャを採用していると同時に、そのヘッダファイルはURIのような形で配備されるようだ。実際、lv2coreのドキュメントは http://lv2plug.in/ns/lv2core/ から読める。

構造体など

#define PLUGIN_URI "http://return0.info/plugins/tube-fucker"

typedef enum {
    PEDAL_GAIN   = 0,
    PEDAL_VOL    = 1,
    PEDAL_INPUT  = 2,
    PEDAL_OUTPUT = 3
} Ports;

typedef struct {
    const float* gain;
    const float* volume;
    const float* input;
    float* output;
} TubeFucker ;

float apply(const TubeFucker* pedal, const float a);
  • PLUGIN_URIの定義はやはりttlの主語と一致している必要があり、これが間違っているとプラグインのロードに失敗する。
  • Ports列挙型はコードの読みやすさのために定義している。
  • この構造体は先のttlファイルで定義したポートを紐付ける役割となっている。出力ポート以外はいずれも書き込み不可能なので、constポインタとして宣言している。
  • applyは実際のオーディオ処理関数のプロトタイプ宣言で、これは説明の順序的にここに入れてあるだけ。

LV2のお作法

static LV2_Handle instantiate(
    const LV2_Descriptor*     descriptor,
    double                    rate,
    const char*               bundle_path,
    const LV2_Feature* const* features) {
    TubeFucker* instance = (TubeFucker*)malloc(sizeof(TubeFucker));
    return instance;
}

instantiateはプラグインの状態管理などを行う構造体のポインタを返す。

static void connect_port(LV2_Handle instance,
             uint32_t   port,
             void*      data)
{
    TubeFucker* pedal = (TubeFucker*)instance;
    switch ((Ports)port) {
    case PEDAL_GAIN:
            pedal->gain = (const float*)data;
            break;
    case PEDAL_VOL:
            pedal->volume = (const float*)data;
            break;
    case PEDAL_INPUT:
            pedal->input = (const float*)data;
            break;
    case PEDAL_OUTPUT:
            pedal->output = (float*)data;
            break;
    }
}

connect_portはLV2ホストより渡されるポートを先の構造体に紐付ける。

static void activate(LV2_Handle instance)
{
}

static void deactivate(LV2_Handle instance) {
}

activate/deactivateは対の存在で、activateはプラグインの実行前に呼ばれ、主にconnect_portで処理されるもの以外の状態リセットを行うようだ。deactivateはプラグインの処理が終わった後に呼び出され、一旦deactivateが呼び出されるとactivateが再度呼び出されるまではプラグインは動作しないようだ。今回は特にポートを介さない状態管理を行っていないので、これらは空にしておく。

static void run(LV2_Handle instance, uint32_t n_samples)
{
    const TubeFucker* pedal = (const TubeFucker*)instance;
    const float* const input  = pedal->input;
    float* const       output = pedal->output;

    for (uint32_t pos = 0; pos < n_samples; pos++) {
        output[pos] = apply(pedal, input[pos]);
    }
}

プラグインの処理本体。n_samplesは実行しているホストの渡してくるオーディオ信号のサンプル数で、このサンプル数分だけ繰り返し入力バッファに対して処理を行い、結果を出力バッファに格納する必要がある。

static void cleanup(LV2_Handle instance) {
    free(instance);
}

最終的なプラグインの破棄処理。今回はfreeを呼ぶだけで良い。

static const void* extension_data(const char* uri)
{
    return NULL;
}

LV2プラグインは拡張データを持つことができるが、このプラグインには存在しないのでNULLを返す。

static const LV2_Descriptor descriptor = {
        PLUGIN_URI,
        instantiate,
        connect_port,
        activate,
        run,
        deactivate,
        cleanup,
        extension_data
};

LV2_SYMBOL_EXPORT const LV2_Descriptor* lv2_descriptor(uint32_t index)
{
        switch (index) {
        case 0:  return &descriptor;
        default: return NULL;
        }
}

全てのLV2プラグインは1つ以上のLV2_Descriptorを定義し、lv2_descriptor関数から返さなければならない。ここでは1つだけDescriptorを定義してそれを返すだけである。複数のDescriptorは、例えばモノラルとステレオで2つのプラグインを提供する場合などに使うようだ。

オーバードライブ処理本体

float apply(const TubeFucker* pedal, const float a) {
    /* [1] */
    float d = (a + powf(a, 3)) * powf(40.0f, (*pedal->gain) * 0.05f) * 10.0f;
    /* [2] */
    if (d > 1) {
        d = 2.0f/3.0f;
    } else if (d < -1) {
        d = -2.0f/3.0f;
    } else {
        d = d - powf(d, 3)/ 3;
    }
    d = d / 10.0f;
    /* [3] */
    return d * powf(10.0f, (*(pedal->volume)) * 0.05f);
}

[1]の処理は単なる入力信号の増幅で完全に適当な倍率で増幅している。低めの入力音量でも後述のクリッピング処理で多少はクリップさせたいので、気持ち多めにゲインを稼いでいる。

[2]の処理は簡単なソフトクリップ処理になっている。スレッショルドに達する前の入力信号はある程度の割合でゲインを削り、スレッショルドに到達したら値を一律固定値にクリップしている。前段の処理でかなり過剰にゲインを稼いでいるので、バランスを取るためにクリッピング後の信号のゲインを適当に下げている。

[3]は単なるアンプ。gainも実際の所そうだが、volumeは物理量の比を対数スケールで表現したデシベルで指定しているので、それを電圧比に直している。^8

サンプル音源

OSSのアンプシミュレーターであるGuitarixと組み合わせて適当に弾いた音源。適当に作っても案外オーバードライブっぽくなるもんだ。


大分駆け足だったが、このぐらいシンプルなオーバードライブであればさほど難しいわけではなかった。(プラグインの作成よりもこの記事の執筆のほうが大分面倒だった。)勿論複雑なプラグインの作成は相応に難しいが、まあ趣味で音を弄る程度であればそこまでハードルは高くはないかなと思う。

オマケ:Turtleのバリデーション

Turtleファイルは書き慣れていないと思わぬチョンボを引き起こすので、何かしらバリデーターが欲しくなる。そして実際にsord_validateというツールがある。

Ubuntuならapt install sordiでインストールできるはず。今回作ったプラグインのTurtleファイルをバリデーションする場合、

そして以下のコマンドを実行すればバリデーションできる。

sord_validate $(find /path/to/lv2/repo -name '*.ttl') $(find /path/to/doap/download_dir -name '*.ttl') $(find /path/to/your/ttl -name '*.ttl')

[^1]: まあLADSPA自体がLinux Audio Developers Simple Plugin APIの略なんだが

[^3]: Resource Description Framework。15年ぐらい前は私も割とセマンティックウェブの夢を見ていたなあ……。