Skip to content

Commit 2b30b37

Browse files
fft effects for opus
1 parent 53c263f commit 2b30b37

11 files changed

Lines changed: 721 additions & 93 deletions
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// Created by arden on 7/31/26.
3+
//
4+
5+
#ifndef MAIM_OPUSBENDFLAGS_H
6+
#define MAIM_OPUSBENDFLAGS_H
7+
8+
#include <array>
9+
10+
// Mirrors the role of BendFlagsAndData (lame) / blade_bend_flags (blade): a single struct
11+
// of "bend" parameters that gets threaded through the encoder to warp the encoding process
12+
// away from the codec's intended behaviour. Opus has no exposed MDCT stage of its own (libopus
13+
// is used unmodified), so these bends are applied to our own FFT of the PCM signal that Opus
14+
// consumes/produces, standing in for the frequency-domain hooks lame/blade have natively.
15+
struct OpusBendFlags
16+
{
17+
// Frequency reassignment: mdctBandReassignment[destBand] = sourceBand, same 32-band
18+
// remapping idea as BendFlagsAndData::mdct_band_reassignments.
19+
static constexpr int numReassignmentBands = 32;
20+
std::array<int, numReassignmentBands> mdctBandReassignment = identityReassignment();
21+
22+
// Spectral effects: horizontal (frequency) and vertical (amplitude) shift, matching
23+
// BendFlagsAndData::mdct_post_h_shift / mdct_post_v_shift.
24+
int mdctPostHShift = 0;
25+
float mdctPostVShift = 0.0f;
26+
27+
// Spectral effects: blends each frame's spectrum with the previous frame's, matching
28+
// BendFlagsAndData::mdct_feedback.
29+
float mdctFeedback = 0.0f;
30+
31+
static std::array<int, numReassignmentBands> identityReassignment()
32+
{
33+
std::array<int, numReassignmentBands> order{};
34+
for (int i = 0; i < numReassignmentBands; ++i) {
35+
order[static_cast<size_t>(i)] = i;
36+
}
37+
return order;
38+
}
39+
40+
bool isIdentity() const
41+
{
42+
return mdctBandReassignment == identityReassignment()
43+
&& mdctPostHShift == 0
44+
&& mdctPostVShift >= 0.0f && mdctPostVShift <= 0.0f
45+
&& mdctFeedback >= 0.0f && mdctFeedback <= 0.0f;
46+
}
47+
};
48+
49+
#endif //MAIM_OPUSBENDFLAGS_H

source/CodecControllers/OpusController.cpp

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "OpusController.h"
66
#include "../parameterIds.h"
77
#include "../Mp3ControllerManager.h"
8+
#include "../SpectrumRescale.h"
89
OpusController::OpusController (juce::AudioProcessorValueTreeState& _parameters)
910
: bInitialized(false),
1011
opusEncoder(nullptr),
@@ -23,7 +24,14 @@ OpusController::OpusController (juce::AudioProcessorValueTreeState& _parameters)
2324
parameters.addParameterListener(PACKET_LOSS_STICK_PARAM_ID, this);
2425
parameters.addParameterListener(PACKET_LOSS_MODE_PARAM_ID, this);
2526
parameters.addParameterListener(ERROR_PARAM_ID, this);
27+
parameters.addParameterListener(MDCT_PITCH_SHIFT_PARAM_ID, this);
28+
parameters.addParameterListener(MDCT_AMPLITUDE_SHIFT_PARAM_ID, this);
29+
parameters.addParameterListener(MDCT_FEEDBACK_PARAM_ID, this);
30+
for (const auto& bandOrderParamId : BAND_ORDER_PARAM_IDS) {
31+
parameters.addParameterListener(bandOrderParamId, this);
32+
}
2633
parametersNeedUpdating = true;
34+
startTimerHz(30);
2735
}
2836

