audio-preview.vue 3.75 KB
<script setup lang="ts">
  import { Space } from '@arco-design/web-vue';
  import { computed, onMounted, ref } from 'vue';
  import axios from 'axios';
  import { audioBufferToWav } from '@/utils';
  import AudioSyncLyric from '@/utils/audioSyncLyric';

  const props = defineProps<{
    src: string | File | ArrayBuffer;
    lyric: string;
    startWithLyric?: boolean;
    startTime?: string;
    endTime?: string;
  }>();

  const audioRef = ref();
  const lyricRef = ref();

  const source = ref();

  const onPay = (e: any) => {
    const audios = document.getElementsByTagName('audio');
    [].forEach.call(audios, (i: HTMLAudioElement) => i !== e.target && i.pause());
  };

  const convertDurationToSeconds = (duration: string) => {
    const timeArray = duration.split(':'); // 将时间字符串拆分为时、分、秒的数组

    switch (timeArray.length) {
      case 1:
        return parseFloat(timeArray[0]);
      case 2:
        return parseInt(timeArray[0], 10) * 60 + parseFloat(timeArray[1]);
      case 3:
        return parseInt(timeArray[0], 10) * 3600 + parseInt(timeArray[1], 10) * 60 + parseFloat(timeArray[2]);
      default:
        return 0;
    }
  };

  const startTime = computed((): number => convertDurationToSeconds(props.startTime ?? '00:00.00'));
  const endTime = computed((): number | undefined => (props.endTime ? convertDurationToSeconds(props.endTime) : undefined));

  const getResult = async (): Promise<ArrayBuffer> => {
    if (props.src instanceof Blob) {
      return props.src.arrayBuffer();
    }
    if (props.src instanceof ArrayBuffer) {
      return Promise.resolve(props.src);
    }

    return axios
      .get(`${props.src}?response-content-type=Blob`, { responseType: 'blob', timeout: 60000 })
      .then(({ data }) => Promise.resolve(data.arrayBuffer()));
  };

  onMounted(async () => {
    // eslint-disable-next-line no-new
    new AudioSyncLyric(lyricRef.value, audioRef.value).setStartTime(startTime.value);
    const result: ArrayBuffer = await getResult();

    const audioCtx = new AudioContext();
    const audioBuffer = await audioCtx.decodeAudioData(result);
    const { numberOfChannels, sampleRate, duration } = audioBuffer;
    const start = startTime.value; // 从第几秒开始复制
    const end = endTime.value || duration; // 复制到第几秒结束

    // eslint-disable-next-line no-bitwise
    const startOffset = (start * sampleRate) >> 0; // 起始位置 = 开始时间 * 采样率
    // eslint-disable-next-line no-bitwise
    const endOffset = (end * sampleRate) >> 0; // 结束位置 = 结束时间 * 采样率
    const frameCount = endOffset - startOffset; // 音频帧数/长度 = 结束位置 - 起始位置
    const newAudioBuffer = audioCtx.createBuffer(numberOfChannels, frameCount, sampleRate);

    // eslint-disable-next-line no-plusplus
    for (let i = 0; i < numberOfChannels; i++) {
      newAudioBuffer.getChannelData(i).set(audioBuffer.getChannelData(i).slice(startOffset, endOffset));
    }

    const blob = audioBufferToWav(newAudioBuffer, frameCount);
    source.value = URL.createObjectURL(blob);
  });
</script>

<template>
  <Space direction="vertical" fill>
    <audio ref="audioRef" :src="source" class="audio" controls controlsList="nodownload noplaybackrate" @play="onPay" />
    <div ref="lyricRef" class="lyric">{{ lyric }}</div>
  </Space>
</template>

<style scoped lang="less">
  .audio {
    height: 30px;
    width: 100%;
    outline: none;
  }

  .lyric {
    border: none !important;
    background-color: #f7f8fa;

    :deep(.rabbit-lyrics__line) {
      padding: 0.3em 1em !important;
    }

    :deep(.rabbit-lyrics__inline) {
      color: #818181;
    }

    :deep(.rabbit-lyrics__inline.rabbit-lyrics__inline--active) {
      font-size: 16px;
      font-weight: 500;
      color: black;
    }
  }
</style>