@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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Microphone use-case policy.
|
|
3
|
+
*
|
|
4
|
+
* Applications say what they are doing ("voice_call"); the engine decides what
|
|
5
|
+
* that requires of the hardware. Nothing above this file knows that a voice
|
|
6
|
+
* call means ADC index 14, and no miniapp can ask for a gain directly.
|
|
7
|
+
*
|
|
8
|
+
* Pure: every function here is a function of the live session set. The lease
|
|
9
|
+
* bookkeeping lives in MicSessionManager, the merge with OS preferences lives
|
|
10
|
+
* in MicStateCoordinator, and the wire write lives below that again.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** What an application is doing with the microphone. */
|
|
14
|
+
export type MicUseCase = "voice_call" | "transcription" | "voice_assistant" | "diagnostic"
|
|
15
|
+
|
|
16
|
+
/** Which microphone the audio comes from. */
|
|
17
|
+
export type MicSource = "glasses" | "phone"
|
|
18
|
+
|
|
19
|
+
/** Subset of the `mic_tuning` wire payload. Fields left out keep firmware defaults. */
|
|
20
|
+
export type MicTuningProfile = {
|
|
21
|
+
/** codec_adc_vol index, 0-15. */
|
|
22
|
+
gain?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A live microphone lease. */
|
|
26
|
+
export type MicSessionSpec = {
|
|
27
|
+
useCase: MicUseCase
|
|
28
|
+
source: MicSource
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What the hardware should do, given every live session. */
|
|
32
|
+
export type ResolvedMicPolicy = {
|
|
33
|
+
/** Someone needs a continuous raw-PCM timeline, which also forces hardware VAD off. */
|
|
34
|
+
rawPcm: boolean
|
|
35
|
+
/** Pin the Bluetooth SDK to the glasses microphone. Only the glasses can be pinned. */
|
|
36
|
+
pinGlasses: boolean
|
|
37
|
+
/** Tuning override for the glasses, or null to leave the OS value in force. */
|
|
38
|
+
micTuning: MicTuningProfile | null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Platform facts the policy cannot infer from the sessions alone. */
|
|
42
|
+
export type MicPlatformCaps = {
|
|
43
|
+
/**
|
|
44
|
+
* Whether this platform can take the wearer's voice off the glasses as raw
|
|
45
|
+
* PCM over BLE LC3. False on iOS, which has no `setMicSourcePin` and never
|
|
46
|
+
* selects the `ble-lc3` uplink.
|
|
47
|
+
*/
|
|
48
|
+
glassesPcmUplink: boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Mentra Live ships CODEC_SADC_VOL = 15, the last entry of codec_adc_vol[] and
|
|
53
|
+
* +32 dB. That table steps 2 dB at a time everywhere except the final step,
|
|
54
|
+
* which jumps 6 dB from +26, so the default sits at the ceiling one oversized
|
|
55
|
+
* step above everything else. It suits a wearer dictating to a transcription
|
|
56
|
+
* miniapp across a room; it clips a wearer talking into a Teams call.
|
|
57
|
+
*
|
|
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
|
+
* Index 14 is +26 dB: one step down, and the step that removes the table's
|
|
63
|
+
* anomalous jump. A same-voice sweep showed 15 rails hard (2.5–4.2% clip);
|
|
64
|
+
* 14/13/12 still kiss the rail on syllable tips. 13 is +24 dB, the middle
|
|
65
|
+
* of that band, while we listen for loudness vs residual clip.
|
|
66
|
+
*/
|
|
67
|
+
export const MIC_USE_CASE_PROFILES: Record<MicUseCase, MicTuningProfile> = {
|
|
68
|
+
voice_call: {gain: 13},
|
|
69
|
+
transcription: {},
|
|
70
|
+
voice_assistant: {},
|
|
71
|
+
diagnostic: {},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Packages allowed to hold a `voice_call` session.
|
|
76
|
+
*
|
|
77
|
+
* Which app may make a voice call is policy, so it lives beside the profiles
|
|
78
|
+
* rather than in the request handler that enforces it.
|
|
79
|
+
*/
|
|
80
|
+
export const VOICE_CALL_PACKAGES: readonly string[] = ["com.mentra.call"]
|
|
81
|
+
|
|
82
|
+
/** Every use case, for validating an inbound request. */
|
|
83
|
+
export const MIC_USE_CASES: readonly MicUseCase[] = [
|
|
84
|
+
"voice_call",
|
|
85
|
+
"transcription",
|
|
86
|
+
"voice_assistant",
|
|
87
|
+
"diagnostic",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
/** Owners of engine-internal sessions. Miniapps cannot claim these use cases. */
|
|
91
|
+
export const ENGINE_OWNER_PREFIX = "engine:"
|
|
92
|
+
|
|
93
|
+
/** Use cases only engine features may acquire. */
|
|
94
|
+
export const ENGINE_ONLY_USE_CASES: readonly MicUseCase[] = ["diagnostic"]
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve every live session into one hardware state.
|
|
98
|
+
*
|
|
99
|
+
* On a platform without a glasses PCM uplink a glasses session is a lease and
|
|
100
|
+
* nothing more: it satisfies ownership checks so an iOS call can still be
|
|
101
|
+
* modelled the same way, but it claims no PCM, pins nothing, and applies no
|
|
102
|
+
* gain, because the wearer's voice does not reach the call through the BES
|
|
103
|
+
* BLE path there.
|
|
104
|
+
*/
|
|
105
|
+
export function resolveMicPolicy(
|
|
106
|
+
sessions: readonly MicSessionSpec[],
|
|
107
|
+
caps: MicPlatformCaps,
|
|
108
|
+
): ResolvedMicPolicy {
|
|
109
|
+
const effective = caps.glassesPcmUplink ? sessions : sessions.filter((s) => s.source !== "glasses")
|
|
110
|
+
|
|
111
|
+
let pinGlasses = false
|
|
112
|
+
let gain: number | undefined
|
|
113
|
+
for (const session of effective) {
|
|
114
|
+
if (session.source !== "glasses") continue
|
|
115
|
+
pinGlasses = true
|
|
116
|
+
const profileGain = MIC_USE_CASE_PROFILES[session.useCase]?.gain
|
|
117
|
+
// Lowest index wins: clipping is the irreversible failure, a slightly
|
|
118
|
+
// quiet assistant is not.
|
|
119
|
+
if (typeof profileGain === "number" && (gain === undefined || profileGain < gain)) {
|
|
120
|
+
gain = profileGain
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
rawPcm: effective.length > 0,
|
|
126
|
+
pinGlasses,
|
|
127
|
+
micTuning: gain === undefined ? null : {gain},
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -7,6 +7,9 @@ export const MENTRA_LIVE_SETTING_KEYS: string[] = [
|
|
|
7
7
|
"preferred_mic",
|
|
8
8
|
"voice_activity_detection_enabled",
|
|
9
9
|
"loudness_gate_enabled",
|
|
10
|
+
// Effective tuning only. mic_tuning_desired is engine-side state and must
|
|
11
|
+
// never appear here, or a persisted super-mode value would reach the glasses.
|
|
12
|
+
"mic_tuning",
|
|
10
13
|
"core_token",
|
|
11
14
|
"auth_email",
|
|
12
15
|
"button_photo_size",
|
package/src/stores/settings.ts
CHANGED
|
@@ -464,6 +464,42 @@ export const SETTINGS: Record<string, Setting> = {
|
|
|
464
464
|
saveOnServer: true,
|
|
465
465
|
persist: true,
|
|
466
466
|
},
|
|
467
|
+
/*
|
|
468
|
+
* Mentra Live mic tuning, split in two on purpose.
|
|
469
|
+
*
|
|
470
|
+
* `mic_tuning_desired` is what the super user set. It persists so that
|
|
471
|
+
* turning super mode off and on again does not lose the calibration, and it
|
|
472
|
+
* stays out of BLUETOOTH_SETTING_KEYS so it can never reach the glasses on
|
|
473
|
+
* its own. It is device-local: this is a hardware calibration aid, not
|
|
474
|
+
* something that should follow an account onto another pair of glasses.
|
|
475
|
+
*
|
|
476
|
+
* `mic_tuning` is the effective value, derived in getBluetoothSettings as
|
|
477
|
+
* `super_mode ? desired : {}`. It is the only one native ever sees. Because
|
|
478
|
+
* it is not persisted and native seeds no default for it, a process that
|
|
479
|
+
* connects before the engine has authorized anything can only send a reset
|
|
480
|
+
* -- the super-mode guarantee is then a property of what native can hold,
|
|
481
|
+
* not of which initialiser happened to run first.
|
|
482
|
+
*
|
|
483
|
+
* `{}` rather than null for "no tuning": the native store drops null writes,
|
|
484
|
+
* so null would leave a previously applied value in place.
|
|
485
|
+
*
|
|
486
|
+
* The empty object is a stable singleton so GlassesSettingsSync's reference
|
|
487
|
+
* diff does not treat every unrelated settings write as a mic_tuning change.
|
|
488
|
+
*/
|
|
489
|
+
mic_tuning_desired: {
|
|
490
|
+
key: "mic_tuning_desired",
|
|
491
|
+
defaultValue: () => null,
|
|
492
|
+
writable: true,
|
|
493
|
+
saveOnServer: false,
|
|
494
|
+
persist: true,
|
|
495
|
+
},
|
|
496
|
+
mic_tuning: {
|
|
497
|
+
key: "mic_tuning",
|
|
498
|
+
defaultValue: () => ({}),
|
|
499
|
+
writable: true,
|
|
500
|
+
saveOnServer: false,
|
|
501
|
+
persist: false,
|
|
502
|
+
},
|
|
467
503
|
always_on_status_bar: {
|
|
468
504
|
key: "always_on_status_bar",
|
|
469
505
|
defaultValue: () => false,
|
|
@@ -757,6 +793,8 @@ export const BLUETOOTH_SETTING_KEYS: string[] = [
|
|
|
757
793
|
SETTINGS.power_saving_mode.key,
|
|
758
794
|
SETTINGS.voice_activity_detection_enabled.key,
|
|
759
795
|
SETTINGS.loudness_gate_enabled.key,
|
|
796
|
+
// Effective tuning only; mic_tuning_desired deliberately stays engine-side.
|
|
797
|
+
SETTINGS.mic_tuning.key,
|
|
760
798
|
SETTINGS.lc3_frame_size.key,
|
|
761
799
|
SETTINGS.preferred_mic.key,
|
|
762
800
|
SETTINGS.screen_disabled.key,
|
|
@@ -819,6 +857,9 @@ export const PAIRING_IDENTITY_KEYS: string[] = Object.values(SETTINGS)
|
|
|
819
857
|
.filter((setting) => setting.nativeAuthoritative)
|
|
820
858
|
.map((setting) => setting.key)
|
|
821
859
|
|
|
860
|
+
/** Stable empty effective tuning. Native reads this as `{"reset":1}`. */
|
|
861
|
+
const EMPTY_MIC_TUNING: Record<string, number> = Object.freeze({})
|
|
862
|
+
|
|
822
863
|
// const PER_GLASSES_SETTINGS_KEYS: string[] = [SETTINGS.preferred_mic.key]
|
|
823
864
|
|
|
824
865
|
export interface SettingsState {
|
|
@@ -834,6 +875,11 @@ export interface SettingsState {
|
|
|
834
875
|
loadAllSettings: () => AsyncResult<void, Error>
|
|
835
876
|
// Utility methods
|
|
836
877
|
getBluetoothSettings: () => Record<string, any>
|
|
878
|
+
/**
|
|
879
|
+
* The mic tuning native is allowed to send: the desired value while super
|
|
880
|
+
* mode is on, `{}` (meaning "reset to firmware defaults") otherwise.
|
|
881
|
+
*/
|
|
882
|
+
getEffectiveMicTuning: () => Record<string, number>
|
|
837
883
|
resetAllSettingsLocally: () => void
|
|
838
884
|
}
|
|
839
885
|
|
|
@@ -1193,10 +1239,30 @@ export const useSettingsStore = create<SettingsState>()(
|
|
|
1193
1239
|
if (key === SETTINGS.core_token.key && (typeof value !== "string" || value.trim().length === 0)) {
|
|
1194
1240
|
continue
|
|
1195
1241
|
}
|
|
1242
|
+
if (key === SETTINGS.mic_tuning.key) {
|
|
1243
|
+
// Derived here rather than stored, so there is no path that can push
|
|
1244
|
+
// a persisted tuning to the glasses while super mode is off. Every
|
|
1245
|
+
// sync route (change diff, on-connect replay, full seed) reads this.
|
|
1246
|
+
bluetoothSettings[key] = state.getEffectiveMicTuning()
|
|
1247
|
+
continue
|
|
1248
|
+
}
|
|
1196
1249
|
bluetoothSettings[key] = value
|
|
1197
1250
|
}
|
|
1198
1251
|
return bluetoothSettings
|
|
1199
1252
|
},
|
|
1253
|
+
getEffectiveMicTuning: () => {
|
|
1254
|
+
const state = get()
|
|
1255
|
+
if (!state.getSetting(SETTINGS.super_mode.key)) return EMPTY_MIC_TUNING
|
|
1256
|
+
const desired = state.getSetting(SETTINGS.mic_tuning_desired.key)
|
|
1257
|
+
if (!desired || typeof desired !== "object") return EMPTY_MIC_TUNING
|
|
1258
|
+
const effective: Record<string, number> = {}
|
|
1259
|
+
for (const [key, value] of Object.entries(desired as Record<string, unknown>)) {
|
|
1260
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1261
|
+
effective[key] = Math.round(value)
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return Object.keys(effective).length === 0 ? EMPTY_MIC_TUNING : effective
|
|
1265
|
+
},
|
|
1200
1266
|
resetAllSettingsLocally: () => {
|
|
1201
1267
|
set((_state) => ({
|
|
1202
1268
|
settings: getDefaultSettings(),
|
package/src/utils/pcm16.ts
CHANGED
|
@@ -29,6 +29,11 @@ export function pcmDataView(frame: unknown): DataView | null {
|
|
|
29
29
|
return null
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** Signed 16-bit full scale. `-32768` abs-counts as 32768 and is also a rail. */
|
|
33
|
+
export const PCM16_FULL_SCALE = 32767
|
|
34
|
+
/** Headroom line: loud speech that is not yet hard-clipped. */
|
|
35
|
+
export const PCM16_NEAR_CLIP = 30000
|
|
36
|
+
|
|
32
37
|
export type Pcm16Level = {
|
|
33
38
|
/** Mean absolute sample value (16-bit scale). ~30–60 is a quiet room on Mentra Live LC3. */
|
|
34
39
|
meanAbs: number
|
|
@@ -36,6 +41,31 @@ export type Pcm16Level = {
|
|
|
36
41
|
peak: number
|
|
37
42
|
/** Samples counted. */
|
|
38
43
|
samples: number
|
|
44
|
+
/** Samples at the int16 rail (`>= 32767`). */
|
|
45
|
+
clipped: number
|
|
46
|
+
/** Samples at or above [PCM16_NEAR_CLIP]. */
|
|
47
|
+
nearClip: number
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type Pcm16WindowStats = Pcm16Level & {
|
|
51
|
+
/** `peak / 32767`, percent. */
|
|
52
|
+
peakPct: number
|
|
53
|
+
/** `clipped / samples`, percent. */
|
|
54
|
+
clipPct: number
|
|
55
|
+
/** `nearClip / samples`, percent. */
|
|
56
|
+
nearClipPct: number
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Percentages for a closed window. Zero samples → all percents 0. */
|
|
60
|
+
export function pcm16WindowStats(level: Pcm16Level): Pcm16WindowStats {
|
|
61
|
+
const denom = level.samples || 1
|
|
62
|
+
const pct = (count: number) => Math.round((count / denom) * 1000) / 10
|
|
63
|
+
return {
|
|
64
|
+
...level,
|
|
65
|
+
peakPct: Math.round((level.peak / PCM16_FULL_SCALE) * 1000) / 10,
|
|
66
|
+
clipPct: level.samples ? pct(level.clipped) : 0,
|
|
67
|
+
nearClipPct: level.samples ? pct(level.nearClip) : 0,
|
|
68
|
+
}
|
|
39
69
|
}
|
|
40
70
|
|
|
41
71
|
/** Level statistics over a batch of PCM16 frames. Unreadable frames are skipped, never thrown on. */
|
|
@@ -43,6 +73,8 @@ export function summarizePcm16(frames: unknown[]): Pcm16Level {
|
|
|
43
73
|
let sum = 0
|
|
44
74
|
let peak = 0
|
|
45
75
|
let samples = 0
|
|
76
|
+
let clipped = 0
|
|
77
|
+
let nearClip = 0
|
|
46
78
|
for (const frame of frames) {
|
|
47
79
|
const view = pcmDataView(frame)
|
|
48
80
|
if (!view) continue
|
|
@@ -51,10 +83,12 @@ export function summarizePcm16(frames: unknown[]): Pcm16Level {
|
|
|
51
83
|
const v = Math.abs(view.getInt16(i * 2, true))
|
|
52
84
|
sum += v
|
|
53
85
|
if (v > peak) peak = v
|
|
86
|
+
if (v >= PCM16_FULL_SCALE) clipped++
|
|
87
|
+
if (v >= PCM16_NEAR_CLIP) nearClip++
|
|
54
88
|
}
|
|
55
89
|
samples += n
|
|
56
90
|
}
|
|
57
|
-
return {meanAbs: samples ? Math.round(sum / samples) : 0, peak, samples}
|
|
91
|
+
return {meanAbs: samples ? Math.round(sum / samples) : 0, peak, samples, clipped, nearClip}
|
|
58
92
|
}
|
|
59
93
|
|
|
60
94
|
/**
|
|
@@ -65,6 +99,8 @@ export class Pcm16LevelMeter {
|
|
|
65
99
|
private sum = 0
|
|
66
100
|
private peakValue = 0
|
|
67
101
|
private count = 0
|
|
102
|
+
private clipped = 0
|
|
103
|
+
private nearClip = 0
|
|
68
104
|
|
|
69
105
|
add(frame: unknown): void {
|
|
70
106
|
const view = pcmDataView(frame)
|
|
@@ -74,6 +110,8 @@ export class Pcm16LevelMeter {
|
|
|
74
110
|
const v = Math.abs(view.getInt16(i * 2, true))
|
|
75
111
|
this.sum += v
|
|
76
112
|
if (v > this.peakValue) this.peakValue = v
|
|
113
|
+
if (v >= PCM16_FULL_SCALE) this.clipped++
|
|
114
|
+
if (v >= PCM16_NEAR_CLIP) this.nearClip++
|
|
77
115
|
}
|
|
78
116
|
this.count += n
|
|
79
117
|
}
|
|
@@ -84,10 +122,14 @@ export class Pcm16LevelMeter {
|
|
|
84
122
|
meanAbs: this.count ? Math.round(this.sum / this.count) : 0,
|
|
85
123
|
peak: this.peakValue,
|
|
86
124
|
samples: this.count,
|
|
125
|
+
clipped: this.clipped,
|
|
126
|
+
nearClip: this.nearClip,
|
|
87
127
|
}
|
|
88
128
|
this.sum = 0
|
|
89
129
|
this.peakValue = 0
|
|
90
130
|
this.count = 0
|
|
131
|
+
this.clipped = 0
|
|
132
|
+
this.nearClip = 0
|
|
91
133
|
return level
|
|
92
134
|
}
|
|
93
135
|
}
|