Skip to content

Commit

Permalink
v2.0.2
Browse files Browse the repository at this point in the history
  • Loading branch information
Pushpsen authored and Pushpsen committed May 18, 2020
1 parent 6ce1bc0 commit dbb7c40
Show file tree
Hide file tree
Showing 24 changed files with 2,580 additions and 248 deletions.
191 changes: 191 additions & 0 deletions Library/Resources/Extensions/AudioWaveForm/AudioPlayerManager.swift
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 Library/Resources/Extensions/AudioWaveForm/AudioRecorderManager.swift
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")
}
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
}
}
Loading

0 comments on commit dbb7c40

Please sign in to comment.