vue实现歌词同步
实现歌词同步的基本思路
歌词同步的核心是将歌词文本与音频播放时间轴匹配,动态高亮当前播放的歌词行。通常需要解析歌词文件(如LRC格式),监听音频时间变化,并计算当前应显示的歌词行。
解析歌词文件
LRC格式的歌词包含时间标签和文本内容,例如:
[00:00.00]歌曲名
[00:01.50]第一行歌词
[00:04.20]第二行歌词
解析时可将歌词转换为对象数组,包含时间(秒)和文本:

function parseLrc(lrcText) {
return lrcText.split('\n').filter(line => {
const timeTag = line.match(/^\[(\d{2}):(\d{2})\.(\d{2})\]/);
return timeTag && line.length > 10;
}).map(line => {
const parts = line.match(/^\[(\d{2}):(\d{2})\.(\d{2})\](.*)/);
const min = parseInt(parts[1]);
const sec = parseInt(parts[2]);
const ms = parseInt(parts[3]);
const text = parts[4].trim();
return {
time: min * 60 + sec + ms / 100,
text
};
});
}
监听音频播放进度
使用HTML5的<audio>元素或第三方音频库(如howler.js)获取当前播放时间:
data() {
return {
currentTime: 0,
audio: null
}
},
mounted() {
this.audio = new Audio('song.mp3');
this.audio.addEventListener('timeupdate', () => {
this.currentTime = this.audio.currentTime;
});
}
计算当前歌词行
根据当前播放时间找到对应的歌词行:

computed: {
currentLineIndex() {
const { lyrics } = this;
for (let i = 0; i < lyrics.length; i++) {
if (this.currentTime < lyrics[i].time) {
return i - 1;
}
}
return lyrics.length - 1;
}
}
渲染歌词列表
在模板中渲染歌词列表,并高亮当前行:
<div class="lyrics-container">
<div
v-for="(line, index) in lyrics"
:key="index"
:class="{ 'active': index === currentLineIndex }"
>
{{ line.text }}
</div>
</div>
添加平滑滚动效果
通过CSS和JavaScript实现歌词自动居中滚动:
watch: {
currentLineIndex(newVal) {
const container = this.$refs.lyricsContainer;
const activeLine = container.children[newVal];
if (activeLine) {
container.scrollTo({
top: activeLine.offsetTop - container.offsetHeight / 2,
behavior: 'smooth'
});
}
}
}
扩展功能建议
- 双语歌词支持:解析双语LRC文件,中英文对照显示
- 逐字高亮:进一步拆分每行歌词为单字,实现卡拉OK式效果
- 动态背景:根据歌词情绪变化背景颜色或动画
- 播放控制:添加进度条拖动、播放速度调节等功能
完整组件示例
<template>
<div>
<audio ref="audio" controls :src="audioUrl"></audio>
<div class="lyrics-container" ref="lyricsContainer">
<div
v-for="(line, index) in lyrics"
:key="index"
:class="{ 'active': index === currentLineIndex }"
>
{{ line.text }}
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
audioUrl: 'path/to/audio.mp3',
lrcText: `[00:01.50]第一行歌词
[00:04.20]第二行歌词`,
currentTime: 0
};
},
computed: {
lyrics() {
return this.parseLrc(this.lrcText);
},
currentLineIndex() {
for (let i = 0; i < this.lyrics.length; i++) {
if (this.currentTime < this.lyrics[i].time) {
return i - 1;
}
}
return this.lyrics.length - 1;
}
},
mounted() {
const audio = this.$refs.audio;
audio.addEventListener('timeupdate', () => {
this.currentTime = audio.currentTime;
});
},
methods: {
parseLrc(lrcText) {
return lrcText.split('\n').filter(line => {
const timeTag = line.match(/^\[(\d{2}):(\d{2})\.(\d{2})\]/);
return timeTag && line.length > 10;
}).map(line => {
const parts = line.match(/^\[(\d{2}):(\d{2})\.(\d{2})\](.*)/);
const min = parseInt(parts[1]);
const sec = parseInt(parts[2]);
const ms = parseInt(parts[3]);
const text = parts[4].trim();
return {
time: min * 60 + sec + ms / 100,
text
};
});
}
},
watch: {
currentLineIndex(newVal) {
const container = this.$refs.lyricsContainer;
const activeLine = container.children[newVal];
if (activeLine) {
container.scrollTo({
top: activeLine.offsetTop - container.offsetHeight / 2,
behavior: 'smooth'
});
}
}
}
};
</script>
<style>
.lyrics-container {
height: 300px;
overflow-y: auto;
text-align: center;
}
.lyrics-container div {
padding: 10px;
transition: all 0.3s ease;
}
.lyrics-container .active {
color: #42b983;
font-weight: bold;
transform: scale(1.05);
}
</style>






