@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.
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/AcsMeetingService.d.ts +22 -6
- package/build/services/AcsMeetingService.d.ts.map +1 -1
- package/build/services/AcsMeetingService.js +39 -20
- package/build/services/AcsMeetingService.js.map +1 -1
- package/build/services/CallGainSweep.d.ts +97 -0
- package/build/services/CallGainSweep.d.ts.map +1 -0
- package/build/services/CallGainSweep.js +249 -0
- package/build/services/CallGainSweep.js.map +1 -0
- package/build/services/GlassesMicProbe.d.ts +2 -0
- package/build/services/GlassesMicProbe.d.ts.map +1 -1
- package/build/services/GlassesMicProbe.js +20 -19
- package/build/services/GlassesMicProbe.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +18 -0
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +131 -1
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MicSessionManager.d.ts +83 -0
- package/build/services/MicSessionManager.d.ts.map +1 -0
- package/build/services/MicSessionManager.js +198 -0
- package/build/services/MicSessionManager.js.map +1 -0
- package/build/services/MicStateCoordinator.d.ts +48 -5
- package/build/services/MicStateCoordinator.d.ts.map +1 -1
- package/build/services/MicStateCoordinator.js +91 -10
- package/build/services/MicStateCoordinator.js.map +1 -1
- package/build/services/micPolicy.d.ts +84 -0
- package/build/services/micPolicy.d.ts.map +1 -0
- package/build/services/micPolicy.js +82 -0
- package/build/services/micPolicy.js.map +1 -0
- package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
- package/build/stores/bluetoothSettingKeys.js +3 -0
- package/build/stores/bluetoothSettingKeys.js.map +1 -1
- package/build/stores/settings.d.ts +5 -0
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +62 -0
- package/build/stores/settings.js.map +1 -1
- package/build/utils/pcm16.d.ts +20 -0
- package/build/utils/pcm16.d.ts.map +1 -1
- package/build/utils/pcm16.js +32 -1
- package/build/utils/pcm16.js.map +1 -1
- package/package.json +8 -8
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/services/AcsMeetingService.ts +45 -20
- package/src/services/CallGainSweep.ts +318 -0
- package/src/services/GlassesMicProbe.ts +20 -17
- package/src/services/LocalMiniappRuntime.ts +152 -3
- package/src/services/MicSessionManager.ts +255 -0
- package/src/services/MicStateCoordinator.ts +91 -10
- package/src/services/micPolicy.ts +129 -0
- package/src/stores/bluetoothSettingKeys.ts +3 -0
- package/src/stores/settings.ts +66 -0
- 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
|
|
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
|
|
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
|
|
63
|
+
* Claim or release raw PCM on behalf of the live microphone sessions.
|
|
44
64
|
*
|
|
45
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
84
|
+
* Claim or release raw PCM on behalf of the live microphone sessions.
|
|
67
85
|
*
|
|
68
|
-
*
|
|
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
|
-
|
|
74
|
-
if (this.
|
|
91
|
+
setSessionRequirement(pcm) {
|
|
92
|
+
if (this.sessionWantsPcm === pcm)
|
|
75
93
|
return;
|
|
76
|
-
this.
|
|
77
|
-
console.log(`${LOG_TAG}:
|
|
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.
|
|
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.
|
|
317
|
+
this.sessionWantsPcm = false;
|
|
318
|
+
this.sessionMicTuning = null;
|
|
238
319
|
this.applyUnion();
|
|
239
320
|
}
|
|
240
321
|
cleanup() {
|