|
@@ -18,6 +18,8 @@ import { storeData } from "/src/store";
|
|
|
import Title from "/src/page-instrument/header-top/title";
|
|
|
import { Vue3Lottie } from "vue3-lottie";
|
|
|
import audioBga from "./image/audioBga.json";
|
|
|
+import audioBga1 from "./image/leftCloud.json";
|
|
|
+import audioBga2 from "./image/rightCloud.json";
|
|
|
|
|
|
type IItemType = "intonation" | "cadence" | "integrity";
|
|
|
|
|
@@ -38,6 +40,9 @@ export default defineComponent({
|
|
|
isInitPlyr: false,
|
|
|
_plrl: null as any,
|
|
|
});
|
|
|
+ const lottieDom = ref<any>()
|
|
|
+ const lottieDom1 = ref<any>()
|
|
|
+ const lottieDom2 = ref<any>()
|
|
|
const level: any = {
|
|
|
BEGINNER: "入门级",
|
|
|
ADVANCED: "进阶级",
|
|
@@ -84,9 +89,123 @@ export default defineComponent({
|
|
|
controls: ["play-large", "play", "progress", "current-time", "duration"],
|
|
|
fullscreen: { enabled: false },
|
|
|
});
|
|
|
+ // 创建音波数据
|
|
|
+ if(mediaType.value === "audio"){
|
|
|
+ setTimeout(() => {
|
|
|
+ const audioDom = document.querySelector("#audioSrc") as HTMLAudioElement
|
|
|
+ const canvasDom = document.querySelector("#audioVisualizer") as HTMLCanvasElement
|
|
|
+ const { pauseVisualDraw, playVisualDraw } = audioVisualDraw(audioDom, canvasDom)
|
|
|
+ shareData._plrl.on('play', () => {
|
|
|
+ lottieDom.value.play()
|
|
|
+ lottieDom1.value.play()
|
|
|
+ lottieDom2.value.play()
|
|
|
+ playVisualDraw()
|
|
|
+ });
|
|
|
+ shareData._plrl.on('pause', () => {
|
|
|
+ lottieDom.value.pause()
|
|
|
+ lottieDom1.value.pause()
|
|
|
+ lottieDom2.value.pause()
|
|
|
+ pauseVisualDraw()
|
|
|
+ });
|
|
|
+ }, 300); // 弹窗动画是0.25秒 这里用定时器 确保canvas 能获取到宽高
|
|
|
+ }
|
|
|
shareData.isInitPlyr = true;
|
|
|
});
|
|
|
};
|
|
|
+ /**
|
|
|
+ * 音频可视化
|
|
|
+ * @param audioDom
|
|
|
+ * @param canvasDom
|
|
|
+ * @param fftSize 2的幂数,最小为32
|
|
|
+ */
|
|
|
+ function audioVisualDraw(audioDom: HTMLAudioElement, canvasDom: HTMLCanvasElement, fftSize = 128) {
|
|
|
+ type propsType = { canvWidth: number; canvHeight: number; canvFillColor: string; lineColor: string; lineGap: number }
|
|
|
+ // canvas
|
|
|
+ const canvasCtx = canvasDom.getContext("2d")!
|
|
|
+ const { width, height } = canvasDom.getBoundingClientRect()
|
|
|
+ canvasDom.width = width
|
|
|
+ canvasDom.height = height
|
|
|
+ // audio
|
|
|
+ const audioCtx = new AudioContext()
|
|
|
+ const analyser = audioCtx.createAnalyser()
|
|
|
+ const source = audioCtx.createMediaElementSource(audioDom)
|
|
|
+ analyser.fftSize = fftSize
|
|
|
+ source.connect(analyser)
|
|
|
+ analyser.connect(audioCtx.destination)
|
|
|
+ const dataArray = new Uint8Array(analyser.frequencyBinCount)
|
|
|
+ const draw = (data: Uint8Array, ctx: CanvasRenderingContext2D, { lineGap, canvWidth, canvHeight, canvFillColor, lineColor }: propsType) => {
|
|
|
+ if (!ctx) return
|
|
|
+ const w = canvWidth
|
|
|
+ const h = canvHeight
|
|
|
+ fillCanvasBackground(ctx, w, h, canvFillColor)
|
|
|
+ // 可视化
|
|
|
+ const dataLen = data.length
|
|
|
+ const step = (w / 2 - lineGap * dataLen) / dataLen
|
|
|
+ const midX = w / 2
|
|
|
+ const midY = h / 2
|
|
|
+ let xLeft = midX
|
|
|
+ for (let i = 0; i < dataLen; i++) {
|
|
|
+ const value = data[i]
|
|
|
+ const percent = value / 255 // 最大值为255
|
|
|
+ const barHeight = percent * midY
|
|
|
+ canvasCtx.fillStyle = lineColor
|
|
|
+ // 中间加间隙
|
|
|
+ if (i === 0) {
|
|
|
+ xLeft -= lineGap / 2
|
|
|
+ }
|
|
|
+ canvasCtx.fillRect(xLeft - step, midY - barHeight, step, barHeight)
|
|
|
+ canvasCtx.fillRect(xLeft - step, midY, step, barHeight)
|
|
|
+ xLeft -= step + lineGap
|
|
|
+ }
|
|
|
+ let xRight = midX
|
|
|
+ for (let i = 0; i < dataLen; i++) {
|
|
|
+ const value = data[i]
|
|
|
+ const percent = value / 255 // 最大值为255
|
|
|
+ const barHeight = percent * midY
|
|
|
+ canvasCtx.fillStyle = lineColor
|
|
|
+ if (i === 0) {
|
|
|
+ xRight += lineGap / 2
|
|
|
+ }
|
|
|
+ canvasCtx.fillRect(xRight, midY - barHeight, step, barHeight)
|
|
|
+ canvasCtx.fillRect(xRight, midY, step, barHeight)
|
|
|
+ xRight += step + lineGap
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const fillCanvasBackground = (ctx: CanvasRenderingContext2D, w: number, h: number, colors: string) => {
|
|
|
+ ctx.clearRect(0, 0, w, h)
|
|
|
+ ctx.fillStyle = colors
|
|
|
+ ctx.fillRect(0, 0, w, h)
|
|
|
+ }
|
|
|
+ const requestAnimationFrameFun = () => {
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ analyser.getByteFrequencyData(dataArray)
|
|
|
+ draw(dataArray, canvasCtx, {
|
|
|
+ lineGap: 2,
|
|
|
+ canvWidth: width,
|
|
|
+ canvHeight: height,
|
|
|
+ canvFillColor: "transparent",
|
|
|
+ lineColor: "rgba(255, 255, 255, 0.3)"
|
|
|
+ })
|
|
|
+ if (!isPause) {
|
|
|
+ requestAnimationFrameFun()
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ let isPause = true
|
|
|
+ const playVisualDraw = () => {
|
|
|
+ isPause = false
|
|
|
+ audioCtx.resume()
|
|
|
+ requestAnimationFrameFun()
|
|
|
+ }
|
|
|
+ const pauseVisualDraw = () => {
|
|
|
+ isPause = true
|
|
|
+ audioCtx.suspend()
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ playVisualDraw,
|
|
|
+ pauseVisualDraw
|
|
|
+ }
|
|
|
+ }
|
|
|
return () => (
|
|
|
<div class={[styles.headerTop, browserInfo.android && styles.android]}>
|
|
|
<div class={styles.left}>
|
|
@@ -324,8 +443,12 @@ export default defineComponent({
|
|
|
{
|
|
|
mediaType.value === "audio" ?
|
|
|
<div class={styles.audioBox}>
|
|
|
- <Vue3Lottie class={styles.audioBga} animationData={audioBga} loop={true}></Vue3Lottie>
|
|
|
+ <canvas class={styles.audioVisualizer} id="audioVisualizer"></canvas>
|
|
|
+ <Vue3Lottie ref={lottieDom} class={styles.audioBga} animationData={audioBga} autoPlay={false} loop={true}></Vue3Lottie>
|
|
|
+ <Vue3Lottie ref={lottieDom1} class={styles.audioBga1} animationData={audioBga1} autoPlay={false} loop={true}></Vue3Lottie>
|
|
|
+ <Vue3Lottie ref={lottieDom2} class={styles.audioBga2} animationData={audioBga2} autoPlay={false} loop={true}></Vue3Lottie>
|
|
|
<audio
|
|
|
+ crossorigin="anonymous"
|
|
|
id="audioSrc"
|
|
|
src={scoreData.value.videoFilePath}
|
|
|
controls="false"
|