twonly-app-dependencies/audio_waveforms/ios/Classes/AudioRecorder.swift
2026-08-22 20:30:14 +02:00

194 lines
7.5 KiB
Swift

import AVFoundation
import Accelerate
public class AudioRecorder: NSObject, AVAudioRecorderDelegate{
var audioRecorder: AVAudioRecorder?
var path: String?
var useLegacyNormalization: Bool = false
var audioUrl: URL?
var recordedDuration: CMTime = CMTime.zero
var flutterChannel: FlutterMethodChannel
var bytesStreamEngine: RecorderBytesStreamEngine
init(channel: FlutterMethodChannel){
flutterChannel = channel
bytesStreamEngine = RecorderBytesStreamEngine(channel: channel)
}
func startRecording(_ result: @escaping FlutterResult,_ recordingSettings: RecordingSettings){
useLegacyNormalization = recordingSettings.useLegacy ?? false
var settings: [String: Any] = [
AVFormatIDKey: getEncoder(recordingSettings.encoder ?? 0),
AVSampleRateKey: recordingSettings.sampleRate ?? 44100,
AVNumberOfChannelsKey: 1,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]
if (recordingSettings.bitRate != nil) {
settings[AVEncoderBitRateKey] = recordingSettings.bitRate
}
if ((recordingSettings.encoder ?? 0) == Constants.kAudioFormatLinearPCM) {
settings[AVLinearPCMBitDepthKey] = recordingSettings.linearPCMBitDepth
settings[AVLinearPCMIsBigEndianKey] = recordingSettings.linearPCMIsBigEndian
settings[AVLinearPCMIsFloatKey] = recordingSettings.linearPCMIsFloat
}
let options: AVAudioSession.CategoryOptions = [.defaultToSpeaker, .allowBluetooth]
if (recordingSettings.path == nil) {
let documentDirectory = getDocumentDirectory(result)
let date = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = recordingSettings.fileNameFormat
let fileName = dateFormatter.string(from: date) + ".m4a"
self.path = "\(documentDirectory)/\(fileName)"
} else {
self.path = recordingSettings.path
}
do {
if recordingSettings.overrideAudioSession {
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, options: options)
try AVAudioSession.sharedInstance().setActive(true)
}
audioUrl = URL(fileURLWithPath: self.path!)
if(audioUrl == nil){
result(FlutterError(code: Constants.audioWaveforms, message: "Failed to initialise file URL", details: nil))
return
}
audioRecorder = try AVAudioRecorder(url: audioUrl!, settings: settings as [String : Any])
audioRecorder?.delegate = self
audioRecorder?.isMeteringEnabled = true
audioRecorder?.record()
bytesStreamEngine
.attach(
result: result,
sampleRate: recordingSettings.sampleRate ?? Constants.defaultSampleRate
)
result(true)
} catch {
result(FlutterError(code: Constants.audioWaveforms, message: "Failed to start recording", details: error.localizedDescription))
}
}
public func stopRecording(_ result: @escaping FlutterResult) {
audioRecorder?.stop()
bytesStreamEngine.detach()
if(audioUrl != nil) {
let asset = AVURLAsset(url: audioUrl!)
if #available(iOS 15.0, *) {
Task {
do {
recordedDuration = try await asset.load(.duration)
sendResult(result, duration: Int(recordedDuration.seconds * 1000))
} catch let err {
debugPrint(err.localizedDescription)
sendResult(result, duration: Int(CMTime.zero.seconds))
}
}
} else {
recordedDuration = asset.duration
sendResult(result, duration: Int(recordedDuration.seconds * 1000))
}
} else {
sendResult(result, duration: Int(CMTime.zero.seconds))
}
audioRecorder = nil
}
private func sendResult(_ result: @escaping FlutterResult, duration:Int){
var params = [String:Any?]()
params[Constants.resultFilePath] = path
params[Constants.resultDuration] = duration
result(params)
}
public func pauseRecording(_ result: @escaping FlutterResult) {
audioRecorder?.pause()
bytesStreamEngine.togglePause()
result(false)
}
public func resumeRecording(_ result: @escaping FlutterResult) {
audioRecorder?.record()
bytesStreamEngine.togglePause();
result(true)
}
public func getDecibel(_ result: @escaping FlutterResult) {
audioRecorder?.updateMeters()
if(useLegacyNormalization){
let amp = audioRecorder?.averagePower(forChannel: 0) ?? 0.0
result(amp)
} else {
let amp = audioRecorder?.peakPower(forChannel: 0) ?? 0.0
let linear = pow(10, amp / 20);
result(linear)
}
}
public func checkHasPermission(_ result: @escaping FlutterResult){
switch AVAudioSession.sharedInstance().recordPermission{
case .undetermined:
AVAudioSession.sharedInstance().requestRecordPermission() { [unowned self] allowed in
DispatchQueue.main.async {
result(allowed)
}
}
case .denied:
result(false)
case .granted:
result(true)
@unknown default:
result(false)
}
}
public func getEncoder(_ enCoder: Int) -> Int {
switch(enCoder) {
case Constants.kAudioFormatMPEG4AAC:
return Int(kAudioFormatMPEG4AAC)
case Constants.kAudioFormatMPEGLayer1:
return Int(kAudioFormatMPEGLayer1)
case Constants.kAudioFormatMPEGLayer2:
return Int(kAudioFormatMPEGLayer2)
case Constants.kAudioFormatMPEGLayer3:
return Int(kAudioFormatMPEGLayer3)
case Constants.kAudioFormatMPEG4AAC_ELD:
return Int(kAudioFormatMPEG4AAC_ELD)
case Constants.kAudioFormatMPEG4AAC_HE:
return Int(kAudioFormatMPEG4AAC_HE)
case Constants.kAudioFormatOpus:
return Int(kAudioFormatOpus)
case Constants.kAudioFormatAMR:
return Int(kAudioFormatAMR)
case Constants.kAudioFormatAMR_WB:
return Int(kAudioFormatAMR_WB)
case Constants.kAudioFormatLinearPCM:
return Int(kAudioFormatLinearPCM)
case Constants.kAudioFormatAppleLossless:
return Int(kAudioFormatAppleLossless)
case Constants.kAudioFormatMPEG4AAC_HE_V2:
return Int(kAudioFormatMPEG4AAC_HE_V2)
default:
return Int(kAudioFormatMPEG4AAC)
}
}
private func getDocumentDirectory(_ result: @escaping FlutterResult) -> String {
let directory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let ifExists = FileManager.default.fileExists(atPath: directory)
if(directory.isEmpty){
result(FlutterError(code: Constants.audioWaveforms, message: "The document directory path is empty", details: nil))
return ""
} else if(!ifExists) {
result(FlutterError(code: Constants.audioWaveforms, message: "The document directory does't exists", details: nil))
return ""
}
return directory
}
}