Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 930509d6be | |||
| c513c723ed | |||
| b34a264aec | |||
| a83c2f702f | |||
| 8644bf24fb | |||
| 69a979cb98 | |||
| 6ba43e70ea | |||
| 6f19009000 | |||
| 64677ad6ce | |||
| 3894309706 | |||
| e44f16258f | |||
| 1e3cf35b7b | |||
| 4bfb3f1774 | |||
| e056336955 | |||
| 64d2959a27 | |||
| eb1675d4fd | |||
| ca7e48cbe7 | |||
| 653f2817bc | |||
| edff806647 | |||
| c47d623118 |
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftAudioPlayer
|
||||
|
||||
struct AudioInfo: Hashable {
|
||||
var index: Int = 0
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -312,9 +317,9 @@ class ViewController: UIViewController {
|
||||
@IBAction func streamTouched(_ sender: Any) {
|
||||
if !isStreaming {
|
||||
if selectedAudio.index == 2 { // radio
|
||||
SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url, bitrate: .low)
|
||||
SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url, bitrate: .low, mediaInfo: selectedAudio.lockscreenInfo)
|
||||
} else {
|
||||
SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url)
|
||||
SAPlayer.shared.startRemoteAudio(withRemoteUrl: selectedAudio.url, mediaInfo: selectedAudio.lockscreenInfo)
|
||||
}
|
||||
|
||||
lastPlayedAudioIndex = selectedAudio.index
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -105,7 +105,7 @@ class AudioParser: AudioParsable {
|
||||
|
||||
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
|
||||
@@ -118,6 +118,7 @@ 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
|
||||
|
||||
/**
|
||||
@@ -152,21 +153,23 @@ class AudioParser: AudioParsable {
|
||||
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
|
||||
if self.fileAudioFormat == nil {
|
||||
self.processNextDataPacket()
|
||||
} else if self.audioPackets.count - self.lastSentAudioPacketIndex < self.MIN_PACKETS_TO_HAVE_AVAILABLE_BEFORE_THROTTLING_PARSING {
|
||||
self.processNextDataPacket()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.throttler = AudioThrottler(withRemoteUrl: url, withDelegate: self)
|
||||
|
||||
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 {
|
||||
@@ -187,31 +190,48 @@ class AudioParser: AudioParsable {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
lastSentAudioPacketIndex = Int(packetIndex)
|
||||
return audioPackets[Int(packetIndex)]
|
||||
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) {
|
||||
if index > audioPackets.count - MIN_PACKETS_TO_HAVE_AVAILABLE_BEFORE_THROTTLING_PARSING {
|
||||
processNextDataPacket()
|
||||
lockQueue.sync {
|
||||
if index > self.audioPackets.count - self.MIN_PACKETS_TO_HAVE_AVAILABLE_BEFORE_THROTTLING_PARSING {
|
||||
self.processNextDataPacket()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -223,10 +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)
|
||||
processNextDataPacket()
|
||||
self.processNextDataPacket()
|
||||
}
|
||||
|
||||
private func getOffset(fromPacketIndex index: AVAudioPacketCount) -> UInt64? {
|
||||
@@ -279,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()
|
||||
|
||||
@@ -311,7 +339,9 @@ class AudioParser: AudioParsable {
|
||||
guard let self = self else { return }
|
||||
guard let data = d else { return }
|
||||
|
||||
Log.debug("processing data count: \(data.count) :: already had \(self.audioPackets.count) audio packets")
|
||||
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!
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+27
-38
@@ -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))"
|
||||
}
|
||||
@@ -415,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)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -454,14 +442,14 @@ extension SAPlayer {
|
||||
- Parameter mediaInfo: The media information of the audio to show on the lockscreen media player (optional).
|
||||
*/
|
||||
public func startRemoteAudio(withRemoteUrl url: URL, bitrate: SAPlayerBitrate = .high, 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.handlePlayStreamedAudio(withRemoteUrl: url, bitrate: bitrate)
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed: "startRemoteAudio")
|
||||
public func initializeRemoteAudio(withRemoteUrl url: URL, mediaInfo: SALockScreenInfo? = nil) {
|
||||
self.mediaInfo = mediaInfo
|
||||
presenter.handlePlayStreamedAudio(withRemoteUrl: url, bitrate: .high)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -475,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)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -524,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)")
|
||||
|
||||
@@ -27,6 +27,7 @@ import Foundation
|
||||
import CoreMedia
|
||||
|
||||
protocol SAPlayerDelegate: AnyObject, LockScreenViewProtocol {
|
||||
var mediaInfo: SALockScreenInfo? { get set }
|
||||
var skipForwardSeconds: Double { get set }
|
||||
var skipBackwardSeconds: Double { get set }
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, bitrate: SAPlayerBitrate) {
|
||||
// Because we support queueing, we want to clear off any existing players.
|
||||
// Therefore, instantiate new player every time, destroy any existing ones.
|
||||
// This prevents a crash where an owning engine already exists.
|
||||
handleClear()
|
||||
attachForUpdates(url: url)
|
||||
delegate?.startAudioStreamed(withRemoteUrl: url, 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, bitrate: .high) // TODO fix to add option for low birate
|
||||
self.handlePlayStreamedAudio(withRemoteUrl: nextAudioURL.url, bitrate: nextAudioURL.bitrate)
|
||||
break
|
||||
case .disk:
|
||||
self.handlePlaySavedAudio(withSavedUrl: nextAudioURL.1)
|
||||
self.handlePlaySavedAudio(withSavedUrl: nextAudioURL.url)
|
||||
}
|
||||
|
||||
self.shouldPlayImmediately = true
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudioPlayer'
|
||||
s.version = '5.0.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'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user