Hi, first post here, I've also raised this question on stack overflow.
I'm getting crashes from a fixed size array even though I thought I'd protected the index from going out of range. The array updates in a loop, like a ring buffer.
This crashes reliably when the array size >638 but I've not managed to get it to crash with fewer elements.
I'm wondering whether this is compiler 'optimisation'... and what to do if it is. Any ideas gratefully received.
struct ZeroCrossing {
//The integer index of the second sample (cross over is between two samples)
var index: UInt
//The highest amplitude peak (negative or positive) between this and the previous crossing
let previousPeak: Float
//The interpolated crossover point between the two sample indices
let indexWithOffset: Double
}
class CrossingBuffer {
private var array: [ZeroCrossing]
private let size: Int
private var nextWriteIndex = 0
private var full: Bool {
nextWriteIndex >= size
}
init(size: Int) {
self.size = size
array = [ZeroCrossing](repeating: ZeroCrossing(index: 0, previousPeak: 0, indexWithOffset: 0), count: size)
array.reserveCapacity(size)
}
public func write(_ val: ZeroCrossing) {
array[nextWriteIndex % size] = val
nextWriteIndex += 1
}
public func getAfterIndex(_ refIndex: Double) -> [ZeroCrossing]? {
if !full { return nil }
var subArray = [ZeroCrossing]()
let lastElementIndex = nextWriteIndex - 1
for i in 0...size - 1 {
// CRASHES ON NEXT LINE !!!
let thisCrossing = array[(lastElementIndex - i) % size]
if thisCrossing.indexWithOffset > refIndex {
subArray.append(thisCrossing)
} else {
break
}
}
return subArray.reversed()
}
public func reset() {
array = [ZeroCrossing](repeating: ZeroCrossing(index: 0, previousPeak: 0, indexWithOffset: 0), count: size)
nextWriteIndex = 0
}
}
The backtrace ends with the following
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x1016dc008)
frame #0: 0x000000018bf5c090 libswiftCore.dylib`swift_retain + 60
frame #1: 0x000000018bf9c704 libswiftCore.dylib`swift_bridgeObjectRetain + 56
* frame #2: 0x0000000100838e50 CrossingBuffer.getAfterIndex(refIndex=431975.76999999583, self=0x00000002800a86f0) at CrossingBuffer.swift:97:31
frame #3: 0x00000001007cd704 correlate(self=0x000000010130e5c0) at PitchEngine.swift:146:52
frame #4: 0x00000001007a155c correlate(self=0x0000000283b91200) at TunerEngine.swift:57:39
frame #5: 0x00000001007a1bd8 @objc correlate() at <compiler-generated>:0
But what's strange is that I can access the element from the debug console using the same index reference as the line it crashed on:
(lldb) print (lastElementIndex - i) % size
(Int) $R4 = 838
(lldb) print array.count
(Int) $R5 = 1000
(lldb) print array[(lastElementIndex - i) % size]
(ZeroCrossing) $R6 = (index = 438691, previousPeak = 0.0251232013, indexWithOffset = 438690.12000000477)
The following code should trigger the crash, the mic needs to pick up some noise for the array to populate.
import Foundation
import AVKit
final class AudioTap {
private var audioEngine = AVAudioEngine()
private var windowIndex: UInt = 0
private var lastPeak: Float = 0
private var lastSample: Sample?
public var minPeakSize: Float = 0.005
// Size of crossingBuffer changes crashing behavior
private var crossingBuffer = CrossingBuffer(size: 2000)
private var lastSeekIndex: Double = 0.0
private var timer: Timer?
init() {
installTunerTap()
audioEngine.prepare()
do {
try audioEngine.start()
} catch let error as NSError {
print("AVAudioEngine error on start: \(error.domain), \(error)")
}
timer = Timer.scheduledTimer(
timeInterval: 0.01,
target: self,
selector: #selector(getNext),
userInfo: nil,
repeats: true)
}
private func installTunerTap() {
let inputNode = audioEngine.inputNode
inputNode.installTap( onBus: 0,
bufferSize: 1000,
format: nil,
block: { buffer, when in
let sampleCount = Int(buffer.frameLength)
var sampleIndex = 0
while (sampleIndex < sampleCount) {
if let val = buffer.floatChannelData?.pointee[sampleIndex]{
let sample = Sample(index: self.windowIndex, val: val)
self.update(sample: sample)
}
self.windowIndex += 1
sampleIndex += 1
}
})
}
private func update(sample: Sample) {
lastPeak = abs(sample.val) > abs(lastPeak) ? sample.val : lastPeak
if let last = lastSample {
if last.val * sample.val < 0 && abs(lastPeak) > minPeakSize { // this is a zero crossing
let offset = Double(sample.index) + Double(round((sample.val/(last.val - sample.val)) * 100) / 100)
let crossing = ZeroCrossing(
index: sample.index,
previousPeak: lastPeak,
indexWithOffset: offset
)
crossingBuffer.write(crossing)
lastPeak = 0
}
}
lastSample = sample
}
@objc func getNext() {
if let arr = crossingBuffer.getAfterIndex(lastSeekIndex) {
if let s = arr.last {
lastSeekIndex = s.indexWithOffset
}
}
}
}