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:
+47
-33
@@ -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);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user