Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e770197e6 | |||
| 6f552e60c0 | |||
| 0f2a1f7b8a | |||
| 0c2c7ba685 | |||
| 50174a7f4a | |||
| cc82e79d50 |
Vendored
BIN
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"
|
||||
|
||||
Vendored
BIN
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,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'
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,21 +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
|
||||
|
||||
var reopened = false
|
||||
let streamStatus = inputStream.streamStatus
|
||||
if streamStatus == .notOpen || streamStatus == .error {
|
||||
let streamStatus = inputStream?.streamStatus ?? .closed
|
||||
if streamStatus == .notOpen || streamStatus == .closed || streamStatus == .error || streamStatus == .atEnd {
|
||||
reopened = true
|
||||
close()
|
||||
open(inputStream: inputStream)
|
||||
try open()
|
||||
}
|
||||
|
||||
let attributes = try fileManager.attributesOfItem(atPath: url.path)
|
||||
length = (attributes[.size] as? Int) ?? 0
|
||||
guard let inputStream = inputStream else {
|
||||
return
|
||||
}
|
||||
|
||||
if inputStream.setProperty(seekOffset, forKey: .fileCurrentOffsetKey) {
|
||||
position = seekOffset
|
||||
@@ -119,10 +110,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 +140,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,13 +134,14 @@ 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)
|
||||
sourceQueue = DispatchQueue(label: "source.queue", qos: .default)
|
||||
|
||||
entryProvider = AudioEntryProvider(networkingClient: NetworkingClient(),
|
||||
underlyingQueue: sourceQueue,
|
||||
@@ -150,12 +151,14 @@ open class AudioPlayer {
|
||||
rendererContext: rendererContext,
|
||||
outputAudioFormat: outputAudioFormat.basicStreamDescription)
|
||||
|
||||
frameFilterProcessor = FrameFilterProcessor(mixerNode: audioEngine.mainMixerNode)
|
||||
|
||||
playerRenderProcessor = AudioPlayerRenderProcessor(playerContext: playerContext,
|
||||
rendererContext: rendererContext,
|
||||
outputAudioFormat: outputAudioFormat.basicStreamDescription)
|
||||
|
||||
|
||||
frameFilterProcessor = FrameFilterProcessor(mixerNodeProvider: {
|
||||
engine.mainMixerNode
|
||||
})
|
||||
configPlayerContext()
|
||||
configPlayerNode()
|
||||
setupEngine()
|
||||
@@ -645,7 +648,7 @@ 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
|
||||
@@ -670,17 +673,17 @@ open class AudioPlayer {
|
||||
|
||||
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 !isPlayingSameItemProbablySeek {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -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)" }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user