From d8f7dcebdc215bddeccbb5cda1e54ba0ecb55107 Mon Sep 17 00:00:00 2001 From: f4exb Date: Sat, 5 Aug 2017 19:08:33 +0200 Subject: [PATCH] SSM modulator: added audio compressor --- plugins/channelrx/demodssb/ssbdemod.cpp | 1 + plugins/channeltx/modssb/ssbmod.cpp | 98 ++++++++++-- plugins/channeltx/modssb/ssbmod.h | 62 +++++++- plugins/channeltx/modssb/ssbmodgui.cpp | 92 ++++++++++- plugins/channeltx/modssb/ssbmodgui.h | 8 + plugins/channeltx/modssb/ssbmodgui.ui | 198 ++++++++++++++++++++++++ sdrbase/dsp/agc.cpp | 30 +++- sdrbase/dsp/agc.h | 17 +- 8 files changed, 479 insertions(+), 27 deletions(-) diff --git a/plugins/channelrx/demodssb/ssbdemod.cpp b/plugins/channelrx/demodssb/ssbdemod.cpp index 416091a42..baf3960ac 100644 --- a/plugins/channelrx/demodssb/ssbdemod.cpp +++ b/plugins/channelrx/demodssb/ssbdemod.cpp @@ -325,6 +325,7 @@ bool SSBDemod::handleMessage(const Message& cmd) if (m_agcNbSamples != agcNbSamples) { m_agc.resize(agcNbSamples, agcTarget); + m_agc.setStepDownDelay(agcNbSamples); m_agcNbSamples = agcNbSamples; } diff --git a/plugins/channeltx/modssb/ssbmod.cpp b/plugins/channeltx/modssb/ssbmod.cpp index 002cd2b15..3eebfe209 100644 --- a/plugins/channeltx/modssb/ssbmod.cpp +++ b/plugins/channeltx/modssb/ssbmod.cpp @@ -24,6 +24,7 @@ #include #include "dsp/dspengine.h" #include "dsp/pidcontroller.h" +#include "util/db.h" MESSAGE_CLASS_DEFINITION(SSBMod::MsgConfigureSSBMod, Message) MESSAGE_CLASS_DEFINITION(SSBMod::MsgConfigureFileSourceName, Message) @@ -45,7 +46,6 @@ SSBMod::SSBMod(BasebandSampleSink* sampleSink) : m_DSBFilterBufferIndex(0), m_sampleSink(sampleSink), m_movingAverage(40, 0), - m_volumeAGC(40, 0), m_audioFifo(4, 48000), m_settingsMutex(QMutex::Recursive), m_fileSize(0), @@ -54,7 +54,8 @@ SSBMod::SSBMod(BasebandSampleSink* sampleSink) : m_afInput(SSBModInputNone), m_levelCalcCount(0), m_peakLevel(0.0f), - m_levelSum(0.0f) + m_levelSum(0.0f), + m_inAGC(9600, 0.2, 1e-4) { setObjectName("SSBMod"); @@ -84,7 +85,6 @@ SSBMod::SSBMod(BasebandSampleSink* sampleSink) : m_sumCount = 0; m_movingAverage.resize(16, 0); - m_volumeAGC.resize(4096, 0.003, 0); m_magsq = 0.0; m_toneNco.setFreq(1000.0, m_config.m_audioSampleRate); @@ -96,6 +96,9 @@ SSBMod::SSBMod(BasebandSampleSink* sampleSink) : m_cwKeyer.setMode(CWKeyer::CWNone); m_cwSmoother.setNbFadeSamples(192); // 4 ms at 48 kHz + m_inAGC.setGate(m_config.m_agcThresholdGate); + m_inAGC.setStepDownDelay(m_config.m_agcThresholdDelay); + m_inAGC.setClamping(true); apply(); } @@ -130,7 +133,12 @@ void SSBMod::configure(MessageQueue* messageQueue, bool audioFlipChannels, bool dsb, bool audioMute, - bool playLoop) + bool playLoop, + bool agc, + int agcTime, + int agcThreshold, + int agcThresholdGate, + int agcThresholdDelay) { Message* cmd = MsgConfigureSSBMod::create(bandwidth, lowCutoff, @@ -141,7 +149,12 @@ void SSBMod::configure(MessageQueue* messageQueue, audioFlipChannels, dsb, audioMute, - playLoop); + playLoop, + agc, + agcTime, + agcThreshold, + agcThresholdGate, + agcThresholdDelay); messageQueue->push(cmd); } @@ -282,8 +295,19 @@ void SSBMod::pullAF(Complex& sample) { Real real; m_ifstream.read(reinterpret_cast(&real), sizeof(Real)); - ci.real(real * m_running.m_volumeFactor); - ci.imag(0.0f); + + if (m_running.m_agc) + { + ci.real(real); + ci.imag(0.0f); + m_inAGC.feed(ci); + ci *= m_running.m_volumeFactor; + } + else + { + ci.real(real * m_running.m_volumeFactor); + ci.imag(0.0f); + } } } } @@ -309,8 +333,18 @@ void SSBMod::pullAF(Complex& sample) } else { - ci.real(((m_audioBuffer[m_audioBufferFill].l + m_audioBuffer[m_audioBufferFill].r) / 65536.0f) * m_running.m_volumeFactor); - ci.imag(0.0f); + if (m_running.m_agc) + { + ci.real(((m_audioBuffer[m_audioBufferFill].l + m_audioBuffer[m_audioBufferFill].r) / 65536.0f)); + ci.imag(0.0f); + m_inAGC.feed(ci); + ci *= m_running.m_volumeFactor; + } + else + { + ci.real(((m_audioBuffer[m_audioBufferFill].l + m_audioBuffer[m_audioBufferFill].r) / 65536.0f) * m_running.m_volumeFactor); + ci.imag(0.0f); + } } break; @@ -580,6 +614,13 @@ bool SSBMod::handleMessage(const Message& cmd) m_config.m_dsb = cfg.getDSB(); m_config.m_audioMute = cfg.getAudioMute(); m_config.m_playLoop = cfg.getPlayLoop(); + m_config.m_agc = cfg.getAGC(); + + m_config.m_agcTime = 48 * cfg.getAGCTime(); // ms + m_config.m_agcThresholdEnable = cfg.getAGCThreshold() != -99; + m_config.m_agcThreshold = CalcDb::powerFromdB(cfg.getAGCThreshold()); // power dB + m_config.m_agcThresholdGate = 48 * cfg.getAGCThresholdGate(); // ms + m_config.m_agcThresholdDelay = 48 * cfg.getAGCThresholdDelay(); // ms apply(); @@ -595,7 +636,13 @@ bool SSBMod::handleMessage(const Message& cmd) << " m_audioFlipChannels: " << m_config.m_audioFlipChannels << " m_dsb: " << m_config.m_dsb << " m_audioMute: " << m_config.m_audioMute - << " m_playLoop: " << m_config.m_playLoop; + << " m_playLoop: " << m_config.m_playLoop + << " m_agc: " << m_config.m_agc + << " m_agcTime: " << m_config.m_agcTime + << " m_agcThresholdEnable: " << m_config.m_agcThresholdEnable + << " m_agcThreshold: " << m_config.m_agcThreshold + << " m_agcThresholdGate: " << m_config.m_agcThresholdGate + << " m_agcThresholdDelay: " << m_config.m_agcThresholdDelay; return true; } @@ -705,6 +752,31 @@ void SSBMod::apply() } } + if (m_config.m_agcTime != m_running.m_agcTime) + { + m_inAGC.resize(m_config.m_agcTime, 0.2); + } + + if (m_config.m_agcThresholdEnable != m_running.m_agcThresholdEnable) + { + m_inAGC.setThresholdEnable(m_config.m_agcThresholdEnable); + } + + if (m_config.m_agcThreshold != m_running.m_agcThreshold) + { + m_inAGC.setThreshold(m_config.m_agcThreshold); + } + + if (m_config.m_agcThresholdGate != m_running.m_agcThresholdGate) + { + m_inAGC.setGate(m_config.m_agcThresholdGate); + } + + if (m_config.m_agcThresholdDelay != m_running.m_agcThresholdDelay) + { + m_inAGC.setStepDownDelay(m_config.m_agcThresholdDelay); + } + m_running.m_outputSampleRate = m_config.m_outputSampleRate; m_running.m_inputFrequencyOffset = m_config.m_inputFrequencyOffset; m_running.m_bandwidth = m_config.m_bandwidth; @@ -719,6 +791,12 @@ void SSBMod::apply() m_running.m_dsb = m_config.m_dsb; m_running.m_audioMute = m_config.m_audioMute; m_running.m_playLoop = m_config.m_playLoop; + m_running.m_agc = m_config.m_agc; + m_running.m_agcTime = m_config.m_agcTime; + m_running.m_agcThresholdEnable = m_config.m_agcThresholdEnable; + m_running.m_agcThreshold = m_config.m_agcThreshold; + m_running.m_agcThresholdGate = m_config.m_agcThresholdGate; + m_running.m_agcThresholdDelay = m_config.m_agcThresholdDelay; } void SSBMod::openFileStream() diff --git a/plugins/channeltx/modssb/ssbmod.h b/plugins/channeltx/modssb/ssbmod.h index 5c8bd9cc4..91814a672 100644 --- a/plugins/channeltx/modssb/ssbmod.h +++ b/plugins/channeltx/modssb/ssbmod.h @@ -187,7 +187,12 @@ public: bool audioFlipChannels, bool dsb, bool audioMute, - bool playLoop); + bool playLoop, + bool agc, + int agcTime, + int agcThreshold, + int agcThresholdGate, + int agcThresholdDelay); virtual void pull(Sample& sample); virtual void pullAudio(int nbSamples); @@ -225,6 +230,11 @@ private: bool getDSB() const { return m_dsb; } bool getAudioMute() const { return m_audioMute; } bool getPlayLoop() const { return m_playLoop; } + bool getAGC() const { return m_agc; } + int getAGCTime() const { return m_agcTime; } + int getAGCThreshold() const { return m_agcThreshold; } + int getAGCThresholdGate() const { return m_agcThresholdGate; } + int getAGCThresholdDelay() const { return m_agcThresholdDelay; } static MsgConfigureSSBMod* create(Real bandwidth, Real lowCutoff, @@ -235,7 +245,12 @@ private: bool audioFlipChannels, bool dsb, bool audioMute, - bool playLoop) + bool playLoop, + bool agc, + int agcTime, + int agcThreshold, + int agcThresholdGate, + int agcThresholdDelay) { return new MsgConfigureSSBMod(bandwidth, lowCutoff, @@ -246,7 +261,12 @@ private: audioFlipChannels, dsb, audioMute, - playLoop); + playLoop, + agc, + agcTime, + agcThreshold, + agcThresholdGate, + agcThresholdDelay); } private: @@ -260,6 +280,11 @@ private: bool m_dsb; bool m_audioMute; bool m_playLoop; + bool m_agc; + int m_agcTime; + int m_agcThreshold; + int m_agcThresholdGate; + int m_agcThresholdDelay; MsgConfigureSSBMod(Real bandwidth, Real lowCutoff, @@ -270,7 +295,12 @@ private: bool audioFlipChannels, bool dsb, bool audioMute, - bool playLoop) : + bool playLoop, + bool agc, + int agcTime, + int agcThreshold, + int agcThresholdGate, + int agcThresholdDelay) : Message(), m_bandwidth(bandwidth), m_lowCutoff(lowCutoff), @@ -281,7 +311,12 @@ private: m_audioFlipChannels(audioFlipChannels), m_dsb(dsb), m_audioMute(audioMute), - m_playLoop(playLoop) + m_playLoop(playLoop), + m_agc(agc), + m_agcTime(agcTime), + m_agcThreshold(agcThreshold), + m_agcThresholdGate(agcThresholdGate), + m_agcThresholdDelay(agcThresholdDelay) { } }; @@ -313,6 +348,12 @@ private: bool m_dsb; bool m_audioMute; bool m_playLoop; + bool m_agc; + int m_agcTime; + bool m_agcThresholdEnable; + double m_agcThreshold; + int m_agcThresholdGate; + int m_agcThresholdDelay; Config() : m_outputSampleRate(0), @@ -328,7 +369,13 @@ private: m_audioFlipChannels(false), m_dsb(false), m_audioMute(false), - m_playLoop(false) + m_playLoop(false), + m_agc(false), + m_agcTime(9600), + m_agcThresholdEnable(true), + m_agcThreshold(1e-4), + m_agcThresholdGate(192), + m_agcThresholdDelay(2400) { } }; @@ -365,7 +412,6 @@ private: double m_magsq; MovingAverage m_movingAverage; - SimpleAGC m_volumeAGC; AudioVector m_audioBuffer; uint m_audioBufferFill; @@ -386,6 +432,8 @@ private: CWKeyer m_cwKeyer; CWSmoother m_cwSmoother; + MagAGC m_inAGC; + static const int m_levelNbSamples; void apply(); diff --git a/plugins/channeltx/modssb/ssbmodgui.cpp b/plugins/channeltx/modssb/ssbmodgui.cpp index e9f0716e3..cdc03fc68 100644 --- a/plugins/channeltx/modssb/ssbmodgui.cpp +++ b/plugins/channeltx/modssb/ssbmodgui.cpp @@ -36,6 +36,18 @@ const QString SSBModGUI::m_channelID = "sdrangel.channeltx.modssb"; +const int SSBModGUI::m_agcTimeConstant[] = { + 1, + 2, + 5, + 10, + 20, + 50, + 100, + 200, + 500, + 990}; + SSBModGUI* SSBModGUI::create(PluginAPI* pluginAPI, DeviceSinkAPI *deviceAPI) { SSBModGUI* gui = new SSBModGUI(pluginAPI, deviceAPI); @@ -105,6 +117,11 @@ QByteArray SSBModGUI::serialize() const s.writeBool(9, ui->audioBinaural->isChecked()); s.writeBool(10, ui->audioFlipChannels->isChecked()); s.writeBool(11, ui->dsb->isChecked()); + s.writeBool(12, ui->agc->isChecked()); + s.writeS32(13, ui->agcTime->value()); + s.writeS32(14, ui->agcThreshold->value()); + s.writeS32(15, ui->agcThresholdGate->value()); + s.writeS32(16, ui->agcThresholdDelay->value()); return s.final(); } @@ -158,6 +175,18 @@ bool SSBModGUI::deserialize(const QByteArray& data) ui->audioFlipChannels->setChecked(booltmp); d.readBool(11, &booltmp); ui->dsb->setChecked(booltmp); + d.readBool(12, &booltmp, false); + ui->agc->setChecked(booltmp); + d.readS32(13, &tmp, 7); + ui->agcTime->setValue(tmp > 9 ? 9 : tmp); + d.readS32(14, &tmp, -40); + ui->agcThreshold->setValue(tmp); + d.readS32(15, &tmp, 4); + ui->agcThresholdGate->setValue(tmp); + d.readS32(16, &tmp, 5); + ui->agcThresholdDelay->setValue(tmp); + + displaySettings(); blockApplySettings(false); m_channelMarker.blockSignals(false); @@ -399,6 +428,37 @@ void SSBModGUI::on_mic_toggled(bool checked) m_ssbMod->getInputMessageQueue()->push(message); } +void SSBModGUI::on_agc_stateChanged(int state __attribute((__unused__))) +{ + applySettings(); +} + +void SSBModGUI::on_agcTime_valueChanged(int value){ + QString s = QString::number(m_agcTimeConstant[value], 'f', 0); + ui->agcTimeText->setText(s); + applySettings(); +} + +void SSBModGUI::on_agcThreshold_valueChanged(int value) +{ + displayAGCPowerThreshold(value); + applySettings(); +} + +void SSBModGUI::on_agcThresholdGate_valueChanged(int value) +{ + QString s = QString::number(value, 'f', 0); + ui->agcThresholdGateText->setText(s); + applySettings(); +} + +void SSBModGUI::on_agcThresholdDelay_valueChanged(int value) +{ + QString s = QString::number(value * 10, 'f', 0); + ui->agcThresholdDelayText->setText(s); + applySettings(); +} + void SSBModGUI::on_navTimeSlider_valueChanged(int value) { if (m_enableNavTime && ((value >= 0) && (value <= 100))) @@ -507,6 +567,7 @@ SSBModGUI::SSBModGUI(PluginAPI* pluginAPI, DeviceSinkAPI *deviceAPI, QWidget* pa ui->cwKeyerGUI->setBuddies(m_ssbMod->getInputMessageQueue(), m_ssbMod->getCWKeyer()); ui->spectrumGUI->setBuddies(m_spectrumVis->getInputMessageQueue(), m_spectrumVis, ui->glSpectrum); + displaySettings(); applySettings(); setNewRate(m_spanLog2); @@ -629,10 +690,39 @@ void SSBModGUI::applySettings() ui->audioFlipChannels->isChecked(), ui->dsb->isChecked(), ui->audioMute->isChecked(), - ui->playLoop->isChecked()); + ui->playLoop->isChecked(), + ui->agc->isChecked(), + m_agcTimeConstant[ui->agcTime->value()], + ui->agcThreshold->value(), + ui->agcThresholdGate->value(), + ui->agcThresholdDelay->value() * 10); } } +void SSBModGUI::displaySettings() +{ + QString s = QString::number(m_agcTimeConstant[ui->agcTime->value()], 'f', 0); + ui->agcTimeText->setText(s); + displayAGCPowerThreshold(ui->agcThreshold->value()); + s = QString::number(ui->agcThresholdGate->value(), 'f', 0); + ui->agcThresholdGateText->setText(s); + s = QString::number(ui->agcThresholdDelay->value() * 10, 'f', 0); + ui->agcThresholdDelayText->setText(s); +} + +void SSBModGUI::displayAGCPowerThreshold(int value) +{ + if (value == -99) + { + ui->agcThresholdText->setText("---"); + } + else + { + QString s = QString::number(value, 'f', 0); + ui->agcThresholdText->setText(s); + } +} + void SSBModGUI::leaveEvent(QEvent*) { blockApplySettings(true); diff --git a/plugins/channeltx/modssb/ssbmodgui.h b/plugins/channeltx/modssb/ssbmodgui.h index 16c32df8c..af43cf219 100644 --- a/plugins/channeltx/modssb/ssbmodgui.h +++ b/plugins/channeltx/modssb/ssbmodgui.h @@ -71,6 +71,11 @@ private slots: void on_tone_toggled(bool checked); void on_toneFrequency_valueChanged(int value); void on_mic_toggled(bool checked); + void on_agc_stateChanged(int state); + void on_agcTime_valueChanged(int value); + void on_agcThreshold_valueChanged(int value); + void on_agcThresholdGate_valueChanged(int value); + void on_agcThresholdDelay_valueChanged(int value); void on_play_toggled(bool checked); void on_playLoop_toggled(bool checked); void on_morseKeyer_toggled(bool checked); @@ -107,6 +112,7 @@ private: std::size_t m_tickCount; bool m_enableNavTime; SSBMod::SSBModInputAF m_modAFInput; + static const int m_agcTimeConstant[]; //!< time constant index to value in ms explicit SSBModGUI(PluginAPI* pluginAPI, DeviceSinkAPI *deviceAPI, QWidget* parent = NULL); virtual ~SSBModGUI(); @@ -116,6 +122,8 @@ private: void blockApplySettings(bool block); void applySettings(); + void displaySettings(); + void displayAGCPowerThreshold(int value); void updateWithStreamData(); void updateWithStreamTime(); diff --git a/plugins/channeltx/modssb/ssbmodgui.ui b/plugins/channeltx/modssb/ssbmodgui.ui index 602ba8fda..2c619f820 100644 --- a/plugins/channeltx/modssb/ssbmodgui.ui +++ b/plugins/channeltx/modssb/ssbmodgui.ui @@ -463,6 +463,204 @@ + + + + + + Toggle audio compressor + + + Cmp + + + + + + + + 24 + 24 + + + + Compressor time constant (attack) + + + 0 + + + 9 + + + 1 + + + 4 + + + + + + + + 25 + 0 + + + + Compressor time constant in ms + + + 000 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 24 + 24 + + + + Audio squelch threshold + + + -99 + + + 0 + + + 1 + + + -40 + + + + + + + + 14 + 0 + + + + Audio squelch threshold (dB power) + + + -00 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 24 + 24 + + + + Audio squelch gate + + + 1 + + + 4 + + + + + + + + 18 + 0 + + + + Audio squelch gate in ms + + + 00 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 24 + 24 + + + + Audio squelch delay (release) + + + 1 + + + 5 + + + + + + + + 25 + 0 + + + + Audio squelch delay in ms + + + 000 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + diff --git a/sdrbase/dsp/agc.cpp b/sdrbase/dsp/agc.cpp index bfffeee69..6f99fc90c 100644 --- a/sdrbase/dsp/agc.cpp +++ b/sdrbase/dsp/agc.cpp @@ -53,7 +53,11 @@ MagAGC::MagAGC(int historySize, double R, double threshold) : m_stepDelta(1.0/m_stepLength), m_stepUpCounter(0), m_stepDownCounter(m_stepLength), - m_gateCounter(0) + m_gateCounter(0), + m_stepDownDelay(historySize), + m_clamping(false), + m_R2(R*R), + m_clampMax(1.0) {} MagAGC::~MagAGC() @@ -61,6 +65,7 @@ MagAGC::~MagAGC() void MagAGC::resize(int historySize, Real R) { + m_R2 = R*R; m_stepLength = std::min(StepLengthMax, historySize/2); m_stepDelta = 1.0 / m_stepLength; m_stepUpCounter = 0; @@ -88,7 +93,24 @@ double MagAGC::feedAndGetValue(const Complex& ci) { m_magsq = ci.real()*ci.real() + ci.imag()*ci.imag(); m_moving_average.feed(m_magsq); - m_u0 = m_R / (m_squared ? m_moving_average.average() : sqrt(m_moving_average.average())); + + if (m_clamping) + { + if (m_squared) + { + double u0 = m_R / m_moving_average.average(); + m_u0 = (u0 * m_magsq > m_clampMax) ? m_clampMax / m_magsq : u0; + } + else + { + double u02 = m_R2 / m_moving_average.average(); + m_u0 = (u02 * m_magsq > m_clampMax) ? m_clampMax / sqrt(m_magsq) : sqrt(u02); + } + } + else + { + m_u0 = m_R / (m_squared ? m_moving_average.average() : sqrt(m_moving_average.average())); + } if (m_thresholdEnable) { @@ -105,14 +127,14 @@ double MagAGC::feedAndGetValue(const Complex& ci) } else { - if (m_count < m_moving_average.historySize()) { + if (m_count < m_stepDownDelay) { m_count++; } m_gateCounter = 0; } - if (m_count < m_moving_average.historySize()) + if (m_count < m_stepDownDelay) { m_stepDownCounter = m_stepUpCounter; diff --git a/sdrbase/dsp/agc.h b/sdrbase/dsp/agc.h index 588516c04..332bec81e 100644 --- a/sdrbase/dsp/agc.h +++ b/sdrbase/dsp/agc.h @@ -22,11 +22,11 @@ public: virtual void feed(Complex& ci) = 0; protected: - double m_u0; - double m_R; // objective mag - MovingAverage m_moving_average; // Averaging engine. The stack length conditions the smoothness of AGC. - int m_historySize; - int m_count; + double m_u0; //!< AGC factor + double m_R; //!< objective mag + MovingAverage m_moving_average; //!< Averaging engine. The stack length conditions the smoothness of AGC. + int m_historySize; //!< Averaging length (attack) + int m_count; //!< Samples counter }; @@ -43,6 +43,9 @@ public: void setThreshold(double threshold) { m_threshold = threshold; } void setThresholdEnable(bool enable); void setGate(int gate) { m_gate = gate; } + void setStepDownDelay(int stepDownDelay) { m_stepDownDelay = stepDownDelay; } + void setClamping(bool clamping) { m_clamping = clamping; } + void setClampMax(double clampMax) { m_clampMax = clampMax; } private: bool m_squared; //!< use squared magnitude (power) to compute AGC value double m_magsq; //!< current squared magnitude (power) @@ -54,6 +57,10 @@ private: int m_stepUpCounter; //!< step up transition samples counter int m_stepDownCounter; //!< step down transition samples counter int m_gateCounter; //!< threshold gate samples counter + int m_stepDownDelay; //!< delay in samples before cutoff (release) + bool m_clamping; //!< clamping active + double m_R2; //!< square of objective magnitude + double m_clampMax; //!< maximum to clamp to };