Reactでオーディオプラグインを作れる時代になったという話

7 min read読了の目安(約6700字

はじめに

この記事では、VST(オーディオプラグインの形式の一つ)開発に突如現れた、
React-JUCEという黒船について紹介します

VSTとは

DAW(Digital Audio Workstation)と呼ばれる音楽制作ソフトウェアでは、VSTと呼ばれるプラグインの仕組みが標準的に用いられています。

VST形式で開発すると、CubaseやロジックといったDAW上でMIDIやオーディオを処理するプラグインを動作させることができます。

VSTの開発においては、JUCE と呼ばれるフレームワークを用いてC++で開発されることが多いようです。

今までのVST開発のツライところ

音楽家でありエンジニアである筆者は、
ずっと興味がありつつも、VST開発からこれまで一定の距離をおいてきました。

その理由としては、

  • C++こわい
  • 信号処理こわい
  • UIを命令的に記述したくない(※後述します)

という理由がありました。

React-JUCE (旧称: Blueprint) の登場

そんな中、2019年の Audio Developers Conferenceに登壇した一つのプレゼンに目が留まりました。

https://www.youtube.com/watch?v=51pdMfGU-4g

React-JUCEの最初に投稿された記事にはこのように説明があります。

Blueprint, a hybrid, experimental JavaScript/C++ framework that enables a React.js frontend for a JUCE application or plugin.

すなわち、
JUCEでプラグインを開発しつつ、そのフロントエンド(UI)をReact.jsで記述できる
とのことです。


これだけ聞いて

_人人人人人人人人人_
> なにそれつよい <
 ̄Y^Y^Y^Y^Y^Y^Y^Y ̄

と思える人は少ないかもしれないですが、紹介します

React-JUCE の特徴

  • JUCEを用いているのでクロスプラットフォームな開発ができる
  • 基本的な仕組みはReact Nativeに似たような感じで、ネイティブ部分をJUCEで記述しつつ、React.js + jsxで宣言的にUIを構築することができる
  • jsのエンジンはDuktapeが使われている
  • レイアウトエンジンにYogaを用いることでレスポンシブ対応
  • webpackのHMRにより、C++の再コンパイルなしでUIを調節できる

2021年にWebエンジニアをやっているとレスポンシブなんて当たり前な気がしてしまうかもしれません。

しかし、これまでのVST開発の世界では、固定のサイズで開発し、UI部分を書くときは「〇〇pxの位置にフェーダーを置く」のような命令的な処理を記述するのも一般的でした。

いわずもがな、「あ〜〜もうちょっと左かな〜2pxずらしてみよう」と思ったら、
C++をもう一度コンパイルしなければなりません。

しかし、React-JUCEにおいては、ネイティブ部分を書き直した時は再コンパイルが必要ですが、それを呼び出すJS部分を書き換えた時には、C++の再コンパイルは必要ありません。
なので、webpackがwatchモードで起動していれば、手元の変更はほんの数秒で反映されます🎉

実例

公式にあるexampleを抜粋しながら例にあげます。

JS部分(抜粋)

jsui/src/App.js
// import AnimatedFlexBoxExample from "./AnimatedFlexBox";
import Meter from "./Meter";
import Knob from "./Knob";
import ParameterToggleButton from "./ParameterToggleButton";
import React, { Component } from "react";
import { Canvas, Image, Text, View } from "react-juce";

function animatedDraw(ctx) {
  let now = Date.now() / 10;
  let width = now % 100;
  let red = Math.sqrt(width / 100) * 255;
  let hex = Math.floor(red).toString(16);

  ctx.fillStyle = `#${hex}ffaa`;
  ctx.fillRect(0, 0, width, 2);
}

// Example of callback for image onLoad/onError
function imageLoaded() {
  console.log("Image is loaded!");
}

function imageError(error) {
  console.log(error.name);
  console.log(error.message);
}

class App extends Component {
  constructor(props) {
    super(props);
    this._onMuteToggled = this._onMuteToggled.bind(this);

    this.state = {
      muted: false,
    };
  }

  _onMuteToggled(toggled) {
    this.setState({
      muted: toggled,
    });
  }

  render() {
    // Uncomment here to watch the animated flex box example in action
    // return (
    //   <View {...styles.container}>
    //     <AnimatedFlexBoxExample />
    //   </View>
    // );

    const muteBackgroundColor = this.state.muted
      ? "#66FDCF"
      : "hsla(162, 97%, 70%, 0)";
    const muteTextColor = this.state.muted
      ? "#17191f"
      : "hsla(162, 97%, 70%, 1)";

    const logo_url =
      "https://raw.githubusercontent.com/nick-thompson/react-juce/master/examples/GainPlugin/jsui/src/logo.png";
    //const logo_png = require('./logo.png');
    //const logo_svg = require('./logo.svg');

    return (
      <View {...styles.container}>
        <View {...styles.content}>
          <Image
            source={logo_url}
            onLoad={imageLoaded}
            onError={imageError}
            {...styles.logo}
          />
          <Knob paramId="MainGain" />
          <Meter {...styles.meter} />
          <Canvas {...styles.canvas} animate={true} onDraw={animatedDraw} />
          <ParameterToggleButton
            paramId="MainMute"
            onToggled={this._onMuteToggled}
            background-color={muteBackgroundColor}
            {...styles.mute_button}
          >
            <Text color={muteTextColor} {...styles.mute_button_text}>
              MUTE
            </Text>
          </ParameterToggleButton>
        </View>
      </View>
    );
  }
}

const styles = {
  container: {
    width: "100%",
    height: "100%",
    backgroundColor:
      "linear-gradient(45deg, hsla(225, 15%, 11%, 0.3), #17191f 50%)",
    justifyContent: "center",
    alignItems: "center",
  },
 // 省略
};

export default App;

このように、

  • CSS ✅
  • State管理 ✅
  • React Context ✅
  • React Hooks ✅
    • ちなみに、このサポートは最近導入されたようです
  • ホットリロード ✅
    など、まるで普通のWeb開発をしているかのようですね!

特筆する点として、

import { Canvas, Image, Text, View } from "react-juce";

このように react-juce からimportされているコンポーネントは、JUCEのネイティブコンポーネントをラップしたReactコンポーネントです。
これもReact Nativeに近さを感じますね!

C++部分(抜粋)

PluginProcessor.cpp
//省略
void GainPluginAudioProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffer& /* midiMessages */)
{
    ScopedNoDenormals noDenormals;
    auto totalNumInputChannels  = getTotalNumInputChannels();
    auto totalNumOutputChannels = getTotalNumOutputChannels();

    // In case we have more outputs than inputs, this code clears any output
    // channels that didn't contain input data, (because these aren't
    // guaranteed to be empty - they may contain garbage).
    // This is here to avoid people getting screaming feedback
    // when they first compile a plugin, but obviously you don't need to keep
    // this code if your algorithm always overwrites all the output channels.
    for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
        buffer.clear (i, 0, buffer.getNumSamples());

    // Our intense dsp processing
    gain.setTargetValue(*params.getRawParameterValue("MainGain"));
    gain.applyGain(buffer, buffer.getNumSamples());

    if (auto *muteParam  = dynamic_cast<AudioParameterBool*>(params.getParameter("MainMute")))
    {
        bool muted = muteParam->get();
        if (muted)
            buffer.applyGain(0.0f);
    }

    // Read current block gain peak value
    gainPeakValue = buffer.getMagnitude (0, buffer.getNumSamples());
}
//省略

このようにJUCEのAudioProcessorを継承したクラスを定義することで、processBlock メソッドにオーディオ処理をする部分を記述することができます。

React-JUCE はもう実際に現場で使えるのか?

React-JUCEを用いて開発されたVST

NovoNotes3DXというプラグインは、
React-JUCEを部分的に使用して開発されました。

現実には、jsとC++をブリッジする部分のオーバーヘッドなど(?)によって、UIの中にもネイティブで書いた方がいい部分がまだある、というような話を中の人から聞きましたが、
それでも開発体験としては快適なようです。

まとめ

UIを記述するという点において、Reactはその手を様々な領域に広げています。
React-JUCEのように野心的なプロジェクトの登場のおかげで、
「僕はオーディオ処理はわからないけど、js部分だけなら書けるよ」というような人が
プラグイン開発にジャンプインしやすくなったという大きな功績があると思います💡