Skip to content
Merged
Show file tree
Hide file tree
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
46 changes: 39 additions & 7 deletions Sources/OpenFaderCore/AudioRenderLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,15 @@ 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
private let maximumSampleCount: Int

init(maximumSampleCount: Int = 48_000 * 8) {
Expand All @@ -309,7 +316,7 @@ final class RenderedSampleBuffer: @unchecked Sendable {

var sampleCount: Int {
lock.withLock {
samples.count
samples.count - readOffset
}
}

Expand All @@ -320,8 +327,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
}
}

Expand All @@ -334,21 +347,40 @@ 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..<readEnd])
readOffset = readEnd
if readOffset == samples.count {
samples.removeAll(keepingCapacity: true)
readOffset = 0
} else {
compactStorageIfNeeded()
}
return nextSamples
}
}

func clear() {
lock.withLock {
samples.removeAll()
samples.removeAll(keepingCapacity: true)
readOffset = 0
}
}

private func compactStorageIfNeeded() {
guard readOffset > 0,
readOffset >= Self.compactionThreshold,
readOffset * 2 >= samples.count else {
return
}

samples.removeFirst(readOffset)
readOffset = 0
}
}

Expand Down
27 changes: 27 additions & 0 deletions Tests/OpenFaderCoreTests/RenderedSampleBufferTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
Loading