<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chromatic Tuner สำหรับกีตาร์</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
color: white;
min-height: 100vh;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.container {
max-width: 900px;
width: 100%;
background-color: rgba(0, 0, 0, 0.7);
border-radius: 20px;
padding: 30px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
margin: 20px 0;
}
header {
text-align: center;
margin-bottom: 30px;
}
h1 {
font-size: 2.5rem;
margin-bottom: 10px;
color: #FFD700;
text-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
}
.subtitle {
font-size: 1.2rem;
opacity: 0.8;
}
.tuner-container {
display: flex;
flex-direction: column;
align-items: center;
margin: 30px 0;
}
.tuner-display {
background: rgba(30, 30, 50, 0.8);
border-radius: 15px;
padding: 25px;
width: 100%;
max-width: 500px;
margin-bottom: 30px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.note-display {
text-align: center;
margin-bottom: 25px;
}
.note-name {
font-size: 5rem;
font-weight: bold;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
color: #FFD700;
text-shadow: 0 0 15px rgba(255, 215, 0, 0.7);
}
.frequency-display {
font-size: 1.5rem;
margin-bottom: 10px;
}
.cent-display {
font-size: 1.2rem;
margin-bottom: 20px;
height: 25px;
}
.tuner-visual {
width: 100%;
height: 150px;
position: relative;
background: rgba(20, 20, 40, 0.8);
border-radius: 10px;
overflow: hidden;
margin-top: 20px;
}
.center-line {
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 3px;
background: #FFD700;
transform: translateX(-50%);
z-index: 1;
}
.needle {
position: absolute;
top: 10px;
bottom: 10px;
left: 50%;
width: 4px;
background: #ff4d4d;
transform-origin: bottom;
transform: translateX(-50%) rotate(0deg);
transition: transform 0.1s ease;
z-index: 2;
box-shadow: 0 0 10px rgba(255, 77, 77, 0.7);
}
.needle-head {
position: absolute;
top: 0;
left: 50%;
width: 20px;
height: 20px;
background: #ff4d4d;
border-radius: 50%;
transform: translateX(-50%) translateY(-50%);
}
.scale {
position: absolute;
bottom: 30px;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
padding: 0 20px;
}
.scale-mark {
color: #aaa;
font-size: 0.9rem;
}
.controls {
display: flex;
justify-content: center;
gap: 20px;
margin: 20px 0;
flex-wrap: wrap;
}
button {
background: linear-gradient(to bottom, #4a7bff, #2a4cb4);
color: white;
border: none;
border-radius: 50px;
padding: 15px 30px;
font-size: 1.1rem;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
gap: 10px;
}
button:hover {
transform: translateY(-3px);
box-shadow: 0 7px 20px rgba(0, 0, 0, 0.4);
background: linear-gradient(to bottom, #5a8bff, #3a5cc4);
}
button:active {
transform: translateY(1px);
}
button:disabled {
background: #555;
transform: none;
cursor: not-allowed;
opacity: 0.7;
}
.mic-indicator {
width: 15px;
height: 15px;
background-color: #ff4d4d;
border-radius: 50%;
display: inline-block;
}
button.active .mic-indicator {
background-color: #4dff4d;
box-shadow: 0 0 10px #4dff4d;
}
.notes-section {
margin-top: 30px;
}
h2 {
text-align: center;
margin-bottom: 20px;
color: #FFD700;
font-size: 1.8rem;
}
.guitar-strings {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 15px;
margin-bottom: 30px;
}
.string {
background: rgba(50, 50, 80, 0.7);
border-radius: 10px;
padding: 15px;
min-width: 120px;
text-align: center;
transition: all 0.3s;
border: 2px solid #444;
}
.string:hover {
transform: translateY(-5px);
border-color: #FFD700;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.string-name {
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 10px;
color: #FFD700;
}
.string-frequency {
font-size: 1.2rem;
margin-bottom: 15px;
}
.chromatic-notes {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12px;
margin-top: 20px;
}
.note-btn {
width: 60px;
height: 60px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(80, 80, 120, 0.8);
border-radius: 10px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid #555;
}
.note-btn:hover {
background: rgba(100, 100, 180, 0.9);
transform: scale(1.1);
}
.note-btn.flat {
background: rgba(90, 60, 100, 0.8);
}
.note-name-btn {
font-size: 1.3rem;
font-weight: bold;
}
.octave {
font-size: 0.9rem;
opacity: 0.8;
}
.instructions {
background: rgba(30, 30, 50, 0.6);
border-radius: 15px;
padding: 20px;
margin-top: 30px;
font-size: 1.1rem;
line-height: 1.6;
}
.instructions h3 {
color: #FFD700;
margin-bottom: 15px;
text-align: center;
}
.instructions ol {
padding-left: 25px;
}
.instructions li {
margin-bottom: 10px;
}
@media (max-width: 768px) {
.container {
padding: 15px;
}
h1 {
font-size: 2rem;
}
.note-name {
font-size: 3.5rem;
height: 70px;
}
.tuner-visual {
height: 120px;
}
.controls {
flex-direction: column;
align-items: center;
}
button {
width: 100%;
max-width: 300px;
}
.guitar-strings {
gap: 10px;
}
.string {
min-width: 100px;
padding: 10px;
}
.note-btn {
width: 50px;
height: 50px;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Chromatic Tuner สำหรับกีตาร์</h1>
<p class="subtitle">ปรับเสียงกีตาร์ของคุณให้แม่นยำด้วยมาตรฐาน A4 = 440Hz</p>
</header>
<div class="tuner-container">
<div class="tuner-display">
<div class="note-display">
<div class="note-name" id="noteDisplay">--</div>
<div class="frequency-display">ความถี่: <span id="frequencyDisplay">0</span> Hz</div>
<div class="cent-display" id="centDisplay"> </div>
</div>
<div class="tuner-visual">
<div class="center-line"></div>
<div class="needle" id="needle">
<div class="needle-head"></div>
</div>
<div class="scale">
<div class="scale-mark">-50¢</div>
<div class="scale-mark">ตรง</div>
<div class="scale-mark">+50¢</div>
</div>
</div>
</div>
<div class="controls">
<button id="listenBtn">
<span class="mic-indicator"></span>
<span>เริ่มฟังจากไมโครโฟน</span>
</button>
<button id="stopBtn">หยุดฟัง</button>
</div>
</div>
<div class="notes-section">
<h2>โน้ตอ้างอิง</h2>
<div class="guitar-strings">
<div class="string">
<div class="string-name">สาย 6 (ต่ำ)</div>
<div class="string-frequency">E2 - 82.41 Hz</div>
<button class="play-btn" data-frequency="82.41">เล่นเสียง</button>
</div>
<div class="string">
<div class="string-name">สาย 5</div>
<div class="string-frequency">A2 - 110.00 Hz</div>
<button class="play-btn" data-frequency="110.00">เล่นเสียง</button>
</div>
<div class="string">
<div class="string-name">สาย 4</div>
<div class="string-frequency">D3 - 146.83 Hz</div>
<button class="play-btn" data-frequency="146.83">เล่นเสียง</button>
</div>
<div class="string">
<div class="string-name">สาย 3</div>
<div class="string-frequency">G3 - 196.00 Hz</div>
<button class="play-btn" data-frequency="196.00">เล่นเสียง</button>
</div>
<div class="string">
<div class="string-name">สาย 2</div>
<div class="string-frequency">B3 - 246.94 Hz</div>
<button class="play-btn" data-frequency="246.94">เล่นเสียง</button>
</div>
<div class="string">
<div class="string-name">สาย 1 (สูง)</div>
<div class="string-frequency">E4 - 329.63 Hz</div>
<button class="play-btn" data-frequency="329.63">เล่นเสียง</button>
</div>
</div>
<h2 style="margin-top: 30px;">Chromatic Scale</h2>
<div class="chromatic-notes" id="chromaticNotes">
<!-- Chromatic notes will be generated here -->
</div>
</div>
<div class="instructions">
<h3>วิธีใช้</h3>
<ol>
<li>กดปุ่ม "เริ่มฟังจากไมโครโฟน" และอนุญาตให้เบราว์เซอร์เข้าถึงไมโครโฟน</li>
<li>ดีดสายกีตาร์ที่ต้องการปรับเสียง โดยถือกีตาร์ใกล้กับไมโครโฟน</li>
<li>ดูผลลัพธ์บนหน้าจอ - ชื่อโน้ตและความถูกต้องของเสียง</li>
<li>ปรับสายกีตาร์จนกว่าเข็มจะอยู่ตรงกลาง (0¢) และโน้ตถูกต้อง</li>
<li>กดปุ่ม "หยุดฟัง" เมื่อปรับเสียงเสร็จแล้ว</li>
<li>ใช้ปุ่ม "เล่นเสียง" เพื่อฟังโน้ตอ้างอิงแต่ละตัว</li>
</ol>
</div>
</div>
<script>
// ตั้งค่า Web Audio API
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
let analyser;
let microphone;
let isListening = false;
let animationId;
// องค์ประกอบ DOM
const noteDisplay = document.getElementById('noteDisplay');
const frequencyDisplay = document.getElementById('frequencyDisplay');
const centDisplay = document.getElementById('centDisplay');
const needle = document.getElementById('needle');
const listenBtn = document.getElementById('listenBtn');
const stopBtn = document.getElementById('stopBtn');
const playButtons = document.querySelectorAll('.play-btn');
const chromaticNotes = document.getElementById('chromaticNotes');
// ชื่อโน้ต chromatic scale
const noteStrings = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
// สร้าง chromatic scale (C4 ถึง B5)
function createChromaticNotes() {
const octaves = [3, 4, 5];
octaves.forEach(octave => {
noteStrings.forEach(note => {
if ((octave === 3 && note === "C") ||
(octave === 5 && note === "B") ||
(octave === 4) ||
(octave === 3 && ["D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"].includes(note)) ||
(octave === 5 && ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#"].includes(note))) {
const noteElement = document.createElement('div');
noteElement.className = `note-btn ${note.includes('#') ? 'flat' : ''}`;
const noteName = document.createElement('div');
noteName.className = 'note-name-btn';
noteName.textContent = note;
const octaveDisplay = document.createElement('div');
octaveDisplay.className = 'octave';
octaveDisplay.textContent = `${octave}`;
noteElement.appendChild(noteName);
noteElement.appendChild(octaveDisplay);
// คำนวณความถี่จากชื่อโน้ต
noteElement.dataset.note = note;
noteElement.dataset.octave = octave;
noteElement.addEventListener('click', playChromaticNote);
chromaticNotes.appendChild(noteElement);
}
});
});
}
// ฟังก์ชันเล่นโน้ต chromatic
function playChromaticNote(e) {
const note = e.currentTarget.dataset.note;
const octave = parseInt(e.currentTarget.dataset.octave);
// คำนวณความถี่จากชื่อโน้ต
const frequency = getFrequencyForNote(note, octave);
playReferenceTone(frequency);
}
// คำนวณความถี่จากชื่อโน้ตและอ็อกเทฟ
function getFrequencyForNote(note, octave) {
// A4 = 440Hz
const A4_FREQ = 440;
const A4_OCTAVE = 4;
const A4_INDEX = 9; // A is index 9 in noteStrings
const noteIndex = noteStrings.indexOf(note);
const semitonesFromA4 = (octave - A4_OCTAVE) * 12 + (noteIndex - A4_INDEX);
return A4_FREQ * Math.pow(2, semitonesFromA4 / 12);
}
// ฟังก์ชันเล่นเสียงอ้างอิง
function playReferenceTone(frequency, duration = 1.5) {
const oscillator = audioContext.createOscillator();
oscillator.type = 'sine';
oscillator.frequency.value = frequency;
const gainNode = audioContext.createGain();
gainNode.gain.value = 0.3;
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.start();
gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + duration);
oscillator.stop(audioContext.currentTime + duration);
}
// ฟังก์ชันเริ่มฟังจากไมโครโฟน
async function startListening() {
if (isListening) return;
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
microphone = audioContext.createMediaStreamSource(stream);
analyser = audioContext.createAnalyser();
// ตั้งค่าการวิเคราะห์เสียง
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.8;
microphone.connect(analyser);
isListening = true;
listenBtn.classList.add('active');
updatePitchDetection();
} catch (err) {
console.error('ไมโครโฟนไม่พร้อมใช้งาน:', err);
alert('ไม่สามารถเข้าถึงไมโครโฟนได้ โปรดตรวจสอบการอนุญาตและลองอีกครั้ง');
}
}
// หยุดการฟัง
function stopListening() {
if (!isListening) return;
if (microphone) {
microphone.disconnect();
}
isListening = false;
listenBtn.classList.remove('active');
if (animationId) {
cancelAnimationFrame(animationId);
}
// รีเซ็ตการแสดงผล
noteDisplay.textContent = '--';
frequencyDisplay.textContent = '0';
centDisplay.innerHTML = ' ';
needle.style.transform = 'translateX(-50%) rotate(0deg)';
}
// ฟังก์ชันตรวจจับ pitch
function updatePitchDetection() {
if (!isListening) return;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Float32Array(bufferLength);
analyser.getFloatTimeDomainData(dataArray);
const pitch = autoCorrelate(dataArray, audioContext.sampleRate);
if (pitch !== -1) {
const note = getNoteFromFrequency(pitch);
updateDisplay(note, pitch);
} else {
noteDisplay.textContent = '--';
frequencyDisplay.textContent = '0';
centDisplay.innerHTML = ' ';
needle.style.transform = 'translateX(-50%) rotate(0deg)';
}
animationId = requestAnimationFrame(updatePitchDetection);
}
// ฟังก์ชัน auto-correlation สำหรับตรวจจับ pitch
function autoCorrelate(buffer, sampleRate) {
// ตรวจสอบว่าเสียงดังพอที่จะวิเคราะห์
let sum = 0;
for (let i = 0; i < buffer.length; i++) {
sum += buffer[i] * buffer[i];
}
const rms = Math.sqrt(sum / buffer.length);
if (rms < 0.01) return -1; // เสียงเบาเกินไป
// หา threshold
let r1 = 0, r2 = buffer.length - 1, threshold = 0.2;
for (let i = 0; i < buffer.length / 2; i++) {
if (Math.abs(buffer[i]) < threshold) {
r1 = i;
break;
}
}
for (let i = 1; i < buffer.length / 2; i++) {
if (Math.abs(buffer[buffer.length - i]) < threshold) {
r2 = buffer.length - i;
break;
}
}
const newBuffer = buffer.slice(r1, r2);
if (newBuffer.length === 0) return -1;
// คำนวณ auto-correlation
const correlations = new Array(newBuffer.length).fill(0);
for (let i = 0; i < newBuffer.length; i++) {
for (let j = 0; j < newBuffer.length - i; j++) {
correlations[i] += newBuffer[j] * newBuffer[j + i];
}
}
// หาค่า peak สูงสุด
let maxIndex = 0;
let maxValue = -1;
for (let i = 1; i < correlations.length; i++) {
if (correlations[i] > maxValue) {
maxValue = correlations[i];
maxIndex = i;
}
}
// หาความถี่
const frequency = sampleRate / maxIndex;
return frequency;
}
// แปลงความถี่เป็นโน้ต
function getNoteFromFrequency(frequency) {
const A4 = 440;
const semitones = 12 * Math.log2(frequency / A4);
const noteIndex = Math.round(semitones) % 12;
const noteIndexAdjusted = (noteIndex + 12) % 12;
const noteName = noteStrings[noteIndexAdjusted];
const octave = Math.floor(4 + semitones / 12);
const cents = Math.round(100 * (semitones - Math.round(semitones)));
return {
name: noteName,
octave: octave,
frequency: frequency,
cents: cents
};
}
// อัพเดทการแสดงผล
function updateDisplay(note, frequency) {
noteDisplay.textContent = `${note.name}<small>${note.octave}</small>`;
frequencyDisplay.textContent = frequency.toFixed(1);
// แสดงค่าความต่าง (cents)
if (Math.abs(note.cents) < 5) {
centDisplay.innerHTML = '<span style="color:#4dff4d">ตรงเป๊ะ! (±0¢)</span>';
} else if (note.cents < 0) {
centDisplay.innerHTML = `<span style="color:#ff9999">ต่ำ ${Math.abs(note.cents)}¢</span>`;
} else {
centDisplay.innerHTML = `<span style="color:#ff9999">สูง ${note.cents}¢</span>`;
}
// อัพเดทเข็ม tuner
const centValue = Math.max(-50, Math.min(50, note.cents));
const rotation = (centValue / 50) * 45; // หมุนได้สูงสุด ±45 องศา
needle.style.transform = `translateX(-50%) rotate(${rotation}deg)`;
}
// ตั้งค่า event listeners
listenBtn.addEventListener('click', startListening);
stopBtn.addEventListener('click', stopListening);
playButtons.forEach(button => {
button.addEventListener('click', () => {
const frequency = parseFloat(button.dataset.frequency);
playReferenceTone(frequency);
});
});
// สร้าง chromatic notes เมื่อโหลดหน้า
window.addEventListener('load', createChromaticNotes);
// แสดงคำเตือนเมื่อปิดหน้าเว็บ
window.addEventListener('beforeunload', () => {
if (isListening) {
stopListening();
}
});
</script>
</body>
</html>
To embed this project on your website, copy the following code and paste it into your website's HTML: