Skip to content

feat play conversation audio from local WAL files when stored on phone#6708

Open
krushnarout wants to merge 2 commits intomainfrom
feat/local-wal-audio-playback
Open

feat play conversation audio from local WAL files when stored on phone#6708
krushnarout wants to merge 2 commits intomainfrom
feat/local-wal-audio-playback

Conversation

@krushnarout
Copy link
Copy Markdown
Member

Summary

  • When Store Audio on Phone is enabled, ConversationAudioPlayerWidget now resolves WAL files with conversationId match + storage == WalStorage.disk from SyncProvider.allWals and decodes them to WAV via the existing AudioPlayerUtils.ensureAudioFileExists before playback
  • Falls back transparently to GCS cloud streaming if no local WALs are found, or if any WAL in the set is missing/unconvertible
  • WALs are sorted by timerStart to preserve correct playback order across multi-chunk conversations

Test plan

  • Enable Store Audio on Phone in settings, record a conversation, then open the audio player — confirm it plays without a network request
  • Disable Store Audio on Phone — confirm cloud streaming still works unchanged
  • Open a conversation that has no local WAL files — confirm cloud fallback is seamless
  • Kill and reopen the app, then open an old conversation with local files — confirm WAV cache is reused

🤖 Generated with Claude Code

When 'Store Audio on Phone' is enabled, local WAL disk files are now
preferred over cloud streaming. Falls back to GCS transparently if no
local WALs are found for the conversation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 16, 2026

Greptile Summary

This PR adds local WAL file playback to ConversationAudioPlayerWidget — when "Store Audio on Phone" is enabled, it resolves on-disk WAL chunks by conversationId, converts them to WAV via AudioPlayerUtils.ensureAudioFileExists, and falls back to GCS cloud streaming on any failure. Two P1 bugs block the feature from working correctly:

  • Local WAL check is always bypassed on first load: _setupAudioPlayer() is called from initState() before didChangeDependencies() sets _syncProvider. Because _buildLocalAudioSources() reads _syncProvider?.allWals synchronously and returns null immediately when it's null, the continuation resumes from the microtask queue with localSources = null. Local WAL files are never used until the user manually retries.
  • Seek and position display are wrong when WAL count ≠ audioFiles count: _trackStartOffsets is always built from conversation.audioFiles (server metadata). A conversation with 3 on-disk WAL chunks but 1 merged server AudioFile will have a 3-track WAL playlist mapped against a 1-entry offset table, causing seeking to target the wrong tracks and the progress slider to display within-track position only.

Confidence Score: 2/5

Not safe to merge — the primary feature (local WAL playback) never activates on first load, and seeking is broken when WAL chunk count differs from server audioFiles count.

Two P1 bugs: (1) _syncProvider is always null during the first _buildLocalAudioSources() call because initState() runs before didChangeDependencies(), so local WAL files are permanently bypassed on the initial open; (2) _trackStartOffsets is computed from audioFiles metadata and not updated when the WAL playlist is used, causing incorrect seek behavior whenever WAL chunk count differs from server audio file count. The cloud fallback path still works, so nothing regresses, but the new feature does not function as described.

app/lib/widgets/conversation_audio_player_widget.dart — both bugs are in this file, in _setupAudioPlayer / _buildLocalAudioSources and the missing WAL-based offset recalculation.

Important Files Changed

Filename Overview
app/lib/widgets/conversation_audio_player_widget.dart Adds local WAL playback with cloud fallback, but two P1 bugs prevent the feature from working: _syncProvider is null on first setup (initState runs before didChangeDependencies), and track offsets remain based on audioFiles metadata rather than WAL boundaries when local sources are used.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[_setupAudioPlayer] --> B{hasAudio?}
    B -- No --> Z[return]
    B -- Yes --> C[_buildLocalAudioSources]
    C --> D{_syncProvider null?}
    D -- "Yes - initState race" --> E["return null ❌ Bug: always on first load"]
    D -- No --> F["Filter WALs: conversationId match + storage==disk + filePath set"]
    F --> G{conversationWals empty?}
    G -- Yes --> E2[return null - Fall back to cloud]
    G -- No --> H[ensureAudioFileExists for each WAL]
    H --> I{Any WAL missing?}
    I -- Yes --> E2
    I -- No --> J[Return local AudioSources list]
    E --> K[Fall back to GCS cloud URLs]
    E2 --> K
    J --> L[ConcatenatingAudioSource from WAL files]
    K --> M[ConcatenatingAudioSource from GCS URLs]
    L --> N[setAudioSource]
    M --> N
    N --> O["_trackStartOffsets from audioFiles ⚠️ May not match WAL count"]
Loading

Reviews (1): Last reviewed commit: "feat play conversation audio from local ..." | Re-trigger Greptile

