需求

实现一个 Audio展示组件 录音文件是 Opus (音频格式),并且可能是流shi

需实现功能
  • 组件可以展示音频的波纹图

  • 可以根据角色区分波纹的展示颜色

  • 同时根据词包在音频上不同时间进行展示

  • 若时间太长可以拖动展示音频

参考

在前端实现录音的可视化展示,包括波纹、进度条、时间刻度、点击波纹定位播放以及不同颜色展示等功能,确实有一些现成的组件库可以帮助你完成这些任务。以下是一些常用的库和方法:

1. Wavesurfer.js

Wavesurfer.js 是一个非常流行的库,用于创建音频波形可视化,并且支持多种功能,包括进度条、时间刻度、点击波形定位播放、以及自定义波形颜色。

安装
npm install wavesurfer.js
使用示例
import WaveSurfer from 'wavesurfer.js';

// Create an instance of WaveSurfer

const wavesurfer = WaveSurfer.create({

  container: '#waveform',

  waveColor: 'violet',

  progressColor: 'purple',

  cursorColor: 'navy',

  barWidth: 2,

  barHeight: 1,

  barGap: null

});

// Load an audio file

wavesurfer.load('path/to/audio/file.mp3');

// Add event listeners

wavesurfer.on('ready', () => {

  console.log('WaveSurfer is ready');

});

wavesurfer.on('audioprocess', () => {

  const currentTime = wavesurfer.getCurrentTime();

  console.log('Current time:', currentTime);

});

// Set different colors for specific segments

wavesurfer.addRegion({

  start: 5, // time in seconds

  end: 10,  // time in seconds

  color: 'rgba(0, 255, 0, 0.1)'

});

2. Waveform Playlist

Waveform Playlist 是一个用于创建和编辑音频波形的库,适合需要更复杂音频编辑功能的场景。

安装
npm install waveform-playlist
使用示例
import WaveformPlaylist from 'waveform-playlist';

const playlist = WaveformPlaylist.init({

  samplesPerPixel: 1000,

  waveHeight: 100,

  container: document.getElementById('playlist'),

  timescale: true,

  state: 'cursor',

  colors: {

    waveOutlineColor: '#E0EFF1',

    timeColor: 'grey',

    fadeColor: 'black'

  },

  controls: {

    show: true, // Whether or not to include the track controls

    width: 200 // Width of controls in pixels

  }

});

playlist.load([

  {

    src: 'path/to/audio/file.mp3',

    name: 'Track 1'

  }

]).then(() => {

  console.log('Audio loaded');

});

3. Amplitude.js

Amplitude.js 是一个简单而强大的音频播放器库,支持波形可视化和其它自定义功能。

安装
npm install amplitudejs
使用示例
<!DOCTYPE html>

<html lang="en">

<head>

  <meta charset="UTF-8">

  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>Amplitude.js Example</title>

  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/amplitudejs@5.0.3/dist/amplitude.css">

</head>

<body>

  <div id="player">

    <div id="amplitude-waveform"></div>

    <div id="amplitude-play-pause"></div>

  </div>

  <script src="https://cdn.jsdelivr.net/npm/amplitudejs@5.0.3/dist/amplitude.js"></script>

  <script>

    Amplitude.init({

      "songs": [

        {

          "name": "Song Name",

          "artist": "Artist Name",

          "url": "path/to/audio/file.mp3",

          "cover_art_url": "path/to/cover/art.jpg"

        }

      ],

      "waveforms": {

        "sample_rate": 50

      }

    });

  </script>

</body>

</html>

总结

以上介绍了几种常用的实现音频波形可视化的前端库。根据你的需求,可以选择合适的库来实现录音的波形展示、进度条、时间刻度、点击波形定位播放以及不同颜色展示等功能。Wavesurfer.js 是其中最流行和功能最全面的一个,适合大多数场景的使用。

以上是GPT的回答

目前参考使用waversurfer.js来做

成果

首先来看看最终效果

  • 目前组件实现了

    • 播放音频功能

    • 暂停、开始、终止

    • 调整播放速度

    • 点击波纹定位到具体位置并播放

    • 可以鼠标滚动调整画布缩放、横向滚动调整到对应时间轴

实现代码

由于开发这个需求使用的是vue3 + vite 所以实现方式如下

