Commit Graph

196 Commits

Author SHA1 Message Date
3dtours e6439c2d9d fix: MIDI NoteOff stops note immediately (60000ms duration + stopNote)
- NoteOn: durationMs=60000 (1 phút, giữ cho đến khi NoteOff)
- NoteOff: stopNote(ch, pitch) gọi synthInstance.noteOff() ngay
- MIDI channel (msg.data[0] & 0x0F) truyền đúng vào playNote
- CC (sustain/modulation) + pitch bend forward qua SpessaSynth
2026-07-27 11:06:35 +07:00
3dtours 5a49d2012b feat: sustain (CC64), modulation (CC1), pitch bend for MIDI keyboard
- Add controllerChange forwarder for all CC messages (cmd 0xB)
- Add pitchBend method to SonicSF + MIDI handler (cmd 0xE)
- CCs routed to _synthInstance.controllerChange(ch, cc, val)
- Pitch bend routed to _synthInstance.pitchBend(ch, 14bit-val)
2026-07-27 11:02:45 +07:00
3dtours 5c47b68a9d fix: single Synth button opens instrument modal, fix init race condition
- Remove small Synth button (already done earlier)
- Large Synth button now calls openInstrumentSelector (instrument modal)
- Fix _initPromise stale cache: reset to null when init fails
- Module not loaded yet → retries on next select instead of stuck
2026-07-27 10:54:58 +07:00
3dtours cf61b8431d revert: restore small Synth button to dropdown (preserve SF3 flow) 2026-07-27 10:49:42 +07:00
3dtours e5bd442b7c fix: small Synth button opens instrument modal (not SF list dropdown) 2026-07-27 10:40:20 +07:00
3dtours 55a27b5957 fix: remove sf.source references causing ReferenceError in PluginManagerModal 2026-07-27 10:35:42 +07:00
3dtours 3a6b44e195 feat: Synth button opens modal with flat instrument list from all SFs
- openInstrumentSelector builds flat list of all instrument presets
- Modal shows all instruments with search bar
- Each preset shows sf name + program name
- Remove old two-step (SF list -> presets) navigation
- Fix pre-existing closing paren mismatch in PluginManagerModal
2026-07-27 10:11:44 +07:00
3dtours fba3664a1c fix: catalog lookup uses lowercased key for merge
- catalog keys are lowercased (soundfont_inspector: .lower())
- sf.id from _scan_soundfonts has original case (SGM_v2.01)
- Use catalog[sfId.toLowerCase()] || catalog[sfId] to match both
2026-07-27 09:44:28 +07:00
3dtours 2ad29226c4 fix: deduplicate soundfont scan + source label + delete only uploads
- _scan_soundfonts: dict-keyed by base_id to prevent duplicates
- Add 'source' field ('system' | 'upload') to each soundfont entry
- Delete endpoint: only allow deleting upload soundfonts (403 for system)
- Frontend: show (system)/(upload) tag, hide Delete for system fonts
2026-07-27 09:36:58 +07:00
3dtours 7544ee2e5f feat: eager soundfont catalog scan at startup, cache presets locally
- App startup fetches full catalog with all instrument presets
- Merges presets into instrumentSelectorData.soundfonts
- Synth button shows presets from cache (no per-SF API call)
- Fallback: if cache miss, fetch individual SF presets on demand
2026-07-27 09:28:34 +07:00
3dtours d04442ad45 fix: click on ruler moves playhead only (no zero-width selection)
- Remove setSelectionStart/End from simple click (non-drag) path
- Only handleMouseMove (drag) creates/modifies selection
- Sub-tab already correct (no selection set on click)
2026-07-27 09:19:18 +07:00
3dtours dff2c81848 fix: sub-tab ruler drag creates local selection, separate from main
- Add isDraggingSubTabRef/subTabDragStartRef for section-tab drag
- Ctrl+Click on sub-tab ruler clears sub-tab selection
- Sub-tab mousedown: set playhead + start drag
- Sub-tab mousemove/mouseup: update st.selectionStart/End locally
- Section-tab selection doesn't affect main session selection
2026-07-27 09:14:34 +07:00
3dtours ef891e9e04 fix: set selectionFollowsTempo based on which ruler is clicked
- Time Ruler click → setSelectionFollowsTempo(false) → time-based
- Tempo Track Lane click → setSelectionFollowsTempo(true) → tempo-based
- Also handles sub-tab ruler (EDITOR tabs)
- Manual toggle button ♪T/⏱T still works for override
2026-07-27 08:59:50 +07:00
3dtours 06480516eb fix: equal bar widths + selection tempo/time toggle
- WaveformLane bar grid: integer beat loop (bn*beatDuration) instead
  of accumulating t += beatDuration — no floating point drift
