Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions Sources/XPadAudio/AudioEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,21 @@ public final class SynthVoice: @unchecked Sendable {

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)

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 👍 / 👎.

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)
}

Comment on lines +1171 to +1185

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.

for frame in 0..<Int(frameCount) {
// Envelope
let envValue: Double
Expand Down Expand Up @@ -1197,11 +1212,9 @@ public final class SynthVoice: @unchecked Sendable {
let first: Double
switch snap.oscillator1 {
case .unison:
let unisonDetunes: [Double] = [-0.35, -0.22, -0.10, 0.0, 0.10, 0.22, 0.35]
var unisonMix = 0.0
for vi in 0..<7 {
let df = frequency * (pow(2.0, unisonDetunes[vi] / 12.0) - 1.0)
let inc = min((frequency + df) / sampleRate, 0.49)
let inc = unisonIncrements[vi]
let s = (unisonPhases[vi] * 2.0 - 1.0)
- DSPMath.polyBLEP(phase: unisonPhases[vi], increment: inc)
unisonMix += s
Expand All @@ -1210,11 +1223,9 @@ public final class SynthVoice: @unchecked Sendable {
}
first = unisonMix * 0.18
case .strings:
let stringDetunes: [Double] = [-0.08, -0.025, 0.025, 0.08]
var strMix = 0.0
for vi in 0..<4 {
let df = frequency * (pow(2.0, stringDetunes[vi] / 12.0) - 1.0)
let inc = min((frequency + df) / sampleRate, 0.49)
let inc = stringIncrements[vi]
let s = (stringsPhases[vi] * 2.0 - 1.0)
- DSPMath.polyBLEP(phase: stringsPhases[vi], increment: inc)
strMix += s
Expand Down
Loading