![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/32da71eb50a84ccc83924d53882fe99c.gif#pic_center)
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/f9a15aa2405d4dfc9cf598678139ffed.png#pic_center)
@[TOC]
## 钢琴模拟器
#### 代码结构
###### HTML结构
\<html>: HTML文档的根元素。
\<head>: 包含文档的元数据。
\<base>: 指定相对URL的基准。
\<title>: 指定页面的标题。
\<style>: 包含嵌入的CSS样式。
\<body>: 包含文档的内容。
\<div class="container">: 容器元素,包含主要内容。
\<div class="controls">: 控件区域,包含选择框、按钮和移调控制。
\<select id="instrument-select">: 乐器选择框。
\<button id="record">: 录音按钮。
\<button id="play">: 播放按钮。
\<button id="stop">: 停止按钮。
\<div class="transpose-controls">: 移调控制区域。
\<button id="transpose-down">: 移调降低按钮。
\<input type="text" id="transpose-value" readonly>: 显示当前移调值。
\<button id="transpose-up">: 移调升高按钮。
\<div id="current-instrument">: 当前乐器显示区域。
\<div id="keyboard">: 键盘区域。
\<div id="chord-pads">: 和弦按钮区域。
\<div id="loading">: 加载提示。
###### CSS样式
body, html: 设置页面的基本样式。
.container: 设置容器的样式。
.controls: 设置控件区域的样式。
select, button, input: 设置选择框、按钮和输入框的样式。
#instrument-select: 设置乐器选择框的样式。
#current-instrument, #transpose-value: 设置当前乐器和移调值的样式。
.transpose-controls: 设置移调控制区域的样式。
#keyboard: 设置键盘区域的样式。
.key: 设置键的样式。
.key.black: 设置黑键的样式。
#loading: 设置加载提示的样式。
#chord-pads: 设置和弦按钮区域的样式。
.chord-pad: 设置和弦按钮的样式。
###### JavaScript功能
**时钟更新**:
updateClock: 更新时钟的时间和日期。
setInterval(updateClock, 1000): 每秒更新一次时钟。
updateClock(): 初次加载时立即更新时钟。
**点击事件**:
监听乐器选择框、录音按钮、播放按钮和停止按钮的点击事件。
根据点击的元素执行相应的操作(如录音、播放、停止等)。
**键盘功能**:
createKeyboard: 创建键盘。
createChordPads: 创建和弦按钮。
loadSoundFonts: 加载SoundFont。
loadSoundFont: 加载指定的SoundFont。
transposeNote: 移调音符。
playNote: 播放音符。
releaseNote: 释放音符。
playChord: 播放和弦。
releaseChord: 释放和弦。
startRecording: 开始录音。
stopRecording: 停止录音。
playRecording: 播放录音。
stopPlayback: 停止播放。
updateTransposeDisplay: 更新移调显示。
#### 源码
```html
<html><head><base href="https://websim.ai/app/soundfont-keyboard"/><title>SoundFont Keyboard: Interactive Musical Experience with Chords</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
color: #fff;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background: rgba(255, 255, 255, 0.1);
border-radius: 20px;
padding: 30px;
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.18);
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
}
.controls {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 20px;
}
select, button, input {
margin: 5px;
padding: 10px 15px;
font-size: 14px;
background-color: rgba(255, 255, 255, 0.2);
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
select:hover, button:hover {
background-color: rgba(255, 255, 255, 0.3);
}
#instrument-select {
width: 200px;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url('data:image/svg+xml;utf8,<svg fill="%23ffffff" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M7 10l5 5 5-5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>');
background-repeat: no-repeat;
background-position-x: 95%;
background-position-y: 50%;
}
#instrument-select option {
background-color: #2a2a2a;
color: #fff;
}
#current-instrument, #transpose-value {
margin-top: 10px;
font-style: italic;
}
.transpose-controls {
display: flex;
align-items: center;
margin-top: 10px;
}
.transpose-controls button {
width: 30px;
height: 30px;
padding: 0;
font-size: 18px;
line-height: 1;
}
#transpose-value {
margin: 0 10px;
width: 40px;
text-align: center;
background-color: rgba(255, 255, 255, 0.1);
}
#keyboard {
display: flex;
justify-content: center;
background: linear-gradient(to bottom, #4a4a4a, #2a2a2a);
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
}
.key {
width: 40px;
height: 150px;
background-color: #f0f0f0;
border: 1px solid #000;
margin: 0 2px;
cursor: pointer;
border-radius: 0 0 5px 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
transition: background-color 0.1s ease;
}
.key.black {
width: 30px;
height: 100px;
background-color: #000;
margin-left: -15px;
margin-right: -15px;
z-index: 1;
}
.key:active, .key.active {
background-color: #ddd;
}
.key.black:active, .key.black.active {
background-color: #333;
}
#loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
background: rgba(0, 0, 0, 0.7);
padding: 20px;
border-radius: 10px;
z-index: 20;
}
#chord-pads {
display: flex;
flex-wrap: wrap;
justify-content: center;
max-width: 600px;
margin-top: 20px;
}
.chord-pad {
width: 60px;
height: 60px;
margin: 5px;
font-size: 16px;
font-weight: bold;
background-color: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 50%;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.1s ease;
}
.chord-pad:hover {
background-color: rgba(255, 255, 255, 0.3);
}
.chord-pad:active {
transform: scale(0.95);
}
</style>
</head>
<body>
<div class="container">
<div class="controls">
<select id="instrument-select">
<option value="">Select an instrument...</option>
</select>
<button id="record">Record</button>
<button id="play">Play</button>
<button id="stop">Stop</button>
<div class="transpose-controls">
<button id="transpose-down">-</button>
<input type="text" id="transpose-value" value="0" readonly>
<button id="transpose-up">+</button>
</div>
</div>
<div id="current-instrument"></div>
<div id="keyboard"></div>
</div>
<div id="chord-pads"></div>
<div id="loading">Loading SoundFonts...</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/howler/2.2.3/howler.min.js"></script>
<script>
const keys = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
const keyMapping = {
'z': 'C3', 's': 'C#3', 'x': 'D3', 'd': 'D#3', 'c': 'E3', 'v': 'F3', 'g': 'F#3',
'b': 'G3', 'h': 'G#3', 'n': 'A3', 'j': 'A#3', 'm': 'B3',
'q': 'C4', '2': 'C#4', 'w': 'D4', '3': 'D#4', 'e': 'E4', 'r': 'F4', '5': 'F#4',
't': 'G4', '6': 'G#4', 'y': 'A4', '7': 'A#4', 'u': 'B4',
'i': 'C5', '9': 'C#5', 'o': 'D5', '0': 'D#5', 'p': 'E5', '[': 'F5', '=': 'F#5',
']': 'G5'
};
const octaves = 3;
let currentInstrument = 'acoustic_grand_piano';
let soundFont;
let recording = false;
let recordedNotes = [];
let startTime;
let playbackTimeouts = [];
let transposeValue = 0;
const pressedKeys = new Set();
const chords = {
'E': ['E4', 'G#4', 'B4'],
'A': ['A3', 'C#4', 'E4'],
'F': ['F3', 'A3', 'C4'],
'D': ['D4', 'F#4', 'A4'],
'G': ['G3', 'B3', 'D4'],
'C': ['C4', 'E4', 'G4'],
'B': ['B3', 'D#4', 'F#4'],
'Em': ['E4', 'G4', 'B4'],
'Am': ['A3', 'C4', 'E4'],
'Dm': ['D4', 'F4', 'A4'],
'Bm': ['B3', 'D4', 'F#4'],
'Cm': ['C4', 'D#4', 'G4'],
'Fm': ['F3', 'G#3', 'C4'],
'E7': ['E4', 'G#4', 'B4', 'D5'],
'A7': ['A3', 'C#4', 'E4', 'G4'],
'D7': ['D4', 'F#4', 'A4', 'C5'],
'G7': ['G3', 'B3', 'D4', 'F4'],
'C7': ['C4', 'E4', 'G4', 'A#4']
};
function createKeyboard() {
const keyboard = document.getElementById('keyboard');
for (let octave = 3; octave < 3 + octaves; octave++) {
keys.forEach((note) => {
const key = document.createElement('div');
key.className = `key ${note.includes('#') ? 'black' : 'white'}`;
key.dataset.note = `${note}${octave}`;
key.addEventListener('mousedown', () => playNote(`${note}${octave}`));
key.addEventListener('mouseup', () => releaseNote(`${note}${octave}`));
key.addEventListener('mouseleave', () => releaseNote(`${note}${octave}`));
keyboard.appendChild(key);
});
}
}
function createChordPads() {
const chordPads = document.getElementById('chord-pads');
Object.keys(chords).forEach(chordName => {
const pad = document.createElement('button');
pad.className = 'chord-pad';
pad.textContent = chordName;
pad.addEventListener('mousedown', () => playChord(chordName));
pad.addEventListener('mouseup', () => releaseChord(chordName));
pad.addEventListener('mouseleave', () => releaseChord(chordName));
chordPads.appendChild(pad);
});
}
createKeyboard();
createChordPads();
async function loadSoundFonts() {
const response = await fetch('https://gleitz.github.io/midi-js-soundfonts/MusyngKite/names.json');
const instruments = await response.json();
const select = document.getElementById('instrument-select');
instruments.forEach(instrument => {
const option = document.createElement('option');
option.value = instrument;
option.textContent = instrument.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
select.appendChild(option);
});
loadSoundFont('acoustic_grand_piano');
}
async function loadSoundFont(instrument) {
document.getElementById('loading').style.display = 'block';
currentInstrument = instrument;
const response = await fetch(`https://gleitz.github.io/midi-js-soundfonts/MusyngKite/${instrument}-mp3.js`);
const soundFontData = await response.text();
eval(soundFontData);
soundFont = MIDI.Soundfont[instrument];
document.getElementById('loading').style.display = 'none';
document.getElementById('current-instrument').textContent = `Current Instrument: ${instrument.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}`;
}
function transposeNote(note) {
const [noteName, octave] = [note.slice(0, -1), parseInt(note.slice(-1))];
let noteIndex = keys.indexOf(noteName);
noteIndex += transposeValue;
let newOctave = octave + Math.floor(noteIndex / 12);
noteIndex = (noteIndex + 12) % 12; // Ensure positive index
return `${keys[noteIndex]}${newOctave}`;
}
function playNote(note) {
if (!soundFont) return;
const transposedNote = transposeNote(note);
let sound = new Howl({
src: [soundFont[transposedNote]],
format: ['mp3']
});
sound.play();
if (recording) {
const time = Date.now() - startTime;
recordedNotes.push({ note, time });
}
// Highlight the key
const key = document.querySelector(`.key[data-note="${note}"]`);
if (key) {
key.classList.add('active');
}
}
function releaseNote(note) {
// Remove highlight from the key
const key = document.querySelector(`.key[data-note="${note}"]`);
if (key) {
key.classList.remove('active');
}
}
function playChord(chordName) {
if (!soundFont) return;
chords[chordName].forEach(note => {
playNote(note);
});
if (recording) {
const time = Date.now() - startTime;
recordedNotes.push({ chord: chordName, time });
}
}
function releaseChord(chordName) {
chords[chordName].forEach(note => {
releaseNote(note);
});
}
function startRecording() {
recording = true;
recordedNotes = [];
startTime = Date.now();
document.getElementById('record').textContent = 'Stop Recording';
}
function stopRecording() {
recording = false;
document.getElementById('record').textContent = 'Record';
}
function playRecording() {
if (recordedNotes.length === 0) return;
stopPlayback(); // Stop any ongoing playback
const playbackStartTime = Date.now();
recordedNotes.forEach(({ note, chord, time }) => {
const timeout = setTimeout(() => {
if (note) {
playNote(note);
setTimeout(() => releaseNote(note), 200);
} else if (chord) {
playChord(chord);
setTimeout(() => releaseChord(chord), 200);
}
}, time);
playbackTimeouts.push(timeout);
});
}
function stopPlayback() {
// Clear all scheduled playback timeouts
playbackTimeouts.forEach(timeout => clearTimeout(timeout));
playbackTimeouts = [];
""
// Stop all currently playing sounds
Howler.stop();
// Reset all key colors
document.querySelectorAll('.key').forEach(key => {
key.classList.remove('active');
});
}
function updateTransposeDisplay() {
const transposeInput = document.getElementById('transpose-value');
transposeInput.value = transposeValue >= 0 ? `+${transposeValue}` : transposeValue;
}
loadSoundFonts();
document.getElementById('instrument-select').addEventListener('change', (e) => loadSoundFont(e.target.value));
document.getElementById('record').addEventListener('click', () => {
if (recording) {
stopRecording();
} else {
startRecording();
}
});
document.getElementById('play').addEventListener('click', playRecording);
document.getElementById('stop').addEventListener('click', stopPlayback);
document.getElementById('transpose-down').addEventListener('click', () => {
transposeValue = Math.max(transposeValue - 1, -12);
updateTransposeDisplay();
});
document.getElementById('transpose-up').addEventListener('click', () => {
transposeValue = Math.min(transposeValue + 1, 12);
updateTransposeDisplay();
});
window.addEventListener('keydown', (e) => {
const note = keyMapping[e.key.toLowerCase()];
if (note && !pressedKeys.has(note)) {
pressedKeys.add(note);
playNote(note);
}
});
window.addEventListener('keyup', (e) => {
const note = keyMapping[e.key.toLowerCase()];
if (note) {
pressedKeys.delete(note);
releaseNote(note);
}
});
// Initialize transpose display
updateTransposeDisplay();
</script>
</body></html>
```
#### 效果图
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/9b767c64364244b5b550d3efcc38423c.png)
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/6756ea126fc24e81b41d019ef8cac09c.png#pic_center)