Comment on lines 54 to +63
void initState() {
super.initState();
_calculateTotalDuration();
_setupAudioPlayer();
}

@override
void didChangeDependencies() {
super.didChangeDependencies();
_syncProvider = context.read<SyncProvider>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 _syncProvider is null when _buildLocalAudioSources() first runs

_setupAudioPlayer() is called from initState(), but _syncProvider is only set in didChangeDependencies(), which runs after initState(). Inside _buildLocalAudioSources(), _syncProvider?.allWals ?? [] evaluates to [] (null), conversationWals is immediately empty, and the method returns null synchronously — before any await is hit. The continuation of _setupAudioPlayer() resumes as a microtask after didChangeDependencies() sets _syncProvider, but by then localSources is already committed as null. Local WAL files are therefore never used on the initial load; the feature only activates on a manual retry.

Move _setupAudioPlayer() to didChangeDependencies() so _syncProvider is guaranteed to be set before the lookup:

Suggested change
void initState() {
super.initState();
_calculateTotalDuration();
_setupAudioPlayer();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_syncProvider = context.read<SyncProvider>();
@override
void initState() {
super.initState();
_calculateTotalDuration();
// _setupAudioPlayer() is called from didChangeDependencies on first run
}
bool _initialSetupDone = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_syncProvider = context.read<SyncProvider>();
if (!_initialSetupDone) {
_initialSetupDone = true;
_setupAudioPlayer();
}
}

Comment on lines +176 to +210
Future<List<AudioSource>?> _buildLocalAudioSources() async {
try {
final List<Wal> allWals = _syncProvider?.allWals ?? [];
final conversationWals = allWals
.where(
(w) =>
w.conversationId == widget.conversation.id &&
w.storage == WalStorage.disk &&
w.filePath != null &&
w.filePath!.isNotEmpty,
)
.toList()
..sort((a, b) => a.timerStart.compareTo(b.timerStart));

if (conversationWals.isEmpty) return null;

final audioUtils = AudioPlayerUtils.instance;
final sources = <AudioSource>[];
for (final wal in conversationWals) {
final localPath = await audioUtils.ensureAudioFileExists(wal);
if (localPath == null || !File(localPath).existsSync()) {
// A WAL file is missing or unconvertible — fall back to cloud entirely
Logger.debug('Local WAL file missing for ${wal.id}, falling back to cloud');
return null;
}
sources.add(AudioSource.uri(Uri.file(localPath)));
}

Logger.debug('Using ${sources.length} local WAL file(s) for conversation ${widget.conversation.id}');
return sources;
} catch (e) {
Logger.debug('Error resolving local WAL audio sources: $e');
return null;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Track offsets not updated to match WAL playlist

_calculateTotalDuration() builds _trackStartOffsets from conversation.audioFiles (server-side metadata). When local WALs are used, the playlist has conversationWals.length children — which may differ from audioFiles.length. A common case: 3 on-disk WAL chunks for a conversation whose server side merged them into 1 AudioFile. In this scenario:

  • _trackStartOffsets = [Duration.zero] (1 entry)
  • Playlist has 3 tracks
  • _seekToCombinedPosition always picks targetIndex = 0 (the only offset), so seeks target the first WAL chunk with an unbounded position — seeking past 20 s clamps to the end of chunk 0 and unpredictably advances rather than landing on the correct chunk.
  • _getCombinedPosition returns only within-track position instead of cumulative position, so the slider and time display drift immediately.

When localSources != null, recalculate durations from the WAL metadata instead of relying on audioFiles:

if (localSources != null) {
  // Rebuild track offsets from WAL durations so seek/display are correct
  double offset = 0;
  _trackStartOffsets = [];
  for (final wal in conversationWals) {
    _trackStartOffsets.add(Duration(milliseconds: (offset * 1000).toInt()));
    offset += wal.seconds;
  }
  _totalDuration = Duration(milliseconds: (offset * 1000).toInt());
  playlist = ConcatenatingAudioSource(useLazyPreparation: true, children: localSources);
}

(conversationWals would need to be returned alongside localSources, or held as a field.)

- Move _setupAudioPlayer() call from initState to didChangeDependencies
  (guarded by _initialSetupDone) so _syncProvider is set before
  _buildLocalAudioSources() reads allWals — previously it always returned
  null on first load
- Change _buildLocalAudioSources return type to (List<AudioSource>, List<Wal>)?
  and recalculate _trackStartOffsets/_totalDuration from WAL seconds when
  local sources are used — fixes seek and progress display when WAL chunk
  count differs from server audioFiles count

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@krushnarout krushnarout requested a review from mdmohsin7 April 23, 2026 12:54
@mdmohsin7
Copy link
Copy Markdown
Member

Wee should rather save those files to gcp while syncing them through the local sync endpoint so that it's consistent

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Use local WAL files for audio playback when 'Store Audio on Phone' is enabled

2 participants