@mentra/engine 3.2.1-dev.277 → 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
@@ -0,0 +1,83 @@
1
+ /**
2
+ * MicSessionManager
3
+ *
4
+ * The engine's microphone ownership layer. Applications acquire a lease that
5
+ * says what they are doing; this class tracks the live leases, resolves them
6
+ * through `micPolicy`, and pushes the result to MicStateCoordinator.
7
+ *
8
+ * Everything above it is semantic ("I need a glasses microphone for a voice
9
+ * call"). Everything below it is hardware. Audio *sinks* — AcsMeetingService
10
+ * today, another transport later — are downstream of the PCM this produces and
11
+ * must not reach past it to the coordinator or the Bluetooth SDK.
12
+ */
13
+ import { type MicPlatformCaps, type MicSource, type MicUseCase } from "./micPolicy";
14
+ /** Raised when a second source is requested while another is already live. */
15
+ export declare const MIC_SOURCE_CONFLICT = "MIC_SOURCE_CONFLICT";
16
+ export interface MicSessionOptions {
17
+ /** Package name for a miniapp, or `engine:<name>` for an engine feature. */
18
+ owner: string;
19
+ source: MicSource;
20
+ useCase: MicUseCase;
21
+ }
22
+ export interface MicSession {
23
+ readonly id: number;
24
+ readonly owner: string;
25
+ readonly source: MicSource;
26
+ readonly useCase: MicUseCase;
27
+ /** Idempotent. Releasing a session that is already gone is a no-op. */
28
+ release(): void;
29
+ }
30
+ declare class MicSessionManager {
31
+ private static instance;
32
+ private readonly sessions;
33
+ private nextId;
34
+ private caps;
35
+ /** Last pin we asked for, so a no-op change does not re-enter native. */
36
+ private pinned;
37
+ /**
38
+ * One 15→14→15→13 walk per voice-call generation. Cleared when the last
39
+ * glasses voice_call session drops so the next join can measure again.
40
+ */
41
+ private sweepFinished;
42
+ /** One-shot A/B walk. Off now that 15/14/13 is measured; Super Mode can still start it. */
43
+ private sweepEnabled;
44
+ private constructor();
45
+ static getInstance(): MicSessionManager;
46
+ /** Test seam. Production reads the platform once at construction. */
47
+ setPlatformCaps(caps: MicPlatformCaps): void;
48
+ /**
49
+ * Take a microphone lease.
50
+ *
51
+ * Throws [MIC_SOURCE_CONFLICT] when a different source is already live: only
52
+ * the glasses can be pinned, so silently mixing sources would leave one
53
+ * consumer reading a microphone it did not ask for. That is the failure ACS
54
+ * already guards against frame by frame.
55
+ */
56
+ acquire(options: MicSessionOptions): MicSession;
57
+ /** Drop every lease an owner holds. The backstop for unregister / disconnect. */
58
+ releaseOwner(owner: string): boolean;
59
+ /**
60
+ * Whether an owner holds a glasses lease.
61
+ *
62
+ * Ownership only. Whether that lease has any hardware effect is
63
+ * [resolveMicPolicy]'s business, so on a platform without a glasses PCM
64
+ * uplink this still answers true while nothing is claimed.
65
+ */
66
+ hasGlassesSession(owner: string): boolean;
67
+ releaseAll(): void;
68
+ cleanup(): void;
69
+ /** Tests disable the auto-walk so acquire still means "policy 14". */
70
+ setCallGainSweepEnabled(enabled: boolean): void;
71
+ /**
72
+ * Start or restart the 15→14→15→13 comparison on a live voice_call session.
73
+ * Super Mode uses this so a call already in progress can be measured.
74
+ */
75
+ startCallGainSweep(): boolean;
76
+ private releaseId;
77
+ private hasVoiceCallGlasses;
78
+ private stopSweep;
79
+ private recompute;
80
+ }
81
+ declare const micSessionManager: MicSessionManager;
82
+ export default micSessionManager;
83
+ //# sourceMappingURL=MicSessionManager.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,198 @@
1
+ /**
2
+ * MicSessionManager
3
+ *
4
+ * The engine's microphone ownership layer. Applications acquire a lease that
5
+ * says what they are doing; this class tracks the live leases, resolves them
6
+ * through `micPolicy`, and pushes the result to MicStateCoordinator.
7
+ *
8
+ * Everything above it is semantic ("I need a glasses microphone for a voice
9
+ * call"). Everything below it is hardware. Audio *sinks* — AcsMeetingService
10
+ * today, another transport later — are downstream of the PCM this produces and
11
+ * must not reach past it to the coordinator or the Bluetooth SDK.
12
+ */
13
+ import { Platform } from "react-native";
14
+ import BluetoothSdk from "@mentra/bluetooth-sdk/internal";
15
+ import { getCallGainSweep } from "./CallGainSweep";
16
+ import micStateCoordinator from "./MicStateCoordinator";
17
+ import { resolveMicPolicy, } from "./micPolicy";
18
+ const LOG_TAG = "MIC_SESSION";
19
+ /** Raised when a second source is requested while another is already live. */
20
+ export const MIC_SOURCE_CONFLICT = "MIC_SOURCE_CONFLICT";
21
+ function defaultCaps() {
22
+ return {
23
+ // iOS has no `setMicSourcePin`, so it cannot promise the phone microphone
24
+ // stays shut, and `glassesLc3UplinkSupported` never selects the BLE LC3
25
+ // uplink there.
26
+ glassesPcmUplink: Platform.OS === "android" && typeof BluetoothSdk.setMicSourcePin === "function",
27
+ };
28
+ }
29
+ class MicSessionManager {
30
+ static instance = null;
31
+ sessions = new Map();
32
+ nextId = 1;
33
+ caps = defaultCaps();
34
+ /** Last pin we asked for, so a no-op change does not re-enter native. */
35
+ pinned = false;
36
+ /**
37
+ * One 15→14→15→13 walk per voice-call generation. Cleared when the last
38
+ * glasses voice_call session drops so the next join can measure again.
39
+ */
40
+ sweepFinished = false;
41
+ /** One-shot A/B walk. Off now that 15/14/13 is measured; Super Mode can still start it. */
42
+ sweepEnabled = false;
43
+ constructor() { }
44
+ static getInstance() {
45
+ if (!MicSessionManager.instance) {
46
+ MicSessionManager.instance = new MicSessionManager();
47
+ }
48
+ return MicSessionManager.instance;
49
+ }
50
+ /** Test seam. Production reads the platform once at construction. */
51
+ setPlatformCaps(caps) {
52
+ this.caps = caps;
53
+ }
54
+ /**
55
+ * Take a microphone lease.
56
+ *
57
+ * Throws [MIC_SOURCE_CONFLICT] when a different source is already live: only
58
+ * the glasses can be pinned, so silently mixing sources would leave one
59
+ * consumer reading a microphone it did not ask for. That is the failure ACS
60
+ * already guards against frame by frame.
61
+ */
62
+ acquire(options) {
63
+ const conflicting = [...this.sessions.values()].find((s) => s.source !== options.source);
64
+ if (conflicting) {
65
+ throw new Error(`${MIC_SOURCE_CONFLICT}: ${options.owner} asked for "${options.source}" while ` +
66
+ `${conflicting.owner} holds "${conflicting.source}"`);
67
+ }
68
+ const id = this.nextId++;
69
+ this.sessions.set(id, { id, owner: options.owner, source: options.source, useCase: options.useCase });
70
+ console.log(`${LOG_TAG}: acquire #${id} ${options.owner} ${options.useCase}/${options.source}`);
71
+ this.recompute();
72
+ return {
73
+ id,
74
+ owner: options.owner,
75
+ source: options.source,
76
+ useCase: options.useCase,
77
+ release: () => this.releaseId(id),
78
+ };
79
+ }
80
+ /** Drop every lease an owner holds. The backstop for unregister / disconnect. */
81
+ releaseOwner(owner) {
82
+ let removed = false;
83
+ for (const [id, session] of this.sessions) {
84
+ if (session.owner !== owner)
85
+ continue;
86
+ this.sessions.delete(id);
87
+ removed = true;
88
+ }
89
+ if (removed) {
90
+ console.log(`${LOG_TAG}: released all sessions for ${owner}`);
91
+ this.recompute();
92
+ }
93
+ return removed;
94
+ }
95
+ /**
96
+ * Whether an owner holds a glasses lease.
97
+ *
98
+ * Ownership only. Whether that lease has any hardware effect is
99
+ * [resolveMicPolicy]'s business, so on a platform without a glasses PCM
100
+ * uplink this still answers true while nothing is claimed.
101
+ */
102
+ hasGlassesSession(owner) {
103
+ for (const session of this.sessions.values()) {
104
+ if (session.owner === owner && session.source === "glasses")
105
+ return true;
106
+ }
107
+ return false;
108
+ }
109
+ releaseAll() {
110
+ if (this.sessions.size === 0) {
111
+ this.stopSweep("released_all");
112
+ return;
113
+ }
114
+ this.sessions.clear();
115
+ console.log(`${LOG_TAG}: released all sessions`);
116
+ this.recompute();
117
+ }
118
+ cleanup() {
119
+ this.stopSweep("cleanup");
120
+ this.sessions.clear();
121
+ this.pinned = false;
122
+ this.sweepFinished = false;
123
+ MicSessionManager.instance = null;
124
+ }
125
+ /** Tests disable the auto-walk so acquire still means "policy 14". */
126
+ setCallGainSweepEnabled(enabled) {
127
+ this.sweepEnabled = enabled;
128
+ if (!enabled)
129
+ this.stopSweep("disabled");
130
+ }
131
+ /**
132
+ * Start or restart the 15→14→15→13 comparison on a live voice_call session.
133
+ * Super Mode uses this so a call already in progress can be measured.
134
+ */
135
+ startCallGainSweep() {
136
+ if (!this.hasVoiceCallGlasses()) {
137
+ console.warn(`${LOG_TAG}: CALL_GAIN_SWEEP ignored — no live voice_call glasses session`);
138
+ return false;
139
+ }
140
+ if (micStateCoordinator.hasConfiguredMicTuning()) {
141
+ console.warn(`${LOG_TAG}: CALL_GAIN_SWEEP Super Mode mic-tuning is set; reset it or the glasses will ignore the sweep`);
142
+ }
143
+ this.sweepFinished = false;
144
+ return getCallGainSweep().restart((gain) => micStateCoordinator.setSessionMicTuning({ gain }), () => {
145
+ this.sweepFinished = true;
146
+ this.recompute();
147
+ });
148
+ }
149
+ releaseId(id) {
150
+ if (!this.sessions.delete(id))
151
+ return;
152
+ console.log(`${LOG_TAG}: release #${id}`);
153
+ this.recompute();
154
+ }
155
+ hasVoiceCallGlasses() {
156
+ if (!this.caps.glassesPcmUplink)
157
+ return false;
158
+ for (const session of this.sessions.values()) {
159
+ if (session.useCase === "voice_call" && session.source === "glasses")
160
+ return true;
161
+ }
162
+ return false;
163
+ }
164
+ stopSweep(reason) {
165
+ if (getCallGainSweep().isActive())
166
+ getCallGainSweep().stop(reason);
167
+ }
168
+ recompute() {
169
+ const policy = resolveMicPolicy([...this.sessions.values()], this.caps);
170
+ const voiceCall = this.hasVoiceCallGlasses();
171
+ if (!voiceCall) {
172
+ this.stopSweep("session_ended");
173
+ this.sweepFinished = false;
174
+ }
175
+ // Pin before claiming PCM, so the first frame the claim produces is
176
+ // already from the right microphone and the phone mic is never opened.
177
+ if (policy.pinGlasses !== this.pinned) {
178
+ this.pinned = policy.pinGlasses;
179
+ void Promise.resolve(BluetoothSdk.setMicSourcePin?.(policy.pinGlasses ? "glasses" : null)).catch((error) => {
180
+ console.warn(`${LOG_TAG}: setMicSourcePin failed`, error);
181
+ });
182
+ }
183
+ micStateCoordinator.setSessionRequirement(policy.rawPcm);
184
+ if (getCallGainSweep().isActive()) {
185
+ const gain = getCallGainSweep().currentGain();
186
+ micStateCoordinator.setSessionMicTuning(gain == null ? policy.micTuning : { gain });
187
+ return;
188
+ }
189
+ if (this.sweepEnabled && voiceCall && !this.sweepFinished) {
190
+ this.startCallGainSweep();
191
+ return;
192
+ }
193
+ micStateCoordinator.setSessionMicTuning(policy.micTuning);
194
+ }
195
+ }
196
+ const micSessionManager = MicSessionManager.getInstance();
197
+ export default micSessionManager;
198
+ //# sourceMappingURL=MicSessionManager.js.map
@@ -0,0 +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"]}
@@ -6,26 +6,46 @@
6
6
  * Local miniapps subscribe to audio_chunk / transcription streams.
