Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 953f463243 | |||
| 70ba1c757e | |||
| 3ab47b568d | |||
| 6fd985d2ad | |||
| cf028e0e36 | |||
| f9e6dafc2c | |||
| e562a259fb | |||
| 9594b560d0 | |||
| bf2dae9569 | |||
| 90ac3a4336 | |||
| 395364b4eb | |||
| 6a2bb94037 | |||
| 7d81953b83 | |||
| feb69174ae | |||
| 00eee68aab | |||
| 6276e97c4c | |||
| 09142ce2d4 | |||
| 90bc2262ec | |||
| 9594449215 | |||
| 6187c9f438 | |||
| b28e815545 | |||
| 17be73bbe8 | |||
| cd35f38db1 | |||
| 3c752d581d | |||
| 1f20a48a20 | |||
| 3a585c1f43 | |||
| 5ac5b93ac4 | |||
| b484f0bfb6 | |||
| 0aeb8b0f88 | |||
| 8e7357860c | |||
| 936de8c996 | |||
| e986be9db5 |
+13
-5
@@ -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, ); }; };
|
||||
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 */; };
|
||||
A4681FC82201138E0018AB51 /* SAPlayerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F8F2200E1450018AB51 /* SAPlayerPresenter.swift */; };
|
||||
@@ -95,6 +96,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>"; };
|
||||
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>"; };
|
||||
A4681F822200D9150018AB51 /* AudioEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioEngine.swift; sourceTree = "<group>"; };
|
||||
@@ -278,6 +280,7 @@
|
||||
A4681F932200E2020018AB51 /* Engine */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A41AA0D1238BB9B600A467E1 /* SAPlayingStatus.swift */,
|
||||
A4FBA6B8221BAF8700D5A353 /* SAAudioAvailabilityRange.swift */,
|
||||
A4681F822200D9150018AB51 /* AudioEngine.swift */,
|
||||
A4681F942200E2220018AB51 /* AudioDiskEngine.swift */,
|
||||
@@ -479,11 +482,14 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0930;
|
||||
LastUpgradeCheck = 0930;
|
||||
LastUpgradeCheck = 1010;
|
||||
TargetAttributes = {
|
||||
042ACE071BA515F4DE0E0C8007C3F0EE = {
|
||||
LastSwiftMigration = 1010;
|
||||
};
|
||||
E50DAD13FFD3FC8036073A58BF8423D4 = {
|
||||
LastSwiftMigration = 1010;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 2D8E8EC45A3A1A1D94AE762CB5028504 /* Build configuration list for PBXProject "Pods" */;
|
||||
@@ -510,6 +516,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A41AA0D2238BB9B600A467E1 /* SAPlayingStatus.swift in Sources */,
|
||||
A4681FDC220113D70018AB51 /* AudioDownloadWorker.swift in Sources */,
|
||||
A4681FD8220113C60018AB51 /* AudioDataManager.swift in Sources */,
|
||||
A4681FD1220113AF0018AB51 /* AudioParsable.swift in Sources */,
|
||||
@@ -634,7 +641,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)";
|
||||
@@ -669,7 +676,7 @@
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 4.2;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
@@ -701,7 +708,7 @@
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 4.2;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -796,10 +803,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,18 +207,18 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0830;
|
||||
LastUpgradeCheck = 0830;
|
||||
LastUpgradeCheck = 1010;
|
||||
ORGANIZATIONNAME = CocoaPods;
|
||||
TargetAttributes = {
|
||||
607FACCF1AFB9204008FA782 = {
|
||||
CreatedOnToolsVersion = 6.3.1;
|
||||
DevelopmentTeam = R2392A68YQ;
|
||||
LastSwiftMigration = 0900;
|
||||
LastSwiftMigration = 1010;
|
||||
};
|
||||
607FACE41AFB9204008FA782 = {
|
||||
CreatedOnToolsVersion = 6.3.1;
|
||||
DevelopmentTeam = R2392A68YQ;
|
||||
LastSwiftMigration = 0900;
|
||||
LastSwiftMigration = 1010;
|
||||
TestTargetID = 607FACCF1AFB9204008FA782;
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
@@ -477,8 +481,7 @@
|
||||
MODULE_NAME = ExampleApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 4.2;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -493,8 +496,7 @@
|
||||
MODULE_NAME = ExampleApp;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 4.2;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -515,8 +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_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 4.2;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudioPlayer_Example.app/SwiftAudioPlayer_Example";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -534,8 +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_SWIFT3_OBJC_INFERENCE = Default;
|
||||
SWIFT_VERSION = 4.0;
|
||||
SWIFT_VERSION = 4.2;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudioPlayer_Example.app/SwiftAudioPlayer_Example";
|
||||
};
|
||||
name = Release;
|
||||
|
||||
+1
-3
@@ -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"
|
||||
|
||||
@@ -15,7 +15,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@@ -20,50 +22,56 @@
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="lTK-Hd-Tl2">
|
||||
<rect key="frame" x="16" y="303" width="343" height="2"/>
|
||||
<rect key="frame" x="16" y="320" width="343" height="2"/>
|
||||
<color key="tintColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="progressTintColor" red="0.46202266219999999" green="0.83828371759999998" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="trackTintColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</progressView>
|
||||
<slider opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="749" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="w2a-RA-zmI">
|
||||
<rect key="frame" x="14" y="288" width="347" height="31"/>
|
||||
<rect key="frame" x="14" y="305" width="347" height="31"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="maximumTrackTintColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<connections>
|
||||
<action selector="beingScrubberSeeked:" destination="vXZ-lx-hvc" eventType="touchDown" id="UXg-Wf-fKv"/>
|
||||
<action selector="scrubberSeeked:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="hTi-fq-lrl"/>
|
||||
<action selector="scrubberSeeked:" destination="vXZ-lx-hvc" eventType="touchUpOutside" id="mFP-SW-38w"/>
|
||||
<action selector="scrubberStartedSeeking:" destination="vXZ-lx-hvc" eventType="touchDown" id="UXg-Wf-fKv"/>
|
||||
</connections>
|
||||
</slider>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="jUc-tP-CC5">
|
||||
<rect key="frame" x="172.5" y="233" width="30" height="30"/>
|
||||
<rect key="frame" x="172.5" y="250" width="30" height="30"/>
|
||||
<state key="normal" title="play"/>
|
||||
<connections>
|
||||
<action selector="playPauseTouched:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="Avk-K3-EZ7"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tFH-sY-Xu9">
|
||||
<rect key="frame" x="62.5" y="233" width="30" height="30"/>
|
||||
<rect key="frame" x="62.5" y="250" width="30" height="30"/>
|
||||
<state key="normal" title="-15"/>
|
||||
<connections>
|
||||
<action selector="skipBackwardTouched:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="PCT-BE-udf"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="0QE-3F-a4G">
|
||||
<rect key="frame" x="282.5" y="233" width="30" height="30"/>
|
||||
<rect key="frame" x="282.5" y="250" width="30" height="30"/>
|
||||
<state key="normal" title="+30"/>
|
||||
<connections>
|
||||
<action selector="skipForwardTouched:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="uXv-bz-tnt"/>
|
||||
</connections>
|
||||
</button>
|
||||
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="1" minValue="0.10000000000000001" maxValue="32" translatesAutoresizingMaskIntoConstraints="NO" id="vfk-OJ-S3T">
|
||||
<rect key="frame" x="14" y="447" width="347" height="31"/>
|
||||
<rect key="frame" x="14" y="464" width="347" height="31"/>
|
||||
<connections>
|
||||
<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="60" width="343" height="32"/>
|
||||
<rect key="frame" x="16" y="80" width="343" height="29"/>
|
||||
<segments>
|
||||
<segment title="Soundbite"/>
|
||||
<segment title="Acquired"/>
|
||||
@@ -74,46 +82,53 @@
|
||||
</connections>
|
||||
</segmentedControl>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="KDu-ea-kF8">
|
||||
<rect key="frame" x="78" y="123" width="69" height="30"/>
|
||||
<rect key="frame" x="78" y="140" width="69" height="30"/>
|
||||
<state key="normal" title="Download"/>
|
||||
<connections>
|
||||
<action selector="downloadTouched:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="8Jg-1C-0Ms"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6d9-Bc-hIz">
|
||||
<rect key="frame" x="244" y="123" width="49" height="30"/>
|
||||
<rect key="frame" x="244" y="140" width="49" height="30"/>
|
||||
<state key="normal" title="Stream"/>
|
||||
<connections>
|
||||
<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="418" 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"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="0:00" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="j3w-gr-HzF">
|
||||
<rect key="frame" x="16" y="280" width="27" height="15"/>
|
||||
<rect key="frame" x="16" y="297" width="27" height="15"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="100:00" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Urj-Dv-41y">
|
||||
<rect key="frame" x="319" y="280" width="40" height="15"/>
|
||||
<rect key="frame" x="319" y="297" width="40" height="15"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="remote url: " textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1IX-z5-wWx">
|
||||
<rect key="frame" x="16" y="190" width="343" height="16"/>
|
||||
<rect key="frame" x="16" y="207" width="343" height="16"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="13"/>
|
||||
<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"/>
|
||||
@@ -131,18 +146,23 @@
|
||||
<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"/>
|
||||
<constraint firstItem="jUc-tP-CC5" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="tFH-sY-Xu9" secondAttribute="trailing" constant="8" symbolic="YES" id="fS9-Ce-4ph"/>
|
||||
<constraint firstItem="Urj-Dv-41y" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="j3w-gr-HzF" secondAttribute="trailing" constant="8" symbolic="YES" id="fu0-ZZ-rj9"/>
|
||||
<constraint firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" constant="16" id="gdg-7Y-7la"/>
|
||||
<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"/>
|
||||
@@ -160,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"/>
|
||||
|
||||
@@ -22,6 +22,12 @@
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>fetch</string>
|
||||
<string>processing</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
import SwiftAudioPlayer
|
||||
import AVFoundation
|
||||
|
||||
class ViewController: UIViewController {
|
||||
struct AudioInfo: Hashable {
|
||||
@@ -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,11 @@ class ViewController: UIViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
adjustSpeed()
|
||||
|
||||
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,15 +117,14 @@ class ViewController: UIViewController {
|
||||
|
||||
_ = SAPlayer.Updates.ElapsedTime.subscribe { [weak self] (url, position) in
|
||||
guard let self = self else { return }
|
||||
if self.beingSeeked == false{
|
||||
guard url == self.selectedAudio.url || url == self.savedUrls[self.selectedAudio] else { return }
|
||||
|
||||
self.currentTimestampLabel.text = SAPlayer.prettifyTimestamp(position)
|
||||
|
||||
guard self.duration != 0 else { return }
|
||||
|
||||
self.scrubberSlider.value = Float(position/self.duration)
|
||||
}
|
||||
guard self.beingSeeked == false else { return }
|
||||
guard url == self.selectedAudio.url || url == self.savedUrls[self.selectedAudio] else { return }
|
||||
|
||||
self.currentTimestampLabel.text = SAPlayer.prettifyTimestamp(position)
|
||||
|
||||
guard self.duration != 0 else { return }
|
||||
|
||||
self.scrubberSlider.value = Float(position/self.duration)
|
||||
}
|
||||
|
||||
_ = SAPlayer.Updates.AudioDownloading.subscribe { [weak self] (url, progress) in
|
||||
@@ -159,13 +161,28 @@ class ViewController: UIViewController {
|
||||
guard let self = self else { return }
|
||||
guard url == self.selectedAudio.url || url == self.savedUrls[self.selectedAudio] else { return }
|
||||
|
||||
if playing {
|
||||
switch playing {
|
||||
case .playing:
|
||||
self.isPlayable = true
|
||||
self.playPauseButton.setTitle("Pause", for: .normal)
|
||||
} else {
|
||||
return
|
||||
case .paused:
|
||||
self.isPlayable = true
|
||||
self.playPauseButton.setTitle("Play", for: .normal)
|
||||
return
|
||||
case .buffering:
|
||||
self.isPlayable = false
|
||||
self.playPauseButton.setTitle("Loading", for: .normal)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addRandomModifiers() {
|
||||
let node = AVAudioUnitReverb()
|
||||
SAPlayer.shared.audioModifiers.append(node)
|
||||
node.wetDryMix = 300
|
||||
}
|
||||
|
||||
override func didReceiveMemoryWarning() {
|
||||
super.didReceiveMemoryWarning()
|
||||
@@ -181,7 +198,7 @@ class ViewController: UIViewController {
|
||||
|
||||
// if let savedUrl = savedUrls[selectedAudio] {}
|
||||
}
|
||||
@IBAction func beingScrubberSeeked(_ sender: UISlider) {
|
||||
@IBAction func scrubberStartedSeeking(_ sender: UISlider) {
|
||||
beingSeeked = true
|
||||
}
|
||||
|
||||
@@ -189,12 +206,23 @@ class ViewController: UIViewController {
|
||||
let value = Double(scrubberSlider.value) * duration
|
||||
SAPlayer.shared.seekTo(seconds: value)
|
||||
beingSeeked = false
|
||||
SAPlayer.shared.play()
|
||||
}
|
||||
|
||||
|
||||
@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) {
|
||||
@@ -228,7 +256,7 @@ class ViewController: UIViewController {
|
||||
|
||||
@IBAction func streamTouched(_ sender: Any) {
|
||||
if !isStreaming {
|
||||
SAPlayer.shared.initializeAudio(withRemoteUrl: selectedAudio.url)
|
||||
SAPlayer.shared.initializeRemoteAudio(withRemoteUrl: selectedAudio.url)
|
||||
streamButton.setTitle("Cancel streaming", for: .normal)
|
||||
downloadButton.isEnabled = false
|
||||
} else {
|
||||
@@ -248,11 +276,5 @@ class ViewController: UIViewController {
|
||||
SAPlayer.shared.skipForward()
|
||||
}
|
||||
|
||||
private func adjustSpeed() {
|
||||
let speed = rateSlider.value
|
||||
rateLabel.text = "rate: \(speed)x"
|
||||
SAPlayer.shared.rate = Double(speed)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ 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")!
|
||||
@@ -65,7 +67,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 +111,73 @@ 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
|
||||
|
||||
To set up player with audio to play, use either:
|
||||
* `initializeSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo?)` to play audio that is saved on the device.
|
||||
* `initializeRemoteAudio(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.
|
||||
|
||||
#### Important
|
||||
|
||||
Any audio manipulation intended to on the audio must have the nodes anticipated to use finalized before initialize is called. Look at [audio manipulation documentation](#realtime-audio-manipulation) for more information.
|
||||
|
||||
All other basic controls are available:
|
||||
```swift
|
||||
play()
|
||||
pause()
|
||||
togglePlayAndPause()
|
||||
seekTo(seconds: Double)
|
||||
skipForward()
|
||||
skipBackwards()
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
@@ -118,6 +206,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
|
||||
@@ -178,9 +268,9 @@ 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.
|
||||
|
||||
### PlayingStatus
|
||||
Payload = `Bool`
|
||||
Payload = `SAPlayingStatus`
|
||||
|
||||
Changes in the playing/paused status of the player.
|
||||
Changes in the playing status of the player. Can be one of the following 3: `playing`, `paused`, `buffering`.
|
||||
|
||||
### StreamingBuffer
|
||||
Payload = `SAAudioAvailabilityRange`
|
||||
|
||||
@@ -31,7 +31,7 @@ class AudioClockDirector {
|
||||
|
||||
private var needleClosures: DirectorThreadSafeClosures<Needle> = DirectorThreadSafeClosures()
|
||||
private var durationClosures: DirectorThreadSafeClosures<Duration> = DirectorThreadSafeClosures()
|
||||
private var playingStatusClosures: DirectorThreadSafeClosures<IsPlaying> = DirectorThreadSafeClosures()
|
||||
private var playingStatusClosures: DirectorThreadSafeClosures<SAPlayingStatus> = DirectorThreadSafeClosures()
|
||||
private var bufferClosures: DirectorThreadSafeClosures<SAAudioAvailabilityRange> = DirectorThreadSafeClosures()
|
||||
|
||||
private init() {}
|
||||
@@ -60,7 +60,7 @@ class AudioClockDirector {
|
||||
|
||||
|
||||
// Playing status
|
||||
func attachToChangesInPlayingStatus(closure: @escaping (Key, IsPlaying) throws -> Void) -> UInt{
|
||||
func attachToChangesInPlayingStatus(closure: @escaping (Key, SAPlayingStatus) throws -> Void) -> UInt{
|
||||
return playingStatusClosures.attach(closure: closure)
|
||||
}
|
||||
|
||||
@@ -103,12 +103,8 @@ extension AudioClockDirector {
|
||||
}
|
||||
|
||||
extension AudioClockDirector {
|
||||
func audioPaused(_ key: Key) {
|
||||
playingStatusClosures.broadcast(key: key, payload: false)
|
||||
}
|
||||
|
||||
func audioPlaying(_ key: Key) {
|
||||
playingStatusClosures.broadcast(key: key, payload: true)
|
||||
func audioPlayingStatusWasChanged(_ key: Key, status: SAPlayingStatus) {
|
||||
playingStatusClosures.broadcast(key: key, payload: status)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ protocol AudioEngineProtocol {
|
||||
func play()
|
||||
func pause()
|
||||
func seek(toNeedle needle: Needle)
|
||||
func setSpeed(speed: Double)
|
||||
func invalidate()
|
||||
}
|
||||
|
||||
@@ -45,7 +44,6 @@ class AudioEngine: AudioEngineProtocol {
|
||||
|
||||
let engine = AVAudioEngine()
|
||||
let playerNode = AVAudioPlayerNode()
|
||||
let rateNode: AVAudioUnitTimePitch
|
||||
|
||||
var timer: Timer?
|
||||
|
||||
@@ -57,12 +55,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 {
|
||||
@@ -79,17 +71,13 @@ class AudioEngine: AudioEngineProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
var isPlaying = false {
|
||||
var playingStatus: SAPlayingStatus? = nil {
|
||||
didSet {
|
||||
guard isPlaying != oldValue else {
|
||||
guard playingStatus != oldValue, let status = playingStatus else {
|
||||
return
|
||||
}
|
||||
|
||||
if isPlaying {
|
||||
AudioClockDirector.shared.audioPlaying(key)
|
||||
} else {
|
||||
AudioClockDirector.shared.audioPaused(key)
|
||||
}
|
||||
AudioClockDirector.shared.audioPlayingStatusWasChanged(key, status: status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,24 +107,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()
|
||||
}
|
||||
@@ -149,7 +148,13 @@ class AudioEngine: AudioEngineProtocol {
|
||||
}
|
||||
|
||||
func updateIsPlaying() {
|
||||
isPlaying = engine.isRunning && playerNode.isPlaying
|
||||
if !bufferedSeconds.isPlayable {
|
||||
playingStatus = .buffering
|
||||
return
|
||||
}
|
||||
|
||||
let isPlaying = engine.isRunning && playerNode.isPlaying
|
||||
playingStatus = isPlaying ? .playing : .paused
|
||||
}
|
||||
|
||||
func play() {
|
||||
@@ -184,10 +189,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() {
|
||||
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ import AVFoundation
|
||||
class AudioStreamEngine: AudioEngine {
|
||||
//Constants
|
||||
private let MAX_POLL_BUFFER_COUNT = 300 //Having one buffer in engine at a time is choppy.
|
||||
private let MIN_BUFFERS_TO_BE_PLAYABLE = 1
|
||||
private let PCM_BUFFER_SIZE: AVAudioFrameCount = 8192
|
||||
|
||||
private let queue = DispatchQueue(label: "SwiftAudioPlayer.engine", qos: .userInitiated)
|
||||
@@ -86,7 +87,9 @@ class AudioStreamEngine: AudioEngine {
|
||||
didSet {
|
||||
if numberOfBuffersScheduledFromPoll > MAX_POLL_BUFFER_COUNT {
|
||||
shouldPollForNextBuffer = false
|
||||
|
||||
}
|
||||
|
||||
if numberOfBuffersScheduledFromPoll > MIN_BUFFERS_TO_BE_PLAYABLE {
|
||||
if wasPlaying {
|
||||
play()
|
||||
wasPlaying = false
|
||||
@@ -213,7 +216,7 @@ class AudioStreamEngine: AudioEngine {
|
||||
|
||||
private func updateNetworkBufferRange() { //for ui
|
||||
let range = converter.pollNetworkAudioAvailabilityRange()
|
||||
isPlayable = (numberOfBuffersScheduledInTotal > 0 && range.1 > 0) && predictedStreamDuration > 0
|
||||
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)
|
||||
}
|
||||
@@ -262,7 +265,6 @@ class AudioStreamEngine: AudioEngine {
|
||||
|
||||
self.needle = needle //to tick while paused
|
||||
|
||||
|
||||
queue.sync { [weak self] in
|
||||
self?.seekHelperDispatchQueue(needle: needle)
|
||||
}
|
||||
@@ -290,6 +292,8 @@ class AudioStreamEngine: AudioEngine {
|
||||
playerNode.stop()
|
||||
|
||||
shouldPollForNextBuffer = true
|
||||
|
||||
updateNetworkBufferRange()
|
||||
}
|
||||
|
||||
override func invalidate() {
|
||||
|
||||
@@ -174,6 +174,10 @@ class AudioConverter: AudioConvertable {
|
||||
}
|
||||
|
||||
private func getPacketIndex(forNeedle needle: Needle) -> AVAudioPacketCount? {
|
||||
guard needle >= 0 else {
|
||||
Log.error("needle should never be a negative number! needle received: \(needle)")
|
||||
return nil
|
||||
}
|
||||
guard let frame = frameOffset(forTime: TimeInterval(needle)) else { return nil }
|
||||
guard let framesPerPacket = parser.fileAudioFormat?.streamDescription.pointee.mFramesPerPacket else { return nil }
|
||||
return AVAudioPacketCount(frame) / AVAudioPacketCount(framesPerPacket)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// SAPlayingStatus.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-11-24.
|
||||
// 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
|
||||
|
||||
public enum SAPlayingStatus {
|
||||
case playing
|
||||
case paused
|
||||
case buffering
|
||||
}
|
||||
@@ -36,9 +36,14 @@ protocol LockScreenViewProtocol {
|
||||
|
||||
extension LockScreenViewProtocol {
|
||||
@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 +73,6 @@ extension LockScreenViewProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
||||
}
|
||||
|
||||
@@ -147,4 +148,23 @@ extension LockScreenViewProtocol {
|
||||
func updateLockscreenPlaybackDuration(duration: Duration) {
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPMediaItemPropertyPlaybackDuration] = NSNumber(value: duration)
|
||||
}
|
||||
|
||||
func updateLockscreenPaused(){
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = 0.0
|
||||
}
|
||||
|
||||
func updateLockscreenPlaying(){
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = 1.0
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
||||
+157
-27
@@ -27,55 +27,127 @@ import Foundation
|
||||
import AVFoundation
|
||||
|
||||
public class SAPlayer {
|
||||
/**
|
||||
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
|
||||
|
||||
public var rate: Double = 1.0 {
|
||||
/**
|
||||
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.
|
||||
*/
|
||||
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,36 +168,89 @@ public class SAPlayer {
|
||||
|
||||
//MARK: - External Player Controls
|
||||
extension SAPlayer {
|
||||
/**
|
||||
Toggles between the play and pause state of the player if the player is not buffering (thus is playable).
|
||||
*/
|
||||
public func togglePlayAndPause() {
|
||||
presenter.handleTogglePlayingAndPausing()
|
||||
}
|
||||
|
||||
/**
|
||||
Attempts to play the player even if nothing playable is loaded (aka still in buffering state or no audio is initialized).
|
||||
*/
|
||||
public func play() {
|
||||
presenter.handlePlay()
|
||||
}
|
||||
|
||||
/**
|
||||
Attempts to pause the player even if nothing playable is loaded (aka still in buffering state or no audio is initialized).
|
||||
*/
|
||||
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 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.
|
||||
|
||||
- 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 initializeRemoteAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
|
||||
self.mediaInfo = mediaInfo
|
||||
presenter.handlePlayStreamedAudio(withRemoteUrl: url)
|
||||
}
|
||||
@@ -159,7 +284,9 @@ extension SAPlayer: SAPlayerDelegate {
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
try AVAudioSession.sharedInstance().setActive(true, with: .notifyOthersOnDeactivation)
|
||||
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: AVAudioSession.Mode(rawValue: convertFromAVAudioSessionMode(AVAudioSession.Mode.default)), options: .allowAirPlay)
|
||||
|
||||
try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
|
||||
} catch {
|
||||
Log.monitor("Problem setting up AVAudioSession to play in:: \(error.localizedDescription)")
|
||||
}
|
||||
@@ -170,11 +297,14 @@ extension SAPlayer: SAPlayerDelegate {
|
||||
}
|
||||
|
||||
func seekEngine(toNeedle needle: Needle) {
|
||||
player?.seek(toNeedle: needle)
|
||||
}
|
||||
|
||||
func setSpeedEngine(withMultiple multiple: Double) {
|
||||
player?.setSpeed(speed: multiple)
|
||||
var seekToNeedle = needle < 0 ? 0 : needle
|
||||
seekToNeedle = needle > Needle(duration ?? 0) ? Needle(duration ?? 0) : needle
|
||||
player?.seek(toNeedle: seekToNeedle)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Helper function inserted by Swift 4.2 migrator.
|
||||
fileprivate func convertFromAVAudioSessionMode(_ input: AVAudioSession.Mode) -> String {
|
||||
return input.rawValue
|
||||
}
|
||||
|
||||
@@ -35,5 +35,4 @@ protocol SAPlayerDelegate: AnyObject, LockScreenViewProtocol {
|
||||
func playEngine()
|
||||
func pauseEngine()
|
||||
func seekEngine(toNeedle needle: Needle) //TODO ensure that engine cleans up out of bounds
|
||||
func setSpeedEngine(withMultiple multiple: Double)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -35,7 +35,7 @@ class SAPlayerPresenter {
|
||||
var duration: Duration?
|
||||
|
||||
private var key: String?
|
||||
private var isPlaying = false
|
||||
private var isPlaying: SAPlayingStatus = .buffering
|
||||
private var mediaInfo: SALockScreenInfo?
|
||||
|
||||
private var urlKeyMap: [Key: URL] = [:]
|
||||
@@ -88,9 +88,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
|
||||
@@ -116,7 +114,7 @@ class SAPlayerPresenter {
|
||||
}
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
func handleLockscreenInfo(info: SALockScreenInfo) {
|
||||
func handleLockscreenInfo(info: SALockScreenInfo?) {
|
||||
self.mediaInfo = info
|
||||
}
|
||||
}
|
||||
@@ -126,16 +124,18 @@ class SAPlayerPresenter {
|
||||
extension SAPlayerPresenter {
|
||||
func handlePause() {
|
||||
delegate?.pauseEngine()
|
||||
self.delegate?.updateLockscreenPaused()
|
||||
}
|
||||
|
||||
func handlePlay() {
|
||||
delegate?.playEngine()
|
||||
self.delegate?.updateLockscreenPlaying()
|
||||
}
|
||||
|
||||
func handleTogglePlayingAndPausing() {
|
||||
if isPlaying {
|
||||
if isPlaying == .playing {
|
||||
handlePause()
|
||||
} else {
|
||||
} else if isPlaying == .paused {
|
||||
handlePlay()
|
||||
}
|
||||
}
|
||||
@@ -154,15 +154,19 @@ extension SAPlayerPresenter {
|
||||
delegate?.seekEngine(toNeedle: needle)
|
||||
}
|
||||
|
||||
func handleSetSpeed(withMultiple: Double) {
|
||||
delegate?.setSpeedEngine(withMultiple: withMultiple)
|
||||
func handleAudioRateChanged(rate: Float) {
|
||||
delegate?.updateLockscreenChangePlaybackRate(speed: rate)
|
||||
}
|
||||
|
||||
func handleScrubbingIntervalsChanged() {
|
||||
delegate?.updateLockscreenSkipIntervals()
|
||||
}
|
||||
}
|
||||
|
||||
//MARK:- For lock screen
|
||||
extension SAPlayerPresenter {
|
||||
func getIsPlaying() -> Bool {
|
||||
return isPlaying
|
||||
return isPlaying == .playing
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ extension SAPlayer {
|
||||
- Parameter playingStatus: Whether the player is playing audio or paused.
|
||||
- Returns: the id for the subscription in the case you would like to unsubscribe to updates for the closure.
|
||||
*/
|
||||
public static func subscribe(_ closure: @escaping (_ url: URL, _ playingStatus: Bool) -> ()) -> UInt {
|
||||
public static func subscribe(_ closure: @escaping (_ url: URL, _ playingStatus: SAPlayingStatus) -> ()) -> UInt {
|
||||
return AudioClockDirector.shared.attachToChangesInPlayingStatus(closure: { (key, isPlaying) in
|
||||
guard let url = SAPlayer.shared.getUrl(forKey: key) else { return }
|
||||
closure(url, isPlaying)
|
||||
|
||||
@@ -23,7 +23,7 @@ class Log {
|
||||
}
|
||||
|
||||
// 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
|
||||
public static var logLevel: LogLevel = LogLevel.MONITOR
|
||||
|
||||
// Used for OSLog
|
||||
private static let SUBSYSTEM: String = "com.SwiftAudioPlayer"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudioPlayer'
|
||||
s.version = '1.1.1'
|
||||
s.version = '2.0.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.0'
|
||||
s.swift_version = '4.2'
|
||||
|
||||
# s.resource_bundles = {
|
||||
# 'SwiftAudioPlayer' => ['SwiftAudioPlayer/Assets/*.png']
|
||||
|
||||
Reference in New Issue
Block a user