Skip to content

feat: add looper effect with UI integration#116

Merged
sudip-mondal-2002 merged 8 commits into
sudip-mondal-2002:mainfrom
ionfwsrijan:feature/looper-pedal-module
May 18, 2026
Merged

feat: add looper effect with UI integration#116
sudip-mondal-2002 merged 8 commits into
sudip-mondal-2002:mainfrom
ionfwsrijan:feature/looper-pedal-module

Conversation

@ionfwsrijan

@ionfwsrijan ionfwsrijan commented May 17, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds a new Looper pedal to Amplitron’s effect chain, including DSP + GUI controls.

  • Implements a new Looper effect with a small state machine (Empty / Stop / Rec / Play / Dub).
  • Pre-allocates the loop buffer (up to 60s) and uses atomics + a command mailbox so the audio callback stays allocation-free and lock-free.
  • Adds a dedicated Looper pedal UI: Record, Play/Stop, Overdub, Clear buttons, a playback progress bar, and a Loop Level control.
  • Integrates Looper into the “+ Add Pedal” menu, theme color table, and CMake build sources.

Related Issue

Fixes #93

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • ♻️ Refactor / code cleanup
  • 📝 Documentation
  • ✅ Tests
  • 🔧 Build / CI / Configuration

How Was This Tested?

  • Platform(s) tested on: Windows 11 (please update if different)
  • Test command run: ./amplitron-tests (not run in this environment; CMake wasn’t available in PATH here)
  • Manual test steps (if applicable):
    1. Build and launch the app.
    2. Click + Add PedalLooper (in the TIME section).
    3. Click Record, play some audio, click Record again to finish and auto-start playback.
    4. Click Overdub to layer additional audio, then toggle it off.
    5. Click Play/Stop to pause/resume playback without clearing the loop.
    6. Click Clear to remove the loop from memory.

Checklist

  • Code compiles and builds successfully on my platform
  • All existing tests pass (./amplitron-tests)
  • New tests added for new functionality (if applicable)
  • Documentation updated (README, code comments) if applicable
  • No blocking calls on the audio thread (no std::mutex::lock() on the hot path)
  • Tested on: Windows 11 (please update if different)

Summary by CodeRabbit

  • New Features

    • Added a Looper audio effect with Record, Play, Overdub and Clear controls.
  • UI

    • Looper-specific pedal display: state label, loop progress bar, Record/Play/Overdub/Clear buttons, and Loop Level slider; footswitch and LED tooltip interactions disabled for loopers.
  • Theme

    • Looper uses a dark purple pedal color and gold-like LED.
  • Tests

    • Unit tests for looper record/play/overdub/clear behaviors.
  • Chores

    • Build updated to include the looper in app and tests; reduced horizontal padding in pedal chain layout.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d47f1859-bdf9-4523-9929-24c3ae6f70c7

📥 Commits

Reviewing files that changed from the base of the PR and between 61e3e58 and d05535c.

📒 Files selected for processing (2)
  • src/gui/pedal_board_chain.cpp
  • src/gui/pedal_widget_body.cpp
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/gui/pedal_board_chain.cpp
  • src/gui/pedal_widget_body.cpp

📝 Walkthrough

Walkthrough

Adds a Looper pedal: header + implementation with preallocated loop buffers and atomic mailbox, CMake registration, unit tests, ImGui menu and custom pedal UI with transport controls and loop-level slider, and theme/chain layout tweaks.

Changes

Looper Pedal Effect and UI Integration