7
7
  * This coordinator pushes the aggregate local requirement set to BluetoothSdk
8
8
  * so the mic runs whenever at least one local consumer needs it.
9
+ *
10
+ * It also merges what the live microphone sessions require (MicSessionManager,
11
+ * via `setSessionRequirement` / `setSessionMicTuning`) with the OS preferences
12
+ * the settings store derives. Applications go through MicSessionManager; this
13
+ * class is an engine implementation detail.
9
14
  */
15
+ import type { MicTuningProfile } from "./micPolicy";
10
16
  type MicGate = "vad" | "loudnessGate";
11
17
  interface ConfiguredMicGates {
12
18
  vadEnabled?: boolean | null;
13
19
  loudnessGateEnabled?: boolean | null;
20
+ micTuning?: Record<string, number> | null;
14
21
  }
15
22
  declare class MicStateCoordinator {
16
23
  private static instance;
17
24
  private localWantsPcm;
18
25
  private localWantsLc3;
19
26
  /**
20
- * A live ACS call taking the wearer's voice off the glasses over BLE LC3.
27
+ * A live microphone session held through MicSessionManager a voice call today.
21
28
  *
22
29
  * Tracked separately from the miniapp requirement because the two have independent lifetimes:
23
30
  * the call miniapp does not subscribe to `audio_chunk`, and a captions miniapp that stops mid-call
24
31
  * must not take the call's microphone with it.
25
32
  */
26
- private callWantsPcm;
33
+ private sessionWantsPcm;
27
34
  private configuredVad;
28
35
  private configuredLoudnessGate;
36
+ /** `mic_tuning` as the settings store last derived it: `super_mode ? desired : {}`. */
37
+ private configuredMicTuning;
38
+ /** Tuning required by the live sessions, resolved by micPolicy. */
39
+ private sessionMicTuning;
40
+ /**
41
+ * Latched once a session profile has actually been written.
42
+ *
43
+ * Before that, `mic_tuning` stays out of every patch, so a device that never
44
+ * runs a profile does not carry the key on unrelated mic writes. After it,
45
+ * the OS value keeps being restated, which is what stops a reconnect after a
46
+ * call from resurrecting the profile.
47
+ */
48
+ private sessionMicTuningWritten;
29
49
  private readonly miniappVadOverrides;
30
50
  private readonly miniappLoudnessGateOverrides;
31
51
  private overrideSequence;
@@ -40,19 +60,42 @@ declare class MicStateCoordinator {
40
60
  lc3: boolean;
41
61
  } & ConfiguredMicGates): void;
