Skip to content

Add AudioRecorder with startup test recording#3226

Open
nic-olo wants to merge 7 commits into
devfrom
cursor/08828ca3
Open

Add AudioRecorder with startup test recording#3226
nic-olo wants to merge 7 commits into
devfrom
cursor/08828ca3

Conversation

@nic-olo

@nic-olo nic-olo commented Jun 22, 2026

Copy link
Copy Markdown
Member

Summary

  • Introduce AudioRecorder — a thread-safe wrapper around Android's AudioRecord that captures 16 kHz mono PCM on a dedicated HandlerThread and delivers chunks via the existing AudioChunkCallback interface
  • Wire the previously-stubbed MicrophoneCommandHandler to start/stop the recorder and toggle VAD mode in response to BLE set_mic_state / set_mic_vad_state commands; register the handler in CommandProcessor
  • Add a startup smoke-test: 10 s after AsgClientService.onCreate() completes the recorder runs for 30 s then stops automatically

Test plan

  • Build and sideload the APK onto Mentra Live glasses
  • Confirm logcat shows Audio capture started ~10 s after boot, then Audio capture stopped ~30 s later
  • Send BLE command set_mic_state { "enabled": true } from the phone and confirm recording starts
  • Send set_mic_state { "enabled": false } and confirm recording stops
  • Send set_mic_vad_state { "enabled": true/false } and confirm VAD flag toggles in logcat
  • Remove the scheduleStartupTestRecording() call before merging if this was only for validation

Made with Cursor

Introduce AudioRecorder, a thread-safe wrapper around AudioRecord that
captures 16 kHz mono PCM from the device microphone on a dedicated
HandlerThread and delivers chunks via AudioChunkCallback.

Wire MicrophoneCommandHandler (previously stubbed) to start/stop the
recorder and toggle VAD mode in response to BLE set_mic_state and
set_mic_vad_state commands. Register the handler in CommandProcessor.

Add a startup test: 10 s after AsgClientService.onCreate() completes,
the recorder runs for 30 s then stops automatically.

Co-authored-by: Cursor <cursoragent@cursor.com>
@nic-olo nic-olo requested a review from aisraelov as a code owner June 22, 2026 09:33
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown

📋 PR Review Helper

📱 Mobile App Build

Waiting for build...

🕶️ ASG Client Build

Ready to test! (commit ccf4855)

📥 Download ASG APK


🔀 Test Locally

gh pr checkout 3226

nic-olo and others added 2 commits June 22, 2026 17:39
AudioRecord.startRecording() is void and silently discards native
errors. Add a getRecordingState() check immediately after the call;
if the recorder is not in RECORDSTATE_RECORDING, release the instance
and return false instead of posting the read loop on a dead recorder.

Root cause observed on device: native start() returned -1 (mic
resource conflict with I2S audio path), but the Java layer logged
"Audio capture started" and the read loop immediately hit
ERROR_INVALID_OPERATION (-3) on every read.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 23, 2026

Copy link
Copy Markdown

Deploying mentra-live-ota-site with  Cloudflare Pages  Cloudflare Pages

Latest commit: 16df055
Status: ✅  Deploy successful!
Preview URL: https://1ddd7ecf.mentra-live-ota-site.pages.dev
Branch Preview URL: https://cursor-08828ca3.mentra-live-ota-site.pages.dev

View logs

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown

PR Agent Orchestrator State

