// SonicForge Studio Audio Engine Service // High-performance Desktop-Grade Client-Side Audio Engine & DSP Service (21_CLIENT_PRE.md) (function() { let audioCtx = null; let workletLoaded = false; function getAudioContext() { if (!audioCtx) { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } if (audioCtx.state === 'suspended') { audioCtx.resume(); } return audioCtx; } async function initAudioWorklet() { if (workletLoaded) return true; const ctx = getAudioContext(); try { if (ctx.audioWorklet) { await ctx.audioWorklet.addModule('/static/js/services/sonicAudioWorklet.js'); workletLoaded = true; console.log('[SonicAudio] AudioWorklet registered successfully.'); return true; } } catch (err) { console.warn('[SonicAudio] AudioWorklet initialization fallback:', err.message); } return false; } function analyzeAudioBufferChannels(audioBuffer) { if (!audioBuffer) return { channels: 1, isStereo: false, label: 'MONO' }; const numChannels = audioBuffer.numberOfChannels; const isStereo = numChannels >= 2; return { channels: numChannels, isStereo: isStereo, label: isStereo ? 'STEREO' : 'MONO', sampleRate: audioBuffer.sampleRate, duration: audioBuffer.duration, length: audioBuffer.length }; } async function decodeAudioFile(file) { const ctx = getAudioContext(); const arrayBuffer = await file.arrayBuffer(); const audioBuffer = await ctx.decodeAudioData(arrayBuffer); const channelInfo = analyzeAudioBufferChannels(audioBuffer); return { audioBuffer, channelInfo }; } // ── 1. Non-Destructive Edit Decision List (EDL VFS Engine - 21_CLIENT_PRE.md §4) ── function createEDL(bufferId, buffer) { if (!buffer) return []; return [{ id: 'seg_' + Math.random().toString(36).substr(2, 9), sourceBufferId: bufferId, startSample: 0, length: buffer.length, playbackRate: 1.0, isSilence: false, isReversed: false }]; } function deleteEDLRange(edlList, startSec, endSec, sampleRate) { const startSample = Math.floor(startSec * sampleRate); const endSample = Math.floor(endSec * sampleRate); const result = []; let currentPos = 0; for (const seg of edlList) { const segStart = currentPos; const segEnd = currentPos + seg.length; if (segEnd <= startSample || segStart >= endSample) { // Completely outside delete window result.push({ ...seg }); } else { // Overlaps delete window if (segStart < startSample) { const keepLen = startSample - segStart; result.push({ ...seg, id: 'seg_' + Math.random().toString(36).substr(2, 9), length: keepLen }); } if (segEnd > endSample) { const cutOffset = endSample - segStart; const keepLen = segEnd - endSample; result.push({ ...seg, id: 'seg_' + Math.random().toString(36).substr(2, 9), startSample: seg.startSample + cutOffset, length: keepLen }); } } currentPos = segEnd; } return result; } function renderEDLToBuffer(edlList, sourceBuffersMap, sampleRate) { let totalSamples = 0; for (const seg of edlList) { totalSamples += seg.length; } const ctx = getAudioContext(); if (totalSamples === 0) { return ctx.createBuffer(2, sampleRate * 0.1, sampleRate); } const numChannels = 2; const outBuffer = ctx.createBuffer(numChannels, totalSamples, sampleRate); const outL = outBuffer.getChannelData(0); const outR = outBuffer.getChannelData(1); let writeOffset = 0; for (const seg of edlList) { if (seg.isSilence) { writeOffset += seg.length; continue; } const srcBuffer = sourceBuffersMap[seg.sourceBufferId]; if (!srcBuffer) { writeOffset += seg.length; continue; } const srcL = srcBuffer.getChannelData(0); const srcR = srcBuffer.numberOfChannels > 1 ? srcBuffer.getChannelData(1) : srcL; const len = Math.min(seg.length, srcBuffer.length - seg.startSample); for (let i = 0; i < len; i++) { const readIdx = seg.isReversed ? seg.startSample + len - 1 - i : seg.startSample + i; if (readIdx >= 0 && readIdx < srcBuffer.length) { outL[writeOffset + i] = srcL[readIdx]; outR[writeOffset + i] = srcR[readIdx]; } } writeOffset += seg.length; } return outBuffer; } // ── 2. Client-Side DSP Core Engine (21_CLIENT_PRE.md §3 & §5) ── // Constant-Power Panning Math function calculateConstantPowerPan(panVal, volDb = 0) { const gain = Math.pow(10, volDb / 20); const theta = ((Math.max(-1, Math.min(1, panVal)) + 1) / 2) * (Math.PI / 2); return { gainL: Math.cos(theta) * gain, gainR: Math.sin(theta) * gain, gainLinear: gain }; } // Dynamics Compressor / Limiter function applyDynamicsCompressor(audioBuffer, thresholdDb = -20, ratio = 4.0, attackMs = 10, releaseMs = 100) { const ctx = getAudioContext(); const numChannels = audioBuffer.numberOfChannels; const sampleRate = audioBuffer.sampleRate; const len = audioBuffer.length; const outBuffer = ctx.createBuffer(numChannels, len, sampleRate); const attackCoef = Math.exp(-1 / (sampleRate * (attackMs / 1000))); const releaseCoef = Math.exp(-1 / (sampleRate * (releaseMs / 1000))); const thresholdLinear = Math.pow(10, thresholdDb / 20); const channelsData = []; const outData = []; for (let ch = 0; ch < numChannels; ch++) { channelsData.push(audioBuffer.getChannelData(ch)); outData.push(outBuffer.getChannelData(ch)); } let envelope = 0; const blockSize = 128; for (let i = 0; i < len; i += blockSize) { const currentBlockSize = Math.min(blockSize, len - i); // Compute RMS energy of block let sumSq = 0; for (let b = 0; b < currentBlockSize; b++) { const sampleL = channelsData[0][i + b]; sumSq += sampleL * sampleL; } const rms = Math.sqrt(sumSq / currentBlockSize); // Envelope follower if (rms > envelope) { envelope = attackCoef * envelope + (1 - attackCoef) * rms; } else { envelope = releaseCoef * envelope + (1 - releaseCoef) * rms; } // Target Gain calculation let targetGain = 1.0; if (envelope > thresholdLinear && envelope > 0) { const envDb = 20 * Math.log10(envelope); const overDb = envDb - thresholdDb; const compressedDb = thresholdDb + overDb / ratio; targetGain = Math.pow(10, (compressedDb - envDb) / 20); } for (let b = 0; b < currentBlockSize; b++) { for (let ch = 0; ch < numChannels; ch++) { outData[ch][i + b] = channelsData[ch][i + b] * targetGain; } } } return outBuffer; } // Phase Vocoder / Overlap-Add Time Stretch function applyPhaseVocoderStretch(audioBuffer, speedRatio) { if (speedRatio <= 0.01 || Math.abs(speedRatio - 1.0) < 0.001) return audioBuffer; const ctx = getAudioContext(); const numChannels = audioBuffer.numberOfChannels; const sampleRate = audioBuffer.sampleRate; const inLen = audioBuffer.length; const outLen = Math.floor(inLen / speedRatio); const outBuffer = ctx.createBuffer(numChannels, outLen, sampleRate); const windowSize = 1024; const inHop = Math.floor(windowSize / 4); const outHop = Math.floor(inHop / speedRatio); // Hanning Window const win = new Float32Array(windowSize); for (let n = 0; n < windowSize; n++) { win[n] = 0.5 * (1 - Math.cos((2 * Math.PI * n) / (windowSize - 1))); } for (let ch = 0; ch < numChannels; ch++) { const inData = audioBuffer.getChannelData(ch); const outData = outBuffer.getChannelData(ch); let inPos = 0; let outPos = 0; while (inPos + windowSize < inLen && outPos + windowSize < outLen) { for (let n = 0; n < windowSize; n++) { outData[outPos + n] += inData[Math.floor(inPos) + n] * win[n]; } inPos += inHop; outPos += outHop; } } return outBuffer; } window.SonicAudio = { getAudioContext, initAudioWorklet, analyzeAudioBufferChannels, decodeAudioFile, // EDL VFS createEDL, deleteEDLRange, renderEDLToBuffer, // DSP Core calculateConstantPowerPan, applyDynamicsCompressor, applyPhaseVocoderStretch }; })();