Skip to content

feat/juce-dsp-engine#753

Open
uju09 wants to merge 2 commits intoAOSSIE-Org:feat/dspfrom
uju09:feat/juce-dsp-engine
Open

feat/juce-dsp-engine#753
uju09 wants to merge 2 commits intoAOSSIE-Org:feat/dspfrom
uju09:feat/juce-dsp-engine

Conversation

@uju09
Copy link
Copy Markdown
Contributor

@uju09 uju09 commented Feb 3, 2026

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:

  • Unified processing chain: Input → NoiseGate → VoiceProcessor → Compressor → ResonanceDSP → Output
  • Voice Profiles: Natural, Deep, Bright, Anonymous, and Robot presets
  • Thread-safe design: All parameter updates can be called from any thread
  • Multi-architecture support: Compiled for arm64-v8a, armeabi-v7a, and x86_64

Fixes #683

Type of change

  • New feature (non-breaking change which adds functionality)

How Has This Been Tested?

  • Successfully built debug APK with ./gradlew assembleDebug
  • Native C++ libraries compiled for all Android architectures (arm64-v8a, armeabi-v7a, x86_64)
  • Verified native libraries are packaged in the APK:
    • libnoise.so (16 KB per architecture)
    • All JUCE dependencies resolved correctly

Test Configuration:

  • macOS with OpenJDK 21
  • Android SDK Build-Tools 35
  • CMake 3.22.1
  • Gradle 8.13

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules
  • I have checked my code and corrected any misspellings

Maintainer Checklist

Summary by CodeRabbit

  • New Features

    • Added voice processing with five voice profiles (Natural, Deep, Bright, Anonymous, Robot)
    • Integrated audio effects including noise gate and dynamic compression for audio enhancement
    • Added voice anonymization strength control
    • Added output gain adjustment capability
  • Style

    • Fixed minor formatting in localization text

@uju09 uju09 requested a review from M4dhav as a code owner February 3, 2026 13:41
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 3, 2026

🎉 Welcome @uju09!
Thank you for your pull request! Our team will review it soon. 🔍

  • Please ensure your PR follows the contribution guidelines. ✅
  • All automated tests should pass before merging. 🔄
  • If this PR fixes an issue, link it in the description. 🔗

We appreciate your contribution! 🚀

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 3, 2026

📝 Walkthrough

Walkthrough

The 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

Cohort / File(s) Summary
Build & Submodule Integration
.gitmodules, android/app/src/main/cpp/dsp/CMakeLists.txt, android/app/src/main/cpp/dsp/juce
Adds JUCE as a git submodule and establishes CMake configuration for building a static resonance_dsp library. Sets compiler flags for performance (fast-math, no-exceptions, no-rtti) and links specific JUCE DSP modules (core, audio-basics, dsp).
Voice Profile & Configuration
android/app/src/main/cpp/dsp/include/VoiceProfile.h, android/app/src/main/cpp/dsp/include/DspParameters.h, android/app/src/main/cpp/dsp/src/DspParameters.cpp
Defines VoiceProfile enum (Natural, Deep, Bright, Anonymous, Robot) and VoiceProfileConfig struct with processing parameters. Provides getVoiceProfileConfig() function mapping profiles to preset DSP configurations for pitch, formants, compression, and noise gate.
DSP Components - Headers
android/app/src/main/cpp/dsp/include/NoiseGate.h, android/app/src/main/cpp/dsp/include/Compressor.h, android/app/src/main/cpp/dsp/include/VoiceProcessor.h, android/app/src/main/cpp/dsp/include/ResonanceDSP.h
Declares wrapper classes for JUCE DSP units. Each component exposes prepare(), process(), reset(), and parameter setters (threshold, ratio, gain, etc.). All maintain thread-safe atomic parameter storage and support runtime configuration.
DSP Components - Implementation
android/app/src/main/cpp/dsp/src/NoiseGate.cpp, android/app/src/main/cpp/dsp/src/Compressor.cpp, android/app/src/main/cpp/dsp/src/ResonanceDSP.cpp
Implements initialization, per-frame processing, parameter clamping, and atomic-based thread-safe updates for noise gate, compressor, and gain-bypass stages. Each reads atomic parameters during processing to reflect real-time changes.
DSP Components - Voice & Pitch Processing
android/app/src/main/cpp/dsp/src/VoiceProcessor.cpp
Implements pitch shifting via delay-line resampling, three-band formant filtering (600 Hz, 1600 Hz, 3200 Hz), and smoothing low-pass filtering. Maps anonymization strength to pitch shift semitones and filter attenuation for voice masking.
Main Engine
android/app/src/main/cpp/dsp/include/DspEngine.h, android/app/src/main/cpp/dsp/src/DspEngine.cpp
Orchestrates the DSP chain (NoiseGate → VoiceProcessor → Compressor → ResonanceDSP output stage). Manages voice profile selection, component enable/disable flags, bypass control, and applies profile-based configuration to all sub-components with thread-safe atomic state.
Localization
lib/l10n/app_localizations_bn.dart
Minor whitespace formatting adjustments to Bengali localization file; no semantic changes.

