fix(bridge): load dung VSTi theo track (V9) - bo early-return mui theo flag, load qua Tauri invoke khi co path, guard path sai (WaveObserver vs Nexus), chi mo GUI khi load OK; sync 6 ban JS + index.html ?v; kem cac fix bridge truoc (bugs 1-4, GUI restore)
This commit is contained in:
@@ -46,6 +46,9 @@ bool FluidSynthInstrument::loadSoundFontFile(const std::string& path, double sam
|
||||
fluid_settings_setint(FS_SETTINGS, "synth.verbose", 0);
|
||||
synth = new_fluid_synth(FS_SETTINGS);
|
||||
if (!synth) return false;
|
||||
// Unity baseline; auto-calibration trong InstrumentEngineManager::calibrate
|
||||
// can bang peak ve -6 dBFS cho moi engine (VST2/VST3/SF2/SFZ).
|
||||
fluid_synth_set_gain(FS_SYNTH, 1.0f);
|
||||
sfontId = fluid_synth_sfload(FS_SYNTH, path.c_str(), 1);
|
||||
if (sfontId == -1) return false;
|
||||
// Reset all channels to font preset 0 (spec §VII: bank0/prog0 piano)
|
||||
@@ -65,7 +68,12 @@ void FluidSynthInstrument::selectProgram(uint32_t channel, uint32_t bank, uint32
|
||||
if (!synth || channel >= 16) return;
|
||||
bank_[channel] = bank;
|
||||
program_[channel] = program;
|
||||
fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank, program);
|
||||
// SF2 fallback: nhieu soundfont khong co bank 128 (GM drum) — neu select
|
||||
// fail, thu lai bank 0 de khong cam (fluidsynth giu preset cu -> sai am).
|
||||
if (fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank, program) != FLUID_OK && bank != 0) {
|
||||
bank_[channel] = 0;
|
||||
fluid_synth_program_select(FS_SYNTH, channel, sfontId, 0, program);
|
||||
}
|
||||
}
|
||||
|
||||
void FluidSynthInstrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) {
|
||||
@@ -92,7 +100,10 @@ void FluidSynthInstrument::programChange(uint32_t channel, uint32_t program) {
|
||||
// Dung bank da nhan tu CC0/CC32 — bank hardcode 0 lam preset o bank != 0
|
||||
// khong duoc chon (fluid giu preset cu -> ra piano sai).
|
||||
program_[channel] = program;
|
||||
fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank_[channel], program);
|
||||
if (fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank_[channel], program) != FLUID_OK && bank_[channel] != 0) {
|
||||
bank_[channel] = 0;
|
||||
fluid_synth_program_select(FS_SYNTH, channel, sfontId, 0, program);
|
||||
}
|
||||
}
|
||||
|
||||
void FluidSynthInstrument::pitchBend(uint32_t channel, uint32_t bend14) {
|
||||
@@ -224,6 +235,9 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
|
||||
if (!loaded) return false;
|
||||
// init() AFTER load: FluidSynth creates its synth inside loadSoundFontFile.
|
||||
if (!inst->init(sampleRate, blockSize)) return false;
|
||||
// Calibrate output level (makeup) - probe render truoc khi publish vao map
|
||||
// de renderAll ap dung ngay, khong block audio thread.
|
||||
const float mk = calibrate(inst.get(), sampleRate, blockSize);
|
||||
// Replacing an existing instrument drops its voices with the old engine.
|
||||
// Load may run on a detached thread (VST3 init is slow): only the map
|
||||
// write is under the mutex so renderAll on the audio loop never stalls.
|
||||
@@ -244,6 +258,7 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
|
||||
// flows to it again. G3.3: a reload also clears the crash-mute.
|
||||
reloadingCh_[channel] = false;
|
||||
crashedCh_[channel] = false;
|
||||
makeup_[channel] = mk;
|
||||
}
|
||||
if (oldInst) {
|
||||
isBypassed_.store(true);
|
||||
@@ -271,6 +286,7 @@ void InstrumentEngineManager::unload(uint32_t channel) {
|
||||
channels_.erase(channel);
|
||||
paths_.erase(channel);
|
||||
types_.erase(channel);
|
||||
makeup_.erase(channel);
|
||||
reloadingCh_[channel] = false;
|
||||
}
|
||||
if (oldInst) {
|
||||
@@ -459,6 +475,63 @@ static bool SafeProcessChannel(INativeInstrument* inst, float* outL, float* outR
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- Auto-calibration (am luong on dinh moi engine VST2/VST3/SF2/SFZ) ---
|
||||
// Probe-render 3 notes (60/64/67, velocity 0.8) ngay sau khi load, do peak,
|
||||
// tinh makeup sao cho note don ra ~ -6 dBFS (0.5). Clamp: khong bu patch cam
|
||||
// qua muc, khong tat patch to. renderAll ap dung makeup + peak-safety @1.0
|
||||
// (chord/transient vuot 1.0 se bi keo ve 1.0 - khong clip, khong meo).
|
||||
static constexpr float kCalTargetPeak = 0.5f;
|
||||
static constexpr float kCalMinMakeup = 0.125f;
|
||||
static constexpr float kCalMaxMakeup = 8.0f;
|
||||
|
||||
float InstrumentEngineManager::calibrate(INativeInstrument* inst, double sampleRate, uint32_t blockSize) {
|
||||
if (!inst) return 1.0f;
|
||||
const uint32_t bs = blockSize ? blockSize : 512;
|
||||
const uint32_t body = (uint32_t)(sampleRate * 0.6);
|
||||
const uint32_t tail = (uint32_t)(sampleRate * 0.25);
|
||||
std::vector<float> L(bs + 64, 0.0f), R(bs + 64, 0.0f);
|
||||
const int pitches[3] = { 60, 64, 67 };
|
||||
float peak = 0.0f;
|
||||
auto renderProbe = [&](uint32_t n) {
|
||||
#ifdef _WIN32
|
||||
DWORD code = 0;
|
||||
#else
|
||||
uint32_t code = 0;
|
||||
#endif
|
||||
if (!SafeProcessChannel(inst, L.data(), R.data(), n, &code)) return false;
|
||||
for (uint32_t i = 0; i < n; ++i) {
|
||||
float a = L[i]; if (a < 0.0f) a = -a; if (a > peak) peak = a;
|
||||
float b = R[i]; if (b < 0.0f) b = -b; if (b > peak) peak = b;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
for (int p : pitches) {
|
||||
inst->noteOn(0, (uint32_t)p, 0.8f, 0);
|
||||
uint32_t off = 0;
|
||||
while (off < body) {
|
||||
uint32_t n = bs; if (off + n > body) n = body - off;
|
||||
if (!renderProbe(n)) { inst->noteOff(0, (uint32_t)p, 0); return 1.0f; }
|
||||
off += n;
|
||||
}
|
||||
inst->noteOff(0, (uint32_t)p, 0);
|
||||
off = 0;
|
||||
while (off < tail) {
|
||||
uint32_t n = bs; if (off + n > tail) n = tail - off;
|
||||
if (!renderProbe(n)) return 1.0f;
|
||||
off += n;
|
||||
}
|
||||
}
|
||||
if (peak < 1e-4f) {
|
||||
std::cerr << "[Calibrate] silent probe peak=" << peak << " makeup=1.0" << std::endl;
|
||||
return 1.0f;
|
||||
}
|
||||
float g = kCalTargetPeak / peak;
|
||||
if (g < kCalMinMakeup) g = kCalMinMakeup;
|
||||
if (g > kCalMaxMakeup) g = kCalMaxMakeup;
|
||||
std::cerr << "[Calibrate] peak=" << peak << " makeup=" << g << std::endl;
|
||||
return g;
|
||||
}
|
||||
|
||||
void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t numSamples) {
|
||||
if (isBypassed_.load()) {
|
||||
std::memset(outputL, 0, numSamples * sizeof(float));
|
||||
@@ -519,9 +592,31 @@ void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 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;
|
||||
for (uint32_t i = 0; i < numSamples; ++i) { spL[i] *= g; spR[i] *= g; }
|
||||
for (uint32_t i = 0; i < numSamples; ++i) {
|
||||
outputL[i] += spL[i];
|
||||
outputR[i] += spR[i];
|
||||
}
|
||||
}
|
||||
// Smoothed brickwall limiter on the summed mix: fast attack (~2-3 samples)
|
||||
// when the mix exceeds 1.0, slow release back to unity. Replaces the old
|
||||
// per-block hard clamp whose gain step at block boundaries caused zipper
|
||||
// crackle. limiterGain_ persists across renderAll calls.
|
||||
const float kAttack = 0.4f;
|
||||
const float kRelease = 0.0006f;
|
||||
for (uint32_t i = 0; i < numSamples; ++i) {
|
||||
float a = outputL[i] < 0.0f ? -outputL[i] : outputL[i];
|
||||
float b = outputR[i] < 0.0f ? -outputR[i] : outputR[i];
|
||||
const float m = a > b ? a : b;
|
||||
const float target = (m * limiterGain_ > 1.0f) ? (1.0f / (m + 1e-12f)) : 1.0f;
|
||||
if (target < limiterGain_)
|
||||
limiterGain_ += (target - limiterGain_) * kAttack;
|
||||
else
|
||||
limiterGain_ += (1.0f - limiterGain_) * kRelease;
|
||||
outputL[i] *= limiterGain_;
|
||||
outputR[i] *= limiterGain_;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user