Compare commits

...

9 Commits

Author SHA1 Message Date
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
Dimitris C c2aee1669b Bump version (#17)
Co-authored-by: Dimitrios C <dimitrisc@DimitrisC-Macbook-Pro.local>
2021-03-22 12:15:42 +02:00
Mushthak Ebrahim 334be32bf9 Fix header not get passed into method (#16) 2021-03-02 18:15:26 +02:00
Dimitris C a2da46f85b Bump version (#15) 2021-02-14 16:37:49 +02:00
Dimitris C aca69debd1 Adds support for Shoutcast headers in audio stream (#14)
* Adds support for Shoutcast headers in audio stream

* Renames proccessIcecastHeaders to process(data:

* Updates comment on IcycastHeadersProcessor
2021-02-14 16:33:37 +02:00
11 changed files with 446 additions and 28 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'AudioStreaming'
s.version = '0.3.0'
s.version = '0.6.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'
+16 -2
View File
@@ -52,7 +52,10 @@
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 */; };
B5D82E65255DD562009EDAA4 /* NetStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D82E64255DD562009EDAA4 /* NetStatusService.swift */; };
B5DB66E2255C2EAB00B8DF53 /* AudioEntryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DB66E1255C2EAB00B8DF53 /* AudioEntryProvider.swift */; };
B5E1DE2524B70B4200955BFB /* AudioPlayerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E1DE2424B70B4200955BFB /* AudioPlayerConfiguration.swift */; };
@@ -142,7 +145,10 @@
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>"; };
B5D82E64255DD562009EDAA4 /* NetStatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetStatusService.swift; sourceTree = "<group>"; };
B5DB66DA255C079C00B8DF53 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
B5DB66E1255C2EAB00B8DF53 /* AudioEntryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioEntryProvider.swift; sourceTree = "<group>"; };
@@ -205,6 +211,7 @@
B55CEAB32485107C0001C498 /* Parser.swift */,
B55A736B247FCB420050C53D /* HTTPHeaderParser.swift */,
B55CE96D248058B60001C498 /* MetadataParser.swift */,
B5D4A40825D9321400E1450C /* IcycastHeaderParser.swift */,
);
path = Parsers;
sourceTree = "<group>";
@@ -254,9 +261,11 @@
B55CEAC024855AA20001C498 /* Processors */ = {
isa = PBXGroup;
children = (
B5B36E422655A32200DC96F5 /* FrameFilterProcessor.swift */,
B5667A8F2499018D00D93F85 /* AudioFileStreamProcessor.swift */,
B5667B3D249BC43000D93F85 /* AudioPlayerRenderProcessor.swift */,
B55CE97024810DE20001C498 /* MetadataStreamProcessor.swift */,
B5D4A40B25D9445600E1450C /* IcycastHeadersProcessor.swift */,
);
path = Processors;
sourceTree = "<group>";
@@ -594,6 +603,7 @@
B51B9F9A24DBE5BF00BDEAA2 /* AVAudioFormat+Convenience.swift in Sources */,
B51FE0C624890CCB00F2A4D2 /* PlayerQueueEntries.swift in Sources */,
B5EF9557247E9439003E8FF8 /* AudioStreamSource.swift in Sources */,
B5D4A40925D9321400E1450C /* IcycastHeaderParser.swift in Sources */,
B59DF1A32493E90C0043C498 /* AudioFileStream+Helpers.swift in Sources */,
B54D876D2490E4A000C361A0 /* UnitDescriptions.swift in Sources */,
B514657F248E3884005C03F7 /* DispatchTimerSource.swift in Sources */,
@@ -604,9 +614,11 @@
B5EF955B247EBCB3003E8FF8 /* AudioFileType.swift in Sources */,
B592E1252545FF9A008866FB /* BiMap.swift in Sources */,
B5DB66E2255C2EAB00B8DF53 /* AudioEntryProvider.swift in Sources */,
B5D4A41025D948EF00E1450C /* IcycastHeadersProcessor.swift in Sources */,
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 */,
@@ -786,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;
@@ -798,7 +811,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 0.3.0;
MARKETING_VERSION = 0.6.0;
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@@ -816,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;
@@ -828,7 +842,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 0.3.0;
MARKETING_VERSION = 0.6.0;
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@@ -9,7 +9,7 @@ extension AVAudioFormat {
/// The underlying audio stream description.
///
/// This exposes the `pointee` value of the `UsafePointer<AudioStreamBasicDescription>`
var basicStreamDescription: AudioStreamBasicDescription {
public var basicStreamDescription: AudioStreamBasicDescription {
return streamDescription.pointee
}
}
@@ -46,10 +46,10 @@ final class AudioEntryProvider: AudioEntryProviding {
FileAudioSource(url: url, underlyingQueue: underlyingQueue)
}
func source(for url: URL, headers _: [String: String]) -> CoreAudioStreamSource {
func source(for url: URL, headers: [String: String]) -> CoreAudioStreamSource {
guard !url.isFileURL else {
return provideFileAudioSource(url: url)
}
return provideAudioSource(url: url, headers: [:])
return provideAudioSource(url: url, headers: headers)
}
}
@@ -33,6 +33,9 @@ public class RemoteAudioSource: AudioStreamSource {
internal var metadataStreamProcessor: MetadataStreamSource
private var shouldTryParsingIcycastHeaders: Bool = false
private let icycastHeadersProcessor: IcycastHeadersProcessor
internal var audioFileHint: AudioFileTypeID {
guard let output = parsedHeaderOutput, output.typeId != 0 else {
return audioFileType(fileExtension: url.pathExtension)
@@ -48,6 +51,7 @@ public class RemoteAudioSource: AudioStreamSource {
init(networking: NetworkingClient,
metadataStreamSource: MetadataStreamSource,
icycastHeadersProcessor: IcycastHeadersProcessor,
netStatusProvider: NetStatusProvider,
retrier: Retrier,
url: URL,
@@ -62,6 +66,7 @@ public class RemoteAudioSource: AudioStreamSource {
seekOffset = 0
supportsSeek = false
netStatusService = netStatusProvider
self.icycastHeadersProcessor = icycastHeadersProcessor
self.underlyingQueue = underlyingQueue
streamOperationQueue = OperationQueue()
streamOperationQueue.underlyingQueue = underlyingQueue
@@ -80,9 +85,11 @@ public class RemoteAudioSource: AudioStreamSource {
let metadataParser = MetadataParser()
let metadataProcessor = MetadataStreamProcessor(parser: metadataParser.eraseToAnyParser())
let netStatusProvider = NetStatusService(network: NWPathMonitor())
let icyheaderProcessor = IcycastHeadersProcessor()
let retrierTimout = Retrier(interval: .seconds(1), maxInterval: 5, underlyingQueue: nil)
self.init(networking: networking,
metadataStreamSource: metadataProcessor,
icycastHeadersProcessor: icyheaderProcessor,
netStatusProvider: netStatusProvider,
retrier: retrierTimout,
url: url,
@@ -124,6 +131,8 @@ public class RemoteAudioSource: AudioStreamSource {
retrierTimeout.cancel()
metadataStreamProcessor.reset()
icycastHeadersProcessor.reset()
shouldTryParsingIcycastHeaders = false
performOpen(seek: offset)
}
@@ -189,16 +198,26 @@ public class RemoteAudioSource: AudioStreamSource {
private func handleStreamEvent(event: NetworkDataStream.StreamResult) {
switch event {
case let .success(value):
if let data = value.data {
if let audioData = value.data {
addStreamOperation { [weak self] in
guard let self = self else { return }
if self.metadataStreamProcessor.canProccessMetadata {
let extractedAudioData = self.metadataStreamProcessor.proccessMetadata(data: data)
self.delegate?.dataAvailable(source: self, data: extractedAudioData)
} else {
self.delegate?.dataAvailable(source: self, data: data)
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
}
}
self.relativePosition += data.count
let audioCount = self.processAudio(data: audioData)
self.relativePosition += audioCount
}
}
case .failure:
@@ -211,12 +230,32 @@ public class RemoteAudioSource: AudioStreamSource {
}
}
/// 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)
return extractedAudioData.count
} else {
self.delegate?.dataAvailable(source: self, data: data)
return data.count
}
}
private func parseResponseHeader(response: HTTPURLResponse?) {
guard let response = response else { return }
let httpStatusCode = response.statusCode
let parser = HTTPHeaderParser()
parsedHeaderOutput = parser.parse(input: response)
if parsedHeaderOutput == nil {
shouldTryParsingIcycastHeaders = true
checkHTTP(statusCode: httpStatusCode)
return
}
if let acceptRanges = parser.value(forHTTPHeaderField: HeaderField.acceptRanges, in: response) {
supportsSeek = acceptRanges != "none"
}
@@ -225,11 +264,15 @@ public class RemoteAudioSource: AudioStreamSource {
if let metadataStep = parsedHeaderOutput?.metadataStep {
metadataStreamProcessor.metadataAvailable(step: metadataStep)
}
checkHTTP(statusCode: httpStatusCode)
}
private func checkHTTP(statusCode: Int) {
// check for error
if httpStatusCode == 416 { // range not satisfied error
if statusCode == 416 { // range not satisfied error
if length >= 0 { seekOffset = length }
delegate?.endOfFileOccured(source: self)
} else if httpStatusCode >= 300 {
} else if statusCode >= 300 {
delegate?.errorOccured(source: self, error: NetworkError.serverError)
}
}
@@ -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,25 +108,22 @@ 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
@@ -142,6 +152,8 @@ public final class AudioPlayer {
rendererContext: rendererContext,
outputAudioFormat: outputAudioFormat.basicStreamDescription)
frameFilterProcessor = FrameFilterProcessor(mixerNode: audioEngine.mainMixerNode)
playerRenderProcessor = AudioPlayerRenderProcessor(playerContext: playerContext,
rendererContext: rendererContext,
outputAudioFormat: outputAudioFormat.basicStreamDescription)
@@ -306,6 +318,8 @@ public final class AudioPlayer {
startReadProcessFromSourceIfNeeded()
}
/// Seeks the audio to the specified time.
/// - Parameter time: A `Double` value specifing the time of the requested seek in seconds
public func seek(to time: Double) {
guard let playingEntry = playerContext.audioPlayingEntry else {
return
@@ -328,10 +342,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 +361,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 +372,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)
@@ -0,0 +1,177 @@
//
// 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 hanlding
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
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
/// - 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)
}
}
@@ -0,0 +1,91 @@
//
// IcycastHeadersProcessor.swift
// AudioStreaming
//
// Created by Dimitrios C on 14/02/2021.
// Copyright © 2021 Decimal. All rights reserved.
//
import Foundation
/// ICY is built on HTTP some old servers might still send headers in the stream.
/// From a server point of view, this should be considered deprecated and should not be used as it might break HTML5 compatibility.
/// Although there are some servers still using this, this class will extract those headers from the stream
///
/// The format of the headers is as follows:
/// ```
/// =================================================================
/// [ ICY 200 OK ]
/// [ icy-mentaint: the number of bytes between 2 metadata chunks ]
/// [ icy-br: send the bitrate in kilobits per second ]
/// [ icy-genre: sends the genre ]
/// [ icy-name: sends the stream's name ]
/// [ icy-url: is the URL of the radio station ]
/// [ icy-pub: can be 1 or 0 to tell if it is listed or not ]
/// =================================================================
/// ```
final class IcycastHeadersProcessor {
private var icecastHeaders = Data(capacity: 1024)
private var searchComplete = false
private var iceHeaderAvailable = false
func reset() {
icecastHeaders = Data(capacity: 1024)
searchComplete = false
iceHeaderAvailable = false
}
@inline(__always)
func proccess(data: Data) -> (Data?, Data) {
let stopProccessingCheckOne: [UInt8] = Array("\n\n".utf8)
let stopProccessingCheckTwo: [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
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
// 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 {
iceHeaderAvailable = true
searchComplete = true
break
}
}
if icecastHeaders.count >= stopProccessingCheckTwo.count {
if icecastHeaders.suffix(stopProccessingCheckTwo.count) == stopProccessingCheckTwo {
iceHeaderAvailable = true
searchComplete = true
break
}
}
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 {
iceHeaderAvailable = false
searchComplete = true
}
}
bytesRead += 1
}
if !iceHeaderAvailable {
return (nil, data)
}
let extractedAudio = data[icecastHeaders.count...]
iceHeaderAvailable = false
return (icecastHeaders, extractedAudio)
}
}
}
@@ -39,7 +39,7 @@ struct HTTPHeaderParser: HTTPHeaderParsing {
typealias Output = HTTPHeaderParserOutput?
func parse(input: HTTPURLResponse) -> HTTPHeaderParserOutput? {
guard let headers = input.allHeaderFields as? [String: String], !headers.isEmpty else { return nil }
guard let headers = input.allHeaderFields as? [String: String], headers.count > 2 else { return nil }
var typeId: UInt32 = 0
if let contentType = input.mimeType {
@@ -0,0 +1,34 @@
//
// IcycastHeaderParser.swift
// AudioStreaming
//
// Created by Dimitrios C on 14/02/2021.
// Copyright © 2021 Decimal. All rights reserved.
//
import Foundation
struct IcycastHeaderParser: Parser {
func parse(input: Data) -> HTTPHeaderParserOutput? {
guard let icecastValue = String(data: input, encoding: .utf8) else {
return nil
}
let headers = icecastValue.components(separatedBy: CharacterSet(charactersIn: "\r\n"))
var result = [String: String]()
for header in headers where !header.isEmpty {
let values = header.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: true)
if let key = values.first, let value = values.last {
result[String(key)] = String(value)
}
}
let metadataStep = Int(result[IcyHeaderField.icyMentaint] ?? "") ?? 0
let contentType = result[HeaderField.contentType.lowercased()] ?? "audio/mpeg"
let typeId = audioFileType(mimeType: contentType)
return HTTPHeaderParserOutput(fileLength: 0,
typeId: typeId,
metadataStep: metadataStep)
}
}
+36 -1
View File
@@ -3,7 +3,7 @@
# 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
@@ -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 fliters 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`.
```
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 fitler 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.
# Installation
### Cocoapods