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"))));
|
}, "Đó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 [activeRollTool, setActiveRollTool] = React.useState('select');
|
||||||
const [snapVal, setSnapVal] = React.useState('1/16');
|
const [snapVal, setSnapVal] = React.useState('1/16');
|
||||||
const [ccMode, setCcMode] = React.useState('velocity');
|
const [ccMode, setCcMode] = React.useState('velocity');
|
||||||
@@ -5875,13 +5875,10 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (clickTime >= 0) {
|
if (clickTime >= 0) {
|
||||||
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: clickTime, isPlaying: false } : s));
|
if (onSeekPlayhead) {
|
||||||
if (isPlaying) {
|
onSeekPlayhead(clickTime);
|
||||||
onStop();
|
} else {
|
||||||
setTimeout(() => {
|
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: clickTime } : s));
|
||||||
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: clickTime } : s));
|
|
||||||
onPlayPause();
|
|
||||||
}, 200);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const snappedStartBeat = getSnapBeat(clickBeat, snapVal);
|
const snappedStartBeat = getSnapBeat(clickBeat, snapVal);
|
||||||
@@ -15819,31 +15816,48 @@ const App = () => {
|
|||||||
activeMidiPitches: activeMidiPitches,
|
activeMidiPitches: activeMidiPitches,
|
||||||
onInstrumentSelect: (trackId) => { openInstrumentSelector(trackId); },
|
onInstrumentSelect: (trackId) => { openInstrumentSelector(trackId); },
|
||||||
onRescheduleMidi: (updatedNotes) => {
|
onRescheduleMidi: (updatedNotes) => {
|
||||||
const playingSub = subTabs.find(s => s.id === activeTab && s.type === 'PIANO_ROLL' && s.isPlaying);
|
const playingSub = subTabs.find(s => s.id === activeTab && s.type === 'PIANO_ROLL' && s.isPlaying);
|
||||||
if (playingSub) {
|
if (playingSub) {
|
||||||
const offset = playingSub.currentTime || 0;
|
const offset = playingSub.currentTime || 0;
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
const tNode = activeTrackNodesRef.current[playingSub.trackId];
|
const tNode = activeTrackNodesRef.current[playingSub.trackId];
|
||||||
if (tNode && tNode.gainNode) {
|
if (tNode && tNode.gainNode) {
|
||||||
tNode.gainNode.gain.setValueAtTime(tNode.gainNode.gain.value || 1, ctx.currentTime);
|
tNode.gainNode.gain.setValueAtTime(tNode.gainNode.gain.value || 1, ctx.currentTime);
|
||||||
tNode.gainNode.gain.linearRampToValueAtTime(0.001, ctx.currentTime + 0.04);
|
tNode.gainNode.gain.linearRampToValueAtTime(0.001, ctx.currentTime + 0.04);
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.SonicSF.stopAll();
|
window.SonicSF.stopAll();
|
||||||
if (tNode && tNode.gainNode) {
|
if (tNode && tNode.gainNode) {
|
||||||
const trackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === playingSub.trackId) : null;
|
const trackData = activeTracksRef.current ? activeTracksRef.current.find(t => t.id === playingSub.trackId) : null;
|
||||||
const volDb = trackData ? (trackData.volumeDb ?? 0) : 0;
|
const volDb = trackData ? (trackData.volumeDb ?? 0) : 0;
|
||||||
const volLinear = volDb <= -50 ? 0 : Math.pow(10, volDb / 20);
|
const volLinear = volDb <= -50 ? 0 : Math.pow(10, volDb / 20);
|
||||||
tNode.gainNode.gain.setValueAtTime(0.001, ctx.currentTime);
|
tNode.gainNode.gain.setValueAtTime(0.001, ctx.currentTime);
|
||||||
tNode.gainNode.gain.linearRampToValueAtTime(volLinear || 0.8, ctx.currentTime + 0.015);
|
tNode.gainNode.gain.linearRampToValueAtTime(volLinear || 0.8, ctx.currentTime + 0.015);
|
||||||
}
|
}
|
||||||
startOffsetTimeRef.current = offset;
|
startOffsetTimeRef.current = offset;
|
||||||
startAudioTimeRef.current = ctx.currentTime;
|
startAudioTimeRef.current = ctx.currentTime;
|
||||||
startBufferOffsetRef.current = offset * (playingSub.speed || 1.0);
|
startBufferOffsetRef.current = offset * (playingSub.speed || 1.0);
|
||||||
schedulePianoRollMidi(playingSub, offset, updatedNotes);
|
schedulePianoRollMidi(playingSub, offset, updatedNotes);
|
||||||
}, 50);
|
}, 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);
|
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.type = oscType;
|
||||||
osc.frequency.value = freq;
|
osc.frequency.setValueAtTime(freq, 0);
|
||||||
|
|
||||||
const startAt = startTime !== undefined ? startTime : ctx.currentTime;
|
const startAt = startTime !== undefined ? startTime : ctx.currentTime;
|
||||||
const durSec = durationMs / 1000;
|
const durSec = durationMs / 1000;
|
||||||
@@ -139,9 +139,8 @@
|
|||||||
noteGain.gain.linearRampToValueAtTime(targetGain * sustainLevel, startAt + attackTime + decayTime);
|
noteGain.gain.linearRampToValueAtTime(targetGain * sustainLevel, startAt + attackTime + decayTime);
|
||||||
|
|
||||||
const releaseStart = startAt + Math.max(attackTime + decayTime, durSec);
|
const releaseStart = startAt + Math.max(attackTime + decayTime, durSec);
|
||||||
noteGain.gain.setValueAtTime(targetGain * sustainLevel, releaseStart);
|
noteGain.gain.linearRampToValueAtTime(targetGain * sustainLevel, releaseStart);
|
||||||
const rampEnd = Math.max(0.001, targetGain * sustainLevel * 0.01);
|
noteGain.gain.linearRampToValueAtTime(0, releaseStart + releaseTime);
|
||||||
noteGain.gain.exponentialRampToValueAtTime(rampEnd, releaseStart + releaseTime);
|
|
||||||
|
|
||||||
osc.connect(noteGain);
|
osc.connect(noteGain);
|
||||||
|
|
||||||
@@ -150,7 +149,7 @@
|
|||||||
|
|
||||||
osc.start(startAt);
|
osc.start(startAt);
|
||||||
|
|
||||||
const stopAt = releaseStart + releaseTime + 0.05;
|
const stopAt = releaseStart + releaseTime + 0.02;
|
||||||
osc.stop(stopAt);
|
osc.stop(stopAt);
|
||||||
|
|
||||||
const oscId = `${note}_${Date.now()}_${Math.random()}`;
|
const oscId = `${note}_${Date.now()}_${Math.random()}`;
|
||||||
@@ -166,17 +165,22 @@
|
|||||||
|
|
||||||
stopAll: function () {
|
stopAll: function () {
|
||||||
const ctx = getCtx();
|
const ctx = getCtx();
|
||||||
|
const now = ctx.currentTime;
|
||||||
Object.values(activeOscillators).forEach(entry => {
|
Object.values(activeOscillators).forEach(entry => {
|
||||||
try {
|
try {
|
||||||
if (entry.gain) {
|
if (entry.gain) {
|
||||||
entry.gain.gain.cancelScheduledValues(ctx.currentTime);
|
entry.gain.gain.cancelScheduledValues(now);
|
||||||
entry.gain.gain.setValueAtTime(entry.gain.gain.value || 1, ctx.currentTime);
|
entry.gain.gain.setValueAtTime(entry.gain.gain.value || 0.8, now);
|
||||||
entry.gain.gain.linearRampToValueAtTime(0.001, ctx.currentTime + 0.008);
|
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) { }
|
} 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
|
// Save user SoundFont to IndexedDB via window.SonicStorage
|
||||||
|
|||||||
@@ -255,3 +255,8 @@
|
|||||||
- **Tóm tắt thay đổi:** (1) Ruler: cursor default, click trong range → drag move (hand cursor → move), click ngoài range → seek/drag tạo selection mới. (2) `soundfontPlayer.js`: `activeOscillators` lưu `{osc, gain}` thay vì `osc`; `stopAll` ramp gain về 0 trong 8ms trước khi `osc.stop(10ms)` — loại bỏ crackling khi realtime update. Cũng xóa `setIsLooping(true)` trong ruler drag cũ.
|
- **Tóm tắt thay đổi:** (1) Ruler: cursor default, click trong range → drag move (hand cursor → move), click ngoài range → seek/drag tạo selection mới. (2) `soundfontPlayer.js`: `activeOscillators` lưu `{osc, gain}` thay vì `osc`; `stopAll` ramp gain về 0 trong 8ms trước khi `osc.stop(10ms)` — loại bỏ crackling khi realtime update. Cũng xóa `setIsLooping(true)` trong ruler drag cũ.
|
||||||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/services/soundfontPlayer.js`
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/services/soundfontPlayer.js`
|
||||||
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||||||
|
|
||||||
|
### [2026-07-26 11:56] Task: Fix Piano Roll playhead seek + sound cracking
|
||||||
|
- **Tóm tắt thay đổi:** (1) Click ruler khi Piano Roll đang play → seek đến vị trí mới và tiếp tục play (không dừng). (2) ADSR envelope trong `soundfontPlayer.js` dùng `linearRampToValueAtTime` thay `setValueAtTime`/`exponentialRampToValueAtTime` để loại bỏ gain jump gây crackling. `stopAll` ramp gain về 0 trong 20ms trước khi stop oscillator.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/services/soundfontPlayer.js`, `app/static/js/app.precompiled.js`
|
||||||
|
- **Ghi chú/Test (nếu có):** `npm run build` pass.
|
||||||
|
|||||||
Reference in New Issue
Block a user