@mentra/engine 3.2.1-dev.279 → 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.
@@ -1 +1 @@
1
- {"version":3,"file":"MicSessionManager.d.ts","sourceRoot":"","sources":["../../src/services/MicSessionManager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,OAAO,EAEL,KAAK,eAAe,EAEpB,KAAK,SAAS,EACd,KAAK,UAAU,EAChB,MAAM,aAAa,CAAA;AAIpB,8EAA8E;AAC9E,eAAO,MAAM,mBAAmB,wBAAwB,CAAA;AAExD,MAAM,WAAW,iBAAiB;IAChC,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,SAAS,CAAA;IACjB,OAAO,EAAE,UAAU,CAAA;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;IAC5B,uEAAuE;IACvE,OAAO,IAAI,IAAI,CAAA;CAChB;AAgBD,cAAM,iBAAiB;IACrB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAiC;IAExD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiC;IAC1D,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,IAAI,CAAiC;IAC7C,yEAAyE;IACzE,OAAO,CAAC,MAAM,CAAQ;IACtB;;;OAGG;IACH,OAAO,CAAC,aAAa,CAAQ;IAC7B,2FAA2F;IAC3F,OAAO,CAAC,YAAY,CAAQ;IAE5B,OAAO;WAEO,WAAW,IAAI,iBAAiB;IAO9C,qEAAqE;IAC9D,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI;IAInD;;;;;;;OAOG;IACI,OAAO,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU;IAuBtD,iFAAiF;IAC1E,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAc3C;;;;;;OAMG;IACI,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAOzC,UAAU,IAAI,IAAI;IAUlB,OAAO,IAAI,IAAI;IAQtB,sEAAsE;IAC/D,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAKtD;;;OAGG;IACI,kBAAkB,IAAI,OAAO;IAoBpC,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,SAAS;CAkClB;AAED,QAAA,MAAM,iBAAiB,mBAAkC,CAAA;AACzD,eAAe,iBAAiB,CAAA"}
1
+ {"version":3,"file":"MicSessionManager.d.ts","sourceRoot":"","sources":["../../src/services/MicSessionManager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,OAAO,EAEL,KAAK,eAAe,EAEpB,KAAK,SAAS,EACd,KAAK,UAAU,EAChB,MAAM,aAAa,CAAA;AAIpB,8EAA8E;AAC9E,eAAO,MAAM,mBAAmB,wBAAwB,CAAA;AAExD,MAAM,WAAW,iBAAiB;IAChC,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,SAAS,CAAA;IACjB,OAAO,EAAE,UAAU,CAAA;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;IAC5B,uEAAuE;IACvE,OAAO,IAAI,IAAI,CAAA;CAChB;AAgBD,cAAM,iBAAiB;IACrB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAiC;IAExD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiC;IAC1D,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,IAAI,CAAiC;IAC7C,yEAAyE;IACzE,OAAO,CAAC,MAAM,CAAQ;IACtB;;;OAGG;IACH,OAAO,CAAC,aAAa,CAAQ;IAC7B,2FAA2F;IAC3F,OAAO,CAAC,YAAY,CAAQ;IAE5B,OAAO;WAEO,WAAW,IAAI,iBAAiB;IAO9C,qEAAqE;IAC9D,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI;IAInD;;;;;;;OAOG;IACI,OAAO,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU;IAuBtD,iFAAiF;IAC1E,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAc3C;;;;;;OAMG;IACI,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAOzC,UAAU,IAAI,IAAI;IAUlB,OAAO,IAAI,IAAI;IAQtB,sEAAsE;IAC/D,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAKtD;;;OAGG;IACI,kBAAkB,IAAI,OAAO;IAoBpC,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,SAAS;CAqClB;AAED,QAAA,MAAM,iBAAiB,mBAAkC,CAAA;AACzD,eAAe,iBAAiB,CAAA"}
@@ -181,6 +181,9 @@ class MicSessionManager {
181
181
  });
182
182
  }
183
183
  micStateCoordinator.setSessionRequirement(policy.rawPcm);
