Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 578bbcdbe8 | |||
| 56c6483fc0 | |||
| fca0930b01 | |||
| 2f08ea4131 | |||
| 5ac825ed7a | |||
| 4856a30bb6 | |||
| f15f0f6eae | |||
| da19dd9488 | |||
| e57c6aabe5 | |||
| f6f9554b25 | |||
| e9bace4447 | |||
| 40b9d03ea8 | |||
| 3247b54c86 | |||
| d78de29daf |
@@ -14,10 +14,10 @@ jobs:
|
||||
name: Test iOS
|
||||
runs-on: macOS-latest
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_12.app/Contents/Developer
|
||||
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
|
||||
strategy:
|
||||
matrix:
|
||||
destination: ["OS=14.0,name=iPhone 11 Pro"] #, "OS=12.4,name=iPhone XS", "OS=11.4,name=iPhone X", "OS=10.3.1,name=iPhone SE"]
|
||||
destination: ["OS=latest,name=iPhone 13 Pro"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: iOS - ${{ matrix.destination }}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import UIKit
|
||||
|
||||
final class AppCoordinator {
|
||||
|
||||
enum Route {
|
||||
case equalizer
|
||||
}
|
||||
@@ -44,8 +43,8 @@ final class AppCoordinator {
|
||||
|
||||
private func routeTo(_ route: AppCoordinator.Route) {
|
||||
switch route {
|
||||
case .equalizer:
|
||||
showEqualizerControls()
|
||||
case .equalizer:
|
||||
showEqualizerControls()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import UIKit
|
||||
|
||||
class EqualizerViewController: UIViewController {
|
||||
|
||||
private lazy var enableTextLabel = UILabel()
|
||||
private lazy var enableButton = UISwitch()
|
||||
|
||||
@@ -22,7 +21,8 @@ class EqualizerViewController: UIViewController {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class EqualizerViewController: UIViewController {
|
||||
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
stackView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.8)
|
||||
stackView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.8),
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -86,7 +86,7 @@ class EqualizerViewController: UIViewController {
|
||||
|
||||
private func buildSliders() -> [UIView] {
|
||||
var sliders = [UIView]()
|
||||
for index in 0..<viewModel.numberOfBands() {
|
||||
for index in 0 ..< viewModel.numberOfBands() {
|
||||
guard let item = viewModel.band(at: index) else { continue }
|
||||
let slider = buildSlider(item: item, index: index)
|
||||
sliders.append(slider)
|
||||
|
||||
@@ -16,7 +16,6 @@ struct EQBand {
|
||||
}
|
||||
|
||||
final class EqualzerViewModel {
|
||||
|
||||
private var bands: [EQBand] = []
|
||||
|
||||
private let equalizerService: EqualizerService
|
||||
@@ -31,7 +30,7 @@ final class EqualzerViewModel {
|
||||
bands = equalizerService.bands.map { item in
|
||||
var measurement = item.frequency
|
||||
var frequency = String(Int(measurement))
|
||||
if item.frequency >= 1_000 {
|
||||
if item.frequency >= 1000 {
|
||||
measurement = item.frequency / 1000
|
||||
frequency = "\(String(Int(measurement)))K"
|
||||
}
|
||||
@@ -43,7 +42,7 @@ final class EqualzerViewModel {
|
||||
if enable {
|
||||
equalizerService.activate()
|
||||
} else {
|
||||
equalizerService.deactive()
|
||||
equalizerService.deactivate()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class PlayerViewController: UIViewController {
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(showEqualizer))
|
||||
|
||||
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
@@ -92,12 +92,13 @@ class PlayerViewController: UIViewController {
|
||||
|
||||
@objc private func addNowPlaylistItem() {
|
||||
let controller = UIAlertController(title: "Add new item", message: "", preferredStyle: .alert)
|
||||
controller.addTextField { (textField) in
|
||||
controller.addTextField { textField in
|
||||
textField.placeholder = "Insert url here"
|
||||
}
|
||||
let saveAction = UIAlertAction(title: "Save", style: .default) { [viewModel] action in
|
||||
let saveAction = UIAlertAction(title: "Save", style: .default) { [viewModel] _ in
|
||||
if let textfield = controller.textFields?.first,
|
||||
let text = textfield.text {
|
||||
let text = textfield.text
|
||||
{
|
||||
viewModel.add(urlString: text)
|
||||
}
|
||||
}
|
||||
@@ -105,7 +106,7 @@ class PlayerViewController: UIViewController {
|
||||
|
||||
controller.addAction(saveAction)
|
||||
controller.addAction(cancelAction)
|
||||
self.present(controller, animated: true, completion: nil)
|
||||
present(controller, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,14 +150,13 @@ extension PlayerViewController: UITableViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class PlaylistTableViewCell: UITableViewCell {
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
override init(style _: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,14 +18,15 @@ final class PlayerViewModel {
|
||||
private let playerService: AudioPlayerService
|
||||
private let playlistItemsService: PlaylistItemsService
|
||||
|
||||
private let routeTo: ((AppCoordinator.Route) -> Void)
|
||||
private let routeTo: (AppCoordinator.Route) -> Void
|
||||
private var currentPlayingItemIndex: Int?
|
||||
|
||||
var reloadContent: ((ReloadAction) -> Void)?
|
||||
|
||||
init(playlistItemsService: PlaylistItemsService,
|
||||
playerService: AudioPlayerService,
|
||||
routeTo: @escaping (AppCoordinator.Route) -> Void) {
|
||||
routeTo: @escaping (AppCoordinator.Route) -> Void)
|
||||
{
|
||||
self.playlistItemsService = playlistItemsService
|
||||
self.playerService = playerService
|
||||
self.routeTo = routeTo
|
||||
|
||||
@@ -10,7 +10,7 @@ import AVFoundation
|
||||
|
||||
final class EqualizerService {
|
||||
private let playerService: AudioPlayerService
|
||||
private let _freqs = [32, 64, 128, 250, 500, 1_000, 2_000, 4_000, 8_000, 16_000]
|
||||
private let _freqs = [32, 64, 128, 250, 500, 1000, 2000, 4000, 8000, 16000]
|
||||
private let eqUnit: AVAudioUnitEQ
|
||||
|
||||
var bands: [AVAudioUnitEQFilterParameters] {
|
||||
@@ -23,7 +23,7 @@ final class EqualizerService {
|
||||
self.playerService = playerService
|
||||
|
||||
eqUnit = AVAudioUnitEQ(numberOfBands: _freqs.count)
|
||||
for i in 0..<_freqs.count {
|
||||
for i in 0 ..< _freqs.count {
|
||||
eqUnit.bands[i].bypass = false
|
||||
eqUnit.bands[i].filterType = .parametric
|
||||
eqUnit.bands[i].frequency = Float(_freqs[i])
|
||||
@@ -45,7 +45,7 @@ final class EqualizerService {
|
||||
playerService.add(eqUnit)
|
||||
}
|
||||
|
||||
func deactive() {
|
||||
func deactivate() {
|
||||
isActivated = false
|
||||
playerService.remove(eqUnit)
|
||||
}
|
||||
|
||||
@@ -9,10 +9,9 @@
|
||||
import MediaPlayer
|
||||
|
||||
final class NowPlayingCenter {
|
||||
|
||||
private let infoCenter: MPNowPlayingInfoCenter
|
||||
|
||||
init(infoCenter: MPNowPlayingInfoCenter = .default()){
|
||||
init(infoCenter: MPNowPlayingInfoCenter = .default()) {
|
||||
self.infoCenter = infoCenter
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ final class PlaylistItemsService {
|
||||
func provideInitialPlaylistItems() -> [PlaylistItem] {
|
||||
let allCases = AudioContent.allCases
|
||||
let casesForQueueing: [AudioContent] = [.piano, .local, .khruangbin]
|
||||
let allItems = allCases.map { PlaylistItem.init(content: $0 , queues: false) }
|
||||
let casesForQueuingItems = casesForQueueing.map { PlaylistItem.init(content: $0 , queues: true) }
|
||||
let allItems = allCases.map { PlaylistItem(content: $0, queues: false) }
|
||||
let casesForQueuingItems = casesForQueueing.map { PlaylistItem(content: $0, queues: true) }
|
||||
return allItems + casesForQueuingItems
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'AudioStreaming'
|
||||
s.version = '0.8.0'
|
||||
s.version = '0.9.0'
|
||||
s.license = 'MIT'
|
||||
s.summary = 'An AudioPlayer/Streaming library for iOS written in Swift using AVAudioEngine.'
|
||||
s.homepage = 'https://github.com/dimitris-c/AudioStreaming'
|
||||
|
||||
@@ -811,7 +811,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.8.0;
|
||||
MARKETING_VERSION = 0.9.0;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
@@ -842,7 +842,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.8.0;
|
||||
MARKETING_VERSION = 0.9.0;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
import AVFoundation
|
||||
|
||||
extension AVAudioFormat {
|
||||
public extension AVAudioFormat {
|
||||
/// The underlying audio stream description.
|
||||
///
|
||||
/// This exposes the `pointee` value of the `UsafePointer<AudioStreamBasicDescription>`
|
||||
public var basicStreamDescription: AudioStreamBasicDescription {
|
||||
var basicStreamDescription: AudioStreamBasicDescription {
|
||||
return streamDescription.pointee
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ final class Retrier {
|
||||
private let maxInterval: Int
|
||||
private let timeoutTimer: DispatchTimerSource
|
||||
|
||||
/// Initiliazes a new object with the given parameters
|
||||
/// Initializes a new object with the given parameters
|
||||
/// - Parameters:
|
||||
/// - interval: The Mach absolute time at which to execute the dispatch source's event handler.
|
||||
/// - maxInterval: The maximum interval in which the internal timer will retry the callback.
|
||||
@@ -38,6 +38,7 @@ final class Retrier {
|
||||
|
||||
/// Cancels retrying
|
||||
func cancel() {
|
||||
interval = .seconds(1)
|
||||
timeoutTimer.removeHandler()
|
||||
timeoutTimer.suspend()
|
||||
}
|
||||
|
||||
@@ -9,12 +9,14 @@ import Network
|
||||
enum NetConnectionType: Equatable {
|
||||
case cellular(connected: Bool)
|
||||
case wifi(connected: Bool)
|
||||
case other(connected: Bool)
|
||||
case undetermined
|
||||
|
||||
var isConnected: Bool {
|
||||
switch self {
|
||||
case let .cellular(connected),
|
||||
let .wifi(connected):
|
||||
let .wifi(connected),
|
||||
let .other(connected):
|
||||
return connected
|
||||
default:
|
||||
return false
|
||||
@@ -39,15 +41,13 @@ final class NetStatusService: NetStatusProvider {
|
||||
network.currentPath.toNetConnectionType()
|
||||
}
|
||||
|
||||
private var currentConnectionType: NetConnectionType = .undetermined
|
||||
|
||||
private let network: NWPathMonitor
|
||||
|
||||
private let monitorQueue: DispatchQueue
|
||||
|
||||
init(network: NWPathMonitor) {
|
||||
self.network = network
|
||||
monitorQueue = DispatchQueue(label: "net.path.queue", qos: .background)
|
||||
monitorQueue = DispatchQueue(label: "net.path.queue", qos: .utility)
|
||||
}
|
||||
|
||||
deinit {
|
||||
@@ -59,20 +59,15 @@ final class NetStatusService: NetStatusProvider {
|
||||
/// - parameter connectionChange: A callback block to listen to changes of the network type, this skips duplicates.
|
||||
/// - Note: The callback will be executed on the main thread.
|
||||
func start(connectionChange: @escaping (NetConnectionType) -> Void) {
|
||||
network.pathUpdateHandler = { [weak self] path in
|
||||
guard let self = self else { return }
|
||||
let connecionType = path.toNetConnectionType()
|
||||
if self.currentConnectionType != connecionType {
|
||||
connectionChange(self.connectionType)
|
||||
self.currentConnectionType = self.connectionType
|
||||
}
|
||||
network.pathUpdateHandler = { path in
|
||||
let connectionType = path.toNetConnectionType()
|
||||
connectionChange(connectionType)
|
||||
}
|
||||
startIfNeeded()
|
||||
}
|
||||
|
||||
func stop() {
|
||||
network.cancel()
|
||||
network.pathUpdateHandler = nil
|
||||
}
|
||||
|
||||
func startIfNeeded() {
|
||||
@@ -85,12 +80,17 @@ extension NWPath {
|
||||
func toNetConnectionType() -> NetConnectionType {
|
||||
let isCellular = usesInterfaceType(.cellular)
|
||||
let isWifi = usesInterfaceType(.wifi)
|
||||
let isOther = usesInterfaceType(.loopback)
|
||||
|| usesInterfaceType(.other)
|
||||
|| usesInterfaceType(.wiredEthernet)
|
||||
let isConnected = status == .satisfied
|
||||
|
||||
if isCellular {
|
||||
return .cellular(connected: isConnected)
|
||||
} else if isWifi {
|
||||
return .wifi(connected: isConnected)
|
||||
} else if isOther {
|
||||
return .other(connected: isConnected)
|
||||
}
|
||||
|
||||
return .undetermined
|
||||
|
||||
@@ -13,12 +13,15 @@ enum DataStreamError: Error {
|
||||
public enum NetworkError: Error, Equatable {
|
||||
case failure(Error)
|
||||
case serverError
|
||||
case missingData
|
||||
public static func == (lhs: NetworkError, rhs: NetworkError) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.failure, failure):
|
||||
return true
|
||||
case (.serverError, .serverError):
|
||||
return true
|
||||
case (.missingData, .missingData):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -82,7 +85,7 @@ internal final class NetworkingClient {
|
||||
|
||||
// MARK: Private
|
||||
|
||||
/// Schedules the given `NetworkDataStream` to be performed immediatelly
|
||||
/// Schedules the given `NetworkDataStream` to be performed immediately
|
||||
/// - parameter stream: The `NetworkDataStream` object to be performed
|
||||
/// - parameter request: The `URLRequest` for the `stream`
|
||||
private func setupRequest(_ stream: NetworkDataStream, request: URLRequest) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A convenient type that holds tasks in a two-way manner, such as `URLSessionTask` to `NetworkDataStream` and reverved
|
||||
/// A convenient type that holds tasks in a two-way manner, such as `URLSessionTask` to `NetworkDataStream` and reversed
|
||||
struct BiMap<Left, Right> where Left: Hashable, Right: Hashable {
|
||||
private var leftToRight: [Left: Right] = [:]
|
||||
private var rightToLeft: [Right: Left] = [:]
|
||||
|
||||
@@ -109,7 +109,6 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
|
||||
func close() {
|
||||
retrierTimeout.cancel()
|
||||
netStatusService.stop()
|
||||
streamOperationQueue.isSuspended = false
|
||||
streamOperationQueue.cancelAllOperations()
|
||||
if let streamTask = streamRequest {
|
||||
@@ -152,8 +151,8 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
guard let self = self else { return }
|
||||
guard connection.isConnected else { return }
|
||||
if self.waitingForNetwork {
|
||||
self.seek(at: self.supportsSeek ? self.position : 0)
|
||||
self.waitingForNetwork = false
|
||||
self.seek(at: self.position)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,14 +160,13 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
private func performOpen(seek seekOffset: Int) {
|
||||
let urlRequest = buildUrlRequest(with: url, seekIfNeeded: seekOffset)
|
||||
|
||||
let request = networkingClient.stream(request: urlRequest)
|
||||
streamRequest = networkingClient.stream(request: urlRequest)
|
||||
.responseStream { [weak self] event in
|
||||
guard let self = self else { return }
|
||||
self.handleResponse(event: event)
|
||||
}
|
||||
.resume()
|
||||
|
||||
streamRequest = request
|
||||
metadataStreamProcessor.delegate = self
|
||||
}
|
||||
|
||||
@@ -179,8 +177,10 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
case let .response(urlResponse):
|
||||
parseResponseHeader(response: urlResponse)
|
||||
streamOperationQueue.isSuspended = false
|
||||
case let .stream(event):
|
||||
handleStreamEvent(event: event)
|
||||
case let .stream(.success(response)):
|
||||
handleSuccessfulStreamEvent(response: response)
|
||||
case let .stream(.failure(error)):
|
||||
handleFailedStreamEvent(error: error)
|
||||
case let .complete(event):
|
||||
if let error = event.error {
|
||||
delegate?.errorOccurred(source: self, error: error)
|
||||
@@ -193,50 +193,51 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
}
|
||||
}
|
||||
|
||||
private func handleStreamEvent(event: NetworkDataStream.StreamResult) {
|
||||
switch event {
|
||||
case let .success(value):
|
||||
if let audioData = value.data {
|
||||
addStreamOperation { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if self.shouldTryParsingIcycastHeaders {
|
||||
let (header, extractedAudio) = self.icycastHeadersProcessor.process(data: audioData)
|
||||
if let header = header {
|
||||
self.shouldTryParsingIcycastHeaders = false
|
||||
let parser = IcycastHeaderParser()
|
||||
self.parsedHeaderOutput = parser.parse(input: header)
|
||||
if let metadataStep = self.parsedHeaderOutput?.metadataStep {
|
||||
self.metadataStreamProcessor.metadataAvailable(step: metadataStep)
|
||||
}
|
||||
}
|
||||
let audioCount = self.processAudio(data: extractedAudio)
|
||||
self.relativePosition += audioCount
|
||||
return
|
||||
private func handleSuccessfulStreamEvent(response: NetworkDataStream.Response) {
|
||||
guard let audioData = response.data else {
|
||||
self.delegate?.errorOccurred(source: self, error: NetworkError.missingData)
|
||||
return
|
||||
}
|
||||
addStreamOperation { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if self.shouldTryParsingIcycastHeaders {
|
||||
let (header, extractedAudio) = self.icycastHeadersProcessor.process(data: audioData)
|
||||
if let header = header {
|
||||
self.shouldTryParsingIcycastHeaders = false
|
||||
let parser = IcycastHeaderParser()
|
||||
self.parsedHeaderOutput = parser.parse(input: header)
|
||||
if let metadataStep = self.parsedHeaderOutput?.metadataStep {
|
||||
self.metadataStreamProcessor.metadataAvailable(step: metadataStep)
|
||||
}
|
||||
let audioCount = self.processAudio(data: audioData)
|
||||
self.relativePosition += audioCount
|
||||
}
|
||||
}
|
||||
case .failure:
|
||||
if !netStatusService.isConnected {
|
||||
waitingForNetwork = true
|
||||
let audioCount = self.processAudio(data: extractedAudio)
|
||||
self.relativePosition += audioCount
|
||||
return
|
||||
}
|
||||
waitingForNetwork = false
|
||||
retryOnError()
|
||||
let audioCount = self.processAudio(data: audioData)
|
||||
self.relativePosition += audioCount
|
||||
}
|
||||
}
|
||||
|
||||
private func handleFailedStreamEvent(error: Error) {
|
||||
if !netStatusService.isConnected {
|
||||
waitingForNetwork = true
|
||||
return
|
||||
}
|
||||
waitingForNetwork = false
|
||||
retryOnError()
|
||||
}
|
||||
|
||||
/// Processing audio data, extracting metadata if needed.
|
||||
/// - Parameter data: The audio to be processed
|
||||
/// - Returns: An `Int` value representing the amount of audio data bytes.
|
||||
private func processAudio(data: Data) -> Int {
|
||||
if self.metadataStreamProcessor.canProcessMetadata {
|
||||
let extractedAudioData = self.metadataStreamProcessor.processMetadata(data: data)
|
||||
self.delegate?.dataAvailable(source: self, data: extractedAudioData)
|
||||
if metadataStreamProcessor.canProcessMetadata {
|
||||
let extractedAudioData = metadataStreamProcessor.processMetadata(data: data)
|
||||
delegate?.dataAvailable(source: self, data: extractedAudioData)
|
||||
return extractedAudioData.count
|
||||
} else {
|
||||
self.delegate?.dataAvailable(source: self, data: data)
|
||||
delegate?.dataAvailable(source: self, data: data)
|
||||
return data.count
|
||||
}
|
||||
}
|
||||
@@ -257,7 +258,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
supportsSeek = acceptRanges != "none"
|
||||
}
|
||||
|
||||
// check to see if we have metadata to proccess
|
||||
// check to see if we have metadata to process
|
||||
if let metadataStep = parsedHeaderOutput?.metadataStep {
|
||||
metadataStreamProcessor.metadataAvailable(step: metadataStep)
|
||||
}
|
||||
@@ -270,7 +271,10 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
if length >= 0 { seekOffset = length }
|
||||
delegate?.endOfFileOccurred(source: self)
|
||||
} else if statusCode >= 300 {
|
||||
delegate?.errorOccurred(source: self, error: NetworkError.serverError)
|
||||
delegate?.errorOccurred(
|
||||
source: self,
|
||||
error: NetworkError.serverError
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +291,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
urlRequest.addValue("1", forHTTPHeaderField: "Icy-MetaData")
|
||||
urlRequest.addValue("identity", forHTTPHeaderField: "Accept-Encoding")
|
||||
|
||||
if supportsSeek && seekOffset > 0 {
|
||||
if supportsSeek, seekOffset > 0 {
|
||||
urlRequest.addValue("bytes=\(seekOffset)-", forHTTPHeaderField: "Range")
|
||||
}
|
||||
return urlRequest
|
||||
@@ -296,7 +300,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
private func retryOnError() {
|
||||
retrierTimeout.retry { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.seek(at: self.position)
|
||||
self.seek(at: self.supportsSeek ? self.position : 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -312,7 +312,7 @@ open class AudioPlayer {
|
||||
}
|
||||
|
||||
/// Seeks the audio to the specified time.
|
||||
/// - Parameter time: A `Double` value specifing the time of the requested seek in seconds
|
||||
/// - Parameter time: A `Double` value specifying the time of the requested seek in seconds
|
||||
public func seek(to time: Double) {
|
||||
guard let playingEntry = playerContext.audioPlayingEntry else {
|
||||
return
|
||||
@@ -402,7 +402,7 @@ open class AudioPlayer {
|
||||
audioEngine.prepare()
|
||||
try audioEngine.start()
|
||||
} catch {
|
||||
Logger.error("⚠️ error setuping audio engine: %@", category: .generic, args: error.localizedDescription)
|
||||
Logger.error("⚠️ error setting up audio engine: %@", category: .generic, args: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,7 +470,7 @@ open class AudioPlayer {
|
||||
if let first = customAttachedNodes.first {
|
||||
audioEngine.connect(rateNode, to: first, format: nil)
|
||||
}
|
||||
for index in 0..<customAttachedNodes.count - 1 {
|
||||
for index in 0 ..< customAttachedNodes.count - 1 {
|
||||
let current = customAttachedNodes[index]
|
||||
let next = customAttachedNodes[index + 1]
|
||||
let format = current.inputFormat(forBus: 0)
|
||||
@@ -523,7 +523,7 @@ open class AudioPlayer {
|
||||
Logger.debug("engine stopped 🛑", category: .generic)
|
||||
}
|
||||
|
||||
/// Starts the audio player, reseting the buffers if requested
|
||||
/// Starts the audio player, resetting the buffers if requested
|
||||
///
|
||||
/// - parameter resetBuffers: A `Bool` value indicating if the buffers should be reset, prior starting the player.
|
||||
private func startPlayer(resetBuffers: Bool) {
|
||||
|
||||
@@ -13,11 +13,11 @@ public struct AudioPlayerConfiguration: Equatable {
|
||||
/// Number of seconds of audio required to before playback first starts.
|
||||
/// - note: Must be larger that `bufferSizeInSeconds`
|
||||
let secondsRequiredToStartPlaying: Double
|
||||
/// Number of seconds of audio required after seek occcurs.
|
||||
/// Number of seconds of audio required after seek occurs.
|
||||
let gracePeriodAfterSeekInSeconds: Double
|
||||
/// Number of seconds of audio required to before playback resumes after a buffer underun
|
||||
/// Number of seconds of audio required to before playback resumes after a buffer underrun
|
||||
/// - note: Must be larger that `bufferSizeInSeconds`
|
||||
let secondsRequiredToStartPlayingAfterBufferUnderun: Int
|
||||
let secondsRequiredToStartPlayingAfterBufferUnderrun: Int
|
||||
|
||||
/// Enables the internal logs
|
||||
let enableLogs: Bool
|
||||
@@ -26,7 +26,7 @@ public struct AudioPlayerConfiguration: Equatable {
|
||||
bufferSizeInSeconds: 10,
|
||||
secondsRequiredToStartPlaying: 1,
|
||||
gracePeriodAfterSeekInSeconds: 0.5,
|
||||
secondsRequiredToStartPlayingAfterBufferUnderun: 1,
|
||||
secondsRequiredToStartPlayingAfterBufferUnderrun: 1,
|
||||
enableLogs: false)
|
||||
/// Initializes the configuration for the `AudioPlayer`
|
||||
///
|
||||
@@ -35,22 +35,22 @@ public struct AudioPlayerConfiguration: Equatable {
|
||||
/// - parameter flushQueueOnSeek: All pending items will be flushed when seeking a track if this is set to `true`
|
||||
/// - parameter bufferSizeInSeconds: The size of the decompressed buffer.
|
||||
/// - parameter secondsRequiredToStartPlaying: Number of seconds of audio required to before playback first starts.
|
||||
/// - parameter gracePeriodAfterSeekInSeconds: Number of seconds of audio required after seek occcurs.
|
||||
/// - parameter secondsRequiredToStartPlayingAfterBufferUnderun: Number of seconds of audio required to before playback resumes after a buffer underun
|
||||
/// - parameter gracePeriodAfterSeekInSeconds: Number of seconds of audio required after seek occurs.
|
||||
/// - parameter secondsRequiredToStartPlayingAfterBufferUnderrun: Number of seconds of audio required to before playback resumes after a buffer underrun
|
||||
/// - parameter enableLogs: Enables the internal logs
|
||||
///
|
||||
public init(flushQueueOnSeek: Bool = true,
|
||||
bufferSizeInSeconds: Double = 10,
|
||||
secondsRequiredToStartPlaying: Double = 1,
|
||||
gracePeriodAfterSeekInSeconds: Double = 0.5,
|
||||
secondsRequiredToStartPlayingAfterBufferUnderun: Int = 1,
|
||||
secondsRequiredToStartPlayingAfterBufferUnderrun: Int = 1,
|
||||
enableLogs: Bool = false)
|
||||
{
|
||||
self.flushQueueOnSeek = flushQueueOnSeek
|
||||
self.bufferSizeInSeconds = bufferSizeInSeconds
|
||||
self.secondsRequiredToStartPlaying = secondsRequiredToStartPlaying
|
||||
self.gracePeriodAfterSeekInSeconds = gracePeriodAfterSeekInSeconds
|
||||
self.secondsRequiredToStartPlayingAfterBufferUnderun = secondsRequiredToStartPlayingAfterBufferUnderun
|
||||
self.secondsRequiredToStartPlayingAfterBufferUnderrun = secondsRequiredToStartPlayingAfterBufferUnderrun
|
||||
self.enableLogs = enableLogs
|
||||
}
|
||||
|
||||
@@ -70,15 +70,15 @@ public struct AudioPlayerConfiguration: Equatable {
|
||||
? defaults.gracePeriodAfterSeekInSeconds
|
||||
: self.gracePeriodAfterSeekInSeconds
|
||||
|
||||
let secondsRequiredToStartPlayingAfterBufferUnderun = self.secondsRequiredToStartPlayingAfterBufferUnderun == 0
|
||||
? defaults.secondsRequiredToStartPlayingAfterBufferUnderun
|
||||
: self.secondsRequiredToStartPlayingAfterBufferUnderun
|
||||
let secondsRequiredToStartPlayingAfterBufferUnderrun = self.secondsRequiredToStartPlayingAfterBufferUnderrun == 0
|
||||
? defaults.secondsRequiredToStartPlayingAfterBufferUnderrun
|
||||
: self.secondsRequiredToStartPlayingAfterBufferUnderrun
|
||||
|
||||
return AudioPlayerConfiguration(flushQueueOnSeek: flushQueueOnSeek,
|
||||
bufferSizeInSeconds: bufferSizeInSeconds,
|
||||
secondsRequiredToStartPlaying: secondsRequiredToStartPlaying,
|
||||
gracePeriodAfterSeekInSeconds: gracePeriodAfterSeekInSeconds,
|
||||
secondsRequiredToStartPlayingAfterBufferUnderun: secondsRequiredToStartPlayingAfterBufferUnderun,
|
||||
secondsRequiredToStartPlayingAfterBufferUnderrun: secondsRequiredToStartPlayingAfterBufferUnderrun,
|
||||
enableLogs: enableLogs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ final class AudioRendererContext {
|
||||
let canonicalStream = outputAudioFormat.basicStreamDescription
|
||||
|
||||
framesRequiredToStartPlaying = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.secondsRequiredToStartPlaying)
|
||||
framesRequiredAfterRebuffering = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.secondsRequiredToStartPlayingAfterBufferUnderun)
|
||||
framesRequiredAfterRebuffering = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.secondsRequiredToStartPlayingAfterBufferUnderrun)
|
||||
framesRequiredForDataAfterSeekPlaying = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.gracePeriodAfterSeekInSeconds)
|
||||
|
||||
let dataByteSize = Int(canonicalStream.mSampleRate * configuration.bufferSizeInSeconds) * Int(canonicalStream.mBytesPerFrame)
|
||||
@@ -75,8 +75,8 @@ private func allocateBufferList(dataByteSize: Int) -> UnsafeMutablePointer<Audio
|
||||
let _bufferList = AudioBufferList.allocate(maximumBuffers: 1)
|
||||
|
||||
_bufferList[0].mDataByteSize = UInt32(dataByteSize)
|
||||
let alingment = MemoryLayout<UInt8>.alignment
|
||||
let mData = UnsafeMutableRawPointer.allocate(byteCount: dataByteSize, alignment: alingment)
|
||||
let alignment = MemoryLayout<UInt8>.alignment
|
||||
let mData = UnsafeMutableRawPointer.allocate(byteCount: dataByteSize, alignment: alignment)
|
||||
_bufferList[0].mData = mData
|
||||
_bufferList[0].mNumberChannels = 2
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ final class AudioFileStreamProcessor {
|
||||
}
|
||||
guard let converter = audioConverter else {
|
||||
fileStreamCallback?(.raiseError(.audioSystemError(.fileStreamError(.unknownError))))
|
||||
return
|
||||
return
|
||||
}
|
||||
guard AudioConverterSetProperty(converter, kAudioConverterDecompressionMagicCookie, cookieSize, cookie) == noErr else {
|
||||
fileStreamCallback?(.raiseError(.audioSystemError(.fileStreamError(.unknownError))))
|
||||
@@ -231,9 +231,9 @@ final class AudioFileStreamProcessor {
|
||||
case kAudioFileStreamProperty_AudioDataByteCount:
|
||||
processDataByteCount(fileStream: fileStream)
|
||||
case kAudioFileStreamProperty_AudioDataPacketCount:
|
||||
proccessAudioDataPacketCount(fileStream: fileStream)
|
||||
processAudioDataPacketCount(fileStream: fileStream)
|
||||
case kAudioFileStreamProperty_ReadyToProducePackets:
|
||||
// check converter for discontious stream
|
||||
// check converter for discontinuous stream
|
||||
processReadyToProducePackets(fileStream: fileStream)
|
||||
case kAudioFileStreamProperty_FormatList:
|
||||
processFormatList(fileStream: fileStream)
|
||||
@@ -241,7 +241,7 @@ final class AudioFileStreamProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: AudioFileStream properties Proccessing
|
||||
// MARK: AudioFileStream properties Processing
|
||||
|
||||
private func processDataOffset(fileStream: AudioFileStreamID) {
|
||||
var offset: UInt64 = 0
|
||||
@@ -265,7 +265,7 @@ final class AudioFileStreamProcessor {
|
||||
var size = UInt32(4)
|
||||
AudioFileStreamGetProperty(fileStream, kAudioFileStreamProperty_FileFormat, &size, &fileFormat)
|
||||
if let stringFileFormat = String(data: Data(fileFormat), encoding: .utf8) {
|
||||
self.currentFileFormat = stringFileFormat
|
||||
currentFileFormat = stringFileFormat
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +308,7 @@ final class AudioFileStreamProcessor {
|
||||
entry.audioStreamState.dataByteCount = audioDataByteCount
|
||||
}
|
||||
|
||||
private func proccessAudioDataPacketCount(fileStream: AudioFileStreamID) {
|
||||
private func processAudioDataPacketCount(fileStream: AudioFileStreamID) {
|
||||
guard let entry = playerContext.audioReadingEntry else { return }
|
||||
var audioDataPacketCount: UInt64 = 0
|
||||
fileStreamGetProperty(value: &audioDataPacketCount, fileStream: fileStream, propertyId: kAudioFileStreamProperty_AudioDataPacketCount)
|
||||
@@ -378,7 +378,7 @@ final class AudioFileStreamProcessor {
|
||||
}
|
||||
|
||||
updateProcessedPackets(inPacketDescriptions: inPacketDescriptions,
|
||||
inNumberPackets: inNumberPackets)
|
||||
inNumberPackets: inNumberPackets)
|
||||
|
||||
var status: OSStatus = noErr
|
||||
packetProcess: while status == noErr {
|
||||
|
||||
@@ -33,7 +33,6 @@ public struct FilterEntry: Equatable {
|
||||
}
|
||||
|
||||
public protocol FrameFiltering {
|
||||
|
||||
/// A Boolean value indicating whether there are filter entries
|
||||
var hasEntries: Bool { get }
|
||||
|
||||
@@ -50,21 +49,21 @@ public protocol FrameFiltering {
|
||||
/// Adds a filter entry with the given parameters
|
||||
/// - Parameters:
|
||||
/// - named: The name of the entry to be added
|
||||
/// - filter: The block for the filter hanlding
|
||||
/// - filter: The block for the filter handling
|
||||
func add(entry named: String, filter: @escaping FilterCallback)
|
||||
|
||||
/// Adds a filter entry with the given parameters
|
||||
/// - Parameters:
|
||||
/// - name: The name for the new entry
|
||||
/// - filterName: The name of a previously added filters
|
||||
/// - filter: The block for the filter hanlding
|
||||
/// - filter: The block for the filter handling
|
||||
func add(entry named: String, after filterName: String, filter: @escaping FilterCallback)
|
||||
|
||||
/// Removes a filter entry
|
||||
/// - Parameter entry: An instance of `FilterEntry` to be removed
|
||||
func remove(entry: FilterEntry)
|
||||
|
||||
/// Attemps to remove a filter entry by its name
|
||||
/// Attempts to remove a filter entry by its name
|
||||
/// - Parameter named: A `String` representing the name of the filter entry
|
||||
func remove(entry named: String)
|
||||
|
||||
@@ -73,7 +72,6 @@ public protocol FrameFiltering {
|
||||
}
|
||||
|
||||
final class FrameFilterProcessor: NSObject, FrameFiltering {
|
||||
|
||||
public var hasEntries: Bool {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
return !entries.isEmpty
|
||||
|
||||
@@ -26,7 +26,6 @@ import Foundation
|
||||
/// ```
|
||||
|
||||
final class IcycastHeadersProcessor {
|
||||
|
||||
private var icecastHeaders = Data(capacity: 1024)
|
||||
private var searchComplete = false
|
||||
private var iceHeaderAvailable = false
|
||||
@@ -72,8 +71,9 @@ final class IcycastHeadersProcessor {
|
||||
|
||||
if icecastHeaders.count >= icyPrefix.count {
|
||||
// in case the first 4 chars are not "ICY " nor "HTTP" then we stop the flow
|
||||
if icecastHeaders[..<icyPrefix.count].elementsEqual(icyPrefix) == false &&
|
||||
icecastHeaders[..<httpPrefix.count].elementsEqual(httpPrefix) == false {
|
||||
if icecastHeaders[..<icyPrefix.count].elementsEqual(icyPrefix) == false,
|
||||
icecastHeaders[..<httpPrefix.count].elementsEqual(httpPrefix) == false
|
||||
{
|
||||
iceHeaderAvailable = false
|
||||
searchComplete = true
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ protocol MetadataStreamSourceDelegate: AnyObject {
|
||||
protocol MetadataStreamSource {
|
||||
var delegate: MetadataStreamSourceDelegate? { get set }
|
||||
|
||||
/// Returns `true` when the stream header has indicated that we can proccess metadata, otherwise `false`.
|
||||
/// Returns `true` when the stream header has indicated that we can process metadata, otherwise `false`.
|
||||
var canProcessMetadata: Bool { get }
|
||||
|
||||
/// Assigns the metadata step of the metadata
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
struct IcycastHeaderParser: Parser {
|
||||
|
||||
func parse(input: Data) -> HTTPHeaderParserOutput? {
|
||||
|
||||
guard let icecastValue = String(data: input, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -20,11 +20,15 @@ struct MetadataParser: Parser {
|
||||
guard let string = String(data: input, encoding: .utf8) else { return .failure(.unableToParse) }
|
||||
// remove added bytes (zeros) and separate the string on every ';' char
|
||||
let pairs = string.trimmingCharacters(in: CharacterSet(charactersIn: "\0")).components(separatedBy: ";")
|
||||
let temp: [String: String] = [:]
|
||||
let metadata = pairs.reduce(into: temp) { result, next in
|
||||
let paired = next.components(separatedBy: "=")
|
||||
if let key = paired.first,
|
||||
let value = paired.last?.replacingOccurrences(of: "'", with: ""), !key.isEmpty
|
||||
let metadata = pairs.reduce(into: [String: String]()) { result, next in
|
||||
let split = next.split(
|
||||
separator: "=",
|
||||
maxSplits: 1,
|
||||
omittingEmptySubsequences: true
|
||||
)
|
||||
.map(String.init)
|
||||
if let key = split.first,
|
||||
let value = split.last?.replacingOccurrences(of: "'", with: ""), !key.isEmpty
|
||||
{
|
||||
result[key] = value
|
||||
}
|
||||
|
||||
@@ -50,6 +50,26 @@ class MetadataParserTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testParserOutputsCorrectResultWhenEntryContainsEqualSign() throws {
|
||||
let string = "StreamTitle=\'Gramatik - In This Whole World (Original Mix)\';StreamUrl=\'\';track_info=\'k4Smc3RhdHVzoUihQNJiGp6BpHR5cGWhVKJpZKhNWDUxMTYzNISmc3RhdHVzoUOhQNJiGp9cpHR5cGWhVKJpZKhNWDUxMDM3MoSmc3RhdHVzoUOhQNJiGqAqpHR5cGWhVKJpZKhNWDUxMjA5Ng==\';UTC=\'20220226T214447.206\';\0\0\0\0\0\0\0\0\0"
|
||||
let data = string.data(using: .utf8)!
|
||||
|
||||
let parser = MetadataParser()
|
||||
|
||||
let output = parser.parse(input: data)
|
||||
|
||||
switch output {
|
||||
case let .success(values):
|
||||
XCTAssertFalse(values.isEmpty)
|
||||
XCTAssertEqual(values["StreamTitle"], "Gramatik - In This Whole World (Original Mix)")
|
||||
XCTAssertEqual(values["StreamUrl"], "")
|
||||
XCTAssertEqual(values["track_info"], "k4Smc3RhdHVzoUihQNJiGp6BpHR5cGWhVKJpZKhNWDUxMTYzNISmc3RhdHVzoUOhQNJiGp9cpHR5cGWhVKJpZKhNWDUxMDM3MoSmc3RhdHVzoUOhQNJiGqAqpHR5cGWhVKJpZKhNWDUxMjA5Ng==")
|
||||
XCTAssertEqual(values["UTC"], "20220226T214447.206")
|
||||
case .failure:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
func testParserOutputsFailureOnEmptyStringData() throws {
|
||||
let data = "".data(using: .utf8)!
|
||||
let parser = MetadataParser()
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ let package = Package(
|
||||
.target(
|
||||
name: "AudioStreaming",
|
||||
path: "AudioStreaming"
|
||||
)
|
||||
),
|
||||
],
|
||||
swiftLanguageVersions: [.v5]
|
||||
)
|
||||
|
||||
@@ -6,12 +6,12 @@ An AudioPlayer/Streaming library for iOS written in Swift, allows playback of on
|
||||
Under the hood `AudioStreaming` uses `AVAudioEngine` and `CoreAudio` for playback and provides an easy way of applying real-time [audio enhancements](https://developer.apple.com/documentation/avfaudio/audio_engine/audio_units).
|
||||
|
||||
#### Supported audio
|
||||
- Online streaming (Shoutcast/ICY streams) with metadata parsing
|
||||
- Online streaming (Shoutcast/ICY streams) with metadata parsing
|
||||
- AIFF, AIFC, WAVE, CAF, NeXT, ADTS, MPEG Audio Layer 3, AAC audio formats
|
||||
- M4A (_Optimized files only_)
|
||||
|
||||
Known limitations:
|
||||
- As described above non-optimised M4A files are not supported this is a limitation of [AudioFileStream Services](https://developer.apple.com/documentation/audiotoolbox/audio_file_stream_services?language=swift)
|
||||
- As described above non-optimised M4A files are not supported this is a limitation of [AudioFileStream Services](https://developer.apple.com/documentation/audiotoolbox/audio_file_stream_services?language=swift)
|
||||
|
||||
|
||||
# Requirements
|
||||
@@ -22,18 +22,18 @@ Known limitations:
|
||||
|
||||
### Playing an audio source over HTTP
|
||||
Note: You need to keep a reference to the `AudioPlayer` object
|
||||
```
|
||||
```swift
|
||||
let player = AudioPlayer()
|
||||
player.play(url: URL(string: "https://your-remote-url/to/audio-file.mp3")!)
|
||||
```
|
||||
|
||||
### Playing a local file
|
||||
```
|
||||
```swift
|
||||
let player = AudioPlayer()
|
||||
player.play(url: URL(fileURLWithPath: "your-local-path/to/audio-file.mp3")!)
|
||||
```
|
||||
### Queueing audio files
|
||||
```
|
||||
```swift
|
||||
let player = AudioPlayer()
|
||||
// when you want to queue a single url
|
||||
player.queue(url: URL(string: "https://your-remote-url/to/audio-file.mp3")!)
|
||||
@@ -46,7 +46,7 @@ player.queue(urls: [
|
||||
```
|
||||
|
||||
### Adjusting playback properties
|
||||
```
|
||||
```swift
|
||||
let player = AudioPlayer()
|
||||
player.play(url: URL(fileURLWithPath: "your-local-path/to/audio-file.mp3")!)
|
||||
// adjust the playback rate
|
||||
@@ -72,7 +72,7 @@ player.seek(to: 10)
|
||||
```
|
||||
|
||||
### Audio playback properties
|
||||
```
|
||||
```swift
|
||||
let player = AudioPlayer()
|
||||
player.play(url: URL(fileURLWithPath: "your-local-path/to/audio-file.mp3")!)
|
||||
|
||||
@@ -93,7 +93,7 @@ let state = player.stopReason
|
||||
You can inspect various callbacks by using the `delegate` property of the `AudioPlayer` to get informed about the player state, errors etc.
|
||||
View the [AudioPlayerDelegate](AudioStreaming/Streaming/AudioPlayer/AudioPlayerDelegate.swift) for more details
|
||||
|
||||
```
|
||||
```swift
|
||||
let player = AudioPlayer()
|
||||
player.play(url: URL(fileURLWithPath: "your-local-path/to/audio-file.mp3")!)
|
||||
|
||||
@@ -105,9 +105,9 @@ func audioPlayerStateChanged(player: AudioPlayer, with newState: AudioPlayerStat
|
||||
|
||||
### Adding custom audio nodes to AudioPlayer
|
||||
`AudioStreaming` provides an easy way to attach/remove `AVAudioNode`(s).
|
||||
This provides a powerful way of adjusting the playback audio with various enchncements
|
||||
This provides a powerful way of adjusting the playback audio with various enhancements
|
||||
|
||||
```
|
||||
```swift
|
||||
let reverbNode = AVAudioUnitReverb()
|
||||
reverbNode.wetDryMix = 50
|
||||
|
||||
@@ -126,11 +126,11 @@ The example project shows an example of adding a custom `AVAudioUnitEQ` node for
|
||||
|
||||
### Adding custom frame filter for recording and observation of audio data
|
||||
|
||||
`AudioStreaming` allow for custom frame fliters to be added so that recording or other observation for audio that's playing.
|
||||
`AudioStreaming` allow for custom frame filters to be added so that recording or other observation for audio that's playing.
|
||||
|
||||
You add a frame filter by using the `AudioPlayer`'s property `frameFiltering`.
|
||||
|
||||
```
|
||||
```swift
|
||||
let player = AudioPlayer()
|
||||
let format = player.mainMixerNode.outputFormat(forBus: 0)
|
||||
|
||||
@@ -152,12 +152,12 @@ let record = FilterEntry(name: "record") { buffer, when in
|
||||
|
||||
player.frameFiltering.add(entry: record)
|
||||
```
|
||||
See the `FrameFiltering` protocol for more ways of adding and removing frame filters.
|
||||
The callback in which you observe a filter will be run on a thread other than the main thread.
|
||||
See the `FrameFiltering` protocol for more ways of adding and removing frame filters.
|
||||
The callback in which you observe a filter will be run on a thread other than the main thread.
|
||||
|
||||
Under the hood the concrete class for frame filters, `FrameFilterProcessor` installs a tap on the `mainMixerNode` of `AVAudioEngine` in which all the added fitler will be called from.
|
||||
Under the hood the concrete class for frame filters, `FrameFilterProcessor` installs a tap on the `mainMixerNode` of `AVAudioEngine` in which all the added filter will be called from.
|
||||
|
||||
**Note** since the `mainMixerNode` is publicly exposed extra care should be taken to not install a tap directly and also use frame filters, this result in an exception because only one tap can be installed on an output node, as per Apple's documention.
|
||||
**Note** since the `mainMixerNode` is publicly exposed extra care should be taken to not install a tap directly and also use frame filters, this result in an exception because only one tap can be installed on an output node, as per Apple's documentation.
|
||||
|
||||
# Installation
|
||||
|
||||
@@ -199,5 +199,5 @@ Visit [installation instructions](https://github.com/Carthage/Carthage#adding-fr
|
||||
AudioStreaming is available under the MIT license. See the LICENSE file for more info.
|
||||
|
||||
# Attributions
|
||||
This librabry takes inspiration on the already battled-tested streaming library, [StreamingKit](https://github.com/tumtumtum/StreamingKit).
|
||||
This library takes inspiration on the already battled-tested streaming library, [StreamingKit](https://github.com/tumtumtum/StreamingKit).
|
||||
Big 🙏 to Thong Nguyen (@tumtumtum) and Matt Gallagher (@mattgallagher) for [AudioStreamer](https://github.com/mattgallagher/AudioStreamer)
|
||||
|
||||
Reference in New Issue
Block a user