diff --git a/Meetingnotes.xcodeproj/project.pbxproj b/Meetingnotes.xcodeproj/project.pbxproj index 3a9e611..f50587d 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 = 37; + CURRENT_PROJECT_VERSION = 38; 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.25; + MARKETING_VERSION = 1.1.26; 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 = 37; + CURRENT_PROJECT_VERSION = 38; 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.25; + MARKETING_VERSION = 1.1.26; ONLY_ACTIVE_ARCH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI"; PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes; diff --git a/meetingnotes/ProcessTap/CoreAudioUtils.swift b/meetingnotes/ProcessTap/CoreAudioUtils.swift index 6baa3b0..0b5b8ed 100644 --- a/meetingnotes/ProcessTap/CoreAudioUtils.swift +++ b/meetingnotes/ProcessTap/CoreAudioUtils.swift @@ -116,6 +116,47 @@ extension AudioObjectID { try read(kAudioTapPropertyFormat, defaultValue: AudioStreamBasicDescription()) } + /// Reads the virtual format of the first input stream exposed by this device. + /// + /// An aggregate device can adapt a tap to the active output hardware. Its + /// input stream format is therefore the format delivered to the I/O proc, + /// which can differ from the tap object's originally advertised format. + func readInputStreamBasicDescription() throws -> AudioStreamBasicDescription { + var streamsAddress = AudioObjectPropertyAddress( + mSelector: kAudioDevicePropertyStreams, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain + ) + var dataSize: UInt32 = 0 + var status = AudioObjectGetPropertyDataSize(self, &streamsAddress, 0, nil, &dataSize) + guard status == noErr else { + throw "Error reading device stream list size: \(status)" + } + + var streamIDs = [AudioObjectID]( + repeating: .unknown, + count: Int(dataSize) / MemoryLayout.size + ) + status = AudioObjectGetPropertyData(self, &streamsAddress, 0, nil, &dataSize, &streamIDs) + guard status == noErr else { + throw "Error reading device stream list: \(status)" + } + + for streamID in streamIDs { + let direction: UInt32 = try streamID.read( + kAudioStreamPropertyDirection, + defaultValue: 0 + ) + guard direction == 1 else { continue } + return try streamID.read( + kAudioStreamPropertyVirtualFormat, + defaultValue: AudioStreamBasicDescription() + ) + } + + throw "Device has no input stream." + } + private func requireSystemObject() throws { if self != .system { throw "Only supported for the system object." } } @@ -307,4 +348,4 @@ extension AudioObjectID { func getDeviceName() throws -> String { return try readString(kAudioDevicePropertyDeviceNameCFString) } -} \ No newline at end of file +} diff --git a/meetingnotes/ProcessTap/ProcessTap.swift b/meetingnotes/ProcessTap/ProcessTap.swift index e44a700..f623aa0 100644 --- a/meetingnotes/ProcessTap/ProcessTap.swift +++ b/meetingnotes/ProcessTap/ProcessTap.swift @@ -248,8 +248,20 @@ final class ProcessTap { do { logger.debug("Attempting to read audio tap stream basic description for tapID #\(tapID)...") - self.tapStreamDescription = try tapID.readAudioTapStreamBasicDescription() - logger.debug("Successfully read tap stream description: \(String(describing: self.tapStreamDescription))") + let advertisedDescription = try tapID.readAudioTapStreamBasicDescription() + + // The aggregate device may adapt the tap to the active hardware's + // sample rate. Its input stream is the format actually delivered + // to the I/O proc, so use that rather than the tap's pre-aggregate + // advertisement. Using the latter can halve the written duration + // when, for example, a 48 kHz tap is delivered at 24 kHz. + do { + self.tapStreamDescription = try aggregateDeviceID.readInputStreamBasicDescription() + logger.info("Using aggregate input stream description: \(String(describing: self.tapStreamDescription), privacy: .public); tap advertised: \(String(describing: advertisedDescription), privacy: .public)") + } catch { + self.tapStreamDescription = advertisedDescription + logger.warning("Could not read aggregate input stream format; using tap format: \(error, privacy: .public)") + } } catch { logger.error("Failed to read audio tap stream basic description for tapID #\(tapID): \(error)") throw error // Propagate error