// // AudioRecPlay.swift // SimpleX (iOS) // // Created by Evgeny on 19/11/2022. // Copyright © 2022 SimpleX Chat. All rights reserved. // import Foundation import AVFoundation import SwiftUI import SimpleXChat class AudioRecorder { var onTimer: ((TimeInterval) -> Void)? var onFinishRecording: (() -> Void)? var audioRecorder: AVAudioRecorder? var recordingTimer: Timer? init(onTimer: @escaping ((TimeInterval) -> Void), onFinishRecording: @escaping (() -> Void)) { self.onTimer = onTimer self.onFinishRecording = onFinishRecording } enum StartError { case permission case error(String) } func start(fileName: String) async -> StartError? { let av = AVAudioSession.sharedInstance() if !(await checkPermission()) { return .permission } do { try av.setCategory(AVAudioSession.Category.playAndRecord, options: .defaultToSpeaker) try av.setActive(true) let settings: [String : Any] = [ AVFormatIDKey: kAudioFormatMPEG4AAC, AVSampleRateKey: 16000, AVEncoderBitRateKey: 32000, AVEncoderBitRateStrategyKey: AVAudioBitRateStrategy_VariableConstrained, AVNumberOfChannelsKey: 1 ] let url = getAppFilePath(fileName) audioRecorder = try AVAudioRecorder(url: url, settings: settings) audioRecorder?.record(forDuration: MAX_VOICE_MESSAGE_LENGTH) await MainActor.run { AppDelegate.keepScreenOn(true) recordingTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in guard let time = self.audioRecorder?.currentTime else { return } self.onTimer?(time) if time >= MAX_VOICE_MESSAGE_LENGTH { self.stop() self.onFinishRecording?() } } } return nil } catch let error { await MainActor.run { AppDelegate.keepScreenOn(false) } try? av.setCategory(AVAudioSession.Category.soloAmbient) logger.error("AudioRecorder startAudioRecording error \(error.localizedDescription)") return .error(error.localizedDescription) } } func stop() { if let recorder = audioRecorder { recorder.stop() } audioRecorder = nil if let timer = recordingTimer { timer.invalidate() } recordingTimer = nil AppDelegate.keepScreenOn(false) try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.soloAmbient) } private func checkPermission() async -> Bool { let av = AVAudioSession.sharedInstance() switch av.recordPermission { case .granted: return true case .denied: return false case .undetermined: return await withCheckedContinuation { cont in DispatchQueue.main.async { av.requestRecordPermission { allowed in cont.resume(returning: allowed) } } } @unknown default: return false } } } class AudioPlayer: NSObject, AVAudioPlayerDelegate { var onTimer: ((TimeInterval) -> Void)? var onFinishPlayback: (() -> Void)? var audioPlayer: AVAudioPlayer? var playbackTimer: Timer? init(onTimer: @escaping ((TimeInterval) -> Void), onFinishPlayback: @escaping (() -> Void)) { self.onTimer = onTimer self.onFinishPlayback = onFinishPlayback } func start(fileSource: CryptoFile, at: TimeInterval?) { let url = getAppFilePath(fileSource.filePath) if let cfArgs = fileSource.cryptoArgs { if let data = try? readCryptoFile(path: url.path, cryptoArgs: cfArgs) { audioPlayer = try? AVAudioPlayer(data: data) } } else { audioPlayer = try? AVAudioPlayer(contentsOf: url) } audioPlayer?.delegate = self audioPlayer?.prepareToPlay() if let at = at { audioPlayer?.currentTime = at } audioPlayer?.play() playbackTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in if self.audioPlayer?.isPlaying ?? false { AppDelegate.keepScreenOn(true) guard let time = self.audioPlayer?.currentTime else { return } self.onTimer?(time) AudioPlayer.changeAudioSession(true) } else { AudioPlayer.changeAudioSession(false) } } } func pause() { audioPlayer?.pause() AppDelegate.keepScreenOn(false) } func play() { audioPlayer?.play() } func seek(_ to: TimeInterval) { if audioPlayer?.isPlaying == true { audioPlayer?.pause() audioPlayer?.currentTime = to audioPlayer?.play() } else { audioPlayer?.currentTime = to } self.onTimer?(to) } func stop() { if let player = audioPlayer { player.stop() AppDelegate.keepScreenOn(false) AudioPlayer.changeAudioSession(false) } audioPlayer = nil if let timer = playbackTimer { timer.invalidate() } playbackTimer = nil } static func changeAudioSession(_ playback: Bool) { // When there is a audio recording, setting any other category will disable sound if AVAudioSession.sharedInstance().category == .playAndRecord { return } if playback { if AVAudioSession.sharedInstance().category != .playback { logger.log("AudioSession: playback") try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, options: .duckOthers) } } else { if AVAudioSession.sharedInstance().category != .soloAmbient { logger.log("AudioSession: soloAmbient") try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.soloAmbient) } } } func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { stop() self.onFinishPlayback?() } }