Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e8a3f0289 | |||
| 30b4189778 | |||
| 8bdc2a64f7 | |||
| 65de9d90c0 | |||
| 217a88f171 | |||
| 566dc86f3f | |||
| d8aa58525c | |||
| 8197db0016 |
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'AudioStreaming'
|
||||
s.version = '0.5.0'
|
||||
s.version = '0.7.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'
|
||||
|
||||
@@ -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.7.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.7.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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ internal final class NetworkDataStream {
|
||||
let error: Error?
|
||||
}
|
||||
|
||||
private var lock = UnfairLock()
|
||||
private var streamCallback: StreamCompletion?
|
||||
|
||||
/// The serial queue for all internal async actions.
|
||||
@@ -66,6 +67,7 @@ internal final class NetworkDataStream {
|
||||
|
||||
@discardableResult
|
||||
func responseStream(completion: @escaping StreamCompletion) -> Self {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
streamCallback = completion
|
||||
return self
|
||||
}
|
||||
@@ -79,6 +81,7 @@ internal final class NetworkDataStream {
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
guard state.canBecome(.cancelled) else { return }
|
||||
state = .cancelled
|
||||
streamCallback = nil
|
||||
|
||||
@@ -44,7 +44,6 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
}
|
||||
|
||||
internal let underlyingQueue: DispatchQueue
|
||||
internal let streamOperationQueue: OperationQueue
|
||||
internal let netStatusService: NetStatusProvider
|
||||
internal var waitingForNetwork = false
|
||||
internal let retrierTimeout: Retrier
|
||||
@@ -67,12 +66,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
supportsSeek = false
|
||||
netStatusService = netStatusProvider
|
||||
self.icycastHeadersProcessor = icycastHeadersProcessor
|
||||
self.underlyingQueue = underlyingQueue
|
||||
streamOperationQueue = OperationQueue()
|
||||
streamOperationQueue.underlyingQueue = underlyingQueue
|
||||
streamOperationQueue.maxConcurrentOperationCount = 1
|
||||
streamOperationQueue.isSuspended = true
|
||||
streamOperationQueue.name = "remote.audio.source.data.stream.queue"
|
||||
self.underlyingQueue = DispatchQueue(label: "remote.audio.source.queue", target: underlyingQueue)
|
||||
retrierTimeout = retrier
|
||||
startNetworkService()
|
||||
}
|
||||
@@ -110,8 +104,6 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
func close() {
|
||||
retrierTimeout.cancel()
|
||||
netStatusService.stop()
|
||||
streamOperationQueue.isSuspended = true
|
||||
streamOperationQueue.cancelAllOperations()
|
||||
if let streamTask = streamRequest {
|
||||
streamTask.cancel()
|
||||
networkingClient.remove(task: streamTask)
|
||||
@@ -139,12 +131,10 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
|
||||
func suspend() {
|
||||
streamRequest?.suspend()
|
||||
streamOperationQueue.isSuspended = true
|
||||
}
|
||||
|
||||
func resume() {
|
||||
streamRequest?.resume()
|
||||
streamOperationQueue.isSuspended = false
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
@@ -166,7 +156,9 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
let request = networkingClient.stream(request: urlRequest)
|
||||
.responseStream { [weak self] event in
|
||||
guard let self = self else { return }
|
||||
self.handleResponse(event: event)
|
||||
self.underlyingQueue.sync {
|
||||
self.handleResponse(event: event)
|
||||
}
|
||||
}
|
||||
.resume()
|
||||
|
||||
@@ -180,17 +172,13 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
switch event {
|
||||
case let .response(urlResponse):
|
||||
parseResponseHeader(response: urlResponse)
|
||||
streamOperationQueue.isSuspended = false
|
||||
case let .stream(event):
|
||||
handleStreamEvent(event: event)
|
||||
case let .complete(event):
|
||||
if let error = event.error {
|
||||
delegate?.errorOccured(source: self, error: error)
|
||||
} else {
|
||||
addCompletionOperation { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.delegate?.endOfFileOccured(source: self)
|
||||
}
|
||||
delegate?.endOfFileOccured(source: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,26 +187,23 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
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
|
||||
if shouldTryParsingIcycastHeaders {
|
||||
let (header, extractedAudio) = icycastHeadersProcessor.proccess(data: audioData)
|
||||
if let header = header {
|
||||
shouldTryParsingIcycastHeaders = false
|
||||
let parser = IcycastHeaderParser()
|
||||
parsedHeaderOutput = parser.parse(input: header)
|
||||
if let metadataStep = parsedHeaderOutput?.metadataStep {
|
||||
metadataStreamProcessor.metadataAvailable(step: metadataStep)
|
||||
}
|
||||
|
||||
let audioCount = processAudio(data: extractedAudio)
|
||||
relativePosition += audioCount
|
||||
return
|
||||
}
|
||||
let audioCount = self.processAudio(data: audioData)
|
||||
self.relativePosition += audioCount
|
||||
}
|
||||
let audioCount = processAudio(data: audioData)
|
||||
relativePosition += audioCount
|
||||
}
|
||||
case .failure:
|
||||
if !netStatusService.isConnected {
|
||||
@@ -281,7 +266,7 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
var urlRequest = URLRequest(url: url)
|
||||
urlRequest.networkServiceType = .avStreaming
|
||||
urlRequest.cachePolicy = .reloadIgnoringLocalCacheData
|
||||
urlRequest.timeoutInterval = 60
|
||||
urlRequest.timeoutInterval = 240
|
||||
|
||||
for header in additionalRequestHeaders {
|
||||
urlRequest.addValue(header.value, forHTTPHeaderField: header.key)
|
||||
@@ -302,25 +287,6 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
self.seek(at: self.position)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Network Stream Operation Queue
|
||||
|
||||
/// Schedules the given block on the stream operation queue
|
||||
///
|
||||
/// - Parameter block: A closure to be executed
|
||||
private func addStreamOperation(_ block: @escaping () -> Void) {
|
||||
let operation = BlockOperation(block: block)
|
||||
streamOperationQueue.addOperation(operation)
|
||||
}
|
||||
|
||||
/// Schedules the given block on the stream operation queue as a completion
|
||||
///
|
||||
/// - Parameter block: A closure to be executed
|
||||
private func addCompletionOperation(_ block: @escaping () -> Void) {
|
||||
let operation = BlockOperation(block: block)
|
||||
operation.queuePriority = .veryLow
|
||||
streamOperationQueue.addOperation(operation)
|
||||
}
|
||||
}
|
||||
|
||||
extension RemoteAudioSource: MetadataStreamSourceDelegate {
|
||||
|
||||
@@ -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)
|
||||
@@ -511,10 +535,11 @@ public final class AudioPlayer {
|
||||
/// 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()
|
||||
// TODO: this might be needed after all...
|
||||
// audioReadSource.add { [weak self] in
|
||||
// self?.processSource()
|
||||
// }
|
||||
// audioReadSource.activate()
|
||||
}
|
||||
|
||||
/// Stops and removes the handler from the timer, @see `audioReadSource`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user