- Add selectionFollowsTempo toggle button (♪T/⏱T) in transport bar
- ♪T (tempo): selection recalculates with BPM
- ⏱T (time): selection fixed in seconds regardless of BPM
2026-07-27 08:53:42 +07:00
3dtours af6c77b9ee fix: add bar markers to TimelineRuler aligned with TempoTrackLane
- TimelineRuler draws bar lines at bn * barDuration positions
- Same integer beat loop as TempoTrackLane (bn from firstBarNum)
- Bar number labels at center of ruler
- Time markers still shown below bar markers
- All use same bpm prop → always aligned with tempo track
2026-07-27 08:41:36 +07:00
3dtours 3a84323dd8 fix: force canvas redraw after BPM duration recalculation 2026-07-27 08:36:14 +07:00
3dtours 3c12ce2a22 fix: range loop selection maintains bar count on BPM change
- Add prevBpmRef to track previous BPM
- BPM effect recalculates selectionStart/End using bar count
- startBar = oldStart / oldSecondsPerBar
- newStart = startBar * newSecondsPerBar (same for end)
2026-07-27 08:31:12 +07:00
3dtours f59cc304d4 fix: TempoTrackLane bar spacing drift with floating-point BPM
- Replace t += beatDuration loop (floating accumulation drifts)
- Iterate by integer beat number: t = bn * beatDuration
- Each bar position computed independently, no cumulative error
- Same fix for snapInterval loop
- Fixes unequal bar widths for BPM like 77, 81
2026-07-27 08:25:47 +07:00
3dtours cde29386d2 fix: TempoTrackLane bar spacing by removing broken DOM scrollLeft lookup
- DOM traversal for scrollLeft stops at sticky wrapper (scrollLeft=0)
- Use scrollLeft prop directly instead of traversing parent DOM
- Affects both TimelineRuler and TempoTrackLane canvas effects
- Bars now correctly space when BPM changes
2026-07-27 08:21:16 +07:00
3dtours ea5043c981 fix: auto-compute length_bars for legacy items on BPM change
- Items without length_bars: compute bars from duration/secondsPerBar
- Round to nearest quarter bar for musical accuracy
- Recalculate duration = bars * new secondsPerBar for all items
- Maintains correct bar count for both legacy and new items
2026-07-27 08:10:05 +07:00
3dtours d97f82e93f fix: maintain item duration in bars when BPM changes
- Add length_bars field to midiItems and sections at creation
- useEffect on bpm recalculates duration = length_bars * secondsPerBar
- Fix section creation: use secondsPerBar instead of hardcoded 4s
- Handle existing items without length_bars (no-op)
2026-07-27 08:03:02 +07:00
3dtours f98ade5d58 fix: context menu flips upward when near viewport bottom
- If click.y + 260 > window.innerHeight: position bottom instead of top
- Also clamp left to prevent right-edge overflow
- Menu now appears above the click point when there's no room below
2026-07-27 07:46:06 +07:00
3dtours 993610ca37 fix: AI items at playhead + context menu scrollable within viewport
- Revert AI item startTime to currentTime (playhead position)
- Context menu: maxHeight 60vh + overflow-y-auto, top clamped to
  window.innerHeight - 200 to prevent bottom clipping
- Same fix for both main session and sub-tab context menus
2026-07-27 07:42:01 +07:00
3dtours 1764fc29b5 fix: AI tracks auto-align to end of last existing item
- Replace currentTime with auto-calculated maxEnd from existing items
- Each AI generation starts right after the previous one
- Ensures tracks from different generations align in bar columns
2026-07-26 22:14:02 +07:00
3dtours 0f5f494173 fix: AI-generated tracks save synth_engine + pass to applyAITrackInstrument
- Build synth_engine object with type, plugin_id, soundfont_bank,
  soundfont_program, soundfont_id
- Save to targetTrack.synth_engine so schedulePianoRollMidi reads it
- Pass synthEngine as 3rd arg to applyAITrackInstrument (was missing)
- Remove redundant separate loadSoundFont call (applyAITrackInstrument
  chains to selectInstrument which already calls loadSoundFont)
