Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f3805733b | ||
|
|
4e8fdc7603 | ||
|
|
aed34f6d28 |
@@ -276,7 +276,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 36;
|
CURRENT_PROJECT_VERSION = 39;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = G9LVHZAJNX;
|
DEVELOPMENT_TEAM = G9LVHZAJNX;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@@ -290,7 +290,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 1.1.24;
|
MARKETING_VERSION = 1.1.27;
|
||||||
ONLY_ACTIVE_ARCH = NO;
|
ONLY_ACTIVE_ARCH = NO;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes;
|
PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes;
|
||||||
@@ -312,7 +312,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 36;
|
CURRENT_PROJECT_VERSION = 39;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = G9LVHZAJNX;
|
DEVELOPMENT_TEAM = G9LVHZAJNX;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@@ -326,7 +326,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 1.1.24;
|
MARKETING_VERSION = 1.1.27;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes;
|
PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Implemented:
|
|||||||
- Meeting search functionality
|
- Meeting search functionality
|
||||||
- Abilty to edit system prompt
|
- Abilty to edit system prompt
|
||||||
- Select any compatible Coder model for transcription and note generation
|
- Select any compatible Coder model for transcription and note generation
|
||||||
- Automatic start and stop from MuteDeck through a compatible local API
|
- Automatic start and stop from MuteDeck through a compatible local API, with a 10-second reconnect grace period
|
||||||
- Merge an automatically split continuation back into its previous meeting
|
- Merge an automatically split continuation back into its previous meeting
|
||||||
- Auto updates
|
- Auto updates
|
||||||
- Text formatting
|
- Text formatting
|
||||||
|
|||||||
@@ -116,6 +116,47 @@ extension AudioObjectID {
|
|||||||
try read(kAudioTapPropertyFormat, defaultValue: AudioStreamBasicDescription())
|
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<AudioObjectID>.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 {
|
private func requireSystemObject() throws {
|
||||||
if self != .system { throw "Only supported for the system object." }
|
if self != .system { throw "Only supported for the system object." }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,11 +138,11 @@ final class ProcessTap {
|
|||||||
tapDescription = CATapDescription(stereoMixdownOfProcesses: [process.objectID])
|
tapDescription = CATapDescription(stereoMixdownOfProcesses: [process.objectID])
|
||||||
logger.debug("Configuring tap for single process objectID: \(process.objectID)")
|
logger.debug("Configuring tap for single process objectID: \(process.objectID)")
|
||||||
case .systemAudio:
|
case .systemAudio:
|
||||||
// Keep the HAL tap's buffer layout consistent with the default
|
// The transcription file is mono, so ask Core Audio for a mono
|
||||||
// output stream. AudioManager performs the stereo-to-mono mix when
|
// mixdown at the source. This avoids interpreting a stereo HAL
|
||||||
// it converts the captured audio to the 16 kHz transcription file.
|
// buffer as half as many frames before the 16 kHz conversion.
|
||||||
tapDescription = CATapDescription(stereoGlobalTapButExcludeProcesses: [])
|
tapDescription = CATapDescription(monoGlobalTapButExcludeProcesses: [])
|
||||||
logger.debug("Configuring a stereo global system audio tap.")
|
logger.info("Configuring a mono global system audio tap.")
|
||||||
}
|
}
|
||||||
|
|
||||||
tapDescription.uuid = UUID()
|
tapDescription.uuid = UUID()
|
||||||
@@ -248,8 +248,20 @@ final class ProcessTap {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
logger.debug("Attempting to read audio tap stream basic description for tapID #\(tapID)...")
|
logger.debug("Attempting to read audio tap stream basic description for tapID #\(tapID)...")
|
||||||
self.tapStreamDescription = try tapID.readAudioTapStreamBasicDescription()
|
let advertisedDescription = try tapID.readAudioTapStreamBasicDescription()
|
||||||
logger.debug("Successfully read tap stream description: \(String(describing: self.tapStreamDescription))")
|
|
||||||
|
// 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 {
|
} catch {
|
||||||
logger.error("Failed to read audio tap stream basic description for tapID #\(tapID): \(error)")
|
logger.error("Failed to read audio tap stream basic description for tapID #\(tapID): \(error)")
|
||||||
throw error // Propagate error
|
throw error // Propagate error
|
||||||
|
|||||||
@@ -200,7 +200,9 @@ private final class LocalRecordingController {
|
|||||||
static let shared = LocalRecordingController()
|
static let shared = LocalRecordingController()
|
||||||
|
|
||||||
private let recordingManager = RecordingSessionManager.shared
|
private let recordingManager = RecordingSessionManager.shared
|
||||||
|
private let stopGraceNanoseconds: UInt64 = 10_000_000_000
|
||||||
private var isStopping = false
|
private var isStopping = false
|
||||||
|
private var pendingStopTask: Task<Void, Never>?
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
@@ -237,6 +239,12 @@ private final class LocalRecordingController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startRecording() throws -> [String: Any] {
|
func startRecording() throws -> [String: Any] {
|
||||||
|
if let pendingStopTask {
|
||||||
|
pendingStopTask.cancel()
|
||||||
|
self.pendingStopTask = nil
|
||||||
|
isStopping = false
|
||||||
|
return statusPayload()
|
||||||
|
}
|
||||||
if isStopping || recordingManager.isProcessing {
|
if isStopping || recordingManager.isProcessing {
|
||||||
throw LocalRecordingError.processing
|
throw LocalRecordingError.processing
|
||||||
}
|
}
|
||||||
@@ -258,14 +266,22 @@ private final class LocalRecordingController {
|
|||||||
return statusPayload()
|
return statusPayload()
|
||||||
}
|
}
|
||||||
isStopping = true
|
isStopping = true
|
||||||
Task { [weak self] in
|
pendingStopTask = Task { [weak self] in
|
||||||
|
try? await Task.sleep(nanoseconds: self?.stopGraceNanoseconds ?? 0)
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
await self?.finishRecording()
|
await self?.finishRecording()
|
||||||
}
|
}
|
||||||
return statusPayload()
|
return statusPayload()
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelRecording() -> [String: Any] {
|
func cancelRecording() -> [String: Any] {
|
||||||
guard !isStopping else { return statusPayload() }
|
if let pendingStopTask {
|
||||||
|
pendingStopTask.cancel()
|
||||||
|
self.pendingStopTask = nil
|
||||||
|
isStopping = false
|
||||||
|
} else if isStopping {
|
||||||
|
return statusPayload()
|
||||||
|
}
|
||||||
let meetingID = recordingManager.activeMeetingId
|
let meetingID = recordingManager.activeMeetingId
|
||||||
recordingManager.cancelRecording()
|
recordingManager.cancelRecording()
|
||||||
if let meetingID,
|
if let meetingID,
|
||||||
@@ -277,6 +293,7 @@ private final class LocalRecordingController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func finishRecording() async {
|
private func finishRecording() async {
|
||||||
|
pendingStopTask = nil
|
||||||
let meetingID = recordingManager.activeMeetingId
|
let meetingID = recordingManager.activeMeetingId
|
||||||
let chunks = await recordingManager.stopRecording()
|
let chunks = await recordingManager.stopRecording()
|
||||||
guard let meetingID,
|
guard let meetingID,
|
||||||
|
|||||||
Reference in New Issue
Block a user