tempest-react-sdk 0.52.0 → 0.54.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 +58 -7
- package/dist/access/Can.cjs +1 -1
- package/dist/access/Can.cjs.map +1 -1
- package/dist/access/Can.js +3 -3
- package/dist/access/Can.js.map +1 -1
- package/dist/audio/audio-bus.cjs +2 -0
- package/dist/audio/audio-bus.cjs.map +1 -0
- package/dist/audio/audio-bus.js +116 -0
- package/dist/audio/audio-bus.js.map +1 -0
- package/dist/audio/level-meter.cjs +1 -1
- package/dist/audio/level-meter.cjs.map +1 -1
- package/dist/audio/level-meter.js +15 -15
- package/dist/audio/level-meter.js.map +1 -1
- package/dist/audio/noise-floor.cjs +2 -0
- package/dist/audio/noise-floor.cjs.map +1 -0
- package/dist/audio/noise-floor.js +27 -0
- package/dist/audio/noise-floor.js.map +1 -0
- package/dist/audio/use-audio-bus.cjs +2 -0
- package/dist/audio/use-audio-bus.cjs.map +1 -0
- package/dist/audio/use-audio-bus.js +11 -0
- package/dist/audio/use-audio-bus.js.map +1 -0
- package/dist/audio/voice-activity.cjs +2 -0
- package/dist/audio/voice-activity.cjs.map +1 -0
- package/dist/audio/voice-activity.js +26 -0
- package/dist/audio/voice-activity.js.map +1 -0
- package/dist/audio/voice-chain-stages.cjs +2 -0
- package/dist/audio/voice-chain-stages.cjs.map +1 -0
- package/dist/audio/voice-chain-stages.js +82 -0
- package/dist/audio/voice-chain-stages.js.map +1 -0
- package/dist/audio/voice-chain.cjs +2 -0
- package/dist/audio/voice-chain.cjs.map +1 -0
- package/dist/audio/voice-chain.js +74 -0
- package/dist/audio/voice-chain.js.map +1 -0
- package/dist/auth/lazy-with-retry.cjs.map +1 -1
- package/dist/auth/lazy-with-retry.js.map +1 -1
- package/dist/auth/passkey.cjs.map +1 -1
- package/dist/auth/passkey.js.map +1 -1
- package/dist/br/BrazilStateCitySelect.cjs +1 -1
- package/dist/br/BrazilStateCitySelect.cjs.map +1 -1
- package/dist/br/BrazilStateCitySelect.js +36 -28
- package/dist/br/BrazilStateCitySelect.js.map +1 -1
- package/dist/br/data/br-centroids.cjs +1 -1
- package/dist/br/data/br-centroids.cjs.map +1 -1
- package/dist/br/data/br-centroids.js +11105 -11045
- package/dist/br/data/br-centroids.js.map +1 -1
- package/dist/br/data/br-locations.cjs +1 -1
- package/dist/br/data/br-locations.cjs.map +1 -1
- package/dist/br/data/br-locations.js +5803 -137
- package/dist/br/data/br-locations.js.map +1 -1
- package/dist/br/data/br-uf-geo.cjs +1 -1
- package/dist/br/data/br-uf-geo.cjs.map +1 -1
- package/dist/br/data/br-uf-geo.js +5721 -6777
- package/dist/br/data/br-uf-geo.js.map +1 -1
- package/dist/br/data/mun/AC.cjs +1 -1
- package/dist/br/data/mun/AC.cjs.map +1 -1
- package/dist/br/data/mun/AC.js +681 -689
- package/dist/br/data/mun/AC.js.map +1 -1
- package/dist/br/data/mun/AL.cjs +1 -1
- package/dist/br/data/mun/AL.cjs.map +1 -1
- package/dist/br/data/mun/AL.js +949 -844
- package/dist/br/data/mun/AL.js.map +1 -1
- package/dist/br/data/mun/AM.cjs +1 -1
- package/dist/br/data/mun/AM.cjs.map +1 -1
- package/dist/br/data/mun/AM.js +4337 -4218
- package/dist/br/data/mun/AM.js.map +1 -1
- package/dist/br/data/mun/AP.cjs +1 -1
- package/dist/br/data/mun/AP.cjs.map +1 -1
- package/dist/br/data/mun/AP.js +786 -780
- package/dist/br/data/mun/AP.js.map +1 -1
- package/dist/br/data/mun/BA.cjs +1 -1
- package/dist/br/data/mun/BA.cjs.map +1 -1
- package/dist/br/data/mun/BA.js +6329 -5633
- package/dist/br/data/mun/BA.js.map +1 -1
- package/dist/br/data/mun/CE.cjs +1 -1
- package/dist/br/data/mun/CE.cjs.map +1 -1
- package/dist/br/data/mun/CE.js +2544 -2487
- package/dist/br/data/mun/CE.js.map +1 -1
- package/dist/br/data/mun/DF.cjs +1 -1
- package/dist/br/data/mun/DF.cjs.map +1 -1
- package/dist/br/data/mun/DF.js +17 -16
- package/dist/br/data/mun/DF.js.map +1 -1
- package/dist/br/data/mun/ES.cjs +1 -1
- package/dist/br/data/mun/ES.cjs.map +1 -1
- package/dist/br/data/mun/ES.js +1101 -1070
- package/dist/br/data/mun/ES.js.map +1 -1
- package/dist/br/data/mun/GO.cjs +1 -1
- package/dist/br/data/mun/GO.cjs.map +1 -1
- package/dist/br/data/mun/GO.js +4632 -4636
- package/dist/br/data/mun/GO.js.map +1 -1
- package/dist/br/data/mun/MA.cjs +1 -1
- package/dist/br/data/mun/MA.cjs.map +1 -1
- package/dist/br/data/mun/MA.js +3603 -3457
- package/dist/br/data/mun/MA.js.map +1 -1
- package/dist/br/data/mun/MG.cjs +1 -1
- package/dist/br/data/mun/MG.cjs.map +1 -1
- package/dist/br/data/mun/MG.js +12716 -11739
- package/dist/br/data/mun/MG.js.map +1 -1
- package/dist/br/data/mun/MS.cjs +1 -1
- package/dist/br/data/mun/MS.cjs.map +1 -1
- package/dist/br/data/mun/MS.js +2182 -2335
- package/dist/br/data/mun/MS.js.map +1 -1
- package/dist/br/data/mun/MT.cjs +1 -1
- package/dist/br/data/mun/MT.cjs.map +1 -1
- package/dist/br/data/mun/MT.js +4808 -4896
- package/dist/br/data/mun/MT.js.map +1 -1
- package/dist/br/data/mun/PA.cjs +1 -1
- package/dist/br/data/mun/PA.cjs.map +1 -1
- package/dist/br/data/mun/PA.js +4981 -4901
- package/dist/br/data/mun/PA.js.map +1 -1
- package/dist/br/data/mun/PB.cjs +1 -1
- package/dist/br/data/mun/PB.cjs.map +1 -1
- package/dist/br/data/mun/PB.js +2418 -1956
- package/dist/br/data/mun/PB.js.map +1 -1
- package/dist/br/data/mun/PE.cjs +1 -1
- package/dist/br/data/mun/PE.cjs.map +1 -1
- package/dist/br/data/mun/PE.js +2164 -1987
- package/dist/br/data/mun/PE.js.map +1 -1
- package/dist/br/data/mun/PI.cjs +1 -1
- package/dist/br/data/mun/PI.cjs.map +1 -1
- package/dist/br/data/mun/PI.js +2982 -2844
- package/dist/br/data/mun/PI.js.map +1 -1
- package/dist/br/data/mun/PR.cjs +1 -1
- package/dist/br/data/mun/PR.cjs.map +1 -1
- package/dist/br/data/mun/PR.js +5425 -5240
- package/dist/br/data/mun/PR.js.map +1 -1
- package/dist/br/data/mun/RJ.cjs +1 -1
- package/dist/br/data/mun/RJ.cjs.map +1 -1
- package/dist/br/data/mun/RJ.js +1395 -1178
- package/dist/br/data/mun/RJ.js.map +1 -1
- package/dist/br/data/mun/RN.cjs +1 -1
- package/dist/br/data/mun/RN.cjs.map +1 -1
- package/dist/br/data/mun/RN.js +1608 -1382
- package/dist/br/data/mun/RN.js.map +1 -1
- package/dist/br/data/mun/RO.cjs +1 -1
- package/dist/br/data/mun/RO.cjs.map +1 -1
- package/dist/br/data/mun/RO.js +1392 -1470
- package/dist/br/data/mun/RO.js.map +1 -1
- package/dist/br/data/mun/RR.cjs +1 -1
- package/dist/br/data/mun/RR.cjs.map +1 -1
- package/dist/br/data/mun/RR.js +795 -778
- package/dist/br/data/mun/RR.js.map +1 -1
- package/dist/br/data/mun/RS.cjs +1 -1
- package/dist/br/data/mun/RS.cjs.map +1 -1
- package/dist/br/data/mun/RS.js +7579 -6424
- package/dist/br/data/mun/RS.js.map +1 -1
- package/dist/br/data/mun/SC.cjs +1 -1
- package/dist/br/data/mun/SC.cjs.map +1 -1
- package/dist/br/data/mun/SC.js +3979 -3357
- package/dist/br/data/mun/SC.js.map +1 -1
- package/dist/br/data/mun/SE.cjs +1 -1
- package/dist/br/data/mun/SE.cjs.map +1 -1
- package/dist/br/data/mun/SE.js +812 -625
- package/dist/br/data/mun/SE.js.map +1 -1
- package/dist/br/data/mun/SP.cjs +1 -1
- package/dist/br/data/mun/SP.cjs.map +1 -1
- package/dist/br/data/mun/SP.js +7866 -6803
- package/dist/br/data/mun/SP.js.map +1 -1
- package/dist/br/data/mun/TO.cjs +1 -1
- package/dist/br/data/mun/TO.cjs.map +1 -1
- package/dist/br/data/mun/TO.js +2763 -2817
- package/dist/br/data/mun/TO.js.map +1 -1
- package/dist/br/locations.cjs +1 -1
- package/dist/br/locations.cjs.map +1 -1
- package/dist/br/locations.js +97 -24
- package/dist/br/locations.js.map +1 -1
- package/dist/br/scales.cjs +1 -1
- package/dist/br/scales.cjs.map +1 -1
- package/dist/br/scales.js +8 -4
- package/dist/br/scales.js.map +1 -1
- package/dist/br/whatsapp.cjs +2 -0
- package/dist/br/whatsapp.cjs.map +1 -0
- package/dist/br/whatsapp.js +21 -0
- package/dist/br/whatsapp.js.map +1 -0
- package/dist/br.cjs +1 -1
- package/dist/br.cjs.map +1 -1
- package/dist/br.d.ts +188 -10
- package/dist/br.js +18 -17
- package/dist/br.js.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.cjs +1 -1
- package/dist/components/BottomSheet/BottomSheet.cjs.map +1 -1
- package/dist/components/BottomSheet/BottomSheet.js +30 -27
- package/dist/components/BottomSheet/BottomSheet.js.map +1 -1
- package/dist/components/Command/Command.cjs +1 -1
- package/dist/components/Command/Command.cjs.map +1 -1
- package/dist/components/Command/Command.js +78 -75
- package/dist/components/Command/Command.js.map +1 -1
- package/dist/components/Drawer/Drawer.cjs +1 -1
- package/dist/components/Drawer/Drawer.cjs.map +1 -1
- package/dist/components/Drawer/Drawer.js +42 -39
- package/dist/components/Drawer/Drawer.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.cjs +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.js +111 -67
- package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/dist/components/Modal/Modal.cjs +1 -1
- package/dist/components/Modal/Modal.cjs.map +1 -1
- package/dist/components/Modal/Modal.js +45 -42
- package/dist/components/Modal/Modal.js.map +1 -1
- package/dist/components/Money/Money.cjs +1 -1
- package/dist/components/Money/Money.cjs.map +1 -1
- package/dist/components/Money/Money.js +10 -9
- package/dist/components/Money/Money.js.map +1 -1
- package/dist/components/Pagination/Pagination.cjs +1 -1
- package/dist/components/Pagination/Pagination.cjs.map +1 -1
- package/dist/components/Pagination/Pagination.js +38 -31
- package/dist/components/Pagination/Pagination.js.map +1 -1
- package/dist/components/Pagination/Pagination.module.cjs +1 -1
- package/dist/components/Pagination/Pagination.module.cjs.map +1 -1
- package/dist/components/Pagination/Pagination.module.js +4 -4
- package/dist/components/Pagination/Pagination.module.js.map +1 -1
- package/dist/components/Portal/Portal.cjs +1 -1
- package/dist/components/Portal/Portal.cjs.map +1 -1
- package/dist/components/Portal/Portal.js +9 -6
- package/dist/components/Portal/Portal.js.map +1 -1
- package/dist/components/Portal/portal-host.cjs +2 -0
- package/dist/components/Portal/portal-host.cjs.map +1 -0
- package/dist/components/Portal/portal-host.js +11 -0
- package/dist/components/Portal/portal-host.js.map +1 -0
- package/dist/components/RangeSlider/RangeSlider.cjs +1 -1
- package/dist/components/RangeSlider/RangeSlider.cjs.map +1 -1
- package/dist/components/RangeSlider/RangeSlider.js +23 -23
- package/dist/components/RangeSlider/RangeSlider.js.map +1 -1
- package/dist/components/Scheduler/Scheduler.cjs +1 -1
- package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
- package/dist/components/Scheduler/Scheduler.js +83 -82
- package/dist/components/Scheduler/Scheduler.js.map +1 -1
- package/dist/components/Slider/Slider.cjs +1 -1
- package/dist/components/Slider/Slider.cjs.map +1 -1
- package/dist/components/Slider/Slider.js +9 -9
- package/dist/components/Slider/Slider.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js +55 -53
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/components/VirtualList/VirtualList.cjs +1 -1
- package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
- package/dist/components/VirtualList/VirtualList.js +41 -31
- package/dist/components/VirtualList/VirtualList.js.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.cjs +1 -1
- package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.js +78 -68
- package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
- package/dist/forms/FormField.cjs +1 -1
- package/dist/forms/FormField.cjs.map +1 -1
- package/dist/forms/FormField.js +23 -21
- package/dist/forms/FormField.js.map +1 -1
- package/dist/forms/masked-inputs.cjs +1 -1
- package/dist/forms/masked-inputs.cjs.map +1 -1
- package/dist/forms/masked-inputs.js +20 -19
- package/dist/forms/masked-inputs.js.map +1 -1
- package/dist/hooks/use-fullscreen-element.cjs +2 -0
- package/dist/hooks/use-fullscreen-element.cjs.map +1 -0
- package/dist/hooks/use-fullscreen-element.js +20 -0
- package/dist/hooks/use-fullscreen-element.js.map +1 -0
- package/dist/hooks/use-fullscreen.cjs +2 -0
- package/dist/hooks/use-fullscreen.cjs.map +1 -0
- package/dist/hooks/use-fullscreen.js +66 -0
- package/dist/hooks/use-fullscreen.js.map +1 -0
- package/dist/hooks/use-push-to-talk.cjs +2 -0
- package/dist/hooks/use-push-to-talk.cjs.map +1 -0
- package/dist/hooks/use-push-to-talk.js +61 -0
- package/dist/hooks/use-push-to-talk.js.map +1 -0
- package/dist/http/describe-api-error.cjs.map +1 -1
- package/dist/http/describe-api-error.js.map +1 -1
- package/dist/http/errors.cjs +1 -1
- package/dist/http/errors.cjs.map +1 -1
- package/dist/http/errors.js +34 -16
- package/dist/http/errors.js.map +1 -1
- package/dist/http/parse-response.cjs +2 -2
- package/dist/http/parse-response.cjs.map +1 -1
- package/dist/http/parse-response.js +9 -8
- package/dist/http/parse-response.js.map +1 -1
- package/dist/i18n/create-i18n.cjs +1 -1
- package/dist/i18n/create-i18n.cjs.map +1 -1
- package/dist/i18n/create-i18n.js +28 -27
- package/dist/i18n/create-i18n.js.map +1 -1
- package/dist/query/QueryProvider.cjs +1 -1
- package/dist/query/QueryProvider.cjs.map +1 -1
- package/dist/query/QueryProvider.js +13 -11
- package/dist/query/QueryProvider.js.map +1 -1
- package/dist/query/foreign-client-warning.cjs +2 -0
- package/dist/query/foreign-client-warning.cjs.map +1 -0
- package/dist/query/foreign-client-warning.js +12 -0
- package/dist/query/foreign-client-warning.js.map +1 -0
- package/dist/router/AppRouter.cjs +1 -1
- package/dist/router/AppRouter.cjs.map +1 -1
- package/dist/router/AppRouter.js +28 -21
- package/dist/router/AppRouter.js.map +1 -1
- package/dist/router/RouteGuard.cjs +1 -1
- package/dist/router/RouteGuard.cjs.map +1 -1
- package/dist/router/RouteGuard.js +4 -4
- package/dist/router/RouteGuard.js.map +1 -1
- package/dist/sse/create-event-stream.cjs +1 -1
- package/dist/sse/create-event-stream.cjs.map +1 -1
- package/dist/sse/create-event-stream.js +34 -38
- package/dist/sse/create-event-stream.js.map +1 -1
- package/dist/styles/AIChat.css +79 -0
- package/dist/styles/Accordion.css +12 -0
- package/dist/styles/Alert.css +32 -0
- package/dist/styles/AppBar.css +17 -0
- package/dist/styles/AppShell.css +8 -0
- package/dist/styles/AspectRatio.css +3 -0
- package/dist/styles/AudioPlayer.css +13 -0
- package/dist/styles/AudioRecorder.css +20 -0
- package/dist/styles/Avatar.css +13 -0
- package/dist/styles/AvatarGroup.css +18 -0
- package/dist/styles/Badge.css +25 -0
- package/dist/styles/Banner.css +13 -0
- package/dist/styles/BarList.css +10 -0
- package/dist/styles/BarcodeScanner.css +21 -0
- package/dist/styles/BottomNavigation.css +10 -0
- package/dist/styles/BottomSheet.css +11 -0
- package/dist/styles/BrazilMap.css +20 -0
- package/dist/styles/Breadcrumbs.css +8 -0
- package/dist/styles/Button.css +42 -0
- package/dist/styles/Calendar.css +16 -0
- package/dist/styles/Card.css +15 -0
- package/dist/styles/Carousel.css +17 -0
- package/dist/styles/Center.css +5 -0
- package/dist/styles/Chat.css +35 -0
- package/dist/styles/Checkbox.css +11 -0
- package/dist/styles/ChipInput.css +13 -0
- package/dist/styles/CodeBlock.css +25 -0
- package/dist/styles/Collapsible.css +6 -0
- package/dist/styles/Combobox.css +18 -0
- package/dist/styles/Command.css +20 -0
- package/dist/styles/ContextMenu.css +11 -0
- package/dist/styles/CopyButton.css +4 -0
- package/dist/styles/DataList.css +3 -0
- package/dist/styles/DataTable.css +19 -0
- package/dist/styles/DateRangePicker.css +22 -0
- package/dist/styles/DescriptionList.css +4 -0
- package/dist/styles/Divider.css +11 -0
- package/dist/styles/Drawer.css +22 -0
- package/dist/styles/DropdownMenu.css +16 -0
- package/dist/styles/Dropzone.css +7 -0
- package/dist/styles/EmptyState.css +6 -0
- package/dist/styles/ErrorState.css +6 -0
- package/dist/styles/ErrorText.css +2 -0
- package/dist/styles/FileUpload.css +17 -0
- package/dist/styles/FilterBar.css +19 -0
- package/dist/styles/FloatingActionButton.css +23 -0
- package/dist/styles/Form.css +21 -0
- package/dist/styles/HoverCard.css +11 -0
- package/dist/styles/IconPicker.css +6 -0
- package/dist/styles/Image.css +2 -0
- package/dist/styles/ImageCropper.css +16 -0
- package/dist/styles/Input.css +24 -0
- package/dist/styles/InstallBanner.css +8 -0
- package/dist/styles/Kanban.css +15 -0
- package/dist/styles/Kbd.css +5 -0
- package/dist/styles/Label.css +3 -0
- package/dist/styles/Layout.css +22 -0
- package/dist/styles/Lightbox.css +21 -0
- package/dist/styles/ListTile.css +14 -0
- package/dist/styles/MapLegend.css +9 -0
- package/dist/styles/Markdown.css +24 -0
- package/dist/styles/Masonry.css +4 -0
- package/dist/styles/Menubar.css +17 -0
- package/dist/styles/Modal.css +21 -0
- package/dist/styles/ModalsManager.css +2 -0
- package/dist/styles/MultiSelect.css +21 -0
- package/dist/styles/MunicipalitySearch.css +6 -0
- package/dist/styles/NProgress.css +2 -0
- package/dist/styles/Navbar.css +10 -0
- package/dist/styles/NavigationMenu.css +12 -0
- package/dist/styles/NavigationRail.css +14 -0
- package/dist/styles/NotificationCenter.css +21 -0
- package/dist/styles/OfflineIndicator.css +7 -0
- package/dist/styles/Page.css +13 -0
- package/dist/styles/Pagination.css +12 -0
- package/dist/styles/PasswordInput.css +21 -0
- package/dist/styles/PinInput.css +13 -0
- package/dist/styles/PixQRCode.css +9 -0
- package/dist/styles/Popover.css +10 -0
- package/dist/styles/Progress.css +11 -0
- package/dist/styles/QRCode.css +3 -0
- package/dist/styles/Radio.css +14 -0
- package/dist/styles/RangeSlider.css +15 -0
- package/dist/styles/RatingStars.css +11 -0
- package/dist/styles/RefreshIndicator.css +5 -0
- package/dist/styles/Resizable.css +13 -0
- package/dist/styles/RichTextEditor.css +19 -0
- package/dist/styles/SafeArea.css +7 -0
- package/dist/styles/Scheduler.css +24 -0
- package/dist/styles/ScrollArea.css +7 -0
- package/dist/styles/SearchBar.css +11 -0
- package/dist/styles/SegmentedControl.css +12 -0
- package/dist/styles/Select.css +14 -0
- package/dist/styles/Sidebar.css +19 -0
- package/dist/styles/SignaturePad.css +6 -0
- package/dist/styles/Skeleton.css +7 -0
- package/dist/styles/Slider.css +15 -0
- package/dist/styles/Spacer.css +4 -0
- package/dist/styles/Sparkline.css +4 -0
- package/dist/styles/Spinner.css +12 -0
- package/dist/styles/Stat.css +12 -0
- package/dist/styles/Stepper.css +17 -0
- package/dist/styles/StepperInput.css +15 -0
- package/dist/styles/Switch.css +11 -0
- package/dist/styles/SyncStatusBadge.css +13 -0
- package/dist/styles/Table.css +16 -0
- package/dist/styles/Tabs.css +14 -0
- package/dist/styles/Tag.css +14 -0
- package/dist/styles/Textarea.css +13 -0
- package/dist/styles/TimePicker.css +14 -0
- package/dist/styles/Timeline.css +17 -0
- package/dist/styles/Toast.css +21 -0
- package/dist/styles/Toggle.css +12 -0
- package/dist/styles/ToggleGroup.css +9 -0
- package/dist/styles/Tooltip.css +10 -0
- package/dist/styles/Tour.css +20 -0
- package/dist/styles/TrajectoryMap.css +16 -0
- package/dist/styles/Transfer.css +21 -0
- package/dist/styles/TreeView.css +17 -0
- package/dist/styles/TruncateText.css +2 -0
- package/dist/styles/UpdatePrompt.css +10 -0
- package/dist/styles/VirtualList.css +5 -0
- package/dist/styles/VirtualTable.css +17 -0
- package/dist/styles/VisuallyHidden.css +2 -0
- package/dist/styles/Wizard.css +4 -0
- package/dist/styles/actions.css +104 -0
- package/dist/styles/advanced.css +66 -0
- package/dist/styles/br.css +41 -0
- package/dist/styles/chat.css +113 -0
- package/dist/styles/core.css +40 -0
- package/dist/styles/data.css +222 -0
- package/dist/styles/editor.css +19 -0
- package/dist/styles/feedback.css +201 -0
- package/dist/styles/forms.css +321 -0
- package/dist/styles/geo.css +16 -0
- package/dist/styles/icons.css +6 -0
- package/dist/styles/identity.css +48 -0
- package/dist/styles/layout.css +122 -0
- package/dist/styles/media.css +52 -0
- package/dist/styles/navigation.css +167 -0
- package/dist/styles/overlay.css +73 -0
- package/dist/styles/utility.css +39 -0
- package/dist/styles.css +1 -1
- package/dist/sw/register-service-worker.cjs +1 -1
- package/dist/sw/register-service-worker.cjs.map +1 -1
- package/dist/sw/register-service-worker.js +21 -17
- package/dist/sw/register-service-worker.js.map +1 -1
- package/dist/sw.d.ts +17 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +1382 -76
- package/dist/tempest-react-sdk.js +293 -281
- package/dist/utils/dev-mode.cjs.map +1 -1
- package/dist/utils/dev-mode.js.map +1 -1
- package/dist/utils/duplicate-instance.cjs +2 -0
- package/dist/utils/duplicate-instance.cjs.map +1 -0
- package/dist/utils/duplicate-instance.js +6 -0
- package/dist/utils/duplicate-instance.js.map +1 -0
- package/dist/utils/format.cjs +1 -1
- package/dist/utils/format.cjs.map +1 -1
- package/dist/utils/format.js +22 -14
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/intl-cache.cjs +2 -0
- package/dist/utils/intl-cache.cjs.map +1 -0
- package/dist/utils/intl-cache.js +23 -0
- package/dist/utils/intl-cache.js.map +1 -0
- package/dist/utils/json-frame.cjs +2 -0
- package/dist/utils/json-frame.cjs.map +1 -0
- package/dist/utils/json-frame.js +30 -0
- package/dist/utils/json-frame.js.map +1 -0
- package/dist/utils/numbers.cjs +1 -1
- package/dist/utils/numbers.cjs.map +1 -1
- package/dist/utils/numbers.js +8 -7
- package/dist/utils/numbers.js.map +1 -1
- package/dist/vision/results.cjs +1 -1
- package/dist/vision/results.cjs.map +1 -1
- package/dist/vision/results.js +26 -17
- package/dist/vision/results.js.map +1 -1
- package/dist/vision.d.ts +29 -0
- package/dist/vite/tempest-icons.cjs +1 -1
- package/dist/vite/tempest-icons.cjs.map +1 -1
- package/dist/vite/tempest-icons.js +1 -1
- package/dist/vite/tempest-icons.js.map +1 -1
- package/dist/webrtc/link-stats.cjs +2 -0
- package/dist/webrtc/link-stats.cjs.map +1 -0
- package/dist/webrtc/link-stats.js +74 -0
- package/dist/webrtc/link-stats.js.map +1 -0
- package/dist/webrtc/opus-sdp.cjs +3 -0
- package/dist/webrtc/opus-sdp.cjs.map +1 -0
- package/dist/webrtc/opus-sdp.js +94 -0
- package/dist/webrtc/opus-sdp.js.map +1 -0
- package/dist/webrtc/sender-bitrate.cjs +2 -0
- package/dist/webrtc/sender-bitrate.cjs.map +1 -0
- package/dist/webrtc/sender-bitrate.js +15 -0
- package/dist/webrtc/sender-bitrate.js.map +1 -0
- package/dist/webrtc/use-link-stats.cjs +2 -0
- package/dist/webrtc/use-link-stats.cjs.map +1 -0
- package/dist/webrtc/use-link-stats.js +22 -0
- package/dist/webrtc/use-link-stats.js.map +1 -0
- package/dist/ws/create-web-socket.cjs +1 -1
- package/dist/ws/create-web-socket.cjs.map +1 -1
- package/dist/ws/create-web-socket.js +130 -58
- package/dist/ws/create-web-socket.js.map +1 -1
- package/dist/ws/resilience.cjs +2 -0
- package/dist/ws/resilience.cjs.map +1 -0
- package/dist/ws/resilience.js +21 -0
- package/dist/ws/resilience.js.map +1 -0
- package/dist/ws/use-web-socket.cjs +1 -1
- package/dist/ws/use-web-socket.cjs.map +1 -1
- package/dist/ws/use-web-socket.js +47 -39
- package/dist/ws/use-web-socket.js.map +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { windowRms as e } from "./voice-chain-stages.js";
|
|
2
|
+
//#region src/audio/noise-floor.ts
|
|
3
|
+
var t = 3e3, n = 50, r = 2.4, i = .004, a = .12;
|
|
4
|
+
function o() {
|
|
5
|
+
return typeof AudioContext < "u" ? AudioContext : globalThis.webkitAudioContext;
|
|
6
|
+
}
|
|
7
|
+
async function s(r, { onProgress: i, durationMs: a = t, context: s } = {}) {
|
|
8
|
+
let c = o(), l = s ?? (c ? new c() : null), u = r.getAudioTracks()[0];
|
|
9
|
+
if (!l || !u) return 0;
|
|
10
|
+
let d = l.createMediaStreamSource(r), f = l.createAnalyser();
|
|
11
|
+
f.fftSize = 2048, d.connect(f);
|
|
12
|
+
let p = new Float32Array(f.fftSize), m = performance.now(), h = 0;
|
|
13
|
+
return await new Promise((t) => {
|
|
14
|
+
let r = setInterval(() => {
|
|
15
|
+
f.getFloatTimeDomainData(p), h = Math.max(h, e(p));
|
|
16
|
+
let n = performance.now() - m;
|
|
17
|
+
i?.(Math.min(1, n / a)), !(n < a) && (clearInterval(r), t());
|
|
18
|
+
}, n);
|
|
19
|
+
}), d.disconnect(), f.disconnect(), h;
|
|
20
|
+
}
|
|
21
|
+
function c(e) {
|
|
22
|
+
return Number.isFinite(e) ? Math.min(a, Math.max(i, e * r)) : i;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { s as measureNoiseFloor, c as suggestGateThreshold };
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=noise-floor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noise-floor.js","names":[],"sources":["../../src/audio/noise-floor.ts"],"sourcesContent":["import { windowRms } from \"./voice-chain-stages\";\n\n/** How long the measurement listens by default, in milliseconds. */\nconst DEFAULT_DURATION_MS = 3000;\n\n/** How often it samples the room, in milliseconds. */\nconst SAMPLE_MS = 50;\n\n/**\n * Headroom over the measured floor, so ordinary speech clears the gate.\n *\n * A gate set exactly at the noise level chatters, opening and closing on the\n * room's own variation. Ported from `tempest-mirror-screen`'s `voiceChain.ts`,\n * where it was arrived at by listening.\n */\nconst GATE_MARGIN = 2.4;\n\n/** The narrowest and widest thresholds worth suggesting. */\nconst GATE_MIN = 0.004;\nconst GATE_MAX = 0.12;\n\n/** Options for {@link measureNoiseFloor}. */\nexport interface NoiseFloorOptions {\n /** Called with `0..1` so a dialog can show the wait. */\n onProgress?: (fraction: number) => void;\n /** How long to listen. Default `3000`. */\n durationMs?: number;\n /** Reuse an existing `AudioContext` rather than opening one. */\n context?: AudioContext;\n}\n\n/** The `AudioContext` constructor this engine exposes, prefixed or not. */\nfunction audioContextConstructor(): typeof AudioContext | undefined {\n if (typeof AudioContext !== \"undefined\") return AudioContext;\n return (globalThis as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n}\n\n/**\n * Listen to a room at rest and report how loud its silence is.\n *\n * The number a person cannot guess. Setting a gate by ear means switching it on\n * and hearing what disappeared, which only works if you already know what should\n * have disappeared — so the usual outcome is leaving it at a default that is\n * wrong for that room. Measuring replaces the guess.\n *\n * The result is the **peak** RMS window observed, not the average: a threshold\n * under the loudest thing the quiet room did will be opened by that thing.\n *\n * @param stream - A live capture of the room while nobody speaks.\n * @param options - See {@link NoiseFloorOptions}.\n * @returns The peak RMS observed, `0..1`. `0` when this engine has no Web Audio\n * or the stream carries no audio track — which {@link suggestGateThreshold}\n * turns into its own floor rather than into a gate that never opens.\n *\n * @example\n * const floor = await measureNoiseFloor(stream, { onProgress: setProgress });\n * setThreshold(suggestGateThreshold(floor));\n */\nexport async function measureNoiseFloor(\n stream: MediaStream,\n {\n onProgress,\n durationMs = DEFAULT_DURATION_MS,\n context: injectedContext,\n }: NoiseFloorOptions = {},\n): Promise<number> {\n const Ctor = audioContextConstructor();\n const context = injectedContext ?? (Ctor ? new Ctor() : null);\n const track = stream.getAudioTracks()[0];\n if (!context || !track) return 0;\n\n const source = context.createMediaStreamSource(stream);\n const analyser = context.createAnalyser();\n analyser.fftSize = 2048;\n source.connect(analyser);\n\n const buffer = new Float32Array(analyser.fftSize);\n const started = performance.now();\n let peak = 0;\n\n await new Promise<void>((resolve) => {\n const timer = setInterval(() => {\n analyser.getFloatTimeDomainData(buffer);\n peak = Math.max(peak, windowRms(buffer));\n\n const elapsed = performance.now() - started;\n onProgress?.(Math.min(1, elapsed / durationMs));\n if (elapsed < durationMs) return;\n clearInterval(timer);\n resolve();\n }, SAMPLE_MS);\n });\n\n source.disconnect();\n analyser.disconnect();\n return peak;\n}\n\n/**\n * Turn a measured noise floor into a threshold worth using.\n *\n * Sits above the floor by a margin rather than on it, and is clamped at both\n * ends: a room measured as perfectly silent (or not measured at all) would\n * otherwise get a threshold of zero, which is a gate that never closes, and a\n * room measured while somebody talked would get one nothing opens.\n *\n * @param noiseFloor - Peak RMS measured while nobody spoke.\n * @returns A threshold for {@link VoiceChainSettings.gateThreshold}, `0.004..0.12`.\n */\nexport function suggestGateThreshold(noiseFloor: number): number {\n if (!Number.isFinite(noiseFloor)) return GATE_MIN;\n return Math.min(GATE_MAX, Math.max(GATE_MIN, noiseFloor * GATE_MARGIN));\n}\n"],"mappings":";;AAGA,IAAM,IAAsB,KAGtB,IAAY,IASZ,IAAc,KAGd,IAAW,MACX,IAAW;AAajB,SAAS,IAA2D;CAEhE,OADI,OAAO,eAAiB,MAAoB,eACxC,WAA4D;AACxE;AAuBA,eAAsB,EAClB,GACA,EACI,eACA,gBAAa,GACb,SAAS,MACU,CAAC,GACT;CACf,IAAM,IAAO,EAAwB,GAC/B,IAAU,MAAoB,IAAO,IAAI,EAAK,IAAI,OAClD,IAAQ,EAAO,eAAe,CAAC,CAAC;CACtC,IAAI,CAAC,KAAW,CAAC,GAAO,OAAO;CAE/B,IAAM,IAAS,EAAQ,wBAAwB,CAAM,GAC/C,IAAW,EAAQ,eAAe;CAExC,AADA,EAAS,UAAU,MACnB,EAAO,QAAQ,CAAQ;CAEvB,IAAM,IAAS,IAAI,aAAa,EAAS,OAAO,GAC1C,IAAU,YAAY,IAAI,GAC5B,IAAO;CAiBX,OAfA,MAAM,IAAI,SAAe,MAAY;EACjC,IAAM,IAAQ,kBAAkB;GAE5B,AADA,EAAS,uBAAuB,CAAM,GACtC,IAAO,KAAK,IAAI,GAAM,EAAU,CAAM,CAAC;GAEvC,IAAM,IAAU,YAAY,IAAI,IAAI;GACpC,IAAa,KAAK,IAAI,GAAG,IAAU,CAAU,CAAC,GAC1C,MAAU,OACd,cAAc,CAAK,GACnB,EAAQ;EACZ,GAAG,CAAS;CAChB,CAAC,GAED,EAAO,WAAW,GAClB,EAAS,WAAW,GACb;AACX;AAaA,SAAgB,EAAqB,GAA4B;CAE7D,OADK,OAAO,SAAS,CAAU,IACxB,KAAK,IAAI,GAAU,KAAK,IAAI,GAAU,IAAa,CAAW,CAAC,IAD7B;AAE7C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-audio-bus.cjs","names":[],"sources":["../../src/audio/use-audio-bus.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { createAudioBus, type AudioBus, type AudioBusOptions } from \"./audio-bus\";\n\n/**\n * Keep one {@link AudioBus} alive for as long as the component is mounted.\n *\n * The bus is built on the first render and closed on unmount, which matters more\n * than it looks: browsers cap the number of live `AudioContext`s (Chrome allows\n * around six), so a bus leaked on unmount eventually breaks every later one on\n * the page.\n *\n * Options are read once. A bus is a device route and a mixing graph, not a\n * render output — rebuilding it because a prop changed would drop every attached\n * source mid-sentence. Change gain and output through the bus itself.\n *\n * @param options - See {@link AudioBusOptions}. Read on the first render only.\n * @returns The bus, stable for the component's lifetime.\n *\n * @example\n * const bus = useAudioBus({ maxGain: 3 });\n *\n * useEffect(() => {\n * const handle = bus.attach(stream, { gain: 1 });\n * return () => handle.stop();\n * }, [bus, stream]);\n */\nexport function useAudioBus(options: AudioBusOptions = {}): AudioBus {\n const [bus] = useState<AudioBus>(() => createAudioBus(options));\n\n useEffect(() => {\n return () => bus.close();\n }, [bus]);\n\n return bus;\n}\n"],"mappings":"0DA0BA,SAAgB,EAAY,EAA2B,CAAC,EAAa,CACjE,GAAM,CAAC,IAAA,EAAO,EAAA,SAAA,KAAyB,EAAA,eAAe,CAAO,CAAC,EAM9D,OAJA,EAAA,EAAA,UAAA,SACiB,EAAI,MAAM,EACxB,CAAC,CAAG,CAAC,EAED,CACX"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createAudioBus as e } from "./audio-bus.js";
|
|
2
|
+
import { useEffect as t, useState as n } from "react";
|
|
3
|
+
//#region src/audio/use-audio-bus.ts
|
|
4
|
+
function r(r = {}) {
|
|
5
|
+
let [i] = n(() => e(r));
|
|
6
|
+
return t(() => () => i.close(), [i]), i;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { r as useAudioBus };
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=use-audio-bus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-audio-bus.js","names":[],"sources":["../../src/audio/use-audio-bus.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { createAudioBus, type AudioBus, type AudioBusOptions } from \"./audio-bus\";\n\n/**\n * Keep one {@link AudioBus} alive for as long as the component is mounted.\n *\n * The bus is built on the first render and closed on unmount, which matters more\n * than it looks: browsers cap the number of live `AudioContext`s (Chrome allows\n * around six), so a bus leaked on unmount eventually breaks every later one on\n * the page.\n *\n * Options are read once. A bus is a device route and a mixing graph, not a\n * render output — rebuilding it because a prop changed would drop every attached\n * source mid-sentence. Change gain and output through the bus itself.\n *\n * @param options - See {@link AudioBusOptions}. Read on the first render only.\n * @returns The bus, stable for the component's lifetime.\n *\n * @example\n * const bus = useAudioBus({ maxGain: 3 });\n *\n * useEffect(() => {\n * const handle = bus.attach(stream, { gain: 1 });\n * return () => handle.stop();\n * }, [bus, stream]);\n */\nexport function useAudioBus(options: AudioBusOptions = {}): AudioBus {\n const [bus] = useState<AudioBus>(() => createAudioBus(options));\n\n useEffect(() => {\n return () => bus.close();\n }, [bus]);\n\n return bus;\n}\n"],"mappings":";;;AA0BA,SAAgB,EAAY,IAA2B,CAAC,GAAa;CACjE,IAAM,CAAC,KAAO,QAAyB,EAAe,CAAO,CAAC;CAM9D,OAJA,cACiB,EAAI,MAAM,GACxB,CAAC,CAAG,CAAC,GAED;AACX"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./level-meter.cjs");var t=.045,n=350,r=100;function i(i,a,{threshold:o=t,releaseMs:s=n,pollMs:c=r,context:l}={}){let u=e.createLevelMeter(i,{decay:0,...l?{context:l}:{}}),d=!1,f=0,p=setInterval(()=>{let e=performance.now();u.level()>=o&&(f=e);let t=f>0&&e-f<s;t!==d&&(d=t,a(d))},c);return{get speaking(){return d},stop:()=>{clearInterval(p),u.stop()}}}exports.monitorVoiceActivity=i;
|
|
2
|
+
//# sourceMappingURL=voice-activity.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice-activity.cjs","names":[],"sources":["../../src/audio/voice-activity.ts"],"sourcesContent":["import { createLevelMeter } from \"./level-meter\";\n\n/** RMS at which a window counts as speech. */\nconst DEFAULT_THRESHOLD = 0.045;\n\n/**\n * How long the indicator stays on after the level drops.\n *\n * RMS falls to nothing in the gaps between words, so a bare threshold strobes on\n * every one of them. Ported from `tempest-mirror-screen`'s `voiceActivity.ts`,\n * where it was arrived at by watching a real call.\n */\nconst DEFAULT_RELEASE_MS = 350;\n\n/** How often the level is read. */\nconst DEFAULT_POLL_MS = 100;\n\n/** Options for {@link monitorVoiceActivity}. */\nexport interface VoiceActivityOptions {\n /** RMS at or above which a window counts as speech. Default `0.045`. */\n threshold?: number;\n /** How long the state stays `true` after the level drops. Default `350`. */\n releaseMs?: number;\n /** How often the level is sampled, in ms. Default `100`. */\n pollMs?: number;\n /**\n * Reuse an existing `AudioContext` instead of creating one.\n *\n * A monitor per remote participant hits the browser's cap on live contexts\n * (Chrome allows around six) in a five-person call. Open one and share it.\n */\n context?: AudioContext;\n}\n\n/** A running voice-activity monitor. */\nexport interface VoiceActivityMonitor {\n /** Whether the track is currently carrying speech. */\n readonly speaking: boolean;\n /** Stop sampling and release the analysis graph. */\n stop: () => void;\n}\n\n/**\n * Report when a track is carrying speech.\n *\n * Runs entirely on the **receiving** side, over a track the connection already\n * delivers, and that is the whole point: a speaking indicator changes several\n * times per second, so signalling it through a server floods the room with\n * messages carrying information every client can derive for free.\n *\n * Two details separate this from a threshold on a level meter:\n *\n * 1. **The release.** RMS drops to nothing in the gaps between words, so a bare\n * comparison strobes on every syllable. The state only falls after\n * `releaseMs` of quiet.\n * 2. **`onChange` fires on the flip, not on the sample.** A callback per poll\n * would re-render a participant list ten times a second to say the same thing.\n *\n * The meter underneath runs with smoothing off: its eased decay plus this release\n * would hold the indicator open long after the speech stopped.\n *\n * @param stream - The stream whose first audio track is measured.\n * @param onChange - Called only when the speaking state actually flips.\n * @param options - See {@link VoiceActivityOptions}.\n * @returns A handle carrying the current state and the teardown.\n *\n * @example\n * const vad = monitorVoiceActivity(remoteStream, (speaking) => setSpeaking(speaking));\n * // later\n * vad.stop();\n */\nexport function monitorVoiceActivity(\n stream: MediaStream,\n onChange: (speaking: boolean) => void,\n {\n threshold = DEFAULT_THRESHOLD,\n releaseMs = DEFAULT_RELEASE_MS,\n pollMs = DEFAULT_POLL_MS,\n context,\n }: VoiceActivityOptions = {},\n): VoiceActivityMonitor {\n const meter = createLevelMeter(stream, { decay: 0, ...(context ? { context } : {}) });\n let speaking = false;\n let lastLoudAt = 0;\n\n const timer = setInterval(() => {\n const now = performance.now();\n if (meter.level() >= threshold) lastLoudAt = now;\n const next = lastLoudAt > 0 && now - lastLoudAt < releaseMs;\n if (next === speaking) return;\n speaking = next;\n onChange(speaking);\n }, pollMs);\n\n return {\n get speaking(): boolean {\n return speaking;\n },\n stop: (): void => {\n clearInterval(timer);\n meter.stop();\n },\n };\n}\n"],"mappings":"qCAGA,IAAM,EAAoB,KASpB,EAAqB,IAGrB,EAAkB,IAwDxB,SAAgB,EACZ,EACA,EACA,CACI,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,WACsB,CAAC,EACP,CACpB,IAAM,EAAQ,EAAA,iBAAiB,EAAQ,CAAE,MAAO,EAAG,GAAI,EAAU,CAAE,SAAQ,EAAI,CAAC,CAAG,CAAC,EAChF,EAAW,GACX,EAAa,EAEX,EAAQ,gBAAkB,CAC5B,IAAM,EAAM,YAAY,IAAI,EACxB,EAAM,MAAM,GAAK,IAAW,EAAa,GAC7C,IAAM,EAAO,EAAa,GAAK,EAAM,EAAa,EAC9C,IAAS,IACb,EAAW,EACX,EAAS,CAAQ,EACrB,EAAG,CAAM,EAET,MAAO,CACH,IAAI,UAAoB,CACpB,OAAO,CACX,EACA,SAAkB,CACd,cAAc,CAAK,EACnB,EAAM,KAAK,CACf,CACJ,CACJ"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createLevelMeter as e } from "./level-meter.js";
|
|
2
|
+
//#region src/audio/voice-activity.ts
|
|
3
|
+
var t = .045, n = 350, r = 100;
|
|
4
|
+
function i(i, a, { threshold: o = t, releaseMs: s = n, pollMs: c = r, context: l } = {}) {
|
|
5
|
+
let u = e(i, {
|
|
6
|
+
decay: 0,
|
|
7
|
+
...l ? { context: l } : {}
|
|
8
|
+
}), d = !1, f = 0, p = setInterval(() => {
|
|
9
|
+
let e = performance.now();
|
|
10
|
+
u.level() >= o && (f = e);
|
|
11
|
+
let t = f > 0 && e - f < s;
|
|
12
|
+
t !== d && (d = t, a(d));
|
|
13
|
+
}, c);
|
|
14
|
+
return {
|
|
15
|
+
get speaking() {
|
|
16
|
+
return d;
|
|
17
|
+
},
|
|
18
|
+
stop: () => {
|
|
19
|
+
clearInterval(p), u.stop();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { i as monitorVoiceActivity };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=voice-activity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice-activity.js","names":[],"sources":["../../src/audio/voice-activity.ts"],"sourcesContent":["import { createLevelMeter } from \"./level-meter\";\n\n/** RMS at which a window counts as speech. */\nconst DEFAULT_THRESHOLD = 0.045;\n\n/**\n * How long the indicator stays on after the level drops.\n *\n * RMS falls to nothing in the gaps between words, so a bare threshold strobes on\n * every one of them. Ported from `tempest-mirror-screen`'s `voiceActivity.ts`,\n * where it was arrived at by watching a real call.\n */\nconst DEFAULT_RELEASE_MS = 350;\n\n/** How often the level is read. */\nconst DEFAULT_POLL_MS = 100;\n\n/** Options for {@link monitorVoiceActivity}. */\nexport interface VoiceActivityOptions {\n /** RMS at or above which a window counts as speech. Default `0.045`. */\n threshold?: number;\n /** How long the state stays `true` after the level drops. Default `350`. */\n releaseMs?: number;\n /** How often the level is sampled, in ms. Default `100`. */\n pollMs?: number;\n /**\n * Reuse an existing `AudioContext` instead of creating one.\n *\n * A monitor per remote participant hits the browser's cap on live contexts\n * (Chrome allows around six) in a five-person call. Open one and share it.\n */\n context?: AudioContext;\n}\n\n/** A running voice-activity monitor. */\nexport interface VoiceActivityMonitor {\n /** Whether the track is currently carrying speech. */\n readonly speaking: boolean;\n /** Stop sampling and release the analysis graph. */\n stop: () => void;\n}\n\n/**\n * Report when a track is carrying speech.\n *\n * Runs entirely on the **receiving** side, over a track the connection already\n * delivers, and that is the whole point: a speaking indicator changes several\n * times per second, so signalling it through a server floods the room with\n * messages carrying information every client can derive for free.\n *\n * Two details separate this from a threshold on a level meter:\n *\n * 1. **The release.** RMS drops to nothing in the gaps between words, so a bare\n * comparison strobes on every syllable. The state only falls after\n * `releaseMs` of quiet.\n * 2. **`onChange` fires on the flip, not on the sample.** A callback per poll\n * would re-render a participant list ten times a second to say the same thing.\n *\n * The meter underneath runs with smoothing off: its eased decay plus this release\n * would hold the indicator open long after the speech stopped.\n *\n * @param stream - The stream whose first audio track is measured.\n * @param onChange - Called only when the speaking state actually flips.\n * @param options - See {@link VoiceActivityOptions}.\n * @returns A handle carrying the current state and the teardown.\n *\n * @example\n * const vad = monitorVoiceActivity(remoteStream, (speaking) => setSpeaking(speaking));\n * // later\n * vad.stop();\n */\nexport function monitorVoiceActivity(\n stream: MediaStream,\n onChange: (speaking: boolean) => void,\n {\n threshold = DEFAULT_THRESHOLD,\n releaseMs = DEFAULT_RELEASE_MS,\n pollMs = DEFAULT_POLL_MS,\n context,\n }: VoiceActivityOptions = {},\n): VoiceActivityMonitor {\n const meter = createLevelMeter(stream, { decay: 0, ...(context ? { context } : {}) });\n let speaking = false;\n let lastLoudAt = 0;\n\n const timer = setInterval(() => {\n const now = performance.now();\n if (meter.level() >= threshold) lastLoudAt = now;\n const next = lastLoudAt > 0 && now - lastLoudAt < releaseMs;\n if (next === speaking) return;\n speaking = next;\n onChange(speaking);\n }, pollMs);\n\n return {\n get speaking(): boolean {\n return speaking;\n },\n stop: (): void => {\n clearInterval(timer);\n meter.stop();\n },\n };\n}\n"],"mappings":";;AAGA,IAAM,IAAoB,MASpB,IAAqB,KAGrB,IAAkB;AAwDxB,SAAgB,EACZ,GACA,GACA,EACI,eAAY,GACZ,eAAY,GACZ,YAAS,GACT,eACsB,CAAC,GACP;CACpB,IAAM,IAAQ,EAAiB,GAAQ;EAAE,OAAO;EAAG,GAAI,IAAU,EAAE,WAAQ,IAAI,CAAC;CAAG,CAAC,GAChF,IAAW,IACX,IAAa,GAEX,IAAQ,kBAAkB;EAC5B,IAAM,IAAM,YAAY,IAAI;EAC5B,AAAI,EAAM,MAAM,KAAK,MAAW,IAAa;EAC7C,IAAM,IAAO,IAAa,KAAK,IAAM,IAAa;EAC9C,MAAS,MACb,IAAW,GACX,EAAS,CAAQ;CACrB,GAAG,CAAM;CAET,OAAO;EACH,IAAI,WAAoB;GACpB,OAAO;EACX;EACA,YAAkB;GAEd,AADA,cAAc,CAAK,GACnB,EAAM,KAAK;EACf;CACJ;AACJ"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e=7e3,t={threshold:-24,knee:30,ratio:3,attack:.003,release:.25},n={threshold:-1,knee:0,ratio:20,attack:.001,release:.1},r=.005,i=.12,a=1024;function o(e){let t=0;for(let n of e)t+=n*n;return Math.sqrt(t/e.length)}function s(e){let t=e.createAnalyser();return t.fftSize=a,t}function c(e){let t=e.createBiquadFilter();return t.type=`highpass`,t.frequency.value=85,t}function l(e){let n=e.createDynamicsCompressor();return n.threshold.value=t.threshold,n.knee.value=t.knee,n.ratio.value=t.ratio,n.attack.value=t.attack,n.release.value=t.release,n}function u(e){let t=e.createBiquadFilter();return t.type=`peaking`,t.frequency.value=3e3,t.Q.value=1,t.gain.value=3,t}function d(e){let t=e.createBiquadFilter();return t.type=`lowpass`,t.frequency.value=9e3,t}function f(e){let t=e.createDynamicsCompressor();return t.threshold.value=n.threshold,t.knee.value=n.knee,t.ratio.value=n.ratio,t.attack.value=n.attack,t.release.value=n.release,t}function p(e,t){let n=s(e),a=e.createGain();a.gain.value=0;let c=new Float32Array(n.fftSize),l=0,u=setInterval(()=>{n.getFloatTimeDomainData(c);let s=performance.now();o(c)>=t()&&(l=s);let u=l>0&&s-l<220;a.gain.setTargetAtTime(+!!u,e.currentTime,u?r:i)},20);return{detector:n,gain:a,stop:()=>clearInterval(u)}}function m(t){let n=t.createBiquadFilter();n.type=`bandpass`,n.frequency.value=e,n.Q.value=2;let r=s(t);n.connect(r);let i=t.createBiquadFilter();i.type=`peaking`,i.frequency.value=e,i.Q.value=2,i.gain.value=0;let a=new Float32Array(r.fftSize),c=setInterval(()=>{r.getFloatTimeDomainData(a);let e=Math.max(0,o(a)-.02),n=Math.min(10,e*200);i.gain.setTargetAtTime(-n,t.currentTime,.02)},20);return{band:n,detector:r,shaper:i,stop:()=>clearInterval(c)}}exports.compressorStage=l,exports.deEsserStage=m,exports.gateStage=p,exports.highPassStage=c,exports.hissCutStage=d,exports.limiterStage=f,exports.presenceStage=u,exports.windowRms=o;
|
|
2
|
+
//# sourceMappingURL=voice-chain-stages.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice-chain-stages.cjs","names":[],"sources":["../../src/audio/voice-chain-stages.ts"],"sourcesContent":["/**\n * The individual stages a voice chain is built from, and the two side-chains\n * Web Audio cannot express declaratively.\n *\n * Split out of `voice-chain.ts` because every number here is a tuning decision\n * with a reason attached, and the file that assembles them should read as the\n * signal path rather than as a wall of constants.\n */\n\n/** Everything below this is rumble, not speech. */\nconst HIGH_PASS_HZ = 85;\n\n/** Where consonants live — the band that decides whether a word is understood. */\nconst PRESENCE_HZ = 3000;\nconst PRESENCE_GAIN_DB = 3;\nconst PRESENCE_Q = 1;\n\n/** Where microphone hiss lives, above anything speech needs. */\nconst HISS_HZ = 9000;\n\n/** The sibilance band a de-esser rides. */\nconst SIBILANCE_HZ = 7000;\nconst SIBILANCE_Q = 2;\n\n/** Deepest cut the de-esser applies, in dB. */\nconst DEESSER_MAX_CUT_DB = 10;\n\n/** Band energy above which the cut starts, as RMS. */\nconst DEESSER_THRESHOLD = 0.02;\n\n/** How steeply the cut follows the band's energy above the threshold. */\nconst DEESSER_SLOPE_DB_PER_RMS = 200;\n\n/** Seconds the de-esser takes to reach a new cut. */\nconst DEESSER_SMOOTHING = 0.02;\n\n/**\n * A leveller, not a limiter: 3:1 with a soft knee evens out the distance between\n * a whisper and a shout without audibly pumping.\n */\nconst COMPRESSOR = {\n threshold: -24,\n knee: 30,\n ratio: 3,\n attack: 0.003,\n release: 0.25,\n} as const;\n\n/**\n * The ceiling the limiter holds, in dB relative to full scale.\n *\n * Just under zero, because digital clipping is not a degradation the other end\n * can recover from — it arrives as squared-off samples that no amount of\n * processing undoes.\n */\nconst LIMITER = {\n threshold: -1,\n knee: 0,\n ratio: 20,\n attack: 0.001,\n release: 0.1,\n} as const;\n\n/** How often the gate and the de-esser look at the signal, in milliseconds. */\nexport const DETECTOR_POLL_MS = 20;\n\n/** Time constant for opening the gate. Short, so a first syllable survives. */\nconst GATE_ATTACK = 0.005;\n\n/** Time constant for closing it. Long, so a word does not end abruptly. */\nconst GATE_RELEASE = 0.12;\n\n/**\n * How long the gate stays open after the level drops.\n *\n * RMS falls to nothing between syllables, so a gate without hold chops speech\n * into pieces — it closes inside a word and reopens on the next one. This is the\n * difference between a gate that works and one everybody turns off again.\n */\nexport const GATE_HOLD_MS = 220;\n\n/** The window each detector reads. */\nconst DETECTOR_FFT_SIZE = 1024;\n\n/**\n * Root-mean-square of a time-domain window.\n *\n * RMS rather than peak because both detectors here answer \"how much energy is\n * there\", not \"did one sample spike\": a peak reading opens a gate on a keyboard\n * click and drives a de-esser off a single transient.\n *\n * @param buffer - Time-domain samples, -1..1.\n * @returns The window's RMS, 0..1.\n */\nfunction rms(buffer: Float32Array): number {\n let sum = 0;\n for (const sample of buffer) sum += sample * sample;\n return Math.sqrt(sum / buffer.length);\n}\n\n/** Build the analyser both detectors read through. */\nfunction detectorFor(context: AudioContext): AnalyserNode {\n const analyser = context.createAnalyser();\n analyser.fftSize = DETECTOR_FFT_SIZE;\n return analyser;\n}\n\n/** A high-pass at 85 Hz: fans, traffic, desk knocks, plosives. */\nexport function highPassStage(context: AudioContext): BiquadFilterNode {\n const filter = context.createBiquadFilter();\n filter.type = \"highpass\";\n filter.frequency.value = HIGH_PASS_HZ;\n return filter;\n}\n\n/** A 3:1 leveller with a soft knee. */\nexport function compressorStage(context: AudioContext): DynamicsCompressorNode {\n const comp = context.createDynamicsCompressor();\n comp.threshold.value = COMPRESSOR.threshold;\n comp.knee.value = COMPRESSOR.knee;\n comp.ratio.value = COMPRESSOR.ratio;\n comp.attack.value = COMPRESSOR.attack;\n comp.release.value = COMPRESSOR.release;\n return comp;\n}\n\n/** A +3 dB peak at 3 kHz: speech that reads without getting louder. */\nexport function presenceStage(context: AudioContext): BiquadFilterNode {\n const eq = context.createBiquadFilter();\n eq.type = \"peaking\";\n eq.frequency.value = PRESENCE_HZ;\n eq.Q.value = PRESENCE_Q;\n eq.gain.value = PRESENCE_GAIN_DB;\n return eq;\n}\n\n/** A low-pass at 9 kHz: the hiss of a cheap microphone. */\nexport function hissCutStage(context: AudioContext): BiquadFilterNode {\n const filter = context.createBiquadFilter();\n filter.type = \"lowpass\";\n filter.frequency.value = HISS_HZ;\n return filter;\n}\n\n/** A hard 20:1 ceiling at -1 dBFS, placed after the output gain. */\nexport function limiterStage(context: AudioContext): DynamicsCompressorNode {\n const limiter = context.createDynamicsCompressor();\n limiter.threshold.value = LIMITER.threshold;\n limiter.knee.value = LIMITER.knee;\n limiter.ratio.value = LIMITER.ratio;\n limiter.attack.value = LIMITER.attack;\n limiter.release.value = LIMITER.release;\n return limiter;\n}\n\n/** The nodes a gate stage owns. */\nexport interface GateStage {\n /** Reads the signal entering the gate. */\n detector: AnalyserNode;\n /** The gain the detector drives, 0 when closed. */\n gain: GainNode;\n /** Stop the detector's timer. */\n stop: () => void;\n}\n\n/**\n * Silence the line between phrases, without cutting inside a word.\n *\n * Measured on a timer rather than in an `AudioWorklet`. A worklet would gate\n * sample-accurately at the cost of a separate module file the consumer has to\n * serve and a second graph to keep in sync; at 20 ms the difference is inaudible\n * for speech, because the hold and the release are an order of magnitude longer\n * than the polling interval and are what actually shape how a gate sounds.\n *\n * The gate starts closed and opens on the first window over the threshold, so a\n * chain built while the room is quiet does not leak the first 20 ms of it.\n *\n * @param context - The graph's context.\n * @param threshold - Read on every tick, so the caller can move a slider while\n * the chain is live rather than rebuild it.\n * @returns The stage's nodes and its teardown.\n */\nexport function gateStage(context: AudioContext, threshold: () => number): GateStage {\n const detector = detectorFor(context);\n const gain = context.createGain();\n gain.gain.value = 0;\n\n const buffer = new Float32Array(detector.fftSize);\n let lastLoudAt = 0;\n\n const timer = setInterval(() => {\n detector.getFloatTimeDomainData(buffer);\n const now = performance.now();\n if (rms(buffer) >= threshold()) lastLoudAt = now;\n const open = lastLoudAt > 0 && now - lastLoudAt < GATE_HOLD_MS;\n gain.gain.setTargetAtTime(\n open ? 1 : 0,\n context.currentTime,\n open ? GATE_ATTACK : GATE_RELEASE,\n );\n }, DETECTOR_POLL_MS);\n\n return { detector, gain, stop: () => clearInterval(timer) };\n}\n\n/** The nodes a de-esser stage owns. */\nexport interface DeEsserStage {\n /** Band-pass feeding the detector — the side-chain's input. */\n band: BiquadFilterNode;\n /** Reads 6-8 kHz only. */\n detector: AnalyserNode;\n /** The peaking filter in the signal path, whose gain rides the band. */\n shaper: BiquadFilterNode;\n /** Stop the detector's timer. */\n stop: () => void;\n}\n\n/**\n * Pull the sibilance band down while it is loud, and let go when it is not.\n *\n * Web Audio has no side-chain, so the detector is built by hand: a band-pass\n * feeds an analyser watching only 6-8 kHz, and that analyser's energy drives the\n * gain of a peaking filter sitting in the main path. The distinction is the whole\n * stage — the cut has to answer to the *band*, not to how loud the person is. A\n * static cut at the same frequency would dull every consonant instead of the\n * syllables that actually hiss.\n *\n * @param context - The graph's context.\n * @returns The stage's nodes and its teardown.\n */\nexport function deEsserStage(context: AudioContext): DeEsserStage {\n const band = context.createBiquadFilter();\n band.type = \"bandpass\";\n band.frequency.value = SIBILANCE_HZ;\n band.Q.value = SIBILANCE_Q;\n\n const detector = detectorFor(context);\n band.connect(detector);\n\n const shaper = context.createBiquadFilter();\n shaper.type = \"peaking\";\n shaper.frequency.value = SIBILANCE_HZ;\n shaper.Q.value = SIBILANCE_Q;\n shaper.gain.value = 0;\n\n const buffer = new Float32Array(detector.fftSize);\n const timer = setInterval(() => {\n detector.getFloatTimeDomainData(buffer);\n const over = Math.max(0, rms(buffer) - DEESSER_THRESHOLD);\n const cut = Math.min(DEESSER_MAX_CUT_DB, over * DEESSER_SLOPE_DB_PER_RMS);\n shaper.gain.setTargetAtTime(-cut, context.currentTime, DEESSER_SMOOTHING);\n }, DETECTOR_POLL_MS);\n\n return { band, detector, shaper, stop: () => clearInterval(timer) };\n}\n\nexport { rms as windowRms };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
//#region src/audio/voice-chain-stages.ts
|
|
2
|
+
var e = 7e3, t = {
|
|
3
|
+
threshold: -24,
|
|
4
|
+
knee: 30,
|
|
5
|
+
ratio: 3,
|
|
6
|
+
attack: .003,
|
|
7
|
+
release: .25
|
|
8
|
+
}, n = {
|
|
9
|
+
threshold: -1,
|
|
10
|
+
knee: 0,
|
|
11
|
+
ratio: 20,
|
|
12
|
+
attack: .001,
|
|
13
|
+
release: .1
|
|
14
|
+
}, r = .005, i = .12, a = 1024;
|
|
15
|
+
function o(e) {
|
|
16
|
+
let t = 0;
|
|
17
|
+
for (let n of e) t += n * n;
|
|
18
|
+
return Math.sqrt(t / e.length);
|
|
19
|
+
}
|
|
20
|
+
function s(e) {
|
|
21
|
+
let t = e.createAnalyser();
|
|
22
|
+
return t.fftSize = a, t;
|
|
23
|
+
}
|
|
24
|
+
function c(e) {
|
|
25
|
+
let t = e.createBiquadFilter();
|
|
26
|
+
return t.type = "highpass", t.frequency.value = 85, t;
|
|
27
|
+
}
|
|
28
|
+
function l(e) {
|
|
29
|
+
let n = e.createDynamicsCompressor();
|
|
30
|
+
return n.threshold.value = t.threshold, n.knee.value = t.knee, n.ratio.value = t.ratio, n.attack.value = t.attack, n.release.value = t.release, n;
|
|
31
|
+
}
|
|
32
|
+
function u(e) {
|
|
33
|
+
let t = e.createBiquadFilter();
|
|
34
|
+
return t.type = "peaking", t.frequency.value = 3e3, t.Q.value = 1, t.gain.value = 3, t;
|
|
35
|
+
}
|
|
36
|
+
function d(e) {
|
|
37
|
+
let t = e.createBiquadFilter();
|
|
38
|
+
return t.type = "lowpass", t.frequency.value = 9e3, t;
|
|
39
|
+
}
|
|
40
|
+
function f(e) {
|
|
41
|
+
let t = e.createDynamicsCompressor();
|
|
42
|
+
return t.threshold.value = n.threshold, t.knee.value = n.knee, t.ratio.value = n.ratio, t.attack.value = n.attack, t.release.value = n.release, t;
|
|
43
|
+
}
|
|
44
|
+
function p(e, t) {
|
|
45
|
+
let n = s(e), a = e.createGain();
|
|
46
|
+
a.gain.value = 0;
|
|
47
|
+
let c = new Float32Array(n.fftSize), l = 0, u = setInterval(() => {
|
|
48
|
+
n.getFloatTimeDomainData(c);
|
|
49
|
+
let s = performance.now();
|
|
50
|
+
o(c) >= t() && (l = s);
|
|
51
|
+
let u = l > 0 && s - l < 220;
|
|
52
|
+
a.gain.setTargetAtTime(+!!u, e.currentTime, u ? r : i);
|
|
53
|
+
}, 20);
|
|
54
|
+
return {
|
|
55
|
+
detector: n,
|
|
56
|
+
gain: a,
|
|
57
|
+
stop: () => clearInterval(u)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function m(t) {
|
|
61
|
+
let n = t.createBiquadFilter();
|
|
62
|
+
n.type = "bandpass", n.frequency.value = e, n.Q.value = 2;
|
|
63
|
+
let r = s(t);
|
|
64
|
+
n.connect(r);
|
|
65
|
+
let i = t.createBiquadFilter();
|
|
66
|
+
i.type = "peaking", i.frequency.value = e, i.Q.value = 2, i.gain.value = 0;
|
|
67
|
+
let a = new Float32Array(r.fftSize), c = setInterval(() => {
|
|
68
|
+
r.getFloatTimeDomainData(a);
|
|
69
|
+
let e = Math.max(0, o(a) - .02), n = Math.min(10, e * 200);
|
|
70
|
+
i.gain.setTargetAtTime(-n, t.currentTime, .02);
|
|
71
|
+
}, 20);
|
|
72
|
+
return {
|
|
73
|
+
band: n,
|
|
74
|
+
detector: r,
|
|
75
|
+
shaper: i,
|
|
76
|
+
stop: () => clearInterval(c)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { l as compressorStage, m as deEsserStage, p as gateStage, c as highPassStage, d as hissCutStage, f as limiterStage, u as presenceStage, o as windowRms };
|
|
81
|
+
|
|
82
|
+
//# sourceMappingURL=voice-chain-stages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice-chain-stages.js","names":[],"sources":["../../src/audio/voice-chain-stages.ts"],"sourcesContent":["/**\n * The individual stages a voice chain is built from, and the two side-chains\n * Web Audio cannot express declaratively.\n *\n * Split out of `voice-chain.ts` because every number here is a tuning decision\n * with a reason attached, and the file that assembles them should read as the\n * signal path rather than as a wall of constants.\n */\n\n/** Everything below this is rumble, not speech. */\nconst HIGH_PASS_HZ = 85;\n\n/** Where consonants live — the band that decides whether a word is understood. */\nconst PRESENCE_HZ = 3000;\nconst PRESENCE_GAIN_DB = 3;\nconst PRESENCE_Q = 1;\n\n/** Where microphone hiss lives, above anything speech needs. */\nconst HISS_HZ = 9000;\n\n/** The sibilance band a de-esser rides. */\nconst SIBILANCE_HZ = 7000;\nconst SIBILANCE_Q = 2;\n\n/** Deepest cut the de-esser applies, in dB. */\nconst DEESSER_MAX_CUT_DB = 10;\n\n/** Band energy above which the cut starts, as RMS. */\nconst DEESSER_THRESHOLD = 0.02;\n\n/** How steeply the cut follows the band's energy above the threshold. */\nconst DEESSER_SLOPE_DB_PER_RMS = 200;\n\n/** Seconds the de-esser takes to reach a new cut. */\nconst DEESSER_SMOOTHING = 0.02;\n\n/**\n * A leveller, not a limiter: 3:1 with a soft knee evens out the distance between\n * a whisper and a shout without audibly pumping.\n */\nconst COMPRESSOR = {\n threshold: -24,\n knee: 30,\n ratio: 3,\n attack: 0.003,\n release: 0.25,\n} as const;\n\n/**\n * The ceiling the limiter holds, in dB relative to full scale.\n *\n * Just under zero, because digital clipping is not a degradation the other end\n * can recover from — it arrives as squared-off samples that no amount of\n * processing undoes.\n */\nconst LIMITER = {\n threshold: -1,\n knee: 0,\n ratio: 20,\n attack: 0.001,\n release: 0.1,\n} as const;\n\n/** How often the gate and the de-esser look at the signal, in milliseconds. */\nexport const DETECTOR_POLL_MS = 20;\n\n/** Time constant for opening the gate. Short, so a first syllable survives. */\nconst GATE_ATTACK = 0.005;\n\n/** Time constant for closing it. Long, so a word does not end abruptly. */\nconst GATE_RELEASE = 0.12;\n\n/**\n * How long the gate stays open after the level drops.\n *\n * RMS falls to nothing between syllables, so a gate without hold chops speech\n * into pieces — it closes inside a word and reopens on the next one. This is the\n * difference between a gate that works and one everybody turns off again.\n */\nexport const GATE_HOLD_MS = 220;\n\n/** The window each detector reads. */\nconst DETECTOR_FFT_SIZE = 1024;\n\n/**\n * Root-mean-square of a time-domain window.\n *\n * RMS rather than peak because both detectors here answer \"how much energy is\n * there\", not \"did one sample spike\": a peak reading opens a gate on a keyboard\n * click and drives a de-esser off a single transient.\n *\n * @param buffer - Time-domain samples, -1..1.\n * @returns The window's RMS, 0..1.\n */\nfunction rms(buffer: Float32Array): number {\n let sum = 0;\n for (const sample of buffer) sum += sample * sample;\n return Math.sqrt(sum / buffer.length);\n}\n\n/** Build the analyser both detectors read through. */\nfunction detectorFor(context: AudioContext): AnalyserNode {\n const analyser = context.createAnalyser();\n analyser.fftSize = DETECTOR_FFT_SIZE;\n return analyser;\n}\n\n/** A high-pass at 85 Hz: fans, traffic, desk knocks, plosives. */\nexport function highPassStage(context: AudioContext): BiquadFilterNode {\n const filter = context.createBiquadFilter();\n filter.type = \"highpass\";\n filter.frequency.value = HIGH_PASS_HZ;\n return filter;\n}\n\n/** A 3:1 leveller with a soft knee. */\nexport function compressorStage(context: AudioContext): DynamicsCompressorNode {\n const comp = context.createDynamicsCompressor();\n comp.threshold.value = COMPRESSOR.threshold;\n comp.knee.value = COMPRESSOR.knee;\n comp.ratio.value = COMPRESSOR.ratio;\n comp.attack.value = COMPRESSOR.attack;\n comp.release.value = COMPRESSOR.release;\n return comp;\n}\n\n/** A +3 dB peak at 3 kHz: speech that reads without getting louder. */\nexport function presenceStage(context: AudioContext): BiquadFilterNode {\n const eq = context.createBiquadFilter();\n eq.type = \"peaking\";\n eq.frequency.value = PRESENCE_HZ;\n eq.Q.value = PRESENCE_Q;\n eq.gain.value = PRESENCE_GAIN_DB;\n return eq;\n}\n\n/** A low-pass at 9 kHz: the hiss of a cheap microphone. */\nexport function hissCutStage(context: AudioContext): BiquadFilterNode {\n const filter = context.createBiquadFilter();\n filter.type = \"lowpass\";\n filter.frequency.value = HISS_HZ;\n return filter;\n}\n\n/** A hard 20:1 ceiling at -1 dBFS, placed after the output gain. */\nexport function limiterStage(context: AudioContext): DynamicsCompressorNode {\n const limiter = context.createDynamicsCompressor();\n limiter.threshold.value = LIMITER.threshold;\n limiter.knee.value = LIMITER.knee;\n limiter.ratio.value = LIMITER.ratio;\n limiter.attack.value = LIMITER.attack;\n limiter.release.value = LIMITER.release;\n return limiter;\n}\n\n/** The nodes a gate stage owns. */\nexport interface GateStage {\n /** Reads the signal entering the gate. */\n detector: AnalyserNode;\n /** The gain the detector drives, 0 when closed. */\n gain: GainNode;\n /** Stop the detector's timer. */\n stop: () => void;\n}\n\n/**\n * Silence the line between phrases, without cutting inside a word.\n *\n * Measured on a timer rather than in an `AudioWorklet`. A worklet would gate\n * sample-accurately at the cost of a separate module file the consumer has to\n * serve and a second graph to keep in sync; at 20 ms the difference is inaudible\n * for speech, because the hold and the release are an order of magnitude longer\n * than the polling interval and are what actually shape how a gate sounds.\n *\n * The gate starts closed and opens on the first window over the threshold, so a\n * chain built while the room is quiet does not leak the first 20 ms of it.\n *\n * @param context - The graph's context.\n * @param threshold - Read on every tick, so the caller can move a slider while\n * the chain is live rather than rebuild it.\n * @returns The stage's nodes and its teardown.\n */\nexport function gateStage(context: AudioContext, threshold: () => number): GateStage {\n const detector = detectorFor(context);\n const gain = context.createGain();\n gain.gain.value = 0;\n\n const buffer = new Float32Array(detector.fftSize);\n let lastLoudAt = 0;\n\n const timer = setInterval(() => {\n detector.getFloatTimeDomainData(buffer);\n const now = performance.now();\n if (rms(buffer) >= threshold()) lastLoudAt = now;\n const open = lastLoudAt > 0 && now - lastLoudAt < GATE_HOLD_MS;\n gain.gain.setTargetAtTime(\n open ? 1 : 0,\n context.currentTime,\n open ? GATE_ATTACK : GATE_RELEASE,\n );\n }, DETECTOR_POLL_MS);\n\n return { detector, gain, stop: () => clearInterval(timer) };\n}\n\n/** The nodes a de-esser stage owns. */\nexport interface DeEsserStage {\n /** Band-pass feeding the detector — the side-chain's input. */\n band: BiquadFilterNode;\n /** Reads 6-8 kHz only. */\n detector: AnalyserNode;\n /** The peaking filter in the signal path, whose gain rides the band. */\n shaper: BiquadFilterNode;\n /** Stop the detector's timer. */\n stop: () => void;\n}\n\n/**\n * Pull the sibilance band down while it is loud, and let go when it is not.\n *\n * Web Audio has no side-chain, so the detector is built by hand: a band-pass\n * feeds an analyser watching only 6-8 kHz, and that analyser's energy drives the\n * gain of a peaking filter sitting in the main path. The distinction is the whole\n * stage — the cut has to answer to the *band*, not to how loud the person is. A\n * static cut at the same frequency would dull every consonant instead of the\n * syllables that actually hiss.\n *\n * @param context - The graph's context.\n * @returns The stage's nodes and its teardown.\n */\nexport function deEsserStage(context: AudioContext): DeEsserStage {\n const band = context.createBiquadFilter();\n band.type = \"bandpass\";\n band.frequency.value = SIBILANCE_HZ;\n band.Q.value = SIBILANCE_Q;\n\n const detector = detectorFor(context);\n band.connect(detector);\n\n const shaper = context.createBiquadFilter();\n shaper.type = \"peaking\";\n shaper.frequency.value = SIBILANCE_HZ;\n shaper.Q.value = SIBILANCE_Q;\n shaper.gain.value = 0;\n\n const buffer = new Float32Array(detector.fftSize);\n const timer = setInterval(() => {\n detector.getFloatTimeDomainData(buffer);\n const over = Math.max(0, rms(buffer) - DEESSER_THRESHOLD);\n const cut = Math.min(DEESSER_MAX_CUT_DB, over * DEESSER_SLOPE_DB_PER_RMS);\n shaper.gain.setTargetAtTime(-cut, context.currentTime, DEESSER_SMOOTHING);\n }, DETECTOR_POLL_MS);\n\n return { band, detector, shaper, stop: () => clearInterval(timer) };\n}\n\nexport { rms as windowRms };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./voice-chain-stages.cjs");var t={highPass:!0,gate:!1,gateThreshold:.02,compressor:!0,presence:!1,hissCut:!1,deEsser:!1,limiter:!0};function n(){return typeof AudioContext<`u`?AudioContext:globalThis.webkitAudioContext}function r(e,t=1){return!e.highPass&&!e.gate&&!e.compressor&&!e.presence&&!e.hissCut&&!e.deEsser&&!e.limiter&&t===1}function i(e,t){return{track:e,release:()=>{t&&e.stop()},supported:!1}}function a(a,o=t,{gain:s=1,context:c,ownsSource:l=!0}={}){if(r(o,s))return i(a,l);let u=n(),d=c??(u?new u:null);if(!d)return i(a,l);let f=d.createMediaStreamSource(typeof MediaStream>`u`?a:new MediaStream([a])),p=[f],m=[],h=f,g=e=>{h.connect(e),h=e,p.push(e)};if(o.highPass&&g(e.highPassStage(d)),o.gate){let t=e.gateStage(d,()=>o.gateThreshold);h.connect(t.detector),p.push(t.detector),m.push(t.stop),g(t.gain)}if(o.compressor&&g(e.compressorStage(d)),o.presence&&g(e.presenceStage(d)),o.hissCut&&g(e.hissCutStage(d)),o.deEsser){let t=e.deEsserStage(d);h.connect(t.band),p.push(t.band,t.detector),m.push(t.stop),g(t.shaper)}let _=d.createGain();_.gain.value=s,g(_),o.limiter&&g(e.limiterStage(d));let v=d.createMediaStreamDestination();h.connect(v),p.push(v);let y=v.stream.getAudioTracks()[0],b=()=>{for(let e of m)e();for(let e of p)e.disconnect()};return y?{track:y,release:()=>{b(),y.stop(),l&&a.stop()},supported:!0}:(b(),i(a,l))}function o(e,r=t,i={}){let o=n(),s=i.context??(o?new o:null);if(!s)return{stop:()=>void 0};let c=a(e,r,{...i,context:s,ownsSource:!1}),l=s.createMediaStreamSource(typeof MediaStream>`u`?c.track:new MediaStream([c.track]));return l.connect(s.destination),{stop:()=>{l.disconnect(),c.release()}}}exports.DEFAULT_VOICE_CHAIN=t,exports.createVoiceChain=a,exports.isVoiceChainIdle=r,exports.monitorVoiceChain=o;
|
|
2
|
+
//# sourceMappingURL=voice-chain.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice-chain.cjs","names":[],"sources":["../../src/audio/voice-chain.ts"],"sourcesContent":["import {\n compressorStage,\n deEsserStage,\n gateStage,\n highPassStage,\n hissCutStage,\n limiterStage,\n presenceStage,\n} from \"./voice-chain-stages\";\n\n/** Which stages a voice chain builds. */\nexport interface VoiceChainSettings {\n /** Cut everything below speech: fans, traffic rumble, desk knocks, plosives. */\n highPass: boolean;\n /** Silence the line when nobody is talking. */\n gate: boolean;\n /** RMS below which the gate closes, 0..1. See {@link suggestGateThreshold}. */\n gateThreshold: number;\n /** Even out the distance between a whisper and a shout. */\n compressor: boolean;\n /** Lift the consonant band so speech reads without getting louder. */\n presence: boolean;\n /** Roll off hiss above what speech uses. */\n hissCut: boolean;\n /** Ride down harsh S and CH sounds. */\n deEsser: boolean;\n /**\n * Hard ceiling before the signal leaves.\n *\n * On by default and worth leaving on: the leveller is 3:1, which controls the\n * average and lets a peak through, and `gain` can reach well above 1.\n */\n limiter: boolean;\n}\n\n/**\n * What a call wants before anybody touches a slider.\n *\n * High-pass, leveller and limiter only. Gate, presence, hiss cut and de-esser are\n * off because each has an audible cost when it is not needed — a gate set for the\n * wrong room clips words, presence on a bright microphone is harsh, and a de-esser\n * on a voice that does not hiss just dulls it.\n */\nexport const DEFAULT_VOICE_CHAIN: VoiceChainSettings = {\n highPass: true,\n gate: false,\n gateThreshold: 0.02,\n compressor: true,\n presence: false,\n hissCut: false,\n deEsser: false,\n limiter: true,\n};\n\n/** Options for {@link createVoiceChain}. */\nexport interface VoiceChainOptions {\n /** Output multiplier, applied after every stage and before the limiter. Default `1`. */\n gain?: number;\n /**\n * Reuse an existing `AudioContext` instead of creating one.\n *\n * Browsers cap live contexts (Chrome allows around six), so a page that already\n * has one — a level meter, an audio bus — should hand it over.\n */\n context?: AudioContext;\n /**\n * Whether `release()` also stops `source`. Default `true`.\n *\n * Pass `false` when the track belongs to somebody else: a settings dialog\n * building a second chain over its live preview would otherwise kill the meter\n * the person is watching while they listen.\n */\n ownsSource?: boolean;\n}\n\n/** A running voice chain. */\nexport interface VoiceChain {\n /** The processed track to publish. */\n track: MediaStreamTrack;\n /** Stop the detectors, disconnect the graph, and release the track. */\n release: () => void;\n /**\n * Whether a graph was actually built.\n *\n * `false` when this engine has no Web Audio, and also when the settings asked\n * for nothing — in both cases `track` is `source`, untouched.\n */\n readonly supported: boolean;\n}\n\n/** The `AudioContext` constructor this engine exposes, prefixed or not. */\nfunction audioContextConstructor(): typeof AudioContext | undefined {\n if (typeof AudioContext !== \"undefined\") return AudioContext;\n return (globalThis as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n}\n\n/**\n * Whether any of this would change the signal.\n *\n * A chain that only passes audio through still costs a re-encode and a handful of\n * nodes, so a caller that rebuilds on every settings change should ask first —\n * and {@link createVoiceChain} asks too, handing back `source` rather than\n * building a graph that does nothing.\n *\n * @param settings - The stages under consideration.\n * @param gain - The output multiplier that would be applied. Default `1`.\n * @returns `true` when the chain would be audibly transparent.\n */\nexport function isVoiceChainIdle(settings: VoiceChainSettings, gain = 1): boolean {\n return (\n !settings.highPass &&\n !settings.gate &&\n !settings.compressor &&\n !settings.presence &&\n !settings.hissCut &&\n !settings.deEsser &&\n !settings.limiter &&\n gain === 1\n );\n}\n\n/** A chain that owns nothing, for an engine or a settings object with no work to do. */\nfunction passThrough(source: MediaStreamTrack, ownsSource: boolean): VoiceChain {\n return {\n track: source,\n release: () => {\n if (ownsSource) source.stop();\n },\n supported: false,\n };\n}\n\n/**\n * Build the processing a microphone is published through.\n *\n * Ordered the way a console is: cut what is not speech, gate what is left, level\n * it, shape it, set how loud it goes out, then hold a ceiling. **The order is not\n * cosmetic.** Gating before the high-pass lets a rumble hold the gate open;\n * compressing before the gate lifts the noise floor up to meet the threshold; and\n * a limiter placed before the output gain is a ceiling the gain then walks\n * straight through.\n *\n * This sits *after* the browser's own echo cancellation and noise suppression,\n * which stay on: the two solve different problems. The browser's is trained on\n * stationary noise inside the capture pipeline; this cuts what it leaves behind\n * and decides when the line should be silent at all.\n *\n * @param source - The captured microphone track.\n * @param settings - Which stages to build. Default {@link DEFAULT_VOICE_CHAIN}.\n * @param options - See {@link VoiceChainOptions}.\n * @returns The track to publish plus its teardown. On an engine with no Web Audio,\n * and when every stage is off, the source track is handed back untouched and\n * `supported` is `false` — a call without processing beats a call that throws.\n * The idle case is decided **before** a context is opened, because browsers cap\n * how many can be live and one built for a chain that does nothing still counts.\n *\n * @example\n * const floor = await measureNoiseFloor(stream);\n * const chain = createVoiceChain(micTrack, {\n * ...DEFAULT_VOICE_CHAIN,\n * gate: true,\n * gateThreshold: suggestGateThreshold(floor),\n * }, { gain: 1.4 });\n * await mesh.setLocalTrack(\"mic\", chain.track);\n */\nexport function createVoiceChain(\n source: MediaStreamTrack,\n settings: VoiceChainSettings = DEFAULT_VOICE_CHAIN,\n { gain = 1, context: injectedContext, ownsSource = true }: VoiceChainOptions = {},\n): VoiceChain {\n if (isVoiceChainIdle(settings, gain)) return passThrough(source, ownsSource);\n\n const Ctor = audioContextConstructor();\n const context = injectedContext ?? (Ctor ? new Ctor() : null);\n if (!context) return passThrough(source, ownsSource);\n\n const input = context.createMediaStreamSource(\n typeof MediaStream === \"undefined\"\n ? (source as unknown as MediaStream)\n : new MediaStream([source]),\n );\n const built: AudioNode[] = [input];\n const stops: (() => void)[] = [];\n let node: AudioNode = input;\n\n const chain = (next: AudioNode): void => {\n node.connect(next);\n node = next;\n built.push(next);\n };\n\n if (settings.highPass) chain(highPassStage(context));\n\n if (settings.gate) {\n const stage = gateStage(context, () => settings.gateThreshold);\n node.connect(stage.detector);\n built.push(stage.detector);\n stops.push(stage.stop);\n chain(stage.gain);\n }\n\n if (settings.compressor) chain(compressorStage(context));\n if (settings.presence) chain(presenceStage(context));\n if (settings.hissCut) chain(hissCutStage(context));\n\n if (settings.deEsser) {\n const stage = deEsserStage(context);\n node.connect(stage.band);\n built.push(stage.band, stage.detector);\n stops.push(stage.stop);\n chain(stage.shaper);\n }\n\n const output = context.createGain();\n output.gain.value = gain;\n chain(output);\n\n if (settings.limiter) chain(limiterStage(context));\n\n const destination = context.createMediaStreamDestination();\n node.connect(destination);\n built.push(destination);\n const track = destination.stream.getAudioTracks()[0];\n\n const teardown = (): void => {\n for (const stop of stops) stop();\n for (const built_ of built) built_.disconnect();\n };\n\n if (!track) {\n teardown();\n return passThrough(source, ownsSource);\n }\n\n return {\n track,\n release: (): void => {\n teardown();\n track.stop();\n if (ownsSource) source.stop();\n },\n supported: true,\n };\n}\n\n/** A running monitor. */\nexport interface VoiceMonitor {\n /** Stop listening and release the graph. Does **not** stop the source track. */\n stop: () => void;\n}\n\n/**\n * Play your own processed microphone back to you.\n *\n * The only way to hear what these filters do without a second person on the call.\n * Every number the chain is tuned by — how deep the de-esser cuts, where the gate\n * sits, how much presence is too much — is a judgement made by ear, and without\n * this the only available ear belongs to somebody else.\n *\n * **Feedback is the hazard, and the reason this is a held, explicit action rather\n * than a setting:** without headphones the speakers feed the microphone that feeds\n * the speakers. Say so in the UI that offers it.\n *\n * @param source - The raw captured track to listen to. It is never stopped here.\n * @param settings - The stages to hear it through. Default {@link DEFAULT_VOICE_CHAIN}.\n * @param options - See {@link VoiceChainOptions}; `ownsSource` is forced to `false`.\n * @returns A handle that stops the monitor.\n */\nexport function monitorVoiceChain(\n source: MediaStreamTrack,\n settings: VoiceChainSettings = DEFAULT_VOICE_CHAIN,\n options: Omit<VoiceChainOptions, \"ownsSource\"> = {},\n): VoiceMonitor {\n const Ctor = audioContextConstructor();\n const context = options.context ?? (Ctor ? new Ctor() : null);\n if (!context) return { stop: () => undefined };\n\n const chain = createVoiceChain(source, settings, { ...options, context, ownsSource: false });\n const playback = context.createMediaStreamSource(\n typeof MediaStream === \"undefined\"\n ? (chain.track as unknown as MediaStream)\n : new MediaStream([chain.track]),\n );\n playback.connect(context.destination);\n\n return {\n stop: (): void => {\n playback.disconnect();\n chain.release();\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { compressorStage as e, deEsserStage as t, gateStage as n, highPassStage as r, hissCutStage as i, limiterStage as a, presenceStage as o } from "./voice-chain-stages.js";
|
|
2
|
+
//#region src/audio/voice-chain.ts
|
|
3
|
+
var s = {
|
|
4
|
+
highPass: !0,
|
|
5
|
+
gate: !1,
|
|
6
|
+
gateThreshold: .02,
|
|
7
|
+
compressor: !0,
|
|
8
|
+
presence: !1,
|
|
9
|
+
hissCut: !1,
|
|
10
|
+
deEsser: !1,
|
|
11
|
+
limiter: !0
|
|
12
|
+
};
|
|
13
|
+
function c() {
|
|
14
|
+
return typeof AudioContext < "u" ? AudioContext : globalThis.webkitAudioContext;
|
|
15
|
+
}
|
|
16
|
+
function l(e, t = 1) {
|
|
17
|
+
return !e.highPass && !e.gate && !e.compressor && !e.presence && !e.hissCut && !e.deEsser && !e.limiter && t === 1;
|
|
18
|
+
}
|
|
19
|
+
function u(e, t) {
|
|
20
|
+
return {
|
|
21
|
+
track: e,
|
|
22
|
+
release: () => {
|
|
23
|
+
t && e.stop();
|
|
24
|
+
},
|
|
25
|
+
supported: !1
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function d(d, f = s, { gain: p = 1, context: m, ownsSource: h = !0 } = {}) {
|
|
29
|
+
if (l(f, p)) return u(d, h);
|
|
30
|
+
let g = c(), _ = m ?? (g ? new g() : null);
|
|
31
|
+
if (!_) return u(d, h);
|
|
32
|
+
let v = _.createMediaStreamSource(typeof MediaStream > "u" ? d : new MediaStream([d])), y = [v], b = [], x = v, S = (e) => {
|
|
33
|
+
x.connect(e), x = e, y.push(e);
|
|
34
|
+
};
|
|
35
|
+
if (f.highPass && S(r(_)), f.gate) {
|
|
36
|
+
let e = n(_, () => f.gateThreshold);
|
|
37
|
+
x.connect(e.detector), y.push(e.detector), b.push(e.stop), S(e.gain);
|
|
38
|
+
}
|
|
39
|
+
if (f.compressor && S(e(_)), f.presence && S(o(_)), f.hissCut && S(i(_)), f.deEsser) {
|
|
40
|
+
let e = t(_);
|
|
41
|
+
x.connect(e.band), y.push(e.band, e.detector), b.push(e.stop), S(e.shaper);
|
|
42
|
+
}
|
|
43
|
+
let C = _.createGain();
|
|
44
|
+
C.gain.value = p, S(C), f.limiter && S(a(_));
|
|
45
|
+
let w = _.createMediaStreamDestination();
|
|
46
|
+
x.connect(w), y.push(w);
|
|
47
|
+
let T = w.stream.getAudioTracks()[0], E = () => {
|
|
48
|
+
for (let e of b) e();
|
|
49
|
+
for (let e of y) e.disconnect();
|
|
50
|
+
};
|
|
51
|
+
return T ? {
|
|
52
|
+
track: T,
|
|
53
|
+
release: () => {
|
|
54
|
+
E(), T.stop(), h && d.stop();
|
|
55
|
+
},
|
|
56
|
+
supported: !0
|
|
57
|
+
} : (E(), u(d, h));
|
|
58
|
+
}
|
|
59
|
+
function f(e, t = s, n = {}) {
|
|
60
|
+
let r = c(), i = n.context ?? (r ? new r() : null);
|
|
61
|
+
if (!i) return { stop: () => void 0 };
|
|
62
|
+
let a = d(e, t, {
|
|
63
|
+
...n,
|
|
64
|
+
context: i,
|
|
65
|
+
ownsSource: !1
|
|
66
|
+
}), o = i.createMediaStreamSource(typeof MediaStream > "u" ? a.track : new MediaStream([a.track]));
|
|
67
|
+
return o.connect(i.destination), { stop: () => {
|
|
68
|
+
o.disconnect(), a.release();
|
|
69
|
+
} };
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { s as DEFAULT_VOICE_CHAIN, d as createVoiceChain, l as isVoiceChainIdle, f as monitorVoiceChain };
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=voice-chain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice-chain.js","names":[],"sources":["../../src/audio/voice-chain.ts"],"sourcesContent":["import {\n compressorStage,\n deEsserStage,\n gateStage,\n highPassStage,\n hissCutStage,\n limiterStage,\n presenceStage,\n} from \"./voice-chain-stages\";\n\n/** Which stages a voice chain builds. */\nexport interface VoiceChainSettings {\n /** Cut everything below speech: fans, traffic rumble, desk knocks, plosives. */\n highPass: boolean;\n /** Silence the line when nobody is talking. */\n gate: boolean;\n /** RMS below which the gate closes, 0..1. See {@link suggestGateThreshold}. */\n gateThreshold: number;\n /** Even out the distance between a whisper and a shout. */\n compressor: boolean;\n /** Lift the consonant band so speech reads without getting louder. */\n presence: boolean;\n /** Roll off hiss above what speech uses. */\n hissCut: boolean;\n /** Ride down harsh S and CH sounds. */\n deEsser: boolean;\n /**\n * Hard ceiling before the signal leaves.\n *\n * On by default and worth leaving on: the leveller is 3:1, which controls the\n * average and lets a peak through, and `gain` can reach well above 1.\n */\n limiter: boolean;\n}\n\n/**\n * What a call wants before anybody touches a slider.\n *\n * High-pass, leveller and limiter only. Gate, presence, hiss cut and de-esser are\n * off because each has an audible cost when it is not needed — a gate set for the\n * wrong room clips words, presence on a bright microphone is harsh, and a de-esser\n * on a voice that does not hiss just dulls it.\n */\nexport const DEFAULT_VOICE_CHAIN: VoiceChainSettings = {\n highPass: true,\n gate: false,\n gateThreshold: 0.02,\n compressor: true,\n presence: false,\n hissCut: false,\n deEsser: false,\n limiter: true,\n};\n\n/** Options for {@link createVoiceChain}. */\nexport interface VoiceChainOptions {\n /** Output multiplier, applied after every stage and before the limiter. Default `1`. */\n gain?: number;\n /**\n * Reuse an existing `AudioContext` instead of creating one.\n *\n * Browsers cap live contexts (Chrome allows around six), so a page that already\n * has one — a level meter, an audio bus — should hand it over.\n */\n context?: AudioContext;\n /**\n * Whether `release()` also stops `source`. Default `true`.\n *\n * Pass `false` when the track belongs to somebody else: a settings dialog\n * building a second chain over its live preview would otherwise kill the meter\n * the person is watching while they listen.\n */\n ownsSource?: boolean;\n}\n\n/** A running voice chain. */\nexport interface VoiceChain {\n /** The processed track to publish. */\n track: MediaStreamTrack;\n /** Stop the detectors, disconnect the graph, and release the track. */\n release: () => void;\n /**\n * Whether a graph was actually built.\n *\n * `false` when this engine has no Web Audio, and also when the settings asked\n * for nothing — in both cases `track` is `source`, untouched.\n */\n readonly supported: boolean;\n}\n\n/** The `AudioContext` constructor this engine exposes, prefixed or not. */\nfunction audioContextConstructor(): typeof AudioContext | undefined {\n if (typeof AudioContext !== \"undefined\") return AudioContext;\n return (globalThis as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n}\n\n/**\n * Whether any of this would change the signal.\n *\n * A chain that only passes audio through still costs a re-encode and a handful of\n * nodes, so a caller that rebuilds on every settings change should ask first —\n * and {@link createVoiceChain} asks too, handing back `source` rather than\n * building a graph that does nothing.\n *\n * @param settings - The stages under consideration.\n * @param gain - The output multiplier that would be applied. Default `1`.\n * @returns `true` when the chain would be audibly transparent.\n */\nexport function isVoiceChainIdle(settings: VoiceChainSettings, gain = 1): boolean {\n return (\n !settings.highPass &&\n !settings.gate &&\n !settings.compressor &&\n !settings.presence &&\n !settings.hissCut &&\n !settings.deEsser &&\n !settings.limiter &&\n gain === 1\n );\n}\n\n/** A chain that owns nothing, for an engine or a settings object with no work to do. */\nfunction passThrough(source: MediaStreamTrack, ownsSource: boolean): VoiceChain {\n return {\n track: source,\n release: () => {\n if (ownsSource) source.stop();\n },\n supported: false,\n };\n}\n\n/**\n * Build the processing a microphone is published through.\n *\n * Ordered the way a console is: cut what is not speech, gate what is left, level\n * it, shape it, set how loud it goes out, then hold a ceiling. **The order is not\n * cosmetic.** Gating before the high-pass lets a rumble hold the gate open;\n * compressing before the gate lifts the noise floor up to meet the threshold; and\n * a limiter placed before the output gain is a ceiling the gain then walks\n * straight through.\n *\n * This sits *after* the browser's own echo cancellation and noise suppression,\n * which stay on: the two solve different problems. The browser's is trained on\n * stationary noise inside the capture pipeline; this cuts what it leaves behind\n * and decides when the line should be silent at all.\n *\n * @param source - The captured microphone track.\n * @param settings - Which stages to build. Default {@link DEFAULT_VOICE_CHAIN}.\n * @param options - See {@link VoiceChainOptions}.\n * @returns The track to publish plus its teardown. On an engine with no Web Audio,\n * and when every stage is off, the source track is handed back untouched and\n * `supported` is `false` — a call without processing beats a call that throws.\n * The idle case is decided **before** a context is opened, because browsers cap\n * how many can be live and one built for a chain that does nothing still counts.\n *\n * @example\n * const floor = await measureNoiseFloor(stream);\n * const chain = createVoiceChain(micTrack, {\n * ...DEFAULT_VOICE_CHAIN,\n * gate: true,\n * gateThreshold: suggestGateThreshold(floor),\n * }, { gain: 1.4 });\n * await mesh.setLocalTrack(\"mic\", chain.track);\n */\nexport function createVoiceChain(\n source: MediaStreamTrack,\n settings: VoiceChainSettings = DEFAULT_VOICE_CHAIN,\n { gain = 1, context: injectedContext, ownsSource = true }: VoiceChainOptions = {},\n): VoiceChain {\n if (isVoiceChainIdle(settings, gain)) return passThrough(source, ownsSource);\n\n const Ctor = audioContextConstructor();\n const context = injectedContext ?? (Ctor ? new Ctor() : null);\n if (!context) return passThrough(source, ownsSource);\n\n const input = context.createMediaStreamSource(\n typeof MediaStream === \"undefined\"\n ? (source as unknown as MediaStream)\n : new MediaStream([source]),\n );\n const built: AudioNode[] = [input];\n const stops: (() => void)[] = [];\n let node: AudioNode = input;\n\n const chain = (next: AudioNode): void => {\n node.connect(next);\n node = next;\n built.push(next);\n };\n\n if (settings.highPass) chain(highPassStage(context));\n\n if (settings.gate) {\n const stage = gateStage(context, () => settings.gateThreshold);\n node.connect(stage.detector);\n built.push(stage.detector);\n stops.push(stage.stop);\n chain(stage.gain);\n }\n\n if (settings.compressor) chain(compressorStage(context));\n if (settings.presence) chain(presenceStage(context));\n if (settings.hissCut) chain(hissCutStage(context));\n\n if (settings.deEsser) {\n const stage = deEsserStage(context);\n node.connect(stage.band);\n built.push(stage.band, stage.detector);\n stops.push(stage.stop);\n chain(stage.shaper);\n }\n\n const output = context.createGain();\n output.gain.value = gain;\n chain(output);\n\n if (settings.limiter) chain(limiterStage(context));\n\n const destination = context.createMediaStreamDestination();\n node.connect(destination);\n built.push(destination);\n const track = destination.stream.getAudioTracks()[0];\n\n const teardown = (): void => {\n for (const stop of stops) stop();\n for (const built_ of built) built_.disconnect();\n };\n\n if (!track) {\n teardown();\n return passThrough(source, ownsSource);\n }\n\n return {\n track,\n release: (): void => {\n teardown();\n track.stop();\n if (ownsSource) source.stop();\n },\n supported: true,\n };\n}\n\n/** A running monitor. */\nexport interface VoiceMonitor {\n /** Stop listening and release the graph. Does **not** stop the source track. */\n stop: () => void;\n}\n\n/**\n * Play your own processed microphone back to you.\n *\n * The only way to hear what these filters do without a second person on the call.\n * Every number the chain is tuned by — how deep the de-esser cuts, where the gate\n * sits, how much presence is too much — is a judgement made by ear, and without\n * this the only available ear belongs to somebody else.\n *\n * **Feedback is the hazard, and the reason this is a held, explicit action rather\n * than a setting:** without headphones the speakers feed the microphone that feeds\n * the speakers. Say so in the UI that offers it.\n *\n * @param source - The raw captured track to listen to. It is never stopped here.\n * @param settings - The stages to hear it through. Default {@link DEFAULT_VOICE_CHAIN}.\n * @param options - See {@link VoiceChainOptions}; `ownsSource` is forced to `false`.\n * @returns A handle that stops the monitor.\n */\nexport function monitorVoiceChain(\n source: MediaStreamTrack,\n settings: VoiceChainSettings = DEFAULT_VOICE_CHAIN,\n options: Omit<VoiceChainOptions, \"ownsSource\"> = {},\n): VoiceMonitor {\n const Ctor = audioContextConstructor();\n const context = options.context ?? (Ctor ? new Ctor() : null);\n if (!context) return { stop: () => undefined };\n\n const chain = createVoiceChain(source, settings, { ...options, context, ownsSource: false });\n const playback = context.createMediaStreamSource(\n typeof MediaStream === \"undefined\"\n ? (chain.track as unknown as MediaStream)\n : new MediaStream([chain.track]),\n );\n playback.connect(context.destination);\n\n return {\n stop: (): void => {\n playback.disconnect();\n chain.release();\n },\n };\n}\n"],"mappings":";;AA2CA,IAAa,IAA0C;CACnD,UAAU;CACV,MAAM;CACN,eAAe;CACf,YAAY;CACZ,UAAU;CACV,SAAS;CACT,SAAS;CACT,SAAS;AACb;AAuCA,SAAS,IAA2D;CAEhE,OADI,OAAO,eAAiB,MAAoB,eACxC,WAA4D;AACxE;AAcA,SAAgB,EAAiB,GAA8B,IAAO,GAAY;CAC9E,OACI,CAAC,EAAS,YACV,CAAC,EAAS,QACV,CAAC,EAAS,cACV,CAAC,EAAS,YACV,CAAC,EAAS,WACV,CAAC,EAAS,WACV,CAAC,EAAS,WACV,MAAS;AAEjB;AAGA,SAAS,EAAY,GAA0B,GAAiC;CAC5E,OAAO;EACH,OAAO;EACP,eAAe;GACX,AAAI,KAAY,EAAO,KAAK;EAChC;EACA,WAAW;CACf;AACJ;AAmCA,SAAgB,EACZ,GACA,IAA+B,GAC/B,EAAE,UAAO,GAAG,SAAS,GAAiB,gBAAa,OAA4B,CAAC,GACtE;CACV,IAAI,EAAiB,GAAU,CAAI,GAAG,OAAO,EAAY,GAAQ,CAAU;CAE3E,IAAM,IAAO,EAAwB,GAC/B,IAAU,MAAoB,IAAO,IAAI,EAAK,IAAI;CACxD,IAAI,CAAC,GAAS,OAAO,EAAY,GAAQ,CAAU;CAEnD,IAAM,IAAQ,EAAQ,wBAClB,OAAO,cAAgB,MAChB,IACD,IAAI,YAAY,CAAC,CAAM,CAAC,CAClC,GACM,IAAqB,CAAC,CAAK,GAC3B,IAAwB,CAAC,GAC3B,IAAkB,GAEhB,KAAS,MAA0B;EAGrC,AAFA,EAAK,QAAQ,CAAI,GACjB,IAAO,GACP,EAAM,KAAK,CAAI;CACnB;CAIA,IAFI,EAAS,YAAU,EAAM,EAAc,CAAO,CAAC,GAE/C,EAAS,MAAM;EACf,IAAM,IAAQ,EAAU,SAAe,EAAS,aAAa;EAI7D,AAHA,EAAK,QAAQ,EAAM,QAAQ,GAC3B,EAAM,KAAK,EAAM,QAAQ,GACzB,EAAM,KAAK,EAAM,IAAI,GACrB,EAAM,EAAM,IAAI;CACpB;CAMA,IAJI,EAAS,cAAY,EAAM,EAAgB,CAAO,CAAC,GACnD,EAAS,YAAU,EAAM,EAAc,CAAO,CAAC,GAC/C,EAAS,WAAS,EAAM,EAAa,CAAO,CAAC,GAE7C,EAAS,SAAS;EAClB,IAAM,IAAQ,EAAa,CAAO;EAIlC,AAHA,EAAK,QAAQ,EAAM,IAAI,GACvB,EAAM,KAAK,EAAM,MAAM,EAAM,QAAQ,GACrC,EAAM,KAAK,EAAM,IAAI,GACrB,EAAM,EAAM,MAAM;CACtB;CAEA,IAAM,IAAS,EAAQ,WAAW;CAIlC,AAHA,EAAO,KAAK,QAAQ,GACpB,EAAM,CAAM,GAER,EAAS,WAAS,EAAM,EAAa,CAAO,CAAC;CAEjD,IAAM,IAAc,EAAQ,6BAA6B;CAEzD,AADA,EAAK,QAAQ,CAAW,GACxB,EAAM,KAAK,CAAW;CACtB,IAAM,IAAQ,EAAY,OAAO,eAAe,CAAC,CAAC,IAE5C,UAAuB;EACzB,KAAK,IAAM,KAAQ,GAAO,EAAK;EAC/B,KAAK,IAAM,KAAU,GAAO,EAAO,WAAW;CAClD;CAOA,OALK,IAKE;EACH;EACA,eAAqB;GAGjB,AAFA,EAAS,GACT,EAAM,KAAK,GACP,KAAY,EAAO,KAAK;EAChC;EACA,WAAW;CACf,KAZI,EAAS,GACF,EAAY,GAAQ,CAAU;AAY7C;AAyBA,SAAgB,EACZ,GACA,IAA+B,GAC/B,IAAiD,CAAC,GACtC;CACZ,IAAM,IAAO,EAAwB,GAC/B,IAAU,EAAQ,YAAY,IAAO,IAAI,EAAK,IAAI;CACxD,IAAI,CAAC,GAAS,OAAO,EAAE,YAAY,KAAA,EAAU;CAE7C,IAAM,IAAQ,EAAiB,GAAQ,GAAU;EAAE,GAAG;EAAS;EAAS,YAAY;CAAM,CAAC,GACrF,IAAW,EAAQ,wBACrB,OAAO,cAAgB,MAChB,EAAM,QACP,IAAI,YAAY,CAAC,EAAM,KAAK,CAAC,CACvC;CAGA,OAFA,EAAS,QAAQ,EAAQ,WAAW,GAE7B,EACH,YAAkB;EAEd,AADA,EAAS,WAAW,GACpB,EAAM,QAAQ;CAClB,EACJ;AACJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lazy-with-retry.cjs","names":[],"sources":["../../src/auth/lazy-with-retry.ts"],"sourcesContent":["import { lazy } from \"react\";\nimport type { ComponentType } from \"react\";\n\nexport interface LazyWithRetryOptions {\n /** Max attempts. Default: 3. */\n retries?: number;\n /** Initial delay (ms) before retrying. Default: 400. */\n initialDelay?: number;\n /**\n * Reload the page after every retry fails. Helps when the stale chunk\n * error is caused by an old `index.html` referencing deleted bundles.\n * Default: true.\n */\n reloadOnFinalFailure?: boolean;\n}\n\n/** A lazy component that can also be fetched ahead of being rendered. */\nexport type PreloadableLazy<T extends
|
|
1
|
+
{"version":3,"file":"lazy-with-retry.cjs","names":[],"sources":["../../src/auth/lazy-with-retry.ts"],"sourcesContent":["import { lazy } from \"react\";\nimport type { ComponentType } from \"react\";\n\n/**\n * Any React component, whatever props it declares.\n *\n * Props sit in a *parameter* position, so a bound over them is contravariant:\n * `ComponentType<unknown>` reads as \"a component that accepts every possible\n * props object\", and only a component that declares no props at all satisfies\n * it. A page typed `({ mode }: Props) => …` is rejected, and so is one whose\n * props are entirely optional. `any` is the only bound that admits every\n * component — which is why React itself declares `lazy` and\n * `LazyExoticComponent` as `ComponentType<any>`. `ComponentType<never>` is not\n * an escape: it fails React's own bound through\n * `ComponentClass.getDerivedStateFromProps`, where the props land back in a\n * covariant position.\n *\n * This relaxes the *bound* only. `T` is still inferred as the concrete\n * component, so the rendered element keeps checking its props and `preload()`\n * still resolves to the concrete module.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyComponent = ComponentType<any>;\n\nexport interface LazyWithRetryOptions {\n /** Max attempts. Default: 3. */\n retries?: number;\n /** Initial delay (ms) before retrying. Default: 400. */\n initialDelay?: number;\n /**\n * Reload the page after every retry fails. Helps when the stale chunk\n * error is caused by an old `index.html` referencing deleted bundles.\n * Default: true.\n */\n reloadOnFinalFailure?: boolean;\n}\n\n/** A lazy component that can also be fetched ahead of being rendered. */\nexport type PreloadableLazy<T extends AnyComponent> = ReturnType<typeof lazy<T>> & {\n /**\n * Start fetching the chunk now, before anything renders it.\n *\n * Call it on the interaction that makes the route likely — hovering the\n * link, opening the menu that holds it, finishing the step before it — so\n * the chunk is warm by the time the user commits and the suspense fallback\n * never appears.\n *\n * Shares its work with the render path: whichever fires first performs the\n * single fetch and the other awaits the same promise. Safe to call\n * repeatedly.\n *\n * @returns The module, once loaded. Rejects when every retry failed; that\n * rejection is already handled internally, so a fire-and-forget call\n * never surfaces as an unhandled rejection.\n */\n preload: () => Promise<{ default: T }>;\n};\n\n/**\n * Wrap `React.lazy` with automatic retry and a `preload()` method.\n *\n * Common cause of failure: deployed-then-cached `index.html` references chunk\n * filenames that no longer exist. Retrying after a short delay typically picks\n * up the new bundle; a final `location.reload()` recovers from stale\n * `index.html`.\n *\n * The wrapped component keeps its own props: `T` is inferred from the module\n * the factory resolves to, so a page declaring required, optional or no props\n * all pass through and the rendered element is still checked against them. See\n * {@link AnyComponent} for why the constraint has to be written the way it is.\n *\n * @param factory Dynamic import of the module whose `default` is the component.\n * @param options Retry count, initial backoff and the final-failure reload.\n * @returns The lazy component, carrying an extra `preload()`.\n *\n * @example\n * const Settings = lazyWithRetry(() => import(\"./Settings\"));\n *\n * // Warm the chunk when the route becomes likely, not when it is needed.\n * <a href=\"/settings\" onMouseEnter={() => void Settings.preload()}>Settings</a>\n */\nexport function lazyWithRetry<T extends AnyComponent>(\n factory: () => Promise<{ default: T }>,\n options: LazyWithRetryOptions = {},\n): PreloadableLazy<T> {\n const { retries = 3, initialDelay = 400, reloadOnFinalFailure = true } = options;\n\n async function load(attempt = 1): Promise<{ default: T }> {\n try {\n return await factory();\n } catch (error) {\n if (attempt >= retries) {\n if (reloadOnFinalFailure && typeof window !== \"undefined\") {\n window.location.reload();\n }\n throw error;\n }\n await new Promise((resolve) => setTimeout(resolve, initialDelay * 2 ** (attempt - 1)));\n return load(attempt + 1);\n }\n }\n\n let pending: Promise<{ default: T }> | null = null;\n\n /**\n * Run `load` at most once at a time, so preloading and rendering never\n * fetch the same chunk twice.\n *\n * The internal `catch` does two jobs: it clears the memo, so a component\n * that failed can be tried again once an error boundary resets, and it\n * marks the promise as handled, so a `preload()` nobody awaited does not\n * raise an unhandled rejection. Callers still get a promise that rejects.\n */\n function loadOnce(): Promise<{ default: T }> {\n if (!pending) {\n pending = load();\n pending.catch(() => {\n pending = null;\n });\n }\n return pending;\n }\n\n const component = lazy(loadOnce) as PreloadableLazy<T>;\n component.preload = loadOnce;\n return component;\n}\n"],"mappings":"uBAiFA,SAAgB,EACZ,EACA,EAAgC,CAAC,EACf,CAClB,GAAM,CAAE,UAAU,EAAG,eAAe,IAAK,uBAAuB,IAAS,EAEzE,eAAe,EAAK,EAAU,EAA4B,CACtD,GAAI,CACA,OAAO,MAAM,EAAQ,CACzB,OAAS,EAAO,CACZ,GAAI,GAAW,EAIX,MAHI,GAAwB,OAAO,OAAW,KAC1C,OAAO,SAAS,OAAO,EAErB,EAGV,OADA,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,EAAe,IAAM,EAAU,EAAE,CAAC,EAC9E,EAAK,EAAU,CAAC,CAC3B,CACJ,CAEA,IAAI,EAA0C,KAW9C,SAAS,GAAoC,CAOzC,OANK,IACD,EAAU,EAAK,EACf,EAAQ,UAAY,CAChB,EAAU,IACd,CAAC,GAEE,CACX,CAEA,IAAM,GAAA,EAAY,EAAA,KAAA,CAAK,CAAQ,EAE/B,MADA,GAAU,QAAU,EACb,CACX"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lazy-with-retry.js","names":[],"sources":["../../src/auth/lazy-with-retry.ts"],"sourcesContent":["import { lazy } from \"react\";\nimport type { ComponentType } from \"react\";\n\nexport interface LazyWithRetryOptions {\n /** Max attempts. Default: 3. */\n retries?: number;\n /** Initial delay (ms) before retrying. Default: 400. */\n initialDelay?: number;\n /**\n * Reload the page after every retry fails. Helps when the stale chunk\n * error is caused by an old `index.html` referencing deleted bundles.\n * Default: true.\n */\n reloadOnFinalFailure?: boolean;\n}\n\n/** A lazy component that can also be fetched ahead of being rendered. */\nexport type PreloadableLazy<T extends
|
|
1
|
+
{"version":3,"file":"lazy-with-retry.js","names":[],"sources":["../../src/auth/lazy-with-retry.ts"],"sourcesContent":["import { lazy } from \"react\";\nimport type { ComponentType } from \"react\";\n\n/**\n * Any React component, whatever props it declares.\n *\n * Props sit in a *parameter* position, so a bound over them is contravariant:\n * `ComponentType<unknown>` reads as \"a component that accepts every possible\n * props object\", and only a component that declares no props at all satisfies\n * it. A page typed `({ mode }: Props) => …` is rejected, and so is one whose\n * props are entirely optional. `any` is the only bound that admits every\n * component — which is why React itself declares `lazy` and\n * `LazyExoticComponent` as `ComponentType<any>`. `ComponentType<never>` is not\n * an escape: it fails React's own bound through\n * `ComponentClass.getDerivedStateFromProps`, where the props land back in a\n * covariant position.\n *\n * This relaxes the *bound* only. `T` is still inferred as the concrete\n * component, so the rendered element keeps checking its props and `preload()`\n * still resolves to the concrete module.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyComponent = ComponentType<any>;\n\nexport interface LazyWithRetryOptions {\n /** Max attempts. Default: 3. */\n retries?: number;\n /** Initial delay (ms) before retrying. Default: 400. */\n initialDelay?: number;\n /**\n * Reload the page after every retry fails. Helps when the stale chunk\n * error is caused by an old `index.html` referencing deleted bundles.\n * Default: true.\n */\n reloadOnFinalFailure?: boolean;\n}\n\n/** A lazy component that can also be fetched ahead of being rendered. */\nexport type PreloadableLazy<T extends AnyComponent> = ReturnType<typeof lazy<T>> & {\n /**\n * Start fetching the chunk now, before anything renders it.\n *\n * Call it on the interaction that makes the route likely — hovering the\n * link, opening the menu that holds it, finishing the step before it — so\n * the chunk is warm by the time the user commits and the suspense fallback\n * never appears.\n *\n * Shares its work with the render path: whichever fires first performs the\n * single fetch and the other awaits the same promise. Safe to call\n * repeatedly.\n *\n * @returns The module, once loaded. Rejects when every retry failed; that\n * rejection is already handled internally, so a fire-and-forget call\n * never surfaces as an unhandled rejection.\n */\n preload: () => Promise<{ default: T }>;\n};\n\n/**\n * Wrap `React.lazy` with automatic retry and a `preload()` method.\n *\n * Common cause of failure: deployed-then-cached `index.html` references chunk\n * filenames that no longer exist. Retrying after a short delay typically picks\n * up the new bundle; a final `location.reload()` recovers from stale\n * `index.html`.\n *\n * The wrapped component keeps its own props: `T` is inferred from the module\n * the factory resolves to, so a page declaring required, optional or no props\n * all pass through and the rendered element is still checked against them. See\n * {@link AnyComponent} for why the constraint has to be written the way it is.\n *\n * @param factory Dynamic import of the module whose `default` is the component.\n * @param options Retry count, initial backoff and the final-failure reload.\n * @returns The lazy component, carrying an extra `preload()`.\n *\n * @example\n * const Settings = lazyWithRetry(() => import(\"./Settings\"));\n *\n * // Warm the chunk when the route becomes likely, not when it is needed.\n * <a href=\"/settings\" onMouseEnter={() => void Settings.preload()}>Settings</a>\n */\nexport function lazyWithRetry<T extends AnyComponent>(\n factory: () => Promise<{ default: T }>,\n options: LazyWithRetryOptions = {},\n): PreloadableLazy<T> {\n const { retries = 3, initialDelay = 400, reloadOnFinalFailure = true } = options;\n\n async function load(attempt = 1): Promise<{ default: T }> {\n try {\n return await factory();\n } catch (error) {\n if (attempt >= retries) {\n if (reloadOnFinalFailure && typeof window !== \"undefined\") {\n window.location.reload();\n }\n throw error;\n }\n await new Promise((resolve) => setTimeout(resolve, initialDelay * 2 ** (attempt - 1)));\n return load(attempt + 1);\n }\n }\n\n let pending: Promise<{ default: T }> | null = null;\n\n /**\n * Run `load` at most once at a time, so preloading and rendering never\n * fetch the same chunk twice.\n *\n * The internal `catch` does two jobs: it clears the memo, so a component\n * that failed can be tried again once an error boundary resets, and it\n * marks the promise as handled, so a `preload()` nobody awaited does not\n * raise an unhandled rejection. Callers still get a promise that rejects.\n */\n function loadOnce(): Promise<{ default: T }> {\n if (!pending) {\n pending = load();\n pending.catch(() => {\n pending = null;\n });\n }\n return pending;\n }\n\n const component = lazy(loadOnce) as PreloadableLazy<T>;\n component.preload = loadOnce;\n return component;\n}\n"],"mappings":";;AAiFA,SAAgB,EACZ,GACA,IAAgC,CAAC,GACf;CAClB,IAAM,EAAE,aAAU,GAAG,kBAAe,KAAK,0BAAuB,OAAS;CAEzE,eAAe,EAAK,IAAU,GAA4B;EACtD,IAAI;GACA,OAAO,MAAM,EAAQ;EACzB,SAAS,GAAO;GACZ,IAAI,KAAW,GAIX,MAHI,KAAwB,OAAO,SAAW,OAC1C,OAAO,SAAS,OAAO,GAErB;GAGV,OADA,MAAM,IAAI,SAAS,MAAY,WAAW,GAAS,IAAe,MAAM,IAAU,EAAE,CAAC,GAC9E,EAAK,IAAU,CAAC;EAC3B;CACJ;CAEA,IAAI,IAA0C;CAW9C,SAAS,IAAoC;EAOzC,OANK,MACD,IAAU,EAAK,GACf,EAAQ,YAAY;GAChB,IAAU;EACd,CAAC,IAEE;CACX;CAEA,IAAM,IAAY,EAAK,CAAQ;CAE/B,OADA,EAAU,UAAU,GACb;AACX"}
|