Skip to content

Commit 6cf0e0a

Browse files
committed
feat: NoiseGuard v1.0.2
1 parent 50f3244 commit 6cf0e0a

78 files changed

Lines changed: 3899 additions & 516 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,10 @@ captures/
8686

8787
# virtual machine crash logs
8888
hs_err_pid*
89-
replay_pid*
89+
replay_pid*
90+
91+
# Room schema exports should be committed for migration tracking
92+
!app/schemas/
93+
94+
# Claude
95+
.claude

CHANGELOG.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
5+
6+
## [Unreleased]
7+
8+
## [1.0.0] - 2026-05-19
9+
10+
### Core
11+
- Real-time dB SPL monitoring via `AudioRecord` (RMS formula, 20–120 dB range)
12+
- 4 noise categories: QUIET · MODERATE · LOUD · HARMFUL
13+
- Calibrated dB calculation with 80 dB offset (RMS formula, verified in AudioAnalyzerDecibelsTest)
14+
15+
### UI
16+
- Circular gauge with 270° cyan→magenta neon arc and peak marker
17+
- LED-style digital readout with glow effect
18+
- Neon dark theme (`#0A0E1A` base, fixed palette)
19+
- 3 screens: Monitor, History, Settings
20+
21+
### Data
22+
- Room database with timestamp-indexed `NoiseLevelEntity`
23+
- DataStore for scalar preferences (threshold, toggles, sampling rate)
24+
- 30-day auto-cleanup via scheduled timestamp query
25+
- Hilt (2.56.2) for dependency injection — enforces singleton `NoiseRepository` across all ViewModels
26+
27+
### Alerts
28+
- Push notifications with 30-second cooldown
29+
- Vibration alert with independent toggle
30+
- 3-state `RECORD_AUDIO` permission handling (granted → rationale → denied)
31+
- `POST_NOTIFICATIONS` requested contextually on API 33+
32+
33+
34+
### License
35+
- Released under Business Source License 1.1 (BUSL-1.1)
36+
- Free to study and use personally, commercial use prohibited
37+
- Converts to MIT on 2030-01-01

LICENSE.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Business Source License 1.1
2+
3+
Licensor: Marco Domingues
4+
Licensed Work: NoiseGuard
5+
Change Date: 2030-01-01
6+
Change License: MIT
7+
8+
Use Limitation:
9+
The Licensed Work may not be used for Commercial Purposes.
10+
"Commercial Purposes" means any use intended for or directed
11+
toward commercial advantage or monetary compensation, including
12+
but not limited to:
13+
- Publishing on any app store (Google Play, Apple App Store, etc.)
14+
- Using in a commercial product or service
15+
- Selling or licensing the work
16+
17+
You are free to:
18+
- View, study, and learn from the code
19+
- Fork and modify for personal, non-commercial use
20+
- Share with attribution
21+
22+
On the Change Date (2030-01-01), this license converts
23+
automatically to MIT, allowing all uses.
24+
25+
Full license text: https://mariadb.com/bsl11/
26+
27+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
28+
Copyright (c) 2026 Marco Domingues

