From 1cf5b1849227e84e4b557203ea8b36ce91b62208 Mon Sep 17 00:00:00 2001 From: locpham Date: Mon, 10 Aug 2026 10:50:02 +0700 Subject: [PATCH] =?UTF-8?q?FIX:=20s=E1=BB=ADa=20l=E1=BB=97i=20kh=C3=B4ng?= =?UTF-8?q?=20load=20soundfont=20tr=C3=AAn=20window,=20MIDI=20item=20kh?= =?UTF-8?q?=C3=B4ng=20play=20=C4=91=C6=B0=E1=BB=A3c=20qua=20carla?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/vst_engine.py | 96 +++++++++++++++++++------------ app/static/js/app.jsx | 10 ++++ app/static/js/app.precompiled.js | 4 +- app/static/js/services/runtime.js | 10 ++++ wiki.md | 5 ++ 5 files changed, 87 insertions(+), 38 deletions(-) diff --git a/app/core/vst_engine.py b/app/core/vst_engine.py index 16d7884..b56cc25 100644 --- a/app/core/vst_engine.py +++ b/app/core/vst_engine.py @@ -278,8 +278,6 @@ class PluginManager: return load_soundfont_cached(path) def list_soundfont_instruments(self, sf_id: str): - if not ensure_pyfluidsynth(): - return [] if sf_id in _SF_INSTRUMENTS_CACHE: return _SF_INSTRUMENTS_CACHE[sf_id] search_dirs = [] @@ -293,12 +291,18 @@ class PluginManager: for d in (self.extra_vst_dirs or []): if d and os.path.isdir(d) and d not in search_dirs: search_dirs.append(d) - for d in search_dirs: - for f in os.listdir(d): - if not (f.endswith(".sf2") or f.endswith(".sf3")): - continue - base = os.path.splitext(f)[0] - if base == sf_id or base == sf_id.replace("sf_", ""): + presets = [] + # ── 1) FluidSynth (nếu có lib) — đọc bank/program/name từ engine ── + if ensure_pyfluidsynth(): + for d in search_dirs: + if presets: + break + for f in os.listdir(d): + if not (f.endswith(".sf2") or f.endswith(".sf3")): + continue + base = os.path.splitext(f)[0] + if base != sf_id and base != sf_id.replace("sf_", ""): + continue path = os.path.join(d, f) try: import fluidsynth as _fs @@ -308,35 +312,31 @@ class PluginManager: _synth = _fs.new_fluid_synth(_settings) try: fid = _fs.fluid_synth_sfload(_synth, path.encode("utf-8"), 1) - if fid < 0: - continue - sfont = _fs.fluid_synth_get_sfont_by_id(_synth, fid) - presets = [] - if sfont: - for bank in range(0, 2): - for prog_num in range(0, 128): - try: - preset = _fs.fluid_sfont_get_preset(sfont, bank, prog_num) - except Exception: - break - if preset: + if fid >= 0: + sfont = _fs.fluid_synth_get_sfont_by_id(_synth, fid) + if sfont: + for bank in range(0, 2): + for prog_num in range(0, 128): try: - name_ptr = _fs.fluid_preset_get_name(preset) - if name_ptr: - if hasattr(_fs, "ffi"): - raw = _fs.ffi.string(name_ptr) - else: - raw = c_char_p(name_ptr).value - if raw: - presets.append({ - "bank": bank, - "program": prog_num, - "name": raw.decode("utf-8", errors="replace") - }) + preset = _fs.fluid_sfont_get_preset(sfont, bank, prog_num) except Exception: - continue - _SF_INSTRUMENTS_CACHE[sf_id] = presets[:256] - return presets[:256] + break + if preset: + try: + name_ptr = _fs.fluid_preset_get_name(preset) + if name_ptr: + if hasattr(_fs, "ffi"): + raw = _fs.ffi.string(name_ptr) + else: + raw = c_char_p(name_ptr).value + if raw: + presets.append({ + "bank": bank, + "program": prog_num, + "name": raw.decode("utf-8", errors="replace"), + }) + except Exception: + continue finally: try: _fs.delete_fluid_synth(_synth) @@ -344,8 +344,30 @@ class PluginManager: pass except Exception: import traceback; traceback.print_exc() - _SF_INSTRUMENTS_CACHE[sf_id] = [] - return [] + break # đã xử lý file khớp + # ── 2) Fallback: sf2utils đọc TRỰC TIẾP file (thuần Python) ── + # Không cần libfluidsynth — quan trọng trên Windows khi thiếu DLL. + # Đọc preset header (pdta/phdr) → bank/program/name như nhau. + if not presets: + try: + from app.core.soundfont_inspector import SoundFontInspector + insp = SoundFontInspector(system_sf_dir=self.sf_dir, upload_sf_dir=self.upload_sf_dir) + for d in search_dirs: + if presets: + break + for f in os.listdir(d): + if not (f.endswith(".sf2") or f.endswith(".sf3")): + continue + base = os.path.splitext(f)[0] + if base == sf_id or base == sf_id.replace("sf_", ""): + info = insp.inspect_sf2_file(os.path.join(d, f)) + if info and info.get("instruments"): + presets = info["instruments"] + break + except Exception: + presets = [] + _SF_INSTRUMENTS_CACHE[sf_id] = presets[:256] + return presets[:256] def list_available(self) -> dict: return { diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index a86443b..72818bf 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -21133,6 +21133,16 @@ const App = () => { if (window.SonicSF) { window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, scheduledTime, instrumentProgram, destNode, mainCh, synthEngine); } + // MIDI items → Carla (track VSTi + Carla local): phát VSTi realtime + // (schedule theo audio clock bằng setTimeout — preview, timing gần đúng). + if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)) { + const carlaDelay = Math.max(0, (scheduledTime - context.currentTime) * 1000); + const carlaVel = Math.round((note.velocity || 0.8) * 127); + const carlaCh = (synthEngine && synthEngine.midi_channel !== undefined) ? synthEngine.midi_channel : mainCh; + const carlaPitch = note.pitch || 60; + setTimeout(function () { window.SonicCarlaMidi.noteOn(carlaCh, carlaPitch, carlaVel); }, carlaDelay); + setTimeout(function () { window.SonicCarlaMidi.noteOff(carlaCh, carlaPitch); }, carlaDelay + durMs + 30); + } } }); // Play ghost notes from active tracks diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index ce3b32f..01ee109 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -1319,7 +1319,9 @@ const instCtx=resolveTrackInstrumentCtx(track,activeTracksRef.current||[]);const // items placed later in the project play `item.startTime` seconds in the // future (silence when pressing play). Ghost notes are already relative to // the item window, so no absolute-session offset is applied anywhere here. -midiNotes.forEach(note=>{const noteOnBeat=note.start_beat||0;const noteDurBeat=note.duration_beats||1;const noteStartSec=noteOnBeat*secondsPerBeat;const noteDurSec=noteDurBeat*secondsPerBeat;if(noteStartSec+noteDurSec>offsetSeconds){const effectiveStart=Math.max(0,noteStartSec-offsetSeconds);const effectiveDur=noteDurSec-Math.max(0,offsetSeconds-noteStartSec);const scheduledTime=startWallTime+effectiveStart;const durMs=effectiveDur*1000;if(window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durMs,scheduledTime,instrumentProgram,destNode,mainCh,synthEngine);}}});// Play ghost notes from active tracks +midiNotes.forEach(note=>{const noteOnBeat=note.start_beat||0;const noteDurBeat=note.duration_beats||1;const noteStartSec=noteOnBeat*secondsPerBeat;const noteDurSec=noteDurBeat*secondsPerBeat;if(noteStartSec+noteDurSec>offsetSeconds){const effectiveStart=Math.max(0,noteStartSec-offsetSeconds);const effectiveDur=noteDurSec-Math.max(0,offsetSeconds-noteStartSec);const scheduledTime=startWallTime+effectiveStart;const durMs=effectiveDur*1000;if(window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durMs,scheduledTime,instrumentProgram,destNode,mainCh,synthEngine);}// MIDI items → Carla (track VSTi + Carla local): phát VSTi realtime +// (schedule theo audio clock bằng setTimeout — preview, timing gần đúng). +if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)){const carlaDelay=Math.max(0,(scheduledTime-context.currentTime)*1000);const carlaVel=Math.round((note.velocity||0.8)*127);const carlaCh=synthEngine&&synthEngine.midi_channel!==undefined?synthEngine.midi_channel:mainCh;const carlaPitch=note.pitch||60;setTimeout(function(){window.SonicCarlaMidi.noteOn(carlaCh,carlaPitch,carlaVel);},carlaDelay);setTimeout(function(){window.SonicCarlaMidi.noteOff(carlaCh,carlaPitch);},carlaDelay+durMs+30);}}});// Play ghost notes from active tracks var ghostLayers=st.ghostPlayLayers||[];ghostLayers.forEach(function(layer){var ghostTrack=allTracks.find(function(t){return t.id===layer.trackId;});var ghostProg=layer.instrumentProgram!==undefined?layer.instrumentProgram:ghostTrack?ghostTrack.instrumentProgram:undefined;var ghostSynth=layer.synthEngine||(ghostTrack?ghostTrack.synth_engine:undefined);if(ghostProg===undefined&&!ghostSynth)return;var ghostDest=getOrCreateTrackNode(ghostTrack,context);var ghostCh=ghostTrack?assignTrackMidiChannel(ghostTrack,allTracks):0;layer.notes.forEach(function(note){var beat=note.start_beat||0;var dur=note.duration_beats||1;var startSec=beat*secondsPerBeat;var durSec=dur*secondsPerBeat;if(startSec+durSec>offsetSeconds){var effStart=Math.max(0,startSec-offsetSeconds);var effDur=durSec-Math.max(0,offsetSeconds-startSec);var schedTime=startWallTime+effStart;var durMs=effDur*1000;if(window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durMs,schedTime,ghostProg,ghostDest,ghostCh,ghostSynth);}}});});};const handlePlayPause=()=>{console.log('[Play] click activeTab=',activeTab,'isPlaying=',isPlaying,'subPlaying=',subTabs.filter(s=>s.isPlaying).length);if(activeTab!=='main'&&!activeTab.startsWith('session_')){// Sub-tab playback transport const st=subTabs.find(s=>s.id===activeTab);if(!st||!st.buffer&&st.type!=='PIANO_ROLL')return;// MAIN/SECTION đang play (ngầm — MIDI section/main còn kêu sau khi mở // tab): Space phải STOP trước — không được play sub-tab (user: trong diff --git a/app/static/js/services/runtime.js b/app/static/js/services/runtime.js index 2224104..de335ff 100644 --- a/app/static/js/services/runtime.js +++ b/app/static/js/services/runtime.js @@ -75,6 +75,16 @@ window.SonicCarlaMidi = window.SonicCarlaMidi || { return String(se.type || '').indexOf('vst') !== -1 && !!se.plugin_id; } catch (e) { return false; } }, + // Playback MIDI items: route khi track VSTi + Carla local (không cần ARM — + // user đã chủ động bấm Play trên item đó). + shouldRoutePlayback: function (synthEngine) { + try { + var c = window.SonicRuntime && window.SonicRuntime.capabilities; + if (!c || !c.features || !c.features.carla_local) return false; + var se = synthEngine || {}; + return String(se.type || '').indexOf('vst') !== -1 && !!se.plugin_id; + } catch (e) { return false; } + }, noteOn: function (channel, note, velocity) { if (!window.SonicAPI || !window.SonicAPI.carlaMidi) return; window.SonicAPI.carlaMidi({ event: 'note_on', note: note, velocity: velocity || 100, channel: channel || 0 }).catch(function () {}); diff --git a/wiki.md b/wiki.md index 4f0ce2d..8163dbf 100644 --- a/wiki.md +++ b/wiki.md @@ -3051,3 +3051,8 @@ - **Tóm tắt thay đổi:** (1) **MIDI keyboard → Carla realtime**: `POST /api/v1/plugins/carla-midi` (note_on/note_off) gửi OSC UDP tới `/Carla/0/note_on|note_off` (pluginId 0 = plugin auto-load qua .carxs; cổng mặc định 22752 — đã xác minh từ source Carla: CarlaEngineOsc handleMsgNoteOn/NoteOff nhận `iii`/`ii`, CarlaEngineData oscPortUDP=22752, tên client standalone "Carla" từ carla_host.py; override qua env SF_CARLA_OSC_PORT hoặc `osc_port` trong carla_path.json). Frontend: `window.SonicCarlaMidi` (runtime.js) + hook vào **keybed piano roll** (onMouseDown/onMouseEnter) và **playDrawPreview** (vẽ/click note) — chỉ route khi track VSTi + ARM + carla_local. (2) **Soundfont từ thư mục user**: endpoint `/soundfont-instruments/{sf_id}` giờ truyền plugin_dirs → `PluginManager.list_soundfont_instruments` tìm cả thư mục user (bug "nhấn tên SF không thấy instrument"); `/available` truyền extra dirs; `render_engine._find_sf2_path` tìm thêm thư mục user (trước chỉ UPLOAD + /opt/daw_engine/soundfonts + static → soundfont user render câm). - **Các file ảnh hưởng:** `app/api/v1/plugins.py` (+carla-midi, _send_carla_osc, _carla_osc_port), `app/core/vst_engine.py`, `app/core/render_engine.py`, `app/static/js/services/runtime.js` (+SonicCarlaMidi), `app/static/js/services/api.js` (+carlaMidi), `app/static/js/app.jsx` (keybed + playDrawPreview), `app/static/js/app.precompiled.js` (rebuild), `dist/daw_engine/_internal/app/static/**`, `wiki.md` - **Ghi chú/Test (nếu có):** pytest: 86 passed, 7 skipped. OSC: hexdump 32B = `/Carla/0/note_on` + `,iii` + [0,60,100] (big-endian int32) — khớp expected; note_off = `,ii` + [ch,note]. Rebuild bundle BUILD OK. ⚠️ Cần rebuild PyInstaller trên Windows để nhận backend fix (dist backend nằm trong exe). Lưu ý: Carla phải ĐANG MỞ để nhận OSC; nếu đổi cổng OSC trong Carla → đặt SF_CARLA_OSC_PORT. + +### [2026-08-09] Task: MIDI items play qua Carla + list instrument SF không cần libfluidsynth +- **Tóm tắt thay đổi:** (1) **Playback MIDI items → Carla**: `schedulePianoRollMidi` (app.jsx ~21133) giờ route note_on/note_off tới Carla qua `SonicCarlaMidi.shouldRoutePlayback()` (track VSTi + carla_local, KHÔNG cần ARM — user chủ động bấm Play) — schedule bằng setTimeout theo audio clock (preview, timing gần đúng). Keybed/draw preview vẫn yêu cầu ARM (shouldRoute). (2) **List instrument soundfont hoạt động không cần libfluidsynth**: `PluginManager.list_soundfont_instruments` bỏ early-return `if not ensure_pyfluidsynth(): return []`; thêm fallback đọc TRỰC TIẾP file SF2 qua `SoundFontInspector.inspect_sf2_file` (sf2utils — thuần Python, đọc preset header pdta/phdr → bank/program/name) — quan trọng trên Windows khi thiếu fluidsynth DLL. Giữ đường FluidSynth khi có lib. +- **Các file ảnh hưởng:** `app/core/vst_engine.py`, `app/static/js/services/runtime.js` (+shouldRoutePlayback), `app/static/js/app.jsx` (schedulePianoRollMidi), `app/static/js/app.precompiled.js` (rebuild), `dist/daw_engine/_internal/app/static/**`, `wiki.md` +- **Ghi chú/Test (nếu có):** pytest: 86 passed, 7 skipped. Test fallback trên máy KHÔNG có libfluidsynth (đúng kịch bản Windows): file sf2 thật → 137 presets (first: {bank:128, program:48, name:'Orchestra Kit', is_percussion:True}). Rebuild bundle BUILD OK; dist synced. ⚠️ Cần rebuild PyInstaller trên Windows để nhận backend fix (dist backend trong exe).