42
62
  /**
43
- * Claim or release raw PCM on behalf of an active call.
63
+ * Claim or release raw PCM on behalf of the live microphone sessions.
44
64
  *
45
- * Called by AcsMeetingService around a call whose uplink is the glasses microphone over BLE LC3.
65
+ * MicSessionManager only. Applications acquire a session; they do not reach past it to here.
46
66
  * Releasing is a claim release, not a mic shutdown: if a captions miniapp still wants PCM the
47
67
  * microphone stays on, which is the whole reason this is a separate flag rather than a setter on
48
68
  * the local requirement.
49
69
  */
50
- setCallRequirement(pcm: boolean): void;
70
+ setSessionRequirement(pcm: boolean): void;
71
+ /**
72
+ * Apply or drop the tuning the live sessions require.
73
+ *
74
+ * MicSessionManager only. Rides `applyUnion` so the profile and the PCM claim land in one
75
+ * debounced write, resolved at flush time: a session released inside the debounce window wins
76
+ * over the value that was queued.
77
+ */
78
+ setSessionMicTuning(profile: MicTuningProfile | null): void;
79
+ /** Last session profile queued, or null when the OS value is in force. */
80
+ getSessionMicTuning(): MicTuningProfile | null;
81
+ /**
82
+ * Super Mode sliders outrank a session profile. A live override means a gain
83
+ * sweep would write to the coordinator and the glasses would ignore it.
84
+ */
85
+ hasConfiguredMicTuning(): boolean;
51
86
  /**
52
87
  * Whether anything on this device needs a continuous raw-PCM timeline. Also the condition that
53
88
  * forces hardware VAD off: a gate that drops silence turns a call into clipped half-words.
54
89
  */
