@glassly/engine 0.3.3-dev.655 → 0.3.3-dev.660
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/facades/display.d.ts.map +1 -1
- package/build/facades/display.js +1 -2
- package/build/facades/display.js.map +1 -1
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +1 -13
- package/build/facades/glasses.js.map +1 -1
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/DeviceEventRouter.d.ts.map +1 -1
- package/build/services/DeviceEventRouter.js +0 -7
- package/build/services/DeviceEventRouter.js.map +1 -1
- package/build/services/GlassesTouchInput.d.ts +5 -2
- package/build/services/GlassesTouchInput.d.ts.map +1 -1
- package/build/services/GlassesTouchInput.js +19 -9
- package/build/services/GlassesTouchInput.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +0 -1
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +0 -18
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/utils/audio/pcmLevel.d.ts +9 -0
- package/build/utils/audio/pcmLevel.d.ts.map +1 -0
- package/build/utils/audio/pcmLevel.js +21 -0
- package/build/utils/audio/pcmLevel.js.map +1 -0
- package/package.json +7 -7
- package/src/facades/display.ts +1 -3
- package/src/facades/glasses.ts +2 -14
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/services/DeviceEventRouter.ts +0 -8
- package/src/services/GlassesTouchInput.ts +22 -9
- package/src/services/LocalMiniappRuntime.ts +0 -17
- package/src/utils/audio/pcmLevel.ts +20 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Floor for mic level readings; digital silence would otherwise be -Infinity. */
|
|
2
|
+
export declare const PCM_LEVEL_FLOOR_DBFS = -120;
|
|
3
|
+
/**
|
|
4
|
+
* RMS level of one s16le chunk in dBFS, on the volume gate's 32767 full scale.
|
|
5
|
+
* Native events deliver PCM as a Uint8Array (Expo converts Data/ByteArray),
|
|
6
|
+
* not the ArrayBuffer the event type declares, so read through a DataView.
|
|
7
|
+
*/
|
|
8
|
+
export declare function pcmLevelDbfs(pcm: ArrayBuffer | ArrayBufferView): number;
|
|
9
|
+
//# sourceMappingURL=pcmLevel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pcmLevel.d.ts","sourceRoot":"","sources":["../../../src/utils/audio/pcmLevel.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,eAAO,MAAM,oBAAoB,OAAO,CAAA;AAExC;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,WAAW,GAAG,eAAe,GAAG,MAAM,CAWvE"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Floor for mic level readings; digital silence would otherwise be -Infinity. */
|
|
2
|
+
export const PCM_LEVEL_FLOOR_DBFS = -120;
|
|
3
|
+
/**
|
|
4
|
+
* RMS level of one s16le chunk in dBFS, on the volume gate's 32767 full scale.
|
|
5
|
+
* Native events deliver PCM as a Uint8Array (Expo converts Data/ByteArray),
|
|
6
|
+
* not the ArrayBuffer the event type declares, so read through a DataView.
|
|
7
|
+
*/
|
|
8
|
+
export function pcmLevelDbfs(pcm) {
|
|
9
|
+
const view = ArrayBuffer.isView(pcm) ? new DataView(pcm.buffer, pcm.byteOffset, pcm.byteLength) : new DataView(pcm);
|
|
10
|
+
const count = view.byteLength >> 1;
|
|
11
|
+
if (count === 0)
|
|
12
|
+
return PCM_LEVEL_FLOOR_DBFS;
|
|
13
|
+
let sumSquares = 0;
|
|
14
|
+
for (let i = 0; i < count; i++) {
|
|
15
|
+
const sample = view.getInt16(i * 2, true);
|
|
16
|
+
sumSquares += sample * sample;
|
|
17
|
+
}
|
|
18
|
+
const rms = Math.sqrt(sumSquares / count);
|
|
19
|
+
return Math.max(PCM_LEVEL_FLOOR_DBFS, 20 * Math.log10(rms / 32767));
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=pcmLevel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pcmLevel.js","sourceRoot":"","sources":["../../../src/utils/audio/pcmLevel.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,GAAG,CAAA;AAExC;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,GAAkC;IAC7D,MAAM,IAAI,GAAG,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAA;IACnH,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,IAAI,CAAC,CAAA;IAClC,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,oBAAoB,CAAA;IAC5C,IAAI,UAAU,GAAG,CAAC,CAAA;IAClB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAA;QACzC,UAAU,IAAI,MAAM,GAAG,MAAM,CAAA;IAC/B,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC,CAAA;IACzC,OAAO,IAAI,CAAC,GAAG,CAAC,oBAAoB,EAAE,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC,CAAC,CAAA;AACrE,CAAC","sourcesContent":["/** Floor for mic level readings; digital silence would otherwise be -Infinity. */\nexport const PCM_LEVEL_FLOOR_DBFS = -120\n\n/**\n * RMS level of one s16le chunk in dBFS, on the volume gate's 32767 full scale.\n * Native events deliver PCM as a Uint8Array (Expo converts Data/ByteArray),\n * not the ArrayBuffer the event type declares, so read through a DataView.\n */\nexport function pcmLevelDbfs(pcm: ArrayBuffer | ArrayBufferView): number {\n const view = ArrayBuffer.isView(pcm) ? new DataView(pcm.buffer, pcm.byteOffset, pcm.byteLength) : new DataView(pcm)\n const count = view.byteLength >> 1\n if (count === 0) return PCM_LEVEL_FLOOR_DBFS\n let sumSquares = 0\n for (let i = 0; i < count; i++) {\n const sample = view.getInt16(i * 2, true)\n sumSquares += sample * sample\n }\n const rms = Math.sqrt(sumSquares / count)\n return Math.max(PCM_LEVEL_FLOOR_DBFS, 20 * Math.log10(rms / 32767))\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glassly/engine",
|
|
3
|
-
"version": "0.3.3-dev.
|
|
3
|
+
"version": "0.3.3-dev.660",
|
|
4
4
|
"description": "Glassly Engine — miniapp registry, WebView bridge, and miniapp store",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -97,17 +97,17 @@
|
|
|
97
97
|
"registry": "https://registry.npmjs.org/"
|
|
98
98
|
},
|
|
99
99
|
"dependencies": {
|
|
100
|
-
"@glassly/bluetooth-sdk": "0.3.3-dev.
|
|
101
|
-
"@glassly/cloud-client": "0.3.3-dev.
|
|
102
|
-
"@glassly/cloud-protocol": "0.3.3-dev.
|
|
103
|
-
"@glassly/crust": "0.3.3-dev.
|
|
104
|
-
"@glassly/miniapp": "0.3.3-dev.
|
|
100
|
+
"@glassly/bluetooth-sdk": "0.3.3-dev.660",
|
|
101
|
+
"@glassly/cloud-client": "0.3.3-dev.660",
|
|
102
|
+
"@glassly/cloud-protocol": "0.3.3-dev.660",
|
|
103
|
+
"@glassly/crust": "0.3.3-dev.660",
|
|
104
|
+
"@glassly/miniapp": "0.3.3-dev.660",
|
|
105
105
|
"buffer": "^6.0.3",
|
|
106
106
|
"events": "^3.3.0",
|
|
107
107
|
"react-native-marked": "8.1.1",
|
|
108
108
|
"semver": "^7.7.4",
|
|
109
109
|
"typesafe-ts": "^1.7.1",
|
|
110
|
-
"@glassly/llm-providers": "0.3.3-dev.
|
|
110
|
+
"@glassly/llm-providers": "0.3.3-dev.660"
|
|
111
111
|
},
|
|
112
112
|
"devDependencies": {
|
|
113
113
|
"@glassly/llm-providers": "workspace:*",
|
package/src/facades/display.ts
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import BluetoothSdk from "@glassly/bluetooth-sdk"
|
|
8
8
|
import {sendGesture} from "../services/GlassesTouchInput"
|
|
9
|
-
import localMiniappRuntime from "../services/LocalMiniappRuntime"
|
|
10
9
|
import {useDisplayStore} from "../stores/display"
|
|
11
10
|
import {displayMirror} from "./displayMirror"
|
|
12
11
|
|
|
@@ -22,8 +21,7 @@ export const display = {
|
|
|
22
21
|
/** An overlay app holds the slot. */
|
|
23
22
|
isOpen: (): boolean => useDisplayStore.getState().overlayOwner !== null,
|
|
24
23
|
/** As if the head was raised/lowered (the dashboard app applies contextual_dashboard). */
|
|
25
|
-
setHeadUp: (up: boolean): void =>
|
|
26
|
-
localMiniappRuntime.forwardEvent("head_up", {position: up ? "up" : "down", timestamp: Date.now()}),
|
|
24
|
+
setHeadUp: (up: boolean): void => sendGesture(up ? "head_up" : "head_down"),
|
|
27
25
|
/** A double tap on the empty screen: the dashboard app opens (simulator button). */
|
|
28
26
|
open: (): void => {
|
|
29
27
|
if (!display.dashboard.isOpen()) sendGesture("double_tap")
|
package/src/facades/glasses.ts
CHANGED
|
@@ -32,22 +32,10 @@ import neuralBandInput, {
|
|
|
32
32
|
} from "../services/neuralBand/NeuralBandInput"
|
|
33
33
|
import {recordSupportProfileConnectionFailure} from "../services/SupportProfileSync"
|
|
34
34
|
import {getModelCapabilities, type DeviceTypes} from "../types"
|
|
35
|
+
import {pcmLevelDbfs} from "../utils/audio/pcmLevel"
|
|
35
36
|
import {glassesWifi} from "./glassesWifi"
|
|
36
37
|
import {glassesSettings} from "./glassesSettings"
|
|
37
38
|
|
|
38
|
-
/** Floor for the mic level readout; digital silence would otherwise be -Infinity. */
|
|
39
|
-
const MIC_LEVEL_FLOOR_DBFS = -120
|
|
40
|
-
|
|
41
|
-
/** RMS level of one s16le chunk in dBFS, on the volume gate's 32767 full scale. */
|
|
42
|
-
function pcmLevelDbfs(pcm: ArrayBuffer): number {
|
|
43
|
-
const samples = new Int16Array(pcm, 0, pcm.byteLength >> 1)
|
|
44
|
-
if (samples.length === 0) return MIC_LEVEL_FLOOR_DBFS
|
|
45
|
-
let sumSquares = 0
|
|
46
|
-
for (let i = 0; i < samples.length; i++) sumSquares += samples[i] * samples[i]
|
|
47
|
-
const rms = Math.sqrt(sumSquares / samples.length)
|
|
48
|
-
return Math.max(MIC_LEVEL_FLOOR_DBFS, 20 * Math.log10(rms / 32767))
|
|
49
|
-
}
|
|
50
|
-
|
|
51
39
|
let micLevelSubscriptions = 0
|
|
52
40
|
|
|
53
41
|
function projectStatus() {
|
|
@@ -328,7 +316,7 @@ export const glasses = {
|
|
|
328
316
|
onMicLevel: (cb: (dbfs: number) => void): (() => void) => {
|
|
329
317
|
const owner = `mic_level_${++micLevelSubscriptions}`
|
|
330
318
|
micStateCoordinator.acquireHostPcm(owner)
|
|
331
|
-
const sub = BluetoothSdk.addListener("mic_pcm", (event: {pcm: ArrayBuffer}) => {
|
|
319
|
+
const sub = BluetoothSdk.addListener("mic_pcm", (event: {pcm: ArrayBuffer | Uint8Array}) => {
|
|
332
320
|
if (event?.pcm) cb(pcmLevelDbfs(event.pcm))
|
|
333
321
|
})
|
|
334
322
|
return () => {
|
|
@@ -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": "0.3.3",
|
|
15
|
-
"releaseIdentity": "0.3.3-dev.
|
|
16
|
-
"releaseSetId": "glassly-0.3.3-dev.
|
|
17
|
-
"sourceCommit": "
|
|
15
|
+
"releaseIdentity": "0.3.3-dev.660",
|
|
16
|
+
"releaseSetId": "glassly-0.3.3-dev.660",
|
|
17
|
+
"sourceCommit": "77dd44275e4d9138cd7296730dc23aa92d044d14",
|
|
18
18
|
"otaManifestUrl": null,
|
|
19
19
|
"otaManifestSha256": null
|
|
20
20
|
})
|
|
@@ -257,14 +257,6 @@ export function startDeviceEventRouter(): void {
|
|
|
257
257
|
})
|
|
258
258
|
}),
|
|
259
259
|
)
|
|
260
|
-
// Head position — translate native {up:boolean} → SDK {position:"up"|"down"}.
|
|
261
|
-
// The dashboard miniapp opens on head-up from this stream (ADR-0006).
|
|
262
|
-
subs.push(
|
|
263
|
-
BluetoothSdk.addListener("head_up", (event) => {
|
|
264
|
-
if (event.up) dashboardOpenTrace.trigger("head-up")
|
|
265
|
-
localMiniappRuntime.forwardEvent("head_up", {position: event.up ? "up" : "down", timestamp: Date.now()})
|
|
266
|
-
}),
|
|
267
|
-
)
|
|
268
260
|
subs.push({remove: dashboardOpenTrace.start()})
|
|
269
261
|
// On-device STT transcripts → local miniapps only while the local engine is
|
|
270
262
|
// active (cloud fallback or an explicit forceLocal subscription). The runtime
|
|
@@ -13,6 +13,7 @@ import localMiniappRuntime from "./LocalMiniappRuntime"
|
|
|
13
13
|
import {MiniappExitGesture} from "./MiniappExitGesture"
|
|
14
14
|
import nativeNavigationService from "./NativeNavigationService"
|
|
15
15
|
import {isOverlayApp} from "./pages/overlayApps"
|
|
16
|
+
import {pagesEligible} from "./pages/pageEligibility"
|
|
16
17
|
|
|
17
18
|
const miniappExitGesture = new MiniappExitGesture({
|
|
18
19
|
getState: () => useAppStatusStore.getState(),
|
|
@@ -46,7 +47,10 @@ function shouldForwardScroll(event: TouchEvent): boolean {
|
|
|
46
47
|
* unclaimed one goes to every subscriber and then the engine's default. The
|
|
47
48
|
* one rule ahead of the claims: an unclaimed double tap on a miniapp whose
|
|
48
49
|
* frame is visibly on the glasses leaves that app, whoever else claimed the
|
|
49
|
-
* gesture — the dashboard's claim is for the empty screen.
|
|
50
|
+
* gesture — the dashboard's claim is for the empty screen. Head position
|
|
51
|
+
* (head_up / head_down) is a gesture like any other: the dashboard claims it
|
|
52
|
+
* while it runs, and an unclaimed head_up on glasses that defer the dashboard
|
|
53
|
+
* to the phone opens the native one, as it would with no app at all.
|
|
50
54
|
*/
|
|
51
55
|
export function dispatchTouchEvent(event: TouchEvent): void {
|
|
52
56
|
const gesture = event.gestureName
|
|
@@ -54,6 +58,7 @@ export function dispatchTouchEvent(event: TouchEvent): void {
|
|
|
54
58
|
// The Neural Band's middle finger (index 1) shares the tap names but is left
|
|
55
59
|
// to miniapps: the engine's own double-tap behaviors are the index finger's.
|
|
56
60
|
const osDoubleTap = gesture === "double_tap" && event.index !== 1
|
|
61
|
+
const osHeadUp = gesture === "head_up"
|
|
57
62
|
|
|
58
63
|
if (osDoubleTap) {
|
|
59
64
|
// A visible main miniapp opts out of OS exit by registering double tap (the
|
|
@@ -73,8 +78,10 @@ export function dispatchTouchEvent(event: TouchEvent): void {
|
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
if (claimant !== null) {
|
|
76
|
-
// An overlay app claims the double tap on an empty screen to open the dashboard.
|
|
77
|
-
if (gesture === "double_tap" && isOverlayApp(claimant))
|
|
81
|
+
// An overlay app claims the double tap and head-up on an empty screen to open the dashboard.
|
|
82
|
+
if ((gesture === "double_tap" || osHeadUp) && isOverlayApp(claimant)) {
|
|
83
|
+
dashboardOpenTrace.trigger(osHeadUp ? "head-up" : "double-tap", event.timestamp)
|
|
84
|
+
}
|
|
78
85
|
// The press may have started before this app claimed its release edge.
|
|
79
86
|
gestureDotsService.noteGesture(gesture, true, event.index)
|
|
80
87
|
if (!shouldForwardScroll(event)) return
|
|
@@ -90,12 +97,16 @@ export function dispatchTouchEvent(event: TouchEvent): void {
|
|
|
90
97
|
if (!shouldForwardScroll(event)) return
|
|
91
98
|
localMiniappRuntime.forwardEvent("touch_event", event)
|
|
92
99
|
|
|
93
|
-
// Nobody claimed a double tap: the glasses'
|
|
94
|
-
// Glassly CFW glasses the dashboard miniapp claims
|
|
95
|
-
// (ADR-0006); stock firmware keeps its native dashboard (ADR-0002)
|
|
96
|
-
//
|
|
100
|
+
// Nobody claimed a double tap or a head-up: the glasses' native dashboard.
|
|
101
|
+
// On eligible Glassly CFW glasses the dashboard miniapp claims both while it
|
|
102
|
+
// runs (ADR-0006); stock firmware keeps its native dashboard (ADR-0002) and
|
|
103
|
+
// opens it on a head-up by itself, so only the double tap is relayed there.
|
|
104
|
+
// The modal nav UI owns the screen; showDashboard would fight it. Without
|
|
97
105
|
// glasses there is no dashboard to open, and the mirror keeps its frame.
|
|
98
|
-
|
|
106
|
+
// pagesEligible: connected Glassly CFW glasses, whose firmware leaves the
|
|
107
|
+
// dashboard to the phone; other firmware opens its own on a lift.
|
|
108
|
+
const opensNativeDashboard = osDoubleTap || (osHeadUp && pagesEligible())
|
|
109
|
+
if (opensNativeDashboard && !nativeNavigationService.isActive && isGlassesConnected(useGlassesStore.getState().connection)) {
|
|
99
110
|
useDisplayStore.getState().setDisplayEvent({layout: {layoutType: "unavailable", reason: "firmware-dashboard"}})
|
|
100
111
|
void Promise.resolve(BluetoothSdk.showDashboard()).catch((err) => {
|
|
101
112
|
console.error("DeviceEventRouter: showDashboard failed:", err)
|
|
@@ -123,7 +134,9 @@ export function sendGesture(
|
|
|
123
134
|
| "swipe_right"
|
|
124
135
|
| "long_press"
|
|
125
136
|
| "dial_up"
|
|
126
|
-
| "dial_down"
|
|
137
|
+
| "dial_down"
|
|
138
|
+
| "head_up"
|
|
139
|
+
| "head_down",
|
|
127
140
|
): void {
|
|
128
141
|
const model = useGlassesStore.getState().deviceModel
|
|
129
142
|
dispatchTouchEvent({
|
|
@@ -2345,20 +2345,6 @@ class LocalMiniappRuntime {
|
|
|
2345
2345
|
data: {key, value: this.settingsBridgeValue(key)},
|
|
2346
2346
|
})
|
|
2347
2347
|
}
|
|
2348
|
-
} else if (stream === "head_position") {
|
|
2349
|
-
// The glasses store has no headUp field (same on the pre-engine host
|
|
2350
|
-
// hook), so this stays a no-emit until head state lands in the store.
|
|
2351
|
-
const headUp = (gs as {headUp?: boolean}).headUp
|
|
2352
|
-
if (typeof headUp === "boolean") {
|
|
2353
|
-
this.sendToMiniapp(packageName, {
|
|
2354
|
-
type: MiniappResponseType.EVENT,
|
|
2355
|
-
streamType: "head_position",
|
|
2356
|
-
data: {
|
|
2357
|
-
position: headUp ? "up" : "down",
|
|
2358
|
-
timestamp: Date.now(),
|
|
2359
|
-
},
|
|
2360
|
-
})
|
|
2361
|
-
}
|
|
2362
2348
|
}
|
|
2363
2349
|
}
|
|
2364
2350
|
}
|
|
@@ -5457,7 +5443,6 @@ class LocalMiniappRuntime {
|
|
|
5457
5443
|
* Forward a streamed event to all miniapps subscribed to the given stream.
|
|
5458
5444
|
*
|
|
5459
5445
|
* Event name translation:
|
|
5460
|
-
* - Cloud sends "head_up" → miniapp protocol uses "head_position" (HEAD_POSITION)
|
|
5461
5446
|
* - Cloud sends "VAD" (uppercase) → miniapp protocol uses "vad" (lowercase)
|
|
5462
5447
|
*/
|
|
5463
5448
|
public forwardEvent(
|
|
@@ -5722,8 +5707,6 @@ class LocalMiniappRuntime {
|
|
|
5722
5707
|
// Cloud / Bluetooth SDK → miniapp protocol translations.
|
|
5723
5708
|
// Bluetooth SDK event names don't always match the miniapp wire values.
|
|
5724
5709
|
switch (cloudEventName) {
|
|
5725
|
-
case "head_up":
|
|
5726
|
-
return MiniappStreamType.HEAD_POSITION // head_up → head_position
|
|
5727
5710
|
case "accel_event":
|
|
5728
5711
|
return MiniappStreamType.ACCEL_DATA // accel_event (native) → accel_data
|
|
5729
5712
|
case "VAD":
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Floor for mic level readings; digital silence would otherwise be -Infinity. */
|
|
2
|
+
export const PCM_LEVEL_FLOOR_DBFS = -120
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* RMS level of one s16le chunk in dBFS, on the volume gate's 32767 full scale.
|
|
6
|
+
* Native events deliver PCM as a Uint8Array (Expo converts Data/ByteArray),
|
|
7
|
+
* not the ArrayBuffer the event type declares, so read through a DataView.
|
|
8
|
+
*/
|
|
9
|
+
export function pcmLevelDbfs(pcm: ArrayBuffer | ArrayBufferView): number {
|
|
10
|
+
const view = ArrayBuffer.isView(pcm) ? new DataView(pcm.buffer, pcm.byteOffset, pcm.byteLength) : new DataView(pcm)
|
|
11
|
+
const count = view.byteLength >> 1
|
|
12
|
+
if (count === 0) return PCM_LEVEL_FLOOR_DBFS
|
|
13
|
+
let sumSquares = 0
|
|
14
|
+
for (let i = 0; i < count; i++) {
|
|
15
|
+
const sample = view.getInt16(i * 2, true)
|
|
16
|
+
sumSquares += sample * sample
|
|
17
|
+
}
|
|
18
|
+
const rms = Math.sqrt(sumSquares / count)
|
|
19
|
+
return Math.max(PCM_LEVEL_FLOOR_DBFS, 20 * Math.log10(rms / 32767))
|
|
20
|
+
}
|