Layer / File(s) Summary
Looper effect contract
src/audio/effects/looper.h
Public interface defines State enum (Empty, Idle, Recording, Playing, Overdubbing), audio processing overrides (process, process_stereo, set_sample_rate, reset), UI request methods, and atomic-backed snapshot getters (state(), has_loop(), loop_length_samples(), playhead_samples()).
Real-time audio processing
src/audio/effects/looper.cpp
Implements registration, parameter setup, sample-rate-aware buffer allocation, state reset, UI request handlers, loop lifecycle (start/stop/finalize recording), atomic command application, and core DSP: recording capture, playback with wraparound and boundary crossfading, overdub soft-clipping/accumulation, and per-call UI snapshot publishing.
Build configuration
CMakeLists.txt
Registers looper.cpp in application and test targets (APP_SOURCES, CORE_SOURCES) and adds tests/test_looper.cpp to test sources; updates final platform-selection endif() comment.
GUI menu and pedal widget wiring
src/gui/pedal_board_menu.cpp, src/gui/pedal_widget.cpp, src/gui/pedal_widget.h
Adds "Looper" menu item under TIME, includes looper header; routes Looper pedals to a custom display and disables LED hover and footswitch interactions for loopers.
Looper display UI implementation
src/gui/pedal_widget_body.cpp
Implements render_looper_display(): state label, loop progress bar, Record/Play/Overdub/Clear buttons wired to looper->request_*(), and a "Loop Level" slider bound to parameter 0 with engine push/commit.
Visual theme
src/gui/theme.h
Adds Looper entry to get_effect_color() lookup (dark purple pedal color and gold LED color).
Signal chain layout tweak
src/gui/pedal_board_chain.cpp
Adjusts total_width and ImGui::Dummy spacer to reduce extra padding in the drawn pedal chain.
Unit tests
tests/test_looper.cpp
Adds tests verifying initial empty state, record→play mixing and output, play toggle behavior retaining loops, and clear resets to empty.

Sequence Diagram(s)

sequenceDiagram
  participant User as User/ImGui
  participant UIReq as Looper Request API
  participant Mailbox as Atomic Mailbox
  participant Audio as Audio Thread Callback
  participant Buffers as Loop Buffers
  User->>UIReq: click Record/Play/Overdub/Clear buttons
  UIReq->>Mailbox: pending_commands_ |= COMMAND_BIT
  Audio->>Mailbox: atomically exchange pending_commands_
  Audio->>Audio: apply_pending_commands()
  alt Recording
    Audio->>Buffers: write input samples
  else Playing
    Audio->>Buffers: read loop samples and mix
    Audio->>Audio: apply crossfade at wrap
  else Overdubbing
    Audio->>Buffers: accumulate soft-clipped input into buffer
  end
  Audio->>User: publish_snapshot(state, has_loop, loop_len, playhead)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

Audio Processing, level:intermediate, quality:clean

Suggested reviewers

  • sudip-mondal-2002

Poem

🐇 I hopped in with a purple gleam,
Pressed Record and stitched a looping dream,
Playhead hums and crossfades keep the beat,
Overdub soft-clips make layers sweet,
Zero allocs — jam on repeat!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a looper effect with UI integration, which is the primary objective of this pull request.
Linked Issues check ✅ Passed The implementation addresses all primary objectives from issue #93: looper effect with state machine, pre-allocated buffers, atomic synchronization, dedicated UI with controls/progress bar, and zero-allocation audio callback.
Out of Scope Changes check ✅ Passed All changes are directly related to the looper feature: audio DSP implementation, UI rendering, build configuration, theme support, and tests. Minor adjustments to pedal board horizontal spacing appear to be UI refinements addressing feedback mentioned in comments.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/gui/pedal_widget_body.cpp (1)

382-387: ⚡ Quick win

Coalesce Loop Level undo entries to one command per drag.

commit_param_change is currently called on every slider tick, which will flood undo history during a single drag. Capture the value on activation and commit once on deactivation (matching your knob/popup coalescing behavior).

