Commit Graph

542 Commits

Author SHA1 Message Date
3dtours 60141cef3f style: track buttons with border, rounded, 20px height, 14px font, red active
- Each button: border border-zinc-600 rounded-md, h-[20px], text-[14px]
- Active/play-on track: bg-red-700 (red), selected track: bg-yellow-600
- Off track: bg-zinc-800 text-zinc-500
2026-07-27 19:41:06 +07:00
3dtours 6ff2f72c58 fix: playhead at bar 0 when opening MIDI item in session mode
- Removed st.currentTime >= 0 check from playhead rendering
- handleEditMidiInTab: currentTime = -beatOff * beatSec so phBeat=0
- handleSwitchMidiItem: same logic for dropdown switches
- Playhead draws at x=0 (bar 0) when item opens, moves right during
  playback from the initial negative position
2026-07-27 19:37:59 +07:00
3dtours ab4b91a158 fix: shift+scroll velocity changes all selected notes
When selectedNoteIds.length > 0, shift+scroll on any note changes
velocity for all selected notes. Also fixed hit-test to subtract
renderBeatOffset for correct positioning in session mode.
2026-07-27 19:36:54 +07:00
3dtours 1628a34ea0 fix: ghost overlap use inclusive boundary for adjacent items
Changed itemEndBeat <= windowStartBeat → < and
noteAbsEnd <= windowStartBeat → < so abutting items
(Item 1 ends at bar 1, Item 2 starts at bar 1) show
ghost notes from the adjacent item.
2026-07-27 19:31:36 +07:00
3dtours db45efe53c fix: sync piano roll playhead with main timeline position
handleEditMidiInTab now sets currentTime relative to item start:
currentTime = Math.max(0, mainTimelineCurrentTime - midiItem.startTime)
so playhead reflects the main timeline position instead of bar 0.
2026-07-27 19:29:51 +07:00
3dtours fd23b8c4b0 fix: align CC lane velocity label with keybed column
Added 120px spacer before the CC label div so it aligns with
the piano key column (60px) after the track column (120px).
2026-07-27 19:28:51 +07:00
3dtours 1d3c7a7955 fix: preserve playhead session position across dropdown switch
handleSwitchMidiItem now computes new currentTime from the delta
between old and new renderBeatOffset, keeping the playhead at
the same session-absolute time instead of resetting to bar 0.
2026-07-27 19:28:01 +07:00
3dtours 3ed62b3343 fix: subtract renderBeatOffset from mouse beat for correct positioning
- handleGridMouseDown/move: beat = x/pixelsPerBeat - renderBeatOffset
  so hit-testing and note creation use item-relative coordinates
- CC mouse handlers: same offset subtraction
- Ruler click: clickTime subtracts renderBeatOffset so st.currentTime
  remains item-relative
2026-07-27 19:25:29 +07:00
3dtours 3a775fdda7 fix: track button toggle bug — init Set with all track IDs
When activePlayTrackIds was null (default: all play), clicking a track
created an empty Set and deleted nothing. Now initializes with all
track IDs, so click toggles the clicked track off/on correctly.
2026-07-27 18:37:25 +07:00
3dtours 509e171c20 fix: ghost border, track buttons clickable, per-track instrument silent
- Remove dashed border from ghost notes (fill-only at 25% opacity)
- Track column: replaced div overlay with natural flex child; use
  <button> elements with border-l-2 indicator; removed pointer-events
- Ghost playback: skip layers with no instrument assigned (silent)
  instead of playing with default piano
2026-07-27 18:37:25 +07:00
3dtours 0500d16bc7 feat: track column buttons, ghost playback, per-track channels
- Track column: centered 12px buttons, purple active / yellow highlight
- Ghost playback: ghostLayers synced to st.ghostPlayLayers via useEffect;
  schedulePianoRollMidi plays ghost notes from active tracks
- Per-track MIDI channels: each track gets a unique channel based on
  its index, enabling separate instruments per track
