audio-preview.vue
3.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
<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>