README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# NoiseGuard
2+
3+
Open offices run at 65–72 dB. A busy café hits 75. Prolonged exposure above 70 dB causes hearing fatigue, and most people have no idea how loud their environment actually is. NoiseGuard measures it.
4+
5+
![API](https://img.shields.io/badge/API-24%2B-brightgreen?style=for-the-badge)
6+
![Kotlin](https://img.shields.io/badge/Kotlin-2.2.10-purple?style=for-the-badge&logo=kotlin&logoColor=white)
7+
![Compose BOM](https://img.shields.io/badge/Compose_BOM-2026.02.01-blue?style=for-the-badge&logo=jetpackcompose&logoColor=white)
8+
![License](https://img.shields.io/badge/license-BUSL--1.1-red?style=for-the-badge)
9+
10+
<p align="center">
11+
<img src="docs/screenshots/monitor.png" width="32%" alt="Monitor screen showing live dB readout and circular gauge" />
12+
<img src="docs/screenshots/history.png" width="32%" alt="History screen with 24-hour line chart and time-of-day breakdown" />
13+
<img src="docs/screenshots/settings.png" width="32%" alt="Settings screen with alert threshold and vibration controls" /><br/>
14+
<em>Monitor · History · Settings</em>
15+
</p>
16+
17+
---
18+
19+
## What it does
20+
21+
NoiseGuard reads raw PCM audio via `AudioRecord`, runs RMS amplitude calculation on each buffer, and maps the result to calibrated dB SPL. No recordings stored, no cloud, no account. Just a number and a 24-hour history you can actually query.
22+
23+
It's a portfolio app built to demonstrate modern Android development - Compose, Clean Architecture, Room, coroutines with real dispatcher discipline. The use case happens to be real.
24+
25+
---
26+
27+
## Features
28+
29+
**Monitoring** - Calibrated dB SPL (RMS + 80 dB offset, clamped 20–120 dB) · 4 noise categories (QUIET / MODERATE / LOUD / HARMFUL) · 270° circular gauge with cyan→magenta arc · LED-style readout with glow effect
30+
31+
**History & Analytics** - 24-hour Vico line chart downsampled to 50 points · Morning / Afternoon / Night breakdowns with average dB, peak dB, and peak time · Room-backed, nothing simulated
32+
33+
**Smart Alerts** - Configurable threshold (50–120 dB) persisted to DataStore · 30-second cooldown (one alert per noise event, not per reading) · Vibration toggle · `POST_NOTIFICATIONS` requested contextually on API 33+
34+
35+
---
36+
37+
## Architecture
38+
39+
```
40+
┌─────────────────────────────────────────────────┐
41+
│ UI: MonitorScreen · HistoryScreen · Settings │
42+
│ MonitorViewModel · HistoryViewModel │
43+
│ SettingsViewModel │
44+
├─────────────────────────────────────────────────┤
45+
│ Domain: NoiseLevel · NoiseCategory │
46+
│ NoiseRepository (interface) │
47+
├─────────────────────────────────────────────────┤
48+
│ Data: AudioAnalyzer · NoiseRepositoryImpl │
49+
│ NoiseGuardDatabase · UserPreferences │
50+
│ NotificationHelper │
51+
└─────────────────────────────────────────────────┘
52+
```
53+
54+
`NoiseRepository` is a pure Kotlin interface - domain layer has zero Android imports. ViewModels depend on the interface, never on `NoiseRepositoryImpl` directly. Audio processing runs on `Dispatchers.Default` (CPU-bound RMS math, not I/O).
55+
56+
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) · [docs/ADRs.md](docs/ADRs.md)
57+
58+
---
59+
60+
## Tech Stack
61+
62+
| Library | Version | Purpose |
63+
|---|---|---|
64+
| Jetpack Compose BOM | 2026.02.01 | UI |
65+
| Room | 2.7.1 | Local persistence |
66+
| DataStore | 1.1.1 | Scalar preferences |
67+
| Hilt | 2.56.2 | Dependency injection |
68+
| Vico Charts | 2.0.0-alpha.28 | History chart |
69+
| Kotlin Coroutines | 1.9.0 | Async / Flow |
70+
| Accompanist | 0.37.3 | Runtime permissions |
71+
72+
---
73+
74+
## Performance
75+
76+
First dB reading: 1.8s · 60fps · 4.2%/hr battery · 8.3MB APK · 99.4% crash-free
77+
78+
The 1.8s is dominated by `AudioRecord` hardware warm-up, not app startup. The app is idle until you tap Start.
79+
80+
---
81+
82+
## Quick Start
83+
84+
1. `git clone https://github.com/MarkADom/NoiseGuard.git` and open in Android Studio Meerkat (2025.1.1) or later - AGP 9.x requires it; KSP generates the Room DAOs, so the first Gradle sync takes a minute or two
85+
2. Connect a **physical device** - the emulator mic outputs near-silence, giving you 20 dB regardless of environment
86+
3. Run. No API keys. No Firebase. Entirely offline.
87+
88+
---
89+
90+
## Author
91+
92+
**Marco Domingues** - Android Developer · [GitHub](https://github.com/MarkADom) · [LinkedIn](https://www.linkedin.com/in/marco-dv-domingues/) · marco.domingues.dev@gmail.com
93+
94+
BUSL-1.1 License - see [LICENSE](LICENSE.md)

app/build.gradle.kts

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,44 @@
11
plugins {
22
alias(libs.plugins.android.application)
3+
alias(libs.plugins.hilt.android)
4+
alias(libs.plugins.kotlin.android)
35
alias(libs.plugins.kotlin.compose)
6+
alias(libs.plugins.ksp)
47
}
58

69
android {
7-
namespace = "com.marcodomingues.soundflow"
10+
namespace = "com.marcodomingues.noiseguard"
811
compileSdk = 36
912

1013
defaultConfig {
11-
applicationId = "com.marcodomingues.soundflow"
14+
applicationId = "com.marcodomingues.noiseguard"
1215
minSdk = 24
1316
targetSdk = 36
14-
versionCode = 1
15-
versionName = "1.0.0"
16-
17+
versionCode = 2
18+
versionName = "1.0.2"
1719
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
18-
vectorDrawables {
19-
useSupportLibrary = true
20-
}
2120
}
2221

2322
buildTypes {
2423
release {
25-
isMinifyEnabled = false // ← Desativado por agora
24+
isMinifyEnabled = false
2625
}
2726
}
2827

28+
buildFeatures {
29+
compose = true
30+
buildConfig = true
31+
}
32+
2933
compileOptions {
3034
sourceCompatibility = JavaVersion.VERSION_17
3135
targetCompatibility = JavaVersion.VERSION_17
3236
}
3337

38+
kotlinOptions {
39+
jvmTarget = "17"
40+
}
41+
3442
packaging {
3543
resources {
3644
excludes += "/META-INF/{AL2.0,LGPL2.1}"
@@ -39,7 +47,7 @@ android {
3947
}
4048

4149
dependencies {
42-
// Core Android
50+
// Core
4351
implementation(libs.androidx.core.ktx)
4452
implementation(libs.androidx.lifecycle.runtime.ktx)
4553
implementation(libs.androidx.activity.compose)
@@ -55,16 +63,41 @@ dependencies {
5563
// Navigation
5664
implementation(libs.androidx.navigation.compose)
5765

58-
// ViewModel Compose
66+
// Lifecycle
5967
implementation(libs.androidx.lifecycle.viewmodel.compose)
6068
implementation(libs.androidx.lifecycle.runtime.compose)
6169

6270
// Coroutines
6371
implementation(libs.kotlinx.coroutines.android)
6472
implementation(libs.kotlinx.coroutines.core)
6573

66-
// Testing
74+
// Room
75+
implementation(libs.androidx.room.runtime)
76+
implementation(libs.androidx.room.ktx)
77+
ksp(libs.androidx.room.compiler)
78+
79+
// DataStore
80+
implementation(libs.androidx.datastore.preferences)
81+
82+
// Hilt
83+
implementation(libs.hilt.android)
84+
ksp(libs.hilt.compiler)
85+
implementation(libs.hilt.navigation.compose)
86+
87+
// Permissions
88+
implementation(libs.accompanist.permissions)
89+
90+
// Vico Charts
91+
implementation(libs.vico.compose)
92+
implementation(libs.vico.compose.m3)
93+
implementation(libs.vico.core)
94+
95+
// Unit tests
6796
testImplementation(libs.junit)
97+
testImplementation(libs.kotlinx.coroutines.test)
98+
testImplementation(libs.mockk)
99+
100+
// Instrumented tests
68101
androidTestImplementation(libs.androidx.junit)
69102
androidTestImplementation(libs.androidx.espresso.core)
70103
androidTestImplementation(platform(libs.androidx.compose.bom))
@@ -73,4 +106,4 @@ dependencies {
73106
// Debug
74107
debugImplementation(libs.androidx.ui.tooling)
75108
debugImplementation(libs.androidx.ui.test.manifest)
76-
}
109+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"formatVersion": 1,
3+
"database": {
4+
"version": 1,
5+
"identityHash": "d6c8fb7aa04c92350dd5cfe1f26514e5",
6+
"entities": [
7+
{
8+
"tableName": "noise_readings",
9+
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `decibels` REAL NOT NULL, `timestamp` INTEGER NOT NULL, `category` TEXT NOT NULL)",
10+
"fields": [
11+
{
12+
"fieldPath": "id",
13+
"columnName": "id",
14+
"affinity": "INTEGER",
15+
"notNull": true
16+
},
17+
{
18+
"fieldPath": "decibels",
19+
"columnName": "decibels",
20+
"affinity": "REAL",
21+
"notNull": true
22+
},
23+
{
24+
"fieldPath": "timestamp",
25+
"columnName": "timestamp",
26+
"affinity": "INTEGER",
27+
"notNull": true
28+
},
29+
{
30+
"fieldPath": "category",
31+
"columnName": "category",
32+
"affinity": "TEXT",
33+
"notNull": true
34+
}
35+
],
36+
"primaryKey": {
37+
"autoGenerate": true,
38+
"columnNames": [
39+
"id"
40+
]
41+
},
42+
"indices": [
43+
{
44+
"name": "index_noise_readings_timestamp",
45+
"unique": false,
46+
"columnNames": [
47+
"timestamp"
48+
],
49+
"orders": [],
50+
"createSql": "CREATE INDEX IF NOT EXISTS `index_noise_readings_timestamp` ON `${TABLE_NAME}` (`timestamp`)"
51+
}
52+
]
53+
}
54+
],
55+
"setupQueries": [
56+
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
57+
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd6c8fb7aa04c92350dd5cfe1f26514e5')"
58+
]
59+
}
60+
}

app/src/androidTest/java/com/marcodomingues/soundflow/ExampleInstrumentedTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.marcodomingues.soundflow
1+
package com.marcodomingues.noiseguard
22

33
import androidx.test.platform.app.InstrumentationRegistry
44
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
1919
fun useAppContext() {
2020
// Context of the app under test.
2121
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22-
assertEquals("com.marcodomingues.soundflow", appContext.packageName)
22+
assertEquals("com.marcodomingues.noiseguard", appContext.packageName)
2323
}
2424
}

0 commit comments

Comments
 (0)