Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 86d6e3a05a | |||
| 474a390b29 | |||
| f8cd25bd68 | |||
| 767978e70a | |||
| 85b45f6dfa | |||
| 0e6cadba1b | |||
| ed58739be0 | |||
| 10455ed4be | |||
| 6f48f3a526 | |||
| ed03fcdd0e | |||
| 6e3b50d6f9 | |||
| 21b245c114 | |||
| 8599d66bec | |||
| c6bd74a68c | |||
| bb3e518d08 | |||
| 1097743d57 | |||
| 2efe273f9e | |||
| 2ddd11d255 | |||
| 33575385e3 |
@@ -0,0 +1,24 @@
|
||||
name: "AudioStreaming CI"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- hotfix
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
iOS:
|
||||
name: Test iOS
|
||||
runs-on: macOS-latest
|
||||
env:
|
||||
DEVELOPER_DIR: /Applications/Xcode_12.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"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: iOS - ${{ matrix.destination }}
|
||||
run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "AudioStreaming.xcodeproj" -scheme "AudioStreaming" -destination "${{ matrix.destination }}" clean test | xcpretty
|
||||
@@ -16,6 +16,7 @@ enum AudioContent: Int, CaseIterable {
|
||||
case khruangbin
|
||||
case piano
|
||||
case local
|
||||
case podcast
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
@@ -33,6 +34,8 @@ enum AudioContent: Int, CaseIterable {
|
||||
return "Piano (mp3)"
|
||||
case .local:
|
||||
return "Local file (mp3)"
|
||||
case .podcast:
|
||||
return "Swift by Sundell. Ep. 50 (mp3)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +56,8 @@ enum AudioContent: Int, CaseIterable {
|
||||
case .local:
|
||||
let path = Bundle.main.path(forResource: "bensound-jazzyfrenchy", ofType: "mp3")!
|
||||
return URL(fileURLWithPath: path)
|
||||
case .podcast:
|
||||
return URL(string: "https://hwcdn.libsyn.com/p/f/6/e/f6e7cb785cf0f71f/SwiftBySundell50.mp3?c_id=45232967&cs_id=45232967&expiration=1605613140&hwt=f9ff0b2f758c3286cd75322e14ef7a23")!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'AudioStreaming'
|
||||
s.version = '0.1.0'
|
||||
s.version = '0.2.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'
|
||||
@@ -13,9 +13,7 @@ Pod::Spec.new do |s|
|
||||
|
||||
s.source_files = 'AudioStreaming/**/*.swift'
|
||||
|
||||
s.frameworks = 'AVFoundation', ' CoreAudio', 'AudioToolbox', 'Network'
|
||||
|
||||
s.pod_target_xcconfig = {
|
||||
'SWIFT_INSTALL_OBJC_HEADER' => 'NO'
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -798,7 +798,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.1.0;
|
||||
MARKETING_VERSION = 0.2.0;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
@@ -828,7 +828,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.1.0;
|
||||
MARKETING_VERSION = 0.2.0;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.decimal.AudioStreaming;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
|
||||
@@ -1,278 +0,0 @@
|
||||
//
|
||||
// Created by Dimitrios Chatzieleftheriou on 27/05/2020.
|
||||
// Copyright © 2020 Decimal. All rights reserved.
|
||||
//
|
||||
|
||||
import AudioToolbox
|
||||
import Foundation
|
||||
|
||||
typealias CoreAudioURLBlock = (URL) -> Void
|
||||
|
||||
public class RemoteAudioSource: NSObject, AudioStreamSource {
|
||||
var inputStream: InputStream?
|
||||
|
||||
weak var delegate: AudioStreamSourceDelegate?
|
||||
|
||||
var position: Int {
|
||||
return seekOffset + relativePosition
|
||||
}
|
||||
|
||||
var length: Int {
|
||||
guard let parsedHeader = parsedHeaderOutput else { return 0 }
|
||||
return parsedHeader.fileLength
|
||||
}
|
||||
|
||||
private let url: URL
|
||||
private let networking: NetworkingClient
|
||||
internal let metadataStreamProccessor: MetadataStreamSource
|
||||
private var streamRequest: NetworkDataStream?
|
||||
|
||||
private var additionalRequestHeaders: [String: String]
|
||||
private var httpStatusCode: Int
|
||||
|
||||
private var httpResponse: HTTPURLResponse?
|
||||
private var parsedHeaderOutput: HTTPHeaderParserOutput?
|
||||
private var relativePosition: Int
|
||||
private var seekOffset: Int
|
||||
|
||||
internal var dispatchQueue: DispatchQueue?
|
||||
|
||||
init(networking: NetworkingClient,
|
||||
metadataStreamSource: MetadataStreamSource,
|
||||
url: URL,
|
||||
httpHeaders: [String: String])
|
||||
{
|
||||
self.networking = networking
|
||||
metadataStreamProccessor = metadataStreamSource
|
||||
self.url = url
|
||||
additionalRequestHeaders = httpHeaders
|
||||
httpStatusCode = 0
|
||||
relativePosition = 0
|
||||
seekOffset = 0
|
||||
}
|
||||
|
||||
convenience init(networking: NetworkingClient, url: URL, httpHeaders: [String: String]) {
|
||||
let metadataParser = MetadataParser()
|
||||
let metadataProccessor = MetadataStreamProccessor(parser: metadataParser.eraseToAnyParser())
|
||||
self.init(networking: networking,
|
||||
metadataStreamSource: metadataProccessor,
|
||||
url: url,
|
||||
httpHeaders: httpHeaders)
|
||||
}
|
||||
|
||||
convenience init(networking: NetworkingClient, url: URL) {
|
||||
self.init(networking: networking,
|
||||
url: url,
|
||||
httpHeaders: [:])
|
||||
}
|
||||
|
||||
func setup(for queue: DispatchQueue) {
|
||||
dispatchQueue = queue
|
||||
|
||||
guard let stream = inputStream else {
|
||||
return
|
||||
}
|
||||
|
||||
stream.delegate = self
|
||||
stream.set(onQueue: queue)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func removeFromQueue() {
|
||||
guard let stream = inputStream else { return }
|
||||
stream.delegate = nil
|
||||
stream.unsetFromQueue()
|
||||
}
|
||||
|
||||
func close() {
|
||||
if inputStream != nil {
|
||||
if dispatchQueue != nil {
|
||||
removeFromQueue()
|
||||
}
|
||||
inputStream?.close()
|
||||
inputStream = nil
|
||||
}
|
||||
}
|
||||
|
||||
func seek(at offset: Int) {
|
||||
guard let queue = dispatchQueue else {
|
||||
return
|
||||
}
|
||||
dispatchPrecondition(condition: .onQueue(queue))
|
||||
|
||||
close()
|
||||
|
||||
relativePosition = 0
|
||||
seekOffset = offset
|
||||
|
||||
if let supportsSeek = parsedHeaderOutput?.supportsSeek,
|
||||
!supportsSeek, offset != relativePosition
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
performOpen(seek: seekOffset)
|
||||
}
|
||||
|
||||
func audioFileHint() -> AudioFileTypeID {
|
||||
return audioFileType(fileExtension: url.pathExtension)
|
||||
}
|
||||
|
||||
func read(into buffer: UnsafeMutablePointer<UInt8>, size: Int) -> Int {
|
||||
return performRead(into: buffer, size: size)
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private func performRead(into buffer: UnsafeMutablePointer<UInt8>, size: Int) -> Int {
|
||||
guard size != 0 else { return 0 }
|
||||
guard let stream = inputStream else { return 0 }
|
||||
|
||||
var read: Int = 0
|
||||
// Metadata parsing
|
||||
// if metadataStreamProccessor.canProccessMetadata {
|
||||
// read = metadataStreamProccessor.proccessFromRead(into: buffer, size: size, using: stream) { [weak self] position in
|
||||
// self?.relativePosition += position
|
||||
// }
|
||||
// } else {
|
||||
read = stream.read(buffer, maxLength: size)
|
||||
// }
|
||||
|
||||
guard read > 0 else { return read }
|
||||
relativePosition += read
|
||||
|
||||
return read
|
||||
}
|
||||
|
||||
private func performOpen(seek seekOffset: Int) {
|
||||
let urlRequest = buildUrlRequest(with: url, seekIfNeeded: seekOffset)
|
||||
|
||||
let streamRequest = networking.stream(request: urlRequest)
|
||||
.responseStream(on: .global(qos: .default)) { [weak self] event in
|
||||
switch event {
|
||||
case let .stream(result):
|
||||
switch result {
|
||||
case let .success(response):
|
||||
self?.httpResponse = response.response
|
||||
default:
|
||||
break
|
||||
}
|
||||
case let .complete(completion):
|
||||
print(completion)
|
||||
}
|
||||
}
|
||||
self.streamRequest = streamRequest
|
||||
inputStream = streamRequest.asInputStream()
|
||||
|
||||
guard let inputStream = inputStream else {
|
||||
errorOccured()
|
||||
return
|
||||
}
|
||||
|
||||
inputStream.setProperty(StreamNetworkServiceTypeValue.background, forKey: .networkServiceType)
|
||||
|
||||
if let scheme = url.scheme, scheme == "https" {
|
||||
inputStream.setProperty(StreamSocketSecurityLevel.negotiatedSSL, forKey: .socketSecurityLevelKey)
|
||||
let sslSettings: [String: Any] = [kCFStreamSSLValidatesCertificateChain as String: false]
|
||||
inputStream.setProperty(sslSettings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey)
|
||||
}
|
||||
|
||||
performSoftSetup()
|
||||
httpStatusCode = 0
|
||||
|
||||
inputStream.open()
|
||||
}
|
||||
|
||||
private func parseHeader(response: HTTPURLResponse?) -> Bool {
|
||||
guard let response = response else { return false }
|
||||
httpStatusCode = response.statusCode
|
||||
// parse the header response
|
||||
let parser = HTTPHeaderParser()
|
||||
parsedHeaderOutput = parser.parse(input: response)
|
||||
// check to see if we have metadata to proccess
|
||||
if let metadataStep = parsedHeaderOutput?.metadataStep {
|
||||
metadataStreamProccessor.metadataAvailable(step: metadataStep)
|
||||
}
|
||||
// check for error
|
||||
if httpStatusCode == 416 { // range not satisfied error
|
||||
if length >= 0 { seekOffset = length }
|
||||
endOfFileOccurred()
|
||||
return false
|
||||
} else if httpStatusCode >= 300 {
|
||||
errorOccured()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func performSoftSetup() {
|
||||
guard let queue = dispatchQueue, let inputStream = inputStream else { return }
|
||||
inputStream.delegate = self
|
||||
inputStream.set(onQueue: queue)
|
||||
}
|
||||
|
||||
private func buildUrlRequest(with _: URL, seekIfNeeded seekOffset: Int) -> URLRequest {
|
||||
var urlRequest = URLRequest(url: url)
|
||||
urlRequest.timeoutInterval = 30
|
||||
urlRequest.networkServiceType = .avStreaming
|
||||
|
||||
for header in additionalRequestHeaders {
|
||||
urlRequest.addValue(header.value, forHTTPHeaderField: header.key)
|
||||
}
|
||||
urlRequest.addValue("*/*", forHTTPHeaderField: "Accept")
|
||||
urlRequest.addValue("1", forHTTPHeaderField: "Icy-Metadata")
|
||||
|
||||
if let supportsSeek = parsedHeaderOutput?.supportsSeek, supportsSeek, seekOffset > 0 {
|
||||
urlRequest.addValue("bytes=\(seekOffset)", forHTTPHeaderField: "Range")
|
||||
}
|
||||
|
||||
return urlRequest
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: StreamEventsSource
|
||||
|
||||
extension RemoteAudioSource: StreamEventsSource {
|
||||
func openCompleted() {
|
||||
print("open completed")
|
||||
}
|
||||
|
||||
func dataAvailable() {
|
||||
guard inputStream != nil else { return }
|
||||
if httpStatusCode == 0 {
|
||||
guard parseHeader(response: httpResponse) else { return }
|
||||
if hasBytesAvailable {
|
||||
delegate?.dataAvailable(source: self)
|
||||
}
|
||||
} else {
|
||||
delegate?.dataAvailable(source: self)
|
||||
}
|
||||
}
|
||||
|
||||
func endOfFileOccurred() {
|
||||
delegate?.endOfFileOccured(source: self)
|
||||
}
|
||||
|
||||
func errorOccured() {
|
||||
delegate?.errorOccured(source: self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: StreamDelegate
|
||||
|
||||
extension RemoteAudioSource: StreamDelegate {
|
||||
public func stream(_: Stream, handle eventCode: Stream.Event) {
|
||||
switch eventCode {
|
||||
case .openCompleted:
|
||||
openCompleted()
|
||||
case .hasBytesAvailable:
|
||||
dataAvailable()
|
||||
case .endEncountered:
|
||||
endOfFileOccurred()
|
||||
case .errorOccurred:
|
||||
errorOccured()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,10 @@ final class FileAudioSource: NSObject, CoreAudioStreamSource {
|
||||
length = 0
|
||||
}
|
||||
|
||||
deinit {
|
||||
buffer.deallocate()
|
||||
}
|
||||
|
||||
func close() {
|
||||
guard let inputStream = inputStream else {
|
||||
return
|
||||
|
||||
@@ -189,9 +189,9 @@ public class RemoteAudioSource: AudioStreamSource {
|
||||
private func handleStreamEvent(event: NetworkDataStream.StreamResult) {
|
||||
switch event {
|
||||
case let .success(value):
|
||||
addStreamOperation { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if let data = value.data {
|
||||
if let data = 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)
|
||||
@@ -281,3 +281,4 @@ extension RemoteAudioSource: MetadataStreamSourceDelegate {
|
||||
delegate?.metadataReceived(data: data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -382,41 +382,43 @@ final class AudioFileStreamProcessor {
|
||||
var start = bufferContext.frameStartIndex
|
||||
var end = bufferContext.end
|
||||
|
||||
var framesLeftInBuffer = max(bufferContext.totalFrameCount &- used, 0)
|
||||
var framesLeftInBuffer = bufferContext.totalFrameCount - used
|
||||
rendererContext.lock.unlock()
|
||||
|
||||
if framesLeftInBuffer == 0 {
|
||||
rendererContext.lock.lock()
|
||||
let bufferContext = rendererContext.bufferContext
|
||||
used = bufferContext.frameUsedCount
|
||||
start = bufferContext.frameStartIndex
|
||||
end = bufferContext.end
|
||||
framesLeftInBuffer = max(bufferContext.totalFrameCount &- used, 0)
|
||||
rendererContext.lock.unlock()
|
||||
if framesLeftInBuffer > 0 {
|
||||
break packetProccess
|
||||
}
|
||||
if playerContext.disposedRequested
|
||||
|| playerContext.internalState == .disposed
|
||||
|| playerContext.internalState == .pendingNext
|
||||
|| playerContext.internalState == .stopped
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
if let playingEntry = playerContext.audioPlayingEntry,
|
||||
playingEntry.seekRequest.requested, playingEntry.calculatedBitrate() > 0
|
||||
{
|
||||
fileStreamCallback?(.proccessSource)
|
||||
if rendererContext.waiting.value {
|
||||
rendererContext.packetsSemaphore.signal()
|
||||
while true {
|
||||
rendererContext.lock.lock()
|
||||
let bufferContext = rendererContext.bufferContext
|
||||
used = bufferContext.frameUsedCount
|
||||
start = bufferContext.frameStartIndex
|
||||
end = (bufferContext.frameStartIndex + bufferContext.frameUsedCount) % bufferContext.totalFrameCount
|
||||
framesLeftInBuffer = bufferContext.totalFrameCount - used
|
||||
rendererContext.lock.unlock()
|
||||
if framesLeftInBuffer > 0 {
|
||||
break
|
||||
}
|
||||
if playerContext.disposedRequested
|
||||
|| playerContext.internalState == .disposed
|
||||
|| playerContext.internalState == .pendingNext
|
||||
|| playerContext.internalState == .stopped
|
||||
{
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
rendererContext.waiting.write { $0 = true }
|
||||
rendererContext.packetsSemaphore.wait()
|
||||
rendererContext.waiting.write { $0 = false }
|
||||
if let playingEntry = playerContext.audioPlayingEntry,
|
||||
playingEntry.seekRequest.requested, playingEntry.calculatedBitrate() > 0
|
||||
{
|
||||
fileStreamCallback?(.proccessSource)
|
||||
if rendererContext.waiting.value {
|
||||
rendererContext.packetsSemaphore.signal()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
rendererContext.waiting.write { $0 = true }
|
||||
rendererContext.packetsSemaphore.wait()
|
||||
rendererContext.waiting.write { $0 = false }
|
||||
}
|
||||
}
|
||||
|
||||
let localBufferList = AudioBufferList.allocate(maximumBuffers: 1)
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||

|
||||
|
||||
# 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).
|
||||
|
||||
#### Supported audio
|
||||
- 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)
|
||||
|
||||
|
||||
# Requirements
|
||||
- iOS 12.0+
|
||||
- Swift 5.x
|
||||
|
||||
# Using AudioStreaming
|
||||
|
||||
### Playing an audio source over HTTP
|
||||
Note: You need to keep a reference to the `AudioPlayer` object
|
||||
```
|
||||
let player = AudioPlayer()
|
||||
player.play(url: URL(string: "https://your-remote-url/to/audio-file.mp3")!)
|
||||
```
|
||||
|
||||
### Playing a local file
|
||||
```
|
||||
let player = AudioPlayer()
|
||||
player.play(url: URL(fileURLWithPath: "your-local-path/to/audio-file.mp3")!)
|
||||
```
|
||||
### Queueing audio files
|
||||
```
|
||||
let player = AudioPlayer()
|
||||
player.queue(url: URL(string: "https://your-remote-url/to/audio-file.mp3")!)
|
||||
player.queue(url: URL(fileURLWithPath: "your-local-path/to/audio-file.mp3")!)
|
||||
```
|
||||
|
||||
### Adjusting playback properties
|
||||
```
|
||||
let player = AudioPlayer()
|
||||
player.play(url: URL(fileURLWithPath: "your-local-path/to/audio-file.mp3")!)
|
||||
// adjust the playback rate
|
||||
player.rate = 2.0
|
||||
|
||||
// adjusting the volume
|
||||
player.volume = 0.5
|
||||
|
||||
// mute/unmute the audio
|
||||
player.mute = true
|
||||
|
||||
// pause the playback
|
||||
player.pause()
|
||||
|
||||
// resume the playback
|
||||
player.resume()
|
||||
|
||||
// stop the playback
|
||||
player.stop()
|
||||
|
||||
// seeking to to a time (in seconds)
|
||||
player.seek(to: 10)
|
||||
```
|
||||
|
||||
### Audio playback properties
|
||||
```
|
||||
let player = AudioPlayer()
|
||||
player.play(url: URL(fileURLWithPath: "your-local-path/to/audio-file.mp3")!)
|
||||
|
||||
// To get the audio file duration
|
||||
let duration = player.duration
|
||||
|
||||
// To get the progress of the player
|
||||
let progress = player.progress
|
||||
|
||||
// To get the state of the player, for possible values view the `AudioPlayerState` enum
|
||||
let state = player.state
|
||||
|
||||
// To get the stop reason of the player, for possible values view the `AudioPlayerStopReason` enum
|
||||
let state = player.stopReason
|
||||
```
|
||||
|
||||
### AudioPlayer Delegate
|
||||
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
|
||||
|
||||
```
|
||||
let player = AudioPlayer()
|
||||
player.play(url: URL(fileURLWithPath: "your-local-path/to/audio-file.mp3")!)
|
||||
|
||||
player.delegate = self // an object conforming to AudioPlayerDelegate
|
||||
|
||||
// observing the audio player state, provides the new and previous state of the player.
|
||||
func audioPlayerStateChanged(player: AudioPlayer, with newState: AudioPlayerState, previous: AudioPlayerState) {}
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
```
|
||||
let reverbNode = AVAudioUnitReverb()
|
||||
reverbNode.wetDryMix = 50
|
||||
|
||||
let player = AudioPlayer()
|
||||
// attach a single node
|
||||
player.attach(node: reverbNode)
|
||||
|
||||
// detach a single node
|
||||
player.detach(node: reverbNode)
|
||||
|
||||
// detach all custom added nodes
|
||||
player.detachCustomAttachedNodes()
|
||||
```
|
||||
|
||||
The example project shows an example of adding a custom `AVAudioUnitEQ` node for adding equaliser to the `AudioPlayer`
|
||||
|
||||
# Installation
|
||||
|
||||
### Cocoapods
|
||||
|
||||
[Cocoapods](https://cocoapods.org/) is a dependency manager for Cocoa projects. You can install it with the following command:
|
||||
```
|
||||
$ gem install cocoapods
|
||||
```
|
||||
|
||||
To intergrate AudioStreaming with [Cocoapods](https://cocoapods.org/) to your Xcode project add the following to your `Podfile`:
|
||||
```
|
||||
pod 'AudioStreaming'
|
||||
```
|
||||
|
||||
### Swift Package Manager
|
||||
|
||||
On Xcode 11.0+ you can add a new dependency by going to **File / Swift Packages / Add Package Dependency...**
|
||||
and enter package repository URL https://github.com/dimitris-c/AudioStreaming.git, then follow the instructions.
|
||||
|
||||
### Carthage
|
||||
|
||||
[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with frameworks.
|
||||
|
||||
You can install Carthage with Homebrew using the following command:
|
||||
```
|
||||
$ brew update
|
||||
$ brew install carthage
|
||||
```
|
||||
|
||||
To integrate AudioStreaming into your Xcode project using Carthage, add the following to your `Cartfile`:
|
||||
```
|
||||
github "dimitris-c/AudioStreaming"
|
||||
```
|
||||
Visit [installation instructions](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application) on Carthage to install the framework
|
||||
|
||||
# Licence
|
||||
|
||||
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).
|
||||
Big 🙏 to Thong Nguyen (@tumtumtum) and Matt Gallagher (@mattgallagher) for [AudioStreamer](https://github.com/mattgallagher/AudioStreamer)
|
||||
Reference in New Issue
Block a user