@hasna/recordings 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/index.js +395 -37
  3. package/dist/db/database.d.ts.map +1 -1
  4. package/dist/db/recordings.d.ts.map +1 -1
  5. package/dist/index.js +78 -11
  6. package/dist/lib/config.d.ts.map +1 -1
  7. package/dist/lib/enhancer.d.ts.map +1 -1
  8. package/dist/lib/recorder.d.ts.map +1 -1
  9. package/dist/lib/transcriber.d.ts +8 -2
  10. package/dist/lib/transcriber.d.ts.map +1 -1
  11. package/dist/mcp/index.js +147 -30
  12. package/dist/types/index.d.ts +3 -0
  13. package/dist/types/index.d.ts.map +1 -1
  14. package/dist/version.d.ts +2 -0
  15. package/dist/version.d.ts.map +1 -0
  16. package/package.json +20 -3
  17. package/scripts/install_macos_app.sh +76 -0
  18. package/src/native/Recordings/{Recordings → App}/RecordingsApp.swift +5 -1
  19. package/src/native/Recordings/Package.resolved +21 -3
  20. package/src/native/Recordings/Package.swift +16 -5
  21. package/src/native/Recordings/{Recordings → RecordingsLib}/Info.plist +6 -0
  22. package/src/native/Recordings/{Recordings → RecordingsLib}/MenuBarPopover.swift +55 -11
  23. package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +36 -0
  24. package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +164 -0
  25. package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +113 -0
  26. package/src/native/Recordings/{Recordings → RecordingsLib}/ProjectStore.swift +11 -11
  27. package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +383 -0
  28. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +835 -0
  29. package/src/native/Recordings/{Recordings → RecordingsLib}/SettingsView.swift +19 -5
  30. package/src/native/Recordings/{Recordings → RecordingsLib}/VoiceShortcuts.swift +12 -8
  31. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +63 -0
  32. package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +23 -0
  33. package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +33 -0
  34. package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +92 -0
  35. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +58 -0
  36. package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +113 -0
  37. package/src/native/Recordings/build.sh +6 -6
  38. package/.takumi/settings.local.json +0 -7
  39. package/bun.lock +0 -250
  40. package/bunfig.toml +0 -2
  41. package/src/__tests__/agents.test.ts +0 -136
  42. package/src/__tests__/config.test.ts +0 -252
  43. package/src/__tests__/database.test.ts +0 -167
  44. package/src/__tests__/enhancer.test.ts +0 -639
  45. package/src/__tests__/preload.ts +0 -4
  46. package/src/__tests__/projects.test.ts +0 -109
  47. package/src/__tests__/recorder.test.ts +0 -278
  48. package/src/__tests__/recordings.test.ts +0 -353
  49. package/src/__tests__/transcriber.test.ts +0 -322
  50. package/src/__tests__/types.test.ts +0 -75
  51. package/src/cli/index.ts +0 -988
  52. package/src/db/agents.ts +0 -104
  53. package/src/db/database.ts +0 -163
  54. package/src/db/pg-migrations.ts +0 -82
  55. package/src/db/projects.ts +0 -71
  56. package/src/db/recordings.ts +0 -225
  57. package/src/index.ts +0 -81
  58. package/src/lib/config.ts +0 -223
  59. package/src/lib/enhancer.ts +0 -173
  60. package/src/lib/recorder.ts +0 -198
  61. package/src/lib/transcriber.ts +0 -105
  62. package/src/mcp/index.ts +0 -464
  63. package/src/native/Recordings/Recordings/RecordingEngine.swift +0 -455
  64. package/src/native/Recordings/test_fn.swift +0 -79
  65. package/src/native/Recordings/test_fn2.swift +0 -33
  66. package/src/types/index.ts +0 -144
  67. package/tsconfig.json +0 -21
  68. /package/src/native/Recordings/{Recordings → RecordingsLib}/FnKeyMonitor.swift +0 -0
  69. /package/src/native/Recordings/{Recordings → RecordingsLib}/Recordings.entitlements +0 -0
