tempest-react-sdk 0.53.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 +27 -6
- 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/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/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/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/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/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/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 +904 -41
- package/dist/tempest-react-sdk.js +293 -286
- package/dist/utils/dev-mode.cjs.map +1 -1
- package/dist/utils/dev-mode.js.map +1 -1
- 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/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 +81 -85
- package/dist/ws/create-web-socket.js.map +1 -1
- 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 +42 -48
- package/dist/ws/use-web-socket.js.map +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -202,6 +202,16 @@ This injects the design tokens (`--tempest-primary`, `--tempest-radius-md`, ...)
|
|
|
202
202
|
|
|
203
203
|
The styles ship hashed under the `tempest_` namespace — they do **not** collide with Tailwind, Stitches, Linaria, or app-level CSS Modules.
|
|
204
204
|
|
|
205
|
+
**Importing less.** The JavaScript you import is tree-shaken; the CSS is not, so `styles.css` carries all ~150 components whether you mount thirteen or all of them. Pay for what you use by taking the foundation plus the sheets you need:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import "tempest-react-sdk/styles/core.css"; // reset + tokens — always required
|
|
209
|
+
import "tempest-react-sdk/styles/Button.css";
|
|
210
|
+
import "tempest-react-sdk/styles/forms.css"; // or a whole family
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Measured in a real Vite app mounting twelve components: **236.71 kB raw / 35.38 kB gzip** with `styles.css`, **38.94 kB / 7.70 kB** with `core.css` plus those twelve — 78% less. See [Styles](https://mauriciobenjamin700.github.io/tempest-react-sdk/styles/) for the full list of groups.
|
|
214
|
+
|
|
205
215
|
**Rebranding.** The `--tempest-*` tokens are the only theming API, and `createTheme` writes them for you — `primary` alone yields the ten-step scale for **both** color schemes (dark inverts the ramp), plus the status families, the radius scale and the focus ring:
|
|
206
216
|
|
|
207
217
|
```ts
|
|
@@ -240,9 +250,9 @@ Every module is re-exported from the package root — `import { Button, useDebou
|
|
|
240
250
|
| `app` | `AppProviders` (composes `ErrorBoundary` → `QueryProvider` → `ThemeProvider` → `I18nProvider`), type: `AppProvidersProps` |
|
|
241
251
|
| `vite` _(subpath `tempest-react-sdk/vite`)_ | `createViteConfig`, `tempestPwaManifest` (emits `precache-manifest.json` for offline precache), `tempestPwaIcons` (generates the PNG icon set from one SVG via `sharp`), `tempestPwaDevSw` (serves the SW under `npm run dev`), `tempestPwaIcons({ appleSplash })` (Apple splash screens), types: `CreateViteConfigOptions`, `ProxyEntry`, `TempestViteConfig`, `TempestPwaManifestOptions`, `TempestPwaIconsOptions`, `TempestPwaDevSwOptions`, `AppleSplashSpec`, `TempestVitePlugin` |
|
|
242
252
|
| `forms` _(peer: `zod`, `react-hook-form`)_ | `validateForm`, `zodResolver`, `useZodForm`, `validateCPF`, `validateCNPJ`, `formatCEP`, `formatCNPJ`, `unmask`, `CPFInput`, `CNPJInput`, `PhoneInput`, `CEPInput`, `MoneyInput`, `useViaCEP` |
|
|
243
|
-
| `sse` | `createEventStream`, `useEventStream`
|
|
244
|
-
| `ws` | `createWebSocket` (handshake timeout, silence watchdog, jittered backoff, `navigator.onLine` suspend, close-code classification, `opened` promise), `useWebSocket`, `isRejectionCloseCode`, `HEARTBEAT_CLOSE_CODE`
|
|
245
|
-
| `webrtc` | `tuneOpus` (Opus `fmtp` per audio m-line: per-key merge, `stereo` + `sprop-stereo`, inserts a missing `fmtp`), `setTunedLocalDescription` (falls back to the untouched SDP when the browser refuses the edit), `setSenderBitrate`
|
|
253
|
+
| `sse` | `createEventStream`, `useEventStream` (`onParseError` para frame que não é JSON) |
|
|
254
|
+
| `ws` | `createWebSocket` (handshake timeout, silence watchdog, jittered backoff, `navigator.onLine` suspend, close-code classification, `opened` promise), `useWebSocket`, `onParseError` (frame que não é JSON: descarta em vez de entregar `string` como `T`), `isRejectionCloseCode`, `HEARTBEAT_CLOSE_CODE` |
|
|
255
|
+
| `webrtc` | `tuneOpus` (Opus `fmtp` per audio m-line: per-key merge, `stereo` + `sprop-stereo`, inserts a missing `fmtp`), `setTunedLocalDescription` (falls back to the untouched SDP when the browser refuses the edit), `setSenderBitrate`, `createLinkStatsSampler` / `useLinkStats` / `readRoundTripMs` (throughput as a delta, round trip from the **selected** candidate pair) |
|
|
246
256
|
| `geo` _(opt. peer `leaflet`)_ | `haversineKm`, `pathLengthKm`, `bearingDeg`, `estimateTravel`, `createOSRMBackend`, `boundingBox`, `projectMercator`, `fitProjection`, `createPositionTracker`, `usePositionTracker`, `TrajectoryMap` — tile-free SVG map + trajectory tracking (no external/paid API; Leaflet is an opt-in, lazy-loaded tile layer for self-hosted tiles), types: `Coordinate`, `TrackPoint`, `TravelEstimate`, `TravelMode`, `GeoBounds`, `RoutingBackend` |
|
|
247
257
|
| `br` _(subpath `tempest-react-sdk/br`)_ | `BrazilMap`, `BrazilStateMap`, `BrazilStateCitySelect`, `listStates`, `getState`, `citiesByUf`, `statesByRegion`, `ufChoices`, `cityChoices`, `isValidUf`, `normalizeUf`, `isValidCity`, `loadBrUfGeoJson`, `loadStateMunicipalities`, types `UF`, `BrRegion`, `BrazilState` — clickable 27-UF SVG map + per-state municipality submaps (bundled simplified IBGE GeoJSON, lazy per UF) + states/cities dataset, no external API; mirrors the FastAPI SDK `utils/locations` |
|
|
248
258
|
| `push` | `WebPushClient`, `WebPushUnsupportedError`, `WebPushPermissionDeniedError`, `usePushSubscription`, `urlBase64ToUint8Array`, `isPushSupported` |
|
|
@@ -275,9 +285,9 @@ Every module is re-exported from the package root — `import { Button, useDebou
|
|
|
275
285
|
| `app` | `AppProviders` (composes `ErrorBoundary` → `QueryProvider` → `ThemeProvider` → `I18nProvider`), type: `AppProvidersProps` |
|
|
276
286
|
| `vite` _(subpath `tempest-react-sdk/vite`)_ | `createViteConfig`, `tempestPwaManifest` (emits `precache-manifest.json` for offline precache), `tempestPwaIcons` (generates the PNG icon set from one SVG via `sharp`), `tempestPwaDevSw` (serves the SW under `npm run dev`), `tempestPwaIcons({ appleSplash })` (Apple splash screens), types: `CreateViteConfigOptions`, `ProxyEntry`, `TempestViteConfig`, `TempestPwaManifestOptions`, `TempestPwaIconsOptions`, `TempestPwaDevSwOptions`, `AppleSplashSpec`, `TempestVitePlugin` |
|
|
277
287
|
| `forms` _(peer: `zod`, `react-hook-form`)_ | `validateForm`, `zodResolver`, `useZodForm`, `validateCPF`, `validateCNPJ`, `formatCEP`, `formatCNPJ`, `unmask`, `CPFInput`, `CNPJInput`, `PhoneInput`, `CEPInput`, `MoneyInput`, `useViaCEP` |
|
|
278
|
-
| `sse` | `createEventStream`, `useEventStream`
|
|
279
|
-
| `ws` | `createWebSocket` (handshake timeout, silence watchdog, jittered backoff, `navigator.onLine` suspend, close-code classification, `opened` promise), `useWebSocket`, `isRejectionCloseCode`, `HEARTBEAT_CLOSE_CODE`
|
|
280
|
-
| `webrtc` | `tuneOpus` (Opus `fmtp` per audio m-line: per-key merge, `stereo` + `sprop-stereo`, inserts a missing `fmtp`), `setTunedLocalDescription` (falls back to the untouched SDP when the browser refuses the edit), `setSenderBitrate`
|
|
288
|
+
| `sse` | `createEventStream`, `useEventStream` (`onParseError` para frame que não é JSON) |
|
|
289
|
+
| `ws` | `createWebSocket` (handshake timeout, silence watchdog, jittered backoff, `navigator.onLine` suspend, close-code classification, `opened` promise), `useWebSocket`, `onParseError` (frame que não é JSON: descarta em vez de entregar `string` como `T`), `isRejectionCloseCode`, `HEARTBEAT_CLOSE_CODE` |
|
|
290
|
+
| `webrtc` | `tuneOpus` (Opus `fmtp` per audio m-line: per-key merge, `stereo` + `sprop-stereo`, inserts a missing `fmtp`), `setTunedLocalDescription` (falls back to the untouched SDP when the browser refuses the edit), `setSenderBitrate`, `createLinkStatsSampler` / `useLinkStats` / `readRoundTripMs` (throughput as a delta, round trip from the **selected** candidate pair) |
|
|
281
291
|
| `geo` _(opt. peer `leaflet`)_ | `haversineKm`, `pathLengthKm`, `bearingDeg`, `estimateTravel`, `createOSRMBackend`, `boundingBox`, `projectMercator`, `fitProjection`, `createPositionTracker`, `usePositionTracker`, `TrajectoryMap` — tile-free SVG map + trajectory tracking (no external/paid API; Leaflet is an opt-in, lazy-loaded tile layer for self-hosted tiles), types: `Coordinate`, `TrackPoint`, `TravelEstimate`, `TravelMode`, `GeoBounds`, `RoutingBackend` |
|
|
282
292
|
| `br` _(subpath `tempest-react-sdk/br`)_ | `BrazilMap`, `BrazilStateMap`, `BrazilStateCitySelect`, `listStates`, `getState`, `citiesByUf`, `statesByRegion`, `ufChoices`, `cityChoices`, `isValidUf`, `normalizeUf`, `isValidCity`, `loadBrUfGeoJson`, `loadStateMunicipalities`, types `UF`, `BrRegion`, `BrazilState` — clickable 27-UF SVG map + per-state municipality submaps (bundled simplified IBGE GeoJSON, lazy per UF) + states/cities dataset, no external API; mirrors the FastAPI SDK `utils/locations` |
|
|
283
293
|
| `icons` _(subpath `tempest-react-sdk/icons`)_ | `Icon`, `IconProvider`, `createIconRegistry`, `useIcon`, `preloadIcons`, `iconStatus`, `peekIcon`, `loadIcon`, `resolveIconAlias`, `isIconName`, `iconNames`, `iconAliases`, type `IconName` — render any of lucide's **2024 icons by kebab-case slug**. A literal slug becomes a static import via the `tempestIcons()` Vite plugin (zero extra requests); a runtime slug loads **one chunk per initial letter** (25 max) instead of the ~2000 chunk boundaries `lucide-react`'s own `DynamicIcon` forces. Unknown slug renders `fallback` and never throws; lucide's 257 deprecated aliases keep resolving |
|
|
@@ -1086,6 +1096,16 @@ if (sender) await setSenderBitrate(sender, 48_000);
|
|
|
1086
1096
|
|
|
1087
1097
|
No preset table on purpose — which values to use is the consumer's call. What the module owns is the parsing: per-key `fmtp` merge (so the browser's `minptime` survives), `stereo` written together with `sprop-stereo` (they point in opposite directions), payload types read from the `rtpmap`, an `fmtp` inserted when absent, and video m-lines left alone. Full guide: [WebRTC](https://mauriciobenjamin700.github.io/tempest-react-sdk/webrtc/).
|
|
1088
1098
|
|
|
1099
|
+
The same module reduces `getStats()` into the badge a call actually shows:
|
|
1100
|
+
|
|
1101
|
+
```ts
|
|
1102
|
+
import { useLinkStats } from "tempest-react-sdk";
|
|
1103
|
+
|
|
1104
|
+
const stats = useLinkStats(pc); // { kbps, width, height, fps, rttMs }
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
Two things every hand-rolled copy gets wrong: the round trip has to come from the pair the transport names in `selectedCandidatePairId` (a connection keeps idle host pairs around, and reading those makes the number jump between paths nobody is using), and the throughput has to be a **delta** — `bytesSent` is cumulative, so dividing it by the session length reports a historical average that only ever falls. Sampling pauses while the tab is hidden and resets its baseline on the way back, so the first sample after a background stretch is not the average of it.
|
|
1108
|
+
|
|
1089
1109
|
### Server-Sent Events (SSE) recipe
|
|
1090
1110
|
|
|
1091
1111
|
Stream with exponential reconnect (up to 10 attempts), `ping` heartbeat, JSON parsing by default. For cookie-auth endpoints, pass `withCredentials: true`.
|
|
@@ -1769,6 +1789,7 @@ if (result.unsupported) {
|
|
|
1769
1789
|
| `useKeyboardShortcut(shortcut, handler)` | `"Meta+K"` / `"Ctrl+Shift+P"` patterns. |
|
|
1770
1790
|
| `useBeforeInstallPrompt()` | Capture the PWA install prompt and trigger it later. |
|
|
1771
1791
|
| `useIdle(timeout)` | `true` after `timeout` ms of no input. |
|
|
1792
|
+
| `useFullscreen(ref?)` | Immersive mode driven by `fullscreenchange`, so `Esc`/F11 stay in sync. |
|
|
1772
1793
|
| `useGeolocation()` | One-shot or watch position with permission state. |
|
|
1773
1794
|
| `useScrollLock(active)` | Lock body scroll while a modal is open. |
|
|
1774
1795
|
| `useFocusTrap(ref, active)` | Trap focus inside a container. |
|
package/dist/access/Can.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./use-can.cjs");let t=require("react/jsx-runtime");function n({action:n,resource:r,params:i,children:a,fallback:o}){let{allowed:
|
|
1
|
+
const e=require("./use-can.cjs");let t=require("react/jsx-runtime");function n({action:n,resource:r,params:i,children:a,fallback:o,pending:s}){let{allowed:c,isLoading:l}=e.useCan({action:n,resource:r,params:i});return l?(0,t.jsx)(t.Fragment,{children:s===void 0?o??null:s}):(0,t.jsx)(t.Fragment,{children:c?a:o??null})}exports.Can=n;
|
|
2
2
|
//# sourceMappingURL=Can.cjs.map
|
package/dist/access/Can.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Can.cjs","names":[],"sources":["../../src/access/Can.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { useCan } from \"./use-can\";\nimport type { CanParams } from \"./types\";\n\nexport interface CanProps extends CanParams {\n /** Rendered when the action is allowed. */\n children: ReactNode;\n /** Rendered when the action is denied. Defaults to nothing. */\n fallback?: ReactNode;\n}\n\n/**\n * Conditionally render based on an access check. Renders `children` when the\n * action is allowed
|
|
1
|
+
{"version":3,"file":"Can.cjs","names":[],"sources":["../../src/access/Can.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { useCan } from \"./use-can\";\nimport type { CanParams } from \"./types\";\n\nexport interface CanProps extends CanParams {\n /** Rendered when the action is allowed. */\n children: ReactNode;\n /** Rendered when the action is denied. Defaults to nothing. */\n fallback?: ReactNode;\n /**\n * Rendered while an async check is still in flight. Defaults to\n * {@link fallback}, which keeps the pre-existing behaviour.\n *\n * Worth setting whenever `fallback` says something denial-shaped: without\n * it, a permission that resolves over the network flashes \"no access\" to\n * someone who has it. `pending={null}` renders nothing until the answer\n * lands; `pending={<Spinner />}` says why the screen is waiting.\n */\n pending?: ReactNode;\n}\n\n/**\n * Conditionally render based on an access check. Renders `children` when the\n * action is allowed and `fallback` when it is denied. While an async check is\n * pending it renders `pending`, which defaults to `fallback`.\n *\n * @example\n * ```tsx\n * <Can action=\"create\" resource=\"posts\" fallback={<p>No access</p>}>\n * <NewPostButton />\n * </Can>\n * ```\n *\n * @example\n * ```tsx\n * <Can\n * action=\"read\"\n * resource=\"finance\"\n * fallback={<p>No access</p>}\n * pending={<Spinner />}\n * >\n * <FinancePanel />\n * </Can>\n * ```\n */\nexport function Can({ action, resource, params, children, fallback, pending }: CanProps) {\n const { allowed, isLoading } = useCan({ action, resource, params });\n if (isLoading) return <>{pending === undefined ? (fallback ?? null) : pending}</>;\n return <>{allowed ? children : (fallback ?? null)}</>;\n}\n"],"mappings":"oEA6CA,SAAgB,EAAI,CAAE,SAAQ,WAAU,SAAQ,WAAU,WAAU,WAAqB,CACrF,GAAM,CAAE,UAAS,aAAc,EAAA,OAAO,CAAE,SAAQ,WAAU,QAAO,CAAC,EAElE,OADI,GAAkB,EAAA,EAAA,IAAA,CAAA,EAAA,SAAA,CAAA,SAAG,IAAY,IAAA,GAAa,GAAY,KAAQ,CAAU,CAAA,GACzE,EAAA,EAAA,IAAA,CAAA,EAAA,SAAA,CAAA,SAAG,EAAU,EAAY,GAAY,IAAQ,CAAA,CACxD"}
|
package/dist/access/Can.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { useCan as e } from "./use-can.js";
|
|
2
2
|
import { Fragment as t, jsx as n } from "react/jsx-runtime";
|
|
3
3
|
//#region src/access/Can.tsx
|
|
4
|
-
function r({ action: r, resource: i, params: a, children: o, fallback: s }) {
|
|
5
|
-
let { allowed:
|
|
4
|
+
function r({ action: r, resource: i, params: a, children: o, fallback: s, pending: c }) {
|
|
5
|
+
let { allowed: l, isLoading: u } = e({
|
|
6
6
|
action: r,
|
|
7
7
|
resource: i,
|
|
8
8
|
params: a
|
|
9
9
|
});
|
|
10
|
-
return
|
|
10
|
+
return u ? /* @__PURE__ */ n(t, { children: c === void 0 ? s ?? null : c }) : /* @__PURE__ */ n(t, { children: l ? o : s ?? null });
|
|
11
11
|
}
|
|
12
12
|
//#endregion
|
|
13
13
|
export { r as Can };
|
package/dist/access/Can.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Can.js","names":[],"sources":["../../src/access/Can.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { useCan } from \"./use-can\";\nimport type { CanParams } from \"./types\";\n\nexport interface CanProps extends CanParams {\n /** Rendered when the action is allowed. */\n children: ReactNode;\n /** Rendered when the action is denied. Defaults to nothing. */\n fallback?: ReactNode;\n}\n\n/**\n * Conditionally render based on an access check. Renders `children` when the\n * action is allowed
|
|
1
|
+
{"version":3,"file":"Can.js","names":[],"sources":["../../src/access/Can.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { useCan } from \"./use-can\";\nimport type { CanParams } from \"./types\";\n\nexport interface CanProps extends CanParams {\n /** Rendered when the action is allowed. */\n children: ReactNode;\n /** Rendered when the action is denied. Defaults to nothing. */\n fallback?: ReactNode;\n /**\n * Rendered while an async check is still in flight. Defaults to\n * {@link fallback}, which keeps the pre-existing behaviour.\n *\n * Worth setting whenever `fallback` says something denial-shaped: without\n * it, a permission that resolves over the network flashes \"no access\" to\n * someone who has it. `pending={null}` renders nothing until the answer\n * lands; `pending={<Spinner />}` says why the screen is waiting.\n */\n pending?: ReactNode;\n}\n\n/**\n * Conditionally render based on an access check. Renders `children` when the\n * action is allowed and `fallback` when it is denied. While an async check is\n * pending it renders `pending`, which defaults to `fallback`.\n *\n * @example\n * ```tsx\n * <Can action=\"create\" resource=\"posts\" fallback={<p>No access</p>}>\n * <NewPostButton />\n * </Can>\n * ```\n *\n * @example\n * ```tsx\n * <Can\n * action=\"read\"\n * resource=\"finance\"\n * fallback={<p>No access</p>}\n * pending={<Spinner />}\n * >\n * <FinancePanel />\n * </Can>\n * ```\n */\nexport function Can({ action, resource, params, children, fallback, pending }: CanProps) {\n const { allowed, isLoading } = useCan({ action, resource, params });\n if (isLoading) return <>{pending === undefined ? (fallback ?? null) : pending}</>;\n return <>{allowed ? children : (fallback ?? null)}</>;\n}\n"],"mappings":";;;AA6CA,SAAgB,EAAI,EAAE,WAAQ,aAAU,WAAQ,aAAU,aAAU,cAAqB;CACrF,IAAM,EAAE,YAAS,iBAAc,EAAO;EAAE;EAAQ;EAAU;CAAO,CAAC;CAElE,OADI,IAAkB,kBAAA,GAAA,EAAA,UAAG,MAAY,KAAA,IAAa,KAAY,OAAQ,EAAU,CAAA,IACzE,kBAAA,GAAA,EAAA,UAAG,IAAU,IAAY,KAAY,KAAQ,CAAA;AACxD"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function e(e,{fftSize:t=1024,decay:n=.7}={}){let
|
|
1
|
+
function e(e,{fftSize:t=1024,decay:n=.7,context:r}={}){let i=typeof AudioContext<`u`?AudioContext:globalThis.webkitAudioContext;if(!r&&!i)return{level:()=>0,stop:()=>void 0};let a=r??new i,o=a.createMediaStreamSource(e),s=a.createAnalyser();s.fftSize=t,o.connect(s);let c=new Float32Array(s.fftSize),l=0,u=!1;return{level:()=>{if(u)return 0;s.getFloatTimeDomainData(c);let e=0;for(let t=0;t<c.length;t+=1)e+=c[t]*c[t];let t=Math.sqrt(e/c.length);return l=t>l?t:l*n+t*(1-n),Math.min(1,l)},stop:()=>{if(!u){u=!0;try{o.disconnect(),s.disconnect()}catch{}r||a.close().catch(()=>void 0)}}}}exports.createLevelMeter=e;
|
|
2
2
|
//# sourceMappingURL=level-meter.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"level-meter.cjs","names":[],"sources":["../../src/audio/level-meter.ts"],"sourcesContent":["/** Options for {@link createLevelMeter}. */\nexport interface LevelMeterOptions {\n /**\n * FFT window. 1024 is a good default: ~21 ms at 48 kHz, long enough to be stable\n * and short enough to react to a syllable.\n */\n fftSize?: number;\n /**\n * Extra smoothing on the way down, 0–1. Default `0.7`.\n *\n * A raw RMS reading is jittery enough to make a meter look broken. Attack is left\n * instant — a meter that lags the voice going *up* reads as \"not recording\" —\n * while the decay is eased, which is how every hardware meter behaves.\n */\n decay?: number;\n}\n\n/** A running level meter. */\nexport interface LevelMeter {\n /** Latest level, 0–1. Read it on your own frame loop. */\n level: () => number;\n /** Stop sampling and release the Web Audio graph. */\n stop: () => void;\n}\n\n/**\n * Sample the loudness of a live stream, 0–1.\n *\n * This exists because a recorder with no visible level is indistinguishable from a\n * broken one. A muted OS input, a headset whose mic arm is switched off, the wrong\n * device selected — all three produce a perfectly successful recording of silence,\n * and the user only finds out after they finish talking.\n *\n * The value is RMS, not peak: peak reacts to a single sample and flickers, RMS tracks\n * perceived loudness. It is deliberately **not** React state — updating state per\n * frame would re-render the tree 60 times a second. Poll `level()` from a\n * `requestAnimationFrame` loop and write to the DOM, or read it inside an existing\n * animation.\n *\n * The `AudioContext` is created here and closed by `stop()`. Browsers cap the number\n * of live contexts (Chrome allows around six), so a meter left running on unmount\n * eventually breaks every later one on the page.\n *\n * `level()` applies instant attack and eased release: a value above the current one\n * is taken as is, a lower one decays by `decay`. A meter that fell as fast as it rose\n * reads as noise instead of as loudness.\n *\n * @tempest-limits empty-catch — `stop()` disconnects nodes whose context may already\n * be closed (the tab was backgrounded, the stream's track ended, `stop()` raced an\n * unmount), and a disconnect on a dead graph throws. There is nothing to report and\n * nothing to retry: the resource this call would have released is already gone, and\n * the `context.close()` right after it is what actually matters.\n *\n * @param stream - A live audio stream.\n * @param options - See {@link LevelMeterOptions}.\n * @returns A `level()` reader and a `stop()`.\n *\n * @example\n * const meter = createLevelMeter(stream);\n * const tick = () => { bar.style.transform = `scaleX(${meter.level()})`; raf = requestAnimationFrame(tick); };\n */\nexport function createLevelMeter(\n stream: MediaStream,\n { fftSize = 1024, decay = 0.7 }: LevelMeterOptions = {},\n): LevelMeter {\n const Ctor: typeof AudioContext | undefined =\n typeof AudioContext !== \"undefined\"\n ? AudioContext\n : (globalThis as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n\n if (!Ctor) {\n return { level: () => 0, stop: () => undefined };\n }\n\n const context = new Ctor();\n const source = context.createMediaStreamSource(stream);\n const analyser = context.createAnalyser();\n analyser.fftSize = fftSize;\n source.connect(analyser);\n\n const samples = new Float32Array(analyser.fftSize);\n let smoothed = 0;\n let closed = false;\n\n const read = (): number => {\n if (closed) return 0;\n analyser.getFloatTimeDomainData(samples);\n let sum = 0;\n for (let index = 0; index < samples.length; index += 1) {\n sum += samples[index] * samples[index];\n }\n const rms = Math.sqrt(sum / samples.length);\n smoothed = rms > smoothed ? rms : smoothed * decay + rms * (1 - decay);\n return Math.min(1, smoothed);\n };\n\n return {\n level: read,\n stop: (): void => {\n if (closed) return;\n closed = true;\n try {\n source.disconnect();\n analyser.disconnect();\n } catch {\n /* empty */\n }\n void context.close().catch(() => undefined);\n },\n };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"level-meter.cjs","names":[],"sources":["../../src/audio/level-meter.ts"],"sourcesContent":["/** Options for {@link createLevelMeter}. */\nexport interface LevelMeterOptions {\n /**\n * FFT window. 1024 is a good default: ~21 ms at 48 kHz, long enough to be stable\n * and short enough to react to a syllable.\n */\n fftSize?: number;\n /**\n * Extra smoothing on the way down, 0–1. Default `0.7`.\n *\n * A raw RMS reading is jittery enough to make a meter look broken. Attack is left\n * instant — a meter that lags the voice going *up* reads as \"not recording\" —\n * while the decay is eased, which is how every hardware meter behaves.\n *\n * `0` turns the smoothing off and reports the window's RMS as measured, which is\n * what a detector wants: {@link monitorVoiceActivity} applies its own release,\n * and two of them stacked would hold the indicator open long past the speech.\n */\n decay?: number;\n /**\n * Reuse an existing `AudioContext` instead of creating one.\n *\n * Browsers cap live contexts (Chrome allows around six), and a meter per remote\n * participant reaches that in a five-person call. A caller measuring several\n * streams should open one context and hand it to every meter. `stop()` closes\n * only a context this meter created — an injected one belongs to the caller.\n */\n context?: AudioContext;\n}\n\n/** A running level meter. */\nexport interface LevelMeter {\n /** Latest level, 0–1. Read it on your own frame loop. */\n level: () => number;\n /** Stop sampling and release the Web Audio graph. */\n stop: () => void;\n}\n\n/**\n * Sample the loudness of a live stream, 0–1.\n *\n * This exists because a recorder with no visible level is indistinguishable from a\n * broken one. A muted OS input, a headset whose mic arm is switched off, the wrong\n * device selected — all three produce a perfectly successful recording of silence,\n * and the user only finds out after they finish talking.\n *\n * The value is RMS, not peak: peak reacts to a single sample and flickers, RMS tracks\n * perceived loudness. It is deliberately **not** React state — updating state per\n * frame would re-render the tree 60 times a second. Poll `level()` from a\n * `requestAnimationFrame` loop and write to the DOM, or read it inside an existing\n * animation.\n *\n * The `AudioContext` is created here and closed by `stop()`. Browsers cap the number\n * of live contexts (Chrome allows around six), so a meter left running on unmount\n * eventually breaks every later one on the page.\n *\n * `level()` applies instant attack and eased release: a value above the current one\n * is taken as is, a lower one decays by `decay`. A meter that fell as fast as it rose\n * reads as noise instead of as loudness.\n *\n * @tempest-limits empty-catch — `stop()` disconnects nodes whose context may already\n * be closed (the tab was backgrounded, the stream's track ended, `stop()` raced an\n * unmount), and a disconnect on a dead graph throws. There is nothing to report and\n * nothing to retry: the resource this call would have released is already gone, and\n * the `context.close()` right after it is what actually matters.\n *\n * @param stream - A live audio stream.\n * @param options - See {@link LevelMeterOptions}.\n * @returns A `level()` reader and a `stop()`.\n *\n * @example\n * const meter = createLevelMeter(stream);\n * const tick = () => { bar.style.transform = `scaleX(${meter.level()})`; raf = requestAnimationFrame(tick); };\n */\nexport function createLevelMeter(\n stream: MediaStream,\n { fftSize = 1024, decay = 0.7, context: injectedContext }: LevelMeterOptions = {},\n): LevelMeter {\n const Ctor: typeof AudioContext | undefined =\n typeof AudioContext !== \"undefined\"\n ? AudioContext\n : (globalThis as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n\n if (!injectedContext && !Ctor) {\n return { level: () => 0, stop: () => undefined };\n }\n\n const context = injectedContext ?? new (Ctor as typeof AudioContext)();\n const source = context.createMediaStreamSource(stream);\n const analyser = context.createAnalyser();\n analyser.fftSize = fftSize;\n source.connect(analyser);\n\n const samples = new Float32Array(analyser.fftSize);\n let smoothed = 0;\n let closed = false;\n\n const read = (): number => {\n if (closed) return 0;\n analyser.getFloatTimeDomainData(samples);\n let sum = 0;\n for (let index = 0; index < samples.length; index += 1) {\n sum += samples[index] * samples[index];\n }\n const rms = Math.sqrt(sum / samples.length);\n smoothed = rms > smoothed ? rms : smoothed * decay + rms * (1 - decay);\n return Math.min(1, smoothed);\n };\n\n return {\n level: read,\n stop: (): void => {\n if (closed) return;\n closed = true;\n try {\n source.disconnect();\n analyser.disconnect();\n } catch {\n /* empty */\n }\n if (!injectedContext) void context.close().catch(() => undefined);\n },\n };\n}\n"],"mappings":"AA0EA,SAAgB,EACZ,EACA,CAAE,UAAU,KAAM,QAAQ,GAAK,QAAS,GAAuC,CAAC,EACtE,CACV,IAAM,EACF,OAAO,aAAiB,IAClB,aACC,WAA4D,mBAEvE,GAAI,CAAC,GAAmB,CAAC,EACrB,MAAO,CAAE,UAAa,EAAG,SAAY,IAAA,EAAU,EAGnD,IAAM,EAAU,GAAmB,IAAK,EAClC,EAAS,EAAQ,wBAAwB,CAAM,EAC/C,EAAW,EAAQ,eAAe,EACxC,EAAS,QAAU,EACnB,EAAO,QAAQ,CAAQ,EAEvB,IAAM,EAAU,IAAI,aAAa,EAAS,OAAO,EAC7C,EAAW,EACX,EAAS,GAcb,MAAO,CACH,UAbuB,CACvB,GAAI,EAAQ,MAAO,GACnB,EAAS,uBAAuB,CAAO,EACvC,IAAI,EAAM,EACV,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAQ,OAAQ,GAAS,EACjD,GAAO,EAAQ,GAAS,EAAQ,GAEpC,IAAM,EAAM,KAAK,KAAK,EAAM,EAAQ,MAAM,EAE1C,MADA,GAAW,EAAM,EAAW,EAAM,EAAW,EAAQ,GAAO,EAAI,GACzD,KAAK,IAAI,EAAG,CAAQ,CAC/B,EAII,SAAkB,CACV,MACJ,GAAS,GACT,GAAI,CACA,EAAO,WAAW,EAClB,EAAS,WAAW,CACxB,MAAQ,CAER,CACK,GAAiB,EAAa,MAAM,CAAC,CAAC,UAAY,IAAA,EAAS,CAPvD,CAQb,CACJ,CACJ"}
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
//#region src/audio/level-meter.ts
|
|
2
|
-
function e(e, { fftSize: t = 1024, decay: n = .7 } = {}) {
|
|
3
|
-
let
|
|
4
|
-
if (!r) return {
|
|
2
|
+
function e(e, { fftSize: t = 1024, decay: n = .7, context: r } = {}) {
|
|
3
|
+
let i = typeof AudioContext < "u" ? AudioContext : globalThis.webkitAudioContext;
|
|
4
|
+
if (!r && !i) return {
|
|
5
5
|
level: () => 0,
|
|
6
6
|
stop: () => void 0
|
|
7
7
|
};
|
|
8
|
-
let
|
|
9
|
-
|
|
10
|
-
let
|
|
8
|
+
let a = r ?? new i(), o = a.createMediaStreamSource(e), s = a.createAnalyser();
|
|
9
|
+
s.fftSize = t, o.connect(s);
|
|
10
|
+
let c = new Float32Array(s.fftSize), l = 0, u = !1;
|
|
11
11
|
return {
|
|
12
12
|
level: () => {
|
|
13
|
-
if (
|
|
14
|
-
|
|
13
|
+
if (u) return 0;
|
|
14
|
+
s.getFloatTimeDomainData(c);
|
|
15
15
|
let e = 0;
|
|
16
|
-
for (let t = 0; t <
|
|
17
|
-
let t = Math.sqrt(e /
|
|
18
|
-
return
|
|
16
|
+
for (let t = 0; t < c.length; t += 1) e += c[t] * c[t];
|
|
17
|
+
let t = Math.sqrt(e / c.length);
|
|
18
|
+
return l = t > l ? t : l * n + t * (1 - n), Math.min(1, l);
|
|
19
19
|
},
|
|
20
20
|
stop: () => {
|
|
21
|
-
if (!
|
|
22
|
-
|
|
21
|
+
if (!u) {
|
|
22
|
+
u = !0;
|
|
23
23
|
try {
|
|
24
|
-
|
|
24
|
+
o.disconnect(), s.disconnect();
|
|
25
25
|
} catch {}
|
|
26
|
-
|
|
26
|
+
r || a.close().catch(() => void 0);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"level-meter.js","names":[],"sources":["../../src/audio/level-meter.ts"],"sourcesContent":["/** Options for {@link createLevelMeter}. */\nexport interface LevelMeterOptions {\n /**\n * FFT window. 1024 is a good default: ~21 ms at 48 kHz, long enough to be stable\n * and short enough to react to a syllable.\n */\n fftSize?: number;\n /**\n * Extra smoothing on the way down, 0–1. Default `0.7`.\n *\n * A raw RMS reading is jittery enough to make a meter look broken. Attack is left\n * instant — a meter that lags the voice going *up* reads as \"not recording\" —\n * while the decay is eased, which is how every hardware meter behaves.\n */\n decay?: number;\n}\n\n/** A running level meter. */\nexport interface LevelMeter {\n /** Latest level, 0–1. Read it on your own frame loop. */\n level: () => number;\n /** Stop sampling and release the Web Audio graph. */\n stop: () => void;\n}\n\n/**\n * Sample the loudness of a live stream, 0–1.\n *\n * This exists because a recorder with no visible level is indistinguishable from a\n * broken one. A muted OS input, a headset whose mic arm is switched off, the wrong\n * device selected — all three produce a perfectly successful recording of silence,\n * and the user only finds out after they finish talking.\n *\n * The value is RMS, not peak: peak reacts to a single sample and flickers, RMS tracks\n * perceived loudness. It is deliberately **not** React state — updating state per\n * frame would re-render the tree 60 times a second. Poll `level()` from a\n * `requestAnimationFrame` loop and write to the DOM, or read it inside an existing\n * animation.\n *\n * The `AudioContext` is created here and closed by `stop()`. Browsers cap the number\n * of live contexts (Chrome allows around six), so a meter left running on unmount\n * eventually breaks every later one on the page.\n *\n * `level()` applies instant attack and eased release: a value above the current one\n * is taken as is, a lower one decays by `decay`. A meter that fell as fast as it rose\n * reads as noise instead of as loudness.\n *\n * @tempest-limits empty-catch — `stop()` disconnects nodes whose context may already\n * be closed (the tab was backgrounded, the stream's track ended, `stop()` raced an\n * unmount), and a disconnect on a dead graph throws. There is nothing to report and\n * nothing to retry: the resource this call would have released is already gone, and\n * the `context.close()` right after it is what actually matters.\n *\n * @param stream - A live audio stream.\n * @param options - See {@link LevelMeterOptions}.\n * @returns A `level()` reader and a `stop()`.\n *\n * @example\n * const meter = createLevelMeter(stream);\n * const tick = () => { bar.style.transform = `scaleX(${meter.level()})`; raf = requestAnimationFrame(tick); };\n */\nexport function createLevelMeter(\n stream: MediaStream,\n { fftSize = 1024, decay = 0.7 }: LevelMeterOptions = {},\n): LevelMeter {\n const Ctor: typeof AudioContext | undefined =\n typeof AudioContext !== \"undefined\"\n ? AudioContext\n : (globalThis as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n\n if (!Ctor) {\n return { level: () => 0, stop: () => undefined };\n }\n\n const context = new Ctor();\n const source = context.createMediaStreamSource(stream);\n const analyser = context.createAnalyser();\n analyser.fftSize = fftSize;\n source.connect(analyser);\n\n const samples = new Float32Array(analyser.fftSize);\n let smoothed = 0;\n let closed = false;\n\n const read = (): number => {\n if (closed) return 0;\n analyser.getFloatTimeDomainData(samples);\n let sum = 0;\n for (let index = 0; index < samples.length; index += 1) {\n sum += samples[index] * samples[index];\n }\n const rms = Math.sqrt(sum / samples.length);\n smoothed = rms > smoothed ? rms : smoothed * decay + rms * (1 - decay);\n return Math.min(1, smoothed);\n };\n\n return {\n level: read,\n stop: (): void => {\n if (closed) return;\n closed = true;\n try {\n source.disconnect();\n analyser.disconnect();\n } catch {\n /* empty */\n }\n void context.close().catch(() => undefined);\n },\n };\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"file":"level-meter.js","names":[],"sources":["../../src/audio/level-meter.ts"],"sourcesContent":["/** Options for {@link createLevelMeter}. */\nexport interface LevelMeterOptions {\n /**\n * FFT window. 1024 is a good default: ~21 ms at 48 kHz, long enough to be stable\n * and short enough to react to a syllable.\n */\n fftSize?: number;\n /**\n * Extra smoothing on the way down, 0–1. Default `0.7`.\n *\n * A raw RMS reading is jittery enough to make a meter look broken. Attack is left\n * instant — a meter that lags the voice going *up* reads as \"not recording\" —\n * while the decay is eased, which is how every hardware meter behaves.\n *\n * `0` turns the smoothing off and reports the window's RMS as measured, which is\n * what a detector wants: {@link monitorVoiceActivity} applies its own release,\n * and two of them stacked would hold the indicator open long past the speech.\n */\n decay?: number;\n /**\n * Reuse an existing `AudioContext` instead of creating one.\n *\n * Browsers cap live contexts (Chrome allows around six), and a meter per remote\n * participant reaches that in a five-person call. A caller measuring several\n * streams should open one context and hand it to every meter. `stop()` closes\n * only a context this meter created — an injected one belongs to the caller.\n */\n context?: AudioContext;\n}\n\n/** A running level meter. */\nexport interface LevelMeter {\n /** Latest level, 0–1. Read it on your own frame loop. */\n level: () => number;\n /** Stop sampling and release the Web Audio graph. */\n stop: () => void;\n}\n\n/**\n * Sample the loudness of a live stream, 0–1.\n *\n * This exists because a recorder with no visible level is indistinguishable from a\n * broken one. A muted OS input, a headset whose mic arm is switched off, the wrong\n * device selected — all three produce a perfectly successful recording of silence,\n * and the user only finds out after they finish talking.\n *\n * The value is RMS, not peak: peak reacts to a single sample and flickers, RMS tracks\n * perceived loudness. It is deliberately **not** React state — updating state per\n * frame would re-render the tree 60 times a second. Poll `level()` from a\n * `requestAnimationFrame` loop and write to the DOM, or read it inside an existing\n * animation.\n *\n * The `AudioContext` is created here and closed by `stop()`. Browsers cap the number\n * of live contexts (Chrome allows around six), so a meter left running on unmount\n * eventually breaks every later one on the page.\n *\n * `level()` applies instant attack and eased release: a value above the current one\n * is taken as is, a lower one decays by `decay`. A meter that fell as fast as it rose\n * reads as noise instead of as loudness.\n *\n * @tempest-limits empty-catch — `stop()` disconnects nodes whose context may already\n * be closed (the tab was backgrounded, the stream's track ended, `stop()` raced an\n * unmount), and a disconnect on a dead graph throws. There is nothing to report and\n * nothing to retry: the resource this call would have released is already gone, and\n * the `context.close()` right after it is what actually matters.\n *\n * @param stream - A live audio stream.\n * @param options - See {@link LevelMeterOptions}.\n * @returns A `level()` reader and a `stop()`.\n *\n * @example\n * const meter = createLevelMeter(stream);\n * const tick = () => { bar.style.transform = `scaleX(${meter.level()})`; raf = requestAnimationFrame(tick); };\n */\nexport function createLevelMeter(\n stream: MediaStream,\n { fftSize = 1024, decay = 0.7, context: injectedContext }: LevelMeterOptions = {},\n): LevelMeter {\n const Ctor: typeof AudioContext | undefined =\n typeof AudioContext !== \"undefined\"\n ? AudioContext\n : (globalThis as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n\n if (!injectedContext && !Ctor) {\n return { level: () => 0, stop: () => undefined };\n }\n\n const context = injectedContext ?? new (Ctor as typeof AudioContext)();\n const source = context.createMediaStreamSource(stream);\n const analyser = context.createAnalyser();\n analyser.fftSize = fftSize;\n source.connect(analyser);\n\n const samples = new Float32Array(analyser.fftSize);\n let smoothed = 0;\n let closed = false;\n\n const read = (): number => {\n if (closed) return 0;\n analyser.getFloatTimeDomainData(samples);\n let sum = 0;\n for (let index = 0; index < samples.length; index += 1) {\n sum += samples[index] * samples[index];\n }\n const rms = Math.sqrt(sum / samples.length);\n smoothed = rms > smoothed ? rms : smoothed * decay + rms * (1 - decay);\n return Math.min(1, smoothed);\n };\n\n return {\n level: read,\n stop: (): void => {\n if (closed) return;\n closed = true;\n try {\n source.disconnect();\n analyser.disconnect();\n } catch {\n /* empty */\n }\n if (!injectedContext) void context.close().catch(() => undefined);\n },\n };\n}\n"],"mappings":";AA0EA,SAAgB,EACZ,GACA,EAAE,aAAU,MAAM,WAAQ,IAAK,SAAS,MAAuC,CAAC,GACtE;CACV,IAAM,IACF,OAAO,eAAiB,MAClB,eACC,WAA4D;CAEvE,IAAI,CAAC,KAAmB,CAAC,GACrB,OAAO;EAAE,aAAa;EAAG,YAAY,KAAA;CAAU;CAGnD,IAAM,IAAU,KAAmB,IAAK,EAA6B,GAC/D,IAAS,EAAQ,wBAAwB,CAAM,GAC/C,IAAW,EAAQ,eAAe;CAExC,AADA,EAAS,UAAU,GACnB,EAAO,QAAQ,CAAQ;CAEvB,IAAM,IAAU,IAAI,aAAa,EAAS,OAAO,GAC7C,IAAW,GACX,IAAS;CAcb,OAAO;EACH,aAbuB;GACvB,IAAI,GAAQ,OAAO;GACnB,EAAS,uBAAuB,CAAO;GACvC,IAAI,IAAM;GACV,KAAK,IAAI,IAAQ,GAAG,IAAQ,EAAQ,QAAQ,KAAS,GACjD,KAAO,EAAQ,KAAS,EAAQ;GAEpC,IAAM,IAAM,KAAK,KAAK,IAAM,EAAQ,MAAM;GAE1C,OADA,IAAW,IAAM,IAAW,IAAM,IAAW,IAAQ,KAAO,IAAI,IACzD,KAAK,IAAI,GAAG,CAAQ;EAC/B;EAII,YAAkB;GACV,QACJ;QAAS;IACT,IAAI;KAEA,AADA,EAAO,WAAW,GAClB,EAAS,WAAW;IACxB,QAAQ,CAER;IACA,AAAK,KAAiB,EAAa,MAAM,CAAC,CAAC,YAAY,KAAA,CAAS;GAPvD;EAQb;CACJ;AACJ"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./voice-chain-stages.cjs");var t=3e3,n=50,r=2.4,i=.004,a=.12;function o(){return typeof AudioContext<`u`?AudioContext:globalThis.webkitAudioContext}async function s(r,{onProgress:i,durationMs:a=t,context:s}={}){let c=o(),l=s??(c?new c:null),u=r.getAudioTracks()[0];if(!l||!u)return 0;let d=l.createMediaStreamSource(r),f=l.createAnalyser();f.fftSize=2048,d.connect(f);let p=new Float32Array(f.fftSize),m=performance.now(),h=0;return await new Promise(t=>{let r=setInterval(()=>{f.getFloatTimeDomainData(p),h=Math.max(h,e.windowRms(p));let n=performance.now()-m;i?.(Math.min(1,n/a)),!(n<a)&&(clearInterval(r),t())},n)}),d.disconnect(),f.disconnect(),h}function c(e){return Number.isFinite(e)?Math.min(a,Math.max(i,e*r)):i}exports.measureNoiseFloor=s,exports.suggestGateThreshold=c;
|
|
2
|
+
//# sourceMappingURL=noise-floor.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noise-floor.cjs","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":"4CAGA,IAAM,EAAsB,IAGtB,EAAY,GASZ,EAAc,IAGd,EAAW,KACX,EAAW,IAajB,SAAS,GAA2D,CAEhE,OADI,OAAO,aAAiB,IAAoB,aACxC,WAA4D,kBACxE,CAuBA,eAAsB,EAClB,EACA,CACI,aACA,aAAa,EACb,QAAS,GACU,CAAC,EACT,CACf,IAAM,EAAO,EAAwB,EAC/B,EAAU,IAAoB,EAAO,IAAI,EAAS,MAClD,EAAQ,EAAO,eAAe,CAAC,CAAC,GACtC,GAAI,CAAC,GAAW,CAAC,EAAO,MAAO,GAE/B,IAAM,EAAS,EAAQ,wBAAwB,CAAM,EAC/C,EAAW,EAAQ,eAAe,EACxC,EAAS,QAAU,KACnB,EAAO,QAAQ,CAAQ,EAEvB,IAAM,EAAS,IAAI,aAAa,EAAS,OAAO,EAC1C,EAAU,YAAY,IAAI,EAC5B,EAAO,EAiBX,OAfA,MAAM,IAAI,QAAe,GAAY,CACjC,IAAM,EAAQ,gBAAkB,CAC5B,EAAS,uBAAuB,CAAM,EACtC,EAAO,KAAK,IAAI,EAAM,EAAA,UAAU,CAAM,CAAC,EAEvC,IAAM,EAAU,YAAY,IAAI,EAAI,EACpC,IAAa,KAAK,IAAI,EAAG,EAAU,CAAU,CAAC,EAC1C,IAAU,KACd,cAAc,CAAK,EACnB,EAAQ,EACZ,EAAG,CAAS,CAChB,CAAC,EAED,EAAO,WAAW,EAClB,EAAS,WAAW,EACb,CACX,CAaA,SAAgB,EAAqB,EAA4B,CAE7D,OADK,OAAO,SAAS,CAAU,EACxB,KAAK,IAAI,EAAU,KAAK,IAAI,EAAU,EAAa,CAAW,CAAC,EAD7B,CAE7C"}
|
|
@@ -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,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":"4CA2CA,IAAa,EAA0C,CACnD,SAAU,GACV,KAAM,GACN,cAAe,IACf,WAAY,GACZ,SAAU,GACV,QAAS,GACT,QAAS,GACT,QAAS,EACb,EAuCA,SAAS,GAA2D,CAEhE,OADI,OAAO,aAAiB,IAAoB,aACxC,WAA4D,kBACxE,CAcA,SAAgB,EAAiB,EAA8B,EAAO,EAAY,CAC9E,MACI,CAAC,EAAS,UACV,CAAC,EAAS,MACV,CAAC,EAAS,YACV,CAAC,EAAS,UACV,CAAC,EAAS,SACV,CAAC,EAAS,SACV,CAAC,EAAS,SACV,IAAS,CAEjB,CAGA,SAAS,EAAY,EAA0B,EAAiC,CAC5E,MAAO,CACH,MAAO,EACP,YAAe,CACP,GAAY,EAAO,KAAK,CAChC,EACA,UAAW,EACf,CACJ,CAmCA,SAAgB,EACZ,EACA,EAA+B,EAC/B,CAAE,OAAO,EAAG,QAAS,EAAiB,aAAa,IAA4B,CAAC,EACtE,CACV,GAAI,EAAiB,EAAU,CAAI,EAAG,OAAO,EAAY,EAAQ,CAAU,EAE3E,IAAM,EAAO,EAAwB,EAC/B,EAAU,IAAoB,EAAO,IAAI,EAAS,MACxD,GAAI,CAAC,EAAS,OAAO,EAAY,EAAQ,CAAU,EAEnD,IAAM,EAAQ,EAAQ,wBAClB,OAAO,YAAgB,IAChB,EACD,IAAI,YAAY,CAAC,CAAM,CAAC,CAClC,EACM,EAAqB,CAAC,CAAK,EAC3B,EAAwB,CAAC,EAC3B,EAAkB,EAEhB,EAAS,GAA0B,CACrC,EAAK,QAAQ,CAAI,EACjB,EAAO,EACP,EAAM,KAAK,CAAI,CACnB,EAIA,GAFI,EAAS,UAAU,EAAM,EAAA,cAAc,CAAO,CAAC,EAE/C,EAAS,KAAM,CACf,IAAM,EAAQ,EAAA,UAAU,MAAe,EAAS,aAAa,EAC7D,EAAK,QAAQ,EAAM,QAAQ,EAC3B,EAAM,KAAK,EAAM,QAAQ,EACzB,EAAM,KAAK,EAAM,IAAI,EACrB,EAAM,EAAM,IAAI,CACpB,CAMA,GAJI,EAAS,YAAY,EAAM,EAAA,gBAAgB,CAAO,CAAC,EACnD,EAAS,UAAU,EAAM,EAAA,cAAc,CAAO,CAAC,EAC/C,EAAS,SAAS,EAAM,EAAA,aAAa,CAAO,CAAC,EAE7C,EAAS,QAAS,CAClB,IAAM,EAAQ,EAAA,aAAa,CAAO,EAClC,EAAK,QAAQ,EAAM,IAAI,EACvB,EAAM,KAAK,EAAM,KAAM,EAAM,QAAQ,EACrC,EAAM,KAAK,EAAM,IAAI,EACrB,EAAM,EAAM,MAAM,CACtB,CAEA,IAAM,EAAS,EAAQ,WAAW,EAClC,EAAO,KAAK,MAAQ,EACpB,EAAM,CAAM,EAER,EAAS,SAAS,EAAM,EAAA,aAAa,CAAO,CAAC,EAEjD,IAAM,EAAc,EAAQ,6BAA6B,EACzD,EAAK,QAAQ,CAAW,EACxB,EAAM,KAAK,CAAW,EACtB,IAAM,EAAQ,EAAY,OAAO,eAAe,CAAC,CAAC,GAE5C,MAAuB,CACzB,IAAK,IAAM,KAAQ,EAAO,EAAK,EAC/B,IAAK,IAAM,KAAU,EAAO,EAAO,WAAW,CAClD,EAOA,OALK,EAKE,CACH,QACA,YAAqB,CACjB,EAAS,EACT,EAAM,KAAK,EACP,GAAY,EAAO,KAAK,CAChC,EACA,UAAW,EACf,GAZI,EAAS,EACF,EAAY,EAAQ,CAAU,EAY7C,CAyBA,SAAgB,EACZ,EACA,EAA+B,EAC/B,EAAiD,CAAC,EACtC,CACZ,IAAM,EAAO,EAAwB,EAC/B,EAAU,EAAQ,UAAY,EAAO,IAAI,EAAS,MACxD,GAAI,CAAC,EAAS,MAAO,CAAE,SAAY,IAAA,EAAU,EAE7C,IAAM,EAAQ,EAAiB,EAAQ,EAAU,CAAE,GAAG,EAAS,UAAS,WAAY,EAAM,CAAC,EACrF,EAAW,EAAQ,wBACrB,OAAO,YAAgB,IAChB,EAAM,MACP,IAAI,YAAY,CAAC,EAAM,KAAK,CAAC,CACvC,EAGA,OAFA,EAAS,QAAQ,EAAQ,WAAW,EAE7B,CACH,SAAkB,CACd,EAAS,WAAW,EACpB,EAAM,QAAQ,CAClB,CACJ,CACJ"}
|