fix(piano-roll): playhead seek + sound cracking

- Piano roll ruler click during playback now seeks and continues playing
- ADSR envelope uses linearRampToValueAtTime for crack-free release
- stopAll ramps gain to 0 in 20ms before stopping oscillators
This commit is contained in:
2026-07-26 12:03:29 +07:00
parent 1ffdec68c6
commit 4e880850f5
4 changed files with 69 additions and 46 deletions
+14 -10
View File
@@ -125,7 +125,7 @@
}
osc.type = oscType;
osc.frequency.value = freq;
osc.frequency.setValueAtTime(freq, 0);
const startAt = startTime !== undefined ? startTime : ctx.currentTime;
const durSec = durationMs / 1000;
@@ -139,9 +139,8 @@
noteGain.gain.linearRampToValueAtTime(targetGain * sustainLevel, startAt + attackTime + decayTime);
const releaseStart = startAt + Math.max(attackTime + decayTime, durSec);
noteGain.gain.setValueAtTime(targetGain * sustainLevel, releaseStart);
const rampEnd = Math.max(0.001, targetGain * sustainLevel * 0.01);
noteGain.gain.exponentialRampToValueAtTime(rampEnd, releaseStart + releaseTime);
noteGain.gain.linearRampToValueAtTime(targetGain * sustainLevel, releaseStart);
noteGain.gain.linearRampToValueAtTime(0, releaseStart + releaseTime);
osc.connect(noteGain);
@@ -150,7 +149,7 @@
osc.start(startAt);
const stopAt = releaseStart + releaseTime + 0.05;
const stopAt = releaseStart + releaseTime + 0.02;
osc.stop(stopAt);
const oscId = `${note}_${Date.now()}_${Math.random()}`;
@@ -166,17 +165,22 @@
stopAll: function () {
const ctx = getCtx();
const now = ctx.currentTime;
Object.values(activeOscillators).forEach(entry => {
try {
if (entry.gain) {
entry.gain.gain.cancelScheduledValues(ctx.currentTime);
entry.gain.gain.setValueAtTime(entry.gain.gain.value || 1, ctx.currentTime);
entry.gain.gain.linearRampToValueAtTime(0.001, ctx.currentTime + 0.008);
entry.gain.gain.cancelScheduledValues(now);
entry.gain.gain.setValueAtTime(entry.gain.gain.value || 0.8, now);
entry.gain.gain.linearRampToValueAtTime(0, now + 0.02);
}
if (entry.osc) {
try { entry.osc.stop(now + 0.025); } catch (e) { }
}
if (entry.osc) entry.osc.stop(ctx.currentTime + 0.01);
} catch (e) { }
});
Object.keys(activeOscillators).forEach(k => delete activeOscillators[k]);
setTimeout(() => {
Object.keys(activeOscillators).forEach(k => delete activeOscillators[k]);
}, 50);
},
// Save user SoundFont to IndexedDB via window.SonicStorage