Comlink is a high-fidelity, peer-to-peer (P2P) voice intercom application designed for loud environments such as concerts, clubs, and festivals. The app enables offline voice communication using Apple's Multipeer Connectivity Framework over Bluetooth and Wi-Fi Direct, without requiring an internet connection.
- Offline-First Architecture: Functions entirely in Airplane Mode (with Wi-Fi/Bluetooth enabled)
- Background Audio Support: Continues transmitting and receiving audio when the phone is locked
- Noise Isolation: Leverages iOS built-in voice processing to filter background noise and isolate the user's voice
- Ultra-Low Latency: Optimized audio pipeline for real-time communication
- OLED-Friendly UI: Dark/black theme optimized for low-light concert environments
- Language: Swift 6.0+
- UI Framework: SwiftUI
- Architecture: MVVM (Model-View-ViewModel)
- Networking: Apple Multipeer Connectivity Framework
- Audio: AVFoundation & AVAudioEngine
- Target iOS: iOS 16.0+
┌─────────────────────────────────────────────────────────────────┐
│ SwiftUI Views │
│ ┌──────────────────┐ ┌──────────────────┐ ┌───────────────┐ │
│ │ ConnectionView │ │ TalkView │ │ SettingsView │ │
│ └────────┬─────────┘ └────────┬─────────┘ └───────┬───────┘ │
└───────────┼────────────────────┼────────────────────┼──────────┘
│ │ │
└────────────────────┼────────────────────┘
│
┌────────────▼────────────┐
│ ComlinkViewModel │
│ (MVVM Coordinator) │
└────┬──────────────┬─────┘
│ │
┌────────────▼────┐ ┌───▼──────────────┐
│ MultipeerManager│ │ AudioManager │
│ (P2P Networking)│ │ (Audio Pipeline) │
└────┬─────────────┘ └──┬───────────────┘
│ │
┌───────────▼──────────┐ ┌──────▼────────────────┐
│ MCNearbyServiceBrowser│ │ AVAudioEngine │
│ MCNearbyServiceAdv. │ │ - Input Node │
│ MCSession │ │ - Audio Tap │
└───────────┬───────────┘ │ - Output Node │
│ │ - Voice Processing │
│ └──────┬───────────────┘
│ │
└──────────┬──────────┘
│
┌───────────▼────────────┐
│ Data Flow Pipeline │
│ │
│ Mic → Tap → Buffer → │
│ → Network → Peer → │
│ → Speaker │
└────────────────────────┘
- Audio Capture: Microphone → AVAudioInputNode → Audio Tap
- Processing: Audio Tap → Voice Isolation → PCM Buffer
- Transmission: PCM Buffer → MultipeerManager → MCSession → Peer
- Reception: Peer → MCSession → MultipeerManager → Audio Buffer
- Playback: Audio Buffer → AVAudioPlayerNode → AVAudioOutputNode → Speaker
Goal: Configure Xcode project with required capabilities and permissions.
-
Create Xcode Project
- New iOS App with SwiftUI
- Minimum Deployment Target: iOS 16.0
- Enable Swift 6.0 language mode
-
Configure Info.plist
<key>NSMicrophoneUsageDescription</key> <string>Comlink needs microphone access to transmit your voice to connected peers.</string> <key>NSLocalNetworkUsageDescription</key> <string>Comlink uses local network to discover and connect to nearby devices.</string> <key>NSBonjourServices</key> <array> <string>_comlink._tcp</string> <string>_comlink._udp</string> </array> <key>UIBackgroundModes</key> <array> <string>audio</string> <string>voip</string> </array>
-
Configure Capabilities
- Enable "Audio, AirPlay, and Picture in Picture"
- Enable "Background Modes" → audio, voip
- Consider enabling "Network Extensions" if needed
-
Project Structure
Comlink/ ├── App/ │ ├── ComlinkApp.swift │ └── AppDelegate.swift (for background audio) ├── Models/ │ ├── Peer.swift │ └── AudioPacket.swift ├── ViewModels/ │ └── ComlinkViewModel.swift ├── Views/ │ ├── ConnectionView.swift │ ├── TalkView.swift │ └── Components/ ├── Managers/ │ ├── MultipeerManager.swift │ ├── AudioManager.swift │ └── PermissionsManager.swift ├── Utilities/ │ ├── AudioCodec.swift │ └── Logger.swift └── Resources/ └── Assets.xcassets
Deliverables:
- ✅ Xcode project configured
- ✅ Info.plist with all required permissions
- ✅ Directory structure established
Goal: Implement P2P discovery and connection logic using MultipeerConnectivity.
-
Create MultipeerManager Class
- Singleton pattern with
@Observablefor SwiftUI integration - Properties:
peerID: MCPeerIDsession: MCSessionserviceAdvertiser: MCNearbyServiceAdvertiserserviceBrowser: MCNearbyServiceBrowser@Published var connectedPeers: [MCPeerID]@Published var availablePeers: [MCPeerID]
- Singleton pattern with
-
Service Discovery Configuration
- Service Type:
"comlink"(max 15 characters, lowercase, alphanumeric) - Discovery Info: Include device name, app version
- Security: Implement custom invitation handler (accept/decline)
- Service Type:
-
Session Management
- Implement
MCSessionDelegatemethods:session(_:peer:didChange:)→ Update connection statesession(_:didReceive:fromPeer:)→ Handle audio datasession(_:didReceive:withName:fromPeer:)→ Handle streams (future)
- Implement
-
Connection Flow
Device A (Host) Device B (Client) ───────────────── ───────────────── startAdvertising() startBrowsing() │ │ │◄────Discovery────────────┤ │ │ │────Invitation Request────► │ │ │◄───Accept/Decline────────┤ │ │ Connected ◄──────────────► Connected -
Data Transmission Methods
sendAudioData(_ data: Data, to peer: MCPeerID)→ Use.reliableor.unreliablemode- Decision: Use
.unreliablefor lower latency, handle packet loss gracefully
Deliverables:
- ✅ MultipeerManager class with discovery/advertising
- ✅ Peer connection and disconnection handling
- ✅ Data transmission infrastructure
Goal: Configure AVAudioEngine to capture microphone input, process it, and send to network.
-
Create AudioManager Class
@Observableclass with AVAudioEngine lifecycle management- Properties:
private let audioEngine: AVAudioEngineprivate let inputNode: AVAudioInputNodeprivate let audioSession: AVAudioSession@Published var isRecording: Bool@Published var audioLevel: Float(for UI meter)
-
Configure AVAudioSession
let session = AVAudioSession.sharedInstance() try session.setCategory(.playAndRecord, mode: .voiceChat, options: [ .defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP ]) try session.setActive(true, options: .notifyOthersOnDeactivation)
Why
.voiceChatmode?- Enables built-in echo cancellation
- Enables voice isolation (filters background noise)
- Optimizes for low-latency duplex communication
-
Install Audio Tap on Input Node
let format = inputNode.outputFormat(forBus: 0) inputNode.installTap(onBus: 0, bufferSize: 4096, format: format) { [weak self] buffer, time in self?.processAudioBuffer(buffer) }
Buffer Size Selection:
- 4096 frames = ~85ms latency at 48kHz (acceptable for voice)
- Smaller buffer = lower latency but higher CPU usage
- Larger buffer = smoother but increased delay
-
Process Audio Buffer
func processAudioBuffer(_ buffer: AVAudioPCMBuffer) { guard let channelData = buffer.floatChannelData else { return } // Convert PCM to Data let audioData = Data(bytes: channelData[0], count: Int(buffer.frameLength) * MemoryLayout<Float>.size) // Optional: Compress with Opus codec (future enhancement) // Send to connected peers multipeerManager.sendAudioData(audioData) }
-
Start/Stop Audio Engine
startRecording()→ Prepare engine, start engine, activate sessionstopRecording()→ Stop engine, remove taps, deactivate session- Handle interruptions (phone calls, alarms)
Deliverables:
- ✅ AudioManager with AVAudioEngine configuration
- ✅ Audio tap installed on input node
- ✅ Audio buffer processing and transmission
Goal: Receive audio data from peers and play it through the speaker.
-
Receive Audio Data in MultipeerManager
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { // Route to AudioManager for playback audioManager.playReceivedAudio(data, from: peerID) }
-
Create Audio Playback Pipeline
- Use
AVAudioPlayerNodefor real-time playback - Attach player node to audio engine
- Connect player node to output node (speaker)
- Use
-
Convert Data to AVAudioPCMBuffer
func playReceivedAudio(_ data: Data, from peer: MCPeerID) { guard let buffer = createPCMBuffer(from: data) else { return } playerNode.scheduleBuffer(buffer) { // Buffer finished playing } if !playerNode.isPlaying { playerNode.play() } }
-
Handle Buffer Queue
- Implement jitter buffer to handle network variability
- Drop packets if queue exceeds threshold (prevent accumulating delay)
- Smooth playback with interpolation if needed
-
Prevent Feedback Loop
- Ensure echo cancellation is working (
.voiceChatmode) - Test with two physical devices (NOT simulator)
- Consider muting local input during playback (optional)
- Ensure echo cancellation is working (
Deliverables:
- ✅ Audio reception and playback pipeline
- ✅ AVAudioPlayerNode integration
- ✅ Jitter buffer implementation
- ✅ Echo cancellation verification
Goal: Create intuitive, dark-themed UI for connection and communication.
-
ConnectionView (Initial Screen)
- Display list of discovered peers
- Show connection status (Searching, Found, Connected)
- Allow user to select a peer to connect
- Display user's own device name
- "Start Broadcasting" / "Stop Broadcasting" toggle
-
TalkView (Active Communication Screen)
- Large "Push to Talk" button (or toggle for continuous transmission)
- Audio level meter (visual feedback)
- Connected peer name/status
- "Disconnect" button
- Battery-saving dark background
-
SwiftUI Integration with ViewModel
@Observable class ComlinkViewModel { let multipeerManager: MultipeerManager let audioManager: AudioManager var isConnected: Bool { !multipeerManager.connectedPeers.isEmpty } var availablePeers: [MCPeerID] { multipeerManager.availablePeers } func connect(to peer: MCPeerID) { ... } func disconnect() { ... } func startTalking() { audioManager.startRecording() } func stopTalking() { audioManager.stopRecording() } }
-
UI Design Principles
- OLED Black: Use
Color.blackfor backgrounds (true black = pixels off) - High Contrast: White/green text on black background
- Large Touch Targets: Minimum 44x44pt for buttons
- Haptic Feedback: Use
UIImpactFeedbackGeneratorfor interactions
- OLED Black: Use
-
Handle Background State
- Display notification when app enters background
- Continue audio transmission (background mode enabled)
- Lock screen controls (MPRemoteCommandCenter - optional)
Deliverables:
- ✅ ConnectionView with peer discovery UI
- ✅ TalkView with push-to-talk functionality
- ✅ Dark theme optimized for OLED
- ✅ ViewModel coordinating managers
Description: When two devices are physically close, audio from the speaker can be picked up by the microphone, creating a feedback loop.
Mitigation:
- ✅ Use
.voiceChatmode for built-in echo cancellation - ✅ Test with headphones/earbuds (recommended use case)
- ✅ Implement AGC (Automatic Gain Control) if needed
- ✅ Consider adding "mute speaker during talk" option
Description: iOS may suspend the app in background to save battery, interrupting audio transmission.
Mitigation:
- ✅ Enable
UIBackgroundModes: audioin Info.plist - ✅ Keep AVAudioSession active with
.playAndRecordcategory - ✅ Use
beginBackgroundTaskfor critical operations - ✅ Test extensively with device locked and app in background
- ✅ Monitor
AVAudioSession.interruptionNotificationand resume session
Description: MCSession can be unstable, especially with unreliable data mode. Packets may be lost or arrive out of order.
Mitigation:
- ✅ Use
.unreliablemode for low latency (accept some packet loss) - ✅ Implement sequence numbers in audio packets for ordering
- ✅ Add jitter buffer to smooth playback
- ✅ Gracefully handle missing packets (interpolate or skip)
- ✅ Fallback to
.reliablemode if latency is acceptable
Description: Users may deny microphone or local network permissions, breaking core functionality.
Mitigation:
- ✅ Create
PermissionsManagerto check and request permissions upfront - ✅ Show educational alert explaining why permissions are needed
- ✅ Provide deep link to Settings if permission is denied
- ✅ Gracefully degrade (disable features) if permissions unavailable
Description: Bluetooth/Wi-Fi performance may degrade in crowded environments (concerts) due to interference.
Mitigation:
- ✅ Prefer Wi-Fi Direct over Bluetooth when available
- ✅ Use smallest feasible buffer size (balance latency vs stability)
- ✅ Implement adaptive bitrate (reduce quality if connection degrades)
- ✅ Display connection quality indicator in UI
- ✅ Consider Opus codec for better compression (future)
Description: Continuous audio processing and transmission will drain battery quickly.
Mitigation:
- ✅ Optimize audio pipeline (avoid unnecessary processing)
- ✅ Use efficient data formats (compressed audio)
- ✅ Provide "Low Power Mode" option (lower sample rate)
- ✅ Display battery usage warning in UI
- ✅ Allow user to close connection when not needed
Description: Audio data transmitted over local network could be intercepted or eavesdropped.
Mitigation:
- ✅ Use MCSession's built-in encryption (enabled by default)
- ✅ Implement peer verification (confirm identity before accepting)
- ✅ Add optional passcode/PIN for pairing
- ✅ Display warning about secure environment usage
- ✅ Future: Implement end-to-end encryption with custom keys
- Swift 6 Concurrency: Use
async/awaitand@MainActorwhere appropriate - Error Handling: Comprehensive
do-catchblocks, never force-unwrap in production - Logging: Use
OSLogfor debugging audio and network events - Testing: Unit tests for AudioManager and MultipeerManager logic
- Code Review: All phases reviewed for performance and security
- Test with two physical devices (iPhone required, simulator insufficient)
- Test in Airplane Mode with Wi-Fi/BT enabled
- Test with app in background and device locked
- Test in noisy environment (play loud music)
- Test with Bluetooth headphones connected
- Test battery usage over 30-minute session
- Test permission denial scenarios
- Test connection/disconnection edge cases
- Latency: < 200ms end-to-end (audio input → transmission → playback)
- Packet Loss Tolerance: < 5% packet loss without noticeable degradation
- Battery Life: > 2 hours of continuous use at 50% brightness
- Discovery Time: < 5 seconds to find nearby peer
- Opus Codec Integration: Replace PCM with Opus for 10x better compression
- Multi-Peer Support: Allow 3+ people in a group chat
- Noise Gate: Automatically mute when below threshold (save bandwidth)
- Voice Effects: Optional filters (reverb, pitch shift) for fun
- Message History: Brief text messages alongside voice
- Spatial Audio: Use device orientation for 3D positioning
- Adaptive Bitrate: Dynamically adjust quality based on connection
- Custom Transport Protocol: Replace MCSession with lower-level UDP if needed
- Machine Learning Noise Reduction: Core ML model for superior filtering
- Battery Optimization: Dynamic sample rate adjustment
git clone <repo-url>
cd comlink-ios
open Comlink.xcodeproj- Select physical iOS device (NOT simulator - audio features require hardware)
- Cmd+R to build and run
- Grant microphone and local network permissions
- Repeat on second device for testing
- Device A: Tap "Start Broadcasting"
- Device B: Tap "Find Peers" → Select Device A
- Device A: Accept connection request
- Both devices: Test voice transmission
- Swift Files: PascalCase (e.g.,
MultipeerManager.swift) - Models: Singular nouns (e.g.,
Peer.swift, notPeers.swift) - Views: Descriptive + "View" suffix (e.g.,
ConnectionView.swift) - ViewModels: Same as View + "ViewModel" (e.g.,
ComlinkViewModel.swift) - Managers: Descriptive + "Manager" suffix (e.g.,
AudioManager.swift)
Follow conventional commits:
feat:New feature (e.g.,feat: implement MultipeerManager peer discovery)fix:Bug fix (e.g.,fix: resolve audio feedback loop)refactor:Code restructuring (e.g.,refactor: extract audio processing into utility)docs:Documentation (e.g.,docs: update AGENT.md with Phase 3 details)test:Add tests (e.g.,test: add unit tests for AudioManager)chore:Maintenance (e.g.,chore: update Xcode project settings)
This project uses only Apple frameworks to minimize complexity and binary size.
- Opus-iOS: Opus codec bindings (if AVAudioEngine compression insufficient)
- CocoaAsyncSocket: Alternative to MCSession for custom networking (if needed)
- Realm/SwiftData: For message history persistence
Decision: Start with zero dependencies, add only if native frameworks are insufficient.
- Audio buffers are ephemeral (not stored to disk)
- No telemetry or analytics (fully offline)
- User data never leaves device except during active P2P session
- MCSession uses TLS-like encryption by default
- Peer identity verified via device name (user confirmation required)
- Future: Add optional passcode pairing
- Request microphone access only when needed
- Local network usage limited to Bonjour service type
- No location, camera, or contacts access required
✅ Two devices can discover each other offline ✅ Audio transmitted with < 200ms latency ✅ Voice isolation filters background noise ✅ App continues working when device is locked ✅ Clean, dark UI suitable for concerts ✅ Stable connection for > 10 minutes without drops
✅ All MVP features + tested by 10 users ✅ Battery life > 2 hours ✅ No critical bugs in 1-week testing period ✅ App Store compliance (privacy policy, metadata)
Project Lead: Senior iOS Engineer (AI-Assisted Development)
Issues: Use GitHub Issues for bug reports and feature requests
Documentation: This file (AGENT.md) is the source of truth
Important: Always refer to this document before making architectural decisions.
| Version | Date | Changes |
|---|---|---|
| 1.0.0 | 2025-12-11 | Initial architecture document created |
Next Step for AI Agent: Proceed to Phase 1 implementation after confirming this plan with the user.