Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65de9d90c0 | |||
| 217a88f171 |
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'AudioStreaming'
|
||||
s.version = '0.5.1'
|
||||
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'
|
||||
|
||||
@@ -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 */,
|
||||
@@ -807,7 +811,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.5.1;
|
||||
MARKETING_VERSION = 0.6.0;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
@@ -838,7 +842,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.5.1;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,19 @@ open 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)!
|
||||
@@ -101,15 +114,6 @@ open class AudioPlayer {
|
||||
/// An `AVAudioUnitTimePitch` that controls the playback rate of the audio engine
|
||||
private let rateNode = AVAudioUnitTimePitch()
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// An object representing the context of the audio render.
|
||||
/// Holds the audio buffer and in/out lists as required by the audio rendering
|
||||
private let rendererContext: AudioRendererContext
|
||||
@@ -119,6 +123,7 @@ open class AudioPlayer {
|
||||
|
||||
private let fileStreamProcessor: AudioFileStreamProcessor
|
||||
private let playerRenderProcessor: AudioPlayerRenderProcessor
|
||||
private let frameFilterProcessor: FrameFilterProcessor
|
||||
|
||||
private let audioReadSource: DispatchTimerSource
|
||||
private let serializationQueue: DispatchQueue
|
||||
@@ -147,6 +152,8 @@ open class AudioPlayer {
|
||||
rendererContext: rendererContext,
|
||||
outputAudioFormat: outputAudioFormat.basicStreamDescription)
|
||||
|
||||
frameFilterProcessor = FrameFilterProcessor(mixerNode: audioEngine.mainMixerNode)
|
||||
|
||||
playerRenderProcessor = AudioPlayerRenderProcessor(playerContext: playerContext,
|
||||
rendererContext: rendererContext,
|
||||
outputAudioFormat: outputAudioFormat.basicStreamDescription)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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