Skip to content

Commit 5a376a8

Browse files
committed
Enable strict concurrency
1 parent 48c6024 commit 5a376a8

12 files changed

+149
-96
lines changed

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ GENERIC_PLATFORM_MAC_CATALYST = platform=macOS,variant=Mac Catalyst
77
GENERIC_PLATFORM_TVOS = generic/platform=tvOS
88
GENERIC_PLATFORM_VISIONOS = generic/platform=visionOS
99

10-
SIM_PLATFORM_IOS = platform=iOS Simulator,id=$(call udid_for,iOS 18.0,iPhone \d\+ Pro [^M])
10+
SIM_PLATFORM_IOS = platform=iOS Simulator,id=$(call udid_for,iOS 18.2,iPhone \d\+ Pro [^M])
1111
SIM_PLATFORM_MACOS = platform=macOS,arch=arm64
1212
SIM_PLATFORM_MAC_CATALYST = platform=macOS,variant=Mac Catalyst,arch=arm64
13-
SIM_PLATFORM_TVOS = platform=tvOS Simulator,id=$(call udid_for,tvOS 18.0,TV)
14-
SIM_PLATFORM_VISIONOS = platform=visionOS Simulator,id=$(call udid_for,visionOS 2.0,Vision)
13+
SIM_PLATFORM_TVOS = platform=tvOS Simulator,id=$(call udid_for,tvOS 18.2,TV)
14+
SIM_PLATFORM_VISIONOS = platform=visionOS Simulator,id=$(call udid_for,visionOS 2.2,Vision)
1515

1616
GREEN='\033[0;32m'
1717
NC='\033[0m'

Package.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
// swift-tools-version:5.9
1+
// swift-tools-version:6.0
2+
23
import PackageDescription
34

45
let package = Package(
@@ -19,7 +20,9 @@ let package = Package(
1920
.target(
2021
name: "ChunkedAudioPlayer",
2122
path: "Sources",
22-
resources: [.copy("Resources/PrivacyInfo.xcprivacy")]
23+
resources: [.copy("Resources/PrivacyInfo.xcprivacy")],
24+
swiftSettings: [.enableExperimentalFeature("StrictConcurrency")]
2325
)
24-
]
26+
],
27+
swiftLanguageModes: [.v6]
2528
)

[email protected]

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// swift-tools-version:5.10
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "swift-chunked-audio-player",
7+
platforms: [
8+
.iOS(.v15),
9+
.macOS(.v12),
10+
.tvOS(.v15),
11+
.visionOS(.v1)
12+
],
13+
products: [
14+
.library(
15+
name: "ChunkedAudioPlayer",
16+
targets: ["ChunkedAudioPlayer"]
17+
)
18+
],
19+
targets: [
20+
.target(
21+
name: "ChunkedAudioPlayer",
22+
path: "Sources",
23+
resources: [.copy("Resources/PrivacyInfo.xcprivacy")]
24+
)
25+
],
26+
swiftLanguageVersions: [.v5]
27+
)

Sources/AudioBuffersQueue.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import AVFoundation
22
import os
33

