Conversation
Signed-off-by: uju09 <24bcs012@iiitdwd.ac.in>
|
🎉 Welcome @uju09!
We appreciate your contribution! 🚀 |
📝 WalkthroughWalkthroughThe PR introduces a JUCE-based DSP engine for real-time client-side voice processing. It adds JUCE as a submodule, establishes CMake build configuration for a static DSP library, and implements seven C++ classes that provide noise gating, dynamic compression, pitch shifting, formant filtering, and voice profile management orchestrated through a central DspEngine. Changes
Sequence DiagramsequenceDiagram
participant Client
participant DspEngine
participant NoiseGate
participant VoiceProcessor
participant Compressor
participant ResonanceDSP
Client->>DspEngine: prepare(sampleRate, blockSize)
DspEngine->>NoiseGate: prepare(...)
DspEngine->>VoiceProcessor: prepare(...)
DspEngine->>Compressor: prepare(...)
DspEngine->>ResonanceDSP: prepare(...)
Client->>DspEngine: setVoiceProfile(profile)
DspEngine->>DspEngine: applyProfileConfig(config)
DspEngine->>NoiseGate: setThreshold/Ratio(...)
DspEngine->>VoiceProcessor: setAnonymizationStrength(...)
DspEngine->>Compressor: setThreshold/Ratio/Attack/Release(...)
DspEngine->>ResonanceDSP: setGain(...)
loop Per Audio Block
Client->>DspEngine: process(AudioBuffer)
alt bypass disabled
DspEngine->>NoiseGate: process(buffer)
NoiseGate-->>DspEngine: return
DspEngine->>VoiceProcessor: process(buffer)
VoiceProcessor-->>DspEngine: return
DspEngine->>Compressor: process(buffer)
Compressor-->>DspEngine: return
DspEngine->>ResonanceDSP: process(buffer)
ResonanceDSP-->>DspEngine: return
else bypass enabled
DspEngine-->>Client: return
end
end
Client->>DspEngine: reset()
DspEngine->>NoiseGate: reset()
DspEngine->>Compressor: reset()
DspEngine->>ResonanceDSP: reset()
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai Review. |
|
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Fix all issues with AI agents
In `@android/app/src/main/cpp/dsp/include/VoiceProcessor.h`:
- Around line 14-16: The member float strength must be made thread-safe: include
<atomic> and change the field type to std::atomic<float> strength (keep default
0.5f), update setAnonymizationStrength(...) to call strength.store(newValue,
std::memory_order_relaxed) and update all reads inside process(),
processBlock(), or any other audio-thread functions to use
strength.load(std::memory_order_relaxed); ensure function signatures
(setAnonymizationStrength, process) remain the same and only the
field/type/loads/stores are changed.
In `@android/app/src/main/cpp/dsp/include/VoiceProfile.h`:
- Around line 36-39: VoiceProfileConfig currently exposes useBandpass,
bandpassLowFreq, and bandpassHighFreq but VoiceProcessor still applies hardcoded
formant frequencies (600/1600/3200Hz); either remove these unused members from
VoiceProfile.h (float bandpassLowFreq, float bandpassHighFreq, bool useBandpass)
and any references in profile setup, or implement bandpass behavior in
VoiceProcessor: in the method that applies formant filtering (e.g.,
applyFormantFilter/processFrame in VoiceProcessor) check
VoiceProfileConfig::useBandpass and when true compute a bandpass filter using
VoiceProfileConfig::bandpassLowFreq and bandpassHighFreq (design IIR/FIR
coefficients or use existing filter utilities), replace the hardcoded
formant-stage frequencies with the bandpass filter output, and ensure parameter
validation (clamp freqs < nyquist) and unit tests are updated accordingly.
In `@android/app/src/main/cpp/dsp/src/Compressor.cpp`:
- Line 1: The compressor is being prepared with a hardcoded mono channel count
(ProcessSpec.numChannels = 1) while DspEngine::process() may receive
multi-channel buffers, causing only the first channel to be processed; update
the prepare()/initialization logic to use the actual channel count (e.g., take
buffer.getNumChannels() or a numChannels parameter) or reinitialize the
compressor per buffer so ProcessSpec.numChannels matches the incoming buffer,
and ensure DspEngine::process() forwards the correct channel count when calling
the compressor so all channels are processed.
- Around line 3-16: Compressor::prepare currently hardcodes spec.numChannels = 1
without explaining or safeguarding that the pipeline is mono; update the method
to either accept an int numChannels parameter (e.g., change signature to
prepare(double sampleRate, int blockSize, int numChannels)) and validate
numChannels > 0 (and use it when setting spec.numChannels before calling
compressor.prepare(spec)), or add an explicit runtime check/assertion at the top
of Compressor::prepare (e.g., assert(numChannels == 1) or
jassert(spec.numChannels == 1) with a clear comment) and log a warning if a
non-mono channel count is detected; ensure references to currentSampleRate and
compressor.prepare(spec) remain intact and that threshold/ratio/attack/release
loads run only after the channel validation.
In `@android/app/src/main/cpp/dsp/src/DspEngine.cpp`:
- Around line 71-87: applyProfileConfig is only applying anonymization,
compressor, noise gate and gain, leaving pitchShiftSemitones,
formantPreservation and band‑pass related fields unused which makes profiles
misleading; update DspEngine::applyProfileConfig to wire those fields into the
processing chain (e.g. call
pitchShifter.setSemitones(config.pitchShiftSemitones), call
formantPreserver.setPreservationAmount or
formantPreserver.setEnabled(config.formantPreservation), and set band‑pass
parameters on the bandPassFilter or filterBank via methods like
setLowCut/setHighCut or setBand(config.bandPassLowHz, config.bandPassHighHz)),
or if those processing objects don’t exist remove the unused config fields and
corresponding profile definitions so profiles don’t advertise unsupported
features.
In `@android/app/src/main/cpp/dsp/src/VoiceProcessor.cpp`:
- Around line 39-52: processPitch is passing a pitch ratio into
pitchDelay.popSample (which expects a delay in samples), so no real pitch shift
occurs; change VoiceProcessor::processPitch to compute a proper read-delay in
samples from the desired semitoneShift (use sampleRate and a base delay window)
and drive the read index over time (e.g., fractional read with interpolation
plus an LFO/modulated delay sweep or a granular/time‑stretch routine) instead of
feeding the ratio directly to pitchDelay.popSample; update uses of
semitoneShift, ratio, pitchDelay.pushSample and pitchDelay.popSample to
implement either (A) a modulated fractional read position = baseDelaySamples /
ratio (with interpolation) or (B) a granular overlap-add/time-stretching method
that reads from pitchDelay buffer at fractional positions to achieve the
intended pitch shift.
- Around line 24-37: The strength member is accessed concurrently and must be
made atomic: change the VoiceProcessor::strength field to std::atomic<float>,
update VoiceProcessor::setAnonymizationStrength to call
strength.store(sanitizedValue, std::memory_order_relaxed) (use jlimit before
storing), and replace all direct reads of strength in VoiceProcessor::process,
VoiceProcessor::processPitch and VoiceProcessor::processFormants with
strength.load(std::memory_order_relaxed); ensure the initial value and any other
reads/writes use the atomic APIs to remove the data race.
🧹 Nitpick comments (3)
android/app/src/main/cpp/dsp/include/VoiceProfile.h (1)
45-94: Add adefaultcase for forward compatibility.If a new
VoiceProfileenum value is added later but the switch isn't updated, the function will return a partially-initializedconfig(only default-constructed values). Adding adefaultcase makes this explicit and prevents silent bugs.♻️ Proposed fix
case VoiceProfile::Robot: config.pitchShiftSemitones = 0.0f; config.formantPreservation = 0.2f; config.anonymizationStrength = 0.0f; config.compressorThresholdDb = -6.0f; config.compressorRatio = 6.0f; config.useBandpass = true; config.bandpassLowFreq = 300.0f; config.bandpassHighFreq = 3000.0f; config.outputGain = 1.0f; break; + + default: + // Fall back to Natural profile for unknown values + config.pitchShiftSemitones = 0.0f; + config.formantPreservation = 0.5f; + config.anonymizationStrength = 0.0f; + config.compressorThresholdDb = -12.0f; + config.compressorRatio = 2.0f; + config.outputGain = 1.0f; + break; }android/app/src/main/cpp/dsp/CMakeLists.txt (1)
13-19: Consider explicit source listing overfile(GLOB).CMake documentation discourages
file(GLOB)for source collection because adding/removing files won't trigger a CMake reconfiguration. For a small, stable set of DSP sources this is often acceptable, but be aware that you'll need to manually re-run CMake when files change.android/app/src/main/cpp/dsp/src/DspEngine.cpp (1)
34-38: Consider resetting VoiceProcessor state too.
reset()clears noise gate/compressor/output stage but leaves VoiceProcessor stateful filters/delay intact; consider addingVoiceProcessor::reset()and calling it here to avoid stale state after a reset.
| private: | ||
| double sampleRate = 44100.0; | ||
| float strength = 0.5f; |
There was a problem hiding this comment.
Thread safety: strength should be atomic for cross-thread access.
Per the PR objectives, parameter updates should be callable from any thread. The strength field is a plain float but setAnonymizationStrength() can be called from a non-audio thread while process() reads it on the audio thread. This constitutes a data race.
🔒 Proposed fix
private:
double sampleRate = 44100.0;
- float strength = 0.5f;
+ std::atomic<float> strength{0.5f};Also add #include <atomic> at the top of the file, and update setAnonymizationStrength and usages in process* methods to use .store() and .load() respectively.
🤖 Prompt for AI Agents
In `@android/app/src/main/cpp/dsp/include/VoiceProcessor.h` around lines 14 - 16,
The member float strength must be made thread-safe: include <atomic> and change
the field type to std::atomic<float> strength (keep default 0.5f), update
setAnonymizationStrength(...) to call strength.store(newValue,
std::memory_order_relaxed) and update all reads inside process(),
processBlock(), or any other audio-thread functions to use
strength.load(std::memory_order_relaxed); ensure function signatures
(setAnonymizationStrength, process) remain the same and only the
field/type/loads/stores are changed.
| // Bandpass filter for robot effect (Hz) | ||
| float bandpassLowFreq = 200.0f; | ||
| float bandpassHighFreq = 4000.0f; | ||
| bool useBandpass = false; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for usages of useBandpass or bandpass parameters in the DSP codebase
rg -n "useBandpass|bandpassLowFreq|bandpassHighFreq" android/app/src/main/cpp/dsp/Repository: AOSSIE-Org/Resonate
Length of output: 597
🏁 Script executed:
#!/bin/bash
# Get context around the assignments to understand where they occur
sed -n '80,100p' android/app/src/main/cpp/dsp/include/VoiceProfile.h
# Search for VoiceProcessor implementation and how it uses the config
fd -e "cpp" -e "h" | xargs grep -l "VoiceProcessor" | head -5Repository: AOSSIE-Org/Resonate
Length of output: 702
🏁 Script executed:
#!/bin/bash
# Search VoiceProcessor for usage of bandpass parameters and how config is consumed
rg -n "bandpass|useBandpass" android/app/src/main/cpp/dsp/src/VoiceProcessor.cpp
rg -n "bandpass|useBandpass" android/app/src/main/cpp/dsp/include/VoiceProcessor.h
# Also check if config is used to consume the bandpass settings
rg -n "config\." android/app/src/main/cpp/dsp/src/VoiceProcessor.cpp | head -20Repository: AOSSIE-Org/Resonate
Length of output: 45
🏁 Script executed:
#!/bin/bash
# Check the size and content of VoiceProcessor.cpp to understand config usage
wc -l android/app/src/main/cpp/dsp/src/VoiceProcessor.cpp
# Get the function that processes using config
rg -n "void process|applyFilters|Robot" android/app/src/main/cpp/dsp/src/VoiceProcessor.cpp -A 5 | head -40Repository: AOSSIE-Org/Resonate
Length of output: 117
🏁 Script executed:
#!/bin/bash
# Read the entire VoiceProcessor.cpp
cat -n android/app/src/main/cpp/dsp/src/VoiceProcessor.cpp
# Also check the header
echo "=== VoiceProcessor.h ==="
cat -n android/app/src/main/cpp/dsp/include/VoiceProcessor.hRepository: AOSSIE-Org/Resonate
Length of output: 3848
Remove unused bandpass configuration or implement the feature.
The useBandpass, bandpassLowFreq, and bandpassHighFreq parameters are set in VoiceProfileConfig for the Robot profile, but VoiceProcessor does not consume them. The processor uses fixed hardcoded formant filter frequencies (600 Hz, 1600 Hz, 3200 Hz) instead of the bandpass parameters. Either remove these unused configuration fields or implement bandpass filtering to use them.
🤖 Prompt for AI Agents
In `@android/app/src/main/cpp/dsp/include/VoiceProfile.h` around lines 36 - 39,
VoiceProfileConfig currently exposes useBandpass, bandpassLowFreq, and
bandpassHighFreq but VoiceProcessor still applies hardcoded formant frequencies
(600/1600/3200Hz); either remove these unused members from VoiceProfile.h (float
bandpassLowFreq, float bandpassHighFreq, bool useBandpass) and any references in
profile setup, or implement bandpass behavior in VoiceProcessor: in the method
that applies formant filtering (e.g., applyFormantFilter/processFrame in
VoiceProcessor) check VoiceProfileConfig::useBandpass and when true compute a
bandpass filter using VoiceProfileConfig::bandpassLowFreq and bandpassHighFreq
(design IIR/FIR coefficients or use existing filter utilities), replace the
hardcoded formant-stage frequencies with the bandpass filter output, and ensure
parameter validation (clamp freqs < nyquist) and unit tests are updated
accordingly.
| @@ -0,0 +1,46 @@ | |||
| #include "Compressor.h" | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Step 1: Locate Compressor.h header
echo "=== Locating Compressor.h ==="
fd "Compressor.h" --type f
# Step 2: Check include directories in CMakeLists.txt files
echo -e "\n=== Include paths in CMakeLists.txt ==="
fd "CMakeLists.txt" | xargs grep -l "dsp\|Compressor" | head -5
fd "CMakeLists.txt" | xargs grep -n "target_include_directories\|include_directories" | grep -i "dsp\|cpp" | head -20Repository: AOSSIE-Org/Resonate
Length of output: 271
🏁 Script executed:
#!/bin/bash
# Step 3: Examine Compressor.cpp to find prepare() method
echo "=== Checking Compressor.cpp structure ==="
wc -l android/app/src/main/cpp/dsp/src/Compressor.cpp
head -50 android/app/src/main/cpp/dsp/src/Compressor.cpp
# Step 4: Search for prepare() method and ProcessSpec.numChannels
echo -e "\n=== Searching for prepare() method ==="
rg -n "void prepare|ProcessSpec.numChannels" android/app/src/main/cpp/dsp/src/Compressor.cpp -A 3Repository: AOSSIE-Org/Resonate
Length of output: 1481
🏁 Script executed:
#!/bin/bash
# Step 5: Check DspEngine for buffer setup
echo "=== Locating DspEngine ==="
fd "DspEngine" --type f | head -10
# Step 6: Look for how DspEngine initializes Compressor
echo -e "\n=== DspEngine usage of Compressor ==="
rg -n "Compressor\|prepare" android/app/src/main/cpp/dsp/src/ --type cpp -A 2 | head -40Repository: AOSSIE-Org/Resonate
Length of output: 224
🏁 Script executed:
#!/bin/bash
# Step 1: Check CMakeLists.txt include configuration
echo "=== CMakeLists.txt include_directories setup (line 28 context) ==="
sed -n '20,40p' android/app/src/main/cpp/dsp/CMakeLists.txt
# Step 2: Check if include directory is in the target_include_directories
echo -e "\n=== Full target_include_directories block ==="
sed -n '28,45p' android/app/src/main/cpp/dsp/CMakeLists.txtRepository: AOSSIE-Org/Resonate
Length of output: 963
🏁 Script executed:
#!/bin/bash
# Step 3: Examine DspEngine.cpp to see how Compressor is used
echo "=== DspEngine.cpp size and Compressor usage ==="
wc -l android/app/src/main/cpp/dsp/src/DspEngine.cpp
echo -e "\n=== Searching Compressor references in DspEngine.cpp ==="
rg -n "Compressor\|compressor" android/app/src/main/cpp/dsp/src/DspEngine.cpp -B 2 -A 5
# Step 4: Check DspEngine.h header for Compressor declaration
echo -e "\n=== DspEngine.h Compressor members ==="
rg -n "Compressor" android/app/src/main/cpp/dsp/include/DspEngine.h -B 1 -A 1Repository: AOSSIE-Org/Resonate
Length of output: 556
🏁 Script executed:
#!/bin/bash
# Step 5: Check process() call in DspEngine to see what buffer is passed
echo "=== DspEngine process method ==="
rg -n "void process|AudioBuffer" android/app/src/main/cpp/dsp/src/DspEngine.cpp -A 10 | head -50Repository: AOSSIE-Org/Resonate
Length of output: 394
Hardcoded mono channel count doesn't match buffer being processed.
The prepare() method sets ProcessSpec.numChannels = 1, but DspEngine::process() passes buffers that may be stereo or multi-channel. This causes the JUCE compressor to process only the first channel and ignore others. Either pass the actual channel count from the buffer or dynamically determine it during processing.
The include path #include "Compressor.h" is properly resolved by CMakeLists.txt which adds ${CMAKE_CURRENT_LIST_DIR}/include to public include directories, so no build-blocking issue exists there.
🧰 Tools
🪛 Clang (14.0.6)
[error] 1-1: 'Compressor.h' file not found
(clang-diagnostic-error)
🤖 Prompt for AI Agents
In `@android/app/src/main/cpp/dsp/src/Compressor.cpp` at line 1, The compressor is
being prepared with a hardcoded mono channel count (ProcessSpec.numChannels = 1)
while DspEngine::process() may receive multi-channel buffers, causing only the
first channel to be processed; update the prepare()/initialization logic to use
the actual channel count (e.g., take buffer.getNumChannels() or a numChannels
parameter) or reinitialize the compressor per buffer so ProcessSpec.numChannels
matches the incoming buffer, and ensure DspEngine::process() forwards the
correct channel count when calling the compressor so all channels are processed.
| void Compressor::prepare(double sampleRate, int blockSize) { | ||
| currentSampleRate = sampleRate; | ||
|
|
||
| juce::dsp::ProcessSpec spec; | ||
| spec.sampleRate = sampleRate; | ||
| spec.maximumBlockSize = static_cast<juce::uint32>(blockSize); | ||
| spec.numChannels = 1; | ||
|
|
||
| compressor.prepare(spec); | ||
| compressor.setThreshold(threshold.load()); | ||
| compressor.setRatio(ratio.load()); | ||
| compressor.setAttack(attack.load()); | ||
| compressor.setRelease(release.load()); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find where Compressor::prepare is called
rg -n "Compressor::prepare" android/app/src/main/cpp/dspRepository: AOSSIE-Org/Resonate
Length of output: 173
🏁 Script executed:
#!/bin/bash
# Search for AudioBuffer and ProcessSpec usage in the dsp directory
rg -n "AudioBuffer<|ProcessSpec|numChannels" android/app/src/main/cpp/dsp | head -50Repository: AOSSIE-Org/Resonate
Length of output: 2520
🏁 Script executed:
#!/bin/bash
# Find Compressor class definition and its usage context
find android/app/src/main/cpp/dsp -name "*.h" -o -name "*.cpp" | xargs rg -l "Compressor"Repository: AOSSIE-Org/Resonate
Length of output: 361
🏁 Script executed:
#!/bin/bash
# Check DspEngine.cpp to see how it initializes processors
cat -n android/app/src/main/cpp/dsp/src/DspEngine.cppRepository: AOSSIE-Org/Resonate
Length of output: 2966
🏁 Script executed:
#!/bin/bash
# Check Compressor.h header to see the class definition
cat -n android/app/src/main/cpp/dsp/include/Compressor.hRepository: AOSSIE-Org/Resonate
Length of output: 957
🏁 Script executed:
#!/bin/bash
# Search for any "prepare" calls in the codebase to find initialization points
rg -n "\.prepare\(" android/app/src/main/cpp/dspRepository: AOSSIE-Org/Resonate
Length of output: 765
🏁 Script executed:
#!/bin/bash
# Search for where DspEngine::process is called and how buffer is created
rg -n "DspEngine.*process|AudioBuffer" android/app/src/main/cpp --type cpp | grep -E "(DspEngine|AudioBuffer)" | head -30Repository: AOSSIE-Org/Resonate
Length of output: 1865
🏁 Script executed:
#!/bin/bash
# Check for main audio processing entry points (JNI, callbacks, etc.)
find android/app/src/main/cpp -name "*.cpp" -o -name "*.h" | xargs rg -l "processBlock|getNextAudioBlock|juce::AudioIODeviceCallback" 2>/dev/nullRepository: AOSSIE-Org/Resonate
Length of output: 47
🏁 Script executed:
#!/antml:
# Look for buffer configuration or initialization
rg -n "getNumChannels|setNumChannels|buffer\s*=" android/app/src/main/cpp/dsp | head -20Repository: AOSSIE-Org/Resonate
Length of output: 45
Document the mono channel design and add safeguards.
The Compressor hardcodes numChannels = 1, but this is consistent across the entire DSP chain (NoiseGate, VoiceProcessor, Compressor all do the same). While the "mono for now" comment in ResonanceDSP suggests intentional design, there are no safeguards if stereo audio reaches the pipeline. If the audio architecture ever changes or accepts stereo input, the JUCE DSP processors will fail silently or assert.
Consider: (1) Add an assertion or static check confirming mono input, or (2) refactor prepare() to accept and validate the channel count parameter rather than hardcoding it.
🤖 Prompt for AI Agents
In `@android/app/src/main/cpp/dsp/src/Compressor.cpp` around lines 3 - 16,
Compressor::prepare currently hardcodes spec.numChannels = 1 without explaining
or safeguarding that the pipeline is mono; update the method to either accept an
int numChannels parameter (e.g., change signature to prepare(double sampleRate,
int blockSize, int numChannels)) and validate numChannels > 0 (and use it when
setting spec.numChannels before calling compressor.prepare(spec)), or add an
explicit runtime check/assertion at the top of Compressor::prepare (e.g.,
assert(numChannels == 1) or jassert(spec.numChannels == 1) with a clear comment)
and log a warning if a non-mono channel count is detected; ensure references to
currentSampleRate and compressor.prepare(spec) remain intact and that
threshold/ratio/attack/release loads run only after the channel validation.
| void DspEngine::applyProfileConfig(const VoiceProfileConfig &config) { | ||
| // Apply to VoiceProcessor | ||
| voiceProcessor.setAnonymizationStrength(config.anonymizationStrength); | ||
|
|
||
| // Apply to Compressor | ||
| compressor.setThreshold(config.compressorThresholdDb); | ||
| compressor.setRatio(config.compressorRatio); | ||
| compressor.setAttack(config.compressorAttackMs); | ||
| compressor.setRelease(config.compressorReleaseMs); | ||
|
|
||
| // Apply to NoiseGate | ||
| noiseGate.setThreshold(config.noiseGateThresholdDb); | ||
| noiseGate.setRatio(config.noiseGateRatio); | ||
|
|
||
| // Apply to output stage | ||
| outputStage.setGain(config.outputGain); | ||
| } |
There was a problem hiding this comment.
Voice profile fields aren’t fully applied.
applyProfileConfig uses only anonymizationStrength, compressor settings, noise gate threshold/ratio, and output gain. Fields like pitchShiftSemitones, formantPreservation, and band‑pass settings never affect the pipeline, so Deep/Bright/Robot profiles won’t reflect their intended pitch/filter changes. Either wire these through the processing chain or remove the unused fields to avoid misleading profiles.
🤖 Prompt for AI Agents
In `@android/app/src/main/cpp/dsp/src/DspEngine.cpp` around lines 71 - 87,
applyProfileConfig is only applying anonymization, compressor, noise gate and
gain, leaving pitchShiftSemitones, formantPreservation and band‑pass related
fields unused which makes profiles misleading; update
DspEngine::applyProfileConfig to wire those fields into the processing chain
(e.g. call pitchShifter.setSemitones(config.pitchShiftSemitones), call
formantPreserver.setPreservationAmount or
formantPreserver.setEnabled(config.formantPreservation), and set band‑pass
parameters on the bandPassFilter or filterBank via methods like
setLowCut/setHighCut or setBand(config.bandPassLowHz, config.bandPassHighHz)),
or if those processing objects don’t exist remove the unused config fields and
corresponding profile definitions so profiles don’t advertise unsupported
features.
| void VoiceProcessor::setAnonymizationStrength(float s) | ||
| { | ||
| strength = juce::jlimit(0.0f, 1.0f, s); | ||
| } | ||
|
|
||
| void VoiceProcessor::process(juce::AudioBuffer<float> &buffer) | ||
| { | ||
| if (strength <= 0.01f) | ||
| return; | ||
|
|
||
| processPitch(buffer); | ||
| processFormants(buffer); | ||
| processSmoothing(buffer); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find VoiceProcessor.h and inspect the strength member type
fd -a 'VoiceProcessor\.h$' . | head -n 1 | xargs -I {} sh -c 'echo "File: {}"; cat -n "{}"'Repository: AOSSIE-Org/Resonate
Length of output: 1249
🏁 Script executed:
#!/bin/bash
# Check VoiceProcessor.cpp for mutex or synchronization mechanisms
fd -a 'VoiceProcessor\.cpp$' . | head -n 1 | xargs -I {} sh -c 'echo "File: {}"; cat -n "{}"'Repository: AOSSIE-Org/Resonate
Length of output: 2793
Fix thread-safety race condition on strength member.
The strength member is a plain float accessed without synchronization across setAnonymizationStrength() (write) and multiple reads in process(), processPitch(), and processFormants(). In multi-threaded audio processing, this creates a data race. Convert to std::atomic<float> and use store()/load() with memory_order_relaxed:
Suggested changes
--- a/android/app/src/main/cpp/dsp/include/VoiceProcessor.h
+++ b/android/app/src/main/cpp/dsp/include/VoiceProcessor.h
@@ -13,7 +13,7 @@ private:
double sampleRate = 44100.0;
- float strength = 0.5f;
+ std::atomic<float> strength{0.5f};--- a/android/app/src/main/cpp/dsp/src/VoiceProcessor.cpp
+++ b/android/app/src/main/cpp/dsp/src/VoiceProcessor.cpp
@@ -24,13 +24,13 @@ void VoiceProcessor::prepare(double sr, int blockSize)
void VoiceProcessor::setAnonymizationStrength(float s)
{
- strength = juce::jlimit(0.0f, 1.0f, s);
+ strength.store(juce::jlimit(0.0f, 1.0f, s), std::memory_order_relaxed);
}
void VoiceProcessor::process(juce::AudioBuffer<float> &buffer)
{
- if (strength <= 0.01f)
+ if (strength.load(std::memory_order_relaxed) <= 0.01f)
return;Also update line 41 (processPitch) and line 66 (processFormants) to use .load(std::memory_order_relaxed).
🤖 Prompt for AI Agents
In `@android/app/src/main/cpp/dsp/src/VoiceProcessor.cpp` around lines 24 - 37,
The strength member is accessed concurrently and must be made atomic: change the
VoiceProcessor::strength field to std::atomic<float>, update
VoiceProcessor::setAnonymizationStrength to call strength.store(sanitizedValue,
std::memory_order_relaxed) (use jlimit before storing), and replace all direct
reads of strength in VoiceProcessor::process, VoiceProcessor::processPitch and
VoiceProcessor::processFormants with strength.load(std::memory_order_relaxed);
ensure the initial value and any other reads/writes use the atomic APIs to
remove the data race.
| void VoiceProcessor::processPitch(juce::AudioBuffer<float> &buffer) | ||
| { | ||
| const float semitoneShift = juce::jmap(strength, -4.0f, 4.0f); | ||
| const float ratio = std::pow(2.0f, semitoneShift / 12.0f); | ||
|
|
||
| auto *data = buffer.getWritePointer(0); | ||
| const int n = buffer.getNumSamples(); | ||
|
|
||
| for (int i = 0; i < n; ++i) | ||
| { | ||
| pitchDelay.pushSample(0, data[i]); | ||
| data[i] = pitchDelay.popSample(0, ratio); | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
JUCE dsp::DelayLine popSample API parameter meaning
💡 Result:
juce::dsp::DelayLine::popSample (int channel, SampleType delayInSamples = -1, bool updateReadPointer = true) parameters mean:
channel: which delay-line channel to read from (e.g., 0 = left, 1 = right). (docs.juce.com)delayInSamples: the desired delay time (can be fractional) in samples for this read; pass-1to reuse the delay currently stored in the object (set previously, e.g. viasetDelay). (docs.juce.com)updateReadPointer: whether this read should advance the internal read position. Usetruefor normal “one output sample per input sample” operation; usefalseto do multi-tap reads (several reads at different delays from the same write position) without advancing the read pointer between taps. (docs.juce.com)
Citations:
- 1: https://docs.juce.com/develop/classjuce_1_1dsp_1_1DelayLine.html?utm_source=openai
- 2: https://docs.juce.com/develop/classjuce_1_1dsp_1_1DelayLine.html?utm_source=openai
- 3: https://docs.juce.com/develop/classjuce_1_1dsp_1_1DelayLine.html?utm_source=openai
Fix pitch‑shift logic in processPitch.
popSample requires the second parameter to be the delay time in samples, not a pitch ratio. Passing a ratio value (~0.8–1.26) as delay-in-samples adds only a negligible 1-sample delay and does not shift pitch. Implement actual pitch shifting using techniques like:
- Time-stretching/granular synthesis with phase voicing
- Modulating the delay line read position over time (e.g., sinusoidal delay sweep)
- A dedicated pitch-shift algorithm
Current approach does not achieve the intended pitch-shift effect.
🤖 Prompt for AI Agents
In `@android/app/src/main/cpp/dsp/src/VoiceProcessor.cpp` around lines 39 - 52,
processPitch is passing a pitch ratio into pitchDelay.popSample (which expects a
delay in samples), so no real pitch shift occurs; change
VoiceProcessor::processPitch to compute a proper read-delay in samples from the
desired semitoneShift (use sampleRate and a base delay window) and drive the
read index over time (e.g., fractional read with interpolation plus an
LFO/modulated delay sweep or a granular/time‑stretch routine) instead of feeding
the ratio directly to pitchDelay.popSample; update uses of semitoneShift, ratio,
pitchDelay.pushSample and pitchDelay.popSample to implement either (A) a
modulated fractional read position = baseDelaySamples / ratio (with
interpolation) or (B) a granular overlap-add/time-stretching method that reads
from pitchDelay buffer at fractional positions to achieve the intended pitch
shift.
|
@M4dhav I request you to review it. |
Description
Implements a native C++ DSP (Digital Signal Processing) engine for real-time voice processing in the Resonate Android app. This PR adds a complete audio processing pipeline with modular effects for voice transformation and enhancement.
The DSP engine provides:
Fixes #683
Type of change
How Has This Been Tested?
./gradlew assembleDebuglibnoise.so(16 KB per architecture)Test Configuration:
Checklist:
Maintainer Checklist
dsp-featSummary by CodeRabbit
New Features
Style