Skip to content

Commit c70e0ee

Browse files
Relax vocals gate thresholds and explicitly write main bus output (#4)
1 parent 360ff72 commit c70e0ee

7 files changed

Lines changed: 147 additions & 10 deletions

File tree

plugin/include/StemgenRT/Constants.h

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,19 @@ constexpr float kSoftGateFloor = 0.000016f; // -96dB in linear (16-bit noise
4343
// On instrumental tracks, the model often outputs spurious low-level content in the vocals stem.
4444
// This gate detects when vocals energy is very low relative to the mix and transfers it to "other".
4545
// Two criteria: (1) ratio of vocals to total energy, (2) absolute vocals level.
46-
// Real vocals are typically above -25dB; be aggressive about gating quiet content.
46+
// Keep gating conservative enough to avoid suppressing quiet but valid vocals.
4747
//
4848
// Ratio-based gating: when vocals are a tiny fraction of the mix, they're likely noise
49-
constexpr float kVocalsGateRatioThreshold = 0.01f; // Below 1% of mix energy, start gating
50-
constexpr float kVocalsGateRatioFloor = 0.003f; // Below 0.3%, fully gate (transfer to other)
49+
constexpr float kVocalsGateRatioThreshold = 0.0040f; // Below 0.40% of mix energy, start gating
50+
constexpr float kVocalsGateRatioFloor = 0.0008f; // Below 0.08%, fully gate (transfer to other)
5151
//
5252
// Level-based gating: absolute vocals level threshold (real vocals are rarely this quiet)
5353
// Uses peak amplitude (max of L/R) rather than RMS for faster response
54-
constexpr float kVocalsGateLevelThresholdDb = -28.0f; // Above this, vocals pass through
55-
constexpr float kVocalsGateLevelFloorDb = -32.0f; // Below this, fully gate
54+
constexpr float kVocalsGateLevelThresholdDb = -39.0f; // Above this, vocals pass through
55+
constexpr float kVocalsGateLevelFloorDb = -50.0f; // Below this, fully gate
5656
// Precomputed linear values: 10^(dB/20)
57-
constexpr float kVocalsGateLevelThreshold = 0.04f; // -28dB in linear
58-
constexpr float kVocalsGateLevelFloor = 0.025f; // -32dB in linear
57+
constexpr float kVocalsGateLevelThreshold = 0.0112f; // -39dB in linear
58+
constexpr float kVocalsGateLevelFloor = 0.0032f; // -50dB in linear
5959
//
6060
// Asymmetric attack/release time constants for vocals gate (in seconds)
6161
// Fast attack so vocals come in quickly, slow release to avoid pumping on gaps

plugin/include/StemgenRT/OverlapAddProcessor.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ class OverlapAddProcessor {
9696
// Advance dry delay positions (call once per sample after reading)
9797
void advanceDryDelayPos();
9898

99+
// Dry delay priming helpers
100+
bool isDryDelayPrimed() const { return dryDelayPrimed_; }
101+
void primeDryDelayFromInput(const float* inputPointers[kNumChannels], int numSamples);
102+
99103
// === Chunk boundary crossfade state ===
100104

101105
// Previous chunk's overlap tail for crossfading at chunk boundaries.
@@ -135,6 +139,7 @@ class OverlapAddProcessor {
135139
std::array<std::vector<float>, kNumChannels> dryDelayLine_;
136140
size_t dryDelayWritePos_{0};
137141
size_t dryDelayReadPos_{0};
142+
bool dryDelayPrimed_{false};
138143

139144
// Chunk boundary crossfade state
140145
std::array<std::array<std::vector<float>, kNumChannels>, kNumStems> prevOverlapTail_;

plugin/source/OutputWriter.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ void OutputWriter::writeBlock(
5757
dry[ch] = overlapAdd.readDryDelaySample(ch);
5858
}
5959

60-
// Main bus: don't write — input passes through unmodified via
61-
// JUCE in-place buffer sharing.
60+
// Main bus is copied from live input in PluginProcessor before this call.
61+
// Keep it untouched here so bus 0 remains true dry passthrough.
6262

6363
// Stem buses (if enabled)
6464
// During underrun, output dry/4 to each stem (approximate equal split)

plugin/source/OverlapAddProcessor.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ void OverlapAddProcessor::reset() {
6464
// Initialize dry delay positions so readPos lags behind writePos by kOutputChunkSize
6565
dryDelayWritePos_ = static_cast<size_t>(kOutputChunkSize);
6666
dryDelayReadPos_ = 0;
67+
dryDelayPrimed_ = false;
6768
}
6869

6970
void OverlapAddProcessor::resetIndices() {
@@ -79,6 +80,7 @@ void OverlapAddProcessor::resetIndices() {
7980
// Initialize dry delay positions so readPos lags behind writePos by kOutputChunkSize
8081
dryDelayWritePos_ = static_cast<size_t>(kOutputChunkSize);
8182
dryDelayReadPos_ = 0;
83+
dryDelayPrimed_ = false;
8284
}
8385

8486
void OverlapAddProcessor::pushInputSample(int channel, float hpSample, float lpSample, float drySample) {
@@ -161,4 +163,28 @@ void OverlapAddProcessor::advanceDryDelayPos() {
161163
dryDelayReadPos_ = (dryDelayReadPos_ + 1) % dryDelaySize;
162164
}
163165

166+
void OverlapAddProcessor::primeDryDelayFromInput(
167+
const float* inputPointers[kNumChannels], int numSamples) {
168+
if (numSamples <= 0)
169+
return;
170+
171+
const size_t targetFill = static_cast<size_t>(kOutputChunkSize);
172+
const size_t inputCount = static_cast<size_t>(numSamples);
173+
const size_t copyCount = std::min(targetFill, inputCount);
174+
const size_t srcOffset = inputCount - copyCount;
175+
176+
for (int ch = 0; ch < kNumChannels; ++ch) {
177+
auto& dryDelay = dryDelayLine_[static_cast<size_t>(ch)];
178+
std::fill_n(dryDelay.begin(), targetFill, 0.0f);
179+
180+
if (inputPointers[ch] != nullptr) {
181+
std::memcpy(dryDelay.data(),
182+
inputPointers[ch] + srcOffset,
183+
copyCount * sizeof(float));
184+
}
185+
}
186+
187+
dryDelayPrimed_ = true;
188+
}
189+
164190
} // namespace audio_plugin

plugin/source/PluginProcessor.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,12 @@ void AudioPluginAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer,
707707
}
708708
}
709709

710+
// Prime dry delay line before main output takes a sample so the first block
711+
// isn't silent due to the initial zeroed buffer.
712+
if (!overlapAdd_.isDryDelayPrimed()) {
713+
overlapAdd_.primeDryDelayFromInput(inputChannelPtrs, numSamples);
714+
}
715+
710716
// ===== Write separated stems to output buses =====
711717
const int numOutputBuses = getBusCount(false /* isInput */);
712718

test/CMakeLists.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ project(AudioPluginTest)
66
enable_testing()
77

88
# Creates the test console application.
9-
set(SOURCE_FILES source/AudioProcessorTest.cpp)
9+
set(SOURCE_FILES
10+
source/AudioProcessorTest.cpp
11+
source/OverlapAddProcessorTest.cpp
12+
)
1013
add_executable(${PROJECT_NAME} ${SOURCE_FILES})
1114

1215
# Sets the necessary include directories of googletest.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#include <StemgenRT/Constants.h>
2+
#include <StemgenRT/OverlapAddProcessor.h>
3+
#include <gtest/gtest.h>
4+
#include <vector>
5+
6+
namespace audio_plugin_test {
7+
8+
namespace {
9+
10+
void pushDryBlock(audio_plugin::OverlapAddProcessor& processor,
11+
const std::vector<float>& left,
12+
const std::vector<float>& right) {
13+
ASSERT_EQ(left.size(), right.size());
14+
for (size_t i = 0; i < left.size(); ++i) {
15+
processor.pushInputSample(0, 0.0f, 0.0f, left[i]);
16+
processor.pushInputSample(1, 0.0f, 0.0f, right[i]);
17+
}
18+
}
19+
20+
std::vector<float> readDrySamples(audio_plugin::OverlapAddProcessor& processor,
21+
int channel,
22+
size_t count) {
23+
std::vector<float> out(count);
24+
for (size_t i = 0; i < count; ++i) {
25+
out[i] = processor.readDryDelaySample(channel);
26+
processor.advanceDryDelayPos();
27+
}
28+
return out;
29+
}
30+
31+
} // namespace
32+
33+
TEST(OverlapAddProcessorTest, PrimeDryDelayDoesNotTileShortHostBlock) {
34+
audio_plugin::OverlapAddProcessor processor;
35+
processor.allocate();
36+
37+
// Simulate RT reset behavior: indices are reset but dry delay storage is retained.
38+
std::vector<float> stale(audio_plugin::kOutputChunkSize * 2, -1.0f);
39+
pushDryBlock(processor, stale, stale);
40+
processor.resetIndices();
41+
42+
constexpr int kHostBlockSize = 64;
43+
std::vector<float> input(static_cast<size_t>(kHostBlockSize));
44+
for (int i = 0; i < kHostBlockSize; ++i) {
45+
input[static_cast<size_t>(i)] = static_cast<float>(i + 1);
46+
}
47+
48+
pushDryBlock(processor, input, input);
49+
50+
const float* inputPointers[audio_plugin::kNumChannels] = {
51+
input.data(), input.data()};
52+
processor.primeDryDelayFromInput(inputPointers, kHostBlockSize);
53+
54+
auto primed = readDrySamples(processor, 0, audio_plugin::kOutputChunkSize);
55+
56+
for (int i = 0; i < kHostBlockSize; ++i) {
57+
EXPECT_FLOAT_EQ(primed[static_cast<size_t>(i)], input[static_cast<size_t>(i)]);
58+
}
59+
for (int i = kHostBlockSize; i < audio_plugin::kOutputChunkSize; ++i) {
60+
EXPECT_FLOAT_EQ(primed[static_cast<size_t>(i)], 0.0f);
61+
}
62+
}
63+
64+
TEST(OverlapAddProcessorTest, PrimeDryDelayKeepsNewestWrappedSamplesForLargeHostBlock) {
65+
audio_plugin::OverlapAddProcessor withPriming;
66+
withPriming.allocate();
67+
withPriming.resetIndices();
68+
69+
audio_plugin::OverlapAddProcessor withoutPriming;
70+
withoutPriming.allocate();
71+
withoutPriming.resetIndices();
72+
73+
constexpr int kHostBlockSize = 1024;
74+
static_assert(kHostBlockSize > audio_plugin::kOutputChunkSize);
75+
std::vector<float> input(static_cast<size_t>(kHostBlockSize));
76+
for (int i = 0; i < kHostBlockSize; ++i) {
77+
input[static_cast<size_t>(i)] = static_cast<float>(i + 1);
78+
}
79+
80+
pushDryBlock(withPriming, input, input);
81+
pushDryBlock(withoutPriming, input, input);
82+
83+
const float* inputPointers[audio_plugin::kNumChannels] = {
84+
input.data(), input.data()};
85+
withPriming.primeDryDelayFromInput(inputPointers, kHostBlockSize);
86+
87+
auto primed = readDrySamples(withPriming, 0, audio_plugin::kOutputChunkSize);
88+
auto baseline = readDrySamples(withoutPriming, 0, audio_plugin::kOutputChunkSize);
89+
90+
for (int i = 0; i < audio_plugin::kOutputChunkSize; ++i) {
91+
EXPECT_FLOAT_EQ(primed[static_cast<size_t>(i)], baseline[static_cast<size_t>(i)]);
92+
EXPECT_FLOAT_EQ(primed[static_cast<size_t>(i)],
93+
input[static_cast<size_t>(i + audio_plugin::kOutputChunkSize)]);
94+
}
95+
}
96+
97+
} // namespace audio_plugin_test

0 commit comments

Comments
 (0)