| 103 | } |
| 104 | |
| 105 | void ClientPhaseRotator::process(float* interleaved, int frames, int channels) noexcept |
| 106 | { |
| 107 | if (!isEnabled() || frames <= 0 || channels < 1 || channels > 2) return; |
| 108 | recacheIfDirty(); |
| 109 | if (m_cachedStages <= 0) return; |
| 110 | |
| 111 | // Block-level peak tracking, signed — record max positive sample and |
| 112 | // most negative sample at both input and output. Asymmetry meter |
| 113 | // visualises peakPos vs |peakNeg|; phase rotation should make those |
| 114 | // values converge. |
| 115 | float inPos = 0.0f, inNeg = 0.0f, outPos = 0.0f, outNeg = 0.0f; |
| 116 | |
| 117 | for (int i = 0; i < frames; ++i) { |
| 118 | for (int ch = 0; ch < channels; ++ch) { |
| 119 | const float x = interleaved[i * channels + ch]; |
| 120 | if (x > inPos) inPos = x; |
| 121 | if (x < inNeg) inNeg = x; |
| 122 | |
| 123 | float v = x; |
| 124 | for (int s = 0; s < m_cachedStages; ++s) { |
| 125 | auto& f = m_sections[s]; |
| 126 | // Direct Form I all-pass: |
| 127 | // y[n] = a2*x[n] + a1*x[n-1] + x[n-2] - a1*y[n-1] - a2*y[n-2] |
| 128 | const float y = f.a2 * v |
| 129 | + f.a1 * f.x1[ch] |
| 130 | + f.x2[ch] |
| 131 | - f.a1 * f.y1[ch] |
| 132 | - f.a2 * f.y2[ch]; |
| 133 | f.x2[ch] = f.x1[ch]; |
| 134 | f.x1[ch] = v; |
| 135 | f.y2[ch] = f.y1[ch]; |
| 136 | f.y1[ch] = y; |
| 137 | v = y; |
| 138 | } |
| 139 | interleaved[i * channels + ch] = v; |
| 140 | |
| 141 | if (v > outPos) outPos = v; |
| 142 | if (v < outNeg) outNeg = v; |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | m_meters.inPosDb.store(linToDb(inPos), std::memory_order_relaxed); |
| 147 | m_meters.inNegDb.store(linToDb(-inNeg), std::memory_order_relaxed); |
| 148 | m_meters.outPosDb.store(linToDb(outPos), std::memory_order_relaxed); |
| 149 | m_meters.outNegDb.store(linToDb(-outNeg), std::memory_order_relaxed); |
| 150 | } |
| 151 | |
| 152 | } // namespace AetherSDR |