◇WebAudioで音階を鳴らす
25
フェーズ: load待ち
WebAudioで音階再生
WebAudioを使い、MIDI音階指定で発声します。
コードを示します。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>GMっぽい音(Soundfont版)</title>
<style>
body { font-family: sans-serif; padding: 16px; }
button { margin-right: 8px; }
select { margin-right: 8px; }
#status { margin-top: 12px; padding: 8px; border: 1px solid #ccc; }
#log { margin-top: 12px; white-space: pre-wrap; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
#effectPanel { margin-top: 12px; padding: 8px; border: 1px solid #ccc; }
#echoValue { display: inline-block; min-width: 3em; }
</style>
</head>
<body>
<label for="selInstrument">音源</label>
<select id="selInstrument">
<option value="0000">0000 Acoustic Grand Piano</option>
<option value="0001">0001 Bright Acoustic Piano</option>
<option value="0006">0006 Harpsichord</option>
<option value="0007">0007 Clavinet</option>
<option value="0008">0008 Celesta</option>
<option value="0009">0009 Glockenspiel</option>
<option value="0011">0011 Vibraphone</option>
<option value="0019">0019 Church Organ</option>
<option value="0048">0048 String Ensemble 1</option>
<option value="0061">0061 Brass Section</option>
</select>
<button id="btnLoad">load</button>
<button id="btnPlay" disabled>play</button>
<div id="effectPanel">
<label>
<input id="chkEcho" type="checkbox" checked />
echo on
</label>
<br />
<label for="rngEcho">echo量</label>
<input id="rngEcho" type="range" min="0" max="100" value="25" />
<span id="echoValue">25</span>
</div>
<div id="status">フェーズ: load待ち</div>
<div id="log"></div>
<script type="module">
import {
Soundfont
} from "https://unpkg.com/smplr/dist/index.mjs";
// ----------------------------
// 画面要素
// ----------------------------
const selInstrument = document.getElementById("selInstrument");
const btnLoad = document.getElementById("btnLoad");
const btnPlay = document.getElementById("btnPlay");
const chkEcho = document.getElementById("chkEcho");
const rngEcho = document.getElementById("rngEcho");
const echoValue = document.getElementById("echoValue");
const statusEl = document.getElementById("status");
const logEl = document.getElementById("log");
// ----------------------------
// Web Audio 初期化
// ----------------------------
const AudioContextFunc = window.AudioContext || window.webkitAudioContext;
const ac = new AudioContextFunc();
// ----------------------------
// フェーズ定義
// ----------------------------
const Phase = Object.freeze({
WAIT_LOAD: "load待ち",
LOADING: "load中",
READY: "load完了",
PLAYING: "play",
PLAY_DONE: "play完了",
});
/** @type {keyof typeof Phase} */
let currentPhase = "WAIT_LOAD";
// ----------------------------
// 発声対象ノート
// 添付版に合わせて C4~C5 の並び
// ----------------------------
const notes = [60, 62, 64, 65, 67, 69, 71, 72];
// ----------------------------
// 発声設定
// ----------------------------
const NOTE_INTERVAL_SEC = 0.35;
const NOTE_DURATION_SEC = 0.30;
const NOTE_VELOCITY = 90; // smplr は 0..127
// ----------------------------
// SoundFontキット設定
// MusyngKite の方が高音質寄り
// ----------------------------
const SOUND_FONT_BASE_URL = "https://gleitz.github.io/midi-js-soundfonts/MusyngKite/";
// ----------------------------
// 音源テーブル
// instrumentUrl を直接指定して確実に読み込む形
// ----------------------------
const instrumentTable = {
"0000": {
label: "0000 Acoustic Grand Piano",
instrumentUrl: SOUND_FONT_BASE_URL + "acoustic_grand_piano-mp3.js"
},
"0001": {
label: "0001 Bright Acoustic Piano",
instrumentUrl: SOUND_FONT_BASE_URL + "bright_acoustic_piano-mp3.js"
},
"0006": {
label: "0006 Harpsichord",
instrumentUrl: SOUND_FONT_BASE_URL + "harpsichord-mp3.js"
},
"0007": {
label: "0007 Clavinet",
instrumentUrl: SOUND_FONT_BASE_URL + "clavinet-mp3.js"
},
"0008": {
label: "0008 Celesta",
instrumentUrl: SOUND_FONT_BASE_URL + "celesta-mp3.js"
},
"0009": {
label: "0009 Glockenspiel",
instrumentUrl: SOUND_FONT_BASE_URL + "glockenspiel-mp3.js"
},
"0011": {
label: "0011 Vibraphone",
instrumentUrl: SOUND_FONT_BASE_URL + "vibraphone-mp3.js"
},
"0019": {
label: "0019 Church Organ",
instrumentUrl: SOUND_FONT_BASE_URL + "church_organ-mp3.js"
},
"0048": {
label: "0048 String Ensemble 1",
instrumentUrl: SOUND_FONT_BASE_URL + "string_ensemble_1-mp3.js"
},
"0061": {
label: "0061 Brass Section",
instrumentUrl: SOUND_FONT_BASE_URL + "brass_section-mp3.js"
}
};
/** @type {any} */
let instrument = null;
// ----------------------------
// エコー用ノード
// smplr の destination に effectInput を渡します
// ----------------------------
const effectInput = ac.createGain();
const dryGain = ac.createGain();
const delayNode = ac.createDelay();
const wetGain = ac.createGain();
const feedbackGain = ac.createGain();
// ----------------------------
// エコー初期値
// ----------------------------
dryGain.gain.value = 1.0;
delayNode.delayTime.value = 0.28;
wetGain.gain.value = 0.0;
feedbackGain.gain.value = 0.0;
// ----------------------------
// ノード接続
// ----------------------------
effectInput.connect(
dryGain // 引数: 原音用ゲインノード
);
dryGain.connect(
ac.destination // 引数: 最終出力先
);
effectInput.connect(
delayNode // 引数: ディレイノード
);
delayNode.connect(
wetGain // 引数: エコー音量ノード
);
wetGain.connect(
ac.destination // 引数: 最終出力先
);
delayNode.connect(
feedbackGain // 引数: フィードバック量ノード
);
feedbackGain.connect(
delayNode // 引数: ディレイノードへ戻す接続
);
// ----------------------------
// ログ出力
// ----------------------------
function appendLog(line) {
logEl.textContent += line + "\n";
}
// ----------------------------
// フェーズ設定
// ----------------------------
function setPhase(nextPhase) {
currentPhase = nextPhase;
statusEl.textContent = "フェーズ: " + Phase[nextPhase];
updateButtons();
appendLog("-> " + Phase[nextPhase]);
}
// ----------------------------
// ボタン状態更新
// ----------------------------
function updateButtons() {
if (currentPhase === "WAIT_LOAD") {
selInstrument.disabled = false;
btnLoad.disabled = false;
btnPlay.disabled = true;
return;
}
if (currentPhase === "LOADING") {
selInstrument.disabled = true;
btnLoad.disabled = true;
btnPlay.disabled = true;
return;
}
if (currentPhase === "READY") {
selInstrument.disabled = false;
btnLoad.disabled = false;
btnPlay.disabled = false;
return;
}
if (currentPhase === "PLAYING") {
selInstrument.disabled = true;
btnLoad.disabled = true;
btnPlay.disabled = true;
return;
}
if (currentPhase === "PLAY_DONE") {
selInstrument.disabled = false;
btnLoad.disabled = false;
btnPlay.disabled = false;
return;
}
}
// ----------------------------
// エコー設定反映
// ----------------------------
function updateEchoSettings() {
const echoAmount = Number(rngEcho.value) / 100.0;
echoValue.textContent = rngEcho.value;
if (chkEcho.checked) {
wetGain.gain.value = 0.6 * echoAmount;
feedbackGain.gain.value = 0.7 * echoAmount;
} else {
wetGain.gain.value = 0.0;
feedbackGain.gain.value = 0.0;
}
}
// ----------------------------
// 音源名取得
// ----------------------------
function getSelectedInstrumentLabel() {
return selInstrument.options[selInstrument.selectedIndex].text;
}
// ----------------------------
// ノート番号を音名へ変換
// ----------------------------
function midiToNoteName(
midiNote // 引数: MIDIノート番号
) {
const names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
const octave = Math.floor(midiNote / 12) - 1;
const note = names[midiNote % 12];
return note + String(octave);
}
// ----------------------------
// load処理
// Soundfont を生成し、load Promise 完了まで待機
// ----------------------------
async function doLoad() {
if (!(currentPhase === "WAIT_LOAD" || currentPhase === "READY" || currentPhase === "PLAY_DONE")) {
return;
}
setPhase("LOADING");
await ac.resume();
const selected = instrumentTable[selInstrument.value];
instrument = null;
try {
const nextInstrument = new Soundfont(
ac, // 引数: AudioContext
{
instrumentUrl: selected.instrumentUrl, // 引数: 読み込むSoundFont定義URL
destination: effectInput, // 引数: 出力先ノード
volume: 100, // 引数: グローバル音量
loadLoopData: true, // 引数: 持続音用ループ情報も読む設定
onLoadProgress: ({ loaded, total }) => { // 引数: 読込進捗コールバック
statusEl.textContent = "フェーズ: load中";
if (total > 0) {
appendLog("load進捗: " + loaded + "/" + total);
}
}
}
);
instrument = await nextInstrument.load;
appendLog("音源: " + selected.label);
appendLog("ロード対象ノート: " + notes.join(","));
// 初回play時のもたつきを減らすために、無音ウォームアップを行います
dryGain.gain.value = 0.0;
for (let i = 0; i < notes.length; i += 1) {
instrument.start(
{
note: midiToNoteName(
notes[i] // 引数: MIDIノート番号
),
time: ac.currentTime + 0.05 + (i * 0.01), // 引数: 発音時刻
duration: 0.05, // 引数: 音の長さ
velocity: 1 // 引数: 音量
}
);
}
window.setTimeout(
() => {
dryGain.gain.value = 1.0;
updateEchoSettings();
setPhase("READY");
},
200
);
} catch (error) {
instrument = null;
appendLog(String(error));
setPhase("WAIT_LOAD");
}
}
// ----------------------------
// play処理
// ----------------------------
async function doPlay() {
if (!(currentPhase === "READY" || currentPhase === "PLAY_DONE")) {
return;
}
if (!instrument) {
appendLog("音源が未ロードです");
setPhase("WAIT_LOAD");
return;
}
setPhase("PLAYING");
await ac.resume();
const t0 = ac.currentTime + 0.08;
for (let i = 0; i < notes.length; i += 1) {
const note = notes[i];
const when = t0 + (i * NOTE_INTERVAL_SEC);
instrument.start(
{
note: midiToNoteName(
note // 引数: MIDIノート番号
),
time: when, // 引数: 発音時刻
duration: NOTE_DURATION_SEC, // 引数: 音の長さ
velocity: NOTE_VELOCITY // 引数: 音量
}
);
appendLog(
"発声予約: note=" +
note +
" at +" +
(when - t0).toFixed(2) +
"s"
);
}
const totalTime =
(notes.length - 1) * NOTE_INTERVAL_SEC +
NOTE_DURATION_SEC +
1.00;
window.setTimeout(
() => {
setPhase("PLAY_DONE");
},
Math.ceil(totalTime * 1000)
);
}
// ----------------------------
// イベント割り当て
// ----------------------------
btnLoad.addEventListener(
"click",
() => {
doLoad();
}
);
btnPlay.addEventListener(
"click",
() => {
doPlay();
}
);
selInstrument.addEventListener(
"change",
() => {
instrument = null;
appendLog("音源変更: " + getSelectedInstrumentLabel());
setPhase("WAIT_LOAD");
}
);
chkEcho.addEventListener(
"change",
() => {
updateEchoSettings();
appendLog("echo: " + (chkEcho.checked ? "on" : "off"));
}
);
rngEcho.addEventListener(
"input",
() => {
updateEchoSettings();
appendLog("echo量: " + rngEcho.value);
}
);
// ----------------------------
// 初期化
// ----------------------------
updateEchoSettings();
setPhase("WAIT_LOAD");
</script>
</body>
</html>

