@mentra/engine 3.2.1-dev.278 → 3.2.1-dev.282

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/build/generated/releaseMetadata.js +5 -5
  2. package/build/generated/releaseMetadata.js.map +1 -1
  3. package/build/services/AcsMeetingService.d.ts +56 -6
  4. package/build/services/AcsMeetingService.d.ts.map +1 -1
  5. package/build/services/AcsMeetingService.js +162 -40
  6. package/build/services/AcsMeetingService.js.map +1 -1
  7. package/build/services/CallGainSweep.d.ts +97 -0
  8. package/build/services/CallGainSweep.d.ts.map +1 -0
  9. package/build/services/CallGainSweep.js +249 -0
  10. package/build/services/CallGainSweep.js.map +1 -0
  11. package/build/services/GlassesMicProbe.d.ts +2 -0
  12. package/build/services/GlassesMicProbe.d.ts.map +1 -1
  13. package/build/services/GlassesMicProbe.js +20 -19
  14. package/build/services/GlassesMicProbe.js.map +1 -1
  15. package/build/services/LocalMiniappRuntime.d.ts +18 -0
  16. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  17. package/build/services/LocalMiniappRuntime.js +131 -1
  18. package/build/services/LocalMiniappRuntime.js.map +1 -1
  19. package/build/services/MicSessionManager.d.ts +83 -0
  20. package/build/services/MicSessionManager.d.ts.map +1 -0
  21. package/build/services/MicSessionManager.js +201 -0
  22. package/build/services/MicSessionManager.js.map +1 -0
  23. package/build/services/MicStateCoordinator.d.ts +71 -5
  24. package/build/services/MicStateCoordinator.d.ts.map +1 -1
  25. package/build/services/MicStateCoordinator.js +161 -10
  26. package/build/services/MicStateCoordinator.js.map +1 -1
  27. package/build/services/micPolicy.d.ts +131 -0
  28. package/build/services/micPolicy.d.ts.map +1 -0
  29. package/build/services/micPolicy.js +140 -0
  30. package/build/services/micPolicy.js.map +1 -0
  31. package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
  32. package/build/stores/bluetoothSettingKeys.js +3 -0
  33. package/build/stores/bluetoothSettingKeys.js.map +1 -1
  34. package/build/stores/settings.d.ts +5 -0
  35. package/build/stores/settings.d.ts.map +1 -1
  36. package/build/stores/settings.js +62 -0
  37. package/build/stores/settings.js.map +1 -1
  38. package/build/utils/pcm16.d.ts +20 -0
  39. package/build/utils/pcm16.d.ts.map +1 -1
  40. package/build/utils/pcm16.js +32 -1
  41. package/build/utils/pcm16.js.map +1 -1
  42. package/package.json +8 -8
  43. package/src/generated/releaseMetadata.ts +5 -5
  44. package/src/services/AcsMeetingService.ts +167 -40
  45. package/src/services/CallGainSweep.ts +318 -0
  46. package/src/services/GlassesMicProbe.ts +20 -17
  47. package/src/services/LocalMiniappRuntime.ts +152 -3
  48. package/src/services/MicSessionManager.ts +258 -0
  49. package/src/services/MicStateCoordinator.ts +153 -10
  50. package/src/services/micPolicy.ts +198 -0
  51. package/src/stores/bluetoothSettingKeys.ts +3 -0
  52. package/src/stores/settings.ts +66 -0
  53. package/src/utils/pcm16.ts +43 -1
@@ -28,11 +28,28 @@ export function pcmDataView(frame) {
28
28
  }
29
29
  return null;
30
30
  }
31
+ /** Signed 16-bit full scale. `-32768` abs-counts as 32768 and is also a rail. */
32
+ export const PCM16_FULL_SCALE = 32767;
33
+ /** Headroom line: loud speech that is not yet hard-clipped. */
34
+ export const PCM16_NEAR_CLIP = 30000;
35
+ /** Percentages for a closed window. Zero samples → all percents 0. */
36
+ export function pcm16WindowStats(level) {
37
+ const denom = level.samples || 1;
38
+ const pct = (count) => Math.round((count / denom) * 1000) / 10;
39
+ return {
40
+ ...level,
41
+ peakPct: Math.round((level.peak / PCM16_FULL_SCALE) * 1000) / 10,
42
+ clipPct: level.samples ? pct(level.clipped) : 0,
43
+ nearClipPct: level.samples ? pct(level.nearClip) : 0,
44
+ };
45
+ }
31
46
  /** Level statistics over a batch of PCM16 frames. Unreadable frames are skipped, never thrown on. */