2937
OpusController::~OpusController()
@@ -38,6 +46,12 @@ OpusController::~OpusController()
3846
parameters.removeParameterListener(PACKET_LOSS_STICK_PARAM_ID, this);
3947
parameters.removeParameterListener(PACKET_LOSS_MODE_PARAM_ID, this);
4048
parameters.removeParameterListener(ERROR_PARAM_ID, this);
49+
parameters.removeParameterListener(MDCT_PITCH_SHIFT_PARAM_ID, this);
50+
parameters.removeParameterListener(MDCT_AMPLITUDE_SHIFT_PARAM_ID, this);
51+
parameters.removeParameterListener(MDCT_FEEDBACK_PARAM_ID, this);
52+
for (const auto& bandOrderParamId : BAND_ORDER_PARAM_IDS) {
53+
parameters.removeParameterListener(bandOrderParamId, this);
54+
}
4155
}
4256

4357
bool OpusController::init (int sampleRate, int, int)
@@ -64,6 +78,20 @@ bool OpusController::init (int sampleRate, int, int)
6478

6579
packetLossModel = std::make_unique<PacketLossModel>(sampleRate);
6680

81+
fftOrder = 1;
82+
while ((1 << fftOrder) < samplesPerFrame) {
83+
++fftOrder;
84+
}
85+
fftSize = 1 << fftOrder;
86+
fft = std::make_unique<juce::dsp::FFT>(fftOrder);
87+
88+
const auto numBins = fftSize / 2 + 1;
89+
for (auto channel = 0; channel < 2; ++channel) {
90+
fftBuffers[static_cast<size_t>(channel)].assign(static_cast<size_t>(fftSize) * 2, 0.0f);
91+
feedbackSpectrum[static_cast<size_t>(channel)].assign(static_cast<size_t>(numBins), std::complex<float>(0.0f, 0.0f));
92+
}
93+
reassignmentScratch.assign(static_cast<size_t>(numBins), std::complex<float>(0.0f, 0.0f));
94+
6795
return true;
6896
}
6997

