この説明は3部作の最後。
JUCEで簡単なソフトシンセを作る(準備編)
JUCEで簡単なソフトシンセを作る(UI実装編)
ソースはここ。
超シンプルな三角波シンセの実装
何も考えないごくごくシンプルな三角波シンセのクラスを作る。ちょっと何言ってるかわからない部分が多いかもしれないけどソースは下記(めっちゃ手抜き)。
SimpleSynthCore.h
/*
==============================================================================
SimpleSynthCore.h
Created: 30 Dec 2018 9:02:30am
Author: ring2
==============================================================================
*/
# pragma once
class SimpleSynthCore
{
public:
SimpleSynthCore();
~SimpleSynthCore();
void reset (double newSampleRate, int bufferSize);
void process(float** buffers, int numSamples);
void midi(unsigned char status, unsigned char data1, unsigned char data2, int sampleOffset = 0);
private:
double sampleRate; // ホストから渡されたsampleRateを保持しておく
float phase; // saw波/tri波のphase
float delta; // phaseを進めるためのdelta
float gate; // note on/offで開け閉めするゲート
float deltaData[128]; // ノートナンバーに対応するdeltaのテーブル
};
SimpleSynthCore.cpp
/*
==============================================================================
SimpleSynthCore.cpp
Created: 30 Dec 2018 9:02:30am
Author: ring2
==============================================================================
*/
# include "SimpleSynthCore.h"
# include <stdio.h>
# include <math.h>
# include <cassert>
SimpleSynthCore::SimpleSynthCore()
{
sampleRate = 0.0;
phase = 0.0f;
delta = 0.0f;
gate = 0.0f;
}
SimpleSynthCore::~SimpleSynthCore()
{
}
void SimpleSynthCore::reset (double newSampleRate, int /*bufferSize*/)
{
sampleRate = newSampleRate;
phase = 0.0f;
delta = 0.0f;
gate = 0.0f;
for (int note = 0; note < 128; note++)
{
double freq = 440.0 * pow(2.0, (note - 69) / 12.0);
deltaData[note] = (float)(freq / sampleRate);
}
}
void SimpleSynthCore::process(float** buffers, int numSamples)
{
assert(sampleRate > 0.0);
float* outputL = buffers[0];
float* outputR = buffers[1];
for (int i = 0; i < numSamples; i++)
{
float saw, tri;
phase += delta;
phase -= (float)((int)phase);
saw = phase * 2.0f - 1.0f;
tri = (saw > 0.0f)? saw: -saw;
tri = tri * 2.0f - 1.0f;
*outputL++ = *outputR++ = tri * gate * 0.5f;
}
}
void SimpleSynthCore::midi(unsigned char status, unsigned char data1, unsigned char data2, int /*sampleOffset*/)
{
unsigned char s = status & 0xf0;
if ((s == 0x90) && (data2 > 0))
{
delta = deltaData[data1 & 0x7f];
gate = 1.0f;
}
else if ((s == 0x80) || ((s == 0x90) && (data2 == 0)))
{
gate = 0.0f;
}
}
シンセのコアとJUCEのAudio処理を繋ぐ
上記のクラスをSimpleSynthAudioProcessorから使う。
SimpleSynthAudioProcessor.h
# pragma once
# include "../JuceLibraryCode/JuceHeader.h"
# include "SimpleSynthCore.h" // ヘッダファイルを追加
class SimpleSynthAudioProcessor : public AudioProcessor
{
public:
SimpleSynthAudioProcessor();
~SimpleSynthAudioProcessor();
(中略)
// ホスト以外からMIDIメッセージを受けるためのメソッド
void midi(unsigned char status, unsigned char data1, unsigned char data2);
private:
ScopedPointer<SimpleSynthCore> core; // シンセのインスタンスを保持するメンバを追加
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SimpleSynthAudioProcessor)
};
SimpleSynthAudioProcessor.cpp
(前略)
// コンストラクタ
SimpleSynthAudioProcessor::SimpleSynthAudioProcessor()
# ifndef JucePlugin_PreferredChannelConfigurations
: AudioProcessor (BusesProperties()
#if ! JucePlugin_IsMidiEffect
#if ! JucePlugin_IsSynth
.withInput ("Input", AudioChannelSet::stereo(), true)
#endif
.withOutput ("Output", AudioChannelSet::stereo(), true)
#endif
)
# endif
{
core = new SimpleSynthCore();
addParameter(new SimpleSynthParameter);
}
// デストラクタ
SimpleSynthAudioProcessor::~SimpleSynthAudioProcessor()
{
core = nullptr;
}
// 最初の初期化時を含め、サンプルレートが変更になった場合に呼ばれる
void SimpleSynthAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
// Use this method as the place to do any pre-playback
// initialisation that you need..
core->reset(sampleRate, samplesPerBlock);
}
// Audio処理のコールバック
void SimpleSynthAudioProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages)
{
// 最初にMIDIメッセージを処理
if (!midiMessages.isEmpty())
{
int i;
MidiMessage m;
MidiBuffer::Iterator it(midiMessages);
while (it.getNextEvent(m, i))
{
const unsigned char* rawData = (unsigned char*)m.getRawData();
core->midi(rawData[0], rawData[1], rawData[2]);
}
}
ScopedNoDenormals noDenormals;
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();
for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
buffer.clear (i, 0, buffer.getNumSamples());
// Audioのバッファをシンセのコアに渡す
float* temp[2];
temp[0] = buffer.getWritePointer (0);
temp[1] = buffer.getWritePointer (1);
core->process(temp, buffer.getNumSamples());
}
// ホスト以外からMIDI受けしたものもシンセに渡す
void SimpleSynthAudioProcessor::midi(unsigned char status, unsigned char data1, unsigned char data2)
{
core->midi(status, data1, data2);
}
UI上の鍵盤とも繋ぐ
SimpleSynthMainComponent.cpp
void SimpleSynthMainComponent::handleNoteOn (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity)
{
processor->midi((unsigned char)(0x90 | (midiChannel & 0x0f)), (unsigned char)midiNoteNumber, (unsigned char)(velocity * 127.0f));
labelNote->setText(String::formatted("%d", midiNoteNumber), dontSendNotification);
}
void SimpleSynthMainComponent::handleNoteOff (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity)
{
processor->midi((unsigned char)(0x80 | (midiChannel & 0x0f)), (unsigned char)midiNoteNumber, (unsigned char)(velocity * 127.0f));
labelNote->setText(String("---"), dontSendNotification);
}
ホントにインチキというか手抜きな実装なので、モノフォニックだし、折り返しノイズ対策してないし、パツパツノイズ出まくりだし、ノートオン・オフの処理もいい加減だけど、これで一応音は鳴る。