Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bcdd51201 | ||
|
|
ea44104458 |
@@ -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 = 18;
|
CURRENT_PROJECT_VERSION = 20;
|
||||||
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.6;
|
MARKETING_VERSION = 1.1.8;
|
||||||
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 = 18;
|
CURRENT_PROJECT_VERSION = 20;
|
||||||
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.6;
|
MARKETING_VERSION = 1.1.8;
|
||||||
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;
|
||||||
|
|||||||
@@ -183,9 +183,13 @@ final class AudioManager: NSObject, ObservableObject {
|
|||||||
throw NSError(domain: "AudioManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unsupported microphone format"])
|
throw NSError(domain: "AudioManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unsupported microphone format"])
|
||||||
}
|
}
|
||||||
inputNode.installTap(onBus: 0, bufferSize: 1024, format: inputFormat) { [weak self] buffer, _ in
|
inputNode.installTap(onBus: 0, bufferSize: 1024, format: inputFormat) { [weak self] buffer, _ in
|
||||||
guard let self, buffer.frameLength > 0 else { return }
|
guard let self else { return }
|
||||||
self.updateAudioLevel(buffer, source: .mic)
|
self.processAudioBuffer(
|
||||||
self.processAudioBuffer(buffer, converter: converter, targetFormat: targetFormat, source: .mic)
|
{ buffer },
|
||||||
|
converter: converter,
|
||||||
|
targetFormat: targetFormat,
|
||||||
|
source: .mic
|
||||||
|
)
|
||||||
}
|
}
|
||||||
audioEngine.prepare()
|
audioEngine.prepare()
|
||||||
try audioEngine.start()
|
try audioEngine.start()
|
||||||
@@ -303,11 +307,13 @@ final class AudioManager: NSObject, ObservableObject {
|
|||||||
throw NSError(domain: "AudioManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unsupported system audio format"])
|
throw NSError(domain: "AudioManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unsupported system audio format"])
|
||||||
}
|
}
|
||||||
try tap.run(on: tapQueue) { [weak self] _, inputData, _, _, _ in
|
try tap.run(on: tapQueue) { [weak self] _, inputData, _, _, _ in
|
||||||
guard let self,
|
guard let self else { return }
|
||||||
let buffer = AVAudioPCMBuffer(pcmFormat: inputFormat, bufferListNoCopy: inputData, deallocator: nil),
|
self.processAudioBuffer(
|
||||||
buffer.frameLength > 0 else { return }
|
{ AVAudioPCMBuffer(pcmFormat: inputFormat, bufferListNoCopy: inputData, deallocator: nil) },
|
||||||
self.updateAudioLevel(buffer, source: .system)
|
converter: converter,
|
||||||
self.processAudioBuffer(buffer, converter: converter, targetFormat: targetFormat, source: .system)
|
targetFormat: targetFormat,
|
||||||
|
source: .system
|
||||||
|
)
|
||||||
} invalidationHandler: { [weak self] _ in
|
} invalidationHandler: { [weak self] _ in
|
||||||
guard let self, !self.isRestartingSystemTap, self.isRecording else { return }
|
guard let self, !self.isRestartingSystemTap, self.isRecording else { return }
|
||||||
Task { await self.restartSystemAudioTap() }
|
Task { await self.restartSystemAudioTap() }
|
||||||
@@ -315,11 +321,20 @@ final class AudioManager: NSObject, ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func processAudioBuffer(
|
private func processAudioBuffer(
|
||||||
_ inputBuffer: AVAudioPCMBuffer,
|
_ inputBufferProvider: () -> AVAudioPCMBuffer?,
|
||||||
converter: AVAudioConverter,
|
converter: AVAudioConverter,
|
||||||
targetFormat: AVAudioFormat,
|
targetFormat: AVAudioFormat,
|
||||||
source: AudioSource
|
source: AudioSource
|
||||||
) {
|
) {
|
||||||
|
// Keep callback-owned buffers alive until conversion finishes. Teardown
|
||||||
|
// takes this same lock before invalidating the Core Audio process tap.
|
||||||
|
audioFileLock.lock()
|
||||||
|
defer { audioFileLock.unlock() }
|
||||||
|
guard isAcceptingAudio,
|
||||||
|
let inputBuffer = inputBufferProvider(),
|
||||||
|
inputBuffer.frameLength > 0 else { return }
|
||||||
|
|
||||||
|
updateAudioLevel(inputBuffer, source: source)
|
||||||
let ratio = targetFormat.sampleRate / inputBuffer.format.sampleRate
|
let ratio = targetFormat.sampleRate / inputBuffer.format.sampleRate
|
||||||
let capacity = max(1, AVAudioFrameCount(ceil(Double(inputBuffer.frameLength) * ratio)))
|
let capacity = max(1, AVAudioFrameCount(ceil(Double(inputBuffer.frameLength) * ratio)))
|
||||||
guard let outputBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: capacity) else { return }
|
guard let outputBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: capacity) else { return }
|
||||||
@@ -336,11 +351,6 @@ final class AudioManager: NSObject, ObservableObject {
|
|||||||
}
|
}
|
||||||
guard status != .error, conversionError == nil, outputBuffer.frameLength > 0 else { return }
|
guard status != .error, conversionError == nil, outputBuffer.frameLength > 0 else { return }
|
||||||
|
|
||||||
audioFileLock.lock()
|
|
||||||
defer { audioFileLock.unlock() }
|
|
||||||
guard isAcceptingAudio else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
do {
|
do {
|
||||||
switch source {
|
switch source {
|
||||||
case .mic:
|
case .mic:
|
||||||
@@ -378,8 +388,8 @@ final class AudioManager: NSObject, ObservableObject {
|
|||||||
pendingMicRestart = nil
|
pendingMicRestart = nil
|
||||||
AudioLevelManager.shared.updateRecordingState(false)
|
AudioLevelManager.shared.updateRecordingState(false)
|
||||||
|
|
||||||
// Stop new writes and wait for any callback already writing before
|
// Stop new callbacks and wait for any active conversion/write before
|
||||||
// AVAudioFile is finalized and released.
|
// invalidating callback-owned buffers or finalizing AVAudioFile.
|
||||||
audioFileLock.lock()
|
audioFileLock.lock()
|
||||||
isAcceptingAudio = false
|
isAcceptingAudio = false
|
||||||
audioFileLock.unlock()
|
audioFileLock.unlock()
|
||||||
|
|||||||
@@ -54,15 +54,6 @@ class MeetingViewModel: ObservableObject {
|
|||||||
|
|
||||||
private let recordingSessionManager = RecordingSessionManager.shared
|
private let recordingSessionManager = RecordingSessionManager.shared
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
private var isNewMeeting = false
|
|
||||||
|
|
||||||
// Computed property to check if meeting is empty
|
|
||||||
var isEmpty: Bool {
|
|
||||||
return meeting.transcriptChunks.isEmpty &&
|
|
||||||
meeting.userNotes.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
|
|
||||||
meeting.generatedNotes.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
|
|
||||||
meeting.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
init(meeting: Meeting = Meeting()) {
|
init(meeting: Meeting = Meeting()) {
|
||||||
// Load the latest version of the meeting from storage if it exists
|
// Load the latest version of the meeting from storage if it exists
|
||||||
@@ -76,9 +67,6 @@ class MeetingViewModel: ObservableObject {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Detect if this is a new meeting based on content, not storage existence
|
|
||||||
isNewMeeting = isEmpty
|
|
||||||
|
|
||||||
// Set initial tab based on notes existence
|
// Set initial tab based on notes existence
|
||||||
if !self.meeting.generatedNotes.isEmpty {
|
if !self.meeting.generatedNotes.isEmpty {
|
||||||
selectedTab = .enhancedNotes
|
selectedTab = .enhancedNotes
|
||||||
@@ -333,12 +321,4 @@ class MeetingViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteIfEmpty() {
|
|
||||||
if isEmpty && !isRecording && !isProcessing {
|
|
||||||
print("🗑️ Auto-deleting empty meeting")
|
|
||||||
deleteMeeting()
|
|
||||||
} else {
|
|
||||||
saveMeeting()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -445,8 +445,9 @@ struct MeetingDetailContentView: View {
|
|||||||
Text("Are you sure you want to delete this meeting? This action cannot be undone.")
|
Text("Are you sure you want to delete this meeting? This action cannot be undone.")
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
// Auto-delete empty meetings when leaving, otherwise save
|
// A failed recording may still be empty. Keep it until the user
|
||||||
viewModel.deleteIfEmpty()
|
// explicitly deletes it so app updates cannot erase history.
|
||||||
|
viewModel.saveMeeting()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user