Edited at
JUCEDay 22

Max/Mspのgen~とJUCEを使ったVSTの作り方


「gen~」について

「gen~」はMax/Msp内部でサンプル単位のDSPを行うためのオブジェクトです。

pathcerオブジェクトなどのようにgen~オブジェクトの中にさらにパッチを組んでいくのですが、

ここで使われるオブジェクトはすべて一サンプル単位での演算オブジェクトとなっています。

さらにcodeboxというオブジェクトを使うことでCUIのコードベースで記述することもできるようになっています。

このgen~オブジェクトは内部のパッチをc++のソースコードにコンパイルして出力する機能も持っているので、それをJUCEに入れ込んでVSTにしてみました。そのやり方を紹介します。

ソースコードの全文はこちらにあげています。

ちなみにこの方法はCycling74の公式動画およびサンプルを参考にしています。

公式動画

サンプル


gen~を使った双二次フィルタ

今回はgen~を使ったシンプルな双二次ローパスフィルタをJUCEに入れ込んでみました。

gen~内部の実装は以下の通りです。

スクリーンショット (5).png

右と左のチャンネルで同じ処理をするために同じコードを二つ書いています。

周波数とQはパラメーターとして左右で共有しています。


gen~からソースコードを出力

gen~にexportcodeというメッセージを送ると、共通ファイルの入ったgen_dspというフォルダとgen~の中に実装したものがc++に翻訳されたgen_exported.hとgen_exported.cppが生成されます。

スクリーンショット (1).png

ちなみにこのときexportfolderやexportnameなどのアトリビュートを設定することで出力されるフォルダーやファイルの名前などを設定することができます。


JUCEへの組み込み

projucerで通常通り新しいプロジェクトを作ったら先ほどgen~から出力したフォルダとファイルをインポートします。

わかりやすいようにgen_exportというフォルダにすべてをまとめてからインポートしています。

スクリーンショット (2).png

今回はGUIは特別つけていないので編集部分は主にPluginProcessorです。


ヘッダーファイルへの追加部分

PluginProcessor.hに追加した部分は以下の箇所です。


PluginProcessor.cpp

#pragma once


#include "../JuceLibraryCode/JuceHeader.h"
#include "gen_export/gen_exported.h" //追加

class gen2vstAudioProcessor : public AudioProcessor
{
public:
//中略
int getNumParameters() override; //追加
float getParameter(int index) override; //追加
void setParameter(int index, float newValue) override; //追加

const String getParameterName(int index) override; //追加
const String getParameterText(int index) override; //追加

const String getInputChannelName(int channelIndex) const override; //追加
const String getOutputChannelName(int channelIndex) const override; //追加
bool isInputChannelStereoPair(int index) const override; //追加
bool isOutputChannelStereoPair(int index) const override; //追加
//中略
protected:
void assureBufferSize(long bufferSize); //追加
private:
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (gen2vstAudioProcessor)

CommonState *m_C74PluginState; //追加
long m_CurrentBufferSize; //追加
t_sample **m_InputBuffers; //追加
t_sample **m_OutputBuffers; //追加


主にgen~が出力してくれたコードとのデータやり取りのためのもろもろといった感じです。(だと思います。)


cppファイルへの追加部分

cppは書き足したり書き換えたりする部分が多少多いですが、サンプルコードをほぼコピペで行けます。

#include "PluginProcessor.h"

#include "PluginEditor.h"

//==============================================================================
gen2vstAudioProcessor::Practice19_genAudioProcessor()
:m_CurrentBufferSize(0), //追加
#ifndef JucePlugin_PreferredChannelConfigurations
AudioProcessor (BusesProperties()
#if ! JucePlugin_IsMidiEffect
#if ! JucePlugin_IsSynth
.withInput ("Input", AudioChannelSet::stereo(), true)
#endif
.withOutput ("Output", AudioChannelSet::stereo(), true)
#endif
)
#endif
{
#pragma region 追加箇所1
m_C74PluginState = (CommonState *)gen_exported::create(44100, 64);
gen_exported::reset(m_C74PluginState);

m_InputBuffers = new t_sample *[gen_exported::num_inputs()];
m_OutputBuffers = new t_sample *[gen_exported::num_outputs()];

for (int i = 0; i < gen_exported::num_inputs(); i++)
{
m_InputBuffers[i] = NULL;
}
for (int i = 0; i < gen_exported::num_outputs(); i++)
{
m_OutputBuffers[i] = NULL;
}
#pragma endregion
}

Practice19_genAudioProcessor::~Practice19_genAudioProcessor()
{
gen_exported::destroy(m_C74PluginState); //追加
}

//中略

#pragma region 追加箇所2
int Practice19_genAudioProcessor::getNumParameters()
{
return gen_exported::num_params();
}
float Practice19_genAudioProcessor::getParameter(int index)
{
t_param value;
t_param min = gen_exported::getparametermin(m_C74PluginState, index);
t_param range = fabs(gen_exported::getparametermax(m_C74PluginState, index) - min);

gen_exported::getparameter(m_C74PluginState, index, &value);

value = (value - min) / range;

return value;
}
void Practice19_genAudioProcessor::setParameter(int index, float newValue)
{
t_param min = gen_exported::getparametermin(m_C74PluginState, index);
t_param range = fabs(gen_exported::getparametermax(m_C74PluginState, index) - min);
t_param value = newValue * range + min;

gen_exported::setparameter(m_C74PluginState, index, value, NULL);
}
const String Practice19_genAudioProcessor::getParameterName(int index)
{
return String(gen_exported::getparametername(m_C74PluginState, index));
}
const String Practice19_genAudioProcessor::getParameterText(int index)
{
String text = String(getParameter(index));
text += String(" ");
text += String(gen_exported::getparameterunits(m_C74PluginState, index));

return text;
}
const String Practice19_genAudioProcessor::getInputChannelName(int channelIndex) const
{
return String(channelIndex + 1);
}

const String Practice19_genAudioProcessor::getOutputChannelName(int channelIndex) const
{
return String(channelIndex + 1);
}

bool Practice19_genAudioProcessor::isInputChannelStereoPair(int index) const
{
return gen_exported::num_inputs() == 2;
}

bool Practice19_genAudioProcessor::isOutputChannelStereoPair(int index) const
{
return gen_exported::num_outputs() == 2;
}
#pragma endregion

//中略

void Practice19_genAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
m_C74PluginState->sr = sampleRate; //追加
m_C74PluginState->vs = samplesPerBlock; //追加
assureBufferSize(samplesPerBlock); //追加
}

