Skip to content

Instantly share code, notes, and snippets.

@TwoSquirrels
Last active July 23, 2025 21:11
Show Gist options
  • Select an option

  • Save TwoSquirrels/2bd2f0971c4d53573bc1ac5bbe30c87b to your computer and use it in GitHub Desktop.

Select an option

Save TwoSquirrels/2bd2f0971c4d53573bc1ac5bbe30c87b to your computer and use it in GitHub Desktop.
パラメーターを調整して「あ」の音をフォルマント合成して遊べる Web アプリ (create by Gemini)
<!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>
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;
});
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