@mentra/engine 3.2.1-dev.279 → 3.2.1-dev.283
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 +41 -7
- package/build/services/AcsMeetingService.d.ts.map +1 -1
- package/build/services/AcsMeetingService.js +130 -27
- package/build/services/AcsMeetingService.js.map +1 -1
- package/build/services/MicSessionManager.d.ts.map +1 -1
- package/build/services/MicSessionManager.js +3 -0
- package/build/services/MicSessionManager.js.map +1 -1
- package/build/services/MicStateCoordinator.d.ts +23 -0
- package/build/services/MicStateCoordinator.d.ts.map +1 -1
- package/build/services/MicStateCoordinator.js +71 -1
- package/build/services/MicStateCoordinator.js.map +1 -1
- package/build/services/micPolicy.d.ts +51 -4
- package/build/services/micPolicy.d.ts.map +1 -1
- package/build/services/micPolicy.js +70 -12
- package/build/services/micPolicy.js.map +1 -1
- package/package.json +8 -8
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/services/AcsMeetingService.ts +129 -27
- package/src/services/MicSessionManager.ts +3 -0
- package/src/services/MicStateCoordinator.ts +63 -1
- package/src/services/micPolicy.ts +81 -12
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"micPolicy.js","sourceRoot":"","sources":["../../src/services/micPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAwCH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAyC;IACzE,UAAU,EAAE,EAAC,IAAI,EAAE,EAAE,EAAC;IACtB,aAAa,EAAE,EAAE;IACjB,eAAe,EAAE,EAAE;IACnB,UAAU,EAAE,EAAE;CACf,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAsB,CAAC,iBAAiB,CAAC,CAAA;AAEzE,yDAAyD;AACzD,MAAM,CAAC,MAAM,aAAa,GAA0B;IAClD,YAAY;IACZ,eAAe;IACf,iBAAiB;IACjB,YAAY;CACb,CAAA;AAED,iFAAiF;AACjF,MAAM,CAAC,MAAM,mBAAmB,GAAG,SAAS,CAAA;AAE5C,kDAAkD;AAClD,MAAM,CAAC,MAAM,qBAAqB,GAA0B,CAAC,YAAY,CAAC,CAAA;AAE1E;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAAmC,EACnC,IAAqB;IAErB,MAAM,SAAS,GAAG,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAA;IAEnG,IAAI,UAAU,GAAG,KAAK,CAAA;IACtB,IAAI,IAAwB,CAAA;IAC5B,KAAK,MAAM,OAAO,IAAI,SAAS,EAAE,CAAC;QAChC,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS;YAAE,SAAQ;QAC1C,UAAU,GAAG,IAAI,CAAA;QACjB,MAAM,WAAW,GAAG,qBAAqB,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,IAAI,CAAA;QAChE,sEAAsE;QACtE,0BAA0B;QAC1B,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,WAAW,GAAG,IAAI,CAAC,EAAE,CAAC;YAClF,IAAI,GAAG,WAAW,CAAA;QACpB,CAAC;IACH,CAAC;IAED,OAAO;QACL,MAAM,EAAE,SAAS,CAAC,MAAM,GAAG,CAAC;QAC5B,UAAU;QACV,SAAS,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAC,IAAI,EAAC;KAC9C,CAAA;AACH,CAAC","sourcesContent":["/**\n * Microphone use-case policy.\n *\n * Applications say what they are doing (\"voice_call\"); the engine decides what\n * that requires of the hardware. Nothing above this file knows that a voice\n * call means ADC index 14, and no miniapp can ask for a gain directly.\n *\n * Pure: every function here is a function of the live session set. The lease\n * bookkeeping lives in MicSessionManager, the merge with OS preferences lives\n * in MicStateCoordinator, and the wire write lives below that again.\n */\n\n/** What an application is doing with the microphone. */\nexport type MicUseCase = \"voice_call\" | \"transcription\" | \"voice_assistant\" | \"diagnostic\"\n\n/** Which microphone the audio comes from. */\nexport type MicSource = \"glasses\" | \"phone\"\n\n/** Subset of the `mic_tuning` wire payload. Fields left out keep firmware defaults. */\nexport type MicTuningProfile = {\n /** codec_adc_vol index, 0-15. */\n gain?: number\n}\n\n/** A live microphone lease. */\nexport type MicSessionSpec = {\n useCase: MicUseCase\n source: MicSource\n}\n\n/** What the hardware should do, given every live session. */\nexport type ResolvedMicPolicy = {\n /** Someone needs a continuous raw-PCM timeline, which also forces hardware VAD off. */\n rawPcm: boolean\n /** Pin the Bluetooth SDK to the glasses microphone. Only the glasses can be pinned. */\n pinGlasses: boolean\n /** Tuning override for the glasses, or null to leave the OS value in force. */\n micTuning: MicTuningProfile | null\n}\n\n/** Platform facts the policy cannot infer from the sessions alone. */\nexport type MicPlatformCaps = {\n /**\n * Whether this platform can take the wearer's voice off the glasses as raw\n * PCM over BLE LC3. False on iOS, which has no `setMicSourcePin` and never\n * selects the `ble-lc3` uplink.\n */\n glassesPcmUplink: boolean\n}\n\n/**\n * Mentra Live ships CODEC_SADC_VOL = 15, the last entry of codec_adc_vol[] and\n * +32 dB. That table steps 2 dB at a time everywhere except the final step,\n * which jumps 6 dB from +26, so the default sits at the ceiling one oversized\n * step above everything else. It suits a wearer dictating to a transcription\n * miniapp across a room; it clips a wearer talking into a Teams call.\n *\n * Gain is the only knob here on purpose. A call forces hardware VAD off and\n * the Barrier is off by default, which puts the firmware gate in its `pass`\n * branch, so the RMS thresholds cannot affect a call.\n *\n * Index 14 is +26 dB: one step down, and the step that removes the table's\n * anomalous jump. A same-voice sweep showed 15 rails hard (2.5–4.2% clip);\n * 14/13/12 still kiss the rail on syllable tips. 13 is +24 dB, the middle\n * of that band, while we listen for loudness vs residual clip.\n */\nexport const MIC_USE_CASE_PROFILES: Record<MicUseCase, MicTuningProfile> = {\n voice_call: {gain: 13},\n transcription: {},\n voice_assistant: {},\n diagnostic: {},\n}\n\n/**\n * Packages allowed to hold a `voice_call` session.\n *\n * Which app may make a voice call is policy, so it lives beside the profiles\n * rather than in the request handler that enforces it.\n */\nexport const VOICE_CALL_PACKAGES: readonly string[] = [\"com.mentra.call\"]\n\n/** Every use case, for validating an inbound request. */\nexport const MIC_USE_CASES: readonly MicUseCase[] = [\n \"voice_call\",\n \"transcription\",\n \"voice_assistant\",\n \"diagnostic\",\n]\n\n/** Owners of engine-internal sessions. Miniapps cannot claim these use cases. */\nexport const ENGINE_OWNER_PREFIX = \"engine:\"\n\n/** Use cases only engine features may acquire. */\nexport const ENGINE_ONLY_USE_CASES: readonly MicUseCase[] = [\"diagnostic\"]\n\n/**\n * Resolve every live session into one hardware state.\n *\n * On a platform without a glasses PCM uplink a glasses session is a lease and\n * nothing more: it satisfies ownership checks so an iOS call can still be\n * modelled the same way, but it claims no PCM, pins nothing, and applies no\n * gain, because the wearer's voice does not reach the call through the BES\n * BLE path there.\n */\nexport function resolveMicPolicy(\n sessions: readonly MicSessionSpec[],\n caps: MicPlatformCaps,\n): ResolvedMicPolicy {\n const effective = caps.glassesPcmUplink ? sessions : sessions.filter((s) => s.source !== \"glasses\")\n\n let pinGlasses = false\n let gain: number | undefined\n for (const session of effective) {\n if (session.source !== \"glasses\") continue\n pinGlasses = true\n const profileGain = MIC_USE_CASE_PROFILES[session.useCase]?.gain\n // Lowest index wins: clipping is the irreversible failure, a slightly\n // quiet assistant is not.\n if (typeof profileGain === \"number\" && (gain === undefined || profileGain < gain)) {\n gain = profileGain\n }\n }\n\n return {\n rawPcm: effective.length > 0,\n pinGlasses,\n micTuning: gain === undefined ? null : {gain},\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"micPolicy.js","sourceRoot":"","sources":["../../src/services/micPolicy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Microphone use-case policy.\n *\n * Applications say what they are doing (\"voice_call\"); the engine decides what\n * that requires of the hardware. Nothing above this file knows that a voice\n * call means ADC index 14, and no miniapp can ask for a gain directly.\n *\n * Pure: every function here is a function of the live session set. The lease\n * bookkeeping lives in MicSessionManager, the merge with OS preferences lives\n * in MicStateCoordinator, and the wire write lives below that again.\n */\n\n/** What an application is doing with the microphone. */\nexport type MicUseCase = \"voice_call\" | \"transcription\" | \"voice_assistant\" | \"diagnostic\"\n\n/** Which microphone the audio comes from. */\nexport type MicSource = \"glasses\" | \"phone\"\n\n/** Subset of the `mic_tuning` wire payload. Fields left out keep firmware defaults. */\nexport type MicTuningProfile = {\n /** codec_adc_vol index, 0-15. */\n gain?: number\n /** Center-mic RMS to open the gate. */\n open?: number\n /** Center-mic RMS to close it again. */\n close?: number\n /** Open threshold while the speaker is elevated. */\n sp_open?: number\n /** Close threshold while the speaker is elevated. */\n sp_close?: number\n}\n\n/** `codec_adc_vol[]`: index to dB. Index 0 is mute and is never offered. */\nexport const GAIN_DB = [-99, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 32]\n\n/**\n * Mirrored from `center_mic_vad_get_default_config` and `CODEC_SADC_VOL`.\n *\n * The four RMS numbers are measured *after* the ADC gain, so they are only\n * meaningful next to the gain they were calibrated at, which is index 15.\n */\nexport const MIC_TUNING_FIRMWARE_DEFAULTS = {\n gain: 15,\n open: 1350,\n close: 945,\n sp_open: 2900,\n sp_close: 1600,\n} as const\n\n/**\n * Re-express the firmware's thresholds at a different gain.\n *\n * The gate compares post-gain RMS, so moving the gain without moving the\n * thresholds silently changes what the gate means: at -8 dB the wearer has to\n * be 2.5x louder to clear a number that was chosen for +32 dB. Scaling both by\n * the same factor keeps the *acoustic* trip point the profile was tuned for,\n * for the wearer and for speaker leak alike.\n */\nexport function scaleMicTuningToGain(gain: number): Required<MicTuningProfile> {\n const index = Math.min(GAIN_DB.length - 1, Math.max(1, Math.round(gain)))\n const factor = Math.pow(10, (GAIN_DB[index]! - GAIN_DB[MIC_TUNING_FIRMWARE_DEFAULTS.gain]!) / 20)\n return {\n gain: index,\n open: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.open * factor),\n close: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.close * factor),\n sp_open: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.sp_open * factor),\n sp_close: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.sp_close * factor),\n }\n}\n\n/** A live microphone lease. */\nexport type MicSessionSpec = {\n useCase: MicUseCase\n source: MicSource\n}\n\n/** What the hardware should do, given every live session. */\nexport type ResolvedMicPolicy = {\n /** Someone needs a continuous raw-PCM timeline, which also forces hardware VAD off. */\n rawPcm: boolean\n /** Pin the Bluetooth SDK to the glasses microphone. Only the glasses can be pinned. */\n pinGlasses: boolean\n /** Tuning override for the glasses, or null to leave the OS value in force. */\n micTuning: MicTuningProfile | null\n /** Run the center-mic loudness gate, or null to leave the OS value in force. */\n loudnessGate: boolean | null\n}\n\n/** Platform facts the policy cannot infer from the sessions alone. */\nexport type MicPlatformCaps = {\n /**\n * Whether this platform can take the wearer's voice off the glasses as raw\n * PCM over BLE LC3. False on iOS, which has no `setMicSourcePin` and never\n * selects the `ble-lc3` uplink.\n */\n glassesPcmUplink: boolean\n}\n\n/**\n * Mentra Live ships CODEC_SADC_VOL = 15, the last entry of codec_adc_vol[] and\n * +32 dB. That table steps 2 dB at a time everywhere except the final step,\n * which jumps 6 dB from +26, so the default sits at the ceiling one oversized\n * step above everything else. It suits a wearer dictating to a transcription\n * miniapp across a room; it clips a wearer talking into a Teams call.\n *\n * Index 14 is +26 dB: one step down, and the step that removes the table's\n * anomalous jump. A same-voice sweep showed 15 rails hard (2.5–4.2% clip);\n * 14/13/12 still kiss the rail on syllable tips. 13 is +24 dB, the middle\n * of that band, while we listen for loudness vs residual clip.\n *\n * The thresholds ride along with the gain rather than being stated here, so\n * changing the index above cannot leave the gate calibrated for a level the\n * ADC no longer produces.\n */\nexport const MIC_USE_CASE_PROFILES: Record<MicUseCase, MicTuningProfile> = {\n voice_call: scaleMicTuningToGain(13),\n transcription: {},\n voice_assistant: {},\n diagnostic: {},\n}\n\n/**\n * Use cases that run the center-mic loudness gate (\"Barrier\").\n *\n * A voice call is the one place it earns its keep. There is no echo canceller\n * on the LC3 uplink — `MENTRA_LC3_SPEECH_PROCESSING` is 0 and the capture path\n * ignores the playback reference — so without a gate the far end hears itself\n * through the wearer's speaker. Barrier is the safe half of the firmware's two\n * gates: it zeroes a quiet frame but never stops transmitting, where VAD drops\n * the stream outright. Paired with the speaker-elevated thresholds above, that\n * suppresses leak while the far end is talking and costs the wearer nothing.\n */\nexport const MIC_USE_CASE_LOUDNESS_GATE: Record<MicUseCase, boolean> = {\n voice_call: true,\n transcription: false,\n voice_assistant: false,\n diagnostic: false,\n}\n\n/**\n * Packages allowed to hold a `voice_call` session.\n *\n * Which app may make a voice call is policy, so it lives beside the profiles\n * rather than in the request handler that enforces it.\n */\nexport const VOICE_CALL_PACKAGES: readonly string[] = [\"com.mentra.call\"]\n\n/** Every use case, for validating an inbound request. */\nexport const MIC_USE_CASES: readonly MicUseCase[] = [\n \"voice_call\",\n \"transcription\",\n \"voice_assistant\",\n \"diagnostic\",\n]\n\n/** Owners of engine-internal sessions. Miniapps cannot claim these use cases. */\nexport const ENGINE_OWNER_PREFIX = \"engine:\"\n\n/** Use cases only engine features may acquire. */\nexport const ENGINE_ONLY_USE_CASES: readonly MicUseCase[] = [\"diagnostic\"]\n\n/**\n * Resolve every live session into one hardware state.\n *\n * On a platform without a glasses PCM uplink a glasses session is a lease and\n * nothing more: it satisfies ownership checks so an iOS call can still be\n * modelled the same way, but it claims no PCM, pins nothing, and applies no\n * gain, because the wearer's voice does not reach the call through the BES\n * BLE path there.\n */\nexport function resolveMicPolicy(\n sessions: readonly MicSessionSpec[],\n caps: MicPlatformCaps,\n): ResolvedMicPolicy {\n const effective = caps.glassesPcmUplink ? sessions : sessions.filter((s) => s.source !== \"glasses\")\n\n let pinGlasses = false\n let loudnessGate = false\n let winning: MicTuningProfile | null = null\n for (const session of effective) {\n if (session.source !== \"glasses\") continue\n pinGlasses = true\n if (MIC_USE_CASE_LOUDNESS_GATE[session.useCase]) loudnessGate = true\n const profile = MIC_USE_CASE_PROFILES[session.useCase]\n if (typeof profile?.gain !== \"number\") continue\n // Lowest index wins: clipping is the irreversible failure, a slightly\n // quiet assistant is not. The whole profile travels with it, because its\n // thresholds are only meaningful at its own gain.\n if (winning === null || profile.gain < winning.gain!) winning = profile\n }\n\n return {\n rawPcm: effective.length > 0,\n pinGlasses,\n micTuning: winning,\n loudnessGate: pinGlasses ? loudnessGate : null,\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.283",
|
|
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.283",
|
|
101
|
+
"@mentra/glasses-media": "3.2.1-dev.283",
|
|
102
|
+
"@mentra/bluetooth-sdk": "3.2.1-dev.283",
|
|
103
|
+
"@mentra/cloud-client": "3.2.1-dev.283",
|
|
104
|
+
"@mentra/cloud-protocol": "3.2.1-dev.283",
|
|
105
|
+
"@mentra/crust": "3.2.1-dev.283",
|
|
106
|
+
"@mentra/miniapp": "3.2.1-dev.283",
|
|
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.283",
|
|
16
|
+
"releaseSetId": "mentra-3.2.1-dev.283",
|
|
17
|
+
"sourceCommit": "da43fa96393855e1a19dedc923bc496c315d2da7",
|
|
18
|
+
"otaManifestUrl": "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-dev.283.json",
|
|
19
|
+
"otaManifestSha256": "8a0b32bd78c4f7b58b67d9eddd854b70f6a417d698a52d38b1f5ead81910e068"
|
|
20
20
|
})
|
|
@@ -11,6 +11,8 @@ import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
|
|
|
11
11
|
import audioPlaybackService from "./AudioPlaybackService"
|
|
12
12
|
import {getCallGainSweep} from "./CallGainSweep"
|
|
13
13
|
import micStateCoordinator from "./MicStateCoordinator"
|
|
14
|
+
import micSessionManager, {type MicSession} from "./MicSessionManager"
|
|
15
|
+
import {ENGINE_OWNER_PREFIX} from "./micPolicy"
|
|
14
16
|
import {SETTINGS, useSettingsStore} from "../stores/settings"
|
|
15
17
|
import {Pcm16LevelMeter, pcm16WindowStats} from "../utils/pcm16"
|
|
16
18
|
import {softapTrace, softapTraceFailure, softapTraceId} from "../utils/softapTrace"
|
|
@@ -620,6 +622,17 @@ class AcsMeetingService {
|
|
|
620
622
|
private callOrigin: AcsCallOrigin = "unknown"
|
|
621
623
|
private micTransport: MicTransport = "whip"
|
|
622
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
|
|
623
636
|
/** True between the pin/requirement being taken and released, so release is exactly once. */
|
|
624
637
|
private micUplinkActive = false
|
|
625
638
|
private micFramesForwarded = 0
|
|
@@ -1302,10 +1315,12 @@ class AcsMeetingService {
|
|
|
1302
1315
|
/**
|
|
1303
1316
|
* Start forwarding the glasses microphone into ACS for this call.
|
|
1304
1317
|
*
|
|
1305
|
-
* This class is a sink
|
|
1306
|
-
*
|
|
1307
|
-
*
|
|
1308
|
-
*
|
|
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.
|
|
1309
1324
|
*
|
|
1310
1325
|
* `generation` is captured by the listener so a frame that lands after this call ended is
|
|
1311
1326
|
* dropped rather than pushed at a native session that has left the meeting.
|
|
@@ -1327,6 +1342,8 @@ class AcsMeetingService {
|
|
|
1327
1342
|
this.micLevel.take()
|
|
1328
1343
|
this.lastMicLevel = null
|
|
1329
1344
|
try {
|
|
1345
|
+
this.acquireMicSession()
|
|
1346
|
+
this.startMicGateTelemetry()
|
|
1330
1347
|
this.micUplinkActive = true
|
|
1331
1348
|
this.micSub = BluetoothSdk.addListener("mic_pcm", (event: {pcm?: ArrayBuffer; sampleRate?: number; source?: string}) => {
|
|
1332
1349
|
if (generation !== this.callGeneration) {
|
|
@@ -1378,13 +1395,14 @@ class AcsMeetingService {
|
|
|
1378
1395
|
/**
|
|
1379
1396
|
* Stop reading the glasses microphone. Safe to call when this call never started.
|
|
1380
1397
|
*
|
|
1381
|
-
* Only
|
|
1382
|
-
*
|
|
1383
|
-
*
|
|
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.
|
|
1384
1401
|
*/
|
|
1385
1402
|
private stopGlassesMicUplink(): void {
|
|
1386
1403
|
this.micSub?.remove()
|
|
1387
1404
|
this.micSub = null
|
|
1405
|
+
this.releaseMicSession()
|
|
1388
1406
|
if (this.micUplinkActive) {
|
|
1389
1407
|
this.micUplinkActive = false
|
|
1390
1408
|
console.log("[AcsMeeting] phase=glasses-mic-uplink-stop", {
|
|
@@ -1393,11 +1411,113 @@ class AcsMeetingService {
|
|
|
1393
1411
|
dropsNonGlasses: this.micDropsNonGlasses,
|
|
1394
1412
|
gaps: this.micGaps,
|
|
1395
1413
|
gapMsMax: this.micGapMsMax,
|
|
1414
|
+
...this.gateSummary(),
|
|
1396
1415
|
})
|
|
1397
1416
|
}
|
|
1417
|
+
this.stopMicGateTelemetry()
|
|
1398
1418
|
this.micTransport = "whip"
|
|
1399
1419
|
}
|
|
1400
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
|
+
|
|
1401
1521
|
/**
|
|
1402
1522
|
* Report a call whose pinned microphone stopped delivering. There is deliberately no fallback:
|
|
1403
1523
|
* the wearer agreed to be heard from the glasses, and quietly switching to the phone in the
|
|
@@ -1448,32 +1568,14 @@ class AcsMeetingService {
|
|
|
1448
1568
|
nearClip: stats.nearClip,
|
|
1449
1569
|
nearClipPct: stats.nearClipPct,
|
|
1450
1570
|
gain,
|
|
1571
|
+
barrier: micStateCoordinator.getSessionLoudnessGate(),
|
|
1451
1572
|
sweep: sweep.currentLabel(),
|
|
1452
1573
|
dropsStale: this.micDropsStale,
|
|
1453
1574
|
dropsNonGlasses: this.micDropsNonGlasses,
|
|
1454
1575
|
gaps: this.micGaps,
|
|
1455
1576
|
gapMsMax: this.micGapMsMax,
|
|
1577
|
+
...this.gateSummary(),
|
|
1456
1578
|
})
|
|
1457
|
-
// #region agent log
|
|
1458
|
-
fetch("http://127.0.0.1:7905/ingest/5a9713c9-45ff-4d09-9435-2adc5db5e91d", {
|
|
1459
|
-
method: "POST",
|
|
1460
|
-
headers: {"Content-Type": "application/json", "X-Debug-Session-Id": "828181"},
|
|
1461
|
-
body: JSON.stringify({
|
|
1462
|
-
sessionId: "828181",
|
|
1463
|
-
runId: "run1",
|
|
1464
|
-
hypothesisId: "E",
|
|
1465
|
-
location: "AcsMeetingService.ts:logMicUplink",
|
|
1466
|
-
message: "phone decoded glasses PCM window",
|
|
1467
|
-
data: {
|
|
1468
|
-
fps: Math.round((this.micFramesWindow * 1000) / elapsed),
|
|
1469
|
-
meanAbs: level.meanAbs,
|
|
1470
|
-
peak: level.peak,
|
|
1471
|
-
frames: this.micFramesForwarded,
|
|
1472
|
-
},
|
|
1473
|
-
timestamp: Date.now(),
|
|
1474
|
-
}),
|
|
1475
|
-
}).catch(() => {})
|
|
1476
|
-
// #endregion
|
|
1477
1579
|
this.micFramesWindow = 0
|
|
1478
1580
|
}
|
|
1479
1581
|
|
|
@@ -235,6 +235,9 @@ class MicSessionManager {
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
micStateCoordinator.setSessionRequirement(policy.rawPcm)
|
|
238
|
+
// Barrier is the only echo control left once the PCM claim turns hardware VAD off, so it
|
|
239
|
+
// rides with the claim rather than with the tuning a sweep may be rewriting.
|
|
240
|
+
micStateCoordinator.setSessionLoudnessGate(policy.loudnessGate)
|
|
238
241
|
|
|
239
242
|
if (getCallGainSweep().isActive()) {
|
|
240
243
|
const gain = getCallGainSweep().currentGain()
|
|
@@ -20,6 +20,15 @@ import type {MicTuningProfile} from "./micPolicy"
|
|
|
20
20
|
|
|
21
21
|
const LOG_TAG = "MIC_COORDINATOR"
|
|
22
22
|
|
|
23
|
+
/** Field-wise compare, so a profile that moves a threshold without the gain still lands. */
|
|
24
|
+
function sameProfile(a: MicTuningProfile | null, b: MicTuningProfile | null): boolean {
|
|
25
|
+
if (a === b) return true
|
|
26
|
+
if (!a || !b) return false
|
|
27
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]) as Set<keyof MicTuningProfile>
|
|
28
|
+
for (const key of keys) if (a[key] !== b[key]) return false
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
type MicGate = "vad" | "loudnessGate"
|
|
24
33
|
|
|
25
34
|
interface GateOverride {
|
|
@@ -41,6 +50,10 @@ const flushMicRequirementsPatch = createDebouncedPatchFlusher<Record<string, unk
|
|
|
41
50
|
// gate override during the debounce window, and a captured stale value
|
|
42
51
|
// must not win after that lifecycle transition.
|
|
43
52
|
const runtimePatch = micStateCoordinator.applyEffectiveGatePolicy(patch)
|
|
53
|
+
// The merged patch, not the intent that produced it. Everything above this line is a
|
|
54
|
+
// preference; this is the only record of what the glasses were actually told, and the three
|
|
55
|
+
// keys a call depends on (VAD off, Barrier on, the tuning) are only decided here at flush.
|
|
56
|
+
console.log(`${LOG_TAG}: write`, runtimePatch)
|
|
44
57
|
void Promise.resolve(BluetoothSdk.updateBluetoothSettings(runtimePatch)).catch((err) => {
|
|
45
58
|
console.error(`${LOG_TAG}: failed to apply mic requirements:`, err)
|
|
46
59
|
})
|
|
@@ -78,6 +91,10 @@ class MicStateCoordinator {
|
|
|
78
91
|
* call from resurrecting the profile.
|
|
79
92
|
*/
|
|
80
93
|
private sessionMicTuningWritten = false
|
|
94
|
+
/** Barrier required by the live sessions, or null when they have no opinion. */
|
|
95
|
+
private sessionLoudnessGate: boolean | null = null
|
|
96
|
+
/** Latched like the tuning, so the OS value is restated once a session has moved it. */
|
|
97
|
+
private sessionLoudnessGateWritten = false
|
|
81
98
|
private readonly miniappVadOverrides = new Map<string, GateOverride>()
|
|
82
99
|
private readonly miniappLoudnessGateOverrides = new Map<string, GateOverride>()
|
|
83
100
|
private overrideSequence = 0
|
|
@@ -126,7 +143,7 @@ class MicStateCoordinator {
|
|
|
126
143
|
* over the value that was queued.
|
|
127
144
|
*/
|
|
128
145
|
public setSessionMicTuning(profile: MicTuningProfile | null): void {
|
|
129
|
-
if (profile
|
|
146
|
+
if (sameProfile(profile, this.sessionMicTuning)) return
|
|
130
147
|
this.sessionMicTuning = profile
|
|
131
148
|
if (profile) this.sessionMicTuningWritten = true
|
|
132
149
|
// Nothing was ever written, so there is nothing to restore.
|
|
@@ -140,6 +157,28 @@ class MicStateCoordinator {
|
|
|
140
157
|
return this.sessionMicTuning
|
|
141
158
|
}
|
|
142
159
|
|
|
160
|
+
/** Last session Barrier queued, or null when the OS value is in force. For call logging. */
|
|
161
|
+
public getSessionLoudnessGate(): boolean | null {
|
|
162
|
+
return this.sessionLoudnessGate
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Run or stop the center-mic loudness gate on behalf of the live sessions.
|
|
167
|
+
*
|
|
168
|
+
* MicSessionManager only, and the counterpart to the PCM claim rather than a second opinion on
|
|
169
|
+
* it: raw PCM turns hardware VAD off, which leaves Barrier as the only thing standing between
|
|
170
|
+
* the far end and its own echo. Unlike VAD this never stops the stream, so the worst a wrong
|
|
171
|
+
* threshold costs is a zeroed frame instead of a dropped word.
|
|
172
|
+
*/
|
|
173
|
+
public setSessionLoudnessGate(enabled: boolean | null): void {
|
|
174
|
+
if (this.sessionLoudnessGate === enabled) return
|
|
175
|
+
this.sessionLoudnessGate = enabled
|
|
176
|
+
if (enabled !== null) this.sessionLoudnessGateWritten = true
|
|
177
|
+
else if (!this.sessionLoudnessGateWritten) return
|
|
178
|
+
console.log(`${LOG_TAG}: session loudness gate ${enabled === null ? "cleared" : enabled}`)
|
|
179
|
+
this.applyUnion()
|
|
180
|
+
}
|
|
181
|
+
|
|
143
182
|
/**
|
|
144
183
|
* Super Mode sliders outrank a session profile. A live override means a gain
|
|
145
184
|
* sweep would write to the coordinator and the glasses would ignore it.
|
|
@@ -169,6 +208,19 @@ class MicStateCoordinator {
|
|
|
169
208
|
return {...this.sessionMicTuning} as Record<string, number>
|
|
170
209
|
}
|
|
171
210
|
|
|
211
|
+
/**
|
|
212
|
+
* The session's Barrier, but only when it wins.
|
|
213
|
+
*
|
|
214
|
+
* Suppressed by a live Super Mode tuning value for the same reason the gain is: that screen is
|
|
215
|
+
* the manual override, and a gate running against hand-entered thresholds is the one case where
|
|
216
|
+
* the session's numbers are the wrong ones.
|
|
217
|
+
*/
|
|
218
|
+
private winningSessionLoudnessGate(): boolean | undefined {
|
|
219
|
+
if (this.sessionLoudnessGate === null) return undefined
|
|
220
|
+
if (Object.keys(this.configuredMicTuning).length > 0) return undefined
|
|
221
|
+
return this.sessionLoudnessGate
|
|
222
|
+
}
|
|
223
|
+
|
|
172
224
|
/**
|
|
173
225
|
* Apply a miniapp-owned gate override without changing the OS preference.
|
|
174
226
|
* Overrides are lifecycle-scoped and last-live-owner-wins independently for
|
|
@@ -262,8 +314,11 @@ class MicStateCoordinator {
|
|
|
262
314
|
} else if (vadOverride) {
|
|
263
315
|
runtimeSettings.voice_activity_detection_enabled = vadOverride.enabled
|
|
264
316
|
}
|
|
317
|
+
const sessionGate = this.winningSessionLoudnessGate()
|
|
265
318
|
if (loudnessOverride) {
|
|
266
319
|
runtimeSettings.loudness_gate_enabled = loudnessOverride.enabled
|
|
320
|
+
} else if (sessionGate !== undefined) {
|
|
321
|
+
runtimeSettings.loudness_gate_enabled = sessionGate
|
|
267
322
|
}
|
|
268
323
|
|
|
269
324
|
// BES forgets mic_tuning on disconnect, so the on-connect replay is what
|
|
@@ -336,9 +391,15 @@ class MicStateCoordinator {
|
|
|
336
391
|
else if (vadOverride) patch.voice_activity_detection_enabled = vadOverride.enabled
|
|
337
392
|
else if (this.configuredVad !== undefined) patch.voice_activity_detection_enabled = this.configuredVad
|
|
338
393
|
|
|
394
|
+
const sessionGate = this.winningSessionLoudnessGate()
|
|
339
395
|
if (loudnessOverride) patch.loudness_gate_enabled = loudnessOverride.enabled
|
|
396
|
+
else if (sessionGate !== undefined) patch.loudness_gate_enabled = sessionGate
|
|
340
397
|
else if (this.configuredLoudnessGate !== undefined) {
|
|
341
398
|
patch.loudness_gate_enabled = this.configuredLoudnessGate
|
|
399
|
+
} else if (this.sessionLoudnessGateWritten) {
|
|
400
|
+
// A session moved it and the device carries no preference, so restate the product default
|
|
401
|
+
// rather than leaving the call's gate running after it ended.
|
|
402
|
+
patch.loudness_gate_enabled = false
|
|
342
403
|
}
|
|
343
404
|
|
|
344
405
|
const sessionTuning = this.winningSessionMicTuning()
|
|
@@ -356,6 +417,7 @@ class MicStateCoordinator {
|
|
|
356
417
|
this.localWantsLc3 = false
|
|
357
418
|
this.sessionWantsPcm = false
|
|
358
419
|
this.sessionMicTuning = null
|
|
420
|
+
this.sessionLoudnessGate = null
|
|
359
421
|
this.applyUnion()
|
|
360
422
|
}
|
|
361
423
|
|
|
@@ -20,6 +20,52 @@ export type MicSource = "glasses" | "phone"
|
|
|
20
20
|
export type MicTuningProfile = {
|
|
21
21
|
/** codec_adc_vol index, 0-15. */
|
|
22
22
|
gain?: number
|
|
23
|
+
/** Center-mic RMS to open the gate. */
|
|
24
|
+
open?: number
|
|
25
|
+
/** Center-mic RMS to close it again. */
|
|
26
|
+
close?: number
|
|
27
|
+
/** Open threshold while the speaker is elevated. */
|
|
28
|
+
sp_open?: number
|
|
29
|
+
/** Close threshold while the speaker is elevated. */
|
|
30
|
+
sp_close?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** `codec_adc_vol[]`: index to dB. Index 0 is mute and is never offered. */
|
|
34
|
+
export const GAIN_DB = [-99, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 32]
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Mirrored from `center_mic_vad_get_default_config` and `CODEC_SADC_VOL`.
|
|
38
|
+
*
|
|
39
|
+
* The four RMS numbers are measured *after* the ADC gain, so they are only
|
|
40
|
+
* meaningful next to the gain they were calibrated at, which is index 15.
|
|
41
|
+
*/
|
|
42
|
+
export const MIC_TUNING_FIRMWARE_DEFAULTS = {
|
|
43
|
+
gain: 15,
|
|
44
|
+
open: 1350,
|
|
45
|
+
close: 945,
|
|
46
|
+
sp_open: 2900,
|
|
47
|
+
sp_close: 1600,
|
|
48
|
+
} as const
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Re-express the firmware's thresholds at a different gain.
|
|
52
|
+
*
|
|
53
|
+
* The gate compares post-gain RMS, so moving the gain without moving the
|
|
54
|
+
* thresholds silently changes what the gate means: at -8 dB the wearer has to
|
|
55
|
+
* be 2.5x louder to clear a number that was chosen for +32 dB. Scaling both by
|
|
56
|
+
* the same factor keeps the *acoustic* trip point the profile was tuned for,
|
|
57
|
+
* for the wearer and for speaker leak alike.
|
|
58
|
+
*/
|
|
59
|
+
export function scaleMicTuningToGain(gain: number): Required<MicTuningProfile> {
|
|
60
|
+
const index = Math.min(GAIN_DB.length - 1, Math.max(1, Math.round(gain)))
|
|
61
|
+
const factor = Math.pow(10, (GAIN_DB[index]! - GAIN_DB[MIC_TUNING_FIRMWARE_DEFAULTS.gain]!) / 20)
|
|
62
|
+
return {
|
|
63
|
+
gain: index,
|
|
64
|
+
open: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.open * factor),
|
|
65
|
+
close: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.close * factor),
|
|
66
|
+
sp_open: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.sp_open * factor),
|
|
67
|
+
sp_close: Math.round(MIC_TUNING_FIRMWARE_DEFAULTS.sp_close * factor),
|
|
68
|
+
}
|
|
23
69
|
}
|
|
24
70
|
|
|
25
71
|
/** A live microphone lease. */
|
|
@@ -36,6 +82,8 @@ export type ResolvedMicPolicy = {
|
|
|
36
82
|
pinGlasses: boolean
|
|
37
83
|
/** Tuning override for the glasses, or null to leave the OS value in force. */
|
|
38
84
|
micTuning: MicTuningProfile | null
|
|
85
|
+
/** Run the center-mic loudness gate, or null to leave the OS value in force. */
|
|
86
|
+
loudnessGate: boolean | null
|
|
39
87
|
}
|
|
40
88
|
|
|
41
89
|
/** Platform facts the policy cannot infer from the sessions alone. */
|
|
@@ -55,22 +103,40 @@ export type MicPlatformCaps = {
|
|
|
55
103
|
* step above everything else. It suits a wearer dictating to a transcription
|
|
56
104
|
* miniapp across a room; it clips a wearer talking into a Teams call.
|
|
57
105
|
*
|
|
58
|
-
* Gain is the only knob here on purpose. A call forces hardware VAD off and
|
|
59
|
-
* the Barrier is off by default, which puts the firmware gate in its `pass`
|
|
60
|
-
* branch, so the RMS thresholds cannot affect a call.
|
|
61
|
-
*
|
|
62
106
|
* Index 14 is +26 dB: one step down, and the step that removes the table's
|
|
63
107
|
* anomalous jump. A same-voice sweep showed 15 rails hard (2.5–4.2% clip);
|
|
64
108
|
* 14/13/12 still kiss the rail on syllable tips. 13 is +24 dB, the middle
|
|
65
109
|
* of that band, while we listen for loudness vs residual clip.
|
|
110
|
+
*
|
|
111
|
+
* The thresholds ride along with the gain rather than being stated here, so
|
|
112
|
+
* changing the index above cannot leave the gate calibrated for a level the
|
|
113
|
+
* ADC no longer produces.
|
|
66
114
|
*/
|
|
67
115
|
export const MIC_USE_CASE_PROFILES: Record<MicUseCase, MicTuningProfile> = {
|
|
68
|
-
voice_call:
|
|
116
|
+
voice_call: scaleMicTuningToGain(13),
|
|
69
117
|
transcription: {},
|
|
70
118
|
voice_assistant: {},
|
|
71
119
|
diagnostic: {},
|
|
72
120
|
}
|
|
73
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Use cases that run the center-mic loudness gate ("Barrier").
|
|
124
|
+
*
|
|
125
|
+
* A voice call is the one place it earns its keep. There is no echo canceller
|
|
126
|
+
* on the LC3 uplink — `MENTRA_LC3_SPEECH_PROCESSING` is 0 and the capture path
|
|
127
|
+
* ignores the playback reference — so without a gate the far end hears itself
|
|
128
|
+
* through the wearer's speaker. Barrier is the safe half of the firmware's two
|
|
129
|
+
* gates: it zeroes a quiet frame but never stops transmitting, where VAD drops
|
|
130
|
+
* the stream outright. Paired with the speaker-elevated thresholds above, that
|
|
131
|
+
* suppresses leak while the far end is talking and costs the wearer nothing.
|
|
132
|
+
*/
|
|
133
|
+
export const MIC_USE_CASE_LOUDNESS_GATE: Record<MicUseCase, boolean> = {
|
|
134
|
+
voice_call: true,
|
|
135
|
+
transcription: false,
|
|
136
|
+
voice_assistant: false,
|
|
137
|
+
diagnostic: false,
|
|
138
|
+
}
|
|
139
|
+
|
|
74
140
|
/**
|
|
75
141
|
* Packages allowed to hold a `voice_call` session.
|
|
76
142
|
*
|
|
@@ -109,21 +175,24 @@ export function resolveMicPolicy(
|
|
|
109
175
|
const effective = caps.glassesPcmUplink ? sessions : sessions.filter((s) => s.source !== "glasses")
|
|
110
176
|
|
|
111
177
|
let pinGlasses = false
|
|
112
|
-
let
|
|
178
|
+
let loudnessGate = false
|
|
179
|
+
let winning: MicTuningProfile | null = null
|
|
113
180
|
for (const session of effective) {
|
|
114
181
|
if (session.source !== "glasses") continue
|
|
115
182
|
pinGlasses = true
|
|
116
|
-
|
|
183
|
+
if (MIC_USE_CASE_LOUDNESS_GATE[session.useCase]) loudnessGate = true
|
|
184
|
+
const profile = MIC_USE_CASE_PROFILES[session.useCase]
|
|
185
|
+
if (typeof profile?.gain !== "number") continue
|
|
117
186
|
// Lowest index wins: clipping is the irreversible failure, a slightly
|
|
118
|
-
// quiet assistant is not.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
187
|
+
// quiet assistant is not. The whole profile travels with it, because its
|
|
188
|
+
// thresholds are only meaningful at its own gain.
|
|
189
|
+
if (winning === null || profile.gain < winning.gain!) winning = profile
|
|
122
190
|
}
|
|
123
191
|
|
|
124
192
|
return {
|
|
125
193
|
rawPcm: effective.length > 0,
|
|
126
194
|
pinGlasses,
|
|
127
|
-
micTuning:
|
|
195
|
+
micTuning: winning,
|
|
196
|
+
loudnessGate: pinGlasses ? loudnessGate : null,
|
|
128
197
|
}
|
|
129
198
|
}
|