3
3
// FirebladeECSDemo
4
4
//
5
5
// Created by Igor Kravchenko on 18.11.2020.
6
+ // Modified by @pcbeard on 29.9.2021.
6
7
//
7
8
8
9
import Dispatch
9
10
import FirebladeECS
10
11
import SDL2
11
12
13
+ /// In-memory copy of a sound file. Currently these remain loaded forever to avoid audio dropouts.
14
+ struct AudioData {
15
+ var sound : Audio . Sound
16
+ var spec : SDL_AudioSpec
17
+ var buffer : UnsafeMutableRawPointer
18
+ var length : Int
19
+ }
20
+
21
+ typealias MixBuffer = ( count: Int , format: SDL_AudioFormat , pointer: UnsafeMutableRawPointer )
22
+
23
+ struct MixCallbackData {
24
+ var player : MixingAudioPlayer
25
+ var queue : DispatchQueue
26
+ var buffers : [ MixBuffer ] = [ ]
27
+ }
28
+
29
+ /// helper function to convert arrays or inout parameters into scoped pointers.
30
+ @inlinable public func withPointer< R, T1> ( _ p1 : UnsafePointer < T1 > ,
31
+ _ body: ( UnsafePointer < T1 > ) throws -> R )
32
+ rethrows -> R {
33
+ try body ( p1)
34
+ }
35
+
36
+ @inlinable public func withMutablePointer< R, T1> ( _ p1 : UnsafeMutablePointer < T1 > ,
37
+ _ body: ( UnsafeMutablePointer < T1 > ) throws -> R )
38
+ rethrows -> R {
39
+ try body ( p1)
40
+ }
41
+
42
+ typealias MutableSamplePtr = UnsafeMutablePointer < UInt8 >
43
+ typealias SamplePtr = UnsafePointer < UInt8 >
44
+ typealias Mixer = ( MutableSamplePtr , SamplePtr , SDL_AudioFormat , Int ) -> Void
45
+
46
+ func mixAudioBuffers( _ data: inout MixCallbackData ,
47
+ _ audioStream: MutableSamplePtr ,
48
+ _ length: Int ) -> Bool
49
+ {
50
+ var finished = false
51
+ func mixBuffer( _ buffer : inout MixBuffer , mixer: Mixer ) {
52
+ let count = min ( length, buffer. count)
53
+ if count == 0 {
54
+ // This buffer is empty. Schedule buffer removal.
55
+ finished = true
56
+ } else {
57
+ // mix the next range of samples.
58
+ let bufferPointer = UnsafeRawPointer ( buffer. pointer) . assumingMemoryBound ( to: UInt8 . self)
59
+ mixer ( audioStream, bufferPointer, buffer. format, count)
60
+ buffer. pointer = buffer. pointer. advanced ( by: count)
61
+ buffer. count -= count
62
+ }
63
+ }
64
+ let bufferCount = data. buffers. count
65
+ withMutablePointer ( & data. buffers) { buffers in
66
+ if bufferCount == 1 {
67
+ // special case single buffer case, no need to mix.
68
+ mixBuffer ( & buffers[ 0 ] ) {
69
+ memcpy ( $0, $1, $3)
70
+ // but need to clear remainder.
71
+ let count = $3, remaining = length - count
72
+ if remaining > 0 {
73
+ memset ( UnsafeMutableRawPointer ( audioStream) . advanced ( by: count) , 0 , remaining)
74
+ }
75
+ }
76
+ } else {
77
+ // fill stream with silence before mixing.
78
+ memset ( audioStream, 0 , length)
79
+ for i in 0 ..< bufferCount {
80
+ mixBuffer ( & buffers[ i] ) {
81
+ SDL_MixAudioFormat ( $0, $1, $2, UInt32 ( $3) , SDL_MIX_MAXVOLUME)
82
+ }
83
+ }
84
+ }
85
+ }
86
+ return finished
87
+ }
88
+
89
+ func mixAudioCallback( userDataOrNil : UnsafeMutableRawPointer ? ,
90
+ audioStreamOrNil: UnsafeMutablePointer < UInt8 > ? ,
91
+ length : Int32 )
92
+ {
93
+ // validate that pointers aren't nil
94
+ if let userData = userDataOrNil, let audioStream = audioStreamOrNil {
95
+ let data = userData. assumingMemoryBound ( to: MixCallbackData . self)
96
+ let finished = mixAudioBuffers ( & data. pointee, audioStream, Int ( length) )
97
+ if finished {
98
+ data. pointee. queue. async {
99
+ let player = data. pointee. player
100
+ player. buffersFinished ( )
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ class MixingAudioPlayer {
107
+ let data : UnsafeMutablePointer < MixCallbackData >
108
+ var device : SDL_AudioDeviceID
109
+ var playing = false
110
+
111
+ init ( _ queue : DispatchQueue , _ audioSpec : SDL_AudioSpec ) {
112
+ data = UnsafeMutablePointer< MixCallbackData> . allocate( capacity: 1 )
113
+
114
+ var want = audioSpec
115
+ want. samples = 512
116
+ want. callback = mixAudioCallback
117
+ want. userdata = UnsafeMutableRawPointer ( data)
118
+ var have = SDL_AudioSpec ( )
119
+ device = SDL_OpenAudioDevice ( nil , 0 , & want, & have, 0 )
120
+ if device > 0 {
121
+ data. initialize ( to: MixCallbackData ( player: self , queue: queue) )
122
+ }
123
+ }
124
+
125
+ deinit {
126
+ if device > 0 {
127
+ SDL_CloseAudioDevice ( device)
128
+ }
129
+ // For this to be safe, we need a guarantee that the callback will never be called
130
+ // again. Testing shows that closing the audio device isn't a strong enough guarantee,
131
+ // so the `CallbackData` blocks are leaked deliberately.
132
+ // data.deallocate()
133
+ }
134
+
135
+ /// Prepare the player to play audio data. In theory, this could be called
136
+ /// at any moment, to interrupt the currently playing sound, but in practice
137
+ /// that will cause clicks, so this is only ever called when the audio
138
+ /// device is paused.
139
+ /// - Parameter audioData:
140
+ func prepare( _ audioData : AudioData ) {
141
+ SDL_LockAudioDevice ( device)
142
+ data. pointee. buffers. append (
143
+ ( count: audioData. length,
144
+ format: audioData. spec. format,
145
+ pointer: audioData. buffer) )
146
+ SDL_UnlockAudioDevice ( device)
147
+ }
148
+
149
+ func buffersFinished( ) {
150
+ SDL_LockAudioDevice ( device)
151
+ data. pointee. buffers = data. pointee. buffers. filter { $0. count > 0 }
152
+ SDL_UnlockAudioDevice ( device)
153
+ if data. pointee. buffers. isEmpty {
154
+ self . pause ( )
155
+ }
156
+ }
157
+
158
+ func start( ) {
159
+ if !playing {
160
+ SDL_PauseAudioDevice ( device, 0 )
161
+ playing = true
162
+ }
163
+ }
164
+
165
+ func pause( ) {
166
+ if playing {
167
+ SDL_PauseAudioDevice ( device, 1 )
168
+ playing = false
169
+ }
170
+ }
171
+
172
+ func dump( ) {
173
+ Swift . dump ( data. pointee, maxDepth: 1 )
174
+ }
175
+ }
176
+
12
177
class AudioSystem {
13
178
private let queue = DispatchQueue ( label: " asteroids.audio " ,
14
179
qos: . userInteractive,
@@ -19,6 +184,12 @@ class AudioSystem {
19
184
20
185
init ( nexus: Nexus ) {
21
186
family = nexus. family ( requires: Audio . self)
187
+ queue. async {
188
+ // preload all known sounds.
189
+ Audio . Sound. allCases. forEach { sound in
190
+ self . prepare ( sound: sound)
191
+ }
192
+ }
22
193
}
23
194
24
195
func update( ) {
@@ -32,40 +203,76 @@ class AudioSystem {
32
203
}
33
204
}
34
205
35
- func play( sound: Audio . Sound ) {
36
- guard let path = bundleResourcesPath ( ) ? . appendingPathComponent ( sound. rawValue) . path else {
37
- assertionFailure ( " unable to find path for ' \( sound. rawValue) ' resource " )
38
- return
206
+ private var audioDataCache : [ Audio . Sound : AudioData ] = [ : ]
207
+
208
+ func fetchAudioData( _ sound: Audio . Sound ) -> AudioData ? {
209
+ if let audioData = audioDataCache [ sound] {
210
+ return audioData
39
211
}
40
- assert ( sound. rawValue. hasSuffix ( " .wav " ) )
41
- var specIn = SDL_AudioSpec ( )
42
- var specOut = SDL_AudioSpec ( )
43
- let audio = SDL_OpenAudioDevice ( . none, 0 , & specIn, & specOut, 0 )
44
- guard audio > 0 , let ops = SDL_RWFromFile ( path, " rb " ) else {
45
- print ( String ( cString: SDL_GetError ( ) ) )
46
- return
212
+ if let path = bundleResourcesPath ( ) ? . appendingPathComponent ( sound. rawValue) . path {
213
+ if let ops = SDL_RWFromFile ( path, " rb " ) {
214
+ var spec = SDL_AudioSpec ( )
215
+ var length : UInt32 = 0
216
+ var bufferOrNil : UnsafeMutablePointer < UInt8 > ?
217
+ if SDL_LoadWAV_RW ( ops, 1 , & spec, & bufferOrNil, & length) != nil , let buffer = bufferOrNil {
218
+ let audioData = AudioData ( sound: sound, spec: spec, buffer: UnsafeMutableRawPointer ( buffer) , length: Int ( length) )
219
+ audioDataCache [ sound] = audioData
220
+ return audioData
221
+ }
222
+ }
223
+ }
224
+ if let error = SDL_GetError ( ) {
225
+ print ( String ( cString: error) )
47
226
}
227
+ return nil
228
+ }
48
229
49
- var wavLength : Uint32 = 0
50
- var wavBuffer : UnsafeMutablePointer < Uint8 > ?
51
- if SDL_LoadWAV_RW (
52
- ops,
53
- 1 ,
54
- & specIn,
55
- & wavBuffer,
56
- & wavLength
57
- ) == nil {
58
- print ( String ( cString: SDL_GetError ( ) ) )
230
+ private var mixingPlayer : MixingAudioPlayer ?
231
+
232
+ func getMixingPlayer( _ audioSpec : SDL_AudioSpec ) -> MixingAudioPlayer ? {
233
+ if let player = mixingPlayer {
234
+ return player
235
+ }
236
+ let player = MixingAudioPlayer ( queue, audioSpec)
237
+ mixingPlayer = player
238
+ return player
239
+ }
240
+
241
+ private func play( audio audioData : AudioData ) {
242
+ if let player = getMixingPlayer ( audioData. spec) {
243
+ player. prepare ( audioData)
244
+ player. start ( )
59
245
}
246
+ }
60
247
61
- SDL_QueueAudio ( audio, wavBuffer, wavLength)
62
- SDL_PauseAudioDevice ( audio, 0 )
248
+ private func prepare( audio audioData : AudioData ) -> Bool {
249
+ getMixingPlayer ( audioData. spec) != nil
250
+ }
63
251
64
- while SDL_GetQueuedAudioSize ( audio) > 0 {
65
- SDL_Delay ( 1000 )
252
+ /**
253
+ * Plays a sound asynchronously using an SDL sound device. Because closing audio devices
254
+ * after playback seems to be so race-prone (causing crashes), this creates a single instance of
255
+ * the class `MixAudioPlayer` which keeps the audio device paused when not in use
256
+ * (to reduce CPU). The player allocates a an unsafe pointer to a `MixCallbackData` struct,
257
+ * which is passed to the callback function `mixAudioCallback`, when the audio device needs
258
+ * audio buffers to play. The `MixCallbackData` struct contains an array of audio buffers to be mixed.
259
+ * When an audio buffer is fully consumed, `mixAudioBuffers()` returns true, and
260
+ * `mixAudioCallback` calls `MixAudioPlayer.buffersFinished()` which removes
261
+ * any buffers where `buffer.count == 0`.
262
+ * - Parameter sound: an enum that specifies a known .wav file resource.
263
+ */
264
+ func play( sound: Audio . Sound ) {
265
+ assert ( sound. rawValue. hasSuffix ( " .wav " ) )
266
+ if let audioData = fetchAudioData ( sound) {
267
+ play ( audio: audioData)
66
268
}
67
-
68
- SDL_CloseAudioDevice ( audio)
69
- SDL_FreeWAV ( wavBuffer)
269
+ }
270
+
271
+ /// Preloads audio data and starts the audio system so sounds will play immediately.
272
+ @discardableResult func prepare( sound: Audio . Sound ) -> Bool {
273
+ if let audioData = fetchAudioData ( sound) {
274
+ return prepare ( audio: audioData)
275
+ }
276
+ return false
70
277
}
71
278
}
0 commit comments