G1: JUCE engine SHM chain rong POC - probe pass, khong lag them
This commit is contained in:
@@ -61,3 +61,4 @@ native_bridge/debug/shm_selfcheck.exe
|
|||||||
# Linux-only runtime artifacts (built via Docker; never committed)
|
# Linux-only runtime artifacts (built via Docker; never committed)
|
||||||
install/daw_vst_bridge
|
install/daw_vst_bridge
|
||||||
install/libsfizz.so.1
|
install/libsfizz.so.1
|
||||||
|
build_juce
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
[submodule "native_bridge/vst3sdk"]
|
[submodule "native_bridge/vst3sdk"]
|
||||||
path = native_bridge/vst3sdk
|
path = native_bridge/vst3sdk
|
||||||
url = https://github.com/steinbergmedia/vst3sdk.git
|
url = https://github.com/steinbergmedia/vst3sdk.git
|
||||||
|
[submodule "native_bridge/JUCE"]
|
||||||
|
path = native_bridge/JUCE
|
||||||
|
url = https://github.com/juce-framework/JUCE
|
||||||
|
branch = 8.0.7
|
||||||
|
shallow = true
|
||||||
|
|||||||
@@ -162,7 +162,8 @@ class FxRealtimeSession:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
def _spawn(self):
|
def _spawn(self):
|
||||||
exe = find_fx_bridge_exe()
|
juce_exe = os.getenv("SF_JUCE_FX_BRIDGE_PATH", "").strip()
|
||||||
|
exe = juce_exe or find_fx_bridge_exe()
|
||||||
if not exe:
|
if not exe:
|
||||||
raise RuntimeError("Không tìm thấy fx_vst_bridge.exe")
|
raise RuntimeError("Không tìm thấy fx_vst_bridge.exe")
|
||||||
job = {
|
job = {
|
||||||
@@ -174,8 +175,12 @@ class FxRealtimeSession:
|
|||||||
fd, self._job_path = tempfile.mkstemp(suffix=".json", prefix="fxrt_")
|
fd, self._job_path = tempfile.mkstemp(suffix=".json", prefix="fxrt_")
|
||||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||||
json.dump(job, f)
|
json.dump(job, f)
|
||||||
cmd = [exe, "--realtime-fx", self._job_path, "--shm", self.name,
|
if juce_exe:
|
||||||
"--parent", str(os.getpid())]
|
cmd = [exe, "--juce-fx", self._job_path, "--shm", self.name,
|
||||||
|
"--parent", str(os.getpid())]
|
||||||
|
else:
|
||||||
|
cmd = [exe, "--realtime-fx", self._job_path, "--shm", self.name,
|
||||||
|
"--parent", str(os.getpid())]
|
||||||
# FXRT_BRIDGE_LOG=<path>: bat stderr bridge (FxRTPerf) vao file — chan
|
# FXRT_BRIDGE_LOG=<path>: bat stderr bridge (FxRTPerf) vao file — chan
|
||||||
# doan outFull/drop; mac dinh DEVNULL nhu cu.
|
# doan outFull/drop; mac dinh DEVNULL nhu cu.
|
||||||
# V34 diag: mac dinh ghi FxRTPerf vao fxrt_bridge.log de phan tich
|
# V34 diag: mac dinh ghi FxRTPerf vao fxrt_bridge.log de phan tich
|
||||||
|
|||||||
Submodule
+1
Submodule native_bridge/JUCE added at 10a589619b
@@ -0,0 +1,50 @@
|
|||||||
|
// native_bridge/include/FxShm.h
|
||||||
|
// openShm/closeShm dùng chung giữa RealtimeFxLoop.cpp (fx_vst_bridge
|
||||||
|
// --realtime-fx) và juce_fx/JuceFxLoop.cpp (juce_fx_bridge --juce-fx).
|
||||||
|
// Windows-only (FileMapping); mở mapping có sẵn do engine (Python) tạo.
|
||||||
|
#pragma once
|
||||||
|
#ifdef _WIN32
|
||||||
|
#ifndef NOMINMAX
|
||||||
|
#define NOMINMAX
|
||||||
|
#endif
|
||||||
|
#include <windows.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace fxshm {
|
||||||
|
|
||||||
|
struct ShmView {
|
||||||
|
void* map = nullptr;
|
||||||
|
void* view = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline ShmView* openShm(const std::string& name, size_t size) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
int wlen = MultiByteToWideChar(CP_UTF8, 0, name.c_str(), -1, nullptr, 0);
|
||||||
|
std::wstring wname(wlen, L'\0');
|
||||||
|
MultiByteToWideChar(CP_UTF8, 0, name.c_str(), -1, &wname[0], wlen);
|
||||||
|
HANDLE map = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, wname.c_str());
|
||||||
|
if (!map) return nullptr;
|
||||||
|
void* view = MapViewOfFile(map, FILE_MAP_ALL_ACCESS, 0, 0, size);
|
||||||
|
if (!view) { CloseHandle(map); return nullptr; }
|
||||||
|
ShmView* v = new ShmView();
|
||||||
|
v->map = map;
|
||||||
|
v->view = view;
|
||||||
|
return v;
|
||||||
|
#else
|
||||||
|
(void)name; (void)size;
|
||||||
|
return nullptr;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void closeShm(ShmView* v) {
|
||||||
|
if (!v) return;
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (v->view) UnmapViewOfFile(v->view);
|
||||||
|
if (v->map) CloseHandle((HANDLE)v->map);
|
||||||
|
#endif
|
||||||
|
delete v;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fxshm
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# native_bridge/juce_fx/CMakeLists.txt
|
||||||
|
# G1 POC: juce_fx_bridge.exe — JUCE AudioProcessorGraph thay RealtimeFxChain
|
||||||
|
# trong SHM realtime loop. Windows only. Build riêng (không chung với
|
||||||
|
# daw_vst_bridge) để JUCE không kéo opengl/fluidsynth vào bridge cũ.
|
||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
project(juce_fx_bridge LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../JUCE/CMakeLists.txt")
|
||||||
|
message(FATAL_ERROR "JUCE submodule missing — run: git submodule update --init native_bridge/JUCE")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# JUCE framework (submodule, tag 8.0.7). add_subdirectory tạo targets
|
||||||
|
# juce::core, juce::audio_basics, juce::audio_processors, ...
|
||||||
|
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../JUCE JUCE EXCLUDE_FROM_ALL)
|
||||||
|
|
||||||
|
add_executable(juce_fx_bridge
|
||||||
|
main_juce_fx.cpp
|
||||||
|
JuceFxLoop.cpp
|
||||||
|
JuceFxEngine.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(juce_fx_bridge PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/../include
|
||||||
|
)
|
||||||
|
target_link_libraries(juce_fx_bridge PRIVATE
|
||||||
|
juce::juce_core
|
||||||
|
juce::juce_audio_basics
|
||||||
|
juce::juce_audio_processors
|
||||||
|
)
|
||||||
|
if(WIN32)
|
||||||
|
# timeBeginPeriod (winmm) — như fx_vst_bridge
|
||||||
|
target_link_libraries(juce_fx_bridge PRIVATE winmm)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// native_bridge/juce_fx/JuceFxEngine.cpp
|
||||||
|
// Implementation: AudioProcessorGraph với input/output node — chain rỗng G1.
|
||||||
|
#include "JuceFxEngine.h"
|
||||||
|
|
||||||
|
#include <juce_audio_processors/juce_audio_processors.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct JuceFxEngine::Impl {
|
||||||
|
juce::AudioProcessorGraph graph;
|
||||||
|
juce::AudioBuffer<float> buf;
|
||||||
|
uint32_t sampleRate = 0;
|
||||||
|
uint32_t blockSize = 0;
|
||||||
|
bool prepared = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
JuceFxEngine::JuceFxEngine() : impl_(std::make_unique<Impl>()) {}
|
||||||
|
|
||||||
|
JuceFxEngine::~JuceFxEngine() { shutdown(); }
|
||||||
|
|
||||||
|
void JuceFxEngine::shutdown() {
|
||||||
|
if (impl_->prepared) {
|
||||||
|
impl_->graph.releaseResources();
|
||||||
|
impl_->graph.clear();
|
||||||
|
impl_->prepared = false;
|
||||||
|
impl_->sampleRate = 0;
|
||||||
|
impl_->blockSize = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void JuceFxEngine::prepare(uint32_t sampleRate, uint32_t blockSize) {
|
||||||
|
if (impl_->prepared && impl_->sampleRate == sampleRate &&
|
||||||
|
impl_->blockSize == blockSize)
|
||||||
|
return; // không đổi — giữ graph
|
||||||
|
shutdown();
|
||||||
|
|
||||||
|
impl_->sampleRate = sampleRate;
|
||||||
|
impl_->blockSize = blockSize;
|
||||||
|
impl_->graph.setPlayConfigDetails(2, 2, (double)sampleRate, (int)blockSize);
|
||||||
|
|
||||||
|
// Chain rỗng G1: audio input -> audio output (passthrough). G2: chèn
|
||||||
|
// plugin node giữa hai node này theo fx_chain.
|
||||||
|
auto& g = impl_->graph;
|
||||||
|
const auto inNode = g.addNode(std::make_unique<
|
||||||
|
juce::AudioProcessorGraph::AudioGraphIOProcessor>(
|
||||||
|
juce::AudioProcessorGraph::AudioGraphIOProcessor::audioInputNode));
|
||||||
|
const auto outNode = g.addNode(std::make_unique<
|
||||||
|
juce::AudioProcessorGraph::AudioGraphIOProcessor>(
|
||||||
|
juce::AudioProcessorGraph::AudioGraphIOProcessor::audioOutputNode));
|
||||||
|
using NCh = juce::AudioProcessorGraph::NodeAndChannel;
|
||||||
|
using Conn = juce::AudioProcessorGraph::Connection;
|
||||||
|
for (int ch = 0; ch < 2; ++ch) {
|
||||||
|
NCh src; src.nodeID = inNode->nodeID; src.channelIndex = ch;
|
||||||
|
NCh dst; dst.nodeID = outNode->nodeID; dst.channelIndex = ch;
|
||||||
|
g.addConnection(Conn(src, dst));
|
||||||
|
}
|
||||||
|
|
||||||
|
g.prepareToPlay((double)sampleRate, (int)(blockSize * 4)); // batch tối đa 4 block
|
||||||
|
impl_->buf.setSize(2, (int)(blockSize * 4), false, false, true);
|
||||||
|
impl_->prepared = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void JuceFxEngine::process(float* inL, float* inR, uint32_t n) {
|
||||||
|
if (!impl_->prepared || n == 0) return;
|
||||||
|
auto& b = impl_->buf;
|
||||||
|
b.setSize(2, (int)n, false, false, true);
|
||||||
|
b.copyFrom(0, 0, inL, (int)n);
|
||||||
|
b.copyFrom(1, 0, inR, (int)n);
|
||||||
|
juce::MidiBuffer midi;
|
||||||
|
impl_->graph.processBlock(b, midi);
|
||||||
|
// G1 passthrough: copy kết quả về buffer đầu vào (in-place như contract).
|
||||||
|
const auto* outL = b.getReadPointer(0);
|
||||||
|
const auto* outR = b.getReadPointer(1);
|
||||||
|
std::copy(outL, outL + n, inL);
|
||||||
|
std::copy(outR, outR + n, inR);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t JuceFxEngine::latencySamples() const {
|
||||||
|
return impl_->prepared ? (uint32_t)impl_->graph.getLatencySamples() : 0u;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// native_bridge/juce_fx/JuceFxEngine.h
|
||||||
|
// JUCE AudioProcessorGraph engine (G1): đọc input ring → graph (rỗng) →
|
||||||
|
// ghi output ring. Tách khỏi loop để G2 chỉ thêm plugin vào graph.
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
class JuceFxEngine {
|
||||||
|
public:
|
||||||
|
JuceFxEngine();
|
||||||
|
~JuceFxEngine();
|
||||||
|
// Không copy.
|
||||||
|
JuceFxEngine(const JuceFxEngine&) = delete;
|
||||||
|
JuceFxEngine& operator=(const JuceFxEngine&) = delete;
|
||||||
|
|
||||||
|
// Tạo graph mới (input node -> output node, passthrough), prepareToPlay.
|
||||||
|
// sampleRate/blockSize đổi → teardown graph cũ + prepare lại (validate
|
||||||
|
// header G1). blockSize là kích thước mỗi SHM block (256).
|
||||||
|
void prepare(uint32_t sampleRate, uint32_t blockSize);
|
||||||
|
// Audio thread: xử lý n mẫu stereo in-place (n <= blockSize * FXRT_IN_SLOTS).
|
||||||
|
void process(float* inL, float* inR, uint32_t n);
|
||||||
|
// Graph latency samples (G1 chain rỗng = 0; G2 = getLatencySamples thật).
|
||||||
|
uint32_t latencySamples() const;
|
||||||
|
void shutdown();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Impl;
|
||||||
|
std::unique_ptr<Impl> impl_;
|
||||||
|
};
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
// native_bridge/juce_fx/JuceFxLoop.cpp
|
||||||
|
// juce_fx_bridge --juce-fx <job.json> --shm <name> [--parent <pid>]
|
||||||
|
// G1 POC: vòng lặp SHM giống RealtimeFxLoop.cpp nhưng xử lý qua JuceFxEngine
|
||||||
|
// (JUCE AudioProcessorGraph chain rỗng) thay vì RealtimeFxChain. Job format
|
||||||
|
// giữ nguyên {sample_rate, block_size, fx_chain} — G1 bỏ qua fx_chain.
|
||||||
|
// Validate header mỗi iteration: sampleRate/blockSize đổi giữa chừng →
|
||||||
|
// teardown + prepareToPlay lại + report latency mới qua FxLatReport.
|
||||||
|
#include "JuceFxEngine.h"
|
||||||
|
#include "FxRealtimeIPC.h"
|
||||||
|
#include "FxShm.h"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#ifndef NOMINMAX
|
||||||
|
#define NOMINMAX
|
||||||
|
#endif
|
||||||
|
#include <windows.h>
|
||||||
|
#include <mmsystem.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
static bool parentAlive(uint32_t pid) {
|
||||||
|
if (pid == 0) return true;
|
||||||
|
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
|
||||||
|
if (!h) return false;
|
||||||
|
CloseHandle(h);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sleepMs(uint32_t ms) { Sleep(ms); }
|
||||||
|
|
||||||
|
std::string readFile(const std::string& path) {
|
||||||
|
std::ifstream f(path, std::ios::binary);
|
||||||
|
return std::string((std::istreambuf_iterator<char>(f)),
|
||||||
|
std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
|
||||||
|
double jsonNumber(const std::string& s, const std::string& key, double def) {
|
||||||
|
std::string k = "\"" + key + "\"";
|
||||||
|
size_t p = s.find(k);
|
||||||
|
if (p == std::string::npos) return def;
|
||||||
|
p += k.size();
|
||||||
|
while (p < s.size() && (s[p] == ':' || s[p] == ' ' || s[p] == '\t' ||
|
||||||
|
s[p] == '\r' || s[p] == '\n'))
|
||||||
|
++p;
|
||||||
|
size_t e = p;
|
||||||
|
while (e < s.size() && (std::isdigit((unsigned char)s[e]) || s[e] == '.' ||
|
||||||
|
s[e] == '-' || s[e] == '+' || s[e] == 'e' ||
|
||||||
|
s[e] == 'E'))
|
||||||
|
++e;
|
||||||
|
if (e == p) return def;
|
||||||
|
try { return std::stod(s.substr(p, e - p)); } catch (...) { return def; }
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName,
|
||||||
|
uint32_t parentPid) {
|
||||||
|
std::cerr << "[JuceFxLoop] start job=" << jobPath << " shm=" << shmName
|
||||||
|
<< " parent=" << parentPid << std::endl;
|
||||||
|
const std::string job = readFile(jobPath);
|
||||||
|
if (job.empty()) {
|
||||||
|
std::cerr << "[JuceFxLoop] cannot read job file" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const double srD = jsonNumber(job, "sample_rate", 44100.0);
|
||||||
|
const uint32_t sampleRate = (uint32_t)srD;
|
||||||
|
const double blkD = jsonNumber(job, "block_size", (double)FXRT_BLOCK);
|
||||||
|
const uint32_t block = (uint32_t)std::max<double>(32.0, std::min<double>(blkD, (double)FXRT_BLOCK));
|
||||||
|
// G1: fx_chain bỏ qua (chain rỗng). G2: đọc fx_chain -> build graph.
|
||||||
|
|
||||||
|
fxshm::ShmView* v = fxshm::openShm(shmName, sizeof(FxRealtimeIPC));
|
||||||
|
if (!v) {
|
||||||
|
std::cerr << "[JuceFxLoop] cannot open SHM: " << shmName
|
||||||
|
<< " (engine phải tạo trước)" << std::endl;
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
auto* ipc = static_cast<FxRealtimeIPC*>(v->view);
|
||||||
|
|
||||||
|
for (int i = 0; i < 200 && ipc->h.magic != FXRT_MAGIC; ++i) sleepMs(10);
|
||||||
|
if (ipc->h.magic != FXRT_MAGIC) {
|
||||||
|
std::cerr << "[JuceFxLoop] SHM magic mismatch (engine chưa init?)" << std::endl;
|
||||||
|
fxshm::closeShm(v);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (ipc->h.inSlots != FXRT_IN_SLOTS || ipc->h.outSlots != FXRT_OUT_SLOTS) {
|
||||||
|
std::cerr << "[JuceFxLoop] slot count mismatch" << std::endl;
|
||||||
|
fxshm::closeShm(v);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
timeBeginPeriod(1);
|
||||||
|
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Engine: prepare graph (chain rỗng). sr/block từ job; header là nguồn
|
||||||
|
// thật (engine ghi lúc tạo SHM) — nếu khác, lấy header.
|
||||||
|
uint32_t curSr = sampleRate;
|
||||||
|
uint32_t curBlock = block;
|
||||||
|
JuceFxEngine engine;
|
||||||
|
if (ipc->h.sampleRate) curSr = ipc->h.sampleRate;
|
||||||
|
if (ipc->h.blockSize) curBlock = ipc->h.blockSize;
|
||||||
|
engine.prepare(curSr, curBlock);
|
||||||
|
std::cerr << "[JuceFxLoop] prepared sr=" << curSr << " block=" << curBlock
|
||||||
|
<< " latency=" << engine.latencySamples() << std::endl;
|
||||||
|
|
||||||
|
// Report latency ban đầu (slot 0 = tổng chain).
|
||||||
|
{
|
||||||
|
const uint32_t ls = ipc->h.latWrite & (FXRT_LAT_SLOTS - 1);
|
||||||
|
ipc->lat[ls].slot = 0;
|
||||||
|
ipc->lat[ls].samples = engine.latencySamples();
|
||||||
|
MemoryBarrier();
|
||||||
|
ipc->h.latWrite++;
|
||||||
|
}
|
||||||
|
ipc->h.state = FXRT_STATE_READY;
|
||||||
|
std::cerr << "[JuceFxLoop] ready" << std::endl;
|
||||||
|
|
||||||
|
std::thread hb([&]() {
|
||||||
|
while (ipc->h.running && ipc->h.state == FXRT_STATE_READY) {
|
||||||
|
ipc->h.heartbeat++;
|
||||||
|
sleepMs(100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const uint32_t n = curBlock;
|
||||||
|
const uint32_t inMask = ipc->h.inSlots - 1;
|
||||||
|
const uint32_t outMask = ipc->h.outSlots - 1;
|
||||||
|
float L[FXRT_BLOCK * FXRT_IN_SLOTS], R[FXRT_BLOCK * FXRT_IN_SLOTS];
|
||||||
|
uint64_t processed = 0;
|
||||||
|
uint64_t perfIter = 0, perfProc = 0, perfIdle = 0, perfOutFull = 0;
|
||||||
|
double perfProcSum = 0.0, perfProcMax = 0.0;
|
||||||
|
double perfLoopSum = 0.0, perfLoopMax = 0.0;
|
||||||
|
uint32_t perfTakeSum = 0;
|
||||||
|
auto perfT0 = std::chrono::steady_clock::now();
|
||||||
|
auto perfRunStart = perfT0;
|
||||||
|
|
||||||
|
while (ipc->h.running) {
|
||||||
|
if (parentPid && !parentAlive(parentPid)) {
|
||||||
|
std::cerr << "[JuceFxLoop] parent gone — exiting" << std::endl;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Validate header mỗi iteration: sampleRate/blockSize đổi giữa chừng
|
||||||
|
// (đổi thiết bị audio / session mới khác rate) → teardown graph +
|
||||||
|
// prepareToPlay lại + report latency mới. WebAudio sampleRate bất
|
||||||
|
// biến — check này phòng header bị ghi lại.
|
||||||
|
if (ipc->h.sampleRate && (ipc->h.sampleRate != curSr ||
|
||||||
|
ipc->h.blockSize != curBlock)) {
|
||||||
|
curSr = ipc->h.sampleRate;
|
||||||
|
curBlock = ipc->h.blockSize;
|
||||||
|
engine.prepare(curSr, curBlock);
|
||||||
|
const uint32_t ls = ipc->h.latWrite & (FXRT_LAT_SLOTS - 1);
|
||||||
|
ipc->lat[ls].slot = 0;
|
||||||
|
ipc->lat[ls].samples = engine.latencySamples();
|
||||||
|
MemoryBarrier();
|
||||||
|
ipc->h.latWrite++;
|
||||||
|
std::cerr << "[JuceFxLoop] re-prepared sr=" << curSr
|
||||||
|
<< " block=" << curBlock
|
||||||
|
<< " latency=" << engine.latencySamples() << std::endl;
|
||||||
|
}
|
||||||
|
// OUT-ring backpressure (giữ nguyên cơ chế RealtimeFxLoop — pointer
|
||||||
|
// collision → torn frame nếu ghi đè slot chưa đọc).
|
||||||
|
const uint32_t outFree = ipc->h.outSlots - (ipc->h.outWrite - ipc->h.outRead);
|
||||||
|
if (outFree < std::min<uint32_t>(ipc->h.inWrite - ipc->h.inRead, FXRT_IN_SLOTS)) {
|
||||||
|
++perfOutFull;
|
||||||
|
std::this_thread::sleep_until(std::chrono::steady_clock::now() +
|
||||||
|
std::chrono::milliseconds(1));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const uint32_t avail = ipc->h.inWrite - ipc->h.inRead;
|
||||||
|
if (avail == 0) {
|
||||||
|
++perfIdle;
|
||||||
|
std::this_thread::sleep_until(std::chrono::steady_clock::now() +
|
||||||
|
std::chrono::milliseconds(1));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const uint32_t take = std::min<uint32_t>(avail, FXRT_IN_SLOTS);
|
||||||
|
uint32_t off = 0;
|
||||||
|
for (uint32_t i = 0; i < take; ++i) {
|
||||||
|
const uint32_t slot = ipc->h.inRead & inMask;
|
||||||
|
std::memcpy(L + off, ipc->inL[slot], n * sizeof(float));
|
||||||
|
std::memcpy(R + off, ipc->inR[slot], n * sizeof(float));
|
||||||
|
ipc->h.inRead++;
|
||||||
|
off += n;
|
||||||
|
}
|
||||||
|
const auto perfP0 = std::chrono::steady_clock::now();
|
||||||
|
engine.process(L, R, off); // chain rỗng = passthrough
|
||||||
|
const auto perfP1 = std::chrono::steady_clock::now();
|
||||||
|
const double perfProcMs = std::chrono::duration<double, std::milli>(perfP1 - perfP0).count();
|
||||||
|
++perfProc; perfProcSum += perfProcMs; perfTakeSum += take;
|
||||||
|
if (perfProcMs > perfProcMax) perfProcMax = perfProcMs;
|
||||||
|
for (uint32_t i = 0, o = 0; i < take; ++i, o += n) {
|
||||||
|
const uint32_t oslot = ipc->h.outWrite & outMask;
|
||||||
|
std::memcpy(ipc->outL[oslot], L + o, n * sizeof(float));
|
||||||
|
std::memcpy(ipc->outR[oslot], R + o, n * sizeof(float));
|
||||||
|
MemoryBarrier();
|
||||||
|
ipc->h.outWrite++;
|
||||||
|
++processed;
|
||||||
|
}
|
||||||
|
const auto perfIterT1 = std::chrono::steady_clock::now();
|
||||||
|
const double perfLoopMs = std::chrono::duration<double, std::milli>(perfIterT1 - perfT0).count();
|
||||||
|
perfT0 = perfIterT1; ++perfIter;
|
||||||
|
perfLoopSum += perfLoopMs; if (perfLoopMs > perfLoopMax) perfLoopMax = perfLoopMs;
|
||||||
|
if (perfIter % 100 == 0) {
|
||||||
|
const double runSec = std::chrono::duration<double>(std::chrono::steady_clock::now() - perfRunStart).count();
|
||||||
|
std::cerr << "[JuceFxPerf] iter=" << perfIter
|
||||||
|
<< " procN=" << perfProc
|
||||||
|
<< " procAvg=" << (perfProc ? perfProcSum / perfProc : 0.0) << "ms"
|
||||||
|
<< " procMax=" << perfProcMax << "ms"
|
||||||
|
<< " loopAvg=" << (perfIter ? perfLoopSum / perfIter : 0.0) << "ms"
|
||||||
|
<< " loopMax=" << perfLoopMax << "ms"
|
||||||
|
<< " idle=" << perfIdle
|
||||||
|
<< " outFull=" << perfOutFull
|
||||||
|
<< " takeAvg=" << (perfProc ? (double)perfTakeSum / perfProc : 0.0)
|
||||||
|
<< " procBlocks=" << processed
|
||||||
|
<< " rate=" << (runSec > 0 ? processed / runSec : 0.0) << "blk/s"
|
||||||
|
<< " outDepth=" << (ipc->h.outWrite - ipc->h.outRead)
|
||||||
|
<< std::endl;
|
||||||
|
perfProc = 0; perfProcSum = 0.0; perfTakeSum = 0; perfIdle = 0; perfOutFull = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ipc->h.state = FXRT_STATE_STARTING;
|
||||||
|
hb.join();
|
||||||
|
engine.shutdown();
|
||||||
|
fxshm::closeShm(v);
|
||||||
|
#ifdef _WIN32
|
||||||
|
timeEndPeriod(1);
|
||||||
|
#endif
|
||||||
|
std::cerr << "[JuceFxLoop] exit processed=" << processed << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// native_bridge/juce_fx/JuceFxLoop.h
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// juce_fx_bridge --juce-fx <job.json> --shm <name> [--parent <pid>]
|
||||||
|
int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName,
|
||||||
|
uint32_t parentPid);
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// native_bridge/juce_fx/main_juce_fx.cpp
|
||||||
|
// Entry point của juce_fx_bridge.exe — G1 POC: JUCE AudioProcessorGraph
|
||||||
|
// thay RealtimeFxChain trong SHM realtime loop. Windows only.
|
||||||
|
// --juce-fx <job.json> --shm <name> [--parent <pid>]
|
||||||
|
#include "JuceFxLoop.h"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
static LONG WINAPI JuceCrashDumpHandler(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] juce_fx_bridge exception code=0x" << std::hex
|
||||||
|
<< (er ? er->ExceptionCode : 0) << " addr=0x"
|
||||||
|
<< (er ? (void*)er->ExceptionAddress : nullptr) << std::dec << std::endl;
|
||||||
|
return EXCEPTION_CONTINUE_SEARCH;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
SetUnhandledExceptionFilter(JuceCrashDumpHandler);
|
||||||
|
#endif
|
||||||
|
std::string mode, jobPath, shmName;
|
||||||
|
uint32_t parentPid = 0;
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
const std::string a = argv[i];
|
||||||
|
if (a == "--juce-fx" && i + 1 < argc) {
|
||||||
|
mode = a;
|
||||||
|
jobPath = argv[++i];
|
||||||
|
} else if (a == "--shm" && i + 1 < argc) {
|
||||||
|
shmName = argv[++i];
|
||||||
|
} else if (a == "--parent" && i + 1 < argc) {
|
||||||
|
parentPid = (uint32_t)std::strtoul(argv[++i], nullptr, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mode != "--juce-fx" || jobPath.empty() || shmName.empty()) {
|
||||||
|
std::cerr << "usage: juce_fx_bridge --juce-fx <job.json> --shm <name> "
|
||||||
|
"[--parent <pid>]" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return run_juce_fx_loop(jobPath, shmName, parentPid);
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
// nhận JSON mảng {path,bypass,preset_b64} (type/name bỏ qua).
|
// nhận JSON mảng {path,bypass,preset_b64} (type/name bỏ qua).
|
||||||
#include "RenderFxJob.h"
|
#include "RenderFxJob.h"
|
||||||
#include "FxRealtimeIPC.h"
|
#include "FxRealtimeIPC.h"
|
||||||
|
#include "FxShm.h"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#ifndef NOMINMAX
|
#ifndef NOMINMAX
|
||||||
@@ -31,32 +32,6 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
struct ShmView {
|
|
||||||
void* map = nullptr;
|
|
||||||
void* view = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
ShmView* openShm(const std::string& name, size_t size) {
|
|
||||||
int wlen = MultiByteToWideChar(CP_UTF8, 0, name.c_str(), -1, nullptr, 0);
|
|
||||||
std::wstring wname(wlen, L'\0');
|
|
||||||
MultiByteToWideChar(CP_UTF8, 0, name.c_str(), -1, &wname[0], wlen);
|
|
||||||
HANDLE map = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, wname.c_str());
|
|
||||||
if (!map) return nullptr;
|
|
||||||
void* view = MapViewOfFile(map, FILE_MAP_ALL_ACCESS, 0, 0, size);
|
|
||||||
if (!view) { CloseHandle(map); return nullptr; }
|
|
||||||
ShmView* v = new ShmView();
|
|
||||||
v->map = map;
|
|
||||||
v->view = view;
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
void closeShm(ShmView* v) {
|
|
||||||
if (!v) return;
|
|
||||||
if (v->view) UnmapViewOfFile(v->view);
|
|
||||||
if (v->map) CloseHandle((HANDLE)v->map);
|
|
||||||
delete v;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool parentAlive(uint32_t pid) {
|
static bool parentAlive(uint32_t pid) {
|
||||||
if (pid == 0) return true;
|
if (pid == 0) return true;
|
||||||
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
|
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
|
||||||
@@ -137,7 +112,7 @@ int run_realtime_fx_loop(const std::string& jobPath, const std::string& shmName,
|
|||||||
RealtimeFxChain chain;
|
RealtimeFxChain chain;
|
||||||
chain.setChain(chainJson, sampleRate, (int32_t)batchN);
|
chain.setChain(chainJson, sampleRate, (int32_t)batchN);
|
||||||
|
|
||||||
ShmView* v = openShm(shmName, sizeof(FxRealtimeIPC));
|
fxshm::ShmView* v = fxshm::openShm(shmName, sizeof(FxRealtimeIPC));
|
||||||
if (!v) {
|
if (!v) {
|
||||||
std::cerr << "[RealtimeFxLoop] cannot open SHM: " << shmName
|
std::cerr << "[RealtimeFxLoop] cannot open SHM: " << shmName
|
||||||
<< " (engine phải tạo trước)" << std::endl;
|
<< " (engine phải tạo trước)" << std::endl;
|
||||||
@@ -149,12 +124,12 @@ int run_realtime_fx_loop(const std::string& jobPath, const std::string& shmName,
|
|||||||
for (int i = 0; i < 200 && ipc->h.magic != FXRT_MAGIC; ++i) sleepMs(10);
|
for (int i = 0; i < 200 && ipc->h.magic != FXRT_MAGIC; ++i) sleepMs(10);
|
||||||
if (ipc->h.magic != FXRT_MAGIC) {
|
if (ipc->h.magic != FXRT_MAGIC) {
|
||||||
std::cerr << "[RealtimeFxLoop] SHM magic mismatch (engine chưa init?)" << std::endl;
|
std::cerr << "[RealtimeFxLoop] SHM magic mismatch (engine chưa init?)" << std::endl;
|
||||||
closeShm(v);
|
fxshm::closeShm(v);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
if (ipc->h.inSlots != FXRT_IN_SLOTS || ipc->h.outSlots != FXRT_OUT_SLOTS) {
|
if (ipc->h.inSlots != FXRT_IN_SLOTS || ipc->h.outSlots != FXRT_OUT_SLOTS) {
|
||||||
std::cerr << "[RealtimeFxLoop] slot count mismatch" << std::endl;
|
std::cerr << "[RealtimeFxLoop] slot count mismatch" << std::endl;
|
||||||
closeShm(v);
|
fxshm::closeShm(v);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
// Windows timer resolution 1ms — bỏ granularity Sleep 15.6ms cho toàn
|
// Windows timer resolution 1ms — bỏ granularity Sleep 15.6ms cho toàn
|
||||||
@@ -359,7 +334,7 @@ int run_realtime_fx_loop(const std::string& jobPath, const std::string& shmName,
|
|||||||
}
|
}
|
||||||
ipc->h.state = FXRT_STATE_STARTING; // đã dừng
|
ipc->h.state = FXRT_STATE_STARTING; // đã dừng
|
||||||
hb.join();
|
hb.join();
|
||||||
closeShm(v);
|
fxshm::closeShm(v);
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
timeEndPeriod(1);
|
timeEndPeriod(1);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user