Compare commits

...

33 Commits

Author SHA1 Message Date
tanhakabir bf724ab04e Merge branch 'master' into issue-67 2021-03-22 23:06:44 -07:00
tanhakabir 97909bacce Release 4.1.0 2021-03-22 23:04:53 -07:00
tanhakabir 30b0189f61 Merge Fix PCM memory leak #87 2021-03-22 23:01:35 -07:00
tanhakabir 5bde849bf0 fix PCM memory leak 2021-03-22 23:00:35 -07:00
tanhakabir b3b519ab4c Revert "audio skips, but reusing same pcm buffers"
This reverts commit f3b62cc756.
2021-03-22 22:49:31 -07:00
tanhakabir f3b62cc756 audio skips, but reusing same pcm buffers 2021-03-22 11:52:29 -07:00
tanhakabir a56d3314ad Update README.md 2021-03-22 11:50:53 -07:00
tanhakabir f75d743cd9 small refractor 2021-03-20 11:59:06 -07:00
tanhakabir f8876d821e remove unnecessary recursion helper 2021-03-20 11:54:56 -07:00
tanhakabir 81d93fd886 Merge branch 'master' into issue-67 2021-03-18 01:06:17 -07:00
tanhakabir bca8fde2de Release 4.0.0 2021-03-18 00:57:00 -07:00
tanhakabir efbaa465b2 remove unused functions 2021-03-18 00:44:30 -07:00
tanhakabir 20f1d72058 implement basic autoplay 2021-03-18 00:44:30 -07:00
tanhakabir 6c3b1efe97 fully implement queuing audio 2021-03-18 00:44:30 -07:00
tanhakabir a98f090b6a checkpoint, timer has strong reference to engine 2021-03-18 00:44:30 -07:00
tanhakabir 542f65f044 add initial queuing functionality from Joe but this will crash
Co-Authored-By: Joe Williams <14778951+jw1540@users.noreply.github.com>
2021-03-18 00:44:30 -07:00
tanhakabir f4a1141f65 clean up project 2021-03-18 00:44:30 -07:00
tanhakabir a034c7dc6f reset project settings 2021-03-18 00:44:30 -07:00
tanhakabir d9c6d18921 Fix initial state of feature switches to off 2021-03-10 17:41:53 -08:00
tanhakabir 7eb3d601fa Add minor documentation to skip silences feature
Co-Authored-By: Joe Williams <14778951+jw1540@users.noreply.github.com>
2021-03-10 14:43:19 -08:00
tanhakabir 9b375b99dc Release 3.0.0 2021-03-10 14:37:13 -08:00
tanhakabir ee80976e92 add sleep timer feature
Co-Authored-By: Joe Williams <14778951+jw1540@users.noreply.github.com>
2021-03-10 14:25:43 -08:00
tanhakabir 10aea39cae expose ID for current player 2021-03-10 14:25:43 -08:00
tanhakabir 431fdc6428 add rate as a core property of SAPlayer (#78) 2021-03-10 14:05:58 -08:00
tanhakabir eda60a3c3d remove random audio modifiers to see skip silences in work 2021-03-10 12:59:02 -08:00
tanhakabir d7b90f1f58 use Joe's original rate for speeding through silences
Co-Authored-By: Joe Williams <14778951+jw1540@users.noreply.github.com>
2021-03-10 12:59:02 -08:00
tanhakabir 08b30307aa Add PR from Joe for skipping silences
Co-Authored-By: Joe Williams <14778951+jw1540@users.noreply.github.com>
2021-03-10 12:59:02 -08:00
tanhakabir 22bbe7fa5a checkpoint 2021-03-09 16:58:51 -08:00
tanhakabir 475a2a2b37 add seeking to throttler diagram 2021-03-06 21:13:36 -08:00
tanhakabir b4b571f68b create throttler state machine diagram 2021-03-06 21:06:46 -08:00
tanhakabir 1dfc5095e9 Merge branch 'master' into issue-67 2021-03-06 20:36:27 -08:00
tanhakabir b6da69c798 checkpoint 2021-03-06 18:37:04 -08:00
tanhakabir ce07c919a9 add states to logs 2021-03-05 18:20:50 -08:00
25 changed files with 989 additions and 278 deletions
+32 -8
View File
@@ -15,12 +15,13 @@
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 */; };
A411CE3E25F55C0E0039E1CD /* AudioThrottlerNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = A411CE3D25F55C0E0039E1CD /* AudioThrottlerNew.swift */; };
A411CE4625F9609D0039E1CD /* SAPlayerFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = A411CE4525F9609D0039E1CD /* SAPlayerFeatures.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 */; };
A4681FC82201138E0018AB51 /* SAPlayerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F8F2200E1450018AB51 /* SAPlayerPresenter.swift */; };
A4681FC9220113920018AB51 /* LockScreenViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681FBE22010ECF0018AB51 /* LockScreenViewProtocol.swift */; };
A4681FCA220113940018AB51 /* AudioClockDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F852200DA8B0018AB51 /* AudioClockDirector.swift */; };
A4681FCB220113980018AB51 /* AudioEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F822200D9150018AB51 /* AudioEngine.swift */; };
A4681FCC2201139B0018AB51 /* AudioDiskEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F942200E2220018AB51 /* AudioDiskEngine.swift */; };
A4681FCD2201139E0018AB51 /* AudioStreamEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681FBC220100AB0018AB51 /* AudioStreamEngine.swift */; };
@@ -44,8 +45,11 @@
A4681FDF220113E20018AB51 /* DirectorThreadSafeClosures.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F872200DAD50018AB51 /* DirectorThreadSafeClosures.swift */; };
A4681FE0220113E40018AB51 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F802200D0500018AB51 /* Log.swift */; };
A4681FE1220113E70018AB51 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F8B2200DDD50018AB51 /* Constants.swift */; };
A470FE0825F9ADF800F135FF /* AudioClockDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A470FE0625F9ADF800F135FF /* AudioClockDirector.swift */; };
A470FE0925F9ADF800F135FF /* DownloadProgressDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A470FE0725F9ADF800F135FF /* DownloadProgressDirector.swift */; };
A470FE1C25F9AEB900F135FF /* AudioQueueDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A470FE1B25F9AEB900F135FF /* AudioQueueDirector.swift */; };
A470FE2125F9AF1400F135FF /* AudioQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = A470FE2025F9AF1400F135FF /* AudioQueue.swift */; };
A4B4CC122223ED2A0045554B /* SAPlayerDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B4CC112223ED2A0045554B /* SAPlayerDownloader.swift */; };
A4FBA6B2221B538E00D5A353 /* DownloadProgressDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A49B78C3221A78DE00BBA862 /* DownloadProgressDirector.swift */; };
A4FBA6B5221B74C900D5A353 /* SALockScreenInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */; };
A4FBA6B7221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */; };
A4FBA6B9221BAF8700D5A353 /* SAAudioAvailabilityRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B8221BAF8700D5A353 /* SAAudioAvailabilityRange.swift */; };
@@ -98,11 +102,12 @@
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>"; };
A411CE3D25F55C0E0039E1CD /* AudioThrottlerNew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioThrottlerNew.swift; sourceTree = "<group>"; };
A411CE4525F9609D0039E1CD /* SAPlayerFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerFeatures.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>"; };
A4681F822200D9150018AB51 /* AudioEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioEngine.swift; sourceTree = "<group>"; };
A4681F852200DA8B0018AB51 /* AudioClockDirector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioClockDirector.swift; sourceTree = "<group>"; };
A4681F872200DAD50018AB51 /* DirectorThreadSafeClosures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectorThreadSafeClosures.swift; sourceTree = "<group>"; };
A4681F892200DB3C0018AB51 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = "<group>"; };
A4681F8B2200DDD50018AB51 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
@@ -128,7 +133,10 @@
A4681FBA2201002F0018AB51 /* AudioConverterListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverterListener.swift; sourceTree = "<group>"; };
A4681FBC220100AB0018AB51 /* AudioStreamEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioStreamEngine.swift; sourceTree = "<group>"; };
A4681FBE22010ECF0018AB51 /* LockScreenViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenViewProtocol.swift; sourceTree = "<group>"; };
A49B78C3221A78DE00BBA862 /* DownloadProgressDirector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadProgressDirector.swift; sourceTree = "<group>"; };
A470FE0625F9ADF800F135FF /* AudioClockDirector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioClockDirector.swift; sourceTree = "<group>"; };
A470FE0725F9ADF800F135FF /* DownloadProgressDirector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadProgressDirector.swift; sourceTree = "<group>"; };
A470FE1B25F9AEB900F135FF /* AudioQueueDirector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioQueueDirector.swift; sourceTree = "<group>"; };
A470FE2025F9AF1400F135FF /* AudioQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioQueue.swift; sourceTree = "<group>"; };
A4B4CC112223ED2A0045554B /* SAPlayerDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerDownloader.swift; sourceTree = "<group>"; };
A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SALockScreenInfo.swift; sourceTree = "<group>"; };
A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerUpdateSubscription.swift; sourceTree = "<group>"; };
@@ -291,6 +299,7 @@
A4681FB52200FDF30018AB51 /* Converter */,
A4681FAA2200F8280018AB51 /* Parser */,
A4681FA82200F5A20018AB51 /* AudioThrottler.swift */,
A411CE3D25F55C0E0039E1CD /* AudioThrottlerNew.swift */,
);
path = Engine;
sourceTree = "<group>";
@@ -298,6 +307,7 @@
A4681F9B2200E4850018AB51 /* Model */ = {
isa = PBXGroup;
children = (
A470FE2025F9AF1400F135FF /* AudioQueue.swift */,
A4681F992200E3D90018AB51 /* AudioDataManager.swift */,
A4681FA62200F0130018AB51 /* StreamProgressPTO.swift */,
A4681FA02200E5F50018AB51 /* Streaming */,
@@ -351,20 +361,30 @@
children = (
A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */,
A4681F8D2200E00E0018AB51 /* SAPlayer.swift */,
A411CE4525F9609D0039E1CD /* SAPlayerFeatures.swift */,
A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */,
A4B4CC112223ED2A0045554B /* SAPlayerDownloader.swift */,
A4681F912200E1950018AB51 /* SAPlayerDelegate.swift */,
A4681F8F2200E1450018AB51 /* SAPlayerPresenter.swift */,
A4681FBE22010ECF0018AB51 /* LockScreenViewProtocol.swift */,
A4681F852200DA8B0018AB51 /* AudioClockDirector.swift */,
A49B78C3221A78DE00BBA862 /* DownloadProgressDirector.swift */,
A4681F932200E2020018AB51 /* Engine */,
A470FE0D25F9AE1800F135FF /* Directors */,
A4681F9B2200E4850018AB51 /* Model */,
A4681F842200D91D0018AB51 /* Util */,
);
path = Source;
sourceTree = "<group>";
};
A470FE0D25F9AE1800F135FF /* Directors */ = {
isa = PBXGroup;
children = (
A470FE0725F9ADF800F135FF /* DownloadProgressDirector.swift */,
A470FE0625F9ADF800F135FF /* AudioClockDirector.swift */,
A470FE1B25F9AEB900F135FF /* AudioQueueDirector.swift */,
);
path = Directors;
sourceTree = "<group>";
};
BC3CA7F9E30CC8F7E2DD044DD34432FC /* Frameworks */ = {
isa = PBXGroup;
children = (
@@ -520,7 +540,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A470FE0925F9ADF800F135FF /* DownloadProgressDirector.swift in Sources */,
A41AA0D2238BB9B600A467E1 /* SAPlayingStatus.swift in Sources */,
A470FE1C25F9AEB900F135FF /* AudioQueueDirector.swift in Sources */,
A4681FDC220113D70018AB51 /* AudioDownloadWorker.swift in Sources */,
A4681FD8220113C60018AB51 /* AudioDataManager.swift in Sources */,
A4681FD1220113AF0018AB51 /* AudioParsable.swift in Sources */,
@@ -543,17 +565,19 @@
E08AD6157EF688FE832F866CBCDA3532 /* SwiftAudioPlayer-dummy.m in Sources */,
A4681FDD220113DC0018AB51 /* URL.swift in Sources */,
A4681FC82201138E0018AB51 /* SAPlayerPresenter.swift in Sources */,
A470FE2125F9AF1400F135FF /* AudioQueue.swift in Sources */,
A4681FD3220113B60018AB51 /* AudioParserPropertyListener.swift in Sources */,
A4681FCA220113940018AB51 /* AudioClockDirector.swift in Sources */,
A4B4CC122223ED2A0045554B /* SAPlayerDownloader.swift in Sources */,
A4681FD0220113A70018AB51 /* AudioConverterErrors.swift in Sources */,
A4FBA6B2221B538E00D5A353 /* DownloadProgressDirector.swift in Sources */,
A4681FD7220113C30018AB51 /* StreamProgressPTO.swift in Sources */,
A4681FE0220113E40018AB51 /* Log.swift in Sources */,
A4681FCE220113A20018AB51 /* AudioConverter.swift in Sources */,
A470FE0825F9ADF800F135FF /* AudioClockDirector.swift in Sources */,
A4FBA6B9221BAF8700D5A353 /* SAAudioAvailabilityRange.swift in Sources */,
A4681FCD2201139E0018AB51 /* AudioStreamEngine.swift in Sources */,
A411CE4625F9609D0039E1CD /* SAPlayerFeatures.swift in Sources */,
A4681FD9220113CD0018AB51 /* AudioStreamWorker.swift in Sources */,
A411CE3E25F55C0E0039E1CD /* AudioThrottlerNew.swift in Sources */,
A4681FDF220113E20018AB51 /* DirectorThreadSafeClosures.swift in Sources */,
A4681FCB220113980018AB51 /* AudioEngine.swift in Sources */,
);
@@ -14,6 +14,7 @@
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; };
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; };
607FACEC1AFB9204008FA782 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* Tests.swift */; };
A470FEE2260303DA00F135FF /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = A470FEE1260303DA00F135FF /* Model.swift */; };
E5808EC0557FB2395AA56468 /* Pods_SwiftAudioPlayer_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E5C0E3F3235B6FFE85EF425 /* Pods_SwiftAudioPlayer_Example.framework */; };
/* End PBXBuildFile section */
@@ -43,6 +44,7 @@
607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
607FACEB1AFB9204008FA782 /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = "<group>"; };
65A66AB4C3016E8BB53FF3E0 /* Pods-SwiftAudioPlayer_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftAudioPlayer_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftAudioPlayer_Example/Pods-SwiftAudioPlayer_Example.debug.xcconfig"; sourceTree = "<group>"; };
A470FEE1260303DA00F135FF /* Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Model.swift; sourceTree = "<group>"; };
AF6A2C6BF79C291056D27D5D /* SwiftAudioPlayer.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = SwiftAudioPlayer.podspec; path = ../SwiftAudioPlayer.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
BBD877782CC67FBCC7BF7532 /* Pods-SwiftAudioPlayer_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftAudioPlayer_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftAudioPlayer_Tests/Pods-SwiftAudioPlayer_Tests.debug.xcconfig"; sourceTree = "<group>"; };
DA80DEA33D13EC91EB531881 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
@@ -115,6 +117,7 @@
children = (
607FACD51AFB9204008FA782 /* AppDelegate.swift */,
607FACD71AFB9204008FA782 /* ViewController.swift */,
A470FEE1260303DA00F135FF /* Model.swift */,
607FACD91AFB9204008FA782 /* Main.storyboard */,
607FACDC1AFB9204008FA782 /* Images.xcassets */,
607FACDE1AFB9204008FA782 /* LaunchScreen.xib */,
@@ -324,6 +327,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A470FEE2260303DA00F135FF /* Model.swift in Sources */,
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */,
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */,
);
@@ -1,11 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -22,13 +20,13 @@
<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="320" width="343" height="2"/>
<rect key="frame" x="16" y="303" width="343" height="4"/>
<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="305" width="347" height="31"/>
<rect key="frame" x="14" y="289" 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>
@@ -37,41 +35,41 @@
<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="250" width="30" height="30"/>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="jUc-tP-CC5">
<rect key="frame" x="172.5" y="233" 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="250" width="30" height="30"/>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tFH-sY-Xu9">
<rect key="frame" x="62.5" y="233" 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="250" width="30" height="30"/>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="0QE-3F-a4G">
<rect key="frame" x="282.5" y="233" 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="464" width="347" height="31"/>
<rect key="frame" x="14" y="448" 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"/>
<rect key="frame" x="14" y="381" 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"/>
<rect key="frame" x="16" y="60" width="343" height="32"/>
<segments>
<segment title="Soundbite"/>
<segment title="Acquired"/>
@@ -81,50 +79,85 @@
<action selector="audioSelected:" destination="vXZ-lx-hvc" eventType="valueChanged" id="oYE-yq-348"/>
</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="140" width="69" height="30"/>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="KDu-ea-kF8">
<rect key="frame" x="43" y="123" 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="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.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"/>
<rect key="frame" x="153" y="419" 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="297" width="27" height="15"/>
<rect key="frame" x="16" y="280" 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="297" width="40" height="15"/>
<rect key="frame" x="319" y="280" 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="207" width="343" height="16"/>
<rect key="frame" x="16" y="190" 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"/>
<rect key="frame" x="136.5" y="352" width="102" 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" fixedFrame="YES" text="Skip Silences" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="M2y-FP-H1D">
<rect key="frame" x="89" y="504" width="101" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="2cn-E5-TeQ">
<rect key="frame" x="226" y="499" width="49" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<connections>
<action selector="skipSilencesSwitched:" destination="vXZ-lx-hvc" eventType="valueChanged" id="p7X-Y8-7hO"/>
</connections>
</switch>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="IGe-aU-Y6D">
<rect key="frame" x="226" y="540" width="49" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<connections>
<action selector="sleepSwitched:" destination="vXZ-lx-hvc" eventType="valueChanged" id="noa-m8-VHy"/>
</connections>
</switch>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Sleep After 5 s" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vf6-kr-yWa">
<rect key="frame" x="83" y="545" width="112" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="pVf-cJ-9ca">
<rect key="frame" x="164.5" y="123" width="46" height="30"/>
<state key="normal" title="Queue"/>
<connections>
<action selector="queueTouched:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="qRj-oT-AV1"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6d9-Bc-hIz">
<rect key="frame" x="282" y="123" 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>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
@@ -137,13 +170,14 @@
<constraint firstItem="vfk-OJ-S3T" firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" id="8PP-Pp-1Hc"/>
<constraint firstItem="joK-xi-MCo" firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" id="AH1-Uu-eLB"/>
<constraint firstItem="joK-xi-MCo" firstAttribute="top" secondItem="jyV-Pf-zRb" secondAttribute="bottom" constant="60" id="Ba7-nd-oCD"/>
<constraint firstItem="pVf-cJ-9ca" firstAttribute="centerY" secondItem="KDu-ea-kF8" secondAttribute="centerY" id="Cma-VU-v2t"/>
<constraint firstItem="Urj-Dv-41y" firstAttribute="centerY" secondItem="j3w-gr-HzF" secondAttribute="centerY" id="Fvd-7V-Rr8"/>
<constraint firstItem="1IX-z5-wWx" firstAttribute="leading" secondItem="joK-xi-MCo" secondAttribute="leading" id="GeX-7f-jzu"/>
<constraint firstItem="0QE-3F-a4G" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="jUc-tP-CC5" secondAttribute="trailing" constant="8" symbolic="YES" id="JP5-yW-eVB"/>
<constraint firstItem="yUQ-mI-ozK" firstAttribute="top" secondItem="w2a-RA-zmI" secondAttribute="bottom" constant="100" id="K1K-8N-SpD"/>
<constraint firstItem="vfk-OJ-S3T" firstAttribute="leading" secondItem="lTK-Hd-Tl2" secondAttribute="leading" id="NOY-IO-NIJ"/>
<constraint firstItem="tFH-sY-Xu9" firstAttribute="centerY" secondItem="jUc-tP-CC5" secondAttribute="centerY" id="Rre-EY-kVY"/>
<constraint firstItem="KDu-ea-kF8" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="78" id="SRU-sX-z5b"/>
<constraint firstItem="KDu-ea-kF8" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="43" 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"/>
@@ -159,12 +193,13 @@
<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="pVf-cJ-9ca" firstAttribute="centerX" secondItem="joK-xi-MCo" secondAttribute="centerX" id="lOM-Fa-KdR"/>
<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 firstAttribute="trailing" secondItem="6d9-Bc-hIz" secondAttribute="trailing" constant="44" id="vtN-y4-iqp"/>
<constraint firstItem="0QE-3F-a4G" firstAttribute="centerY" secondItem="jUc-tP-CC5" secondAttribute="centerY" id="xDi-tj-bBF"/>
<constraint firstItem="lTK-Hd-Tl2" firstAttribute="top" secondItem="jUc-tP-CC5" secondAttribute="bottom" constant="40" id="ytQ-s4-kJm"/>
<constraint firstItem="w2a-RA-zmI" firstAttribute="centerY" secondItem="lTK-Hd-Tl2" secondAttribute="centerY" constant="-1" id="zHt-h3-4ig"/>
@@ -185,6 +220,8 @@
<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"/>
<outlet property="skipSilencesSwitch" destination="2cn-E5-TeQ" id="TRI-IT-YJT"/>
<outlet property="sleepSwitch" destination="IGe-aU-Y6D" id="BZn-9C-hOk"/>
<outlet property="streamButton" destination="6d9-Bc-hIz" id="DZe-ga-3RV"/>
</connections>
</viewController>
+82
View File
@@ -0,0 +1,82 @@
//
// Model.swift
// SwiftAudioPlayer_Example
//
// Created by Tanha Kabir on 3/17/21.
// Copyright © 2021 CocoaPods. All rights reserved.
//
import Foundation
struct AudioInfo: Hashable {
var index: Int = 0
var urls: [URL] = [URL(string: "https://www.fesliyanstudios.com/musicfiles/2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com/15SecVersion2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com.mp3")!,
URL(string: "https://chtbl.com/track/18338/traffic.libsyn.com/secure/acquired/acquired_-_armrev_2.mp3?dest-id=376122")!,
URL(string: "https://backtracks.fm/ycombinator/pr/0f685f72-29b1-11e9-9bcf-0ece7a7d2472/111---jake-klamka-and-kevin-hale---y-combinator.mp3?s=1&amp;sd=1&amp;u=1549423185")!]
var url: URL {
switch index {
case 0:
return URL(string: "https://www.fesliyanstudios.com/musicfiles/2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com/15SecVersion2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com.mp3")!
case 1:
return URL(string: "https://chtbl.com/track/18338/traffic.libsyn.com/secure/acquired/acquired_-_armrev_2.mp3?dest-id=376122")!
case 2:
return URL(string: "https://backtracks.fm/ycombinator/pr/0f685f72-29b1-11e9-9bcf-0ece7a7d2472/111---jake-klamka-and-kevin-hale---y-combinator.mp3?s=1&amp;sd=1&amp;u=1549423185")!
default:
return URL(string: "https://www.fesliyanstudios.com/musicfiles/2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com/15SecVersion2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com.mp3")!
}
}
var title: String {
switch index {
case 0:
return "Soundbite"
case 1:
return "Acquired"
case 2:
return "Y Combinator"
default:
return "Soundbite"
}
}
let artist: String = "SwiftAudioPlayer Sample App"
let releaseDate: Int = 1550790640
var savedUrl: URL? {
get {
return savedUrls[index]
}
}
var savedUrls: [URL?] = [nil, nil, nil]
mutating func addSavedUrl(_ url: URL) {
savedUrls[index] = url
}
mutating func deleteSavedUrl() {
savedUrls[index] = nil
}
mutating func addSavedUrl(_ url: URL, atIndex i: Int) {
savedUrls[i] = url
}
mutating func deleteSavedUrl(atIndex i: Int) {
savedUrls[i] = nil
}
func getUrl(atIndex i: Int) -> URL {
return urls[i]
}
mutating func setIndex(_ i: Int) {
index = i
}
func getIndex(forURL url: URL) -> Int? {
return urls.firstIndex(of: url) ?? savedUrls.firstIndex(of: url)
}
}
+195 -128
View File
@@ -11,54 +11,8 @@ import SwiftAudioPlayer
import AVFoundation
class ViewController: UIViewController {
struct AudioInfo: Hashable {
let index: Int
var url: URL {
switch index {
case 0:
return URL(string: "https://cdn.fastlearner.media/bensound-rumble.mp3")!
case 1:
return URL(string: "https://chtbl.com/track/18338/traffic.libsyn.com/secure/acquired/acquired_-_armrev_2.mp3?dest-id=376122")!
case 2:
return URL(string: "https://backtracks.fm/ycombinator/pr/0f685f72-29b1-11e9-9bcf-0ece7a7d2472/111---jake-klamka-and-kevin-hale---y-combinator.mp3?s=1&amp;sd=1&amp;u=1549423185")!
default:
return URL(string: "https://cdn.fastlearner.media/bensound-rumble.mp3")!
}
}
var title: String {
switch index {
case 0:
return "Soundbite"
case 1:
return "Acquired"
case 2:
return "Y Combinator"
default:
return "Soundbite"
}
}
let artist: String = "SwiftAudioPlayer Sample App"
let releaseDate: Int = 1550790640
}
var selectedAudio: AudioInfo = AudioInfo(index: 0)
var savedUrls: [AudioInfo: URL] = [:]
var selectedAudio: AudioInfo = AudioInfo(index: 0) {
didSet {
if SAPlayer.Downloader.isDownloaded(withRemoteUrl: selectedAudio.url) {
downloadButton.setTitle("Delete downloaded", for: .normal)
streamButton.isEnabled = false
} else {
downloadButton.setTitle("Download", for: .normal)
streamButton.isEnabled = true
}
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!
@@ -84,7 +38,18 @@ class ViewController: UIViewController {
var isStreaming: Bool = false
var beingSeeked: Bool = false
var downloadId: UInt?
var durationId: UInt?
var bufferId: UInt?
var playingStatusId: UInt?
var queueId: UInt?
var elapsedId: UInt?
var duration: Double = 0.0
var playbackStatus: SAPlayingStatus = .paused
var lastPlayedAudioIndex: Int?
var isPlayable: Bool = false {
didSet {
@@ -105,84 +70,15 @@ class ViewController: UIViewController {
SAPlayer.Downloader.allowUsingCellularData = true
SAPlayer.shared.DEBUG_MODE = true
// SAPlayer.shared.DEBUG_MODE = true
isPlayable = false
selectedAudio = AudioInfo(index: 0)
checkIfAudioDownloaded()
selectAudio(atIndex: 0)
addRandomModifiers()
// 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 }
self.durationLabel.text = SAPlayer.prettifyTimestamp(duration)
self.duration = duration
}
_ = SAPlayer.Updates.ElapsedTime.subscribe { [weak self] (url, position) in
guard let self = self 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
guard let self = self else { return }
guard url == self.selectedAudio.url else { return }
if self.isDownloading {
DispatchQueue.main.async {
UIView.performWithoutAnimation {
self.downloadButton.setTitle("Cancel \(String(format: "%.2f", (progress * 100)))%", for: .normal)
}
}
}
}
_ = SAPlayer.Updates.StreamingBuffer.subscribe{ [weak self] (url, buffer) in
guard let self = self else { return }
guard url == self.selectedAudio.url || url == self.savedUrls[self.selectedAudio] else { return }
if self.duration == 0.0 { return }
self.bufferProgress.progress = Float(buffer.bufferingProgress)
if buffer.bufferingProgress >= 0.99 {
self.streamButton.isEnabled = false
} else {
self.streamButton.isEnabled = true
}
self.isPlayable = buffer.isReadyForPlaying
}
_ = SAPlayer.Updates.PlayingStatus.subscribe { [weak self] (url, playing) in
guard let self = self else { return }
guard url == self.selectedAudio.url || url == self.savedUrls[self.selectedAudio] else { return }
switch playing {
case .playing:
self.isPlayable = true
self.playPauseButton.setTitle("Pause", for: .normal)
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
case .ended:
self.isPlayable = false
self.playPauseButton.setTitle("Done", for: .normal)
return
}
}
subscribeToChanges()
}
func addRandomModifiers() {
@@ -209,12 +105,146 @@ class ViewController: UIViewController {
@IBAction func audioSelected(_ sender: Any) {
let selected = audioSelector.selectedSegmentIndex
selectedAudio = AudioInfo(index: selected)
selectAudio(atIndex: selected)
}
func selectAudio(atIndex i: Int) {
selectedAudio.setIndex(i)
SAPlayer.shared.mediaInfo = SALockScreenInfo(title: selectedAudio.title, artist: selectedAudio.artist, artwork: UIImage(), releaseDate: selectedAudio.releaseDate)
if selectedAudio.savedUrl != nil {
downloadButton.setTitle("Delete downloaded", for: .normal)
streamButton.isEnabled = false
} else {
downloadButton.setTitle("Download", for: .normal)
streamButton.isEnabled = true
}
if let savedUrl = selectedAudio.savedUrl {
self.currentUrlLocationLabel.text = "saved url: \(savedUrl.absoluteString)"
} else {
self.currentUrlLocationLabel.text = "remote url: \(selectedAudio.url.absoluteString)"
}
// if let savedUrl = savedUrls[selectedAudio] {}
scrubberSlider.value = 0
bufferProgress.progress = 0
// unsubscribeFromChanges()
// subscribeToChanges()
SAPlayer.shared.mediaInfo = SALockScreenInfo(title: selectedAudio.title, artist: selectedAudio.artist, artwork: UIImage(), releaseDate: selectedAudio.releaseDate)
}
func checkIfAudioDownloaded() {
for i in 0...2 {
if let savedUrl = SAPlayer.Downloader.getSavedUrl(forRemoteUrl: selectedAudio.getUrl(atIndex: i)) {
selectedAudio.addSavedUrl(savedUrl, atIndex: i)
}
}
}
func subscribeToChanges() {
durationId = SAPlayer.Updates.Duration.subscribe { [weak self] (url, duration) in
guard let self = self else { return }
guard url == self.selectedAudio.savedUrl || url == self.selectedAudio.url else { return }
self.durationLabel.text = SAPlayer.prettifyTimestamp(duration)
self.duration = duration
}
elapsedId = SAPlayer.Updates.ElapsedTime.subscribe { [weak self] (url, position) in
guard let self = self else { return }
guard url == self.selectedAudio.savedUrl || url == self.selectedAudio.url else { return }
self.currentTimestampLabel.text = SAPlayer.prettifyTimestamp(position)
guard self.duration != 0 else { return }
self.scrubberSlider.value = Float(position/self.duration)
}
downloadId = SAPlayer.Updates.AudioDownloading.subscribe { [weak self] (url, progress) in
guard let self = self else { return }
guard url == self.selectedAudio.url else { return }
if self.isDownloading {
DispatchQueue.main.async {
UIView.performWithoutAnimation {
self.downloadButton.setTitle("Cancel \(String(format: "%.2f", (progress * 100)))%", for: .normal)
}
}
}
}
bufferId = SAPlayer.Updates.StreamingBuffer.subscribe{ [weak self] (url, buffer) in
guard let self = self else { return }
guard url == self.selectedAudio.savedUrl || url == self.selectedAudio.url else { return }
if self.duration == 0.0 { return }
self.bufferProgress.progress = Float(buffer.bufferingProgress)
if buffer.bufferingProgress >= 0.99 {
self.streamButton.isEnabled = false
} else {
self.streamButton.isEnabled = true
}
self.isPlayable = buffer.isReadyForPlaying
}
playingStatusId = SAPlayer.Updates.PlayingStatus.subscribe { [weak self] (url, playing) in
guard let self = self else { return }
guard url == self.selectedAudio.savedUrl || url == self.selectedAudio.url else { return }
self.playbackStatus = playing
switch playing {
case .playing:
self.isPlayable = true
self.playPauseButton.setTitle("Pause", for: .normal)
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
case .ended:
self.isPlayable = false
self.playPauseButton.setTitle("Done", for: .normal)
return
}
}
queueId = SAPlayer.Updates.AudioQueue.subscribe { [weak self] key, forthcomingPlaybackUrl in
guard let self = self else { return }
/// we update the selected audio. this is a little contrived, but allows us to update outlets
if let indexFound = self.selectedAudio.getIndex(forURL: forthcomingPlaybackUrl) {
self.selectAudio(atIndex: indexFound)
}
print("💥 Received queue update 💥")
}
}
func unsubscribeFromChanges() {
guard let durationId = self.durationId,
let elapsedId = self.elapsedId,
let downloadId = self.downloadId,
let queueId = self.queueId,
let bufferId = self.bufferId,
let playingStatusId = self.playingStatusId else { return }
SAPlayer.Updates.Duration.unsubscribe(durationId)
SAPlayer.Updates.ElapsedTime.unsubscribe(elapsedId)
SAPlayer.Updates.AudioDownloading.unsubscribe(downloadId)
SAPlayer.Updates.AudioQueue.unsubscribe(queueId)
SAPlayer.Updates.StreamingBuffer.unsubscribe(bufferId)
SAPlayer.Updates.PlayingStatus.unsubscribe(playingStatusId)
}
@IBAction func scrubberStartedSeeking(_ sender: UISlider) {
beingSeeked = true
}
@@ -229,10 +259,7 @@ class ViewController: UIViewController {
@IBAction func rateChanged(_ sender: Any) {
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)
}
SAPlayer.shared.rate = speed
}
@IBAction func reverbChanged(_ sender: Any) {
let reverb = reverbSlider.value
@@ -241,11 +268,21 @@ class ViewController: UIViewController {
node.wetDryMix = reverb
}
}
@IBAction func queueTouched(_ sender: Any) {
if let savedUrl = selectedAudio.savedUrl {
SAPlayer.shared.queueSavedAudio(withSavedUrl: savedUrl)
} else {
SAPlayer.shared.queueRemoteAudio(withRemoteUrl: selectedAudio.url)
}
print("queue: \(SAPlayer.shared.audioQueued)")
}
@IBAction func downloadTouched(_ sender: Any) {
if !isDownloading {
if let savedUrl = SAPlayer.Downloader.getSavedUrl(forRemoteUrl: selectedAudio.url) {
SAPlayer.Downloader.deleteDownloaded(withSavedUrl: savedUrl)
selectedAudio.deleteSavedUrl()
downloadButton.setTitle("Download", for: .normal)
streamButton.isEnabled = true
isDownloading = false
@@ -256,9 +293,10 @@ class ViewController: UIViewController {
guard let self = self else { return }
DispatchQueue.main.async {
self.currentUrlLocationLabel.text = "saved to: \(url.lastPathComponent)"
self.savedUrls[self.selectedAudio] = url
self.selectedAudio.addSavedUrl(url)
SAPlayer.shared.startSavedAudio(withSavedUrl: url)
self.lastPlayedAudioIndex = self.selectedAudio.index
}
})
streamButton.isEnabled = false
@@ -274,6 +312,7 @@ class ViewController: UIViewController {
@IBAction func streamTouched(_ sender: Any) {
if !isStreaming {
SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url)
lastPlayedAudioIndex = selectedAudio.index
streamButton.setTitle("Cancel streaming", for: .normal)
downloadButton.isEnabled = false
isStreaming = true
@@ -286,6 +325,16 @@ class ViewController: UIViewController {
}
@IBAction func playPauseTouched(_ sender: Any) {
// if lastPlayedAudioIndex != selectedAudio.index {
// if let savedUrl = selectedAudio.savedUrl {
// SAPlayer.shared.startSavedAudio(withSavedUrl: savedUrl)
// } else {
// SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url)
// }
//
// return
// }
SAPlayer.shared.togglePlayAndPause()
}
@@ -310,5 +359,23 @@ class ViewController: UIViewController {
}
@IBOutlet weak var skipSilencesSwitch: UISwitch!
@IBAction func skipSilencesSwitched(_ sender: Any) {
if skipSilencesSwitch.isOn {
_ = SAPlayer.Features.SkipSilences.enable()
} else {
_ = SAPlayer.Features.SkipSilences.disable()
}
}
@IBOutlet weak var sleepSwitch: UISwitch!
@IBAction func sleepSwitched(_ sender: Any) {
if sleepSwitch.isOn {
_ = SAPlayer.Features.SleepTimer.enable(afterDelay: 5.0)
} else {
_ = SAPlayer.Features.SleepTimer.disable()
}
}
}
+26 -1
View File
@@ -10,6 +10,21 @@ This player was built for [podcasting](https://chameleonpodcast.com/). We origin
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).
### Basic Features
1. Realtime audio manipulation that includes going up to 10x speed, using [equalizers and other manipulations](https://developer.apple.com/documentation/avfaudio/avaudiouniteq)
1. Stream online audio using AVAudioEngine
1. Play locally saved audio with the same API
1. Download audio
1. Queue up downloaded and streamed audio for autoplay
1. Uses only 1-2% CPU for optimal performance for the rest of your app
1. You're able to install taps and any other AVAudioEngine features to do cool things like skipping silences
### Special Features
These are community supported audio manipulation features using this audio engine. You can implement your own version of these features and you can look at [SAPlayerFeatures](https://github.com/tanhakabir/SwiftAudioPlayer/blob/master/Source/SAPlayerFeatures.swift) to learn how they were implemented using the library.
1. Skip silences in audio
1. Sleep timer to stop playing audio after a delay
### Requirements
iOS 10.0 and higher.
@@ -19,7 +34,7 @@ iOS 10.0 and higher.
### Running the Example Project
1. Clone repo
2. CD to directory
2. CD to the `Example` folder where the Example app lives
3. Run `pod install` in terminal
4. Build and run
@@ -147,6 +162,16 @@ skipForward()
skipBackwards()
```
### Queuing Audio for Autoplay
You can queue either remote or locally saved audio to be played automatically next.
To queue:
```swift
SAPlayer.shared.queueSavedAudio(withSavedUrl: C://random_folder/audio.mp3) // or
SAPlayer.shared.queueRemoteAudio(withRemoteUrl: https://randomwebsite.com/audio.mp3)
```
#### 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.
+32
View File
@@ -0,0 +1,32 @@
//
// AudioQueueDirector.swift
// SwiftAudioPlayer
//
// Created by Joe Williams on 3/10/21.
//
import Foundation
class AudioQueueDirector {
static let shared = AudioQueueDirector()
var closures: DirectorThreadSafeClosures<URL> = DirectorThreadSafeClosures()
private init() {}
func create() {}
func clear() {
closures.clear()
}
func attach(closure: @escaping (Key, URL) throws -> Void) -> UInt {
return closures.attach(closure: closure)
}
func detach(withID id: UInt) {
closures.detach(id: id)
}
func changeInQueue(_ key: Key, url: URL) {
closures.broadcast(key: key, payload: url)
}
}
+6 -6
View File
@@ -69,12 +69,12 @@ class AudioDiskEngine: AudioEngine {
Log.monitor("Could not load downloaded file with url: \(url)")
}
Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { [weak self] (timer: Timer) in
guard let _ = self else { return }
self?.timer = timer
self?.updateIsPlaying()
self?.updateNeedle()
doRepeatedly(timeInterval: 0.2) { [weak self] in
guard let self = self else { return }
guard self.playingStatus != .ended else { return }
self.updateIsPlaying()
self.updateNeedle()
}
scheduleAudioFile()
+46 -15
View File
@@ -27,7 +27,8 @@ import Foundation
import AVFoundation
protocol AudioEngineProtocol {
var engine: AVAudioEngine { get set }
var key: Key { get }
var engine: AVAudioEngine! { get }
func play()
func pause()
func seek(toNeedle needle: Needle)
@@ -35,18 +36,15 @@ protocol AudioEngineProtocol {
}
protocol AudioEngineDelegate: AnyObject {
func didEndPlaying() //for auto play
func didError()
}
class AudioEngine: AudioEngineProtocol {
weak var delegate:AudioEngineDelegate?
let key:Key
var key:Key
var engine = AVAudioEngine()
let playerNode = AVAudioPlayerNode()
var timer: Timer?
var engine: AVAudioEngine!
var playerNode: AVAudioPlayerNode!
static let defaultEngineAudioFormat: AVAudioFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 2, interleaved: false)!
@@ -77,11 +75,7 @@ class AudioEngine: AudioEngineProtocol {
guard playingStatus != oldValue, let status = playingStatus else {
return
}
if status == .ended {
delegate?.didEndPlaying()
}
AudioClockDirector.shared.audioPlayingStatusWasChanged(key, status: status)
}
}
@@ -113,6 +107,13 @@ class AudioEngine: AudioEngineProtocol {
self.key = url.key
self.delegate = delegate
engine = AVAudioEngine()
playerNode = AVAudioPlayerNode()
initHelper(engineAudioFormat)
}
func initHelper(_ engineAudioFormat: AVAudioFormat) {
engine.attach(playerNode)
for node in SAPlayer.shared.audioModifiers {
@@ -146,15 +147,43 @@ class AudioEngine: AudioEngineProtocol {
}
deinit {
timer?.invalidate()
if state == .resumed {
engine.stop()
}
engine.disconnectNodeInput(self.playerNode)
engine.detach(self.playerNode)
engine = nil
playerNode = nil
Log.info("deinit AVAudioEngine for \(key)")
}
func doRepeatedly(timeInterval: Double, _ closure: @escaping () -> ()) {
// A common error in AVAudioEngine is 'required condition is false: nil == owningEngine || GetEngine() == owningEngine'
// where there can only be one instance of engine running at a time and if there is already one when trying to start
// a new one then this error will be thrown.
// To handle this error we need to make sure we properly dispose of the engine when done using. In the case of timers, a
// repeating timer will maintain a strong reference to the body even if you state that you wanted a weak reference to self
// to mitigate this for repeating timers, you can either call timer.invalidate() properly or don't use repeat block timers.
// To be in better control of references and to mitigate any unforeseen issues, I decided to implement a recurisive version
// of the repeat block timer so I'm in full control of when to invalidate.
Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false) { [weak self] (timer: Timer) in
guard let self = self else { return }
guard self.playingStatus != .ended else {
self.delegate = nil
return
}
closure()
self.doRepeatedly(timeInterval: timeInterval, closure)
}
}
func updateIsPlaying() {
if !bufferedSeconds.isPlayable {
if bufferedSeconds.bufferingProgress > 0.999 {
if bufferedSeconds.reachedEndOfAudio(needle: needle) {
playingStatus = .ended
} else {
playingStatus = .buffering
@@ -164,11 +193,13 @@ class AudioEngine: AudioEngineProtocol {
let isPlaying = engine.isRunning && playerNode.isPlaying
playingStatus = isPlaying ? .playing : .paused
// playingStatus = .paused
}
func play() {
// https://stackoverflow.com/questions/36754934/update-mpremotecommandcenter-play-pause-button
if !engine.isRunning {
if !(engine.isRunning) {
do {
try engine.start()
+26 -40
View File
@@ -61,7 +61,7 @@ class AudioStreamEngine: AudioEngine {
private let MIN_BUFFERS_TO_BE_PLAYABLE = 1
private let PCM_BUFFER_SIZE: AVAudioFrameCount = 8192
private let queue = DispatchQueue(label: "SwiftAudioPlayer.engine", qos: .userInitiated)
private let queue = DispatchQueue(label: "SwiftAudioPlayer.StreamEngine", qos: .userInitiated)
//From init
private var converter: AudioConvertable!
@@ -137,7 +137,7 @@ class AudioStreamEngine: AudioEngine {
Log.info(url)
super.init(url: url, delegate: delegate, engineAudioFormat: AudioEngine.defaultEngineAudioFormat)
do {
converter = try AudioConverter(withRemoteUrl: url, toEngineAudioFormat: AudioEngine.defaultEngineAudioFormat)
converter = try AudioConverter(withRemoteUrl: url, toEngineAudioFormat: AudioEngine.defaultEngineAudioFormat, withPCMBufferSize: PCM_BUFFER_SIZE)
} catch {
delegate?.didError()
}
@@ -145,13 +145,15 @@ class AudioStreamEngine: AudioEngine {
let timeInterval = 1 / (converter.engineAudioFormat.sampleRate / Double(PCM_BUFFER_SIZE))
Timer.scheduledTimer(withTimeInterval: timeInterval / 32, repeats: true) { [weak self] (timer: Timer) in
self?.timer = timer
self?.pollForNextBuffer()
self?.updateNetworkBufferRange()
self?.updateNeedle()
self?.updateIsPlaying()
self?.updateDuration()
doRepeatedly(timeInterval: timeInterval) { [weak self] in
guard let self = self else { return }
guard self.playingStatus != .ended else { return }
self.pollForNextBufferRecursive()
self.updateNetworkBufferRange()
self.updateNeedle()
self.updateIsPlaying()
self.updateDuration()
}
}
@@ -160,19 +162,29 @@ class AudioStreamEngine: AudioEngine {
//Called when
//1. First time audio is finally parsed
//2. When we run to the end of the network buffer and we're waiting again
private func pollForNextBuffer() {
private func pollForNextBufferRecursive() {
guard shouldPollForNextBuffer else { return }
do {
let nextScheduledBuffer = try converter.pullBuffer(withSize: PCM_BUFFER_SIZE)
var nextScheduledBuffer: AVAudioPCMBuffer! = try converter.pullBuffer()
numberOfBuffersScheduledFromPoll += 1
numberOfBuffersScheduledInTotal += 1
Log.debug("processed buffer for engine of frame length \(nextScheduledBuffer.frameLength)")
queue.async { [weak self] in
self?.playerNode.scheduleBuffer(nextScheduledBuffer) {
self?.numberOfBuffersScheduledInTotal -= 1
self?.pollForNextBufferRecursionHelper()
if #available(iOS 11.0, *) {
// to make sure the pcm buffers are properly free'd from memory we need to nil them after the player has used them
self?.playerNode.scheduleBuffer(nextScheduledBuffer, completionCallbackType: .dataRendered, completionHandler: { (_) in
nextScheduledBuffer = nil
self?.numberOfBuffersScheduledInTotal -= 1
self?.pollForNextBufferRecursive()
})
} else {
self?.playerNode.scheduleBuffer(nextScheduledBuffer) {
nextScheduledBuffer = nil
self?.numberOfBuffersScheduledInTotal -= 1
self?.pollForNextBufferRecursive()
}
}
}
@@ -188,32 +200,6 @@ class AudioStreamEngine: AudioEngine {
}
}
private func pollForNextBufferRecursionHelper() {
do {
let nextScheduledBuffer = try converter.pullBuffer(withSize: PCM_BUFFER_SIZE)
Log.debug("processed buffer for engine of frame lengthL \(nextScheduledBuffer.frameLength)")
numberOfBuffersScheduledInTotal += 1
queue.async { [weak self] in
self?.playerNode.scheduleBuffer(nextScheduledBuffer) {
self?.numberOfBuffersScheduledInTotal -= 1
self?.pollForNextBufferRecursionHelper()
}
}
} catch ConverterError.reachedEndOfFile {
Log.info(ConverterError.reachedEndOfFile.localizedDescription)
} catch ConverterError.notEnoughData {
shouldPollForNextBuffer = true
Log.debug(ConverterError.notEnoughData.localizedDescription)
} catch ConverterError.superConcerningShouldNeverHappen {
Log.error(ConverterError.superConcerningShouldNeverHappen.localizedDescription)
} catch {
Log.debug(error.localizedDescription)
}
}
private func updateNetworkBufferRange() { //for ui
let range = converter.pollNetworkAudioAvailabilityRange()
isPlayable = (numberOfBuffersScheduledInTotal >= MIN_BUFFERS_TO_BE_PLAYABLE && range.1 > 0) && predictedStreamDuration > 0
+33 -11
View File
@@ -42,6 +42,15 @@ protocol AudioThrottleable {
}
class AudioThrottler: AudioThrottleable {
enum State: String {
case INIT
case WAITING_FOR_DATA
case RECEIVING_DATA
case PUSH_DATA
case END_OF_DATA
case CLEAN_UP
}
private class NetworkDataWrapper: NSObject {
let startOffset: UInt
var data: Data
@@ -93,6 +102,7 @@ class AudioThrottler: AudioThrottleable {
//Init
let url: AudioURL
weak var delegate: AudioThrottleDelegate?
private var state: State
private var networkData: [NetworkDataWrapper] = []
var shouldThrottle = false
@@ -110,12 +120,16 @@ class AudioThrottler: AudioThrottleable {
var largestPollingOffsetDifference: UInt64 = 1
required init(withRemoteUrl url: AudioURL, withDelegate delegate: AudioThrottleDelegate) {
self.state = .INIT
self.url = url
self.delegate = delegate
self.state = .WAITING_FOR_DATA
AudioDataManager.shared.startStream(withRemoteURL: url) { [weak self] (pto: StreamProgressPTO) in
guard let self = self else {return}
Log.debug("received stream data of size \(pto.getData().count) and progress: \(pto.getProgress())")
guard let self = self else { return }
if !self.shouldThrottle { self.state = .RECEIVING_DATA }
Log.debug("received stream data of size \(pto.getData().count) and progress: \(pto.getProgress())", state: self.state.rawValue)
self.delegate?.didUpdate(networkStreamProgress: pto.getProgress())
if let totalBytesExpected = pto.getTotalBytesExpected() {
@@ -129,7 +143,8 @@ class AudioThrottler: AudioThrottleable {
self.networkData.append(wrappedNetworkData)
if !self.shouldThrottle {
Log.debug("sending up packet from stream untrottled at start: \(wrappedNetworkData.startOffset)")
self.state = .PUSH_DATA
Log.debug("sending up packet from stream untrottled at start: \(wrappedNetworkData.startOffset)", state: self.state.rawValue)
//NOTE: the order here matters.
//We have to set to true before sending up to be processed because
//tellByteOffset() is ran in a separate thread than this one
@@ -143,6 +158,7 @@ class AudioThrottler: AudioThrottleable {
func tellAudioFormatFound() {
shouldThrottle = true //the above layer has enough info that we can throttle
self.state = .RECEIVING_DATA
}
func tellBytesPerAudioPacket(count: UInt64) {
@@ -152,14 +168,14 @@ class AudioThrottler: AudioThrottleable {
}
func tellByteOffset(offset: UInt64) {
Log.debug("offset \(offset)")
Log.debug("offset \(offset)", state: self.state.rawValue)
for wrappedNetworkData in networkData {
if wrappedNetworkData.containsOffset(UInt(offset)) {
Log.debug("offset: \(offset) within network packet of range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset) is next sent: \(wrappedNetworkData.isNextSent())")
Log.debug("offset: \(offset) within network packet of range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset) is next sent: \(wrappedNetworkData.isNextSent())", state: self.state.rawValue)
if wrappedNetworkData.alreadySent {
Log.debug("already sent offset: \(offset) within network packet of range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)")
Log.debug("already sent offset: \(offset) within network packet of range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)", state: self.state.rawValue)
var bytesSent: UInt = 0
var current = wrappedNetworkData
@@ -169,14 +185,16 @@ class AudioThrottler: AudioThrottleable {
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")
self.state = .PUSH_DATA
Log.info("Sending next network packet with range: \(next.startOffset) to \(next.endOffset), have sent \(bytesSent) bytes so far from \(largestPollingOffsetDifference) bytes", self.state.rawValue)
next.alreadySent = true
delegate?.shouldProcess(networkData: next.data)
}
bytesSent += next.byteCount
current = next
} else {
Log.debug("next package doesn't exist, bytes sent so far: \(bytesSent)")
Log.debug("next package doesn't exist, bytes sent so far: \(bytesSent)", state: self.state.rawValue)
self.state = .END_OF_DATA
return
}
}
@@ -184,16 +202,19 @@ class AudioThrottler: AudioThrottleable {
return
}
Log.info("Found network packet to send with range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)")
Log.info("Found network packet to send with range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)", self.state.rawValue)
wrappedNetworkData.alreadySent = true
self.state = .PUSH_DATA
delegate?.shouldProcess(networkData: wrappedNetworkData.data)
return
} else {
self.state = .END_OF_DATA
}
}
}
func tellSeek(offset: UInt64) {
Log.info("seek with offset: \(offset)")
Log.info("seek with offset: \(offset)", self.state.rawValue)
if networkData.count == 0 {
byteOffsetBecauseOfSeek = UInt(offset)
@@ -225,7 +246,7 @@ class AudioThrottler: AudioThrottleable {
d.alreadySent = false
wrappedData.alreadySent = true
Log.info("\(d) ::: \(wrappedData)")
Log.info("\(d) ::: \(wrappedData)", self.state.rawValue)
delegate?.shouldProcess(networkData: wrappedData.data)
return
@@ -242,5 +263,6 @@ class AudioThrottler: AudioThrottleable {
func invalidate() {
AudioDataManager.shared.deleteStream(withRemoteURL: url)
self.state = .CLEAN_UP
}
}
+49
View File
@@ -0,0 +1,49 @@
//
// AudioThrottlerNew.swift
// SwiftAudioPlayer
//
// Created by Tanha Kabir on 3/7/21.
//
import Foundation
class AudioThrottlerNew: AudioThrottleable {
enum State: String {
case INIT
case WAITING_FOR_DATA
case RECEIVING_DATA
case PUSH_DATA
case END_OF_DATA
case CLEAN_UP
}
private var delegate: AudioThrottleDelegate
required init(withRemoteUrl url: AudioURL, withDelegate delegate: AudioThrottleDelegate) {
self.delegate = delegate
}
func tellAudioFormatFound() {
<#code#>
}
func tellByteOffset(offset: UInt64) {
<#code#>
}
func tellSeek(offset: UInt64) {
<#code#>
}
func tellBytesPerAudioPacket(count: UInt64) {
<#code#>
}
func pollRangeOfBytesAvailable() -> (UInt64, UInt64) {
<#code#>
}
func invalidate() {
<#code#>
}
}
+14 -7
View File
@@ -36,8 +36,8 @@ import AudioToolbox
protocol AudioConvertable {
var engineAudioFormat: AVAudioFormat {get}
init(withRemoteUrl url: AudioURL, toEngineAudioFormat: AVAudioFormat) throws
func pullBuffer(withSize size: AVAudioFrameCount) throws -> AVAudioPCMBuffer
init(withRemoteUrl url: AudioURL, toEngineAudioFormat: AVAudioFormat, withPCMBufferSize size: AVAudioFrameCount) throws
func pullBuffer() throws -> AVAudioPCMBuffer
func pollPredictedDuration() -> Duration?
func pollNetworkAudioAvailabilityRange() -> (Needle, Duration)
func seek(_ needle: Needle)
@@ -70,13 +70,20 @@ class AudioConverter: AudioConvertable {
//From protocol
public var engineAudioFormat: AVAudioFormat
let pcmBufferSize: AVAudioFrameCount
//Field
var converter: AudioConverterRef? //set by AudioConverterNew
var currentAudioPacketIndex: AVAudioPacketCount = 0
required init(withRemoteUrl url: AudioURL, toEngineAudioFormat: AVAudioFormat) throws {
// use to store reference to the allocated buffers from the converter to properly deallocate them before the next packet is being converted
var converterBuffer: UnsafeMutableRawPointer?
var converterDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?
required init(withRemoteUrl url: AudioURL, toEngineAudioFormat: AVAudioFormat, withPCMBufferSize size: AVAudioFrameCount) throws {
self.engineAudioFormat = toEngineAudioFormat
self.pcmBufferSize = size
do {
parser = try AudioParser(withRemoteUrl: url, parsedFileAudioFormatCallback: {
[weak self] (fileAudioFormat: AVAudioFormat) in
@@ -108,17 +115,17 @@ class AudioConverter: AudioConvertable {
}
}
func pullBuffer(withSize size: AVAudioFrameCount) throws -> AVAudioPCMBuffer {
func pullBuffer() throws -> AVAudioPCMBuffer {
guard let converter = converter else {
Log.debug("reader_error trying to read before converter has been created")
throw ConverterError.cannotCreatePCMBufferWithoutConverter
}
guard let pcmBuffer = AVAudioPCMBuffer(pcmFormat: engineAudioFormat, frameCapacity: size) else {
guard let pcmBuffer = AVAudioPCMBuffer(pcmFormat: engineAudioFormat, frameCapacity: pcmBufferSize) else {
Log.monitor(ConverterError.failedToCreatePCMBuffer.errorDescription as Any)
throw ConverterError.failedToCreatePCMBuffer
}
pcmBuffer.frameLength = size
pcmBuffer.frameLength = pcmBufferSize
/**
The whole thing is wrapped in queue.sync() because the converter listener
@@ -127,7 +134,7 @@ class AudioConverter: AudioConvertable {
*/
return try queue.sync { () -> AVAudioPCMBuffer in
let framesPerPacket = engineAudioFormat.streamDescription.pointee.mFramesPerPacket
var numberOfPacketsWeWantTheBufferToFill = size / framesPerPacket
var numberOfPacketsWeWantTheBufferToFill = pcmBuffer.frameLength / framesPerPacket
let context = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
let status = AudioConverterFillComplexBuffer(converter, ConverterListener, context, &numberOfPacketsWeWantTheBufferToFill, pcmBuffer.mutableAudioBufferList, nil)
@@ -65,6 +65,10 @@ func ConverterListener(_ converter: AudioConverterRef, _ packetCount: UnsafeMuta
return ReaderShouldNotHappenError
}
if let lastBuffer = selfAudioConverter.converterBuffer {
lastBuffer.deallocate()
}
// Copy data over (note we've only processing a single packet of data at a time)
var packet = audioPacket.1
let packetByteCount = packet.count //this is not the count of an array
@@ -75,6 +79,12 @@ func ConverterListener(_ converter: AudioConverterRef, _ packetCount: UnsafeMuta
})
ioData.pointee.mBuffers.mDataByteSize = UInt32(packetByteCount)
selfAudioConverter.converterBuffer = ioData.pointee.mBuffers.mData
if let lastDescription = selfAudioConverter.converterDescriptions {
lastDescription.deallocate()
}
// Handle packet descriptions for compressed formats (MP3, AAC, etc)
let fileFormatDescription = fileAudioFormat.streamDescription.pointee
if fileFormatDescription.mFormatID != kAudioFormatLinearPCM {
@@ -86,6 +96,8 @@ func ConverterListener(_ converter: AudioConverterRef, _ packetCount: UnsafeMuta
outPacketDescriptions?.pointee?.pointee.mVariableFramesInPacket = 0
}
selfAudioConverter.converterDescriptions = outPacketDescriptions?.pointee
packetCount.pointee = 1
//we've successfully given a packet to the LPCM buffer now we can process the next audio packet
@@ -56,10 +56,30 @@ public struct SAAudioAvailabilityRange {
}
}
var secondsLeftToBuffer: Double {
get {
return predictedDurationToLoad - (startingNeedle + durationLoadedByNetwork)
}
}
public func contains(_ needle: Double) -> Bool {
return needle >= startingNeedle && (needle - startingNeedle) < durationLoadedByNetwork
}
public func reachedEndOfAudio(needle: Double) -> Bool {
var needleAtEnd = false
if(totalDurationBuffered > 0 && needle > 0) {
needleAtEnd = needle >= totalDurationBuffered - 1
}
// if most of the audio is buffered for long audio or in short audio there isn't many seconds left to buffer it means wwe've reached the end of the audio
let isBuffered = (bufferingProgress > 0.99 || secondsLeftToBuffer < 5)
return isBuffered && needleAtEnd
}
public func isCompletelyBuffered() -> Bool {
return startingNeedle + durationLoadedByNetwork >= predictedDurationToLoad
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

+34
View File
@@ -0,0 +1,34 @@
//
// AudioQueue.swift
// SwiftAudioPlayer
//
// Created by Joe Williams on 3/10/21.
//
import Foundation
// wrapper for array of urls
struct AudioQueue<T> {
private var audioUrls: [T] = []
var isQueueEmpty: Bool {
return audioUrls.isEmpty
}
var count: Int {
return audioUrls.count
}
var front: T? {
return audioUrls.first
}
mutating func append(item: T) {
audioUrls.append(item)
}
mutating func dequeue() -> T? {
guard !isQueueEmpty else { return nil }
return audioUrls.removeFirst()
}
}
+83 -5
View File
@@ -56,6 +56,15 @@ public class SAPlayer {
}
}
/**
Unique ID for the current engine. This will be nil if no audio has been initialized which means no engine exists.
*/
public var engineUID: String? {
get {
return player?.key
}
}
/**
Access the player node of the engine. Node is nil if player has not been initialized with audio.
@@ -83,6 +92,47 @@ public class SAPlayer {
}
}
/**
Corresponding to the rate of audio playback. This rate assumes use of the default rate modifier at the first index of `audioModifiers`; if you removed that modifier than this will be nil. If no audio has been initialized then this will also be nil.
- Note: By default this engine has added a pitch modifier node to change the pitch so that on playback rate changes of spoken word the pitch isn't shifted.
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.
For more details on pitch modifiers for playback rate changes please look at [developer.apple.com/forums/thread/6050](https://developer.apple.com/forums/thread/6050).
*/
public var rate: Float? {
get {
return (audioModifiers.first as? AVAudioUnitTimePitch)?.rate
}
set {
guard let value = newValue else { return }
guard let node = audioModifiers.first as? AVAudioUnitTimePitch else { return }
node.rate = value
playbackRateOfAudioChanged(rate: value)
// if skip silences was on, reset it to have the new rate
// TODO fix this to rate being broadcasted and handled in only Features.SkipSilences https://github.com/tanhakabir/SwiftAudioPlayer/issues/77
// if Features.SkipSilences.enabled && !(value == rate ?? 1.0 - 0.5 || value == rate ?? 1.0 + 0.5) {
// _ = Features.SkipSilences.disable()
// _ = Features.SkipSilences.enable()
// }
}
}
/**
Corresponding to the skipping forward button on the media player on the lockscreen. Default is set to 30 seconds.
*/
@@ -127,6 +177,17 @@ public class SAPlayer {
*/
public var audioModifiers: [AVAudioUnit] = []
/**
List of audio URLs queued for playback.
*/
public var audioQueued: [URL] {
get {
return presenter.audioQueue.map { (queued) -> URL in
return queued.1
}
}
}
/**
Total duration of current audio initialized. Returns nil if no audio is initialized in player.
@@ -394,15 +455,35 @@ extension SAPlayer {
presenter.handlePlayStreamedAudio(withRemoteUrl: url)
}
/**
Stops any streaming in progress.
*/
public func stopStreamingRemoteAudio() {
presenter.handleStopStreamingAudio()
}
/**
Queues remote audio to be played next. The URLs in the queue can be both remote or on disk but once the queued audio starts playing it will start buffering and loading then. This means no guarantee for a 'gapless' playback where there might be several moments in between one audio ending and another starting due to buffering remote audio.
- Parameter withRemoteUrl: The URL of the remote audio.
*/
public func queueRemoteAudio(withRemoteUrl url: URL) {
presenter.handleQueueStreamedAudio(withRemoteUrl: url)
}
/**
Queues saved audio to be played next. The URLs in the queuecan be both remote or on disk but once the queued audio starts playing it will start buffering and loading then. This means no guarantee for a 'gapless' playback where there might be several moments in between one audio ending and another starting due to buffering remote audio.
- Parameter withSavedUrl: The URL of the audio saved on the device.
*/
public func queueSavedAudio(withSavedUrl url: URL) {
presenter.handleQueueSavedAudio(withSavedUrl: url)
}
/**
Resets the player to the state before initializing audio and setting media info.
*/
public func clear() {
player = nil
presenter.handleClear()
}
}
@@ -411,14 +492,10 @@ extension SAPlayer {
//MARK: - Internal implementation of delegate
extension SAPlayer: SAPlayerDelegate {
func startAudioDownloaded(withSavedUrl url: AudioURL) {
player?.pause()
player?.invalidate()
player = AudioDiskEngine(withSavedUrl: url, delegate: presenter)
}
func startAudioStreamed(withRemoteUrl url: AudioURL) {
player?.pause()
player?.invalidate()
player = AudioStreamEngine(withRemoteUrl: url, delegate: presenter)
}
@@ -426,6 +503,7 @@ extension SAPlayer: SAPlayerDelegate {
player?.pause()
player?.invalidate()
player = nil
Log.info("cleared engine")
}
func playEngine() {
+123
View File
@@ -0,0 +1,123 @@
//
// SAPlayerFeature.swift
// SwiftAudioPlayer
//
// Created by Tanha Kabir on 3/10/21.
//
import Foundation
import AVFoundation
extension SAPlayer {
/**
Special features for audio manipulation. These are examples of manipulations you can do with the player outside of this library. This is just an aggregation of community contibuted ones.
- Note: These features assume default state of the player and `audioModifiers` meaning some expect the first audio modifier to be the default `AVAudioUnitTimePitch` that comes with the SAPlayer.
*/
public struct Features {
/**
Feature to skip silences in spoken word audio. The player will speed up the rate of audio playback when silence is detected.
- Important: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
*/
public struct SkipSilences {
static var enabled: Bool = false
/**
Enable feature to skip silences in spoken word audio. The player will speed up the rate of audio playback when silence is detected. This can be called at any point of audio playback.
- Important: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
*/
public static func enable() -> Bool {
guard let engine = SAPlayer.shared.engine else { return false }
Log.info("enabling skip silences feature")
enabled = true
let originalRate = SAPlayer.shared.rate ?? 1.0
let format = engine.mainMixerNode.outputFormat(forBus: 0)
// look at documentation here to get an understanding of what is happening here: https://www.raywenderlich.com/5154-avaudioengine-tutorial-for-ios-getting-started#toc-anchor-005
engine.mainMixerNode.installTap(onBus: 0, bufferSize: 1024, format: format) { buffer, when in
guard let channelData = buffer.floatChannelData else {
return
}
let channelDataValue = channelData.pointee
let channelDataValueArray = stride(from: 0,
to: Int(buffer.frameLength),
by: buffer.stride).map { channelDataValue[$0] }
let rms = sqrt(channelDataValueArray.map { $0 * $0 }.reduce(0, +) / Float(buffer.frameLength))
let avgPower = 20 * log10(rms)
let meterLevel = self.scaledPower(power: avgPower)
Log.debug("meterLevel: \(meterLevel)")
if meterLevel < 0.6 { // below 0.6 decibels is below audible audio
SAPlayer.shared.rate = originalRate + 0.5
Log.test("speed up rate to \(String(describing: SAPlayer.shared.rate))")
} else {
SAPlayer.shared.rate = originalRate
Log.test("slow down rate to \(String(describing: SAPlayer.shared.rate))")
}
}
return true
}
/**
Disable feature to skip silences in spoken word audio. The player will speed up the rate of audio playback when silence is detected. This can be called at any point of audio playback.
- Important: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
*/
public static func disable() -> Bool {
// TODO fix disabling on speed up portion and being stuck at faster speed https://github.com/tanhakabir/SwiftAudioPlayer/issues/76
guard let engine = SAPlayer.shared.engine else { return false }
Log.info("disabling skip silences feature")
engine.mainMixerNode.removeTap(onBus: 0)
enabled = false
return true
}
private static func scaledPower(power: Float) -> Float {
guard power.isFinite else { return 0.0 }
let minDb: Float = -80.0
if power < minDb {
return 0.0
} else if power >= 1.0 {
return 1.0
} else {
return (abs(minDb) - abs(power)) / abs(minDb)
}
}
}
/**
Feature to pause the player after a delay. This will happen regardless of if another audio clip has started.
*/
public struct SleepTimer {
static var timer: Timer?
/**
Enable feature to pause the player after a delay. This will happen regardless of if another audio clip has started.
- Parameter afterDelay: The number of seconds to wait before pausing the audio
*/
public static func enable(afterDelay delay: Double) {
timer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false, block: { _ in
SAPlayer.shared.pause()
})
}
/**
Disable feature to pause the player after a delay.
*/
public static func disable() {
timer?.invalidate()
}
}
}
}
+62 -10
View File
@@ -28,6 +28,11 @@ import AVFoundation
import MediaPlayer
class SAPlayerPresenter {
enum Location {
case remote
case disk
}
weak var delegate: SAPlayerDelegate?
var shouldPlayImmediately = false //for auto-play
@@ -43,13 +48,12 @@ class SAPlayerPresenter {
var durationRef:UInt = 0
var needleRef:UInt = 0
var playingStatusRef:UInt = 0
var audioQueue: [(Location, URL)] = []
init(delegate: SAPlayerDelegate?) {
self.delegate = delegate
delegate?.setLockScreenControls(presenter: self)
prepareNextEpisodeToPlay()
}
func getUrl(forKey key: Key) -> URL? {
@@ -61,6 +65,8 @@ class SAPlayerPresenter {
}
func handleClear() {
delegate?.clearEngine()
needle = nil
duration = nil
key = nil
@@ -73,15 +79,31 @@ class SAPlayerPresenter {
}
func handlePlaySavedAudio(withSavedUrl url: URL) {
// Because we support queueing, we want to clear off any existing players.
// Therefore, instantiate new player every time, destroy any existing ones.
// This prevents a crash where an owning engine already exists.
handleClear()
attachForUpdates(url: url)
delegate?.startAudioDownloaded(withSavedUrl: url)
}
func handlePlayStreamedAudio(withRemoteUrl url: URL) {
// Because we support queueing, we want to clear off any existing players.
// Therefore, instantiate new player every time, destroy any existing ones.
// This prevents a crash where an owning engine already exists.
handleClear()
attachForUpdates(url: url)
delegate?.startAudioStreamed(withRemoteUrl: url)
}
func handleQueueStreamedAudio(withRemoteUrl url: URL) {
audioQueue.append((.remote, url))
}
func handleQueueSavedAudio(withSavedUrl url: URL) {
audioQueue.append((.disk, url))
}
private func attachForUpdates(url: URL) {
detachFromUpdates()
@@ -120,6 +142,15 @@ class SAPlayerPresenter {
}
self.isPlaying = isPlaying
if(self.isPlaying == .paused && self.shouldPlayImmediately) {
self.shouldPlayImmediately = false
self.handlePlay()
}
if(self.isPlaying == .ended) {
self.playNextAudioIfExists()
}
})
}
@@ -196,17 +227,38 @@ extension SAPlayerPresenter: AudioEngineDelegate {
func didError() {
Log.monitor("We should have handled engine error")
}
func didEndPlaying() {
// TODO
// playNextEpisode()
}
}
//MARK:- Autoplay
//FIXME: This needs to be refactored
extension SAPlayerPresenter {
func prepareNextEpisodeToPlay() {
// TODO
func playNextAudioIfExists() {
Log.info("looking foor next audio in queue to play")
guard audioQueue.count > 0 else {
Log.info("no queued audio")
return
}
let nextAudioURL = audioQueue.removeFirst()
let key = nextAudioURL.1.key
Log.info("getting ready to play \(nextAudioURL)")
AudioQueueDirector.shared.changeInQueue(key, url: nextAudioURL.1)
handleClear()
// We need to give a second to clean up the previous engine properly. Deinit takes some time.
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { [weak self] (_) in
guard let self = self else { return }
switch nextAudioURL.0 {
case .remote:
self.handlePlayStreamedAudio(withRemoteUrl: nextAudioURL.1)
break
case .disk:
self.handlePlaySavedAudio(withSavedUrl: nextAudioURL.1)
}
self.shouldPlayImmediately = true
}
}
}
+24
View File
@@ -197,6 +197,30 @@ extension SAPlayer {
DownloadProgressDirector.shared.detach(withID: id)
}
}
public struct AudioQueue {
/**
Subscribe to updates to changes in the progress of your audio queue. When streaming audio playback completes
and continues onto the next track, the closure is invoked.
- 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.
- Parameter url: The corresponding remote URL for the forthcoming audio file.
- 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 (_ key: String, _ newUrl: URL) -> ()) -> UInt {
return AudioQueueDirector.shared.attach(closure: { (key, url) in
closure(key, url)
})
}
/**
Stop recieving updates of changes in download progress.
- Parameter id: The closure with this id will stop receiving updates.
*/
public static func unsubscribe(_ id: UInt) {
AudioQueueDirector.shared.detach(withID: id)
}
}
}
}
+16 -14
View File
@@ -23,6 +23,8 @@ enum LogLevel: Int {
// 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
typealias State = String
class Log {
private init() {}
@@ -42,11 +44,11 @@ class Log {
- Parameter functionName: automatically generated based on the function that called this function
- Parameter lineNumber: automatically generated based on the line that called this function
*/
public static func test(_ logMessage: Any, classPath: String = #file, functionName: String = #function, lineNumber: Int = #line) {
public static func test(_ logMessage: Any, _ state:State? = nil, classPath: String = #file, functionName: String = #function, lineNumber: Int = #line) {
let fileName = URLUtil.getNameFromStringPath(classPath)
if logLevel.rawValue <= LogLevel.TEST.rawValue {
let log = OSLog(subsystem: SUBSYSTEM, category: "TEST ❇️❇️❇️❇️")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(state ?? "") \(logMessage)")
}
}
@@ -63,16 +65,16 @@ class Log {
- Parameter functionName: automatically generated based on the function that called this function
- Parameter lineNumber: automatically generated based on the line that called this function
*/
public static func error(_ logMessage: Any, classPath: String = #file, functionName: String = #function, lineNumber: Int = #line) {
public static func error(_ logMessage: Any, _ state:State? = nil, 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: "ERROR 🛑🛑🛑🛑")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(state ?? "") \(logMessage)")
}
if logLevel.rawValue <= LogLevel.EXTERNAL_DEBUG.rawValue {
let log = OSLog(subsystem: SUBSYSTEM, category: "WARNING")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(state ?? "") \(logMessage)")
}
}
@@ -89,11 +91,11 @@ class Log {
- Parameter functionName: automatically generated based on the function that called this function
- Parameter lineNumber: automatically generated based on the line that called this function
*/
public static func monitor(_ logMessage: Any, classPath: String = #file, functionName: String = #function, lineNumber: Int = #line) {
public static func monitor(_ logMessage: Any, _ state:State? = nil, 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: "ERROR 🔥🔥🔥🔥")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(state ?? "") \(logMessage)")
}
}
@@ -110,16 +112,16 @@ class Log {
- Parameter functionName: automatically generated based on the function that called this function
- Parameter lineNumber: automatically generated based on the line that called this function
*/
public static func warn(_ logMessage: Any, classPath: String = #file, functionName: String = #function, lineNumber: Int = #line) {
public static func warn(_ logMessage: Any, _ state:State? = nil, classPath: String = #file, functionName: String = #function, lineNumber: Int = #line) {
let fileName = URLUtil.getNameFromStringPath(classPath)
if logLevel.rawValue <= LogLevel.WARN.rawValue {
let log = OSLog(subsystem: SUBSYSTEM, category: "WARN ⚠️⚠️⚠️⚠️")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(state ?? "") \(logMessage)")
}
if logLevel.rawValue <= LogLevel.EXTERNAL_DEBUG.rawValue {
let log = OSLog(subsystem: SUBSYSTEM, category: "DEBUG")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(state ?? "") \(logMessage)")
}
}
@@ -136,11 +138,11 @@ class Log {
- Parameter functionName: automatically generated based on the function that called this function
- Parameter lineNumber: automatically generated based on the line that called this function
*/
public static func info(_ logMessage: Any, classPath: String = #file, functionName: String = #function, lineNumber: Int = #line) {
public static func info(_ logMessage: Any, _ state:State? = nil, classPath: String = #file, functionName: String = #function, lineNumber: Int = #line) {
let fileName = URLUtil.getNameFromStringPath(classPath)
if logLevel.rawValue <= LogLevel.INFO.rawValue {
let log = OSLog(subsystem: SUBSYSTEM, category: "INFO 🖤🖤🖤🖤")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(state ?? "") \(logMessage)")
}
}
@@ -157,11 +159,11 @@ class Log {
- Parameter functionName: automatically generated based on the function that called this function
- Parameter lineNumber: automatically generated based on the line that called this function
*/
public static func debug(_ logMessage: Any?..., classPath: String = #file, functionName: String = #function, lineNumber: Int = #line) {
public static func debug(_ logMessage: Any?..., state:State? = nil, classPath: String = #file, functionName: String = #function, lineNumber: Int = #line) {
let fileName = URLUtil.getNameFromStringPath(classPath)
if logLevel.rawValue <= LogLevel.DEBUG.rawValue {
let log = OSLog(subsystem: SUBSYSTEM, category: "DEBUG 🐝🐝🐝🐝")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(state ?? "") \(logMessage)")
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudioPlayer'
s.version = '2.13.0'
s.version = '4.1.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.