本記事は理情 Advent Calendar 2025の23日目の記事です (多忙により数日遅刻しての投稿です)。
自己紹介
どうもこんにちは、 IS25er1のJam Yabusame (William Jones)と申します。
Qiitaは初投稿なのでどうぞお手柔らかに... 学科内では()内のハンネでよく知られていますが、よく使っているのはJam Yabusameという名前なのでどうかよろしくお願いします。こんな記事を書くくらいなので一応作曲家を名乗っています。
(よろしければ作った曲を聞いてみてください)
https://soundcloud.com/419zypkt9ag6
事故(?)の経緯
今年も東京大学理学部情報科学科ではCPU実験2が行われ、白熱した競争が繰り広げられています。私も例に漏れずコア係として懸命にCPU本体の作成に取り組んでいるわけですが、コアの作成は大変疲れるものです。バグを見つけては治すの繰り返しであり、2桁時間パソコンに向かう日もあります。
実はCPU実験には余興という概念があります。やることは単純で、ストレスフルなデバッグの傍ら、開発したCPU、コンパイラ、自らの技量、$\mathbf{creativity}$を活かして何か悪ふざけをするというものです。
私は多分鬱憤が溜まって配布されたFPGAをいろいろいじってやろうという気持ちになりました。そして、色々調べていくうちにこんな機能を見つけました:
The on-board audio jack (J8) is driven by a Sallen-Key Butterworth Low-pass 4th Order Filter that provides mono audio output. The circuit of the low-pass filter is shown in Figure 15.1. The input of the filter (AUD_PWM) is connected to the FPGA pin A11. A digital input will typically be a pulse-width modulated (PWM) or pulse density modulated (PDM) open-drain signal produced by the FPGA.
(https://digilent.com/reference/programmable-logic/nexys-a7/reference-manual)
要約すると、このFPGAは音を出せます。
「これはもうシンセサイザ作るしかないでしょ!」
もっとkwsk
いちおうこの記事のタイトルにある "FPGA" と "シンセ (= シンセサイザー)" の2つについて超ざっくり説明したいと思います。
FPGA
まずこの記事を読んでいる人は多分知っているFPGAとはField Programmable Gate Arrayの略称です。で、こいつで何ができるのかというと、回路素子を一つ一つ選んで基盤に付けなくてもコード上の記述を元に論理回路を合成して作ることができます。ここでいう"コード"というのはハードウェア記述言語 (HDL)によるもので、私はSystemVerilogという言語でシンセサイザの大部分を作成しました。ここまで聞くとマイコンに似ていると思われるかもしれませんが、明確にできることの幅が違います。マイコンは既にプロセッサを搭載済みなのでその処理能力に依存した実行形態になりますが、FPGAはあくまでミニマムな論理回路の組み合わせなので設計次第では性能に大きな差をつけることができます。ちなみに、 今回私が使ったのは Nexys A7 100Tというもので、比較的安価なモデルだそうです。 (それでも4,5万円しますが...)
シンセサイザー
シンセサイザーと聞いて、それが何かを説明できる人は案外少ないと思います。おそらく真っ先に思い浮かぶのは電子ピアノのような鍵盤がついたなにかである、というイメージではないかと思いますが、これは中らずとも遠からずです。シンセサイザーも多種多様なのでかなりざっくりとした説明にはなりますが、その名の通り音波を合成 (synthesize) する楽器がシンセサイザーです。よって、鍵盤が付いていて、押すと電子音が鳴るというだけだとシンセサイザーと呼べるか微妙です。音波の合成とは、周波数を少しずつずらした複数のノコギリ波を足し合わせてシュワシュワした音を作ったり (所謂supersaw)、正弦波と矩形波を1:1で足したり... などです。
(多分世界一有名なsupersawの使用例)基本的にはこの音をそのまま使用するだけでなく、LPフィルタやHPフィルタを駆使して特定の周波数帯を強め/弱めたり、エンベロープを使って音量を時間変化させたりすることで豊かな音表現が可能になります
このような方式のほかにFMシンセ3など若干思想の異なった方式もありますが、それは今後余裕があれば実装できればと思います。
さて、今回私が制作しているシンセサイザーは波形テーブルを用いてデジタル波形をFPGAに内蔵されているBRAMから読み出して合成する方式です (最近のソフトシンセだとこのような使い勝手になっていることが多い気がしますが、どうなんでしょうか)。詳しくは以下のようなパイプラインを構成します。
1. 与えられた周波数に従って波形を読み出す
2. 各チャンネルで音波を足し合わせる
3. エンベロープをつける
4. エフェクトをかける
5. 各チャンネルの出力を足し合わせる
6. 出力
この記事では太字の制作です。
矩形波、三角波、ノコギリ波などの基本波形はFPGAの内部で容易に生成できるのですが、今後の発展を見据え、まずは汎用性の高そうな方式としてすべての波形をメモリに乗せて読み出すことにしました (特に、今回の目標の正弦波は計算して出力するより素直に波形テーブルを参照する方が容易だと思います)。
この図を見ると意外と単純そうですが、意外に躓く点があります。今回の記事では正弦波を出力するところまでまとめようと思います。
音波の出力
PWM
早速FPGAでシンセサイザーを作っていきたいわけなのですが、まずは音波の出力がどのように実現できるかを知らなければなりません。そもそも、合成した波形 (例えば、サンプルレート48kHz, 量子化ビット数16)は直接スピーカーに電気信号として送って音を鳴らすことができるできるわけではありません。そこで、このFPGAの制約ファイルを眺めてみます。
set_property -dict { PACKAGE_PIN A11 IOSTANDARD LVCMOS33 } [get_ports { AUD_PWM }]; #IO_L4N_T0_15 Sch=aud_pwm
ご覧の通り、音声出力用のポートは1ビット分しかありません。ポート名を見ればわかる通り、PWM方式で変調する必要があります。日本語だとパルス幅変調と呼んだりしますが、簡単に言えば、「矩形波の1 or 0 の振幅を時間平均していろんな波形を再現できるよ!」というもので、有名な例だと電車のモーター制御に使われていますね。あまり音波に対して使うのは一般的でなさそうで、PDM (パルス密度変調) のほうが主流らしいですが、PWMでもそれなりの音は出力できました。
PWM, PDMの詳しい実装はこちらの記事を参照するといいかもしれない。
実はPWMの実装はそれほど難しくなく、下のような比較的短い回路でどうにかなりました。このあたりは想像していたより簡単でよかったですね。(この実装では高々58kbpsが最高品質です。PDMならもっと出せるはず。)
`timescale 1ns / 1ps
`default_nettype none
module pwm_output # (
parameter SAMPLE_CLK = 4166,
parameter CLK_FREQ = 100_000_000
)(
input wire clk,
input wire resetn,
input wire signed [21:0] amp,
output wire aud_pwm,
output wire aud_sd
);
reg [31:0] hold_counter;
reg [31:0] counter;
reg pwm_reg;
wire [21:0]amp_norm;
wire [18:0]amp_abs;
reg signed [21:0] amp_reg;
assign aud_pwm = pwm_reg;
assign amp_abs = {~amp_reg[18], amp_reg[17:0]};
assign aud_sd = 1'b1;
always @(posedge clk)begin
if(~resetn)begin
amp_reg <= 22'b0;
hold_counter <= 32'b0;
counter <= 32'b0;
pwm_reg <= 1'b0;
end else begin
if(hold_counter == SAMPLE_CLK)begin
amp_reg <= amp;
counter <= 32'b0;
hold_counter <= 32'b0;
end else begin
hold_counter <= hold_counter+32'b1;
counter <= counter + 32'b1;
end
pwm_reg <= (counter[11:0] < amp_abs[18:7]);
end
end
endmodule
波形テーブル
波形テーブルには再生する音波の1周期分を1024点でサンプリングしたものを用いました。冷静になって再考するとかなりオーバーサンプリング気味です。 振幅は符号付き16ビットで量子化しましたが、PWMの実装では活かしきれないので要改良。
今回の目標である正弦波のほかに、矩形波、三角波、ノコギリ波、短周期ノイズもせっかくなので入れてみました (このへんはハードウェアで生成したほうが効率が良さそうですね)。波形テーブルはFPGAのBRAMの初期値として与え、 ビットストリーム生成時に自動的に書き込まれます。生成には下のようなコードを書いてみました。
#include <stdio.h>
#include <math.h>
int main(){
printf("memory_initialization_radix=16;\n");
printf("memory_initialization_vector=");
double phi = 2.0*(3.14159265358979)/1024.0;
//正弦波
for(int i = 0; i<1024; i++)printf("%hx ",(short)(32767 * sin(phi*i)));
//矩形波
for(int i = 0; i<1024; i++)printf("%hx ",(short)((i < 512)? 32767 : -32768));
//三角波
for(int i = 0; i<1024; i++){
if(i < 512)printf("%hx ",(short)((32767.0 * i)/256.0 - 32767));
else printf("%hx ",(short)(-(32767.0 * (i-512))/256.0 + 32767));
}
//ノコギリ波
for(int i = 0; i<1024; i++){
printf("%hx ",(short)((32767.0 * i)/512.0 - 32767.0));
}
//短周期ノイズ
for(int i = 0; i<1024; i++){
if(i<1023)printf("%hx ",(short)(0.5*(rand() % 65536 - 32768.0)));
else printf("%hx;",(short)(0.5*(rand() % 65536 - 32768.0)));
}
return 0;
}
* ノイズに関しては1周期分で同じパターンが繰り返される関係で所謂短周期ノイズになってます。
周波数の表現
地味に厄介なのが周波数の扱いです。シンセサイザーを楽器として成立させるには様々な周波数で音を鳴らせる必要があるでしょう。愚直に周波数をパラメータとして与えるとカウンタの更新をする際にFPGAのクロック周波数を音波の周波数で割った余りを求める必要がありますが、除算・乗算回路は論理回路が巨大になりやすいのでなるべく避けたいです (特に除算は合成すら難しい場合もある)。よって、パラメータとして与える数値は音波の周波数をクロック周波数で分周したものにするとよいです。この方式はそこまで特別ではないらしく、例えばファミコンのAPU4ではこの方式がとられているそうです。100MHzのFPGAの場合だと次のような数値をパラメータとして与えればよいことがわかります (表中の値は平均律に基づく):
| 音階 | 周波数 | 分周率 |
|---|---|---|
| ド (C4) | 261.626 | 382225 |
| ド♯ (C♯4) | 277.183 | 360772 |
| レ (D4) | 293.665 | 340524 |
| レ (D♯4) | 311.127 | 321412 |
| ミ (E4) | 329.628 | 303372 |
| ファ (F4) | 349.228 | 286346 |
| ファ♯ (F♯4) | 369.994 | 270275 |
| ソ (G4) | 391.995 | 255105 |
| ソ♯ (G♯4) | 415.305 | 240787 |
| ラ (A4) | 440.000 | 227273 |
| ラ♯ (A♯4) | 466.164 | 214517 |
| シ (B4) | 493.883 | 202477 |
今回は上の表中の周波数をハードコードして発声させる実装を行いましたが、次回以降は微分音 (半音以下の周波数のずれをもった音) に関しても扱えるようになる予定です。実際、現代的なシンセサイザーにはdetuneといって、敢えて整ったチューニングから外れた音を用いる機能が必ずと言っていいほどついているので欠かせません。
鍵盤の導入
せっかく音が出ても演奏ができないと正しい音が出ているかの確認が面倒です。そこで、USB接続のキーボードを使って演奏用の鍵盤を作ることにしました。これもFPGAにPS/2通信用のUSBホストが付いていたので実装は容易でした (同期通信なのにクロック入れ忘れてて数時間悩んだ)。が、キーが押しっぱなしになるバグが発生しており後の課題です。
`timescale 1ns / 1ps
`default_nettype none
module ps2_output
(
input wire clk,
input wire resetn,
input wire valid,
input wire[7:0] ps2_data_in,
input wire ready,
output wire[15:0] led,
output wire new_data,
output wire[7:0] key_code
);
reg[7:0] prev_data;
reg[7:0] ps2_data;
assign key_code = ps2_data;
assign led = {prev_data,ps2_data};
assign new_data = ready;
always @(posedge clk)begin
if(~resetn)begin
ps2_data <= 8'b0;
prev_data <= 8'b0;
end else begin
ps2_data <= ps2_data_in;
prev_data <= ps2_data;
end
end
endmodule
module ps2_driver (
input wire clk,
input wire resetn,
input wire ps2_clk,
input wire ps2_data,
input reg ready,
output reg[7:0]dout,
output reg busy
);
reg[7:0] data;
reg[7:0] data_prev;
reg [4:0]cnt;
reg valid;
always@(negedge ps2_clk)begin
if(~resetn)begin
data <= 8'b0;
ready <= 1'b0;
valid <= 1'b0;
cnt <= 32'b0;
end else begin
case(cnt)
0:busy <= 1'b1;//Start bit
1:data[0]<=ps2_data;
2:data[1]<=ps2_data;
3:data[2]<=ps2_data;
4:data[3]<=ps2_data;
5:data[4]<=ps2_data;
6:data[5]<=ps2_data;
7:data[6]<=ps2_data;
8:data[7]<=ps2_data;
9:valid <= 1'b1;
10:valid<= 1'b0;
endcase
if(cnt<=9) cnt<=cnt+1;
else if(cnt==10)begin
cnt<=0;
busy <= 1'b0;
end
end
end
always @(posedge valid)begin
ready <= 1'b1;
data_prev <= data;
dout <= (data_prev == 8'hf0)? data + 8'h80 : data;
end
endmodule
ちなみにキーボードと16進数は下のように対応しているようです:

(出典 https://digilent.com/reference/programmable-logic/nexys-a7/reference-manual?srsltid=AfmBOoqeAliXQN95aoXgO5Gmmoo18sfrZ6WtTgZde2mEsy2shfAl_gsJ)
正弦波再生
さて、必要なものは揃ったのでいよいよ正弦波を再生できます。とはいえ、ここまでに作ってきた断片的なモジュールを組み合わせる必要があります。詳しい実装は割愛しますが、下図のような構成です:
- キーボード入力をどのチャンネルにsendするかはFPGAのスイッチで切り替えられます
- PWMの前に音割れ防止のため振幅に1より小さい倍率をかけています
- 波形テーブルは一つでもよかったが、アービターを書くのが面倒だったので許して...
正弦波を鳴らすにはややオーバースペックですが、今後の発展を見越しています。
実際に再生されているところをお見せしようと思いましたが、非常に地味なので次回さらに改良したバージョンで紹介できたらと思います。
おわりに
ここまで頑張って制作をしましたが、当然正弦波や矩形波が再生できるだけではまだシンセサイザーとは呼べません。次回以降は複数の波形を重ね、さらにエンベロープ機能 (ADSR)やフィルターも実装してより本格的な形にしていきます。
最後までお読みいただきありがとうございました。


