马黑黑 发表于 2024-2-4 09:00

range进度条播放器+LRC歌词同步教程(四)

本帖最后由 马黑黑 于 2024-2-4 09:02 编辑 <br /><br /><style>
.mum { font-size: 18px; }
.mum pre { padding: 12px; background: #eee; color: navy; font: normal 16px/20px Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; tab-size: 4; white-space: pre-wrap; word-wrap: break-word; }
.mum a { color: red; }
.mum a:hover { color: darkred; }
.tGreen { color: green; }
.tRed { color: red; }
</style>

<div class="mum">

<p>在第三讲,<a href="https://www.huachaowang.com/forum.php?mod=viewthread&tid=74000&extra=page%3D1" target="_blank">《range进度条播放器+LRC歌词同步教程(三)》</a>,range播放器和歌词同步已经集成在一起,可以正常工作,只是处理 lrcKey 变量值的 calcKey() 函数尚未完善,本节专门处理这个问题。</p>
<p>全局变量 lrcKey 是处理播放哪一句歌词的关键索引,它是音频控件 timeupdate 监听事件触发歌词显示与模拟同步的依据,同时,当用户手动调整播放进度或循环播放机制下的重新播放,都得重新确立其值的变量,所以 calcKey() 函数必须准确运算出这个 lrcKey 变量值。看如下已经编写好的函数代码,随后我们再慢慢地去理解:</p>
<pre>
<span class="tGreen">/*
        计算 lrcKey 变量值的函数 :seeked 监听事件调用;手动调整播放位置或
        重新播放产生的播放位置变化,lrcKey 应跟随变化,需要精准计算
*/</span>

var calcKey = () => {
        for (var j = 0; j &gt; geci.length; j++) {
                if (aud.currentTime &lt;= geci) {
                        lrcKey = j - 1;
                        break;
                }
        }
        if (lrcKey < 0) lrcKey = 0;
        if (lrcKey > geci.length - 1) lrcKey = geci.length - 1;
        let time = geci - (aud.currentTime - geci);
        showLrc(time);
        pa.style.setProperty('--state', aud.paused ? 'paused' : 'running');
};
</pre>
<p>for循环语句:从头到尾循环 geci 歌词数组,用一个if条件语句比对当下的用户已经改变了的播放位置和歌词数组记录的起唱时间,如果播放器的播放位置时间小于等于歌词的起唱时间,则获得 lrcKey 的值为第 j - 1 句,然后退出for循环(break)。为什么是第 j - 1 句呢?这与我们的播放器工作机制以及这里在循环语句中所设定的条件有关,让我们看下面的演示分析:</p>
<pre>
<span class="tRed">2.0</span>   <span class="tGreen">8.5</span>   35.2   49.6   ...   (歌词起唱时间)
    <span class="tRed">|</span>                           (拖曳进度条到)   
<span class="tRed">0</span>   <span class="tGreen">1</span>   2      3            (自增变量 j 的值)

<span class="tGreen">/* 用户拖曳进度条滑块到第一句歌词和第二句歌词之间,这时,根据for循环语句内设定的比对条件,8.5这个起唱时间符合条件,但它指向第二句歌词,而用户调节的进度应在第一句歌词的时间区间,故j要减去1才符合用户调节进度的意愿,即 j-1 才是对应需要的歌词的数组下标。这里,8.5是作为自左往右的判断边界,预期的时间区间在它的前边。 */</span>
</pre>
<p>如此,如果 j 在 0 的时候条件就成立,就会存在 lrcKey 小于 0 的情况,故后面又用一个if语句进行判断,令 lrcKey 等于 0,第一个if除了处理手动调节到第一句歌词时间区间的情况,也刚好可以应对重新播放的情形;手动调节进度调到最后一句歌词时间区间,lrcKey 会大于歌词总数减去1(j从0开始),再用一个if语句做判断,若此,则令其等于歌词总数减1。这个可能不是很好理解,慢慢体会吧。</p>
<p>计算好 lrcKey 之后,再计算正在播放的歌词的剩余时间作为传参,然后调用显示lrc歌词函数 showLrc(time),同时根据播放器暂停或播放状态设置 CSS变量 --state 为停止还是运行。剩余时间的计算,是那当下歌词的唱时减去播放位置时间和上一句歌词的差。</p>
<p>calcKey() 函数应应用在播放控件的 onseeked 事件里,换言之,当用户手动调整了播放进度或循环播放机制下的重新播放,都应运行 calcKey() 函数,以便保证歌词同步模拟机制的正常运行。</p>
<p>最后,整理完整代码如下:</p>
<pre>
&lt;style&gt;
#papa {
        margin: auto;
        width: 800px;
        height: 360px;
        background: linear-gradient(tan,gray);
        box-shadow: 3px 3px 20px #000;
        position: relative;
        display: grid;
        place-items: center;
}
#lrc {
        position: absolute;
        top: 10px;
        font: bold 2.4em sans-serif;
        color: lightblue;
        text-shadow: 1px 1px 1px rgba(0,0,0,.45);
        --ani: lrcGo1;
        --duration: 1s;
}
#lrc::before {
        position: absolute;
        content: attr(data-lrc);
        width: 100%;
        height: 100%;
        color: transparent;
        background: linear-gradient(rgba(250,0,0,.7),rgba(0,0,180,.8));
        background-clip: text;
        -webkit-background-clip: text;
        clip-path: inset(0 100% 0 0);
        animation: var(--ani) var(--duration) linear forwards var(--state);
        border-bottom: 1px solid navy;
}
#mplayer {
        position: absolute;
        bottom: 10px;
        text-align: center;
}
#mplayer::before {
        position: absolute;
        content: attr(data-tt);
        left: 0;
        bottom: 25px;
        width: 100%;
        text-align-last: justify;
}
#mprog {
        width: 240px;
        accent-color: darkgreen;
        outline: none;
        cursor: pointer;
}
#btnplay {
        width: 80px;
        height: 80px;
        cursor: pointer;
        animation: rotating 6s infinite linear var(--state);
}
@keyframes rotating { to { transform: rotate(360deg); } }
@keyframes lrcGo0 { to { clip-path: inset(0 0 0 0); } }
@keyframes lrcGo1 { to { clip-path: inset(0 0 0 0); } }
&lt;/style&gt;

