refactor(bridge): split fx bridge, stabilize VST3 host chain, drop old install engines
- remove diagnostic canaries from audio hot path (crackle fix) and stuck C5 note raw-MIDI test (Vst2Instrument::init noteOff) - split native bridge into daw_vst_bridge (VSTi) + fx_vst_bridge (VST FX) - VST3: processData prepare before setActive, zero input buses, accept both .vstpreset and bridge-state blob in RenderJob presets - native_render/vst_engine: bridge-exe discovery + render_fx_chain + /fx-gui - delete old install/ engine binaries (install via sidecar+resources now) - add BUILD_LAYOUT.md, build_selfcheck, tools/ diagnostics
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
# Build Layout — vị trí chuẩn (ổn định qua build & đổi code)
|
||||
|
||||
## Canonical outputs (đều gitignored, tái tạo từ build)
|
||||
|
||||
| Vị trí | Nội dung | Nguồn |
|
||||
|---|---|---|
|
||||
| `src-tauri/binaries/` | Sidecars + DLL runtime | `build/scripts/build_native_bridge.ps1` |
|
||||
| `src-tauri/binaries/daw_vst_bridge-x86_64-pc-windows-msvc.exe` | Instrument bridge (VSTi + SF2/SF3/SFZ, SHM realtime, control 1-6) | CMake `daw_vst_bridge` |
|
||||
| `src-tauri/binaries/fx_vst_bridge-x86_64-pc-windows-msvc.exe` | FX bridge (VST3 FX: `--render-fx`, `--scan`, `--open-fx-gui`, `--fx-gui`) | CMake `fx_vst_bridge` |
|
||||
| `src-tauri/binaries/libfluidsynth-3.dll`, `jpeg62.dll` | Runtime DLL (fluidsynth, libjpeg-turbo) | vcpkg manifest install |
|
||||
| `src-tauri/resources/daw_engine/` | Python engine đã đóng băng (PyInstaller) | `build/scripts/build_windows.ps1` → `dist/daw_engine` |
|
||||
|
||||
## External (shared, không nằm trong repo)
|
||||
|
||||
| Vị trí | Nội dung |
|
||||
|---|---|
|
||||
| `%USERPROFILE%\vcpkg` | vcpkg.exe + package cache (fluidsynth, libjpeg-turbo, pkgconf) |
|
||||
| `%USERPROFILE%\vcpkg\packages\libjpeg-turbo_x64-windows\bin\jpeg62.dll` | Fallback nguồn jpeg62.dll khi manifest chưa tái tạo |
|
||||
|
||||
## Dev-time resolution (Python)
|
||||
|
||||
`app/core/native_render.py`:
|
||||
- `find_bridge_exe()` — `SF_BRIDGE_PATH` → frozen (`_MEIPASS`/`exe_dir`) → `src-tauri/binaries/<sidecar>` → `install/` (legacy). Instrument bridge.
|
||||
- `find_fx_bridge_exe()` — `SF_FX_BRIDGE_PATH` → cùng chuỗi. FX bridge.
|
||||
|
||||
Tauri sidecar rename: `binaries/daw_vst_bridge` (externalBin) → `daw_vst_bridge-x86_64-pc-windows-msvc.exe` — cả hai dạng đều được tìm.
|
||||
|
||||
## Build
|
||||
|
||||
```powershell
|
||||
# Native bridges (C++): vcpkg manifest + sfizz + CMake → src-tauri/binaries
|
||||
build/scripts/build_native_bridge.ps1
|
||||
|
||||
# Full desktop (engine PyInstaller + bridges + Tauri NSIS/MSI)
|
||||
build/scripts/build_windows.ps1
|
||||
```
|
||||
|
||||
`build/scripts/` là tracked (dù `build/` gitignored) — không xoá `build/` wholesale.
|
||||
|
||||
## Split bridges
|
||||
|
||||
- `daw_vst_bridge` — VSTi instruments, VST2 (.dll) + VST3 (.vst3), native GUI qua SHM control 4 (`OPEN_GUI`), SF2/SF3/SFZ (fluidsynth/sfizz), realtime FX chain in-process (control 6 `SET_FX_CHAIN`).
|
||||
- `fx_vst_bridge` — VST3 FX only. One-shot: `--scan <dir>`, `--render-fx <job> --in --out`, `--open-fx-gui <job>` (cửa sổ rời), `--fx-gui <job>` (embed + HTTP `SF_FXGUI_PORT`). Không SHM, không fluidsynth/sfizz. VST2 FX không hỗ trợ (VST2 không introspect được — xem là instrument; ponytail: thêm AEffect hosting khi có nhu cầu).
|
||||
@@ -701,12 +701,12 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
|
||||
(exit 0). Không spawn 2 lần cho cùng plugin đang mở (tránh trùng GUI).
|
||||
"""
|
||||
enforce_password_changed(current_user)
|
||||
from app.core.native_render import find_bridge_exe
|
||||
exe = find_bridge_exe()
|
||||
from app.core.native_render import find_fx_bridge_exe
|
||||
exe = find_fx_bridge_exe()
|
||||
if not exe:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Không tìm thấy bridge native (daw_vst_bridge.exe). "
|
||||
detail="Không tìm thấy bridge native (fx_vst_bridge.exe). "
|
||||
"Hãy chạy bản desktop đã build đầy đủ.",
|
||||
)
|
||||
plugin_path = (req.path or "").strip()
|
||||
|
||||
+27
-13
@@ -19,33 +19,47 @@ from fastapi import HTTPException
|
||||
from app.config import settings
|
||||
|
||||
BRIDGE_EXE = "daw_vst_bridge.exe" if os.name == "nt" else "daw_vst_bridge"
|
||||
FX_BRIDGE_EXE = "fx_vst_bridge.exe" if os.name == "nt" else "fx_vst_bridge"
|
||||
|
||||
|
||||
def find_bridge_exe() -> str:
|
||||
"""Đường dẫn bridge exe: env SF_BRIDGE_PATH → bundle (frozen) → install/ (dev)."""
|
||||
env = os.getenv("SF_BRIDGE_PATH", "").strip()
|
||||
def find_bridge_exe(exe_name=BRIDGE_EXE, env_key="SF_BRIDGE_PATH") -> str:
|
||||
"""Đường dẫn bridge exe: env <env_key> → bundle (frozen) → src-tauri/binaries → install/ (dev)."""
|
||||
env = os.getenv(env_key, "").strip()
|
||||
if env and os.path.isfile(env):
|
||||
return env
|
||||
# Tauri externalBin renames sidecars → <name>-x86_64-pc-windows-msvc.exe.
|
||||
sidecar = exe_name[:-4] + "-x86_64-pc-windows-msvc.exe" if exe_name.lower().endswith(".exe") else exe_name
|
||||
cands = []
|
||||
if getattr(sys, "frozen", False):
|
||||
root = getattr(sys, "_MEIPASS", "")
|
||||
if root:
|
||||
cands += [os.path.join(root, "binaries", BRIDGE_EXE),
|
||||
os.path.join(root, BRIDGE_EXE)]
|
||||
cands += [os.path.join(root, "binaries", exe_name),
|
||||
os.path.join(root, "binaries", sidecar),
|
||||
os.path.join(root, exe_name),
|
||||
os.path.join(root, sidecar)]
|
||||
# PyInstaller onedir: daw_engine.exe nam <app_root>/daw_engine/, bridge
|
||||
# sidecar nam canh app root (khong trong _MEIPASS) - tim ca exe_dir.
|
||||
exe_dir = os.path.dirname(os.path.abspath(sys.executable))
|
||||
cands += [os.path.join(exe_dir, "binaries", BRIDGE_EXE),
|
||||
os.path.join(exe_dir, BRIDGE_EXE),
|
||||
os.path.join(os.path.dirname(exe_dir), "binaries", BRIDGE_EXE),
|
||||
os.path.join(os.path.dirname(exe_dir), BRIDGE_EXE)]
|
||||
cands.append(os.path.join(settings.BASE_DIR, "install", BRIDGE_EXE))
|
||||
cands += [os.path.join(exe_dir, "binaries", exe_name),
|
||||
os.path.join(exe_dir, "binaries", sidecar),
|
||||
os.path.join(exe_dir, exe_name),
|
||||
os.path.join(exe_dir, sidecar),
|
||||
os.path.join(os.path.dirname(exe_dir), "binaries", exe_name),
|
||||
os.path.join(os.path.dirname(exe_dir), "binaries", sidecar),
|
||||
os.path.join(os.path.dirname(exe_dir), exe_name),
|
||||
os.path.join(os.path.dirname(exe_dir), sidecar)]
|
||||
# Canonical build output (build_native_bridge.ps1) + legacy install/ (dev).
|
||||
cands.append(os.path.join(settings.BASE_DIR, "src-tauri", "binaries", sidecar))
|
||||
cands.append(os.path.join(settings.BASE_DIR, "install", exe_name))
|
||||
for p in cands:
|
||||
if p and os.path.isfile(p):
|
||||
return p
|
||||
return ""
|
||||
|
||||
|
||||
def find_fx_bridge_exe() -> str:
|
||||
"""Đường dẫn FX bridge (fx_vst_bridge.exe): SF_FX_BRIDGE_PATH → bundle → install/."""
|
||||
return find_bridge_exe(FX_BRIDGE_EXE, "SF_FX_BRIDGE_PATH")
|
||||
def normalize_audio_peak(audio, target_db=0.0):
|
||||
"""Scale audio (numpy array, peak) về target_db nếu vượt; giữ nguyên nếu dưới.
|
||||
|
||||
@@ -209,18 +223,18 @@ def render_fx_chain(input_wav, fx_chain, sample_rate, out_path=None) -> str:
|
||||
out_path; lỗi → HTTPException (501 thiếu bridge, 404 plugin lạ, 500 render).
|
||||
"""
|
||||
from app.core.vst_engine import resolve_plugin_path
|
||||
exe = find_bridge_exe()
|
||||
exe = find_fx_bridge_exe()
|
||||
if not exe:
|
||||
raise HTTPException(
|
||||
status_code=501,
|
||||
detail="native_bridge không khả dụng — đặt SF_BRIDGE_PATH hoặc cài daw_vst_bridge",
|
||||
detail="native_bridge không khả dụng — đặt SF_FX_BRIDGE_PATH hoặc cài fx_vst_bridge",
|
||||
)
|
||||
slots = []
|
||||
for fx in fx_chain or []:
|
||||
typ = fx.get("type")
|
||||
if typ == "vst3":
|
||||
p = (fx.get("path") or "").strip()
|
||||
resolved = p if os.path.isfile(p) else resolve_plugin_path(p, ".vst3")
|
||||
resolved = p if os.path.exists(p) else resolve_plugin_path(p, ".vst3")
|
||||
if not resolved:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
|
||||
@@ -481,8 +481,8 @@ class PluginManager:
|
||||
"""
|
||||
if not refresh and self._fx_scan_cache is not None:
|
||||
return self._fx_scan_cache
|
||||
from app.core.native_render import find_bridge_exe
|
||||
exe = find_bridge_exe()
|
||||
from app.core.native_render import find_fx_bridge_exe
|
||||
exe = find_fx_bridge_exe()
|
||||
results = []
|
||||
if exe:
|
||||
dirs = [d for d in [self.vst_dir] + self.extra_vst_dirs
|
||||
@@ -629,7 +629,8 @@ def resolve_plugin_path(name_or_path: str, ext: str = "") -> str:
|
||||
dirs = list(_load_user_plugin_dirs()) or []
|
||||
except Exception:
|
||||
pass
|
||||
dirs += [settings.VST_DIR, settings.SOUNDFONT_DIR]
|
||||
from app.config import settings as _st
|
||||
dirs += [_st.VST_DIR, _st.SOUNDFONT_DIR]
|
||||
for base_dir in dirs:
|
||||
if not os.path.isdir(base_dir):
|
||||
continue
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -79,7 +79,7 @@ cmake -S $NB_DIR -B $BUILD_DIR -G $VS_GEN -A $VS_ARCH `
|
||||
-DCMAKE_BUILD_TYPE=Release `
|
||||
-DVCPKG_TARGET_TRIPLET=x64-windows
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: cmake configure failed" -ForegroundColor Red; exit 1 }
|
||||
cmake --build $BUILD_DIR --config Release --target daw_vst_bridge -- /m
|
||||
cmake --build $BUILD_DIR --config Release --target daw_vst_bridge fx_vst_bridge -- /m
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: cmake build failed" -ForegroundColor Red; exit 1 }
|
||||
|
||||
# 6. Copy sidecar to Tauri externalBin location
|
||||
@@ -89,9 +89,13 @@ New-Item -ItemType Directory -Force $BIN_DIR | Out-Null
|
||||
$EXE = Join-Path $BUILD_DIR "Release\daw_vst_bridge.exe"
|
||||
if (-not (Test-Path $EXE)) { Write-Host "ERROR: $EXE not found" -ForegroundColor Red; exit 1 }
|
||||
Copy-Item $EXE (Join-Path $BIN_DIR "daw_vst_bridge-x86_64-pc-windows-msvc.exe") -Force
|
||||
# fx_vst_bridge: VST FX host sidecar (split — see CMakeLists.txt)
|
||||
$FX_EXE = Join-Path $BUILD_DIR "Release\fx_vst_bridge.exe"
|
||||
if (-not (Test-Path $FX_EXE)) { Write-Host "ERROR: $FX_EXE not found" -ForegroundColor Red; exit 1 }
|
||||
Copy-Item $FX_EXE (Join-Path $BIN_DIR "fx_vst_bridge-x86_64-pc-windows-msvc.exe") -Force
|
||||
|
||||
# 7. Copy fluidsynth runtime DLL next to sidecar (Tauri externalBin bundles only the exe)
|
||||
Write-Host "== [7/7] Copy libfluidsynth-3.dll =="
|
||||
# 7. Copy runtime DLLs next to sidecar (Tauri externalBin bundles only the exe)
|
||||
Write-Host "== [7/7] Copy runtime DLLs (fluidsynth + jpeg) =="
|
||||
$FS_DLL = Join-Path $BUILD_DIR "vcpkg_installed\x64-windows\bin\libfluidsynth-3.dll"
|
||||
if (-not (Test-Path $FS_DLL)) { $FS_DLL = Join-Path $VCPKG_ROOT "installed\x64-windows\bin\libfluidsynth-3.dll" }
|
||||
if (Test-Path $FS_DLL) {
|
||||
@@ -101,4 +105,15 @@ if (Test-Path $FS_DLL) {
|
||||
} else {
|
||||
Write-Host "WARN: libfluidsynth-3.dll not found - bridge needs it at runtime" -ForegroundColor Yellow
|
||||
}
|
||||
# jpeg62.dll (VST3 hosting image decode; vcpkg libjpeg-turbo) - same candidates as fluidsynth
|
||||
$JPEG_DLL = Join-Path $NB_DIR "vcpkg_installed\x64-windows\bin\jpeg62.dll"
|
||||
if (-not (Test-Path $JPEG_DLL)) { $JPEG_DLL = Join-Path $BUILD_DIR "vcpkg_installed\x64-windows\bin\jpeg62.dll" }
|
||||
if (-not (Test-Path $JPEG_DLL)) { $JPEG_DLL = Join-Path $VCPKG_ROOT "packages\libjpeg-turbo_x64-windows\bin\jpeg62.dll" }
|
||||
if (Test-Path $JPEG_DLL) {
|
||||
Copy-Item $JPEG_DLL (Join-Path $BIN_DIR "jpeg62.dll") -Force
|
||||
Copy-Item $JPEG_DLL (Join-Path $BUILD_DIR "Release\jpeg62.dll") -Force
|
||||
Write-Host " jpeg62.dll copied to src-tauri\binaries"
|
||||
} else {
|
||||
Write-Host "WARN: jpeg62.dll not found - VST3 GUIs may fail to show images" -ForegroundColor Yellow
|
||||
}
|
||||
Write-Host "== DONE: sidecar + DLL in src-tauri\binaries =="
|
||||
|
||||
@@ -57,8 +57,8 @@ if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: bridge sidecar missing" -Foregroun
|
||||
npm install -D @tauri-apps/cli
|
||||
# Frontend host: copy template -> src-tauri/ui/index.html (giu Tauri context,
|
||||
# document.write UI engine; neu thieu -> window mat __TAURI__ -> bridge chet).
|
||||
Copy-Item buildscriptsfrontend_host_index.html src-tauriuiindex.html -Force
|
||||
if (-not (Test-Path src-tauriuiindex.html)) { Write-Host "ERROR: frontend host missing" -ForegroundColor Red; exit 1 }
|
||||
Copy-Item build\scripts\index.html src-tauri\ui\index.html -Force
|
||||
if (-not (Test-Path src-tauri\ui\index.html)) { Write-Host "ERROR: frontend host missing" -ForegroundColor Red; exit 1 }
|
||||
npx tauri build
|
||||
|
||||
Write-Host ""
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -98,6 +98,32 @@ else()
|
||||
target_link_libraries(daw_vst_bridge PRIVATE ${FLUIDSYNTH_LIBRARIES} ${SFIZZ_LIBRARIES})
|
||||
endif()
|
||||
|
||||
# ── fx_vst_bridge: VST FX host (split from daw_vst_bridge). Windows-only:
|
||||
# one-shot modes --render-fx / --scan / --open-fx-gui / --fx-gui. No SHM
|
||||
# realtime loop (the live master FX chain stays in daw_vst_bridge because
|
||||
# it processes the instrument mix in-process). ──
|
||||
if(WIN32)
|
||||
add_executable(fx_vst_bridge
|
||||
src/main_fx.cpp
|
||||
src/RenderFxJob.cpp
|
||||
src/FxGuiServer.cpp
|
||||
)
|
||||
if(VST3_SDK_TARGET)
|
||||
target_compile_definitions(fx_vst_bridge PRIVATE HAVE_VST3SDK=1)
|
||||
target_link_libraries(fx_vst_bridge PRIVATE ${VST3_SDK_TARGET} sdk_hosting sdk_common)
|
||||
target_sources(fx_vst_bridge PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/vst/hosting/module_win32.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vst3sdk/public.sdk/source/common/memorystream.cpp
|
||||
)
|
||||
endif()
|
||||
find_package(JPEG REQUIRED) # libjpeg-turbo (vcpkg) — FxGuiServer.cpp JPEG encode
|
||||
# FX modes are VST3 + builtin DSP only — no fluidsynth/sfizz (avoids a
|
||||
# runtime libfluidsynth-3.dll dependency for the FX bridge).
|
||||
target_link_libraries(fx_vst_bridge PRIVATE JPEG::JPEG)
|
||||
# Required Windows libs
|
||||
target_link_libraries(fx_vst_bridge PRIVATE winmm imm32 ws2_32)
|
||||
endif()
|
||||
|
||||
# ── gui_probe: standalone VST3 GUI attach probe (debug tool, not shipped) ──
|
||||
if(WIN32)
|
||||
add_executable(gui_probe
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Auxiliary\Build\vcvars64.bat" >nul 2>&1
|
||||
cd /d C:\Users\locpham\SonicForgeStudio\native_bridge
|
||||
cl /nologo /std:c++17 /EHsc /I include /I vestige tests\shm_selfcheck.cpp /Fe:debug\shm_selfcheck_new.exe
|
||||
if errorlevel 1 exit /b 1
|
||||
debug\shm_selfcheck_new.exe
|
||||
@@ -24,7 +24,6 @@
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#ifdef _WIN32
|
||||
#include <malloc.h> // _heapchk (TEMP diag)
|
||||
#endif
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
@@ -438,9 +437,8 @@ void InstrumentEngineManager::allNotesOff() {
|
||||
// audio buffers; plain std::vector is only 16B-aligned. Over-allocate and
|
||||
// align the pointer handed to plugins to 64B.
|
||||
static float* alignF(std::vector<float>& v, size_t n) {
|
||||
// 64B-aligned pointer can sit up to 63B into the allocation; TEMP diag:
|
||||
// huge tail so we can measure how far DUNE writes past n.
|
||||
const size_t kTail = 2097152 + 64;
|
||||
// 64B-aligned pointer can sit up to 63B into the allocation.
|
||||
const size_t kTail = 64;
|
||||
if (v.size() < n + kTail) v.assign(n + kTail, 0.0f);
|
||||
uintptr_t a = reinterpret_cast<uintptr_t>(v.data());
|
||||
return reinterpret_cast<float*>((a + 63) & ~(uintptr_t)63);
|
||||
@@ -562,17 +560,6 @@ void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t
|
||||
if (channelQuiet(ch)) continue; // editor open / crashed: do not process
|
||||
float* spL = alignF(scratchL_, numSamples);
|
||||
float* spR = alignF(scratchR_, numSamples);
|
||||
std::cerr << "[NativeBridge][diag] renderAll ch=" << ch << " n=" << numSamples
|
||||
<< " spL=0x" << std::hex << (uintptr_t)spL
|
||||
<< " spR=0x" << (uintptr_t)spR
|
||||
<< " capL=" << std::dec << scratchL_.size()
|
||||
<< " capR=" << scratchR_.size() << std::endl;
|
||||
// TEMP diag: fill canary with a NaN pattern so writes of 0.0 (silence)
|
||||
// are still detected.
|
||||
for (uint32_t i = 0; i < numSamples + 2097152; ++i) {
|
||||
spL[i] = 0x7FC00000u; // quiet NaN
|
||||
spR[i] = 0x7FC00000u;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
DWORD sehCode = 0;
|
||||
#else
|
||||
@@ -617,28 +604,6 @@ void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// TEMP diag: plugin output-overrun canary + heap health.
|
||||
{
|
||||
static uint64_t diagBlock = 0; ++diagBlock;
|
||||
int farL = -1, farR = -1;
|
||||
for (uint32_t i = 0; i < 2097152; ++i) {
|
||||
uint32_t bL, bR;
|
||||
std::memcpy(&bL, &spL[numSamples + i], 4);
|
||||
std::memcpy(&bR, &spR[numSamples + i], 4);
|
||||
if (bL != 0x7FC00000u) farL = (int)i;
|
||||
if (bR != 0x7FC00000u) farR = (int)i;
|
||||
}
|
||||
if (farL >= 0 || farR >= 0)
|
||||
std::cerr << "[NativeBridge][diag] block " << diagBlock
|
||||
<< " OUTPUT OVERRUN past n: L=" << farL
|
||||
<< " R=" << farR << " (ch=" << ch << ")" << std::endl;
|
||||
#ifdef _WIN32
|
||||
int h = _heapchk();
|
||||
if (h != _HEAPOK)
|
||||
std::cerr << "[NativeBridge][diag] block " << diagBlock
|
||||
<< " heap NOT OK code=" << h << std::endl;
|
||||
#endif
|
||||
}
|
||||
// Auto-normalize: makeup calibrated (constant per-channel gain, no steps).
|
||||
auto mit = makeup_.find(ch);
|
||||
const float g = (mit != makeup_.end()) ? mit->second : 1.0f;
|
||||
|
||||
@@ -351,11 +351,24 @@ int run_render_job(const std::string& jobPath, const std::string& outPath) {
|
||||
std::cerr << "[RenderJob] preset ignored for non-VST3 type " << (int)type
|
||||
<< " (SF2/SFZ use soundfont_bank/soundfont_program)" << std::endl;
|
||||
#ifdef HAVE_VST3SDK
|
||||
} else if (!applyVst3Preset(mgr.get(0), preset)) {
|
||||
fail("preset import FAILED for " + pluginPath);
|
||||
return rc; // rc stays 1: job/argument error
|
||||
} else {
|
||||
std::cout << "[RenderJob] preset imported (VST3 .vstpreset) type=" << (int)type << std::endl;
|
||||
// Accept BOTH .vstpreset ('VST3' magic) and the bridge state
|
||||
// blob ([4B BE compLen][comp][4B BE ctrlLen][ctrl]) -- realtime
|
||||
// LOAD restores with loadSerializedState, so offline --render
|
||||
// must too (plugins whose default preset is silent -- Ample,
|
||||
// Toontrack, Kong Audio -- otherwise render digital silence).
|
||||
std::vector<uint8_t> head;
|
||||
bool isVstPreset = base64Decode(preset, head) && head.size() >= 4 &&
|
||||
std::memcmp(head.data(), "VST3", 4) == 0;
|
||||
if (!isVstPreset) {
|
||||
mgr.get(0)->loadSerializedState(preset);
|
||||
std::cout << "[RenderJob] preset restored (bridge state blob) type=" << (int)type << std::endl;
|
||||
} else if (!applyVst3Preset(mgr.get(0), preset)) {
|
||||
fail("preset import FAILED for " + pluginPath);
|
||||
return rc; // rc stays 1: job/argument error
|
||||
} else {
|
||||
std::cout << "[RenderJob] preset imported (VST3 .vstpreset) type=" << (int)type << std::endl;
|
||||
}
|
||||
}
|
||||
#else
|
||||
} else {
|
||||
|
||||
@@ -25,8 +25,6 @@ std::unordered_map<const void*, Vst2Instrument*> g_v2Registry;
|
||||
// the callback may be invoked with a null effect (some plugins query the
|
||||
// sample rate during construction).
|
||||
thread_local Vst2Instrument* t_loading = nullptr;
|
||||
// TEMP probe: log every audioMaster call while effEditOpen is running.
|
||||
thread_local bool t_editingProbe = false;
|
||||
// Resize hook registered by the bridge (main.cpp): plugin audioMaster
|
||||
// (SizeWindow) -> move the host window on the UI worker thread.
|
||||
static void (*g_vst2ResizeHook)(void*, int, int) = nullptr;
|
||||
@@ -98,9 +96,6 @@ struct Vst2HostState {
|
||||
intptr_t audioMaster(AEffect* effect, int32_t opcode, int32_t index,
|
||||
intptr_t value, void* ptr, float opt) {
|
||||
(void)index; (void)value; (void)ptr; (void)opt;
|
||||
if (t_editingProbe || true)
|
||||
std::cerr << "[Vst2Instrument][editprobe] audioMaster op=" << opcode
|
||||
<< " idx=" << index << " val=" << value << " ptr=" << ptr << std::endl;
|
||||
Vst2Instrument* self = nullptr;
|
||||
if (effect) {
|
||||
std::lock_guard<std::mutex> lock(g_v2Mutex);
|
||||
@@ -155,7 +150,6 @@ intptr_t audioMaster(AEffect* effect, int32_t opcode, int32_t index,
|
||||
ti.ppqPos = pos * 120.0 / 60.0 / 44100.0;
|
||||
ti.flags = kVstTempoValid | kVstTransportPlaying; // TEMP diag: DUNE silent - try forcing transport playing
|
||||
pos += (self ? self->maxBlockSize() : 512);
|
||||
std::cerr << "[Vst2Instrument][diag] getTime flags=" << ti.flags << " pos=" << pos << std::endl;
|
||||
return (intptr_t)&ti;
|
||||
}
|
||||
case kAudioMasterGetCurrentProcessLevel: return 1; // audio thread
|
||||
@@ -268,9 +262,7 @@ static intptr_t v2_dispatch(AEffect* fx, int32_t op, int32_t idx, intptr_t val,
|
||||
static void* g_sehAddr = nullptr;
|
||||
static CONTEXT g_ctxCopy;
|
||||
__try {
|
||||
if (op == kEffectEditOpen) t_editingProbe = true;
|
||||
intptr_t r = fx->dispatcher(fx, op, idx, val, ptr, opt);
|
||||
if (op == kEffectEditOpen) t_editingProbe = false;
|
||||
return r;
|
||||
} __except (g_sehCode = GetExceptionCode(),
|
||||
g_sehAddr = GetExceptionInformation()->ExceptionRecord->ExceptionAddress,
|
||||
@@ -388,73 +380,11 @@ bool Vst2Instrument::init(double sampleRate, uint32_t maxBlockSize) {
|
||||
maxBlockSize_ = maxBlockSize;
|
||||
AEffect* fx = s->effect;
|
||||
try {
|
||||
// TEMP diag variant B: sampleRate/blockSize BEFORE effOpen, block size
|
||||
// matches the actual render size (256), no StartProcess.
|
||||
fx->dispatcher(fx, kEffectSetSampleRate, 0, 0, nullptr, (float)sampleRate);
|
||||
fx->dispatcher(fx, kEffectSetBlockSize, 0, (intptr_t)maxBlockSize, nullptr, 0);
|
||||
fx->dispatcher(fx, kEffectOpen, 0, 0, nullptr, 0);
|
||||
fx->dispatcher(fx, kEffectSetProgram, 0, 0, nullptr, 0);
|
||||
fx->dispatcher(fx, kEffectMainsChanged, 0, 1, nullptr, 0);
|
||||
// TEMP diag: dump first params to see if the default patch is audible
|
||||
{
|
||||
const int pis[] = {0, 1, 10, 100, 500, 1000, 1160, 1168};
|
||||
for (int pi : pis) {
|
||||
if (pi >= fx->numParams) continue;
|
||||
char nm[128] = {0}, disp[128] = {0}, lbl[128] = {0};
|
||||
fx->dispatcher(fx, kEffectGetParamName, 0, pi, nm, 0);
|
||||
fx->dispatcher(fx, kEffectGetParamDisplay, 0, pi, disp, 0);
|
||||
fx->dispatcher(fx, kEffectGetParamLabel, 0, pi, lbl, 0);
|
||||
std::cerr << "[Vst2Instrument][diag] param " << pi << " name=" << nm << " disp=" << disp << " label=" << lbl << std::endl;
|
||||
}
|
||||
}
|
||||
// TEMP diag: does the plugin itself claim to receive MIDI?
|
||||
{
|
||||
const char* probes[] = {"receiveVstEvents", "receiveVstMidiEvent", "midiProgramNames", "bypass"};
|
||||
for (auto pr : probes) {
|
||||
intptr_t r = fx->dispatcher(fx, kEffectCanDo, 0, 0, (void*)pr, 0);
|
||||
std::cerr << "[Vst2Instrument][diag] plugin canDo " << pr << " -> " << r << std::endl;
|
||||
}
|
||||
}
|
||||
// TEMP diag: send raw MIDI directly (bypass SHM path). CC7 vol max + noteOn
|
||||
// with kVstMidiEventIsRealtime + retriggers.
|
||||
{
|
||||
auto sendMidi = [&](int b0, int b1, int b2) {
|
||||
VstMidiEvent e;
|
||||
std::memset(&e, 0, sizeof(e));
|
||||
e.type = kVstMidiType;
|
||||
e.byteSize = (int32_t)sizeof(e);
|
||||
e.flags = kVstMidiEventIsRealtime;
|
||||
e.midiData[0] = (char)b0; e.midiData[1] = (char)b1; e.midiData[2] = (char)b2;
|
||||
VstEvent* evs[1] = { reinterpret_cast<VstEvent*>(&e) };
|
||||
VstEvents events;
|
||||
std::memset(&events, 0, sizeof(events));
|
||||
events.numEvents = 1;
|
||||
events.events[0] = evs[0];
|
||||
fx->dispatcher(fx, kEffectProcessEvents, 0, 0, &events, 0);
|
||||
};
|
||||
std::cerr << "[Vst2Instrument][diag] raw CC7 127 + noteOn 60/100 x8" << std::endl;
|
||||
sendMidi(0xB0, 7, 127);
|
||||
for (int r = 0; r < 8; ++r) sendMidi(0x90, 60, 100);
|
||||
}
|
||||
// TEMP diag: does DUNE hold a preset chunk? dump its head as hex+ascii
|
||||
{
|
||||
void* chunkPtr = nullptr;
|
||||
intptr_t sz = fx->dispatcher(fx, kEffectGetChunk, 0, 1, &chunkPtr, 0);
|
||||
std::cerr << "[Vst2Instrument][diag] getChunk size=" << sz << " ptr=" << chunkPtr << std::endl;
|
||||
if (sz > 0 && chunkPtr) {
|
||||
const unsigned char* b = (const unsigned char*)chunkPtr;
|
||||
std::string ascii;
|
||||
for (intptr_t i = 0; i < sz && i < 512; ++i) {
|
||||
ascii += (b[i] >= 32 && b[i] < 127) ? (char)b[i] : '.';
|
||||
}
|
||||
std::cerr << "[Vst2Instrument][diag] chunkhead: " << ascii << std::endl;
|
||||
std::cerr << "[Vst2Instrument][diag] chunkbytes: ";
|
||||
for (intptr_t i = 0; i < sz && i < 64; ++i) {
|
||||
char hx[8]; snprintf(hx, sizeof(hx), "%02x ", b[i]); std::cerr << hx;
|
||||
}
|
||||
std::cerr << std::endl;
|
||||
}
|
||||
}
|
||||
} catch (...) {
|
||||
std::cerr << "[Vst2Instrument] init dispatcher EXCEPTION ch=" << channel_ << std::endl;
|
||||
return false;
|
||||
@@ -683,29 +613,25 @@ void Vst2Instrument::loadSerializedState(const std::string& base64) {
|
||||
}
|
||||
|
||||
void Vst2Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) {
|
||||
std::cerr << "[Vst2Instrument] pab START n=" << numSamples << std::endl;
|
||||
auto* s = static_cast<Vst2HostState*>(state_);
|
||||
std::cerr << "[Vst2Instrument] pab state ok=" << (s && s->effect) << std::endl;
|
||||
if (!s || !s->effect) {
|
||||
std::memset(outputL, 0, numSamples * sizeof(float));
|
||||
std::memset(outputR, 0, numSamples * sizeof(float));
|
||||
return;
|
||||
}
|
||||
std::cerr << "[Vst2Instrument] pab before alignF" << std::endl;
|
||||
AEffect* fx = s->effect;
|
||||
// 64B-align inputs: DUNE 3 etc. use aligned SIMD on audio buffers.
|
||||
auto alignF = [](std::vector<float>& v, size_t n) -> float* {
|
||||
// TEMP diag (heap-corrupt hunt): huge tail + input canary to measure
|
||||
// how far DUNE writes past n.
|
||||
const size_t kTail = 2097152 + 64;
|
||||
// 64B-aligned pointer can sit up to 63B into the allocation.
|
||||
const size_t kTail = 64;
|
||||
if (v.size() < n + kTail) v.assign(n + kTail, 0.0f);
|
||||
uintptr_t a = reinterpret_cast<uintptr_t>(v.data());
|
||||
return reinterpret_cast<float*>((a + 63) & ~(uintptr_t)63);
|
||||
};
|
||||
float* inL = alignF(inL_, numSamples);
|
||||
float* inR = alignF(inR_, numSamples);
|
||||
for (uint32_t i = 0; i < numSamples + 2097152; ++i) {
|
||||
inL[i] = 0.0f; // TEMP: clean input (NaN poisons effect plugins)
|
||||
for (uint32_t i = 0; i < numSamples; ++i) {
|
||||
inL[i] = 0.0f;
|
||||
inR[i] = 0.0f;
|
||||
}
|
||||
// Build pointer arrays covering the plugin's full ins/outs: plugins with
|
||||
@@ -731,42 +657,7 @@ void Vst2Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t
|
||||
}
|
||||
if (nIn == 0)
|
||||
inBufs[0] = nullptr; // ins=0: pass NULL input array entry
|
||||
std::cerr << "[Vst2Instrument] processReplacing IN ch=" << channel_ << " n=" << numSamples
|
||||
<< " ins=" << nIn << " outs=" << nOut
|
||||
<< " inL=0x" << std::hex << (uintptr_t)inL << " inR=0x" << (uintptr_t)inR
|
||||
<< " outL=0x" << (uintptr_t)outputL << " outR=0x" << (uintptr_t)outputR
|
||||
<< std::dec << std::endl;
|
||||
fx->processReplacing(fx, inBufs.data(), outBufs.data(), (int32_t)numSamples);
|
||||
std::cerr << "[Vst2Instrument] processReplacing OUT ch=" << channel_ << std::endl;
|
||||
// TEMP diag: does the plugin write any audio?
|
||||
{
|
||||
static int diagBlk = 0;
|
||||
float peakL = 0.f, peakR = 0.f;
|
||||
for (uint32_t i = 0; i < numSamples; ++i) {
|
||||
float a = outputL[i], b = outputR[i];
|
||||
if (a > peakL) peakL = a; else if (-a > peakL) peakL = -a;
|
||||
if (b > peakR) peakR = b; else if (-b > peakR) peakR = -b;
|
||||
}
|
||||
if (peakL != 0.f || peakR != 0.f || diagBlk < 10) {
|
||||
std::cerr << "[Vst2Instrument][diag] PEAK blk=" << diagBlk
|
||||
<< " n=" << numSamples << " L=" << peakL << " R=" << peakR << std::endl;
|
||||
diagBlk++;
|
||||
}
|
||||
}
|
||||
// TEMP diag: how far past n did DUNE write into the INPUT buffers?
|
||||
{
|
||||
int farL = -1, farR = -1;
|
||||
for (uint32_t i = 0; i < 2097152; ++i) {
|
||||
uint32_t bL, bR;
|
||||
std::memcpy(&bL, &inL[numSamples + i], 4);
|
||||
std::memcpy(&bR, &inR[numSamples + i], 4);
|
||||
if (bL != 0x7FC00000u) farL = (int)i;
|
||||
if (bR != 0x7FC00000u) farR = (int)i;
|
||||
}
|
||||
if (farL >= 0 || farR >= 0)
|
||||
std::cerr << "[Vst2Instrument][diag] INPUT OVERRUN past n: L=" << farL
|
||||
<< " R=" << farR << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
const void* Vst2Instrument::effect() const {
|
||||
|
||||
@@ -321,12 +321,6 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
|
||||
std::cerr << "[Vst3Instrument] setupProcessing failed" << std::endl;
|
||||
return false;
|
||||
}
|
||||
if (component->setActive(true) != kResultOk) {
|
||||
std::cerr << "[Vst3Instrument] setActive failed" << std::endl;
|
||||
return false;
|
||||
}
|
||||
processor->setProcessing(true);
|
||||
|
||||
// Build per-bus buffers sized maxBlockSize_ (HostProcessData owns them;
|
||||
// we must NOT override channelBuffers with external pointers — unprepare
|
||||
// would delete[] them).
|
||||
@@ -335,6 +329,13 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (component->setActive(true) != kResultOk) {
|
||||
std::cerr << "[Vst3Instrument] setActive failed" << std::endl;
|
||||
return false;
|
||||
}
|
||||
processor->setProcessing(true);
|
||||
|
||||
// Count output channels of bus 0 (mono plugins duplicate ch0 → both outs).
|
||||
int32 outChannels = 2;
|
||||
if (component->getBusCount(kAudio, kOutput) > 0) {
|
||||
@@ -848,6 +849,14 @@ void Vst3Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t
|
||||
std::memset(ob.channelBuffers32[c], 0, numSamples * sizeof(float));
|
||||
}
|
||||
}
|
||||
if (s->processData.numInputs > 0) {
|
||||
for (int32 b = 0; b < s->processData.numInputs; ++b) {
|
||||
const Steinberg::Vst::AudioBusBuffers& ib = s->processData.inputs[b];
|
||||
for (int32 c = 0; c < ib.numChannels; ++c)
|
||||
if (ib.channelBuffers32[c])
|
||||
std::memset(ib.channelBuffers32[c], 0, numSamples * sizeof(float));
|
||||
}
|
||||
}
|
||||
tresult pr = processor->process(s->processData);
|
||||
float mx = 0.f;
|
||||
if (pr >= 0 && s->processData.numOutputs > 0) {
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
// native_bridge/src/main_fx.cpp
|
||||
// Entry point of fx_vst_bridge.exe — VST FX host bridge (task: split FX out
|
||||
// of daw_vst_bridge). Windows only. One-shot modes only:
|
||||
// --render-fx <job.json> --in <input.wav> --out <output.wav>
|
||||
// --scan <dir>
|
||||
// --open-fx-gui <job.json>
|
||||
// --fx-gui <job.json>
|
||||
// No SHM realtime loop: the realtime master FX chain (control type 6) stays
|
||||
// in daw_vst_bridge because it processes the live instrument mix in-process.
|
||||
#include "RenderFxJob.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <imm.h>
|
||||
#include <dbghelp.h>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#else
|
||||
#include <cstdlib>
|
||||
#endif
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
// Minidump on crash -> %TEMP%\sf_dumps (same convention as daw_vst_bridge).
|
||||
static LONG WINAPI FxCrashDumpHandler(EXCEPTION_POINTERS* ep) {
|
||||
static LONG once = 0;
|
||||
if (InterlockedCompareExchange(&once, 1, 0)) return EXCEPTION_CONTINUE_SEARCH;
|
||||
PEXCEPTION_RECORD er = ep ? ep->ExceptionRecord : nullptr;
|
||||
std::cerr << "[CRASH] fx_vst_bridge exception code=0x" << std::hex
|
||||
<< (er ? er->ExceptionCode : 0) << " addr=0x"
|
||||
<< (er ? (void*)er->ExceptionAddress : nullptr) << std::dec << std::endl;
|
||||
char dumpDir[MAX_PATH];
|
||||
if (GetTempPathA(MAX_PATH, dumpDir)) {
|
||||
std::strncat(dumpDir, "sf_dumps", MAX_PATH - std::strlen(dumpDir) - 1);
|
||||
CreateDirectoryA(dumpDir, nullptr);
|
||||
char dumpPath[MAX_PATH];
|
||||
std::snprintf(dumpPath, sizeof(dumpPath), "%s/fx_bridge_crash_%lu.dmp",
|
||||
dumpDir, GetCurrentProcessId());
|
||||
HMODULE hDbg = LoadLibraryA("dbghelp.dll");
|
||||
if (hDbg) {
|
||||
typedef BOOL(WINAPI* MiniDumpWriteDumpFn)(
|
||||
HANDLE, DWORD, HANDLE, MINIDUMP_TYPE,
|
||||
PMINIDUMP_EXCEPTION_INFORMATION, PMINIDUMP_USER_STREAM_INFORMATION,
|
||||
PMINIDUMP_CALLBACK_INFORMATION);
|
||||
auto pMiniDumpWriteDump =
|
||||
(MiniDumpWriteDumpFn)GetProcAddress(hDbg, "MiniDumpWriteDump");
|
||||
if (pMiniDumpWriteDump) {
|
||||
HANDLE hFile = CreateFileA(dumpPath, GENERIC_WRITE, 0, nullptr,
|
||||
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr);
|
||||
if (hFile != INVALID_HANDLE_VALUE) {
|
||||
MINIDUMP_EXCEPTION_INFORMATION mei = {};
|
||||
mei.ThreadId = GetCurrentThreadId();
|
||||
mei.ExceptionPointers = ep;
|
||||
mei.ClientPointers = TRUE;
|
||||
pMiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(),
|
||||
hFile, MiniDumpNormal, &mei, nullptr,
|
||||
nullptr);
|
||||
CloseHandle(hFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
static LONG WINAPI FxVectoredCrashHandler(PEXCEPTION_POINTERS ep) {
|
||||
PEXCEPTION_RECORD er = ep ? ep->ExceptionRecord : nullptr;
|
||||
DWORD code = er ? er->ExceptionCode : 0;
|
||||
if (code != 0xC0000374 && code != 0xC0000005 && code != 0xC0000409)
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
return FxCrashDumpHandler(ep);
|
||||
}
|
||||
|
||||
// Same daemon init as daw_vst_bridge main(): no console flash, no power
|
||||
// throttling, per-monitor DPI (plugin renderers must not be OS-scaled),
|
||||
// no default IME on the main thread (IME recursion fix). Redirected stdio
|
||||
// (bridge.log pipe / SF_FXGUI_PORT reader) survives FreeConsole.
|
||||
static void win_daemon_init() {
|
||||
if (!std::getenv("SF_KEEP_CONSOLE")) FreeConsole();
|
||||
#ifndef PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION
|
||||
#define PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION 0x00000004
|
||||
#endif
|
||||
#ifndef PROCESS_POWER_THROTTLING_EXECUTION_SPEED
|
||||
#define PROCESS_POWER_THROTTLING_EXECUTION_SPEED 0x00000001
|
||||
#endif
|
||||
PROCESS_POWER_THROTTLING_STATE powerState = {};
|
||||
powerState.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION;
|
||||
powerState.ControlMask = PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION |
|
||||
PROCESS_POWER_THROTTLING_EXECUTION_SPEED;
|
||||
powerState.StateMask = 0;
|
||||
SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling,
|
||||
&powerState, sizeof(powerState));
|
||||
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
ImmAssociateContextEx(nullptr, nullptr, IACE_DEFAULT);
|
||||
}
|
||||
#endif // _WIN32
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
#ifdef _WIN32
|
||||
SetUnhandledExceptionFilter(FxCrashDumpHandler);
|
||||
AddVectoredExceptionHandler(1, FxVectoredCrashHandler);
|
||||
win_daemon_init();
|
||||
#endif
|
||||
// stderr, not stdout: stdout is reserved for machine-readable output in
|
||||
// one-shot modes (--scan JSON, --fx-gui "SF_FXGUI_PORT=...").
|
||||
std::cerr << "[FxBridge] Starting DAW FX Host Bridge..." << std::endl;
|
||||
|
||||
std::string renderFxJob, renderFxIn, renderFxOut;
|
||||
std::string scanDir;
|
||||
std::string openFxGuiJob;
|
||||
std::string fxGuiServerJob;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
if (std::strcmp(argv[i], "--render-fx") == 0 && i + 1 < argc)
|
||||
renderFxJob = argv[i + 1];
|
||||
else if (std::strcmp(argv[i], "--in") == 0 && i + 1 < argc)
|
||||
renderFxIn = argv[i + 1];
|
||||
else if (std::strcmp(argv[i], "--out") == 0 && i + 1 < argc)
|
||||
renderFxOut = argv[i + 1];
|
||||
else if (std::strcmp(argv[i], "--scan") == 0 && i + 1 < argc)
|
||||
scanDir = argv[i + 1];
|
||||
else if (std::strcmp(argv[i], "--open-fx-gui") == 0 && i + 1 < argc)
|
||||
openFxGuiJob = argv[i + 1];
|
||||
else if (std::strcmp(argv[i], "--fx-gui") == 0 && i + 1 < argc)
|
||||
fxGuiServerJob = argv[i + 1];
|
||||
}
|
||||
if (!renderFxJob.empty()) {
|
||||
if (renderFxIn.empty() || renderFxOut.empty()) {
|
||||
std::cerr << "[RenderFxJob] --render-fx requires --in <input.wav>"
|
||||
" --out <output.wav>" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
return run_render_fx_job(renderFxJob, renderFxIn, renderFxOut);
|
||||
}
|
||||
if (!scanDir.empty()) return run_scan_dir(scanDir);
|
||||
if (!openFxGuiJob.empty()) return run_open_fx_gui(openFxGuiJob);
|
||||
if (!fxGuiServerJob.empty()) return run_fx_gui_server(fxGuiServerJob);
|
||||
|
||||
std::cerr << "fx_vst_bridge: no mode. Usage:\n"
|
||||
" --render-fx <job.json> --in <input.wav> --out <output.wav>\n"
|
||||
" --scan <dir>\n"
|
||||
" --open-fx-gui <job.json>\n"
|
||||
" --fx-gui <job.json>" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
Generated
-15
@@ -700,9 +700,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -720,9 +717,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -740,9 +734,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -760,9 +751,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -780,9 +768,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
||||
@@ -697,7 +697,8 @@ pub fn run() {
|
||||
.args(["/IM", "daw_engine.exe", "/T", "/F"])
|
||||
.status();
|
||||
let _ = std::process::Command::new("taskkill")
|
||||
.args(["/IM", "daw_vst_bridge.exe", "/T", "/F"])
|
||||
.args(["/IM", "daw_vst_bridge.exe", "/T", "/F",
|
||||
"/IM", "fx_vst_bridge.exe", "/T", "/F"])
|
||||
.status();
|
||||
window.app_handle().exit(0);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"binaries/libfluidsynth-3.dll": "libfluidsynth-3.dll",
|
||||
"binaries/jpeg62.dll": "jpeg62.dll"
|
||||
},
|
||||
"externalBin": ["binaries/daw_vst_bridge"],
|
||||
"externalBin": ["binaries/daw_vst_bridge", "binaries/fx_vst_bridge"],
|
||||
"windows": {
|
||||
"nsis": {
|
||||
"installerHooks": "hooks.nsh"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,43 @@
|
||||
// tools/check_gui_fix.js — self-check for the TCP GUI button fix (V9 regression).
|
||||
// Run: node tools/check_gui_fix.js (exit 0 = OK)
|
||||
// Verifies: guard exists in source, compiled bundle, and all 6 shipped copies
|
||||
// are byte-identical to the compiled bundle; bundle is syntactically valid.
|
||||
const { readFileSync } = require('fs');
|
||||
const { execFileSync } = require('child_process');
|
||||
const path = require('path');
|
||||
const root = path.join(__dirname, '..');
|
||||
|
||||
const SRC = path.join(root, 'app/static/js/app.jsx');
|
||||
const PRECOMPILED = path.join(root, 'app/static/js/app.precompiled.js');
|
||||
const GUARD_SRC = 'if (!loadOk && !window.__bridgeLoadedChannels[bch]) {';
|
||||
const GUARD_COMPILED = 'if(!loadOk&&!window.__bridgeLoadedChannels[bch]){';
|
||||
const COPIES = [
|
||||
'dist/daw_engine/_internal/app/static/js/app.precompiled.js',
|
||||
'install/daw_engine/_internal/app/static/js/app.precompiled.js',
|
||||
'src-tauri/resources/daw_engine/_internal/app/static/js/app.precompiled.js',
|
||||
'src-tauri/target/debug/daw_engine/_internal/app/static/js/app.precompiled.js',
|
||||
'src-tauri/target/release/daw_engine/_internal/app/static/js/app.precompiled.js'
|
||||
];
|
||||
|
||||
function assert(cond, msg) {
|
||||
if (!cond) { console.error('FAIL: ' + msg); process.exit(1); }
|
||||
}
|
||||
|
||||
const src = readFileSync(SRC, 'utf8');
|
||||
assert(src.includes(GUARD_SRC), 'guard missing in app.jsx');
|
||||
|
||||
const compiled = readFileSync(PRECOMPILED, 'utf8');
|
||||
assert(compiled.includes(GUARD_COMPILED), 'guard missing in app.precompiled.js');
|
||||
assert(compiled.includes('__bridgePluginPaths[instrumentId]=knownPath') || compiled.includes('window.__bridgePluginPaths[instrumentId]=knownPath'), 'path cache write missing in precompiled');
|
||||
assert(src.includes('__bridgeLoadedState'), 'V10 loaded-state guard missing in app.jsx');
|
||||
assert(compiled.includes('__bridgeLoadedState'), 'V10 loaded-state guard missing in precompiled');
|
||||
|
||||
// Bundle must parse (Babel output is plain script).
|
||||
execFileSync(process.execPath, ['--check', PRECOMPILED], { stdio: 'pipe' });
|
||||
|
||||
for (const c of COPIES) {
|
||||
const p = path.join(root, c);
|
||||
assert(readFileSync(p, 'utf8') === compiled, 'copy out of sync: ' + c);
|
||||
}
|
||||
|
||||
console.log('OK: GUI fix guard present in source + compiled + all ' + (COPIES.length + 1) + ' bundles; bundle parses.');
|
||||
@@ -0,0 +1,22 @@
|
||||
import io
|
||||
|
||||
p = 'app/api/v1/plugins.py'
|
||||
s = io.open(p, encoding='utf-8', newline='').read()
|
||||
|
||||
a = '".vst2" if instrument_type.upper() == "VST2" else'
|
||||
b = '(".dll", ".so") if instrument_type.upper() == "VST2" else'
|
||||
assert s.count(a) == 1, s.count(a)
|
||||
s = s.replace(a, b)
|
||||
|
||||
a2 = 'if fn.lower().endswith(ext) and \\'
|
||||
b2 = 'if (fn.lower().endswith(ext) if isinstance(ext, tuple) else fn.lower().endswith(ext)) and \\'
|
||||
assert s.count(a2) == 1, s.count(a2)
|
||||
s = s.replace(a2, b2)
|
||||
|
||||
a3 = 'if os.path.isdir(sp) and sub.lower().endswith(ext) and \\'
|
||||
b3 = 'if os.path.isdir(sp) and (sub.lower().endswith(ext) if isinstance(ext, tuple) else sub.lower().endswith(ext)) and \\'
|
||||
assert s.count(a3) == 1, s.count(a3)
|
||||
s = s.replace(a3, b3)
|
||||
|
||||
io.open(p, 'w', encoding='utf-8', newline='').write(s)
|
||||
print('ok')
|
||||
@@ -0,0 +1,30 @@
|
||||
import io
|
||||
|
||||
p = 'app/core/vst_engine.py'
|
||||
s = io.open(p, encoding='utf-8', newline='').read()
|
||||
|
||||
old = ''' def list_available(self) -> dict:
|
||||
return {
|
||||
"vst_instruments": [
|
||||
{"id": k, "name": k, "type": "VST3", "has_native_support": HAS_PEDALBOARD}
|
||||
for k in self._scan_plugins().keys()
|
||||
],
|
||||
"soundfonts": self._scan_soundfonts()
|
||||
}'''
|
||||
|
||||
new = ''' def list_available(self) -> dict:
|
||||
plugins = self._scan_plugins()
|
||||
return {
|
||||
"vst_instruments": [
|
||||
{"id": k, "name": k,
|
||||
"type": "VST3" if p.lower().endswith(".vst3") else "VST2",
|
||||
"has_native_support": HAS_PEDALBOARD}
|
||||
for k, p in plugins.items()
|
||||
],
|
||||
"soundfonts": self._scan_soundfonts()
|
||||
}'''
|
||||
|
||||
assert old in s, 'not found'
|
||||
s = s.replace(old, new)
|
||||
io.open(p, 'w', encoding='utf-8', newline='').write(s)
|
||||
print('ok')
|
||||
@@ -0,0 +1,105 @@
|
||||
import struct, sys
|
||||
|
||||
path = r"C:/Users/locpham/AppData/Local/Temp/sf_dumps/bridge_crash_1696.dmp"
|
||||
data = open(path, "rb").read()
|
||||
|
||||
sig, ver, numstreams, sdir = struct.unpack_from("<IIIIIIQ", data, 0)[:4]
|
||||
print("streams", numstreams, "sdir", hex(sdir))
|
||||
|
||||
streams = {}
|
||||
for i in range(numstreams):
|
||||
stype, datasize, rva = struct.unpack_from("<III", data, sdir + i * 12)
|
||||
streams[stype] = (rva, datasize)
|
||||
|
||||
ThreadList = 3; ModuleList = 4; MemoryList = 5; Exception = 6; SystemInfo = 7
|
||||
|
||||
# Exception stream: ThreadId(4) align(4) MINIDUMP_EXCEPTION(code,flags,rec,addr,nparams,align,params[15]) ThreadContext
|
||||
if Exception in streams:
|
||||
rva, sz = streams[Exception]
|
||||
tid, _ = struct.unpack_from("<II", data, rva)
|
||||
code, flags, rec, exaddr, nparams, _ = struct.unpack_from("<IIQQII", data, rva + 8)
|
||||
print("EXCEPTION tid", tid, "code", hex(code), "exaddr", hex(exaddr), "nparams", nparams)
|
||||
for i in range(nparams):
|
||||
p = struct.unpack_from("<Q", data, rva + 8 + 40 + i * 8)[0]
|
||||
print(" param[%d]" % i, hex(p))
|
||||
|
||||
# Modules
|
||||
mods = []
|
||||
if ModuleList in streams:
|
||||
rva, sz = streams[ModuleList]
|
||||
n = struct.unpack_from("<I", data, rva)[0]
|
||||
pos = rva + 4
|
||||
for i in range(n):
|
||||
base, imgsz, cksum, ts, name_rva = struct.unpack_from("<QIIII", data, pos)
|
||||
nlen = struct.unpack_from("<I", data, name_rva)[0]
|
||||
name = data[name_rva + 4:name_rva + 4 + nlen].decode("utf-16-le", "replace").rstrip("\x00")
|
||||
mods.append((base, imgsz, name))
|
||||
pos += 108 # MINIDUMP_MODULE
|
||||
for base, szz, name in sorted(mods):
|
||||
print("MOD %016x %8x %s" % (base, szz, name))
|
||||
|
||||
# Thread list: ThreadId, SuspendCount, PriorityClass, Priority, Teb, Stack(8), Context(8)
|
||||
thr = {}
|
||||
if ThreadList in streams:
|
||||
rva, sz = streams[ThreadList]
|
||||
n = struct.unpack_from("<I", data, rva)[0]
|
||||
pos = rva + 4
|
||||
for i in range(n):
|
||||
tid2, susp, pric, pri, teb, st_start, st_sz, st_rva, ctx_sz, ctx_rva = struct.unpack_from("<IIIIQQIIII", data, pos)
|
||||
thr[tid2] = (ctx_rva, ctx_sz, st_start, st_rva)
|
||||
pos += 4 * 4 + 8 + 16 + 8
|
||||
for tid2, (ctx_rva, ctx_sz, st_start, st_rva) in sorted(thr.items()):
|
||||
# AMD64 CONTEXT: ContextFlags(4) ... Rax at offset 0x78? Use known layout:
|
||||
# CONTEXT_AMD64: P1Home..P6Home(48) ContextFlags(4) MxCsr(4) SegCs(2)SegDs(2)SegEs(2)SegFs(2)SegGs(2)EFlags(4)Dr0..Dr3(32)Dr6(8)Dr7(8)Rax(8)Rcx(8)Rdx(8)Rbx(8)Rsp(8)Rbp(8)Rsi(8)Rdi(8)R8..R15(64)Rip(8)...
|
||||
if not ctx_rva:
|
||||
continue
|
||||
cf = struct.unpack_from("<I", data, ctx_rva)[0]
|
||||
# offsets (decimal) from CONTEXT_AMD64
|
||||
o_rax = 0x78; o_rcx = 0x80; o_rdx = 0x88; o_rbx = 0x90; o_rsp = 0x98; o_rbp = 0xA0; o_rsi = 0xA8; o_rdi = 0xB0; o_r8 = 0xB8; o_r9 = 0xC0; o_rip = 0xF8
|
||||
rax = struct.unpack_from("<Q", data, ctx_rva + o_rax)[0]
|
||||
rcx = struct.unpack_from("<Q", data, ctx_rva + o_rcx)[0]
|
||||
rdx = struct.unpack_from("<Q", data, ctx_rva + o_rdx)[0]
|
||||
rbx = struct.unpack_from("<Q", data, ctx_rva + o_rbx)[0]
|
||||
rsp = struct.unpack_from("<Q", data, ctx_rva + o_rsp)[0]
|
||||
rbp = struct.unpack_from("<Q", data, ctx_rva + o_rbp)[0]
|
||||
rsi = struct.unpack_from("<Q", data, ctx_rva + o_rsi)[0]
|
||||
rdi = struct.unpack_from("<Q", data, ctx_rva + o_rdi)[0]
|
||||
r8 = struct.unpack_from("<Q", data, ctx_rva + o_r8)[0]
|
||||
r9 = struct.unpack_from("<Q", data, ctx_rva + o_r9)[0]
|
||||
rip = struct.unpack_from("<Q", data, ctx_rva + o_rip)[0]
|
||||
print("THREAD %d RIP %016x RSP %016x RBP %016x RAX %016x RCX %016x RDX %016x R8 %016x R9 %016x" % (tid2, rip, rsp, rbp, rax, rcx, rdx, r8, r9))
|
||||
|
||||
# Memory list for stack walk
|
||||
mems = []
|
||||
if MemoryList in streams:
|
||||
rva, sz = streams[MemoryList]
|
||||
n = struct.unpack_from("<I", data, rva)[0]
|
||||
pos = rva + 4
|
||||
for i in range(n):
|
||||
start, msz, mrva = struct.unpack_from("<QII", data, pos)
|
||||
mems.append((start, msz, mrva))
|
||||
pos += 16
|
||||
|
||||
def mod_for(addr):
|
||||
for base, szz, name in mods:
|
||||
if base <= addr < base + szz:
|
||||
return base, name
|
||||
return None, None
|
||||
|
||||
if Exception in streams and ThreadList in streams:
|
||||
ex_tid = struct.unpack_from("<I", data, streams[Exception][0])[0]
|
||||
if ex_tid in thr:
|
||||
ctx_rva, ctx_sz, st_start, st_rva = thr[ex_tid]
|
||||
rip = struct.unpack_from("<Q", data, ctx_rva + 0xF8)[0]
|
||||
rsp = struct.unpack_from("<Q", data, ctx_rva + 0x98)[0]
|
||||
b, n = mod_for(rip)
|
||||
print("\nCRASH THREAD %d RIP %016x -> %s+%x" % (ex_tid, rip, n.split("\\")[-1] if n else "?", (rip - b) if b else 0))
|
||||
print("stack descriptor start=%016x rva=%x" % (st_start, st_rva))
|
||||
if st_rva:
|
||||
stack = data[st_rva:st_rva + ctx_sz * 2]
|
||||
print("stack bytes available:", len(stack))
|
||||
for j in range(0, min(len(stack), 16384) - 8, 8):
|
||||
val = struct.unpack_from("<Q", stack, j)[0]
|
||||
b2, n2 = mod_for(val)
|
||||
if n2:
|
||||
print(" +%04x %016x -> %s+%x" % (j, val, n2.split("\\")[-1], val - b2))
|
||||
@@ -0,0 +1,345 @@
|
||||
# Phase 3 UI patch: VST FX chain per-track (FX Rack) + masterbus (Mastering modal).
|
||||
# CRLF-safe: normalize to LF, patch, restore CRLF.
|
||||
import io, sys
|
||||
|
||||
PATH = 'app/static/js/app.jsx'
|
||||
|
||||
def main():
|
||||
with io.open(PATH, encoding='utf-8', newline='') as f:
|
||||
s = f.read()
|
||||
crlf = '\r\n' in s
|
||||
s = s.replace('\r\n', '\n')
|
||||
orig = s
|
||||
|
||||
def sub(old, new):
|
||||
nonlocal s
|
||||
if old not in s:
|
||||
print('NOT FOUND:', old[:90].replace('\n', '\\n'))
|
||||
sys.exit(1)
|
||||
if s.count(old) > 1:
|
||||
print('NOT UNIQUE:', old[:90].replace('\n', '\\n'), 'count=', s.count(old))
|
||||
sys.exit(1)
|
||||
s = s.replace(old, new)
|
||||
|
||||
# 1. serializeTracksList — add vst_fx_chain + vst_fx_bypass
|
||||
sub(
|
||||
" fx_chain: (t.fxChain || []).map(m => typeof m === 'string' ? { type: m, active: true } : { type: m.type, active: m.active !== false }),\n"
|
||||
" server_file_id: t.serverFileId || null,",
|
||||
" fx_chain: (t.fxChain || []).map(m => typeof m === 'string' ? { type: m, active: true } : { type: m.type, active: m.active !== false }),\n"
|
||||
" vst_fx_chain: (t.vstFxChain || []).map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass })),\n"
|
||||
" vst_fx_bypass: !!t.vstFxBypass,\n"
|
||||
" server_file_id: t.serverFileId || null,"
|
||||
)
|
||||
|
||||
# 2. deserializeTracksList — restore vstFxChain + vstFxBypass
|
||||
sub(
|
||||
" fxChain: (t.fx_chain || []).map(m => typeof m === 'string' ? { type: m, active: true } : { type: m.type || m, active: m.active !== false }),\n"
|
||||
" };",
|
||||
" fxChain: (t.fx_chain || []).map(m => typeof m === 'string' ? { type: m, active: true } : { type: m.type || m, active: m.active !== false }),\n"
|
||||
" vstFxChain: (t.vst_fx_chain || []).map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass })),\n"
|
||||
" vstFxBypass: t.vst_fx_bypass !== undefined ? !!t.vst_fx_bypass : false,\n"
|
||||
" };"
|
||||
)
|
||||
|
||||
# 3. serializeProjectToSchema — master key (only when VST slots exist)
|
||||
sub(
|
||||
" tracks: mainTracks\n"
|
||||
" },",
|
||||
" tracks: mainTracks,\n"
|
||||
" ...(masteringSettings && Array.isArray(masteringSettings.vstFxChain) && masteringSettings.vstFxChain.length > 0 ? {\n"
|
||||
" master: {\n"
|
||||
" fx_chain: masteringSettings.vstFxChain.map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass })),\n"
|
||||
" volume_db: 0,\n"
|
||||
" mute: false,\n"
|
||||
" bypass: !!masteringSettings.vstBypass\n"
|
||||
" }\n"
|
||||
" } : {})\n"
|
||||
" },"
|
||||
)
|
||||
|
||||
# 4. deserializeProjectFromSchema — restore master VST chain
|
||||
sub(
|
||||
" return {\n"
|
||||
" bpm: bpmVal,\n"
|
||||
" tracks: restoredTracks,\n"
|
||||
" sessionTabs: restoredSessionTabs,\n"
|
||||
" subTabs: restoredSubTabs,\n"
|
||||
" masteringSettings: _migrateMasteringSettings(schemaObj.mastering_settings),\n"
|
||||
" zoom: schemaObj.metadata && schemaObj.metadata.zoom ? parseFloat(schemaObj.metadata.zoom) : 1.0\n"
|
||||
" };",
|
||||
" const _migratedMastering = _migrateMasteringSettings(schemaObj.mastering_settings) || {};\n"
|
||||
" const _masterBus = (schemaObj.main_session && schemaObj.main_session.master) || {};\n"
|
||||
" const _masterFx = Array.isArray(_masterBus.fx_chain) ? _masterBus.fx_chain : [];\n"
|
||||
" return {\n"
|
||||
" bpm: bpmVal,\n"
|
||||
" tracks: restoredTracks,\n"
|
||||
" sessionTabs: restoredSessionTabs,\n"
|
||||
" subTabs: restoredSubTabs,\n"
|
||||
" masteringSettings: {\n"
|
||||
" ..._migratedMastering,\n"
|
||||
" vstFxChain: _masterFx.map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass })),\n"
|
||||
" vstBypass: !!_masterBus.bypass\n"
|
||||
" },\n"
|
||||
" zoom: schemaObj.metadata && schemaObj.metadata.zoom ? parseFloat(schemaObj.metadata.zoom) : 1.0\n"
|
||||
" };"
|
||||
)
|
||||
|
||||
# 5. masteringSettings initial state
|
||||
sub(
|
||||
" rebalActive: false, rebalMid: 0, rebalSide: 0,\n"
|
||||
" });",
|
||||
" rebalActive: false, rebalMid: 0, rebalSide: 0,\n"
|
||||
" vstFxChain: [],\n"
|
||||
" vstBypass: false,\n"
|
||||
" });"
|
||||
)
|
||||
|
||||
# 6. App scope: offline render preview (preview = âm export)
|
||||
sub(
|
||||
" window.__openFxRack = (trackId, trackName) => setFxRackTarget({ trackId, trackName });",
|
||||
" window.__openFxRack = (trackId, trackName) => setFxRackTarget({ trackId, trackName });\n"
|
||||
" // Render offline toàn project qua native_bridge (VST FX masterbus + track) → WAV.\n"
|
||||
" // Dùng cho nút \"Nghe thử\" trong FX Rack / Mastering — preview = âm export.\n"
|
||||
" window.__renderProjectPreview = async () => {\n"
|
||||
" const schema = serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, tracks, subTabs, sessionTabs, masteringSettings);\n"
|
||||
" try {\n"
|
||||
" const res = await window.SonicAPI.renderProject(schema, 'preview_' + Date.now() + '.wav');\n"
|
||||
" return res;\n"
|
||||
" } catch (e) {\n"
|
||||
" window.showToast && window.showToast('Render preview lỗi: ' + (e.message || e), 'error');\n"
|
||||
" throw e;\n"
|
||||
" }\n"
|
||||
" };"
|
||||
)
|
||||
|
||||
# 7. FXRackModal hooks — before early return
|
||||
sub(
|
||||
" }, [track && track.id]);\n"
|
||||
"\n"
|
||||
" if (!track) return null;",
|
||||
" }, [track && track.id]);\n"
|
||||
"\n"
|
||||
" // VST FX (native_bridge --render-fx) — list VST3 effect plugins + preview\n"
|
||||
" const [fxList, setFxList] = React.useState([]);\n"
|
||||
" React.useEffect(() => {\n"
|
||||
" if (window.SonicAPI && window.SonicAPI.listFx) {\n"
|
||||
" window.SonicAPI.listFx().then(d => setFxList((d && d.plugins) || [])).catch(() => setFxList([]));\n"
|
||||
" }\n"
|
||||
" }, [track && track.id]);\n"
|
||||
" const previewAudioRef = React.useRef(null);\n"
|
||||
" const [vstBypass, setVstBypass] = React.useState(!!(track && track.vstFxBypass));\n"
|
||||
" const [addVstSel, setAddVstSel] = React.useState('');\n"
|
||||
"\n"
|
||||
" if (!track) return null;"
|
||||
)
|
||||
|
||||
# 8. FXRackModal helpers
|
||||
sub(
|
||||
" const chain = track.fxChain || [];\n"
|
||||
" const setChain = (next) => {",
|
||||
" const chain = track.fxChain || [];\n"
|
||||
"\n"
|
||||
" // ── VST FX chain (native_bridge --render-fx, áp dụng khi Export/Offline render) ──\n"
|
||||
" const vstChain = (track.vstFxChain || []);\n"
|
||||
" const setVstChain = (next) => { if (onUpdateTrack) onUpdateTrack(track.id, { vstFxChain: next, vstFxBypass: vstBypass }); };\n"
|
||||
" const toggleVst = (idx) => setVstChain(vstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));\n"
|
||||
" const removeVst = (idx) => setVstChain(vstChain.filter((_, i) => i !== idx));\n"
|
||||
" const moveVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= vstChain.length) return; const next = [...vstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setVstChain(next); };\n"
|
||||
" const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path); if (!fx) return; setVstChain([...vstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]); };\n"
|
||||
" const changeVstPlugin = (idx, path) => { const fx = (fxList || []).find(f => f.path === path); setVstChain(vstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };\n"
|
||||
" const uploadVstPreset = (idx) => {\n"
|
||||
" const inp = document.createElement('input');\n"
|
||||
" inp.type = 'file';\n"
|
||||
" inp.accept = '.vstpreset,.fxp,.fxb,.dspreset';\n"
|
||||
" inp.onchange = () => {\n"
|
||||
" const f = inp.files && inp.files[0];\n"
|
||||
" if (!f) return;\n"
|
||||
" const reader = new FileReader();\n"
|
||||
" reader.onload = () => {\n"
|
||||
" const b64 = String(reader.result || '').split(',')[1] || '';\n"
|
||||
" setVstChain(vstChain.map((s, i) => i === idx ? { ...s, preset_b64: b64 } : s));\n"
|
||||
" window.showToast && window.showToast('Đã gắn preset .vstpreset vào slot ' + (idx + 1), 'success');\n"
|
||||
" };\n"
|
||||
" reader.readAsDataURL(f);\n"
|
||||
" };\n"
|
||||
" inp.click();\n"
|
||||
" };\n"
|
||||
" const previewFx = async () => {\n"
|
||||
" try {\n"
|
||||
" if (previewAudioRef.current) { previewAudioRef.current.pause(); previewAudioRef.current = null; }\n"
|
||||
" if (!window.__renderProjectPreview) { window.showToast && window.showToast('Không có hàm render preview', 'warning'); return; }\n"
|
||||
" window.showToast && window.showToast('Đang render offline (bridge)...', 'info');\n"
|
||||
" const res = await window.__renderProjectPreview();\n"
|
||||
" const au = new Audio((window.API_BASE_URL || '') + res.url);\n"
|
||||
" previewAudioRef.current = au;\n"
|
||||
" au.onended = () => { previewAudioRef.current = null; };\n"
|
||||
" au.play().then(() => window.showToast && window.showToast('Preview offline — âm thật = âm export (VST FX)', 'success')).catch(() => {});\n"
|
||||
" } catch (e) { /* toast ở __renderProjectPreview */ }\n"
|
||||
" };\n"
|
||||
"\n"
|
||||
" const setChain = (next) => {"
|
||||
)
|
||||
|
||||
# 9. FXRackModal JSX — VST FX section between CHAIN strip and WORKSPACE CONTROLS
|
||||
vst_row = (
|
||||
" const vstRow = (s, idx, toggleV, changeV, uploadV, moveV, removeV, fxl, chainLen) => (\n"
|
||||
" <div key={idx} className=\"flex items-center gap-2 bg-slate-900/60 border border-slate-800 rounded-lg px-2 py-1.5 flex-wrap\">\n"
|
||||
" <button onClick={() => toggleV(idx)} className={`w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0 ${!s.bypass ? 'bg-teal-500 text-slate-950' : 'bg-slate-700 text-slate-300'}`} title={s.bypass ? 'Bypass' : 'Active'}>\n"
|
||||
" <i data-lucide=\"power\" className=\"w-2.5 h-2.5\"></i>\n"
|
||||
" </button>\n"
|
||||
" <span className=\"text-[10px] font-bold text-slate-300 font-mono w-6 shrink-0\">{idx + 1}</span>\n"
|
||||
" <select value={s.path || ''} onChange={e => changeV(idx, e.target.value)} className=\"flex-1 min-w-[200px] bg-slate-950 border border-slate-700 text-teal-300 rounded px-2 py-1 text-[11px] outline-none focus:border-teal-500\">\n"
|
||||
" <option value=\"\">— Chọn VST FX —</option>\n"
|
||||
" {(fxl || []).map(f => <option key={f.path} value={f.path}>{f.name || f.id}</option>)}\n"
|
||||
" </select>\n"
|
||||
" <span className={`text-[9px] font-mono ${s.preset_b64 ? 'text-emerald-400' : 'text-slate-600'}`}>{s.preset_b64 ? '✓ preset' : 'no preset'}</span>\n"
|
||||
" <button onClick={() => uploadV(idx)} className=\"px-2 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 text-[10px] font-semibold rounded transition\" title=\"Upload .vstpreset xuất từ Carla (Save preset)\">Preset</button>\n"
|
||||
" <button onClick={() => { if (!s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } window.SonicAPI.openInCarla(s.name || s.path, s.path).then(r => { if (r && r.success) window.showToast && window.showToast('Đã mở Carla với ' + (s.name || s.path) + ' — chỉnh âm trong Carla rồi Save preset', 'success'); else window.showToast && window.showToast('Không mở được Carla', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở Carla: ' + (err.message || err), 'error')); }} className=\"px-2 py-1 bg-teal-800 hover:bg-teal-700 text-teal-100 text-[10px] font-semibold rounded transition\" title=\"Mở native GUI trong Carla\">Carla</button>\n"
|
||||
" <button onClick={() => moveV(idx, -1)} disabled={idx === 0} className=\"w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30\" title=\"Lên\"><i data-lucide=\"chevron-up\" className=\"w-3 h-3\"></i></button>\n"
|
||||
" <button onClick={() => moveV(idx, 1)} disabled={idx === chainLen - 1} className=\"w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30\" title=\"Xuống\"><i data-lucide=\"chevron-down\" className=\"w-3 h-3\"></i></button>\n"
|
||||
" <button onClick={() => removeV(idx)} className=\"text-slate-600 hover:text-red-400 text-xs px-0.5\" title=\"Xóa slot\"><i data-lucide=\"x\" className=\"w-3 h-3\"></i></button>\n"
|
||||
" </div>\n"
|
||||
" );\n"
|
||||
)
|
||||
# Insert helper ABOVE the FXRackModal component so both modals can use it.
|
||||
sub(
|
||||
"const FXRackModal = ({ track, onUpdateTrack, onClose }) => {",
|
||||
vst_row + "\n"
|
||||
"const FXRackModal = ({ track, onUpdateTrack, onClose }) => {"
|
||||
)
|
||||
|
||||
fxrack_vst_section = (
|
||||
" {/* VST FX CHAIN (native_bridge --render-fx — áp dụng khi Export/Offline render) */}\n"
|
||||
" <div className=\"bg-slate-950 border border-teal-900/60 rounded-xl px-3 py-2 flex flex-col gap-2 shrink-0 select-none\">\n"
|
||||
" <div className=\"flex items-center justify-between flex-wrap gap-2\">\n"
|
||||
" <div className=\"flex items-center gap-2 min-w-0\">\n"
|
||||
" <span className=\"text-[10px] font-bold text-teal-400 uppercase tracking-widest font-mono shrink-0\">VST FX:</span>\n"
|
||||
" <span className=\"text-[9px] text-slate-500 font-mono truncate\">Plugin VST3 (Ozone...) — realtime browser vẫn WebAudio builtin; VST áp dụng lúc Export/Offline render (preview = âm export)</span>\n"
|
||||
" </div>\n"
|
||||
" <div className=\"flex items-center gap-2 shrink-0\">\n"
|
||||
" <button onClick={previewFx} className=\"px-2 py-1 bg-teal-800 hover:bg-teal-700 text-teal-100 text-[10px] font-semibold rounded transition flex items-center gap-1\">\n"
|
||||
" <i data-lucide=\"play\" className=\"w-3 h-3\"></i> Nghe thử (render offline)\n"
|
||||
" </button>\n"
|
||||
" <label className=\"flex items-center gap-1 text-[10px] text-slate-400 font-mono cursor-pointer\">\n"
|
||||
" <input type=\"checkbox\" checked={vstBypass} onChange={e => { const val = e.target.checked; setVstBypass(val); if (onUpdateTrack) onUpdateTrack(track.id, { vstFxBypass: val }); }} className=\"w-3 h-3\" /> Bypass\n"
|
||||
" </label>\n"
|
||||
" </div>\n"
|
||||
" </div>\n"
|
||||
" {vstChain.length === 0 && <div className=\"text-[10px] text-slate-500 font-mono\">Chưa có VST FX — chọn plugin bên dưới để thêm vào insert chain (WebAudio chain → VST FX → Fader).</div>}\n"
|
||||
" {vstChain.map((s, idx) => vstRow(s, idx, toggleVst, changeVstPlugin, uploadVstPreset, moveVst, removeVst, fxList, vstChain.length))}\n"
|
||||
" <select value={addVstSel} onChange={e => { setAddVstSel(e.target.value); if (e.target.value) { addVst(e.target.value); setAddVstSel(''); } }} className=\"w-full bg-slate-950 border border-dashed border-slate-700 hover:border-teal-600 text-teal-300 rounded px-2 py-1.5 text-[11px] outline-none\" title=\"Thêm VST FX\">\n"
|
||||
" <option value=\"\">+ Thêm VST FX...</option>\n"
|
||||
" {(fxList || []).map(f => <option key={f.path} value={f.path}>{f.name || f.id}</option>)}\n"
|
||||
" </select>\n"
|
||||
" </div>\n"
|
||||
"\n"
|
||||
" {/* WORKSPACE CONTROLS */}"
|
||||
)
|
||||
sub(
|
||||
" {/* WORKSPACE CONTROLS */}",
|
||||
fxrack_vst_section
|
||||
)
|
||||
|
||||
# 10. MasteringModal hooks
|
||||
sub(
|
||||
" const knobsInitializedRef = React.useRef(false);",
|
||||
" const knobsInitializedRef = React.useRef(false);\n"
|
||||
"\n"
|
||||
" // VST FX masterbus (native_bridge --render-fx) — list VST3 effect plugins + preview\n"
|
||||
" const [masterFxList, setMasterFxList] = React.useState([]);\n"
|
||||
" React.useEffect(() => {\n"
|
||||
" if (window.SonicAPI && window.SonicAPI.listFx) {\n"
|
||||
" window.SonicAPI.listFx().then(d => setMasterFxList((d && d.plugins) || [])).catch(() => setMasterFxList([]));\n"
|
||||
" }\n"
|
||||
" }, []);\n"
|
||||
" const previewAudioRef = React.useRef(null);\n"
|
||||
" const [addMasterVstSel, setAddMasterVstSel] = React.useState('');"
|
||||
)
|
||||
|
||||
# 11. MasteringModal helpers
|
||||
sub(
|
||||
" const audioRef = React.useRef({ source: null });",
|
||||
" // ── VST FX masterbus (native_bridge --render-fx, áp dụng khi Export/Offline render) ──\n"
|
||||
" const masterVstChain = (ozState.vstFxChain || []);\n"
|
||||
" const setMasterVstChain = (next) => setOzState(prev => ({ ...prev, vstFxChain: next }));\n"
|
||||
" const toggleMasterVst = (idx) => setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));\n"
|
||||
" const removeMasterVst = (idx) => setMasterVstChain(masterVstChain.filter((_, i) => i !== idx));\n"
|
||||
" const moveMasterVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= masterVstChain.length) return; const next = [...masterVstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setMasterVstChain(next); };\n"
|
||||
" const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.path === path); if (!fx) return; setMasterVstChain([...masterVstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]); };\n"
|
||||
" const changeMasterVstPlugin = (idx, path) => { const fx = (masterFxList || []).find(f => f.path === path); setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };\n"
|
||||
" const uploadMasterVstPreset = (idx) => {\n"
|
||||
" const inp = document.createElement('input');\n"
|
||||
" inp.type = 'file';\n"
|
||||
" inp.accept = '.vstpreset,.fxp,.fxb,.dspreset';\n"
|
||||
" inp.onchange = () => {\n"
|
||||
" const f = inp.files && inp.files[0];\n"
|
||||
" if (!f) return;\n"
|
||||
" const reader = new FileReader();\n"
|
||||
" reader.onload = () => {\n"
|
||||
" const b64 = String(reader.result || '').split(',')[1] || '';\n"
|
||||
" setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, preset_b64: b64 } : s));\n"
|
||||
" window.showToast && window.showToast('Đã gắn preset .vstpreset vào slot ' + (idx + 1), 'success');\n"
|
||||
" };\n"
|
||||
" reader.readAsDataURL(f);\n"
|
||||
" };\n"
|
||||
" inp.click();\n"
|
||||
" };\n"
|
||||
" const previewMasterFx = async () => {\n"
|
||||
" try {\n"
|
||||
" if (previewAudioRef.current) { previewAudioRef.current.pause(); previewAudioRef.current = null; }\n"
|
||||
" if (!window.__renderProjectPreview) { window.showToast && window.showToast('Không có hàm render preview', 'warning'); return; }\n"
|
||||
" window.showToast && window.showToast('Đang render offline (bridge)...', 'info');\n"
|
||||
" const res = await window.__renderProjectPreview();\n"
|
||||
" const au = new Audio((window.API_BASE_URL || '') + res.url);\n"
|
||||
" previewAudioRef.current = au;\n"
|
||||
" au.onended = () => { previewAudioRef.current = null; };\n"
|
||||
" au.play().then(() => window.showToast && window.showToast('Preview offline — âm thật = âm export (VST FX)', 'success')).catch(() => {});\n"
|
||||
" } catch (e) { /* toast ở __renderProjectPreview */ }\n"
|
||||
" };\n"
|
||||
"\n"
|
||||
" const audioRef = React.useRef({ source: null });"
|
||||
)
|
||||
|
||||
# 12. MasteringModal JSX — VST section after CHAIN strip, before MAIN WORKSPACE
|
||||
master_vst_section = (
|
||||
" {/* VST FX MASTERBUS (native_bridge --render-fx — áp dụng khi Export/Offline render) */}\n"
|
||||
" <div className=\"bg-slate-950 border-b border-teal-900/60 px-4 py-2 flex flex-col gap-2 shrink-0\">\n"
|
||||
" <div className=\"flex items-center justify-between flex-wrap gap-2\">\n"
|
||||
" <div className=\"flex items-center gap-2 min-w-0\">\n"
|
||||
" <span className=\"text-[10px] font-bold text-teal-400 uppercase tracking-widest oz-font-mono shrink-0\">VST FX MASTERBUS:</span>\n"
|
||||
" <span className=\"text-[9px] text-slate-500 oz-font-mono truncate\">Plugin VST3 (Ozone...) — realtime browser vẫn WebAudio builtin; VST áp dụng lúc Export/Offline render (preview = âm export)</span>\n"
|
||||
" </div>\n"
|
||||
" <div className=\"flex items-center gap-2 shrink-0\">\n"
|
||||
" <button onClick={previewMasterFx} className=\"px-2 py-1 bg-teal-800 hover:bg-teal-700 text-teal-100 text-[10px] font-semibold rounded transition flex items-center gap-1\">\n"
|
||||
" <i data-lucide=\"play\" className=\"w-3 h-3\"></i> Nghe thử (render offline)\n"
|
||||
" </button>\n"
|
||||
" <label className=\"flex items-center gap-1 text-[10px] text-slate-400 oz-font-mono cursor-pointer\">\n"
|
||||
" <input type=\"checkbox\" checked={!!ozState.vstBypass} onChange={e => setOzState(prev => ({ ...prev, vstBypass: e.target.checked }))} className=\"w-3 h-3\" /> Bypass\n"
|
||||
" </label>\n"
|
||||
" </div>\n"
|
||||
" </div>\n"
|
||||
" {masterVstChain.length === 0 && <div className=\"text-[10px] text-slate-500 oz-font-mono\">Chưa có VST FX masterbus — chọn plugin bên dưới (Ozone...) để chạy chain sau khi mix WebAudio.</div>}\n"
|
||||
" {masterVstChain.map((s, idx) => vstRow(s, idx, toggleMasterVst, changeMasterVstPlugin, uploadMasterVstPreset, moveMasterVst, removeMasterVst, masterFxList, masterVstChain.length))}\n"
|
||||
" <select value={addMasterVstSel} onChange={e => { setAddMasterVstSel(e.target.value); if (e.target.value) { addMasterVst(e.target.value); setAddMasterVstSel(''); } }} className=\"w-full bg-slate-950 border border-dashed border-slate-700 hover:border-teal-600 text-teal-300 rounded px-2 py-1.5 text-[11px] outline-none\" title=\"Thêm VST FX\">\n"
|
||||
" <option value=\"\">+ Thêm VST FX...</option>\n"
|
||||
" {(masterFxList || []).map(f => <option key={f.path} value={f.path}>{f.name || f.id}</option>)}\n"
|
||||
" </select>\n"
|
||||
" </div>\n"
|
||||
"\n"
|
||||
" {/* MAIN WORKSPACE */}"
|
||||
)
|
||||
sub(
|
||||
" {/* MAIN WORKSPACE */}",
|
||||
master_vst_section
|
||||
)
|
||||
|
||||
if s == orig:
|
||||
print('NO CHANGE — nothing patched')
|
||||
sys.exit(1)
|
||||
|
||||
with io.open(PATH, 'w', encoding='utf-8', newline='') as f:
|
||||
f.write(s if not crlf else s.replace('\n', '\r\n'))
|
||||
print('OK patched app.jsx')
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,80 @@
|
||||
import io, sys
|
||||
|
||||
p = r"C:/Users/locpham/SonicForgeStudio/native_bridge/src/main.cpp"
|
||||
data = open(p, "rb").read()
|
||||
|
||||
def rep(old, new, count=1):
|
||||
global data
|
||||
old = old.encode()
|
||||
new = new.encode()
|
||||
assert data.count(old) >= count, "pattern not found: %r" % old
|
||||
data = data.replace(old, new, count)
|
||||
|
||||
# 1. includes: cstdio + dbghelp.h into the _WIN32 block
|
||||
rep("#include <cstdlib>\n#include <thread>\n",
|
||||
"#include <cstdlib>\n#include <cstdio>\n#include <dbghelp.h>\n#include <thread>\n")
|
||||
|
||||
# 2. handler + registration at main entry
|
||||
handler = r"""
|
||||
// --- crash minidump handler (temp diagnostics) -------------------------------
|
||||
// WER LocalDumps produces NO dump for 0xC0000374 (heap corruption) — write our
|
||||
// own minidump via dbghelp.dll when an unhandled exception reaches this filter.
|
||||
#ifdef _WIN32
|
||||
static LONG WINAPI CrashDumpHandler(EXCEPTION_POINTERS* ep) {
|
||||
static LONG once = 0;
|
||||
if (InterlockedCompareExchange(&once, 1, 0)) return EXCEPTION_CONTINUE_SEARCH;
|
||||
PEXCEPTION_RECORD er = ep ? ep->ExceptionRecord : nullptr;
|
||||
std::cerr << "[CRASH] exception code=0x" << std::hex
|
||||
<< (er ? er->ExceptionCode : 0) << " addr=0x"
|
||||
<< (er ? (void*)er->ExceptionAddress : nullptr) << std::dec << std::endl;
|
||||
if (er) {
|
||||
HMODULE hMod = nullptr;
|
||||
char modPath[MAX_PATH] = {0};
|
||||
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
(LPCSTR)er->ExceptionAddress, &hMod) && hMod) {
|
||||
GetModuleFileNameA(hMod, modPath, MAX_PATH);
|
||||
std::cerr << "[CRASH] faulting module: " << modPath << std::endl;
|
||||
}
|
||||
}
|
||||
const char* dumpDir = "C:/Users/locpham/AppData/Local/Temp/sf_dumps";
|
||||
CreateDirectoryA(dumpDir, nullptr);
|
||||
char dumpPath[MAX_PATH];
|
||||
snprintf(dumpPath, sizeof(dumpPath), "%s/bridge_crash_%lu.dmp", dumpDir, GetCurrentProcessId());
|
||||
HMODULE hDbg = LoadLibraryA("dbghelp.dll");
|
||||
if (hDbg) {
|
||||
typedef BOOL(WINAPI* MiniDumpWriteDumpFn)(HANDLE, DWORD, HANDLE, MINIDUMP_TYPE,
|
||||
PMINIDUMP_EXCEPTION_INFORMATION, PMINIDUMP_USER_STREAM_INFORMATION,
|
||||
PMINIDUMP_CALLBACK_INFORMATION);
|
||||
auto pMiniDumpWriteDump = (MiniDumpWriteDumpFn)GetProcAddress(hDbg, "MiniDumpWriteDump");
|
||||
if (pMiniDumpWriteDump) {
|
||||
HANDLE hFile = CreateFileA(dumpPath, GENERIC_WRITE, 0, nullptr,
|
||||
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (hFile != INVALID_HANDLE_VALUE) {
|
||||
MINIDUMP_EXCEPTION_INFORMATION mei = {};
|
||||
mei.ThreadId = GetCurrentThreadId();
|
||||
mei.ExceptionPointers = ep;
|
||||
mei.ClientPointers = TRUE;
|
||||
BOOL ok = pMiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(),
|
||||
hFile, MiniDumpNormal, &mei, nullptr, nullptr);
|
||||
CloseHandle(hFile);
|
||||
std::cerr << "[CRASH] minidump " << (ok ? "written: " : "FAILED: ") << dumpPath << std::endl;
|
||||
} else {
|
||||
std::cerr << "[CRASH] cannot open dump file" << std::endl;
|
||||
}
|
||||
} else {
|
||||
std::cerr << "[CRASH] MiniDumpWriteDump not found in dbghelp.dll" << std::endl;
|
||||
}
|
||||
} else {
|
||||
std::cerr << "[CRASH] cannot load dbghelp.dll" << std::endl;
|
||||
}
|
||||
return EXCEPTION_CONTINUE_SEARCH; // let WER proceed, process dies normally
|
||||
}
|
||||
#endif
|
||||
"""
|
||||
|
||||
rep("int main(int argc, char* argv[]) {\n",
|
||||
handler + "\nint main(int argc, char* argv[]) {\n SetUnhandledExceptionFilter(CrashDumpHandler);\n")
|
||||
|
||||
open(p, "wb").write(data)
|
||||
print("patched OK, size=", len(data))
|
||||
@@ -0,0 +1,327 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Patch app/static/js/app.jsx: thay render per-item (scheduleNativeSfItem /
|
||||
scheduleNativeVstItem) bằng scheduleNativeTrack (render cả track 1 request)
|
||||
+ epoch guard + decode queue + prerenderNativeTracks. Debug tool — không commit."""
|
||||
import io, sys
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
PATH = "app/static/js/app.jsx"
|
||||
with open(PATH, "rb") as f:
|
||||
raw = f.read()
|
||||
src = raw.decode("utf-8").replace("\r\n", "\n")
|
||||
orig = src
|
||||
|
||||
|
||||
def save():
|
||||
with open(PATH, "wb") as f:
|
||||
f.write(src.replace("\n", "\r\n").encode("utf-8"))
|
||||
|
||||
|
||||
# ── Patch 1: thay scheduleNativeSfItem (per-item) bằng họ scheduleNativeTrack ──
|
||||
if "_nativePlayEpoch" not in src:
|
||||
OLD_START = "// \u2500\u2500 Native soundfont item render (standalone, transport) \u2500"
|
||||
OLD_END = " } catch (e) { console.warn('[NativeSF] scheduleNativeSfItem error:', e); }\n};"
|
||||
i0 = src.index(OLD_START)
|
||||
i1 = src.index(OLD_END) + len(OLD_END)
|
||||
assert src[i1:i1+4] == "\n\n//", repr(src[i1:i1+40])
|
||||
NEW = """// \u2500\u2500 Native track render (standalone, transport) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
||||
// Render TO\u00c0N B\u1ed8 MIDI items c\u1ee7a track b\u1eb1ng 1 request (native FluidSynth cho
|
||||
// soundfont, native_bridge --render cho VSTi) \u2192 decode \u2192 1 BufferSource duy
|
||||
// nh\u1ea5t cho c\u1ea3 track (nh\u01b0 audio clip). Thay th\u1ebf render per-item: RENDER_LOCK
|
||||
// server serialize t\u1eebng item \u2192 project nhi\u1ec1u item = delay v\u00e0i ch\u1ee5c gi\u00e2y.
|
||||
// opts:
|
||||
// baseOffsetSec: offset th\u00eam (section: secStart) | limitSec: ch\u1eb7n t\u1ea1i secEnd
|
||||
// isActive(): guard stop gi\u1eefa ch\u1eebng | sources: m\u1ea3ng ngu\u1ed3n \u0111\u1ec3 stop
|
||||
// playheadNow(): playhead TH\u1eacT t\u1ea1i th\u1eddi \u0111i\u1ec3m resolve \u2014 render tr\u1ec5 v\u00e0i ch\u1ee5c
|
||||
// gi\u00e2y \u2192 item v\u1eabn schedule \u0111\u00fang v\u1ecb tr\u00ed playhead hi\u1ec7n t\u1ea1i (kh\u00f4ng ch\u1ed3ng
|
||||
// \u00e2m/wall-of-sound nh\u01b0 d\u00f9ng offsetTime c\u0169).
|
||||
// notesOverride: d\u00f9ng thay track.midiItems (piano roll tab).
|
||||
// Epoch guard: stop/rebuild t\u0103ng _nativePlayEpoch \u2192 m\u1ecdi render pending c\u1ee7a
|
||||
// l\u1ea7n play c\u0169 resolve nh\u01b0ng b\u1ecb skip (kh\u00f4ng duplicate schedule).
|
||||
let _nativePlayEpoch = 0;
|
||||
const _nativeTrackUrls = {};
|
||||
const _nativeTrackPending = {};
|
||||
const _buildNativeTrackNotes = (track, bpm, baseOffsetSec, notesOverride) => {
|
||||
const secPerBeat = 60.0 / (parseInt(bpm) || 120);
|
||||
const items = notesOverride ? [{ startTime: 0, notes: notesOverride }] : ((track && track.midiItems) || []);
|
||||
let trackStartAbs = Infinity;
|
||||
const absNotes = [];
|
||||
items.forEach(function (item) {
|
||||
const itemStartAbs = (baseOffsetSec || 0) + (item.startTime || 0);
|
||||
if (itemStartAbs < trackStartAbs) trackStartAbs = itemStartAbs;
|
||||
(item.notes || []).forEach(function (n) {
|
||||
absNotes.push({
|
||||
pitch: n.pitch || 60,
|
||||
start_beat: itemStartAbs / secPerBeat + (n.start_beat || 0),
|
||||
duration_beats: n.duration_beats || 1,
|
||||
velocity: n.velocity != null ? n.velocity : 0.8,
|
||||
});
|
||||
});
|
||||
});
|
||||
return { absNotes: absNotes, trackStartAbs: trackStartAbs };
|
||||
};
|
||||
const _nativeTrackKey = (track, bpm, baseOffsetSec, notesOverride) => {
|
||||
try {
|
||||
const eng = track && track.synth_engine;
|
||||
const instrumentId = (eng && eng.plugin_id) || (track && track.instrumentId) || '';
|
||||
const sfId = (eng && eng.soundfont_id) || '';
|
||||
const data = _buildNativeTrackNotes(track, bpm, baseOffsetSec, notesOverride);
|
||||
const nk = data.absNotes.map(function (n) {
|
||||
return n.pitch + ':' + n.start_beat + ':' + n.duration_beats + ':' + n.velocity;
|
||||
}).join(',');
|
||||
return [track && track.id ? track.id : '', instrumentId, sfId, (eng && eng.preset_id) || '', (eng && eng.preset_path) || '', parseFloat(bpm) || 120, baseOffsetSec || 0, nk].join('|');
|
||||
} catch (e) { return ''; }
|
||||
};
|
||||
const _renderNativeTrack = (track, bpm, baseOffsetSec, notesOverride) => {
|
||||
try {
|
||||
const eng = track && track.synth_engine;
|
||||
const data = _buildNativeTrackNotes(track, bpm, baseOffsetSec, notesOverride);
|
||||
if (!data.absNotes.length) return Promise.resolve(null);
|
||||
if (isSfTrackEngine(eng)) {
|
||||
const sfId = eng && eng.soundfont_id;
|
||||
if (!sfId) return Promise.resolve(null);
|
||||
return window.SonicAPI.soundfontRender({
|
||||
soundfont_id: sfId,
|
||||
bank: (eng && eng.soundfont_bank) || 0,
|
||||
program: (eng && eng.soundfont_program) || 0,
|
||||
bpm: parseFloat(bpm) || 120,
|
||||
notes: data.absNotes,
|
||||
}).then(function (res) {
|
||||
if (!res || !res.success || !res.url) return null;
|
||||
return API_BASE_URL + res.url;
|
||||
}).catch(function () { return null; });
|
||||
}
|
||||
const instrumentId = (eng && eng.plugin_id) || (track && track.instrumentId);
|
||||
if (!instrumentId || !isVstTrackEngine(eng)) return Promise.resolve(null);
|
||||
return window.SonicAPI.midiRender({
|
||||
instrument_id: instrumentId,
|
||||
notes: data.absNotes,
|
||||
bpm: parseFloat(bpm) || 120,
|
||||
preset_id: (eng && eng.preset_id) || undefined,
|
||||
preset_path: (eng && eng.preset_path) || undefined,
|
||||
preset_data: (eng && eng.preset_data) || undefined,
|
||||
}).then(function (res) {
|
||||
if (!res || !res.success || !res.url) return null;
|
||||
return API_BASE_URL + res.url;
|
||||
}).catch(function () { return null; });
|
||||
} catch (e) { return Promise.resolve(null); }
|
||||
};
|
||||
// Dedupe in-flight render + cache WAV: play l\u1eb7p/re-schedule trong l\u00fac render
|
||||
// \u0111ang ch\u1ea1y \u2192 ch\u1edd c\u00f9ng promise, kh\u00f4ng b\u1eafn duplicate request (tr\u00e1nh queue sau
|
||||
// RENDER_LOCK + render 2 l\u1ea7n c\u00f9ng n\u1ed9i dung).
|
||||
const ensureRenderNativeTrack = (track, bpm, baseOffsetSec, notesOverride) => {
|
||||
const key = _nativeTrackKey(track, bpm, baseOffsetSec, notesOverride);
|
||||
if (!key) return _renderNativeTrack(track, bpm, baseOffsetSec, notesOverride);
|
||||
if (_nativeTrackUrls[key]) return Promise.resolve(_nativeTrackUrls[key]);
|
||||
if (_nativeTrackPending[key]) return _nativeTrackPending[key];
|
||||
const p = _renderNativeTrack(track, bpm, baseOffsetSec, notesOverride).then(function (url) {
|
||||
if (url) _nativeTrackUrls[key] = url;
|
||||
return url;
|
||||
}).then(function (url) {
|
||||
delete _nativeTrackPending[key];
|
||||
return url;
|
||||
});
|
||||
_nativeTrackPending[key] = p;
|
||||
return p;
|
||||
};
|
||||
// Gi\u1edbi h\u1ea1n decodeAudioData song song (max 3) \u2014 nhi\u1ec1u WAV fetch v\u1ec1 c\u00f9ng l\u00fac \u2192
|
||||
// main thread ngh\u1ebdn \u2192 UI \u0111\u01a1, Stop kh\u00f4ng ph\u1ea3n h\u1ed3i.
|
||||
let _nativeDecodeActive = 0;
|
||||
const _nativeDecodeQueue = [];
|
||||
const _pumpNativeDecodes = () => {
|
||||
while (_nativeDecodeActive < 3 && _nativeDecodeQueue.length) {
|
||||
const job = _nativeDecodeQueue.shift();
|
||||
_nativeDecodeActive++;
|
||||
try {
|
||||
job.ctx.decodeAudioData(job.buf, function (ab) {
|
||||
_nativeDecodeActive--;
|
||||
job.cb(ab);
|
||||
_pumpNativeDecodes();
|
||||
}, function () {
|
||||
_nativeDecodeActive--;
|
||||
job.cb(null);
|
||||
_pumpNativeDecodes();
|
||||
});
|
||||
} catch (e) {
|
||||
_nativeDecodeActive--;
|
||||
job.cb(null);
|
||||
_pumpNativeDecodes();
|
||||
}
|
||||
}
|
||||
};
|
||||
const _decodeNativeQueue = (ctx, buf, cb) => {
|
||||
_nativeDecodeQueue.push({ ctx: ctx, buf: buf, cb: cb });
|
||||
_pumpNativeDecodes();
|
||||
};
|
||||
const scheduleNativeTrack = (track, offsetTime, context, destNode, bpm, opts) => {
|
||||
try {
|
||||
const eng = track && track.synth_engine;
|
||||
if (!isSfTrackEngine(eng) && !isVstTrackEngine(eng)) return;
|
||||
const baseOffsetSec = (opts && opts.baseOffsetSec) || 0;
|
||||
const notesOverride = (opts && opts.notesOverride) || null;
|
||||
const data = _buildNativeTrackNotes(track, bpm, baseOffsetSec, notesOverride);
|
||||
if (!data.absNotes.length) return;
|
||||
const key = _nativeTrackKey(track, bpm, baseOffsetSec, notesOverride);
|
||||
const epoch = _nativePlayEpoch;
|
||||
const scheduleUrl = function (url) {
|
||||
if (!url) return;
|
||||
fetch(url).then(function (r) { return r.arrayBuffer(); }).then(function (buf) {
|
||||
_decodeNativeQueue(context, buf, function (audioBuf) {
|
||||
try {
|
||||
if (!audioBuf) return;
|
||||
if (epoch !== _nativePlayEpoch) return; // stale play (stop/rebuild)
|
||||
if (opts && typeof opts.isActive === 'function' && !opts.isActive()) return;
|
||||
const ph = (opts && typeof opts.playheadNow === 'function') ? opts.playheadNow() : offsetTime;
|
||||
const src = context.createBufferSource();
|
||||
src.buffer = audioBuf;
|
||||
src.connect(destNode);
|
||||
const trackEndAbs = data.trackStartAbs + audioBuf.duration;
|
||||
if (ph < data.trackStartAbs) {
|
||||
src.start(context.currentTime + (data.trackStartAbs - ph), 0);
|
||||
} else if (ph < trackEndAbs) {
|
||||
src.start(context.currentTime, Math.min(ph - data.trackStartAbs, Math.max(0, audioBuf.duration - 0.05)));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (opts && opts.limitSec && opts.limitSec < trackEndAbs) {
|
||||
src.stop(context.currentTime + Math.max(0.02, opts.limitSec - Math.max(ph, data.trackStartAbs)));
|
||||
}
|
||||
if (opts && opts.sources && opts.sources.push) opts.sources.push(src);
|
||||
} catch (e) { console.warn('[NativeTrack] schedule decode error:', e); }
|
||||
});
|
||||
}).catch(function () {});
|
||||
};
|
||||
if (key && _nativeTrackUrls[key]) { scheduleUrl(_nativeTrackUrls[key]); return; }
|
||||
ensureRenderNativeTrack(track, bpm, baseOffsetSec, notesOverride).then(function (url) {
|
||||
if (!url) return;
|
||||
scheduleUrl(url);
|
||||
});
|
||||
} catch (e) { console.warn('[NativeTrack] scheduleNativeTrack error:', e); }
|
||||
};"""
|
||||
src = src[:i0] + NEW + src[i1:]
|
||||
print("OK patch 1 (scheduleNativeSfItem -> scheduleNativeTrack)")
|
||||
else:
|
||||
print("SKIP patch 1 (da ap)")
|
||||
|
||||
# ── Patch 2: URL cache per-item + prerenderNativeVstItems → prerenderNativeTracks ──
|
||||
if "prerenderNativeTracks" not in src:
|
||||
P2_START = "// URL cache cho midi-render: key = fingerprint (item+instrument+preset+notes)"
|
||||
P2_END = " }, 800);\n } catch (e) {}\n};"
|
||||
i0 = src.index(P2_START)
|
||||
i1 = src.index(P2_END, i0) + len(P2_END)
|
||||
P2 = """// \u2500\u2500 Native track pre-render (standalone) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
||||
// Render C\u1ea2 TRACK 1 request khi project load \u2192 play l\u1ea7n \u0111\u1ea7u kh\u00f4ng ch\u1edd render.
|
||||
let _prerenderNativeTracksTimer = null;
|
||||
const prerenderNativeTracks = (tracks, bpm) => {
|
||||
try {
|
||||
if (!isStandaloneSf()) return;
|
||||
if (_prerenderNativeTracksTimer) { clearTimeout(_prerenderNativeTracksTimer); _prerenderNativeTracksTimer = null; }
|
||||
_prerenderNativeTracksTimer = setTimeout(function () {
|
||||
_prerenderNativeTracksTimer = null;
|
||||
const seen = {};
|
||||
(tracks || []).forEach(function (track) {
|
||||
const eng = track && track.synth_engine;
|
||||
const native = isStandaloneSf() && (isVstTrackEngine(eng) || isSfTrackEngine(eng)) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
|
||||
if (!native) return;
|
||||
const key = _nativeTrackKey(track, bpm, 0, null);
|
||||
if (!key || seen[key] || _nativeTrackUrls[key]) return;
|
||||
seen[key] = true;
|
||||
ensureRenderNativeTrack(track, bpm, 0, null).then(function () {});
|
||||
});
|
||||
}, 800);
|
||||
} catch (e) {}
|
||||
};"""
|
||||
src = src[:i0] + P2 + src[i1:]
|
||||
print("OK patch 2 (prerenderNativeVstItems -> prerenderNativeTracks)")
|
||||
else:
|
||||
print("SKIP patch 2 (da ap)")
|
||||
|
||||
# ── Patch 3: xóa scheduleNativeVstItem (không còn dùng) ──
|
||||
if "scheduleNativeVstItem" in src:
|
||||
P3_START = "// \u2500\u2500 Native VSTi item render (standalone, transport) \u2500"
|
||||
P3_END = " } catch (e) { console.warn('[NativeVST] scheduleNativeVstItem error:', e); }\n};"
|
||||
i0 = src.index(P3_START)
|
||||
i1 = src.index(P3_END) + len(P3_END)
|
||||
assert src[i1:i1+2] == "\n\n", repr(src[i1:i1+30])
|
||||
src = src[:i0] + src[i1+2:]
|
||||
print("OK patch 3 (xoa scheduleNativeVstItem)")
|
||||
else:
|
||||
print("SKIP patch 3 (da ap)")
|
||||
|
||||
# ── Patch 4: đổi 4 call sites sang scheduleNativeTrack ──
|
||||
C4 = [
|
||||
# site 1: startTrackPlayback
|
||||
(""" if (nativeSf) {
|
||||
midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
|
||||
} else if (nativeVst) {
|
||||
midiItems.forEach(item => scheduleNativeVstItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
|
||||
} else {""",
|
||||
""" if (nativeSf || nativeVst) {
|
||||
scheduleNativeTrack(track, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
|
||||
} else {"""),
|
||||
# site 2: section playback
|
||||
(""" if (subNativeSf) {
|
||||
subMidiItems.forEach(item => scheduleNativeSfItem(subTrack, item, offsetTime, context, subNode, bpm, { baseOffsetSec: secStart, limitSec: secEnd, sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
|
||||
return; // native render \u2192 audio buffer (kh\u00f4ng qua WASM/Carla per-note)
|
||||
}
|
||||
if (subNativeVst) {
|
||||
subMidiItems.forEach(item => scheduleNativeVstItem(subTrack, item, offsetTime, context, subNode, bpm, { baseOffsetSec: secStart, limitSec: secEnd, sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
|
||||
return; // native render \u2192 audio buffer (kh\u00f4ng qua WASM/Carla per-note)
|
||||
}""",
|
||||
""" if (subNativeSf || subNativeVst) {
|
||||
scheduleNativeTrack(subTrack, offsetTime, context, subNode, bpm, { baseOffsetSec: secStart, limitSec: secEnd, sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
|
||||
return; // native render \u2192 audio buffer (kh\u00f4ng qua WASM/Carla per-note)
|
||||
}"""),
|
||||
# site 3: startLocalTrackPlayback
|
||||
(""" if (nativeSf) {
|
||||
midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
|
||||
return; // native render \u2192 audio buffer (kh\u00f4ng qua WASM/Carla per-note)
|
||||
}
|
||||
if (nativeVst) {
|
||||
midiItems.forEach(item => scheduleNativeVstItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
|
||||
return; // native render \u2192 audio buffer (kh\u00f4ng qua WASM/Carla per-note)
|
||||
}""",
|
||||
""" if (nativeSf || nativeVst) {
|
||||
scheduleNativeTrack(track, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
|
||||
return; // native render \u2192 audio buffer (kh\u00f4ng qua WASM/Carla per-note)
|
||||
}"""),
|
||||
# site 4: piano roll (notesOverride)
|
||||
(""" if (isStandaloneSf() && !routeCarla && isSfTrackEngine(synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
|
||||
scheduleNativeSfItem(track, { startTime: 0, notes: midiNotes }, offsetSeconds, context, destNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
|
||||
return; // native render c\u1ea3 tab \u2192 audio buffer (kh\u00f4ng per-note WASM/Carla)
|
||||
}
|
||||
if (isStandaloneSf() && !routeCarla && isVstTrackEngine(synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
|
||||
scheduleNativeVstItem(track, { startTime: 0, notes: midiNotes }, offsetSeconds, context, destNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
|
||||
return; // native render c\u1ea3 tab \u2192 audio buffer (kh\u00f4ng per-note WASM/Carla)
|
||||
}""",
|
||||
""" if (isStandaloneSf() && !routeCarla && (isSfTrackEngine(synthEngine) || isVstTrackEngine(synthEngine)) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
|
||||
scheduleNativeTrack(track, offsetSeconds, context, destNode, bpm, { notesOverride: midiNotes, sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
|
||||
return; // native render c\u1ea3 tab \u2192 audio buffer (kh\u00f4ng per-note WASM/Carla)
|
||||
}"""),
|
||||
]
|
||||
n4 = 0
|
||||
for old, new in C4:
|
||||
if old in src:
|
||||
src = src.replace(old, new, 1)
|
||||
n4 += 1
|
||||
print("OK patch 4 (%d/4 call sites)" % n4)
|
||||
|
||||
# ── Patch 5: stopAllPlayback tăng epoch ──
|
||||
if "_nativePlayEpoch++" not in src:
|
||||
anchor = " try {\n isPlayingRef.current = false;"
|
||||
assert src.count(anchor) == 1, "anchor stopAllPlayback khong duy nhat"
|
||||
src = src.replace(anchor, " try {\n isPlayingRef.current = false;\n _nativePlayEpoch++; // h\u1ee7y m\u1ecdi render/decode pending c\u1ee7a l\u1ea7n play c\u0169", 1)
|
||||
print("OK patch 5 (epoch guard trong stopAllPlayback)")
|
||||
else:
|
||||
print("SKIP patch 5 (da ap)")
|
||||
|
||||
# ── Patch 6: đổi call sites prerender ──
|
||||
n6 = src.count("prerenderNativeVstItems(")
|
||||
if n6:
|
||||
src = src.replace("prerenderNativeVstItems(", "prerenderNativeTracks(")
|
||||
print("OK patch 6 (%d prerender call sites)" % n6)
|
||||
else:
|
||||
print("SKIP patch 6 (da ap)")
|
||||
|
||||
save()
|
||||
print("DONE. changed=", src != orig, "len=", len(src))
|
||||
@@ -0,0 +1,8 @@
|
||||
import io
|
||||
p = 'tools/test_vst2_host.py'
|
||||
s = io.open(p, encoding='utf-8', newline='').read()
|
||||
old = "PLUGIN = r'C:\\Program Files\\Steinberg\\VSTPlugins\\DUNE 3.dll'"
|
||||
assert old in s, 'not found'
|
||||
s = s.replace(old, "PLUGIN = r'C:\\Program Files\\Steinberg\\VSTPlugins\\WaveObserver.dll'")
|
||||
io.open(p, 'w', encoding='utf-8', newline='').write(s)
|
||||
print('patched')
|
||||
@@ -0,0 +1,54 @@
|
||||
import sys
|
||||
|
||||
p = r"C:/Users/locpham/SonicForgeStudio/native_bridge/src/main.cpp"
|
||||
data = open(p, "rb").read()
|
||||
|
||||
def rep(old, new, count=1):
|
||||
global data
|
||||
old = old.encode(); new = new.encode()
|
||||
assert data.count(old) >= count, "pattern not found: %r" % old
|
||||
data = data.replace(old, new, count)
|
||||
|
||||
# unwind stack from exception context BEFORE the dump writer clobbers it
|
||||
stackdump = r"""
|
||||
// Unwind the LIVE stack from the exception context (MiniDumpWriteDump captures
|
||||
// the context at dump time, not at raise time — by then the handler's own
|
||||
// frames replace the real ones). Print return-address candidates that land in
|
||||
// loaded modules, lowest stack address first.
|
||||
#ifdef _WIN32
|
||||
static void DumpStackTraceFromContext(const CONTEXT* ctx) {
|
||||
if (!ctx) return;
|
||||
std::cerr << "[VCRASH] stack trace (raw scan):" << std::endl;
|
||||
const unsigned char* sp = (const unsigned char*)ctx->Rsp;
|
||||
for (int i = 0; i < 512; i++) {
|
||||
uintptr_t val = 0;
|
||||
memcpy(&val, sp + i * 8, sizeof(val)); // stack is readable in a handler
|
||||
HMODULE h = nullptr;
|
||||
char m[MAX_PATH] = {0};
|
||||
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
(LPCSTR)val, &h) && h && GetModuleFileNameA(h, m, MAX_PATH)) {
|
||||
const char* base = strrchr(m, '\\');
|
||||
base = base ? base + 1 : m;
|
||||
std::cerr << " +" << i * 8 << " " << (void*)val << " " << base
|
||||
<< "+0x" << std::hex << (val - (uintptr_t)h) << std::dec << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
"""
|
||||
|
||||
rep("static LONG WINAPI VectoredCrashHandler(PEXCEPTION_POINTERS ep) {",
|
||||
stackdump + "\nstatic LONG WINAPI VectoredCrashHandler(PEXCEPTION_POINTERS ep) {")
|
||||
|
||||
# call it right at handler entry
|
||||
rep(""" std::cerr << "[VCRASH] first-chance code=0x" << std::hex << code
|
||||
<< " addr=0x" << (er ? (void*)er->ExceptionAddress : nullptr)
|
||||
<< std::dec << " tid=" << GetCurrentThreadId() << std::endl;""",
|
||||
""" std::cerr << "[VCRASH] first-chance code=0x" << std::hex << code
|
||||
<< " addr=0x" << (er ? (void*)er->ExceptionAddress : nullptr)
|
||||
<< std::dec << " tid=" << GetCurrentThreadId() << std::endl;
|
||||
DumpStackTraceFromContext(ep ? ep->ContextRecord : nullptr);""")
|
||||
|
||||
open(p, "wb").write(data)
|
||||
print("patched OK, size=", len(data))
|
||||
@@ -0,0 +1,39 @@
|
||||
import sys
|
||||
|
||||
p = r"C:/Users/locpham/SonicForgeStudio/native_bridge/src/main.cpp"
|
||||
data = open(p, "rb").read()
|
||||
|
||||
def rep(old, new, count=1):
|
||||
global data
|
||||
old = old.encode(); new = new.encode()
|
||||
assert data.count(old) >= count, "pattern not found: %r" % old
|
||||
data = data.replace(old, new, count)
|
||||
|
||||
# 1. vectored handler ghi dump cho first-chance; cai gan CrashDumpHandler
|
||||
vectored = r"""
|
||||
// Vectored handler: catches first-chance exceptions INCLUDING heap-corruption
|
||||
// raises that never reach SetUnhandledExceptionFilter (0xC0000374 observed to
|
||||
// bypass it on this machine). Writes the same minidump then continues.
|
||||
#ifdef _WIN32
|
||||
static LONG WINAPI VectoredCrashHandler(PEXCEPTION_POINTERS ep) {
|
||||
PEXCEPTION_RECORD er = ep ? ep->ExceptionRecord : nullptr;
|
||||
DWORD code = er ? er->ExceptionCode : 0;
|
||||
if (code != 0xC0000374 && code != 0xC0000005 && code != 0xC0000409) {
|
||||
return EXCEPTION_CONTINUE_SEARCH; // ignore benign first-chance noise
|
||||
}
|
||||
std::cerr << "[VCRASH] first-chance code=0x" << std::hex << code
|
||||
<< " addr=0x" << (er ? (void*)er->ExceptionAddress : nullptr)
|
||||
<< std::dec << " tid=" << GetCurrentThreadId() << std::endl;
|
||||
// route into the same dump writer
|
||||
return CrashDumpHandler(ep) == EXCEPTION_CONTINUE_SEARCH
|
||||
? EXCEPTION_CONTINUE_SEARCH
|
||||
: EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
#endif
|
||||
"""
|
||||
|
||||
rep("int main(int argc, char* argv[]) {\n SetUnhandledExceptionFilter(CrashDumpHandler);\n",
|
||||
vectored + "\nint main(int argc, char* argv[]) {\n SetUnhandledExceptionFilter(CrashDumpHandler);\n AddVectoredExceptionHandler(1, VectoredCrashHandler);\n")
|
||||
|
||||
open(p, "wb").write(data)
|
||||
print("patched OK, size=", len(data))
|
||||
@@ -0,0 +1,54 @@
|
||||
import struct
|
||||
|
||||
path = r"C:/Users/locpham/AppData/Local/Temp/sf_dumps/bridge_crash_1696.dmp"
|
||||
data = open(path, "rb").read()
|
||||
|
||||
sig, ver, numstreams, sdir = struct.unpack_from("<IIIIIIQ", data, 0)[:4]
|
||||
streams = {}
|
||||
for i in range(numstreams):
|
||||
stype, datasize, rva = struct.unpack_from("<III", data, sdir + i * 12)
|
||||
streams[stype] = (rva, datasize)
|
||||
|
||||
mods = []
|
||||
rva, sz = streams[4]
|
||||
n = struct.unpack_from("<I", data, rva)[0]
|
||||
pos = rva + 4
|
||||
for i in range(n):
|
||||
base, imgsz, cksum, ts, name_rva = struct.unpack_from("<QIIII", data, pos)
|
||||
nlen = struct.unpack_from("<I", data, name_rva)[0]
|
||||
name = data[name_rva + 4:name_rva + 4 + nlen].decode("utf-16-le", "replace").rstrip("\x00")
|
||||
mods.append((base, imgsz, name))
|
||||
pos += 108
|
||||
|
||||
def mod_for(addr):
|
||||
for base, szz, name in mods:
|
||||
if base <= addr < base + szz:
|
||||
return base, name
|
||||
return None, None
|
||||
|
||||
# dump all memory regions, scan for qwords inside DUNE 3.dll or bridge exe
|
||||
targets = []
|
||||
for base, szz, name in mods:
|
||||
if "DUNE" in name or "daw_vst_bridge" in name:
|
||||
targets.append((base, szz, name))
|
||||
|
||||
print("targets:", [(hex(b), s, n.split("\\")[-1]) for b, s, n in targets])
|
||||
|
||||
rva, sz = streams[5]
|
||||
n = struct.unpack_from("<I", data, rva)[0]
|
||||
pos = rva + 4
|
||||
hits = {}
|
||||
for i in range(n):
|
||||
start, msz, mrva = struct.unpack_from("<QII", data, pos)
|
||||
pos += 16
|
||||
chunk = data[mrva:mrva + msz]
|
||||
for j in range(0, len(chunk) - 8, 8):
|
||||
val = struct.unpack_from("<Q", chunk, j)[0]
|
||||
for tb, tsz, tn in targets:
|
||||
if tb <= val < tb + tsz:
|
||||
hits.setdefault((val, tn, val - tb), []).append((start + j, msz))
|
||||
break
|
||||
|
||||
print("\nunique qwords pointing into DUNE/bridge (addr, module, offset, count):")
|
||||
for (val, tn, off), locs in sorted(hits.items()):
|
||||
print(" %016x %s+%x (seen %d times, e.g. mem@%016x size %x)" % (val, tn.split("\\")[-1], off, len(locs), locs[0][0], locs[0][1]))
|
||||
@@ -0,0 +1,78 @@
|
||||
# Inject note-on (live=1) and measure master buffer energy while held.
|
||||
import ctypes, sys, time
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
APP_PID = sys.argv[1]
|
||||
SHM_NAME = f'SonicForge_DAW_IPC_{APP_PID}'
|
||||
OFF_MQ = 2064
|
||||
OFF_MQ_COUNT = 2832
|
||||
OFF_CQ = 2836
|
||||
OFF_CQ_COUNT = 11156
|
||||
OFF_MASTER_L = 8
|
||||
OFF_MASTER_R = 1032
|
||||
MIDI_ENTRY = 12
|
||||
CTRL_ENTRY = 1040
|
||||
|
||||
k32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
||||
k32.OpenFileMappingA.restype = ctypes.c_void_p
|
||||
k32.OpenFileMappingA.argtypes = [ctypes.c_uint32, ctypes.c_int, ctypes.c_char_p]
|
||||
k32.MapViewOfFile.restype = ctypes.c_void_p
|
||||
k32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
|
||||
k32.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
|
||||
k32.CloseHandle.argtypes = [ctypes.c_void_p]
|
||||
|
||||
h = k32.OpenFileMappingA(0x0002, 0, SHM_NAME.encode())
|
||||
if not h: print('FATAL open', ctypes.get_last_error()); sys.exit(1)
|
||||
ptr = k32.MapViewOfFile(h, 0x0002, 0, 0, 0)
|
||||
if not ptr: print('FATAL map', ctypes.get_last_error()); sys.exit(1)
|
||||
|
||||
def send_ctrl(type_, arg0):
|
||||
idx = ctypes.c_uint32.from_address(ptr + OFF_CQ_COUNT).value
|
||||
base = ptr + OFF_CQ + idx * CTRL_ENTRY
|
||||
ctypes.c_uint32.from_address(base + 0).value = type_
|
||||
ctypes.c_uint32.from_address(base + 4).value = arg0
|
||||
ctypes.c_uint32.from_address(ptr + OFF_CQ_COUNT).value = idx + 1
|
||||
|
||||
def send_midi(cmd, ch, pitch, vel, live, data2=0, data3=0):
|
||||
idx = ctypes.c_uint32.from_address(ptr + OFF_MQ_COUNT).value
|
||||
base = ptr + OFF_MQ + idx * MIDI_ENTRY
|
||||
ctypes.c_uint8.from_address(base + 0).value = cmd
|
||||
ctypes.c_uint8.from_address(base + 1).value = ch
|
||||
ctypes.c_uint8.from_address(base + 2).value = pitch
|
||||
ctypes.c_uint8.from_address(base + 3).value = vel
|
||||
ctypes.c_uint8.from_address(base + 4).value = data2
|
||||
ctypes.c_uint8.from_address(base + 5).value = data3
|
||||
ctypes.c_uint8.from_address(base + 6).value = 1 if live else 0
|
||||
ctypes.c_uint8.from_address(base + 7).value = 0
|
||||
ctypes.c_uint32.from_address(base + 8).value = 0
|
||||
ctypes.c_uint32.from_address(ptr + OFF_MQ_COUNT).value = idx + 1
|
||||
|
||||
def energy(off):
|
||||
arr = (ctypes.c_float * 256).from_address(ptr + off)
|
||||
return sum(abs(v) for v in arr)
|
||||
|
||||
def sample(label, n=8):
|
||||
tot = 0.0
|
||||
for _ in range(n):
|
||||
tot += energy(OFF_MASTER_L) + energy(OFF_MASTER_R)
|
||||
time.sleep(0.05)
|
||||
print(f'{label}: avg energy = {tot/(2*n):.8f}')
|
||||
|
||||
print('baseline (no note)')
|
||||
sample('before', 5)
|
||||
print('--- NOTE_ON ch=0 p=60 vel=100 live=1 ---')
|
||||
send_midi(0x9, 0, 60, 100, live=1)
|
||||
time.sleep(0.8)
|
||||
sample('held', 8)
|
||||
print('--- NOTE_ON ch=0 p=72 vel=120 live=1 ---')
|
||||
send_midi(0x9, 0, 72, 120, live=1)
|
||||
time.sleep(0.8)
|
||||
sample('held2', 8)
|
||||
print('--- NOTE_OFF ---')
|
||||
send_midi(0x9, 0, 60, 0, live=1)
|
||||
send_midi(0x9, 0, 72, 0, live=1)
|
||||
time.sleep(0.5)
|
||||
sample('after off', 5)
|
||||
|
||||
k32.UnmapViewOfFile(ptr); k32.CloseHandle(h)
|
||||
print('DONE')
|
||||
@@ -0,0 +1,54 @@
|
||||
# Read SHM state: heartbeat/writeIndex advance + master buffer energy.
|
||||
import ctypes, sys, time
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
APP_PID = sys.argv[1]
|
||||
SHM_NAME = f'SonicForge_DAW_IPC_{APP_PID}'
|
||||
|
||||
# offsets from header: clientReadIndex@0, bridgeWriteIndex@4, masterLeft@8 (256*4),
|
||||
# masterRight@1032, blockTimestamp@2056, midiQueue@2064, midiQueueCount@2832,
|
||||
# controlQueue@2836, controlQueueCount@11156, attachInFlight@11160, heartbeat@11164
|
||||
OFF_CLIENT_READ = 0
|
||||
OFF_BRIDGE_WRITE = 4
|
||||
OFF_MASTER_L = 8
|
||||
OFF_MASTER_R = 1032
|
||||
OFF_HEARTBEAT = 11164
|
||||
|
||||
k32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
||||
k32.OpenFileMappingA.restype = ctypes.c_void_p
|
||||
k32.OpenFileMappingA.argtypes = [ctypes.c_uint32, ctypes.c_int, ctypes.c_char_p]
|
||||
k32.MapViewOfFile.restype = ctypes.c_void_p
|
||||
k32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
|
||||
k32.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
|
||||
k32.CloseHandle.argtypes = [ctypes.c_void_p]
|
||||
|
||||
h = k32.OpenFileMappingA(0x0002, 0, SHM_NAME.encode())
|
||||
if not h:
|
||||
print('FATAL: open SHM failed', ctypes.get_last_error())
|
||||
sys.exit(1)
|
||||
ptr = k32.MapViewOfFile(h, 0x0002, 0, 0, 0)
|
||||
if not ptr:
|
||||
print('FATAL: map failed', ctypes.get_last_error())
|
||||
sys.exit(1)
|
||||
|
||||
def r32(off): return ctypes.c_uint32.from_address(ptr + off).value
|
||||
def energy(off):
|
||||
arr = (ctypes.c_float * 256).from_address(ptr + off)
|
||||
return sum(abs(v) for v in arr)
|
||||
|
||||
samples = []
|
||||
for i in range(20):
|
||||
hb = r32(OFF_HEARTBEAT)
|
||||
wi = r32(OFF_BRIDGE_WRITE)
|
||||
eL = energy(OFF_MASTER_L)
|
||||
eR = energy(OFF_MASTER_R)
|
||||
samples.append((hb, wi, eL, eR))
|
||||
time.sleep(0.15)
|
||||
print('sample hb wi eL eR')
|
||||
for s in samples: print(s)
|
||||
print('hb advanced:', len(set(s[0] for s in samples)) > 1)
|
||||
print('wi advanced:', len(set(s[1] for s in samples)) > 1)
|
||||
print('max eL:', max(s[2] for s in samples), 'max eR:', max(s[3] for s in samples))
|
||||
|
||||
k32.UnmapViewOfFile(ptr)
|
||||
k32.CloseHandle(h)
|
||||
@@ -0,0 +1,96 @@
|
||||
# SHM injection test for live-flag fix: verify transportStopped guard drops
|
||||
# non-live note-ons but passes live ones (reserved[0]=1).
|
||||
# Layout from SharedMemoryIPC.h (verified): base=2064 bytes, midiQueue 64x12 @2064,
|
||||
# midiQueueCount @2832, controlQueue 8x1040 @2836, controlQueueCount @11156.
|
||||
import ctypes, sys, time, os
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
|
||||
APP_PID = sys.argv[1] if len(sys.argv) > 1 else '9712'
|
||||
SHM_NAME = f'SonicForge_DAW_IPC_{APP_PID}'
|
||||
|
||||
OFF_MQ = 2064
|
||||
OFF_MQ_COUNT = 2832
|
||||
OFF_CQ = 2836
|
||||
OFF_CQ_COUNT = 11156
|
||||
MIDI_ENTRY = 12
|
||||
CTRL_ENTRY = 1040
|
||||
|
||||
k32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
||||
k32.OpenFileMappingA.restype = ctypes.c_void_p
|
||||
k32.OpenFileMappingA.argtypes = [ctypes.c_uint32, ctypes.c_int, ctypes.c_char_p]
|
||||
k32.MapViewOfFile.restype = ctypes.c_void_p
|
||||
k32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
|
||||
k32.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
|
||||
k32.CloseHandle.argtypes = [ctypes.c_void_p]
|
||||
|
||||
def get_shm():
|
||||
h = k32.OpenFileMappingA(0x0002, 0, SHM_NAME.encode()) # FILE_MAP_WRITE first
|
||||
if not h:
|
||||
h = k32.OpenFileMappingA(0x0004, 0, SHM_NAME.encode()) # fallback FILE_MAP_READ
|
||||
if not h:
|
||||
err = ctypes.get_last_error()
|
||||
print(f'FATAL: cannot open SHM {SHM_NAME} (err={err})')
|
||||
sys.exit(1)
|
||||
ptr = k32.MapViewOfFile(h, 0x0002, 0, 0, 0) # FILE_MAP_WRITE
|
||||
if not ptr:
|
||||
print(f'FATAL: MapViewOfFile err={ctypes.get_last_error()}')
|
||||
sys.exit(1)
|
||||
return h, ptr
|
||||
|
||||
def w32(ptr, off, val):
|
||||
ctypes.c_uint32.from_address(ptr + off).value = val
|
||||
|
||||
def send_ctrl(ptr, type_, arg0, arg1=0, channel=0):
|
||||
idx = ctypes.c_uint32.from_address(ptr + OFF_CQ_COUNT).value
|
||||
if idx >= 8:
|
||||
print('FATAL: control queue full')
|
||||
sys.exit(1)
|
||||
base = ptr + OFF_CQ + idx * CTRL_ENTRY
|
||||
ctypes.c_uint32.from_address(base + 0).value = type_
|
||||
ctypes.c_uint32.from_address(base + 4).value = arg0
|
||||
ctypes.c_uint32.from_address(base + 8).value = arg1
|
||||
ctypes.c_uint32.from_address(base + 12).value = channel
|
||||
ctypes.c_uint32.from_address(ptr + OFF_CQ_COUNT).value = idx + 1
|
||||
print(f'sent ctrl type={type_} arg0={arg0}')
|
||||
|
||||
def send_midi(ptr, cmd, channel, pitch, velocity, live, data2=0, data3=0):
|
||||
idx = ctypes.c_uint32.from_address(ptr + OFF_MQ_COUNT).value
|
||||
if idx >= 64:
|
||||
print('FATAL: midi queue full')
|
||||
sys.exit(1)
|
||||
base = ptr + OFF_MQ + idx * MIDI_ENTRY
|
||||
ctypes.c_uint8.from_address(base + 0).value = cmd
|
||||
ctypes.c_uint8.from_address(base + 1).value = channel
|
||||
ctypes.c_uint8.from_address(base + 2).value = pitch
|
||||
ctypes.c_uint8.from_address(base + 3).value = velocity
|
||||
ctypes.c_uint8.from_address(base + 4).value = data2
|
||||
ctypes.c_uint8.from_address(base + 5).value = data3
|
||||
ctypes.c_uint8.from_address(base + 6).value = 1 if live else 0 # reserved[0] = live flag
|
||||
ctypes.c_uint8.from_address(base + 7).value = 0
|
||||
ctypes.c_uint32.from_address(base + 8).value = 0 # sampleOffset
|
||||
ctypes.c_uint32.from_address(ptr + OFF_MQ_COUNT).value = idx + 1
|
||||
print(f'sent midi cmd=0x{cmd:x} ch={channel} pitch={pitch} vel={velocity} live={live}')
|
||||
|
||||
h, ptr = get_shm()
|
||||
print(f'SHM {SHM_NAME} mapped at {ptr:#x}')
|
||||
|
||||
# 1) transport STOP -> guard arms
|
||||
send_ctrl(ptr, 3, 0) # TRANSPORT STOP
|
||||
time.sleep(1.0)
|
||||
# 2) non-live NOTE_ON -> must be dropped (no [midi] ON log)
|
||||
send_midi(ptr, 0x9, 0, 60, 100, live=0)
|
||||
time.sleep(1.0)
|
||||
# 3) live NOTE_ON -> must pass
|
||||
send_midi(ptr, 0x9, 0, 60, 100, live=1)
|
||||
time.sleep(1.0)
|
||||
# 4) non-live NOTE_ON again -> dropped (sanity)
|
||||
send_midi(ptr, 0x9, 0, 62, 90, live=0)
|
||||
time.sleep(1.0)
|
||||
# 5) restore PLAY so app continues working
|
||||
send_ctrl(ptr, 3, 1)
|
||||
time.sleep(0.5)
|
||||
|
||||
k32.UnmapViewOfFile(ptr)
|
||||
k32.CloseHandle(h)
|
||||
print('DONE')
|
||||
@@ -0,0 +1,59 @@
|
||||
// tools/test_midi_polyphony.js — runnable check cho Phase A (dedup/legato/polyphony).
|
||||
// Đọc app.jsx, extract 2 hàm thuần, eval, assert spec §II.1 + §II.5.
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const src = fs.readFileSync(path.join(__dirname, '..', 'app', 'static', 'js', 'app.jsx'), 'utf8');
|
||||
|
||||
const start = src.indexOf('const _cleanNativeNotes = (notes) => {');
|
||||
const end = src.indexOf('const _nativeTrackKey', start);
|
||||
if (start < 0 || end < 0) { console.error('FAIL: khong tim thay 2 ham'); process.exit(1); }
|
||||
const code = src.slice(start, end) +
|
||||
';globalThis._cleanNativeNotes=_cleanNativeNotes;' +
|
||||
'globalThis._scaleNativePolyphony=_scaleNativePolyphony;';
|
||||
// eslint-disable-next-line no-eval
|
||||
(0, eval)(code); // indirect eval: const khai báo ở global scope
|
||||
|
||||
let fails = 0;
|
||||
const assert = (cond, msg) => { if (!cond) { fails++; console.error('FAIL: ' + msg); } else { console.log('ok: ' + msg); } };
|
||||
const near = (a, b, eps) => Math.abs(a - b) < (eps || 1e-9);
|
||||
|
||||
// 1. 8-note chord cùng lúc → tổng peak ≈ 0.3*8/sqrt(8) ≤ 1.0
|
||||
const chord = [];
|
||||
for (let i = 0; i < 8; i++) chord.push({ pitch: 60 + i, start_beat: 0, duration_beats: 2, velocity: 0.3 });
|
||||
const sc = _scaleNativePolyphony(_cleanNativeNotes(chord));
|
||||
const sum = sc.reduce((a, n) => a + n.velocity, 0);
|
||||
assert(sc.length === 8, 'chord giu 8 note');
|
||||
assert(near(sum, 8 * 0.3 / Math.sqrt(8)), 'tong peak ' + sum.toFixed(4) + ' ~= ' + (8 * 0.3 / Math.sqrt(8)).toFixed(4));
|
||||
assert(sum <= 1.0, 'tong peak <= 1.0');
|
||||
|
||||
// 2. Solo → giữ nguyên velocity
|
||||
const solo = _scaleNativePolyphony([{ pitch: 60, start_beat: 0, duration_beats: 1, velocity: 0.8 }]);
|
||||
assert(near(solo[0].velocity, 0.8), 'solo velocity giu nguyen 0.8');
|
||||
|
||||
// 3. Dedup: cùng pitch + start_beat → gộp, velocity max
|
||||
const dup = _cleanNativeNotes([
|
||||
{ pitch: 60, start_beat: 1, duration_beats: 2, velocity: 0.5 },
|
||||
{ pitch: 60, start_beat: 1, duration_beats: 3, velocity: 0.7 },
|
||||
]);
|
||||
assert(dup.length === 1, 'dedup gop 2 note');
|
||||
assert(near(dup[0].velocity, 0.7), 'dedup giu velocity cao 0.7');
|
||||
assert(near(dup[0].duration_beats, 3), 'dedup giu duration dai nhat');
|
||||
|
||||
// 4. Legato: note trước cắt 0.01 beats trước note sau cùng pitch
|
||||
const leg = _cleanNativeNotes([
|
||||
{ pitch: 60, start_beat: 0, duration_beats: 4, velocity: 0.8 },
|
||||
{ pitch: 60, start_beat: 2, duration_beats: 1, velocity: 0.8 },
|
||||
]);
|
||||
assert(leg.length === 2, 'legato giu 2 note');
|
||||
assert(near(leg[0].duration_beats, 2 - 0.01, 1e-6), 'note dau cat con ' + leg[0].duration_beats);
|
||||
assert(near(leg[1].duration_beats, 1), 'note sau giu nguyen');
|
||||
|
||||
// 5. Nối tiếp khác pitch → không cắt
|
||||
const seq = _cleanNativeNotes([
|
||||
{ pitch: 60, start_beat: 0, duration_beats: 4, velocity: 0.8 },
|
||||
{ pitch: 64, start_beat: 2, duration_beats: 1, velocity: 0.8 },
|
||||
]);
|
||||
assert(near(seq[0].duration_beats, 4), 'khac pitch khong bi cat');
|
||||
|
||||
console.log(fails ? 'FAILED: ' + fails : 'ALL PASS');
|
||||
process.exit(fails ? 1 : 0);
|
||||
@@ -0,0 +1,151 @@
|
||||
# Test VST2 host: create SHM, spawn bridge (stdout->file), LOAD_INSTRUMENT (VST2),
|
||||
# noteOn, read ring audio energy. Verify [Vst2Instrument] loaded log.
|
||||
import ctypes, subprocess, sys, time, os, tempfile
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
SHM_NAME = 'SonicForge_DAW_IPC_V2TEST'
|
||||
SHM_SIZE = 30000
|
||||
PLUGIN = r'C:/Users/locpham/SonicForgeStudio/app/storage/soundfonts/518e850f-a5d3-4790-b1f9-0c90c203c524.sf2'
|
||||
BRIDGE = r'C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe'
|
||||
|
||||
k32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
||||
k32.CreateFileMappingA.restype = ctypes.c_void_p
|
||||
k32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_char_p]
|
||||
k32.MapViewOfFile.restype = ctypes.c_void_p
|
||||
k32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
|
||||
k32.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
|
||||
k32.CloseHandle.argtypes = [ctypes.c_void_p]
|
||||
|
||||
h = k32.CreateFileMappingA(ctypes.c_void_p(-1), None, 0x04, 0, SHM_SIZE, SHM_NAME.encode())
|
||||
if not h:
|
||||
print('FATAL CreateFileMappingA', ctypes.get_last_error()); sys.exit(1)
|
||||
ptr = k32.MapViewOfFile(h, 0x02, 0, 0, 0)
|
||||
if not ptr:
|
||||
print('FATAL MapViewOfFile', ctypes.get_last_error()); sys.exit(1)
|
||||
|
||||
def w32(off, val): ctypes.c_uint32.from_address(ptr + off).value = val
|
||||
def r32(off): return ctypes.c_uint32.from_address(ptr + off).value
|
||||
def energy(off, n=256):
|
||||
arr = (ctypes.c_float * n).from_address(ptr + off)
|
||||
return sum(abs(v) for v in arr)
|
||||
|
||||
OFF_MQ = 2064; OFF_MQ_COUNT = 2832
|
||||
OFF_CQ = 2836; OFF_CQ_COUNT = 11156
|
||||
OFF_HEARTBEAT = 11164
|
||||
MIDI_ENTRY = 12; CTRL_ENTRY = 1040
|
||||
OFF_RING_L = 11172; OFF_RING_R = 11172 + 8192
|
||||
OFF_RING_MASK = 11168
|
||||
|
||||
def send_ctrl(type_, arg0, arg1, channel, path=b''):
|
||||
idx = r32(OFF_CQ_COUNT)
|
||||
if idx >= 8: print('FATAL ctrl full'); sys.exit(1)
|
||||
base = ptr + OFF_CQ + idx * CTRL_ENTRY
|
||||
ctypes.c_uint32.from_address(base + 0).value = type_
|
||||
ctypes.c_uint32.from_address(base + 4).value = arg0
|
||||
ctypes.c_uint32.from_address(base + 8).value = arg1
|
||||
ctypes.c_uint32.from_address(base + 12).value = channel
|
||||
if path:
|
||||
ctypes.memmove(base + 16, path, len(path))
|
||||
ctypes.c_char.from_address(base + 16 + len(path)).value = b'\0'
|
||||
w32(OFF_CQ_COUNT, idx + 1)
|
||||
print(f'sent ctrl type={type_} arg0={arg0} arg1={arg1} ch={channel}')
|
||||
|
||||
def send_midi(cmd, channel, pitch, velocity, live=1, d2=0, d3=0):
|
||||
idx = r32(OFF_MQ_COUNT)
|
||||
if idx >= 64: print('FATAL midi full'); sys.exit(1)
|
||||
base = ptr + OFF_MQ + idx * MIDI_ENTRY
|
||||
ctypes.c_uint8.from_address(base + 0).value = cmd
|
||||
ctypes.c_uint8.from_address(base + 1).value = channel
|
||||
ctypes.c_uint8.from_address(base + 2).value = pitch
|
||||
ctypes.c_uint8.from_address(base + 3).value = velocity
|
||||
ctypes.c_uint8.from_address(base + 4).value = d2
|
||||
ctypes.c_uint8.from_address(base + 5).value = d3
|
||||
ctypes.c_uint8.from_address(base + 6).value = 1 if live else 0
|
||||
ctypes.c_uint32.from_address(base + 8).value = 0
|
||||
w32(OFF_MQ_COUNT, idx + 1)
|
||||
print(f'sent midi cmd=0x{cmd:x} ch={channel} p={pitch} v={velocity}')
|
||||
|
||||
env = dict(os.environ)
|
||||
env['SF_SHM_NAME'] = SHM_NAME
|
||||
env['SF_SAMPLE_RATE'] = '44100'
|
||||
env['SF_BLOCK_SIZE'] = '256'
|
||||
env['SF_PARENT_PID'] = str(os.getpid())
|
||||
empty_state = os.path.join(tempfile.gettempdir(), 'sf_v2test_state.json')
|
||||
open(empty_state, 'w').write('{}')
|
||||
env['SF_STATE_FILE'] = empty_state
|
||||
|
||||
bout = tempfile.NamedTemporaryFile(delete=False, suffix='.log', mode='w', encoding='utf-8')
|
||||
bout_path = bout.name
|
||||
bout.close()
|
||||
with open(bout_path, 'w', encoding='utf-8') as f:
|
||||
proc = subprocess.Popen([BRIDGE, '--shm', SHM_NAME], env=env,
|
||||
stdout=f, stderr=subprocess.STDOUT)
|
||||
print('bridge pid', proc.pid)
|
||||
time.sleep(2.5)
|
||||
|
||||
def tail_bridge(tag):
|
||||
with open(bout_path, encoding='utf-8', errors='replace') as f:
|
||||
lines = f.read().splitlines()
|
||||
print(f'--- bridge {tag} ({len(lines)} lines) ---')
|
||||
for l in lines[-50:]:
|
||||
print(l)
|
||||
|
||||
def head_bridge(tag, n=60):
|
||||
with open(bout_path, encoding='utf-8', errors='replace') as f:
|
||||
lines = f.read().splitlines()
|
||||
print(f'--- bridge HEAD {tag} ({len(lines)} lines) ---')
|
||||
for l in lines[:n]:
|
||||
print(l)
|
||||
|
||||
# LOAD VST2 (type enum: VST2=1)
|
||||
send_ctrl(2, 2, len(PLUGIN.encode()), 0, PLUGIN.encode())
|
||||
print('proc alive after load:', proc.poll() is None)
|
||||
time.sleep(2.0)
|
||||
head_bridge('after LOAD')
|
||||
tail_bridge('after LOAD')
|
||||
|
||||
# TRANSPORT PLAY so plugins gating DSP on transport start producing sound
|
||||
send_ctrl(3, 1, 0, 0)
|
||||
time.sleep(0.3)
|
||||
# CC7 volume max (some presets default to 0 volume).
|
||||
# NOTE: bridge reads CC value from data2 (matches app.jsx pushEvent data2).
|
||||
send_midi(0xB, 0, 7, 0, d2=127)
|
||||
time.sleep(0.2)
|
||||
|
||||
send_midi(0x9, 0, 60, 100)
|
||||
print('mq count after noteOn send:', r32(OFF_MQ_COUNT))
|
||||
time.sleep(0.5)
|
||||
print('mq count 0.5s later:', r32(OFF_MQ_COUNT))
|
||||
for t in (0.5, 1.0, 2.0, 4.0):
|
||||
time.sleep(t)
|
||||
print('energy at +', t, ':', round(max(sum(abs(v) for v in (ctypes.c_float * 256).from_address(ptr + OFF_RING_L + s * 1024)) + sum(abs(v) for v in (ctypes.c_float * 256).from_address(ptr + OFF_RING_R + s * 1024)) for s in range(8)), 3))
|
||||
|
||||
mask = r32(OFF_RING_MASK) or 7
|
||||
wi = r32(4)
|
||||
slots = [(wi - i) & mask for i in range(mask + 1)]
|
||||
energies = []
|
||||
for s in slots:
|
||||
e = energy(OFF_RING_L + s * 1024) + energy(OFF_RING_R + s * 1024)
|
||||
energies.append(e)
|
||||
print('ringMask', mask, 'writeIndex', wi, 'heartbeat', r32(OFF_HEARTBEAT))
|
||||
print('slot energies (L+R):', [round(e, 3) for e in energies])
|
||||
print('max energy:', round(max(energies), 3))
|
||||
|
||||
send_midi(0x8, 0, 60, 0)
|
||||
time.sleep(0.5)
|
||||
tail_bridge('tail')
|
||||
|
||||
proc.terminate()
|
||||
time.sleep(1)
|
||||
if proc.poll() is None:
|
||||
proc.kill()
|
||||
k32.UnmapViewOfFile(ptr); k32.CloseHandle(h)
|
||||
print('--- grep Vst2Instrument in log ---')
|
||||
with open(bout_path, encoding='utf-8', errors='replace') as f:
|
||||
hits = [l.rstrip() for l in f if 'Vst2Instrument' in l]
|
||||
for l in hits[-20:]:
|
||||
print(l)
|
||||
import shutil
|
||||
shutil.copy(bout_path, r'C:/Users/locpham/SonicForgeStudio/tools/_bridge_keep.log')
|
||||
os.remove(bout_path)
|
||||
print('done')
|
||||
@@ -0,0 +1,214 @@
|
||||
# Test VST2 64-bit GUI: create SHM, spawn bridge, LOAD DUNE 3 (VST2),
|
||||
# OPEN_GUI (bridge auto-creates native window), verify log "GUI attached",
|
||||
# EnumWindows finds the host window + editor child, noteOn -> ring energy.
|
||||
import ctypes, subprocess, sys, time, os, tempfile
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
SHM_NAME = 'SonicForge_DAW_IPC_V2GUITEST'
|
||||
SHM_SIZE = 30000
|
||||
PLUGIN = sys.argv[1] if len(sys.argv) > 1 else r'C:\Program Files\Steinberg\VSTPlugins\DUNE 3.dll'
|
||||
BRIDGE = r'C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe'
|
||||
|
||||
k32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
||||
k32.CreateFileMappingA.restype = ctypes.c_void_p
|
||||
k32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_char_p]
|
||||
k32.MapViewOfFile.restype = ctypes.c_void_p
|
||||
k32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
|
||||
k32.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
|
||||
k32.CloseHandle.argtypes = [ctypes.c_void_p]
|
||||
user32 = ctypes.WinDLL('user32', use_last_error=True)
|
||||
user32.EnumWindows.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
||||
user32.GetWindowThreadProcessId.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint32)]
|
||||
user32.GetClassNameA.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int]
|
||||
user32.GetWindowTextA.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int]
|
||||
user32.EnumChildWindows.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
|
||||
user32.PostMessageA.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p, ctypes.c_void_p]
|
||||
|
||||
h = k32.CreateFileMappingA(ctypes.c_void_p(-1), None, 0x04, 0, SHM_SIZE, SHM_NAME.encode())
|
||||
if not h:
|
||||
print('FATAL CreateFileMappingA', ctypes.get_last_error()); sys.exit(1)
|
||||
ptr = k32.MapViewOfFile(h, 0x02, 0, 0, 0)
|
||||
if not ptr:
|
||||
print('FATAL MapViewOfFile', ctypes.get_last_error()); sys.exit(1)
|
||||
|
||||
def w32(off, val): ctypes.c_uint32.from_address(ptr + off).value = val
|
||||
def r32(off): return ctypes.c_uint32.from_address(ptr + off).value
|
||||
|
||||
OFF_MQ = 2064; OFF_MQ_COUNT = 2832
|
||||
OFF_CQ = 2836; OFF_CQ_COUNT = 11156
|
||||
OFF_HEARTBEAT = 11164
|
||||
MIDI_ENTRY = 12; CTRL_ENTRY = 1040
|
||||
OFF_RING_L = 11172; OFF_RING_R = 11172 + 8192
|
||||
OFF_RING_MASK = 11168
|
||||
|
||||
def send_ctrl(type_, arg0, arg1, channel, path=b''):
|
||||
idx = r32(OFF_CQ_COUNT)
|
||||
if idx >= 8: print('FATAL ctrl full'); sys.exit(1)
|
||||
base = ptr + OFF_CQ + idx * CTRL_ENTRY
|
||||
ctypes.c_uint32.from_address(base + 0).value = type_
|
||||
ctypes.c_uint32.from_address(base + 4).value = arg0
|
||||
ctypes.c_uint32.from_address(base + 8).value = arg1
|
||||
ctypes.c_uint32.from_address(base + 12).value = channel
|
||||
if path:
|
||||
ctypes.memmove(base + 16, path, len(path))
|
||||
ctypes.c_char.from_address(base + 16 + len(path)).value = b'\0'
|
||||
w32(OFF_CQ_COUNT, idx + 1)
|
||||
print(f'sent ctrl type={type_} arg0={arg0} arg1={arg1} ch={channel}')
|
||||
|
||||
def send_midi(cmd, channel, pitch, velocity, live=1, d2=0, d3=0):
|
||||
idx = r32(OFF_MQ_COUNT)
|
||||
if idx >= 64: print('FATAL midi full'); sys.exit(1)
|
||||
base = ptr + OFF_MQ + idx * MIDI_ENTRY
|
||||
ctypes.c_uint8.from_address(base + 0).value = cmd
|
||||
ctypes.c_uint8.from_address(base + 1).value = channel
|
||||
ctypes.c_uint8.from_address(base + 2).value = pitch
|
||||
ctypes.c_uint8.from_address(base + 3).value = velocity
|
||||
ctypes.c_uint8.from_address(base + 4).value = d2
|
||||
ctypes.c_uint8.from_address(base + 5).value = d3
|
||||
ctypes.c_uint8.from_address(base + 6).value = 1 if live else 0
|
||||
ctypes.c_uint32.from_address(base + 8).value = 0
|
||||
w32(OFF_MQ_COUNT, idx + 1)
|
||||
print(f'sent midi cmd=0x{cmd:x} ch={channel} p={pitch} v={velocity}')
|
||||
|
||||
env = dict(os.environ)
|
||||
env['SF_SHM_NAME'] = SHM_NAME
|
||||
env['SF_SAMPLE_RATE'] = '44100'
|
||||
env['SF_BLOCK_SIZE'] = '256'
|
||||
env['SF_PARENT_PID'] = str(os.getpid())
|
||||
empty_state = os.path.join(tempfile.gettempdir(), 'sf_v2guittest_state.json')
|
||||
open(empty_state, 'w').write('{}')
|
||||
env['SF_STATE_FILE'] = empty_state
|
||||
|
||||
bout_path = os.path.join(tempfile.gettempdir(), 'sf_v2guittest_bridge.log')
|
||||
with open(bout_path, 'w', encoding='utf-8') as f:
|
||||
proc = subprocess.Popen([BRIDGE, '--shm', SHM_NAME], env=env,
|
||||
stdout=f, stderr=subprocess.STDOUT)
|
||||
print('bridge pid', proc.pid)
|
||||
time.sleep(2.5)
|
||||
|
||||
def tail_bridge(tag):
|
||||
with open(bout_path, encoding='utf-8', errors='replace') as f:
|
||||
lines = f.read().splitlines()
|
||||
print(f'--- bridge {tag} ({len(lines)} lines) ---')
|
||||
for l in lines[-40:]:
|
||||
print(l)
|
||||
|
||||
def dump_modules(bridge_pid):
|
||||
# module base..end ranges of the bridge process (CreateToolhelp32Snapshot)
|
||||
k32t = ctypes.WinDLL('kernel32', use_last_error=True)
|
||||
class MODULEENTRY32W(ctypes.Structure):
|
||||
_fields_ = [('dwSize', ctypes.c_uint32), ('th32ModuleID', ctypes.c_uint32),
|
||||
('th32ProcessID', ctypes.c_uint32), ('GlblcntUsage', ctypes.c_uint32),
|
||||
('ProccntUsage', ctypes.c_uint32), ('modBaseAddr', ctypes.c_void_p),
|
||||
('modBaseSize', ctypes.c_uint32), ('hModule', ctypes.c_void_p),
|
||||
('szModule', ctypes.c_wchar * 256), ('szExePath', ctypes.c_wchar * 260)]
|
||||
k32t.CreateToolhelp32Snapshot.restype = ctypes.c_void_p
|
||||
k32t.CreateToolhelp32Snapshot.argtypes = [ctypes.c_uint32, ctypes.c_uint32]
|
||||
k32t.Module32FirstW.argtypes = [ctypes.c_void_p, ctypes.POINTER(MODULEENTRY32W)]
|
||||
k32t.Module32NextW.argtypes = [ctypes.c_void_p, ctypes.POINTER(MODULEENTRY32W)]
|
||||
snap = k32t.CreateToolhelp32Snapshot(0x8, bridge_pid) # TH32CS_SNAPMODULE
|
||||
if snap == ctypes.c_void_p(-1).value or not snap:
|
||||
print('module snapshot failed', ctypes.get_last_error()); return []
|
||||
me = MODULEENTRY32W(); me.dwSize = ctypes.sizeof(MODULEENTRY32W)
|
||||
mods = []
|
||||
if k32t.Module32FirstW(snap, ctypes.byref(me)):
|
||||
while True:
|
||||
base = me.modBaseAddr or 0
|
||||
mods.append((base, base + me.modBaseSize, me.szModule))
|
||||
if not k32t.Module32NextW(snap, ctypes.byref(me)):
|
||||
break
|
||||
k32t.CloseHandle(snap)
|
||||
return mods
|
||||
|
||||
def dump_windows(bridge_pid):
|
||||
found = []
|
||||
@ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
|
||||
def cb(hwnd, lparam):
|
||||
pid = ctypes.c_uint32()
|
||||
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
||||
if pid.value == bridge_pid:
|
||||
cls = ctypes.create_string_buffer(128)
|
||||
user32.GetClassNameA(hwnd, cls, 128)
|
||||
title = ctypes.create_string_buffer(256)
|
||||
user32.GetWindowTextA(hwnd, title, 256)
|
||||
found.append((hwnd, cls.value.decode('latin1'), title.value.decode('latin1', 'replace')))
|
||||
return 1
|
||||
user32.EnumWindows(cb, 0)
|
||||
print(f'--- top-level windows of pid {bridge_pid}: {len(found)} ---')
|
||||
for hwnd, cls, title in found:
|
||||
print(f' hwnd={hwnd:#x} class={cls!r} title={title!r}')
|
||||
# enumerate children of this window
|
||||
kids = []
|
||||
@ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
|
||||
def cbk(hw, lp):
|
||||
c2 = ctypes.create_string_buffer(128)
|
||||
user32.GetClassNameA(hw, c2, 128)
|
||||
t2 = ctypes.create_string_buffer(256)
|
||||
user32.GetWindowTextA(hw, t2, 256)
|
||||
kids.append((hw, c2.value.decode('latin1'), t2.value.decode('latin1', 'replace')))
|
||||
return 1
|
||||
user32.EnumChildWindows(hwnd, cbk, 0)
|
||||
for kh, kc, kt in kids:
|
||||
print(f' child hwnd={kh:#x} class={kc!r} title={kt!r}')
|
||||
return found
|
||||
|
||||
# LOAD VST2 (type enum: VST2=1)
|
||||
send_ctrl(2, 1, len(PLUGIN.encode()), 0, PLUGIN.encode())
|
||||
print('proc alive after load:', proc.poll() is None)
|
||||
time.sleep(2.0)
|
||||
tail_bridge('after LOAD')
|
||||
|
||||
pbase = os.path.basename(PLUGIN).lower().replace('.dll', '')
|
||||
print('--- bridge modules (plugin-related) ---')
|
||||
for b, e, name in dump_modules(proc.pid):
|
||||
nl = name.lower()
|
||||
if 'sonicforge' in nl or 'daw_vst' in nl or pbase in nl:
|
||||
print(f' {b:#x}..{e:#x} {name}')
|
||||
|
||||
# OPEN_GUI: type=4, arg1=0 (bridge creates native window), arg2=pluginId
|
||||
send_ctrl(4, 0, 0, 0, PLUGIN.encode())
|
||||
time.sleep(3.0)
|
||||
tail_bridge('after OPEN_GUI')
|
||||
print('proc alive after open_gui:', proc.poll() is None)
|
||||
wins = dump_windows(proc.pid)
|
||||
host = [w for w in wins if 'SonicForge_Native_VST3_Class' in w[1]]
|
||||
print('host window found:', len(host) > 0)
|
||||
if host:
|
||||
child_count = 0
|
||||
for w in wins:
|
||||
pass
|
||||
# count editor children under the host window
|
||||
kids = []
|
||||
@ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
|
||||
def cbk2(hw, lp):
|
||||
c2 = ctypes.create_string_buffer(128)
|
||||
user32.GetClassNameA(hw, c2, 128)
|
||||
kids.append((hw, c2.value.decode('latin1')))
|
||||
return 1
|
||||
user32.EnumChildWindows(host[0][0], cbk2, 0)
|
||||
print('editor child windows under host:', len(kids), [k[1] for k in kids])
|
||||
|
||||
# TRANSPORT PLAY + note -> energy
|
||||
send_ctrl(3, 1, 0, 0)
|
||||
time.sleep(0.3)
|
||||
send_midi(0xB, 0, 7, 127)
|
||||
time.sleep(0.2)
|
||||
send_midi(0x9, 0, 60, 100)
|
||||
for t in (0.5, 1.0, 2.0):
|
||||
time.sleep(t)
|
||||
mask = r32(OFF_RING_MASK) or 7
|
||||
wi = r32(4)
|
||||
slots = [(wi - i) & mask for i in range(mask + 1)]
|
||||
es = []
|
||||
for s in slots:
|
||||
e = sum(abs(v) for v in (ctypes.c_float * 256).from_address(ptr + OFF_RING_L + s * 1024)) + \
|
||||
sum(abs(v) for v in (ctypes.c_float * 256).from_address(ptr + OFF_RING_R + s * 1024))
|
||||
es.append(e)
|
||||
print(f'energy at +{t}: max={round(max(es), 3)} heartbeat={r32(OFF_HEARTBEAT)}')
|
||||
|
||||
print('proc alive at end:', proc.poll() is None)
|
||||
proc.terminate()
|
||||
time.sleep(0.5)
|
||||
k32.UnmapViewOfFile(ptr)
|
||||
k32.CloseHandle(h)
|
||||
print('DONE')
|
||||
@@ -0,0 +1,137 @@
|
||||
# Test VST2 host: create SHM, spawn bridge (stdout->file), LOAD_INSTRUMENT (VST2),
|
||||
# noteOn, read ring audio energy. Verify [Vst2Instrument] loaded log.
|
||||
import ctypes, subprocess, sys, time, os, tempfile
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
SHM_NAME = 'SonicForge_DAW_IPC_V2TEST'
|
||||
SHM_SIZE = 30000
|
||||
PLUGIN = r'C:\Program Files\Steinberg\VSTPlugins\DUNE 3.dll'
|
||||
BRIDGE = r'C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe'
|
||||
|
||||
k32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
||||
k32.CreateFileMappingA.restype = ctypes.c_void_p
|
||||
k32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_char_p]
|
||||
k32.MapViewOfFile.restype = ctypes.c_void_p
|
||||
k32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
|
||||
k32.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
|
||||
k32.CloseHandle.argtypes = [ctypes.c_void_p]
|
||||
|
||||
h = k32.CreateFileMappingA(ctypes.c_void_p(-1), None, 0x04, 0, SHM_SIZE, SHM_NAME.encode())
|
||||
if not h:
|
||||
print('FATAL CreateFileMappingA', ctypes.get_last_error()); sys.exit(1)
|
||||
ptr = k32.MapViewOfFile(h, 0x02, 0, 0, 0)
|
||||
if not ptr:
|
||||
print('FATAL MapViewOfFile', ctypes.get_last_error()); sys.exit(1)
|
||||
|
||||
def w32(off, val): ctypes.c_uint32.from_address(ptr + off).value = val
|
||||
def r32(off): return ctypes.c_uint32.from_address(ptr + off).value
|
||||
def energy(off, n=256):
|
||||
arr = (ctypes.c_float * n).from_address(ptr + off)
|
||||
return sum(abs(v) for v in arr)
|
||||
|
||||
OFF_MQ = 2064; OFF_MQ_COUNT = 2832
|
||||
OFF_CQ = 2836; OFF_CQ_COUNT = 11156
|
||||
OFF_HEARTBEAT = 11164
|
||||
MIDI_ENTRY = 12; CTRL_ENTRY = 1040
|
||||
OFF_RING_L = 11172; OFF_RING_R = 11172 + 8192
|
||||
OFF_RING_MASK = 11168
|
||||
|
||||
def send_ctrl(type_, arg0, arg1, channel, path=b''):
|
||||
idx = r32(OFF_CQ_COUNT)
|
||||
if idx >= 8: print('FATAL ctrl full'); sys.exit(1)
|
||||
base = ptr + OFF_CQ + idx * CTRL_ENTRY
|
||||
ctypes.c_uint32.from_address(base + 0).value = type_
|
||||
ctypes.c_uint32.from_address(base + 4).value = arg0
|
||||
ctypes.c_uint32.from_address(base + 8).value = arg1
|
||||
ctypes.c_uint32.from_address(base + 12).value = channel
|
||||
if path:
|
||||
ctypes.memmove(base + 16, path, len(path))
|
||||
ctypes.c_char.from_address(base + 16 + len(path)).value = b'\0'
|
||||
w32(OFF_CQ_COUNT, idx + 1)
|
||||
print(f'sent ctrl type={type_} arg0={arg0} arg1={arg1} ch={channel}')
|
||||
|
||||
def send_midi(cmd, channel, pitch, velocity, live=1, d2=0, d3=0):
|
||||
idx = r32(OFF_MQ_COUNT)
|
||||
if idx >= 64: print('FATAL midi full'); sys.exit(1)
|
||||
base = ptr + OFF_MQ + idx * MIDI_ENTRY
|
||||
ctypes.c_uint8.from_address(base + 0).value = cmd
|
||||
ctypes.c_uint8.from_address(base + 1).value = channel
|
||||
ctypes.c_uint8.from_address(base + 2).value = pitch
|
||||
ctypes.c_uint8.from_address(base + 3).value = velocity
|
||||
ctypes.c_uint8.from_address(base + 4).value = d2
|
||||
ctypes.c_uint8.from_address(base + 5).value = d3
|
||||
ctypes.c_uint8.from_address(base + 6).value = 1 if live else 0
|
||||
ctypes.c_uint32.from_address(base + 8).value = 0
|
||||
w32(OFF_MQ_COUNT, idx + 1)
|
||||
print(f'sent midi cmd=0x{cmd:x} ch={channel} p={pitch} v={velocity}')
|
||||
|
||||
env = dict(os.environ)
|
||||
env['SF_SHM_NAME'] = SHM_NAME
|
||||
env['SF_SAMPLE_RATE'] = '44100'
|
||||
env['SF_BLOCK_SIZE'] = '256'
|
||||
env['SF_PARENT_PID'] = str(os.getpid())
|
||||
empty_state = os.path.join(tempfile.gettempdir(), 'sf_v2test_state.json')
|
||||
open(empty_state, 'w').write('{}')
|
||||
env['SF_STATE_FILE'] = empty_state
|
||||
|
||||
bout = tempfile.NamedTemporaryFile(delete=False, suffix='.log', mode='w', encoding='utf-8')
|
||||
bout_path = bout.name
|
||||
bout.close()
|
||||
with open(bout_path, 'w', encoding='utf-8') as f:
|
||||
proc = subprocess.Popen([BRIDGE, '--shm', SHM_NAME], env=env,
|
||||
stdout=f, stderr=subprocess.STDOUT)
|
||||
print('bridge pid', proc.pid)
|
||||
time.sleep(2.5)
|
||||
|
||||
def tail_bridge(tag):
|
||||
with open(bout_path, encoding='utf-8', errors='replace') as f:
|
||||
lines = f.read().splitlines()
|
||||
print(f'--- bridge {tag} ({len(lines)} lines) ---')
|
||||
for l in lines[-50:]:
|
||||
print(l)
|
||||
|
||||
# LOAD VST2 (type enum: VST2=1)
|
||||
send_ctrl(2, 1, len(PLUGIN.encode()), 0, PLUGIN.encode())
|
||||
print('proc alive after load:', proc.poll() is None)
|
||||
time.sleep(2.0)
|
||||
tail_bridge('after LOAD')
|
||||
|
||||
# TRANSPORT PLAY so plugins gating DSP on transport start producing sound
|
||||
send_ctrl(3, 1, 0, 0)
|
||||
time.sleep(0.3)
|
||||
# CC7 volume max (some presets default to 0 volume)
|
||||
send_midi(0xB, 0, 7, 127)
|
||||
time.sleep(0.2)
|
||||
|
||||
send_midi(0x9, 0, 60, 100)
|
||||
for t in (0.5, 1.0, 2.0, 4.0):
|
||||
time.sleep(t)
|
||||
print('energy at +', t, ':', round(max(sum(abs(v) for v in (ctypes.c_float * 256).from_address(ptr + OFF_RING_L + s * 1024)) + sum(abs(v) for v in (ctypes.c_float * 256).from_address(ptr + OFF_RING_R + s * 1024)) for s in range(8)), 3))
|
||||
|
||||
mask = r32(OFF_RING_MASK) or 7
|
||||
wi = r32(4)
|
||||
slots = [(wi - i) & mask for i in range(mask + 1)]
|
||||
energies = []
|
||||
for s in slots:
|
||||
e = energy(OFF_RING_L + s * 1024) + energy(OFF_RING_R + s * 1024)
|
||||
energies.append(e)
|
||||
print('ringMask', mask, 'writeIndex', wi, 'heartbeat', r32(OFF_HEARTBEAT))
|
||||
print('slot energies (L+R):', [round(e, 3) for e in energies])
|
||||
print('max energy:', round(max(energies), 3))
|
||||
|
||||
send_midi(0x8, 0, 60, 0)
|
||||
time.sleep(0.5)
|
||||
tail_bridge('tail')
|
||||
|
||||
proc.terminate()
|
||||
time.sleep(1)
|
||||
if proc.poll() is None:
|
||||
proc.kill()
|
||||
k32.UnmapViewOfFile(ptr); k32.CloseHandle(h)
|
||||
print('--- grep Vst2Instrument in log ---')
|
||||
with open(bout_path, encoding='utf-8', errors='replace') as f:
|
||||
hits = [l.rstrip() for l in f if 'Vst2Instrument' in l]
|
||||
for l in hits[-20:]:
|
||||
print(l)
|
||||
os.remove(bout_path)
|
||||
print('done')
|
||||
@@ -0,0 +1,69 @@
|
||||
import struct
|
||||
|
||||
path = r"C:/Users/locpham/AppData/Local/Temp/sf_dumps/bridge_crash_1696.dmp"
|
||||
data = open(path, "rb").read()
|
||||
sig, ver, numstreams, sdir = struct.unpack_from("<IIIIIIQ", data, 0)[:4]
|
||||
streams = {}
|
||||
for i in range(numstreams):
|
||||
stype, datasize, rva = struct.unpack_from("<III", data, sdir + i * 12)
|
||||
streams[stype] = (rva, datasize)
|
||||
|
||||
mods = []
|
||||
rva, sz = streams[4]
|
||||
n = struct.unpack_from("<I", data, rva)[0]
|
||||
pos = rva + 4
|
||||
for i in range(n):
|
||||
base, imgsz, cksum, ts, name_rva = struct.unpack_from("<QIIII", data, pos)
|
||||
nlen = struct.unpack_from("<I", data, name_rva)[0]
|
||||
name = data[name_rva + 4:name_rva + 4 + nlen].decode("utf-16-le", "replace").rstrip("\x00")
|
||||
mods.append((base, imgsz, name))
|
||||
pos += 108
|
||||
|
||||
def mod_for(addr):
|
||||
for base, szz, name in mods:
|
||||
if base <= addr < base + szz:
|
||||
return base, name.split("\\")[-1]
|
||||
return None, None
|
||||
|
||||
# crash thread context
|
||||
rva, sz = streams[6]
|
||||
ex_tid = struct.unpack_from("<I", data, rva)[0]
|
||||
rva, sz = streams[3]
|
||||
n = struct.unpack_from("<I", data, rva)[0]
|
||||
pos = rva + 4
|
||||
ctx_rva = None
|
||||
for i in range(n):
|
||||
tid2, susp, pric, pri, teb, st_start, st_sz, st_rva, ctx_sz2, ctx_rva2 = struct.unpack_from("<IIIIQQIIII", data, pos)
|
||||
if tid2 == ex_tid:
|
||||
ctx_rva = ctx_rva2
|
||||
st_rva_used = st_rva
|
||||
st_start_used = st_start
|
||||
pos += 48
|
||||
rip = struct.unpack_from("<Q", data, ctx_rva + 0xF8)[0]
|
||||
rsp = struct.unpack_from("<Q", data, ctx_rva + 0x98)[0]
|
||||
print("crash tid", ex_tid, "RIP", hex(rip), "RSP", hex(rsp))
|
||||
|
||||
# find memory region covering RSP
|
||||
rva, sz = streams[5]
|
||||
n = struct.unpack_from("<I", data, rva)[0]
|
||||
pos = rva + 4
|
||||
region = None
|
||||
for i in range(n):
|
||||
start, msz, mrva = struct.unpack_from("<QII", data, pos)
|
||||
if start <= rsp < start + msz:
|
||||
region = (start, msz, mrva)
|
||||
pos += 16
|
||||
print("stack region", region and (hex(region[0]), hex(region[1])))
|
||||
|
||||
# walk stack: assume return addresses are qwords that fall into executable modules
|
||||
start, msz, mrva = region
|
||||
base_rsp = rsp
|
||||
print("\nstack walk from RSP:")
|
||||
prev = None
|
||||
for j in range(0, 16384 - 8, 8):
|
||||
addr = base_rsp + j
|
||||
if addr >= start + msz: break
|
||||
val = struct.unpack_from("<Q", data, mrva + (addr - start))[0]
|
||||
b, nm = mod_for(val)
|
||||
if nm and (val - b) > 0x1000: # code pointer heuristic (offset beyond header)
|
||||
print(" %016x [%04x] %016x -> %s+%x" % (addr, j, val, nm, val - b))
|
||||
Reference in New Issue
Block a user