UNPKG

mstf-kit

Version:

一个现代化的 JavaScript/TypeScript 工具库,提供了丰富的常用工具函数

446 lines (445 loc) 13.9 kB
import { createLogger, LogLevel } from "./logger.js"; function initAudioContext() { const AudioContextClass = window.AudioContext || window.webkitAudioContext; return { audioCtx: new AudioContextClass(), currentSource: null, isPlaying: false, audioQueue: [], wavHeader: null, previousChunk: "", isFirstChunk: true, audioChunks: [], hasStartedPlaying: false, detectedFormat: null }; } function stringToUint8Array(str) { const arr = new Uint8Array(str.length); for (let i = 0; i < str.length; i++) { arr[i] = str.charCodeAt(i); } return arr; } function base64ToUint8Array(base64) { try { const binaryString = atob(base64); return stringToUint8Array(binaryString); } catch (error) { console.error("Base64解码错误:", error); return new Uint8Array(0); } } function hasWavHeader(data) { if (data.length < 4) return false; const header = String.fromCharCode(...data.slice(0, 4)); return header === "RIFF"; } function extractWavHeader(data) { if (data.length < 44) return null; if (!hasWavHeader(data)) return null; return data.slice(0, 44); } function detectAudioFormat(data, context) { if (context.detectedFormat) { return context.detectedFormat; } if (hasWavHeader(data)) { return "auto"; } else { if (!context.isFirstChunk) { context.detectedFormat = "without-header"; return "without-header"; } return "auto"; } } function resetAudioContext(context) { if (context.currentSource) { try { context.currentSource.stop(); } catch (e) { } context.currentSource = null; } context.isPlaying = false; context.audioQueue = []; context.wavHeader = null; context.previousChunk = ""; context.isFirstChunk = true; context.audioChunks = []; context.detectedFormat = null; } async function playAudio(audioBuffer, context, onStart, onEnded) { return new Promise((resolve) => { if (context.isPlaying) { context.audioQueue.push(audioBuffer); resolve(); return; } const source = context.audioCtx.createBufferSource(); source.buffer = audioBuffer; source.connect(context.audioCtx.destination); source.onended = () => { context.isPlaying = false; context.currentSource = null; if (context.audioQueue.length === 0 && onEnded) { onEnded(); } resolve(); playNextInQueue(context, onStart, onEnded); }; context.currentSource = source; context.isPlaying = true; source.start(0); if (onStart && !context.hasStartedPlaying) { context.hasStartedPlaying = true; onStart(); } }); } async function playNextInQueue(context, onStart, onEnded) { if (context.audioQueue.length > 0 && !context.isPlaying) { const nextAudio = context.audioQueue.shift(); await playAudio(nextAudio, context, onStart, onEnded); } } function createAudioBlob(audioData, wavHeader, mimeType = "audio/wav") { const fullAudioData = new Uint8Array(wavHeader.length + audioData.length); fullAudioData.set(wavHeader); fullAudioData.set(audioData, wavHeader.length); return new Blob([fullAudioData], { type: mimeType }); } function createCompleteAudioBlob(chunks, wavHeader, mimeType = "audio/wav") { let totalLength = wavHeader.length; chunks.forEach((chunk) => { totalLength += chunk.length; }); const fullAudioData = new Uint8Array(totalLength); let offset = 0; fullAudioData.set(wavHeader); offset += wavHeader.length; chunks.forEach((chunk) => { fullAudioData.set(chunk, offset); offset += chunk.length; }); return new Blob([fullAudioData], { type: mimeType }); } async function tryDecodeAndQueue(audioData, context, onStart, onEnded) { try { if (!context.wavHeader) { return; } const fullAudioData = new Uint8Array(context.wavHeader.length + audioData.length); fullAudioData.set(context.wavHeader); fullAudioData.set(audioData, context.wavHeader.length); const audioBuffer = await context.audioCtx.decodeAudioData(fullAudioData.buffer.slice(0)); if (context.isPlaying) { context.audioQueue.push(audioBuffer); } else { await playAudio(audioBuffer, context, onStart, onEnded); } } catch (error) { } } async function processAudioChunk(base64Data, context, options, logger) { try { const audioArray = base64ToUint8Array(base64Data); const format = options.dataFormat === "auto" ? detectAudioFormat(audioArray, context) : options.dataFormat || "auto"; logger.debug("处理音频块:", { size: audioArray.length, isFirstChunk: context.isFirstChunk, format, hasHeader: hasWavHeader(audioArray) }); if (format === "with-header" || format === "auto" && hasWavHeader(audioArray)) { await processChunkWithHeader(audioArray, context, options, logger); } else if (format === "without-header" || format === "auto" && !hasWavHeader(audioArray)) { await processChunkWithoutHeader(audioArray, context, options, logger); } context.isFirstChunk = false; } catch (error) { logger.error("处理音频块错误:", error); if (options.onError) { options.onError(error instanceof Error ? error : new Error(String(error))); } } } async function processChunkWithHeader(audioArray, context, options, logger) { logger.debug("处理带头部的音频块"); const header = extractWavHeader(audioArray); if (!header) { logger.warn("无法提取WAV头部"); return; } const audioData = audioArray.slice(44); if (!context.wavHeader) { context.wavHeader = header; } context.audioChunks.push(audioData); const blob = createAudioBlob(audioData, header, options.mimeType); if (options.onAudioData) { options.onAudioData(blob); } if (options.autoPlay) { await tryDecodeAndQueue(audioData, context, options.onStart, options.onEnded); } } async function processChunkWithoutHeader(audioArray, context, options, logger) { logger.debug("处理无头部的音频块"); if (context.isFirstChunk) { const header = extractWavHeader(audioArray); if (header) { context.wavHeader = header; const audioData = audioArray.slice(44); if (audioData.length > 0) { context.audioChunks.push(audioData); const blob = createAudioBlob(audioData, header, options.mimeType); if (options.onAudioData) { options.onAudioData(blob); } if (options.autoPlay) { await tryDecodeAndQueue(audioData, context, options.onStart, options.onEnded); } } } } else { if (!context.wavHeader) { logger.warn("未找到WAV头部,无法处理音频数据"); return; } context.audioChunks.push(audioArray); const blob = createAudioBlob(audioArray, context.wavHeader, options.mimeType); if (options.onAudioData) { options.onAudioData(blob); } if (options.autoPlay) { await tryDecodeAndQueue(audioArray, context, options.onStart, options.onEnded); } } } function extractAudioData(data, audioField) { if (!data) return null; if (!audioField) { if (typeof data === "string") { return data; } if (data.audio && typeof data.audio === "string") { return data.audio; } if (data.data && data.data.audio && typeof data.data.audio === "string") { return data.data.audio; } return null; } const fields = audioField.split("."); let value = data; for (const field of fields) { if (value && typeof value === "object" && field in value) { value = value[field]; } else { return null; } } return typeof value === "string" ? value : null; } async function processDataLine(line, context, options, logger) { if (!line.startsWith("data: ")) return; try { const jsonStr = line.substring(6); if (!jsonStr.trim()) return; const jsonData = JSON.parse(jsonStr); const audioBase64 = extractAudioData(jsonData, options.audioField); if (audioBase64) { await processAudioChunk(audioBase64, context, options, logger); } } catch (error) { logger.debug("跳过不完整的数据:", line.substring(0, 50)); } } async function processStreamAudioV2(reader, options = {}) { const { autoPlay = false, returnBlob = false, mimeType = "audio/wav", audioField, dataFormat = "auto", debug = false, onAudioData, onError, onComplete, onStart, onEnded, onRawChunk } = options; const logger = createLogger({ enabled: debug, prefix: "[StreamAudioV2]", level: debug ? LogLevel.DEBUG : LogLevel.NONE }); logger.log("初始化流式音频处理器 V2", { autoPlay, returnBlob, mimeType, audioField, dataFormat }); const audioContext = initAudioContext(); const decoder = new TextDecoder(); let buffer = ""; let finalBlobResolve = null; let aborted = false; const finalBlobPromise = returnBlob ? new Promise((resolve) => { finalBlobResolve = resolve; }) : void 0; const processStream = async () => { try { while (!aborted) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); if (onRawChunk) { onRawChunk(buffer); } const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (line.trim()) { await processDataLine(line.trim(), audioContext, options, logger); } } } if (!aborted && buffer.trim()) { await processDataLine(buffer.trim(), audioContext, options, logger); } logger.log("流处理完成", { chunksCount: audioContext.audioChunks.length, detectedFormat: audioContext.detectedFormat }); if (returnBlob && audioContext.wavHeader && audioContext.audioChunks.length > 0) { const completeBlob = createCompleteAudioBlob( audioContext.audioChunks, audioContext.wavHeader, mimeType ); finalBlobResolve == null ? void 0 : finalBlobResolve(completeBlob); onComplete == null ? void 0 : onComplete(completeBlob); } else { onComplete == null ? void 0 : onComplete(); } } catch (error) { logger.error("流处理错误:", error); if (error instanceof Error) { onError == null ? void 0 : onError(error); } } }; processStream(); return { pause: () => { if (audioContext.currentSource) { audioContext.currentSource.stop(); audioContext.currentSource = null; } audioContext.isPlaying = false; logger.log("暂停播放"); }, resume: () => { if (!audioContext.isPlaying && audioContext.audioQueue.length > 0) { playNextInQueue(audioContext, onStart, onEnded); logger.log("恢复播放"); } }, abort: () => { aborted = true; reader.cancel().catch(() => { }); resetAudioContext(audioContext); logger.log("中止处理"); }, isPlaying: () => audioContext.isPlaying, finalBlob: finalBlobPromise }; } function createAxiosAudioOptionsV2(options = {}) { const logger = createLogger({ enabled: options.debug, prefix: "[AxiosAudioV2]", level: options.debug ? LogLevel.DEBUG : LogLevel.NONE }); const audioContext = initAudioContext(); let previousLength = 0; let aborted = false; let finalBlobResolve = null; const finalBlobPromise = options.returnBlob ? new Promise((resolve) => { finalBlobResolve = resolve; }) : void 0; const onDownloadProgress = async (progressEvent) => { var _a, _b, _c, _d, _e; if (aborted) return; try { const fullResponse = (_b = (_a = progressEvent.event) == null ? void 0 : _a.target) == null ? void 0 : _b.response; if (fullResponse && typeof fullResponse === "string") { const newData = fullResponse.substring(previousLength); previousLength = fullResponse.length; if (newData) { if (options.onRawChunk) { options.onRawChunk(newData); } const lines = newData.split("\n"); for (const line of lines) { if (line.trim() && line.startsWith("data: ")) { await processDataLine(line.trim(), audioContext, options, logger); } } } } if (progressEvent.loaded === progressEvent.total) { if (options.returnBlob && audioContext.wavHeader && audioContext.audioChunks.length > 0) { const completeBlob = createCompleteAudioBlob( audioContext.audioChunks, audioContext.wavHeader, options.mimeType || "audio/wav" ); finalBlobResolve == null ? void 0 : finalBlobResolve(completeBlob); (_c = options.onComplete) == null ? void 0 : _c.call(options, completeBlob); } else { (_d = options.onComplete) == null ? void 0 : _d.call(options); } } } catch (error) { logger.error("Axios进度处理错误:", error); if (error instanceof Error) { (_e = options.onError) == null ? void 0 : _e.call(options, error); } } }; return { axiosConfig: { responseType: "text", onDownloadProgress }, pause: () => { if (audioContext.currentSource) { audioContext.currentSource.stop(); audioContext.currentSource = null; } audioContext.isPlaying = false; }, resume: () => { if (!audioContext.isPlaying && audioContext.audioQueue.length > 0) { playNextInQueue(audioContext, options.onStart, options.onEnded); } }, abort: () => { aborted = true; resetAudioContext(audioContext); }, isPlaying: () => audioContext.isPlaying, finalBlob: finalBlobPromise }; } export { createAxiosAudioOptionsV2, processStreamAudioV2 };