# BackTalk Reverse Recorder — build spec

An offline reverse voice recorder that lets users record, reverse, replay, and save several clips before any paid prompt, with no ads, no trial maze, and no account.

## Project context

This spec describes an independent, alternative Android app you are building from scratch to compete with an existing incumbent app on Google Play — not a modification, clone, or reskin of the incumbent's own code, assets, or branding. Use the incumbent only as a market reference (via the report data below), and design working_name/package_id/store_listing/design_system so the result is clearly its own product.

## Incumbent app

- **Name:** Reverse Song & Voice Challenge
- **Package id:** `com.reverse.audio.singchallenge`
- **Google Play:** https://play.google.com/store/apps/details?id=com.reverse.audio.singchallenge
- **appy.fyi report:** https://appy.fyi/report/com.reverse.audio.singchallenge
- **Category:** Music & Audio

## Overview

- **Working name:** BackTalk Reverse Recorder (trademark cleared: no)
- **Package id:** `fyi.appy.backtalkreverserecorder`
- **Min / target SDK:** 26 / 35
- **Backend:** none
- **Estimated build time:** 4 weeks
- **Pricing:** one-time purchase, $2.99 via `play_billing_direct`
- **Runtime AI:** none
- **Permissions:** `RECORD_AUDIO`

## Non-goals (out of scope for v1)

- No display ads, rewarded ads, interstitial ads, or ad SDK integration in v1.
- No subscriptions, free trials, weekly plans, monthly plans, credits, or token systems.
- No cloud accounts, sync, remote backup, social feed, or server-side clip storage.
- No AI-generated prompts, captions, voice effects, or clip labels in v1.
- No background audio recording, background services, home-screen overlays, launchers, widgets, or uninstall-related behavior.
- No professional audio editor features beyond record, reverse, speed, pitch, save, rename, and share.

## Tech stack

- **Language / UI:** Kotlin, Jetpack Compose
- **Kotlin:** 2.0.21
- **Compose BOM:** 2024.10.01
- **Gradle:** 8.9

| Purpose | Gradle coordinate |
| --- | --- |
| Android Kotlin extensions and FileProvider support for safe clip sharing | `androidx.core:core-ktx:1.13.1` |
| Compose activity host | `androidx.activity:activity-compose:1.9.3` |
| Compose UI runtime and primitives | `androidx.compose.ui:ui:1.7.4` |
| Material 3 Compose components | `androidx.compose.material3:material3:1.3.0` |
| Compose preview tooling hooks | `androidx.compose.ui:ui-tooling-preview:1.7.4` |
| Compose Navigation graph | `androidx.navigation:navigation-compose:2.8.3` |
| Lifecycle-aware Compose state collection | `androidx.lifecycle:lifecycle-runtime-compose:2.8.6` |
| Compose ViewModel integration | `androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6` |
| Room local database runtime for clip metadata and settings | `androidx.room:room-runtime:2.6.1` |
| Room coroutine DAO support | `androidx.room:room-ktx:2.6.1` |
| Room annotation processor | `androidx.room:room-compiler:2.6.1` |
| Coroutines for recording, reversing, playback, database, and billing flows | `org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0` |
| Direct Google Play one-time in-app purchase unlock | `com.android.billingclient:billing-ktx:7.1.1` |

## Design system

- **Primary color:** `#2F6F73`
- **Background color:** `#FFFBF5`
- **Error color:** `#B3261E`
- **Typography:** Material 3 default type scale, no custom font
- **Launcher icon glyph:** Phosphor `rewind-circle` (regular weight)
- **Theme notes:** Use Material 3 dynamic color only when the user device supports it; otherwise use #2F6F73 as primary, #FFFBF5 as light background, #111827 as light on-background, #0B1F22 as dark background, #E6F4F1 as dark on-background, and #B3261E for errors. Keep screens sparse with large record/replay controls and no decorative banners that could be mistaken for ads.

## Screens

