Compare commits

...

8 Commits

Author SHA1 Message Date
Tanha 4b57fee75c Release 2.2.0 2019-11-29 17:18:33 -08:00
tanhakabir fc9c43a23c Update accessing bytes of Data for Swift 5 (#17) 2019-11-29 17:16:53 -08:00
Tanha fd4e4e3b77 Release 2.1.0 2019-11-28 21:37:16 -08:00
tanhakabir f1200252be Update to Swift 5 (#16) 2019-11-28 21:36:30 -08:00
Jonathan Mercer 046e64b2b8 Update README.md (#15)
* Update readme

- Completely re-worded the first paragraph
- Re-worded some sentences that confused me
- Moved audio manipulation in the end. Assuming that advanced users will read it there while keeping it hidden from other users

* Update README.md
2019-11-27 16:36:24 -08:00
Tanha ad9e40ad1c Update README.md 2019-11-26 01:37:53 -08:00
Tanha f19eaf7ec9 Release 2.0.1 2019-11-26 01:04:51 -08:00
tanhakabir 012291c1c9 Merge pull request #14 from tanhakabir/open_nodes_interface
Open interface to control audio manipulation nodes
2019-11-26 01:03:11 -08:00
8 changed files with 127 additions and 58 deletions
+9 -4
View File
@@ -14,6 +14,7 @@
79D8DF73FA7CDD6E266BAE71D46E035F /* Pods-SwiftAudioPlayer_Tests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 50C71346CE708A211A5AFAC20BAE48CB /* Pods-SwiftAudioPlayer_Tests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
831B263D357A5FA2DDC7B1AE4B374092 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A16F4CFC63FAC439D7A04994F579A03 /* Foundation.framework */; };
8F93DB166237195ED222EE55B6404625 /* Pods-SwiftAudioPlayer_Example-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B0B76CB1439F4D361322144E5A65C3A /* Pods-SwiftAudioPlayer_Example-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
A40DBE292391D9CA00F86146 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40DBE282391D9C900F86146 /* Data.swift */; };
A41AA0D2238BB9B600A467E1 /* SAPlayingStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = A41AA0D1238BB9B600A467E1 /* SAPlayingStatus.swift */; };
A4681FC6220113880018AB51 /* SAPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F8D2200E00E0018AB51 /* SAPlayer.swift */; };
A4681FC72201138B0018AB51 /* SAPlayerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F912200E1950018AB51 /* SAPlayerDelegate.swift */; };
@@ -96,6 +97,7 @@
99925F09FC9C6EA4B9C0508F4E2D1FE2 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A19C8F889C787C19BE4123C1896AF501 /* Pods-SwiftAudioPlayer_Example-resources.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-SwiftAudioPlayer_Example-resources.sh"; sourceTree = "<group>"; };
A39F2A138CF40C1051CA9E227429A86D /* SwiftAudioPlayer.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = SwiftAudioPlayer.modulemap; sourceTree = "<group>"; };
A40DBE282391D9C900F86146 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
A41AA0D1238BB9B600A467E1 /* SAPlayingStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayingStatus.swift; sourceTree = "<group>"; };
A4523BC8220A0B3C0079C4BC /* Credited_LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = Credited_LICENSE; sourceTree = "<group>"; };
A4681F802200D0500018AB51 /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = "<group>"; };
@@ -273,6 +275,7 @@
A4681F872200DAD50018AB51 /* DirectorThreadSafeClosures.swift */,
A4681F892200DB3C0018AB51 /* Date.swift */,
A4681F962200E2E20018AB51 /* URL.swift */,
A40DBE282391D9C900F86146 /* Data.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -488,16 +491,17 @@
LastSwiftMigration = 1010;
};
E50DAD13FFD3FC8036073A58BF8423D4 = {
LastSwiftMigration = 1010;
LastSwiftMigration = 1120;
};
};
};
buildConfigurationList = 2D8E8EC45A3A1A1D94AE762CB5028504 /* Build configuration list for PBXProject "Pods" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 7DB346D0F39D3F0E887471402A8071AB;
productRefGroup = 21D946895A4F57F51246F3EBCF330719 /* Products */;
@@ -523,6 +527,7 @@
A4681FD2220113B20018AB51 /* AudioParser.swift in Sources */,
A4681FCF220113A40018AB51 /* AudioConverterListener.swift in Sources */,
A4681FE1220113E70018AB51 /* Constants.swift in Sources */,
A40DBE292391D9CA00F86146 /* Data.swift in Sources */,
A4FBA6B5221B74C900D5A353 /* SALockScreenInfo.swift in Sources */,
A4681FC6220113880018AB51 /* SAPlayer.swift in Sources */,
A4FBA6B7221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift in Sources */,
@@ -676,7 +681,7 @@
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
@@ -708,7 +713,7 @@
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
@@ -213,19 +213,19 @@
607FACCF1AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = R2392A68YQ;
LastSwiftMigration = 1010;
LastSwiftMigration = 1120;
};
607FACE41AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = R2392A68YQ;
LastSwiftMigration = 1010;
LastSwiftMigration = 1120;
TestTargetID = 607FACCF1AFB9204008FA782;
};
};
};
buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "SwiftAudioPlayer" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
@@ -481,7 +481,7 @@
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
@@ -496,7 +496,7 @@
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
};
name = Release;
};
@@ -517,7 +517,7 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudioPlayer_Example.app/SwiftAudioPlayer_Example";
};
name = Debug;
@@ -535,7 +535,7 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudioPlayer_Example.app/SwiftAudioPlayer_Example";
};
name = Release;
+43 -35
View File
@@ -4,21 +4,24 @@
[![License](https://img.shields.io/cocoapods/l/SwiftAudioPlayer.svg?style=flat)](https://cocoapods.org/pods/SwiftAudioPlayer)
[![Platform](https://img.shields.io/cocoapods/p/SwiftAudioPlayer.svg?style=flat)](https://cocoapods.org/pods/SwiftAudioPlayer)
Swift based audio player that is able to both stream remote audio and play locally saved audio, while performing audio manipulations in real-time. Underlying using AVAudioEngine, and you can change the rate of audio (up to 32x), change pitch, and [other audio enhancements](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements).
Swift-based audio player with AVAudioEngine as its base. Allows for: streaming online audio, playing local file, changing audio speed (3.5X, 4X, 32X), pitch, and real-time audio manipulation using custom [audio enhancements](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements).
This player was originally developed to be used in a [podcast player](https://chameleonpodcast.com/). We had originally used AVPlayer for playing audio but we wanted to manipulate audio that was being streamed. We set up AVAudioEngine at first just to play a file saved on the phone and it worked great, but AVAudioEngine on its own doesn't support streaming audio as easily as AVPlayer.
This player was built for [podcasting](https://chameleonpodcast.com/). We originally used AVPlayer for playing audio but we wanted to manipulate audio that was being streamed. We set up AVAudioEngine at first just to play a file saved on the phone and it worked great, but AVAudioEngine on its own doesn't support streaming audio as easily as AVPlayer.
Thus, using [AudioToolbox](https://developer.apple.com/documentation/audiotoolbox), we are able to stream audio and convert the downloaded data into usable data for the AVAudioEngine to play. For an overview of our solution check out our [blog post](https://medium.com/chameleon-podcast/creating-an-advanced-streaming-audio-engine-for-ios-9fbc7aef4115).
### Requirements
SwiftAudioPlayer is only available for iOS 10.0 and higher.
iOS 10.0 and higher.
## Getting Started
### Example Project
### Running the Example Project
To run the example project, clone the repo, and run `pod install` from the Example directory first.
1. Clone repo
2. CD to directory
3. run `pod install` in terminal
4. Run
### Installation
@@ -46,7 +49,7 @@ let info = SALockScreenInfo(title: "Random audio", artist: "Foo", artwork: UIIma
SAPlayer.shared.mediaInfo = info
```
To receive streaming progress:
To receive streaming progress (for buffer progress %):
```swift
@IBOutlet weak var bufferProgress: UIProgressView!
@@ -115,7 +118,7 @@ SwiftAudioPlayer is available under the MIT license. See the LICENSE file for mo
Access the player and all of its fields and functions through `SAPlayer.shared`.
### Playing Audio
### Playing Audio (Basic Commands)
To set up player with audio to play, use either:
* `initializeSavedAudio(withSavedUrl url: URL, mediaInfo: SALockScreenInfo?)` to play audio that is saved on the device.
@@ -125,11 +128,7 @@ Both of these expect a URL of the location of the audio and an optional media in
For streaming remote audio, subscribe to `SAPlayer.Updates.StreamingBuffer` for updates on streaming progress.
#### Important
Any audio manipulation intended to on the audio must have the nodes anticipated to use finalized before initialize is called. Look at [audio manipulation documentation](#realtime-audio-manipulation) for more information.
All other basic controls are available:
Basic controls available:
```swift
play()
pause()
@@ -139,32 +138,13 @@ skipForward()
skipBackwards()
```
### Realtime Audio Manipulation
All audio effects on the player is done through [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/avaudiounit) nodes. These include adding reverb, changing pitch and playback rate, and adding distortion. Full list of effects available [here](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements).
The effects intended to use are stored in `audioModifiers` as a list of nodes. These nodes are in the order that the engine will attach them to one another.
**Note:** By default `SAPlayer` starts off with one node, an [AVAudioUnitTimePitch](https://developer.apple.com/documentation/avfoundation/avaudiounittimepitch) node, that is set to change the rate of audio without changing the pitch of the audio (intended for changing the rate of spoken word).
#### Important
All the nodes intended to be used on the playing audio must be finalized before calling `initializeSavedAudio(...)` or `initializeRemoteAudio(...)`. Any changes to list of nodes after initialize is called for a given audio file will not be reflected in playback.
Once all nodes are added to `audioModifiers` and the player has been initialized, any manipulations done with the nodes are performed in realtime. The example app shows manipulating the playback rate in realtime:
```swift
let speed = rateSlider.value
if let node = SAPlayer.shared.audioModifiers[0] as? AVAudioUnitTimePitch {
node.rate = speed
SAPlayer.shared.playbackRateOfAudioChanged(rate: speed)
}
```
**Note:** if the rate of the audio is changed, `playbackRateOfAudioChanged` should also be called to update the lockscreen's media player.
The engine can handle audio manipulations like speed, pitch, effects, etc. To do this, nodes for effects must be finalized before initialize is called. Look at [audio manipulation documentation](#realtime-audio-manipulation) for more information.
### Lockscreen Media Player
Update and set what displays on the lockscreen's media player when the player is active.
Update and set what displays on the lockscreen's media player when the player is active.
`skipForwardSeconds` and `skipBackwardSeconds` for the intervals to skip forward and back with.
@@ -196,7 +176,7 @@ func application(_ application: UIApplication, handleEventsForBackgroundURLSessi
### Downloading
Downloads will be held on pause when active stream is started, and will resume downloads when streaming is done.
All downloads will be paused when audio is streamed from a URL. They will automatically resume when streaming is done.
Use the following to start downloading audio in the background:
@@ -233,6 +213,8 @@ Delete downloaded audio if it exists:
func deleteDownloaded(withSavedUrl url: URL)
```
**NOTE:** You're in charge or clearing downloads when your don't need them anymore
## SAPlayer.Updates
Receive updates for changing values from the player, such as the duration, elapsed time of playing audio, download progress, and etc.
@@ -265,7 +247,7 @@ Subscribe to this to update views on changes in position of which part of audio
### Duration
Payload = `Double`
Changes in the duration of the current initialized audio. Especially helpful for audio that is being streamed and can change with more data.
Changes in the duration of the current initialized audio. Especially helpful for audio that is being streamed and can change with more data. The engine makes a best effort guess as to the duration of the audio. The guess gets better with more bytes streamed from the web.
### PlayingStatus
Payload = `SAPlayingStatus`
@@ -283,3 +265,29 @@ For progress of downloading audio that saves to the phone for playback later, lo
Payload = `Double`
Changes in the progress of downloading audio in the background. This does not correspond to progress in streaming downloads, look at StreamingBuffer for streaming progress.
## Audio Effects
### Realtime Audio Manipulation
All audio effects on the player is done through [AVAudioUnit](https://developer.apple.com/documentation/avfoundation/avaudiounit) nodes. These include adding reverb, changing pitch and playback rate, and adding distortion. Full list of effects available [here](https://developer.apple.com/documentation/avfoundation/audio_track_engineering/audio_engine_building_blocks/audio_enhancements).
The effects intended to use are stored in `audioModifiers` as a list of nodes. These nodes are in the order that the engine will attach them to one another.
**Note:** By default `SAPlayer` starts off with one node, an [AVAudioUnitTimePitch](https://developer.apple.com/documentation/avfoundation/avaudiounittimepitch) node, that is set to change the rate of audio without changing the pitch of the audio (intended for changing the rate of spoken word).
#### Important
All the nodes intended to be used on the playing audio must be finalized before calling `initializeSavedAudio(...)` or `initializeRemoteAudio(...)`. Any changes to list of nodes after initialize is called for a given audio file will not be reflected in playback.
Once all nodes are added to `audioModifiers` and the player has been initialized, any manipulations done with the nodes are performed in realtime. The example app shows manipulating the playback rate in realtime:
```swift
let speed = rateSlider.value
if let node = SAPlayer.shared.audioModifiers[0] as? AVAudioUnitTimePitch {
node.rate = speed
SAPlayer.shared.playbackRateOfAudioChanged(rate: speed)
}
```
**Note:** if the rate of the audio is changed, `playbackRateOfAudioChanged` should also be called to update the lockscreen's media player.
@@ -70,7 +70,7 @@ func ConverterListener(_ converter: AudioConverterRef, _ packetCount: UnsafeMuta
let packetByteCount = packet.count //this is not the count of an array
ioData.pointee.mNumberBuffers = 1
ioData.pointee.mBuffers.mData = UnsafeMutableRawPointer.allocate(byteCount: packetByteCount, alignment: 0)
_ = packet.withUnsafeMutableBytes({ (bytes: UnsafeMutablePointer<UInt8>) in
_ = packet.accessMutableBytes({ (bytes: UnsafeMutablePointer<UInt8>) in
memcpy((ioData.pointee.mBuffers.mData?.assumingMemoryBound(to: UInt8.self))!, bytes, packetByteCount)
})
ioData.pointee.mBuffers.mDataByteSize = UInt32(packetByteCount)
+1 -1
View File
@@ -295,7 +295,7 @@ extension AudioParser: AudioThrottleDelegate {
let sID = self.streamID!
let dataSize = data.count
let _ = try data.withUnsafeBytes({ (bytes:UnsafePointer<UInt8>) in
_ = try data.accessBytes({ (bytes: UnsafePointer<UInt8>) in
let result:OSStatus = AudioFileStreamParseBytes(sID, UInt32(dataSize), bytes, [])
guard result == noErr else {
Log.monitor(ParserError.failedToParseBytes(result).errorDescription as Any)
@@ -271,21 +271,18 @@ extension AudioDownloadWorker {
return lhs.id == rhs.id && lhs.remoteUrl == rhs.remoteUrl
}
var hashValue: Int {
return id.hashValue ^ remoteUrl.hashValue
}
let id: ID
let remoteUrl: URL
let rank: Int
var completionHandlers: [(URL) -> ()]
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(remoteUrl)
}
}
private class ActiveDownload: Hashable {
var hashValue: Int {
return info.id.hashValue ^ task.hashValue
}
static func == (lhs: AudioDownloadWorker.ActiveDownload, rhs: AudioDownloadWorker.ActiveDownload) -> Bool {
return lhs.info.id == rhs.info.id
}
@@ -299,6 +296,11 @@ extension AudioDownloadWorker {
self.info = info
self.task = task
}
func hash(into hasher: inout Hasher) {
hasher.combine(info.id)
hasher.combine(task)
}
}
}
+54
View File
@@ -0,0 +1,54 @@
//
// Data.swift
// SwiftAudioPlayer
//
// Created by Tanha Kabir on 2019-11-29.
// Copyright © 2019 Tanha Kabir, Jon Mercer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Foundation
extension Data {
// Introduced in Swift 5, withUnsafeBytes using UnsafePointers is deprecated
// https://mjtsai.com/blog/2019/03/27/swift-5-released/
func accessBytes<R>(_ body: (UnsafePointer<UInt8>) throws -> R) rethrows -> R {
return try withUnsafeBytes { (rawBufferPointer: UnsafeRawBufferPointer) -> R in
let unsafeBufferPointer = rawBufferPointer.bindMemory(to: UInt8.self)
guard let unsafePointer = unsafeBufferPointer.baseAddress else {
Log.error("")
var int: UInt8 = 0
return try body(&int)
}
return try body(unsafePointer)
}
}
mutating func accessMutableBytes<R>(_ body: (UnsafeMutablePointer<UInt8>) throws -> R) rethrows -> R {
return try withUnsafeMutableBytes { (rawBufferPointer: UnsafeMutableRawBufferPointer) -> R in
let unsafeMutableBufferPointer = rawBufferPointer.bindMemory(to: UInt8.self)
guard let unsafeMutablePointer = unsafeMutableBufferPointer.baseAddress else {
Log.error("")
var int: UInt8 = 0
return try body(&int)
}
return try body(unsafeMutablePointer)
}
}
}
+2 -2
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudioPlayer'
s.version = '1.3.0'
s.version = '2.2.0'
s.summary = 'SwiftAudioPlayer is a Swift based audio player that can handle streaming from a remote location and audio manipulation.'
# This description is used to generate tags and improve search results.
@@ -31,7 +31,7 @@ SwiftAudioPlayer is a Swift based audio player that can handle streaming from a
s.ios.deployment_target = '10.0'
s.source_files = 'Source/**/*'
s.swift_version = '4.2'
s.swift_version = '5.0'
# s.resource_bundles = {
# 'SwiftAudioPlayer' => ['SwiftAudioPlayer/Assets/*.png']