useSpeechToAudio.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. import { onUnmounted, reactive, ref } from 'vue'
  2. import { ElMessage } from 'element-plus'
  3. export default function useSpeechToAudio({
  4. onEnd = (audio) => {},
  5. onSpeak = ({ duration }) => {},
  6. timeout = 0,
  7. }) {
  8. const state: any = reactive({
  9. duration: 0,
  10. mediaRecorder: null,
  11. analyser: null,
  12. timer: null,
  13. dataArray: null,
  14. animationId: null,
  15. timestamp: 0,
  16. })
  17. const volume = ref(0)
  18. const speak = async () => {
  19. try {
  20. // 请求麦克风权限
  21. const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
  22. const audioContext = new (window.AudioContext ||
  23. window.webkitAudioContext)()
  24. state.analyser = audioContext.createAnalyser()
  25. state.analyser.fftSize = 256
  26. const microphone = audioContext.createMediaStreamSource(stream)
  27. microphone.connect(state.analyser)
  28. state.dataArray = new Uint8Array(state.analyser.frequencyBinCount)
  29. state.mediaRecorder = new MediaRecorder(stream)
  30. const audioChunks: any = []
  31. state.mediaRecorder.ondataavailable = (event) => {
  32. audioChunks.push(event.data)
  33. }
  34. state.mediaRecorder.onstop = async () => {
  35. cancelAnimationFrame(state.animationId)
  36. clearInterval(state.timer)
  37. state.timer = null
  38. state.timestamp = 0
  39. const audioBlob = new Blob(audioChunks, { type: 'audio/mp3' })
  40. // this.audioUrl = URL.createObjectURL(this.audioBlob)
  41. stream.getTracks().forEach((track) => track.stop())
  42. if (microphone) {
  43. microphone.disconnect()
  44. }
  45. if (audioContext && audioContext.state !== 'closed') {
  46. audioContext.close()
  47. }
  48. if (!audioBlob) {
  49. ElMessage.error('没有可上传的录音文件')
  50. return
  51. }
  52. try {
  53. onEnd(audioBlob)
  54. } catch (err) {
  55. onEnd(null)
  56. ElMessage.error('上传错误:' + err)
  57. }
  58. }
  59. state.mediaRecorder.start()
  60. updateVolume()
  61. const startTime = Date.now()
  62. state.duration = 0
  63. volume.value = 0
  64. onSpeak({ duration: state.duration })
  65. // 更新录音时长
  66. state.timer = setInterval(() => {
  67. state.duration = Math.floor((Date.now() - startTime) / 1000)
  68. onSpeak({ duration: state.duration })
  69. }, 1000)
  70. } catch (err: any) {
  71. ElMessage.error('无法访问麦克风: ' + err.message)
  72. console.error('录音错误:', err)
  73. }
  74. }
  75. const stop = () => {
  76. state.mediaRecorder?.stop()
  77. }
  78. // 更新音量显示
  79. const updateVolume = () => {
  80. if (!state.analyser) return
  81. state.analyser.getByteFrequencyData(state.dataArray)
  82. // 计算平均音量
  83. let sum = 0
  84. for (let i = 0; i < state.dataArray.length; i++) {
  85. sum += state.dataArray[i]
  86. }
  87. const average = sum / state.dataArray.length
  88. // 将音量映射到0-100范围
  89. volume.value = Math.min(100, Math.max(0, Math.round(average / 2.55)))
  90. if (timeout > 0) {
  91. if (volume.value > 10) {
  92. state.timestamp = 0
  93. } else {
  94. if (state.timestamp > 0) {
  95. if (new Date().getTime() - state.timestamp > timeout * 1000) {
  96. stop()
  97. }
  98. } else {
  99. state.timestamp = new Date().getTime()
  100. }
  101. }
  102. }
  103. // 继续动画
  104. state.animationId = requestAnimationFrame(updateVolume)
  105. }
  106. return {
  107. speak,
  108. stop,
  109. volume,
  110. }
  111. }