import type { AudioToolRequest } from "@/lib/video-canvas/audio-tools"; import { normalizeAudioPlaybackRate, normalizeAudioTrimRange, } from "@/lib/video-canvas/audio-tools"; function getAudioContextConstructor() { return window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext; } export async function decodeAudioArrayBuffer(arrayBuffer: ArrayBuffer) { const AudioContextCtor = getAudioContextConstructor(); if (!AudioContextCtor) throw new Error("当前浏览器不支持音频处理"); const context = new AudioContextCtor(); try { return await context.decodeAudioData(arrayBuffer.slice(0)); } finally { await context.close().catch(() => undefined); } } async function decodeAudioSource(mediaUrl: string) { const parsedUrl = new URL(mediaUrl, window.location.origin); const sourceUrl = parsedUrl.origin === window.location.origin ? parsedUrl.toString() : `/api/canvas/audio-source?url=${encodeURIComponent(parsedUrl.toString())}`; const response = await fetch(sourceUrl, { credentials: "include" }); if (!response.ok) throw new Error("音频素材读取失败"); return decodeAudioArrayBuffer(await response.arrayBuffer()); } function trimAudioBuffer(source: AudioBuffer, startSeconds: number, endSeconds: number) { const range = normalizeAudioTrimRange(source.duration, startSeconds, endSeconds); const startFrame = Math.floor(range.startSeconds * source.sampleRate); const endFrame = Math.min(source.length, Math.ceil(range.endSeconds * source.sampleRate)); const frameCount = Math.max(1, endFrame - startFrame); const output = new AudioBuffer({ length: frameCount, numberOfChannels: source.numberOfChannels, sampleRate: source.sampleRate, }); for (let channel = 0; channel < source.numberOfChannels; channel += 1) { output.copyToChannel(source.getChannelData(channel).subarray(startFrame, endFrame), channel); } return output; } async function changeAudioPlaybackRate(source: AudioBuffer, playbackRate: number) { const rate = normalizeAudioPlaybackRate(playbackRate); const frameCount = Math.max(1, Math.ceil(source.length / rate)); const offline = new OfflineAudioContext( source.numberOfChannels, frameCount, source.sampleRate, ); const sourceNode = offline.createBufferSource(); sourceNode.buffer = source; sourceNode.playbackRate.value = rate; sourceNode.connect(offline.destination); sourceNode.start(0); return offline.startRendering(); } function writeAscii(view: DataView, offset: number, value: string) { for (let index = 0; index < value.length; index += 1) { view.setUint8(offset + index, value.charCodeAt(index)); } } export function encodeAudioBufferAsWav(buffer: AudioBuffer) { const channelCount = Math.max(1, Math.min(2, buffer.numberOfChannels)); const bytesPerSample = 2; const dataSize = buffer.length * channelCount * bytesPerSample; const arrayBuffer = new ArrayBuffer(44 + dataSize); const view = new DataView(arrayBuffer); writeAscii(view, 0, "RIFF"); view.setUint32(4, 36 + dataSize, true); writeAscii(view, 8, "WAVE"); writeAscii(view, 12, "fmt "); view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, channelCount, true); view.setUint32(24, buffer.sampleRate, true); view.setUint32(28, buffer.sampleRate * channelCount * bytesPerSample, true); view.setUint16(32, channelCount * bytesPerSample, true); view.setUint16(34, 16, true); writeAscii(view, 36, "data"); view.setUint32(40, dataSize, true); const channels = Array.from({ length: channelCount }, (_, index) => buffer.getChannelData(Math.min(index, buffer.numberOfChannels - 1)), ); let offset = 44; for (let frame = 0; frame < buffer.length; frame += 1) { for (let channel = 0; channel < channelCount; channel += 1) { const sample = Math.max(-1, Math.min(1, channels[channel]![frame] || 0)); view.setInt16(offset, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true); offset += bytesPerSample; } } return new Blob([arrayBuffer], { type: "audio/wav" }); } export async function processCanvasAudio(mediaUrl: string, request: AudioToolRequest) { const source = await decodeAudioSource(mediaUrl); const output = request.kind === "trim" ? trimAudioBuffer(source, request.startSeconds, request.endSeconds) : await changeAudioPlaybackRate(source, request.playbackRate); return { blob: encodeAudioBufferAsWav(output), durationSeconds: output.duration, sampleRate: output.sampleRate, }; }