Compare commits

...

9 Commits

Author SHA1 Message Date
dimitris-c 8f0e894f86 fix a glitch sound on pause and play 2024-03-10 19:22:47 +02:00
Dimitris C decb12641d fix incorrect stopReason on finish delegate method (#66) 2024-02-29 14:49:51 +02:00
Dimitris C 4e485f924a Fixes an issue with seek on FileAudioSource (#65) 2024-02-27 19:03:57 +02:00
junyaninflection 7e770197e6 version bump to 1.1.0 (#59) 2023-08-15 17:22:17 +03:00
junyaninflection 6f552e60c0 Lazy initialize singleton mixer node (#58) 2023-08-14 19:22:24 +03:00
dimitris-c 0f2a1f7b8a Version Bump
Signed-off-by: dimitris-c <d.chatzieleftheriou@gmail.com>
2022-09-01 18:16:24 +03:00
dimitris-c 0c2c7ba685 Fixes an issue with seek functionality
Lowers the sourceQueue qos to default

Signed-off-by: dimitris-c <d.chatzieleftheriou@gmail.com>
2022-09-01 17:46:13 +03:00
dimitris-c 50174a7f4a Fix wrong next entry on audioPlayerDidStartPlaying
Signed-off-by: dimitris-c <d.chatzieleftheriou@gmail.com>
2022-08-30 12:59:02 +03:00
dimitris-c cc82e79d50 Updates UnfairLock
Signed-off-by: dimitris-c <d.chatzieleftheriou@gmail.com>
2022-08-30 01:47:26 +03:00
28 changed files with 237 additions and 200 deletions
Vendored
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
984808A028C0F549001160E6 /* hipjazz.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9848089F28C0F549001160E6 /* hipjazz.wav */; };
B5220836256051830086FB3A /* AudioPlayerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5220835256051830086FB3A /* AudioPlayerService.swift */; };
B5220948256074910086FB3A /* MulticastDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5220947256074910086FB3A /* MulticastDelegate.swift */; };
B52209502561883E0086FB3A /* EqualizerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B522094F2561883E0086FB3A /* EqualizerViewController.swift */; };
@@ -42,6 +43,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
9848089F28C0F549001160E6 /* hipjazz.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = hipjazz.wav; sourceTree = "<group>"; };
B5220835256051830086FB3A /* AudioPlayerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerService.swift; sourceTree = "<group>"; };
B5220947256074910086FB3A /* MulticastDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MulticastDelegate.swift; sourceTree = "<group>"; };
B522094F2561883E0086FB3A /* EqualizerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualizerViewController.swift; sourceTree = "<group>"; };
@@ -78,6 +80,7 @@
B524D59D2560177C00F5A88F /* Resources */ = {
isa = PBXGroup;
children = (
9848089F28C0F549001160E6 /* hipjazz.wav */,
B524D59B2560176C00F5A88F /* bensound-jazzyfrenchy.mp3 */,
B5AEDBDD2475274D007D8101 /* Assets.xcassets */,
B5AEDBDF2475274D007D8101 /* LaunchScreen.storyboard */,
@@ -211,6 +214,7 @@
B524D59C2560176C00F5A88F /* bensound-jazzyfrenchy.mp3 in Resources */,
B5AEDBE12475274D007D8101 /* LaunchScreen.storyboard in Resources */,
B5AEDBDE2475274D007D8101 /* Assets.xcassets in Resources */,
984808A028C0F549001160E6 /* hipjazz.wav in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -371,8 +375,8 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 5Y92JCRVR7;
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = AudioExample/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -381,6 +385,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioExample;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@@ -390,8 +395,8 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 5Y92JCRVR7;
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = AudioExample/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -400,6 +405,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioExample;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@@ -85,18 +85,6 @@
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
<AdditionalOptions>
<AdditionalOption
key = "MallocStackLogging"
value = ""
isEnabled = "YES">
</AdditionalOption>
<AdditionalOption
key = "PrefersMallocStackLoggingLite"
value = ""
isEnabled = "YES">
</AdditionalOption>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
Binary file not shown.
@@ -121,7 +121,8 @@ extension PlayerViewController: UITableViewDataSource {
return cell
}
cell.textLabel?.text = item.name
cell.detailTextLabel?.text = item.queues ? "Queue item" : nil
let queuedItem = item.queues ? "Queue item" : nil
cell.detailTextLabel?.text = queuedItem ?? item.subtitle
update(status: item.status, of: cell)
return cell
}
@@ -48,7 +48,7 @@ final class PlayerViewModel {
print("malformed url error")
return
}
playlistItemsService.add(item: PlaylistItem(url: url, name: urlString, status: .stopped, queues: false))
playlistItemsService.add(item: PlaylistItem(url: url, name: urlString, subtitle: nil, status: .stopped, queues: false))
reloadContent?(.all)
}
Binary file not shown.
@@ -16,8 +16,9 @@ enum AudioContent: Int, CaseIterable {
case radiox
case khruangbin
case piano
case remoteWave
case local
case podcast
case localWave
var title: String {
switch self {
@@ -35,10 +36,37 @@ enum AudioContent: Int, CaseIterable {
return "Khruangbin (mp3 preview)"
case .piano:
return "Piano (mp3)"
case .remoteWave:
return "Sample remote (wave)"
case .local:
return "Local file (mp3)"
case .podcast:
return "Swift by Sundell. Ep. 50 (mp3)"
return "Jazzy Frenchy (local mp3)"
case .localWave:
return "Local file (local wave)"
}
}
var subtitle: String? {
switch self {
case .offradio:
return nil
case .enlefko:
return nil
case .pepper966:
return nil
case .kosmos:
return nil
case .radiox:
return nil
case .khruangbin:
return nil
case .piano:
return nil
case .remoteWave:
return nil
case .local:
return "Music by: bensound.com"
case .localWave:
return "Music by: bensound.com"
}
}
@@ -49,7 +77,7 @@ enum AudioContent: Int, CaseIterable {
case .offradio:
return URL(string: "https://s3.yesstreaming.net:17062/stream")!
case .pepper966:
return URL(string: "https://ample-09.radiojar.com/pepper.m4a?1593699983=&rj-tok=AAABcw_1KyMAIViq2XpI098ZSQ&rj-ttl=5")!
return URL(string: "https://n04.radiojar.com/pepper.m4a?1662039818=&rj-tok=AAABgvlUaioALhdOXDt0mgajoA&rj-ttl=5")!
case .kosmos:
return URL(string: "https://radiostreaming.ert.gr/ert-kosmos")!
case .radiox:
@@ -61,8 +89,11 @@ enum AudioContent: Int, CaseIterable {
case .local:
let path = Bundle.main.path(forResource: "bensound-jazzyfrenchy", ofType: "mp3")!
return URL(fileURLWithPath: path)
case .podcast:
return URL(string: "https://hwcdn.libsyn.com/p/f/6/e/f6e7cb785cf0f71f/SwiftBySundell50.mp3?c_id=45232967&cs_id=45232967&expiration=1605613140&hwt=f9ff0b2f758c3286cd75322e14ef7a23")!
case .localWave:
let path = Bundle.main.path(forResource: "hipjazz", ofType: "wav")!
return URL(fileURLWithPath: path)
case .remoteWave:
return URL(string: "https://file-examples.com/storage/fe183d9197630fb5c969255/2017/11/file_example_WAV_5MG.wav")!
}
}
}
@@ -18,19 +18,22 @@ struct PlaylistItem: Equatable {
let url: URL
let name: String
let subtitle: String?
let status: Status
let queues: Bool
init(content: AudioContent, queues: Bool) {
name = content.title
subtitle = content.subtitle
url = content.streamUrl
status = .stopped
self.queues = queues
}
init(url: URL, name: String, status: Status, queues: Bool) {
init(url: URL, name: String, subtitle: String?, status: Status, queues: Bool) {
self.url = url
self.name = name
self.subtitle = subtitle
self.status = status
self.queues = queues
}
@@ -73,7 +76,13 @@ final class PlaylistItemsService {
guard let item = item(at: index) else {
return
}
items[index] = PlaylistItem(url: item.url, name: item.name, status: status, queues: item.queues)
items[index] = PlaylistItem(
url: item.url,
name: item.name,
subtitle: item.subtitle,
status: status,
queues: item.queues
)
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'AudioStreaming'
s.version = '0.9.0'
s.version = '1.1.0'
s.license = 'MIT'
s.summary = 'An AudioPlayer/Streaming library for iOS written in Swift using AVAudioEngine.'
s.homepage = 'https://github.com/dimitris-c/AudioStreaming'
+12 -12
View File
@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
98CC396E28BD651E006C9FF9 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98CC396D28BD651E006C9FF9 /* Atomic.swift */; };
B500732024D00BAC00BB4475 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500731F24D00BAC00BB4475 /* Logger.swift */; };
B514657F248E3884005C03F7 /* DispatchTimerSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514657E248E3884005C03F7 /* DispatchTimerSource.swift */; };
B51B9F9A24DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51B9F9924DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift */; };
@@ -64,8 +65,7 @@
B5EF9557247E9439003E8FF8 /* AudioStreamSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF9556247E9439003E8FF8 /* AudioStreamSource.swift */; };
B5EF955B247EBCB3003E8FF8 /* AudioFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF955A247EBCB3003E8FF8 /* AudioFileType.swift */; };
B5EF955D247ECBB1003E8FF8 /* RemoteAudioSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF955C247ECBB1003E8FF8 /* RemoteAudioSource.swift */; };
B5F883B62476DADB00D277C1 /* Protected.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F883B52476DADB00D277C1 /* Protected.swift */; };
B5F883BA2477CEFC00D277C1 /* ProtectedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F883B82477CBF600D277C1 /* ProtectedTests.swift */; };
B5F883BA2477CEFC00D277C1 /* AtomicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F883B82477CBF600D277C1 /* AtomicTests.swift */; };
B5F883C32477DC4400D277C1 /* NetworkDataStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F883C22477DC4400D277C1 /* NetworkDataStream.swift */; };
B5FB6C0525516507002C0A37 /* AudioConverter+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5FB6C0425516507002C0A37 /* AudioConverter+Helpers.swift */; };
/* End PBXBuildFile section */
@@ -94,6 +94,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
98CC396D28BD651E006C9FF9 /* Atomic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = "<group>"; };
B500731F24D00BAC00BB4475 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
B514657E248E3884005C03F7 /* DispatchTimerSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchTimerSource.swift; sourceTree = "<group>"; };
B51B9F9924DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAudioFormat+Convenience.swift"; sourceTree = "<group>"; };
@@ -158,8 +159,7 @@
B5EF9556247E9439003E8FF8 /* AudioStreamSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioStreamSource.swift; sourceTree = "<group>"; };
B5EF955A247EBCB3003E8FF8 /* AudioFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFileType.swift; sourceTree = "<group>"; };
B5EF955C247ECBB1003E8FF8 /* RemoteAudioSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteAudioSource.swift; sourceTree = "<group>"; };
B5F883B52476DADB00D277C1 /* Protected.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Protected.swift; sourceTree = "<group>"; };
B5F883B82477CBF600D277C1 /* ProtectedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtectedTests.swift; sourceTree = "<group>"; };
B5F883B82477CBF600D277C1 /* AtomicTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicTests.swift; sourceTree = "<group>"; };
B5F883C22477DC4400D277C1 /* NetworkDataStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkDataStream.swift; sourceTree = "<group>"; };
B5FB6C0425516507002C0A37 /* AudioConverter+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AudioConverter+Helpers.swift"; sourceTree = "<group>"; };
B5FFF5FD2549FA02006BBB7C /* AudioExample.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AudioExample.xctestplan; sourceTree = "<group>"; };
@@ -321,11 +321,11 @@
B592E13025460883008866FB /* Helpers */ = {
isa = PBXGroup;
children = (
98CC396D28BD651E006C9FF9 /* Atomic.swift */,
B573733F254DE43E003DFBEC /* measure.swift */,
B514657E248E3884005C03F7 /* DispatchTimerSource.swift */,
B57829CE2548B32B00C78D36 /* Lock.swift */,
B500731F24D00BAC00BB4475 /* Logger.swift */,
B5F883B52476DADB00D277C1 /* Protected.swift */,
B54C3E55255F286D00B356F2 /* Retrier.swift */,
);
path = Helpers;
@@ -443,7 +443,7 @@
isa = PBXGroup;
children = (
B5EF954A247DA450003E8FF8 /* Network */,
B5F883B82477CBF600D277C1 /* ProtectedTests.swift */,
B5F883B82477CBF600D277C1 /* AtomicTests.swift */,
B51FE0C12488F96A00F2A4D2 /* QueueTests.swift */,
B592E12825460146008866FB /* BiMapTests.swift */,
B592E133254608B4008866FB /* DispatchTimerSourceTests.swift */,
@@ -599,6 +599,7 @@
B5838640254584A50087A712 /* ProcessedPackets.swift in Sources */,
B54C3E56255F286D00B356F2 /* Retrier.swift in Sources */,
B59DF10424916FD50043C498 /* DispatchQueue+Helpers.swift in Sources */,
98CC396E28BD651E006C9FF9 /* Atomic.swift in Sources */,
B5B3B7CC248647ED00656828 /* AudioPlayerState.swift in Sources */,
B51B9F9A24DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift in Sources */,
B51FE0C624890CCB00F2A4D2 /* PlayerQueueEntries.swift in Sources */,
@@ -638,7 +639,6 @@
B5838648254584D90087A712 /* SeekRequest.swift in Sources */,
B5D82E65255DD562009EDAA4 /* NetStatusService.swift in Sources */,
B55CE97824813BCA0001C498 /* UnsafeMutablePointer+Helpers.swift in Sources */,
B5F883B62476DADB00D277C1 /* Protected.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -651,7 +651,7 @@
B51FE0C824892D1600F2A4D2 /* PlayerQueueEntriesTest.swift in Sources */,
B55CEABA248530C00001C498 /* MetadataParser.swift in Sources */,
B51FE0C22488F96A00F2A4D2 /* QueueTests.swift in Sources */,
B5F883BA2477CEFC00D277C1 /* ProtectedTests.swift in Sources */,
B5F883BA2477CEFC00D277C1 /* AtomicTests.swift in Sources */,
B592E134254608B4008866FB /* DispatchTimerSourceTests.swift in Sources */,
B55CEAB82485172D0001C498 /* HTTPHeaderParserTests.swift in Sources */,
B592E12925460146008866FB /* BiMapTests.swift in Sources */,
@@ -722,7 +722,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 0.1.0;
MARKETING_VERSION = 1.1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@@ -781,7 +781,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 0.1.0;
MARKETING_VERSION = 1.1.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
@@ -811,7 +811,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 0.9.0;
MARKETING_VERSION = 1.1.0;
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@@ -842,7 +842,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 0.9.0;
MARKETING_VERSION = 1.1.0;
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@@ -13,10 +13,10 @@ final class Atomic<Value> {
_value = value
}
var value: Value { lock.around { _value } }
var value: Value { lock.withLock { _value } }
func write(_ transform: (inout Value) -> Void) {
lock.around { transform(&self._value) }
lock.withLock { transform(&self._value) }
}
}
+21 -14
View File
@@ -8,28 +8,18 @@ import Foundation
protocol Lock {
func lock()
func unlock()
}
extension Lock {
// Execute a closure while acquiring a lock and returns the closure value
@inline(__always)
func around<Value>(_ closure: () -> Value) -> Value {
lock(); defer { unlock() }
return closure()
}
func withLock<Result>(body: () throws -> Result) rethrows -> Result
// Execute a closure while acquiring a lock
@inline(__always)
func around(_ closure: () -> Void) {
lock(); defer { unlock() }
closure()
}
func withLock(body: () -> Void)
}
/// A wrapper for `os_unfair_lock`
/// - Tag: UnfairLock
final class UnfairLock: Lock {
private let unfairLock: os_unfair_lock_t
@usableFromInline let unfairLock: UnsafeMutablePointer<os_unfair_lock>
internal init() {
unfairLock = .allocate(capacity: 1)
@@ -37,15 +27,32 @@ final class UnfairLock: Lock {
}
deinit {
unfairLock.deinitialize(count: 1)
unfairLock.deallocate()
}
@inlinable
@inline(__always)
func withLock<Result>(body: () throws -> Result) rethrows -> Result {
os_unfair_lock_lock(unfairLock)
defer { os_unfair_lock_unlock(unfairLock) }
return try body()
}
@inlinable
@inline(__always)
func withLock(body: () -> Void) {
os_unfair_lock_lock(unfairLock)
defer { os_unfair_lock_unlock(unfairLock) }
body()
}
@inlinable
@inline(__always)
internal func lock() {
os_unfair_lock_lock(unfairLock)
}
@inlinable
@inline(__always)
internal func unlock() {
os_unfair_lock_unlock(unfairLock)
@@ -1,24 +0,0 @@
//
// Created by Dimitrios Chatzieleftheriou on 21/05/2020.
// Copyright © 2020 Decimal. All rights reserved.
//
internal final class Protected<Value> {
var value: Value { lock.around { _value } }
private let lock = UnfairLock()
private var _value: Value
init(_ value: Value) {
_value = value
}
func read<Element>(_ closure: (Value) -> Element) -> Element {
lock.around { closure(self._value) }
}
@discardableResult
func write<Element>(_ closure: (inout Value) -> Element) -> Element {
lock.around { closure(&self._value) }
}
}
@@ -77,9 +77,10 @@ internal final class NetworkingClient {
}
internal func remove(task: NetworkDataStream) {
tasksLock.lock(); defer { tasksLock.unlock() }
if !tasks.isEmpty {
tasks[task] = nil
tasksLock.withLock {
if !tasks.isEmpty {
tasks[task] = nil
}
}
}
@@ -100,13 +101,15 @@ internal final class NetworkingClient {
extension NetworkingClient: StreamTaskProvider {
internal func dataStream(for request: URLSessionTask) -> NetworkDataStream? {
tasksLock.lock(); defer { tasksLock.unlock() }
return tasks[request] ?? nil
tasksLock.withLock {
tasks[request] ?? nil
}
}
internal func sessionTask(for stream: NetworkDataStream) -> URLSessionTask? {
tasksLock.lock(); defer { tasksLock.unlock() }
return tasks[stream] ?? nil
tasksLock.withLock {
tasks[stream] ?? nil
}
}
}
@@ -22,9 +22,7 @@ internal class AudioEntry {
let id: AudioEntryId
/// The sample rate from the `audioStreamFormat`
var sampleRate: Float {
Float(audioStreamFormat.mSampleRate)
}
var sampleRate: Float
var audioFileHint: AudioFileTypeID {
source.audioFileHint
@@ -49,9 +47,7 @@ internal class AudioEntry {
private(set) var framesState: EntryFramesState
private(set) var processedPacketsState: ProcessedPacketsState
var packetDuration: Double {
return Double(audioStreamFormat.mFramesPerPacket) / Double(sampleRate)
}
var packetDuration: Double
private var averagePacketByteSize: Double {
let packets = processedPacketsState
@@ -72,6 +68,8 @@ internal class AudioEntry {
processedPacketsState = ProcessedPacketsState()
framesState = EntryFramesState()
audioStreamState = AudioStreamState()
sampleRate = 0
packetDuration = 0
}
func close() {
@@ -8,6 +8,6 @@ import Foundation
final class SeekRequest {
let lock = UnfairLock()
var requested: Bool = false
var version = Protected<Int>(0)
var version = Atomic<Int>(0)
var time: Double = 0
}
@@ -54,12 +54,8 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
inputStream.delegate = nil
}
func suspend() {
guard let inputStream = inputStream else {
return
}
CFReadStreamSetDispatchQueue(inputStream, nil)
}
// no-op
func suspend() { }
func resume() {
guard let inputStream = inputStream else {
@@ -69,8 +65,6 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
}
func seek(at offset: Int) {
close()
do {
try performOpen(seek: offset)
} catch {
@@ -79,32 +73,18 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
}
private func performOpen(seek seekOffset: Int) throws {
guard let inputStream = InputStream(url: url) else {
throw AudioSystemError.playerStartError
}
self.inputStream = inputStream
close()
try open()
var reopened = false
let streamStatus = inputStream.streamStatus
if streamStatus == .notOpen || streamStatus == .error {
reopened = true
close()
open(inputStream: inputStream)
guard let inputStream = inputStream else {
return
}
let attributes = try fileManager.attributesOfItem(atPath: url.path)
length = (attributes[.size] as? Int) ?? 0
if inputStream.setProperty(seekOffset, forKey: .fileCurrentOffsetKey) {
position = seekOffset
} else {
position = 0
}
if !reopened {
if inputStream.hasBytesAvailable {
dataAvailable()
}
}
}
private func dataAvailable() {
@@ -119,10 +99,17 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
}
}
private func open(inputStream: InputStream) {
private func open() throws {
guard let inputStream = InputStream(url: url) else {
throw AudioSystemError.playerStartError
}
self.inputStream = inputStream
CFReadStreamSetDispatchQueue(inputStream, underlyingQueue)
inputStream.delegate = self
inputStream.open()
let attributes = try fileManager.attributesOfItem(atPath: url.path)
length = (attributes[.size] as? Int) ?? 0
}
private func getCurrentOffsetFromStream() -> Int {
@@ -142,8 +129,6 @@ extension FileAudioSource: StreamDelegate {
delegate?.endOfFileOccurred(source: self)
case .errorOccurred:
delegate?.errorOccurred(source: self, error: AudioPlayerError.codecError)
case .endEncountered:
delegate?.endOfFileOccurred(source: self)
default:
break
}
@@ -108,7 +108,7 @@ open class AudioPlayer {
private var stateBeforePaused: InternalState = .initial
/// The underlying `AVAudioEngine` object
private let audioEngine = AVAudioEngine()
private let audioEngine: AVAudioEngine
/// An `AVAudioUnit` object that represents the audio player
private(set) var player = AVAudioUnit()
/// An `AVAudioUnitTimePitch` that controls the playback rate of the audio engine
@@ -134,28 +134,36 @@ open class AudioPlayer {
public init(configuration: AudioPlayerConfiguration = .default) {
self.configuration = configuration.normalizeValues()
let engine = AVAudioEngine()
self.audioEngine = engine
rendererContext = AudioRendererContext(configuration: configuration, outputAudioFormat: outputAudioFormat)
playerContext = AudioPlayerContext()
entriesQueue = PlayerQueueEntries()
serializationQueue = DispatchQueue(label: "streaming.core.queue", qos: .userInitiated)
sourceQueue = DispatchQueue(label: "source.queue", qos: .userInitiated)
entryProvider = AudioEntryProvider(networkingClient: NetworkingClient(),
underlyingQueue: sourceQueue,
outputAudioFormat: outputAudioFormat)
fileStreamProcessor = AudioFileStreamProcessor(playerContext: playerContext,
rendererContext: rendererContext,
outputAudioFormat: outputAudioFormat.basicStreamDescription)
frameFilterProcessor = FrameFilterProcessor(mixerNode: audioEngine.mainMixerNode)
playerRenderProcessor = AudioPlayerRenderProcessor(playerContext: playerContext,
rendererContext: rendererContext,
outputAudioFormat: outputAudioFormat.basicStreamDescription)
sourceQueue = DispatchQueue(label: "source.queue", qos: .default)
entryProvider = AudioEntryProvider(
networkingClient: NetworkingClient(),
underlyingQueue: sourceQueue,
outputAudioFormat: outputAudioFormat
)
fileStreamProcessor = AudioFileStreamProcessor(
playerContext: playerContext,
rendererContext: rendererContext,
outputAudioFormat: outputAudioFormat.basicStreamDescription)
playerRenderProcessor = AudioPlayerRenderProcessor(
playerContext: playerContext,
rendererContext: rendererContext,
outputAudioFormat: outputAudioFormat.basicStreamDescription)
frameFilterProcessor = FrameFilterProcessor(
mixerNodeProvider: {
engine.mainMixerNode
}
)
configPlayerContext()
configPlayerNode()
setupEngine()
@@ -506,6 +514,7 @@ open class AudioPlayer {
/// Pauses the audio engine and stops the player's hardware
private func pauseEngine() {
guard isEngineRunning else { return }
audioEngine.reset()
audioEngine.pause()
player.auAudioUnit.stopHardware()
Logger.debug("engine paused ⏸", category: .generic)
@@ -645,44 +654,41 @@ open class AudioPlayer {
}
private func processFinishPlaying(entry: AudioEntry?, with nextEntry: AudioEntry?) {
let playingEntry = playerContext.entriesLock.around { playerContext.audioPlayingEntry }
let playingEntry = playerContext.entriesLock.withLock { playerContext.audioPlayingEntry }
guard entry == playingEntry else { return }
let isPlayingSameItemProbablySeek = playerContext.audioPlayingEntry === nextEntry
let notifyDelegateEntryFinishedPlaying: (AudioEntry?, Bool) -> Void = { [weak self] entry, _ in
guard let self = self else { return }
if let entry = entry, !isPlayingSameItemProbablySeek {
let entryId = entry.id
let progressInFrames = entry.progressInFrames()
let progress = Double(progressInFrames) / self.outputAudioFormat.basicStreamDescription.mSampleRate
let duration = entry.duration()
asyncOnMain {
self.delegate?.audioPlayerDidFinishPlaying(player: self,
entryId: entryId,
stopReason: self.stopReason,
progress: progress,
duration: duration)
}
}
}
if let nextEntry = nextEntry {
if !isPlayingSameItemProbablySeek {
nextEntry.lock.around {
nextEntry.lock.withLock {
nextEntry.seekTime = 0
}
nextEntry.seekRequest.lock.around {
nextEntry.seekRequest.lock.withLock {
nextEntry.seekRequest.requested = false
}
}
playerContext.entriesLock.lock()
playerContext.audioPlayingEntry = nextEntry
let playingQueueEntryId = playerContext.audioPlayingEntry?.id ?? AudioEntryId(id: "")
playerContext.entriesLock.unlock()
let playingQueueEntryId = playingEntry?.id ?? AudioEntryId(id: "")
notifyDelegateEntryFinishedPlaying(entry, isPlayingSameItemProbablySeek)
if let entry = entry, !isPlayingSameItemProbablySeek {
let entryId = entry.id
let progressInFrames = entry.progressInFrames()
let progress = Double(progressInFrames) / self.outputAudioFormat.basicStreamDescription.mSampleRate
let duration = entry.duration()
asyncOnMain { [weak self] in
guard let self else { return }
self.delegate?.audioPlayerDidFinishPlaying(
player: self,
entryId: entryId,
stopReason: self.stopReason,
progress: progress,
duration: duration
)
}
}
if !isPlayingSameItemProbablySeek {
playerContext.setInternalState(to: .waitingForData)
@@ -692,10 +698,29 @@ open class AudioPlayer {
}
}
} else {
notifyDelegateEntryFinishedPlaying(entry, isPlayingSameItemProbablySeek)
playerContext.entriesLock.lock()
playerContext.audioPlayingEntry = nil
playerContext.entriesLock.unlock()
if let entry = entry, !isPlayingSameItemProbablySeek {
let entryId = entry.id
let progressInFrames = entry.progressInFrames()
let progress = Double(progressInFrames) / self.outputAudioFormat.basicStreamDescription.mSampleRate
let duration = entry.duration()
sourceQueue.async { [weak self] in
guard let self else { return }
self.processSource()
asyncOnMain {
self.delegate?.audioPlayerDidFinishPlaying(
player: self,
entryId: entryId,
stopReason: self.stopReason,
progress: progress,
duration: duration
)
}
}
}
}
sourceQueue.async { [weak self] in
self?.processSource()
@@ -6,12 +6,12 @@
import Foundation
internal final class AudioPlayerContext {
var stopReason: Protected<AudioPlayerStopReason>
var stopReason: Atomic<AudioPlayerStopReason>
var state: Protected<AudioPlayerState>
var state: Atomic<AudioPlayerState>
var stateChanged: ((_ oldState: AudioPlayerState, _ newState: AudioPlayerState) -> Void)?
var muted: Protected<Bool>
var muted: Atomic<Bool>
var internalState: AudioPlayer.InternalState {
playerInternalState.value
@@ -24,12 +24,12 @@ internal final class AudioPlayerContext {
/// This is the player's internal state to use
/// - NOTE: Do not use directly instead use the `internalState` to set and get the property
/// or the `setInternalState(to:when:)`method
private var playerInternalState = Protected<AudioPlayer.InternalState>(.initial)
private var playerInternalState = Atomic<AudioPlayer.InternalState>(.initial)
init() {
stopReason = Protected<AudioPlayerStopReason>(.none)
state = Protected<AudioPlayerState>(.ready)
muted = Protected<Bool>(false)
stopReason = Atomic<AudioPlayerStopReason>(.none)
state = Atomic<AudioPlayerState>(.ready)
muted = Atomic<Bool>(false)
entriesLock = UnfairLock()
}
@@ -42,7 +42,9 @@ internal final class AudioPlayerContext {
when inState: ((AudioPlayer.InternalState) -> Bool)? = nil)
{
let newValues = playerStateAndStopReason(for: state)
stopReason.write { $0 = newValues.stopReason }
if let stopReason = newValues.stopReason {
self.stopReason.write { $0 = stopReason }
}
guard state != internalState else { return }
if let inState = inState, !inState(internalState) {
return
@@ -30,26 +30,27 @@ extension AudioPlayer {
/// Helper method that returns `AudioPlayerState` and `StopReason` based on the given `InternalState`
/// - Parameter internalState: A value of `InternalState`
/// - Returns: A tuple of `(AudioPlayerState, AudioPlayerStopReason)`
func playerStateAndStopReason(for internalState: AudioPlayer.InternalState) -> (state: AudioPlayerState,
stopReason: AudioPlayerStopReason)
func playerStateAndStopReason(
for internalState: AudioPlayer.InternalState
) -> (state: AudioPlayerState, stopReason: AudioPlayerStopReason?)
{
switch internalState {
case .initial:
return (.ready, .none)
return (.ready, AudioPlayerStopReason.none)
case .running, .playing, .waitingForDataAfterSeek:
return (.playing, .none)
return (.playing, AudioPlayerStopReason.none)
case .pendingNext, .rebuffering, .waitingForData:
return (.bufferring, .none)
return (.bufferring, AudioPlayerStopReason.none)
case .stopped:
return (.stopped, .userAction)
return (.stopped, nil)
case .paused:
return (.paused, .none)
return (.paused, AudioPlayerStopReason.none)
case .disposed:
return (.disposed, .userAction)
case .error:
return (.error, .error)
return (.error, AudioPlayerStopReason.error)
default:
return (.ready, .none)
return (.ready, AudioPlayerStopReason.none)
}
}
@@ -9,7 +9,7 @@ import CoreAudio
internal var maxFramesPerSlice: AVAudioFrameCount = 8192
final class AudioRendererContext {
var waiting = Protected<Bool>(false)
var waiting = Atomic<Bool>(false)
let lock = UnfairLock()
@@ -24,7 +24,7 @@ final class AudioRendererContext {
let framesRequiredAfterRebuffering: UInt32
let framesRequiredForDataAfterSeekPlaying: UInt32
var waitingForDataAfterSeekFrameCount = Protected<Int32>(0)
var waitingForDataAfterSeekFrameCount = Atomic<Int32>(0)
private let configuration: AudioPlayerConfiguration
@@ -116,7 +116,7 @@ final class AudioFileStreamProcessor {
readingEntry.lock.unlock()
let bitrate = readingEntry.calculatedBitrate()
if readingEntry.processedPacketsState.count > 0, bitrate > 0 {
if readingEntry.packetDuration > 0, bitrate > 0 {
var ioFlags = AudioFileStreamSeekFlags(rawValue: 0)
var packetsAlignedByteOffset: Int64 = 0
let seekPacket = Int64(floor(readingEntry.seekRequest.time / readingEntry.packetDuration))
@@ -279,6 +279,9 @@ final class AudioFileStreamProcessor {
entry.audioStreamFormat = audioStreamFormat
}
entry.sampleRate = Float(audioStreamFormat.mSampleRate)
entry.packetDuration = Double(audioStreamFormat.mFramesPerPacket) / Double(entry.sampleRate)
var packetBufferSize: UInt32 = 0
var status = fileStreamGetProperty(value: &packetBufferSize,
fileStream: fileStream,
@@ -291,7 +294,7 @@ final class AudioFileStreamProcessor {
packetBufferSize = 2048 // default value
}
}
entry.lock.around {
entry.lock.withLock {
entry.processedPacketsState.bufferSize = packetBufferSize
}
@@ -198,7 +198,6 @@ final class AudioPlayerRenderProcessor: NSObject {
state.contains(.running) && state != .playing
}
}
rendererContext.waitingForDataAfterSeekFrameCount.write { $0 = 0 }
}
} else {
rendererContext.waitingForDataAfterSeekFrameCount.write { $0 = 0 }
@@ -78,14 +78,17 @@ final class FrameFilterProcessor: NSObject, FrameFiltering {
}
private let lock = UnfairLock()
private let mixerNode: AVAudioMixerNode
private let mixerNodeProvider: (() -> AVAudioMixerNode)
private lazy var mixerNode: AVAudioMixerNode = {
return mixerNodeProvider()
}()
private(set) var entries: [FilterEntry] = []
private var hasInstalledTap: Bool = false
init(mixerNode: AVAudioMixerNode) {
self.mixerNode = mixerNode
init(mixerNodeProvider: @escaping (() -> AVAudioMixerNode)) {
self.mixerNodeProvider = mixerNodeProvider
}
public func add(entry: FilterEntry) {
@@ -17,14 +17,14 @@ final class PlayerQueueEntries {
/// Returns `true` when both underlying entries are empty
var isEmpty: Bool {
lock.around {
lock.withLock {
bufferring.isEmpty && upcoming.isEmpty
}
}
/// Returns the count of both underlying entries
var count: Int {
lock.around {
lock.withLock {
bufferring.count + upcoming.count
}
}
@@ -7,27 +7,27 @@ import XCTest
@testable import AudioStreaming
class ProtectedTests: XCTestCase {
class AtomicTests: XCTestCase {
func testProtectedValuesAreAccessedSafely() {
measure {
let protected = Protected<Int>(0)
let atomic = Atomic<Int>(0)
DispatchQueue.concurrentPerform(iterations: 1_000_000) { _ in
_ = protected.value
protected.write { $0 += 1 }
DispatchQueue.concurrentPerform(iterations: 100000) { _ in
_ = atomic.value
atomic.write { $0 += 1 }
}
XCTAssertEqual(protected.value, 1_000_000)
XCTAssertEqual(atomic.value, 100000)
}
}
func testThatProtectedReadAndWriteAreSafe() {
measure {
let initialValue = "aValue"
let protected = Protected<String>(initialValue)
let protected = Atomic<String>(initialValue)
DispatchQueue.concurrentPerform(iterations: 1000) { i in
_ = protected.read { $0 }
_ = protected.value
protected.write { $0 = "\(i)" }
}