Version: 1.0 Date: 2026-03-04 Author: Jatin Kumar Malik Status: Draft
VocaMac is a native macOS menu bar application built with Swift and SwiftUI. It captures microphone audio, transcribes it locally using WhisperKit, and injects the resulting text at the cursor position in any application.
┌──────────────────────────────────────────────────────────────┐
│ macOS System Layer │
│ ┌────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ CGEventTap │ │ AVAudioEngine│ │ NSPasteboard │ │
│ │ (Hotkeys) │ │ (Microphone) │ │ + CGEvent (Paste) │ │
│ └─────┬──────┘ └──────┬───────┘ └────────┬──────────┘ │
│ │ │ │ │
├────────┼────────────────┼─────────────────────┼───────────────┤
│ │ VocaMac Application │ │
│ ▼ ▼ ▲ │
│ ┌───────────┐ ┌─────────────┐ ┌───────────────────┐ │
│ │ HotKey │ │ Audio │ │ TextInjector │ │
│ │ Manager │ │ Engine │ │ │ │
│ └─────┬─────┘ └──────┬──────┘ └────────▲──────────┘ │
│ │ │ │ │
│ ▼ ▼ │ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ AppState │ │
│ │ (Observable, Reactive State) │ │
│ └───────────────────────┬──────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ WhisperService │ │
│ │ (WhisperKit (CoreML)) │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ ModelManager │ │ │
│ │ │ (Download, Load, Detect) │ │ │
│ │ └──────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ SwiftUI Layer │ │
│ │ ┌──────────────┐ ┌────────────┐ ┌──────────────┐ │ │
│ │ │ MenuBarView │ │SettingsView│ │ │
│ │ └──────────────┘ └────────────┘ └──────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
| Layer | Technology | Version | Purpose |
|---|---|---|---|
| Language | Swift | 5.9+ | Primary development language |
| UI | SwiftUI | macOS 13+ | Menu bar UI, settings, onboarding |
| Audio | AVAudioEngine | macOS 13+ | Real-time microphone capture |
| Hotkeys | CGEventTap (Quartz) | macOS 13+ | System-wide key event interception |
| Text Injection | NSPasteboard + CGEvent | macOS 13+ | Clipboard-based text insertion |
| STT Engine | WhisperKit | 0.9.4+ | CoreML-based on-device speech-to-text |
| Acceleration | Metal | macOS 13+ | GPU-accelerated inference on Apple Silicon |
| Build | Swift Package Manager | 5.9+ | Dependency management and build |
| Min OS | macOS 13 Ventura | - | Minimum supported macOS version |
| Update Checks | GitHub Releases API | v3 | In-app release detection and DMG download |
VocaMacApp (entry point)
├── AppState (shared state)
│ ├── HotKeyManager
│ ├── AudioEngine
│ ├── WhisperService
│ │ └── ModelManager
│ │ └── SystemInfo
│ ├── UpdateChecker
│ └── TextInjector
│ └── SoundManager
├── MenuBarView
├── SettingsView
└── SettingsView
Responsibility: Bootstrap the app, configure as menu bar-only (no Dock icon), initialize all services.
Key Design Decisions:
- Uses
MenuBarExtra(SwiftUI, macOS 13+) for the menu bar presence - Sets
LSUIElement = truein Info.plist to hide from Dock - Creates
AppStateas@StateObjectand passes it through the environment
Lifecycle:
App Launch
→ Initialize AppState
→ AppState checks permissions
→ AppState loads default model
→ MenuBarExtra renders
→ HotKeyManager starts listening
→ App is ready
Responsibility: Single source of truth for all app state. Observable object that drives reactive UI updates.
Key State Properties:
@Published var appStatus: AppStatus // .idle, .recording, .processing, .error
@Published var currentModel: WhisperModelInfo // Currently loaded model
@Published var activationMode: ActivationMode // .pushToTalk, .doubleTapToggle
@Published var isRecording: Bool
@Published var audioLevel: Float // 0.0 - 1.0, for visual feedback
@Published var lastTranscription: String?
@Published var micPermission: PermissionStatus
@Published var accessibilityPermission: PermissionStatus
@Published var selectedLanguage: String // "auto" or ISO 639-1 codeOrchestration Logic:
HotKey Triggered (start)
→ Set appStatus = .recording
→ AudioEngine.startRecording()
HotKey Triggered (stop)
→ AudioEngine.stopRecording() → returns [Float]
→ Set appStatus = .processing
→ WhisperService.transcribe([Float]) → returns String
→ TextInjector.inject(String)
→ Set appStatus = .idle
Responsibility: Listen for system-wide key events to trigger recording start/stop.
Implementation Approach:
- Uses
CGEvent.tapCreate()to create a Mach port event tap - Tap is inserted at
.cgSessionEventTaplevel for user-session coverage - The tap acts as an event filter and consumes only the configured hotkey events so they don't leak into the frontmost app
- Callback processes
keyDown/keyUpevents for regular keys andflagsChangedevents for modifier keys
Activation Modes:
| Mode | Trigger Start | Trigger Stop |
|---|---|---|
| Push-to-Talk | Key down | Key up |
| Double-Tap Toggle | 2nd tap within threshold | Next double-tap, or silence detection |
Double-Tap Detection Algorithm:
On keyDown:
currentTime = now()
if (currentTime - lastKeyDownTime) < doubleTapThreshold:
→ Fire "double tap" event
→ Reset lastKeyDownTime
else:
→ Store lastKeyDownTime = currentTime
On keyUp:
(Used only for push-to-talk mode)
Default Hotkey: Right Option (keyCode 61). Users can choose a preset or record any single activation key from Settings; the selected key is reserved by VocaMac while the app is running.
Required Permissions: Accessibility and Input Monitoring (System Settings → Privacy & Security)
Responsibility: Capture audio from the microphone in the format required by WhisperKit.
Audio Pipeline:
Microphone → AVAudioInputNode → Format Converter → Buffer Accumulator
(16kHz, mono, ([Float] array)
Float32 PCM)
Key Configuration:
- Sample rate: 16,000 Hz (WhisperKit requirement)
- Channels: 1 (mono)
- Format: Float32 PCM
- Buffer size: 4096 frames per callback
Silence Detection:
- Calculate RMS energy of each buffer
- Track time since last buffer above silence threshold
- Trigger silence callback when silence exceeds configured duration
- Configurable threshold (default: 0.01 RMS) and duration (default: 2.0s)
Audio Level Reporting:
- Normalize RMS energy to 0.0–1.0 range
- Report to AppState on each buffer for UI visualization
- Throttle updates to ~15 Hz to avoid excessive UI refreshes
Responsibility: Load WhisperKit models and perform transcription.
Integration Strategy:
- WhisperKit is included as a Swift Package Manager dependency (see Package.swift)
- Provides a native Swift async/await API — no C bridging required
- Models are in CoreML format, optimized per-device by Apple's Neural Engine
Core API:
class WhisperService {
func loadModel(path: String) throws
func transcribe(audioData: [Float], language: String?) async throws -> TranscriptionResult
func unloadModel()
var isModelLoaded: Bool { get }
}Transcription Flow:
audioData: [Float]
→ whisper_full_default_params(WHISPER_SAMPLING_GREEDY)
→ Configure params (language, threads, etc.)
→ whisper_full(ctx, params, audioData, count)
→ Iterate segments: whisper_full_get_segment_text()
→ Concatenate segments → TranscriptionResult
Threading:
- Transcription runs on a background thread (Swift async/await)
- Number of threads = min(processorCount, 4) for optimal performance
- Model loading also happens on background thread
Metal Acceleration:
- Enabled by default on Apple Silicon when WhisperKit is compiled with Metal support
- Compile flag:
WHISPER_METAL=1orCoreML_METAL=1 - VocaMac targets Apple Silicon only; Intel Macs are not a supported runtime
Responsibility: Discover, download, verify, and manage whisper model files.
Model Storage:
~/Library/Application Support/VocaMac/
└── models/
├── ggml-tiny.bin (39 MB)
├── ggml-base.bin (142 MB) ← downloaded on demand
├── ggml-small.bin (466 MB) ← downloaded on demand
├── ggml-medium.bin (1.5 GB) ← downloaded on demand
└── ggml-large-v3.bin (3.1 GB) ← downloaded on demand
Model Catalog:
| Model | Size | RAM Required | Relative Speed | Accuracy |
|---|---|---|---|---|
| tiny | 39 MB | ~1 GB | 1x (fastest) | Good |
| base | 142 MB | ~1.5 GB | 2x | Better |
| small | 466 MB | ~2 GB | 4x | Great |
| medium | 1.5 GB | ~5 GB | 8x | Excellent |
| large-v3 | 3.1 GB | ~10 GB | 16x | Best |
Download Source: Hugging Face (https://huggingface.co/ggerganov/WhisperKit/resolve/main/)
Download Process:
- Check if model file exists locally
- If not, initiate async download with URLSession
- Report progress via delegate/closure
- Verify SHA256 checksum after download
- Move to models directory on success
Responsibility: Detect system hardware capabilities and recommend optimal model size.
Detection Points:
- CPU architecture:
uname()→ arm64 (Apple Silicon) — the only supported runtime target - Physical RAM:
ProcessInfo.processInfo.physicalMemory - Processor name:
sysctlbyname("machdep.cpu.brand_string") - Core count:
ProcessInfo.processInfo.activeProcessorCount
Recommendation Algorithm:
Apple Silicon:
RAM ≤ 8 GB → tiny (safe default)
RAM = 16 GB → small (good balance)
RAM ≥ 24 GB → medium (high quality)
The
recommendModelfunction inSystemInfo.swiftretains a defensive Intel branch (smaller models, no Metal). It exists only to keep the code valid if someone compiles from source on Intel; the released DMG isarm64-only and Intel Macs are not a supported configuration.
Responsibility: Insert transcribed text at the cursor position in any application.
Algorithm:
1. Save current clipboard contents
2. Write transcribed text to clipboard (NSPasteboard)
3. Wait 50ms (ensure clipboard is updated)
4. Simulate Cmd+V keypress via CGEvent
5. Wait 100ms (ensure paste is processed)
6. Restore original clipboard contents
CGEvent Simulation:
CGEventSource(stateID: .hidSystemState)
→ Create keyDown for Cmd (keyCode 55)
→ Create keyDown for V (keyCode 9) with .maskCommand flag
→ Create keyUp for V
→ Create keyUp for Cmd
→ Post all events to .cghidEventTap
Required Permission: Accessibility (same as HotKeyManager)
Edge Cases:
- If clipboard contains non-text content (images, files), save and restore the full pasteboard items
- Add configurable delay between paste simulation events for slower apps
- Handle the case where the user's clipboard is empty
Responsibility: Detect new stable releases from GitHub, download the latest signed DMG, verify integrity, and guide the user through drag-to-replace installation.
Update Flow:
On launch (max once every 24h)
→ GET /repos/jatinkrmalik/vocamac/releases/latest
→ Compare tag_name vs CFBundleShortVersionString
→ If newer: show update banner in MenuBarView
→ User opens update sheet and starts download
→ Download DMG with progress
→ Verify SHA-256 using assets[].digest
→ Open DMG in Finder (user drags app to /Applications)
Manual Check:
- Settings → About includes Check for Updates...
Key Constraints:
- Uses GitHub API unauthenticated (rate-limited), so checks are throttled to once per day automatically
- Works with existing DMG release artifacts and current release workflow
User Action
│
▼
HotKeyManager (CGEventTap)
│ detects hotkey press/release
▼
AppState (orchestrator)
│ sets status = .recording
▼
AudioEngine (AVAudioEngine)
│ captures mic audio → [Float] buffer
│ reports audio levels → AppState → MenuBarView
▼
AppState (orchestrator)
│ sets status = .processing
▼
WhisperService (WhisperKit)
│ transcribes [Float] → String
▼
AppState (orchestrator)
│ sets status = .idle
▼
TextInjector (NSPasteboard + CGEvent)
│ injects text at cursor
▼
Target Application (Safari, Slack, VS Code, etc.)
│ receives pasted text
▼
Done
| Stage | Format | Details |
|---|---|---|
| Microphone input | Hardware-dependent | Usually 44.1kHz or 48kHz, stereo |
| After format conversion | Float32 PCM | 16kHz, mono, [-1.0, 1.0] range |
| Audio buffer | [Float] |
Swift array of samples |
| WhisperKit input | const float * |
C pointer to samples array |
| WhisperKit output | const char * |
C string per segment |
| Transcription result | String |
Swift string, all segments concatenated |
| Clipboard | NSPasteboard.string |
UTF-8 string |
| Key simulation | CGEvent |
Keyboard events posted to HID |
Main Thread (UI)
├── SwiftUI rendering
├── AppState @Published updates
└── Menu bar icon updates
Background Thread (Audio)
└── AVAudioEngine tap callback
└── Audio buffer accumulation
Background Thread (Transcription)
└── whisper_full() call
└── Can take 1-10+ seconds depending on model
Main Thread (Text Injection)
└── NSPasteboard + CGEvent posting
└── Must be on main thread for CGEvent
Key Threading Rules:
- Audio capture callbacks run on AVAudioEngine's internal thread - keep work minimal
- Transcription runs via
Task { }on a background executor - never block the main thread - UI updates via
@MainActororDispatchQueue.main - CGEvent posting should happen from the main thread
- Model loading/downloading uses async/await on background threads
| Permission | macOS API | Required For | How to Request |
|---|---|---|---|
| Microphone | AVCaptureDevice.requestAccess | AudioEngine | Programmatic prompt |
| Accessibility | AXIsProcessTrusted | HotKeyManager, TextInjector | Manual: System Settings → Privacy → Accessibility |
Accessibility Permission Check:
let options = [kAXTrustedCheckOptionPrompt.takeRetainedValue(): true] as CFDictionary
let isTrusted = AXIsProcessTrustedWithOptions(options)Note: Unlike microphone access, Accessibility permission cannot be granted via a system dialog - the user must manually add the app in System Settings. The app should provide clear instructions.
Package.swift
├── Platform: .macOS(.v13)
├── Products: VocaMac executable
├── Dependencies: WhisperKit (vendored or submodule)
├── Swift settings: -O (optimized for release)
└── Platforms: .macOS(.v13)
# Build + install to /Applications (recommended)
make install
# Build .app bundle in repo root (fast dev iteration)
make build
# Install CLI commands to ~/.local/bin
make install-cli
# Run tests
make test
# Debug build (SPM only, no .app bundle)
swift build
# Release build (SPM only, no .app bundle)
swift build -c release- GitHub Releases — Developer ID signed & notarized DMG and ZIP, built by CI
- Homebrew Cask -
brew install --cask vocamac(see docs/HOMEBREW.md) - Mac App Store - Future consideration (requires sandbox compliance)
While VocaMac is a native macOS app, the architecture is designed to facilitate a future Windows port (VocaWin):
Shared Concepts:
└── Whisper models ← Same model family across platforms
└── UX patterns ← Same user interaction design
Platform-Specific:
┌──────────────────────┬──────────────────────┐
│ macOS │ Windows │
├──────────────────────┼──────────────────────┤
│ Swift + SwiftUI │ C++/C# + WinUI 3 │
│ AVAudioEngine │ WASAPI / NAudio │
│ CGEventTap │ SetWindowsHookEx │
│ NSPasteboard + CGEvt │ Clipboard + SendInput│
│ Metal acceleration │ CUDA / DirectML │
│ MenuBarExtra │ System Tray (NotifyIcon) │
└──────────────────────┴──────────────────────┘
- Whisper models - Same OpenAI Whisper model family on all platforms
- Model files - Each platform uses its optimal format (CoreML on macOS, GGML on Linux/Windows)
- Model catalog - Same model variants and metadata
- UX patterns - Same user flows and interaction design
- UI framework and rendering
- Audio capture API
- Global hotkey mechanism
- Text injection method
- Permission handling
- App lifecycle and distribution
| Error Scenario | Handling |
|---|---|
| Microphone permission denied | Show guidance to enable in System Settings |
| Accessibility permission denied | Show step-by-step guide with screenshots |
| Model file corrupted/missing | Re-download model, fall back to bundled tiny |
| Audio device disconnected | Detect and notify user, pause recording |
| Transcription fails | Show error in menu bar popover, log details |
| Clipboard restore fails | Log warning, don't crash - clipboard is transient |
| Out of memory during transcription | Suggest a smaller model, show clear error |
| Network error during model download | Retry with exponential backoff, allow manual retry |
- No network communication except model downloads from Hugging Face
- No telemetry or analytics - fully offline operation
- Audio data never leaves the device - processed entirely in-memory
- No persistent audio storage - audio buffers are discarded after transcription
- Model files verified by checksum - prevent tampering
- Code signing — Release builds are signed with a Developer ID certificate and notarized by Apple
- Hardened runtime — Enabled for Gatekeeper compatibility and notarization