{
  "cycle": 7,
  "fixRound": 0,
  "totalReviewerRuns": 7,
  "consecutiveNoNewReviews": 0,
  "openFindings": [
    {
      "id": "6f4cef",
      "fingerprint": "asg_client/app/src/main/java/com/mentra/asg_client/service/core/asgclientservice.java:993b2871236a",
      "source": "depth",
      "severity": "blocking",
      "file": "asg_client/app/src/main/java/com/mentra/asg_client/service/core/AsgClientService.java",
      "line": 219,
      "message": "scheduleStartupTestRecording() is debug/test code in the production startup path: it unconditionally records the microphone for 30s on every service start, 10s after init, with no user action. Privacy/behavioral regression and contends with the legitimate set_mic_state and I2S audio paths for the single mic resource. Remove before merge.",
      "status": "open",
      "introducedCycle": 4,
      "lastSeenCycle": 4
    },
    {
      "id": "48b16b",
      "fingerprint": "asg_client/app/src/main/java/com/mentra/asg_client/audio/audiorecorder.java:dc87a9654e6c",
      "source": "depth",
      "severity": "blocking",
      "file": "asg_client/app/src/main/java/com/mentra/asg_client/audio/AudioRecorder.java",
      "line": 83,
      "message": "AudioRecorder.setCallback() is never called anywhere, so mCallback is always null in readLoop() and all captured PCM is discarded. set_mic_state:ON starts capture and returns true but delivers no audio downstream — the mic feature is non-functional end-to-end.",
      "status": "open",
      "introducedCycle": 4,
      "lastSeenCycle": 4
    },
    {
      "id": "d19c45",
      "fingerprint": "asg_client/app/src/main/java/com/mentra/asg_client/service/core/asgclientservice.java:2e1ed91b9b51",
      "source": "depth",
      "severity": "blocking",
      "file": "asg_client/app/src/main/java/com/mentra/asg_client/service/core/AsgClientService.java",
      "line": 219,
      "message": "scheduleStartupTestRecording() still in production startup path: unconditionally records the mic for 30s on every service start (20s after init) with no user action. Privacy/behavioral regression; contends with set_mic_state and the I2S path for the single mic resource. Remove before merge. (open finding 6f4cef, unresolved)",
      "status": "open",
      "introducedCycle": 6,
      "lastSeenCycle": 6
    },
    {
      "id": "382352",
      "fingerprint": "asg_client/app/src/main/java/com/mentra/asg_client/audio/audiorecorder.java:0da221804568",
      "source": "depth",
      "severity": "blocking",
      "file": "asg_client/app/src/main/java/com/mentra/asg_client/audio/AudioRecorder.java",
      "line": 102,
      "message": "AudioRecorder.setCallback() is never called anywhere in production, so mCallback is always null in readLoop() and every PCM chunk is discarded. set_mic_state:ON starts capture and returns true but delivers no audio downstream — mic feature non-functional end-to-end. (open finding 48b16b, unresolved)",
      "status": "open",
      "introducedCycle": 6,
      "lastSeenCycle": 6
    },
    {
      "id": "659daa",
      "fingerprint": "asg_client/app/src/main/java/com/mentra/asg_client/audio/audiorecorder.java:453953746e46",
      "source": "depth",
      "severity": "blocking",
      "file": "asg_client/app/src/main/java/com/mentra/asg_client/audio/AudioRecorder.java",
      "line": 183,
      "message": "Race between unsynchronized deferred start runnable and synchronized stop(): if stop() runs after the runnable's mRecording guard but before mAudioRecord is assigned (during buildAudioRecord's probing), stop() releases nothing and disables I2S, then the runnable startRecording()s an AudioRecord that readLoop never reads and nobody releases — leaked open mic handle with I2S gate torn down beneath it; next start() leaks another.",
      "status": "open",
      "introducedCycle": 6,
      "lastSeenCycle": 6
    }
  ],
  "resolvedFindings": [],
  "nitFindings": [
    {
      "id": "a82fee",
      "fingerprint": "asg_client/app/src/main/java/com/mentra/asg_client/audio/audiorecorder.java:3fe468f6e68d",
      "source": "depth",
      "severity": "nit",
      "file": "asg_client/app/src/main/java/com/mentra/asg_client/audio/AudioRecorder.java",
      "line": 93,
      "message": "setVadEnabled() only sets a flag that readLoop() never reads; VAD mode (set_mic_vad_state) has no functional effect despite reporting success.",
      "status": "open",
      "introducedCycle": 4,
      "lastSeenCycle": 4
    },
    {
      "id": "a26109",
      "fingerprint": "asg_client/app/src/main/java/com/mentra/asg_client/audio/audiorecorder.java:04e481fe0e34",
      "source": "depth",
      "severity": "nit",
      "file": "asg_client/app/src/main/java/com/mentra/asg_client/audio/AudioRecorder.java",
      "line": 282,
      "message": "readLoop() allocates a new byte[mChunkBytes] every iteration, causing avoidable GC churn on the audio hot path; reuse a single buffer.",
      "status": "open",
      "introducedCycle": 4,
      "lastSeenCycle": 4
    },
    {
      "id": "03200f",
      "fingerprint": "asg_client/app/src/main/java/com/mentra/asg_client/service/core/processors/commandprocessor.java:eb9dbb9ca234",
      "source": "depth",
      "severity": "nit",
      "file": "asg_client/app/src/main/java/com/mentra/asg_client/service/core/processors/CommandProcessor.java",
      "line": 112,
      "message": "I2S gate is a single non-refcounted boolean (lastI2sPlaying) shared by mic capture, I2SAudioCommandHandler playback, and I2SAudioController. Mic stop() can issue mh_stopi2s while playback is still active (and vice-versa a prior enable makes the mic's mh_starti2s a no-op). Add refcounting/ownership before mic + playback ship together.",
      "status": "open",
      "introducedCycle": 6,
      "lastSeenCycle": 6
    }
  ],
  "frozenPair": [
    "depth",
    "bugbot"
  ],
  "phase": "convergence",
  "status": "in_progress",
  "lastPair": [
    "depth",
    "bugbot"
  ],
  "stagnationFixRounds": 1,
  "lastOpenCount": 5,
  "fingerprintReopenCounts": {},
  "mutedFingerprints": []
}

