diff --git a/src/engine/enginechannel.cpp b/src/engine/enginechannel.cpp index bcd1693f0c70..bb05ce914230 100644 --- a/src/engine/enginechannel.cpp +++ b/src/engine/enginechannel.cpp @@ -21,8 +21,10 @@ #include "control/controlpushbutton.h" EngineChannel::EngineChannel(const ChannelHandleAndGroup& handle_group, - EngineChannel::ChannelOrientation defaultOrientation) - : m_group(handle_group) { + EngineChannel::ChannelOrientation defaultOrientation, + bool isTalkoverChannel) + : m_group(handle_group), + m_bIsTalkoverChannel(isTalkoverChannel) { m_pPFL = new ControlPushButton(ConfigKey(getGroup(), "pfl")); m_pPFL->setButtonMode(ControlPushButton::TOGGLE); m_pMaster = new ControlPushButton(ConfigKey(getGroup(), "master")); diff --git a/src/engine/enginechannel.h b/src/engine/enginechannel.h index 2f55e6faf5c9..6c6478a90046 100644 --- a/src/engine/enginechannel.h +++ b/src/engine/enginechannel.h @@ -39,7 +39,8 @@ class EngineChannel : public EngineObject { }; EngineChannel(const ChannelHandleAndGroup& handle_group, - ChannelOrientation defaultOrientation = CENTER); + ChannelOrientation defaultOrientation = CENTER, + bool isTalkoverChannel = false); virtual ~EngineChannel(); virtual ChannelOrientation getOrientation() const; @@ -59,6 +60,7 @@ class EngineChannel : public EngineObject { virtual bool isMasterEnabled() const; void setTalkover(bool enabled); virtual bool isTalkoverEnabled() const; + inline bool isTalkoverChannel() { return m_bIsTalkoverChannel; }; virtual void process(CSAMPLE* pOut, const int iBufferSize) = 0; virtual void postProcess(const int iBuffersize) = 0; @@ -82,6 +84,8 @@ class EngineChannel : public EngineObject { ControlPushButton* m_pOrientationRight; ControlPushButton* m_pOrientationCenter; ControlPushButton* m_pTalkover; + bool m_bIsTalkoverChannel; }; #endif + diff --git a/src/engine/enginedelay.cpp b/src/engine/enginedelay.cpp index 3e8acfaa7e22..a4d63a9baba9 100644 --- a/src/engine/enginedelay.cpp +++ b/src/engine/enginedelay.cpp @@ -18,18 +18,22 @@ #include "control/controlproxy.h" #include "control/controlpotmeter.h" +#include "util/audiosignal.h" #include "util/assert.h" #include "util/sample.h" -const int kiMaxDelay = 40000; // 208 ms @ 96 kb/s -const double kdMaxDelayPot = 200; // 200 ms +namespace { +constexpr double kdMaxDelayPot = 500; +constexpr int kiMaxDelay = (kdMaxDelayPot + 8) / 1000 * + mixxx::AudioSignal::kSamplingRateMax * mixxx::AudioSignal::kChannelCountStereo; +} // anonymous namespace -EngineDelay::EngineDelay(const char* group, ConfigKey delayControl) +EngineDelay::EngineDelay(const char* group, ConfigKey delayControl, bool bPersist) : m_iDelayPos(0), m_iDelay(0) { m_pDelayBuffer = SampleUtil::alloc(kiMaxDelay); SampleUtil::clear(m_pDelayBuffer, kiMaxDelay); - m_pDelayPot = new ControlPotmeter(delayControl, 0, kdMaxDelayPot, false, true, false, true); + m_pDelayPot = new ControlPotmeter(delayControl, 0, kdMaxDelayPot, false, true, false, bPersist); m_pDelayPot->setDefaultValue(0); connect(m_pDelayPot, SIGNAL(valueChanged(double)), this, SLOT(slotDelayChanged()), Qt::DirectConnection); @@ -81,3 +85,7 @@ void EngineDelay::process(CSAMPLE* pInOut, const int iBufferSize) { } } } + +void EngineDelay::setDelay(double newDelay) { + m_pDelayPot->set(newDelay); +} diff --git a/src/engine/enginedelay.h b/src/engine/enginedelay.h index 6ee01bfeb65c..8fab4b867ef3 100644 --- a/src/engine/enginedelay.h +++ b/src/engine/enginedelay.h @@ -26,11 +26,13 @@ class ControlProxy; class EngineDelay : public EngineObject { Q_OBJECT public: - EngineDelay(const char* group, ConfigKey delayControl); + EngineDelay(const char* group, ConfigKey delayControl, bool bPersist = true); virtual ~EngineDelay(); void process(CSAMPLE* pInOut, const int iBufferSize); + void setDelay(double newDelay); + public slots: void slotDelayChanged(); diff --git a/src/engine/enginemaster.cpp b/src/engine/enginemaster.cpp index 9145a5e09111..3218ed1c31b0 100644 --- a/src/engine/enginemaster.cpp +++ b/src/engine/enginemaster.cpp @@ -36,8 +36,8 @@ EngineMaster::EngineMaster(UserSettingsPointer pConfig, bool bRampingGain) : m_pEngineEffectsManager(pEffectsManager ? pEffectsManager->getEngineEffectsManager() : NULL), m_bRampingGain(bRampingGain), - m_ppSidechain(&m_pTalkover), m_masterGainOld(0.0), + m_boothGainOld(0.0), m_headphoneMasterGainOld(0.0), m_headphoneGainOld(1.0), m_masterHandle(registerChannelGroup("[Master]")), @@ -48,6 +48,7 @@ EngineMaster::EngineMaster(UserSettingsPointer pConfig, m_bBusOutputConnected[EngineChannel::LEFT] = false; m_bBusOutputConnected[EngineChannel::CENTER] = false; m_bBusOutputConnected[EngineChannel::RIGHT] = false; + m_bExternalRecordBroadcastInputConnected = false; m_pWorkerScheduler = new EngineWorkerScheduler(this); m_pWorkerScheduler->start(QThread::HighPriority); @@ -87,6 +88,9 @@ EngineMaster::EngineMaster(UserSettingsPointer pConfig, // Master gain m_pMasterGain = new ControlAudioTaperPot(ConfigKey(group, "gain"), -14, 14, 0.5); + // Booth gain + m_pBoothGain = new ControlAudioTaperPot(ConfigKey(group, "booth_gain"), -14, 14, 0.5); + // Legacy: the master "gain" control used to be named "volume" in Mixxx // 1.11.0 and earlier. See Bug #1306253. ControlDoublePrivate::insertAlias(ConfigKey(group, "volume"), @@ -97,6 +101,10 @@ EngineMaster::EngineMaster(UserSettingsPointer pConfig, m_pMasterDelay = new EngineDelay(group, ConfigKey(group, "delay")); m_pHeadDelay = new EngineDelay(group, ConfigKey(group, "headDelay")); + m_pBoothDelay = new EngineDelay(group, ConfigKey(group, "boothDelay")); + m_pLatencyCompensationDelay = new EngineDelay(group, + ConfigKey(group, "microphoneLatencyCompensation")); + m_pNumMicsConfigured = new ControlObject(ConfigKey(group, "num_mics_configured")); // Headphone volume m_pHeadGain = new ControlAudioTaperPot(ConfigKey(group, "headGain"), -14, 14, 0.5); @@ -121,10 +129,16 @@ EngineMaster::EngineMaster(UserSettingsPointer pConfig, // Allocate buffers m_pHead = SampleUtil::alloc(MAX_BUFFER_LEN); m_pMaster = SampleUtil::alloc(MAX_BUFFER_LEN); + m_pBooth = SampleUtil::alloc(MAX_BUFFER_LEN); m_pTalkover = SampleUtil::alloc(MAX_BUFFER_LEN); + m_pTalkoverHeadphones = SampleUtil::alloc(MAX_BUFFER_LEN); + m_pSidechainMix = SampleUtil::alloc(MAX_BUFFER_LEN); SampleUtil::clear(m_pHead, MAX_BUFFER_LEN); SampleUtil::clear(m_pMaster, MAX_BUFFER_LEN); + SampleUtil::clear(m_pBooth, MAX_BUFFER_LEN); SampleUtil::clear(m_pTalkover, MAX_BUFFER_LEN); + SampleUtil::clear(m_pTalkoverHeadphones, MAX_BUFFER_LEN); + SampleUtil::clear(m_pSidechainMix, MAX_BUFFER_LEN); // Setup the output buses for (int o = EngineChannel::LEFT; o <= EngineChannel::RIGHT; ++o) { @@ -132,6 +146,8 @@ EngineMaster::EngineMaster(UserSettingsPointer pConfig, SampleUtil::clear(m_pOutputBusBuffers[o], MAX_BUFFER_LEN); } + m_ppSidechainOutput = &m_pMaster; + // Starts a thread for recording and broadcast m_pEngineSideChain = bEnableSidechain ? new EngineSideChain(pConfig) : NULL; @@ -155,14 +171,18 @@ EngineMaster::EngineMaster(UserSettingsPointer pConfig, m_pKeylockEngine->set(pConfig->getValueString( ConfigKey(group, "keylock_engine")).toDouble()); + // TODO: Make this read only and make EngineMaster decide whether + // processing the master mix is necessary. m_pMasterEnabled = new ControlObject(ConfigKey(group, "enabled"), true, false, true); // persist = true + m_pBoothEnabled = new ControlObject(ConfigKey(group, "booth_enabled")); + m_pBoothEnabled->setReadOnly(); m_pMasterMonoMixdown = new ControlObject(ConfigKey(group, "mono_mixdown"), true, false, true); // persist = true - m_pMasterTalkoverMix = new ControlObject(ConfigKey(group, "talkover_mix"), + m_pMicMonitorMode = new ControlObject(ConfigKey(group, "talkover_mix"), true, false, true); // persist = true m_pHeadphoneEnabled = new ControlObject(ConfigKey(group, "headEnabled")); - m_pHeadphoneEnabled = new ControlObject(ConfigKey(group, "sidechainEnabled")); + m_pHeadphoneEnabled->setReadOnly(); // Note: the EQ Rack is set in EffectsManager::setupDefaults(); } @@ -175,12 +195,16 @@ EngineMaster::~EngineMaster() { delete m_pHeadMix; delete m_pHeadSplitEnabled; delete m_pMasterGain; + delete m_pBoothGain; delete m_pHeadGain; delete m_pTalkoverDucking; delete m_pVumeter; delete m_pEngineSideChain; delete m_pMasterDelay; delete m_pHeadDelay; + delete m_pBoothDelay; + delete m_pLatencyCompensationDelay; + delete m_pNumMicsConfigured; delete m_pXFaderReverse; delete m_pXFaderCalibration; @@ -197,12 +221,14 @@ EngineMaster::~EngineMaster() { delete m_pMasterEnabled; delete m_pMasterMonoMixdown; - delete m_pMasterTalkoverMix; + delete m_pMicMonitorMode; delete m_pHeadphoneEnabled; SampleUtil::free(m_pHead); SampleUtil::free(m_pMaster); + SampleUtil::free(m_pBooth); SampleUtil::free(m_pTalkover); + SampleUtil::free(m_pTalkoverHeadphones); for (int o = EngineChannel::LEFT; o <= EngineChannel::RIGHT; o++) { SampleUtil::free(m_pOutputBusBuffers[o]); } @@ -223,12 +249,16 @@ const CSAMPLE* EngineMaster::getMasterBuffer() const { return m_pMaster; } +const CSAMPLE* EngineMaster::getBoothBuffer() const { + return m_pBooth; +} + const CSAMPLE* EngineMaster::getHeadphoneBuffer() const { return m_pHead; } const CSAMPLE* EngineMaster::getSidechainBuffer() const { - return *m_ppSidechain; + return *m_ppSidechainOutput; } void EngineMaster::processChannels(int iBufferSize) { @@ -337,8 +367,12 @@ void EngineMaster::process(const int iBufferSize) { Trace t("EngineMaster::process"); bool masterEnabled = m_pMasterEnabled->get(); + bool boothEnabled = m_pBoothEnabled->get(); bool headphoneEnabled = m_pHeadphoneEnabled->get(); + // TODO: remove assumption of stereo buffer + const unsigned int kChannels = 2; + const unsigned int iFrames = iBufferSize / kChannels; unsigned int iSampleRate = static_cast(m_pMasterSampleRate->get()); if (m_pEngineEffectsManager) { m_pEngineEffectsManager->onCallbackStart(); @@ -366,16 +400,27 @@ void EngineMaster::process(const int iBufferSize) { // Mix all the PFL enabled channels together. m_headphoneGain.setGain(chead_gain); - if (m_bRampingGain) { - ChannelMixer::mixChannelsRamping( - m_headphoneGain, &m_activeHeadphoneChannels, - &m_channelHeadphoneGainCache, - m_pHead, iBufferSize); - } else { - ChannelMixer::mixChannels( - m_headphoneGain, &m_activeHeadphoneChannels, - &m_channelHeadphoneGainCache, - m_pHead, iBufferSize); + if (headphoneEnabled) { + if (m_bRampingGain) { + ChannelMixer::mixChannelsRamping( + m_headphoneGain, &m_activeHeadphoneChannels, + &m_channelHeadphoneGainCache, + m_pHead, iBufferSize); + } else { + ChannelMixer::mixChannels( + m_headphoneGain, &m_activeHeadphoneChannels, + &m_channelHeadphoneGainCache, + m_pHead, iBufferSize); + } + + // Process headphone channel effects + if (m_pEngineEffectsManager) { + GroupFeatureState headphoneFeatures; + m_pEngineEffectsManager->process(m_headphoneHandle.handle(), + m_pHead, + iBufferSize, iSampleRate, + headphoneFeatures); + } } // Mix all the talkover enabled channels together. @@ -398,20 +443,20 @@ void EngineMaster::process(const int iBufferSize) { } // Calculate the crossfader gains for left and right side of the crossfader - double c1_gain, c2_gain; + double crossfaderLeftGain, crossfaderRightGain; EngineXfader::getXfadeGains(m_pCrossfader->get(), m_pXFaderCurve->get(), m_pXFaderCalibration->get(), m_pXFaderMode->get(), m_pXFaderReverse->toBool(), - &c1_gain, &c2_gain); + &crossfaderLeftGain, &crossfaderRightGain); - // All other channels should be adjusted by ducking gain. - // The talkover channels are mixed in later - m_masterGain.setGains(m_pTalkoverDucking->getGain(iBufferSize / 2), - c1_gain, 1.0, c2_gain); + m_masterGain.setGains(crossfaderLeftGain, 1.0, crossfaderRightGain, + m_pTalkoverDucking->getGain(iBufferSize / 2)); - // Make the mix for each output bus. m_masterGain takes care of applying the - // master volume, the channel volume, and the orientation gain. + // Make the mix for each crossfader orientation output bus. + // m_masterGain takes care of applying the attenuation from + // channel volume faders, crossfader, and talkover ducking. + // Talkover is mixed in later according to the configured MicMonitorMode for (int o = EngineChannel::LEFT; o <= EngineChannel::RIGHT; o++) { if (m_bRampingGain) { ChannelMixer::mixChannelsRamping( @@ -428,7 +473,7 @@ void EngineMaster::process(const int iBufferSize) { } } - // Process master channel effects + // Process crossfader orientation bus channel effects if (m_pEngineEffectsManager) { GroupFeatureState busFeatures; m_pEngineEffectsManager->process(m_busLeftHandle.handle(), @@ -443,48 +488,184 @@ void EngineMaster::process(const int iBufferSize) { } if (masterEnabled) { - // Mix the three channels together. We already mixed the busses together - // with the channel gains and overall master gain. - if (!m_pMasterTalkoverMix->toBool()) { - // Add Talkover to Master output - SampleUtil::copy4WithGain(m_pMaster, - m_pOutputBusBuffers[EngineChannel::LEFT], 1.0, - m_pOutputBusBuffers[EngineChannel::CENTER], 1.0, - m_pOutputBusBuffers[EngineChannel::RIGHT], 1.0, + // Mix the crossfader orientation buffers together into the master mix + SampleUtil::copy3WithGain(m_pMaster, + m_pOutputBusBuffers[EngineChannel::LEFT], 1.0, + m_pOutputBusBuffers[EngineChannel::CENTER], 1.0, + m_pOutputBusBuffers[EngineChannel::RIGHT], 1.0, + iBufferSize); + + MicMonitorMode configuredMicMonitorMode = static_cast( + static_cast(m_pMicMonitorMode->get())); + + // Process master, booth, and record/broadcast buffers according to the + // MicMonitorMode configured in DlgPrefSound + // TODO(Be): make SampleUtil ramping functions update the old gain variable + if (configuredMicMonitorMode == MicMonitorMode::MASTER) { + // Process master channel effects + // TODO(Be): Move this after mixing in talkover. To apply master effects + // to both the master and booth in that case will require refactoring + // the effects system to be able to process the same effects on multiple + // buffers within the same callback. + applyMasterEffects(iBufferSize, iSampleRate); + + // Copy master mix to booth output with booth gain before mixing + // talkover with master mix + if (boothEnabled) { + CSAMPLE boothGain = m_pBoothGain->get(); + if (m_bRampingGain) { + SampleUtil::copy1WithRampingGain(m_pBooth, m_pMaster, + m_boothGainOld, boothGain, iBufferSize); + } else { + SampleUtil::copy1WithGain(m_pBooth, m_pMaster, + boothGain, iBufferSize); + } + m_boothGainOld = boothGain; + } + + // Mix talkover into master mix + if (m_pNumMicsConfigured->get() > 0) { + SampleUtil::copy2WithGain(m_pMaster, + m_pMaster, 1.0, m_pTalkover, 1.0, iBufferSize); - } else { - SampleUtil::copy3WithGain(m_pMaster, - m_pOutputBusBuffers[EngineChannel::LEFT], 1.0, - m_pOutputBusBuffers[EngineChannel::CENTER], 1.0, - m_pOutputBusBuffers[EngineChannel::RIGHT], 1.0, + } + + // Apply master gain + // TODO(Be): make this not affect the headphones. Refer to + // https://bugs.launchpad.net/mixxx/+bug/1458213 + CSAMPLE master_gain = m_pMasterGain->get(); + if (m_bRampingGain) { + SampleUtil::applyRampingGain(m_pMaster, m_masterGainOld, master_gain, + iBufferSize); + } else { + SampleUtil::applyGain(m_pMaster, master_gain, iBufferSize); + } + m_masterGainOld = master_gain; + + // Record/broadcast signal is the same as the master output + m_ppSidechainOutput = &m_pMaster; + } else if (configuredMicMonitorMode == MicMonitorMode::MASTER_AND_BOOTH) { + // Process master channel effects + // TODO(Be): Move this after mixing in talkover. For the MASTER only + // MicMonitorMode above, that will require refactoring the effects system + // to be able to process the same effects on different buffers + // within the same callback. For consistency between the MicMonitorModes, + // process master effects here before mixing in talkover. + applyMasterEffects(iBufferSize, iSampleRate); + + // Mix talkover with master + if (m_pNumMicsConfigured->get() > 0) { + SampleUtil::copy2WithGain(m_pMaster, + m_pMaster, 1.0, + m_pTalkover, 1.0, iBufferSize); - } + } - // Process master channel effects - if (m_pEngineEffectsManager) { - GroupFeatureState masterFeatures; - // Well, this is delayed by one buffer (it's dependent on the - // output). Oh well. - if (m_pVumeter != NULL) { - m_pVumeter->collectFeatures(&masterFeatures); + // Copy master mix (with talkover mixed in) to booth output with booth gain + if (boothEnabled) { + CSAMPLE boothGain = m_pBoothGain->get(); + if (m_bRampingGain) { + SampleUtil::copy1WithRampingGain(m_pBooth, m_pMaster, + m_boothGainOld, boothGain, iBufferSize); + } else { + SampleUtil::copy1WithGain(m_pBooth, m_pMaster, + boothGain, iBufferSize); + } + m_boothGainOld = boothGain; + } + + // Apply master gain + // TODO(Be): make this not affect the headphones. Refer to + // https://bugs.launchpad.net/mixxx/+bug/1458213 + CSAMPLE master_gain = m_pMasterGain->get(); + if (m_bRampingGain) { + SampleUtil::applyRampingGain(m_pMaster, m_masterGainOld, master_gain, + iBufferSize); + } else { + SampleUtil::applyGain(m_pMaster, master_gain, iBufferSize); + } + m_masterGainOld = master_gain; + + // Record/broadcast signal is the same as the master output + m_ppSidechainOutput = &m_pMaster; + } else if (configuredMicMonitorMode == MicMonitorMode::DIRECT_MONITOR) { + // Skip mixing talkover with the master and booth outputs + // if using direct monitoring because it is being mixed in hardware + // without the latency of sending the signal into Mixxx for processing. + // However, include the talkover mix in the record/broadcast signal. + + // Copy master mix to booth output with booth gain + if (boothEnabled) { + CSAMPLE boothGain = m_pBoothGain->get(); + if (m_bRampingGain) { + SampleUtil::copy1WithRampingGain(m_pBooth, m_pMaster, + m_boothGainOld, boothGain, iBufferSize); + } else { + SampleUtil::copy1WithGain(m_pBooth, m_pMaster, + boothGain, iBufferSize); + } + m_boothGainOld = boothGain; + } + + // Process master channel effects + // NOTE(Be): This should occur before mixing in talkover for the + // record/broadcast signal so the record/broadcast signal is the same + // as what is heard on the master & booth outputs. + applyMasterEffects(iBufferSize, iSampleRate); + + // Apply master gain + // TODO(Be): make this not affect the headphones. Refer to + // https://bugs.launchpad.net/mixxx/+bug/1458213 + CSAMPLE master_gain = m_pMasterGain->get(); + if (m_bRampingGain) { + SampleUtil::applyRampingGain(m_pMaster, m_masterGainOld, master_gain, + iBufferSize); + } else { + SampleUtil::applyGain(m_pMaster, master_gain, iBufferSize); + } + m_masterGainOld = master_gain; + + // The talkover signal Mixxx receives is delayed by the round trip latency. + // There is an output latency between the time Mixxx processes the audio + // and the user hears it. So if the microphone user plays on beat with + // what they hear, they will be playing out of sync with the engine's + // processing by the output latency. Additionally, Mixxx gets input signals + // delayed by the input latency. By the time Mixxx receives the input signal, + // a full round trip through the signal chain has elapsed since Mixxx + // processed the output signal. + // Although Mixxx receives the input signal delayed, the user hears it mixed + // in hardware with the master & booth outputs without that + // latency, so to record/broadcast the same signal that is heard + // on the master & booth outputs, the master mix must be delayed before + // mixing the talkover signal for the record/broadcast mix. + // If not using microphone inputs or recording/broadcasting from + // a sound card input, skip unnecessary processing here. + if (m_pNumMicsConfigured->get() > 0 + && !m_bExternalRecordBroadcastInputConnected) { + // Copy the master mix to a separate buffer before delaying it + // to avoid delaying the master output. + SampleUtil::copy(m_pSidechainMix, m_pMaster, iBufferSize); + m_pLatencyCompensationDelay->process(m_pSidechainMix, iBufferSize); + SampleUtil::copy2WithGain(m_pSidechainMix, + m_pSidechainMix, 1.0, + m_pTalkover, 1.0, + iBufferSize); + m_ppSidechainOutput = &m_pSidechainMix; + } else { + m_ppSidechainOutput = &m_pMaster; } - masterFeatures.has_gain = true; - masterFeatures.gain = m_pMasterGain->get(); - m_pEngineEffectsManager->process(m_masterHandle.handle(), m_pMaster, - iBufferSize, iSampleRate, - masterFeatures); } - // Apply master gain after effects. - CSAMPLE master_gain = m_pMasterGain->get(); - if (m_bRampingGain) { - SampleUtil::applyRampingGain(m_pMaster, m_masterGainOld, - master_gain, iBufferSize); - } else { - SampleUtil::applyGain(m_pMaster, master_gain, iBufferSize); + // Submit buffer to the side chain to do broadcasting, recording, + // etc. (CPU intensive non-realtime tasks) + // If recording/broadcasting from a sound card input, + // SoundManager will send the input buffer from the sound card to m_pSidechain + // so skip sending a buffer to m_pSidechain here. + if (!m_bExternalRecordBroadcastInputConnected + && m_pEngineSideChain != nullptr) { + m_pEngineSideChain->writeSamples(*m_ppSidechainOutput, iFrames); } - m_masterGainOld = master_gain; // Balance values CSAMPLE balright = 1.; @@ -499,30 +680,10 @@ void EngineMaster::process(const int iBufferSize) { // Perform balancing on main out SampleUtil::applyAlternatingGain(m_pMaster, balleft, balright, iBufferSize); - // Submit master samples to the side chain to do broadcasting, recording, - // etc. (cpu intensive non-realtime tasks) - if (m_pEngineSideChain != NULL) { - if (m_pMasterTalkoverMix->toBool()) { - // Add Master and Talkover to Sidechain output, re-use the - // talkover buffer - // Note: m_ppSidechain = &m_pTalkover; - SampleUtil::addWithGain(m_pTalkover, - m_pMaster, 1.0, - iBufferSize); - } else { - // Just Copy Master to Sidechain since we have already added - // Talkover above - SampleUtil::copy(*m_ppSidechain, - m_pMaster, - iBufferSize); - } - m_pEngineSideChain->writeSamples(*m_ppSidechain, iBufferSize); - } - // Update VU meter (it does not return anything). Needs to be here so that // master balance and talkover is reflected in the VU meter. if (m_pVumeter != NULL) { - m_pVumeter->process(*m_ppSidechain, iBufferSize); + m_pVumeter->process(m_pMaster, iBufferSize); } // Add master to headphone with appropriate gain @@ -583,12 +744,31 @@ void EngineMaster::process(const int iBufferSize) { if (headphoneEnabled) { m_pHeadDelay->process(m_pHead, iBufferSize); } + if (boothEnabled) { + m_pBoothDelay->process(m_pBooth, iBufferSize); + } // We're close to the end of the callback. Wake up the engine worker // scheduler so that it runs the workers. m_pWorkerScheduler->runWorkers(); } +void EngineMaster::applyMasterEffects(const int iBufferSize, const int iSampleRate) { + if (m_pEngineEffectsManager) { + GroupFeatureState masterFeatures; + // Well, this is delayed by one buffer (it's dependent on the + // output). Oh well. + if (m_pVumeter != NULL) { + m_pVumeter->collectFeatures(&masterFeatures); + } + masterFeatures.has_gain = true; + masterFeatures.gain = m_pMasterGain->get(); + m_pEngineEffectsManager->process(m_masterHandle.handle(), m_pMaster, + iBufferSize, iSampleRate, + masterFeatures); + } +} + void EngineMaster::addChannel(EngineChannel* pChannel) { ChannelInfo* pChannelInfo = new ChannelInfo(m_channels.size()); pChannelInfo->m_pChannel = pChannel; @@ -660,6 +840,9 @@ const CSAMPLE* EngineMaster::buffer(AudioOutput output) const { case AudioOutput::MASTER: return getMasterBuffer(); break; + case AudioOutput::BOOTH: + return getBoothBuffer(); + break; case AudioOutput::HEADPHONES: return getHeadphoneBuffer(); break; @@ -669,7 +852,7 @@ const CSAMPLE* EngineMaster::buffer(AudioOutput output) const { case AudioOutput::DECK: return getDeckBuffer(output.getIndex()); break; - case AudioOutput::SIDECHAIN: + case AudioOutput::RECORD_BROADCAST: return getSidechainBuffer(); break; default: @@ -681,10 +864,15 @@ void EngineMaster::onOutputConnected(AudioOutput output) { switch (output.getType()) { case AudioOutput::MASTER: // overwrite config option if a master output is configured - m_pMasterEnabled->set(1.0); + m_pMasterEnabled->forceSet(1.0); break; case AudioOutput::HEADPHONES: - m_pHeadphoneEnabled->set(1.0); + m_pMasterEnabled->forceSet(1.0); + m_pHeadphoneEnabled->forceSet(1.0); + break; + case AudioOutput::BOOTH: + m_pMasterEnabled->forceSet(1.0); + m_pBoothEnabled->forceSet(1.0); break; case AudioOutput::BUS: m_bBusOutputConnected[output.getIndex()] = true; @@ -692,7 +880,7 @@ void EngineMaster::onOutputConnected(AudioOutput output) { case AudioOutput::DECK: // We don't track enabled decks. break; - case AudioOutput::SIDECHAIN: + case AudioOutput::RECORD_BROADCAST: // We don't track enabled sidechain. break; default: @@ -706,8 +894,11 @@ void EngineMaster::onOutputDisconnected(AudioOutput output) { // not used, because we need the master buffer for headphone mix // and recording/broadcasting as well break; + case AudioOutput::BOOTH: + m_pBoothEnabled->forceSet(0.0); + break; case AudioOutput::HEADPHONES: - m_pHeadphoneEnabled->set(0.0); + m_pHeadphoneEnabled->forceSet(0.0); break; case AudioOutput::BUS: m_bBusOutputConnected[output.getIndex()] = false; @@ -715,10 +906,61 @@ void EngineMaster::onOutputDisconnected(AudioOutput output) { case AudioOutput::DECK: // We don't track enabled decks. break; - case AudioOutput::SIDECHAIN: + case AudioOutput::RECORD_BROADCAST: // We don't track enabled sidechain. break; default: break; } } + +void EngineMaster::onInputConnected(AudioInput input) { + switch (input.getType()) { + case AudioInput::MICROPHONE: + m_pNumMicsConfigured->set(m_pNumMicsConfigured->get() + 1); + break; + case AudioInput::AUXILIARY: + // We don't track enabled auxiliary inputs. + break; + case AudioInput::VINYLCONTROL: + // We don't track enabled vinyl control inputs. + break; + case AudioInput::RECORD_BROADCAST: + m_bExternalRecordBroadcastInputConnected = true; + break; + default: + break; + } +} + +void EngineMaster::onInputDisconnected(AudioInput input) { + switch (input.getType()) { + case AudioInput::MICROPHONE: + m_pNumMicsConfigured->set(m_pNumMicsConfigured->get() - 1); + break; + case AudioInput::AUXILIARY: + // We don't track enabled auxiliary inputs. + break; + case AudioInput::VINYLCONTROL: + // We don't track enabled vinyl control inputs. + break; + case AudioInput::RECORD_BROADCAST: + m_bExternalRecordBroadcastInputConnected = false; + break; + default: + break; + } +} + +void EngineMaster::registerNonEngineChannelSoundIO(SoundManager* pSoundManager) { + pSoundManager->registerInput(AudioInput(AudioPath::RECORD_BROADCAST, 0, 2), + m_pEngineSideChain); + + pSoundManager->registerOutput(AudioOutput(AudioOutput::MASTER, 0, 2), this); + pSoundManager->registerOutput(AudioOutput(AudioOutput::HEADPHONES, 0, 2), this); + pSoundManager->registerOutput(AudioOutput(AudioOutput::BOOTH, 0, 2), this); + for (int o = EngineChannel::LEFT; o <= EngineChannel::RIGHT; o++) { + pSoundManager->registerOutput(AudioOutput(AudioOutput::BUS, 0, 2, o), this); + } + pSoundManager->registerOutput(AudioOutput(AudioOutput::RECORD_BROADCAST, 0, 2), this); +} diff --git a/src/engine/enginemaster.h b/src/engine/enginemaster.h index b6cc17f7f0d8..bdaacc93b510 100644 --- a/src/engine/enginemaster.h +++ b/src/engine/enginemaster.h @@ -27,6 +27,7 @@ #include "engine/engineobject.h" #include "engine/enginechannel.h" #include "engine/channelhandle.h" +#include "soundio/soundmanager.h" #include "soundio/soundmanagerutil.h" #include "recording/recordingmanager.h" @@ -90,6 +91,9 @@ class EngineMaster : public QObject, public AudioSource { m_channelHandleFactory.getOrCreateHandle(group), group); } + // Register the sound I/O that does not correspond to any EngineChannel object + void registerNonEngineChannelSoundIO(SoundManager* pSoundManager); + // WARNING: These methods are called by the main thread. They should only // touch the volatile bool connected indicators (see below). However, when // these methods are called the callback is guaranteed to be inactive @@ -97,6 +101,8 @@ class EngineMaster : public QObject, public AudioSource { // in the future. virtual void onOutputConnected(AudioOutput output); virtual void onOutputDisconnected(AudioOutput output); + void onInputConnected(AudioInput input); + void onInputDisconnected(AudioInput input); void process(const int iBufferSize); @@ -126,6 +132,7 @@ class EngineMaster : public QObject, public AudioSource { // These are really only exposed for tests to use. const CSAMPLE* getMasterBuffer() const; + const CSAMPLE* getBoothBuffer() const; const CSAMPLE* getHeadphoneBuffer() const; const CSAMPLE* getOutputBusBuffer(unsigned int i) const; const CSAMPLE* getDeckBuffer(unsigned int i) const; @@ -183,10 +190,10 @@ class EngineMaster : public QObject, public AudioSource { class OrientationVolumeGainCalculator : public GainCalculator { public: OrientationVolumeGainCalculator() - : m_dVolume(1.0), - m_dLeftGain(1.0), + : m_dLeftGain(1.0), m_dCenterGain(1.0), - m_dRightGain(1.0) { + m_dRightGain(1.0), + m_dTalkoverDuckingGain(1.0) { } inline double getGain(ChannelInfo* pChannelInfo) const { @@ -194,23 +201,34 @@ class EngineMaster : public QObject, public AudioSource { const double orientationGain = EngineMaster::gainForOrientation( pChannelInfo->m_pChannel->getOrientation(), m_dLeftGain, m_dCenterGain, m_dRightGain); - return m_dVolume * channelVolume * orientationGain; + return channelVolume * orientationGain * m_dTalkoverDuckingGain; } - inline void setGains(double dVolume, double leftGain, - double centerGain, double rightGain) { - m_dVolume = dVolume; + inline void setGains(double leftGain, double centerGain, double rightGain, + double talkoverDuckingGain) { m_dLeftGain = leftGain; m_dCenterGain = centerGain; m_dRightGain = rightGain; + m_dTalkoverDuckingGain = talkoverDuckingGain; } private: - double m_dVolume; double m_dLeftGain; double m_dCenterGain; double m_dRightGain; + double m_dTalkoverDuckingGain; + }; + + enum class MicMonitorMode { + // These are out of order with how they are listed in DlgPrefSound for backwards + // compatibility with Mixxx 2.0 user settings. In Mixxx 2.0, before the + // booth output was added, this was a binary option without + // the MASTER_AND_BOOTH mode. + MASTER = 0, + DIRECT_MONITOR, + MASTER_AND_BOOTH }; + template class FastVector { public: @@ -258,6 +276,12 @@ class EngineMaster : public QObject, public AudioSource { // The master buffer is protected so it can be accessed by test subclasses. CSAMPLE* m_pMaster; + // ControlObjects for switching off unnecessary processing + // These are protected so tests can set them + ControlObject* m_pHeadphoneEnabled; + ControlObject* m_pMasterEnabled; + ControlObject* m_pBoothEnabled; + private: void mixChannels(unsigned int channelBitvector, unsigned int maxChannels, CSAMPLE* pOutput, unsigned int iBufferSize, GainCalculator* pGainCalculator); @@ -269,6 +293,8 @@ class EngineMaster : public QObject, public AudioSource { // respective output. void processChannels(int iBufferSize); + void applyMasterEffects(const int iBufferSize, const int iSampleRate); + ChannelHandleFactory m_channelHandleFactory; EngineEffectsManager* m_pEngineEffectsManager; bool m_bRampingGain; @@ -290,25 +316,31 @@ class EngineMaster : public QObject, public AudioSource { // Mixing buffers for each output. CSAMPLE* m_pOutputBusBuffers[3]; + CSAMPLE* m_pBooth; CSAMPLE* m_pHead; CSAMPLE* m_pTalkover; - - CSAMPLE** m_ppSidechain; // points to master or to talkover buffer + CSAMPLE* m_pTalkoverHeadphones; + CSAMPLE* m_pSidechainMix; + CSAMPLE** m_ppSidechainOutput; EngineWorkerScheduler* m_pWorkerScheduler; EngineSync* m_pMasterSync; ControlObject* m_pMasterGain; + ControlObject* m_pBoothGain; ControlObject* m_pHeadGain; ControlObject* m_pMasterSampleRate; ControlObject* m_pMasterLatency; ControlObject* m_pMasterAudioBufferSize; ControlObject* m_pAudioLatencyOverloadCount; + ControlObject* m_pNumMicsConfigured; ControlPotmeter* m_pAudioLatencyUsage; ControlPotmeter* m_pAudioLatencyOverload; EngineTalkoverDucking* m_pTalkoverDucking; EngineDelay* m_pMasterDelay; EngineDelay* m_pHeadDelay; + EngineDelay* m_pBoothDelay; + EngineDelay* m_pLatencyCompensationDelay; EngineVuMeter* m_pVumeter; EngineSideChain* m_pEngineSideChain; @@ -327,6 +359,7 @@ class EngineMaster : public QObject, public AudioSource { TalkoverGainCalculator m_talkoverGain; OrientationVolumeGainCalculator m_masterGain; CSAMPLE m_masterGainOld; + CSAMPLE m_boothGainOld; CSAMPLE m_headphoneMasterGainOld; CSAMPLE m_headphoneGainOld; @@ -336,15 +369,12 @@ class EngineMaster : public QObject, public AudioSource { const ChannelHandleAndGroup m_busCenterHandle; const ChannelHandleAndGroup m_busRightHandle; - // Produce the Master Mixxx, not Required if connected to left - // and right Bus and no recording and broadcast active - ControlObject* m_pMasterEnabled; // Mix two Mono channels. This is useful for outdoor gigs ControlObject* m_pMasterMonoMixdown; - ControlObject* m_pMasterTalkoverMix; - ControlObject* m_pHeadphoneEnabled; + ControlObject* m_pMicMonitorMode; volatile bool m_bBusOutputConnected[3]; + bool m_bExternalRecordBroadcastInputConnected; }; #endif diff --git a/src/engine/enginemicrophone.cpp b/src/engine/enginemicrophone.cpp index d41458998790..c46db05b8df1 100644 --- a/src/engine/enginemicrophone.cpp +++ b/src/engine/enginemicrophone.cpp @@ -14,7 +14,7 @@ EngineMicrophone::EngineMicrophone(const ChannelHandleAndGroup& handle_group, EffectsManager* pEffectsManager) - : EngineChannel(handle_group, EngineChannel::CENTER), + : EngineChannel(handle_group, EngineChannel::CENTER, true), m_pEngineEffectsManager(pEffectsManager ? pEffectsManager->getEngineEffectsManager() : NULL), m_vuMeter(getGroup()), m_pInputConfigured(new ControlObject(ConfigKey(getGroup(), "input_configured"))), diff --git a/src/engine/sidechain/enginesidechain.cpp b/src/engine/sidechain/enginesidechain.cpp index 6081fd966bcf..3dbd3f45903d 100644 --- a/src/engine/sidechain/enginesidechain.cpp +++ b/src/engine/sidechain/enginesidechain.cpp @@ -75,11 +75,24 @@ void EngineSideChain::addSideChainWorker(SideChainWorker* pWorker) { m_workers.append(pWorker); } -void EngineSideChain::writeSamples(const CSAMPLE* newBuffer, int buffer_size) { +void EngineSideChain::receiveBuffer(AudioInput input, + const CSAMPLE* pBuffer, + unsigned int iFrames) { + if (input.getType() != AudioInput::RECORD_BROADCAST) { + qDebug() << "WARNING: AudioInput type is not RECORD_BROADCAST. Ignoring incoming buffer."; + return; + } + writeSamples(pBuffer, iFrames); +} + +void EngineSideChain::writeSamples(const CSAMPLE* pBuffer, int iFrames) { Trace sidechain("EngineSideChain::writeSamples"); - int samples_written = m_sampleFifo.write(newBuffer, buffer_size); + // TODO: remove assumption of stereo buffer + const int kChannels = 2; + const int iSamples = iFrames * kChannels; + int samples_written = m_sampleFifo.write(pBuffer, iSamples); - if (samples_written != buffer_size) { + if (samples_written != iSamples) { Counter("EngineSideChain::writeSamples buffer overrun").increment(); } diff --git a/src/engine/sidechain/enginesidechain.h b/src/engine/sidechain/enginesidechain.h index 72904b3a9436..8497c07d91bd 100644 --- a/src/engine/sidechain/enginesidechain.h +++ b/src/engine/sidechain/enginesidechain.h @@ -24,11 +24,12 @@ #include "preferences/usersettings.h" #include "engine/sidechain/sidechainworker.h" +#include "soundio/soundmanagerutil.h" #include "util/fifo.h" #include "util/mutex.h" #include "util/types.h" -class EngineSideChain : public QThread { +class EngineSideChain : public QThread, public AudioDestination { Q_OBJECT public: EngineSideChain(UserSettingsPointer pConfig); @@ -37,13 +38,19 @@ class EngineSideChain : public QThread { // Not thread-safe, wait-free. Submit buffer of samples to the sidechain for // processing. Should only be called from a single writer thread (typically // the engine callback). - void writeSamples(const CSAMPLE* buffer, int buffer_size); + void writeSamples(const CSAMPLE* pBuffer, int iFrames); + + // Thin wrapper around writeSamples that is used by SoundManager when receiving + // from a sound card input instead of the engine + void receiveBuffer(AudioInput input, + const CSAMPLE* pBuffer, + unsigned int iFrames) override; // Thread-safe, blocking. void addSideChainWorker(SideChainWorker* pWorker); private: - void run(); + void run() override; UserSettingsPointer m_pConfig; // Indicates that the thread should exit. diff --git a/src/mixer/playermanager.cpp b/src/mixer/playermanager.cpp index 553d807f56d2..090ecfc5733f 100644 --- a/src/mixer/playermanager.cpp +++ b/src/mixer/playermanager.cpp @@ -79,18 +79,6 @@ PlayerManager::PlayerManager(UserSettingsPointer pConfig, // This is parented to the PlayerManager so does not need to be deleted m_pSamplerBank = new SamplerBank(this); - - // register the engine's outputs - m_pSoundManager->registerOutput(AudioOutput(AudioOutput::MASTER, 0, 2), - m_pEngine); - m_pSoundManager->registerOutput(AudioOutput(AudioOutput::HEADPHONES, 0, 2), - m_pEngine); - for (int o = EngineChannel::LEFT; o <= EngineChannel::RIGHT; o++) { - m_pSoundManager->registerOutput(AudioOutput(AudioOutput::BUS, 0, 2, o), - m_pEngine); - } - m_pSoundManager->registerOutput(AudioOutput(AudioOutput::SIDECHAIN, 0, 2), - m_pEngine); } PlayerManager::~PlayerManager() { diff --git a/src/mixxx.cpp b/src/mixxx.cpp index 238f23803792..80bcf935f23e 100644 --- a/src/mixxx.cpp +++ b/src/mixxx.cpp @@ -192,11 +192,10 @@ void MixxxMainWindow::initialize(QApplication* pApp, const CmdlineArgs& args) { launchProgress(8); - // Initialize player device - // while this is created here, setupDevices needs to be called sometime - // after the players are added to the engine (as is done currently) -- bkgood - // (long) + // Although m_pSoundManager is created here, m_pSoundManager->setupDevices() + // needs to be called after m_pPlayerManager registers sound IO for each EngineChannel. m_pSoundManager = new SoundManager(pConfig, m_pEngine); + m_pEngine->registerNonEngineChannelSoundIO(m_pSoundManager); m_pRecordingManager = new RecordingManager(pConfig, m_pEngine); diff --git a/src/preferences/dialog/dlgprefsound.cpp b/src/preferences/dialog/dlgprefsound.cpp index 00c03cdf42c1..65f2ec3f8ff4 100644 --- a/src/preferences/dialog/dlgprefsound.cpp +++ b/src/preferences/dialog/dlgprefsound.cpp @@ -37,6 +37,8 @@ DlgPrefSound::DlgPrefSound(QWidget* pParent, SoundManager* pSoundManager, m_pPlayerManager(pPlayerManager), m_pConfig(pConfig), m_settingsModified(false), + m_bLatencyChanged(false), + m_bSkipConfigClear(true), m_loading(false) { setupUi(this); @@ -79,6 +81,40 @@ DlgPrefSound::DlgPrefSound(QWidget* pParent, SoundManager* pSoundManager, static_cast(i))); } + m_pLatencyCompensation = new ControlProxy("[Master]", "microphoneLatencyCompensation", this); + m_pMasterDelay = new ControlProxy("[Master]", "delay", this); + m_pHeadDelay = new ControlProxy("[Master]", "headDelay", this); + m_pBoothDelay = new ControlProxy("[Master]", "boothDelay", this); + + latencyCompensationSpinBox->setValue(m_pLatencyCompensation->get()); + latencyCompensationWarningLabel->setWordWrap(true); + masterDelaySpinBox->setValue(m_pMasterDelay->get()); + headDelaySpinBox->setValue(m_pHeadDelay->get()); + boothDelaySpinBox->setValue(m_pBoothDelay->get()); + + connect(latencyCompensationSpinBox, SIGNAL(valueChanged(double)), + this, SLOT(latencyCompensationSpinboxChanged(double))); + connect(masterDelaySpinBox, SIGNAL(valueChanged(double)), + this, SLOT(masterDelaySpinboxChanged(double))); + connect(headDelaySpinBox, SIGNAL(valueChanged(double)), + this, SLOT(headDelaySpinboxChanged(double))); + connect(boothDelaySpinBox, SIGNAL(valueChanged(double)), + this, SLOT(boothDelaySpinboxChanged(double))); + + m_pMicMonitorMode = new ControlProxy("[Master]", "talkover_mix", this); + micMonitorModeComboBox->addItem(tr("Master output only"), + QVariant(static_cast(EngineMaster::MicMonitorMode::MASTER))); + micMonitorModeComboBox->addItem(tr("Master and booth outputs"), + QVariant(static_cast(EngineMaster::MicMonitorMode::MASTER_AND_BOOTH))); + micMonitorModeComboBox->addItem(tr("Direct monitor (recording and broadcasting only)"), + QVariant(static_cast(EngineMaster::MicMonitorMode::DIRECT_MONITOR))); + int modeIndex = micMonitorModeComboBox->findData( + static_cast(m_pMicMonitorMode->get())); + micMonitorModeComboBox->setCurrentIndex(modeIndex); + micMonitorModeComboBoxChanged(modeIndex); + connect(micMonitorModeComboBox, SIGNAL(currentIndexChanged(int)), + this, SLOT(micMonitorModeComboBoxChanged(int))); + initializePaths(); loadSettings(); @@ -113,13 +149,8 @@ DlgPrefSound::DlgPrefSound(QWidget* pParent, SoundManager* pSoundManager, m_pMasterLatency = new ControlProxy("[Master]", "latency", this); m_pMasterLatency->connectValueChanged(SLOT(masterLatencyChanged(double))); - - m_pHeadDelay = new ControlProxy("[Master]", "headDelay", this); - m_pMasterDelay = new ControlProxy("[Master]", "delay", this); - - headDelaySpinBox->setValue(m_pHeadDelay->get()); - masterDelaySpinBox->setValue(m_pMasterDelay->get()); - + // TODO: remove this option by automatically disabling/enabling the master mix + // when recording, broadcasting, headphone, and master outputs are enabled/disabled m_pMasterEnabled = new ControlProxy("[Master]", "enabled", this); masterMixComboBox->addItem(tr("Disabled")); masterMixComboBox->addItem(tr("Enabled")); @@ -136,24 +167,9 @@ DlgPrefSound::DlgPrefSound(QWidget* pParent, SoundManager* pSoundManager, this, SLOT(masterOutputModeComboBoxChanged(int))); m_pMasterMonoMixdown->connectValueChanged(SLOT(masterMonoMixdownChanged(double))); - m_pMasterTalkoverMix = new ControlProxy("[Master]", "talkover_mix", this); - micMixComboBox->addItem(tr("Master output")); - micMixComboBox->addItem(tr("Broadcast and Recording only")); - micMixComboBox->setCurrentIndex((int)m_pMasterTalkoverMix->get()); - connect(micMixComboBox, SIGNAL(currentIndexChanged(int)), - this, SLOT(talkoverMixComboBoxChanged(int))); - m_pMasterTalkoverMix->connectValueChanged(SLOT(talkoverMixChanged(double))); - - m_pKeylockEngine = new ControlProxy("[Master]", "keylock_engine", this); - connect(headDelaySpinBox, SIGNAL(valueChanged(double)), - this, SLOT(headDelayChanged(double))); - connect(masterDelaySpinBox, SIGNAL(valueChanged(double)), - this, SLOT(masterDelayChanged(double))); - - #ifdef __LINUX__ qDebug() << "RLimit Cur " << RLimit::getCurRtPrio(); qDebug() << "RLimit Max " << RLimit::getMaxRtPrio(); @@ -165,10 +181,10 @@ DlgPrefSound::DlgPrefSound(QWidget* pParent, SoundManager* pSoundManager, // the limits warning is a Linux only thing limitsHint->hide(); #endif // __LINUX__ - } DlgPrefSound::~DlgPrefSound() { + delete m_pLatencyCompensation; } /** @@ -181,8 +197,10 @@ void DlgPrefSound::slotUpdate() { // every time. There's no real way around this, just another argument // for a prefs rewrite -- bkgood m_settingsModified = false; + m_bSkipConfigClear = true; loadSettings(); - + checkLatencyCompensation(); + m_bSkipConfigClear = false; } /** @@ -193,6 +211,10 @@ void DlgPrefSound::slotApply() { return; } + m_config.clearInputs(); + m_config.clearOutputs(); + emit(writePaths(&m_config)); + SoundDeviceError err = SOUNDDEVICE_ERROR_OK; { ScopedWaitCursor cursor; @@ -200,9 +222,6 @@ void DlgPrefSound::slotApply() { m_pConfig->set(ConfigKey("[Master]", "keylock_engine"), ConfigValue(keylockComboBox->currentIndex())); - m_config.clearInputs(); - m_config.clearOutputs(); - emit(writePaths(&m_config)); err = m_pSoundManager->setConfig(m_config); } if (err != SOUNDDEVICE_ERROR_OK) { @@ -210,8 +229,12 @@ void DlgPrefSound::slotApply() { QMessageBox::warning(NULL, tr("Configuration error"), error); } else { m_settingsModified = false; + m_bLatencyChanged = false; } + m_bSkipConfigClear = true; loadSettings(); // in case SM decided to change anything it didn't like + checkLatencyCompensation(); + m_bSkipConfigClear = false; } /** @@ -297,7 +320,7 @@ void DlgPrefSound::addPath(AudioInput input) { void DlgPrefSound::connectSoundItem(DlgPrefSoundItem *item) { connect(item, SIGNAL(settingChanged()), - this, SLOT(settingChanged())); + this, SLOT(deviceSettingChanged())); connect(this, SIGNAL(loadPaths(const SoundManagerConfig&)), item, SLOT(loadPath(const SoundManagerConfig&))); connect(this, SIGNAL(writePaths(SoundManagerConfig*)), @@ -358,6 +381,11 @@ void DlgPrefSound::loadSettings(const SoundManagerConfig &config) { audioBufferComboBox->setCurrentIndex(sizeIndex); } + // Setting the index of audioBufferComboBox here sets m_bLatencyChanged to true, + // but m_bLatencyChanged should only be true when the user has edited the + // buffer size or sample rate. + m_bLatencyChanged = false; + int syncBuffers = m_config.getSyncBuffers(); if (syncBuffers == 0) { // "Experimental (no delay)")) @@ -389,11 +417,15 @@ void DlgPrefSound::loadSettings(const SoundManagerConfig &config) { void DlgPrefSound::apiChanged(int index) { m_config.setAPI(apiComboBox->itemData(index).toString()); refreshDevices(); - // JACK sets its own latency + // JACK sets its own buffer size and sample rate that Mixxx cannot change. + // TODO(Be): Get the buffer size from JACK and update audioBufferComboBox. + // PortAudio does not have a way to get the buffer size from JACK as of July 2017. if (m_config.getAPI() == MIXXX_PORTAUDIO_JACK_STRING) { + sampleRateComboBox->setEnabled(false); latencyLabel->setEnabled(false); audioBufferComboBox->setEnabled(false); } else { + sampleRateComboBox->setEnabled(true); latencyLabel->setEnabled(true); audioBufferComboBox->setEnabled(true); } @@ -426,6 +458,8 @@ void DlgPrefSound::updateAPIs() { void DlgPrefSound::sampleRateChanged(int index) { m_config.setSampleRate( sampleRateComboBox->itemData(index).toUInt()); + m_bLatencyChanged = true; + checkLatencyCompensation(); } /** @@ -435,6 +469,8 @@ void DlgPrefSound::sampleRateChanged(int index) { void DlgPrefSound::audioBufferChanged(int index) { m_config.setAudioBufferSizeIndex( audioBufferComboBox->itemData(index).toUInt()); + m_bLatencyChanged = true; + checkLatencyCompensation(); } void DlgPrefSound::syncBuffersChanged(int index) { @@ -511,6 +547,12 @@ void DlgPrefSound::settingChanged() { m_settingsModified = true; } +void DlgPrefSound::deviceSettingChanged() { + if (m_loading) return; + checkLatencyCompensation(); + m_settingsModified = true; +} + /** * Slot called when the "Query Devices" button is clicked. */ @@ -539,9 +581,18 @@ void DlgPrefSound::slotResetToDefaults() { headDelaySpinBox->setValue(0.0); m_pHeadDelay->set(0.0); + boothDelaySpinBox->setValue(0.0); + m_pBoothDelay->set(0.0); + // Enable talkover master output - m_pMasterTalkoverMix->set(0.0); - micMixComboBox->setCurrentIndex(0); + m_pMicMonitorMode->set( + static_cast( + static_cast(EngineMaster::MicMonitorMode::MASTER))); + micMonitorModeComboBox->setCurrentIndex( + micMonitorModeComboBox->findData( + static_cast(EngineMaster::MicMonitorMode::MASTER))); + + latencyCompensationSpinBox->setValue(latencyCompensationSpinBox->minimum()); settingChanged(); // force the apply button to enable } @@ -556,14 +607,23 @@ void DlgPrefSound::masterLatencyChanged(double latency) { update(); } -void DlgPrefSound::headDelayChanged(double value) { - m_pHeadDelay->set(value); +void DlgPrefSound::latencyCompensationSpinboxChanged(double value) { + m_pLatencyCompensation->set(value); + checkLatencyCompensation(); } -void DlgPrefSound::masterDelayChanged(double value) { +void DlgPrefSound::masterDelaySpinboxChanged(double value) { m_pMasterDelay->set(value); } +void DlgPrefSound::headDelaySpinboxChanged(double value) { + m_pHeadDelay->set(value); +} + +void DlgPrefSound::boothDelaySpinboxChanged(double value) { + m_pBoothDelay->set(value); +} + void DlgPrefSound::masterMixChanged(int value) { m_pMasterEnabled->set(value); } @@ -580,10 +640,59 @@ void DlgPrefSound::masterMonoMixdownChanged(double value) { masterOutputModeComboBox->setCurrentIndex(value ? 1 : 0); } -void DlgPrefSound::talkoverMixComboBoxChanged(int value) { - m_pMasterTalkoverMix->set((double)value); +void DlgPrefSound::micMonitorModeComboBoxChanged(int value) { + EngineMaster::MicMonitorMode newMode = + static_cast( + micMonitorModeComboBox->itemData(value).toInt()); + + m_pMicMonitorMode->set(static_cast(newMode)); + + checkLatencyCompensation(); } -void DlgPrefSound::talkoverMixChanged(double value) { - micMixComboBox->setCurrentIndex(value ? 1 : 0); +void DlgPrefSound::checkLatencyCompensation() { + EngineMaster::MicMonitorMode configuredMicMonitorMode = + static_cast( + static_cast(m_pMicMonitorMode->get())); + + // Do not clear the SoundManagerConfig on startup, from slotApply, or from slotUpdate + if (!m_bSkipConfigClear) { + m_config.clearInputs(); + m_config.clearOutputs(); + } + + emit(writePaths(&m_config)); + + if (m_config.hasMicInputs() && !m_config.hasExternalRecordBroadcast()) { + micMonitorModeComboBox->setEnabled(true); + if (configuredMicMonitorMode == EngineMaster::MicMonitorMode::DIRECT_MONITOR) { + latencyCompensationSpinBox->setEnabled(true); + QString warningIcon(" "); + QString lineBreak("
"); + // TODO(Be): Make the "User Manual" text link to the manual. + if (m_pLatencyCompensation->get() == 0.0) { + latencyCompensationWarningLabel->setText( + warningIcon + + tr("Microphone inputs are out of time in the record & broadcast signal compared to what you hear.") + lineBreak + + tr("Measure round trip latency and enter it above for Microphone Latency Compensation to align microphone timing.") + lineBreak + + tr("Refer to the Mixxx User Manual for details.") + ""); + latencyCompensationWarningLabel->show(); + } else if (m_bLatencyChanged) { + latencyCompensationWarningLabel->setText( + warningIcon + + tr("Configured latency has changed.") + lineBreak + + tr("Remeasure round trip latency and enter it above for Microphone Latency Compensation to align microphone timing.") + lineBreak + + tr("Refer to the Mixxx User Manual for details.") + ""); + latencyCompensationWarningLabel->show(); + } else { + latencyCompensationWarningLabel->hide(); + } + } else { + latencyCompensationSpinBox->setEnabled(false); + } + } else { + micMonitorModeComboBox->setEnabled(false); + latencyCompensationSpinBox->setEnabled(false); + latencyCompensationWarningLabel->hide(); + } } diff --git a/src/preferences/dialog/dlgprefsound.h b/src/preferences/dialog/dlgprefsound.h index f94faa0fd49b..ef81d68de708 100644 --- a/src/preferences/dialog/dlgprefsound.h +++ b/src/preferences/dialog/dlgprefsound.h @@ -60,14 +60,15 @@ class DlgPrefSound : public DlgPreferencePage, public Ui::DlgPrefSoundDlg { void slotResetToDefaults(); void bufferUnderflow(double count); void masterLatencyChanged(double latency); - void headDelayChanged(double value); - void masterDelayChanged(double value); + void latencyCompensationSpinboxChanged(double value); + void masterDelaySpinboxChanged(double value); + void headDelaySpinboxChanged(double value); + void boothDelaySpinboxChanged(double value); void masterMixChanged(int value); void masterEnabledChanged(double value); void masterOutputModeComboBoxChanged(int value); void masterMonoMixdownChanged(double value); - void talkoverMixComboBoxChanged(int value); - void talkoverMixChanged(double value); + void micMonitorModeComboBoxChanged(int value); private slots: void addPath(AudioOutput output); @@ -81,6 +82,7 @@ class DlgPrefSound : public DlgPreferencePage, public Ui::DlgPrefSoundDlg { void syncBuffersChanged(int index); void refreshDevices(); void settingChanged(); + void deviceSettingChanged(); void queryClicked(); private: @@ -88,6 +90,7 @@ class DlgPrefSound : public DlgPreferencePage, public Ui::DlgPrefSoundDlg { void connectSoundItem(DlgPrefSoundItem *item); void loadSettings(const SoundManagerConfig &config); void insertItem(DlgPrefSoundItem *pItem, QVBoxLayout *pLayout); + void checkLatencyCompensation(); SoundManager *m_pSoundManager; PlayerManager *m_pPlayerManager; @@ -96,13 +99,17 @@ class DlgPrefSound : public DlgPreferencePage, public Ui::DlgPrefSoundDlg { ControlProxy* m_pMasterLatency; ControlProxy* m_pHeadDelay; ControlProxy* m_pMasterDelay; + ControlProxy* m_pBoothDelay; + ControlProxy* m_pLatencyCompensation; ControlProxy* m_pKeylockEngine; ControlProxy* m_pMasterEnabled; ControlProxy* m_pMasterMonoMixdown; - ControlProxy* m_pMasterTalkoverMix; + ControlProxy* m_pMicMonitorMode; QList m_inputDevices; QList m_outputDevices; bool m_settingsModified; + bool m_bLatencyChanged; + bool m_bSkipConfigClear; SoundManagerConfig m_config; bool m_loading; }; diff --git a/src/preferences/dialog/dlgprefsounddlg.ui b/src/preferences/dialog/dlgprefsounddlg.ui index 9a435090c066..6df1ee5a36d2 100644 --- a/src/preferences/dialog/dlgprefsounddlg.ui +++ b/src/preferences/dialog/dlgprefsounddlg.ui @@ -7,7 +7,7 @@ 0 0 686 - 584 + 852 @@ -16,16 +16,32 @@ + + + + Sound API + + + apiComboBox + + + - - + + - Headphone Delay + Sa&mple Rate + + + sampleRateComboBox + + + @@ -36,134 +52,161 @@ - - - - ms - - - 2 - - - 200.000000000000000 + + + + + + + Multi-Soundcard Synchronization - - + + + + + - Sample Rate + &Keylock/Pitch-Bending Engine - sampleRateComboBox + keylockComboBox - - - - Qt::Vertical - - - QSizePolicy::MinimumExpanding - - - - 20 - 1 - + + + + + + + + + + Master Mix - + - - + + - - + + + + Master Output Mode + + - - + + + + + - Sound API + Microphone Monitor Mode - - apiComboBox + + + + + + Microphone Latency Compensation - + - ms + ms - 2 + 3 - 200.000000000000000 + 500.000000000000000 + + + 0.100000000000000 - + Master Delay - - - - Multi-Soundcard Synchronization + + + + ms + + + 3 + + + 500.000000000000000 + + + 0.100000000000000 - - - - - + + - Master Mix + Headphone Delay - - - - - - - Keylock/Pitch-Bending Engine + + + + ms - - keylockComboBox + + 3 + + + 500.000000000000000 + + + 0.100000000000000 - - - - - + + - Master Output Mode + Booth Delay - - + + + + ms + + + 3 + + + 500.000000000000000 + + + 0.100000000000000 + + - - + + - Microphone/Talkover Mix + warning goes here - - - @@ -316,39 +359,39 @@ - - - System Reported Latency - - - audioBufferComboBox - - - - - - - 20 ms - - - - - - - Buffer Underflow Count - - - audioBufferComboBox - - - - - - - 0 - - - + + + System Reported &Latency + + + audioBufferComboBox + + + + + + + 20 ms + + + + + + + Buffer &Underflow Count + + + audioBufferComboBox + + + + + + + 0 + + + diff --git a/src/soundio/soundmanager.cpp b/src/soundio/soundmanager.cpp index 95aa85a03e60..0e9545eb1012 100644 --- a/src/soundio/soundmanager.cpp +++ b/src/soundio/soundmanager.cpp @@ -28,6 +28,7 @@ #include "engine/enginebuffer.h" #include "engine/enginemaster.h" #include "engine/sidechain/enginenetworkstream.h" +#include "engine/sidechain/enginesidechain.h" #include "soundio/sounddevice.h" #include "soundio/sounddevicenetwork.h" #include "soundio/sounddevicenotfound.h" @@ -179,6 +180,7 @@ void SoundManager::closeDevices(bool sleepAfterClosing) { m_registeredDestinations.find(in); it != m_registeredDestinations.end() && it.key() == in; ++it) { it.value()->onInputUnconfigured(in); + m_pMaster->onInputDisconnected(in); } } foreach (AudioOutput out, pDevice->outputs()) { @@ -381,6 +383,7 @@ SoundDeviceError SoundManager::setupDevices() { it != m_registeredDestinations.end() && it.key() == in; ++it) { it.value()->onInputConfigured(in); + m_pMaster->onInputConnected(in); } } QList outputs = @@ -388,7 +391,7 @@ SoundDeviceError SoundManager::setupDevices() { // Statically connect the Network Device to the Sidechain if (device->getInternalName() == kNetworkDeviceInternalName) { - AudioOutput out(AudioPath::SIDECHAIN, 0, 2, 0); + AudioOutput out(AudioPath::RECORD_BROADCAST, 0, 2, 0); outputs.append(out); } @@ -610,7 +613,6 @@ void SoundManager::readProcess() { } } - void SoundManager::registerOutput(AudioOutput output, AudioSource *src) { if (m_registeredSources.contains(output)) { qDebug() << "WARNING: AudioOutput already registered!"; diff --git a/src/soundio/soundmanagerconfig.cpp b/src/soundio/soundmanagerconfig.cpp index f6b7463f9e70..232a790e91ef 100644 --- a/src/soundio/soundmanagerconfig.cpp +++ b/src/soundio/soundmanagerconfig.cpp @@ -38,7 +38,9 @@ SoundManagerConfig::SoundManagerConfig() m_sampleRate(kFallbackSampleRate), m_deckCount(kDefaultDeckCount), m_audioBufferSizeIndex(kDefaultAudioBufferSizeIndex), - m_syncBuffers(2) { + m_syncBuffers(2), + m_iNumMicInputs(0), + m_bExternalRecordBroadcastConnected(false) { m_configFile = QFileInfo(QDir(CmdlineArgs::Instance().getSettingsPath()).filePath(SOUNDMANAGERCONFIG_FILENAME)); } @@ -250,6 +252,8 @@ unsigned int SoundManagerConfig::getAudioBufferSizeIndex() const { return m_audioBufferSizeIndex; } +// FIXME: This is incorrect when using JACK as the sound API! +// m_audioBufferSizeIndex does not reflect JACK's buffer size. unsigned int SoundManagerConfig::getFramesPerBuffer() const { // endless loop otherwise unsigned int audioBufferSizeIndex = m_audioBufferSizeIndex; @@ -268,6 +272,12 @@ unsigned int SoundManagerConfig::getFramesPerBuffer() const { return framesPerBuffer; } +// FIXME: This is incorrect when using JACK as the sound API! +// m_audioBufferSizeIndex does not reflect JACK's buffer size. +double SoundManagerConfig::getProcessingLatency() const { + return static_cast(getFramesPerBuffer()) / m_sampleRate * 1000.0; +} + // Set the audio buffer size // @warning This IS NOT a value in milliseconds, or a number of frames per @@ -287,6 +297,11 @@ void SoundManagerConfig::addOutput(const QString &device, const AudioOutput &out void SoundManagerConfig::addInput(const QString &device, const AudioInput &in) { m_inputs.insert(device, in); + if (in.getType() == AudioPath::MICROPHONE) { + m_iNumMicInputs++; + } else if (in.getType() == AudioPath::RECORD_BROADCAST) { + m_bExternalRecordBroadcastConnected = true; + } } QMultiHash SoundManagerConfig::getOutputs() const { @@ -303,6 +318,16 @@ void SoundManagerConfig::clearOutputs() { void SoundManagerConfig::clearInputs() { m_inputs.clear(); + m_iNumMicInputs = 0; + m_bExternalRecordBroadcastConnected = false; +} + +bool SoundManagerConfig::hasMicInputs() { + return m_iNumMicInputs; +} + +bool SoundManagerConfig::hasExternalRecordBroadcast() { + return m_bExternalRecordBroadcastConnected; } /** diff --git a/src/soundio/soundmanagerconfig.h b/src/soundio/soundmanagerconfig.h index e01b91a7ff2f..b84526bc5dca 100644 --- a/src/soundio/soundmanagerconfig.h +++ b/src/soundio/soundmanagerconfig.h @@ -65,6 +65,8 @@ class SoundManagerConfig { unsigned int getAudioBufferSizeIndex() const; unsigned int getFramesPerBuffer() const; + // Returns the processing latency in milliseconds + double getProcessingLatency() const; void setAudioBufferSizeIndex(unsigned int latency); unsigned int getSyncBuffers() const; void setSyncBuffers(unsigned int sampleRate); @@ -74,6 +76,8 @@ class SoundManagerConfig { QMultiHash getInputs() const; void clearOutputs(); void clearInputs(); + bool hasMicInputs(); + bool hasExternalRecordBroadcast(); void loadDefaults(SoundManager *soundManager, unsigned int flags); private: QFileInfo m_configFile; @@ -90,5 +94,7 @@ class SoundManagerConfig { unsigned int m_syncBuffers; QMultiHash m_outputs; QMultiHash m_inputs; + int m_iNumMicInputs; + bool m_bExternalRecordBroadcastConnected; }; #endif diff --git a/src/soundio/soundmanagerutil.cpp b/src/soundio/soundmanagerutil.cpp index 9d2031714441..56d74e84572a 100644 --- a/src/soundio/soundmanagerutil.cpp +++ b/src/soundio/soundmanagerutil.cpp @@ -155,20 +155,22 @@ QString AudioPath::getStringFromType(AudioPathType type) { return QString::fromAscii("Invalid"); case MASTER: return QString::fromAscii("Master"); + case BOOTH: + return QString::fromAscii("Booth"); case HEADPHONES: return QString::fromAscii("Headphones"); case BUS: return QString::fromAscii("Bus"); case DECK: return QString::fromAscii("Deck"); + case RECORD_BROADCAST: + return QString::fromAscii("Record/Broadcast"); case VINYLCONTROL: return QString::fromAscii("Vinyl Control"); case MICROPHONE: return QString::fromAscii("Microphone"); case AUXILIARY: return QString::fromAscii("Auxiliary"); - case SIDECHAIN: - return QString::fromAscii("Sidechain"); } return QString::fromAscii("Unknown path type %1").arg(type); } @@ -185,6 +187,8 @@ QString AudioPath::getTrStringFromType(AudioPathType type, unsigned char index) return QObject::tr("Invalid"); case MASTER: return QObject::tr("Master"); + case BOOTH: + return QObject::tr("Booth"); case HEADPHONES: return QObject::tr("Headphones"); case BUS: @@ -201,6 +205,8 @@ QString AudioPath::getTrStringFromType(AudioPathType type, unsigned char index) case DECK: return QString("%1 %2").arg(QObject::tr("Deck"), QString::number(index + 1)); + case RECORD_BROADCAST: + return QObject::tr("Record/Broadcast"); case VINYLCONTROL: return QString("%1 %2").arg(QObject::tr("Vinyl Control"), QString::number(index + 1)); @@ -210,8 +216,6 @@ QString AudioPath::getTrStringFromType(AudioPathType type, unsigned char index) case AUXILIARY: return QString("%1 %2").arg(QObject::tr("Auxiliary"), QString::number(index + 1)); - case SIDECHAIN: - return QObject::tr("Sidechain"); } return QObject::tr("Unknown path type %1").arg(type); } @@ -224,6 +228,8 @@ AudioPathType AudioPath::getTypeFromString(QString string) { string = string.toLower(); if (string == AudioPath::getStringFromType(AudioPath::MASTER).toLower()) { return AudioPath::MASTER; + } else if (string == AudioPath::getStringFromType(AudioPath::BOOTH).toLower()) { + return AudioPath::BOOTH; } else if (string == AudioPath::getStringFromType(AudioPath::HEADPHONES).toLower()) { return AudioPath::HEADPHONES; } else if (string == AudioPath::getStringFromType(AudioPath::BUS).toLower()) { @@ -236,8 +242,8 @@ AudioPathType AudioPath::getTypeFromString(QString string) { return AudioPath::MICROPHONE; } else if (string == AudioPath::getStringFromType(AudioPath::AUXILIARY).toLower()) { return AudioPath::AUXILIARY; - } else if (string == AudioPath::getStringFromType(AudioPath::SIDECHAIN).toLower()) { - return AudioPath::SIDECHAIN; + } else if (string == AudioPath::getStringFromType(AudioPath::RECORD_BROADCAST).toLower()) { + return AudioPath::RECORD_BROADCAST; } else { return AudioPath::INVALID; } @@ -350,15 +356,16 @@ AudioOutput AudioOutput::fromXML(const QDomElement &xml) { QList AudioOutput::getSupportedTypes() { QList types; types.append(MASTER); + types.append(BOOTH); types.append(HEADPHONES); types.append(BUS); types.append(DECK); - types.append(SIDECHAIN); + types.append(RECORD_BROADCAST); return types; } bool AudioOutput::isHidden() { - return m_type == SIDECHAIN; + return m_type == RECORD_BROADCAST; } @@ -439,6 +446,7 @@ QList AudioInput::getSupportedTypes() { #endif types.append(AUXILIARY); types.append(MICROPHONE); + types.append(RECORD_BROADCAST); return types; } diff --git a/src/soundio/soundmanagerutil.h b/src/soundio/soundmanagerutil.h index e0010011aeae..82baa1f41456 100644 --- a/src/soundio/soundmanagerutil.h +++ b/src/soundio/soundmanagerutil.h @@ -57,12 +57,13 @@ class AudioPath { enum AudioPathType { MASTER, HEADPHONES, + BOOTH, BUS, DECK, + RECORD_BROADCAST, VINYLCONTROL, MICROPHONE, AUXILIARY, - SIDECHAIN, INVALID, // if this isn't last bad things will happen -bkgood }; AudioPath(unsigned char channelBase, unsigned char channels); diff --git a/src/test/enginemastertest.cpp b/src/test/enginemastertest.cpp index 1bd44473fe66..12e246028a21 100644 --- a/src/test/enginemastertest.cpp +++ b/src/test/enginemastertest.cpp @@ -7,6 +7,7 @@ #include "engine/enginechannel.h" #include "engine/enginemaster.h" #include "test/mixxxtest.h" +#include "test/signalpathtest.h" #include "util/defs.h" #include "util/sample.h" #include "util/types.h" @@ -40,14 +41,11 @@ class EngineChannelMock : public EngineChannel { class EngineMasterTest : public MixxxTest { protected: void SetUp() override { - m_pMaster = new EngineMaster(config(), "[Master]", NULL, false, false); - m_pMasterEnabled = new ControlProxy(ConfigKey("[Master]", "enabled")); - m_pMasterEnabled->set(1); + m_pMaster = new TestEngineMaster(config(), "[Master]", NULL, false, false); } void TearDown() override { delete m_pMaster; - delete m_pMasterEnabled; } void ClearBuffer(CSAMPLE* pBuffer, int length) { @@ -71,7 +69,6 @@ class EngineMasterTest : public MixxxTest { } EngineMaster* m_pMaster; - ControlProxy* m_pMasterEnabled; }; TEST_F(EngineMasterTest, SingleChannelOutputWorks) { diff --git a/src/test/signalpathtest.h b/src/test/signalpathtest.h index f04d989f95b0..c6096565633a 100644 --- a/src/test/signalpathtest.h +++ b/src/test/signalpathtest.h @@ -40,7 +40,11 @@ class TestEngineMaster : public EngineMaster { bool bEnableSidechain, bool bRampingGain) : EngineMaster(_config, group, pEffectsManager, - bEnableSidechain, bRampingGain) { } + bEnableSidechain, bRampingGain) { + m_pMasterEnabled->forceSet(1); + m_pHeadphoneEnabled->forceSet(1); + m_pBoothEnabled->forceSet(1); + } CSAMPLE* masterBuffer() { return m_pMaster;