Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
0c00263
[#49]: Add expo-audio and expo-crypto for draft recording
fel-cesar Jul 2, 2026
156485c
[#49]: Wire recordings table with take/is_latest invariants
fel-cesar Jul 2, 2026
16373c7
[#49]: Persist paused-take marker for recording recovery
fel-cesar Jul 2, 2026
0dee736
[#49]: Build Record tab with draft recording and playback
fel-cesar Jul 2, 2026
c7562a7
[#49]: Document drafting page and recording wiring
fel-cesar Jul 2, 2026
667c0a4
[#49]: Sharpen idle record button to circular icon-only affordance
fel-cesar Jul 2, 2026
9ccfe4d
[#49]: Polish Record tab live and review states
fel-cesar Jul 2, 2026
73509ad
[#49]: Lock verse chevrons while recording
fel-cesar Jul 2, 2026
7128bfd
[#49]: Track recording elapsed by wall clock and show centiseconds
fel-cesar Jul 2, 2026
8d5dd40
[#49]: Tidy verse-nav spacing and drop dead waveform styles
fel-cesar Jul 2, 2026
c21116a
[#49]: Extract draft playback into a dedicated useDraftPlayback hook
fel-cesar Jul 2, 2026
595896f
[#49]: Wire draft playback into the Record tab review controls
fel-cesar Jul 2, 2026
cc07aeb
[#49]: Add expo-file-system for durable recording storage
fel-cesar Jul 2, 2026
56581b5
[#49]: Add durable recording storage layout and file helpers
fel-cesar Jul 2, 2026
68d3b29
[#49]: Persist recording attribution and relative storage keys
fel-cesar Jul 2, 2026
bac7e4b
[#49]: Move committed takes into durable store in useRecorder
fel-cesar Jul 2, 2026
cbc180f
[#49]: Wire attribution context into the Record tab
fel-cesar Jul 2, 2026
bc87ec2
[#49]: Capture takes into durable dir and unlink discarded partials
fel-cesar Jul 2, 2026
2516d1a
[#49]: Extract a generic recorder hook behind a verse adapter
fel-cesar Jul 2, 2026
f2e3a6e
[#49]: Resolve playback uri through the adapter ref
fel-cesar Jul 2, 2026
e14e51e
[#49]: Defer Record tab paint to avoid an idle-to-review flash
fel-cesar Jul 2, 2026
544b789
[#49]: Reorganize Record tab into a record module
fel-cesar Jul 3, 2026
e92c11a
[#49]: Harden getPausedTake bibleTextId validation
fel-cesar Jul 3, 2026
6c85585
[#49]: Move RecordingWaveform bar styles out of inline JSX
fel-cesar Jul 3, 2026
1887bdf
[#49]: Add expo-asset peer dep and config plugin for expo-audio
fel-cesar Jul 3, 2026
123a11d
[#49]: Clear bibleTextId before verse lookup resolves
fel-cesar Jul 3, 2026
b9d5751
[#49]: Guard discard-then-continue handlers in record tab guards
fel-cesar Jul 3, 2026
ec1e31a
[#49]: Guard navigation leave while actively recording
fel-cesar Jul 3, 2026
82eab1a
[#49]: Unlink durable file when insertRecording fails after move
fel-cesar Jul 3, 2026
718ea65
[#49]: Handle pause, resume, and stop recorder failures in RecordTab
fel-cesar Jul 3, 2026
64efd0e
[#49]: Block resume on rehydrated paused takes without live recorder
fel-cesar Jul 3, 2026
5126f7f
[#49]: Harden useRecorder stop and delete error handling
fel-cesar Jul 3, 2026
fe0bb3c
[#49]: Reject reserved segments in recording storage paths
fel-cesar Jul 3, 2026
b6a07e4
Merge branch 'main' into 49-build-record-tab-with-draft-recording-and…
fel-cesar Jul 3, 2026
1c219e2
[#49]: Guard tab switches during recording and fix discard cleanup
fel-cesar Jul 6, 2026
9bb7fe5
[#49]: Toast when verse has no bible_text id on record
fel-cesar Jul 7, 2026
8f61958
[#49]: Keep recording paused across app backgrounding
fel-cesar Jul 7, 2026
24de04f
[#49]: Make partial recordings resilient to app kills
fel-cesar Jul 7, 2026
87773b8
[#49]: Document kill-resilient recording design
fel-cesar Jul 7, 2026
2dce913
[#49]: Persist recovery marker at recording start
fel-cesar Jul 7, 2026
d7a389e
[#49]: Resolve verse navigation from a chapter assignment
fel-cesar Jul 7, 2026
149de3f
[#49]: Prompt to recover a killed recording from home
fel-cesar Jul 7, 2026
200544b
[#49]: Document home recovery prompt and kill persistence
fel-cesar Jul 7, 2026
135b54b
[#49]: Derive recording duration from audio at commit time
fel-cesar Jul 7, 2026
3025c08
[#49]: Document audio-derived duration at commit time
fel-cesar Jul 7, 2026
700c95c
[#49]: Remove the documentation for kill-resilient partial recordings.
fel-cesar Jul 7, 2026
31e0c24
[#49]: Clean up partial merged AAC file on concat failure
fel-cesar Jul 7, 2026
17712e3
Merge branch 'main' into 49-build-record-tab-with-draft-recording-and…
fel-cesar Jul 7, 2026
8e1ae1a
[#49]: Bump Expo SDK 56 patch versions
fel-cesar Jul 7, 2026
357235a
Merge branch 'main' into 49-build-record-tab-with-draft-recording-and…
fel-cesar Jul 9, 2026
0f9ba9e
[#49]: Add draft review scrubbing via interactive waveform
fel-cesar Jul 9, 2026
609569c
[#49]: Generalize draft playback into useAudioPlayback
fel-cesar Jul 9, 2026
67b68ed
[#49]: Reuse UseAudioPlaybackApi for recorder playback
fel-cesar Jul 9, 2026
ee562e2
[#49]: Remove interactive scrub UI from review waveform
fel-cesar Jul 10, 2026
36ff481
Merge branch 'main' into 49-build-record-tab-with-draft-recording-and…
fel-cesar Jul 10, 2026
0b2c52e
[#49]: Propagate initialVerse changes in DraftingProvider
fel-cesar Jul 10, 2026
3ab266d
Merge branch 'main' into 49-build-record-tab-with-draft-recording-and…
fel-cesar Jul 10, 2026
a29a30c
[#176]: Add native AacRemux module for lossless ADTS-to-MP4 remux
fel-cesar Jul 10, 2026
1834a48
[#176]: Remux committed takes to seekable M4A on stop
fel-cesar Jul 10, 2026
3e0d383
[#176]: Add seekable review scrubbing to the draft waveform
fel-cesar Jul 10, 2026
1c3e57b
[#176]: Show scrub/playback position readout in review
fel-cesar Jul 10, 2026
e18d990
[#176]: Pause player before rewinding finished playback
fel-cesar Jul 10, 2026
516dbf0
[#176]: Fix useAudioPlayback test typing and stale poll-interval comment
fel-cesar Jul 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cursor/rules/project-overview.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Offline-first **Expo SDK 56** (React Native **0.85**, React **19.2.3**) companio

- Entry: `App.tsx` → `initializeDatabase()` → `syncAllData()` → `AppNavigator`
- App config: `app.config.ts` (EAS project ID, plugins, package id)
- Screens: `src/app/tabs/` (`ProjectList`, `ViewProject`, `ViewChapter`)
- Screens: `src/app/tabs/` (`ProjectList`, `ViewProject`, `drafting/DraftingPage` + `RecordTab`)
- API: `src/services/api.ts` (`FluentAPI`)
- Sync: `src/services/sync.ts`
- DB writes: `src/db/repository.ts` | reads: `src/db/queries.ts`
Expand Down
8 changes: 8 additions & 0 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ const config: ExpoConfig = {
],
'./plugins/withRNScreensFragmentFactory',
'expo-secure-store',
'expo-asset',
[
'expo-audio',
{
microphonePermission:
'Fluent needs access to your microphone so translators can record verse drafts.',
},
],
],
};

Expand Down
6 changes: 4 additions & 2 deletions docs/AGENT_ONBOARDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Quick map for Cursor agents, other coding tools, and new contributors. Verified
| Path | Purpose |
|------|---------|
| [`App.tsx`](../App.tsx) | Root: DB init + initial `syncAllData`, then navigator |
| [`src/app/tabs/`](../src/app/tabs/) | Screens: `ProjectList`, `ViewProject`, `ViewChapter` |
| [`src/app/tabs/`](../src/app/tabs/) | Screens: `ProjectList`, `ViewProject`, `drafting/DraftingPage` (parent shell stub for #47) with `RecordTab` (#49) |
| [`src/navigation/`](../src/navigation/) | Stack navigator |
| [`src/services/api.ts`](../src/services/api.ts) | HTTP client (`FluentAPI`) — see [api-client-standard.md](guides/api-client-standard.md) |
| [`src/services/sync.ts`](../src/services/sync.ts) | Sync orchestration, retries, KV counts |
Expand Down Expand Up @@ -209,7 +209,9 @@ When adding features: mock `op-sqlite`, navigation, and sync in screen tests fol
- [x] Dependabot reviewers: `eten-tech-foundation/fluent-admin`
- [x] Mock or gate `fluent-api.test.ts` for offline CI
- [ ] Align `format:check` glob with `format` or document intentionally narrow check
- [ ] Wire `recordings` table to actual audio capture/upload
- [x] Wire `recordings` table to actual audio capture (issue #49 — upload still pending)
- [ ] Upload/sync draft recordings to the Fluent server (`sync_status='pending'` rows)
- [ ] Replace parent drafting shell stub with full #47 implementation (chapter audio player, tab persistence)

## Related docs

Expand Down
18 changes: 18 additions & 0 deletions modules/aac-remux/android/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
plugins {
id 'com.android.library'
id 'expo-module-gradle-plugin'
}

group = 'expo.modules.aacremux'
version = '0.1.0'

android {
namespace "expo.modules.aacremux"
defaultConfig {
versionCode 1
versionName "0.1.0"
}
lintOptions {
abortOnError false
}
}
2 changes: 2 additions & 0 deletions modules/aac-remux/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package expo.modules.aacremux

import android.media.MediaCodec
import android.media.MediaExtractor
import android.media.MediaFormat
import android.media.MediaMuxer
import android.net.Uri
import expo.modules.kotlin.exception.CodedException
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import java.nio.ByteBuffer

/**
* Repackages a raw ADTS AAC (`.aac`) recording into an MP4/M4A container without
* re-encoding. ADTS is what we capture (self-framing, so it survives a process
* kill and concatenates by byte-append), but ExoPlayer cannot reliably seek a
* raw ADTS stream. Copying the AAC elementary stream into an MP4 container adds
* the sample table that makes review playback seekable — a lossless remux, not
* a transcode.
*/
class AacRemuxModule : Module() {
override fun definition() = ModuleDefinition {
Name("AacRemux")

AsyncFunction("remuxToMp4") { sourceUri: String, destUri: String ->
remux(toFilePath(sourceUri), toFilePath(destUri))
destUri
}
}

private fun toFilePath(uri: String): String =
if (uri.startsWith("file://")) {
Uri.parse(uri).path ?: uri.removePrefix("file://")
} else {
uri
}

private fun remux(srcPath: String, dstPath: String) {
val extractor = MediaExtractor()
var muxer: MediaMuxer? = null
var muxerStarted = false
try {
extractor.setDataSource(srcPath)

var audioTrackIndex = -1
var trackFormat: MediaFormat? = null
for (i in 0 until extractor.trackCount) {
val candidate = extractor.getTrackFormat(i)
val mime = candidate.getString(MediaFormat.KEY_MIME)
if (mime != null && mime.startsWith("audio/")) {
audioTrackIndex = i
trackFormat = candidate
break
}
}

val format = trackFormat
?: throw CodedException("No audio track found in $srcPath")
extractor.selectTrack(audioTrackIndex)

// ADTS carries no container timestamps, so synthesize monotonically
// increasing presentation times from the fixed AAC frame length
// (1024 samples per frame). MP4 requires increasing timestamps.
val sampleRate = if (format.containsKey(MediaFormat.KEY_SAMPLE_RATE)) {
format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
} else {
44100
}
val frameDurationUs = 1_000_000L * 1024L / sampleRate

muxer = MediaMuxer(dstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
val outputTrackIndex = muxer.addTrack(format)
muxer.start()
muxerStarted = true

val maxInputSize = if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) {
format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE).coerceAtLeast(64 * 1024)
} else {
256 * 1024
}
val buffer = ByteBuffer.allocate(maxInputSize)
val bufferInfo = MediaCodec.BufferInfo()
var presentationTimeUs = 0L

while (true) {
val sampleSize = extractor.readSampleData(buffer, 0)
if (sampleSize < 0) {
break
}
bufferInfo.offset = 0
bufferInfo.size = sampleSize
bufferInfo.presentationTimeUs = presentationTimeUs
bufferInfo.flags = MediaCodec.BUFFER_FLAG_KEY_FRAME
muxer.writeSampleData(outputTrackIndex, buffer, bufferInfo)
presentationTimeUs += frameDurationUs
extractor.advance()
}
} catch (e: CodedException) {
throw e
} catch (e: Exception) {
throw CodedException("Failed to remux AAC to MP4: ${e.message}")
} finally {
if (muxerStarted) {
try {
muxer?.stop()
} catch (_: Exception) {
}
}
try {
muxer?.release()
} catch (_: Exception) {
}
extractor.release()
}
}
}
6 changes: 6 additions & 0 deletions modules/aac-remux/expo-module.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"platforms": ["android"],
"android": {
"modules": ["expo.modules.aacremux.AacRemuxModule"]
}
}
Loading
Loading