wavesurferjs+vue+ElementUi(音频标注案例)全网完整案例实现免费查看

官网地址

音频可视化插件wavesurferjs

需求示例

最近接触到了音频标注,以下是demo示例。
音频标注示例图

代码才是硬实力

话不多说,直接上代码:

<template>
  <div>
      <div class="mixin-components-container">
          <el-card class="box-card" v-if="audioUrl">
              <!-- 人员类型 -->
              <div class="primaryType">
                  <p
                      v-for="(tag, index) in fixedType"
                      :key="index"
                      @click = "changeCurType(index)"
                      :style='`background: ${curType === index ? primaryColor[index].dark : primaryColor[index].shallow}; color: ${curType === index ? "#FFF" : "#000"}`'
                  >{{tag}}</p>
              </div>
              <!-- waveform -->
              <div id="waveform" ref="waveform"></div>
              <!--时间轴 -->
              <div id="wave-timeline" ref="wave-timeline"></div>
              <!-- 控件元素 -->
              <div v-show="wavesurfer !== null && audioUrl !== ''" class="control">
                  <!-- 播放按钮 -->
                  <div  @click="playMusic" class="playBtn">
                      <i class="el-icon-video-play" v-if="!playFlag"></i>
                      <i class="el-icon-video-pause" v-else></i>
                  </div>
                  <!-- 纵向缩放 -->
                  <div class="zoom">
                      <p>振幅缩放</p>
                      <input
                          data-action="zoom"
                          @change="changeBarHeight()"
                          v-model="barHeight"
                          type="range"
                          min="1"
                          max="10"
                          value="1"
                      />
                  </div>
                  <!-- 水平缩放 -->
                  <div class="zoom">
                      <p>水平缩放</p>
                      <input
                          data-action="zoom"
                          @change="zoom(zooms)"
                          v-model="zooms"
                          type="range"
                          min="20"
                          max="1000"
                          value="20"
                      />
                  </div>
                  <!-- 音量 -->
                  <div class="grid-content bg-purple-dark">
                      <el-popover
                          placement="top-start"
                          trigger="click"
                          width="45"
                          min-width="45"
                          style="min-width: 38px"
                      >
                          <div class="block" style="width: 42px">
                          <el-slider
                              v-model="volumeValue"
                              vertical
                              height="100px"
                              @change="setVolume(volumeValue)"
                          />
                          </div>
                          <el-button class="normal allbtn primary" slot="reference">
                              音量
                          </el-button>
                      </el-popover>
                  </div>
                  <!-- 倍速播放 -->
                  <div class="grid-content bg-purple-dark">
                      <el-tooltip
                          class="item"
                          effect="dark"
                          content="倍速调整"
                          placement="bottom"
                      >
                          <el-popover
                          placement="top"
                          width="180"
                          trigger="click"
                          style="margin-left: 10px"
                          >
                          <el-input-number
                              v-model="speed"
                              width="180"
                              :precision="2"
                              :step="0.25"
                              :min="0.5"
                              :max="2"
                              @change="doubleSpeed(speed)"
                          />
                          <el-button slot="reference" round>
                              {{ speed + " X" }}
                          </el-button>
                          </el-popover>
                      </el-tooltip>
                  </div>
              </div>
          </el-card>
          <!-- 空状态 -->
          <el-empty :image-size="200" v-else></el-empty>
      </div>
      <!-- 标注内容编辑区 -->
      <ul class="labelTextArea" v-if="showList.length > 0" >
          <div :style="`border:2px solid ${setColor()};color:${setColor()}`" @click="isChange = !isChange">
              {{ showList[curEditRegionIndex].speaker }}
              <i :class="isChange ? 'el-icon-caret-top' : 'el-icon-caret-bottom'"></i>
              <ul v-show="isChange">
                  <li
                      v-for="(tag, index) in fixedType"
                      :key="index"
                      @click="editClass(index,$event)"
                      :style="`color: ${primaryColor[index].dark}`"
                  >{{ tag }}</li>
              </ul>
          </div>
          <div v-show="primaryTypeName.length>0" class="tagArea">
              <p>标注标签:</p>
              <div class="tagList">
                  <p
                      v-for="(v, k) of primaryTypeName"
                      :key="k"
                  >
                      <input
                          type="checkbox"
                          v-model="showList[curEditRegionIndex].class"
                          :value="v"
                      >
                      {{ v }}
                  </p>
              </div>
          </div>
          <li class="detailInfo">
              <div>
                  <p>标注ID: {{ showList[curEditRegionIndex].id }}</p>
                  <span class="deleteBtn" @click.prevent="deleteRegion(showList[curEditRegionIndex].id)">删除</span>
              </div>
              <textarea
                  @mouseenter="changeActive(showList[curEditRegionIndex], 'enter')"
                  @mouseleave="changeActive(showList[curEditRegionIndex], 'leave')"
                  v-model.lazy="showList[curEditRegionIndex].label"
                  style="height: 100;"
                  placeholder="暂无标注信息,请点击输入..."
              ></textarea>
          </li>
      </ul>
  </div>
