From 66447b2510f8d89ffb3f216b6d90ca83c5f2c71e Mon Sep 17 00:00:00 2001 From: SuperDooper86 Date: Wed, 26 Aug 2026 14:36:32 +0200 Subject: [PATCH] fix: count Core Audio callback frames explicitly --- Meetingnotes.xcodeproj/project.pbxproj | 8 ++--- meetingnotes/Managers/AudioManager.swift | 42 ++++++++++++++---------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/Meetingnotes.xcodeproj/project.pbxproj b/Meetingnotes.xcodeproj/project.pbxproj index e67df1f..7a9fc11 100644 --- a/Meetingnotes.xcodeproj/project.pbxproj +++ b/Meetingnotes.xcodeproj/project.pbxproj @@ -276,7 +276,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 39; + CURRENT_PROJECT_VERSION = 40; DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\""; DEVELOPMENT_TEAM = G9LVHZAJNX; ENABLE_HARDENED_RUNTIME = YES; @@ -290,7 +290,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.1.27; + MARKETING_VERSION = 1.1.28; ONLY_ACTIVE_ARCH = NO; OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI"; PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes; @@ -312,7 +312,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 39; + CURRENT_PROJECT_VERSION = 40; DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\""; DEVELOPMENT_TEAM = G9LVHZAJNX; ENABLE_HARDENED_RUNTIME = YES; @@ -326,7 +326,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.1.27; + MARKETING_VERSION = 1.1.28; ONLY_ACTIVE_ARCH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI"; PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes; diff --git a/meetingnotes/Managers/AudioManager.swift b/meetingnotes/Managers/AudioManager.swift index ca46e81..7108892 100644 --- a/meetingnotes/Managers/AudioManager.swift +++ b/meetingnotes/Managers/AudioManager.swift @@ -443,14 +443,15 @@ final class AudioManager: NSObject, ObservableObject { let channelCount = buffers.reduce(UInt32(0)) { $0 + $1.mNumberChannels } guard channelCount > 0 else { return nil } - // HAL tap metadata can advertise interleaved stereo while the callback - // supplies one mono buffer per channel (or the reverse). Constructing a - // PCM buffer with that mismatched layout halves its frame count and - // produces 2x-speed system audio. The callback's AudioBufferList is the - // authoritative layout for the memory we are copying. + // HAL I/O proc samples use the canonical Float32 representation. The + // tap's stream description can advertise a different common format; + // using that to interpret the callback bytes can halve the frame count + // (for example, treating four-byte Float32 samples as eight-byte + // Float64 samples). The callback's AudioBufferList is authoritative for + // its channel layout, while its sample rate comes from the input stream. let isInterleaved = buffers.count == 1 && channelCount > 1 return AVAudioFormat( - commonFormat: advertisedFormat.commonFormat, + commonFormat: .pcmFormatFloat32, sampleRate: advertisedFormat.sampleRate, channels: AVAudioChannelCount(channelCount), interleaved: isInterleaved @@ -461,20 +462,27 @@ final class AudioManager: NSObject, ObservableObject { from inputData: UnsafePointer, format: AVAudioFormat ) -> AVAudioPCMBuffer? { - guard let borrowedBuffer = AVAudioPCMBuffer( - pcmFormat: format, - bufferListNoCopy: inputData, - deallocator: nil - ), borrowedBuffer.frameLength > 0, - let ownedBuffer = AVAudioPCMBuffer( - pcmFormat: format, - frameCapacity: borrowedBuffer.frameLength - ) else { return nil } - - ownedBuffer.frameLength = borrowedBuffer.frameLength let sourceBuffers = UnsafeMutableAudioBufferListPointer( UnsafeMutablePointer(mutating: inputData) ) + let frameLengths = sourceBuffers.compactMap { source -> AVAudioFrameCount? in + let bytesPerFrame = Int(source.mNumberChannels) * MemoryLayout.size + guard bytesPerFrame > 0, + Int(source.mDataByteSize).isMultiple(of: bytesPerFrame) else { return nil } + return AVAudioFrameCount(Int(source.mDataByteSize) / bytesPerFrame) + } + guard frameLengths.count == sourceBuffers.count, + let frameLength = frameLengths.first, + frameLength > 0, + frameLengths.allSatisfy({ $0 == frameLength }), + let ownedBuffer = AVAudioPCMBuffer( + pcmFormat: format, + frameCapacity: frameLength + ) else { return nil } + + // Set the frame count explicitly instead of asking AVAudioPCMBuffer to + // infer it from potentially inconsistent tap metadata. + ownedBuffer.frameLength = frameLength let destinationBuffers = UnsafeMutableAudioBufferListPointer( ownedBuffer.mutableAudioBufferList )