2026-07-26 21:55:47 +07:00
3dtours 7bbe1f3be0 feat: sticky time ruler + tempo lane when scrolling tracks
- Wrap TimelineRuler + TempoTrackLane in sticky-top div
- Same sticky pattern for both main session and sub-tab views
- TCP column already has sticky header (top-0) and tempo row (top-10)
- Background color hides tracks scrolling behind ruler
2026-07-26 21:32:08 +07:00
3dtours bdaa5594bd feat: sync loop button with sub-tab + auto-derive loop boundaries
- Transport Loop button now syncs isLooping with active sub-tab
- Auto-computes loopEnd from midi notes, section clips, or audio duration
- Sub-tab selectionStart=0, selectionEnd=computed boundary on loop enable
- Main timeline also auto-derives loop end from track items/clips
- All three tab types: MAIN SESSION, SECTION-TAB, PIANO ROLL
2026-07-26 21:03:17 +07:00
3dtours 91e228ad6a fix: sfBank/sfProg ReferenceError — move declarations outside closure 2026-07-26 19:41:49 +07:00
3dtours 09231d9b21 fix: explicit selectInstrument for SpessaSynth bank+program routing
Per md/37_FIXVST3_SUGGEST.md:
- Add SonicSF.selectInstrument(channel, bank, program, sfId) method
- Always sends controllerChange(CC0, bank) + programChange(prog) to SpessaSynth
- _playNoteSpessa also sends CC32 (bank LSB) before programChange
- Remove default bank auto-load from init (cleaner startup)
- Fix syntax error (double }} from previous edit)
- playNote Spessa: use program arg + synthEngine bank/program properly
2026-07-26 19:37:29 +07:00
3dtours e843f55483 fix: strip null synth_engine from project JSON
- Frontend serialize: use undefined instead of null for synth_engine
- Backend validate: strip None synth_engine from tracks before schema validation
- Prevents 'None is not of type object' schema error
2026-07-26 18:22:35 +07:00
3dtours 23601007bf fix: SpessaSynth init retry + default SF3 auto-load
- getAudioContext() calls SonicSF.init on every call (not just first)
- soundfontPlayer.init retries after 1s if SpessaSynth module not loaded yet
- Auto-loads sgm_v2.01 SF3 on successful init
- Fixes race condition: module script loads after regular scripts
2026-07-26 17:59:15 +07:00
3dtours e51b7fd355 feat: SF3 conversion + SpessaSynth client player
Server:
- soundfont_converter.py: Python SF2->SF3 via ffmpeg Ogg compression
- batch_convert_all runs on startup (daemon thread)
- GET /soundfonts/download/{sf_id} serves SF3 with SF2 fallback
- Dockerfile: add fluidsynth, vorbis-tools

Client:
- soundfontStorage.js: IndexedDB cache for SF3 buffers
- soundfontPlayer.js: dual-mode (SpessaSynth + oscillator fallback)
- app.jsx: init SpessaSynth, loadSF on instrument select
- index.html: SpessaSynth CDN import + storage script tag

