├── .gitignore
├── asset
└── screenshot.png
├── readme.md
└── src
├── dft.js
├── index.html
├── main.css
├── main.js
├── play.png
├── play.svg
├── tw067.mp3
├── volume.png
└── volume.svg
/.gitignore:
--------------------------------------------------------------------------------
1 | ################################################################
2 | #### for macOS
3 |
4 | # General
5 | .DS_Store
6 | .AppleDouble
7 | .LSOverride
8 |
9 | # Icon must end with two \r
10 | Icon
11 |
12 |
13 | # Thumbnails
14 | ._*
15 |
16 | # Files that might appear in the root of a volume
17 | .DocumentRevisions-V100
18 | .fseventsd
19 | .Spotlight-V100
20 | .TemporaryItems
21 | .Trashes
22 | .VolumeIcon.icns
23 | .com.apple.timemachine.donotpresent
24 |
25 | # Directories potentially created on remote AFP share
26 | .AppleDB
27 | .AppleDesktop
28 | Network Trash Folder
29 | Temporary Items
30 | .apdisk
31 |
32 |
33 | ################################################################
34 | #### for Windows
35 |
36 | # Windows thumbnail cache files
37 | Thumbs.db
38 | ehthumbs.db
39 | ehthumbs_vista.db
40 |
41 | # Dump file
42 | *.stackdump
43 |
44 | # Folder config file
45 | [Dd]esktop.ini
46 |
47 | # Recycle Bin used on file shares
48 | $RECYCLE.BIN/
49 |
50 | # Windows Installer files
51 | *.cab
52 | *.msi
53 | *.msix
54 | *.msm
55 | *.msp
56 |
57 | # Windows shortcuts
58 | *.lnk
59 |
60 |
61 | ################################################################
62 | #### for JetBrains
63 |
64 | .idea
65 |
--------------------------------------------------------------------------------
/asset/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redlily/training-webaudio-equalizer/1a1077d7f2037911194486d0162038aca02891a1/asset/screenshot.png
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # WebAudioで高速フーリエ変換
2 |
3 |
4 |
5 | ## 概要
6 |
7 | ScriptProcessorNodeと高速フーリエ変換(FFT)を使用してパラメトリック・イコライザを実装しています。
8 |
9 | さらに変換途中の周波数成分情報をWebGLに流し込んでオーディオ・ビジュアライザも実装しています。
10 |
11 | ## 依存
12 |
13 | 面倒なことをせずとも簡単に実行できるように他のライブラリに依存しないように作成してあります。
14 |
15 | ## 実行方法
16 |
17 | 適当なApacheやnginx等のミドルウェアでサーバを立ててsrcディレクトリにブラウザでアクセスするか、
18 | srcディレクトリに入っているindex.htmlをブラウザで開くだけです。
19 |
20 | ## 使用方法
21 |
22 | - 再生/停止
23 | 上部にある左から2番目のボタンを押すと音声データの再生と停止を行うことができます。
24 | あとプログラムを開始した直前は音声がならないので再生ボタンを押してください。
25 |
26 | - イコライザの表示/非表示
27 | 上部にある1番左のボタンを押すとイコライザの表示/非表示を切り替えることができます。
28 |
29 | - ビジュアライザの回転
30 | ビジュアライザをドラッグすることにより任意の方向に回転することができます。
31 |
32 | - 音声データの指定
33 | イコライザの下にある音声ファイル指定のボタンを押すか音声ファイルをビジュアライザの上にドラッグ・アンド・ドロップ
34 | してください。
35 |
36 | ## 音声データについて
37 |
38 | このサンプルプログラムで使用している音楽データはMusMus様よりお借りしています。
39 |
40 | http://musmus.main.jp/
41 |
42 | ## プログラムのライセンス
43 |
44 | MITライセンスです。ご自由にお使いください。
45 |
--------------------------------------------------------------------------------
/src/dft.js:
--------------------------------------------------------------------------------
1 | class DFT {
2 |
3 | static swap(v, a, b) {
4 | let ar = v[a + 0];
5 | let ai = v[a + 1];
6 | v[a + 0] = v[b + 0];
7 | v[a + 1] = v[b + 1];
8 | v[b + 0] = ar;
9 | v[b + 1] = ai;
10 | }
11 |
12 | static swapElements(n, v) {
13 | let n2 = n + 2;
14 | let nh = n >>> 1;
15 |
16 | for (let i = 0, j = 0; i < n; i += 4) {
17 | DFT.swap(v, i + n, j + 2);
18 | if (i < j) {
19 | DFT.swap(v, i + n2, j + n2);
20 | DFT.swap(v, i, j);
21 | }
22 |
23 | // ビットオーダを反転した変数としてインクリメント
24 | for (let k = nh; (j ^= k) < k; k >>= 1) {
25 | }
26 | }
27 | }
28 |
29 | static scaleElements(n, v, s, off = 0) {
30 | for (let i = 0; i < n; ++i) {
31 | v[off + i] /= s;
32 | }
33 | }
34 |
35 | /**
36 | * 離散フーリエ変換
37 | * @param n 変換するデータの要素数
38 | * @param a 入力用のデータ、実数、虚数の順で配置する必要がある
39 | * @param b 出力用のデータ、実数、虚数の順で配列される
40 | */
41 | static dft(n, a, b) {
42 | // b[k] = Σ[N - 1, j = 0] a[j] * e^(-2.0 * π * i * j * k / N)
43 | for (let k = 0; k < n; ++k) {
44 | // Σ[N - 1, j = 0] a[j] * e^(-2.0 * π * i * j * k / N)
45 | let sumRe = 0;
46 | let sumIm = 0;
47 | for (let j = 0; j < n; ++j) {
48 | // e^(-2.0 * π * i * j * k / N)
49 | let rad = -2.0 * Math.PI * k * j / n;
50 | let cs = Math.cos(rad), sn = Math.sin(rad);
51 | let re = a[(j << 1) + 0], im = a[(j << 1) + 1];
52 | // a[j] * e^(-2.0 * π * i * j * k / N)
53 | sumRe += re * cs - im * sn;
54 | sumIm += re * sn + im * cs;
55 | }
56 | b[(k << 1) + 0] = sumRe;
57 | b[(k << 1) + 1] = sumIm;
58 | }
59 | }
60 |
61 | /**
62 | * 逆離散フーリエ変換
63 | * @param n 変換するデータの要素数
64 | * @param a 入力用のデータ、実数、虚数の順で配置する必要がある
65 | * @param b 出力用のデータ、実数、虚数の順で配列される
66 | */
67 | static idft(n, a, b) {
68 | // b[j] = Σ[N - 1, k = 0] (1 / N) * a[k] * e^(2.0 * π * i * j * k / N)
69 | for (let j = 0; j < n; ++j) {
70 | // Σ[N - 1, k = 0] (1 / N) * a[k] * e^(2.0 * π * i * j * k / N)
71 | let sumRe = 0;
72 | let sumIm = 0;
73 | for (let k = 0; k < n; ++k) {
74 | // e^(2.0 * π * i * j * k / N)
75 | let rad = 2.0 * Math.PI * j * k / n;
76 | let cs = Math.cos(rad), sn = Math.sin(rad);
77 | let re = a[(k << 1) + 0], im = a[(k << 1) + 1];
78 | // a[k] * e^(2.0 * π * i * j * k / N)
79 | sumRe += re * cs - im * sn;
80 | sumIm += re * sn + im * cs;
81 | }
82 | b[(j << 1) + 0] = sumRe / n;
83 | b[(j << 1) + 1] = sumIm / n;
84 | }
85 | }
86 |
87 | /**
88 | * 高速フーリエ変換、単純な再帰呼び出しを使用した実装
89 | * @param n 変換するデータの要素数、実装の特性上2のべき乗を指定する必要がある
90 | * @param v 変換するデータ、実数、虚数の順で配置された複素数の配列
91 | * @param inv 逆変換を行う場合は true を設定する
92 | * @param off 変換を行う配列vの配列インデックス
93 | */
94 | static simpleFFT(n, v, inv = false, off = 0) {
95 | // 前処理
96 | let rad = (inv ? Math.PI : -Math.PI) / n;
97 | for (let j = 0; j < n; j += 2) {
98 | let a = off + j;
99 | let ar = v[a + 0], ai = v[a + 1];
100 | let b = off + n + j;
101 | let br = v[b + 0], bi = v[b + 1];
102 |
103 | // 偶数列 (a + b)
104 | v[a + 0] = ar + br;
105 | v[a + 1] = ai + bi;
106 |
107 | // 奇数列 (a - b) * w
108 | let xr = ar - br, xi = ai - bi;
109 | let r = rad * j;
110 | let wr = Math.cos(r), wi = Math.sin(r); // 回転因子 e^(-2 * π * i * j / N)
111 | v[b + 0] = xr * wr - xi * wi;
112 | v[b + 1] = xr * wi + xi * wr;
113 | }
114 |
115 | // 再帰的にDFTをかける
116 | let nd = n << 1;
117 | if (n > 2) {
118 | DFT.simpleFFT(n >>> 1, v, inv, off); // 偶数列
119 | DFT.simpleFFT(n >>> 1, v, inv, off + n); // 奇数列
120 |
121 | // 並べ替え
122 | for (let m = nd, mh = n, mq; 1 < (mq = mh >>> 1); m = mh, mh = mq) {
123 | for (let i = mq; i < nd - mh; i += m) {
124 | for (let j = i, k = i + mq; j < i + mq; j += 2, k += 2) {
125 | DFT.swap(v, off + j, off + k);
126 | }
127 | }
128 | }
129 | }
130 |
131 | // 逆変換用のスケール
132 | if (inv) {
133 | DFT.scaleElements(nd, v, 2, off);
134 | }
135 | }
136 |
137 | /**
138 | * 高速フーリエ変換
139 | * @param n 変換するデータの要素数、実装の特性上2のべき乗を指定する必要がある
140 | * @param v 変換するデータ、実数、虚数の順で配置された複素数の配列
141 | * @param inv 逆変換を行う場合は true を設定する
142 | */
143 | static fft(n, v, inv = false) {
144 | let rad = (inv ? 2.0 : -2.0) * Math.PI / n;
145 | let nd = n << 1;
146 |
147 | for (let m = nd, mh; 2 <= (mh = m >>> 1); m = mh) {
148 | for (let i = 0; i < mh; i += 2) {
149 | let rd = rad * (i >> 1);
150 | let cs = Math.cos(rd), sn = Math.sin(rd); // 回転因子
151 |
152 | for (let j = i; j < nd; j += m) {
153 | let k = j + mh;
154 | let ar = v[j + 0], ai = v[j + 1];
155 | let br = v[k + 0], bi = v[k + 1];
156 |
157 | // 前半 (a + b)
158 | v[j + 0] = ar + br;
159 | v[j + 1] = ai + bi;
160 |
161 | // 後半 (a - b) * w
162 | let xr = ar - br;
163 | let xi = ai - bi;
164 | v[k + 0] = xr * cs - xi * sn;
165 | v[k + 1] = xr * sn + xi * cs;
166 | }
167 | }
168 | rad *= 2;
169 | }
170 |
171 | // 要素の入れ替え
172 | DFT.swapElements(n, v);
173 |
174 | // 逆変換用のスケール
175 | if (inv) {
176 | DFT.scaleElements(nd, v, n);
177 | }
178 | }
179 |
180 | /**
181 | * 高速フーリエ変換、精度を多少犠牲にして速度を向上させたタイプ。
182 | * @param n 変換するデータの要素数、実装の特性上2のべき乗を指定する必要がある。
183 | * @param v 変換するデータ、実数、虚数の順で配置された複素数の配列。
184 | * @param inv 逆変換を行う場合は true を設定する。
185 | */
186 | static fftHighSpeed(n, v, inv = false) {
187 | let rad = (inv ? 2.0 : -2.0) * Math.PI / n;
188 | let cs = Math.cos(rad), sn = Math.sin(rad); // 回転因子の回転用複素数
189 | let nd = n << 1;
190 |
191 | for (let m = nd, mh; 2 <= (mh = m >>> 1); m = mh) {
192 | // 回転因子が0°の箇所を処理
193 | for (let i = 0; i < nd; i += m) {
194 | let j = i + mh;
195 | let ar = v[i + 0], ai = v[i + 1];
196 | let br = v[j + 0], bi = v[j + 1];
197 |
198 | // 前半 (a + b)
199 | v[i + 0] = ar + br;
200 | v[i + 1] = ai + bi;
201 |
202 | // 後半 (a - b)
203 | v[j + 0] = ar - br;
204 | v[j + 1] = ai - bi;
205 | }
206 |
207 | // 回転因子が0°以外の箇所を処理
208 | let wcs = cs, wsn = sn; // 回転因子
209 | for (let i = 2; i < mh; i += 2) {
210 | for (let j = i; j < nd; j += m) {
211 | let k = j + mh;
212 | let ar = v[j + 0], ai = v[j + 1];
213 | let br = v[k + 0], bi = v[k + 1];
214 |
215 | // 前半 (a + b)
216 | v[j + 0] = ar + br;
217 | v[j + 1] = ai + bi;
218 |
219 | // 後半 (a - b) * w
220 | let xr = ar - br;
221 | let xi = ai - bi;
222 | v[k + 0] = xr * wcs - xi * wsn;
223 | v[k + 1] = xr * wsn + xi * wcs;
224 | }
225 |
226 | // 回転因子を回転
227 | let tcs = wcs * cs - wsn * sn;
228 | wsn = wcs * sn + wsn * cs;
229 | wcs = tcs;
230 | }
231 |
232 | // 回転因子の回転用の複素数を自乗して回転
233 | let tcs = cs * cs - sn * sn;
234 | sn = 2.0 * (cs * sn);
235 | cs = tcs;
236 | }
237 |
238 | // 要素の入れ替え
239 | DFT.swapElements(n, v);
240 |
241 | // 逆変換用のスケール
242 | if (inv) {
243 | DFT.scaleElements(nd, v, n);
244 | }
245 | }
246 | }
247 |
248 | function testDFT() {
249 | let a = [10, 2, 4, 3, 1, -1, -4, 2, 3, -9, 20, 12, -30, 15, -13, -20];
250 | let b = new Array(16);
251 |
252 | DFT.dft(8, a, b);
253 | console.log(b);
254 |
255 | DFT.simpleFFT(8, a);
256 | console.log(a);
257 |
258 | a = [10, 2, 4, 3, 1, -1, -4, 2, 3, -9, 20, 12, -30, 15, -13, -20];
259 | DFT.fft(8, a);
260 | console.log(a);
261 |
262 | a = [10, 2, 4, 3, 1, -1, -4, 2, 3, -9, 20, 12, -30, 15, -13, -20];
263 | DFT.fftHighSpeed(8, a);
264 | console.log(a);
265 | }
266 |
267 | function testPerformance() {
268 | let a = new Float64Array(2048);
269 | let b = new Float64Array(2048);
270 | for (let i = 0; i < a.length; ++i) {
271 | a[i] = Math.random();
272 | }
273 |
274 | let begin = new Date();
275 | for (let i = 0; i < 10000; ++i) {
276 | DFT.simpleFFT(1024, a);
277 | }
278 | console.log(`simpleFFT ${new Date().getTime() - begin.getTime()}`);
279 |
280 | begin = new Date();
281 | for (let i = 0; i < 10000; ++i) {
282 | DFT.fft(1024, a);
283 | }
284 | console.log(`fft ${new Date().getTime() - begin.getTime()}`);
285 |
286 | begin = new Date();
287 | for (let i = 0; i < 10000; ++i) {
288 | DFT.fftHighSpeed(1024, a);
289 | }
290 | console.log(`fftHighSpeed ${new Date().getTime() - begin.getTime()}`);
291 |
292 | begin = new Date();
293 | for (let i = 0; i < 1000; ++i) {
294 | DFT.dft(1024, a, b);
295 | }
296 | console.log(`dft ${new Date().getTime() - begin.getTime()}`);
297 | }
298 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
175 | デフォルトBGM : MusMus
176 |
177 | ドラッグ&ドロップで任意の曲を再生可能
178 |