2026-07-27 18:37:25 +07:00
3dtours a98dd58744 fix: renderBeatOffset in beats (multiply by timeSigNum)
renderBeatOffset was in bars but used as beat offset in rendering.
Multiplying by timeSigNum=4 converts to beats so items appear at
correct session-absolute positions.
2026-07-27 18:03:44 +07:00
3dtours 1ba9e8379f feat(piano-roll): session bar0, fixed track column, renderBeatOffset
- sessionStartBar = 0 always; notes rendered at session-absolute positions
- renderBeatOffset aligns active/ghost notes to session timeline
- Track column uses absolute overlay with pointer-events for always-visible
- Removed auto-scroll (viewport starts at bar 0 in session mode)
- Grid spacer (120px) aligns grid with ruler track header
2026-07-27 17:57:47 +07:00
3dtours b9113f8a06 fix: dropdown switch keeps ghost notes from other tracks only; track column toggles MIDI play
- handleSwitchMidiItem no longer saves/sets previous item as ghost
- Removed ghostTrackFilter/filteredGhostLayers
- Track column shows clickable track names that toggle play state
- activePlayTrackIds Set controls which tracks' MIDI are playable
2026-07-27 17:40:31 +07:00
3dtours 91ddc3cb6f fix: move ghost decls before totalBeats to fix TDZ 2026-07-27 17:34:14 +07:00
3dtours fb0cd248ad feat(piano-roll): context menu flip, track column, session duration
- Context menu flips upward near viewport bottom edge
- Track column (120px) left of keybed: checkboxes per track to
  filter ghost notes; active track highlighted