Proposed fix
-        float old_val = level;
-        if (ImGui::SliderFloat(slider_id, &level, 0.0f, 1.0f, "Loop Level: %.2f")) {
-            level = clamp(level, 0.0f, 1.0f);
-            engine_.push_param_change(index_, 0, level);
-            commit_param_change(0, old_val, level);
-        }
+        if (ImGui::IsItemActivated()) {
+            popup_active_param_index_ = 0;
+            popup_param_value_before_edit_ = level;
+        }
+        if (ImGui::SliderFloat(slider_id, &level, 0.0f, 1.0f, "Loop Level: %.2f")) {
+            level = clamp(level, 0.0f, 1.0f);
+            engine_.push_param_change(index_, 0, level);
+        }
+        if (ImGui::IsItemDeactivatedAfterEdit() && popup_active_param_index_ == 0) {
+            commit_param_change(0, popup_param_value_before_edit_, level);
+            popup_active_param_index_ = -1;
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gui/pedal_widget_body.cpp` around lines 382 - 387, The slider currently
calls commit_param_change every tick; change it to record the starting value
when the slider becomes active and only call commit_param_change once when the
user releases the slider. Specifically, on the SliderFloat interaction for
slider_id, keep using engine_.push_param_change(index_, 0, level) per-tick, but
capture old_val at activation (use ImGui::IsItemActivated/IsItemActive with
slider_id or an instance member like saved_old_level and dragging flag) and
defer commit_param_change(0, saved_old_level, level) until deactivation (use
ImGui::IsItemDeactivated/IsItemDeactivatedAfterChange or check IsItemActive
transition); ensure you reset the saved_old_level/dragging state after
committing so subsequent drags behave the same.
src/audio/effects/looper.cpp (1)

102-109: 💤 Low value

Unreachable branch: State::Empty check is dead code.

The early return at line 103 (if (!has_loop_rt_) return;) means line 106's State::Empty condition can never be true when reached, since Empty implies no loop exists. This branch is unreachable.

Consider simplifying:

Suggested cleanup
 void Looper::toggle_play_rt() {
     if (!has_loop_rt_) return;
     if (state_rt_ == State::Playing || state_rt_ == State::Overdubbing) {
         state_rt_ = State::Idle;
-    } else if (state_rt_ == State::Idle || state_rt_ == State::Empty) {
+    } else if (state_rt_ == State::Idle) {
         state_rt_ = State::Playing;
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/audio/effects/looper.cpp` around lines 102 - 109, The branch in
Looper::toggle_play_rt is unreachable because has_loop_rt_ being false causes an
early return, so checking State::Empty in the later condition is dead code;
update toggle_play_rt to remove State::Empty from the else-if (i.e., change
"else if (state_rt_ == State::Idle || state_rt_ == State::Empty)" to just check
State::Idle) or, if intended, reorder or adjust the has_loop_rt_ guard so
State::Empty can be reached—ensure you modify only Looper::toggle_play_rt and
keep references to has_loop_rt_ and state_rt_ consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/audio/effects/looper.cpp`:
- Line 167: The code reads loop_level directly from params_[0].value in
process_core, causing zipper noise; add a one-pole smoothing state (e.g., float
loop_level_smooth_ = 0.80f in the class) and update it each sample inside
process_core by applying a one-pole low-pass toward params_[0].value
(loop_level_smooth_ += coef*(target - loop_level_smooth_)) then use
loop_level_smooth_ wherever loop_level is used; choose a small smoothing
coefficient (derived from desired time constant and sample rate) and ensure the
state is initialized in the header/class so smoothing persists across calls.

---

Nitpick comments:
In `@src/audio/effects/looper.cpp`:
- Around line 102-109: The branch in Looper::toggle_play_rt is unreachable
because has_loop_rt_ being false causes an early return, so checking
State::Empty in the later condition is dead code; update toggle_play_rt to
remove State::Empty from the else-if (i.e., change "else if (state_rt_ ==
State::Idle || state_rt_ == State::Empty)" to just check State::Idle) or, if
intended, reorder or adjust the has_loop_rt_ guard so State::Empty can be
reached—ensure you modify only Looper::toggle_play_rt and keep references to
has_loop_rt_ and state_rt_ consistent.

In `@src/gui/pedal_widget_body.cpp`:
- Around line 382-387: The slider currently calls commit_param_change every
tick; change it to record the starting value when the slider becomes active and
only call commit_param_change once when the user releases the slider.
Specifically, on the SliderFloat interaction for slider_id, keep using
engine_.push_param_change(index_, 0, level) per-tick, but capture old_val at
activation (use ImGui::IsItemActivated/IsItemActive with slider_id or an
instance member like saved_old_level and dragging flag) and defer
commit_param_change(0, saved_old_level, level) until deactivation (use
ImGui::IsItemDeactivated/IsItemDeactivatedAfterChange or check IsItemActive
transition); ensure you reset the saved_old_level/dragging state after
committing so subsequent drags behave the same.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4d311d2a-2b05-4eed-80e6-7a1618acf43e

📥 Commits

Reviewing files that changed from the base of the PR and between 0fa8d1b and 43a1b4e.

📒 Files selected for processing (8)
  • CMakeLists.txt
  • src/audio/effects/looper.cpp
  • src/audio/effects/looper.h
  • src/gui/pedal_board_menu.cpp
  • src/gui/pedal_widget.cpp
  • src/gui/pedal_widget.h
  • src/gui/pedal_widget_body.cpp
  • src/gui/theme.h

Comment thread src/audio/effects/looper.cpp Outdated
@github-actions

github-actions Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor

PR Preview Removed

The GitHub Pages preview for this PR has been removed because the PR was closed.

@ionfwsrijan ionfwsrijan mentioned this pull request May 17, 2026
4 tasks

@sudip-mondal-2002 sudip-mondal-2002 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Hi please add test coverage for this pedal

@sudip-mondal-2002

Copy link
Copy Markdown
Owner

Hi @ionfwsrijan the UI is very inconsistent, can you please fix it?

Use AI if needed, you can use antigravity for free

image

@sudip-mondal-2002 sudip-mondal-2002 added type:feature New feature or request gssoc:approved GSSoC 2026 contribution level:advanced Advanced task · 45 GSSoC points quality:exceptional mentor:MohitBareja16 labels May 18, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/audio/effects/looper.cpp`:
- Around line 66-69: Add a one-pole smoothing state for the crossfade parameter
and use it everywhere the raw params_[1].value is converted to samples (notably
in Looper::crossfade_samples_rt()); replace direct reads of params_[1].value
with the smoothed value when computing ms and xf, update the smoother each audio
block with an appropriate attack/release or coefficient, clamp as before, and
apply the same change to the other locations that read params_[1].value for
sample conversion (the occurrences referenced around the other usage) so all
crossfade conversions use the one-pole filtered parameter.
- Around line 179-189: When recording hits capacity mid-buffer (variables:
record_pos_, cap) and stop_recording_rt_and_play_rt() is called, the remainder
of the current audio callback can still be processed in the playback branch,
causing already-recorded samples to be processed twice; modify the sample loop
in State::Recording (the block using buffer_l_, buffer_r_, left/right,
num_samples, stereo) so that after calling stop_recording_rt_and_play_rt() you
immediately exit the processing callback (e.g., return from the function) or
otherwise ensure no further sample processing occurs in this callback, rather
than just breaking the inner loop—this prevents reprocessing the same samples in
the same callback when playback starts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6654be43-3620-4627-a471-fb99ed57204f

📥 Commits

Reviewing files that changed from the base of the PR and between 43a1b4e and bd63aa4.

📒 Files selected for processing (4)
  • CMakeLists.txt
  • src/audio/effects/looper.cpp
  • src/audio/effects/looper.h
  • src/gui/pedal_board_menu.cpp
🚧 Files skipped from review as they are similar to previous changes (2)
  • CMakeLists.txt
  • src/audio/effects/looper.h

Comment on lines +66 to +69
int Looper::crossfade_samples_rt() const {
const float ms = params_[1].value;
const int xf = static_cast<int>(std::round((ms / 1000.0f) * static_cast<float>(sample_rate_)));
return std::clamp(xf, 0, std::max(loop_length_ / 2, 0));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Smooth the Crossfade parameter before converting it to samples.

params_[1].value is used directly, so abrupt UI changes can still produce boundary artifacts. Add a one-pole smoothed crossfade state and use that in crossfade_samples_rt().

Proposed fix
 int Looper::crossfade_samples_rt() const {
-    const float ms = params_[1].value;
+    const float ms = crossfade_ms_smoothed_;
     const int xf = static_cast<int>(std::round((ms / 1000.0f) * static_cast<float>(sample_rate_)));
     return std::clamp(xf, 0, std::max(loop_length_ / 2, 0));
 }

 void Looper::process_core(float* left, float* right, int num_samples, bool stereo) {
     apply_pending_commands();

     const float loop_level_target = clamp(params_[0].value, 0.0f, 1.0f);
+    const float crossfade_target_ms = clamp(params_[1].value, 0.0f, 20.0f);
+    crossfade_ms_smoothed_ += crossfade_alpha_ * (crossfade_target_ms - crossfade_ms_smoothed_);
     const int cap = max_samples_;

As per coding guidelines: "DSP Agents must utilize one-pole filters internally on parameter inputs to prevent audible 'zipper' noise or clicking when parameters change abruptly".

Also applies to: 193-194

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/audio/effects/looper.cpp` around lines 66 - 69, Add a one-pole smoothing
state for the crossfade parameter and use it everywhere the raw params_[1].value
is converted to samples (notably in Looper::crossfade_samples_rt()); replace
direct reads of params_[1].value with the smoothed value when computing ms and
xf, update the smoother each audio block with an appropriate attack/release or
coefficient, clamp as before, and apply the same change to the other locations
that read params_[1].value for sample conversion (the occurrences referenced
around the other usage) so all crossfade conversions use the one-pole filtered
parameter.

Comment on lines +179 to +189
if (state_rt_ == State::Recording) {
for (int i = 0; i < num_samples; ++i) {
if (record_pos_ >= cap) {
stop_recording_rt_and_play_rt();
break;
}
buffer_l_[record_pos_] = left[i];
if (stereo && right) buffer_r_[record_pos_] = right[i];
++record_pos_;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid reprocessing already-recorded samples when recording auto-stops at capacity.

If record_pos_ hits cap mid-buffer, recording breaks and playback then starts from sample index 0, so earlier samples in the same callback are processed twice.

Proposed fix
 void Looper::process_core(float* left, float* right, int num_samples, bool stereo) {
     apply_pending_commands();
+    int process_from = 0;

@@
     if (state_rt_ == State::Recording) {
         for (int i = 0; i < num_samples; ++i) {
             if (record_pos_ >= cap) {
                 stop_recording_rt_and_play_rt();
+                process_from = i; // first unprocessed sample in this callback
                 break;
             }
             buffer_l_[record_pos_] = left[i];
             if (stereo && right) buffer_r_[record_pos_] = right[i];
             ++record_pos_;
+            process_from = i + 1;
         }
     }

@@
-        for (int i = 0; i < num_samples; ++i) {
+        for (int i = process_from; i < num_samples; ++i) {
             ...
         }

Also applies to: 191-194

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/audio/effects/looper.cpp` around lines 179 - 189, When recording hits
capacity mid-buffer (variables: record_pos_, cap) and
stop_recording_rt_and_play_rt() is called, the remainder of the current audio
callback can still be processed in the playback branch, causing already-recorded
samples to be processed twice; modify the sample loop in State::Recording (the
block using buffer_l_, buffer_r_, left/right, num_samples, stereo) so that after
calling stop_recording_rt_and_play_rt() you immediately exit the processing
callback (e.g., return from the function) or otherwise ensure no further sample
processing occurs in this callback, rather than just breaking the inner
loop—this prevents reprocessing the same samples in the same callback when
playback starts.

@ionfwsrijan

Copy link
Copy Markdown
Contributor Author

@sudip-mondal-2002 Addressed the requested changes:

  • Added test coverage for the new Looper pedal (tests/test_looper.cpp) and wired it into amplitron-tests via CMakeLists.txt.
  • Fixed the UI inconsistency by constraining the Looper “Loop Level” slider width to the pedal body (src/gui/pedal_widget_body.cpp).

Thanks for the review — please take another look.

@sudip-mondal-2002

Copy link
Copy Markdown
Owner

One small minor issue @ionfwsrijan
When I add the looper the horizontal scroll comes along, even if it is not required, can you please check?

https://www.loom.com/share/75898d6dfdb54800992651331517ca50

@ionfwsrijan

Copy link
Copy Markdown
Contributor Author

@sudip-mondal-2002 Fixed it.

@sudip-mondal-2002 sudip-mondal-2002 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

LGTM

@sudip-mondal-2002 sudip-mondal-2002 merged commit eb2d0c1 into sudip-mondal-2002:main May 18, 2026
8 checks passed
github-actions Bot added a commit that referenced this pull request May 18, 2026
@sudip-mondal-2002 sudip-mondal-2002 added type:performance Performance optimization or latency improvement type:testing Unit tests, integration tests, or test coverage type:accessibility type:design labels May 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gssoc:approved GSSoC 2026 contribution level:advanced Advanced task · 45 GSSoC points mentor:MohitBareja16 mentor:sudip-mondal-2002 quality:exceptional type:accessibility type:design type:feature New feature or request type:performance Performance optimization or latency improvement type:testing Unit tests, integration tests, or test coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Looper Pedal Module

2 participants