On-device cough classification for iOS, powered by Core ML.
Coughylyzer is a native iOS application that records your cough, processes the audio signal using digital signal processing techniques, and runs a hybrid machine learning model entirely on-device to produce a Lung Score — a 0–100 metric reflecting respiratory health patterns. No data ever leaves the device.
This project was developed as a bachelor's thesis exploring the feasibility of acoustic cough analysis on mobile devices using on-device machine learning.
Disclaimer: Coughalyzer is a research prototype and is not a medical device. It cannot diagnose any condition and is not a substitute for professional medical advice.
- Features
- How It Works
- Architecture
- Project Structure
- Requirements
- Getting Started
- The Core ML Model
- Tech Stack
- License
- Fully on-device inference — zero network calls, zero data collection, complete privacy
- Hybrid ML model — combines a CNN operating on mel spectrograms with engineered peak features for classification
- Real-time FFT waveform — live audio visualization during recording using the Accelerate framework
- Automated recording sequence — guided 3-recording flow with animated transitions between each capture
- Lung Score (0–100) — composite health metric derived from model probabilities and confidence
- Score history & trends — persistent result tracking with an interactive Swift Charts trend line
- Custom design system — animated mesh gradients, glass morphism UI (iOS 26), arc gauge, custom typography
- Zero third-party dependencies — built entirely with Apple frameworks
The app captures 3 consecutive 3-second cough recordings at 16 kHz, mono, 16-bit linear PCM. A guided flow walks the user through medical disclaimers and positioning instructions before recording begins. Each recording is processed independently through the ML pipeline, and the results are averaged for a final prediction.
Each audio recording goes through a multi-stage signal processing and inference pipeline:
Audio (16kHz WAV)
│
├──► Mel Spectrogram Branch
│ ├── Extract highest-energy 1-second segment
│ ├── FFT with Hann windowing (n_fft=2048, hop=1024)
│ ├── Apply 128-bin mel filterbank (50 Hz – 8000 Hz)
│ ├── Convert to log scale (dB)
│ ├── Bilinear interpolation resize to 224×224
│ └── Output: MLMultiArray [1, 3, 224, 224]
│
├──► Peak Feature Branch
│ ├── Compute RMS energy envelope (512-sample window, 50% overlap)
│ ├── Threshold-based peak detection
│ ├── Extract: num_peaks, mean_height, mean_prominence, mean_width
│ ├── Z-score normalize using training statistics
│ └── Output: MLMultiArray [1, 4]
│
└──► Core ML Model (CoughylyzerModel)
├── Dual-input: spectrogram + peak features
├── Output: classLabel ("normal" / "abnormal")
└── Output: classLabel_probs (probability dictionary)
All DSP operations use Apple's Accelerate framework (vDSP) for hardware-optimized performance.
The Lung Score is a composite metric calculated from the averaged predictions across all 3 recordings:
lungScore = normalProbability × 60
+ (normalProbability − abnormalProbability) × 30
+ confidence × 10
The score is clamped to [0, 100] and maps to four categories:
| Score Range | Category |
|---|---|
| 80 – 100 | Excellent |
| 60 – 79 | Good |
| 40 – 59 | Fair |
| 0 – 39 | Needs Attention |
The app follows MVVM with a Router-based navigation pattern:
┌──────────────────────────────────────────────────┐
│ CoughylyzerApp (@main) │
│ RouterViewModifier (NavigationStack) │
├──────────────────────────────────────────────────┤
│ │
│ Onboarding ──► AppTabView │
│ ├── Results Tab │
│ │ ├── LungScoreCard │
│ │ ├── LungScoreChart │
│ │ └── ResultView │
│ └── Record Tab │
│ ├── RecordWarningView │
│ ├── RecordGuideView │
│ └── AnimatedRecordButton │
│ │
├──────────────────────────────────────────────────┤
│ RecordViewModel │
│ └── Recording orchestration & analysis │
├──────────────────────────────────────────────────┤
│ Helpers │
│ ├── AudioProcessorHelper (mel spectrogram) │
│ ├── PeakFeatureHelper (peak extraction) │
│ ├── CoughClassifierHelper (Core ML inference) │
│ └── CoughDataHelper (UserDefaults persistence)│
├──────────────────────────────────────────────────┤
│ Core ML: CoughylyzerModel.mlpackage │
└──────────────────────────────────────────────────┘
Coughylyzer/
├── CoughylyzerApp.swift # App entry point
├── ContentView.swift # Onboarding gate
│
├── Navigation/
│ ├── Router.swift # Observable router with NavigationPath
│ └── RouterViewModifier.swift # NavigationStack injection
│
├── Views/
│ ├── Onboarding/
│ │ └── OnboardingView.swift # Welcome screen
│ ├── AppTabView.swift # Tab bar (Results + Record)
│ ├── RecordView.swift # Recording screen
│ ├── RecordWarningView.swift # Medical disclaimers
│ ├── RecordGuideView.swift # Recording instructions
│ ├── ResultView.swift # Single result detail
│ └── ResultsView.swift # Score history & chart
│
├── ViewModels/
│ └── RecordViewModel.swift # Recording + analysis orchestration
│
├── Models/
│ ├── CoughRecording.swift # Recording metadata
│ ├── CoughPrediction.swift # Classification + lung score
│ ├── CoughAnalysisResult.swift # Composite result model
│ └── ML/
│ └── CoughylyzerModel.mlpackage # Core ML model (~46 MB)
│
├── Helpers/
│ ├── AudioProcessorHelper.swift # Mel spectrogram generation (FFT, filterbank)
│ ├── PeakFeatureHelper.swift # Peak feature extraction (energy, prominence)
│ ├── CoughClassifierHelper.swift # Core ML inference wrapper
│ └── CoughDataHelper.swift # UserDefaults persistence
│
├── Design System/
│ ├── Assets/
│ │ ├── Typography.swift # Type scale (SF Pro, 4 weights)
│ │ ├── Icons.swift # SF Symbol constants
│ │ ├── Fonts/ # Bundled SF Pro font files
│ │ ├── DesignSystemAssets.xcassets # Color palette
│ │ └── CoughylyzerAppIcon.icon # App icon (Icon Composer)
│ └── Views/
│ ├── AnimatedMeshGradient.swift # 3×3 animated mesh background
│ ├── AnimatedRecordButton.swift # Glass morphism button + live FFT waveform
│ ├── LungScoreCard.swift # Arc gauge with animated pointer
│ └── LungScoreChart.swift # Interactive Swift Charts trend line
│
├── Enums/
│ ├── RecordingUiStateType.swift # Recording state machine
│ ├── RecordingWaveformConstantsType.swift # FFT display constants
│ └── SheetPageType.swift # Sheet navigation
│
└── Resources/
├── CoughNormalizationConstants.swift # Z-score stats + audio config
├── LungScoreCardConstants.swift # Score categories & descriptions
├── RecordGuideConstants.swift # Guide step content
└── RecordWarningConstants.swift # Disclaimer content
| Requirement | Version |
|---|---|
| iOS | 26.0+ |
| Xcode | 26.0+ |
| Swift | 6.0 |
| Device | iPhone or iPad with microphone |
Note: The app uses iOS 26 APIs including
GlassEffectContainer,.glassEffect(),.buttonStyle(.glass), and.toolbarTitleDisplayMode(.inlineLarge). It will not compile on earlier SDK versions.
-
Clone the repository
git clone https://github.com/egemengunel/coughylyzer.git cd coughylyzer -
Open in Xcode
open Coughylyzer.xcodeproj
-
Select a target device — the app requires a physical device with a microphone for full functionality (the simulator cannot capture audio).
-
Build and run — no dependency installation required. The Core ML model is bundled in the repository.
CoughylyzerModel.mlpackage is a dual-input classifier trained to distinguish between normal and abnormal cough sounds:
| Property | Value |
|---|---|
| Input 1 | spectrogram — [1, 3, 224, 224] float32 |
| Input 2 | peak_features — [1, 4] float32 |
| Output | classLabel — "normal" or "abnormal" |
| Output | classLabel_probs — probability dictionary |
| Weights | ~46 MB |
| Compute Units | All (CPU, GPU, Neural Engine) |
The model was trained on a dataset of cough audio samples as part of the associated bachelor's thesis. The iOS app replicates the Python training pipeline's preprocessing (mel spectrogram generation and peak feature extraction with matching normalization statistics) to ensure inference consistency.
| Component | Technology |
|---|---|
| UI Framework | SwiftUI |
| ML Inference | Core ML |
| Audio Recording | AVFoundation (AVAudioRecorder) |
| Audio Monitoring | AVFoundation (AVAudioEngine) |
| Signal Processing | Accelerate (vDSP) |
| Charts | Swift Charts |
| Concurrency | Swift 6 structured concurrency, @Observable |
| Persistence | UserDefaults (JSON-encoded) |
| Dependencies | None |
This project is open source.
Built with SwiftUI and Core ML by Egemen Günel