Compression: DSK 11M->1.1M (90%), SGM 529M->18M (97%)
2026-07-26 17:51:37 +07:00
3dtours bfb636f0bc fix(ui): remove duplicate Synth button, fix MIDI keyboard handler
- Remove small Synth button between ARM and Monitor (duplicate)
- Remove empty try/catch that swallowed all MIDI handler errors
- Ensure MIDI keyboard passes synth_engine context to playNote
- Direct fallback to armed main track when no PIANO_ROLL sub-tab armed
2026-07-26 17:25:38 +07:00
3dtours 443e09b592 feat(midi): play selected instrument on armed main track via MIDI keyboard
When MIDI note-on received and no PIANO_ROLL sub-tab is armed,
fallback to armed track in main session. Read its synth_engine and
instrumentProgram to route audio through correct instrument.
Route to track gainNode if available.
2026-07-26 17:19:51 +07:00
3dtours ef6fe0e718 fix(ui): search instrument names inside SoundFont, not just SF names
Add sfPresetSearchQuery state + search input in instrument selector
modal. Filters sfPresets by name (case-insensitive) when user types.
Clear search on SoundFont switch / modal close.
2026-07-26 17:15:50 +07:00
3dtours c2bfeb7163 feat(ui): add Drum channel toggle button on track panel
- toggleTrackDrum: sets is_percussion + soundfont_bank=128
- 🥁 button in track control panel (between Solo and Record)
- Shows 'D' badge when active, routes to MIDI channel 9
- Instrument preset list shows 🥁 prefix for bank=128 presets
2026-07-26 17:11:42 +07:00
3dtours 280c8f598f fix(engine): case-insensitive SF2 lookup, add synth_engine to serialization
- _find_sf2_path now case-insensitive to match catalog IDs
- serializeTracksList includes synth_engine, soundfont_id, instrument_source
- deserializeTracksList restores synth_engine and soundfont_id
2026-07-26 17:00:28 +07:00
3dtours d48262d468 feat(engine): fix SF2 path resolution, add synth_engine struct and fallback chain
- Fix SF2 path: search upload dir, system dir, static dir (backward compat)
- Add synth_engine struct parsing with flat field fallback
- Add 3-level fallback: selected SF -> default SF -> oscillator synth
- Write synth_engine in setTrackInstrument UI setters
- Pass synth_engine context to client SoundFontPlayer and AI gateway
2026-07-26 16:49:40 +07:00
3dtours 2ab989132b fix(ui): wire Synth button to VST3 selection and render engine
- UI: setTrackInstrumentWithProgram stores instrumentId + enables MIDI type
- UI: SoundFont preset selection now passes soundfont_bank to server
- Server: render_engine reads instrument_id field (fallback instrument)
- Serialize/deserialize: soundfont_bank, soundfont_program preserved
- Rebuilt app.precompiled.js from app.jsx
2026-07-26 16:25:26 +07:00
3dtours 89c7237379 feat: add SoundFont inspection engine + AI instrument schema
- SoundFontInspector (sf2utils) scans .sf2, generates full/condensed catalog
- GET /api/v1/plugins/soundfonts/catalog with lazy init + cache invalidation
- AI tool generate_multitrack_midi now requires soundfont_id/bank/program
- Condensed catalog auto-injected into AI system prompt with bank rules
- Server render: FluidSynth program_select uses bank/program + channel routing (drums→ch9)
- VST3 pedalboard path inserts CC0 bank select + program change before notes
- DecentSamplerManager loads .dspreset with CWD fix for relative sample paths
- Pianobook render branch in render_engine.py
- Client SonicSF: controllerChange, programChange, applyAITrackInstrument
- Post-AI track creation applies instrument via applyAITrackInstrument
- Background cache rescan on .sf2 upload, frontend re-fetches catalog
- libcurl4 + VST3 dirs in Dockerfile
2026-07-26 12:36:48 +07:00
3dtours b1bbf567ab fix(piano-roll): seek-to-start restarts playback
- Add seekPlaybackTo(time) that stops, updates refs, reschedules
  MIDI, and restarts playback for both sub-tab and main timeline
- 'Quay lại đầu' and 'Đầu vùng chọn' buttons use seekPlaybackTo
2026-07-26 12:22:51 +07:00
3dtours b0d7f35a1e fix(piano-roll): loop only on btn + instant stop
- Sub-tab selection loop only active when st.isLooping is true
- End-of-playback loop uses st.isLooping, not global isLoopingSelection
- SonicSF.stopAll() sets gain 0 and stops oscillators immediately
2026-07-26 12:16:01 +07:00
3dtours 4e880850f5 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
2026-07-26 12:03:29 +07:00
3dtours 1ffdec68c6 fix: ruler click 200ms timeout + default instrument attackTime 0.03 + safe ramp 2026-07-26 11:51:50 +07:00
3dtours 523a754949 fix: remove grab handle e.stopPropagation blocking ruler clicks 2026-07-26 11:48:21 +07:00
3dtours 79999065a7 fix: ruler click moves playhead, drag moves range 2026-07-26 11:45:47 +07:00
3dtours e18b261943 fix: ruler range drag/move, default cursor, SonicSF smooth stopAll 2026-07-26 11:42:06 +07:00
3dtours 7601cc72c9 fix: ruler click playhead update using available props onStop+onPlayPause 2026-07-26 11:36:55 +07:00
3dtours 5e096b5594 fix: remove range auto-loop + gain fade-out for reschedule crackling 2026-07-26 11:34:02 +07:00
3dtours d3a3adca8a fix: realtime velocity with updatedNotes + ruler restart without onStop 2026-07-26 11:28:47 +07:00