亚伦影音工作室 发表于 2025-8-14 11:18

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>

杨帆 发表于 2025-8-14 11:49

亚伦老师您辛苦了!谢谢精彩分享{:4_191:}

红影 发表于 2025-8-14 19:37

这个好,有逐字的歌词同步,还有进度条。
感谢亚伦老师带来的好东西{:4_187:}

红影 发表于 2025-8-14 19:38

还能调音高,这播放器的功能很齐全{:4_187:}
好东西,收藏了。
页: [1]
查看完整版本: KSC/LRC歌词同步播放器