### Onboarding
- **Route:** `onboarding`
- **Purpose:** First-run trust screen that explains the offline, no-ad, no-subscription promise before the user reaches the recorder.
- **Reached via:** app launch when AppSetting key onboarding_completed is absent or false; tap Privacy promise from Recorder overflow menu
- **Key UI elements:** App title; Three trust bullets: Offline recording, No ads during use, One-time optional unlock; Plain-language privacy note: recordings stay on this device unless shared; Start recording button; Privacy details link
- **States:** first_run, returning_user, privacy_link_openable, completed

### Recorder
- **Route:** `recorder`
- **Purpose:** Primary screen for recording voice, generating a reversed clip, replaying it, adjusting speed and pitch, saving, and reaching unlock or library.
- **Reached via:** app launch after onboarding is completed; tap Back from Library; tap Back from Clip Detail; tap Back from Unlock
- **Key UI elements:** Large record/stop button; Recording timer; Reverse processing progress indicator; Play/pause reversed audio button; Speed slider; Pitch slider; Save clip button; Share temporary clip button; Library button; Upgrade button; Privacy/safety menu item
- **States:** permission_not_requested, permission_denied, idle_empty, recording, free_length_limit_reached, processing_reverse, playback_ready, playback_active, save_blocked_free_limit, paid_unlocked, recording_error, playback_error

### Clip Library
- **Route:** `library`
- **Purpose:** Local list of saved reversed clips with duration, date, rename, play, share, and delete entry points.
- **Reached via:** tap Library button on Recorder; tap View library after saving a clip
- **Key UI elements:** Top app bar with Back button; Saved clip count; LazyColumn of clip rows; Clip title; Clip duration; Created date; Inline play button; Share button; Delete button; Empty-state call to record first clip
- **States:** loading, empty, populated, delete_confirming, error

### Clip Detail
- **Route:** `clip/{clipId}`
- **Purpose:** Detailed local clip screen for replaying, renaming, changing speed/pitch for playback, sharing, and deleting one saved clip.
- **Reached via:** tap a clip row in Clip Library
- **Key UI elements:** Top app bar with Back button; Editable clip title; Duration and created date; Play/pause button; Speed slider; Pitch slider; Rename confirmation button; Share button; Delete button
- **States:** loading, populated, renaming, sharing, missing_file_error, delete_confirming, error

### Unlock
- **Route:** `unlock`
- **Purpose:** Transparent one-time purchase screen for unlimited saves, longer clips, and extra speed/pitch ranges without subscriptions or trials.
- **Reached via:** tap Upgrade button on Recorder; tap Upgrade after free save limit is reached; tap Upgrade after free length limit is reached; tap Upgrade when selecting paid-only speed or pitch range
- **Key UI elements:** One-time unlock title; $2.99 price text from Play Billing ProductDetails; Benefit bullets: unlimited saved clips, longer recordings, extra speed and pitch range; Buy once button; Restore purchase button; No subscriptions or trials note; Back button
- **States:** loading_product, free_status, purchase_pending, purchased, purchase_canceled, billing_unavailable_error, product_unavailable_error

### Privacy & Safety
- **Route:** `privacy`
- **Purpose:** Plain safety screen that explains offline storage, requested permission, sharing behavior, and the absence of ads, accounts, background services, and subscriptions.
- **Reached via:** tap Privacy details on Onboarding; tap Privacy/safety menu item on Recorder; tap Privacy link on Unlock
- **Key UI elements:** Back button; Offline storage section; Microphone permission explanation; Sharing explanation; No ads section; No account section; No subscription section; Uninstall reassurance section
- **States:** populated

## Data model

### Clip (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `Long` | primary key, autogenerate |
| title | `String` | user-visible title; default format is Reverse Clip yyyy-MM-dd HH:mm |
| originalFileName | `String` | file name under app internal files/audio/original/ |
| reversedFileName | `String` | file name under app internal files/audio/reversed/ |
| durationMs | `Long` | duration derived from PCM byte count and sample rate |
| sampleRate | `Int` | 44100 for v1 recordings |
| channels | `Int` | 1 for v1 mono recordings |
| createdAtEpochMs | `Long` | System.currentTimeMillis at save time |
| updatedAtEpochMs | `Long` | updated on rename |
| lastSpeed | `Float` | last playback speed selected for this clip; default 1.0 |
| lastPitch | `Float` | last playback pitch selected for this clip; default 1.0 |

