JUCEを使って、サイン波形を作って音を鳴らす。
簡単なはず。と試みて、つまづきそうな点を例に上げていきます。
まずはJUCEをダウンロード
JUCEのサイトからGet JUCEで、ダウンロード。
自分の環境はMacなので、Mac版をダウンロードしています。
Projucerを起動
アプリケーションフォルダにJUCEフォルダをドロップして、中にあるProjucerを起動。
ファイルメニューからNew Project>AudioPlug-inを選択。
Project名はSinSynthとでもしておきます。
Createを押して、
右上のXCodeのアイコンをクリックすると
XCodeが起動します。
左上のメニューからSinSynthのアプリを選びます。
再生ボタンみたいなビルドボタンを押して実行し、
MIDIキーボードを繋いで、
オプションから選択しておきます。
まだ、鍵盤を押しても何も音がしません。
Projucerにもどって、
左上の歯車アイコンから、設定を開いて、
Plugin is a Synth
Plugin MIDI Input
をチェックします。
右上のXCodeアイコンを押して、XCodeでビルド
Optionを見ると、オーディオインプットが無くなっています。
Instrumentとしてビルドされたようです。
まだ、MIDI鍵盤押しても音がしません。
Sin波形を鳴らしたい
Projucerにもどって、
Sin波形を鳴らすコードを書きます。
PluginProcessor.cppのSinSynthAudioProcessor::processBlockというところにコード書きます。
for (int channel = 0; channel < totalNumInputChannels; ++channel)
{
auto* channelData = buffer.getWritePointer (channel);
// ..do something to the data...
for(int sample = 0;sample < buffer.getNumSamples();sample++)
{
channelData[sample] = std::sin(sample);
}
}
音が出ない?
そう
totalNumInputChannelsは0ですね。
ここをtotalNumOutputChannelsにします。
何かやばいサイン波形 pic.twitter.com/jsTW0rZQoi
— tatmos (@tatmos) 2018年11月23日
音は一応でました。
しかし、これはサイン波形ではありません。
std::sin()の引数にsampleを直接入れてはいけません。
これは、サイン波形を0~NumSamplesの数までの波形を作っているのですが、
この関数は、外から呼ばれ、どれくらいの周期で呼ばれるかがわかりません。
ためしに、SampleRateやAudio buffer sizeを変えて見ると、なる音の高さや音色が変化します!!
これはよくない
音色をなんとかする
おそらく、buffer sizeごとに波形が細切れになって、それが繋がって鳴っているので、
buffer sizeの音が聞こえています。
buffer sizeに依存しないように
static int phase = 0;
for (int channel = 0; channel < totalNumOutputChannels; ++channel)
{
auto* channelData = buffer.getWritePointer (channel);
// ..do something to the data...
for(int sample = 0;sample < buffer.getNumSamples();sample++)
{
channelData[sample] = std::sin(phase++);
}
}
少し滑らかになったけどまだ変ですね。
はい、これステレオですね。
static int phase = 0;
int startPhase = phase;
for (int channel = 0; channel < totalNumOutputChannels; ++channel)
{
phase = startPhase;
auto* channelData = buffer.getWritePointer (channel);
// ..do something to the data...
for(int sample = 0;sample < buffer.getNumSamples();sample++)
{
channelData[sample] = std::sin(phase++);
}
}
開始phaseを入れて、ステレオで同期しておくようにしておきました。
(片方作って、コピーでも良いね)
めっちゃ高い音なんやねん
これ、sin波形に周波数要素が絡んでいません。
音程を鳴らしたい、440Hzを鳴らしたい。
そんな時はこんな感じ
channelData[sample] = std::sin(2.0 * double_Pi * 440.0 * (float)phase++ / 44100.0);
これで、落ち着いたサイン波形が得られました。
あ、このままだと再生周波数変えると音が変わってしまうので
channelData[sample] = std::sin(2.0 * double_Pi * 440.0 * (float)phase++ / this->getSampleRate());
これでOK。
音がなりっぱなしで発狂しそう
NoteOn,NoteOffで音が鳴る、鳴らないにしてみましょう。
サイン波形は鳴らせたがプチプチ pic.twitter.com/sPUE3qApD7
— tatmos (@tatmos) 2018年11月23日
と、その前にチャンネル処理変更
2つめ以降は1つめのチャンネルのデータを流用するようにちょっと変更しておきます。
static int phase = 0;
for (int channel = 0; channel < totalNumOutputChannels; ++channel)
{
auto* channelData = buffer.getWritePointer (channel);
// ..do something to the data...
for(int sample = 0;sample < buffer.getNumSamples();sample++)
{
if(channel > 0)
{
channelData[sample] = buffer.getWritePointer(0)[sample];
continue;
}
channelData[sample] = std::sin(2.0 * double_Pi * 440.0 * (float)phase++ / this->getSampleRate());
}
}
音がね。プチプチいってます。なんとかしたい。
波形の始めと終わりにプチプチという音。
これ、波形の0でない時に一番大きく音がなります。
なだらかに0~1.0 1.0~0.0へと変化させることで解決します。
これ直すとこんな感じ。
static int note = 0;
static float amp = 0.0;
MidiBuffer::Iterator MidiItr(midiMessages);
MidiMessage MidiMsg;int smpPos;
while(MidiItr.getNextEvent(MidiMsg,smpPos))
{
if(MidiMsg.isNoteOn())
{
note = 1;
}
if(MidiMsg.isNoteOff())
{
note = 0;
}
}
static int phase = 0;
for (int channel = 0; channel < totalNumOutputChannels; ++channel)
{
auto* channelData = buffer.getWritePointer (channel);
// ..do something to the data...
for(int sample = 0;sample < buffer.getNumSamples();sample++)
{
if(channel > 0)
{
channelData[sample] = buffer.getWritePointer(0)[sample];
continue;
}
if(note == 1 && amp < 1.0f) amp += 0.01f;
if(note == 0 && amp > 0.0f) amp -= 0.01f;
channelData[sample] = amp * std::sin(2.0 * double_Pi * 440.0 * (float)phase++ / this->getSampleRate());
}
}
noteOff時に完全に0になっていないのか音が鳴っていますね。
static int phase = 0;
for (int channel = 0; channel < totalNumOutputChannels; ++channel)
{
auto* channelData = buffer.getWritePointer (channel);
// ..do something to the data...
for(int sample = 0;sample < buffer.getNumSamples();sample++)
{
if(channel > 0)
{
channelData[sample] = buffer.getWritePointer(0)[sample];
continue;
}
if(note == 1 && amp < 1.0f) amp += 0.01f;
if(note == 0 && amp > 0.0f) amp -= 0.01f;
if(amp < 0) amp = 0;
channelData[sample] = amp * std::sin(2.0 * double_Pi * 440.0 * (float)phase++ / this->getSampleRate());
}
}
音程が欲しい
440Hzだけだと寂しいのでMIDI NoteNoから音程をとってきて周波数に変換します。
static int note = 0;
static float amp = 0.0;
MidiBuffer::Iterator MidiItr(midiMessages);
MidiMessage MidiMsg;int smpPos;
while(MidiItr.getNextEvent(MidiMsg,smpPos))
{
if(MidiMsg.isNoteOn())
{
note = MidiMsg.getNoteNumber();
}
if(MidiMsg.isNoteOff())
{
note = 0;
}
}
static int phase = 0;
for (int channel = 0; channel < totalNumOutputChannels; ++channel)
{
auto* channelData = buffer.getWritePointer (channel);
// ..do something to the data...
for(int sample = 0;sample < buffer.getNumSamples();sample++)
{
if(channel > 0)
{
channelData[sample] = buffer.getWritePointer(0)[sample];
continue;
}
if(note != 0 && amp < 1.0f) amp += 0.0001f;
if(note == 0 && amp >= 0.0f) amp -= 0.0001f;
channelData[sample] = amp * std::sin(2.0 * double_Pi * ((440.0f * std::exp(0.057762265f * (note - 69.0f)))) * (float)phase++ / this->getSampleRate());
}
}
んー、まだぷちぷちいっていますね・・・。
音程もつけたけど、プチプチは改善しない・・・ pic.twitter.com/5LOoBx6Ndj
— tatmos (@tatmos) 2018年11月23日
まずい点、note==0の音がsin関数にわたっているので、即時無音になっている。(ゲートゆるやかにした意味ないし・・・)
static float note = 0;
static bool gate = false;
static float amp = 0.0;
MidiBuffer::Iterator MidiItr(midiMessages);
MidiMessage MidiMsg;int smpPos;
while(MidiItr.getNextEvent(MidiMsg,smpPos))
{
if(MidiMsg.isNoteOn())
{
gate = true;
note = MidiMsg.getNoteNumber();
}
if(MidiMsg.isNoteOff())
{
gate = false;
}
}
static int phase = 0;
for (int channel = 0; channel < totalNumOutputChannels; ++channel)
{
auto* channelData = buffer.getWritePointer (channel);
// ..do something to the data...
for(int sample = 0;sample < buffer.getNumSamples();sample++)
{
if(channel > 0)
{
channelData[sample] = buffer.getWritePointer(0)[sample];
continue;
}
if(gate == true && amp < 1.0f) amp += 0.01f;
if(gate == false && amp >= 0.0f) amp -= 0.01f;
channelData[sample] = amp * std::sin(2.0 * double_Pi * ((440.0f * std::exp(0.057762265f * (note - 69.0f)))) * (float)phase++ / this->getSampleRate());
}
}
だいぶプチプチは減ったが pic.twitter.com/LAhnv1z5gI
— tatmos (@tatmos) 2018年11月28日
音程をなだらかに変える
さらに
音程変更時に断絶しているのが原因とみて、
少し補間処理を入れるようにします。
実はタイミングが怪しい
で、どのタイミングでMIDIがトリガーされたかは、MIDIのsmpPosという値があって、これを使わないと、
audio buffer sizeに依存したnoteOn,noteOff になってしまっていました。
あと、ポリフォニックとか考えると・・・
音を変化させるのではなく交互にトリガーして、フェードするような仕組みにしたり・・・
あぁ、もう面倒です。どうしたらサイン波形が綺麗に鳴らせるのか?
JUCEにあるシンセを使う
JUCEのSynthモジュールを使うという手があります。