[Rust] NIH-plugでCLAPオーディオプラグインを作る
プラグインにGUIを実装したい
前回の記事 では、RustでCLAPオーディオプラグインを作る方法について解説しました。
これにGUIを実装したかったのですが、上手くいかなかったためClackではなく別のライブラリを使ってみることにしました。
前提
- Pythonをインストール済み (pip、pipxを使います)
- Rust開発環境を構築済み
- CLAPに対応したDAWをインストール済み
- Windows x86-64を想定
NIH-plug
VST3とCLAPのフォーマットに対応しています。
GUIはegui、iced、VIZIAがサポートされています。
ライセンスに関しては、VST3バインディングを行うとGPLv3が適用されるため、ISCや独自のライセンスを適用したい場合はCLAPバインディングのみにする必要があります。
本記事では、CLAPプラグインをISCで配布することを想定して、GUI付きのプラグインを作る方法を解説します。
プロジェクト作成
ライブラリ製作者が公開しているプロジェクトテンプレートを使います。
cookiecutterを利用してNIH-plugプロジェクトのビルドができます。
以下のコマンドでpipxをインストールします。
pip install pipx
次のコマンドでプロジェクトの作成ウィザードが起動します。
pipx run cookiecutter gh:robbert-vdh/nih-plug-template
# プロジェクト名を入力
[1/11] project_name (your_plugin_name (use underscores)): claptest
# struct_nameとplugin_nameは必要があれば変更してください
[2/11] struct_name (Claptest):
[3/11] plugin_name (Claptest):
# 制作者の名前を入力
[4/11] author (Your Name): Saisana299
# 連絡用メールアドレスを入力
[5/11] email_address (your@email.com):
# プラグインのWebサイトなどのURLを入力
[6/11] url (https://youtu.be/dQw4w9WgXcQ):
# プラグインについての説明を入力
[7/11] description (A short description of your plugin):
# CLAPプラグイン用のID (ドメインの逆順)です
[8/11] clap_id (com.your-domain.claptest):
# 今回はVST3バインディングを利用しないのでそのままにします
[9/11] vst3_id (Exactly16Chars!!):
# 2のISCを選択
[10/11] Select license
1 - GPL-3.0-or-later
2 - ISC
3 - Other licenses can be set in Cargo.toml,
but using the project needs to be GPLv3 compliant to be able
to use the VST3 exporter.Check Cargo.toml for more information.
Choose from [1/2/3] (1): 2
# Enterでウィザードが終了します
Make sure to change the CLAP features and VST3 categories
in src/lib.rs (press enter to finish):
VST3機能を無効化
Cargo.tomlのdependenciesの項目を編集して、VST3機能を無効にします。
[dependencies]
# Remove the `assert_process_allocs` feature to allow allocations on the audio
# thread in debug builds.
# 以下をコメントアウト
# nih_plug = { git = "https://github.com/robbert-vdh/nih-plug.git", features = ["assert_process_allocs"] }
# Uncomment the below line to disable the on-by-default VST3 feature to remove
# the GPL compatibility requirement
# 以下のコメントを解除
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug.git", default-features = false, features = ["assert_process_allocs"] }
GUI機能の追加
今回はGUIにicedを使います。
icedはRust用のクロスプラットフォームGUIライブラリです。
nih_plug_icedはNIH-plugと同じリポジトリにあります。
dependenciesに以下を追加します。
nih_plug_iced = { git = "https://github.com/robbert-vdh/nih-plug.git" }
lib.rsからVST3に関する記述を消す
必要のないVST3の項目を削除します。
impl Vst3Plugin for Claptest {
const VST3_CLASS_ID: [u8; 16] = *b"Exactly16Chars!!";
// And also don't forget to change these categories
const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] =
&[Vst3SubCategory::Fx, Vst3SubCategory::Dynamics];
}
nih_export_vst3!(Claptest);
以上で準備は完了です。
コードを書く
公式にあるicedを使ったgainプラグインのexampleを実装してみたいと思います。
Cargo.tomlのdependenciesに以下を追加します。
atomic_float = "0.1"
lib.rs
変更の無いコードは省略しています。
...
use atomic_float::AtomicF32; // 追加
use nih_plug_iced::IcedState; // 追加
mod editor; // 追加
/// 完全に無音に切り替えた後、ピークメーターが12dB減衰するまでの時間。
const PEAK_METER_DECAY_MS: f64 = 150.0; // 追加
struct Claptest {
...
/// ピークメーターの正規化用
peak_meter_decay_weight: f32, // 追加
/// ピークメーターの現在のデータ
peak_meter: Arc<AtomicF32>, // 追加
}
...
struct ClaptestParams {
/// エディターの状態
#[persist = "editor-state"] // 追加
editor_state: Arc<IcedState>, // 追加
...
impl Default for Claptest {
fn default() -> Self {
Self {
...
peak_meter_decay_weight: 1.0, // 追加
peak_meter: Arc::new(AtomicF32::new(util::MINUS_INFINITY_DB)), // 追加
}
}
}
impl Default for ClaptestParams {
fn default() -> Self {
Self {
editor_state: editor::default_state(), // 追加
...
}
}
}
impl Plugin for Claptest {
...
/// AUDIO_IO_LAYOUTSを変更
const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[
AudioIOLayout {
main_input_channels: NonZeroU32::new(2),
main_output_channels: NonZeroU32::new(2),
..AudioIOLayout::const_default()
},
AudioIOLayout {
main_input_channels: NonZeroU32::new(1),
main_output_channels: NonZeroU32::new(1),
..AudioIOLayout::const_default()
},
];
/// 以下は削除
// const MIDI_INPUT: MidiConfig = MidiConfig::None;
// const MIDI_OUTPUT: MidiConfig = MidiConfig::None;
...
/// 以下を追加
fn editor(&mut self, _async_executor: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
editor::create(
self.params.clone(),
self.peak_meter.clone(),
self.params.editor_state.clone(),
)
}
fn initialize(
&mut self,
_audio_io_layout: &AudioIOLayout,
buffer_config: &BufferConfig, // アンダーバーを消す
_context: &mut impl InitContext<Self>,
) -> bool {
// 以下を追加
self.peak_meter_decay_weight = 0.25f64
.powf((buffer_config.sample_rate as f64 * PEAK_METER_DECAY_MS / 1000.0).recip())
as f32;
true
}
/// 以下を削除
// fn reset(&mut self) {
// }
/// processの内容を編集
fn process(
&mut self,
buffer: &mut Buffer,
_aux: &mut AuxiliaryBuffers,
_context: &mut impl ProcessContext<Self>,
) -> ProcessStatus {
for channel_samples in buffer.iter_samples() {
let mut amplitude = 0.0;
let num_samples = channel_samples.len();
let gain = self.params.gain.smoothed.next();
for sample in channel_samples {
*sample *= gain;
amplitude += *sample;
}
// GUIが表示されている時のみGUIの計算を行う
if self.params.editor_state.is_open() {
amplitude = (amplitude / num_samples as f32).abs();
let current_peak_meter = self.peak_meter.load(std::sync::atomic::Ordering::Relaxed);
let new_peak_meter = if amplitude > current_peak_meter {
amplitude
} else {
current_peak_meter * self.peak_meter_decay_weight
+ amplitude * (1.0 - self.peak_meter_decay_weight)
};
self.peak_meter
.store(new_peak_meter, std::sync::atomic::Ordering::Relaxed)
}
}
ProcessStatus::Normal
}
}
...
editor.rs
新しく editor.rs というファイルを作成してください。
use atomic_float::AtomicF32;
use nih_plug::prelude::{util, Editor, GuiContext};
use nih_plug_iced::widgets as nih_widgets;
use nih_plug_iced::*;
use std::sync::Arc;
use std::time::Duration;
use crate::ClaptestParams;
pub(crate) fn default_state() -> Arc<IcedState> {
IcedState::from_size(200, 150)
}
pub(crate) fn create(
params: Arc<ClaptestParams>,
peak_meter: Arc<AtomicF32>,
editor_state: Arc<IcedState>,
) -> Option<Box<dyn Editor>> {
create_iced_editor::<GainEditor>(editor_state, (params, peak_meter))
}
struct GainEditor {
params: Arc<ClaptestParams>,
context: Arc<dyn GuiContext>,
peak_meter: Arc<AtomicF32>,
gain_slider_state: nih_widgets::param_slider::State,
peak_meter_state: nih_widgets::peak_meter::State,
}
#[derive(Debug, Clone, Copy)]
enum Message {
/// パラメータの値を更新
ParamUpdate(nih_widgets::ParamMessage),
}
impl IcedEditor for GainEditor {
type Executor = executor::Default;
type Message = Message;
type InitializationFlags = (Arc<ClaptestParams>, Arc<AtomicF32>);
fn new(
(params, peak_meter): Self::InitializationFlags,
context: Arc<dyn GuiContext>,
) -> (Self, Command<Self::Message>) {
let editor = GainEditor {
params,
context,
peak_meter,
gain_slider_state: Default::default(),
peak_meter_state: Default::default(),
};
(editor, Command::none())
}
fn context(&self) -> &dyn GuiContext {
self.context.as_ref()
}
fn update(
&mut self,
_window: &mut WindowQueue,
message: Self::Message,
) -> Command<Self::Message> {
match message {
Message::ParamUpdate(message) => self.handle_param_message(message),
}
Command::none()
}
fn view(&mut self) -> Element<'_, Self::Message> {
Column::new()
.align_items(Alignment::Center)
.push(
Text::new("Gain GUI")
.font(assets::NOTO_SANS_LIGHT)
.size(40)
.height(50.into())
.width(Length::Fill)
.horizontal_alignment(alignment::Horizontal::Center)
.vertical_alignment(alignment::Vertical::Bottom),
)
.push(
Text::new("Gain")
.height(20.into())
.width(Length::Fill)
.horizontal_alignment(alignment::Horizontal::Center)
.vertical_alignment(alignment::Vertical::Center),
)
.push(
nih_widgets::ParamSlider::new(&mut self.gain_slider_state, &self.params.gain)
.map(Message::ParamUpdate),
)
.push(Space::with_height(10.into()))
.push(
nih_widgets::PeakMeter::new(
&mut self.peak_meter_state,
util::gain_to_db(self.peak_meter.load(std::sync::atomic::Ordering::Relaxed)),
)
.hold_time(Duration::from_millis(600)),
)
.into()
}
fn background_color(&self) -> nih_plug_iced::Color {
nih_plug_iced::Color {
r: 0.98,
g: 0.98,
b: 0.98,
a: 1.0,
}
}
}
プロジェクトのビルド
以下のコマンドでCLAPプラグインをビルドします。
cargo xtask bundle <プロジェクト名> --release
# ここでは
cargo xtask bundle claptest --release
target/bundled
にclapファイルが生成されているので、C:/Program Files/Common Files/CLAP
内に移動させてDAWで読み込むことができます。
実際に動かしてみる
以下のような画面が出て、パラメータを調整して音量が変化すれば成功です。
Discussion