@mearman/doppler 0.0.0 → 1.1.0
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/README.md +98 -0
- package/dist/browser/entry.d.ts +105 -0
- package/dist/browser/entry.js +276 -0
- package/dist/browser/mic-capture.worklet.d.ts +1 -0
- package/dist/browser/mic-capture.worklet.js +35 -0
- package/dist/index-TOQF-n8K.d.ts +1577 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/mic-capture-protocol-CfYodiRN.js +4 -0
- package/dist/node/entry.d.ts +128 -0
- package/dist/node/entry.js +478 -0
- package/dist/src-C4UucgwP.js +2807 -0
- package/dist/time-DWqWWgPL.js +4 -0
- package/dist-cli/doppler.mjs +3085 -0
- package/package.json +104 -2
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Doppler
|
|
2
|
+
|
|
3
|
+
Offline and real-time aircraft Doppler audio geolocation: a TypeScript library, a CLI (`doppler`) and an installable PWA (Progressive Web App) that locate where an audio recording was made from the Doppler shifts of aircraft flyovers.
|
|
4
|
+
|
|
5
|
+
Stack: TypeScript (ESM only), Zod, React with Mantine and vanilla-extract (site), tsdown (library and CLI build), esbuild (site build), Vitest, Playwright, semantic-release. pnpm manages packages.
|
|
6
|
+
|
|
7
|
+
## Getting started
|
|
8
|
+
|
|
9
|
+
Prerequisites: Node 22 or later, and pnpm at the version in `packageManager` in `package.json` (Corepack installs it).
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pnpm install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`pnpm-workspace.yaml` is not a real workspace. It holds pnpm settings only:
|
|
16
|
+
|
|
17
|
+
- `saveExact` writes exact versions. Add and upgrade packages with `pnpm add`, never by editing `package.json`.
|
|
18
|
+
- `minimumReleaseAge` rejects any release younger than seven days. An install that fails as "too new" needs an older version, or `minimumReleaseAgeExclude` for a package you own.
|
|
19
|
+
- `allowBuilds` permits the `esbuild` install script only. Every other dependency install script is blocked.
|
|
20
|
+
|
|
21
|
+
No environment variables are required to build, test or run. The scheduled ADS-B workflow reads `HF_TOKEN` from CI secrets.
|
|
22
|
+
|
|
23
|
+
## Build, test, and lint
|
|
24
|
+
|
|
25
|
+
| Task | Command |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| Build library (`dist/`) and CLI (`dist-cli/`) | `pnpm build` |
|
|
28
|
+
| Build the PWA into `site-dist/` | `pnpm site` |
|
|
29
|
+
| Typecheck (five tsconfig projects) | `pnpm typecheck` |
|
|
30
|
+
| Lint, as CI runs it | `pnpm lint:check` |
|
|
31
|
+
| Lint and auto-fix | `pnpm lint` |
|
|
32
|
+
| Unit and integration tests | `pnpm test` |
|
|
33
|
+
| Browser tests (builds first) | `pnpm test:e2e` |
|
|
34
|
+
|
|
35
|
+
Run one test file or one test:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
pnpm vitest run src/physics/geo.unit.test.ts
|
|
39
|
+
pnpm vitest run src/physics/geo.unit.test.ts -t "test name" --coverage.enabled=false
|
|
40
|
+
pnpm exec playwright test -g "test name"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Coverage is on by default in `vitest.config.ts`. Add `--coverage.enabled=false` to skip it. `playwright test` serves `site-dist/` and does not rebuild it, so run `pnpm build && pnpm site` first after a source change.
|
|
44
|
+
|
|
45
|
+
Run the CLI from source or from the build:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
pnpm tsx cli/doppler.ts locate fixtures/colsto-flyover-36m.wav --json
|
|
49
|
+
node dist-cli/doppler.mjs --help
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
CI (`.github/workflows/ci.yml`) runs, in order: typecheck, lint, tests, build, `publint` plus `attw --pack --profile esm-only` (package export checks), site build, Playwright. `pnpm prepublishOnly` runs the same gates locally.
|
|
53
|
+
|
|
54
|
+
## Architecture
|
|
55
|
+
|
|
56
|
+
The repository has four consumers of one core:
|
|
57
|
+
|
|
58
|
+
- **`src/`** is the isomorphic core. It must run in Node, browsers and Web Workers. `tsconfig.json` sets `types: []` with a `WebWorker` lib, so a Node-only global in `src/` fails to compile there. It excludes `src/node/`, `src/browser/` and `*.test.ts`, which other tsconfig projects check.
|
|
59
|
+
- **`src/node/`** and **`src/browser/`** are the platform edges (file system, ADS-B streaming, microphone capture, `localStorage` session storage). Each has an `entry.ts` that re-exports `src/index.ts` plus its own surface. These are the package exports `@mearman/doppler`, `@mearman/doppler/node` and `@mearman/doppler/browser`.
|
|
60
|
+
- **`cli/doppler.ts`** is the CLI. It imports from `../src` directly and is bundled to `dist-cli/doppler.mjs`, the package `bin`.
|
|
61
|
+
- **`site/`** is the PWA. `site/build.ts` bundles `site/client/app.tsx`, the solver Web Worker and the service worker with esbuild. `site/build-site.ts` writes `index.html`, the manifest and `precache.json`. `site/precache.ts` appends a SHA-256 digest of the precached files to the manifest version, so the service worker refreshes its cache whenever any precached byte changes.
|
|
62
|
+
|
|
63
|
+
Data flows through four layers inside `src/`, each importing only from the layers above it in this list:
|
|
64
|
+
|
|
65
|
+
1. `schemas.ts` (with `define.ts`, `own.ts`, `record.ts`): Zod schemas and the types inferred from them. All data that crosses a boundary (JSON files, worker messages, stored sessions) is parsed with them.
|
|
66
|
+
2. `physics/`: geodesy (`geo.ts`), atmosphere and speed of sound, retarded-time closest-point-of-approach, Ramer-Douglas-Peucker trajectory simplification (`rdp.ts`) and algebra helpers.
|
|
67
|
+
3. `dsp/`: FFT, decimation, spectrogram, ridge tracking that turns a spectrogram into Doppler observations, dechirp, and side channels such as ENF (Electric Network Frequency) detection and WAV encoding.
|
|
68
|
+
4. `solver/`: the constellation index of known flight segments, Gauss-Newton refinement of the observer position, and `DopplerSessionEngine` (`session.ts`), which accumulates observations across listening episodes into a log-posterior over location hypotheses.
|
|
69
|
+
|
|
70
|
+
`bundled.ts` holds a synthetic reference dataset (Oxford, UK) used as the default dataset by the CLI and the site worker. The browser runs the engine inside `site/client/solver-worker.ts`, which the UI drives through typed request and reply messages.
|
|
71
|
+
|
|
72
|
+
## Conventions
|
|
73
|
+
|
|
74
|
+
- **Schema-attached guards.** Define a boundary schema as `export const XSchema = define(z.object({...}))` and export the type under the bare name (`export type X = z.infer<typeof XSchema>`), so the value and the type never share a name. `define` adds `.is()` by defining the property on the schema itself, since a spread copy of a Zod 4 schema loses its prototype methods.
|
|
75
|
+
- **Records keyed by data** use `recordOf` (rejects `__proto__` keys) for parsing and `getOwn` for reading. A plain `record[key]` read finds inherited properties such as `constructor`.
|
|
76
|
+
- **Strict TypeScript.** `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess` are on, so an optional field that you pass through is typed `T | undefined` and an indexed read must be narrowed. Build option objects with conditional spreads, as `solver-worker.ts` does.
|
|
77
|
+
- **File suffixes decide the runner.** `*.unit.test.ts` and `*.integration.test.ts` run in Vitest. `*.browser.ts` under `site/e2e/` runs in Playwright only. Unit tests sit next to the source they test.
|
|
78
|
+
- **Lint comes from `@exadev/eslint-config`.** Overrides live in `eslint.config.ts` with a reason for each. Add a files-scoped override there with a reason, not an inline `eslint-disable`. The magic-number rule is off for numerical code (`physics`, `dsp`, `solver`, tests, `site`, `cli`) and on elsewhere.
|
|
79
|
+
- **Code comments** state the invariant or the reason. Public functions carry a doc comment with `@param` and `@returns`.
|
|
80
|
+
- **Git hooks** are in `.husky/` and are installed by `pnpm install` (the `prepare` script). `pre-commit` lints staged `.ts` and `.tsx` files with `--fix`. `commit-msg` runs commitlint. `pre-push` runs typecheck, lint, tests and build, which is CI without the browser tests. Never bypass them with `--no-verify`: fix the failure.
|
|
81
|
+
- **Commits** follow Conventional Commits. The allowed types and their release levels are the `commitTypes` list in `release.config.ts`, which `commitlint.config.ts` also reads. Every listed type releases at least a patch, `docs`, `test`, `chore` and `ci` included. `feat` releases a minor and a breaking change releases a major.
|
|
82
|
+
- Use British English in prose and comments.
|
|
83
|
+
|
|
84
|
+
## Non-obvious behaviour
|
|
85
|
+
|
|
86
|
+
- **External fixture audio is not in git.** `fixtures/*.wav`, `*.m4a` and `*.mp3` are gitignored (the full recording is about 300 MB). Tests against downloaded fixtures guard with `if (!existsSync(path)) { return; }`, while a built-in synthetic benchmark executes and verifies the complete inversion pipeline in CI and clean checkouts. Put recordings in `fixtures/` for full offline forensics verification.
|
|
87
|
+
- **`pnpm test:e2e` needs a free port 4174** and does not reuse a running server (`reuseExistingServer: false`). Chromium runs with fake audio devices so that the microphone permission prompt does not block.
|
|
88
|
+
- **The CLI bundle does not depend on `dist/`.** `tsdown.config.ts` marks `@mearman/doppler` and `@mearman/doppler/node` external for the CLI, but `cli/doppler.ts` imports `../src` directly, so the two builds are independent.
|
|
89
|
+
- **The CLI version is read from `package.json`** via `import.meta.url`, ensuring CLI output (`doppler -v`) stays synchronized with semantic-release without hardcoded literals.
|
|
90
|
+
- **`incremental-adsb.yml` builds a day's dataset but does not publish it.** It streams the `adsb.lol` `globe_history` release for the target day through `doppler ingest --input -` and writes `YYYY-MM-DD.dataset.json` on the runner. Where the file is stored is not configured yet.
|
|
91
|
+
- **Real ADS-B data enters through `doppler ingest`.** It accepts a raw `adsb.lol` or `tar1090` trace file, a directory of them, ready-made segments, or (`--input -`) the day's tar on standard input, with `--bbox` to keep one region. Without `--dataset`, `locate` and the PWA match against the three synthetic flyovers in `src/bundled.ts` only.
|
|
92
|
+
- **Workflows pin actions differently.** `pages.yml` pins actions by commit SHA. `ci.yml` and `incremental-adsb.yml` use version tags. Keep the pins in the style of the file you edit.
|
|
93
|
+
|
|
94
|
+
## References
|
|
95
|
+
|
|
96
|
+
- `release.config.ts`: release rules and the commit-type list.
|
|
97
|
+
- `eslint.config.ts`: lint overrides and the reason for each.
|
|
98
|
+
- `.github/workflows/`: CI, GitHub Pages deploy and the scheduled ADS-B pipeline.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { $ as computeSegmentCpa, $t as QuietIntervalSchema, A as synthesizeMultiFlyoverAudio, At as ConstellationEntry, B as SpectrogramOptions, Bt as GeoPoint3D, C as formatConstellationKey, Ct as define, D as SyntheticAudioConfig, Dt as AircraftCategorySchema, E as encodeWavPcm16, Et as AircraftCategory, F as extractDopplerObservations, Ft as DopplerObservationSchema, G as downmixToMono, Gt as LinearFlightSegmentSchema, H as computeSingleSpectrogramFrame, Ht as HypothesisMatch, I as ComputedSingleFrame, It as DopplerRidgePoint, J as fft, Jt as LocationHypothesis, K as ComplexSpectrum, Kt as ListeningEpisode, L as DopplerSpectrogram, Lt as DopplerRidgePointSchema, M as DechirpScoreResult, Mt as DopplerDataset, N as scoreByEmissionDechirping, Nt as DopplerDatasetSchema, O as detectEnfSignature, Ot as AsymptoticKinematics, P as RidgeTrackerOptions, Pt as DopplerObservation, Q as SynthesizeCurveOptions, Qt as QuietInterval, R as SingleFrameOptions, Rt as EnfSignature, S as expandConstellationQueryKeys, St as getOwn, T as decodeWavBytes, Tt as AdsbPingSchema, U as decimateAudioStream, Ut as HypothesisMatchSchema, V as computeDopplerSpectrogram, Vt as GeoPoint3DSchema, W as designLowPassFirKernel, Wt as LinearFlightSegment, X as simplifyTrajectory4D, Xt as LocationSession, Y as TraceMetadata, Yt as LocationHypothesisSchema, Z as RetardedAcousticPoint, Zt as LocationSessionSchema, _ as unmatchedObservationLogPenalty, _t as geoToLocalEnu, a as BUNDLED_SEGMENTS, at as DEFAULT_SOUND_SPEED_MPS, b as QUERY_BUCKET_SPAN, bt as slantDistanceMeters, c as DopplerSessionEngine, ct as DEFAULT_ELLIPSE_SEGMENTS, d as ObservationSegmentPair, dt as HALF_TURN_DEG, en as SearchConstraints, et as solveRetardedDelaySec, f as RefineCoordinateOptions, ft as HorizontalEllipse, g as refineObserverCoordinateGaussNewton, gt as geoToEcef, h as fisherToConfidenceEllipse95, ht as encodeSpatialCell, i as BUNDLED_GROUND_TRUTH, it as AtmosphereState, j as DechirpOptions, jt as ConstellationEntrySchema, k as estimateTempFromCricketChirpsCelsius, kt as AsymptoticKinematicsSchema, l as SessionEngineOptions, lt as DEFAULT_SPATIAL_RESOLUTION, m as SLANT_SIGMA_FLOOR_METERS, mt as ellipseOutline, n as BUNDLED_DATASET, nn as Vector3D, nt as DopplerCurveMeasurement, o as generateBundledReferenceAudio, ot as effectiveRaySoundSpeedMps, p as RefinedLocationResult, pt as decodeSpatialCellCenter, q as computeHannPowerSpectrumDb, qt as ListeningEpisodeSchema, r as BUNDLED_DEM_GRID, rn as Vector3DSchema, rt as extractAsymptoticKinematics, s as generateBundledReferenceWavBytes, st as speedOfSoundAtTempMps, t as BUNDLED_CONSTELLATIONS, tn as SearchConstraintsSchema, tt as synthesizeRetardedDopplerCurve, u as MAX_ALONG_TRACK_WIND_MPS, ut as FULL_TURN_DEG, v as BuildConstellationOptions, vt as interpolateTerrainElevationMeters, w as matchesSearchConstraints, wt as AdsbPing, x as buildConstellationIndex, xt as recordOf, y as CandidateConstraintTarget, yt as localEnuToGeo, z as SpectrogramFrame, zt as EnfSignatureSchema } from "../index-TOQF-n8K.js";
|
|
2
|
+
/**
|
|
3
|
+
* Decodes an audio or video file's soundtrack into mono samples. WAV decodes in-process at its own sample rate; every other container goes through the browser's decoder, so the supported formats are the ones the browser plays.
|
|
4
|
+
* @param buffer - The file's bytes. A browser decode detaches this buffer.
|
|
5
|
+
* @returns Mono samples and their sample rate in Hz.
|
|
6
|
+
* @throws When the bytes are not a WAV file and the browser cannot decode them.
|
|
7
|
+
*/
|
|
8
|
+
export declare function decodeAudioFile(buffer: Readonly<ArrayBuffer>): Promise<{
|
|
9
|
+
readonly samples: Float32Array;
|
|
10
|
+
readonly sampleRateHz: number;
|
|
11
|
+
}>;
|
|
12
|
+
/**
|
|
13
|
+
* Event callbacks emitted during continuous or episodic microphone listening.
|
|
14
|
+
*/
|
|
15
|
+
export interface MicMonitorCallbacks {
|
|
16
|
+
readonly onSpectrogramFrame?: (frame: Readonly<ComputedSingleFrame>) => void;
|
|
17
|
+
readonly onNoiseUpdate?: (noiseFloorDb: number, noisePolluted: boolean) => void;
|
|
18
|
+
readonly onObservationDetected?: (observation: Readonly<DopplerObservation>) => void;
|
|
19
|
+
readonly onError?: (error: Readonly<Error>) => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Live audio monitor that captures raw microphone audio without WebRTC voice filters,
|
|
23
|
+
* applies streaming 24x decimation to 2,000 Hz, tracks in-band noise pollution, and extracts
|
|
24
|
+
* aircraft Doppler S-curves in real time.
|
|
25
|
+
* Capture runs in an audio worklet loaded from `mic-capture.worklet.js`, which must be served next to the module that contains this class.
|
|
26
|
+
*/
|
|
27
|
+
export declare class MicMonitor {
|
|
28
|
+
private audioCtx;
|
|
29
|
+
private mediaStream;
|
|
30
|
+
private captureNode;
|
|
31
|
+
private sampleBuffer;
|
|
32
|
+
private isListening;
|
|
33
|
+
private isPaused;
|
|
34
|
+
private episodeId;
|
|
35
|
+
private episodeStartEpochSec;
|
|
36
|
+
private actualSampleRateHz;
|
|
37
|
+
private readonly callbacks;
|
|
38
|
+
constructor(callbacks?: MicMonitorCallbacks);
|
|
39
|
+
/**
|
|
40
|
+
* Starts microphone capture with all browser noise suppression, echo cancellation, and AGC explicitly disabled.
|
|
41
|
+
* @param episodeId - Identifier for the active listening episode.
|
|
42
|
+
*/
|
|
43
|
+
start(episodeId: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Temporarily pauses listening (e.g. user pauses or moves through a high-noise area).
|
|
46
|
+
*/
|
|
47
|
+
pause(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Resumes listening from a paused state.
|
|
50
|
+
*/
|
|
51
|
+
resume(): void;
|
|
52
|
+
/**
|
|
53
|
+
* Stops microphone capture, releases the hardware audio stream, and flushes remaining buffer.
|
|
54
|
+
*/
|
|
55
|
+
stop(): void;
|
|
56
|
+
/**
|
|
57
|
+
* Processes a raw 48 kHz chunk, decimates it to 2,000 Hz, appends it to the rolling window, and checks for Doppler ridges.
|
|
58
|
+
*/
|
|
59
|
+
private handleRawAudioChunk;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Saves a LocationSession snapshot into local browser storage.
|
|
63
|
+
* @param session - The LocationSession to save.
|
|
64
|
+
*/
|
|
65
|
+
export declare function saveSessionToStorage(session: Readonly<LocationSession>): void;
|
|
66
|
+
/**
|
|
67
|
+
* Loads all saved sessions from browser storage.
|
|
68
|
+
* @returns The stored sessions, or an empty array when nothing is stored or the stored value is not a list of valid sessions.
|
|
69
|
+
*/
|
|
70
|
+
export declare function loadSessionsFromStorage(): LocationSession[];
|
|
71
|
+
/**
|
|
72
|
+
* Loads a specific session by ID from browser storage.
|
|
73
|
+
* @param sessionId - Session identifier string.
|
|
74
|
+
* @returns Found LocationSession or undefined.
|
|
75
|
+
*/
|
|
76
|
+
export declare function loadSessionById(sessionId: string): LocationSession | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* Deletes a session from browser storage.
|
|
79
|
+
* @param sessionId - Session identifier string.
|
|
80
|
+
*/
|
|
81
|
+
export declare function deleteSessionFromStorage(sessionId: string): void;
|
|
82
|
+
/**
|
|
83
|
+
* Saves an uploaded or synced DopplerDataset into local browser storage.
|
|
84
|
+
* @param dataset - The DopplerDataset to save.
|
|
85
|
+
*/
|
|
86
|
+
export declare function saveCustomDataset(dataset: Readonly<DopplerDataset>): void;
|
|
87
|
+
/**
|
|
88
|
+
* Loads any custom saved dataset from browser storage.
|
|
89
|
+
* @returns The saved DopplerDataset, or null when nothing is stored or the stored value is not a valid dataset.
|
|
90
|
+
*/
|
|
91
|
+
export declare function loadCustomDataset(): DopplerDataset | null;
|
|
92
|
+
/**
|
|
93
|
+
* Exports a LocationSession as a downloadable JSON string.
|
|
94
|
+
* @param session - LocationSession to export.
|
|
95
|
+
* @returns Pretty-printed JSON string.
|
|
96
|
+
*/
|
|
97
|
+
export declare function exportSessionJson(session: Readonly<LocationSession>): string;
|
|
98
|
+
/**
|
|
99
|
+
* Parses and validates an imported session JSON string.
|
|
100
|
+
* @param jsonText - Raw imported JSON text.
|
|
101
|
+
* @returns Parsed `LocationSession`.
|
|
102
|
+
* @throws When the text is not JSON or not a valid session.
|
|
103
|
+
*/
|
|
104
|
+
export declare function importSessionJson(jsonText: string): LocationSession;
|
|
105
|
+
export { AdsbPing, AdsbPingSchema, AircraftCategory, AircraftCategorySchema, AsymptoticKinematics, AsymptoticKinematicsSchema, AtmosphereState, BUNDLED_CONSTELLATIONS, BUNDLED_DATASET, BUNDLED_DEM_GRID, BUNDLED_GROUND_TRUTH, BUNDLED_SEGMENTS, BuildConstellationOptions, CandidateConstraintTarget, ComplexSpectrum, ComputedSingleFrame, ConstellationEntry, ConstellationEntrySchema, DEFAULT_ELLIPSE_SEGMENTS, DEFAULT_SOUND_SPEED_MPS, DEFAULT_SPATIAL_RESOLUTION, DechirpOptions, DechirpScoreResult, DopplerCurveMeasurement, DopplerDataset, DopplerDatasetSchema, DopplerObservation, DopplerObservationSchema, DopplerRidgePoint, DopplerRidgePointSchema, DopplerSessionEngine, DopplerSpectrogram, EnfSignature, EnfSignatureSchema, FULL_TURN_DEG, GeoPoint3D, GeoPoint3DSchema, HALF_TURN_DEG, HorizontalEllipse, HypothesisMatch, HypothesisMatchSchema, LinearFlightSegment, LinearFlightSegmentSchema, ListeningEpisode, ListeningEpisodeSchema, LocationHypothesis, LocationHypothesisSchema, LocationSession, LocationSessionSchema, MAX_ALONG_TRACK_WIND_MPS, ObservationSegmentPair, QUERY_BUCKET_SPAN, QuietInterval, QuietIntervalSchema, RefineCoordinateOptions, RefinedLocationResult, RetardedAcousticPoint, RidgeTrackerOptions, SLANT_SIGMA_FLOOR_METERS, SearchConstraints, SearchConstraintsSchema, SessionEngineOptions, SingleFrameOptions, SpectrogramFrame, SpectrogramOptions, SynthesizeCurveOptions, SyntheticAudioConfig, TraceMetadata, Vector3D, Vector3DSchema, buildConstellationIndex, computeDopplerSpectrogram, computeHannPowerSpectrumDb, computeSegmentCpa, computeSingleSpectrogramFrame, decimateAudioStream, decodeSpatialCellCenter, decodeWavBytes, define, designLowPassFirKernel, detectEnfSignature, downmixToMono, effectiveRaySoundSpeedMps, ellipseOutline, encodeSpatialCell, encodeWavPcm16, estimateTempFromCricketChirpsCelsius, expandConstellationQueryKeys, extractAsymptoticKinematics, extractDopplerObservations, fft, fisherToConfidenceEllipse95, formatConstellationKey, generateBundledReferenceAudio, generateBundledReferenceWavBytes, geoToEcef, geoToLocalEnu, getOwn, interpolateTerrainElevationMeters, localEnuToGeo, matchesSearchConstraints, recordOf, refineObserverCoordinateGaussNewton, scoreByEmissionDechirping, simplifyTrajectory4D, slantDistanceMeters, solveRetardedDelaySec, speedOfSoundAtTempMps, synthesizeMultiFlyoverAudio, synthesizeRetardedDopplerCurve, unmatchedObservationLogPenalty };
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { $ as recordOf, A as downmixToMono, B as speedOfSoundAtTempMps, C as synthesizeMultiFlyoverAudio, D as computeSingleSpectrogramFrame, E as computeDopplerSpectrogram, F as solveRetardedDelaySec, G as decodeSpatialCellCenter, H as DEFAULT_SPATIAL_RESOLUTION, I as synthesizeRetardedDopplerCurve, J as geoToEcef, K as ellipseOutline, L as extractAsymptoticKinematics, M as fft, N as simplifyTrajectory4D, O as decimateAudioStream, P as computeSegmentCpa, Q as slantDistanceMeters, R as DEFAULT_SOUND_SPEED_MPS, S as estimateTempFromCricketChirpsCelsius, T as extractDopplerObservations, U as FULL_TURN_DEG, V as DEFAULT_ELLIPSE_SEGMENTS, W as HALF_TURN_DEG, X as interpolateTerrainElevationMeters, Y as geoToLocalEnu, Z as localEnuToGeo, _ as formatConstellationKey, _t as Vector3DSchema, a as BUNDLED_SEGMENTS, at as DopplerDatasetSchema, b as encodeWavPcm16, c as DopplerSessionEngine, ct as EnfSignatureSchema, d as fisherToConfidenceEllipse95, dt as LinearFlightSegmentSchema, et as getOwn, f as refineObserverCoordinateGaussNewton, ft as ListeningEpisodeSchema, g as expandConstellationQueryKeys, gt as SearchConstraintsSchema, h as buildConstellationIndex, ht as QuietIntervalSchema, i as BUNDLED_GROUND_TRUTH, it as ConstellationEntrySchema, j as computeHannPowerSpectrumDb, k as designLowPassFirKernel, l as MAX_ALONG_TRACK_WIND_MPS, lt as GeoPoint3DSchema, m as QUERY_BUCKET_SPAN, mt as LocationSessionSchema, n as BUNDLED_DATASET, nt as AircraftCategorySchema, o as generateBundledReferenceAudio, ot as DopplerObservationSchema, p as unmatchedObservationLogPenalty, pt as LocationHypothesisSchema, q as encodeSpatialCell, r as BUNDLED_DEM_GRID, rt as AsymptoticKinematicsSchema, s as generateBundledReferenceWavBytes, st as DopplerRidgePointSchema, t as BUNDLED_CONSTELLATIONS, tt as AdsbPingSchema, u as SLANT_SIGMA_FLOOR_METERS, ut as HypothesisMatchSchema, v as matchesSearchConstraints, vt as define, w as scoreByEmissionDechirping, x as detectEnfSignature, y as decodeWavBytes, z as effectiveRaySoundSpeedMps } from "../src-C4UucgwP.js";
|
|
2
|
+
import { t as MS_PER_SECOND } from "../time-DWqWWgPL.js";
|
|
3
|
+
import { t as MIC_CAPTURE_PROCESSOR_NAME } from "../mic-capture-protocol-CfYodiRN.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
// src/browser/decode-audio.ts
|
|
6
|
+
/** Rate the browser decodes compressed audio at. Analysis decimates to a far lower rate, so the exact value only has to cover the aircraft band. */
|
|
7
|
+
const COMPRESSED_DECODE_RATE_HZ = 44100;
|
|
8
|
+
/** Bytes of a RIFF four-character code, such as `RIFF` or `WAVE`. */
|
|
9
|
+
const FOUR_CC_BYTES = 4;
|
|
10
|
+
/** Bytes of the RIFF header's tag plus size field that precede the WAVE form type. */
|
|
11
|
+
const RIFF_FORM_OFFSET = 8;
|
|
12
|
+
function isWav(bytes) {
|
|
13
|
+
const tag = (offset) => String.fromCharCode(...bytes.subarray(offset, offset + FOUR_CC_BYTES));
|
|
14
|
+
return bytes.length >= 12 && tag(0) === "RIFF" && tag(RIFF_FORM_OFFSET) === "WAVE";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Decodes an audio or video file's soundtrack into mono samples. WAV decodes in-process at its own sample rate; every other container goes through the browser's decoder, so the supported formats are the ones the browser plays.
|
|
18
|
+
* @param buffer - The file's bytes. A browser decode detaches this buffer.
|
|
19
|
+
* @returns Mono samples and their sample rate in Hz.
|
|
20
|
+
* @throws When the bytes are not a WAV file and the browser cannot decode them.
|
|
21
|
+
*/
|
|
22
|
+
async function decodeAudioFile(buffer) {
|
|
23
|
+
if (isWav(new Uint8Array(buffer))) return decodeWavBytes(new Uint8Array(buffer));
|
|
24
|
+
const decoded = await new OfflineAudioContext(1, 1, COMPRESSED_DECODE_RATE_HZ).decodeAudioData(buffer);
|
|
25
|
+
const channels = Array.from({ length: decoded.numberOfChannels }, (_, channel) => decoded.getChannelData(channel));
|
|
26
|
+
return {
|
|
27
|
+
samples: downmixToMono(channels),
|
|
28
|
+
sampleRateHz: decoded.sampleRate
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
// src/browser/mic-monitor.ts
|
|
32
|
+
/** Sample rate the captured audio is decimated to before analysis. The aircraft Doppler band lies far below its Nyquist frequency of 1,000 Hz. */
|
|
33
|
+
const ANALYSIS_RATE_HZ = 2e3;
|
|
34
|
+
/** Capture rate assumed until the `AudioContext` reports the real one, in Hz. */
|
|
35
|
+
const DEFAULT_CAPTURE_RATE_HZ = 48e3;
|
|
36
|
+
/** Samples of the rolling window at the analysis rate. */
|
|
37
|
+
const MAX_WINDOW_SAMPLES = 60 * ANALYSIS_RATE_HZ;
|
|
38
|
+
/** FFT length of both the live waterfall frame and the ridge-tracking spectrogram: a power of two giving about 1 Hz of resolution at the analysis rate. */
|
|
39
|
+
const FFT_SIZE = 2048;
|
|
40
|
+
/** Passband of the live waterfall frame, in Hz. */
|
|
41
|
+
const LIVE_MIN_FREQ_HZ = 50;
|
|
42
|
+
const LIVE_MAX_FREQ_HZ = 500;
|
|
43
|
+
/** Analysis window that must be buffered before ridges are extracted: 4 s of audio at the analysis rate. */
|
|
44
|
+
const RIDGE_MIN_WINDOW_SAMPLES = 8e3;
|
|
45
|
+
/** Hop of the ridge-tracking spectrogram, in samples at the analysis rate. */
|
|
46
|
+
const SPECTROGRAM_HOP_SAMPLES = 500;
|
|
47
|
+
/** Smallest signal-to-noise ratio, in dB, at which a spectrogram ridge counts as an observation. */
|
|
48
|
+
const MIN_RIDGE_SNR_DB = 6;
|
|
49
|
+
/** Location of the audio worklet module, which the site build emits next to the bundle that contains this code. */
|
|
50
|
+
const WORKLET_MODULE_URL = new URL("./mic-capture.worklet.js", import.meta.url);
|
|
51
|
+
/**
|
|
52
|
+
* Live audio monitor that captures raw microphone audio without WebRTC voice filters,
|
|
53
|
+
* applies streaming 24x decimation to 2,000 Hz, tracks in-band noise pollution, and extracts
|
|
54
|
+
* aircraft Doppler S-curves in real time.
|
|
55
|
+
* Capture runs in an audio worklet loaded from `mic-capture.worklet.js`, which must be served next to the module that contains this class.
|
|
56
|
+
*/
|
|
57
|
+
var MicMonitor = class {
|
|
58
|
+
audioCtx = null;
|
|
59
|
+
mediaStream = null;
|
|
60
|
+
captureNode = null;
|
|
61
|
+
sampleBuffer = /* @__PURE__ */ new Float32Array(0);
|
|
62
|
+
isListening = false;
|
|
63
|
+
isPaused = false;
|
|
64
|
+
episodeId = "";
|
|
65
|
+
episodeStartEpochSec = 0;
|
|
66
|
+
actualSampleRateHz = DEFAULT_CAPTURE_RATE_HZ;
|
|
67
|
+
callbacks;
|
|
68
|
+
constructor(callbacks = {}) {
|
|
69
|
+
this.callbacks = callbacks;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Starts microphone capture with all browser noise suppression, echo cancellation, and AGC explicitly disabled.
|
|
73
|
+
* @param episodeId - Identifier for the active listening episode.
|
|
74
|
+
*/
|
|
75
|
+
async start(episodeId) {
|
|
76
|
+
if (this.isListening) return;
|
|
77
|
+
this.episodeId = episodeId;
|
|
78
|
+
this.episodeStartEpochSec = Math.floor(Date.now() / MS_PER_SECOND);
|
|
79
|
+
this.isListening = true;
|
|
80
|
+
this.isPaused = false;
|
|
81
|
+
this.sampleBuffer = /* @__PURE__ */ new Float32Array(0);
|
|
82
|
+
try {
|
|
83
|
+
this.mediaStream = await navigator.mediaDevices.getUserMedia({ audio: {
|
|
84
|
+
echoCancellation: false,
|
|
85
|
+
noiseSuppression: false,
|
|
86
|
+
autoGainControl: false
|
|
87
|
+
} });
|
|
88
|
+
this.audioCtx = new AudioContext();
|
|
89
|
+
this.actualSampleRateHz = this.audioCtx.sampleRate;
|
|
90
|
+
const source = this.audioCtx.createMediaStreamSource(this.mediaStream);
|
|
91
|
+
await this.audioCtx.audioWorklet.addModule(WORKLET_MODULE_URL);
|
|
92
|
+
this.captureNode = new AudioWorkletNode(this.audioCtx, MIC_CAPTURE_PROCESSOR_NAME, {
|
|
93
|
+
numberOfInputs: 1,
|
|
94
|
+
numberOfOutputs: 1,
|
|
95
|
+
outputChannelCount: [1],
|
|
96
|
+
channelCount: 1,
|
|
97
|
+
channelCountMode: "explicit"
|
|
98
|
+
});
|
|
99
|
+
this.captureNode.port.onmessage = (event) => {
|
|
100
|
+
if (!this.isListening || this.isPaused || !(event.data instanceof Float32Array)) return;
|
|
101
|
+
this.handleRawAudioChunk(event.data);
|
|
102
|
+
};
|
|
103
|
+
source.connect(this.captureNode);
|
|
104
|
+
this.captureNode.connect(this.audioCtx.destination);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
this.stop();
|
|
107
|
+
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
108
|
+
this.callbacks.onError?.(errorObj);
|
|
109
|
+
throw errorObj;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Temporarily pauses listening (e.g. user pauses or moves through a high-noise area).
|
|
114
|
+
*/
|
|
115
|
+
pause() {
|
|
116
|
+
this.isPaused = true;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Resumes listening from a paused state.
|
|
120
|
+
*/
|
|
121
|
+
resume() {
|
|
122
|
+
this.isPaused = false;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Stops microphone capture, releases the hardware audio stream, and flushes remaining buffer.
|
|
126
|
+
*/
|
|
127
|
+
stop() {
|
|
128
|
+
this.isListening = false;
|
|
129
|
+
this.isPaused = false;
|
|
130
|
+
if (this.captureNode !== null) {
|
|
131
|
+
this.captureNode.port.onmessage = null;
|
|
132
|
+
this.captureNode.disconnect();
|
|
133
|
+
this.captureNode = null;
|
|
134
|
+
}
|
|
135
|
+
if (this.mediaStream !== null) {
|
|
136
|
+
for (const track of this.mediaStream.getTracks()) track.stop();
|
|
137
|
+
this.mediaStream = null;
|
|
138
|
+
}
|
|
139
|
+
if (this.audioCtx !== null) {
|
|
140
|
+
this.audioCtx.close();
|
|
141
|
+
this.audioCtx = null;
|
|
142
|
+
}
|
|
143
|
+
this.sampleBuffer = /* @__PURE__ */ new Float32Array(0);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Processes a raw 48 kHz chunk, decimates it to 2,000 Hz, appends it to the rolling window, and checks for Doppler ridges.
|
|
147
|
+
*/
|
|
148
|
+
handleRawAudioChunk(rawChunk) {
|
|
149
|
+
const decimated = decimateAudioStream(rawChunk, this.actualSampleRateHz, ANALYSIS_RATE_HZ);
|
|
150
|
+
const newBuf = new Float32Array(this.sampleBuffer.length + decimated.samples.length);
|
|
151
|
+
newBuf.set(this.sampleBuffer, 0);
|
|
152
|
+
newBuf.set(decimated.samples, this.sampleBuffer.length);
|
|
153
|
+
this.sampleBuffer = newBuf;
|
|
154
|
+
if (this.sampleBuffer.length > MAX_WINDOW_SAMPLES) this.sampleBuffer = this.sampleBuffer.slice(this.sampleBuffer.length - MAX_WINDOW_SAMPLES);
|
|
155
|
+
if (this.sampleBuffer.length >= FFT_SIZE) {
|
|
156
|
+
const windowSlice = this.sampleBuffer.subarray(this.sampleBuffer.length - FFT_SIZE);
|
|
157
|
+
const frameTimeSec = this.episodeStartEpochSec + this.sampleBuffer.length / ANALYSIS_RATE_HZ;
|
|
158
|
+
const liveFrame = computeSingleSpectrogramFrame(windowSlice, {
|
|
159
|
+
sampleRateHz: ANALYSIS_RATE_HZ,
|
|
160
|
+
fftSize: FFT_SIZE,
|
|
161
|
+
minFreqHz: LIVE_MIN_FREQ_HZ,
|
|
162
|
+
maxFreqHz: LIVE_MAX_FREQ_HZ,
|
|
163
|
+
timeSec: frameTimeSec
|
|
164
|
+
});
|
|
165
|
+
this.callbacks.onSpectrogramFrame?.(liveFrame);
|
|
166
|
+
this.callbacks.onNoiseUpdate?.(liveFrame.medianNoiseFloorDb, liveFrame.noisePolluted);
|
|
167
|
+
}
|
|
168
|
+
if (this.sampleBuffer.length >= RIDGE_MIN_WINDOW_SAMPLES) {
|
|
169
|
+
const spectrogram = computeDopplerSpectrogram(this.sampleBuffer, {
|
|
170
|
+
sampleRateHz: ANALYSIS_RATE_HZ,
|
|
171
|
+
startTimeSec: this.episodeStartEpochSec,
|
|
172
|
+
fftSize: FFT_SIZE,
|
|
173
|
+
hopSamples: SPECTROGRAM_HOP_SAMPLES
|
|
174
|
+
});
|
|
175
|
+
const observations = extractDopplerObservations(spectrogram, {
|
|
176
|
+
episodeId: this.episodeId,
|
|
177
|
+
minSnrDb: MIN_RIDGE_SNR_DB
|
|
178
|
+
});
|
|
179
|
+
for (const obs of observations) this.callbacks.onObservationDetected?.(obs);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
// src/browser/storage.ts
|
|
184
|
+
const SESSIONS_KEY = "doppler_sessions_v1";
|
|
185
|
+
const DATASET_KEY = "doppler_custom_dataset_v1";
|
|
186
|
+
/**
|
|
187
|
+
* Saves a LocationSession snapshot into local browser storage.
|
|
188
|
+
* @param session - The LocationSession to save.
|
|
189
|
+
*/
|
|
190
|
+
function saveSessionToStorage(session) {
|
|
191
|
+
try {
|
|
192
|
+
const list = loadSessionsFromStorage().filter((s) => s.id !== session.id);
|
|
193
|
+
list.unshift(session);
|
|
194
|
+
localStorage.setItem(SESSIONS_KEY, JSON.stringify(list));
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.warn("Failed to save session to localStorage:", err);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Loads all saved sessions from browser storage.
|
|
201
|
+
* @returns The stored sessions, or an empty array when nothing is stored or the stored value is not a list of valid sessions.
|
|
202
|
+
*/
|
|
203
|
+
function loadSessionsFromStorage() {
|
|
204
|
+
try {
|
|
205
|
+
const raw = localStorage.getItem(SESSIONS_KEY);
|
|
206
|
+
if (raw === null) return [];
|
|
207
|
+
const parsed = JSON.parse(raw);
|
|
208
|
+
return z.array(LocationSessionSchema).parse(parsed);
|
|
209
|
+
} catch {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Loads a specific session by ID from browser storage.
|
|
215
|
+
* @param sessionId - Session identifier string.
|
|
216
|
+
* @returns Found LocationSession or undefined.
|
|
217
|
+
*/
|
|
218
|
+
function loadSessionById(sessionId) {
|
|
219
|
+
return loadSessionsFromStorage().find((s) => s.id === sessionId);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Deletes a session from browser storage.
|
|
223
|
+
* @param sessionId - Session identifier string.
|
|
224
|
+
*/
|
|
225
|
+
function deleteSessionFromStorage(sessionId) {
|
|
226
|
+
try {
|
|
227
|
+
const list = loadSessionsFromStorage().filter((s) => s.id !== sessionId);
|
|
228
|
+
localStorage.setItem(SESSIONS_KEY, JSON.stringify(list));
|
|
229
|
+
} catch (err) {
|
|
230
|
+
console.warn("Failed to delete session from localStorage:", err);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Saves an uploaded or synced DopplerDataset into local browser storage.
|
|
235
|
+
* @param dataset - The DopplerDataset to save.
|
|
236
|
+
*/
|
|
237
|
+
function saveCustomDataset(dataset) {
|
|
238
|
+
try {
|
|
239
|
+
localStorage.setItem(DATASET_KEY, JSON.stringify(dataset));
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.warn("Failed to save custom dataset to localStorage:", err);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Loads any custom saved dataset from browser storage.
|
|
246
|
+
* @returns The saved DopplerDataset, or null when nothing is stored or the stored value is not a valid dataset.
|
|
247
|
+
*/
|
|
248
|
+
function loadCustomDataset() {
|
|
249
|
+
try {
|
|
250
|
+
const raw = localStorage.getItem(DATASET_KEY);
|
|
251
|
+
if (raw === null) return null;
|
|
252
|
+
const parsed = JSON.parse(raw);
|
|
253
|
+
return DopplerDatasetSchema.parse(parsed);
|
|
254
|
+
} catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Exports a LocationSession as a downloadable JSON string.
|
|
260
|
+
* @param session - LocationSession to export.
|
|
261
|
+
* @returns Pretty-printed JSON string.
|
|
262
|
+
*/
|
|
263
|
+
function exportSessionJson(session) {
|
|
264
|
+
return JSON.stringify(session, null, 2);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Parses and validates an imported session JSON string.
|
|
268
|
+
* @param jsonText - Raw imported JSON text.
|
|
269
|
+
* @returns Parsed `LocationSession`.
|
|
270
|
+
* @throws When the text is not JSON or not a valid session.
|
|
271
|
+
*/
|
|
272
|
+
function importSessionJson(jsonText) {
|
|
273
|
+
const parsed = JSON.parse(jsonText);
|
|
274
|
+
return LocationSessionSchema.parse(parsed);
|
|
275
|
+
}
|
|
276
|
+
export { AdsbPingSchema, AircraftCategorySchema, AsymptoticKinematicsSchema, BUNDLED_CONSTELLATIONS, BUNDLED_DATASET, BUNDLED_DEM_GRID, BUNDLED_GROUND_TRUTH, BUNDLED_SEGMENTS, ConstellationEntrySchema, DEFAULT_ELLIPSE_SEGMENTS, DEFAULT_SOUND_SPEED_MPS, DEFAULT_SPATIAL_RESOLUTION, DopplerDatasetSchema, DopplerObservationSchema, DopplerRidgePointSchema, DopplerSessionEngine, EnfSignatureSchema, FULL_TURN_DEG, GeoPoint3DSchema, HALF_TURN_DEG, HypothesisMatchSchema, LinearFlightSegmentSchema, ListeningEpisodeSchema, LocationHypothesisSchema, LocationSessionSchema, MAX_ALONG_TRACK_WIND_MPS, MicMonitor, QUERY_BUCKET_SPAN, QuietIntervalSchema, SLANT_SIGMA_FLOOR_METERS, SearchConstraintsSchema, Vector3DSchema, buildConstellationIndex, computeDopplerSpectrogram, computeHannPowerSpectrumDb, computeSegmentCpa, computeSingleSpectrogramFrame, decimateAudioStream, decodeAudioFile, decodeSpatialCellCenter, decodeWavBytes, define, deleteSessionFromStorage, designLowPassFirKernel, detectEnfSignature, downmixToMono, effectiveRaySoundSpeedMps, ellipseOutline, encodeSpatialCell, encodeWavPcm16, estimateTempFromCricketChirpsCelsius, expandConstellationQueryKeys, exportSessionJson, extractAsymptoticKinematics, extractDopplerObservations, fft, fisherToConfidenceEllipse95, formatConstellationKey, generateBundledReferenceAudio, generateBundledReferenceWavBytes, geoToEcef, geoToLocalEnu, getOwn, importSessionJson, interpolateTerrainElevationMeters, loadCustomDataset, loadSessionById, loadSessionsFromStorage, localEnuToGeo, matchesSearchConstraints, recordOf, refineObserverCoordinateGaussNewton, saveCustomDataset, saveSessionToStorage, scoreByEmissionDechirping, simplifyTrajectory4D, slantDistanceMeters, solveRetardedDelaySec, speedOfSoundAtTempMps, synthesizeMultiFlyoverAudio, synthesizeRetardedDopplerCurve, unmatchedObservationLogPenalty };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { t as MIC_CAPTURE_PROCESSOR_NAME } from "../mic-capture-protocol-CfYodiRN.js";
|
|
2
|
+
// src/browser/mic-capture.worklet.ts
|
|
3
|
+
/**
|
|
4
|
+
* Audio worklet module that batches the microphone's 128-frame render quanta into fixed-size chunks and posts each chunk to the main thread, which receives it as a `Float32Array` message.
|
|
5
|
+
* It runs in the `AudioWorkletGlobalScope`, which the DOM library does not describe, so the two globals it uses are declared here. Nothing is exported: the module's only effect is registering the processor, and the main thread loads it with `audioWorklet.addModule`.
|
|
6
|
+
*/
|
|
7
|
+
/** Samples per posted chunk: about 85 ms at 48 kHz, a multiple of the 128-frame render quantum. */
|
|
8
|
+
const CAPTURE_CHUNK_FRAMES = 4096;
|
|
9
|
+
var MicCaptureProcessor = class extends AudioWorkletProcessor {
|
|
10
|
+
chunk = new Float32Array(CAPTURE_CHUNK_FRAMES);
|
|
11
|
+
filled = 0;
|
|
12
|
+
/**
|
|
13
|
+
* Appends one render quantum of the first input's first channel to the current chunk and posts the chunk, transferring its buffer, whenever it is full.
|
|
14
|
+
* @param inputs - The node's inputs; empty while nothing is connected.
|
|
15
|
+
* @returns True, so the processor stays alive for as long as the node does.
|
|
16
|
+
*/
|
|
17
|
+
process(inputs) {
|
|
18
|
+
const channel = inputs[0]?.[0];
|
|
19
|
+
if (channel === void 0) return true;
|
|
20
|
+
let offset = 0;
|
|
21
|
+
while (offset < channel.length) {
|
|
22
|
+
const count = Math.min(channel.length - offset, this.chunk.length - this.filled);
|
|
23
|
+
this.chunk.set(channel.subarray(offset, offset + count), this.filled);
|
|
24
|
+
this.filled += count;
|
|
25
|
+
offset += count;
|
|
26
|
+
if (this.filled === this.chunk.length) {
|
|
27
|
+
this.port.postMessage(this.chunk, [this.chunk.buffer]);
|
|
28
|
+
this.chunk = new Float32Array(CAPTURE_CHUNK_FRAMES);
|
|
29
|
+
this.filled = 0;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
registerProcessor(MIC_CAPTURE_PROCESSOR_NAME, MicCaptureProcessor);
|