7c22ae1f02
- 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
215 lines
9.4 KiB
Python
215 lines
9.4 KiB
Python
# 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')
|