Compare commits

...

163 Commits

Author SHA1 Message Date
tanhakabir ddf26e206e Release 7.3.0 (adds Changelog) 2021-08-17 10:16:26 -07:00
tanhakabir e319134eb8 Merge pull request #144 from cntrump/pr/replace_deprecated_method
Replace deprecated subscribe method.
2021-08-17 10:12:51 -07:00
Lvv.me 9599f66a0f Replace deprecated subscribe method. 2021-08-17 19:58:44 +08:00
tanhakabir ef231a2570 Release 7.2.1 2021-08-17 03:51:21 -07:00
tanhakabir 350e6ec064 Fix directory bug 2021-08-17 03:51:01 -07:00
tanhakabir ad63b89ede Release 7.2.0 2021-08-17 03:38:52 -07:00
tanhakabir 43e887b823 Merge pull request #143 from tanhakabir/issue-138
Experimental set download location
2021-08-17 03:38:28 -07:00
tanhakabir 006b94ea10 experimental set download location 2021-08-17 03:38:00 -07:00
tanhakabir abb0a29fb4 Release 7.1.0 2021-08-17 03:13:19 -07:00
tanhakabir ed3ba9698d Merge pull request #132 from cntrump/pr_fix_seek_fail
Fix seek fail issue when data is not loaded for AudioStreamEngine.
2021-08-17 03:12:43 -07:00
tanhakabir 294902e3fe Release 7.0.1 2021-08-17 03:06:08 -07:00
tanhakabir 764f0b130b Merge pull request #141 from tanhakabir/issue-140
Refractor cache for broadcasting updates
2021-08-17 03:04:48 -07:00
tanhakabir 952dc1978f Update README.md 2021-08-17 03:04:11 -07:00
tanhakabir 1752572770 clean up example app 2021-08-17 02:59:42 -07:00
tanhakabir 223600f30c add deprecation warnings 2021-08-17 02:39:31 -07:00
tanhakabir 4d90c539cd update streaming director to new broadcasting 2021-08-17 02:38:16 -07:00
tanhakabir fd9b451f45 remove unnecessary key from queue director 2021-08-17 02:22:29 -07:00
tanhakabir fd3e78f13c refractor director broadcasting 2021-08-17 02:09:57 -07:00
tanhakabir af3d553011 Release 6.4.0 2021-08-16 21:09:48 -07:00
tanhakabir d0f9127c65 Merge pull request #139 from tanhakabir/issue-128
Fix runtime error on queuing audio.
2021-08-16 21:09:04 -07:00
tanhakabir 27d5ce4f03 fix error 2021-08-16 21:04:23 -07:00
tanhakabir 3f93bd1a86 Release 6.3.1 2021-08-16 19:52:35 -07:00
tanhakabir 20c0253f68 Fix documentation 2021-08-16 19:52:15 -07:00
tanhakabir 5e78c446a9 Release 6.3.0 2021-08-16 19:36:27 -07:00
tanhakabir 487b071490 Make audio queue an easy to edit list 2021-08-16 19:33:29 -07:00
tanhakabir b79d16b409 Add functions to remove queued audio items 2021-08-14 21:38:31 -07:00
tanhakabir 4684a92380 Release 6.2.0 2021-08-14 21:08:44 -07:00
tanhakabir 2cff597e45 Add documentation 2021-08-14 21:06:38 -07:00
tanhakabir c25071d83a Merge branch 'master' into pr_fix_seek_fail 2021-08-12 23:56:19 -07:00
tanhakabir 98dc7cfa3c Release 6.1.0 2021-08-12 23:53:47 -07:00
tanhakabir 4f1242f56d Merge pull request #136 from tanhakabir/issue-loop
Issue loop
2021-08-12 19:44:47 -07:00
tanhakabir f3c91ccc34 Fix buffering causing full pause 2021-08-12 19:44:12 -07:00
tanhakabir 2d88b69aa7 Fix update loops killing on end of audio 2021-08-12 16:58:38 -07:00
tanhakabir f67b939ac4 Merge branch 'master' into issue-loop 2021-08-12 16:27:34 -07:00
tanhakabir a0e9b973e0 checkpoint, working but bug in seek 2021-08-12 16:27:20 -07:00
tanhakabir ef54080a68 Merge pull request #133 from cntrump/pr_fix_some_required_condition_is_false_cases
Fix some "required condition is false" cases.
2021-08-12 16:26:39 -07:00
tanhakabir 2d35bbad59 Merge pull request #135 from cntrump/pr_improve_tvos_support
Improve tvOS support
2021-08-12 15:55:04 -07:00
tanhakabir 13b68920d1 Merge pull request #134 from cntrump/pr_improve_lockscreen_info
Improve LockScreen Info
2021-08-12 14:04:15 -07:00
Lvv.me 2e8f44c553 Improve tvOS support 2021-08-12 21:00:21 +08:00
Lvv.me 58ac9b5ae5 Improve LockScreen Info
- Add albumTitle for SALockScreenInfo
- Support disable skip forward/backward command
2021-08-10 21:22:31 +08:00
Lvv.me 706ab5961c Fix some "required condition is false" cases.
case 1:
```
"required condition is false: nil == owningEngine || GetEngine() == owningEngine"
```

case 2:
```
"required condition is false: _engine->IsRunning()"
```
2021-08-10 17:38:08 +08:00
Lvv.me 19db1fc74b Fix seek fail issue when data is not loaded for AudioStreamEngine. 2021-08-09 15:50:38 +08:00
tanhakabir 50139ca8c5 Merge pull request #130 from cntrump/pr_improve_dataTask_config_order
set taskDescription before resume() called.
2021-08-09 00:30:29 -07:00
tanhakabir 6c3e52b66e Merge pull request #131 from cntrump/pr_add_missing_return
Add missing `return` in doneCallback() for AudioStreamWorker.
2021-08-09 00:30:08 -07:00
Lvv.me 6d955687a3 Add missing return in doneCallback() for AudioStreamWorker. 2021-08-09 14:11:13 +08:00
Lvv.me 38d5740f4d set taskDescription before resume() called. 2021-08-09 11:12:55 +08:00
tanhakabir 4cbfb4b16b Improve end of audio detection 2021-08-08 11:22:22 -07:00
tanhakabir 01668790f3 Release 6.0.0 2021-08-08 11:04:23 -07:00
tanhakabir 1cf8fb99ba Remove unnecessary project files 2021-08-08 11:02:37 -07:00
tanhakabir 2d3fe83a56 Merge pull request #125 from cntrump/pr_spm_and_tvos_support
Add SPM and tvOS support
2021-08-08 10:46:57 -07:00
tanhakabir 5f63b52592 support custom HTTPHeaderFields
Fixes #85

