wavesurfer.js-声纹可视化
站长
· 阅读数 14
一、效果
- 将一段声频以波形展示在页面上,支持播放/暂停、重放、停止、点击跳转播放
- 支持渲染区域,支持用户手动选择区域和删除区域,支持拖动区域和调整区域大小;当操作区域时,最好能实时循环播放区域
- 点击区域时循环播放本区域,点击区域外时正常播放至结束
二、vue实现
components/Wavesurfer.vue
<template>
<div>
<div ref="waveformRef"></div>
<div ref="waveTimelineRef"></div>
<el-button type="primary" @click="wavesurfer.skip(-3)">后退</el-button>
<el-button type="primary" @click="wavesurfer.playPause()">
<i class="el-icon-video-play" />
播放 / 暂停
</el-button> <el-button type="primary" @click="wavesurfer.skip(3)">前进</el-button>
<el-button type="primary" @click="rebroadcast">重放</el-button>
<el-button type="primary" @click="handleStop">停止</el-button>
<el-button @click="getRegions">打印区域</el-button>
</div>
</template>
<script>
import WaveSurfer from 'wavesurfer.js'
import Region from 'wavesurfer.js/dist/plugin/wavesurfer.regions'
import Cursor from 'wavesurfer.js/dist/plugin/wavesurfer.cursor'
import Timeline from 'wavesurfer.js/dist/plugin/wavesurfer.timeline'
export default {
props: ['voiceSrc'],
data() {
return { wavesurfer: null }
},
mounted() {
this.init()
},
beforeDestroy() {
this.wavesurfer && this.wavesurfer.destroy()
this.wavesurfer = null
},
methods: {
init() {
const container = this.$refs.waveformRef
container.addEventListener('click', () => {
console.log('点击容器')
this.clearLoop()
})
this.wavesurfer = WaveSurfer.create({
container, // 容器,唯一一个必须参数
cursorColor: 'red', // 音频光标颜色
waveColor: '#ddd', // 波形颜色
progressColor: '#bbb', // 已完成播放的波形颜色 当progressColor和waveColor相同时,完全不渲染进度波
backend: 'MediaElement', // 如果是在线音频,请将这行注释
autoCenter: false,
plugins: [
Region.create({
// regionsMinLength: 1.5,
regions: [
{ start: 1, end: 3, color: 'hsla(400, 100%, 30%, 0.5)' },
{ start: 5, end: 7, color: 'hsla(200, 50%, 70%, 0.4)' }
],
dragSelection: true,
}),
Cursor.create({
showTime: true,
opacity: 1,
customShowTimeStyle: { 'background-color': '#000', color: '#fff', padding: '2px', 'font-size': '10px' }
}),
Timeline.create({ container: this.$refs.waveTimelineRef })
]
})
this.wavesurfer.load(this.voiceSrc) // 加载音频url
// 点击区域
this.wavesurfer.on('region-click', (region) => {
const timer = setTimeout(() => {
console.log('定时器')
region.playLoop()
})
this.$once('hook:beforeDestroy', () => {
clearTimeout(timer)
timer = null
})
})
// 完成拖动或者完成大小调整时触发
this.wavesurfer.on('region-update-end', (region) => {
region.playLoop() // 循环播放选中区域
this.createDeleteButton(region)
})
this.wavesurfer.on('ready', () => {
// 为区域追加一个删除按钮
const regionList = Object.values(this.wavesurfer.regions.list)
for (const region of regionList) {
this.createDeleteButton(region)
}
})
},
// 格式化时间
formatTime(seconds) {
seconds = Number(seconds)
const minutes = Math.floor(seconds / 60)
seconds = seconds % 60
const secondsStr = Math.round(seconds).toString()
secondsStr = seconds.toFixed(2)
if (minutes > 0) {
return `${minutes < 10 ? '0' + minutes : minutes}:${seconds < 10 ? '0' + secondsStr : secondsStr}`
}
return `00:${seconds < 10 ? '0' + secondsStr : secondsStr}`
},
// 给区域创建删除按钮
createDeleteButton(region) {
if (!region.hasDeleteButton) {
const deleteButton = region.element.appendChild(document.createElement('button'))
deleteButton.innerText = '删除'
deleteButton.addEventListener('click', (e) => {
e.stopPropagation()
this.$confirm('确认删除此区域嘛?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => { region.remove() }).catch(() => { })
})
const css = { float: 'right', position: 'relative', cursor: 'pointer', color: 'red' }
region.style(deleteButton, css)
region.hasDeleteButton = true
}
},
// 获取区域列表
getRegions() {
const regionList = Object.values(this.wavesurfer.regions.list)
console.log(regionList)
},
// 重放
rebroadcast() {
this.clearLoop()
this.wavesurfer.play(0)
},
// 停止
handleStop() {
this.clearLoop()
this.wavesurfer.stop()
},
// 设置每个区域的loop为false
clearLoop() {
const regionList = Object.values(this.wavesurfer.regions.list)
for (const item of regionList) item.loop = false
}
}
}
</script>
App.vue
<Wavesurfer :voiceSrc="voiceSrc" />
import Wavesurfer from './components/Wavesurfer.vue'
import voiceSrc from '@/assets/来自我心.mp3'
data() {
return { voiceSrc }
},
components: { Wavesurfer }
三、描述
1、实例方法
- playPause 暂停时播放,播放时暂停
- play(0) 从0开始播放
- stop() 停止
- skip() 正数为前进,负数为后退
2、区域的删除按钮怎么添加的
- createDeleteButton函数用于创建button按钮,
region.element
可以用来appendChild节点 - 监听
ready
事件,这里可以获取到已有的区域列表,循环添加按钮 - 新添加的区域,会触发
region-update-end
事件,回调函数的参数是region
,这里可以再次调用createDeleteButton函数
3、点击区域进行循环播放,操作区域位置和大小时也会进行循环播放
调用region.playLoop()
即可
4、点击区域时循环播放本区域,点击区域外时正常播放至结束
- clearLoop函数用于将每个区域中的
loop
设置为false,playLoop方法会将当前区域的loop设置为true - 当点击区域外时,我在container的click事件回调中调用clearLoop,这样就可以正常播放至结束
- 当点击区域时,在定时器中调用playLoop方法,便又可以循环播放本区域
四、20230420补充功能:当拖动区域或调整区域大小时,重叠部分自动吸附
1、效果
2、代码实现
还是在region-update-end
事件中处理
// 完成拖动或者完成大小调整时触发
this.wavesurfer.on('region-update-end', (region) => {
// region.playLoop() // 循环播放选中区域
this.createDeleteButton(region)
const { prevElement, nextElement, prevRegionId, nextRegionId } = this.getPrevAndNextElement(region) // 获取相邻的两个节点
if (prevElement && prevElement.className === 'wavesurfer-region') { // 和前一个dom对齐
const prevRegion = this.getRegion(prevRegionId)
if (region.start < prevRegion.end) {
prevRegion.update({ start: prevRegion.start, end: region.start })
}
}
if (nextElement && nextElement.className === 'wavesurfer-region') { // 和后一个dom对齐
const nextRegion = this.getRegion(nextRegionId)
if (region.end > nextRegion.start) {
nextRegion.update({ start: region.end, end: nextRegion.end })
}
}
})
methods:
// 根据区域id,获取区域实例
getRegion(id) {
return this.wavesurfer.regions.list[id]
},
// 获取当前region的上一个region和下一个region
getPrevAndNextElement(currentRegion) {
const regionList = Object.entries(this.wavesurfer.regions.list);
const prevList = [], nextList = [];
for (const [key, region] of regionList) {
if (key !== currentRegion.id) {
if (region.start < currentRegion.start) {
prevList.push(region);
} else {
nextList.push(region);
}
}
}
const prevRegionId = prevList.length ? prevList[prevList.length - 1].id : null;
const nextRegionId = nextList.length ? nextList[0].id : null;
const regionDOMList = Array.from(document.querySelectorAll('.wavesurfer-region'));
const prevElement = regionDOMList.find(regionDOM => regionDOM.getAttribute('data-id') === prevRegionId);
const nextElement = regionDOMList.find(regionDOM => regionDOM.getAttribute('data-id') === nextRegionId);
return { prevRegionId, nextRegionId, prevElement, nextElement };
}
又有问题了,按照以上的代码,如果用户操作的这个区域只是“侵占”了相邻区域的一部分,那么直接将这部分内容“吞了”,但是“侵占”了整个区域时,会发现交互有一点点难看哦
实际我们期望的效果可能是这样的:
添加一个判断就可以了:
for (const [key, region] of regionList) {
if (key !== currentRegion.id) {
if (currentRegion.start <= region.start && currentRegion.end >= region.end) {
region.remove();
} else if (region.start < currentRegion.start) {
prevList.push(region);
} else {
nextList.push(region);
}
}
}