An offline Android app for mixing looping sounds. Pick audio files from your device, play any number of them at the same time, and set each one's volume independently — rain under a fan under a café, at whatever balance you like.
Free software (GPL-3), no account, no network calls, no telemetry.
- Add sounds with the
+button. The system file picker opens and accepts a multi-selection, so a whole folder of ambience lands in one go. The files themselves are never copied or moved — Soundboard just remembers where they are. - Play any combination at once. Each row has its own play/pause toggle, and sounds loop until you stop them. Tapping anywhere on a row toggles it too.
- Set each sound's volume by tapping its volume pill, then dragging around the dial. The percentage is relative: your device's volume keys still move the whole mix together, and each sound keeps its balance within it.
- Keeps playing in the background with the screen off. A notification shows how many sounds are running and offers "Stop all".
- Remove a sound by long-pressing its row and choosing Delete. This only takes it off the board — your audio file is left exactly where it is.
- Android 8.0 (API 26) or newer.
- Audio files already on your device or reachable through a document provider.
git clone <this repo>
cd soundboard
./gradlew installDebug # with a device connected or an emulator runningOr open the repo root in Android Studio and hit Run. First-time setup and the
JDK note for CLI builds are in docs/development.md.
Kotlin + Jetpack Compose, single Gradle module, no backend.
| Component | Version |
|---|---|
| Android Gradle Plugin | 9.2.0 |
| Gradle | 9.4.1 |
| Kotlin | 2.3.20 |
| Compose BOM | 2026.08.00 |
| Media3 (ExoPlayer) | 1.11.0 |
| DataStore | 1.2.1 |
| minSdk / targetSdk / compileSdk | 26 / 36 / 37 |
Full table and pinning policy: docs/tooling.md.
./gradlew check # ktlint + unit tests + Android lint
./gradlew assembleDebug # ...and package the APKUnit tests cover the pure logic — board mutations, persistence and its
corruption handling, and the volume dial's angle→percent mapping. Playback,
mixing, audio focus, and the foreground service need a real device; the manual
pass is written up in
docs/development.md.
Four small layers:
data/— the board is one JSON document (DataStore).SoundLibraryholds every mutation as a pure function, so the list logic tests without an Android runtime.SoundRepositoryalso owns the Storage Access Framework read grants.audio/—SoundboardEngineruns one ExoPlayer per sound, which is what makes simultaneous, independently-mixed, gaplessly-looping playback work. Audio focus is requested once for the whole app rather than per player.PlaybackServiceis a foreground service that exists only to keep the process alive and show the notification.ui/— a navigation drawer over three destinations (the board, About, Licenses), a Compose list of cards, and the customCanvasvolume dial.SoundboardApp— the composition root. No DI framework.
The reasoning behind each of those, and the options that were rejected, is in
DECISIONS.md. Read it before changing anything under
audio/ — the threading and audio-focus rules are easy to break with a change
that compiles cleanly.
| Path | What |
|---|---|
app/ |
The single Gradle module; all app code. |
docs/ |
Development, tooling, deployment, security, supply-chain docs. |
scripts/ |
Thin ./gradlew wrappers used by CI and locally. |
extras/github/ |
Opt-in GitHub hygiene (CODEOWNERS, gitleaks, dependency review). |
SPEC.md |
The MVP product spec. |
DECISIONS.md |
Architecture decisions and their reasoning. |
AGENTS.md |
Operating instructions for AI agents working in this repo. |
MANIFEST.md |
Inventory of the 508.dev devkit this repo was generated from. |
Everything stays on your device. The app requests no INTERNET permission, so
it cannot phone home even by accident. It never asks for broad storage access
either — only per-file read permission for the sounds you explicitly pick.
See SECURITY.md for the full data-handling note and how to
report a vulnerability.
GPL-3.0. See LICENSE.