|
|
@@ -309,6 +309,232 @@ import wOffice from "../test/file/wOffice.vue";
|
|
|
import { v4 as uuidv4 } from "uuid";
|
|
|
import checkDialog from "./components/checkDialog";
|
|
|
|
|
|
+
|
|
|
+// 音频格式转换:将非MP3音频转换为真正的MP3格式(使用@breezystack/lamejs库)
|
|
|
+const convertAudioToMp3 = async (file) => {
|
|
|
+ let lamejs;
|
|
|
+ let Mp3Encoder;
|
|
|
+ let audioContext = null;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 确保传入的是 File 对象
|
|
|
+ let fileObj = file;
|
|
|
+
|
|
|
+ // 如果不是 File 对象,尝试从可能的属性中获取
|
|
|
+ if (!(fileObj instanceof File) && !(fileObj instanceof Blob)) {
|
|
|
+ // 尝试从 raw 属性获取
|
|
|
+ if (fileObj.raw && (fileObj.raw instanceof File || fileObj.raw instanceof Blob)) {
|
|
|
+ fileObj = fileObj.raw;
|
|
|
+ } else {
|
|
|
+ throw new Error('传入的不是有效的 File 或 Blob 对象');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查文件大小(限制为50MB)
|
|
|
+ if (fileObj.size > 50 * 1024 * 1024) {
|
|
|
+ throw new Error('音频文件过大(超过50MB),无法转换');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 导入修复版本的lamejs
|
|
|
+ console.log('开始加载lamejs库...');
|
|
|
+ const lamejsModule = await import('@breezystack/lamejs');
|
|
|
+
|
|
|
+ // 获取Mp3Encoder
|
|
|
+ if (lamejsModule.default && lamejsModule.default.Mp3Encoder) {
|
|
|
+ Mp3Encoder = lamejsModule.default.Mp3Encoder;
|
|
|
+ lamejs = lamejsModule.default;
|
|
|
+ } else if (lamejsModule.Mp3Encoder) {
|
|
|
+ Mp3Encoder = lamejsModule.Mp3Encoder;
|
|
|
+ lamejs = lamejsModule;
|
|
|
+ } else if (lamejsModule.default) {
|
|
|
+ // 检查是否是命名空间导出
|
|
|
+ lamejs = lamejsModule.default;
|
|
|
+ if (lamejs.Mp3Encoder) {
|
|
|
+ Mp3Encoder = lamejs.Mp3Encoder;
|
|
|
+ } else {
|
|
|
+ throw new Error('lamejs库结构异常:无法找到Mp3Encoder');
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ throw new Error('lamejs库结构异常:无法找到Mp3Encoder');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!Mp3Encoder || typeof Mp3Encoder !== 'function') {
|
|
|
+ throw new Error('lamejs库加载失败:Mp3Encoder未找到或不是构造函数');
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('lamejs加载成功,Mp3Encoder类型:', typeof Mp3Encoder);
|
|
|
+
|
|
|
+ // 创建AudioContext
|
|
|
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
|
|
+ if (!AudioContextClass) {
|
|
|
+ throw new Error('浏览器不支持Web Audio API');
|
|
|
+ }
|
|
|
+
|
|
|
+ audioContext = new AudioContextClass();
|
|
|
+
|
|
|
+ // 读取文件为ArrayBuffer
|
|
|
+ console.log('开始读取音频文件,大小:', fileObj.size, 'bytes');
|
|
|
+
|
|
|
+ // 读取文件为ArrayBuffer
|
|
|
+ let arrayBuffer;
|
|
|
+ if (typeof fileObj.arrayBuffer === 'function') {
|
|
|
+ // 使用 File/Blob 的 arrayBuffer 方法
|
|
|
+ arrayBuffer = await fileObj.arrayBuffer();
|
|
|
+ } else {
|
|
|
+ // 如果没有 arrayBuffer 方法,使用 FileReader
|
|
|
+ arrayBuffer = await new Promise((resolve, reject) => {
|
|
|
+ const reader = new FileReader();
|
|
|
+ reader.onload = (e) => resolve(e.target.result);
|
|
|
+ reader.onerror = reject;
|
|
|
+ reader.readAsArrayBuffer(fileObj);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ console.log('文件读取完成,ArrayBuffer大小:', arrayBuffer.byteLength);
|
|
|
+
|
|
|
+ // 解码音频文件
|
|
|
+ console.log('开始解码音频数据...');
|
|
|
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer.slice(0));
|
|
|
+ console.log('音频解码完成,采样率:', audioBuffer.sampleRate, '声道数:', audioBuffer.numberOfChannels, '时长:', audioBuffer.duration, '秒');
|
|
|
+
|
|
|
+ // 获取音频数据
|
|
|
+ const sampleRate = audioBuffer.sampleRate;
|
|
|
+ const numberOfChannels = audioBuffer.numberOfChannels;
|
|
|
+ const samples = audioBuffer.getChannelData(0); // 使用第一个声道
|
|
|
+
|
|
|
+ // 处理音频声道:如果是立体声,保持立体声;如果是多声道,混合为单声道
|
|
|
+ let audioData;
|
|
|
+ let channels = numberOfChannels;
|
|
|
+
|
|
|
+ if (numberOfChannels === 1) {
|
|
|
+ // 单声道,直接使用
|
|
|
+ audioData = samples;
|
|
|
+ channels = 1;
|
|
|
+ } else if (numberOfChannels === 2) {
|
|
|
+ // 立体声,保持立体声(交错存储)
|
|
|
+ console.log('检测到立体声音频,保持立体声格式...');
|
|
|
+ const leftChannel = audioBuffer.getChannelData(0);
|
|
|
+ const rightChannel = audioBuffer.getChannelData(1);
|
|
|
+ audioData = new Float32Array(leftChannel.length * 2);
|
|
|
+ for (let i = 0; i < leftChannel.length; i++) {
|
|
|
+ audioData[i * 2] = leftChannel[i];
|
|
|
+ audioData[i * 2 + 1] = rightChannel[i];
|
|
|
+ }
|
|
|
+ channels = 2;
|
|
|
+ } else {
|
|
|
+ // 多声道(超过2个),混合为单声道
|
|
|
+ console.log('检测到多声道音频(超过2个),正在混合为单声道...');
|
|
|
+ audioData = new Float32Array(samples.length);
|
|
|
+ for (let i = 0; i < samples.length; i++) {
|
|
|
+ let sum = samples[i];
|
|
|
+ for (let ch = 1; ch < numberOfChannels; ch++) {
|
|
|
+ const channelData = audioBuffer.getChannelData(ch);
|
|
|
+ sum += channelData[i];
|
|
|
+ }
|
|
|
+ audioData[i] = sum / numberOfChannels;
|
|
|
+ }
|
|
|
+ channels = 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 转换为16位PCM
|
|
|
+ console.log('开始转换为16位PCM...');
|
|
|
+ const pcm16 = new Int16Array(audioData.length);
|
|
|
+ for (let i = 0; i < audioData.length; i++) {
|
|
|
+ const s = Math.max(-1, Math.min(1, audioData[i]));
|
|
|
+ pcm16[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
|
|
|
+ }
|
|
|
+ console.log('PCM转换完成,样本数:', pcm16.length, '声道数:', channels);
|
|
|
+
|
|
|
+ // 创建MP3编码器
|
|
|
+ // 参数:声道数(1=单声道, 2=立体声), 采样率, 比特率(kbps)
|
|
|
+ console.log('创建MP3编码器,声道数:', channels, '采样率:', sampleRate, '比特率: 128kbps');
|
|
|
+ const mp3Encoder = new Mp3Encoder(channels, sampleRate, 128);
|
|
|
+ const sampleBlockSize = 1152; // MP3编码的块大小
|
|
|
+ const mp3Data = [];
|
|
|
+
|
|
|
+ // 编码音频数据
|
|
|
+ console.log('开始MP3编码...');
|
|
|
+ let processedSamples = 0;
|
|
|
+ const totalSamples = pcm16.length;
|
|
|
+
|
|
|
+ for (let i = 0; i < pcm16.length; i += sampleBlockSize) {
|
|
|
+ const sampleChunk = pcm16.subarray(i, i + sampleBlockSize);
|
|
|
+ const mp3buf = mp3Encoder.encodeBuffer(sampleChunk);
|
|
|
+ if (mp3buf.length > 0) {
|
|
|
+ mp3Data.push(mp3buf);
|
|
|
+ }
|
|
|
+
|
|
|
+ processedSamples += sampleChunk.length;
|
|
|
+ // 每处理10%的数据输出一次进度
|
|
|
+ if (processedSamples % Math.max(1, Math.floor(totalSamples / 10)) < sampleBlockSize) {
|
|
|
+ const progress = Math.round((processedSamples / totalSamples) * 100);
|
|
|
+ console.log(`MP3编码进度: ${progress}%`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 完成编码
|
|
|
+ console.log('完成MP3编码,正在刷新缓冲区...');
|
|
|
+ const mp3buf = mp3Encoder.flush();
|
|
|
+ if (mp3buf.length > 0) {
|
|
|
+ mp3Data.push(mp3buf);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算总大小
|
|
|
+ const totalSize = mp3Data.reduce((sum, buf) => sum + buf.length, 0);
|
|
|
+ console.log('MP3编码完成,总大小:', totalSize, 'bytes');
|
|
|
+
|
|
|
+ // 检查是否有数据
|
|
|
+ if (totalSize === 0) {
|
|
|
+ throw new Error('MP3编码失败:生成的MP3文件为空');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建Blob
|
|
|
+ const mp3Blob = new Blob(mp3Data, { type: 'audio/mpeg' });
|
|
|
+
|
|
|
+ // 创建新的File对象
|
|
|
+ const originalFileName = fileObj.name || (file && file.name) || 'audio';
|
|
|
+ const baseName = originalFileName.replace(/\.[^/.]+$/, ""); // 移除扩展名
|
|
|
+ const mp3File = new File([mp3Blob], `${baseName}.mp3`, { type: 'audio/mpeg' });
|
|
|
+
|
|
|
+ console.log('音频转换成功,输出文件:', mp3File.name, '大小:', mp3File.size, 'bytes', '格式: MP3');
|
|
|
+
|
|
|
+ return mp3File;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('音频转换失败,详细错误:', error);
|
|
|
+ console.error('错误堆栈:', error.stack);
|
|
|
+
|
|
|
+ // 提供更详细的错误信息
|
|
|
+ let errorMessage = '音频转换失败';
|
|
|
+ if (error.message) {
|
|
|
+ errorMessage += `: ${error.message}`;
|
|
|
+ } else {
|
|
|
+ errorMessage += `: ${error}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据错误类型提供建议
|
|
|
+ if (error.name === 'EncodingError' || error.message.includes('decode')) {
|
|
|
+ errorMessage += '。可能是音频文件格式不支持或文件已损坏';
|
|
|
+ } else if (error.message.includes('AudioContext')) {
|
|
|
+ errorMessage += '。浏览器不支持音频处理功能';
|
|
|
+ } else if (error.message.includes('大小') || error.message.includes('size')) {
|
|
|
+ errorMessage += '。请尝试使用较小的音频文件';
|
|
|
+ } else if (error.message.includes('lamejs') || error.message.includes('Mp3Encoder')) {
|
|
|
+ errorMessage += '。请确保已安装 @breezystack/lamejs:npm install @breezystack/lamejs';
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new Error(errorMessage);
|
|
|
+ } finally {
|
|
|
+ // 确保清理AudioContext
|
|
|
+ if (audioContext && audioContext.state !== 'closed') {
|
|
|
+ try {
|
|
|
+ await audioContext.close();
|
|
|
+ console.log('AudioContext已关闭');
|
|
|
+ } catch (closeError) {
|
|
|
+ console.warn('关闭AudioContext时出错:', closeError);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
export default {
|
|
|
components: {
|
|
|
wVideo,
|
|
|
@@ -549,17 +775,43 @@ export default {
|
|
|
// "m4b",
|
|
|
// "m4p"
|
|
|
];
|
|
|
+ const audioExtensions = ["wav", "m4a", "aac", "ogg", "flac", "wma"];
|
|
|
+
|
|
|
|
|
|
const uploadFiles = async files => {
|
|
|
this.pcount = 0
|
|
|
this.ptotal = files.length
|
|
|
for (let cfindex = 0; cfindex < files.length; cfindex++) {
|
|
|
file = files[cfindex];
|
|
|
- const fileExtension = file.name
|
|
|
+ let fileExtension = file.name
|
|
|
.split(".")
|
|
|
.pop()
|
|
|
.toLowerCase();
|
|
|
|
|
|
+ if (audioExtensions.includes(fileExtension)) {
|
|
|
+ try {
|
|
|
+ console.log(this.lang.converting_audio);
|
|
|
+ const convertingMsg = this.lang.converting_audio.replace('{format}', fileExtension.toUpperCase());
|
|
|
+ this.$message.info(convertingMsg);
|
|
|
+ // 使用原生 File 对象进行转换
|
|
|
+ file = await convertAudioToMp3(file);
|
|
|
+ fileExtension = file.name.split(".").pop().toLowerCase();
|
|
|
+ const successMsg = this.lang.convert_success.replace('{filename}', file.name);
|
|
|
+ this.$message.success(successMsg);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('音频转换失败:', error);
|
|
|
+ const errorMsg = error.message || error.toString();
|
|
|
+ const failMsg = this.lang.convert_fail.replace('{error}', errorMsg);
|
|
|
+ this.$message.error({
|
|
|
+ message: failMsg,
|
|
|
+ duration: 5000,
|
|
|
+ showClose: true
|
|
|
+ });
|
|
|
+ // 转换失败时移除文件并跳过
|
|
|
+ await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
if (!allowedExtensions.includes(fileExtension)) {
|
|
|
this.$message.error(`${this.lang.unsupFileformats}: ${file.name}`);
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000)); // 延迟1秒再跳过
|
|
|
@@ -589,7 +841,7 @@ export default {
|
|
|
}
|
|
|
setTimeout(() => {
|
|
|
this.proVisible = false;
|
|
|
- this.$message.success(this,lang.operComplete);
|
|
|
+ this.$message.success(this.lang.operComplete);
|
|
|
this.getData(); // 在上传完所有文件后再调用getData
|
|
|
}, 1000);
|
|
|
};
|