Compare commits

..

47 Commits

Author SHA1 Message Date
Tanha c325caa914 Release 2.9.0 2020-03-06 23:53:42 -08:00
tanhakabir dd54d81573 implement streaming cancellation (#32)
* cancels streaming initally, but player enters weird state trying to stream again

* calling superclass invalidate from engines for good measure

* Fix UI to fix seemingly weird state

* Fix error warning
2020-03-06 23:52:22 -08:00
tanhakabir ebc282d5c2 fix crash on cancel streaming pressed (#30) 2020-03-06 23:28:30 -08:00
Tanha 80ce253f92 Release 2.8.2 2020-01-21 00:29:28 -08:00
tanhakabir fe2395066f Add equalizer example (#28)
* project configure for me

* to see log

* set project

* new UI

* fix ui

* re-fix UI

* add example of equalizer

* Revert "re-fix UI"

This reverts commit 05ed993a52.

* Revert "fix ui"

This reverts commit 0da9f6adea.

* Revert "new UI"

This reverts commit ffd6a95a2d.

* Add verbose debug mode to player

Co-authored-by: cendolinside123 <jnsbstn391@gmail.com>
2020-01-21 00:27:51 -08:00
Tanha 3e66b4b4d4 Release 2.8.1 2020-01-04 13:35:29 -08:00
Tanha 58bbc97a1b Minor fix on end of audio notification 2020-01-04 13:35:03 -08:00
Tanha 8d9e9d92f4 Release 2.8.0 2020-01-04 13:24:47 -08:00
tanhakabir 03392c21e0 fix bug in notification of end of audio in PlayingStatus (#25) 2020-01-04 13:23:50 -08:00
Tanha 924170d159 Release 2.7.0 2020-01-04 02:04:35 -08:00
tanhakabir b355eb4e09 add api for buffer progress in SAAvailabilityRange (#24) 2020-01-04 02:02:56 -08:00
Tanha 1373a816a6 Release 2.6.0 2019-12-18 21:23:07 -08:00
tanhakabir 196b04a703 Rename and deprecate initialize functions (#23) 2019-12-18 21:22:28 -08:00
Tanha ac971e65a6 Release 2.5.2 2019-12-18 16:19:57 -08:00
Tanha 2c50502b28 update documentation for live streaming audio 2019-12-18 16:19:37 -08:00
Tanha c222b5a745 Release 2.5.1 2019-12-18 00:27:49 -08:00
Tanha 2e86a6503c Clean up debug logging 2019-12-18 00:27:21 -08:00
Tanha 9ebd7fa7fe Release 2.5.0 2019-12-18 00:24:11 -08:00
tanhakabir 5197a16023 Fix live streams/servers with unpredictable size at beginning of stream being playable (#21)
* Handle case when header for stream does not contain expected content

* update documentation

* fix elapsed time updating on seek in example app
2019-12-18 00:22:31 -08:00
Tanha 159627c63e Release 2.4.0 2019-12-03 01:43:45 -08:00
tanhakabir 07230cce1a add another status to PlayingStatus for end of audio (#19) 2019-12-03 01:42:56 -08:00
tanhakabir a33aee80d1 Expose engine outside of SAPlayer (#18)
* expose engine outside of player

* add player clearing functionality
2019-12-03 01:25:58 -08:00
tanhakabir e1d3da1ddb Update README.md 2019-12-01 11:10:12 -08:00
Tanha 8c2524d990 Release 2.3.0 2019-12-01 01:17:54 -08:00
Tanha be1b7aa05f fix bug on bad network and streaming being stuck in missing data state
close #4
2019-12-01 01:16:03 -08:00
Tanha 4b57fee75c Release 2.2.0 2019-11-29 17:18:33 -08:00
tanhakabir fc9c43a23c Update accessing bytes of Data for Swift 5 (#17) 2019-11-29 17:16:53 -08:00
Tanha fd4e4e3b77 Release 2.1.0 2019-11-28 21:37:16 -08:00
tanhakabir f1200252be Update to Swift 5 (#16) 2019-11-28 21:36:30 -08:00
Jonathan Mercer 046e64b2b8 Update README.md (#15)
* Update readme

- Completely re-worded the first paragraph
- Re-worded some sentences that confused me
- Moved audio manipulation in the end. Assuming that advanced users will read it there while keeping it hidden from other users

* Update README.md
2019-11-27 16:36:24 -08:00
Tanha ad9e40ad1c Update README.md 2019-11-26 01:37:53 -08:00
Tanha f19eaf7ec9 Release 2.0.1 2019-11-26 01:04:51 -08:00
tanhakabir 012291c1c9 Merge pull request #14 from tanhakabir/open_nodes_interface
Open interface to control audio manipulation nodes
2019-11-26 01:03:11 -08:00
Tanha 70ba1c757e add controls for realtime reverb change in example app 2019-11-26 01:02:06 -08:00
Tanha 3ab47b568d nit 2019-11-26 00:53:52 -08:00
Tanha 6fd985d2ad Update documentation 2019-11-26 00:52:18 -08:00
Tanha cf028e0e36 nit rename 2019-11-26 00:52:08 -08:00
Tanha f9e6dafc2c add documentation for player functions 2019-11-26 00:00:44 -08:00
Tanha e562a259fb only show monitoring worthy errors outside of library 2019-11-25 23:48:24 -08:00
Tanha 9594b560d0 update lockscreen interval skip control on change of fields 2019-11-25 23:23:07 -08:00
Tanha bf2dae9569 add documentation for fields of SAPlayer 2019-11-25 23:19:29 -08:00
Tanha 90ac3a4336 clean up typing for rate 2019-11-25 22:23:05 -08:00
Tanha 395364b4eb test run with more modifiers 2019-11-25 22:17:55 -08:00
Tanha 6a2bb94037 fix freezing bug 2019-11-25 22:17:07 -08:00
Tanha 7d81953b83 removed usage of rate/speed within the engine 2019-11-25 22:10:57 -08:00
Tanha feb69174ae refactor to have external to library control nodes 2019-11-25 21:57:55 -08:00
Tanha 00eee68aab silenced warnings for example app 2019-11-25 21:01:53 -08:00
26 changed files with 707 additions and 190 deletions
+13 -7
View File
@@ -14,6 +14,7 @@
79D8DF73FA7CDD6E266BAE71D46E035F /* Pods-SwiftAudioPlayer_Tests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 50C71346CE708A211A5AFAC20BAE48CB /* Pods-SwiftAudioPlayer_Tests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
831B263D357A5FA2DDC7B1AE4B374092 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A16F4CFC63FAC439D7A04994F579A03 /* Foundation.framework */; };
8F93DB166237195ED222EE55B6404625 /* Pods-SwiftAudioPlayer_Example-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B0B76CB1439F4D361322144E5A65C3A /* Pods-SwiftAudioPlayer_Example-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
A40DBE292391D9CA00F86146 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40DBE282391D9C900F86146 /* Data.swift */; };
A41AA0D2238BB9B600A467E1 /* SAPlayingStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = A41AA0D1238BB9B600A467E1 /* SAPlayingStatus.swift */; };
A4681FC6220113880018AB51 /* SAPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F8D2200E00E0018AB51 /* SAPlayer.swift */; };
A4681FC72201138B0018AB51 /* SAPlayerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F912200E1950018AB51 /* SAPlayerDelegate.swift */; };
@@ -96,6 +97,7 @@
99925F09FC9C6EA4B9C0508F4E2D1FE2 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A19C8F889C787C19BE4123C1896AF501 /* Pods-SwiftAudioPlayer_Example-resources.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-SwiftAudioPlayer_Example-resources.sh"; sourceTree = "<group>"; };
A39F2A138CF40C1051CA9E227429A86D /* SwiftAudioPlayer.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = SwiftAudioPlayer.modulemap; sourceTree = "<group>"; };
A40DBE282391D9C900F86146 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
A41AA0D1238BB9B600A467E1 /* SAPlayingStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayingStatus.swift; sourceTree = "<group>"; };
A4523BC8220A0B3C0079C4BC /* Credited_LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = Credited_LICENSE; sourceTree = "<group>"; };
A4681F802200D0500018AB51 /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = "<group>"; };
@@ -273,6 +275,7 @@
A4681F872200DAD50018AB51 /* DirectorThreadSafeClosures.swift */,
A4681F892200DB3C0018AB51 /* Date.swift */,
A4681F962200E2E20018AB51 /* URL.swift */,
A40DBE282391D9C900F86146 /* Data.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -482,22 +485,23 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0930;
LastUpgradeCheck = 0930;
LastUpgradeCheck = 1010;
TargetAttributes = {
042ACE071BA515F4DE0E0C8007C3F0EE = {
LastSwiftMigration = 1010;
};
E50DAD13FFD3FC8036073A58BF8423D4 = {
LastSwiftMigration = 1010;
LastSwiftMigration = 1120;
};
};
};
buildConfigurationList = 2D8E8EC45A3A1A1D94AE762CB5028504 /* Build configuration list for PBXProject "Pods" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 7DB346D0F39D3F0E887471402A8071AB;
productRefGroup = 21D946895A4F57F51246F3EBCF330719 /* Products */;
@@ -523,6 +527,7 @@
A4681FD2220113B20018AB51 /* AudioParser.swift in Sources */,
A4681FCF220113A40018AB51 /* AudioConverterListener.swift in Sources */,
A4681FE1220113E70018AB51 /* Constants.swift in Sources */,
A40DBE292391D9CA00F86146 /* Data.swift in Sources */,
A4FBA6B5221B74C900D5A353 /* SALockScreenInfo.swift in Sources */,
A4681FC6220113880018AB51 /* SAPlayer.swift in Sources */,
A4FBA6B7221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift in Sources */,
@@ -641,7 +646,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.3;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -676,7 +681,7 @@
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
@@ -708,7 +713,7 @@
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
@@ -803,10 +808,11 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.3;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_NAME = "$(TARGET_NAME)";
STRIP_INSTALLED_PRODUCT = NO;
SWIFT_COMPILATION_MODE = wholemodule;
SYMROOT = "${SRCROOT}/../build";
};
name = Release;
@@ -207,25 +207,25 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0830;
LastUpgradeCheck = 0830;
LastUpgradeCheck = 1010;
ORGANIZATIONNAME = CocoaPods;
TargetAttributes = {
607FACCF1AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = R2392A68YQ;
LastSwiftMigration = 1010;
DevelopmentTeam = H9Y26B6GZB;
LastSwiftMigration = 1120;
};
607FACE41AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = R2392A68YQ;
LastSwiftMigration = 1010;
DevelopmentTeam = H9Y26B6GZB;
LastSwiftMigration = 1120;
TestTargetID = 607FACCF1AFB9204008FA782;
};
};
};
buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "SwiftAudioPlayer" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
@@ -379,12 +379,14 @@
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_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_RANGE_LOOP_ANALYSIS = YES;
@@ -432,12 +434,14 @@
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_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_RANGE_LOOP_ANALYSIS = YES;
@@ -471,13 +475,13 @@
baseConfigurationReference = 65A66AB4C3016E8BB53FF3E0 /* Pods-SwiftAudioPlayer_Example.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = R2392A68YQ;
DEVELOPMENT_TEAM = H9Y26B6GZB;
INFOPLIST_FILE = SwiftAudioPlayer/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo-test.SwiftAudioPlayer-Example";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
@@ -486,13 +490,13 @@
baseConfigurationReference = 4B5DD2AE0B23A759D18926DC /* Pods-SwiftAudioPlayer_Example.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = R2392A68YQ;
DEVELOPMENT_TEAM = H9Y26B6GZB;
INFOPLIST_FILE = SwiftAudioPlayer/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo-test.SwiftAudioPlayer-Example";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
};
name = Release;
};
@@ -500,7 +504,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = BBD877782CC67FBCC7BF7532 /* Pods-SwiftAudioPlayer_Tests.debug.xcconfig */;
buildSettings = {
DEVELOPMENT_TEAM = R2392A68YQ;
DEVELOPMENT_TEAM = H9Y26B6GZB;
FRAMEWORK_SEARCH_PATHS = (
"$(SDKROOT)/Developer/Library/Frameworks",
"$(inherited)",
@@ -513,7 +517,7 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudioPlayer_Example.app/SwiftAudioPlayer_Example";
};
name = Debug;
@@ -522,7 +526,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 0B7D1E6C00E83B4AF8AA1781 /* Pods-SwiftAudioPlayer_Tests.release.xcconfig */;
buildSettings = {
DEVELOPMENT_TEAM = R2392A68YQ;
DEVELOPMENT_TEAM = H9Y26B6GZB;
FRAMEWORK_SEARCH_PATHS = (
"$(SDKROOT)/Developer/Library/Frameworks",
"$(inherited)",
@@ -531,7 +535,7 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudioPlayer_Example.app/SwiftAudioPlayer_Example";
};
name = Release;
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0900"
LastUpgradeVersion = "1010"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -40,7 +40,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
@@ -70,7 +69,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
@@ -64,6 +64,12 @@
<action selector="rateChanged:" destination="vXZ-lx-hvc" eventType="valueChanged" id="FDJ-jA-bm8"/>
</connections>
</slider>
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="300" minValue="0.10000000149011612" maxValue="1000" translatesAutoresizingMaskIntoConstraints="NO" id="nsl-df-P21">
<rect key="frame" x="14" y="397" width="347" height="31"/>
<connections>
<action selector="reverbChanged:" destination="vXZ-lx-hvc" eventType="valueChanged" id="J8Q-be-35q"/>
</connections>
</slider>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="joK-xi-MCo">
<rect key="frame" x="16" y="80" width="343" height="29"/>
<segments>
@@ -89,8 +95,8 @@
<action selector="streamTouched:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="AXY-N7-87Y"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="rate: 1.0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="yUQ-mI-ozK">
<rect key="frame" x="157" y="435" width="61" height="21"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="rate: 1.0x" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="yUQ-mI-ozK">
<rect key="frame" x="153" y="435" width="69" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@@ -113,9 +119,16 @@
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="reverb: 300.0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="y5i-MZ-Qat">
<rect key="frame" x="136" y="368" width="103" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="nsl-df-P21" firstAttribute="top" secondItem="y5i-MZ-Qat" secondAttribute="bottom" constant="8" id="0aM-Sz-J9k"/>
<constraint firstItem="lTK-Hd-Tl2" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="16" id="1wb-IW-jYz"/>
<constraint firstItem="j3w-gr-HzF" firstAttribute="leading" secondItem="lTK-Hd-Tl2" secondAttribute="leading" id="26c-ZJ-768"/>
<constraint firstItem="jUc-tP-CC5" firstAttribute="top" secondItem="KDu-ea-kF8" secondAttribute="bottom" constant="80" id="5sT-An-9vw"/>
@@ -133,10 +146,12 @@
<constraint firstItem="KDu-ea-kF8" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="78" id="SRU-sX-z5b"/>
<constraint firstItem="w2a-RA-zmI" firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" id="Vki-IZ-AdN"/>
<constraint firstItem="lTK-Hd-Tl2" firstAttribute="top" secondItem="j3w-gr-HzF" secondAttribute="bottom" constant="8" id="Wwx-Uo-yIC"/>
<constraint firstItem="nsl-df-P21" firstAttribute="leading" secondItem="vfk-OJ-S3T" secondAttribute="leading" id="a5C-nZ-8Jc"/>
<constraint firstItem="yUQ-mI-ozK" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="a66-h4-WVf"/>
<constraint firstItem="Urj-Dv-41y" firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" id="aKt-EV-Bwd"/>
<constraint firstItem="tFH-sY-Xu9" firstAttribute="top" secondItem="1IX-z5-wWx" secondAttribute="bottom" constant="27" id="bIq-V0-Sac"/>
<constraint firstItem="tFH-sY-Xu9" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="62.5" id="cH6-q6-Lel"/>
<constraint firstItem="yUQ-mI-ozK" firstAttribute="top" secondItem="nsl-df-P21" secondAttribute="bottom" constant="8" id="cKV-wk-6P9"/>
<constraint firstItem="jUc-tP-CC5" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="cgM-Nj-yit"/>
<constraint firstItem="KDu-ea-kF8" firstAttribute="top" secondItem="joK-xi-MCo" secondAttribute="bottom" constant="32" id="dLw-rF-Pfb"/>
<constraint firstItem="w2a-RA-zmI" firstAttribute="leading" secondItem="lTK-Hd-Tl2" secondAttribute="leading" id="daz-b0-eCC"/>
@@ -146,6 +161,8 @@
<constraint firstAttribute="trailing" secondItem="1IX-z5-wWx" secondAttribute="trailing" constant="16" id="hHM-jO-RZd"/>
<constraint firstItem="6d9-Bc-hIz" firstAttribute="top" secondItem="joK-xi-MCo" secondAttribute="bottom" constant="32" id="m9s-An-IWV"/>
<constraint firstItem="vfk-OJ-S3T" firstAttribute="top" secondItem="yUQ-mI-ozK" secondAttribute="bottom" constant="8" id="oaW-rr-UVN"/>
<constraint firstItem="nsl-df-P21" firstAttribute="trailing" secondItem="vfk-OJ-S3T" secondAttribute="trailing" id="r5e-Wq-dqV"/>
<constraint firstItem="y5i-MZ-Qat" firstAttribute="centerX" secondItem="nsl-df-P21" secondAttribute="centerX" id="reC-GA-ZgT"/>
<constraint firstAttribute="trailing" secondItem="0QE-3F-a4G" secondAttribute="trailing" constant="62.5" id="tg1-gr-hdd"/>
<constraint firstAttribute="trailing" secondItem="6d9-Bc-hIz" secondAttribute="trailing" constant="82" id="vtN-y4-iqp"/>
<constraint firstItem="0QE-3F-a4G" firstAttribute="centerY" secondItem="jUc-tP-CC5" secondAttribute="centerY" id="xDi-tj-bBF"/>
@@ -163,6 +180,8 @@
<outlet property="playPauseButton" destination="jUc-tP-CC5" id="e9C-zV-A1B"/>
<outlet property="rateLabel" destination="yUQ-mI-ozK" id="Dx4-lO-A1B"/>
<outlet property="rateSlider" destination="vfk-OJ-S3T" id="mNc-ET-aNM"/>
<outlet property="reverbLabel" destination="y5i-MZ-Qat" id="8YR-mc-GFA"/>
<outlet property="reverbSlider" destination="nsl-df-P21" id="BKt-Hb-akj"/>
<outlet property="scrubberSlider" destination="w2a-RA-zmI" id="VbI-tT-lbc"/>
<outlet property="skipBackwardButton" destination="tFH-sY-Xu9" id="LwM-2S-m6F"/>
<outlet property="skipForwardButton" destination="0QE-3F-a4G" id="cQ7-b7-pW7"/>
+63 -16
View File
@@ -8,6 +8,7 @@
import UIKit
import SwiftAudioPlayer
import AVFoundation
class ViewController: UIViewController {
struct AudioInfo: Hashable {
@@ -58,7 +59,7 @@ class ViewController: UIViewController {
self.currentUrlLocationLabel.text = "remote url: \(selectedAudio.url.absoluteString)"
}
}
var freq:[Int] = [0,0,0,0,0,0,0,0,0,0]
@IBOutlet weak var currentUrlLocationLabel: UILabel!
@IBOutlet weak var bufferProgress: UIProgressView!
@IBOutlet weak var scrubberSlider: UISlider!
@@ -74,6 +75,8 @@ class ViewController: UIViewController {
@IBOutlet weak var rateLabel: UILabel!
@IBOutlet weak var reverbLabel: UILabel!
@IBOutlet weak var reverbSlider: UISlider!
@IBOutlet weak var durationLabel: UILabel!
@IBOutlet weak var currentTimestampLabel: UILabel!
@@ -100,11 +103,13 @@ class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
adjustSpeed()
SAPlayer.shared.DEBUG_MODE = true
isPlayable = false
selectedAudio = AudioInfo(index: 0)
addRandomModifiers()
_ = SAPlayer.Updates.Duration.subscribe { [weak self] (url, duration) in
guard let self = self else { return }
guard url == self.selectedAudio.url || url == self.savedUrls[self.selectedAudio] else { return }
@@ -114,7 +119,6 @@ class ViewController: UIViewController {
_ = SAPlayer.Updates.ElapsedTime.subscribe { [weak self] (url, position) in
guard let self = self else { return }
guard self.beingSeeked == false else { return }
guard url == self.selectedAudio.url || url == self.savedUrls[self.selectedAudio] else { return }
self.currentTimestampLabel.text = SAPlayer.prettifyTimestamp(position)
@@ -143,12 +147,12 @@ class ViewController: UIViewController {
if self.duration == 0.0 { return }
let progress = Float((buffer.totalDurationBuffered + buffer.startingBufferTimePositon) / self.duration)
self.bufferProgress.progress = Float(buffer.bufferingProgress)
self.bufferProgress.progress = progress
if progress >= 0.99 {
if buffer.bufferingProgress >= 0.99 {
self.streamButton.isEnabled = false
} else {
self.streamButton.isEnabled = true
}
self.isPlayable = buffer.isReadyForPlaying
@@ -171,9 +175,29 @@ class ViewController: UIViewController {
self.isPlayable = false
self.playPauseButton.setTitle("Loading", for: .normal)
return
case .ended:
self.isPlayable = false
self.playPauseButton.setTitle("Done", for: .normal)
return
}
}
}
func addRandomModifiers() {
let node = AVAudioUnitReverb()
SAPlayer.shared.audioModifiers.append(node)
node.wetDryMix = 300
let frequency:[Int] = [60,170,310,600,1000,3000,6000,12000,14000,16000]
let node2 = AVAudioUnitEQ(numberOfBands:frequency.count)
node2.globalGain = 1
for i in 0...(node2.bands.count-1) {
node2.bands[i].frequency = Float(frequency[i])
node2.bands[i].gain = 0
node2.bands[i].bypass = false
node2.bands[i].filterType = .parametric
}
SAPlayer.shared.audioModifiers.append(node2)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
@@ -201,7 +225,19 @@ class ViewController: UIViewController {
@IBAction func rateChanged(_ sender: Any) {
adjustSpeed()
let speed = rateSlider.value
rateLabel.text = "rate: \(speed)x"
if let node = SAPlayer.shared.audioModifiers[0] as? AVAudioUnitTimePitch {
node.rate = speed
SAPlayer.shared.playbackRateOfAudioChanged(rate: speed)
}
}
@IBAction func reverbChanged(_ sender: Any) {
let reverb = reverbSlider.value
reverbLabel.text = "reverb: \(reverb)"
if let node = SAPlayer.shared.audioModifiers[1] as? AVAudioUnitReverb {
node.wetDryMix = reverb
}
}
@IBAction func downloadTouched(_ sender: Any) {
@@ -220,7 +256,7 @@ class ViewController: UIViewController {
self.currentUrlLocationLabel.text = "saved to: \(url.lastPathComponent)"
self.savedUrls[self.selectedAudio] = url
SAPlayer.shared.initializeSavedAudio(withSavedUrl: url)
SAPlayer.shared.startSavedAudio(withSavedUrl: url)
}
})
streamButton.isEnabled = false
@@ -235,11 +271,15 @@ class ViewController: UIViewController {
@IBAction func streamTouched(_ sender: Any) {
if !isStreaming {
SAPlayer.shared.initializeAudio(withRemoteUrl: selectedAudio.url)
SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url)
streamButton.setTitle("Cancel streaming", for: .normal)
downloadButton.isEnabled = false
isStreaming = true
} else {
// TODO
SAPlayer.shared.stopStreamingRemoteAudio()
streamButton.setTitle("Stream", for: .normal)
downloadButton.isEnabled = true
isStreaming = false
}
}
@@ -254,11 +294,18 @@ class ViewController: UIViewController {
@IBAction func skipForwardTouched(_ sender: Any) {
SAPlayer.shared.skipForward()
}
private func adjustSpeed() {
let speed = rateSlider.value
rateLabel.text = "rate: \(speed)x"
SAPlayer.shared.rate = Double(speed)
@IBAction func setEqualizerValue(_ sender: Any) {
if let slider = sender as? UISlider{
print("slider of index:", slider.tag, "is changed to", slider.value)
freq[slider.tag] = Int(slider.value)
print("current frequency : ",freq)
if let node = SAPlayer.shared.audioModifiers[2] as? AVAudioUnitEQ{
for i in 0...(node.bands.count - 1){
node.bands[i].gain = Float(freq[i])
}
}
}
}
}
+109 -11
View File
@@ -4,21 +4,24 @@
[![License](https://img.shields.io/cocoapods/l/SwiftAudioPlayer.svg?style=flat)](https://cocoapods.org/pods/SwiftAudioPlayer)
[![Platform](https://img.shields.io/cocoapods/p/SwiftAudioPlayer.svg?style=flat)](https://cocoapods.org/pods/SwiftAudioPlayer)
Swift based audio player that is able to both stream remote audio and play locally saved audio, while performing audio manipulations in real-time. Underlying using AVAudioEngine, and you can change the rate of audio (up to 32x), change pitch, and [other audio enhancements](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements).
Swift-based audio player with AVAudioEngine as its base. Allows for: streaming online audio, playing local file, changing audio speed (3.5X, 4X, 32X), pitch, and real-time audio manipulation using custom [audio enhancements](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements).
This player was originally developed to be used in a [podcast player](https://chameleonpodcast.com/). We had originally used AVPlayer for playing audio but we wanted to manipulate audio that was being streamed. We set up AVAudioEngine at first just to play a file saved on the phone and it worked great, but AVAudioEngine on its own doesn't support streaming audio as easily as AVPlayer.
This player was built for [podcasting](https://chameleonpodcast.com/). We originally used AVPlayer for playing audio but we wanted to manipulate audio that was being streamed. We set up AVAudioEngine at first just to play a file saved on the phone and it worked great, but AVAudioEngine on its own doesn't support streaming audio as easily as AVPlayer.
Thus, using [AudioToolbox](https://developer.apple.com/documentation/audiotoolbox), we are able to stream audio and convert the downloaded data into usable data for the AVAudioEngine to play. For an overview of our solution check out our [blog post](https://medium.com/chameleon-podcast/creating-an-advanced-streaming-audio-engine-for-ios-9fbc7aef4115).
### Requirements
SwiftAudioPlayer is only available for iOS 10.0 and higher.
iOS 10.0 and higher.
## Getting Started
### Example Project
### Running the Example Project
To run the example project, clone the repo, and run `pod install` from the Example directory first.
1. Clone repo
2. CD to directory
3. Run `pod install` in terminal
4. Build and run
### Installation
@@ -31,10 +34,12 @@ pod 'SwiftAudioPlayer'
### Usage
**Important:** For app in background downloading please refer to [note](#important-step-for-background-downloads).
To play remote audio:
```swift
let url = URL(string: "https://randomwebsite.com/audio.mp3")!
SAPlayer.shared.initializeAudio(withRemoteUrl: url)
SAPlayer.shared.startRemoteAudio(withRemoteUrl: url)
SAPlayer.shared.play()
```
@@ -44,7 +49,7 @@ let info = SALockScreenInfo(title: "Random audio", artist: "Foo", artwork: UIIma
SAPlayer.shared.mediaInfo = info
```
To receive streaming progress:
To receive streaming progress (for buffer progress %):
```swift
@IBOutlet weak var bufferProgress: UIProgressView!
@@ -65,7 +70,26 @@ override func viewDidLoad() {
```
Look at the [Updates](#SAPlayer.Updates) section to see usage details and other updates to follow.
**Important:** For app in background downloading please refer to [note](#important-step-for-background-downloads).
For realtime audio manipulations, [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/avaudiounit) nodes are used. For example to adjust the reverb through a slider in the UI:
```swift
@IBOutlet weak var reverbSlider: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
let node = AVAudioUnitReverb()
SAPlayer.shared.audioModifiers.append(node)
node.wetDryMix = 300
}
@IBAction func reverbSliderChanged(_ sender: Any) {
if let node = SAPlayer.shared.audioModifiers[1] as? AVAudioUnitReverb {
node.wetDryMix = reverbSlider.value
}
}
```
For a more detailed explanation on usage, look at the [Realtime Audio Manipulations](#realtime-audio-manipulation) section.
For more details and specifics look at the [API documentation](#api-in-detail) below.
@@ -90,6 +114,50 @@ SwiftAudioPlayer is available under the MIT license. See the LICENSE file for mo
# API in detail
## SAPlayer
Access the player and all of its fields and functions through `SAPlayer.shared`.
### Playing Audio (Basic Commands)
To set up player with audio to play, use either:
* `startSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo?)` to play audio that is saved on the device.
* `startRemoteAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo?)` to play audio streamed from a remote location.
Both of these expect a URL of the location of the audio and an optional media information to display on the lockscreen.
For streaming remote audio, subscribe to `SAPlayer.Updates.StreamingBuffer` for updates on streaming progress.
Basic controls available:
```swift
play()
pause()
togglePlayAndPause()
seekTo(seconds: Double)
skipForward()
skipBackwards()
```
#### Important
The engine can handle audio manipulations like speed, pitch, effects, etc. To do this, nodes for effects must be finalized before initialize is called. Look at [audio manipulation documentation](#realtime-audio-manipulation) for more information.
### Lockscreen Media Player
Update and set what displays on the lockscreen's media player when the player is active.
`skipForwardSeconds` and `skipBackwardSeconds` for the intervals to skip forward and back with.
`mediaInfo` for the audio's information to display on the lockscreen. Is of type `SALockScreenInfo` which contains:
```swift
title: String
artist: String
artwork: UIImage?
releaseDate: UTC // Int
```
`playbackRateOfAudioChanged(rate: Float)` is used to update the lockscreen media player that the playback rate has changed.
## SAPlayer.Downloader
Use functionaity from Downloader to save audio files from remote locations for future offline playback.
@@ -108,7 +176,7 @@ func application(_ application: UIApplication, handleEventsForBackgroundURLSessi
### Downloading
Downloads will be held on pause when active stream is started, and will resume downloads when streaming is done.
All downloads will be paused when audio is streamed from a URL. They will automatically resume when streaming is done.
Use the following to start downloading audio in the background:
@@ -118,6 +186,8 @@ func downloadAudio(withRemoteUrl url: URL, completion: @escaping (_ savedUrl: UR
It will call the completion handler you pass after successful download with the location of the downloaded file on the device.
Subscribe to `SAPlayer.Updates.AudioDownloading` for downloading progress updates.
And use the following to stop any active or prevent future downloads of the corresponding remote URL:
```swift
@@ -143,6 +213,8 @@ Delete downloaded audio if it exists:
func deleteDownloaded(withSavedUrl url: URL)
```
**NOTE:** You're in charge or clearing downloads when your don't need them anymore
## SAPlayer.Updates
Receive updates for changing values from the player, such as the duration, elapsed time of playing audio, download progress, and etc.
@@ -175,12 +247,12 @@ Subscribe to this to update views on changes in position of which part of audio
### Duration
Payload = `Double`
Changes in the duration of the current initialized audio. Especially helpful for audio that is being streamed and can change with more data.
Changes in the duration of the current initialized audio. Especially helpful for audio that is being streamed and can change with more data. The engine makes a best effort guess as to the duration of the audio. The guess gets better with more bytes streamed from the web.
### PlayingStatus
Payload = `SAPlayingStatus`
Changes in the playing status of the player. Can be one of the following 3: `playing`, `paused`, `buffering`.
Changes in the playing status of the player. Can be one of the following 4: `playing`, `paused`, `buffering`, `ended` (audio ended).
### StreamingBuffer
Payload = `SAAudioAvailabilityRange`
@@ -193,3 +265,29 @@ For progress of downloading audio that saves to the phone for playback later, lo
Payload = `Double`
Changes in the progress of downloading audio in the background. This does not correspond to progress in streaming downloads, look at StreamingBuffer for streaming progress.
## Audio Effects
### Realtime Audio Manipulation
All audio effects on the player is done through [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/avaudiounit) nodes. These include adding reverb, changing pitch and playback rate, and adding distortion. Full list of effects available [here](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements).
The effects intended to use are stored in `audioModifiers` as a list of nodes. These nodes are in the order that the engine will attach them to one another.
**Note:** By default `SAPlayer` starts off with one node, an [AVAudioUnitTimePitch](https://developer.apple.com/documentation/avfoundation/avaudiounittimepitch) node, that is set to change the rate of audio without changing the pitch of the audio (intended for changing the rate of spoken word).
#### Important
All the nodes intended to be used on the playing audio must be finalized before calling `initializeSavedAudio(...)` or `initializeRemoteAudio(...)`. Any changes to list of nodes after initialize is called for a given audio file will not be reflected in playback.
Once all nodes are added to `audioModifiers` and the player has been initialized, any manipulations done with the nodes are performed in realtime. The example app shows manipulating the playback rate in realtime:
```swift
let speed = rateSlider.value
if let node = SAPlayer.shared.audioModifiers[0] as? AVAudioUnitTimePitch {
node.rate = speed
SAPlayer.shared.playbackRateOfAudioChanged(rate: speed)
}
```
**Note:** if the rate of the audio is changed, `playbackRateOfAudioChanged` should also be called to update the lockscreen's media player.
+3 -2
View File
@@ -64,7 +64,7 @@ class AudioDiskEngine: AudioEngine {
audioSampleRate = Float(audioFormat?.sampleRate ?? 44100)
audioLengthSeconds = Float(audioLengthSamples) / audioSampleRate
duration = Duration(audioLengthSeconds)
bufferedSeconds = SAAudioAvailabilityRange(startingNeedle: 0, durationLoadedByNetwork: duration, isPlayable: true)
bufferedSeconds = SAAudioAvailabilityRange(startingNeedle: 0, durationLoadedByNetwork: duration, predictedDurationToLoad: duration, isPlayable: true)
} else {
Log.monitor("Could not load downloaded file with url: \(url)")
}
@@ -98,7 +98,7 @@ class AudioDiskEngine: AudioEngine {
if state == .resumed {
state = .suspended
}
delegate?.didEndPlaying()
playingStatus = .ended
}
guard audioSampleRate != 0 else {
@@ -136,6 +136,7 @@ class AudioDiskEngine: AudioEngine {
}
override func invalidate() {
super.invalidate()
//Nothing to invalidate for disk
}
}
+40 -32
View File
@@ -27,10 +27,10 @@ import Foundation
import AVFoundation
protocol AudioEngineProtocol {
var engine: AVAudioEngine { get set }
func play()
func pause()
func seek(toNeedle needle: Needle)
func setSpeed(speed: Double)
func invalidate()
}
@@ -43,9 +43,8 @@ class AudioEngine: AudioEngineProtocol {
weak var delegate:AudioEngineDelegate?
let key:Key
let engine = AVAudioEngine()
var engine = AVAudioEngine()
let playerNode = AVAudioPlayerNode()
let rateNode: AVAudioUnitTimePitch
var timer: Timer?
@@ -57,12 +56,6 @@ class AudioEngine: AudioEngineProtocol {
case resumed
}
var audioSpeed: Double = 1.0 {
didSet {
rateNode.rate = Float(audioSpeed)
}
}
var needle: Needle = -1 {
didSet {
if needle >= 0 && oldValue != needle {
@@ -85,12 +78,16 @@ class AudioEngine: AudioEngineProtocol {
return
}
if status == .ended {
delegate?.didEndPlaying()
}
AudioClockDirector.shared.audioPlayingStatusWasChanged(key, status: status)
}
}
var bufferedSecondsDebouncer: SAAudioAvailabilityRange = SAAudioAvailabilityRange(startingNeedle: 0.0, durationLoadedByNetwork: 0.0, isPlayable: false)
var bufferedSeconds: SAAudioAvailabilityRange = SAAudioAvailabilityRange(startingNeedle: 0.0, durationLoadedByNetwork: 0.0, isPlayable: false) {
var bufferedSecondsDebouncer: SAAudioAvailabilityRange = SAAudioAvailabilityRange(startingNeedle: 0.0, durationLoadedByNetwork: 0.0, predictedDurationToLoad: Double.greatestFiniteMagnitude, isPlayable: false)
var bufferedSeconds: SAAudioAvailabilityRange = SAAudioAvailabilityRange(startingNeedle: 0.0, durationLoadedByNetwork: 0.0, predictedDurationToLoad: Double.greatestFiniteMagnitude, isPlayable: false) {
didSet {
if bufferedSeconds.startingNeedle == 0.0 && bufferedSeconds.durationLoadedByNetwork == 0.0 {
bufferedSecondsDebouncer = bufferedSeconds
@@ -115,24 +112,35 @@ class AudioEngine: AudioEngineProtocol {
init(url: AudioURL, delegate:AudioEngineDelegate?, engineAudioFormat: AVAudioFormat) {
self.key = url.key
self.delegate = delegate
// https://forums.developer.apple.com/thread/5874
// https://forums.developer.apple.com/thread/6050
// AVAudioTimePitchAlgorithm.timeDomain (just in case we want it)
var componentDescription: AudioComponentDescription {
get {
var ret = AudioComponentDescription()
ret.componentType = kAudioUnitType_FormatConverter
ret.componentSubType = kAudioUnitSubType_AUiPodTimeOther
return ret
}
}
rateNode = AVAudioUnitTimePitch(audioComponentDescription: componentDescription)
engine.attach(playerNode)
engine.attach(rateNode)
engine.connect(playerNode, to: rateNode, format: engineAudioFormat)
engine.connect(rateNode, to: engine.mainMixerNode, format: engineAudioFormat)
for node in SAPlayer.shared.audioModifiers {
engine.attach(node)
}
if SAPlayer.shared.audioModifiers.count > 0 {
var i = 0
let node = SAPlayer.shared.audioModifiers[i]
engine.connect(playerNode, to: node, format: engineAudioFormat)
i += 1
while i < SAPlayer.shared.audioModifiers.count {
let lastNode = SAPlayer.shared.audioModifiers[i - 1]
let currNode = SAPlayer.shared.audioModifiers[i]
engine.connect(lastNode, to: currNode, format: engineAudioFormat)
i += 1
}
let finalNode = SAPlayer.shared.audioModifiers[SAPlayer.shared.audioModifiers.count - 1]
engine.connect(finalNode, to: engine.mainMixerNode, format: engineAudioFormat)
} else {
engine.connect(playerNode, to: engine.mainMixerNode, format: engineAudioFormat)
}
engine.prepare()
}
@@ -146,7 +154,11 @@ class AudioEngine: AudioEngineProtocol {
func updateIsPlaying() {
if !bufferedSeconds.isPlayable {
playingStatus = .buffering
if bufferedSeconds.bufferingProgress > 0.999 {
playingStatus = .ended
} else {
playingStatus = .buffering
}
return
}
@@ -186,10 +198,6 @@ class AudioEngine: AudioEngineProtocol {
fatalError("No implementation for seek inAudioEngine, should be using streaming or disk type")
}
func setSpeed(speed: Double) {
audioSpeed = speed
}
func invalidate() {
}
+2 -7
View File
@@ -218,7 +218,7 @@ class AudioStreamEngine: AudioEngine {
let range = converter.pollNetworkAudioAvailabilityRange()
isPlayable = (numberOfBuffersScheduledInTotal >= MIN_BUFFERS_TO_BE_PLAYABLE && range.1 > 0) && predictedStreamDuration > 0
Log.debug("loaded \(range), numberOfBuffersScheduledInTotal: \(numberOfBuffersScheduledInTotal), isPlayable: \(isPlayable)")
bufferedSeconds = SAAudioAvailabilityRange(startingNeedle: range.0, durationLoadedByNetwork: range.1, isPlayable: isPlayable)
bufferedSeconds = SAAudioAvailabilityRange(startingNeedle: range.0, durationLoadedByNetwork: range.1, predictedDurationToLoad: predictedStreamDuration, isPlayable: isPlayable)
}
private func updateNeedle() {
@@ -236,12 +236,6 @@ class AudioStreamEngine: AudioEngine {
var currentTime = TimeInterval(playerTime.sampleTime) / playerTime.sampleRate
currentTime = currentTime > 0 ? currentTime : 0
if currentTime > predictedStreamDuration {
Log.info("reached end of audio")
seek(toNeedle: 0)
pause()
delegate?.didEndPlaying()
}
needle = (currentTime + currentTimeOffset)
}
@@ -297,6 +291,7 @@ class AudioStreamEngine: AudioEngine {
}
override func invalidate() {
super.invalidate()
converter.invalidate()
}
}
+29 -18
View File
@@ -97,7 +97,15 @@ class AudioThrottler: AudioThrottleable {
private var networkData: [NetworkDataWrapper] = []
var shouldThrottle = false
var byteOffsetBecauseOfSeek: UInt = 0
var totalBytesExpected: Int64? //this got sent up twice. Once at beginning of stream and second from network seek. We honor the first send
//This will be sent once at beginning of stream and every network seek
var totalBytesExpected: Int64? {
didSet {
if let bytes = totalBytesExpected {
delegate?.didUpdate(totalBytesExpected: Int64(byteOffsetBecauseOfSeek) + bytes)
}
}
}
var largestPollingOffsetDifference: UInt64 = 1
@@ -110,9 +118,8 @@ class AudioThrottler: AudioThrottleable {
Log.debug("received stream data of size \(pto.getData().count) and progress: \(pto.getProgress())")
self.delegate?.didUpdate(networkStreamProgress: pto.getProgress())
if self.totalBytesExpected == nil, let totalBytesExpected = pto.getTotalBytesExpected() {
if let totalBytesExpected = pto.getTotalBytesExpected() {
self.totalBytesExpected = totalBytesExpected
self.delegate?.didUpdate(totalBytesExpected: totalBytesExpected)
}
let lastItem = self.networkData.last
@@ -152,30 +159,34 @@ class AudioThrottler: AudioThrottleable {
Log.debug("offset: \(offset) within network packet of range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset) is next sent: \(wrappedNetworkData.isNextSent())")
if wrappedNetworkData.alreadySent {
if !wrappedNetworkData.isNextSent() {
var bytesSent: UInt = 0
var current = wrappedNetworkData
// Sometimes the next data packet is smaller than a full audio chunk size, so we need to ensure we send up enough packets for the audio chunk. This prevented Issue #4 where tsreaming would randomly get stuck in a state needing more data up the chain.
// https://github.com/tanhakabir/SwiftAudioPlayer/issues/4
while bytesSent < largestPollingOffsetDifference {
if let next = current.next {
Log.debug("Sending next network packet with range: \(next.startOffset) to \(next.endOffset)")
Log.debug("already sent offset: \(offset) within network packet of range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)")
var bytesSent: UInt = 0
var current = wrappedNetworkData
// Sometimes the next data packet is smaller than a full audio chunk size, so we need to ensure we send up enough packets for the audio chunk. This prevented Issue #4 where tsreaming would randomly get stuck in a state needing more data up the chain.
// https://github.com/tanhakabir/SwiftAudioPlayer/issues/4
while bytesSent < largestPollingOffsetDifference {
if let next = current.next {
if !next.alreadySent {
Log.info("Sending next network packet with range: \(next.startOffset) to \(next.endOffset), have sent \(bytesSent) bytes so far from \(largestPollingOffsetDifference) bytes")
next.alreadySent = true
delegate?.shouldProcess(networkData: next.data)
bytesSent += next.byteCount
current = next
} else {
return
}
bytesSent += next.byteCount
current = next
} else {
Log.debug("next package doesn't exist, bytes sent so far: \(bytesSent)")
return
}
}
return
}
Log.debug("Found network packet to send with range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)")
delegate?.shouldProcess(networkData: wrappedNetworkData.data)
Log.info("Found network packet to send with range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)")
wrappedNetworkData.alreadySent = true
delegate?.shouldProcess(networkData: wrappedNetworkData.data)
return
}
}
@@ -57,28 +57,39 @@ public enum ConverterError: LocalizedError {
public var errorDescription: String? {
switch self {
case .cannotLockQueue:
Log.warn("Failed to lock queue")
return "Failed to lock queue"
case .converterFailed(let status):
Log.warn(localizedDescriptionFromConverterError(status))
return localizedDescriptionFromConverterError(status)
case .failedToCreateDestinationFormat:
Log.warn("Failed to create a destination (processing) format")
return "Failed to create a destination (processing) format"
case .failedToCreatePCMBuffer:
Log.warn("Failed to create PCM buffer for reading data")
return "Failed to create PCM buffer for reading data"
case .notEnoughData:
Log.warn("Not enough data for read-conversion operation")
return "Not enough data for read-conversion operation"
case .parserMissingDataFormat:
Log.warn("Parser is missing a valid data format")
return "Parser is missing a valid data format"
case .reachedEndOfFile:
Log.warn("Reached the end of the file")
return "Reached the end of the file"
case .unableToCreateConverter(let status):
return localizedDescriptionFromConverterError(status)
case .superConcerningShouldNeverHappen:
Log.warn("Weird unexpected reader error. Should not have happened")
return "Weird unexpected reader error. Should not have happened"
case .cannotCreatePCMBufferWithoutConverter:
Log.warn("Could not create a PCM Buffer because reader does not have a converter yet")
return "Could not create a PCM Buffer because reader does not have a converter yet"
case .throttleParsingBuffersForEngine:
Log.warn("Preventing the reader from creating more PCM buffers since the player has more than 60 seconds of audio already to play")
return "Preventing the reader from creating more PCM buffers since the player has more than 60 seconds of audio already to play"
case .failedToCreateParser:
Log.warn("Could not create a parser")
return "Could not create a parser"
}
}
@@ -70,7 +70,7 @@ func ConverterListener(_ converter: AudioConverterRef, _ packetCount: UnsafeMuta
let packetByteCount = packet.count //this is not the count of an array
ioData.pointee.mNumberBuffers = 1
ioData.pointee.mBuffers.mData = UnsafeMutableRawPointer.allocate(byteCount: packetByteCount, alignment: 0)
_ = packet.withUnsafeMutableBytes({ (bytes: UnsafeMutablePointer<UInt8>) in
_ = packet.accessMutableBytes({ (bytes: UnsafeMutablePointer<UInt8>) in
memcpy((ioData.pointee.mBuffers.mData?.assumingMemoryBound(to: UInt8.self))!, bytes, packetByteCount)
})
ioData.pointee.mBuffers.mDataByteSize = UInt32(packetByteCount)
+4 -2
View File
@@ -85,7 +85,9 @@ class AudioParser: AudioParsable {
return max(AVAudioPacketCount(parsedAudioHeaderPacketCount), AVAudioPacketCount(audioPackets.count))
}
guard let sizeOfFileInBytes = expectedFileSizeInBytes, let bytesPerPacket = averageBytesPerPacket else {
let sizeOfFileInBytes: UInt64 = expectedFileSizeInBytes != nil ? expectedFileSizeInBytes! : 0
guard let bytesPerPacket = averageBytesPerPacket else {
return AVAudioPacketCount(0)
}
@@ -295,7 +297,7 @@ extension AudioParser: AudioThrottleDelegate {
let sID = self.streamID!
let dataSize = data.count
let _ = try data.withUnsafeBytes({ (bytes:UnsafePointer<UInt8>) in
_ = try data.accessBytes({ (bytes: UnsafePointer<UInt8>) in
let result:OSStatus = AudioFileStreamParseBytes(sID, UInt32(dataSize), bytes, [])
guard result == noErr else {
Log.monitor(ParserError.failedToParseBytes(result).errorDescription as Any)
@@ -29,8 +29,15 @@ import Foundation
public struct SAAudioAvailabilityRange {
let startingNeedle: Needle
let durationLoadedByNetwork: Duration
let predictedDurationToLoad: Duration
let isPlayable: Bool
public var bufferingProgress: Double {
get {
return (startingNeedle + durationLoadedByNetwork) / predictedDurationToLoad
}
}
public var startingBufferTimePositon: Double {
get {
return startingNeedle
@@ -52,4 +59,8 @@ public struct SAAudioAvailabilityRange {
public func contains(_ needle: Double) -> Bool {
return needle >= startingNeedle && (needle - startingNeedle) < durationLoadedByNetwork
}
public func isCompletelyBuffered() -> Bool {
return startingNeedle + durationLoadedByNetwork >= predictedDurationToLoad
}
}
+1
View File
@@ -30,4 +30,5 @@ public enum SAPlayingStatus {
case playing
case paused
case buffering
case ended
}
+16 -7
View File
@@ -35,10 +35,19 @@ protocol LockScreenViewProtocol {
}
extension LockScreenViewProtocol {
func clearLockScreenInfo() {
MPNowPlayingInfoCenter.default().nowPlayingInfo = [:]
}
@available(iOS 10.0, *)
func setLockScreenInfo(withMediaInfo info: SALockScreenInfo, duration: Duration) {
func setLockScreenInfo(withMediaInfo info: SALockScreenInfo?, duration: Duration) {
var nowPlayingInfo:[String : Any] = [:]
guard let info = info else {
MPNowPlayingInfoCenter.default().nowPlayingInfo = [:]
return
}
let title = info.title
let artist = info.artist
let releaseDate = info.releaseDate
@@ -68,10 +77,6 @@ extension LockScreenViewProtocol {
}
}
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
@@ -156,10 +161,14 @@ extension LockScreenViewProtocol {
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = 1.0
}
func updateLockscreenChangePlaybackRate(speed: Double){
func updateLockscreenChangePlaybackRate(speed: Float){
if speed > 0.0{
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = speed
}
}
func updateLockscreenSkipIntervals() {
MPRemoteCommandCenter.shared().skipBackwardCommand.preferredIntervals = [skipBackwardSeconds] as [NSNumber]
MPRemoteCommandCenter.shared().skipForwardCommand.preferredIntervals = [skipForwardSeconds] as [NSNumber]
}
}
@@ -271,21 +271,18 @@ extension AudioDownloadWorker {
return lhs.id == rhs.id && lhs.remoteUrl == rhs.remoteUrl
}
var hashValue: Int {
return id.hashValue ^ remoteUrl.hashValue
}
let id: ID
let remoteUrl: URL
let rank: Int
var completionHandlers: [(URL) -> ()]
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(remoteUrl)
}
}
private class ActiveDownload: Hashable {
var hashValue: Int {
return info.id.hashValue ^ task.hashValue
}
static func == (lhs: AudioDownloadWorker.ActiveDownload, rhs: AudioDownloadWorker.ActiveDownload) -> Bool {
return lhs.info.id == rhs.info.id
}
@@ -299,6 +296,11 @@ extension AudioDownloadWorker {
self.info = info
self.task = task
}
func hash(into hasher: inout Hasher) {
hasher.combine(info.id)
hasher.combine(task)
}
}
}
@@ -243,15 +243,19 @@ extension AudioStreamWorker: URLSessionDataDelegate {
}
guard self.task == dataTask else {
Log.error("stream_error not the same task") //Probably because of seek
Log.error("stream_error not the same task 638283") //Probably because of seek
return
}
guard let totalBytesExpected = totalBytesExpectedForCurrentStream, totalBytesExpected > 0 else {
guard var totalBytesExpected = totalBytesExpectedForCurrentStream else {
Log.monitor("should not be called 223r2")
return
}
if totalBytesExpected <= 0 {
totalBytesExpected = totalBytesReceived
}
totalBytesReceived = totalBytesReceived + Int64(data.count)
let progress = Double(totalBytesReceived)/Double(totalBytesExpected)
@@ -267,7 +271,7 @@ extension AudioStreamWorker: URLSessionDataDelegate {
}
guard self.task == dataTask else {
Log.error("stream_error not the same task")
Log.error("stream_error not the same task 517253")
return
}
@@ -289,8 +293,8 @@ extension AudioStreamWorker: URLSessionDataDelegate {
return
}
guard self.task == task else {
Log.error("stream_error not the same task")
if self.task != task && self.task != nil {
Log.error("stream_error not the same task 3901833")
return
}
+221 -25
View File
@@ -27,55 +27,166 @@ import Foundation
import AVFoundation
public class SAPlayer {
public var DEBUG_MODE: Bool = false {
didSet {
if(DEBUG_MODE) {
logLevel = LogLevel.EXTERNAL_DEBUG
} else {
logLevel = LogLevel.MONITOR
}
}
}
/**
Access to the player.
*/
public static let shared: SAPlayer = SAPlayer()
private var presenter: SAPlayerPresenter!
private var player: AudioEngine?
public var skipForwardSeconds: Double = 30
public var skipBackwardSeconds: Double = 15
/**
Access the engine of the player. Engine is nil if player has not been initialized with audio.
- Important: Changes to the engine are not safe guarded, thus unknown behaviour can arise from changing the engine. Just be wary and read [documentation of AVAudioEngine](https://developer.apple.com/documentation/avfoundation/avaudioengine) well when modifying,
*/
public var engine: AVAudioEngine? {
get {
return player?.engine
}
}
public var rate: Double = 1.0 {
/**
Corresponding to the overall volume of the player. Volume's default value is 1.0 and the range of valid values is 0.0 to 1.0. Volume is nil if no audio has been initialized yet.
*/
public var volume: Float? {
get {
return player?.engine.mainMixerNode.volume
}
set {
guard let value = newValue else { return }
guard value >= 0.0 && value <= 1.0 else { return }
player?.engine.mainMixerNode.volume = value
}
}
/**
Corresponding to the skipping forward button on the media player on the lockscreen. Default is set to 30 seconds.
*/
public var skipForwardSeconds: Double = 30 {
didSet {
presenter.handleSetSpeed(withMultiple: rate)
presenter.handleScrubbingIntervalsChanged()
}
}
public var duration: Double {
/**
Corresponding to the skipping backwards button on the media player on the lockscreen. Default is set to 15 seconds.
*/
public var skipBackwardSeconds: Double = 15 {
didSet {
presenter.handleScrubbingIntervalsChanged()
}
}
/**
List of [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements) audio modifiers to pass to the engine on initialization.
- Important: To have the intended effects, the list of modifiers must be finalized before initializing the audio to be played. The modifers are added to the engine in order of the list.
- Note: The default list already has an AVAudioUnitTimePitch node first in the list. This node is specifically set to change the rate of audio without changing the pitch of the audio (intended for changing the rate of spoken word).
The component description of this node is:
````
var componentDescription: AudioComponentDescription {
get {
var ret = AudioComponentDescription()
ret.componentType = kAudioUnitType_FormatConverter
ret.componentSubType = kAudioUnitSubType_AUiPodTimeOther
return ret
}
}
````
Please look at [forums.developer.apple.com/thread/5874](https://forums.developer.apple.com/thread/5874) and [forums.developer.apple.com/thread/6050](https://forums.developer.apple.com/thread/6050) for more details.
*/
public var audioModifiers: [AVAudioUnit] = []
/**
Total duration of current audio initialized. Returns nil if no audio is initialized in player.
- Note: If you are streaming from a source that does not have an expected size at the beginning of a stream, such as live streams, this value will be constantly updating to best known value at the time.
*/
public var duration: Double? {
get {
return presenter.duration ?? 0.0
return presenter.duration
}
}
public var prettyDuration: String {
/**
A textual representation of the duration of the current audio initialized. Returns nil if no audio is initialized in player.
*/
public var prettyDuration: String? {
get {
return SAPlayer.prettifyTimestamp(duration)
guard let d = duration else { return nil }
return SAPlayer.prettifyTimestamp(d)
}
}
public var elapsedTime: Double {
/**
Elapsed playback time of the current audio initialized. Returns nil if no audio is initialized in player.
*/
public var elapsedTime: Double? {
get {
return presenter.needle ?? 0
return presenter.needle
}
}
public var prettyElapsedTime: String {
/**
A textual representation of the elapsed playback time of the current audio initialized. Returns nil if no audio is initialized in player.
*/
public var prettyElapsedTime: String? {
get {
return SAPlayer.prettifyTimestamp(elapsedTime)
guard let e = elapsedTime else { return nil }
return SAPlayer.prettifyTimestamp(e)
}
}
/**
Corresponding to the media info to display on the lockscreen for the current audio.
- Note: Setting this to nil clears the information displayed on the lockscreen media player.
*/
public var mediaInfo: SALockScreenInfo? = nil {
didSet {
if let info = mediaInfo {
presenter.handleLockscreenInfo(info: info)
}
presenter.handleLockscreenInfo(info: mediaInfo)
}
}
private init() {
presenter = SAPlayerPresenter(delegate: self)
// https://forums.developer.apple.com/thread/5874
// https://forums.developer.apple.com/thread/6050
// AVAudioTimePitchAlgorithm.timeDomain (just in case we want it)
var componentDescription: AudioComponentDescription {
get {
var ret = AudioComponentDescription()
ret.componentType = kAudioUnitType_FormatConverter
ret.componentSubType = kAudioUnitSubType_AUiPodTimeOther
return ret
}
}
audioModifiers.append(AVAudioUnitTimePitch(audioComponentDescription: componentDescription))
}
/**
Formats a textual representation of a given timestamp for display in hh:MM:SS format, that is hours:minutes:seconds.
- Parameter timestamp: The timestamp to format.
- Returns: A textual representation of the given timestamp
*/
public static func prettifyTimestamp(_ timestamp: Double) -> String {
let hours = Int(timestamp / 60 / 60)
let minutes = Int((timestamp - Double(hours * 60)) / 60)
@@ -96,39 +207,122 @@ public class SAPlayer {
//MARK: - External Player Controls
extension SAPlayer {
/**
Toggles between the play and pause state of the player. If nothing is playable (aka still in buffering state or no audio is initialized) no action will be taken. Please call `startSavedAudio` or `startRemoteAudio` to set up the player with audio before this.
- Note: If you are streaming, wait till the status from `SAPlayer.Updates.PlayingStatus` is not `.buffering`.
*/
public func togglePlayAndPause() {
presenter.handleTogglePlayingAndPausing()
}
/**
Attempts to play the player. If nothing is playable (aka still in buffering state or no audio is initialized) no action will be taken. Please call `startSavedAudio` or `startRemoteAudio` to set up the player with audio before this.
- Note: If you are streaming, wait till the status from `SAPlayer.Updates.PlayingStatus` is not `.buffering`.
*/
public func play() {
presenter.handlePlay()
}
/**
Attempts to pause the player. If nothing is playable (aka still in buffering state or no audio is initialized) no action will be taken. Please call `startSavedAudio` or `startRemoteAudio` to set up the player with audio before this.
- Note:If you are streaming, wait till the status from `SAPlayer.Updates.PlayingStatus` is not `.buffering`.
*/
public func pause() {
presenter.handlePause()
}
public func skipBackwards() {
presenter.handleSkipBackward()
}
/**
Attempts to skip forward in audio even if nothing playable is loaded (aka still in buffering state or no audio is initialized). The interval to which to skip forward is defined by `SAPlayer.shared.skipForwardSeconds`.
- Note: The skipping is limited to the duration of the audio, if the intended skip is past the duration of the current audio, the skip will just go to the end.
*/
public func skipForward() {
presenter.handleSkipForward()
}
/**
Attempts to skip backwards in audio even if nothing playable is loaded (aka still in buffering state or no audio is initialized). The interval to which to skip backwards is defined by `SAPlayer.shared.skipBackwardSeconds`.
- Note: The skipping is limited to the playable timestamps, if the intended skip is below 0 seconds, the skip will just go to 0 seconds.
*/
public func skipBackwards() {
presenter.handleSkipBackward()
}
/**
Attempts to seek/scrub through the audio even if nothing playable is loaded (aka still in buffering state or no audio is initialized).
- Parameter seconds: The intended seconds within the audio to seek to.
- Note: The seeking is limited to the playable timestamps, if the intended seek is below 0 seconds, the skip will just go to 0 seconds. If the intended seek is past the curation of the current audio, the seek will just go to the end.
*/
public func seekTo(seconds: Double) {
presenter.handleSeek(toNeedle: seconds)
}
/**
If using an AVAudioUnitTimePitch, it's important to notify the player that the rate at which the audio playing has changed to keep the media player in the lockscreen up to date. This is only important for playback rate changes.
- Parameter rate: The current rate at which the audio is playing.
*/
public func playbackRateOfAudioChanged(rate: Float) {
presenter.handleAudioRateChanged(rate: rate)
}
/**
Sets up player to play audio that has been saved on the device.
- Important: If intending to use [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements) audio modifiers during playback, the list of audio modifiers under `SAPlayer.shared.audioModifiers` must be finalized before calling this function. After all realtime audio manipulations within the this will be effective.
- Parameter withSavedUrl: The URL of the audio saved on the device.
- Parameter mediaInfo: The media information of the audio to show on the lockscreen media player (optional).
*/
public func startSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
self.mediaInfo = mediaInfo
presenter.handlePlaySavedAudio(withSavedUrl: url)
}
@available(*, deprecated, renamed: "startSavedAudio")
public func initializeSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
self.mediaInfo = mediaInfo
presenter.handlePlaySavedAudio(withSavedUrl: url)
}
public func initializeAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
/**
Sets up player to play audio that will be streamed from a remote location. After this is called, it will connect to the server and start to receive and process data. The player is not playable the SAAudioAvailabilityRange notifies that player is ready for playing (you can subscribe to these updates through `SAPlayer.Updates.StreamingBuffer`). You can alternatively see when the player is available to play by subscribing to `SAPlayer.Updates.PlayingStatus` and waiting for a status that isn't `.buffering`.
- Important: If intending to use [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements) audio modifiers during playback, the list of audio modifiers under `SAPlayer.shared.audioModifiers` must be finalized before calling this function. After all realtime audio manipulations within the this will be effective.
- Note: Subscribe to `SAPlayer.Updates.StreamingBuffer` to see updates in streaming progress.
- Parameter withRemoteUrl: The URL of the remote audio.
- Parameter mediaInfo: The media information of the audio to show on the lockscreen media player (optional).
*/
public func startRemoteAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
self.mediaInfo = mediaInfo
presenter.handlePlayStreamedAudio(withRemoteUrl: url)
}
@available(*, deprecated, renamed: "startRemoteAudio")
public func initializeRemoteAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
self.mediaInfo = mediaInfo
presenter.handlePlayStreamedAudio(withRemoteUrl: url)
}
public func stopStreamingRemoteAudio() {
presenter.handleStopStreamingAudio()
}
/**
Resets the player to the state before initializing audio and setting media info.
*/
public func clear() {
player = nil
presenter.handleClear()
}
}
@@ -146,6 +340,12 @@ extension SAPlayer: SAPlayerDelegate {
player = AudioStreamEngine(withRemoteUrl: url, delegate: presenter)
}
func clearEngine() {
player?.pause()
player?.invalidate()
player = nil
}
func playEngine() {
becomeDeviceAudioPlayer()
player?.play()
@@ -173,13 +373,9 @@ extension SAPlayer: SAPlayerDelegate {
func seekEngine(toNeedle needle: Needle) {
var seekToNeedle = needle < 0 ? 0 : needle
seekToNeedle = needle > Needle(duration) ? Needle(duration) : needle
seekToNeedle = needle > Needle(duration ?? 0) ? Needle(duration ?? 0) : needle
player?.seek(toNeedle: seekToNeedle)
}
func setSpeedEngine(withMultiple multiple: Double) {
player?.setSpeed(speed: multiple)
}
}
+1 -1
View File
@@ -32,8 +32,8 @@ protocol SAPlayerDelegate: AnyObject, LockScreenViewProtocol {
func startAudioDownloaded(withSavedUrl url: AudioURL)
func startAudioStreamed(withRemoteUrl url: AudioURL)
func clearEngine()
func playEngine()
func pauseEngine()
func seekEngine(toNeedle needle: Needle) //TODO ensure that engine cleans up out of bounds
func setSpeedEngine(withMultiple multiple: Double)
}
+2
View File
@@ -41,6 +41,8 @@ extension SAPlayer {
- Note: It's recommended to have a weak reference to a class that uses this function
- Note: Subscribe to `SAPlayer.Updates.AudioDownloading` to see updates in downloading progress.
- Parameter url: The remote url to download audio from.
- Parameter completion: Completion handler that will return once the download is successful and complete.
- Parameter savedUrl: The url of where the audio was saved locally on the device. Will receive once download has completed.
+32 -11
View File
@@ -60,6 +60,18 @@ class SAPlayerPresenter {
urlKeyMap[url.key] = url
}
func handleClear() {
needle = nil
duration = nil
key = nil
mediaInfo = nil
delegate?.clearLockScreenInfo()
AudioClockDirector.shared.detachFromChangesInDuration(withID: durationRef)
AudioClockDirector.shared.detachFromChangesInNeedle(withID: needleRef)
AudioClockDirector.shared.detachFromChangesInPlayingStatus(withID: playingStatusRef)
}
func handlePlaySavedAudio(withSavedUrl url: URL) {
attachForUpdates(url: url)
delegate?.startAudioDownloaded(withSavedUrl: url)
@@ -71,9 +83,7 @@ class SAPlayerPresenter {
}
private func attachForUpdates(url: URL) {
AudioClockDirector.shared.detachFromChangesInDuration(withID: durationRef)
AudioClockDirector.shared.detachFromChangesInNeedle(withID: needleRef)
AudioClockDirector.shared.detachFromChangesInPlayingStatus(withID: playingStatusRef)
detachFromUpdates()
self.key = url.key
urlKeyMap[url.key] = url
@@ -88,9 +98,7 @@ class SAPlayerPresenter {
self.delegate?.updateLockscreenPlaybackDuration(duration: duration)
self.duration = duration
if let info = self.mediaInfo {
self.delegate?.setLockScreenInfo(withMediaInfo: info, duration: duration)
}
self.delegate?.setLockScreenInfo(withMediaInfo: self.mediaInfo, duration: duration)
})
needleRef = AudioClockDirector.shared.attachToChangesInNeedle(closure: { [weak self] (key, needle) in
@@ -115,8 +123,19 @@ class SAPlayerPresenter {
})
}
private func detachFromUpdates() {
AudioClockDirector.shared.detachFromChangesInDuration(withID: durationRef)
AudioClockDirector.shared.detachFromChangesInNeedle(withID: needleRef)
AudioClockDirector.shared.detachFromChangesInPlayingStatus(withID: playingStatusRef)
}
func handleStopStreamingAudio() {
delegate?.clearEngine()
detachFromUpdates()
}
@available(iOS 10.0, *)
func handleLockscreenInfo(info: SALockScreenInfo) {
func handleLockscreenInfo(info: SALockScreenInfo?) {
self.mediaInfo = info
}
}
@@ -156,10 +175,12 @@ extension SAPlayerPresenter {
delegate?.seekEngine(toNeedle: needle)
}
func handleSetSpeed(withMultiple: Double) {
delegate?.setSpeedEngine(withMultiple: withMultiple)
self.delegate?.updateLockscreenChangePlaybackRate(speed: withMultiple)
func handleAudioRateChanged(rate: Float) {
delegate?.updateLockscreenChangePlaybackRate(speed: rate)
}
func handleScrubbingIntervalsChanged() {
delegate?.updateLockscreenSkipIntervals()
}
}
+6
View File
@@ -66,12 +66,16 @@ extension SAPlayer {
/**
Updates to changes in the duration of the current initialized audio. Especially helpful for audio that is being streamed and can change with more data.
- Note: If you are streaming from a source that does not have an expected size at the beginning of a stream, such as live streams, duration will be constantly updating to best known value at the time (which is the seconds buffered currently and not necessarily the actual total duration of audio).
*/
public struct Duration {
/**
Subscribe to updates to changes in duration of the current audio initialized.
- Note: If you are streaming from a source that does not have an expected size at the beginning of a stream, such as live streams, duration will be constantly updating to best known value at the time (which is the seconds buffered currently and not necessarily the actual total duration of audio).
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in duration.
@@ -136,6 +140,8 @@ extension SAPlayer {
/**
Subscribe to updates to changes in the progress of downloading audio for streaming. Information about range of audio available and if the audio is playable. Look at SAAudioAvailabilityRange for more information. For progress of downloading audio that saves to the phone for playback later, look at AudioDownloading instead.
- Note: For live streams that don't have an expected audio length from the beginning of the stream; the duration is constantly changing and equal to the total seconds buffered from the SAAudioAvailabilityRange.
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in duration.
+54
View File
@@ -0,0 +1,54 @@
//
// Data.swift
// SwiftAudioPlayer
//
// Created by Tanha Kabir on 2019-11-29.
// Copyright © 2019 Tanha Kabir, Jon Mercer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
extension Data {
// Introduced in Swift 5, withUnsafeBytes using UnsafePointers is deprecated
// https://mjtsai.com/blog/2019/03/27/swift-5-released/
func accessBytes<R>(_ body: (UnsafePointer<UInt8>) throws -> R) rethrows -> R {
return try withUnsafeBytes { (rawBufferPointer: UnsafeRawBufferPointer) -> R in
let unsafeBufferPointer = rawBufferPointer.bindMemory(to: UInt8.self)
guard let unsafePointer = unsafeBufferPointer.baseAddress else {
Log.error("")
var int: UInt8 = 0
return try body(&int)
}
return try body(unsafePointer)
}
}
mutating func accessMutableBytes<R>(_ body: (UnsafeMutablePointer<UInt8>) throws -> R) rethrows -> R {
return try withUnsafeMutableBytes { (rawBufferPointer: UnsafeMutableRawBufferPointer) -> R in
let unsafeMutableBufferPointer = rawBufferPointer.bindMemory(to: UInt8.self)
guard let unsafeMutablePointer = unsafeMutableBufferPointer.baseAddress else {
Log.error("")
var int: UInt8 = 0
return try body(&int)
}
return try body(unsafeMutablePointer)
}
}
}
+25 -14
View File
@@ -9,22 +9,23 @@
import Foundation
import os.log
// Possible levels of log messages to log
enum LogLevel: Int {
case DEBUG = 1
case INFO = 2
case WARN = 3
case ERROR = 4
case EXTERNAL_DEBUG = 5
case MONITOR = 6
case TEST = 7
}
// Specify which types of log messages to display. Default level is set to WARN, which means Log will print any log messages of type only WARN, ERROR, MONITOR, and TEST. To print DEBUG and INFO logs, set the level to a lower value.
var logLevel: LogLevel = LogLevel.MONITOR
class Log {
private init() {}
// Possible levels of log messages to log
public enum LogLevel: Int {
case DEBUG = 1
case INFO = 2
case WARN = 3
case ERROR = 4
case MONITOR = 5
case TEST = 6
}
// Specify which types of log messages to display. Default level is set to WARN, which means Log will print any log messages of type only WARN, ERROR, MONITOR, and TEST. To print DEBUG and INFO logs, set the level to a lower value.
public static var logLevel: LogLevel = LogLevel.ERROR
// Used for OSLog
private static let SUBSYSTEM: String = "com.SwiftAudioPlayer"
@@ -68,6 +69,11 @@ class Log {
let log = OSLog(subsystem: SUBSYSTEM, category: "ERROR 🛑🛑🛑🛑")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
}
if logLevel.rawValue <= LogLevel.EXTERNAL_DEBUG.rawValue {
let log = OSLog(subsystem: SUBSYSTEM, category: "WARNING")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
}
}
/**
@@ -86,7 +92,7 @@ class Log {
public static func monitor(_ logMessage: Any, classPath: String = #file, functionName: String = #function, lineNumber: Int = #line) {
let fileName = URLUtil.getNameFromStringPath(classPath)
if logLevel.rawValue <= LogLevel.ERROR.rawValue {
let log = OSLog(subsystem: SUBSYSTEM, category: "MONITOR 🔥🔥🔥🔥")
let log = OSLog(subsystem: SUBSYSTEM, category: "ERROR 🔥🔥🔥🔥")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
}
}
@@ -110,6 +116,11 @@ class Log {
let log = OSLog(subsystem: SUBSYSTEM, category: "WARN ⚠️⚠️⚠️⚠️")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
}
if logLevel.rawValue <= LogLevel.EXTERNAL_DEBUG.rawValue {
let log = OSLog(subsystem: SUBSYSTEM, category: "DEBUG")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
}
}
/**
+2 -2
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudioPlayer'
s.version = '1.3.0'
s.version = '2.9.0'
s.summary = 'SwiftAudioPlayer is a Swift based audio player that can handle streaming from a remote location and audio manipulation.'
# This description is used to generate tags and improve search results.
@@ -31,7 +31,7 @@ SwiftAudioPlayer is a Swift based audio player that can handle streaming from a
s.ios.deployment_target = '10.0'
s.source_files = 'Source/**/*'
s.swift_version = '4.2'
s.swift_version = '5.0'
# s.resource_bundles = {
# 'SwiftAudioPlayer' => ['SwiftAudioPlayer/Assets/*.png']