@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,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timed A/B/A/C gain comparison for a live voice call.
|
|
3
|
+
*
|
|
4
|
+
* Mentra Live's last ADC step is +32 dB (index 15); the step below is +26 dB
|
|
5
|
+
* (14), then 2 dB per index. Close-talk clips at 15 and still rails at 14, so
|
|
6
|
+
* a call has to be measured at 15 → 14 → 15 → 13 on the same voice, not by
|
|
7
|
+
* swapping phones or days.
|
|
8
|
+
*
|
|
9
|
+
* Production policy stays in micPolicy. This object only writes a temporary
|
|
10
|
+
* override and logs speech-gated clip stats so a human can pick the index.
|
|
11
|
+
*/
|
|
12
|
+
import { type Pcm16Level } from "../utils/pcm16";
|
|
13
|
+
export declare const CALL_GAIN_SWEEP_STEPS: readonly [{
|
|
14
|
+
readonly gain: 15;
|
|
15
|
+
readonly label: "15a";
|
|
16
|
+
}, {
|
|
17
|
+
readonly gain: 14;
|
|
18
|
+
readonly label: "14";
|
|
19
|
+
}, {
|
|
20
|
+
readonly gain: 15;
|
|
21
|
+
readonly label: "15b";
|
|
22
|
+
}, {
|
|
23
|
+
readonly gain: 13;
|
|
24
|
+
readonly label: "13";
|
|
25
|
+
}];
|
|
26
|
+
export declare const CALL_GAIN_SWEEP_STEP_MS = 25000;
|
|
27
|
+
/** Quiet room on this path is meanAbs ≈ 90–120. Talk starts well above this. */
|
|
28
|
+
export declare const CALL_GAIN_SWEEP_SPEECH_MEAN_ABS = 400;
|
|
29
|
+
export type CallGainSweepStep = (typeof CALL_GAIN_SWEEP_STEPS)[number];
|
|
30
|
+
export type CallGainSweepPhaseSummary = {
|
|
31
|
+
label: string;
|
|
32
|
+
gain: number;
|
|
33
|
+
db: number;
|
|
34
|
+
windows: number;
|
|
35
|
+
speechWindows: number;
|
|
36
|
+
quietWindows: number;
|
|
37
|
+
clippedWindows: number;
|
|
38
|
+
nearClipWindows: number;
|
|
39
|
+
speechMeanAbs: number;
|
|
40
|
+
speechPeakMax: number;
|
|
41
|
+
speechPeakPct: number;
|
|
42
|
+
speechClipPct: number;
|
|
43
|
+
speechNearClipPct: number;
|
|
44
|
+
samples: number;
|
|
45
|
+
speechSamples: number;
|
|
46
|
+
clipped: number;
|
|
47
|
+
nearClip: number;
|
|
48
|
+
};
|
|
49
|
+
export type CallGainSweepRecommendation = {
|
|
50
|
+
gain: number;
|
|
51
|
+
reason: string;
|
|
52
|
+
};
|
|
53
|
+
export type CallGainSweepClock = {
|
|
54
|
+
now: () => number;
|
|
55
|
+
setTimeout: (fn: () => void, ms: number) => unknown;
|
|
56
|
+
clearTimeout: (id: unknown) => void;
|
|
57
|
+
log: (...args: unknown[]) => void;
|
|
58
|
+
};
|
|
59
|
+
export declare function gainDb(index: number): number;
|
|
60
|
+
/**
|
|
61
|
+
* Highest gain whose speech windows stay under the clip lines.
|
|
62
|
+
*
|
|
63
|
+
* Clip is the failure (rail = 32767). Near-clip at 30000 is the warning that
|
|
64
|
+
* the next syllable will hit the rail. 15 is only legal if both 15a and 15b
|
|
65
|
+
* agree; otherwise a loud stretch on one of them was luck.
|
|
66
|
+
*/
|
|
67
|
+
export declare function recommendCallGain(phases: readonly CallGainSweepPhaseSummary[]): CallGainSweepRecommendation;
|
|
68
|
+
export declare class CallGainSweep {
|
|
69
|
+
private readonly clock;
|
|
70
|
+
private applyGain;
|
|
71
|
+
private onDone;
|
|
72
|
+
private index;
|
|
73
|
+
private timer;
|
|
74
|
+
private enteredAt;
|
|
75
|
+
private bucket;
|
|
76
|
+
private readonly finished;
|
|
77
|
+
constructor(clock?: CallGainSweepClock);
|
|
78
|
+
isActive(): boolean;
|
|
79
|
+
currentGain(): number | null;
|
|
80
|
+
currentLabel(): string | null;
|
|
81
|
+
/**
|
|
82
|
+
* Begin the 15 → 14 → 15 → 13 walk.
|
|
83
|
+
* Returns false when a sweep is already mid-phase so a recompute cannot reset the clock.
|
|
84
|
+
*/
|
|
85
|
+
start(applyGain: (gain: number) => void, onDone?: () => void): boolean;
|
|
86
|
+
/** Super Mode / explicit re-run. Aborts the current phase if one is live. */
|
|
87
|
+
restart(applyGain: (gain: number) => void, onDone?: () => void): boolean;
|
|
88
|
+
stop(reason: string): void;
|
|
89
|
+
ingest(level: Pcm16Level): void;
|
|
90
|
+
private enter;
|
|
91
|
+
private advance;
|
|
92
|
+
private closePhase;
|
|
93
|
+
private clearTimer;
|
|
94
|
+
}
|
|
95
|
+
/** Process-wide sweep. MicSessionManager writes it; the ACS uplink meters it. */
|
|
96
|
+
export declare function getCallGainSweep(): CallGainSweep;
|
|
97
|
+
//# sourceMappingURL=CallGainSweep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CallGainSweep.d.ts","sourceRoot":"","sources":["../../src/services/CallGainSweep.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timed A/B/A/C gain comparison for a live voice call.
|
|
3
|
+
*
|
|
4
|
+
* Mentra Live's last ADC step is +32 dB (index 15); the step below is +26 dB
|
|
5
|
+
* (14), then 2 dB per index. Close-talk clips at 15 and still rails at 14, so
|
|
6
|
+
* a call has to be measured at 15 → 14 → 15 → 13 on the same voice, not by
|
|
7
|
+
* swapping phones or days.
|
|
8
|
+
*
|
|
9
|
+
* Production policy stays in micPolicy. This object only writes a temporary
|
|
10
|
+
* override and logs speech-gated clip stats so a human can pick the index.
|
|
11
|
+
*/
|
|
12
|
+
import { PCM16_FULL_SCALE, pcm16WindowStats } from "../utils/pcm16";
|
|
13
|
+
const LOG_TAG = "CALL_GAIN_SWEEP";
|
|
14
|
+
/** codec_adc_vol[] dB, same table as Super Mode. */
|
|
15
|
+
const GAIN_DB = [-99, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 32];
|
|
16
|
+
export const CALL_GAIN_SWEEP_STEPS = [
|
|
17
|
+
{ gain: 15, label: "15a" },
|
|
18
|
+
{ gain: 14, label: "14" },
|
|
19
|
+
{ gain: 15, label: "15b" },
|
|
20
|
+
{ gain: 13, label: "13" },
|
|
21
|
+
];
|
|
22
|
+
export const CALL_GAIN_SWEEP_STEP_MS = 25_000;
|
|
23
|
+
/** Quiet room on this path is meanAbs ≈ 90–120. Talk starts well above this. */
|
|
24
|
+
export const CALL_GAIN_SWEEP_SPEECH_MEAN_ABS = 400;
|
|
25
|
+
const defaultClock = () => ({
|
|
26
|
+
now: () => Date.now(),
|
|
27
|
+
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
28
|
+
clearTimeout: (id) => clearTimeout(id),
|
|
29
|
+
log: (...args) => console.log(...args),
|
|
30
|
+
});
|
|
31
|
+
export function gainDb(index) {
|
|
32
|
+
return GAIN_DB[index] ?? Number.NaN;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Highest gain whose speech windows stay under the clip lines.
|
|
36
|
+
*
|
|
37
|
+
* Clip is the failure (rail = 32767). Near-clip at 30000 is the warning that
|
|
38
|
+
* the next syllable will hit the rail. 15 is only legal if both 15a and 15b
|
|
39
|
+
* agree; otherwise a loud stretch on one of them was luck.
|
|
40
|
+
*/
|
|
41
|
+
export function recommendCallGain(phases) {
|
|
42
|
+
const speech = phases.filter((p) => p.speechWindows > 0);
|
|
43
|
+
if (speech.length === 0) {
|
|
44
|
+
return { gain: 13, reason: "no speech windows; stay at 13 until the sweep is talked through" };
|
|
45
|
+
}
|
|
46
|
+
const fifteen = speech.filter((p) => p.gain === 15);
|
|
47
|
+
const fourteen = speech.find((p) => p.gain === 14);
|
|
48
|
+
const thirteen = speech.find((p) => p.gain === 13);
|
|
49
|
+
const clean = (p) => p.speechClipPct < 0.5 && p.speechNearClipPct < 5;
|
|
50
|
+
const fifteenClean = fifteen.length >= 2 && fifteen.every(clean);
|
|
51
|
+
if (fifteenClean)
|
|
52
|
+
return { gain: 15, reason: "both 15 phases stayed under 0.5% clip / 5% near-clip" };
|
|
53
|
+
if (fourteen && clean(fourteen)) {
|
|
54
|
+
return { gain: 14, reason: "15 clips; 14 stayed under 0.5% clip / 5% near-clip" };
|
|
55
|
+
}
|
|
56
|
+
if (thirteen && clean(thirteen)) {
|
|
57
|
+
return { gain: 13, reason: "15 and 14 clip; 13 stayed under 0.5% clip / 5% near-clip" };
|
|
58
|
+
}
|
|
59
|
+
return { gain: 13, reason: "every measured step still clipped; 13 is the floor of this sweep" };
|
|
60
|
+
}
|
|
61
|
+
function emptyBucket(step) {
|
|
62
|
+
return {
|
|
63
|
+
step,
|
|
64
|
+
windows: 0,
|
|
65
|
+
speechWindows: 0,
|
|
66
|
+
quietWindows: 0,
|
|
67
|
+
clippedWindows: 0,
|
|
68
|
+
nearClipWindows: 0,
|
|
69
|
+
speechMeanAbsSum: 0,
|
|
70
|
+
speechPeakMax: 0,
|
|
71
|
+
samples: 0,
|
|
72
|
+
speechSamples: 0,
|
|
73
|
+
clipped: 0,
|
|
74
|
+
nearClip: 0,
|
|
75
|
+
speechClipped: 0,
|
|
76
|
+
speechNearClip: 0,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function summarizeBucket(bucket) {
|
|
80
|
+
const speechClipPct = bucket.speechSamples > 0 ? Math.round((bucket.speechClipped / bucket.speechSamples) * 1000) / 10 : 0;
|
|
81
|
+
const speechNearClipPct = bucket.speechSamples > 0 ? Math.round((bucket.speechNearClip / bucket.speechSamples) * 1000) / 10 : 0;
|
|
82
|
+
return {
|
|
83
|
+
label: bucket.step.label,
|
|
84
|
+
gain: bucket.step.gain,
|
|
85
|
+
db: gainDb(bucket.step.gain),
|
|
86
|
+
windows: bucket.windows,
|
|
87
|
+
speechWindows: bucket.speechWindows,
|
|
88
|
+
quietWindows: bucket.quietWindows,
|
|
89
|
+
clippedWindows: bucket.clippedWindows,
|
|
90
|
+
nearClipWindows: bucket.nearClipWindows,
|
|
91
|
+
speechMeanAbs: bucket.speechWindows > 0 ? Math.round(bucket.speechMeanAbsSum / bucket.speechWindows) : 0,
|
|
92
|
+
speechPeakMax: bucket.speechPeakMax,
|
|
93
|
+
speechPeakPct: Math.round((bucket.speechPeakMax / PCM16_FULL_SCALE) * 1000) / 10,
|
|
94
|
+
speechClipPct,
|
|
95
|
+
speechNearClipPct,
|
|
96
|
+
samples: bucket.samples,
|
|
97
|
+
speechSamples: bucket.speechSamples,
|
|
98
|
+
clipped: bucket.clipped,
|
|
99
|
+
nearClip: bucket.nearClip,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export class CallGainSweep {
|
|
103
|
+
clock;
|
|
104
|
+
applyGain = null;
|
|
105
|
+
onDone = null;
|
|
106
|
+
index = -1;
|
|
107
|
+
timer = null;
|
|
108
|
+
enteredAt = 0;
|
|
109
|
+
bucket = null;
|
|
110
|
+
finished = [];
|
|
111
|
+
constructor(clock = defaultClock()) {
|
|
112
|
+
this.clock = clock;
|
|
113
|
+
}
|
|
114
|
+
isActive() {
|
|
115
|
+
return this.index >= 0;
|
|
116
|
+
}
|
|
117
|
+
currentGain() {
|
|
118
|
+
return this.index >= 0 ? CALL_GAIN_SWEEP_STEPS[this.index].gain : null;
|
|
119
|
+
}
|
|
120
|
+
currentLabel() {
|
|
121
|
+
return this.index >= 0 ? CALL_GAIN_SWEEP_STEPS[this.index].label : null;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Begin the 15 → 14 → 15 → 13 walk.
|
|
125
|
+
* Returns false when a sweep is already mid-phase so a recompute cannot reset the clock.
|
|
126
|
+
*/
|
|
127
|
+
start(applyGain, onDone) {
|
|
128
|
+
if (this.isActive())
|
|
129
|
+
return false;
|
|
130
|
+
this.applyGain = applyGain;
|
|
131
|
+
this.onDone = onDone ?? null;
|
|
132
|
+
this.finished.length = 0;
|
|
133
|
+
this.enter(0);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
/** Super Mode / explicit re-run. Aborts the current phase if one is live. */
|
|
137
|
+
restart(applyGain, onDone) {
|
|
138
|
+
if (this.isActive())
|
|
139
|
+
this.stop("restart");
|
|
140
|
+
return this.start(applyGain, onDone);
|
|
141
|
+
}
|
|
142
|
+
stop(reason) {
|
|
143
|
+
if (!this.isActive() && this.timer == null)
|
|
144
|
+
return;
|
|
145
|
+
this.closePhase("aborted");
|
|
146
|
+
this.clearTimer();
|
|
147
|
+
this.clock.log(`${LOG_TAG} stop`, { reason, phases: this.finished.slice() });
|
|
148
|
+
this.index = -1;
|
|
149
|
+
this.bucket = null;
|
|
150
|
+
this.applyGain = null;
|
|
151
|
+
this.onDone = null;
|
|
152
|
+
}
|
|
153
|
+
ingest(level) {
|
|
154
|
+
if (!this.bucket)
|
|
155
|
+
return;
|
|
156
|
+
const stats = pcm16WindowStats(level);
|
|
157
|
+
const speech = level.meanAbs >= CALL_GAIN_SWEEP_SPEECH_MEAN_ABS;
|
|
158
|
+
this.bucket.windows++;
|
|
159
|
+
this.bucket.samples += level.samples;
|
|
160
|
+
this.bucket.clipped += level.clipped;
|
|
161
|
+
this.bucket.nearClip += level.nearClip;
|
|
162
|
+
if (speech) {
|
|
163
|
+
this.bucket.speechWindows++;
|
|
164
|
+
this.bucket.speechSamples += level.samples;
|
|
165
|
+
this.bucket.speechClipped += level.clipped;
|
|
166
|
+
this.bucket.speechNearClip += level.nearClip;
|
|
167
|
+
this.bucket.speechMeanAbsSum += level.meanAbs;
|
|
168
|
+
if (level.peak > this.bucket.speechPeakMax)
|
|
169
|
+
this.bucket.speechPeakMax = level.peak;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
this.bucket.quietWindows++;
|
|
173
|
+
}
|
|
174
|
+
if (level.clipped > 0)
|
|
175
|
+
this.bucket.clippedWindows++;
|
|
176
|
+
if (level.nearClip > 0)
|
|
177
|
+
this.bucket.nearClipWindows++;
|
|
178
|
+
this.clock.log(`${LOG_TAG} window`, {
|
|
179
|
+
label: this.bucket.step.label,
|
|
180
|
+
gain: this.bucket.step.gain,
|
|
181
|
+
db: gainDb(this.bucket.step.gain),
|
|
182
|
+
speech,
|
|
183
|
+
meanAbs: stats.meanAbs,
|
|
184
|
+
peak: stats.peak,
|
|
185
|
+
peakPct: stats.peakPct,
|
|
186
|
+
clipped: stats.clipped,
|
|
187
|
+
clipPct: stats.clipPct,
|
|
188
|
+
nearClip: stats.nearClip,
|
|
189
|
+
nearClipPct: stats.nearClipPct,
|
|
190
|
+
samples: stats.samples,
|
|
191
|
+
});
|
|
192
|
+
// RN pauses setTimeout while Mentra is behind Mentra Call. The ACS
|
|
193
|
+
// uplink still delivers 1 s windows, so the hold is measured here.
|
|
194
|
+
if (this.clock.now() - this.enteredAt >= CALL_GAIN_SWEEP_STEP_MS)
|
|
195
|
+
this.advance();
|
|
196
|
+
}
|
|
197
|
+
enter(index) {
|
|
198
|
+
this.index = index;
|
|
199
|
+
const step = CALL_GAIN_SWEEP_STEPS[index];
|
|
200
|
+
this.bucket = emptyBucket(step);
|
|
201
|
+
this.enteredAt = this.clock.now();
|
|
202
|
+
this.applyGain?.(step.gain);
|
|
203
|
+
this.clock.log(`${LOG_TAG} phase-start`, {
|
|
204
|
+
label: step.label,
|
|
205
|
+
gain: step.gain,
|
|
206
|
+
db: gainDb(step.gain),
|
|
207
|
+
holdMs: CALL_GAIN_SWEEP_STEP_MS,
|
|
208
|
+
remaining: CALL_GAIN_SWEEP_STEPS.length - index,
|
|
209
|
+
});
|
|
210
|
+
this.timer = this.clock.setTimeout(() => this.advance(), CALL_GAIN_SWEEP_STEP_MS);
|
|
211
|
+
}
|
|
212
|
+
advance() {
|
|
213
|
+
this.timer = null;
|
|
214
|
+
this.closePhase("complete");
|
|
215
|
+
const next = this.index + 1;
|
|
216
|
+
if (next >= CALL_GAIN_SWEEP_STEPS.length) {
|
|
217
|
+
const pick = recommendCallGain(this.finished);
|
|
218
|
+
this.clock.log(`${LOG_TAG} done`, { phases: this.finished.slice(), pick });
|
|
219
|
+
this.index = -1;
|
|
220
|
+
this.bucket = null;
|
|
221
|
+
const done = this.onDone;
|
|
222
|
+
this.applyGain = null;
|
|
223
|
+
this.onDone = null;
|
|
224
|
+
done?.();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
this.enter(next);
|
|
228
|
+
}
|
|
229
|
+
closePhase(how) {
|
|
230
|
+
if (!this.bucket || this.bucket.windows === 0)
|
|
231
|
+
return;
|
|
232
|
+
const summary = summarizeBucket(this.bucket);
|
|
233
|
+
this.finished.push(summary);
|
|
234
|
+
this.clock.log(`${LOG_TAG} phase-${how}`, summary);
|
|
235
|
+
this.bucket = null;
|
|
236
|
+
}
|
|
237
|
+
clearTimer() {
|
|
238
|
+
if (this.timer == null)
|
|
239
|
+
return;
|
|
240
|
+
this.clock.clearTimeout(this.timer);
|
|
241
|
+
this.timer = null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const sharedSweep = new CallGainSweep();
|
|
245
|
+
/** Process-wide sweep. MicSessionManager writes it; the ACS uplink meters it. */
|
|
246
|
+
export function getCallGainSweep() {
|
|
247
|
+
return sharedSweep;
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=CallGainSweep.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CallGainSweep.js","sourceRoot":"","sources":["../../src/services/CallGainSweep.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Timed A/B/A/C gain comparison for a live voice call.\n *\n * Mentra Live's last ADC step is +32 dB (index 15); the step below is +26 dB\n * (14), then 2 dB per index. Close-talk clips at 15 and still rails at 14, so\n * a call has to be measured at 15 → 14 → 15 → 13 on the same voice, not by\n * swapping phones or days.\n *\n * Production policy stays in micPolicy. This object only writes a temporary\n * override and logs speech-gated clip stats so a human can pick the index.\n */\n\nimport {PCM16_FULL_SCALE, pcm16WindowStats, type Pcm16Level} from \"../utils/pcm16\"\n\nconst LOG_TAG = \"CALL_GAIN_SWEEP\"\n\n/** codec_adc_vol[] dB, same table as Super Mode. */\nconst GAIN_DB = [-99, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 32]\n\nexport const CALL_GAIN_SWEEP_STEPS = [\n {gain: 15, label: \"15a\"},\n {gain: 14, label: \"14\"},\n {gain: 15, label: \"15b\"},\n {gain: 13, label: \"13\"},\n] as const\n\nexport const CALL_GAIN_SWEEP_STEP_MS = 25_000\n/** Quiet room on this path is meanAbs ≈ 90–120. Talk starts well above this. */\nexport const CALL_GAIN_SWEEP_SPEECH_MEAN_ABS = 400\n\nexport type CallGainSweepStep = (typeof CALL_GAIN_SWEEP_STEPS)[number]\n\nexport type CallGainSweepPhaseSummary = {\n label: string\n gain: number\n db: number\n windows: number\n speechWindows: number\n quietWindows: number\n clippedWindows: number\n nearClipWindows: number\n speechMeanAbs: number\n speechPeakMax: number\n speechPeakPct: number\n speechClipPct: number\n speechNearClipPct: number\n samples: number\n speechSamples: number\n clipped: number\n nearClip: number\n}\n\nexport type CallGainSweepRecommendation = {\n gain: number\n reason: string\n}\n\nexport type CallGainSweepClock = {\n now: () => number\n setTimeout: (fn: () => void, ms: number) => unknown\n clearTimeout: (id: unknown) => void\n log: (...args: unknown[]) => void\n}\n\ntype PhaseBucket = {\n step: CallGainSweepStep\n windows: number\n speechWindows: number\n quietWindows: number\n clippedWindows: number\n nearClipWindows: number\n speechMeanAbsSum: number\n speechPeakMax: number\n samples: number\n speechSamples: number\n clipped: number\n nearClip: number\n speechClipped: number\n speechNearClip: number\n}\n\nconst defaultClock = (): CallGainSweepClock => ({\n now: () => Date.now(),\n setTimeout: (fn, ms) => setTimeout(fn, ms),\n clearTimeout: (id) => clearTimeout(id as ReturnType<typeof setTimeout>),\n log: (...args) => console.log(...args),\n})\n\nexport function gainDb(index: number): number {\n return GAIN_DB[index] ?? Number.NaN\n}\n\n/**\n * Highest gain whose speech windows stay under the clip lines.\n *\n * Clip is the failure (rail = 32767). Near-clip at 30000 is the warning that\n * the next syllable will hit the rail. 15 is only legal if both 15a and 15b\n * agree; otherwise a loud stretch on one of them was luck.\n */\nexport function recommendCallGain(phases: readonly CallGainSweepPhaseSummary[]): CallGainSweepRecommendation {\n const speech = phases.filter((p) => p.speechWindows > 0)\n if (speech.length === 0) {\n return {gain: 13, reason: \"no speech windows; stay at 13 until the sweep is talked through\"}\n }\n\n const fifteen = speech.filter((p) => p.gain === 15)\n const fourteen = speech.find((p) => p.gain === 14)\n const thirteen = speech.find((p) => p.gain === 13)\n\n const clean = (p: CallGainSweepPhaseSummary) => p.speechClipPct < 0.5 && p.speechNearClipPct < 5\n const fifteenClean = fifteen.length >= 2 && fifteen.every(clean)\n if (fifteenClean) return {gain: 15, reason: \"both 15 phases stayed under 0.5% clip / 5% near-clip\"}\n if (fourteen && clean(fourteen)) {\n return {gain: 14, reason: \"15 clips; 14 stayed under 0.5% clip / 5% near-clip\"}\n }\n if (thirteen && clean(thirteen)) {\n return {gain: 13, reason: \"15 and 14 clip; 13 stayed under 0.5% clip / 5% near-clip\"}\n }\n return {gain: 13, reason: \"every measured step still clipped; 13 is the floor of this sweep\"}\n}\n\nfunction emptyBucket(step: CallGainSweepStep): PhaseBucket {\n return {\n step,\n windows: 0,\n speechWindows: 0,\n quietWindows: 0,\n clippedWindows: 0,\n nearClipWindows: 0,\n speechMeanAbsSum: 0,\n speechPeakMax: 0,\n samples: 0,\n speechSamples: 0,\n clipped: 0,\n nearClip: 0,\n speechClipped: 0,\n speechNearClip: 0,\n }\n}\n\nfunction summarizeBucket(bucket: PhaseBucket): CallGainSweepPhaseSummary {\n const speechClipPct =\n bucket.speechSamples > 0 ? Math.round((bucket.speechClipped / bucket.speechSamples) * 1000) / 10 : 0\n const speechNearClipPct =\n bucket.speechSamples > 0 ? Math.round((bucket.speechNearClip / bucket.speechSamples) * 1000) / 10 : 0\n return {\n label: bucket.step.label,\n gain: bucket.step.gain,\n db: gainDb(bucket.step.gain),\n windows: bucket.windows,\n speechWindows: bucket.speechWindows,\n quietWindows: bucket.quietWindows,\n clippedWindows: bucket.clippedWindows,\n nearClipWindows: bucket.nearClipWindows,\n speechMeanAbs:\n bucket.speechWindows > 0 ? Math.round(bucket.speechMeanAbsSum / bucket.speechWindows) : 0,\n speechPeakMax: bucket.speechPeakMax,\n speechPeakPct: Math.round((bucket.speechPeakMax / PCM16_FULL_SCALE) * 1000) / 10,\n speechClipPct,\n speechNearClipPct,\n samples: bucket.samples,\n speechSamples: bucket.speechSamples,\n clipped: bucket.clipped,\n nearClip: bucket.nearClip,\n }\n}\n\nexport class CallGainSweep {\n private readonly clock: CallGainSweepClock\n private applyGain: ((gain: number) => void) | null = null\n private onDone: (() => void) | null = null\n private index = -1\n private timer: unknown = null\n private enteredAt = 0\n private bucket: PhaseBucket | null = null\n private readonly finished: CallGainSweepPhaseSummary[] = []\n\n constructor(clock: CallGainSweepClock = defaultClock()) {\n this.clock = clock\n }\n\n isActive(): boolean {\n return this.index >= 0\n }\n\n currentGain(): number | null {\n return this.index >= 0 ? CALL_GAIN_SWEEP_STEPS[this.index].gain : null\n }\n\n currentLabel(): string | null {\n return this.index >= 0 ? CALL_GAIN_SWEEP_STEPS[this.index].label : null\n }\n\n /**\n * Begin the 15 → 14 → 15 → 13 walk.\n * Returns false when a sweep is already mid-phase so a recompute cannot reset the clock.\n */\n start(applyGain: (gain: number) => void, onDone?: () => void): boolean {\n if (this.isActive()) return false\n this.applyGain = applyGain\n this.onDone = onDone ?? null\n this.finished.length = 0\n this.enter(0)\n return true\n }\n\n /** Super Mode / explicit re-run. Aborts the current phase if one is live. */\n restart(applyGain: (gain: number) => void, onDone?: () => void): boolean {\n if (this.isActive()) this.stop(\"restart\")\n return this.start(applyGain, onDone)\n }\n\n stop(reason: string): void {\n if (!this.isActive() && this.timer == null) return\n this.closePhase(\"aborted\")\n this.clearTimer()\n this.clock.log(`${LOG_TAG} stop`, {reason, phases: this.finished.slice()})\n this.index = -1\n this.bucket = null\n this.applyGain = null\n this.onDone = null\n }\n\n ingest(level: Pcm16Level): void {\n if (!this.bucket) return\n const stats = pcm16WindowStats(level)\n const speech = level.meanAbs >= CALL_GAIN_SWEEP_SPEECH_MEAN_ABS\n this.bucket.windows++\n this.bucket.samples += level.samples\n this.bucket.clipped += level.clipped\n this.bucket.nearClip += level.nearClip\n if (speech) {\n this.bucket.speechWindows++\n this.bucket.speechSamples += level.samples\n this.bucket.speechClipped += level.clipped\n this.bucket.speechNearClip += level.nearClip\n this.bucket.speechMeanAbsSum += level.meanAbs\n if (level.peak > this.bucket.speechPeakMax) this.bucket.speechPeakMax = level.peak\n } else {\n this.bucket.quietWindows++\n }\n if (level.clipped > 0) this.bucket.clippedWindows++\n if (level.nearClip > 0) this.bucket.nearClipWindows++\n\n this.clock.log(`${LOG_TAG} window`, {\n label: this.bucket.step.label,\n gain: this.bucket.step.gain,\n db: gainDb(this.bucket.step.gain),\n speech,\n meanAbs: stats.meanAbs,\n peak: stats.peak,\n peakPct: stats.peakPct,\n clipped: stats.clipped,\n clipPct: stats.clipPct,\n nearClip: stats.nearClip,\n nearClipPct: stats.nearClipPct,\n samples: stats.samples,\n })\n // RN pauses setTimeout while Mentra is behind Mentra Call. The ACS\n // uplink still delivers 1 s windows, so the hold is measured here.\n if (this.clock.now() - this.enteredAt >= CALL_GAIN_SWEEP_STEP_MS) this.advance()\n }\n\n private enter(index: number): void {\n this.index = index\n const step = CALL_GAIN_SWEEP_STEPS[index]\n this.bucket = emptyBucket(step)\n this.enteredAt = this.clock.now()\n this.applyGain?.(step.gain)\n this.clock.log(`${LOG_TAG} phase-start`, {\n label: step.label,\n gain: step.gain,\n db: gainDb(step.gain),\n holdMs: CALL_GAIN_SWEEP_STEP_MS,\n remaining: CALL_GAIN_SWEEP_STEPS.length - index,\n })\n this.timer = this.clock.setTimeout(() => this.advance(), CALL_GAIN_SWEEP_STEP_MS)\n }\n\n private advance(): void {\n this.timer = null\n this.closePhase(\"complete\")\n const next = this.index + 1\n if (next >= CALL_GAIN_SWEEP_STEPS.length) {\n const pick = recommendCallGain(this.finished)\n this.clock.log(`${LOG_TAG} done`, {phases: this.finished.slice(), pick})\n this.index = -1\n this.bucket = null\n const done = this.onDone\n this.applyGain = null\n this.onDone = null\n done?.()\n return\n }\n this.enter(next)\n }\n\n private closePhase(how: \"complete\" | \"aborted\"): void {\n if (!this.bucket || this.bucket.windows === 0) return\n const summary = summarizeBucket(this.bucket)\n this.finished.push(summary)\n this.clock.log(`${LOG_TAG} phase-${how}`, summary)\n this.bucket = null\n }\n\n private clearTimer(): void {\n if (this.timer == null) return\n this.clock.clearTimeout(this.timer)\n this.timer = null\n }\n}\n\nconst sharedSweep = new CallGainSweep()\n\n/** Process-wide sweep. MicSessionManager writes it; the ACS uplink meters it. */\nexport function getCallGainSweep(): CallGainSweep {\n return sharedSweep\n}\n"]}
|
|
@@ -62,6 +62,8 @@ declare class GlassesMicProbe {
|
|
|
62
62
|
private nonGlasses;
|
|
63
63
|
/** `preferred_mic` before a `source=phone` control run changed it. */
|
|
64
64
|
private savedPreferredMic;
|
|
65
|
+
/** The probe's microphone lease, which owns the pin and the PCM claim while a run is live. */
|
|
66
|
+
private micSession;
|
|
65
67
|
private listeners;
|
|
66
68
|
private lastSample;
|
|
67
69
|
isRunning(): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GlassesMicProbe.d.ts","sourceRoot":"","sources":["../../src/services/GlassesMicProbe.ts"],"names":[],"mappings":"AAmBA,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAA;AAE1D;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,OAAO,CAAA;AAEhD,MAAM,MAAM,eAAe,GAAG;IAC5B,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAA;IAClB,mEAAmE;IACnE,IAAI,EAAE,gBAAgB,CAAA;IACtB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oDAAoD;IACpD,MAAM,CAAC,EAAE,cAAc,CAAA;CACxB,CAAA;AAKD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAAC,GAAG,eAAe,CAkB1G;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,uCAAuC;IACvC,CAAC,EAAE,MAAM,CAAA;IACT,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAA;IACf,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAA;IACZ,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAA;IACd,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAA;IACd,oEAAoE;IACpE,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,gBAAgB,CAAA;CACvB,CAAA;AAED,KAAK,QAAQ,GAAG,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"GlassesMicProbe.d.ts","sourceRoot":"","sources":["../../src/services/GlassesMicProbe.ts"],"names":[],"mappings":"AAmBA,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAA;AAE1D;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,OAAO,CAAA;AAEhD,MAAM,MAAM,eAAe,GAAG;IAC5B,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAA;IAClB,mEAAmE;IACnE,IAAI,EAAE,gBAAgB,CAAA;IACtB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oDAAoD;IACpD,MAAM,CAAC,EAAE,cAAc,CAAA;CACxB,CAAA;AAKD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAAC,GAAG,eAAe,CAkB1G;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,uCAAuC;IACvC,CAAC,EAAE,MAAM,CAAA;IACT,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAA;IACf,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAA;IACZ,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAA;IACd,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAA;IACd,oEAAoE;IACpE,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,gBAAgB,CAAA;CACvB,CAAA;AAED,KAAK,QAAQ,GAAG,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAA;AAUhD,OAAO,EAAC,WAAW,EAAE,cAAc,EAAC,MAAM,gBAAgB,CAAA;AAE1D,8FAA8F;AAC9F,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,gBAAgB,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,SAAM;;;EAWjH;AAWD,cAAM,eAAe;IACnB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,QAAQ,CAA6B;IAC7C,0FAA0F;IAC1F,OAAO,CAAC,UAAU,CAAI;IACtB,OAAO,CAAC,OAAO,CAA+B;IAC9C,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,MAAM,CAAoC;IAClD,OAAO,CAAC,IAAI,CAAsB;IAClC,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,UAAU,CAAI;IACtB,sEAAsE;IACtE,OAAO,CAAC,iBAAiB,CAAsB;IAC/C,8FAA8F;IAC9F,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,UAAU,CAA8B;IAEhD,SAAS,IAAI,OAAO;IAIpB,IAAI,IAAI,cAAc,GAAG,IAAI;IAI7B,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,IAAI;IAKnC,KAAK,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IA+C9C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA4B3B,OAAO,CAAC,MAAM;YAgCA,SAAS;YA8BT,QAAQ;CAOvB;AAED,QAAA,MAAM,eAAe,iBAAwB,CAAA;AAC7C,eAAe,eAAe,CAAA"}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import BluetoothSdk from "@mentra/bluetooth-sdk/internal";
|
|
13
13
|
import audioPlaybackService from "./AudioPlaybackService";
|
|
14
|
-
import
|
|
14
|
+
import micSessionManager from "./MicSessionManager";
|
|
15
15
|
import { SETTINGS, useSettingsStore } from "../stores/settings";
|
|
16
16
|
import { summarizePcm16 } from "../utils/pcm16";
|
|
17
17
|
import { BgTimer } from "../utils/timers";
|
|
@@ -43,6 +43,8 @@ export function parseMicProbeParams(params) {
|
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
const GLASSES = "glasses";
|
|
46
|
+
/** Lease owner for probe runs. Engine features own their sessions; miniapps cannot claim this. */
|
|
47
|
+
const MIC_PROBE_OWNER = "engine:mic-probe";
|
|
46
48
|
const A2DP_RATE = 16000;
|
|
47
49
|
const A2DP_CHUNK_MS = 60;
|
|
48
50
|
const TONE_HZ = 440;
|
|
@@ -87,6 +89,8 @@ class GlassesMicProbe {
|
|
|
87
89
|
nonGlasses = 0;
|
|
88
90
|
/** `preferred_mic` before a `source=phone` control run changed it. */
|
|
89
91
|
savedPreferredMic = null;
|
|
92
|
+
/** The probe's microphone lease, which owns the pin and the PCM claim while a run is live. */
|
|
93
|
+
micSession = null;
|
|
90
94
|
listeners = new Set();
|
|
91
95
|
lastSample = null;
|
|
92
96
|
isRunning() {
|
|
@@ -115,23 +119,23 @@ class GlassesMicProbe {
|
|
|
115
119
|
this.lastSample = null;
|
|
116
120
|
const source = options.source ?? GLASSES;
|
|
117
121
|
console.log("[MIC_PROBE] start", options);
|
|
118
|
-
if (source
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
console.warn("[MIC_PROBE] pin failed", error);
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
// Control run: no pin (only the glasses can be pinned); steer the ranking with the
|
|
127
|
-
// user preference and put it back on stop.
|
|
122
|
+
if (source !== GLASSES) {
|
|
123
|
+
// Control run: the manager cannot pin a phone (only the glasses can be pinned), so steer the
|
|
124
|
+
// ranking with the user preference and put it back on stop. That is probe behaviour, not mic
|
|
125
|
+
// policy, which is why it stays here rather than moving into MicSessionManager.
|
|
128
126
|
const settings = useSettingsStore.getState();
|
|
129
127
|
this.savedPreferredMic = settings.getSetting(SETTINGS.preferred_mic.key) ?? "auto";
|
|
130
128
|
await settings.setSetting(SETTINGS.preferred_mic.key, source, false);
|
|
131
129
|
}
|
|
132
130
|
if (this.generation !== generation)
|
|
133
131
|
return;
|
|
134
|
-
|
|
132
|
+
// `diagnostic` carries no tuning profile on purpose: the probe has to measure the gain users
|
|
133
|
+
// actually get, not one it asked for.
|
|
134
|
+
this.micSession = micSessionManager.acquire({
|
|
135
|
+
owner: MIC_PROBE_OWNER,
|
|
136
|
+
source: source === GLASSES ? "glasses" : "phone",
|
|
137
|
+
useCase: "diagnostic",
|
|
138
|
+
});
|
|
135
139
|
this.micSub = BluetoothSdk.addListener("mic_pcm", (event) => {
|
|
136
140
|
this.lastSource = event.source ?? "";
|
|
137
141
|
if (event.source !== source) {
|
|
@@ -162,14 +166,11 @@ class GlassesMicProbe {
|
|
|
162
166
|
this.report();
|
|
163
167
|
this.micSub?.remove();
|
|
164
168
|
this.micSub = null;
|
|
165
|
-
|
|
169
|
+
// Releasing the lease is what unpins the glasses and drops the PCM claim.
|
|
170
|
+
this.micSession?.release();
|
|
171
|
+
this.micSession = null;
|
|
166
172
|
this.stopping = (async () => {
|
|
167
|
-
if ((this.options?.source ?? GLASSES)
|
|
168
|
-
await Promise.resolve(BluetoothSdk.setMicSourcePin?.(null)).catch((error) => {
|
|
169
|
-
console.warn("[MIC_PROBE] unpin failed", error);
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
else if (this.savedPreferredMic !== null) {
|
|
173
|
+
if ((this.options?.source ?? GLASSES) !== GLASSES && this.savedPreferredMic !== null) {
|
|
173
174
|
const restore = this.savedPreferredMic;
|
|
174
175
|
this.savedPreferredMic = null;
|
|
175
176
|
await useSettingsStore.getState().setSetting(SETTINGS.preferred_mic.key, restore, false);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GlassesMicProbe.js","sourceRoot":"","sources":["../../src/services/GlassesMicProbe.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * GlassesMicProbe — on-device level meter for the Bluetooth LC3 microphone.\n *\n * Pins the SDK to the glasses microphone exactly the way a Mentra Call does, then reports the\n * decoded PCM level once a second as `[MIC_PROBE]` log lines, so a soak can tell \"the glasses\n * mic is analog-quiet\" apart from \"the uplink dropped it\". Optionally keeps an A2DP PCM stream\n * open to the glasses at the same time (silence or a tone), which is the duplex condition a\n * Teams call puts the BES in: far-end audio playing while the LC3 mic is live.\n *\n * Dev tooling. Reachable from `com.mentra://test/mic-probe`.\n */\nimport BluetoothSdk from \"@mentra/bluetooth-sdk/internal\"\n\nimport audioPlaybackService from \"./AudioPlaybackService\"\nimport micStateCoordinator from \"./MicStateCoordinator\"\nimport {SETTINGS, useSettingsStore} from \"../stores/settings\"\nimport {summarizePcm16} from \"../utils/pcm16\"\nimport {BgTimer} from \"../utils/timers\"\n\nexport type MicProbeA2dpMode = \"none\" | \"silence\" | \"tone\"\n\n/**\n * Which microphone to meter. `glasses` is the call path (pinned, LC3 over BLE). `phone` is a\n * control: same meter on the phone's own microphone, so a flat glasses trace can be told apart\n * from a quiet room.\n */\nexport type MicProbeSource = \"glasses\" | \"phone\"\n\nexport type MicProbeOptions = {\n /** How long to run before stopping on its own. */\n durationMs: number\n /** What to play to the glasses over A2DP while the mic is live. */\n a2dp: MicProbeA2dpMode\n /** Peak amplitude of the tone as a fraction of full scale (0..1). */\n toneLevel?: number\n /** Microphone under test. Defaults to `glasses`. */\n source?: MicProbeSource\n}\n\nconst A2DP_MODES: MicProbeA2dpMode[] = [\"none\", \"silence\", \"tone\"]\nconst SOURCES: MicProbeSource[] = [\"glasses\", \"phone\"]\n\n/**\n * Parse `com.mentra://test/mic-probe?seconds=&a2dp=&level=` query values.\n *\n * adb/shell often backslash-escapes `&`, so `seconds=15\\&a2dp=none` arrives as\n * `seconds=15\\`. Digits-only parsing is what keeps that from falling back to 20s.\n */\nexport function parseMicProbeParams(params: Record<string, string | undefined | string[]>): MicProbeOptions {\n const raw = (key: string) => {\n const value = params[key]\n return String(Array.isArray(value) ? value[0] : (value ?? \"\"))\n }\n const seconds = Number.parseInt(raw(\"seconds\").replace(/[^\\d]/g, \"\"), 10)\n const durationMs = (Number.isFinite(seconds) && seconds > 0 ? Math.min(600, seconds) : 20) * 1000\n const a2dpRaw = raw(\"a2dp\")\n const a2dp = (A2DP_MODES.includes(a2dpRaw as MicProbeA2dpMode) ? a2dpRaw : \"none\") as MicProbeA2dpMode\n const toneLevel = Number.parseFloat(raw(\"level\").replace(/[^\\d.]/g, \"\"))\n const sourceRaw = raw(\"mic\")\n const source = (SOURCES.includes(sourceRaw as MicProbeSource) ? sourceRaw : \"glasses\") as MicProbeSource\n return {\n durationMs,\n a2dp,\n toneLevel: Number.isFinite(toneLevel) && toneLevel > 0 ? Math.min(1, toneLevel) : 0.2,\n source,\n }\n}\n\nexport type MicProbeSample = {\n /** Seconds since the probe started. */\n t: number\n /** Mean absolute sample value over the last second (16-bit scale). */\n meanAbs: number\n /** Largest absolute sample over the last second. */\n peak: number\n /** PCM frames received in the last second. */\n frames: number\n /** Microphone source reported by the SDK for the last frame. */\n source: string\n /** Frames dropped because the SDK reported a non-glasses source. */\n nonGlasses: number\n a2dp: MicProbeA2dpMode\n}\n\ntype Listener = (sample: MicProbeSample) => void\n\nconst GLASSES = \"glasses\"\nconst A2DP_RATE = 16000\nconst A2DP_CHUNK_MS = 60\nconst TONE_HZ = 440\n\n// Same meter the call uplink logs with, so probe and call numbers are comparable.\nexport {pcmDataView, summarizePcm16} from \"../utils/pcm16\"\n\n/** `chunkMs` of 16 kHz mono PCM16, either digital silence or a sine at `level` full scale. */\nexport function makeA2dpChunk(mode: Exclude<MicProbeA2dpMode, \"none\">, chunkMs: number, phase: number, level = 0.2) {\n const samples = Math.round((A2DP_RATE * chunkMs) / 1000)\n const bytes = new Uint8Array(samples * 2)\n const view = new DataView(bytes.buffer)\n const amp = Math.max(0, Math.min(1, level)) * 32767\n const step = (2 * Math.PI * TONE_HZ) / A2DP_RATE\n for (let i = 0; i < samples; i++) {\n const v = mode === \"tone\" ? Math.round(Math.sin(phase + i * step) * amp) : 0\n view.setInt16(i * 2, v, true)\n }\n return {bytes, phase: (phase + samples * step) % (2 * Math.PI)}\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n let binary = \"\"\n const step = 0x8000\n for (let i = 0; i < bytes.length; i += step) {\n binary += String.fromCharCode(...bytes.subarray(i, i + step))\n }\n return btoa(binary)\n}\n\nclass GlassesMicProbe {\n private running = false\n private stopping: Promise<void> | null = null\n /** Bumped on every start/stop so an await that loses the race cannot re-arm the probe. */\n private generation = 0\n private options: MicProbeOptions | null = null\n private startedAt = 0\n private micSub: {remove: () => void} | null = null\n private tick: number | null = null\n private stopTimer: number | null = null\n private a2dpTimer: number | null = null\n private a2dpStreamId: string | null = null\n private a2dpPhase = 0\n private window: unknown[] = []\n private lastSource = \"\"\n private nonGlasses = 0\n /** `preferred_mic` before a `source=phone` control run changed it. */\n private savedPreferredMic: string | null = null\n private listeners = new Set<Listener>()\n private lastSample: MicProbeSample | null = null\n\n isRunning(): boolean {\n return this.running\n }\n\n last(): MicProbeSample | null {\n return this.lastSample\n }\n\n subscribe(listener: Listener): () => void {\n this.listeners.add(listener)\n return () => this.listeners.delete(listener)\n }\n\n async start(options: MicProbeOptions): Promise<void> {\n // A stop still unpinning the mic must finish before we pin again, or its unpin lands on us.\n if (this.running) await this.stop()\n else if (this.stopping) await this.stopping\n const generation = ++this.generation\n this.running = true\n this.options = options\n this.startedAt = Date.now()\n this.window = []\n this.nonGlasses = 0\n this.lastSource = \"\"\n this.lastSample = null\n const source = options.source ?? GLASSES\n console.log(\"[MIC_PROBE] start\", options)\n\n if (source === GLASSES) {\n // Same order as AcsMeetingService.startGlassesMicUplink: pin first so the first frame the\n // requirement produces is already from the glasses and the phone mic is never opened.\n await Promise.resolve(BluetoothSdk.setMicSourcePin?.(GLASSES)).catch((error) => {\n console.warn(\"[MIC_PROBE] pin failed\", error)\n })\n } else {\n // Control run: no pin (only the glasses can be pinned); steer the ranking with the\n // user preference and put it back on stop.\n const settings = useSettingsStore.getState()\n this.savedPreferredMic = settings.getSetting(SETTINGS.preferred_mic.key) ?? \"auto\"\n await settings.setSetting(SETTINGS.preferred_mic.key, source, false)\n }\n if (this.generation !== generation) return\n micStateCoordinator.setCallRequirement(true)\n this.micSub = BluetoothSdk.addListener(\"mic_pcm\", (event: {pcm?: unknown; source?: string}) => {\n this.lastSource = event.source ?? \"\"\n if (event.source !== source) {\n this.nonGlasses += 1\n return\n }\n if (event.pcm) this.window.push(event.pcm)\n })\n\n if (options.a2dp !== \"none\") await this.startA2dp(options.a2dp, options.toneLevel ?? 0.2)\n if (this.generation !== generation) return\n\n this.tick = BgTimer.setInterval(() => this.report(), 1000)\n this.stopTimer = BgTimer.setTimeout(() => void this.stop(), options.durationMs)\n }\n\n async stop(): Promise<void> {\n this.generation += 1\n if (!this.running) return this.stopping ?? undefined\n this.running = false\n if (this.tick !== null) BgTimer.clearInterval(this.tick)\n if (this.stopTimer !== null) BgTimer.clearTimeout(this.stopTimer)\n this.tick = null\n this.stopTimer = null\n this.report()\n this.micSub?.remove()\n this.micSub = null\n micStateCoordinator.setCallRequirement(false)\n this.stopping = (async () => {\n if ((this.options?.source ?? GLASSES) === GLASSES) {\n await Promise.resolve(BluetoothSdk.setMicSourcePin?.(null)).catch((error) => {\n console.warn(\"[MIC_PROBE] unpin failed\", error)\n })\n } else if (this.savedPreferredMic !== null) {\n const restore = this.savedPreferredMic\n this.savedPreferredMic = null\n await useSettingsStore.getState().setSetting(SETTINGS.preferred_mic.key, restore, false)\n }\n await this.stopA2dp()\n console.log(\"[MIC_PROBE] stop\")\n })().finally(() => {\n this.stopping = null\n })\n return this.stopping\n }\n\n private report(): void {\n const frames = this.window\n this.window = []\n let stats = {meanAbs: 0, peak: 0}\n try {\n stats = summarizePcm16(frames)\n } catch (error) {\n const first = frames[0]\n console.warn(\"[MIC_PROBE] pcm summarize failed\", {\n frames: frames.length,\n type: first == null ? \"empty\" : Object.prototype.toString.call(first),\n byteLength: (first as {byteLength?: number} | undefined)?.byteLength,\n error,\n })\n }\n const sample: MicProbeSample = {\n t: Math.round((Date.now() - this.startedAt) / 1000),\n meanAbs: stats.meanAbs,\n peak: stats.peak,\n frames: frames.length,\n source: this.lastSource,\n nonGlasses: this.nonGlasses,\n a2dp: this.options?.a2dp ?? \"none\",\n }\n this.lastSample = sample\n console.log(\n `[MIC_PROBE] t=${sample.t} meanAbs=${sample.meanAbs} peak=${sample.peak} frames=${frames.length} ` +\n `source=${sample.source || \"-\"} nonGlasses=${sample.nonGlasses} a2dp=${sample.a2dp}`,\n )\n for (const listener of this.listeners) listener(sample)\n }\n\n private async startA2dp(mode: Exclude<MicProbeA2dpMode, \"none\">, level: number): Promise<void> {\n const streamId = `mic-probe-${Date.now()}`\n await audioPlaybackService.openStream({\n streamId,\n appId: \"mic-probe\",\n sampleRate: A2DP_RATE,\n channels: 1,\n stopOtherAudio: true,\n jitterMs: 120,\n onEnded: () => {\n if (this.a2dpStreamId === streamId) this.a2dpStreamId = null\n },\n })\n this.a2dpStreamId = streamId\n this.a2dpPhase = 0\n let inFlight = false\n this.a2dpTimer = BgTimer.setInterval(() => {\n if (inFlight || this.a2dpStreamId !== streamId) return\n inFlight = true\n const chunk = makeA2dpChunk(mode, A2DP_CHUNK_MS, this.a2dpPhase, level)\n this.a2dpPhase = chunk.phase\n audioPlaybackService\n .writeStreamChunk(streamId, bytesToBase64(chunk.bytes))\n .catch((error) => console.warn(\"[MIC_PROBE] a2dp write failed\", error))\n .finally(() => {\n inFlight = false\n })\n }, A2DP_CHUNK_MS)\n }\n\n private async stopA2dp(): Promise<void> {\n if (this.a2dpTimer !== null) BgTimer.clearInterval(this.a2dpTimer)\n this.a2dpTimer = null\n const streamId = this.a2dpStreamId\n this.a2dpStreamId = null\n if (streamId) await audioPlaybackService.abortStream(streamId).catch(() => undefined)\n }\n}\n\nconst glassesMicProbe = new GlassesMicProbe()\nexport default glassesMicProbe\n"]}
|
|
1
|
+
{"version":3,"file":"GlassesMicProbe.js","sourceRoot":"","sources":["../../src/services/GlassesMicProbe.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * GlassesMicProbe — on-device level meter for the Bluetooth LC3 microphone.\n *\n * Pins the SDK to the glasses microphone exactly the way a Mentra Call does, then reports the\n * decoded PCM level once a second as `[MIC_PROBE]` log lines, so a soak can tell \"the glasses\n * mic is analog-quiet\" apart from \"the uplink dropped it\". Optionally keeps an A2DP PCM stream\n * open to the glasses at the same time (silence or a tone), which is the duplex condition a\n * Teams call puts the BES in: far-end audio playing while the LC3 mic is live.\n *\n * Dev tooling. Reachable from `com.mentra://test/mic-probe`.\n */\nimport BluetoothSdk from \"@mentra/bluetooth-sdk/internal\"\n\nimport audioPlaybackService from \"./AudioPlaybackService\"\nimport micSessionManager, {type MicSession} from \"./MicSessionManager\"\nimport {SETTINGS, useSettingsStore} from \"../stores/settings\"\nimport {summarizePcm16} from \"../utils/pcm16\"\nimport {BgTimer} from \"../utils/timers\"\n\nexport type MicProbeA2dpMode = \"none\" | \"silence\" | \"tone\"\n\n/**\n * Which microphone to meter. `glasses` is the call path (pinned, LC3 over BLE). `phone` is a\n * control: same meter on the phone's own microphone, so a flat glasses trace can be told apart\n * from a quiet room.\n */\nexport type MicProbeSource = \"glasses\" | \"phone\"\n\nexport type MicProbeOptions = {\n /** How long to run before stopping on its own. */\n durationMs: number\n /** What to play to the glasses over A2DP while the mic is live. */\n a2dp: MicProbeA2dpMode\n /** Peak amplitude of the tone as a fraction of full scale (0..1). */\n toneLevel?: number\n /** Microphone under test. Defaults to `glasses`. */\n source?: MicProbeSource\n}\n\nconst A2DP_MODES: MicProbeA2dpMode[] = [\"none\", \"silence\", \"tone\"]\nconst SOURCES: MicProbeSource[] = [\"glasses\", \"phone\"]\n\n/**\n * Parse `com.mentra://test/mic-probe?seconds=&a2dp=&level=` query values.\n *\n * adb/shell often backslash-escapes `&`, so `seconds=15\\&a2dp=none` arrives as\n * `seconds=15\\`. Digits-only parsing is what keeps that from falling back to 20s.\n */\nexport function parseMicProbeParams(params: Record<string, string | undefined | string[]>): MicProbeOptions {\n const raw = (key: string) => {\n const value = params[key]\n return String(Array.isArray(value) ? value[0] : (value ?? \"\"))\n }\n const seconds = Number.parseInt(raw(\"seconds\").replace(/[^\\d]/g, \"\"), 10)\n const durationMs = (Number.isFinite(seconds) && seconds > 0 ? Math.min(600, seconds) : 20) * 1000\n const a2dpRaw = raw(\"a2dp\")\n const a2dp = (A2DP_MODES.includes(a2dpRaw as MicProbeA2dpMode) ? a2dpRaw : \"none\") as MicProbeA2dpMode\n const toneLevel = Number.parseFloat(raw(\"level\").replace(/[^\\d.]/g, \"\"))\n const sourceRaw = raw(\"mic\")\n const source = (SOURCES.includes(sourceRaw as MicProbeSource) ? sourceRaw : \"glasses\") as MicProbeSource\n return {\n durationMs,\n a2dp,\n toneLevel: Number.isFinite(toneLevel) && toneLevel > 0 ? Math.min(1, toneLevel) : 0.2,\n source,\n }\n}\n\nexport type MicProbeSample = {\n /** Seconds since the probe started. */\n t: number\n /** Mean absolute sample value over the last second (16-bit scale). */\n meanAbs: number\n /** Largest absolute sample over the last second. */\n peak: number\n /** PCM frames received in the last second. */\n frames: number\n /** Microphone source reported by the SDK for the last frame. */\n source: string\n /** Frames dropped because the SDK reported a non-glasses source. */\n nonGlasses: number\n a2dp: MicProbeA2dpMode\n}\n\ntype Listener = (sample: MicProbeSample) => void\n\nconst GLASSES = \"glasses\"\n/** Lease owner for probe runs. Engine features own their sessions; miniapps cannot claim this. */\nconst MIC_PROBE_OWNER = \"engine:mic-probe\"\nconst A2DP_RATE = 16000\nconst A2DP_CHUNK_MS = 60\nconst TONE_HZ = 440\n\n// Same meter the call uplink logs with, so probe and call numbers are comparable.\nexport {pcmDataView, summarizePcm16} from \"../utils/pcm16\"\n\n/** `chunkMs` of 16 kHz mono PCM16, either digital silence or a sine at `level` full scale. */\nexport function makeA2dpChunk(mode: Exclude<MicProbeA2dpMode, \"none\">, chunkMs: number, phase: number, level = 0.2) {\n const samples = Math.round((A2DP_RATE * chunkMs) / 1000)\n const bytes = new Uint8Array(samples * 2)\n const view = new DataView(bytes.buffer)\n const amp = Math.max(0, Math.min(1, level)) * 32767\n const step = (2 * Math.PI * TONE_HZ) / A2DP_RATE\n for (let i = 0; i < samples; i++) {\n const v = mode === \"tone\" ? Math.round(Math.sin(phase + i * step) * amp) : 0\n view.setInt16(i * 2, v, true)\n }\n return {bytes, phase: (phase + samples * step) % (2 * Math.PI)}\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n let binary = \"\"\n const step = 0x8000\n for (let i = 0; i < bytes.length; i += step) {\n binary += String.fromCharCode(...bytes.subarray(i, i + step))\n }\n return btoa(binary)\n}\n\nclass GlassesMicProbe {\n private running = false\n private stopping: Promise<void> | null = null\n /** Bumped on every start/stop so an await that loses the race cannot re-arm the probe. */\n private generation = 0\n private options: MicProbeOptions | null = null\n private startedAt = 0\n private micSub: {remove: () => void} | null = null\n private tick: number | null = null\n private stopTimer: number | null = null\n private a2dpTimer: number | null = null\n private a2dpStreamId: string | null = null\n private a2dpPhase = 0\n private window: unknown[] = []\n private lastSource = \"\"\n private nonGlasses = 0\n /** `preferred_mic` before a `source=phone` control run changed it. */\n private savedPreferredMic: string | null = null\n /** The probe's microphone lease, which owns the pin and the PCM claim while a run is live. */\n private micSession: MicSession | null = null\n private listeners = new Set<Listener>()\n private lastSample: MicProbeSample | null = null\n\n isRunning(): boolean {\n return this.running\n }\n\n last(): MicProbeSample | null {\n return this.lastSample\n }\n\n subscribe(listener: Listener): () => void {\n this.listeners.add(listener)\n return () => this.listeners.delete(listener)\n }\n\n async start(options: MicProbeOptions): Promise<void> {\n // A stop still unpinning the mic must finish before we pin again, or its unpin lands on us.\n if (this.running) await this.stop()\n else if (this.stopping) await this.stopping\n const generation = ++this.generation\n this.running = true\n this.options = options\n this.startedAt = Date.now()\n this.window = []\n this.nonGlasses = 0\n this.lastSource = \"\"\n this.lastSample = null\n const source = options.source ?? GLASSES\n console.log(\"[MIC_PROBE] start\", options)\n\n if (source !== GLASSES) {\n // Control run: the manager cannot pin a phone (only the glasses can be pinned), so steer the\n // ranking with the user preference and put it back on stop. That is probe behaviour, not mic\n // policy, which is why it stays here rather than moving into MicSessionManager.\n const settings = useSettingsStore.getState()\n this.savedPreferredMic = settings.getSetting(SETTINGS.preferred_mic.key) ?? \"auto\"\n await settings.setSetting(SETTINGS.preferred_mic.key, source, false)\n }\n if (this.generation !== generation) return\n // `diagnostic` carries no tuning profile on purpose: the probe has to measure the gain users\n // actually get, not one it asked for.\n this.micSession = micSessionManager.acquire({\n owner: MIC_PROBE_OWNER,\n source: source === GLASSES ? \"glasses\" : \"phone\",\n useCase: \"diagnostic\",\n })\n this.micSub = BluetoothSdk.addListener(\"mic_pcm\", (event: {pcm?: unknown; source?: string}) => {\n this.lastSource = event.source ?? \"\"\n if (event.source !== source) {\n this.nonGlasses += 1\n return\n }\n if (event.pcm) this.window.push(event.pcm)\n })\n\n if (options.a2dp !== \"none\") await this.startA2dp(options.a2dp, options.toneLevel ?? 0.2)\n if (this.generation !== generation) return\n\n this.tick = BgTimer.setInterval(() => this.report(), 1000)\n this.stopTimer = BgTimer.setTimeout(() => void this.stop(), options.durationMs)\n }\n\n async stop(): Promise<void> {\n this.generation += 1\n if (!this.running) return this.stopping ?? undefined\n this.running = false\n if (this.tick !== null) BgTimer.clearInterval(this.tick)\n if (this.stopTimer !== null) BgTimer.clearTimeout(this.stopTimer)\n this.tick = null\n this.stopTimer = null\n this.report()\n this.micSub?.remove()\n this.micSub = null\n // Releasing the lease is what unpins the glasses and drops the PCM claim.\n this.micSession?.release()\n this.micSession = null\n this.stopping = (async () => {\n if ((this.options?.source ?? GLASSES) !== GLASSES && this.savedPreferredMic !== null) {\n const restore = this.savedPreferredMic\n this.savedPreferredMic = null\n await useSettingsStore.getState().setSetting(SETTINGS.preferred_mic.key, restore, false)\n }\n await this.stopA2dp()\n console.log(\"[MIC_PROBE] stop\")\n })().finally(() => {\n this.stopping = null\n })\n return this.stopping\n }\n\n private report(): void {\n const frames = this.window\n this.window = []\n let stats = {meanAbs: 0, peak: 0}\n try {\n stats = summarizePcm16(frames)\n } catch (error) {\n const first = frames[0]\n console.warn(\"[MIC_PROBE] pcm summarize failed\", {\n frames: frames.length,\n type: first == null ? \"empty\" : Object.prototype.toString.call(first),\n byteLength: (first as {byteLength?: number} | undefined)?.byteLength,\n error,\n })\n }\n const sample: MicProbeSample = {\n t: Math.round((Date.now() - this.startedAt) / 1000),\n meanAbs: stats.meanAbs,\n peak: stats.peak,\n frames: frames.length,\n source: this.lastSource,\n nonGlasses: this.nonGlasses,\n a2dp: this.options?.a2dp ?? \"none\",\n }\n this.lastSample = sample\n console.log(\n `[MIC_PROBE] t=${sample.t} meanAbs=${sample.meanAbs} peak=${sample.peak} frames=${frames.length} ` +\n `source=${sample.source || \"-\"} nonGlasses=${sample.nonGlasses} a2dp=${sample.a2dp}`,\n )\n for (const listener of this.listeners) listener(sample)\n }\n\n private async startA2dp(mode: Exclude<MicProbeA2dpMode, \"none\">, level: number): Promise<void> {\n const streamId = `mic-probe-${Date.now()}`\n await audioPlaybackService.openStream({\n streamId,\n appId: \"mic-probe\",\n sampleRate: A2DP_RATE,\n channels: 1,\n stopOtherAudio: true,\n jitterMs: 120,\n onEnded: () => {\n if (this.a2dpStreamId === streamId) this.a2dpStreamId = null\n },\n })\n this.a2dpStreamId = streamId\n this.a2dpPhase = 0\n let inFlight = false\n this.a2dpTimer = BgTimer.setInterval(() => {\n if (inFlight || this.a2dpStreamId !== streamId) return\n inFlight = true\n const chunk = makeA2dpChunk(mode, A2DP_CHUNK_MS, this.a2dpPhase, level)\n this.a2dpPhase = chunk.phase\n audioPlaybackService\n .writeStreamChunk(streamId, bytesToBase64(chunk.bytes))\n .catch((error) => console.warn(\"[MIC_PROBE] a2dp write failed\", error))\n .finally(() => {\n inFlight = false\n })\n }, A2DP_CHUNK_MS)\n }\n\n private async stopA2dp(): Promise<void> {\n if (this.a2dpTimer !== null) BgTimer.clearInterval(this.a2dpTimer)\n this.a2dpTimer = null\n const streamId = this.a2dpStreamId\n this.a2dpStreamId = null\n if (streamId) await audioPlaybackService.abortStream(streamId).catch(() => undefined)\n }\n}\n\nconst glassesMicProbe = new GlassesMicProbe()\nexport default glassesMicProbe\n"]}
|
|
@@ -102,6 +102,13 @@ declare class LocalMiniappRuntime {
|
|
|
102
102
|
* cloud transcription subscription in buildCloudAudioSubscriptions.
|
|
103
103
|
*/
|
|
104
104
|
private transcriptionHintsByApp;
|
|
105
|
+
/**
|
|
106
|
+
* Microphone sessions taken through session.mic.acquire, by session id.
|
|
107
|
+
*
|
|
108
|
+
* Bookkeeping only, so a release can be matched to its owner; MicSessionManager holds the real
|
|
109
|
+
* leases and resolves them into hardware state.
|
|
110
|
+
*/
|
|
111
|
+
private micSessionsByApp;
|
|
105
112
|
/** Ping interval handle. */
|
|
106
113
|
private pingIntervalId;
|
|
107
114
|
private foregroundProbeTimers;
|
|
@@ -356,6 +363,17 @@ declare class LocalMiniappRuntime {
|
|
|
356
363
|
* manifest. Mentra Live only today — other models no-op via DeviceStore.
|
|
357
364
|
*/
|
|
358
365
|
private handleMicSetLoudnessGateEnabled;
|
|
366
|
+
/**
|
|
367
|
+
* session.mic.acquire — take a semantic microphone session.
|
|
368
|
+
*
|
|
369
|
+
* The miniapp names a use case; MicSessionManager and micPolicy decide what that means for the
|
|
370
|
+
* hardware. This handler is only the gate: who is allowed to ask for what.
|
|
371
|
+
*/
|
|
372
|
+
private handleMicAcquire;
|
|
373
|
+
/** session.mic.acquire(...).release() — drop one session, if the caller owns it. */
|
|
374
|
+
private handleMicRelease;
|
|
375
|
+
/** Drop the bookkeeping for every session an app owns. The manager is the source of truth. */
|
|
376
|
+
private forgetMicSessions;
|
|
359
377
|
/**
|
|
360
378
|
* session.system.scanQr — host camera overlay. Must not clear miniapp
|
|
361
379
|
* foreground; the host seam is responsible for presenting a Modal on top.
|