### AppSetting (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| key | `String` | primary key; use onboarding_completed and unlock_forever_entitled |
| value | `String` | store booleans as true or false strings |
| updatedAtEpochMs | `Long` | System.currentTimeMillis when written |

## Features

### No-ad first session onboarding

Shows a short trust-focused onboarding and then takes the user directly to recording without ads, trial prompts, or subscription prompts.

- **Answers complaint:** Ads before and during use

- **Screens:** Onboarding, Recorder, Privacy & Safety

- **Estimated hours:** 18

**Implementation notes:** Do not add any ad SDK dependency or ad view to the project. On first launch, read AppSetting key onboarding_completed from Room; if absent or false, navigate to route onboarding. The Start recording button writes onboarding_completed=true and navigates to recorder. The app must not call Play Billing, show Unlock, request payment, or show any modal before the user taps Start recording. Request RECORD_AUDIO only when the user taps the record button on Recorder, using rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()).

**Acceptance criteria:**
- A fresh install opens Onboarding, not Unlock.
- Onboarding contains no text matching trial, subscription, token, credit, ad, or rewarded except the trust promise stating no ads during use.
- After tapping Start recording, the Recorder screen appears without any interstitial, modal, purchase sheet, or external browser.
- The project dependency graph contains no Google Mobile Ads, AppLovin, Unity Ads, ironSource, or other ad network artifact.

### Offline local voice recording

Records microphone audio to app-private local WAV files without accounts, cloud upload, or external storage permissions.

- **Answers complaint:** baseline parity

- **Screens:** Recorder

- **Estimated hours:** 30

**Implementation notes:** Use android.media.AudioRecord with sample rate 44100 Hz, CHANNEL_IN_MONO, ENCODING_PCM_16BIT, and buffer size max(AudioRecord.getMinBufferSize(...), 4096) * 2. While recording, stream PCM bytes to a temp raw file under filesDir/audio/temp/. On stop, write a 44-byte WAV header plus the raw PCM to filesDir/audio/original/{uuid}.wav. Free users auto-stop at 30 seconds with a visible 'Free recordings stop at 30 seconds' message; unlocked users auto-stop at 300 seconds. Delete the temp raw file after both original and reversed WAV files are created. All recording and file IO runs on Dispatchers.IO; UI state is emitted as idle_empty, recording, processing_reverse, or recording_error.

**Acceptance criteria:**
- Tapping Record on a device without microphone permission opens the Android microphone permission dialog.
- Denying permission leaves the app on Recorder in permission_denied state with an explanation and no crash.
- Granting permission and recording for 3 seconds creates an original WAV file under app-private internal storage.
- The app requests RECORD_AUDIO and no media-library or external-storage permission.

### Reverse playback with speed and pitch controls

Creates a reversed version of each recording and plays it back locally with adjustable speed and pitch.

- **Answers complaint:** baseline parity

- **Screens:** Recorder, Clip Detail, Unlock

- **Estimated hours:** 40

**Implementation notes:** After recording stops, read the raw PCM as 16-bit little-endian mono samples, reverse the sample order, and write a new WAV file to filesDir/audio/reversed/{uuid}.wav using the same 44100 Hz mono header. For playback, use android.media.AudioTrack in MODE_STREAM with sample rate 44100, CHANNEL_OUT_MONO, ENCODING_PCM_16BIT. Feed PCM data from the reversed WAV after skipping its 44-byte header. Apply speed and pitch with PlaybackParams: setSpeed(speed) and setPitch(pitch) before play. Free range is speed 0.75x to 1.25x and pitch 0.85x to 1.15x. Unlocked range is speed 0.50x to 2.00x and pitch 0.50x to 2.00x. If the user drags beyond the free range while not unlocked, snap back to the free edge and show an Upgrade affordance; do not interrupt current playback.