</template>

<script>
import WaveSurfer from 'wavesurfer.js';
import RegionsPlugin from 'wavesurfer.js/dist/plugin/wavesurfer.regions.min.js';
import CursorPlugin from 'wavesurfer.js/dist/plugin/wavesurfer.cursor.js';
import Timeline from 'wavesurfer.js/dist/plugin/wavesurfer.timeline.js';
export default {
  name: 'AudioLabel',
  props: ['markerImgs', 'pid'],
  data () {
      return {
          isChange: false,
          zooms: 100, // 缩放
          volumeValue: [1], // 音频音量
          speed: 1.0, // 倍速
          barHeight: 1, // 振幅比例(波线高度)
          fixedType: ['第一种', '第二种', '第三种','第四种'], // (固定三个)
          wavesurfer: null, // 音频波线承载器
          playFlag: false, // 播放按钮切换器
          audioUrl: '', // 音频地址
          initGetData: {}, // 初始化数据留存根
          curType: -1, // 当前标注类型 -1默认 0-9对应primaryTypeName分类下标
          curEditRegionIndex: 0, // 当前可编辑数据下标
          primaryTypeName: [], // 预分类名(最多10个)
          showList: [], // 标注信息列表
          primaryColor: [
              {
                  shallow: 'rgba(255, 0, 0, 0.3)',
                  dark: 'rgba(255, 0, 0, 1)'
              }, // 第一种
              {
                  shallow: 'rgba(0, 0, 255, 0.3)',
                  dark: 'rgba(0, 0, 255, 1)'
              }, // 第二种
              {
                  shallow: 'rgba(40, 190, 130, 0.3)',
                  dark: 'rgba(40, 190, 130, 1)'
              }, // 第三种
              {
                  shallow: 'rgba(0, 255, 255, 0.3)',
                  dark: 'rgba(0, 255, 255, 1)'
              }, // 第四种
              {
                  shallow: 'rgba(255, 0, 255, 0.3)',
                  dark: 'rgba(255, 0, 255, 1)'
              }, // 第五种
              {
                  shallow: 'rgba(34, 193, 34, 0.3)',
                  dark: 'rgba(34, 193, 34, 1)'
              }, // 第六种
              {
                  shallow: 'rgba(51, 161, 201, 0.3)',
                  dark: 'rgba(51, 161, 201, 1)'
              }, // 第七种
              {
                  shallow: 'rgba(255, 192, 203, 0.3)',
                  dark: 'rgba(255, 192, 203, 1)'
              }, // 第八种
              {
                  shallow: 'rgba(244, 164, 96, 0.3)',
                  dark: 'rgba(244, 164, 96, 1)'
              }, // 第九种
              {
                  shallow: 'rgba(138, 199, 140, 0.3)',
                  dark: 'rgba(138, 199, 140, 1)'
              } // 第十种
          ] // 预选标注颜色(十种,只需前端页面展示使用,后端数据无需提供和保存)
      };
  },
  watch: {
    audioUrl() {
      this.initAudioWave()
    }
  },
  mounted() {
    this.audioUrl = 'http://music.163.com/song/media/outer/url?id=447925558.mp3'
  },
  beforeDestroy () {
      this.wavesurfer && this.wavesurfer.unAll();
      this.wavesurfer && this.wavesurfer.destroy();
      this.wavesurfer = null;
      this.audioUrl = '';
  },
  methods: {
      /** 设置标题颜色 */
      setColor () {
          if (!this.showList[this.curEditRegionIndex].color) return '#CCC';
          const color = this.showList[this.curEditRegionIndex].color.split(',');
          color[3] = '1)';
          return color.join(',');
      },
      /**
       * 点击标注片段切换对应文本框信息
       * @param {object} regionInfo 标注片段信息
       */
      changeTextArea (regionInfo) {
          this.curEditRegionIndex = this.showList.findIndex((item) => item.id === regionInfo.id); // 查找是否有对应标注
      },
      /** 初始化音频波插件 */
      initAudioWave () {
          this.$nextTick(() => {
              this.wavesurfer = WaveSurfer.create({
                  container: this.$refs.waveform, // 音频波线的容器
                  waveColor: '#ccc', // 波形的填充颜色(未播放区域)
                  progressColor: 'skyblue', // 进度颜色
                  backend: 'MediaElement',
                  scrollParent: true, // 开启滚动
                  cursorColor: 'red', // 指定进度光标颜色
                  barMinHeight: 1, // 振幅最小高度
                  barHeight: Number(this.barHeight), // 波形振幅
                  mediaControls: false, // 启用媒体基本控件
                  audioRate: '1', // 音频波放速度 数字约小播放约慢
                  autoCenter: true, // 有滚动条音频线居中展示
                  plugins: [
                      RegionsPlugin.create({}), // 开启标注区
                      Timeline.create({ container: '#wave-timeline' }), // 开启时间线轴
                      CursorPlugin.create({
                          showTime: true, // 展示鼠标位置对应时间
                          opacity: 1, // 透明度
                          customShowTimeStyle: {
                              backgroundColor: '#000',
                              color: '#fff',
                              padding: '2px',
                              fontSize: '10px'
                          } // 指针轴时间展示区样式
                      }) // 插件--指针轴的配置
                  ] // 插件
              });
              // 线上地址直接引用
              this.wavesurfer.load(this.audioUrl);
              this.wavesurfer.on('finish', () => { this.playFlag = false; }); // 播放完毕自动关闭
              this.wavesurfer.on('region-update-end', (obj) => { this.toUpdateShowList(obj); }); // 标注区更新
              this.wavesurfer.on('region-click', (obj) => { this.changeTextArea(obj); }); // 单击标注片段
              this.wavesurfer.on('waveform-ready', () => { // 音波图渲染完毕
                  // 默认全选
                  if (this.showList.length === 0) {
                      this.showList.push({
                          id: new Date().getTime(),
                          label: '',
                          start: 0,
                          end: this.wavesurfer.getDuration(), // 获取音频全部时常
                          color: this.primaryColor[0].shallow, // 如果选了类型则为对应类型颜色,否则为默认灰色
                          class: [],
                          speaker: this.fixedType[0]
                      });
                      this.drawRegion();
                  }
              });
              this.wavesurfer.enableDragSelection({ color: 'rgba(0,0,0,.3)' }); // 允许鼠标拖动创建标注区
              this.drawRegion(); // 有标注则自动绘制已标注部分
          });
      },
      /**
       * 选择标注类型
       * @param {number} index 标注类型索引
       */
      changeCurType (index) {
          this.curType = this.curType === index ? -1 : index;
      },
      /**
       * 通过自定义属性查找元素
       * @param {string} tag 元素
       * @param {string} attr 自定义属性名
       * @param {number} value 自定义属性值
       */
      getElementByAttr (tag, attr, value) {
          var aElements = document.getElementsByTagName(tag);
          var aEle = [];
          for (var i = 0; i < aElements.length; i++) {
              if (aElements[i].getAttribute(attr) === value) aEle.push(aElements[i]);
          }
          return aEle;
      },
      /**
       * 划过标注信息对应标注区高亮
       * @param {object} regionInfo 标注片段信息
       * @param {string} type 交互类型 enter->进入 leave->滑出
       */
      changeActive (regionInfo, type) {
          if (this.wavesurfer === null) return;
          let elm = this.getElementByAttr(
              'region',
              'data-id',
              `${regionInfo.id}`
          )[0];
          const color = regionInfo.color.split(',');
          color[3] = type === 'enter' ? '0.8)' : '0.3)';
          elm.style.backgroundColor = color.join(',');
          elm.style.border = type === 'enter' ? '2px dashed blue' : 'none';
          elm.style.boxSizing = 'border-box';
      },
      /**
       * 新建/更改 标注片段+自动切换文本框信息
       * 选择标注类型->创建新的标注片段
       * 未选择标注类型->仅预生成灰色选中区且不作为新的标注片段添加
       */
      toUpdateShowList (info) {
          const newArr = JSON.parse(JSON.stringify(this.showList));
          let ind = newArr.findIndex((item) => item.id === info.id); // 查找是否有对应标注
          let newRegion = this.curType > -1
              ? {
                  id: new Date().getTime(),
                  label: '',
                  start: info.start,
                  end: info.end,
                  color: this.primaryColor[this.curType].shallow || '#ccc', // 如果选了类型则为对应类型颜色,否则为默认灰色
                  class: [],
                  speaker: this.fixedType[this.curType] || '坐席'
              }
              : { };
          if (ind === -1) { // 有类型才能添加标注数据
              this.curType > -1 && newArr.push(newRegion);
          } else { // 更新选择区选择范围
              newArr[ind].start = info.start;
              newArr[ind].end = info.end;
          }
          this.showList = newArr;
          this.drawRegion();
          ind === -1 && this.curType > -1 && this.changeTextArea(newRegion); // 创建新的选择区后自动切换文本框内容
      },
      /**
       * "播放/暂停"按钮
       * 单击触发事件,暂停的话单击则播放,正在播放的话单击则暂停播放
       */
      playMusic () {
          this.playFlag ? this.wavesurfer.pause() : this.wavesurfer.play();
          this.playFlag = !this.playFlag;
      },
      /** 绘制音轨标注片段 */
      drawRegion () {
          this.wavesurfer.clearRegions();
          this.showList.forEach((regionInfo) => {
              this.wavesurfer.addRegion({
                  id: regionInfo.id,
                  start: regionInfo.start, // 区域开始时间
                  end: regionInfo.end, // 区域结束时间
                  color: regionInfo.color, // 区域颜色
                  drag: true, // 是否可拖拽
                  resize: true // 是否可改变大小
              });
          });
      },
      /**
       * 点击删除标注 自动切换文本框信息为已标注片段的最后一项
       * @param {number} id 标注信息ID
       */
      deleteRegion (id) {
          const newArr = JSON.parse(JSON.stringify(this.showList));
          let ind = newArr.findIndex((item) => item.id === id); // 查找是否有对应标注
          if (ind === -1) return;
          newArr.splice(ind, 1);
          this.showList = newArr;
          this.drawRegion();
          this.changeTextArea(newArr.length > 0 ? newArr[newArr.length - 1] : 0);
      },
      /**
       * 更改已标注片段的类型
       * @param {number} key 索引
       * @param {object} e 点击元素信息
       */
      editClass (key, e) {
          e.stopPropagation();
          const newArr = JSON.parse(JSON.stringify(this.showList));
          let index = newArr.findIndex((item) => item.id === this.showList[this.curEditRegionIndex].id);
          if (index === -1) return;
          newArr[index].speaker = this.fixedType[key];
          newArr[index].color = this.primaryColor[key].shallow;
          this.showList = newArr;
          this.drawRegion();
          this.isChange = false;
      },
      /**
       * 振幅放大/缩小
       * 该功能支持需要摒弃音频资源失效策略
       */
      changeBarHeight () {
          this.wavesurfer.unAll();
          this.wavesurfer.destroy();
          this.wavesurfer = null;
          this.initAudioWave();
      },
      /**
       * 波形图缩放
       * @param {number} val 缩放值
       * 调用zoom() API更改水平比例
       */
      zoom (val) {
          this.wavesurfer && this.wavesurfer.zoom(val);
      },
      /**
       * 设置音量
       * @param {number} val 音量值
       */
      setVolume (val) {
          this.wavesurfer && this.wavesurfer.setVolume(val / 100);
      },
      /**
       * 倍速播放
       * @param {number} rate 0.5-2的取值范围 加减差值为0.25
       */
      doubleSpeed (rate) {
          this.wavesurfer && this.wavesurfer.setPlaybackRate(rate);
      }

  }
};
</script>

