<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>《将进酒》节奏检测工具</title>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
display: flex;
flex-direction: column;
align-items: center;
background-color: #f5f5f5;
padding: 20px;
}
.poem-box {
background: linear-gradient(135deg, #f8d7da, #cfe8fc);
border-radius: 15px;
padding: 20px;
margin-bottom: 20px;
width: 80%;
max-width: 600px;
}
.line {
font-size: 1.2em;
margin: 10px 0;
padding-left: 20px;
position: relative;
}
.line::before {
content: "";
position: absolute;
left: 0;
width: 5px;
height: 80%;
background-color: #6c757d;
opacity: 0.3;
}
.control-box {
display: flex;
gap: 20px;
margin-bottom: 30px;
}
button {
padding: 10px 20px;
font-size: 1em;
border: none;
border-radius: 5px;
cursor: pointer;
background-color: #007bff;
color: white;
}
button:hover {
background-color: #0056b3;
}
#waveform {
width: 100%;
height: 150px;
border: 1px solid #dee2e6;
margin: 20px 0;
background-color: white;
}
.score {
font-size: 1.5em;
color: #28a745;
font-weight: bold;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="poem-box">
<h2>《将进酒》节奏检测</h2>
<div class="line" data-time="0.0-1.5">君不见黄河之水天上来</div>
<div class="line" data-time="1.5-3.0">奔流到海不复回</div>
<div class="line" data-time="3.0-4.5">君不见高堂明镜悲白发</div>
<div class="line" data-time="4.5-6.0">朝如青丝暮成雪</div>
<!-- 可继续添加完整诗句,data-time为预设节奏区间(秒) -->
</div>
<div class="control-box">
<button id="recordBtn">开始录音</button>
<button id="playBtn" disabled>播放录音</button>
<button id="analyzeBtn" disabled>分析节奏</button>
</div>
<canvas id="waveform"></canvas>
<div class="score" id="scoreDisplay"></div>
<div id="feedback"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/recorderjs/0.10.3/recorder.js"></script>
<script>
const poemLines = Array.from(document.querySelectorAll('.line'));
const presetRhythm = poemLines.map(line => ({
text: line.textContent,
timeRange: line.dataset.time.split('-').map(Number)
}));
let recorder, audioContext, audioBlob, audioStream;
const recordBtn = document.getElementById('recordBtn');
const playBtn = document.getElementById('playBtn');
const analyzeBtn = document.getElementById('analyzeBtn');
const waveformCanvas = document.getElementById('waveform');
const scoreDisplay = document.getElementById('scoreDisplay');
const feedback = document.getElementById('feedback');
let isRecording = false;
// 初始化录音
async function initRecorder() {
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
const input = audioContext.createMediaStreamSource(audioStream);
recorder = new Recorder(input);
recordBtn.disabled = false;
} catch (err) {
console.error('录音权限申请失败:', err);
alert('请允许麦克风权限以使用录音功能');
}
}
// 开始/停止录音
recordBtn.addEventListener('click', () => {
if (!isRecording) {
// 开始录音
recorder.clear();
recorder.record();
isRecording = true;
recordBtn.textContent = '停止录音';
playBtn.disabled = true;
analyzeBtn.disabled = true;
} else {
// 停止录音
recorder.stop();
isRecording = false;
recordBtn.textContent = '开始录音';
// 保存录音数据
recorder.exportWAV(blob => {
audioBlob = blob;
playBtn.disabled = false;
analyzeBtn.disabled = false;
// 绘制波形图
drawWaveform(blob);
});
}
});
// 播放音频
playBtn.addEventListener('click', () => {
if (!audioBlob) return;
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
audio.play();
});
// 分析节奏
analyzeBtn.addEventListener('click', async () => {
if (!audioBlob) return alert('请先录制音频');
// 实际应用中应调用真实的语音识别API
const userRhythm = await analyzeAudio(audioBlob);
const accuracy = calculateAccuracy(presetRhythm, userRhythm);
// 显示评分
scoreDisplay.textContent = `节奏准确率:${accuracy.toFixed(2)}%`;
feedback.textContent = accuracy > 85 ? '节奏准确!' : '需加强重音与停顿练习';
// 高亮显示朗读效果
highlightLines(userRhythm);
});
// 分析音频(模拟)
async function analyzeAudio(blob) {
// 实际应用中应替换为真实的语音识别API
return new Promise(resolve => {
setTimeout(() => {
// 模拟识别结果,这里使用预设节奏作为参考
const userTime = presetRhythm.map((line, i) => {
const duration = line.timeRange[1] - line.timeRange[0];
// 添加一些随机波动模拟真实情况
const offset = (Math.random() - 0.5) * 0.5;
return {
start: line.timeRange[0] + offset,
end: line.timeRange[0] + duration + offset
};
});
resolve(userTime);
}, 1000);
});
}
// 计算准确率
function calculateAccuracy(preset, user) {
const total = preset.length;
let match = 0;
for (let i = 0; i < total; i++) {
const p = preset[i];
const u = user[i];
if (u &&
u.start >= p.timeRange[0] - 0.3 &&
u.end <= p.timeRange[1] + 0.3) {
match++;
}
}
return (match / total) * 100;
}
// 高亮显示朗读效果
function highlightLines(userRhythm) {
poemLines.forEach((line, i) => {
const lineData = presetRhythm[i];
const userData = userRhythm[i];
if (!userData) return;
const isMatch =
userData.start >= lineData.timeRange[0] - 0.3 &&
userData.end <= lineData.timeRange[1] + 0.3;
if (isMatch) {
line.style.backgroundColor = '#d4edda'; // 绿色背景表示节奏正确
} else {
line.style.backgroundColor = '#f8d7da'; // 红色背景表示节奏错误
}
// 3秒后重置颜色
setTimeout(() => {
line.style.backgroundColor = '';
}, 3000);
});
}
// 绘制波形图
function drawWaveform(blob) {
const ctx = waveformCanvas.getContext('2d');
ctx.clearRect(0, 0, waveformCanvas.width, waveformCanvas.height);
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const reader = new FileReader();
reader.onload = function(e) {
audioContext.decodeAudioData(e.target.result, function(buffer) {
const data = buffer.getChannelData(0);
const step = Math.ceil(data.length / 1000);
const waveform = [];
for (let i = 0; i < 1000; i++) {
let min = 1.0;
let max = -1.0;
for (let j = 0; j < step; j++) {
const value = data[i * step + j];
if (value < min) min = value;
if (value > max) max = value;
}
waveform.push({ min, max });
}
// 绘制波形
ctx.beginPath();
ctx.strokeStyle = '#007bff';
ctx.lineWidth = 2;
const width = waveformCanvas.width;
const height = waveformCanvas.height;
for (let i = 0; i < waveform.length; i++) {
const x = (i / waveform.length) * width;
const minY = height / 2 + waveform[i].min * height / 2;
const maxY = height / 2 + waveform[i].max * height / 2;
if (i === 0) {
ctx.moveTo(x, minY);
} else {
ctx.lineTo(x, minY);
}
ctx.lineTo(x, maxY);
}
ctx.stroke();
});
};
reader.readAsArrayBuffer(blob);
}
// 初始化
initRecorder();
</script>
</body>
</html>
index.html
index.html