npm install --save wavesurfer.js
组件代码
<template>
  <a-spin :spinning="!isAssetReady">
    <a-divider />
    <div id="waveform" :style="{ height: playerHeight + 'px' }"></div>
    <div class="controls" align="center">
      <div class="controls-border"></div>
      <a-flex gap="8" align="center">
        <span class="controls-text">{{ currentTime }} / {{ duration }}</span>
        <a-button type="primary" shape="circle" @click="stopPlayback">
          <template #icon>
            <BorderOutlined />
          </template>
        </a-button>
        <a-button
          type="primary"
          size="large"
          shape="circle"
          @click="togglePlayPause"
        >
          <template #icon>
            <CaretRightOutlined v-if="!isPlaying" />
            <PauseOutlined v-else />
          </template>
        </a-button>
        <a-popover
          v-model:open="volumePopoverVisible"
          :overlayStyle="{ width: '200px' }"
          :overlayInnerStyle="{ padding: '4px' }"
          placement="right"
          trigger="click"
          color="rgb(5, 142, 204)"
        >
          <template #content>
            <a-slider v-model:value="volume" @change="changeVolume" />
          </template>
          <a-button type="primary" shape="circle">
            <template #icon>
              <SoundOutlined />
            </template>
          </a-button>
        </a-popover>
        <a-select
          v-model:value="playbackRate"
          :style="{ width: '80px' }"
          :bordered="false"
          @change="changePlaybackRate"
          :options="playbackRateOptions"
        />
      </a-flex>
      <div class="controls-border"></div>
    </div>
    <a-divider />
  </a-spin>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import {
  CaretRightOutlined,
  PauseOutlined,
  BorderOutlined,
  SoundOutlined,
} from '@ant-design/icons-vue';
import WaveSurfer from 'wavesurfer.js';
import TimelinePlugin from 'wavesurfer.js/dist/plugins/timeline.esm.js';
// import EnvelopePlugin from 'wavesurfer.js/dist/plugins/envelope.esm.js';
import audioAsset from '../../assets/test.opus';

const playbackRateOptions = [
  { value: '0.5', label: 'X 0.5' },
  { value: '1.0', label: 'X 1.0' },
  { value: '1.5', label: 'X 1.5' },
  { value: '2.0', label: 'X 2.0' },
  { value: '2.5', label: 'X 2.5' },
  { value: '3.0', label: 'X 3.0' },
];

function formatTime(time) {
  const minutes = Math.floor(time / 60);
  const seconds = Math.floor(time % 60);
  return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}

// 定义组件的 props
const props = defineProps({
  height: {
    type: Number,
    default: 200,
  },
});

const isAssetReady = ref(false);
const playerHeight = ref(props.height);
const currentTime = ref('00:00');
const duration = ref('00:00');
const isPlaying = ref(false);
const playbackRate = ref('1.0');
const volume = ref(100); // 默认音量为100%
const volumePopoverVisible = ref(false);

let wavesurfer;

const togglePlayPause = () => {
  if (wavesurfer.isPlaying()) {
    wavesurfer.pause();
    isPlaying.value = false;
  } else {
    wavesurfer.play();
    isPlaying.value = true;
  }
};

const stopPlayback = () => {
  wavesurfer.stop();
  isPlaying.value = false;
};

const changePlaybackRate = (value) => {
  wavesurfer.setPlaybackRate(parseFloat(value));
};

const changeVolume = (value) => {
  const volumeLevel = value / 100; // 将百分比转换为 0 到 1 之间的值
  wavesurfer.setVolume(volumeLevel);
};

/** 滚动控制缩放 */
const handleScroll = (event) => {
  const deltaY = event.deltaY;
  const currentZoom = wavesurfer.options.minPxPerSec;
  const newZoom = deltaY > 0 ? currentZoom - 10 : currentZoom + 10;
  // X 移动的时候,需要控制下Y的移动确定不是去缩放
  const deltaYAbs = Math.abs(deltaY);
  if (deltaYAbs > 10 && newZoom >= 0) {
    wavesurfer.zoom(newZoom);
  }
};

onMounted(() => {
  const waveformElement = document.getElementById('waveform');
  const audio = new Audio();
  audio.controls = true;
  audio.src = audioAsset;

  wavesurfer = WaveSurfer.create({
    container: '#waveform',
    waveColor: 'rgb(5,142,204)',
    progressColor: 'rgb(195,175,56)',
    height: playerHeight.value - 20,
    media: audio,
    minPxPerSec: 50,
    plugins: [TimelinePlugin.create()],
  });

  wavesurfer.on('ready', () => {
    duration.value = formatTime(wavesurfer.getDuration());
    wavesurfer.setVolume(volume.value / 100); // 初始化音量
    isAssetReady.value = true;
  });

  wavesurfer.on('audioprocess', () => {
    currentTime.value = formatTime(wavesurfer.getCurrentTime());
  });

  wavesurfer.on('seek', () => {
    currentTime.value = formatTime(wavesurfer.getCurrentTime());
  });

  wavesurfer.on('finish', () => {
    isPlaying.value = false;
  });

  wavesurfer.setPlaybackRate(parseFloat(playbackRate.value));

  waveformElement.addEventListener('wheel', handleScroll);
});

onBeforeUnmount(() => {
  const waveformElement = document.getElementById('waveform');
  waveformElement.removeEventListener('wheel', handleScroll);
});
</script>

<style lang="less" scoped>
#waveform {
  width: 100%;
}
#wave-timeline {
  height: 30px;
}
.controls {
  display: flex;
  align-items: center;
  margin-top: 10px;
  &-text {
    width: 100px;
    color: rgb(5, 142, 204);
  }
  &-border {
    flex: 1;
    height: 1px;
    background: #ccc;
  }
  :deep {
    .ant-btn-primary {
      background-color: rgb(5, 142, 204);
    }
    .ant-popover-inner-content {
      padding: 0;
    }
    .ant-slider {
      width: 100px;
    }
  }
}
</style>

组件实现代码中具体的方式比较简单可以参考一下

区域 Region

由于需要针对不同的语音片段去区分判断是谁说的某句话所以使用对应的Region插件去做

增加区域模块后的代码如下

实现效果

代码