fix: session-absolute timing for playhead + MIDI scheduling

- st.currentTime is now session-absolute (0 = session bar 0)
- schedulePianoRollMidi adds sessionBeatOffset to note timing so
  notes at renderBeatOffset beats are scheduled with correct delay
- Playhead rendering: removed renderBeatOffset (uses st.currentTime
  directly as session-absolute beats)
- handleEditMidiInTab/handleSwitchMidiItem: currentTime = 0
- Ruler click: clickTime = clickBeat * beatSec (session-absolute)
This commit is contained in:
2026-07-27 19:55:58 +07:00
parent a60607c673
commit e14554a75a
2 changed files with 21 additions and 18 deletions
+14 -11
View File
@@ -4646,7 +4646,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
var trk = (activeTracks || []).find(function(t) { return t.id === match._trackId; }); var trk = (activeTracks || []).find(function(t) { return t.id === match._trackId; });
var newBeatOff = (match.startTime / secondsPerBar) * timeSigNum; var newBeatOff = (match.startTime / secondsPerBar) * timeSigNum;
var spb = 60.0 / (parseInt(bpm) || 120); var spb = 60.0 / (parseInt(bpm) || 120);
var newTime = -newBeatOff * spb; var newTime = 0;
setSubTabs(function(prev) { setSubTabs(function(prev) {
return prev.map(function(s) { return prev.map(function(s) {
if (s.id !== st.id) return s; if (s.id !== st.id) return s;
@@ -4986,7 +4986,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
// Draw playhead // Draw playhead
if (st.currentTime !== undefined && st.currentTime !== null) { if (st.currentTime !== undefined && st.currentTime !== null) {
const phBeat = renderBeatOffset + st.currentTime / (60.0 / (parseInt(bpm) || 120)); const phBeat = st.currentTime / (60.0 / (parseInt(bpm) || 120));
const phX = phBeat * pixelsPerBeat; const phX = phBeat * pixelsPerBeat;
if (phX >= 0 && phX <= viewWidth) { if (phX >= 0 && phX <= viewWidth) {
ctx.strokeStyle = '#f59e0b'; ctx.strokeStyle = '#f59e0b';
@@ -6017,7 +6017,7 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
const rect = e.currentTarget.getBoundingClientRect(); const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left + e.currentTarget.scrollLeft; const x = e.clientX - rect.left + e.currentTarget.scrollLeft;
const clickBeat = x / pixelsPerBeat; const clickBeat = x / pixelsPerBeat;
const clickTime = (clickBeat - renderBeatOffset) * beatSec; const clickTime = clickBeat * beatSec;
const clickInRange = loopStartBeat !== null && loopEndBeat !== null && clickBeat >= loopStartBeat && clickBeat <= loopEndBeat; const clickInRange = loopStartBeat !== null && loopEndBeat !== null && clickBeat >= loopStartBeat && clickBeat <= loopEndBeat;
if (e.ctrlKey || e.metaKey) { if (e.ctrlKey || e.metaKey) {
setLoopStartBeat(null); setLoopStartBeat(null);
@@ -8282,10 +8282,6 @@ const App = () => {
const tabLabel = `Piano Roll: ${midiItem.name || 'MIDI'}`; const tabLabel = `Piano Roll: ${midiItem.name || 'MIDI'}`;
const ctx = getAudioContext(); const ctx = getAudioContext();
const silentBuffer = ctx.createBuffer(1, 128, ctx.sampleRate); const silentBuffer = ctx.createBuffer(1, 128, ctx.sampleRate);
var bpmVal = parseInt(bpm) || 120;
var beatSec = 60.0 / bpmVal;
var secPerBar = beatSec * 4;
var beatOff = (midiItem.startTime / secPerBar) * 4;
const newTab = { const newTab = {
id: tabId, id: tabId,
label: tabLabel, label: tabLabel,
@@ -8298,7 +8294,7 @@ const App = () => {
buffer: silentBuffer, buffer: silentBuffer,
instrumentProgram: track.instrumentProgram, instrumentProgram: track.instrumentProgram,
instrumentName: track.instrumentName, instrumentName: track.instrumentName,
currentTime: -beatOff * beatSec, currentTime: 0,
isPlaying: false, isPlaying: false,
selectionStart: null, selectionStart: null,
selectionEnd: null, selectionEnd: null,
@@ -10403,8 +10399,15 @@ const App = () => {
var allTracks = activeTracksRef.current || []; var allTracks = activeTracksRef.current || [];
var mainCh = 0; var mainCh = 0;
for (var i = 0; i < allTracks.length; i++) { if (allTracks[i].id === st.trackId) { mainCh = i >= 9 ? i + 1 : i; if (mainCh >= 16) mainCh = 0; break; } } for (var i = 0; i < allTracks.length; i++) { if (allTracks[i].id === st.trackId) { mainCh = i >= 9 ? i + 1 : i; if (mainCh >= 16) mainCh = 0; break; } }
// Compute session beat offset from target item
var sessionBeatOffset = 0;
var targetTrk = allTracks.find(function(t) { return t.id === st.trackId; });
if (targetTrk) {
var targetIt = (targetTrk.midiItems || []).find(function(m) { return m.id === st.target_id; });
if (targetIt) sessionBeatOffset = (targetIt.startTime / ((60.0 / bpmVal) * 4)) * 4;
}
midiNotes.forEach(note => { midiNotes.forEach(note => {
const noteOnBeat = note.start_beat || 0; const noteOnBeat = (note.start_beat || 0) + sessionBeatOffset;
const noteDurBeat = note.duration_beats || 1; const noteDurBeat = note.duration_beats || 1;
const noteStartSec = noteOnBeat * secondsPerBeat; const noteStartSec = noteOnBeat * secondsPerBeat;
const noteDurSec = noteDurBeat * secondsPerBeat; const noteDurSec = noteDurBeat * secondsPerBeat;
@@ -10424,12 +10427,12 @@ const App = () => {
var ghostTrack = allTracks.find(function(t) { return t.id === layer.trackId; }); var ghostTrack = allTracks.find(function(t) { return t.id === layer.trackId; });
var ghostProg = layer.instrumentProgram !== undefined ? layer.instrumentProgram : (ghostTrack ? ghostTrack.instrumentProgram : undefined); var ghostProg = layer.instrumentProgram !== undefined ? layer.instrumentProgram : (ghostTrack ? ghostTrack.instrumentProgram : undefined);
var ghostSynth = layer.synthEngine || (ghostTrack ? ghostTrack.synth_engine : undefined); var ghostSynth = layer.synthEngine || (ghostTrack ? ghostTrack.synth_engine : undefined);
if (ghostProg === undefined && !ghostSynth) return; // skip tracks with no instrument if (ghostProg === undefined && !ghostSynth) return;
var ghostDest = getOrCreateTrackNode(ghostTrack, context); var ghostDest = getOrCreateTrackNode(ghostTrack, context);
var ghostCh = 0; var ghostCh = 0;
for (var i = 0; i < allTracks.length; i++) { if (allTracks[i].id === layer.trackId) { ghostCh = i >= 9 ? i + 1 : i; if (ghostCh >= 16) ghostCh = 0; break; } } for (var i = 0; i < allTracks.length; i++) { if (allTracks[i].id === layer.trackId) { ghostCh = i >= 9 ? i + 1 : i; if (ghostCh >= 16) ghostCh = 0; break; } }
layer.notes.forEach(function(note) { layer.notes.forEach(function(note) {
var beat = note.start_beat || 0; var beat = (note.start_beat || 0) + sessionBeatOffset;
var dur = note.duration_beats || 1; var dur = note.duration_beats || 1;
var startSec = beat * secondsPerBeat; var startSec = beat * secondsPerBeat;
var durSec = dur * secondsPerBeat; var durSec = dur * secondsPerBeat;
File diff suppressed because one or more lines are too long