Sequence Diagram

sequenceDiagram
    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()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hops of joy for DSP dreams
JUCE voices flow through audio streams
Noise gates and pitch shift align,
Resonance deep with formant design—
Real-time magic, on-device and fine!

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Minor whitespace changes in a Dart localization file (app_localizations_bn.dart) are out of scope with DSP engine development and unrelated to #683 requirements. Revert the unrelated whitespace changes to app_localizations_bn.dart to keep the PR focused on DSP engine implementation.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR implements all core requirements: JUCE-based DSP engine [#683], voice profiles (Natural, Deep, Bright, Anonymous, Robot), thread-safe parameter handling, real-time safe processing chain, and platform-agnostic C++ code.
Title check ✅ Passed The title 'feat/juce-dsp-engine' clearly describes the main addition: a JUCE-based DSP engine for voice processing, which aligns with the substantial changes introducing DspEngine, voice profiles, and audio processing components.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@uju09
Copy link
Copy Markdown
Contributor Author

uju09 commented Feb 3, 2026

@coderabbitai Review.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 3, 2026

@uju09: I'll review the changes in this PR now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@uju09 uju09 changed the title Feat/juce dsp engine feat/juce dsp engine Feb 3, 2026
@uju09 uju09 changed the title feat/juce dsp engine feat/juce-dsp-engine Feb 3, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 a default case for forward compatibility.

If a new VoiceProfile enum value is added later but the switch isn't updated, the function will return a partially-initialized config (only default-constructed values). Adding a default case 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 over file(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 adding VoiceProcessor::reset() and calling it here to avoid stale state after a reset.

Comment on lines +14 to +16
private:
double sampleRate = 44100.0;
float strength = 0.5f;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +36 to +39
// Bandpass filter for robot effect (Hz)
float bandpassLowFreq = 200.0f;
float bandpassHighFreq = 4000.0f;
bool useBandpass = false;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 -5

Repository: 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 -20

Repository: 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 -40

Repository: 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.h

Repository: 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"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -20

Repository: 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 3

Repository: 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 -40

Repository: 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.txt

Repository: 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 1

Repository: 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 -50

Repository: 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.

Comment on lines +3 to +16
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());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find where Compressor::prepare is called
rg -n "Compressor::prepare" android/app/src/main/cpp/dsp

Repository: 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 -50

Repository: 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.cpp

Repository: 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.h

Repository: 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/dsp

Repository: 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 -30

Repository: 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/null

Repository: 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 -20

Repository: 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.

Comment on lines +71 to +87
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);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +24 to +37
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);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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.

Comment on lines +39 to +52
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);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 -1 to reuse the delay currently stored in the object (set previously, e.g. via setDelay). (docs.juce.com)
  • updateReadPointer: whether this read should advance the internal read position. Use true for normal “one output sample per input sample” operation; use false to 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:


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.

@uju09
Copy link
Copy Markdown
Contributor Author

uju09 commented Feb 5, 2026

@M4dhav I request you to review it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant