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