32
47
  export function summarizePcm16(frames) {
33
48
  let sum = 0;
34
49
  let peak = 0;
35
50
  let samples = 0;
51
+ let clipped = 0;
52
+ let nearClip = 0;
36
53
  for (const frame of frames) {
37
54
  const view = pcmDataView(frame);
38
55
  if (!view)
@@ -43,10 +60,14 @@ export function summarizePcm16(frames) {
43
60
  sum += v;
44
61
  if (v > peak)
45
62
  peak = v;
63
+ if (v >= PCM16_FULL_SCALE)
64
+ clipped++;
65
+ if (v >= PCM16_NEAR_CLIP)
66
+ nearClip++;
46
67
  }
47
68
  samples += n;
48
69
  }
49
- return { meanAbs: samples ? Math.round(sum / samples) : 0, peak, samples };
70
+ return { meanAbs: samples ? Math.round(sum / samples) : 0, peak, samples, clipped, nearClip };
50
71
  }
51
72
  /**
52
73
  * Incremental accumulator for the same statistics, for hot paths that see one frame at a time
@@ -56,6 +77,8 @@ export class Pcm16LevelMeter {
56
77
  sum = 0;
57
78
  peakValue = 0;
58
79
  count = 0;
80
+ clipped = 0;
81
+ nearClip = 0;
59
82
  add(frame) {
60
83
  const view = pcmDataView(frame);
61
84
  if (!view)
@@ -66,6 +89,10 @@ export class Pcm16LevelMeter {
66
89
  this.sum += v;
67
90
  if (v > this.peakValue)
68
91
  this.peakValue = v;
92
+ if (v >= PCM16_FULL_SCALE)
93
+ this.clipped++;
94
+ if (v >= PCM16_NEAR_CLIP)
95
+ this.nearClip++;
69
96
  }
70
97
  this.count += n;
71
98
  }
@@ -75,10 +102,14 @@ export class Pcm16LevelMeter {
75
102
  meanAbs: this.count ? Math.round(this.sum / this.count) : 0,
76
103
  peak: this.peakValue,
77
104
  samples: this.count,
105
+ clipped: this.clipped,
106
+ nearClip: this.nearClip,
78
107
  };
79
108
  this.sum = 0;
80
109
  this.peakValue = 0;
81
110
  this.count = 0;
111
+ this.clipped = 0;
112
+ this.nearClip = 0;
82
113
  return level;
83
114
  }
84
115
  }
@@ -1 +1 @@
1
- {"version":3,"file":"pcm16.js","sourceRoot":"","sources":["../../src/utils/pcm16.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Level statistics for 16-bit little-endian mono PCM, as delivered by `mic_pcm`.\n *\n * Shared by the call uplink and the dev mic probe so both report the same number: a soak that\n * says `meanAbs=40` in one log and `meanAbs=40` in the other is measuring the same thing.\n */\n\n/**\n * Hermes / the Expo bridge delivers `mic_pcm.pcm` as a Uint8Array, not an ArrayBuffer.\n * `new DataView(uint8Array)` throws `buffer must be an ArrayBuffer` — that is the redbox\n * attributed to whatever screen happens to be up (wifi scan, home, …).\n */\nexport function pcmDataView(frame: unknown): DataView | null {\n if (frame instanceof ArrayBuffer) {\n return new DataView(frame)\n }\n if (ArrayBuffer.isView(frame)) {\n try {\n return new DataView(frame.buffer, frame.byteOffset, frame.byteLength)\n } catch {\n const copy = Uint8Array.from(frame as unknown as ArrayLike<number>)\n return new DataView(copy.buffer)\n }\n }\n if (Array.isArray(frame)) {\n const copy = Uint8Array.from(frame)\n return new DataView(copy.buffer)\n }\n return null\n}\n\nexport type Pcm16Level = {\n /** Mean absolute sample value (16-bit scale). ~30–60 is a quiet room on Mentra Live LC3. */\n meanAbs: number\n /** Largest absolute sample. */\n peak: number\n /** Samples counted. */\n samples: number\n}\n\n/** Level statistics over a batch of PCM16 frames. Unreadable frames are skipped, never thrown on. */\nexport function summarizePcm16(frames: unknown[]): Pcm16Level {\n let sum = 0\n let peak = 0\n let samples = 0\n for (const frame of frames) {\n const view = pcmDataView(frame)\n if (!view) continue\n const n = Math.floor(view.byteLength / 2)\n for (let i = 0; i < n; i++) {\n const v = Math.abs(view.getInt16(i * 2, true))\n sum += v\n if (v > peak) peak = v\n }\n samples += n\n }\n return {meanAbs: samples ? Math.round(sum / samples) : 0, peak, samples}\n}\n\n/**\n * Incremental accumulator for the same statistics, for hot paths that see one frame at a time\n * and report once a window (the call uplink logs every 5 s at 20 frames/s).\n */\nexport class Pcm16LevelMeter {\n private sum = 0\n private peakValue = 0\n private count = 0\n\n add(frame: unknown): void {\n const view = pcmDataView(frame)\n if (!view) return\n const n = Math.floor(view.byteLength / 2)\n for (let i = 0; i < n; i++) {\n const v = Math.abs(view.getInt16(i * 2, true))\n this.sum += v\n if (v > this.peakValue) this.peakValue = v\n }\n this.count += n\n }\n\n /** Read the window and start a new one. */\n take(): Pcm16Level {\n const level = {\n meanAbs: this.count ? Math.round(this.sum / this.count) : 0,\n peak: this.peakValue,\n samples: this.count,\n }\n this.sum = 0\n this.peakValue = 0\n this.count = 0\n return level\n }\n}\n"]}
1
+ {"version":3,"file":"pcm16.js","sourceRoot":"","sources":["../../src/utils/pcm16.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Level statistics for 16-bit little-endian mono PCM, as delivered by `mic_pcm`.\n *\n * Shared by the call uplink and the dev mic probe so both report the same number: a soak that\n * says `meanAbs=40` in one log and `meanAbs=40` in the other is measuring the same thing.\n */\n\n/**\n * Hermes / the Expo bridge delivers `mic_pcm.pcm` as a Uint8Array, not an ArrayBuffer.\n * `new DataView(uint8Array)` throws `buffer must be an ArrayBuffer` — that is the redbox\n * attributed to whatever screen happens to be up (wifi scan, home, …).\n */\nexport function pcmDataView(frame: unknown): DataView | null {\n if (frame instanceof ArrayBuffer) {\n return new DataView(frame)\n }\n if (ArrayBuffer.isView(frame)) {\n try {\n return new DataView(frame.buffer, frame.byteOffset, frame.byteLength)\n } catch {\n const copy = Uint8Array.from(frame as unknown as ArrayLike<number>)\n return new DataView(copy.buffer)\n }\n }\n if (Array.isArray(frame)) {\n const copy = Uint8Array.from(frame)\n return new DataView(copy.buffer)\n }\n return null\n}\n\n/** Signed 16-bit full scale. `-32768` abs-counts as 32768 and is also a rail. */\nexport const PCM16_FULL_SCALE = 32767\n/** Headroom line: loud speech that is not yet hard-clipped. */\nexport const PCM16_NEAR_CLIP = 30000\n\nexport type Pcm16Level = {\n /** Mean absolute sample value (16-bit scale). ~30–60 is a quiet room on Mentra Live LC3. */\n meanAbs: number\n /** Largest absolute sample. */\n peak: number\n /** Samples counted. */\n samples: number\n /** Samples at the int16 rail (`>= 32767`). */\n clipped: number\n /** Samples at or above [PCM16_NEAR_CLIP]. */\n nearClip: number\n}\n\nexport type Pcm16WindowStats = Pcm16Level & {\n /** `peak / 32767`, percent. */\n peakPct: number\n /** `clipped / samples`, percent. */\n clipPct: number\n /** `nearClip / samples`, percent. */\n nearClipPct: number\n}\n\n/** Percentages for a closed window. Zero samples → all percents 0. */\nexport function pcm16WindowStats(level: Pcm16Level): Pcm16WindowStats {\n const denom = level.samples || 1\n const pct = (count: number) => Math.round((count / denom) * 1000) / 10\n return {\n ...level,\n peakPct: Math.round((level.peak / PCM16_FULL_SCALE) * 1000) / 10,\n clipPct: level.samples ? pct(level.clipped) : 0,\n nearClipPct: level.samples ? pct(level.nearClip) : 0,\n }\n}\n\n/** Level statistics over a batch of PCM16 frames. Unreadable frames are skipped, never thrown on. */\nexport function summarizePcm16(frames: unknown[]): Pcm16Level {\n let sum = 0\n let peak = 0\n let samples = 0\n let clipped = 0\n let nearClip = 0\n for (const frame of frames) {\n const view = pcmDataView(frame)\n if (!view) continue\n const n = Math.floor(view.byteLength / 2)\n for (let i = 0; i < n; i++) {\n const v = Math.abs(view.getInt16(i * 2, true))\n sum += v\n if (v > peak) peak = v\n if (v >= PCM16_FULL_SCALE) clipped++\n if (v >= PCM16_NEAR_CLIP) nearClip++\n }\n samples += n\n }\n return {meanAbs: samples ? Math.round(sum / samples) : 0, peak, samples, clipped, nearClip}\n}\n\n/**\n * Incremental accumulator for the same statistics, for hot paths that see one frame at a time\n * and report once a window (the call uplink logs every 5 s at 20 frames/s).\n */\nexport class Pcm16LevelMeter {\n private sum = 0\n private peakValue = 0\n private count = 0\n private clipped = 0\n private nearClip = 0\n\n add(frame: unknown): void {\n const view = pcmDataView(frame)\n if (!view) return\n const n = Math.floor(view.byteLength / 2)\n for (let i = 0; i < n; i++) {\n const v = Math.abs(view.getInt16(i * 2, true))\n this.sum += v\n if (v > this.peakValue) this.peakValue = v\n if (v >= PCM16_FULL_SCALE) this.clipped++\n if (v >= PCM16_NEAR_CLIP) this.nearClip++\n }\n this.count += n\n }\n\n /** Read the window and start a new one. */\n take(): Pcm16Level {\n const level = {\n meanAbs: this.count ? Math.round(this.sum / this.count) : 0,\n peak: this.peakValue,\n samples: this.count,\n clipped: this.clipped,\n nearClip: this.nearClip,\n }\n this.sum = 0\n this.peakValue = 0\n this.count = 0\n this.clipped = 0\n this.nearClip = 0\n return level\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/engine",
3
- "version": "3.2.1-dev.278",
3
+ "version": "3.2.1-dev.282",
4
4
  "description": "Mentra Engine — miniapp registry, WebView bridge, and miniapp store",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -97,13 +97,13 @@
97
97
  "registry": "https://registry.npmjs.org/"
98
98
  },
99
99
  "dependencies": {
100
- "@mentra/acs-meeting": "3.2.1-dev.278",
101
- "@mentra/glasses-media": "3.2.1-dev.278",
102
- "@mentra/bluetooth-sdk": "3.2.1-dev.278",
103
- "@mentra/cloud-client": "3.2.1-dev.278",
104
- "@mentra/cloud-protocol": "3.2.1-dev.278",
105
- "@mentra/crust": "3.2.1-dev.278",
106
- "@mentra/miniapp": "3.2.1-dev.278",
100
+ "@mentra/acs-meeting": "3.2.1-dev.282",
101
+ "@mentra/glasses-media": "3.2.1-dev.282",
102
+ "@mentra/bluetooth-sdk": "3.2.1-dev.282",
103
+ "@mentra/cloud-client": "3.2.1-dev.282",
104
+ "@mentra/cloud-protocol": "3.2.1-dev.282",
105
+ "@mentra/crust": "3.2.1-dev.282",
106
+ "@mentra/miniapp": "3.2.1-dev.282",
107
107
  "buffer": "^6.0.3",
108
108
  "events": "^3.3.0",
109
109
  "react-native-marked": "8.1.1",
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
12
12
  export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "3.2.1",
15
- "releaseIdentity": "3.2.1-dev.278",
16
- "releaseSetId": "mentra-3.2.1-dev.278",
17
- "sourceCommit": "3fdfe01c096c79797d51fdef608ddb8ed91e14c3",
18
- "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-dev.278.json",
19
- "otaManifestSha256": "1ab2a29845eb5b865b15ad16e2006d34a2f9590dcade5bf085722c4aba0ca978"
15
+ "releaseIdentity": "3.2.1-dev.282",
16
+ "releaseSetId": "mentra-3.2.1-dev.282",
17
+ "sourceCommit": "53defbe045377507bdd6c188d303be138040c450",
18
+ "otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-dev.282.json",
19
+ "otaManifestSha256": "c69e779261aac872c9b0a26abbdb2d7e6549db577ee4bf0755adf770d8f879f6"
20
20
  })