@@ -89,6 +117,7 @@ void OpusController::processBlock (juce::AudioBuffer<float>& buffer)
89117
output[static_cast<size_t>(sampleCounter * 2 + 1)] = 0;
90118
sampleCounter++;
91119
if (sampleCounter == samplesPerFrame) {
120+
applyFrequencyBends();
92121
int framesizeDownscaleFactor = std::round(random.nextFloat() * turbo * 3);
93122
framesizeDownscaleFactor = 1 << std::min(std::max(0, framesizeDownscaleFactor), 2);
94123
auto subframeSize = samplesPerFrame / framesizeDownscaleFactor;
@@ -164,4 +193,184 @@ void OpusController::updateParameters()
164193

165194
error = *((juce::AudioParameterFloat*)parameters.getParameter(ERROR_PARAM_ID));
166195
error = std::pow(error, 3);
196+
197+
setMDCTpostshiftBends(
198+
((juce::AudioParameterInt*) parameters.getParameter(MDCT_PITCH_SHIFT_PARAM_ID))->get(),
199+
((juce::AudioParameterFloat*) parameters.getParameter(MDCT_AMPLITUDE_SHIFT_PARAM_ID))->get()
200+
);
201+
202+
setMDCTfeedback(
203+
((juce::AudioParameterFloat*) parameters.getParameter(MDCT_FEEDBACK_PARAM_ID))->get()
204+
);
205+
206+
int bandReassign[OpusBendFlags::numReassignmentBands];
207+
int i;
208+
for (i = 0; i < NUM_REASSIGNMENT_BANDS; ++i) {
209+
bandReassign[i] = ((juce::AudioParameterInt*) parameters.getParameter(BAND_ORDER_PARAM_IDS[static_cast<size_t>(i)]))->get();
210+
}
211+
for (; i < OpusBendFlags::numReassignmentBands; ++i) {
212+
bandReassign[i] = i;
213+
}
214+
setMDCTBandReassignmentBends(bandReassign);
215+
}
216+
217+
void OpusController::applyFrequencyBends()
218+
{
219+
const auto numBins = fftSize / 2 + 1;
220+
const bool identity = bendFlags.isIdentity();
221+
222+
// Accumulated across both channels, for the live "before/after" spectrum display - this
223+
// runs whether or not any bend is active, mirroring how lame/blade always snapshot their
224+
// MDCT spectrum for the graph regardless of bend settings.
225+
std::array<float, mdctDisplaySize> preSnapshot{};
226+
std::array<float, mdctDisplaySize> postSnapshot{};
227+
228+
for (auto channel = 0; channel < 2; ++channel) {
229+
auto& fftBuffer = fftBuffers[static_cast<size_t>(channel)];
230+
std::fill(fftBuffer.begin(), fftBuffer.end(), 0.0f);
231+
for (auto s = 0; s < samplesPerFrame; ++s) {
232+
fftBuffer[static_cast<size_t>(s)] = input[static_cast<size_t>(s * 2 + channel)];
233+
}
234+
235+
fft->performRealOnlyForwardTransform(fftBuffer.data(), true);
236+
237+
auto* bins = reinterpret_cast<std::complex<float>*>(fftBuffer.data());
238+
239+
accumulateSpectrumSnapshot(bins, numBins, preSnapshot);
240+
241+
// When every bend is at its neutral value, skip mutating the spectrum and skip the
242+
// inverse transform entirely, so the encoded audio is bit-for-bit identical to before
243+
// this FFT stage existed - only the (otherwise unused) snapshot above is computed.
244+
if (!identity) {
245+
applyBandReassignment(bins, numBins);
246+
applyPostShift(bins, numBins);
247+
applyFeedback(bins, numBins, channel);
248+
}
249+
250+
accumulateSpectrumSnapshot(bins, numBins, postSnapshot);
251+
252+
if (!identity) {
253+
fft->performRealOnlyInverseTransform(fftBuffer.data());
254+
255+
for (auto s = 0; s < samplesPerFrame; ++s) {
256+
input[static_cast<size_t>(s * 2 + channel)] = fftBuffer[static_cast<size_t>(s)];
257+
}
258+
}
259+
}
260+
261+
for (auto i = 0; i < mdctDisplaySize; ++i) {
262+
mdctPreBend[static_cast<size_t>(i)] = preSnapshot[static_cast<size_t>(i)] / 2.0f;
263+
mdctPostBend[static_cast<size_t>(i)] = postSnapshot[static_cast<size_t>(i)] / 2.0f;
264+
}
265+
}
266+
267+
void OpusController::accumulateSpectrumSnapshot(const std::complex<float>* bins, int numBins, std::array<float, mdctDisplaySize>& target) const
268+
{
269+
// Normalizes FFT bin magnitude to roughly the 0-1(+) range rescaleMDCT() expects, and
270+
// resamples the (sample-rate-dependent) bin count down to the graph's fixed display width.
271+
const auto normalization = static_cast<float>(fftSize) / 2.0f;
272+
for (auto i = 0; i < mdctDisplaySize; ++i) {
273+
auto sourceBin = std::min(numBins - 1, (i * numBins) / mdctDisplaySize);
274+
target[static_cast<size_t>(i)] += std::abs(bins[static_cast<size_t>(sourceBin)]) / normalization;
275+
}
276+
}
277+
278+
void OpusController::applyBandReassignment(std::complex<float>* bins, int numBins)
279+
{
280+
const auto numBands = OpusBendFlags::numReassignmentBands;
281+
if (bendFlags.mdctBandReassignment == OpusBendFlags::identityReassignment()) {
282+
return;
283+
}
284+
285+
std::copy(bins, bins + numBins, reassignmentScratch.begin());
286+
287+
auto bandBoundary = [numBins, numBands] (int band) {
288+
return (band * numBins) / numBands;
289+
};
290+
291+
for (auto band = 0; band < numBands; ++band) {
292+
auto destStart = bandBoundary(band);
293+
auto destEnd = bandBoundary(band + 1);
294+
auto srcBand = juce::jlimit(0, numBands - 1, bendFlags.mdctBandReassignment[static_cast<size_t>(band)]);
295+
auto srcStart = bandBoundary(srcBand);
296+
auto srcEnd = bandBoundary(srcBand + 1);
297+
auto count = std::min(destEnd - destStart, srcEnd - srcStart);
298+
for (auto i = 0; i < count; ++i) {
299+
bins[destStart + i] = reassignmentScratch[static_cast<size_t>(srcStart + i)];
300+
}
301+
}
302+
}
303+
304+
void OpusController::applyPostShift(std::complex<float>* bins, int numBins)
305+
{
306+
const auto hShift = bendFlags.mdctPostHShift;
307+
if (hShift != 0) {
308+
std::copy(bins, bins + numBins, reassignmentScratch.begin());
309+
for (auto i = 0; i < numBins; ++i) {
310+
auto srcIndex = i - hShift;
311+
bins[i] = (srcIndex >= 0 && srcIndex < numBins)
312+
? reassignmentScratch[static_cast<size_t>(srcIndex)]
313+
: std::complex<float>(0.0f, 0.0f);
314+
}
315+
}
316+
317+
const auto vShift = bendFlags.mdctPostVShift;
318+
if (vShift < 0.0f || vShift > 0.0f) {
319+
auto peak = 0.0f;
320+
for (auto i = 0; i < numBins; ++i) {
321+
peak = std::max(peak, std::abs(bins[i]));
322+
}
323+
if (peak > 0.0f) {
324+
for (auto i = 0; i < numBins; ++i) {
325+
auto mag = std::abs(bins[i]);
326+
float newMag;
327+
if (vShift > 0.0f) {
328+
newMag = mag + vShift * (peak - mag);
329+
} else {
330+
auto attenuation = std::max(0.0f, 1.0f + vShift * (1.0f - mag / peak));
331+
newMag = mag * attenuation;
332+
}
333+
if (mag > 1.0e-8f) {
334+
bins[i] *= (newMag / mag);
335+
} else if (vShift > 0.0f) {
336+
bins[i] = std::complex<float>(newMag, 0.0f);
337+
}
338+
}
339+
}
340+
}
341+
}
342+
343+
void OpusController::applyFeedback(std::complex<float>* bins, int numBins, int channel)
344+
{
345+
auto& previous = feedbackSpectrum[static_cast<size_t>(channel)];
346+
const auto feedback = bendFlags.mdctFeedback;
347+
if (feedback > 0.0f) {
348+
for (auto i = 0; i < numBins; ++i) {
349+
bins[i] = bins[i] * (1.0f - feedback) + previous[static_cast<size_t>(i)] * feedback;
350+
}
351+
}
352+
std::copy(bins, bins + numBins, previous.begin());
353+
}
354+
355+
void OpusController::timerCallback()
356+
{
357+
// Mp3ControllerManager (lame/blade) owns this same "mdct" ValueTree node while it's the
358+
// active encoder - only push while Opus is actually selected, so the two don't fight.
359+
bool isOpus = (((juce::AudioParameterChoice*)parameters.getParameter(ENCODER_PARAM_ID))->getIndex() == 2);
360+
if (!isOpus) {
361+
return;
362+
}
363+
364+
auto mdctSamples = parameters.state.getChildWithName("mdct");
365+
if (!mdctSamples.isValid()) {
366+
return;
367+
}
368+
369+
juce::var preBendV, postBendV;
370+
for (auto i = 0; i < mdctDisplaySize; ++i) {
371+
preBendV.append(rescaleMDCT(mdctPreBend[static_cast<size_t>(i)]));
372+
postBendV.append(rescaleMDCT(mdctPostBend[static_cast<size_t>(i)]));
373+
}
374+
mdctSamples.setProperty("pre", preBendV, nullptr);
375+
mdctSamples.setProperty("post", postBendV, nullptr);
167376
}

