ios – AVAudioRecorder.averagePower returns bad values on Simulator


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?

Latest articles

spot_imgspot_img

Related articles

Leave a reply

Please enter your comment!
Please enter your name here

spot_imgspot_img