├── .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 | 3 | Parametric Equalizer 4 | 5 | 6 | 7 | 8 | 23 | 35 | 36 | 37 | 38 |
39 |
40 | 41 |
42 | 43 |
44 |
45 |
46 | 47 |
48 |
49 | 50 |
51 |
52 | 53 |
54 |
55 | 56 | 173 | 174 |

175 | デフォルトBGM : MusMus 176 |
177 | ドラッグ&ドロップで任意の曲を再生可能 178 |

179 |
180 |
181 | 182 | 183 | -------------------------------------------------------------------------------- /src/main.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | margin: 0; 4 | overflow: hidden; 5 | } 6 | 7 | hr { 8 | height: 0; 9 | margin: 0; 10 | padding: 0; 11 | border: 0; 12 | } 13 | 14 | ::-webkit-scrollbar { 15 | width: 0; 16 | height: 0; 17 | } 18 | 19 | div.screen { 20 | position: relative; 21 | width: 100vw; 22 | height: 100vh; 23 | } 24 | 25 | div.background { 26 | position: absolute; 27 | width: 100%; 28 | height: 100%; 29 | background-color: rgba(16, 16, 16, 1.0); 30 | } 31 | 32 | canvas.mainCanvas { 33 | width: 100%; 34 | height: 100%; 35 | } 36 | 37 | div.foreground { 38 | position: absolute; 39 | width: 100%; 40 | height: 100%; 41 | pointer-events: none; 42 | } 43 | 44 | div.header { 45 | display: table; 46 | position: relative; 47 | width: calc(100vw - 30px); 48 | padding: 0 15px 0 15px; 49 | white-space: nowrap; 50 | line-height: 0; 51 | background-color: rgba(64, 64, 64, 0.5); 52 | pointer-events: auto; 53 | } 54 | 55 | div.header div { 56 | display: table-cell; 57 | height: 50px; 58 | margin: 0 15px 0 15px; 59 | text-align: center; 60 | vertical-align: middle; 61 | } 62 | 63 | div.header input[type=image] { 64 | width: 32px; 65 | height: 32px; 66 | margin: 0 20px 0 20px; 67 | outline: none; 68 | } 69 | 70 | div.header input[type=range] { 71 | width: 90%; 72 | height: 32px; 73 | margin: 0 15px 0 15px; 74 | } 75 | 76 | div.navigation { 77 | position: absolute; 78 | top: 50px; 79 | bottom: 0; 80 | background-color: rgba(64, 64, 64, 0.3); 81 | overflow-x: scroll; 82 | pointer-events: auto; 83 | } 84 | 85 | div.navigation div { 86 | padding: 15px 2px 15px 2px; 87 | } 88 | 89 | hr.separator { 90 | margin-top: 5px; 91 | margin-bottom: 5px; 92 | border-top: 1px double rgb(92, 92, 92); 93 | } 94 | 95 | td.rangeTitle { 96 | padding: 2px 10px 2px 10px; 97 | text-align: center; 98 | font-size: small; 99 | color: rgb(192, 192, 192); 100 | } 101 | 102 | td.rangeSlider { 103 | padding: 2px 10px 2px 10px; 104 | } 105 | 106 | td.selectTitle { 107 | padding: 2px 10px 2px 10px; 108 | text-align: center; 109 | font-size: small; 110 | color: rgb(192, 192, 192); 111 | } 112 | 113 | td.selectBox { 114 | padding: 2px 10px 2px 10px; 115 | text-align: center; 116 | } 117 | 118 | td.fileSelector { 119 | padding: 10px 10px 10px 10px; 120 | text-align: center; 121 | } 122 | 123 | p.musicName { 124 | position: absolute; 125 | right: 0; 126 | bottom: 0; 127 | padding: 10px 10px 10px 10px; 128 | text-align: right; 129 | color: rgb(192, 192, 192); 130 | pointer-events: auto; 131 | } 132 | 133 | p.musicName a:link { 134 | color: rgb(192, 192, 192); 135 | } 136 | 137 | p.musicName a:visited { 138 | color: rgb(192, 192, 192); 139 | } 140 | 141 | p.musicName a:hover { 142 | color: rgb(192, 192, 192); 143 | } 144 | 145 | p.musicName a:active { 146 | color: rgb(192, 192, 192); 147 | } 148 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // 3D系のユーティリティ 2 | let tutils = {}; 3 | (function () { 4 | "use strict"; 5 | 6 | //////////////////////////////////////////////////////////////// 7 | // WebGL関連 8 | 9 | // シェーダを生成 10 | function createShader(gl, type, code) { 11 | // シェーダの作成 12 | let shader = gl.createShader(type); 13 | gl.shaderSource(shader, code); 14 | 15 | // コンパイル 16 | gl.compileShader(shader); 17 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 18 | alert(gl.getShaderInfoLog(shader)); 19 | gl.deleteShader(shader); 20 | return null; 21 | } 22 | 23 | return shader; 24 | } 25 | 26 | // シェーダプログラムを生成 27 | function createProgram(gl, vs, fs, attr) { 28 | // プログラムの作成 29 | let program = gl.createProgram(); 30 | gl.attachShader(program, vs); 31 | gl.attachShader(program, fs); 32 | Object.keys(attr).forEach((key) => { 33 | gl.bindAttribLocation(program, attr[key], key); 34 | }); 35 | 36 | // リンケージ 37 | gl.linkProgram(program); 38 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 39 | alert(gl.getProgramInfoLog(program)); 40 | gl.deleteProgram(program); 41 | return null; 42 | } 43 | 44 | return program; 45 | } 46 | 47 | // シェーダプログラムを生成 48 | tutils.createShaderProgram = function (gl, vsCode, fsCode, attributes, uniforms) { 49 | // 頂点シェーダのコンパイル 50 | let vs = createShader(gl, gl.VERTEX_SHADER, vsCode); 51 | if (vs == null) { 52 | return null; 53 | } 54 | 55 | // フラグメントシェーダのコンパイル 56 | let fs = createShader(gl, gl.FRAGMENT_SHADER, fsCode); 57 | if (fs == null) { 58 | gl.deleteShader(vs); 59 | return null; 60 | } 61 | 62 | // プログラムのリンク 63 | let pg = createProgram(gl, vs, fs, attributes); 64 | gl.deleteShader(vs); 65 | gl.deleteShader(fs); 66 | if (uniforms != null) { 67 | for (let name in uniforms) { 68 | uniforms[name] = gl.getUniformLocation(pg, name); 69 | } 70 | } 71 | return pg; 72 | }; 73 | 74 | // バッファを作成 75 | tutils.createBuffer = function (gl, type, data, usage) { 76 | let buf = gl.createBuffer(); 77 | if (buf != null) { 78 | gl.bindBuffer(type, buf); 79 | gl.bufferData(type, data, usage); 80 | } 81 | return buf; 82 | }; 83 | 84 | //////////////////////////////////////////////////////////////// 85 | // 幾何学関連 86 | 87 | const VX = 0, VY = 1, VZ = 2; 88 | 89 | const M00 = 0, M01 = 4, M02 = 8, M03 = 12; 90 | const M10 = 1, M11 = 5, M12 = 9, M13 = 13; 91 | const M20 = 2, M21 = 6, M22 = 10, M23 = 14; 92 | const M30 = 3, M31 = 7, M32 = 11, M33 = 15; 93 | 94 | // 行列の設定 95 | tutils.setMatrix = function (m, 96 | m00, m01, m02, m03, 97 | m10, m11, m12, m13, 98 | m20, m21, m22, m23, 99 | m30, m31, m32, m33) { 100 | m[M00] = m00; 101 | m[M01] = m01; 102 | m[M02] = m02; 103 | m[M03] = m03; 104 | m[M10] = m10; 105 | m[M11] = m11; 106 | m[M12] = m12; 107 | m[M13] = m13; 108 | m[M20] = m20; 109 | m[M21] = m21; 110 | m[M22] = m22; 111 | m[M23] = m23; 112 | m[M30] = m30; 113 | m[M31] = m31; 114 | m[M32] = m32; 115 | m[M33] = m33; 116 | return m; 117 | }; 118 | 119 | // 単位行列の設定 120 | tutils.setIdentityMatrix = function (m) { 121 | return tutils.setMatrix( 122 | m, 123 | 1, 0, 0, 0, 124 | 0, 1, 0, 0, 125 | 0, 0, 1, 0, 126 | 0, 0, 0, 1); 127 | }; 128 | 129 | // 拡大行列を掛ける 130 | tutils.mulScaleMatrix = function (m, x, y, z) { 131 | m[M00] *= x; 132 | m[M01] *= y; 133 | m[M02] *= z; 134 | m[M10] *= x; 135 | m[M11] *= y; 136 | m[M12] *= z; 137 | m[M20] *= x; 138 | m[M21] *= y; 139 | m[M22] *= z; 140 | m[M30] *= x; 141 | m[M31] *= y; 142 | m[M32] *= z; 143 | 144 | return m; 145 | }; 146 | 147 | // 回転行列を掛ける 148 | tutils.mulRotationMatrix = function (m, x, y, z, rad) { 149 | let cs = Math.cos(rad); 150 | let sn = Math.sin(rad); 151 | let len = x * x + y * y + z * z; 152 | if (0 < len) { 153 | len = Math.sqrt(len); 154 | x /= len; 155 | y /= len; 156 | z /= len; 157 | } 158 | 159 | // 共通項を算出 160 | let cs1 = 1.0 - cs; 161 | let xcs1 = x * cs1, ycs1 = y * cs1; 162 | let xycs1 = y * xcs1, xzcs1 = z * xcs1, yzcs1 = z * ycs1; 163 | let xsn = x * sn, ysn = y * sn, zsn = z * sn; 164 | 165 | // 掛けあわせて、結果の書き出し 166 | let a00 = m[M00], a01 = m[M01], a02 = m[M02]; 167 | let a10 = m[M10], a11 = m[M11], a12 = m[M12]; 168 | let a20 = m[M20], a21 = m[M21], a22 = m[M22]; 169 | let a30 = m[M30], a31 = m[M31], a32 = m[M32]; 170 | let b00 = cs + x * xcs1, b01 = xycs1 - zsn, b02 = xzcs1 + ysn; 171 | let b10 = xycs1 + zsn, b11 = cs + y * ycs1, b12 = yzcs1 - xsn; 172 | let b20 = xzcs1 - ysn, b21 = yzcs1 + xsn, b22 = cs + z * z * cs1; 173 | 174 | m[M00] = a00 * b00 + a01 * b10 + a02 * b20; 175 | m[M01] = a00 * b01 + a01 * b11 + a02 * b21; 176 | m[M02] = a00 * b02 + a01 * b12 + a02 * b22; 177 | m[M10] = a10 * b00 + a11 * b10 + a12 * b20; 178 | m[M11] = a10 * b01 + a11 * b11 + a12 * b21; 179 | m[M12] = a10 * b02 + a11 * b12 + a12 * b22; 180 | m[M20] = a20 * b00 + a21 * b10 + a22 * b20; 181 | m[M21] = a20 * b01 + a21 * b11 + a22 * b21; 182 | m[M22] = a20 * b02 + a21 * b12 + a22 * b22; 183 | m[M30] = a30 * b00 + a31 * b10 + a32 * b20; 184 | m[M31] = a30 * b01 + a31 * b11 + a32 * b21; 185 | m[M32] = a30 * b02 + a31 * b12 + a32 * b22; 186 | 187 | return m; 188 | }; 189 | 190 | // 平行移動行列を掛ける 191 | tutils.mulTranslateMatrix = function (m, x, y, z) { 192 | m[M03] += m[M00] * x + m[M01] * y + m[M02] * z; 193 | m[M13] += m[M10] * x + m[M11] * y + m[M12] * z; 194 | m[M23] += m[M20] * x + m[M21] * y + m[M22] * z; 195 | m[M33] += m[M30] * x + m[M31] * y + m[M32] * z; 196 | return m; 197 | }; 198 | 199 | // ビュー行列を掛ける 200 | tutils.mulViewMatrix = function (m, 201 | eyeX, eyeY, eyeZ, 202 | centerX, centerY, centerZ, 203 | upperX, upperY, upperZ) { 204 | // Z軸のベクトルを算出 205 | // (center - eye) / |center - eye| 206 | let zx = centerX - eyeX; 207 | let zy = centerY - eyeY; 208 | let zz = centerZ - eyeZ; 209 | let zLen = zx * zx + zy * zy + zz * zz; 210 | if (0 < zLen) { 211 | zLen = Math.sqrt(zLen); 212 | zx /= zLen; 213 | zy /= zLen; 214 | zz /= zLen; 215 | } 216 | 217 | // X軸のベクトルを算出 218 | // (z_axis × upper) / |z_axis × upper| 219 | let xx = zy * upperZ - zz * upperY; 220 | let xy = zz * upperX - zx * upperZ; 221 | let xz = zx * upperY - zy * upperX; 222 | let xLen = xx * xx + xy * xy + xz * xz; 223 | if (0 < xLen) { 224 | xLen = Math.sqrt(xLen); 225 | xx /= xLen; 226 | xy /= xLen; 227 | xz /= xLen; 228 | } 229 | 230 | // Y軸のベクトルを算出 231 | // z_axis × x_axis 232 | let yx = zy * xz - zz * xy; 233 | let yy = zz * xx - zx * xz; 234 | let yz = zx * xy - zy * xx; 235 | 236 | // 平行移動に回転を掛ける 237 | // | x.x, x.y, x.z | | eye_x | 238 | // | y.x, y.y, y.z | * | eye_y | * - 1 239 | // | z.x, z.y, z.z | | eye_z | 240 | let tx = -(xx * eyeX + xy * eyeY + xz * eyeZ); 241 | let ty = -(yx * eyeX + yy * eyeY + yz * eyeZ); 242 | let tz = -(zx * eyeX + zy * eyeY + zz * eyeZ); 243 | 244 | // 掛けあわせて、結果の書き出し 245 | let a00 = m[M00], a01 = m[M01], a02 = m[M02]; 246 | let a10 = m[M10], a11 = m[M11], a12 = m[M12]; 247 | let a20 = m[M20], a21 = m[M21], a22 = m[M22]; 248 | let a30 = m[M30], a31 = m[M31], a32 = m[M32]; 249 | 250 | m[M00] = a00 * xx + a01 * yx + a02 * zx; 251 | m[M01] = a00 * xy + a01 * yy + a02 * zy; 252 | m[M02] = a00 * xz + a01 * yz + a02 * zz; 253 | m[M03] += a00 * tx + a01 * ty + a02 * tz; 254 | m[M10] = a10 * xx + a11 * yx + a12 * zx; 255 | m[M11] = a10 * xy + a11 * yy + a12 * zy; 256 | m[M12] = a10 * xz + a11 * yz + a12 * zz; 257 | m[M13] += a10 * tx + a11 * ty + a12 * tz; 258 | m[M20] = a20 * xx + a21 * yx + a22 * zx; 259 | m[M21] = a20 * xy + a21 * yy + a22 * zy; 260 | m[M22] = a20 * xz + a21 * yz + a22 * zz; 261 | m[M23] += a20 * tx + a21 * ty + a22 * tz; 262 | m[M30] = a30 * xx + a31 * yx + a32 * zx; 263 | m[M31] = a30 * xy + a31 * yy + a32 * zy; 264 | m[M32] = a30 * xz + a31 * yz + a32 * zz; 265 | m[M33] += a30 * tx + a31 * ty + a32 * tz; 266 | 267 | return m; 268 | }; 269 | 270 | // プロジェクション行列を掛ける 271 | tutils.mulProjectionMatrix = function (m, viewWidth, viewHeight, viewNear, viewFar) { 272 | // 共通項を算出 273 | let rangeView = viewFar - viewNear; 274 | let scaledFar = viewFar * (2.0); 275 | 276 | // 行列に掛けあわせて、結果の書き出し 277 | let a02 = m[M02]; 278 | let a12 = m[M12]; 279 | let a22 = m[M22]; 280 | let a32 = m[M32]; 281 | let b00 = viewNear * 2.0 / viewWidth; 282 | let b11 = viewNear * 2.0 / viewHeight; 283 | let b22 = scaledFar / rangeView - 1.0; 284 | let b23 = viewNear * scaledFar / -rangeView; 285 | 286 | m[M00] *= b00; 287 | m[M01] *= b11; 288 | m[M02] = a02 * b22 + m[M03]; 289 | m[M03] = a02 * b23; 290 | m[M10] *= b00; 291 | m[M11] *= b11; 292 | m[M12] = a12 * b22 + m[M13]; 293 | m[M13] = a12 * b23; 294 | m[M20] *= b00; 295 | m[M21] *= b11; 296 | m[M22] = a22 * b22 + m[M23]; 297 | m[M23] = a22 * b23; 298 | m[M30] *= b00; 299 | m[M31] *= b11; 300 | m[M32] = a32 * b22 + m[M33]; 301 | m[M33] = a32 * b23; 302 | 303 | return m; 304 | }; 305 | 306 | })(); 307 | 308 | // メインの処理 309 | (function () { 310 | "use strict"; 311 | 312 | // 定数 313 | const NUM_FREQUENCY_BUNDLES = 10; 314 | const FREQUENCIES_PRESETS = [ 315 | [100, 100, 100, 100, 100, 100, 100, 100, 100, 100], // デフォルト 316 | [118, 131, 125, 85, 46, 82, 108, 127, 136, 141], // ロック 317 | [87, 80, 106, 132, 138, 117, 92, 87, 87, 96], // ポップ 318 | [110, 134, 117, 76, 100, 127, 131, 122, 108, 82], // ダンス 319 | [117, 106, 76, 104, 73, 76, 96, 115, 125, 124], // ジャズ 320 | [0, 0, 0, 3, 13, 96, 129, 146, 152, 139], // 古いラジオ 321 | [120, 139, 127, 76, 8, 4, 0, 0, 0, 0], // 水中 322 | [150, 150, 150, 1, 1, 1, 1, 1, 1, 1], // 低音 323 | [1, 1, 1, 150, 150, 150, 1, 1, 1, 1], // 中音 324 | [1, 1, 1, 1, 1, 1, 150, 150, 150, 150], // 高音 325 | ]; 326 | const NUM_SAMPLES = 1 << NUM_FREQUENCY_BUNDLES; // 2^10 = 1024 327 | const NUM_VISUALIZE_BINDS = 22; 328 | const NUM_VISUALIZE_HISTORIES = 20; 329 | 330 | //////////////////////////////////////////////////////////////// 331 | // サウンド関連 332 | 333 | let audioContext; 334 | let audioElement; 335 | let audioSource; 336 | let scriptProcessor; 337 | 338 | let audioPrevInputs; 339 | let audioPrevOutputs; 340 | let audioWorkBuffer; 341 | 342 | let audioUrl = "tw067.mp3"; 343 | 344 | // イニシャライズされているか否か 345 | function isInitializedAudio() { 346 | return audioContext != null; 347 | } 348 | 349 | // オーディオ関連の初期化 350 | function initializeAudio() { 351 | // 処理用のバッファを初期化 352 | audioPrevInputs = [ 353 | new Float32Array(NUM_SAMPLES), 354 | new Float32Array(NUM_SAMPLES) 355 | ]; 356 | audioPrevOutputs = [ 357 | new Float32Array(NUM_SAMPLES), 358 | new Float32Array(NUM_SAMPLES) 359 | ]; 360 | audioWorkBuffer = new Float32Array(NUM_SAMPLES * 4); // FFT処理用のバッファ、処理サンプルの倍の複素数を収納できるようにする 361 | 362 | // WebAudio系の初期化 363 | audioContext = window.AudioContext != null ? 364 | new window.AudioContext() : 365 | new window.webkitAudioContext(); 366 | 367 | scriptProcessor = audioContext.createScriptProcessor(NUM_SAMPLES, 2, 2); 368 | scriptProcessor.addEventListener("audioprocess", onAudioProcess); 369 | scriptProcessor.connect(audioContext.destination); 370 | 371 | audioElement = new Audio(); 372 | audioElement.loop = true; 373 | audioElement.autoplay = true; 374 | audioElement.addEventListener("timeupdate", onUpdatedAudioTime); 375 | 376 | audioSource = audioContext.createMediaElementSource(audioElement); 377 | audioSource.connect(scriptProcessor); 378 | } 379 | 380 | // オーディオ関連の後処理 381 | function terminateAudio() { 382 | audioElement.stop(); 383 | } 384 | 385 | // 音声ファイルを読み込む 386 | function loadAudio(url) { 387 | if (isInitializedAudio()) { 388 | audioElement.src = url; 389 | } else { 390 | audioUrl = url; 391 | } 392 | } 393 | 394 | // 音声ファイルの再生時間が更新 395 | function onUpdatedAudioTime(event) { 396 | timeSeek.value = 1000 * (audioElement.currentTime / audioElement.duration); 397 | } 398 | 399 | // 音声ファイルが再生中か否か 400 | function isPlayAudio() { 401 | return !audioElement.paused 402 | } 403 | 404 | // 音声ファイルを再生する 405 | function playAudio() { 406 | if (!isPlayAudio()) { 407 | audioElement.play(); 408 | } 409 | } 410 | 411 | // 音声ファイルを停止する 412 | function stopAudio() { 413 | audioElement.pause(); 414 | } 415 | 416 | // 音声の波形を処理 417 | function onAudioProcess(event) { 418 | let input = event.inputBuffer; 419 | let output = event.outputBuffer; 420 | for (let i = 0; i < output.numberOfChannels; ++i) { 421 | let inputData = input.getChannelData(i); 422 | let outputData = output.getChannelData(i); 423 | let prevInput = audioPrevInputs[i]; 424 | let prevOutput = audioPrevOutputs[i]; 425 | 426 | // 前半に前回の入力波形、後半に今回の入力波形を実数を複素数に変換して作業用バッファに詰める 427 | for (let j = 0; j < NUM_SAMPLES; ++j) { 428 | // 前半 429 | let prevIndex = j * 2; 430 | audioWorkBuffer[prevIndex] = prevInput[j]; 431 | audioWorkBuffer[prevIndex + 1] = 0.0; 432 | 433 | // 後半 434 | let nextIndex = (NUM_SAMPLES + j) * 2; 435 | audioWorkBuffer[nextIndex] = inputData[j]; 436 | audioWorkBuffer[nextIndex + 1] = 0.0; 437 | 438 | // 今回の波形を保存 439 | prevInput[j] = inputData[j]; 440 | } 441 | 442 | // FFTをかけて周波数 443 | DFT.fftHighSpeed(NUM_SAMPLES * 2, audioWorkBuffer); 444 | 445 | /* 446 | 離散フーリエ変換の特性に対する概要 447 | 448 | 離散フーリエ変換の特性として、例えば8個の要素に処理を行うと下記のような並びで周波数成分が 449 | 複素数(2次元ベクトル)の配列として並ぶ。 450 | 451 | 0: 0Hz, 1: 1Hz, 2: 2Hz 3: 4Hz, 4: 8Hz, 5: -4Hz, 6: -2Hz, 7: -1Hz 452 | 453 | そして、実数データのみで構成された配列に対して離散フーリエ変換をかけた場合、中心の要素、上記で言えば8Hzを 454 | 中心に実数部は左右対称(偶関数的)、虚数は符号が反転して左右対称(奇関数的)になる。 455 | 456 | 例: 457 | 458 | 実数部:[ 10, 20, -2, 4, 40, 4, -2, 20 ] 459 | 虚数部:[ 0, -4, -12, 7, 0, -7, 12, 4 ] 460 | 461 | そして、この複素数の絶対値 ( √(Re^2 + Im^2) ) が各周波数成分のボリュームとなる。 462 | */ 463 | 464 | /* 465 | 周波数バンドル 466 | 467 | 30 Hz 2^0, 2^1 - 1 468 | 60 Hz 2^1, 2^2 - 1 469 | 120 Hz 2^2, 2^3 - 1 470 | 240 Hz 2^3, 2^4 - 1 471 | 500 Hz 2^4, 2^5 - 1 472 | 1k Hz 2^5, 2^6 - 1 473 | 2k Hz 2^6, 2^7 - 1 474 | 4k Hz 2^7, 2^8 - 1 475 | 8k Hz 2^8, 2^9 - 1 476 | 16k Hz 2^9, 2^10 - 1 477 | */ 478 | 479 | // 各周波数のボリュームを設定 480 | for (let j = 0; j < frequencySliders.length; ++j) { 481 | let volume = frequencySliders[j].value / 100.0; 482 | 483 | for (let k = 1 << j, kEnd = 1 << (j + 1); k < kEnd; ++k) { 484 | let positiveFq = k * 2; 485 | audioWorkBuffer[positiveFq] *= volume; 486 | audioWorkBuffer[positiveFq + 1] *= volume; 487 | 488 | let negativeFq = (NUM_SAMPLES * 2 - k) * 2; 489 | audioWorkBuffer[negativeFq] *= volume; 490 | audioWorkBuffer[negativeFq + 1] *= volume; 491 | } 492 | } 493 | 494 | // 直流部分のボリュームを設定 495 | let minFqVolume = frequencySliders[0].value / 100.0; 496 | audioWorkBuffer[0] *= minFqVolume; 497 | audioWorkBuffer[1] *= minFqVolume; 498 | 499 | // 最高周波数のボリュームを設定 500 | let maxFqVolume = frequencySliders[frequencySliders.length - 1].value / 100.0; 501 | audioWorkBuffer[NUM_SAMPLES * 2] *= maxFqVolume; 502 | audioWorkBuffer[NUM_SAMPLES * 2 + 1] *= maxFqVolume; 503 | 504 | // ビジュアライザの更新 505 | updateFrequencyVisualizerParam(i, audioWorkBuffer); 506 | 507 | // 逆FFTをかける 508 | DFT.fftHighSpeed(NUM_SAMPLES * 2, audioWorkBuffer, true); 509 | 510 | // 前回の出力波形の後半と今回の出力波形の前半をクロスフェードさせて出力する 511 | let master = masterSlider.value / 100.0; 512 | for (let j = 0; j < NUM_SAMPLES; ++j) { 513 | let prev = prevOutput[j] * (NUM_SAMPLES - j) / NUM_SAMPLES; 514 | let next = audioWorkBuffer[j * 2] * j / NUM_SAMPLES; 515 | outputData[j] = (prev + next) * master; 516 | prevOutput[j] = audioWorkBuffer[(NUM_SAMPLES + j) * 2]; 517 | } 518 | } 519 | } 520 | 521 | //////////////////////////////////////////////////////////////// 522 | // UI関連 523 | 524 | let mainCanvas; 525 | let fileSelector; 526 | let volumeButton; 527 | let playButton; 528 | let timeSeek; 529 | let navigation; 530 | let masterSlider; 531 | let frequencySliders; 532 | let frequencyPreset; 533 | let musicName; 534 | 535 | let touchX; 536 | let touchY; 537 | let touchId; 538 | let isDownMouse; 539 | 540 | // 初期化 541 | function onLoad(event) { 542 | console.log("onLoad"); 543 | 544 | // UIの初期化 545 | mainCanvas = document.getElementById("mainCanvas"); 546 | mainCanvas.addEventListener("mousedown", onMouseMotion); 547 | mainCanvas.addEventListener("mouseup", onMouseMotion); 548 | mainCanvas.addEventListener("mousemove", onMouseMotion); 549 | mainCanvas.addEventListener("mouseout", onMouseMotion); 550 | mainCanvas.addEventListener("touchstart", onTouchMotion); 551 | mainCanvas.addEventListener("touchend", onTouchMotion); 552 | mainCanvas.addEventListener("touchmove", onTouchMotion); 553 | mainCanvas.addEventListener("touchcancel", onTouchMotion); 554 | fileSelector = document.getElementById("fileSelector"); 555 | fileSelector.addEventListener("change", onSelectedFile); 556 | volumeButton = document.getElementById("volume"); 557 | volumeButton.addEventListener("click", onClickVolumeButton); 558 | playButton = document.getElementById("play"); 559 | playButton.addEventListener("click", onClickPlayButton); 560 | timeSeek = document.getElementById("time"); 561 | navigation = document.getElementById("navigation"); 562 | masterSlider = document.getElementById("master"); 563 | masterSlider.addEventListener("change", onChangedMasterVolume); 564 | frequencySliders = new Array(NUM_FREQUENCY_BUNDLES); 565 | for (let i = 0; i < frequencySliders.length; ++i) { 566 | frequencySliders[i] = document.getElementById("frequency" + i); 567 | frequencySliders[i].addEventListener("change", onChangedFrequencyVolume); 568 | } 569 | frequencyPreset = document.getElementById("preset"); 570 | frequencyPreset.addEventListener("change", onChangedPreset); 571 | musicName = document.getElementById("musicName"); 572 | 573 | // その他の初期化 574 | onResize(); 575 | initializeGL(); 576 | onAnimationGL(); 577 | } 578 | 579 | // 後処理 580 | function onUnload(event) { 581 | console.log("onUnload"); 582 | terminateGL(); 583 | terminateAudio(); 584 | } 585 | 586 | // 画面のサイズ変更 587 | function onResize(event) { 588 | console.log("onResize"); 589 | let width = window.innerWidth; 590 | let height = window.innerHeight; 591 | let devicePixelRatio = window.devicePixelRatio || 1; 592 | mainCanvas.style.width = width + "px"; 593 | mainCanvas.style.height = height + "px"; 594 | mainCanvas.width = width;// * devicePixelRatio; 595 | mainCanvas.height = height;// * devicePixelRatio; 596 | } 597 | 598 | // ファイルをドラッグ 599 | function onDragOver(event) { 600 | event.stopPropagation(); 601 | event.preventDefault(); 602 | } 603 | 604 | // ファイルをドロップ 605 | function onDrop(event) { 606 | event.stopPropagation(); 607 | event.preventDefault(); 608 | loadAudioFile(event.dataTransfer.files[0]); 609 | } 610 | 611 | // ファイルを選択 612 | function onSelectedFile(event) { 613 | loadAudioFile(event.target.files[0]); 614 | } 615 | 616 | // 音声ファイルの読み込み 617 | function loadAudioFile(file) { 618 | musicName.innerText = "Playing music is " + file.name + "."; 619 | loadAudio(URL.createObjectURL(file)); 620 | } 621 | 622 | // ボリュームボタンを押下 623 | function onClickVolumeButton() { 624 | if (navigation.style.display == "none") { 625 | navigation.style.display = "block"; 626 | } else { 627 | navigation.style.display = "none"; 628 | } 629 | } 630 | 631 | // 再生ボタンを押下 632 | function onClickPlayButton(event) { 633 | if (!isInitializedAudio()) { 634 | initializeAudio(); 635 | loadAudio(audioUrl); 636 | } 637 | if (!isPlayAudio()) { 638 | playAudio(); 639 | } else { 640 | stopAudio(); 641 | } 642 | } 643 | 644 | // マスターボリュームを変更 645 | function onChangedMasterVolume(event) { 646 | } 647 | 648 | // 各周波数もリュームを変更 649 | function onChangedFrequencyVolume(event) { 650 | frequencyPreset.value = -1; 651 | } 652 | 653 | // プリセットを変更 654 | function onChangedPreset(event) { 655 | let preset = FREQUENCIES_PRESETS[event.target.value]; 656 | for (let i = 0; i < frequencySliders.length; ++i) { 657 | frequencySliders[i].value = preset[i]; 658 | } 659 | } 660 | 661 | // マウスイベント 662 | function onMouseMotion(event) { 663 | touchId = null; 664 | 665 | switch (event.type) { 666 | case "mousedown": 667 | touchX = event.clientX; 668 | touchY = event.clientY; 669 | isDownMouse = true; 670 | break; 671 | 672 | case "mousemove": 673 | if (isDownMouse) { 674 | onMotionGL(event.clientX - touchX, event.clientY - touchY); 675 | touchX = event.clientX; 676 | touchY = event.clientY; 677 | } 678 | break; 679 | 680 | case "mouseup": 681 | case "mouseout": 682 | isDownMouse = false; 683 | break; 684 | } 685 | } 686 | 687 | // タッチイベント 688 | function onTouchMotion(event) { 689 | event.preventDefault(); 690 | 691 | switch (event.type) { 692 | case "touchstart": 693 | if (touchId == null) { 694 | let touch = event.targetTouches[0]; 695 | touchX = touch.clientX; 696 | touchY = touch.clientY; 697 | touchId = touch.identifier; 698 | isDownMouse = true; 699 | } 700 | break; 701 | 702 | case "touchmove": 703 | if (touchId != null && isDownMouse) { 704 | for (let i = 0; i < event.changedTouches.length; ++i) { 705 | let touch = event.targetTouches[i]; 706 | if (touch.identifier == touchId) { 707 | onMotionGL(touch.clientX - touchX, touch.clientY - touchY); 708 | touchX = touch.clientX; 709 | touchY = touch.clientY; 710 | } 711 | } 712 | } 713 | break; 714 | 715 | case "touchend": 716 | case "touchcancel": 717 | if (touchId != null) { 718 | for (let i = 0; i < event.changedTouches.length; ++i) { 719 | let touch = event.changedTouches[i]; 720 | if (touch.identifier == touchId) { 721 | touchId = null; 722 | isDownMouse = false; 723 | } 724 | } 725 | } 726 | break; 727 | } 728 | } 729 | 730 | // イベント登録 731 | addEventListener("load", onLoad); 732 | addEventListener("unload", onUnload); 733 | addEventListener("resize", onResize); 734 | addEventListener('dragover', onDragOver); 735 | addEventListener('drop', onDrop); 736 | 737 | //////////////////////////////////////////////////////////////// 738 | // 描画関連 739 | 740 | let gl; 741 | let shaderProgram; 742 | let shaderAttributes; 743 | let shaderUniforms; 744 | let boxVertices; 745 | 746 | let boxElements; 747 | let workMatrix; 748 | 749 | let canAnimation; 750 | let autoRotation; 751 | let viewYow; 752 | let viewPitch; 753 | let prevTime; 754 | let frequencyVisualizerParams; // ビジュアライザ用のパラメータ [チャネル][周波数バインド][履歴] 755 | 756 | // WebGLの初期化 757 | function initializeGL() { 758 | // コンテキストの初期化 759 | gl = mainCanvas.getContext("webgl") || mainCanvas.getContext("experimental-webgl"); 760 | 761 | // シェーダの生成 762 | shaderAttributes = { 763 | "a_position": 0, 764 | "a_color": 1 765 | }; 766 | shaderUniforms = { 767 | "u_projection": -1, 768 | "u_view": -1, 769 | "u_model": -1, 770 | "u_color": -1 771 | }; 772 | shaderProgram = tutils.createShaderProgram( 773 | gl, document.getElementById("vs").text, document.getElementById("fs").text, 774 | shaderAttributes, shaderUniforms); 775 | 776 | // モデルの生成 777 | boxVertices = tutils.createBuffer(gl, gl.ARRAY_BUFFER, new Float32Array([ 778 | 0.5, 0.0, 0.5, 0.0, 0.0, 0.0, 1.0, 779 | 0.5, 0.0, -0.5, 0.0, 0.0, 0.0, 1.0, 780 | -0.5, 0.0, -0.5, 0.0, 0.0, 0.0, 1.0, 781 | -0.5, 0.0, 0.5, 0.0, 0.0, 0.0, 1.0, 782 | 0.5, 1.0, 0.5, 1.0, 1.0, 1.0, 1.0, 783 | 0.5, 1.0, -0.5, 1.0, 1.0, 1.0, 1.0, 784 | -0.5, 1.0, -0.5, 1.0, 1.0, 1.0, 1.0, 785 | -0.5, 1.0, 0.5, 1.0, 1.0, 1.0, 1.0 786 | ]), gl.STATIC_DRAW); 787 | boxElements = tutils.createBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([ 788 | 0, 1, 2, 0, 2, 3, 789 | 4, 6, 5, 4, 7, 6, 790 | 0, 3, 4, 3, 7, 4, 791 | 0, 4, 1, 1, 4, 5, 792 | 2, 7, 3, 2, 6, 7, 793 | 1, 5, 2, 2, 5, 6 794 | ]), gl.STATIC_DRAW); 795 | 796 | // 汎用設定の初期化 797 | gl.clearColor(0.125, 0.125, 0.125, 1.0); 798 | gl.enable(gl.CULL_FACE); 799 | //gl.enable(gl.DEPTH_TEST); // 加算合成のみで描画順に依存させないため深度テストは使用しない 800 | gl.enable(gl.BLEND); 801 | gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE); 802 | 803 | // その他の初期化 804 | workMatrix = new Float32Array(16); 805 | frequencyVisualizerParams = new Array(2); 806 | for (let i = 0; i < 2; ++i) { 807 | frequencyVisualizerParams[i] = new Array(NUM_VISUALIZE_BINDS); 808 | for (let j = 0; j < NUM_VISUALIZE_BINDS; ++j) { 809 | frequencyVisualizerParams[i][j] = new Float32Array(NUM_VISUALIZE_HISTORIES); 810 | } 811 | } 812 | 813 | autoRotation = true; 814 | viewYow = -0.5; 815 | viewPitch = 0.2; 816 | canAnimation = true; 817 | } 818 | 819 | // WebGLの後処理 820 | function terminateGL() { 821 | if (!isLostGL()) { 822 | canAnimation = false; 823 | gl.deleteBuffer(boxVertices); 824 | gl.deleteBuffer(boxElements); 825 | gl.deleteProgram(shaderProgram); 826 | } 827 | } 828 | 829 | // コンテキストロストが起こっていないかチェック 830 | function isLostGL() { 831 | let error = gl.getError(); 832 | return error != gl.NO_ERROR && error != gl.CONTEXT_LOST_WEBGL; 833 | } 834 | 835 | // ビジュアライザーのパラメータを更新 836 | function updateFrequencyVisualizerParam(channel, frequency) { 837 | for (let i = 0; i < NUM_VISUALIZE_BINDS; ++i) { 838 | // 特徴的な周波数成分は低周波数に集まりやすいので、低周波数の音を中心に解像度を上げる 839 | let indexFq = (Math.round(NUM_SAMPLES * Math.pow(0.5, i * NUM_FREQUENCY_BUNDLES / NUM_VISUALIZE_BINDS)) - 1) << 1; 840 | 841 | // 複素数の絶対値を求める 842 | let re = frequency[indexFq]; 843 | let im = frequency[indexFq + 1]; 844 | let volume = Math.sqrt(re * re + im * im) * 0.1; 845 | 846 | // 保持されているボリューム値より大きければ上書き 847 | let indexVol = NUM_VISUALIZE_BINDS - i - 1; 848 | frequencyVisualizerParams[channel][indexVol][0] = 849 | Math.max(frequencyVisualizerParams[channel][indexVol][0], volume); 850 | } 851 | } 852 | 853 | // モーションイベント 854 | function onMotionGL(dx, dy) { 855 | autoRotation = false; 856 | let weight = Math.min(mainCanvas.width, mainCanvas.height); 857 | viewYow += Math.PI * dx / weight; 858 | viewPitch += Math.PI * dy / weight; 859 | } 860 | 861 | // アニメーション処理 862 | function onAnimationGL() { 863 | if (!canAnimation) { 864 | return; 865 | } 866 | requestAnimationFrame(onAnimationGL); 867 | 868 | // 時間の差分を計測 869 | let nowTime = new Date().getTime(); 870 | let delta = prevTime != null ? Math.min(Math.max((nowTime - prevTime) / 1000.0, 1.0 / 120.0), 1.0 / 30.0) : 0.0; 871 | prevTime = nowTime; 872 | 873 | // コンテキストのロストチェック 874 | if (isLostGL()) { 875 | initializeGL(); 876 | } 877 | 878 | // 描画前の設定 879 | gl.viewport(0, 0, mainCanvas.width, mainCanvas.height); 880 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 881 | gl.useProgram(shaderProgram); 882 | 883 | // プロジェクション行列の設定 884 | tutils.setIdentityMatrix(workMatrix); 885 | if (navigation.style.display == "" || navigation.style.display == "block") { 886 | tutils.mulTranslateMatrix(workMatrix, navigation.clientWidth / mainCanvas.clientWidth, 0, 0); 887 | } 888 | tutils.mulProjectionMatrix( 889 | workMatrix, 890 | Math.min(mainCanvas.width / mainCanvas.height, 1.0), 891 | Math.min(mainCanvas.height / mainCanvas.width, 1.0), 892 | 0.4, 100); 893 | gl.uniformMatrix4fv(shaderUniforms.u_projection, false, workMatrix); 894 | 895 | // ビュー行列の設定 896 | tutils.setIdentityMatrix(workMatrix); 897 | tutils.mulViewMatrix( 898 | workMatrix, 899 | 0, 0, -30, 900 | 0, 0, 0, 901 | 0, -1, 0); 902 | tutils.mulRotationMatrix(workMatrix, -1.0, 0.0, 0.0, viewPitch); 903 | tutils.mulRotationMatrix(workMatrix, 0.0, -1.0, 0.0, viewYow); 904 | tutils.mulTranslateMatrix(workMatrix, 0.0, -7.0, 0.0); 905 | 906 | gl.uniformMatrix4fv(shaderUniforms.u_view, false, workMatrix); 907 | 908 | // モデルの有効化 909 | gl.bindBuffer(gl.ARRAY_BUFFER, boxVertices); 910 | gl.enableVertexAttribArray(shaderAttributes.a_position); 911 | gl.vertexAttribPointer(shaderAttributes.a_position, 3, gl.FLOAT, false, 4 * (3 + 4), 0); 912 | gl.enableVertexAttribArray(shaderAttributes.a_color); 913 | gl.vertexAttribPointer(shaderAttributes.a_color, 4, gl.FLOAT, false, 4 * (3 + 4), 4 * 3); 914 | 915 | // ビジュアライザの描画 916 | for (let i = 0; i < 2; ++i) { 917 | let xSign; 918 | if (i == 0) { 919 | xSign = 1; 920 | gl.uniform4f(shaderUniforms.u_color, 1.0, 0.25, 0.125, 1.0); 921 | } else { 922 | xSign = -1; 923 | gl.uniform4f(shaderUniforms.u_color, 0.125, 0.5, 1.0, 1.0); 924 | } 925 | 926 | for (let j = 0; j < NUM_VISUALIZE_BINDS; ++j) { 927 | for (let k = 0; k < NUM_VISUALIZE_HISTORIES; ++k) { 928 | let volume = frequencyVisualizerParams[i][j][k] * (NUM_VISUALIZE_HISTORIES - k) / NUM_VISUALIZE_HISTORIES; 929 | 930 | // モデル行列の設定 931 | tutils.setIdentityMatrix(workMatrix); 932 | tutils.mulTranslateMatrix(workMatrix, xSign * (-1.0 + -1.25 * k), 0, -1.25 * (j - NUM_VISUALIZE_BINDS / 2)); 933 | tutils.mulScaleMatrix(workMatrix, 0.5, Math.max(volume, 0.01), 0.5); 934 | gl.uniformMatrix4fv(shaderUniforms.u_model, false, workMatrix); 935 | 936 | // 箱の描画 937 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, boxElements); 938 | gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0); 939 | 940 | // 減衰処理 941 | if (k == 0) { 942 | frequencyVisualizerParams[i][j][k] *= Math.pow(0.125, delta); 943 | } else if (k > 0) { 944 | frequencyVisualizerParams[i][j][k] *= Math.pow(0.125, delta); 945 | frequencyVisualizerParams[i][j][k] += 946 | (frequencyVisualizerParams[i][j][k - 1] - frequencyVisualizerParams[i][j][k]) * delta * 15.0; 947 | } 948 | } 949 | } 950 | } 951 | 952 | // 箱の無効化 953 | gl.disableVertexAttribArray(shaderAttributes.a_position); 954 | gl.disableVertexAttribArray(shaderAttributes.a_color); 955 | gl.bindBuffer(gl.ARRAY_BUFFER, null); 956 | gl.bindBuffer(gl.ARRAY_BUFFER, null); 957 | 958 | // アニメーション 959 | if (autoRotation) { 960 | viewYow += 0.04 * delta; 961 | } 962 | } 963 | 964 | })(); 965 | -------------------------------------------------------------------------------- /src/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redlily/training-webaudio-equalizer/1a1077d7f2037911194486d0162038aca02891a1/src/play.png -------------------------------------------------------------------------------- /src/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/tw067.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redlily/training-webaudio-equalizer/1a1077d7f2037911194486d0162038aca02891a1/src/tw067.mp3 -------------------------------------------------------------------------------- /src/volume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redlily/training-webaudio-equalizer/1a1077d7f2037911194486d0162038aca02891a1/src/volume.png -------------------------------------------------------------------------------- /src/volume.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 11 | 14 | 18 | 21 | 25 | 26 | 27 | --------------------------------------------------------------------------------