55
90
  private get wantsRawPcm();
91
+ /**
92
+ * The session profile, but only when it wins.
93
+ *
94
+ * A live Super Mode tuning value outranks it: that screen is how a profile's numbers get found
95
+ * on a real call in the first place. Emptiness is by key count — the settings store hands back a
96
+ * fresh `{}` every time, so reference checks would never match.
97
+ */
98
+ private winningSessionMicTuning;
56
99
  /**
57
100
  * Apply a miniapp-owned gate override without changing the OS preference.
58
101
  * 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;;;;;;;;GAQG;AAQH,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;CACrC;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,YAAY,CAAQ;IAC5B,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,sBAAsB,CAAqB;IACnD,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,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI;IAO7C;;;OAGG;IACH,OAAO,KAAK,WAAW,GAEtB;IAED;;;;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;IAaxF;;;;OAIG;IACI,wBAAwB,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAO3F,OAAO,CAAC,2BAA2B;IAiBnC;;;OAGG;IACH,OAAO,CAAC,UAAU;IAoBlB,OAAO,CAAC,uBAAuB;IAW/B,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,kBAAkB;IAmB1B;;OAEG;IACI,KAAK,IAAI,IAAI;IAOb,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;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"}
@@ -6,6 +6,11 @@
6
6
  * Local miniapps subscribe to audio_chunk / transcription streams.
7
7
  * This coordinator pushes the aggregate local requirement set to BluetoothSdk
8
8
  * so the mic runs whenever at least one local consumer needs it.
9
+ *
10
+ * It also merges what the live microphone sessions require (MicSessionManager,
11
+ * via `setSessionRequirement` / `setSessionMicTuning`) with the OS preferences
12
+ * the settings store derives. Applications go through MicSessionManager; this
13
+ * class is an engine implementation detail.
9
14
  */