**Acceptance criteria:**
- A known PCM sample sequence [1000, -1000, 2000, -2000] reverses to [-2000, 2000, -1000, 1000].
- After a recording finishes, the Play button becomes enabled only after the reversed WAV exists.
- Changing speed from 1.0x to 0.75x updates the AudioTrack PlaybackParams speed value before playback starts.
- A free user selecting 2.0x speed is shown the Unlock screen entry point but can continue using 1.25x without losing the clip.

### Local clip library with rename and delete

Lets users save several reversed clips locally, browse them, rename them, replay them, and delete them.

- **Answers complaint:** baseline parity

- **Screens:** Recorder, Clip Library, Clip Detail

- **Estimated hours:** 24

**Implementation notes:** Persist clip metadata in Room Clip. When Save clip is tapped, move the current original and reversed WAV files from temp paths into filesDir/audio/original/ and filesDir/audio/reversed/, insert Clip with title 'Reverse Clip yyyy-MM-dd HH:mm', durationMs, sampleRate=44100, channels=1, and lastSpeed/lastPitch=1.0. Library queries Room sorted by createdAtEpochMs descending. Rename updates Clip.title and updatedAtEpochMs only; it never renames the underlying file. Delete shows a confirmation dialog, deletes both internal WAV files if present, then deletes the Room row. Missing files show missing_file_error instead of crashing.

**Acceptance criteria:**
- Saving a clip inserts exactly one Clip row and two app-private WAV files.
- Clip Library empty state appears when Room returns zero Clip rows.
- Renaming a clip changes the title displayed in Clip Library after navigating back.
- Deleting a clip removes its Room row and both associated WAV files.

### Safe system sharing

Shares a reversed WAV clip through Android's standard share sheet without broad storage permissions.

- **Answers complaint:** baseline parity

- **Screens:** Recorder, Clip Library, Clip Detail

- **Estimated hours:** 16

**Implementation notes:** Configure androidx.core.content.FileProvider with authority '${applicationId}.fileprovider' and a file_paths XML entry that exposes only cacheDir/share/. When Share is tapped, copy the selected reversed WAV into cacheDir/share/{clip-title-sanitized}.wav, get a content URI from FileProvider, and launch Intent.ACTION_SEND with type 'audio/wav', EXTRA_STREAM set to the URI, FLAG_GRANT_READ_URI_PERMISSION, and Intent.createChooser. Do not request READ_MEDIA_AUDIO, MANAGE_EXTERNAL_STORAGE, or write to public storage. If no activity can handle the share intent, show sharing_error.

**Acceptance criteria:**
- Sharing launches Android's chooser with an audio/wav content URI.
- The shared URI authority ends with .fileprovider.
- The app does not request MANAGE_EXTERNAL_STORAGE or READ_MEDIA permissions.
- If the reversed file is missing, Clip Detail shows missing_file_error and does not launch a chooser.

### Generous free mode limits

Allows multiple free recordings, reversals, replays, and saved clips before asking for the one-time unlock.

- **Answers complaint:** Too few free tries

- **Screens:** Recorder, Clip Library, Unlock

- **Estimated hours:** 10

**Implementation notes:** A non-unlocked user may save up to 5 clips and may record clips up to 30 seconds. Recording, reversing, replaying, renaming existing saved clips, deleting clips, and sharing saved clips remain available without ads or tokens. The save gate checks SELECT COUNT(*) FROM Clip; counts 0 through 4 allow Save, count 5 or higher shows save_blocked_free_limit with buttons Delete old clip, Continue unsaved, and Upgrade. Deleting a saved clip lowers the count and allows another free save. Paid unlock removes the save count gate and raises the length cap to 300 seconds.

**Acceptance criteria:**
- A free user can save the first, second, third, fourth, and fifth clips without seeing Unlock.
- A free user attempting to save a sixth clip sees save_blocked_free_limit rather than an ad, token prompt, or trial prompt.
- A free user can replay an unsaved reversed clip repeatedly without any prompt.
- After deleting one of five saved clips, a free user can save one additional clip.

### One-time unlock through Play Billing

