UNPKG

mstf-kit

Version:

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

1,217 lines (1,216 loc) 36.9 kB
const SAMPLE_RATES = { /** 语音识别推荐 */ SPEECH: 16e3, /** 标准音质 */ STANDARD: 22050, /** CD音质 */ CD: 44100, /** 高清音质 */ HD: 48e3 }; const CHANNEL_COUNTS = { /** 单声道,适合语音识别 */ MONO: 1, /** 立体声,适合音乐录制 */ STEREO: 2 }; const AUDIO_FORMATS = { /** WAV格式,无损音质,文件较大 */ WAV: "wav", /** MP3格式,有损压缩,文件小,普遍支持 */ MP3: "mp3", /** WebM格式,专为网络优化,支持流式传输 */ WEBM: "webm", /** Ogg格式,开放格式,支持流式传输 */ OGG: "ogg" }; class AudioProcessingError extends Error { constructor(message) { super(message); this.name = "AudioProcessingError"; } } function validateSampleRate(sampleRate) { if (!sampleRate) { return SAMPLE_RATES.SPEECH; } if (sampleRate < SAMPLE_RATES.SPEECH) { console.warn(`采样率 ${sampleRate}Hz 低于最小值,已调整为 ${SAMPLE_RATES.SPEECH}Hz`); return SAMPLE_RATES.SPEECH; } if (sampleRate > SAMPLE_RATES.HD) { console.warn(`采样率 ${sampleRate}Hz 高于最大值,已调整为 ${SAMPLE_RATES.HD}Hz`); return SAMPLE_RATES.HD; } return sampleRate; } async function createRecorder(options = {}) { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { throw new AudioProcessingError("浏览器不支持录音功能"); } const validatedSampleRate = validateSampleRate(options.sampleRate); const isStreaming = options.isStreaming !== void 0 ? options.isStreaming : !!(options.onDataAvailable || options.timeslice); const config = { format: options.format || AUDIO_FORMATS.WEBM, sampleRate: validatedSampleRate, channelCount: options.channelCount || CHANNEL_COUNTS.MONO, bitRate: options.bitRate || 128, echoCancellation: options.echoCancellation !== void 0 ? options.echoCancellation : true, noiseSuppression: options.noiseSuppression !== void 0 ? options.noiseSuppression : true, autoGainControl: options.autoGainControl !== void 0 ? options.autoGainControl : true, timeslice: options.timeslice || 100, onDataAvailable: options.onDataAvailable, onAudioProcess: options.onAudioProcess, isStreaming }; let stream; try { stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: config.echoCancellation, noiseSuppression: config.noiseSuppression, autoGainControl: config.autoGainControl, channelCount: config.channelCount } }); } catch (error) { console.error("获取麦克风权限失败:", error); throw new AudioProcessingError(`无法访问麦克风: ${error instanceof Error ? error.message : String(error)}`); } let audioContext = null; let audioSource = null; let analyser = null; let dataArray = new Uint8Array(0); let analyzerConnected = false; try { audioContext = new (window.AudioContext || window.webkitAudioContext)(); const actualSampleRate = audioContext.sampleRate; audioSource = audioContext.createMediaStreamSource(stream); analyser = audioContext.createAnalyser(); analyser.fftSize = 2048; dataArray = new Uint8Array(analyser.frequencyBinCount); audioSource.connect(analyser); analyzerConnected = true; if (config.sampleRate !== actualSampleRate) { console.info(`系统采样率为 ${actualSampleRate}Hz,将在录音结束后重采样为 ${config.sampleRate}Hz`); } } catch (error) { console.warn("初始化音频分析器失败:", error); releaseAudioResources(audioContext, audioSource, analyser); audioContext = null; audioSource = null; analyser = null; analyzerConnected = false; } const mimeType = getSupportedMimeType(config.format); let mediaRecorder; try { mediaRecorder = new MediaRecorder(stream, { mimeType, audioBitsPerSecond: config.bitRate * 1e3 }); } catch (error) { console.warn(`创建MediaRecorder失败,使用默认配置: ${error}`); try { mediaRecorder = new MediaRecorder(stream); } catch (fallbackError) { releaseAudioResources(audioContext, audioSource, analyser); stream.getTracks().forEach((track) => track.stop()); throw new AudioProcessingError(`无法创建录音器: ${fallbackError}`); } } let chunks = []; let startTime = 0; let pausedTime = 0; let totalPausedTime = 0; let audioProcessInterval = null; mediaRecorder.ondataavailable = (e) => { if (e.data && e.data.size > 0) { chunks.push(e.data); if (config.isStreaming && config.onDataAvailable) { config.onDataAvailable(e.data); } } }; const recorder = { async start() { if (mediaRecorder.state === "inactive") { chunks = []; startTime = Date.now(); totalPausedTime = 0; const timeslice = config.isStreaming ? config.timeslice : 1e3; mediaRecorder.start(timeslice); if (config.onAudioProcess) { startAudioProcessing(config.onAudioProcess, analyser, analyzerConnected, dataArray); } } }, pause() { if (mediaRecorder.state === "recording") { mediaRecorder.pause(); pausedTime = Date.now(); stopAudioProcessing(); } }, resume() { if (mediaRecorder.state === "paused") { mediaRecorder.resume(); totalPausedTime += Date.now() - pausedTime; if (config.onAudioProcess && audioProcessInterval === null) { startAudioProcessing(config.onAudioProcess, analyser, analyzerConnected, dataArray); } } }, async stop() { return new Promise((resolve) => { const handleStop = async () => { stopAudioProcessing(); if (chunks.length === 0) { resolve(new Blob([], { type: mimeType })); return; } const rawBlob = new Blob(chunks, { type: mimeType }); try { const processedBlob = await processAudio(rawBlob, { format: config.format, sampleRate: config.sampleRate, channelCount: config.channelCount, bitRate: config.bitRate }); resolve(processedBlob); } catch (error) { console.error("音频处理失败:", error); resolve(rawBlob); } }; if (mediaRecorder.state !== "inactive") { mediaRecorder.onstop = handleStop; if ("requestData" in mediaRecorder && mediaRecorder.state === "recording") { try { mediaRecorder.requestData(); } catch (e) { } } mediaRecorder.stop(); } else { handleStop(); } }); }, cancel() { if (mediaRecorder.state !== "inactive") { mediaRecorder.stop(); } stopAudioProcessing(); chunks = []; }, getState() { return mediaRecorder.state; }, getDuration() { if (startTime === 0) return 0; if (mediaRecorder.state === "paused") { return (pausedTime - startTime - totalPausedTime) / 1e3; } return (Date.now() - startTime - totalPausedTime) / 1e3; }, getAudioData() { if (!analyzerConnected || !analyser) { return new Uint8Array(0); } try { analyser.getByteFrequencyData(dataArray); return dataArray.slice(0); } catch (e) { return new Uint8Array(0); } }, async getCurrentAudio() { if (chunks.length === 0) { return new Blob([], { type: mimeType }); } const rawBlob = new Blob(chunks.slice(0), { type: mimeType }); try { const processedBlob = await processAudio(rawBlob, { format: config.format, sampleRate: config.sampleRate, channelCount: config.channelCount, bitRate: config.bitRate }); return processedBlob; } catch (error) { console.error("处理当前音频失败:", error); return rawBlob; } }, release() { stopAudioProcessing(); releaseAudioResources(audioContext, audioSource, analyser); stream.getTracks().forEach((track) => track.stop()); } }; function startAudioProcessing(callback, analyser2, connected, dataArray2) { if (audioProcessInterval !== null) { clearInterval(audioProcessInterval); } audioProcessInterval = window.setInterval(() => { if (connected && analyser2) { try { analyser2.getByteFrequencyData(dataArray2); callback(dataArray2.slice(0)); } catch (e) { callback(new Uint8Array(0)); } } else { callback(new Uint8Array(0)); } }, 50); } function stopAudioProcessing() { if (audioProcessInterval !== null) { clearInterval(audioProcessInterval); audioProcessInterval = null; } } return recorder; } function releaseAudioResources(context, source, analyser) { if (analyser) { try { analyser.disconnect(); } catch (e) { } } if (source) { try { source.disconnect(); } catch (e) { } } if (context) { try { context.close(); } catch (e) { } } } function getSupportedMimeType(format) { const preferredType = getFormatMimeType(format); if (MediaRecorder.isTypeSupported(preferredType)) { return preferredType; } const fallbackTypes = [ "audio/webm;codecs=opus", "audio/webm", "audio/ogg;codecs=opus", "audio/mp4", "audio/mpeg", "" // 空字符串表示浏览器默认 ]; for (const type of fallbackTypes) { if (type === "" || MediaRecorder.isTypeSupported(type)) { console.warn(`格式 ${format} 不受支持,使用 ${type || "浏览器默认"} 替代`); return type; } } return ""; } function getFormatMimeType(format) { const mimeTypes = { [AUDIO_FORMATS.WAV]: "audio/wav", [AUDIO_FORMATS.MP3]: "audio/mpeg", [AUDIO_FORMATS.OGG]: "audio/ogg;codecs=opus", [AUDIO_FORMATS.WEBM]: "audio/webm;codecs=opus" }; return mimeTypes[format] || "audio/webm;codecs=opus"; } async function processAudio(blob, options) { const { format, sampleRate, channelCount, bitRate } = options; const validatedSampleRate = validateSampleRate(sampleRate); const audioContext = new AudioContext(); try { const arrayBuffer = await blob.arrayBuffer(); const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); const originalSampleRate = audioBuffer.sampleRate; const originalChannels = audioBuffer.numberOfChannels; let processedBuffer = audioBuffer; if (validatedSampleRate !== originalSampleRate) { processedBuffer = await resampleAudio( audioBuffer, validatedSampleRate, originalChannels ); } if (channelCount !== originalChannels) { processedBuffer = adjustChannels( processedBuffer, channelCount, audioContext ); } let outputBlob; if (format === AUDIO_FORMATS.WAV) { outputBlob = encodeToWav(processedBuffer); } else { outputBlob = await encodeWithMediaRecorder( processedBuffer, format, bitRate ); } audioContext.close(); return outputBlob; } catch (error) { audioContext.close(); console.error("音频处理失败:", error); throw new AudioProcessingError(`音频处理失败: ${error instanceof Error ? error.message : String(error)}`); } } async function resampleAudio(buffer, targetSampleRate, channels) { const duration = buffer.duration; const newLength = Math.ceil(duration * targetSampleRate); const offlineContext = new OfflineAudioContext( channels, newLength, targetSampleRate ); const source = offlineContext.createBufferSource(); source.buffer = buffer; source.connect(offlineContext.destination); source.start(0); const resampledBuffer = await offlineContext.startRendering(); console.info(`重采样完成: ${buffer.sampleRate}Hz -> ${targetSampleRate}Hz`); return resampledBuffer; } function adjustChannels(buffer, targetChannels, context) { const sourceChannels = buffer.numberOfChannels; if (sourceChannels === targetChannels) { return buffer; } const newBuffer = context.createBuffer( targetChannels, buffer.length, buffer.sampleRate ); if (targetChannels === 1 && sourceChannels > 1) { const output = newBuffer.getChannelData(0); for (let i = 0; i < buffer.length; i++) { let sum = 0; for (let channel = 0; channel < sourceChannels; channel++) { sum += buffer.getChannelData(channel)[i]; } output[i] = sum / sourceChannels; } } else if (targetChannels > sourceChannels) { for (let channel = 0; channel < targetChannels; channel++) { const output = newBuffer.getChannelData(channel); const sourceChannel = channel % sourceChannels; output.set(buffer.getChannelData(sourceChannel)); } } else { for (let channel = 0; channel < targetChannels; channel++) { newBuffer.copyToChannel(buffer.getChannelData(channel), channel); } } console.info(`通道调整完成: ${sourceChannels} -> ${targetChannels}`); return newBuffer; } function encodeToWav(buffer) { const numChannels = buffer.numberOfChannels; const sampleRate = buffer.sampleRate; const channels = []; for (let i = 0; i < numChannels; i++) { channels.push(buffer.getChannelData(i)); } let interleaved; if (numChannels === 2) { interleaved = interleaveChannels(channels[0], channels[1]); } else if (numChannels === 1) { interleaved = channels[0]; } else { interleaved = new Float32Array(buffer.length * numChannels); for (let i = 0; i < buffer.length; i++) { for (let channel = 0; channel < numChannels; channel++) { interleaved[i * numChannels + channel] = channels[channel][i]; } } } const wavData = createWavHeader(interleaved, { sampleRate, numChannels, bitsPerSample: 16 }); return new Blob([wavData], { type: "audio/wav" }); } async function encodeWithMediaRecorder(buffer, format, bitRate) { const context = new AudioContext(); const source = context.createBufferSource(); source.buffer = buffer; const destination = context.createMediaStreamDestination(); source.connect(destination); const mimeType = getSupportedMimeType(format); const options = {}; if (mimeType) { options.mimeType = mimeType; } if (bitRate) { options.audioBitsPerSecond = bitRate * 1e3; } let recorder; try { recorder = new MediaRecorder(destination.stream, options); } catch (error) { console.warn(`创建编码器失败,使用默认设置: ${error}`); recorder = new MediaRecorder(destination.stream); } const chunks = []; recorder.ondataavailable = (e) => { if (e.data && e.data.size > 0) { chunks.push(e.data); } }; return new Promise((resolve, reject) => { recorder.onstop = () => { source.disconnect(); context.close(); if (chunks.length === 0) { reject(new Error("编码过程中没有数据生成")); return; } const finalBlob = new Blob(chunks, { type: mimeType || `audio/${format}` }); resolve(finalBlob); }; recorder.start(); source.start(0); const duration = buffer.duration * 1e3; setTimeout(() => { try { recorder.stop(); } catch (error) { reject(error); } }, duration + 500); }); } function interleaveChannels(left, right) { const length = left.length + right.length; const result = new Float32Array(length); let inputIndex = 0; for (let i = 0; i < length; ) { result[i++] = left[inputIndex]; result[i++] = right[inputIndex]; inputIndex++; } return result; } function createWavHeader(samples, options) { const { sampleRate, numChannels, bitsPerSample } = options; const bytesPerSample = bitsPerSample / 8; const blockAlign = numChannels * bytesPerSample; const dataLength = samples.length * bytesPerSample; const buffer = new ArrayBuffer(44 + dataLength); const view = new DataView(buffer); writeString(view, 0, "RIFF"); view.setUint32(4, 36 + dataLength, true); writeString(view, 8, "WAVE"); writeString(view, 12, "fmt "); view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, numChannels, true); view.setUint32(24, sampleRate, true); view.setUint32(28, sampleRate * blockAlign, true); view.setUint16(32, blockAlign, true); view.setUint16(34, bitsPerSample, true); writeString(view, 36, "data"); view.setUint32(40, dataLength, true); if (bitsPerSample === 16) { floatToInt16(view, 44, samples); } else if (bitsPerSample === 8) { floatToInt8(view, 44, samples); } return view; } function writeString(view, offset, string) { for (let i = 0; i < string.length; i++) { view.setUint8(offset + i, string.charCodeAt(i)); } } function floatToInt16(view, offset, samples) { for (let i = 0; i < samples.length; i++, offset += 2) { const s = Math.max(-1, Math.min(1, samples[i])); view.setInt16(offset, s < 0 ? s * 32768 : s * 32767, true); } } function floatToInt8(view, offset, samples) { for (let i = 0; i < samples.length; i++, offset++) { const s = Math.max(-1, Math.min(1, samples[i])); const val = s < 0 ? s * 128 : s * 127; view.setInt8(offset, val + 128); } } async function blobToAudioBuffer(blob, context) { const audioContext = context || new (window.AudioContext || window.webkitAudioContext)(); const arrayBuffer = await blob.arrayBuffer(); return await audioContext.decodeAudioData(arrayBuffer); } function playAudioBlob(blob) { const url = URL.createObjectURL(blob); const audio = new Audio(url); audio.onended = () => { URL.revokeObjectURL(url); }; audio.play(); return audio; } function base64ToAudioBlob(base64, mimeType) { const base64Data = base64.includes(",") ? base64.split(",")[1] : base64; try { const byteString = atob(base64Data); const arrayBuffer = new ArrayBuffer(byteString.length); const uint8Array = new Uint8Array(arrayBuffer); for (let i = 0; i < byteString.length; i++) { uint8Array[i] = byteString.charCodeAt(i); } return new Blob([arrayBuffer], { type: mimeType }); } catch (error) { console.error("Base64转换失败:", error); throw new AudioProcessingError("无效的Base64音频数据"); } } function audioBlobToBase64(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result); reader.onerror = (error) => reject(new AudioProcessingError(`转换Blob到Base64失败: ${error}`)); reader.readAsDataURL(blob); }); } async function streamToAudioBlob(stream, mimeType) { try { const bodyStream = stream instanceof Response ? stream.body : stream; if (!bodyStream) { throw new Error("无效的流数据"); } const reader = bodyStream.getReader(); const chunks = []; let totalLength = 0; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); totalLength += value.length; } const result = new Uint8Array(totalLength); let offset = 0; for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; } if (!mimeType) { mimeType = detectAudioMimeType(result); } return new Blob([result], { type: mimeType }); } catch (error) { console.error("转换音频流到 Blob 失败:", error); throw error; } } function detectAudioMimeType(data) { if (data.length > 12 && data[0] === 82 && data[1] === 73 && data[2] === 70 && data[3] === 70 && data[8] === 87 && data[9] === 65 && data[10] === 86 && data[11] === 69) { return "audio/wav"; } if (data.length > 3 && (data[0] === 73 && data[1] === 68 && data[2] === 51 || // ID3 data[0] === 255 && (data[1] & 224) === 224)) { return "audio/mp3"; } return "audio/mp3"; } async function playAudioStream(stream, mimeType) { const blob = await streamToAudioBlob(stream, mimeType); return playAudioBlob(blob); } async function createStreamingAudioPlayer(stream, mimeType, options = {}) { const defaultOptions = { autoPlay: false, volume: 1, loop: false, bufferSize: 3, // 秒 useMediaSource: true, playWhenEnoughData: true, bufferUpdateInterval: 250 // 毫秒 }; const config = { ...defaultOptions, ...options }; let bodyStream = stream instanceof Response ? stream.body : stream; if (!bodyStream) { throw new Error("无效的流数据"); } let actualMimeType = mimeType; let initialData = null; if (!actualMimeType) { const reader = bodyStream.getReader(); const { value, done } = await reader.read(); if (done || !value) { throw new Error("流为空或已结束"); } initialData = value; actualMimeType = detectAudioMimeType(initialData); const remainingStream = new ReadableStream({ start(controller) { controller.enqueue(initialData); }, async pull(controller) { try { const { value: value2, done: done2 } = await reader.read(); if (done2) { controller.close(); } else { controller.enqueue(value2); } } catch (error) { controller.error(error); } } }); bodyStream = remainingStream; } const isMP3 = actualMimeType.includes("mp3") || actualMimeType.includes("mpeg"); const isWebM = actualMimeType.includes("webm"); const isMP4 = actualMimeType.includes("mp4"); const canUseMediaSource = (isMP3 || isWebM || isMP4) && config.useMediaSource && typeof MediaSource !== "undefined"; if (canUseMediaSource) { return createMediaSourcePlayer(bodyStream, actualMimeType, config); } else { return createAudioElementPlayer(bodyStream, actualMimeType, config); } } async function createMediaSourcePlayer(stream, mimeType, options) { let sourceType = mimeType; if (mimeType.includes("mp3") || mimeType.includes("mpeg")) { sourceType = "audio/mpeg"; } else if (mimeType.includes("wav")) { throw new Error("MediaSource API 不支持 WAV 格式,请使用 MP3/WebM/MP4 格式"); } const mediaSource = new MediaSource(); const sourceUrl = URL.createObjectURL(mediaSource); const audioElement = new Audio(); audioElement.volume = options.volume; audioElement.loop = options.loop; audioElement.src = sourceUrl; let sourceBuffer = null; let isPlaying = false; let isPaused = false; let isStopped = true; let enqueuedChunks = []; let isBufferUpdating = false; let reader = null; let initialSegmentAppended = false; let bufferUpdateIntervalId = null; let bufferedProgress = 0; let onEndedCallback = null; let onBufferUpdateCallback = null; let onTimeUpdateCallback = null; let onErrorCallback = null; await new Promise((resolve) => { mediaSource.addEventListener("sourceopen", () => { try { sourceBuffer = mediaSource.addSourceBuffer(sourceType); sourceBuffer.addEventListener("updateend", processQueue); resolve(); } catch (error) { onError(new Error(`创建 SourceBuffer 失败: ${error}`)); resolve(); } }, { once: true }); }); if (!sourceBuffer) { URL.revokeObjectURL(sourceUrl); return createAudioElementPlayer(stream, mimeType, options); } const startBufferUpdateInterval = () => { if (bufferUpdateIntervalId !== null) { clearInterval(bufferUpdateIntervalId); } bufferUpdateIntervalId = window.setInterval(() => { if (audioElement && sourceBuffer) { updateBufferedProgress(); } }, options.bufferUpdateInterval); }; const updateBufferedProgress = () => { if (!audioElement || audioElement.duration === Infinity || isNaN(audioElement.duration)) { return; } const timeRanges = audioElement.buffered; if (timeRanges.length > 0) { const bufferedEnd = timeRanges.end(timeRanges.length - 1); bufferedProgress = Math.min(1, bufferedEnd / audioElement.duration); if (onBufferUpdateCallback) { onBufferUpdateCallback(bufferedProgress); } } }; const processQueue = async () => { if (!sourceBuffer || isBufferUpdating || enqueuedChunks.length === 0) { return; } isBufferUpdating = true; try { const chunk = enqueuedChunks.shift(); sourceBuffer.appendBuffer(chunk); if (!initialSegmentAppended) { initialSegmentAppended = true; if (options.autoPlay && options.playWhenEnoughData) { playAudio(); } } updateBufferedProgress(); } catch (error) { onError(new Error(`缓冲数据失败: ${error}`)); } }; const startStreaming = async () => { try { reader = stream.getReader(); while (true) { const { value, done } = await reader.read(); if (done) { if (mediaSource.readyState === "open") { try { mediaSource.endOfStream(); } catch (error) { console.warn("媒体源关闭失败:", error); } } break; } enqueuedChunks.push(value); if (!isBufferUpdating) { processQueue(); } } } catch (error) { onError(new Error(`流读取失败: ${error}`)); } }; const playAudio = () => { if (!isPlaying) { const playPromise = audioElement.play(); if (playPromise !== void 0) { playPromise.catch((error) => { console.warn("自动播放失败:", error); }); } isPlaying = true; isPaused = false; isStopped = false; } }; const onError = (error) => { if (onErrorCallback) { onErrorCallback(error); } else { console.error("流式音频播放器错误:", error); } }; audioElement.addEventListener("ended", () => { isPlaying = false; if (onEndedCallback) { onEndedCallback(); } }); audioElement.addEventListener("timeupdate", () => { if (onTimeUpdateCallback) { onTimeUpdateCallback(audioElement.currentTime, audioElement.duration || 0); } }); audioElement.addEventListener("error", () => { var _a; onError(new Error(`音频元素错误: ${((_a = audioElement.error) == null ? void 0 : _a.message) || "未知错误"}`)); }); startBufferUpdateInterval(); startStreaming(); const player = { play() { playAudio(); }, pause() { if (isPlaying && !isPaused) { audioElement.pause(); isPaused = true; isPlaying = false; } }, resume() { if (isPaused) { playAudio(); } }, stop() { if (!isStopped) { audioElement.pause(); audioElement.currentTime = 0; isPlaying = false; isPaused = false; isStopped = true; if (reader) { try { reader.cancel(); } catch (e) { } reader = null; } if (bufferUpdateIntervalId !== null) { clearInterval(bufferUpdateIntervalId); bufferUpdateIntervalId = null; } URL.revokeObjectURL(sourceUrl); } }, setVolume(volume) { audioElement.volume = Math.max(0, Math.min(1, volume)); }, getState() { if (isPlaying) return "playing"; if (isPaused) return "paused"; return "stopped"; }, getDuration() { return isNaN(audioElement.duration) ? 0 : audioElement.duration; }, getCurrentTime() { return audioElement.currentTime; }, seek(time) { if (!isNaN(audioElement.duration) && time >= 0 && time <= audioElement.duration) { audioElement.currentTime = time; } }, getBufferedProgress() { return bufferedProgress; }, onEnded: null, onBufferUpdate: null, onTimeUpdate: null, onError: null }; Object.defineProperties(player, { onEnded: { get: () => onEndedCallback, set: (callback) => { onEndedCallback = callback; } }, onBufferUpdate: { get: () => onBufferUpdateCallback, set: (callback) => { onBufferUpdateCallback = callback; } }, onTimeUpdate: { get: () => onTimeUpdateCallback, set: (callback) => { onTimeUpdateCallback = callback; } }, onError: { get: () => onErrorCallback, set: (callback) => { onErrorCallback = callback; } } }); if (options.autoPlay && !options.playWhenEnoughData) { playAudio(); } return player; } async function createAudioElementPlayer(stream, mimeType, options) { let audioElement = new Audio(); let isPlaying = false; let isPaused = false; let isStopped = true; let bufferedProgress = 0; let bufferUpdateIntervalId = null; let audioBlob = null; let audioUrl = null; let onEndedCallback = null; let onBufferUpdateCallback = null; let onTimeUpdateCallback = null; let onErrorCallback = null; let hasStartedPlaying = false; let dataChunks = []; let totalBytesReceived = 0; const targetBufferSize = options.bufferSize * 128 * 1024; const onError = (error) => { if (onErrorCallback) { onErrorCallback(error); } else { console.error("流式音频播放器错误:", error); } }; const reader = stream.getReader(); const readChunks = async () => { try { const { value, done } = await reader.read(); if (done) { createAndPlayFinalAudio(); return true; } dataChunks.push(value); totalBytesReceived += value.length; updateBufferedProgress(totalBytesReceived / (totalBytesReceived + 512 * 1024)); if (!hasStartedPlaying && options.autoPlay && options.playWhenEnoughData && totalBytesReceived >= targetBufferSize) { createAndPlayIntermediateAudio(); } return false; } catch (error) { onError(new Error(`读取流失败: ${error}`)); return true; } }; const startBufferUpdateInterval = () => { if (bufferUpdateIntervalId !== null) { clearInterval(bufferUpdateIntervalId); } bufferUpdateIntervalId = window.setInterval(() => { if (audioElement) { updateActualBufferedProgress(); } }, options.bufferUpdateInterval); }; const updateActualBufferedProgress = () => { if (!audioElement || audioElement.duration === Infinity || isNaN(audioElement.duration)) { return; } const timeRanges = audioElement.buffered; if (timeRanges.length > 0) { const bufferedEnd = timeRanges.end(timeRanges.length - 1); bufferedProgress = Math.min(1, bufferedEnd / audioElement.duration); if (onBufferUpdateCallback) { onBufferUpdateCallback(bufferedProgress); } } }; const updateBufferedProgress = (progress) => { bufferedProgress = progress; if (onBufferUpdateCallback) { onBufferUpdateCallback(bufferedProgress); } }; const createAndPlayIntermediateAudio = () => { hasStartedPlaying = true; const combinedData = concatenateUint8Arrays(dataChunks); const tempBlob = new Blob([combinedData], { type: mimeType }); if (audioElement) { const tempUrl = URL.createObjectURL(tempBlob); audioElement.src = tempUrl; audioElement.oncanplaythrough = () => { playAudio(); audioElement.oncanplaythrough = null; }; setupAudioEvents(audioElement); if (audioUrl) { URL.revokeObjectURL(audioUrl); } audioUrl = tempUrl; } }; const createAndPlayFinalAudio = () => { const combinedData = concatenateUint8Arrays(dataChunks); audioBlob = new Blob([combinedData], { type: mimeType }); if (audioElement) { const currentTime = audioElement.currentTime; const wasPlaying = isPlaying && !isPaused; if (audioUrl) { URL.revokeObjectURL(audioUrl); } audioUrl = URL.createObjectURL(audioBlob); audioElement.src = audioUrl; audioElement.currentTime = currentTime; setupAudioEvents(audioElement); if (wasPlaying) { playAudio(); } } updateBufferedProgress(1); }; const setupAudioEvents = (audio) => { audio.addEventListener("ended", () => { isPlaying = false; if (onEndedCallback) { onEndedCallback(); } }); audio.addEventListener("timeupdate", () => { if (onTimeUpdateCallback) { onTimeUpdateCallback(audio.currentTime, audio.duration || 0); } }); audio.addEventListener("error", () => { var _a; onError(new Error(`音频元素错误: ${((_a = audio.error) == null ? void 0 : _a.message) || "未知错误"}`)); }); }; const concatenateUint8Arrays = (arrays) => { let totalLength = 0; arrays.forEach((arr) => { totalLength += arr.length; }); const result = new Uint8Array(totalLength); let offset = 0; arrays.forEach((arr) => { result.set(arr, offset); offset += arr.length; }); return result; }; const startStreamReading = async () => { let completed = false; while (!completed) { completed = await readChunks(); } }; const playAudio = () => { if (!isPlaying && audioElement) { const playPromise = audioElement.play(); if (playPromise !== void 0) { playPromise.catch((error) => { console.warn("自动播放失败:", error); }); } isPlaying = true; isPaused = false; isStopped = false; } }; if (audioElement) { audioElement.volume = options.volume; audioElement.loop = options.loop; setupAudioEvents(audioElement); } startBufferUpdateInterval(); startStreamReading(); if (options.autoPlay && !options.playWhenEnoughData) { const silentBlob = new Blob([new Uint8Array(1)], { type: mimeType }); if (audioElement) { audioUrl = URL.createObjectURL(silentBlob); audioElement.src = audioUrl; playAudio(); } } const player = { play() { if (dataChunks.length > 0 && !isPlaying) { if (!hasStartedPlaying) { createAndPlayIntermediateAudio(); } else { playAudio(); } } }, pause() { if (isPlaying && !isPaused && audioElement) { audioElement.pause(); isPaused = true; isPlaying = false; } }, resume() { if (isPaused) { playAudio(); } }, stop() { if (!isStopped && audioElement) { audioElement.pause(); audioElement.currentTime = 0; isPlaying = false; isPaused = false; isStopped = true; if (bufferUpdateIntervalId !== null) { clearInterval(bufferUpdateIntervalId); bufferUpdateIntervalId = null; } if (audioUrl) { URL.revokeObjectURL(audioUrl); audioUrl = null; } try { reader.cancel(); } catch (e) { } } }, setVolume(volume) { if (audioElement) { audioElement.volume = Math.max(0, Math.min(1, volume)); } }, getState() { if (isPlaying) return "playing"; if (isPaused) return "paused"; return "stopped"; }, getDuration() { return audioElement && !isNaN(audioElement.duration) ? audioElement.duration : 0; }, getCurrentTime() { return audioElement ? audioElement.currentTime : 0; }, seek(time) { if (audioElement && !isNaN(audioElement.duration) && time >= 0 && time <= audioElement.duration) { audioElement.currentTime = time; } }, getBufferedProgress() { return bufferedProgress; }, onEnded: null, onBufferUpdate: null, onTimeUpdate: null, onError: null }; Object.defineProperties(player, { onEnded: { get: () => onEndedCallback, set: (callback) => { onEndedCallback = callback; } }, onBufferUpdate: { get: () => onBufferUpdateCallback, set: (callback) => { onBufferUpdateCallback = callback; } }, onTimeUpdate: { get: () => onTimeUpdateCallback, set: (callback) => { onTimeUpdateCallback = callback; } }, onError: { get: () => onErrorCallback, set: (callback) => { onErrorCallback = callback; } } }); return player; } export { AUDIO_FORMATS, CHANNEL_COUNTS, SAMPLE_RATES, audioBlobToBase64, base64ToAudioBlob, blobToAudioBuffer, createRecorder, createStreamingAudioPlayer, playAudioBlob, playAudioStream, streamToAudioBlob };