Skip to content

Commit 7c3cca8

Browse files
committed
Add a Balance section to the StringVoice
- Human Voice and Strings levels, one per section, so the two can be balanced without re-tuning each register's footage - Sits below the Strings section, where the hardware keeps it - Both default to full, leaving existing projects as they were
1 parent 3e3303b commit 7c3cca8

12 files changed

Lines changed: 220 additions & 4 deletions

CHANGELOG

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ Release date:
55

66
New features:
77

8+
* Add a Balance section to the StringVoice, below the Strings section
9+
- Human Voice and Strings levels, one per section, so the two can be
10+
balanced without re-tuning each register's footage
11+
- Both default to full, leaving existing projects as they were
12+
813
* Add the Tube Stage effect
914
- Valve preamp with a Bias control for the operating point, which is what
1015
decides how much even-harmonic warmth the stage generates

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ set(QML_SOURCE_FILES
107107
${QML_BASE_DIR}/Dialogs/StringEnsembleDialog_Global.qml
108108
${QML_BASE_DIR}/Dialogs/StringEnsembleDialog_Registers.qml
109109
${QML_BASE_DIR}/Dialogs/StringVoiceDialog.qml
110+
${QML_BASE_DIR}/Dialogs/StringVoiceDialog_Balance.qml
110111
${QML_BASE_DIR}/Dialogs/StringVoiceDialog_Ensemble.qml
111112
${QML_BASE_DIR}/Dialogs/StringVoiceDialog_Global.qml
112113
${QML_BASE_DIR}/Dialogs/StringVoiceDialog_Strings.qml

src/common/constants.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2042,11 +2042,21 @@ QString xmlKeyFftRate()
20422042
return "fftRate";
20432043
}
20442044

2045+
QString xmlKeyStringsBalance()
2046+
{
2047+
return "stringsBalance";
2048+
}
2049+
20452050
QString xmlKeyStringsLevel8()
20462051
{
20472052
return "stringsLevel8";
20482053
}
20492054