Offers a single $2.99 lifetime unlock for unlimited saves, longer clips, and extra speed/pitch range.

- **Answers complaint:** Subscription distrust

- **Screens:** Unlock, Recorder

- **Estimated hours:** 16

**Implementation notes:** Use Google Play Billing Library with one in-app product id 'unlock_forever_299'. Query ProductDetails for ProductType.INAPP on Unlock screen entry and display the Play-provided formatted price; listing copy and fallback copy state $2.99 one-time unlock. Launch BillingFlowParams with that ProductDetails when Buy once is tapped. On PurchasesUpdatedListener, acknowledge unacknowledged PURCHASED purchases with acknowledgePurchase and write AppSetting unlock_forever_entitled=true. On app start and Restore purchase, call queryPurchasesAsync(ProductType.INAPP); if the product is PURCHASED, set entitlement true. Do not create SUBS products, base plans, trials, offers, credits, or renewal language.

**Acceptance criteria:**
- Unlock screen contains the phrase 'One-time unlock' and does not contain subscription, weekly, monthly, free trial, or auto-renew.
- A successful test purchase writes AppSetting unlock_forever_entitled=true.
- After entitlement is true, a user with 5 saved clips can save a sixth clip.
- Restore purchase sets entitlement true when Play Billing returns a PURCHASED unlock_forever_299 purchase.

### Offline privacy and uninstall-safe behavior

Makes the app boringly safe by keeping recordings local, avoiding background behavior, and explaining exactly what the app does and does not do.

- **Answers complaint:** Safety and uninstall fear

- **Screens:** Privacy & Safety, Onboarding, Recorder

- **Estimated hours:** 6

**Implementation notes:** Do not implement BroadcastReceiver, foreground service, background service, overlay permission, notification permission, device admin receiver, accessibility service, launcher shortcut creation, or home-screen behavior. Store all audio under app internal filesDir so Android removes it on uninstall. The Privacy & Safety screen must state: recordings stay on this device unless shared, microphone is used only after tapping Record, the app has no ads, no account, no subscription, and no background recording. AndroidManifest should declare only RECORD_AUDIO plus the FileProvider provider needed for sharing.

**Acceptance criteria:**
- AndroidManifest contains no service declarations.
- AndroidManifest contains no receiver declarations.
- AndroidManifest contains no SYSTEM_ALERT_WINDOW, POST_NOTIFICATIONS, accessibility, device admin, or storage permission.
- Privacy & Safety screen explicitly says that uninstalling the app removes app-private recordings unless the user shared copies elsewhere.

## Store listing

- **Title:** BackTalk Reverse Recorder
- **Short description:** Offline reverse voice recorder. No ads, no trials, one-time unlock.
- **Category:** Music & Audio
- **Keywords:** reverse voice, voice recorder, backwards audio, reverse audio, offline recorder, funny voice, audio share, speed pitch
- **Icon prompt:** Create a clean Android app icon for an offline reverse voice recorder: rounded square background in deep teal #2F6F73, centered white rewind-circle symbol combined subtly with a small microphone shape, flat vector style, high contrast, no text, no ads imagery, no gradients beyond a very soft shadow, suitable for Play Store launcher icon.

**Long description:**

Record your voice, flip it backwards, replay it, save it, and share it — without the ad barrage. BackTalk Reverse Recorder is a simple offline toy utility built around trust: no account, no recording-interrupting ads, no subscription maze, and no forced trial.

Free core:
• Record voice locally
• Reverse audio on device
• Replay clips as much as you want
• Save several clips before upgrading
• Rename and delete saved clips
• Share reversed WAV files through Android's share sheet
• Basic speed and pitch controls

One-time unlock:
• Unlimited saved clips
• Longer recordings
• Extra speed and pitch range

Your recordings stay on your device unless you choose to share them. The app uses the microphone only when you tap Record.

## Legal

- **Regulated category:** none
- **Privacy policy URL:** https://example.com/backtalk-reverse-recorder/privacy (privacy claims verified: no)
- **Data collected:** none

## Test plan

### 1. Ads before and during use (instrumented)

