Last active
July 23, 2025 21:11
-
-
Save TwoSquirrels/2bd2f0971c4d53573bc1ac5bbe30c87b to your computer and use it in GitHub Desktop.
パラメーターを調整して「あ」の音をフォルマント合成して遊べる Web アプリ (create by Gemini)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="ja"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>「あ」の音ジェネレーター</title> | |
| <link rel="stylesheet" href="style.css" /> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>「あ」の音ジェネレーター 🗣️</h1> | |
| <button id="playStopButton">再生</button> | |
| <div class="controls"> | |
| <h2>基本設定</h2> | |
| <div class="control-group"> | |
| <label for="f0">基本周波数 (F0): <span id="f0Value">120</span> Hz</label> | |
| <input type="range" id="f0" min="50" max="300" value="120" step="1" /> | |
| </div> | |
| <div class="control-group"> | |
| <label for="masterGain">マスター音量: <span id="masterGainValue">0.5</span></label> | |
| <input type="range" id="masterGain" min="0" max="1" value="0.5" step="0.01" /> | |
| </div> | |
| <h2>フォルマント 1 (F1)</h2> | |
| <div class="control-group"> | |
| <label for="f1">周波数 (F1): <span id="f1Value">730</span> Hz</label> | |
| <input type="range" id="f1" min="200" max="1200" value="730" step="10" /> | |
| </div> | |
| <div class="control-group"> | |
| <label for="q1">Q値 (Q1): <span id="q1Value">20</span></label> | |
| <input type="range" id="q1" min="1" max="100" value="20" step="1" /> | |
| </div> | |
| <div class="control-group"> | |
| <label for="gain1">ゲイン (Gain1): <span id="gain1Value">1</span></label> | |
| <input type="range" id="gain1" min="0" max="2" value="1" step="0.05" /> | |
| </div> | |
| <h2>フォルマント 2 (F2)</h2> | |
| <div class="control-group"> | |
| <label for="f2">周波数 (F2): <span id="f2Value">1100</span> Hz</label> | |
| <input type="range" id="f2" min="500" max="2500" value="1100" step="10" /> | |
| </div> | |
| <div class="control-group"> | |
| <label for="q2">Q値 (Q2): <span id="q2Value">20</span></label> | |
| <input type="range" id="q2" min="1" max="100" value="20" step="1" /> | |
| </div> | |
| <div class="control-group"> | |
| <label for="gain2">ゲイン (Gain2): <span id="gain2Value">0.8</span></label> | |
| <input type="range" id="gain2" min="0" max="2" value="0.8" step="0.05" /> | |
| </div> | |
| <h2>フォルマント 3 (F3)</h2> | |
| <div class="control-group"> | |
| <label for="f3">周波数 (F3): <span id="f3Value">2700</span> Hz</label> | |
| <input type="range" id="f3" min="1500" max="4000" value="2700" step="10" /> | |
| </div> | |
| <div class="control-group"> | |
| <label for="q3">Q値 (Q3): <span id="q3Value">20</span></label> | |
| <input type="range" id="q3" min="1" max="100" value="20" step="1" /> | |
| </div> | |
| <div class="control-group"> | |
| <label for="gain3">ゲイン (Gain3): <span id="gain3Value">0.5</span></label> | |
| <input type="range" id="gain3" min="0" max="2" value="0.5" step="0.05" /> | |
| </div> | |
| </div> | |
| </div> | |
| <script src="script.js"></script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| let oscillator = null; | |
| let masterGainNode = null; | |
| let formantFilters = []; | |
| let formantGains = []; | |
| let isPlaying = false; | |
| const playStopButton = document.getElementById("playStopButton"); | |
| const f0Control = document.getElementById("f0"); | |
| const f0ValueDisplay = document.getElementById("f0Value"); | |
| const masterGainControl = document.getElementById("masterGain"); | |
| const masterGainValueDisplay = document.getElementById("masterGainValue"); | |
| const fControls = [ | |
| { | |
| freq: document.getElementById("f1"), | |
| q: document.getElementById("q1"), | |
| gain: document.getElementById("gain1"), | |
| freqVal: document.getElementById("f1Value"), | |
| qVal: document.getElementById("q1Value"), | |
| gainVal: document.getElementById("gain1Value"), | |
| }, | |
| { | |
| freq: document.getElementById("f2"), | |
| q: document.getElementById("q2"), | |
| gain: document.getElementById("gain2"), | |
| freqVal: document.getElementById("f2Value"), | |
| qVal: document.getElementById("q2Value"), | |
| gainVal: document.getElementById("gain2Value"), | |
| }, | |
| { | |
| freq: document.getElementById("f3"), | |
| q: document.getElementById("q3"), | |
| gain: document.getElementById("gain3"), | |
| freqVal: document.getElementById("f3Value"), | |
| qVal: document.getElementById("q3Value"), | |
| gainVal: document.getElementById("gain3Value"), | |
| }, | |
| ]; | |
| function createSound() { | |
| if (isPlaying) return; // すでに再生中の場合は何もしない | |
| // Oscillator (Source) | |
| oscillator = audioContext.createOscillator(); | |
| oscillator.type = "sawtooth"; // ノコギリ波は倍音が豊富でフォルマント処理に適している | |
| oscillator.frequency.setValueAtTime(parseFloat(f0Control.value), audioContext.currentTime); | |
| // Master Gain | |
| masterGainNode = audioContext.createGain(); | |
| masterGainNode.gain.setValueAtTime(parseFloat(masterGainControl.value), audioContext.currentTime); | |
| // Formant Filters and Gains | |
| formantFilters = []; | |
| formantGains = []; | |
| let previousNode = oscillator; | |
| fControls.forEach((controlSet, i) => { | |
| const filter = audioContext.createBiquadFilter(); | |
| filter.type = "bandpass"; | |
| filter.frequency.setValueAtTime(parseFloat(controlSet.freq.value), audioContext.currentTime); | |
| filter.Q.setValueAtTime(parseFloat(controlSet.q.value), audioContext.currentTime); | |
| const gainNode = audioContext.createGain(); | |
| gainNode.gain.setValueAtTime(parseFloat(controlSet.gain.value), audioContext.currentTime); | |
| previousNode.connect(filter); | |
| filter.connect(gainNode); | |
| gainNode.connect(masterGainNode); // 各フォルマントフィルター出力をマスターゲインに接続 (並列) | |
| // もし直列にしたい場合は、gainNode.connect(nextFilter or masterGainNode) のように調整 | |
| formantFilters.push(filter); | |
| formantGains.push(gainNode); | |
| // この構成ではフィルターは並列的にソースに作用するのではなく、 | |
| // 実際にはソース -> F1 -> G1 -> Master, ソース -> F2 -> G2 -> Master... のように | |
| // ソースが分岐して各フィルターチェーンに入る形が望ましい。 | |
| // 簡単のため、ここではソースを共通にし、各フィルター+ゲインの出力をマスターゲインにミックスする。 | |
| // より正確なフォルマント合成はフィルターを直列にするか、各フィルター出力を加算する形になる。 | |
| // ここでは簡略化のため、各フィルターを独立してマスターゲインに接続する | |
| // ただし、この接続だと意図したフィルタリングにならない可能性が高い。 | |
| // 正しくは、オシレーター出力を一度分岐させ、各フィルターを通したものを最後にミックスするか、 | |
| // オシレーター -> F1 -> F2 -> F3 -> マスターゲイン のように直列に接続する。 | |
| // ここでは、各フィルターが独立して原音に作用し、その結果をマスターゲインで合わせる形を意図したが、 | |
| // 実際の接続は oscillator -> filter1 -> gain1 -> masterGain, oscillator -> filter2 -> gain2 -> masterGain ... | |
| // のような形になる。これは正しくない。 | |
| // | |
| // 修正:オシレーターの出力を各フォルマントフィルターパスに供給し、 | |
| // 各パスの出力を最終的にマスターゲインにミックスする。 | |
| // oscillator -> gainNodeForSplitting -> filter1 -> gain1 -> sumGain | |
| // -> filter2 -> gain2 -> sumGain | |
| // -> filter3 -> gain3 -> sumGain | |
| // sumGain -> masterGainNode -> destination | |
| // | |
| // よりシンプルな直列モデル: | |
| // oscillator -> filter1 -> filter2 -> filter3 -> masterGainNode -> destination | |
| // 今回はこちらを採用してみる。ゲインは各フィルターの後ではなく、フィルター自体で調整するか、 | |
| // もしくは各フィルターのゲインは固定とし、周波数とQで調整する。 | |
| // ここでは、各フォルマントパスにゲインノードを挟む形を取る。 | |
| // oscillator -> filter1 -> gain1 -> filter2 -> gain2 -> filter3 -> gain3 -> masterGainNode -> destination | |
| // === 接続の修正 === | |
| // 以前のコードでは previousNode を更新していなかったため、全てのフィルターが直接 oscillator に接続されていた。 | |
| // 正しい直列接続にする: | |
| // oscillator -> F1 -> G1 -> F2 -> G2 -> F3 -> G3 -> masterGainNode | |
| }); | |
| // 接続を再構築 (直列モデル) | |
| oscillator.connect(formantFilters[0]); | |
| formantFilters[0].connect(formantGains[0]); | |
| formantGains[0].connect(formantFilters[1]); | |
| formantFilters[1].connect(formantGains[1]); | |
| formantGains[1].connect(formantFilters[2]); | |
| formantFilters[2].connect(formantGains[2]); | |
| formantGains[2].connect(masterGainNode); | |
| masterGainNode.connect(audioContext.destination); | |
| oscillator.start(); | |
| isPlaying = true; | |
| playStopButton.textContent = "停止"; | |
| playStopButton.classList.add("playing"); | |
| } | |
| function stopSound() { | |
| if (!isPlaying) return; | |
| if (oscillator) { | |
| oscillator.stop(); | |
| oscillator.disconnect(); | |
| } | |
| if (masterGainNode) { | |
| masterGainNode.disconnect(); | |
| } | |
| formantFilters.forEach((filter) => filter.disconnect()); | |
| formantGains.forEach((gain) => gain.disconnect()); | |
| oscillator = null; | |
| masterGainNode = null; | |
| formantFilters = []; | |
| formantGains = []; | |
| isPlaying = false; | |
| playStopButton.textContent = "再生"; | |
| playStopButton.classList.remove("playing"); | |
| } | |
| playStopButton.addEventListener("click", () => { | |
| if (audioContext.state === "suspended") { | |
| audioContext.resume(); | |
| } | |
| if (isPlaying) { | |
| stopSound(); | |
| } else { | |
| createSound(); | |
| } | |
| }); | |
| // --- Parameter Updates --- | |
| f0Control.addEventListener("input", (e) => { | |
| const value = parseFloat(e.target.value); | |
| f0ValueDisplay.textContent = value; | |
| if (oscillator) { | |
| oscillator.frequency.setValueAtTime(value, audioContext.currentTime); | |
| } | |
| }); | |
| masterGainControl.addEventListener("input", (e) => { | |
| const value = parseFloat(e.target.value); | |
| masterGainValueDisplay.textContent = value; | |
| if (masterGainNode) { | |
| masterGainNode.gain.setValueAtTime(value, audioContext.currentTime); | |
| } | |
| }); | |
| fControls.forEach((controlSet, i) => { | |
| controlSet.freq.addEventListener("input", (e) => { | |
| const value = parseFloat(e.target.value); | |
| controlSet.freqVal.textContent = value; | |
| if (formantFilters[i]) { | |
| formantFilters[i].frequency.setValueAtTime(value, audioContext.currentTime); | |
| } | |
| }); | |
| controlSet.q.addEventListener("input", (e) => { | |
| const value = parseFloat(e.target.value); | |
| controlSet.qVal.textContent = value; | |
| if (formantFilters[i]) { | |
| formantFilters[i].Q.setValueAtTime(value, audioContext.currentTime); | |
| } | |
| }); | |
| controlSet.gain.addEventListener("input", (e) => { | |
| const value = parseFloat(e.target.value); | |
| controlSet.gainVal.textContent = value; | |
| if (formantGains[i]) { | |
| formantGains[i].gain.setValueAtTime(value, audioContext.currentTime); | |
| } | |
| }); | |
| }); | |
| // 初期値表示の更新 | |
| f0ValueDisplay.textContent = f0Control.value; | |
| masterGainValueDisplay.textContent = masterGainControl.value; | |
| fControls.forEach((cs) => { | |
| cs.freqVal.textContent = cs.freq.value; | |
| cs.qVal.textContent = cs.q.value; | |
| cs.gainVal.textContent = cs.gain.value; | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| body { | |
| font-family: sans-serif; | |
| display: flex; | |
| justify-content: center; | |
| align-items: flex-start; /* 上寄せに変更 */ | |
| min-height: 100vh; | |
| background-color: #f4f4f4; | |
| margin: 0; | |
| padding-top: 20px; /* 上部に余白を追加 */ | |
| box-sizing: border-box; | |
| } | |
| .container { | |
| background-color: #fff; | |
| padding: 30px; | |
| border-radius: 8px; | |
| box-shadow: 0 0 15px rgba(0, 0, 0, 0.1); | |
| width: 90%; | |
| max-width: 600px; | |
| } | |
| h1 { | |
| text-align: center; | |
| color: #333; | |
| margin-bottom: 25px; | |
| } | |
| h2 { | |
| color: #555; | |
| border-bottom: 1px solid #eee; | |
| padding-bottom: 8px; | |
| margin-top: 30px; | |
| margin-bottom: 15px; | |
| } | |
| .controls { | |
| margin-top: 20px; | |
| } | |
| .control-group { | |
| margin-bottom: 20px; | |
| } | |
| .control-group label { | |
| display: block; | |
| margin-bottom: 8px; | |
| color: #666; | |
| font-size: 0.9em; | |
| } | |
| .control-group input[type="range"] { | |
| width: 100%; | |
| cursor: pointer; | |
| } | |
| #playStopButton { | |
| display: block; | |
| width: 100%; | |
| padding: 12px; | |
| background-color: #007bff; | |
| color: white; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-size: 1.1em; | |
| margin-bottom: 25px; | |
| transition: background-color 0.3s; | |
| } | |
| #playStopButton:hover { | |
| background-color: #0056b3; | |
| } | |
| #playStopButton.playing { | |
| background-color: #dc3545; | |
| } | |
| #playStopButton.playing:hover { | |
| background-color: #c82333; | |
| } | |
| span[id$="Value"] { | |
| /* IDが"Value"で終わるspan要素すべてに適用 */ | |
| font-weight: bold; | |
| color: #007bff; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment