audioVisualDraw.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. /**
  2. * 音频可视化
  3. * @param audioDom
  4. * @param canvasDom
  5. * @param fftSize 2的幂数,最小为32
  6. * 注意 由于ios低版本必须在用户操作之后才能初始化 createMediaElementSource 所以必须在用户操作之后初始化
  7. */
  8. export default function audioVisualDraw(audioDom: HTMLAudioElement, canvasDom: HTMLCanvasElement, fftSize = 128) {
  9. type propsType = { canvWidth: number; canvHeight: number; canvFillColor: string; lineColor: string; lineGap: number }
  10. // canvas
  11. const canvasCtx = canvasDom.getContext("2d")!
  12. let { width, height } = canvasDom.getBoundingClientRect()
  13. // 向上取整,当with为小数或者小于当前dom时候,切换app之后 会出现黑边
  14. width = Math.ceil(width)
  15. height = Math.ceil(height)
  16. canvasDom.width = width
  17. canvasDom.height = height
  18. // audio
  19. //const audioCtx = new AudioContext()
  20. //const source = audioCtx.createMediaElementSource(audioDom)
  21. //const analyser = audioCtx.createAnalyser()
  22. //analyser.fftSize = fftSize
  23. //source?.connect(analyser)
  24. //analyser.connect(audioCtx.destination)
  25. //const dataArray = new Uint8Array(fftSize / 2)
  26. const draw = (data: Uint8Array, ctx: CanvasRenderingContext2D, { lineGap, canvWidth, canvHeight, canvFillColor, lineColor }: propsType) => {
  27. if (!ctx) return
  28. const w = canvWidth
  29. const h = canvHeight
  30. fillCanvasBackground(ctx, w, h, canvFillColor)
  31. // 可视化
  32. const dataLen = data.length
  33. let step = (w / 2 - lineGap * dataLen) / dataLen
  34. step < 1 && (step = 1)
  35. const midX = w / 2
  36. const midY = h / 2
  37. let xLeft = midX
  38. for (let i = 0; i < dataLen; i++) {
  39. const value = data[i]
  40. const percent = value / 255 // 最大值为255
  41. const barHeight = percent * midY
  42. canvasCtx.fillStyle = lineColor
  43. // 中间加间隙
  44. if (i === 0) {
  45. xLeft -= lineGap / 2
  46. }
  47. canvasCtx.fillRect(xLeft - step, midY - barHeight, step, barHeight)
  48. canvasCtx.fillRect(xLeft - step, midY, step, barHeight)
  49. xLeft -= step + lineGap
  50. }
  51. let xRight = midX
  52. for (let i = 0; i < dataLen; i++) {
  53. const value = data[i]
  54. const percent = value / 255 // 最大值为255
  55. const barHeight = percent * midY
  56. canvasCtx.fillStyle = lineColor
  57. if (i === 0) {
  58. xRight += lineGap / 2
  59. }
  60. canvasCtx.fillRect(xRight, midY - barHeight, step, barHeight)
  61. canvasCtx.fillRect(xRight, midY, step, barHeight)
  62. xRight += step + lineGap
  63. }
  64. }
  65. const fillCanvasBackground = (ctx: CanvasRenderingContext2D, w: number, h: number, colors: string) => {
  66. ctx.clearRect(0, 0, w, h)
  67. ctx.fillStyle = colors
  68. ctx.fillRect(0, 0, w, h)
  69. }
  70. const requestAnimationFrameFun = () => {
  71. // requestAnimationFrame(() => {
  72. // //analyser?.getByteFrequencyData(dataArray)
  73. // draw(generateMixedData(48), canvasCtx, {
  74. // lineGap: 2,
  75. // canvWidth: width,
  76. // canvHeight: height,
  77. // canvFillColor: "transparent",
  78. // lineColor: "rgba(255, 255, 255, 0.7)"
  79. // })
  80. // if (!isPause) {
  81. // requestAnimationFrameFun()
  82. // }
  83. // })
  84. const _time = setInterval(() => {
  85. if (isPause) {
  86. clearInterval(_time)
  87. return
  88. }
  89. //analyser?.getByteFrequencyData(dataArray)
  90. draw(generateMixedData(48), canvasCtx, {
  91. lineGap: 2,
  92. canvWidth: width,
  93. canvHeight: height,
  94. canvFillColor: "transparent",
  95. lineColor: "rgba(28,172,241, 0.58)"
  96. })
  97. }, 300);
  98. }
  99. let isPause = true
  100. const playVisualDraw = () => {
  101. //audioCtx.resume() // 重新更新状态 加了暂停和恢复音频音质发生了变化 所以这里取消了
  102. isPause = false
  103. requestAnimationFrameFun()
  104. }
  105. const pauseVisualDraw = () => {
  106. isPause = true
  107. requestAnimationFrame(()=>{
  108. canvasCtx.clearRect(0, 0, width, height);
  109. })
  110. //audioCtx?.suspend() // 暂停 加了暂停和恢复音频音质发生了变化 所以这里取消了
  111. // source?.disconnect()
  112. // analyser?.disconnect()
  113. }
  114. return {
  115. playVisualDraw,
  116. pauseVisualDraw
  117. }
  118. }
  119. export function generateMixedData(size: number) {
  120. const dataArray = new Uint8Array(size);
  121. const baseNoiseAmplitude = 30;
  122. const minFrequency = 0.01;
  123. const maxFrequency = 0.2;
  124. const minAmplitude = 50;
  125. const maxAmplitude = 150;
  126. let lastAmplitude = maxAmplitude; // 初始振幅设置为最大值
  127. let lastFrequency = minFrequency + Math.random() * (maxFrequency - minFrequency);
  128. for (let i = 0; i < size; i++) {
  129. const decayFactor = 1 - (i / size); // 使振幅随时间递减
  130. const amplitude = lastAmplitude * decayFactor + (Math.random() - 0.5) * 10;
  131. const frequency = lastFrequency + (Math.random() - 0.5) * 0.01;
  132. const wave = amplitude * (0.5 + 0.5 * Math.sin(frequency * i));
  133. const noise = Math.floor(Math.random() * baseNoiseAmplitude) - baseNoiseAmplitude / 2;
  134. dataArray[i] = Math.min(255, Math.max(0, Math.floor(wave + noise)));
  135. lastAmplitude += (amplitude - lastAmplitude) * 0.05;
  136. lastFrequency += (frequency - lastFrequency) * 0.05;
  137. }
  138. return dataArray;
  139. }