Compare commits

...

33 Commits

Author SHA1 Message Date
tanhakabir 930509d6be fix maintaining rate changes while skip silences is enabled 2021-05-09 17:15:18 -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
16 changed files with 420 additions and 306 deletions
+4
View File
@@ -48,6 +48,7 @@
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 */; };
A4FBA6B5221B74C900D5A353 /* SALockScreenInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */; };
A4FBA6B7221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */; };
@@ -135,6 +136,7 @@
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>"; };
A4B4CC112223ED2A0045554B /* SAPlayerDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerDownloader.swift; sourceTree = "<group>"; };
A4FBA6B3221B74C900D5A353 /* SALockScreenInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SALockScreenInfo.swift; sourceTree = "<group>"; };
A4FBA6B6221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayerUpdateSubscription.swift; sourceTree = "<group>"; };
@@ -378,6 +380,7 @@
A470FE0725F9ADF800F135FF /* DownloadProgressDirector.swift */,
A470FE0625F9ADF800F135FF /* AudioClockDirector.swift */,
A470FE1B25F9AEB900F135FF /* AudioQueueDirector.swift */,
A4827770262A216C00B6918A /* StreamingDownloadDirector.swift */,
);
path = Directors;
sourceTree = "<group>";
@@ -553,6 +556,7 @@
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 */,
@@ -72,8 +72,8 @@
<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"/>
+14 -7
View File
@@ -7,24 +7,25 @@
//
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://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")!]
URL(string: "https://ice6.somafm.com/groovesalad-256-mp3")!]
var url: URL {
switch index {
case 0:
return URL(string: "https://www.fesliyanstudios.com/musicfiles/2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com/15SecVersion2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com.mp3")!
return urls[0]
case 1:
return URL(string: "https://chtbl.com/track/18338/traffic.libsyn.com/secure/acquired/acquired_-_armrev_2.mp3?dest-id=376122")!
return urls[1]
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")!
return urls[2]
default:
return URL(string: "https://www.fesliyanstudios.com/musicfiles/2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com/15SecVersion2019-04-23_-_Trusted_Advertising_-_www.fesliyanstudios.com.mp3")!
return urls[0]
}
}
@@ -33,9 +34,9 @@ struct AudioInfo: Hashable {
case 0:
return "Soundbite"
case 1:
return "Acquired"
return "Podcast"
case 2:
return "Y Combinator"
return "Radio"
default:
return "Soundbite"
}
@@ -44,6 +45,12 @@ struct AudioInfo: Hashable {
let artist: String = "SwiftAudioPlayer Sample App"
let releaseDate: Int = 1550790640
var lockscreenInfo: SALockScreenInfo {
get {
return SALockScreenInfo(title: self.title, artist: self.artist, artwork: nil, releaseDate: self.releaseDate)
}
}
var savedUrl: URL? {
get {
return savedUrls[index]
+13 -3
View File
@@ -259,7 +259,12 @@ class ViewController: UIViewController {
@IBAction func rateChanged(_ sender: Any) {
let speed = rateSlider.value
rateLabel.text = "rate: \(speed)x"
SAPlayer.shared.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) {
let reverb = reverbSlider.value
@@ -295,7 +300,7 @@ class ViewController: UIViewController {
self.currentUrlLocationLabel.text = "saved to: \(url.lastPathComponent)"
self.selectedAudio.addSavedUrl(url)
SAPlayer.shared.startSavedAudio(withSavedUrl: url)
SAPlayer.shared.startSavedAudio(withSavedUrl: url, mediaInfo: self.selectedAudio.lockscreenInfo)
self.lastPlayedAudioIndex = self.selectedAudio.index
}
})
@@ -311,7 +316,12 @@ class ViewController: UIViewController {
@IBAction func streamTouched(_ sender: Any) {
if !isStreaming {
SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url)
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
+5 -3
View File
@@ -14,6 +14,7 @@ Thus, using [AudioToolbox](https://developer.apple.com/documentation/audiotoolbo
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
@@ -88,7 +89,7 @@ override func viewDidLoad() {
}
}
```
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:
@@ -113,6 +114,7 @@ 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
@@ -146,9 +148,9 @@ Known supported file types are `.mp3` and `.wav`.
To set up player with audio to play, use either:
* `startSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo?)` to play audio that is saved on the device.
* `startRemoteAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo?)` to play audio streamed from a remote location.
* `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.
@@ -0,0 +1,53 @@
//
// 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()
var closures: DirectorThreadSafeClosures<Double> = DirectorThreadSafeClosures()
private init() {}
func create() {}
func clear() {
closures.clear()
}
func attach(closure: @escaping (Key, 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) {
closures.broadcast(key: key, payload: networkStreamProgress)
}
}
+50 -9
View File
@@ -59,7 +59,7 @@ 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.StreamEngine", qos: .userInitiated)
@@ -68,6 +68,7 @@ class AudioStreamEngine: AudioEngine {
//Fields
private var currentTimeOffset: TimeInterval = 0
private var streamChangeListenerId: UInt?
private var numberOfBuffersScheduledInTotal = 0 {
didSet {
@@ -133,15 +134,31 @@ 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, withPCMBufferSize: PCM_BUFFER_SIZE)
} catch {
delegate?.didError()
}
streamChangeListenerId = StreamingDownloadDirector.shared.attach { [weak self] (key, progress) in
guard let self = self else { return }
guard key == url.key 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))
@@ -149,22 +166,36 @@ class AudioStreamEngine: AudioEngine {
guard let self = self else { return }
guard self.playingStatus != .ended else { return }
self.pollForNextBufferRecursive()
self.updateNetworkBufferRange()
self.updateNeedle()
self.updateIsPlaying()
self.updateDuration()
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
//1. First time audio is finally parsed
//2. When we run to the end of the network buffer and we're waiting again
private func pollForNextBufferRecursive() {
private func pollForNextBuffer() {
guard shouldPollForNextBuffer else { return }
pollForNextBufferRecursive()
}
private func pollForNextBufferRecursive() {
do {
var nextScheduledBuffer: AVAudioPCMBuffer! = try converter.pullBuffer()
numberOfBuffersScheduledFromPoll += 1
@@ -174,7 +205,7 @@ class AudioStreamEngine: AudioEngine {
queue.async { [weak self] in
if #available(iOS 11.0, *) {
// to make sure the pcm buffers are properly free'd from memory we need to nil them after the player has used them
self?.playerNode.scheduleBuffer(nextScheduledBuffer, completionCallbackType: .dataRendered, completionHandler: { (_) in
self?.playerNode.scheduleBuffer(nextScheduledBuffer, completionCallbackType: .dataConsumed, completionHandler: { (_) in
nextScheduledBuffer = nil
self?.numberOfBuffersScheduledInTotal -= 1
self?.pollForNextBufferRecursive()
@@ -286,6 +317,16 @@ class AudioStreamEngine: AudioEngine {
super.pause()
}
override func play() {
queue.async { [weak self] in
self?.playHelperDispatchQueue()
}
}
private func playHelperDispatchQueue() {
super.play()
}
override func invalidate() {
super.invalidate()
converter.invalidate()
+79 -151
View File
@@ -27,74 +27,30 @@ 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
@@ -116,131 +72,103 @@ 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 let totalBytesExpected = pto.getTotalBytesExpected() {
self.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 {
Log.debug("already sent offset: \(offset) within network packet of range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)")
var bytesSent: UInt = 0
var current = wrappedNetworkData
// Sometimes the next data packet is smaller than a full audio chunk size, so we need to ensure we send up enough packets for the audio chunk. This prevented Issue #4 where tsreaming would randomly get stuck in a state needing more data up the chain.
// https://github.com/tanhakabir/SwiftAudioPlayer/issues/4
while bytesSent < largestPollingOffsetDifference {
if let next = current.next {
if !next.alreadySent {
Log.info("Sending next network packet with range: \(next.startOffset) to \(next.endOffset), have sent \(bytesSent) bytes so far from \(largestPollingOffsetDifference) bytes")
next.alreadySent = true
delegate?.shouldProcess(networkData: next.data)
}
bytesSent += next.byteCount
current = next
} else {
Log.debug("next package doesn't exist, bytes sent so far: \(bytesSent)")
return
}
}
return
}
Log.info("Found network packet to send with range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)")
wrappedNetworkData.alreadySent = true
delegate?.shouldProcess(networkData: wrappedNetworkData.data)
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 {
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
}
}
+1 -1
View File
@@ -85,7 +85,7 @@ class AudioConverter: AudioConvertable {
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 }
+105 -49
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()
}
}
}
@@ -100,16 +103,9 @@ 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
@@ -122,6 +118,8 @@ class AudioParser: AudioParsable {
//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
@@ -148,12 +146,30 @@ 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] (key, progress) in
guard let self = self else { return }
guard key == url.key 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 {
@@ -161,33 +177,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
@@ -199,9 +243,12 @@ 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? {
@@ -254,6 +301,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()
@@ -281,6 +334,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
@@ -288,27 +367,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
_ = 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)
}
}
}
@@ -65,7 +65,7 @@ func parserPacket(_ context: UnsafeMutableRawPointer, _ byteCount: UInt32, _ pac
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 { // not compressed audio (.wav)
Log.debug("uncompressed audio")
@@ -75,7 +75,7 @@ func parserPacket(_ context: UnsafeMutableRawPointer, _ byteCount: UInt32, _ pac
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)
}
}
+40 -42
View File
@@ -81,14 +81,14 @@ public class SAPlayer {
*/
public var volume: Float? {
get {
return player?.engine.mainMixerNode.volume
return player?.playerNode.volume
}
set {
guard let value = newValue else { return }
guard value >= 0.0 && value <= 1.0 else { return }
player?.engine.mainMixerNode.volume = value
player?.playerNode.volume = value
}
}
@@ -123,13 +123,6 @@ public class SAPlayer {
node.rate = value
playbackRateOfAudioChanged(rate: value)
// if skip silences was on, reset it to have the new rate
// TODO fix this to rate being broadcasted and handled in only Features.SkipSilences https://github.com/tanhakabir/SwiftAudioPlayer/issues/77
// if Features.SkipSilences.enabled && !(value == rate ?? 1.0 - 0.5 || value == rate ?? 1.0 + 0.5) {
// _ = Features.SkipSilences.disable()
// _ = Features.SkipSilences.enable()
// }
}
}
@@ -183,7 +176,7 @@ public class SAPlayer {
public var audioQueued: [URL] {
get {
return presenter.audioQueue.map { (queued) -> URL in
return queued.1
return queued.url
}
}
}
@@ -233,11 +226,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)
@@ -281,9 +270,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))"
}
@@ -297,6 +285,14 @@ public class SAPlayer {
}
}
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 {
/**
@@ -407,14 +403,14 @@ extension SAPlayer {
- Parameter mediaInfo: The media information of the audio to show on the lockscreen media player (optional).
*/
public func startSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
self.mediaInfo = mediaInfo
// 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()
presenter.handlePlaySavedAudio(withSavedUrl: url)
}
@available(*, deprecated, renamed: "startSavedAudio")
public func initializeSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
self.mediaInfo = mediaInfo
presenter.handlePlaySavedAudio(withSavedUrl: url)
}
/**
@@ -442,17 +438,18 @@ extension SAPlayer {
- 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 startRemoteAudio(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()
presenter.handlePlayStreamedAudio(withRemoteUrl: url, bitrate: bitrate)
self.mediaInfo = mediaInfo
presenter.handlePlayStreamedAudio(withRemoteUrl: url)
}
@available(*, deprecated, renamed: "startRemoteAudio")
public func initializeRemoteAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
self.mediaInfo = mediaInfo
presenter.handlePlayStreamedAudio(withRemoteUrl: url)
}
/**
@@ -466,18 +463,21 @@ extension SAPlayer {
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) {
presenter.handleQueueStreamedAudio(withRemoteUrl: url)
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 queuecan be both remote or on disk but once the queued audio starts playing it will start buffering and loading then. This means no guarantee for a 'gapless' playback where there might be several moments in between one audio ending and another starting due to buffering remote audio.
- Parameter withSavedUrl: The URL of the audio saved on the device.
- Parameter mediaInfo: The media information of the audio to show on the lockscreen media player (optional).
*/
public func queueSavedAudio(withSavedUrl url: URL) {
presenter.handleQueueSavedAudio(withSavedUrl: url)
public func queueSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
presenter.handleQueueSavedAudio(withSavedUrl: url, mediaInfo: mediaInfo)
}
/**
@@ -495,8 +495,8 @@ extension SAPlayer: SAPlayerDelegate {
player = AudioDiskEngine(withSavedUrl: url, delegate: presenter)
}
func startAudioStreamed(withRemoteUrl url: AudioURL) {
player = AudioStreamEngine(withRemoteUrl: url, delegate: presenter)
func startAudioStreamed(withRemoteUrl url: AudioURL, bitrate: SAPlayerBitrate) {
player = AudioStreamEngine(withRemoteUrl: url, delegate: presenter, bitrate: bitrate)
}
func clearEngine() {
@@ -515,12 +515,10 @@ extension SAPlayer: SAPlayerDelegate {
private func becomeDeviceAudioPlayer() {
do {
if #available(iOS 11.0, *) {
// try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, policy: .longForm, options: [])
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)")
+2 -1
View File
@@ -27,11 +27,12 @@ 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()
+18 -6
View File
@@ -24,18 +24,20 @@ extension SAPlayer {
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.
- Important: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
- 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
let originalRate = SAPlayer.shared.rate ?? 1.0
originalRate = SAPlayer.shared.rate ?? 1.0
let format = engine.mainMixerNode.outputFormat(forBus: 0)
@@ -58,10 +60,10 @@ extension SAPlayer {
Log.debug("meterLevel: \(meterLevel)")
if meterLevel < 0.6 { // below 0.6 decibels is below audible audio
SAPlayer.shared.rate = originalRate + 0.5
Log.test("speed up rate to \(String(describing: SAPlayer.shared.rate))")
Log.debug("speed up rate to \(String(describing: SAPlayer.shared.rate))")
} else {
SAPlayer.shared.rate = originalRate
Log.test("slow down rate to \(String(describing: SAPlayer.shared.rate))")
Log.debug("slow down rate to \(String(describing: SAPlayer.shared.rate))")
}
}
@@ -71,17 +73,27 @@ extension SAPlayer {
/**
Disable feature to skip silences in spoken word audio. The player will speed up the rate of audio playback when silence is detected. This can be called at any point of audio playback.
- Important: The first audio modifier must be the default `AVAudioUnitTimePitch` that comes with the SAPlayer for this feature to work.
- 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 {
// TODO fix disabling on speed up portion and being stuck at faster speed https://github.com/tanhakabir/SwiftAudioPlayer/issues/76
guard let engine = SAPlayer.shared.engine else { return false }
Log.info("disabling skip silences feature")
engine.mainMixerNode.removeTap(onBus: 0)
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
+30 -28
View File
@@ -28,6 +28,20 @@ import AVFoundation
import MediaPlayer
class SAPlayerPresenter {
struct QueueItem {
var loc: Location
var url: URL
var mediaInfo: SALockScreenInfo?
var bitrate: SAPlayerBitrate
init(loc: Location, url: URL, mediaInfo: SALockScreenInfo?, bitrate: SAPlayerBitrate = .high) {
self.loc = loc
self.url = url
self.mediaInfo = mediaInfo
self.bitrate = bitrate
}
}
enum Location {
case remote
case disk
@@ -41,14 +55,13 @@ 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 audioQueue: [(Location, URL)] = []
var audioQueue: [QueueItem] = []
init(delegate: SAPlayerDelegate?) {
self.delegate = delegate
@@ -70,7 +83,7 @@ class SAPlayerPresenter {
needle = nil
duration = nil
key = nil
mediaInfo = nil
delegate?.mediaInfo = nil
delegate?.clearLockScreenInfo()
AudioClockDirector.shared.detachFromChangesInDuration(withID: durationRef)
@@ -79,29 +92,21 @@ class SAPlayerPresenter {
}
func handlePlaySavedAudio(withSavedUrl url: URL) {
// Because we support queueing, we want to clear off any existing players.
// Therefore, instantiate new player every time, destroy any existing ones.
// This prevents a crash where an owning engine already exists.
handleClear()
attachForUpdates(url: url)
delegate?.startAudioDownloaded(withSavedUrl: url)
}
func handlePlayStreamedAudio(withRemoteUrl url: URL) {
// Because we support queueing, we want to clear off any existing players.
// Therefore, instantiate new player every time, destroy any existing ones.
// This prevents a crash where an owning engine already exists.
handleClear()
func handlePlayStreamedAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate) {
attachForUpdates(url: url)
delegate?.startAudioStreamed(withRemoteUrl: url)
delegate?.startAudioStreamed(withRemoteUrl: url, bitrate: bitrate)
}
func handleQueueStreamedAudio(withRemoteUrl url: URL) {
audioQueue.append((.remote, url))
func handleQueueStreamedAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo?, bitrate: SAPlayerBitrate) {
audioQueue.append(QueueItem(loc: .remote, url: url, mediaInfo: mediaInfo, bitrate: bitrate))
}
func handleQueueSavedAudio(withSavedUrl url: URL) {
audioQueue.append((.disk, url))
func handleQueueSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo?) {
audioQueue.append(QueueItem(loc: .disk, url: url, mediaInfo: mediaInfo))
}
private func attachForUpdates(url: URL) {
@@ -120,7 +125,7 @@ class SAPlayerPresenter {
self.delegate?.updateLockscreenPlaybackDuration(duration: duration)
self.duration = duration
self.delegate?.setLockScreenInfo(withMediaInfo: self.mediaInfo, duration: duration)
self.delegate?.setLockScreenInfo(withMediaInfo: self.delegate?.mediaInfo, duration: duration)
})
needleRef = AudioClockDirector.shared.attachToChangesInNeedle(closure: { [weak self] (key, needle) in
@@ -164,11 +169,6 @@ class SAPlayerPresenter {
delegate?.clearEngine()
detachFromUpdates()
}
@available(iOS 10.0, *)
func handleLockscreenInfo(info: SALockScreenInfo?) {
self.mediaInfo = info
}
}
//MARK:- Used by outside world including:
@@ -238,24 +238,26 @@ extension SAPlayerPresenter {
return
}
let nextAudioURL = audioQueue.removeFirst()
let key = nextAudioURL.1.key
let key = nextAudioURL.url.key
Log.info("getting ready to play \(nextAudioURL)")
AudioQueueDirector.shared.changeInQueue(key, url: nextAudioURL.1)
AudioQueueDirector.shared.changeInQueue(key, url: nextAudioURL.url)
handleClear()
delegate?.mediaInfo = nextAudioURL.mediaInfo
// We need to give a second to clean up the previous engine properly. Deinit takes some time.
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { [weak self] (_) in
guard let self = self else { return }
switch nextAudioURL.0 {
switch nextAudioURL.loc {
case .remote:
self.handlePlayStreamedAudio(withRemoteUrl: nextAudioURL.1)
self.handlePlayStreamedAudio(withRemoteUrl: nextAudioURL.url, bitrate: nextAudioURL.bitrate)
break
case .disk:
self.handlePlaySavedAudio(withSavedUrl: nextAudioURL.1)
self.handlePlaySavedAudio(withSavedUrl: nextAudioURL.url)
}
self.shouldPlayImmediately = true
+2 -2
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudioPlayer'
s.version = '4.1.0'
s.version = '5.0.4'
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,7 +26,7 @@ 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'