Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea44104458 | ||
|
|
336fb95b07 | ||
|
|
c7e29828b4 |
@@ -276,7 +276,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 16;
|
||||
CURRENT_PROJECT_VERSION = 19;
|
||||
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.4;
|
||||
MARKETING_VERSION = 1.1.7;
|
||||
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 = 16;
|
||||
CURRENT_PROJECT_VERSION = 19;
|
||||
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.4;
|
||||
MARKETING_VERSION = 1.1.7;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI";
|
||||
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"])
|
||||
}
|
||||
inputNode.installTap(onBus: 0, bufferSize: 1024, format: inputFormat) { [weak self] buffer, _ in
|
||||
guard let self, buffer.frameLength > 0 else { return }
|
||||
self.updateAudioLevel(buffer, source: .mic)
|
||||
self.processAudioBuffer(buffer, converter: converter, targetFormat: targetFormat, source: .mic)
|
||||
guard let self else { return }
|
||||
self.processAudioBuffer(
|
||||
{ buffer },
|
||||
converter: converter,
|
||||
targetFormat: targetFormat,
|
||||
source: .mic
|
||||
)
|
||||
}
|
||||
audioEngine.prepare()
|
||||
try audioEngine.start()
|
||||
@@ -303,11 +307,13 @@ final class AudioManager: NSObject, ObservableObject {
|
||||
throw NSError(domain: "AudioManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unsupported system audio format"])
|
||||
}
|
||||
try tap.run(on: tapQueue) { [weak self] _, inputData, _, _, _ in
|
||||
guard let self,
|
||||
let buffer = AVAudioPCMBuffer(pcmFormat: inputFormat, bufferListNoCopy: inputData, deallocator: nil),
|
||||
buffer.frameLength > 0 else { return }
|
||||
self.updateAudioLevel(buffer, source: .system)
|
||||
self.processAudioBuffer(buffer, converter: converter, targetFormat: targetFormat, source: .system)
|
||||
guard let self else { return }
|
||||
self.processAudioBuffer(
|
||||
{ AVAudioPCMBuffer(pcmFormat: inputFormat, bufferListNoCopy: inputData, deallocator: nil) },
|
||||
converter: converter,
|
||||
targetFormat: targetFormat,
|
||||
source: .system
|
||||
)
|
||||
} invalidationHandler: { [weak self] _ in
|
||||
guard let self, !self.isRestartingSystemTap, self.isRecording else { return }
|
||||
Task { await self.restartSystemAudioTap() }
|
||||
@@ -315,11 +321,20 @@ final class AudioManager: NSObject, ObservableObject {
|
||||
}
|
||||
|
||||
private func processAudioBuffer(
|
||||
_ inputBuffer: AVAudioPCMBuffer,
|
||||
_ inputBufferProvider: () -> AVAudioPCMBuffer?,
|
||||
converter: AVAudioConverter,
|
||||
targetFormat: AVAudioFormat,
|
||||
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 capacity = max(1, AVAudioFrameCount(ceil(Double(inputBuffer.frameLength) * ratio)))
|
||||
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 }
|
||||
|
||||
audioFileLock.lock()
|
||||
defer { audioFileLock.unlock() }
|
||||
guard isAcceptingAudio else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
switch source {
|
||||
case .mic:
|
||||
@@ -378,8 +388,8 @@ final class AudioManager: NSObject, ObservableObject {
|
||||
pendingMicRestart = nil
|
||||
AudioLevelManager.shared.updateRecordingState(false)
|
||||
|
||||
// Stop new writes and wait for any callback already writing before
|
||||
// AVAudioFile is finalized and released.
|
||||
// Stop new callbacks and wait for any active conversion/write before
|
||||
// invalidating callback-owned buffers or finalizing AVAudioFile.
|
||||
audioFileLock.lock()
|
||||
isAcceptingAudio = false
|
||||
audioFileLock.unlock()
|
||||
|
||||
@@ -33,9 +33,16 @@ struct TemplateEditView: View {
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
TextField("Meeting Context", text: $template.context, axis: .vertical)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.lineLimit(4...10)
|
||||
TextEditor(text: $template.context)
|
||||
.scrollContentBackground(.hidden)
|
||||
.padding(8)
|
||||
.frame(height: 140)
|
||||
.background(Color.secondary.opacity(0.06))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.stroke(Color.secondary.opacity(0.25), lineWidth: 1)
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
|
||||
// Sections
|
||||
@@ -75,9 +82,16 @@ struct TemplateEditView: View {
|
||||
TextField("Section Title", text: $section.title)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
|
||||
TextField("Section Description", text: $section.description, axis: .vertical)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.lineLimit(3...8)
|
||||
TextEditor(text: $section.description)
|
||||
.scrollContentBackground(.hidden)
|
||||
.padding(8)
|
||||
.frame(height: 90)
|
||||
.background(Color.secondary.opacity(0.06))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.stroke(Color.secondary.opacity(0.25), lineWidth: 1)
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
|
||||
Button {
|
||||
@@ -113,12 +127,19 @@ struct TemplateEditView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.top)
|
||||
.disabled(template.title.isEmpty || template.sections.isEmpty)
|
||||
.disabled(!canSave)
|
||||
}
|
||||
.padding(24)
|
||||
}
|
||||
.navigationTitle("Edit Template")
|
||||
}
|
||||
|
||||
private var canSave: Bool {
|
||||
let hasTitle = !template.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
let hasInstructions = !template.context.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
|| !template.sections.isEmpty
|
||||
return hasTitle && hasInstructions
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
|
||||
@@ -2,15 +2,15 @@ import SwiftUI
|
||||
|
||||
struct TemplateListView: View {
|
||||
@StateObject private var viewModel = TemplatesViewModel()
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var editingTemplate: NoteTemplate?
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(viewModel.templates) { template in
|
||||
HStack {
|
||||
NavigationLink(destination: TemplateEditView(template: template) { updatedTemplate in
|
||||
viewModel.saveTemplate(updatedTemplate)
|
||||
}) {
|
||||
Button {
|
||||
presentEditor(for: template)
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(template.title)
|
||||
@@ -36,6 +36,7 @@ struct TemplateListView: View {
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -87,9 +88,9 @@ struct TemplateListView: View {
|
||||
.navigationTitle("Note Templates")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
NavigationLink(destination: TemplateEditView(template: viewModel.createNewTemplate()) { updatedTemplate in
|
||||
viewModel.saveTemplate(updatedTemplate)
|
||||
}) {
|
||||
Button {
|
||||
presentEditor(for: viewModel.createNewTemplate())
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
}
|
||||
@@ -106,6 +107,19 @@ struct TemplateListView: View {
|
||||
} message: {
|
||||
Text(viewModel.errorMessage ?? "")
|
||||
}
|
||||
.sheet(item: $editingTemplate) { template in
|
||||
NavigationStack {
|
||||
TemplateEditView(template: template) { updatedTemplate in
|
||||
viewModel.saveTemplate(updatedTemplate)
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 680, minHeight: 620)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentEditor(for template: NoteTemplate) {
|
||||
guard editingTemplate == nil else { return }
|
||||
editingTemplate = template
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user