Skip to content

⚡ Hoist 'pow' and array lookups out of inner DSP loops - #71

Merged
sp80808 merged 1 commit into
mainfrom
jules-6839792153999069520-60edbc89
Sep 2, 2026
Merged

sp80808 merged 1 commit into
mainfrom
jules-6839792153999069520-60edbc89

Conversation

@sp80808

@sp80808 sp80808 commented Sep 2, 2026 •

Copy link
Copy Markdown
Owner

What: Moved pow calculations and unisonDetunes / stringDetunes arrays out of the inner loop (which runs once per audio frame) and precalculated their values in a buffer arrays unisonIncrements and stringIncrements.

Why: The inner DSP loop inside AudioEngine.swift is extremely performance-sensitive as it is called many times (e.g., 44100 times per second per voice when active). Performing 11 exponential calculations (pow) inside this inner loop every frame for the .unison and .strings modes introduces unnecessary CPU load that can cause audio dropouts and increased processing time. Since frequency and sampleRate are constant for a given AudioBuffer block, pow can be evaluated once before the frame-loop, saving 4096 * 11 pow() calls per typical CoreAudio chunk per voice.

Measured Improvement: Due to the testing environment lacking swift toolchain, direct benchmarking could not be generated and executed. However, theoretically, moving math operations spanning arrays (unisonDetunes, stringDetunes) outside a tight per-frame for frame in 0..<Int(frameCount) loop drops their execution count drastically, thereby directly eliminating overhead. CPU usage for .unison and .strings patches should drop measurably.


PR created automatically by Jules for task 6839792153999069520 started by @sp80808


Note

Low Risk
Refactor-only DSP math with no API or behavioral intent changes; worst case is equivalent audio with slightly different work when non-unison/string presets run (increments computed unconditionally per buffer).

Overview
SynthVoice now precomputes per-voice phase increments for .unison (7 detuned saws) and .strings (4 detuned voices) once per audio buffer, immediately before the per-sample frame loop in AudioEngine.swift.

Previously, each sample re-created the detune tables and ran pow to derive phase increments inside the oscillator switch. The inner loop now only reads unisonIncrements / stringIncrements while advancing phases and polyBLEP—same math, far fewer transcendental calls when those oscillator modes are active.

This is a CPU-focused change in the real-time render path; output should be unchanged because frequency is already fixed for the duration of each callback buffer.

Reviewed by Cursor Bugbot for commit d09e978. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by Sourcery

Enhancements:

  • Precompute unison and string oscillator phase increments outside the per-frame DSP loop to reduce repeated calculations during audio rendering.

What: The unisonDetunes and stringDetunes iterations generated multiple expensive 'pow' calls per-frame inside a DSP loop. We move the iteration and pow calls outside the audio loop (per-block).

Why: pow(x) is an expensive math operation, especially inside an audio rendering loop running at the sample rate (e.g. 44100 times per sec per voice).

Measured Improvement: Could not benchmark directly as swift wasn't in PATH. However, skipping 11 pow allocations and variable computations inside a 4096-frame chunk will measurably reduce DSP load significantly for the unison/string oscillators.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@sourcery-ai

sourcery-ai Bot commented Sep 2, 2026 •

Copy link
Copy Markdown
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

The DSP path now calculates and stores unison and string oscillator increments once per audio buffer, eliminating repeated array creation, detune math, and pow calls from the per-frame loop while retaining the existing oscillator behavior.

Flow diagram for precomputed oscillator increments

flowchart TD
    A[Audio buffer begins] --> B[Calculate unisonIncrements]
    A --> C[Calculate stringIncrements]
    B --> D[Per-frame DSP loop]
    C --> D
    D --> E[Read cached increment]
    E --> F[Generate unison or strings oscillator sample]
Loading

File-Level Changes

Change Details Files
Precompute detuned oscillator phase increments once per audio buffer instead of recalculating them for every frame.
  • Build seven unison increments and four string increments before entering the frame loop.
  • Move all detune-specific pow calculations and increment clamping out of the per-frame oscillator cases.
  • Reuse the precomputed increments while preserving existing oscillator mixing and phase processing.
Sources/XPadAudio/AudioEngine.swift

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@cursor