Prefer MediaRecorder.AudioSource.UNPROCESSED when the device
advertises PROPERTY_SUPPORT_AUDIO_SOURCE_UNPROCESSED, bypassing
vendor DSP (beamforming, noise suppression, AGC, echo cancellation)
that would otherwise be applied upstream by the MTK audio stack.
Falls back to MIC on devices that do not support the unprocessed path.

Also pass Context through AudioRecorder constructor (required for
AudioManager property query) and log the active source on start.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown

🤖 PR Agent Review — cycle 7

⚠️ Changes requested · 5 blocking · 3 nits
Reviewers this cycle: depth, bugbot

Claude — depth — ⚠️ changes requested

Commander, depth review complete. Mission accomplished — recon confirms both standing blocking findings remain live at HEAD, and I've identified one additional concurrency defect. Situation report below.

Review

Open findings — both still present, NOT resolved (remain blocking):

  • scheduleStartupTestRecording() (finding 6f4cef) is intact and wired into the production startup path. AsgClientService.java:219 calls it inside service init, and the body (AsgClientService.java:1058-1084) unconditionally fires the mic for 30s, 20s after every service start, with zero user action. Confirmed privacy/behavioral regression; it grabs the same single AudioRecorder/mic resource shared with set_mic_state and the I2S path. Must be removed before merge.

  • AudioRecorder.setCallback() (finding 48b16b) is still dead. Grep across the whole module shows the only setCallback call sites are VideoRecordingSession (an unrelated class). AudioRecorder.setCallback() is never invoked, so mCallback is permanently null in readLoop() (AudioRecorder.java:336-337) and every captured PCM chunk is silently dropped. set_mic_state:ON starts capture and returns true, but no audio reaches AudioChunkCallback.onSuccess() — the mic feature is non-functional end-to-end. The class doc even says "a callback must be set first," but nothing does.

New blocking finding — start/stop race leaks the mic:

  • The deferred startup runnable in start() (AudioRecorder.java:183-205) runs on the recorder thread and is not synchronized, while stop() is synchronized. Window: the runnable passes its if (!mRecording) return; guard (line 184), then spends real time in buildAudioRecord() probing up to 16 source/rate combinations and constructing AudioRecord objects. If stop() lands during that window, it sets mRecording=false, sees mAudioRecord == null, releases nothing, and fires mI2SDisable. The runnable then proceeds to mAudioRecord = record; record.startRecording(); and enters readLoop(), which immediately exits because mRecording is false. Result: a started AudioRecord that is never stop()/release()d — leaked mic handle with the I2S gate torn down underneath it. The next start() builds a second AudioRecord on top, compounding the leak. Given the 1.5s I2S_OPEN_DELAY_MS and that set_mic_state:OFF can arrive at any time (and the startup-test path stops at a fixed offset), this window is realistically hit.