// 中略
//processBlockはほぼ書き換え
void Practice19_genAudioProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages)
{
assureBufferSize(buffer.getNumSamples());

for (int i = 0; i < gen_exported::num_inputs(); i++) {
if (i < getNumInputChannels()) {
for (int j = 0; j < m_CurrentBufferSize; j++) {
m_InputBuffers[i][j] = buffer.getReadPointer(i)[j];
}
}
else {
memset(m_InputBuffers[i], 0, m_CurrentBufferSize * sizeof(double));
}
}

gen_exported::perform(m_C74PluginState,
m_InputBuffers,
gen_exported::num_inputs(),
m_OutputBuffers,
gen_exported::num_outputs(),
buffer.getNumSamples());

for (int i = 0; i < getNumOutputChannels(); i++) {
if (i < gen_exported::num_outputs()) {
for (int j = 0; j < buffer.getNumSamples(); j++) {
buffer.getWritePointer(i)[j] = m_OutputBuffers[i][j];
}
}
else {
buffer.clear(i, 0, buffer.getNumSamples());
}
}
}

//中略
//これもほぼ書き換え
void Practice19_genAudioProcessor::getStateInformation (MemoryBlock& destData)
{
char *state;
size_t statesize = gen_exported::getstatesize(m_C74PluginState);
state = (char *)malloc(sizeof(char) * statesize);

gen_exported::getstate(m_C74PluginState, state);
destData.replaceWith(state, sizeof(char) * statesize);

if (state) free(state);
}

//中略
// 最後にこの関数を加える
void Practice19_genAudioProcessor::assureBufferSize(long bufferSize)
{
if (bufferSize > m_CurrentBufferSize) {
for (int i = 0; i < gen_exported::num_inputs(); i++) {
if (m_InputBuffers[i]) delete m_InputBuffers[i];
m_InputBuffers[i] = new t_sample[bufferSize];
}
for (int i = 0; i < gen_exported::num_outputs(); i++) {
if (m_OutputBuffers[i]) delete m_OutputBuffers[i];
m_OutputBuffers[i] = new t_sample[bufferSize];
}
m_CurrentBufferSize = bufferSize;
}
}

GUI部分はCOx2さんの「JUCEでVSTプラグインを作るときにGUIを実装するのが面倒な時に役立つTips」の記事にあった通り、



AudioProcessorEditor* gen2vstAudioProcessor::createEditor()

{

return new GenericAudioProcessorEditor(this);

}



で簡易的に作りました。

gen~を実装したときのparamが自動でパラメーターとして設定されているのでこれだけでGUI上にも表れてくれます。


まとめ

JUCEは簡易的にVSTの実装ができるので大変便利ですが、DSP部分に関しては自力での実装が必要となり、なかなか開発スピードを速めるのが難しいと思っていました。

しかしgen~を使うことで大量の資産を生かせられるので今後うまく使いこなせればなあと思います。