| | |
| | | minutes: 0, |
| | | hours: 0, |
| | | recordSegments: [], // 存储录音片段 |
| | | currentSegment: null, // 当前录音片段 |
| | | status: false, |
| | | recorder: null |
| | | recorder: null, |
| | | currentSegment: null // 当前录音片段 |
| | | } |
| | | }, |
| | | computed: { |
| | |
| | | } |
| | | }, |
| | | startNewRecording() { |
| | | console.log('开始新的录音') |
| | | this.isRecording = true |
| | | this.isPaused = false |
| | | this.startTimer() |
| | | this.$refs.mumuRecorder.start() |
| | | }, |
| | | pauseRecording() { |
| | | console.log('暂停录音') |
| | | this.isRecording = false |
| | | this.isPaused = true |
| | | this.stopTimer() |
| | | this.$refs.mumuRecorder.stop() |
| | | }, |
| | | resumeRecording() { |
| | | console.log('继续录音') |
| | | this.isRecording = true |
| | | this.isPaused = false |
| | | this.startTimer() |
| | | this.$refs.mumuRecorder.start() |
| | | }, |
| | | handlerSuccess(res) { |
| | | console.log('录音成功:', res) |
| | | console.log('录音成功回调:', res) |
| | | this.recorder = res |
| | | // 保存当前录音片段 |
| | | if (res.localUrl) { |
| | | this.recordSegments.push({ |
| | | url: res.localUrl, |
| | | duration: this.seconds + this.minutes * 60 + this.hours * 3600 |
| | | }) |
| | | console.log('当前录音片段URL:', res.localUrl) |
| | | console.log('当前已存储的片段数:', this.recordSegments.length) |
| | | console.log('当前片段:', this.currentSegment) |
| | | |
| | | // 只有当当前片段与上一个不同时才添加 |
| | | if (!this.currentSegment || this.currentSegment.url !== res.localUrl) { |
| | | console.log('添加新的录音片段') |
| | | this.currentSegment = { |
| | | url: res.localUrl, |
| | | data: res.data, |
| | | duration: this.seconds + this.minutes * 60 + this.hours * 3600 |
| | | } |
| | | this.recordSegments.push(this.currentSegment) |
| | | console.log('更新后的片段列表:', this.recordSegments) |
| | | } else { |
| | | console.log('跳过重复的录音片段') |
| | | } |
| | | } |
| | | }, |
| | | handlerError(code) { |
| | |
| | | closePopup() { |
| | | // 清空录音片段 |
| | | this.recordSegments = [] |
| | | this.currentSegment = null |
| | | // 重置计时器 |
| | | this.seconds = 0 |
| | | this.minutes = 0 |
| | |
| | | updateTimeDisplay() { |
| | | this.time = `${String(this.hours).padStart(2, '0')}:${String(this.minutes).padStart(2, '0')}:${String(this.seconds).padStart(2, '0')}`; |
| | | }, |
| | | onStop() { |
| | | // 合并音频文件 |
| | | async mergeAudioFiles(audioUrls) { |
| | | try { |
| | | const audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
| | | const audioBuffers = []; |
| | | |
| | | // 加载所有音频文件 |
| | | for (const url of audioUrls) { |
| | | const response = await fetch(url); |
| | | const arrayBuffer = await response.arrayBuffer(); |
| | | const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); |
| | | audioBuffers.push(audioBuffer); |
| | | } |
| | | |
| | | // 计算总时长 |
| | | const totalLength = audioBuffers.reduce((acc, buffer) => acc + buffer.length, 0); |
| | | |
| | | // 创建新的音频缓冲区 |
| | | const mergedBuffer = audioContext.createBuffer( |
| | | audioBuffers[0].numberOfChannels, |
| | | totalLength, |
| | | audioBuffers[0].sampleRate |
| | | ); |
| | | |
| | | // 合并音频数据 |
| | | let offset = 0; |
| | | for (const buffer of audioBuffers) { |
| | | for (let channel = 0; channel < buffer.numberOfChannels; channel++) { |
| | | const channelData = buffer.getChannelData(channel); |
| | | mergedBuffer.copyToChannel(channelData, channel, offset); |
| | | } |
| | | offset += buffer.length; |
| | | } |
| | | |
| | | // 将合并后的音频转换为 Blob |
| | | const wavBlob = await this.audioBufferToWav(mergedBuffer); |
| | | const mergedUrl = URL.createObjectURL(wavBlob); |
| | | |
| | | return mergedUrl; |
| | | } catch (error) { |
| | | console.error('合并音频失败:', error); |
| | | throw error; |
| | | } |
| | | }, |
| | | |
| | | // 将 AudioBuffer 转换为 WAV 格式 |
| | | audioBufferToWav(buffer) { |
| | | const numOfChan = buffer.numberOfChannels; |
| | | const length = buffer.length * numOfChan * 2; |
| | | const buffer2 = new ArrayBuffer(44 + length); |
| | | const view = new DataView(buffer2); |
| | | const channels = []; |
| | | let sample; |
| | | let offset = 0; |
| | | let pos = 0; |
| | | |
| | | // 写入 WAV 文件头 |
| | | setUint32(0x46464952); // "RIFF" |
| | | setUint32(36 + length); // 文件长度 |
| | | setUint32(0x45564157); // "WAVE" |
| | | setUint32(0x20746d66); // "fmt " chunk |
| | | setUint32(16); // 长度 = 16 |
| | | setUint16(1); // PCM (uncompressed) |
| | | setUint16(numOfChan); |
| | | setUint32(buffer.sampleRate); |
| | | setUint32(buffer.sampleRate * 2 * numOfChan); // avg. bytes/sec |
| | | setUint16(numOfChan * 2); // block-align |
| | | setUint16(16); // 16-bit |
| | | setUint32(0x61746164); // "data" - chunk |
| | | setUint32(length); // chunk length |
| | | |
| | | // 写入音频数据 |
| | | for (let i = 0; i < buffer.numberOfChannels; i++) { |
| | | channels.push(buffer.getChannelData(i)); |
| | | } |
| | | |
| | | while (pos < buffer.length) { |
| | | for (let i = 0; i < numOfChan; i++) { |
| | | sample = Math.max(-1, Math.min(1, channels[i][pos])); |
| | | sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767) | 0; |
| | | view.setInt16(44 + offset, sample, true); |
| | | offset += 2; |
| | | } |
| | | pos++; |
| | | } |
| | | |
| | | return new Blob([buffer2], { type: 'audio/wav' }); |
| | | |
| | | function setUint16(data) { |
| | | view.setUint16(pos, data, true); |
| | | pos += 2; |
| | | } |
| | | |
| | | function setUint32(data) { |
| | | view.setUint32(pos, data, true); |
| | | pos += 4; |
| | | } |
| | | }, |
| | | |
| | | // 修改 onStop 方法 |
| | | async onStop() { |
| | | console.log('停止录音') |
| | | // 如果正在录音,先停止当前录音 |
| | | if (this.isRecording) { |
| | | this.$refs.mumuRecorder.stop() |
| | |
| | | // 重置状态 |
| | | this.isRecording = false |
| | | this.isPaused = false |
| | | this.currentSegment = null |
| | | |
| | | // 处理录音片段 |
| | | if (this.recordSegments.length > 0) { |
| | | // 发送第一个录音片段的URL |
| | | this.$emit('submit', this.recordSegments[0].url) |
| | | this.closePopup() |
| | | try { |
| | | uni.showLoading({ |
| | | title: '正在处理音频...' |
| | | }) |
| | | |
| | | // 获取所有录音片段的URL |
| | | const audioUrls = this.recordSegments.map(segment => segment.url) |
| | | console.log('准备合并的音频片段列表:', audioUrls) |
| | | console.log('音频片段数量:', audioUrls.length) |
| | | |
| | | // 合并音频文件 |
| | | const mergedUrl = await this.mergeAudioFiles(audioUrls) |
| | | |
| | | // 创建合并后的音频数据对象 |
| | | const mergedAudioData = { |
| | | url: this.recordSegments[this.recordSegments.length - 1].url, |
| | | data: this.recordSegments[this.recordSegments.length - 1].data, |
| | | duration: this.seconds + this.minutes * 60 + this.hours * 3600 |
| | | } |
| | | |
| | | uni.showToast({ |
| | | title: '录制成功', |
| | | icon: 'success', |
| | | duration: 2000 |
| | | }) |
| | | |
| | | setTimeout(() => { |
| | | this.$emit('submit', mergedAudioData) |
| | | }, 2000) |
| | | |
| | | uni.hideLoading() |
| | | this.closePopup() |
| | | } catch (error) { |
| | | console.error('音频合并失败:', error) |
| | | uni.hideLoading() |
| | | uni.showToast({ |
| | | title: '音频合并失败,请重试', |
| | | icon: 'none', |
| | | duration: 2000 |
| | | }) |
| | | } |
| | | } else { |
| | | uni.showToast({ |
| | | title: '未检测到录音文件', |