- Session mode: canvas extends to full session duration
- Filtered ghost layers via ghostTrackFilter state (Set of track IDs)
- Fixed bar label seek in session mode (was using wrong offset)
2026-07-27 17:32:06 +07:00
3dtours 53a27b5af4 fix: move ghostLayers decl before useLayoutEffect to fix TDZ 2026-07-27 17:15:04 +07:00
3dtours 035d75e504 feat(piano-roll): ghost notes + dropdown item switcher + session sync
- MIDI ghost notes: all non-selected items rendered at 25% opacity
- Dropdown at tab title: switch active edit target across all tracks
- Session Sync mode (default): viewport aligned to session bars
- Isolated mode toggle: bar 0, no ghost notes
- Ghost toggle: show/hide ghost layer (only in session mode)
- Auto-scroll to session position in session sync mode
- Bar labels show absolute session bar numbers
2026-07-27 17:08:59 +07:00
3dtours 095a0680e8 fix(build): remove extra brace at L6795, Babel build now succeeds; feat(ai-copilot): rows=4, resize-y, cursor-aware ArrowUp/Down 2026-07-27 16:36:26 +07:00
3dtours 4ed0b1e5be fix(ai-copilot): patch trực tiếp app.precompiled.js rows=4 + cursor check cho ArrowUp/Down 2026-07-27 16:21:24 +07:00
3dtours 29206d62a3 fix(ai-copilot): tăng chiều cao prompt input rows=4 + resize-y, sửa ArrowUp/ArrowDown chỉ gọi history khi cursor ở đầu/cuối text 2026-07-27 16:16:26 +07:00
3dtours 2b04b32ea8 fix: dùng fluid_synth_program_select thay vì bank_select+program_change
Khi load SF2, preset lookup của program_change dùng search order
(lần lượt qua các SF đã load). Track 1 load SF2→SF2 presets override
SF1 trên tất cả channel, track 2 chọn nhầm preset của SF2.
Fix: program_select(synth, ch, sfHandle, bank, prog) chỉ định rõ
SoundFont ID, không phụ thuộc search order.
2026-07-27 16:08:54 +07:00
3dtours 8965643450 cleanup: bỏ dead code stopNote + debug log _playNoteFluid 2026-07-27 16:00:33 +07:00
3dtours 16e33b2e6a fix: _activeNotes lưu array channel cho mỗi key
Cùng MIDI pitch 60, track 1→ch0, track 2→ch1 cùng key '0:60'
→ single value bị overwrite, stopNote chỉ stop 1 channel.
Fix: _activeNotes[key] = [ch1, ch2, ...], stopNote iterate all.
2026-07-27 16:00:11 +07:00
3dtours 838e173dd7 debug: log _playNoteFluid params + stopNote activeNotes keys 2026-07-27 15:56:28 +07:00
3dtours ce7d23c351 fix: _origChannel tránh channel bị ghi đè bởi mappedCh
Line 378: 'if (channel===undefined) channel=ch' gán channel = FluidSynth
channel (2), sau đó _activeNotes key dùng channel (2) thay vì MIDI
channel (0). stopNote lookup bằng 0 → key mismatch.
Fix: _origChannel = channel ngay đầu hàm, dùng _origChannel cho key.
2026-07-27 15:54:06 +07:00
3dtours 987ecffc6c fix: activeNotes key dùng MIDI channel gốc, không phải FluidSynth channel
Key _activeNotes[ch:midiPitch] dùng ch đã remap (FluidSynth channel),
stopNote lookup bằng MIDI channel (0) → không tìm thấy.
Fix: key = (channel ?? 0) + ':' + midiPitch (MIDI channel gốc).
2026-07-27 15:50:28 +07:00
3dtours cb23d36bc8 fix: multi-track ARM + activeNotes map cho stopNote
1. precompiled: find(t=>isArmed) → filter(t=>isArmed) loop all tracks
2. soundfontPlayer: _activeNotes[ch:pitch] = mappedCh lưu khi noteOn
3. stopNote lookup mappedCh từ _activeNotes thay vì dùng MIDI ch 0
→ Fix: đổi SF track → channel mới → noteOff vẫn tìm đúng voice
2026-07-27 15:48:34 +07:00
3dtours 638d56ce63 fix: engine->channel map cho multi-track ARM
_engineChMap: key=sfId:bank:prog → allocated MIDI channel.
Mỗi track ARM riêng biệt có engKey khác nhau → allocated channel
riêng (0-8 round-robin). _playNoteFluid lookup channel từ map
thay vì dùng MIDI channel gốc (luôn 0).
2026-07-27 15:40:45 +07:00
3dtours 3d7bae94f2 fix: schedule noteOff even cho immediate notes (durationMs < 60000)
Trước đây chỉ schedule noteOff khi delay > 0 (notes scheduled).
Virtual piano keyboard play note immediate (delay===0) với duration=500ms
nhưng không có noteOff → âm thanh play mãi không tắt.
Fix: schedule noteOff khi 0 < durationMs < 60000, không phụ thuộc delay.
2026-07-27 15:36:03 +07:00
3dtours 00566e8ea6 fix: stopNote dùng noteoff+noteon(vel0) thay vì all_sounds_off
- Bỏ _fluid_synth_all_sounds_off (kill cả chord)
- Thử noteoff + noteon(vel=0) - MIDI noteOff alternate
- Log cả 2 return code
2026-07-27 15:27:53 +07:00
3dtours d9ad60768e fix: patch precompiled JS - thêm stopNote + duration 60000
app.precompiled.js được build từ app.jsx cũ (10:54), thiếu:
- SonicSF.stopNote() trong MIDI note-off handler → note không tắt
- duration 500ms (quá ngắn, nên 60000ms = hold)
Patch thủ công bằng Python replace trên minified code.
2026-07-27 15:24:33 +07:00
3dtours 5c7f922eb0 fix: _fluid_synth_all_sounds_off + log noteoff return code
- Dùng _fluid_synth_all_sounds_off thay vì CC120
- Log return value của _fluid_synth_noteoff (0=OK, -1=fail)
- Log ch+pitch trong stopNote
- Sửa printErr filter (bỏ check 'stderr')
2026-07-27 15:21:50 +07:00
3dtours ae6abc3ea5 fix: thêm CC120 (All Sound Off) vào stopNote
_fluid_synth_noteoff chưa đủ để tắt âm với instrument loop.
Thêm CC120 kill tất cả voices trên channel để note tắt ngay.
2026-07-27 15:20:01 +07:00
3dtours cdfe88ba76 fix: force ScriptProcessorNode (bypass AudioWorklet) để debug no-audio 2026-07-27 15:15:20 +07:00
3dtours 05113e17f3 fix: gain 2.0 + zero buffer trước render + log get_gain thực tế
- synth.gain=2.0 (thay vì 0.5 + set_gain 1.0) để tăng volume
- Pre-zero HEAPF32 buffer trước khi gọi write_float
- Log peak, avg, và value từ _fluid_synth_get_gain để debug
2026-07-27 15:14:49 +07:00
3dtours 4d2da5a272 fix: ScriptProcessorNode fallback + worklet process() logging
- ScriptProcessorNode fallback khi AudioWorkletNode fails
- Render trực tiếp trong onaudioprocess callback, không cần setInterval
- Worklet log peak + queue depth mỗi 50 process() calls
- Log audioCtx state + sampleRate khi init
2026-07-27 15:09:47 +07:00
3dtours 4ca6e7c6ad fix: thêm logging debug cho noteOn + render + gain explicit
- _fluid_synth_set_gain(1.0) sau khi tạo synth (đảm bảo gain > 0)
- Log noteOn(ch, pitch, vel) mỗi lần play
- Log peak frame đầu tiên khi render > 0.001
- Error log trong catch pushFrame
- Sửa _playNoteFluid: dùng usedBank để xác định ch khi undefined
2026-07-27 15:02:27 +07:00
3dtours 2c012e3348 fix: AudioWorklet bridge xử lý sai frame → silence
process() gán Float32Array (cả frame 512 samples) vào out[0][i]
thay vì sample riêng lẻ → NaN → silence.
Fix: track fi (frame index) + si (sample index), đọc qL[fi][si].
2026-07-27 14:59:39 +07:00
3dtours a887db587f fix: clean sfload with _allocCStr helper + printErr debug + verbose FluidSynth
- Thay MEMFS path từ /soundfonts/ → root / (tránh lỗi thư mục)
- Helper _allocCStr + _tryLoadSFL: unique filename, manual C string alloc, cleanup
- Thêm printErr callback để capture FluidSynth C stderr log
- Bật synth.verbose=1, synth.ladspa.active=0
- Loại bỏ _fluid_synth_get_sfont_count (không export)
- Log buffer magic bytes (RIFF/sfbk) để verify file valid
2026-07-27 14:56:07 +07:00
3dtours 6fe0cd8d61 fix: bắt lỗi _fluid_synth_sfload + fallback SF2 + tăng WASM memory
- Gọi _fluid_synth_error để lấy error message từ C engine
- Fallback từ .sf3 → .sf2 nếu sfload thất bại
- TOTAL_MEMORY=256MB cho SoundFont 11MB
- unlink file cũ trước writeFile tránh MEMFS conflict
- Log MEMFS stat để debug
2026-07-27 14:51:39 +07:00
3dtours e801023beb fix: locateFile cho FluidSynth WASM module (dùng self-host/CDN)
Module script (type=module) có document.currentScript === null,
nên libfluidsynth không thể tự locate .wasm file. Fix: pass
locateFile() trả về URL .wasm tương ứng với URL .js đã load.

