mstf-kit
Version:
一个现代化的 JavaScript/TypeScript 工具库,提供了丰富的常用工具函数
446 lines (445 loc) • 13.9 kB
JavaScript
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
};