I am using AVAudioRecorder and using its metering feature: AVAudioRecorder.averagePower(forChannel:)
In a simulator running IOS 17.0.1, when a loud sound occurs, the value returned by this function can be higher than zero, even though the documentation states that the value should be between 0 and -160. This problem does not seem to happen on a real device, but unfortunately this does not reassure me completely because I only have one real device to test with, and when testing with it, there is not really a way to verify that I am creating a sound greater than which no other sound exists. Ya know?
Here is some code you can verify this with (problematic parts are marked “debug”):
/// Records audio to a temp file
/// and returns it as an in-memory AVAudioPCMBuffer on stop().
/// While recording, three things are published: level, peak level, and % of [peak hold time] remaining
enum AudioExtractionError: Error {
case fileNotFound
case bufferCreationFailed
case fileReadError(Error)
case unknown(Error)
}
@Observable // <---- remove this if testing with UIKit
class MemoRecorder {
private var randomPrefixForTempFile:String = UUID().uuidString
private var audioRecorder: AVAudioRecorder?
private var isRecording: Bool = false
private var meteringTimer: Timer?
private let METERING_TIMER_RESOLUTION_SECONDS:Double = 1.0/60.0
private let PEAK_HOLD_DURATION_SECONDS:Float = 1
var currentRecLevel:Float = 0 // from 0 to 1
var peakRecLevel:Float = 0 // from 0 to 1
var peakHoldTimeRemaining:Float = 0 // from 1 to 0 : percent of peak hold time remaining
static var shared = MemoRecorder()
private init() {
}
// caller must already have permission
func startRecordingWithPermission() {
if (isRecording) {fatalError ("dsfsdfs 9345843534")}
isRecording = true
randomPrefixForTempFile = UUID().uuidString
let audioFilename = H.getDocumentsDirectory().appendingPathComponent("recording-\(randomPrefixForTempFile).wav")
do {
audioRecorder = try AVAudioRecorder(url: audioFilename, format:AT.vanillaFormat)
} catch {
fatalError("sdfsdsfsdfdgdsfg")
}
guard let audioRecorder = audioRecorder else {fatalError("sdfsdf")}
audioRecorder.isMeteringEnabled = true
audioRecorder.record()
currentRecLevel = 0
peakRecLevel = 0
peakHoldTimeRemaining = 0
startMeteringTimer()
}
// Called when we need to stop recording but the caller does not want/need the buffer
func stopRecordingAndDiscard() {
meteringTimer?.invalidate()
audioRecorder?.stop()
if (isRecording) {
isRecording = false
}
}
func stopRecordingAndReturnBuffer() -> AVAudioPCMBuffer {
if (!isRecording) {fatalError("stopped recording when not recording")}
meteringTimer?.invalidate()
audioRecorder?.stop()
audioRecorder = nil
var result:AVAudioPCMBuffer?
do {
result = try extractBufferFromFile()
} catch {
fatalError("sdalfh9898797")
}
isRecording = false
return result!
}
// ------------------------------------------------------------------ INTERNAL
private func extractBufferFromFile() throws -> AVAudioPCMBuffer {
let recordedFile = H.getDocumentsDirectory().appendingPathComponent("recording-\(randomPrefixForTempFile).wav")
guard FileManager.default.fileExists(atPath: recordedFile.path) else {
throw AudioExtractionError.fileNotFound
}
do {
let avAudioFile = try AVAudioFile(forReading: recordedFile)
// Check if buffer creation is possible
guard let buffer = AVAudioPCMBuffer(pcmFormat: AT.vanillaFormat, frameCapacity: AVAudioFrameCount(avAudioFile.length)) else {
throw AudioExtractionError.bufferCreationFailed
}
// Attempt to read the file into the buffer
do {
try avAudioFile.read(into: buffer)
} catch {
// Specific error for file read failure
throw AudioExtractionError.fileReadError(error)
}
// Return the filled buffer
return buffer
} catch {
// Catch any other errors that were not anticipated
throw AudioExtractionError.unknown(error)
}
}
// --------------- METERING TIMER
private func startMeteringTimer() {
meteringTimer?.invalidate() // Invalidate any existing timer
meteringTimer = Timer.scheduledTimer(withTimeInterval: METERING_TIMER_RESOLUTION_SECONDS, repeats: true) { _ in
self.callThisFunctionWithTheTimer()
}
}
private func callThisFunctionWithTheTimer() {
guard let audioRecorder = audioRecorder else { return }
audioRecorder.updateMeters()
let averagePower = audioRecorder.averagePower(forChannel: 0)
// debug
if (averagePower > 0 || averagePower < -160) {
print("out of bounds: \(averagePower)")
}
let newValue = convertDecibelsToLinear(audioRecorder.averagePower(forChannel: 0))
currentRecLevel = newValue
// debug
if (currentRecLevel > 1) {
print("overage: \(currentRecLevel)")
}
if newValue > peakRecLevel {
peakRecLevel = newValue
peakHoldTimeRemaining = PEAK_HOLD_DURATION_SECONDS
} else if peakHoldTimeRemaining > 0 {
peakHoldTimeRemaining -= Float(METERING_TIMER_RESOLUTION_SECONDS)
} else {
peakRecLevel = 0 // Reset peak value when hold time has elapsed
}
}
private func convertDecibelsToLinear(_ decibels: Float) -> Float {
let clampedDecibels = max(decibels, -160.0) // Clamp the minimum decibel value
return pow(10.0, clampedDecibels / 20.0) // Convert to linear scale
}
}
Why is this happening? Is this a known issue that only effects the simulator for some reason? Can I assume it wont happen on any real device?