<style scoped lang="scss">
.mixin-components-container {
  background: #fff;
  height: max-content;
  border: none;
}

.colorTag {
  display: inline-block;
  height: 10px;
  width: 30px;
}

.primaryType {
  width: 100%;
  color:#000;
  display: flex;
  >p {
      padding: 0 3px;
      margin-left: 10px;
  }
}

.labelTextArea {
  width: 90%;
  height: max-content;
  margin-top: 20px;
  padding-left: 10px;
  >div {
      padding: 2px 10px;
      width: max-content;
  }

  .detailInfo {
      width: 100%;
      >div {
          height: 40px;
          display: flex;
          justify-content: space-between;
          align-items: center;
          >p {
              height: max-content;
              margin-bottom: 0;
          }
          .deleteBtn {
              padding: 3px 10px;
              display: inline-block;
              border-radius: 5px;
              text-align: center;
              background-color: red;
              font-size: 12px;
              color: #fff;
          }
      }
      textarea {
          width: 100%;
          height: 80px;
          border: 1px solid #ccc;
      }
  }
  .tagArea {
      width: 100%;
      display: flex;
      padding: 0;
      margin: 10px auto 0;
      p {
          margin-bottom: 0;
      }
      .tagList {
          width: max-content;
          display: flex;
          flex-wrap: wrap;
          margin-bottom: 0;
          p {
              margin-right: 30px;
              margin-bottom: 0!important;
          }
          input {
              margin-right: 3px;
          }
      }
  }
}
.control {
  display: flex;
  height: max-content;
  align-items: center;

  .playBtn{
      margin: auto;
      height: max-content;
      i {
          display: block;
          font-size: 60px;
          color: #409EFF;
      }
  }

  .zoom {
      width: max-content;
      height: 100%;
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-right: 10px;
      >p {
          width: max-content;
          margin-bottom: 0;
      }
      input {
          width: 100px;
      }
  }
}

