| 75 | // ============================================================================= |
| 76 | |
| 77 | void LfoManager::processBlock(juce::AudioBuffer<float>& outputBuffer, float sampleRate, juce::AudioPlayHead* playHead, int numSamples) |
| 78 | { |
| 79 | const juce::ScopedLock sl(dataAccessLock); |
| 80 | // 1. Generate all raw LFO signals for the current block. |
| 81 | // This fills the internal 'lfoOutputBuffer'. |
| 82 | generateLfoOutput(sampleRate, playHead, numSamples); |
| 83 | |
| 84 | // 2. Copy the generated LFO signals to the output buffer. |
| 85 | jassert(outputBuffer.getNumSamples() == lfoOutputBuffer.getNumSamples()); |
| 86 | jassert(outputBuffer.getNumChannels() >= lfoOutputBuffer.getNumChannels()); |
| 87 | |
| 88 | for (int channel = 0; channel < lfoOutputBuffer.getNumChannels(); ++channel) |
| 89 | { |
| 90 | outputBuffer.copyFrom(channel, 0, lfoOutputBuffer, channel, 0, numSamples); |
| 91 | } |
| 92 | |
| 93 | // 3. Clear the map of calculated values from the previous block. |
| 94 | // We now store normalized values. |
| 95 | modulatedValues.clear(); |
| 96 | |
| 97 | // 4. Iterate through all modulation routings to calculate final parameter values. |
| 98 | for (const auto& routing : modulationRoutings) |
| 99 | { |
| 100 | // Skip invalid or unassigned routings |
| 101 | if (routing.isBypassed || routing.targetParameterID.isEmpty()) |
| 102 | continue; |
| 103 | |
| 104 | // Use the first sample of the LFO output as the representative value for the whole block. |
| 105 | float lfoValue = lfoOutputBuffer.getSample(routing.sourceLfoIndex, 0); |
| 106 | |
| 107 | // Get the RangedAudioParameter for conversions |
| 108 | auto* parameter = treeState.getParameter(routing.targetParameterID); |
| 109 | if (parameter == nullptr) |
| 110 | continue; |
| 111 | |
| 112 | // Get the parameter's original NORMALIZED value (from the GUI knob) |
| 113 | const float normalizedBaseValue = parameter->getValue(); |
| 114 | |
| 115 | // For bipolar mode, the effective modulation depth should be halved to match |
| 116 | // the perceived range of unipolar mode. |
| 117 | float effectiveDepth = routing.depth; |
| 118 | |
| 119 | // Apply bipolar (-1 to 1) or unipolar (0 to 1) mapping to the LFO signal. |
| 120 | if (routing.isBipolar) |
| 121 | { |
| 122 | lfoValue = lfoValue * 2.0f - 1.0f; // Map LFO from [0, 1] to [-1, 1] |
| 123 | effectiveDepth *= 0.5f; // Halve the depth for bipolar |
| 124 | } |
| 125 | |
| 126 | // The modulation amount is now a simple multiplication in the normalized space. |
| 127 | // The depth parameter scales the LFO output directly. |
| 128 | const float normalizedModulationAmount = lfoValue * effectiveDepth; |
| 129 | |
| 130 | // If this parameter hasn't been touched yet in this block, initialize it with its base normalized value. |
| 131 | if (modulatedValues.find(routing.targetParameterID) == modulatedValues.end()) |
| 132 | { |
| 133 | modulatedValues[routing.targetParameterID] = normalizedBaseValue; |
| 134 | } |
no outgoing calls