This question was revised significantly to narrow its scope upon identifying a more fundamental issue with the code sample. The previous version contained a discussion of multiple AVAudioPlayer related errors, while this one focuses on the error in the title.
The other errors were mostly resolved in the Community Wiki answer below, and there were no answers or comments by other users at the time of this edit, so I've taken the opportunity to improve the focus of this question by narrowing its scope and updating the code sample to better reflect the underlying issue.
In attempting to play sounds interactively in fast succession, I have encountered a recurring issue with data race conflicts resulting from multiple threads trying to access the same variable. This occurs when attempting to update a status boolean (representing whether an instance of AVAudioPlayer has finished playing a sound) from a global DispatchQueue. This answer correctly pointed out the cause of the error, and applying Thread Sanitizer confirmed it (the error traceback is included at the bottom of the question).
Declaring singleton queues instead of creating a new queue every time playEffect is called did not fix the problem. How can the 'timer' process for resetting sound effect availability be made thread safe, to avoid any possibility of simultaneous accesses? This is the part of the code which is causing the error:
audioPlayers[fileName]?.1.asyncAfter(deadline: .now() + (audioPlayers[fileName]?.0 ?? 1.39)) {
audioPlayers[fileName]?.2[playerIndex].1 = true
print("Reset audio player #\(playerIndex) for sound effect \(fileName) status to Available")
}
Below is the MCVE and the error traceback:
AppDelegate didFinishLaunchingWithOptions
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Send this to the utility queue, to avoid stalling the app launch if it takes a long time
DispatchQueue.global(qos: .utility).async {
do {
let emptySoundFileURL = Bundle.main.url(forResource: "Empty", withExtension: "m4a")
ErrorGenerator.AVInitializer = try AVAudioPlayer(contentsOf: emptySoundFileURL!)
ErrorGenerator.AVInitializer?.play()
ErrorGenerator.soundIsReady = true
print("SOUND IS READY!")
}
catch {
print("Error initializing AVAudioPlayer: \(error)")
}
}
return true
}
ViewController file
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var tappableView: UIView!
let colors = [UIColor.systemGreen, UIColor.systemPink]
var colorIndex = true
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func viewTapped(_ sender: UITapGestureRecognizer) {
// Shows that the square view has recognized a tap by toggling its color
colorIndex.toggle()
tappableView.backgroundColor = colors[Int(truncating: NSNumber(value: colorIndex))]
// Call the sound generating function from the ErrorGenerator struct
ErrorGenerator.playTapSounds()
}
}
ErrorGenerator file
import Foundation
import AVFoundation
struct ErrorGenerator {
// Will be set inside AppDelegate, as soon as the app launches
static var AVInitializer: AVAudioPlayer?
static var soundIsReady = false
// Instead of generating a new queue every time a sound is played, define 4 singleton queues and only use these to avoid collisions, as discussed in this answer by matt: https://stackoverflow.com/a/68250594/18248018
static let queue1 = DispatchQueue(label: "Sound 1 Queue", qos: .utility)
static let queue2 = DispatchQueue(label: "Sound 2 Queue", qos: .utility)
static let queue3 = DispatchQueue(label: "Sound 3 Queue", qos: .utility)
static let queue4 = DispatchQueue(label: "Sound 4 Queue", qos: .utility)
// EACH SOUND EFFECT HAS 3 ASSOCIATED AVAUDIOPLAYER INSTANCES, TO ALLOW IT TO BE PLAYED SIMULTANEOUSLY UP TO 3 TIMES.
// Dictionary keys are the sound effect names.
// Values are tuples containing 0. as length of the sound effect in seconds, 1. as the .utility queue associated with the sound effect, and 2. as an array of 3 inner tuples.
// Inner tuples have 0. as an AVAudioPlayer instance, and 1. as a boolean representing whether the instance has finished playing/is ready to play its corresponding sound effect.
static var audioPlayers: [String: (Double, DispatchQueue, [(AVAudioPlayer?, Bool)])] = [
"Sound 1": (1.39, queue1, [(nil, true), (nil, true), (nil, true)]),
"Sound 2": (1.1, queue2, [(nil, true), (nil, true), (nil, true)]),
"Sound 3": (1.28, queue3, [(nil, true), (nil, true), (nil, true)]),
"Sound 4": (1.06, queue4, [(nil, true), (nil, true), (nil, true)])
]
// The sound effect name will be provided by the random number generator when this function is called inside playTapSounds
static func playEffect(named fileName: String) {
if soundIsReady {
let playerIndex = soundEffectIsAvailableAtIndex(named: fileName)
// Don't attempt to play the sound effect unless there is an available AVAudioPlayer for it.
// This caps the number of simultaneous threads that can be generated by this function at 12.
if playerIndex != -1 {
do {
let soundFileURL = Bundle.main.url(forResource: fileName, withExtension: "m4a")
audioPlayers[fileName]?.2[playerIndex].0 = try AVAudioPlayer(contentsOf: soundFileURL!)
// Shows that this audio player is busy - do not attempt to reuse it until it has finished playing the sound effect
audioPlayers[fileName]?.2[playerIndex].1 = false
audioPlayers[fileName]?.2[playerIndex].0?.play()
print("Playing sound effect \(fileName) on audio player #\(playerIndex)")
// Reset the audio player's availability status after the sound has finished playing. The default value is the length of the longest sound effect (in seconds).
// Use the correct one of the 4 queues defined above. Do not generate new queues here, as that was the source of the EXC_BAD_ACCESS errors.
audioPlayers[fileName]?.1.asyncAfter(deadline: .now() + (audioPlayers[fileName]?.0 ?? 1.39)) {
audioPlayers[fileName]?.2[playerIndex].1 = true
print("Reset audio player #\(playerIndex) for sound effect \(fileName) status to Available")
}
}
catch {
print("Error playing sound effect \(fileName): \(error)")
}
}
} else {
print("Sound effect capability has not yet been initialized by AppDelegate")
}
}
// Returns the first index (possibilities are 0, 1 or 2) of an available (i.e. associated boolean value is true) sound effect in the audioPlayers dict. Returns -1 if none are available.
static func soundEffectIsAvailableAtIndex(named fileName: String) -> Int {
var index = -1
if let dictValue = audioPlayers[fileName] {
for i in 0..<dictValue.2.count {
if dictValue.2[i].1 {
index = i
break
}
}
}
return index
}
static func playTapSounds() {
let s = "Sound \(Int.random(in: 1...4))"
playEffect(named: s)
}
}
Error messages from Xcode console
Sound Errors(40921,0x104b20580) malloc: nano zone abandoned due to inability to preallocate reserved vm space.
2022-09-22 14:19:03.911423-0500 Sound Errors[40921:2942859] DataSource read failed
2022-09-22 14:19:04.153477-0500 Sound Errors[40921:2942865] [plugin] AddInstanceForFactory: No factory registered for id <CFUUID 0x112351560> F8BB1C28-BAE8-11D6-9C31-00039315CD46
SOUND IS READY!
2022-09-22 14:19:07.614898-0500 Sound Errors[40921:2940418] DataSource read failed
Playing sound effect Sound 3 on audio player #0
2022-09-22 14:19:08.740789-0500 Sound Errors[40921:2940418] DataSource read failed
Playing sound effect Sound 4 on audio player #0
2022-09-22 14:19:08.939415-0500 Sound Errors[40921:2940418] DataSource read failed
Playing sound effect Sound 1 on audio player #0
Reset audio player #0 for sound effect Sound 3 status to Available
2022-09-22 14:19:09.089194-0500 Sound Errors[40921:2940418] DataSource read failed
Playing sound effect Sound 3 on audio player #0
2022-09-22 14:19:09.256983-0500 Sound Errors[40921:2940418] DataSource read failed
Playing sound effect Sound 2 on audio player #0
2022-09-22 14:19:09.406033-0500 Sound Errors[40921:2940418] DataSource read failed
Playing sound effect Sound 3 on audio player #1
2022-09-22 14:19:09.540224-0500 Sound Errors[40921:2940418] DataSource read failed
Playing sound effect Sound 2 on audio player #1
2022-09-22 14:19:09.823113-0500 Sound Errors[40921:2940418] DataSource read failed
Playing sound effect Sound 2 on audio player #2
Reset audio player #0 for sound effect Sound 4 status to Available
2022-09-22 14:19:09.990802-0500 Sound Errors[40921:2940418] DataSource read failed
Playing sound effect Sound 4 on audio player #0
2022-09-22 14:19:10.158240-0500 Sound Errors[40921:2940418] DataSource read failed
Playing sound effect Sound 1 on audio player #1
2022-09-22 14:19:10.310215-0500 Sound Errors[40921:2940418] DataSource read failed
Playing sound effect Sound 1 on audio player #2
Reset audio player #0 for sound effect Sound 1 status to Available
Reset audio player #0 for sound effect Sound 2 status to Available
2022-09-22 14:19:10.474106-0500 Sound Errors[40921:2940418] DataSource read failed
==================
WARNING: ThreadSanitizer: Swift access race (pid=40921)
Modifying access of Swift variable at 0x000104869bd0 by thread T6:
#0 closure #1 in static ErrorGenerator.playEffect(named:) ErrorOrigin.swift:50 (Sound Errors:arm64+0x10000dc90)
#1 partial apply for closure #1 in static ErrorGenerator.playEffect(named:) <compiler-generated> (Sound Errors:arm64+0x10000e118)
#2 thunk for @escaping @callee_guaranteed () -> () <compiler-generated> (Sound Errors:arm64+0x100008620)
#3 __tsan::invoke_and_release_block(void*) <null>:81817444 (libclang_rt.tsan_iossim_dynamic.dylib:arm64+0x7b690)
#4 _dispatch_client_callout <null>:81817444 (libdispatch.dylib:arm64+0x5d58)
Previous modifying access of Swift variable at 0x000104869bd0 by thread T8:
#0 closure #1 in static ErrorGenerator.playEffect(named:) ErrorOrigin.swift:50 (Sound Errors:arm64+0x10000dc90)
#1 partial apply for closure #1 in static ErrorGenerator.playEffect(named:) <compiler-generated> (Sound Errors:arm64+0x10000e118)
#2 thunk for @escaping @callee_guaranteed () -> () <compiler-generated> (Sound Errors:arm64+0x100008620)
#3 __tsan::invoke_and_release_block(void*) <null>:81817444 (libclang_rt.tsan_iossim_dynamic.dylib:arm64+0x7b690)
#4 _dispatch_client_callout <null>:81817444 (libdispatch.dylib:arm64+0x5d58)
Location is global 'static ErrorGenerator.audioPlayers' at 0x000104869bd0 (Sound Errors+0x100019bd0)
Thread T6 (tid=2942864, running) is a GCD worker thread
Thread T8 (tid=2942865, running) is a GCD worker thread
SUMMARY: ThreadSanitizer: Swift access race ErrorOrigin.swift:50 in closure #1 in static ErrorGenerator.playEffect(named:)
==================
ThreadSanitizer report breakpoint hit. Use 'thread info -s' to get extended information about the report.
Reset audio player #0 for sound effect Sound 3 status to Available
Reset audio player #0 for sound effect Sound 4 status to Available
Reset audio player #1 for sound effect Sound 3 status to Available
2022-09-22 14:19:11.462591-0500 Sound Errors[40921:2942865] -[NSTaggedPointerString count]: unrecognized selector sent to instance 0x8000000000000000
Playing sound effect Sound 1 on audio player #0
2022-09-22 14:19:11.473913-0500 Sound Errors[40921:2940418] [SystemGestureGate] <0x111f0b900> Gesture: System gesture gate timed out.
2022-09-22 14:19:11.475033-0500 Sound Errors[40921:2940418] DataSource read failed
2022-09-22 14:19:11.501216-0500 Sound Errors[40921:2942865] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSTaggedPointerString count]: unrecognized selector sent to instance 0x8000000000000000'
*** First throw call stack:
(
0 CoreFoundation 0x000000018040c304 __exceptionPreprocess + 172
1 libobjc.A.dylib 0x000000018005131c objc_exception_throw + 56
2 CoreFoundation 0x000000018041afbc +[NSObject(NSObject) instanceMethodSignatureForSelector:] + 0
3 CoreFoundation 0x0000000180410204 ___forwarding___ + 1308
4 CoreFoundation 0x000000018041268c _CF_forwarding_prep_0 + 92
5 libswiftCore.dylib 0x000000018bcb611c $sSD8_VariantVyq_SgxciM + 164
6 libswiftCore.dylib 0x000000018bcb6000 $sSDyq_SgxciM + 132
7 Sound Errors 0x000000010485dcb0 $s12Sound_Errors14ErrorGeneratorV10playEffect5namedySS_tFZyycfU_ + 280
8 Sound Errors 0x000000010485e11c $s12Sound_Errors14ErrorGeneratorV10playEffect5namedySS_tFZyycfU_TA + 36
9 Sound Errors 0x0000000104858624 $sIeg_IeyB_TR + 96
10 libclang_rt.tsan_iossim_dynamic.dyl 0x0000000104e77694 _ZN6__tsanL24invoke_and_release_blockEPv + 24
11 libclang_rt.tsan_iossim_dynamic.dyl 0x0000000104e774f0 _ZN6__tsanL22dispatch_callback_wrapEPv + 664
12 libdispatch.dylib 0x0000000104d55d5c _dispatch_client_callout + 16
13 libdispatch.dylib 0x0000000104d59210 _dispatch_continuation_pop + 756
14 libdispatch.dylib 0x0000000104d708c4 _dispatch_source_invoke + 1684
15 libdispatch.dylib 0x0000000104d5ddfc _dispatch_lane_serial_drain + 348
16 libdispatch.dylib 0x0000000104d5ed80 _dispatch_lane_invoke + 428
17 libdispatch.dylib 0x0000000104d6cb40 _dispatch_workloop_worker_thread + 1720
18 libsystem_pthread.dylib 0x00000001ae55eb40 _pthread_wqthread + 284
19 libsystem_pthread.dylib 0x00000001ae55d904 start_wqthread + 8
)
libc++abi: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSTaggedPointerString count]: unrecognized selector sent to instance 0x8000000000000000'
terminating with uncaught exception of type NSException
CoreSimulator 857.7 - Device: iPhone 12 (D510B935-5295-43DC-AC72-E779D87B1D64) - Runtime: iOS 16.0 (20A360) - DeviceType: iPhone 12
