共计 4872 个字符,预计需要花费 13 分钟才能阅读完成。
前言
最近电视剧《点燃我,和煦你》中的一个桥段,李峋写的爱心代码火了 ❤️🔥❤️🔥❤️🔥,相似的表白神器每年都会在特定的工夫(这些日期你都晓得吗)出圈几个。
当各路大佬开始晒各种语言各种类型的爱心代码,我一个顶级 CV 工程师,在这塌实的环境下天然也想着夸耀一番,于是在这片汪洋大海中通过我的层层筛选,这款爱心代码被我挑中并对他进行了小小的革新。
插件地址
对于插件:
这款插件的作者从代码来看曾经无从讲究了,感激大佬栽树。
作为前端实现爱心代码的酷炫成果,尤其是 3d 成果应用的技术栈根本都是以 three.js
+canvas
为主
这款插件也是应用 three.js + canvas 开发,具体的开发过程不做过多的剖析,cv 工程师不太会 three.js,见笑了😂
重点认识一下
Web Audio API:
Web Audio API 为管制 Web 上的音频提供了一个弱小且通用的零碎,容许开发人员抉择音频源、为音频增加成果、创立音频可视化、利用空间成果(如平移)等等。
声音远近,大小,触动等等这些成果的可视化都须要 Web Audio API 的帮忙
接口:这里简略列举了几个插件中用到的接口,更多接口可返回 MDN 查看
AudioListener
AudioListener 用一个虚构的 listener 示意在场景中所有的地位和非地位相干的音效.
一个 three.js 程序通常创立一个 AudioListener. 它是音频实体构造函数的必须参数,比方 Audio and PositionalAudio.
大多数状况下, listener 对象是 camera 的子对象. Camera 的 3D 变换示意了 listener 的 3D 变换.AudioLoader
用来加载 AudioBuffer 的一个类。
外部默认应用 FileLoader> > 来加载文件。(我的项目中用来加载音频文件)AudioAnalyser
创立 AudioAnalyser 对象, 应用 AnalyserNode 去剖析音频数据.
开始
插件动画成果曾经完满了,合乎装🤫要求。
去掉边框更显大气;
bgm 像铃声换成我周董的更能俘获人心;
周董咬字不清加个歌词显示更加人性化(开玩笑的啦,喜爱的就是这个味)。
动画追随旋律的成果插件曾经有了,配上歌词那不就是一个现成的播放器吗
思路一:
实现辨认音频显示歌词,相似视频播放显示字幕的成果。
一通百度失去的后果次要分两种
- 上传语音到服务端,服务端利用解析语音的 API,返回文字;
- 利用原生 api 或各种库调取浏览器麦克风,辨认语音转文字
办法一在我这个捣鼓着玩的我的项目里显著不实用,办法二延时太久,辨认精确度太低
思路二:√
获取歌词,实现歌词与音频的同步,这种计划是可行的
实现
获取歌词
网上找到 LRC 格局的歌词,肯定要是LRC 格局,放到一个文本域中,设置为暗藏
<textarea id="lrc_content" cols="30" style="display: none">
[ti: 花海]
[ar: 周杰伦]
[00:03.63]花海
[00:06.35]周杰伦
[00:14.49]...
[00:18.56]...
[00:26.90]静止了 所有的花开
[00:33.31]边远了 清晰了爱
[00:39.53]情侣们 爱却更喜爱
[00:45.99]那时候 我不懂 这场爱
[00:51.55]
[00:52.50]你喜爱 站在那窗台
[00:58.47]你良久 都没再来
[01:05.12]黑白的 世界染上空白
[01:12.18]是你流的泪晕开
[01:16.14]
[01:17.26]不要你来到 间隔隔不开
[01:23.65]怀念变成海 在窗外进不来
[01:29.82]原谅说太快 爱成了妨碍
[01:36.24]手中的风筝放太快回不来
[01:42.22]
[01:42.74]不要你来到 泪已化不开
[01:48.96]经验的妨碍 我在期待重来
[01:55.51]天空仍璀璨 她爱着大海
[02:01.79]情歌被战胜 爱已不存在
[02:09.83]
[02:34.80]你喜爱 站在那窗台
[02:41.14]你良久 都没再来
[02:47.40]黑白的 世界染上空白
[02:54.66]是你流的泪晕开
[02:59.87]
[03:00.24]不要你来到 间隔隔不开
[03:05.75]怀念变成海 在窗外进不来
[03:12.16]原谅说太快 爱成了妨碍
[03:18.56]手中的风筝放太快回不来
[03:24.53]
[03:25.07]不要你来到 泪已化不开
[03:31.37]经验的妨碍 我在期待重来
[03:37.81]天空仍璀璨 她爱着大海
[03:44.12]情歌被战胜 爱已不存在
[03:54.85]</textarea>
js 读取歌词
var lrc = document.getElementById("lrc_content").innerHTML;
将 LRC 歌词解析为 JS 对象
var oLRC = {
ti: "", // 歌曲名
ar: "", // 演唱者
al: "", // 专辑名
by: "", // 歌词制作人
offset: 0, // 工夫弥补值,单位毫秒,用于调整歌词整体地位
ms: [] // 歌词数组{t: 工夫,c: 歌词}
};
function createLrcObj(lrc) {if(lrc.length==0) return;
var lrcs = lrc.split('\n');// 用回车拆分成数组
for(var i in lrcs) {// 遍历歌词数组
lrcs[i] = lrcs[i].replace(/(^\s*)|(\s*$)/g, ""); // 去除前后空格
var t = lrcs[i].substring(lrcs[i].indexOf("[") + 1, lrcs[i].indexOf("]"));// 取 [] 间的内容
var s = t.split(":");// 拆散: 前后文字
if(isNaN(parseInt(s[0]))) { // 不是数值
for (var i in oLRC) {if (i != "ms" && i == s[0].toLowerCase()) {oLRC[i] = s[1];
}
}
}else { // 是数值
var arr = lrcs[i].match(/[(\d+:.+?)]/g);// 提取工夫字段,可能有多个
var start = 0;
for(var k in arr){start += arr[k].length; // 计算歌词地位
}
var content = lrcs[i].substring(start);// 获取歌词内容
for (var k in arr){var t = arr[k].substring(1, arr[k].length-1);// 取 [] 间的内容
var s = t.split(":");// 拆散: 前后文字
oLRC.ms.push({// 对象 {t: 工夫,c: 歌词} 退出 ms 数组
t: (parseFloat(s[0])*60+parseFloat(s[1])).toFixed(3),
c: content
});
}
}
}
oLRC.ms.sort(function (a, b) {// 按工夫程序排序
return a.t-b.t;
});
}
createLrcObj(lrc);
将解析后的歌词出现在页面上
创立一个容器展现歌词
<ul id="lyric">
<!-- <li> 花海 </li>-->
</ul>
用 JS 把歌词写到标签外面
注:如果歌词显示成果是全副展现,高亮以后歌词须要这一步操作;如果只显示以后歌词不展现其余歌词,可省略这一步
function showLRC() {
var s="";
for(var i in oLRC.ms){// 遍历 ms 数组,把歌词退出列表
s+='<li>'+oLRC.ms[i].c+'</li>';
}
document.getElementById("lyric").innerHTML = s;
console.log(s)
}
showLRC();
JS 实现歌词与播放音乐同步
歌词滚动成果
CSS
.lyric_area{/* 歌词显示区域 */
height: 300px; /* 歌词区域高度 */
overflow: hidden; /* 暗藏超出局部 */
margin-top: 15px;
}
#lyric{/* 歌词列表 */
line-height: 20px;/* 行高,这个值要用在歌词滚动间隔上 */
transition-duration: 600ms;/* 滚动速度 */
}
#lyric .lineHigh{/* 高亮行 */
color: red;
}
JS
var lineNo = 0; // 以后行
var C_pos = 6; // C 位
var offset = -20; // 滚动间隔(应等于行高)var audio = document.getElementById("audio");// 播放器
var ul = document.getElementById("lyric"); // 歌词容器列表
// 高亮显示歌词以后行及文字滚动管制,行号为 lineNo
function lineHigh() {var lis = ul.getElementsByTagName("li");// 歌词数组
if(lineNo>0){lis[lineNo-1].removeAttribute("class");// 去掉上一行的高亮款式
}
lis[lineNo].className = "lineHigh";// 高亮显示以后行
// 文字滚动
if(lineNo>C_pos){ul.style.transform = "translateY("+(lineNo-C_pos)*offset+"px)"; // 整体向上滚动一行高度
}
}
// 滚回到结尾,用于播放完结时
function goback() {document.querySelector("#lyric .lineHigh").removeAttribute("class");
ul.style.transform = "translateY(0)";
lineNo = 0;
}
// 监听播放器的 timeupdate 事件,实现文字与音频播放同步
audio.ontimeupdate = function () {if(lineNo==oLRC.ms.length)
return;
var curTime = audio.currentTime; // 播放器工夫
if(parseFloat(oLRC.ms[lineNo].t)<=curTime){lineHigh();// 高亮以后行
lineNo++;
}
};
// 监听播放器的 ended 事件,播放完结时回滚歌词
audio.onended = function () {goback(); // 回滚歌词
};
单句歌词切换成果
var lineNo = 0; // 以后行
var audio = document.getElementById("auo");// 播放器
// 滚回到结尾,用于播放完结时
function goback() {// document.getElementById('lyric').innerText = '花海'
lineNo = 0;
}
// 监听播放器的 timeupdate 事件,实现文字与音频播放同步
audio.ontimeupdate = function () {if(lineNo==oLRC.ms.length)
return;
var curTime = audio.currentTime; // 播放器工夫
if(parseFloat(oLRC.ms[lineNo].t)<=curTime){
// 歌词切换成果
document.getElementById('lyric').innerText = oLRC.ms[lineNo].c
lineNo++;
}
};
// 监听播放器的 ended 事件,播放完结时回滚歌词
audio.onended = function () {goback(); // 回滚歌词
};
问题
插件自身用 web audio api 加载的音频,而歌词追随音频显示是读取的 audio 标签,这里两者有抵触,利用 web audio api 中的属性是能够实现文字与音频的播放同步的,这里我没有学习并进行对立。
而是利用 audio 标签加载,防止抵触点击播放之后将 audio 绑定的音频音量静音
有须要代码的大家可自行下载预览(我在这里)