diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml
index e66b82b..2cf0e06 100644
--- a/.github/workflows/swift.yml
+++ b/.github/workflows/swift.yml
@@ -9,34 +9,16 @@ on:
branches:
- '*'
-# jobs:
-# iOS:
-# name: Test iOS
-# runs-on: macOS-latest
-# env:
-# DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
-# strategy:
-# matrix:
-# destination: ["OS=latest,name=iPhone 13 Pro"]
-# steps:
-# - uses: actions/checkout@v2
-# - name: iOS - ${{ matrix.destination }}
-# run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "AudioStreaming.xcodeproj" -scheme "AudioStreaming" -destination "${{ matrix.destination }}" clean test | xcpretty
-
jobs:
- build:
- name: Swift ${{ matrix.swift }} on ${{ matrix.os }}
+ iOS:
+ name: Test iOS
+ runs-on: macOS-latest
+ env:
+ DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
strategy:
matrix:
- os: [macos-latest]
- swift: ["5.9"]
- runs-on: ${{ matrix.os }}
+ destination: ["OS=latest,name=iPhone 15 Pro"]
steps:
- - uses: swift-actions/setup-swift@65540b95f51493d65f5e59e97dcef9629ddf11bf
- with:
- swift-version: ${{ matrix.swift }}
- - uses: actions/checkout@v4
- - name: Build
- run: swift build
- - name: Run tests
- run: swift test
+ - uses: actions/checkout@v2
+ - name: iOS - ${{ matrix.destination }}
+ run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "AudioStreaming.xcodeproj" -scheme "AudioStreaming" -destination "${{ matrix.destination }}" clean test | xcpretty
diff --git a/.swiftpm/xcode/xcshareddata/xcbaselines/AudioStreamingTests.xcbaseline/E340D9FA-D19A-49BB-82AA-9D0E236D4288.plist b/.swiftpm/xcode/xcshareddata/xcbaselines/AudioStreamingTests.xcbaseline/E340D9FA-D19A-49BB-82AA-9D0E236D4288.plist
new file mode 100644
index 0000000..1f88fa8
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcbaselines/AudioStreamingTests.xcbaseline/E340D9FA-D19A-49BB-82AA-9D0E236D4288.plist
@@ -0,0 +1,22 @@
+
+
+
+
+ classNames
+
+ AtomicTests
+
+ testProtectedValuesAreAccessedSafely()
+
+ com.apple.XCTPerformanceMetric_WallClockTime
+
+ baselineAverage
+ 0.029769
+ baselineIntegrationDisplayName
+ Local Baseline
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcbaselines/AudioStreamingTests.xcbaseline/Info.plist b/.swiftpm/xcode/xcshareddata/xcbaselines/AudioStreamingTests.xcbaseline/Info.plist
new file mode 100644
index 0000000..af37023
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcbaselines/AudioStreamingTests.xcbaseline/Info.plist
@@ -0,0 +1,40 @@
+
+
+
+
+ runDestinationsByUUID
+
+ E340D9FA-D19A-49BB-82AA-9D0E236D4288
+
+ localComputer
+
+ busSpeedInMHz
+ 0
+ cpuCount
+ 1
+ cpuKind
+ Apple M1 Pro
+ cpuSpeedInMHz
+ 0
+ logicalCPUCoresPerPackage
+ 10
+ modelCode
+ MacBookPro18,1
+ physicalCPUCoresPerPackage
+ 10
+ platformIdentifier
+ com.apple.platform.macosx
+
+ targetArchitecture
+ arm64
+ targetDevice
+
+ modelCode
+ iPhone16,1
+ platformIdentifier
+ com.apple.platform.iphonesimulator
+
+
+
+
+
diff --git a/AudioExample/.DS_Store b/AudioExample/.DS_Store
deleted file mode 100644
index b6eab89..0000000
Binary files a/AudioExample/.DS_Store and /dev/null differ
diff --git a/AudioExample/AudioExample.xcodeproj/project.pbxproj b/AudioExample/AudioExample.xcodeproj/project.pbxproj
deleted file mode 100644
index 8648cc6..0000000
--- a/AudioExample/AudioExample.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,442 +0,0 @@
-// !$*UTF8*$!
-{
- archiveVersion = 1;
- classes = {
- };
- objectVersion = 50;
- objects = {
-
-/* Begin PBXBuildFile section */
- 984808A028C0F549001160E6 /* hipjazz.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9848089F28C0F549001160E6 /* hipjazz.wav */; };
- 98C82AE22B8CA16A00AED485 /* bensound-jazzyfrenchy.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 98C82AE12B8CA0F000AED485 /* bensound-jazzyfrenchy.m4a */; };
- 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 */; };
- B5220954256188590086FB3A /* EqualizerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5220953256188590086FB3A /* EqualizerViewModel.swift */; };
- B524D59C2560176C00F5A88F /* bensound-jazzyfrenchy.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = B524D59B2560176C00F5A88F /* bensound-jazzyfrenchy.mp3 */; };
- B524D5A12560302100F5A88F /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B524D5A02560302100F5A88F /* PlayerViewController.swift */; };
- B524D5A32560303000F5A88F /* PlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B524D5A22560303000F5A88F /* PlayerViewModel.swift */; };
- B524D5A52560303D00F5A88F /* PlayerControlsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B524D5A42560303D00F5A88F /* PlayerControlsViewController.swift */; };
- B524D5A72560305800F5A88F /* PlayerControlsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B524D5A62560305800F5A88F /* PlayerControlsViewModel.swift */; };
- B524D5A9256031DE00F5A88F /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B524D5A8256031DE00F5A88F /* AppCoordinator.swift */; };
- B524D5AD25604E4B00F5A88F /* PlaylistItemsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B524D5AC25604E4B00F5A88F /* PlaylistItemsService.swift */; };
- B524D5AF25604ED900F5A88F /* AudioContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B524D5AE25604ED900F5A88F /* AudioContent.swift */; };
- B580CB0E2561B912006D7DD8 /* EqualizerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B580CB0D2561B912006D7DD8 /* EqualizerService.swift */; };
- B5AEDBD52475274C007D8101 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5AEDBD42475274C007D8101 /* AppDelegate.swift */; };
- B5AEDBDE2475274D007D8101 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B5AEDBDD2475274D007D8101 /* Assets.xcassets */; };
- B5AEDBE12475274D007D8101 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B5AEDBDF2475274D007D8101 /* LaunchScreen.storyboard */; };
- B5F883C624780A3D00D277C1 /* AudioStreaming.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5F883C524780A3C00D277C1 /* AudioStreaming.framework */; };
- B5F883C724780A3D00D277C1 /* AudioStreaming.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B5F883C524780A3C00D277C1 /* AudioStreaming.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
-/* End PBXBuildFile section */
-
-/* Begin PBXCopyFilesBuildPhase section */
- B5F883C824780A3D00D277C1 /* Embed Frameworks */ = {
- isa = PBXCopyFilesBuildPhase;
- buildActionMask = 2147483647;
- dstPath = "";
- dstSubfolderSpec = 10;
- files = (
- B5F883C724780A3D00D277C1 /* AudioStreaming.framework in Embed Frameworks */,
- );
- name = "Embed Frameworks";
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXCopyFilesBuildPhase section */
-
-/* Begin PBXFileReference section */
- 9848089F28C0F549001160E6 /* hipjazz.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = hipjazz.wav; sourceTree = ""; };
- 98C82AE12B8CA0F000AED485 /* bensound-jazzyfrenchy.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = "bensound-jazzyfrenchy.m4a"; sourceTree = ""; };
- B5220835256051830086FB3A /* AudioPlayerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerService.swift; sourceTree = ""; };
- B5220947256074910086FB3A /* MulticastDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MulticastDelegate.swift; sourceTree = ""; };
- B522094F2561883E0086FB3A /* EqualizerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualizerViewController.swift; sourceTree = ""; };
- B5220953256188590086FB3A /* EqualizerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualizerViewModel.swift; sourceTree = ""; };
- B524D59B2560176C00F5A88F /* bensound-jazzyfrenchy.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "bensound-jazzyfrenchy.mp3"; sourceTree = ""; };
- B524D5A02560302100F5A88F /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = ""; };
- B524D5A22560303000F5A88F /* PlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewModel.swift; sourceTree = ""; };
- B524D5A42560303D00F5A88F /* PlayerControlsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsViewController.swift; sourceTree = ""; };
- B524D5A62560305800F5A88F /* PlayerControlsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsViewModel.swift; sourceTree = ""; };
- B524D5A8256031DE00F5A88F /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; };
- B524D5AC25604E4B00F5A88F /* PlaylistItemsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistItemsService.swift; sourceTree = ""; };
- B524D5AE25604ED900F5A88F /* AudioContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContent.swift; sourceTree = ""; };
- B580CB0D2561B912006D7DD8 /* EqualizerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualizerService.swift; sourceTree = ""; };
- B5AEDBD12475274C007D8101 /* AudioExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AudioExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
- B5AEDBD42475274C007D8101 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
- B5AEDBDD2475274D007D8101 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
- B5AEDBE02475274D007D8101 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
- B5AEDBE22475274D007D8101 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- B5F883C524780A3C00D277C1 /* AudioStreaming.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AudioStreaming.framework; sourceTree = BUILT_PRODUCTS_DIR; };
-/* End PBXFileReference section */
-
-/* Begin PBXFrameworksBuildPhase section */
- B5AEDBCE2475274C007D8101 /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- B5F883C624780A3D00D277C1 /* AudioStreaming.framework in Frameworks */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXFrameworksBuildPhase section */
-
-/* Begin PBXGroup section */
- B524D59D2560177C00F5A88F /* Resources */ = {
- isa = PBXGroup;
- children = (
- 98C82AE12B8CA0F000AED485 /* bensound-jazzyfrenchy.m4a */,
- 9848089F28C0F549001160E6 /* hipjazz.wav */,
- B524D59B2560176C00F5A88F /* bensound-jazzyfrenchy.mp3 */,
- B5AEDBDD2475274D007D8101 /* Assets.xcassets */,
- B5AEDBDF2475274D007D8101 /* LaunchScreen.storyboard */,
- );
- path = Resources;
- sourceTree = "";
- };
- B524D5AA25604E2E00F5A88F /* Services */ = {
- isa = PBXGroup;
- children = (
- B524D5AE25604ED900F5A88F /* AudioContent.swift */,
- B524D5AC25604E4B00F5A88F /* PlaylistItemsService.swift */,
- B5220835256051830086FB3A /* AudioPlayerService.swift */,
- B5220947256074910086FB3A /* MulticastDelegate.swift */,
- B580CB0D2561B912006D7DD8 /* EqualizerService.swift */,
- );
- path = Services;
- sourceTree = "";
- };
- B524D5AB25604E3500F5A88F /* Controllers */ = {
- isa = PBXGroup;
- children = (
- B524D5A02560302100F5A88F /* PlayerViewController.swift */,
- B524D5A22560303000F5A88F /* PlayerViewModel.swift */,
- B524D5A42560303D00F5A88F /* PlayerControlsViewController.swift */,
- B524D5A62560305800F5A88F /* PlayerControlsViewModel.swift */,
- B522094F2561883E0086FB3A /* EqualizerViewController.swift */,
- B5220953256188590086FB3A /* EqualizerViewModel.swift */,
- );
- path = Controllers;
- sourceTree = "";
- };
- B5AEDBC82475274C007D8101 = {
- isa = PBXGroup;
- children = (
- B5AEDBD32475274C007D8101 /* AudioExample */,
- B5AEDBD22475274C007D8101 /* Products */,
- B5F883C424780A3C00D277C1 /* Frameworks */,
- );
- sourceTree = "";
- wrapsLines = 0;
- };
- B5AEDBD22475274C007D8101 /* Products */ = {
- isa = PBXGroup;
- children = (
- B5AEDBD12475274C007D8101 /* AudioExample.app */,
- );
- name = Products;
- sourceTree = "";
- };
- B5AEDBD32475274C007D8101 /* AudioExample */ = {
- isa = PBXGroup;
- children = (
- B5AEDBD42475274C007D8101 /* AppDelegate.swift */,
- B524D5A8256031DE00F5A88F /* AppCoordinator.swift */,
- B524D5AA25604E2E00F5A88F /* Services */,
- B524D5AB25604E3500F5A88F /* Controllers */,
- B524D59D2560177C00F5A88F /* Resources */,
- B5AEDBE22475274D007D8101 /* Info.plist */,
- );
- path = AudioExample;
- sourceTree = "";
- };
- B5F883C424780A3C00D277C1 /* Frameworks */ = {
- isa = PBXGroup;
- children = (
- B5F883C524780A3C00D277C1 /* AudioStreaming.framework */,
- );
- name = Frameworks;
- sourceTree = "";
- };
-/* End PBXGroup section */
-
-/* Begin PBXNativeTarget section */
- B5AEDBD02475274C007D8101 /* AudioExample */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = B5AEDBE52475274D007D8101 /* Build configuration list for PBXNativeTarget "AudioExample" */;
- buildPhases = (
- B5AEDBCD2475274C007D8101 /* Sources */,
- B5AEDBCE2475274C007D8101 /* Frameworks */,
- B5AEDBCF2475274C007D8101 /* Resources */,
- B5F883C824780A3D00D277C1 /* Embed Frameworks */,
- );
- buildRules = (
- );
- dependencies = (
- );
- name = AudioExample;
- productName = AudioExample;
- productReference = B5AEDBD12475274C007D8101 /* AudioExample.app */;
- productType = "com.apple.product-type.application";
- };
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
- B5AEDBC92475274C007D8101 /* Project object */ = {
- isa = PBXProject;
- attributes = {
- LastSwiftUpdateCheck = 1140;
- LastUpgradeCheck = 1200;
- ORGANIZATIONNAME = "Dimitrios Chatzieleftheriou";
- TargetAttributes = {
- B5AEDBD02475274C007D8101 = {
- CreatedOnToolsVersion = 11.4;
- };
- };
- };
- buildConfigurationList = B5AEDBCC2475274C007D8101 /* Build configuration list for PBXProject "AudioExample" */;
- compatibilityVersion = "Xcode 9.3";
- developmentRegion = en;
- hasScannedForEncodings = 0;
- knownRegions = (
- en,
- Base,
- );
- mainGroup = B5AEDBC82475274C007D8101;
- productRefGroup = B5AEDBD22475274C007D8101 /* Products */;
- projectDirPath = "";
- projectRoot = "";
- targets = (
- B5AEDBD02475274C007D8101 /* AudioExample */,
- );
- };
-/* End PBXProject section */
-
-/* Begin PBXResourcesBuildPhase section */
- B5AEDBCF2475274C007D8101 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- B524D59C2560176C00F5A88F /* bensound-jazzyfrenchy.mp3 in Resources */,
- B5AEDBE12475274D007D8101 /* LaunchScreen.storyboard in Resources */,
- B5AEDBDE2475274D007D8101 /* Assets.xcassets in Resources */,
- 98C82AE22B8CA16A00AED485 /* bensound-jazzyfrenchy.m4a in Resources */,
- 984808A028C0F549001160E6 /* hipjazz.wav in Resources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
- B5AEDBCD2475274C007D8101 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- B524D5AF25604ED900F5A88F /* AudioContent.swift in Sources */,
- B524D5A9256031DE00F5A88F /* AppCoordinator.swift in Sources */,
- B524D5AD25604E4B00F5A88F /* PlaylistItemsService.swift in Sources */,
- B524D5A32560303000F5A88F /* PlayerViewModel.swift in Sources */,
- B5220836256051830086FB3A /* AudioPlayerService.swift in Sources */,
- B5AEDBD52475274C007D8101 /* AppDelegate.swift in Sources */,
- B524D5A12560302100F5A88F /* PlayerViewController.swift in Sources */,
- B580CB0E2561B912006D7DD8 /* EqualizerService.swift in Sources */,
- B5220954256188590086FB3A /* EqualizerViewModel.swift in Sources */,
- B5220948256074910086FB3A /* MulticastDelegate.swift in Sources */,
- B524D5A52560303D00F5A88F /* PlayerControlsViewController.swift in Sources */,
- B524D5A72560305800F5A88F /* PlayerControlsViewModel.swift in Sources */,
- B52209502561883E0086FB3A /* EqualizerViewController.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXSourcesBuildPhase section */
-
-/* Begin PBXVariantGroup section */
- B5AEDBDF2475274D007D8101 /* LaunchScreen.storyboard */ = {
- isa = PBXVariantGroup;
- children = (
- B5AEDBE02475274D007D8101 /* Base */,
- );
- name = LaunchScreen.storyboard;
- sourceTree = "";
- };
-/* End PBXVariantGroup section */
-
-/* Begin XCBuildConfiguration section */
- B5AEDBE32475274D007D8101 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_ENABLE_OBJC_WEAK = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- 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;
- CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = dwarf;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_TESTABILITY = YES;
- GCC_C_LANGUAGE_STANDARD = gnu11;
- GCC_DYNAMIC_NO_PIC = NO;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_OPTIMIZATION_LEVEL = 0;
- GCC_PREPROCESSOR_DEFINITIONS = (
- "DEBUG=1",
- "$(inherited)",
- );
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.4;
- MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
- MTL_FAST_MATH = YES;
- ONLY_ACTIVE_ARCH = YES;
- SDKROOT = iphoneos;
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- };
- name = Debug;
- };
- B5AEDBE42475274D007D8101 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_ENABLE_OBJC_WEAK = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- 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;
- CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_NS_ASSERTIONS = NO;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- GCC_C_LANGUAGE_STANDARD = gnu11;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.4;
- MTL_ENABLE_DEBUG_INFO = NO;
- MTL_FAST_MATH = YES;
- SDKROOT = iphoneos;
- SWIFT_COMPILATION_MODE = wholemodule;
- SWIFT_OPTIMIZATION_LEVEL = "-O";
- VALIDATE_PRODUCT = YES;
- };
- name = Release;
- };
- B5AEDBE62475274D007D8101 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CODE_SIGN_STYLE = Manual;
- DEVELOPMENT_TEAM = "";
- INFOPLIST_FILE = AudioExample/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioExample;
- PRODUCT_NAME = "$(TARGET_NAME)";
- PROVISIONING_PROFILE_SPECIFIER = "";
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Debug;
- };
- B5AEDBE72475274D007D8101 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- CODE_SIGN_STYLE = Manual;
- DEVELOPMENT_TEAM = "";
- INFOPLIST_FILE = AudioExample/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioExample;
- PRODUCT_NAME = "$(TARGET_NAME)";
- PROVISIONING_PROFILE_SPECIFIER = "";
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Release;
- };
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
- B5AEDBCC2475274C007D8101 /* Build configuration list for PBXProject "AudioExample" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- B5AEDBE32475274D007D8101 /* Debug */,
- B5AEDBE42475274D007D8101 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- B5AEDBE52475274D007D8101 /* Build configuration list for PBXNativeTarget "AudioExample" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- B5AEDBE62475274D007D8101 /* Debug */,
- B5AEDBE72475274D007D8101 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
-/* End XCConfigurationList section */
- };
- rootObject = B5AEDBC92475274C007D8101 /* Project object */;
-}
diff --git a/AudioExample/AudioExample.xcodeproj/xcuserdata/dimitrisc.xcuserdatad/xcschemes/xcschememanagement.plist b/AudioExample/AudioExample.xcodeproj/xcuserdata/dimitrisc.xcuserdatad/xcschemes/xcschememanagement.plist
deleted file mode 100644
index aa6680a..0000000
--- a/AudioExample/AudioExample.xcodeproj/xcuserdata/dimitrisc.xcuserdatad/xcschemes/xcschememanagement.plist
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
- SchemeUserState
-
- AudioExample.xcscheme_^#shared#^_
-
- orderHint
- 0
-
-
- SuppressBuildableAutocreation
-
- B5AEDBD02475274C007D8101
-
- primary
-
-
-
-
-
diff --git a/AudioExample/AudioExample/AppCoordinator.swift b/AudioExample/AudioExample/AppCoordinator.swift
deleted file mode 100644
index 0749f55..0000000
--- a/AudioExample/AudioExample/AppCoordinator.swift
+++ /dev/null
@@ -1,63 +0,0 @@
-//
-// AppCoordinator.swift
-// AudioExample
-//
-// Created by Dimitrios Chatzieleftheriou on 14/11/2020.
-// Copyright © 2020 Dimitrios Chatzieleftheriou. All rights reserved.
-//
-
-import AVFoundation
-import UIKit
-
-final class AppCoordinator {
- enum Route {
- case equalizer
- }
-
- private var navigationController: UINavigationController?
-
- private let playerService: AudioPlayerService
- private let equaliserService: EqualizerService
-
- init() {
- playerService = AudioPlayerService()
- equaliserService = EqualizerService(playerService: playerService)
- }
-
- func start(window: UIWindow) {
- window.rootViewController = buildMain()
- window.makeKeyAndVisible()
- }
-
- private func buildMain() -> UINavigationController {
- let playlistItemsService = PlaylistItemsService(initialItemsProvider: provideInitialPlaylistItems)
- let viewModel = PlayerViewModel(playlistItemsService: playlistItemsService,
- playerService: playerService,
- routeTo: { [weak self] in self?.routeTo($0) })
- let viewController = PlayerViewController(viewModel: viewModel,
- controlsProvider: providePlayerControls)
-
- let navigationController = UINavigationController(rootViewController: viewController)
- self.navigationController = navigationController
- return navigationController
- }
-
- private func routeTo(_ route: AppCoordinator.Route) {
- switch route {
- case .equalizer:
- showEqualizerControls()
- }
- }
-
- private func providePlayerControls() -> UIViewController {
- let viewModel = PlayerControlsViewModel(playerService: playerService)
- return PlayerControlsViewController(viewModel: viewModel)
- }
-
- private func showEqualizerControls() {
- let viewModel = EqualzerViewModel(equalizerService: equaliserService)
- let viewController = EqualizerViewController(viewModel: viewModel)
- let navigationController = UINavigationController(rootViewController: viewController)
- self.navigationController?.present(navigationController, animated: true, completion: nil)
- }
-}
diff --git a/AudioExample/AudioExample/AppDelegate.swift b/AudioExample/AudioExample/AppDelegate.swift
deleted file mode 100644
index 929188a..0000000
--- a/AudioExample/AudioExample/AppDelegate.swift
+++ /dev/null
@@ -1,25 +0,0 @@
-//
-// AppDelegate.swift
-// AudioExample
-//
-// Created by Dimitrios Chatzieleftheriou on 20/05/2020.
-// Copyright © 2020 Dimitrios Chatzieleftheriou. All rights reserved.
-//
-
-import UIKit
-
-@UIApplicationMain
-class AppDelegate: UIResponder, UIApplicationDelegate {
- var window: UIWindow?
- var appCoordinator: AppCoordinator?
-
- func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
- let window = UIWindow(frame: UIScreen.main.bounds)
- let appCoordinator = AppCoordinator()
- appCoordinator.start(window: window)
- self.window = window
- self.appCoordinator = appCoordinator
-
- return true
- }
-}
diff --git a/AudioExample/AudioExample/Controllers/EqualizerViewController.swift b/AudioExample/AudioExample/Controllers/EqualizerViewController.swift
deleted file mode 100644
index c3d8a94..0000000
--- a/AudioExample/AudioExample/Controllers/EqualizerViewController.swift
+++ /dev/null
@@ -1,155 +0,0 @@
-//
-// EqualizerViewController.swift
-// AudioExample
-//
-// Created by Dimitrios Chatzieleftheriou on 15/11/2020.
-// Copyright © 2020 Dimitrios Chatzieleftheriou. All rights reserved.
-//
-
-import UIKit
-
-class EqualizerViewController: UIViewController {
- private lazy var enableTextLabel = UILabel()
- private lazy var enableButton = UISwitch()
-
- private var eqSlider = [UISlider]()
-
- private let viewModel: EqualzerViewModel
-
- init(viewModel: EqualzerViewModel) {
- self.viewModel = viewModel
- super.init(nibName: nil, bundle: nil)
- }
-
- @available(*, unavailable)
- required init?(coder _: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
- title = "Equaliser"
- view.backgroundColor = .systemBackground
-
- navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Reset", style: .plain, target: self, action: #selector(resetEq))
-
- enableTextLabel.translatesAutoresizingMaskIntoConstraints = false
- enableTextLabel.text = "Enable"
-
- enableButton.translatesAutoresizingMaskIntoConstraints = false
- enableButton.isOn = viewModel.equaliserIsOn
- enableButton.onTintColor = .systemTeal
- enableButton.addTarget(self, action: #selector(enableEq), for: .valueChanged)
-
- let enableStackView = UIStackView(arrangedSubviews: [enableTextLabel, enableButton])
- enableStackView.translatesAutoresizingMaskIntoConstraints = false
- enableStackView.axis = .horizontal
- enableStackView.alignment = .center
- enableStackView.spacing = 10
- enableStackView.isLayoutMarginsRelativeArrangement = true
- enableStackView.directionalLayoutMargins = .init(top: 10, leading: 10, bottom: 10, trailing: 10)
-
- let equaliserControls = UIStackView(arrangedSubviews: buildSliders())
- equaliserControls.translatesAutoresizingMaskIntoConstraints = false
- equaliserControls.axis = .vertical
- equaliserControls.alignment = .fill
- equaliserControls.distribution = .fillEqually
-
- let stackView = UIStackView(arrangedSubviews: [enableStackView, equaliserControls])
- stackView.translatesAutoresizingMaskIntoConstraints = false
- stackView.axis = .vertical
- stackView.isLayoutMarginsRelativeArrangement = true
- stackView.directionalLayoutMargins = .init(top: 10, leading: 10, bottom: 10, trailing: 10)
-
- view.addSubview(stackView)
-
- NSLayoutConstraint.activate(
- [
- enableStackView.heightAnchor.constraint(equalToConstant: 60),
- stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
- stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- stackView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.8),
- ]
- )
- }
-
- @objc func enableEq() {
- viewModel.enableEq(enableButton.isOn)
- }
-
- @objc func resetEq() {
- viewModel.resetEq { value in
- eqSlider.forEach { $0.setValue(value, animated: true) }
- }
- }
-
- private func buildSliders() -> [UIView] {
- var sliders = [UIView]()
- for index in 0 ..< viewModel.numberOfBands() {
- guard let item = viewModel.band(at: index) else { continue }
- let slider = buildSlider(item: item, index: index)
- sliders.append(slider)
- }
- return sliders
- }
-
- @objc private func valueChanged(_ slider: UISlider) {
- viewModel.update(gain: slider.value, for: slider.tag)
- }
-
- private func buildSlider(item: EQBand, index: Int) -> UIView {
- let freqLabel = UILabel()
- freqLabel.translatesAutoresizingMaskIntoConstraints = false
- freqLabel.text = item.frequency
- freqLabel.textAlignment = .right
- freqLabel.widthAnchor.constraint(equalToConstant: 40).isActive = true
-
- let slider = UISlider()
- slider.translatesAutoresizingMaskIntoConstraints = false
- slider.tag = index // cheating here
- slider.minimumValue = item.min
- slider.maximumValue = item.max
- slider.value = item.value
- slider.isContinuous = true
- slider.addTarget(self, action: #selector(valueChanged(_:)), for: .valueChanged)
- eqSlider.append(slider)
-
- let minLabel = UILabel()
- minLabel.translatesAutoresizingMaskIntoConstraints = false
- minLabel.text = "\(item.min)db"
-
- let centerLabel = UILabel()
- centerLabel.translatesAutoresizingMaskIntoConstraints = false
- centerLabel.text = "0db"
- centerLabel.textAlignment = .center
-
- let maxLabel = UILabel()
- maxLabel.translatesAutoresizingMaskIntoConstraints = false
- maxLabel.text = "\(item.max)db"
- maxLabel.textAlignment = .right
-
- let dbStackView = UIStackView(arrangedSubviews: [minLabel, centerLabel, maxLabel])
- dbStackView.translatesAutoresizingMaskIntoConstraints = false
- dbStackView.axis = .horizontal
- dbStackView.distribution = .fillEqually
-
- let stackViewSlider = UIStackView(arrangedSubviews: [slider, dbStackView])
- stackViewSlider.spacing = 5
- stackViewSlider.translatesAutoresizingMaskIntoConstraints = false
- stackViewSlider.axis = .vertical
- stackViewSlider.setContentHuggingPriority(.fittingSizeLevel, for: .horizontal)
- stackViewSlider.setContentCompressionResistancePriority(.fittingSizeLevel, for: .horizontal)
-
- let stackView = UIStackView(arrangedSubviews: [freqLabel, stackViewSlider])
- stackView.spacing = 10
- stackView.translatesAutoresizingMaskIntoConstraints = false
- stackView.axis = .horizontal
- stackView.distribution = .fillProportionally
- stackView.alignment = .fill
- stackView.isLayoutMarginsRelativeArrangement = true
- stackView.directionalLayoutMargins = .init(top: 0, leading: 10, bottom: 0, trailing: 10)
-
- return stackView
- }
-}
diff --git a/AudioExample/AudioExample/Controllers/EqualizerViewModel.swift b/AudioExample/AudioExample/Controllers/EqualizerViewModel.swift
deleted file mode 100644
index 7e27b27..0000000
--- a/AudioExample/AudioExample/Controllers/EqualizerViewModel.swift
+++ /dev/null
@@ -1,66 +0,0 @@
-//
-// EqualizerViewModel.swift
-// AudioExample
-//
-// Created by Dimitrios Chatzieleftheriou on 15/11/2020.
-// Copyright © 2020 Dimitrios Chatzieleftheriou. All rights reserved.
-//
-
-import AVFoundation
-
-struct EQBand {
- let frequency: String
- let min: Float
- let max: Float
- let value: Float
-}
-
-final class EqualzerViewModel {
- private var bands: [EQBand] = []
-
- private let equalizerService: EqualizerService
-
- var equaliserIsOn: Bool {
- equalizerService.isActivated
- }
-
- init(equalizerService: EqualizerService) {
- self.equalizerService = equalizerService
-
- bands = equalizerService.bands.map { item in
- var measurement = item.frequency
- var frequency = String(Int(measurement))
- if item.frequency >= 1000 {
- measurement = item.frequency / 1000
- frequency = "\(String(Int(measurement)))K"
- }
- return EQBand(frequency: frequency, min: -12, max: 12, value: item.gain)
- }
- }
-
- func enableEq(_ enable: Bool) {
- if enable {
- equalizerService.activate()
- } else {
- equalizerService.deactivate()
- }
- }
-
- func resetEq(updateSliders: (_ value: Float) -> Void) {
- equalizerService.reset()
- updateSliders(0)
- }
-
- func update(gain: Float, for index: Int) {
- equalizerService.update(gain: gain, for: index)
- }
-
- func numberOfBands() -> Int {
- equalizerService.bands.count
- }
-
- func band(at index: Int) -> EQBand? {
- guard index < numberOfBands() else { return nil }
- return bands[index]
- }
-}
diff --git a/AudioExample/AudioExample/Controllers/PlayerControlsViewController.swift b/AudioExample/AudioExample/Controllers/PlayerControlsViewController.swift
deleted file mode 100644
index a31c582..0000000
--- a/AudioExample/AudioExample/Controllers/PlayerControlsViewController.swift
+++ /dev/null
@@ -1,213 +0,0 @@
-//
-// PlayerControlsViewController.swift
-// AudioExample
-//
-// Created by Dimitrios Chatzieleftheriou on 14/11/2020.
-// Copyright © 2020 Dimitrios Chatzieleftheriou. All rights reserved.
-//
-
-import UIKit
-
-class PlayerControlsViewController: UIViewController {
- private lazy var resumeButton = UIButton()
- private lazy var stopButton = UIButton(type: .custom)
- private lazy var muteButton = UIButton()
-
- private lazy var slider = UISlider()
- private lazy var elapsedPlayTimeLabel = UILabel()
- private lazy var remainingPlayTimeLabel = UILabel()
-
- private lazy var rateSlider = UISlider()
- private lazy var rateSliderValueLabel = UILabel()
-
- private lazy var playerStatus = UILabel()
-
- private let viewModel: PlayerControlsViewModel
-
- init(viewModel: PlayerControlsViewModel) {
- self.viewModel = viewModel
- super.init(nibName: nil, bundle: nil)
- }
-
- @available(*, unavailable)
- required init?(coder _: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
- view.backgroundColor = .systemBackground
- setupUI()
- setupBinding()
- }
-
- private func setupUI() {
- muteButton.translatesAutoresizingMaskIntoConstraints = false
- muteButton.setTitle("Mute", for: .normal)
- muteButton.setTitleColor(.label, for: .normal)
- muteButton.setTitleColor(.secondaryLabel, for: .highlighted)
- muteButton.setTitleColor(.tertiaryLabel, for: .disabled)
- muteButton.accessibilityIdentifier = "muteButton"
- muteButton.addTarget(self, action: #selector(toggleMute), for: .touchUpInside)
-
- resumeButton.translatesAutoresizingMaskIntoConstraints = false
- resumeButton.setTitle("Pause", for: .normal)
- resumeButton.accessibilityIdentifier = "resumeButton"
- resumeButton.setTitleColor(.label, for: .normal)
- resumeButton.setTitleColor(.secondaryLabel, for: .highlighted)
- resumeButton.setTitleColor(.tertiaryLabel, for: .disabled)
- resumeButton.addTarget(self, action: #selector(pauseResume), for: .touchUpInside)
-
- stopButton.translatesAutoresizingMaskIntoConstraints = false
- stopButton.setTitle("Stop", for: .normal)
- stopButton.setTitleColor(.label, for: .normal)
- stopButton.setTitleColor(.secondaryLabel, for: .highlighted)
- stopButton.setTitleColor(.tertiaryLabel, for: .disabled)
- stopButton.accessibilityIdentifier = "stopButton"
- stopButton.addTarget(self, action: #selector(stop), for: .touchUpInside)
-
- let controlsStackView = UIStackView(arrangedSubviews: [resumeButton, stopButton, muteButton])
- controlsStackView.translatesAutoresizingMaskIntoConstraints = false
- controlsStackView.axis = .horizontal
- controlsStackView.distribution = .fillEqually
- controlsStackView.alignment = .center
- controlsStackView.accessibilityIdentifier = "controlsStackView"
-
- slider.translatesAutoresizingMaskIntoConstraints = false
- slider.accessibilityIdentifier = "slider"
- slider.tintColor = .systemGray2
- slider.thumbTintColor = .systemGray
- slider.isContinuous = true
- slider.semanticContentAttribute = .playback
- slider.addTarget(self, action: #selector(sliderTouchedDown), for: .touchDown)
- slider.addTarget(self, action: #selector(sliderTouchedUp), for: [.touchUpInside, .touchUpOutside])
- slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
-
- elapsedPlayTimeLabel.text = "--:--"
- elapsedPlayTimeLabel.accessibilityIdentifier = "elapsedPlayTimeLabel"
- elapsedPlayTimeLabel.translatesAutoresizingMaskIntoConstraints = false
- elapsedPlayTimeLabel.font = UIFont.preferredFont(forTextStyle: .footnote)
- elapsedPlayTimeLabel.textAlignment = .left
- remainingPlayTimeLabel.text = "--:--"
- remainingPlayTimeLabel.accessibilityIdentifier = "remainingPlayTimeLabel"
- remainingPlayTimeLabel.translatesAutoresizingMaskIntoConstraints = false
- remainingPlayTimeLabel.font = UIFont.preferredFont(forTextStyle: .footnote)
- remainingPlayTimeLabel.textAlignment = .right
-
- let playbackTimeLabelsStack = UIStackView(arrangedSubviews: [elapsedPlayTimeLabel, remainingPlayTimeLabel])
- playbackTimeLabelsStack.translatesAutoresizingMaskIntoConstraints = false
- playbackTimeLabelsStack.axis = .horizontal
- playbackTimeLabelsStack.distribution = .fillEqually
- playbackTimeLabelsStack.accessibilityIdentifier = "playbackTimeLabelsStack"
-
- playerStatus.text = ""
- playerStatus.translatesAutoresizingMaskIntoConstraints = false
- playerStatus.numberOfLines = 0
- playerStatus.accessibilityIdentifier = "playerStatus-label"
-
- let sliderLabel = UILabel()
- sliderLabel.translatesAutoresizingMaskIntoConstraints = false
- sliderLabel.text = "Rate: "
-
- rateSliderValueLabel.translatesAutoresizingMaskIntoConstraints = false
- rateSliderValueLabel.text = viewModel.currentRateTitle
-
- rateSlider.translatesAutoresizingMaskIntoConstraints = false
- rateSlider.minimumValue = viewModel.rateMinValue
- rateSlider.maximumValue = viewModel.rateMaxValue
- rateSlider.value = viewModel.rateMinValue
- rateSlider.addTarget(self, action: #selector(rateValueChanged), for: .valueChanged)
-
- let sliderWarningLabel = UILabel()
- sliderWarningLabel.translatesAutoresizingMaskIntoConstraints = false
- sliderWarningLabel.text = "Adjusting rate in live broadcast is not recommended"
- sliderWarningLabel.numberOfLines = 2
- sliderWarningLabel.textColor = .systemRed
-
- let rateSliderStackView = UIStackView(arrangedSubviews: [sliderLabel, rateSlider, rateSliderValueLabel])
- rateSliderStackView.spacing = 10
- rateSliderStackView.axis = .horizontal
-
- let controlsAndSliderStack = UIStackView(arrangedSubviews: [controlsStackView,
- slider,
- playbackTimeLabelsStack,
- playerStatus,
- rateSliderStackView,
- sliderWarningLabel])
- controlsAndSliderStack.translatesAutoresizingMaskIntoConstraints = false
- controlsAndSliderStack.spacing = 10
- controlsAndSliderStack.setCustomSpacing(15, after: playbackTimeLabelsStack)
- controlsAndSliderStack.axis = .vertical
- controlsAndSliderStack.distribution = .fill
- controlsAndSliderStack.alignment = .fill
- controlsAndSliderStack.isLayoutMarginsRelativeArrangement = true
- controlsAndSliderStack.layoutMargins = .init(top: 15, left: 10, bottom: 0, right: 10)
- controlsAndSliderStack.accessibilityIdentifier = "controlsAndSliderStack"
-
- view.addSubview(controlsAndSliderStack)
- view.accessibilityIdentifier = "controller-view"
-
- NSLayoutConstraint.activate([
- controlsAndSliderStack.topAnchor.constraint(equalTo: view.topAnchor),
- controlsAndSliderStack.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- controlsAndSliderStack.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- ])
- }
-
- private func setupBinding() {
- viewModel.updateContent = { [unowned self] effect in
- switch effect {
- case let .updateMuteButton(title):
- self.muteButton.setTitle(title, for: .normal)
- case let .updatePauseResumeButton(title):
- self.resumeButton.setTitle(title, for: .normal)
- case let .updateSliderMinMaxValue(min, max):
- self.slider.minimumValue = min
- self.slider.maximumValue = max
- case let .updateSliderValue(value):
- self.slider.value = value
- case let .updateMetadata(title):
- self.playerStatus.text = title
- }
- }
-
- viewModel.updateProgressAndDurationTitles = { [elapsedPlayTimeLabel, remainingPlayTimeLabel] progress, duration in
- elapsedPlayTimeLabel.text = progress
- remainingPlayTimeLabel.text = duration
- }
- }
-
- @objc private func rateValueChanged() {
- viewModel.update(rate: rateSlider.value) { [rateSlider] value in
- rateSlider.value = value
- }
- rateSliderValueLabel.text = viewModel.currentRateTitle
- }
-
- @objc private func toggleMute() {
- viewModel.toggleMute()
- }
-
- @objc private func pauseResume() {
- viewModel.togglePauseResume()
- }
-
- @objc private func stop() {
- viewModel.stop()
- }
-
- @objc
- func sliderTouchedDown() {
- viewModel.seek(action: .started)
- }
-
- @objc
- func sliderTouchedUp() {
- viewModel.seek(action: .ended)
- }
-
- @objc
- func sliderValueChanged() {
- viewModel.seek(action: .updateSeek(time: slider.value))
- }
-}
diff --git a/AudioExample/AudioExample/Controllers/PlayerControlsViewModel.swift b/AudioExample/AudioExample/Controllers/PlayerControlsViewModel.swift
deleted file mode 100644
index 38b20df..0000000
--- a/AudioExample/AudioExample/Controllers/PlayerControlsViewModel.swift
+++ /dev/null
@@ -1,166 +0,0 @@
-//
-// PlayerControlsViewModel.swift
-// AudioExample
-//
-// Created by Dimitrios Chatzieleftheriou on 14/11/2020.
-// Copyright © 2020 Dimitrios Chatzieleftheriou. All rights reserved.
-//
-
-import AudioStreaming
-import Foundation
-import UIKit
-
-enum SeekAction: Equatable {
- case started
- case updateSeek(time: Float)
- case ended
-}
-
-enum ControlsEffects {
- case updateMuteButton(String)
- case updatePauseResumeButton(String)
- case updateSliderMinMaxValue(min: Float, max: Float)
- case updateSliderValue(value: Float)
- case updateMetadata(String)
-}
-
-final class PlayerControlsViewModel {
- var updateContent: ((ControlsEffects) -> Void)?
- var updateProgressAndDurationTitles: ((String, String) -> Void)?
-
- private let playerService: AudioPlayerService
-
- private var displayLink: CADisplayLink?
-
- private var seekTime: Float = 0
- private var isScrubbing: Bool = false
-
- let rateMinValue: Float = 1.0
- let rateMaxValue: Float = 3.0
-
- var currentRateTitle: String {
- String(format: "%.1fx", playerService.rate)
- }
-
- init(playerService: AudioPlayerService) {
- self.playerService = playerService
- self.playerService.delegate.add(delegate: self)
- }
-
- func stop() {
- playerService.stop()
- stopDisplayLink(resetLabels: true)
- updateContent?(.updatePauseResumeButton("Pause"))
- }
-
- func togglePauseResume() {
- playerService.toggle()
- let isPaused = playerService.state == .paused
- updateContent?(.updatePauseResumeButton(isPaused ? "Resume" : "Pause"))
- }
-
- func toggleMute() {
- playerService.toggleMute()
- let isMuted = playerService.isMuted
- updateContent?(.updateMuteButton(isMuted ? "Unmute" : "Mute"))
- }
-
- func seek(action: SeekAction) {
- switch action {
- case .started:
- isScrubbing = true
- seekTime = 0
- case let .updateSeek(time):
- seekTime = time
- case .ended:
- isScrubbing = false
- if playerService.duration > 0 {
- playerService.seek(at: seekTime)
- }
- }
- }
-
- func update(rate: Float, updater: (Float) -> Void) {
- let rate = round(rate / 0.5) * 0.5
- playerService.update(rate: rate)
- updater(rate)
- }
-
- private func startDisplayLink() {
- displayLink?.invalidate()
- displayLink = nil
- displayLink = UIScreen.main.displayLink(withTarget: self, selector: #selector(tick))
- displayLink?.preferredFramesPerSecond = 6
- displayLink?.add(to: .current, forMode: .common)
- }
-
- private func stopDisplayLink(resetLabels: Bool) {
- displayLink?.invalidate()
- displayLink = nil
- if resetLabels {
- resetLabelsAndSlider()
- }
- }
-
- @objc private func tick() {
- let duration = playerService.duration
- let progress = playerService.progress
- if duration > 0 {
- let elapsed = Int(progress)
- let remaining = Int(duration - progress)
-
- updateContent?(.updateSliderMinMaxValue(min: 0.0, max: Float(duration)))
- if !isScrubbing {
- updateContent?(.updateSliderValue(value: Float(progress)))
- }
-
- updateProgressAndDurationTitles?(timeFrom(seconds: elapsed), timeFrom(seconds: remaining))
- } else {
- let elapsed = Int(progress)
- updateProgressAndDurationTitles?("Live broadcast", timeFrom(seconds: elapsed))
- }
- }
-
- private func resetLabelsAndSlider() {
- updateProgressAndDurationTitles?("--:--", "--:--")
- updateContent?(.updateSliderMinMaxValue(min: 0, max: 0))
- updateContent?(.updateSliderValue(value: 0))
- }
-
- private func timeFrom(seconds: Int) -> String {
- let correctSeconds = seconds % 60
- let minutes = (seconds / 60) % 60
- let hours = seconds / 3600
-
- if hours > 0 {
- return String(format: "%02d:%02d:%02d", hours, minutes, correctSeconds)
- }
- return String(format: "%02d:%02d", minutes, correctSeconds)
- }
-}
-
-extension PlayerControlsViewModel: AudioPlayerServiceDelegate {
- func didStopPlaying() {
- stopDisplayLink(resetLabels: true)
- updateContent?(.updateMetadata(""))
- }
-
- func statusChanged(status _: AudioPlayerState) {}
-
- func didStartPlaying() {
- startDisplayLink()
- resetLabelsAndSlider()
- updateContent?(.updateMetadata(""))
- }
-
- func errorOccurred(error _: AudioPlayerError) {}
-
- func metadataReceived(metadata: [String: String]) {
- guard !metadata.isEmpty else { return }
- if let title = metadata["StreamTitle"] {
- updateContent?(.updateMetadata("Now Playing: \(title)"))
- } else {
- updateContent?(.updateMetadata(""))
- }
- }
-}
diff --git a/AudioExample/AudioExample/Controllers/PlayerViewController.swift b/AudioExample/AudioExample/Controllers/PlayerViewController.swift
deleted file mode 100644
index ad72596..0000000
--- a/AudioExample/AudioExample/Controllers/PlayerViewController.swift
+++ /dev/null
@@ -1,167 +0,0 @@
-//
-// PlayerViewController.swift
-// AudioExample
-//
-// Created by Dimitrios Chatzieleftheriou on 14/11/2020.
-// Copyright © 2020 Dimitrios Chatzieleftheriou. All rights reserved.
-//
-
-import UIKit
-
-class PlayerViewController: UIViewController {
- private lazy var tableView = UITableView()
-
- private let viewModel: PlayerViewModel
- private var controlsProvider: () -> UIViewController
- private var playerControlsController: UIViewController?
-
- init(viewModel: PlayerViewModel, controlsProvider: @escaping () -> UIViewController) {
- self.viewModel = viewModel
- self.controlsProvider = controlsProvider
- super.init(nibName: nil, bundle: nil)
- }
-
- @available(*, unavailable)
- required init?(coder _: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
- setupUI()
-
- viewModel.reloadContent = { [weak self] action in
- switch action {
- case .all:
- self?.tableView.reloadData()
- case let .item(indexPath):
- self?.tableView.reloadRows(at: [indexPath], with: .automatic)
- }
- }
- }
-
- private func setupUI() {
- title = "Player"
- view.backgroundColor = .systemBackground
- navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add,
- target: self,
- action: #selector(addNowPlaylistItem))
-
- navigationItem.leftBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "slider.horizontal.3"),
- style: .plain,
- target: self,
- action: #selector(showEqualizer))
-
- tableView.translatesAutoresizingMaskIntoConstraints = false
- tableView.delegate = self
- tableView.dataSource = self
- tableView.register(PlaylistTableViewCell.self, forCellReuseIdentifier: "PlaylistCell")
-
- let controlsController = controlsProvider()
- playerControlsController = controlsController
-
- let stackView = UIStackView()
- stackView.axis = .vertical
- stackView.alignment = .fill
- stackView.distribution = .fillProportionally
- stackView.translatesAutoresizingMaskIntoConstraints = false
-
- stackView.addArrangedSubview(tableView)
-
- addChild(controlsController)
- controlsController.view.translatesAutoresizingMaskIntoConstraints = false
- stackView.addArrangedSubview(controlsController.view)
- controlsController.didMove(toParent: self)
-
- view.addSubview(stackView)
-
- NSLayoutConstraint.activate(
- [
- controlsController.view.widthAnchor.constraint(equalTo: view.widthAnchor),
- stackView.topAnchor.constraint(equalTo: view.topAnchor),
- stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
- ]
- )
- }
-
- @objc private func showEqualizer() {
- viewModel.showEqualizer()
- }
-
- @objc private func addNowPlaylistItem() {
- let controller = UIAlertController(title: "Add new item", message: "", preferredStyle: .alert)
- controller.addTextField { textField in
- textField.placeholder = "Insert url here"
- }
- let saveAction = UIAlertAction(title: "Save", style: .default) { [viewModel] _ in
- if let textfield = controller.textFields?.first,
- let text = textfield.text
- {
- viewModel.add(urlString: text)
- }
- }
- let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
-
- controller.addAction(saveAction)
- controller.addAction(cancelAction)
- present(controller, animated: true, completion: nil)
- }
-}
-
-extension PlayerViewController: UITableViewDataSource {
- func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
- viewModel.itemsCount
- }
-
- func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- let cell = tableView.dequeueReusableCell(withIdentifier: "PlaylistCell", for: indexPath)
- guard let item = viewModel.item(at: indexPath) else {
- return cell
- }
- cell.textLabel?.text = item.name
- let queuedItem = item.queues ? "Queue item" : nil
- cell.detailTextLabel?.text = queuedItem ?? item.subtitle
- update(status: item.status, of: cell)
- return cell
- }
-
- private func update(status: PlaylistItem.Status, of cell: UITableViewCell) {
- switch status {
- case .buffering:
- let activity = UIActivityIndicatorView(style: .medium)
- activity.startAnimating()
- cell.accessoryView = activity
- case .playing:
- cell.accessoryView = UIImageView(image: UIImage(systemName: "play.fill"))
- case .paused:
- cell.accessoryView = UIImageView(image: UIImage(systemName: "pause.fill"))
- case .stopped:
- cell.accessoryView = nil
- case .error:
- cell.accessoryView = UIImageView(image: UIImage(systemName: "exclamationmark.octagon"))
- cell.accessoryView?.tintColor = .red
- }
- guard status != .error else { return }
- cell.accessoryView?.tintColor = .systemTeal
- }
-}
-
-extension PlayerViewController: UITableViewDelegate {
- func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- tableView.deselectRow(at: indexPath, animated: true)
- viewModel.playItem(at: indexPath)
- }
-}
-
-final class PlaylistTableViewCell: UITableViewCell {
- override init(style _: UITableViewCell.CellStyle, reuseIdentifier: String?) {
- super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
- }
-
- @available(*, unavailable)
- required init?(coder _: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-}
diff --git a/AudioExample/AudioExample/Controllers/PlayerViewModel.swift b/AudioExample/AudioExample/Controllers/PlayerViewModel.swift
deleted file mode 100644
index b2b9c49..0000000
--- a/AudioExample/AudioExample/Controllers/PlayerViewModel.swift
+++ /dev/null
@@ -1,112 +0,0 @@
-//
-// PlayerViewModel.swift
-// AudioExample
-//
-// Created by Dimitrios Chatzieleftheriou on 14/11/2020.
-// Copyright © 2020 Dimitrios Chatzieleftheriou. All rights reserved.
-//
-
-import AudioStreaming
-import Foundation
-
-enum ReloadAction {
- case all
- case item(IndexPath)
-}
-
-final class PlayerViewModel {
- private let playerService: AudioPlayerService
- private let playlistItemsService: PlaylistItemsService
-
- private let routeTo: (AppCoordinator.Route) -> Void
- private var currentPlayingItemIndex: Int?
-
- var reloadContent: ((ReloadAction) -> Void)?
-
- init(playlistItemsService: PlaylistItemsService,
- playerService: AudioPlayerService,
- routeTo: @escaping (AppCoordinator.Route) -> Void)
- {
- self.playlistItemsService = playlistItemsService
- self.playerService = playerService
- self.routeTo = routeTo
- self.playerService.delegate.add(delegate: self)
- }
-
- func showEqualizer() {
- routeTo(.equalizer)
- }
-
- var itemsCount: Int {
- playlistItemsService.itemsCount
- }
-
- func add(urlString: String) {
- let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
- let result = detector.firstMatch(in: urlString, options: [], range: NSRange(location: 0, length: urlString.utf16.count))
- guard let url = URL(string: urlString), result != nil else {
- print("malformed url error")
- return
- }
- playlistItemsService.add(item: PlaylistItem(url: url, name: urlString, subtitle: nil, status: .stopped, queues: false))
- reloadContent?(.all)
- }
-
- func item(at indexPath: IndexPath) -> PlaylistItem? {
- playlistItemsService.item(at: indexPath.row)
- }
-
- func playItem(at indexPath: IndexPath) {
- guard let item = item(at: indexPath) else { return }
- if item.queues {
- playerService.queue(url: item.url)
- if currentPlayingItemIndex == nil {
- currentPlayingItemIndex = indexPath.row
- }
- } else {
- if let index = currentPlayingItemIndex {
- playlistItemsService.setStatus(for: index, status: .stopped)
- reloadContent?(.item(IndexPath(row: index, section: 0)))
- currentPlayingItemIndex = nil
- }
- playerService.play(url: item.url)
- currentPlayingItemIndex = indexPath.row
- }
- }
-}
-
-extension PlayerViewModel: AudioPlayerServiceDelegate {
- func statusChanged(status: AudioPlayerState) {
- guard let item = currentPlayingItemIndex else { return }
-
- switch status {
- case .bufferring:
- playlistItemsService.setStatus(for: item, status: .buffering)
- reloadContent?(.item(IndexPath(item: item, section: 0)))
- case .playing:
- playlistItemsService.setStatus(for: item, status: .playing)
- reloadContent?(.item(IndexPath(item: item, section: 0)))
- case .paused:
- playlistItemsService.setStatus(for: item, status: .paused)
- reloadContent?(.item(IndexPath(item: item, section: 0)))
- case .stopped:
- playlistItemsService.setStatus(for: item, status: .stopped)
- reloadContent?(.item(IndexPath(item: item, section: 0)))
- case .error:
- playlistItemsService.setStatus(for: item, status: .error)
- reloadContent?(.item(IndexPath(item: item, section: 0)))
- default:
- playlistItemsService.setStatus(for: item, status: .stopped)
- reloadContent?(.all)
- }
- }
-
- func errorOccurred(error _: AudioPlayerError) {
- currentPlayingItemIndex = nil
- }
-
- func metadataReceived(metadata _: [String: String]) {}
- func didStopPlaying() {}
-
- func didStartPlaying() {}
-}
diff --git a/AudioExample/AudioExample/Info.plist b/AudioExample/AudioExample/Info.plist
deleted file mode 100644
index 5d757ed..0000000
--- a/AudioExample/AudioExample/Info.plist
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- $(PRODUCT_NAME)
- CFBundlePackageType
- $(PRODUCT_BUNDLE_PACKAGE_TYPE)
- CFBundleShortVersionString
- 1.0
- CFBundleVersion
- 1
- LSRequiresIPhoneOS
-
- NSAppTransportSecurity
-
- NSAllowsArbitraryLoads
-
-
- UIBackgroundModes
-
- audio
-
- UILaunchStoryboardName
- LaunchScreen
- UIRequiredDeviceCapabilities
-
- armv7
-
- UIRequiresFullScreen
-
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
-
-
diff --git a/AudioExample/AudioExample/Resources/.DS_Store b/AudioExample/AudioExample/Resources/.DS_Store
deleted file mode 100644
index d6eb710..0000000
Binary files a/AudioExample/AudioExample/Resources/.DS_Store and /dev/null differ
diff --git a/AudioExample/AudioExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/AudioExample/AudioExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index 9221b9b..0000000
--- a/AudioExample/AudioExample/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,98 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "iphone",
- "scale" : "2x",
- "size" : "20x20"
- },
- {
- "idiom" : "iphone",
- "scale" : "3x",
- "size" : "20x20"
- },
- {
- "idiom" : "iphone",
- "scale" : "2x",
- "size" : "29x29"
- },
- {
- "idiom" : "iphone",
- "scale" : "3x",
- "size" : "29x29"
- },
- {
- "idiom" : "iphone",
- "scale" : "2x",
- "size" : "40x40"
- },
- {
- "idiom" : "iphone",
- "scale" : "3x",
- "size" : "40x40"
- },
- {
- "idiom" : "iphone",
- "scale" : "2x",
- "size" : "60x60"
- },
- {
- "idiom" : "iphone",
- "scale" : "3x",
- "size" : "60x60"
- },
- {
- "idiom" : "ipad",
- "scale" : "1x",
- "size" : "20x20"
- },
- {
- "idiom" : "ipad",
- "scale" : "2x",
- "size" : "20x20"
- },
- {
- "idiom" : "ipad",
- "scale" : "1x",
- "size" : "29x29"
- },
- {
- "idiom" : "ipad",
- "scale" : "2x",
- "size" : "29x29"
- },
- {
- "idiom" : "ipad",
- "scale" : "1x",
- "size" : "40x40"
- },
- {
- "idiom" : "ipad",
- "scale" : "2x",
- "size" : "40x40"
- },
- {
- "idiom" : "ipad",
- "scale" : "1x",
- "size" : "76x76"
- },
- {
- "idiom" : "ipad",
- "scale" : "2x",
- "size" : "76x76"
- },
- {
- "idiom" : "ipad",
- "scale" : "2x",
- "size" : "83.5x83.5"
- },
- {
- "idiom" : "ios-marketing",
- "scale" : "1x",
- "size" : "1024x1024"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/AudioExample/AudioExample/Resources/Base.lproj/LaunchScreen.storyboard b/AudioExample/AudioExample/Resources/Base.lproj/LaunchScreen.storyboard
deleted file mode 100644
index 865e932..0000000
--- a/AudioExample/AudioExample/Resources/Base.lproj/LaunchScreen.storyboard
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/AudioExample/AudioExample/Services/MulticastDelegate.swift b/AudioExample/AudioExample/Services/MulticastDelegate.swift
deleted file mode 100644
index 4b928c5..0000000
--- a/AudioExample/AudioExample/Services/MulticastDelegate.swift
+++ /dev/null
@@ -1,31 +0,0 @@
-//
-// MulticastDelegate.swift
-// AudioExample
-//
-// Created by Dimitrios Chatzieleftheriou on 14/11/2020.
-// Copyright © 2020 Dimitrios Chatzieleftheriou. All rights reserved.
-//
-
-import Foundation
-
-class MulticastDelegate {
- private let delegates = NSHashTable.weakObjects()
-
- func add(delegate: Delegate) {
- delegates.add(delegate as AnyObject)
- }
-
- func remove(delegate: Delegate) {
- for oneDelegate in delegates.allObjects.reversed() {
- if oneDelegate === delegate as AnyObject {
- delegates.remove(oneDelegate)
- }
- }
- }
-
- func invoke(invocation: (Delegate) -> Void) {
- for delegate in delegates.allObjects.reversed() {
- invocation(delegate as! Delegate)
- }
- }
-}
diff --git a/AudioExample/AudioExample/Services/NowPlayingCenter.swift b/AudioExample/AudioExample/Services/NowPlayingCenter.swift
deleted file mode 100644
index 4ea2a34..0000000
--- a/AudioExample/AudioExample/Services/NowPlayingCenter.swift
+++ /dev/null
@@ -1,36 +0,0 @@
-//
-// NowPlayingCenter.swift
-// AudioExample
-//
-// Created by Dimitrios Chatzieleftheriou on 15/11/2020.
-// Copyright © 2020 Dimitrios Chatzieleftheriou. All rights reserved.
-//
-
-import MediaPlayer
-
-final class NowPlayingCenter {
- private let infoCenter: MPNowPlayingInfoCenter
-
- init(infoCenter: MPNowPlayingInfoCenter = .default()) {
- self.infoCenter = infoCenter
- }
-
- func change(item: PlaylistItem, isLiveStream: Bool) {
- var nowPlayingInfo = infoCenter.nowPlayingInfo ?? [String: Any]()
-
- nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.audio.rawValue
- nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = isLiveStream
- nowPlayingInfo[MPMediaItemPropertyArtist] = item.name
-
- infoCenter.nowPlayingInfo = nowPlayingInfo
- }
-
- func update(with metadata: [String: String], with item: PlaylistItem) {
- var nowPlayingInfo = infoCenter.nowPlayingInfo ?? [String: Any]()
-
- nowPlayingInfo[MPMediaItemPropertyTitle] = metadata["StreamTitle"]
- nowPlayingInfo[MPMediaItemPropertyArtist] = item.name
-
- infoCenter.nowPlayingInfo = nowPlayingInfo
- }
-}
diff --git a/AudioExample/AudioExample/Services/PlaylistItemsService.swift b/AudioExample/AudioExample/Services/PlaylistItemsService.swift
deleted file mode 100644
index 555c359..0000000
--- a/AudioExample/AudioExample/Services/PlaylistItemsService.swift
+++ /dev/null
@@ -1,96 +0,0 @@
-//
-// PlaylistItemsService.swift
-// AudioExample
-//
-// Created by Dimitrios Chatzieleftheriou on 14/11/2020.
-// Copyright © 2020 Dimitrios Chatzieleftheriou. All rights reserved.
-//
-
-import Foundation
-
-struct PlaylistItem: Equatable {
- enum Status: Equatable {
- case playing
- case paused
- case buffering
- case stopped
- case error
- }
-
- 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, subtitle: String?, status: Status, queues: Bool) {
- self.url = url
- self.name = name
- self.subtitle = subtitle
- self.status = status
- self.queues = queues
- }
-}
-
-final class PlaylistItemsService {
- private var items: [PlaylistItem] = []
-
- var itemsCount: Int {
- items.count
- }
-
- let protectedItemCount: Int
-
- init(initialItemsProvider: () -> [PlaylistItem]) {
- items = initialItemsProvider()
- protectedItemCount = items.count
- }
-
- func item(at index: Int) -> PlaylistItem? {
- guard index < items.count else { return nil }
- return items[index]
- }
-
- func index(for item: PlaylistItem) -> Int? {
- items.firstIndex(of: item)
- }
-
- func add(item: PlaylistItem) {
- items.append(item)
- }
-
- func remove(item: PlaylistItem) {
- if let index = items.firstIndex(of: item) {
- items.remove(at: index)
- }
- }
-
- func setStatus(for index: Int, status: PlaylistItem.Status) {
- guard let item = item(at: index) else {
- return
- }
- items[index] = PlaylistItem(
- url: item.url,
- name: item.name,
- subtitle: item.subtitle,
- status: status,
- queues: item.queues
- )
- }
-}
-
-func provideInitialPlaylistItems() -> [PlaylistItem] {
- let allCases = AudioContent.allCases
- let casesForQueueing: [AudioContent] = [.piano, .local, .khruangbin]
- let allItems = allCases.map { PlaylistItem(content: $0, queues: false) }
- let casesForQueuingItems = casesForQueueing.map { PlaylistItem(content: $0, queues: true) }
- return allItems + casesForQueuingItems
-}
diff --git a/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj b/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..08b7942
--- /dev/null
+++ b/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj
@@ -0,0 +1,503 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 56;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 9806E8182BC5D12500757370 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9806E8172BC5D12500757370 /* App.swift */; };
+ 9806E81A2BC5D12500757370 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9806E8192BC5D12500757370 /* ContentView.swift */; };
+ 9806E81C2BC5D12700757370 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9806E81B2BC5D12700757370 /* Assets.xcassets */; };
+ 9806E81F2BC5D12700757370 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9806E81E2BC5D12700757370 /* Preview Assets.xcassets */; };
+ 9806E8262BC5D2A900757370 /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9806E8252BC5D2A900757370 /* Sidebar.swift */; };
+ 9806E82A2BC68F8700757370 /* AudioPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9806E8292BC68F8700757370 /* AudioPlayerView.swift */; };
+ 9806E8312BC6927D00757370 /* AudioPlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9806E8302BC6927D00757370 /* AudioPlayerModel.swift */; };
+ 9816A8A52BC7D8A200AD1299 /* AudioStreaming.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9816A8A42BC7D8A200AD1299 /* AudioStreaming.framework */; };
+ 9816A8A62BC7D8A200AD1299 /* AudioStreaming.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9816A8A42BC7D8A200AD1299 /* AudioStreaming.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 9816A8AA2BC7F4F000AD1299 /* AudioTrack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9816A8A92BC7F4F000AD1299 /* AudioTrack.swift */; };
+ 9816A8AC2BC820DF00AD1299 /* AudioContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9816A8AB2BC820DF00AD1299 /* AudioContent.swift */; };
+ 9816A8B12BC8330C00AD1299 /* bensound-jazzyfrenchy.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 9816A8AD2BC832DB00AD1299 /* bensound-jazzyfrenchy.mp3 */; };
+ 9816A8B22BC8330C00AD1299 /* bensound-jazzyfrenchy.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 9816A8AE2BC832DB00AD1299 /* bensound-jazzyfrenchy.m4a */; };
+ 9816A8B32BC8330C00AD1299 /* hipjazz.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9816A8AF2BC832DC00AD1299 /* hipjazz.wav */; };
+ 9816A8BB2BC87BC200AD1299 /* AudioPlayerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */; };
+ 984DE9552BDAE59C004B427A /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984DE9542BDAE59C004B427A /* Notifier.swift */; };
+ 984DE9572BDAFC7E004B427A /* AudioPlayerControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984DE9562BDAFC7E004B427A /* AudioPlayerControlsView.swift */; };
+ 98BFB41A2BC97AF800E812C0 /* DisplayLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB4192BC97AF800E812C0 /* DisplayLink.swift */; };
+ 98BFB41D2BCD7BB800E812C0 /* EqualizerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */; };
+ 98BFB41F2BCD814000E812C0 /* EqualizerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB41E2BCD814000E812C0 /* EqualizerService.swift */; };
+ 98BFB4232BCE78AB00E812C0 /* AddNewAudioURLView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB4222BCE78AB00E812C0 /* AddNewAudioURLView.swift */; };
+ 98E6119C2BC72C0E0036BC47 /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E6119B2BC72C0E0036BC47 /* DetailView.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9816A8A72BC7D8A200AD1299 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ 9816A8A62BC7D8A200AD1299 /* AudioStreaming.framework in Embed Frameworks */,
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 9806E8142BC5D12500757370 /* AudioPlayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AudioPlayer.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 9806E8172BC5D12500757370 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; };
+ 9806E8192BC5D12500757370 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
+ 9806E81B2BC5D12700757370 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 9806E81E2BC5D12700757370 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
+ 9806E8252BC5D2A900757370 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = ""; };
+ 9806E8292BC68F8700757370 /* AudioPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerView.swift; sourceTree = ""; };
+ 9806E8302BC6927D00757370 /* AudioPlayerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerModel.swift; sourceTree = ""; };
+ 9816A8A42BC7D8A200AD1299 /* AudioStreaming.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AudioStreaming.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 9816A8A92BC7F4F000AD1299 /* AudioTrack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioTrack.swift; sourceTree = ""; };
+ 9816A8AB2BC820DF00AD1299 /* AudioContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContent.swift; sourceTree = ""; };
+ 9816A8AD2BC832DB00AD1299 /* bensound-jazzyfrenchy.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "bensound-jazzyfrenchy.mp3"; sourceTree = ""; };
+ 9816A8AE2BC832DB00AD1299 /* bensound-jazzyfrenchy.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = "bensound-jazzyfrenchy.m4a"; sourceTree = ""; };
+ 9816A8AF2BC832DC00AD1299 /* hipjazz.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = hipjazz.wav; sourceTree = ""; };
+ 9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerService.swift; sourceTree = ""; };
+ 984DE9542BDAE59C004B427A /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = ""; };
+ 984DE9562BDAFC7E004B427A /* AudioPlayerControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerControlsView.swift; sourceTree = ""; };
+ 98BFB4192BC97AF800E812C0 /* DisplayLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayLink.swift; sourceTree = ""; };
+ 98BFB41B2BCAAD8A00E812C0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
+ 98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualizerView.swift; sourceTree = ""; };
+ 98BFB41E2BCD814000E812C0 /* EqualizerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualizerService.swift; sourceTree = ""; };
+ 98BFB4222BCE78AB00E812C0 /* AddNewAudioURLView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddNewAudioURLView.swift; sourceTree = ""; };
+ 98E6119B2BC72C0E0036BC47 /* DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailView.swift; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 9806E8112BC5D12500757370 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 9816A8A52BC7D8A200AD1299 /* AudioStreaming.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 9806E80B2BC5D12500757370 = {
+ isa = PBXGroup;
+ children = (
+ 9806E8162BC5D12500757370 /* AudioPlayer */,
+ 9806E8152BC5D12500757370 /* Products */,
+ 9816A8A32BC7D8A200AD1299 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 9806E8152BC5D12500757370 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 9806E8142BC5D12500757370 /* AudioPlayer.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 9806E8162BC5D12500757370 /* AudioPlayer */ = {
+ isa = PBXGroup;
+ children = (
+ 98BFB41B2BCAAD8A00E812C0 /* Info.plist */,
+ 984DE9532BDAE57F004B427A /* Dependencies */,
+ 984DE9522BDAE571004B427A /* Helpers */,
+ 9816A8A82BC7F4DE00AD1299 /* Common */,
+ 9806E8282BC68F7300757370 /* Content */,
+ 9806E8272BC68F6600757370 /* Navigation */,
+ 9806E8172BC5D12500757370 /* App.swift */,
+ 9806E81B2BC5D12700757370 /* Assets.xcassets */,
+ 9816A8B02BC832E100AD1299 /* Resources */,
+ 9806E81D2BC5D12700757370 /* Preview Content */,
+ );
+ path = AudioPlayer;
+ sourceTree = "";
+ };
+ 9806E81D2BC5D12700757370 /* Preview Content */ = {
+ isa = PBXGroup;
+ children = (
+ 9806E81E2BC5D12700757370 /* Preview Assets.xcassets */,
+ );
+ path = "Preview Content";
+ sourceTree = "";
+ };
+ 9806E8272BC68F6600757370 /* Navigation */ = {
+ isa = PBXGroup;
+ children = (
+ 9806E8192BC5D12500757370 /* ContentView.swift */,
+ 98E6119B2BC72C0E0036BC47 /* DetailView.swift */,
+ 9806E8252BC5D2A900757370 /* Sidebar.swift */,
+ );
+ path = Navigation;
+ sourceTree = "";
+ };
+ 9806E8282BC68F7300757370 /* Content */ = {
+ isa = PBXGroup;
+ children = (
+ 98E3921C2BD845E100B586E9 /* AudioPlayer */,
+ );
+ path = Content;
+ sourceTree = "";
+ };
+ 9816A8A32BC7D8A200AD1299 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 9816A8A42BC7D8A200AD1299 /* AudioStreaming.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ 9816A8A82BC7F4DE00AD1299 /* Common */ = {
+ isa = PBXGroup;
+ children = (
+ 98BFB4222BCE78AB00E812C0 /* AddNewAudioURLView.swift */,
+ 9816A8A92BC7F4F000AD1299 /* AudioTrack.swift */,
+ 9816A8AB2BC820DF00AD1299 /* AudioContent.swift */,
+ );
+ path = Common;
+ sourceTree = "";
+ };
+ 9816A8B02BC832E100AD1299 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ 9816A8AE2BC832DB00AD1299 /* bensound-jazzyfrenchy.m4a */,
+ 9816A8AF2BC832DC00AD1299 /* hipjazz.wav */,
+ 9816A8AD2BC832DB00AD1299 /* bensound-jazzyfrenchy.mp3 */,
+ );
+ path = Resources;
+ sourceTree = "";
+ };
+ 984DE9522BDAE571004B427A /* Helpers */ = {
+ isa = PBXGroup;
+ children = (
+ 98BFB4192BC97AF800E812C0 /* DisplayLink.swift */,
+ 984DE9542BDAE59C004B427A /* Notifier.swift */,
+ );
+ path = Helpers;
+ sourceTree = "";
+ };
+ 984DE9532BDAE57F004B427A /* Dependencies */ = {
+ isa = PBXGroup;
+ children = (
+ 98BFB41E2BCD814000E812C0 /* EqualizerService.swift */,
+ 9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */,
+ );
+ path = Dependencies;
+ sourceTree = "";
+ };
+ 98E3921C2BD845E100B586E9 /* AudioPlayer */ = {
+ isa = PBXGroup;
+ children = (
+ 9806E8302BC6927D00757370 /* AudioPlayerModel.swift */,
+ 9806E8292BC68F8700757370 /* AudioPlayerView.swift */,
+ 98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */,
+ 984DE9562BDAFC7E004B427A /* AudioPlayerControlsView.swift */,
+ );
+ path = AudioPlayer;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 9806E8132BC5D12500757370 /* AudioPlayer */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 9806E8222BC5D12700757370 /* Build configuration list for PBXNativeTarget "AudioPlayer" */;
+ buildPhases = (
+ 9806E8102BC5D12500757370 /* Sources */,
+ 9806E8112BC5D12500757370 /* Frameworks */,
+ 9806E8122BC5D12500757370 /* Resources */,
+ 9816A8A72BC7D8A200AD1299 /* Embed Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = AudioPlayer;
+ productName = AudioPlayer;
+ productReference = 9806E8142BC5D12500757370 /* AudioPlayer.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 9806E80C2BC5D12500757370 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1530;
+ LastUpgradeCheck = 1530;
+ TargetAttributes = {
+ 9806E8132BC5D12500757370 = {
+ CreatedOnToolsVersion = 15.3;
+ };
+ };
+ };
+ buildConfigurationList = 9806E80F2BC5D12500757370 /* Build configuration list for PBXProject "AudioPlayer" */;
+ compatibilityVersion = "Xcode 14.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 9806E80B2BC5D12500757370;
+ productRefGroup = 9806E8152BC5D12500757370 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 9806E8132BC5D12500757370 /* AudioPlayer */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 9806E8122BC5D12500757370 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 9806E81F2BC5D12700757370 /* Preview Assets.xcassets in Resources */,
+ 9806E81C2BC5D12700757370 /* Assets.xcassets in Resources */,
+ 9816A8B12BC8330C00AD1299 /* bensound-jazzyfrenchy.mp3 in Resources */,
+ 9816A8B22BC8330C00AD1299 /* bensound-jazzyfrenchy.m4a in Resources */,
+ 9816A8B32BC8330C00AD1299 /* hipjazz.wav in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 9806E8102BC5D12500757370 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 98BFB4232BCE78AB00E812C0 /* AddNewAudioURLView.swift in Sources */,
+ 98BFB41D2BCD7BB800E812C0 /* EqualizerView.swift in Sources */,
+ 98BFB41A2BC97AF800E812C0 /* DisplayLink.swift in Sources */,
+ 9806E81A2BC5D12500757370 /* ContentView.swift in Sources */,
+ 98E6119C2BC72C0E0036BC47 /* DetailView.swift in Sources */,
+ 9816A8AC2BC820DF00AD1299 /* AudioContent.swift in Sources */,
+ 9806E8262BC5D2A900757370 /* Sidebar.swift in Sources */,
+ 984DE9552BDAE59C004B427A /* Notifier.swift in Sources */,
+ 9806E82A2BC68F8700757370 /* AudioPlayerView.swift in Sources */,
+ 9806E8312BC6927D00757370 /* AudioPlayerModel.swift in Sources */,
+ 98BFB41F2BCD814000E812C0 /* EqualizerService.swift in Sources */,
+ 9816A8AA2BC7F4F000AD1299 /* AudioTrack.swift in Sources */,
+ 9816A8BB2BC87BC200AD1299 /* AudioPlayerService.swift in Sources */,
+ 984DE9572BDAFC7E004B427A /* AudioPlayerControlsView.swift in Sources */,
+ 9806E8182BC5D12500757370 /* App.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ 9806E8202BC5D12700757370 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ 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;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 17.4;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 9806E8212BC5D12700757370 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ 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;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 17.4;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 9806E8232BC5D12700757370 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"AudioPlayer/Preview Content\"";
+ DEVELOPMENT_TEAM = TJ7GUC6B8Y;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = AudioPlayer/Info.plist;
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioPlayer;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 9806E8242BC5D12700757370 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"AudioPlayer/Preview Content\"";
+ DEVELOPMENT_TEAM = TJ7GUC6B8Y;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = AudioPlayer/Info.plist;
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioPlayer;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 9806E80F2BC5D12500757370 /* Build configuration list for PBXProject "AudioPlayer" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 9806E8202BC5D12700757370 /* Debug */,
+ 9806E8212BC5D12700757370 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 9806E8222BC5D12700757370 /* Build configuration list for PBXNativeTarget "AudioPlayer" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 9806E8232BC5D12700757370 /* Debug */,
+ 9806E8242BC5D12700757370 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 9806E80C2BC5D12500757370 /* Project object */;
+}
diff --git a/AudioExample/AudioExample.xcodeproj/xcshareddata/xcschemes/AudioExample.xcscheme b/AudioPlayer/AudioPlayer.xcodeproj/xcshareddata/xcschemes/AudioPlayer.xcscheme
similarity index 50%
rename from AudioExample/AudioExample.xcodeproj/xcshareddata/xcschemes/AudioExample.xcscheme
rename to AudioPlayer/AudioPlayer.xcodeproj/xcshareddata/xcschemes/AudioPlayer.xcscheme
index 52549b0..5f1763e 100644
--- a/AudioExample/AudioExample.xcodeproj/xcshareddata/xcschemes/AudioExample.xcscheme
+++ b/AudioPlayer/AudioPlayer.xcodeproj/xcshareddata/xcschemes/AudioPlayer.xcscheme
@@ -1,10 +1,11 @@
+ buildImplicitDependencies = "YES"
+ buildArchitectures = "Automatic">
+ BlueprintIdentifier = "9806E8132BC5D12500757370"
+ BuildableName = "AudioPlayer.app"
+ BlueprintName = "AudioPlayer"
+ ReferencedContainer = "container:AudioPlayer.xcodeproj">
@@ -27,64 +28,28 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
- enableThreadSanitizer = "YES"
- codeCoverageEnabled = "YES">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ shouldAutocreateTestPlan = "YES">
+ BlueprintIdentifier = "9806E8132BC5D12500757370"
+ BuildableName = "AudioPlayer.app"
+ BlueprintName = "AudioPlayer"
+ ReferencedContainer = "container:AudioPlayer.xcodeproj">
-
-
-
-
+ BlueprintIdentifier = "9806E8132BC5D12500757370"
+ BuildableName = "AudioPlayer.app"
+ BlueprintName = "AudioPlayer"
+ ReferencedContainer = "container:AudioPlayer.xcodeproj">
diff --git a/AudioPlayer/AudioPlayer/App.swift b/AudioPlayer/AudioPlayer/App.swift
new file mode 100644
index 0000000..eb25da7
--- /dev/null
+++ b/AudioPlayer/AudioPlayer/App.swift
@@ -0,0 +1,55 @@
+//
+// Created by Dimitris C.
+// Copyright © 2024 Decimal. All rights reserved.
+//
+
+import AudioStreaming
+import SwiftUI
+
+@main
+struct AudioPlayerApp: App {
+
+ @State var model = AppModel()
+
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ .environment(model)
+ }
+ }
+}
+
+@Observable
+class AppModel {
+ @ObservationIgnored
+ let audioPlayerService: AudioPlayerService
+ @ObservationIgnored
+ let equalizerService: EqualizerService
+
+ init(
+ audioPlayerService: AudioPlayerService = provideAudioPlayerService(),
+ equalizerService: (AudioPlayerService) -> EqualizerService = provideEqualizerService
+ ) {
+ self.audioPlayerService = audioPlayerService
+ self.equalizerService = equalizerService(audioPlayerService)
+ }
+}
+
+func provideEqualizerService(playerService: AudioPlayerService) -> EqualizerService {
+ EqualizerService(playerService: playerService)
+}
+
+func provideAudioPlayerService() -> AudioPlayerService {
+ AudioPlayerService(
+ audioPlayer: provideDefaultAudioPlayer()
+ )
+}
+
+func provideDefaultAudioPlayer() -> AudioPlayer {
+ AudioPlayer(
+ configuration: .init(
+ flushQueueOnSeek: false,
+ enableLogs: true
+ )
+ )
+}
diff --git a/AudioPlayer/AudioPlayer/Assets.xcassets/AccentColor.colorset/Contents.json b/AudioPlayer/AudioPlayer/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/AudioPlayer/AudioPlayer/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/AudioPlayer/AudioPlayer/Assets.xcassets/AppIcon.appiconset/Contents.json b/AudioPlayer/AudioPlayer/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..13613e3
--- /dev/null
+++ b/AudioPlayer/AudioPlayer/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,13 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/AudioExample/AudioExample/Resources/Assets.xcassets/Contents.json b/AudioPlayer/AudioPlayer/Assets.xcassets/Contents.json
similarity index 100%
rename from AudioExample/AudioExample/Resources/Assets.xcassets/Contents.json
rename to AudioPlayer/AudioPlayer/Assets.xcassets/Contents.json
diff --git a/AudioPlayer/AudioPlayer/Common/AddNewAudioURLView.swift b/AudioPlayer/AudioPlayer/Common/AddNewAudioURLView.swift
new file mode 100644
index 0000000..4021fc7
--- /dev/null
+++ b/AudioPlayer/AudioPlayer/Common/AddNewAudioURLView.swift
@@ -0,0 +1,76 @@
+//
+// Created by Dimitris C.
+// Copyright © 2024 Decimal. All rights reserved.
+//
+
+import SwiftUI
+
+struct AddNewAudioURLView: View {
+ @Environment(\.dismiss) var dismiss
+
+ private let urlStyle = URL.FormatStyle(path: .omitWhen(.path, matches: ["/"]), query: .omitWhen(.query, matches: [""]))
+
+ @State private var audioUrl: URL?
+
+ var onAddNewUrl: (URL) -> Void
+
+ init(onAddNewUrl: @escaping (URL) -> Void) {
+ self.onAddNewUrl = onAddNewUrl
+ }
+
+ var body: some View {
+ NavigationStack {
+ VStack {
+ VStack(alignment: .leading) {
+ TextField(value: $audioUrl, format: urlStyle, prompt: nil, label: {
+ Text("Insert URL")
+ })
+ .keyboardType(.URL)
+ .autocorrectionDisabled()
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ .onSubmit {
+ if let url = audioUrl {
+ onAddNewUrl(url)
+ dismiss()
+ }
+ }
+ }
+ .padding(.horizontal, 16)
+ Button {
+ if let url = audioUrl {
+ onAddNewUrl(url)
+ dismiss()
+ }
+ } label: {
+ HStack {
+ Image(systemName: "plus")
+ Text("Add")
+ }
+ .foregroundStyle(Color.white)
+ }
+ .disabled(audioUrl == nil)
+ .opacity(audioUrl == nil ? 0.5 : 1.0)
+ .padding(.horizontal, 16)
+ .padding(.vertical, 8)
+ .background(.mint)
+ .clipShape(RoundedRectangle(cornerRadius: 16))
+ }
+ .navigationTitle("Add Audio URL")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ Button {
+ dismiss()
+ } label: {
+ Image(systemName: "xmark.circle.fill")
+ .foregroundStyle(Color.gray)
+ }
+ }
+ }
+ }
+ }
+}
+
+#Preview {
+ AddNewAudioURLView(onAddNewUrl: { _ in })
+}
diff --git a/AudioExample/AudioExample/Services/AudioContent.swift b/AudioPlayer/AudioPlayer/Common/AudioContent.swift
similarity index 62%
rename from AudioExample/AudioExample/Services/AudioContent.swift
rename to AudioPlayer/AudioPlayer/Common/AudioContent.swift
index 662f0f5..1375d3c 100644
--- a/AudioExample/AudioExample/Services/AudioContent.swift
+++ b/AudioPlayer/AudioPlayer/Common/AudioContent.swift
@@ -1,18 +1,16 @@
//
-// AudioContent.swift
-// AudioExample
-//
-// Created by Dimitrios Chatzieleftheriou on 14/11/2020.
-// Copyright © 2020 Dimitrios Chatzieleftheriou. All rights reserved.
+// Created by Dimitris C.
+// Copyright © 2024 Decimal. All rights reserved.
//
import Foundation
-enum AudioContent: Int, CaseIterable {
+enum AudioContent {
case offradio
case enlefko
case pepper966
case kosmos
+ case kosmosJazz
case radiox
case khruangbin
case piano
@@ -21,62 +19,71 @@ enum AudioContent: Int, CaseIterable {
case remoteWave
case local
case localWave
+ case custom(String)
var title: String {
switch self {
case .offradio:
- return "Offradio (stream)"
+ return "Offradio"
case .enlefko:
- return "Enlefko (stream)"
+ return "Enlefko"
case .pepper966:
- return "Pepper 96.6 (stream)"
+ return "Pepper 96.6"
case .kosmos:
- return "Kosmos 93.6 (stream)"
+ return "Kosmos 93.6"
+ case .kosmosJazz:
+ return "Kosmos Jazz"
case .radiox:
- return "Radio X (stream)"
+ return "Radio X"
case .khruangbin:
- return "Khruangbin (mp3 preview)"
+ return "Khruangbin"
case .piano:
- return "Piano (mp3)"
+ return "Piano"
case .remoteWave:
- return "Sample remote (wave)"
+ return "Sample remote"
case .local:
- return "Jazzy Frenchy (local mp3)"
+ return "Jazzy Frenchy"
case .localWave:
- return "Local file (local wave)"
+ return "Local file"
case .optimized:
- return "Jazze French (m4a - optimized)"
+ return "Jazzy Frenchy"
case .nonOptimized:
- return "Jazze French (m4a - non-optimized)"
+ return "Jazzy Frenchy"
+ case .custom(let url):
+ return url
}
}
var subtitle: String? {
switch self {
case .offradio:
- return nil
+ return "Stream • offradio.gr"
case .enlefko:
- return nil
+ return "Stream • enlefko.fm"
case .pepper966:
- return nil
+ return "Stream • pepper966.gr"
case .kosmos:
- return nil
+ return "Stream • ertecho.gr"
+ case .kosmosJazz:
+ return "Stream • ertecho.gr"
case .radiox:
- return nil
+ return "Stream • globalplayer.com"
case .khruangbin:
- return nil
+ return "Remote mp3"
case .piano:
- return nil
+ return "Remote mp3"
case .remoteWave:
- return nil
+ return "wave"
case .local:
return "Music by: bensound.com"
case .localWave:
return "Music by: bensound.com"
case .optimized:
- return "Music by: bensound.com"
+ return "Music by: bensound.com - m4a optimized"
case .nonOptimized:
- return "Music by: bensound.com"
+ return "Music by: bensound.com - m4a non-optimized"
+ case .custom:
+ return ""
}
}
@@ -90,6 +97,8 @@ enum AudioContent: Int, CaseIterable {
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 .kosmosJazz:
+ return URL(string: "https://radiostreaming.ert.gr/ert-webjazz")!
case .radiox:
return URL(string: "https://media-ssl.musicradio.com/RadioXLondon")!
case .khruangbin:
@@ -101,13 +110,15 @@ enum AudioContent: Int, CaseIterable {
case .nonOptimized:
return URL(string: "https://github.com/dimitris-c/sample-audio/raw/main/bensound-jazzyfrenchy.m4a")!
case .local:
- let path = Bundle.main.path(forResource: "bensound-jazzyfrenchy", ofType: "m4a")!
+ let path = Bundle.main.path(forResource: "bensound-jazzyfrenchy", ofType: "mp3")!
return URL(fileURLWithPath: path)
case .localWave:
let path = Bundle.main.path(forResource: "hipjazz", ofType: "wav")!
return URL(fileURLWithPath: path)
case .remoteWave:
- return URL(string: "https://file-examples.com/wp-content/storage/2017/11/file_example_WAV_5MG.wav")!
+ return URL(string: "https://github.com/dimitris-c/sample-audio/raw/main/5-MB-WAV.wav")!
+ case .custom(let url):
+ return URL(string: url)!
}
}
}
diff --git a/AudioPlayer/AudioPlayer/Common/AudioTrack.swift b/AudioPlayer/AudioPlayer/Common/AudioTrack.swift
new file mode 100644
index 0000000..c7456eb
--- /dev/null
+++ b/AudioPlayer/AudioPlayer/Common/AudioTrack.swift
@@ -0,0 +1,118 @@
+//
+// Created by Dimitris C.
+// Copyright © 2024 Decimal. All rights reserved.
+//
+
+
+import SwiftUI
+
+enum AudioTrackStatus {
+ case playing
+ case paused
+ case buffering
+ case error
+ case idle
+
+ var isPlaying: Bool {
+ self == .playing || self == .paused
+ }
+
+ var isError: Bool {
+ self == .error
+ }
+}
+
+@Observable
+public class AudioTrack: Identifiable, Equatable {
+ public static func == (lhs: AudioTrack, rhs: AudioTrack) -> Bool {
+ lhs.id == rhs.id
+ }
+
+ public var id: String {
+ url.absoluteString
+ }
+ let title: String
+ let subtitle: String?
+ let url: URL
+
+ var status: AudioTrackStatus
+
+ private let content: AudioContent
+
+ init(from content: AudioContent, status: AudioTrackStatus = .idle) {
+ self.title = content.title
+ self.subtitle = content.subtitle
+ self.status = status
+ self.url = content.streamUrl
+ self.content = content
+ }
+}
+
+
+struct AudioTrackView: View {
+ @Bindable var track: AudioTrack
+
+ private let action: () -> Void
+
+ init(track: AudioTrack, action: @escaping () -> Void = {}) {
+ self.track = track
+ self.action = action
+ }
+
+ var body: some View {
+ Button(action: action, label: {
+ HStack {
+ VStack(alignment: .leading) {
+ Text(track.title)
+ .font(.headline)
+ .fontWeight(.medium)
+ .foregroundStyle(.black)
+ .padding(.top, 8)
+ .padding(.bottom, track.subtitle == nil ? 8 : 0)
+ if let subtitle = track.subtitle {
+ Text(subtitle)
+ .font(.subheadline)
+ .fontWeight(.regular)
+ .foregroundStyle(Color.gray)
+ }
+ }
+ Spacer()
+ status
+ }
+ })
+ }
+
+ @ViewBuilder var status: some View {
+ if track.status == .error {
+ Image(systemName: "exclamationmark.circle")
+ .font(.headline)
+ .foregroundStyle(.red)
+ } else {
+ if track.status.isPlaying {
+ Image(systemName: track.status == .playing ? "play.fill" : "pause.fill")
+ .font(.headline)
+ .foregroundStyle(.mint)
+ } else if track.status == .buffering {
+ ProgressView()
+ .progressViewStyle(.circular)
+ }
+ }
+ }
+}
+
+#Preview {
+ List {
+ AudioTrackView(
+ track: AudioTrack(from: .enlefko)
+ )
+ AudioTrackView(
+ track: AudioTrack(from: .enlefko, status: .playing)
+ )
+ AudioTrackView(
+ track: AudioTrack(from: .enlefko, status: .paused)
+ )
+ AudioTrackView(
+ track: AudioTrack(from: .enlefko, status: .error)
+ )
+ }
+}
diff --git a/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerControlsView.swift b/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerControlsView.swift
new file mode 100644
index 0000000..35a4d0b
--- /dev/null
+++ b/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerControlsView.swift
@@ -0,0 +1,285 @@
+//
+// Created by Dimitris Chatzieleftheriou on 26/04/2024.
+//
+
+import SwiftUI
+
+struct AudioPlayerControls: View {
+ @State var model: Model
+ @Binding var currentTrack: AudioTrack?
+
+ init(appModel: AppModel, currentTrack: Binding) {
+ self._model = State(wrappedValue: Model(audioPlayerService: appModel.audioPlayerService))
+ self._currentTrack = currentTrack
+ }
+
+ var body: some View {
+ VStack(alignment: .leading) {
+ HStack {
+ Button(action: { model.playPause() }) {
+ Image(systemName: model.isPlaying ? "pause.fill" : "play.fill")
+ .font(.title)
+ .imageScale(.small)
+ }
+ .contentTransition(.symbolEffect(.replace))
+ Button(action: {
+ model.stop()
+ currentTrack = nil
+ }) {
+ Image(systemName: "stop.fill")
+ .font(.title)
+ .imageScale(.small)
+ }
+ .padding(.leading, 8)
+ Spacer()
+ Button(action: { model.mute() }) {
+ Image(systemName: model.isMuted ? "speaker.slash.fill" : "speaker.fill")
+ .font(.title)
+ .imageScale(.small)
+ }
+ .frame(width: 20, height: 20)
+ .contentTransition(.symbolEffect(.replace))
+ }
+ .tint(.mint)
+ .padding(16)
+ if let audioMetadata = model.liveAudioMetadata, model.isLiveAudioStreaming {
+ Text("Now Playing: \(audioMetadata)")
+ .font(.caption)
+ .foregroundStyle(.black)
+ .padding(.horizontal, 16)
+ }
+ Divider()
+ VStack {
+ Slider(
+ value: $model.currentTime,
+ in: 0...(model.totalTime ?? 1.0),
+ onEditingChanged: { scrubStarted in
+ if scrubStarted {
+ model.scrubState = .started
+ } else {
+ model.scrubState = .ended(model.currentTime)
+ }
+ }
+ )
+ .disabled(model.totalTime == nil)
+ HStack {
+ Text(model.formattedCurrentTime ?? "--:--")
+ Spacer()
+ Text(model.formattedTotalTime ?? "")
+ }
+ .foregroundStyle(.black)
+ .font(.caption)
+ .fontWeight(.medium)
+ }
+ .padding(.bottom, 8)
+ .padding(.horizontal, 16)
+ Divider()
+ VStack(alignment: .leading) {
+ Text("Playback Rate: \(String(format: "%0.1f", model.playbackRate))")
+ .font(.subheadline)
+ .fontWeight(.medium)
+ .foregroundStyle(.black)
+ Slider(value: $model.playbackRate, in: 1.0...4.0, step: 0.2)
+ .onChange(of: model.playbackRate) { _, new in
+ model.update(rate: Float(new))
+ }
+ }
+ .padding(.bottom, 8)
+ .padding(.horizontal, 16)
+ }
+ .onChange(of: currentTrack) { oldValue, newValue in
+ if let track = newValue {
+ model.play(track)
+ }
+ }
+ }
+}
+
+enum ScrubState: Equatable {
+ case idle
+ case started
+ case ended(Double)
+}
+
+extension AudioPlayerControls {
+ @Observable
+ final class Model {
+ @ObservationIgnored
+ private(set) var audioPlayerService: AudioPlayerService
+ @ObservationIgnored
+ private var displayLink: DisplayLink?
+
+ var isLiveAudioStreaming: Bool {
+ totalTime == 0
+ }
+
+ var liveAudioMetadata: String?
+
+ var isPlaying: Bool = false
+ var isMuted: Bool = false
+
+ var playbackRate: Double = 0.0
+
+ var currentTime: Double = 0
+ var totalTime: Double?
+
+ var scrubState: ScrubState = .idle
+
+ var formattedCurrentTime: String?
+ var formattedTotalTime: String?
+
+ var currentTrack: AudioTrack?
+
+ init(audioPlayerService: AudioPlayerService) {
+ self.audioPlayerService = audioPlayerService
+
+ registerObservations()
+ }
+
+ deinit {
+ displayLink?.deactivate()
+ displayLink = nil
+ }
+
+ func registerObservations() {
+ Task { @MainActor in
+ for await status in await audioPlayerService.statusChangedNotifier.values() {
+ isPlaying = status == .playing
+ displayLink?.isPaused = !isPlaying
+ switch status {
+ case .bufferring:
+ currentTrack?.status = .buffering
+ case .error:
+ currentTrack?.status = .error
+ currentTrack = nil
+ case .playing:
+ currentTrack?.status = .playing
+ case .paused:
+ currentTrack?.status = .paused
+ case .stopped:
+ currentTrack?.status = .idle
+ default:
+ currentTrack?.status = .idle
+ }
+ }
+ }
+
+ Task { @MainActor in
+ for await metadata in await audioPlayerService.metadataReceivedNotifier.values() {
+ guard !metadata.isEmpty else { break }
+ if let title = metadata["StreamTitle"] {
+ liveAudioMetadata = title.isEmpty ? "-" : title
+ } else {
+ liveAudioMetadata = nil
+ }
+ }
+ }
+
+ Task { @MainActor in
+ for await startStopped in await audioPlayerService.playingStartedStopped.values() {
+ if startStopped.started {
+ self.didStartPlaying()
+ } else {
+ self.didStopPlaying()
+ }
+ }
+ }
+ }
+
+ func mute() {
+ isMuted.toggle()
+ audioPlayerService.toggleMute()
+ }
+
+ func playPause() {
+ if audioPlayerService.state == .playing {
+ audioPlayerService.pause()
+ } else {
+ audioPlayerService.resume()
+ }
+ }
+
+ func update(rate: Float) {
+ let rate = round(rate / 0.2) * 0.2
+ audioPlayerService.update(rate: rate)
+ }
+
+ func stop() {
+ isPlaying = false
+ audioPlayerService.stop()
+ currentTrack?.status = .idle
+ currentTrack = nil
+ }
+
+ func play(_ track: AudioTrack) {
+ if track != currentTrack {
+ currentTrack?.status = .idle
+ audioPlayerService.play(url: track.url)
+ currentTrack = track
+ }
+ }
+
+ func onTick() {
+ let duration = audioPlayerService.duration
+ let progress = audioPlayerService.progress
+ if duration > 0 {
+ let elapsed = Int(progress)
+ let remaining = Int(duration - progress)
+ totalTime = duration
+ switch scrubState {
+ case .idle:
+ currentTime = progress
+ case .started:
+ break
+ case .ended(let seekTime):
+ currentTime = seekTime
+ if audioPlayerService.duration > 0 {
+ audioPlayerService.seek(at: seekTime)
+ }
+ scrubState = .idle
+ }
+ formattedCurrentTime = timeFrom(seconds: Int(elapsed))
+ formattedTotalTime = timeFrom(seconds: remaining)
+ } else {
+ let elapsed = Int(progress)
+ formattedCurrentTime = timeFrom(seconds: Int(elapsed))
+ if formattedTotalTime != nil {
+ formattedTotalTime = nil
+ }
+ }
+ }
+
+ func resetLabels() {
+ currentTime = 0
+ totalTime = 0
+ formattedCurrentTime = nil
+ formattedTotalTime = nil
+ }
+
+ private func timeFrom(seconds: Int) -> String {
+ let correctSeconds = seconds % 60
+ let minutes = (seconds / 60) % 60
+ let hours = seconds / 3600
+
+ if hours > 0 {
+ return String(format: "%02d:%02d:%02d", hours, minutes, correctSeconds)
+ }
+ return String(format: "%02d:%02d", minutes, correctSeconds)
+ }
+
+ private func didStartPlaying() {
+ self.displayLink = DisplayLink(onTick: { [weak self] _ in
+ self?.onTick()
+ })
+ displayLink?.activate()
+ }
+
+ private func didStopPlaying() {
+ resetLabels()
+ liveAudioMetadata = nil
+ playbackRate = 1.0
+ displayLink?.deactivate()
+ }
+ }
+
+}
diff --git a/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerModel.swift b/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerModel.swift
new file mode 100644
index 0000000..3c4deff
--- /dev/null
+++ b/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerModel.swift
@@ -0,0 +1,69 @@
+//
+// Created by Dimitris C.
+// Copyright © 2024 Decimal. All rights reserved.
+//
+
+import UIKit
+import Foundation
+import AudioStreaming
+
+struct AudioPlaylist: Equatable, Identifiable {
+ var id: String { title }
+ let title: String
+ var tracks: [AudioTrack]
+}
+
+@Observable
+public class AudioPlayerModel {
+ @ObservationIgnored
+ private(set) var audioPlayerService: AudioPlayerService
+
+ var audioTracks: [AudioPlaylist] = []
+
+ var currentTrack: AudioTrack?
+
+ init(audioTracksProvider: () -> [AudioPlaylist] = audioTracksProvider, audioPlayerService: AudioPlayerService) {
+ self.audioPlayerService = audioPlayerService
+ self.audioTracks = audioTracksProvider()
+ }
+
+ deinit {
+ audioPlayerService.stop()
+ }
+
+ func addNewAudioTrack(url: URL) {
+ let customIndex = audioTracks.firstIndex(where: { $0.id == "Custom" })
+ let audioTrack = AudioTrack(from: .custom(url.absoluteString), status: .idle)
+ let playlist = AudioPlaylist(title: "Custom", tracks: [audioTrack])
+ if let customIndex {
+ let tracks = audioTracks[customIndex].tracks
+ if !tracks.contains(audioTrack) {
+ audioTracks[customIndex].tracks.append(audioTrack)
+ }
+ } else {
+ audioTracks.append(playlist)
+ }
+ }
+
+ func play(_ track: AudioTrack) {
+ if track != currentTrack {
+ currentTrack = track
+ }
+ }
+}
+
+private let radioTracks: [AudioContent] = [.offradio, .enlefko, .pepper966, .kosmos, .kosmosJazz, .radiox]
+private let audioTracks: [AudioContent] = [.khruangbin, .piano, .optimized, .nonOptimized, .remoteWave, .local, .localWave]
+
+func audioTracksProvider() -> [AudioPlaylist] {
+ [
+ AudioPlaylist(title: "Radio", tracks: radioTracks.map { AudioTrack.init(from: $0) }),
+ AudioPlaylist(title: "Tracks", tracks: audioTracks.map { AudioTrack.init(from:$0) })
+ ]
+}
+
+func audioQueueTrackProvider() -> [AudioPlaylist] {
+ [
+ AudioPlaylist(title: "Tracks", tracks: audioTracks.map { AudioTrack.init(from:$0) })
+ ]
+}
diff --git a/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerView.swift b/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerView.swift
new file mode 100644
index 0000000..3853fa7
--- /dev/null
+++ b/AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerView.swift
@@ -0,0 +1,86 @@
+//
+// Created by Dimitris C.
+// Copyright © 2024 Decimal. All rights reserved.
+//
+
+import SwiftUI
+
+struct AudioPlayerView: View {
+ @Environment(AppModel.self) var appModel
+
+ @State var model: AudioPlayerModel
+
+ @State var eqSheetIsShown: Bool = false
+ @State var addNewAudioIsShown: Bool = false
+
+ init(appModel: AppModel) {
+ self._model = State(wrappedValue: AudioPlayerModel(audioPlayerService: appModel.audioPlayerService))
+ }
+
+ var body: some View {
+ ScrollViewReader { proxy in
+ List {
+ ForEach(model.audioTracks) { section in
+ Section {
+ ForEach(section.tracks) { track in
+ AudioTrackView(track: track) {
+ model.play(track)
+ }
+ .id(track.id)
+ }
+ } header: {
+ Text(section.title)
+ }
+ }
+ }
+ .onChange(of: model.audioTracks) { _, newValue in
+ if let lastId = newValue.last?.tracks.last?.id {
+ withAnimation {
+ proxy.scrollTo(lastId, anchor: .bottom)
+ }
+ }
+ }
+ }
+ .safeAreaInset(edge: .bottom) {
+ AudioPlayerControls(appModel: appModel, currentTrack: $model.currentTrack)
+ .background(
+ .ultraThinMaterial.shadow(
+ ShadowStyle.drop(color: .black.opacity(0.1), radius: 8, x: 0, y: -10)
+ )
+ )
+ }
+ .ignoresSafeArea(.keyboard, edges: .bottom)
+ .navigationTitle("Audio Player")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItemGroup(placement: .topBarTrailing) {
+ Button {
+ eqSheetIsShown.toggle()
+ } label: {
+ Image(systemName: "slider.horizontal.3")
+ }
+ Button {
+ addNewAudioIsShown.toggle()
+ } label: {
+ Image(systemName: "plus")
+ }
+ }
+ }
+ .sheet(isPresented: $eqSheetIsShown) {
+ EqualizerView(appModel: appModel)
+ .presentationDetents([.medium])
+ }
+ .sheet(isPresented: $addNewAudioIsShown) {
+ AddNewAudioURLView(
+ onAddNewUrl: { url in
+ model.addNewAudioTrack(url: url)
+ }
+ )
+ .presentationDetents([.height(150)])
+ }
+ }
+}
+
+#Preview {
+ AudioPlayerView(appModel: AppModel())
+}
diff --git a/AudioPlayer/AudioPlayer/Content/AudioPlayer/EqualizerView.swift b/AudioPlayer/AudioPlayer/Content/AudioPlayer/EqualizerView.swift
new file mode 100644
index 0000000..1e0bf36
--- /dev/null
+++ b/AudioPlayer/AudioPlayer/Content/AudioPlayer/EqualizerView.swift
@@ -0,0 +1,327 @@
+//
+// Created by Dimitris C.
+// Copyright © 2024 Decimal. All rights reserved.
+//
+
+import SwiftUI
+
+@Observable
+class EQBand: Identifiable {
+ var frequency: String
+ var min: Float
+ var max: Float
+ var value: Float
+
+ @ObservationIgnored
+ let index: Int
+
+ init(index: Int, frequency: String, min: Float, max: Float, value: Float) {
+ self.index = index
+ self.frequency = frequency
+ self.min = min
+ self.max = max
+ self.value = value
+ }
+}
+
+struct EqualizerView: View {
+ @Environment(\.dismiss) var dismiss
+
+ @Environment(AppModel.self) var appModel
+
+ @State var model: Model
+
+ init(appModel: AppModel) {
+ self._model = State(wrappedValue: Model(equalizerService: appModel.equalizerService))
+ }
+
+ var body: some View {
+ NavigationStack {
+ VStack(spacing: 16) {
+ EQSliderView()
+ .frame(height: 180)
+ .padding(.horizontal, 16)
+ .environment(model)
+
+ HStack(alignment: .center, spacing: 16) {
+ Button {
+ withAnimation {
+ model.isEnabled.toggle()
+ model.enable()
+ }
+ } label: {
+ HStack {
+ Image(systemName: model.isEnabled ? "waveform.slash" : "waveform")
+ .contentTransition(.symbolEffect(.replace))
+ Text(model.isEnabled ? "Disable": "Enable")
+ .font(.body)
+ }
+ .foregroundStyle(Color.white)
+ }
+ .padding(.horizontal, 16)
+ .padding(.vertical, 16)
+ .background(model.isEnabled ? .red : .mint)
+ .clipShape(RoundedRectangle(cornerRadius: 16))
+
+ Button {
+ model.reset()
+ } label: {
+ HStack {
+ Text("Reset")
+ .font(.body)
+ }
+ .foregroundStyle(Color.white)
+ }
+ .padding(.horizontal, 16)
+ .padding(.vertical, 16)
+ .background(.mint)
+ .clipShape(RoundedRectangle(cornerRadius: 16))
+ }
+ .padding(.top, 24)
+ }
+ .task {
+ Task {
+ model.generateBands()
+ }
+ }
+ .navigationTitle("Equalizer")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ Button {
+ dismiss()
+ } label: {
+ Image(systemName: "xmark.circle.fill")
+ .foregroundStyle(Color.gray)
+ }
+ }
+ }
+ }
+ }
+}
+
+struct EQSliderView: View {
+ @Environment(EqualizerView.Model.self) var eqModel
+
+ @State private var dragPointYLocations: [CGFloat] = Array(repeating: .zero, count: 6)
+ @State private var resetPoints: [Double] = Array(repeating: .zero, count: 6)
+
+ @State private var eqViewFrame: CGRect = .zero
+
+ var body: some View {
+ VStack(spacing: 0) {
+ HStack(spacing: 0) {
+ // Draw labels for gain values
+ VStack {
+ Text("\(Int(eqModel.maxGain))db")
+ .font(.caption2)
+ .foregroundColor(.black)
+ Spacer()
+ Text("0dB")
+ .font(.caption2)
+ .foregroundColor(.black)
+ Spacer()
+ Text("\(Int(eqModel.minGain))db")
+ .font(.caption2)
+ .foregroundColor(.black)
+ }
+ GeometryReader { innerGeo in
+ ZStack {
+ LineShape(values: eqModel.shouldReset ? resetPoints : dragPointYLocations.map { Double($0) })
+ .stroke(Color.mint, lineWidth: 2)
+ .animation(.easeInOut(duration: 0.2), value: eqModel.shouldReset)
+ .onAppear {
+ resetPoints = resetPoints.map { _ in Double(gainToYPosition(at: 0, in: innerGeo.size)) }
+ }
+
+ Path { path in
+ for index in 0.. CGFloat {
+ size.width / 12 * CGFloat(index * 2 + 1)
+ }
+
+ func updateGainValue(at index: Int, in size: CGSize) {
+ let percentage = dragPointYLocations[index] / size.height
+ let gain = (1 - Float(percentage)) * (eqModel.maxGain - eqModel.minGain) + eqModel.minGain
+ eqModel.update(gain: gain, index: index)
+ }
+
+ func gainToYPosition(at gain: Float, in size: CGSize) -> CGFloat {
+ let percentage = 1 - (gain - eqModel.minGain) / (eqModel.maxGain - eqModel.minGain)
+ return CGFloat(percentage) * size.height
+ }
+
+ func resetPositions(in size: CGSize) {
+ let reset = dragPointYLocations.map { _ in gainToYPosition(at: 0, in: size) }
+ withAnimation(.easeInOut(duration: 0.2)) {
+ dragPointYLocations = reset
+ }
+
+ }
+}
+
+extension EqualizerView {
+ @Observable
+ class Model {
+ @ObservationIgnored
+ private let equalizerService: EqualizerService
+
+ var dragPointYLocations: [CGFloat] = Array(repeating: .zero, count: 6)
+
+ var isEnabled: Bool = false
+
+ var bands: [EQBand] = []
+
+ let minGain: Float = -12
+ let maxGain: Float = 12
+
+ var shouldReset: Bool = false
+
+ init(equalizerService: EqualizerService) {
+ self.equalizerService = equalizerService
+ isEnabled = equalizerService.isActivated
+ }
+
+ func generateBands() {
+ bands = equalizerService.bands.enumerated().map { index, item in
+ var measurement = item.frequency
+ var frequency = String(Int(measurement))
+ if item.frequency >= 1000 {
+ measurement = item.frequency / 1000
+ frequency = "\(String(Int(measurement)))K"
+ }
+ return EQBand(index: index, frequency: frequency, min: minGain, max: maxGain, value: item.gain)
+ }
+ }
+
+ func enable() {
+ if isEnabled {
+ equalizerService.activate()
+ } else {
+ equalizerService.deactivate()
+ }
+ }
+
+ func update(gain: Float, index: Int) {
+ shouldReset = false
+ bands[index].value = gain
+ equalizerService.update(gain: gain, for: index)
+ }
+
+ func reset() {
+ guard !shouldReset else {
+ return
+ }
+ shouldReset = true
+ equalizerService.reset()
+ for band in bands {
+ band.value = 0.0
+ }
+ }
+ }
+}
+
+struct LineShape: Shape {
+ var values: [Double]
+
+ var animatableData: AnimatableLine {
+ get { AnimatableLine(values: values) }
+ set { values = newValue.values }
+ }
+
+ func path(in rect: CGRect) -> Path {
+ var path = Path()
+ path.move(to: CGPoint(x: rect.size.width / 12, y: values.first ?? 0))
+ for index in 1.. CGFloat {
+ size.width / 12 * CGFloat(index * 2 + 1)
+ }
+}
+
+struct AnimatableLine : VectorArithmetic {
+ var values: [Double]
+
+ var magnitudeSquared: Double {
+ return values.map { $0 * $0 }.reduce(0, +)
+ }
+
+ mutating func scale(by rhs: Double) {
+ values = values.map { $0 * rhs }
+ }
+
+ static var zero: AnimatableLine {
+ return AnimatableLine(values: [0.0])
+ }
+
+ static func - (lhs: AnimatableLine, rhs: AnimatableLine) -> AnimatableLine {
+ return AnimatableLine(values: zip(lhs.values, rhs.values).map(-))
+ }
+
+ static func -= (lhs: inout AnimatableLine, rhs: AnimatableLine) {
+ lhs = lhs - rhs
+ }
+
+ static func + (lhs: AnimatableLine, rhs: AnimatableLine) -> AnimatableLine {
+ return AnimatableLine(values: zip(lhs.values, rhs.values).map(+))
+ }
+
+ static func += (lhs: inout AnimatableLine, rhs: AnimatableLine) {
+ lhs = lhs + rhs
+ }
+}
+
+#Preview {
+ EqualizerView(appModel: AppModel())
+}
diff --git a/AudioExample/AudioExample/Services/AudioPlayerService.swift b/AudioPlayer/AudioPlayer/Dependencies/AudioPlayerService.swift
similarity index 77%
rename from AudioExample/AudioExample/Services/AudioPlayerService.swift
rename to AudioPlayer/AudioPlayer/Dependencies/AudioPlayerService.swift
index 1578924..0db2e44 100644
--- a/AudioExample/AudioExample/Services/AudioPlayerService.swift
+++ b/AudioPlayer/AudioPlayer/Dependencies/AudioPlayerService.swift
@@ -1,24 +1,21 @@
//
-// AudioPlayerService.swift
-// AudioExample
-//
-// Created by Dimitrios Chatzieleftheriou on 14/11/2020.
-// Copyright © 2020 Dimitrios Chatzieleftheriou. All rights reserved.
+// Created by Dimitris C.
+// Copyright © 2024 Decimal. All rights reserved.
//
import AudioStreaming
import AVFoundation
protocol AudioPlayerServiceDelegate: AnyObject {
- func didStartPlaying()
- func didStopPlaying()
+ func didStartPlaying(id: AudioEntryId)
+ func didStopPlaying(id: AudioEntryId, reason: AudioPlayerStopReason)
func statusChanged(status: AudioPlayerState)
func errorOccurred(error: AudioPlayerError)
func metadataReceived(metadata: [String: String])
}
final class AudioPlayerService {
- var delegate = MulticastDelegate()
+ weak var delegate: AudioPlayerServiceDelegate?
private var player: AudioPlayer
private var audioSystemResetObserver: Any?
@@ -43,8 +40,12 @@ final class AudioPlayerService {
player.state
}
- init() {
- player = AudioPlayer(configuration: .init(enableLogs: true))
+ var statusChangedNotifier = Notifier()
+ var metadataReceivedNotifier = Notifier<[String: String]>()
+ var playingStartedStopped = Notifier<(started: Bool, AudioEntryId, AudioPlayerStopReason?)>()
+
+ init(audioPlayer: AudioPlayer) {
+ player = audioPlayer
player.delegate = self
configureAudioSession()
@@ -98,8 +99,8 @@ final class AudioPlayerService {
}
}
- func seek(at time: Float) {
- player.seek(to: Double(time))
+ func seek(at time: Double) {
+ player.seek(to: time)
}
private func recreatePlayer() {
@@ -109,10 +110,11 @@ final class AudioPlayerService {
private func registerSessionEvents() {
// Note that a real app might need to observer other AVAudioSession notifications as well
- audioSystemResetObserver = NotificationCenter.default.addObserver(forName: AVAudioSession.mediaServicesWereResetNotification,
- object: nil,
- queue: nil)
- { [unowned self] _ in
+ audioSystemResetObserver = NotificationCenter.default.addObserver(
+ forName: AVAudioSession.mediaServicesWereResetNotification,
+ object: nil,
+ queue: nil
+ ) { [unowned self] _ in
self.configureAudioSession()
self.recreatePlayer()
}
@@ -151,14 +153,16 @@ final class AudioPlayerService {
extension AudioPlayerService: AudioPlayerDelegate {
func audioPlayerDidStartPlaying(player _: AudioPlayer, with id: AudioEntryId) {
print("audioPlayerDidStartPlaying entryId: \(id)")
- delegate.invoke(invocation: { $0.didStartPlaying() })
+ delegate?.didStartPlaying(id: id)
+ Task { await playingStartedStopped.send((true, id, nil)) }
}
func audioPlayerDidFinishBuffering(player _: AudioPlayer, with _: AudioEntryId) {}
func audioPlayerStateChanged(player _: AudioPlayer, with newState: AudioPlayerState, previous _: AudioPlayerState) {
print("audioPlayerDidStartPlaying newState: \(newState)")
- delegate.invoke(invocation: { $0.statusChanged(status: newState) })
+ Task { await statusChangedNotifier.send(newState) }
+ delegate?.statusChanged(status: newState)
}
func audioPlayerDidFinishPlaying(player _: AudioPlayer,
@@ -168,16 +172,19 @@ extension AudioPlayerService: AudioPlayerDelegate {
duration _: Double)
{
print("audioPlayerDidFinishPlaying entryId: \(id), reason: \(reason)")
- delegate.invoke(invocation: { $0.didStopPlaying() })
+ Task { await playingStartedStopped.send((false, id, reason)) }
+ delegate?.didStopPlaying(id: id, reason: reason)
}
func audioPlayerUnexpectedError(player _: AudioPlayer, error: AudioPlayerError) {
- delegate.invoke(invocation: { $0.errorOccurred(error: error) })
+ delegate?.errorOccurred(error: error)
}
func audioPlayerDidCancel(player _: AudioPlayer, queuedItems _: [AudioEntryId]) {}
func audioPlayerDidReadMetadata(player _: AudioPlayer, metadata: [String: String]) {
- delegate.invoke(invocation: { $0.metadataReceived(metadata: metadata) })
+ Task { await metadataReceivedNotifier.send(metadata) }
+ delegate?.metadataReceived(metadata: metadata)
}
}
+
diff --git a/AudioExample/AudioExample/Services/EqualizerService.swift b/AudioPlayer/AudioPlayer/Dependencies/EqualizerService.swift
similarity index 80%
rename from AudioExample/AudioExample/Services/EqualizerService.swift
rename to AudioPlayer/AudioPlayer/Dependencies/EqualizerService.swift
index 90599b4..c52a3ef 100644
--- a/AudioExample/AudioExample/Services/EqualizerService.swift
+++ b/AudioPlayer/AudioPlayer/Dependencies/EqualizerService.swift
@@ -1,16 +1,13 @@
//
-// EqualizerService.swift
-// AudioExample
-//
-// Created by Dimitrios Chatzieleftheriou on 15/11/2020.
-// Copyright © 2020 Dimitrios Chatzieleftheriou. All rights reserved.
+// Created by Dimitris C.
+// Copyright © 2024 Decimal. All rights reserved.
//
import AVFoundation
final class EqualizerService {
private let playerService: AudioPlayerService
- private let _freqs = [32, 64, 128, 250, 500, 1000, 2000, 4000, 8000, 16000]
+ private let _freqs = [60, 150, 400, 1000, 2400, 15000]
private let eqUnit: AVAudioUnitEQ
var bands: [AVAudioUnitEQFilterParameters] {
diff --git a/AudioPlayer/AudioPlayer/Helpers/DisplayLink.swift b/AudioPlayer/AudioPlayer/Helpers/DisplayLink.swift
new file mode 100644
index 0000000..29cbd06
--- /dev/null
+++ b/AudioPlayer/AudioPlayer/Helpers/DisplayLink.swift
@@ -0,0 +1,49 @@
+//
+// Created by Dimitris C.
+// Copyright © 2024 Decimal. All rights reserved.
+//
+
+import UIKit
+
+final class DisplayLink {
+
+ private var displayLink: CADisplayLink?
+ private var target = DisplayLinkTarget()
+
+ var isPaused: Bool = true {
+ didSet {
+ displayLink?.isPaused = isPaused
+ }
+ }
+
+ init(onTick: @escaping (CADisplayLink) -> Void) {
+ target.onTick = onTick
+ }
+
+ deinit {
+ deactivate()
+ }
+
+ func activate() {
+ displayLink?.invalidate()
+ displayLink = nil
+ displayLink = CADisplayLink(target: target, selector: #selector(DisplayLinkTarget.tick(_:)))
+ displayLink?.preferredFrameRateRange = .init(minimum: 6, maximum: 10)
+ displayLink?.add(to: .current, forMode: .common)
+ self.isPaused = false
+ }
+
+ func deactivate() {
+ isPaused = true
+ displayLink?.invalidate()
+ displayLink = nil
+ }
+}
+
+private final class DisplayLinkTarget {
+ var onTick: ((CADisplayLink) -> Void)?
+
+ @objc func tick(_ link: CADisplayLink) {
+ onTick?(link)
+ }
+}
diff --git a/AudioPlayer/AudioPlayer/Helpers/Notifier.swift b/AudioPlayer/AudioPlayer/Helpers/Notifier.swift
new file mode 100644
index 0000000..ec03c40
--- /dev/null
+++ b/AudioPlayer/AudioPlayer/Helpers/Notifier.swift
@@ -0,0 +1,29 @@
+//
+// Created by Dimitris Chatzieleftheriou on 25/04/2024.
+//
+
+import Foundation
+
+actor Notifier