GearSync is a high-performance, local-first native Android application designed to assist manual transmission drivers. By analyzing real-time acoustic signatures and chassis vibrations, GearSync dynamically identifies optimal shifting thresholds and provides low-latency visual feedback—completely independent of an OBD-II interface or cloud connectivity.
Note (v2): The procedural audio cue output has been deprecated. Earlier builds synthesized a high-frequency blip through a second Oboe output stream, which competed with the microphone-capture DSP pipeline for the low-latency audio path. The system is now visual-only: a hardware-accelerated VU meter conveys current gear, optimal-shift zone, and shift events without any audio output. See Shift Decision Logic and Current Limitations.
The core digital signal processing (DSP) pipeline and machine learning calibration engines are implemented in native C++ via the Android NDK to bypass virtual machine overhead and ensure sub-millisecond execution.
- Acoustic & Vibration Tachometer: Utilizes hardware microphone PCM data and high-frequency linear accelerometer tracking to extract engine firing frequencies without modifying vehicle electronics.
- Glanceable Visual Interface: A hardware-accelerated, custom VU meter UI that conveys current gear, the optimal-shift zone, calibration confidence, and shift events with zero audio distraction.
- Edge ML Automated Calibration: Integrates 1-D K-Means clustering seeded from a configurable per-vehicle profile to adapt to any vehicle's gear ratios and mechanical signature.
- Configurable Vehicle Profile: A JSON asset (
assets/vehicle_config.json) defines transmission ratios, final drive, tire circumference, and tolerance bands — ships pre-tuned for the Toyota Wigo 1.0 E M/T but editable for any vehicle without recompiling. - Fragmented Session Stitching: Built-in state persistence utilizing Welford's Online Algorithm to allow drivers to complete calibration across multiple, non-contiguous driving sessions.
GearSync leverages a clear separation of concerns between high-level Android operating system interactions and low-level mathematical processing threads:
[Android System]
│ (1 Hz GPS Speed Updates)
▼
[JNI Bridge Surface]
│
▼
[Native C++ Core] ◄──── [Oboe Input Stream] (Raw Audio PCM, mic)
│ ◄──── [ASensorManager] (Linear Acceleration, 100 Hz)
├─► [FFT / Spectral Analysis Layer] (DSP worker thread)
├─► [Edge ML Calibration Engine] (Welford + K-Means)
└─► [Shift Decision Logic] ──► [VU Meter UI] (60 FPS Canvas)
The DSP runs on a dedicated worker thread fed by a lock-free single-producer/single-consumer (SPSC) snapshot handoff from the real-time audio input callback. The audio callback itself is wait-free (no locks, no allocation), and there is no audio output stream — the microphone path owns the low-latency audio resource exclusively.
- Kotlin Layer (
/app/src/main/java): Handles lifecycle management, maps the UI Canvas pipeline, hosts the structural Foreground Service to prevent background execution suspension, and feeds GPS metrics downstream. - Native NDK Layer (
/app/src/main/cpp): Manages lock-free ring buffers, implements real-time Fourier transforms, calculates running statistical variances, classifies the current gear, and derives the shift recommendation.
This is the heart of the system: how GearSync "understands" whether to recommend an upshift, a downshift, or holding the current gear. The logic is purely inferential — there is no OBD-II tachometer feed. Everything is reconstructed from two cheap signals: the microphone and the GPS.
The audio input stream captures mono PCM at 48 kHz. Every time FFT_SIZE (4096) fresh samples are available, the input callback hands a windowed snapshot to the DSP worker thread. The worker applies a Hamming window and a radix-2 Cooley–Tukey FFT, then searches the 20–250 Hz band (the plausible firing-frequency range for a passenger engine) for the dominant spectral peak. That peak frequency f is the engine's fundamental firing frequency — a proxy for RPM:
where P is the engine's cylinder count (3 for the Wigo's 1KR-FE).
When the clutch is fully engaged, engine frequency and road speed are rigidly proportional within a single gear:
The worker reads the latest GPS speed v (m/s, 1 Hz). If v ≥ MIN_SPEED_MPS (1 m/s, to reject GPS jitter at standstill), it computes the instantaneous ratio r = f / v. This ratio r is the single observable that uniquely identifies which gear the car is in — it is (nearly) constant within a gear and steps to a different value after every shift.
Each gear g has a theoretical slope derived directly from the vehicle profile:
These theoretical k_g values are loaded from assets/vehicle_config.json at startup and seed the K-Means centroids, so the app classifies gears correctly from the very first drive. As real (f, v) observations accumulate, 1-D K-Means (5 centroids, farthest-first init) refines those centroids to match the actual car — absorbing tire wear, load, and pressure drift over time.
classifyGear(r) finds the nearest centroid to the live ratio r. Critically, it then applies an asymmetric tolerance band before accepting the match:
For the Wigo this band is [0.98, 1.025] (−2% / +2.5%). The asymmetry is deliberate — it accommodates the real-world physics of this specific lightweight hatchback:
| Effect | Direction | Magnitude |
|---|---|---|
| Tire wear (8 mm → 1.6 mm tread) | k_g drifts up |
up to +2.2% |
| Under-inflation (32 → 24 PSI) | k_g shifts |
−0.5% to −1.5% |
| Passenger/cargo load (+300 kg) | flattens rolling radius, k_g up |
small |
| Centrifugal tire expansion (highway) | k_g down |
fraction of a percent |
If r falls outside the band for every gear (e.g., mid-shift, clutch slip, or a bad FFT peak), classification returns −1 / unknown rather than snapping to a spurious gear. This is what keeps the display from flickering between gears during a shift.
Once the gear is known, the worker maps r to a needle position in [0, 1] within that gear's band:
Because gears are sorted descending (k_1 > k_2 > … > k_5), a higher r means the engine is revving high relative to where the next gear would put it. The VU meter renders this as three zones:
| Needle zone | Meaning | Recommendation |
|---|---|---|
| 0–33% (blue, "lugging") | RPM low for this gear | Downshift — engine is below its efficient band |
| 33–66% (green, "optimal") | RPM in the sweet spot | Hold the current gear |
| 66–100% (red, "redline") | RPM high, near the next gear's entry point | Upshift — you're over-revving |
In short: lugging → downshift, optimal → hold, redline → upshift. The needle drifts smoothly (exponential moving average, α = 0.18) so it never jitters, and resets to the lug end when the gear is unknown.
Two guards prevent the model from being poisoned by noisy data:
- GPS stability window. GPS updates at 1 Hz while the acoustic pipeline runs at ~12 Hz, so during acceleration/braking the two signals are temporally misaligned and
ris meaningless. The engine only feeds a ratio into Welford/K-Means after GPS speed has been stable (Δ ≤speedJitterThresholdMps, default 0.5 m/s) forsteadyStateWindowSecondsconsecutive samples (default 4 s of steady-state cruising). - Welford confidence. The running variance
σ² = M₂/(n−1)is exposed as a confidence score1/(1+σ²); the VU meter dims its zones until confidence rises, signalling that calibration is still converging.
Treating k_g as a rigid constant fails in practice because the rolling circumference is not fixed — it shrinks ~2.2% over a tire's life, sags 0.5–1.5% with low pressure, flattens further under load, and expands slightly at highway speed. The asymmetric band (−2% / +2.5%) and the 4-second steady-state lock window are sized to swallow all of these compounding drifts without ever mistaking one gear for its neighbour. All four numbers live in vehicle_config.json and can be widened or tightened per vehicle.
When the vehicle's clutch is fully engaged, the relationship between the engine's fundamental frequency (
Where
For a standard four-stroke internal combustion engine, the operational revolutions per minute (RPM) is evaluated from the dominant spectral peak frequency (
Where
To stitch fragmented calibration runs without logging thousands of raw historical coordinates, the application updates its structural confidence boundaries using Welford's method for calculating running variances:
The tracking variance
- Android Studio Hedgehog (2023.3.1) or newer
- Android NDK (Version 26.x or newer)
- CMake 3.22.1+
- A physical Android device running API Level 26 (Android 8.0) or higher (Sensors and mic access are limited within virtual emulators).
- Clone the repository down to your local developer workspace:
git clone https://github.com/your-repo/GearSync.git
cd GearSync
- Open the project in Android Studio. The IDE will automatically sync the build files and recognize the native configuration paths designated inside
CMakeLists.txt. - Ensure your local configuration points directly to your installed NDK path within your local properties file:
ndk.dir=/path/to/android-sdk/ndk/26.x.x
- Compile and deploy the debug APK target onto your hardware testing device.
GearSync ships pre-configured for the Toyota Wigo 1.0 E M/T and requires no manual per-gear calibration to start working — the theoretical k_g seeds get it classifying gears on the first drive, and K-Means refines them automatically as you cruise.
All vehicle-specific parameters live in app/src/main/assets/vehicle_config.json. No code changes or recompilation of the native layer are needed to retune them:
{
"engine": { "cylinders": 3, "strokeCycle": 4 },
"transmission": { "gears": 5, "ratios": [3.545, 1.904, 1.233, 0.906, 0.738], "finalDriveRatio": 4.312 },
"tire": { "spec": "155/80 R13", "nominalCircumferenceMeters": 1.816 },
"calibration": {
"ratioToleranceLow": 0.98, "ratioToleranceHigh": 1.025,
"steadyStateWindowSeconds": 4, "speedJitterThresholdMps": 0.5
}
}| Field | Effect on the algorithm |
|---|---|
cylinders / strokeCycle |
Sets the firing factor that converts firing frequency ↔ RPM |
ratios / finalDriveRatio |
Seed the theoretical k_g centroids (one per gear) |
nominalCircumferenceMeters |
Scales k_g; the dominant source of real-world drift |
ratioToleranceLow/High |
Width of the accept band around each centroid |
steadyStateWindowSeconds |
Seconds of stable cruising before a ratio is learned |
speedJitterThresholdMps |
How much GPS speed may vary and still count as "stable" |
On startup VehicleConfig.kt loads this JSON, computes the k_g seeds, and pushes them to the native engine via NativeEngine.setVehicleConfig(...). If the file is missing or malformed, the engine falls back to open tolerances and learns purely from K-Means.
The shift-decision approach is deliberately lightweight (mic + GPS only, no OBD-II). That buys broad compatibility at the cost of several known constraints:
- GPS speed is the weakest link. Consumer GPS updates at ~1 Hz with latency and accuracy that degrade in tunnels, urban canyons, and parking structures. Because the gear observable is
f / v, any error invpropagates directly into the ratio. The 4-second steady-state lock mitigates but does not eliminate this. - Temporal misalignment during transients. Acoustic frames arrive ~12×/s but GPS only 1×/s. During hard acceleration or braking the two are out of sync, so ratios are only trusted during steady cruising — meaning the system effectively cannot recommend a shift in the middle of aggressive driving, which is exactly when shift timing matters most.
- Single-peak FFT is fragile.
findDominantHztakes the single loudest bin in the 20–250 Hz band. Road noise, exhaust resonance, HVAC fans, music, or a passenger talking can momentarily outweigh the true firing peak, producing a spurious ratio. There is currently no harmonic-product-spectrum or peak-tracking confirmation. - No explicit shift-event detection in the model. The accelerometer spike only drives a visual flash; it is not fed into the classifier to anticipate a gear change. Gear changes are inferred purely from the ratio jumping bands after the fact.
- Clutch / neutral states are invisible. When the clutch is depressed (or in neutral),
fandvdecouple and thef = k_g·vmodel is invalid. The tolerance gate rejects these as "unknown," but the system does not affirmatively detect a disengaged clutch. - Frequency resolution is coarse at idle. A 4096-point FFT at 48 kHz yields ~11.7 Hz/bin. Near the 20 Hz low end, a one-bin error is a large fractional RPM error, so low-idle gears are the noisiest to classify.
- K-Means can converge degenerately. With only 5 centroids over a narrow 1-D range, an unlucky data distribution (e.g., mostly highway driving in 5th) can collapse centroids; there is no per-cluster occupancy floor enforced yet.
- Single fixed sample rate. The pipeline assumes 48 kHz; devices that negotiate a different rate are not yet handled, and aliasing/scaling math is hardcoded to that assumption.
- Sensor fusion for speed. Blend GPS with wheel-speed-derived accelerometer integration (or device IMU dead-reckoning) to get a higher-rate, lower-latency
vestimate and unlock shift recommendations during transients. - Robust pitch detection. Replace the single-bin peak with a Harmonic Product Spectrum or YIN-style estimator plus frame-to-frame peak tracking to reject transient noise and lock onto the true fundamental.
- Predictive shift timing. Feed the accelerometer and the needle's velocity (dNeedle/dt) into a small state machine so the UI can recommend "upshift now" slightly ahead of redline rather than reporting it after the fact.
- Explicit clutch/neutral detection. Use the sudden decoupling of
fandv(frequency rises while speed holds, or vice-versa) to detect disengagement and freeze classification cleanly. - Adaptive tolerance. Let the accept band tighten automatically as Welford confidence grows, instead of using static
tolLow/tolHighfor the whole vehicle lifecycle. - Per-cluster validation. Add an occupancy floor and silhouette/separation check to K-Means so degenerate centroid collapse is detected and the affected gear falls back to its theoretical seed.
- Automatic gain control on input. Normalize mic levels across devices and cabin environments to stabilize the FFT peak magnitude.
- Settings UI for the vehicle profile. Surface
vehicle_config.jsonfields in-app (currently edit-the-asset) with validation, presets per make/model, and a live preview of the seededk_gvalues.
This project is licensed under the MIT License - see the LICENSE file for details.