fix: sửa lỗi di chuyển items
This commit is contained in:
@@ -1,174 +0,0 @@
|
||||
# DIAGNOSTIC REPORT: WHY MIDI SIGNAL IS RECEIVED BUT NOT RECORDED / RENDERED
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary & Root Cause Analysis
|
||||
|
||||
Based on the DAW UI screenshot provided, the system is successfully receiving MIDI hardware signals (as indicated by the active VU meter on Track 01 set to `MIDIIN2 (SE49)`), but no MIDI data is being written or displayed on the timeline.
|
||||
|
||||
This issue occurs due to four architectural and state-management gaps in the current implementation.
|
||||
|
||||
---
|
||||
|
||||
## 2. Detailed Root Causes
|
||||
|
||||
### Root Cause 1: Global Transport Record vs. Track Arm Disconnect
|
||||
|
||||
* **Observed State:** Track 01 has its individual Arm `[R]` button active (red indicator ON). However, the Global Transport Record button (red circle on the top toolbar) is inactive/stopped at time position `0:01.951`.
|
||||
* **Technical Issue:** Arming a track only enables Live Monitoring (routing MIDI input to the virtual synth engine for real-time audio playback). Recording MIDI into timeline buffers requires both **Track Arm = `true**` AND **Transport Engine State = `RECORDING**`.
|
||||
|
||||
```text
|
||||
[ Track Armed ] + [ Transport STOPPED ] --> Live Monitoring ONLY (VU meter lights up, no recording)
|
||||
[ Track Armed ] + [ Transport RECORDING ] --> Live Monitoring + Event Buffer Write + Canvas Redraw
|
||||
|
||||
```
|
||||
|
||||
### Root Cause 2: Gate Condition in `handleMIDIMessage`
|
||||
|
||||
In the client recording engine (`ClientMIDIRecorder`), incoming MIDI events trigger live synth audio, but note recording is gated behind a transport flag:
|
||||
|
||||
```javascript
|
||||
handleMIDIMessage(event) {
|
||||
// BUG: If global transport is not in RECORD mode, execution stops here.
|
||||
// Synth gets triggered elsewhere, but recordedNotes array remains empty.
|
||||
if (!this.isRecording) return;
|
||||
|
||||
const [status, pitch, velocity] = event.data;
|
||||
// ... logic to write to activeNotes and recordedNotes
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Root Cause 3: Absence of Real-Time Canvas Redraw Loop
|
||||
|
||||
For notes to render dynamically inside the MIDI Item clip as keys are pressed:
|
||||
|
||||
* The UI Canvas must run a `requestAnimationFrame` render loop while `isRecording === true`.
|
||||
* The renderer must query the `activeNotes` Map (currently held keys) in addition to finalized `recordedNotes`.
|
||||
* If the UI only renders on static session updates (e.g., when clicking or stopping transport), live notes will not appear on screen during playback.
|
||||
|
||||
### Root Cause 4: Track Target ID Unbound to Input Stream
|
||||
|
||||
If multiple tracks exist, `ClientMIDIRecorder` must know which `track_id` is currently armed and matched to device `MIDIIN2 (SE49)`. If events arrive without a target track context, they cannot be routed into the target `MIDIItem.source_data.notes` array.
|
||||
|
||||
---
|
||||
|
||||
## 3. Technical Solutions & Code Adjustments
|
||||
|
||||
### Step 1: Ensure Dual-Stage Recording State Verification
|
||||
|
||||
Update the transport control logic so pressing **Record + Play** on the top toolbar initializes active record buffers on all armed tracks:
|
||||
|
||||
```javascript
|
||||
// Transport Controller
|
||||
function startTransportRecording() {
|
||||
const armedTracks = session.tracks.filter(t => t.is_armed);
|
||||
|
||||
if (armedTracks.length === 0) {
|
||||
console.warn("No tracks armed for recording.");
|
||||
startPlaybackOnly();
|
||||
return;
|
||||
}
|
||||
|
||||
// Activate global transport record state
|
||||
transport.isRecording = true;
|
||||
transport.isPlaying = true;
|
||||
|
||||
// Initialize temporary recording items on each armed track
|
||||
armedTracks.forEach(track => {
|
||||
const newRecordingItem = {
|
||||
id: `rec_item_${Date.now()}`,
|
||||
type: "MIDI_ITEM",
|
||||
start_bar: transport.currentBar,
|
||||
duration_bars: 0.1, // Expands dynamically during recording
|
||||
clip_start_offset_bars: 0.0,
|
||||
source_data: { total_buffer_bars: 8.0, notes: [] }
|
||||
};
|
||||
|
||||
track.activeRecordingItem = newRecordingItem;
|
||||
midiRecorder.start(track.id, transport.currentBar);
|
||||
});
|
||||
|
||||
// Start UI animation loop for live waveform/note preview
|
||||
requestAnimationFrame(renderLiveRecordingUI);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Step 2: Live MIDI Note Binding & Duration Expansion
|
||||
|
||||
Update `ClientMIDIRecorder` to feed both the active buffer and the active recording clip:
|
||||
|
||||
```javascript
|
||||
handleMIDIMessage(event) {
|
||||
const [status, pitch, velocity] = event.data;
|
||||
const command = status >> 4;
|
||||
|
||||
// 1. Always trigger Live Audio Preview (VU Meter + Synth Node)
|
||||
this.triggerSynthPreview(pitch, velocity);
|
||||
|
||||
// 2. Gate recording buffer write behind global transport record state
|
||||
if (!transport.isRecording || !this.targetTrack) return;
|
||||
|
||||
const currentBeat = this.calculateLatencyCompensatedBeat();
|
||||
|
||||
// Command 0x9: Note On
|
||||
if (command === 0x9 && velocity > 0) {
|
||||
const liveNote = {
|
||||
id: `note_${Date.now()}_${pitch}`,
|
||||
pitch: pitch,
|
||||
start_beat: currentBeat,
|
||||
duration_beats: 0.25, // Default initial length until Note Off
|
||||
velocity: velocity / 127.0
|
||||
};
|
||||
|
||||
this.activeNotes.set(pitch, liveNote);
|
||||
this.targetTrack.activeRecordingItem.source_data.notes.push(liveNote);
|
||||
}
|
||||
// Command 0x8: Note Off
|
||||
else if (command === 0x8 || (command === 0x9 && velocity === 0)) {
|
||||
if (this.activeNotes.has(pitch)) {
|
||||
const note = this.activeNotes.get(pitch);
|
||||
note.duration_beats = Math.max(0.125, currentBeat - note.start_beat);
|
||||
this.activeNotes.delete(pitch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Step 3: Real-Time UI Canvas Render Loop
|
||||
|
||||
Add real-time item length expansion and live note drawing on the main canvas during recording:
|
||||
|
||||
```javascript
|
||||
function renderLiveRecordingUI() {
|
||||
if (!transport.isRecording) return;
|
||||
|
||||
const currentBar = transport.getCurrentBarPosition();
|
||||
|
||||
session.tracks.forEach(track => {
|
||||
if (track.is_armed && track.activeRecordingItem) {
|
||||
const item = track.activeRecordingItem;
|
||||
|
||||
// Expand item duration on timeline as playhead moves forward
|
||||
item.duration_bars = Math.max(0.5, currentBar - item.start_bar);
|
||||
|
||||
// Draw item bounding box and active/completed MIDI note rectangles
|
||||
drawTimelineItem(trackCanvasCtx, item);
|
||||
}
|
||||
});
|
||||
|
||||
requestAnimationFrame(renderLiveRecordingUI);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Checklist to Fix in Your Application
|
||||
|
||||
* [ ] Check if clicking top toolbar **Record + Play** sets `transport.isRecording = true`.
|
||||
* [ ] Verify that Track 01 generates a temporary `activeRecordingItem` on record start.
|
||||
* [ ] Confirm `requestAnimationFrame` is re-rendering the canvas continuously while transport is moving.
|
||||
* [ ] Ensure incoming MIDI events on `MIDIIN2 (SE49)` push notes into Track 01's item note array rather than just playing the synth.
|
||||
Reference in New Issue
Block a user