@@ -15,6 +15,7 @@ export interface Recording {
15
15
  goal: string | null;
16
16
  role: string | null;
17
17
  task_list_id: string | null;
18
+ machine_id: string | null;
18
19
  metadata: Record<string, unknown>;
19
20
  created_at: string;
20
21
  }
@@ -35,6 +36,7 @@ export interface CreateRecordingInput {
35
36
  goal?: string;
36
37
  role?: string;
37
38
  task_list_id?: string;
39
+ machine_id?: string;
38
40
  metadata?: Record<string, unknown>;
39
41
  }
40
42
  export interface RecordingFilter {
@@ -78,6 +80,7 @@ export interface RecordingsConfig {
78
80
  hotkey: string;
79
81
  auto_enhance: boolean;
80
82
  enhance_triggers: string[];
83
+ keyword_transforms: Record<string, string>;
81
84
  db_path: string;
82
85
  audio_dir: string;
83
86
  max_recording_seconds: number;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare const VERSION = "0.1.13";
2
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,WAAW,CAAC"}
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@hasna/recordings",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "description": "Speech-to-text recording tool with MCP and CLI — records, transcribes, and optionally enhances text using AI",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/hasna/recordings.git"
9
+ },
6
10
  "main": "dist/index.js",
7
11
  "types": "dist/index.d.ts",
8
12
  "bin": {
@@ -25,8 +29,21 @@
25
29
  "test:coverage": "bun test --coverage",
26
30
  "dev:cli": "bun run src/cli/index.ts",
27
31
  "dev:mcp": "bun run src/mcp/index.ts",
28
- "postinstall": "mkdir -p $HOME/.hasna/recordings/audio 2>/dev/null || true"
32
+ "prepack": "bun run build",
33
+ "postinstall": "bash scripts/install_macos_app.sh --postinstall"
29
34
  },
35
+ "files": [
36
+ "dist/",
37
+ "scripts/",
38
+ "src/native/Recordings/App/",
39
+ "src/native/Recordings/RecordingsLib/",
40
+ "src/native/Recordings/RecordingsTests/",
41
+ "src/native/Recordings/Package.swift",
42
+ "src/native/Recordings/Package.resolved",
43
+ "src/native/Recordings/build.sh",
44
+ "README.md",
45
+ "LICENSE"
46
+ ],
30
47
  "dependencies": {
31
48
  "@hasna/cloud": "^0.1.24",
32
49
  "@modelcontextprotocol/sdk": "^1.12.1",
@@ -45,4 +62,4 @@
45
62
  },
46
63
  "license": "Apache-2.0",
47
64
  "author": "Hasna <andrei@hasna.com>"
48
- }
65
+ }
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env bash
2
+ set -u
3
+
4
+ MODE="release"
5
+ POSTINSTALL=0
6
+
7
+ while [ "$#" -gt 0 ]; do
8
+ case "$1" in
9
+ --mode)
10
+ MODE="${2:-release}"
11
+ shift 2
12
+ ;;
13
+ --postinstall)
14
+ POSTINSTALL=1
15
+ shift
16
+ ;;
17
+ *)
18
+ echo "Unknown argument: $1" >&2
19
+ exit 2
20
+ ;;
21
+ esac
22
+ done
23
+
24
+ case "$MODE" in
25
+ debug|release) ;;
26
+ *)
27
+ echo "Mode must be debug or release" >&2
28
+ exit 2
29
+ ;;
30
+ esac
31
+
32
+ PACKAGE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
33
+ DATA_DIR="${HOME}/.hasna/recordings"
34
+ NATIVE_DIR="${PACKAGE_ROOT}/src/native/Recordings"
35
+ APP_SOURCE="${NATIVE_DIR}/.build/${MODE}/Recordings.app"
36
+ APP_DEST="${DATA_DIR}/Recordings.app"
37
+
38
+ mkdir -p "${DATA_DIR}/audio"
39
+
40
+ if [ "$(uname -s)" != "Darwin" ]; then
41
+ exit 0
42
+ fi
43
+
44
+ warn_or_fail() {
45
+ local message="$1"
46
+ if [ "$POSTINSTALL" -eq 1 ]; then
47
+ echo "Recordings.app was not installed: ${message}" >&2
48
+ echo "Run 'recordings app install' after fixing this machine." >&2
49
+ exit 0
50
+ fi
51
+ echo "${message}" >&2
52
+ exit 1
53
+ }
54
+
55
+ if ! command -v swift >/dev/null 2>&1; then
56
+ warn_or_fail "Swift toolchain not found"
57
+ fi
58
+
59
+ if [ ! -d "$NATIVE_DIR" ]; then
60
+ warn_or_fail "native app sources are missing from package: ${NATIVE_DIR}"
61
+ fi
62
+
63
+ (
64
+ cd "$NATIVE_DIR" || exit 1
65
+ ./build.sh "$MODE"
66
+ ) || warn_or_fail "native app build failed"
67
+
68
+ if [ ! -d "$APP_SOURCE" ]; then
69
+ warn_or_fail "native build did not produce ${APP_SOURCE}"
70
+ fi
71
+
72
+ rm -rf "$APP_DEST"
73
+ mkdir -p "$DATA_DIR"
74
+ cp -R "$APP_SOURCE" "$APP_DEST" || warn_or_fail "failed to copy app bundle"
75
+
76
+ echo "Installed Recordings.app from package: ${APP_DEST}"
@@ -1,6 +1,7 @@
1
1
  @preconcurrency import Cocoa