2055+
QString xmlKeyVoiceBalance()
2056+
{
2057+
return "voiceBalance";
2058+
}
2059+
20502060
QString xmlKeyStringsLevel4()
20512061
{
20522062
return "stringsLevel4";

src/common/constants.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,7 @@ QString xmlKeyPreDelay();
416416
QString xmlKeyVoiceDepth();
417417
QString xmlKeyPortamento();
418418
QString xmlKeyPanSpread();
419+
QString xmlKeyVoiceBalance();
419420
QString xmlKeyPitchBendRange();
420421
QString xmlKeyResonance();
421422
QString xmlKeyKeyTrack();
@@ -591,6 +592,7 @@ QString xmlValueSynths();
591592
QString xmlValueMixers();
592593
QString xmlValueDrums();
593594

595+
QString xmlKeyStringsBalance();
594596
QString xmlKeyStringsLevel8();
595597
QString xmlKeyStringsLevel4();
596598
QString xmlKeyStringsAttack();

src/domain/devices/string_voice_device.cpp

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ void StringVoiceDevice::Voice::reset()
5959
StringVoiceDevice::StringVoiceDevice(std::string name)
6060
: m_name { std::move(name) }
6161
{
62+
addParameter(Parameter { Constants::NahdXml::xmlKeyStringsBalance().toStdString(), 1.0f, 0, 100, 100, 100 });
63+
addParameter(Parameter { Constants::NahdXml::xmlKeyVoiceBalance().toStdString(), 1.0f, 0, 100, 100, 100 });
64+
6265
addParameter(Parameter { Constants::NahdXml::xmlKeyStringsLevel8().toStdString(), 0.8f, 0, 100, 80, 100 });
6366
addParameter(Parameter { Constants::NahdXml::xmlKeyStringsLevel4().toStdString(), 0.0f, 0, 100, 0, 100 });
6467
addParameter(Parameter { Constants::NahdXml::xmlKeyStringsAttack().toStdString(), 0.2f, 0, 100, 20, 100 });
@@ -444,9 +447,9 @@ void StringVoiceDevice::processAudio(AudioContext & context)
444447

445448
const double voiceGain { static_cast<double>(v.velocity) * linearGainInternal() * 0.25 * polyphonyGain };
446449

447-
const double strSample { (strSample8 * m_stringsLevel8 + strSample4 * m_stringsLevel4) * strEnv * voiceGain };
448-
const double maleSample { (vocSample8 * m_voiceMale8 + vocSample4 * m_voiceMale4) * vocEnv * voiceGain };
449-
const double femaleSample { (vocSample8 * m_voiceFemale8 + vocSample4 * m_voiceFemale4) * vocEnv * voiceGain };
450+
const double strSample { (strSample8 * m_stringsLevel8 + strSample4 * m_stringsLevel4) * strEnv * voiceGain * m_stringsBalance };
451+
const double maleSample { (vocSample8 * m_voiceMale8 + vocSample4 * m_voiceMale4) * vocEnv * voiceGain * m_voiceBalance };
452+
const double femaleSample { (vocSample8 * m_voiceFemale8 + vocSample4 * m_voiceFemale4) * vocEnv * voiceGain * m_voiceBalance };
450453

451454
stringsSumL += strSample * (1.0 - v.pan);
452455
stringsSumR += strSample * v.pan;
@@ -597,6 +600,26 @@ void StringVoiceDevice::deserializeFromXml(ProjectReader & reader)
597600
}
598601

599602
// Getters and Setters implementation
603+
float StringVoiceDevice::stringsBalance() const
604+
{
605+
return m_stringsBalance;
606+
}
607+
608+
void StringVoiceDevice::setStringsBalance(float val)
609+
{
610+
setContinuousParameterValue(Constants::NahdXml::xmlKeyStringsBalance().toStdString(), val);
611+
}
612+
613+
float StringVoiceDevice::voiceBalance() const
614+
{
615+
return m_voiceBalance;
616+
}
617+
618+
void StringVoiceDevice::setVoiceBalance(float val)
619+
{
620+
setContinuousParameterValue(Constants::NahdXml::xmlKeyVoiceBalance().toStdString(), val);
621+
}
622+
600623
float StringVoiceDevice::stringsLevel8() const
601624
{
602625
return m_stringsLevel8;
@@ -814,6 +837,12 @@ void StringVoiceDevice::syncParameters()
814837

815838
Device::syncParameters();
816839

840+
if (const auto p = parameter(Constants::NahdXml::xmlKeyStringsBalance().toStdString()); p) {
841+
m_stringsBalance = p->get().value();
842+
}
843+
if (const auto p = parameter(Constants::NahdXml::xmlKeyVoiceBalance().toStdString()); p) {
844+
m_voiceBalance = p->get().value();
845+
}
817846
if (const auto p = parameter(Constants::NahdXml::xmlKeyStringsLevel8().toStdString()); p) {
818847
m_stringsLevel8 = p->get().value();
819848
}

src/domain/devices/string_voice_device.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ class StringVoiceDevice : public Device
6363
void deserializeFromXml(ProjectReader & reader) override;
6464

6565
// Getters and Setters for Parameters
66+
float stringsBalance() const;
67+
void setStringsBalance(float val);
68+
69+
float voiceBalance() const;
70+
void setVoiceBalance(float val);
71+
6672
float stringsLevel8() const;
6773
void setStringsLevel8(float val);
6874

@@ -164,6 +170,11 @@ class StringVoiceDevice : public Device
164170
uint64_t m_nextTriggerId { 1 };
165171

166172
// Device Parameters cache
173+
//! Section balance, the hardware's own Balance sliders: one level per section, so a patch can
174+
//! be tipped between strings and voices without touching each register's footage.
175+
float m_stringsBalance { 1.0f };
176+
float m_voiceBalance { 1.0f };
177+
167178
float m_stringsLevel8 { 0.8f };
168179
float m_stringsLevel4 { 0.0f };
169180
float m_stringsAttack { 50.0f };

src/unit_tests/string_voice_test/string_voice_test.cpp

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ void StringVoiceTest::test_serialization_shouldRestoreParameters()
232232
{
233233
NahdXmlWriter writer { buffer };
234234
StringVoiceDevice dev { "Test StringVoice" };
235+
dev.setStringsBalance(0.35f);
236+
dev.setVoiceBalance(0.65f);
235237
dev.setStringsLevel8(0.45f);
236238
dev.setStringsLevel4(0.6f);
237239
dev.setVoiceMale4(0.3f);
@@ -258,6 +260,8 @@ void StringVoiceTest::test_serialization_shouldRestoreParameters()
258260

259261
dev.deserializeFromXml(reader);
260262

263+
QCOMPARE(dev.stringsBalance(), 0.35f);
264+
QCOMPARE(dev.voiceBalance(), 0.65f);
261265
QCOMPARE(dev.stringsLevel8(), 0.45f);
262266
QCOMPARE(dev.stringsLevel4(), 0.6f);
263267
QCOMPARE(dev.voiceMale4(), 0.3f);
@@ -412,6 +416,48 @@ void StringVoiceTest::test_voiceRegisters_shouldRouteMale4AndFemale8Independentl
412416
QCOMPARE(renderPeak(silentDev), 0.0);
413417
}
414418

419+
void StringVoiceTest::test_balance_shouldScaleEachSectionIndependently()
420+
{
421+
// One level per section, the hardware's Balance sliders. Turning one down must not touch the
422+
// other, which is the whole point of not having to reach for each register's footage.
423+
const auto render = [](float stringsBalance, float voiceBalance) {
424+
StringVoiceDevice dev { "Test StringVoice" };
425+
dev.setSampleRate(44100);
426+
dev.setStringsLevel8(1.0f);
427+
dev.setVoiceMale8(1.0f);
428+
dev.setStringsAttack(0.0f);
429+
dev.setVoiceAttack(0.0f);
430+
dev.setEnsembleEnabled(false);
431+
dev.setStringsBalance(stringsBalance);
432+
dev.setVoiceBalance(voiceBalance);
433+
dev.processMidiNoteOn(48, 100);
434+
435+
constexpr uint32_t frameCount { 8192 };
436+
std::vector<double> buffer(static_cast<size_t>(frameCount) * 2, 0.0);
437+
auto ctx = makeContext(buffer, frameCount);
438+
dev.processAudio(ctx);
439+
return buffer;
440+
};
441+
442+
const double both { rmsLevel(render(1.0f, 1.0f)) };
443+
const double stringsOnly { rmsLevel(render(1.0f, 0.0f)) };
444+
const double voiceOnly { rmsLevel(render(0.0f, 1.0f)) };
445+
const double neither { rmsLevel(render(0.0f, 0.0f)) };
446+
447+
QVERIFY(both > 0.0);
448+
QVERIFY2(stringsOnly > 0.0 && voiceOnly > 0.0,
449+
QString("A section fell silent with its own balance up: strings %1, voice %2").arg(stringsOnly).arg(voiceOnly).toUtf8().constData());
450+
QVERIFY2(neither < both * 0.001,
451+
QString("Both balances down still gave %1 against %2").arg(neither).arg(both).toUtf8().constData());
452+
QVERIFY(stringsOnly < both);
453+
QVERIFY(voiceOnly < both);
454+
455+
// Half of the strings section must land halfway in level, not somewhere the voice section moved.
456+
const double halfStrings { rmsLevel(render(0.5f, 0.0f)) };
457+
QVERIFY2(std::abs(halfStrings - stringsOnly * 0.5) < stringsOnly * 0.05,
458+
QString("Half balance gave %1, expected about %2").arg(halfStrings).arg(stringsOnly * 0.5).toUtf8().constData());
459+
}
460+
415461
void StringVoiceTest::test_formants_male_shouldPeakAtOohFrequencies()
416462
{
417463
// The male register sings an /u/: F1 325, F2 700, F3 2530 Hz. F3 is the one that decides whether

src/unit_tests/string_voice_test/string_voice_test.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ private slots:
3636
void test_vibrato_shouldModulatePitchWithinASingleBuffer();
3737
void test_ensembleMode_shouldSupportChorusIPlusII();
3838
void test_voiceRegisters_shouldRouteMale4AndFemale8Independently();
39+
void test_balance_shouldScaleEachSectionIndependently();
3940
void test_formants_male_shouldPeakAtOohFrequencies();
4041
void test_formants_female_shouldNotNotchBetweenPeaks();
4142
void test_voiceStealing_shouldRemainStableWhenOversubscribed();

src/view/controllers/string_voice_controller.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,30 @@ bool StringVoiceController::setDevice(DeviceS device)
4545
return false;
4646
}
4747

48+
int StringVoiceController::stringsBalance() const
49+
{
50+
return m_device ? static_cast<int>(std::round(m_device->stringsBalance() * Constants::uiInternalScaling())) : 0;
51+
}
52+
53+
void StringVoiceController::setStringsBalance(int value)
54+
{
55+
if (m_device) {
56+
m_device->setStringsBalance(static_cast<float>(value) / Constants::uiInternalScaling());
57+
}
58+
}
59+
60+
int StringVoiceController::voiceBalance() const
61+
{
62+
return m_device ? static_cast<int>(std::round(m_device->voiceBalance() * Constants::uiInternalScaling())) : 0;
63+
}
64+
65+
void StringVoiceController::setVoiceBalance(int value)
66+
{
67+
if (m_device) {
68+
m_device->setVoiceBalance(static_cast<float>(value) / Constants::uiInternalScaling());
69+
}
70+
}
71+
4872
int StringVoiceController::stringsLevel8() const
4973
{
5074
return m_device ? static_cast<int>(std::round(m_device->stringsLevel8() * Constants::uiInternalScaling())) : 0;
@@ -287,6 +311,8 @@ void StringVoiceController::setPanSpread(int value)
287311

288312
void StringVoiceController::requestSettings()
289313
{
314+
emit stringsBalanceChanged();
315+
emit voiceBalanceChanged();
290316
emit stringsLevel8Changed();
291317
emit stringsLevel4Changed();
292318
emit stringsAttackChanged();

src/view/controllers/string_voice_controller.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ class StringVoiceController : public DeviceController
2727
{
2828
Q_OBJECT
2929

30+
Q_PROPERTY(int stringsBalance READ stringsBalance WRITE setStringsBalance NOTIFY stringsBalanceChanged)
31+
Q_PROPERTY(int voiceBalance READ voiceBalance WRITE setVoiceBalance NOTIFY voiceBalanceChanged)
32+
3033
Q_PROPERTY(int stringsLevel8 READ stringsLevel8 WRITE setStringsLevel8 NOTIFY stringsLevel8Changed)
3134
Q_PROPERTY(int stringsLevel4 READ stringsLevel4 WRITE setStringsLevel4 NOTIFY stringsLevel4Changed)
3235
Q_PROPERTY(int stringsAttack READ stringsAttack WRITE setStringsAttack NOTIFY stringsAttackChanged)
@@ -60,6 +63,12 @@ class StringVoiceController : public DeviceController
6063
DeviceS device() const override;
6164
bool setDevice(DeviceS device) override;
6265

66+
int stringsBalance() const;
67+
void setStringsBalance(int value);
68+
69+
int voiceBalance() const;
70+
void setVoiceBalance(int value);
71+
6372
int stringsLevel8() const;
6473
void setStringsLevel8(int value);
6574

@@ -124,6 +133,8 @@ class StringVoiceController : public DeviceController
124133

125134
signals:
126135
void deviceChanged();
136+
void stringsBalanceChanged();
137+
void voiceBalanceChanged();
127138
void stringsLevel8Changed();
128139
void stringsLevel4Changed();
129140
void stringsAttackChanged();

0 commit comments

Comments
 (0)