1. Clear app data.
2. Launch MainActivity.
3. Assert the Onboarding screen is displayed.
4. Assert no node contains text 'trial', 'subscription', 'token', 'credit', or 'rewarded'.
5. Tap 'Start recording'.
6. Assert the Recorder screen is displayed.
7. Assert no Unlock screen, purchase sheet, web view, interstitial container, or modal dialog is displayed.

**Expected:** A fresh user reaches Recorder from Onboarding with no ad, trial, subscription, token, or purchase interruption.

### 2. Too few free tries (unit)

1. Create a fake ClipDao returning clip counts 0, 1, 2, 3, and 4.
2. For each count, call FreeGate.canSaveClip(isUnlocked=false).
3. Create a fake ClipDao returning clip count 5.
4. Call FreeGate.canSaveClip(isUnlocked=false).
5. Call FreeGate.canSaveClip(isUnlocked=true) with clip count 5.

**Expected:** Counts 0 through 4 return Allowed, count 5 while free returns BlockedFreeLimit, and count 5 while unlocked returns Allowed.

### 3. Too few free tries (instrumented)

1. Install fresh and complete onboarding.
2. Grant RECORD_AUDIO permission.
3. Record five short clips of at least 1 second each using the Recorder screen.
4. After each clip, tap Save.
5. After the fifth saved clip, record a sixth short clip and tap Save.
6. Observe the UI shown after the sixth Save tap.

**Expected:** The first five clips save without showing Unlock; the sixth save shows save_blocked_free_limit with Delete old clip, Continue unsaved, and Upgrade options, and no ad or trial prompt.

### 4. Subscription distrust (instrumented)

1. Launch the app with onboarding completed.
2. Tap Upgrade on Recorder.
3. Wait for Unlock screen to finish loading or show product_unavailable_error.
4. Read all visible text nodes on Unlock.

**Expected:** Unlock copy says one-time unlock and $2.99 or the Play Billing formatted in-app product price; it contains no weekly, monthly, subscription, free trial, auto-renew, or recurring billing language.

### 5. Safety and uninstall fear (manual)

1. Open the generated AndroidManifest.xml from the release build intermediates.
2. Verify declared permissions.
3. Verify application components.
4. Install the app on a test device.
5. Open Privacy & Safety from Recorder.
6. Uninstall the app using the Android system app info screen.

**Expected:** The manifest declares RECORD_AUDIO and the FileProvider only, with no services or receivers; Privacy & Safety states no ads, no account, no subscription, no background recording, and uninstalling removes app-private recordings; the app uninstalls through the normal Android uninstall flow.

### 6. baseline parity (unit)

1. Create a temporary raw PCM file containing four little-endian 16-bit mono samples: 1000, -1000, 2000, -2000.
2. Run the reverse PCM function used by the app.
3. Read the output PCM samples in order.

**Expected:** The output samples are exactly -2000, 2000, -1000, 1000.

### 7. baseline parity (instrumented)

1. Launch the app with a pre-populated Room database containing one Clip row and matching original and reversed WAV files in internal storage.
2. Open Clip Library.
3. Tap the clip row.
4. Tap the title edit field, replace the title with 'Test Backwards Clip', and confirm rename.
5. Navigate back to Clip Library.

**Expected:** Clip Library displays 'Test Backwards Clip' for that row, and the app remains on local screens without requesting any network account or storage permission.

### 8. baseline parity (instrumented)

1. Launch the app with one saved Clip and a valid reversed WAV file.
2. Open Clip Detail.
3. Tap Share.
4. Intercept the outgoing intent with intended(hasAction(Intent.ACTION_CHOOSER)).
5. Inspect the wrapped SEND intent extras.

**Expected:** The outgoing share intent has type audio/wav, includes EXTRA_STREAM with a content URI from the app FileProvider, and grants read URI permission.

## Build instructions

```sh
./gradlew clean
./gradlew testDebugUnitTest
./gradlew connectedDebugAndroidTest
./gradlew bundleRelease
```

## Human gates still required

- `trademark_and_privacy_review`
- `closed_testing_recruitment`
