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
+47 -33
View File
@@ -4543,7 +4543,7 @@ const AIPresetModal = ({ isOpen, onClose }) => {
}, "Đóng"))));
};
const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNotes, onSaveNotes, setSubTabs, onPlayPause, onStop, isPlaying, playPreviewNote, showToast, midiDevices, recordingState, recTempMidiNotes, onRecord, selectedMidiInputId, onMidiInputSelect, activeMidiPitches, onInstrumentSelect, onRescheduleMidi }) => {
const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNotes, onSaveNotes, setSubTabs, onPlayPause, onStop, isPlaying, playPreviewNote, showToast, midiDevices, recordingState, recTempMidiNotes, onRecord, selectedMidiInputId, onMidiInputSelect, activeMidiPitches, onInstrumentSelect, onRescheduleMidi, onSeekPlayhead }) => {
const [activeRollTool, setActiveRollTool] = React.useState('select');
const [snapVal, setSnapVal] = React.useState('1/16');
const [ccMode, setCcMode] = React.useState('velocity');
@@ -5875,13 +5875,10 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
return;
}
if (clickTime >= 0) {
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: clickTime, isPlaying: false } : s));
if (isPlaying) {
onStop();
setTimeout(() => {
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: clickTime } : s));
onPlayPause();
}, 200);
if (onSeekPlayhead) {
onSeekPlayhead(clickTime);
} else {
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: clickTime } : s));
}
}
const snappedStartBeat = getSnapBeat(clickBeat, snapVal);
@@ -15819,31 +15816,48 @@ const App = () => {
activeMidiPitches: activeMidiPitches,
onInstrumentSelect: (trackId) => { openInstrumentSelector(trackId); },
onRescheduleMidi: (updatedNotes) => {
const playingSub = subTabs.find(s => s.id === activeTab && s.type === 'PIANO_ROLL' && s.isPlaying);
if (playingSub) {
const offset = playingSub.currentTime || 0;
const ctx = getAudioContext();
const tNode = activeTrackNodesRef.current[playingSub.trackId];
if (tNode && tNode.gainNode) {
tNode.gainNode.gain.setValueAtTime(tNode.gainNode.gain.value || 1, ctx.currentTime);
tNode.gainNode.gain.linearRampToValueAtTime(0.001, ctx.currentTime + 0.04);
}
setTimeout(() => {
window.SonicSF.stopAll();
if (tNode && tNode.gainNode) {
const trackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === playingSub.trackId) : null;
const volDb = trackData ? (trackData.volumeDb ?? 0) : 0;
const volLinear = volDb <= -50 ? 0 : Math.pow(10, volDb / 20);
tNode.gainNode.gain.setValueAtTime(0.001, ctx.currentTime);
tNode.gainNode.gain.linearRampToValueAtTime(volLinear || 0.8, ctx.currentTime + 0.015);
}
startOffsetTimeRef.current = offset;
startAudioTimeRef.current = ctx.currentTime;
startBufferOffsetRef.current = offset * (playingSub.speed || 1.0);
schedulePianoRollMidi(playingSub, offset, updatedNotes);
}, 50);
}
}
const playingSub = subTabs.find(s => s.id === activeTab && s.type === 'PIANO_ROLL' && s.isPlaying);
if (playingSub) {
const offset = playingSub.currentTime || 0;
const ctx = getAudioContext();
const tNode = activeTrackNodesRef.current[playingSub.trackId];
if (tNode && tNode.gainNode) {
tNode.gainNode.gain.setValueAtTime(tNode.gainNode.gain.value || 1, ctx.currentTime);
tNode.gainNode.gain.linearRampToValueAtTime(0.001, ctx.currentTime + 0.04);
}
setTimeout(() => {
window.SonicSF.stopAll();
if (tNode && tNode.gainNode) {
const trackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === playingSub.trackId) : null;
const volDb = trackData ? (trackData.volumeDb ?? 0) : 0;
const volLinear = volDb <= -50 ? 0 : Math.pow(10, volDb / 20);
tNode.gainNode.gain.setValueAtTime(0.001, ctx.currentTime);
tNode.gainNode.gain.linearRampToValueAtTime(volLinear || 0.8, ctx.currentTime + 0.015);
}
startOffsetTimeRef.current = offset;
startAudioTimeRef.current = ctx.currentTime;
startBufferOffsetRef.current = offset * (playingSub.speed || 1.0);
schedulePianoRollMidi(playingSub, offset, updatedNotes);
}, 50);
}
},
onSeekPlayhead: (clickTime) => {
const seekSt = subTabs.find(s => s.id === activeTab && s.type === 'PIANO_ROLL');
if (!seekSt) return;
if (seekSt.isPlaying) {
stopAllPlayback();
window.SonicSF.stopAll();
setSubTabs(prev => prev.map(s => s.id === seekSt.id ? { ...s, currentTime: clickTime, isPlaying: true } : s));
const ctx = getAudioContext();
startOffsetTimeRef.current = clickTime;
startAudioTimeRef.current = ctx.currentTime;
startBufferOffsetRef.current = clickTime * (seekSt.speed || 1.0);
schedulePianoRollMidi(seekSt, clickTime);
startSubTabPlayback(seekSt, clickTime);
} else {
setSubTabs(prev => prev.map(s => s.id === seekSt.id ? { ...s, currentTime: clickTime } : s));
}
}
});
}
const subTrack = tracks.find(t => t.id === st.trackId);