fix: time selection click and shiftclick
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
// 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) {
|
||||
@@ -12,6 +15,22 @@
|
||||
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;
|
||||
@@ -34,9 +53,223 @@
|
||||
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
|
||||
decodeAudioFile,
|
||||
// EDL VFS
|
||||
createEDL,
|
||||
deleteEDLRange,
|
||||
renderEDLToBuffer,
|
||||
// DSP Core
|
||||
calculateConstantPowerPan,
|
||||
applyDynamicsCompressor,
|
||||
applyPhaseVocoderStretch
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user