</style>

实现功能

可能有的小伙伴看完还是有点懵,那么,总结一下基本上实现的功能吧:

  1. 语音可视化(频波图绘制)
  2. 对应音频时间轴绘制
  3. 音频播放与暂停控制
  4. 手动点击可变更播放进度
  5. 音频部分区域选中(多区域)
  6. 选中区域可更改(移动,拉伸)
  7. 可更改标注区的展示与隐藏
  8. 可删除标注区
  9. 鼠标滑动至标注信息,对应标注区高亮
  10. 音量、倍速、横/纵向调整

依赖包版本相关

{
  "name": "vh",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.8.3",
    "element-ui": "^2.15.13",
    "vue": "^2.6.14",
    "vue-router": "^3.5.1",
    "vuex": "^3.6.2",
    "wavesurfer.js": "^6.4.0"
  },
  "devDependencies": {
    "@babel/core": "^7.12.16",
    "@babel/eslint-parser": "^7.12.16",
    "@vue/cli-plugin-babel": "~5.0.0",
    "@vue/cli-plugin-eslint": "~5.0.0",
    "@vue/cli-plugin-router": "~5.0.0",
    "@vue/cli-plugin-vuex": "~5.0.0",
    "@vue/cli-service": "~5.0.0",
    "eslint": "^7.32.0",
    "eslint-plugin-vue": "^8.0.3",
    "sass": "^1.32.7",
    "sass-loader": "^12.0.0",
    "vue-template-compiler": "^2.6.14"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "@babel/eslint-parser"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ],
  "_id": "vh@0.1.0",
  "readme": "ERROR: No README data found!"
}

求点赞+关注

小伙伴们可根据自己实际需求更改代码,代码区呢也进行了详细的功能注释,创作不易,多多点赞+关注,谢谢支持,同时,还有很多好的技术网站点关注之后即可查看~
更多技术分享,敬请期待

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值