Co-Authored-By: cntrump <me@lvv.me>
2021-08-08 10:31:56 -07:00
Lvv.me 9111ac6257 Add SPM and tvOS support 2021-08-08 17:08:54 +08:00
tanhakabir bfbb979897 Add stopping engine and playerNode on invalidate() and deinit
Co-Authored-By: cntrump <me@lvv.me>
2021-08-07 19:06:59 -07:00
tanhakabir 0b40a6f0b4 Release 5.2.0 2021-07-20 10:50:43 -07:00
tanhakabir f9465f54a0 Merge pull request #118 from dylancom/issue-117
fix: prevent mediainfo not being available yet in callbacks
2021-07-20 10:32:34 -07:00
tanhakabir 8ce28db471 Add documentation for important order 2021-07-20 10:32:16 -07:00
Dylan Companjen a84f834f45 fix: prevent mediainfo not being available yet in callbacks 2021-07-19 18:40:01 +02:00
tanhakabir e3e3af2b7a Release 5.1.0 2021-07-18 23:09:08 -07:00
tanhakabir 6987458f0a Merge pull request #114 from dylancom/issue-112
feat: pause player on interruption, resume when necessary
2021-07-18 22:58:50 -07:00
tanhakabir c912d5f381 update formatting and control functions 2021-07-18 22:57:58 -07:00
Dylan c444ae4c9f feat: pause player on interruption, resume when necessary 2021-07-16 15:08:44 +02:00
tanhakabir 6d3f3c6d6f Update README.md 2021-07-12 14:18:08 -04:00
tanhakabir bb7f1d1d0a Release 5.0.5 2021-05-09 17:16:22 -07:00
tanhakabir 6c446f27e0 fix maintaining rate changes while skip silences is enabled (#110) 2021-05-09 17:15:44 -07:00
tanhakabir c513c723ed Release 5.0.4 2021-05-08 18:57:11 -07:00
tanhakabir b34a264aec Fix bug introduced by queuing for lockscreen info (#108)
* add extra data for queuing and change order of clearing mediaInfo

* make SAPlayer the only source for mediaInfo

Co-authored-by: @dylancom
Co-authored-by: @dezinezync
2021-05-08 18:55:08 -07:00
tanhakabir a83c2f702f pass lockscreen info to player in example app (#107)
related to #106
2021-05-07 13:16:24 -07:00
tanhakabir 8644bf24fb Merge pull request #105 from Husseinhj/patch-1
Anchor link in SAPlayer.Updates fixed
2021-05-03 20:42:19 -07:00
tanhakabir 69a979cb98 Add twitter handle to podspec 2021-05-03 20:40:57 -07:00
Hussein Habibi Juybari 6ba43e70ea Rollback Contact header changes 2021-05-03 11:34:30 +04:30
Hussein Habibi Juybari 6f19009000 Anchor link in SAPlayer.Updates fixed 2021-05-03 11:32:30 +04:30
tanhakabir 64677ad6ce Release 5.0.3 2021-04-28 15:33:46 -07:00
tanhakabir 3894309706 Merge pull request #102 from niczyja/session-fix
Fix for audio session setup on iOS 11 and up
2021-04-26 09:41:07 -07:00
Maciej Sienkiewicz e44f16258f Fix for audio session setup on iOS 11 and up 2021-04-26 14:43:13 +02:00
tanhakabir 1e3cf35b7b Release 5.0.2 2021-04-21 10:06:11 -07:00
tanhakabir 4bfb3f1774 fix packet parsing crash by putting audiopacket actions on a lock
close #94

Co-Authored-By: fayinsky <38639193+fayinsky@users.noreply.github.com>
2021-04-21 10:05:39 -07:00
tanhakabir e056336955 Release 5.0.1 2021-04-20 23:40:07 -07:00
tanhakabir 64d2959a27 fix volume changes taking effect on engine
Co-Authored-By: fayinsky <38639193+fayinsky@users.noreply.github.com>
2021-04-20 23:37:36 -07:00
tanhakabir eb1675d4fd Merge pull request #97 from niczyja/issue-95
Fix for https://github.com/tanhakabir/SwiftAudioPlayer/issues/95
2021-04-20 23:35:33 -07:00
tanhakabir ca7e48cbe7 Merge pull request #96 from niczyja/issue-76
Fix for https://github.com/tanhakabir/SwiftAudioPlayer/issues/76
2021-04-20 23:33:37 -07:00
tanhakabir 653f2817bc remove need to pass in current rate 2021-04-20 23:32:53 -07:00
Maciej Sienkiewicz edff806647 Fix for https://github.com/tanhakabir/SwiftAudioPlayer/issues/95 2021-04-20 14:56:27 +02:00
Maciej Sienkiewicz c47d623118 Fix for https://github.com/tanhakabir/SwiftAudioPlayer/issues/76 2021-04-20 14:36:51 +02:00
tanhakabir b270cf86ab Release 5.0.0 2021-04-17 09:50:47 -07:00
tanhakabir 5c2fd7dc97 Fix radio streaming playback to be smooth (#92)
Fix radio streaming playback to be smooth
2021-04-17 09:49:40 -07:00
tanhakabir d21ef34392 update README with bitrate info 2021-04-17 09:48:02 -07:00
tanhakabir e6d54b0c33 remove test logs 2021-04-17 09:44:17 -07:00
tanhakabir 7a1e5bca74 put all playernode actions on a queue 2021-04-17 09:35:42 -07:00
tanhakabir 1996812c90 checkpoint 2021-04-17 09:17:19 -07:00
tanhakabir 6e1f8f12d4 fix crash 2021-04-16 18:03:34 -07:00
tanhakabir 625e1ab169 remove unused code 2021-04-16 17:30:09 -07:00
tanhakabir 52c33518ad add option to change bitrate for radio 2021-04-16 17:20:48 -07:00
tanhakabir 3f6fc327ff remove network data wrapper, seeking broken 2021-04-16 16:17:36 -07:00
tanhakabir e3e4e4dd46 Release 4.2.0 2021-04-16 10:05:57 -07:00
tanhakabir b60e567a83 fix recursive polling logic (#91) 2021-04-16 10:05:21 -07:00
tanhakabir 17e0ee5dd8 Update example app to include a radio stream link (#89)
* update links

* update example app to show podcast, soundbite, and radio
2021-04-06 21:16:13 -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 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 751ca765d5 Release 2.13.0 2021-03-06 19:51:39 -08:00
tanhakabir 68ea5a9468 fix crash on uncompressed audio (#69) 2021-03-06 19:50:18 -08:00
tanhakabir 46ab845c8e Release 2.12.0 2021-02-22 22:47:40 -08:00
tanhakabir b597704115 Fix locating files in downloads
Co-Authored-By: fayinsky <38639193+fayinsky@users.noreply.github.com>
2021-02-22 22:42:27 -08:00
tanhakabir 889e2257ab Add documentation for supported file types (#63) 2021-02-22 22:37:26 -08:00
tanhakabir e962008b4c Fix freezing on seek (#62) 2021-02-22 22:32:54 -08:00
tanhakabir d6c1d13d7d create API for setting preference for downloading on cellular data (#61)
* set cellular downloads to true to allow for simulator downloads

* add documentation
2021-02-22 22:13:36 -08:00
tanhakabir 922a794d09 remove noisey logs 2021-02-22 21:48:34 -08:00
tanhakabir 96092a208c Add files per Xcode 9.3 2021-02-22 17:37:07 -08:00
tanhakabir b71729035d Open up access to playerNode (#60) 2021-02-22 17:34:59 -08:00
tanhakabir 2abba6f0cc Release 2.11.0 2020-10-22 12:22:20 -07:00
tanhakabir f081b7549d add endpoint to remove default pitch modifier (#51) 2020-10-22 11:59:01 -07:00
tanhakabir 55fbae7b4a Update README to include module name for import (#47) 2020-09-19 16:02:46 -07:00
tanhakabir 2acbde2efa Release 2.10.0 2020-09-19 15:52:25 -07:00
Moises Inzunza acbdf05d4f Fixed bug in swift 5.3 (#46) 2020-09-19 12:19:50 -07:00
Tanha c325caa914 Release 2.9.0 2020-03-06 23:53:42 -08:00
tanhakabir dd54d81573 implement streaming cancellation (#32)
* cancels streaming initally, but player enters weird state trying to stream again

* calling superclass invalidate from engines for good measure

* Fix UI to fix seemingly weird state

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

* to see log

* set project

* new UI

* fix ui

* re-fix UI

* add example of equalizer

* Revert "re-fix UI"

This reverts commit 05ed993a52.

* Revert "fix ui"

This reverts commit 0da9f6adea.

* Revert "new UI"

This reverts commit ffd6a95a2d.

* Add verbose debug mode to player

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

* update documentation

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

* add player clearing functionality
2019-12-03 01:25:58 -08:00
tanhakabir e1d3da1ddb Update README.md 2019-12-01 11:10:12 -08:00
Tanha 8c2524d990 Release 2.3.0 2019-12-01 01:17:54 -08:00
Tanha be1b7aa05f fix bug on bad network and streaming being stuck in missing data state
close #4
2019-12-01 01:16:03 -08:00
Tanha 4b57fee75c Release 2.2.0 2019-11-29 17:18:33 -08:00
tanhakabir fc9c43a23c Update accessing bytes of Data for Swift 5 (#17) 2019-11-29 17:16:53 -08:00
43 changed files with 2260 additions and 680 deletions
+6
View File
@@ -0,0 +1,6 @@
# Changelog
## 7.3.0
- Take in PR from @cntrump to use the non-deprecated subscription pattern in loop feature
+45 -35
View File
@@ -7,19 +7,19 @@
objects = {
/* Begin PBXBuildFile section */
27E3EC64A90305ACA68AE35A7DC597E0 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A16F4CFC63FAC439D7A04994F579A03 /* Foundation.framework */; };
2A421C2A94DF56A00FF73322C6B470C8 /* SwiftAudioPlayer-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E268C8D5FBBF7E0E790D3AA6A70FEC2 /* SwiftAudioPlayer-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
3A31FEF49CC8C3B757EEB4EBCC9BCCF4 /* Pods-SwiftAudioPlayer_Tests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 351771425C270B04BF2A07F0262DA192 /* Pods-SwiftAudioPlayer_Tests-dummy.m */; };
418D41690EF20077112E2BE86E32FB6A /* Pods-SwiftAudioPlayer_Example-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = AB41D88A2C694FBDF26EA56381EED25F /* Pods-SwiftAudioPlayer_Example-dummy.m */; };
79D8DF73FA7CDD6E266BAE71D46E035F /* Pods-SwiftAudioPlayer_Tests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 50C71346CE708A211A5AFAC20BAE48CB /* Pods-SwiftAudioPlayer_Tests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
831B263D357A5FA2DDC7B1AE4B374092 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A16F4CFC63FAC439D7A04994F579A03 /* Foundation.framework */; };
8F93DB166237195ED222EE55B6404625 /* Pods-SwiftAudioPlayer_Example-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B0B76CB1439F4D361322144E5A65C3A /* Pods-SwiftAudioPlayer_Example-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
A40DBE292391D9CA00F86146 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40DBE282391D9C900F86146 /* Data.swift */; };
A411CE4625F9609D0039E1CD /* SAPlayerFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = A411CE4525F9609D0039E1CD /* SAPlayerFeatures.swift */; };
A41AA0D2238BB9B600A467E1 /* SAPlayingStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = A41AA0D1238BB9B600A467E1 /* SAPlayingStatus.swift */; };
A448635026CB783800CFDC29 /* DirectorThreadSafeClosures.swift in Sources */ = {isa = PBXBuildFile; fileRef = A448634F26CB783800CFDC29 /* DirectorThreadSafeClosures.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 */; };
@@ -40,15 +40,18 @@
A4681FDC220113D70018AB51 /* AudioDownloadWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681FA22200E6710018AB51 /* AudioDownloadWorker.swift */; };
A4681FDD220113DC0018AB51 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F962200E2E20018AB51 /* URL.swift */; };
A4681FDE220113DE0018AB51 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F892200DB3C0018AB51 /* Date.swift */; };
A4681FDF220113E20018AB51 /* DirectorThreadSafeClosures.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F872200DAD50018AB51 /* DirectorThreadSafeClosures.swift */; };
A4681FDF220113E20018AB51 /* DirectorThreadSafeClosuresDeprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F872200DAD50018AB51 /* DirectorThreadSafeClosuresDeprecated.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 */; };
A4827771262A216C00B6918A /* StreamingDownloadDirector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4827770262A216C00B6918A /* StreamingDownloadDirector.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 */; };
A4FBA6B5221B74C900D5A353 /* SAPlayerHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B3221B74C900D5A353 /* SAPlayerHelpers.swift */; };
A4FBA6B7221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */; };
A4FBA6B9221BAF8700D5A353 /* SAAudioAvailabilityRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B8221BAF8700D5A353 /* SAAudioAvailabilityRange.swift */; };
B73D01578ABBDB6FF402D868A6C547FF /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A16F4CFC63FAC439D7A04994F579A03 /* Foundation.framework */; };
E08AD6157EF688FE832F866CBCDA3532 /* SwiftAudioPlayer-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = FB83B3B4253D41C37C5563D34D450BF8 /* SwiftAudioPlayer-dummy.m */; };
/* End PBXBuildFile section */
@@ -85,7 +88,6 @@
509D93CD81F074F6E7C4B9DE13210ACF /* Pods_SwiftAudioPlayer_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftAudioPlayer_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
50C71346CE708A211A5AFAC20BAE48CB /* Pods-SwiftAudioPlayer_Tests-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-SwiftAudioPlayer_Tests-umbrella.h"; sourceTree = "<group>"; };
55AB0CDF00C23619C7F54FE21D0C9534 /* Pods-SwiftAudioPlayer_Example-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-SwiftAudioPlayer_Example-frameworks.sh"; sourceTree = "<group>"; };
5A16F4CFC63FAC439D7A04994F579A03 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.3.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
69AF5444212FEC2674325627F26305AD /* Pods-SwiftAudioPlayer_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-SwiftAudioPlayer_Example.release.xcconfig"; sourceTree = "<group>"; };
6EC04ECC8F7CB2AF2E4E042A6A8ECFA1 /* SwiftAudioPlayer.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; path = SwiftAudioPlayer.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
70839C5AD428953FAF3091E814FF6E31 /* Pods-SwiftAudioPlayer_Example.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-SwiftAudioPlayer_Example.modulemap"; sourceTree = "<group>"; };
@@ -96,12 +98,14 @@
99925F09FC9C6EA4B9C0508F4E2D1FE2 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A19C8F889C787C19BE4123C1896AF501 /* Pods-SwiftAudioPlayer_Example-resources.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-SwiftAudioPlayer_Example-resources.sh"; sourceTree = "<group>"; };
A39F2A138CF40C1051CA9E227429A86D /* SwiftAudioPlayer.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = SwiftAudioPlayer.modulemap; sourceTree = "<group>"; };
A40DBE282391D9C900F86146 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
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>"; };
A448634F26CB783800CFDC29 /* DirectorThreadSafeClosures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectorThreadSafeClosures.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>"; };
A4681F872200DAD50018AB51 /* DirectorThreadSafeClosuresDeprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectorThreadSafeClosuresDeprecated.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>"; };
A4681F8D2200E00E0018AB51 /* SAPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayer.swift; sourceTree = "<group>"; };
@@ -126,9 +130,14 @@
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>"; };
A4827770262A216C00B6918A /* StreamingDownloadDirector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamingDownloadDirector.swift; sourceTree = "<group>"; };
A4883AC926CC25DE0073B8B6 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; 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>"; };
A4FBA6B3221B74C900D5A353 /* SAPlayerHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerHelpers.swift; sourceTree = "<group>"; };
A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerUpdateSubscription.swift; sourceTree = "<group>"; };
A4FBA6B8221BAF8700D5A353 /* SAAudioAvailabilityRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAAudioAvailabilityRange.swift; sourceTree = "<group>"; };
AB41D88A2C694FBDF26EA56381EED25F /* Pods-SwiftAudioPlayer_Example-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-SwiftAudioPlayer_Example-dummy.m"; sourceTree = "<group>"; };
@@ -146,7 +155,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
B73D01578ABBDB6FF402D868A6C547FF /* Foundation.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -154,7 +162,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
27E3EC64A90305ACA68AE35A7DC597E0 /* Foundation.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -162,7 +169,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
831B263D357A5FA2DDC7B1AE4B374092 /* Foundation.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -208,14 +214,6 @@
path = "Target Support Files/Pods-SwiftAudioPlayer_Tests";
sourceTree = "<group>";
};
5E0D919E635D23B70123790B8308F8EF /* iOS */ = {
isa = PBXGroup;
children = (
5A16F4CFC63FAC439D7A04994F579A03 /* Foundation.framework */,
);
name = iOS;
sourceTree = "<group>";
};
5F444B7A1C462A30A1CA4CCD3A7CF7B0 /* Targets Support Files */ = {
isa = PBXGroup;
children = (
@@ -248,7 +246,6 @@
children = (
93A4A3777CF96A4AAC1D13BA6DCCEA73 /* Podfile */,
D2A5FF8756A6E3EEEA69006E1A3C81F7 /* Development Pods */,
BC3CA7F9E30CC8F7E2DD044DD34432FC /* Frameworks */,
21D946895A4F57F51246F3EBCF330719 /* Products */,
5F444B7A1C462A30A1CA4CCD3A7CF7B0 /* Targets Support Files */,
);
@@ -259,6 +256,7 @@
children = (
15DF3E7F1B5E10B1BBE49D3E9A67C938 /* LICENSE */,
B8C829A46249957CD3056074B5CC0BBB /* README.md */,
A4883AC926CC25DE0073B8B6 /* CHANGELOG.md */,
6EC04ECC8F7CB2AF2E4E042A6A8ECFA1 /* SwiftAudioPlayer.podspec */,
A4523BC8220A0B3C0079C4BC /* Credited_LICENSE */,
);
@@ -270,9 +268,11 @@
children = (
A4681F8B2200DDD50018AB51 /* Constants.swift */,
A4681F802200D0500018AB51 /* Log.swift */,
A4681F872200DAD50018AB51 /* DirectorThreadSafeClosures.swift */,
A4681F872200DAD50018AB51 /* DirectorThreadSafeClosuresDeprecated.swift */,
A4681F892200DB3C0018AB51 /* Date.swift */,
A4681F962200E2E20018AB51 /* URL.swift */,
A40DBE282391D9C900F86146 /* Data.swift */,
A448634F26CB783800CFDC29 /* DirectorThreadSafeClosures.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -295,6 +295,7 @@
A4681F9B2200E4850018AB51 /* Model */ = {
isa = PBXGroup;
children = (
A470FE2025F9AF1400F135FF /* AudioQueue.swift */,
A4681F992200E3D90018AB51 /* AudioDataManager.swift */,
A4681FA62200F0130018AB51 /* StreamProgressPTO.swift */,
A4681FA02200E5F50018AB51 /* Streaming */,
@@ -346,28 +347,31 @@
A4681FE2220117B50018AB51 /* Source */ = {
isa = PBXGroup;
children = (
A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */,
A4FBA6B3221B74C900D5A353 /* SAPlayerHelpers.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>";
};
BC3CA7F9E30CC8F7E2DD044DD34432FC /* Frameworks */ = {
A470FE0D25F9AE1800F135FF /* Directors */ = {
isa = PBXGroup;
children = (
5E0D919E635D23B70123790B8308F8EF /* iOS */,
A470FE0725F9ADF800F135FF /* DownloadProgressDirector.swift */,
A470FE0625F9ADF800F135FF /* AudioClockDirector.swift */,
A470FE1B25F9AEB900F135FF /* AudioQueueDirector.swift */,
A4827770262A216C00B6918A /* StreamingDownloadDirector.swift */,
);
name = Frameworks;
path = Directors;
sourceTree = "<group>";
};
D2A5FF8756A6E3EEEA69006E1A3C81F7 /* Development Pods */ = {
@@ -517,19 +521,24 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A448635026CB783800CFDC29 /* DirectorThreadSafeClosures.swift in Sources */,
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 */,
A4681FD2220113B20018AB51 /* AudioParser.swift in Sources */,
A4681FCF220113A40018AB51 /* AudioConverterListener.swift in Sources */,
A4681FE1220113E70018AB51 /* Constants.swift in Sources */,
A4FBA6B5221B74C900D5A353 /* SALockScreenInfo.swift in Sources */,
A40DBE292391D9CA00F86146 /* Data.swift in Sources */,
A4FBA6B5221B74C900D5A353 /* SAPlayerHelpers.swift in Sources */,
A4681FC6220113880018AB51 /* SAPlayer.swift in Sources */,
A4FBA6B7221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift in Sources */,
A4681FC72201138B0018AB51 /* SAPlayerDelegate.swift in Sources */,
A4681FD5220113BD0018AB51 /* AudioParserErrors.swift in Sources */,
A4681FC9220113920018AB51 /* LockScreenViewProtocol.swift in Sources */,
A4827771262A216C00B6918A /* StreamingDownloadDirector.swift in Sources */,
A4681FD6220113BF0018AB51 /* AudioThrottler.swift in Sources */,
A4681FCC2201139B0018AB51 /* AudioDiskEngine.swift in Sources */,
A4681FDE220113DE0018AB51 /* Date.swift in Sources */,
@@ -539,18 +548,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 */,
A4681FDF220113E20018AB51 /* DirectorThreadSafeClosures.swift in Sources */,
A4681FDF220113E20018AB51 /* DirectorThreadSafeClosuresDeprecated.swift in Sources */,
A4681FCB220113980018AB51 /* AudioEngine.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -7,14 +7,13 @@
objects = {
/* Begin PBXBuildFile section */
41B4A1BE666DAEDD342DBACF /* Pods_SwiftAudioPlayer_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E9F82E3AA46F1DA40F32F7F /* Pods_SwiftAudioPlayer_Tests.framework */; };
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; };
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; };
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; };
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 */; };
E5808EC0557FB2395AA56468 /* Pods_SwiftAudioPlayer_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E5C0E3F3235B6FFE85EF425 /* Pods_SwiftAudioPlayer_Example.framework */; };
A470FEE2260303DA00F135FF /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = A470FEE1260303DA00F135FF /* Model.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -29,9 +28,7 @@
/* Begin PBXFileReference section */
0B7D1E6C00E83B4AF8AA1781 /* Pods-SwiftAudioPlayer_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftAudioPlayer_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftAudioPlayer_Tests/Pods-SwiftAudioPlayer_Tests.release.xcconfig"; sourceTree = "<group>"; };
1E5C0E3F3235B6FFE85EF425 /* Pods_SwiftAudioPlayer_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftAudioPlayer_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
4B5DD2AE0B23A759D18926DC /* Pods-SwiftAudioPlayer_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftAudioPlayer_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftAudioPlayer_Example/Pods-SwiftAudioPlayer_Example.release.xcconfig"; sourceTree = "<group>"; };
4E9F82E3AA46F1DA40F32F7F /* Pods_SwiftAudioPlayer_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftAudioPlayer_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
607FACD01AFB9204008FA782 /* SwiftAudioPlayer_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftAudioPlayer_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -43,6 +40,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>"; };
@@ -54,7 +52,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E5808EC0557FB2395AA56468 /* Pods_SwiftAudioPlayer_Example.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -62,22 +59,12 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
41B4A1BE666DAEDD342DBACF /* Pods_SwiftAudioPlayer_Tests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
408E805A4561B2F63083E539 /* Frameworks */ = {
isa = PBXGroup;
children = (
1E5C0E3F3235B6FFE85EF425 /* Pods_SwiftAudioPlayer_Example.framework */,
4E9F82E3AA46F1DA40F32F7F /* Pods_SwiftAudioPlayer_Tests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
4246ED1215E81CA7B8F0AB36 /* Pods */ = {
isa = PBXGroup;
children = (
@@ -97,7 +84,6 @@
607FACE81AFB9204008FA782 /* Tests */,
607FACD11AFB9204008FA782 /* Products */,
4246ED1215E81CA7B8F0AB36 /* Pods */,
408E805A4561B2F63083E539 /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -115,6 +101,7 @@
children = (
607FACD51AFB9204008FA782 /* AppDelegate.swift */,
607FACD71AFB9204008FA782 /* ViewController.swift */,
A470FEE1260303DA00F135FF /* Model.swift */,
607FACD91AFB9204008FA782 /* Main.storyboard */,
607FACDC1AFB9204008FA782 /* Images.xcassets */,
607FACDE1AFB9204008FA782 /* LaunchScreen.xib */,
@@ -212,12 +199,12 @@
TargetAttributes = {
607FACCF1AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = R2392A68YQ;
DevelopmentTeam = H9Y26B6GZB;
LastSwiftMigration = 1120;
};
607FACE41AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = R2392A68YQ;
DevelopmentTeam = H9Y26B6GZB;
LastSwiftMigration = 1120;
TestTargetID = 607FACCF1AFB9204008FA782;
};
@@ -324,6 +311,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A470FEE2260303DA00F135FF /* Model.swift in Sources */,
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */,
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */,
);
@@ -475,11 +463,11 @@
baseConfigurationReference = 65A66AB4C3016E8BB53FF3E0 /* Pods-SwiftAudioPlayer_Example.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = R2392A68YQ;
DEVELOPMENT_TEAM = H9Y26B6GZB;
INFOPLIST_FILE = SwiftAudioPlayer/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo-test.SwiftAudioPlayer-Example";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
@@ -490,11 +478,11 @@
baseConfigurationReference = 4B5DD2AE0B23A759D18926DC /* Pods-SwiftAudioPlayer_Example.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = R2392A68YQ;
DEVELOPMENT_TEAM = H9Y26B6GZB;
INFOPLIST_FILE = SwiftAudioPlayer/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo-test.SwiftAudioPlayer-Example";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
@@ -504,7 +492,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = BBD877782CC67FBCC7BF7532 /* Pods-SwiftAudioPlayer_Tests.debug.xcconfig */;
buildSettings = {
DEVELOPMENT_TEAM = R2392A68YQ;
DEVELOPMENT_TEAM = H9Y26B6GZB;
FRAMEWORK_SEARCH_PATHS = (
"$(SDKROOT)/Developer/Library/Frameworks",
"$(inherited)",
@@ -526,7 +514,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 0B7D1E6C00E83B4AF8AA1781 /* Pods-SwiftAudioPlayer_Tests.release.xcconfig */;
buildSettings = {
DEVELOPMENT_TEAM = R2392A68YQ;
DEVELOPMENT_TEAM = H9Y26B6GZB;
FRAMEWORK_SEARCH_PATHS = (
"$(SDKROOT)/Developer/Library/Frameworks",
"$(inherited)",
@@ -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="18122" 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="18093"/>
<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,134 +35,191 @@
<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"/>
<segment title="Y Combinator"/>
<segment title="Podcast"/>
<segment title="Radio"/>
</segments>
<connections>
<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" 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"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="2cn-E5-TeQ">
<rect key="frame" x="226" y="499" width="51" height="31"/>
<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" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="IGe-aU-Y6D">
<rect key="frame" x="226" y="540" width="51" height="31"/>
<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" 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"/>
<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="Loop" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JOr-pf-CKN">
<rect key="frame" x="152" y="588" width="38" height="21"/>
<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>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="cfU-Rp-Kqf">
<rect key="frame" x="226" y="583" width="51" height="31"/>
<connections>
<action selector="loopSwitched:" destination="vXZ-lx-hvc" eventType="valueChanged" id="psj-Vs-9BI"/>
</connections>
</switch>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="nsl-df-P21" firstAttribute="top" secondItem="y5i-MZ-Qat" secondAttribute="bottom" constant="8" id="0aM-Sz-J9k"/>
<constraint firstItem="lTK-Hd-Tl2" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="16" id="1wb-IW-jYz"/>
<constraint firstItem="j3w-gr-HzF" firstAttribute="leading" secondItem="lTK-Hd-Tl2" secondAttribute="leading" id="26c-ZJ-768"/>
<constraint firstItem="JOr-pf-CKN" firstAttribute="top" secondItem="vf6-kr-yWa" secondAttribute="bottom" constant="22" id="4UI-XL-M9D"/>
<constraint firstItem="jUc-tP-CC5" firstAttribute="top" secondItem="KDu-ea-kF8" secondAttribute="bottom" constant="80" id="5sT-An-9vw"/>
<constraint firstItem="6d9-Bc-hIz" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="KDu-ea-kF8" secondAttribute="trailing" constant="8" symbolic="YES" id="60t-zV-EiY"/>
<constraint firstItem="2cn-E5-TeQ" firstAttribute="centerY" secondItem="M2y-FP-H1D" secondAttribute="centerY" id="6QX-Ru-ZbO"/>
<constraint firstItem="joK-xi-MCo" firstAttribute="leading" secondItem="lTK-Hd-Tl2" secondAttribute="leading" id="7KA-Mg-HFD"/>
<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="cfU-Rp-Kqf" firstAttribute="leading" secondItem="JOr-pf-CKN" secondAttribute="trailing" constant="36" id="JxU-kl-pkL"/>
<constraint firstItem="yUQ-mI-ozK" firstAttribute="top" secondItem="w2a-RA-zmI" secondAttribute="bottom" constant="100" id="K1K-8N-SpD"/>
<constraint firstItem="IGe-aU-Y6D" firstAttribute="centerY" secondItem="vf6-kr-yWa" secondAttribute="centerY" id="K1s-td-R7b"/>
<constraint firstItem="vf6-kr-yWa" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="83" id="M0b-b2-UnQ"/>
<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="cfU-Rp-Kqf" firstAttribute="centerY" secondItem="JOr-pf-CKN" secondAttribute="centerY" id="Tox-y4-XVg"/>
<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="IGe-aU-Y6D" firstAttribute="leading" secondItem="vf6-kr-yWa" secondAttribute="trailing" constant="31" id="XpW-wP-Iyh"/>
<constraint firstItem="vf6-kr-yWa" firstAttribute="top" secondItem="M2y-FP-H1D" secondAttribute="bottom" constant="20" id="Y8L-El-ycq"/>
<constraint firstItem="nsl-df-P21" firstAttribute="leading" secondItem="vfk-OJ-S3T" secondAttribute="leading" id="a5C-nZ-8Jc"/>
<constraint firstItem="yUQ-mI-ozK" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="a66-h4-WVf"/>
<constraint firstItem="Urj-Dv-41y" firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" id="aKt-EV-Bwd"/>
<constraint firstItem="tFH-sY-Xu9" firstAttribute="top" secondItem="1IX-z5-wWx" secondAttribute="bottom" constant="27" id="bIq-V0-Sac"/>
<constraint firstItem="M2y-FP-H1D" firstAttribute="top" secondItem="vfk-OJ-S3T" secondAttribute="bottom" constant="26" id="bsl-hj-xUt"/>
<constraint firstItem="tFH-sY-Xu9" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="62.5" id="cH6-q6-Lel"/>
<constraint firstItem="yUQ-mI-ozK" firstAttribute="top" secondItem="nsl-df-P21" secondAttribute="bottom" constant="8" id="cKV-wk-6P9"/>
<constraint firstItem="jUc-tP-CC5" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="cgM-Nj-yit"/>
<constraint firstItem="JOr-pf-CKN" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="152" id="cgd-E2-XpJ"/>
<constraint firstItem="KDu-ea-kF8" firstAttribute="top" secondItem="joK-xi-MCo" secondAttribute="bottom" constant="32" id="dLw-rF-Pfb"/>
<constraint firstItem="w2a-RA-zmI" firstAttribute="leading" secondItem="lTK-Hd-Tl2" secondAttribute="leading" id="daz-b0-eCC"/>
<constraint firstItem="jUc-tP-CC5" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="tFH-sY-Xu9" secondAttribute="trailing" constant="8" symbolic="YES" id="fS9-Ce-4ph"/>
<constraint firstItem="Urj-Dv-41y" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="j3w-gr-HzF" secondAttribute="trailing" constant="8" symbolic="YES" id="fu0-ZZ-rj9"/>
<constraint firstAttribute="trailing" secondItem="lTK-Hd-Tl2" secondAttribute="trailing" constant="16" id="gdg-7Y-7la"/>
<constraint firstAttribute="trailing" secondItem="1IX-z5-wWx" secondAttribute="trailing" constant="16" id="hHM-jO-RZd"/>
<constraint firstItem="pVf-cJ-9ca" firstAttribute="centerX" secondItem="joK-xi-MCo" secondAttribute="centerX" id="lOM-Fa-KdR"/>
<constraint firstItem="2cn-E5-TeQ" firstAttribute="leading" secondItem="M2y-FP-H1D" secondAttribute="trailing" constant="36" id="laG-3h-LI7"/>
<constraint firstItem="6d9-Bc-hIz" firstAttribute="top" secondItem="joK-xi-MCo" secondAttribute="bottom" constant="32" id="m9s-An-IWV"/>
<constraint firstItem="vfk-OJ-S3T" firstAttribute="top" secondItem="yUQ-mI-ozK" secondAttribute="bottom" constant="8" id="oaW-rr-UVN"/>
<constraint firstItem="nsl-df-P21" firstAttribute="trailing" secondItem="vfk-OJ-S3T" secondAttribute="trailing" id="r5e-Wq-dqV"/>
<constraint firstItem="y5i-MZ-Qat" firstAttribute="centerX" secondItem="nsl-df-P21" secondAttribute="centerX" id="reC-GA-ZgT"/>
<constraint firstAttribute="trailing" secondItem="0QE-3F-a4G" secondAttribute="trailing" constant="62.5" id="tg1-gr-hdd"/>
<constraint firstAttribute="trailing" secondItem="6d9-Bc-hIz" secondAttribute="trailing" constant="82" id="vtN-y4-iqp"/>
<constraint firstItem="M2y-FP-H1D" firstAttribute="leading" secondItem="kh9-bI-dsS" secondAttribute="leading" constant="89" id="vcF-gP-oe0"/>
<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"/>
@@ -177,6 +232,7 @@
<outlet property="currentUrlLocationLabel" destination="1IX-z5-wWx" id="MuO-fF-ZxL"/>
<outlet property="downloadButton" destination="KDu-ea-kF8" id="5o4-1h-y06"/>
<outlet property="durationLabel" destination="Urj-Dv-41y" id="mIq-eh-int"/>
<outlet property="loopSwitch" destination="cfU-Rp-Kqf" id="wTZ-Sr-mV4"/>
<outlet property="playPauseButton" destination="jUc-tP-CC5" id="e9C-zV-A1B"/>
<outlet property="rateLabel" destination="yUQ-mI-ozK" id="Dx4-lO-A1B"/>
<outlet property="rateSlider" destination="vfk-OJ-S3T" id="mNc-ET-aNM"/>
@@ -185,6 +241,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>
+89
View File
@@ -0,0 +1,89 @@
//
// Model.swift
// SwiftAudioPlayer_Example
//
// Created by Tanha Kabir on 3/17/21.
// Copyright © 2021 CocoaPods. All rights reserved.
//
import Foundation
import SwiftAudioPlayer
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://ice6.somafm.com/groovesalad-256-mp3")!]
var url: URL {
switch index {
case 0:
return urls[0]
case 1:
return urls[1]
case 2:
return urls[2]
default:
return urls[0]
}
}
var title: String {
switch index {
case 0:
return "Soundbite"
case 1:
return "Podcast"
case 2:
return "Radio"
default:
return "Soundbite"
}
}
let artist: String = "SwiftAudioPlayer Sample App"
let releaseDate: Int = 1550790640
var lockscreenInfo: SALockScreenInfo {
get {
return SALockScreenInfo(title: self.title, artist: self.artist, albumTitle: nil, artwork: nil, releaseDate: self.releaseDate)
}
}
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)
}
}
+190 -92
View File
@@ -11,55 +11,9 @@ 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 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 selectedAudio: AudioInfo = AudioInfo(index: 0)
var freq:[Int] = [0,0,0,0,0,0,0,0,0,0]
@IBOutlet weak var currentUrlLocationLabel: UILabel!
@IBOutlet weak var bufferProgress: UIProgressView!
@IBOutlet weak var scrubberSlider: UISlider!
@@ -83,8 +37,20 @@ class ViewController: UIViewController {
var isDownloading: Bool = false
var isStreaming: Bool = false
var beingSeeked: Bool = false
var loopEnabled = 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 {
@@ -103,22 +69,78 @@ class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
SAPlayer.Downloader.allowUsingCellularData = true
SAPlayer.shared.HTTPHeaderFields = ["User-Agent": "foobar"]
// 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
subscribeToChanges()
}
func addRandomModifiers() {
let node = AVAudioUnitReverb()
SAPlayer.shared.audioModifiers.append(node)
node.wetDryMix = 300
let frequency:[Int] = [60,170,310,600,1000,3000,6000,12000,14000,16000]
let node2 = AVAudioUnitEQ(numberOfBands:frequency.count)
node2.globalGain = 1
for i in 0...(node2.bands.count-1) {
node2.bands[i].frequency = Float(frequency[i])
node2.bands[i].gain = 0
node2.bands[i].bypass = false
node2.bands[i].filterType = .parametric
}
SAPlayer.shared.audioModifiers.append(node2)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@IBAction func audioSelected(_ sender: Any) {
let selected = audioSelector.selectedSegmentIndex
selectAudio(atIndex: selected)
}
func selectAudio(atIndex i: Int) {
selectedAudio.setIndex(i)
if selectedAudio.savedUrl != nil {
downloadButton.isEnabled = true
downloadButton.setTitle("Delete downloaded", for: .normal)
streamButton.isEnabled = false
} else {
downloadButton.isEnabled = true
downloadButton.setTitle("Download", for: .normal)
streamButton.isEnabled = true
}
}
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] (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
elapsedId = SAPlayer.Updates.ElapsedTime.subscribe { [weak self] (position) in
guard let self = self else { return }
guard self.beingSeeked == false else { return }
guard url == self.selectedAudio.url || url == self.savedUrls[self.selectedAudio] else { return }
self.currentTimestampLabel.text = SAPlayer.prettifyTimestamp(position)
@@ -127,7 +149,7 @@ class ViewController: UIViewController {
self.scrubberSlider.value = Float(position/self.duration)
}
_ = SAPlayer.Updates.AudioDownloading.subscribe { [weak self] (url, progress) in
downloadId = SAPlayer.Updates.AudioDownloading.subscribe { [weak self] (url, progress) in
guard let self = self else { return }
guard url == self.selectedAudio.url else { return }
@@ -140,26 +162,24 @@ class ViewController: UIViewController {
}
}
_ = SAPlayer.Updates.StreamingBuffer.subscribe{ [weak self] (url, buffer) in
bufferId = SAPlayer.Updates.StreamingBuffer.subscribe{ [weak self] (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)
let progress = Float((buffer.totalDurationBuffered + buffer.startingBufferTimePositon) / self.duration)
self.bufferProgress.progress = progress
if progress >= 0.99 {
if buffer.bufferingProgress >= 0.99 {
self.streamButton.isEnabled = false
} else {
self.streamButton.isEnabled = true
}
self.isPlayable = buffer.isReadyForPlaying
}
_ = SAPlayer.Updates.PlayingStatus.subscribe { [weak self] (url, playing) in
playingStatusId = SAPlayer.Updates.PlayingStatus.subscribe { [weak self] (playing) in
guard let self = self else { return }
guard url == self.selectedAudio.url || url == self.savedUrls[self.selectedAudio] else { return }
self.playbackStatus = playing
switch playing {
case .playing:
@@ -174,30 +194,43 @@ class ViewController: UIViewController {
self.isPlayable = false
self.playPauseButton.setTitle("Loading", for: .normal)
return
case .ended:
if !self.loopEnabled {
self.isPlayable = false
self.playPauseButton.setTitle("Done", for: .normal)
}
return
}
}
queueId = SAPlayer.Updates.AudioQueue.subscribe { [weak self] 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)
}
self.currentUrlLocationLabel.text = "\(forthcomingPlaybackUrl.absoluteString)"
}
}
func addRandomModifiers() {
let node = AVAudioUnitReverb()
SAPlayer.shared.audioModifiers.append(node)
node.wetDryMix = 300
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
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 audioSelected(_ sender: Any) {
let selected = audioSelector.selectedSegmentIndex
selectedAudio = AudioInfo(index: selected)
SAPlayer.shared.mediaInfo = SALockScreenInfo(title: selectedAudio.title, artist: selectedAudio.artist, artwork: UIImage(), releaseDate: selectedAudio.releaseDate)
// if let savedUrl = savedUrls[selectedAudio] {}
}
@IBAction func scrubberStartedSeeking(_ sender: UISlider) {
beingSeeked = true
}
@@ -212,9 +245,11 @@ 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)
if skipSilencesSwitch.isOn {
SAPlayer.Features.SkipSilences.setRateSafely(speed) // if using Skip Silences, we need use this version of setting rate to safely change the rate with the feature enabled.
} else {
SAPlayer.shared.rate = speed
}
}
@IBAction func reverbChanged(_ sender: Any) {
@@ -224,11 +259,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
@@ -239,9 +284,7 @@ class ViewController: UIViewController {
guard let self = self else { return }
DispatchQueue.main.async {
self.currentUrlLocationLabel.text = "saved to: \(url.lastPathComponent)"
self.savedUrls[self.selectedAudio] = url
SAPlayer.shared.initializeSavedAudio(withSavedUrl: url)
self.selectedAudio.addSavedUrl(url)
}
})
streamButton.isEnabled = false
@@ -256,11 +299,22 @@ class ViewController: UIViewController {
@IBAction func streamTouched(_ sender: Any) {
if !isStreaming {
SAPlayer.shared.initializeRemoteAudio(withRemoteUrl: selectedAudio.url)
self.currentUrlLocationLabel.text = "remote url: \(selectedAudio.url.absoluteString)"
if selectedAudio.index == 2 { // radio
SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url, bitrate: .low, mediaInfo: selectedAudio.lockscreenInfo)
} else {
SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url, mediaInfo: selectedAudio.lockscreenInfo)
}
lastPlayedAudioIndex = selectedAudio.index
streamButton.setTitle("Cancel streaming", for: .normal)
downloadButton.isEnabled = false
isStreaming = true
} else {
// TODO
SAPlayer.shared.stopStreamingRemoteAudio()
streamButton.setTitle("Stream", for: .normal)
downloadButton.isEnabled = true
isStreaming = false
}
}
@@ -275,6 +329,50 @@ class ViewController: UIViewController {
@IBAction func skipForwardTouched(_ sender: Any) {
SAPlayer.shared.skipForward()
}
@IBAction func setEqualizerValue(_ sender: Any) {
if let slider = sender as? UISlider{
print("slider of index:", slider.tag, "is changed to", slider.value)
freq[slider.tag] = Int(slider.value)
print("current frequency : ",freq)
if let node = SAPlayer.shared.audioModifiers[2] as? AVAudioUnitEQ{
for i in 0...(node.bands.count - 1){
node.bands[i].gain = Float(freq[i])
}
}
}
}
@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()
}
}
@IBOutlet weak var loopSwitch: UISwitch!
@IBAction func loopSwitched(_ sender: Any) {
loopEnabled = loopSwitch.isOn
if loopSwitch.isOn {
SAPlayer.Features.Loop.enable()
} else {
SAPlayer.Features.Loop.disable()
}
}
}
+30
View File
@@ -0,0 +1,30 @@
// swift-tools-version:5.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "SwiftAudioPlayer",
platforms: [
.iOS(.v10), .tvOS(.v10)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "SwiftAudioPlayer",
targets: ["SwiftAudioPlayer"])
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "SwiftAudioPlayer",
path: "Source"
)
],
swiftLanguageVersions: [.v5]
)
+67 -25
View File
@@ -10,6 +10,23 @@ 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. Stream radio
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
1. Loop audio playback for both streamed and saved audio
### Requirements
iOS 10.0 and higher.
@@ -19,9 +36,9 @@ iOS 10.0 and higher.
### Running the Example Project
1. Clone repo
2. CD to directory
3. run `pod install` in terminal
4. Run
2. CD to the `Example` folder where the Example app lives
3. Run `pod install` in terminal
4. Build and run
### Installation
@@ -34,12 +51,17 @@ pod 'SwiftAudioPlayer'
### Usage
Import the player at the top:
```swift
import SwiftAudioPlayer
```
**Important:** For app in background downloading please refer to [note](#important-step-for-background-downloads).
To play remote audio:
```swift
let url = URL(string: "https://randomwebsite.com/audio.mp3")!
SAPlayer.shared.initializeAudio(withRemoteUrl: url)
SAPlayer.shared.startRemoteAudio(withRemoteUrl: url)
SAPlayer.shared.play()
```
@@ -56,19 +78,16 @@ To receive streaming progress (for buffer progress %):
override func viewDidLoad() {
super.viewDidLoad()
_ = SAPlayer.Updates.StreamingBuffer.subscribe{ [weak self] (url, buffer) in
_ = SAPlayer.Updates.StreamingBuffer.subscribe{ [weak self] buffer in
guard let self = self else { return }
guard url == self.selectedAudioUrl else { return }
let progress = Float((buffer.totalDurationBuffered + buffer.startingBufferTimePositon) / self.duration)
self.bufferProgress.progress = progress
self.bufferProgress.progress = Float(buffer.bufferingProgress)
self.isPlayable = buffer.isReadyForPlaying
}
}
```
Look at the [Updates](#SAPlayer.Updates) section to see usage details and other updates to follow.
Look at the [Updates](#saplayerupdates) section to see usage details and other updates to follow.
For realtime audio manipulations, [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/avaudiounit) nodes are used. For example to adjust the reverb through a slider in the UI:
@@ -93,18 +112,12 @@ For a more detailed explanation on usage, look at the [Realtime Audio Manipulati
For more details and specifics look at the [API documentation](#api-in-detail) below.
## Contact
### Issues
### Issues or questions
Submit any issues or requests [on the Github repo](https://github.com/tanhakabir/SwiftAudioPlayer/issues).
### Any questions?
Feel free to reach out to either of us:
[tanhakabir](https://github.com/tanhakabir), tanhakabir.ca@gmail.com
[JonMercer](https://github.com/JonMercer), mercer.jon@gmail.com
Submit any issues, requests, and questions [on the Github repo](https://github.com/tanhakabir/SwiftAudioPlayer/issues).
### License
@@ -118,13 +131,17 @@ SwiftAudioPlayer is available under the MIT license. See the LICENSE file for mo
Access the player and all of its fields and functions through `SAPlayer.shared`.
### Supported file types
Known supported file types are `.mp3` and `.wav`.
### Playing Audio (Basic Commands)
To set up player with audio to play, use either:
* `initializeSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo?)` to play audio that is saved on the device.
* `initializeRemoteAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo?)` to play audio streamed from a remote location.
* `startSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo?)` to play audio that is saved on the device.
* `startRemoteAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate, mediaInfo: SALockScreenInfo?)` to play audio streamed from a remote location.
Both of these expect a URL of the location of the audio and an optional media information to display on the lockscreen.
Both of these expect a URL of the location of the audio and an optional media information to display on the lockscreen. For streamed audio you can optionally set the bitrate to be `.high` or `.low`. High is more performant but won't work well for radio streams; for radio streams you should use low. The default bitrate if you don't set it is `.high`.
For streaming remote audio, subscribe to `SAPlayer.Updates.StreamingBuffer` for updates on streaming progress.
@@ -138,6 +155,18 @@ 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)
```
You can also directly access and modify the queue from `SAPlayer.shared.audioQueued`.
#### 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.
@@ -194,6 +223,12 @@ And use the following to stop any active or prevent future downloads of the corr
func cancelDownload(withRemoteUrl url: URL)
```
By default downloading will be allowed on cellular data. If you would like to turn this off set:
```swift
SAPlayer.Downloader.allowUsingCellularData = false
```
You can also retrieve what preference you have set for cellular downloads through `allowUsingCellularData`.
### Manage Downloaded
Use the following to manage downloaded audio files.
@@ -221,14 +256,16 @@ Receive updates for changing values from the player, such as the duration, elaps
All subscription functions for updates take the form of:
```swift
func subscribe(_ closure: @escaping (_ url: URL, _ payload: <Payload>) -> ()) -> UInt
func subscribe(_ closure: @escaping (_ payload: <Payload>) -> ()) -> UInt
```
- `closure`: The closure that will receive the updates. It's recommended to have a weak reference to a class that uses these functions.
- `url`: The corresponding remote URL for the update. In the case there might be multiple files observed, such as downloading many files at once or switching over from playing one audio to another and the updates corresponding to the previous aren't silenced on switch-over.
- `payload`: The updated value.
- Returns: the id for the subscription in the case you would like to unsubscribe to updates for the closure.
Sometimes there is:
- `url`: The corresponding remote URL for the update. In the case there might be multiple files observed, such as downloading many files at once.
Similarily unsubscribe takes the form of:
```swift
func unsubscribe(_ id: UInt)
@@ -252,7 +289,7 @@ Changes in the duration of the current initialized audio. Especially helpful for
### PlayingStatus
Payload = `SAPlayingStatus`
Changes in the playing status of the player. Can be one of the following 3: `playing`, `paused`, `buffering`.
Changes in the playing status of the player. Can be one of the following 4: `playing`, `paused`, `buffering`, `ended` (audio ended).
### StreamingBuffer
Payload = `SAAudioAvailabilityRange`
@@ -266,6 +303,11 @@ Payload = `Double`
Changes in the progress of downloading audio in the background. This does not correspond to progress in streaming downloads, look at StreamingBuffer for streaming progress.
### AudioQueue
Payload = `URL`
Notification of the URL of the upcoming audio to be played. This URL may be remote or locally saved.
## Audio Effects
### Realtime Audio Manipulation
@@ -28,6 +28,12 @@ import CoreMedia
class AudioClockDirector {
static let shared = AudioClockDirector()
private var currentAudioKey: Key?
private var depNeedleClosures: DirectorThreadSafeClosuresDeprecated<Needle> = DirectorThreadSafeClosuresDeprecated()
private var depDurationClosures: DirectorThreadSafeClosuresDeprecated<Duration> = DirectorThreadSafeClosuresDeprecated()
private var depPlayingStatusClosures: DirectorThreadSafeClosuresDeprecated<SAPlayingStatus> = DirectorThreadSafeClosuresDeprecated()
private var depBufferClosures: DirectorThreadSafeClosuresDeprecated<SAAudioAvailabilityRange> = DirectorThreadSafeClosuresDeprecated()
private var needleClosures: DirectorThreadSafeClosures<Needle> = DirectorThreadSafeClosures()
private var durationClosures: DirectorThreadSafeClosures<Duration> = DirectorThreadSafeClosures()
@@ -36,9 +42,23 @@ class AudioClockDirector {
private init() {}
func create() {}
func setKey(_ key: Key) {
currentAudioKey = key
}
func resetCache() {
needleClosures.resetCache()
durationClosures.resetCache()
playingStatusClosures.resetCache()
bufferClosures.resetCache()
}
func clear() {
depNeedleClosures.clear()
depDurationClosures.clear()
depPlayingStatusClosures.clear()
depBufferClosures.clear()
needleClosures.clear()
durationClosures.clear()
playingStatusClosures.clear()
@@ -48,43 +68,67 @@ class AudioClockDirector {
// MARK: - Attaches
// Needle
@available(*, deprecated, message: "Use subscribe without key in the closure for current audio updates")
func attachToChangesInNeedle(closure: @escaping (Key, Needle) throws -> Void) -> UInt {
return depNeedleClosures.attach(closure: closure)
}
func attachToChangesInNeedle(closure: @escaping (Needle) throws -> Void) -> UInt {
return needleClosures.attach(closure: closure)
}
// Duration
@available(*, deprecated, message: "Use subscribe without key in the closure for current audio updates")
func attachToChangesInDuration(closure: @escaping (Key, Duration) throws -> Void) -> UInt {
return depDurationClosures.attach(closure: closure)
}
func attachToChangesInDuration(closure: @escaping (Duration) throws -> Void) -> UInt {
return durationClosures.attach(closure: closure)
}
// Playing status
@available(*, deprecated, message: "Use subscribe without key in the closure for current audio updates")
func attachToChangesInPlayingStatus(closure: @escaping (Key, SAPlayingStatus) throws -> Void) -> UInt{
return depPlayingStatusClosures.attach(closure: closure)
}
func attachToChangesInPlayingStatus(closure: @escaping (SAPlayingStatus) throws -> Void) -> UInt{
return playingStatusClosures.attach(closure: closure)
}
// Buffer
@available(*, deprecated, message: "Use subscribe without key in the closure for current audio updates")
func attachToChangesInBufferedRange(closure: @escaping (Key, SAAudioAvailabilityRange) throws -> Void) -> UInt{
return depBufferClosures.attach(closure: closure)
}
func attachToChangesInBufferedRange(closure: @escaping (SAAudioAvailabilityRange) throws -> Void) -> UInt{
return bufferClosures.attach(closure: closure)
}
// MARK: - Detaches
func detachFromChangesInNeedle(withID id: UInt) {
depNeedleClosures.detach(id: id)
needleClosures.detach(id: id)
}
func detachFromChangesInDuration(withID id: UInt) {
depDurationClosures.detach(id: id)
durationClosures.detach(id: id)
}
func detachFromChangesInPlayingStatus(withID id: UInt) {
depPlayingStatusClosures.detach(id: id)
playingStatusClosures.detach(id: id)
}
func detachFromChangesInBufferedRange(withID id: UInt) {
depBufferClosures.detach(id: id)
bufferClosures.detach(id: id)
}
}
@@ -92,24 +136,44 @@ class AudioClockDirector {
// MARK: - Receives notifications from AudioEngine on ticks
extension AudioClockDirector {
func needleTick(_ key: Key, needle: Needle) {
needleClosures.broadcast(key: key, payload: needle)
guard key == currentAudioKey else {
Log.debug("silence old updates")
return
}
depNeedleClosures.broadcast(key: key, payload: needle)
needleClosures.broadcast(payload: needle)
}
}
extension AudioClockDirector {
func durationWasChanged(_ key: Key, duration: Duration) {
durationClosures.broadcast(key: key, payload: duration)
guard key == currentAudioKey else {
Log.debug("silence old updates")
return
}
depDurationClosures.broadcast(key: key, payload: duration)
durationClosures.broadcast(payload: duration)
}
}
extension AudioClockDirector {
func audioPlayingStatusWasChanged(_ key: Key, status: SAPlayingStatus) {
playingStatusClosures.broadcast(key: key, payload: status)
guard key == currentAudioKey else {
Log.debug("silence old updates")
return
}
depPlayingStatusClosures.broadcast(key: key, payload: status)
playingStatusClosures.broadcast(payload: status)
}
}
extension AudioClockDirector {
func changeInAudioBuffered(_ key: Key, buffered: SAAudioAvailabilityRange) {
bufferClosures.broadcast(key: key, payload: buffered)
guard key == currentAudioKey else {
Log.debug("silence old updates")
return
}
depBufferClosures.broadcast(key: key, payload: buffered)
bufferClosures.broadcast(payload: buffered)
}
}
+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 (URL) throws -> Void) -> UInt {
return closures.attach(closure: closure)
}
func detach(withID id: UInt) {
closures.detach(id: id)
}
func changeInQueue(url: URL) {
closures.broadcast(payload: url)
}
}
@@ -28,7 +28,7 @@ import Foundation
class DownloadProgressDirector {
static let shared = DownloadProgressDirector()
var closures: DirectorThreadSafeClosures<Double> = DirectorThreadSafeClosures()
var closures: DirectorThreadSafeClosuresDeprecated<Double> = DirectorThreadSafeClosuresDeprecated()
private init() {
AudioDataManager.shared.attach { [weak self] (key, progress) in
@@ -0,0 +1,65 @@
//
// StreamingDownloadDirector.swift
// SwiftAudioPlayer
//
// Created by Tanha Kabir on 4/16/21.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
class StreamingDownloadDirector {
static let shared = StreamingDownloadDirector()
private var currentAudioKey: Key?
var closures: DirectorThreadSafeClosures<Double> = DirectorThreadSafeClosures()
private init() {}
func setKey(_ key: Key) {
currentAudioKey = key
}
func resetCache() {
closures.resetCache()
}
func clear() {
closures.clear()
}
func attach(closure: @escaping (Double) throws -> Void) -> UInt {
return closures.attach(closure: closure)
}
func detach(withID id: UInt) {
closures.detach(id: id)
}
}
extension StreamingDownloadDirector {
func didUpdate(_ key: Key, networkStreamProgress: Double) {
guard key == currentAudioKey else {
Log.debug("silence old updates")
return
}
closures.broadcast(payload: networkStreamProgress)
}
}
+11 -10
View File
@@ -64,17 +64,16 @@ class AudioDiskEngine: AudioEngine {
audioSampleRate = Float(audioFormat?.sampleRate ?? 44100)
audioLengthSeconds = Float(audioLengthSamples) / audioSampleRate
duration = Duration(audioLengthSeconds)
bufferedSeconds = SAAudioAvailabilityRange(startingNeedle: 0, durationLoadedByNetwork: duration, isPlayable: true)
bufferedSeconds = SAAudioAvailabilityRange(startingNeedle: 0, durationLoadedByNetwork: duration, predictedDurationToLoad: duration, isPlayable: true)
} else {
Log.monitor("Could not load downloaded file with url: \(url)")
}
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 }
self.updateIsPlaying()
self.updateNeedle()
}
scheduleAudioFile()
@@ -98,7 +97,7 @@ class AudioDiskEngine: AudioEngine {
if state == .resumed {
state = .suspended
}
delegate?.didEndPlaying()
playingStatus = .ended
}
guard audioSampleRate != 0 else {
@@ -116,10 +115,11 @@ class AudioDiskEngine: AudioEngine {
}
let playing = playerNode.isPlaying
let seekToNeedle = needle > Needle(duration) ? Needle(duration) : needle
self.needle = needle // to tick while paused
self.needle = seekToNeedle // to tick while paused
seekFrame = AVAudioFramePosition(Float(needle) * audioSampleRate)
seekFrame = AVAudioFramePosition(Float(seekToNeedle) * audioSampleRate)
seekFrame = max(seekFrame, 0)
seekFrame = min(seekFrame, audioLengthSamples)
currentPosition = seekFrame
@@ -136,6 +136,7 @@ class AudioDiskEngine: AudioEngine {
}
override func invalidate() {
super.invalidate()
//Nothing to invalidate for disk
}
}
+88 -39
View File
@@ -27,6 +27,8 @@ import Foundation
import AVFoundation
protocol AudioEngineProtocol {
var key: Key { get }
var engine: AVAudioEngine! { get }
func play()
func pause()
func seek(toNeedle needle: Needle)
@@ -34,18 +36,16 @@ 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
let engine = AVAudioEngine()
let playerNode = AVAudioPlayerNode()
var timer: Timer?
var engine: AVAudioEngine!
var playerNode: AVAudioPlayerNode!
private var engineInvalidated: Bool = false
static let defaultEngineAudioFormat: AVAudioFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 2, interleaved: false)!
@@ -76,13 +76,13 @@ class AudioEngine: AudioEngineProtocol {
guard playingStatus != oldValue, let status = playingStatus else {
return
}
AudioClockDirector.shared.audioPlayingStatusWasChanged(key, status: status)
}
}
var bufferedSecondsDebouncer: SAAudioAvailabilityRange = SAAudioAvailabilityRange(startingNeedle: 0.0, durationLoadedByNetwork: 0.0, isPlayable: false)
var bufferedSeconds: SAAudioAvailabilityRange = SAAudioAvailabilityRange(startingNeedle: 0.0, durationLoadedByNetwork: 0.0, isPlayable: false) {
var bufferedSecondsDebouncer: SAAudioAvailabilityRange = SAAudioAvailabilityRange(startingNeedle: 0.0, durationLoadedByNetwork: 0.0, predictedDurationToLoad: Double.greatestFiniteMagnitude, isPlayable: false)
var bufferedSeconds: SAAudioAvailabilityRange = SAAudioAvailabilityRange(startingNeedle: 0.0, durationLoadedByNetwork: 0.0, predictedDurationToLoad: Double.greatestFiniteMagnitude, isPlayable: false) {
didSet {
if bufferedSeconds.startingNeedle == 0.0 && bufferedSeconds.durationLoadedByNetwork == 0.0 {
bufferedSecondsDebouncer = bufferedSeconds
@@ -103,53 +103,95 @@ class AudioEngine: AudioEngineProtocol {
AudioClockDirector.shared.changeInAudioBuffered(key, buffered: bufferedSeconds)
}
}
private var audioModifiers: [AVAudioUnit]?
init(url: AudioURL, delegate:AudioEngineDelegate?, engineAudioFormat: AVAudioFormat) {
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 {
engine.attach(node)
}
if SAPlayer.shared.audioModifiers.count > 0 {
var i = 0
let node = SAPlayer.shared.audioModifiers[i]
engine.connect(playerNode, to: node, format: engineAudioFormat)
i += 1
while i < SAPlayer.shared.audioModifiers.count {
let lastNode = SAPlayer.shared.audioModifiers[i - 1]
let currNode = SAPlayer.shared.audioModifiers[i]
engine.connect(lastNode, to: currNode, format: engineAudioFormat)
i += 1
}
let finalNode = SAPlayer.shared.audioModifiers[SAPlayer.shared.audioModifiers.count - 1]
engine.connect(finalNode, to: engine.mainMixerNode, format: engineAudioFormat)
} else {
audioModifiers = SAPlayer.shared.audioModifiers
defer { engine.prepare() }
guard let audioModifiers = audioModifiers, audioModifiers.count > 0 else {
engine.connect(playerNode, to: engine.mainMixerNode, format: engineAudioFormat)
return
}
engine.prepare()
audioModifiers.forEach { engine.attach($0) }
var i = 0
let node = audioModifiers[i]
engine.connect(playerNode, to: node, format: engineAudioFormat)
i += 1
while i < audioModifiers.count {
let lastNode = audioModifiers[i - 1]
let currNode = audioModifiers[i]
engine.connect(lastNode, to: currNode, format: engineAudioFormat)
i += 1
}
let finalNode = audioModifiers[audioModifiers.count - 1]
engine.connect(finalNode, to: engine.mainMixerNode, format: engineAudioFormat)
}
deinit {
timer?.invalidate()
if state == .resumed {
playerNode.stop()
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.engineInvalidated else {
self.delegate = nil
return
}
closure()
self.doRepeatedly(timeInterval: timeInterval, closure)
}
}
func updateIsPlaying() {
if !bufferedSeconds.isPlayable {
playingStatus = .buffering
if bufferedSeconds.reachedEndOfAudio(needle: needle) {
playingStatus = .ended
} else {
playingStatus = .buffering
}
return
}
@@ -159,7 +201,7 @@ class AudioEngine: AudioEngineProtocol {
func play() {
// https://stackoverflow.com/questions/36754934/update-mpremotecommandcenter-play-pause-button
if !engine.isRunning {
if !(engine.isRunning) {
do {
try engine.start()
@@ -190,6 +232,13 @@ class AudioEngine: AudioEngineProtocol {
}
func invalidate() {
engineInvalidated = true
playerNode.stop()
engine.stop()
if let audioModifiers = audioModifiers, audioModifiers.count > 0 {
audioModifiers.forEach { engine.detach($0) }
}
Log.info("invalidated engine for key \(key)")
}
}
+103 -49
View File
@@ -59,26 +59,33 @@ class AudioStreamEngine: AudioEngine {
//Constants
private let MAX_POLL_BUFFER_COUNT = 300 //Having one buffer in engine at a time is choppy.
private let MIN_BUFFERS_TO_BE_PLAYABLE = 1
private let PCM_BUFFER_SIZE: AVAudioFrameCount = 8192
private var 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!
//Fields
private var currentTimeOffset: TimeInterval = 0
private var streamChangeListenerId: UInt?
private var numberOfBuffersScheduledInTotal = 0 {
didSet {
Log.debug("number of buffers scheduled in total: \(numberOfBuffersScheduledInTotal)")
if numberOfBuffersScheduledInTotal == 0 {
if playingStatus == .playing { wasPlaying = true }
pause()
// delegate?.didError()
// TODO: we should not have an error here. We should instead have the throttler
// propegate when it doesn't enough buffers while they were playing
// TODO: "Make this a legitimate warning to user about needing more data from stream"
}
if numberOfBuffersScheduledInTotal > MIN_BUFFERS_TO_BE_PLAYABLE && wasPlaying {
wasPlaying = false
play()
}
}
}
@@ -133,28 +140,57 @@ class AudioStreamEngine: AudioEngine {
}
}
init(withRemoteUrl url: AudioURL, delegate:AudioEngineDelegate?) {
init(withRemoteUrl url: AudioURL, delegate:AudioEngineDelegate?, bitrate: SAPlayerBitrate) {
Log.info(url)
super.init(url: url, delegate: delegate, engineAudioFormat: AudioEngine.defaultEngineAudioFormat)
switch bitrate {
case .high:
PCM_BUFFER_SIZE = 8192
case .low:
PCM_BUFFER_SIZE = 4096
}
do {
converter = try AudioConverter(withRemoteUrl: url, toEngineAudioFormat: AudioEngine.defaultEngineAudioFormat)
converter = try AudioConverter(withRemoteUrl: url, toEngineAudioFormat: AudioEngine.defaultEngineAudioFormat, withPCMBufferSize: PCM_BUFFER_SIZE)
} catch {
delegate?.didError()
}
StreamingDownloadDirector.shared.setKey(key)
StreamingDownloadDirector.shared.resetCache()
streamChangeListenerId = StreamingDownloadDirector.shared.attach { [weak self] (progress) in
guard let self = self else { return }
// polling for buffers when we receive data. This won't be throttled on fresh new audio or seeked audio but in all other cases it most likely will be throttled
self.pollForNextBuffer() // no buffer updates because thread issues if I try to update buffer status in streaming listener
}
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 }
self.repeatedUpdates()
}
}
deinit {
if let id = streamChangeListenerId {
StreamingDownloadDirector.shared.detach(withID: id)
}
}
private func repeatedUpdates() {
self.pollForNextBuffer()
self.updateNetworkBufferRange() // thread issues if I try to update buffer status in streaming listener
self.updateNeedle()
self.updateIsPlaying()
self.updateDuration()
}
//MARK:- Timer loop
//Called when
@@ -163,16 +199,30 @@ class AudioStreamEngine: AudioEngine {
private func pollForNextBuffer() {
guard shouldPollForNextBuffer else { return }
pollForNextBufferRecursive()
}
private func pollForNextBufferRecursive() {
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, tvOS 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: .dataConsumed, completionHandler: { (_) in
nextScheduledBuffer = nil
self?.numberOfBuffersScheduledInTotal -= 1
self?.pollForNextBufferRecursive()
})
} else {
self?.playerNode.scheduleBuffer(nextScheduledBuffer) {
nextScheduledBuffer = nil
self?.numberOfBuffersScheduledInTotal -= 1
self?.pollForNextBufferRecursive()
}
}
}
@@ -188,37 +238,11 @@ 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
Log.debug("loaded \(range), numberOfBuffersScheduledInTotal: \(numberOfBuffersScheduledInTotal), isPlayable: \(isPlayable)")
bufferedSeconds = SAAudioAvailabilityRange(startingNeedle: range.0, durationLoadedByNetwork: range.1, isPlayable: isPlayable)
bufferedSeconds = SAAudioAvailabilityRange(startingNeedle: range.0, durationLoadedByNetwork: range.1, predictedDurationToLoad: predictedStreamDuration, isPlayable: isPlayable)
}
private func updateNeedle() {
@@ -236,12 +260,6 @@ class AudioStreamEngine: AudioEngine {
var currentTime = TimeInterval(playerTime.sampleTime) / playerTime.sampleRate
currentTime = currentTime > 0 ? currentTime : 0
if currentTime > predictedStreamDuration {
Log.info("reached end of audio")
seek(toNeedle: 0)
pause()
delegate?.didEndPlaying()
}
needle = (currentTime + currentTimeOffset)
}
@@ -255,6 +273,15 @@ class AudioStreamEngine: AudioEngine {
//MARK:- Overriden From Parent
override func seek(toNeedle needle: Needle) {
Log.info("didSeek to needle: \(needle)")
// if not playable (data not loaded etc), duration could be zero.
guard isPlayable else {
if predictedStreamDuration == 0 {
seekNeedleCommandBeforeEngineWasReady = needle
}
return
}
guard needle < (ceil(predictedStreamDuration)) else {
if !isPlayable {
seekNeedleCommandBeforeEngineWasReady = needle
@@ -296,7 +323,34 @@ class AudioStreamEngine: AudioEngine {
updateNetworkBufferRange()
}
override func pause() {
queue.async { [weak self] in
self?.pauseHelperDispatchQueue()
}
}
private func pauseHelperDispatchQueue() {
super.pause()
}
override func play() {
queue.async { [weak self] in
self?.playHelperDispatchQueue()
}
}
private func playHelperDispatchQueue() {
super.play()
}
override func invalidate() {
converter.invalidate()
queue.sync { [weak self] in
self?.invalidateHelperDispatchQueue()
self?.converter.invalidate()
}
}
private func invalidateHelperDispatchQueue() {
super.invalidate()
}
}
+90 -150
View File
@@ -27,77 +27,41 @@ import Foundation
protocol AudioThrottleDelegate: AnyObject {
func didUpdate(totalBytesExpected bytes: Int64)
func didUpdate(networkStreamProgress progress: Double)
func shouldProcess(networkData data: Data)
}
protocol AudioThrottleable {
init(withRemoteUrl url: AudioURL, withDelegate delegate: AudioThrottleDelegate)
func tellAudioFormatFound()
func tellByteOffset(offset: UInt64)
func pullNextDataPacket(_ callback: @escaping (Data?) -> ())
func tellSeek(offset: UInt64)
func tellBytesPerAudioPacket(count: UInt64)
func pollRangeOfBytesAvailable() -> (UInt64, UInt64)
func invalidate()
}
class AudioThrottler: AudioThrottleable {
private class NetworkDataWrapper: NSObject {
let startOffset: UInt
var data: Data
var alreadySent: Bool
var next: NetworkDataWrapper?
var byteCount: UInt {
return UInt(data.count)
}
var endOffset: UInt {
return startOffset + UInt(data.count) - 1
}
init(startingOffset: UInt, data: Data) {
self.startOffset = startingOffset
self.data = data
self.alreadySent = false
}
func containsOffset(_ offset: UInt) -> Bool {
return startOffset <= offset && offset <= endOffset
}
func isNextSent() -> Bool {
return next?.alreadySent ?? false
}
//FIXME: what is the offset was at the edge of the split? We will have empty data
func splitToRight(atOffset offset: UInt) -> NetworkDataWrapper {
let splitPoint:Int = Int(offset - startOffset)
let leftData = data.subdata(in: 0..<splitPoint)
let rightData = data.subdata(in: splitPoint..<data.count)
data = leftData
let rightWrapper:NetworkDataWrapper = NetworkDataWrapper(startingOffset: offset, data: rightData)
rightWrapper.next = next
next = rightWrapper
return rightWrapper
}
override var description: String {
return "startOffset:\(startOffset), endOffset:\(endOffset), dataCount:\(data.count), sent:\(alreadySent), next:\(next != nil ?"hasNext":"noNext")"
}
}
private let queue = DispatchQueue(label: "SwiftAudioPlayer.Throttler", qos: .userInitiated)
//Init
let url: AudioURL
weak var delegate: AudioThrottleDelegate?
private var networkData: [NetworkDataWrapper] = []
private var networkData: [Data] = [] {
didSet {
// Log.test("NETWORK DATA \(networkData.count)")
}
}
private var lastSentDataPacketIndex = -1
var shouldThrottle = false
var byteOffsetBecauseOfSeek: UInt = 0
var totalBytesExpected: Int64? //this got sent up twice. Once at beginning of stream and second from network seek. We honor the first send
//This will be sent once at beginning of stream and every network seek
var totalBytesExpected: Int64? {
didSet {
if let bytes = totalBytesExpected {
delegate?.didUpdate(totalBytesExpected: Int64(byteOffsetBecauseOfSeek) + bytes)
}
}
}
var largestPollingOffsetDifference: UInt64 = 1
@@ -108,128 +72,104 @@ class AudioThrottler: AudioThrottleable {
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())")
self.delegate?.didUpdate(networkStreamProgress: pto.getProgress())
if self.totalBytesExpected == nil, let totalBytesExpected = pto.getTotalBytesExpected() {
if let totalBytesExpected = pto.getTotalBytesExpected() {
self.totalBytesExpected = totalBytesExpected
self.delegate?.didUpdate(totalBytesExpected: totalBytesExpected)
}
let lastItem = self.networkData.last
let startoffset = lastItem == nil ? self.byteOffsetBecauseOfSeek : lastItem!.endOffset + 1
let wrappedNetworkData = NetworkDataWrapper(startingOffset: startoffset, data: pto.getData())
lastItem?.next = wrappedNetworkData
self.networkData.append(wrappedNetworkData)
if !self.shouldThrottle {
Log.debug("sending up packet from stream untrottled at start: \(wrappedNetworkData.startOffset)")
//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
//We got in a state where 10% of the time an episode will keep polling because
//the first 30 buffers have not been filled
wrappedNetworkData.alreadySent = true
delegate.shouldProcess(networkData: wrappedNetworkData.data)
}
}
}
func tellAudioFormatFound() {
shouldThrottle = true //the above layer has enough info that we can throttle
}
func tellBytesPerAudioPacket(count: UInt64) {
if count > largestPollingOffsetDifference {
largestPollingOffsetDifference = count
}
}
func tellByteOffset(offset: UInt64) {
Log.debug("offset \(offset)")
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())")
if wrappedNetworkData.alreadySent {
if !wrappedNetworkData.isNextSent() {
var bytesSent: UInt = 0
var current = wrappedNetworkData
// Sometimes the next data packet is smaller than a full audio chunk size, so we need to ensure we send up enough packets for the audio chunk. This prevented Issue #4 where tsreaming would randomly get stuck in a state needing more data up the chain.
// https://github.com/tanhakabir/SwiftAudioPlayer/issues/4
while bytesSent < largestPollingOffsetDifference {
if let next = current.next {
Log.debug("Sending next network packet with range: \(next.startOffset) to \(next.endOffset)")
next.alreadySent = true
delegate?.shouldProcess(networkData: next.data)
bytesSent += next.byteCount
current = next
} else {
return
}
}
}
return
}
Log.debug("Found network packet to send with range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)")
delegate?.shouldProcess(networkData: wrappedNetworkData.data)
wrappedNetworkData.alreadySent = true
return
self.queue.async { [weak self] in
self?.networkData.append(pto.getData())
StreamingDownloadDirector.shared.didUpdate(url.key, networkStreamProgress: pto.getProgress())
}
}
}
func tellSeek(offset: UInt64) {
Log.info("seek with offset: \(offset)")
self.queue.async { [weak self] in
self?.seekQueueHelper(offset)
}
}
func seekQueueHelper(_ offset: UInt64) {
let offsetToFind = Int(offset) - Int(byteOffsetBecauseOfSeek)
var shouldStartNewStream: Bool = false
// if we have no data start a new stream after seek
if networkData.count == 0 {
shouldStartNewStream = true
}
// if what we're looking for is outside of available data, start a new stream
if offset < byteOffsetBecauseOfSeek || offsetToFind > networkData.sum {
shouldStartNewStream = true
}
// we should have the data within our cache. find it and save the index for the next pull
if let indexOfDataContainingOffset = networkData.getIndexContainingByteOffset(offsetToFind) {
lastSentDataPacketIndex = indexOfDataContainingOffset - 1
}
if shouldStartNewStream {
byteOffsetBecauseOfSeek = UInt(offset)
lastSentDataPacketIndex = -1
AudioDataManager.shared.seekStream(withRemoteURL: url, toByteOffset: offset)
networkData = []
return
}
if let finalOffset = networkData.last?.endOffset, let firstOffset = networkData.first?.startOffset {
if offset < firstOffset || offset > finalOffset {
byteOffsetBecauseOfSeek = UInt(offset)
AudioDataManager.shared.seekStream(withRemoteURL: url, toByteOffset: offset)
networkData = []
return
}
}
for (i, d) in networkData.enumerated() {
if offset > d.endOffset {
d.alreadySent = false
continue
}
if d.containsOffset(UInt(offset)) {
let wrappedData = d.splitToRight(atOffset: UInt(offset))
networkData.insert(wrappedData, at: i+1)
d.alreadySent = false
wrappedData.alreadySent = true
Log.info("\(d) ::: \(wrappedData)")
delegate?.shouldProcess(networkData: wrappedData.data)
return
}
}
Log.error("83672 Should not get here")
}
func pollRangeOfBytesAvailable() -> (UInt64, UInt64) {
let start = networkData.first?.startOffset ?? 0
let end = networkData.last?.endOffset ?? 0
let start = byteOffsetBecauseOfSeek
let end = networkData.sum + Int(byteOffsetBecauseOfSeek)
return (UInt64(start), UInt64(end))
}
func pullNextDataPacket(_ callback: @escaping (Data?) -> ()) {
queue.async { [weak self] in
guard let self = self else { return }
guard self.lastSentDataPacketIndex < self.networkData.count - 1 else {
callback(nil)
return
}
self.lastSentDataPacketIndex += 1
callback(self.networkData[self.lastSentDataPacketIndex])
}
}
func invalidate() {
AudioDataManager.shared.deleteStream(withRemoteURL: url)
}
}
extension Array where Element == Data {
var sum: Int {
get {
guard count > 0 else { return 0 }
return self.reduce(0) { $0 + $1.count }
}
}
func getIndexContainingByteOffset(_ offset: Int) -> Int? {
var dataCount = 0
for (i, data) in self.enumerated() {
if offset >= dataCount && offset <= dataCount + data.count {
return i
}
dataCount += data.count
}
return nil
}
}
+15 -8
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,15 +70,22 @@ 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: {
parser = try AudioParser(withRemoteUrl: url, bufferSize: Int(size), parsedFileAudioFormatCallback: {
[weak self] (fileAudioFormat: AVAudioFormat) in
guard let strongSelf = self else { return }
@@ -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)
@@ -57,28 +57,39 @@ public enum ConverterError: LocalizedError {
public var errorDescription: String? {
switch self {
case .cannotLockQueue:
Log.warn("Failed to lock queue")
return "Failed to lock queue"
case .converterFailed(let status):
Log.warn(localizedDescriptionFromConverterError(status))
return localizedDescriptionFromConverterError(status)
case .failedToCreateDestinationFormat:
Log.warn("Failed to create a destination (processing) format")
return "Failed to create a destination (processing) format"
case .failedToCreatePCMBuffer:
Log.warn("Failed to create PCM buffer for reading data")
return "Failed to create PCM buffer for reading data"
case .notEnoughData:
Log.warn("Not enough data for read-conversion operation")
return "Not enough data for read-conversion operation"
case .parserMissingDataFormat:
Log.warn("Parser is missing a valid data format")
return "Parser is missing a valid data format"
case .reachedEndOfFile:
Log.warn("Reached the end of the file")
return "Reached the end of the file"
case .unableToCreateConverter(let status):
return localizedDescriptionFromConverterError(status)
case .superConcerningShouldNeverHappen:
Log.warn("Weird unexpected reader error. Should not have happened")
return "Weird unexpected reader error. Should not have happened"
case .cannotCreatePCMBufferWithoutConverter:
Log.debug("Could not create a PCM Buffer because reader does not have a converter yet")
return "Could not create a PCM Buffer because reader does not have a converter yet"
case .throttleParsingBuffersForEngine:
Log.warn("Preventing the reader from creating more PCM buffers since the player has more than 60 seconds of audio already to play")
return "Preventing the reader from creating more PCM buffers since the player has more than 60 seconds of audio already to play"
case .failedToCreateParser:
Log.warn("Could not create a parser")
return "Could not create a parser"
}
}
@@ -65,16 +65,26 @@ 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
ioData.pointee.mNumberBuffers = 1
ioData.pointee.mBuffers.mData = UnsafeMutableRawPointer.allocate(byteCount: packetByteCount, alignment: 0)
_ = packet.withUnsafeMutableBytes({ (bytes: UnsafeMutablePointer<UInt8>) in
_ = packet.accessMutableBytes({ (bytes: UnsafeMutablePointer<UInt8>) in
memcpy((ioData.pointee.mBuffers.mData?.assumingMemoryBound(to: UInt8.self))!, bytes, packetByteCount)
})
ioData.pointee.mBuffers.mDataByteSize = UInt32(packetByteCount)
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
+113 -53
View File
@@ -53,6 +53,9 @@ import AVFoundation
//TODO: what if user seeks beyond the data we have? What if we're done but user seeks even further than what we have
class AudioParser: AudioParsable {
private var MIN_PACKETS_TO_HAVE_AVAILABLE_BEFORE_THROTTLING_PARSING = 8192 // this will be modified when we know the file format to be just enough packets to fill up 1 pcm buffer
private var framesPerBuffer: Int = 1
//MARK:- For OS parser class
var parsedAudioHeaderPacketCount: UInt64 = 0
var parsedAudioPacketDataSize: UInt64 = 0
@@ -61,8 +64,8 @@ class AudioParser: AudioParsable {
public var fileAudioFormat: AVAudioFormat? {
didSet {
if let format = fileAudioFormat, oldValue == nil {
MIN_PACKETS_TO_HAVE_AVAILABLE_BEFORE_THROTTLING_PARSING = framesPerBuffer/Int(format.streamDescription.pointee.mFramesPerPacket)
parsedFileAudioFormatCallback(format)
throttler.tellAudioFormatFound()
}
}
}
@@ -85,7 +88,9 @@ class AudioParser: AudioParsable {
return max(AVAudioPacketCount(parsedAudioHeaderPacketCount), AVAudioPacketCount(audioPackets.count))
}
guard let sizeOfFileInBytes = expectedFileSizeInBytes, let bytesPerPacket = averageBytesPerPacket else {
let sizeOfFileInBytes: UInt64 = expectedFileSizeInBytes != nil ? expectedFileSizeInBytes! : 0
guard let bytesPerPacket = averageBytesPerPacket else {
return AVAudioPacketCount(0)
}
@@ -98,25 +103,23 @@ class AudioParser: AudioParsable {
return predictedCount
}
var sumOfParsedAudioBytes:UInt32 = 0 {
didSet {
if let byteCount = averageBytesPerPacket {
throttler.tellBytesPerAudioPacket(count: UInt64(byteCount))
}
}
}
var sumOfParsedAudioBytes:UInt32 = 0
var numberOfPacketsParsed:UInt32 = 0
var audioPackets: [(AudioStreamPacketDescription?,Data)] = [] {
var audioPackets: [(AudioStreamPacketDescription?,Data)] = [] {
didSet {
if let audioPacketByteSize = audioPackets.last?.0?.mDataByteSize {
sumOfParsedAudioBytes += audioPacketByteSize
numberOfPacketsParsed += 1
} else if let audioPacketByteSize = audioPackets.last?.1.count { // for uncompressed audio there are no descriptors to say how many bytes of audio are in this packet so we approximate by data size
sumOfParsedAudioBytes += UInt32(audioPacketByteSize)
}
//TODO: duration will not work with WAV or AIFF
numberOfPacketsParsed += 1
//TODO: duration will not be accurate with WAV or AIFF
}
}
private let lockQueue = DispatchQueue(label: "SwiftAudioPlayer.Parser.packets.lock")
var lastSentAudioPacketIndex = -1
/**
Audio packets varry in size. The first one parsed in a batch of audio
@@ -143,12 +146,29 @@ class AudioParser: AudioParsable {
return audioPackets.count == totalPredictedPacketCount
}
var streamChangeListenerId: UInt?
init(withRemoteUrl url: AudioURL, parsedFileAudioFormatCallback: @escaping(AVAudioFormat) -> ()) throws {
init(withRemoteUrl url: AudioURL, bufferSize: Int, parsedFileAudioFormatCallback: @escaping(AVAudioFormat) -> ()) throws {
self.url = url
self.framesPerBuffer = bufferSize
self.parsedFileAudioFormatCallback = parsedFileAudioFormatCallback
self.throttler = AudioThrottler(withRemoteUrl: url, withDelegate: self)
streamChangeListenerId = StreamingDownloadDirector.shared.attach { [weak self] (progress) in
guard let self = self else { return }
self.networkProgress = progress
// initially parse a bunch of packets
self.lockQueue.sync {
if self.fileAudioFormat == nil {
self.processNextDataPacket()
} else if self.audioPackets.count - self.lastSentAudioPacketIndex < self.MIN_PACKETS_TO_HAVE_AVAILABLE_BEFORE_THROTTLING_PARSING {
self.processNextDataPacket()
}
}
}
let context = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
//Open the stream and when we call parse data is fed into this stream
guard AudioFileStreamOpen(context, ParserPropertyListener, ParserPacketListener, kAudioFileMP3Type, &streamID) == noErr else {
@@ -156,33 +176,61 @@ class AudioParser: AudioParsable {
}
}
func pullPacket(atIndex index: AVAudioPacketCount) throws -> (AudioStreamPacketDescription?, Data) {
if let offset = getOffset(fromPacketIndex: index) {
throttler.tellByteOffset(offset: offset)
deinit {
if let id = streamChangeListenerId {
StreamingDownloadDirector.shared.detach(withID: id)
}
}
func pullPacket(atIndex index: AVAudioPacketCount) throws -> (AudioStreamPacketDescription?, Data) {
determineIfMoreDataNeedsToBeParsed(index: index)
// Check if we've reached the end of the packets. We have two scenarios:
// 1. We've reached the end of the packet data and the file has been completely parsed
// 2. We've reached the end of the data we currently have downloaded, but not the file
let packetIndex = index - indexSeekOffset
let isEndOfData = packetIndex >= audioPackets.count
if isEndOfData {
if isParsingComplete {
throw ParserError.readerAskingBeyondEndOfFile
} else {
Log.debug("Tried to pull packet at index: \(packetIndex) when only have: \(audioPackets.count), we predict \(totalPredictedPacketCount) in total")
throw ParserError.notEnoughDataForReader
var exception: ParserError? = nil
var packet: (AudioStreamPacketDescription?, Data) = (nil, Data())
lockQueue.sync {
if packetIndex >= self.audioPackets.count {
if isParsingComplete {
exception = ParserError.readerAskingBeyondEndOfFile
return
} else {
Log.debug("Tried to pull packet at index: \(packetIndex) when only have: \(self.audioPackets.count), we predict \(self.totalPredictedPacketCount) in total")
exception = ParserError.notEnoughDataForReader
return
}
}
lastSentAudioPacketIndex = Int(packetIndex)
packet = audioPackets[Int(packetIndex)]
}
if let exception = exception {
throw exception
} else {
return packet
}
}
private func determineIfMoreDataNeedsToBeParsed(index: AVAudioPacketCount) {
lockQueue.sync {
if index > self.audioPackets.count - self.MIN_PACKETS_TO_HAVE_AVAILABLE_BEFORE_THROTTLING_PARSING {
self.processNextDataPacket()
}
}
return audioPackets[Int(packetIndex)]
}
func tellSeek(toIndex index: AVAudioPacketCount) {
//Already within the processed audio packets. Ignore
if indexSeekOffset <= index && index < audioPackets.count + Int(indexSeekOffset) {
return
var isIndexValid: Bool = true
lockQueue.sync {
if self.indexSeekOffset <= index && index < self.audioPackets.count + Int(self.indexSeekOffset) {
isIndexValid = false
}
}
guard isIndexValid else { return }
guard let byteOffset = getOffset(fromPacketIndex: index) else {
return
@@ -194,15 +242,18 @@ class AudioParser: AudioParsable {
// NOTE: Order matters. Need to prevent appending to the array before we clean it. Just in case
// then we tell the throttler to send us appropriate packet
shouldPreventPacketFromFillingUp = true
audioPackets = []
lockQueue.sync {
self.audioPackets = []
}
throttler.tellSeek(offset: byteOffset)
self.processNextDataPacket()
}
private func getOffset(fromPacketIndex index: AVAudioPacketCount) -> UInt64? {
//Clear current buffer if we have audio format
guard fileAudioFormat != nil, let bytesPerPacket = self.averageBytesPerPacket else {
Log.error("should not get here")
Log.error("should not get here \(String(describing: fileAudioFormat)) and \(String(describing: self.averageBytesPerPacket))")
return nil
}
@@ -249,6 +300,12 @@ class AudioParser: AudioParsable {
return Needle(TimeInterval(frame)/TimeInterval(frameCount)*duration)
}
func append(description: AudioStreamPacketDescription?, data: Data) {
lockQueue.sync {
self.audioPackets.append((description, data))
}
}
func invalidate() {
throttler.invalidate()
@@ -276,6 +333,32 @@ class AudioParser: AudioParsable {
}
private func processNextDataPacket() {
throttler.pullNextDataPacket { [weak self] (d) in
guard let self = self else { return }
guard let data = d else { return }
self.lockQueue.sync {
Log.debug("processing data count: \(data.count) :: already had \(self.audioPackets.count) audio packets")
}
self.shouldPreventPacketFromFillingUp = false
do {
let sID = self.streamID!
let dataSize = data.count
_ = try data.accessBytes({ (bytes: UnsafePointer<UInt8>) in
let result:OSStatus = AudioFileStreamParseBytes(sID, UInt32(dataSize), bytes, [])
guard result == noErr else {
Log.monitor(ParserError.failedToParseBytes(result).errorDescription as Any)
throw ParserError.failedToParseBytes(result)
}
})
} catch {
Log.monitor(error.localizedDescription)
}
}
}
}
//MARK:- AudioThrottleDelegate
@@ -283,27 +366,4 @@ extension AudioParser: AudioThrottleDelegate {
func didUpdate(totalBytesExpected bytes: Int64) {
expectedFileSizeInBytes = UInt64(bytes)
}
func didUpdate(networkStreamProgress progress: Double) {
networkProgress = progress
}
func shouldProcess(networkData data: Data) {
Log.debug("processing data count: \(data.count) :: already had \(audioPackets.count) audio packets")
self.shouldPreventPacketFromFillingUp = false
do {
let sID = self.streamID!
let dataSize = data.count
let _ = try data.withUnsafeBytes({ (bytes:UnsafePointer<UInt8>) in
let result:OSStatus = AudioFileStreamParseBytes(sID, UInt32(dataSize), bytes, [])
guard result == noErr else {
Log.monitor(ParserError.failedToParseBytes(result).errorDescription as Any)
throw ParserError.failedToParseBytes(result)
}
})
} catch {
Log.monitor(error.localizedDescription)
}
}
}
@@ -3,7 +3,7 @@
// SwiftAudioPlayer
//
// Created by Tanha Kabir on 2019-01-29.
// Copyright © 2019 Tanha Kabir, Jon Mercer
// Copyright © 2019 Tanha Kabir, Jon Mercer, Moy Inzunza
//
// This file was modified and adapted from https://github.com/syedhali/AudioStreamer
// which was released under Apache License 2.0. Apache License 2.0 requires explicit
@@ -32,15 +32,23 @@
import Foundation
import AVFoundation
func ParserPacketListener(_ context: UnsafeMutableRawPointer, _ byteCount: UInt32, _ packetCount: UInt32, _ streamData: UnsafeRawPointer, _ packetDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>) {
#if swift(>=5.3)
func ParserPacketListener (_ context: UnsafeMutableRawPointer, _ byteCount: UInt32, _ packetCount: UInt32, _ streamData: UnsafeRawPointer, _ packetDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?) {
parserPacket(context, byteCount, packetCount, streamData, packetDescriptions)
}
#else
func ParserPacketListener (_ context: UnsafeMutableRawPointer, _ byteCount: UInt32, _ packetCount: UInt32, _ streamData: UnsafeRawPointer, _ packetDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>) {
parserPacket(context, byteCount, packetCount, streamData, packetDescriptions)
}
#endif
func parserPacket(_ context: UnsafeMutableRawPointer, _ byteCount: UInt32, _ packetCount: UInt32, _ streamData: UnsafeRawPointer, _ packetDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?){
let selfAudioParser = Unmanaged<AudioParser>.fromOpaque(context).takeUnretainedValue()
//bug in core audio where this could be nil
let packetDescriptionOrNil: UnsafeMutablePointer<AudioStreamPacketDescription>? = packetDescriptions
let isCompressed = packetDescriptionOrNil != nil
guard let fileAudioFormat = selfAudioParser.fileAudioFormat else {
Log.monitor("shouldnot have reached packet listener without a data format")
Log.monitor("should not have reached packet listener without a data format")
return
}
@@ -50,22 +58,25 @@ func ParserPacketListener(_ context: UnsafeMutableRawPointer, _ byteCount: UInt3
}
//TODO refactor this after we get it working
if isCompressed {
if let compressedPacketDescriptions = packetDescriptions { // is compressed audio (.mp3)
Log.debug("compressed audio")
for i in 0 ..< Int(packetCount) {
let audioPacketDescription = packetDescriptions[i]
let audioPacketDescription = compressedPacketDescriptions[i]
let audioPacketStart = Int(audioPacketDescription.mStartOffset)
let audioPacketSize = Int(audioPacketDescription.mDataByteSize)
let audioPacketData = Data(bytes: streamData.advanced(by: audioPacketStart), count: audioPacketSize)
selfAudioParser.audioPackets.append((audioPacketDescription,audioPacketData))
selfAudioParser.append(description: audioPacketDescription, data: audioPacketData)
}
} else {
} else { // not compressed audio (.wav)
Log.debug("uncompressed audio")
let format = fileAudioFormat.streamDescription.pointee
let bytesPerAudioPacket = Int(format.mBytesPerPacket)
for i in 0 ..< Int(packetCount) {
let audioPacketStart = i * bytesPerAudioPacket
let audioPacketSize = bytesPerAudioPacket
let audioPacketData = Data(bytes: streamData.advanced(by: audioPacketStart), count: audioPacketSize)
selfAudioParser.audioPackets.append((nil, audioPacketData))
selfAudioParser.append(description: nil, data: audioPacketData)
}
}
}
@@ -29,8 +29,15 @@ import Foundation
public struct SAAudioAvailabilityRange {
let startingNeedle: Needle
let durationLoadedByNetwork: Duration
let predictedDurationToLoad: Duration
let isPlayable: Bool
public var bufferingProgress: Double {
get {
return (startingNeedle + durationLoadedByNetwork) / predictedDurationToLoad
}
}
public var startingBufferTimePositon: Double {
get {
return startingNeedle
@@ -49,7 +56,31 @@ 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 - 5
}
// 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
}
}
+1
View File
@@ -30,4 +30,5 @@ public enum SAPlayingStatus {
case playing
case paused
case buffering
case ended
}
+12 -4
View File
@@ -35,7 +35,11 @@ protocol LockScreenViewProtocol {
}
extension LockScreenViewProtocol {
@available(iOS 10.0, *)
func clearLockScreenInfo() {
MPNowPlayingInfoCenter.default().nowPlayingInfo = [:]
}
@available(iOS 10.0, tvOS 10.0, *)
func setLockScreenInfo(withMediaInfo info: SALockScreenInfo?, duration: Duration) {
var nowPlayingInfo:[String : Any] = [:]
@@ -46,6 +50,7 @@ extension LockScreenViewProtocol {
let title = info.title
let artist = info.artist
let albumTitle = info.albumTitle ?? artist
let releaseDate = info.releaseDate
// For some reason we need to set a duration here for the needle?
@@ -53,7 +58,7 @@ extension LockScreenViewProtocol {
nowPlayingInfo[MPMediaItemPropertyTitle] = title
nowPlayingInfo[MPMediaItemPropertyArtist] = artist
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = artist
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = albumTitle
//nowPlayingInfo[MPMediaItemPropertyGenre] = //maybe later when we have it
//nowPlayingInfo[MPMediaItemPropertyIsExplicit] = //maybe later when we have it
nowPlayingInfo[MPMediaItemPropertyAlbumArtist] = artist
@@ -164,7 +169,10 @@ extension LockScreenViewProtocol {
}
func updateLockscreenSkipIntervals() {
MPRemoteCommandCenter.shared().skipBackwardCommand.preferredIntervals = [skipBackwardSeconds] as [NSNumber]
MPRemoteCommandCenter.shared().skipForwardCommand.preferredIntervals = [skipForwardSeconds] as [NSNumber]
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.skipBackwardCommand.isEnabled = skipBackwardSeconds > 0
commandCenter.skipBackwardCommand.preferredIntervals = [skipBackwardSeconds] as [NSNumber]
commandCenter.skipForwardCommand.isEnabled = skipForwardSeconds > 0
commandCenter.skipForwardCommand.preferredIntervals = [skipForwardSeconds] as [NSNumber]
}
}
+19 -1
View File
@@ -30,8 +30,12 @@ protocol AudioDataManagable {
var numberOfActive: Int { get }
var allowCellular: Bool { get set }
var downloadDirectory: FileManager.SearchPathDirectory { get }
func setHTTPHeaderFields(_ fields: [String: String]?)
func setBackgroundCompletionHandler(_ completionHandler: @escaping () -> ())
func setAllowCellularDownloadPreference(_ preference: Bool)
func setDownloadDirectory(_ dir: FileManager.SearchPathDirectory)
func clear()
@@ -51,7 +55,8 @@ protocol AudioDataManagable {
}
class AudioDataManager: AudioDataManagable {
var allowCellular: Bool = false
var allowCellular: Bool = true
var downloadDirectory: FileManager.SearchPathDirectory = .documentDirectory
static let shared: AudioDataManagable = AudioDataManager()
@@ -95,10 +100,23 @@ class AudioDataManager: AudioDataManagable {
streamingCallbacks = []
}
func setHTTPHeaderFields(_ fields: [String: String]?) {
streamWorker.HTTPHeaderFields = fields
downloadWorker.HTTPHeaderFields = fields
}
func setBackgroundCompletionHandler(_ completionHandler: @escaping () -> ()) {
backgroundCompletion = completionHandler
}
func setAllowCellularDownloadPreference(_ preference: Bool) {
allowCellular = preference
}
func setDownloadDirectory(_ dir: FileManager.SearchPathDirectory) {
downloadDirectory = dir
}
func attach(callback: @escaping (_ id: ID, _ progress: Double)->()) {
globalDownloadProgressCallback = callback
}
+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()
}
}
@@ -31,6 +31,8 @@ protocol AudioDataDownloadable: AnyObject {
var numberOfActive: Int { get }
var numberOfQueued: Int { get }
var HTTPHeaderFields: [String: String]? { get set }
func getProgressOfDownload(withID id: ID) -> Double?
func start(withID id: ID, withRemoteUrl remoteUrl: URL, completion: @escaping (URL) -> ())
@@ -57,6 +59,8 @@ class AudioDownloadWorker: NSObject, AudioDataDownloadable {
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
var HTTPHeaderFields: [String: String]?
private var activeDownloads: [ActiveDownload] = []
private var queuedDownloads = Set<DownloadInfo>()
@@ -111,7 +115,10 @@ class AudioDownloadWorker: NSObject, AudioDataDownloadable {
queuedDownloads.remove(info)
let task: URLSessionDownloadTask = session.downloadTask(with: info.remoteUrl)
var request = URLRequest(url: info.remoteUrl)
HTTPHeaderFields?.forEach { request.setValue($1, forHTTPHeaderField: $0) }
let task: URLSessionDownloadTask = session.downloadTask(with: request)
task.taskDescription = info.id
let activeTask = ActiveDownload(info: info, task: task)
+15 -7
View File
@@ -64,9 +64,14 @@ struct FileStorage {
// MARK:- Audio
extension FileStorage {
struct Audio {
private static let directory: FileManager.SearchPathDirectory = .documentDirectory
private init() {}
private static var directory: FileManager.SearchPathDirectory {
get {
return AudioDataManager.shared.downloadDirectory
}
}
static func isStored(_ id: ID) -> Bool {
guard let url = locate(id)?.path else {
return false
@@ -103,12 +108,15 @@ extension FileStorage {
}
static func locate(_ id: ID) -> URL? {
let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
for url in urls {
if url.absoluteString.contains(id) && url.pathExtension != "" {
_ = getUrl(givenId: id, andFileExtension: url.pathExtension)
return url
let folderUrls = FileManager.default.urls(for: directory, in: .userDomainMask)
guard folderUrls.count != 0 else { return nil }
if let urls = try? FileManager.default.contentsOfDirectory(at: folderUrls[0], includingPropertiesForKeys: nil) {
for url in urls {
if url.absoluteString.contains(id) && url.pathExtension != "" {
_ = getUrl(givenId: id, andFileExtension: url.pathExtension)
return url
}
}
}
return nil
+22 -9
View File
@@ -44,6 +44,9 @@ import Foundation
protocol AudioDataStreamable {
//if user taps download then starts to stream
init(progressCallback: @escaping (_ id: ID, _ dto: StreamProgressDTO) -> (), doneCallback: @escaping (_ id: ID, _ error: Error?)->Bool) //Bool is should save or not
var HTTPHeaderFields: [String: String]? { get set }
func start(withID id: ID, withRemoteURL url: URL, withInitialData data: Data?, andTotalBytesExpectedPreviously previousTotalBytesExpected: Int64?)
func pause(withId id: ID)
func resume(withId id: ID)
@@ -66,6 +69,8 @@ class AudioStreamWorker:NSObject, AudioDataStreamable {
fileprivate let doneCallback: (_ id: ID, _ error: Error?) -> Bool
private var session: URLSession!
var HTTPHeaderFields: [String: String]?
private var id: ID?
private var url: URL?
private var task: URLSessionDataTask?
@@ -89,7 +94,7 @@ class AudioStreamWorker:NSObject, AudioDataStreamable {
let config = URLSessionConfiguration.background(withIdentifier: "SwiftAudioPlayer.stream")
// Specifies that the phone should keep trying till it receives connection instead of dropping immediately
if #available(iOS 11.0, *) {
if #available(iOS 11.0, tvOS 11.0, *) {
config.waitsForConnectivity = true
}
self.session = URLSession(configuration: config, delegate: self, delegateQueue: nil) //TODO: should we use ephemeral
@@ -105,6 +110,7 @@ class AudioStreamWorker:NSObject, AudioDataStreamable {
if let data = data {
var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: TIMEOUT)
HTTPHeaderFields?.forEach { request.setValue($1, forHTTPHeaderField: $0) }
request.addValue("bytes=\(data.count)-", forHTTPHeaderField: "Range")
task = session.dataTask(with: request)
task?.taskDescription = id
@@ -121,10 +127,11 @@ class AudioStreamWorker:NSObject, AudioDataStreamable {
task?.resume()
} else {
task = session.dataTask(with: url)
task?.resume()
var request = URLRequest(url: url)
HTTPHeaderFields?.forEach { request.setValue($1, forHTTPHeaderField: $0) }
task = session.dataTask(with: request)
task?.taskDescription = id
task?.resume()
}
}
@@ -217,6 +224,7 @@ class AudioStreamWorker:NSObject, AudioDataStreamable {
self.progressCallback(id, StreamProgressDTO(progress: 0, data: Data(), totalBytesExpected: totalBytesExpectedForWholeFile))
var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: TIMEOUT)
HTTPHeaderFields?.forEach { request.setValue($1, forHTTPHeaderField: $0) }
request.addValue("bytes=\(offset)-", forHTTPHeaderField: "Range")
task = session.dataTask(with: request)
task?.resume()
@@ -243,15 +251,19 @@ extension AudioStreamWorker: URLSessionDataDelegate {
}
guard self.task == dataTask else {
Log.error("stream_error not the same task") //Probably because of seek
Log.error("stream_error not the same task 638283") //Probably because of seek
return
}
guard let totalBytesExpected = totalBytesExpectedForCurrentStream, totalBytesExpected > 0 else {
guard var totalBytesExpected = totalBytesExpectedForCurrentStream else {
Log.monitor("should not be called 223r2")
return
}
if totalBytesExpected <= 0 {
totalBytesExpected = totalBytesReceived
}
totalBytesReceived = totalBytesReceived + Int64(data.count)
let progress = Double(totalBytesReceived)/Double(totalBytesExpected)
@@ -267,7 +279,7 @@ extension AudioStreamWorker: URLSessionDataDelegate {
}
guard self.task == dataTask else {
Log.error("stream_error not the same task")
Log.error("stream_error not the same task 517253")
return
}
@@ -289,8 +301,8 @@ extension AudioStreamWorker: URLSessionDataDelegate {
return
}
guard self.task == task else {
Log.error("stream_error not the same task")
if self.task != task && self.task != nil {
Log.error("stream_error not the same task 3901833")
return
}
@@ -310,6 +322,7 @@ extension AudioStreamWorker: URLSessionDataDelegate {
Log.monitor("\(task.currentRequest?.url?.absoluteString ?? "nil url") error: \(err.localizedDescription)")
let _ = doneCallback(id, err)
return
}
let shouldSave = doneCallback(id, nil)
+319 -32
View File
@@ -27,6 +27,16 @@ import Foundation
import AVFoundation
public class SAPlayer {
public var DEBUG_MODE: Bool = false {
didSet {
if(DEBUG_MODE) {
logLevel = LogLevel.EXTERNAL_DEBUG
} else {
logLevel = LogLevel.MONITOR
}
}
}
/**
Access to the player.
*/
@@ -35,6 +45,96 @@ public class SAPlayer {
private var presenter: SAPlayerPresenter!
private var player: AudioEngine?
/**
Any necessary header fields for streaming and downloading requests can be set here.
*/
public var HTTPHeaderFields: [String: String]? {
didSet {
AudioDataManager.shared.setHTTPHeaderFields(HTTPHeaderFields)
}
}
/**
Access the engine of the player. Engine is nil if player has not been initialized with audio.
- Important: Changes to the engine are not safe guarded, thus unknown behaviour can arise from changing the engine. Just be wary and read [documentation of AVAudioEngine](https://developer.apple.com/documentation/avfoundation/avaudioengine) well when modifying,
*/
public var engine: AVAudioEngine? {
get {
return player?.engine
}
}
/**
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.
- Important: Changes to the engine and this node are not safe guarded, thus unknown behaviour can arise from changing the engine or this node. Just be wary and read [documentation of AVAudioEngine](https://developer.apple.com/documentation/avfoundation/avaudioengine) well when modifying,
*/
public var playerNode: AVAudioPlayerNode? {
get {
return player?.playerNode
}
}
/**
Corresponding to the overall volume of the player. Volume's default value is 1.0 and the range of valid values is 0.0 to 1.0. Volume is nil if no audio has been initialized yet.
*/
public var volume: Float? {
get {
return player?.playerNode.volume
}
set {
guard let value = newValue else { return }
guard value >= 0.0 && value <= 1.0 else { return }
player?.playerNode.volume = value
}
}
/**
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)
}
}
/**
Corresponding to the skipping forward button on the media player on the lockscreen. Default is set to 30 seconds.
*/
@@ -72,11 +172,22 @@ public class SAPlayer {
}
````
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).
To remove this default pitch modifier for playback rate changes, remove the node by calling `SAPlayer.shared.clearAudioModifiers()`.
*/
public var audioModifiers: [AVAudioUnit] = []
/**
List of queued audio for playback. You can edit this list as you wish to modify the queue.
*/
public var audioQueued: [SAAudioQueueItem] = []
/**
Total duration of current audio initialized. Returns nil if no audio is initialized in player.
- Note: If you are streaming from a source that does not have an expected size at the beginning of a stream, such as live streams, this value will be constantly updating to best known value at the time.
*/
public var duration: Double? {
get {
@@ -118,11 +229,7 @@ public class SAPlayer {
- Note: Setting this to nil clears the information displayed on the lockscreen media player.
*/
public var mediaInfo: SALockScreenInfo? = nil {
didSet {
presenter.handleLockscreenInfo(info: mediaInfo)
}
}
public var mediaInfo: SALockScreenInfo? = nil
private init() {
presenter = SAPlayerPresenter(delegate: self)
@@ -140,6 +247,23 @@ public class SAPlayer {
}
audioModifiers.append(AVAudioUnitTimePitch(audioComponentDescription: componentDescription))
NotificationCenter.default.addObserver(self, selector: #selector(handleInterruption), name: AVAudioSession.interruptionNotification, object: nil)
}
/**
Clears all [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements) modifiers intended to be used for realtime audio manipulation.
*/
public func clearAudioModifiers() {
audioModifiers.removeAll()
}
/**
Append an [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements) modifier to the list of modifiers used for realtime audio manipulation. The modifier will be added to the end of the list.
- Parameter modifier: The modifier to append.
*/
public func addAudioModifier(_ modifer: AVAudioUnit) {
audioModifiers.append(modifer)
}
/**
@@ -150,9 +274,8 @@ public class SAPlayer {
*/
public static func prettifyTimestamp(_ timestamp: Double) -> String {
let hours = Int(timestamp / 60 / 60)
let minutes = Int((timestamp - Double(hours * 60)) / 60)
let secondsLeft = Int(timestamp) - (minutes * 60)
let minutes = Int((timestamp - Double(hours * 60 * 60)) / 60)
let secondsLeft = Int(timestamp - Double(hours * 60 * 60) - Double(minutes * 60))
return "\(hours):\(String(format: "%02d", minutes)):\(String(format: "%02d", secondsLeft))"
}
@@ -164,26 +287,70 @@ public class SAPlayer {
func addUrlToMapping(url: URL) {
presenter.addUrlToKeyMap(url)
}
@objc func handleInterruption(notification: Notification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
return
}
// Switch over the interruption type.
switch type {
case .began:
// An interruption began. Update the UI as necessary.
pause()
case .ended:
// An interruption ended. Resume playback, if appropriate.
guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
// An interruption ended. Resume playback.
play()
} else {
// An interruption ended. Don't resume playback.
}
default: ()
}
}
}
public enum SAPlayerBitrate {
/// This bitrate is good for radio streams that are passing ittle amounts of audio data at a time. This will allow the player to process the audio data in a fast enough rate to not pause or get stuck playing. This rate however ends up using more CPU and is worse for your battery-life and performance of your app.
case low
/// This bitrate is good for streaming saved audio files like podcasts where most of the audio data will be received from the remote server at the beginning in a short time. This rate is more performant by using much less CPU and being better for your battery-life and app performance.
case high // go for audio files being streamed. This is uses less CPU and
}
//MARK: - External Player Controls
extension SAPlayer {
/**
Toggles between the play and pause state of the player if the player is not buffering (thus is playable).
Toggles between the play and pause state of the player. If nothing is playable (aka still in buffering state or no audio is initialized) no action will be taken. Please call `startSavedAudio` or `startRemoteAudio` to set up the player with audio before this.
- Note: If you are streaming, wait till the status from `SAPlayer.Updates.PlayingStatus` is not `.buffering`.
*/
public func togglePlayAndPause() {
presenter.handleTogglePlayingAndPausing()
}
/**
Attempts to play the player even if nothing playable is loaded (aka still in buffering state or no audio is initialized).
Attempts to play the player. If nothing is playable (aka still in buffering state or no audio is initialized) no action will be taken. Please call `startSavedAudio` or `startRemoteAudio` to set up the player with audio before this.
- Note: If you are streaming, wait till the status from `SAPlayer.Updates.PlayingStatus` is not `.buffering`.
*/
public func play() {
presenter.handlePlay()
}
/**
Attempts to pause the player even if nothing playable is loaded (aka still in buffering state or no audio is initialized).
Attempts to pause the player. If nothing is playable (aka still in buffering state or no audio is initialized) no action will be taken. Please call `startSavedAudio` or `startRemoteAudio` to set up the player with audio before this.
- Note:If you are streaming, wait till the status from `SAPlayer.Updates.PlayingStatus` is not `.buffering`.
*/
public func pause() {
presenter.handlePause()
@@ -221,6 +388,23 @@ extension SAPlayer {
/**
If using an AVAudioUnitTimePitch, it's important to notify the player that the rate at which the audio playing has changed to keep the media player in the lockscreen up to date. This is only important for playback rate changes.
- 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).
- Parameter rate: The current rate at which the audio is playing.
*/
public func playbackRateOfAudioChanged(rate: Float) {
@@ -232,46 +416,152 @@ extension SAPlayer {
- Important: If intending to use [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements) audio modifiers during playback, the list of audio modifiers under `SAPlayer.shared.audioModifiers` must be finalized before calling this function. After all realtime audio manipulations within the this will be effective.
- Note: The default list already has an AVAudioUnitTimePitch node first in the list. This node is specifically set to change the rate of audio without changing the pitch of the audio (intended for changing the rate of spoken word).
The component description of this node is:
````
var componentDescription: AudioComponentDescription {
get {
var ret = AudioComponentDescription()
ret.componentType = kAudioUnitType_FormatConverter
ret.componentSubType = kAudioUnitSubType_AUiPodTimeOther
return ret
}
}
````
Please look at [forums.developer.apple.com/thread/5874](https://forums.developer.apple.com/thread/5874) and [forums.developer.apple.com/thread/6050](https://forums.developer.apple.com/thread/6050) for more details.
To remove this default pitch modifier for playback rate changes, remove the node by calling `SAPlayer.shared.clearAudioModifiers()`.
- Parameter withSavedUrl: The URL of the audio saved on the device.
- Parameter mediaInfo: The media information of the audio to show on the lockscreen media player (optional).
*/
public func initializeSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
public func startSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
// 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.
presenter.handleClear()
// order here matters, need to set media info before trying to play audio
self.mediaInfo = mediaInfo
presenter.handlePlaySavedAudio(withSavedUrl: url)
}
/**
Sets up player to play audio that will be streamed from a remote location.
Sets up player to play audio that will be streamed from a remote location. After this is called, it will connect to the server and start to receive and process data. The player is not playable the SAAudioAvailabilityRange notifies that player is ready for playing (you can subscribe to these updates through `SAPlayer.Updates.StreamingBuffer`). You can alternatively see when the player is available to play by subscribing to `SAPlayer.Updates.PlayingStatus` and waiting for a status that isn't `.buffering`.
- Important: If intending to use [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements) audio modifiers during playback, the list of audio modifiers under `SAPlayer.shared.audioModifiers` must be finalized before calling this function. After all realtime audio manipulations within the this will be effective.
- Note: The default list already has an AVAudioUnitTimePitch node first in the list. This node is specifically set to change the rate of audio without changing the pitch of the audio (intended for changing the rate of spoken word).
The component description of this node is:
````
var componentDescription: AudioComponentDescription {
get {
var ret = AudioComponentDescription()
ret.componentType = kAudioUnitType_FormatConverter
ret.componentSubType = kAudioUnitSubType_AUiPodTimeOther
return ret
}
}
````
Please look at [forums.developer.apple.com/thread/5874](https://forums.developer.apple.com/thread/5874) and [forums.developer.apple.com/thread/6050](https://forums.developer.apple.com/thread/6050) for more details.
To remove this default pitch modifier for playback rate changes, remove the node by calling `SAPlayer.shared.clearAudioModifiers()`.
- Note: Subscribe to `SAPlayer.Updates.StreamingBuffer` to see updates in streaming progress.
- Parameter withRemoteUrl: The URL of the remote audio.
- Parameter bitrate: The bitrate of the streamed audio. By default the bitrate is set to high for streaming saved audio files. If you want to stream radios then you should use the `low` bitrate option.
- Parameter mediaInfo: The media information of the audio to show on the lockscreen media player (optional).
*/
public func initializeRemoteAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
public func startRemoteAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate = .high, mediaInfo: SALockScreenInfo? = nil) {
// 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.
presenter.handleClear()
// order here matters, need to set media info before trying to play audio
self.mediaInfo = mediaInfo
presenter.handlePlayStreamedAudio(withRemoteUrl: url)
presenter.handlePlayStreamedAudio(withRemoteUrl: url, bitrate: bitrate)
}
/**
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.
- Parameter bitrate: The bitrate of the streamed audio. By default the bitrate is set to high for streaming saved audio files. If you want to stream radios then you should use the `low` bitrate option.
- Parameter mediaInfo: The media information of the audio to show on the lockscreen media player (optional).
*/
public func queueRemoteAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate = .high, mediaInfo: SALockScreenInfo? = nil) {
presenter.handleQueueStreamedAudio(withRemoteUrl: url, mediaInfo: mediaInfo, bitrate: bitrate)
}
/**
Queues saved 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 withSavedUrl: The URL of the audio saved on the device.
- Parameter mediaInfo: The media information of the audio to show on the lockscreen media player (optional).
*/
public func queueSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
presenter.handleQueueSavedAudio(withSavedUrl: url, mediaInfo: mediaInfo)
}
/**
Remove the first queued audio if one exists. Receive the first URL removed back.
- Returns the URL of the removed audio.
*/
public func removeFirstQueuedAudio() -> URL? {
guard audioQueued.count != 0 else { return nil }
return presenter.handleRemoveFirstQueuedItem()
}
/**
Clear the list of queued audio.
- Returns the list of removed audio URLs
*/
public func clearAllQueuedAudio() -> [URL] {
return presenter.handleClearQueued()
}
/**
Resets the player to the state before initializing audio and setting media info.
*/
public func clear() {
presenter.handleClear()
}
}
//MARK: - Internal implementation of delegate
extension SAPlayer: SAPlayerDelegate {
func startAudioDownloaded(withSavedUrl url: AudioURL) {
player?.pause()
player?.invalidate()
internal func startAudioDownloaded(withSavedUrl url: AudioURL) {
player = AudioDiskEngine(withSavedUrl: url, delegate: presenter)
}
func startAudioStreamed(withRemoteUrl url: AudioURL) {
player?.pause()
player?.invalidate()
player = AudioStreamEngine(withRemoteUrl: url, delegate: presenter)
internal func startAudioStreamed(withRemoteUrl url: AudioURL, bitrate: SAPlayerBitrate) {
player = AudioStreamEngine(withRemoteUrl: url, delegate: presenter, bitrate: bitrate)
}
func playEngine() {
internal func clearEngine() {
player?.pause()
player?.invalidate()
player = nil
Log.info("cleared engine")
}
internal func playEngine() {
becomeDeviceAudioPlayer()
player?.play()
}
@@ -279,26 +569,23 @@ extension SAPlayer: SAPlayerDelegate {
//Start taking control as the device's player
private func becomeDeviceAudioPlayer() {
do {
if #available(iOS 11.0, *) {
// try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, policy: .longForm, options: [])
if #available(iOS 11.0, tvOS 11.0, *) {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, policy: .longFormAudio, options: [])
} else {
// Fallback on earlier versions
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: AVAudioSession.Mode(rawValue: convertFromAVAudioSessionMode(AVAudioSession.Mode.default)), options: .allowAirPlay)
}
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: AVAudioSession.Mode(rawValue: convertFromAVAudioSessionMode(AVAudioSession.Mode.default)), options: .allowAirPlay)
try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
} catch {
Log.monitor("Problem setting up AVAudioSession to play in:: \(error.localizedDescription)")
}
}
func pauseEngine() {
internal func pauseEngine() {
player?.pause()
}
func seekEngine(toNeedle needle: Needle) {
var seekToNeedle = needle < 0 ? 0 : needle
seekToNeedle = needle > Needle(duration ?? 0) ? Needle(duration ?? 0) : needle
internal func seekEngine(toNeedle needle: Needle) {
let seekToNeedle = needle < 0 ? 0 : needle
player?.seek(toNeedle: seekToNeedle)
}
}
+3 -1
View File
@@ -27,11 +27,13 @@ import Foundation
import CoreMedia
protocol SAPlayerDelegate: AnyObject, LockScreenViewProtocol {
var mediaInfo: SALockScreenInfo? { get set }
var skipForwardSeconds: Double { get set }
var skipBackwardSeconds: Double { get set }
func startAudioDownloaded(withSavedUrl url: AudioURL)
func startAudioStreamed(withRemoteUrl url: AudioURL)
func startAudioStreamed(withRemoteUrl url: AudioURL, bitrate: SAPlayerBitrate)
func clearEngine()
func playEngine()
func pauseEngine()
func seekEngine(toNeedle needle: Needle) //TODO ensure that engine cleans up out of bounds
+18
View File
@@ -100,5 +100,23 @@ extension SAPlayer {
public static func setBackgroundCompletionHandler(_ completionHandler: @escaping () -> ()) {
AudioDataManager.shared.setBackgroundCompletionHandler(completionHandler)
}
/**
Whether downloading audio on cellular data is allowed. By default this is set to `true`.
*/
public static var allowUsingCellularData = true {
didSet {
AudioDataManager.shared.setAllowCellularDownloadPreference(allowUsingCellularData)
}
}
/**
EXPERIMENTAL!
*/
public static var downloadDirectory: FileManager.SearchPathDirectory = .documentDirectory {
didSet {
AudioDataManager.shared.setDownloadDirectory(downloadDirectory)
}
}
}
}
+167
View File
@@ -0,0 +1,167 @@
//
// 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
static var originalRate: Float = 1.0
/**
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.
- Precondition: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
- Important: If you want to change the rate of the overall player while having skip silences on, please use `SAPlayer.Features.SkipSilences.setRateSafely()` to properly set the rate of the player. Any rate changes to the player will be ignored while using Skip Silences otherwise.
*/
public static func enable() -> Bool {
guard let engine = SAPlayer.shared.engine else { return false }
Log.info("enabling skip silences feature")
enabled = true
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.debug("speed up rate to \(String(describing: SAPlayer.shared.rate))")
} else {
SAPlayer.shared.rate = originalRate
Log.debug("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.
- Precondition: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
*/
public static func disable() -> Bool {
guard let engine = SAPlayer.shared.engine else { return false }
Log.info("disabling skip silences feature")
engine.mainMixerNode.removeTap(onBus: 0)
SAPlayer.shared.rate = originalRate
enabled = false
return true
}
/**
Use this function to set the overall rate of the player for when skip silences is on. This ensures that the overall rate will be what is set through this function even as skip silences is on; if this function is not used then any changes asked of from the overall player while skip silences is on won't be recorded!
- Important: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
*/
public static func setRateSafely(_ rate: Float) {
originalRate = rate
SAPlayer.shared.rate = rate
}
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()
}
}
/**
Feature to play the current playing audio on repeat until feature is disabled.
*/
public struct Loop {
static var enabled: Bool = false
static var playingStatusId: UInt?
/**
Enable feature to play the current playing audio on loop. This will continue until the feature is disabled. And this feature works for both remote and saved audio.
*/
public static func enable() {
enabled = true
guard playingStatusId == nil else { return }
playingStatusId = SAPlayer.Updates.PlayingStatus.subscribe({ (status) in
if status == .ended && enabled {
SAPlayer.shared.seekTo(seconds: 0.0)
SAPlayer.shared.play()
}
})
}
/**
Disable feature playing audio on loop.
*/
public static func disable() {
enabled = false
}
}
}
}
@@ -37,13 +37,49 @@ public typealias UTC = Int
public struct SALockScreenInfo {
var title: String
var artist: String
var albumTitle: String?
var artwork: UIImage?
var releaseDate: UTC
public init(title: String, artist: String, artwork: UIImage?, releaseDate: UTC) {
public init(title: String, artist: String, albumTitle: String?, artwork: UIImage?, releaseDate: UTC) {
self.title = title
self.artist = artist
self.albumTitle = albumTitle
self.artwork = artwork
self.releaseDate = releaseDate
}
}
/**
Use to add audio to be queued for playback.
*/
public struct SAAudioQueueItem {
public var loc: Location
public var url: URL
public var mediaInfo: SALockScreenInfo?
public var bitrate: SAPlayerBitrate
/**
Use to add audio to be queued for playback.
- Parameter loc: If the URL for the file is remote or saved on device.
- Parameter url: URL of audio to be queued
- Parameter mediaInfo: Relevant lockscreen media info for the queued audio.
- Parameter bitrate: For streamed remote audio specifiy a bitrate if different from high. Use low bitrate for radio streams.
*/
public init(loc: Location, url: URL, mediaInfo: SALockScreenInfo?, bitrate: SAPlayerBitrate = .high) {
self.loc = loc
self.url = url
self.mediaInfo = mediaInfo
self.bitrate = bitrate
}
/**
Where the queued audio is sourced. Remote to be streamed or locally saved on device.
*/
public enum Location {
case remote
case saved
}
}
+109 -58
View File
@@ -36,20 +36,53 @@ class SAPlayerPresenter {
private var key: String?
private var isPlaying: SAPlayingStatus = .buffering
private var mediaInfo: SALockScreenInfo?
private var urlKeyMap: [Key: URL] = [:]
var durationRef:UInt = 0
var needleRef:UInt = 0
var playingStatusRef:UInt = 0
var durationRef: UInt = 0
var needleRef: UInt = 0
var playingStatusRef: UInt = 0
var audioQueue: [SAAudioQueueItem] = []
init(delegate: SAPlayerDelegate?) {
self.delegate = delegate
delegate?.setLockScreenControls(presenter: self)
prepareNextEpisodeToPlay()
durationRef = AudioClockDirector.shared.attachToChangesInDuration(closure: { [weak self] (duration) in
guard let self = self else { throw DirectorError.closureIsDead }
self.delegate?.updateLockscreenPlaybackDuration(duration: duration)
self.duration = duration
self.delegate?.setLockScreenInfo(withMediaInfo: self.delegate?.mediaInfo, duration: duration)
})
needleRef = AudioClockDirector.shared.attachToChangesInNeedle(closure: { [weak self] (needle) in
guard let self = self else { throw DirectorError.closureIsDead }
self.needle = needle
self.delegate?.updateLockscreenElapsedTime(needle: needle)
})
playingStatusRef = AudioClockDirector.shared.attachToChangesInPlayingStatus(closure: { [weak self] (isPlaying) in
guard let self = self else { throw DirectorError.closureIsDead }
if(isPlaying == .paused && self.shouldPlayImmediately) {
self.shouldPlayImmediately = false
self.handlePlay()
}
// solves bug nil == owningEngine || GetEngine() == owningEngine where too many
// ended statuses were notified to cause 2 engines to be initialized and causes an error.
// TODO don't need guard
guard isPlaying != self.isPlaying else { return }
self.isPlaying = isPlaying
if(self.isPlaying == .ended) {
self.playNextAudioIfExists()
}
})
}
func getUrl(forKey key: Key) -> URL? {
@@ -60,62 +93,63 @@ class SAPlayerPresenter {
urlKeyMap[url.key] = url
}
func handleClear() {
delegate?.clearEngine()
AudioClockDirector.shared.resetCache()
needle = nil
duration = nil
key = nil
delegate?.mediaInfo = nil
delegate?.clearLockScreenInfo()
}
func handlePlaySavedAudio(withSavedUrl url: URL) {
attachForUpdates(url: url)
resetCacheForNewAudio(url: url)
delegate?.startAudioDownloaded(withSavedUrl: url)
}
func handlePlayStreamedAudio(withRemoteUrl url: URL) {
attachForUpdates(url: url)
delegate?.startAudioStreamed(withRemoteUrl: url)
func handlePlayStreamedAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate) {
resetCacheForNewAudio(url: url)
delegate?.startAudioStreamed(withRemoteUrl: url, bitrate: bitrate)
}
private func attachForUpdates(url: URL) {
AudioClockDirector.shared.detachFromChangesInDuration(withID: durationRef)
AudioClockDirector.shared.detachFromChangesInNeedle(withID: needleRef)
AudioClockDirector.shared.detachFromChangesInPlayingStatus(withID: playingStatusRef)
private func resetCacheForNewAudio(url: URL) {
self.key = url.key
urlKeyMap[url.key] = url
durationRef = AudioClockDirector.shared.attachToChangesInDuration(closure: { [weak self] (key, duration) in
guard let self = self else { throw DirectorError.closureIsDead }
guard key == self.key else {
Log.debug("misfire expected key: \(self.key ?? "none") payload key: \(key)")
return
}
self.delegate?.updateLockscreenPlaybackDuration(duration: duration)
self.duration = duration
self.delegate?.setLockScreenInfo(withMediaInfo: self.mediaInfo, duration: duration)
})
needleRef = AudioClockDirector.shared.attachToChangesInNeedle(closure: { [weak self] (key, needle) in
guard let self = self else { throw DirectorError.closureIsDead }
guard key == self.key else {
Log.debug("misfire expected key: \(self.key ?? "none") payload key: \(key)")
return
}
self.needle = needle
self.delegate?.updateLockscreenElapsedTime(needle: needle)
})
playingStatusRef = AudioClockDirector.shared.attachToChangesInPlayingStatus(closure: { [weak self] (key, isPlaying) in
guard let self = self else { throw DirectorError.closureIsDead }
guard key == self.key else {
Log.debug("misfire expected key: \(self.key ?? "none") payload key: \(key)")
return
}
self.isPlaying = isPlaying
})
AudioClockDirector.shared.setKey(url.key)
AudioClockDirector.shared.resetCache()
}
@available(iOS 10.0, *)
func handleLockscreenInfo(info: SALockScreenInfo?) {
self.mediaInfo = info
func handleQueueStreamedAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo?, bitrate: SAPlayerBitrate) {
audioQueue.append(SAAudioQueueItem(loc: .remote, url: url, mediaInfo: mediaInfo, bitrate: bitrate))
}
func handleQueueSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo?) {
audioQueue.append(SAAudioQueueItem(loc: .saved, url: url, mediaInfo: mediaInfo))
}
func handleRemoveFirstQueuedItem() -> URL? {
guard audioQueue.count != 0 else { return nil }
return audioQueue.remove(at: 0).url
}
func handleClearQueued() -> [URL] {
guard audioQueue.count != 0 else { return [] }
let urls = audioQueue.map { item in
return item.url
}
audioQueue = []
return urls
}
func handleStopStreamingAudio() {
delegate?.clearEngine()
AudioClockDirector.shared.resetCache()
}
}
@@ -175,17 +209,34 @@ 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()
Log.info("getting ready to play \(nextAudioURL)")
AudioQueueDirector.shared.changeInQueue(url: nextAudioURL.url)
handleClear()
delegate?.mediaInfo = nextAudioURL.mediaInfo
switch nextAudioURL.loc {
case .remote:
handlePlayStreamedAudio(withRemoteUrl: nextAudioURL.url, bitrate: nextAudioURL.bitrate)
break
case .saved:
handlePlaySavedAudio(withSavedUrl: nextAudioURL.url)
break
}
shouldPlayImmediately = true
}
}
+88
View File
@@ -47,6 +47,7 @@ extension SAPlayer {
- Parameter timePosition: The current time within the audio that is playing.
- Returns: the id for the subscription in the case you would like to unsubscribe to updates for the closure.
*/
@available(*, deprecated, message: "Use subscribe without the url in the closure for current audio updates")
public static func subscribe(_ closure: @escaping (_ url: URL, _ timePosition: Double) -> ()) -> UInt {
return AudioClockDirector.shared.attachToChangesInNeedle(closure: { (key, needle) in
guard let url = SAPlayer.shared.getUrl(forKey: key) else { return }
@@ -54,6 +55,19 @@ extension SAPlayer {
})
}
/**
Subscribe to updates in elapsed time of the playing audio. Aka, the current timestamp of the audio.
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in time.
- Parameter timePosition: The current time within the audio that is playing.
- 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 (_ timePosition: Double) -> ()) -> UInt {
AudioClockDirector.shared.attachToChangesInNeedle(closure: closure)
}
/**
Stop recieving updates of changes in elapsed time of audio.
@@ -66,12 +80,16 @@ extension SAPlayer {
/**
Updates to changes in the duration of the current initialized audio. Especially helpful for audio that is being streamed and can change with more data.
- Note: If you are streaming from a source that does not have an expected size at the beginning of a stream, such as live streams, duration will be constantly updating to best known value at the time (which is the seconds buffered currently and not necessarily the actual total duration of audio).
*/
public struct Duration {
/**
Subscribe to updates to changes in duration of the current audio initialized.
- Note: If you are streaming from a source that does not have an expected size at the beginning of a stream, such as live streams, duration will be constantly updating to best known value at the time (which is the seconds buffered currently and not necessarily the actual total duration of audio).
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in duration.
@@ -79,6 +97,7 @@ extension SAPlayer {
- Parameter duration: The duration of the current initialized audio.
- Returns: the id for the subscription in the case you would like to unsubscribe to updates for the closure.
*/
@available(*, deprecated, message: "Use subscribe without the url in the closure for current audio updates")
public static func subscribe(_ closure: @escaping (_ url: URL, _ duration: Double) -> ()) -> UInt {
return AudioClockDirector.shared.attachToChangesInDuration(closure: { (key, duration) in
guard let url = SAPlayer.shared.getUrl(forKey: key) else { return }
@@ -86,6 +105,21 @@ extension SAPlayer {
})
}
/**
Subscribe to updates to changes in duration of the current audio initialized.
- Note: If you are streaming from a source that does not have an expected size at the beginning of a stream, such as live streams, duration will be constantly updating to best known value at the time (which is the seconds buffered currently and not necessarily the actual total duration of audio).
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in duration.
- Parameter duration: The duration of the current initialized audio.
- 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 (_ duration: Double) -> ()) -> UInt {
return AudioClockDirector.shared.attachToChangesInDuration(closure: closure)
}
/**
Stop recieving updates of changes in duration of the current initialized audio.
@@ -111,6 +145,7 @@ extension SAPlayer {
- Parameter playingStatus: Whether the player is playing audio or paused.
- Returns: the id for the subscription in the case you would like to unsubscribe to updates for the closure.
*/
@available(*, deprecated, message: "Use subscribe without the url in the closure for current audio updates")
public static func subscribe(_ closure: @escaping (_ url: URL, _ playingStatus: SAPlayingStatus) -> ()) -> UInt {
return AudioClockDirector.shared.attachToChangesInPlayingStatus(closure: { (key, isPlaying) in
guard let url = SAPlayer.shared.getUrl(forKey: key) else { return }
@@ -118,6 +153,19 @@ extension SAPlayer {
})
}
/**
Subscribe to updates to changes in the playing/paused status of audio.
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in duration.
- Parameter playingStatus: Whether the player is playing audio or paused.
- Returns: the id for the subscription in the case you would like to unsubscribe to updates for the closure.
*/
public static func subscribe(_ closure: @escaping (_ playingStatus: SAPlayingStatus) -> ()) -> UInt {
return AudioClockDirector.shared.attachToChangesInPlayingStatus(closure: closure)
}
/**
Stop recieving updates of changes in the playing/paused status of audio.
@@ -136,6 +184,8 @@ extension SAPlayer {
/**
Subscribe to updates to changes in the progress of downloading audio for streaming. Information about range of audio available and if the audio is playable. Look at SAAudioAvailabilityRange for more information. For progress of downloading audio that saves to the phone for playback later, look at AudioDownloading instead.
- Note: For live streams that don't have an expected audio length from the beginning of the stream; the duration is constantly changing and equal to the total seconds buffered from the SAAudioAvailabilityRange.
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in duration.
@@ -143,6 +193,7 @@ extension SAPlayer {
- Parameter buffer: Availabity of audio that has been downloaded to play.
- Returns: the id for the subscription in the case you would like to unsubscribe to updates for the closure.
*/
@available(*, deprecated, message: "Use subscribe without the url in the closure for current audio updates")
public static func subscribe(_ closure: @escaping (_ url: URL, _ buffer: SAAudioAvailabilityRange) -> ()) -> UInt {
return AudioClockDirector.shared.attachToChangesInBufferedRange(closure: { (key, buffer) in
guard let url = SAPlayer.shared.getUrl(forKey: key) else { return }
@@ -150,6 +201,21 @@ extension SAPlayer {
})
}
/**
Subscribe to updates to changes in the progress of downloading audio for streaming. Information about range of audio available and if the audio is playable. Look at SAAudioAvailabilityRange for more information. For progress of downloading audio that saves to the phone for playback later, look at AudioDownloading instead.
- Note: For live streams that don't have an expected audio length from the beginning of the stream; the duration is constantly changing and equal to the total seconds buffered from the SAAudioAvailabilityRange.
- Note: It's recommended to have a weak reference to a class that uses this function
- Parameter closure: The closure that will receive the updates of the changes in duration.
- Parameter buffer: Availabity of audio that has been downloaded to play.
- 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 (_ buffer: SAAudioAvailabilityRange) -> ()) -> UInt {
return AudioClockDirector.shared.attachToChangesInBufferedRange(closure: closure)
}
/**
Stop recieving updates of changes in streaming progress.
@@ -191,6 +257,28 @@ 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 (_ newUrl: URL) -> ()) -> UInt {
return AudioQueueDirector.shared.attach(closure: closure)
}
/**
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)
}
}
}
}
+54
View File
@@ -0,0 +1,54 @@
//
// Data.swift
// SwiftAudioPlayer
//
// Created by Tanha Kabir on 2019-11-29.
// Copyright © 2019 Tanha Kabir, Jon Mercer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
extension Data {
// Introduced in Swift 5, withUnsafeBytes using UnsafePointers is deprecated
// https://mjtsai.com/blog/2019/03/27/swift-5-released/
func accessBytes<R>(_ body: (UnsafePointer<UInt8>) throws -> R) rethrows -> R {
return try withUnsafeBytes { (rawBufferPointer: UnsafeRawBufferPointer) -> R in
let unsafeBufferPointer = rawBufferPointer.bindMemory(to: UInt8.self)
guard let unsafePointer = unsafeBufferPointer.baseAddress else {
Log.error("")
var int: UInt8 = 0
return try body(&int)
}
return try body(unsafePointer)
}
}
mutating func accessMutableBytes<R>(_ body: (UnsafeMutablePointer<UInt8>) throws -> R) rethrows -> R {
return try withUnsafeMutableBytes { (rawBufferPointer: UnsafeMutableRawBufferPointer) -> R in
let unsafeMutableBufferPointer = rawBufferPointer.bindMemory(to: UInt8.self)
guard let unsafeMutablePointer = unsafeMutableBufferPointer.baseAddress else {
Log.error("")
var int: UInt8 = 0
return try body(&int)
}
return try body(unsafeMutablePointer)
}
}
}
+12 -11
View File
@@ -25,18 +25,15 @@
import Foundation
enum DirectorError: Error {
case closureIsDead
}
/**
P for payload
*/
class DirectorThreadSafeClosures<P> {
typealias TypeClosure = (Key, P) throws -> Void
typealias TypeClosure = (P) throws -> Void
private var queue: DispatchQueue = DispatchQueue(label: "SwiftAudioPlayer.thread_safe_map", attributes: .concurrent)
private var closures: [UInt: TypeClosure] = [:]
private var cache: [Key: P] = [:]
private var cache: P? = nil
var count: Int {
get {
@@ -44,13 +41,17 @@ class DirectorThreadSafeClosures<P> {
}
}
func broadcast(key: Key, payload: P) {
func resetCache() {
cache = nil
}
func broadcast(payload: P) {
queue.sync {
self.cache[key] = payload
self.cache = payload
var iterator = self.closures.makeIterator()
while let element = iterator.next() {
do {
try element.value(key, payload)
try element.value(payload)
} catch {
helperRemove(withKey: element.key)
}
@@ -64,9 +65,9 @@ class DirectorThreadSafeClosures<P> {
//The director may not yet have the status yet. We should only call the closure if we have it
//Let the caller know the immediate value. If it's dead already then stop
for (key, val) in cache {
if let val = cache {
do {
try closure(key, val)
try closure(val)
} catch {
return id
}
@@ -85,7 +86,7 @@ class DirectorThreadSafeClosures<P> {
func clear() {
queue.async(flags: .barrier) {
self.closures.removeAll()
self.cache.removeAll()
self.cache = nil
}
}
@@ -0,0 +1,103 @@
//
// DirectorThreadSafeClosures.swift
// SwiftAudioPlayer
//
// Created by Tanha Kabir on 2019-01-29.
// Copyright © 2019 Tanha Kabir, Jon Mercer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
enum DirectorError: Error {
case closureIsDead
}
/**
P for payload
*/
class DirectorThreadSafeClosuresDeprecated<P> {
typealias TypeClosure = (Key, P) throws -> Void
private var queue: DispatchQueue = DispatchQueue(label: "SwiftAudioPlayer.thread_safe_map", attributes: .concurrent)
private var closures: [UInt: TypeClosure] = [:]
private var cache: [Key: P] = [:]
var count: Int {
get {
return closures.count
}
}
func broadcast(key: Key, payload: P) {
queue.sync {
self.cache[key] = payload
var iterator = self.closures.makeIterator()
while let element = iterator.next() {
do {
try element.value(key, payload)
} catch {
helperRemove(withKey: element.key)
}
}
}
}
//UInt is actually 64-bits on modern devices
func attach(closure: @escaping TypeClosure) -> UInt {
let id: UInt = Date.getUTC64()
//The director may not yet have the status yet. We should only call the closure if we have it
//Let the caller know the immediate value. If it's dead already then stop
for (key, val) in cache {
do {
try closure(key, val)
} catch {
return id
}
}
//Replace what's in the map with the new closure
helperInsert(withKey: id, closure: closure)
return id
}
func detach(id: UInt) {
helperRemove(withKey: id)
}
func clear() {
queue.async(flags: .barrier) {
self.closures.removeAll()
self.cache.removeAll()
}
}
private func helperRemove(withKey key: UInt) {
queue.async(flags: .barrier) {
self.closures[key] = nil
}
}
private func helperInsert(withKey key: UInt, closure: @escaping TypeClosure) {
queue.async(flags: .barrier) {
self.closures[key] = closure
}
}
}
+25 -14
View File
@@ -9,22 +9,23 @@
import Foundation
import os.log
// Possible levels of log messages to log
enum LogLevel: Int {
case DEBUG = 1
case INFO = 2
case WARN = 3
case ERROR = 4
case EXTERNAL_DEBUG = 5
case MONITOR = 6
case TEST = 7
}
// Specify which types of log messages to display. Default level is set to WARN, which means Log will print any log messages of type only WARN, ERROR, MONITOR, and TEST. To print DEBUG and INFO logs, set the level to a lower value.
var logLevel: LogLevel = LogLevel.MONITOR
class Log {
private init() {}
// Possible levels of log messages to log
public enum LogLevel: Int {
case DEBUG = 1
case INFO = 2
case WARN = 3
case ERROR = 4
case MONITOR = 5
case TEST = 6
}
// Specify which types of log messages to display. Default level is set to WARN, which means Log will print any log messages of type only WARN, ERROR, MONITOR, and TEST. To print DEBUG and INFO logs, set the level to a lower value.
public static var logLevel: LogLevel = LogLevel.MONITOR
// Used for OSLog
private static let SUBSYSTEM: String = "com.SwiftAudioPlayer"
@@ -68,6 +69,11 @@ class Log {
let log = OSLog(subsystem: SUBSYSTEM, category: "ERROR 🛑🛑🛑🛑")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
}
if logLevel.rawValue <= LogLevel.EXTERNAL_DEBUG.rawValue {
let log = OSLog(subsystem: SUBSYSTEM, category: "WARNING")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
}
}
/**
@@ -86,7 +92,7 @@ class Log {
public static func monitor(_ logMessage: Any, classPath: String = #file, functionName: String = #function, lineNumber: Int = #line) {
let fileName = URLUtil.getNameFromStringPath(classPath)
if logLevel.rawValue <= LogLevel.ERROR.rawValue {
let log = OSLog(subsystem: SUBSYSTEM, category: "MONITOR 🔥🔥🔥🔥")
let log = OSLog(subsystem: SUBSYSTEM, category: "ERROR 🔥🔥🔥🔥")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
}
}
@@ -110,6 +116,11 @@ class Log {
let log = OSLog(subsystem: SUBSYSTEM, category: "WARN ⚠️⚠️⚠️⚠️")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
}
if logLevel.rawValue <= LogLevel.EXTERNAL_DEBUG.rawValue {
let log = OSLog(subsystem: SUBSYSTEM, category: "DEBUG")
os_log("%@:%@:%d:: %@", log: log, fileName, functionName, lineNumber, "\(logMessage)")
}
}
/**
+3 -3
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudioPlayer'
s.version = '2.1.0'
s.version = '7.3.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.
@@ -26,9 +26,9 @@ SwiftAudioPlayer is a Swift based audio player that can handle streaming from a
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'tanhakabir' => 'tanhakabir.ca@gmail.com', 'JonMercer' => 'mercer.jon@gmail.com' }
s.source = { :git => 'https://github.com/tanhakabir/SwiftAudioPlayer.git', :tag => s.version.to_s }
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
s.social_media_url = 'https://twitter.com/_tanhakabir'
s.ios.deployment_target = '10.0'
s.platforms = { :ios => '10.0', :tvos => '10.0' }
s.source_files = 'Source/**/*'
s.swift_version = '5.0'