184
+ // Barrier is the only echo control left once the PCM claim turns hardware VAD off, so it
185
+ // rides with the claim rather than with the tuning a sweep may be rewriting.
186
+ micStateCoordinator.setSessionLoudnessGate(policy.loudnessGate);
184
187
  if (getCallGainSweep().isActive()) {
185
188
  const gain = getCallGainSweep().currentGain();
186
189
  micStateCoordinator.setSessionMicTuning(gain == null ? policy.micTuning : { gain });
@@ -1 +1 @@
1
- {"version":3,"file":"MicSessionManager.js","sourceRoot":"","sources":["../../src/services/MicSessionManager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * MicSessionManager\n *\n * The engine's microphone ownership layer. Applications acquire a lease that\n * says what they are doing; this class tracks the live leases, resolves them\n * through `micPolicy`, and pushes the result to MicStateCoordinator.\n *\n * Everything above it is semantic (\"I need a glasses microphone for a voice\n * call\"). Everything below it is hardware. Audio *sinks* — AcsMeetingService\n * today, another transport later — are downstream of the PCM this produces and\n * must not reach past it to the coordinator or the Bluetooth SDK.\n */\n\nimport {Platform} from \"react-native\"\nimport BluetoothSdk from \"@mentra/bluetooth-sdk/internal\"\n\nimport {getCallGainSweep} from \"./CallGainSweep\"\nimport micStateCoordinator from \"./MicStateCoordinator\"\nimport {\n resolveMicPolicy,\n type MicPlatformCaps,\n type MicSessionSpec,\n type MicSource,\n type MicUseCase,\n} from \"./micPolicy\"\n\nconst LOG_TAG = \"MIC_SESSION\"\n\n/** Raised when a second source is requested while another is already live. */\nexport const MIC_SOURCE_CONFLICT = \"MIC_SOURCE_CONFLICT\"\n\nexport interface MicSessionOptions {\n /** Package name for a miniapp, or `engine:<name>` for an engine feature. */\n owner: string\n source: MicSource\n useCase: MicUseCase\n}\n\nexport interface MicSession {\n readonly id: number\n readonly owner: string\n readonly source: MicSource\n readonly useCase: MicUseCase\n /** Idempotent. Releasing a session that is already gone is a no-op. */\n release(): void\n}\n\ninterface LiveSession extends MicSessionSpec {\n id: number\n owner: string\n}\n\nfunction defaultCaps(): MicPlatformCaps {\n return {\n // iOS has no `setMicSourcePin`, so it cannot promise the phone microphone\n // stays shut, and `glassesLc3UplinkSupported` never selects the BLE LC3\n // uplink there.\n glassesPcmUplink: Platform.OS === \"android\" && typeof BluetoothSdk.setMicSourcePin === \"function\",\n }\n}\n\nclass MicSessionManager {\n private static instance: MicSessionManager | null = null\n\n private readonly sessions = new Map<number, LiveSession>()\n private nextId = 1\n private caps: MicPlatformCaps = defaultCaps()\n /** Last pin we asked for, so a no-op change does not re-enter native. */\n private pinned = false\n /**\n * One 15→14→15→13 walk per voice-call generation. Cleared when the last\n * glasses voice_call session drops so the next join can measure again.\n */\n private sweepFinished = false\n /** One-shot A/B walk. Off now that 15/14/13 is measured; Super Mode can still start it. */\n private sweepEnabled = false\n\n private constructor() {}\n\n public static getInstance(): MicSessionManager {\n if (!MicSessionManager.instance) {\n MicSessionManager.instance = new MicSessionManager()\n }\n return MicSessionManager.instance\n }\n\n /** Test seam. Production reads the platform once at construction. */\n public setPlatformCaps(caps: MicPlatformCaps): void {\n this.caps = caps\n }\n\n /**\n * Take a microphone lease.\n *\n * Throws [MIC_SOURCE_CONFLICT] when a different source is already live: only\n * the glasses can be pinned, so silently mixing sources would leave one\n * consumer reading a microphone it did not ask for. That is the failure ACS\n * already guards against frame by frame.\n */\n public acquire(options: MicSessionOptions): MicSession {\n const conflicting = [...this.sessions.values()].find((s) => s.source !== options.source)\n if (conflicting) {\n throw new Error(\n `${MIC_SOURCE_CONFLICT}: ${options.owner} asked for \"${options.source}\" while ` +\n `${conflicting.owner} holds \"${conflicting.source}\"`,\n )\n }\n\n const id = this.nextId++\n this.sessions.set(id, {id, owner: options.owner, source: options.source, useCase: options.useCase})\n console.log(`${LOG_TAG}: acquire #${id} ${options.owner} ${options.useCase}/${options.source}`)\n this.recompute()\n\n return {\n id,\n owner: options.owner,\n source: options.source,\n useCase: options.useCase,\n release: () => this.releaseId(id),\n }\n }\n\n /** Drop every lease an owner holds. The backstop for unregister / disconnect. */\n public releaseOwner(owner: string): boolean {\n let removed = false\n for (const [id, session] of this.sessions) {\n if (session.owner !== owner) continue\n this.sessions.delete(id)\n removed = true\n }\n if (removed) {\n console.log(`${LOG_TAG}: released all sessions for ${owner}`)\n this.recompute()\n }\n return removed\n }\n\n /**\n * Whether an owner holds a glasses lease.\n *\n * Ownership only. Whether that lease has any hardware effect is\n * [resolveMicPolicy]'s business, so on a platform without a glasses PCM\n * uplink this still answers true while nothing is claimed.\n */\n public hasGlassesSession(owner: string): boolean {\n for (const session of this.sessions.values()) {\n if (session.owner === owner && session.source === \"glasses\") return true\n }\n return false\n }\n\n public releaseAll(): void {\n if (this.sessions.size === 0) {\n this.stopSweep(\"released_all\")\n return\n }\n this.sessions.clear()\n console.log(`${LOG_TAG}: released all sessions`)\n this.recompute()\n }\n\n public cleanup(): void {\n this.stopSweep(\"cleanup\")\n this.sessions.clear()\n this.pinned = false\n this.sweepFinished = false\n MicSessionManager.instance = null\n }\n\n /** Tests disable the auto-walk so acquire still means \"policy 14\". */\n public setCallGainSweepEnabled(enabled: boolean): void {\n this.sweepEnabled = enabled\n if (!enabled) this.stopSweep(\"disabled\")\n }\n\n /**\n * Start or restart the 15→14→15→13 comparison on a live voice_call session.\n * Super Mode uses this so a call already in progress can be measured.\n */\n public startCallGainSweep(): boolean {\n if (!this.hasVoiceCallGlasses()) {\n console.warn(`${LOG_TAG}: CALL_GAIN_SWEEP ignored — no live voice_call glasses session`)\n return false\n }\n if (micStateCoordinator.hasConfiguredMicTuning()) {\n console.warn(\n `${LOG_TAG}: CALL_GAIN_SWEEP Super Mode mic-tuning is set; reset it or the glasses will ignore the sweep`,\n )\n }\n this.sweepFinished = false\n return getCallGainSweep().restart(\n (gain) => micStateCoordinator.setSessionMicTuning({gain}),\n () => {\n this.sweepFinished = true\n this.recompute()\n },\n )\n }\n\n private releaseId(id: number): void {\n if (!this.sessions.delete(id)) return\n console.log(`${LOG_TAG}: release #${id}`)\n this.recompute()\n }\n\n private hasVoiceCallGlasses(): boolean {\n if (!this.caps.glassesPcmUplink) return false\n for (const session of this.sessions.values()) {\n if (session.useCase === \"voice_call\" && session.source === \"glasses\") return true\n }\n return false\n }\n\n private stopSweep(reason: string): void {\n if (getCallGainSweep().isActive()) getCallGainSweep().stop(reason)\n }\n\n private recompute(): void {\n const policy = resolveMicPolicy([...this.sessions.values()], this.caps)\n const voiceCall = this.hasVoiceCallGlasses()\n if (!voiceCall) {\n this.stopSweep(\"session_ended\")\n this.sweepFinished = false\n }\n\n // Pin before claiming PCM, so the first frame the claim produces is\n // already from the right microphone and the phone mic is never opened.\n if (policy.pinGlasses !== this.pinned) {\n this.pinned = policy.pinGlasses\n void Promise.resolve(BluetoothSdk.setMicSourcePin?.(policy.pinGlasses ? \"glasses\" : null)).catch(\n (error) => {\n console.warn(`${LOG_TAG}: setMicSourcePin failed`, error)\n },\n )\n }\n\n micStateCoordinator.setSessionRequirement(policy.rawPcm)\n\n if (getCallGainSweep().isActive()) {\n const gain = getCallGainSweep().currentGain()\n micStateCoordinator.setSessionMicTuning(gain == null ? policy.micTuning : {gain})\n return\n }\n\n if (this.sweepEnabled && voiceCall && !this.sweepFinished) {\n this.startCallGainSweep()\n return\n }\n\n micStateCoordinator.setSessionMicTuning(policy.micTuning)\n }\n}\n\nconst micSessionManager = MicSessionManager.getInstance()\nexport default micSessionManager\n"]}
1
+ {"version":3,"file":"MicSessionManager.js","sourceRoot":"","sources":["../../src/services/MicSessionManager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * MicSessionManager\n *\n * The engine's microphone ownership layer. Applications acquire a lease that\n * says what they are doing; this class tracks the live leases, resolves them\n * through `micPolicy`, and pushes the result to MicStateCoordinator.\n *\n * Everything above it is semantic (\"I need a glasses microphone for a voice\n * call\"). Everything below it is hardware. Audio *sinks* — AcsMeetingService\n * today, another transport later — are downstream of the PCM this produces and\n * must not reach past it to the coordinator or the Bluetooth SDK.\n */\n\nimport {Platform} from \"react-native\"\nimport BluetoothSdk from \"@mentra/bluetooth-sdk/internal\"\n\nimport {getCallGainSweep} from \"./CallGainSweep\"\nimport micStateCoordinator from \"./MicStateCoordinator\"\nimport {\n resolveMicPolicy,\n type MicPlatformCaps,\n type MicSessionSpec,\n type MicSource,\n type MicUseCase,\n} from \"./micPolicy\"\n\nconst LOG_TAG = \"MIC_SESSION\"\n\n/** Raised when a second source is requested while another is already live. */\nexport const MIC_SOURCE_CONFLICT = \"MIC_SOURCE_CONFLICT\"\n\nexport interface MicSessionOptions {\n /** Package name for a miniapp, or `engine:<name>` for an engine feature. */\n owner: string\n source: MicSource\n useCase: MicUseCase\n}\n\nexport interface MicSession {\n readonly id: number\n readonly owner: string\n readonly source: MicSource\n readonly useCase: MicUseCase\n /** Idempotent. Releasing a session that is already gone is a no-op. */\n release(): void\n}\n\ninterface LiveSession extends MicSessionSpec {\n id: number\n owner: string\n}\n\nfunction defaultCaps(): MicPlatformCaps {\n return {\n // iOS has no `setMicSourcePin`, so it cannot promise the phone microphone\n // stays shut, and `glassesLc3UplinkSupported` never selects the BLE LC3\n // uplink there.\n glassesPcmUplink: Platform.OS === \"android\" && typeof BluetoothSdk.setMicSourcePin === \"function\",\n }\n}\n\nclass MicSessionManager {\n private static instance: MicSessionManager | null = null\n\n private readonly sessions = new Map<number, LiveSession>()\n private nextId = 1\n private caps: MicPlatformCaps = defaultCaps()\n /** Last pin we asked for, so a no-op change does not re-enter native. */\n private pinned = false\n /**\n * One 15→14→15→13 walk per voice-call generation. Cleared when the last\n * glasses voice_call session drops so the next join can measure again.\n */\n private sweepFinished = false\n /** One-shot A/B walk. Off now that 15/14/13 is measured; Super Mode can still start it. */\n private sweepEnabled = false\n\n private constructor() {}\n\n public static getInstance(): MicSessionManager {\n if (!MicSessionManager.instance) {\n MicSessionManager.instance = new MicSessionManager()\n }\n return MicSessionManager.instance\n }\n\n /** Test seam. Production reads the platform once at construction. */\n public setPlatformCaps(caps: MicPlatformCaps): void {\n this.caps = caps\n }\n\n /**\n * Take a microphone lease.\n *\n * Throws [MIC_SOURCE_CONFLICT] when a different source is already live: only\n * the glasses can be pinned, so silently mixing sources would leave one\n * consumer reading a microphone it did not ask for. That is the failure ACS\n * already guards against frame by frame.\n */\n public acquire(options: MicSessionOptions): MicSession {\n const conflicting = [...this.sessions.values()].find((s) => s.source !== options.source)\n if (conflicting) {\n throw new Error(\n `${MIC_SOURCE_CONFLICT}: ${options.owner} asked for \"${options.source}\" while ` +\n `${conflicting.owner} holds \"${conflicting.source}\"`,\n )\n }\n\n const id = this.nextId++\n this.sessions.set(id, {id, owner: options.owner, source: options.source, useCase: options.useCase})\n console.log(`${LOG_TAG}: acquire #${id} ${options.owner} ${options.useCase}/${options.source}`)\n this.recompute()\n\n return {\n id,\n owner: options.owner,\n source: options.source,\n useCase: options.useCase,\n release: () => this.releaseId(id),\n }\n }\n\n /** Drop every lease an owner holds. The backstop for unregister / disconnect. */\n public releaseOwner(owner: string): boolean {\n let removed = false\n for (const [id, session] of this.sessions) {\n if (session.owner !== owner) continue\n this.sessions.delete(id)\n removed = true\n }\n if (removed) {\n console.log(`${LOG_TAG}: released all sessions for ${owner}`)\n this.recompute()\n }\n return removed\n }\n\n /**\n * Whether an owner holds a glasses lease.\n *\n * Ownership only. Whether that lease has any hardware effect is\n * [resolveMicPolicy]'s business, so on a platform without a glasses PCM\n * uplink this still answers true while nothing is claimed.\n */\n public hasGlassesSession(owner: string): boolean {\n for (const session of this.sessions.values()) {\n if (session.owner === owner && session.source === \"glasses\") return true\n }\n return false\n }\n\n public releaseAll(): void {\n if (this.sessions.size === 0) {\n this.stopSweep(\"released_all\")\n return\n }\n this.sessions.clear()\n console.log(`${LOG_TAG}: released all sessions`)\n this.recompute()\n }\n\n public cleanup(): void {\n this.stopSweep(\"cleanup\")\n this.sessions.clear()\n this.pinned = false\n this.sweepFinished = false\n MicSessionManager.instance = null\n }\n\n /** Tests disable the auto-walk so acquire still means \"policy 14\". */\n public setCallGainSweepEnabled(enabled: boolean): void {\n this.sweepEnabled = enabled\n if (!enabled) this.stopSweep(\"disabled\")\n }\n\n /**\n * Start or restart the 15→14→15→13 comparison on a live voice_call session.\n * Super Mode uses this so a call already in progress can be measured.\n */\n public startCallGainSweep(): boolean {\n if (!this.hasVoiceCallGlasses()) {\n console.warn(`${LOG_TAG}: CALL_GAIN_SWEEP ignored — no live voice_call glasses session`)\n return false\n }\n if (micStateCoordinator.hasConfiguredMicTuning()) {\n console.warn(\n `${LOG_TAG}: CALL_GAIN_SWEEP Super Mode mic-tuning is set; reset it or the glasses will ignore the sweep`,\n )\n }\n this.sweepFinished = false\n return getCallGainSweep().restart(\n (gain) => micStateCoordinator.setSessionMicTuning({gain}),\n () => {\n this.sweepFinished = true\n this.recompute()\n },\n )\n }\n\n private releaseId(id: number): void {\n if (!this.sessions.delete(id)) return\n console.log(`${LOG_TAG}: release #${id}`)\n this.recompute()\n }\n\n private hasVoiceCallGlasses(): boolean {\n if (!this.caps.glassesPcmUplink) return false\n for (const session of this.sessions.values()) {\n if (session.useCase === \"voice_call\" && session.source === \"glasses\") return true\n }\n return false\n }\n\n private stopSweep(reason: string): void {\n if (getCallGainSweep().isActive()) getCallGainSweep().stop(reason)\n }\n\n private recompute(): void {\n const policy = resolveMicPolicy([...this.sessions.values()], this.caps)\n const voiceCall = this.hasVoiceCallGlasses()\n if (!voiceCall) {\n this.stopSweep(\"session_ended\")\n this.sweepFinished = false\n }\n\n // Pin before claiming PCM, so the first frame the claim produces is\n // already from the right microphone and the phone mic is never opened.\n if (policy.pinGlasses !== this.pinned) {\n this.pinned = policy.pinGlasses\n void Promise.resolve(BluetoothSdk.setMicSourcePin?.(policy.pinGlasses ? \"glasses\" : null)).catch(\n (error) => {\n console.warn(`${LOG_TAG}: setMicSourcePin failed`, error)\n },\n )\n }\n\n micStateCoordinator.setSessionRequirement(policy.rawPcm)\n // Barrier is the only echo control left once the PCM claim turns hardware VAD off, so it\n // rides with the claim rather than with the tuning a sweep may be rewriting.\n micStateCoordinator.setSessionLoudnessGate(policy.loudnessGate)\n\n if (getCallGainSweep().isActive()) {\n const gain = getCallGainSweep().currentGain()\n micStateCoordinator.setSessionMicTuning(gain == null ? policy.micTuning : {gain})\n return\n }\n\n if (this.sweepEnabled && voiceCall && !this.sweepFinished) {\n this.startCallGainSweep()\n return\n }\n\n micStateCoordinator.setSessionMicTuning(policy.micTuning)\n }\n}\n\nconst micSessionManager = MicSessionManager.getInstance()\nexport default micSessionManager\n"]}
@@ -46,6 +46,10 @@ declare class MicStateCoordinator {
46
46
  * call from resurrecting the profile.
47
47
  */
48
48
  private sessionMicTuningWritten;
49
+ /** Barrier required by the live sessions, or null when they have no opinion. */
50
+ private sessionLoudnessGate;
51
+ /** Latched like the tuning, so the OS value is restated once a session has moved it. */
52
+ private sessionLoudnessGateWritten;
49
53
  private readonly miniappVadOverrides;
50
54
  private readonly miniappLoudnessGateOverrides;
51
55
  private overrideSequence;
@@ -78,6 +82,17 @@ declare class MicStateCoordinator {
78
82
  setSessionMicTuning(profile: MicTuningProfile | null): void;
79
83
  /** Last session profile queued, or null when the OS value is in force. */
80
84
  getSessionMicTuning(): MicTuningProfile | null;
85
+ /** Last session Barrier queued, or null when the OS value is in force. For call logging. */
86
+ getSessionLoudnessGate(): boolean | null;
87
+ /**
88
+ * Run or stop the center-mic loudness gate on behalf of the live sessions.
89
+ *
90
+ * MicSessionManager only, and the counterpart to the PCM claim rather than a second opinion on
91
+ * it: raw PCM turns hardware VAD off, which leaves Barrier as the only thing standing between
92
+ * the far end and its own echo. Unlike VAD this never stops the stream, so the worst a wrong
93
+ * threshold costs is a zeroed frame instead of a dropped word.
94
+ */
95
+ setSessionLoudnessGate(enabled: boolean | null): void;
81
96
  /**
82
97
  * Super Mode sliders outrank a session profile. A live override means a gain
83
98
  * sweep would write to the coordinator and the glasses would ignore it.
@@ -96,6 +111,14 @@ declare class MicStateCoordinator {
96
111
  * fresh `{}` every time, so reference checks would never match.
97
112
  */
98
113
  private winningSessionMicTuning;
114
+ /**
115
+ * The session's Barrier, but only when it wins.
116
+ *
117
+ * Suppressed by a live Super Mode tuning value for the same reason the gain is: that screen is
118
+ * the manual override, and a gate running against hand-entered thresholds is the one case where
119
+ * the session's numbers are the wrong ones.
120
+ */
121
+ private winningSessionLoudnessGate;
99
122
  /**
100
123
  * Apply a miniapp-owned gate override without changing the OS preference.
101
124
  * Overrides are lifecycle-scoped and last-live-owner-wins independently for
@@ -1 +1 @@
1
- {"version":3,"file":"MicStateCoordinator.d.ts","sourceRoot":"","sources":["../../src/services/MicStateCoordinator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,aAAa,CAAA;AAIjD,KAAK,OAAO,GAAG,KAAK,GAAG,cAAc,CAAA;AAOrC,UAAU,kBAAkB;IAC1B,UAAU,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;IAC3B,mBAAmB,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;CAC1C;AAkBD,cAAM,mBAAmB;IACvB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAmC;IAG1D,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,aAAa,CAAQ;IAC7B;;;;;;OAMG;IACH,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,sBAAsB,CAAqB;IACnD,uFAAuF;IACvF,OAAO,CAAC,mBAAmB,CAA6B;IACxD,mEAAmE;IACnE,OAAO,CAAC,gBAAgB,CAAgC;IACxD;;;;;;;OAOG;IACH,OAAO,CAAC,uBAAuB,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAkC;IACtE,OAAO,CAAC,QAAQ,CAAC,4BAA4B,CAAkC;IAC/E,OAAO,CAAC,gBAAgB,CAAI;IAE5B,OAAO;WAEO,WAAW,IAAI,mBAAmB;IAOhD;;;OAGG;IACI,oBAAoB,CAAC,GAAG,EAAE;QAAC,GAAG,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,OAAO,CAAA;KAAC,GAAG,kBAAkB,GAAG,IAAI;IAQzF;;;;;;;OAOG;IACI,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI;IAOhD;;;;;;OAMG;IACI,mBAAmB,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,GAAG,IAAI;IAUlE,0EAA0E;IACnE,mBAAmB,IAAI,gBAAgB,GAAG,IAAI;IAIrD;;;OAGG;IACI,sBAAsB,IAAI,OAAO;IAIxC;;;OAGG;IACH,OAAO,KAAK,WAAW,GAEtB;IAED;;;;;;OAMG;IACH,OAAO,CAAC,uBAAuB;IAM/B;;;;OAIG;IACU,sBAAsB,CACjC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,OAAO,EAChB,UAAU,GAAE,kBAAuB,GAClC,OAAO,CAAC,IAAI,CAAC;IAoBhB;;;OAGG;IACI,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAM9D,sEAAsE;IACzD,uBAAuB,CAAC,UAAU,GAAE,kBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;IAOxF;;;;;OAKG;IACI,qBAAqB,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAiBxF;;;;OAIG;IACI,wBAAwB,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAO3F,OAAO,CAAC,2BAA2B;IAuBnC;;;OAGG;IACH,OAAO,CAAC,UAAU;IAoBlB,OAAO,CAAC,uBAAuB;IAc/B,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,kBAAkB;IAuB1B;;OAEG;IACI,KAAK,IAAI,IAAI;IAQb,OAAO,IAAI,IAAI;CAOvB;AAED,QAAA,MAAM,mBAAmB,qBAAoC,CAAA;AAC7D,eAAe,mBAAmB,CAAA"}
1
+ {"version":3,"file":"MicStateCoordinator.d.ts","sourceRoot":"","sources":["../../src/services/MicStateCoordinator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,aAAa,CAAA;AAajD,KAAK,OAAO,GAAG,KAAK,GAAG,cAAc,CAAA;AAOrC,UAAU,kBAAkB;IAC1B,UAAU,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;IAC3B,mBAAmB,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;CAC1C;AAsBD,cAAM,mBAAmB;IACvB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAmC;IAG1D,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,aAAa,CAAQ;IAC7B;;;;;;OAMG;IACH,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,sBAAsB,CAAqB;IACnD,uFAAuF;IACvF,OAAO,CAAC,mBAAmB,CAA6B;IACxD,mEAAmE;IACnE,OAAO,CAAC,gBAAgB,CAAgC;IACxD;;;;;;;OAOG;IACH,OAAO,CAAC,uBAAuB,CAAQ;IACvC,gFAAgF;IAChF,OAAO,CAAC,mBAAmB,CAAuB;IAClD,wFAAwF;IACxF,OAAO,CAAC,0BAA0B,CAAQ;IAC1C,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAkC;IACtE,OAAO,CAAC,QAAQ,CAAC,4BAA4B,CAAkC;IAC/E,OAAO,CAAC,gBAAgB,CAAI;IAE5B,OAAO;WAEO,WAAW,IAAI,mBAAmB;IAOhD;;;OAGG;IACI,oBAAoB,CAAC,GAAG,EAAE;QAAC,GAAG,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,OAAO,CAAA;KAAC,GAAG,kBAAkB,GAAG,IAAI;IAQzF;;;;;;;OAOG;IACI,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI;IAOhD;;;;;;OAMG;IACI,mBAAmB,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,GAAG,IAAI;IAUlE,0EAA0E;IACnE,mBAAmB,IAAI,gBAAgB,GAAG,IAAI;IAIrD,4FAA4F;IACrF,sBAAsB,IAAI,OAAO,GAAG,IAAI;IAI/C;;;;;;;OAOG;IACI,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI;IAS5D;;;OAGG;IACI,sBAAsB,IAAI,OAAO;IAIxC;;;OAGG;IACH,OAAO,KAAK,WAAW,GAEtB;IAED;;;;;;OAMG;IACH,OAAO,CAAC,uBAAuB;IAM/B;;;;;;OAMG;IACH,OAAO,CAAC,0BAA0B;IAMlC;;;;OAIG;IACU,sBAAsB,CACjC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,OAAO,EAChB,UAAU,GAAE,kBAAuB,GAClC,OAAO,CAAC,IAAI,CAAC;IAoBhB;;;OAGG;IACI,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAM9D,sEAAsE;IACzD,uBAAuB,CAAC,UAAU,GAAE,kBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;IAOxF;;;;;OAKG;IACI,qBAAqB,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAiBxF;;;;OAIG;IACI,wBAAwB,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAO3F,OAAO,CAAC,2BAA2B;IA0BnC;;;OAGG;IACH,OAAO,CAAC,UAAU;IAoBlB,OAAO,CAAC,uBAAuB;IAc/B,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,kBAAkB;IA6B1B;;OAEG;IACI,KAAK,IAAI,IAAI;IASb,OAAO,IAAI,IAAI;CAOvB;AAED,QAAA,MAAM,mBAAmB,qBAAoC,CAAA;AAC7D,eAAe,mBAAmB,CAAA"}
@@ -15,6 +15,18 @@
15
15
  import BluetoothSdk from "@mentra/bluetooth-sdk/internal";
16
16
  import { createDebouncedPatchFlusher } from "../utils/debouncedPatch";
17
17
  const LOG_TAG = "MIC_COORDINATOR";
18
+ /** Field-wise compare, so a profile that moves a threshold without the gain still lands. */
19
+ function sameProfile(a, b) {
20
+ if (a === b)
21
+ return true;
22
+ if (!a || !b)
23
+ return false;
24
+ const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
25
+ for (const key of keys)
26
+ if (a[key] !== b[key])
27
+ return false;
28
+ return true;
29
+ }
18
30
  /** Mic-requirement flips are debounced (300ms) and merged into one BLE write
19
31
  * (wire v2 keeps BLE JSON small and infrequent). */
20
32
  const flushMicRequirementsPatch = createDebouncedPatchFlusher((patch) => {
@@ -23,6 +35,10 @@ const flushMicRequirementsPatch = createDebouncedPatchFlusher((patch) => {
23
35
  // gate override during the debounce window, and a captured stale value
24
36
  // must not win after that lifecycle transition.
25
37
  const runtimePatch = micStateCoordinator.applyEffectiveGatePolicy(patch);
38
+ // The merged patch, not the intent that produced it. Everything above this line is a
39
+ // preference; this is the only record of what the glasses were actually told, and the three
40
+ // keys a call depends on (VAD off, Barrier on, the tuning) are only decided here at flush.
41
+ console.log(`${LOG_TAG}: write`, runtimePatch);
26
42
  void Promise.resolve(BluetoothSdk.updateBluetoothSettings(runtimePatch)).catch((err) => {
27
43
  console.error(`${LOG_TAG}: failed to apply mic requirements:`, err);
28
44
  });
@@ -59,6 +75,10 @@ class MicStateCoordinator {
59
75
  * call from resurrecting the profile.
60
76
  */
61
77
  sessionMicTuningWritten = false;
78
+ /** Barrier required by the live sessions, or null when they have no opinion. */
79
+ sessionLoudnessGate = null;
80
+ /** Latched like the tuning, so the OS value is restated once a session has moved it. */
81
+ sessionLoudnessGateWritten = false;
62
82
  miniappVadOverrides = new Map();
63
83
  miniappLoudnessGateOverrides = new Map();
64
84
  overrideSequence = 0;
@@ -103,7 +123,7 @@ class MicStateCoordinator {
103
123
  * over the value that was queued.
104
124
  */
105
125
  setSessionMicTuning(profile) {
106
- if (profile?.gain === this.sessionMicTuning?.gain)
126
+ if (sameProfile(profile, this.sessionMicTuning))
107
127
  return;
108
128
  this.sessionMicTuning = profile;
109
129
  if (profile)
@@ -118,6 +138,29 @@ class MicStateCoordinator {
118
138
  getSessionMicTuning() {
119
139
  return this.sessionMicTuning;
120
140
  }
141
+ /** Last session Barrier queued, or null when the OS value is in force. For call logging. */
142
+ getSessionLoudnessGate() {
143
+ return this.sessionLoudnessGate;
144
+ }
145
+ /**
146
+ * Run or stop the center-mic loudness gate on behalf of the live sessions.
147
+ *
148
+ * MicSessionManager only, and the counterpart to the PCM claim rather than a second opinion on
149
+ * it: raw PCM turns hardware VAD off, which leaves Barrier as the only thing standing between
150
+ * the far end and its own echo. Unlike VAD this never stops the stream, so the worst a wrong
151
+ * threshold costs is a zeroed frame instead of a dropped word.
152
+ */
153
+ setSessionLoudnessGate(enabled) {
154
+ if (this.sessionLoudnessGate === enabled)
155
+ return;
156
+ this.sessionLoudnessGate = enabled;
157
+ if (enabled !== null)
158
+ this.sessionLoudnessGateWritten = true;
159
+ else if (!this.sessionLoudnessGateWritten)
160
+ return;
161
+ console.log(`${LOG_TAG}: session loudness gate ${enabled === null ? "cleared" : enabled}`);
162
+ this.applyUnion();
163
+ }
121
164
  /**
122
165
  * Super Mode sliders outrank a session profile. A live override means a gain
123
166
  * sweep would write to the coordinator and the glasses would ignore it.
@@ -146,6 +189,20 @@ class MicStateCoordinator {
146
189
  return undefined;
147
190
  return { ...this.sessionMicTuning };
148
191
  }
192
+ /**
193
+ * The session's Barrier, but only when it wins.
194
+ *
195
+ * Suppressed by a live Super Mode tuning value for the same reason the gain is: that screen is
196
+ * the manual override, and a gate running against hand-entered thresholds is the one case where
197
+ * the session's numbers are the wrong ones.
198
+ */
199
+ winningSessionLoudnessGate() {
200
+ if (this.sessionLoudnessGate === null)
201
+ return undefined;
202
+ if (Object.keys(this.configuredMicTuning).length > 0)
203
+ return undefined;
204
+ return this.sessionLoudnessGate;
205
+ }
149
206
  /**
150
207
  * Apply a miniapp-owned gate override without changing the OS preference.
151
208
  * Overrides are lifecycle-scoped and last-live-owner-wins independently for
@@ -228,9 +285,13 @@ class MicStateCoordinator {
228
285
  else if (vadOverride) {
229
286
  runtimeSettings.voice_activity_detection_enabled = vadOverride.enabled;
230
287
  }
288
+ const sessionGate = this.winningSessionLoudnessGate();
231
289
  if (loudnessOverride) {
232
290
  runtimeSettings.loudness_gate_enabled = loudnessOverride.enabled;
233
291
  }
292
+ else if (sessionGate !== undefined) {
293
+ runtimeSettings.loudness_gate_enabled = sessionGate;
294
+ }
234
295
  // BES forgets mic_tuning on disconnect, so the on-connect replay is what
235
296
  // puts a live session's profile back.
236
297
  const sessionTuning = this.winningSessionMicTuning();
@@ -296,11 +357,19 @@ class MicStateCoordinator {
296
357
  patch.voice_activity_detection_enabled = vadOverride.enabled;
297
358
  else if (this.configuredVad !== undefined)
298
359
  patch.voice_activity_detection_enabled = this.configuredVad;
360
+ const sessionGate = this.winningSessionLoudnessGate();
299
361
  if (loudnessOverride)
300
362
  patch.loudness_gate_enabled = loudnessOverride.enabled;
363
+ else if (sessionGate !== undefined)
364
+ patch.loudness_gate_enabled = sessionGate;
301
365
  else if (this.configuredLoudnessGate !== undefined) {
302
366
  patch.loudness_gate_enabled = this.configuredLoudnessGate;
303
367
  }
368
+ else if (this.sessionLoudnessGateWritten) {
369
+ // A session moved it and the device carries no preference, so restate the product default
370
+ // rather than leaving the call's gate running after it ended.
371
+ patch.loudness_gate_enabled = false;
372
+ }
304
373
  const sessionTuning = this.winningSessionMicTuning();
305
374
  if (sessionTuning)
306
375
  patch.mic_tuning = sessionTuning;
@@ -316,6 +385,7 @@ class MicStateCoordinator {
316
385
  this.localWantsLc3 = false;
317
386
  this.sessionWantsPcm = false;
318
387
  this.sessionMicTuning = null;
388
+ this.sessionLoudnessGate = null;
319
389
  this.applyUnion();
320
390
  }
321
391
  cleanup() {
@@ -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 * 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"]}
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\n/** Field-wise compare, so a profile that moves a threshold without the gain still lands. */\nfunction sameProfile(a: MicTuningProfile | null, b: MicTuningProfile | null): boolean {\n if (a === b) return true\n if (!a || !b) return false\n const keys = new Set([...Object.keys(a), ...Object.keys(b)]) as Set<keyof MicTuningProfile>\n for (const key of keys) if (a[key] !== b[key]) return false\n return true\n}\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 // The merged patch, not the intent that produced it. Everything above this line is a\n // preference; this is the only record of what the glasses were actually told, and the three\n // keys a call depends on (VAD off, Barrier on, the tuning) are only decided here at flush.\n console.log(`${LOG_TAG}: write`, runtimePatch)\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 /** Barrier required by the live sessions, or null when they have no opinion. */\n private sessionLoudnessGate: boolean | null = null\n /** Latched like the tuning, so the OS value is restated once a session has moved it. */\n private sessionLoudnessGateWritten = 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 (sameProfile(profile, this.sessionMicTuning)) 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 /** Last session Barrier queued, or null when the OS value is in force. For call logging. */\n public getSessionLoudnessGate(): boolean | null {\n return this.sessionLoudnessGate\n }\n\n /**\n * Run or stop the center-mic loudness gate on behalf of the live sessions.\n *\n * MicSessionManager only, and the counterpart to the PCM claim rather than a second opinion on\n * it: raw PCM turns hardware VAD off, which leaves Barrier as the only thing standing between\n * the far end and its own echo. Unlike VAD this never stops the stream, so the worst a wrong\n * threshold costs is a zeroed frame instead of a dropped word.\n */\n public setSessionLoudnessGate(enabled: boolean | null): void {\n if (this.sessionLoudnessGate === enabled) return\n this.sessionLoudnessGate = enabled\n if (enabled !== null) this.sessionLoudnessGateWritten = true\n else if (!this.sessionLoudnessGateWritten) return\n console.log(`${LOG_TAG}: session loudness gate ${enabled === null ? \"cleared\" : enabled}`)\n this.applyUnion()\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 * The session's Barrier, but only when it wins.\n *\n * Suppressed by a live Super Mode tuning value for the same reason the gain is: that screen is\n * the manual override, and a gate running against hand-entered thresholds is the one case where\n * the session's numbers are the wrong ones.\n */\n private winningSessionLoudnessGate(): boolean | undefined {\n if (this.sessionLoudnessGate === null) return undefined\n if (Object.keys(this.configuredMicTuning).length > 0) return undefined\n return this.sessionLoudnessGate\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 const sessionGate = this.winningSessionLoudnessGate()\n if (loudnessOverride) {\n runtimeSettings.loudness_gate_enabled = loudnessOverride.enabled\n } else if (sessionGate !== undefined) {\n runtimeSettings.loudness_gate_enabled = sessionGate\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 const sessionGate = this.winningSessionLoudnessGate()\n if (loudnessOverride) patch.loudness_gate_enabled = loudnessOverride.enabled\n else if (sessionGate !== undefined) patch.loudness_gate_enabled = sessionGate\n else if (this.configuredLoudnessGate !== undefined) {\n patch.loudness_gate_enabled = this.configuredLoudnessGate\n } else if (this.sessionLoudnessGateWritten) {\n // A session moved it and the device carries no preference, so restate the product default\n // rather than leaving the call's gate running after it ended.\n patch.loudness_gate_enabled = false\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.sessionLoudnessGate = 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"]}
@@ -17,7 +17,40 @@ export type MicSource = "glasses" | "phone";
17
17
  export type MicTuningProfile = {
18
18
  /** codec_adc_vol index, 0-15. */
19
19
  gain?: number;
20
+ /** Center-mic RMS to open the gate. */
21
+ open?: number;
22
+ /** Center-mic RMS to close it again. */
23
+ close?: number;
24
+ /** Open threshold while the speaker is elevated. */
25
+ sp_open?: number;
26
+ /** Close threshold while the speaker is elevated. */
27
+ sp_close?: number;
20
28
  };
29
+ /** `codec_adc_vol[]`: index to dB. Index 0 is mute and is never offered. */
30
+ export declare const GAIN_DB: number[];
31
+ /**
32
+ * Mirrored from `center_mic_vad_get_default_config` and `CODEC_SADC_VOL`.
33
+ *
34
+ * The four RMS numbers are measured *after* the ADC gain, so they are only
35
+ * meaningful next to the gain they were calibrated at, which is index 15.
36
+ */
37
+ export declare const MIC_TUNING_FIRMWARE_DEFAULTS: {
38
+ readonly gain: 15;
39
+ readonly open: 1350;
40
+ readonly close: 945;
41
+ readonly sp_open: 2900;
42
+ readonly sp_close: 1600;
43
+ };
44
+ /**
45
+ * Re-express the firmware's thresholds at a different gain.
46
+ *
47
+ * The gate compares post-gain RMS, so moving the gain without moving the
48
+ * thresholds silently changes what the gate means: at -8 dB the wearer has to
49
+ * be 2.5x louder to clear a number that was chosen for +32 dB. Scaling both by
50
+ * the same factor keeps the *acoustic* trip point the profile was tuned for,
51
+ * for the wearer and for speaker leak alike.
52
+ */
53
+ export declare function scaleMicTuningToGain(gain: number): Required<MicTuningProfile>;
21
54
  /** A live microphone lease. */
22
55
  export type MicSessionSpec = {
23
56
  useCase: MicUseCase;
@@ -31,6 +64,8 @@ export type ResolvedMicPolicy = {
31
64
  pinGlasses: boolean;
32
65
  /** Tuning override for the glasses, or null to leave the OS value in force. */
33
66
  micTuning: MicTuningProfile | null;
67
+ /** Run the center-mic loudness gate, or null to leave the OS value in force. */
68
+ loudnessGate: boolean | null;
34
69
  };
35
70
  /** Platform facts the policy cannot infer from the sessions alone. */
36
71
  export type MicPlatformCaps = {
@@ -48,16 +83,28 @@ export type MicPlatformCaps = {
48
83
  * step above everything else. It suits a wearer dictating to a transcription
49
84
  * miniapp across a room; it clips a wearer talking into a Teams call.
50
85
  *
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
86
  * Index 14 is +26 dB: one step down, and the step that removes the table's
56
87
  * anomalous jump. A same-voice sweep showed 15 rails hard (2.5–4.2% clip);
57
88
  * 14/13/12 still kiss the rail on syllable tips. 13 is +24 dB, the middle
58
89
  * of that band, while we listen for loudness vs residual clip.
90
+ *
91
+ * The thresholds ride along with the gain rather than being stated here, so
92
+ * changing the index above cannot leave the gate calibrated for a level the
93
+ * ADC no longer produces.
59
94
  */
60
95
  export declare const MIC_USE_CASE_PROFILES: Record<MicUseCase, MicTuningProfile>;
96
+ /**
97
+ * Use cases that run the center-mic loudness gate ("Barrier").
98
+ *
99
+ * A voice call is the one place it earns its keep. There is no echo canceller
100
+ * on the LC3 uplink — `MENTRA_LC3_SPEECH_PROCESSING` is 0 and the capture path
101
+ * ignores the playback reference — so without a gate the far end hears itself
102
+ * through the wearer's speaker. Barrier is the safe half of the firmware's two
103
+ * gates: it zeroes a quiet frame but never stops transmitting, where VAD drops
104
+ * the stream outright. Paired with the speaker-elevated thresholds above, that
105
+ * suppresses leak while the far end is talking and costs the wearer nothing.
106
+ */
107
+ export declare const MIC_USE_CASE_LOUDNESS_GATE: Record<MicUseCase, boolean>;
61
108
  /**
62
109
  * Packages allowed to hold a `voice_call` session.
63
110
  *
@@ -1 +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"}
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;IACb,uCAAuC;IACvC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,4EAA4E;AAC5E,eAAO,MAAM,OAAO,UAA+D,CAAA;AAEnF;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B;;;;;;CAM/B,CAAA;AAEV;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC,gBAAgB,CAAC,CAU7E;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;IAClC,gFAAgF;IAChF,YAAY,EAAE,OAAO,GAAG,IAAI,CAAA;CAC7B,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;;;;;;;;;;GAUG;AACH,eAAO,MAAM,0BAA0B,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,CAKlE,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,CAwBnB"}
@@ -9,6 +9,41 @@
9
9
  * bookkeeping lives in MicSessionManager, the merge with OS preferences lives
10
10
  * in MicStateCoordinator, and the wire write lives below that again.
11
11
  */
12
+ /** `codec_adc_vol[]`: index to dB. Index 0 is mute and is never offered. */
13
+ export const GAIN_DB = [-99, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 32];
14
+ /**
15
+ * Mirrored from `center_mic_vad_get_default_config` and `CODEC_SADC_VOL`.
16
+ *
17
+ * The four RMS numbers are measured *after* the ADC gain, so they are only
18
+ * meaningful next to the gain they were calibrated at, which is index 15.
19
+ */
20
+ export const MIC_TUNING_FIRMWARE_DEFAULTS = {
21
+ gain: 15,
22
+ open: 1350,
23
+ close: 945,
24
+ sp_open: 2900,
25
+ sp_close: 1600,
26
+ };
27
+ /**
28
+ * Re-express the firmware's thresholds at a different gain.
29
+ *
30
+ * The gate compares post-gain RMS, so moving the gain without moving the
31
+ * thresholds silently changes what the gate means: at -8 dB the wearer has to
32
+ * be 2.5x louder to clear a number that was chosen for +32 dB. Scaling both by
33
+ * the same factor keeps the *acoustic* trip point the profile was tuned for,
34
+ * for the wearer and for speaker leak alike.
35
+ */
36
+ export function scaleMicTuningToGain(gain) {
37
+ const index = Math.min(GAIN_DB.length - 1, Math.max(1, Math.round(gain)));
38
+ const factor = Math.pow(10, (GAIN_DB[index] - GAIN_DB[MIC_TUNING_FIRMWARE_DEFAULTS.gain]) / 20);
39
+ return {
40
+ gain: index,
41
+ open: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.open * factor),
42
+ close: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.close * factor),
43
+ sp_open: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.sp_open * factor),
44
+ sp_close: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.sp_close * factor),
45
+ };
46
+ }
12
47
  /**
13
48
  * Mentra Live ships CODEC_SADC_VOL = 15, the last entry of codec_adc_vol[] and
14
49
  * +32 dB. That table steps 2 dB at a time everywhere except the final step,
@@ -16,21 +51,38 @@
16
51
  * step above everything else. It suits a wearer dictating to a transcription
17
52
  * miniapp across a room; it clips a wearer talking into a Teams call.
18
53
  *
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
54
  * Index 14 is +26 dB: one step down, and the step that removes the table's
24
55
  * anomalous jump. A same-voice sweep showed 15 rails hard (2.5–4.2% clip);
25
56
  * 14/13/12 still kiss the rail on syllable tips. 13 is +24 dB, the middle
26
57
  * of that band, while we listen for loudness vs residual clip.
58
+ *
59
+ * The thresholds ride along with the gain rather than being stated here, so
60
+ * changing the index above cannot leave the gate calibrated for a level the
61
+ * ADC no longer produces.
27
62
  */
28
63
  export const MIC_USE_CASE_PROFILES = {
29
- voice_call: { gain: 13 },
64
+ voice_call: scaleMicTuningToGain(13),
30
65
  transcription: {},
31
66
  voice_assistant: {},
32
67
  diagnostic: {},
33
68
  };
69
+ /**
70
+ * Use cases that run the center-mic loudness gate ("Barrier").
71
+ *
72
+ * A voice call is the one place it earns its keep. There is no echo canceller
73
+ * on the LC3 uplink — `MENTRA_LC3_SPEECH_PROCESSING` is 0 and the capture path
74
+ * ignores the playback reference — so without a gate the far end hears itself
75
+ * through the wearer's speaker. Barrier is the safe half of the firmware's two
76
+ * gates: it zeroes a quiet frame but never stops transmitting, where VAD drops
77
+ * the stream outright. Paired with the speaker-elevated thresholds above, that
78
+ * suppresses leak while the far end is talking and costs the wearer nothing.
79
+ */
80
+ export const MIC_USE_CASE_LOUDNESS_GATE = {
81
+ voice_call: true,
82
+ transcription: false,
83
+ voice_assistant: false,
84
+ diagnostic: false,
85
+ };
34
86
  /**
35
87
  * Packages allowed to hold a `voice_call` session.
36
88
  *
@@ -61,22 +113,28 @@ export const ENGINE_ONLY_USE_CASES = ["diagnostic"];
61
113
  export function resolveMicPolicy(sessions, caps) {
62
114
  const effective = caps.glassesPcmUplink ? sessions : sessions.filter((s) => s.source !== "glasses");
63
115
  let pinGlasses = false;
64
- let gain;
116
+ let loudnessGate = false;
117
+ let winning = null;
65
118
  for (const session of effective) {
66
119
  if (session.source !== "glasses")
67
120
  continue;
68
121
  pinGlasses = true;
69
- const profileGain = MIC_USE_CASE_PROFILES[session.useCase]?.gain;
122
+ if (MIC_USE_CASE_LOUDNESS_GATE[session.useCase])
123
+ loudnessGate = true;
124
+ const profile = MIC_USE_CASE_PROFILES[session.useCase];
125
+ if (typeof profile?.gain !== "number")
126
+ continue;
70
127
  // 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
- }
128
+ // quiet assistant is not. The whole profile travels with it, because its
129
+ // thresholds are only meaningful at its own gain.
130
+ if (winning === null || profile.gain < winning.gain)
131
+ winning = profile;
75
132
  }
76
133
  return {
77
134
  rawPcm: effective.length > 0,
78
135
  pinGlasses,
79
- micTuning: gain === undefined ? null : { gain },
136
+ micTuning: winning,
137
+ loudnessGate: pinGlasses ? loudnessGate : null,
80
138
  };
81
139
  }
82
140
  //# sourceMappingURL=micPolicy.js.map