@@ -26,6 +26,25 @@ enum AudioCapturePhase: Equatable {
2626 var evaluatesStopConditions : Bool { self == . recording }
2727}
2828
29+ /// Reuses the sample-rate converter while the microphone format is stable.
30+ /// A route change replaces it; each independent tap buffer resets its state.
31+ final class AudioConverterCache {
32+ private var converter : AVAudioConverter ?
33+ private( set) var creationCount = 0
34+
35+ func converter( from source: AVAudioFormat , to destination: AVAudioFormat ) -> AVAudioConverter ? {
36+ if let converter,
37+ converter. inputFormat == source,
38+ converter. outputFormat == destination {
39+ converter. reset ( )
40+ return converter
41+ }
42+ converter = AVAudioConverter ( from: source, to: destination)
43+ if converter != nil { creationCount += 1 }
44+ return converter
45+ }
46+ }
47+
2948/// Tracks continuous silence independently of total recording time.
3049struct SilenceDetector {
3150 private( set) var lastSoundTime : Date
@@ -74,6 +93,7 @@ final class AudioEngine {
7493 private var engine : AVAudioEngine ?
7594 private var pendingEngineRelease : DispatchWorkItem ?
7695 private var audioBuffer : [ Float ] = [ ]
96+ private let converterCache = AudioConverterCache ( )
7797 private var _isCurrentlyRecording = false
7898 /// Realtime-safe capture lifecycle for the input tap.
7999 ///
@@ -1076,7 +1096,10 @@ final class AudioEngine {
10761096 guard let convertedBuffer = Self . convertToWhisperFormat (
10771097 buffer,
10781098 from: inputFormat,
1079- inputChannel: preferredInputChannel
1099+ inputChannel: preferredInputChannel,
1100+ converterProvider: { [ converterCache] source, destination in
1101+ converterCache. converter ( from: source, to: destination)
1102+ }
10801103 ) else {
10811104 return
10821105 }
@@ -1101,10 +1124,12 @@ final class AudioEngine {
11011124 if let channelData = convertedBuffer. floatChannelData {
11021125 let frameCount = Int ( convertedBuffer. frameLength)
11031126 bufferQueue. sync {
1104- audioBuffer. reserveCapacity ( audioBuffer. count + frameCount)
1105- for i in 0 ..< frameCount {
1106- audioBuffer. append ( isApplicationInputMuted ? 0 : channelData [ 0 ] [ i] )
1107- }
1127+ Self . appendCapturedSamples (
1128+ channelData [ 0 ] ,
1129+ count: frameCount,
1130+ muted: isApplicationInputMuted,
1131+ to: & audioBuffer
1132+ )
11081133 }
11091134 }
11101135
@@ -1149,7 +1174,8 @@ final class AudioEngine {
11491174 static func convertToWhisperFormat(
11501175 _ buffer: AVAudioPCMBuffer ,
11511176 from inputFormat: AVAudioFormat ,
1152- inputChannel: Int = 0
1177+ inputChannel: Int = 0 ,
1178+ converterProvider: ( ( AVAudioFormat , AVAudioFormat ) -> AVAudioConverter ? ) ? = nil
11531179 ) -> AVAudioPCMBuffer ? {
11541180 let sourceBuffer : AVAudioPCMBuffer
11551181 if inputFormat. channelCount > 1 {
@@ -1176,7 +1202,8 @@ final class AudioEngine {
11761202 }
11771203
11781204 // Create a converter
1179- guard let converter = AVAudioConverter ( from: sourceFormat, to: whisperFormat) else {
1205+ guard let converter = converterProvider ? ( sourceFormat, whisperFormat)
1206+ ?? AVAudioConverter ( from: sourceFormat, to: whisperFormat) else {
11801207 VocaLogger . error ( . audioEngine, " Failed to create audio format converter " )
11811208 return nil
11821209 }
@@ -1218,6 +1245,22 @@ final class AudioEngine {
12181245 return outputBuffer
12191246 }
12201247
1248+ /// Bulk append keeps Array's geometric growth instead of reallocating to
1249+ /// the exact size for every realtime callback.
1250+ static func appendCapturedSamples(
1251+ _ samples: UnsafePointer < Float > ,
1252+ count: Int ,
1253+ muted: Bool ,
1254+ to destination: inout [ Float ]
1255+ ) {
1256+ guard count > 0 else { return }
1257+ if muted {
1258+ destination. append ( contentsOf: repeatElement ( Float . zero, count: count) )
1259+ } else {
1260+ destination. append ( contentsOf: UnsafeBufferPointer ( start: samples, count: count) )
1261+ }
1262+ }
1263+
12211264 /// Keeps a channel selection only while it still describes the live device layout.
12221265 private func syncPreferredInputChannel(
12231266 with deviceID: AudioDeviceID ,
0 commit comments