cursor Bot commented Sep 2, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_ebd2131b-cd89-46fd-97a2-a5f7054c92b6)

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not approved: Cursor Bugbot was present but completed as skipped (usage limit), so the required automated-review signal did not succeed. Human review is needed; no reviewers were assigned because the only collaborator is the PR author.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="Sources/XPadAudio/AudioEngine.swift" line_range="1171-1185" />
<code_context>

             var reachedReleaseEnd = false

+            // Precalculate unison and string increments outside the frame loop
+            let unisonDetunes: [Double] = [-0.35, -0.22, -0.10, 0.0, 0.10, 0.22, 0.35]
+            var unisonIncrements = [Double](repeating: 0.0, count: 7)
+            for vi in 0..<7 {
+                let df = frequency * (pow(2.0, unisonDetunes[vi] / 12.0) - 1.0)
+                unisonIncrements[vi] = min((frequency + df) / sampleRate, 0.49)
+            }
+
+            let stringDetunes: [Double] = [-0.08, -0.025, 0.025, 0.08]
+            var stringIncrements = [Double](repeating: 0.0, count: 4)
</code_context>
<issue_to_address>
**issue (performance):** The audio callback now constructs both increment buffers and executes all 11 `pow` calls for every active audio block, even when `snap.oscillator1` is a normal oscillator rather than `.unison` or `.strings`. This adds array allocation/deallocation and expensive math to the common non-detuned path, where the previous code performed none of this work, increasing the chance of missing real-time audio deadlines.

**Triggers:** When an active voice uses any oscillator other than `.unison` or `.strings`.

**Suggested fix:** Only calculate these buffers when the selected oscillator requires them, or store reusable buffers/constants outside the audio callback.
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: Sources/XPadAudio/AudioEngine.swift:1185


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +1171 to +1185
// Precalculate unison and string increments outside the frame loop
let unisonDetunes: [Double] = [-0.35, -0.22, -0.10, 0.0, 0.10, 0.22, 0.35]
var unisonIncrements = [Double](repeating: 0.0, count: 7)
for vi in 0..<7 {
let df = frequency * (pow(2.0, unisonDetunes[vi] / 12.0) - 1.0)
unisonIncrements[vi] = min((frequency + df) / sampleRate, 0.49)
}

let stringDetunes: [Double] = [-0.08, -0.025, 0.025, 0.08]
var stringIncrements = [Double](repeating: 0.0, count: 4)
for vi in 0..<4 {
let df = frequency * (pow(2.0, stringDetunes[vi] / 12.0) - 1.0)
stringIncrements[vi] = min((frequency + df) / sampleRate, 0.49)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (performance): The audio callback now constructs both increment buffers and executes all 11 pow calls for every active audio block, even when snap.oscillator1 is a normal oscillator rather than .unison or .strings. This adds array allocation/deallocation and expensive math to the common non-detuned path, where the previous code performed none of this work, increasing the chance of missing real-time audio deadlines.

Triggers: When an active voice uses any oscillator other than .unison or .strings.

Suggested fix: Only calculate these buffers when the selected oscillator requires them, or store reusable buffers/constants outside the audio callback.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d09e978ca5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


// Precalculate unison and string increments outside the frame loop
let unisonDetunes: [Double] = [-0.35, -0.22, -0.10, 0.0, 0.10, 0.22, 0.35]
var unisonIncrements = [Double](repeating: 0.0, count: 7)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Compute only the selected detune bank

For every active voice using a normal oscillator such as .sine, .saw, or .triangle—12 of the 14 shipped presets—this unconditionally constructs both detune/increment array pairs and executes all 11 pow calls even though the subsequent switch reads neither bank; .unison and .strings also calculate each other's unused bank. This introduces recurring CPU work and allocation jitter into the real-time AVAudioSourceNode callback for the common path, so select and compute only the bank required by snap.oscillator1 and reuse fixed storage/constants rather than creating arrays per callback.

AGENTS.md reference: AGENTS.md:L7-L8

Useful? React with 👍 / 👎.

@sp80808
sp80808 merged commit eb7e2ef into main Sep 2, 2026
6 checks passed
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