From 5feee800fa2ddfcd6ec517165c8f35fa8c5cbadc Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Sat, 30 Mar 2024 11:37:26 -0700 Subject: [PATCH 01/17] Added initial macOS support. --- Package.swift | 2 +- Sources/SwiftAudioEx/AudioItem.swift | 27 ++++++++++++------- .../AudioSessionController/AudioSession.swift | 3 ++- .../AudioSessionController.swift | 4 +++ 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/Package.swift b/Package.swift index 408ec47..fc6448d 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription let package = Package( name: "SwiftAudioEx", - platforms: [.iOS(.v11)], + platforms: [.iOS(.v11), .macOS(.v11)], products: [ .library( name: "SwiftAudioEx", diff --git a/Sources/SwiftAudioEx/AudioItem.swift b/Sources/SwiftAudioEx/AudioItem.swift index 4b70dbd..833afe3 100755 --- a/Sources/SwiftAudioEx/AudioItem.swift +++ b/Sources/SwiftAudioEx/AudioItem.swift @@ -7,7 +7,14 @@ import Foundation import AVFoundation + +#if os(iOS) import UIKit +public typealias AudioItemImage = UIImage +#elseif os(macOS) +import AppKit +public typealias AudioItemImage = NSImage +#endif public enum SourceType { case stream @@ -21,7 +28,7 @@ public protocol AudioItem { func getTitle() -> String? func getAlbumTitle() -> String? func getSourceType() -> SourceType - func getArtwork(_ handler: @escaping (UIImage?) -> Void) + func getArtwork(_ handler: @escaping (AudioItemImage?) -> Void) } @@ -54,9 +61,9 @@ public class DefaultAudioItem: AudioItem { public var sourceType: SourceType - public var artwork: UIImage? + public var artwork: AudioItemImage? - public init(audioUrl: String, artist: String? = nil, title: String? = nil, albumTitle: String? = nil, sourceType: SourceType, artwork: UIImage? = nil) { + public init(audioUrl: String, artist: String? = nil, title: String? = nil, albumTitle: String? = nil, sourceType: SourceType, artwork: AudioItemImage? = nil) { self.audioUrl = audioUrl self.artist = artist self.title = title @@ -85,7 +92,7 @@ public class DefaultAudioItem: AudioItem { sourceType } - public func getArtwork(_ handler: @escaping (UIImage?) -> Void) { + public func getArtwork(_ handler: @escaping (AudioItemImage?) -> Void) { handler(artwork) } @@ -96,12 +103,12 @@ public class DefaultAudioItemTimePitching: DefaultAudioItem, TimePitching { public var pitchAlgorithmType: AVAudioTimePitchAlgorithm - public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) { + public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: AudioItemImage?) { pitchAlgorithmType = AVAudioTimePitchAlgorithm.timeDomain super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork) } - public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm) { + public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: AudioItemImage?, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm) { pitchAlgorithmType = audioTimePitchAlgorithm super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork) } @@ -116,12 +123,12 @@ public class DefaultAudioItemInitialTime: DefaultAudioItem, InitialTiming { public var initialTime: TimeInterval - public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) { + public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: AudioItemImage?) { initialTime = 0.0 super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork) } - public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, initialTime: TimeInterval) { + public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: AudioItemImage?, initialTime: TimeInterval) { self.initialTime = initialTime super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork) } @@ -137,12 +144,12 @@ public class DefaultAudioItemAssetOptionsProviding: DefaultAudioItem, AssetOptio public var options: [String: Any] - public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) { + public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: AudioItemImage?) { options = [:] super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork) } - public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, options: [String: Any]) { + public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: AudioItemImage?, options: [String: Any]) { self.options = options super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork) } diff --git a/Sources/SwiftAudioEx/AudioSessionController/AudioSession.swift b/Sources/SwiftAudioEx/AudioSessionController/AudioSession.swift index 6cdcefe..e1640c0 100644 --- a/Sources/SwiftAudioEx/AudioSessionController/AudioSession.swift +++ b/Sources/SwiftAudioEx/AudioSessionController/AudioSession.swift @@ -8,7 +8,7 @@ import Foundation import AVFoundation - +#if os(iOS) protocol AudioSession { var isOtherAudioPlaying: Bool { get } @@ -30,3 +30,4 @@ protocol AudioSession { } extension AVAudioSession: AudioSession {} +#endif diff --git a/Sources/SwiftAudioEx/AudioSessionController/AudioSessionController.swift b/Sources/SwiftAudioEx/AudioSessionController/AudioSessionController.swift index 02b1bf6..c13d6b0 100644 --- a/Sources/SwiftAudioEx/AudioSessionController/AudioSessionController.swift +++ b/Sources/SwiftAudioEx/AudioSessionController/AudioSessionController.swift @@ -13,6 +13,8 @@ public enum InterruptionType: Equatable { case ended(shouldResume: Bool) } +#if os(iOS) + public protocol AudioSessionControllerDelegate: AnyObject { func handleInterruption(type: InterruptionType) } @@ -129,3 +131,5 @@ public class AudioSessionController { } } + +#endif From d65e3eaedbe7471639654f431a22907a1a7f6c2c Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Sun, 31 Mar 2024 15:28:23 -0700 Subject: [PATCH 02/17] Added macOS sample app --- .../SwiftAudio.xcodeproj/project.pbxproj | 51 ++- .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcschemes/SwiftAudio-Example.xcscheme | 2 +- .../SwiftAudio}/SwiftAudio/AppDelegate.swift | 0 .../SwiftAudio/AudioController.swift | 0 .../SwiftAudio/Base.lproj/LaunchScreen.xib | 0 .../SwiftAudio/Base.lproj/Main.storyboard | 0 .../SwiftAudio/Double + Extensions.swift | 0 .../22AMI.imageset/22AMillion.jpg | Bin .../22AMI.imageset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../SwiftAudio/Images.xcassets/Contents.json | 0 .../cover.imageset/Contents.json | 0 .../Images.xcassets/cover.imageset/cover.jpg | Bin .../SwiftAudio}/SwiftAudio/Info.plist | 0 .../SwiftAudio/QueueTableViewCell.swift | 0 .../SwiftAudio/QueueTableViewCell.xib | 0 .../SwiftAudio/QueueViewController.swift | 0 .../SwiftAudio/ViewController.swift | 0 .../SwiftAudio.xcodeproj/project.pbxproj | 394 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../22AMI.imageset/22AMillion.jpg | Bin 0 -> 65640 bytes .../22AMI.imageset/Contents.json | 21 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 58 +++ .../SwiftAudio/Assets.xcassets/Contents.json | 6 + .../cover.imageset/Contents.json | 21 + .../Assets.xcassets/cover.imageset/cover.jpg | Bin 0 -> 28688 bytes .../SwiftAudio/AudioController.swift | 46 ++ .../SwiftAudio/SwiftAudio/Extensions.swift | 22 + .../SwiftAudio/PlayerListener.swift | 92 ++++ .../SwiftAudio/SwiftAudio/PlayerState.swift | 25 ++ .../SwiftAudio/SwiftAudio/PlayerView.swift | 88 ++++ .../Preview Assets.xcassets/Contents.json | 6 + .../SwiftAudio/SwiftAudio.entitlements | 12 + .../SwiftAudio/SwiftAudio/SwiftAudioApp.swift | 26 ++ 38 files changed, 878 insertions(+), 18 deletions(-) rename Example/{ => iOS/SwiftAudio}/SwiftAudio.xcodeproj/project.pbxproj (92%) rename Example/{ => iOS/SwiftAudio}/SwiftAudio.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) rename Example/{ => iOS/SwiftAudio}/SwiftAudio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename Example/{ => iOS/SwiftAudio}/SwiftAudio.xcodeproj/xcshareddata/xcschemes/SwiftAudio-Example.xcscheme (99%) rename Example/{ => iOS/SwiftAudio}/SwiftAudio/AppDelegate.swift (100%) rename Example/{ => iOS/SwiftAudio}/SwiftAudio/AudioController.swift (100%) rename Example/{ => iOS/SwiftAudio}/SwiftAudio/Base.lproj/LaunchScreen.xib (100%) rename Example/{ => iOS/SwiftAudio}/SwiftAudio/Base.lproj/Main.storyboard (100%) rename Example/{ => iOS/SwiftAudio}/SwiftAudio/Double + Extensions.swift (100%) rename Example/{ => iOS/SwiftAudio}/SwiftAudio/Images.xcassets/22AMI.imageset/22AMillion.jpg (100%) rename Example/{ => iOS/SwiftAudio}/SwiftAudio/Images.xcassets/22AMI.imageset/Contents.json (100%) rename Example/{ => iOS/SwiftAudio}/SwiftAudio/Images.xcassets/AppIcon.appiconset/Contents.json (100%) rename Example/{ => iOS/SwiftAudio}/SwiftAudio/Images.xcassets/Contents.json (100%) rename Example/{ => iOS/SwiftAudio}/SwiftAudio/Images.xcassets/cover.imageset/Contents.json (100%) rename Example/{ => iOS/SwiftAudio}/SwiftAudio/Images.xcassets/cover.imageset/cover.jpg (100%) rename Example/{ => iOS/SwiftAudio}/SwiftAudio/Info.plist (100%) rename Example/{ => iOS/SwiftAudio}/SwiftAudio/QueueTableViewCell.swift (100%) rename Example/{ => iOS/SwiftAudio}/SwiftAudio/QueueTableViewCell.xib (100%) rename Example/{ => iOS/SwiftAudio}/SwiftAudio/QueueViewController.swift (100%) rename Example/{ => iOS/SwiftAudio}/SwiftAudio/ViewController.swift (100%) create mode 100644 Example/macOS/SwiftAudio/SwiftAudio.xcodeproj/project.pbxproj create mode 100644 Example/macOS/SwiftAudio/SwiftAudio.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Example/macOS/SwiftAudio/SwiftAudio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Example/macOS/SwiftAudio/SwiftAudio/Assets.xcassets/22AMI.imageset/22AMillion.jpg create mode 100644 Example/macOS/SwiftAudio/SwiftAudio/Assets.xcassets/22AMI.imageset/Contents.json create mode 100644 Example/macOS/SwiftAudio/SwiftAudio/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Example/macOS/SwiftAudio/SwiftAudio/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Example/macOS/SwiftAudio/SwiftAudio/Assets.xcassets/Contents.json create mode 100644 Example/macOS/SwiftAudio/SwiftAudio/Assets.xcassets/cover.imageset/Contents.json create mode 100644 Example/macOS/SwiftAudio/SwiftAudio/Assets.xcassets/cover.imageset/cover.jpg create mode 100644 Example/macOS/SwiftAudio/SwiftAudio/AudioController.swift create mode 100644 Example/macOS/SwiftAudio/SwiftAudio/Extensions.swift create mode 100644 Example/macOS/SwiftAudio/SwiftAudio/PlayerListener.swift create mode 100644 Example/macOS/SwiftAudio/SwiftAudio/PlayerState.swift create mode 100644 Example/macOS/SwiftAudio/SwiftAudio/PlayerView.swift create mode 100644 Example/macOS/SwiftAudio/SwiftAudio/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 Example/macOS/SwiftAudio/SwiftAudio/SwiftAudio.entitlements create mode 100644 Example/macOS/SwiftAudio/SwiftAudio/SwiftAudioApp.swift diff --git a/Example/SwiftAudio.xcodeproj/project.pbxproj b/Example/iOS/SwiftAudio/SwiftAudio.xcodeproj/project.pbxproj similarity index 92% rename from Example/SwiftAudio.xcodeproj/project.pbxproj rename to Example/iOS/SwiftAudio/SwiftAudio.xcodeproj/project.pbxproj index 98200b8..0e4642c 100644 --- a/Example/SwiftAudio.xcodeproj/project.pbxproj +++ b/Example/iOS/SwiftAudio/SwiftAudio.xcodeproj/project.pbxproj @@ -12,12 +12,12 @@ 0707130B2067F2E000F789B3 /* QueueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0707130A2067F2E000F789B3 /* QueueViewController.swift */; }; 0707130F2067F40A00F789B3 /* QueueTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0707130D2067F40A00F789B3 /* QueueTableViewCell.swift */; }; 070713102067F40A00F789B3 /* QueueTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0707130E2067F40A00F789B3 /* QueueTableViewCell.xib */; }; + 461B275E2BB89747004E6744 /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 461B275D2BB89747004E6744 /* SwiftAudioEx */; }; 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; }; 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; }; 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; - 9B1D5E2027C76F6F004CA883 /* SwiftAudioEx in Frameworks */ = {isa = PBXBuildFile; productRef = 9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -26,6 +26,7 @@ 0707130A2067F2E000F789B3 /* QueueViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueViewController.swift; sourceTree = ""; }; 0707130D2067F40A00F789B3 /* QueueTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueTableViewCell.swift; sourceTree = ""; }; 0707130E2067F40A00F789B3 /* QueueTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QueueTableViewCell.xib; sourceTree = ""; }; + 461B27592BB89735004E6744 /* SwiftAudioEx */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SwiftAudioEx; path = ../../..; sourceTree = ""; }; 607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftAudio_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -33,7 +34,6 @@ 607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; - 9B1D5E1C27C76F49004CA883 /* SwiftAudioEx */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SwiftAudioEx; path = ..; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -41,19 +41,27 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9B1D5E2027C76F6F004CA883 /* SwiftAudioEx in Frameworks */, + 461B275E2BB89747004E6744 /* SwiftAudioEx in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 461B275C2BB89747004E6744 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; 607FACC71AFB9204008FA782 = { isa = PBXGroup; children = ( + 461B27592BB89735004E6744 /* SwiftAudioEx */, 607FACD21AFB9204008FA782 /* Example for SwiftAudio */, 607FACD11AFB9204008FA782 /* Products */, - 9B05AA2F2660276400C7A389 /* Frameworks */, + 461B275C2BB89747004E6744 /* Frameworks */, ); sourceTree = ""; }; @@ -92,14 +100,6 @@ name = "Supporting Files"; sourceTree = ""; }; - 9B05AA2F2660276400C7A389 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 9B1D5E1C27C76F49004CA883 /* SwiftAudioEx */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -114,10 +114,11 @@ buildRules = ( ); dependencies = ( + 461B275B2BB89740004E6744 /* PBXTargetDependency */, ); name = SwiftAudio_Example; packageProductDependencies = ( - 9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */, + 461B275D2BB89747004E6744 /* SwiftAudioEx */, ); productName = SwiftAudio; productReference = 607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */; @@ -129,8 +130,9 @@ 607FACC81AFB9204008FA782 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0830; - LastUpgradeCheck = 1010; + LastUpgradeCheck = 1530; ORGANIZATIONNAME = CocoaPods; TargetAttributes = { 607FACCF1AFB9204008FA782 = { @@ -194,6 +196,13 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 461B275B2BB89740004E6744 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = 461B275A2BB89740004E6744 /* SwiftAudioEx */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 607FACD91AFB9204008FA782 /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -236,6 +245,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -246,6 +256,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -261,7 +272,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -291,6 +302,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -301,6 +313,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -309,7 +322,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; @@ -380,7 +393,11 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - 9B1D5E1F27C76F6F004CA883 /* SwiftAudioEx */ = { + 461B275A2BB89740004E6744 /* SwiftAudioEx */ = { + isa = XCSwiftPackageProductDependency; + productName = SwiftAudioEx; + }; + 461B275D2BB89747004E6744 /* SwiftAudioEx */ = { isa = XCSwiftPackageProductDependency; productName = SwiftAudioEx; }; diff --git a/Example/SwiftAudio.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Example/iOS/SwiftAudio/SwiftAudio.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Example/SwiftAudio.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Example/iOS/SwiftAudio/SwiftAudio.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Example/SwiftAudio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example/iOS/SwiftAudio/SwiftAudio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Example/SwiftAudio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Example/iOS/SwiftAudio/SwiftAudio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Example/SwiftAudio.xcodeproj/xcshareddata/xcschemes/SwiftAudio-Example.xcscheme b/Example/iOS/SwiftAudio/SwiftAudio.xcodeproj/xcshareddata/xcschemes/SwiftAudio-Example.xcscheme similarity index 99% rename from Example/SwiftAudio.xcodeproj/xcshareddata/xcschemes/SwiftAudio-Example.xcscheme rename to Example/iOS/SwiftAudio/SwiftAudio.xcodeproj/xcshareddata/xcschemes/SwiftAudio-Example.xcscheme index b7c8596..74884e0 100644 --- a/Example/SwiftAudio.xcodeproj/xcshareddata/xcschemes/SwiftAudio-Example.xcscheme +++ b/Example/iOS/SwiftAudio/SwiftAudio.xcodeproj/xcshareddata/xcschemes/SwiftAudio-Example.xcscheme @@ -1,6 +1,6 @@ + + + + diff --git a/Example/macOS/SwiftAudio/SwiftAudio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example/macOS/SwiftAudio/SwiftAudio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Example/macOS/SwiftAudio/SwiftAudio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Example/macOS/SwiftAudio/SwiftAudio/Assets.xcassets/22AMI.imageset/22AMillion.jpg b/Example/macOS/SwiftAudio/SwiftAudio/Assets.xcassets/22AMI.imageset/22AMillion.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4f5c56fdb8ea9a654c6e978417e779d8a8e445e0 GIT binary patch literal 65640 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 z zF+P#aP|0f%(ty8xOlM3Eve$SD8k(v*0Y%BE=blH-4vY!v{UB+4+L1p2hN(@%5=!>= z_{+4H2kqHDlIbB{QJ57kk-y{rB+#hJyt*NnZ3bRczgAu~zXw^`s&tV7H8yzN0K!pF z(~xC)y;2h8`pe@RKq!j2S5bd#f7gpax2_^nauHmASgmLX%dpRX8ANGcoRuBTL&BXM z4fwa#m0flS5%<5lN(OvP$13?AyA{rJ@cg))Vzq&}wCcnN0=~Qq2 zes@(#j;XO165bw_hZ=of7uGIVL?O!(?Fa3}&HFBiTV8Skhm){nCA-zsok@;E7m8NU zK6B|+Q}O%k=!@?Uc!nYOxTjg}88xXLtrzo^XGintux3Yd127AyBR_Q_fW195x|c~A zjPz1Iwb^i-Ze%-D)|_}~yp8oraEPJzdJGCXK!Y-T1*q4pzBbB7j|3uF(w z7w)p0e!3t!bfG+l>K!t{KNgyNQzFx*T7GarzU2pV#7dazGSWG-n%_s7uld$qAtPxH zqnZT!Tt3|Z*hjC-4=uTCW__K7`Hsc41*yRGP3Zg+82fk?ag)YwnHTX}K`!UAU1;C1 zo)O=UtDhGMeHHiiLgC?u{_bWGgJJ<`PmDJ=#QonJ{Oe8r>&j;rk+8NbAydG|p?9Lf z+Cbkr&nd9~BybWxbs7#41vM$0poQQ^Fq>hoU2(Ij+ncGXA~yiaI6B$r6KZ6J(HLy+ zc=k(Od2J%$SWfp3u|))uUgF&#q(P|dsy?t8@w3(Tr2I6(#UB)nIze5pPmUWToIhS} zSN%HMneb*ItNv}U4jOrS0r}o!G`L0F=b1Taw-(W@Hxdp!k3PRLBdVq}smgJYgRBL` z-DY}xhuJ}-B~=f}iSgL@?SDT=|AipujjSmCVK*7;QD_2pxwT<}{MqWowP_}cAdjB% zY1nO)i?zpp?0N&(rVSu_cLB}yd4b$IhdZci?7M6aFLl3sa9{1jtSRLNFjcsT<~a;b zTB?Q2=99WA>Hh?MoITi)9q@&|L$f*jWL>~q-KU&mPOD(q@ zGMQ?R9`vO=;SwSa9BIKMYoo60>|AcWcM*!7m#G>ho2nY0zL`R@Fd0DTm&t15h2-*P z8H?F7=b%qD!|1A?VFW#_(J1T&7J@5D!ZoWY-N5`@&TZIUeum?9_D|{WN0)zd55Gnd;NamC|-C zD4FuuD%6XxD;)FDEZ0(`{+OkuNCo(RwiL?vFsb*}aRxAu;(k*Lqo1%i5qQRVL8GdQ zh+3F(C70nF{du&nug64k!jGW`u1}DIn~ZiMgX;itZO48Y^FyP_v7nl$1n2w>XU5>& z;P7IwP^n*Pbf9UmZ=$8kI-fHc=5D8mT}eYdOs<1o0;4R5HL3P41)75}k-k!Kr7ZcA zR?h~L2X;}ZRYCff&l?;w)YjDBLVRS%Ur1pc7d_B2SEp4XRO0Pr5##yp^j%{&f=WwQ z8qILBxvZpUku-I9Ft)?)Zc^KR5!=^xD0EwY~R_QZh=lVwnwjay(m~l z^cjg?P31_H*UtP<_0URg;o5?fJaDq?Fsd}p5LuIc1JTY;mV51mzimW~e3?{aZhyzo zBX6$P<^En&!7=-1=I$nNI}7!0S+%im1kH?d*b@HU>=q~r6~u=)MB%=YU041IS#CBB zHg`LvB|*pC9WLGLI{?c;L5(MByb_$z%Ty_Zlq%^qreq4tS>wrzuYz#*{*inC?Q@I=6VMT1qx1H7#RYzq&`xp5qmH)`?tuh~Om4+d!RMUw6`r*j7r4_YrdT zEE}N0<;Em9h;B8JZnu$-BGy&DG1(n{J+XU!w8Pi)O03rm14~DQl1BF73zuHxV_a(h{H7=pW2Ym)CM)yH6qm zM{1UST*eqwVX`!55^N{Tn?@WC;nhbyzX5!mM`t*$8}WUI+t3EuHcsTu2RGdSN;P$W zpO)_jZ}~uMFTBx}a;*c@NATBj>|6CTJ%YV06^ySEnmsYu;24w}U=SIom+Koxdg0Y7 z5}F!{HoUV1Dp+n7XG*rbuW@7m@v6B3?%tOyW1%m_xAzXX0knzRWk(-rs{C7s?k|Xt zC$T3%X;f`1A#1K54#1jATashF(mBAu7WPD*^~;ypIqy_xY4ESVjewz5fg{U8Z$4#bZ0wP1@v-7v!P248X9=xpsBg0wV6zJH1C~9a?Iphc7ji~2fZYn^Yt>9IupeQ zMXzvHLN_z*u#>x1JHIilWK%xpy#bgz(40!JTd^)3l#z@hY$+w5|yk;B|r561e{rIsYXloktNBA8>kwDytV)y(3(1u|?)6 zt~esF#4D693dpz>pCk8SLoRt#B3fG5#ra*tk{)|6*SP-;Ak%T7?`W$wK&NzH`Otj* z=yC|!?@sjm-FVKgVY!ODjJe?P5@k+xAxTAkA?3lcd^v}Z6@|bzRR^))S_$SBk=?^x z%4sn$)HNKw?5cP0b+wcs*xXl3(d6rxGfXmCwA{<1mAcBPJcrpo_THM6+WT$w#55-q z)s}wENAJkkP?^v;$Ip(&vK%>*M~tMi5bc1&AHjLwcJtF#zoZ>hQiMHcWeg$L4#@lf z95?86Q;2?C#|k*54)fBYZ_cid{7 zWnTzfu)b~9kh`VtUz$C)IVC(NKTk&n25tn!2BsQGF;^AX`B<*2*Ci54hXm_0zxKS_ z9Kh0XTx{kG_BO2tw_+mi9ag89-3%?nUl!9Zw4tlyVA+jBn8eHPMVd0RRhrzp_zP%4 z_zb;|fH(Q;x^ujNG2koFk>q0vIEe;jHH}Gfj_viUAy|jy#&XV(k^#E@^SG9xM+Vz} ztBcm2I8kR}&`i11YX}=0WJ^u6B`7{mL$xfM?{a@y&^(}GKzcK&ExAgAiyxm{4xpal zx14|Lnh}cNpF|ip$C0j6bYoJmTy##-$)*`DLNi9H@}oc0NszV=`k7e?_bL-OLgthjq8F)gz_9>#|Bq>^ z2EJAtaiW942R&u$t*4K=$NSgsLGa7r;31R&-<{;s7J)B@#``J2W;_ttQGgNm=^nVEc@ia9#Hm>BqC>dV&#w79a1u_WO^@eAm+# z$q%xKX{27!?4sW})msk=mId3sx&hEgcDd}|E^1xrp}N}ZYbO&u&xFQWwXK`@apD3% z{e$xq_%-hSsN-ySlP_L+>Owh9&Gyxs(h zyG~Mq`l?`v5t?qu1*U_cXwA)klOTq=ER{5n-GibpIP6m zv>KyF2rLqq=`3}wOy8cAkf({rOdWAzJcMx(m#bz9Wm&k;$7j0wrQ2^%vRCGtf4X;( z_qUV}bH9{~v=c=0hZxq)qHH;)lx|b-3)oFUG&gggcOv6>V+#6U#~*7H9}29|rYS^v zk-{JE@l0Ge@g@}GYDA(JOH?N+}REK%6r5e4;PXm=h15QFb z=U~rkzO?MGrAisTNL>S^_l~-s1^wN>$t4^>AeB%GoHwec>WImo+LEB1%qY~Qfw?&= z53d4Fm?Vxtmhan(Dtg$=M2>K8ECckxW>dGCNLC<86(Q8kPE=D;J3btK0g=r0-?RWQJ>A2`E~ zH+RhpuDjqNp-kbcR;tnaDo6PnIiV_k1@wxUHiM@9#q0u~gdx;G{%vv7gLS(6bLK#|Y>_ zvA1_>+V=-}$DDF6r|CE{`!czKRkf3ek{pqP5*C0kLwU=fZ`*putM%h2u3SD4E;sEC z4y@iH8Y4zkMz^=qrxmYm_nygx)xQ%;5v_Q1aI5?n0b7-)ch@N2y~T|~TJ+^uuClsc zd&Ywpuu34<5tAvw^0DYXoT)o2`rFFg6RsmJ#(*H+h_02?v@n7n_c9vxaNJs$R9O_d z0ETsSj?-!1@%gYL4etTVu(tP}KKlBII=g!4eR@NXlryMDms{_-U(~BF!Ya!k$)3v} zSN`Kp65ceKy3)7NhMzgOzKO$NCTRfog%n~ZhtSWnm%Y{T)^g!jnno#GRwQ`OI|wxZ z9>}7IV=hLrbg!a|h=Vt@B_EWwJL;ZgkzJh}UqyJNMT~t{JtB-Pr~9x{$1)niZ@R#D zmF@DO;$BGW`rHZUmoY({Gvc+#=*cMo9@*)JfOM;)GGFwmN_#l{Iy(^hbiru5q_SmR zf$#F3Ly(l0@&)X%*}x*EAG+`^ z{kot}Y~kxWr@5DY$-M9wH3hdq@}4^mLYr6wriw5rXs4WRmqhQg&U|X-5Qb#fjv}OSGX0J#oQhuo3jkvdrTl_J_|_f+IC+ zVNgNNlr^8UWrGxZLR%lp{>~GQ%)FdG(@g@K@u(?wR%$a1$NZYN`uHh60+q)np!F)t zO=%^mQ5pp`?s)HL@R!F$dGG(4{Rc<2tE}5h z#_g!60ZX5{`AQ)GCIM;KC$MYsN`)B?5*|Oma=vFBt9}D0iFcNZme%?4(XD0OBL2(R zDssv?;Oz?l^}R;2=Y&9~V@N4L(*|4V^Lm&F1HX|Odfsi*xZY;m9lyHYEU~(v3 z`z$anZ=9;qD`#}(>;_%$vO#rAeBZYfYH`IV%u$;*d8Ks{%K1URu8oQ#3Q*0sorXRmwLQ867RLj(g%pw zezby>{3h z0}#!8r&60N%=RN6_Q4&Vo*1;Hu+)b^>+06?k{{DbG6^1$h@YwUJP&FQAZS~`C%hQM8RO#*cqp3Cx zpZE%VU-G@`E&7Hh4bd9Mx&TYbL#5fQl46Fj*jVu;;jb=q4A!6?Bw>DZg?TnkE1|Kq z%yXk(m|sB!v2h23e6vXTIF-JA^5uMlz2bcXkf6@7a#=Ngk7^EfZ&GFiL;Z8|g;Li= z*@TN+oAcSD69Xb+QWBb9d4#uSk#Z+WeD2>WFF?z!&!ndwK0ca$Qu6(=O;ZD+$+E*W z2{B-a=R5RE5UInAq7!2=YjP<5*Yyoid3ruCjyBtji88GD*ei(Mre70~7Je3eA-k z1GKpx^86J7d5b>$6UB-S*-?FK*yT9F^U0qo|ZV;pw)j!1ki2Q&{s1JHsO z;Q7KEKt%Abl+voy?*h^mL9ZbcsRPT&As70!;|cxWy|=@T{Z(;Nbzb8^cuDZKh94Jf z+F>sb_7M`@e{SVdlPgPomFq=&ul`yXeK(x$p>SV94K2tdEk)1sR(86JSxyW1Ad6u1 zj#n=Wr=MJ2Wr@Hff}xL}2bm~$#CScTrOe)4NH1Z-#a}u+8JnhFz{MOD`ObSdx=A%_Pk8=IowJ}NWdzHT@`;fk>kZAhXvGoRtSV=PxIF@o@ zS-#pq$rxAv4hCN{kdSvw)?f2IsTv!0G8aC;dU<+Us&)~0hjf=;2PHId14#WD*ew25 zJdQeWVWVYePo%O5uTN8tH`6*R=xiOyRIDG(x4B`lX@Tff<6&RpTjpK9H>CgcSJlz# z)*o>v|AWVYo8+&dNX_GxOZyu@4t)a=0!<0-3C+C$%xYfKPW&)fquoDCgD!(Vfu6PA z02*{|07O^un6&!c>vvnTV3Tbql71Yty{vgnFtsK2ckhRB>3@}uwWiY4xPig#1goPj zkBF0R6SU$pxqDn~1*x#!kC4xkK*8N+iVa&5!=~Sov{`S?StCx^4~oz9zi!bt9vUej z{Sp(4X+a(ZXivl)F6*)`9`52AYimj5?pea+^bm6|Mn)=o*Iq52nwa3^z|1A#V-X=Q zv3)H)rT)d2`C)?Xq;WqhiG0*5i_SH!cll#R3jSwG72P>RMfdTS%#}lywvwG>H0yuW z*h5iJamX^SCyYd7E$9N=b`>Mb(m|6##`&SD^NIzUtOHj#CtAP+_M99|3cU=5f)0#h zr#84P=s((k5#Z`cu>Yx+?zh7qP1#29fTCIBem91ifn!F*g+cx&<9ju?;Ev9B0i=1t z;buxUpE|H4+PbPwP|VH7Nkc4zPgNBKq__O2sehWE`}~sJr}Xtvz-woa(NOKdYMSjG z%EGfY>KKW4`1gt(SJ6siOLNv{Z$VI0dIoi_Pu>?ILuVlQnWOOZGwbs0IB5eD(Lk!x zf@c3Q#IBU6s51VoqV<`G zDopRdAdLO%^L?+^l%_!|uaOm(P@Svd8em9qEcBjOzQG9CG>+d}E` z3(2|EzHSQNEOK(POM#B1(Ib7uHtS4mGlhnxn&MSugcUxW?9li`5#Qc{q$vsI8TIP? z{tW=Q638t502Uj3Ss5VEvUt@V%?*2U6!tGW$DbVqUul;y`mn-4Ctlggv>ICW?j7ma zeabjmO!u?24Kiv@>z?L_ctbL4%mf~yUa~!ZKG|MheU=03E8KHM^Xj^(C0|c(9&Pv- z<-cLCeDr-m1X54VF?GAhcVeDQHw!0(n9MP$@er%lV@*9%ILTx!w?L`CFXX#)+PUa{ z+(4sNgPe80UM&JP`fYW}Y2*hgDznkUgmLOHPA^#ny{qq}>XAGLek{-Q`4H&W3GDVQ zH5Q^!kI)?y9*HtK@`a7pa`KRu@j1Y?!vh17Q<;dZ_1|v|zOO6n&0n$Ae$NX}`J!WH zTQI%#_TfYVQ3Kc~ZIm59t?gj^)PSIDui24ZDEjNCSZQ2DtIfZWcYF9vC z{Q{bNEpQDK#WUbx5i~W8X$OHw5xY{pQchn5yYJtjJDG~Mx0H=88s!Az?s;e>f~TB@ z-gjE?SwIEss;+&0s9{2u%=I5+*Wb;Uy`cYB%l zVoOij-_7*@>|}|B?+@`Sb{|bTww#$o$XO&TVF_g@|Z94NId*Isk_t85@p>GhbMF1S7UXb z#zYZS0u9Y9rVVU#YUyT*VCxUmG;JSPYPY1(XUQ!;C=KxlG6<&>2bWp|3fCXxB`?qF zAI5M#l}%`Sk^A~-_v8yn*&BecK3dSsSI6Yg;Reu72cXfKRlePm;HSe4Tihquz_K&Gpo%K02Uwn~gr?DHAxMV4U>(Ha_7# zn8ETRdhP%o^M&%A;|(Cu8l@3%IiaK-zV_}Lr&i1qfAYwNk2i%AJo zivdSPKhmx*9#7hOkJnkQ{ojtfe3L4}-JXepfdz+(MiL|M9Vxfyp>2oO3`u!MJURpo zy)jKnPk(lDxf2~vE_Xv-;FIhnE;_yk7&mzq7TpasBrWN%vddP~vv=lT+|}EW z?QOksNS@4b!*s*Op5KiOSvW#=pj5`7kSyAzt$QTtK@4o+>M0iJ~dn6$fxGs86tCaO^T^eP^8kM{B6Gz#CDK`enKjQ zo&j%km$ld405GtI)&8$l1{EoNy_SCtT^ky|584~Y+_Dx9%+I@LV6OC^2O-VVjlkhB zu>to}zi&Ur*SQD(6`-9$*EDVUN$$z@v9L0n5P0~POP1|&z--Sm!agi-?~M8kK@6o zI5|bQ#Fo3V!>JI}xzU-y1pgkp-A94heq{dBp7m;F|IlN?v?rvDgjIdKogccD20Kc&imW))4t(GpZ5NFNr2|2umx07)Y-F zYxJ$D-Sp%!Yj2(Ol4$u0Y>#6^(@R)H6^cFa;g}#bQ_GtIppV%#H}D<0|4#jUqkh8| za#k*xc&SWB*r9mDXaFaU;G+XfCEX7T#H*LFbS#k_i_^4W4yz{~Ae%>6@Wp;{&&dS@ zLMi8r=b9Q@wr}V7Pr$=ezt2A*Ok;~;`eE?(9NHnJSTED_jw8LLujqm5hBL#OY+?^^_4yUHaG8@9@nNyPdu)j+bDW)yH*49rR))XWBW$4I#8__`)znE%XTc!O9JQ)cqnddkE((Pk*8Wg95@2 zz8V1t=9;YHl!|Qfb9?Y`fDPo6em4edAuc^g)ZiG5!=21Eu|`=R-%g9(kVOWpj4z4I zX$YYAqXQeXh&?NnZz|O@_d&8+erd({SkaIaX8lFR^hLNjs>^MeWyd?sT&$k zfX=xd9X1aa!RZENFUJG}=Noif;r*?Q*p5`^Yr}#x?dExhy0Abm58mCXZ($T$D|P7P z`#lxkyjN<9%@5QHGZ$aLhQLp50E_gg{>j%Z$@KzTUur<>X$6_A^+?zZ+iw!!qCTdAbWiS=$^akMhOZz|DzS|Up5Nc@voT{4o& z3bn4#(l3yzJ9w=-G6Z6z)44`OwKkSxZIN0T&?4XO_vYfwHk`r{>@*pkn89 zzM1{K8$g%>bFy2I_t!aDqV#dE=jmiU)9uZ^7z23?YLY5u@;x* zT@jAnBV+(Z5#FBSK*qdc$`D6Rok%TxTFfge63T>rCf-0Bm)nhp`D3Mzxa7Zqb7w)# zgw?$eva;i^t81;cTtTJQbLZ9ikIId*ldd#oTiPr06RlpNiTx|z6lN@DwY&vCnQpEw zC{NKN>Zqme|2g;gcC9^C#)2nfT3_S%zK}$wH{bOL64Il`dwPFPq^1)?J`G^HD1@bN z#x91x#-M;3fJhk(r)G4Ck>g32av7=4Q?mIyk@+Oo=^mw}J9N7Xh1*8s2BlHG_dW0R z=lHA_t}Ki8+xQ~u4E^lx7U&UW;W7c3c#mH;T_sfvT)S=_Mm^OGD692n>@V`Bj z|J!-rd=Zh)FSV&iR-)gIM~L}VrhatImx>#H+9IGdV(4F1MV9$dxG`!r6qUB*1%6|& zq4#RHba_70SWx~Nf78t7)jqEJ?!uV)Iexw}NB6`f&B+*=aC6)sa_+ptw0`hfNtZbR z0PP_vnu%B)y%W%vYH=2>20z?33p~$^KIf@96FQQmb?ve}99LgiKry2dwzy%LGVtU# zB8fr7!Ii|HK8O^`6t$ihlCvaR*M9cOJ--38iUsmt=v{#pp^rz*fOrkXPlBInoX9iD ztbry=x|Xl~MUh)NEN3AuYkYQxviOGzu-a}DVbefS&3)O)=tGq&pJkkFUl{tB2Tkog zyR1|}$Ly(}M231qzTs$h$V{>Cg}ec9S_m?93#6M5U5#vy8$dCf*=+%nh?a^JmecGP z2&6<=gX&b}z;kfcJe>Uo;F?ha0uJ~;4}_Dx9eqC*R1Nl!nNu>2?r_v66}*Hs(R*qs zdZ#P@i4pk?9}78Gy#ZiIk^eR&^8bBp`pdzn={HS7mHLnHU-6=Hw(4IuGIl;V0WJ8+7oy(jxmxF7E3txx!TO_gkQFDvI z$-@I|twh0JCe-uWA;DEgRYxT@E;Tl*m`inDNYM!rL4GtGVXm`it?KAP6cJ4ue}PC_ z7i$Q4cek1-bhBGpe=UbAF&yU(95`A#@Bfl$?QsOq4FtIXm^Dt1CoB6^FMQm5lx|{Wq> z2FFhN`^t^RpXk&iwxsVLv)94`1cw}!(*Cqn0o@?l*6+?M@qS%-9=do99$?9C`BhIC z1N}u#7;47a0?jNC9`hksQeGOWnyllfVdQGwV|t{UN37aWy0=qH=Sg{dved2DHL60b z7~ff!!u~xj?0-8emdcVV)lKX@Y=79^609v@ekeDQff>uh*UtH>^uAC7!d_q8OA_Cb-}BslE&SMexvB{riuyYO4!#{rEal0`%& z?-w|62;tB#29W{bOsy-{FZ6>}>S9ABxH8|X3!Cis;OAG_n9dcpGkYYeW)T@xZlBCf z#Z7cLralQ=`rvWqF%@?*y3mi~tf63$=ytMbKwdV7{VDZcq+7xq^dO6jMZfka*WHf; z399GI@Qow3!mfsWfBXDEVaTYrIjF(XEXzG;1N$vw8gJ(2I66&$PmWI9;9z zT}GUOd;l4*XXIv_{`{NmR8GaM7F>uzL#RBpU9PM>H}IJJ)EWbOSeBq?FEKh7(x6}}HMk#&e0(K_ z-dVViQ#+ym!<}z@E9$WGi%-|5Xs?s`-{KHh1e&)+XdI=%eQqCk19-&Co!wlE`X>kG z|M3j$pP9xT8NMYQSM7Zr?Q$;_h{44L(P|fh$Aafl4R*EgdE#~3M7Qp#XDUia8xq+l zCRe1t>^foy>^l}h@1IX)&&(>=Vz<4Dev<>>E;J{k?8Yt%4aq5IPByZaxaKd!#*E6OUu7K)$!-xctO*PO?=~Vv zIJV@h!ld-<_wGM`v7D$L6E}T4pq2KBa1Ja5DD`ohyFdW19EtYaVYN)th$-qEA1MDd zn7*}P5K)`XqBR;KDkjPNxC&2Yoo=~y;HCFEi(j1RW*n&0E7iffH$=lfbZ&}i`Ofn! z3)(g=Ap2;D#2P7o6p`wD2(Ia9qonN4d@?60PnCvevK}WoBx4j|;9&P1{_7ZauvP^WMiVI%Yn}V|~Hs?$^NUkD!&PbKC3bmgB(W zEwR;wZ(LUxDU$>vv5BK0iA_p>yvy=`qq;*GHM!=LKngmN-Nb1cJq3?+0?Q48AM+e^ zaUe@vb7W#%sg+bquld@K=93+xDA;x+txP8qz8KlZL%+5+z<>IzLNJ_Lgztxb%wA0e zG1a5KK9`m&hzowarDNq*nF?zE4E!;C2}YcoQ#$wsIGBG|t|)VTe9Gtuua&NKLv$uK zpG09kL91v-X?9A+pxd&-%gswR*TCmo=<)9T4{FcVf(H}7UYQq6|45``$k#L~<_pZ# zl=jE^ng2dJWie^@*)NOT;#SPi(YmOFV2<1S@U3o_|wkMdr2TG_3bUHH#I&`>Tw z9=UR1J2jb=XtI)wVa}fIbSbm=K-Gp`&Sy*y1>0fOKr%A`y~kn^fxA4?-Z3X~Rk^{d zzChL!$&!FmT1i@9h#m0ZZabSRpjA>XuNFitcP~!NsS#K+Y+&yG(-K?c`6=V!fUEy; zp5wYruSQ$#atLd=VOr2I%SYb}0}Co~N?g(hAr!s4(cK4gFP`kU_LGT7Pnl9G)oKTj zy~m2@e}5k?v(8~afsoD>edpOtby~LHDOI9R1@7Oo6N zR2bhTd!%$HL=}(;0-B3|aKqsd*CgGSy9OiC%+FaqCfhI7vc@vI zj@IyZ#sgT?CacRMIY=s^9w{FebT* zYEF|0>xGjaziP~J^fHx}sV58WyVVr8i9>5x{R8w`6)e(snVKA8D1|=U;V9gXZeTzX zmGPx-Fi&$8JPYuThOzaw&IrS)MFMl%Sa(l$c*)OLqlm6tFj(sgJHBoG6 z%=hbj`mxLre_K zrElpb^>T5I>nD$YG*b>t+BNAeKr(*K6oq`+-jt{H@6WLkmG&6-p&qEl7RHxYXKq;` z`Z4KDoD2lu!I`!xk>y4@4Hn^z0g{>rZdB^ll~j#pN!=sHBeJYNUx+ITU>5QeBm3_vBr~5OZ>mR|a!YVAsr`j>} zB7TtW2JJX1T<#-XjEx;f^jH%(41 zadbk|y*6j6_CLOUZaPPD`{KBSZyKn;Dv7G?K~Q1!nfHH_WOWl5b_sXQZEM zku%%qUFV2*OdDqryk!L}8ovxPJ#|5cTKw0m!b{(9JyYJUFSrOEAgv0P(=n1>QKhYB( z(2|F|_P2;t7_LPuGRcKuu<=uv>{DZZ$WMx)$G<>T$4`t=WiG0sl5;C@nG4qrRd>Q* z%p#YqZQb|%V1wWI11l!d7uu`M%-1fUIX1Nfi*^M;vg8t-)!`GK2u>=hXr@rtyw1f`Qg|DtRS&?TAIMXX~vak@D+yu@v@`} z6Cu7Nb$TFO>6>>&B;37BUHbJ0-7p}zuw=KJJd<}aqPRiwW9S??!ZbMHkV>tqtX!>- zSM+D2lDe!C_Txv>Os>4-l>>DB(XGSy!xvF+oc5`%GhZ}(OSg)5h$}8=YP7#>1;1fD zJ(jClI@qS{Y%P}PGBx#IdqJ(Q`rNZ;kQ+F~HZ^La>ybPtfe?c+m#yKpwtK@1#gdCK}QQV zMepAIc)v&TrHqFWk*)?cJJH7;AoOSeV8qVNco+&rH@!5yWy9*+wx#Jm5h%i&xt>4v z^P9_E0A>HfIQ-EN5$7@1kXEcQv1H*kb{28Xe0=C=m7psFJHt_2lcJ4zKc0K=+4@Y# zeb}lkdDJQVX?MBTUCUp>fJn`A`X4RtnQColha6T;K80Zh{aKD<1HN@ zJtQ2ilIP^w(OI%#nLl=EZ>nN4r5*|ABMFnD*f!6$kvNIX`gEQGxk7_Kq9$EZ8>xL< zB8^~3RPdt_%+`U-zlbt_MT37(|5QmdMPJ9NT4B22U;jsu;v>rR_b~y zk$diLK-+c4yC5Mkgh~S56o}?}v!NYWsgrb>ibCTLTljiV=y~Wn@a=ZO?Wyv(Bm5M zb*CDmSq3TL^M4LCZ!iclUO;0NRX<$2heRNJxXLt}?4H}RM#_H)YXxdn+#h%qgEuW2 z#T0Ts2DW$@Q-O@KM%k7ZXNFtNF`jOQ^xDM_{gAsu*?vpN)fh4w%8 zoIaDeB@JWErhTt(^2@WTeU+@hUZ^c1&yoS~5WgU8rU)yO5L6k$2VMJjyWsxlb$Ymh z%CU+MILrhY?dZKsxa+4$1ZdliS;s#r>C4a;=JR6>F=iN&*hEud4H}(eck2jlw0}(} zSKF)v6WXVF7P7=g945*$32PhA4T$oP`MbJ>8eQm0wAUxFIjb`s?#okd(c`|Gt<$~i zJ9Cx844=R8ROhxPvhp?bBR#9wHT}*;7?Oi}nceWM^C?e<@H$rk^Lwla$J@$`4jt}i zrQD_C@k3`Jg9^uaigbcS;*|I>YD*3qO3`)v>@LRW^4YpvJWi)iamdy zVuSxv+jT}Yxoz9fMG(+q=uH8oOBDzu2oj1)=)FjjUZhD6ir|snQACP>0Rlu65b3>3 z=)Lz|0-+P%=e_T|ckUSXob%p&_x;HDGO{!BWAC-+-fPXdW~gYA-N8yvx&$FubrptL zKMhj#d>(_*{Z<&^83Kn&t4iFQ^s@!|4^L;^EL9@Vy)DIlP;7+PWOgc+-zMMLk*7a8 zGUwUBD2|{Vmc*q)%EENg7Wa%QLwl%nn6Iv->GKp!k#n8Er~@=e$TR%UbUgpuPwKyp z)hmeqtYiJJPSq61%6V8B_33c}P;3=HjWYj-55x#yHKKDduRqf%*9IY$#zudD7Bc2e z{Pf=rWP3*|`HZ6S+aJ5#E3T#z@qu&}$~jg_36DxzaaU@e$aSBpg)g5|&vT8GcB{$P zyd+6N)T}mboNwfy`?>YQZlIJ` zQodIVZ{~I#a9n7cI@~(oZZruK9#8Y!KE&6M85orHtXP$EF2%UZq7dplgwFLE&`L;* z$&hY^j;?hV<>OHLmYZt2QzmZ)DWipCH|D-j_LM`+4&zqqipGu%gAA34*q#}B0TtW6 zXG-o}FD`AQ48j@8z#SesMeT#n{f1BoTH{u$ zG2R!V31$LfqOoeyM60~iU#5AOOv3_KiDu^a9@QG%V{fQufkc;PH}_sDr=t`OiAx*7 z_|!#(7e+;TMC159X4+o#EcxLwseMf?iM#ll3Ek`OiniEx6*o}C7Ajg5`=HtJ{Z*39 zF{Mw33l;MuZuYmf9%{)hs*ttRqK~j!mYi{~oo0z9roRR9lHcN* zTs+kL?hWC;MM4>UJt11z2()%EN;t>M>)&F17xJZ z^xYJqGzD+So_DmcCw>l-xl-o|R69@bNhD)8I{0B!SFa!Bh>F=U>X~*+2!ZgPps{YP zYnWSZq>}82^rVEKQTeg@rPqLHTdl;W+r6dg$>(Alo#!QyaTCMAB6o^|EuVpWIEgSu zi(aa4PTosN%laIOXQND5ew-V#Phvt+40fHs%n|_)A;3KiSIY-zb(pz95?k?`hAHg`L~1n5{Wf3VuGZ5+gs$ zH@+yvXOnH1VU$2YF6WBJuJh)g={H_yWX9f3O1^X$;yT!7U^I2v)3Vc)ywPz7J#E8v z+TJOn&MNCLYpQ73OEQcnU+0mV9ge;$4y*)koo`E7A=PIpU5}Wtb&7G@+(b(;)D8o7 zm8BA~3D`8?Bn^aXnq4@{JiEIGr*H{ZqCyT{jWZ*g7a-EzokrfuC?17d`LY=-n^^mW z^u|1G@tbLSJC3+C<|@VWjjUI=B@lka(i+K2?Pz|ENuGBSGnJgDU7J|yU9^nk!#>W6&BgJ;{vli+WubeCp$MIa$lYy~I3p19@dLH%JR|SX^rGDM3(mTaB*%K{BWJc^NL8-Efcl&ze7ybGXjr`~;%IVCEm2l8JrBE=Z zRmUbTxzq4MDDe@S+JVI!eH>AR$r0w@`rI5io;s06{yWI`gZ({KY+o916)AK5_@a|$ z?i34EJ8(27w`p85Ro0BOEC%Gpx6a&4Ldo)pia<{pmC_r9uCuO)4=9qi-youVI)g3h zxMw) zF_szW45Nh0cx7IukQDfu!cc#df6w*H_EhQYMPI60xn3e?h`o6=-A4zh5WY+E!|GCg3T zJv;89UTFd8*HvMYG^6KXZpd=TFcsRrgyL#wJ4QSFmF|#nH_U*6A&ls2e%sRcxA&G< zb={nqgoJdFKoXE{U6ucl`f0Lf4iI)!WEMV1{!QG1KG!vqdC^~{4v(< z)V_(rjcnYSO|g+0EjLIo&&t|c2EV%gbnTQSv<0`A;mxg4(w2X~x)OexFlWBm=Ipq* z&I*0&VmlZ1&Sj>`+@3jbCv#zPr1n^}VVpX|v%zX^3c@7mGutlINxYNOM2I~vzaKsLNR~>b^J6{ zmE-JD?Ap^y?L-$@Lx_}`z;Yr;Rz#_Jl_2H-F(Ajm7C$eP5`UCE6@)rf<^l9}pM2(i zQ^`L})d{;5udfpYGkh~kuwyXIu_u?21ygEHx9MOopjnC;=?!%32vD7mxbSQaxpy z_{z2Rec-^|+;02n=Xxa8$u}?jW_`3{e<gWLky^X!mUTwLp?Kl>zOH^gr+s7%C7 zqsKcbuBl(qEaA%5oIv*H+BCmB!)0Enc#17qsc3OtOOd7*(w&A;*SHV&lbS$R(v$L; z-)X7?H>Sf(YL$L3sH8H})lg~m>99(ABhvO~{7lccaNrdP$E3+GEQubEZg4C`i?m^h z#8)BkugpAVGu!rh5 zHf<$)=i&__n_Ph!c?uO!w%@bbm>93Rp(Uyx0w2)6n>v{+$w6@6u5b!oc8%ZphAv1# z`+2Vp_F5jN!d+Ix<$Sl7$j(tWxuY_JuntOgtC+;JzC#;|*ag!3?rRcJN0i%A9SfO2 z&aAy20?O@$5dnsKV3#YRw)xuGca(}XjFT=uQe3@ zG@c1ykIl7{&{QEhxoZD|JmP-hRCziVArSMd7mBw0$U}8$%($};j!@$mt)=i3zQlr$=; zJ5-G_={Dyty_EWS)C6Mvs44SJkO#ByE_?wGmOOUOf=Vc?&!*XWR(Zr6$)h135g}py zu6>C2QF^r5s{vd7c`cB9)tQNKCfn#!%M3>kbxY1PQ+ zsd1pMq!0DehM#VIi_GE@Ts6Yptp6^alNuA&tSrV7VR8J>`@QsfjLb=%$A8{(P5>${(a~A z*?Mjg->_aHk*1J>6t0P}qS_yzeQ`9}xlkZ= z9%~&2Uhe@q|2M>!Yi}Kgik)p8C<(Ro_B5?P?@gRs@I?>f-c32IVi3X}=iHJytigI- z0X-HOLuen3BdZzI5732}XSuDOvx}@WY6z2dSjq>+c z_5NGZ%;Q+L>)J#O_W6{(Hf|jU7^K#i;MZmRUiiA!Gxii2XvT=Ea#pu~Slo<8JnU*H zrFvE5Vpq#FRJPIJnr$#?raIH9pb+D|N;U#MpnD!fV9yBQuf16nsI`zED$30M1GM_+ zBqrCNHp9yP<9KdyhVzx8%D~Yq`EV$KIW%G(qH{3`Sq_|zIG>sIQY61!SSshe+qQX; zH|dnp3`z6mnP#$BRy`0nQl3l2ITOt(zx4D{5bN^?(75YC>%PUhI-;phlP5~5wO_;vlXDcqARAa;a+y9&*3b$lIn z#p}VZqKxG};Vme&P+{k=Lb~Q^RrCgQZ&tcdKhbb>58plS!AV7c3Fj!Tj89f2pI#-JVQii#RC`n~& zKr`>2{F`tdtgSUrSSJ1C>!7p1_N+?@hTaSM9ObF~2&k!*7FqD}SpCl+`R;PNr@5vq(9jcp>IsK)-HiD%2x?|bpt|UC$t{lR*Lz{wo zg9VXzv(ey%Sz^{hQoCr^uy#?Up6m_?v5NczJXW6&$4l0}YT zgRL!1qtay(%@YKq=3n!d`mg2cI^L)cV%h6Sca~8?Dm@Lp?-7%15K~aG7zkmV9RV8N z0~Gh*hn{<6q30VrSLlWhlDe)d{Q#-{>YsNuqkA6eV~pbXT$yAe$sS4Lh*PVsn}5{+ zr#Qx0S0qz!PCbskQ=~)Kc2NfOduAPFQe!IlM$6D+3vGJW$cJfN_6cB3Vy#JDE?w>X*w)QR z`yAzXq%fm2P>g9Pr!_ z5F8RHoiGp(7zvH0Z{$8FF-E60@3Hu$YMP*S5R2Z5y3AlY9}~QRQ4`mRYN^dnE-eWa0V*2K2yJP84mdmj3q^dg%yzMqZK(%H-%U^HZv_xwFZSC+#s zSnt-kD3{ya@3@)h$#Ekwk|q#^ks&*~>*IKT=*ghhh_k^-_u+7{Fr{inq7Yt$6tcJJ zxOI#G>lM+dfAEgRbbal@%1r8TFqnGD*&f^MMzpZe+)@>rfPY@|ZJL>UQ9^B6GkHec z9DWMy?`I8AieqxiZz|AIToUt)DK{7VLe%UvZt-jcd5=&X7tf>cJ z273X9Bqd@R74{d95G9{0hGRclc(s1NT0hyFpDXrD;`_>GC3Q!)QWgrmsyl& zQ80hILrucE^XZG~wKCLd5_=n;R*nzX`d?4E#{!$V)7!Z-N{k;R_g{xp? zMXC=ppJBejkD+l*`q!spd&8%vr9CIEk+2R4TnCh@yu5vS%3MEf=7j#Vs{AiD@$gO? zGTARJzPH&J1Xxxly7-znf7>S{{Wm!S)F+qmk}}z%!V!UAQ#h(vUz9nOx(OXA z-#!aL&O4f3)l~QpN|Z@hn?TewkZ(V44)l4fOzcbX&d2wYFbN1U1&HFYaP8he5<5U$ zVnZp*FC)bRYoAL@d%xHS_Cn zL4WI>l;L$0RcjR_e14kmm&9*+HXO;Kk zY%*`%KiG!0e!0Xx8_9S=sYeBv3kB!8ZuPL2Pw@m5(I-s-VCOh_H|C%?;FLd%&;A$oSy1JwtoHo#B5FYQSz&+ z9jA;ZPS2>yeCNzN@BrDnU!Hs?r^h=yd3oH}c7pfnsy}0Rt!1uMf5OY5)NSvNImJLM z=lzV~pYZV;$P-@X(PUJ<9BvpRPvT@~chY3ajQ9l2FYdlYSSzBJ%OZVnGuL3iY%j&x zS)gm{H91Z;)Wy(VSSjq?H7L@;NVqkQ-C&S6x?X!@Q%H)1QXC2OGxNWrgtyJm|Al9@ zRGyR!fB}D7$={tXf512Y6(9Y78z19Pr6~+&fNLmOPOlz!jb|JaK--6xz#S3wy)A9Y zg{~_@0bQZ0+Rq{+JFaHC7v~yy-+X84HB_k2SFS_Wdjt;k&*>sH;_qPls%d^Bx*cV) z12H>5fPpr-I~6;*uOknVBYS{kuo0s0w-Dj|{yuUTqgj`F*u<46EW)=g*A!h)zq@H=KmUHl=IeLd3A64~=QTnKV}qiU ztL&`EZ~3bqYEx+DYkfEO1&`_XrGPJZ65(;eBDK$__sOfj3P5??cs8;vc&>-Ij%G49}g zn#eNb$5=%v-1wYBqd@xdp4+*E=3vmY+4@!$V3hH}pv=|F;mMfYURzkjO&ES>H)gL$ z1NeKlU>-YkUXz8rg;j^zUGw41`LrboUm!){%Zvpg{x6XGe=~mNzm>5XcE87o9e`x= k{@GTV`FoOS;A2(5n2><{9 literal 0 HcmV?d00001 diff --git a/Example/macOS/SwiftAudio/SwiftAudio/AudioController.swift b/Example/macOS/SwiftAudio/SwiftAudio/AudioController.swift new file mode 100644 index 0000000..ff75fd2 --- /dev/null +++ b/Example/macOS/SwiftAudio/SwiftAudio/AudioController.swift @@ -0,0 +1,46 @@ +// +// AudioController.swift +// SwiftAudio_Example +// +// Created by Jørgen Henrichsen on 25/03/2018. +// Copyright © 2018 CocoaPods. All rights reserved. +// + +import Foundation +import SwiftAudioEx + +class AudioController { + + static let shared = AudioController() + let player: QueuedAudioPlayer + + let sources: [AudioItem] = [ + DefaultAudioItem(audioUrl: "https://rntp.dev/example/Longing.mp3", artist: "David Chavez", title: "Longing", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")), + DefaultAudioItem(audioUrl: "https://rntp.dev/example/Soul%20Searching.mp3", artist: "David Chavez", title: "Soul Searching (Demo)", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")), + DefaultAudioItem(audioUrl: "https://rntp.dev/example/Lullaby%20(Demo).mp3", artist: "David Chavez", title: "Lullaby (Demo)", sourceType: .stream, artwork: #imageLiteral(resourceName: "cover")), + DefaultAudioItem(audioUrl: "https://rntp.dev/example/Rhythm%20City%20(Demo).mp3", artist: "David Chavez", title: "Rhythm City (Demo)", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")), + DefaultAudioItem(audioUrl: "https://rntp.dev/example/hls/whip/playlist.m3u8", title: "Whip", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")), + DefaultAudioItem(audioUrl: "https://ais-sa5.cdnstream1.com/b75154_128mp3", artist: "New York, NY", title: "Smooth Jazz 24/7", sourceType: .stream, artwork: #imageLiteral(resourceName: "cover")), + DefaultAudioItem(audioUrl: "https://traffic.libsyn.com/atpfm/atp545.mp3", title: "Chapters", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")), + ] + + init() { + let controller = RemoteCommandController() + player = QueuedAudioPlayer(remoteCommandController: controller) + player.remoteCommands = [ + .stop, + .play, + .pause, + .togglePlayPause, + .next, + .previous, + .changePlaybackPosition + ] + + player.repeatMode = .queue + DispatchQueue.main.async { + self.player.add(items: self.sources) + } + } + +} diff --git a/Example/macOS/SwiftAudio/SwiftAudio/Extensions.swift b/Example/macOS/SwiftAudio/SwiftAudio/Extensions.swift new file mode 100644 index 0000000..53b0295 --- /dev/null +++ b/Example/macOS/SwiftAudio/SwiftAudio/Extensions.swift @@ -0,0 +1,22 @@ +// +// Extensions.swift +// SwiftAudio +// +// Created by Brandon Sneed on 3/30/24. +// + +import Foundation + +extension Double { + private var formatter: DateComponentsFormatter { + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.minute, .second] + formatter.unitsStyle = .positional + formatter.zeroFormattingBehavior = .pad + return formatter + } + + func secondsToString() -> String { + return formatter.string(from: self) ?? "" + } +} diff --git a/Example/macOS/SwiftAudio/SwiftAudio/PlayerListener.swift b/Example/macOS/SwiftAudio/SwiftAudio/PlayerListener.swift new file mode 100644 index 0000000..6605b13 --- /dev/null +++ b/Example/macOS/SwiftAudio/SwiftAudio/PlayerListener.swift @@ -0,0 +1,92 @@ +// +// PlayerListener.swift +// SwiftAudio +// +// Created by Brandon Sneed on 3/31/24. +// + +import Foundation +import SwiftAudioEx + +class PlayerListener { + var state: PlayerState + let controller = AudioController.shared + + init(state: PlayerState) { + self.state = state + + controller.player.event.playWhenReadyChange.addListener(self, handlePlayWhenReadyChange) + controller.player.event.stateChange.addListener(self, handleAudioPlayerStateChange) + controller.player.event.playbackEnd.addListener(self, handleAudioPlayerPlaybackEnd(data:)) + controller.player.event.secondElapse.addListener(self, handleAudioPlayerSecondElapsed) + controller.player.event.seek.addListener(self, handleAudioPlayerDidSeek) + controller.player.event.updateDuration.addListener(self, handleAudioPlayerUpdateDuration) + controller.player.event.didRecreateAVPlayer.addListener(self, handleAVPlayerRecreated) + render() + } + + func render() { + DispatchQueue.main.async { [weak self] in + guard let self else { return } + state.playing = (controller.player.playerState == .playing) + state.position = controller.player.currentTime + state.maxTime = controller.player.duration + state.artist = controller.player.currentItem?.getArtist() ?? "" + state.title = controller.player.currentItem?.getTitle() ?? "" + state.elapsedTime = controller.player.currentTime.secondsToString() + state.remainingTime = (controller.player.duration - controller.player.currentTime).secondsToString() + if let item = controller.player.currentItem as? DefaultAudioItem { + state.artwork = item.artwork + } else { + state.artwork = nil + } + } + } + + func renderTimes() { + DispatchQueue.main.async { [weak self] in + guard let self else { return } + state.position = controller.player.currentTime + state.maxTime = controller.player.duration + state.elapsedTime = controller.player.currentTime.secondsToString() + state.remainingTime = (controller.player.duration - controller.player.currentTime).secondsToString() + print(state.elapsedTime) + } + } + + // MARK: - AudioPlayer Event Handlers + + func handleAudioPlayerStateChange(data: AudioPlayer.StateChangeEventData) { + print("state=\(data)") + self.render() + } + + func handlePlayWhenReadyChange(data: AudioPlayer.PlayWhenReadyChangeData) { + print("playWhenReady=\(data)") + self.render() + } + + func handleAudioPlayerPlaybackEnd(data: AudioPlayer.PlaybackEndEventData) { + print("playEndReason=\(data)") + } + + func handleAudioPlayerSecondElapsed(data: AudioPlayer.SecondElapseEventData) { + if !state.isScrubbing { + self.renderTimes() + } + } + + func handleAudioPlayerDidSeek(data: AudioPlayer.SeekEventData) { + // .. don't need this + } + + func handleAudioPlayerUpdateDuration(data: AudioPlayer.UpdateDurationEventData) { + if !state.isScrubbing { + self.renderTimes() + } + } + + func handleAVPlayerRecreated() { + // .. don't need this + } +} diff --git a/Example/macOS/SwiftAudio/SwiftAudio/PlayerState.swift b/Example/macOS/SwiftAudio/SwiftAudio/PlayerState.swift new file mode 100644 index 0000000..891d6ce --- /dev/null +++ b/Example/macOS/SwiftAudio/SwiftAudio/PlayerState.swift @@ -0,0 +1,25 @@ +// +// PlayerState.swift +// SwiftAudio +// +// Created by Brandon Sneed on 3/30/24. +// + +import Foundation +import SwiftAudioEx +import AppKit +import SwiftUI + +class PlayerState: ObservableObject { + @Published var playing: Bool = false + @Published var position: Double = 0 + @Published var artwork: NSImage? = nil + @Published var title: String = "" + @Published var artist: String = "" + @Published var maxTime: TimeInterval = 100 + @Published var isScrubbing: Bool = false + @Published var elapsedTime: String = "00:00" + @Published var remainingTime: String = "00:00" +} + + diff --git a/Example/macOS/SwiftAudio/SwiftAudio/PlayerView.swift b/Example/macOS/SwiftAudio/SwiftAudio/PlayerView.swift new file mode 100644 index 0000000..fd714dc --- /dev/null +++ b/Example/macOS/SwiftAudio/SwiftAudio/PlayerView.swift @@ -0,0 +1,88 @@ +// +// PlayerView.swift +// SwiftAudio +// +// Created by Brandon Sneed on 3/30/24. +// + +import SwiftUI +import SwiftAudioEx + +struct PlayerView: View { + @ObservedObject var state: PlayerState + + let controller = AudioController.shared + let listener: PlayerListener + + var body: some View { + VStack { + Spacer() + HStack(alignment: .center) { + Spacer() + Button("Queue") { + // open the queue + } + } + + if let image = state.artwork { + Image(nsImage: image) + .resizable() + .scaledToFit() + .frame(width: 500, height: 500) + } else { + AsyncImage(url: nil) + .frame(width: 500, height: 500) + } + + Text(state.title) + .bold() + Text(state.artist) + if state.maxTime > 0 { + Slider(value: $state.position, in: 0...state.maxTime) { editing in + state.isScrubbing = editing + print("scrubbing = \(state.isScrubbing)") + if state.isScrubbing == false { + controller.player.seek(to: state.position) + } + } + HStack { + Text(state.elapsedTime) + Spacer() + Text(state.remainingTime) + } + } else { + Text("Live Streaming") + Spacer() + } + + HStack { + Button("Prev") { + controller.player.next() + } + + Button(state.playing ? "Pause" : "Play") { + if state.playing { + controller.player.pause() + } else { + controller.player.play() + } + }.bold() + + Button("Next") { + controller.player.next() + } + } + + Spacer() + Spacer() + Spacer() + } + .padding() + } + + init(state: PlayerState, listener: PlayerListener) { + self.state = state + self.listener = listener + } +} + diff --git a/Example/macOS/SwiftAudio/SwiftAudio/Preview Content/Preview Assets.xcassets/Contents.json b/Example/macOS/SwiftAudio/SwiftAudio/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Example/macOS/SwiftAudio/SwiftAudio/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/macOS/SwiftAudio/SwiftAudio/SwiftAudio.entitlements b/Example/macOS/SwiftAudio/SwiftAudio/SwiftAudio.entitlements new file mode 100644 index 0000000..625af03 --- /dev/null +++ b/Example/macOS/SwiftAudio/SwiftAudio/SwiftAudio.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + + + diff --git a/Example/macOS/SwiftAudio/SwiftAudio/SwiftAudioApp.swift b/Example/macOS/SwiftAudio/SwiftAudio/SwiftAudioApp.swift new file mode 100644 index 0000000..6916f56 --- /dev/null +++ b/Example/macOS/SwiftAudio/SwiftAudio/SwiftAudioApp.swift @@ -0,0 +1,26 @@ +// +// SwiftAudioApp.swift +// SwiftAudio +// +// Created by Brandon Sneed on 3/30/24. +// + +import SwiftUI + +@main +struct SwiftAudioApp: App { + let state: PlayerState + let listener: PlayerListener + + var body: some Scene { + WindowGroup { + PlayerView(state: state, listener: listener) + } + } + + init() { + let state = PlayerState() + self.state = state + self.listener = PlayerListener(state: state) + } +} From a1668ef1f1a147568b1feace4efa210230fde4c8 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Mon, 1 Apr 2024 10:59:06 -0700 Subject: [PATCH 03/17] Add audio tap capabilities --- .../AVPlayerWrapper/AVPlayerWrapper.swift | 2 + .../AVPlayerWrapperProtocol.swift | 2 + Sources/SwiftAudioEx/AudioPlayer.swift | 8 +++ Sources/SwiftAudioEx/AudioTap.swift | 63 +++++++++++++++++++ .../Observer/AVPlayerItemObserver.swift | 4 ++ 5 files changed, 79 insertions(+) create mode 100644 Sources/SwiftAudioEx/AudioTap.swift diff --git a/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift b/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift index a7b168e..d08866f 100755 --- a/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift +++ b/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift @@ -24,6 +24,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { // MARK: - Properties fileprivate var avPlayer = AVPlayer() + internal var audioTap: AudioTap? = nil private let playerObserver = AVPlayerObserver() internal let playerTimeObserver: AVPlayerTimeObserver private let playerItemNotificationObserver = AVPlayerItemNotificationObserver() @@ -385,6 +386,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { private func startObservingAVPlayer(item: AVPlayerItem) { playerItemObserver.startObserving(item: item) playerItemNotificationObserver.startObserving(item: item) + attachTap(audioTap, to: item) } private func stopObservingAVPlayerItem() { diff --git a/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapperProtocol.swift b/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapperProtocol.swift index 0903339..0716eca 100755 --- a/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapperProtocol.swift +++ b/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapperProtocol.swift @@ -13,6 +13,8 @@ protocol AVPlayerWrapperProtocol: AnyObject { var state: AVPlayerWrapperState { get set } + var audioTap: AudioTap? { get set } + var playWhenReady: Bool { get set } var currentItem: AVPlayerItem? { get } diff --git a/Sources/SwiftAudioEx/AudioPlayer.swift b/Sources/SwiftAudioEx/AudioPlayer.swift index 4e4d023..fe4ffdf 100755 --- a/Sources/SwiftAudioEx/AudioPlayer.swift +++ b/Sources/SwiftAudioEx/AudioPlayer.swift @@ -13,6 +13,14 @@ public typealias AudioPlayerState = AVPlayerWrapperState public class AudioPlayer: AVPlayerWrapperDelegate { /// The wrapper around the underlying AVPlayer let wrapper: AVPlayerWrapperProtocol = AVPlayerWrapper() + + /** + Set an instance of AudioTap, to receive frame information and audio buffer access during playback. + */ + public var audioTap: AudioTap? { + get { return wrapper.audioTap } + set(value) { wrapper.audioTap = value } + } public let nowPlayingInfoController: NowPlayingInfoControllerProtocol public let remoteCommandController: RemoteCommandController diff --git a/Sources/SwiftAudioEx/AudioTap.swift b/Sources/SwiftAudioEx/AudioTap.swift new file mode 100644 index 0000000..403404d --- /dev/null +++ b/Sources/SwiftAudioEx/AudioTap.swift @@ -0,0 +1,63 @@ +// +// AudioTap.swift +// +// +// Created by Brandon Sneed on 3/31/24. +// + +import Foundation +import AVFoundation + +public protocol AudioTap { + func initialize() + func finalize() + func prepare(description: AudioStreamBasicDescription) + func unprepare() + func process(numberOfFrames: Int, buffer: UnsafeMutableAudioBufferListPointer) +} + +extension AVPlayerWrapper { + internal func attachTap(_ tap: AudioTap?, to item: AVPlayerItem) { + guard let tap else { return } + guard let track = item.asset.tracks(withMediaType: .audio).first else { + return + } + + let audioMix = AVMutableAudioMix() + let params = AVMutableAudioMixInputParameters(track: track) + var callbacks = MTAudioProcessingTapCallbacks(version: kMTAudioProcessingTapCallbacksVersion_0, clientInfo: nil) + { tapRef, _, tapStorageOut in + // initialize + print("tap initialized") + } finalize: { tapRef in + // clean up + print("tap finalized") + } prepare: { tapRef, maxFrames, processingFormat in + // allocate memory for sound processing + } unprepare: { tapRef in + // deallocate memory for sound processing + } process: { tapRef, numberFrames, flags, bufferListInOut, numberFramesOut, flagsOut in + guard noErr == MTAudioProcessingTapGetSourceAudio(tapRef, numberFrames, bufferListInOut, flagsOut, nil, numberFramesOut) else { + return + } + + // retrieve AudioBuffer using UnsafeMutableAudioBufferListPointer + for buffer in UnsafeMutableAudioBufferListPointer(bufferListInOut) { + // process audio samples here + //memset(buffer.mData, 0, Int(buffer.mDataByteSize)) + } + print("tap processed") + } + + var tapRef: Unmanaged? + let error = MTAudioProcessingTapCreate(kCFAllocatorDefault, &callbacks, kMTAudioProcessingTapCreationFlag_PreEffects, &tapRef) + assert(error == noErr) + + params.audioTapProcessor = tapRef?.takeUnretainedValue() + tapRef?.release() + + audioMix.inputParameters = [params] + item.audioMix = audioMix + } +} + diff --git a/Sources/SwiftAudioEx/Observer/AVPlayerItemObserver.swift b/Sources/SwiftAudioEx/Observer/AVPlayerItemObserver.swift index f46e3d3..4b93d0b 100644 --- a/Sources/SwiftAudioEx/Observer/AVPlayerItemObserver.swift +++ b/Sources/SwiftAudioEx/Observer/AVPlayerItemObserver.swift @@ -63,6 +63,7 @@ class AVPlayerItemObserver: NSObject { self.isObserving = true self.observingItem = item + item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context) item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, options: [.new], context: &AVPlayerItemObserver.context) item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.playbackLikelyToKeepUp, options: [.new], context: &AVPlayerItemObserver.context) @@ -79,6 +80,9 @@ class AVPlayerItemObserver: NSObject { return } + // BKS: remove a tap if we had one. + observingItem.audioMix = nil + observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context) observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, context: &AVPlayerItemObserver.context) observingItem.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.playbackLikelyToKeepUp, context: &AVPlayerItemObserver.context) From 128fa0f52d73e6a6093e16b92f6be4b5040b4659 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Mon, 1 Apr 2024 11:58:06 -0700 Subject: [PATCH 04/17] Fixed warnings --- Sources/SwiftAudioEx/QueueManager.swift | 33 ++++++++++++------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/Sources/SwiftAudioEx/QueueManager.swift b/Sources/SwiftAudioEx/QueueManager.swift index 8d968a7..8cf58b6 100755 --- a/Sources/SwiftAudioEx/QueueManager.swift +++ b/Sources/SwiftAudioEx/QueueManager.swift @@ -13,7 +13,7 @@ protocol QueueManagerDelegate: AnyObject { func onSkippedToSameCurrentItem() } -class QueueManager { +class QueueManager { fileprivate let recursiveLock = NSRecursiveLock() @@ -54,7 +54,7 @@ class QueueManager { /** All items held by the queue. */ - private(set) var items: [T] = [] { + private(set) var items: [Element] = [] { didSet { return synchronize { if oldValue.count == 0 && items.count > 0 { @@ -64,7 +64,7 @@ class QueueManager { } } - public var nextItems: [T] { + public var nextItems: [Element] { return synchronize { return currentIndex == -1 || currentIndex == items.count - 1 ? [] @@ -72,7 +72,7 @@ class QueueManager { } } - public var previousItems: [T] { + public var previousItems: [Element] { return synchronize { return currentIndex <= 0 ? [] @@ -83,7 +83,7 @@ class QueueManager { /** The current item for the queue. */ - public var current: T? { + public var current: Element? { return synchronize { return 0 <= _currentIndex && _currentIndex < items.count ? items[_currentIndex] : nil } @@ -114,7 +114,7 @@ class QueueManager { - parameter item: The `AudioItem` to be added. */ - public func add(_ item: T) { + public func add(_ item: Element) { synchronize { items.append(item) } @@ -125,7 +125,7 @@ class QueueManager { - parameter items: The `AudioItem`s to be added. */ - public func add(_ items: [T]) { + public func add(_ items: [Element]) { synchronize { if (items.count == 0) { return } self.items.append(contentsOf: items) @@ -138,7 +138,7 @@ class QueueManager { - parameter items: The `AudioItem`s to be added. - parameter at: The index to insert the items at. */ - public func add(_ items: [T], at index: Int) throws { + public func add(_ items: [Element], at index: Int) throws { try synchronizeThrows { if (items.count == 0) { return } guard index >= 0 && self.items.count >= index else { @@ -157,7 +157,7 @@ class QueueManager { case previous = -1 } - private func skip(direction: SkipDirection, wrap: Bool) -> T? { + private func skip(direction: SkipDirection, wrap: Bool) -> Element? { let count = items.count if (current == nil || count == 0) { return nil @@ -174,9 +174,7 @@ class QueueManager { let oldIndex = currentIndex currentIndex = max(0, min(items.count - 1, index)) if (oldIndex != currentIndex) { - defer { - delegate?.onCurrentItemChanged() - } + delegate?.onCurrentItemChanged() } } return current @@ -188,7 +186,7 @@ class QueueManager { - returns: The next (or current) item. */ @discardableResult - public func next(wrap: Bool = false) -> T? { + public func next(wrap: Bool = false) -> Element? { synchronize { return skip(direction: SkipDirection.next, wrap: wrap); } @@ -201,7 +199,7 @@ class QueueManager { - returns: The previous item. */ @discardableResult - public func previous(wrap: Bool = false) -> T? { + public func previous(wrap: Bool = false) -> Element? { return synchronize { return skip(direction: SkipDirection.previous, wrap: wrap); } @@ -216,7 +214,7 @@ class QueueManager { - returns: The item at the index. */ @discardableResult - public func jump(to index: Int) throws -> T { + public func jump(to index: Int) throws -> Element { var skippedToSameCurrentItem = false var currentItemChanged = false let result = try synchronizeThrows { @@ -268,7 +266,8 @@ class QueueManager { - throws: AudioPlayerError.QueueError - returns: The removed item. */ - public func removeItem(at index: Int) throws -> T { + @discardableResult + public func removeItem(at index: Int) throws -> Element { var currentItemChanged = false let result = try synchronizeThrows { try throwIfQueueEmpty() @@ -294,7 +293,7 @@ class QueueManager { - parameter item: The item to set as the new current item. */ - public func replaceCurrentItem(with item: T) { + public func replaceCurrentItem(with item: Element) { var currentItemChanged = false synchronize { if currentIndex == -1 { From cbfcdbf10513b520af80b6f567824c2ffca09ed4 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Mon, 1 Apr 2024 12:05:21 -0700 Subject: [PATCH 05/17] Fix test mocks for macOS --- Tests/SwiftAudioExTests/Mocks/AudioSession.swift | 4 ++++ Tests/SwiftAudioExTests/Utils/Resources.swift | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Tests/SwiftAudioExTests/Mocks/AudioSession.swift b/Tests/SwiftAudioExTests/Mocks/AudioSession.swift index 4ecbed5..dc4aeac 100644 --- a/Tests/SwiftAudioExTests/Mocks/AudioSession.swift +++ b/Tests/SwiftAudioExTests/Mocks/AudioSession.swift @@ -9,6 +9,8 @@ import Foundation import AVFoundation +#if os(iOS) + @testable import SwiftAudioEx @@ -64,3 +66,5 @@ class FailingAudioSession: AudioSession { } + +#endif diff --git a/Tests/SwiftAudioExTests/Utils/Resources.swift b/Tests/SwiftAudioExTests/Utils/Resources.swift index 236b1ee..e585ae6 100644 --- a/Tests/SwiftAudioExTests/Utils/Resources.swift +++ b/Tests/SwiftAudioExTests/Utils/Resources.swift @@ -1,13 +1,12 @@ import Foundation import SwiftAudioEx -import UIKit struct Source { static let path: String = Bundle.module.path(forResource: "TestSound", ofType: "m4a")! static let url: URL = URL(fileURLWithPath: Source.path) static func getAudioItem() -> AudioItem { - return DefaultAudioItem(audioUrl: self.path, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .file, artwork: UIImage()) + return DefaultAudioItem(audioUrl: self.path, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .file, artwork: AudioItemImage()) } } From b29ac53f55a78d52f694ba6118e3e7715eb58d07 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Mon, 1 Apr 2024 12:07:24 -0700 Subject: [PATCH 06/17] Disable session tests in macOS --- Tests/SwiftAudioExTests/AudioSessionControllerTests.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tests/SwiftAudioExTests/AudioSessionControllerTests.swift b/Tests/SwiftAudioExTests/AudioSessionControllerTests.swift index 6c16aa1..337fbf0 100644 --- a/Tests/SwiftAudioExTests/AudioSessionControllerTests.swift +++ b/Tests/SwiftAudioExTests/AudioSessionControllerTests.swift @@ -1,5 +1,8 @@ import XCTest import AVFoundation + +#if os(iOS) + @testable import SwiftAudioEx class AudioSessionControllerTests: XCTestCase { @@ -89,3 +92,5 @@ class AudioSessionControllerDelegateImplementation: AudioSessionControllerDelega self.interruptionType = type } } + +#endif From 7fe0be074a6c65dc0eacaf295d8c552a6f9e992a Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Mon, 1 Apr 2024 12:53:59 -0700 Subject: [PATCH 07/17] Added test --- Sources/SwiftAudioEx/AudioTap.swift | 69 ++++++++++++++----- .../SwiftAudioExTests/AudioPlayerTests.swift | 31 +++++++++ .../Mocks/DummyAudioTap.swift | 40 +++++++++++ 3 files changed, 123 insertions(+), 17 deletions(-) create mode 100644 Tests/SwiftAudioExTests/Mocks/DummyAudioTap.swift diff --git a/Sources/SwiftAudioEx/AudioTap.swift b/Sources/SwiftAudioEx/AudioTap.swift index 403404d..f350269 100644 --- a/Sources/SwiftAudioEx/AudioTap.swift +++ b/Sources/SwiftAudioEx/AudioTap.swift @@ -8,12 +8,34 @@ import Foundation import AVFoundation -public protocol AudioTap { - func initialize() - func finalize() - func prepare(description: AudioStreamBasicDescription) - func unprepare() - func process(numberOfFrames: Int, buffer: UnsafeMutableAudioBufferListPointer) +/** + Subclass this and set the AudioPlayer's `audioTap` property to start receiving the + audio stream. + */ +open class AudioTap { + // Called at tap initialization for a given player item. Use this to setup anything you might need. + open func initialize() { print("audioTap: initialize") } + // Called at teardown of the internal tap. Use this to reset any memory buffers you have created, etc. + open func finalize() { print("audioTap: finalize") } + // Called just before playback so you can perform setup based on the stream description. + open func prepare(description: AudioStreamBasicDescription) { print("audioTap: prepare") } + // Called just before finalize. + open func unprepare() { print("audioTap: unprepare") } + /** + Called periodically during audio stream playback. + + Example: + + ``` + func process(numberOfFrames: Int, buffer: UnsafeMutableAudioBufferListPointer) { + for channel in buffer { + // process audio samples here + //memset(channel.mData, 0, Int(channel.mDataByteSize)) + } + } + ``` + */ + open func process(numberOfFrames: Int, buffer: UnsafeMutableAudioBufferListPointer) { print("audioTap: process") } } extension AVPlayerWrapper { @@ -25,28 +47,41 @@ extension AVPlayerWrapper { let audioMix = AVMutableAudioMix() let params = AVMutableAudioMixInputParameters(track: track) - var callbacks = MTAudioProcessingTapCallbacks(version: kMTAudioProcessingTapCallbacksVersion_0, clientInfo: nil) - { tapRef, _, tapStorageOut in - // initialize - print("tap initialized") + + // we need to retain this pointer so it doesn't disappear out from under us. + // we'll then let it go after we finalize. If the tap changed upstream, we + // aren't going to pick up the new one until after this player item goes away. + let client = UnsafeMutableRawPointer(Unmanaged.passRetained(tap).toOpaque()) + + var callbacks = MTAudioProcessingTapCallbacks(version: kMTAudioProcessingTapCallbacksVersion_0, clientInfo: client) + { tapRef, clientInfo, tapStorageOut in + // initial tap setup + guard let clientInfo else { return } + tapStorageOut.pointee = clientInfo + let audioTap = Unmanaged.fromOpaque(clientInfo).takeUnretainedValue() + audioTap.initialize() } finalize: { tapRef in // clean up - print("tap finalized") + let audioTap = Unmanaged.fromOpaque(MTAudioProcessingTapGetStorage(tapRef)).takeUnretainedValue() + audioTap.finalize() + // we're done, we can let go of the pointer we retained. + Unmanaged.passUnretained(audioTap).release() } prepare: { tapRef, maxFrames, processingFormat in // allocate memory for sound processing + let audioTap = Unmanaged.fromOpaque(MTAudioProcessingTapGetStorage(tapRef)).takeUnretainedValue() + audioTap.prepare(description: processingFormat.pointee) } unprepare: { tapRef in // deallocate memory for sound processing + let audioTap = Unmanaged.fromOpaque(MTAudioProcessingTapGetStorage(tapRef)).takeUnretainedValue() + audioTap.unprepare() } process: { tapRef, numberFrames, flags, bufferListInOut, numberFramesOut, flagsOut in guard noErr == MTAudioProcessingTapGetSourceAudio(tapRef, numberFrames, bufferListInOut, flagsOut, nil, numberFramesOut) else { return } - // retrieve AudioBuffer using UnsafeMutableAudioBufferListPointer - for buffer in UnsafeMutableAudioBufferListPointer(bufferListInOut) { - // process audio samples here - //memset(buffer.mData, 0, Int(buffer.mDataByteSize)) - } - print("tap processed") + // process sound data + let audioTap = Unmanaged.fromOpaque(MTAudioProcessingTapGetStorage(tapRef)).takeUnretainedValue() + audioTap.process(numberOfFrames: numberFrames, buffer: UnsafeMutableAudioBufferListPointer(bufferListInOut)) } var tapRef: Unmanaged? diff --git a/Tests/SwiftAudioExTests/AudioPlayerTests.swift b/Tests/SwiftAudioExTests/AudioPlayerTests.swift index f71bb2e..19ff4ab 100644 --- a/Tests/SwiftAudioExTests/AudioPlayerTests.swift +++ b/Tests/SwiftAudioExTests/AudioPlayerTests.swift @@ -110,6 +110,37 @@ class AudioPlayerTests: XCTestCase { XCTAssertEqual(audioPlayer.duration, 0) } + // MARK: - Audio Tap testing + + func testAudioTapSwitching() { + listener.onSecondsElapse = { position in + if position > 4 { + // swap it out part-way through the first track. + self.audioPlayer.audioTap = DummyAudioTap(tapIndex: 2) + } + } + + audioPlayer.audioTap = DummyAudioTap(tapIndex: 1) + audioPlayer.load(item: FiveSecondSource.getAudioItem()) + audioPlayer.play() + + RunLoop.current.run(until: Date(timeIntervalSinceNow: 6)) + + audioPlayer.load(item: FiveSecondSource.getAudioItem()) + audioPlayer.play() + + RunLoop.current.run(until: Date(timeIntervalSinceNow: 6)) + + let tap1Active = DummyAudioTap.outputs.contains { output in + return output.contains("audioTap 1: process") + } + + let tap2Active = DummyAudioTap.outputs.contains { output in + return output.contains("audioTap 2: process") + } + XCTAssertTrue(tap2Active) + } + // MARK: - Failure func testFailEventOnLoadWithNonMalformedURL() { diff --git a/Tests/SwiftAudioExTests/Mocks/DummyAudioTap.swift b/Tests/SwiftAudioExTests/Mocks/DummyAudioTap.swift new file mode 100644 index 0000000..e952e62 --- /dev/null +++ b/Tests/SwiftAudioExTests/Mocks/DummyAudioTap.swift @@ -0,0 +1,40 @@ +// +// File.swift +// +// +// Created by Brandon Sneed on 4/1/24. +// + +import Foundation +import CoreAudio +@testable import SwiftAudioEx + +class DummyAudioTap: AudioTap { + static var outputs = [String]() + + let tapIndex: Int + + init(tapIndex: Int) { + self.tapIndex = tapIndex + } + + override func initialize() { + Self.outputs.append("audioTap \(tapIndex): initialize") + } + + override func finalize() { + Self.outputs.append("audioTap \(tapIndex): finalize") + } + + override func prepare(description: AudioStreamBasicDescription) { + Self.outputs.append("audioTap \(tapIndex): prepare") + } + + override func unprepare() { + Self.outputs.append("audioTap \(tapIndex): unprepare") + } + + override func process(numberOfFrames: Int, buffer: UnsafeMutableAudioBufferListPointer) { + Self.outputs.append("audioTap \(tapIndex): process") + } +} From d287cf065c3b8b3ff20963a02529706c5495f7b1 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Mon, 1 Apr 2024 11:58:06 -0700 Subject: [PATCH 08/17] Fixed warnings --- Sources/SwiftAudioEx/QueueManager.swift | 33 ++++++++++++------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/Sources/SwiftAudioEx/QueueManager.swift b/Sources/SwiftAudioEx/QueueManager.swift index 8d968a7..8cf58b6 100755 --- a/Sources/SwiftAudioEx/QueueManager.swift +++ b/Sources/SwiftAudioEx/QueueManager.swift @@ -13,7 +13,7 @@ protocol QueueManagerDelegate: AnyObject { func onSkippedToSameCurrentItem() } -class QueueManager { +class QueueManager { fileprivate let recursiveLock = NSRecursiveLock() @@ -54,7 +54,7 @@ class QueueManager { /** All items held by the queue. */ - private(set) var items: [T] = [] { + private(set) var items: [Element] = [] { didSet { return synchronize { if oldValue.count == 0 && items.count > 0 { @@ -64,7 +64,7 @@ class QueueManager { } } - public var nextItems: [T] { + public var nextItems: [Element] { return synchronize { return currentIndex == -1 || currentIndex == items.count - 1 ? [] @@ -72,7 +72,7 @@ class QueueManager { } } - public var previousItems: [T] { + public var previousItems: [Element] { return synchronize { return currentIndex <= 0 ? [] @@ -83,7 +83,7 @@ class QueueManager { /** The current item for the queue. */ - public var current: T? { + public var current: Element? { return synchronize { return 0 <= _currentIndex && _currentIndex < items.count ? items[_currentIndex] : nil } @@ -114,7 +114,7 @@ class QueueManager { - parameter item: The `AudioItem` to be added. */ - public func add(_ item: T) { + public func add(_ item: Element) { synchronize { items.append(item) } @@ -125,7 +125,7 @@ class QueueManager { - parameter items: The `AudioItem`s to be added. */ - public func add(_ items: [T]) { + public func add(_ items: [Element]) { synchronize { if (items.count == 0) { return } self.items.append(contentsOf: items) @@ -138,7 +138,7 @@ class QueueManager { - parameter items: The `AudioItem`s to be added. - parameter at: The index to insert the items at. */ - public func add(_ items: [T], at index: Int) throws { + public func add(_ items: [Element], at index: Int) throws { try synchronizeThrows { if (items.count == 0) { return } guard index >= 0 && self.items.count >= index else { @@ -157,7 +157,7 @@ class QueueManager { case previous = -1 } - private func skip(direction: SkipDirection, wrap: Bool) -> T? { + private func skip(direction: SkipDirection, wrap: Bool) -> Element? { let count = items.count if (current == nil || count == 0) { return nil @@ -174,9 +174,7 @@ class QueueManager { let oldIndex = currentIndex currentIndex = max(0, min(items.count - 1, index)) if (oldIndex != currentIndex) { - defer { - delegate?.onCurrentItemChanged() - } + delegate?.onCurrentItemChanged() } } return current @@ -188,7 +186,7 @@ class QueueManager { - returns: The next (or current) item. */ @discardableResult - public func next(wrap: Bool = false) -> T? { + public func next(wrap: Bool = false) -> Element? { synchronize { return skip(direction: SkipDirection.next, wrap: wrap); } @@ -201,7 +199,7 @@ class QueueManager { - returns: The previous item. */ @discardableResult - public func previous(wrap: Bool = false) -> T? { + public func previous(wrap: Bool = false) -> Element? { return synchronize { return skip(direction: SkipDirection.previous, wrap: wrap); } @@ -216,7 +214,7 @@ class QueueManager { - returns: The item at the index. */ @discardableResult - public func jump(to index: Int) throws -> T { + public func jump(to index: Int) throws -> Element { var skippedToSameCurrentItem = false var currentItemChanged = false let result = try synchronizeThrows { @@ -268,7 +266,8 @@ class QueueManager { - throws: AudioPlayerError.QueueError - returns: The removed item. */ - public func removeItem(at index: Int) throws -> T { + @discardableResult + public func removeItem(at index: Int) throws -> Element { var currentItemChanged = false let result = try synchronizeThrows { try throwIfQueueEmpty() @@ -294,7 +293,7 @@ class QueueManager { - parameter item: The item to set as the new current item. */ - public func replaceCurrentItem(with item: T) { + public func replaceCurrentItem(with item: Element) { var currentItemChanged = false synchronize { if currentIndex == -1 { From 0d6d0ec18b11529726dfb0863d0f0568a27be8ac Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Mon, 1 Apr 2024 12:05:21 -0700 Subject: [PATCH 09/17] Fix test mocks for macOS --- Tests/SwiftAudioExTests/Mocks/AudioSession.swift | 4 ++++ Tests/SwiftAudioExTests/Utils/Resources.swift | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Tests/SwiftAudioExTests/Mocks/AudioSession.swift b/Tests/SwiftAudioExTests/Mocks/AudioSession.swift index 4ecbed5..dc4aeac 100644 --- a/Tests/SwiftAudioExTests/Mocks/AudioSession.swift +++ b/Tests/SwiftAudioExTests/Mocks/AudioSession.swift @@ -9,6 +9,8 @@ import Foundation import AVFoundation +#if os(iOS) + @testable import SwiftAudioEx @@ -64,3 +66,5 @@ class FailingAudioSession: AudioSession { } + +#endif diff --git a/Tests/SwiftAudioExTests/Utils/Resources.swift b/Tests/SwiftAudioExTests/Utils/Resources.swift index 236b1ee..e585ae6 100644 --- a/Tests/SwiftAudioExTests/Utils/Resources.swift +++ b/Tests/SwiftAudioExTests/Utils/Resources.swift @@ -1,13 +1,12 @@ import Foundation import SwiftAudioEx -import UIKit struct Source { static let path: String = Bundle.module.path(forResource: "TestSound", ofType: "m4a")! static let url: URL = URL(fileURLWithPath: Source.path) static func getAudioItem() -> AudioItem { - return DefaultAudioItem(audioUrl: self.path, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .file, artwork: UIImage()) + return DefaultAudioItem(audioUrl: self.path, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .file, artwork: AudioItemImage()) } } From cbb4928aff4ad4ce3aa26bc772cabcafb50c1dd2 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Mon, 1 Apr 2024 12:07:24 -0700 Subject: [PATCH 10/17] Disable session tests in macOS --- Tests/SwiftAudioExTests/AudioSessionControllerTests.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tests/SwiftAudioExTests/AudioSessionControllerTests.swift b/Tests/SwiftAudioExTests/AudioSessionControllerTests.swift index 6c16aa1..337fbf0 100644 --- a/Tests/SwiftAudioExTests/AudioSessionControllerTests.swift +++ b/Tests/SwiftAudioExTests/AudioSessionControllerTests.swift @@ -1,5 +1,8 @@ import XCTest import AVFoundation + +#if os(iOS) + @testable import SwiftAudioEx class AudioSessionControllerTests: XCTestCase { @@ -89,3 +92,5 @@ class AudioSessionControllerDelegateImplementation: AudioSessionControllerDelega self.interruptionType = type } } + +#endif From ec2c8dfcd5bc8669054221595aec0094e83d9207 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Mon, 1 Apr 2024 15:15:15 -0700 Subject: [PATCH 11/17] Updated test --- Sources/SwiftAudioEx/Utils/Devices.swift | 8 ++++++++ Tests/SwiftAudioExTests/AudioPlayerTests.swift | 1 + 2 files changed, 9 insertions(+) create mode 100644 Sources/SwiftAudioEx/Utils/Devices.swift diff --git a/Sources/SwiftAudioEx/Utils/Devices.swift b/Sources/SwiftAudioEx/Utils/Devices.swift new file mode 100644 index 0000000..21bccf0 --- /dev/null +++ b/Sources/SwiftAudioEx/Utils/Devices.swift @@ -0,0 +1,8 @@ +// +// File.swift +// +// +// Created by Brandon Sneed on 4/1/24. +// + +import Foundation diff --git a/Tests/SwiftAudioExTests/AudioPlayerTests.swift b/Tests/SwiftAudioExTests/AudioPlayerTests.swift index 19ff4ab..4fe2ab9 100644 --- a/Tests/SwiftAudioExTests/AudioPlayerTests.swift +++ b/Tests/SwiftAudioExTests/AudioPlayerTests.swift @@ -138,6 +138,7 @@ class AudioPlayerTests: XCTestCase { let tap2Active = DummyAudioTap.outputs.contains { output in return output.contains("audioTap 2: process") } + XCTAssertTrue(tap1Active) XCTAssertTrue(tap2Active) } From 7ace7651bf67aa592d3909a92e263a3dcb8d11f5 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Tue, 2 Apr 2024 14:23:58 -0700 Subject: [PATCH 12/17] Initial devices & output --- .../AVPlayerWrapper/AVPlayerWrapper.swift | 2 +- Sources/SwiftAudioEx/Utils/Devices.swift | 170 ++++++++++++++++++ .../SwiftAudioExTests/AudioPlayerTests.swift | 7 + 3 files changed, 178 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift b/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift index d08866f..5291c5b 100755 --- a/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift +++ b/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift @@ -23,7 +23,7 @@ public enum PlaybackEndedReason: String { class AVPlayerWrapper: AVPlayerWrapperProtocol { // MARK: - Properties - fileprivate var avPlayer = AVPlayer() + internal var avPlayer = AVPlayer() internal var audioTap: AudioTap? = nil private let playerObserver = AVPlayerObserver() internal let playerTimeObserver: AVPlayerTimeObserver diff --git a/Sources/SwiftAudioEx/Utils/Devices.swift b/Sources/SwiftAudioEx/Utils/Devices.swift index 21bccf0..c55fa91 100644 --- a/Sources/SwiftAudioEx/Utils/Devices.swift +++ b/Sources/SwiftAudioEx/Utils/Devices.swift @@ -6,3 +6,173 @@ // import Foundation +import AVFoundation + +public class AudioDevice { + static var system: AudioDevice = { + return AudioDevice() + }() + + public let deviceID: AudioDeviceID? + public let uniqueID: String? + public let name: String? + + internal init(deviceID: AudioDeviceID) { + self.deviceID = deviceID + self.uniqueID = Self.propertyValue(deviceID: deviceID, selector: AudioObjectPropertySelector(kAudioDevicePropertyDeviceUID)) + self.name = Self.propertyValue(deviceID: deviceID, selector: AudioObjectPropertySelector(kAudioDevicePropertyDeviceNameCFString)) + } + + internal init() { + self.deviceID = 0 + self.uniqueID = nil + self.name = "System" + } +} + +extension AudioDevice { + static func hasOutput(deviceID: AudioDeviceID) -> Bool { + var status: OSStatus = 0 + var address = AudioObjectPropertyAddress( + mSelector: AudioObjectPropertySelector(kAudioDevicePropertyStreamConfiguration), + mScope: AudioObjectPropertyScope(kAudioObjectPropertyScopeOutput), + mElement: 0) + + var size: UInt32 = 0 + withUnsafeMutablePointer(to: &size) { size in + withUnsafePointer(to: &address) { addressPtr in + status = AudioObjectGetPropertyDataSize(deviceID, addressPtr, 0, nil, size) + } + } + + if status != 0 { + // we weren't able to get the size + return false + } + + let bufferList = UnsafeMutablePointer.allocate(capacity: Int(size)) + withUnsafeMutablePointer(to: &size) { size in + withUnsafePointer(to: &address) { addressPtr in + status = AudioObjectGetPropertyData(deviceID, addressPtr, 0, nil, size, bufferList) + } + } + + if status != 0 { + // we couldn't get the buffer list + return false + } + + let buffers = UnsafeMutableAudioBufferListPointer(bufferList) + for buffer in buffers { + if buffer.mNumberChannels > 0 { + return true + } + } + + return false + } + + static internal func propertyValue(deviceID: AudioDeviceID, selector: AudioObjectPropertySelector) -> String? { + var result: String? = nil + + var address = AudioObjectPropertyAddress( + mSelector: selector, + mScope: AudioObjectPropertyScope(kAudioObjectPropertyScopeGlobal), + mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMain)) + + var name: Unmanaged? + var size = UInt32(MemoryLayout.size) + withUnsafeMutablePointer(to: &size) { size in + withUnsafePointer(to: &address) { addressPtr in + let status = AudioObjectGetPropertyData(deviceID, addressPtr, 0, nil, size, &name) + if status != 0 { + return + } + result = name?.takeUnretainedValue() as String? + } + } + + return result + } +} + +extension AudioPlayer { + /** + Set the output device for the Player. Default is system. + */ + public func setOutputDevice(_ device: AudioDevice) { + guard let wrapper = wrapper as? AVPlayerWrapper else { return } + wrapper.avPlayer.audioOutputDeviceUniqueID = device.uniqueID + } + + /** + Get the current output device + */ + + /** + Get a list of local audio devices capable of output. + + This list will *NOT* include AirPlay devices. For Airplay and other streaming + audio devices, see AVRoutePickerView. + */ + public var localDevices: [AudioDevice] { + get { + var status: OSStatus = 0 + var address = AudioObjectPropertyAddress( + mSelector: AudioObjectPropertySelector(kAudioHardwarePropertyDevices), + mScope: AudioObjectPropertyScope(kAudioObjectPropertyScopeGlobal), + mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)) + + var size: UInt32 = 0 + withUnsafeMutablePointer(to: &size) { size in + withUnsafePointer(to: &address) { address in + status = AudioObjectGetPropertyDataSize( + AudioObjectID(kAudioObjectSystemObject), + address, + UInt32(MemoryLayout.size), + nil, + size) + } + } + + if status != 0 { + // we couldn't get a data size + return [] + } + + let deviceCount = size / UInt32(MemoryLayout.size) + var deviceIDs = [AudioDeviceID]() + for _ in 0.. Date: Thu, 4 Apr 2024 11:43:45 -0700 Subject: [PATCH 13/17] Added support for specifying/listing output devices --- Sources/SwiftAudioEx/Utils/Devices.swift | 28 +++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftAudioEx/Utils/Devices.swift b/Sources/SwiftAudioEx/Utils/Devices.swift index c55fa91..6e43907 100644 --- a/Sources/SwiftAudioEx/Utils/Devices.swift +++ b/Sources/SwiftAudioEx/Utils/Devices.swift @@ -7,8 +7,17 @@ import Foundation import AVFoundation +import CoreAudio -public class AudioDevice { +public class AudioDevice: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { + return name ?? "Unknown" + } + + public var debugDescription: String { + return name ?? "Unknown" + } + static var system: AudioDevice = { return AudioDevice() }() @@ -108,6 +117,23 @@ extension AudioPlayer { /** Get the current output device */ + public var outputDevice: AudioDevice { + get { + guard let wrapper = wrapper as? AVPlayerWrapper else { return AudioDevice.system } + guard let uniqueID = wrapper.avPlayer.audioOutputDeviceUniqueID else { return AudioDevice.system } + let devices = localDevices.filter { device in + return device.uniqueID == uniqueID + } + if let match = devices.first { + return match + } + return AudioDevice.system + } + set(value) { + guard let wrapper = wrapper as? AVPlayerWrapper else { return } + wrapper.avPlayer.audioOutputDeviceUniqueID = value.uniqueID + } + } /** Get a list of local audio devices capable of output. From 30825d174491eb6ab6682079a0d0a15c29894649 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Sun, 7 Apr 2024 10:05:39 -0700 Subject: [PATCH 14/17] Fixed bugs --- Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift | 6 +++++- Sources/SwiftAudioEx/AudioItem.swift | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift b/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift index 5291c5b..f451141 100755 --- a/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift +++ b/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift @@ -70,7 +70,10 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { let currentState = self._state if (currentState != newValue) { self._state = newValue - self.delegate?.AVWrapper(didChangeState: newValue) + // the delegate can initiate a state change, resulting in a dealock in the getter. + DispatchQueue.main.async { + self.delegate?.AVWrapper(didChangeState: newValue) + } } } } @@ -332,6 +335,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { func load( from url: String, + next: String? = nil, type: SourceType = .stream, playWhenReady: Bool = false, initialTime: TimeInterval? = nil, diff --git a/Sources/SwiftAudioEx/AudioItem.swift b/Sources/SwiftAudioEx/AudioItem.swift index 833afe3..12e4946 100755 --- a/Sources/SwiftAudioEx/AudioItem.swift +++ b/Sources/SwiftAudioEx/AudioItem.swift @@ -29,7 +29,7 @@ public protocol AudioItem { func getAlbumTitle() -> String? func getSourceType() -> SourceType func getArtwork(_ handler: @escaping (AudioItemImage?) -> Void) - + func getArtworkURL() -> URL? } /// Make your `AudioItem`-subclass conform to this protocol to control which AVAudioTimePitchAlgorithm is used for each item. @@ -96,6 +96,9 @@ public class DefaultAudioItem: AudioItem { handler(artwork) } + public func getArtworkURL() -> URL? { + return nil + } } /// An AudioItem that also conforms to the `TimePitching`-protocol From c2f1f1b307fed54e53cb4635fc860c53ccf564f0 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Sun, 7 Apr 2024 10:07:49 -0700 Subject: [PATCH 15/17] Fixed error --- Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift b/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift index f451141..bc199f6 100755 --- a/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift +++ b/Sources/SwiftAudioEx/AVPlayerWrapper/AVPlayerWrapper.swift @@ -335,7 +335,6 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { func load( from url: String, - next: String? = nil, type: SourceType = .stream, playWhenReady: Bool = false, initialTime: TimeInterval? = nil, From 5426306cb1fe3f8dd01f5c90aba2b129493f252f Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Thu, 11 Apr 2024 11:00:55 -0700 Subject: [PATCH 16/17] Added device list test comment --- Tests/SwiftAudioExTests/AudioPlayerTests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/SwiftAudioExTests/AudioPlayerTests.swift b/Tests/SwiftAudioExTests/AudioPlayerTests.swift index 964cf8c..bf73127 100644 --- a/Tests/SwiftAudioExTests/AudioPlayerTests.swift +++ b/Tests/SwiftAudioExTests/AudioPlayerTests.swift @@ -145,6 +145,8 @@ class AudioPlayerTests: XCTestCase { // MARK: - Device Tests func testAudioDeviceListing() { + // I know this test kind of stinks. Devices will vary on every system, + // and i can't really test device output in CI. :/ let list = audioPlayer.localDevices print(list) } From 8dd0a04968d2c39a21dfa0da793ecb73b9e22513 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Thu, 11 Apr 2024 11:22:35 -0700 Subject: [PATCH 17/17] Fixed test to account for deadlock fix. --- Tests/SwiftAudioExTests/NowPlayingInfoTests.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tests/SwiftAudioExTests/NowPlayingInfoTests.swift b/Tests/SwiftAudioExTests/NowPlayingInfoTests.swift index 075458e..db6ae8a 100644 --- a/Tests/SwiftAudioExTests/NowPlayingInfoTests.swift +++ b/Tests/SwiftAudioExTests/NowPlayingInfoTests.swift @@ -33,7 +33,12 @@ class NowPlayingInfoTests: XCTestCase { func testNowPlayingInfoControllerPlaybackValuesUpdate() { let item = LongSource.getAudioItem() + + // State has become somewhat async to prevent a deadlock, + // so this isn't instantaneous anymore and needs a teensy bit of time. audioPlayer.load(item: item, playWhenReady: true) + + RunLoop.current.run(until: Date(timeIntervalSinceNow: 1)) XCTAssertNotNil(nowPlayingController.getRate()) XCTAssertNotNil(nowPlayingController.getDuration())