Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1801e5065 | |||
| d041f17e76 | |||
| 5db7c2921b | |||
| dec732a597 | |||
| 51af91f4d4 | |||
| 02ba7012da | |||
| 94bc48c7f1 | |||
| 473afdaa50 | |||
| 8a13074c21 | |||
| 1b7558a4f1 | |||
| 7d91e318d1 | |||
| 4e2cd36677 | |||
| f2adacb687 | |||
| 553132f486 | |||
| 1f5f7c8be3 | |||
| 70a4dfd698 | |||
| 81bf90f5c4 | |||
| 8424611b34 | |||
| de2c04cdaf | |||
| 738397c637 | |||
| 1f70860473 | |||
| a8865bb4d8 | |||
| dd2e790ca6 | |||
| c5bdbdd692 | |||
| ffa5bf8f2c | |||
| 9d8973e971 | |||
| cb72197f8e | |||
| 374da9bc22 | |||
| 38d0bdb5d9 | |||
| decb12641d | |||
| 4e485f924a | |||
| 7e770197e6 | |||
| 6f552e60c0 | |||
| 0f2a1f7b8a | |||
| 0c2c7ba685 | |||
| 50174a7f4a | |||
| cc82e79d50 | |||
| 578bbcdbe8 | |||
| 56c6483fc0 | |||
| fca0930b01 | |||
| 2f08ea4131 | |||
| 5ac825ed7a | |||
| 4856a30bb6 | |||
| f15f0f6eae | |||
| da19dd9488 | |||
| e57c6aabe5 | |||
| f6f9554b25 | |||
| e9bace4447 | |||
| 40b9d03ea8 | |||
| 3247b54c86 | |||
| d78de29daf | |||
| 0758c14909 | |||
| 03c6a7692c | |||
| 02a3606185 | |||
| 7e45a7b2f5 |
@@ -14,10 +14,10 @@ jobs:
|
|||||||
name: Test iOS
|
name: Test iOS
|
||||||
runs-on: macOS-latest
|
runs-on: macOS-latest
|
||||||
env:
|
env:
|
||||||
DEVELOPER_DIR: /Applications/Xcode_12.app/Contents/Developer
|
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
destination: ["OS=14.0,name=iPhone 11 Pro"] #, "OS=12.4,name=iPhone XS", "OS=11.4,name=iPhone X", "OS=10.3.1,name=iPhone SE"]
|
destination: ["OS=latest,name=iPhone 15 Pro"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: iOS - ${{ matrix.destination }}
|
- name: iOS - ${{ matrix.destination }}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ xcuserdata/
|
|||||||
*.xccheckout
|
*.xccheckout
|
||||||
|
|
||||||
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
|
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
|
||||||
|
**/.DS_Store
|
||||||
build/
|
build/
|
||||||
DerivedData/
|
DerivedData/
|
||||||
*.moved-aside
|
*.moved-aside
|
||||||
@@ -88,3 +89,5 @@ fastlane/test_output
|
|||||||
# https://github.com/johnno1962/injectionforxcode
|
# https://github.com/johnno1962/injectionforxcode
|
||||||
|
|
||||||
iOSInjectionProject/
|
iOSInjectionProject/
|
||||||
|
/.DS_Store
|
||||||
|
/AudioExample/AudioExample/.DS_Store
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>classNames</key>
|
||||||
|
<dict>
|
||||||
|
<key>AtomicTests</key>
|
||||||
|
<dict>
|
||||||
|
<key>testProtectedValuesAreAccessedSafely()</key>
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.XCTPerformanceMetric_WallClockTime</key>
|
||||||
|
<dict>
|
||||||
|
<key>baselineAverage</key>
|
||||||
|
<real>0.029769</real>
|
||||||
|
<key>baselineIntegrationDisplayName</key>
|
||||||
|
<string>Local Baseline</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>runDestinationsByUUID</key>
|
||||||
|
<dict>
|
||||||
|
<key>E340D9FA-D19A-49BB-82AA-9D0E236D4288</key>
|
||||||
|
<dict>
|
||||||
|
<key>localComputer</key>
|
||||||
|
<dict>
|
||||||
|
<key>busSpeedInMHz</key>
|
||||||
|
<integer>0</integer>
|
||||||
|
<key>cpuCount</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
<key>cpuKind</key>
|
||||||
|
<string>Apple M1 Pro</string>
|
||||||
|
<key>cpuSpeedInMHz</key>
|
||||||
|
<integer>0</integer>
|
||||||
|
<key>logicalCPUCoresPerPackage</key>
|
||||||
|
<integer>10</integer>
|
||||||
|
<key>modelCode</key>
|
||||||
|
<string>MacBookPro18,1</string>
|
||||||
|
<key>physicalCPUCoresPerPackage</key>
|
||||||
|
<integer>10</integer>
|
||||||
|
<key>platformIdentifier</key>
|
||||||
|
<string>com.apple.platform.macosx</string>
|
||||||
|
</dict>
|
||||||
|
<key>targetArchitecture</key>
|
||||||
|
<string>arm64</string>
|
||||||
|
<key>targetDevice</key>
|
||||||
|
<dict>
|
||||||
|
<key>modelCode</key>
|
||||||
|
<string>iPhone16,1</string>
|
||||||
|
<key>platformIdentifier</key>
|
||||||
|
<string>com.apple.platform.iphonesimulator</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -1,432 +0,0 @@
|
|||||||
// !$*UTF8*$!
|
|
||||||
{
|
|
||||||
archiveVersion = 1;
|
|
||||||
classes = {
|
|
||||||
};
|
|
||||||
objectVersion = 50;
|
|
||||||
objects = {
|
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
|
||||||
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 */
|
|
||||||
B5220835256051830086FB3A /* AudioPlayerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerService.swift; sourceTree = "<group>"; };
|
|
||||||
B5220947256074910086FB3A /* MulticastDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MulticastDelegate.swift; sourceTree = "<group>"; };
|
|
||||||
B522094F2561883E0086FB3A /* EqualizerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualizerViewController.swift; sourceTree = "<group>"; };
|
|
||||||
B5220953256188590086FB3A /* EqualizerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualizerViewModel.swift; sourceTree = "<group>"; };
|
|
||||||
B524D59B2560176C00F5A88F /* bensound-jazzyfrenchy.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "bensound-jazzyfrenchy.mp3"; sourceTree = "<group>"; };
|
|
||||||
B524D5A02560302100F5A88F /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = "<group>"; };
|
|
||||||
B524D5A22560303000F5A88F /* PlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewModel.swift; sourceTree = "<group>"; };
|
|
||||||
B524D5A42560303D00F5A88F /* PlayerControlsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsViewController.swift; sourceTree = "<group>"; };
|
|
||||||
B524D5A62560305800F5A88F /* PlayerControlsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsViewModel.swift; sourceTree = "<group>"; };
|
|
||||||
B524D5A8256031DE00F5A88F /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; };
|
|
||||||
B524D5AC25604E4B00F5A88F /* PlaylistItemsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistItemsService.swift; sourceTree = "<group>"; };
|
|
||||||
B524D5AE25604ED900F5A88F /* AudioContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContent.swift; sourceTree = "<group>"; };
|
|
||||||
B580CB0D2561B912006D7DD8 /* EqualizerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualizerService.swift; sourceTree = "<group>"; };
|
|
||||||
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 = "<group>"; };
|
|
||||||
B5AEDBDD2475274D007D8101 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
|
||||||
B5AEDBE02475274D007D8101 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
|
||||||
B5AEDBE22475274D007D8101 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
|
||||||
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 = (
|
|
||||||
B524D59B2560176C00F5A88F /* bensound-jazzyfrenchy.mp3 */,
|
|
||||||
B5AEDBDD2475274D007D8101 /* Assets.xcassets */,
|
|
||||||
B5AEDBDF2475274D007D8101 /* LaunchScreen.storyboard */,
|
|
||||||
);
|
|
||||||
path = Resources;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
B524D5AA25604E2E00F5A88F /* Services */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
B524D5AE25604ED900F5A88F /* AudioContent.swift */,
|
|
||||||
B524D5AC25604E4B00F5A88F /* PlaylistItemsService.swift */,
|
|
||||||
B5220835256051830086FB3A /* AudioPlayerService.swift */,
|
|
||||||
B5220947256074910086FB3A /* MulticastDelegate.swift */,
|
|
||||||
B580CB0D2561B912006D7DD8 /* EqualizerService.swift */,
|
|
||||||
);
|
|
||||||
path = Services;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
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 = "<group>";
|
|
||||||
};
|
|
||||||
B5AEDBC82475274C007D8101 = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
B5AEDBD32475274C007D8101 /* AudioExample */,
|
|
||||||
B5AEDBD22475274C007D8101 /* Products */,
|
|
||||||
B5F883C424780A3C00D277C1 /* Frameworks */,
|
|
||||||
);
|
|
||||||
sourceTree = "<group>";
|
|
||||||
wrapsLines = 0;
|
|
||||||
};
|
|
||||||
B5AEDBD22475274C007D8101 /* Products */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
B5AEDBD12475274C007D8101 /* AudioExample.app */,
|
|
||||||
);
|
|
||||||
name = Products;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
B5AEDBD32475274C007D8101 /* AudioExample */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
B5AEDBD42475274C007D8101 /* AppDelegate.swift */,
|
|
||||||
B524D5A8256031DE00F5A88F /* AppCoordinator.swift */,
|
|
||||||
B524D5AA25604E2E00F5A88F /* Services */,
|
|
||||||
B524D5AB25604E3500F5A88F /* Controllers */,
|
|
||||||
B524D59D2560177C00F5A88F /* Resources */,
|
|
||||||
B5AEDBE22475274D007D8101 /* Info.plist */,
|
|
||||||
);
|
|
||||||
path = AudioExample;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
B5F883C424780A3C00D277C1 /* Frameworks */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
B5F883C524780A3C00D277C1 /* AudioStreaming.framework */,
|
|
||||||
);
|
|
||||||
name = Frameworks;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* 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 */,
|
|
||||||
);
|
|
||||||
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 = "<group>";
|
|
||||||
};
|
|
||||||
/* 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 = Automatic;
|
|
||||||
DEVELOPMENT_TEAM = 5Y92JCRVR7;
|
|
||||||
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)";
|
|
||||||
SWIFT_VERSION = 5.0;
|
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
B5AEDBE72475274D007D8101 /* Release */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
buildSettings = {
|
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
||||||
CODE_SIGN_STYLE = Automatic;
|
|
||||||
DEVELOPMENT_TEAM = 5Y92JCRVR7;
|
|
||||||
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)";
|
|
||||||
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 */;
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Scheme
|
|
||||||
LastUpgradeVersion = "1200"
|
|
||||||
version = "1.7">
|
|
||||||
<BuildAction
|
|
||||||
parallelizeBuildables = "YES"
|
|
||||||
buildImplicitDependencies = "YES">
|
|
||||||
<BuildActionEntries>
|
|
||||||
<BuildActionEntry
|
|
||||||
buildForTesting = "YES"
|
|
||||||
buildForRunning = "YES"
|
|
||||||
buildForProfiling = "YES"
|
|
||||||
buildForArchiving = "YES"
|
|
||||||
buildForAnalyzing = "YES">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "B5AEDBD02475274C007D8101"
|
|
||||||
BuildableName = "AudioExample.app"
|
|
||||||
BlueprintName = "AudioExample"
|
|
||||||
ReferencedContainer = "container:AudioExample.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildActionEntry>
|
|
||||||
</BuildActionEntries>
|
|
||||||
</BuildAction>
|
|
||||||
<TestAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
|
||||||
enableThreadSanitizer = "YES"
|
|
||||||
codeCoverageEnabled = "YES">
|
|
||||||
<TestPlans>
|
|
||||||
<TestPlanReference
|
|
||||||
reference = "container:../AudioStreamingTests/AudioExample.xctestplan"
|
|
||||||
default = "YES">
|
|
||||||
</TestPlanReference>
|
|
||||||
</TestPlans>
|
|
||||||
<Testables>
|
|
||||||
<TestableReference
|
|
||||||
skipped = "NO"
|
|
||||||
parallelizable = "YES"
|
|
||||||
testExecutionOrdering = "random">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "B5AEDBB624744153007D8101"
|
|
||||||
BuildableName = "AudioStreamingTests.xctest"
|
|
||||||
BlueprintName = "AudioStreamingTests"
|
|
||||||
ReferencedContainer = "container:../AudioStreaming.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
<SkippedTests>
|
|
||||||
<Test
|
|
||||||
Identifier = "ProtectedTests">
|
|
||||||
</Test>
|
|
||||||
</SkippedTests>
|
|
||||||
</TestableReference>
|
|
||||||
</Testables>
|
|
||||||
</TestAction>
|
|
||||||
<LaunchAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
||||||
enableASanStackUseAfterReturn = "YES"
|
|
||||||
disableMainThreadChecker = "YES"
|
|
||||||
launchStyle = "0"
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
|
||||||
debugDocumentVersioning = "YES"
|
|
||||||
debugServiceExtension = "internal"
|
|
||||||
enableGPUValidationMode = "1"
|
|
||||||
allowLocationSimulation = "YES">
|
|
||||||
<BuildableProductRunnable
|
|
||||||
runnableDebuggingMode = "0">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "B5AEDBD02475274C007D8101"
|
|
||||||
BuildableName = "AudioExample.app"
|
|
||||||
BlueprintName = "AudioExample"
|
|
||||||
ReferencedContainer = "container:AudioExample.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildableProductRunnable>
|
|
||||||
<EnvironmentVariables>
|
|
||||||
<EnvironmentVariable
|
|
||||||
key = "OS_ACTIVITY_MODE"
|
|
||||||
value = "disable"
|
|
||||||
isEnabled = "NO">
|
|
||||||
</EnvironmentVariable>
|
|
||||||
</EnvironmentVariables>
|
|
||||||
<AdditionalOptions>
|
|
||||||
<AdditionalOption
|
|
||||||
key = "MallocStackLogging"
|
|
||||||
value = ""
|
|
||||||
isEnabled = "YES">
|
|
||||||
</AdditionalOption>
|
|
||||||
<AdditionalOption
|
|
||||||
key = "PrefersMallocStackLoggingLite"
|
|
||||||
value = ""
|
|
||||||
isEnabled = "YES">
|
|
||||||
</AdditionalOption>
|
|
||||||
</AdditionalOptions>
|
|
||||||
</LaunchAction>
|
|
||||||
<ProfileAction
|
|
||||||
buildConfiguration = "Release"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
|
||||||
savedToolIdentifier = ""
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
debugDocumentVersioning = "YES">
|
|
||||||
<BuildableProductRunnable
|
|
||||||
runnableDebuggingMode = "0">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "B5AEDBD02475274C007D8101"
|
|
||||||
BuildableName = "AudioExample.app"
|
|
||||||
BlueprintName = "AudioExample"
|
|
||||||
ReferencedContainer = "container:AudioExample.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildableProductRunnable>
|
|
||||||
</ProfileAction>
|
|
||||||
<AnalyzeAction
|
|
||||||
buildConfiguration = "Debug">
|
|
||||||
</AnalyzeAction>
|
|
||||||
<ArchiveAction
|
|
||||||
buildConfiguration = "Release"
|
|
||||||
revealArchiveInOrganizer = "YES">
|
|
||||||
</ArchiveAction>
|
|
||||||
</Scheme>
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>SchemeUserState</key>
|
|
||||||
<dict>
|
|
||||||
<key>AudioExample.xcscheme_^#shared#^_</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>0</integer>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<key>SuppressBuildableAutocreation</key>
|
|
||||||
<dict>
|
|
||||||
<key>B5AEDBD02475274C007D8101</key>
|
|
||||||
<dict>
|
|
||||||
<key>primary</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
//
|
|
||||||
// AppCoordinator.swift
|
|
||||||
// AudioExample
|
|
||||||
//
|
|
||||||
// Created by Dimitrios Chatzieleftheriou on 14/11/2020.
|
|
||||||
// Copyright © 2020 Dimitrios Chatzieleftheriou. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
//
|
|
||||||
// EqualzerViewModel.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 >= 1_000 {
|
|
||||||
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.deactive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 errorOccured(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(""))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,162 +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] action 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)
|
|
||||||
self.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
|
|
||||||
cell.detailTextLabel?.text = item.queues ? "Queue item" : nil
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +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, 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)))
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func errorOccured(error _: AudioPlayerError) {
|
|
||||||
currentPlayingItemIndex = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func metadataReceived(metadata _: [String: String]) {}
|
|
||||||
func didStopPlaying() {}
|
|
||||||
|
|
||||||
func didStartPlaying() {}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>$(PRODUCT_NAME)</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>1.0</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>1</string>
|
|
||||||
<key>LSRequiresIPhoneOS</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSAppTransportSecurity</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
<key>UIBackgroundModes</key>
|
|
||||||
<array>
|
|
||||||
<string>audio</string>
|
|
||||||
</array>
|
|
||||||
<key>UILaunchStoryboardName</key>
|
|
||||||
<string>LaunchScreen</string>
|
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
|
||||||
<array>
|
|
||||||
<string>armv7</string>
|
|
||||||
</array>
|
|
||||||
<key>UIRequiresFullScreen</key>
|
|
||||||
<true/>
|
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
</array>
|
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
|
||||||
<dependencies>
|
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
|
||||||
</dependencies>
|
|
||||||
<scenes>
|
|
||||||
<!--View Controller-->
|
|
||||||
<scene sceneID="EHf-IW-A2E">
|
|
||||||
<objects>
|
|
||||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
|
||||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
|
||||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
|
||||||
</view>
|
|
||||||
</viewController>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
|
||||||
</objects>
|
|
||||||
<point key="canvasLocation" x="53" y="375"/>
|
|
||||||
</scene>
|
|
||||||
</scenes>
|
|
||||||
</document>
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
//
|
|
||||||
// AudioContent.swift
|
|
||||||
// AudioExample
|
|
||||||
//
|
|
||||||
// Created by Dimitrios Chatzieleftheriou on 14/11/2020.
|
|
||||||
// Copyright © 2020 Dimitrios Chatzieleftheriou. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
enum AudioContent: Int, CaseIterable {
|
|
||||||
case offradio
|
|
||||||
case enlefko
|
|
||||||
case pepper966
|
|
||||||
case kosmos
|
|
||||||
case radiox
|
|
||||||
case khruangbin
|
|
||||||
case piano
|
|
||||||
case local
|
|
||||||
case podcast
|
|
||||||
|
|
||||||
var title: String {
|
|
||||||
switch self {
|
|
||||||
case .offradio:
|
|
||||||
return "Offradio (stream)"
|
|
||||||
case .enlefko:
|
|
||||||
return "Enlefko (stream)"
|
|
||||||
case .pepper966:
|
|
||||||
return "Pepper 96.6 (stream)"
|
|
||||||
case .kosmos:
|
|
||||||
return "Kosmos 93.6 (stream)"
|
|
||||||
case .radiox:
|
|
||||||
return "Radio X (stream)"
|
|
||||||
case .khruangbin:
|
|
||||||
return "Khruangbin (mp3 preview)"
|
|
||||||
case .piano:
|
|
||||||
return "Piano (mp3)"
|
|
||||||
case .local:
|
|
||||||
return "Local file (mp3)"
|
|
||||||
case .podcast:
|
|
||||||
return "Swift by Sundell. Ep. 50 (mp3)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var streamUrl: URL {
|
|
||||||
switch self {
|
|
||||||
case .enlefko:
|
|
||||||
return URL(string: "https://stream.radiojar.com/srzwv225e3quv")!
|
|
||||||
case .offradio:
|
|
||||||
return URL(string: "https://s3.yesstreaming.net:17062/stream")!
|
|
||||||
case .pepper966:
|
|
||||||
return URL(string: "https://ample-09.radiojar.com/pepper.m4a?1593699983=&rj-tok=AAABcw_1KyMAIViq2XpI098ZSQ&rj-ttl=5")!
|
|
||||||
case .kosmos:
|
|
||||||
return URL(string: "https://radiostreaming.ert.gr/ert-kosmos")!
|
|
||||||
case .radiox:
|
|
||||||
return URL(string: "https://media-ssl.musicradio.com/RadioXLondon")!
|
|
||||||
case .khruangbin:
|
|
||||||
return URL(string: "https://p.scdn.co/mp3-preview/cab4b09c23ffc11774d879977131df9d150fcef4?cid=d8a5ed958d274c2e8ee717e6a4b0971d")!
|
|
||||||
case .piano:
|
|
||||||
return URL(string: "https://www.kozco.com/tech/piano2-CoolEdit.mp3")!
|
|
||||||
case .local:
|
|
||||||
let path = Bundle.main.path(forResource: "bensound-jazzyfrenchy", ofType: "mp3")!
|
|
||||||
return URL(fileURLWithPath: path)
|
|
||||||
case .podcast:
|
|
||||||
return URL(string: "https://hwcdn.libsyn.com/p/f/6/e/f6e7cb785cf0f71f/SwiftBySundell50.mp3?c_id=45232967&cs_id=45232967&expiration=1605613140&hwt=f9ff0b2f758c3286cd75322e14ef7a23")!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<Delegate> {
|
|
||||||
private let delegates = NSHashTable<AnyObject>.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
let url: URL
|
|
||||||
let name: String
|
|
||||||
let status: Status
|
|
||||||
let queues: Bool
|
|
||||||
|
|
||||||
init(content: AudioContent, queues: Bool) {
|
|
||||||
name = content.title
|
|
||||||
url = content.streamUrl
|
|
||||||
status = .stopped
|
|
||||||
self.queues = queues
|
|
||||||
}
|
|
||||||
|
|
||||||
init(url: URL, name: String, status: Status, queues: Bool) {
|
|
||||||
self.url = url
|
|
||||||
self.name = name
|
|
||||||
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, status: status, queues: item.queues)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func provideInitialPlaylistItems() -> [PlaylistItem] {
|
|
||||||
let allCases = AudioContent.allCases
|
|
||||||
let casesForQueueing: [AudioContent] = [.piano, .local, .khruangbin]
|
|
||||||
let allItems = allCases.map { PlaylistItem.init(content: $0 , queues: false) }
|
|
||||||
let casesForQueuingItems = casesForQueueing.map { PlaylistItem.init(content: $0 , queues: true) }
|
|
||||||
return allItems + casesForQueuingItems
|
|
||||||
}
|
|
||||||
@@ -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 = "<group>"; };
|
||||||
|
9806E8192BC5D12500757370 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
|
9806E81B2BC5D12700757370 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
9806E81E2BC5D12700757370 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||||
|
9806E8252BC5D2A900757370 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = "<group>"; };
|
||||||
|
9806E8292BC68F8700757370 /* AudioPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerView.swift; sourceTree = "<group>"; };
|
||||||
|
9806E8302BC6927D00757370 /* AudioPlayerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerModel.swift; sourceTree = "<group>"; };
|
||||||
|
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 = "<group>"; };
|
||||||
|
9816A8AB2BC820DF00AD1299 /* AudioContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContent.swift; sourceTree = "<group>"; };
|
||||||
|
9816A8AD2BC832DB00AD1299 /* bensound-jazzyfrenchy.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "bensound-jazzyfrenchy.mp3"; sourceTree = "<group>"; };
|
||||||
|
9816A8AE2BC832DB00AD1299 /* bensound-jazzyfrenchy.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = "bensound-jazzyfrenchy.m4a"; sourceTree = "<group>"; };
|
||||||
|
9816A8AF2BC832DC00AD1299 /* hipjazz.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = hipjazz.wav; sourceTree = "<group>"; };
|
||||||
|
9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerService.swift; sourceTree = "<group>"; };
|
||||||
|
984DE9542BDAE59C004B427A /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
|
||||||
|
984DE9562BDAFC7E004B427A /* AudioPlayerControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerControlsView.swift; sourceTree = "<group>"; };
|
||||||
|
98BFB4192BC97AF800E812C0 /* DisplayLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayLink.swift; sourceTree = "<group>"; };
|
||||||
|
98BFB41B2BCAAD8A00E812C0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualizerView.swift; sourceTree = "<group>"; };
|
||||||
|
98BFB41E2BCD814000E812C0 /* EqualizerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualizerService.swift; sourceTree = "<group>"; };
|
||||||
|
98BFB4222BCE78AB00E812C0 /* AddNewAudioURLView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddNewAudioURLView.swift; sourceTree = "<group>"; };
|
||||||
|
98E6119B2BC72C0E0036BC47 /* DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailView.swift; sourceTree = "<group>"; };
|
||||||
|
/* 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 = "<group>";
|
||||||
|
};
|
||||||
|
9806E8152BC5D12500757370 /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9806E8142BC5D12500757370 /* AudioPlayer.app */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
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 = "<group>";
|
||||||
|
};
|
||||||
|
9806E81D2BC5D12700757370 /* Preview Content */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9806E81E2BC5D12700757370 /* Preview Assets.xcassets */,
|
||||||
|
);
|
||||||
|
path = "Preview Content";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9806E8272BC68F6600757370 /* Navigation */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9806E8192BC5D12500757370 /* ContentView.swift */,
|
||||||
|
98E6119B2BC72C0E0036BC47 /* DetailView.swift */,
|
||||||
|
9806E8252BC5D2A900757370 /* Sidebar.swift */,
|
||||||
|
);
|
||||||
|
path = Navigation;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9806E8282BC68F7300757370 /* Content */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
98E3921C2BD845E100B586E9 /* AudioPlayer */,
|
||||||
|
);
|
||||||
|
path = Content;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9816A8A32BC7D8A200AD1299 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9816A8A42BC7D8A200AD1299 /* AudioStreaming.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9816A8A82BC7F4DE00AD1299 /* Common */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
98BFB4222BCE78AB00E812C0 /* AddNewAudioURLView.swift */,
|
||||||
|
9816A8A92BC7F4F000AD1299 /* AudioTrack.swift */,
|
||||||
|
9816A8AB2BC820DF00AD1299 /* AudioContent.swift */,
|
||||||
|
);
|
||||||
|
path = Common;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9816A8B02BC832E100AD1299 /* Resources */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9816A8AE2BC832DB00AD1299 /* bensound-jazzyfrenchy.m4a */,
|
||||||
|
9816A8AF2BC832DC00AD1299 /* hipjazz.wav */,
|
||||||
|
9816A8AD2BC832DB00AD1299 /* bensound-jazzyfrenchy.mp3 */,
|
||||||
|
);
|
||||||
|
path = Resources;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
984DE9522BDAE571004B427A /* Helpers */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
98BFB4192BC97AF800E812C0 /* DisplayLink.swift */,
|
||||||
|
984DE9542BDAE59C004B427A /* Notifier.swift */,
|
||||||
|
);
|
||||||
|
path = Helpers;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
984DE9532BDAE57F004B427A /* Dependencies */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
98BFB41E2BCD814000E812C0 /* EqualizerService.swift */,
|
||||||
|
9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */,
|
||||||
|
);
|
||||||
|
path = Dependencies;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
98E3921C2BD845E100B586E9 /* AudioPlayer */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9806E8302BC6927D00757370 /* AudioPlayerModel.swift */,
|
||||||
|
9806E8292BC68F8700757370 /* AudioPlayerView.swift */,
|
||||||
|
98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */,
|
||||||
|
984DE9562BDAFC7E004B427A /* AudioPlayerControlsView.swift */,
|
||||||
|
);
|
||||||
|
path = AudioPlayer;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* 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 */;
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1530"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "9806E8132BC5D12500757370"
|
||||||
|
BuildableName = "AudioPlayer.app"
|
||||||
|
BlueprintName = "AudioPlayer"
|
||||||
|
ReferencedContainer = "container:AudioPlayer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "9806E8132BC5D12500757370"
|
||||||
|
BuildableName = "AudioPlayer.app"
|
||||||
|
BlueprintName = "AudioPlayer"
|
||||||
|
ReferencedContainer = "container:AudioPlayer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "9806E8132BC5D12500757370"
|
||||||
|
BuildableName = "AudioPlayer.app"
|
||||||
|
BlueprintName = "AudioPlayer"
|
||||||
|
ReferencedContainer = "container:AudioPlayer.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 })
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
//
|
||||||
|
// Created by Dimitris C.
|
||||||
|
// Copyright © 2024 Decimal. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum AudioContent {
|
||||||
|
case offradio
|
||||||
|
case enlefko
|
||||||
|
case pepper966
|
||||||
|
case kosmos
|
||||||
|
case kosmosJazz
|
||||||
|
case radiox
|
||||||
|
case khruangbin
|
||||||
|
case piano
|
||||||
|
case optimized
|
||||||
|
case nonOptimized
|
||||||
|
case remoteWave
|
||||||
|
case local
|
||||||
|
case localWave
|
||||||
|
case custom(String)
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .offradio:
|
||||||
|
return "Offradio"
|
||||||
|
case .enlefko:
|
||||||
|
return "Enlefko"
|
||||||
|
case .pepper966:
|
||||||
|
return "Pepper 96.6"
|
||||||
|
case .kosmos:
|
||||||
|
return "Kosmos 93.6"
|
||||||
|
case .kosmosJazz:
|
||||||
|
return "Kosmos Jazz"
|
||||||
|
case .radiox:
|
||||||
|
return "Radio X"
|
||||||
|
case .khruangbin:
|
||||||
|
return "Khruangbin"
|
||||||
|
case .piano:
|
||||||
|
return "Piano"
|
||||||
|
case .remoteWave:
|
||||||
|
return "Sample remote"
|
||||||
|
case .local:
|
||||||
|
return "Jazzy Frenchy"
|
||||||
|
case .localWave:
|
||||||
|
return "Local file"
|
||||||
|
case .optimized:
|
||||||
|
return "Jazzy Frenchy"
|
||||||
|
case .nonOptimized:
|
||||||
|
return "Jazzy Frenchy"
|
||||||
|
case .custom(let url):
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var subtitle: String? {
|
||||||
|
switch self {
|
||||||
|
case .offradio:
|
||||||
|
return "Stream • offradio.gr"
|
||||||
|
case .enlefko:
|
||||||
|
return "Stream • enlefko.fm"
|
||||||
|
case .pepper966:
|
||||||
|
return "Stream • pepper966.gr"
|
||||||
|
case .kosmos:
|
||||||
|
return "Stream • ertecho.gr"
|
||||||
|
case .kosmosJazz:
|
||||||
|
return "Stream • ertecho.gr"
|
||||||
|
case .radiox:
|
||||||
|
return "Stream • globalplayer.com"
|
||||||
|
case .khruangbin:
|
||||||
|
return "Remote mp3"
|
||||||
|
case .piano:
|
||||||
|
return "Remote mp3"
|
||||||
|
case .remoteWave:
|
||||||
|
return "wave"
|
||||||
|
case .local:
|
||||||
|
return "Music by: bensound.com"
|
||||||
|
case .localWave:
|
||||||
|
return "Music by: bensound.com"
|
||||||
|
case .optimized:
|
||||||
|
return "Music by: bensound.com - m4a optimized"
|
||||||
|
case .nonOptimized:
|
||||||
|
return "Music by: bensound.com - m4a non-optimized"
|
||||||
|
case .custom:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var streamUrl: URL {
|
||||||
|
switch self {
|
||||||
|
case .enlefko:
|
||||||
|
return URL(string: "https://stream.radiojar.com/srzwv225e3quv")!
|
||||||
|
case .offradio:
|
||||||
|
return URL(string: "https://s3.yesstreaming.net:17062/stream")!
|
||||||
|
case .pepper966:
|
||||||
|
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:
|
||||||
|
return URL(string: "https://p.scdn.co/mp3-preview/cab4b09c23ffc11774d879977131df9d150fcef4?cid=d8a5ed958d274c2e8ee717e6a4b0971d")!
|
||||||
|
case .piano:
|
||||||
|
return URL(string: "https://www.kozco.com/tech/piano2-CoolEdit.mp3")!
|
||||||
|
case .optimized:
|
||||||
|
return URL(string: "https://github.com/dimitris-c/sample-audio/raw/main/bensound-jazzyfrenchy-optimized.m4a")!
|
||||||
|
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: "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://github.com/dimitris-c/sample-audio/raw/main/5-MB-WAV.wav")!
|
||||||
|
case .custom(let url):
|
||||||
|
return URL(string: url)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<AudioTrack?>) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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) })
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -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..<dragPointYLocations.count {
|
||||||
|
let x = positionForDragPoint(at: index, size: innerGeo.size)
|
||||||
|
path.move(to: CGPoint(x: x, y: 0))
|
||||||
|
path.addLine(to: CGPoint(x: x, y: innerGeo.size.height))
|
||||||
|
}
|
||||||
|
path.move(to: CGPoint(x: 0, y: innerGeo.size.height / 2))
|
||||||
|
path.addLine(to: CGPoint(x: innerGeo.size.width, y: innerGeo.size.height / 2))
|
||||||
|
}
|
||||||
|
.stroke(Color.gray.opacity(0.5), lineWidth: 1)
|
||||||
|
|
||||||
|
ForEach(eqModel.bands) { band in
|
||||||
|
Circle()
|
||||||
|
.fill(Color.mint)
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
.position(x: positionForDragPoint(at: band.index, size: innerGeo.size), y: dragPointYLocations[band.index])
|
||||||
|
.gesture(
|
||||||
|
DragGesture()
|
||||||
|
.onChanged { value in
|
||||||
|
let newY = min(max(value.location.y, 0), innerGeo.size.height)
|
||||||
|
dragPointYLocations[band.index] = newY
|
||||||
|
updateGainValue(at: band.index, in: innerGeo.size)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.onAppear {
|
||||||
|
dragPointYLocations[band.index] = gainToYPosition(at: band.value, in: innerGeo.size)
|
||||||
|
}
|
||||||
|
.onChange(of: eqModel.shouldReset) { _, reset in
|
||||||
|
if reset {
|
||||||
|
resetPositions(in: innerGeo.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach(eqModel.bands) { band in
|
||||||
|
Text(band.frequency)
|
||||||
|
.position(x: positionForDragPoint(at: band.index, size: innerGeo.size), y: innerGeo.size.height + 8)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.black)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func positionForDragPoint(at index: Int, size: CGSize) -> 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..<values.count {
|
||||||
|
let x = positionForDragPoint(at: index, size: rect.size)
|
||||||
|
let y = values[index]
|
||||||
|
path.addLine(to: CGPoint(x: x, y: y))
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func positionForDragPoint(at index: Int, size: CGSize) -> 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())
|
||||||
|
}
|
||||||
+35
-24
@@ -1,24 +1,21 @@
|
|||||||
//
|
//
|
||||||
// AudioPlayerService.swift
|
// Created by Dimitris C.
|
||||||
// AudioExample
|
// Copyright © 2024 Decimal. All rights reserved.
|
||||||
//
|
|
||||||
// Created by Dimitrios Chatzieleftheriou on 14/11/2020.
|
|
||||||
// Copyright © 2020 Dimitrios Chatzieleftheriou. All rights reserved.
|
|
||||||
//
|
//
|
||||||
|
|
||||||
import AudioStreaming
|
import AudioStreaming
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
|
||||||
protocol AudioPlayerServiceDelegate: AnyObject {
|
protocol AudioPlayerServiceDelegate: AnyObject {
|
||||||
func didStartPlaying()
|
func didStartPlaying(id: AudioEntryId)
|
||||||
func didStopPlaying()
|
func didStopPlaying(id: AudioEntryId, reason: AudioPlayerStopReason)
|
||||||
func statusChanged(status: AudioPlayerState)
|
func statusChanged(status: AudioPlayerState)
|
||||||
func errorOccured(error: AudioPlayerError)
|
func errorOccurred(error: AudioPlayerError)
|
||||||
func metadataReceived(metadata: [String: String])
|
func metadataReceived(metadata: [String: String])
|
||||||
}
|
}
|
||||||
|
|
||||||
final class AudioPlayerService {
|
final class AudioPlayerService {
|
||||||
var delegate = MulticastDelegate<AudioPlayerServiceDelegate>()
|
weak var delegate: AudioPlayerServiceDelegate?
|
||||||
|
|
||||||
private var player: AudioPlayer
|
private var player: AudioPlayer
|
||||||
private var audioSystemResetObserver: Any?
|
private var audioSystemResetObserver: Any?
|
||||||
@@ -43,8 +40,12 @@ final class AudioPlayerService {
|
|||||||
player.state
|
player.state
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
var statusChangedNotifier = Notifier<AudioPlayerState>()
|
||||||
player = AudioPlayer(configuration: .init(enableLogs: true))
|
var metadataReceivedNotifier = Notifier<[String: String]>()
|
||||||
|
var playingStartedStopped = Notifier<(started: Bool, AudioEntryId, AudioPlayerStopReason?)>()
|
||||||
|
|
||||||
|
init(audioPlayer: AudioPlayer) {
|
||||||
|
player = audioPlayer
|
||||||
player.delegate = self
|
player.delegate = self
|
||||||
|
|
||||||
configureAudioSession()
|
configureAudioSession()
|
||||||
@@ -98,8 +99,8 @@ final class AudioPlayerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func seek(at time: Float) {
|
func seek(at time: Double) {
|
||||||
player.seek(to: Double(time))
|
player.seek(to: time)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func recreatePlayer() {
|
private func recreatePlayer() {
|
||||||
@@ -109,9 +110,11 @@ final class AudioPlayerService {
|
|||||||
|
|
||||||
private func registerSessionEvents() {
|
private func registerSessionEvents() {
|
||||||
// Note that a real app might need to observer other AVAudioSession notifications as well
|
// Note that a real app might need to observer other AVAudioSession notifications as well
|
||||||
audioSystemResetObserver = NotificationCenter.default.addObserver(forName: AVAudioSession.mediaServicesWereResetNotification,
|
audioSystemResetObserver = NotificationCenter.default.addObserver(
|
||||||
object: nil,
|
forName: AVAudioSession.mediaServicesWereResetNotification,
|
||||||
queue: nil) { [unowned self] _ in
|
object: nil,
|
||||||
|
queue: nil
|
||||||
|
) { [unowned self] _ in
|
||||||
self.configureAudioSession()
|
self.configureAudioSession()
|
||||||
self.recreatePlayer()
|
self.recreatePlayer()
|
||||||
}
|
}
|
||||||
@@ -148,32 +151,40 @@ final class AudioPlayerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension AudioPlayerService: AudioPlayerDelegate {
|
extension AudioPlayerService: AudioPlayerDelegate {
|
||||||
func audioPlayerDidStartPlaying(player _: AudioPlayer, with _: AudioEntryId) {
|
func audioPlayerDidStartPlaying(player _: AudioPlayer, with id: AudioEntryId) {
|
||||||
delegate.invoke(invocation: { $0.didStartPlaying() })
|
print("audioPlayerDidStartPlaying entryId: \(id)")
|
||||||
|
delegate?.didStartPlaying(id: id)
|
||||||
|
Task { await playingStartedStopped.send((true, id, nil)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func audioPlayerDidFinishBuffering(player _: AudioPlayer, with _: AudioEntryId) {}
|
func audioPlayerDidFinishBuffering(player _: AudioPlayer, with _: AudioEntryId) {}
|
||||||
|
|
||||||
func audioPlayerStateChanged(player _: AudioPlayer, with newState: AudioPlayerState, previous _: AudioPlayerState) {
|
func audioPlayerStateChanged(player _: AudioPlayer, with newState: AudioPlayerState, previous _: AudioPlayerState) {
|
||||||
delegate.invoke(invocation: { $0.statusChanged(status: newState) })
|
print("audioPlayerDidStartPlaying newState: \(newState)")
|
||||||
|
Task { await statusChangedNotifier.send(newState) }
|
||||||
|
delegate?.statusChanged(status: newState)
|
||||||
}
|
}
|
||||||
|
|
||||||
func audioPlayerDidFinishPlaying(player _: AudioPlayer,
|
func audioPlayerDidFinishPlaying(player _: AudioPlayer,
|
||||||
entryId _: AudioEntryId,
|
entryId id: AudioEntryId,
|
||||||
stopReason _: AudioPlayerStopReason,
|
stopReason reason: AudioPlayerStopReason,
|
||||||
progress _: Double,
|
progress _: Double,
|
||||||
duration _: Double)
|
duration _: Double)
|
||||||
{
|
{
|
||||||
delegate.invoke(invocation: { $0.didStopPlaying() })
|
print("audioPlayerDidFinishPlaying entryId: \(id), reason: \(reason)")
|
||||||
|
Task { await playingStartedStopped.send((false, id, reason)) }
|
||||||
|
delegate?.didStopPlaying(id: id, reason: reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
func audioPlayerUnexpectedError(player _: AudioPlayer, error: AudioPlayerError) {
|
func audioPlayerUnexpectedError(player _: AudioPlayer, error: AudioPlayerError) {
|
||||||
delegate.invoke(invocation: { $0.errorOccured(error: error) })
|
delegate?.errorOccurred(error: error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func audioPlayerDidCancel(player _: AudioPlayer, queuedItems _: [AudioEntryId]) {}
|
func audioPlayerDidCancel(player _: AudioPlayer, queuedItems _: [AudioEntryId]) {}
|
||||||
|
|
||||||
func audioPlayerDidReadMetadata(player _: AudioPlayer, metadata: [String: String]) {
|
func audioPlayerDidReadMetadata(player _: AudioPlayer, metadata: [String: String]) {
|
||||||
delegate.invoke(invocation: { $0.metadataReceived(metadata: metadata) })
|
Task { await metadataReceivedNotifier.send(metadata) }
|
||||||
|
delegate?.metadataReceived(metadata: metadata)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+5
-8
@@ -1,16 +1,13 @@
|
|||||||
//
|
//
|
||||||
// EqualizerService.swift
|
// Created by Dimitris C.
|
||||||
// AudioExample
|
// Copyright © 2024 Decimal. All rights reserved.
|
||||||
//
|
|
||||||
// Created by Dimitrios Chatzieleftheriou on 15/11/2020.
|
|
||||||
// Copyright © 2020 Dimitrios Chatzieleftheriou. All rights reserved.
|
|
||||||
//
|
//
|
||||||
|
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
|
||||||
final class EqualizerService {
|
final class EqualizerService {
|
||||||
private let playerService: AudioPlayerService
|
private let playerService: AudioPlayerService
|
||||||
private let _freqs = [32, 64, 128, 250, 500, 1_000, 2_000, 4_000, 8_000, 16_000]
|
private let _freqs = [60, 150, 400, 1000, 2400, 15000]
|
||||||
private let eqUnit: AVAudioUnitEQ
|
private let eqUnit: AVAudioUnitEQ
|
||||||
|
|
||||||
var bands: [AVAudioUnitEQFilterParameters] {
|
var bands: [AVAudioUnitEQFilterParameters] {
|
||||||
@@ -23,7 +20,7 @@ final class EqualizerService {
|
|||||||
self.playerService = playerService
|
self.playerService = playerService
|
||||||
|
|
||||||
eqUnit = AVAudioUnitEQ(numberOfBands: _freqs.count)
|
eqUnit = AVAudioUnitEQ(numberOfBands: _freqs.count)
|
||||||
for i in 0..<_freqs.count {
|
for i in 0 ..< _freqs.count {
|
||||||
eqUnit.bands[i].bypass = false
|
eqUnit.bands[i].bypass = false
|
||||||
eqUnit.bands[i].filterType = .parametric
|
eqUnit.bands[i].filterType = .parametric
|
||||||
eqUnit.bands[i].frequency = Float(_freqs[i])
|
eqUnit.bands[i].frequency = Float(_freqs[i])
|
||||||
@@ -45,7 +42,7 @@ final class EqualizerService {
|
|||||||
playerService.add(eqUnit)
|
playerService.add(eqUnit)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deactive() {
|
func deactivate() {
|
||||||
isActivated = false
|
isActivated = false
|
||||||
playerService.remove(eqUnit)
|
playerService.remove(eqUnit)
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// Created by Dimitris Chatzieleftheriou on 25/04/2024.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
actor Notifier<Output> {
|
||||||
|
private var continuations: [UUID: AsyncStream<Output>.Continuation] = [:]
|
||||||
|
|
||||||
|
func values(bufferingPolicy limit: AsyncStream<Output>.Continuation.BufferingPolicy = .bufferingNewest(1)) -> AsyncStream<Output> {
|
||||||
|
AsyncStream<Output>(bufferingPolicy: limit) { continuation in
|
||||||
|
let id = UUID()
|
||||||
|
continuations[id] = continuation
|
||||||
|
continuation.onTermination = { _ in
|
||||||
|
Task { await self.cancel(id) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func send(_ value: Output) {
|
||||||
|
for continuation in continuations.values {
|
||||||
|
continuation.yield(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cancel(_ id: UUID) {
|
||||||
|
continuations[id] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>audio</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// Created by Dimitris C.
|
||||||
|
// Copyright © 2024 Decimal. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ContentView: View {
|
||||||
|
|
||||||
|
@Environment(AppModel.self) var appModel
|
||||||
|
|
||||||
|
@State private var selection: NavigationContent?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
List {
|
||||||
|
NavigationLink(value: NavigationContent.audioPlayer) {
|
||||||
|
Label("Audio Player", systemImage: "play")
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink(value: NavigationContent.audioQueue) {
|
||||||
|
Label("Audio Queue", systemImage: "play.square.stack")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Home")
|
||||||
|
.navigationDestination(for: NavigationContent.self) { content in
|
||||||
|
DetailView(selection: content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ContentView()
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// Created by Dimitris C.
|
||||||
|
// Copyright © 2024 Decimal. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct DetailView: View {
|
||||||
|
@Environment(AppModel.self) var appModel
|
||||||
|
|
||||||
|
var selection: NavigationContent
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
switch selection {
|
||||||
|
case .audioPlayer:
|
||||||
|
AudioPlayerView(appModel: appModel)
|
||||||
|
case .audioQueue: // TODO
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
//
|
||||||
|
// Created by Dimitris C.
|
||||||
|
// Copyright © 2024 Decimal. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum NavigationContent: Hashable {
|
||||||
|
case audioPlayer
|
||||||
|
case audioQueue
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ContentSidebar: View {
|
||||||
|
@Binding var selection: NavigationContent?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List(selection: $selection) {
|
||||||
|
NavigationLink(value: NavigationContent.audioPlayer) {
|
||||||
|
Label("Audio Player", systemImage: "play")
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink(value: NavigationContent.audioQueue) {
|
||||||
|
Label("Audio Queue", systemImage: "play.square.stack")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Home")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Sidebar_Previews: PreviewProvider {
|
||||||
|
struct Preview: View {
|
||||||
|
@State private var selection: NavigationContent? = NavigationContent.audioPlayer
|
||||||
|
var body: some View {
|
||||||
|
ContentSidebar(selection: $selection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
NavigationSplitView {
|
||||||
|
Preview()
|
||||||
|
} detail: {
|
||||||
|
Text("Detail!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,13 +1,13 @@
|
|||||||
Pod::Spec.new do |s|
|
Pod::Spec.new do |s|
|
||||||
s.name = 'AudioStreaming'
|
s.name = 'AudioStreaming'
|
||||||
s.version = '0.7.0'
|
s.version = '1.2.1'
|
||||||
s.license = 'MIT'
|
s.license = 'MIT'
|
||||||
s.summary = 'An AudioPlayer/Streaming library for iOS written in Swift using AVAudioEngine.'
|
s.summary = 'An AudioPlayer/Streaming library for iOS written in Swift using AVAudioEngine.'
|
||||||
s.homepage = 'https://github.com/dimitris-c/AudioStreaming'
|
s.homepage = 'https://github.com/dimitris-c/AudioStreaming'
|
||||||
s.authors = { 'Dimitris C.' => 'dimmdesign@gmail.com' }
|
s.authors = { 'Dimitris C.' => 'dimmdesign@gmail.com' }
|
||||||
s.source = { :git => 'https://github.com/dimitris-c/AudioStreaming.git', :tag => s.version }
|
s.source = { :git => 'https://github.com/dimitris-c/AudioStreaming.git', :tag => s.version }
|
||||||
|
|
||||||
s.ios.deployment_target = '12.0'
|
s.ios.deployment_target = '13.0'
|
||||||
|
|
||||||
s.swift_versions = ['5.1', '5.2', '5.3']
|
s.swift_versions = ['5.1', '5.2', '5.3']
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
98ABF69E2BAB07A20059C441 /* Mp4Restructure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ABF69D2BAB07A20059C441 /* Mp4Restructure.swift */; };
|
||||||
|
98C82AE62B8CA8BC00AED485 /* RemoteMp4Restructure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C82AE52B8CA8BC00AED485 /* RemoteMp4Restructure.swift */; };
|
||||||
|
98CC396E28BD651E006C9FF9 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98CC396D28BD651E006C9FF9 /* Atomic.swift */; };
|
||||||
|
98DC00CC2B961F5E0068900A /* ByteBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98DC00CB2B961F5E0068900A /* ByteBuffer.swift */; };
|
||||||
|
98DC00CE2B9726380068900A /* ByteBufferTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98DC00CD2B9726380068900A /* ByteBufferTests.swift */; };
|
||||||
B500732024D00BAC00BB4475 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500731F24D00BAC00BB4475 /* Logger.swift */; };
|
B500732024D00BAC00BB4475 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500731F24D00BAC00BB4475 /* Logger.swift */; };
|
||||||
B514657F248E3884005C03F7 /* DispatchTimerSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514657E248E3884005C03F7 /* DispatchTimerSource.swift */; };
|
B514657F248E3884005C03F7 /* DispatchTimerSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B514657E248E3884005C03F7 /* DispatchTimerSource.swift */; };
|
||||||
B51B9F9A24DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51B9F9924DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift */; };
|
B51B9F9A24DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51B9F9924DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift */; };
|
||||||
@@ -33,7 +38,6 @@
|
|||||||
B5667A902499018D00D93F85 /* AudioFileStreamProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5667A8F2499018D00D93F85 /* AudioFileStreamProcessor.swift */; };
|
B5667A902499018D00D93F85 /* AudioFileStreamProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5667A8F2499018D00D93F85 /* AudioFileStreamProcessor.swift */; };
|
||||||
B5667A922499063D00D93F85 /* AudioPlayerContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5667A912499063D00D93F85 /* AudioPlayerContext.swift */; };
|
B5667A922499063D00D93F85 /* AudioPlayerContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5667A912499063D00D93F85 /* AudioPlayerContext.swift */; };
|
||||||
B5667B3E249BC43100D93F85 /* AudioPlayerRenderProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5667B3D249BC43000D93F85 /* AudioPlayerRenderProcessor.swift */; };
|
B5667B3E249BC43100D93F85 /* AudioPlayerRenderProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5667B3D249BC43000D93F85 /* AudioPlayerRenderProcessor.swift */; };
|
||||||
B5737340254DE43E003DFBEC /* measure.swift in Sources */ = {isa = PBXBuildFile; fileRef = B573733F254DE43E003DFBEC /* measure.swift */; };
|
|
||||||
B57829CF2548B32B00C78D36 /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57829CE2548B32B00C78D36 /* Lock.swift */; };
|
B57829CF2548B32B00C78D36 /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57829CE2548B32B00C78D36 /* Lock.swift */; };
|
||||||
B58386382544A2C10087A712 /* EntryFrames.swift in Sources */ = {isa = PBXBuildFile; fileRef = B58386372544A2C10087A712 /* EntryFrames.swift */; };
|
B58386382544A2C10087A712 /* EntryFrames.swift in Sources */ = {isa = PBXBuildFile; fileRef = B58386372544A2C10087A712 /* EntryFrames.swift */; };
|
||||||
B5838640254584A50087A712 /* ProcessedPackets.swift in Sources */ = {isa = PBXBuildFile; fileRef = B583863F254584A50087A712 /* ProcessedPackets.swift */; };
|
B5838640254584A50087A712 /* ProcessedPackets.swift in Sources */ = {isa = PBXBuildFile; fileRef = B583863F254584A50087A712 /* ProcessedPackets.swift */; };
|
||||||
@@ -64,8 +68,7 @@
|
|||||||
B5EF9557247E9439003E8FF8 /* AudioStreamSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF9556247E9439003E8FF8 /* AudioStreamSource.swift */; };
|
B5EF9557247E9439003E8FF8 /* AudioStreamSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF9556247E9439003E8FF8 /* AudioStreamSource.swift */; };
|
||||||
B5EF955B247EBCB3003E8FF8 /* AudioFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF955A247EBCB3003E8FF8 /* AudioFileType.swift */; };
|
B5EF955B247EBCB3003E8FF8 /* AudioFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF955A247EBCB3003E8FF8 /* AudioFileType.swift */; };
|
||||||
B5EF955D247ECBB1003E8FF8 /* RemoteAudioSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF955C247ECBB1003E8FF8 /* RemoteAudioSource.swift */; };
|
B5EF955D247ECBB1003E8FF8 /* RemoteAudioSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EF955C247ECBB1003E8FF8 /* RemoteAudioSource.swift */; };
|
||||||
B5F883B62476DADB00D277C1 /* Protected.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F883B52476DADB00D277C1 /* Protected.swift */; };
|
B5F883BA2477CEFC00D277C1 /* AtomicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F883B82477CBF600D277C1 /* AtomicTests.swift */; };
|
||||||
B5F883BA2477CEFC00D277C1 /* ProtectedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F883B82477CBF600D277C1 /* ProtectedTests.swift */; };
|
|
||||||
B5F883C32477DC4400D277C1 /* NetworkDataStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F883C22477DC4400D277C1 /* NetworkDataStream.swift */; };
|
B5F883C32477DC4400D277C1 /* NetworkDataStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F883C22477DC4400D277C1 /* NetworkDataStream.swift */; };
|
||||||
B5FB6C0525516507002C0A37 /* AudioConverter+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5FB6C0425516507002C0A37 /* AudioConverter+Helpers.swift */; };
|
B5FB6C0525516507002C0A37 /* AudioConverter+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5FB6C0425516507002C0A37 /* AudioConverter+Helpers.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
@@ -94,6 +97,11 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
98ABF69D2BAB07A20059C441 /* Mp4Restructure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mp4Restructure.swift; sourceTree = "<group>"; };
|
||||||
|
98C82AE52B8CA8BC00AED485 /* RemoteMp4Restructure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMp4Restructure.swift; sourceTree = "<group>"; };
|
||||||
|
98CC396D28BD651E006C9FF9 /* Atomic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = "<group>"; };
|
||||||
|
98DC00CB2B961F5E0068900A /* ByteBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ByteBuffer.swift; sourceTree = "<group>"; };
|
||||||
|
98DC00CD2B9726380068900A /* ByteBufferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ByteBufferTests.swift; sourceTree = "<group>"; };
|
||||||
B500731F24D00BAC00BB4475 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
B500731F24D00BAC00BB4475 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||||
B514657E248E3884005C03F7 /* DispatchTimerSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchTimerSource.swift; sourceTree = "<group>"; };
|
B514657E248E3884005C03F7 /* DispatchTimerSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchTimerSource.swift; sourceTree = "<group>"; };
|
||||||
B51B9F9924DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAudioFormat+Convenience.swift"; sourceTree = "<group>"; };
|
B51B9F9924DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAudioFormat+Convenience.swift"; sourceTree = "<group>"; };
|
||||||
@@ -121,7 +129,6 @@
|
|||||||
B5667A8F2499018D00D93F85 /* AudioFileStreamProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFileStreamProcessor.swift; sourceTree = "<group>"; };
|
B5667A8F2499018D00D93F85 /* AudioFileStreamProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFileStreamProcessor.swift; sourceTree = "<group>"; };
|
||||||
B5667A912499063D00D93F85 /* AudioPlayerContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerContext.swift; sourceTree = "<group>"; };
|
B5667A912499063D00D93F85 /* AudioPlayerContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerContext.swift; sourceTree = "<group>"; };
|
||||||
B5667B3D249BC43000D93F85 /* AudioPlayerRenderProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerRenderProcessor.swift; sourceTree = "<group>"; };
|
B5667B3D249BC43000D93F85 /* AudioPlayerRenderProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerRenderProcessor.swift; sourceTree = "<group>"; };
|
||||||
B573733F254DE43E003DFBEC /* measure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = measure.swift; sourceTree = "<group>"; };
|
|
||||||
B57829CE2548B32B00C78D36 /* Lock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lock.swift; sourceTree = "<group>"; };
|
B57829CE2548B32B00C78D36 /* Lock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lock.swift; sourceTree = "<group>"; };
|
||||||
B580CB1D25628CF4006D7DD8 /* AudioStreaming.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = AudioStreaming.podspec; sourceTree = "<group>"; };
|
B580CB1D25628CF4006D7DD8 /* AudioStreaming.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = AudioStreaming.podspec; sourceTree = "<group>"; };
|
||||||
B580CB1E25628CF4006D7DD8 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
|
B580CB1E25628CF4006D7DD8 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
|
||||||
@@ -158,8 +165,7 @@
|
|||||||
B5EF9556247E9439003E8FF8 /* AudioStreamSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioStreamSource.swift; sourceTree = "<group>"; };
|
B5EF9556247E9439003E8FF8 /* AudioStreamSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioStreamSource.swift; sourceTree = "<group>"; };
|
||||||
B5EF955A247EBCB3003E8FF8 /* AudioFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFileType.swift; sourceTree = "<group>"; };
|
B5EF955A247EBCB3003E8FF8 /* AudioFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFileType.swift; sourceTree = "<group>"; };
|
||||||
B5EF955C247ECBB1003E8FF8 /* RemoteAudioSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteAudioSource.swift; sourceTree = "<group>"; };
|
B5EF955C247ECBB1003E8FF8 /* RemoteAudioSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteAudioSource.swift; sourceTree = "<group>"; };
|
||||||
B5F883B52476DADB00D277C1 /* Protected.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Protected.swift; sourceTree = "<group>"; };
|
B5F883B82477CBF600D277C1 /* AtomicTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicTests.swift; sourceTree = "<group>"; };
|
||||||
B5F883B82477CBF600D277C1 /* ProtectedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtectedTests.swift; sourceTree = "<group>"; };
|
|
||||||
B5F883C22477DC4400D277C1 /* NetworkDataStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkDataStream.swift; sourceTree = "<group>"; };
|
B5F883C22477DC4400D277C1 /* NetworkDataStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkDataStream.swift; sourceTree = "<group>"; };
|
||||||
B5FB6C0425516507002C0A37 /* AudioConverter+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AudioConverter+Helpers.swift"; sourceTree = "<group>"; };
|
B5FB6C0425516507002C0A37 /* AudioConverter+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AudioConverter+Helpers.swift"; sourceTree = "<group>"; };
|
||||||
B5FFF5FD2549FA02006BBB7C /* AudioExample.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AudioExample.xctestplan; sourceTree = "<group>"; };
|
B5FFF5FD2549FA02006BBB7C /* AudioExample.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AudioExample.xctestplan; sourceTree = "<group>"; };
|
||||||
@@ -184,6 +190,15 @@
|
|||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
98C82AE42B8CA8AA00AED485 /* Mp4 */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
98C82AE52B8CA8BC00AED485 /* RemoteMp4Restructure.swift */,
|
||||||
|
98ABF69D2BAB07A20059C441 /* Mp4Restructure.swift */,
|
||||||
|
);
|
||||||
|
path = Mp4;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
B5276B70247D4D3D00D2F56A /* Network */ = {
|
B5276B70247D4D3D00D2F56A /* Network */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -302,6 +317,7 @@
|
|||||||
B58BD7FC255DB653005B756D /* Audio Source */ = {
|
B58BD7FC255DB653005B756D /* Audio Source */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
98C82AE42B8CA8AA00AED485 /* Mp4 */,
|
||||||
B5EF9556247E9439003E8FF8 /* AudioStreamSource.swift */,
|
B5EF9556247E9439003E8FF8 /* AudioStreamSource.swift */,
|
||||||
B5EF955C247ECBB1003E8FF8 /* RemoteAudioSource.swift */,
|
B5EF955C247ECBB1003E8FF8 /* RemoteAudioSource.swift */,
|
||||||
B59D0B6E255C904900D6CCE5 /* FileAudioSource.swift */,
|
B59D0B6E255C904900D6CCE5 /* FileAudioSource.swift */,
|
||||||
@@ -321,11 +337,11 @@
|
|||||||
B592E13025460883008866FB /* Helpers */ = {
|
B592E13025460883008866FB /* Helpers */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
B573733F254DE43E003DFBEC /* measure.swift */,
|
98DC00CB2B961F5E0068900A /* ByteBuffer.swift */,
|
||||||
|
98CC396D28BD651E006C9FF9 /* Atomic.swift */,
|
||||||
B514657E248E3884005C03F7 /* DispatchTimerSource.swift */,
|
B514657E248E3884005C03F7 /* DispatchTimerSource.swift */,
|
||||||
B57829CE2548B32B00C78D36 /* Lock.swift */,
|
B57829CE2548B32B00C78D36 /* Lock.swift */,
|
||||||
B500731F24D00BAC00BB4475 /* Logger.swift */,
|
B500731F24D00BAC00BB4475 /* Logger.swift */,
|
||||||
B5F883B52476DADB00D277C1 /* Protected.swift */,
|
|
||||||
B54C3E55255F286D00B356F2 /* Retrier.swift */,
|
B54C3E55255F286D00B356F2 /* Retrier.swift */,
|
||||||
);
|
);
|
||||||
path = Helpers;
|
path = Helpers;
|
||||||
@@ -431,8 +447,8 @@
|
|||||||
B5F883B42476DABE00D277C1 /* Core */ = {
|
B5F883B42476DABE00D277C1 /* Core */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
B592E11E2545FF33008866FB /* Structures */,
|
|
||||||
B55CE97624813BA10001C498 /* Extensions */,
|
B55CE97624813BA10001C498 /* Extensions */,
|
||||||
|
B592E11E2545FF33008866FB /* Structures */,
|
||||||
B5276B70247D4D3D00D2F56A /* Network */,
|
B5276B70247D4D3D00D2F56A /* Network */,
|
||||||
B592E13025460883008866FB /* Helpers */,
|
B592E13025460883008866FB /* Helpers */,
|
||||||
);
|
);
|
||||||
@@ -443,10 +459,11 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
B5EF954A247DA450003E8FF8 /* Network */,
|
B5EF954A247DA450003E8FF8 /* Network */,
|
||||||
B5F883B82477CBF600D277C1 /* ProtectedTests.swift */,
|
B5F883B82477CBF600D277C1 /* AtomicTests.swift */,
|
||||||
B51FE0C12488F96A00F2A4D2 /* QueueTests.swift */,
|
B51FE0C12488F96A00F2A4D2 /* QueueTests.swift */,
|
||||||
B592E12825460146008866FB /* BiMapTests.swift */,
|
B592E12825460146008866FB /* BiMapTests.swift */,
|
||||||
B592E133254608B4008866FB /* DispatchTimerSourceTests.swift */,
|
B592E133254608B4008866FB /* DispatchTimerSourceTests.swift */,
|
||||||
|
98DC00CD2B9726380068900A /* ByteBufferTests.swift */,
|
||||||
);
|
);
|
||||||
path = Core;
|
path = Core;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -584,7 +601,7 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
|
shellScript = "if [[ \"$(uname -m)\" == arm64 ]]; then\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
|
||||||
};
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
@@ -599,6 +616,7 @@
|
|||||||
B5838640254584A50087A712 /* ProcessedPackets.swift in Sources */,
|
B5838640254584A50087A712 /* ProcessedPackets.swift in Sources */,
|
||||||
B54C3E56255F286D00B356F2 /* Retrier.swift in Sources */,
|
B54C3E56255F286D00B356F2 /* Retrier.swift in Sources */,
|
||||||
B59DF10424916FD50043C498 /* DispatchQueue+Helpers.swift in Sources */,
|
B59DF10424916FD50043C498 /* DispatchQueue+Helpers.swift in Sources */,
|
||||||
|
98CC396E28BD651E006C9FF9 /* Atomic.swift in Sources */,
|
||||||
B5B3B7CC248647ED00656828 /* AudioPlayerState.swift in Sources */,
|
B5B3B7CC248647ED00656828 /* AudioPlayerState.swift in Sources */,
|
||||||
B51B9F9A24DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift in Sources */,
|
B51B9F9A24DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift in Sources */,
|
||||||
B51FE0C624890CCB00F2A4D2 /* PlayerQueueEntries.swift in Sources */,
|
B51FE0C624890CCB00F2A4D2 /* PlayerQueueEntries.swift in Sources */,
|
||||||
@@ -607,7 +625,6 @@
|
|||||||
B59DF1A32493E90C0043C498 /* AudioFileStream+Helpers.swift in Sources */,
|
B59DF1A32493E90C0043C498 /* AudioFileStream+Helpers.swift in Sources */,
|
||||||
B54D876D2490E4A000C361A0 /* UnitDescriptions.swift in Sources */,
|
B54D876D2490E4A000C361A0 /* UnitDescriptions.swift in Sources */,
|
||||||
B514657F248E3884005C03F7 /* DispatchTimerSource.swift in Sources */,
|
B514657F248E3884005C03F7 /* DispatchTimerSource.swift in Sources */,
|
||||||
B5737340254DE43E003DFBEC /* measure.swift in Sources */,
|
|
||||||
B55CEABC24853CD20001C498 /* AudioPlayer.swift in Sources */,
|
B55CEABC24853CD20001C498 /* AudioPlayer.swift in Sources */,
|
||||||
B5667B3E249BC43100D93F85 /* AudioPlayerRenderProcessor.swift in Sources */,
|
B5667B3E249BC43100D93F85 /* AudioPlayerRenderProcessor.swift in Sources */,
|
||||||
B5276B6F247D21A000D2F56A /* NetworkingClient.swift in Sources */,
|
B5276B6F247D21A000D2F56A /* NetworkingClient.swift in Sources */,
|
||||||
@@ -617,6 +634,7 @@
|
|||||||
B5D4A41025D948EF00E1450C /* IcycastHeadersProcessor.swift in Sources */,
|
B5D4A41025D948EF00E1450C /* IcycastHeadersProcessor.swift in Sources */,
|
||||||
B5667A902499018D00D93F85 /* AudioFileStreamProcessor.swift in Sources */,
|
B5667A902499018D00D93F85 /* AudioFileStreamProcessor.swift in Sources */,
|
||||||
B59D0B6F255C904900D6CCE5 /* FileAudioSource.swift in Sources */,
|
B59D0B6F255C904900D6CCE5 /* FileAudioSource.swift in Sources */,
|
||||||
|
98DC00CC2B961F5E0068900A /* ByteBuffer.swift in Sources */,
|
||||||
B5EF9555247E9393003E8FF8 /* AudioEntry.swift in Sources */,
|
B5EF9555247E9393003E8FF8 /* AudioEntry.swift in Sources */,
|
||||||
B5B36E432655A32200DC96F5 /* FrameFilterProcessor.swift in Sources */,
|
B5B36E432655A32200DC96F5 /* FrameFilterProcessor.swift in Sources */,
|
||||||
B51FE0C02488F67C00F2A4D2 /* Queue.swift in Sources */,
|
B51FE0C02488F67C00F2A4D2 /* Queue.swift in Sources */,
|
||||||
@@ -633,12 +651,13 @@
|
|||||||
B55CE96E248058B60001C498 /* MetadataParser.swift in Sources */,
|
B55CE96E248058B60001C498 /* MetadataParser.swift in Sources */,
|
||||||
B5838644254584BE0087A712 /* AudioStreamState.swift in Sources */,
|
B5838644254584BE0087A712 /* AudioStreamState.swift in Sources */,
|
||||||
B500732024D00BAC00BB4475 /* Logger.swift in Sources */,
|
B500732024D00BAC00BB4475 /* Logger.swift in Sources */,
|
||||||
|
98C82AE62B8CA8BC00AED485 /* RemoteMp4Restructure.swift in Sources */,
|
||||||
B5276B74247D4D9F00D2F56A /* NetworkSessionDelegate.swift in Sources */,
|
B5276B74247D4D9F00D2F56A /* NetworkSessionDelegate.swift in Sources */,
|
||||||
B55F77D624DACE140057F431 /* BufferContext.swift in Sources */,
|
B55F77D624DACE140057F431 /* BufferContext.swift in Sources */,
|
||||||
B5838648254584D90087A712 /* SeekRequest.swift in Sources */,
|
B5838648254584D90087A712 /* SeekRequest.swift in Sources */,
|
||||||
B5D82E65255DD562009EDAA4 /* NetStatusService.swift in Sources */,
|
B5D82E65255DD562009EDAA4 /* NetStatusService.swift in Sources */,
|
||||||
B55CE97824813BCA0001C498 /* UnsafeMutablePointer+Helpers.swift in Sources */,
|
B55CE97824813BCA0001C498 /* UnsafeMutablePointer+Helpers.swift in Sources */,
|
||||||
B5F883B62476DADB00D277C1 /* Protected.swift in Sources */,
|
98ABF69E2BAB07A20059C441 /* Mp4Restructure.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -651,10 +670,11 @@
|
|||||||
B51FE0C824892D1600F2A4D2 /* PlayerQueueEntriesTest.swift in Sources */,
|
B51FE0C824892D1600F2A4D2 /* PlayerQueueEntriesTest.swift in Sources */,
|
||||||
B55CEABA248530C00001C498 /* MetadataParser.swift in Sources */,
|
B55CEABA248530C00001C498 /* MetadataParser.swift in Sources */,
|
||||||
B51FE0C22488F96A00F2A4D2 /* QueueTests.swift in Sources */,
|
B51FE0C22488F96A00F2A4D2 /* QueueTests.swift in Sources */,
|
||||||
B5F883BA2477CEFC00D277C1 /* ProtectedTests.swift in Sources */,
|
B5F883BA2477CEFC00D277C1 /* AtomicTests.swift in Sources */,
|
||||||
B592E134254608B4008866FB /* DispatchTimerSourceTests.swift in Sources */,
|
B592E134254608B4008866FB /* DispatchTimerSourceTests.swift in Sources */,
|
||||||
B55CEAB82485172D0001C498 /* HTTPHeaderParserTests.swift in Sources */,
|
B55CEAB82485172D0001C498 /* HTTPHeaderParserTests.swift in Sources */,
|
||||||
B592E12925460146008866FB /* BiMapTests.swift in Sources */,
|
B592E12925460146008866FB /* BiMapTests.swift in Sources */,
|
||||||
|
98DC00CE2B9726380068900A /* ByteBufferTests.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -721,8 +741,8 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
MARKETING_VERSION = 0.1.0;
|
MARKETING_VERSION = 1.1.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
@@ -780,8 +800,8 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
MARKETING_VERSION = 0.1.0;
|
MARKETING_VERSION = 1.1.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -805,13 +825,13 @@
|
|||||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
INFOPLIST_FILE = AudioStreaming/Info.plist;
|
INFOPLIST_FILE = AudioStreaming/Info.plist;
|
||||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@loader_path/Frameworks",
|
"@loader_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.7.0;
|
MARKETING_VERSION = 1.2.1;
|
||||||
OTHER_LDFLAGS = "-ObjC";
|
OTHER_LDFLAGS = "-ObjC";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
@@ -836,13 +856,13 @@
|
|||||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
INFOPLIST_FILE = AudioStreaming/Info.plist;
|
INFOPLIST_FILE = AudioStreaming/Info.plist;
|
||||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@loader_path/Frameworks",
|
"@loader_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.7.0;
|
MARKETING_VERSION = 1.2.1;
|
||||||
OTHER_LDFLAGS = "-ObjC";
|
OTHER_LDFLAGS = "-ObjC";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
@@ -861,6 +881,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
INFOPLIST_FILE = AudioStreamingTests/Info.plist;
|
INFOPLIST_FILE = AudioStreamingTests/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -881,6 +902,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
INFOPLIST_FILE = AudioStreamingTests/Info.plist;
|
INFOPLIST_FILE = AudioStreamingTests/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<Workspace
|
<Workspace
|
||||||
version = "1.0">
|
version = "1.0">
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "group:AudioExample/AudioExample.xcodeproj">
|
location = "group:AudioPlayer/AudioPlayer.xcodeproj">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "container:AudioStreaming.xcodeproj">
|
location = "container:AudioStreaming.xcodeproj">
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
|
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
|
||||||
extension AVAudioFormat {
|
public extension AVAudioFormat {
|
||||||
/// The underlying audio stream description.
|
/// The underlying audio stream description.
|
||||||
///
|
///
|
||||||
/// This exposes the `pointee` value of the `UsafePointer<AudioStreamBasicDescription>`
|
/// This exposes the `pointee` value of the `UsafePointer<AudioStreamBasicDescription>`
|
||||||
public var basicStreamDescription: AudioStreamBasicDescription {
|
var basicStreamDescription: AudioStreamBasicDescription {
|
||||||
return streamDescription.pointee
|
return streamDescription.pointee
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import AVFoundation
|
|||||||
@discardableResult
|
@discardableResult
|
||||||
func fileStreamGetProperty<Value>(value: inout Value, fileStream streamId: AudioFileStreamID, propertyId: AudioFileStreamPropertyID) -> OSStatus {
|
func fileStreamGetProperty<Value>(value: inout Value, fileStream streamId: AudioFileStreamID, propertyId: AudioFileStreamPropertyID) -> OSStatus {
|
||||||
var (size, _) = fileStreamGetPropertyInfo(fileStream: streamId, propertyId: propertyId)
|
var (size, _) = fileStreamGetPropertyInfo(fileStream: streamId, propertyId: propertyId)
|
||||||
let status = AudioFileStreamGetProperty(streamId, propertyId, &size, &value)
|
return withUnsafeMutablePointer(to: &value) { pointer in
|
||||||
guard status == noErr else {
|
let status = AudioFileStreamGetProperty(streamId, propertyId, &size, pointer)
|
||||||
|
guard status == noErr else {
|
||||||
|
return status
|
||||||
|
}
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
return status
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileStreamGetPropertyInfo(fileStream streamId: AudioFileStreamID, propertyId: AudioFileStreamPropertyID) -> (size: UInt32, status: OSStatus) {
|
func fileStreamGetPropertyInfo(fileStream streamId: AudioFileStreamID, propertyId: AudioFileStreamPropertyID) -> (size: UInt32, status: OSStatus) {
|
||||||
@@ -112,3 +114,50 @@ public enum AudioFileStreamError: CustomDebugStringConvertible {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension AudioFileStreamPropertyID {
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case kAudioFileStreamProperty_ReadyToProducePackets:
|
||||||
|
return "Ready to produce packets"
|
||||||
|
case kAudioFileStreamProperty_FileFormat:
|
||||||
|
return "File format"
|
||||||
|
case kAudioFileStreamProperty_DataFormat:
|
||||||
|
return "Data format"
|
||||||
|
case kAudioFileStreamProperty_AudioDataByteCount:
|
||||||
|
return "Byte count"
|
||||||
|
case kAudioFileStreamProperty_AudioDataPacketCount:
|
||||||
|
return "Packet count"
|
||||||
|
case kAudioFileStreamProperty_DataOffset:
|
||||||
|
return "Data offset"
|
||||||
|
case kAudioFileStreamProperty_BitRate:
|
||||||
|
return "Bit rate"
|
||||||
|
case kAudioFileStreamProperty_FormatList:
|
||||||
|
return "Format list"
|
||||||
|
case kAudioFileStreamProperty_MagicCookieData:
|
||||||
|
return "Magic cookie"
|
||||||
|
case kAudioFileStreamProperty_MaximumPacketSize:
|
||||||
|
return "Max packet size"
|
||||||
|
case kAudioFileStreamProperty_ChannelLayout:
|
||||||
|
return "Channel layout"
|
||||||
|
case kAudioFileStreamProperty_PacketToFrame:
|
||||||
|
return "Packet to frame"
|
||||||
|
case kAudioFileStreamProperty_FrameToPacket:
|
||||||
|
return "Frame to packet"
|
||||||
|
case kAudioFileStreamProperty_PacketToByte:
|
||||||
|
return "Packet to byte"
|
||||||
|
case kAudioFileStreamProperty_ByteToPacket:
|
||||||
|
return "Byte to packet"
|
||||||
|
case kAudioFileStreamProperty_PacketTableInfo:
|
||||||
|
return "Packet table"
|
||||||
|
case kAudioFileStreamProperty_PacketSizeUpperBound:
|
||||||
|
return "Packet size upper bound"
|
||||||
|
case kAudioFileStreamProperty_AverageBytesPerPacket:
|
||||||
|
return "Average bytes per packet"
|
||||||
|
case kAudioFileStreamProperty_InfoDictionary:
|
||||||
|
return "Info dictionary"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ final class Atomic<Value> {
|
|||||||
_value = value
|
_value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
var value: Value { lock.around { _value } }
|
var value: Value { lock.withLock { _value } }
|
||||||
|
|
||||||
func write(_ transform: (inout Value) -> Void) {
|
func write(_ transform: (inout Value) -> Void) {
|
||||||
lock.around { transform(&self._value) }
|
lock.withLock { transform(&self._value) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
//
|
||||||
|
// Created by Dimitrios Chatzieleftheriou on 4/03/2024.
|
||||||
|
// Copyright © 2024 Decimal. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// Struct representing a buffer for handling binary data
|
||||||
|
struct ByteBuffer {
|
||||||
|
// Custom errors for ByteBuffer
|
||||||
|
enum Error: Swift.Error {
|
||||||
|
case eof // End of file
|
||||||
|
case parse // Parsing error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data storage for the buffer
|
||||||
|
private(set) var storage = Data()
|
||||||
|
|
||||||
|
// Current offset in the buffer
|
||||||
|
var offset: Int = 0
|
||||||
|
|
||||||
|
// Calculated property for the number of bytes available for reading
|
||||||
|
var bytesAvailable: Int {
|
||||||
|
storage.count - offset
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculated property for the length of the buffer
|
||||||
|
var length: Int {
|
||||||
|
get {
|
||||||
|
storage.count
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
// Adjusting the length of the buffer
|
||||||
|
switch true {
|
||||||
|
case storage.count < newValue:
|
||||||
|
storage.append(Data(count: newValue - storage.count))
|
||||||
|
case newValue < storage.count:
|
||||||
|
storage = storage.subdata(in: 0 ..< newValue)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscript for accessing individual bytes in the buffer
|
||||||
|
subscript(i: Int) -> UInt8 {
|
||||||
|
get { storage[i] }
|
||||||
|
set { storage[i] = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the buffer with given data
|
||||||
|
init(data: Data) {
|
||||||
|
storage = data
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the buffer with a specified size, filling it with zeros
|
||||||
|
init(size: Int) {
|
||||||
|
storage = Data(repeating: 0x00, count: size)
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the buffer (reset offset to zero)
|
||||||
|
@discardableResult
|
||||||
|
mutating func clear() -> Self {
|
||||||
|
offset = 0
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewind the buffer (reset offset to zero)
|
||||||
|
mutating func rewind() {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a specified number of bytes from the buffer
|
||||||
|
mutating func readBytes(_ length: Int) throws -> Data {
|
||||||
|
guard length <= bytesAvailable else {
|
||||||
|
throw ByteBuffer.Error.eof
|
||||||
|
}
|
||||||
|
offset += length
|
||||||
|
return storage.subdata(in: offset - length ..< offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write data into the buffer
|
||||||
|
@discardableResult
|
||||||
|
mutating func writeBytes(_ value: Data) -> Self {
|
||||||
|
// If the offset is at the end, append the value to the data
|
||||||
|
if offset == storage.count {
|
||||||
|
storage.append(value)
|
||||||
|
offset = storage.count
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
// Otherwise, write the value into the buffer at the current offset
|
||||||
|
let length: Int = min(storage.count, value.count)
|
||||||
|
storage[offset ..< offset + length] = value[0 ..< length]
|
||||||
|
// If the value is longer than the remaining space, append the rest to the data
|
||||||
|
if length == storage.count {
|
||||||
|
storage.append(value[length ..< value.count])
|
||||||
|
}
|
||||||
|
offset += value.count
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write integer value into the buffer
|
||||||
|
@discardableResult
|
||||||
|
mutating func put<T: FixedWidthInteger>(_ value: T) -> ByteBuffer {
|
||||||
|
writeBytes(value.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write float value into the buffer
|
||||||
|
@discardableResult
|
||||||
|
mutating func put(_ value: Float) -> ByteBuffer {
|
||||||
|
writeBytes(Data(value.data.reversed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write double value into the buffer
|
||||||
|
@discardableResult
|
||||||
|
mutating func put(_ value: Double) -> ByteBuffer {
|
||||||
|
writeBytes(Data(value.data.reversed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read an integer value from the buffer
|
||||||
|
mutating func getInteger<T: FixedWidthInteger>() throws -> T {
|
||||||
|
let sizeOfInteger = MemoryLayout<T>.size
|
||||||
|
guard sizeOfInteger <= bytesAvailable else {
|
||||||
|
throw ByteBuffer.Error.eof
|
||||||
|
}
|
||||||
|
offset += sizeOfInteger
|
||||||
|
return T(data: storage[offset - sizeOfInteger ..< offset]).bigEndian
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read an integer value from a specific index in the buffer
|
||||||
|
func getInteger<T: FixedWidthInteger>(_ index: Int) throws -> T {
|
||||||
|
let sizeOfInteger = MemoryLayout<T>.size
|
||||||
|
guard sizeOfInteger + index <= length else {
|
||||||
|
throw ByteBuffer.Error.eof
|
||||||
|
}
|
||||||
|
return T(data: storage[index ..< index + sizeOfInteger]).bigEndian
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a float value from the buffer
|
||||||
|
mutating func getFloat() throws -> Float {
|
||||||
|
let sizeOfFloat = MemoryLayout<UInt32>.size
|
||||||
|
guard sizeOfFloat <= bytesAvailable else {
|
||||||
|
throw ByteBuffer.Error.eof
|
||||||
|
}
|
||||||
|
offset += sizeOfFloat
|
||||||
|
return Float(data: Data(storage.subdata(in: offset - sizeOfFloat ..< offset).reversed()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a double value from the buffer
|
||||||
|
mutating func getDouble() throws -> Double {
|
||||||
|
let sizeOfDouble = MemoryLayout<UInt64>.size
|
||||||
|
guard sizeOfDouble <= bytesAvailable else {
|
||||||
|
throw ByteBuffer.Error.eof
|
||||||
|
}
|
||||||
|
offset += sizeOfDouble
|
||||||
|
return Double(data: Data(storage.subdata(in: offset - sizeOfDouble ..< offset).reversed()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension to provide conformance to ExpressibleByIntegerLiteral for easy conversion between integers and Data
|
||||||
|
extension ExpressibleByIntegerLiteral {
|
||||||
|
// Convert integer to Data
|
||||||
|
var data: Data {
|
||||||
|
return withUnsafePointer(to: self) { pointer in
|
||||||
|
Data(bytes: pointer, count: MemoryLayout<Self>.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize from Data
|
||||||
|
init(data: Data) {
|
||||||
|
let diff: Int = MemoryLayout<Self>.size - data.count
|
||||||
|
if diff > 0 {
|
||||||
|
var buffer = Data(repeating: 0, count: diff)
|
||||||
|
buffer.append(data)
|
||||||
|
self = buffer.withUnsafeBytes { $0.baseAddress!.assumingMemoryBound(to: Self.self).pointee }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self = data.withUnsafeBytes { $0.baseAddress!.assumingMemoryBound(to: Self.self).pointee }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize from Data slice
|
||||||
|
init(data: Slice<Data>) {
|
||||||
|
self.init(data: Data(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,10 @@ import Foundation
|
|||||||
final class DispatchTimerSource {
|
final class DispatchTimerSource {
|
||||||
private var handler: (() -> Void)?
|
private var handler: (() -> Void)?
|
||||||
private let timer: DispatchSourceTimer
|
private let timer: DispatchSourceTimer
|
||||||
internal var state: SourceState = .suspended
|
var state: SourceState = .suspended
|
||||||
|
|
||||||
/// The state of the timer
|
/// The state of the timer
|
||||||
internal enum SourceState {
|
enum SourceState {
|
||||||
case activated
|
case activated
|
||||||
case suspended
|
case suspended
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,46 +8,53 @@ import Foundation
|
|||||||
protocol Lock {
|
protocol Lock {
|
||||||
func lock()
|
func lock()
|
||||||
func unlock()
|
func unlock()
|
||||||
}
|
|
||||||
|
|
||||||
extension Lock {
|
|
||||||
// Execute a closure while acquiring a lock and returns the closure value
|
// Execute a closure while acquiring a lock and returns the closure value
|
||||||
@inline(__always)
|
func withLock<Result>(body: () throws -> Result) rethrows -> Result
|
||||||
func around<Value>(_ closure: () -> Value) -> Value {
|
|
||||||
lock(); defer { unlock() }
|
|
||||||
return closure()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute a closure while acquiring a lock
|
// Execute a closure while acquiring a lock
|
||||||
@inline(__always)
|
func withLock(body: () -> Void)
|
||||||
func around(_ closure: () -> Void) {
|
|
||||||
lock(); defer { unlock() }
|
|
||||||
closure()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A wrapper for `os_unfair_lock`
|
/// A wrapper for `os_unfair_lock`
|
||||||
/// - Tag: UnfairLock
|
/// - Tag: UnfairLock
|
||||||
final class UnfairLock: Lock {
|
final class UnfairLock: Lock {
|
||||||
private let unfairLock: os_unfair_lock_t
|
@usableFromInline let unfairLock: UnsafeMutablePointer<os_unfair_lock>
|
||||||
|
|
||||||
internal init() {
|
init() {
|
||||||
unfairLock = .allocate(capacity: 1)
|
unfairLock = .allocate(capacity: 1)
|
||||||
unfairLock.initialize(to: os_unfair_lock())
|
unfairLock.initialize(to: os_unfair_lock())
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
unfairLock.deinitialize(count: 1)
|
|
||||||
unfairLock.deallocate()
|
unfairLock.deallocate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
@inline(__always)
|
@inline(__always)
|
||||||
internal func lock() {
|
func withLock<Result>(body: () throws -> Result) rethrows -> Result {
|
||||||
|
os_unfair_lock_lock(unfairLock)
|
||||||
|
defer { os_unfair_lock_unlock(unfairLock) }
|
||||||
|
return try body()
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
@inline(__always)
|
||||||
|
func withLock(body: () -> Void) {
|
||||||
|
os_unfair_lock_lock(unfairLock)
|
||||||
|
defer { os_unfair_lock_unlock(unfairLock) }
|
||||||
|
body()
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
@inline(__always)
|
||||||
|
func lock() {
|
||||||
os_unfair_lock_lock(unfairLock)
|
os_unfair_lock_lock(unfairLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
@inline(__always)
|
@inline(__always)
|
||||||
internal func unlock() {
|
func unlock() {
|
||||||
os_unfair_lock_unlock(unfairLock)
|
os_unfair_lock_unlock(unfairLock)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import os
|
|||||||
|
|
||||||
private let loggingSubsystem = "audio.streaming.log"
|
private let loggingSubsystem = "audio.streaming.log"
|
||||||
|
|
||||||
internal enum Logger {
|
enum Logger {
|
||||||
private static let audioRendering = OSLog(subsystem: loggingSubsystem, category: "audio.rendering")
|
private static let audioRendering = OSLog(subsystem: loggingSubsystem, category: "audio.rendering")
|
||||||
private static let networking = OSLog(subsystem: loggingSubsystem, category: "audio.networking")
|
private static let networking = OSLog(subsystem: loggingSubsystem, category: "audio.networking")
|
||||||
private static let generic = OSLog(subsystem: loggingSubsystem, category: "audio.streaming.generic")
|
private static let generic = OSLog(subsystem: loggingSubsystem, category: "audio.streaming.generic")
|
||||||
@@ -31,7 +31,7 @@ internal enum Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func error(_ message: StaticString, category: Category, args: CVarArg...) {
|
static func error(_ message: StaticString, category: Category, args: CVarArg...) {
|
||||||
proccess(message, category: category, type: .error, args: args)
|
process(message, category: category, type: .error, args: args)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func error(_ message: StaticString, category: Category) {
|
static func error(_ message: StaticString, category: Category) {
|
||||||
@@ -39,14 +39,14 @@ internal enum Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func debug(_ message: StaticString, category: Category, args: CVarArg...) {
|
static func debug(_ message: StaticString, category: Category, args: CVarArg...) {
|
||||||
proccess(message, category: category, type: .debug, args: args)
|
process(message, category: category, type: .debug, args: args)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func debug(_ message: StaticString, category: Category) {
|
static func debug(_ message: StaticString, category: Category) {
|
||||||
debug(message, category: category, args: [])
|
debug(message, category: category, args: [])
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func proccess(_ message: StaticString, category: Category, type: OSLogType, args: CVarArg...) {
|
private static func process(_ message: StaticString, category: Category, type: OSLogType, args: CVarArg...) {
|
||||||
guard isEnabled else { return }
|
guard isEnabled else { return }
|
||||||
os_log(message, log: category.toOSLog(), type: type, args)
|
os_log(message, log: category.toOSLog(), type: type, args)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
//
|
|
||||||
// Created by Dimitrios Chatzieleftheriou on 21/05/2020.
|
|
||||||
// Copyright © 2020 Decimal. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
internal final class Protected<Value> {
|
|
||||||
var value: Value { lock.around { _value } }
|
|
||||||
|
|
||||||
private let lock = UnfairLock()
|
|
||||||
private var _value: Value
|
|
||||||
|
|
||||||
init(_ value: Value) {
|
|
||||||
_value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
func read<Element>(_ closure: (Value) -> Element) -> Element {
|
|
||||||
lock.around { closure(self._value) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func write<Element>(_ closure: (inout Value) -> Element) -> Element {
|
|
||||||
lock.around { closure(&self._value) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,7 @@ final class Retrier {
|
|||||||
private let maxInterval: Int
|
private let maxInterval: Int
|
||||||
private let timeoutTimer: DispatchTimerSource
|
private let timeoutTimer: DispatchTimerSource
|
||||||
|
|
||||||
/// Initiliazes a new object with the given parameters
|
/// Initializes a new object with the given parameters
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - interval: The Mach absolute time at which to execute the dispatch source's event handler.
|
/// - interval: The Mach absolute time at which to execute the dispatch source's event handler.
|
||||||
/// - maxInterval: The maximum interval in which the internal timer will retry the callback.
|
/// - maxInterval: The maximum interval in which the internal timer will retry the callback.
|
||||||
@@ -38,6 +38,7 @@ final class Retrier {
|
|||||||
|
|
||||||
/// Cancels retrying
|
/// Cancels retrying
|
||||||
func cancel() {
|
func cancel() {
|
||||||
|
interval = .seconds(1)
|
||||||
timeoutTimer.removeHandler()
|
timeoutTimer.removeHandler()
|
||||||
timeoutTimer.suspend()
|
timeoutTimer.suspend()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
//
|
|
||||||
// measure.swift
|
|
||||||
// AudioStreaming
|
|
||||||
//
|
|
||||||
// Created by Dimitrios Chatzieleftheriou on 31/10/2020.
|
|
||||||
// Copyright © 2020 Decimal. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
func measure(name: String = "", block: () -> Void) {
|
|
||||||
let started = ProcessInfo.processInfo.systemUptime
|
|
||||||
block()
|
|
||||||
print("diff for \(name): \(String(format: "%.6f", ProcessInfo.processInfo.systemUptime - started))")
|
|
||||||
}
|
|
||||||
@@ -9,12 +9,14 @@ import Network
|
|||||||
enum NetConnectionType: Equatable {
|
enum NetConnectionType: Equatable {
|
||||||
case cellular(connected: Bool)
|
case cellular(connected: Bool)
|
||||||
case wifi(connected: Bool)
|
case wifi(connected: Bool)
|
||||||
|
case other(connected: Bool)
|
||||||
case undetermined
|
case undetermined
|
||||||
|
|
||||||
var isConnected: Bool {
|
var isConnected: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case let .cellular(connected),
|
case let .cellular(connected),
|
||||||
let .wifi(connected):
|
let .wifi(connected),
|
||||||
|
let .other(connected):
|
||||||
return connected
|
return connected
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@@ -39,15 +41,13 @@ final class NetStatusService: NetStatusProvider {
|
|||||||
network.currentPath.toNetConnectionType()
|
network.currentPath.toNetConnectionType()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var currentConnectionType: NetConnectionType = .undetermined
|
|
||||||
|
|
||||||
private let network: NWPathMonitor
|
private let network: NWPathMonitor
|
||||||
|
|
||||||
private let monitorQueue: DispatchQueue
|
private let monitorQueue: DispatchQueue
|
||||||
|
|
||||||
init(network: NWPathMonitor) {
|
init(network: NWPathMonitor) {
|
||||||
self.network = network
|
self.network = network
|
||||||
monitorQueue = DispatchQueue(label: "net.path.queue", qos: .background)
|
monitorQueue = DispatchQueue(label: "net.path.queue", qos: .utility)
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@@ -59,20 +59,15 @@ final class NetStatusService: NetStatusProvider {
|
|||||||
/// - parameter connectionChange: A callback block to listen to changes of the network type, this skips duplicates.
|
/// - parameter connectionChange: A callback block to listen to changes of the network type, this skips duplicates.
|
||||||
/// - Note: The callback will be executed on the main thread.
|
/// - Note: The callback will be executed on the main thread.
|
||||||
func start(connectionChange: @escaping (NetConnectionType) -> Void) {
|
func start(connectionChange: @escaping (NetConnectionType) -> Void) {
|
||||||
network.pathUpdateHandler = { [weak self] path in
|
network.pathUpdateHandler = { path in
|
||||||
guard let self = self else { return }
|
let connectionType = path.toNetConnectionType()
|
||||||
let connecionType = path.toNetConnectionType()
|
connectionChange(connectionType)
|
||||||
if self.currentConnectionType != connecionType {
|
|
||||||
connectionChange(self.connectionType)
|
|
||||||
self.currentConnectionType = self.connectionType
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
startIfNeeded()
|
startIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop() {
|
func stop() {
|
||||||
network.cancel()
|
network.cancel()
|
||||||
network.pathUpdateHandler = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func startIfNeeded() {
|
func startIfNeeded() {
|
||||||
@@ -85,12 +80,17 @@ extension NWPath {
|
|||||||
func toNetConnectionType() -> NetConnectionType {
|
func toNetConnectionType() -> NetConnectionType {
|
||||||
let isCellular = usesInterfaceType(.cellular)
|
let isCellular = usesInterfaceType(.cellular)
|
||||||
let isWifi = usesInterfaceType(.wifi)
|
let isWifi = usesInterfaceType(.wifi)
|
||||||
|
let isOther = usesInterfaceType(.loopback)
|
||||||
|
|| usesInterfaceType(.other)
|
||||||
|
|| usesInterfaceType(.wiredEthernet)
|
||||||
let isConnected = status == .satisfied
|
let isConnected = status == .satisfied
|
||||||
|
|
||||||
if isCellular {
|
if isCellular {
|
||||||
return .cellular(connected: isConnected)
|
return .cellular(connected: isConnected)
|
||||||
} else if isWifi {
|
} else if isWifi {
|
||||||
return .wifi(connected: isConnected)
|
return .wifi(connected: isConnected)
|
||||||
|
} else if isOther {
|
||||||
|
return .other(connected: isConnected)
|
||||||
}
|
}
|
||||||
|
|
||||||
return .undetermined
|
return .undetermined
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
internal final class NetworkDataStream {
|
final class NetworkDataStream {
|
||||||
typealias StreamResult = Result<Response, Error>
|
typealias StreamResult = Result<Response, Error>
|
||||||
typealias StreamCompletion = (_ event: NetworkDataStream.ResponseEvent) -> Void
|
typealias StreamCompletion = (_ event: NetworkDataStream.ResponseEvent) -> Void
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ internal final class NetworkDataStream {
|
|||||||
task?.response as? HTTPURLResponse
|
task?.response as? HTTPURLResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
internal init(id: UUID, underlyingQueue: DispatchQueue) {
|
init(id: UUID, underlyingQueue: DispatchQueue) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.underlyingQueue = underlyingQueue
|
self.underlyingQueue = underlyingQueue
|
||||||
state = .initialised
|
state = .initialised
|
||||||
@@ -94,7 +94,7 @@ internal final class NetworkDataStream {
|
|||||||
|
|
||||||
// MARK: Internal
|
// MARK: Internal
|
||||||
|
|
||||||
internal func didReceive(response: HTTPURLResponse?) {
|
func didReceive(response: HTTPURLResponse?) {
|
||||||
underlyingQueue.async { [weak self] in
|
underlyingQueue.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard let streamCallback = self.streamCallback else { return }
|
guard let streamCallback = self.streamCallback else { return }
|
||||||
@@ -102,7 +102,7 @@ internal final class NetworkDataStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal func didReceive(data: Data, response: HTTPURLResponse?) {
|
func didReceive(data: Data, response: HTTPURLResponse?) {
|
||||||
underlyingQueue.async { [weak self] in
|
underlyingQueue.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard let streamCallback = self.streamCallback else { return }
|
guard let streamCallback = self.streamCallback else { return }
|
||||||
@@ -111,7 +111,7 @@ internal final class NetworkDataStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal func didComplete(with error: Error?, response: HTTPURLResponse?) {
|
func didComplete(with error: Error?, response: HTTPURLResponse?) {
|
||||||
underlyingQueue.async { [weak self] in
|
underlyingQueue.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard let stream = self.streamCallback else { return }
|
guard let stream = self.streamCallback else { return }
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
internal final class NetworkSessionDelegate: NSObject, URLSessionDataDelegate {
|
final class NetworkSessionDelegate: NSObject, URLSessionDataDelegate {
|
||||||
weak var taskProvider: StreamTaskProvider?
|
weak var taskProvider: StreamTaskProvider?
|
||||||
|
|
||||||
internal func stream(for task: URLSessionTask) -> NetworkDataStream? {
|
func stream(for task: URLSessionTask) -> NetworkDataStream? {
|
||||||
guard let taskProvider = taskProvider else {
|
guard let taskProvider = taskProvider else {
|
||||||
assertionFailure("couldn't found taskProvider")
|
assertionFailure("couldn't found taskProvider")
|
||||||
return nil
|
return nil
|
||||||
@@ -16,22 +16,22 @@ internal final class NetworkSessionDelegate: NSObject, URLSessionDataDelegate {
|
|||||||
return taskProvider.dataStream(for: task)
|
return taskProvider.dataStream(for: task)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal func urlSession(_: URLSession,
|
func urlSession(_: URLSession,
|
||||||
dataTask: URLSessionDataTask,
|
dataTask: URLSessionDataTask,
|
||||||
didReceive data: Data)
|
didReceive data: Data)
|
||||||
{
|
{
|
||||||
guard let stream = self.stream(for: dataTask) else {
|
guard let stream = stream(for: dataTask) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
stream.didReceive(data: data,
|
stream.didReceive(data: data,
|
||||||
response: dataTask.response as? HTTPURLResponse)
|
response: dataTask.response as? HTTPURLResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal func urlSession(_: URLSession,
|
func urlSession(_: URLSession,
|
||||||
task: URLSessionTask,
|
task: URLSessionTask,
|
||||||
didCompleteWithError error: Error?)
|
didCompleteWithError error: Error?)
|
||||||
{
|
{
|
||||||
guard let stream = self.stream(for: task) else {
|
guard let stream = stream(for: task) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
stream.didComplete(with: error, response: task.response as? HTTPURLResponse)
|
stream.didComplete(with: error, response: task.response as? HTTPURLResponse)
|
||||||
@@ -42,7 +42,7 @@ internal final class NetworkSessionDelegate: NSObject, URLSessionDataDelegate {
|
|||||||
didReceive response: URLResponse,
|
didReceive response: URLResponse,
|
||||||
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
|
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
|
||||||
{
|
{
|
||||||
guard let stream = self.stream(for: dataTask) else {
|
guard let stream = stream(for: dataTask) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
stream.didReceive(response: response as? HTTPURLResponse)
|
stream.didReceive(response: response as? HTTPURLResponse)
|
||||||
|
|||||||
@@ -13,12 +13,15 @@ enum DataStreamError: Error {
|
|||||||
public enum NetworkError: Error, Equatable {
|
public enum NetworkError: Error, Equatable {
|
||||||
case failure(Error)
|
case failure(Error)
|
||||||
case serverError
|
case serverError
|
||||||
|
case missingData
|
||||||
public static func == (lhs: NetworkError, rhs: NetworkError) -> Bool {
|
public static func == (lhs: NetworkError, rhs: NetworkError) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case (.failure, failure):
|
case (.failure, failure):
|
||||||
return true
|
return true
|
||||||
case (.serverError, .serverError):
|
case (.serverError, .serverError):
|
||||||
return true
|
return true
|
||||||
|
case (.missingData, .missingData):
|
||||||
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -41,7 +44,7 @@ extension URLSessionConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal final class NetworkingClient {
|
final class NetworkingClient {
|
||||||
let session: URLSession
|
let session: URLSession
|
||||||
weak var delegate: NetworkSessionDelegate?
|
weak var delegate: NetworkSessionDelegate?
|
||||||
let networkQueue: DispatchQueue
|
let networkQueue: DispatchQueue
|
||||||
@@ -49,9 +52,9 @@ internal final class NetworkingClient {
|
|||||||
var tasksLock = UnfairLock()
|
var tasksLock = UnfairLock()
|
||||||
var tasks = BiMap<URLSessionTask, NetworkDataStream>()
|
var tasks = BiMap<URLSessionTask, NetworkDataStream>()
|
||||||
|
|
||||||
internal init(configuration: URLSessionConfiguration = .networkingConfiguration,
|
init(configuration: URLSessionConfiguration = .networkingConfiguration,
|
||||||
delegate: NetworkSessionDelegate = NetworkSessionDelegate(),
|
delegate: NetworkSessionDelegate = NetworkSessionDelegate(),
|
||||||
networkQueue: DispatchQueue = DispatchQueue(label: "audio.streaming.session.network.queue"))
|
networkQueue: DispatchQueue = DispatchQueue(label: "audio.streaming.session.network.queue"))
|
||||||
{
|
{
|
||||||
let delegateQueue = operationQueue(underlyingQueue: networkQueue)
|
let delegateQueue = operationQueue(underlyingQueue: networkQueue)
|
||||||
let session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: delegateQueue)
|
let session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: delegateQueue)
|
||||||
@@ -67,22 +70,38 @@ internal final class NetworkingClient {
|
|||||||
|
|
||||||
/// Creates a data stream for the given `URLRequest`
|
/// Creates a data stream for the given `URLRequest`
|
||||||
/// - parameter request: A `URLRequest` to be used for the data stream
|
/// - parameter request: A `URLRequest` to be used for the data stream
|
||||||
internal func stream(request: URLRequest) -> NetworkDataStream {
|
func stream(request: URLRequest) -> NetworkDataStream {
|
||||||
let stream = NetworkDataStream(id: UUID(), underlyingQueue: networkQueue)
|
let stream = NetworkDataStream(id: UUID(), underlyingQueue: networkQueue)
|
||||||
setupRequest(stream, request: request)
|
setupRequest(stream, request: request)
|
||||||
return stream
|
return stream
|
||||||
}
|
}
|
||||||
|
|
||||||
internal func remove(task: NetworkDataStream) {
|
func remove(task: NetworkDataStream) {
|
||||||
tasksLock.lock(); defer { tasksLock.unlock() }
|
tasksLock.withLock {
|
||||||
if !tasks.isEmpty {
|
if !tasks.isEmpty {
|
||||||
tasks[task] = nil
|
tasks[task] = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func task(request: URLRequest, completion: @escaping (Result<Data, Error>) -> Void) -> URLSessionDataTask {
|
||||||
|
let task = session.dataTask(with: request) { data, _, error in
|
||||||
|
if let error {
|
||||||
|
completion(Result<Data, Error>.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let data {
|
||||||
|
completion(Result<Data, Error>.success(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
task.resume()
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
/// Schedules the given `NetworkDataStream` to be performed immediatelly
|
/// Schedules the given `NetworkDataStream` to be performed immediately
|
||||||
/// - parameter stream: The `NetworkDataStream` object to be performed
|
/// - parameter stream: The `NetworkDataStream` object to be performed
|
||||||
/// - parameter request: The `URLRequest` for the `stream`
|
/// - parameter request: The `URLRequest` for the `stream`
|
||||||
private func setupRequest(_ stream: NetworkDataStream, request: URLRequest) {
|
private func setupRequest(_ stream: NetworkDataStream, request: URLRequest) {
|
||||||
@@ -96,14 +115,16 @@ internal final class NetworkingClient {
|
|||||||
// MARK: StreamTaskProvider conformance
|
// MARK: StreamTaskProvider conformance
|
||||||
|
|
||||||
extension NetworkingClient: StreamTaskProvider {
|
extension NetworkingClient: StreamTaskProvider {
|
||||||
internal func dataStream(for request: URLSessionTask) -> NetworkDataStream? {
|
func dataStream(for request: URLSessionTask) -> NetworkDataStream? {
|
||||||
tasksLock.lock(); defer { tasksLock.unlock() }
|
tasksLock.withLock {
|
||||||
return tasks[request] ?? nil
|
tasks[request] ?? nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal func sessionTask(for stream: NetworkDataStream) -> URLSessionTask? {
|
func sessionTask(for stream: NetworkDataStream) -> URLSessionTask? {
|
||||||
tasksLock.lock(); defer { tasksLock.unlock() }
|
tasksLock.withLock {
|
||||||
return tasks[stream] ?? nil
|
tasks[stream] ?? nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// A convenient type that holds tasks in a two-way manner, such as `URLSessionTask` to `NetworkDataStream` and reverved
|
/// A convenient type that holds tasks in a two-way manner, such as `URLSessionTask` to `NetworkDataStream` and reversed
|
||||||
struct BiMap<Left, Right> where Left: Hashable, Right: Hashable {
|
struct BiMap<Left, Right> where Left: Hashable, Right: Hashable {
|
||||||
private var leftToRight: [Left: Right] = [:]
|
private var leftToRight: [Left: Right] = [:]
|
||||||
private var rightToLeft: [Right: Left] = [:]
|
private var rightToLeft: [Right: Left] = [:]
|
||||||
|
|||||||
@@ -25,13 +25,15 @@
|
|||||||
+---+ +---+
|
+---+ +---+
|
||||||
```
|
```
|
||||||
*/
|
*/
|
||||||
final class Queue<Element>: Sequence, CustomDebugStringConvertible {
|
final class Queue<Element: Equatable>: Sequence, CustomDebugStringConvertible {
|
||||||
private var _storage: [Element] = []
|
private var _storage: [Element] = []
|
||||||
|
|
||||||
var isEmpty: Bool { _storage.isEmpty }
|
var isEmpty: Bool { _storage.isEmpty }
|
||||||
|
|
||||||
var count: Int { _storage.count }
|
var count: Int { _storage.count }
|
||||||
|
|
||||||
|
var items: [Element] { _storage }
|
||||||
|
|
||||||
/// Inserts an item at the end of the queue
|
/// Inserts an item at the end of the queue
|
||||||
func enqueue(item: Element) {
|
func enqueue(item: Element) {
|
||||||
_storage.insert(item, at: 0)
|
_storage.insert(item, at: 0)
|
||||||
@@ -55,6 +57,30 @@ final class Queue<Element>: Sequence, CustomDebugStringConvertible {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inserts an item at a specific index in the queue
|
||||||
|
func insert(item: Element, at index: Int) {
|
||||||
|
guard index >= 0 && index <= count else {
|
||||||
|
fatalError("Index out of range")
|
||||||
|
}
|
||||||
|
_storage.insert(item, at: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(item: Element) {
|
||||||
|
guard let index = _storage.firstIndex(of: item) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_storage.remove(at: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the item at the specified index in the queue
|
||||||
|
@discardableResult
|
||||||
|
func remove(at index: Int) -> Element? {
|
||||||
|
guard index >= 0 && index < count else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return _storage.remove(at: index)
|
||||||
|
}
|
||||||
|
|
||||||
/// Retrieves the last item
|
/// Retrieves the last item
|
||||||
func peek() -> Element? {
|
func peek() -> Element? {
|
||||||
_storage.last
|
_storage.last
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import AudioToolbox
|
|||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
|
||||||
public struct AudioEntryId: Equatable {
|
public struct AudioEntryId: Equatable {
|
||||||
internal var unique = UUID()
|
var unique = UUID()
|
||||||
public var id: String
|
public var id: String
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class AudioEntry {
|
class AudioEntry {
|
||||||
private let estimationMinPackets = 2
|
private let estimationMinPackets = 2
|
||||||
private let estimationMinPacketsPreferred = 64
|
private let estimationMinPacketsPreferred = 64
|
||||||
|
|
||||||
@@ -22,9 +22,7 @@ internal class AudioEntry {
|
|||||||
let id: AudioEntryId
|
let id: AudioEntryId
|
||||||
|
|
||||||
/// The sample rate from the `audioStreamFormat`
|
/// The sample rate from the `audioStreamFormat`
|
||||||
var sampleRate: Float {
|
var sampleRate: Float
|
||||||
Float(audioStreamFormat.mSampleRate)
|
|
||||||
}
|
|
||||||
|
|
||||||
var audioFileHint: AudioFileTypeID {
|
var audioFileHint: AudioFileTypeID {
|
||||||
source.audioFileHint
|
source.audioFileHint
|
||||||
@@ -49,11 +47,9 @@ internal class AudioEntry {
|
|||||||
private(set) var framesState: EntryFramesState
|
private(set) var framesState: EntryFramesState
|
||||||
private(set) var processedPacketsState: ProcessedPacketsState
|
private(set) var processedPacketsState: ProcessedPacketsState
|
||||||
|
|
||||||
var packetDuration: Double {
|
var packetDuration: Double
|
||||||
return Double(audioStreamFormat.mFramesPerPacket) / Double(sampleRate)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var avaragePacketByteSize: Double {
|
private var averagePacketByteSize: Double {
|
||||||
let packets = processedPacketsState
|
let packets = processedPacketsState
|
||||||
guard !packets.isEmpty else { return 0 }
|
guard !packets.isEmpty else { return 0 }
|
||||||
return Double(packets.sizeTotal / packets.count)
|
return Double(packets.sizeTotal / packets.count)
|
||||||
@@ -72,6 +68,8 @@ internal class AudioEntry {
|
|||||||
processedPacketsState = ProcessedPacketsState()
|
processedPacketsState = ProcessedPacketsState()
|
||||||
framesState = EntryFramesState()
|
framesState = EntryFramesState()
|
||||||
audioStreamState = AudioStreamState()
|
audioStreamState = AudioStreamState()
|
||||||
|
sampleRate = 0
|
||||||
|
packetDuration = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func close() {
|
func close() {
|
||||||
@@ -94,7 +92,9 @@ internal class AudioEntry {
|
|||||||
|
|
||||||
func reset() {
|
func reset() {
|
||||||
lock.lock(); defer { lock.unlock() }
|
lock.lock(); defer { lock.unlock() }
|
||||||
framesState = EntryFramesState()
|
framesState.played = 0
|
||||||
|
framesState.queued = 0
|
||||||
|
framesState.lastFrameQueued = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
func has(same source: CoreAudioStreamSource) -> Bool {
|
func has(same source: CoreAudioStreamSource) -> Bool {
|
||||||
@@ -109,7 +109,7 @@ internal class AudioEntry {
|
|||||||
if packetsCount > estimationMinPacketsPreferred ||
|
if packetsCount > estimationMinPacketsPreferred ||
|
||||||
(audioStreamFormat.mBytesPerFrame == 0 && packetsCount > estimationMinPackets)
|
(audioStreamFormat.mBytesPerFrame == 0 && packetsCount > estimationMinPackets)
|
||||||
{
|
{
|
||||||
return avaragePacketByteSize / packetDuration * 8
|
return averagePacketByteSize / packetDuration * 8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (Double(audioStreamFormat.mBytesPerFrame) * audioStreamFormat.mSampleRate) * 8
|
return (Double(audioStreamFormat.mBytesPerFrame) * audioStreamFormat.mSampleRate) * 8
|
||||||
@@ -151,12 +151,12 @@ extension AudioEntry: AudioStreamSourceDelegate {
|
|||||||
delegate?.dataAvailable(source: source, data: data)
|
delegate?.dataAvailable(source: source, data: data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func errorOccured(source: CoreAudioStreamSource, error: Error) {
|
func errorOccurred(source: CoreAudioStreamSource, error: Error) {
|
||||||
delegate?.errorOccured(source: source, error: error)
|
delegate?.errorOccurred(source: source, error: error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func endOfFileOccured(source: CoreAudioStreamSource) {
|
func endOfFileOccurred(source: CoreAudioStreamSource) {
|
||||||
delegate?.endOfFileOccured(source: source)
|
delegate?.endOfFileOccurred(source: source)
|
||||||
}
|
}
|
||||||
|
|
||||||
func metadataReceived(data: [String: String]) {
|
func metadataReceived(data: [String: String]) {
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ import Foundation
|
|||||||
final class SeekRequest {
|
final class SeekRequest {
|
||||||
let lock = UnfairLock()
|
let lock = UnfairLock()
|
||||||
var requested: Bool = false
|
var requested: Bool = false
|
||||||
var version = Protected<Int>(0)
|
var version = Atomic<Int>(0)
|
||||||
var time: Double = 0
|
var time: Double = 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ protocol AudioStreamSourceDelegate: AnyObject {
|
|||||||
/// Indicates that there's data available
|
/// Indicates that there's data available
|
||||||
func dataAvailable(source: CoreAudioStreamSource, data: Data)
|
func dataAvailable(source: CoreAudioStreamSource, data: Data)
|
||||||
/// Indicates an error occurred
|
/// Indicates an error occurred
|
||||||
func errorOccured(source: CoreAudioStreamSource, error: Error)
|
func errorOccurred(source: CoreAudioStreamSource, error: Error)
|
||||||
/// Indicates end of file has occurred
|
/// Indicates end of file has occurred
|
||||||
func endOfFileOccured(source: CoreAudioStreamSource)
|
func endOfFileOccurred(source: CoreAudioStreamSource)
|
||||||
/// Indicates metadata read from stream
|
/// Indicates metadata read from stream
|
||||||
func metadataReceived(data: [String: String])
|
func metadataReceived(data: [String: String])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// Copyright © 2020 Decimal. All rights reserved.
|
// Copyright © 2020 Decimal. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
|
||||||
final class FileAudioSource: NSObject, CoreAudioStreamSource {
|
final class FileAudioSource: NSObject, CoreAudioStreamSource {
|
||||||
@@ -17,6 +18,12 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
|
|||||||
audioFileType(fileExtension: url.pathExtension)
|
audioFileType(fileExtension: url.pathExtension)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var isMp4: Bool {
|
||||||
|
audioFileHint == kAudioFileM4AType || audioFileHint == kAudioFileMPEG4Type
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mp4IsAlreadyOptimized: Bool = false
|
||||||
|
|
||||||
private var seekOffset: Int
|
private var seekOffset: Int
|
||||||
|
|
||||||
private let url: URL
|
private let url: URL
|
||||||
@@ -26,6 +33,8 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
|
|||||||
private var buffer: UnsafeMutablePointer<UInt8>
|
private var buffer: UnsafeMutablePointer<UInt8>
|
||||||
private var inputStream: InputStream?
|
private var inputStream: InputStream?
|
||||||
|
|
||||||
|
private var mp4Restructure: Mp4Restructure
|
||||||
|
|
||||||
init(url: URL,
|
init(url: URL,
|
||||||
fileManager: FileManager = .default,
|
fileManager: FileManager = .default,
|
||||||
underlyingQueue: DispatchQueue,
|
underlyingQueue: DispatchQueue,
|
||||||
@@ -35,6 +44,7 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
|
|||||||
self.underlyingQueue = underlyingQueue
|
self.underlyingQueue = underlyingQueue
|
||||||
self.fileManager = fileManager
|
self.fileManager = fileManager
|
||||||
self.readSize = readSize
|
self.readSize = readSize
|
||||||
|
self.mp4Restructure = Mp4Restructure()
|
||||||
buffer = UnsafeMutablePointer.uint8pointer(of: readSize)
|
buffer = UnsafeMutablePointer.uint8pointer(of: readSize)
|
||||||
seekOffset = 0
|
seekOffset = 0
|
||||||
position = 0
|
position = 0
|
||||||
@@ -43,6 +53,7 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
|
|||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
buffer.deallocate()
|
buffer.deallocate()
|
||||||
|
mp4Restructure.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
func close() {
|
func close() {
|
||||||
@@ -54,12 +65,8 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
|
|||||||
inputStream.delegate = nil
|
inputStream.delegate = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func suspend() {
|
// no-op
|
||||||
guard let inputStream = inputStream else {
|
func suspend() {}
|
||||||
return
|
|
||||||
}
|
|
||||||
CFReadStreamSetDispatchQueue(inputStream, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func resume() {
|
func resume() {
|
||||||
guard let inputStream = inputStream else {
|
guard let inputStream = inputStream else {
|
||||||
@@ -69,40 +76,38 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func seek(at offset: Int) {
|
func seek(at offset: Int) {
|
||||||
close()
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try performOpen(seek: offset)
|
try performOpen(seek: offset)
|
||||||
} catch {
|
} catch {
|
||||||
delegate?.errorOccured(source: self, error: error)
|
delegate?.errorOccurred(source: self, error: error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func performOpen(seek seekOffset: Int) throws {
|
private func performOpen(seek seekOffset: Int) throws {
|
||||||
guard let inputStream = InputStream(url: url) else {
|
|
||||||
throw AudioSystemError.playerStartError
|
|
||||||
}
|
|
||||||
self.inputStream = inputStream
|
|
||||||
|
|
||||||
var reopened = false
|
var reopened = false
|
||||||
let streamStatus = inputStream.streamStatus
|
let status = inputStream?.streamStatus ?? .closed
|
||||||
if streamStatus == .notOpen || streamStatus == .error {
|
if status == .atEnd || status == .closed || status == .error {
|
||||||
reopened = true
|
reopened = true
|
||||||
close()
|
close()
|
||||||
open(inputStream: inputStream)
|
try open()
|
||||||
}
|
}
|
||||||
|
|
||||||
let attributes = try fileManager.attributesOfItem(atPath: url.path)
|
var offset = seekOffset
|
||||||
length = (attributes[.size] as? Int) ?? 0
|
if isMp4, mp4Restructure.dataOptimized {
|
||||||
|
offset = mp4Restructure.seekAdjusted(offset: seekOffset)
|
||||||
|
}
|
||||||
|
|
||||||
if inputStream.setProperty(seekOffset, forKey: .fileCurrentOffsetKey) {
|
if inputStream?.setProperty(offset, forKey: .fileCurrentOffsetKey) == true {
|
||||||
position = seekOffset
|
position = offset
|
||||||
} else {
|
} else {
|
||||||
position = 0
|
position = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reopened {
|
if !reopened {
|
||||||
if inputStream.hasBytesAvailable {
|
underlyingQueue.async { [weak self] in
|
||||||
dataAvailable()
|
if self?.inputStream?.hasBytesAvailable == true {
|
||||||
|
self?.dataAvailable()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,17 +117,62 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
|
|||||||
let read = inputStream.read(buffer, maxLength: readSize)
|
let read = inputStream.read(buffer, maxLength: readSize)
|
||||||
if read > 0 {
|
if read > 0 {
|
||||||
let data = Data(bytes: buffer, count: read)
|
let data = Data(bytes: buffer, count: read)
|
||||||
delegate?.dataAvailable(source: self, data: data)
|
if isMp4, !mp4IsAlreadyOptimized {
|
||||||
|
if !mp4Restructure.dataOptimized {
|
||||||
|
do {
|
||||||
|
if let mp4OptimizeInfo = try mp4Restructure.checkIsOptimized(data: data) {
|
||||||
|
try performMp4Restructure(inputStream: inputStream, mp4OptimizeInfo: mp4OptimizeInfo)
|
||||||
|
} else {
|
||||||
|
mp4IsAlreadyOptimized = true
|
||||||
|
delegate?.dataAvailable(source: self, data: data)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
delegate?.errorOccurred(source: self, error: error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delegate?.dataAvailable(source: self, data: data)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delegate?.dataAvailable(source: self, data: data)
|
||||||
|
}
|
||||||
position += read
|
position += read
|
||||||
} else {
|
} else {
|
||||||
position += getCurrentOffsetFromStream()
|
position += getCurrentOffsetFromStream()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func open(inputStream: InputStream) {
|
func performMp4Restructure(inputStream: InputStream, mp4OptimizeInfo: Mp4OptimizeInfo) throws {
|
||||||
|
let offsetAccepted = inputStream.setProperty(mp4OptimizeInfo.moovOffset, forKey: .fileCurrentOffsetKey)
|
||||||
|
if offsetAccepted {
|
||||||
|
let moovDataBuffer = UnsafeMutablePointer.uint8pointer(of: mp4OptimizeInfo.moovSize)
|
||||||
|
defer { moovDataBuffer.deallocate() }
|
||||||
|
let moovRead = inputStream.read(moovDataBuffer, maxLength: mp4OptimizeInfo.moovSize)
|
||||||
|
if moovRead > 0 {
|
||||||
|
let data = Data(bytes: moovDataBuffer, count: moovRead)
|
||||||
|
let moovData = try mp4Restructure.restructureMoov(data: data)
|
||||||
|
delegate?.dataAvailable(source: self, data: moovData.initialData)
|
||||||
|
if !inputStream.setProperty(moovData.mdatOffset, forKey: .fileCurrentOffsetKey) {
|
||||||
|
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delegate?.errorOccurred(source: self, error: AudioSystemError.playerStartError)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delegate?.errorOccurred(source: self, error: inputStream.streamError ?? AudioSystemError.playerStartError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func open() throws {
|
||||||
|
guard let inputStream = InputStream(url: url) else {
|
||||||
|
throw AudioSystemError.playerStartError
|
||||||
|
}
|
||||||
|
self.inputStream = inputStream
|
||||||
CFReadStreamSetDispatchQueue(inputStream, underlyingQueue)
|
CFReadStreamSetDispatchQueue(inputStream, underlyingQueue)
|
||||||
inputStream.delegate = self
|
inputStream.delegate = self
|
||||||
inputStream.open()
|
inputStream.open()
|
||||||
|
|
||||||
|
let attributes = try fileManager.attributesOfItem(atPath: url.path)
|
||||||
|
length = (attributes[.size] as? Int) ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getCurrentOffsetFromStream() -> Int {
|
private func getCurrentOffsetFromStream() -> Int {
|
||||||
@@ -139,11 +189,9 @@ extension FileAudioSource: StreamDelegate {
|
|||||||
case .hasBytesAvailable:
|
case .hasBytesAvailable:
|
||||||
dataAvailable()
|
dataAvailable()
|
||||||
case .endEncountered:
|
case .endEncountered:
|
||||||
delegate?.endOfFileOccured(source: self)
|
delegate?.endOfFileOccurred(source: self)
|
||||||
case .errorOccurred:
|
case .errorOccurred:
|
||||||
delegate?.errorOccured(source: self, error: AudioPlayerError.codecError)
|
delegate?.errorOccurred(source: self, error: AudioPlayerError.codecError)
|
||||||
case .endEncountered:
|
|
||||||
delegate?.endOfFileOccured(source: self)
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
//
|
||||||
|
// Created by Dimitrios Chatzieleftheriou on 20/03/2024.
|
||||||
|
// Copyright © 2020 Decimal. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct MP4Atom: Equatable, CustomDebugStringConvertible {
|
||||||
|
let type: Int
|
||||||
|
let size: Int
|
||||||
|
let offset: Int
|
||||||
|
var data: Data?
|
||||||
|
|
||||||
|
var isFreeSpaceAtom: Bool {
|
||||||
|
type == Atoms.free || type == Atoms.skip || type == Atoms.wide
|
||||||
|
}
|
||||||
|
|
||||||
|
var debugDescription: String {
|
||||||
|
"[Atom][size: \(size))][type: \(Atoms.integerToFourCC(type) ?? "")][offset: \(offset)]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Mp4OptimizeInfo: Equatable {
|
||||||
|
let moovOffset: Int
|
||||||
|
let moovSize: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
/// These are some atoms, helpful for audio mp4
|
||||||
|
enum Atoms {
|
||||||
|
static var ftyp: Int { fourCcToInt("ftyp") }
|
||||||
|
static var moov: Int { fourCcToInt("moov") }
|
||||||
|
static var mdat: Int { fourCcToInt("mdat") }
|
||||||
|
static var free: Int { fourCcToInt("free") }
|
||||||
|
static var skip: Int { fourCcToInt("skip") }
|
||||||
|
static var wide: Int { fourCcToInt("wide") }
|
||||||
|
|
||||||
|
static var cmov: Int { fourCcToInt("cmov") }
|
||||||
|
static var stco: Int { fourCcToInt("stco") }
|
||||||
|
static var co64: Int { fourCcToInt("c064") }
|
||||||
|
|
||||||
|
static var atomPreampleSize: Int = 8
|
||||||
|
|
||||||
|
static func fourCcToInt(_ fourCc: String) -> Int {
|
||||||
|
let data = fourCc.data(using: .ascii)!
|
||||||
|
return Int(bigEndian: Int(data: data))
|
||||||
|
}
|
||||||
|
|
||||||
|
static func integerToFourCC(_ value: Int) -> String? {
|
||||||
|
guard value >= 0, value <= 0xFFFF_FFFF else {
|
||||||
|
return nil // Integer value out of range
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes: [UInt8] = []
|
||||||
|
bytes.append(UInt8((value >> 24) & 0xFF))
|
||||||
|
bytes.append(UInt8((value >> 16) & 0xFF))
|
||||||
|
bytes.append(UInt8((value >> 8) & 0xFF))
|
||||||
|
bytes.append(UInt8(value & 0xFF))
|
||||||
|
|
||||||
|
let data = Data(bytes)
|
||||||
|
return String(data: data, encoding: .ascii)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Mp4RestructureError: Error {
|
||||||
|
case unableToRestructureData
|
||||||
|
case missingMoovData
|
||||||
|
case invalidMoovAtom
|
||||||
|
case invalidAtomSize
|
||||||
|
case invalidAtomType
|
||||||
|
case invalidOffset
|
||||||
|
case missingMdatAtom
|
||||||
|
case missingMoovAtom
|
||||||
|
case compressedAtomNotSupported
|
||||||
|
case nonOptimizedMp4AndServerCannotSeek
|
||||||
|
case networkError(Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Mp4Restructure {
|
||||||
|
|
||||||
|
private var atomOffset: Int = 0
|
||||||
|
private var atoms: [MP4Atom] = []
|
||||||
|
private var ftyp: MP4Atom?
|
||||||
|
private var foundMoov = false
|
||||||
|
private var foundMdat = false
|
||||||
|
|
||||||
|
private(set) var dataOptimized: Bool = false
|
||||||
|
|
||||||
|
private var moovAtomSize: Int = 0
|
||||||
|
|
||||||
|
func clear() {
|
||||||
|
atomOffset = 0
|
||||||
|
atoms = []
|
||||||
|
ftyp = nil
|
||||||
|
foundMdat = false
|
||||||
|
foundMoov = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adjust the seekOffset of subtracting the moovAtomSize
|
||||||
|
/// - Parameter offset: A byte offset
|
||||||
|
/// - Returns: An adjusted byte offset
|
||||||
|
func seekAdjusted(offset: Int) -> Int {
|
||||||
|
offset - moovAtomSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func restructureMoov(data: Data) throws -> (initialData: Data, mdatOffset: Int) {
|
||||||
|
let (atomData, moovSize) = try doRestructureMoov(data: data)
|
||||||
|
moovAtomSize = moovSize
|
||||||
|
guard let mdatIndex = atoms.firstIndex(where: { $0.type == Atoms.mdat }) else {
|
||||||
|
throw Mp4RestructureError.missingMdatAtom
|
||||||
|
}
|
||||||
|
let mdatAtom = atoms[mdatIndex]
|
||||||
|
let atoms = Array(atoms[..<mdatIndex])
|
||||||
|
let dataOfAtomsBefore = atoms.filter { $0.data != nil }.compactMap(\.data)
|
||||||
|
let accumulatedInitialData = dataOfAtomsBefore.reduce(into: Data()) { partialResult, data in
|
||||||
|
partialResult.append(data)
|
||||||
|
}
|
||||||
|
let initialData = accumulatedInitialData + atomData
|
||||||
|
let mdatOffset: Int
|
||||||
|
if let ftyp = ftyp {
|
||||||
|
mdatOffset = ftyp.offset + ftyp.size
|
||||||
|
} else {
|
||||||
|
let freeSpaceAtoms = atoms.filter(\.isFreeSpaceAtom)
|
||||||
|
let freeSpaceSize = freeSpaceAtoms.reduce(into: 0) { partialResult, atom in
|
||||||
|
partialResult += atom.size
|
||||||
|
}
|
||||||
|
mdatOffset = mdatAtom.offset - freeSpaceSize
|
||||||
|
}
|
||||||
|
dataOptimized = true
|
||||||
|
return (initialData, mdatOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `nil` if the data is optimized otherwise `Mp4OptimizeInfo`
|
||||||
|
func checkIsOptimized(data: Data) throws -> Mp4OptimizeInfo? {
|
||||||
|
while atomOffset < UInt64(data.count) {
|
||||||
|
var atomSize = try Int(getInteger(data: data, offset: atomOffset) as UInt32)
|
||||||
|
let atomType = try Int(getInteger(data: data, offset: atomOffset + 4) as UInt32)
|
||||||
|
switch atomType {
|
||||||
|
case Atoms.ftyp:
|
||||||
|
let ftypData = data[Int(atomOffset) ..< atomSize]
|
||||||
|
let ftyp = MP4Atom(type: atomType, size: atomSize, offset: atomOffset, data: ftypData)
|
||||||
|
self.ftyp = ftyp
|
||||||
|
atoms.append(ftyp)
|
||||||
|
case Atoms.mdat:
|
||||||
|
// ref: https://developer.apple.com/documentation/quicktime-file-format/movie_data_atom
|
||||||
|
// This atom can be quite large, and may exceed 2^32 bytes, in which case the size field will be set to 1,
|
||||||
|
// and the header will contain a 64-bit extended size field.
|
||||||
|
if atomSize == 1 {
|
||||||
|
atomSize = Int(try getInteger(data: data, offset: atomOffset + 8) as UInt64)
|
||||||
|
}
|
||||||
|
let mdat = MP4Atom(type: atomType, size: atomSize, offset: atomOffset)
|
||||||
|
atoms.append(mdat)
|
||||||
|
foundMdat = true
|
||||||
|
case Atoms.moov:
|
||||||
|
let moov = MP4Atom(type: atomType, size: atomSize, offset: atomOffset)
|
||||||
|
atoms.append(moov)
|
||||||
|
foundMoov = true
|
||||||
|
default:
|
||||||
|
let atom = MP4Atom(type: atomType, size: atomSize, offset: atomOffset)
|
||||||
|
atoms.append(atom)
|
||||||
|
}
|
||||||
|
if ftyp != nil {
|
||||||
|
if foundMoov && !foundMdat {
|
||||||
|
Logger.debug("🕵️ detected an optimized mp4", category: .generic)
|
||||||
|
return nil
|
||||||
|
} else if !foundMoov && foundMdat {
|
||||||
|
Logger.debug("🕵️ detected an non-optimized mp4", category: .generic)
|
||||||
|
let possibleMoovOffset = Int(atomOffset) + atomSize
|
||||||
|
return Mp4OptimizeInfo(moovOffset: possibleMoovOffset, moovSize: atomSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
atomOffset += atomSize
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// logic taken from qt-faststart.c over at ffmpeg
|
||||||
|
/// https://github.com/FFmpeg/FFmpeg/blob/b47b2c5b912558b639c8542993e1256f9c69e675/tools/qt-faststart.c
|
||||||
|
private func doRestructureMoov(data: Data) throws -> (Data, Int) {
|
||||||
|
var moovAtomSize: Int = 0
|
||||||
|
var moovAtomType: Int = 0
|
||||||
|
var originalData = ByteBuffer(data: data)
|
||||||
|
var offset: Int = 0
|
||||||
|
// do search for moov within the new data
|
||||||
|
while offset < originalData.length {
|
||||||
|
moovAtomSize = Int(try originalData.getInteger() as UInt32)
|
||||||
|
moovAtomType = Int(try originalData.getInteger() as UInt32)
|
||||||
|
|
||||||
|
if moovAtomType == Atoms.moov {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
offset += moovAtomSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// error if we couldn't find an moov type
|
||||||
|
guard moovAtomType == Atoms.moov else {
|
||||||
|
throw Mp4RestructureError.missingMoovAtom
|
||||||
|
}
|
||||||
|
|
||||||
|
originalData.offset = offset
|
||||||
|
var moovAtom = ByteBuffer(size: moovAtomSize)
|
||||||
|
let slicedData: Data = try originalData.readBytes(moovAtom.length)
|
||||||
|
moovAtom.writeBytes(slicedData)
|
||||||
|
moovAtom.rewind()
|
||||||
|
|
||||||
|
if try Int(moovAtom.getInteger(12) as UInt32) == Atoms.cmov {
|
||||||
|
Logger.debug("Compressed moov atom not supported", category: .generic)
|
||||||
|
throw Mp4RestructureError.compressedAtomNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
var atomType: Int
|
||||||
|
var atomSize: Int
|
||||||
|
|
||||||
|
// crawl through the atom and restructure offsets
|
||||||
|
while moovAtom.bytesAvailable >= 8 {
|
||||||
|
let atomHead = moovAtom.offset
|
||||||
|
atomType = try Int(moovAtom.getInteger(atomHead + 4) as UInt32)
|
||||||
|
|
||||||
|
if !(atomType == Atoms.stco || atomType == Atoms.co64) {
|
||||||
|
moovAtom.offset += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
atomSize = try Int(moovAtom.getInteger(atomHead) as UInt32)
|
||||||
|
if atomSize > moovAtom.bytesAvailable {
|
||||||
|
Logger.debug("aborting due to a bad size on an atom", category: .generic)
|
||||||
|
throw Mp4RestructureError.unableToRestructureData
|
||||||
|
}
|
||||||
|
// we need to skip the offset by `12` which come from the bytes of [size/4][type/4][version/1][flags/3]
|
||||||
|
// more info https://developer.apple.com/documentation/quicktime-file-format/chunk_offset_atom
|
||||||
|
moovAtom.offset = atomHead + 12
|
||||||
|
if moovAtom.bytesAvailable < 4 {
|
||||||
|
Logger.debug("aborting due to a malformed atom", category: .generic)
|
||||||
|
throw Mp4RestructureError.unableToRestructureData
|
||||||
|
}
|
||||||
|
|
||||||
|
// the next integer determines the `Number of entries`
|
||||||
|
// https://developer.apple.com/documentation/quicktime-file-format/chunk_offset_atom/number_of_entries
|
||||||
|
let numberOfOffsetEntries = try Int(moovAtom.getInteger() as UInt32)
|
||||||
|
if atomType == Atoms.stco {
|
||||||
|
Logger.debug("🏗️ patching stco atom...", category: .generic)
|
||||||
|
if moovAtom.bytesAvailable < numberOfOffsetEntries * 4 {
|
||||||
|
Logger.debug("aborting due to bad atom..", category: .generic)
|
||||||
|
throw Mp4RestructureError.unableToRestructureData
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in 0 ..< numberOfOffsetEntries {
|
||||||
|
let currentOffset = try Int(moovAtom.getInteger(moovAtom.offset) as UInt32)
|
||||||
|
// adjust the offset by adding the size of moov atom
|
||||||
|
let adjustOffset = currentOffset + moovAtomSize
|
||||||
|
|
||||||
|
if currentOffset < 0, adjustOffset >= 0 {
|
||||||
|
throw Mp4RestructureError.unableToRestructureData
|
||||||
|
}
|
||||||
|
moovAtom.put(UInt32(adjustOffset).bigEndian)
|
||||||
|
}
|
||||||
|
} else if atomType == Atoms.co64 {
|
||||||
|
Logger.debug("🏗️ patching co64 atom...", category: .generic)
|
||||||
|
if moovAtom.bytesAvailable < numberOfOffsetEntries * 8 {
|
||||||
|
Logger.debug("aborting due to bad atom..", category: .generic)
|
||||||
|
throw Mp4RestructureError.unableToRestructureData
|
||||||
|
}
|
||||||
|
for _ in 0 ..< numberOfOffsetEntries {
|
||||||
|
let currentOffset: Int = try moovAtom.getInteger(moovAtom.offset)
|
||||||
|
// adjust the offset by adding the size of moov atom
|
||||||
|
moovAtom.put(currentOffset + moovAtomSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (moovAtom.storage, moovAtomSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInteger<T: FixedWidthInteger>(data: Data, offset: Int) throws -> T {
|
||||||
|
let sizeOfInteger = MemoryLayout<T>.size
|
||||||
|
guard sizeOfInteger <= data.count else {
|
||||||
|
throw ByteBuffer.Error.eof
|
||||||
|
}
|
||||||
|
let _offset = offset + sizeOfInteger
|
||||||
|
return T(data: data[_offset - sizeOfInteger ..< _offset]).bigEndian
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
//
|
||||||
|
// Created by Dimitrios Chatzieleftheriou on 10/03/2024.
|
||||||
|
// Copyright © 2020 Decimal. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class RemoteMp4Restructure {
|
||||||
|
struct RestructuredData {
|
||||||
|
var initialData: Data
|
||||||
|
var mdatOffset: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
private var audioData: Data
|
||||||
|
|
||||||
|
private var atomOffset: Int = 0
|
||||||
|
private var atoms: [MP4Atom] = []
|
||||||
|
private var ftyp: MP4Atom?
|
||||||
|
private var foundMoov = false
|
||||||
|
private var foundMdat = false
|
||||||
|
|
||||||
|
private var task: NetworkDataStream?
|
||||||
|
|
||||||
|
private(set) var dataOptimized: Bool = false
|
||||||
|
|
||||||
|
private var moovAtomSize: Int = 0
|
||||||
|
|
||||||
|
private let url: URL
|
||||||
|
private let networking: NetworkingClient
|
||||||
|
|
||||||
|
private let mp4Restructure: Mp4Restructure
|
||||||
|
|
||||||
|
init(url: URL, networking: NetworkingClient, restructure: Mp4Restructure = Mp4Restructure()) {
|
||||||
|
self.url = url
|
||||||
|
self.networking = networking
|
||||||
|
self.audioData = Data()
|
||||||
|
self.mp4Restructure = restructure
|
||||||
|
}
|
||||||
|
|
||||||
|
func clear() {
|
||||||
|
mp4Restructure.clear()
|
||||||
|
audioData = Data()
|
||||||
|
task?.cancel()
|
||||||
|
task = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adjust the seekOffset of subtracting the moovAtomSize
|
||||||
|
/// - Parameter offset: A byte offset
|
||||||
|
/// - Returns: An adjusted byte offset
|
||||||
|
func seekAdjusted(offset: Int) -> Int {
|
||||||
|
mp4Restructure.seekAdjusted(offset: offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Gather audio and parse along the way, if moov atom is found, continue as usual
|
||||||
|
/// if mdat is found before moov:
|
||||||
|
/// - Get mdat size and make a byte request Range: bytes=mdatAtomSize- for possible moov atom
|
||||||
|
/// - once the request is complete search for an moov atom and restructure it
|
||||||
|
/// - finally, make a byte request Range: bytes=mdatOffset- to get the mdat
|
||||||
|
/// Atoms needs to be as following for the AudioFileStreamParse to work
|
||||||
|
/// [ftyp][moov][mdat]
|
||||||
|
///
|
||||||
|
func optimizeIfNeeded(completion: @escaping (Result<RestructuredData?, Error>) -> Void) {
|
||||||
|
task = networking.stream(request: urlForPartialContent(with: url, offset: 0))
|
||||||
|
.responseStream { [weak self] event in
|
||||||
|
guard let self else { return }
|
||||||
|
switch event {
|
||||||
|
case .response:
|
||||||
|
break
|
||||||
|
case let .stream(.success(response)):
|
||||||
|
guard let data = response.data else {
|
||||||
|
self.audioData = Data()
|
||||||
|
completion(.failure(Mp4RestructureError.unableToRestructureData))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.audioData.append(data)
|
||||||
|
do {
|
||||||
|
let value = try self.mp4Restructure.checkIsOptimized(data: self.audioData)
|
||||||
|
if let value {
|
||||||
|
guard response.response?.statusCode == 206 else {
|
||||||
|
Logger.error("⛔️ mp4 error: no moov before mdat and the stream is not seekable", category: .networking)
|
||||||
|
completion(.failure(Mp4RestructureError.nonOptimizedMp4AndServerCannotSeek))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// stop request, fetch moov and restructure
|
||||||
|
self.audioData = Data()
|
||||||
|
self.task?.cancel()
|
||||||
|
self.task = nil
|
||||||
|
self.fetchAndRestructureMoovAtom(offset: value.moovOffset) { result in
|
||||||
|
switch result {
|
||||||
|
case let .success(value):
|
||||||
|
let data = value.data
|
||||||
|
let offset = value.offset
|
||||||
|
self.dataOptimized = true
|
||||||
|
completion(.success(RestructuredData(initialData: data, mdatOffset: offset)))
|
||||||
|
case let .failure(error):
|
||||||
|
completion(.failure(Mp4RestructureError.networkError(error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.audioData = Data()
|
||||||
|
self.task?.cancel()
|
||||||
|
self.task = nil
|
||||||
|
completion(.success(nil))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
completion(.failure(Mp4RestructureError.invalidAtomSize))
|
||||||
|
}
|
||||||
|
case let .stream(.failure(error)):
|
||||||
|
completion(.failure(Mp4RestructureError.networkError(error)))
|
||||||
|
case .complete:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
task?.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchAndRestructureMoovAtom(offset: Int, completion: @escaping (Result<(data: Data, offset: Int), Error>) -> Void) {
|
||||||
|
networking.task(request: urlForPartialContent(with: url, offset: offset)) { [weak self] result in
|
||||||
|
guard let self else { return }
|
||||||
|
switch result {
|
||||||
|
case let .success(data):
|
||||||
|
do {
|
||||||
|
let (initialData, mdatOffset) = try self.mp4Restructure.restructureMoov(data: data)
|
||||||
|
completion(.success((initialData, mdatOffset)))
|
||||||
|
} catch {
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
case let .failure(failure):
|
||||||
|
completion(.failure(Mp4RestructureError.networkError(failure)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func urlForPartialContent(with url: URL, offset: Int) -> URLRequest {
|
||||||
|
var urlRequest = URLRequest(url: url)
|
||||||
|
urlRequest.networkServiceType = .avStreaming
|
||||||
|
urlRequest.cachePolicy = .reloadIgnoringLocalCacheData
|
||||||
|
urlRequest.timeoutInterval = 60
|
||||||
|
|
||||||
|
urlRequest.addValue("*/*", forHTTPHeaderField: "Accept")
|
||||||
|
urlRequest.addValue("identity", forHTTPHeaderField: "Accept-Encoding")
|
||||||
|
urlRequest.addValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
|
||||||
|
return urlRequest
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,10 @@ import AVFoundation
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Network
|
import Network
|
||||||
|
|
||||||
|
enum RemoteAudioSourceError: Error {
|
||||||
|
case mp4NotSeekable
|
||||||
|
}
|
||||||
|
|
||||||
public class RemoteAudioSource: AudioStreamSource {
|
public class RemoteAudioSource: AudioStreamSource {
|
||||||
weak var delegate: AudioStreamSourceDelegate?
|
weak var delegate: AudioStreamSourceDelegate?
|
||||||
|
|
||||||
@@ -31,23 +35,25 @@ public class RemoteAudioSource: AudioStreamSource {
|
|||||||
private var seekOffset: Int
|
private var seekOffset: Int
|
||||||
private var supportsSeek: Bool
|
private var supportsSeek: Bool
|
||||||
|
|
||||||
internal var metadataStreamProcessor: MetadataStreamSource
|
var metadataStreamProcessor: MetadataStreamSource
|
||||||
|
|
||||||
private var shouldTryParsingIcycastHeaders: Bool = false
|
private var shouldTryParsingIcycastHeaders: Bool = false
|
||||||
private let icycastHeadersProcessor: IcycastHeadersProcessor
|
private let icycastHeadersProcessor: IcycastHeadersProcessor
|
||||||
|
|
||||||
internal var audioFileHint: AudioFileTypeID {
|
var audioFileHint: AudioFileTypeID {
|
||||||
guard let output = parsedHeaderOutput, output.typeId != 0 else {
|
guard let output = parsedHeaderOutput, output.typeId != 0 else {
|
||||||
return audioFileType(fileExtension: url.pathExtension)
|
return audioFileType(fileExtension: url.pathExtension)
|
||||||
}
|
}
|
||||||
return output.typeId
|
return output.typeId
|
||||||
}
|
}
|
||||||
|
|
||||||
internal let underlyingQueue: DispatchQueue
|
private let mp4Restructure: RemoteMp4Restructure
|
||||||
internal let streamOperationQueue: OperationQueue
|
|
||||||
internal let netStatusService: NetStatusProvider
|
let underlyingQueue: DispatchQueue
|
||||||
internal var waitingForNetwork = false
|
let streamOperationQueue: OperationQueue
|
||||||
internal let retrierTimeout: Retrier
|
let netStatusService: NetStatusProvider
|
||||||
|
var waitingForNetwork = false
|
||||||
|
let retrierTimeout: Retrier
|
||||||
|
|
||||||
init(networking: NetworkingClient,
|
init(networking: NetworkingClient,
|
||||||
metadataStreamSource: MetadataStreamSource,
|
metadataStreamSource: MetadataStreamSource,
|
||||||
@@ -74,6 +80,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
|||||||
streamOperationQueue.isSuspended = true
|
streamOperationQueue.isSuspended = true
|
||||||
streamOperationQueue.name = "remote.audio.source.data.stream.queue"
|
streamOperationQueue.name = "remote.audio.source.data.stream.queue"
|
||||||
retrierTimeout = retrier
|
retrierTimeout = retrier
|
||||||
|
mp4Restructure = RemoteMp4Restructure(url: url, networking: networkingClient)
|
||||||
startNetworkService()
|
startNetworkService()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,12 +93,12 @@ public class RemoteAudioSource: AudioStreamSource {
|
|||||||
let metadataProcessor = MetadataStreamProcessor(parser: metadataParser.eraseToAnyParser())
|
let metadataProcessor = MetadataStreamProcessor(parser: metadataParser.eraseToAnyParser())
|
||||||
let netStatusProvider = NetStatusService(network: NWPathMonitor())
|
let netStatusProvider = NetStatusService(network: NWPathMonitor())
|
||||||
let icyheaderProcessor = IcycastHeadersProcessor()
|
let icyheaderProcessor = IcycastHeadersProcessor()
|
||||||
let retrierTimout = Retrier(interval: .seconds(1), maxInterval: 5, underlyingQueue: nil)
|
let retrierTimeout = Retrier(interval: .seconds(1), maxInterval: 5, underlyingQueue: nil)
|
||||||
self.init(networking: networking,
|
self.init(networking: networking,
|
||||||
metadataStreamSource: metadataProcessor,
|
metadataStreamSource: metadataProcessor,
|
||||||
icycastHeadersProcessor: icyheaderProcessor,
|
icycastHeadersProcessor: icyheaderProcessor,
|
||||||
netStatusProvider: netStatusProvider,
|
netStatusProvider: netStatusProvider,
|
||||||
retrier: retrierTimout,
|
retrier: retrierTimeout,
|
||||||
url: url,
|
url: url,
|
||||||
underlyingQueue: underlyingQueue,
|
underlyingQueue: underlyingQueue,
|
||||||
httpHeaders: httpHeaders)
|
httpHeaders: httpHeaders)
|
||||||
@@ -109,7 +116,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
|||||||
|
|
||||||
func close() {
|
func close() {
|
||||||
retrierTimeout.cancel()
|
retrierTimeout.cancel()
|
||||||
netStatusService.stop()
|
streamOperationQueue.isSuspended = false
|
||||||
streamOperationQueue.cancelAllOperations()
|
streamOperationQueue.cancelAllOperations()
|
||||||
if let streamTask = streamRequest {
|
if let streamTask = streamRequest {
|
||||||
streamTask.cancel()
|
streamTask.cancel()
|
||||||
@@ -128,6 +135,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mp4Restructure.clear()
|
||||||
retrierTimeout.cancel()
|
retrierTimeout.cancel()
|
||||||
metadataStreamProcessor.reset()
|
metadataStreamProcessor.reset()
|
||||||
icycastHeadersProcessor.reset()
|
icycastHeadersProcessor.reset()
|
||||||
@@ -137,12 +145,10 @@ public class RemoteAudioSource: AudioStreamSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func suspend() {
|
func suspend() {
|
||||||
streamRequest?.suspend()
|
|
||||||
streamOperationQueue.isSuspended = true
|
streamOperationQueue.isSuspended = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func resume() {
|
func resume() {
|
||||||
streamRequest?.resume()
|
|
||||||
streamOperationQueue.isSuspended = false
|
streamOperationQueue.isSuspended = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,26 +159,79 @@ public class RemoteAudioSource: AudioStreamSource {
|
|||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard connection.isConnected else { return }
|
guard connection.isConnected else { return }
|
||||||
if self.waitingForNetwork {
|
if self.waitingForNetwork {
|
||||||
|
self.seek(at: self.supportsSeek ? self.position : 0)
|
||||||
self.waitingForNetwork = false
|
self.waitingForNetwork = false
|
||||||
self.seek(at: self.position)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func performOpen(seek seekOffset: Int) {
|
private func performOpen(seek seekOffset: Int) {
|
||||||
let urlRequest = buildUrlRequest(with: url, seekIfNeeded: seekOffset)
|
if seekOffset == 0 {
|
||||||
|
initialRequest { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
if self.parsedHeaderOutput?.isMp4 == true {
|
||||||
|
self.handleMp4Files()
|
||||||
|
} else {
|
||||||
|
self.doPerfomOpen(seek: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if mp4Restructure.dataOptimized {
|
||||||
|
let adjustedOffset = mp4Restructure.seekAdjusted(offset: seekOffset)
|
||||||
|
doPerfomOpen(seek: adjustedOffset)
|
||||||
|
} else {
|
||||||
|
doPerfomOpen(seek: seekOffset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let request = networkingClient.stream(request: urlRequest)
|
private func doPerfomOpen(seek seekOffset: Int) {
|
||||||
|
let urlRequest = buildUrlRequest(with: url, seekIfNeeded: seekOffset)
|
||||||
|
streamRequest = networkingClient.stream(request: urlRequest)
|
||||||
.responseStream { [weak self] event in
|
.responseStream { [weak self] event in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.handleResponse(event: event)
|
self.handleResponse(event: event)
|
||||||
}
|
}
|
||||||
.resume()
|
.resume()
|
||||||
|
|
||||||
streamRequest = request
|
|
||||||
metadataStreamProcessor.delegate = self
|
metadataStreamProcessor.delegate = self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func initialRequest(completion: @escaping () -> Void) {
|
||||||
|
let urlRequest = fetchUrlForPartialContent(with: url)
|
||||||
|
let task: NetworkDataStream = networkingClient.stream(request: urlRequest)
|
||||||
|
task.responseStream { [weak self] event in
|
||||||
|
switch event {
|
||||||
|
case let .response(urlResponse):
|
||||||
|
self?.parseResponseHeader(response: urlResponse)
|
||||||
|
task.cancel()
|
||||||
|
completion()
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleMp4Files() {
|
||||||
|
mp4Restructure.optimizeIfNeeded { [weak self] result in
|
||||||
|
guard let self else { return }
|
||||||
|
switch result {
|
||||||
|
case let .success(value):
|
||||||
|
if let value {
|
||||||
|
self.addStreamOperation {
|
||||||
|
let audioCount = self.processAudio(data: value.initialData)
|
||||||
|
self.relativePosition += audioCount
|
||||||
|
}
|
||||||
|
self.doPerfomOpen(seek: value.mdatOffset)
|
||||||
|
} else {
|
||||||
|
self.doPerfomOpen(seek: 0)
|
||||||
|
}
|
||||||
|
case let .failure(failure):
|
||||||
|
self.delegate?.errorOccurred(source: self, error: failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Network Handle Methods
|
// MARK: - Network Handle Methods
|
||||||
|
|
||||||
private func handleResponse(event: NetworkDataStream.ResponseEvent) {
|
private func handleResponse(event: NetworkDataStream.ResponseEvent) {
|
||||||
@@ -180,65 +239,67 @@ public class RemoteAudioSource: AudioStreamSource {
|
|||||||
case let .response(urlResponse):
|
case let .response(urlResponse):
|
||||||
parseResponseHeader(response: urlResponse)
|
parseResponseHeader(response: urlResponse)
|
||||||
streamOperationQueue.isSuspended = false
|
streamOperationQueue.isSuspended = false
|
||||||
case let .stream(event):
|
case let .stream(.success(response)):
|
||||||
handleStreamEvent(event: event)
|
handleSuccessfulStreamEvent(response: response)
|
||||||
|
case let .stream(.failure(error)):
|
||||||
|
handleFailedStreamEvent(error: error)
|
||||||
case let .complete(event):
|
case let .complete(event):
|
||||||
if let error = event.error {
|
if let error = event.error {
|
||||||
delegate?.errorOccured(source: self, error: error)
|
delegate?.errorOccurred(source: self, error: error)
|
||||||
} else {
|
} else {
|
||||||
addCompletionOperation { [weak self] in
|
addCompletionOperation { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.delegate?.endOfFileOccured(source: self)
|
self.delegate?.endOfFileOccurred(source: self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleStreamEvent(event: NetworkDataStream.StreamResult) {
|
private func handleSuccessfulStreamEvent(response: NetworkDataStream.Response) {
|
||||||
switch event {
|
guard let audioData = response.data else {
|
||||||
case let .success(value):
|
delegate?.errorOccurred(source: self, error: NetworkError.missingData)
|
||||||
if let audioData = value.data {
|
return
|
||||||
addStreamOperation { [weak self] in
|
}
|
||||||
guard let self = self else { return }
|
addStreamOperation { [weak self] in
|
||||||
if self.shouldTryParsingIcycastHeaders {
|
guard let self = self else { return }
|
||||||
let (header, extractedAudio) = self.icycastHeadersProcessor.proccess(data: audioData)
|
if self.shouldTryParsingIcycastHeaders {
|
||||||
if let header = header {
|
let (header, extractedAudio) = self.icycastHeadersProcessor.process(data: audioData)
|
||||||
self.shouldTryParsingIcycastHeaders = false
|
if let header = header {
|
||||||
let parser = IcycastHeaderParser()
|
self.shouldTryParsingIcycastHeaders = false
|
||||||
self.parsedHeaderOutput = parser.parse(input: header)
|
let parser = IcycastHeaderParser()
|
||||||
if let metadataStep = self.parsedHeaderOutput?.metadataStep {
|
self.parsedHeaderOutput = parser.parse(input: header)
|
||||||
self.metadataStreamProcessor.metadataAvailable(step: metadataStep)
|
if let metadataStep = self.parsedHeaderOutput?.metadataStep {
|
||||||
}
|
self.metadataStreamProcessor.metadataAvailable(step: metadataStep)
|
||||||
|
|
||||||
let audioCount = self.processAudio(data: extractedAudio)
|
|
||||||
self.relativePosition += audioCount
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let audioCount = self.processAudio(data: audioData)
|
|
||||||
self.relativePosition += audioCount
|
|
||||||
}
|
}
|
||||||
}
|
let audioCount = self.processAudio(data: extractedAudio)
|
||||||
case .failure:
|
self.relativePosition += audioCount
|
||||||
if !netStatusService.isConnected {
|
|
||||||
waitingForNetwork = true
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
waitingForNetwork = false
|
let audioCount = self.processAudio(data: audioData)
|
||||||
retryOnError()
|
self.relativePosition += audioCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handleFailedStreamEvent(error _: Error) {
|
||||||
|
if !netStatusService.isConnected {
|
||||||
|
waitingForNetwork = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
waitingForNetwork = false
|
||||||
|
retryOnError()
|
||||||
|
}
|
||||||
|
|
||||||
/// Processing audio data, extracting metadata if needed.
|
/// Processing audio data, extracting metadata if needed.
|
||||||
/// - Parameter data: The audio to be processed
|
/// - Parameter data: The audio to be processed
|
||||||
/// - Returns: An `Int` value representing the amount of audio data bytes.
|
/// - Returns: An `Int` value representing the amount of audio data bytes.
|
||||||
private func processAudio(data: Data) -> Int {
|
private func processAudio(data: Data) -> Int {
|
||||||
if self.metadataStreamProcessor.canProccessMetadata {
|
if metadataStreamProcessor.canProcessMetadata {
|
||||||
let extractedAudioData = self.metadataStreamProcessor.proccessMetadata(data: data)
|
let extractedAudioData = metadataStreamProcessor.processMetadata(data: data)
|
||||||
self.delegate?.dataAvailable(source: self, data: extractedAudioData)
|
delegate?.dataAvailable(source: self, data: extractedAudioData)
|
||||||
return extractedAudioData.count
|
return extractedAudioData.count
|
||||||
} else {
|
} else {
|
||||||
self.delegate?.dataAvailable(source: self, data: data)
|
delegate?.dataAvailable(source: self, data: data)
|
||||||
return data.count
|
return data.count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,11 +316,13 @@ public class RemoteAudioSource: AudioStreamSource {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let acceptRanges = parser.value(forHTTPHeaderField: HeaderField.acceptRanges, in: response) {
|
if httpStatusCode == 206 {
|
||||||
|
supportsSeek = true
|
||||||
|
} else if let acceptRanges = parser.value(forHTTPHeaderField: HeaderField.acceptRanges, in: response) {
|
||||||
supportsSeek = acceptRanges != "none"
|
supportsSeek = acceptRanges != "none"
|
||||||
}
|
}
|
||||||
|
|
||||||
// check to see if we have metadata to proccess
|
// check to see if we have metadata to process
|
||||||
if let metadataStep = parsedHeaderOutput?.metadataStep {
|
if let metadataStep = parsedHeaderOutput?.metadataStep {
|
||||||
metadataStreamProcessor.metadataAvailable(step: metadataStep)
|
metadataStreamProcessor.metadataAvailable(step: metadataStep)
|
||||||
}
|
}
|
||||||
@@ -270,9 +333,12 @@ public class RemoteAudioSource: AudioStreamSource {
|
|||||||
// check for error
|
// check for error
|
||||||
if statusCode == 416 { // range not satisfied error
|
if statusCode == 416 { // range not satisfied error
|
||||||
if length >= 0 { seekOffset = length }
|
if length >= 0 { seekOffset = length }
|
||||||
delegate?.endOfFileOccured(source: self)
|
delegate?.endOfFileOccurred(source: self)
|
||||||
} else if statusCode >= 300 {
|
} else if statusCode >= 300 {
|
||||||
delegate?.errorOccured(source: self, error: NetworkError.serverError)
|
delegate?.errorOccurred(
|
||||||
|
source: self,
|
||||||
|
error: NetworkError.serverError
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,16 +355,32 @@ public class RemoteAudioSource: AudioStreamSource {
|
|||||||
urlRequest.addValue("1", forHTTPHeaderField: "Icy-MetaData")
|
urlRequest.addValue("1", forHTTPHeaderField: "Icy-MetaData")
|
||||||
urlRequest.addValue("identity", forHTTPHeaderField: "Accept-Encoding")
|
urlRequest.addValue("identity", forHTTPHeaderField: "Accept-Encoding")
|
||||||
|
|
||||||
if supportsSeek && seekOffset > 0 {
|
if supportsSeek, seekOffset > 0 {
|
||||||
urlRequest.addValue("bytes=\(seekOffset)-", forHTTPHeaderField: "Range")
|
urlRequest.addValue("bytes=\(seekOffset)-", forHTTPHeaderField: "Range")
|
||||||
}
|
}
|
||||||
return urlRequest
|
return urlRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func fetchUrlForPartialContent(with url: URL) -> URLRequest {
|
||||||
|
var urlRequest = URLRequest(url: url)
|
||||||
|
urlRequest.networkServiceType = .avStreaming
|
||||||
|
urlRequest.cachePolicy = .reloadIgnoringLocalCacheData
|
||||||
|
urlRequest.timeoutInterval = 60
|
||||||
|
|
||||||
|
for header in additionalRequestHeaders {
|
||||||
|
urlRequest.addValue(header.value, forHTTPHeaderField: header.key)
|
||||||
|
}
|
||||||
|
urlRequest.addValue("*/*", forHTTPHeaderField: "Accept")
|
||||||
|
urlRequest.addValue("1", forHTTPHeaderField: "Icy-MetaData")
|
||||||
|
urlRequest.addValue("identity", forHTTPHeaderField: "Accept-Encoding")
|
||||||
|
urlRequest.addValue("bytes=0-1", forHTTPHeaderField: "Range")
|
||||||
|
return urlRequest
|
||||||
|
}
|
||||||
|
|
||||||
private func retryOnError() {
|
private func retryOnError() {
|
||||||
retrierTimeout.retry { [weak self] in
|
retrierTimeout.retry { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.seek(at: self.position)
|
self.seek(at: self.supportsSeek ? self.position : 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,6 +391,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
|||||||
/// - Parameter block: A closure to be executed
|
/// - Parameter block: A closure to be executed
|
||||||
private func addStreamOperation(_ block: @escaping () -> Void) {
|
private func addStreamOperation(_ block: @escaping () -> Void) {
|
||||||
let operation = BlockOperation(block: block)
|
let operation = BlockOperation(block: block)
|
||||||
|
operation.qualityOfService = .userInitiated
|
||||||
streamOperationQueue.addOperation(operation)
|
streamOperationQueue.addOperation(operation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,15 +100,13 @@ open class AudioPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// An `AVAudioFormat` object for the canonical audio stream
|
/// An `AVAudioFormat` object for the canonical audio stream
|
||||||
private var outputAudioFormat: AVAudioFormat = {
|
private var outputAudioFormat: AVAudioFormat = .init(commonFormat: .pcmFormatFloat32, sampleRate: 44100.0, channels: 2, interleaved: true)!
|
||||||
AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100.0, channels: 2, interleaved: true)!
|
|
||||||
}()
|
|
||||||
|
|
||||||
/// Keeps track of the player's state before being paused.
|
/// Keeps track of the player's state before being paused.
|
||||||
private var stateBeforePaused: InternalState = .initial
|
private var stateBeforePaused: InternalState = .initial
|
||||||
|
|
||||||
/// The underlying `AVAudioEngine` object
|
/// The underlying `AVAudioEngine` object
|
||||||
private let audioEngine = AVAudioEngine()
|
private let audioEngine: AVAudioEngine
|
||||||
/// An `AVAudioUnit` object that represents the audio player
|
/// An `AVAudioUnit` object that represents the audio player
|
||||||
private(set) var player = AVAudioUnit()
|
private(set) var player = AVAudioUnit()
|
||||||
/// An `AVAudioUnitTimePitch` that controls the playback rate of the audio engine
|
/// An `AVAudioUnitTimePitch` that controls the playback rate of the audio engine
|
||||||
@@ -125,7 +123,6 @@ open class AudioPlayer {
|
|||||||
private let playerRenderProcessor: AudioPlayerRenderProcessor
|
private let playerRenderProcessor: AudioPlayerRenderProcessor
|
||||||
private let frameFilterProcessor: FrameFilterProcessor
|
private let frameFilterProcessor: FrameFilterProcessor
|
||||||
|
|
||||||
private let audioReadSource: DispatchTimerSource
|
|
||||||
private let serializationQueue: DispatchQueue
|
private let serializationQueue: DispatchQueue
|
||||||
private let sourceQueue: DispatchQueue
|
private let sourceQueue: DispatchQueue
|
||||||
|
|
||||||
@@ -135,29 +132,38 @@ open class AudioPlayer {
|
|||||||
|
|
||||||
public init(configuration: AudioPlayerConfiguration = .default) {
|
public init(configuration: AudioPlayerConfiguration = .default) {
|
||||||
self.configuration = configuration.normalizeValues()
|
self.configuration = configuration.normalizeValues()
|
||||||
|
let engine = AVAudioEngine()
|
||||||
|
audioEngine = engine
|
||||||
rendererContext = AudioRendererContext(configuration: configuration, outputAudioFormat: outputAudioFormat)
|
rendererContext = AudioRendererContext(configuration: configuration, outputAudioFormat: outputAudioFormat)
|
||||||
playerContext = AudioPlayerContext()
|
playerContext = AudioPlayerContext()
|
||||||
entriesQueue = PlayerQueueEntries()
|
entriesQueue = PlayerQueueEntries()
|
||||||
|
|
||||||
serializationQueue = DispatchQueue(label: "streaming.core.queue", qos: .userInitiated)
|
serializationQueue = DispatchQueue(label: "streaming.core.queue", qos: .userInitiated)
|
||||||
sourceQueue = DispatchQueue(label: "source.queue", qos: .userInitiated)
|
sourceQueue = DispatchQueue(label: "source.queue", qos: .default)
|
||||||
audioReadSource = DispatchTimerSource(interval: .milliseconds(200), queue: sourceQueue)
|
|
||||||
|
|
||||||
entryProvider = AudioEntryProvider(networkingClient: NetworkingClient(),
|
entryProvider = AudioEntryProvider(
|
||||||
underlyingQueue: sourceQueue,
|
networkingClient: NetworkingClient(),
|
||||||
outputAudioFormat: outputAudioFormat)
|
underlyingQueue: sourceQueue,
|
||||||
|
outputAudioFormat: outputAudioFormat
|
||||||
|
)
|
||||||
|
|
||||||
fileStreamProcessor = AudioFileStreamProcessor(playerContext: playerContext,
|
fileStreamProcessor = AudioFileStreamProcessor(
|
||||||
rendererContext: rendererContext,
|
playerContext: playerContext,
|
||||||
outputAudioFormat: outputAudioFormat.basicStreamDescription)
|
rendererContext: rendererContext,
|
||||||
|
outputAudioFormat: outputAudioFormat.basicStreamDescription
|
||||||
|
)
|
||||||
|
|
||||||
frameFilterProcessor = FrameFilterProcessor(mixerNode: audioEngine.mainMixerNode)
|
playerRenderProcessor = AudioPlayerRenderProcessor(
|
||||||
|
playerContext: playerContext,
|
||||||
playerRenderProcessor = AudioPlayerRenderProcessor(playerContext: playerContext,
|
rendererContext: rendererContext,
|
||||||
rendererContext: rendererContext,
|
outputAudioFormat: outputAudioFormat.basicStreamDescription
|
||||||
outputAudioFormat: outputAudioFormat.basicStreamDescription)
|
)
|
||||||
|
|
||||||
|
frameFilterProcessor = FrameFilterProcessor(
|
||||||
|
mixerNodeProvider: {
|
||||||
|
engine.mainMixerNode
|
||||||
|
}
|
||||||
|
)
|
||||||
configPlayerContext()
|
configPlayerContext()
|
||||||
configPlayerNode()
|
configPlayerNode()
|
||||||
setupEngine()
|
setupEngine()
|
||||||
@@ -166,7 +172,6 @@ open class AudioPlayer {
|
|||||||
deinit {
|
deinit {
|
||||||
playerContext.audioPlayingEntry?.close()
|
playerContext.audioPlayingEntry?.close()
|
||||||
clearQueue()
|
clearQueue()
|
||||||
stopReadProccessFromSource()
|
|
||||||
rendererContext.clean()
|
rendererContext.clean()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,14 +200,32 @@ open class AudioPlayer {
|
|||||||
do {
|
do {
|
||||||
try self.startEngineIfNeeded()
|
try self.startEngineIfNeeded()
|
||||||
} catch {
|
} catch {
|
||||||
self.raiseUnxpected(error: .audioSystemError(.engineFailure))
|
self.raiseUnexpected(error: .audioSystemError(.engineFailure))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceQueue.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.processSource()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func playNextInQueue() {
|
||||||
|
checkRenderWaitingAndNotifyIfNeeded()
|
||||||
|
serializationQueue.sync {
|
||||||
|
if entriesQueue.count(for: .upcoming) > 0 {
|
||||||
|
playerContext.setInternalState(to: .pendingNext)
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
try self.startEngineIfNeeded()
|
||||||
|
} catch {
|
||||||
|
self.raiseUnexpected(error: .audioSystemError(.engineFailure))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceQueue.async { [weak self] in
|
sourceQueue.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.processSource()
|
self.processSource()
|
||||||
self.startReadProcessFromSourceIfNeeded()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,15 +243,41 @@ open class AudioPlayer {
|
|||||||
queue(urls: urls, headers: [:])
|
queue(urls: urls, headers: [:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func queue(url: URL, after afterUrl: URL) {
|
||||||
|
queue(url: url, headers: [:], after: afterUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func removeFromQueue(url: URL) {
|
||||||
|
serializationQueue.sync {
|
||||||
|
if let item = entriesQueue.items(type: .upcoming).first(where: { $0.id.id == url.absoluteString }) {
|
||||||
|
entriesQueue.remove(item: item, type: .upcoming)
|
||||||
|
|
||||||
|
if playerContext.audioPlayingEntry?.id.id == item.id.id {
|
||||||
|
stop(clearQueue: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkRenderWaitingAndNotifyIfNeeded()
|
||||||
|
sourceQueue.async { [weak self] in
|
||||||
|
self?.processSource()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Queues the specified URL
|
/// Queues the specified URL
|
||||||
///
|
///
|
||||||
/// - Parameter url: A `URL` specifying the audio content to be played.
|
/// - Parameter url: A `URL` specifying the audio content to be played.
|
||||||
/// - parameter headers: A `Dictionary` specifying any additional headers to be pass to the network request.
|
/// - parameter headers: A `Dictionary` specifying any additional headers to be pass to the network request.
|
||||||
public func queue(url: URL, headers: [String: String]) {
|
public func queue(url: URL, headers: [String: String], after afterUrl: URL? = nil) {
|
||||||
serializationQueue.sync {
|
serializationQueue.sync {
|
||||||
let audioEntry = entryProvider.provideAudioEntry(url: url, headers: headers)
|
let audioEntry = entryProvider.provideAudioEntry(url: url, headers: headers)
|
||||||
audioEntry.delegate = self
|
audioEntry.delegate = self
|
||||||
entriesQueue.enqueue(item: audioEntry, type: .upcoming)
|
if let afterUrl = afterUrl {
|
||||||
|
if let afterUrlEntry = entriesQueue.items(type: .upcoming).first(where: { $0.id.id == afterUrl.absoluteString }) {
|
||||||
|
entriesQueue.insert(item: audioEntry, type: .upcoming, after: afterUrlEntry)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entriesQueue.enqueue(item: audioEntry, type: .upcoming)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
checkRenderWaitingAndNotifyIfNeeded()
|
checkRenderWaitingAndNotifyIfNeeded()
|
||||||
sourceQueue.async { [weak self] in
|
sourceQueue.async { [weak self] in
|
||||||
@@ -255,10 +304,9 @@ open class AudioPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Stops the audio playback
|
/// Stops the audio playback
|
||||||
public func stop() {
|
public func stop(clearQueue: Bool = true) {
|
||||||
guard playerContext.internalState != .stopped else { return }
|
guard playerContext.internalState != .stopped else { return }
|
||||||
|
|
||||||
stopReadProccessFromSource()
|
|
||||||
serializationQueue.sync {
|
serializationQueue.sync {
|
||||||
stopEngine(reason: .userAction)
|
stopEngine(reason: .userAction)
|
||||||
}
|
}
|
||||||
@@ -271,7 +319,9 @@ open class AudioPlayer {
|
|||||||
self.processFinishPlaying(entry: playingEntry, with: nil)
|
self.processFinishPlaying(entry: playingEntry, with: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.clearQueue()
|
if clearQueue {
|
||||||
|
self.clearQueue()
|
||||||
|
}
|
||||||
self.playerContext.entriesLock.lock()
|
self.playerContext.entriesLock.lock()
|
||||||
self.playerContext.audioReadingEntry = nil
|
self.playerContext.audioReadingEntry = nil
|
||||||
self.playerContext.audioPlayingEntry = nil
|
self.playerContext.audioPlayingEntry = nil
|
||||||
@@ -289,7 +339,6 @@ open class AudioPlayer {
|
|||||||
serializationQueue.sync {
|
serializationQueue.sync {
|
||||||
pauseEngine()
|
pauseEngine()
|
||||||
}
|
}
|
||||||
stopReadProccessFromSource()
|
|
||||||
playerContext.audioPlayingEntry?.suspend()
|
playerContext.audioPlayingEntry?.suspend()
|
||||||
sourceQueue.async { [weak self] in
|
sourceQueue.async { [weak self] in
|
||||||
self?.processSource()
|
self?.processSource()
|
||||||
@@ -315,11 +364,10 @@ open class AudioPlayer {
|
|||||||
}
|
}
|
||||||
startPlayer(resetBuffers: false)
|
startPlayer(resetBuffers: false)
|
||||||
}
|
}
|
||||||
startReadProcessFromSourceIfNeeded()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Seeks the audio to the specified time.
|
/// Seeks the audio to the specified time.
|
||||||
/// - Parameter time: A `Double` value specifing the time of the requested seek in seconds
|
/// - Parameter time: A `Double` value specifying the time of the requested seek in seconds
|
||||||
public func seek(to time: Double) {
|
public func seek(to time: Double) {
|
||||||
guard let playingEntry = playerContext.audioPlayingEntry else {
|
guard let playingEntry = playerContext.audioPlayingEntry else {
|
||||||
return
|
return
|
||||||
@@ -353,7 +401,7 @@ open class AudioPlayer {
|
|||||||
/// - Note: The nodes will be added after the default rate node
|
/// - Note: The nodes will be added after the default rate node
|
||||||
/// - Parameter node: An array of `AVAudioNode` instances
|
/// - Parameter node: An array of `AVAudioNode` instances
|
||||||
public func attach(nodes: [AVAudioNode]) {
|
public func attach(nodes: [AVAudioNode]) {
|
||||||
nodes.forEach { node in
|
for node in nodes {
|
||||||
customAttachedNodes.append(node)
|
customAttachedNodes.append(node)
|
||||||
}
|
}
|
||||||
nodes.forEach(audioEngine.attach)
|
nodes.forEach(audioEngine.attach)
|
||||||
@@ -375,7 +423,7 @@ open class AudioPlayer {
|
|||||||
/// Detaches the given `AVAudioNode`s from the engine
|
/// Detaches the given `AVAudioNode`s from the engine
|
||||||
/// - Parameter node: An array of `AVAudioNode` instances
|
/// - Parameter node: An array of `AVAudioNode` instances
|
||||||
public func detachCustomAttachedNodes() {
|
public func detachCustomAttachedNodes() {
|
||||||
customAttachedNodes.forEach { node in
|
for node in customAttachedNodes {
|
||||||
audioEngine.detach(node)
|
audioEngine.detach(node)
|
||||||
}
|
}
|
||||||
attachAndConnectDefaultNodes()
|
attachAndConnectDefaultNodes()
|
||||||
@@ -409,7 +457,7 @@ open class AudioPlayer {
|
|||||||
audioEngine.prepare()
|
audioEngine.prepare()
|
||||||
try audioEngine.start()
|
try audioEngine.start()
|
||||||
} catch {
|
} catch {
|
||||||
Logger.error("⚠️ error setuping audio engine: %@", category: .generic, args: error.localizedDescription)
|
Logger.error("⚠️ error setting up audio engine: %@", category: .generic, args: error.localizedDescription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,7 +472,7 @@ open class AudioPlayer {
|
|||||||
self.playerRenderProcessor.attachCallback(on: unit, audioFormat: self.outputAudioFormat)
|
self.playerRenderProcessor.attachCallback(on: unit, audioFormat: self.outputAudioFormat)
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
assertionFailure("couldn't create player unit: \(error)")
|
assertionFailure("couldn't create player unit: \(error)")
|
||||||
self.raiseUnxpected(error: .audioSystemError(.playerNotFound))
|
self.raiseUnexpected(error: .audioSystemError(.playerNotFound))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -452,12 +500,12 @@ open class AudioPlayer {
|
|||||||
fileStreamProcessor.fileStreamCallback = { [weak self] effect in
|
fileStreamProcessor.fileStreamCallback = { [weak self] effect in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
switch effect {
|
switch effect {
|
||||||
case .proccessSource:
|
case .processSource:
|
||||||
self.sourceQueue.async {
|
self.sourceQueue.async {
|
||||||
self.processSource()
|
self.processSource()
|
||||||
}
|
}
|
||||||
case let .raiseError(error):
|
case let .raiseError(error):
|
||||||
self.raiseUnxpected(error: error)
|
self.raiseUnexpected(error: error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -477,7 +525,7 @@ open class AudioPlayer {
|
|||||||
if let first = customAttachedNodes.first {
|
if let first = customAttachedNodes.first {
|
||||||
audioEngine.connect(rateNode, to: first, format: nil)
|
audioEngine.connect(rateNode, to: first, format: nil)
|
||||||
}
|
}
|
||||||
for index in 0..<customAttachedNodes.count - 1 {
|
for index in 0 ..< customAttachedNodes.count - 1 {
|
||||||
let current = customAttachedNodes[index]
|
let current = customAttachedNodes[index]
|
||||||
let next = customAttachedNodes[index + 1]
|
let next = customAttachedNodes[index + 1]
|
||||||
let format = current.inputFormat(forBus: 0)
|
let format = current.inputFormat(forBus: 0)
|
||||||
@@ -513,6 +561,7 @@ open class AudioPlayer {
|
|||||||
/// Pauses the audio engine and stops the player's hardware
|
/// Pauses the audio engine and stops the player's hardware
|
||||||
private func pauseEngine() {
|
private func pauseEngine() {
|
||||||
guard isEngineRunning else { return }
|
guard isEngineRunning else { return }
|
||||||
|
audioEngine.reset()
|
||||||
audioEngine.pause()
|
audioEngine.pause()
|
||||||
player.auAudioUnit.stopHardware()
|
player.auAudioUnit.stopHardware()
|
||||||
Logger.debug("engine paused ⏸", category: .generic)
|
Logger.debug("engine paused ⏸", category: .generic)
|
||||||
@@ -530,24 +579,7 @@ open class AudioPlayer {
|
|||||||
Logger.debug("engine stopped 🛑", category: .generic)
|
Logger.debug("engine stopped 🛑", category: .generic)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts the timer of `audioReadSource` for proccesing the source read stream
|
/// Starts the audio player, resetting the buffers if requested
|
||||||
///
|
|
||||||
/// This calls `processSource` method every `500 ms`
|
|
||||||
private func startReadProcessFromSourceIfNeeded() {
|
|
||||||
guard audioReadSource.state != .activated else { return }
|
|
||||||
audioReadSource.add { [weak self] in
|
|
||||||
self?.processSource()
|
|
||||||
}
|
|
||||||
audioReadSource.activate()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stops and removes the handler from the timer, @see `audioReadSource`
|
|
||||||
private func stopReadProccessFromSource() {
|
|
||||||
audioReadSource.suspend()
|
|
||||||
audioReadSource.removeHandler()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Starts the audio player, reseting the buffers if requested
|
|
||||||
///
|
///
|
||||||
/// - parameter resetBuffers: A `Bool` value indicating if the buffers should be reset, prior starting the player.
|
/// - parameter resetBuffers: A `Bool` value indicating if the buffers should be reset, prior starting the player.
|
||||||
private func startPlayer(resetBuffers: Bool) {
|
private func startPlayer(resetBuffers: Bool) {
|
||||||
@@ -560,7 +592,7 @@ open class AudioPlayer {
|
|||||||
try player.auAudioUnit.startHardware()
|
try player.auAudioUnit.startHardware()
|
||||||
} catch {
|
} catch {
|
||||||
stopEngine(reason: .error)
|
stopEngine(reason: .error)
|
||||||
raiseUnxpected(error: .audioSystemError(.playerStartError))
|
raiseUnexpected(error: .audioSystemError(.playerStartError))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,7 +600,6 @@ open class AudioPlayer {
|
|||||||
private func processSource() {
|
private func processSource() {
|
||||||
dispatchPrecondition(condition: .onQueue(sourceQueue))
|
dispatchPrecondition(condition: .onQueue(sourceQueue))
|
||||||
|
|
||||||
guard !playerContext.disposedRequested else { return }
|
|
||||||
guard playerContext.internalState != .paused else { return }
|
guard playerContext.internalState != .paused else { return }
|
||||||
|
|
||||||
if playerContext.internalState == .pendingNext {
|
if playerContext.internalState == .pendingNext {
|
||||||
@@ -577,8 +608,8 @@ open class AudioPlayer {
|
|||||||
setCurrentReading(entry: entry, startPlaying: true, shouldClearQueue: true)
|
setCurrentReading(entry: entry, startPlaying: true, shouldClearQueue: true)
|
||||||
rendererContext.resetBuffers()
|
rendererContext.resetBuffers()
|
||||||
} else if let playingEntry = playerContext.audioPlayingEntry,
|
} else if let playingEntry = playerContext.audioPlayingEntry,
|
||||||
playingEntry.seekRequest.requested,
|
playingEntry.seekRequest.requested,
|
||||||
playingEntry != playerContext.audioReadingEntry
|
playingEntry != playerContext.audioReadingEntry
|
||||||
{
|
{
|
||||||
playingEntry.audioStreamState.processedDataFormat = false
|
playingEntry.audioStreamState.processedDataFormat = false
|
||||||
playingEntry.reset()
|
playingEntry.reset()
|
||||||
@@ -605,7 +636,6 @@ open class AudioPlayer {
|
|||||||
setCurrentReading(entry: entry, startPlaying: shouldStartPlaying, shouldClearQueue: false)
|
setCurrentReading(entry: entry, startPlaying: shouldStartPlaying, shouldClearQueue: false)
|
||||||
} else if playerContext.audioPlayingEntry == nil {
|
} else if playerContext.audioPlayingEntry == nil {
|
||||||
if playerContext.internalState != .stopped {
|
if playerContext.internalState != .stopped {
|
||||||
stopReadProccessFromSource()
|
|
||||||
stopEngine(reason: .eof)
|
stopEngine(reason: .eof)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -621,7 +651,7 @@ open class AudioPlayer {
|
|||||||
playingEntry.seekRequest.lock.unlock()
|
playingEntry.seekRequest.lock.unlock()
|
||||||
|
|
||||||
if originalSeekToTimeRequested, playerContext.audioReadingEntry === playingEntry {
|
if originalSeekToTimeRequested, playerContext.audioReadingEntry === playingEntry {
|
||||||
proccessSeekTime()
|
processSeekTime()
|
||||||
|
|
||||||
let version = playingEntry.seekRequest.version.value
|
let version = playingEntry.seekRequest.version.value
|
||||||
if currSeekVersion == version {
|
if currSeekVersion == version {
|
||||||
@@ -633,7 +663,7 @@ open class AudioPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func proccessSeekTime() {
|
private func processSeekTime() {
|
||||||
assert(playerContext.audioReadingEntry === playerContext.audioPlayingEntry,
|
assert(playerContext.audioReadingEntry === playerContext.audioPlayingEntry,
|
||||||
"reading and playing entry must be the same")
|
"reading and playing entry must be the same")
|
||||||
fileStreamProcessor.processSeek()
|
fileStreamProcessor.processSeek()
|
||||||
@@ -671,44 +701,41 @@ open class AudioPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func processFinishPlaying(entry: AudioEntry?, with nextEntry: AudioEntry?) {
|
private func processFinishPlaying(entry: AudioEntry?, with nextEntry: AudioEntry?) {
|
||||||
let playingEntry = playerContext.entriesLock.around { playerContext.audioPlayingEntry }
|
let playingEntry = playerContext.entriesLock.withLock { playerContext.audioPlayingEntry }
|
||||||
guard entry == playingEntry else { return }
|
guard entry == playingEntry else { return }
|
||||||
|
|
||||||
let isPlayingSameItemProbablySeek = playerContext.audioPlayingEntry === nextEntry
|
let isPlayingSameItemProbablySeek = playerContext.audioPlayingEntry === nextEntry
|
||||||
|
|
||||||
let notifyDelegateEntryFinishedPlaying: (AudioEntry?, Bool) -> Void = { [weak self] entry, _ in
|
|
||||||
guard let self = self else { return }
|
|
||||||
if let entry = entry, !isPlayingSameItemProbablySeek {
|
|
||||||
let entryId = entry.id
|
|
||||||
let progressInFrames = entry.progressInFrames()
|
|
||||||
let progress = Double(progressInFrames) / self.outputAudioFormat.basicStreamDescription.mSampleRate
|
|
||||||
let duration = entry.duration()
|
|
||||||
|
|
||||||
asyncOnMain {
|
|
||||||
self.delegate?.audioPlayerDidFinishPlaying(player: self,
|
|
||||||
entryId: entryId,
|
|
||||||
stopReason: self.stopReason,
|
|
||||||
progress: progress,
|
|
||||||
duration: duration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let nextEntry = nextEntry {
|
if let nextEntry = nextEntry {
|
||||||
if !isPlayingSameItemProbablySeek {
|
if !isPlayingSameItemProbablySeek {
|
||||||
nextEntry.lock.around {
|
nextEntry.lock.withLock {
|
||||||
nextEntry.seekTime = 0
|
nextEntry.seekTime = 0
|
||||||
}
|
}
|
||||||
nextEntry.seekRequest.lock.around {
|
nextEntry.seekRequest.lock.withLock {
|
||||||
nextEntry.seekRequest.requested = false
|
nextEntry.seekRequest.requested = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
playerContext.entriesLock.lock()
|
playerContext.entriesLock.lock()
|
||||||
playerContext.audioPlayingEntry = nextEntry
|
playerContext.audioPlayingEntry = nextEntry
|
||||||
|
let playingQueueEntryId = playerContext.audioPlayingEntry?.id ?? AudioEntryId(id: "")
|
||||||
playerContext.entriesLock.unlock()
|
playerContext.entriesLock.unlock()
|
||||||
let playingQueueEntryId = playingEntry?.id ?? AudioEntryId(id: "")
|
|
||||||
|
|
||||||
notifyDelegateEntryFinishedPlaying(entry, isPlayingSameItemProbablySeek)
|
if let entry = entry, !isPlayingSameItemProbablySeek {
|
||||||
|
let entryId = entry.id
|
||||||
|
let progressInFrames = entry.progressInFrames()
|
||||||
|
let progress = Double(progressInFrames) / outputAudioFormat.basicStreamDescription.mSampleRate
|
||||||
|
let duration = entry.duration()
|
||||||
|
asyncOnMain { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
self.delegate?.audioPlayerDidFinishPlaying(
|
||||||
|
player: self,
|
||||||
|
entryId: entryId,
|
||||||
|
stopReason: self.stopReason,
|
||||||
|
progress: progress,
|
||||||
|
duration: duration
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
if !isPlayingSameItemProbablySeek {
|
if !isPlayingSameItemProbablySeek {
|
||||||
playerContext.setInternalState(to: .waitingForData)
|
playerContext.setInternalState(to: .waitingForData)
|
||||||
|
|
||||||
@@ -718,10 +745,29 @@ open class AudioPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
notifyDelegateEntryFinishedPlaying(entry, isPlayingSameItemProbablySeek)
|
|
||||||
playerContext.entriesLock.lock()
|
playerContext.entriesLock.lock()
|
||||||
playerContext.audioPlayingEntry = nil
|
playerContext.audioPlayingEntry = nil
|
||||||
playerContext.entriesLock.unlock()
|
playerContext.entriesLock.unlock()
|
||||||
|
if let entry = entry, !isPlayingSameItemProbablySeek {
|
||||||
|
let entryId = entry.id
|
||||||
|
let progressInFrames = entry.progressInFrames()
|
||||||
|
let progress = Double(progressInFrames) / outputAudioFormat.basicStreamDescription.mSampleRate
|
||||||
|
let duration = entry.duration()
|
||||||
|
|
||||||
|
sourceQueue.async { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
self.processSource()
|
||||||
|
asyncOnMain {
|
||||||
|
self.delegate?.audioPlayerDidFinishPlaying(
|
||||||
|
player: self,
|
||||||
|
entryId: entryId,
|
||||||
|
stopReason: self.stopReason,
|
||||||
|
progress: progress,
|
||||||
|
duration: duration
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sourceQueue.async { [weak self] in
|
sourceQueue.async { [weak self] in
|
||||||
self?.processSource()
|
self?.processSource()
|
||||||
@@ -748,9 +794,8 @@ open class AudioPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func raiseUnxpected(error: AudioPlayerError) {
|
private func raiseUnexpected(error: AudioPlayerError) {
|
||||||
playerContext.setInternalState(to: .error)
|
playerContext.setInternalState(to: .error)
|
||||||
// todo raise on main thread from playback thread
|
|
||||||
asyncOnMain { [weak self] in
|
asyncOnMain { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.delegate?.audioPlayerUnexpectedError(player: self, error: error)
|
self.delegate?.audioPlayerUnexpectedError(player: self, error: error)
|
||||||
@@ -769,7 +814,7 @@ extension AudioPlayer: AudioStreamSourceDelegate {
|
|||||||
let openFileStreamStatus = fileStreamProcessor.openFileStream(with: source.audioFileHint)
|
let openFileStreamStatus = fileStreamProcessor.openFileStream(with: source.audioFileHint)
|
||||||
guard openFileStreamStatus == noErr else {
|
guard openFileStreamStatus == noErr else {
|
||||||
let streamError = AudioFileStreamError(status: openFileStreamStatus)
|
let streamError = AudioFileStreamError(status: openFileStreamStatus)
|
||||||
raiseUnxpected(error: .audioSystemError(.fileStreamError(streamError)))
|
raiseUnexpected(error: .audioSystemError(.fileStreamError(streamError)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -779,7 +824,7 @@ extension AudioPlayer: AudioStreamSourceDelegate {
|
|||||||
guard streamBytesStatus == noErr else {
|
guard streamBytesStatus == noErr else {
|
||||||
if let playingEntry = playerContext.audioPlayingEntry, playingEntry.has(same: source) {
|
if let playingEntry = playerContext.audioPlayingEntry, playingEntry.has(same: source) {
|
||||||
let streamBytesError = AudioFileStreamError(status: streamBytesStatus)
|
let streamBytesError = AudioFileStreamError(status: streamBytesStatus)
|
||||||
raiseUnxpected(error: .streamParseBytesFailure(streamBytesError))
|
raiseUnexpected(error: .streamParseBytesFailure(streamBytesError))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -790,12 +835,12 @@ extension AudioPlayer: AudioStreamSourceDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func errorOccured(source: CoreAudioStreamSource, error: Error) {
|
func errorOccurred(source: CoreAudioStreamSource, error: Error) {
|
||||||
guard let entry = playerContext.audioReadingEntry, entry.has(same: source) else { return }
|
guard let entry = playerContext.audioReadingEntry, entry.has(same: source) else { return }
|
||||||
raiseUnxpected(error: .networkError(.failure(error)))
|
raiseUnexpected(error: .networkError(.failure(error)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func endOfFileOccured(source: CoreAudioStreamSource) {
|
func endOfFileOccurred(source: CoreAudioStreamSource) {
|
||||||
let hasSameSource = playerContext.audioReadingEntry?.has(same: source) ?? false
|
let hasSameSource = playerContext.audioReadingEntry?.has(same: source) ?? false
|
||||||
guard playerContext.audioReadingEntry == nil || hasSameSource else {
|
guard playerContext.audioReadingEntry == nil || hasSameSource else {
|
||||||
source.delegate = nil
|
source.delegate = nil
|
||||||
@@ -826,7 +871,10 @@ extension AudioPlayer: AudioStreamSourceDelegate {
|
|||||||
playerContext.audioReadingEntry = nil
|
playerContext.audioReadingEntry = nil
|
||||||
playerContext.entriesLock.unlock()
|
playerContext.entriesLock.unlock()
|
||||||
|
|
||||||
processSource()
|
sourceQueue.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.processSource()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func metadataReceived(data: [String: String]) {
|
func metadataReceived(data: [String: String]) {
|
||||||
|
|||||||
@@ -13,20 +13,20 @@ public struct AudioPlayerConfiguration: Equatable {
|
|||||||
/// Number of seconds of audio required to before playback first starts.
|
/// Number of seconds of audio required to before playback first starts.
|
||||||
/// - note: Must be larger that `bufferSizeInSeconds`
|
/// - note: Must be larger that `bufferSizeInSeconds`
|
||||||
let secondsRequiredToStartPlaying: Double
|
let secondsRequiredToStartPlaying: Double
|
||||||
/// Number of seconds of audio required after seek occcurs.
|
/// Number of seconds of audio required after seek occurs.
|
||||||
let gracePeriodAfterSeekInSeconds: Double
|
let gracePeriodAfterSeekInSeconds: Double
|
||||||
/// Number of seconds of audio required to before playback resumes after a buffer underun
|
/// Number of seconds of audio required to before playback resumes after a buffer underrun
|
||||||
/// - note: Must be larger that `bufferSizeInSeconds`
|
/// - note: Must be larger that `bufferSizeInSeconds`
|
||||||
let secondsRequiredToStartPlayingAfterBufferUnderun: Int
|
let secondsRequiredToStartPlayingAfterBufferUnderrun: Int
|
||||||
|
|
||||||
/// Enables the internal logs
|
/// Enables the internal logs
|
||||||
let enableLogs: Bool
|
let enableLogs: Bool
|
||||||
|
|
||||||
public static let `default` = AudioPlayerConfiguration(flushQueueOnSeek: true,
|
public static let `default` = AudioPlayerConfiguration(flushQueueOnSeek: false,
|
||||||
bufferSizeInSeconds: 10,
|
bufferSizeInSeconds: 10,
|
||||||
secondsRequiredToStartPlaying: 1,
|
secondsRequiredToStartPlaying: 1,
|
||||||
gracePeriodAfterSeekInSeconds: 0.5,
|
gracePeriodAfterSeekInSeconds: 0.5,
|
||||||
secondsRequiredToStartPlayingAfterBufferUnderun: 1,
|
secondsRequiredToStartPlayingAfterBufferUnderrun: 1,
|
||||||
enableLogs: false)
|
enableLogs: false)
|
||||||
/// Initializes the configuration for the `AudioPlayer`
|
/// Initializes the configuration for the `AudioPlayer`
|
||||||
///
|
///
|
||||||
@@ -35,22 +35,22 @@ public struct AudioPlayerConfiguration: Equatable {
|
|||||||
/// - parameter flushQueueOnSeek: All pending items will be flushed when seeking a track if this is set to `true`
|
/// - parameter flushQueueOnSeek: All pending items will be flushed when seeking a track if this is set to `true`
|
||||||
/// - parameter bufferSizeInSeconds: The size of the decompressed buffer.
|
/// - parameter bufferSizeInSeconds: The size of the decompressed buffer.
|
||||||
/// - parameter secondsRequiredToStartPlaying: Number of seconds of audio required to before playback first starts.
|
/// - parameter secondsRequiredToStartPlaying: Number of seconds of audio required to before playback first starts.
|
||||||
/// - parameter gracePeriodAfterSeekInSeconds: Number of seconds of audio required after seek occcurs.
|
/// - parameter gracePeriodAfterSeekInSeconds: Number of seconds of audio required after seek occurs.
|
||||||
/// - parameter secondsRequiredToStartPlayingAfterBufferUnderun: Number of seconds of audio required to before playback resumes after a buffer underun
|
/// - parameter secondsRequiredToStartPlayingAfterBufferUnderrun: Number of seconds of audio required to before playback resumes after a buffer underrun
|
||||||
/// - parameter enableLogs: Enables the internal logs
|
/// - parameter enableLogs: Enables the internal logs
|
||||||
///
|
///
|
||||||
public init(flushQueueOnSeek: Bool = true,
|
public init(flushQueueOnSeek: Bool = true,
|
||||||
bufferSizeInSeconds: Double = 10,
|
bufferSizeInSeconds: Double = 10,
|
||||||
secondsRequiredToStartPlaying: Double = 1,
|
secondsRequiredToStartPlaying: Double = 1,
|
||||||
gracePeriodAfterSeekInSeconds: Double = 0.5,
|
gracePeriodAfterSeekInSeconds: Double = 0.5,
|
||||||
secondsRequiredToStartPlayingAfterBufferUnderun: Int = 1,
|
secondsRequiredToStartPlayingAfterBufferUnderrun: Int = 1,
|
||||||
enableLogs: Bool = false)
|
enableLogs: Bool = false)
|
||||||
{
|
{
|
||||||
self.flushQueueOnSeek = flushQueueOnSeek
|
self.flushQueueOnSeek = flushQueueOnSeek
|
||||||
self.bufferSizeInSeconds = bufferSizeInSeconds
|
self.bufferSizeInSeconds = bufferSizeInSeconds
|
||||||
self.secondsRequiredToStartPlaying = secondsRequiredToStartPlaying
|
self.secondsRequiredToStartPlaying = secondsRequiredToStartPlaying
|
||||||
self.gracePeriodAfterSeekInSeconds = gracePeriodAfterSeekInSeconds
|
self.gracePeriodAfterSeekInSeconds = gracePeriodAfterSeekInSeconds
|
||||||
self.secondsRequiredToStartPlayingAfterBufferUnderun = secondsRequiredToStartPlayingAfterBufferUnderun
|
self.secondsRequiredToStartPlayingAfterBufferUnderrun = secondsRequiredToStartPlayingAfterBufferUnderrun
|
||||||
self.enableLogs = enableLogs
|
self.enableLogs = enableLogs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,15 +70,15 @@ public struct AudioPlayerConfiguration: Equatable {
|
|||||||
? defaults.gracePeriodAfterSeekInSeconds
|
? defaults.gracePeriodAfterSeekInSeconds
|
||||||
: self.gracePeriodAfterSeekInSeconds
|
: self.gracePeriodAfterSeekInSeconds
|
||||||
|
|
||||||
let secondsRequiredToStartPlayingAfterBufferUnderun = self.secondsRequiredToStartPlayingAfterBufferUnderun == 0
|
let secondsRequiredToStartPlayingAfterBufferUnderrun = self.secondsRequiredToStartPlayingAfterBufferUnderrun == 0
|
||||||
? defaults.secondsRequiredToStartPlayingAfterBufferUnderun
|
? defaults.secondsRequiredToStartPlayingAfterBufferUnderrun
|
||||||
: self.secondsRequiredToStartPlayingAfterBufferUnderun
|
: self.secondsRequiredToStartPlayingAfterBufferUnderrun
|
||||||
|
|
||||||
return AudioPlayerConfiguration(flushQueueOnSeek: flushQueueOnSeek,
|
return AudioPlayerConfiguration(flushQueueOnSeek: flushQueueOnSeek,
|
||||||
bufferSizeInSeconds: bufferSizeInSeconds,
|
bufferSizeInSeconds: bufferSizeInSeconds,
|
||||||
secondsRequiredToStartPlaying: secondsRequiredToStartPlaying,
|
secondsRequiredToStartPlaying: secondsRequiredToStartPlaying,
|
||||||
gracePeriodAfterSeekInSeconds: gracePeriodAfterSeekInSeconds,
|
gracePeriodAfterSeekInSeconds: gracePeriodAfterSeekInSeconds,
|
||||||
secondsRequiredToStartPlayingAfterBufferUnderun: secondsRequiredToStartPlayingAfterBufferUnderun,
|
secondsRequiredToStartPlayingAfterBufferUnderrun: secondsRequiredToStartPlayingAfterBufferUnderrun,
|
||||||
enableLogs: enableLogs)
|
enableLogs: enableLogs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,31 +5,32 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
internal final class AudioPlayerContext {
|
final class AudioPlayerContext {
|
||||||
var stopReason = Protected<AudioPlayerStopReason>(.none)
|
var stopReason: Atomic<AudioPlayerStopReason>
|
||||||
|
|
||||||
var state = Protected<AudioPlayerState>(.ready)
|
var state: Atomic<AudioPlayerState>
|
||||||
var stateChanged: ((_ oldState: AudioPlayerState, _ newState: AudioPlayerState) -> Void)?
|
var stateChanged: ((_ oldState: AudioPlayerState, _ newState: AudioPlayerState) -> Void)?
|
||||||
|
|
||||||
var muted = Protected<Bool>(false)
|
var muted: Atomic<Bool>
|
||||||
|
|
||||||
var internalState: AudioPlayer.InternalState {
|
var internalState: AudioPlayer.InternalState {
|
||||||
playerInternalState.value
|
playerInternalState.value
|
||||||
}
|
}
|
||||||
|
|
||||||
let entriesLock = UnfairLock()
|
let entriesLock: UnfairLock
|
||||||
var audioReadingEntry: AudioEntry?
|
var audioReadingEntry: AudioEntry?
|
||||||
var audioPlayingEntry: AudioEntry?
|
var audioPlayingEntry: AudioEntry?
|
||||||
|
|
||||||
var disposedRequested: Bool
|
|
||||||
|
|
||||||
/// This is the player's internal state to use
|
/// This is the player's internal state to use
|
||||||
/// - NOTE: Do not use directly instead use the `internalState` to set and get the property
|
/// - NOTE: Do not use directly instead use the `internalState` to set and get the property
|
||||||
/// or the `setInternalState(to:when:)`method
|
/// or the `setInternalState(to:when:)`method
|
||||||
private var playerInternalState = Protected<AudioPlayer.InternalState>(.initial)
|
private var playerInternalState = Atomic<AudioPlayer.InternalState>(.initial)
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
disposedRequested = false
|
stopReason = Atomic<AudioPlayerStopReason>(.none)
|
||||||
|
state = Atomic<AudioPlayerState>(.ready)
|
||||||
|
muted = Atomic<Bool>(false)
|
||||||
|
entriesLock = UnfairLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the internal state if given the `inState` will be evaluated before assignment occurs.
|
/// Sets the internal state if given the `inState` will be evaluated before assignment occurs.
|
||||||
@@ -37,11 +38,13 @@ internal final class AudioPlayerContext {
|
|||||||
/// - parameter state: The new `PlayerInternalState`
|
/// - parameter state: The new `PlayerInternalState`
|
||||||
/// - parameter inState: If the `inState` expression is not nil, the internalState will be set if the evaluated expression is `true`
|
/// - parameter inState: If the `inState` expression is not nil, the internalState will be set if the evaluated expression is `true`
|
||||||
/// - NOTE: This sets the underlying `__playerInternalState` variable
|
/// - NOTE: This sets the underlying `__playerInternalState` variable
|
||||||
internal func setInternalState(to state: AudioPlayer.InternalState,
|
func setInternalState(to state: AudioPlayer.InternalState,
|
||||||
when inState: ((AudioPlayer.InternalState) -> Bool)? = nil)
|
when inState: ((AudioPlayer.InternalState) -> Bool)? = nil)
|
||||||
{
|
{
|
||||||
let newValues = playerStateAndStopReason(for: state)
|
let newValues = playerStateAndStopReason(for: state)
|
||||||
stopReason.write { $0 = newValues.stopReason }
|
if let stopReason = newValues.stopReason {
|
||||||
|
self.stopReason.write { $0 = stopReason }
|
||||||
|
}
|
||||||
guard state != internalState else { return }
|
guard state != internalState else { return }
|
||||||
if let inState = inState, !inState(internalState) {
|
if let inState = inState, !inState(internalState) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public protocol AudioPlayerDelegate: AnyObject {
|
|||||||
stopReason: AudioPlayerStopReason,
|
stopReason: AudioPlayerStopReason,
|
||||||
progress: Double,
|
progress: Double,
|
||||||
duration: Double)
|
duration: Double)
|
||||||
/// Tells the delegate when an unexpected error occured.
|
/// Tells the delegate when an unexpected error occurred.
|
||||||
/// - note: Probably a good time to recreate the player when this occurs
|
/// - note: Probably a good time to recreate the player when this occurs
|
||||||
func audioPlayerUnexpectedError(player: AudioPlayer, error: AudioPlayerError)
|
func audioPlayerUnexpectedError(player: AudioPlayer, error: AudioPlayerError)
|
||||||
|
|
||||||
|
|||||||
@@ -30,26 +30,26 @@ extension AudioPlayer {
|
|||||||
/// Helper method that returns `AudioPlayerState` and `StopReason` based on the given `InternalState`
|
/// Helper method that returns `AudioPlayerState` and `StopReason` based on the given `InternalState`
|
||||||
/// - Parameter internalState: A value of `InternalState`
|
/// - Parameter internalState: A value of `InternalState`
|
||||||
/// - Returns: A tuple of `(AudioPlayerState, AudioPlayerStopReason)`
|
/// - Returns: A tuple of `(AudioPlayerState, AudioPlayerStopReason)`
|
||||||
func playerStateAndStopReason(for internalState: AudioPlayer.InternalState) -> (state: AudioPlayerState,
|
func playerStateAndStopReason(
|
||||||
stopReason: AudioPlayerStopReason)
|
for internalState: AudioPlayer.InternalState
|
||||||
{
|
) -> (state: AudioPlayerState, stopReason: AudioPlayerStopReason?) {
|
||||||
switch internalState {
|
switch internalState {
|
||||||
case .initial:
|
case .initial:
|
||||||
return (.ready, .none)
|
return (.ready, AudioPlayerStopReason.none)
|
||||||
case .running, .playing, .waitingForDataAfterSeek:
|
case .running, .playing, .waitingForDataAfterSeek:
|
||||||
return (.playing, .none)
|
return (.playing, AudioPlayerStopReason.none)
|
||||||
case .pendingNext, .rebuffering, .waitingForData:
|
case .pendingNext, .rebuffering, .waitingForData:
|
||||||
return (.bufferring, .none)
|
return (.bufferring, AudioPlayerStopReason.none)
|
||||||
case .stopped:
|
case .stopped:
|
||||||
return (.stopped, .userAction)
|
return (.stopped, nil)
|
||||||
case .paused:
|
case .paused:
|
||||||
return (.paused, .none)
|
return (.paused, AudioPlayerStopReason.none)
|
||||||
case .disposed:
|
case .disposed:
|
||||||
return (.disposed, .userAction)
|
return (.disposed, .userAction)
|
||||||
case .error:
|
case .error:
|
||||||
return (.error, .error)
|
return (.error, AudioPlayerStopReason.error)
|
||||||
default:
|
default:
|
||||||
return (.ready, .none)
|
return (.ready, AudioPlayerStopReason.none)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
import AVFoundation
|
import AVFoundation
|
||||||
import CoreAudio
|
import CoreAudio
|
||||||
|
|
||||||
internal var maxFramesPerSlice: AVAudioFrameCount = 8192
|
var maxFramesPerSlice: AVAudioFrameCount = 8192
|
||||||
|
|
||||||
final class AudioRendererContext {
|
final class AudioRendererContext {
|
||||||
var waiting = Protected<Bool>(false)
|
var waiting = Atomic<Bool>(false)
|
||||||
|
|
||||||
let lock = UnfairLock()
|
let lock = UnfairLock()
|
||||||
|
|
||||||
@@ -20,13 +20,11 @@ final class AudioRendererContext {
|
|||||||
|
|
||||||
let packetsSemaphore = DispatchSemaphore(value: 0)
|
let packetsSemaphore = DispatchSemaphore(value: 0)
|
||||||
|
|
||||||
var discontinuous: Bool = false
|
|
||||||
|
|
||||||
let framesRequiredToStartPlaying: UInt32
|
let framesRequiredToStartPlaying: UInt32
|
||||||
let framesRequiredAfterRebuffering: UInt32
|
let framesRequiredAfterRebuffering: UInt32
|
||||||
let framesRequiredForDataAfterSeekPlaying: UInt32
|
let framesRequiredForDataAfterSeekPlaying: UInt32
|
||||||
|
|
||||||
var waitingForDataAfterSeekFrameCount = Protected<Int32>(0)
|
var waitingForDataAfterSeekFrameCount = Atomic<Int32>(0)
|
||||||
|
|
||||||
private let configuration: AudioPlayerConfiguration
|
private let configuration: AudioPlayerConfiguration
|
||||||
|
|
||||||
@@ -36,7 +34,7 @@ final class AudioRendererContext {
|
|||||||
let canonicalStream = outputAudioFormat.basicStreamDescription
|
let canonicalStream = outputAudioFormat.basicStreamDescription
|
||||||
|
|
||||||
framesRequiredToStartPlaying = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.secondsRequiredToStartPlaying)
|
framesRequiredToStartPlaying = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.secondsRequiredToStartPlaying)
|
||||||
framesRequiredAfterRebuffering = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.secondsRequiredToStartPlayingAfterBufferUnderun)
|
framesRequiredAfterRebuffering = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.secondsRequiredToStartPlayingAfterBufferUnderrun)
|
||||||
framesRequiredForDataAfterSeekPlaying = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.gracePeriodAfterSeekInSeconds)
|
framesRequiredForDataAfterSeekPlaying = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.gracePeriodAfterSeekInSeconds)
|
||||||
|
|
||||||
let dataByteSize = Int(canonicalStream.mSampleRate * configuration.bufferSizeInSeconds) * Int(canonicalStream.mBytesPerFrame)
|
let dataByteSize = Int(canonicalStream.mSampleRate * configuration.bufferSizeInSeconds) * Int(canonicalStream.mBytesPerFrame)
|
||||||
@@ -77,8 +75,8 @@ private func allocateBufferList(dataByteSize: Int) -> UnsafeMutablePointer<Audio
|
|||||||
let _bufferList = AudioBufferList.allocate(maximumBuffers: 1)
|
let _bufferList = AudioBufferList.allocate(maximumBuffers: 1)
|
||||||
|
|
||||||
_bufferList[0].mDataByteSize = UInt32(dataByteSize)
|
_bufferList[0].mDataByteSize = UInt32(dataByteSize)
|
||||||
let alingment = MemoryLayout<UInt8>.alignment
|
let alignment = MemoryLayout<UInt8>.alignment
|
||||||
let mData = UnsafeMutableRawPointer.allocate(byteCount: dataByteSize, alignment: alingment)
|
let mData = UnsafeMutableRawPointer.allocate(byteCount: dataByteSize, alignment: alignment)
|
||||||
_bufferList[0].mData = mData
|
_bufferList[0].mData = mData
|
||||||
_bufferList[0].mNumberChannels = 2
|
_bufferList[0].mNumberChannels = 2
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import AVFoundation
|
|||||||
|
|
||||||
enum AudioConvertStatus: Int32 {
|
enum AudioConvertStatus: Int32 {
|
||||||
case done = 100
|
case done = 100
|
||||||
case proccessed = 0
|
case processed = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AudioConvertInfo {
|
struct AudioConvertInfo {
|
||||||
@@ -20,11 +20,11 @@ struct AudioConvertInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum FileStreamProcessorEffect {
|
enum FileStreamProcessorEffect {
|
||||||
case proccessSource
|
case processSource
|
||||||
case raiseError(AudioPlayerError)
|
case raiseError(AudioPlayerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An object that handles the proccessing of AudioFileStream, its packets etc.
|
/// An object that handles the processing of AudioFileStream, its packets etc.
|
||||||
final class AudioFileStreamProcessor {
|
final class AudioFileStreamProcessor {
|
||||||
private let maxCompressedPacketForBitrate = 4096
|
private let maxCompressedPacketForBitrate = 4096
|
||||||
|
|
||||||
@@ -34,12 +34,13 @@ final class AudioFileStreamProcessor {
|
|||||||
private let rendererContext: AudioRendererContext
|
private let rendererContext: AudioRendererContext
|
||||||
private let outputAudioFormat: AudioStreamBasicDescription
|
private let outputAudioFormat: AudioStreamBasicDescription
|
||||||
|
|
||||||
internal var audioFileStream: AudioFileStreamID?
|
var audioFileStream: AudioFileStreamID?
|
||||||
internal var audioConverter: AudioConverterRef?
|
var audioConverter: AudioConverterRef?
|
||||||
internal var discontinuous: Bool = false
|
var discontinuous: Bool = false
|
||||||
internal var inputFormat = AudioStreamBasicDescription()
|
var inputFormat = AudioStreamBasicDescription()
|
||||||
internal var fileFormat: String = ""
|
|
||||||
internal let fa4mFormat = "fa4m"
|
var currentFileFormat: String = ""
|
||||||
|
let fileFormatsForDelayedConverterCreation: Set = ["fa4m", "f4pm"]
|
||||||
|
|
||||||
var isFileStreamOpen: Bool {
|
var isFileStreamOpen: Bool {
|
||||||
audioFileStream != nil
|
audioFileStream != nil
|
||||||
@@ -115,26 +116,19 @@ final class AudioFileStreamProcessor {
|
|||||||
readingEntry.lock.unlock()
|
readingEntry.lock.unlock()
|
||||||
|
|
||||||
let bitrate = readingEntry.calculatedBitrate()
|
let bitrate = readingEntry.calculatedBitrate()
|
||||||
if readingEntry.processedPacketsState.count > 0, bitrate > 0 {
|
if readingEntry.packetDuration > 0, bitrate > 0 {
|
||||||
var ioFlags = AudioFileStreamSeekFlags(rawValue: 0)
|
var ioFlags = AudioFileStreamSeekFlags(rawValue: 0)
|
||||||
var packetsAlignedByteOffset: Int64 = 0
|
var packetsAlignedByteOffset: Int64 = 0
|
||||||
let seekPacket = Int64(floor(readingEntry.seekRequest.time / readingEntry.packetDuration))
|
let seekPacket = Int64(floor(readingEntry.seekRequest.time / readingEntry.packetDuration))
|
||||||
|
|
||||||
let seekStatus = AudioFileStreamSeek(stream, seekPacket, &packetsAlignedByteOffset, &ioFlags)
|
let seekStatus = AudioFileStreamSeek(stream, seekPacket, &packetsAlignedByteOffset, &ioFlags)
|
||||||
guard seekStatus == noErr else {
|
|
||||||
let streamError = AudioFileStreamError(status: seekStatus)
|
|
||||||
Logger.error("seek failed %@", category: .generic, args: streamError.debugDescription)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let dataOffset = Int64(readingEntry.audioStreamState.dataOffset)
|
let dataOffset = Int64(readingEntry.audioStreamState.dataOffset)
|
||||||
if !ioFlags.contains(.offsetIsEstimated) {
|
if seekStatus == noErr, !ioFlags.contains(.offsetIsEstimated) {
|
||||||
seekByteOffset = packetsAlignedByteOffset + dataOffset
|
let delta = Double((seekByteOffset - dataOffset) - packetsAlignedByteOffset) / (bitrate * 8)
|
||||||
let delta = Double((seekByteOffset - dataOffset) - packetsAlignedByteOffset) / bitrate * 8
|
|
||||||
|
|
||||||
readingEntry.lock.lock()
|
readingEntry.lock.lock()
|
||||||
readingEntry.seekTime -= delta
|
readingEntry.seekTime -= delta
|
||||||
readingEntry.lock.unlock()
|
readingEntry.lock.unlock()
|
||||||
|
seekByteOffset = packetsAlignedByteOffset + dataOffset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +159,7 @@ final class AudioFileStreamProcessor {
|
|||||||
|
|
||||||
var classDesc = AudioClassDescription()
|
var classDesc = AudioClassDescription()
|
||||||
var outputFormat = toFormat
|
var outputFormat = toFormat
|
||||||
if getHardwareCodecClassDescripition(formatId: inputFormat.mFormatID, classDesc: &classDesc) {
|
if getHardwareCodecClassDescription(formatId: inputFormat.mFormatID, classDesc: &classDesc) {
|
||||||
AudioConverterNewSpecific(&inputFormat, &outputFormat, 1, &classDesc, &audioConverter)
|
AudioConverterNewSpecific(&inputFormat, &outputFormat, 1, &classDesc, &audioConverter)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,11 +172,12 @@ final class AudioFileStreamProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.inputFormat = inputFormat
|
self.inputFormat = inputFormat
|
||||||
|
assignMagicCookieToConverterIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func assignMagicCookieToConverterIfNeeded() {
|
||||||
// magic cookie info
|
// magic cookie info
|
||||||
let fileHint = playerContext.audioReadingEntry?.audioFileHint
|
if let fileStream = audioFileStream {
|
||||||
let isProperFormat = fileHint != kAudioFileAAC_ADTSType && fileHint != kAudioFileM4AType && fileHint != kAudioFileMPEG4Type
|
|
||||||
if let fileStream = audioFileStream, isProperFormat {
|
|
||||||
var cookieSize: UInt32 = 0
|
var cookieSize: UInt32 = 0
|
||||||
guard AudioFileStreamGetPropertyInfo(fileStream, kAudioFileStreamProperty_MagicCookieData, &cookieSize, nil) == noErr else {
|
guard AudioFileStreamGetPropertyInfo(fileStream, kAudioFileStreamProperty_MagicCookieData, &cookieSize, nil) == noErr else {
|
||||||
return
|
return
|
||||||
@@ -193,7 +188,7 @@ final class AudioFileStreamProcessor {
|
|||||||
}
|
}
|
||||||
guard let converter = audioConverter else {
|
guard let converter = audioConverter else {
|
||||||
fileStreamCallback?(.raiseError(.audioSystemError(.fileStreamError(.unknownError))))
|
fileStreamCallback?(.raiseError(.audioSystemError(.fileStreamError(.unknownError))))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard AudioConverterSetProperty(converter, kAudioConverterDecompressionMagicCookie, cookieSize, cookie) == noErr else {
|
guard AudioConverterSetProperty(converter, kAudioConverterDecompressionMagicCookie, cookieSize, cookie) == noErr else {
|
||||||
fileStreamCallback?(.raiseError(.audioSystemError(.fileStreamError(.unknownError))))
|
fileStreamCallback?(.raiseError(.audioSystemError(.fileStreamError(.unknownError))))
|
||||||
@@ -229,17 +224,19 @@ final class AudioFileStreamProcessor {
|
|||||||
case kAudioFileStreamProperty_AudioDataByteCount:
|
case kAudioFileStreamProperty_AudioDataByteCount:
|
||||||
processDataByteCount(fileStream: fileStream)
|
processDataByteCount(fileStream: fileStream)
|
||||||
case kAudioFileStreamProperty_AudioDataPacketCount:
|
case kAudioFileStreamProperty_AudioDataPacketCount:
|
||||||
proccessAudioDataPacketCount(fileStream: fileStream)
|
processAudioDataPacketCount(fileStream: fileStream)
|
||||||
case kAudioFileStreamProperty_ReadyToProducePackets:
|
case kAudioFileStreamProperty_ReadyToProducePackets:
|
||||||
// check converter for discontious stream
|
// check converter for discontinuous stream
|
||||||
processReadyToProducePackets(fileStream: fileStream)
|
processReadyToProducePackets(fileStream: fileStream)
|
||||||
|
processPacketUpperBoundAndMaxPacketSize(fileStream: fileStream)
|
||||||
case kAudioFileStreamProperty_FormatList:
|
case kAudioFileStreamProperty_FormatList:
|
||||||
processFormatList(fileStream: fileStream)
|
processFormatList(fileStream: fileStream)
|
||||||
default: break
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: AudioFileStream properties Proccessing
|
// MARK: AudioFileStream properties Processing
|
||||||
|
|
||||||
private func processDataOffset(fileStream: AudioFileStreamID) {
|
private func processDataOffset(fileStream: AudioFileStreamID) {
|
||||||
var offset: UInt64 = 0
|
var offset: UInt64 = 0
|
||||||
@@ -263,7 +260,7 @@ final class AudioFileStreamProcessor {
|
|||||||
var size = UInt32(4)
|
var size = UInt32(4)
|
||||||
AudioFileStreamGetProperty(fileStream, kAudioFileStreamProperty_FileFormat, &size, &fileFormat)
|
AudioFileStreamGetProperty(fileStream, kAudioFileStreamProperty_FileFormat, &size, &fileFormat)
|
||||||
if let stringFileFormat = String(data: Data(fileFormat), encoding: .utf8) {
|
if let stringFileFormat = String(data: Data(fileFormat), encoding: .utf8) {
|
||||||
self.fileFormat = stringFileFormat
|
currentFileFormat = stringFileFormat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,6 +274,9 @@ final class AudioFileStreamProcessor {
|
|||||||
entry.audioStreamFormat = audioStreamFormat
|
entry.audioStreamFormat = audioStreamFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry.sampleRate = Float(audioStreamFormat.mSampleRate)
|
||||||
|
entry.packetDuration = Double(audioStreamFormat.mFramesPerPacket) / Double(entry.sampleRate)
|
||||||
|
|
||||||
var packetBufferSize: UInt32 = 0
|
var packetBufferSize: UInt32 = 0
|
||||||
var status = fileStreamGetProperty(value: &packetBufferSize,
|
var status = fileStreamGetProperty(value: &packetBufferSize,
|
||||||
fileStream: fileStream,
|
fileStream: fileStream,
|
||||||
@@ -289,16 +289,35 @@ final class AudioFileStreamProcessor {
|
|||||||
packetBufferSize = 2048 // default value
|
packetBufferSize = 2048 // default value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entry.lock.around {
|
entry.lock.withLock {
|
||||||
entry.processedPacketsState.bufferSize = packetBufferSize
|
entry.processedPacketsState.bufferSize = packetBufferSize
|
||||||
}
|
}
|
||||||
|
|
||||||
if fileFormat != fa4mFormat {
|
if !fileFormatsForDelayedConverterCreation.contains(currentFileFormat) {
|
||||||
createAudioConverter(from: entry.audioStreamFormat, to: outputAudioFormat)
|
createAudioConverter(from: entry.audioStreamFormat, to: outputAudioFormat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func processPacketUpperBoundAndMaxPacketSize(fileStream: AudioFileStreamID) {
|
||||||
|
guard let entry = playerContext.audioReadingEntry else { return }
|
||||||
|
var packetBufferSize: UInt32 = 0
|
||||||
|
var status = fileStreamGetProperty(value: &packetBufferSize,
|
||||||
|
fileStream: fileStream,
|
||||||
|
propertyId: kAudioFileStreamProperty_PacketSizeUpperBound)
|
||||||
|
if status != 0 || packetBufferSize == 0 {
|
||||||
|
status = fileStreamGetProperty(value: &packetBufferSize,
|
||||||
|
fileStream: fileStream,
|
||||||
|
propertyId: kAudioFileStreamProperty_MaximumPacketSize)
|
||||||
|
if status != 0 || packetBufferSize == 0 {
|
||||||
|
packetBufferSize = 2048 // default value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry.lock.withLock {
|
||||||
|
entry.processedPacketsState.bufferSize = packetBufferSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func processDataByteCount(fileStream: AudioFileStreamID) {
|
private func processDataByteCount(fileStream: AudioFileStreamID) {
|
||||||
guard let entry = playerContext.audioReadingEntry else { return }
|
guard let entry = playerContext.audioReadingEntry else { return }
|
||||||
var audioDataByteCount: UInt64 = 0
|
var audioDataByteCount: UInt64 = 0
|
||||||
@@ -306,7 +325,7 @@ final class AudioFileStreamProcessor {
|
|||||||
entry.audioStreamState.dataByteCount = audioDataByteCount
|
entry.audioStreamState.dataByteCount = audioDataByteCount
|
||||||
}
|
}
|
||||||
|
|
||||||
private func proccessAudioDataPacketCount(fileStream: AudioFileStreamID) {
|
private func processAudioDataPacketCount(fileStream: AudioFileStreamID) {
|
||||||
guard let entry = playerContext.audioReadingEntry else { return }
|
guard let entry = playerContext.audioReadingEntry else { return }
|
||||||
var audioDataPacketCount: UInt64 = 0
|
var audioDataPacketCount: UInt64 = 0
|
||||||
fileStreamGetProperty(value: &audioDataPacketCount, fileStream: fileStream, propertyId: kAudioFileStreamProperty_AudioDataPacketCount)
|
fileStreamGetProperty(value: &audioDataPacketCount, fileStream: fileStream, propertyId: kAudioFileStreamProperty_AudioDataPacketCount)
|
||||||
@@ -331,7 +350,7 @@ final class AudioFileStreamProcessor {
|
|||||||
i += step
|
i += step
|
||||||
}
|
}
|
||||||
|
|
||||||
if fileFormat == fa4mFormat {
|
if fileFormatsForDelayedConverterCreation.contains(currentFileFormat) {
|
||||||
if let inputStreamFormat = playerContext.audioReadingEntry?.audioStreamFormat {
|
if let inputStreamFormat = playerContext.audioReadingEntry?.audioStreamFormat {
|
||||||
createAudioConverter(from: inputStreamFormat, to: outputAudioFormat)
|
createAudioConverter(from: inputStreamFormat, to: outputAudioFormat)
|
||||||
}
|
}
|
||||||
@@ -346,12 +365,12 @@ final class AudioFileStreamProcessor {
|
|||||||
inPacketDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?)
|
inPacketDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?)
|
||||||
{
|
{
|
||||||
guard let entry = playerContext.audioReadingEntry else { return }
|
guard let entry = playerContext.audioReadingEntry else { return }
|
||||||
guard entry.audioStreamState.processedDataFormat, !playerContext.disposedRequested else { return }
|
guard entry.audioStreamState.processedDataFormat else { return }
|
||||||
|
|
||||||
if let playingEntry = playerContext.audioPlayingEntry,
|
if let playingEntry = playerContext.audioPlayingEntry,
|
||||||
playingEntry.seekRequest.requested, playingEntry.calculatedBitrate() > 0
|
playingEntry.seekRequest.requested, playingEntry.calculatedBitrate() > 0
|
||||||
{
|
{
|
||||||
fileStreamCallback?(.proccessSource)
|
fileStreamCallback?(.processSource)
|
||||||
if rendererContext.waiting.value {
|
if rendererContext.waiting.value {
|
||||||
rendererContext.packetsSemaphore.signal()
|
rendererContext.packetsSemaphore.signal()
|
||||||
}
|
}
|
||||||
@@ -375,11 +394,11 @@ final class AudioFileStreamProcessor {
|
|||||||
convertInfo.audioBuffer.mNumberChannels = playingAudioStreamFormat.mChannelsPerFrame
|
convertInfo.audioBuffer.mNumberChannels = playingAudioStreamFormat.mChannelsPerFrame
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProccessedPackets(inPacketDescriptions: inPacketDescriptions,
|
updateProcessedPackets(inPacketDescriptions: inPacketDescriptions,
|
||||||
inNumberPackets: inNumberPackets)
|
inNumberPackets: inNumberPackets)
|
||||||
|
|
||||||
var status: OSStatus = noErr
|
var status: OSStatus = noErr
|
||||||
packetProccess: while status == noErr {
|
packetProcess: while status == noErr {
|
||||||
rendererContext.lock.lock()
|
rendererContext.lock.lock()
|
||||||
let bufferContext = rendererContext.bufferContext
|
let bufferContext = rendererContext.bufferContext
|
||||||
var used = bufferContext.frameUsedCount
|
var used = bufferContext.frameUsedCount
|
||||||
@@ -401,8 +420,7 @@ final class AudioFileStreamProcessor {
|
|||||||
if framesLeftInBuffer > 0 {
|
if framesLeftInBuffer > 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if playerContext.disposedRequested
|
if playerContext.internalState == .disposed
|
||||||
|| playerContext.internalState == .disposed
|
|
||||||
|| playerContext.internalState == .pendingNext
|
|| playerContext.internalState == .pendingNext
|
||||||
|| playerContext.internalState == .stopped
|
|| playerContext.internalState == .stopped
|
||||||
{
|
{
|
||||||
@@ -412,7 +430,7 @@ final class AudioFileStreamProcessor {
|
|||||||
if let playingEntry = playerContext.audioPlayingEntry,
|
if let playingEntry = playerContext.audioPlayingEntry,
|
||||||
playingEntry.seekRequest.requested, playingEntry.calculatedBitrate() > 0
|
playingEntry.seekRequest.requested, playingEntry.calculatedBitrate() > 0
|
||||||
{
|
{
|
||||||
fileStreamCallback?(.proccessSource)
|
fileStreamCallback?(.processSource)
|
||||||
if rendererContext.waiting.value {
|
if rendererContext.waiting.value {
|
||||||
rendererContext.packetsSemaphore.signal()
|
rendererContext.packetsSemaphore.signal()
|
||||||
}
|
}
|
||||||
@@ -457,7 +475,7 @@ final class AudioFileStreamProcessor {
|
|||||||
framesToDecode = start
|
framesToDecode = start
|
||||||
if framesToDecode == 0 {
|
if framesToDecode == 0 {
|
||||||
fillUsedFrames(framesCount: framesAdded)
|
fillUsedFrames(framesCount: framesAdded)
|
||||||
continue packetProccess
|
continue packetProcess
|
||||||
}
|
}
|
||||||
prefillLocalBufferList(bufferList: localBufferList,
|
prefillLocalBufferList(bufferList: localBufferList,
|
||||||
dataOffset: 0,
|
dataOffset: 0,
|
||||||
@@ -475,9 +493,9 @@ final class AudioFileStreamProcessor {
|
|||||||
if status == AudioConvertStatus.done.rawValue {
|
if status == AudioConvertStatus.done.rawValue {
|
||||||
fillUsedFrames(framesCount: framesAdded)
|
fillUsedFrames(framesCount: framesAdded)
|
||||||
return
|
return
|
||||||
} else if status == AudioConvertStatus.proccessed.rawValue {
|
} else if status == AudioConvertStatus.processed.rawValue {
|
||||||
fillUsedFrames(framesCount: framesAdded)
|
fillUsedFrames(framesCount: framesAdded)
|
||||||
continue packetProccess
|
continue packetProcess
|
||||||
} else if status != 0 {
|
} else if status != 0 {
|
||||||
fileStreamCallback?(.raiseError(.codecError))
|
fileStreamCallback?(.raiseError(.codecError))
|
||||||
return
|
return
|
||||||
@@ -502,9 +520,9 @@ final class AudioFileStreamProcessor {
|
|||||||
if status == AudioConvertStatus.done.rawValue {
|
if status == AudioConvertStatus.done.rawValue {
|
||||||
fillUsedFrames(framesCount: framesAdded)
|
fillUsedFrames(framesCount: framesAdded)
|
||||||
return
|
return
|
||||||
} else if status == AudioConvertStatus.proccessed.rawValue {
|
} else if status == AudioConvertStatus.processed.rawValue {
|
||||||
fillUsedFrames(framesCount: framesAdded)
|
fillUsedFrames(framesCount: framesAdded)
|
||||||
continue packetProccess
|
continue packetProcess
|
||||||
} else if status != 0 {
|
} else if status != 0 {
|
||||||
fileStreamCallback?(.raiseError(.codecError))
|
fileStreamCallback?(.raiseError(.codecError))
|
||||||
return
|
return
|
||||||
@@ -545,8 +563,8 @@ final class AudioFileStreamProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@inline(__always)
|
@inline(__always)
|
||||||
private func updateProccessedPackets(inPacketDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?,
|
private func updateProcessedPackets(inPacketDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?,
|
||||||
inNumberPackets: UInt32)
|
inNumberPackets: UInt32)
|
||||||
{
|
{
|
||||||
guard let inPacketDescriptions = inPacketDescriptions else { return }
|
guard let inPacketDescriptions = inPacketDescriptions else { return }
|
||||||
guard let readingEntry = playerContext.audioReadingEntry else { return }
|
guard let readingEntry = playerContext.audioReadingEntry else { return }
|
||||||
@@ -618,12 +636,12 @@ private func _converterCallback(inAudioConverter _: AudioConverterRef,
|
|||||||
ioNumberDataPackets.pointee = convertInfo.pointee.numberOfPackets
|
ioNumberDataPackets.pointee = convertInfo.pointee.numberOfPackets
|
||||||
convertInfo.pointee.done = true
|
convertInfo.pointee.done = true
|
||||||
|
|
||||||
return AudioConvertStatus.proccessed.rawValue
|
return AudioConvertStatus.processed.rawValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: HardwareCodedClass method
|
// MARK: HardwareCodedClass method
|
||||||
|
|
||||||
private func getHardwareCodecClassDescripition(formatId: UInt32, classDesc: UnsafeMutablePointer<AudioClassDescription>) -> Bool {
|
private func getHardwareCodecClassDescription(formatId: UInt32, classDesc: UnsafeMutablePointer<AudioClassDescription>) -> Bool {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
var size: UInt32 = 0
|
var size: UInt32 = 0
|
||||||
let formatIdSize = UInt32(MemoryLayout.size(ofValue: formatId))
|
let formatIdSize = UInt32(MemoryLayout.size(ofValue: formatId))
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ final class AudioPlayerRenderProcessor: NSObject {
|
|||||||
waitForBuffer = true
|
waitForBuffer = true
|
||||||
}
|
}
|
||||||
} else if state == .waitingForDataAfterSeek {
|
} else if state == .waitingForDataAfterSeek {
|
||||||
var requiredFramesToStart: Int = 1024
|
var requiredFramesToStart = 1024
|
||||||
if framesState.lastFrameQueued >= 0 {
|
if framesState.lastFrameQueued >= 0 {
|
||||||
requiredFramesToStart = min(requiredFramesToStart, framesState.lastFrameQueued - framesState.queued)
|
requiredFramesToStart = min(requiredFramesToStart, framesState.lastFrameQueued - framesState.queued)
|
||||||
}
|
}
|
||||||
@@ -109,9 +109,9 @@ final class AudioPlayerRenderProcessor: NSObject {
|
|||||||
bufferList.mBuffers.mDataByteSize = frameSizeInBytes * framesToCopy
|
bufferList.mBuffers.mDataByteSize = frameSizeInBytes * framesToCopy
|
||||||
|
|
||||||
if isMuted {
|
if isMuted {
|
||||||
writeSilence(outputBuffer: &bufferList.mBuffers,
|
if let mData = bufferList.mBuffers.mData {
|
||||||
outputBufferSize: 0,
|
memset(mData, 0, Int(bufferList.mBuffers.mDataByteSize))
|
||||||
offset: Int(bufferList.mBuffers.mDataByteSize))
|
}
|
||||||
} else {
|
} else {
|
||||||
if let mDataBuffer = audioBuffer.mData {
|
if let mDataBuffer = audioBuffer.mData {
|
||||||
memcpy(bufferList.mBuffers.mData,
|
memcpy(bufferList.mBuffers.mData,
|
||||||
@@ -132,9 +132,9 @@ final class AudioPlayerRenderProcessor: NSObject {
|
|||||||
bufferList.mBuffers.mDataByteSize = frameSizeInBytes * frameToCopy
|
bufferList.mBuffers.mDataByteSize = frameSizeInBytes * frameToCopy
|
||||||
|
|
||||||
if isMuted {
|
if isMuted {
|
||||||
writeSilence(outputBuffer: &bufferList.mBuffers,
|
if let mData = bufferList.mBuffers.mData {
|
||||||
outputBufferSize: 0,
|
memset(mData, 0, Int(bufferList.mBuffers.mDataByteSize))
|
||||||
offset: Int(bufferList.mBuffers.mDataByteSize))
|
}
|
||||||
} else {
|
} else {
|
||||||
if let mDataBuffer = audioBuffer.mData {
|
if let mDataBuffer = audioBuffer.mData {
|
||||||
memcpy(bufferList.mBuffers.mData,
|
memcpy(bufferList.mBuffers.mData,
|
||||||
@@ -151,9 +151,7 @@ final class AudioPlayerRenderProcessor: NSObject {
|
|||||||
bufferList.mBuffers.mDataByteSize += frameSizeInBytes * moreFramesToCopy
|
bufferList.mBuffers.mDataByteSize += frameSizeInBytes * moreFramesToCopy
|
||||||
if let ioBufferData = bufferList.mBuffers.mData {
|
if let ioBufferData = bufferList.mBuffers.mData {
|
||||||
if isMuted {
|
if isMuted {
|
||||||
writeSilence(outputBuffer: &bufferList.mBuffers,
|
memset(ioBufferData + Int(frameToCopy * frameSizeInBytes), 0, Int(frameSizeInBytes * moreFramesToCopy))
|
||||||
outputBufferSize: Int(frameSizeInBytes * moreFramesToCopy),
|
|
||||||
offset: Int(frameToCopy * frameSizeInBytes))
|
|
||||||
} else {
|
} else {
|
||||||
if let mDataBuffer = audioBuffer.mData {
|
if let mDataBuffer = audioBuffer.mData {
|
||||||
memcpy(ioBufferData + Int(frameToCopy * frameSizeInBytes),
|
memcpy(ioBufferData + Int(frameToCopy * frameSizeInBytes),
|
||||||
@@ -179,9 +177,9 @@ final class AudioPlayerRenderProcessor: NSObject {
|
|||||||
|
|
||||||
if totalFramesCopied < inNumberFrames {
|
if totalFramesCopied < inNumberFrames {
|
||||||
let delta = inNumberFrames - totalFramesCopied
|
let delta = inNumberFrames - totalFramesCopied
|
||||||
writeSilence(outputBuffer: &bufferList.mBuffers,
|
if let mData = bufferList.mBuffers.mData {
|
||||||
outputBufferSize: Int(delta * frameSizeInBytes),
|
memset(mData + Int(totalFramesCopied * frameSizeInBytes), 0, Int(delta * frameSizeInBytes))
|
||||||
offset: Int(totalFramesCopied * frameSizeInBytes))
|
}
|
||||||
|
|
||||||
if playingEntry != nil || AudioPlayer.InternalState.waiting.contains(state) {
|
if playingEntry != nil || AudioPlayer.InternalState.waiting.contains(state) {
|
||||||
if playerContext.internalState != .rebuffering {
|
if playerContext.internalState != .rebuffering {
|
||||||
@@ -198,7 +196,6 @@ final class AudioPlayerRenderProcessor: NSObject {
|
|||||||
state.contains(.running) && state != .playing
|
state.contains(.running) && state != .playing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rendererContext.waitingForDataAfterSeekFrameCount.write { $0 = 0 }
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
rendererContext.waitingForDataAfterSeekFrameCount.write { $0 = 0 }
|
rendererContext.waitingForDataAfterSeekFrameCount.write { $0 = 0 }
|
||||||
@@ -211,7 +208,7 @@ final class AudioPlayerRenderProcessor: NSObject {
|
|||||||
}
|
}
|
||||||
currentPlayingEntry.lock.lock()
|
currentPlayingEntry.lock.lock()
|
||||||
|
|
||||||
var extraFramesPlayedNotAssigned: Int = 0
|
var extraFramesPlayedNotAssigned = 0
|
||||||
var framesPlayedForCurrent = Int(totalFramesCopied)
|
var framesPlayedForCurrent = Int(totalFramesCopied)
|
||||||
|
|
||||||
if currentPlayingEntry.framesState.lastFrameQueued >= 0 {
|
if currentPlayingEntry.framesState.lastFrameQueued >= 0 {
|
||||||
@@ -320,15 +317,4 @@ final class AudioPlayerRenderProcessor: NSObject {
|
|||||||
guard inputBusNumber == 0 else { return noErr }
|
guard inputBusNumber == 0 else { return noErr }
|
||||||
return render(inNumberFrames: inNumberFrames, ioData: inputData, flags: flags)
|
return render(inNumberFrames: inNumberFrames, ioData: inputData, flags: flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
@inline(__always)
|
|
||||||
private func writeSilence(outputBuffer: inout AudioBuffer,
|
|
||||||
outputBufferSize: Int,
|
|
||||||
offset: Int)
|
|
||||||
{
|
|
||||||
guard let mData = outputBuffer.mData else { return }
|
|
||||||
memset(mData + offset, 0, outputBufferSize)
|
|
||||||
outputBuffer.mDataByteSize = UInt32(outputBufferSize)
|
|
||||||
outputBuffer.mNumberChannels = outputAudioFormat.mChannelsPerFrame
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ public struct FilterEntry: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public protocol FrameFiltering {
|
public protocol FrameFiltering {
|
||||||
|
|
||||||
/// A Boolean value indicating whether there are filter entries
|
/// A Boolean value indicating whether there are filter entries
|
||||||
var hasEntries: Bool { get }
|
var hasEntries: Bool { get }
|
||||||
|
|
||||||
@@ -50,21 +49,21 @@ public protocol FrameFiltering {
|
|||||||
/// Adds a filter entry with the given parameters
|
/// Adds a filter entry with the given parameters
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - named: The name of the entry to be added
|
/// - named: The name of the entry to be added
|
||||||
/// - filter: The block for the filter hanlding
|
/// - filter: The block for the filter handling
|
||||||
func add(entry named: String, filter: @escaping FilterCallback)
|
func add(entry named: String, filter: @escaping FilterCallback)
|
||||||
|
|
||||||
/// Adds a filter entry with the given parameters
|
/// Adds a filter entry with the given parameters
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - name: The name for the new entry
|
/// - name: The name for the new entry
|
||||||
/// - filterName: The name of a previously added filters
|
/// - filterName: The name of a previously added filters
|
||||||
/// - filter: The block for the filter hanlding
|
/// - filter: The block for the filter handling
|
||||||
func add(entry named: String, after filterName: String, filter: @escaping FilterCallback)
|
func add(entry named: String, after filterName: String, filter: @escaping FilterCallback)
|
||||||
|
|
||||||
/// Removes a filter entry
|
/// Removes a filter entry
|
||||||
/// - Parameter entry: An instance of `FilterEntry` to be removed
|
/// - Parameter entry: An instance of `FilterEntry` to be removed
|
||||||
func remove(entry: FilterEntry)
|
func remove(entry: FilterEntry)
|
||||||
|
|
||||||
/// Attemps to remove a filter entry by its name
|
/// Attempts to remove a filter entry by its name
|
||||||
/// - Parameter named: A `String` representing the name of the filter entry
|
/// - Parameter named: A `String` representing the name of the filter entry
|
||||||
func remove(entry named: String)
|
func remove(entry named: String)
|
||||||
|
|
||||||
@@ -73,21 +72,21 @@ public protocol FrameFiltering {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class FrameFilterProcessor: NSObject, FrameFiltering {
|
final class FrameFilterProcessor: NSObject, FrameFiltering {
|
||||||
|
|
||||||
public var hasEntries: Bool {
|
public var hasEntries: Bool {
|
||||||
lock.lock(); defer { lock.unlock() }
|
lock.lock(); defer { lock.unlock() }
|
||||||
return !entries.isEmpty
|
return !entries.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
private let lock = UnfairLock()
|
private let lock = UnfairLock()
|
||||||
private let mixerNode: AVAudioMixerNode
|
private let mixerNodeProvider: () -> AVAudioMixerNode
|
||||||
|
private lazy var mixerNode: AVAudioMixerNode = mixerNodeProvider()
|
||||||
|
|
||||||
private(set) var entries: [FilterEntry] = []
|
private(set) var entries: [FilterEntry] = []
|
||||||
|
|
||||||
private var hasInstalledTap: Bool = false
|
private var hasInstalledTap: Bool = false
|
||||||
|
|
||||||
init(mixerNode: AVAudioMixerNode) {
|
init(mixerNodeProvider: @escaping (() -> AVAudioMixerNode)) {
|
||||||
self.mixerNode = mixerNode
|
self.mixerNodeProvider = mixerNodeProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
public func add(entry: FilterEntry) {
|
public func add(entry: FilterEntry) {
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import Foundation
|
|||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
final class IcycastHeadersProcessor {
|
final class IcycastHeadersProcessor {
|
||||||
|
|
||||||
private var icecastHeaders = Data(capacity: 1024)
|
private var icecastHeaders = Data(capacity: 1024)
|
||||||
private var searchComplete = false
|
private var searchComplete = false
|
||||||
private var iceHeaderAvailable = false
|
private var iceHeaderAvailable = false
|
||||||
@@ -38,31 +37,32 @@ final class IcycastHeadersProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@inline(__always)
|
@inline(__always)
|
||||||
func proccess(data: Data) -> (Data?, Data) {
|
func process(data: Data) -> (Data?, Data) {
|
||||||
let stopProccessingCheckOne: [UInt8] = Array("\n\n".utf8)
|
let stopProcessingCheckOne: [UInt8] = Array("\n\n".utf8)
|
||||||
let stopProccessingCheckTwo: [UInt8] = Array("\r\n\r\n".utf8)
|
let stopProcessingCheckTwo: [UInt8] = Array("\r\n\r\n".utf8)
|
||||||
let icyPrefix: [UInt8] = Array("ICY ".utf8)
|
let icyPrefix: [UInt8] = Array("ICY ".utf8)
|
||||||
let httpPrefix: [UInt8] = Array("HTTP".utf8)
|
let httpPrefix: [UInt8] = Array("HTTP".utf8)
|
||||||
return data.withUnsafeBytes { buffer -> (Data?, Data) in
|
return data.withUnsafeBytes { buffer -> (Data?, Data) in
|
||||||
|
guard !buffer.isEmpty else { return (nil, data) }
|
||||||
var bytesRead = 0
|
var bytesRead = 0
|
||||||
let bytes = buffer.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
let bytes = buffer.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||||
// Read through the bytes and stop when our search is complete
|
// Read through the bytes and stop when our search is complete
|
||||||
// Since we don't know the amount of bytes to be proccessed
|
// Since we don't know the amount of bytes to be processed
|
||||||
// we add each character up until we found on of the checks as defined above.
|
// we add each character up until we found on of the checks as defined above.
|
||||||
while bytesRead < buffer.count, !searchComplete {
|
while bytesRead < buffer.count, !searchComplete {
|
||||||
let pointer = bytes + bytesRead
|
let pointer = bytes + bytesRead
|
||||||
icecastHeaders.append(pointer, count: 1)
|
icecastHeaders.append(pointer, count: 1)
|
||||||
|
|
||||||
if icecastHeaders.count >= stopProccessingCheckOne.count {
|
if icecastHeaders.count >= stopProcessingCheckOne.count {
|
||||||
if icecastHeaders.suffix(stopProccessingCheckOne.count) == stopProccessingCheckOne {
|
if icecastHeaders.suffix(stopProcessingCheckOne.count) == stopProcessingCheckOne {
|
||||||
iceHeaderAvailable = true
|
iceHeaderAvailable = true
|
||||||
searchComplete = true
|
searchComplete = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if icecastHeaders.count >= stopProccessingCheckTwo.count {
|
if icecastHeaders.count >= stopProcessingCheckTwo.count {
|
||||||
if icecastHeaders.suffix(stopProccessingCheckTwo.count) == stopProccessingCheckTwo {
|
if icecastHeaders.suffix(stopProcessingCheckTwo.count) == stopProcessingCheckTwo {
|
||||||
iceHeaderAvailable = true
|
iceHeaderAvailable = true
|
||||||
searchComplete = true
|
searchComplete = true
|
||||||
break
|
break
|
||||||
@@ -71,8 +71,9 @@ final class IcycastHeadersProcessor {
|
|||||||
|
|
||||||
if icecastHeaders.count >= icyPrefix.count {
|
if icecastHeaders.count >= icyPrefix.count {
|
||||||
// in case the first 4 chars are not "ICY " nor "HTTP" then we stop the flow
|
// in case the first 4 chars are not "ICY " nor "HTTP" then we stop the flow
|
||||||
if icecastHeaders[..<icyPrefix.count].elementsEqual(icyPrefix) == false &&
|
if icecastHeaders[..<icyPrefix.count].elementsEqual(icyPrefix) == false,
|
||||||
icecastHeaders[..<httpPrefix.count].elementsEqual(httpPrefix) == false {
|
icecastHeaders[..<httpPrefix.count].elementsEqual(httpPrefix) == false
|
||||||
|
{
|
||||||
iceHeaderAvailable = false
|
iceHeaderAvailable = false
|
||||||
searchComplete = true
|
searchComplete = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,16 +12,16 @@ protocol MetadataStreamSourceDelegate: AnyObject {
|
|||||||
protocol MetadataStreamSource {
|
protocol MetadataStreamSource {
|
||||||
var delegate: MetadataStreamSourceDelegate? { get set }
|
var delegate: MetadataStreamSourceDelegate? { get set }
|
||||||
|
|
||||||
/// Returns `true` when the stream header has indicated that we can proccess metadata, otherwise `false`.
|
/// Returns `true` when the stream header has indicated that we can process metadata, otherwise `false`.
|
||||||
var canProccessMetadata: Bool { get }
|
var canProcessMetadata: Bool { get }
|
||||||
|
|
||||||
/// Assigns the metadata step of the metadata
|
/// Assigns the metadata step of the metadata
|
||||||
func metadataAvailable(step: Int)
|
func metadataAvailable(step: Int)
|
||||||
|
|
||||||
/// Proccess the received data and extract the metadata if any, returns audio data only.
|
/// Process the received data and extract the metadata if any, returns audio data only.
|
||||||
/// - parameter data: A `Data` object for parsing any metadata
|
/// - parameter data: A `Data` object for parsing any metadata
|
||||||
/// - returns: The extracted audio `Data`
|
/// - returns: The extracted audio `Data`
|
||||||
func proccessMetadata(data: Data) -> Data
|
func processMetadata(data: Data) -> Data
|
||||||
|
|
||||||
/// Resets the processor
|
/// Resets the processor
|
||||||
func reset()
|
func reset()
|
||||||
@@ -44,7 +44,7 @@ protocol MetadataStreamSource {
|
|||||||
final class MetadataStreamProcessor: MetadataStreamSource {
|
final class MetadataStreamProcessor: MetadataStreamSource {
|
||||||
weak var delegate: MetadataStreamSourceDelegate?
|
weak var delegate: MetadataStreamSourceDelegate?
|
||||||
|
|
||||||
var canProccessMetadata: Bool {
|
var canProcessMetadata: Bool {
|
||||||
return metadataStep > 0
|
return metadataStep > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,10 +73,10 @@ final class MetadataStreamProcessor: MetadataStreamSource {
|
|||||||
audioDataBytesRead = 0
|
audioDataBytesRead = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Proccess Metadata
|
// MARK: Process Metadata
|
||||||
|
|
||||||
@inline(__always)
|
@inline(__always)
|
||||||
func proccessMetadata(data: Data) -> Data {
|
func processMetadata(data: Data) -> Data {
|
||||||
data.withUnsafeBytes { buffer -> Data in
|
data.withUnsafeBytes { buffer -> Data in
|
||||||
guard !buffer.isEmpty else { return data }
|
guard !buffer.isEmpty else { return data }
|
||||||
var audioData = Data()
|
var audioData = Data()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import AudioToolbox
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// mapping from mime types to `AudioFileTypeID`
|
/// mapping from mime types to `AudioFileTypeID`
|
||||||
internal let fileTypesFromMimeType: [String: AudioFileTypeID] =
|
let fileTypesFromMimeType: [String: AudioFileTypeID] =
|
||||||
[
|
[
|
||||||
"audio/mp3": kAudioFileMP3Type,
|
"audio/mp3": kAudioFileMP3Type,
|
||||||
"audio/mpg": kAudioFileMP3Type,
|
"audio/mpg": kAudioFileMP3Type,
|
||||||
@@ -44,7 +44,7 @@ func audioFileType(mimeType: String) -> AudioFileTypeID {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// mapping from file extension to `AudioFileTypeID`
|
/// mapping from file extension to `AudioFileTypeID`
|
||||||
internal let fileTypesFromFileExtension: [String: AudioFileTypeID] =
|
let fileTypesFromFileExtension: [String: AudioFileTypeID] =
|
||||||
[
|
[
|
||||||
"mp3": kAudioFileMP3Type,
|
"mp3": kAudioFileMP3Type,
|
||||||
"wav": kAudioFileWAVEType,
|
"wav": kAudioFileWAVEType,
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ final class PlayerQueueEntries {
|
|||||||
|
|
||||||
/// Returns `true` when both underlying entries are empty
|
/// Returns `true` when both underlying entries are empty
|
||||||
var isEmpty: Bool {
|
var isEmpty: Bool {
|
||||||
lock.around {
|
lock.withLock {
|
||||||
bufferring.isEmpty && upcoming.isEmpty
|
bufferring.isEmpty && upcoming.isEmpty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the count of both underlying entries
|
/// Returns the count of both underlying entries
|
||||||
var count: Int {
|
var count: Int {
|
||||||
lock.around {
|
lock.withLock {
|
||||||
bufferring.count + upcoming.count
|
bufferring.count + upcoming.count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,6 +34,17 @@ final class PlayerQueueEntries {
|
|||||||
upcoming = Queue<AudioEntry>()
|
upcoming = Queue<AudioEntry>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns an array containing all items in the queue for the specified `type`.
|
||||||
|
///
|
||||||
|
/// - Note: This method returns the items in the queue without removing them.
|
||||||
|
///
|
||||||
|
/// - Parameter type: A `PlayerQueueType` specifying the type of the queue.
|
||||||
|
/// - Returns: An array of `AudioEntry` objects representing the items in the queue.
|
||||||
|
func items(type: PlayerQueueType) -> [AudioEntry] {
|
||||||
|
lock.lock(); defer { lock.unlock() }
|
||||||
|
return queue(for: type).items
|
||||||
|
}
|
||||||
|
|
||||||
/// Adds the `item` to the underlying queue for the specified `type`
|
/// Adds the `item` to the underlying queue for the specified `type`
|
||||||
/// - parameter item: An `AudioEntry` object to be added
|
/// - parameter item: An `AudioEntry` object to be added
|
||||||
/// - parameter type: The type fo the underlying queue as expressed by `PlayerQueueType`
|
/// - parameter type: The type fo the underlying queue as expressed by `PlayerQueueType`
|
||||||
@@ -51,6 +62,32 @@ final class PlayerQueueEntries {
|
|||||||
return queue(for: type).dequeue()
|
return queue(for: type).dequeue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func insert(item: AudioEntry, type: PlayerQueueType, after afterItem: AudioEntry) {
|
||||||
|
lock.lock(); defer { lock.unlock() }
|
||||||
|
if let indexForAfterItem = queue(for: type).items.firstIndex(of: afterItem) {
|
||||||
|
queue(for: .upcoming).insert(item: item, at: indexForAfterItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts the `item` at the specified index in the underlying queue for the specified `type`.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - item: An `AudioEntry` object to be added.
|
||||||
|
/// - type: The type of the underlying queue as expressed by `PlayerQueueType`.
|
||||||
|
/// - index: The index at which to insert the item.
|
||||||
|
func insert(item: AudioEntry, type: PlayerQueueType, at index: Int) {
|
||||||
|
lock.lock(); defer { lock.unlock() }
|
||||||
|
queue(for: type).insert(item: item, at: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the item at the specified index from the underlying queue for the specified `type`.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - type: The type of the underlying queue as expressed by `PlayerQueueType`.
|
||||||
|
/// - index: The index of the item to remove.
|
||||||
|
func remove(item: AudioEntry, type: PlayerQueueType) {
|
||||||
|
lock.lock(); defer { lock.unlock() }
|
||||||
|
queue(for: type).remove(item: item)
|
||||||
|
}
|
||||||
|
|
||||||
/// Appends (skips) the `items` to the underlying queue for the specified `type`
|
/// Appends (skips) the `items` to the underlying queue for the specified `type`
|
||||||
/// - parameter item: An `AudioEntry` object to be added
|
/// - parameter item: An `AudioEntry` object to be added
|
||||||
/// - parameter type: The type fo the underlying queue as expressed by `PlayerQueueType`
|
/// - parameter type: The type fo the underlying queue as expressed by `PlayerQueueType`
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import AudioToolbox.AudioFile
|
import AudioToolbox.AudioFile
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct HeaderField {
|
enum HeaderField {
|
||||||
public static let acceptRanges = "Accept-Ranges"
|
public static let acceptRanges = "Accept-Ranges"
|
||||||
public static let contentLength = "Content-Length"
|
public static let contentLength = "Content-Length"
|
||||||
public static let contentType = "Content-Type"
|
public static let contentType = "Content-Type"
|
||||||
@@ -14,7 +14,7 @@ struct HeaderField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum IcyHeaderField {
|
enum IcyHeaderField {
|
||||||
public static let icyMentaint = "icy-metaint"
|
public static let icyMetaint = "icy-metaint"
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HTTPHeaderParserOutput {
|
struct HTTPHeaderParserOutput {
|
||||||
@@ -22,6 +22,11 @@ struct HTTPHeaderParserOutput {
|
|||||||
let typeId: AudioFileTypeID
|
let typeId: AudioFileTypeID
|
||||||
// Metadata Support
|
// Metadata Support
|
||||||
let metadataStep: Int
|
let metadataStep: Int
|
||||||
|
let seekable: Bool
|
||||||
|
|
||||||
|
var isMp4: Bool {
|
||||||
|
(typeId == kAudioFileMPEG4Type || typeId == kAudioFileM4AType)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol HTTPHeaderParsing: Parser {
|
protocol HTTPHeaderParsing: Parser {
|
||||||
@@ -46,7 +51,7 @@ struct HTTPHeaderParser: HTTPHeaderParsing {
|
|||||||
typeId = audioFileType(mimeType: contentType)
|
typeId = audioFileType(mimeType: contentType)
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileLength: Int = 0
|
var fileLength = 0
|
||||||
if input.statusCode == 200 {
|
if input.statusCode == 200 {
|
||||||
let contentLength = value(forHTTPHeaderField: HeaderField.contentLength, in: input)
|
let contentLength = value(forHTTPHeaderField: HeaderField.contentLength, in: input)
|
||||||
if let contentLength = contentLength, let length = Int(contentLength) {
|
if let contentLength = contentLength, let length = Int(contentLength) {
|
||||||
@@ -64,15 +69,18 @@ struct HTTPHeaderParser: HTTPHeaderParsing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var metadataStep = 0
|
var metadataStep = 0
|
||||||
if let icyMetaint = value(forHTTPHeaderField: IcyHeaderField.icyMentaint, in: input),
|
if let icyMetaint = value(forHTTPHeaderField: IcyHeaderField.icyMetaint, in: input),
|
||||||
let intValue = Int(icyMetaint)
|
let intValue = Int(icyMetaint)
|
||||||
{
|
{
|
||||||
metadataStep = intValue
|
metadataStep = intValue
|
||||||
}
|
}
|
||||||
|
|
||||||
return HTTPHeaderParserOutput(fileLength: fileLength,
|
return HTTPHeaderParserOutput(
|
||||||
typeId: typeId,
|
fileLength: fileLength,
|
||||||
metadataStep: metadataStep)
|
typeId: typeId,
|
||||||
|
metadataStep: metadataStep,
|
||||||
|
seekable: input.statusCode == 206
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,10 +99,8 @@ extension Parser where Self: HTTPHeaderParsing {
|
|||||||
|
|
||||||
private func valueForCaseInsensitiveKey(_ key: String, fields: [String: String]) -> String? {
|
private func valueForCaseInsensitiveKey(_ key: String, fields: [String: String]) -> String? {
|
||||||
let keyToBeFound = key.lowercased()
|
let keyToBeFound = key.lowercased()
|
||||||
for (key, value) in fields {
|
for (key, value) in fields where key.lowercased() == keyToBeFound {
|
||||||
if key.lowercased() == keyToBeFound {
|
return value
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct IcycastHeaderParser: Parser {
|
struct IcycastHeaderParser: Parser {
|
||||||
|
|
||||||
func parse(input: Data) -> HTTPHeaderParserOutput? {
|
func parse(input: Data) -> HTTPHeaderParserOutput? {
|
||||||
|
|
||||||
guard let icecastValue = String(data: input, encoding: .utf8) else {
|
guard let icecastValue = String(data: input, encoding: .utf8) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -23,12 +21,15 @@ struct IcycastHeaderParser: Parser {
|
|||||||
result[String(key)] = String(value)
|
result[String(key)] = String(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let metadataStep = Int(result[IcyHeaderField.icyMentaint] ?? "") ?? 0
|
let metadataStep = Int(result[IcyHeaderField.icyMetaint] ?? "") ?? 0
|
||||||
let contentType = result[HeaderField.contentType.lowercased()] ?? "audio/mpeg"
|
let contentType = result[HeaderField.contentType.lowercased()] ?? "audio/mpeg"
|
||||||
let typeId = audioFileType(mimeType: contentType)
|
let typeId = audioFileType(mimeType: contentType)
|
||||||
|
|
||||||
return HTTPHeaderParserOutput(fileLength: 0,
|
return HTTPHeaderParserOutput(
|
||||||
typeId: typeId,
|
fileLength: 0,
|
||||||
metadataStep: metadataStep)
|
typeId: typeId,
|
||||||
|
metadataStep: metadataStep,
|
||||||
|
seekable: false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,13 +18,17 @@ struct MetadataParser: Parser {
|
|||||||
|
|
||||||
func parse(input: Data) -> MetadataOutput {
|
func parse(input: Data) -> MetadataOutput {
|
||||||
guard let string = String(data: input, encoding: .utf8) else { return .failure(.unableToParse) }
|
guard let string = String(data: input, encoding: .utf8) else { return .failure(.unableToParse) }
|
||||||
// remove added bytes (zeros) and seperate the string on every ';' char
|
// remove added bytes (zeros) and separate the string on every ';' char
|
||||||
let pairs = string.trimmingCharacters(in: CharacterSet(charactersIn: "\0")).components(separatedBy: ";")
|
let pairs = string.trimmingCharacters(in: CharacterSet(charactersIn: "\0")).components(separatedBy: ";")
|
||||||
let temp: [String: String] = [:]
|
let metadata = pairs.reduce(into: [String: String]()) { result, next in
|
||||||
let metadata = pairs.reduce(into: temp) { result, next in
|
let split = next.split(
|
||||||
let paired = next.components(separatedBy: "=")
|
separator: "=",
|
||||||
if let key = paired.first,
|
maxSplits: 1,
|
||||||
let value = paired.last?.replacingOccurrences(of: "'", with: ""), !key.isEmpty
|
omittingEmptySubsequences: true
|
||||||
|
)
|
||||||
|
.map(String.init)
|
||||||
|
if let key = split.first,
|
||||||
|
let value = split.last?.replacingOccurrences(of: "'", with: ""), !key.isEmpty
|
||||||
{
|
{
|
||||||
result[key] = value
|
result[key] = value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"configurations" : [
|
|
||||||
{
|
|
||||||
"id" : "A1B13C01-AF5C-46DD-990A-A369639F2AD3",
|
|
||||||
"name" : "Configuration 1",
|
|
||||||
"options" : {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"defaultOptions" : {
|
|
||||||
"environmentVariableEntries" : [
|
|
||||||
{
|
|
||||||
"enabled" : false,
|
|
||||||
"key" : "OS_ACTIVITY_MODE",
|
|
||||||
"value" : "disable"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"targetForVariableExpansion" : {
|
|
||||||
"containerPath" : "container:AudioExample.xcodeproj",
|
|
||||||
"identifier" : "B5AEDBD02475274C007D8101",
|
|
||||||
"name" : "AudioExample"
|
|
||||||
},
|
|
||||||
"testExecutionOrdering" : "random",
|
|
||||||
"threadSanitizerEnabled" : true
|
|
||||||
},
|
|
||||||
"testTargets" : [
|
|
||||||
{
|
|
||||||
"parallelizable" : true,
|
|
||||||
"skippedTests" : [
|
|
||||||
"ProtectedTests"
|
|
||||||
],
|
|
||||||
"target" : {
|
|
||||||
"containerPath" : "container:..\/AudioStreaming.xcodeproj",
|
|
||||||
"identifier" : "B5AEDBB624744153007D8101",
|
|
||||||
"name" : "AudioStreamingTests"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
+8
-8
@@ -7,27 +7,27 @@ import XCTest
|
|||||||
|
|
||||||
@testable import AudioStreaming
|
@testable import AudioStreaming
|
||||||
|
|
||||||
class ProtectedTests: XCTestCase {
|
class AtomicTests: XCTestCase {
|
||||||
func testProtectedValuesAreAccessedSafely() {
|
func testProtectedValuesAreAccessedSafely() {
|
||||||
measure {
|
measure {
|
||||||
let protected = Protected<Int>(0)
|
let atomic = Atomic<Int>(0)
|
||||||
|
|
||||||
DispatchQueue.concurrentPerform(iterations: 1_000_000) { _ in
|
DispatchQueue.concurrentPerform(iterations: 100_000) { _ in
|
||||||
_ = protected.value
|
_ = atomic.value
|
||||||
protected.write { $0 += 1 }
|
atomic.write { $0 += 1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
XCTAssertEqual(protected.value, 1_000_000)
|
XCTAssertEqual(atomic.value, 100_000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testThatProtectedReadAndWriteAreSafe() {
|
func testThatProtectedReadAndWriteAreSafe() {
|
||||||
measure {
|
measure {
|
||||||
let initialValue = "aValue"
|
let initialValue = "aValue"
|
||||||
let protected = Protected<String>(initialValue)
|
let protected = Atomic<String>(initialValue)
|
||||||
|
|
||||||
DispatchQueue.concurrentPerform(iterations: 1000) { i in
|
DispatchQueue.concurrentPerform(iterations: 1000) { i in
|
||||||
_ = protected.read { $0 }
|
_ = protected.value
|
||||||
protected.write { $0 = "\(i)" }
|
protected.write { $0 = "\(i)" }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// BiMap.swift
|
// BiMapTests.swift
|
||||||
// AudioStreamingTests
|
// AudioStreamingTests
|
||||||
//
|
//
|
||||||
// Created by Dimitrios Chatzieleftheriou on 26/05/2020.
|
// Created by Dimitrios Chatzieleftheriou on 26/05/2020.
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
//
|
||||||
|
// Copyright © Blockchain Luxembourg S.A. All rights reserved.
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import AudioStreaming
|
||||||
|
|
||||||
|
final class ByteBufferTests: XCTestCase {
|
||||||
|
func testWriteAndReadBytes() {
|
||||||
|
var buffer = ByteBuffer(size: 10)
|
||||||
|
|
||||||
|
// Write bytes to the buffer
|
||||||
|
let testData = Data([0x01, 0x02, 0x03, 0x04])
|
||||||
|
buffer.writeBytes(testData)
|
||||||
|
buffer.rewind()
|
||||||
|
|
||||||
|
// Read the written bytes
|
||||||
|
do {
|
||||||
|
let readData = try buffer.readBytes(4)
|
||||||
|
XCTAssertEqual(readData, testData)
|
||||||
|
} catch {
|
||||||
|
XCTFail("Error reading bytes: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWriteAndReadInteger() {
|
||||||
|
var buffer = ByteBuffer(size: 8)
|
||||||
|
|
||||||
|
// Write integer to the buffer
|
||||||
|
let testInteger: UInt32 = 123_456_789
|
||||||
|
buffer.put(testInteger)
|
||||||
|
buffer.rewind()
|
||||||
|
|
||||||
|
// Read the written integer
|
||||||
|
do {
|
||||||
|
let readInteger: UInt32 = try buffer.getInteger()
|
||||||
|
XCTAssertEqual(readInteger, testInteger.bigEndian)
|
||||||
|
} catch {
|
||||||
|
XCTFail("Error reading integer: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWriteAndReadFloat() {
|
||||||
|
var buffer = ByteBuffer(size: 8)
|
||||||
|
|
||||||
|
// Write float to the buffer
|
||||||
|
let testFloat: Float = 123.456
|
||||||
|
buffer.put(testFloat)
|
||||||
|
buffer.rewind()
|
||||||
|
|
||||||
|
// Read the written float
|
||||||
|
do {
|
||||||
|
let readFloat: Float = try buffer.getFloat()
|
||||||
|
XCTAssertEqual(readFloat, testFloat, accuracy: 0.001)
|
||||||
|
} catch {
|
||||||
|
XCTFail("Error reading float: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWriteAndReadDouble() {
|
||||||
|
var buffer = ByteBuffer(size: 8)
|
||||||
|
|
||||||
|
// Write double to the buffer
|
||||||
|
let testDouble = 123.456
|
||||||
|
buffer.put(testDouble)
|
||||||
|
buffer.rewind()
|
||||||
|
|
||||||
|
// Read the written double
|
||||||
|
do {
|
||||||
|
let readDouble: Double = try buffer.getDouble()
|
||||||
|
XCTAssertEqual(readDouble, testDouble, accuracy: 0.001)
|
||||||
|
} catch {
|
||||||
|
XCTFail("Error reading double: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// DispatchReadSourceTests.swift
|
// DispatchTimerSourceTests.swift
|
||||||
// AudioStreamingTests
|
// AudioStreamingTests
|
||||||
//
|
//
|
||||||
// Created by Dimitrios Chatzieleftheriou on 25/10/2020.
|
// Created by Dimitrios Chatzieleftheriou on 25/10/2020.
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
@testable import AudioStreaming
|
@testable import AudioStreaming
|
||||||
|
|
||||||
class NetworkingClientTests: XCTestCase {
|
class NetworkingClientTests: XCTestCase {
|
||||||
|
|||||||
@@ -83,4 +83,27 @@ class QueueTests: XCTestCase {
|
|||||||
queue.removeAll()
|
queue.removeAll()
|
||||||
XCTAssertTrue(queue.isEmpty)
|
XCTAssertTrue(queue.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testInsertingAtSpecificIndex() {
|
||||||
|
let queue = Queue<Int>()
|
||||||
|
queue.enqueue(item: 1)
|
||||||
|
queue.enqueue(item: 2)
|
||||||
|
queue.enqueue(item: 3)
|
||||||
|
|
||||||
|
queue.insert(item: 6, at: 1)
|
||||||
|
|
||||||
|
XCTAssertEqual(queue.count, 4)
|
||||||
|
XCTAssertEqual(queue.remove(at: 1), 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRemovingAtSpecificIndex() {
|
||||||
|
let queue = Queue<Int>()
|
||||||
|
queue.enqueue(item: 1)
|
||||||
|
queue.enqueue(item: 2)
|
||||||
|
queue.enqueue(item: 3)
|
||||||
|
|
||||||
|
XCTAssertEqual(queue.remove(at: 1), 2)
|
||||||
|
|
||||||
|
XCTAssertEqual(queue.count, 2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-12
@@ -13,28 +13,29 @@ import XCTest
|
|||||||
class MetadataStreamProcessorTests: XCTestCase {
|
class MetadataStreamProcessorTests: XCTestCase {
|
||||||
var metadataDelegateSpy = MetadataDelegateSpy()
|
var metadataDelegateSpy = MetadataDelegateSpy()
|
||||||
|
|
||||||
|
let bundle = Bundle(for: MetadataStreamProcessorTests.self)
|
||||||
|
|
||||||
func test_Processor_SendsCorrectValues_IfItCanProcessMetadata() throws {
|
func test_Processor_SendsCorrectValues_IfItCanProcessMetadata() throws {
|
||||||
let parser = MetadataParser()
|
let parser = MetadataParser()
|
||||||
let processor = MetadataStreamProcessor(parser: parser.eraseToAnyParser())
|
let processor = MetadataStreamProcessor(parser: parser.eraseToAnyParser())
|
||||||
|
|
||||||
// without calling `metadataAvailable(step:)` it should be false
|
// without calling `metadataAvailable(step:)` it should be false
|
||||||
XCTAssertFalse(processor.canProccessMetadata)
|
XCTAssertFalse(processor.canProcessMetadata)
|
||||||
|
|
||||||
// calling `metadataAvailable(step:)` with zero
|
// calling `metadataAvailable(step:)` with zero
|
||||||
processor.metadataAvailable(step: 0)
|
processor.metadataAvailable(step: 0)
|
||||||
|
|
||||||
// it should be false
|
// it should be false
|
||||||
XCTAssertFalse(processor.canProccessMetadata)
|
XCTAssertFalse(processor.canProcessMetadata)
|
||||||
|
|
||||||
// calling `metadataAvailable(step:)` with greater zero
|
// calling `metadataAvailable(step:)` with greater zero
|
||||||
processor.metadataAvailable(step: 1)
|
processor.metadataAvailable(step: 1)
|
||||||
|
|
||||||
// it should be true
|
// it should be true
|
||||||
XCTAssertTrue(processor.canProccessMetadata)
|
XCTAssertTrue(processor.canProcessMetadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_Processor_Outputs_Correct_Metadata_ForStep_WithEmptyMetadata() throws {
|
func test_Processor_Outputs_Correct_Metadata_ForStep_WithEmptyMetadata() throws {
|
||||||
let bundle = Bundle(for: MetadataStreamProcessorTests.self)
|
|
||||||
let url = bundle.url(forResource: "raw-stream-audio-empty-metadata", withExtension: nil)!
|
let url = bundle.url(forResource: "raw-stream-audio-empty-metadata", withExtension: nil)!
|
||||||
|
|
||||||
let data = try Data(contentsOf: url)
|
let data = try Data(contentsOf: url)
|
||||||
@@ -45,7 +46,7 @@ class MetadataStreamProcessorTests: XCTestCase {
|
|||||||
// this is the step value as received from the http headers
|
// this is the step value as received from the http headers
|
||||||
processor.metadataAvailable(step: 16000)
|
processor.metadataAvailable(step: 16000)
|
||||||
|
|
||||||
let audio = processor.proccessMetadata(data: data)
|
let audio = processor.processMetadata(data: data)
|
||||||
XCTAssertFalse(audio.isEmpty)
|
XCTAssertFalse(audio.isEmpty)
|
||||||
|
|
||||||
XCTAssertTrue(metadataDelegateSpy.receivedMetadata.called)
|
XCTAssertTrue(metadataDelegateSpy.receivedMetadata.called)
|
||||||
@@ -53,7 +54,6 @@ class MetadataStreamProcessorTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func test_Processor_Outputs_Correct_Metadata_ForStep_WithMetadata() throws {
|
func test_Processor_Outputs_Correct_Metadata_ForStep_WithMetadata() throws {
|
||||||
let bundle = Bundle(for: MetadataStreamProcessorTests.self)
|
|
||||||
let url = bundle.url(forResource: "raw-stream-audio-normal-metadata", withExtension: nil)!
|
let url = bundle.url(forResource: "raw-stream-audio-normal-metadata", withExtension: nil)!
|
||||||
|
|
||||||
let data = try Data(contentsOf: url)
|
let data = try Data(contentsOf: url)
|
||||||
@@ -64,7 +64,7 @@ class MetadataStreamProcessorTests: XCTestCase {
|
|||||||
// this is the step value as received from the http headers
|
// this is the step value as received from the http headers
|
||||||
processor.metadataAvailable(step: 16000)
|
processor.metadataAvailable(step: 16000)
|
||||||
|
|
||||||
let audio = processor.proccessMetadata(data: data)
|
let audio = processor.processMetadata(data: data)
|
||||||
XCTAssertFalse(audio.isEmpty)
|
XCTAssertFalse(audio.isEmpty)
|
||||||
|
|
||||||
XCTAssertTrue(metadataDelegateSpy.receivedMetadata.called)
|
XCTAssertTrue(metadataDelegateSpy.receivedMetadata.called)
|
||||||
@@ -72,7 +72,6 @@ class MetadataStreamProcessorTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func test_Processor_Outputs_Correct_Metadata_ForStep_WithMetadata_Alt() throws {
|
func test_Processor_Outputs_Correct_Metadata_ForStep_WithMetadata_Alt() throws {
|
||||||
let bundle = Bundle(for: MetadataStreamProcessorTests.self)
|
|
||||||
let url = bundle.url(forResource: "raw-stream-audio-normal-metadata-alt", withExtension: nil)!
|
let url = bundle.url(forResource: "raw-stream-audio-normal-metadata-alt", withExtension: nil)!
|
||||||
|
|
||||||
let data = try Data(contentsOf: url)
|
let data = try Data(contentsOf: url)
|
||||||
@@ -83,7 +82,7 @@ class MetadataStreamProcessorTests: XCTestCase {
|
|||||||
// this is the step value as received from the http headers
|
// this is the step value as received from the http headers
|
||||||
processor.metadataAvailable(step: 8000)
|
processor.metadataAvailable(step: 8000)
|
||||||
|
|
||||||
let audio = processor.proccessMetadata(data: data)
|
let audio = processor.processMetadata(data: data)
|
||||||
XCTAssertFalse(audio.isEmpty)
|
XCTAssertFalse(audio.isEmpty)
|
||||||
|
|
||||||
XCTAssertTrue(metadataDelegateSpy.receivedMetadata.called)
|
XCTAssertTrue(metadataDelegateSpy.receivedMetadata.called)
|
||||||
@@ -95,7 +94,6 @@ class MetadataStreamProcessorTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func test_Processor_Outputs_Correct_Metadata_ForStep_NoMetadata() throws {
|
func test_Processor_Outputs_Correct_Metadata_ForStep_NoMetadata() throws {
|
||||||
let bundle = Bundle(for: MetadataStreamProcessorTests.self)
|
|
||||||
let url = bundle.url(forResource: "raw-stream-audio-no-metadata", withExtension: nil)!
|
let url = bundle.url(forResource: "raw-stream-audio-no-metadata", withExtension: nil)!
|
||||||
|
|
||||||
let data = try Data(contentsOf: url)
|
let data = try Data(contentsOf: url)
|
||||||
@@ -106,7 +104,7 @@ class MetadataStreamProcessorTests: XCTestCase {
|
|||||||
// this is the step value as received from the http headers
|
// this is the step value as received from the http headers
|
||||||
processor.metadataAvailable(step: 16000)
|
processor.metadataAvailable(step: 16000)
|
||||||
|
|
||||||
let audio = processor.proccessMetadata(data: data)
|
let audio = processor.processMetadata(data: data)
|
||||||
XCTAssertFalse(audio.isEmpty)
|
XCTAssertFalse(audio.isEmpty)
|
||||||
|
|
||||||
XCTAssertFalse(metadataDelegateSpy.receivedMetadata.called)
|
XCTAssertFalse(metadataDelegateSpy.receivedMetadata.called)
|
||||||
@@ -122,7 +120,7 @@ class MetadataStreamProcessorTests: XCTestCase {
|
|||||||
// this is the step value as received from the http headers
|
// this is the step value as received from the http headers
|
||||||
processor.metadataAvailable(step: 16000)
|
processor.metadataAvailable(step: 16000)
|
||||||
|
|
||||||
let audio = processor.proccessMetadata(data: data)
|
let audio = processor.processMetadata(data: data)
|
||||||
XCTAssertTrue(audio.isEmpty)
|
XCTAssertTrue(audio.isEmpty)
|
||||||
|
|
||||||
XCTAssertFalse(metadataDelegateSpy.receivedMetadata.called)
|
XCTAssertFalse(metadataDelegateSpy.receivedMetadata.called)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class HTTPHeaderParserTests: XCTestCase {
|
|||||||
let headers: [String: String] =
|
let headers: [String: String] =
|
||||||
[HeaderField.contentLength: "1000",
|
[HeaderField.contentLength: "1000",
|
||||||
HeaderField.contentType: "audio/mp3",
|
HeaderField.contentType: "audio/mp3",
|
||||||
IcyHeaderField.icyMentaint: "16000"]
|
IcyHeaderField.icyMetaint: "16000"]
|
||||||
let httpURLResponse = HTTPURLResponse(url: URL(string: "www.google.com")!,
|
let httpURLResponse = HTTPURLResponse(url: URL(string: "www.google.com")!,
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
httpVersion: "",
|
httpVersion: "",
|
||||||
@@ -57,7 +57,7 @@ class HTTPHeaderParserTests: XCTestCase {
|
|||||||
let headers: [String: String] =
|
let headers: [String: String] =
|
||||||
[HeaderField.contentLength.lowercased(): "1000",
|
[HeaderField.contentLength.lowercased(): "1000",
|
||||||
HeaderField.contentType.lowercased(): "audio/mp3",
|
HeaderField.contentType.lowercased(): "audio/mp3",
|
||||||
IcyHeaderField.icyMentaint.lowercased(): "16000"]
|
IcyHeaderField.icyMetaint.lowercased(): "16000"]
|
||||||
let httpURLResponse = HTTPURLResponse(url: URL(string: "www.google.com")!,
|
let httpURLResponse = HTTPURLResponse(url: URL(string: "www.google.com")!,
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
httpVersion: "",
|
httpVersion: "",
|
||||||
|
|||||||
@@ -50,6 +50,26 @@ class MetadataParserTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testParserOutputsCorrectResultWhenEntryContainsEqualSign() throws {
|
||||||
|
let string = "StreamTitle=\'Gramatik - In This Whole World (Original Mix)\';StreamUrl=\'\';track_info=\'k4Smc3RhdHVzoUihQNJiGp6BpHR5cGWhVKJpZKhNWDUxMTYzNISmc3RhdHVzoUOhQNJiGp9cpHR5cGWhVKJpZKhNWDUxMDM3MoSmc3RhdHVzoUOhQNJiGqAqpHR5cGWhVKJpZKhNWDUxMjA5Ng==\';UTC=\'20220226T214447.206\';\0\0\0\0\0\0\0\0\0"
|
||||||
|
let data = string.data(using: .utf8)!
|
||||||
|
|
||||||
|
let parser = MetadataParser()
|
||||||
|
|
||||||
|
let output = parser.parse(input: data)
|
||||||
|
|
||||||
|
switch output {
|
||||||
|
case let .success(values):
|
||||||
|
XCTAssertFalse(values.isEmpty)
|
||||||
|
XCTAssertEqual(values["StreamTitle"], "Gramatik - In This Whole World (Original Mix)")
|
||||||
|
XCTAssertEqual(values["StreamUrl"], "")
|
||||||
|
XCTAssertEqual(values["track_info"], "k4Smc3RhdHVzoUihQNJiGp6BpHR5cGWhVKJpZKhNWDUxMTYzNISmc3RhdHVzoUOhQNJiGp9cpHR5cGWhVKJpZKhNWDUxMDM3MoSmc3RhdHVzoUOhQNJiGqAqpHR5cGWhVKJpZKhNWDUxMjA5Ng==")
|
||||||
|
XCTAssertEqual(values["UTC"], "20220226T214447.206")
|
||||||
|
case .failure:
|
||||||
|
XCTFail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testParserOutputsFailureOnEmptyStringData() throws {
|
func testParserOutputsFailureOnEmptyStringData() throws {
|
||||||
let data = "".data(using: .utf8)!
|
let data = "".data(using: .utf8)!
|
||||||
let parser = MetadataParser()
|
let parser = MetadataParser()
|
||||||
|
|||||||
+15
-3
@@ -1,4 +1,4 @@
|
|||||||
// swift-tools-version:5.3
|
// swift-tools-version:5.9
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
@@ -17,7 +17,19 @@ let package = Package(
|
|||||||
.target(
|
.target(
|
||||||
name: "AudioStreaming",
|
name: "AudioStreaming",
|
||||||
path: "AudioStreaming"
|
path: "AudioStreaming"
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "AudioStreamingTests",
|
||||||
|
dependencies: [
|
||||||
|
"AudioStreaming"
|
||||||
|
],
|
||||||
|
path: "AudioStreamingTests",
|
||||||
|
resources: [
|
||||||
|
.copy("Streaming/Metadata Stream Processor/raw-audio-streams/raw-stream-audio-empty-metadata"),
|
||||||
|
.copy("Streaming/Metadata Stream Processor/raw-audio-streams/raw-stream-audio-no-metadata"),
|
||||||
|
.copy("Streaming/Metadata Stream Processor/raw-audio-streams/raw-stream-audio-normal-metadata"),
|
||||||
|
.copy("Streaming/Metadata Stream Processor/raw-audio-streams/raw-stream-audio-normal-metadata-alt")
|
||||||
|
]
|
||||||
)
|
)
|
||||||
],
|
]
|
||||||
swiftLanguageVersions: [.v5]
|
|
||||||
)
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user