From 35edf651503c98ba3e00e5f532e5435cf9e92b4a Mon Sep 17 00:00:00 2001 From: Solvely-Colin <211764741+Solvely-Colin@users.noreply.github.com> Date: Sun, 7 Jun 2026 00:36:18 -0400 Subject: [PATCH 1/2] Optimize rendered sample buffer queue Replace front-removal dequeue operations in RenderedSampleBuffer with an offset-based queue and periodic compaction to avoid O(n) work on every audio callback-driven dequeue/enqueue path. Add focused tests for FIFO behavior and bounded-capacity trimming. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Sources/OpenFaderCore/AudioRenderLoop.swift | 40 +++++++++++++++---- .../RenderedSampleBufferTests.swift | 27 +++++++++++++ 2 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 Tests/OpenFaderCoreTests/RenderedSampleBufferTests.swift diff --git a/Sources/OpenFaderCore/AudioRenderLoop.swift b/Sources/OpenFaderCore/AudioRenderLoop.swift index 2234aa5..36c55e4 100644 --- a/Sources/OpenFaderCore/AudioRenderLoop.swift +++ b/Sources/OpenFaderCore/AudioRenderLoop.swift @@ -301,6 +301,7 @@ public protocol RenderLoopManaging: Sendable { final class RenderedSampleBuffer: @unchecked Sendable { private let lock = NSLock() private var samples: [Float] = [] + private var readOffset = 0 private let maximumSampleCount: Int init(maximumSampleCount: Int = 48_000 * 8) { @@ -309,7 +310,7 @@ final class RenderedSampleBuffer: @unchecked Sendable { var sampleCount: Int { lock.withLock { - samples.count + samples.count - readOffset } } @@ -320,8 +321,14 @@ final class RenderedSampleBuffer: @unchecked Sendable { lock.withLock { samples.append(contentsOf: nextSamples) - if samples.count > maximumSampleCount { - samples.removeFirst(samples.count - maximumSampleCount) + let overflowCount = (samples.count - readOffset) - maximumSampleCount + if overflowCount > 0 { + readOffset += overflowCount + } + compactStorageIfNeeded() + if readOffset == samples.count { + samples.removeAll(keepingCapacity: true) + readOffset = 0 } } @@ -334,22 +341,41 @@ final class RenderedSampleBuffer: @unchecked Sendable { } return lock.withLock { - let readCount = min(requestedSampleCount, samples.count) + let readCount = min(requestedSampleCount, samples.count - readOffset) guard readCount > 0 else { return [] } - let nextSamples = Array(samples.prefix(readCount)) - samples.removeFirst(readCount) + let readEnd = readOffset + readCount + let nextSamples = Array(samples[readOffset.. 0, + readOffset >= 4096, + readOffset * 2 >= samples.count else { + return + } + + samples.removeFirst(readOffset) + readOffset = 0 + } } protocol RenderedAudioOutputManaging: Sendable { diff --git a/Tests/OpenFaderCoreTests/RenderedSampleBufferTests.swift b/Tests/OpenFaderCoreTests/RenderedSampleBufferTests.swift new file mode 100644 index 0000000..697154a --- /dev/null +++ b/Tests/OpenFaderCoreTests/RenderedSampleBufferTests.swift @@ -0,0 +1,27 @@ +import Testing +@testable import OpenFaderCore + +@Test +func renderedSampleBufferDequeuesInOrder() { + let buffer = RenderedSampleBuffer(maximumSampleCount: 8) + + #expect(buffer.enqueue([1, 2, 3, 4]) == 4) + #expect(buffer.sampleCount == 4) + #expect(buffer.dequeue(sampleCount: 2) == [1, 2]) + #expect(buffer.sampleCount == 2) + #expect(buffer.dequeue(sampleCount: 4) == [3, 4]) + #expect(buffer.sampleCount == 0) +} + +@Test +func renderedSampleBufferDropsOldestSamplesAtCapacity() { + let buffer = RenderedSampleBuffer(maximumSampleCount: 6) + + #expect(buffer.enqueue([1, 2, 3, 4]) == 4) + #expect(buffer.dequeue(sampleCount: 2) == [1, 2]) + #expect(buffer.enqueue([5, 6, 7, 8, 9]) == 5) + + #expect(buffer.sampleCount == 6) + #expect(buffer.dequeue(sampleCount: 10) == [4, 5, 6, 7, 8, 9]) + #expect(buffer.sampleCount == 0) +} From bb6d4fd480274178213c6377633de3d9b127e50e Mon Sep 17 00:00:00 2001 From: Solvely-Colin <211764741+Solvely-Colin@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:19:30 -0400 Subject: [PATCH 2/2] Extract compaction threshold into named constant Replace the magic number 4096 in compactStorageIfNeeded() with a documented compactionThreshold constant explaining the heuristic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Sources/OpenFaderCore/AudioRenderLoop.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/OpenFaderCore/AudioRenderLoop.swift b/Sources/OpenFaderCore/AudioRenderLoop.swift index 36c55e4..c915b82 100644 --- a/Sources/OpenFaderCore/AudioRenderLoop.swift +++ b/Sources/OpenFaderCore/AudioRenderLoop.swift @@ -299,6 +299,12 @@ public protocol RenderLoopManaging: Sendable { } final class RenderedSampleBuffer: @unchecked Sendable { + /// Minimum number of consumed samples before compaction is worthwhile. + /// Compacting shifts the remaining samples to the front of storage, so we + /// only pay that cost once enough dead space (and at least half the array, + /// see `compactStorageIfNeeded()`) has accumulated. + private static let compactionThreshold = 4096 + private let lock = NSLock() private var samples: [Float] = [] private var readOffset = 0 @@ -368,7 +374,7 @@ final class RenderedSampleBuffer: @unchecked Sendable { private func compactStorageIfNeeded() { guard readOffset > 0, - readOffset >= 4096, + readOffset >= Self.compactionThreshold, readOffset * 2 >= samples.count else { return }