T13: NativeMixer mirror server pipeline (track gain/pan constant-power + brickwall limiter tanh, wired VST2/VST3 bridge, golden test 0.0001dB)
This commit is contained in:
@@ -32,7 +32,7 @@ target_compile_definitions(vst3_host_bridge PRIVATE
|
|||||||
NOMINMAX
|
NOMINMAX
|
||||||
WIN32_LEAN_AND_MEAN
|
WIN32_LEAN_AND_MEAN
|
||||||
)
|
)
|
||||||
target_link_libraries(vst3_host_bridge PRIVATE audio_engine)
|
target_link_libraries(vst3_host_bridge PRIVATE audio_engine native_mixer)
|
||||||
|
|
||||||
# Native audio loop (T12): SPSC + WASAPI — dùng chung cho cả 2 bridge.
|
# Native audio loop (T12): SPSC + WASAPI — dùng chung cho cả 2 bridge.
|
||||||
add_library(audio_engine STATIC
|
add_library(audio_engine STATIC
|
||||||
@@ -47,6 +47,12 @@ target_compile_definitions(audio_engine PRIVATE
|
|||||||
)
|
)
|
||||||
target_link_libraries(audio_engine PUBLIC ole32 avrt)
|
target_link_libraries(audio_engine PUBLIC ole32 avrt)
|
||||||
|
|
||||||
|
# NativeMixer (T13): track gain/pan + master brickwall limiter — mirror server.
|
||||||
|
add_library(native_mixer STATIC
|
||||||
|
NativeMixer.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(native_mixer PUBLIC native_host)
|
||||||
|
|
||||||
# VST2 host bridge — chỉ cần vestige.h clean-room, không cần SDK.
|
# VST2 host bridge — chỉ cần vestige.h clean-room, không cần SDK.
|
||||||
add_library(vst2_host_bridge SHARED
|
add_library(vst2_host_bridge SHARED
|
||||||
VST2AudioEngine.cpp
|
VST2AudioEngine.cpp
|
||||||
@@ -57,4 +63,4 @@ target_compile_definitions(vst2_host_bridge PRIVATE
|
|||||||
NOMINMAX
|
NOMINMAX
|
||||||
WIN32_LEAN_AND_MEAN
|
WIN32_LEAN_AND_MEAN
|
||||||
)
|
)
|
||||||
target_link_libraries(vst2_host_bridge PRIVATE audio_engine)
|
target_link_libraries(vst2_host_bridge PRIVATE audio_engine native_mixer)
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// NativeMixer.cpp — xem NativeMixer.h cho cong thuc mirror.
|
||||||
|
#include "NativeMixer.h"
|
||||||
|
|
||||||
|
namespace sonicforge {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Mirror _clamp(v, lo, hi, default) cua mastering_engine.py: NaN -> default.
|
||||||
|
float clampDb (float v, float lo, float hi, float def)
|
||||||
|
{
|
||||||
|
if (!(v == v)) // NaN
|
||||||
|
return def;
|
||||||
|
if (v < lo)
|
||||||
|
return lo;
|
||||||
|
if (v > hi)
|
||||||
|
return hi;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TrackGainPan TrackGainPan::fromDbPan (float gainDb, float pan)
|
||||||
|
{
|
||||||
|
TrackGainPan t;
|
||||||
|
t.gainLin = std::pow (10.0f, gainDb / 20.0f);
|
||||||
|
float p = pan;
|
||||||
|
if (p < -1.0f)
|
||||||
|
p = -1.0f;
|
||||||
|
else if (p > 1.0f)
|
||||||
|
p = 1.0f;
|
||||||
|
if (p == 0.0f)
|
||||||
|
{
|
||||||
|
t.panL = 1.0f;
|
||||||
|
t.panR = 1.0f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const float theta = ((p + 1.0f) / 2.0f) * (3.14159265358979323846f / 2.0f);
|
||||||
|
t.panL = std::cos (theta);
|
||||||
|
t.panR = std::sin (theta);
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NativeMixer::setLimiter (bool active, float thresholdDb)
|
||||||
|
{
|
||||||
|
m_limActive = active;
|
||||||
|
if (!active)
|
||||||
|
{
|
||||||
|
m_limK = 1.0f;
|
||||||
|
m_limTk = 1.0f;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const float t = clampDb (thresholdDb, -24.0f, 0.0f, -1.0f);
|
||||||
|
const float tLin = std::pow (10.0f, t / 20.0f);
|
||||||
|
m_limK = 1.0f / (tLin > 0.02f ? tLin : 0.02f);
|
||||||
|
m_limTk = std::tanh (m_limK);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NativeMixer::processInPlace (float* outL, float* outR, int32 frames) const
|
||||||
|
{
|
||||||
|
if (!outL || !outR || frames <= 0)
|
||||||
|
return;
|
||||||
|
const float gl = m_track.gainLin * m_track.panL;
|
||||||
|
const float gr = m_track.gainLin * m_track.panR;
|
||||||
|
for (int32 i = 0; i < frames; ++i)
|
||||||
|
{
|
||||||
|
float l = outL[i] * gl;
|
||||||
|
float r = outR[i] * gr;
|
||||||
|
if (m_limActive)
|
||||||
|
{
|
||||||
|
l = std::tanh (l * m_limK) / m_limTk;
|
||||||
|
r = std::tanh (r * m_limK) / m_limTk;
|
||||||
|
// Hard clip [-1,1] — mirror render_project sau apply_mastering
|
||||||
|
// (chi khi mastering on; limiter off => khong clip).
|
||||||
|
if (l > 1.0f)
|
||||||
|
l = 1.0f;
|
||||||
|
else if (l < -1.0f)
|
||||||
|
l = -1.0f;
|
||||||
|
if (r > 1.0f)
|
||||||
|
r = 1.0f;
|
||||||
|
else if (r < -1.0f)
|
||||||
|
r = -1.0f;
|
||||||
|
}
|
||||||
|
outL[i] = l;
|
||||||
|
outR[i] = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sonicforge
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// NativeMixer.h — track gain/pan + master brickwall limiter (T13).
|
||||||
|
//
|
||||||
|
// MIRROR server render pipeline (`app/core/render_engine.py` track volume/pan
|
||||||
|
// + `app/core/mastering_engine.py` _apply_limiter):
|
||||||
|
// - track gain: 10^(dB/20)
|
||||||
|
// - pan constant-power: theta = ((pan+1)/2)*pi/2, L *= cos(theta), R *= sin(theta)
|
||||||
|
// (pan == 0 -> unity, khong nhan — dung nhu render_engine.py)
|
||||||
|
// - master: tong cac track (processInPlace ap dung gain/pan roi limiter)
|
||||||
|
// - brickwall limiter: t_lin = 10^(thresholdDb/20) (clamp -24..0, default -1);
|
||||||
|
// k = 1/max(0.02, t_lin); tk = tanh(k); out = tanh(x*k)/tk; roi hard clip [-1,1]
|
||||||
|
// (clip giong render_project sau apply_mastering).
|
||||||
|
//
|
||||||
|
// Audio thread an toan: chi setTrack/setLimiter tu UI thread truoc khi
|
||||||
|
// AudioStart; processInPlace doc state da set, khong lock, khong cap phat.
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace sonicforge {
|
||||||
|
|
||||||
|
using int32 = int32_t;
|
||||||
|
|
||||||
|
struct TrackGainPan
|
||||||
|
{
|
||||||
|
float gainLin = 1.0f; // 10^(dB/20)
|
||||||
|
float panL = 1.0f; // cos(theta)
|
||||||
|
float panR = 1.0f; // sin(theta)
|
||||||
|
|
||||||
|
static TrackGainPan fromDbPan (float gainDb, float pan);
|
||||||
|
};
|
||||||
|
|
||||||
|
class NativeMixer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// gainDb: -inf..+inf dB; pan: -1..+1 (clamp). Mirrors render_engine.py.
|
||||||
|
void setTrack (const TrackGainPan& t) { m_track = t; }
|
||||||
|
|
||||||
|
// thresholdDb clamp -24..0, NaN -> -1 (mirror _clamp trong mastering_engine).
|
||||||
|
void setLimiter (bool active, float thresholdDb);
|
||||||
|
|
||||||
|
// Ap track gain/pan vao buffer roi (neu active) limiter + hard clip [-1,1]
|
||||||
|
// ngay tai buffer (in-place). frames > 0.
|
||||||
|
void processInPlace (float* outL, float* outR, int32 frames) const;
|
||||||
|
|
||||||
|
bool limiterActive () const { return m_limActive; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
TrackGainPan m_track;
|
||||||
|
bool m_limActive = false;
|
||||||
|
float m_limK = 1.0f; // 1/max(0.02, t_lin)
|
||||||
|
float m_limTk = 1.0f; // tanh(k)
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sonicforge
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
#include "vestige.h"
|
#include "vestige.h"
|
||||||
#include "AudioEngine.h"
|
#include "AudioEngine.h"
|
||||||
|
#include "NativeMixer.h"
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ struct Instance
|
|||||||
void* paramCbUserdata = nullptr;
|
void* paramCbUserdata = nullptr;
|
||||||
sonicforge::AudioEngine audio; // T12: native audio loop (SPSC + WASAPI)
|
sonicforge::AudioEngine audio; // T12: native audio loop (SPSC + WASAPI)
|
||||||
std::atomic<bool> audioRunning {false};
|
std::atomic<bool> audioRunning {false};
|
||||||
|
sonicforge::NativeMixer mixer; // T13: track gain/pan + master limiter
|
||||||
};
|
};
|
||||||
|
|
||||||
std::mutex g_mutex;
|
std::mutex g_mutex;
|
||||||
@@ -163,6 +165,7 @@ bool vst2AudioProcess (const sonicforge::MidiEvent* events, int32 eventCount,
|
|||||||
{
|
{
|
||||||
float* outs[2] = { outL, outR };
|
float* outs[2] = { outL, outR };
|
||||||
inst->effect->processReplacing (inst->effect, nullptr, outs, frames);
|
inst->effect->processReplacing (inst->effect, nullptr, outs, frames);
|
||||||
|
inst->mixer.processInPlace (outL, outR, frames); // T13: gain/pan + limiter
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -602,4 +605,26 @@ __declspec (dllexport) int32 SF_VST2_AudioBlocks (int32 handle)
|
|||||||
return it->second->audio.blocksRendered ();
|
return it->second->audio.blocksRendered ();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// T13: track gain/pan + master limiter (NativeMixer). Set truoc AudioStart;
|
||||||
|
// audio thread doc state nay (khong lock).
|
||||||
|
__declspec (dllexport) int32 SF_VST2_SetTrackGainPan (int32 handle, float gainDb, float pan)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock (g_mutex);
|
||||||
|
auto it = g_instances.find (handle);
|
||||||
|
if (it == g_instances.end ())
|
||||||
|
return -1;
|
||||||
|
it->second->mixer.setTrack (sonicforge::TrackGainPan::fromDbPan (gainDb, pan));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
__declspec (dllexport) int32 SF_VST2_SetMasterLimiter (int32 handle, int32 active, float thresholdDb)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock (g_mutex);
|
||||||
|
auto it = g_instances.find (handle);
|
||||||
|
if (it == g_instances.end ())
|
||||||
|
return -1;
|
||||||
|
it->second->mixer.setLimiter (active != 0, thresholdDb);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// native_mixer_test.cpp — golden test driver cho NativeMixer (T13).
|
||||||
|
//
|
||||||
|
// Đọc input raw float32 interleaved stereo, áp NativeMixer (track gain/pan +
|
||||||
|
// master limiter) theo argv, ghi output raw float32 interleaved. Python
|
||||||
|
// (test_native_mixer_golden.py) so RMS/peak với reference server pipeline.
|
||||||
|
//
|
||||||
|
// Usage: native_mixer_test.exe <in.raw> <out.raw> <gainDb> <pan> <limActive> <thresholdDb>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "NativeMixer.h"
|
||||||
|
|
||||||
|
using sonicforge::int32;
|
||||||
|
|
||||||
|
int main (int argc, char** argv)
|
||||||
|
{
|
||||||
|
if (argc < 7)
|
||||||
|
{
|
||||||
|
std::fprintf (stderr, "usage: native_mixer_test in.raw out.raw gainDb pan limActive thresholdDb\n");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
const char* inPath = argv[1];
|
||||||
|
const char* outPath = argv[2];
|
||||||
|
const float gainDb = static_cast<float> (std::atof (argv[3]));
|
||||||
|
const float pan = static_cast<float> (std::atof (argv[4]));
|
||||||
|
const int32 limActive = std::atoi (argv[5]);
|
||||||
|
const float thresholdDb = static_cast<float> (std::atof (argv[6]));
|
||||||
|
|
||||||
|
FILE* f = std::fopen (inPath, "rb");
|
||||||
|
if (!f)
|
||||||
|
{
|
||||||
|
std::fprintf (stderr, "cannot open %s\n", inPath);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
std::fseek (f, 0, SEEK_END);
|
||||||
|
const long nBytes = std::ftell (f);
|
||||||
|
std::fseek (f, 0, SEEK_SET);
|
||||||
|
if (nBytes <= 0 || (nBytes % 8) != 0)
|
||||||
|
{
|
||||||
|
std::fprintf (stderr, "bad input size %ld\n", nBytes);
|
||||||
|
std::fclose (f);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
std::vector<float> buf (static_cast<size_t> (nBytes) / sizeof (float));
|
||||||
|
if (std::fread (buf.data (), sizeof (float), buf.size (), f) != buf.size ())
|
||||||
|
{
|
||||||
|
std::fprintf (stderr, "short read\n");
|
||||||
|
std::fclose (f);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
std::fclose (f);
|
||||||
|
|
||||||
|
const int32 frames = static_cast<int32> (buf.size () / 2);
|
||||||
|
std::vector<float> inL (static_cast<size_t> (frames));
|
||||||
|
std::vector<float> inR (static_cast<size_t> (frames));
|
||||||
|
for (int32 i = 0; i < frames; ++i)
|
||||||
|
{
|
||||||
|
inL[static_cast<size_t> (i)] = buf[static_cast<size_t> (2 * i)];
|
||||||
|
inR[static_cast<size_t> (i)] = buf[static_cast<size_t> (2 * i + 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
sonicforge::NativeMixer mixer;
|
||||||
|
mixer.setTrack (sonicforge::TrackGainPan::fromDbPan (gainDb, pan));
|
||||||
|
mixer.setLimiter (limActive != 0, thresholdDb);
|
||||||
|
mixer.processInPlace (inL.data (), inR.data (), frames);
|
||||||
|
|
||||||
|
for (int32 i = 0; i < frames; ++i)
|
||||||
|
{
|
||||||
|
buf[static_cast<size_t> (2 * i)] = inL[static_cast<size_t> (i)];
|
||||||
|
buf[static_cast<size_t> (2 * i + 1)] = inR[static_cast<size_t> (i)];
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE* g = std::fopen (outPath, "wb");
|
||||||
|
if (!g)
|
||||||
|
{
|
||||||
|
std::fprintf (stderr, "cannot write %s\n", outPath);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
std::fwrite (buf.data (), sizeof (float), buf.size (), g);
|
||||||
|
std::fclose (g);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
"""Golden test T13 — NativeMixer mirror server render pipeline.
|
||||||
|
|
||||||
|
So sanh output native mixer (native_mixer_test.exe) voi reference dung chinh
|
||||||
|
code path cua server `render_project`:
|
||||||
|
- track gain/pan: cong thuc render_engine.py (10^(dB/20), constant-power pan)
|
||||||
|
- master limiter: app.core.mastering_engine.apply_mastering (module limiter)
|
||||||
|
+ hard clip [-1,1] nhu render_project sau apply_mastering
|
||||||
|
|
||||||
|
Yeu cau: |RMS| va |peak| sai lech < 0.1 dB moi case, moi kenh.
|
||||||
|
|
||||||
|
Chay: python native_host/tests/test_native_mixer_golden.py
|
||||||
|
(tu repo root; can native_mixer_test.exe da build + numpy/scipy)
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
EXE = os.path.join(ROOT, "native_host", "tests", "native_mixer_test.exe")
|
||||||
|
SR = 44100
|
||||||
|
|
||||||
|
sys.path.insert(0, ROOT)
|
||||||
|
from app.core.mastering_engine import apply_mastering # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def make_signal(n=SR):
|
||||||
|
t = np.arange(n) / SR
|
||||||
|
sig = (
|
||||||
|
np.sin(2 * np.pi * 220 * t)
|
||||||
|
+ 0.5 * np.sin(2 * np.pi * 440 * t + 0.3)
|
||||||
|
+ 0.3 * np.sin(2 * np.pi * 880 * t + 1.1)
|
||||||
|
)
|
||||||
|
env = np.minimum(1.0, t * 20.0) * np.exp(-t * 0.8)
|
||||||
|
sig *= env
|
||||||
|
rng = np.random.default_rng(1234)
|
||||||
|
sig = sig + rng.standard_normal(n) * 0.05
|
||||||
|
# spike de limiter clip ro rang
|
||||||
|
for i in range(0, n, 22050):
|
||||||
|
sig[i] += 3.0
|
||||||
|
return np.stack([sig, np.roll(sig, 100)])
|
||||||
|
|
||||||
|
|
||||||
|
def ref_process(x, gain_db, pan, lim_active, th_db):
|
||||||
|
y = x * (10.0 ** (gain_db / 20.0))
|
||||||
|
if pan != 0.0:
|
||||||
|
theta = ((np.clip(pan, -1.0, 1.0) + 1.0) / 2.0) * (np.pi / 2.0)
|
||||||
|
y = y.copy()
|
||||||
|
y[0, :] *= np.cos(theta)
|
||||||
|
y[1, :] *= np.sin(theta)
|
||||||
|
if lim_active:
|
||||||
|
settings = {
|
||||||
|
"masterConnected": True,
|
||||||
|
"isBypassed": False,
|
||||||
|
"chain": [{"type": "limiter", "active": True}],
|
||||||
|
"limActive": True,
|
||||||
|
"limThreshold": th_db,
|
||||||
|
}
|
||||||
|
y = apply_mastering(y, settings, SR)
|
||||||
|
np.clip(y, -1.0, 1.0, out=y)
|
||||||
|
return y
|
||||||
|
|
||||||
|
|
||||||
|
def run_native(x, gain_db, pan, lim_active, th_db, tmp):
|
||||||
|
inp = os.path.join(tmp, "in.raw")
|
||||||
|
outp = os.path.join(tmp, "out.raw")
|
||||||
|
with open(inp, "wb") as f:
|
||||||
|
f.write(x.T.astype(np.float32).tobytes())
|
||||||
|
args = [EXE, inp, outp, str(gain_db), str(pan), str(int(lim_active)), str(th_db)]
|
||||||
|
subprocess.run(args, check=True)
|
||||||
|
return np.fromfile(outp, dtype=np.float32).reshape(-1, 2).T
|
||||||
|
|
||||||
|
|
||||||
|
def db_rms_peak(y):
|
||||||
|
rms = 20.0 * np.log10(np.sqrt(np.mean(y * y, axis=1)) + 1e-12)
|
||||||
|
peak = 20.0 * np.log10(np.max(np.abs(y), axis=1) + 1e-12)
|
||||||
|
return rms, peak
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not os.path.exists(EXE):
|
||||||
|
print(f"FAIL: {EXE} not found (build native_mixer_test.exe truoc)")
|
||||||
|
return 1
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
x = make_signal()
|
||||||
|
cases = [
|
||||||
|
(0.0, 0.0, False, -1.0), # identity (limiter off)
|
||||||
|
(6.0, -0.5, False, -1.0), # gain/pan, khong limiter
|
||||||
|
(6.0, -0.5, True, -3.0),
|
||||||
|
(-9.0, 0.8, True, -6.0),
|
||||||
|
(12.0, 0.0, True, 0.0),
|
||||||
|
]
|
||||||
|
worst = 0.0
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
for i, (g, p, la, th) in enumerate(cases):
|
||||||
|
native = run_native(x, g, p, la, th, tmp)
|
||||||
|
ref = ref_process(x, g, p, la, th)
|
||||||
|
nr, npk = db_rms_peak(native)
|
||||||
|
rr, rpk = db_rms_peak(ref)
|
||||||
|
dr = np.max(np.abs(nr - rr))
|
||||||
|
dp = np.max(np.abs(npk - rpk))
|
||||||
|
worst = max(worst, dr, dp)
|
||||||
|
status = "OK " if (dr < 0.1 and dp < 0.1) else "FAIL"
|
||||||
|
print(f"case {i} gain={g} pan={p} lim={int(la)} th={th}: "
|
||||||
|
f"RMS diff={dr:.4f} dB peak diff={dp:.4f} dB [{status}]")
|
||||||
|
if dr >= 0.1 or dp >= 0.1:
|
||||||
|
print(f" native rms={nr} peak={npk}")
|
||||||
|
print(f" ref rms={rr} peak={rpk}")
|
||||||
|
print(f"worst diff: {worst:.4f} dB (< 0.1 -> PASS)")
|
||||||
|
return 0 if worst < 0.1 else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
#include "pluginterfaces/vst/ivstevents.h"
|
#include "pluginterfaces/vst/ivstevents.h"
|
||||||
|
|
||||||
#include "AudioEngine.h"
|
#include "AudioEngine.h"
|
||||||
|
#include "NativeMixer.h"
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
@@ -99,6 +100,7 @@ struct Instance
|
|||||||
ComponentHandler handler;
|
ComponentHandler handler;
|
||||||
sonicforge::AudioEngine audio; // T12: native audio loop (SPSC + WASAPI)
|
sonicforge::AudioEngine audio; // T12: native audio loop (SPSC + WASAPI)
|
||||||
std::atomic<bool> audioRunning {false};
|
std::atomic<bool> audioRunning {false};
|
||||||
|
sonicforge::NativeMixer mixer; // T13: track gain/pan + master limiter
|
||||||
};
|
};
|
||||||
|
|
||||||
// T12: IEventList cố định (không cấp phát trên audio thread).
|
// T12: IEventList cố định (không cấp phát trên audio thread).
|
||||||
@@ -189,7 +191,9 @@ bool vst3AudioProcess (const sonicforge::MidiEvent* events, int32 eventCount,
|
|||||||
data.outputs = &outBus;
|
data.outputs = &outBus;
|
||||||
data.inputEvents = &list;
|
data.inputEvents = &list;
|
||||||
|
|
||||||
return proc->process (data) == kResultOk;
|
const tresult res = proc->process (data);
|
||||||
|
inst->mixer.processInPlace (outL, outR, frames); // T13: gain/pan + limiter
|
||||||
|
return res == kResultOk;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::mutex g_mutex;
|
std::mutex g_mutex;
|
||||||
@@ -547,4 +551,26 @@ __declspec (dllexport) int32 SF_VST3_SendNoteOff (int32 handle, int32 channel, i
|
|||||||
return it->second->audio.pushMidi (ev) ? 0 : -2;
|
return it->second->audio.pushMidi (ev) ? 0 : -2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// T13: track gain/pan + master limiter (NativeMixer). Set truoc AudioStart;
|
||||||
|
// audio thread doc state nay (khong lock).
|
||||||
|
__declspec (dllexport) int32 SF_VST3_SetTrackGainPan (int32 handle, float gainDb, float pan)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock (g_mutex);
|
||||||
|
auto it = g_instances.find (handle);
|
||||||
|
if (it == g_instances.end ())
|
||||||
|
return -1;
|
||||||
|
it->second->mixer.setTrack (sonicforge::TrackGainPan::fromDbPan (gainDb, pan));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
__declspec (dllexport) int32 SF_VST3_SetMasterLimiter (int32 handle, int32 active, float thresholdDb)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock (g_mutex);
|
||||||
|
auto it = g_instances.find (handle);
|
||||||
|
if (it == g_instances.end ())
|
||||||
|
return -1;
|
||||||
|
it->second->mixer.setLimiter (active != 0, thresholdDb);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|||||||
Reference in New Issue
Block a user