&lt;div id="papa"&gt;
        &lt;audio id="aud" src="https://music.163.com/song/media/outer/url?id=212524" autoplay loop&gt;&lt;/audio&gt;
        &lt;div id="mplayer" data-tt="0:00 0:00"&gt;
                &lt;img id="btnplay" src="https://638183.freep.cn/638183/small/002_133507167677724892.png" title="播放/暂停" alt="" /&gt;&lt;br&gt;
                &lt;input id="mprog" type="range" min="0" max="100" step="any" value="0" title="调节进度" /&gt;
        &lt;/div&gt;
        &lt;div id="lrc" data-lrc="HuaChao LRC"&gt;HuaChao LRC&lt;/div&gt;
&lt;/div&gt;

&lt;script&gt;

var mseek = false, aniIdx = 0, lrcKey = 0;

var toMin = (val) =&gt; {
        if(!val) return '0:00';
        var min = parseInt(val / 60), sec = Math.floor(val) % 60;
        if(sec &lt; 10) sec = '0' + sec;
        return min + ':' + sec;
};

var mState = () =&gt; aud.paused ?
        (papa.style.setProperty('--state', 'paused'), btnplay.title = '点击播放') :
        (papa.style.setProperty('--state', 'running'), btnplay.title = '点击暂停');

var showLrc = (time) =&gt; {
        lrc.textContent = lrc.dataset.lrc = geci.replace(/&lt;br&gt;/, '\n');
        lrc.style.setProperty('--ani', ['lrcGo0','lrcGo1']);
        lrc.style.setProperty('--duration', time + 's');
        pa.style.setProperty('--state', 'running');
        aniIdx = aniIdx === 0 ? 1 : 0;
        lrcKey ++;
};

var calcKey = () =&gt; {
        for (var j = 0; j &lt; geci.length; j++) {
                if (aud.currentTime &lt;= geci) {
                        lrcKey = j - 1;
                        break;
                }
        }
        if (lrcKey &lt; 0) lrcKey = 0;
        if (lrcKey &gt; geci.length - 1) lrcKey = geci.length - 1;
        let time = geci - (aud.currentTime - geci);
        showLrc(time);
        pa.style.setProperty('--state', aud.paused ? 'paused' : 'running');
};

aud.addEventListener('pause', () =&gt; mState());
aud.addEventListener('playing', () =&gt; mState());
aud.addEventListener('seeked', () =&gt; calcKey());

aud.addEventListener('timeupdate', () =&gt; {
        if (!mseek) mprog.value = aud.currentTime / aud.duration * mprog.max;
        mplayer.dataset.tt = toMin(aud.currentTime) + ' ' + toMin(aud.duration);
        for(var j = 0; j &lt; geci.length; j ++) {
                if (aud.currentTime &gt;= geci) {
                        if (j === lrcKey ) showLrc(geci);
                }
        }
});

mprog.onmousedown = () =&gt; mseek = true;
mprog.onmouseup = () =&gt; mseek = false;
mprog.onchange = () =&gt; aud.currentTime = aud.currentTime = mprog.value / mprog.max * aud.duration;

btnplay.onclick = () =&gt; aud.paused ? aud.play() : aud.pause();

var geci = [ , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ];