10
15
  import BluetoothSdk from "@mentra/bluetooth-sdk/internal";
11
16
  import { createDebouncedPatchFlusher } from "../utils/debouncedPatch";
@@ -32,15 +37,28 @@ class MicStateCoordinator {
32
37
  localWantsPcm = false;
33
38
  localWantsLc3 = false;
34
39
  /**
35
- * A live ACS call taking the wearer's voice off the glasses over BLE LC3.
40
+ * A live microphone session held through MicSessionManager a voice call today.
36
41
  *
37
42
  * Tracked separately from the miniapp requirement because the two have independent lifetimes:
38
43
  * the call miniapp does not subscribe to `audio_chunk`, and a captions miniapp that stops mid-call
39
44
  * must not take the call's microphone with it.
40
45
  */
41
- callWantsPcm = false;
46
+ sessionWantsPcm = false;
42
47
  configuredVad;
43
48
  configuredLoudnessGate;
49
+ /** `mic_tuning` as the settings store last derived it: `super_mode ? desired : {}`. */
50
+ configuredMicTuning = {};
51
+ /** Tuning required by the live sessions, resolved by micPolicy. */
52
+ sessionMicTuning = null;
53
+ /**
54
+ * Latched once a session profile has actually been written.
55
+ *
56
+ * Before that, `mic_tuning` stays out of every patch, so a device that never
57
+ * runs a profile does not carry the key on unrelated mic writes. After it,
58
+ * the OS value keeps being restated, which is what stops a reconnect after a
59
+ * call from resurrecting the profile.
60
+ */
61
+ sessionMicTuningWritten = false;
44
62
  miniappVadOverrides = new Map();
45
63
  miniappLoudnessGateOverrides = new Map();
46
64
  overrideSequence = 0;
@@ -63,26 +81,70 @@ class MicStateCoordinator {
63
81
  this.applyUnion();
64
82
  }
65
83
  /**
66
- * Claim or release raw PCM on behalf of an active call.
84
+ * Claim or release raw PCM on behalf of the live microphone sessions.
67
85
  *
68
- * Called by AcsMeetingService around a call whose uplink is the glasses microphone over BLE LC3.
86
+ * MicSessionManager only. Applications acquire a session; they do not reach past it to here.
69
87
  * Releasing is a claim release, not a mic shutdown: if a captions miniapp still wants PCM the
70
88
  * microphone stays on, which is the whole reason this is a separate flag rather than a setter on
71
89
  * the local requirement.
72
90
  */
73
- setCallRequirement(pcm) {
74
- if (this.callWantsPcm === pcm)
91
+ setSessionRequirement(pcm) {
92
+ if (this.sessionWantsPcm === pcm)
75
93
  return;
76
- this.callWantsPcm = pcm;
77
- console.log(`${LOG_TAG}: call requirement updated — pcm=${pcm}`);
94
+ this.sessionWantsPcm = pcm;
95
+ console.log(`${LOG_TAG}: session requirement updated — pcm=${pcm}`);
78
96
  this.applyUnion();
79
97
  }
98
+ /**
99
+ * Apply or drop the tuning the live sessions require.
100
+ *
101
+ * MicSessionManager only. Rides `applyUnion` so the profile and the PCM claim land in one
102
+ * debounced write, resolved at flush time: a session released inside the debounce window wins
103
+ * over the value that was queued.
104
+ */
105
+ setSessionMicTuning(profile) {
106
+ if (profile?.gain === this.sessionMicTuning?.gain)
107
+ return;
108
+ this.sessionMicTuning = profile;
109
+ if (profile)
110
+ this.sessionMicTuningWritten = true;
111
+ // Nothing was ever written, so there is nothing to restore.
112
+ else if (!this.sessionMicTuningWritten)
113
+ return;
114
+ console.log(`${LOG_TAG}: session mic tuning ${profile ? JSON.stringify(profile) : "cleared"}`);
115
+ this.applyUnion();
116
+ }
117
+ /** Last session profile queued, or null when the OS value is in force. */
118
+ getSessionMicTuning() {
119
+ return this.sessionMicTuning;
120
+ }
121
+ /**
122
+ * Super Mode sliders outrank a session profile. A live override means a gain
123
+ * sweep would write to the coordinator and the glasses would ignore it.
124
+ */
125
+ hasConfiguredMicTuning() {
126
+ return Object.keys(this.configuredMicTuning).length > 0;
127
+ }
80
128
  /**
81
129
  * Whether anything on this device needs a continuous raw-PCM timeline. Also the condition that
82
130
  * forces hardware VAD off: a gate that drops silence turns a call into clipped half-words.
83
131
  */
84
132
  get wantsRawPcm() {
85
- return this.localWantsPcm || this.callWantsPcm;
133
+ return this.localWantsPcm || this.sessionWantsPcm;
134
+ }
135
+ /**
136
+ * The session profile, but only when it wins.
137
+ *
138
+ * A live Super Mode tuning value outranks it: that screen is how a profile's numbers get found
139
+ * on a real call in the first place. Emptiness is by key count — the settings store hands back a
140
+ * fresh `{}` every time, so reference checks would never match.
141
+ */
142
+ winningSessionMicTuning() {
143
+ if (!this.sessionMicTuning)
144
+ return undefined;
145
+ if (Object.keys(this.configuredMicTuning).length > 0)
146
+ return undefined;
147
+ return { ...this.sessionMicTuning };
86
148
  }
87
149
  /**
88
150
  * Apply a miniapp-owned gate override without changing the OS preference.
@@ -139,6 +201,9 @@ class MicStateCoordinator {
139
201
  ? settings.voice_activity_detection_enabled
140
202
  : undefined,
141
203
  loudnessGateEnabled: typeof settings.loudness_gate_enabled === "boolean" ? settings.loudness_gate_enabled : undefined,
204
+ micTuning: settings.mic_tuning && typeof settings.mic_tuning === "object"
205
+ ? settings.mic_tuning
206
+ : undefined,
142
207
  });
143
208
  return this.applyActiveRuntimeOverrides(settings);
144
209
  }
@@ -166,6 +231,13 @@ class MicStateCoordinator {
166
231
  if (loudnessOverride) {
167
232
  runtimeSettings.loudness_gate_enabled = loudnessOverride.enabled;
168
233
  }
234
+ // BES forgets mic_tuning on disconnect, so the on-connect replay is what
235
+ // puts a live session's profile back.
236
+ const sessionTuning = this.winningSessionMicTuning();
237
+ if (sessionTuning)
238
+ runtimeSettings.mic_tuning = sessionTuning;
239
+ else if (this.sessionMicTuningWritten)
240
+ runtimeSettings.mic_tuning = this.configuredMicTuning;
169
241
  return runtimeSettings;
170
242
  }
171
243
  /**
@@ -197,6 +269,9 @@ class MicStateCoordinator {
197
269
  if (configured.loudnessGateEnabled !== undefined) {
198
270
  this.configuredLoudnessGate = configured.loudnessGateEnabled ?? undefined;
199
271
  }
272
+ if (configured.micTuning !== undefined) {
273
+ this.configuredMicTuning = configured.micTuning ?? {};
274
+ }
200
275
  }
201
276
  overridesFor(gate) {
202
277
  return gate === "vad" ? this.miniappVadOverrides : this.miniappLoudnessGateOverrides;
@@ -226,6 +301,11 @@ class MicStateCoordinator {
226
301
  else if (this.configuredLoudnessGate !== undefined) {
227
302
  patch.loudness_gate_enabled = this.configuredLoudnessGate;
228
303
  }
304
+ const sessionTuning = this.winningSessionMicTuning();
305
+ if (sessionTuning)
306
+ patch.mic_tuning = sessionTuning;
307
+ else if (this.sessionMicTuningWritten)
308
+ patch.mic_tuning = this.configuredMicTuning;
229
309
  return patch;
230
310
  }
231
311
  /**
@@ -234,7 +314,8 @@ class MicStateCoordinator {
234
314
  reset() {
235
315
  this.localWantsPcm = false;
236
316
  this.localWantsLc3 = false;
237
- this.callWantsPcm = false;
317
+ this.sessionWantsPcm = false;
318
+ this.sessionMicTuning = null;
238
319
  this.applyUnion();
239
320
  }
240
321
  cleanup() {