fluidsynthLoader.js: expose __FluidSynthLocateWasm() trả về
FLUIDSYNTH_JS_URL.replace('.js', '.wasm'). Dùng polling
setInterval thay vì resolve callback cho đáng tin cậy hơn.
soundfontPlayer.js: pass locateFile factory khi gọi factory.
2026-07-27 14:48:58 +07:00
3dtours ef1238b87d feat: migrate SpessaSynth → FluidSynth WASM (SF3 native, loop Gen 54 fix)
Replace SpessaSynth JS/AudioWorklet with FluidSynth C++ WASM
(@enikey87/fluidsynth-emscripten@0.1.1). CDN for dev, self-host
for prod. Fixes stuck Tremolo/Saxophone notes via compliant
Gen 44 loop mode processing. 100% audio parity with server
pyfluidsynth render (same C++ core).

- soundfontPlayer.js: FluidSynth WASM engine, preserve SonicSF API
- fluidsynthLoader.js: auto-select CDN (dev) vs self-host (prod)
- fluidsynth-bridge.js: PCM bridge AudioWorklet processor
- index.html: remove SpessaSynth importmap + module, add loader
- vendor/: libfluidsynth-2.3.0-sf3.js + .wasm (1.8MB self-host)
2026-07-27 12:48:30 +07:00
3dtours 18d2503cb9 fix: bypass SpessaSynth AudioWorklet MIDI pipeline for looped voice release
SpessaSynth 4.3.1 bug: looped voices (Tremolo Strings, Trumpet) ignore
release envelope + CC 120 when processed through MIDI message pipeline
(processMessage). Fix: send stopAll directly to worklet via
handleMessage post() bypass, plus noteOn(vel=0) as alternate note-off.
2026-07-27 12:27:51 +07:00
3dtours fa4a38bf3e fix: stopNote/stoplAll uses CC120 (All Sound Off) instead of CC123 (All Notes Off) to kill sustained instrument voices immediately
Root cause: SpessaSynth's CC123 (All Notes Off) calls stopAllNotes(false)
which enters the SoundFont release envelope. Tremolo Strings (GM#44) and
similar sustained instruments have 2-5s release tails, causing notes to
continue playing after MIDI key release.

Fix: Replace CC 123 with CC 120 (All Sound Off) in stopNote() and
stopAll(). CC 120 calls stopAllNotes(true) which sets voice.isActive=false
instantly, bypassing the release envelope entirely.

Removed redundant noteOff(ch,pitch,0) and resetAllControllers(CC121)
which unnecessarily reset volume/pan/expression on the channel.
2026-07-27 12:11:37 +07:00
3dtours 4def86168b fix: send CC64=0 + CC121=0 before noteOff (reset sustain + all controllers) 2026-07-27 12:02:50 +07:00
3dtours f7a46e0baa fix: lazyInit in playNote, oscillator fallback 2s instead of 60s 2026-07-27 11:43:32 +07:00
3dtours 1829b61472 fix: stopNote fires noteOff(3 variants) + allNotesOff + CC123 for redundancy 2026-07-27 11:39:16 +07:00
3dtours 8aeac14e4b fix: only send All Notes Off (CC123) when sustain pedal is not active
- Track sustain state per channel via _sustainStates[]
- sustainActive(channel) returns boolean
- MIDI NoteOn: only send CC123 if !sustainActive(ch)
- Preserves sustained notes when playing new notes
2026-07-27 11:30:35 +07:00
3dtours 48e4047a84 revert: remove 100ms delay from stopNote, keep instant noteOff 2026-07-27 11:27:39 +07:00