UNPKG

mstf-kit

Version:

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

646 lines (645 loc) 19.9 kB
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 }; } 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 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 = []; } 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 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 processAudioChunk(base64Data, context, autoPlay, onAudioData, onStart, onEnded) { try { const audioArray = base64ToUint8Array(base64Data); context.audioChunks.push(audioArray); if (context.isFirstChunk) { if (audioArray.length >= 44 && String.fromCharCode(...audioArray.slice(0, 4)) === "RIFF") { context.wavHeader = audioArray.slice(0, 44); const audioData = audioArray.slice(44); context.isFirstChunk = false; if (audioData.length > 0) { const blob2 = await createAudioBlob(audioData, context.wavHeader); onAudioData == null ? void 0 : onAudioData(blob2); if (autoPlay) { await tryDecodeAndQueue(audioData, context, onStart, onEnded); } } return; } return; } const blob = await createAudioBlob(audioArray, context.wavHeader); onAudioData == null ? void 0 : onAudioData(blob); if (autoPlay) { await tryDecodeAndQueue(audioArray, context, onStart, onEnded); } } catch (error) { console.error("音频处理错误:", error); } } async 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) { console.log("WAV头部未初始化"); 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) { console.log("解码失败,可能是不完整的数据"); } } function processDataLine(line, context, autoPlay, onAudioData, audioField, onStart, onEnded) { if (!line.startsWith("data: ")) return; try { const jsonStr = line.substring(6); if (!jsonStr.trim()) return; const jsonData = JSON.parse(jsonStr); const audioBase64 = extractAudioData(jsonData, audioField); if (audioBase64) { processAudioChunk(audioBase64, context, autoPlay, onAudioData, onStart, onEnded); } } catch (error) { console.log("跳过不完整的数据:", line); } } async function processStreamResponse(reader, options = {}) { const { autoPlay = false, returnBlob = false, mimeType = "audio/wav", onAudioData, onError, onComplete, onStart, onEnded, audioField } = options; const audioContext = initAudioContext(); const decoder = new TextDecoder(); let buffer = ""; let finalBlobResolve = null; const finalBlobPromise = returnBlob ? new Promise((resolve) => { finalBlobResolve = resolve; }) : void 0; let aborted = false; const processStream = async () => { try { while (!aborted) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (line.trim()) { processDataLine(line.trim(), audioContext, autoPlay, onAudioData, audioField, onStart, onEnded); } } } if (!aborted && buffer.trim()) { processDataLine(buffer.trim(), audioContext, autoPlay, onAudioData, audioField, onStart, onEnded); } 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) { if (error instanceof Error) { console.error("流处理错误:", error); onError == null ? void 0 : onError(error); } } }; processStream(); return { pause: () => { if (audioContext.currentSource) { audioContext.currentSource.stop(); audioContext.currentSource = null; } audioContext.isPlaying = false; }, resume: () => { if (!audioContext.isPlaying && audioContext.audioQueue.length > 0) { playNextInQueue(audioContext, onStart, onEnded); } }, abort: () => { aborted = true; reader.cancel().catch(() => { }); resetAudioContext(audioContext); }, isPlaying: () => audioContext.isPlaying, finalBlob: finalBlobPromise }; } function processAxiosProgress(options = {}) { const { autoPlay = false, returnBlob = false, mimeType = "audio/wav", onAudioData, onError, onComplete, onStart, onEnded, audioField } = options; const audioContext = initAudioContext(); let previousLength = 0; let aborted = false; let finalBlobResolve = null; const finalBlobPromise = returnBlob ? new Promise((resolve) => { finalBlobResolve = resolve; }) : void 0; const onDownloadProgress = (progressEvent) => { var _a, _b; 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) { const lines = newData.split("\n"); for (const line of lines) { if (line.trim() && line.startsWith("data: ")) { processDataLine(line.trim(), audioContext, autoPlay, onAudioData, audioField, onStart, onEnded); } } } } if (progressEvent.loaded === progressEvent.total) { 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) { if (error instanceof Error) { console.error("Axios进度处理错误:", error); onError == null ? void 0 : onError(error); } } }; return { onDownloadProgress, pause: () => { if (audioContext.currentSource) { audioContext.currentSource.stop(); audioContext.currentSource = null; } audioContext.isPlaying = false; }, resume: () => { if (!audioContext.isPlaying && audioContext.audioQueue.length > 0) { playNextInQueue(audioContext, onStart, onEnded); } }, abort: () => { aborted = true; resetAudioContext(audioContext); }, isPlaying: () => audioContext.isPlaying, finalBlob: finalBlobPromise }; } function processArrayBuffer(arrayBuffer, options = {}) { const { autoPlay = false, mimeType = "audio/wav", onAudioData, onComplete } = options; const blob = new Blob([arrayBuffer], { type: mimeType }); if (autoPlay) { const audioElement = new Audio(); audioElement.src = URL.createObjectURL(blob); audioElement.play(); } onAudioData == null ? void 0 : onAudioData(blob); onComplete == null ? void 0 : onComplete(blob); return blob; } function processBlob(blob, options = {}) { const { autoPlay = false, onAudioData, onComplete } = options; if (autoPlay) { const audioElement = new Audio(); audioElement.src = URL.createObjectURL(blob); audioElement.play(); } onAudioData == null ? void 0 : onAudioData(blob); onComplete == null ? void 0 : onComplete(blob); return blob; } function createAxiosAudioOptions(options = {}) { { const { onDownloadProgress, pause, resume, abort, isPlaying, finalBlob } = processAxiosProgress(options); return { axiosConfig: { responseType: "text", onDownloadProgress }, pause, resume, abort, isPlaying, finalBlob }; } } function processBase64Audio(base64Audio, options = {}) { const { autoPlay = false, mimeType = "audio/wav", onAudioData, onComplete } = options; const audioContext = initAudioContext(); let audioElement = null; try { const audioArray = base64ToUint8Array(base64Audio); if (audioArray.length >= 44 && String.fromCharCode(...audioArray.slice(0, 4)) === "RIFF") { audioContext.wavHeader = audioArray.slice(0, 44); const audioData = audioArray.slice(44); const blob = new Blob([audioArray], { type: mimeType }); if (autoPlay) { audioElement = new Audio(); audioElement.src = URL.createObjectURL(blob); audioElement.play(); tryDecodeAndQueue(audioData, audioContext); } onAudioData == null ? void 0 : onAudioData(blob); onComplete == null ? void 0 : onComplete(blob); return { blob, pause: () => { if (audioElement) { audioElement.pause(); } if (audioContext.currentSource) { audioContext.currentSource.stop(); audioContext.currentSource = null; } audioContext.isPlaying = false; }, resume: () => { if (audioElement) { audioElement.play(); } if (!audioContext.isPlaying && audioContext.audioQueue.length > 0) { playNextInQueue(audioContext); } }, abort: () => { if (audioElement) { audioElement.pause(); audioElement.src = ""; audioElement = null; } resetAudioContext(audioContext); }, isPlaying: () => audioContext.isPlaying || (audioElement ? !audioElement.paused : false) }; } else { const blob = new Blob([audioArray], { type: mimeType }); if (autoPlay) { audioElement = new Audio(); audioElement.src = URL.createObjectURL(blob); audioElement.play(); } onAudioData == null ? void 0 : onAudioData(blob); onComplete == null ? void 0 : onComplete(blob); return { blob, pause: () => { if (audioElement) { audioElement.pause(); } }, resume: () => { if (audioElement) { audioElement.play(); } }, abort: () => { if (audioElement) { audioElement.pause(); audioElement.src = ""; audioElement = null; } resetAudioContext(audioContext); }, isPlaying: () => audioElement ? !audioElement.paused : false }; } } catch (error) { console.error("处理base64音频数据错误:", error); const emptyBlob = new Blob([], { type: mimeType }); return { blob: emptyBlob, pause: () => { }, resume: () => { }, abort: () => { resetAudioContext(audioContext); }, isPlaying: () => false }; } } function createBase64StreamProcessor(options = {}) { const { autoPlay = false, returnBlob = false, mimeType = "audio/wav", onAudioData, onError, onComplete } = options; const audioContext = initAudioContext(); let isInitialized = false; let finalBlobResolve = null; let aborted = false; let audioUrl = null; let finalAudioBlob = null; let audioElement = null; const finalBlobPromise = returnBlob ? new Promise((resolve) => { finalBlobResolve = resolve; }) : void 0; const processChunk2 = async (base64Chunk, isLast = false) => { if (aborted) return; try { const audioArray = base64ToUint8Array(base64Chunk); if (!isInitialized) { if (audioArray.length >= 44 && String.fromCharCode(...audioArray.slice(0, 4)) === "RIFF") { audioContext.wavHeader = audioArray.slice(0, 44); const audioData = audioArray.slice(44); audioContext.audioChunks.push(audioData); isInitialized = true; const blob = createCompleteAudioBlob([audioData], audioContext.wavHeader, mimeType); onAudioData == null ? void 0 : onAudioData(blob); if (autoPlay) { audioElement = new Audio(); audioElement.src = URL.createObjectURL(blob); audioElement.play(); } } else { audioContext.audioChunks.push(audioArray); const blob = new Blob([audioArray], { type: mimeType }); onAudioData == null ? void 0 : onAudioData(blob); if (autoPlay) { audioElement = new Audio(); audioElement.src = URL.createObjectURL(blob); audioElement.play(); } } } else { audioContext.audioChunks.push(audioArray); if (audioContext.wavHeader) { const blob = await createAudioBlob(audioArray, audioContext.wavHeader, mimeType); onAudioData == null ? void 0 : onAudioData(blob); if (autoPlay) { tryDecodeAndQueue(audioArray, audioContext); } } else { const blob = new Blob([audioArray], { type: mimeType }); onAudioData == null ? void 0 : onAudioData(blob); if (autoPlay) { const audioElement2 = new Audio(); audioElement2.src = URL.createObjectURL(blob); audioElement2.play(); } } } if (isLast) { let finalBlob; if (audioContext.wavHeader && audioContext.audioChunks.length > 0) { finalBlob = createCompleteAudioBlob( audioContext.audioChunks, audioContext.wavHeader, mimeType ); } else { const chunks = audioContext.audioChunks; let totalLength = 0; chunks.forEach((chunk) => { totalLength += chunk.length; }); const fullAudioData = new Uint8Array(totalLength); let offset = 0; chunks.forEach((chunk) => { fullAudioData.set(chunk, offset); offset += chunk.length; }); finalBlob = new Blob([fullAudioData], { type: mimeType }); } finalAudioBlob = finalBlob; if (audioUrl) { URL.revokeObjectURL(audioUrl); } audioUrl = URL.createObjectURL(finalBlob); if (finalBlobResolve) { finalBlobResolve(finalBlob); } onComplete == null ? void 0 : onComplete(finalBlob); } } catch (error) { if (error instanceof Error) { console.error("处理base64数据块错误:", error); onError == null ? void 0 : onError(error); } } }; return { processChunk: processChunk2, getBlob: () => finalAudioBlob, getAudioUrl: () => audioUrl, pause: () => { if (audioElement) { audioElement.pause(); } if (audioContext.currentSource) { audioContext.currentSource.stop(); audioContext.currentSource = null; } audioContext.isPlaying = false; }, resume: () => { if (audioElement) { audioElement.play(); } if (!audioContext.isPlaying && audioContext.audioQueue.length > 0) { playNextInQueue(audioContext); } }, abort: () => { aborted = true; resetAudioContext(audioContext); if (audioUrl) { URL.revokeObjectURL(audioUrl); audioUrl = null; } if (audioElement) { audioElement.pause(); audioElement.src = ""; audioElement = null; } finalAudioBlob = null; }, isPlaying: () => audioContext.isPlaying || (audioElement ? !audioElement.paused : false), finalBlob: finalBlobPromise }; } export { createAxiosAudioOptions, createBase64StreamProcessor, processArrayBuffer, processAxiosProgress, processBase64Audio, processBlob, processStreamResponse };