Voice message recording enhancements (#2042)

* Use Combine to report AudioRecorder state changes

* AudioRecorder is now using AVAudioEngine instead of AVAudioRecorder

* Stop recording when audio configuration changes

* AudioRecorder error handling

* UnitTests

* Use Accelerate to calculate RMS
This commit is contained in:
Nicolas Mauri
2023-11-08 10:19:28 +01:00
committed by GitHub
parent 14e0eba914
commit e04a55eaef
13 changed files with 817 additions and 269 deletions
@@ -3,6 +3,7 @@
// swiftlint:disable all
import AnalyticsEvents
import AVFoundation
import Combine
import Foundation
import LocalAuthentication
@@ -534,7 +535,7 @@ class AudioRecorderMock: AudioRecorderProtocol {
set(value) { underlyingIsRecording = value }
}
var underlyingIsRecording: Bool!
var url: URL?
var audioFileUrl: URL?
//MARK: - record
@@ -544,18 +545,13 @@ class AudioRecorderMock: AudioRecorderProtocol {
}
var recordWithReceivedRecordID: AudioRecordingIdentifier?
var recordWithReceivedInvocations: [AudioRecordingIdentifier] = []
var recordWithReturnValue: Result<Void, AudioRecorderError>!
var recordWithClosure: ((AudioRecordingIdentifier) async -> Result<Void, AudioRecorderError>)?
var recordWithClosure: ((AudioRecordingIdentifier) async -> Void)?
func record(with recordID: AudioRecordingIdentifier) async -> Result<Void, AudioRecorderError> {
func record(with recordID: AudioRecordingIdentifier) async {
recordWithCallsCount += 1
recordWithReceivedRecordID = recordID
recordWithReceivedInvocations.append(recordID)
if let recordWithClosure = recordWithClosure {
return await recordWithClosure(recordID)
} else {
return recordWithReturnValue
}
await recordWithClosure?(recordID)
}
//MARK: - stopRecording
@@ -581,28 +577,103 @@ class AudioRecorderMock: AudioRecorderProtocol {
deleteRecordingCallsCount += 1
await deleteRecordingClosure?()
}
//MARK: - averagePowerForChannelNumber
//MARK: - averagePower
var averagePowerForChannelNumberCallsCount = 0
var averagePowerForChannelNumberCalled: Bool {
return averagePowerForChannelNumberCallsCount > 0
var averagePowerCallsCount = 0
var averagePowerCalled: Bool {
return averagePowerCallsCount > 0
}
var averagePowerForChannelNumberReceivedChannelNumber: Int?
var averagePowerForChannelNumberReceivedInvocations: [Int] = []
var averagePowerForChannelNumberReturnValue: Float!
var averagePowerForChannelNumberClosure: ((Int) -> Float)?
var averagePowerReturnValue: Float!
var averagePowerClosure: (() -> Float)?
func averagePowerForChannelNumber(_ channelNumber: Int) -> Float {
averagePowerForChannelNumberCallsCount += 1
averagePowerForChannelNumberReceivedChannelNumber = channelNumber
averagePowerForChannelNumberReceivedInvocations.append(channelNumber)
if let averagePowerForChannelNumberClosure = averagePowerForChannelNumberClosure {
return averagePowerForChannelNumberClosure(channelNumber)
func averagePower() -> Float {
averagePowerCallsCount += 1
if let averagePowerClosure = averagePowerClosure {
return averagePowerClosure()
} else {
return averagePowerForChannelNumberReturnValue
return averagePowerReturnValue
}
}
}
class AudioSessionMock: AudioSessionProtocol {
//MARK: - requestRecordPermission
var requestRecordPermissionCallsCount = 0
var requestRecordPermissionCalled: Bool {
return requestRecordPermissionCallsCount > 0
}
var requestRecordPermissionReceivedResponse: ((Bool) -> Void)?
var requestRecordPermissionReceivedInvocations: [((Bool) -> Void)] = []
var requestRecordPermissionClosure: ((@escaping (Bool) -> Void) -> Void)?
func requestRecordPermission(_ response: @escaping (Bool) -> Void) {
requestRecordPermissionCallsCount += 1
requestRecordPermissionReceivedResponse = response
requestRecordPermissionReceivedInvocations.append(response)
requestRecordPermissionClosure?(response)
}
//MARK: - setAllowHapticsAndSystemSoundsDuringRecording
var setAllowHapticsAndSystemSoundsDuringRecordingThrowableError: Error?
var setAllowHapticsAndSystemSoundsDuringRecordingCallsCount = 0
var setAllowHapticsAndSystemSoundsDuringRecordingCalled: Bool {
return setAllowHapticsAndSystemSoundsDuringRecordingCallsCount > 0
}
var setAllowHapticsAndSystemSoundsDuringRecordingReceivedInValue: Bool?
var setAllowHapticsAndSystemSoundsDuringRecordingReceivedInvocations: [Bool] = []
var setAllowHapticsAndSystemSoundsDuringRecordingClosure: ((Bool) throws -> Void)?
func setAllowHapticsAndSystemSoundsDuringRecording(_ inValue: Bool) throws {
if let error = setAllowHapticsAndSystemSoundsDuringRecordingThrowableError {
throw error
}
setAllowHapticsAndSystemSoundsDuringRecordingCallsCount += 1
setAllowHapticsAndSystemSoundsDuringRecordingReceivedInValue = inValue
setAllowHapticsAndSystemSoundsDuringRecordingReceivedInvocations.append(inValue)
try setAllowHapticsAndSystemSoundsDuringRecordingClosure?(inValue)
}
//MARK: - setCategory
var setCategoryModeOptionsThrowableError: Error?
var setCategoryModeOptionsCallsCount = 0
var setCategoryModeOptionsCalled: Bool {
return setCategoryModeOptionsCallsCount > 0
}
var setCategoryModeOptionsReceivedArguments: (category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions)?
var setCategoryModeOptionsReceivedInvocations: [(category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions)] = []
var setCategoryModeOptionsClosure: ((AVAudioSession.Category, AVAudioSession.Mode, AVAudioSession.CategoryOptions) throws -> Void)?
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions) throws {
if let error = setCategoryModeOptionsThrowableError {
throw error
}
setCategoryModeOptionsCallsCount += 1
setCategoryModeOptionsReceivedArguments = (category: category, mode: mode, options: options)
setCategoryModeOptionsReceivedInvocations.append((category: category, mode: mode, options: options))
try setCategoryModeOptionsClosure?(category, mode, options)
}
//MARK: - setActive
var setActiveOptionsThrowableError: Error?
var setActiveOptionsCallsCount = 0
var setActiveOptionsCalled: Bool {
return setActiveOptionsCallsCount > 0
}
var setActiveOptionsReceivedArguments: (active: Bool, options: AVAudioSession.SetActiveOptions)?
var setActiveOptionsReceivedInvocations: [(active: Bool, options: AVAudioSession.SetActiveOptions)] = []
var setActiveOptionsClosure: ((Bool, AVAudioSession.SetActiveOptions) throws -> Void)?
func setActive(_ active: Bool, options: AVAudioSession.SetActiveOptions) throws {
if let error = setActiveOptionsThrowableError {
throw error
}
setActiveOptionsCallsCount += 1
setActiveOptionsReceivedArguments = (active: active, options: options)
setActiveOptionsReceivedInvocations.append((active: active, options: options))
try setActiveOptionsClosure?(active, options)
}
}
class BugReportServiceMock: BugReportServiceProtocol {
var isRunning: Bool {
get { return underlyingIsRunning }
@@ -3168,6 +3239,11 @@ class VoiceMessageRecorderMock: VoiceMessageRecorderProtocol {
set(value) { underlyingRecordingDuration = value }
}
var underlyingRecordingDuration: TimeInterval!
var actions: AnyPublisher<VoiceMessageRecorderAction, Never> {
get { return underlyingActions }
set(value) { underlyingActions = value }
}
var underlyingActions: AnyPublisher<VoiceMessageRecorderAction, Never>!
//MARK: - startRecording
@@ -3175,16 +3251,11 @@ class VoiceMessageRecorderMock: VoiceMessageRecorderProtocol {
var startRecordingCalled: Bool {
return startRecordingCallsCount > 0
}
var startRecordingReturnValue: Result<Void, VoiceMessageRecorderError>!
var startRecordingClosure: (() async -> Result<Void, VoiceMessageRecorderError>)?
var startRecordingClosure: (() async -> Void)?
func startRecording() async -> Result<Void, VoiceMessageRecorderError> {
func startRecording() async {
startRecordingCallsCount += 1
if let startRecordingClosure = startRecordingClosure {
return await startRecordingClosure()
} else {
return startRecordingReturnValue
}
await startRecordingClosure?()
}
//MARK: - stopRecording
@@ -3192,16 +3263,11 @@ class VoiceMessageRecorderMock: VoiceMessageRecorderProtocol {
var stopRecordingCalled: Bool {
return stopRecordingCallsCount > 0
}
var stopRecordingReturnValue: Result<Void, VoiceMessageRecorderError>!
var stopRecordingClosure: (() async -> Result<Void, VoiceMessageRecorderError>)?
var stopRecordingClosure: (() async -> Void)?
func stopRecording() async -> Result<Void, VoiceMessageRecorderError> {
func stopRecording() async {
stopRecordingCallsCount += 1
if let stopRecordingClosure = stopRecordingClosure {
return await stopRecordingClosure()
} else {
return stopRecordingReturnValue
}
await stopRecordingClosure?()
}
//MARK: - cancelRecording