Compare commits

..

25 Commits

Author SHA1 Message Date
dimitris-c 578bbcdbe8 Version bump
Signed-off-by: dimitris-c <d.chatzieleftheriou@gmail.com>
2022-02-28 17:46:41 +02:00
dimitris-c 56c6483fc0 Some nits
Signed-off-by: dimitris-c <d.chatzieleftheriou@gmail.com>
2022-02-28 17:46:28 +02:00
Dimitris C fca0930b01 Fixes remote audio source network issues (#35) 2022-02-27 00:05:15 +02:00
Dimitrios C 2f08ea4131 Fixes AudioExample from not compiling 2022-02-21 23:08:31 +02:00
Dimitris C 5ac825ed7a Update swift.yml 2022-02-21 22:59:05 +02:00
Dimitris C 4856a30bb6 Update swift.yml 2022-02-21 22:56:57 +02:00
Dimitris Apostolou f15f0f6eae Fix typos (#34) 2022-02-20 22:40:50 +02:00
Dimitris C da19dd9488 Update swift.yml 2022-02-20 22:30:45 +02:00
Dimitris C e57c6aabe5 Update README.md 2021-12-16 17:46:29 +02:00
Dimitris C f6f9554b25 Update swift.yml 2021-12-07 12:44:03 +02:00
Dimitris C e9bace4447 Update swift.yml 2021-12-07 12:42:33 +02:00
Dimitris C 40b9d03ea8 Update swift.yml 2021-12-07 12:38:06 +02:00
Dimitris C 3247b54c86 Update swift.yml 2021-12-07 12:36:04 +02:00
Dimitris C d78de29daf Update swift.yml 2021-12-06 20:03:09 +02:00
dimitris-c 0758c14909 Bumping version 2021-12-06 20:01:12 +02:00
Dimitris C 03c6a7692c Fixes an issue where the memory is not released after paused audio (#33) 2021-12-06 19:58:50 +02:00
dimitris-c 02a3606185 Fixes unit tests 2021-10-05 18:55:34 +03:00
Dimitris C 7e45a7b2f5 Removes unneeded DispatchTimerSource (#32)
Correct Spelling issues
Fix an issue in RemoteAudioSource when icycast headers are not available
2021-10-05 18:32:55 +03:00
Dimitris C 30b4189778 Bumping version (#30) 2021-09-07 15:47:59 +03:00
Dimitris C 8bdc2a64f7 Fixes a memory leak issue in RemoteAudioSource (#29) 2021-09-07 15:36:51 +03:00
Dimitris C 65de9d90c0 Version bump (#19)
* Bumps version

* Bumps version

Co-authored-by: Dimitrios C <dimitrisc@DimitrisC-Macbook-Pro.local>
2021-05-25 00:01:31 +03:00
Dimitris C 217a88f171 Adds frame filters to allow recording, monitoring, and observation of audio (#18)
* Adds frame filters feature

* nit

* Updates Readme file

Co-authored-by: Dimitrios C <dimitrisc@DimitrisC-Macbook-Pro.local>
2021-05-24 23:58:16 +03:00
Dimitrios C 566dc86f3f Bumps version 2021-05-18 23:57:01 +03:00
Dimitrios C d8aa58525c Makes AudioPlayer an open class
Exposes AudioEngine’s mainMixerNode
Added missing documentation
2021-05-18 23:54:43 +03:00
Dimitris C 8197db0016 Update README.md 2021-04-12 13:10:44 +03:00
40 changed files with 525 additions and 282 deletions
+2 -2
View File
@@ -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()
}
}
@@ -153,7 +153,7 @@ extension PlayerControlsViewModel: AudioPlayerServiceDelegate {
updateContent?(.updateMetadata(""))
}
func errorOccured(error _: AudioPlayerError) {}
func errorOccurred(error _: AudioPlayerError) {}
func metadataReceived(metadata: [String: String]) {
guard !metadata.isEmpty else { return }
@@ -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
@@ -96,7 +97,7 @@ extension PlayerViewModel: AudioPlayerServiceDelegate {
}
}
func errorOccured(error _: AudioPlayerError) {
func errorOccurred(error _: AudioPlayerError) {
currentPlayingItemIndex = nil
}
@@ -13,7 +13,7 @@ protocol AudioPlayerServiceDelegate: AnyObject {
func didStartPlaying()
func didStopPlaying()
func statusChanged(status: AudioPlayerState)
func errorOccured(error: AudioPlayerError)
func errorOccurred(error: AudioPlayerError)
func metadataReceived(metadata: [String: String])
}
@@ -168,7 +168,7 @@ extension AudioPlayerService: AudioPlayerDelegate {
}
func audioPlayerUnexpectedError(player _: AudioPlayer, error: AudioPlayerError) {
delegate.invoke(invocation: { $0.errorOccured(error: error) })
delegate.invoke(invocation: { $0.errorOccurred(error: error) })
}
func audioPlayerDidCancel(player _: AudioPlayer, queuedItems _: [AudioEntryId]) {}
@@ -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 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'AudioStreaming'
s.version = '0.5.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'
+8 -2
View File
@@ -52,6 +52,7 @@
B59DF1A32493E90C0043C498 /* AudioFileStream+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59DF1A22493E90C0043C498 /* AudioFileStream+Helpers.swift */; };
B5AEDBB824744153007D8101 /* AudioStreaming.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5AEDBAE24744153007D8101 /* AudioStreaming.framework */; };
B5AEDBBF24744153007D8101 /* AudioStreaming.h in Headers */ = {isa = PBXBuildFile; fileRef = B5AEDBB124744153007D8101 /* AudioStreaming.h */; settings = {ATTRIBUTES = (Public, ); }; };
B5B36E432655A32200DC96F5 /* FrameFilterProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B36E422655A32200DC96F5 /* FrameFilterProcessor.swift */; };
B5B3B7CC248647ED00656828 /* AudioPlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B3B7CB248647ED00656828 /* AudioPlayerState.swift */; };
B5D4A40925D9321400E1450C /* IcycastHeaderParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D4A40825D9321400E1450C /* IcycastHeaderParser.swift */; };
B5D4A41025D948EF00E1450C /* IcycastHeadersProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D4A40B25D9445600E1450C /* IcycastHeadersProcessor.swift */; };
@@ -144,6 +145,7 @@
B5AEDBB224744153007D8101 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B5AEDBB724744153007D8101 /* AudioStreamingTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AudioStreamingTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
B5AEDBBE24744153007D8101 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B5B36E422655A32200DC96F5 /* FrameFilterProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameFilterProcessor.swift; sourceTree = "<group>"; };
B5B3B7CB248647ED00656828 /* AudioPlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerState.swift; sourceTree = "<group>"; };
B5D4A40825D9321400E1450C /* IcycastHeaderParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IcycastHeaderParser.swift; sourceTree = "<group>"; };
B5D4A40B25D9445600E1450C /* IcycastHeadersProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IcycastHeadersProcessor.swift; sourceTree = "<group>"; };
@@ -259,6 +261,7 @@
B55CEAC024855AA20001C498 /* Processors */ = {
isa = PBXGroup;
children = (
B5B36E422655A32200DC96F5 /* FrameFilterProcessor.swift */,
B5667A8F2499018D00D93F85 /* AudioFileStreamProcessor.swift */,
B5667B3D249BC43000D93F85 /* AudioPlayerRenderProcessor.swift */,
B55CE97024810DE20001C498 /* MetadataStreamProcessor.swift */,
@@ -615,6 +618,7 @@
B5667A902499018D00D93F85 /* AudioFileStreamProcessor.swift in Sources */,
B59D0B6F255C904900D6CCE5 /* FileAudioSource.swift in Sources */,
B5EF9555247E9393003E8FF8 /* AudioEntry.swift in Sources */,
B5B36E432655A32200DC96F5 /* FrameFilterProcessor.swift in Sources */,
B51FE0C02488F67C00F2A4D2 /* Queue.swift in Sources */,
B5667A922499063D00D93F85 /* AudioPlayerContext.swift in Sources */,
B55CE97124810DE20001C498 /* MetadataStreamProcessor.swift in Sources */,
@@ -794,6 +798,7 @@
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
@@ -806,7 +811,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 0.5.0;
MARKETING_VERSION = 0.9.0;
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@@ -824,6 +829,7 @@
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
@@ -836,7 +842,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 0.5.0;
MARKETING_VERSION = 0.9.0;
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@@ -5,7 +5,7 @@
import AVFoundation
extension AVAudioFormat {
public extension AVAudioFormat {
/// The underlying audio stream description.
///
/// This exposes the `pointee` value of the `UsafePointer<AudioStreamBasicDescription>`
+3 -3
View File
@@ -31,7 +31,7 @@ internal enum Logger {
}
static func error(_ message: StaticString, category: Category, args: CVarArg...) {
proccess(message, category: category, type: .error, args: args)
process(message, category: category, type: .error, args: args)
}
static func error(_ message: StaticString, category: Category) {
@@ -39,14 +39,14 @@ internal enum Logger {
}
static func debug(_ message: StaticString, category: Category, args: CVarArg...) {
proccess(message, category: category, type: .debug, args: args)
process(message, category: category, type: .debug, args: args)
}
static func debug(_ message: StaticString, category: Category) {
debug(message, category: category, args: [])
}
private static func proccess(_ message: StaticString, category: Category, type: OSLogType, args: CVarArg...) {
private static func process(_ message: StaticString, category: Category, type: OSLogType, args: CVarArg...) {
guard isEnabled else { return }
os_log(message, log: category.toOSLog(), type: type, args)
}
+2 -1
View File
@@ -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) {
+1 -1
View File
@@ -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] = [:]
@@ -53,7 +53,7 @@ internal class AudioEntry {
return Double(audioStreamFormat.mFramesPerPacket) / Double(sampleRate)
}
private var avaragePacketByteSize: Double {
private var averagePacketByteSize: Double {
let packets = processedPacketsState
guard !packets.isEmpty else { return 0 }
return Double(packets.sizeTotal / packets.count)
@@ -109,7 +109,7 @@ internal class AudioEntry {
if packetsCount > estimationMinPacketsPreferred ||
(audioStreamFormat.mBytesPerFrame == 0 && packetsCount > estimationMinPackets)
{
return avaragePacketByteSize / packetDuration * 8
return averagePacketByteSize / packetDuration * 8
}
}
return (Double(audioStreamFormat.mBytesPerFrame) * audioStreamFormat.mSampleRate) * 8
@@ -151,12 +151,12 @@ extension AudioEntry: AudioStreamSourceDelegate {
delegate?.dataAvailable(source: source, data: data)
}
func errorOccured(source: CoreAudioStreamSource, error: Error) {
delegate?.errorOccured(source: source, error: error)
func errorOccurred(source: CoreAudioStreamSource, error: Error) {
delegate?.errorOccurred(source: source, error: error)
}
func endOfFileOccured(source: CoreAudioStreamSource) {
delegate?.endOfFileOccured(source: source)
func endOfFileOccurred(source: CoreAudioStreamSource) {
delegate?.endOfFileOccurred(source: source)
}
func metadataReceived(data: [String: String]) {
@@ -10,9 +10,9 @@ protocol AudioStreamSourceDelegate: AnyObject {
/// Indicates that there's data available
func dataAvailable(source: CoreAudioStreamSource, data: Data)
/// Indicates an error occurred
func errorOccured(source: CoreAudioStreamSource, error: Error)
func errorOccurred(source: CoreAudioStreamSource, error: Error)
/// Indicates end of file has occurred
func endOfFileOccured(source: CoreAudioStreamSource)
func endOfFileOccurred(source: CoreAudioStreamSource)
/// Indicates metadata read from stream
func metadataReceived(data: [String: String])
}
@@ -74,7 +74,7 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
do {
try performOpen(seek: offset)
} catch {
delegate?.errorOccured(source: self, error: error)
delegate?.errorOccurred(source: self, error: error)
}
}
@@ -139,11 +139,11 @@ extension FileAudioSource: StreamDelegate {
case .hasBytesAvailable:
dataAvailable()
case .endEncountered:
delegate?.endOfFileOccured(source: self)
delegate?.endOfFileOccurred(source: self)
case .errorOccurred:
delegate?.errorOccured(source: self, error: AudioPlayerError.codecError)
delegate?.errorOccurred(source: self, error: AudioPlayerError.codecError)
case .endEncountered:
delegate?.endOfFileOccured(source: self)
delegate?.endOfFileOccurred(source: self)
default:
break
}
@@ -86,12 +86,12 @@ public class RemoteAudioSource: AudioStreamSource {
let metadataProcessor = MetadataStreamProcessor(parser: metadataParser.eraseToAnyParser())
let netStatusProvider = NetStatusService(network: NWPathMonitor())
let icyheaderProcessor = IcycastHeadersProcessor()
let retrierTimout = Retrier(interval: .seconds(1), maxInterval: 5, underlyingQueue: nil)
let retrierTimeout = Retrier(interval: .seconds(1), maxInterval: 5, underlyingQueue: nil)
self.init(networking: networking,
metadataStreamSource: metadataProcessor,
icycastHeadersProcessor: icyheaderProcessor,
netStatusProvider: netStatusProvider,
retrier: retrierTimout,
retrier: retrierTimeout,
url: url,
underlyingQueue: underlyingQueue,
httpHeaders: httpHeaders)
@@ -109,8 +109,7 @@ public class RemoteAudioSource: AudioStreamSource {
func close() {
retrierTimeout.cancel()
netStatusService.stop()
streamOperationQueue.isSuspended = true
streamOperationQueue.isSuspended = false
streamOperationQueue.cancelAllOperations()
if let streamTask = streamRequest {
streamTask.cancel()
@@ -138,12 +137,10 @@ public class RemoteAudioSource: AudioStreamSource {
}
func suspend() {
streamRequest?.suspend()
streamOperationQueue.isSuspended = true
}
func resume() {
streamRequest?.resume()
streamOperationQueue.isSuspended = false
}
@@ -154,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)
}
}
}
@@ -163,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
}
@@ -181,65 +177,67 @@ 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?.errorOccured(source: self, error: error)
delegate?.errorOccurred(source: self, error: error)
} else {
addCompletionOperation { [weak self] in
guard let self = self else { return }
self.delegate?.endOfFileOccured(source: self)
self.delegate?.endOfFileOccurred(source: self)
}
}
}
}
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.proccess(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.canProccessMetadata {
let extractedAudioData = self.metadataStreamProcessor.proccessMetadata(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
}
}
@@ -260,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)
}
@@ -271,9 +269,12 @@ public class RemoteAudioSource: AudioStreamSource {
// check for error
if statusCode == 416 { // range not satisfied error
if length >= 0 { seekOffset = length }
delegate?.endOfFileOccured(source: self)
delegate?.endOfFileOccurred(source: self)
} else if statusCode >= 300 {
delegate?.errorOccured(source: self, error: NetworkError.serverError)
delegate?.errorOccurred(
source: self,
error: NetworkError.serverError
)
}
}
@@ -290,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
@@ -299,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)
}
}
@@ -6,7 +6,7 @@
import AVFoundation
import CoreAudio
public final class AudioPlayer {
open class AudioPlayer {
public weak var delegate: AudioPlayerDelegate?
public var muted: Bool {
@@ -86,6 +86,19 @@ public final class AudioPlayer {
/// The current configuration of the player.
public let configuration: AudioPlayerConfiguration
/// A Boolean value that indicates whether the audio engine is running.
/// `true` if the engine is running, otherwise, `false`
public var isEngineRunning: Bool { audioEngine.isRunning }
/// The `AVAudioMixerNode` as created by the underlying audio engine
public var mainMixerNode: AVAudioMixerNode {
audioEngine.mainMixerNode
}
public var frameFiltering: FrameFiltering {
frameFilterProcessor
}
/// An `AVAudioFormat` object for the canonical audio stream
private var outputAudioFormat: AVAudioFormat = {
AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100.0, channels: 2, interleaved: true)!
@@ -95,27 +108,23 @@ public final class AudioPlayer {
private var stateBeforePaused: InternalState = .initial
/// The underlying `AVAudioEngine` object
let audioEngine = AVAudioEngine()
private let audioEngine = AVAudioEngine()
/// An `AVAudioUnit` object that represents the audio player
private(set) var player = AVAudioUnit()
/// An `AVAudioUnitTimePitch` that controls the playback rate of the audio engine
let rateNode = AVAudioUnitTimePitch()
/// A Boolean value that indicates whether the audio engine is running.
/// `true` if the engine is running, otherwise, `false`
var isEngineRunning: Bool { audioEngine.isRunning }
private let rateNode = AVAudioUnitTimePitch()
/// An object representing the context of the audio render.
/// Holds the audio buffer and in/out lists as required by the audio rendering
let rendererContext: AudioRendererContext
private let rendererContext: AudioRendererContext
/// An object representing the context of the player.
/// Holds the player's state, current playing and reading entries.
let playerContext: AudioPlayerContext
private let playerContext: AudioPlayerContext
let fileStreamProcessor: AudioFileStreamProcessor
let playerRenderProcessor: AudioPlayerRenderProcessor
private let fileStreamProcessor: AudioFileStreamProcessor
private let playerRenderProcessor: AudioPlayerRenderProcessor
private let frameFilterProcessor: FrameFilterProcessor
private let audioReadSource: DispatchTimerSource
private let serializationQueue: DispatchQueue
private let sourceQueue: DispatchQueue
@@ -132,7 +141,6 @@ public final class AudioPlayer {
serializationQueue = DispatchQueue(label: "streaming.core.queue", qos: .userInitiated)
sourceQueue = DispatchQueue(label: "source.queue", qos: .userInitiated)
audioReadSource = DispatchTimerSource(interval: .milliseconds(200), queue: sourceQueue)
entryProvider = AudioEntryProvider(networkingClient: NetworkingClient(),
underlyingQueue: sourceQueue,
@@ -142,6 +150,8 @@ public final class AudioPlayer {
rendererContext: rendererContext,
outputAudioFormat: outputAudioFormat.basicStreamDescription)
frameFilterProcessor = FrameFilterProcessor(mixerNode: audioEngine.mainMixerNode)
playerRenderProcessor = AudioPlayerRenderProcessor(playerContext: playerContext,
rendererContext: rendererContext,
outputAudioFormat: outputAudioFormat.basicStreamDescription)
@@ -154,7 +164,6 @@ public final class AudioPlayer {
deinit {
playerContext.audioPlayingEntry?.close()
clearQueue()
stopReadProccessFromSource()
rendererContext.clean()
}
@@ -183,14 +192,13 @@ public final class AudioPlayer {
do {
try self.startEngineIfNeeded()
} catch {
self.raiseUnxpected(error: .audioSystemError(.engineFailure))
self.raiseUnexpected(error: .audioSystemError(.engineFailure))
}
}
sourceQueue.async { [weak self] in
guard let self = self else { return }
self.processSource()
self.startReadProcessFromSourceIfNeeded()
}
}
@@ -246,7 +254,6 @@ public final class AudioPlayer {
public func stop() {
guard playerContext.internalState != .stopped else { return }
stopReadProccessFromSource()
serializationQueue.sync {
stopEngine(reason: .userAction)
}
@@ -277,7 +284,6 @@ public final class AudioPlayer {
serializationQueue.sync {
pauseEngine()
}
stopReadProccessFromSource()
playerContext.audioPlayingEntry?.suspend()
sourceQueue.async { [weak self] in
self?.processSource()
@@ -303,9 +309,10 @@ public final class AudioPlayer {
}
startPlayer(resetBuffers: false)
}
startReadProcessFromSourceIfNeeded()
}
/// Seeks the audio to the specified time.
/// - 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
@@ -328,10 +335,16 @@ public final class AudioPlayer {
}
}
/// Attaches the given `AVAudioNode` to the engine
/// - Note: The node will be added after the default rate node
/// - Parameter node: An instance of `AVAudioNode`
public func attach(node: AVAudioNode) {
attach(nodes: [node])
}
/// Attaches the given `AVAudioNode`s to the engine
/// - Note: The nodes will be added after the default rate node
/// - Parameter node: An array of `AVAudioNode` instances
public func attach(nodes: [AVAudioNode]) {
nodes.forEach { node in
customAttachedNodes.append(node)
@@ -341,6 +354,8 @@ public final class AudioPlayer {
reattachCustomNodes()
}
/// Detaches the given `AVAudioNode` from the engine
/// - Parameter node: An instance of `AVAudioNode`
public func detach(node: AVAudioNode) {
guard customAttachedNodes.contains(node) else {
return
@@ -350,6 +365,8 @@ public final class AudioPlayer {
reattachCustomNodes()
}
/// Detaches the given `AVAudioNode`s from the engine
/// - Parameter node: An array of `AVAudioNode` instances
public func detachCustomAttachedNodes() {
customAttachedNodes.forEach { node in
audioEngine.detach(node)
@@ -385,7 +402,7 @@ public final 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)
}
}
@@ -400,7 +417,7 @@ public final class AudioPlayer {
self.playerRenderProcessor.attachCallback(on: unit, audioFormat: self.outputAudioFormat)
case let .failure(error):
assertionFailure("couldn't create player unit: \(error)")
self.raiseUnxpected(error: .audioSystemError(.playerNotFound))
self.raiseUnexpected(error: .audioSystemError(.playerNotFound))
}
}
}
@@ -428,12 +445,12 @@ public final class AudioPlayer {
fileStreamProcessor.fileStreamCallback = { [weak self] effect in
guard let self = self else { return }
switch effect {
case .proccessSource:
case .processSource:
self.sourceQueue.async {
self.processSource()
}
case let .raiseError(error):
self.raiseUnxpected(error: error)
self.raiseUnexpected(error: error)
}
}
}
@@ -453,7 +470,7 @@ public final 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)
@@ -506,24 +523,7 @@ public final class AudioPlayer {
Logger.debug("engine stopped 🛑", category: .generic)
}
/// Starts the timer of `audioReadSource` for proccesing the source read stream
///
/// This calls `processSource` method every `500 ms`
private func startReadProcessFromSourceIfNeeded() {
guard audioReadSource.state != .activated else { return }
audioReadSource.add { [weak self] in
self?.processSource()
}
audioReadSource.activate()
}
/// Stops and removes the handler from the timer, @see `audioReadSource`
private func stopReadProccessFromSource() {
audioReadSource.suspend()
audioReadSource.removeHandler()
}
/// 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) {
@@ -536,7 +536,7 @@ public final class AudioPlayer {
try player.auAudioUnit.startHardware()
} catch {
stopEngine(reason: .error)
raiseUnxpected(error: .audioSystemError(.playerStartError))
raiseUnexpected(error: .audioSystemError(.playerStartError))
}
}
@@ -544,7 +544,6 @@ public final class AudioPlayer {
private func processSource() {
dispatchPrecondition(condition: .onQueue(sourceQueue))
guard !playerContext.disposedRequested else { return }
guard playerContext.internalState != .paused else { return }
if playerContext.internalState == .pendingNext {
@@ -581,7 +580,6 @@ public final class AudioPlayer {
setCurrentReading(entry: entry, startPlaying: shouldStartPlaying, shouldClearQueue: false)
} else if playerContext.audioPlayingEntry == nil {
if playerContext.internalState != .stopped {
stopReadProccessFromSource()
stopEngine(reason: .eof)
}
}
@@ -597,7 +595,7 @@ public final class AudioPlayer {
playingEntry.seekRequest.lock.unlock()
if originalSeekToTimeRequested, playerContext.audioReadingEntry === playingEntry {
proccessSeekTime()
processSeekTime()
let version = playingEntry.seekRequest.version.value
if currSeekVersion == version {
@@ -609,7 +607,7 @@ public final class AudioPlayer {
}
}
private func proccessSeekTime() {
private func processSeekTime() {
assert(playerContext.audioReadingEntry === playerContext.audioPlayingEntry,
"reading and playing entry must be the same")
fileStreamProcessor.processSeek()
@@ -724,7 +722,7 @@ public final class AudioPlayer {
}
}
private func raiseUnxpected(error: AudioPlayerError) {
private func raiseUnexpected(error: AudioPlayerError) {
playerContext.setInternalState(to: .error)
// todo raise on main thread from playback thread
asyncOnMain { [weak self] in
@@ -745,7 +743,7 @@ extension AudioPlayer: AudioStreamSourceDelegate {
let openFileStreamStatus = fileStreamProcessor.openFileStream(with: source.audioFileHint)
guard openFileStreamStatus == noErr else {
let streamError = AudioFileStreamError(status: openFileStreamStatus)
raiseUnxpected(error: .audioSystemError(.fileStreamError(streamError)))
raiseUnexpected(error: .audioSystemError(.fileStreamError(streamError)))
return
}
}
@@ -755,7 +753,7 @@ extension AudioPlayer: AudioStreamSourceDelegate {
guard streamBytesStatus == noErr else {
if let playingEntry = playerContext.audioPlayingEntry, playingEntry.has(same: source) {
let streamBytesError = AudioFileStreamError(status: streamBytesStatus)
raiseUnxpected(error: .streamParseBytesFailure(streamBytesError))
raiseUnexpected(error: .streamParseBytesFailure(streamBytesError))
}
return
}
@@ -766,12 +764,12 @@ extension AudioPlayer: AudioStreamSourceDelegate {
}
}
func errorOccured(source: CoreAudioStreamSource, error: Error) {
func errorOccurred(source: CoreAudioStreamSource, error: Error) {
guard let entry = playerContext.audioReadingEntry, entry.has(same: source) else { return }
raiseUnxpected(error: .networkError(.failure(error)))
raiseUnexpected(error: .networkError(.failure(error)))
}
func endOfFileOccured(source: CoreAudioStreamSource) {
func endOfFileOccurred(source: CoreAudioStreamSource) {
let hasSameSource = playerContext.audioReadingEntry?.has(same: source) ?? false
guard playerContext.audioReadingEntry == nil || hasSameSource else {
source.delegate = nil
@@ -802,7 +800,10 @@ extension AudioPlayer: AudioStreamSourceDelegate {
playerContext.audioReadingEntry = nil
playerContext.entriesLock.unlock()
processSource()
sourceQueue.async { [weak self] in
guard let self = self else { return }
self.processSource()
}
}
func metadataReceived(data: [String: String]) {
@@ -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)
}
}
@@ -6,30 +6,31 @@
import Foundation
internal final class AudioPlayerContext {
var stopReason = Protected<AudioPlayerStopReason>(.none)
var stopReason: Protected<AudioPlayerStopReason>
var state = Protected<AudioPlayerState>(.ready)
var state: Protected<AudioPlayerState>
var stateChanged: ((_ oldState: AudioPlayerState, _ newState: AudioPlayerState) -> Void)?
var muted = Protected<Bool>(false)
var muted: Protected<Bool>
var internalState: AudioPlayer.InternalState {
playerInternalState.value
}
let entriesLock = UnfairLock()
let entriesLock: UnfairLock
var audioReadingEntry: AudioEntry?
var audioPlayingEntry: AudioEntry?
var disposedRequested: Bool
/// This is the player's internal state to use
/// - NOTE: Do not use directly instead use the `internalState` to set and get the property
/// or the `setInternalState(to:when:)`method
private var playerInternalState = Protected<AudioPlayer.InternalState>(.initial)
init() {
disposedRequested = false
stopReason = Protected<AudioPlayerStopReason>(.none)
state = Protected<AudioPlayerState>(.ready)
muted = Protected<Bool>(false)
entriesLock = UnfairLock()
}
/// Sets the internal state if given the `inState` will be evaluated before assignment occurs.
@@ -22,7 +22,7 @@ public protocol AudioPlayerDelegate: AnyObject {
stopReason: AudioPlayerStopReason,
progress: Double,
duration: Double)
/// Tells the delegate when an unexpected error occured.
/// Tells the delegate when an unexpected error occurred.
/// - note: Probably a good time to recreate the player when this occurs
func audioPlayerUnexpectedError(player: AudioPlayer, error: AudioPlayerError)
@@ -20,8 +20,6 @@ final class AudioRendererContext {
let packetsSemaphore = DispatchSemaphore(value: 0)
var discontinuous: Bool = false
let framesRequiredToStartPlaying: UInt32
let framesRequiredAfterRebuffering: UInt32
let framesRequiredForDataAfterSeekPlaying: UInt32
@@ -36,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)
@@ -77,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
@@ -9,7 +9,7 @@ import AVFoundation
enum AudioConvertStatus: Int32 {
case done = 100
case proccessed = 0
case processed = 0
}
struct AudioConvertInfo {
@@ -20,11 +20,11 @@ struct AudioConvertInfo {
}
enum FileStreamProcessorEffect {
case proccessSource
case processSource
case raiseError(AudioPlayerError)
}
/// An object that handles the proccessing of AudioFileStream, its packets etc.
/// An object that handles the processing of AudioFileStream, its packets etc.
final class AudioFileStreamProcessor {
private let maxCompressedPacketForBitrate = 4096
@@ -38,8 +38,9 @@ final class AudioFileStreamProcessor {
internal var audioConverter: AudioConverterRef?
internal var discontinuous: Bool = false
internal var inputFormat = AudioStreamBasicDescription()
internal var fileFormat: String = ""
internal let fa4mFormat = "fa4m"
internal var currentFileFormat: String = ""
internal let fileFormatsForDelayedConverterCreation: Set = ["fa4m", "f4pm"]
var isFileStreamOpen: Bool {
audioFileStream != nil
@@ -165,7 +166,7 @@ final class AudioFileStreamProcessor {
var classDesc = AudioClassDescription()
var outputFormat = toFormat
if getHardwareCodecClassDescripition(formatId: inputFormat.mFormatID, classDesc: &classDesc) {
if getHardwareCodecClassDescription(formatId: inputFormat.mFormatID, classDesc: &classDesc) {
AudioConverterNewSpecific(&inputFormat, &outputFormat, 1, &classDesc, &audioConverter)
}
@@ -178,11 +179,12 @@ final class AudioFileStreamProcessor {
}
}
self.inputFormat = inputFormat
assignMagicCookieToConverterIfNeeded()
}
private func assignMagicCookieToConverterIfNeeded() {
// magic cookie info
let fileHint = playerContext.audioReadingEntry?.audioFileHint
let isProperFormat = fileHint != kAudioFileAAC_ADTSType && fileHint != kAudioFileM4AType && fileHint != kAudioFileMPEG4Type
if let fileStream = audioFileStream, isProperFormat {
if let fileStream = audioFileStream {
var cookieSize: UInt32 = 0
guard AudioFileStreamGetPropertyInfo(fileStream, kAudioFileStreamProperty_MagicCookieData, &cookieSize, nil) == noErr else {
return
@@ -193,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))))
@@ -229,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)
@@ -239,7 +241,7 @@ final class AudioFileStreamProcessor {
}
}
// MARK: AudioFileStream properties Proccessing
// MARK: AudioFileStream properties Processing
private func processDataOffset(fileStream: AudioFileStreamID) {
var offset: UInt64 = 0
@@ -263,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.fileFormat = stringFileFormat
currentFileFormat = stringFileFormat
}
}
@@ -293,7 +295,7 @@ final class AudioFileStreamProcessor {
entry.processedPacketsState.bufferSize = packetBufferSize
}
if fileFormat != fa4mFormat {
if !fileFormatsForDelayedConverterCreation.contains(currentFileFormat) {
createAudioConverter(from: entry.audioStreamFormat, to: outputAudioFormat)
}
}
@@ -306,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)
@@ -331,7 +333,7 @@ final class AudioFileStreamProcessor {
i += step
}
if fileFormat == fa4mFormat {
if fileFormatsForDelayedConverterCreation.contains(currentFileFormat) {
if let inputStreamFormat = playerContext.audioReadingEntry?.audioStreamFormat {
createAudioConverter(from: inputStreamFormat, to: outputAudioFormat)
}
@@ -346,12 +348,12 @@ final class AudioFileStreamProcessor {
inPacketDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?)
{
guard let entry = playerContext.audioReadingEntry else { return }
guard entry.audioStreamState.processedDataFormat, !playerContext.disposedRequested else { return }
guard entry.audioStreamState.processedDataFormat else { return }
if let playingEntry = playerContext.audioPlayingEntry,
playingEntry.seekRequest.requested, playingEntry.calculatedBitrate() > 0
{
fileStreamCallback?(.proccessSource)
fileStreamCallback?(.processSource)
if rendererContext.waiting.value {
rendererContext.packetsSemaphore.signal()
}
@@ -375,11 +377,11 @@ final class AudioFileStreamProcessor {
convertInfo.audioBuffer.mNumberChannels = playingAudioStreamFormat.mChannelsPerFrame
}
updateProccessedPackets(inPacketDescriptions: inPacketDescriptions,
inNumberPackets: inNumberPackets)
updateProcessedPackets(inPacketDescriptions: inPacketDescriptions,
inNumberPackets: inNumberPackets)
var status: OSStatus = noErr
packetProccess: while status == noErr {
packetProcess: while status == noErr {
rendererContext.lock.lock()
let bufferContext = rendererContext.bufferContext
var used = bufferContext.frameUsedCount
@@ -401,8 +403,7 @@ final class AudioFileStreamProcessor {
if framesLeftInBuffer > 0 {
break
}
if playerContext.disposedRequested
|| playerContext.internalState == .disposed
if playerContext.internalState == .disposed
|| playerContext.internalState == .pendingNext
|| playerContext.internalState == .stopped
{
@@ -412,7 +413,7 @@ final class AudioFileStreamProcessor {
if let playingEntry = playerContext.audioPlayingEntry,
playingEntry.seekRequest.requested, playingEntry.calculatedBitrate() > 0
{
fileStreamCallback?(.proccessSource)
fileStreamCallback?(.processSource)
if rendererContext.waiting.value {
rendererContext.packetsSemaphore.signal()
}
@@ -457,7 +458,7 @@ final class AudioFileStreamProcessor {
framesToDecode = start
if framesToDecode == 0 {
fillUsedFrames(framesCount: framesAdded)
continue packetProccess
continue packetProcess
}
prefillLocalBufferList(bufferList: localBufferList,
dataOffset: 0,
@@ -475,9 +476,9 @@ final class AudioFileStreamProcessor {
if status == AudioConvertStatus.done.rawValue {
fillUsedFrames(framesCount: framesAdded)
return
} else if status == AudioConvertStatus.proccessed.rawValue {
} else if status == AudioConvertStatus.processed.rawValue {
fillUsedFrames(framesCount: framesAdded)
continue packetProccess
continue packetProcess
} else if status != 0 {
fileStreamCallback?(.raiseError(.codecError))
return
@@ -502,9 +503,9 @@ final class AudioFileStreamProcessor {
if status == AudioConvertStatus.done.rawValue {
fillUsedFrames(framesCount: framesAdded)
return
} else if status == AudioConvertStatus.proccessed.rawValue {
} else if status == AudioConvertStatus.processed.rawValue {
fillUsedFrames(framesCount: framesAdded)
continue packetProccess
continue packetProcess
} else if status != 0 {
fileStreamCallback?(.raiseError(.codecError))
return
@@ -545,8 +546,8 @@ final class AudioFileStreamProcessor {
}
@inline(__always)
private func updateProccessedPackets(inPacketDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?,
inNumberPackets: UInt32)
private func updateProcessedPackets(inPacketDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>?,
inNumberPackets: UInt32)
{
guard let inPacketDescriptions = inPacketDescriptions else { return }
guard let readingEntry = playerContext.audioReadingEntry else { return }
@@ -618,12 +619,12 @@ private func _converterCallback(inAudioConverter _: AudioConverterRef,
ioNumberDataPackets.pointee = convertInfo.pointee.numberOfPackets
convertInfo.pointee.done = true
return AudioConvertStatus.proccessed.rawValue
return AudioConvertStatus.processed.rawValue
}
// MARK: HardwareCodedClass method
private func getHardwareCodecClassDescripition(formatId: UInt32, classDesc: UnsafeMutablePointer<AudioClassDescription>) -> Bool {
private func getHardwareCodecClassDescription(formatId: UInt32, classDesc: UnsafeMutablePointer<AudioClassDescription>) -> Bool {
#if os(iOS)
var size: UInt32 = 0
let formatIdSize = UInt32(MemoryLayout.size(ofValue: formatId))
@@ -0,0 +1,175 @@
//
// Created by Dimitrios C on 19/05/2021.
// Copyright © 2021 Decimal. All rights reserved.
//
import AVFoundation
///
/// - parameter buffer: A buffer of audio captured from the output of an AVAudioNode.
/// - parameter when: The time the buffer was captured.
///
public typealias FilterCallback = (_ buffer: AVAudioPCMBuffer,
_ when: AVAudioTime) -> Void
/// A value type whose instances are used for frame filter
/// - Note:
/// The filter block will be called from a thread other than the main thread
public struct FilterEntry: Equatable {
/// A string value indicating the name of the filter
public let name: String
/// A block in which you apply any filtering
public let filter: FilterCallback
public init(name: String, filter: @escaping FilterCallback) {
self.name = name
self.filter = filter
}
public static func == (lhs: FilterEntry, rhs: FilterEntry) -> Bool {
lhs.name == rhs.name
}
}
public protocol FrameFiltering {
/// A Boolean value indicating whether there are filter entries
var hasEntries: Bool { get }
/// Adds a filter entry at the end of the queue
/// - Parameter entry: An instance of `FilterEntry`
func add(entry: FilterEntry)
/// Adds a filter entry after the specified name of another entry
/// - Parameters:
/// - entry: An instance of `FilterEntry`
/// - named: The name of a previously added filter
func add(entry: FilterEntry, afterEntry named: String)
/// Adds a filter entry with the given parameters
/// - Parameters:
/// - named: The name of the entry to be added
/// - 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 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)
/// 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)
/// Removes all filter entries
func removeAll()
}
final class FrameFilterProcessor: NSObject, FrameFiltering {
public var hasEntries: Bool {
lock.lock(); defer { lock.unlock() }
return !entries.isEmpty
}
private let lock = UnfairLock()
private let mixerNode: AVAudioMixerNode
private(set) var entries: [FilterEntry] = []
private var hasInstalledTap: Bool = false
init(mixerNode: AVAudioMixerNode) {
self.mixerNode = mixerNode
}
public func add(entry: FilterEntry) {
lock.lock(); defer { lock.unlock() }
entries.append(entry)
installTapIfNeeded()
}
public func add(entry: FilterEntry, afterEntry named: String) {
lock.lock(); defer { lock.unlock() }
guard let entryIndex = entries.firstIndex(where: { $0.name == named }) else {
return
}
if entryIndex.advanced(by: 1) > entries.count {
entries.append(entry)
} else {
entries.insert(entry, at: entryIndex + 1)
}
installTapIfNeeded()
}
public func add(entry named: String, filter: @escaping FilterCallback) {
lock.lock(); defer { lock.unlock() }
entries.append(FilterEntry(name: named, filter: filter))
installTapIfNeeded()
}
func add(entry named: String, after filterName: String, filter: @escaping FilterCallback) {
let entry = FilterEntry(name: named, filter: filter)
add(entry: entry, afterEntry: filterName)
}
public func remove(entry: FilterEntry) {
lock.lock(); defer { lock.unlock() }
guard let entryIndex = entries.firstIndex(where: { $0 == entry }) else {
return
}
entries.remove(at: entryIndex)
if entries.isEmpty {
removeTap()
}
}
public func remove(entry named: String) {
lock.lock(); defer { lock.unlock() }
guard let entryIndex = entries.firstIndex(where: { $0.name == named }) else {
return
}
entries.remove(at: entryIndex)
if entries.isEmpty {
removeTap()
}
}
public func removeAll() {
lock.lock(); defer { lock.unlock() }
entries.removeAll()
removeTap()
}
private func process(buffer: AVAudioPCMBuffer, when: AVAudioTime) {
lock.lock(); defer { lock.unlock() }
guard !entries.isEmpty else { return }
for entry in entries {
entry.filter(buffer, when)
}
}
private func installTapIfNeeded() {
guard !hasInstalledTap else { return }
hasInstalledTap = true
let format = mixerNode.outputFormat(forBus: 0)
mixerNode.installTap(onBus: 0, bufferSize: 1024, format: format) { [weak self] buffer, when in
guard let self = self else { return }
guard self.hasEntries else { return }
self.process(
buffer: buffer,
when: when
)
}
}
private func removeTap() {
hasInstalledTap = false
mixerNode.removeTap(onBus: 0)
}
}
@@ -26,7 +26,6 @@ import Foundation
/// ```
final class IcycastHeadersProcessor {
private var icecastHeaders = Data(capacity: 1024)
private var searchComplete = false
private var iceHeaderAvailable = false
@@ -38,31 +37,32 @@ final class IcycastHeadersProcessor {
}
@inline(__always)
func proccess(data: Data) -> (Data?, Data) {
let stopProccessingCheckOne: [UInt8] = Array("\n\n".utf8)
let stopProccessingCheckTwo: [UInt8] = Array("\r\n\r\n".utf8)
func process(data: Data) -> (Data?, Data) {
let stopProcessingCheckOne: [UInt8] = Array("\n\n".utf8)
let stopProcessingCheckTwo: [UInt8] = Array("\r\n\r\n".utf8)
let icyPrefix: [UInt8] = Array("ICY ".utf8)
let httpPrefix: [UInt8] = Array("HTTP".utf8)
return data.withUnsafeBytes { buffer -> (Data?, Data) in
guard !buffer.isEmpty else { return (nil, data) }
var bytesRead = 0
let bytes = buffer.baseAddress!.assumingMemoryBound(to: UInt8.self)
// Read through the bytes and stop when our search is complete
// Since we don't know the amount of bytes to be proccessed
// Since we don't know the amount of bytes to be processed
// we add each character up until we found on of the checks as defined above.
while bytesRead < buffer.count, !searchComplete {
let pointer = bytes + bytesRead
icecastHeaders.append(pointer, count: 1)
if icecastHeaders.count >= stopProccessingCheckOne.count {
if icecastHeaders.suffix(stopProccessingCheckOne.count) == stopProccessingCheckOne {
if icecastHeaders.count >= stopProcessingCheckOne.count {
if icecastHeaders.suffix(stopProcessingCheckOne.count) == stopProcessingCheckOne {
iceHeaderAvailable = true
searchComplete = true
break
}
}
if icecastHeaders.count >= stopProccessingCheckTwo.count {
if icecastHeaders.suffix(stopProccessingCheckTwo.count) == stopProccessingCheckTwo {
if icecastHeaders.count >= stopProcessingCheckTwo.count {
if icecastHeaders.suffix(stopProcessingCheckTwo.count) == stopProcessingCheckTwo {
iceHeaderAvailable = true
searchComplete = true
break
@@ -71,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,16 +12,16 @@ 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`.
var canProccessMetadata: Bool { get }
/// 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
func metadataAvailable(step: Int)
/// Proccess the received data and extract the metadata if any, returns audio data only.
/// Process the received data and extract the metadata if any, returns audio data only.
/// - parameter data: A `Data` object for parsing any metadata
/// - returns: The extracted audio `Data`
func proccessMetadata(data: Data) -> Data
func processMetadata(data: Data) -> Data
/// Resets the processor
func reset()
@@ -44,7 +44,7 @@ protocol MetadataStreamSource {
final class MetadataStreamProcessor: MetadataStreamSource {
weak var delegate: MetadataStreamSourceDelegate?
var canProccessMetadata: Bool {
var canProcessMetadata: Bool {
return metadataStep > 0
}
@@ -73,10 +73,10 @@ final class MetadataStreamProcessor: MetadataStreamSource {
audioDataBytesRead = 0
}
// MARK: Proccess Metadata
// MARK: Process Metadata
@inline(__always)
func proccessMetadata(data: Data) -> Data {
func processMetadata(data: Data) -> Data {
data.withUnsafeBytes { buffer -> Data in
guard !buffer.isEmpty else { return data }
var audioData = Data()
@@ -14,7 +14,7 @@ struct HeaderField {
}
enum IcyHeaderField {
public static let icyMentaint = "icy-metaint"
public static let icyMetaint = "icy-metaint"
}
struct HTTPHeaderParserOutput {
@@ -64,7 +64,7 @@ struct HTTPHeaderParser: HTTPHeaderParsing {
}
var metadataStep = 0
if let icyMetaint = value(forHTTPHeaderField: IcyHeaderField.icyMentaint, in: input),
if let icyMetaint = value(forHTTPHeaderField: IcyHeaderField.icyMetaint, in: input),
let intValue = Int(icyMetaint)
{
metadataStep = intValue
@@ -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
}
@@ -23,7 +21,7 @@ struct IcycastHeaderParser: Parser {
result[String(key)] = String(value)
}
}
let metadataStep = Int(result[IcyHeaderField.icyMentaint] ?? "") ?? 0
let metadataStep = Int(result[IcyHeaderField.icyMetaint] ?? "") ?? 0
let contentType = result[HeaderField.contentType.lowercased()] ?? "audio/mpeg"
let typeId = audioFileType(mimeType: contentType)
@@ -18,13 +18,17 @@ struct MetadataParser: Parser {
func parse(input: Data) -> MetadataOutput {
guard let string = String(data: input, encoding: .utf8) else { return .failure(.unableToParse) }
// remove added bytes (zeros) and seperate the string on every ';' char
// 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
}
@@ -18,19 +18,19 @@ class MetadataStreamProcessorTests: XCTestCase {
let processor = MetadataStreamProcessor(parser: parser.eraseToAnyParser())
// without calling `metadataAvailable(step:)` it should be false
XCTAssertFalse(processor.canProccessMetadata)
XCTAssertFalse(processor.canProcessMetadata)
// calling `metadataAvailable(step:)` with zero
processor.metadataAvailable(step: 0)
// it should be false
XCTAssertFalse(processor.canProccessMetadata)
XCTAssertFalse(processor.canProcessMetadata)
// calling `metadataAvailable(step:)` with greater zero
processor.metadataAvailable(step: 1)
// it should be true
XCTAssertTrue(processor.canProccessMetadata)
XCTAssertTrue(processor.canProcessMetadata)
}
func test_Processor_Outputs_Correct_Metadata_ForStep_WithEmptyMetadata() throws {
@@ -45,7 +45,7 @@ class MetadataStreamProcessorTests: XCTestCase {
// this is the step value as received from the http headers
processor.metadataAvailable(step: 16000)
let audio = processor.proccessMetadata(data: data)
let audio = processor.processMetadata(data: data)
XCTAssertFalse(audio.isEmpty)
XCTAssertTrue(metadataDelegateSpy.receivedMetadata.called)
@@ -64,7 +64,7 @@ class MetadataStreamProcessorTests: XCTestCase {
// this is the step value as received from the http headers
processor.metadataAvailable(step: 16000)
let audio = processor.proccessMetadata(data: data)
let audio = processor.processMetadata(data: data)
XCTAssertFalse(audio.isEmpty)
XCTAssertTrue(metadataDelegateSpy.receivedMetadata.called)
@@ -83,7 +83,7 @@ class MetadataStreamProcessorTests: XCTestCase {
// this is the step value as received from the http headers
processor.metadataAvailable(step: 8000)
let audio = processor.proccessMetadata(data: data)
let audio = processor.processMetadata(data: data)
XCTAssertFalse(audio.isEmpty)
XCTAssertTrue(metadataDelegateSpy.receivedMetadata.called)
@@ -106,7 +106,7 @@ class MetadataStreamProcessorTests: XCTestCase {
// this is the step value as received from the http headers
processor.metadataAvailable(step: 16000)
let audio = processor.proccessMetadata(data: data)
let audio = processor.processMetadata(data: data)
XCTAssertFalse(audio.isEmpty)
XCTAssertFalse(metadataDelegateSpy.receivedMetadata.called)
@@ -122,7 +122,7 @@ class MetadataStreamProcessorTests: XCTestCase {
// this is the step value as received from the http headers
processor.metadataAvailable(step: 16000)
let audio = processor.proccessMetadata(data: data)
let audio = processor.processMetadata(data: data)
XCTAssertTrue(audio.isEmpty)
XCTAssertFalse(metadataDelegateSpy.receivedMetadata.called)
@@ -34,7 +34,7 @@ class HTTPHeaderParserTests: XCTestCase {
let headers: [String: String] =
[HeaderField.contentLength: "1000",
HeaderField.contentType: "audio/mp3",
IcyHeaderField.icyMentaint: "16000"]
IcyHeaderField.icyMetaint: "16000"]
let httpURLResponse = HTTPURLResponse(url: URL(string: "www.google.com")!,
statusCode: 200,
httpVersion: "",
@@ -57,7 +57,7 @@ class HTTPHeaderParserTests: XCTestCase {
let headers: [String: String] =
[HeaderField.contentLength.lowercased(): "1000",
HeaderField.contentType.lowercased(): "audio/mp3",
IcyHeaderField.icyMentaint.lowercased(): "16000"]
IcyHeaderField.icyMetaint.lowercased(): "16000"]
let httpURLResponse = HTTPURLResponse(url: URL(string: "www.google.com")!,
statusCode: 200,
httpVersion: "",
@@ -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
View File
@@ -17,7 +17,7 @@ let package = Package(
.target(
name: "AudioStreaming",
path: "AudioStreaming"
)
),
],
swiftLanguageVersions: [.v5]
)
+47 -12
View File
@@ -3,15 +3,15 @@
# AudioStreaming
An AudioPlayer/Streaming library for iOS written in Swift, allows playback of online audio streaming, local file as well as gapless queueing.
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/avfoundation/audio_playback_recording_and_processing/avaudioengine/audio_units?language=swift).
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
@@ -124,6 +124,41 @@ player.detachCustomAttachedNodes()
The example project shows an example of adding a custom `AVAudioUnitEQ` node for adding equaliser to the `AudioPlayer`
### Adding custom frame filter for recording and observation of audio data
`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)
let settings = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVSampleRateKey: format.sampleRate,
AVNumberOfChannelsKey: format.channelCount
] as [String : Any]
var audioFile = try? AVAudioFile(
forWriting: outputUrl,
settings: settings,
commonFormat: format.commonFormat,
interleaved: format.isInterleaved)
let record = FilterEntry(name: "record") { buffer, when in
try? audioFile?.write(from: buffer)
}
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.
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 documentation.
# Installation
### Cocoapods
@@ -164,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)