&lt;/script&gt;
</pre>
<p>除了歌词,上述代码尽可能分行写了,这样更便于阅读。实际投入使用时,建议将CSS压缩(一个选择器一行)、将不再更改的JS函数压缩(每个函数一行)。</p>
<p>下一讲将是本教程的完结篇,内容的难度要比这一讲和上一讲轻松:可能是一些细小的整合优化之类的内容,还有对键盘调节进度的响应处理。</p>

</div>

红影 发表于 2024-2-4 10:56

黑黑一点点地将歌词同步的设置讲解得这么清楚,太赞了{:4_199:}

红影 发表于 2024-2-4 11:05

运用这些代码,可以保证歌词和音乐同步,还能响应手动调节。“还有对键盘调节进度的响应处理。”这个也可以啊{:4_173:}

红影 发表于 2024-2-4 11:06

分行写的好,看得更清楚。一直喜欢分行的,虽然合起来更不占地方{:4_173:}

马黑黑 发表于 2024-2-4 11:21

红影 发表于 2024-2-4 11:06
分行写的好,看得更清楚。一直喜欢分行的,虽然合起来更不占地方

写的时候分行,要发布了,可以压缩

马黑黑 发表于 2024-2-4 11:21

红影 发表于 2024-2-4 10:56
黑黑一点点地将歌词同步的设置讲解得这么清楚,太赞了

差不多了,明天发终结篇

马黑黑 发表于 2024-2-4 11:22

红影 发表于 2024-2-4 11:05
运用这些代码,可以保证歌词和音乐同步,还能响应手动调节。“还有对键盘调节进度的响应处理。”这个也可以 ...

JS本身就具备基于键盘键位的编程能力,这个必须得有

红影 发表于 2024-2-4 20:28

马黑黑 发表于 2024-2-4 11:21
写的时候分行,要发布了,可以压缩

是的,这样至少自己是明白的{:4_173:}

红影 发表于 2024-2-4 20:29

马黑黑 发表于 2024-2-4 11:21
差不多了,明天发终结篇

这个系列讲座太好,黑黑辛苦了{:4_190:}{:4_184:}

红影 发表于 2024-2-4 20:29

马黑黑 发表于 2024-2-4 11:22
JS本身就具备基于键盘键位的编程能力,这个必须得有

这个还从来没想到过呢{:4_173:}

马黑黑 发表于 2024-2-4 21:35

红影 发表于 2024-2-4 20:29
这个还从来没想到过呢
不过由于标准正在重设中,我刚才在写最后一讲的时候才发现,过去的知识体系跟不上变化了。我得调整一下。

马黑黑 发表于 2024-2-4 21:36

红影 发表于 2024-2-4 20:29
这个系列讲座太好,黑黑辛苦了

我的期望,至少吧,能让大家懂得原理,懂原理之后,对我的设计能够有能力找到出错的地方。

马黑黑 发表于 2024-2-4 21:36

红影 发表于 2024-2-4 20:28
是的,这样至少自己是明白的
其实只要代码量不太大,都分行问题也不大

红影 发表于 2024-2-4 22:24

马黑黑 发表于 2024-2-4 21:35
不过由于标准正在重设中,我刚才在写最后一讲的时候才发现,过去的知识体系跟不上变化了。我得调整一下。

对这个标准完全不知道,只能跟在黑黑后面学习了{:4_187:}

红影 发表于 2024-2-4 22:26

马黑黑 发表于 2024-2-4 21:36
我的期望,至少吧,能让大家懂得原理,懂原理之后,对我的设计能够有能力找到出错的地方。

学习原理是必须的,你的设计很好,很好出错啊,你那么仔细的人{:4_187:}

红影 发表于 2024-2-4 22:26

马黑黑 发表于 2024-2-4 21:36
其实只要代码量不太大,都分行问题也不大

所以,最好是分行的。当然歌词除外,那个不用分行{:4_173:}

马黑黑 发表于 2024-2-4 23:21

红影 发表于 2024-2-4 22:26
所以,最好是分行的。当然歌词除外,那个不用分行

开始时还是要分的,便于修改

马黑黑 发表于 2024-2-4 23:22

红影 发表于 2024-2-4 22:26
学习原理是必须的,你的设计很好,很好出错啊,你那么仔细的人

出错在所难免,还没听说有谁能写出代码不会出错的应用

红影 发表于 2024-2-5 11:08

马黑黑 发表于 2024-2-4 23:21
开始时还是要分的,便于修改

嗯嗯,比如歌词需要加入演唱者等等的信息。

红影 发表于 2024-2-5 11:09

马黑黑 发表于 2024-2-4 23:22
出错在所难免,还没听说有谁能写出代码不会出错的应用

反正黑黑在花潮人的心里就是代码神人{:4_178:}
页: [1] 2
查看完整版本: range进度条播放器+LRC歌词同步教程(四)