4-
final class AudioBuffersQueue {
4+
final class AudioBuffersQueue: Sendable {
55
private let audioDescription: AudioStreamBasicDescription
6-
private var allBuffers = [CMSampleBuffer]()
7-
private var buffers = [CMSampleBuffer]()
6+
private nonisolated(unsafe) var allBuffers = [CMSampleBuffer]()
7+
private nonisolated(unsafe) var buffers = [CMSampleBuffer]()
88
private let lock = NSLock()
99

10-
private(set) var duration = CMTime.zero
10+
private(set) nonisolated(unsafe) var duration = CMTime.zero
1111

1212
var isEmpty: Bool {
1313
withLock { buffers.isEmpty }

Sources/AudioFileStream.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import AVFoundation
22
import AudioToolbox
33

4-
final class AudioFileStream {
5-
typealias ErrorCallback = (_ error: AudioPlayerError) -> Void
6-
typealias ASBDCallback = (_ asbd: AudioStreamBasicDescription) -> Void
7-
typealias PacketsCallback = (
4+
final class AudioFileStream: Sendable {
5+
typealias ErrorCallback = @Sendable (_ error: AudioPlayerError) -> Void
6+
typealias ASBDCallback = @Sendable (_ asbd: AudioStreamBasicDescription) -> Void
7+
typealias PacketsCallback = @Sendable (
88
_ numberOfBytes: UInt32,
99
_ bytes: UnsafeRawPointer,
1010
_ numberOfPackets: UInt32,
@@ -17,9 +17,9 @@ final class AudioFileStream {
1717

1818
private let syncQueue: DispatchQueue
1919

20-
private(set) var audioStreamID: AudioFileStreamID?
21-
private(set) var fileTypeID: AudioFileTypeID?
22-
private(set) var parsingComplete = false
20+
private(set) nonisolated(unsafe) var audioStreamID: AudioFileStreamID?
21+
private(set) nonisolated(unsafe) var fileTypeID: AudioFileTypeID?
22+
private(set) nonisolated(unsafe) var parsingComplete = false
2323

2424
init(
2525
type: AudioFileTypeID? = nil,

Sources/AudioPlayer.swift

Lines changed: 63 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import Combine
33
import AVFoundation
44
import AudioToolbox
55

6-
public final class AudioPlayer: ObservableObject {
6+
@MainActor
7+
public final class AudioPlayer: ObservableObject, Sendable {
78
private let timeUpdateInterval: CMTime
89
private let initialVolume: Float
9-
private var task: Task<Void, Never>?
10-
private var synchronizer: AudioSynchronizer?
10+
private nonisolated(unsafe) var task: Task<Void, Never>?
11+
private nonisolated(unsafe) var synchronizer: AudioSynchronizer?
1112
private let didStartPlaying: @Sendable () -> Void
1213
private let didFinishPlaying: @Sendable () -> Void
1314
private let didUpdateBuffer: @Sendable (CMSampleBuffer) -> Void
@@ -49,7 +50,8 @@ public final class AudioPlayer: ObservableObject {
4950
}
5051

5152
deinit {
52-
stop()
53+
task?.cancel()
54+
synchronizer?.invalidate()
5355
}
5456

5557
public func start(_ stream: AnyPublisher<Data, Error>, type: AudioFileTypeID? = nil) {
@@ -120,76 +122,95 @@ public final class AudioPlayer: ObservableObject {
120122
synchronizer = nil
121123
}
122124

125+
// swiftlint:disable:next function_body_length
123126
private func prepareSynchronizer(type: AudioFileTypeID?) {
124127
synchronizer = AudioSynchronizer(
125128
timeUpdateInterval: timeUpdateInterval,
126129
initialVolume: initialVolume
127130
) { [weak self] rate in
128-
self?.setCurrentRate(rate)
131+
Task {
132+
await MainActor.run { [weak self] in
133+
self?.setCurrentRate(rate)
134+
}
135+
}
129136
} onTimeChanged: { [weak self] time in
130-
self?.setCurrentTime(time)
137+
Task {
138+
await MainActor.run { [weak self] in
139+
self?.setCurrentTime(time)
140+
}
141+
}
131142
} onDurationChanged: { [weak self] duration in
132-
self?.setCurrentDuration(duration)
143+
Task {
144+
await MainActor.run { [weak self] in
145+
self?.setCurrentDuration(duration)
146+
}
147+
}
133148
} onError: { [weak self] error in
134-
self?.setCurrentError(error)
135-
if error != nil {
136-
self?.didFinishPlaying()
149+
Task {
150+
await MainActor.run { [weak self] in
151+
self?.setCurrentError(error)
152+
if error != nil {
153+
self?.didFinishPlaying()
154+
}
155+
}
137156
}
138157
} onComplete: { [weak self] in
139-
self?.setCurrentState(.completed)
140-
self?.didFinishPlaying()
158+
Task {
159+
await MainActor.run { [weak self] in
160+
self?.setCurrentState(.completed)
161+
self?.didFinishPlaying()
162+
}
163+
}
141164
} onPlaying: { [weak self] in
142-
self?.setCurrentState(.playing)
143-
self?.didStartPlaying()
165+
Task {
166+
await MainActor.run { [weak self] in
167+
self?.setCurrentState(.playing)
168+
self?.didStartPlaying()
169+
}
170+
}
144171
} onPaused: { [weak self] in
145-
self?.setCurrentState(.paused)
172+
Task {
173+
await MainActor.run { [weak self] in
174+
self?.setCurrentState(.paused)
175+
}
176+
}
146177
} onSampleBufferChanged: { [weak self] buffer in
147-
self?.setCurrentBuffer(buffer)
178+
Task {
179+
await MainActor.run { [weak self] in
180+
self?.setCurrentBuffer(buffer)
181+
}
182+
}
148183
}
149184
synchronizer?.prepare(type: type)
150185
}
151186

152187
private func setCurrentRate(_ rate: Float) {
153-
DispatchQueue.main.async { [weak self] in
154-
guard let self, currentRate != rate else { return }
155-
currentRate = rate
156-
}
188+
guard currentRate != rate else { return }
189+
currentRate = rate
157190
}
158191

159192
private func setCurrentState(_ state: AudioPlayerState) {
160-
DispatchQueue.main.async { [weak self] in
161-
guard let self, currentState != state else { return }
162-
currentState = state
163-
}
193+
guard currentState != state else { return }
194+
currentState = state
164195
}
165196

166197
private func setCurrentError(_ error: AudioPlayerError?) {
167-
DispatchQueue.main.async { [weak self] in
168-
guard let self else { return }
169-
currentError = error
170-
if error != nil { currentState = .failed }
171-
}
198+
currentError = error
199+
if error != nil { currentState = .failed }
172200
}
173201

174202
private func setCurrentTime(_ time: CMTime) {
175-
DispatchQueue.main.async { [weak self] in
176-
guard let self, currentTime != time else { return }
177-
currentTime = time
178-
}
203+
guard currentTime != time else { return }
204+
currentTime = time
179205
}
180206

181207
private func setCurrentDuration(_ duration: CMTime) {
182-
DispatchQueue.main.async { [weak self] in
183-
guard let self, currentDuration != duration else { return }
184-
currentDuration = duration
185-
}
208+
guard currentDuration != duration else { return }
209+
currentDuration = duration
186210
}
187211

188212
private func setCurrentBuffer(_ buffer: CMSampleBuffer?) {
189-
DispatchQueue.main.async { [weak self] in
190-
guard let self else { return }
191-
currentBuffer = buffer
192-
buffer.flatMap(didUpdateBuffer)
193-
}
213+
currentBuffer = buffer
214+
buffer.flatMap(didUpdateBuffer)
194215
}
195216
}

Sources/AudioPlayerError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Foundation
22

3-
public enum AudioPlayerError: Error, Equatable, CustomDebugStringConvertible {
3+
public enum AudioPlayerError: Error, Equatable, CustomDebugStringConvertible, Sendable {
44
public static func == (lhs: AudioPlayerError, rhs: AudioPlayerError) -> Bool {
55
switch (lhs, rhs) {
66
case (.status(let lhsStatus), .status(let rhsStatus)):

Sources/AudioPlayerState.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
public enum AudioPlayerState: Equatable {
1+
public enum AudioPlayerState: Equatable, Sendable {
22
case initial
33
case playing
44
case paused

Sources/AudioSynchronizer.swift

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import AVFoundation
22
import AudioToolbox
33
import Combine
44

5-
final class AudioSynchronizer {
6-
typealias RateCallback = (_ time: Float) -> Void
7-
typealias TimeCallback = (_ time: CMTime) -> Void
8-
typealias DurationCallback = (_ duration: CMTime) -> Void
9-
typealias ErrorCallback = (_ error: AudioPlayerError?) -> Void
10-
typealias CompleteCallback = () -> Void
11-
typealias PlayingCallback = () -> Void
12-
typealias PausedCallback = () -> Void
13-
typealias SampleBufferCallback = (CMSampleBuffer?) -> Void
5+
final class AudioSynchronizer: Sendable {
6+
typealias RateCallback = @Sendable (_ time: Float) -> Void
7+
typealias TimeCallback = @Sendable (_ time: CMTime) -> Void
8+
typealias DurationCallback = @Sendable (_ duration: CMTime) -> Void
9+
typealias ErrorCallback = @Sendable (_ error: AudioPlayerError?) -> Void
10+
typealias CompleteCallback = @Sendable () -> Void
11+
typealias PlayingCallback = @Sendable () -> Void
12+
typealias PausedCallback = @Sendable () -> Void
13+
typealias SampleBufferCallback = @Sendable (CMSampleBuffer?) -> Void
1414

1515
private let queue = DispatchQueue(label: "audio.player.queue")
1616
private let onRateChanged: RateCallback
@@ -24,16 +24,26 @@ final class AudioSynchronizer {
2424
private let timeUpdateInterval: CMTime
2525
private let initialVolume: Float
2626

27-
private var receiveComplete = false
28-
private var audioBuffersQueue: AudioBuffersQueue?
29-
private var audioFileStream: AudioFileStream?
30-
private var audioRenderer: AVSampleBufferAudioRenderer?
31-
private var audioSynchronizer: AVSampleBufferRenderSynchronizer?
32-
private var currentSampleBufferTime: CMTime?
27+
private nonisolated(unsafe) var receiveComplete = false
28+
private nonisolated(unsafe) var audioBuffersQueue: AudioBuffersQueue?
29+
private nonisolated(unsafe) var audioFileStream: AudioFileStream?
30+
private nonisolated(unsafe) var audioRenderer: AVSampleBufferAudioRenderer?
31+
private nonisolated(unsafe) var audioSynchronizer: AVSampleBufferRenderSynchronizer?
32+
private nonisolated(unsafe) var currentSampleBufferTime: CMTime?
3333

34-
private var audioRendererErrorCancellable: AnyCancellable?
35-
private var audioRendererRateCancellable: AnyCancellable?
36-
private var audioRendererTimeCancellable: AnyCancellable?
34+
private nonisolated(unsafe) var audioRendererErrorCancellable: AnyCancellable?
35+
private nonisolated(unsafe) var audioRendererRateCancellable: AnyCancellable?
36+
private nonisolated(unsafe) var audioRendererTimeCancellable: AnyCancellable?
37+
38+
nonisolated(unsafe) var desiredRate: Float = 1.0 {
39+
didSet {
40+
if desiredRate == 0.0 {
41+
pause()
42+
} else {
43+
resume(at: desiredRate)
44+
}
45+
}
46+
}
3747

3848
var volume: Float {
3949
get { audioRenderer?.volume ?? initialVolume }
@@ -45,16 +55,6 @@ final class AudioSynchronizer {
4555
set { audioRenderer?.isMuted = newValue }
4656
}
4757

48-
var desiredRate: Float = 1.0 {
49-
didSet {
50-
if desiredRate == 0.0 {
51-
pause()
52-
} else {
53-
resume(at: desiredRate)
54-
}
55-
}
56-
}
57-
5858
init(
5959
timeUpdateInterval: CMTime,
6060
initialVolume: Float = 1.0,
@@ -145,7 +145,7 @@ final class AudioSynchronizer {
145145
receiveComplete = true
146146
}
147147

148-
func invalidate(_ completion: @escaping () -> Void = {}) {
148+
func invalidate(_ completion: @escaping @Sendable () -> Void = {}) {
149149
removeBuffers()
150150
closeFileStream()
151151
cancelObservation()
@@ -200,7 +200,7 @@ final class AudioSynchronizer {
200200
}
201201

202202
private func startRequestingMediaData(_ renderer: AVSampleBufferAudioRenderer) {
203-
var didStart = false
203+
nonisolated(unsafe) var didStart = false
204204
renderer.requestMediaDataWhenReady(on: queue) { [weak self] in
205205
guard let self, let audioRenderer, let audioBuffersQueue else { return }
206206
while let buffer = audioBuffersQueue.peek(), audioRenderer.isReadyForMoreMediaData {
@@ -215,7 +215,7 @@ final class AudioSynchronizer {
215215
}
216216

217217
private func restartRequestingMediaData(_ renderer: AVSampleBufferAudioRenderer, from time: CMTime, rate: Float) {
218-
var didStart = false
218+
nonisolated(unsafe) var didStart = false
219219
renderer.requestMediaDataWhenReady(on: queue) { [weak self] in
220220
guard let self, let audioRenderer, let audioSynchronizer, let audioBuffersQueue else { return }
221221
while let buffer = audioBuffersQueue.peek(), audioRenderer.isReadyForMoreMediaData {

Sources/Extensions/AVSampleBufferRenderSynchronizer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import AVFoundation
2-
import Combine
2+
@preconcurrency import Combine
33

44
extension AVSampleBufferRenderSynchronizer {
55
func periodicTimeObserver(interval: CMTime, queue: DispatchQueue = .main) -> AnyPublisher<CMTime, Never> {

0 commit comments

Comments
 (0)