Nit / integration observation (non-blocking):

  • I2S state is shared. The mic path now drives handleI2SAudioState(true/false) via the injected callbacks (CommandProcessor.java:112-120), but the same boolean lastI2sPlaying gate (AsgClientService.java:456) is also driven by I2SAudioCommandHandler (playback) and I2SAudioController. There is no refcounting: a mic stop() will issue mh_stopi2s even if I2S audio playback is still active (and vice-versa, a prior enable_i2s makes the mic's mh_starti2s a silent no-op). Worth a shared/refcounted owner before mic and playback ship together.

Verdict: changes_requested — two standing blocking issues persist and a new mic-leak race is present.

Updated automatically by the PR Agent Orchestrator each review cycle. Nits do not block merge.

@github-actions

Copy link
Copy Markdown

bugbot run

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying mentra-store-dev with  Cloudflare Pages  Cloudflare Pages

Latest commit: 16df055
Status: ✅  Deploy successful!
Preview URL: https://d9d48a67.augmentos-appstore-2.pages.dev
Branch Preview URL: https://cursor-08828ca3.augmentos-appstore-2.pages.dev

View logs

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying dev-augmentos-console with  Cloudflare Pages  Cloudflare Pages

Latest commit: 16df055
Status: ✅  Deploy successful!
Preview URL: https://e171eab1.dev-augmentos-console.pages.dev
Branch Preview URL: https://cursor-08828ca3.dev-augmentos-console.pages.dev

View logs

this.fileManager = fileManager;
this.rgbLedCommandHandler = rgbLedCommandHandler;
this.otaCommandHandler = otaCommandHandler;
this.audioRecorder = new AudioRecorder(context);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No audio chunk consumer wired

High Severity

AudioRecorder is created and started from BLE set_mic_state, but nothing in this change calls setCallback. Capture runs and PCM is read on the recorder thread, yet every chunk is dropped because mCallback stays null, so microphone streaming to the phone cannot work.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f510c72. Configure here.

}

// Schedule a 30-second test recording that starts 10 seconds after service init.
scheduleStartupTestRecording();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Validation recording left in onCreate

Medium Severity

scheduleStartupTestRecording() is invoked from onCreate() for every production boot, starting unsolicited 30-second microphone capture ~10 seconds after startup. The PR test plan says to remove this call before merge, but it is still wired into the main service path.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f510c72. Configure here.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying prod-augmentos-account with  Cloudflare Pages  Cloudflare Pages

Latest commit: 16df055
Status: ✅  Deploy successful!
Preview URL: https://6a02a51b.augmentos-e84.pages.dev
Branch Preview URL: https://cursor-08828ca3.augmentos-e84.pages.dev

View logs

nic-olo added 3 commits June 24, 2026 11:55
…te control for Mentra Live

Changed the audio source to MediaRecorder.AudioSource.MIC, as the unprocessed path is not available on Mentra Live devices. Introduced optional I2S gate callbacks to manage the microphone routing through the BES MCU's I2S bus, ensuring proper initialization before audio recording starts. Updated sample rate to 44.1 kHz to match factory test configurations and adjusted buffer handling accordingly.
Updated the startup test recording to begin 20 seconds after service initialization instead of 10 seconds, ensuring better verification of microphone availability. Adjusted logging to provide clearer error messages in case of microphone unavailability. This change enhances the reliability of audio recording during service startup.
Implemented I2S audio callbacks in the CommandProcessor to manage microphone routing through the BES MCU's I2S bus. This ensures proper initialization of audio recording by notifying the AsgClientService when the I2S audio state changes. This change enhances the audio processing capabilities for Mentra Live devices.
@github-actions

Copy link
Copy Markdown

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

There are 4 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ccf4855. Configure here.

+ " " + record.getSampleRate() + " Hz, " + record.getBufferSizeInFrames()
+ " frames buffer");
readLoop();
}, I2S_OPEN_DELAY_MS);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stop races deferred AudioRecord init

High Severity

start() sets mRecording and posts delayed work on the recorder thread, but that runnable only checks mRecording once at entry. If stop() runs while buildAudioRecord() is still probing, mAudioRecord may still be null so stop() skips release, yet the runnable can still call startRecording() afterward with mRecording already false, leaving an active AudioRecord that nothing stops.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ccf4855. Configure here.

},
() -> {
AsgClientService svc = AsgClientService.getInstance();
if (svc != null) svc.handleI2SAudioState(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Mic stop closes shared I2S path

High Severity

The recorder’s I2S disable callback invokes handleI2SAudioState(false), which sends mh_stopi2s and updates global lastI2sPlaying. Stopping the mic after or during I2S speaker playback can tear down the same BES I2S route playback still relies on, with no reference counting between mic capture and I2SAudioController playback.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ccf4855. Configure here.

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.

1 participant