diff --git a/modules/juce_dsp/processors/juce_DryWetMixer.cpp b/modules/juce_dsp/processors/juce_DryWetMixer.cpp index 21455d86e6..b25cc84eeb 100644 --- a/modules/juce_dsp/processors/juce_DryWetMixer.cpp +++ b/modules/juce_dsp/processors/juce_DryWetMixer.cpp @@ -36,8 +36,9 @@ DryWetMixer::DryWetMixer() } template -DryWetMixer::DryWetMixer (int maximumWetLatencyInSamples) - : dryDelayLine (maximumWetLatencyInSamples) +DryWetMixer::DryWetMixer (int maximumWetLatencyInSamplesIn) + : dryDelayLine (maximumWetLatencyInSamplesIn), + maximumWetLatencyInSamples (maximumWetLatencyInSamplesIn) { dryDelayLine.setDelay (0); @@ -91,31 +92,88 @@ void DryWetMixer::reset() wetVolume.reset (sampleRate, 0.05); dryDelayLine.reset(); + + offsetInBuffer = 0; + numUsedSamples = 0; +} + +template +struct FirstAndSecondPartBlocks +{ + AudioBlock first, second; +}; + +template +static FirstAndSecondPartBlocks getFirstAndSecondPartBlocks (AudioBuffer& bufferDry, + size_t firstPartStart, + size_t channelsToCopy, + size_t samplesToCopy) +{ + const auto actualChannelsToCopy = jmin (channelsToCopy, (size_t) bufferDry.getNumChannels()); + const auto firstPartLength = jmin ((size_t) bufferDry.getNumSamples() - firstPartStart, samplesToCopy); + const auto secondPartLength = samplesToCopy - firstPartLength; + + const auto channelBlock = AudioBlock (bufferDry).getSubsetChannelBlock (0, actualChannelsToCopy); + + return { channelBlock.getSubBlock (firstPartStart, firstPartLength), + secondPartLength != 0 ? channelBlock.getSubBlock (0, samplesToCopy - firstPartLength) : AudioBlock() }; } //============================================================================== template void DryWetMixer::pushDrySamples (const AudioBlock drySamples) { + const auto remainingSpace = (size_t) bufferDry.getNumSamples() - numUsedSamples; + jassert (drySamples.getNumChannels() <= (size_t) bufferDry.getNumChannels()); + jassert (drySamples.getNumSamples() <= remainingSpace); - auto dryBlock = AudioBlock (bufferDry); - dryBlock = dryBlock.getSubsetChannelBlock (0, drySamples.getNumChannels()).getSubBlock (0, drySamples.getNumSamples()); + auto blocks = getFirstAndSecondPartBlocks (bufferDry, + (offsetInBuffer + numUsedSamples) % (size_t) bufferDry.getNumSamples(), + drySamples.getNumChannels(), + jmin (drySamples.getNumSamples(), remainingSpace)); - auto context = ProcessContextNonReplacing(drySamples, dryBlock); - dryDelayLine.process (context); + const auto processSubBlock = [this, &drySamples] (AudioBlock block, size_t startOffset) + { + auto inputBlock = drySamples.getSubBlock (startOffset, block.getNumSamples()); + + if (maximumWetLatencyInSamples == 0) + block.copyFrom (inputBlock); + else + dryDelayLine.process (ProcessContextNonReplacing (inputBlock, block)); + }; + + processSubBlock (blocks.first, 0); + + if (blocks.second.getNumSamples() > 0) + processSubBlock (blocks.second, blocks.first.getNumSamples()); + + numUsedSamples += blocks.first.getNumSamples() + blocks.second.getNumSamples(); } template void DryWetMixer::mixWetSamples (AudioBlock inOutBlock) { - auto dryBlock = AudioBlock (bufferDry); - dryBlock = dryBlock.getSubsetChannelBlock (0, inOutBlock.getNumChannels()).getSubBlock (0, inOutBlock.getNumSamples()); - - dryBlock.multiplyBy (dryVolume); inOutBlock.multiplyBy (wetVolume); - inOutBlock.add (dryBlock); + jassert (inOutBlock.getNumSamples() <= numUsedSamples); + + auto blocks = getFirstAndSecondPartBlocks (bufferDry, + offsetInBuffer, + inOutBlock.getNumChannels(), + jmin (inOutBlock.getNumSamples(), numUsedSamples)); + blocks.first.multiplyBy (dryVolume); + inOutBlock.add (blocks.first); + + if (blocks.second.getNumSamples() != 0) + { + blocks.second.multiplyBy (dryVolume); + inOutBlock.getSubBlock (blocks.first.getNumSamples()).add (blocks.second); + } + + const auto samplesToCopy = blocks.first.getNumSamples() + blocks.second.getNumSamples(); + offsetInBuffer = (offsetInBuffer + samplesToCopy) % (size_t) bufferDry.getNumSamples(); + numUsedSamples -= samplesToCopy; } //============================================================================== @@ -175,5 +233,167 @@ void DryWetMixer::update() template class DryWetMixer; template class DryWetMixer; + +//============================================================================== +//============================================================================== +#if JUCE_UNIT_TESTS + +struct DryWetMixerTests : public UnitTest +{ + DryWetMixerTests() : UnitTest ("DryWetMixer", UnitTestCategories::dsp) {} + + enum class Kind { down, up }; + + static auto getRampBuffer (ProcessSpec spec, Kind kind) + { + AudioBuffer buffer ((int) spec.numChannels, (int) spec.maximumBlockSize); + + for (uint32_t sample = 0; sample < spec.maximumBlockSize; ++sample) + { + for (uint32_t channel = 0; channel < spec.numChannels; ++channel) + { + const auto ramp = kind == Kind::up ? sample : spec.maximumBlockSize - sample; + + buffer.setSample ((int) channel, + (int) sample, + jmap ((float) ramp, 0.0f, (float) spec.maximumBlockSize, 0.0f, 1.0f)); + } + } + + return buffer; + } + + void runTest() override + { + constexpr ProcessSpec spec { 44100.0, 512, 2 }; + constexpr auto numBlocks = 5; + + const auto wetBuffer = getRampBuffer (spec, Kind::up); + const auto dryBuffer = getRampBuffer (spec, Kind::down); + + for (auto maxLatency : { 0, 512 }) + { + beginTest ("Mixer can push multiple small buffers"); + { + DryWetMixer mixer (maxLatency); + mixer.setWetMixProportion (0.5f); + mixer.prepare (spec); + + for (auto block = 0; block < numBlocks; ++block) + { + // Push samples one-by-one + for (uint32_t sample = 0; sample < spec.maximumBlockSize; ++sample) + mixer.pushDrySamples (AudioBlock (dryBuffer).getSubBlock (sample, 1)); + + // Mix wet samples in one go + auto outputBlock = wetBuffer; + mixer.mixWetSamples ({ outputBlock }); + + // The output block should contain the wet and dry samples averaged + for (uint32_t sample = 0; sample < spec.maximumBlockSize; ++sample) + { + for (uint32_t channel = 0; channel < spec.numChannels; ++channel) + { + const auto outputValue = outputBlock.getSample ((int) channel, (int) sample); + expectWithinAbsoluteError (outputValue, 0.5f, 0.0001f); + } + } + } + } + + beginTest ("Mixer can pop multiple small buffers"); + { + DryWetMixer mixer (maxLatency); + mixer.setWetMixProportion (0.5f); + mixer.prepare (spec); + + for (auto block = 0; block < numBlocks; ++block) + { + // Push samples in one go + mixer.pushDrySamples ({ dryBuffer }); + + // Process wet samples one-by-one + for (uint32_t sample = 0; sample < spec.maximumBlockSize; ++sample) + { + AudioBuffer outputBlock ((int) spec.numChannels, 1); + AudioBlock (wetBuffer).getSubBlock (sample, 1).copyTo (outputBlock); + mixer.mixWetSamples ({ outputBlock }); + + // The output block should contain the wet and dry samples averaged + for (uint32_t channel = 0; channel < spec.numChannels; ++channel) + { + const auto outputValue = outputBlock.getSample ((int) channel, 0); + expectWithinAbsoluteError (outputValue, 0.5f, 0.0001f); + } + } + } + } + + beginTest ("Mixer can push and pop multiple small buffers"); + { + DryWetMixer mixer (maxLatency); + mixer.setWetMixProportion (0.5f); + mixer.prepare (spec); + + for (auto block = 0; block < numBlocks; ++block) + { + // Push dry samples and process wet samples one-by-one + for (uint32_t sample = 0; sample < spec.maximumBlockSize; ++sample) + { + mixer.pushDrySamples (AudioBlock (dryBuffer).getSubBlock (sample, 1)); + + AudioBuffer outputBlock ((int) spec.numChannels, 1); + AudioBlock (wetBuffer).getSubBlock (sample, 1).copyTo (outputBlock); + mixer.mixWetSamples ({ outputBlock }); + + // The output block should contain the wet and dry samples averaged + for (uint32_t channel = 0; channel < spec.numChannels; ++channel) + { + const auto outputValue = outputBlock.getSample ((int) channel, 0); + expectWithinAbsoluteError (outputValue, 0.5f, 0.0001f); + } + } + } + } + + beginTest ("Mixer can push and pop full-sized blocks after encountering a shorter block"); + { + DryWetMixer mixer (maxLatency); + mixer.setWetMixProportion (0.5f); + mixer.prepare (spec); + + constexpr auto shortBlockLength = spec.maximumBlockSize / 2; + AudioBuffer shortBlock (spec.numChannels, shortBlockLength); + mixer.pushDrySamples (AudioBlock (dryBuffer).getSubBlock (shortBlockLength)); + mixer.mixWetSamples ({ shortBlock }); + + for (auto block = 0; block < numBlocks; ++block) + { + // Push a full block of dry samples + mixer.pushDrySamples ({ dryBuffer }); + + // Mix a full block of wet samples + auto outputBlock = wetBuffer; + mixer.mixWetSamples ({ outputBlock }); + + // The output block should contain the wet and dry samples averaged + for (uint32_t sample = 0; sample < spec.maximumBlockSize; ++sample) + { + for (uint32_t channel = 0; channel < spec.numChannels; ++channel) + { + const auto outputValue = outputBlock.getSample ((int) channel, (int) sample); + expectWithinAbsoluteError (outputValue, 0.5f, 0.0001f); + } + } + } + } + } + } +}; + +static const DryWetMixerTests dryWetMixerTests; + +#endif + } // namespace dsp } // namespace juce diff --git a/modules/juce_dsp/processors/juce_DryWetMixer.h b/modules/juce_dsp/processors/juce_DryWetMixer.h index f2071501ea..0c243f49dc 100644 --- a/modules/juce_dsp/processors/juce_DryWetMixer.h +++ b/modules/juce_dsp/processors/juce_DryWetMixer.h @@ -112,6 +112,8 @@ private: SampleType mix = 1.0; MixingRule currentMixingRule = MixingRule::linear; double sampleRate = 44100.0; + size_t offsetInBuffer = 0, numUsedSamples = 0; + int maximumWetLatencyInSamples = 0; }; } // namespace dsp