🦀

CLAPオーディオプラグインをRustで作る(入門)

2024/11/13に公開

CLAPとは何か

CLever Audio Plug-in とは、Bitwigとu-heが共同で開発し2022年6月にリリースされた新しいオーディオプラグインの規格です。
CLAPはMITライセンスのもとで提供され、商用・非商用問わず誰でもプラグインの開発が可能です。
今回は、Rustを使ってゲインを50%にするCLAPプラグインを作成します。
https://cleveraudio.org/

前提

  • Rust開発環境を構築済み
  • CLAPに対応したDAWをインストール済み
  • Windows x86-64を想定

プロジェクトの作成

新しいプロジェクトを作成します。

cargo new --lib claptest

Cargo.tomlを編集

動的リンクライブラリを生成するため cbylib を追加します。

Cargo.toml
[lib]
crate-type = ["cdylib"]

依存関係の追加

CLAPのRust用ライブラリは、Clackという低レベルで安全と主張されているラッパーを使います。
crates.ioで公開されていないため、以下のコマンドを入力して依存関係を追加します。

cargo add --git https://github.com/prokopyl/clack.git clack-plugin
cargo add --git https://github.com/prokopyl/clack.git clack-extensions --features clack-plugin,audio-ports

コードを書く

src/lib.rs にプラグインのコードを書きます。
既に何かしらのコードが記述されている場合、全て削除してください。

今回書いたコードの全体はこちらで公開しています。

インポートと構造体の定義

audio_ports はオーディオ入出力を行う場合に必要になります。

use clack_extensions::audio_ports::*;
use clack_plugin::prelude::*;

pub struct ClapTest;

Pluginトレイトの実装

impl Plugin for ClapTest {
    type AudioProcessor<'a> = ClapTestAudioProcessor<'a>;
    type Shared<'a> = ClapTestShared;
    type MainThread<'a> = ClapTestMainThread<'a>;

    fn declare_extensions(builder: &mut PluginExtensions<Self>, _shared: Option<&ClapTestShared>) {
        builder
            .register::<PluginAudioPorts>();
    }
}

DefaultPluginFactoryトレイトの実装

PluginDescriptor でプラグイン名や作成者、バージョンやタグを設定できます。
with_features で AUDIO_EFFECT を入れるとDAWでエフェクト一覧に表示されます。
INSTRUMENT を入れると、音源一覧に表示されます。

impl DefaultPluginFactory for ClapTest {
    fn get_descriptor() -> PluginDescriptor {
        use clack_plugin::plugin::features::*;

        PluginDescriptor::new("com.github.saisana299.clap-test", "Clap Test")
            .with_vendor("Saisana299")
            .with_features([AUDIO_EFFECT, STEREO])
    }

    fn new_shared(_host: HostSharedHandle) -> Result<Self::Shared<'_>, PluginError> {
        Ok(ClapTestShared {})
    }

    fn new_main_thread<'a>(
        _host: HostMainThreadHandle<'a>,
        shared: &'a Self::Shared<'a>,
    ) -> Result<Self::MainThread<'a>, PluginError> {
        Ok(Self::MainThread { shared })
    }
}

AudioProcessor構造体とそのトレイト実装

オーディオ処理を行う構造体です。
process メソッド内で、オーディオバッファを取得し、各サンプルの音量を0.5倍にしています。

pub struct ClapTestAudioProcessor<'a> {
    _shared: &'a ClapTestShared,
}

impl<'a> PluginAudioProcessor<'a, ClapTestShared, ClapTestMainThread<'a>>
    for ClapTestAudioProcessor<'a>
{
    fn activate(
        _host: HostAudioProcessorHandle<'a>,
        _main_thread: &mut ClapTestMainThread<'a>,
        _shared: &'a ClapTestShared,
        _audio_config: PluginAudioConfiguration,
    ) -> Result<Self, PluginError> {
        Ok(Self { _shared })
    }

    fn process(
        &mut self,
        _process: Process,
        mut audio: Audio,
        events: Events,
    ) -> Result<ProcessStatus, PluginError> {
        let mut port_pair = audio
            .port_pair(0)
            .ok_or(PluginError::Message("No input/output ports found"))?;

        let mut output_channels = port_pair
            .channels()?
            .into_f32()
            .ok_or(PluginError::Message("Expected f32 input/output"))?;

        let mut channel_buffers = [None, None];

        for (pair, buf) in output_channels.iter_mut().zip(&mut channel_buffers) {
            *buf = match pair {
                ChannelPair::InputOnly(_) => None,
                ChannelPair::OutputOnly(_) => None,
                ChannelPair::InPlace(b) => Some(b),
                ChannelPair::InputOutput(i, o) => {
                    o.copy_from_slice(i);
                    Some(o)
                }
            }
        }

        #[allow(unused_variables)]
        for event_batch in events.input.batch() {
            // 音量を0.5倍にする
            for buf in channel_buffers.iter_mut().flatten() {
               for sample in buf.iter_mut() {
                   *sample *= 0.5;
               }
            }
        }

        Ok(ProcessStatus::ContinueIfNotQuiet)
    }
}

SharedとMainThreadの定義

メインスレッドの処理では、オーディオポートの数と情報を設定しています。
count メソッドではオーディオポートの数を指定しています。
is_input がtrueの時は入力、falseの時は出力のポート数を返すようにします。
今回は入力、出力共にポート数1で設定しています。

pub struct ClapTestShared {}

impl<'a> PluginShared<'a> for ClapTestShared {}

pub struct ClapTestMainThread<'a> {
    #[allow(dead_code)]
    shared: &'a ClapTestShared,
}

impl<'a> PluginAudioPortsImpl for ClapTestMainThread<'a> {
    fn count(&mut self, _is_input: bool) -> u32 {
        1
    }

    fn get(&mut self, index: u32, _is_input: bool, writer: &mut AudioPortInfoWriter) {
        if index == 0 {
            writer.set(&AudioPortInfo {
                id: ClapId::new(0),
                name: b"main",
                channel_count: 2,
                flags: AudioPortFlags::IS_MAIN,
                port_type: Some(AudioPortType::STEREO),
                in_place_pair: None,
            })
        }
    }
}

impl<'a> PluginMainThread<'a, ClapTestShared> for ClapTestMainThread<'a> {}

エントリーポイント

これを入れる事で、ホストがプラグインを認識して、ロードできるようになります。

clack_export_entry!(SinglePluginEntry<ClapTest>);

ビルドしてDAWで読み込む

以下のコマンドでビルドを行います。

cargo build --release

ビルドが完了すると、target フォルダ内に、<プロジェクト名>.dll ファイルが生成されています。
このdllファイルを <プロジェクト名>.clap にリネームして、CLAPプラグインフォルダに入れます。

今回はBitwig Studioで読み込んでみました。
オーディオトラックにプラグインを挿すと音量が50%減ることが確認できました。

参考

https://kwarf.com/2024/07/writing-a-clap-synthesizer-in-rust-part-1/
https://github.com/prokopyl/clack/tree/main/plugin/examples/gain

Discussion