-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Pushpsen
authored and
Pushpsen
committed
May 18, 2020
1 parent
6ce1bc0
commit dbb7c40
Showing
24 changed files
with
2,580 additions
and
248 deletions.
There are no files selected for viewing
191 changes: 191 additions & 0 deletions
191
Library/Resources/Extensions/AudioWaveForm/AudioPlayerManager.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
// | ||
// AudioPlayerManager.swift | ||
// ela | ||
// | ||
// Created by Bastien Falcou on 4/14/16. | ||
// Copyright © 2016 Fueled. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
import AVFoundation | ||
import UIKit | ||
|
||
final class AudioPlayerManager: NSObject { | ||
static let shared = AudioPlayerManager() | ||
|
||
var isRunning: Bool { | ||
guard let audioPlayer = self.audioPlayer, audioPlayer.isPlaying else { | ||
return false | ||
} | ||
return true | ||
} | ||
|
||
private var audioPlayer: AVAudioPlayer? | ||
private var audioMeteringLevelTimer: Timer? | ||
|
||
// MARK: - Reinit and play from the beginning | ||
|
||
func play(at url: URL, with audioVisualizationTimeInterval: TimeInterval = 0.05) throws -> TimeInterval { | ||
if AudioRecorderManager.shared.isRunning { | ||
print("Audio Player did fail to start: AVFoundation is recording") | ||
throw AudioErrorType.alreadyRecording | ||
} | ||
|
||
if self.isRunning { | ||
print("Audio Player did fail to start: already playing a file") | ||
throw AudioErrorType.alreadyPlaying | ||
} | ||
|
||
if !URL.checkPath(url.path) { | ||
print("Audio Player did fail to start: file doesn't exist") | ||
throw AudioErrorType.audioFileWrongPath | ||
} | ||
|
||
try self.audioPlayer = AVAudioPlayer(contentsOf: url) | ||
self.setupPlayer(with: audioVisualizationTimeInterval) | ||
print("Started to play sound") | ||
|
||
return self.audioPlayer!.duration | ||
} | ||
|
||
func play(_ data: Data, with audioVisualizationTimeInterval: TimeInterval = 0.05) throws -> TimeInterval { | ||
try self.audioPlayer = AVAudioPlayer(data: data) | ||
self.setupPlayer(with: audioVisualizationTimeInterval) | ||
print("Started to play sound") | ||
|
||
return self.audioPlayer!.duration | ||
} | ||
|
||
private func setupPlayer(with audioVisualizationTimeInterval: TimeInterval) { | ||
if let player = self.audioPlayer { | ||
player.play() | ||
player.isMeteringEnabled = true | ||
player.delegate = self | ||
|
||
self.audioMeteringLevelTimer = Timer.scheduledTimer(timeInterval: audioVisualizationTimeInterval, target: self, | ||
selector: #selector(AudioPlayerManager.timerDidUpdateMeter), userInfo: nil, repeats: true) | ||
} | ||
} | ||
|
||
// MARK: - Resume and pause current if exists | ||
|
||
func resume() throws -> TimeInterval { | ||
if self.audioPlayer?.play() == false { | ||
print("Audio Player did fail to resume for internal reason") | ||
throw AudioErrorType.internalError | ||
} | ||
|
||
print("Resumed sound") | ||
return self.audioPlayer!.duration - self.audioPlayer!.currentTime | ||
} | ||
|
||
func pause() throws { | ||
if !self.isRunning { | ||
print("Audio Player did fail to start: there is nothing currently playing") | ||
throw AudioErrorType.notCurrentlyPlaying | ||
} | ||
|
||
self.audioPlayer?.pause() | ||
print("Paused current playing sound") | ||
} | ||
|
||
func stop() throws { | ||
if !self.isRunning { | ||
print("Audio Player did fail to stop: there is nothing currently playing") | ||
throw AudioErrorType.notCurrentlyPlaying | ||
} | ||
|
||
self.audioPlayer?.stop() | ||
print("Audio player stopped") | ||
} | ||
|
||
// MARK: - Private | ||
|
||
@objc private func timerDidUpdateMeter() { | ||
if self.isRunning { | ||
self.audioPlayer!.updateMeters() | ||
let averagePower = self.audioPlayer!.averagePower(forChannel: 0) | ||
let percentage: Float = pow(10, (0.05 * averagePower)) | ||
NotificationCenter.default.post(name: .audioPlayerManagerMeteringLevelDidUpdateNotification, object: self, userInfo: [audioPercentageUserInfoKey: percentage]) | ||
} | ||
} | ||
} | ||
|
||
extension AudioPlayerManager: AVAudioPlayerDelegate { | ||
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { | ||
NotificationCenter.default.post(name: .audioPlayerManagerMeteringLevelDidFinishNotification, object: self) | ||
} | ||
} | ||
|
||
extension Notification.Name { | ||
static let audioPlayerManagerMeteringLevelDidUpdateNotification = Notification.Name("AudioPlayerManagerMeteringLevelDidUpdateNotification") | ||
static let audioPlayerManagerMeteringLevelDidFinishNotification = Notification.Name("AudioPlayerManagerMeteringLevelDidFinishNotification") | ||
} | ||
|
||
|
||
|
||
enum AudioErrorType: Error { | ||
case alreadyRecording | ||
case alreadyPlaying | ||
case notCurrentlyPlaying | ||
case audioFileWrongPath | ||
case recordFailed | ||
case playFailed | ||
case recordPermissionNotGranted | ||
case internalError | ||
} | ||
|
||
extension AudioErrorType: LocalizedError { | ||
public var errorDescription: String? { | ||
switch self { | ||
case .alreadyRecording: | ||
return "The application is currently recording sounds" | ||
case .alreadyPlaying: | ||
return "The application is already playing a sound" | ||
case .notCurrentlyPlaying: | ||
return "The application is not currently playing" | ||
case .audioFileWrongPath: | ||
return "Invalid path for audio file" | ||
case .recordFailed: | ||
return "Unable to record sound at the moment, please try again" | ||
case .playFailed: | ||
return "Unable to play sound at the moment, please try again" | ||
case .recordPermissionNotGranted: | ||
return "Unable to record sound because the permission has not been granted. This can be changed in your settings." | ||
case .internalError: | ||
return "An error occured while trying to process audio command, please try again" | ||
} | ||
} | ||
} | ||
|
||
|
||
extension URL { | ||
static func checkPath(_ path: String) -> Bool { | ||
let isFileExist = FileManager.default.fileExists(atPath: path) | ||
return isFileExist | ||
} | ||
|
||
static func documentsPath(forFileName fileName: String) -> URL? { | ||
let documents = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] | ||
let writePath = URL(string: documents)!.appendingPathComponent(fileName) | ||
|
||
var directory: ObjCBool = ObjCBool(false) | ||
if FileManager.default.fileExists(atPath: documents, isDirectory:&directory) { | ||
return directory.boolValue ? writePath : nil | ||
} | ||
return nil | ||
} | ||
} | ||
|
||
extension UIViewController { | ||
func showAlert(with error: Error) { | ||
let alertController = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert) | ||
alertController.addAction(UIAlertAction(title: "OK", style: .cancel) { _ in | ||
alertController.dismiss(animated: true, completion: nil) | ||
}) | ||
|
||
DispatchQueue.main.async { | ||
self.present(alertController, animated: true, completion: nil) | ||
} | ||
} | ||
} |
157 changes: 157 additions & 0 deletions
157
Library/Resources/Extensions/AudioWaveForm/AudioRecorderManager.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
// | ||
// AudioRecorderManager.swift | ||
// ela | ||
// | ||
// Created by Bastien Falcou on 4/14/16. | ||
// Copyright © 2016 Fueled. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
import AVFoundation | ||
|
||
let audioPercentageUserInfoKey = "percentage" | ||
|
||
final class AudioRecorderManager: NSObject { | ||
let audioFileNamePrefix = "audio.m4a" | ||
let encoderBitRate: Int = 80000 | ||
let numberOfChannels: Int = 2 | ||
let sampleRate: Double = 44100.0 | ||
|
||
static let shared = AudioRecorderManager() | ||
|
||
var isPermissionGranted = false | ||
var isRunning: Bool { | ||
guard let recorder = self.recorder, recorder.isRecording else { | ||
return false | ||
} | ||
return true | ||
} | ||
|
||
var currentRecordPath: URL? | ||
|
||
private var recorder: AVAudioRecorder? | ||
private var audioMeteringLevelTimer: Timer? | ||
|
||
func askPermission(completion: ((Bool) -> Void)? = nil) { | ||
AVAudioSession.sharedInstance().requestRecordPermission { [weak self] granted in | ||
self?.isPermissionGranted = granted | ||
completion?(granted) | ||
print("Audio Recorder did not grant permission") | ||
} | ||
} | ||
|
||
func startRecording(with audioVisualizationTimeInterval: TimeInterval = 0.05, completion: @escaping (URL?, Error?) -> Void) { | ||
func startRecordingReturn() { | ||
do { | ||
completion(try internalStartRecording(with: audioVisualizationTimeInterval), nil) | ||
} catch { | ||
completion(nil, error) | ||
} | ||
} | ||
|
||
if !self.isPermissionGranted { | ||
self.askPermission { granted in | ||
startRecordingReturn() | ||
} | ||
} else { | ||
startRecordingReturn() | ||
} | ||
} | ||
|
||
fileprivate func internalStartRecording(with audioVisualizationTimeInterval: TimeInterval) throws -> URL { | ||
if self.isRunning { | ||
throw AudioErrorType.alreadyPlaying | ||
} | ||
|
||
let recordSettings = [ | ||
AVFormatIDKey: NSNumber(value:kAudioFormatMPEG4AAC), | ||
AVEncoderAudioQualityKey : AVAudioQuality.max.rawValue, | ||
AVEncoderBitRateKey : self.encoderBitRate, | ||
AVNumberOfChannelsKey: self.numberOfChannels, | ||
AVSampleRateKey : self.sampleRate | ||
] as [String : Any] | ||
|
||
guard let path = URL.documentsPath(forFileName: self.audioFileNamePrefix) else { | ||
print("Incorrect path for new audio file") | ||
throw AudioErrorType.audioFileWrongPath | ||
} | ||
|
||
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default, options: .defaultToSpeaker) | ||
try AVAudioSession.sharedInstance().setActive(true) | ||
|
||
self.recorder = try AVAudioRecorder(url: path, settings: recordSettings) | ||
self.recorder!.delegate = self | ||
self.recorder!.isMeteringEnabled = true | ||
|
||
if !self.recorder!.prepareToRecord() { | ||
print("Audio Recorder prepare failed") | ||
throw AudioErrorType.recordFailed | ||
} | ||
|
||
if !self.recorder!.record() { | ||
print("Audio Recorder start failed") | ||
throw AudioErrorType.recordFailed | ||
} | ||
|
||
self.audioMeteringLevelTimer = Timer.scheduledTimer(timeInterval: audioVisualizationTimeInterval, target: self, | ||
selector: #selector(AudioRecorderManager.timerDidUpdateMeter), userInfo: nil, repeats: true) | ||
|
||
print("Audio Recorder did start - creating file at index: \(path.absoluteString)") | ||
|
||
self.currentRecordPath = path | ||
return path | ||
} | ||
|
||
func stopRecording() throws { | ||
self.audioMeteringLevelTimer?.invalidate() | ||
self.audioMeteringLevelTimer = nil | ||
|
||
if !self.isRunning { | ||
print("Audio Recorder did fail to stop") | ||
throw AudioErrorType.notCurrentlyPlaying | ||
} | ||
|
||
self.recorder!.stop() | ||
print("Audio Recorder did stop successfully") | ||
} | ||
|
||
func reset() throws { | ||
if self.isRunning { | ||
print("Audio Recorder tried to remove recording before stopping it") | ||
throw AudioErrorType.alreadyRecording | ||
} | ||
|
||
self.recorder?.deleteRecording() | ||
self.recorder = nil | ||
self.currentRecordPath = nil | ||
|
||
print("Audio Recorder did remove current record successfully") | ||
} | ||
|
||
@objc func timerDidUpdateMeter() { | ||
if self.isRunning { | ||
self.recorder!.updateMeters() | ||
let averagePower = recorder!.averagePower(forChannel: 0) | ||
let percentage: Float = pow(10, (0.05 * averagePower)) | ||
NotificationCenter.default.post(name: .audioRecorderManagerMeteringLevelDidUpdateNotification, object: self, userInfo: [audioPercentageUserInfoKey: percentage]) | ||
} | ||
} | ||
} | ||
|
||
extension AudioRecorderManager: AVAudioRecorderDelegate { | ||
func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) { | ||
NotificationCenter.default.post(name: .audioRecorderManagerMeteringLevelDidFinishNotification, object: self) | ||
print("Audio Recorder finished successfully") | ||
} | ||
|
||
func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) { | ||
NotificationCenter.default.post(name: .audioRecorderManagerMeteringLevelDidFailNotification, object: self) | ||
print("Audio Recorder error") | ||
} | ||
} | ||
|
||
extension Notification.Name { | ||
static let audioRecorderManagerMeteringLevelDidUpdateNotification = Notification.Name("AudioRecorderManagerMeteringLevelDidUpdateNotification") | ||
static let audioRecorderManagerMeteringLevelDidFinishNotification = Notification.Name("AudioRecorderManagerMeteringLevelDidFinishNotification") | ||
static let audioRecorderManagerMeteringLevelDidFailNotification = Notification.Name("AudioRecorderManagerMeteringLevelDidFailNotification") | ||
} |
36 changes: 36 additions & 0 deletions
36
Library/Resources/Extensions/AudioWaveForm/Classes/AVAudioFileExtensions.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
// | ||
// AVAudioFileExtensions.swift | ||
// Pods-SoundWave_Example | ||
// | ||
// Created by Bastien Falcou on 4/21/19. | ||
// Inspired from https://stackoverflow.com/a/52280271 | ||
// | ||
|
||
import AVFoundation | ||
|
||
extension AVAudioFile { | ||
func buffer() throws -> [[Float]] { | ||
let format = AVAudioFormat(commonFormat: .pcmFormatFloat32, | ||
sampleRate: self.fileFormat.sampleRate, | ||
channels: self.fileFormat.channelCount, | ||
interleaved: false) | ||
let buffer = AVAudioPCMBuffer(pcmFormat: format!, frameCapacity: UInt32(self.length))! | ||
try self.read(into: buffer, frameCount: UInt32(self.length)) | ||
return self.analyze(buffer: buffer) | ||
} | ||
|
||
private func analyze(buffer: AVAudioPCMBuffer) -> [[Float]] { | ||
let channelCount = Int(buffer.format.channelCount) | ||
let frameLength = Int(buffer.frameLength) | ||
var result = Array(repeating: [Float](repeatElement(0, count: frameLength)), count: channelCount) | ||
for channel in 0..<channelCount { | ||
for sampleIndex in 0..<frameLength { | ||
let sqrtV = sqrt(buffer.floatChannelData![channel][sampleIndex*buffer.stride]/Float(buffer.frameLength)) | ||
let dbPower = 20 * log10(sqrtV) | ||
result[channel][sampleIndex] = dbPower | ||
} | ||
} | ||
print(result) | ||
return result | ||
} | ||
} |
Oops, something went wrong.