三、基于wavesurfer封装可视化音标标注组件


一、基于wavesurfer,regions封装的可视化音标标注控件

1、简单介绍

  基于wavesurfer,regions 封装的可视化音标标注控件。

2、基本流程

2.1、先简单看下效果

1、将一段声频以波形展示在页面上,支持播放/暂停、重放、停止、点击跳转播放

2、支持渲染区域,支持用户手动选择区域和删除区域,支持拖动区域和调整区域大小;当操作区域时,最好能实时循环播放区域

3、点击区域时循环播放本区域,点击区域外时正常播放至结束

2.2、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>

2.3、重点描述

2.3.1、实例方法

  playPause 暂停时播放,播放时暂停;play(0) 从0开始播放;stop() 停止;skip() 正数为前进,负数为后退!

2.3.2、区域的删除按钮怎么添加的

  createDeleteButton函数用于创建button按钮,region.element可以用来appendChild节点;监听ready事件,这里可以获取到已有的区域列表,循环添加按钮;新添加的区域,会触发region-update-end事件,回调函数的参数是region,这里可以再次调用createDeleteButton函数。

2.3.3、点击区域进行循环播放,操作区域位置和大小时也会进行循环播放

  调用region.playLoop()即可!

2.3.4、点击区域时循环播放本区域,点击区域外时正常播放至结束

  clearLoop函数用于将每个区域中的loop设置为false,playLoop方法会将当前区域的loop设置为true;当点击区域外时,我在container的click事件回调中调用clearLoop,这样就可以正常播放至结束;当点击区域时,在定时器中调用playLoop方法,便又可以循环播放本区域。

2.4、补充功能:当拖动区域或调整区域大小时,重叠部分自动吸附

2.4.1、先看效果

2.4.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 })
    }
  }
})


// 根据区域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({ key, val: currentRegion.start - region.start })
      } else {
        nextList.push({ key, val: region.end - currentRegion.end })
      }
    }
  }
  const prevListSort = this.sortArr(prevList, 'val'), nextListSort = this.sortArr(nextList, 'val') // 排序后的prevList和nextList
  const prevRegionId = prevListSort ? prevListSort[0].key : null
  const nextRegionId = nextListSort ? nextListSort[0].key : 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 }
},
// 数组对象根据指定属性值进行升序排序
sortArr(list, property) {
  if (list.length) {
    return [...list].sort((a, b) => a[property] - b[property])
  } else {
    return null
  }
},

注意:需要提前引入wavesurfer、wavesurfer.regions 两个文件


文章作者: 吴俊杰
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 吴俊杰 !
 上一篇
一、js手写函数第一批 一、js手写函数第一批
Object(this)、Reflect、无符号右移(>>>)、手写typeof、数组去重!你真的有深刻理解JS吗?请你务必花心思好好研究手写JS......
2024-12-02
下一篇 
二、iview-Table之动态合并单元格 二、iview-Table之动态合并单元格
iview越用越觉得不好用,很多内容不得不适用render函数去实现,这里记录一下使用iview table组件的时候实现单元格的合并......
2024-10-02
  目录