source/CodecControllers/OpusController.h

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@
88
#include "CodecController.h"
99
#include "juce_audio_basics/juce_audio_basics.h"
1010
#include "juce_audio_processors/juce_audio_processors.h"
11+
#include "juce_dsp/juce_dsp.h"
1112
#include "opus.h"
1213
#include <array>
14+
#include <complex>
1315
#include <vector>
1416
#include "../QueueBuffer.h"
1517
#include "PacketLossModel.h"
18+
#include "OpusBendFlags.h"
1619

17-
class OpusController : public CodecController, public juce::AudioProcessorValueTreeState::Listener {
20+
class OpusController : public CodecController, public juce::AudioProcessorValueTreeState::Listener, public juce::Timer {
1821
public:
1922
explicit OpusController(juce::AudioProcessorValueTreeState& _parameters);
2023

@@ -33,12 +36,27 @@ class OpusController : public CodecController, public juce::AudioProcessorValueT
3336
void setError(float) override {}
3437
void setButterflyBends(float, float, float, float) override {}
3538
void setMDCTbandstepBends(bool, int) override {}
36-
void setMDCTpostshiftBends(int, float) override {}
3739
void setMDCTwindowincrBends(int) override {}
38-
void setMDCTBandReassignmentBends(int*) override {}
3940
void setBitrateSquishBends(float) override {}
4041
void setThresholdBias(float) override {}
41-
void setMDCTfeedback(float) override {}
42+
43+
void setMDCTpostshiftBends(int h_shift, float v_shift) override
44+
{
45+
bendFlags.mdctPostHShift = h_shift;
46+
bendFlags.mdctPostVShift = v_shift;
47+
}
48+
49+
void setMDCTBandReassignmentBends(int* order) override
50+
{
51+
for (int i = 0; i < OpusBendFlags::numReassignmentBands; ++i) {
52+
bendFlags.mdctBandReassignment[static_cast<size_t>(i)] = order[i];
53+
}
54+
}
55+
56+
void setMDCTfeedback(float feedback) override
57+
{
58+
bendFlags.mdctFeedback = feedback;
59+
}
4260

4361
float* getPsychoanalThreshold() override {
4462
return nullptr;
@@ -47,10 +65,10 @@ class OpusController : public CodecController, public juce::AudioProcessorValueT
4765
return nullptr;
4866
}
4967
float* getMDCTpreBend() override {
50-
return nullptr;
68+
return mdctPreBend.data();
5169
}
5270
float* getMDCTpostBend() override {
53-
return nullptr;
71+
return mdctPostBend.data();
5472
}
5573
int getShortBlockStatus() override {
5674
return 0;
@@ -73,6 +91,14 @@ class OpusController : public CodecController, public juce::AudioProcessorValueT
7391
return false;
7492
}
7593

94+
static constexpr int mdctDisplaySize = 576;
95+
96+
void applyFrequencyBends();
97+
void applyBandReassignment(std::complex<float>* bins, int numBins);
98+
void applyPostShift(std::complex<float>* bins, int numBins);
99+
void applyFeedback(std::complex<float>* bins, int numBins, int channel);
100+
void accumulateSpectrumSnapshot(const std::complex<float>* bins, int numBins, std::array<float, mdctDisplaySize>& target) const;
101+
void timerCallback() override;
76102

77103
bool bInitialized;
78104
OpusEncoder* opusEncoder;
@@ -106,6 +132,16 @@ class OpusController : public CodecController, public juce::AudioProcessorValueT
106132
PacketLossMode packetLossMode{PacketLossMode::pulse};
107133
int encodeResult{};
108134
float error{};
135+
136+
OpusBendFlags bendFlags;
137+
std::unique_ptr<juce::dsp::FFT> fft;
138+
int fftOrder{};
139+
int fftSize{};
140+
std::array<std::vector<float>, 2> fftBuffers;
141+
std::vector<std::complex<float>> reassignmentScratch;
142+
std::array<std::vector<std::complex<float>>, 2> feedbackSpectrum;
143+
std::array<float, mdctDisplaySize> mdctPreBend{};
144+
std::array<float, mdctDisplaySize> mdctPostBend{};
109145
};
110146

111147
#endif //MAIM_OPUSCONTROLLER_H

0 commit comments

Comments
 (0)