基本波形をモーフィングする
( 1.0 - α ) * a + α * b
( 1.0 - α ) * a + α * b
YouTubeの動画の音声でも分かる「ギュルギュル」した音に気付いた人は、私の仲間です。
NuVCOの最初の記事では、STM32G431とPCM5102Aを使ってVCOを動かし、実際の音もYoutubeへアップしました。
最初の記事:
https://dad8893.blogspot.com/2026/07/nuvco-stm32vco.html
最初の記事の動画では、Sawtoothの周波数を上げていくと、音程とは別に動くような「ギュルギュル」した音が聞こえます。
固定した周波数で聞いているより、Tune POTを回して周波数を変化させたときの方が違和感 が出ます。
Sawtooth本来の音程とは別の音が、妙な動き方をしています。
Sawtoothは波形の端で、
+1 → -1
のように急激に変化します。
この不連続部分には非常に高い周波数まで高調波が含まれます。
NuVCOのSampling Rateは48kHzなので、Nyquist周波数は24kHzです。
24kHzを超えた高調波はそのまま再生できず、可聴帯域へ折り返してきます。
これがaliasingです。
Sawtoothの音程を変えると元の高調波も動きますが、折り返してきた成分は本来の高調波とは違う動きをします。
Tune POTを回したときに「ギュルギュル」というノイズは、最初はUSB接続で現れがちなノイズ、GNDループによって拾ってしまうデジタル機器のノイズなど疑いまくりました。
約1725HzのSawtoothをFFTで見ると、本来の高調波とは別に多くの成分が現れます。
周波数を変化させると、これらの成分も本来の音程とは違う動きをします。
耳で聞いていた「ギュルギュル」と対応しているように見えます。
Sawtoothに PolyBLEP を入れた場合です。
PolyBLEPは、Sawtooth全体へフィルターをかけるのではなく、波形が不連続になる部分の近くだけを補正する方法です。
今回使った一般的なPolyBLEPでは、正規化した位置を x とすると、
-x² + 2x - 1
のような2次関数を使って、不連続点付近の波形を少しなめらかにつなぎます。
実装では、現在の位相が不連続点から1 sample分程度の範囲にあるときだけ補正値を加えています。
つまり、Sawtooth全体の形を大きく変えるのではなく、問題になる急峻な部分だけを局所的に修正しています。
もともとどこで急峻な部分が現れるかがわかっているからできるのがPolyBLEPです。リアルタイムに検知しているわけではないのでご承知ください。
PolyBLEP適用後のFFTです。
初期状態で見えていた、本来の高調波とは異なる成分がかなり減りました。
聴感上の変化も分かりやすいです。
特にTune POTを回して周波数を連続的に変えたとき、PolyBLEP OFFでは「ギュルギュル」とした成分が目立ちます。
PolyBLEPをONにすると、その成分がかなり減ります。
一方で、Sawtoothらしい明るい音まで大きく失われた感じはありませんでした。
私の場合、固定した1音を聞き比べるよりも、周波数を動かしたときの方がPolyBLEPの効果はずっと分かりやすく感じました。
PolyBLEPをON / OFFしながら、Sawtoothの周波数を変化させた様子を動画でも撮影しました。
OFFのときは、Sawtoothの音程とは別に動く「ギュルギュル」した音に注目すると違いが分かりやすいと思います。
ONにすると、この成分がかなり小さくなります。
当然、PolyBLEPを入れると計算量は増えます。
NuVCOではCycle Counterを使って11 Voice動作時の処理時間も確認しました。
PolyBLEPを有効にしてもaudio生成のdeadline超過は発生せず、まだ十分な余裕がありました。
そのためNuVCOでは、Sawtooth系の波形についてPolyBLEPを基本的にONで使うことにしています。
今回面白かったのは、FFTを見る前にまず耳で違和感に気付いたことです。
単純なSawtoothでも、高い周波数へ上げていくとaliasingはかなり目立ちます。
特に周波数を連続して変化させると、本来の音程とは別に動く成分として聞こえるので分かりやすいです。
PolyBLEPという比較的軽い補正を加えるだけでも、その違和感はかなり減りました。
最初のNuVCO動画に入っていた「ギュルギュル」が、後になってこういう形で改善できたのは面白い結果でした。
前回までで、NuVCOは11 Voiceまで動作するようになり、Sine生成の処理時間についてもCycle Counterで測定し、LUT化まで行いました。
基本的なVCOを動かすこと以外にも、いろいろ試してみたいことが増えてきました。
例えば、現在考えているものには次のようなものがあります。
Detune Range
Detuneの可変域を設定する。
Voice Spread
Detuneの広がり方を設定する。
Pair Morph
Sine → Triangle → Square → Sawtoothという一続きのMorphだけでなく、Sine → Square、Triangle → Sawtoothなど、2つの波形を選んでMorphする。
Voiceの初期phase
複数Voiceを同じphaseから始めるか、Voiceごとにphaseをずらすかなどを設定する。波形の重なり方が変わるので、音の立ち上がりや厚みなどに違いが出る可能性がある。
Stereo / Pan
MorphingやDetuneしたVoiceをL/Rへ振り分け、Stereoの広がりを持たせる。
Sub
1 Octave下の波形を生成してMixする。
これらは私にとって未知の世界です。
名前や考え方は分かっていても、NuVCOで実際に音を出したときにどういう効果になるのかは、試してみないと分かりません。
最初のNuVCOの設計では、
Tune POT
Wave POT
Detune POT
USER1 SW
USER2 SW
を使っていました。
Tune POTは基準周波数、Wave POTはSine → Triangle → Square → SawtoothのMorph、Detune POTは複数VoiceのDetune量を操作します。
USER1 SWではVoice数を、
1 / 2 / 3 / 5 / 7 / 9 / 11
と切り替え、USER2 SWではTune Rangeを切り替えるようにしました。
この方法で機能を増やしていく場合、機能ごとにPOTやSWを追加する必要があります。
部品や配線が増えるだけでなく、Nucleo-G431KB側もADCやGPIOとして自由に使えるpinには限りがあります。
UI用のI2Cを追加したときにも、pinの制約が実際に出てきました。
当初はI2C2を、
SCL: PA9
SDA: PF0
で使う予定でした。
ところがPF0が実機でうまく使えなかったため、SDAをPA8へ変更しました。
PA8は、それまでUSER1 SWのVoice数切り替えに割り当てていたpinです。
そのためI2Cを組み込んだ直後は、USER1 SWによるVoice数切り替えが使えなくなりました。
今はVoice切り替えをMCP23017側へ移しています。
このように、POTやSWを単純に増やしていく方法では、部品数だけでなくMCUのpin資源にも制約があります。
まだNuVCOに搭載する音作り機能は絞れていません。
そこで、最終形のUIを決める前に、いろいろな機能を試すための実験用UIを用意しました。
現在の状態を確認するためにOLEDを使い、必要な設定値も表示します。
値の変更にはRotary Encoderを使います。
1個のPOTで複数のパラメータを切り替えて操作する方法もありますが、POTには物理的な位置があります。
例えば、あるパラメータを大きな値に設定してPOTが右側にある状態で別のパラメータへ切り替えると、POTの位置と新しいパラメータの値が一致しません。
Rotary Encoderなら絶対的な位置を持たないので、表示している現在値を基準に増減できます。
OLEDで現在の項目と値を表示し、Encoderで変更する方式なら、操作するパラメータが増えても主にFirmware側の変更で対応できます。
さらにMCP23017を使い、SwitchやLEDも追加できるようにしています。
NuVCOの音作りを試すために作ったUI Expander Proto-A。OLED、Rotary Encoder、MCP23017、5個のSwitchとLEDを搭載しています。
現在は、OLED、Rotary Encoder、MCP23017、Switch、LEDを NuVCO UI Expander Proto-A にまとめています。
NuVCO本体とはAdapter基板を介して接続します。
このUIは最終形ではなく、Detune RangeやVoice Spreadなど、これから試してみたい機能を操作するための実験用です。
機能を増やしても、すぐに専用POTや専用SWを追加する必要はなく、Firmware上で項目を追加し、OLEDで状態を確認しながらEncoderやSwitchで操作できます。
次回は、この実験用UIで使っているOLED、Rotary Encoder、MCP23017をどのように試し、NuVCOへ組み込んでいったかを書こうと思います。
前回は、STM32G431でNuVCOを11 Voice化したとき、Sineだけ波形が破綻した話を書きました。
前回の記事:
https://dad8893.blogspot.com/2026/08/nuvco11-voice-fpu.html
Triangle、Saw、Squareは11 Voiceでも動くのに、Sineだけは9 Voiceまでは正常で、11 Voiceにすると波形が崩れます。
当初はSineの生成にC標準ライブラリの sinf() を使っていました。
static float AppWaveform_GenerateSine(float phase)
{
return sinf(phase * two_pi);
}
11 Voiceでは、この sinf() の計算が間に合っていないのではないか。
前回の記事ではそこまで書きました。
sinf() が重いならLUT化すれば良い、というのはすぐ思いつきます。しかし今回は、まずCycle Counterで実際の処理時間を測り、コンパイラの最適化でどこまで行けるか試しました。
Release相当の最適化を使うと一度は11 Voice Sineも正常に動作します。しかし、その後機能を追加していくと再び処理時間の限界に達しました。そこで最後に、SineをLUT化しています。
今回は、この 測定 → 最適化 → 再び限界 → LUT化 という流れを振り返ります。
NuVCOのPCM5102Aへのオーディオ出力は、公称48kHzです。
DMAでは256 frames分のバッファを用意し、その半分である128 framesごとに次のデータを生成しています。
つまり128 framesを再生する時間は、
128 / 48000 = 0.002666...
約 2.667ms です。
STM32G431は170MHzで動作しているので、CPU cycleにすると、
170,000,000 × 128 / 48,000
= 約453,333 cycles
となります。
この 453,333 cycles が重要です。
最初は「2.667msくらい時間がある」と考えていましたが、リアルタイムのオーディオ処理では、これは余裕時間ではありません。
次のDMA転送に間に合わせるための締切です。
128 framesの生成にこれ以上かかると、次に送るべきオーディオデータの準備が間に合わなくなります。
STM32G431のCortex-M4には、DWT(Data Watchpoint and Trace)のCycle Counterがあります。
CPUが何cycle動いたかを数えるカウンタです。
初期化は簡単に書けばこのようなコードです。
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
初期化したら、測りたい処理の前後で DWT->CYCCNT を読みます。
start = DWT->CYCCNT;
/* 128 framesを生成 */
elapsed = DWT->CYCCNT - start;
NuVCOではDMAのhalf/full callbackから呼ばれる128-frame生成処理の前後を測定しました。
さらに、
最後のcycle数
最小値
最大値
累積cycle数
測定回数
453,333 cyclesを超えた回数
を変数に保存するようにしました。
Cycle Counterの詳しい使い方はAIに質問してみましょう。
測定値を見るのにはSTM32CubeIDEの Live Expressions を使いました。
Live Expressionsは最初は驚くと思います。MCUを動作させたまま、その内部にある変数をPCでほぼリアルタイムに観察出来ます。
NuVCOでは、
app_vco_active_voice_count
app_audio_perf_max_cycles
app_audio_perf_deadline_cycles
app_audio_perf_overrun_count
app_audio_perf_measurement_count
などを表示しておけば、音を出したままVoice数、最大処理時間、deadline、deadline超過回数などを確認できます。
Voice 11
Max cycles 168,563
Deadline cycles 453,333
Overrun 0
Measurement count 40,202
となっています。
現在のLUT版では、11 Voice Sineでもdeadlineまでかなり余裕があることが分かります。
※ この画像は、
sinf()でサイン波を生成していたときのLive Expressions画面ではありません。サイン波生成にLUTを使った場合の測定値です。
まずSTM32CubeIDEの通常のDebug構成で測りました。
| 条件 | Average | Max | Deadline超過 |
|---|---|---|---|
| 9 Voice Sine | 約424,534 cycles | 448,349 cycles | 0 |
| 11 Voice Sine | 約500,839 cycles | 536,504 cycles | 15,892回 |
| 11 Voice Saw | 約199,000 cycles | 199,040 cycles | 0 |
11 Voice Sineは23,378回測定して、15,892回がdeadlineを超えています。
約68%です。
時間に直すと、
11 Voice Sine Average
500,839 / 170MHz ≒ 2.946ms
11 Voice Sine Max
536,504 / 170MHz ≒ 3.156ms
です。
締切は約2.667msですから、最大値だけがたまたま超えたわけではありません。
平均値の時点ですでに間に合っていません。
一方、9 Voice Sineの最大値は448,349 cycles、約2.637ms。
締切の453,333 cyclesまで、わずか約29μsしかありません。
9 Voiceで動いていたのも、かなりぎりぎりだったことが分かりました。
コンピューターにとって、Sineは計算量が多いですが、Sawはかなり簡単な計算で済みます。
Debug構成でも11 Voice Sawは約199,000 cycles、時間にすると約1.17msでした。
同じ11 Voiceなのに、Sineとは大きな差があります。
当時のSine生成では、1 Voiceにつき1 sampleごとに sinf() を1回呼んでいました。
128 frames × 11 Voiceなので、半バッファを1回生成する間に、
1,408回の sinf()を呼ぶ計算になります。
STM32G431には単精度FPUがありますが、sinf() は「FPUのSIN命令を1回実行すれば終わり」という処理ではありません。
各sample、各Voiceで sinf() を大量に呼び出すSine生成処理が、リアルタイムオーディオの締切を超えていました。
ここでもうひとつ重要なことが分かりました。
STM32CubeIDEのDebug構成では、C Compilerの最適化が、
-O0
になっています。
一方、NuVCOのRelease構成では、
-Os
です。
つまりDebugとReleaseでは、単に「デバッグ情報が入っているかどうか」だけでなく、コンパイラの最適化条件そのものが違います。
性能を測るときには、この差を無視できません。
実際、初期の11 Voice SineをRelease相当の -Os で測ると、
| 条件 | Average | Max |
|---|---|---|
| 11 Voice Sine / Debug | 約500,839 | 536,504 |
| 11 Voice Sine / ReleasePerf | 約305,747 | 313,841 |
まで減りました。
Debug版だけを見て、「STM32G431では11 Voice Sineは無理」と判断するのは早かったわけです。
しかし普通のRelease構成ではデバッグ情報がありません。
Cycle Counterの値をLive Expressionsから眺めたいので、NuVCOでは ReleasePerf というBuild Configurationを追加しました。
内容はシンプルです。
Base : Release
C optimization : -Os
C debug information: -g3
FPU : fpv4-sp-d16
Float ABI : hard
つまり、
Releaseの最適化を維持したまま、Cのデバッグ情報を残した性能測定用構成
です。
STM32CubeIDEで同じものを作る場合は、
Projectを右クリック
Build Configurations → Manage...
Releaseを複製して ReleasePerf を作成
C/C++ Build → Settings
C CompilerのOptimizationを -Os
Debug informationを -g3
ReleasePerf用のELFを指定してDebug
という流れです。
NuVCOでは、
ReleasePerf/NuVCO-G431KB.elf
をデバッガから使っています。
これで、Releaseに近い最適化条件のコードを、ST-LinkとLive Expressionsを使いながら観察できます。
初期の11 Voice Sineは、ReleasePerfにすると最大313,841 cyclesまで下がり、453,333 cyclesの締切内に収まりました。
この時点では一度問題が解決しています。
しかし、その後NuVCOに機能を追加していくと、再びSineが限界へ近づいていきました。
Sine LUT化直前のReleasePerf測定がこちらです。
| Voice | Wave | Max cycles | 時間 | Overrun |
|---|---|---|---|---|
| 1 | Sine | 69,122 | 約0.407ms | 0 |
| 3 | Sine | 158,926 | 約0.935ms | 0 |
| 5 | Sine | 236,074 | 約1.389ms | 0 |
| 7 | Sine | 315,203 | 約1.854ms | 0 |
| 9 | Sine | 392,026 | 約2.306ms | 0 |
| 11 | Sine | 461,587 | 約2.715ms | 146 |
| 11 | Triangle | 123,145 | 約0.724ms | 0 |
条件はReleasePerf -Os、55Hz付近です。
Voice数を増やしていくと処理時間も増え、
9 Voiceまではdeadline内、11 Voiceで453,333 cyclesを超えました。
同じ11 VoiceでもTriangleは123,145 cyclesしか使っていません。
前回の記事で感じていた、
「11 Voice Sineだけ何かがおかしい」
が、cycle数としてはっきり見えました。
リアルタイム処理では、
一度性能測定して動いたから大丈夫
ではありません。
機能を追加すれば、それまであった処理時間の余裕を少しずつ削っていきます。
機能追加後にもう一度測る必要があります。
そこで、整数DDSのころから使い慣れたSineのLUT化を行いました。
もちろん今回は整数の波形テーブルではなく、単精度浮動小数点数の波形テーブルです。足りない部分は計算で線形補間するという、少し贅沢なLUT化です。
NuVCOでは、
full-cycle 256分割
wrap用を含め257要素
float
Flashへ配置
隣接2点を線形補間
という方法にしました。
LUT化後の11 Voice Sineは、当時の測定で、
Max cycles: 168,004
Overrun: 0
でした。
時間にすると、
168,004 / 170MHz ≒ 0.988ms
です。
LUT化直前の、
461,587 cycles ≒ 2.715ms
から、
168,004 cycles ≒ 0.988ms
まで減りました。
締切2.667msに対して、約1.68msの余裕があります。
今回、現在のFirmwareでもう一度Live Expressionsを表示してみたところ、11 Voice Sineで最大168,563 cyclesでした。
過去に記録していた168,004 cyclesとほぼ同じです。
LUT化で正常に動くようになっただけでなく、処理時間にかなり余裕を作ることができました。
これは当時Analog Discovery 2で観測した11 Voice Sineの破綻波形です。
正弦波そのものは生成されていますが、波形途中に大きな不連続が現れています。
この状態では周期的な異音が発生し、Tune / Wave / Detune POTも反応しなくなりました。
デバッガを切断してLive Expressionsを表示しない状態でも11 Voice Sineの破綻は再現しました。したがって、Live Expressionsを表示していたこと自体が原因ではありません。
※ CH2に見える高周波成分は、11 Voice Sineの処理破綻そのものとは別の現象です。撮影時、オシロ測定点と並列になっているオーディオ出力をBehringer UMC404HDの入力へ接続しており、その状態で発生していました。今回注目しているのは、波形途中に現れる大きな不連続です。
写真は今回のCycle Counter測定当時そのものではなく、現在の実験環境です。
機能を増やしていくにつれて、最初のPOTとUSERスイッチだけでは操作が難しくなってきました。
このUIの話は次回以降に書こうと思います。
今回Cycle Counterを使ってみて良かったのは、
「たぶん
sinf()が重い」
という推測を、
「128 framesを作るのに2.667ms以上かかっている」
という数字で確認できたことです。
11 Voice Sineでは締切を超える。
11 Voice SawやTriangleなら余裕がある。
Release最適化をすると速くなる。
それでも機能追加を続けると、また締切に近づく。
SineをLUT化すると大きく余裕が増える。
同じcycle数という尺度で比較すると、何が起きていたのかが分かりやすくなりました。
組み込みのリアルタイム処理では、「動いた/動かなかった」だけを見ていても原因が分からないことがあります。
今回のNuVCOでは、2.667msという締切を意識してCycle Counterで測ることで、11 Voice Sineが壊れた理由を数字で確認できました。
そしてもうひとつ、今回覚えておきたいのがBuild Configurationです。
STM32CubeIDEではDebugとReleaseだけを使う必要はありません。
Releaseをベースに最適化を残したBuild Configurationを作れば、実際の動作速度に近い状態でST-LinkとLive Expressionsを使って性能を観察できます。
今回作った ReleasePerf は、今後STM32で処理時間を調べるときにも使えそうです。
次は、11 Voice化やMorphing、Detuneなど実験したいことが増えた結果、POTとUSERスイッチだけでは操作しづらくなってきた話を書こうと思っています。
NuVCOを「VCOを作るプロジェクト」から、いろいろな音作りを実験できるVCOへ広げるため、OLEDやロータリーエンコーダを使ったUIを追加していきます。
1 ÷ 48,000 ≒ 20.8マイクロ秒
128 ÷ 48,000 ≒ 2.667ミリ秒
現在はNuVCO、ArLFO、TLF01、Dual OTA VCAを掲載しています。今後も順次追加していきます。
製作過程や実験の詳しい記録はこれまでどおりこのBlogに掲載し、完成したものや開発中のモジュールはPNPN Manufactoryに整理していきます。
[製作モジュールを見る]
しばらくごぶさたしておりました。
Arduinoを使った ArLFO を製作して以来、しばらくシンセDIYから離れていましたが、久しぶりに新しいモジュールを作り始めることにしました。
今回のテーマは STM32を使ったデジタルVCO です。
NuVCOは、STM32 Nucleoボードを使ったデジタルVCOです。
名前の Nu は Nucleo の頭文字から取りました。また、"New" の意味も少し込めています。
これまでにもNucleoボードを使っていくつか音源を試作してきましたが、今回は本格的なEurorack向けVCOとして開発を進めます。
これまでにディスクリートVCOや3340を使ったアナログVCOをいくつか製作してきました。
アナログVCOには独特の揺らぎがあり、複数のVCOを重ねるだけでも温かく太い音になります。これはアナログならではの魅力です。
一方で、生成できる波形は比較的シンプルです。
もし、
SuperSawのような厚いDetune
デジタルシンセならではの複雑な波形
リアルタイムで変化するWave Morphing
こんな音をモジュラーシンセに持ち込めたら面白いのではないか。
そんな思いから、このプロジェクトを始めました。
もちろん、市販のデジタルVCOを買った方が安くて高性能でしょう。
それでも、自分で作る楽しさには抗えません。
今回の目標はシンプルです。
STM32G431 + PCM5102AによるデジタルVCO
16bit / 48kHzオーディオ
1V/Oct入力対応
サイン波・三角波・ノコギリ波・矩形波
将来的にはDetuneやWave Morphingも実装
デジタルならではの自由度を活かしながら、Eurorackの世界で気軽に使えるVCOを目指します。
コアとなるマイコンは STM32G431 を搭載した Nucleo-32。
オーディオDACには PCM5102A を使用しています。
どちらも比較的入手しやすく、性能も十分です。
基板を作る前は、ブレッドボード上でひたすら実験を繰り返していました。
現時点では次の機能が動作しています。
正弦波
三角波
ノコギリ波
矩形波
Waveノブによる波形切り替え
Tuneノブによる周波数変更
2 Voice Detune
ノコギリ波はこんな感じです。
PCM5102Aには内部デジタルフィルタが入っているため、急峻な波形では少しリンギングが見られます。
今回はあえてDACの生の出力を観察しています。このクセをどう扱うかも今後の楽しみの一つです。
2 Voice Detuneも動き始めました。
まだ完成ではありませんが、「おっ、シンセらしい音になってきた」と感じられるところまで来ています。
ソフトウェアはSTM32CubeMXとCubeIDEを使っています。
音声出力はI2S + DMA。
波形生成はDDS(位相アキュムレータ方式)で実装しました。
STM32G431にはFPUが搭載されているため、現在はWavetableを使わず浮動小数点演算でリアルタイム生成しています。
今後は処理速度や音質とのバランスを見ながら、アルゴリズムを改良していく予定です。
まだまだやりたいことはたくさんあります。
Wave Morphing
PolyBLEPによるアンチエイリアス
1V/Octキャリブレーション
Voice数の切り替え
Proto-B基板での評価
デジタルVCOならではの自由度を活かして、少しずつ育てていこうと思っています。
ようやく「音が出る」段階までたどり着きました。
ここからは音質や演奏性を磨きながら、本格的なEurorackモジュールへ仕上げていきます。
今後はDDSの実装やPolyBLEP、Detuneアルゴリズム、1V/Octの調整など、開発の過程も順番に紹介していく予定です。
お楽しみに。
USER SWで1 Voice、2 Voice Detune、3 Voice Detuneを切り替えた様子を動画にしました。Detune POTを回したときの音の変化も確認できます。