@mentra/engine 3.2.1-dev.278 → 3.2.1-dev.282
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/AcsMeetingService.d.ts +56 -6
- package/build/services/AcsMeetingService.d.ts.map +1 -1
- package/build/services/AcsMeetingService.js +162 -40
- 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 +201 -0
- package/build/services/MicSessionManager.js.map +1 -0
- package/build/services/MicStateCoordinator.d.ts +71 -5
- package/build/services/MicStateCoordinator.d.ts.map +1 -1
- package/build/services/MicStateCoordinator.js +161 -10
- package/build/services/MicStateCoordinator.js.map +1 -1
- package/build/services/micPolicy.d.ts +131 -0
- package/build/services/micPolicy.d.ts.map +1 -0
- package/build/services/micPolicy.js +140 -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 +167 -40
- 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 +258 -0
- package/src/services/MicStateCoordinator.ts +153 -10
- package/src/services/micPolicy.ts +198 -0
- package/src/stores/bluetoothSettingKeys.ts +3 -0
- package/src/stores/settings.ts +66 -0
- package/src/utils/pcm16.ts +43 -1
package/build/utils/pcm16.js
CHANGED
|
@@ -28,11 +28,28 @@ export function pcmDataView(frame) {
|
|
|
28
28
|
}
|
|
29
29
|
return null;
|
|
30
30
|
}
|
|
31
|
+
/** Signed 16-bit full scale. `-32768` abs-counts as 32768 and is also a rail. */
|
|
32
|
+
export const PCM16_FULL_SCALE = 32767;
|
|
33
|
+
/** Headroom line: loud speech that is not yet hard-clipped. */
|
|
34
|
+
export const PCM16_NEAR_CLIP = 30000;
|
|
35
|
+
/** Percentages for a closed window. Zero samples → all percents 0. */
|
|
36
|
+
export function pcm16WindowStats(level) {
|
|
37
|
+
const denom = level.samples || 1;
|
|
38
|
+
const pct = (count) => Math.round((count / denom) * 1000) / 10;
|
|
39
|
+
return {
|
|
40
|
+
...level,
|
|
41
|
+
peakPct: Math.round((level.peak / PCM16_FULL_SCALE) * 1000) / 10,
|
|
42
|
+
clipPct: level.samples ? pct(level.clipped) : 0,
|
|
43
|
+
nearClipPct: level.samples ? pct(level.nearClip) : 0,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
31
46
|
/** Level statistics over a batch of PCM16 frames. Unreadable frames are skipped, never thrown on. */
|
|
32
47
|
export function summarizePcm16(frames) {
|
|
33
48
|
let sum = 0;
|
|
34
49
|
let peak = 0;
|
|
35
50
|
let samples = 0;
|
|
51
|
+
let clipped = 0;
|
|
52
|
+
let nearClip = 0;
|
|
36
53
|
for (const frame of frames) {
|
|
37
54
|
const view = pcmDataView(frame);
|
|
38
55
|
if (!view)
|
|
@@ -43,10 +60,14 @@ export function summarizePcm16(frames) {
|
|
|
43
60
|
sum += v;
|
|
44
61
|
if (v > peak)
|
|
45
62
|
peak = v;
|
|
63
|
+
if (v >= PCM16_FULL_SCALE)
|
|
64
|
+
clipped++;
|
|
65
|
+
if (v >= PCM16_NEAR_CLIP)
|
|
66
|
+
nearClip++;
|
|
46
67
|
}
|
|
47
68
|
samples += n;
|
|
48
69
|
}
|
|
49
|
-
return { meanAbs: samples ? Math.round(sum / samples) : 0, peak, samples };
|
|
70
|
+
return { meanAbs: samples ? Math.round(sum / samples) : 0, peak, samples, clipped, nearClip };
|
|
50
71
|
}
|
|
51
72
|
/**
|
|
52
73
|
* Incremental accumulator for the same statistics, for hot paths that see one frame at a time
|
|
@@ -56,6 +77,8 @@ export class Pcm16LevelMeter {
|
|
|
56
77
|
sum = 0;
|
|
57
78
|
peakValue = 0;
|
|
58
79
|
count = 0;
|
|
80
|
+
clipped = 0;
|
|
81
|
+
nearClip = 0;
|
|
59
82
|
add(frame) {
|
|
60
83
|
const view = pcmDataView(frame);
|
|
61
84
|
if (!view)
|
|
@@ -66,6 +89,10 @@ export class Pcm16LevelMeter {
|
|
|
66
89
|
this.sum += v;
|
|
67
90
|
if (v > this.peakValue)
|
|
68
91
|
this.peakValue = v;
|
|
92
|
+
if (v >= PCM16_FULL_SCALE)
|
|
93
|
+
this.clipped++;
|
|
94
|
+
if (v >= PCM16_NEAR_CLIP)
|
|
95
|
+
this.nearClip++;
|
|
69
96
|
}
|
|
70
97
|
this.count += n;
|
|
71
98
|
}
|
|
@@ -75,10 +102,14 @@ export class Pcm16LevelMeter {
|
|
|
75
102
|
meanAbs: this.count ? Math.round(this.sum / this.count) : 0,
|
|
76
103
|
peak: this.peakValue,
|
|
77
104
|
samples: this.count,
|
|
105
|
+
clipped: this.clipped,
|
|
106
|
+
nearClip: this.nearClip,
|
|
78
107
|
};
|
|
79
108
|
this.sum = 0;
|
|
80
109
|
this.peakValue = 0;
|
|
81
110
|
this.count = 0;
|
|
111
|
+
this.clipped = 0;
|
|
112
|
+
this.nearClip = 0;
|
|
82
113
|
return level;
|
|
83
114
|
}
|
|
84
115
|
}
|
package/build/utils/pcm16.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pcm16.js","sourceRoot":"","sources":["../../src/utils/pcm16.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;QACjC,OAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC5B,CAAC;IACD,IAAI,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,OAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,CAAA;QACvE,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,KAAqC,CAAC,CAAA;YACnE,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAClC,CAAC;IACH,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACnC,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAClC,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;
|
|
1
|
+
{"version":3,"file":"pcm16.js","sourceRoot":"","sources":["../../src/utils/pcm16.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Level statistics for 16-bit little-endian mono PCM, as delivered by `mic_pcm`.\n *\n * Shared by the call uplink and the dev mic probe so both report the same number: a soak that\n * says `meanAbs=40` in one log and `meanAbs=40` in the other is measuring the same thing.\n */\n\n/**\n * Hermes / the Expo bridge delivers `mic_pcm.pcm` as a Uint8Array, not an ArrayBuffer.\n * `new DataView(uint8Array)` throws `buffer must be an ArrayBuffer` — that is the redbox\n * attributed to whatever screen happens to be up (wifi scan, home, …).\n */\nexport function pcmDataView(frame: unknown): DataView | null {\n if (frame instanceof ArrayBuffer) {\n return new DataView(frame)\n }\n if (ArrayBuffer.isView(frame)) {\n try {\n return new DataView(frame.buffer, frame.byteOffset, frame.byteLength)\n } catch {\n const copy = Uint8Array.from(frame as unknown as ArrayLike<number>)\n return new DataView(copy.buffer)\n }\n }\n if (Array.isArray(frame)) {\n const copy = Uint8Array.from(frame)\n return new DataView(copy.buffer)\n }\n return null\n}\n\n/** Signed 16-bit full scale. `-32768` abs-counts as 32768 and is also a rail. */\nexport const PCM16_FULL_SCALE = 32767\n/** Headroom line: loud speech that is not yet hard-clipped. */\nexport const PCM16_NEAR_CLIP = 30000\n\nexport type Pcm16Level = {\n /** Mean absolute sample value (16-bit scale). ~30–60 is a quiet room on Mentra Live LC3. */\n meanAbs: number\n /** Largest absolute sample. */\n peak: number\n /** Samples counted. */\n samples: number\n /** Samples at the int16 rail (`>= 32767`). */\n clipped: number\n /** Samples at or above [PCM16_NEAR_CLIP]. */\n nearClip: number\n}\n\nexport type Pcm16WindowStats = Pcm16Level & {\n /** `peak / 32767`, percent. */\n peakPct: number\n /** `clipped / samples`, percent. */\n clipPct: number\n /** `nearClip / samples`, percent. */\n nearClipPct: number\n}\n\n/** Percentages for a closed window. Zero samples → all percents 0. */\nexport function pcm16WindowStats(level: Pcm16Level): Pcm16WindowStats {\n const denom = level.samples || 1\n const pct = (count: number) => Math.round((count / denom) * 1000) / 10\n return {\n ...level,\n peakPct: Math.round((level.peak / PCM16_FULL_SCALE) * 1000) / 10,\n clipPct: level.samples ? pct(level.clipped) : 0,\n nearClipPct: level.samples ? pct(level.nearClip) : 0,\n }\n}\n\n/** Level statistics over a batch of PCM16 frames. Unreadable frames are skipped, never thrown on. */\nexport function summarizePcm16(frames: unknown[]): Pcm16Level {\n let sum = 0\n let peak = 0\n let samples = 0\n let clipped = 0\n let nearClip = 0\n for (const frame of frames) {\n const view = pcmDataView(frame)\n if (!view) continue\n const n = Math.floor(view.byteLength / 2)\n for (let i = 0; i < n; i++) {\n const v = Math.abs(view.getInt16(i * 2, true))\n sum += v\n if (v > peak) peak = v\n if (v >= PCM16_FULL_SCALE) clipped++\n if (v >= PCM16_NEAR_CLIP) nearClip++\n }\n samples += n\n }\n return {meanAbs: samples ? Math.round(sum / samples) : 0, peak, samples, clipped, nearClip}\n}\n\n/**\n * Incremental accumulator for the same statistics, for hot paths that see one frame at a time\n * and report once a window (the call uplink logs every 5 s at 20 frames/s).\n */\nexport class Pcm16LevelMeter {\n private sum = 0\n private peakValue = 0\n private count = 0\n private clipped = 0\n private nearClip = 0\n\n add(frame: unknown): void {\n const view = pcmDataView(frame)\n if (!view) return\n const n = Math.floor(view.byteLength / 2)\n for (let i = 0; i < n; i++) {\n const v = Math.abs(view.getInt16(i * 2, true))\n this.sum += v\n if (v > this.peakValue) this.peakValue = v\n if (v >= PCM16_FULL_SCALE) this.clipped++\n if (v >= PCM16_NEAR_CLIP) this.nearClip++\n }\n this.count += n\n }\n\n /** Read the window and start a new one. */\n take(): Pcm16Level {\n const level = {\n meanAbs: this.count ? Math.round(this.sum / this.count) : 0,\n peak: this.peakValue,\n samples: this.count,\n clipped: this.clipped,\n nearClip: this.nearClip,\n }\n this.sum = 0\n this.peakValue = 0\n this.count = 0\n this.clipped = 0\n this.nearClip = 0\n return level\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/engine",
|
|
3
|
-
"version": "3.2.1-dev.
|
|
3
|
+
"version": "3.2.1-dev.282",
|
|
4
4
|
"description": "Mentra Engine — miniapp registry, WebView bridge, and miniapp store",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -97,13 +97,13 @@
|
|
|
97
97
|
"registry": "https://registry.npmjs.org/"
|
|
98
98
|
},
|
|
99
99
|
"dependencies": {
|
|
100
|
-
"@mentra/acs-meeting": "3.2.1-dev.
|
|
101
|
-
"@mentra/glasses-media": "3.2.1-dev.
|
|
102
|
-
"@mentra/bluetooth-sdk": "3.2.1-dev.
|
|
103
|
-
"@mentra/cloud-client": "3.2.1-dev.
|
|
104
|
-
"@mentra/cloud-protocol": "3.2.1-dev.
|
|
105
|
-
"@mentra/crust": "3.2.1-dev.
|
|
106
|
-
"@mentra/miniapp": "3.2.1-dev.
|
|
100
|
+
"@mentra/acs-meeting": "3.2.1-dev.282",
|
|
101
|
+
"@mentra/glasses-media": "3.2.1-dev.282",
|
|
102
|
+
"@mentra/bluetooth-sdk": "3.2.1-dev.282",
|
|
103
|
+
"@mentra/cloud-client": "3.2.1-dev.282",
|
|
104
|
+
"@mentra/cloud-protocol": "3.2.1-dev.282",
|
|
105
|
+
"@mentra/crust": "3.2.1-dev.282",
|
|
106
|
+
"@mentra/miniapp": "3.2.1-dev.282",
|
|
107
107
|
"buffer": "^6.0.3",
|
|
108
108
|
"events": "^3.3.0",
|
|
109
109
|
"react-native-marked": "8.1.1",
|
|
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
|
|
|
12
12
|
export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
|
|
13
13
|
"schemaVersion": 1,
|
|
14
14
|
"familyBaseVersion": "3.2.1",
|
|
15
|
-
"releaseIdentity": "3.2.1-dev.
|
|
16
|
-
"releaseSetId": "mentra-3.2.1-dev.
|
|
17
|
-
"sourceCommit": "
|
|
18
|
-
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-dev.
|
|
19
|
-
"otaManifestSha256": "
|
|
15
|
+
"releaseIdentity": "3.2.1-dev.282",
|
|
16
|
+
"releaseSetId": "mentra-3.2.1-dev.282",
|
|
17
|
+
"sourceCommit": "53defbe045377507bdd6c188d303be138040c450",
|
|
18
|
+
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-dev.282.json",
|
|
19
|
+
"otaManifestSha256": "c69e779261aac872c9b0a26abbdb2d7e6549db577ee4bf0755adf770d8f879f6"
|
|
20
20
|
})
|
|
@@ -9,9 +9,12 @@ import {Platform} from "react-native"
|
|
|
9
9
|
import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
|
|
10
10
|
|
|
11
11
|
import audioPlaybackService from "./AudioPlaybackService"
|
|
12
|
+
import {getCallGainSweep} from "./CallGainSweep"
|
|
12
13
|
import micStateCoordinator from "./MicStateCoordinator"
|
|
14
|
+
import micSessionManager, {type MicSession} from "./MicSessionManager"
|
|
15
|
+
import {ENGINE_OWNER_PREFIX} from "./micPolicy"
|
|
13
16
|
import {SETTINGS, useSettingsStore} from "../stores/settings"
|
|
14
|
-
import {Pcm16LevelMeter} from "../utils/pcm16"
|
|
17
|
+
import {Pcm16LevelMeter, pcm16WindowStats} from "../utils/pcm16"
|
|
15
18
|
import {softapTrace, softapTraceFailure, softapTraceId} from "../utils/softapTrace"
|
|
16
19
|
import {ACS_CALL_MIC, type AcsAudioSource, type ResolvedAudioSource, type SourceReason} from "./acsAudioSource"
|
|
17
20
|
import type {SoftapProgress} from "./SoftapCallTransport"
|
|
@@ -204,8 +207,13 @@ export function glassesLc3UplinkSupported(args: {
|
|
|
204
207
|
audioSource: AcsAudioSource
|
|
205
208
|
hasPushOutgoingPcm: boolean
|
|
206
209
|
platform: string
|
|
210
|
+
/** Somebody holds a glasses microphone session through MicSessionManager. */
|
|
211
|
+
glassesSession: boolean
|
|
207
212
|
}): boolean {
|
|
208
213
|
if (!(softapBleLc3UplinkForTests ?? SOFTAP_BLE_LC3_UPLINK)) return false
|
|
214
|
+
// The microphone belongs to whoever leased it. Without a lease nothing has pinned the glasses
|
|
215
|
+
// or claimed raw PCM, so this call has no wearer audio to read off them.
|
|
216
|
+
if (!args.glassesSession) return false
|
|
209
217
|
// iOS has no `setMicSourcePin` yet, so it cannot promise the phone microphone stays shut.
|
|
210
218
|
if (args.platform !== "android") return false
|
|
211
219
|
// WHEP audio comes back from Cloudflare already mixed into the subscribed track; there is no
|
|
@@ -537,6 +545,8 @@ const GLASSES_MIC_SOURCE = "glasses"
|
|
|
537
545
|
const GLASSES_MIC_GRACE_MS = 1000
|
|
538
546
|
/** Cadence of the uplink health line, matching the native P8 ladder. */
|
|
539
547
|
const MIC_UPLINK_LOG_INTERVAL_MS = 5000
|
|
548
|
+
/** During a gain sweep, one-second windows so each 25 s phase has enough speech samples. */
|
|
549
|
+
const MIC_UPLINK_SWEEP_LOG_INTERVAL_MS = 1000
|
|
540
550
|
/** A 50 ms LC3 frame arriving more than this late is a missed beat, not jitter. */
|
|
541
551
|
const MIC_GAP_WARN_MS = 90
|
|
542
552
|
|
|
@@ -612,6 +622,17 @@ class AcsMeetingService {
|
|
|
612
622
|
private callOrigin: AcsCallOrigin = "unknown"
|
|
613
623
|
private micTransport: MicTransport = "whip"
|
|
614
624
|
private micSub: {remove: () => void} | null = null
|
|
625
|
+
/** Backstop lease, held only while this call is actually reading the glasses mic. */
|
|
626
|
+
private micSession: MicSession | null = null
|
|
627
|
+
private micTuningSub: {remove: () => void} | null = null
|
|
628
|
+
private micRmsSub: {remove: () => void} | null = null
|
|
629
|
+
/** Only turn telemetry off again if this call is what turned it on. */
|
|
630
|
+
private micRmsEnabled = false
|
|
631
|
+
private gateSamples = 0
|
|
632
|
+
private gateClosed = 0
|
|
633
|
+
private gateElevated = 0
|
|
634
|
+
private gateClosedElevated = 0
|
|
635
|
+
private gateRmsMax = 0
|
|
615
636
|
/** True between the pin/requirement being taken and released, so release is exactly once. */
|
|
616
637
|
private micUplinkActive = false
|
|
617
638
|
private micFramesForwarded = 0
|
|
@@ -972,6 +993,12 @@ class AcsMeetingService {
|
|
|
972
993
|
displayName?: string
|
|
973
994
|
video?: AcsOutgoingVideo
|
|
974
995
|
origin?: AcsCallOrigin
|
|
996
|
+
/**
|
|
997
|
+
* Whether the caller holds a glasses microphone session. The uplink is the wearer's voice,
|
|
998
|
+
* so it is only taken off the glasses when somebody has actually claimed that microphone
|
|
999
|
+
* through MicSessionManager; this class never claims it itself.
|
|
1000
|
+
*/
|
|
1001
|
+
glassesSession?: boolean
|
|
975
1002
|
},
|
|
976
1003
|
): Promise<MeetingState> {
|
|
977
1004
|
const native = getNative()
|
|
@@ -998,6 +1025,7 @@ class AcsMeetingService {
|
|
|
998
1025
|
audioSource: resolved.source,
|
|
999
1026
|
hasPushOutgoingPcm: typeof native.pushOutgoingPcm === "function",
|
|
1000
1027
|
platform: Platform.OS,
|
|
1028
|
+
glassesSession: args.glassesSession ?? false,
|
|
1001
1029
|
})
|
|
1002
1030
|
this.micTransport = lc3Uplink ? "ble-lc3" : resolved.source === "phone" ? "phone" : "whip"
|
|
1003
1031
|
this.bindNative(native, packageName)
|
|
@@ -1287,11 +1315,15 @@ class AcsMeetingService {
|
|
|
1287
1315
|
/**
|
|
1288
1316
|
* Start forwarding the glasses microphone into ACS for this call.
|
|
1289
1317
|
*
|
|
1290
|
-
*
|
|
1291
|
-
*
|
|
1292
|
-
* the
|
|
1293
|
-
*
|
|
1294
|
-
*
|
|
1318
|
+
* This class is a sink: it never names a gain, a pin or a setting. It does take a semantic
|
|
1319
|
+
* voice_call lease first, because reading the glasses mic without one is what put this call on
|
|
1320
|
+
* the OS default of VAD-on — a speech gate that drops the wearer's uplink whenever the GX8002
|
|
1321
|
+
* disagrees, which during a call is most of the time the far end is talking. The lease is a
|
|
1322
|
+
* backstop: when the miniapp already holds one this simply merges with it, and micPolicy still
|
|
1323
|
+
* decides what voice_call means for the hardware.
|
|
1324
|
+
*
|
|
1325
|
+
* `generation` is captured by the listener so a frame that lands after this call ended is
|
|
1326
|
+
* dropped rather than pushed at a native session that has left the meeting.
|
|
1295
1327
|
*/
|
|
1296
1328
|
private startGlassesMicUplink(generation: number): void {
|
|
1297
1329
|
if (this.micTransport !== "ble-lc3") return
|
|
@@ -1310,11 +1342,9 @@ class AcsMeetingService {
|
|
|
1310
1342
|
this.micLevel.take()
|
|
1311
1343
|
this.lastMicLevel = null
|
|
1312
1344
|
try {
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
})
|
|
1345
|
+
this.acquireMicSession()
|
|
1346
|
+
this.startMicGateTelemetry()
|
|
1316
1347
|
this.micUplinkActive = true
|
|
1317
|
-
micStateCoordinator.setCallRequirement(true)
|
|
1318
1348
|
this.micSub = BluetoothSdk.addListener("mic_pcm", (event: {pcm?: ArrayBuffer; sampleRate?: number; source?: string}) => {
|
|
1319
1349
|
if (generation !== this.callGeneration) {
|
|
1320
1350
|
this.micDropsStale += 1
|
|
@@ -1362,29 +1392,132 @@ class AcsMeetingService {
|
|
|
1362
1392
|
}
|
|
1363
1393
|
}
|
|
1364
1394
|
|
|
1365
|
-
/**
|
|
1395
|
+
/**
|
|
1396
|
+
* Stop reading the glasses microphone. Safe to call when this call never started.
|
|
1397
|
+
*
|
|
1398
|
+
* Only this call's own backstop lease is dropped. A lease the miniapp took is its to release,
|
|
1399
|
+
* and letting go of ours before that owner releases is what keeps a normal hang-up from looking
|
|
1400
|
+
* like the wearer's microphone disappearing.
|
|
1401
|
+
*/
|
|
1366
1402
|
private stopGlassesMicUplink(): void {
|
|
1367
1403
|
this.micSub?.remove()
|
|
1368
1404
|
this.micSub = null
|
|
1405
|
+
this.releaseMicSession()
|
|
1369
1406
|
if (this.micUplinkActive) {
|
|
1370
1407
|
this.micUplinkActive = false
|
|
1371
|
-
micStateCoordinator.setCallRequirement(false)
|
|
1372
|
-
// Last, and unconditionally: while the pin is set no other consumer can pick a microphone,
|
|
1373
|
-
// so leaving it behind would leave captions and the cloud uplink stuck on the glasses.
|
|
1374
|
-
void Promise.resolve(BluetoothSdk.setMicSourcePin?.(null)).catch((error) => {
|
|
1375
|
-
console.warn("[AcsMeeting] releasing the glasses microphone pin failed", error)
|
|
1376
|
-
})
|
|
1377
1408
|
console.log("[AcsMeeting] phase=glasses-mic-uplink-stop", {
|
|
1378
1409
|
frames: this.micFramesForwarded,
|
|
1379
1410
|
dropsStale: this.micDropsStale,
|
|
1380
1411
|
dropsNonGlasses: this.micDropsNonGlasses,
|
|
1381
1412
|
gaps: this.micGaps,
|
|
1382
1413
|
gapMsMax: this.micGapMsMax,
|
|
1414
|
+
...this.gateSummary(),
|
|
1383
1415
|
})
|
|
1384
1416
|
}
|
|
1417
|
+
this.stopMicGateTelemetry()
|
|
1385
1418
|
this.micTransport = "whip"
|
|
1386
1419
|
}
|
|
1387
1420
|
|
|
1421
|
+
/**
|
|
1422
|
+
* Watch what the glasses are actually running for the length of the call.
|
|
1423
|
+
*
|
|
1424
|
+
* Two different questions, both unanswerable from this side otherwise. `mic_tuning_state` is
|
|
1425
|
+
* the post-clamp reply, so it catches a profile the firmware rewrote rather than accepted.
|
|
1426
|
+
* `mic_rms` carries the Barrier's own verdict per frame, which is the only way to tell a gate
|
|
1427
|
+
* that is suppressing speaker leak from one that is suppressing the wearer.
|
|
1428
|
+
*/
|
|
1429
|
+
private startMicGateTelemetry(): void {
|
|
1430
|
+
this.gateSamples = 0
|
|
1431
|
+
this.gateClosed = 0
|
|
1432
|
+
this.gateElevated = 0
|
|
1433
|
+
this.gateClosedElevated = 0
|
|
1434
|
+
this.gateRmsMax = 0
|
|
1435
|
+
|
|
1436
|
+
try {
|
|
1437
|
+
this.micTuningSub = BluetoothSdk.addListener("mic_tuning_state", (event: Record<string, unknown>) => {
|
|
1438
|
+
console.log("[AcsMeeting] phase=glasses-mic-tuning-applied", event)
|
|
1439
|
+
})
|
|
1440
|
+
this.micRmsSub = BluetoothSdk.addListener(
|
|
1441
|
+
"mic_rms",
|
|
1442
|
+
(event: {rms?: number; gateOpen?: boolean; speakerElevated?: boolean}) => {
|
|
1443
|
+
this.gateSamples += 1
|
|
1444
|
+
if (event.gateOpen === false) this.gateClosed += 1
|
|
1445
|
+
if (event.speakerElevated) {
|
|
1446
|
+
this.gateElevated += 1
|
|
1447
|
+
if (event.gateOpen === false) this.gateClosedElevated += 1
|
|
1448
|
+
}
|
|
1449
|
+
if (typeof event.rms === "number" && event.rms > this.gateRmsMax) this.gateRmsMax = event.rms
|
|
1450
|
+
},
|
|
1451
|
+
)
|
|
1452
|
+
void Promise.resolve(BluetoothSdk.setMicRmsTelemetry?.(true))
|
|
1453
|
+
.then(() => {
|
|
1454
|
+
this.micRmsEnabled = true
|
|
1455
|
+
})
|
|
1456
|
+
.catch((error) => console.warn("[AcsMeeting] mic RMS telemetry unavailable", error))
|
|
1457
|
+
void Promise.resolve(BluetoothSdk.requestMicTuningState?.()).catch(() => {})
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
console.warn("[AcsMeeting] mic gate telemetry unavailable", error)
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
private stopMicGateTelemetry(): void {
|
|
1464
|
+
this.micTuningSub?.remove()
|
|
1465
|
+
this.micTuningSub = null
|
|
1466
|
+
this.micRmsSub?.remove()
|
|
1467
|
+
this.micRmsSub = null
|
|
1468
|
+
if (!this.micRmsEnabled) return
|
|
1469
|
+
this.micRmsEnabled = false
|
|
1470
|
+
// Super Mode may have the readout open behind this call; it re-requests on focus.
|
|
1471
|
+
void Promise.resolve(BluetoothSdk.setMicRmsTelemetry?.(false)).catch(() => {})
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/** Barrier's behaviour over the window, as percentages a listening test can be checked against. */
|
|
1475
|
+
private gateSummary(): Record<string, number> {
|
|
1476
|
+
if (this.gateSamples === 0) return {}
|
|
1477
|
+
const pct = (n: number, of: number) => (of === 0 ? 0 : Math.round((n / of) * 1000) / 10)
|
|
1478
|
+
return {
|
|
1479
|
+
gateSamples: this.gateSamples,
|
|
1480
|
+
gateClosedPct: pct(this.gateClosed, this.gateSamples),
|
|
1481
|
+
speakerElevatedPct: pct(this.gateElevated, this.gateSamples),
|
|
1482
|
+
// The number that matters: closed while the far end was quiet means the wearer was cut.
|
|
1483
|
+
gateClosedQuietPct: pct(this.gateClosed - this.gateClosedElevated, this.gateSamples - this.gateElevated),
|
|
1484
|
+
gateRmsMax: this.gateRmsMax,
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* Guarantee a voice_call session for as long as this call reads the glasses mic.
|
|
1490
|
+
*
|
|
1491
|
+
* The miniapp asking for one is the intended path; this covers the builds where it cannot,
|
|
1492
|
+
* because a bundled Call that predates MIC_ACQUIRE joins anyway rather than failing. Without
|
|
1493
|
+
* this the uplink runs under whatever the OS settings say, and the shipped default is VAD-on.
|
|
1494
|
+
*/
|
|
1495
|
+
private acquireMicSession(): void {
|
|
1496
|
+
if (this.micSession) return
|
|
1497
|
+
try {
|
|
1498
|
+
this.micSession = micSessionManager.acquire({
|
|
1499
|
+
owner: `${ENGINE_OWNER_PREFIX}acs-uplink`,
|
|
1500
|
+
source: "glasses",
|
|
1501
|
+
useCase: "voice_call",
|
|
1502
|
+
})
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
// A source conflict means something else already owns the microphone. Forwarding whatever
|
|
1505
|
+
// it captures is still better than dropping the wearer from the call.
|
|
1506
|
+
console.warn("[AcsMeeting] glasses mic session unavailable", error)
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
private releaseMicSession(): void {
|
|
1511
|
+
const session = this.micSession
|
|
1512
|
+
if (!session) return
|
|
1513
|
+
this.micSession = null
|
|
1514
|
+
try {
|
|
1515
|
+
session.release()
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
console.warn("[AcsMeeting] releasing the glasses mic session failed", error)
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1388
1521
|
/**
|
|
1389
1522
|
* Report a call whose pinned microphone stopped delivering. There is deliberately no fallback:
|
|
1390
1523
|
* the wearer agreed to be heard from the glasses, and quietly switching to the phone in the
|
|
@@ -1413,42 +1546,36 @@ class AcsMeetingService {
|
|
|
1413
1546
|
private logMicUplink(): void {
|
|
1414
1547
|
const now = Date.now()
|
|
1415
1548
|
const elapsed = now - this.lastMicUplinkLogAt
|
|
1416
|
-
|
|
1549
|
+
const sweep = getCallGainSweep()
|
|
1550
|
+
const interval = sweep.isActive() ? MIC_UPLINK_SWEEP_LOG_INTERVAL_MS : MIC_UPLINK_LOG_INTERVAL_MS
|
|
1551
|
+
if (elapsed < interval) return
|
|
1417
1552
|
this.lastMicUplinkLogAt = now
|
|
1418
1553
|
// Level, not just cadence: a 20 Hz stream of the noise floor and a 20 Hz stream of speech
|
|
1419
1554
|
// have the same framesPerSecond. Quiet room on Mentra Live LC3 is meanAbs ≈30–60.
|
|
1420
1555
|
const level = this.micLevel.take()
|
|
1556
|
+
const stats = pcm16WindowStats(level)
|
|
1421
1557
|
this.lastMicLevel = {meanAbs: level.meanAbs, peak: level.peak}
|
|
1558
|
+
const gain = sweep.currentGain() ?? micStateCoordinator.getSessionMicTuning()?.gain ?? null
|
|
1559
|
+
if (sweep.isActive()) sweep.ingest(level)
|
|
1422
1560
|
console.log("[AcsMeeting] phase=glasses-mic-uplink", {
|
|
1423
1561
|
framesPerSecond: Math.round((this.micFramesWindow * 1000) / elapsed),
|
|
1424
1562
|
frames: this.micFramesForwarded,
|
|
1425
|
-
meanAbs:
|
|
1426
|
-
peak:
|
|
1563
|
+
meanAbs: stats.meanAbs,
|
|
1564
|
+
peak: stats.peak,
|
|
1565
|
+
peakPct: stats.peakPct,
|
|
1566
|
+
clipped: stats.clipped,
|
|
1567
|
+
clipPct: stats.clipPct,
|
|
1568
|
+
nearClip: stats.nearClip,
|
|
1569
|
+
nearClipPct: stats.nearClipPct,
|
|
1570
|
+
gain,
|
|
1571
|
+
barrier: micStateCoordinator.getSessionLoudnessGate(),
|
|
1572
|
+
sweep: sweep.currentLabel(),
|
|
1427
1573
|
dropsStale: this.micDropsStale,
|
|
1428
1574
|
dropsNonGlasses: this.micDropsNonGlasses,
|
|
1429
1575
|
gaps: this.micGaps,
|
|
1430
1576
|
gapMsMax: this.micGapMsMax,
|
|
1577
|
+
...this.gateSummary(),
|
|
1431
1578
|
})
|
|
1432
|
-
// #region agent log
|
|
1433
|
-
fetch("http://127.0.0.1:7905/ingest/5a9713c9-45ff-4d09-9435-2adc5db5e91d", {
|
|
1434
|
-
method: "POST",
|
|
1435
|
-
headers: {"Content-Type": "application/json", "X-Debug-Session-Id": "828181"},
|
|
1436
|
-
body: JSON.stringify({
|
|
1437
|
-
sessionId: "828181",
|
|
1438
|
-
runId: "run1",
|
|
1439
|
-
hypothesisId: "E",
|
|
1440
|
-
location: "AcsMeetingService.ts:logMicUplink",
|
|
1441
|
-
message: "phone decoded glasses PCM window",
|
|
1442
|
-
data: {
|
|
1443
|
-
fps: Math.round((this.micFramesWindow * 1000) / elapsed),
|
|
1444
|
-
meanAbs: level.meanAbs,
|
|
1445
|
-
peak: level.peak,
|
|
1446
|
-
frames: this.micFramesForwarded,
|
|
1447
|
-
},
|
|
1448
|
-
timestamp: Date.now(),
|
|
1449
|
-
}),
|
|
1450
|
-
}).catch(() => {})
|
|
1451
|
-
// #endregion
|
|
1452
1579
|
this.micFramesWindow = 0
|
|
1453
1580
|
}
|
|
1454
1581
|
|