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

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 +22 -6
  4. package/build/services/AcsMeetingService.d.ts.map +1 -1
  5. package/build/services/AcsMeetingService.js +39 -20
  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 +198 -0
  22. package/build/services/MicSessionManager.js.map +1 -0
  23. package/build/services/MicStateCoordinator.d.ts +48 -5
  24. package/build/services/MicStateCoordinator.d.ts.map +1 -1
  25. package/build/services/MicStateCoordinator.js +91 -10
  26. package/build/services/MicStateCoordinator.js.map +1 -1
  27. package/build/services/micPolicy.d.ts +84 -0
  28. package/build/services/micPolicy.d.ts.map +1 -0
  29. package/build/services/micPolicy.js +82 -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 +45 -20
  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 +255 -0
  49. package/src/services/MicStateCoordinator.ts +91 -10
  50. package/src/services/micPolicy.ts +129 -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
@@ -1 +1 @@
1
- {"version":3,"file":"MicStateCoordinator.js","sourceRoot":"","sources":["../../src/services/MicStateCoordinator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * MicStateCoordinator\n *\n * Owns local-miniapp-driven microphone requirements.\n *\n * Local miniapps subscribe to audio_chunk / transcription streams.\n * This coordinator pushes the aggregate local requirement set to BluetoothSdk\n * so the mic runs whenever at least one local consumer needs it.\n */\n\nimport BluetoothSdk from \"@mentra/bluetooth-sdk/internal\"\n\nimport {createDebouncedPatchFlusher} from \"../utils/debouncedPatch\"\n\nconst LOG_TAG = \"MIC_COORDINATOR\"\n\ntype MicGate = \"vad\" | \"loudnessGate\"\n\ninterface GateOverride {\n enabled: boolean\n order: number\n}\n\ninterface ConfiguredMicGates {\n vadEnabled?: boolean | null\n loudnessGateEnabled?: boolean | null\n}\n\n/** Mic-requirement flips are debounced (300ms) and merged into one BLE write\n * (wire v2 keeps BLE JSON small and infrequent). */\nconst flushMicRequirementsPatch = createDebouncedPatchFlusher<Record<string, unknown>>((patch) => {\n try {\n // Resolve runtime overrides at flush time. A miniapp can acquire/release a\n // gate override during the debounce window, and a captured stale value\n // must not win after that lifecycle transition.\n const runtimePatch = micStateCoordinator.applyEffectiveGatePolicy(patch)\n void Promise.resolve(BluetoothSdk.updateBluetoothSettings(runtimePatch)).catch((err) => {\n console.error(`${LOG_TAG}: failed to apply mic requirements:`, err)\n })\n } catch (err) {\n console.error(`${LOG_TAG}: failed to apply mic requirements:`, err)\n }\n}, 300)\n\nclass MicStateCoordinator {\n private static instance: MicStateCoordinator | null = null\n\n // Local miniapp requirements (set when miniapps subscribe to audio streams)\n private localWantsPcm = false\n private localWantsLc3 = false\n /**\n * A live ACS call taking the wearer's voice off the glasses over BLE LC3.\n *\n * Tracked separately from the miniapp requirement because the two have independent lifetimes:\n * the call miniapp does not subscribe to `audio_chunk`, and a captions miniapp that stops mid-call\n * must not take the call's microphone with it.\n */\n private callWantsPcm = false\n private configuredVad: boolean | undefined\n private configuredLoudnessGate: boolean | undefined\n private readonly miniappVadOverrides = new Map<string, GateOverride>()\n private readonly miniappLoudnessGateOverrides = new Map<string, GateOverride>()\n private overrideSequence = 0\n\n private constructor() {}\n\n public static getInstance(): MicStateCoordinator {\n if (!MicStateCoordinator.instance) {\n MicStateCoordinator.instance = new MicStateCoordinator()\n }\n return MicStateCoordinator.instance\n }\n\n /**\n * Update local miniapp requirements. Called by LocalMiniappRuntime when\n * the aggregated set of local subscriptions changes.\n */\n public setLocalRequirements(req: {pcm: boolean; lc3: boolean} & ConfiguredMicGates): void {\n this.localWantsPcm = req.pcm\n this.localWantsLc3 = req.lc3\n this.rememberConfiguredGates(req)\n console.log(`${LOG_TAG}: local requirements updated — pcm=${req.pcm} lc3=${req.lc3}`)\n this.applyUnion()\n }\n\n /**\n * Claim or release raw PCM on behalf of an active call.\n *\n * Called by AcsMeetingService around a call whose uplink is the glasses microphone over BLE LC3.\n * Releasing is a claim release, not a mic shutdown: if a captions miniapp still wants PCM the\n * microphone stays on, which is the whole reason this is a separate flag rather than a setter on\n * the local requirement.\n */\n public setCallRequirement(pcm: boolean): void {\n if (this.callWantsPcm === pcm) return\n this.callWantsPcm = pcm\n console.log(`${LOG_TAG}: call requirement updated — pcm=${pcm}`)\n this.applyUnion()\n }\n\n /**\n * Whether anything on this device needs a continuous raw-PCM timeline. Also the condition that\n * forces hardware VAD off: a gate that drops silence turns a call into clipped half-words.\n */\n private get wantsRawPcm(): boolean {\n return this.localWantsPcm || this.callWantsPcm\n }\n\n /**\n * Apply a miniapp-owned gate override without changing the OS preference.\n * Overrides are lifecycle-scoped and last-live-owner-wins independently for\n * VAD and Barrier.\n */\n public async setMiniappGateOverride(\n packageName: string,\n gate: MicGate,\n enabled: boolean,\n configured: ConfiguredMicGates = {},\n ): Promise<void> {\n this.rememberConfiguredGates(configured)\n const overrides = this.overridesFor(gate)\n const previous = overrides.get(packageName)\n const next = {enabled, order: ++this.overrideSequence}\n overrides.set(packageName, next)\n\n try {\n await BluetoothSdk.updateBluetoothSettings(this.effectiveGatePatch())\n } catch (error) {\n // Do not roll back a newer request from the same package that landed\n // while this native write was in flight.\n if (overrides.get(packageName) === next) {\n if (previous) overrides.set(packageName, previous)\n else overrides.delete(packageName)\n }\n throw error\n }\n }\n\n /**\n * Remove every gate override owned by a miniapp. This is synchronous so\n * unregister can drop ownership before recomputing aggregate mic state.\n */\n public clearMiniappGateOverrides(packageName: string): boolean {\n const removedVad = this.miniappVadOverrides.delete(packageName)\n const removedLoudness = this.miniappLoudnessGateOverrides.delete(packageName)\n return removedVad || removedLoudness\n }\n\n /** Re-apply the current runtime policy after an owner is released. */\n public async syncEffectiveGatePolicy(configured: ConfiguredMicGates = {}): Promise<void> {\n this.rememberConfiguredGates(configured)\n const patch = this.effectiveGatePatch()\n if (Object.keys(patch).length === 0) return\n await BluetoothSdk.updateBluetoothSettings(patch)\n }\n\n /**\n * Preserve the runtime microphone contract when the persisted device\n * settings are replayed (for example after a glasses reconnect). Active\n * miniapp gate overrides replace the OS values, and raw PCM keeps VAD off\n * until the last raw-audio consumer unsubscribes.\n */\n public applyRuntimeOverrides(settings: Record<string, unknown>): Record<string, unknown> {\n this.rememberConfiguredGates({\n vadEnabled:\n typeof settings.voice_activity_detection_enabled === \"boolean\"\n ? settings.voice_activity_detection_enabled\n : undefined,\n loudnessGateEnabled:\n typeof settings.loudness_gate_enabled === \"boolean\" ? settings.loudness_gate_enabled : undefined,\n })\n\n return this.applyActiveRuntimeOverrides(settings)\n }\n\n /**\n * Apply the complete current gate policy without treating the input as an OS\n * preference update. Used by debounced mic-requirement writes, whose queued\n * gate values may be stale after an override is acquired or released.\n */\n public applyEffectiveGatePolicy(settings: Record<string, unknown>): Record<string, unknown> {\n return {\n ...settings,\n ...this.effectiveGatePatch(),\n }\n }\n\n private applyActiveRuntimeOverrides(settings: Record<string, unknown>): Record<string, unknown> {\n const runtimeSettings = {...settings}\n const vadOverride = this.latestOverride(this.miniappVadOverrides)\n const loudnessOverride = this.latestOverride(this.miniappLoudnessGateOverrides)\n\n if (this.wantsRawPcm) {\n runtimeSettings.voice_activity_detection_enabled = false\n } else if (vadOverride) {\n runtimeSettings.voice_activity_detection_enabled = vadOverride.enabled\n }\n if (loudnessOverride) {\n runtimeSettings.loudness_gate_enabled = loudnessOverride.enabled\n }\n\n return runtimeSettings\n }\n\n /**\n * Push local requirements to BluetoothSdk. `should_send_pcm` is strictly for\n * on-device PCM consumers; cloud audio uses LC3 through AudioCloudUplink.\n */\n private applyUnion(): void {\n const shouldSendPcm = this.wantsRawPcm\n const shouldSendLc3 = this.localWantsLc3\n\n // console.log(\n // `${LOG_TAG}: applying requirements — pcm=${shouldSendPcm} lc3=${shouldSendLc3}`,\n // )\n\n // The mic control plane is a direct btsdk call now (was a host setMicRequirements\n // hook) so a bare OEM streams audio without wiring it.\n const patch: Record<string, unknown> = {\n should_send_pcm: shouldSendPcm,\n should_send_lc3: shouldSendLc3,\n should_send_transcript: false,\n ...this.effectiveGatePatch(),\n }\n\n flushMicRequirementsPatch(patch)\n }\n\n private rememberConfiguredGates(configured: ConfiguredMicGates): void {\n // null means the connected device intentionally omits that setting.\n // undefined means the caller is not updating the remembered preference.\n if (configured.vadEnabled !== undefined) {\n this.configuredVad = configured.vadEnabled ?? undefined\n }\n if (configured.loudnessGateEnabled !== undefined) {\n this.configuredLoudnessGate = configured.loudnessGateEnabled ?? undefined\n }\n }\n\n private overridesFor(gate: MicGate): Map<string, GateOverride> {\n return gate === \"vad\" ? this.miniappVadOverrides : this.miniappLoudnessGateOverrides\n }\n\n private latestOverride(overrides: Map<string, GateOverride>): GateOverride | undefined {\n let latest: GateOverride | undefined\n for (const entry of overrides.values()) {\n if (!latest || entry.order > latest.order) latest = entry\n }\n return latest\n }\n\n private effectiveGatePatch(): Record<string, unknown> {\n const patch: Record<string, unknown> = {}\n const vadOverride = this.latestOverride(this.miniappVadOverrides)\n const loudnessOverride = this.latestOverride(this.miniappLoudnessGateOverrides)\n\n // Hardware VAD suppresses silence. Raw-audio consumers need a continuous\n // timeline, so their requirement wins over both OS and miniapp VAD values.\n if (this.wantsRawPcm) patch.voice_activity_detection_enabled = false\n else if (vadOverride) patch.voice_activity_detection_enabled = vadOverride.enabled\n else if (this.configuredVad !== undefined) patch.voice_activity_detection_enabled = this.configuredVad\n\n if (loudnessOverride) patch.loudness_gate_enabled = loudnessOverride.enabled\n else if (this.configuredLoudnessGate !== undefined) {\n patch.loudness_gate_enabled = this.configuredLoudnessGate\n }\n\n return patch\n }\n\n /**\n * Reset all requirements to off. Called during cleanup.\n */\n public reset(): void {\n this.localWantsPcm = false\n this.localWantsLc3 = false\n this.callWantsPcm = false\n this.applyUnion()\n }\n\n public cleanup(): void {\n console.log(`${LOG_TAG}: cleanup()`)\n this.miniappVadOverrides.clear()\n this.miniappLoudnessGateOverrides.clear()\n this.reset()\n MicStateCoordinator.instance = null\n }\n}\n\nconst micStateCoordinator = MicStateCoordinator.getInstance()\nexport default micStateCoordinator\n"]}
1
+ {"version":3,"file":"MicStateCoordinator.js","sourceRoot":"","sources":["../../src/services/MicStateCoordinator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * MicStateCoordinator\n *\n * Owns local-miniapp-driven microphone requirements.\n *\n * Local miniapps subscribe to audio_chunk / transcription streams.\n * This coordinator pushes the aggregate local requirement set to BluetoothSdk\n * so the mic runs whenever at least one local consumer needs it.\n *\n * It also merges what the live microphone sessions require (MicSessionManager,\n * via `setSessionRequirement` / `setSessionMicTuning`) with the OS preferences\n * the settings store derives. Applications go through MicSessionManager; this\n * class is an engine implementation detail.\n */\n\nimport BluetoothSdk from \"@mentra/bluetooth-sdk/internal\"\n\nimport {createDebouncedPatchFlusher} from \"../utils/debouncedPatch\"\nimport type {MicTuningProfile} from \"./micPolicy\"\n\nconst LOG_TAG = \"MIC_COORDINATOR\"\n\ntype MicGate = \"vad\" | \"loudnessGate\"\n\ninterface GateOverride {\n enabled: boolean\n order: number\n}\n\ninterface ConfiguredMicGates {\n vadEnabled?: boolean | null\n loudnessGateEnabled?: boolean | null\n micTuning?: Record<string, number> | null\n}\n\n/** Mic-requirement flips are debounced (300ms) and merged into one BLE write\n * (wire v2 keeps BLE JSON small and infrequent). */\nconst flushMicRequirementsPatch = createDebouncedPatchFlusher<Record<string, unknown>>((patch) => {\n try {\n // Resolve runtime overrides at flush time. A miniapp can acquire/release a\n // gate override during the debounce window, and a captured stale value\n // must not win after that lifecycle transition.\n const runtimePatch = micStateCoordinator.applyEffectiveGatePolicy(patch)\n void Promise.resolve(BluetoothSdk.updateBluetoothSettings(runtimePatch)).catch((err) => {\n console.error(`${LOG_TAG}: failed to apply mic requirements:`, err)\n })\n } catch (err) {\n console.error(`${LOG_TAG}: failed to apply mic requirements:`, err)\n }\n}, 300)\n\nclass MicStateCoordinator {\n private static instance: MicStateCoordinator | null = null\n\n // Local miniapp requirements (set when miniapps subscribe to audio streams)\n private localWantsPcm = false\n private localWantsLc3 = false\n /**\n * A live microphone session held through MicSessionManager — a voice call today.\n *\n * Tracked separately from the miniapp requirement because the two have independent lifetimes:\n * the call miniapp does not subscribe to `audio_chunk`, and a captions miniapp that stops mid-call\n * must not take the call's microphone with it.\n */\n private sessionWantsPcm = false\n private configuredVad: boolean | undefined\n private configuredLoudnessGate: boolean | undefined\n /** `mic_tuning` as the settings store last derived it: `super_mode ? desired : {}`. */\n private configuredMicTuning: Record<string, number> = {}\n /** Tuning required by the live sessions, resolved by micPolicy. */\n private sessionMicTuning: MicTuningProfile | null = null\n /**\n * Latched once a session profile has actually been written.\n *\n * Before that, `mic_tuning` stays out of every patch, so a device that never\n * runs a profile does not carry the key on unrelated mic writes. After it,\n * the OS value keeps being restated, which is what stops a reconnect after a\n * call from resurrecting the profile.\n */\n private sessionMicTuningWritten = false\n private readonly miniappVadOverrides = new Map<string, GateOverride>()\n private readonly miniappLoudnessGateOverrides = new Map<string, GateOverride>()\n private overrideSequence = 0\n\n private constructor() {}\n\n public static getInstance(): MicStateCoordinator {\n if (!MicStateCoordinator.instance) {\n MicStateCoordinator.instance = new MicStateCoordinator()\n }\n return MicStateCoordinator.instance\n }\n\n /**\n * Update local miniapp requirements. Called by LocalMiniappRuntime when\n * the aggregated set of local subscriptions changes.\n */\n public setLocalRequirements(req: {pcm: boolean; lc3: boolean} & ConfiguredMicGates): void {\n this.localWantsPcm = req.pcm\n this.localWantsLc3 = req.lc3\n this.rememberConfiguredGates(req)\n console.log(`${LOG_TAG}: local requirements updated — pcm=${req.pcm} lc3=${req.lc3}`)\n this.applyUnion()\n }\n\n /**\n * Claim or release raw PCM on behalf of the live microphone sessions.\n *\n * MicSessionManager only. Applications acquire a session; they do not reach past it to here.\n * Releasing is a claim release, not a mic shutdown: if a captions miniapp still wants PCM the\n * microphone stays on, which is the whole reason this is a separate flag rather than a setter on\n * the local requirement.\n */\n public setSessionRequirement(pcm: boolean): void {\n if (this.sessionWantsPcm === pcm) return\n this.sessionWantsPcm = pcm\n console.log(`${LOG_TAG}: session requirement updated — pcm=${pcm}`)\n this.applyUnion()\n }\n\n /**\n * Apply or drop the tuning the live sessions require.\n *\n * MicSessionManager only. Rides `applyUnion` so the profile and the PCM claim land in one\n * debounced write, resolved at flush time: a session released inside the debounce window wins\n * over the value that was queued.\n */\n public setSessionMicTuning(profile: MicTuningProfile | null): void {\n if (profile?.gain === this.sessionMicTuning?.gain) return\n this.sessionMicTuning = profile\n if (profile) this.sessionMicTuningWritten = true\n // Nothing was ever written, so there is nothing to restore.\n else if (!this.sessionMicTuningWritten) return\n console.log(`${LOG_TAG}: session mic tuning ${profile ? JSON.stringify(profile) : \"cleared\"}`)\n this.applyUnion()\n }\n\n /** Last session profile queued, or null when the OS value is in force. */\n public getSessionMicTuning(): MicTuningProfile | null {\n return this.sessionMicTuning\n }\n\n /**\n * Super Mode sliders outrank a session profile. A live override means a gain\n * sweep would write to the coordinator and the glasses would ignore it.\n */\n public hasConfiguredMicTuning(): boolean {\n return Object.keys(this.configuredMicTuning).length > 0\n }\n\n /**\n * Whether anything on this device needs a continuous raw-PCM timeline. Also the condition that\n * forces hardware VAD off: a gate that drops silence turns a call into clipped half-words.\n */\n private get wantsRawPcm(): boolean {\n return this.localWantsPcm || this.sessionWantsPcm\n }\n\n /**\n * The session profile, but only when it wins.\n *\n * A live Super Mode tuning value outranks it: that screen is how a profile's numbers get found\n * on a real call in the first place. Emptiness is by key count — the settings store hands back a\n * fresh `{}` every time, so reference checks would never match.\n */\n private winningSessionMicTuning(): Record<string, number> | undefined {\n if (!this.sessionMicTuning) return undefined\n if (Object.keys(this.configuredMicTuning).length > 0) return undefined\n return {...this.sessionMicTuning} as Record<string, number>\n }\n\n /**\n * Apply a miniapp-owned gate override without changing the OS preference.\n * Overrides are lifecycle-scoped and last-live-owner-wins independently for\n * VAD and Barrier.\n */\n public async setMiniappGateOverride(\n packageName: string,\n gate: MicGate,\n enabled: boolean,\n configured: ConfiguredMicGates = {},\n ): Promise<void> {\n this.rememberConfiguredGates(configured)\n const overrides = this.overridesFor(gate)\n const previous = overrides.get(packageName)\n const next = {enabled, order: ++this.overrideSequence}\n overrides.set(packageName, next)\n\n try {\n await BluetoothSdk.updateBluetoothSettings(this.effectiveGatePatch())\n } catch (error) {\n // Do not roll back a newer request from the same package that landed\n // while this native write was in flight.\n if (overrides.get(packageName) === next) {\n if (previous) overrides.set(packageName, previous)\n else overrides.delete(packageName)\n }\n throw error\n }\n }\n\n /**\n * Remove every gate override owned by a miniapp. This is synchronous so\n * unregister can drop ownership before recomputing aggregate mic state.\n */\n public clearMiniappGateOverrides(packageName: string): boolean {\n const removedVad = this.miniappVadOverrides.delete(packageName)\n const removedLoudness = this.miniappLoudnessGateOverrides.delete(packageName)\n return removedVad || removedLoudness\n }\n\n /** Re-apply the current runtime policy after an owner is released. */\n public async syncEffectiveGatePolicy(configured: ConfiguredMicGates = {}): Promise<void> {\n this.rememberConfiguredGates(configured)\n const patch = this.effectiveGatePatch()\n if (Object.keys(patch).length === 0) return\n await BluetoothSdk.updateBluetoothSettings(patch)\n }\n\n /**\n * Preserve the runtime microphone contract when the persisted device\n * settings are replayed (for example after a glasses reconnect). Active\n * miniapp gate overrides replace the OS values, and raw PCM keeps VAD off\n * until the last raw-audio consumer unsubscribes.\n */\n public applyRuntimeOverrides(settings: Record<string, unknown>): Record<string, unknown> {\n this.rememberConfiguredGates({\n vadEnabled:\n typeof settings.voice_activity_detection_enabled === \"boolean\"\n ? settings.voice_activity_detection_enabled\n : undefined,\n loudnessGateEnabled:\n typeof settings.loudness_gate_enabled === \"boolean\" ? settings.loudness_gate_enabled : undefined,\n micTuning:\n settings.mic_tuning && typeof settings.mic_tuning === \"object\"\n ? (settings.mic_tuning as Record<string, number>)\n : undefined,\n })\n\n return this.applyActiveRuntimeOverrides(settings)\n }\n\n /**\n * Apply the complete current gate policy without treating the input as an OS\n * preference update. Used by debounced mic-requirement writes, whose queued\n * gate values may be stale after an override is acquired or released.\n */\n public applyEffectiveGatePolicy(settings: Record<string, unknown>): Record<string, unknown> {\n return {\n ...settings,\n ...this.effectiveGatePatch(),\n }\n }\n\n private applyActiveRuntimeOverrides(settings: Record<string, unknown>): Record<string, unknown> {\n const runtimeSettings = {...settings}\n const vadOverride = this.latestOverride(this.miniappVadOverrides)\n const loudnessOverride = this.latestOverride(this.miniappLoudnessGateOverrides)\n\n if (this.wantsRawPcm) {\n runtimeSettings.voice_activity_detection_enabled = false\n } else if (vadOverride) {\n runtimeSettings.voice_activity_detection_enabled = vadOverride.enabled\n }\n if (loudnessOverride) {\n runtimeSettings.loudness_gate_enabled = loudnessOverride.enabled\n }\n\n // BES forgets mic_tuning on disconnect, so the on-connect replay is what\n // puts a live session's profile back.\n const sessionTuning = this.winningSessionMicTuning()\n if (sessionTuning) runtimeSettings.mic_tuning = sessionTuning\n else if (this.sessionMicTuningWritten) runtimeSettings.mic_tuning = this.configuredMicTuning\n\n return runtimeSettings\n }\n\n /**\n * Push local requirements to BluetoothSdk. `should_send_pcm` is strictly for\n * on-device PCM consumers; cloud audio uses LC3 through AudioCloudUplink.\n */\n private applyUnion(): void {\n const shouldSendPcm = this.wantsRawPcm\n const shouldSendLc3 = this.localWantsLc3\n\n // console.log(\n // `${LOG_TAG}: applying requirements — pcm=${shouldSendPcm} lc3=${shouldSendLc3}`,\n // )\n\n // The mic control plane is a direct btsdk call now (was a host setMicRequirements\n // hook) so a bare OEM streams audio without wiring it.\n const patch: Record<string, unknown> = {\n should_send_pcm: shouldSendPcm,\n should_send_lc3: shouldSendLc3,\n should_send_transcript: false,\n ...this.effectiveGatePatch(),\n }\n\n flushMicRequirementsPatch(patch)\n }\n\n private rememberConfiguredGates(configured: ConfiguredMicGates): void {\n // null means the connected device intentionally omits that setting.\n // undefined means the caller is not updating the remembered preference.\n if (configured.vadEnabled !== undefined) {\n this.configuredVad = configured.vadEnabled ?? undefined\n }\n if (configured.loudnessGateEnabled !== undefined) {\n this.configuredLoudnessGate = configured.loudnessGateEnabled ?? undefined\n }\n if (configured.micTuning !== undefined) {\n this.configuredMicTuning = configured.micTuning ?? {}\n }\n }\n\n private overridesFor(gate: MicGate): Map<string, GateOverride> {\n return gate === \"vad\" ? this.miniappVadOverrides : this.miniappLoudnessGateOverrides\n }\n\n private latestOverride(overrides: Map<string, GateOverride>): GateOverride | undefined {\n let latest: GateOverride | undefined\n for (const entry of overrides.values()) {\n if (!latest || entry.order > latest.order) latest = entry\n }\n return latest\n }\n\n private effectiveGatePatch(): Record<string, unknown> {\n const patch: Record<string, unknown> = {}\n const vadOverride = this.latestOverride(this.miniappVadOverrides)\n const loudnessOverride = this.latestOverride(this.miniappLoudnessGateOverrides)\n\n // Hardware VAD suppresses silence. Raw-audio consumers need a continuous\n // timeline, so their requirement wins over both OS and miniapp VAD values.\n if (this.wantsRawPcm) patch.voice_activity_detection_enabled = false\n else if (vadOverride) patch.voice_activity_detection_enabled = vadOverride.enabled\n else if (this.configuredVad !== undefined) patch.voice_activity_detection_enabled = this.configuredVad\n\n if (loudnessOverride) patch.loudness_gate_enabled = loudnessOverride.enabled\n else if (this.configuredLoudnessGate !== undefined) {\n patch.loudness_gate_enabled = this.configuredLoudnessGate\n }\n\n const sessionTuning = this.winningSessionMicTuning()\n if (sessionTuning) patch.mic_tuning = sessionTuning\n else if (this.sessionMicTuningWritten) patch.mic_tuning = this.configuredMicTuning\n\n return patch\n }\n\n /**\n * Reset all requirements to off. Called during cleanup.\n */\n public reset(): void {\n this.localWantsPcm = false\n this.localWantsLc3 = false\n this.sessionWantsPcm = false\n this.sessionMicTuning = null\n this.applyUnion()\n }\n\n public cleanup(): void {\n console.log(`${LOG_TAG}: cleanup()`)\n this.miniappVadOverrides.clear()\n this.miniappLoudnessGateOverrides.clear()\n this.reset()\n MicStateCoordinator.instance = null\n }\n}\n\nconst micStateCoordinator = MicStateCoordinator.getInstance()\nexport default micStateCoordinator\n"]}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Microphone use-case policy.
3
+ *
4
+ * Applications say what they are doing ("voice_call"); the engine decides what
5
+ * that requires of the hardware. Nothing above this file knows that a voice
6
+ * call means ADC index 14, and no miniapp can ask for a gain directly.
7
+ *
8
+ * Pure: every function here is a function of the live session set. The lease
9
+ * bookkeeping lives in MicSessionManager, the merge with OS preferences lives
10
+ * in MicStateCoordinator, and the wire write lives below that again.
11
+ */
12
+ /** What an application is doing with the microphone. */
13
+ export type MicUseCase = "voice_call" | "transcription" | "voice_assistant" | "diagnostic";
14
+ /** Which microphone the audio comes from. */
15
+ export type MicSource = "glasses" | "phone";
16
+ /** Subset of the `mic_tuning` wire payload. Fields left out keep firmware defaults. */
17
+ export type MicTuningProfile = {
18
+ /** codec_adc_vol index, 0-15. */
19
+ gain?: number;
20
+ };
21
+ /** A live microphone lease. */
22
+ export type MicSessionSpec = {
23
+ useCase: MicUseCase;
24
+ source: MicSource;
25
+ };
26
+ /** What the hardware should do, given every live session. */
27
+ export type ResolvedMicPolicy = {
28
+ /** Someone needs a continuous raw-PCM timeline, which also forces hardware VAD off. */
29
+ rawPcm: boolean;
30
+ /** Pin the Bluetooth SDK to the glasses microphone. Only the glasses can be pinned. */
31
+ pinGlasses: boolean;
32
+ /** Tuning override for the glasses, or null to leave the OS value in force. */
33
+ micTuning: MicTuningProfile | null;
34
+ };
35
+ /** Platform facts the policy cannot infer from the sessions alone. */
36
+ export type MicPlatformCaps = {
37
+ /**
38
+ * Whether this platform can take the wearer's voice off the glasses as raw
39
+ * PCM over BLE LC3. False on iOS, which has no `setMicSourcePin` and never
40
+ * selects the `ble-lc3` uplink.
41
+ */
42
+ glassesPcmUplink: boolean;
43
+ };
44
+ /**
45
+ * Mentra Live ships CODEC_SADC_VOL = 15, the last entry of codec_adc_vol[] and
46
+ * +32 dB. That table steps 2 dB at a time everywhere except the final step,
47
+ * which jumps 6 dB from +26, so the default sits at the ceiling one oversized
48
+ * step above everything else. It suits a wearer dictating to a transcription
49
+ * miniapp across a room; it clips a wearer talking into a Teams call.
50
+ *
51
+ * Gain is the only knob here on purpose. A call forces hardware VAD off and
52
+ * the Barrier is off by default, which puts the firmware gate in its `pass`
53
+ * branch, so the RMS thresholds cannot affect a call.
54
+ *
55
+ * Index 14 is +26 dB: one step down, and the step that removes the table's
56
+ * anomalous jump. A same-voice sweep showed 15 rails hard (2.5–4.2% clip);
57
+ * 14/13/12 still kiss the rail on syllable tips. 13 is +24 dB, the middle
58
+ * of that band, while we listen for loudness vs residual clip.
59
+ */
60
+ export declare const MIC_USE_CASE_PROFILES: Record<MicUseCase, MicTuningProfile>;
61
+ /**
62
+ * Packages allowed to hold a `voice_call` session.
63
+ *
64
+ * Which app may make a voice call is policy, so it lives beside the profiles
65
+ * rather than in the request handler that enforces it.
66
+ */
67
+ export declare const VOICE_CALL_PACKAGES: readonly string[];
68
+ /** Every use case, for validating an inbound request. */
69
+ export declare const MIC_USE_CASES: readonly MicUseCase[];
70
+ /** Owners of engine-internal sessions. Miniapps cannot claim these use cases. */
71
+ export declare const ENGINE_OWNER_PREFIX = "engine:";
72
+ /** Use cases only engine features may acquire. */
73
+ export declare const ENGINE_ONLY_USE_CASES: readonly MicUseCase[];
74
+ /**
75
+ * Resolve every live session into one hardware state.
76
+ *
77
+ * On a platform without a glasses PCM uplink a glasses session is a lease and
78
+ * nothing more: it satisfies ownership checks so an iOS call can still be
79
+ * modelled the same way, but it claims no PCM, pins nothing, and applies no
80
+ * gain, because the wearer's voice does not reach the call through the BES
81
+ * BLE path there.
82
+ */
83
+ export declare function resolveMicPolicy(sessions: readonly MicSessionSpec[], caps: MicPlatformCaps): ResolvedMicPolicy;
84
+ //# sourceMappingURL=micPolicy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"micPolicy.d.ts","sourceRoot":"","sources":["../../src/services/micPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,wDAAwD;AACxD,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,eAAe,GAAG,iBAAiB,GAAG,YAAY,CAAA;AAE1F,6CAA6C;AAC7C,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,OAAO,CAAA;AAE3C,uFAAuF;AACvF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,iCAAiC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED,+BAA+B;AAC/B,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,UAAU,CAAA;IACnB,MAAM,EAAE,SAAS,CAAA;CAClB,CAAA;AAED,6DAA6D;AAC7D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,uFAAuF;IACvF,MAAM,EAAE,OAAO,CAAA;IACf,uFAAuF;IACvF,UAAU,EAAE,OAAO,CAAA;IACnB,+EAA+E;IAC/E,SAAS,EAAE,gBAAgB,GAAG,IAAI,CAAA;CACnC,CAAA;AAED,sEAAsE;AACtE,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;OAIG;IACH,gBAAgB,EAAE,OAAO,CAAA;CAC1B,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,UAAU,EAAE,gBAAgB,CAKtE,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAAwB,CAAA;AAEzE,yDAAyD;AACzD,eAAO,MAAM,aAAa,EAAE,SAAS,UAAU,EAK9C,CAAA;AAED,iFAAiF;AACjF,eAAO,MAAM,mBAAmB,YAAY,CAAA;AAE5C,kDAAkD;AAClD,eAAO,MAAM,qBAAqB,EAAE,SAAS,UAAU,EAAmB,CAAA;AAE1E;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,SAAS,cAAc,EAAE,EACnC,IAAI,EAAE,eAAe,GACpB,iBAAiB,CAqBnB"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Microphone use-case policy.
3
+ *
4
+ * Applications say what they are doing ("voice_call"); the engine decides what
5
+ * that requires of the hardware. Nothing above this file knows that a voice
6
+ * call means ADC index 14, and no miniapp can ask for a gain directly.
7
+ *
8
+ * Pure: every function here is a function of the live session set. The lease
9
+ * bookkeeping lives in MicSessionManager, the merge with OS preferences lives
10
+ * in MicStateCoordinator, and the wire write lives below that again.
11
+ */
12
+ /**
13
+ * Mentra Live ships CODEC_SADC_VOL = 15, the last entry of codec_adc_vol[] and
14
+ * +32 dB. That table steps 2 dB at a time everywhere except the final step,
15
+ * which jumps 6 dB from +26, so the default sits at the ceiling one oversized
16
+ * step above everything else. It suits a wearer dictating to a transcription
17
+ * miniapp across a room; it clips a wearer talking into a Teams call.
18
+ *
19
+ * Gain is the only knob here on purpose. A call forces hardware VAD off and
20
+ * the Barrier is off by default, which puts the firmware gate in its `pass`
21
+ * branch, so the RMS thresholds cannot affect a call.
22
+ *
23
+ * Index 14 is +26 dB: one step down, and the step that removes the table's
24
+ * anomalous jump. A same-voice sweep showed 15 rails hard (2.5–4.2% clip);
25
+ * 14/13/12 still kiss the rail on syllable tips. 13 is +24 dB, the middle
26
+ * of that band, while we listen for loudness vs residual clip.
27
+ */
28
+ export const MIC_USE_CASE_PROFILES = {
29
+ voice_call: { gain: 13 },
30
+ transcription: {},
31
+ voice_assistant: {},
32
+ diagnostic: {},
33
+ };
34
+ /**
35
+ * Packages allowed to hold a `voice_call` session.
36
+ *
37
+ * Which app may make a voice call is policy, so it lives beside the profiles
38
+ * rather than in the request handler that enforces it.
39
+ */
40
+ export const VOICE_CALL_PACKAGES = ["com.mentra.call"];
41
+ /** Every use case, for validating an inbound request. */
42
+ export const MIC_USE_CASES = [
43
+ "voice_call",
44
+ "transcription",
45
+ "voice_assistant",
46
+ "diagnostic",
47
+ ];
48
+ /** Owners of engine-internal sessions. Miniapps cannot claim these use cases. */
49
+ export const ENGINE_OWNER_PREFIX = "engine:";
50
+ /** Use cases only engine features may acquire. */
51
+ export const ENGINE_ONLY_USE_CASES = ["diagnostic"];
52
+ /**
53
+ * Resolve every live session into one hardware state.
54
+ *
55
+ * On a platform without a glasses PCM uplink a glasses session is a lease and
56
+ * nothing more: it satisfies ownership checks so an iOS call can still be
57
+ * modelled the same way, but it claims no PCM, pins nothing, and applies no
58
+ * gain, because the wearer's voice does not reach the call through the BES
59
+ * BLE path there.
60
+ */
61
+ export function resolveMicPolicy(sessions, caps) {
62
+ const effective = caps.glassesPcmUplink ? sessions : sessions.filter((s) => s.source !== "glasses");
63
+ let pinGlasses = false;
64
+ let gain;
65
+ for (const session of effective) {
66
+ if (session.source !== "glasses")
67
+ continue;
68
+ pinGlasses = true;
69
+ const profileGain = MIC_USE_CASE_PROFILES[session.useCase]?.gain;
70
+ // Lowest index wins: clipping is the irreversible failure, a slightly
71
+ // quiet assistant is not.
72
+ if (typeof profileGain === "number" && (gain === undefined || profileGain < gain)) {
73
+ gain = profileGain;
74
+ }
75
+ }
76
+ return {
77
+ rawPcm: effective.length > 0,
78
+ pinGlasses,
79
+ micTuning: gain === undefined ? null : { gain },
80
+ };
81
+ }
82
+ //# sourceMappingURL=micPolicy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"micPolicy.js","sourceRoot":"","sources":["../../src/services/micPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAwCH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAyC;IACzE,UAAU,EAAE,EAAC,IAAI,EAAE,EAAE,EAAC;IACtB,aAAa,EAAE,EAAE;IACjB,eAAe,EAAE,EAAE;IACnB,UAAU,EAAE,EAAE;CACf,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAsB,CAAC,iBAAiB,CAAC,CAAA;AAEzE,yDAAyD;AACzD,MAAM,CAAC,MAAM,aAAa,GAA0B;IAClD,YAAY;IACZ,eAAe;IACf,iBAAiB;IACjB,YAAY;CACb,CAAA;AAED,iFAAiF;AACjF,MAAM,CAAC,MAAM,mBAAmB,GAAG,SAAS,CAAA;AAE5C,kDAAkD;AAClD,MAAM,CAAC,MAAM,qBAAqB,GAA0B,CAAC,YAAY,CAAC,CAAA;AAE1E;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAAmC,EACnC,IAAqB;IAErB,MAAM,SAAS,GAAG,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAA;IAEnG,IAAI,UAAU,GAAG,KAAK,CAAA;IACtB,IAAI,IAAwB,CAAA;IAC5B,KAAK,MAAM,OAAO,IAAI,SAAS,EAAE,CAAC;QAChC,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS;YAAE,SAAQ;QAC1C,UAAU,GAAG,IAAI,CAAA;QACjB,MAAM,WAAW,GAAG,qBAAqB,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,IAAI,CAAA;QAChE,sEAAsE;QACtE,0BAA0B;QAC1B,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,WAAW,GAAG,IAAI,CAAC,EAAE,CAAC;YAClF,IAAI,GAAG,WAAW,CAAA;QACpB,CAAC;IACH,CAAC;IAED,OAAO;QACL,MAAM,EAAE,SAAS,CAAC,MAAM,GAAG,CAAC;QAC5B,UAAU;QACV,SAAS,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAC,IAAI,EAAC;KAC9C,CAAA;AACH,CAAC","sourcesContent":["/**\n * Microphone use-case policy.\n *\n * Applications say what they are doing (\"voice_call\"); the engine decides what\n * that requires of the hardware. Nothing above this file knows that a voice\n * call means ADC index 14, and no miniapp can ask for a gain directly.\n *\n * Pure: every function here is a function of the live session set. The lease\n * bookkeeping lives in MicSessionManager, the merge with OS preferences lives\n * in MicStateCoordinator, and the wire write lives below that again.\n */\n\n/** What an application is doing with the microphone. */\nexport type MicUseCase = \"voice_call\" | \"transcription\" | \"voice_assistant\" | \"diagnostic\"\n\n/** Which microphone the audio comes from. */\nexport type MicSource = \"glasses\" | \"phone\"\n\n/** Subset of the `mic_tuning` wire payload. Fields left out keep firmware defaults. */\nexport type MicTuningProfile = {\n /** codec_adc_vol index, 0-15. */\n gain?: number\n}\n\n/** A live microphone lease. */\nexport type MicSessionSpec = {\n useCase: MicUseCase\n source: MicSource\n}\n\n/** What the hardware should do, given every live session. */\nexport type ResolvedMicPolicy = {\n /** Someone needs a continuous raw-PCM timeline, which also forces hardware VAD off. */\n rawPcm: boolean\n /** Pin the Bluetooth SDK to the glasses microphone. Only the glasses can be pinned. */\n pinGlasses: boolean\n /** Tuning override for the glasses, or null to leave the OS value in force. */\n micTuning: MicTuningProfile | null\n}\n\n/** Platform facts the policy cannot infer from the sessions alone. */\nexport type MicPlatformCaps = {\n /**\n * Whether this platform can take the wearer's voice off the glasses as raw\n * PCM over BLE LC3. False on iOS, which has no `setMicSourcePin` and never\n * selects the `ble-lc3` uplink.\n */\n glassesPcmUplink: boolean\n}\n\n/**\n * Mentra Live ships CODEC_SADC_VOL = 15, the last entry of codec_adc_vol[] and\n * +32 dB. That table steps 2 dB at a time everywhere except the final step,\n * which jumps 6 dB from +26, so the default sits at the ceiling one oversized\n * step above everything else. It suits a wearer dictating to a transcription\n * miniapp across a room; it clips a wearer talking into a Teams call.\n *\n * Gain is the only knob here on purpose. A call forces hardware VAD off and\n * the Barrier is off by default, which puts the firmware gate in its `pass`\n * branch, so the RMS thresholds cannot affect a call.\n *\n * Index 14 is +26 dB: one step down, and the step that removes the table's\n * anomalous jump. A same-voice sweep showed 15 rails hard (2.5–4.2% clip);\n * 14/13/12 still kiss the rail on syllable tips. 13 is +24 dB, the middle\n * of that band, while we listen for loudness vs residual clip.\n */\nexport const MIC_USE_CASE_PROFILES: Record<MicUseCase, MicTuningProfile> = {\n voice_call: {gain: 13},\n transcription: {},\n voice_assistant: {},\n diagnostic: {},\n}\n\n/**\n * Packages allowed to hold a `voice_call` session.\n *\n * Which app may make a voice call is policy, so it lives beside the profiles\n * rather than in the request handler that enforces it.\n */\nexport const VOICE_CALL_PACKAGES: readonly string[] = [\"com.mentra.call\"]\n\n/** Every use case, for validating an inbound request. */\nexport const MIC_USE_CASES: readonly MicUseCase[] = [\n \"voice_call\",\n \"transcription\",\n \"voice_assistant\",\n \"diagnostic\",\n]\n\n/** Owners of engine-internal sessions. Miniapps cannot claim these use cases. */\nexport const ENGINE_OWNER_PREFIX = \"engine:\"\n\n/** Use cases only engine features may acquire. */\nexport const ENGINE_ONLY_USE_CASES: readonly MicUseCase[] = [\"diagnostic\"]\n\n/**\n * Resolve every live session into one hardware state.\n *\n * On a platform without a glasses PCM uplink a glasses session is a lease and\n * nothing more: it satisfies ownership checks so an iOS call can still be\n * modelled the same way, but it claims no PCM, pins nothing, and applies no\n * gain, because the wearer's voice does not reach the call through the BES\n * BLE path there.\n */\nexport function resolveMicPolicy(\n sessions: readonly MicSessionSpec[],\n caps: MicPlatformCaps,\n): ResolvedMicPolicy {\n const effective = caps.glassesPcmUplink ? sessions : sessions.filter((s) => s.source !== \"glasses\")\n\n let pinGlasses = false\n let gain: number | undefined\n for (const session of effective) {\n if (session.source !== \"glasses\") continue\n pinGlasses = true\n const profileGain = MIC_USE_CASE_PROFILES[session.useCase]?.gain\n // Lowest index wins: clipping is the irreversible failure, a slightly\n // quiet assistant is not.\n if (typeof profileGain === \"number\" && (gain === undefined || profileGain < gain)) {\n gain = profileGain\n }\n }\n\n return {\n rawPcm: effective.length > 0,\n pinGlasses,\n micTuning: gain === undefined ? null : {gain},\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"bluetoothSettingKeys.d.ts","sourceRoot":"","sources":["../../src/stores/bluetoothSettingKeys.ts"],"names":[],"mappings":"AAEA,iFAAiF;AACjF,eAAO,MAAM,wBAAwB,EAAE,MAAM,EAuB5C,CAAA;AAMD,wBAAgB,gCAAgC,CAC9C,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,QAAQ,EAAE,MAAM,EAAE,GACjB,MAAM,EAAE,CAEV"}
1
+ {"version":3,"file":"bluetoothSettingKeys.d.ts","sourceRoot":"","sources":["../../src/stores/bluetoothSettingKeys.ts"],"names":[],"mappings":"AAEA,iFAAiF;AACjF,eAAO,MAAM,wBAAwB,EAAE,MAAM,EA0B5C,CAAA;AAMD,wBAAgB,gCAAgC,CAC9C,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,QAAQ,EAAE,MAAM,EAAE,GACjB,MAAM,EAAE,CAEV"}
@@ -6,6 +6,9 @@ export const MENTRA_LIVE_SETTING_KEYS = [
6
6
  "preferred_mic",
7
7
  "voice_activity_detection_enabled",
8
8
  "loudness_gate_enabled",
9
+ // Effective tuning only. mic_tuning_desired is engine-side state and must
10
+ // never appear here, or a persisted super-mode value would reach the glasses.
11
+ "mic_tuning",
9
12
  "core_token",
10
13
  "auth_email",
11
14
  "button_photo_size",
@@ -1 +1 @@
1
- {"version":3,"file":"bluetoothSettingKeys.js","sourceRoot":"","sources":["../../src/stores/bluetoothSettingKeys.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAC,MAAM,UAAU,CAAA;AAEpC,iFAAiF;AACjF,MAAM,CAAC,MAAM,wBAAwB,GAAa;IAChD,iBAAiB;IACjB,gBAAgB;IAChB,eAAe;IACf,kCAAkC;IAClC,uBAAuB;IACvB,YAAY;IACZ,YAAY;IACZ,mBAAmB;IACnB,uBAAuB;IACvB,2BAA2B;IAC3B,YAAY;IACZ,cAAc;IACd,2BAA2B;IAC3B,cAAc;IACd,kBAAkB;IAClB,kBAAkB;IAClB,aAAa;IACb,gBAAgB;IAChB,oBAAoB;IACpB,oBAAoB;IACpB,wBAAwB;IACxB,oBAAoB;CACrB,CAAA;AAED,SAAS,kBAAkB,CAAC,KAAyB;IACnD,OAAO,KAAK,KAAK,WAAW,CAAC,IAAI,IAAI,KAAK,KAAK,aAAa,CAAA;AAC9D,CAAC;AAED,MAAM,UAAU,gCAAgC,CAC9C,WAA+B,EAC/B,QAAkB;IAElB,OAAO,kBAAkB,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,QAAQ,CAAA;AAC9E,CAAC","sourcesContent":["import {DeviceTypes} from \"../types\"\n\n/** Mentra Live has no display — omit dashboard/brightness keys from BLE sync. */\nexport const MENTRA_LIVE_SETTING_KEYS: string[] = [\n \"sensing_enabled\",\n \"lc3_frame_size\",\n \"preferred_mic\",\n \"voice_activity_detection_enabled\",\n \"loudness_gate_enabled\",\n \"core_token\",\n \"auth_email\",\n \"button_photo_size\",\n \"button_video_settings\",\n \"button_max_recording_time\",\n \"camera_fov\",\n \"offline_mode\",\n \"local_stt_fallback_active\",\n \"gallery_mode\",\n \"default_wearable\",\n \"pending_wearable\",\n \"device_name\",\n \"device_address\",\n \"default_controller\",\n \"pending_controller\",\n \"controller_device_name\",\n \"controller_address\",\n]\n\nfunction isMentraLiveDevice(model: string | undefined): boolean {\n return model === DeviceTypes.LIVE || model === \"Mentra Live\"\n}\n\nexport function getBluetoothSettingKeysForDevice(\n deviceModel: string | undefined,\n fullKeys: string[],\n): string[] {\n return isMentraLiveDevice(deviceModel) ? MENTRA_LIVE_SETTING_KEYS : fullKeys\n}\n"]}
1
+ {"version":3,"file":"bluetoothSettingKeys.js","sourceRoot":"","sources":["../../src/stores/bluetoothSettingKeys.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAC,MAAM,UAAU,CAAA;AAEpC,iFAAiF;AACjF,MAAM,CAAC,MAAM,wBAAwB,GAAa;IAChD,iBAAiB;IACjB,gBAAgB;IAChB,eAAe;IACf,kCAAkC;IAClC,uBAAuB;IACvB,0EAA0E;IAC1E,8EAA8E;IAC9E,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,mBAAmB;IACnB,uBAAuB;IACvB,2BAA2B;IAC3B,YAAY;IACZ,cAAc;IACd,2BAA2B;IAC3B,cAAc;IACd,kBAAkB;IAClB,kBAAkB;IAClB,aAAa;IACb,gBAAgB;IAChB,oBAAoB;IACpB,oBAAoB;IACpB,wBAAwB;IACxB,oBAAoB;CACrB,CAAA;AAED,SAAS,kBAAkB,CAAC,KAAyB;IACnD,OAAO,KAAK,KAAK,WAAW,CAAC,IAAI,IAAI,KAAK,KAAK,aAAa,CAAA;AAC9D,CAAC;AAED,MAAM,UAAU,gCAAgC,CAC9C,WAA+B,EAC/B,QAAkB;IAElB,OAAO,kBAAkB,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,QAAQ,CAAA;AAC9E,CAAC","sourcesContent":["import {DeviceTypes} from \"../types\"\n\n/** Mentra Live has no display — omit dashboard/brightness keys from BLE sync. */\nexport const MENTRA_LIVE_SETTING_KEYS: string[] = [\n \"sensing_enabled\",\n \"lc3_frame_size\",\n \"preferred_mic\",\n \"voice_activity_detection_enabled\",\n \"loudness_gate_enabled\",\n // Effective tuning only. mic_tuning_desired is engine-side state and must\n // never appear here, or a persisted super-mode value would reach the glasses.\n \"mic_tuning\",\n \"core_token\",\n \"auth_email\",\n \"button_photo_size\",\n \"button_video_settings\",\n \"button_max_recording_time\",\n \"camera_fov\",\n \"offline_mode\",\n \"local_stt_fallback_active\",\n \"gallery_mode\",\n \"default_wearable\",\n \"pending_wearable\",\n \"device_name\",\n \"device_address\",\n \"default_controller\",\n \"pending_controller\",\n \"controller_device_name\",\n \"controller_address\",\n]\n\nfunction isMentraLiveDevice(model: string | undefined): boolean {\n return model === DeviceTypes.LIVE || model === \"Mentra Live\"\n}\n\nexport function getBluetoothSettingKeysForDevice(\n deviceModel: string | undefined,\n fullKeys: string[],\n): string[] {\n return isMentraLiveDevice(deviceModel) ? MENTRA_LIVE_SETTING_KEYS : fullKeys\n}\n"]}
@@ -39,6 +39,11 @@ export interface SettingsState {
39
39
  getSetting: (key: string) => any;
40
40
  loadAllSettings: () => AsyncResult<void, Error>;
41
41
  getBluetoothSettings: () => Record<string, any>;
42
+ /**
43
+ * The mic tuning native is allowed to send: the desired value while super
44
+ * mode is on, `{}` (meaning "reset to firmware defaults") otherwise.
45
+ */
46
+ getEffectiveMicTuning: () => Record<string, number>;
42
47
  resetAllSettingsLocally: () => void;
43
48
  }
44
49
  export declare const useSettingsStore: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<SettingsState>, "subscribe"> & {
@@ -1 +1 @@
1
- {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/stores/settings.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,WAAW,EAAwB,MAAM,aAAa,CAAA;AAQ9D,OAAO,EAAC,wBAAwB,EAAE,gCAAgC,EAAC,MAAM,wBAAwB,CAAA;AAEjG,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAA;IACX,YAAY,EAAE,MAAM,GAAG,CAAA;IACvB,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,EAAE,OAAO,CAAA;IAGrB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;IAEjC,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAA;IAEpB,OAAO,EAAE,OAAO,CAAA;IAQhB,mBAAmB,CAAC,EAAE,IAAI,CAAA;IAC1B;;;;;;;;;;;;;;;OAeG;IACH,qBAAqB,CAAC,EAAE,IAAI,CAAA;CAC7B;AAED,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAwrBnC,CAAA;AAEV,eAAO,MAAM,eAAe,EAAE,MAAM,EAAqD,CAAA;AAIzF,eAAO,MAAM,sBAAsB,EAAE,MAAM,EAiD1C,CAAA;AAeD,eAAO,MAAM,qBAAqB,EAAE,MAAM,EAEV,CAAA;AAIhC,MAAM,WAAW,aAAa;IAE5B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAE7B,aAAa,EAAE,OAAO,CAAA;IAEtB,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,YAAY,CAAC,EAAE,OAAO,KAAK,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IACzF,cAAc,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IAC3E,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,GAAG,CAAA;IAEhC,eAAe,EAAE,MAAM,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IAE/C,oBAAoB,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC/C,uBAAuB,EAAE,MAAM,IAAI,CAAA;CACpC;AAkBD,eAAO,MAAM,gBAAgB;;;;;;;;EA+V5B,CAAA;AAED,eAAO,MAAM,UAAU,GAAI,CAAC,GAAG,GAAG,EAAE,KAAK,MAAM,KAAG,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAK3F,CAAA"}
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/stores/settings.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,WAAW,EAAwB,MAAM,aAAa,CAAA;AAQ9D,OAAO,EAAC,wBAAwB,EAAE,gCAAgC,EAAC,MAAM,wBAAwB,CAAA;AAEjG,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAA;IACX,YAAY,EAAE,MAAM,GAAG,CAAA;IACvB,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,EAAE,OAAO,CAAA;IAGrB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;IAEjC,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAA;IAEpB,OAAO,EAAE,OAAO,CAAA;IAQhB,mBAAmB,CAAC,EAAE,IAAI,CAAA;IAC1B;;;;;;;;;;;;;;;OAeG;IACH,qBAAqB,CAAC,EAAE,IAAI,CAAA;CAC7B;AAED,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CA4tBnC,CAAA;AAEV,eAAO,MAAM,eAAe,EAAE,MAAM,EAAqD,CAAA;AAIzF,eAAO,MAAM,sBAAsB,EAAE,MAAM,EAmD1C,CAAA;AAeD,eAAO,MAAM,qBAAqB,EAAE,MAAM,EAEV,CAAA;AAOhC,MAAM,WAAW,aAAa;IAE5B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAE7B,aAAa,EAAE,OAAO,CAAA;IAEtB,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,YAAY,CAAC,EAAE,OAAO,KAAK,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IACzF,cAAc,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IAC3E,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,GAAG,CAAA;IAEhC,eAAe,EAAE,MAAM,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IAE/C,oBAAoB,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC/C;;;OAGG;IACH,qBAAqB,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnD,uBAAuB,EAAE,MAAM,IAAI,CAAA;CACpC;AAkBD,eAAO,MAAM,gBAAgB;;;;;;;;EAmX5B,CAAA;AAED,eAAO,MAAM,UAAU,GAAI,CAAC,GAAG,GAAG,EAAE,KAAK,MAAM,KAAG,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAK3F,CAAA"}
@@ -421,6 +421,42 @@ export const SETTINGS = {
421
421
  saveOnServer: true,
422
422
  persist: true,
423
423
  },
424
+ /*
425
+ * Mentra Live mic tuning, split in two on purpose.
426
+ *
427
+ * `mic_tuning_desired` is what the super user set. It persists so that
428
+ * turning super mode off and on again does not lose the calibration, and it
429
+ * stays out of BLUETOOTH_SETTING_KEYS so it can never reach the glasses on
430
+ * its own. It is device-local: this is a hardware calibration aid, not
431
+ * something that should follow an account onto another pair of glasses.
432
+ *
433
+ * `mic_tuning` is the effective value, derived in getBluetoothSettings as
434
+ * `super_mode ? desired : {}`. It is the only one native ever sees. Because
435
+ * it is not persisted and native seeds no default for it, a process that
436
+ * connects before the engine has authorized anything can only send a reset
437
+ * -- the super-mode guarantee is then a property of what native can hold,
438
+ * not of which initialiser happened to run first.
439
+ *
440
+ * `{}` rather than null for "no tuning": the native store drops null writes,
441
+ * so null would leave a previously applied value in place.
442
+ *
443
+ * The empty object is a stable singleton so GlassesSettingsSync's reference
444
+ * diff does not treat every unrelated settings write as a mic_tuning change.
445
+ */
446
+ mic_tuning_desired: {
447
+ key: "mic_tuning_desired",
448
+ defaultValue: () => null,
449
+ writable: true,
450
+ saveOnServer: false,
451
+ persist: true,
452
+ },
453
+ mic_tuning: {
454
+ key: "mic_tuning",
455
+ defaultValue: () => ({}),
456
+ writable: true,
457
+ saveOnServer: false,
458
+ persist: false,
459
+ },
424
460
  always_on_status_bar: {
425
461
  key: "always_on_status_bar",
426
462
  defaultValue: () => false,
@@ -711,6 +747,8 @@ export const BLUETOOTH_SETTING_KEYS = [
711
747
  SETTINGS.power_saving_mode.key,
712
748
  SETTINGS.voice_activity_detection_enabled.key,
713
749
  SETTINGS.loudness_gate_enabled.key,
750
+ // Effective tuning only; mic_tuning_desired deliberately stays engine-side.
751
+ SETTINGS.mic_tuning.key,
714
752
  SETTINGS.lc3_frame_size.key,
715
753
  SETTINGS.preferred_mic.key,
716
754
  SETTINGS.screen_disabled.key,
@@ -771,6 +809,8 @@ export const BLUETOOTH_SETTING_KEYS = [
771
809
  export const PAIRING_IDENTITY_KEYS = Object.values(SETTINGS)
772
810
  .filter((setting) => setting.nativeAuthoritative)
773
811
  .map((setting) => setting.key);
812
+ /** Stable empty effective tuning. Native reads this as `{"reset":1}`. */
813
+ const EMPTY_MIC_TUNING = Object.freeze({});
774
814
  const getDefaultSettings = () => Object.keys(SETTINGS).reduce((acc, key) => {
775
815
  acc[key] = SETTINGS[key].defaultValue();
776
816
  return acc;
@@ -1100,10 +1140,32 @@ export const useSettingsStore = create()(subscribeWithSelector((set, get) => ({
1100
1140
  if (key === SETTINGS.core_token.key && (typeof value !== "string" || value.trim().length === 0)) {
1101
1141
  continue;
1102
1142
  }
1143
+ if (key === SETTINGS.mic_tuning.key) {
1144
+ // Derived here rather than stored, so there is no path that can push
1145
+ // a persisted tuning to the glasses while super mode is off. Every
1146
+ // sync route (change diff, on-connect replay, full seed) reads this.
1147
+ bluetoothSettings[key] = state.getEffectiveMicTuning();
1148
+ continue;
1149
+ }
1103
1150
  bluetoothSettings[key] = value;
1104
1151
  }
1105
1152
  return bluetoothSettings;
1106
1153
  },
1154
+ getEffectiveMicTuning: () => {
1155
+ const state = get();
1156
+ if (!state.getSetting(SETTINGS.super_mode.key))
1157
+ return EMPTY_MIC_TUNING;
1158
+ const desired = state.getSetting(SETTINGS.mic_tuning_desired.key);
1159
+ if (!desired || typeof desired !== "object")
1160
+ return EMPTY_MIC_TUNING;
1161
+ const effective = {};
1162
+ for (const [key, value] of Object.entries(desired)) {
1163
+ if (typeof value === "number" && Number.isFinite(value)) {
1164
+ effective[key] = Math.round(value);
1165
+ }
1166
+ }
1167
+ return Object.keys(effective).length === 0 ? EMPTY_MIC_TUNING : effective;
1168
+ },
1107
1169
  resetAllSettingsLocally: () => {
1108
1170
  set((_state) => ({
1109
1171
  settings: getDefaultSettings(),