<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      JS mediaDevices 錄音轉文字分片處理并上傳

      需求是手機錄音,并實時轉文字,結束后上傳錄音及支持修改錄音名稱。

       

      遇到的坑:

        1、頁面切換tab或應用,錄音和WebSocket根據不同手機會有斷連的情況

          1:wakeLock保持常亮

          2:監聽visibilitychange,切回后重新連接音頻、WebSocket

        2、音頻格式不滿足第三方api,無法識別

          使用AudioWorkletNode 代替 MediaRecorder

      特性AudioWorkletNodeMediaRecorder
      實時性 極低延遲(<50ms) 較高延遲(100-500ms)
      數據處理能力 完全訪問原始PCM數據 只能獲取編碼后的數據
      開發復雜度 高(需自行編碼/封裝) 低(API簡單)
      輸出格式 需手動實現(如WAV) 瀏覽器預設(如webm)
      內存使用 需自行管理緩沖 自動分塊處理
      適用場景 專業音頻處理/實時效果 簡單錄音/快速實現
      線程模型 專用音頻線程 主線程
      瀏覽器支持 較新瀏覽器 廣泛支持

        3、長時間錄音文件過大

          wav格式大,mp3格式大概是1/7,分片轉化為mp3格式,再分片上傳文件

       

      worklet-processor.js,錄音并轉文字
      //worklet-processor.js
      
      export default class AudioStreamer {
          constructor(obj) {
              this.audioContext = null;
              this.stream = null;
              this.workletNode = null;
              this.ws = null;
              this.wakeLock = null;
              this.analyser = null;
              this.mediaRecorder = null;
              this.audioBuffer = [];
              this.buffer = [];
              this.CHUNK_SIZE = 16000; // 1秒的16kHz音頻數據
              this.size = 0;
              this.inputSampleRate = 0; //輸入采樣率
              this.outputSampleRate = 16000; //輸出采樣率
              this.inputSampleBits = 16; //輸入采樣數位 8, 16
              this.outputSampleBits = 16; //輸出采樣數位 8, 16
              this.wsUrl = obj.wsUrl;
              this.onMessageCallback = null; //消息回調函數
              this.onAudioBlobCallback = null; //完整錄音回調函數
              this.active = true;
              this.timer = null;
              this.audioChunks = [];
              this.readyData = [];
          }
      
          // 初始化音頻流和WebSocket
          async init(onMessageCallback, onAudioBlobCallback) {
              // 存儲回調函數
              this.onMessageCallback = onMessageCallback;
              this.onAudioBlobCallback = onAudioBlobCallback;
              return new Promise(async (resolve, reject) => {
                  try {
                      //0. 請求屏幕保持開啟
                      this.requestWakeLock();
      
                      // 1. 獲取用戶音頻流
                      this.stream = await navigator.mediaDevices.getUserMedia({
                          audio: true
                      });
      
                      // 2. 創建音頻上下文
                      this.audioContext = new(window.AudioContext || window.webkitAudioContext)();
                      this.analyser = this.audioContext.createAnalyser();
                      this.analyser.fftSize = 256;
                      this.inputSampleRate = this.audioContext.sampleRate;
      
                      // 3. 創建WebSocket連接
                      this.connectWebSocket();
      
                      // 4. 注冊并創建AudioWorklet
                      await this.setupAudioWorklet();
      
                      // 5. 連接音頻節點
                      this.connectAudioNodes();
      
                      // 6. 完整音頻錄制保存
                      //this.saveAudioBlob();
      
                      resolve({
                          analyser: this.analyser,
                          ws: this.ws
                      })
                      console.log('Audio streaming initialized');
                  } catch (error) {
                      console.error('Initialization failed:', error);
                  }
              });
      
          }
      
          //重新連接
          async reconnect(wsUrl) {
              //console.log(this.active, '頁面恢復 - 重新連接音頻');
              return new Promise(async (resolve, reject) => {
                  try {
                      //0. 請求屏幕保持開啟
                      this.requestWakeLock();
      
                      //WebSocket斷連
                      if (wsUrl) {
                          this.wsUrl = wsUrl;
                          this.connectWebSocket(true);
                      }
      
                      // 1. 恢復音頻上下文
                      if (this.audioContext.state === 'suspended') {
                          await this.audioContext.resume();
                      }
      
                      // 2. 重新創建 AudioWorkletNode 如果需要
                      if (!this.workletNode || this.workletNode.context !== this
                          .audioContext) {
                          await this.setupAudioWorklet();
                      }
      
                      // 重新連接音頻節點
                      this.connectAudioNodes();
      
                      resolve({
                          ws: this.ws
                      })
                  } catch (error) {
                      console.error('Initialization failed:', error);
                  }
              })
      
          }
      
          //完整音頻錄制保存(頁面切換tab后沒有 AudioWorkletNode 穩定)
          saveAudioBlob() {
              const mediaRecorder = new MediaRecorder(this.stream);
              mediaRecorder.addEventListener("dataavailable", event => {
                  // 暫停處理
                  if (!this.active) {
                      return;
                  }
                  console.log('dataavailable', event.data);
                  this.audioChunks.push(event.data);
              });
              mediaRecorder.addEventListener("stop", async () => {
                  console.log('audioChunks', this.audioChunks)
                  const audioBlob = new Blob(this.audioChunks, {
                      type: 'audio/wav'
                  });
                  if (this.onAudioBlobCallback && !this.active) {
                      const buffer = await audioBlob.arrayBuffer();
                      const decodedData = await this.audioContext.decodeAudioData(buffer);
                      this.onAudioBlobCallback({
                          audioBlob,
                          duration: decodedData.duration
                      });
                  }
              });
              mediaRecorder.start(100);
              this.mediaRecorder = mediaRecorder;
          }
      
          // 請求屏幕保持開啟
          async requestWakeLock() {
              let wakeLock = null;
              try {
                  // 檢查API是否可用
                  if ('wakeLock' in navigator) {
                      wakeLock = await navigator.wakeLock.request('screen');
                      console.log('屏幕保持常亮已激活');
                      this.wakeLock = wakeLock;
                  } else {
                      console.warn('Wake Lock API 不支持');
                  }
              } catch (err) {
                  console.error(`無法保持屏幕常亮: ${err.message}`);
              }
      
          }
      
          // 連接WebSocket
          connectWebSocket(isReconnect) {
              this.ws = new WebSocket(this.wsUrl);
              this.ws.binaryType = 'arraybuffer';
      
              this.ws.onopen = () => {
                  console.log('WebSocket connection established');
                  // 發送開始幀
                  const startFrame = {
                      header: {
                          name: "StartTranscription",
                          namespace: "SpeechTranscriber",
                      },
                      payload: {
                          format: "pcm"
                      }
                  };
                  this.ws.send(JSON.stringify(startFrame));
                  //重連,發送斷開期間暫存音頻數據
                  if (isReconnect) {
                      this.readyData.forEach((data) => {
                          this.ws.send(data);
                      })
                      this.readyData = [];
                  }
              };
      
              this.ws.onmessage = (msg) => {
                  // msg.data 是接收到的數據(具體參考「實時推流返回事件」)
                  // 根據業務處理數據
                  if (typeof msg.data === "string") {
                      const dataJson = JSON.parse(msg.data);
                      switch (dataJson.header.name) {
                          case "SentenceBegin": {
                              // 句子開始事件
                              console.log("句子", dataJson.payload.index, "開始");
                              break;
                          }
                          case "TranscriptionResultChanged":
                              // 句中識別結果變化事件
                              // console.log(
                              //     "句子" + dataJson.payload.index + "中間結果:",
                              //     dataJson.payload.result
                              // );
                              if (this.onMessageCallback) {
                                  this.onMessageCallback({
                                      ...dataJson.payload,
                                      name: 'TranscriptionResultChanged'
                                  });
                              }
                              break;
      
                          case "SentenceEnd": {
                              // 句子結束事件
                              console.log(
                                  "句子" + dataJson.payload.index + "結束:",
                                  dataJson.payload.result + dataJson.payload.stash_result.text
                              );
                              if (this.onMessageCallback) {
                                  this.onMessageCallback({
                                      ...dataJson.payload,
                                      name: 'SentenceEnd'
                                  });
                              }
                              break;
                          }
                          case "ResultTranslated": {
                              // 識別結果翻譯事件
                              console.log(
                                  "句子翻譯結果",
                                  JSON.stringify(dataJson.payload.translate_result)
                              );
                              break;
                          }
                          //... 
                      }
                  }
              };
              this.ws.onclose = (event) => {
                  console.log(`WebSocket連接關閉: ${event.code} ${event.reason}`);
                  //alert('Wsclose' + event.code);
              };
              this.ws.onerror = (error) => {
                  console.error('WebSocket error:', error);
                  //alert('Wserror' + error);
              };
          }
      
          // 設置AudioWorklet
          async setupAudioWorklet() {
              // 內聯方式注冊AudioWorkletProcessor
              const workletCode = `
            class AudioStreamProcessor extends AudioWorkletProcessor {
              process(inputs, outputs, parameters) {
                  const inputData = inputs[0][0]; // 獲取單聲道數據
                  this.port.postMessage(inputData);
                  return true;
              }
            }
            registerProcessor('audio-stream-processor', AudioStreamProcessor);
          `;
      
              // 創建Blob URL方式加載Worklet
              const blob = new Blob([workletCode], {
                  type: 'application/javascript'
              });
              const blobUrl = URL.createObjectURL(blob);
      
              try {
                  // 注冊AudioWorklet
                  await this.audioContext.audioWorklet.addModule(blobUrl);
      
                  // 創建Worklet節點
                  this.workletNode = new AudioWorkletNode(
                      this.audioContext,
                      'audio-stream-processor'
                  );
      
                  // 處理接收到的音頻數據
                  this.workletNode.port.onmessage = (e) => {
                      // 暫停處理
                      if (!this.active) {
                          return;
                      }
                      this.handleAudioData(e.data);
                  };
      
              } finally {
                  URL.revokeObjectURL(blobUrl); // 清理Blob URL
              }
          }
      
          // 連接音頻節點
          connectAudioNodes() {
              const sourceNode = this.audioContext.createMediaStreamSource(this.stream);
              sourceNode.connect(this.workletNode);
              sourceNode.connect(this.analyser);
              this.workletNode.connect(this.audioContext.destination);
          }
      
          // 處理音頻數據
          handleAudioData(float32Data) {
              // 添加到緩沖區
              this.audioBuffer = this.audioBuffer.concat(Array.from(float32Data));
      
              // 檢查是否達到發送閾值
              if (this.audioBuffer.length >= this.CHUNK_SIZE) {
                  const chunk = this.audioBuffer.slice(0, this.CHUNK_SIZE);
                  this.audioBuffer = this.audioBuffer.slice(this.CHUNK_SIZE);
      
                  // 轉換為16位PCM
                  //onst int16Data = this.floatTo16BitPCM(chunk);
                  //暫存完整錄制
                  this.audioChunks.push(new Float32Array(chunk));
                  // 轉換為PCM
                  this.buffer.push(new Float32Array(chunk));
                  this.size += chunk.length;
                  const int16Data = this.encodePCM();
                  //console.log('onmessage');
                  // 通過WebSocket發送
                  if (this.ws.readyState === WebSocket.OPEN) {
                      this.ws.send(int16Data);
                  } else {
                      this.readyData.push(int16Data);
                  }
                  this.clear();
              }
          }
          compress() { //對數據 進行 合并壓縮
              var data = new Float32Array(this.size);
              var offset = 0;
              for (var i = 0; i < this.buffer.length; i++) {
                  data.set(this.buffer[i], offset);
                  offset += this.buffer[i].length;
              }
              var compression = parseInt(this.inputSampleRate / this.outputSampleRate);
              var length = data.length / compression;
              var result = new Float32Array(length);
              var index = 0,
                  j = 0;
              while (index < length) {
                  result[index] = data[j];
                  j += compression;
                  index++;
              }
              return result;
          }
      
          encodePCM() {
              var sampleRate = Math.min(this.inputSampleRate,
                  this.outputSampleRate);
              var sampleBits = Math.min(this.inputSampleBits,
                  this.outputSampleBits);
              var bytes = this.compress();
              var dataLength = bytes.length * (sampleBits / 8);
              var buffer = new ArrayBuffer(dataLength);
              var data = new DataView(buffer);
              var offset = 0;
              for (var i = 0; i < bytes.length; i++, offset += 2) {
                  var s = Math.max(-1, Math.min(1, bytes[i]));
                  data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
              }
              return data.buffer;
          }
      
          clear() {
              this.buffer = [];
              this.size = 0;
          }
      
          // 浮點轉16位PCM
          floatTo16BitPCM(input) {
              const output = new Int16Array(input.length);
              for (let i = 0; i < input.length; i++) {
                  const s = Math.max(-1, Math.min(1, input[i]));
                  output[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
              }
              return output.buffer;
          }
      
          // 輔助函數:合并 Float32Array
          mergeArrays(arrays) {
              let totalLength = arrays.reduce((acc, value) => acc + value.length, 0);
              let result = new Float32Array(totalLength);
              let offset = 0;
              for (let array of arrays) {
                  result.set(array, offset);
                  offset += array.length;
              }
              return result;
          }
          // 輔助函數:將 Float32Array 轉為 WAV Blob
          encodeWAV(samples) {
              const buffer = new ArrayBuffer(44 + samples.length * 2);
              const view = new DataView(buffer);
      
              // WAV 文件頭
              this.writeString(view, 0, 'RIFF');
              view.setUint32(4, 36 + samples.length * 2, true);
              this.writeString(view, 8, 'WAVE');
              this.writeString(view, 12, 'fmt ');
              view.setUint32(16, 16, true); // chunk length
              view.setUint16(20, 1, true); // PCM format
              view.setUint16(22, 1, true); // mono
              view.setUint32(24, this.inputSampleRate, true);
              view.setUint32(28, this.inputSampleRate * 2, true); // byte rate
              view.setUint16(32, 2, true); // block align
              view.setUint16(34, 16, true); // bits per sample
              this.writeString(view, 36, 'data');
              view.setUint32(40, samples.length * 2, true);
      
              // 寫入 PCM 數據
              this.setFloatTo16BitPCM(view, 44, samples);
      
              return new Blob([view], {
                  type: 'audio/wav'
              });
          }
      
          setFloatTo16BitPCM(output, offset, input) {
              for (let i = 0; i < input.length; i++, offset += 2) {
                  const s = Math.max(-1, Math.min(1, input[i]));
                  output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
              }
          }
      
          writeString(view, offset, string) {
              for (let i = 0; i < string.length; i++) {
                  view.setUint8(offset + i, string.charCodeAt(i));
              }
          }
      
          // 停止音頻流
          async stop() {
              this.active = false;
              if (this.workletNode) {
                  this.workletNode.disconnect();
              }
              if (this.mediaRecorder) {
                  this.mediaRecorder.stop();
              }
              if (this.stream) {
                  this.stream.getTracks().forEach(track => track.stop());
              }
              if (this.wakeLock !== null && !this.wakeLock.released) {
                  this.wakeLock.release();
                  this.wakeLock = null;
              }
              const params = {
                  header: {
                      name: 'StopTranscription',
                      namespace: 'SpeechTranscriber',
                  },
                  payload: {},
              };
              this.ws.send(JSON.stringify(params));
              setTimeout(() => {
                  if (this.ws) {
                      this.ws.close();
                      this.ws = null;
                  }
              }, 1000);
              console.log('Audio streaming stopped');
      
              //回調保存完整錄音
              if (this.onAudioBlobCallback && !this.active) {
                  // 合并所有音頻塊
                  const mergedArray = this.mergeArrays(this.audioChunks);
      
                  // 轉換為 WAV
                  const audioBlob = this.encodeWAV(mergedArray);
                  const buffer = await audioBlob.arrayBuffer();
                  const decodedData = await this.audioContext.decodeAudioData(buffer);
                  this.onAudioBlobCallback({
                      audioBlob,
                      duration: decodedData.duration
                  });
              }
          }
          //暫停、繼續
          setActive(val) {
              this.active = val;
              if (!val) {
                  // 保持鏈接,創建一個長度為 10 的空 Buffer,默認用 0 填充
                  const emptyBuffer = Buffer.alloc(10);
                  this.timer = setInterval(() => {
                      this.ws && this.ws.send(emptyBuffer)
                  }, 1000);
              } else {
                  if (this.timer) {
                      clearInterval(this.timer);
                      this.timer = null;
                  }
              }
          }
      }
      View Code

       



      chunked-uploader.js 分片轉mp3、上傳
        
      lamejs 使用1.2.0,1.2.1版會報錯
      import {
          uploadPartTask,
      } from "@/api/common.api.js";
      import lamejs from 'lamejs';
      export default class ChunkedUploader {
          constructor(file, options = {}) {
              this.file = file;
              this.chunkSizeMB = options.chunkSizeMB || 5; // 5MB
              this.retries = options.retries || 3;
              this.concurrent = options.concurrent || 3;
              this.chunks = [];
              this.totalSize = 0;
              this.uploaded = 0;
              this.taskId = '';
              this.key = '';
              this.onSplitProgress = options.onSplitProgress;
              this.onUploadProgress = options.onUploadProgress;
              this.authorization = uni.getStorageSync('auth');
          }
      
          async upload() {
              const mp3Chunks = await this.compressWavToMp3(this.file, 0.3, this.onSplitProgress, this.chunkSizeMB);
              this.chunks = mp3Chunks.map((blob, index) => {
                  return {
                      id: `${index}-${Date.now()}`,
                      index: index + 1,
                      blob,
                      retry: 0,
                      status: 'pending'
                  }
              })
              try {
                  //開始分片上傳附件任務
                  const {
                      data
                  } = await uploadPartTask();
                  this.taskId = data.taskId;
                  this.key = data.key;
      
                  // 并發控制
                  const queue = [];
                  for (let i = 0; i < this.chunks.length; i += this.concurrent) {
                      const chunks = this.chunks.slice(i, i + this.concurrent);
                      const results = await Promise.all(chunks.map(chunk => this.processQueue(chunk)));
                      queue.push(...results);
                  }
                  return queue;
              } catch (err) {
                  throw new Error(err);
              }
          }
      
          async processQueue(chunk) {
              chunk.status = 'uploading';
              try {
                  const data = await this.uploadChunk(chunk);
                  chunk.status = 'completed';
                  this.uploaded++;
                  this.onProgress();
                  return data;
              } catch (err) {
                  if (chunk.retry++ < this.retries) {
                      chunk.status = 'pending';
                      return this.processQueue(chunk);
                  } else {
                      chunk.status = 'failed';
                      throw new Error(`Chunk ${chunk.index} failed after ${this.retries} retries`);
                  }
              }
          }
      
          uploadChunk(chunk) {
              return new Promise(async (resolve, reject) => {
                  uni.uploadFile({
                      url: `/uploadPartFiles?taskId=${this.taskId}&key=${this.key}&index=${chunk.index}`,
                      file: chunk.blob,
                      name: 'file',
                      header: {
                          authorization: this.authorization
                      },
                      success: (uploadFileRes) => {
                          if (uploadFileRes.statusCode === 200) {
                              const data = JSON.parse(uploadFileRes.data);
                              resolve(data.partETag);
                          } else {
                              reject('error');
                          }
                      },
                  });
              })
          }
      
          onProgress() {
              const progress = Math.round((this.uploaded / this.chunks.length) * 100);
              console.log(`上傳進度: ${progress}%`);
              // 觸發UI更新事件...
              this.onUploadProgress(progress);
          }
      
          /**
           * 將WAV音頻分片壓縮為MP3
           * @param {Blob} wavBlob - 輸入的WAV音頻Blob對象
           * @param {number} quality - 壓縮質量 (0.1~1.0)
           * @param {function} onProgress - 進度回調 (0~100)
           * @param {number} chunkSizeMB - 分片大小 (單位MB,默認5MB)
           * @returns {Promise<Blob>} - 返回MP3格式的Blob
           */
          async compressWavToMp3(wavBlob, quality = 0.7, onProgress, chunkSizeMB = 5) {
              // 1. 初始化參數
              //const chunkSize = chunkSizeMB * 1024 * 1024; // 轉為字節
              //const totalChunks = Math.ceil(wavBlob.size / chunkSize);
              const mp3Chunks = [];
              let processedChunks = 0;
      
              const chunks = await this.splitWavBySize(wavBlob, chunkSizeMB);
      
              // 2. 分片處理函數
              const processChunk = async (blob, end) => {
                  // 1. 解碼WAV(增加錯誤處理)
                  const audioCtx = new(window.AudioContext || window.webkitAudioContext)();
                  let audioBuffer;
                  try {
                      const arrayBuffer = await blob.arrayBuffer();
                      audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
                  } catch (err) {
                      console.error("WAV解碼失敗:", err);
                      throw new Error("無效的WAV文件");
                  }
      
      
                  // 3. 準備編碼器(確保參數匹配)
                  //  動態計算比特率 (32kbps~320kbps)
                  const bitrate = Math.floor(32 + (320 - 32) * quality);
                  const mp3Encoder = new lamejs.Mp3Encoder(
                      audioBuffer.numberOfChannels,
                      audioBuffer.sampleRate,
                      bitrate // 比特率(kbps)
                  );
      
                  // 4. 獲取PCM數據(兼容單聲道)
                  const leftChannel = audioBuffer.getChannelData(0);
                  const rightChannel = audioBuffer.numberOfChannels > 1 ?
                      audioBuffer.getChannelData(1) :
                      leftChannel;
      
                  // 5. 轉換到16位整數(關鍵修復!)
                  const convertTo16Bit = (float32) => {
                      const int16 = new Int16Array(float32.length);
                      for (let i = 0; i < float32.length; i++) {
                          // 重要:必須先縮放再取整!
                          int16[i] = Math.min(32767, Math.max(-32768, float32[i] * 32767));
                      }
                      return int16;
                  };
      
                  const left = convertTo16Bit(leftChannel);
                  const right = convertTo16Bit(rightChannel);
      
                  // 6. 分塊編碼(調整塊大小)
                  const SAMPLE_BLOCK = 1152; // MP3幀標準樣本數
                  const mp3Data = [];
      
                  for (let i = 0; i < left.length; i += SAMPLE_BLOCK) {
                      let leftChunk = left.subarray(i, i + SAMPLE_BLOCK);
                      let rightChunk = right.subarray(i, i + SAMPLE_BLOCK);
      
                      // 確保塊大小一致(補零處理)
                      if (leftChunk.length < SAMPLE_BLOCK) {
                          const paddedLeft = new Int16Array(SAMPLE_BLOCK).fill(0);
                          paddedLeft.set(leftChunk);
                          leftChunk = paddedLeft;
      
                          const paddedRight = new Int16Array(SAMPLE_BLOCK).fill(0);
                          paddedRight.set(rightChunk);
                          rightChunk = paddedRight;
                      }
      
                      const mp3buf = mp3Encoder.encodeBuffer(leftChunk, rightChunk);
                      if (mp3buf.length > 0) {
                          mp3Data.push(mp3buf);
                          //console.log(`編碼塊 ${i/SAMPLE_BLOCK}: ${mp3buf.length}字節`);
                      }
                  }
      
                  // 7. 結束編碼(關鍵步驟)
                  const lastChunk = mp3Encoder.flush();
                  if (lastChunk.length > 0) {
                      mp3Data.push(lastChunk);
                      //console.log("最終塊:", lastChunk.length, "字節");
                  }
      
                  // 8. 驗證輸出
                  if (mp3Data.length === 0) {
                      throw new Error("編碼器未產生任何數據");
                  }
      
                  const totalSize = mp3Data.reduce((sum, buf) => sum + buf.length, 0);
                  this.totalSize += totalSize;
                  //console.log("總MP3大小:", totalSize, "bytes");
      
                  // 9. 更新進度
                  processedChunks++;
                  const progress = Math.floor((processedChunks / chunks.length) * 100);
                  if (onProgress) onProgress(progress);
      
                  return new Blob(mp3Data, {
                      type: 'audio/mp3'
                  });
              };
      
              // 8. 并行處理所有分片(限制并發數)
              const MAX_CONCURRENT = 3; // 最大并發數
      
              // 9. 分批次處理(避免內存爆炸)
              for (let i = 0; i < chunks.length; i += MAX_CONCURRENT) {
                  const batch = chunks.slice(i, i + MAX_CONCURRENT);
                  const results = await Promise.all(batch.map(chunk => processChunk(chunk)));
                  mp3Chunks.push(...results);
              }
              return mp3Chunks;
      
              // 10. 合并所有分片
              // return new Blob(mp3Chunks, {
              //     type: 'audio/mp3'
              // });
          }
      
          /**
           * 按文件大小切分WAV文件(保持WAV頭完整)
           * @param {Blob} wavBlob - 原始WAV文件
           * @param {number} chunkSizeMB - 每個分片的大小(MB)
           * @return {Promise<Array<Blob>>} - 切分后的WAV片段數組
           */
          async splitWavBySize(wavBlob, chunkSizeMB = 10) {
              const CHUNK_SIZE = chunkSizeMB * 1024 * 1024;
              const arrayBuffer = await wavBlob.arrayBuffer();
              const header = this.parseWavHeader(arrayBuffer);
      
              // 確保切分點在數據塊邊界上
              const bytesPerSample = header.bitsPerSample / 8;
              const bytesPerFrame = bytesPerSample * header.numChannels;
              const dataStart = header.dataOffset;
              const dataSize = header.dataSize;
      
              // 計算合理的切分點
              const chunkSize = Math.max(
                  CHUNK_SIZE - (CHUNK_SIZE % bytesPerFrame),
                  bytesPerFrame * header.sampleRate // 至少1秒音頻
              );
      
              const chunks = [];
              let offset = dataStart;
      
              while (offset < dataStart + dataSize) {
                  const chunkEnd = Math.min(offset + chunkSize, dataStart + dataSize);
                  const chunkDataSize = chunkEnd - offset;
      
                  // 創建新的WAV頭
                  const newHeader = this.createWavHeader(
                      header.numChannels,
                      header.sampleRate,
                      header.bitsPerSample,
                      chunkDataSize / bytesPerFrame
                  );
      
                  // 合并頭和音頻數據
                  const chunkArray = new Uint8Array(newHeader.byteLength + chunkDataSize);
                  chunkArray.set(new Uint8Array(newHeader), 0);
                  chunkArray.set(
                      new Uint8Array(arrayBuffer, offset, chunkDataSize),
                      newHeader.byteLength
                  );
      
                  chunks.push(new Blob([chunkArray], {
                      type: 'audio/wav'
                  }));
                  offset = chunkEnd;
              }
      
              return chunks;
          }
      
          // 解析WAV頭信息
          parseWavHeader(arrayBuffer) {
              const view = new DataView(arrayBuffer);
              let offset = 0;
      
              const chunkID = this.readString(view, offset, 4);
              offset += 4;
              const chunkSize = view.getUint32(offset, true);
              offset += 4;
              const format = this.readString(view, offset, 4);
              offset += 4;
      
              // 查找fmt塊
              let fmtOffset = 12;
              while (fmtOffset < view.byteLength) {
                  const subchunkID = this.readString(view, fmtOffset, 4);
                  const subchunkSize = view.getUint32(fmtOffset + 4, true);
      
                  if (subchunkID === 'fmt ') {
                      offset = fmtOffset + 8;
                      break;
                  }
      
                  fmtOffset += 8 + subchunkSize;
              }
      
              const audioFormat = view.getUint16(offset, true);
              offset += 2;
              const numChannels = view.getUint16(offset, true);
              offset += 2;
              const sampleRate = view.getUint32(offset, true);
              offset += 4;
              const byteRate = view.getUint32(offset, true);
              offset += 4;
              const blockAlign = view.getUint16(offset, true);
              offset += 2;
              const bitsPerSample = view.getUint16(offset, true);
              offset += 2;
      
              // 查找data塊
              let dataOffset = fmtOffset;
              while (dataOffset < view.byteLength) {
                  const subchunkID = this.readString(view, dataOffset, 4);
                  const subchunkSize = view.getUint32(dataOffset + 4, true);
      
                  if (subchunkID === 'data') {
                      return {
                          dataOffset: dataOffset + 8,
                          dataSize: subchunkSize,
                          numChannels,
                          sampleRate,
                          bitsPerSample,
                          byteRate,
                          blockAlign,
                          audioFormat
                      };
                  }
      
                  dataOffset += 8 + subchunkSize;
              }
      
              throw new Error('Invalid WAV file: data chunk not found');
          }
      
          readString(view, offset, length) {
              let str = '';
              for (let i = 0; i < length; i++) {
                  str += String.fromCharCode(view.getUint8(offset + i));
              }
              return str;
          }
      
          // 輔助函數:創建WAV頭
          createWavHeader(numChannels, sampleRate, bitDepth, numSamples) {
              const byteRate = (sampleRate * numChannels * bitDepth) / 8;
              const blockAlign = (numChannels * bitDepth) / 8;
              const dataSize = numSamples * numChannels * (bitDepth / 8);
      
              const buffer = new ArrayBuffer(44);
              const view = new DataView(buffer);
      
              // RIFF標識
              this.writeString(view, 0, 'RIFF');
              view.setUint32(4, 36 + dataSize, true);
              this.writeString(view, 8, 'WAVE');
              // fmt子塊
              this.writeString(view, 12, 'fmt ');
              view.setUint32(16, 16, true); // 子塊大小
              view.setUint16(20, 1, true); // PCM格式
              view.setUint16(22, numChannels, true);
              view.setUint32(24, sampleRate, true);
              view.setUint32(28, byteRate, true);
              view.setUint16(32, blockAlign, true);
              view.setUint16(34, bitDepth, true);
              // data子塊
              this.writeString(view, 36, 'data');
              view.setUint32(40, dataSize, true);
      
              return buffer;
          }
      
          writeString(view, offset, string) {
              for (let i = 0; i < string.length; i++) {
                  view.setUint8(offset + i, string.charCodeAt(i));
              }
          }
      
      }
      View Code

       

      調用:(canvas 屬性寬高要2倍與css,否則會模糊)

      //頁面可見性變化處理
                  visibilitychange() {
                      document.addEventListener('visibilitychange', () => {
                          if (!document.hidden && this.isRecording && this.task.id) {
                              console.log('show---');
                              // 檢查WebSocket狀態
                              if (this.ws.readyState === WebSocket.CLOSED) {
                                  //清空上個任務的中間狀態
                                  this.dataList.forEach((item) => {
                                      if (item.isChange) {
                                          item.isChange = false;
                                      }
                                  })
      
                                  //重新創建新任務
                                  getRestartVoiceWs({
                                      id: this.task.id
                                  }, async (res) => {
                                      console.log('task-------', res.data);
                                      this.task = res.data;
                                      const {
                                          ws
                                      } = await this.audioStreamer.reconnect(res.data.ws);
                                      this.ws = ws;
                                  })
                              } else {
                                  this.audioStreamer.reconnect();
                              }
                          } else if (document.hidden) {
                              console.log('hedden---');
                              //this.ws.close(); //模擬斷連
                          }
                      });
                  },
                  //請求麥克風權限并開始錄音
                  startRecording() {
                      if (!this.task.id) {
                          getVoiceWs({}, async (res) => {
                              this.isRecording = true;
                              this.task = res.data;
                              const audioStreamer = new AudioStreamer({
                                  wsUrl: res.data.ws,
                              });
                              this.audioStreamer = audioStreamer;
                              const worklet = await audioStreamer.init(this.messageHandle, this.audioBlobHandle);
                              this.analyser = worklet.analyser;
                              this.ws = worklet.ws;
                              // 開始動畫
                              this.visualize();
      
                              this.timer = setInterval(() => {
                                  this.duration += 1;
                              }, 1000);
                          })
                      } else {
                          this.isRecording = true;
                          this.audioStreamer.setActive(true);
                          this.timer = setInterval(() => {
                              this.duration += 1;
                          }, 1000);
                          // 開始動畫
                          this.visualize();
                      }
                  },
                  // 暫停錄音
                  stopRecording() {
                      this.isRecording = false;
                      if (this.timer) {
                          clearInterval(this.timer);
                          this.timer = null;
                      }
                      cancelAnimationFrame(this.animationId);
                      this.audioStreamer.setActive(false);
                  },
                  messageHandle(msg) {
                      if (msg.name === 'TranscriptionResultChanged') {
                          //中間結果
                          const index = this.dataList.findIndex((item) => item.isChange && item.index === msg
                              .index);
                          if (index >= 0) {
                              this.resultChangedHandle(msg.result);
                          } else {
                              this.dataList.push({
                                  isChange: true,
                                  index: msg.index,
                                  speakerId: msg.speaker_id || 0,
                                  content: msg.result,
                                  duration: util.secondsToHMS(0, false),
                                  bg: this.colors[Number(msg.speaker_id || 0)] || ''
                              });
                          }
                      } else {
                          this.lineBuffer = '';
                          //完整句子結果
                          //刪除上個中間值
                          const index = this.dataList.findIndex((item) => item.isChange && item.index === msg
                              .index);
                          index >= 0 && this.dataList.splice(index, 1);
                          this.dataList.push({
                              index: msg.index,
                              speakerId: msg.speaker_id,
                              content: msg.result,
                              duration: util.secondsToHMS(parseInt((msg.time - msg.begin_time) / 1000), false),
                              bg: this.colors[Number(msg.speaker_id)] || ''
                          });
                      }
      
                      // 將聊天框的滾動條滑動到最底部
                      this.$nextTick(() => {
                          const box = document.getElementById('chat-box');
                          box.scrollTo(0, box.scrollHeight)
                      })
                  },
                  resultChangedHandle(newText) {
                      let lastReceived = this.dataList.find((item) => item.isChange);;
                      let lastReceivedText = this.lineBuffer;
                      if (lastReceivedText === '一') {
                          lastReceivedText = '';
                      }
                      // 找出新增的字符
                      let newCharacters = '';
                      for (let i = 0; i < newText.length; i++) {
                          if (newText[i] !== lastReceivedText[i]) {
                              newCharacters = newText.substring(i);
                              lastReceivedText = lastReceivedText.substring(0, i);
                              break;
                          }
                      }
      
                      // 如果有新增字符,追加字符
                      if (newCharacters) {
                          this.lineBuffer = lastReceivedText + newCharacters;
                          lastReceived.content = this.lineBuffer;
                      }
                  },
                  async audioBlobHandle(data) {
                      this.task.duration = data.duration;
                      const uploader = new ChunkedUploader(data.audioBlob, {
                          chunkSizeMB: 50, // 10MB
                          //concurrent: 1,
                          onSplitProgress(progress) {
                              uni.showLoading({
                                  title: `錄音轉碼中${progress}%`
                              });
                          },
                          onUploadProgress(progress) {
                              uni.showLoading({
                                  title: `上傳錄音中${progress}%`
                              });
                          },
                      });
                      uploader.upload().then((partETags) => {
                          // 通知服務器合并分片
                          overUploadPartFiles({
                              taskId: uploader.taskId,
                              key: uploader.key,
                              partETags,
                              fileName: moment().format('YYYY-MM-DD HH:mm:ss') + '.mp3',
                              fileSize: uploader.totalSize
                          }, res => {
                              const data = JSON.parse(res.data);
                              uni.hideLoading();
                              this.task.voiceUrl = data.url;
                              this.saveHandle();
                          })
                      });
                  },
                  save() {
                      uni.showLoading({
                          title: `加載中`
                      });
                      this.isRecording && this.stopRecording();
                      this.audioStreamer.stop();
                  },
                  saveHandle() {
                      this.task.voiceTitle = moment().format('YYYY-MM-DD HH:mm:ss');
                      this.showModal = true;
                  },
                  // 可視化動畫
                  visualize(isInit) {
                      const _this = this;
                      const canvas = document.querySelector('.canvas');
                      // 獲取音頻數據
                      const bufferLength = this.analyser?.frequencyBinCount;
                      const dataArray = new Uint8Array(bufferLength);
      
                      // 動畫函數
                      function draw() {
                          _this.animationId = requestAnimationFrame(draw);
      
                          // 清空畫布
                          _this.ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
                          _this.ctx.fillRect(0, 0, canvas.width, canvas.height);
      
                          // 獲取頻率數據
                          _this.analyser.getByteFrequencyData(dataArray);
      
                          // 繪制
                          drawCenteredBars(dataArray, bufferLength);
      
                      }
                      if (isInit) {
                          const emptyBuffer = Buffer.alloc(10);
                          drawCenteredBars(emptyBuffer, 10);
                      } else {
                          draw();
                      }
      
                      // 繪制柱狀圖效果
                      function drawCenteredBars(dataArray, bufferLength, displayBars = 6) {
                          const centerX = canvas.width / 2;
                          const maxBarWidth = 15;
                          const minGap = 3;
      
                          // 動態計算柱寬和間隙
                          const barWidth = Math.min(maxBarWidth,
                              (canvas.width - minGap * (displayBars - 1)) / displayBars);
                          const gap = Math.max(minGap,
                              (canvas.width - barWidth * displayBars) / (displayBars - 1));
      
                          const totalWidth = (barWidth + gap) * displayBars - gap;
                          const startX = centerX - totalWidth / 2;
      
                          _this.ctx.clearRect(0, 0, canvas.width, canvas.height);
      
                          // 只繪制部分柱子(提高性能)
                          const step = Math.max(1, Math.floor(bufferLength / displayBars));
      
                          for (let i = 0; i < displayBars; i++) {
                              const dataIndex = Math.min(i * step, bufferLength - 1);
                              const barHeight = Math.max(2, (dataArray[dataIndex] / 255) * canvas.height * 0.8);
                              const x = startX + i * (barWidth + gap);
                              const y = (canvas.height - barHeight) / 2;
      
                              // 漸變填充
                              //const gradient = _this.ctx.createLinearGradient(x, y, x, canvas.height);
                              // //gradient.addColorStop(0, getBarColor(i, displayBars, dataArray[dataIndex]));
                              //gradient.addColorStop(0, '#1F65FF');
                              //gradient.addColorStop(1, 'rgba(0,0,0,0.7)');
      
                              _this.ctx.fillStyle = '#1F65FF';
                              _this.ctx.fillRect(x, y, barWidth, barHeight);
      
                              // 高光效果
                              _this.ctx.fillStyle = 'rgba(255,255,255,0.2)';
                              _this.ctx.fillRect(x + 1, y + 1, barWidth - 2, 2);
                          }
                      }
                      // 輔助函數:獲取柱子顏色
                      function getBarColor(index, total, value) {
                          const hue = (index / total) * 360;
                          const saturation = 100;
                          const lightness = 30 + (value / 255) * 40;
                          return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
                      }
                  },
              

      總結:deepseek很好用,基本都是問它,不過有些方法不知道是不是瞎編的,看著很有邏輯,但是處理的音頻是損壞的,得換個角度單獨問

       
      posted @ 2025-07-11 15:38  Jade_g  閱讀(61)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 色噜噜亚洲男人的天堂| 国产一区日韩二区三区| 久久久这里只有精品10| 亚洲国产成人无码电影| 玩弄美艳馊子高潮无码| 欧美高清狂热视频60一70| 无码人妻丰满熟妇区96| 亚洲五月丁香综合视频| 一卡二卡三卡四卡视频区| 日本欧美大码aⅴ在线播放| 色成人精品免费视频| 亚洲国产精品一区二区久| 日本亚洲色大成网站www久久| 高清无码18| 激情亚洲专区一区二区三区| 亚洲第一无码专区天堂| 日本一区二区三区在线播放| 国产精品国产三级国产专业 | 国产精品一区中文字幕| 欧美猛少妇色xxxxx猛叫| 99国产精品永久免费视频| 国产精品疯狂输出jk草莓视频| 黑人巨大av无码专区| 国产午夜福利免费入口| 一个色综合国产色综合| 无码专区一va亚洲v专区在线| 久久人人爽人人爽人人av| 国产一区二区三区在线看| 四虎国产精品成人免费久久| 日韩精品国产二区三区| 日韩熟女乱综合一区二区| 久久这里都是精品一区| 亚洲码国产精品高潮在线| 亚洲av永久无码精品水牛影视| 中文字幕亚洲一区二区va在线 | 少妇高潮太爽了在线视频| 综合欧美视频一区二区三区| 亚洲天堂av在线免费看| 久热这里只有精品12| 一本大道av人久久综合| 被拉到野外强要好爽|