2
2
  import SwiftUI
3
3
  import KeyboardShortcuts
4
+ import RecordingsLib
4
5
 
5
6
  @main
6
7
  struct RecordingsApp: App {
@@ -18,7 +19,10 @@ struct RecordingsApp: App {
18
19
  MenuBarExtra {
19
20
  MenuBarPopover(engine: engine, shortcuts: shortcuts, projectStore: projectStore)
20
21
  .frame(width: 320, height: 420)
21
- .onAppear { engine.projectStore = projectStore }
22
+ .onAppear {
23
+ engine.projectStore = projectStore
24
+ engine.voiceShortcuts = shortcuts
25
+ }
22
26
  } label: {
23
27
  if engine.isRecording {
24
28
  Image(systemName: "record.circle.fill")
@@ -1,13 +1,31 @@
1
1
  {
2
- "originHash" : "a6c896b6f831f2130914352c7417e95b036b1f02a1ff70318f1f4e5aa8966e9b",
2
+ "originHash" : "a63dc0aa4ab0839e19cd7644cfd2bb42729aafbf9ef920adeb5c3ac856b7701e",
3
3
  "pins" : [
4
4
  {
5
5
  "identity" : "keyboardshortcuts",
6
6
  "kind" : "remoteSourceControl",
7
7
  "location" : "https://github.com/sindresorhus/KeyboardShortcuts",
8
8
  "state" : {
9
- "revision" : "1aef85578fdd4f9eaeeb8d53b7b4fc31bf08fe27",
10
- "version" : "2.4.0"
9
+ "revision" : "de5b143889b9aa90a997b18e311527095dfb28e0",
10
+ "version" : "1.12.0"
11
+ }
12
+ },
13
+ {
14
+ "identity" : "swift-syntax",
15
+ "kind" : "remoteSourceControl",
16
+ "location" : "https://github.com/swiftlang/swift-syntax.git",
17
+ "state" : {
18
+ "revision" : "0687f71944021d616d34d922343dcef086855920",
19
+ "version" : "600.0.1"
20
+ }
21
+ },
22
+ {
23
+ "identity" : "swift-testing",
24
+ "kind" : "remoteSourceControl",
25
+ "location" : "https://github.com/apple/swift-testing.git",
26
+ "state" : {
27
+ "revision" : "399f76dcd91e4c688ca2301fa24a8cc6d9927211",
28
+ "version" : "0.99.0"
11
29
  }
12
30
  }
13
31
  ],
@@ -8,14 +8,25 @@ let package = Package(
8
8
  .macOS(.v26)
9
9
  ],
10
10
  dependencies: [
11
- .package(url: "https://github.com/sindresorhus/KeyboardShortcuts", from: "2.0.0")
11
+ .package(url: "https://github.com/sindresorhus/KeyboardShortcuts", exact: "1.12.0"),
12
+ .package(url: "https://github.com/apple/swift-testing.git", .upToNextMinor(from: "0.99.0")),
12
13
  ],
13
14
  targets: [
14
- .executableTarget(
15
- name: "Recordings",
15
+ .target(
16
+ name: "RecordingsLib",
16
17
  dependencies: ["KeyboardShortcuts"],
17
- path: "Recordings",
18
+ path: "RecordingsLib",
18
19
  exclude: ["Info.plist", "Recordings.entitlements"]
19
- )
20
+ ),
21
+ .executableTarget(
22
+ name: "App",
23
+ dependencies: ["RecordingsLib"],
24
+ path: "App"
25
+ ),
26
+ .testTarget(
27
+ name: "RecordingsTests",
28
+ dependencies: ["RecordingsLib", .product(name: "Testing", package: "swift-testing")],
29
+ path: "RecordingsTests"
30
+ ),
20
31
  ]
21
32
  )
@@ -8,15 +8,21 @@
8
8
  <string>Recordings</string>
9
9
  <key>CFBundleDisplayName</key>
10
10
  <string>Recordings</string>
11
+ <key>CFBundleExecutable</key>
12
+ <string>Recordings</string>
11
13
  <key>CFBundleIdentifier</key>
12
14
  <string>com.hasna.recordings</string>
13
15
  <key>CFBundleVersion</key>
14
16
  <string>1</string>
17
+ <key>CFBundlePackageType</key>
18
+ <string>APPL</string>
15
19
  <key>CFBundleShortVersionString</key>
16
20
  <string>0.1.0</string>
17
21
  <key>LSMinimumSystemVersion</key>
18
22
  <string>26.0</string>
19
23
  <key>NSMicrophoneUsageDescription</key>
20
24
  <string>Recordings needs microphone access to record and transcribe your speech.</string>
25
+ <key>NSAppleEventsUsageDescription</key>
26
+ <string>Recordings needs Apple Events access to paste transcribed text into your active app.</string>
21
27
  </dict>
22
28
  </plist>
@@ -1,13 +1,19 @@
1
1
  import SwiftUI
2
2
  import KeyboardShortcuts
3
3
 
4
- struct MenuBarPopover: View {
5
- @ObservedObject var engine: RecordingEngine
6
- @ObservedObject var shortcuts: VoiceShortcuts
7
- @ObservedObject var projectStore: ProjectStore
4
+ public struct MenuBarPopover: View {
5
+ @ObservedObject public var engine: RecordingEngine
6
+ @ObservedObject public var shortcuts: VoiceShortcuts
7
+ @ObservedObject public var projectStore: ProjectStore
8
8
  @State private var copiedIndex: Int?
9
9
  @State private var filterProjectId: String?
10
10
 
11
+ public init(engine: RecordingEngine, shortcuts: VoiceShortcuts, projectStore: ProjectStore) {
12
+ self.engine = engine
13
+ self.shortcuts = shortcuts
14
+ self.projectStore = projectStore
15
+ }
16
+
11
17
  private var filteredTranscriptions: [TranscriptionResult] {
12
18
  guard let filter = filterProjectId else {
13
19
  return engine.recentTranscriptions
@@ -18,13 +24,15 @@ struct MenuBarPopover: View {
18
24
  return engine.recentTranscriptions.filter { $0.projectId == filter }
19
25
  }
20
26
 
21
- var body: some View {
27
+ public var body: some View {
22
28
  VStack(spacing: 0) {
23
29
  header
24
30
  .padding(.horizontal, 16).padding(.top, 12).padding(.bottom, 8)
25
31
  Divider()
32
+ modePicker
33
+ .padding(.horizontal, 16).padding(.top, 10)
26
34
  recordingArea
27
- .padding(.horizontal, 16).padding(.vertical, 12)
35
+ .padding(.horizontal, 16).padding(.top, 8).padding(.bottom, 12)
28
36
  Divider()
29
37
  if !engine.recentTranscriptions.isEmpty {
30
38
  filterBar
@@ -85,9 +93,20 @@ struct MenuBarPopover: View {
85
93
 
86
94
  // MARK: - Recording Area
87
95
 
96
+ private var modePicker: some View {
97
+ Picker("Mode", selection: $engine.mode) {
98
+ ForEach(RecordingMode.allCases) { mode in
99
+ Image(systemName: mode.icon)
100
+ .tag(mode)
101
+ .help(mode.rawValue)
102
+ }
103
+ }
104
+ .pickerStyle(.segmented)
105
+ }
106
+
107
+ @ViewBuilder
88
108
  private var recordingArea: some View {
89
- Group {
90
- if engine.isRecording {
109
+ if engine.isRecording {
91
110
  VStack(spacing: 6) {
92
111
  HStack {
93
112
  Circle().fill(.red).frame(width: 8, height: 8)
@@ -97,6 +116,18 @@ struct MenuBarPopover: View {
97
116
  Button("Stop") { engine.stopAndTranscribe() }
98
117
  .controlSize(.small)
99
118
  }
119
+ // Live streaming transcription text
120
+ if !engine.liveTranscriptionText.isEmpty {
121
+ Text(engine.liveTranscriptionText)
122
+ .font(.callout)
123
+ .foregroundStyle(.primary)
124
+ .multilineTextAlignment(.leading)
125
+ .lineLimit(3)
126
+ .padding(.vertical, 4)
127
+ Text("Listening...")
128
+ .font(.caption)
129
+ .foregroundStyle(.green)
130
+ }
100
131
  if let project = projectStore.activeProject {
101
132
  HStack(spacing: 4) {
102
133
  Image(systemName: "folder.fill").foregroundStyle(.tint)
@@ -107,7 +138,7 @@ struct MenuBarPopover: View {
107
138
  }
108
139
  .padding(10)
109
140
  .glassEffect(.regular)
110
- } else if engine.isTranscribing {
141
+ } else if engine.isTranscribing {
111
142
  VStack(spacing: 6) {
112
143
  HStack(spacing: 8) {
113
144
  ProgressView().controlSize(.small)
@@ -115,6 +146,15 @@ struct MenuBarPopover: View {
115
146
  .foregroundStyle(.secondary)
116
147
  Spacer()
117
148
  }
149
+ // Show accumulated text during transcribing
150
+ if !engine.liveTranscriptionText.isEmpty {
151
+ Text(engine.liveTranscriptionText)
152
+ .font(.callout)
153
+ .foregroundStyle(.primary)
154
+ .multilineTextAlignment(.leading)
155
+ .lineLimit(3)
156
+ .padding(.vertical, 4)
157
+ }
118
158
  if let project = projectStore.activeProject {
119
159
  HStack(spacing: 4) {
120
160
  Image(systemName: "folder.fill").foregroundStyle(.tint)
@@ -126,7 +166,7 @@ struct MenuBarPopover: View {
126
166
  .frame(maxWidth: .infinity)
127
167
  .padding(10)
128
168
  .glassEffect(.regular)
129
- } else {
169
+ } else {
130
170
  VStack(spacing: 6) {
131
171
  Button { engine.startRecording() } label: {
132
172
  Label("Record", systemImage: "mic.circle.fill")
@@ -137,11 +177,15 @@ struct MenuBarPopover: View {
137
177
  Text("or hold \(shortcut.description)")
138
178
  .foregroundStyle(.tertiary)
139
179
  }
180
+ Text(engine.statusMessage)
181
+ .font(.caption)
182
+ .foregroundStyle(engine.statusMessage == "Ready" ? Color.secondary : Color.orange)
183
+ .multilineTextAlignment(.center)
184
+ .lineLimit(2)
140
185
  }
141
186
  .frame(maxWidth: .infinity)
142
187
  .padding(10)
143
188
  .glassEffect(.regular)
144
- }
145
189
  }
146
190
  }
147
191
 
@@ -0,0 +1,36 @@
1
+ import Foundation
2
+
3
+ enum NativeAppLog {
4
+ private static let lock = NSLock()
5
+
6
+ static func write(_ message: String, homePath: String = FileManager.default.homeDirectoryForCurrentUser.path) {
7
+ lock.lock()
8
+ defer { lock.unlock() }
9
+
10
+ let dir = "\(homePath)/.hasna/recordings"
11
+ let path = "\(dir)/Recordings.log"
12
+ let line = "[\(Self.timestamp())] \(message)\n"
13
+
14
+ do {
15
+ try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
16
+ if FileManager.default.fileExists(atPath: path),
17
+ let handle = FileHandle(forWritingAtPath: path) {
18
+ defer { try? handle.close() }
19
+ try handle.seekToEnd()
20
+ if let data = line.data(using: .utf8) {
21
+ try handle.write(contentsOf: data)
22
+ }
23
+ } else {
24
+ try line.write(toFile: path, atomically: true, encoding: .utf8)
25
+ }
26
+ } catch {
27
+ fputs("[Recordings] log write failed: \(error.localizedDescription)\n", stderr)
28
+ }
29
+ }
30
+
31
+ private static func timestamp() -> String {
32
+ let formatter = ISO8601DateFormatter()
33
+ formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
34
+ return formatter.string(from: Date())
35
+ }
36
+ }
@@ -0,0 +1,164 @@
1
+ @preconcurrency import AVFoundation
2
+ import Foundation
3
+
4
+ enum NativePCMRecorderError: LocalizedError {
5
+ case noInputDevice
6
+ case unsupportedInputFormat
7
+ case failedToCreateConverter
8
+ case failedToStart(String)
9
+
10
+ var errorDescription: String? {
11
+ switch self {
12
+ case .noInputDevice:
13
+ return "No microphone input device is available"
14
+ case .unsupportedInputFormat:
15
+ return "The selected microphone format is not supported"
16
+ case .failedToCreateConverter:
17
+ return "Could not prepare microphone audio conversion"
18
+ case .failedToStart(let message):
19
+ return "Could not start microphone capture: \(message)"
20
+ }
21
+ }
22
+ }
23
+
24
+ final class NativePCMRecorder: @unchecked Sendable {
25
+ private let engine = AVAudioEngine()
26
+ private let onPCM: @Sendable (Data) -> Void
27
+ private var converter: AVAudioConverter?
28
+ private var inputFormat: AVAudioFormat?
29
+ private var outputFormat: AVAudioFormat?
30
+ private var started = false
31
+
32
+ init(onPCM: @escaping @Sendable (Data) -> Void) {
33
+ self.onPCM = onPCM
34
+ }
35
+
36
+ func start() throws {
37
+ guard !started else { return }
38
+
39
+ let inputNode = engine.inputNode
40
+ let inputFormat = inputNode.inputFormat(forBus: 0)
41
+ guard inputFormat.channelCount > 0 else {
42
+ throw NativePCMRecorderError.noInputDevice
43
+ }
44
+ guard inputFormat.sampleRate > 0 else {
45
+ throw NativePCMRecorderError.unsupportedInputFormat
46
+ }
47
+ guard let outputFormat = Self.realtimeOutputFormat() else {
48
+ throw NativePCMRecorderError.unsupportedInputFormat
49
+ }
50
+ guard let converter = AVAudioConverter(from: inputFormat, to: outputFormat) else {
51
+ throw NativePCMRecorderError.failedToCreateConverter
52
+ }
53
+
54
+ self.converter = converter
55
+ self.inputFormat = inputFormat
56
+ self.outputFormat = outputFormat
57
+
58
+ inputNode.installTap(onBus: 0, bufferSize: 1_024, format: inputFormat) { [weak self] buffer, _ in
59
+ self?.convertAndEmit(buffer)
60
+ }
61
+
62
+ do {
63
+ engine.prepare()
64
+ try engine.start()
65
+ started = true
66
+ } catch {
67
+ inputNode.removeTap(onBus: 0)
68
+ self.converter = nil
69
+ self.inputFormat = nil
70
+ self.outputFormat = nil
71
+ throw NativePCMRecorderError.failedToStart(error.localizedDescription)
72
+ }
73
+ }
74
+
75
+ func stop() {
76
+ guard started else {
77
+ converter = nil
78
+ inputFormat = nil
79
+ outputFormat = nil
80
+ return
81
+ }
82
+
83
+ engine.inputNode.removeTap(onBus: 0)
84
+ engine.stop()
85
+ converter = nil
86
+ inputFormat = nil
87
+ outputFormat = nil
88
+ started = false
89
+ }
90
+
91
+ deinit {
92
+ stop()
93
+ }
94
+
95
+ private func convertAndEmit(_ inputBuffer: AVAudioPCMBuffer) {
96
+ guard let converter, let inputFormat, let outputFormat else { return }
97
+
98
+ let sampleRateRatio = outputFormat.sampleRate / inputFormat.sampleRate
99
+ let estimatedFrames = AVAudioFrameCount(Double(inputBuffer.frameLength) * sampleRateRatio) + 512
100
+ guard let outputBuffer = AVAudioPCMBuffer(
101
+ pcmFormat: outputFormat,
102
+ frameCapacity: max(estimatedFrames, 1)
103
+ ) else {
104
+ return
105
+ }
106
+
107
+ let inputSource = AudioConverterInputSource(buffer: inputBuffer)
108
+ var conversionError: NSError?
109
+ converter.convert(to: outputBuffer, error: &conversionError) { _, status in
110
+ inputSource.next(status: status)
111
+ }
112
+
113
+ guard conversionError == nil else { return }
114
+ let data = Self.extractPCM16Data(from: outputBuffer)
115
+ if !data.isEmpty {
116
+ onPCM(data)
117
+ }
118
+ }
119
+
120
+ static func realtimeOutputFormat() -> AVAudioFormat? {
121
+ AVAudioFormat(
122
+ commonFormat: .pcmFormatInt16,
123
+ sampleRate: 24_000,
124
+ channels: 1,
125
+ interleaved: true
126
+ )
127
+ }
128
+
129
+ static func extractPCM16Data(from buffer: AVAudioPCMBuffer) -> Data {
130
+ var data = Data()
131
+ for audioBuffer in UnsafeMutableAudioBufferListPointer(buffer.mutableAudioBufferList) {
132
+ guard let bytes = audioBuffer.mData?.assumingMemoryBound(to: UInt8.self),
133
+ audioBuffer.mDataByteSize > 0 else {
134
+ continue
135
+ }
136
+ data.append(bytes, count: Int(audioBuffer.mDataByteSize))
137
+ }
138
+ return data
139
+ }
140
+ }
141
+
142
+ private final class AudioConverterInputSource: @unchecked Sendable {
143
+ private let buffer: AVAudioPCMBuffer
144
+ private let lock = NSLock()
145
+ private var consumed = false
146
+
147
+ init(buffer: AVAudioPCMBuffer) {
148
+ self.buffer = buffer
149
+ }
150
+
151
+ func next(status: UnsafeMutablePointer<AVAudioConverterInputStatus>) -> AVAudioBuffer? {
152
+ lock.lock()
153
+ defer { lock.unlock() }
154
+
155
+ if consumed {
156
+ status.pointee = .noDataNow
157
+ return nil
158
+ }
159
+
160
+ consumed = true
161
+ status.pointee = .haveData
162
+ return buffer
163
+ }
164
+ }