@@ -9,9 +9,12 @@ import {Platform} from "react-native"
9
9
  import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
10
10
 
11
11
  import audioPlaybackService from "./AudioPlaybackService"
12
+ import {getCallGainSweep} from "./CallGainSweep"
12
13
  import micStateCoordinator from "./MicStateCoordinator"
14
+ import micSessionManager, {type MicSession} from "./MicSessionManager"
15
+ import {ENGINE_OWNER_PREFIX} from "./micPolicy"
13
16
  import {SETTINGS, useSettingsStore} from "../stores/settings"
14
- import {Pcm16LevelMeter} from "../utils/pcm16"
17
+ import {Pcm16LevelMeter, pcm16WindowStats} from "../utils/pcm16"
15
18
  import {softapTrace, softapTraceFailure, softapTraceId} from "../utils/softapTrace"
16
19
  import {ACS_CALL_MIC, type AcsAudioSource, type ResolvedAudioSource, type SourceReason} from "./acsAudioSource"
17
20
  import type {SoftapProgress} from "./SoftapCallTransport"
@@ -204,8 +207,13 @@ export function glassesLc3UplinkSupported(args: {
204
207
  audioSource: AcsAudioSource
205
208
  hasPushOutgoingPcm: boolean
206
209
  platform: string
210
+ /** Somebody holds a glasses microphone session through MicSessionManager. */
211
+ glassesSession: boolean
207
212
  }): boolean {
208
213
  if (!(softapBleLc3UplinkForTests ?? SOFTAP_BLE_LC3_UPLINK)) return false
214
+ // The microphone belongs to whoever leased it. Without a lease nothing has pinned the glasses
215
+ // or claimed raw PCM, so this call has no wearer audio to read off them.
216
+ if (!args.glassesSession) return false
209
217
  // iOS has no `setMicSourcePin` yet, so it cannot promise the phone microphone stays shut.
210
218
  if (args.platform !== "android") return false
211
219
  // WHEP audio comes back from Cloudflare already mixed into the subscribed track; there is no
@@ -537,6 +545,8 @@ const GLASSES_MIC_SOURCE = "glasses"
537
545
  const GLASSES_MIC_GRACE_MS = 1000
538
546
  /** Cadence of the uplink health line, matching the native P8 ladder. */
539
547
  const MIC_UPLINK_LOG_INTERVAL_MS = 5000
548
+ /** During a gain sweep, one-second windows so each 25 s phase has enough speech samples. */
549
+ const MIC_UPLINK_SWEEP_LOG_INTERVAL_MS = 1000
540
550
  /** A 50 ms LC3 frame arriving more than this late is a missed beat, not jitter. */
541
551
  const MIC_GAP_WARN_MS = 90
542
552
 
@@ -612,6 +622,17 @@ class AcsMeetingService {
612
622
  private callOrigin: AcsCallOrigin = "unknown"
613
623
  private micTransport: MicTransport = "whip"
614
624
  private micSub: {remove: () => void} | null = null
625
+ /** Backstop lease, held only while this call is actually reading the glasses mic. */
626
+ private micSession: MicSession | null = null
627
+ private micTuningSub: {remove: () => void} | null = null
628
+ private micRmsSub: {remove: () => void} | null = null
629
+ /** Only turn telemetry off again if this call is what turned it on. */
630
+ private micRmsEnabled = false
631
+ private gateSamples = 0
632
+ private gateClosed = 0
633
+ private gateElevated = 0
634
+ private gateClosedElevated = 0
635
+ private gateRmsMax = 0
615
636
  /** True between the pin/requirement being taken and released, so release is exactly once. */
616
637
  private micUplinkActive = false
617
638
  private micFramesForwarded = 0
@@ -972,6 +993,12 @@ class AcsMeetingService {
972
993
  displayName?: string
973
994
  video?: AcsOutgoingVideo
974
995
  origin?: AcsCallOrigin
996
+ /**
997
+ * Whether the caller holds a glasses microphone session. The uplink is the wearer's voice,
998
+ * so it is only taken off the glasses when somebody has actually claimed that microphone
999
+ * through MicSessionManager; this class never claims it itself.
1000
+ */
1001
+ glassesSession?: boolean
975
1002
  },
976
1003
  ): Promise<MeetingState> {
977
1004
  const native = getNative()
@@ -998,6 +1025,7 @@ class AcsMeetingService {
998
1025
  audioSource: resolved.source,
999
1026
  hasPushOutgoingPcm: typeof native.pushOutgoingPcm === "function",
1000
1027
  platform: Platform.OS,
1028
+ glassesSession: args.glassesSession ?? false,
1001
1029
  })
1002
1030
  this.micTransport = lc3Uplink ? "ble-lc3" : resolved.source === "phone" ? "phone" : "whip"
1003
1031
  this.bindNative(native, packageName)
@@ -1287,11 +1315,15 @@ class AcsMeetingService {
1287
1315
  /**
1288
1316
  * Start forwarding the glasses microphone into ACS for this call.
1289
1317
  *
1290
- * Order matters and is the whole point: pin the Bluetooth SDK to the glasses *before* asking it
1291
- * for PCM, so the first frame the mic requirement produces is already from the right source and
1292
- * the phone microphone is never opened even for one buffer. `generation` is captured by the
1293
- * listener so a frame that lands after this call ended is dropped rather than pushed at a native
1294
- * session that has left the meeting.
1318
+ * This class is a sink: it never names a gain, a pin or a setting. It does take a semantic
1319
+ * voice_call lease first, because reading the glasses mic without one is what put this call on
1320
+ * the OS default of VAD-on a speech gate that drops the wearer's uplink whenever the GX8002
1321
+ * disagrees, which during a call is most of the time the far end is talking. The lease is a
1322
+ * backstop: when the miniapp already holds one this simply merges with it, and micPolicy still
1323
+ * decides what voice_call means for the hardware.
1324
+ *
1325
+ * `generation` is captured by the listener so a frame that lands after this call ended is
1326
+ * dropped rather than pushed at a native session that has left the meeting.
1295
1327
  */
1296
1328
  private startGlassesMicUplink(generation: number): void {
1297
1329
  if (this.micTransport !== "ble-lc3") return
@@ -1310,11 +1342,9 @@ class AcsMeetingService {
1310
1342
  this.micLevel.take()
1311
1343
  this.lastMicLevel = null
1312
1344
  try {
1313
- void Promise.resolve(BluetoothSdk.setMicSourcePin?.(GLASSES_MIC_SOURCE)).catch((error) => {
1314
- console.warn("[AcsMeeting] pinning the glasses microphone failed", error)
1315
- })
1345
+ this.acquireMicSession()
1346
+ this.startMicGateTelemetry()
1316
1347
  this.micUplinkActive = true
1317
- micStateCoordinator.setCallRequirement(true)
1318
1348
  this.micSub = BluetoothSdk.addListener("mic_pcm", (event: {pcm?: ArrayBuffer; sampleRate?: number; source?: string}) => {
1319
1349
  if (generation !== this.callGeneration) {
1320
1350
  this.micDropsStale += 1
@@ -1362,29 +1392,132 @@ class AcsMeetingService {
1362
1392
  }
1363
1393
  }
1364
1394
 
1365
- /** Release the microphone claims this call took. Safe to call when it never took them. */
1395
+ /**
1396
+ * Stop reading the glasses microphone. Safe to call when this call never started.
1397
+ *
1398
+ * Only this call's own backstop lease is dropped. A lease the miniapp took is its to release,
1399
+ * and letting go of ours before that owner releases is what keeps a normal hang-up from looking
1400
+ * like the wearer's microphone disappearing.
1401
+ */
1366
1402
  private stopGlassesMicUplink(): void {
1367
1403
  this.micSub?.remove()
1368
1404
  this.micSub = null
1405
+ this.releaseMicSession()
1369
1406
  if (this.micUplinkActive) {
1370
1407
  this.micUplinkActive = false
1371
- micStateCoordinator.setCallRequirement(false)
1372
- // Last, and unconditionally: while the pin is set no other consumer can pick a microphone,
1373
- // so leaving it behind would leave captions and the cloud uplink stuck on the glasses.
1374
- void Promise.resolve(BluetoothSdk.setMicSourcePin?.(null)).catch((error) => {
1375
- console.warn("[AcsMeeting] releasing the glasses microphone pin failed", error)
1376
- })
1377
1408
  console.log("[AcsMeeting] phase=glasses-mic-uplink-stop", {
1378
1409
  frames: this.micFramesForwarded,
1379
1410
  dropsStale: this.micDropsStale,
1380
1411
  dropsNonGlasses: this.micDropsNonGlasses,
1381
1412
  gaps: this.micGaps,
1382
1413
  gapMsMax: this.micGapMsMax,
1414
+ ...this.gateSummary(),
1383
1415
  })
1384
1416
  }
1417
+ this.stopMicGateTelemetry()
1385
1418
  this.micTransport = "whip"
1386
1419
  }
1387
1420
 
1421
+ /**
1422
+ * Watch what the glasses are actually running for the length of the call.
1423
+ *
1424
+ * Two different questions, both unanswerable from this side otherwise. `mic_tuning_state` is
1425
+ * the post-clamp reply, so it catches a profile the firmware rewrote rather than accepted.
1426
+ * `mic_rms` carries the Barrier's own verdict per frame, which is the only way to tell a gate
1427
+ * that is suppressing speaker leak from one that is suppressing the wearer.
1428
+ */
1429
+ private startMicGateTelemetry(): void {
1430
+ this.gateSamples = 0
1431
+ this.gateClosed = 0
1432
+ this.gateElevated = 0
1433
+ this.gateClosedElevated = 0
1434
+ this.gateRmsMax = 0
1435
+
1436
+ try {
1437
+ this.micTuningSub = BluetoothSdk.addListener("mic_tuning_state", (event: Record<string, unknown>) => {
1438
+ console.log("[AcsMeeting] phase=glasses-mic-tuning-applied", event)
1439
+ })
1440
+ this.micRmsSub = BluetoothSdk.addListener(
1441
+ "mic_rms",
1442
+ (event: {rms?: number; gateOpen?: boolean; speakerElevated?: boolean}) => {
1443
+ this.gateSamples += 1
1444
+ if (event.gateOpen === false) this.gateClosed += 1
1445
+ if (event.speakerElevated) {
1446
+ this.gateElevated += 1
1447
+ if (event.gateOpen === false) this.gateClosedElevated += 1
1448
+ }
1449
+ if (typeof event.rms === "number" && event.rms > this.gateRmsMax) this.gateRmsMax = event.rms
1450
+ },
1451
+ )
1452
+ void Promise.resolve(BluetoothSdk.setMicRmsTelemetry?.(true))
1453
+ .then(() => {
1454
+ this.micRmsEnabled = true
1455
+ })
1456
+ .catch((error) => console.warn("[AcsMeeting] mic RMS telemetry unavailable", error))
1457
+ void Promise.resolve(BluetoothSdk.requestMicTuningState?.()).catch(() => {})
1458
+ } catch (error) {
1459
+ console.warn("[AcsMeeting] mic gate telemetry unavailable", error)
1460
+ }
1461
+ }
1462
+
1463
+ private stopMicGateTelemetry(): void {
1464
+ this.micTuningSub?.remove()
1465
+ this.micTuningSub = null
1466
+ this.micRmsSub?.remove()
1467
+ this.micRmsSub = null
1468
+ if (!this.micRmsEnabled) return
1469
+ this.micRmsEnabled = false
1470
+ // Super Mode may have the readout open behind this call; it re-requests on focus.
1471
+ void Promise.resolve(BluetoothSdk.setMicRmsTelemetry?.(false)).catch(() => {})
1472
+ }
1473
+
1474
+ /** Barrier's behaviour over the window, as percentages a listening test can be checked against. */
1475
+ private gateSummary(): Record<string, number> {
1476
+ if (this.gateSamples === 0) return {}
1477
+ const pct = (n: number, of: number) => (of === 0 ? 0 : Math.round((n / of) * 1000) / 10)
1478
+ return {
1479
+ gateSamples: this.gateSamples,
1480
+ gateClosedPct: pct(this.gateClosed, this.gateSamples),
1481
+ speakerElevatedPct: pct(this.gateElevated, this.gateSamples),
1482
+ // The number that matters: closed while the far end was quiet means the wearer was cut.
1483
+ gateClosedQuietPct: pct(this.gateClosed - this.gateClosedElevated, this.gateSamples - this.gateElevated),
1484
+ gateRmsMax: this.gateRmsMax,
1485
+ }
1486
+ }
1487
+
1488
+ /**
1489
+ * Guarantee a voice_call session for as long as this call reads the glasses mic.
1490
+ *
1491
+ * The miniapp asking for one is the intended path; this covers the builds where it cannot,
1492
+ * because a bundled Call that predates MIC_ACQUIRE joins anyway rather than failing. Without
1493
+ * this the uplink runs under whatever the OS settings say, and the shipped default is VAD-on.
1494
+ */
1495
+ private acquireMicSession(): void {
1496
+ if (this.micSession) return
1497
+ try {
1498
+ this.micSession = micSessionManager.acquire({
1499
+ owner: `${ENGINE_OWNER_PREFIX}acs-uplink`,
1500
+ source: "glasses",
1501
+ useCase: "voice_call",
1502
+ })
1503
+ } catch (error) {
1504
+ // A source conflict means something else already owns the microphone. Forwarding whatever
1505
+ // it captures is still better than dropping the wearer from the call.
1506
+ console.warn("[AcsMeeting] glasses mic session unavailable", error)
1507
+ }
1508
+ }
1509
+
1510
+ private releaseMicSession(): void {
1511
+ const session = this.micSession
1512
+ if (!session) return
1513
+ this.micSession = null
1514
+ try {
1515
+ session.release()
1516
+ } catch (error) {
1517
+ console.warn("[AcsMeeting] releasing the glasses mic session failed", error)
1518
+ }
1519
+ }
1520
+
1388
1521
  /**
1389
1522
  * Report a call whose pinned microphone stopped delivering. There is deliberately no fallback:
1390
1523
  * the wearer agreed to be heard from the glasses, and quietly switching to the phone in the
@@ -1413,42 +1546,36 @@ class AcsMeetingService {
1413
1546
  private logMicUplink(): void {
1414
1547
  const now = Date.now()
1415
1548
  const elapsed = now - this.lastMicUplinkLogAt
1416
- if (elapsed < MIC_UPLINK_LOG_INTERVAL_MS) return
1549
+ const sweep = getCallGainSweep()
1550
+ const interval = sweep.isActive() ? MIC_UPLINK_SWEEP_LOG_INTERVAL_MS : MIC_UPLINK_LOG_INTERVAL_MS
1551
+ if (elapsed < interval) return
1417
1552
  this.lastMicUplinkLogAt = now
1418
1553
  // Level, not just cadence: a 20 Hz stream of the noise floor and a 20 Hz stream of speech
1419
1554
  // have the same framesPerSecond. Quiet room on Mentra Live LC3 is meanAbs ≈30–60.
1420
1555
  const level = this.micLevel.take()
1556
+ const stats = pcm16WindowStats(level)
1421
1557
  this.lastMicLevel = {meanAbs: level.meanAbs, peak: level.peak}
1558
+ const gain = sweep.currentGain() ?? micStateCoordinator.getSessionMicTuning()?.gain ?? null
1559
+ if (sweep.isActive()) sweep.ingest(level)
1422
1560
  console.log("[AcsMeeting] phase=glasses-mic-uplink", {
1423
1561
  framesPerSecond: Math.round((this.micFramesWindow * 1000) / elapsed),
1424
1562
  frames: this.micFramesForwarded,
1425
- meanAbs: level.meanAbs,
1426
- peak: level.peak,
1563
+ meanAbs: stats.meanAbs,
1564
+ peak: stats.peak,
1565
+ peakPct: stats.peakPct,
1566
+ clipped: stats.clipped,
1567
+ clipPct: stats.clipPct,
1568
+ nearClip: stats.nearClip,
1569
+ nearClipPct: stats.nearClipPct,
1570
+ gain,
1571
+ barrier: micStateCoordinator.getSessionLoudnessGate(),
1572
+ sweep: sweep.currentLabel(),
1427
1573
  dropsStale: this.micDropsStale,
1428
1574
  dropsNonGlasses: this.micDropsNonGlasses,
1429
1575
  gaps: this.micGaps,
1430
1576
  gapMsMax: this.micGapMsMax,
1577
+ ...this.gateSummary(),
1431
1578
  })
1432
- // #region agent log
1433
- fetch("http://127.0.0.1:7905/ingest/5a9713c9-45ff-4d09-9435-2adc5db5e91d", {
1434
- method: "POST",
1435
- headers: {"Content-Type": "application/json", "X-Debug-Session-Id": "828181"},
1436
- body: JSON.stringify({
1437
- sessionId: "828181",
1438
- runId: "run1",
1439
- hypothesisId: "E",
1440
- location: "AcsMeetingService.ts:logMicUplink",
1441
- message: "phone decoded glasses PCM window",
1442
- data: {
1443
- fps: Math.round((this.micFramesWindow * 1000) / elapsed),
1444
- meanAbs: level.meanAbs,
1445
- peak: level.peak,
1446
- frames: this.micFramesForwarded,
1447
- },
1448
- timestamp: Date.now(),
1449
- }),
1450
- }).catch(() => {})
1451
- // #endregion
1452
1579
  this.micFramesWindow = 0
1453
1580
  }
1454
1581