KSC/LRC歌词同步播放器
本帖最后由 亚伦影音工作室 于 2025-10-10 22:45 编辑 <br /><br /><style>#bofangqiquanju{left: 15%; top:0px;width:85%;position:relative;height: 300px; overflow: hidden; align-items: center;display: flex;align-items: center; gap: 10px;justify-content: space-between; }
#bnt{position:absolute;border: 0px solid #ff3300;width:40px;overflow: hidden;height: 40px;border-radius: 0%;cursor: pointer;a}
#pic{position:absolute;top:25px; left:28px;background:#000;
transform: translate(-50%, -50%);
clip-path: polygon(0% 0%, 0% 100%, 25% 100%, 25% 0, 50% 0, 50% 100%, 75% 100%, 75% 0);
width:20px;
height: 20px;}
#picc{opacity:0;position:absolute;top:25px; left:30px;background:#000; transform: translate(-50%, -50%);
clip-path: polygon(75% 50%, 0 0, 0 100%);
width:20px;
height: 20px}
.progress {width: 100%;display: flex;align-items: center; gap: 10px;justify-content: space-between;position:relative;left: 45px; top:5px; }
.progress-bar {
position:relative;
width: 100%;
height: 4px;
background-color: #000;
left: 0%;border-radius: 20px;
cursor: pointer;
}
.now {
position: absolute;
left: 0%;
display: inline-block;
height: 4px;border-radius: 20px;
width: 45%;
background: #009900;
}
.now::after {
content: '';
position: absolute;
left: 100%;
width: 10px;margin: -3px -3px;
height: 10px;border-radius: 50px;
background-color: #009900;
}
.start{color: #000; font: 400 14px sans-serif;
}
.end{color: #000; font: 400 14px sans-serif;
}
#btnMute{
position:relative;
width: 30px;
height: 30px;
z-index: 1;
right: 0px;
cursor: pointer;
}
#ptxt{ position:relative;font-size:14px;right: 80px;top:0px;width: 100px;color: #000;}
#volwrap { position:relative; width: 100px; height: 80px;place-items: center;border-radius: 20px;transform:rotate(-90deg);right: 20px;bottom: 42px;background: none;}
#volume {
-webkit-appearance: none;
appearance: none;
margin: 0;
outline: 0;
background-color: transparent;
width: 100%;
}
#volume::-webkit-slider-runnable-track {
height: 3px;border-radius: 20px;
background: #333;
}
#volume::-webkit-slider-container {
height: 18px;border-radius: 30px;
overflow: hidden;
}
#volume::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background:#f44336;
border: 1px solid transparent;
margin-top: -4px;
border-image: linear-gradient(#f44336,#f44336) 0 fill / 5 11 5 0 / 0px 0px 0 2000px;
}
#volwrap:hover #volume { opacity:1;}
#btnMute:hover ~ #volwrap > #volume {opacity:1; }
#volume { position: absolute; width: 100px; height: 2px;opacity:0;}
#lyr{width: 100%;
top: 10px;
left: 0%;
height: 100px;
text-align: center;
position: relative;
}
.lyrics{margin: 0px;
top: 0px;
left: 50%;
transform: translate(-50%, -50%);
height: 100px; /* 调整高度,只容纳当前歌词 */
text-align: center;
position:absolute;
}
.lyric-line{
width: 100%;
position: relative;
height: 60px;
overflow: visible;
font: 300 50px '华文隶书', sans-serif;
line-height: 60px;
text-align: left;
white-space: nowrap; /* 禁止换行 */
filter: drop-shadow(#fff 1px 0 0) drop-shadow(#fff 0 1px 0) drop-shadow(#fff -1px 0 0) drop-shadow(#fff 0 -1px 0);
}
.lyric-mask {
position: absolute;
top: 0;
left: 0;
width: 0;
overflow: hidden;
color: #8B4513;
height: 100%;
white-space: nowrap;
}
.lyric-original {
color: #ag0000;
white-space: nowrap;
}
</style>
<audio id="audio"autoplay loop>
<source src="https://s2.cldisk.com/sv-w7/audio/89/47/53/d8008dabc6b693af9167731e756409ba/audio.mp3" type="audio/mpeg">
</audio>
<div id="bofangqiquanju">
<div id="bnt">
<div id="pic"></div>
<div id="picc"></div>
</div>
<div class="progress">
<span class="start">00:00</span>
<div class="progress-bar">
<div class="now"></div>
</div>
<span class="end">00:00</span>
<img id="btnMute" onClick="pc()"src="https://638183.freep.cn/638183/web/icon/unmuted.svg" title="静音" alt="" />
<div id="volwrap"><input id="volume" type="range" min="0" max="1" step="0.1" value="0.8" /></div>
、<div id="ptxt">0%</div>
</div>
</div>
<div id="lyr" >
<div class="lyrics" >
<div class="lyric-line">
<div class="lyric-mask"></div>
<div class="lyric-original"></div>
</div>
</div>
</div>
<script>
bnt.onclick = () => audio.paused ? (audio.play(),picc.style.opacity= '0',pic.style.opacity = '1') : (audio.pause(),picc.style.opacity = '1',pic.style.opacity = '0');
const audio = document.getElementById('audio')
const start = document.querySelector('.start')
const end = document.querySelector('.end')
const progressBar = document.querySelector('.progress-bar')
const now = document.querySelector('.now')
function conversion (value) {
let minute = Math.floor(value / 60)
minute = minute.toString().length === 1 ? ('0' + minute) : minute
let second = Math.round(value % 60)
second = second.toString().length === 1 ? ('0' + second) : second
return `${minute}:${second}`
}
audio.onloadedmetadata = function () {
end.innerHTML = conversion(audio.duration)
start.innerHTML = conversion(audio.currentTime)
}
audio.addEventListener("timeupdate",function(){
var percent= audio.currentTime / audio.duration
var sp = 600 / 100 ;
var swidth =(percent * 100 * sp) + "px";
console.log(percent*100,swidth)
//保留2位小数
document.getElementById("ptxt").innerText = ((percent*100).toFixed(2))+"%"
})
progressBar.addEventListener('click', function (event) {
let coordStart = this.getBoundingClientRect().left
let coordEnd = event.pageX
let p = (coordEnd - coordStart) / this.offsetWidth
now.style.width = p.toFixed(3) * 100 + '%'
audio.currentTime = p * audio.duration
audio.play()
})
setInterval(() => {
start.innerHTML = conversion(audio.currentTime)
now.style.width = audio.currentTime / audio.duration.toFixed(3) * 100 + '%'
}, 1000)
var volume = document.getElementById('volume');
volume.addEventListener('input', function() {audio.volume =volume.value;
});
volume.onchange = () => audio.volume = lastVolume = volume.value;
function pc() {
var img = document.getElementById("btnMute");
if (img.getAttribute("src", 2) == "https://638183.freep.cn/638183/web/icon/unmuted.svg") {audio.muted= true;
img.src = "https://638183.freep.cn/638183/web/icon/muted.svg"; volume.value=0;
} else {audio.muted= false;volume.value=0.8;
img.src = "https://638183.freep.cn/638183/web/icon/unmuted.svg";}
}
</script>
<script>
// 歌词解析ksc歌词或lrc歌词
const lrc = `karaoke.add('00:00.010', '00:01.529', '与你到永久 - 吉他的天空', '117,117,117,117,117,117,117,117,117,117,117,117,117');
karaoke.add('00:02.050', '00:03.788', '词:伍佰', '212,212,212,1112');
karaoke.add('00:06.490', '00:08.270', '曲:伍佰', '445,445,445,445');
karaoke.add('00:16.770', '00:18.408', '风儿轻轻的吹', '373,173,173,173,173,573');
karaoke.add('00:20.110', '00:22.614', '雨也绵绵下个不停', '438,338,138,138,338,138,438,538');
karaoke.add('00:24.120', '00:25.800', '望着走过的脚印', '340,240,140,140,140,340,340');
karaoke.add('00:27.000', '00:29.316', '有崎岖有平静', '536,236,336,336,236,636');
karaoke.add('00:31.320', '00:33.168', '看着你的眼睛', '408,208,308,308,208,408');
karaoke.add('00:34.270', '00:36.720', '我最熟悉的表情', '450,250,250,250,450,350,450');
karaoke.add('00:37.920', '00:39.230', '一路上有你', '262,262,262,262,262');
karaoke.add('00:39.730', '00:41.040', '因为有了你', '262,262,262,262,262');
karaoke.add('00:41.540', '00:45.446', '人生旅程不再冷清', '349,178,226,432,284,31,435,1971');
karaoke.add('00:47.460', '00:50.218', '迎着风', '886,586,1286');
karaoke.add('00:50.620', '00:53.117', '迎向远方的天空', '401,101,301,351,451,411,481');
karaoke.add('00:54.280', '00:55.498', '路上也有艰难', '203,203,203,203,203,203');
karaoke.add('00:56.400', '00:57.910', '也有那解脱', '233,100,705,70,402');
karaoke.add('00:58.310', '01:00.663', '都走得从容', '732,32,48,758,783');
karaoke.add('01:02.470', '01:03.801', '因为你是我', '263,218,418,153,105');
karaoke.add('01:05.200', '01:09.704', '生命中的所有', '574,315,218,218,261,2918');
karaoke.add('01:11.010', '01:13.772', '将我的心放在你手中', '342,342,436,523,159,49,207,83,623');
karaoke.add('01:14.590', '01:17.635', '陪你到永久', '396,182,122,423,1922');
karaoke.add('01:35.030', '01:36.920', '风儿轻轻的吹', '176,146,136,146,446,846');
karaoke.add('01:38.380', '01:41.088', '雨也绵绵下个不停', '126,26,426,226,326,126,426,1026');
karaoke.add('01:42.290', '01:44.070', '望着走过的脚印', '340,140,340,140,140,340,340');
karaoke.add('01:45.130', '01:48.346', '有崎岖有平静', '636,336,336,636,636,636');
karaoke.add('01:49.590', '01:51.560', '看着你的眼睛', '395,195,195,395,395,395');
karaoke.add('01:52.460', '01:54.947', '我最熟悉的表情', '441,441,141,141,441,441,441');
karaoke.add('01:56.060', '01:57.328', '一路上有你', '256,256,256,256,256');
karaoke.add('01:57.830', '01:59.180', '因为有了你', '270,270,270,270,270');
karaoke.add('01:59.680', '02:03.120', '人生旅程不再冷清', '280,280,180,480,480,180,380,1180');
karaoke.add('02:05.620', '02:08.320', '迎着风', '900,900,900');
karaoke.add('02:08.820', '02:11.886', '迎向远方的天空', '438,438,438,438,438,438,438');
karaoke.add('02:12.390', '02:13.986', '路上也有艰难', '266,266,266,266,266,266');
karaoke.add('02:14.490', '02:15.970', '也有那解脱', '296,296,296,296,296');
karaoke.add('02:16.470', '02:18.810', '都走得从容', '328,528,428,328,728');
karaoke.add('02:20.610', '02:22.820', '因为你是我', '442,442,442,442,442');
karaoke.add('02:23.320', '02:28.278', '生命中的所有', '393,193,493,493,193,3193');
karaoke.add('02:29.180', '02:31.758', '将我的心放在你手中', '342,342,342,342,342,342,142,342,42');
karaoke.add('02:32.760', '02:37.720', '陪你到永久', '552,552,552,52,3252');
`;
const lyrics = parseLyrics(lrc);
const lyricMask = document.querySelector('.lyric-mask');
const lyricOriginal = document.querySelector('.lyric-original');
let currentIndex = -1;
let currentLyric = null;
// 解析歌词(支持两种格式)
function parseLyrics(lrcText) {
const lyrics = [];
if (lrcText.includes('karaoke.add')) {
const lineRegex = /karaoke\.add\('([^']+)', '([^']+)', '([^']+)', '([^']+)'\);/g;
let match;
while ((match = lineRegex.exec(lrcText)) !== null) {
const startTime = timeToMs(match);
const endTime = timeToMs(match);
const text = match.replace(/\[|\]/g, '').trim();
const durations = match.split(',').map(Number);
if (text) {
lyrics.push({startTime, endTime, text, durations});
}
}
}
else if (lrcText.includes('[')) {
const lines = lrcText.split('\n').filter(line => line.trim());
lines.forEach((line, index) => {
const timeMatch = line.match(/\[(\d+:\d+\.\d+)\]/);
if (timeMatch) {
const timeStr = timeMatch;
const text = line.replace(/\[.*?\]/, '').trim();
if (text) {
const startTime = timeToMs(timeStr);
const nextLine = lines;
const nextTimeMatch = nextLine ? nextLine.match(/\[(\d+:\d+\.\d+)\]/) : null;
const endTime = nextTimeMatch ? timeToMs(nextTimeMatch) : startTime + 5000;
lyrics.push({
startTime,
endTime,
text,
durations: calculateCharDurations(text, startTime, endTime)
});
}
}
});
}
return lyrics;
}
function calculateCharDurations(text, startTime, endTime) {
const totalDuration = endTime - startTime;
const charCount = text.length;
const baseDur = Math.floor(totalDuration / charCount);
const durations = new Array(charCount).fill(baseDur);
const remainder = totalDuration % charCount;
for (let i = 0; i < remainder; i++) {
durations++;
}
return durations;
}
function timeToMs(timeStr) {
const parts = timeStr.split(':');
const minutes = parseInt(parts, 10);
const secondsAndMs = parts.split('.');
const seconds = parseInt(secondsAndMs, 10);
const ms = parseInt(secondsAndMs || 0, 10);
return minutes * 60 * 1000 + seconds * 1000 + ms;
}
function getCurrentLyricIndex(lyrics, currentTimeMs) {
for (let i = 0; i < lyrics.length; i++) {
if (currentTimeMs >= lyrics.startTime && currentTimeMs <= lyrics.endTime) {
return i;
}
}
return -1;
}
function updateLyricDisplay(index) {
if (index < 0 || index >= lyrics.length) return;
currentIndex = index;
currentLyric = lyrics;
lyricOriginal.textContent = currentLyric.text;
lyricMask.textContent = currentLyric.text;
lyricMask.style.width = '0%';
}
function updateLyricMask(currentTimeMs) {
if (!currentLyric) return;
const lyricStartTime = currentLyric.startTime;
const elapsed = currentTimeMs - lyricStartTime;
const totalDuration = currentLyric.durations.reduce((sum, d) => sum + d, 0);
let charIndex = 0;
let accumulatedTime = 0;
for (let i = 0; i < currentLyric.durations.length; i++) {
accumulatedTime += currentLyric.durations;
if (elapsed <= accumulatedTime) {
charIndex = i + 1;
break;
}
}
if (elapsed >= totalDuration) {
charIndex = currentLyric.text.length;
}
charIndex = Math.min(charIndex, currentLyric.text.length);
const tempSpan = document.createElement('span');
tempSpan.style.visibility = 'hidden';
tempSpan.style.position = 'absolute';
tempSpan.style.fontSize = '50px';
tempSpan.style.fontWeight = '800';
document.body.appendChild(tempSpan);
const visibleText = currentLyric.text.substring(0, charIndex);
tempSpan.textContent = visibleText;
const width = tempSpan.offsetWidth;
document.body.removeChild(tempSpan);
lyricMask.style.width = `${width}px`;
}
// 监听更新歌词
audio.addEventListener('timeupdate', () => {
const currentTimeMs = audio.currentTime * 1000;
const index = getCurrentLyricIndex(lyrics, currentTimeMs);
if (index !== currentIndex) {
updateLyricDisplay(index);
}
updateLyricMask(currentTimeMs);
});
updateLyricDisplay(0);
</script>
亚伦老师您辛苦了!谢谢精彩分享{:4_191:} 这个好,有逐字的歌词同步,还有进度条。
感谢亚伦老师带来的好东西{:4_187:} 还能调音高,这播放器的功能很齐全{:4_187:}
好东西,收藏了。
页:
[1]