LV2 adalah standar protokol buat plugin audio, mirip seperti VST. Hari ini saya mencoba untuk memahami cara membuat sebuah LV2 plugin yang sederhana dari contoh yang ada di repo resminya. Dokumentasi yang tersedia di sana mudah diikuti. Saya hanya mencoba merangkum di sini.
Coba Download dan Compile
Saya coba clone source codenya dari repo resmiya.
$ git clone https://gitlab.com/lv2/lv2.git
Ternyata dia pakai meson untuk sistem buildnya. Saya belum punya jadi install dulu (belajarnya belakangan, lol). Saya pakai debian jadi menurut instruksi dari meson:
$ sudo apt install meson ninja-build
(Saya tak tahu ninja-build itu buat apa, tapi skip dulu saja belajarnya).
Dan untuk build, instruksinya adalah cd ke yang baru saja di-clone, lalu jalankan:
$ meson setup builddir && cd builddir $ meson compile $ meson test
Kalau sukses, hasil pluginnya akan muncul dalam folder:
lv2/builddir/plugins/
Saya paling tertarik dengan plugin efek audio, jadi saya ingin mencoba plugin eg-amp.lv2 apakah dia bisa di-load dalam DAW. Saya copy folder eg-amp.lv2 ke dalam ~/.lv2 (folder standar untuk lv2 plugin) dan coba scan dalam DAW.
Wohooo, muncul dia dalam plugin manager, apakah bisa diload?
Nice.
Coba Lihat Struktur LV2 Plugin
Beda dari VST yang biasany hanya berupa satu file binary, (misalnya sebuah .dll di Windows, sebuah .so di Linux) , sebuah plugin LV2 dipak dalam folder yang namanya bisa apa saja. Di dalam folder tersebut ada file berikut:
- file binary
- manifest.ttl
- [plugin].ttl yang mana [plugin] adalah nama plugin tersebut.
Isi Manifest
File manifest.ttl ini adalah yang nantinya di-scan oleh host LV2 agar dilist dalam daftar plugin yang tersedia.
Isi file manifest adalah info untuk mempermudah listing di host. Utamanya dia berisi info tentang file apa saja yang termuat dalam plugin tersebut dan referensi tentang lokasinya dala bundle.
Isi [plugin].ttl
File [plugin].ttl adalah file yang nantinya di-scan oleh host LV2 saat plugin tersebut akan di-load ke dalam rantai proses sinyal audio. Dia berisi info sbb:
- Nama plugin
- License
- Daftar fitur
- Daftar port
Dalam plugin eg-amp.lv2 yang saya load tadi, port yang termuat ada 3:
- Control Port "Gain"
- Audio Port "In"
- Audio Port "Out"
Untuk port control, untuk mempermudah host, ada definisi tentang nilai min-max-nya. Di contoh ini, min = -90.0 dB, max = 24.0 dB.
Tentang Format .ttl
Format file .ttl ini ditulis dalam kerangka RDF dengan sintaks Turtle. RDF dan Turtle ini menarik sekali sebagai konsep. Sepertinya pantas didalami di lain waktu. Saya penasaran kenapa tidak pakai JSON, YAML, atau TOML seperti software kebanyakan.
Untuk sekarang cukup dipahami saja kalau dia adalah sebuah bahasa untuk deskripsi data berupa graph. Setiap item dinyatakan dengan SUBYEK, PREDIKAT, dan OBYEK. Misalnya (eg-amp.lv2) (adalah sebuah) (LV2 Plugin). Masing-masing SPO ini bisa dinyatakan dengan URI. Misalnya, predikat (adalah sebuah) sudah termuat dalam standar RDF sebagai http://www.w3.org/1999/02/22-rdf-syntax-ns#type.
Baca Source Code
Nah, sekarang kita sudah definisikan data tentang plugin bagi host. Mari kita lihat bagaimana plugin itu sendiri diimplementasikan dalam program.
Oke, LV2 spec punya banyak header file dalam "include/lv2". Sepertinya perlu baca referensi untuk memahaminya. Untuk sekarang kita fokus ke source untuk eg-amp.lv2. Plugin ini menggunakan header "lv2/core/lv2.h" sepertinya ini header wajibnya.
#include "lv2/core/lv2.h"
Ah, berikutnya adalah definisi yang mengikat kepada metadata di file .ttl.
#define AMP_URI "http://lv2plug.in/plugins/eg-amp" typedef enum {AMP_GAIN = 0, AMP_INPUT = 1, AMP_OUTPUT = 2 } PortIndex;
Ini kelihatan seperti definisi sementara, eksekusi pendaftarannya pasti ada di bawah. Benar, ternyata ID plugin ini "AMP_URI" dipakai dalam fungsi lv2_descriptor() sebagai proses registrasi. Selain AMP_URI, ternyata definisi port juga didaftarkan dengan fungsi connect_port().
Berikutnya data private yang dipegang oleh plugin itu sendiri. Semuanya float ya? Ini bisa dianggap sample-rate sama bit-depthnya dihandle sama LV2 kan?
typedef struct { // Port buffers const float* gain; const float* input; float* output; } Amp;
Berikutnya fungsi buat instantiasi plugin. Oke ini constructor buat hostnya ya. Dia return handle.
static LV2_Handle instantiate(const LV2_Descriptor* descriptor, double rate, const char* bundle_path, const LV2_Feature* const* features) { Amp* amp = (Amp*)calloc(1, sizeof(Amp)); return (LV2_Handle)amp; }
Waktu plugin kita di-instantiate, kita dapet info soal sample rate, path ke plugin kita di komputer, sama daftar fitur host. Oke makes sense. Tugas kita di sini cuma alokasi data privat yang perlu dikomunikasikan ke host.
Berikutnya connect_port(), ini ternyata dipanggil oleh host saat mau koneksi port ya.
static void connect_port(LV2_Handle instance, uint32_t port, void* data) { Amp* amp = (Amp*)instance; switch ((PortIndex)port) { case AMP_GAIN: amp->gain = (const float*)data; break; case AMP_INPUT: amp->input = (const float*)data; break; case AMP_OUTPUT: amp->output = (float*)data; break; } }
Handle instance yang tadi di-return ke host ternyata dipakai lagi di sini. Kita disuruh refer ke data yang ditaruh oleh host, dan dicatat dalam struktur privat kita. Dan lagi-lagi, saya bingung kenapa plugin ini pakai float. Data yang dikirim oleh host datang tanpa tipe. Kita cast sendiri ke float. Perlu baca spec lebih jauh soal tipe data di buffer.
Berikutnya ada aktifasi. Ini dipanggil saat pluginnya siap mau dijalankan, dan kita disuruh init semua data yang kita perlu.
static void activate(LV2_Handle instance) {}
Tapi ternyata plugin ini cuma punya data Gain (yang dikontrol oleh host) dan ga punya state lain, jadi kosong aja. Sekarang ga kebayang data apa yang perlu disimpan tapi bukan bagian dari kontrol. Mungkin makin jelas dengan pengalaman.
Waaaaaah, ini dia jantung dari plugin ini. THE ONE AND ONI, run() function. Lol.
#define DB_CO(g) ((g) > -90.0f ? powf(10.0f, (g)*0.05f) : 0.0f) static void run(LV2_Handle instance, uint32_t n_samples) { const Amp* amp = (const Amp*)instance; const float gain = *(amp->gain); const float* const input = amp->input; float* const output = amp->output; const float coef = DB_CO(gain); for (uint32_t pos = 0; pos < n_samples; pos++) { output[pos] = input[pos] * coef; } }
Well, straightforward. Kita dikasih data audio sebanyak n_samples, datanya ada di instance->input. Hitung outputnya berapa, tulis di instance->output. Sepertinya perlu hati-hati soal tipe datanya, tapi moga-moga ga perlu khawatir terlalu banyak.
Ah berikutnya fungsi buat hapus-hapus ya.
static void deactivate(LV2_Handle instance) {}
Ini lawannya activate(), jadi hapus-hapus yang tadi di-init di activate().
Berikutnya ini cleanup() lawannya instantiate().
static void cleanup(LV2_Handle instance) { free(instance); }
Berikutnya ada extension data. Sepertinya ini wajib di-define, tapi kali ini ga ada extension data jadi kosong aja, return NULL.
static const void* extension_data(const char* uri) { return NULL; }
Yang terakhir adalah yang menggabungkan semuanya di atas menjadi satu struktur descriptor, lalu dipak dalam sebuah fungsi yang mengembalikan descriptor tersebut.
static const LV2_Descriptor descriptor = {AMP_URI, instantiate, connect_port, activate, run, deactivate, cleanup, extension_data}; LV2_SYMBOL_EXPORT const LV2_Descriptor* lv2_descriptor(uint32_t index) { return index == 0 ? &descriptor : NULL; }
Tentang Threading
Ada 3 konteks thread dalam LV2 plugin. Fungsi di atas termasuk dalam konteks thread tertentu, dan masing-masing adalah berikut:
**Discovery** | - descriptor - extension_data | konteks saat host mencari plugin dan mendata isi plugin |
**Instantiation** | - instantiate - activate - deactivate - cleanup | konteks saat host memasang plugin ke dalam suatu channel misalnya |
**Audio** | - connect_port - run | konteks saat data audio mengalir melalui plugin |
Lumayan jelas pembagiannya ya.
Pe Er kali ini adalah baca tentang tipe data yang ulang alik antara host-plugin. Bagaimana sample rate dan bit-depth dihandle. Mungkin bisa dengan membaca contoh plugin lain seperti eg-scope.lv2. Dan juga, kalau sempat, belajar meson dan RDF.
Oke sekian dulu eksplorasi saya.