Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 04296fa681 | |||
| 252ed947d2 | |||
| b5fdb5c54e | |||
| 1b4c0b0d3b | |||
| e0aa2a09a9 | |||
| ab0eb4f8eb | |||
| 99e7c65bbc | |||
| 9072259631 | |||
| eb9af1007a | |||
| 69d3a9c0c0 | |||
| 43821c68a9 | |||
| 610ff4c7f3 | |||
| 1fc533214f | |||
| 71f22c3e25 | |||
| 8a5e6d18cc | |||
| a7f53bfec9 | |||
| 7b10f0476b | |||
| 618da75339 |
@@ -17,10 +17,10 @@ class AudioController {
|
||||
let audioSessionController = AudioSessionController.shared
|
||||
|
||||
let sources: [AudioItem] = [
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/67b51d90ffddd6bb3f095059997021b589845f81?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "33 \"GOD\"", albumTitle: "22, A Million", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/081447adc23dad4f79ba4f1082615d1c56edf5e1?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "8 (circle)", albumTitle: "22, A Million", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/6f9999d909b017eabef97234dd7a206355720d9d?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "715 - CRΣΣKS", albumTitle: "22, A Million", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/bf9bdd403c67fdbe06a582e7b292487c8cfd1f7e?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "____45_____", albumTitle: "22, A Million", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency, artwork: #imageLiteral(resourceName: "22AMI"))
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/67b51d90ffddd6bb3f095059997021b589845f81?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "33 \"GOD\"", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/081447adc23dad4f79ba4f1082615d1c56edf5e1?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "8 (circle)", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/6f9999d909b017eabef97234dd7a206355720d9d?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "715 - CRΣΣKS", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
|
||||
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/bf9bdd403c67fdbe06a582e7b292487c8cfd1f7e?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "____45_____", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI"))
|
||||
]
|
||||
|
||||
init() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Quick
|
||||
import Nimble
|
||||
import AVFoundation
|
||||
|
||||
@testable import SwiftAudio
|
||||
|
||||
@@ -151,6 +152,34 @@ class AudioPlayerTests: QuickSpec {
|
||||
expect(audioPlayer.currentItem).toNot(beNil())
|
||||
})
|
||||
})
|
||||
|
||||
context("when setting the timePitchAlgorithm", {
|
||||
|
||||
beforeEach {
|
||||
audioPlayer.audioTimePitchAlgorithm = .timeDomain
|
||||
}
|
||||
|
||||
context("then loading an item", {
|
||||
beforeEach {
|
||||
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
|
||||
}
|
||||
|
||||
it("should have the applied timePitchAlgorithm", closure: {
|
||||
expect(audioPlayer.wrapper.currentItem?.audioTimePitchAlgorithm).to(equal(AVAudioTimePitchAlgorithm.timeDomain))
|
||||
})
|
||||
})
|
||||
|
||||
context("then loading a timepitching item", {
|
||||
beforeEach {
|
||||
let item = DefaultAudioItemTimePitching(audioUrl: Source.path, artist: nil, title: nil, albumTitle: nil, sourceType: .file, artwork: nil, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm.spectral)
|
||||
try? audioPlayer.load(item: item, playWhenReady: false)
|
||||
}
|
||||
|
||||
it("should have the applied timePitchAlgorithm", closure: {
|
||||
expect(audioPlayer.wrapper.currentItem?.audioTimePitchAlgorithm).to(equal(AVAudioTimePitchAlgorithm.spectral))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ struct Source {
|
||||
static let url: URL = URL(fileURLWithPath: Source.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
return DefaultAudioItem(audioUrl: Source.path, sourceType: .file, pitchAlgorithmType: .lowQualityZeroLatency)
|
||||
return DefaultAudioItem(audioUrl: Source.path, sourceType: .file)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,6 @@ struct ShortSource {
|
||||
static let url: URL = URL(fileURLWithPath: Source.path)
|
||||
|
||||
static func getAudioItem() -> AudioItem {
|
||||
return DefaultAudioItem(audioUrl: ShortSource.path, sourceType: .file, pitchAlgorithmType: .lowQualityZeroLatency)
|
||||
return DefaultAudioItem(audioUrl: ShortSource.path, sourceType: .file)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,13 @@ SwiftAudio is available through [CocoaPods](http://cocoapods.org). To install
|
||||
it, simply add the following line to your Podfile:
|
||||
|
||||
```ruby
|
||||
pod 'SwiftAudio', '~> 0.4.2'
|
||||
pod 'SwiftAudio', '~> 0.5.0'
|
||||
```
|
||||
|
||||
### Carthage
|
||||
SwiftAudio supports [Carthage](https://github.com/Carthage/Carthage). Add this to your Cartfile:
|
||||
```ruby
|
||||
github "jorgenhenrichsen/SwiftAudio" ~> 0.4.2
|
||||
github "jorgenhenrichsen/SwiftAudio" ~> 0.5.0
|
||||
```
|
||||
Then follow the rest of Carthage instructions on [adding a framework](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).
|
||||
|
||||
@@ -39,7 +39,7 @@ Then follow the rest of Carthage instructions on [adding a framework](https://gi
|
||||
To get started playing some audio:
|
||||
```swift
|
||||
let player = AudioPlayer()
|
||||
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency)
|
||||
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
|
||||
player.load(item: audioItem, playWhenReady: true) // Load the item and start playing when the player is ready.
|
||||
```
|
||||
|
||||
@@ -49,7 +49,7 @@ Implement `AudioPlayerDelegate` to get notified about useful events and updates
|
||||
The `QueuedAudioPlayer` is asubclass of `AudioPlayer` that maintains a queue of audio tracks.
|
||||
```swift
|
||||
let player = QueuedAudioPlayer()
|
||||
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency)
|
||||
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
|
||||
player.add(item: audioItem, playWhenReady: true) // Since this is the first item, we can supply playWhenReady: true to immedietaly start playing when the item is loaded.
|
||||
```
|
||||
|
||||
@@ -77,6 +77,7 @@ Current options for configuring the `AudioPlayer`:
|
||||
- `volume`
|
||||
- `isMuted`
|
||||
- `rate`
|
||||
- `audioTimePitchAlgorithm`: This value decides the `AVAudioTimePitchAlgorithm` used for each `AudioItem`. Implement `TimePitching` in your `AudioItem`-subclass to override individually for each `AudioItem`.
|
||||
|
||||
### Audio Session
|
||||
Remember to activate an audio session with an appropriate category for your app. This can be done with `AudioSessionController`:
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudio'
|
||||
s.version = '0.4.2'
|
||||
s.version = '0.5.0'
|
||||
s.summary = 'Easy audio streaming for iOS'
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
|
||||
@@ -88,7 +88,11 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
|
||||
return seconds
|
||||
}
|
||||
return 0
|
||||
else if let seconds = currentItem?.loadedTimeRanges.first?.timeRangeValue.duration.seconds,
|
||||
!seconds.isNaN {
|
||||
return seconds
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
weak var delegate: AVPlayerWrapperDelegate? = nil
|
||||
@@ -165,12 +169,13 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
|
||||
// MARK: - Util
|
||||
|
||||
private func reset(soft: Bool) {
|
||||
playerItemObserver.stopObservingCurrentItem()
|
||||
playerTimeObserver.unregisterForBoundaryTimeEvents()
|
||||
playerItemNotificationObserver.stopObservingCurrentItem()
|
||||
|
||||
if !soft {
|
||||
avPlayer.replaceCurrentItem(with: nil)
|
||||
}
|
||||
|
||||
playerTimeObserver.unregisterForBoundaryTimeEvents()
|
||||
playerItemNotificationObserver.stopObservingCurrentItem()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ protocol AVPlayerWrapperProtocol {
|
||||
|
||||
var reasonForWaitingToPlay: AVPlayer.WaitingReason? { get }
|
||||
|
||||
var rate: Float { get set }
|
||||
|
||||
var rate: Float { get set }
|
||||
|
||||
var delegate: AVPlayerWrapperDelegate? { get set }
|
||||
|
||||
@@ -35,7 +35,7 @@ protocol AVPlayerWrapperProtocol {
|
||||
var isMuted: Bool { get set }
|
||||
|
||||
var automaticallyWaitsToMinimizeStalling: Bool { get set }
|
||||
|
||||
|
||||
|
||||
func play()
|
||||
|
||||
|
||||
@@ -20,14 +20,19 @@ public protocol AudioItem {
|
||||
func getTitle() -> String?
|
||||
func getAlbumTitle() -> String?
|
||||
func getSourceType() -> SourceType
|
||||
func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm
|
||||
func getArtwork(_ handler: @escaping (UIImage?) -> Void)
|
||||
|
||||
}
|
||||
|
||||
public struct DefaultAudioItem: AudioItem {
|
||||
/// Make your `AudioItem`-subclass conform to this protocol to control which AVAudioTimePitchAlgorithm is used for each item.
|
||||
public protocol TimePitching {
|
||||
|
||||
func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm
|
||||
|
||||
}
|
||||
|
||||
public class DefaultAudioItem: AudioItem {
|
||||
|
||||
public var audioUrl: String
|
||||
|
||||
public var artist: String?
|
||||
@@ -38,17 +43,14 @@ public struct DefaultAudioItem: AudioItem {
|
||||
|
||||
public var sourceType: SourceType
|
||||
|
||||
public var pitchAlgorithmType: AVAudioTimePitchAlgorithm
|
||||
|
||||
public var artwork: UIImage?
|
||||
|
||||
public init(audioUrl: String, artist: String? = nil, title: String? = nil, albumTitle: String? = nil, sourceType: SourceType, pitchAlgorithmType: AVAudioTimePitchAlgorithm, artwork: UIImage? = nil) {
|
||||
public init(audioUrl: String, artist: String? = nil, title: String? = nil, albumTitle: String? = nil, sourceType: SourceType, artwork: UIImage? = nil) {
|
||||
self.audioUrl = audioUrl
|
||||
self.artist = artist
|
||||
self.title = title
|
||||
self.albumTitle = albumTitle
|
||||
self.sourceType = sourceType
|
||||
self.pitchAlgorithmType = pitchAlgorithmType
|
||||
self.artwork = artwork
|
||||
}
|
||||
|
||||
@@ -71,13 +73,29 @@ public struct DefaultAudioItem: AudioItem {
|
||||
public func getSourceType() -> SourceType {
|
||||
return sourceType
|
||||
}
|
||||
|
||||
public func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm {
|
||||
return pitchAlgorithmType
|
||||
}
|
||||
|
||||
|
||||
public func getArtwork(_ handler: @escaping (UIImage?) -> Void) {
|
||||
handler(artwork)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// An AudioItem that also conforms to the `TimePitching`-protocol
|
||||
public class DefaultAudioItemTimePitching: DefaultAudioItem, TimePitching {
|
||||
|
||||
public var pitchAlgorithmType: AVAudioTimePitchAlgorithm
|
||||
|
||||
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
|
||||
self.pitchAlgorithmType = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
|
||||
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm) {
|
||||
self.pitchAlgorithmType = audioTimePitchAlgorithm
|
||||
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
|
||||
}
|
||||
|
||||
public func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm {
|
||||
return pitchAlgorithmType
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,12 @@ public protocol AudioPlayerDelegate: class {
|
||||
|
||||
public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
|
||||
private var wrapper: AVPlayerWrapperProtocol
|
||||
private var _wrapper: AVPlayerWrapperProtocol
|
||||
|
||||
/// The wrapper around the underlying AVPlayer
|
||||
var wrapper: AVPlayerWrapperProtocol {
|
||||
return _wrapper
|
||||
}
|
||||
|
||||
public let nowPlayingInfoController: NowPlayingInfoController
|
||||
public let remoteCommandController: RemoteCommandController
|
||||
@@ -44,6 +49,12 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
*/
|
||||
public var automaticallyUpdateNowPlayingInfo: Bool = true
|
||||
|
||||
/**
|
||||
Controls the time pitch algorithm applied to each item loaded into the player.
|
||||
If the loaded `AudioItem` conforms to `TimePitcher`-protocol this will be overriden.
|
||||
*/
|
||||
public var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
|
||||
|
||||
/**
|
||||
Default remote commands to use for each playing item
|
||||
*/
|
||||
@@ -84,7 +95,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
*/
|
||||
public var bufferDuration: TimeInterval {
|
||||
get { return wrapper.bufferDuration }
|
||||
set { wrapper.bufferDuration = newValue }
|
||||
set { _wrapper.bufferDuration = newValue }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,7 +103,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
*/
|
||||
public var timeEventFrequency: TimeEventFrequency {
|
||||
get { return wrapper.timeEventFrequency }
|
||||
set { wrapper.timeEventFrequency = newValue }
|
||||
set { _wrapper.timeEventFrequency = newValue }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,22 +111,22 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
*/
|
||||
public var automaticallyWaitsToMinimizeStalling: Bool {
|
||||
get { return wrapper.automaticallyWaitsToMinimizeStalling }
|
||||
set { wrapper.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
set { _wrapper.automaticallyWaitsToMinimizeStalling = newValue }
|
||||
}
|
||||
|
||||
public var volume: Float {
|
||||
get { return wrapper.volume }
|
||||
set { wrapper.volume = newValue }
|
||||
set { _wrapper.volume = newValue }
|
||||
}
|
||||
|
||||
public var isMuted: Bool {
|
||||
get { return wrapper.isMuted }
|
||||
set { wrapper.isMuted = newValue }
|
||||
set { _wrapper.isMuted = newValue }
|
||||
}
|
||||
|
||||
public var rate: Float {
|
||||
get { return wrapper.rate }
|
||||
set { wrapper.rate = newValue }
|
||||
set { _wrapper.rate = newValue }
|
||||
}
|
||||
|
||||
// MARK: - Init
|
||||
@@ -128,11 +139,11 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
public init(avPlayer: AVPlayer = AVPlayer(),
|
||||
nowPlayingInfoController: NowPlayingInfoController = NowPlayingInfoController(),
|
||||
remoteCommandController: RemoteCommandController = RemoteCommandController()) {
|
||||
self.wrapper = AVPlayerWrapper(avPlayer: avPlayer)
|
||||
self._wrapper = AVPlayerWrapper(avPlayer: avPlayer)
|
||||
self.nowPlayingInfoController = nowPlayingInfoController
|
||||
self.remoteCommandController = remoteCommandController
|
||||
|
||||
self.wrapper.delegate = self
|
||||
self._wrapper.delegate = self
|
||||
self.remoteCommandController.audioPlayer = self
|
||||
}
|
||||
|
||||
@@ -158,7 +169,12 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
|
||||
wrapper.load(from: URL(fileURLWithPath: item.getSourceUrl()), playWhenReady: playWhenReady)
|
||||
}
|
||||
|
||||
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
|
||||
if let item = item as? TimePitching {
|
||||
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
|
||||
}
|
||||
else {
|
||||
wrapper.currentItem?.audioTimePitchAlgorithm = audioTimePitchAlgorithm
|
||||
}
|
||||
|
||||
self._currentItem = item
|
||||
self.updateMetaValues(item: item)
|
||||
|
||||
@@ -27,6 +27,7 @@ class AVPlayerItemObserver: NSObject {
|
||||
|
||||
private struct AVPlayerItemKeyPath {
|
||||
static let duration = #keyPath(AVPlayerItem.duration)
|
||||
static let loadedTimeRanges = #keyPath(AVPlayerItem.loadedTimeRanges)
|
||||
}
|
||||
|
||||
var isObserving: Bool = false
|
||||
@@ -53,11 +54,13 @@ class AVPlayerItemObserver: NSObject {
|
||||
self.isObserving = true
|
||||
self.observingItem = item
|
||||
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context)
|
||||
item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, options: [.new], context: &AVPlayerItemObserver.context)
|
||||
}
|
||||
}
|
||||
|
||||
private func stopObservingCurrentItem() {
|
||||
func stopObservingCurrentItem() {
|
||||
observingItem?.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, context: &AVPlayerItemObserver.context)
|
||||
observingItem?.removeObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, context: &AVPlayerItemObserver.context)
|
||||
self.isObserving = false
|
||||
self.observingItem = nil
|
||||
}
|
||||
@@ -73,8 +76,12 @@ class AVPlayerItemObserver: NSObject {
|
||||
if let duration = change?[.newKey] as? CMTime {
|
||||
self.delegate?.item(didUpdateDuration: duration.seconds)
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
||||
case AVPlayerItemKeyPath.loadedTimeRanges:
|
||||
if let ranges = change?[.newKey] as? [NSValue], let duration = ranges.first?.timeRangeValue.duration {
|
||||
self.delegate?.item(didUpdateDuration: duration.seconds)
|
||||
}
|
||||
default: break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user