tempest-react-sdk 0.34.0 → 0.36.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 +72 -68
- package/dist/audio/audio-output.cjs +2 -0
- package/dist/audio/audio-output.cjs.map +1 -0
- package/dist/audio/audio-output.js +17 -0
- package/dist/audio/audio-output.js.map +1 -0
- package/dist/audio/audio-player.cjs +1 -1
- package/dist/audio/audio-player.cjs.map +1 -1
- package/dist/audio/audio-player.js +21 -20
- package/dist/audio/audio-player.js.map +1 -1
- package/dist/audio/audio-recorder.cjs +2 -0
- package/dist/audio/audio-recorder.cjs.map +1 -0
- package/dist/audio/audio-recorder.js +30 -0
- package/dist/audio/audio-recorder.js.map +1 -0
- package/dist/audio/duration.cjs +2 -0
- package/dist/audio/duration.cjs.map +1 -0
- package/dist/audio/duration.js +10 -0
- package/dist/audio/duration.js.map +1 -0
- package/dist/audio/level-meter.cjs +2 -0
- package/dist/audio/level-meter.cjs.map +1 -0
- package/dist/audio/level-meter.js +34 -0
- package/dist/audio/level-meter.js.map +1 -0
- package/dist/audio/media-access.cjs +2 -0
- package/dist/audio/media-access.cjs.map +1 -0
- package/dist/audio/media-access.js +51 -0
- package/dist/audio/media-access.js.map +1 -0
- package/dist/audio/use-audio-recorder.cjs +2 -0
- package/dist/audio/use-audio-recorder.cjs.map +1 -0
- package/dist/audio/use-audio-recorder.js +94 -0
- package/dist/audio/use-audio-recorder.js.map +1 -0
- package/dist/audio/use-audio.cjs.map +1 -1
- package/dist/audio/use-audio.js.map +1 -1
- package/dist/audio/use-microphone.cjs +2 -0
- package/dist/audio/use-microphone.cjs.map +1 -0
- package/dist/audio/use-microphone.js +54 -0
- package/dist/audio/use-microphone.js.map +1 -0
- package/dist/audio/wav.cjs +2 -0
- package/dist/audio/wav.cjs.map +1 -0
- package/dist/audio/wav.js +49 -0
- package/dist/audio/wav.js.map +1 -0
- package/dist/auth/passkey.cjs +2 -0
- package/dist/auth/passkey.cjs.map +1 -0
- package/dist/auth/passkey.js +171 -0
- package/dist/auth/passkey.js.map +1 -0
- package/dist/auth/use-passkey.cjs +2 -0
- package/dist/auth/use-passkey.cjs.map +1 -0
- package/dist/auth/use-passkey.js +120 -0
- package/dist/auth/use-passkey.js.map +1 -0
- package/dist/br/PixQRCode.cjs +2 -0
- package/dist/br/PixQRCode.cjs.map +1 -0
- package/dist/br/PixQRCode.js +70 -0
- package/dist/br/PixQRCode.js.map +1 -0
- package/dist/br/PixQRCode.module.cjs +2 -0
- package/dist/br/PixQRCode.module.cjs.map +1 -0
- package/dist/br/PixQRCode.module.js +15 -0
- package/dist/br/PixQRCode.module.js.map +1 -0
- package/dist/br/boleto.cjs +2 -0
- package/dist/br/boleto.cjs.map +1 -0
- package/dist/br/boleto.js +225 -0
- package/dist/br/boleto.js.map +1 -0
- package/dist/br/holidays.cjs +2 -0
- package/dist/br/holidays.cjs.map +1 -0
- package/dist/br/holidays.js +148 -0
- package/dist/br/holidays.js.map +1 -0
- package/dist/br/nfe.cjs +2 -0
- package/dist/br/nfe.cjs.map +1 -0
- package/dist/br/nfe.js +104 -0
- package/dist/br/nfe.js.map +1 -0
- package/dist/br/pix.cjs +2 -0
- package/dist/br/pix.cjs.map +1 -0
- package/dist/br/pix.js +148 -0
- package/dist/br/pix.js.map +1 -0
- package/dist/br.cjs +1 -1
- package/dist/br.cjs.map +1 -1
- package/dist/br.d.ts +878 -0
- package/dist/br.js +7 -2
- package/dist/br.js.map +1 -1
- package/dist/capture/barcode.cjs +2 -0
- package/dist/capture/barcode.cjs.map +1 -0
- package/dist/capture/barcode.js +59 -0
- package/dist/capture/barcode.js.map +1 -0
- package/dist/capture/media-recorder.cjs +2 -0
- package/dist/capture/media-recorder.cjs.map +1 -0
- package/dist/capture/media-recorder.js +64 -0
- package/dist/capture/media-recorder.js.map +1 -0
- package/dist/capture/use-barcode-scanner.cjs +2 -0
- package/dist/capture/use-barcode-scanner.cjs.map +1 -0
- package/dist/capture/use-barcode-scanner.js +86 -0
- package/dist/capture/use-barcode-scanner.js.map +1 -0
- package/dist/capture/use-screen-capture.cjs +2 -0
- package/dist/capture/use-screen-capture.cjs.map +1 -0
- package/dist/capture/use-screen-capture.js +79 -0
- package/dist/capture/use-screen-capture.js.map +1 -0
- package/dist/capture/use-speech-recognition.cjs +2 -0
- package/dist/capture/use-speech-recognition.cjs.map +1 -0
- package/dist/capture/use-speech-recognition.js +118 -0
- package/dist/capture/use-speech-recognition.js.map +1 -0
- package/dist/capture/use-torch.cjs +2 -0
- package/dist/capture/use-torch.cjs.map +1 -0
- package/dist/capture/use-torch.js +33 -0
- package/dist/capture/use-torch.js.map +1 -0
- package/dist/capture/use-video-recorder.cjs +2 -0
- package/dist/capture/use-video-recorder.cjs.map +1 -0
- package/dist/capture/use-video-recorder.js +90 -0
- package/dist/capture/use-video-recorder.js.map +1 -0
- package/dist/capture/video-recorder.cjs +2 -0
- package/dist/capture/video-recorder.cjs.map +1 -0
- package/dist/capture/video-recorder.js +31 -0
- package/dist/capture/video-recorder.js.map +1 -0
- package/dist/components/AIChat/AIChat.cjs +1 -1
- package/dist/components/AIChat/AIChat.cjs.map +1 -1
- package/dist/components/AIChat/AIChat.js +93 -97
- package/dist/components/AIChat/AIChat.js.map +1 -1
- package/dist/components/AIChat/AIChatComposer.cjs +1 -1
- package/dist/components/AIChat/AIChatComposer.cjs.map +1 -1
- package/dist/components/AIChat/AIChatComposer.js +2 -1
- package/dist/components/AIChat/AIChatComposer.js.map +1 -1
- package/dist/components/AIChat/AIChatTurn.cjs +2 -2
- package/dist/components/AIChat/AIChatTurn.js +6 -6
- package/dist/components/AIChat/AIChatTurn.js.map +1 -1
- package/dist/components/Alert/Alert.module.cjs.map +1 -1
- package/dist/components/Alert/Alert.module.js.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.cjs +2 -0
- package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -0
- package/dist/components/AudioPlayer/AudioPlayer.js +123 -0
- package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -0
- package/dist/components/AudioPlayer/AudioPlayer.module.cjs +2 -0
- package/dist/components/AudioPlayer/AudioPlayer.module.cjs.map +1 -0
- package/dist/components/AudioPlayer/AudioPlayer.module.js +12 -0
- package/dist/components/AudioPlayer/AudioPlayer.module.js.map +1 -0
- package/dist/components/AudioRecorder/AudioRecorder.cjs +2 -0
- package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -0
- package/dist/components/AudioRecorder/AudioRecorder.js +179 -0
- package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -0
- package/dist/components/AudioRecorder/AudioRecorder.module.cjs +2 -0
- package/dist/components/AudioRecorder/AudioRecorder.module.cjs.map +1 -0
- package/dist/components/AudioRecorder/AudioRecorder.module.js +19 -0
- package/dist/components/AudioRecorder/AudioRecorder.module.js.map +1 -0
- package/dist/components/Badge/Badge.module.cjs.map +1 -1
- package/dist/components/Badge/Badge.module.js.map +1 -1
- package/dist/components/BarcodeScanner/BarcodeScanner.cjs +2 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.cjs.map +1 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.js +123 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.js.map +1 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.module.cjs +2 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.module.cjs.map +1 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.module.js +22 -0
- package/dist/components/BarcodeScanner/BarcodeScanner.module.js.map +1 -0
- package/dist/components/BottomNavigation/BottomNavigation.module.cjs.map +1 -1
- package/dist/components/BottomNavigation/BottomNavigation.module.js.map +1 -1
- package/dist/components/Button/Button.module.cjs.map +1 -1
- package/dist/components/Button/Button.module.js.map +1 -1
- package/dist/components/Combobox/Combobox.module.cjs.map +1 -1
- package/dist/components/Combobox/Combobox.module.js.map +1 -1
- package/dist/components/DataTable/DataTable.cjs +1 -1
- package/dist/components/DataTable/DataTable.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.js +203 -66
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/DataTable/DataTable.module.cjs +1 -1
- package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.module.js +9 -3
- package/dist/components/DataTable/DataTable.module.js.map +1 -1
- package/dist/components/DataTable/EditableCell.cjs +2 -0
- package/dist/components/DataTable/EditableCell.cjs.map +1 -0
- package/dist/components/DataTable/EditableCell.js +77 -0
- package/dist/components/DataTable/EditableCell.js.map +1 -0
- package/dist/components/DataTable/edit-labels.cjs +2 -0
- package/dist/components/DataTable/edit-labels.cjs.map +1 -0
- package/dist/components/DataTable/edit-labels.js +11 -0
- package/dist/components/DataTable/edit-labels.js.map +1 -0
- package/dist/components/DateRangePicker/DateRangePicker.module.cjs.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.module.js.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.module.cjs.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.module.js.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.module.cjs.map +1 -1
- package/dist/components/NavigationRail/NavigationRail.module.js.map +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.cjs +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.cjs.map +1 -1
- package/dist/components/SyncStatusBadge/SyncStatusBadge.js +33 -32
- package/dist/components/SyncStatusBadge/SyncStatusBadge.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 +1 -2
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/hooks/use-announce.cjs +2 -0
- package/dist/hooks/use-announce.cjs.map +1 -0
- package/dist/hooks/use-announce.js +50 -0
- package/dist/hooks/use-announce.js.map +1 -0
- package/dist/hooks/use-media-devices.cjs +2 -0
- package/dist/hooks/use-media-devices.cjs.map +1 -0
- package/dist/hooks/use-media-devices.js +46 -0
- package/dist/hooks/use-media-devices.js.map +1 -0
- package/dist/hooks/use-media-permission.cjs +2 -0
- package/dist/hooks/use-media-permission.cjs.map +1 -0
- package/dist/hooks/use-media-permission.js +33 -0
- package/dist/hooks/use-media-permission.js.map +1 -0
- package/dist/http/resumable-upload.cjs +2 -0
- package/dist/http/resumable-upload.cjs.map +1 -0
- package/dist/http/resumable-upload.js +249 -0
- package/dist/http/resumable-upload.js.map +1 -0
- package/dist/perf/cache-size.cjs +2 -0
- package/dist/perf/cache-size.cjs.map +1 -0
- package/dist/perf/cache-size.js +16 -0
- package/dist/perf/cache-size.js.map +1 -0
- package/dist/perf/device.cjs +2 -0
- package/dist/perf/device.cjs.map +1 -0
- package/dist/perf/device.js +22 -0
- package/dist/perf/device.js.map +1 -0
- package/dist/perf/format.cjs +2 -0
- package/dist/perf/format.cjs.map +1 -0
- package/dist/perf/format.js +8 -0
- package/dist/perf/format.js.map +1 -0
- package/dist/perf/profiler.cjs +2 -0
- package/dist/perf/profiler.cjs.map +1 -0
- package/dist/perf/profiler.js +46 -0
- package/dist/perf/profiler.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +2705 -38
- package/dist/tempest-react-sdk.js +268 -240
- package/dist/vision/core/timing.cjs +2 -0
- package/dist/vision/core/timing.cjs.map +1 -0
- package/dist/vision/core/timing.js +24 -0
- package/dist/vision/core/timing.js.map +1 -0
- package/dist/vision/index.cjs +1 -1
- package/dist/vision/index.cjs.map +1 -1
- package/dist/vision/index.js +16 -15
- package/dist/vision/index.js.map +1 -1
- package/dist/vision/postprocess/detection.cjs +1 -1
- package/dist/vision/postprocess/detection.cjs.map +1 -1
- package/dist/vision/postprocess/detection.js +2 -2
- package/dist/vision/postprocess/detection.js.map +1 -1
- package/dist/vision/postprocess/segmentation.cjs +1 -1
- package/dist/vision/postprocess/segmentation.cjs.map +1 -1
- package/dist/vision/postprocess/segmentation.js +1 -1
- package/dist/vision/postprocess/segmentation.js.map +1 -1
- package/dist/vision/results.cjs +1 -1
- package/dist/vision/results.cjs.map +1 -1
- package/dist/vision/results.js +18 -13
- package/dist/vision/results.js.map +1 -1
- package/dist/vision/tasks/classifier.cjs +1 -1
- package/dist/vision/tasks/classifier.cjs.map +1 -1
- package/dist/vision/tasks/classifier.js +46 -39
- package/dist/vision/tasks/classifier.js.map +1 -1
- package/dist/vision/tasks/detector.cjs +1 -1
- package/dist/vision/tasks/detector.cjs.map +1 -1
- package/dist/vision/tasks/detector.js +40 -33
- package/dist/vision/tasks/detector.js.map +1 -1
- package/dist/vision/tasks/segmenter.cjs +1 -1
- package/dist/vision/tasks/segmenter.cjs.map +1 -1
- package/dist/vision/tasks/segmenter.js +35 -28
- package/dist/vision/tasks/segmenter.js.map +1 -1
- package/dist/vision/use-camera-stream.cjs +1 -1
- package/dist/vision/use-camera-stream.cjs.map +1 -1
- package/dist/vision/use-camera-stream.js +40 -57
- package/dist/vision/use-camera-stream.js.map +1 -1
- package/dist/vision.cjs +1 -1
- package/dist/vision.d.ts +93 -13
- package/dist/vision.js +21 -20
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio-player.js","names":[],"sources":["../../src/audio/audio-player.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"audio-player.js","names":[],"sources":["../../src/audio/audio-player.ts"],"sourcesContent":["import { setAudioOutput } from \"./audio-output\";\n\nexport interface PlayAudioOptions {\n /** Volume between 0 and 1. Default: 1. */\n volume?: number;\n /** Loop the clip. Default: false. */\n loop?: boolean;\n /** Begin playback immediately. Default: true. */\n autoplay?: boolean;\n /** Stop the previous clip managed by this player. Default: false. */\n stopPrevious?: boolean;\n /**\n * Output device to play on, from `useMediaDevices().audioOutputs`.\n *\n * Ignored where the browser has no output routing (everything but Chromium) —\n * the clip still plays, on the system default. Useful for a chime that must land\n * on a headset while the call audio stays on the speakers.\n */\n sinkId?: string;\n /** Fired when playback ends naturally. */\n onEnded?: () => void;\n /** Fired on playback error. */\n onError?: (error: unknown) => void;\n}\n\n/**\n * Imperative handle over one \"current\" clip.\n *\n * Named `*Handle` like the SDK's other imperative handles (`ChatComposerHandle`,\n * `SignaturePadHandle`) — it is a control surface, not the `<AudioPlayer>` component.\n */\nexport interface AudioPlayerHandle {\n /** Play `src`. Returns the underlying element, or `null` when the browser blocked autoplay. */\n play: (src: string, options?: PlayAudioOptions) => Promise<HTMLAudioElement | null>;\n /** Stop the currently-playing clip and rewind it. */\n stop: () => void;\n /** Currently playing audio element, or `null`. */\n current: () => HTMLAudioElement | null;\n}\n\n/**\n * Create an isolated audio player that tracks a single \"current\" clip.\n * Multiple players coexist independently; use this when several layers of UI\n * need their own playback state.\n */\nexport function createAudioPlayer(): AudioPlayerHandle {\n let current: HTMLAudioElement | null = null;\n\n async function play(\n src: string,\n {\n volume = 1,\n loop = false,\n autoplay = true,\n stopPrevious = false,\n sinkId,\n onEnded,\n onError,\n }: PlayAudioOptions = {},\n ): Promise<HTMLAudioElement | null> {\n try {\n if (stopPrevious && current) {\n current.pause();\n current.currentTime = 0;\n }\n const audio = new Audio(src);\n audio.volume = Math.max(0, Math.min(1, volume));\n audio.loop = loop;\n audio.preload = \"auto\";\n if (onEnded) audio.onended = onEnded;\n if (onError) audio.onerror = (event) => onError(event);\n current = audio;\n // Routed before `play()`: applying a sink to an element that is already\n // playing restarts its audio pipeline and clips the first few ms.\n if (sinkId !== undefined) await setAudioOutput(audio, sinkId);\n if (autoplay) await audio.play();\n return audio;\n } catch (error) {\n onError?.(error);\n return null;\n }\n }\n\n function stop(): void {\n if (!current) return;\n current.pause();\n current.currentTime = 0;\n }\n\n return { play, stop, current: () => current };\n}\n\nlet defaultPlayer: AudioPlayerHandle | null = null;\n\nfunction getDefaultPlayer(): AudioPlayerHandle {\n if (!defaultPlayer) defaultPlayer = createAudioPlayer();\n return defaultPlayer;\n}\n\n/**\n * Convenience wrapper around a shared {@link AudioPlayerHandle}. Use this for\n * one-off notification sounds. For more complex flows (e.g. several\n * simultaneous channels), build a dedicated player with {@link createAudioPlayer}.\n */\nexport async function playAudio(\n src: string,\n options?: PlayAudioOptions,\n): Promise<HTMLAudioElement | null> {\n return getDefaultPlayer().play(src, options);\n}\n\n/** Stop the clip currently playing on the shared default player. */\nexport function stopAudio(): void {\n getDefaultPlayer().stop();\n}\n"],"mappings":";;AA6CA,SAAgB,IAAuC;CACnD,IAAI,IAAmC;CAEvC,eAAe,EACX,GACA,EACI,YAAS,GACT,UAAO,IACP,cAAW,IACX,kBAAe,IACf,WACA,YACA,eACkB,CAAC,GACS;EAChC,IAAI;GACA,AAAI,KAAgB,MAChB,EAAQ,MAAM,GACd,EAAQ,cAAc;GAE1B,IAAM,IAAQ,IAAI,MAAM,CAAG;GAW3B,OAVA,EAAM,SAAS,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,CAAM,CAAC,GAC9C,EAAM,OAAO,GACb,EAAM,UAAU,QACZ,MAAS,EAAM,UAAU,IACzB,MAAS,EAAM,WAAW,MAAU,EAAQ,CAAK,IACrD,IAAU,GAGN,MAAW,KAAA,KAAW,MAAM,EAAe,GAAO,CAAM,GACxD,KAAU,MAAM,EAAM,KAAK,GACxB;EACX,SAAS,GAAO;GAEZ,OADA,IAAU,CAAK,GACR;EACX;CACJ;CAEA,SAAS,IAAa;EACb,MACL,EAAQ,MAAM,GACd,EAAQ,cAAc;CAC1B;CAEA,OAAO;EAAE;EAAM;EAAM,eAAe;CAAQ;AAChD;AAEA,IAAI,IAA0C;AAE9C,SAAS,IAAsC;CAE3C,OADA,AAAoB,MAAgB,EAAkB,GAC/C;AACX;AAOA,eAAsB,EAClB,GACA,GACgC;CAChC,OAAO,EAAiB,CAAC,CAAC,KAAK,GAAK,CAAO;AAC/C;AAGA,SAAgB,IAAkB;CAC9B,EAAiB,CAAC,CAAC,KAAK;AAC5B"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../capture/media-recorder.cjs");var t=[`audio/webm;codecs=opus`,`audio/webm`,`audio/ogg;codecs=opus`,`audio/mp4;codecs=mp4a.40.2`,`audio/mp4`];function n(){return typeof MediaRecorder<`u`&&r()!==null}function r(n=t){return e.pickRecordingMimeType(n)}function i(n,r={}){return e.createMediaRecorder(n,{candidates:t,kind:`audio`,mimeType:r.mimeType,audioBitsPerSecond:r.audioBitsPerSecond,timesliceMs:r.timesliceMs,onChunk:r.onChunk,onError:r.onError})}exports.AUDIO_MIME_CANDIDATES=t,exports.createAudioRecorder=i,exports.isAudioRecordingSupported=n,exports.pickAudioMimeType=r;
|
|
2
|
+
//# sourceMappingURL=audio-recorder.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-recorder.cjs","names":[],"sources":["../../src/audio/audio-recorder.ts"],"sourcesContent":["import {\n createMediaRecorder,\n pickRecordingMimeType,\n type MediaRecorderStatus,\n} from \"@/capture/media-recorder\";\n\n/** Lifecycle of a recording. Shared with the video recorder — the states are the same. */\nexport type AudioRecorderStatus = MediaRecorderStatus;\n\n/**\n * Container/codec candidates, best first.\n *\n * No browser supports all of these, and none supports MP3 or WAV from\n * `MediaRecorder` — see the note on {@link pickAudioMimeType}. Opus in WebM is the\n * first choice because it is the smallest at speech bitrates and is what Chromium\n * and Firefox produce natively; `audio/mp4` is here for Safari, which produces AAC\n * and nothing else.\n *\n * Ported from the intersection of what `MediaRecorder.isTypeSupported` accepts in\n * Chrome 120+, Firefox 115+ and Safari 17. Re-check against\n * <https://developer.mozilla.org/docs/Web/API/MediaRecorder/isTypeSupported> before\n * adding to the list.\n */\nexport const AUDIO_MIME_CANDIDATES: readonly string[] = [\n \"audio/webm;codecs=opus\",\n \"audio/webm\",\n \"audio/ogg;codecs=opus\",\n \"audio/mp4;codecs=mp4a.40.2\",\n \"audio/mp4\",\n];\n\n/** A finished recording. */\nexport interface AudioRecording {\n /** The audio. Wrap with `useObjectUrl` to play it, or POST it as-is. */\n blob: Blob;\n /** What the browser actually produced — not necessarily what you asked for. */\n mimeType: string;\n /** Recorded length, excluding time spent paused. */\n durationMs: number;\n}\n\n/** Options for {@link createAudioRecorder}. */\nexport interface AudioRecorderOptions {\n /**\n * Force a container. Throws when the browser cannot produce it.\n *\n * Leave it out. The default negotiates from {@link AUDIO_MIME_CANDIDATES}, which\n * is the only way one call site works on both Chromium and Safari.\n */\n mimeType?: string;\n /** Target bitrate. 32000–64000 is plenty for speech in Opus. */\n audioBitsPerSecond?: number;\n /**\n * Emit a chunk every N ms through `onChunk`, for streaming upload.\n *\n * Without it the whole recording is buffered in memory until `stop()` — fine for\n * a voice note, not fine for an hour-long meeting.\n */\n timesliceMs?: number;\n /** Receives each chunk when `timesliceMs` is set. Chunks are **not** independently playable. */\n onChunk?: (chunk: Blob) => void;\n /** Recorder-level failure (device unplugged mid-recording, encoder error). */\n onError?: (error: unknown) => void;\n}\n\n/** Imperative recorder. */\nexport interface AudioRecorderHandle {\n /** Begin recording. No-op when already recording or paused. */\n start: () => void;\n /** Pause. The clock stops; `durationMs` freezes. */\n pause: () => void;\n /** Resume after `pause()`. */\n resume: () => void;\n /** Stop and resolve with the assembled recording. */\n stop: () => Promise<AudioRecording>;\n /** Stop and throw the audio away. */\n cancel: () => void;\n status: () => AudioRecorderStatus;\n /** Recorded length so far, excluding paused time. */\n durationMs: () => number;\n /** The negotiated container. */\n mimeType: string;\n}\n\n/** Whether `MediaRecorder` exists and can produce at least one audio container. */\nexport function isAudioRecordingSupported(): boolean {\n return typeof MediaRecorder !== \"undefined\" && pickAudioMimeType() !== null;\n}\n\n/**\n * First container in `preferred` the browser can actually produce, or `null`.\n *\n * **There is no MP3 or WAV here, and that is not an omission.** `MediaRecorder`\n * emits Opus (in WebM or Ogg) on Chromium and Firefox and AAC (in MP4) on Safari —\n * no engine implements an MP3 or WAV encoder for it. If a backend needs WAV,\n * {@link blobToWav} converts one client-side with no dependency; if it needs MP3,\n * transcode on the server. Shipping an MP3 encoder would mean a WASM build of the\n * order of 150 KB in every consumer's bundle to serve one format, which is the\n * trade this SDK does not make.\n *\n * @param preferred - Candidates, best first. Defaults to {@link AUDIO_MIME_CANDIDATES}.\n * @returns A supported MIME type, or `null` when none is.\n */\nexport function pickAudioMimeType(\n preferred: readonly string[] = AUDIO_MIME_CANDIDATES,\n): string | null {\n return pickRecordingMimeType(preferred);\n}\n\n/**\n * Wrap a `MediaStream` in a recorder with a real state machine and an honest clock.\n *\n * The engine is shared with the video recorder (`createMediaRecorder`), because the\n * subtle parts are not audio-specific: `MediaRecorder` reports **no duration** (WebM\n * it writes carries none in its header, which is why `<audio>` shows `Infinity` for a\n * fresh recording) so the clock is kept by hand and subtracts paused time; and\n * `stop()` returns *before* the last `dataavailable` event, so the blob can only be\n * assembled in `onstop`. What stays here is the part that genuinely differs — the\n * container list.\n *\n * The stream is **not** owned here: `stop()` leaves the microphone open so a retake\n * does not need a second permission round-trip. Release it with the owning\n * `useMicrophone().stop()`.\n *\n * @param stream - A live audio stream, usually from {@link useMicrophone}.\n * @param options - See {@link AudioRecorderOptions}.\n * @returns The imperative recorder.\n * @throws When `MediaRecorder` is unavailable, or an explicit `mimeType` is unsupported.\n */\nexport function createAudioRecorder(\n stream: MediaStream,\n options: AudioRecorderOptions = {},\n): AudioRecorderHandle {\n return createMediaRecorder(stream, {\n candidates: AUDIO_MIME_CANDIDATES,\n kind: \"audio\",\n mimeType: options.mimeType,\n audioBitsPerSecond: options.audioBitsPerSecond,\n timesliceMs: options.timesliceMs,\n onChunk: options.onChunk,\n onError: options.onError,\n });\n}\n"],"mappings":"iDAuBA,IAAa,EAA2C,CACpD,yBACA,aACA,wBACA,6BACA,WACJ,EAwDA,SAAgB,GAAqC,CACjD,OAAO,OAAO,cAAkB,KAAe,EAAkB,IAAM,IAC3E,CAgBA,SAAgB,EACZ,EAA+B,EAClB,CACb,OAAO,EAAA,sBAAsB,CAAS,CAC1C,CAsBA,SAAgB,EACZ,EACA,EAAgC,CAAC,EACd,CACnB,OAAO,EAAA,oBAAoB,EAAQ,CAC/B,WAAY,EACZ,KAAM,QACN,SAAU,EAAQ,SAClB,mBAAoB,EAAQ,mBAC5B,YAAa,EAAQ,YACrB,QAAS,EAAQ,QACjB,QAAS,EAAQ,OACrB,CAAC,CACL"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createMediaRecorder as e, pickRecordingMimeType as t } from "../capture/media-recorder.js";
|
|
2
|
+
//#region src/audio/audio-recorder.ts
|
|
3
|
+
var n = [
|
|
4
|
+
"audio/webm;codecs=opus",
|
|
5
|
+
"audio/webm",
|
|
6
|
+
"audio/ogg;codecs=opus",
|
|
7
|
+
"audio/mp4;codecs=mp4a.40.2",
|
|
8
|
+
"audio/mp4"
|
|
9
|
+
];
|
|
10
|
+
function r() {
|
|
11
|
+
return typeof MediaRecorder < "u" && i() !== null;
|
|
12
|
+
}
|
|
13
|
+
function i(e = n) {
|
|
14
|
+
return t(e);
|
|
15
|
+
}
|
|
16
|
+
function a(t, r = {}) {
|
|
17
|
+
return e(t, {
|
|
18
|
+
candidates: n,
|
|
19
|
+
kind: "audio",
|
|
20
|
+
mimeType: r.mimeType,
|
|
21
|
+
audioBitsPerSecond: r.audioBitsPerSecond,
|
|
22
|
+
timesliceMs: r.timesliceMs,
|
|
23
|
+
onChunk: r.onChunk,
|
|
24
|
+
onError: r.onError
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { n as AUDIO_MIME_CANDIDATES, a as createAudioRecorder, r as isAudioRecordingSupported, i as pickAudioMimeType };
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=audio-recorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-recorder.js","names":[],"sources":["../../src/audio/audio-recorder.ts"],"sourcesContent":["import {\n createMediaRecorder,\n pickRecordingMimeType,\n type MediaRecorderStatus,\n} from \"@/capture/media-recorder\";\n\n/** Lifecycle of a recording. Shared with the video recorder — the states are the same. */\nexport type AudioRecorderStatus = MediaRecorderStatus;\n\n/**\n * Container/codec candidates, best first.\n *\n * No browser supports all of these, and none supports MP3 or WAV from\n * `MediaRecorder` — see the note on {@link pickAudioMimeType}. Opus in WebM is the\n * first choice because it is the smallest at speech bitrates and is what Chromium\n * and Firefox produce natively; `audio/mp4` is here for Safari, which produces AAC\n * and nothing else.\n *\n * Ported from the intersection of what `MediaRecorder.isTypeSupported` accepts in\n * Chrome 120+, Firefox 115+ and Safari 17. Re-check against\n * <https://developer.mozilla.org/docs/Web/API/MediaRecorder/isTypeSupported> before\n * adding to the list.\n */\nexport const AUDIO_MIME_CANDIDATES: readonly string[] = [\n \"audio/webm;codecs=opus\",\n \"audio/webm\",\n \"audio/ogg;codecs=opus\",\n \"audio/mp4;codecs=mp4a.40.2\",\n \"audio/mp4\",\n];\n\n/** A finished recording. */\nexport interface AudioRecording {\n /** The audio. Wrap with `useObjectUrl` to play it, or POST it as-is. */\n blob: Blob;\n /** What the browser actually produced — not necessarily what you asked for. */\n mimeType: string;\n /** Recorded length, excluding time spent paused. */\n durationMs: number;\n}\n\n/** Options for {@link createAudioRecorder}. */\nexport interface AudioRecorderOptions {\n /**\n * Force a container. Throws when the browser cannot produce it.\n *\n * Leave it out. The default negotiates from {@link AUDIO_MIME_CANDIDATES}, which\n * is the only way one call site works on both Chromium and Safari.\n */\n mimeType?: string;\n /** Target bitrate. 32000–64000 is plenty for speech in Opus. */\n audioBitsPerSecond?: number;\n /**\n * Emit a chunk every N ms through `onChunk`, for streaming upload.\n *\n * Without it the whole recording is buffered in memory until `stop()` — fine for\n * a voice note, not fine for an hour-long meeting.\n */\n timesliceMs?: number;\n /** Receives each chunk when `timesliceMs` is set. Chunks are **not** independently playable. */\n onChunk?: (chunk: Blob) => void;\n /** Recorder-level failure (device unplugged mid-recording, encoder error). */\n onError?: (error: unknown) => void;\n}\n\n/** Imperative recorder. */\nexport interface AudioRecorderHandle {\n /** Begin recording. No-op when already recording or paused. */\n start: () => void;\n /** Pause. The clock stops; `durationMs` freezes. */\n pause: () => void;\n /** Resume after `pause()`. */\n resume: () => void;\n /** Stop and resolve with the assembled recording. */\n stop: () => Promise<AudioRecording>;\n /** Stop and throw the audio away. */\n cancel: () => void;\n status: () => AudioRecorderStatus;\n /** Recorded length so far, excluding paused time. */\n durationMs: () => number;\n /** The negotiated container. */\n mimeType: string;\n}\n\n/** Whether `MediaRecorder` exists and can produce at least one audio container. */\nexport function isAudioRecordingSupported(): boolean {\n return typeof MediaRecorder !== \"undefined\" && pickAudioMimeType() !== null;\n}\n\n/**\n * First container in `preferred` the browser can actually produce, or `null`.\n *\n * **There is no MP3 or WAV here, and that is not an omission.** `MediaRecorder`\n * emits Opus (in WebM or Ogg) on Chromium and Firefox and AAC (in MP4) on Safari —\n * no engine implements an MP3 or WAV encoder for it. If a backend needs WAV,\n * {@link blobToWav} converts one client-side with no dependency; if it needs MP3,\n * transcode on the server. Shipping an MP3 encoder would mean a WASM build of the\n * order of 150 KB in every consumer's bundle to serve one format, which is the\n * trade this SDK does not make.\n *\n * @param preferred - Candidates, best first. Defaults to {@link AUDIO_MIME_CANDIDATES}.\n * @returns A supported MIME type, or `null` when none is.\n */\nexport function pickAudioMimeType(\n preferred: readonly string[] = AUDIO_MIME_CANDIDATES,\n): string | null {\n return pickRecordingMimeType(preferred);\n}\n\n/**\n * Wrap a `MediaStream` in a recorder with a real state machine and an honest clock.\n *\n * The engine is shared with the video recorder (`createMediaRecorder`), because the\n * subtle parts are not audio-specific: `MediaRecorder` reports **no duration** (WebM\n * it writes carries none in its header, which is why `<audio>` shows `Infinity` for a\n * fresh recording) so the clock is kept by hand and subtracts paused time; and\n * `stop()` returns *before* the last `dataavailable` event, so the blob can only be\n * assembled in `onstop`. What stays here is the part that genuinely differs — the\n * container list.\n *\n * The stream is **not** owned here: `stop()` leaves the microphone open so a retake\n * does not need a second permission round-trip. Release it with the owning\n * `useMicrophone().stop()`.\n *\n * @param stream - A live audio stream, usually from {@link useMicrophone}.\n * @param options - See {@link AudioRecorderOptions}.\n * @returns The imperative recorder.\n * @throws When `MediaRecorder` is unavailable, or an explicit `mimeType` is unsupported.\n */\nexport function createAudioRecorder(\n stream: MediaStream,\n options: AudioRecorderOptions = {},\n): AudioRecorderHandle {\n return createMediaRecorder(stream, {\n candidates: AUDIO_MIME_CANDIDATES,\n kind: \"audio\",\n mimeType: options.mimeType,\n audioBitsPerSecond: options.audioBitsPerSecond,\n timesliceMs: options.timesliceMs,\n onChunk: options.onChunk,\n onError: options.onError,\n });\n}\n"],"mappings":";;AAuBA,IAAa,IAA2C;CACpD;CACA;CACA;CACA;CACA;AACJ;AAwDA,SAAgB,IAAqC;CACjD,OAAO,OAAO,gBAAkB,OAAe,EAAkB,MAAM;AAC3E;AAgBA,SAAgB,EACZ,IAA+B,GAClB;CACb,OAAO,EAAsB,CAAS;AAC1C;AAsBA,SAAgB,EACZ,GACA,IAAgC,CAAC,GACd;CACnB,OAAO,EAAoB,GAAQ;EAC/B,YAAY;EACZ,MAAM;EACN,UAAU,EAAQ;EAClB,oBAAoB,EAAQ;EAC5B,aAAa,EAAQ;EACrB,SAAS,EAAQ;EACjB,SAAS,EAAQ;CACrB,CAAC;AACL"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duration.cjs","names":[],"sources":["../../src/audio/duration.ts"],"sourcesContent":["/**\n * Format a length in milliseconds as a clock: `0:07`, `2:41`, `1:03:12`.\n *\n * Returns `\"--:--\"` for anything not finite, which is the case that matters: a fresh\n * `MediaRecorder` WebM carries no duration in its header, so `<audio>.duration` reads\n * `Infinity` until the browser has been coaxed into resolving it. Rendering `NaN:aN`\n * there is the bug every home-made audio player ships with.\n *\n * The hour field appears only when there is one, so a voice note reads `0:07` rather\n * than `0:00:07`.\n *\n * @param ms - Length in milliseconds, or a non-finite value for \"unknown\".\n * @returns A clock string.\n */\nexport function formatDuration(ms: number): string {\n if (!Number.isFinite(ms) || ms < 0) return \"--:--\";\n const total = Math.floor(ms / 1000);\n const seconds = total % 60;\n const minutes = Math.floor(total / 60) % 60;\n const hours = Math.floor(total / 3600);\n const pad = (value: number): string => String(value).padStart(2, \"0\");\n return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${minutes}:${pad(seconds)}`;\n}\n"],"mappings":"AAcA,SAAgB,EAAe,EAAoB,CAC/C,GAAI,CAAC,OAAO,SAAS,CAAE,GAAK,EAAK,EAAG,MAAO,QAC3C,IAAM,EAAQ,KAAK,MAAM,EAAK,GAAI,EAC5B,EAAU,EAAQ,GAClB,EAAU,KAAK,MAAM,EAAQ,EAAE,EAAI,GACnC,EAAQ,KAAK,MAAM,EAAQ,IAAI,EAC/B,EAAO,GAA0B,OAAO,CAAK,CAAC,CAAC,SAAS,EAAG,GAAG,EACpE,OAAO,EAAQ,EAAI,GAAG,EAAM,GAAG,EAAI,CAAO,EAAE,GAAG,EAAI,CAAO,IAAM,GAAG,EAAQ,GAAG,EAAI,CAAO,GAC7F"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/audio/duration.ts
|
|
2
|
+
function e(e) {
|
|
3
|
+
if (!Number.isFinite(e) || e < 0) return "--:--";
|
|
4
|
+
let t = Math.floor(e / 1e3), n = t % 60, r = Math.floor(t / 60) % 60, i = Math.floor(t / 3600), a = (e) => String(e).padStart(2, "0");
|
|
5
|
+
return i > 0 ? `${i}:${a(r)}:${a(n)}` : `${r}:${a(n)}`;
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
export { e as formatDuration };
|
|
9
|
+
|
|
10
|
+
//# sourceMappingURL=duration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duration.js","names":[],"sources":["../../src/audio/duration.ts"],"sourcesContent":["/**\n * Format a length in milliseconds as a clock: `0:07`, `2:41`, `1:03:12`.\n *\n * Returns `\"--:--\"` for anything not finite, which is the case that matters: a fresh\n * `MediaRecorder` WebM carries no duration in its header, so `<audio>.duration` reads\n * `Infinity` until the browser has been coaxed into resolving it. Rendering `NaN:aN`\n * there is the bug every home-made audio player ships with.\n *\n * The hour field appears only when there is one, so a voice note reads `0:07` rather\n * than `0:00:07`.\n *\n * @param ms - Length in milliseconds, or a non-finite value for \"unknown\".\n * @returns A clock string.\n */\nexport function formatDuration(ms: number): string {\n if (!Number.isFinite(ms) || ms < 0) return \"--:--\";\n const total = Math.floor(ms / 1000);\n const seconds = total % 60;\n const minutes = Math.floor(total / 60) % 60;\n const hours = Math.floor(total / 3600);\n const pad = (value: number): string => String(value).padStart(2, \"0\");\n return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${minutes}:${pad(seconds)}`;\n}\n"],"mappings":";AAcA,SAAgB,EAAe,GAAoB;CAC/C,IAAI,CAAC,OAAO,SAAS,CAAE,KAAK,IAAK,GAAG,OAAO;CAC3C,IAAM,IAAQ,KAAK,MAAM,IAAK,GAAI,GAC5B,IAAU,IAAQ,IAClB,IAAU,KAAK,MAAM,IAAQ,EAAE,IAAI,IACnC,IAAQ,KAAK,MAAM,IAAQ,IAAI,GAC/B,KAAO,MAA0B,OAAO,CAAK,CAAC,CAAC,SAAS,GAAG,GAAG;CACpE,OAAO,IAAQ,IAAI,GAAG,EAAM,GAAG,EAAI,CAAO,EAAE,GAAG,EAAI,CAAO,MAAM,GAAG,EAAQ,GAAG,EAAI,CAAO;AAC7F"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e,{fftSize:t=1024,decay:n=.7}={}){let r=typeof AudioContext<`u`?AudioContext:globalThis.webkitAudioContext;if(!r)return{level:()=>0,stop:()=>void 0};let i=new r,a=i.createMediaStreamSource(e),o=i.createAnalyser();o.fftSize=t,a.connect(o);let s=new Float32Array(o.fftSize),c=0,l=!1;return{level:()=>{if(l)return 0;o.getFloatTimeDomainData(s);let e=0;for(let t=0;t<s.length;t+=1)e+=s[t]*s[t];let t=Math.sqrt(e/s.length);return c=t>c?t:c*n+t*(1-n),Math.min(1,c)},stop:()=>{if(!l){l=!0;try{a.disconnect(),o.disconnect()}catch{}i.close().catch(()=>void 0)}}}}exports.createLevelMeter=e;
|
|
2
|
+
//# sourceMappingURL=level-meter.cjs.map
|
|
@@ -0,0 +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 * @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 // Instant attack, eased release.\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 // Already torn down with the context — nothing to release.\n }\n void context.close().catch(() => undefined);\n },\n };\n}\n"],"mappings":"AAmDA,SAAgB,EACZ,EACA,CAAE,UAAU,KAAM,QAAQ,IAA2B,CAAC,EAC5C,CACV,IAAM,EACF,OAAO,aAAiB,IAClB,aACC,WAA4D,mBAEvE,GAAI,CAAC,EACD,MAAO,CAAE,UAAa,EAAG,SAAY,IAAA,EAAU,EAGnD,IAAM,EAAU,IAAI,EACd,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,GAeb,MAAO,CACH,UAduB,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,EAG1C,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,CACA,EAAa,MAAM,CAAC,CAAC,UAAY,IAAA,EAAS,CAPjC,CAQb,CACJ,CACJ"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/audio/level-meter.ts
|
|
2
|
+
function e(e, { fftSize: t = 1024, decay: n = .7 } = {}) {
|
|
3
|
+
let r = typeof AudioContext < "u" ? AudioContext : globalThis.webkitAudioContext;
|
|
4
|
+
if (!r) return {
|
|
5
|
+
level: () => 0,
|
|
6
|
+
stop: () => void 0
|
|
7
|
+
};
|
|
8
|
+
let i = new r(), a = i.createMediaStreamSource(e), o = i.createAnalyser();
|
|
9
|
+
o.fftSize = t, a.connect(o);
|
|
10
|
+
let s = new Float32Array(o.fftSize), c = 0, l = !1;
|
|
11
|
+
return {
|
|
12
|
+
level: () => {
|
|
13
|
+
if (l) return 0;
|
|
14
|
+
o.getFloatTimeDomainData(s);
|
|
15
|
+
let e = 0;
|
|
16
|
+
for (let t = 0; t < s.length; t += 1) e += s[t] * s[t];
|
|
17
|
+
let t = Math.sqrt(e / s.length);
|
|
18
|
+
return c = t > c ? t : c * n + t * (1 - n), Math.min(1, c);
|
|
19
|
+
},
|
|
20
|
+
stop: () => {
|
|
21
|
+
if (!l) {
|
|
22
|
+
l = !0;
|
|
23
|
+
try {
|
|
24
|
+
a.disconnect(), o.disconnect();
|
|
25
|
+
} catch {}
|
|
26
|
+
i.close().catch(() => void 0);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { e as createLevelMeter };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=level-meter.js.map
|
|
@@ -0,0 +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 * @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 // Instant attack, eased release.\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 // Already torn down with the context — nothing to release.\n }\n void context.close().catch(() => undefined);\n },\n };\n}\n"],"mappings":";AAmDA,SAAgB,EACZ,GACA,EAAE,aAAU,MAAM,WAAQ,OAA2B,CAAC,GAC5C;CACV,IAAM,IACF,OAAO,eAAiB,MAClB,eACC,WAA4D;CAEvE,IAAI,CAAC,GACD,OAAO;EAAE,aAAa;EAAG,YAAY,KAAA;CAAU;CAGnD,IAAM,IAAU,IAAI,EAAK,GACnB,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;CAeb,OAAO;EACH,aAduB;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;GAG1C,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,EAAa,MAAM,CAAC,CAAC,YAAY,KAAA,CAAS;GAPjC;EAQb;CACJ;AACJ"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e={microphone:`Microphone`,camera:`Camera`,screen:`Screen`};function t(t,n=`microphone`){let r=e[n];if(typeof window>`u`)return{kind:`unsupported`,message:`${r} is unavailable in this environment.`};if(!window.isSecureContext)return{kind:`insecure`,message:`${r} access requires a secure (HTTPS) connection.`};if(t instanceof DOMException)switch(t.name){case`NotAllowedError`:case`SecurityError`:return{kind:`permission-denied`,message:`${r} permission denied. Enable access in your browser settings.`};case`NotFoundError`:case`OverconstrainedError`:return{kind:`not-found`,message:`No ${n} available on this device.`};case`NotReadableError`:case`AbortError`:return{kind:`in-use`,message:`The ${n} is in use by another app. Close it and try again.`}}return{kind:`unknown`,message:t instanceof Error?t.message:`Unexpected error while accessing the ${n}.`}}function n(){return typeof navigator<`u`&&navigator.mediaDevices!==void 0&&typeof navigator.mediaDevices.getUserMedia==`function`}function r(n=`microphone`){return typeof window<`u`&&!window.isSecureContext?t(null,n):{kind:`unsupported`,message:`${e[n]} capture is not supported in this browser.`}}exports.classifyMediaError=t,exports.isMediaCaptureSupported=n,exports.missingCaptureApiError=r;
|
|
2
|
+
//# sourceMappingURL=media-access.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-access.cjs","names":[],"sources":["../../src/audio/media-access.ts"],"sourcesContent":["/**\n * What a media error is about.\n *\n * `\"screen\"` is not a device in the `getUserMedia` sense — it is what\n * `getDisplayMedia` captures — but it fails through the same `DOMException` names, so\n * it is classified by the same function rather than by a second copy of the table.\n */\nexport type MediaDeviceKindLabel = \"microphone\" | \"camera\" | \"screen\";\n\n/** Classified reason a capture device could not be acquired. */\nexport type MediaAccessErrorKind =\n \"unsupported\" | \"insecure\" | \"permission-denied\" | \"not-found\" | \"in-use\" | \"unknown\";\n\n/** A classified capture error with a human-readable, English message. */\nexport interface MediaAccessError {\n kind: MediaAccessErrorKind;\n message: string;\n}\n\n/** Sentence fragment naming the device, so one classifier serves both. */\nconst NOUN: Record<MediaDeviceKindLabel, string> = {\n microphone: \"Microphone\",\n camera: \"Camera\",\n screen: \"Screen\",\n};\n\n/**\n * Map an unknown `getUserMedia` failure into a {@link MediaAccessError}.\n *\n * The order matters. A missing `navigator.mediaDevices` is almost never \"this\n * browser cannot do audio\" — it is a page served over plain HTTP, where the whole\n * API is simply absent. Reporting `unsupported` there sends the developer looking\n * for a polyfill for a problem an `https://` URL fixes, so the secure-context check\n * runs first and wins.\n *\n * The `DOMException.name` values are then collapsed into kinds an app can actually\n * branch on, because the raw names do not group the way a UI needs them:\n * `NotFoundError` and `OverconstrainedError` both mean \"you will not get a device\n * with these constraints\", and `NotReadableError` and `AbortError` both mean \"the\n * hardware is busy\" — most often another tab of the same app.\n *\n * @param err - Whatever `getUserMedia` (or a device query) rejected with.\n * @param device - Which device was being opened, for the message.\n * @returns A stable kind plus a message safe to show a user.\n */\nexport function classifyMediaError(\n err: unknown,\n device: MediaDeviceKindLabel = \"microphone\",\n): MediaAccessError {\n const noun = NOUN[device];\n\n if (typeof window === \"undefined\") {\n return { kind: \"unsupported\", message: `${noun} is unavailable in this environment.` };\n }\n if (!window.isSecureContext) {\n return {\n kind: \"insecure\",\n message: `${noun} access requires a secure (HTTPS) connection.`,\n };\n }\n if (err instanceof DOMException) {\n switch (err.name) {\n case \"NotAllowedError\":\n case \"SecurityError\":\n return {\n kind: \"permission-denied\",\n message: `${noun} permission denied. Enable access in your browser settings.`,\n };\n case \"NotFoundError\":\n case \"OverconstrainedError\":\n return {\n kind: \"not-found\",\n message: `No ${device} available on this device.`,\n };\n case \"NotReadableError\":\n case \"AbortError\":\n return {\n kind: \"in-use\",\n message: `The ${device} is in use by another app. Close it and try again.`,\n };\n }\n }\n return {\n kind: \"unknown\",\n message:\n err instanceof Error ? err.message : `Unexpected error while accessing the ${device}.`,\n };\n}\n\n/**\n * Whether `getUserMedia` is reachable at all.\n *\n * Separate from the classifier because a page often wants to hide a \"record\" button\n * entirely rather than render it and fail on click.\n */\nexport function isMediaCaptureSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n navigator.mediaDevices !== undefined &&\n typeof navigator.mediaDevices.getUserMedia === \"function\"\n );\n}\n\n/**\n * The error for \"the API is not here at all\", which is a different question from\n * \"the API rejected\".\n *\n * `classifyMediaError` takes a thrown value; a missing `navigator.mediaDevices` never\n * threw one, and handing it `null` would come back `unknown` — the least actionable\n * kind there is. The two real causes are an insecure page (the fix is a URL) and an\n * engine that genuinely cannot capture (the fix is another browser), and telling them\n * apart is the whole point.\n *\n * @param device - Which device was being opened, for the message.\n */\nexport function missingCaptureApiError(\n device: MediaDeviceKindLabel = \"microphone\",\n): MediaAccessError {\n if (typeof window !== \"undefined\" && !window.isSecureContext) {\n return classifyMediaError(null, device);\n }\n return {\n kind: \"unsupported\",\n message: `${NOUN[device]} capture is not supported in this browser.`,\n };\n}\n"],"mappings":"AAoBA,IAAM,EAA6C,CAC/C,WAAY,aACZ,OAAQ,SACR,OAAQ,QACZ,EAqBA,SAAgB,EACZ,EACA,EAA+B,aACf,CAChB,IAAM,EAAO,EAAK,GAElB,GAAI,OAAO,OAAW,IAClB,MAAO,CAAE,KAAM,cAAe,QAAS,GAAG,EAAK,qCAAsC,EAEzF,GAAI,CAAC,OAAO,gBACR,MAAO,CACH,KAAM,WACN,QAAS,GAAG,EAAK,8CACrB,EAEJ,GAAI,aAAe,aACf,OAAQ,EAAI,KAAZ,CACI,IAAK,kBACL,IAAK,gBACD,MAAO,CACH,KAAM,oBACN,QAAS,GAAG,EAAK,4DACrB,EACJ,IAAK,gBACL,IAAK,uBACD,MAAO,CACH,KAAM,YACN,QAAS,MAAM,EAAO,2BAC1B,EACJ,IAAK,mBACL,IAAK,aACD,MAAO,CACH,KAAM,SACN,QAAS,OAAO,EAAO,mDAC3B,CACR,CAEJ,MAAO,CACH,KAAM,UACN,QACI,aAAe,MAAQ,EAAI,QAAU,wCAAwC,EAAO,EAC5F,CACJ,CAQA,SAAgB,GAAmC,CAC/C,OACI,OAAO,UAAc,KACrB,UAAU,eAAiB,IAAA,IAC3B,OAAO,UAAU,aAAa,cAAiB,UAEvD,CAcA,SAAgB,EACZ,EAA+B,aACf,CAIhB,OAHI,OAAO,OAAW,KAAe,CAAC,OAAO,gBAClC,EAAmB,KAAM,CAAM,EAEnC,CACH,KAAM,cACN,QAAS,GAAG,EAAK,GAAQ,2CAC7B,CACJ"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//#region src/audio/media-access.ts
|
|
2
|
+
var e = {
|
|
3
|
+
microphone: "Microphone",
|
|
4
|
+
camera: "Camera",
|
|
5
|
+
screen: "Screen"
|
|
6
|
+
};
|
|
7
|
+
function t(t, n = "microphone") {
|
|
8
|
+
let r = e[n];
|
|
9
|
+
if (typeof window > "u") return {
|
|
10
|
+
kind: "unsupported",
|
|
11
|
+
message: `${r} is unavailable in this environment.`
|
|
12
|
+
};
|
|
13
|
+
if (!window.isSecureContext) return {
|
|
14
|
+
kind: "insecure",
|
|
15
|
+
message: `${r} access requires a secure (HTTPS) connection.`
|
|
16
|
+
};
|
|
17
|
+
if (t instanceof DOMException) switch (t.name) {
|
|
18
|
+
case "NotAllowedError":
|
|
19
|
+
case "SecurityError": return {
|
|
20
|
+
kind: "permission-denied",
|
|
21
|
+
message: `${r} permission denied. Enable access in your browser settings.`
|
|
22
|
+
};
|
|
23
|
+
case "NotFoundError":
|
|
24
|
+
case "OverconstrainedError": return {
|
|
25
|
+
kind: "not-found",
|
|
26
|
+
message: `No ${n} available on this device.`
|
|
27
|
+
};
|
|
28
|
+
case "NotReadableError":
|
|
29
|
+
case "AbortError": return {
|
|
30
|
+
kind: "in-use",
|
|
31
|
+
message: `The ${n} is in use by another app. Close it and try again.`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
kind: "unknown",
|
|
36
|
+
message: t instanceof Error ? t.message : `Unexpected error while accessing the ${n}.`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function n() {
|
|
40
|
+
return typeof navigator < "u" && navigator.mediaDevices !== void 0 && typeof navigator.mediaDevices.getUserMedia == "function";
|
|
41
|
+
}
|
|
42
|
+
function r(n = "microphone") {
|
|
43
|
+
return typeof window < "u" && !window.isSecureContext ? t(null, n) : {
|
|
44
|
+
kind: "unsupported",
|
|
45
|
+
message: `${e[n]} capture is not supported in this browser.`
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { t as classifyMediaError, n as isMediaCaptureSupported, r as missingCaptureApiError };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=media-access.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-access.js","names":[],"sources":["../../src/audio/media-access.ts"],"sourcesContent":["/**\n * What a media error is about.\n *\n * `\"screen\"` is not a device in the `getUserMedia` sense — it is what\n * `getDisplayMedia` captures — but it fails through the same `DOMException` names, so\n * it is classified by the same function rather than by a second copy of the table.\n */\nexport type MediaDeviceKindLabel = \"microphone\" | \"camera\" | \"screen\";\n\n/** Classified reason a capture device could not be acquired. */\nexport type MediaAccessErrorKind =\n \"unsupported\" | \"insecure\" | \"permission-denied\" | \"not-found\" | \"in-use\" | \"unknown\";\n\n/** A classified capture error with a human-readable, English message. */\nexport interface MediaAccessError {\n kind: MediaAccessErrorKind;\n message: string;\n}\n\n/** Sentence fragment naming the device, so one classifier serves both. */\nconst NOUN: Record<MediaDeviceKindLabel, string> = {\n microphone: \"Microphone\",\n camera: \"Camera\",\n screen: \"Screen\",\n};\n\n/**\n * Map an unknown `getUserMedia` failure into a {@link MediaAccessError}.\n *\n * The order matters. A missing `navigator.mediaDevices` is almost never \"this\n * browser cannot do audio\" — it is a page served over plain HTTP, where the whole\n * API is simply absent. Reporting `unsupported` there sends the developer looking\n * for a polyfill for a problem an `https://` URL fixes, so the secure-context check\n * runs first and wins.\n *\n * The `DOMException.name` values are then collapsed into kinds an app can actually\n * branch on, because the raw names do not group the way a UI needs them:\n * `NotFoundError` and `OverconstrainedError` both mean \"you will not get a device\n * with these constraints\", and `NotReadableError` and `AbortError` both mean \"the\n * hardware is busy\" — most often another tab of the same app.\n *\n * @param err - Whatever `getUserMedia` (or a device query) rejected with.\n * @param device - Which device was being opened, for the message.\n * @returns A stable kind plus a message safe to show a user.\n */\nexport function classifyMediaError(\n err: unknown,\n device: MediaDeviceKindLabel = \"microphone\",\n): MediaAccessError {\n const noun = NOUN[device];\n\n if (typeof window === \"undefined\") {\n return { kind: \"unsupported\", message: `${noun} is unavailable in this environment.` };\n }\n if (!window.isSecureContext) {\n return {\n kind: \"insecure\",\n message: `${noun} access requires a secure (HTTPS) connection.`,\n };\n }\n if (err instanceof DOMException) {\n switch (err.name) {\n case \"NotAllowedError\":\n case \"SecurityError\":\n return {\n kind: \"permission-denied\",\n message: `${noun} permission denied. Enable access in your browser settings.`,\n };\n case \"NotFoundError\":\n case \"OverconstrainedError\":\n return {\n kind: \"not-found\",\n message: `No ${device} available on this device.`,\n };\n case \"NotReadableError\":\n case \"AbortError\":\n return {\n kind: \"in-use\",\n message: `The ${device} is in use by another app. Close it and try again.`,\n };\n }\n }\n return {\n kind: \"unknown\",\n message:\n err instanceof Error ? err.message : `Unexpected error while accessing the ${device}.`,\n };\n}\n\n/**\n * Whether `getUserMedia` is reachable at all.\n *\n * Separate from the classifier because a page often wants to hide a \"record\" button\n * entirely rather than render it and fail on click.\n */\nexport function isMediaCaptureSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n navigator.mediaDevices !== undefined &&\n typeof navigator.mediaDevices.getUserMedia === \"function\"\n );\n}\n\n/**\n * The error for \"the API is not here at all\", which is a different question from\n * \"the API rejected\".\n *\n * `classifyMediaError` takes a thrown value; a missing `navigator.mediaDevices` never\n * threw one, and handing it `null` would come back `unknown` — the least actionable\n * kind there is. The two real causes are an insecure page (the fix is a URL) and an\n * engine that genuinely cannot capture (the fix is another browser), and telling them\n * apart is the whole point.\n *\n * @param device - Which device was being opened, for the message.\n */\nexport function missingCaptureApiError(\n device: MediaDeviceKindLabel = \"microphone\",\n): MediaAccessError {\n if (typeof window !== \"undefined\" && !window.isSecureContext) {\n return classifyMediaError(null, device);\n }\n return {\n kind: \"unsupported\",\n message: `${NOUN[device]} capture is not supported in this browser.`,\n };\n}\n"],"mappings":";AAoBA,IAAM,IAA6C;CAC/C,YAAY;CACZ,QAAQ;CACR,QAAQ;AACZ;AAqBA,SAAgB,EACZ,GACA,IAA+B,cACf;CAChB,IAAM,IAAO,EAAK;CAElB,IAAI,OAAO,SAAW,KAClB,OAAO;EAAE,MAAM;EAAe,SAAS,GAAG,EAAK;CAAsC;CAEzF,IAAI,CAAC,OAAO,iBACR,OAAO;EACH,MAAM;EACN,SAAS,GAAG,EAAK;CACrB;CAEJ,IAAI,aAAe,cACf,QAAQ,EAAI,MAAZ;EACI,KAAK;EACL,KAAK,iBACD,OAAO;GACH,MAAM;GACN,SAAS,GAAG,EAAK;EACrB;EACJ,KAAK;EACL,KAAK,wBACD,OAAO;GACH,MAAM;GACN,SAAS,MAAM,EAAO;EAC1B;EACJ,KAAK;EACL,KAAK,cACD,OAAO;GACH,MAAM;GACN,SAAS,OAAO,EAAO;EAC3B;CACR;CAEJ,OAAO;EACH,MAAM;EACN,SACI,aAAe,QAAQ,EAAI,UAAU,wCAAwC,EAAO;CAC5F;AACJ;AAQA,SAAgB,IAAmC;CAC/C,OACI,OAAO,YAAc,OACrB,UAAU,iBAAiB,KAAA,KAC3B,OAAO,UAAU,aAAa,gBAAiB;AAEvD;AAcA,SAAgB,EACZ,IAA+B,cACf;CAIhB,OAHI,OAAO,SAAW,OAAe,CAAC,OAAO,kBAClC,EAAmB,MAAM,CAAM,IAEnC;EACH,MAAM;EACN,SAAS,GAAG,EAAK,GAAQ;CAC7B;AACJ"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../hooks/use-stable-callback.cjs"),t=require("./audio-recorder.cjs"),n=require("./level-meter.cjs");let r=require("react");function i(i,a={}){let{maxDurationMs:o,onRecorded:s,onChunk:c,onError:l,mimeType:u,audioBitsPerSecond:d,timesliceMs:f,tickMs:p=100,disableLevelMeter:m=!1}=a,[h,g]=(0,r.useState)(`idle`),[_,v]=(0,r.useState)(0),[y,b]=(0,r.useState)(0),[x,S]=(0,r.useState)(null),[C,w]=(0,r.useState)(null),[T,E]=(0,r.useState)(!1),D=(0,r.useRef)(null),O=(0,r.useRef)(null),k=e.useStableCallback(e=>s?.(e)),A=e.useStableCallback(e=>c?.(e)),j=e.useStableCallback(e=>{w(e),l?.(e)});(0,r.useEffect)(()=>{if(!i){E(!1);return}let e;try{e=t.createAudioRecorder(i,{mimeType:u,audioBitsPerSecond:d,timesliceMs:f,onChunk:A,onError:j})}catch(e){w(e),E(!1);return}return D.current=e,O.current=m?null:n.createLevelMeter(i),E(!0),g(`idle`),()=>{(e.status()===`recording`||e.status()===`paused`)&&e.cancel(),O.current?.stop(),O.current=null,D.current=null,E(!1)}},[i,m,u,d,f,A,j]);let M=(0,r.useCallback)(async()=>{let e=D.current;if(!e)return null;let t=e.status();if(t!==`recording`&&t!==`paused`)return null;let n=await e.stop();return g(`stopped`),v(n.durationMs),b(0),S(n),k(n),n},[k]);return(0,r.useEffect)(()=>{if(h!==`recording`&&h!==`paused`){b(0);return}let e=setInterval(()=>{let e=D.current;if(!e)return;let t=e.durationMs();v(t),b(h===`recording`?O.current?.level()??0:0),o!==void 0&&t>=o&&M()},p);return()=>clearInterval(e)},[h,p,o,M]),{status:h,durationMs:_,level:y,recording:x,error:C,ready:T,start:(0,r.useCallback)(()=>{let e=D.current;e&&(S(null),w(null),v(0),e.start(),g(e.status()))},[]),pause:(0,r.useCallback)(()=>{let e=D.current;e&&(e.pause(),v(e.durationMs()),g(e.status()))},[]),resume:(0,r.useCallback)(()=>{let e=D.current;e&&(e.resume(),g(e.status()))},[]),stop:M,cancel:(0,r.useCallback)(()=>{let e=D.current;e&&(e.cancel(),g(`idle`),v(0),b(0),S(null))},[])}}exports.useAudioRecorder=i;
|
|
2
|
+
//# sourceMappingURL=use-audio-recorder.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-audio-recorder.cjs","names":[],"sources":["../../src/audio/use-audio-recorder.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\nimport {\n createAudioRecorder,\n type AudioRecorderHandle,\n type AudioRecorderOptions,\n type AudioRecorderStatus,\n type AudioRecording,\n} from \"./audio-recorder\";\nimport { createLevelMeter, type LevelMeter } from \"./level-meter\";\n\n/** Options for {@link useAudioRecorder}. */\nexport interface UseAudioRecorderOptions extends AudioRecorderOptions {\n /**\n * Stop automatically after this many milliseconds.\n *\n * Worth setting on anything user-facing: a recording left running by accident is\n * a memory buffer that grows until the tab dies.\n */\n maxDurationMs?: number;\n /** Called once the recording is assembled, whether by `stop()` or `maxDurationMs`. */\n onRecorded?: (recording: AudioRecording) => void;\n /** How often the clock and the level are published, in ms. Default 100. */\n tickMs?: number;\n /** Skip the level meter — it costs an `AudioContext`. Default `false`. */\n disableLevelMeter?: boolean;\n}\n\n/** Value returned by {@link useAudioRecorder}. */\nexport interface UseAudioRecorderResult {\n status: AudioRecorderStatus;\n /** Recorded length so far, excluding paused time. */\n durationMs: number;\n /** Loudness 0–1, or `0` when the meter is off or nothing is recording. */\n level: number;\n /** The finished recording, or `null` before the first `stop()`. */\n recording: AudioRecording | null;\n /** Recorder-level error (encoder failure, device pulled mid-recording). */\n error: unknown;\n /** `false` when there is no stream yet, or the browser cannot record. */\n ready: boolean;\n start: () => void;\n pause: () => void;\n resume: () => void;\n /** Stop and resolve with the recording. Also published on `recording`. */\n stop: () => Promise<AudioRecording | null>;\n /** Stop and throw the audio away. */\n cancel: () => void;\n}\n\n/**\n * Record the given stream, with a clock, a level meter and a state machine.\n *\n * Pass the stream from {@link useMicrophone}; the hook stays `ready: false` until\n * there is one, so a page can render the whole recorder UI before permission is\n * granted and simply have it disabled.\n *\n * The clock and the level are published on an interval (`tickMs`, default 100 ms)\n * rather than per animation frame. A hook that set state 60 times a second would\n * re-render everything the recorder is inside; 10 Hz is smooth enough for a timer and\n * a meter, and the underlying `createLevelMeter` is still available for anyone who\n * wants a frame-accurate bar written straight to the DOM.\n *\n * @param stream - The stream to record, or `null` while permission is pending.\n * @param options - See {@link UseAudioRecorderOptions}.\n *\n * @example\n * const mic = useMicrophone();\n * const rec = useAudioRecorder(mic.stream, { maxDurationMs: 120_000 });\n * <button disabled={!rec.ready} onClick={rec.start}>Gravar</button>\n */\nexport function useAudioRecorder(\n stream: MediaStream | null,\n options: UseAudioRecorderOptions = {},\n): UseAudioRecorderResult {\n const {\n maxDurationMs,\n onRecorded,\n onChunk,\n onError,\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n tickMs = 100,\n disableLevelMeter = false,\n } = options;\n\n const [status, setStatus] = useState<AudioRecorderStatus>(\"idle\");\n const [durationMs, setDurationMs] = useState(0);\n const [level, setLevel] = useState(0);\n const [recording, setRecording] = useState<AudioRecording | null>(null);\n const [error, setError] = useState<unknown>(null);\n const [ready, setReady] = useState(false);\n\n const recorderRef = useRef<AudioRecorderHandle | null>(null);\n const meterRef = useRef<LevelMeter | null>(null);\n\n const emitRecorded = useStableCallback((result: AudioRecording) => onRecorded?.(result));\n const emitChunk = useStableCallback((chunk: Blob) => onChunk?.(chunk));\n const emitError = useStableCallback((err: unknown) => {\n setError(err);\n onError?.(err);\n });\n\n /**\n * Build the recorder and the meter for a stream, and tear both down when it\n * changes.\n *\n * The recorder is rebuilt per stream because `MediaRecorder` is bound to the\n * stream it was constructed with — swapping microphones mid-session hands us a\n * new `MediaStream`, and reusing the old recorder would keep recording the device\n * the user just switched away from. It is rebuilt on a container or bitrate change\n * for the same reason: both are constructor arguments.\n */\n useEffect(() => {\n if (!stream) {\n setReady(false);\n return;\n }\n let recorder: AudioRecorderHandle;\n try {\n recorder = createAudioRecorder(stream, {\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n onChunk: emitChunk,\n onError: emitError,\n });\n } catch (err) {\n setError(err);\n setReady(false);\n return;\n }\n recorderRef.current = recorder;\n meterRef.current = disableLevelMeter ? null : createLevelMeter(stream);\n setReady(true);\n setStatus(\"idle\");\n\n return () => {\n if (recorder.status() === \"recording\" || recorder.status() === \"paused\") {\n recorder.cancel();\n }\n meterRef.current?.stop();\n meterRef.current = null;\n recorderRef.current = null;\n setReady(false);\n };\n }, [\n stream,\n disableLevelMeter,\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n emitChunk,\n emitError,\n ]);\n\n const stop = useCallback(async (): Promise<AudioRecording | null> => {\n const recorder = recorderRef.current;\n if (!recorder) return null;\n const current = recorder.status();\n if (current !== \"recording\" && current !== \"paused\") return null;\n const result = await recorder.stop();\n setStatus(\"stopped\");\n setDurationMs(result.durationMs);\n setLevel(0);\n setRecording(result);\n emitRecorded(result);\n return result;\n }, [emitRecorded]);\n\n /**\n * Publish the clock and the level while something is running, and enforce\n * `maxDurationMs`.\n *\n * The cap is checked on the same tick rather than with a `setTimeout` armed at\n * `start()`, because a paused recording must not keep counting toward it — a\n * timeout would fire while the user is paused mid-sentence.\n */\n useEffect(() => {\n if (status !== \"recording\" && status !== \"paused\") {\n setLevel(0);\n return;\n }\n const id = setInterval(() => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n const elapsed = recorder.durationMs();\n setDurationMs(elapsed);\n setLevel(status === \"recording\" ? (meterRef.current?.level() ?? 0) : 0);\n if (maxDurationMs !== undefined && elapsed >= maxDurationMs) void stop();\n }, tickMs);\n return () => clearInterval(id);\n }, [status, tickMs, maxDurationMs, stop]);\n\n const start = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n setRecording(null);\n setError(null);\n setDurationMs(0);\n recorder.start();\n setStatus(recorder.status());\n }, []);\n\n const pause = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.pause();\n setDurationMs(recorder.durationMs());\n setStatus(recorder.status());\n }, []);\n\n const resume = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.resume();\n setStatus(recorder.status());\n }, []);\n\n const cancel = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.cancel();\n setStatus(\"idle\");\n setDurationMs(0);\n setLevel(0);\n setRecording(null);\n }, []);\n\n return {\n status,\n durationMs,\n level,\n recording,\n error,\n ready,\n start,\n pause,\n resume,\n stop,\n cancel,\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useStableCallback as e } from "../hooks/use-stable-callback.js";
|
|
2
|
+
import { createAudioRecorder as t } from "./audio-recorder.js";
|
|
3
|
+
import { createLevelMeter as n } from "./level-meter.js";
|
|
4
|
+
import { useCallback as r, useEffect as i, useRef as a, useState as o } from "react";
|
|
5
|
+
//#region src/audio/use-audio-recorder.ts
|
|
6
|
+
function s(s, c = {}) {
|
|
7
|
+
let { maxDurationMs: l, onRecorded: u, onChunk: d, onError: f, mimeType: p, audioBitsPerSecond: m, timesliceMs: h, tickMs: g = 100, disableLevelMeter: _ = !1 } = c, [v, y] = o("idle"), [b, x] = o(0), [S, C] = o(0), [w, T] = o(null), [E, D] = o(null), [O, k] = o(!1), A = a(null), j = a(null), M = e((e) => u?.(e)), N = e((e) => d?.(e)), P = e((e) => {
|
|
8
|
+
D(e), f?.(e);
|
|
9
|
+
});
|
|
10
|
+
i(() => {
|
|
11
|
+
if (!s) {
|
|
12
|
+
k(!1);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
let e;
|
|
16
|
+
try {
|
|
17
|
+
e = t(s, {
|
|
18
|
+
mimeType: p,
|
|
19
|
+
audioBitsPerSecond: m,
|
|
20
|
+
timesliceMs: h,
|
|
21
|
+
onChunk: N,
|
|
22
|
+
onError: P
|
|
23
|
+
});
|
|
24
|
+
} catch (e) {
|
|
25
|
+
D(e), k(!1);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
return A.current = e, j.current = _ ? null : n(s), k(!0), y("idle"), () => {
|
|
29
|
+
(e.status() === "recording" || e.status() === "paused") && e.cancel(), j.current?.stop(), j.current = null, A.current = null, k(!1);
|
|
30
|
+
};
|
|
31
|
+
}, [
|
|
32
|
+
s,
|
|
33
|
+
_,
|
|
34
|
+
p,
|
|
35
|
+
m,
|
|
36
|
+
h,
|
|
37
|
+
N,
|
|
38
|
+
P
|
|
39
|
+
]);
|
|
40
|
+
let F = r(async () => {
|
|
41
|
+
let e = A.current;
|
|
42
|
+
if (!e) return null;
|
|
43
|
+
let t = e.status();
|
|
44
|
+
if (t !== "recording" && t !== "paused") return null;
|
|
45
|
+
let n = await e.stop();
|
|
46
|
+
return y("stopped"), x(n.durationMs), C(0), T(n), M(n), n;
|
|
47
|
+
}, [M]);
|
|
48
|
+
return i(() => {
|
|
49
|
+
if (v !== "recording" && v !== "paused") {
|
|
50
|
+
C(0);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let e = setInterval(() => {
|
|
54
|
+
let e = A.current;
|
|
55
|
+
if (!e) return;
|
|
56
|
+
let t = e.durationMs();
|
|
57
|
+
x(t), C(v === "recording" ? j.current?.level() ?? 0 : 0), l !== void 0 && t >= l && F();
|
|
58
|
+
}, g);
|
|
59
|
+
return () => clearInterval(e);
|
|
60
|
+
}, [
|
|
61
|
+
v,
|
|
62
|
+
g,
|
|
63
|
+
l,
|
|
64
|
+
F
|
|
65
|
+
]), {
|
|
66
|
+
status: v,
|
|
67
|
+
durationMs: b,
|
|
68
|
+
level: S,
|
|
69
|
+
recording: w,
|
|
70
|
+
error: E,
|
|
71
|
+
ready: O,
|
|
72
|
+
start: r(() => {
|
|
73
|
+
let e = A.current;
|
|
74
|
+
e && (T(null), D(null), x(0), e.start(), y(e.status()));
|
|
75
|
+
}, []),
|
|
76
|
+
pause: r(() => {
|
|
77
|
+
let e = A.current;
|
|
78
|
+
e && (e.pause(), x(e.durationMs()), y(e.status()));
|
|
79
|
+
}, []),
|
|
80
|
+
resume: r(() => {
|
|
81
|
+
let e = A.current;
|
|
82
|
+
e && (e.resume(), y(e.status()));
|
|
83
|
+
}, []),
|
|
84
|
+
stop: F,
|
|
85
|
+
cancel: r(() => {
|
|
86
|
+
let e = A.current;
|
|
87
|
+
e && (e.cancel(), y("idle"), x(0), C(0), T(null));
|
|
88
|
+
}, [])
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
export { s as useAudioRecorder };
|
|
93
|
+
|
|
94
|
+
//# sourceMappingURL=use-audio-recorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-audio-recorder.js","names":[],"sources":["../../src/audio/use-audio-recorder.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { useStableCallback } from \"@/hooks/use-stable-callback\";\n\nimport {\n createAudioRecorder,\n type AudioRecorderHandle,\n type AudioRecorderOptions,\n type AudioRecorderStatus,\n type AudioRecording,\n} from \"./audio-recorder\";\nimport { createLevelMeter, type LevelMeter } from \"./level-meter\";\n\n/** Options for {@link useAudioRecorder}. */\nexport interface UseAudioRecorderOptions extends AudioRecorderOptions {\n /**\n * Stop automatically after this many milliseconds.\n *\n * Worth setting on anything user-facing: a recording left running by accident is\n * a memory buffer that grows until the tab dies.\n */\n maxDurationMs?: number;\n /** Called once the recording is assembled, whether by `stop()` or `maxDurationMs`. */\n onRecorded?: (recording: AudioRecording) => void;\n /** How often the clock and the level are published, in ms. Default 100. */\n tickMs?: number;\n /** Skip the level meter — it costs an `AudioContext`. Default `false`. */\n disableLevelMeter?: boolean;\n}\n\n/** Value returned by {@link useAudioRecorder}. */\nexport interface UseAudioRecorderResult {\n status: AudioRecorderStatus;\n /** Recorded length so far, excluding paused time. */\n durationMs: number;\n /** Loudness 0–1, or `0` when the meter is off or nothing is recording. */\n level: number;\n /** The finished recording, or `null` before the first `stop()`. */\n recording: AudioRecording | null;\n /** Recorder-level error (encoder failure, device pulled mid-recording). */\n error: unknown;\n /** `false` when there is no stream yet, or the browser cannot record. */\n ready: boolean;\n start: () => void;\n pause: () => void;\n resume: () => void;\n /** Stop and resolve with the recording. Also published on `recording`. */\n stop: () => Promise<AudioRecording | null>;\n /** Stop and throw the audio away. */\n cancel: () => void;\n}\n\n/**\n * Record the given stream, with a clock, a level meter and a state machine.\n *\n * Pass the stream from {@link useMicrophone}; the hook stays `ready: false` until\n * there is one, so a page can render the whole recorder UI before permission is\n * granted and simply have it disabled.\n *\n * The clock and the level are published on an interval (`tickMs`, default 100 ms)\n * rather than per animation frame. A hook that set state 60 times a second would\n * re-render everything the recorder is inside; 10 Hz is smooth enough for a timer and\n * a meter, and the underlying `createLevelMeter` is still available for anyone who\n * wants a frame-accurate bar written straight to the DOM.\n *\n * @param stream - The stream to record, or `null` while permission is pending.\n * @param options - See {@link UseAudioRecorderOptions}.\n *\n * @example\n * const mic = useMicrophone();\n * const rec = useAudioRecorder(mic.stream, { maxDurationMs: 120_000 });\n * <button disabled={!rec.ready} onClick={rec.start}>Gravar</button>\n */\nexport function useAudioRecorder(\n stream: MediaStream | null,\n options: UseAudioRecorderOptions = {},\n): UseAudioRecorderResult {\n const {\n maxDurationMs,\n onRecorded,\n onChunk,\n onError,\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n tickMs = 100,\n disableLevelMeter = false,\n } = options;\n\n const [status, setStatus] = useState<AudioRecorderStatus>(\"idle\");\n const [durationMs, setDurationMs] = useState(0);\n const [level, setLevel] = useState(0);\n const [recording, setRecording] = useState<AudioRecording | null>(null);\n const [error, setError] = useState<unknown>(null);\n const [ready, setReady] = useState(false);\n\n const recorderRef = useRef<AudioRecorderHandle | null>(null);\n const meterRef = useRef<LevelMeter | null>(null);\n\n const emitRecorded = useStableCallback((result: AudioRecording) => onRecorded?.(result));\n const emitChunk = useStableCallback((chunk: Blob) => onChunk?.(chunk));\n const emitError = useStableCallback((err: unknown) => {\n setError(err);\n onError?.(err);\n });\n\n /**\n * Build the recorder and the meter for a stream, and tear both down when it\n * changes.\n *\n * The recorder is rebuilt per stream because `MediaRecorder` is bound to the\n * stream it was constructed with — swapping microphones mid-session hands us a\n * new `MediaStream`, and reusing the old recorder would keep recording the device\n * the user just switched away from. It is rebuilt on a container or bitrate change\n * for the same reason: both are constructor arguments.\n */\n useEffect(() => {\n if (!stream) {\n setReady(false);\n return;\n }\n let recorder: AudioRecorderHandle;\n try {\n recorder = createAudioRecorder(stream, {\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n onChunk: emitChunk,\n onError: emitError,\n });\n } catch (err) {\n setError(err);\n setReady(false);\n return;\n }\n recorderRef.current = recorder;\n meterRef.current = disableLevelMeter ? null : createLevelMeter(stream);\n setReady(true);\n setStatus(\"idle\");\n\n return () => {\n if (recorder.status() === \"recording\" || recorder.status() === \"paused\") {\n recorder.cancel();\n }\n meterRef.current?.stop();\n meterRef.current = null;\n recorderRef.current = null;\n setReady(false);\n };\n }, [\n stream,\n disableLevelMeter,\n mimeType,\n audioBitsPerSecond,\n timesliceMs,\n emitChunk,\n emitError,\n ]);\n\n const stop = useCallback(async (): Promise<AudioRecording | null> => {\n const recorder = recorderRef.current;\n if (!recorder) return null;\n const current = recorder.status();\n if (current !== \"recording\" && current !== \"paused\") return null;\n const result = await recorder.stop();\n setStatus(\"stopped\");\n setDurationMs(result.durationMs);\n setLevel(0);\n setRecording(result);\n emitRecorded(result);\n return result;\n }, [emitRecorded]);\n\n /**\n * Publish the clock and the level while something is running, and enforce\n * `maxDurationMs`.\n *\n * The cap is checked on the same tick rather than with a `setTimeout` armed at\n * `start()`, because a paused recording must not keep counting toward it — a\n * timeout would fire while the user is paused mid-sentence.\n */\n useEffect(() => {\n if (status !== \"recording\" && status !== \"paused\") {\n setLevel(0);\n return;\n }\n const id = setInterval(() => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n const elapsed = recorder.durationMs();\n setDurationMs(elapsed);\n setLevel(status === \"recording\" ? (meterRef.current?.level() ?? 0) : 0);\n if (maxDurationMs !== undefined && elapsed >= maxDurationMs) void stop();\n }, tickMs);\n return () => clearInterval(id);\n }, [status, tickMs, maxDurationMs, stop]);\n\n const start = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n setRecording(null);\n setError(null);\n setDurationMs(0);\n recorder.start();\n setStatus(recorder.status());\n }, []);\n\n const pause = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.pause();\n setDurationMs(recorder.durationMs());\n setStatus(recorder.status());\n }, []);\n\n const resume = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.resume();\n setStatus(recorder.status());\n }, []);\n\n const cancel = useCallback((): void => {\n const recorder = recorderRef.current;\n if (!recorder) return;\n recorder.cancel();\n setStatus(\"idle\");\n setDurationMs(0);\n setLevel(0);\n setRecording(null);\n }, []);\n\n return {\n status,\n durationMs,\n level,\n recording,\n error,\n ready,\n start,\n pause,\n resume,\n stop,\n cancel,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-audio.cjs","names":[],"sources":["../../src/audio/use-audio.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\nimport { createAudioPlayer, type
|
|
1
|
+
{"version":3,"file":"use-audio.cjs","names":[],"sources":["../../src/audio/use-audio.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\nimport { createAudioPlayer, type AudioPlayerHandle, type PlayAudioOptions } from \"./audio-player\";\n\nexport interface UseAudioResult {\n /** Play `src` on the hook's private player. */\n play: (src: string, options?: PlayAudioOptions) => Promise<void>;\n /** Stop the current clip. */\n stop: () => void;\n /**\n * Whether the browser's autoplay policy has been satisfied. Becomes\n * `true` after the first successful `play()`.\n */\n unlocked: boolean;\n}\n\n/**\n * The player lives in a state initializer, not in a lazily-written ref: the\n * initializer runs exactly once and is not a side effect during render, which is\n * what the ref version was. The player identity never changes, so state holds it\n * fine and the value is non-null from the first render.\n *\n * Hook-managed audio player. Each component instance gets its own\n * {@link AudioPlayerHandle}, so unmounting cleanly stops playback. Useful for\n * notification chimes, UI feedback sounds, and per-component soundtracks.\n */\nexport function useAudio(): UseAudioResult {\n const [player] = useState<AudioPlayerHandle>(() => createAudioPlayer());\n const [unlocked, setUnlocked] = useState<boolean>(false);\n\n useEffect(() => {\n return () => {\n player.stop();\n };\n }, [player]);\n\n const play = useCallback(\n async (src: string, options?: PlayAudioOptions): Promise<void> => {\n const result = await player.play(src, options);\n if (result) setUnlocked(true);\n },\n [player],\n );\n\n const stop = useCallback((): void => {\n player.stop();\n }, [player]);\n\n return { play, stop, unlocked };\n}\n"],"mappings":"6DAyBA,SAAgB,GAA2B,CACvC,GAAM,CAAC,IAAA,EAAA,EAAA,SAAA,KAA4C,EAAA,kBAAkB,CAAC,EAChE,CAAC,EAAU,IAAA,EAAA,EAAA,SAAA,CAAiC,EAAK,EAoBvD,OAlBA,EAAA,EAAA,UAAA,SACiB,CACT,EAAO,KAAK,CAChB,EACD,CAAC,CAAM,CAAC,EAcJ,CAAE,MAAA,EAAA,EAAA,YAAA,CAXL,MAAO,EAAa,IAA8C,CAE1D,MADiB,EAAO,KAAK,EAAK,CAAO,GACjC,EAAY,EAAI,CAChC,EACA,CAAC,CAAM,CAOF,EAAM,MAAA,EAAA,EAAA,YAAA,KAJsB,CACjC,EAAO,KAAK,CAChB,EAAG,CAAC,CAAM,CAEK,EAAM,UAAS,CAClC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-audio.js","names":[],"sources":["../../src/audio/use-audio.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\nimport { createAudioPlayer, type
|
|
1
|
+
{"version":3,"file":"use-audio.js","names":[],"sources":["../../src/audio/use-audio.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\nimport { createAudioPlayer, type AudioPlayerHandle, type PlayAudioOptions } from \"./audio-player\";\n\nexport interface UseAudioResult {\n /** Play `src` on the hook's private player. */\n play: (src: string, options?: PlayAudioOptions) => Promise<void>;\n /** Stop the current clip. */\n stop: () => void;\n /**\n * Whether the browser's autoplay policy has been satisfied. Becomes\n * `true` after the first successful `play()`.\n */\n unlocked: boolean;\n}\n\n/**\n * The player lives in a state initializer, not in a lazily-written ref: the\n * initializer runs exactly once and is not a side effect during render, which is\n * what the ref version was. The player identity never changes, so state holds it\n * fine and the value is non-null from the first render.\n *\n * Hook-managed audio player. Each component instance gets its own\n * {@link AudioPlayerHandle}, so unmounting cleanly stops playback. Useful for\n * notification chimes, UI feedback sounds, and per-component soundtracks.\n */\nexport function useAudio(): UseAudioResult {\n const [player] = useState<AudioPlayerHandle>(() => createAudioPlayer());\n const [unlocked, setUnlocked] = useState<boolean>(false);\n\n useEffect(() => {\n return () => {\n player.stop();\n };\n }, [player]);\n\n const play = useCallback(\n async (src: string, options?: PlayAudioOptions): Promise<void> => {\n const result = await player.play(src, options);\n if (result) setUnlocked(true);\n },\n [player],\n );\n\n const stop = useCallback((): void => {\n player.stop();\n }, [player]);\n\n return { play, stop, unlocked };\n}\n"],"mappings":";;;AAyBA,SAAgB,IAA2B;CACvC,IAAM,CAAC,KAAU,QAAkC,EAAkB,CAAC,GAChE,CAAC,GAAU,KAAe,EAAkB,EAAK;CAoBvD,OAlBA,cACiB;EACT,EAAO,KAAK;CAChB,GACD,CAAC,CAAM,CAAC,GAcJ;EAAE,MAZI,EACT,OAAO,GAAa,MAA8C;GAE9D,AAAI,MADiB,EAAO,KAAK,GAAK,CAAO,KACjC,EAAY,EAAI;EAChC,GACA,CAAC,CAAM,CAOF;EAAM,MAJF,QAAwB;GACjC,EAAO,KAAK;EAChB,GAAG,CAAC,CAAM,CAEK;EAAM;CAAS;AAClC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./media-access.cjs");let t=require("react");function n(n={}){let{autoStart:r=!1,deviceId:i,echoCancellation:a=!0,noiseSuppression:o=!0,autoGainControl:s=!0,constraints:c}=n,[l,u]=(0,t.useState)(`idle`),[d,f]=(0,t.useState)(null),[p,m]=(0,t.useState)(null),h=(0,t.useRef)(null),g=(0,t.useRef)(0),_=(0,t.useCallback)(()=>{h.current?.getTracks().forEach(e=>e.stop()),h.current=null},[]),v=(0,t.useCallback)(()=>{g.current+=1,_(),f(null),u(`idle`),m(null)},[_]),y=(0,t.useCallback)(()=>{if(h.current)return;let t=g.current+=1;if(u(`requesting`),m(null),!e.isMediaCaptureSupported()){m(e.missingCaptureApiError(`microphone`)),u(`error`);return}let n=c??{audio:{echoCancellation:a,noiseSuppression:o,autoGainControl:s,...i?{deviceId:{exact:i}}:{}},video:!1};navigator.mediaDevices.getUserMedia(n).then(e=>{if(t!==g.current){e.getTracks().forEach(e=>e.stop());return}h.current=e,f(e),u(`ready`)}).catch(n=>{t===g.current&&(m(e.classifyMediaError(n,`microphone`)),u(`error`))})},[c,i,a,o,s]);return(0,t.useEffect)(()=>{r&&y()},[r]),(0,t.useEffect)(()=>_,[_]),{status:l,stream:d,error:p,start:y,stop:v}}exports.useMicrophone=n;
|
|
2
|
+
//# sourceMappingURL=use-microphone.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-microphone.cjs","names":[],"sources":["../../src/audio/use-microphone.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport {\n classifyMediaError,\n isMediaCaptureSupported,\n missingCaptureApiError,\n type MediaAccessError,\n} from \"./media-access\";\n\n/** Lifecycle of a microphone stream. */\nexport type MicrophoneStatus = \"idle\" | \"requesting\" | \"ready\" | \"error\";\n\n/** Options for {@link useMicrophone}. */\nexport interface UseMicrophoneOptions {\n /**\n * Open the stream on mount instead of waiting for `start()`. Default `false`.\n *\n * Left off on purpose. Opening on mount means prompting on mount, and a\n * permission prompt the user did not provoke is the most reliable way to earn a\n * permanent \"Block\" — after which `getUserMedia` rejects without ever prompting\n * again. Wire `start()` to the button that needs the microphone.\n */\n autoStart?: boolean;\n /** Specific microphone, from `useMediaDevices().audioInputs`. */\n deviceId?: string;\n /**\n * Browser voice processing. All three default to `true`, which is what speech\n * wants; turn them off for music, where a gate chewing on a decaying note is\n * worse than the room noise it removes.\n */\n echoCancellation?: boolean;\n noiseSuppression?: boolean;\n autoGainControl?: boolean;\n /** Escape hatch: full constraints, replacing everything above. */\n constraints?: MediaStreamConstraints;\n}\n\n/** Value returned by {@link useMicrophone}. */\nexport interface UseMicrophoneResult {\n status: MicrophoneStatus;\n /** The live stream, or `null`. Feed it to {@link useAudioRecorder} or an analyser. */\n stream: MediaStream | null;\n /** Classified error, or `null` outside the `error` status. */\n error: MediaAccessError | null;\n /** Open the stream. Safe to call when already open — it is a no-op. */\n start: () => void;\n /** Release the microphone. The browser's recording indicator only clears here. */\n stop: () => void;\n}\n\n/**\n * Acquire a microphone `MediaStream`, classified errors included, and release it\n * properly.\n *\n * Releasing matters more than it looks. Every track has to be stopped by hand:\n * dropping the last reference to a `MediaStream` does **not** turn off the\n * microphone, so the browser keeps showing its recording indicator and the OS keeps\n * the device busy — which then makes the *next* `getUserMedia` fail with\n * `NotReadableError` in another tab. The hook stops tracks on `stop()`, on unmount,\n * and before re-opening.\n *\n * @param options - See {@link UseMicrophoneOptions}.\n * @returns The stream, its status, a classified error, and `start`/`stop`.\n *\n * @example\n * const mic = useMicrophone();\n * const recorder = useAudioRecorder(mic.stream);\n * <button onClick={mic.start}>Liberar microfone</button>\n */\nexport function useMicrophone(options: UseMicrophoneOptions = {}): UseMicrophoneResult {\n const {\n autoStart = false,\n deviceId,\n echoCancellation = true,\n noiseSuppression = true,\n autoGainControl = true,\n constraints,\n } = options;\n\n const [status, setStatus] = useState<MicrophoneStatus>(\"idle\");\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [error, setError] = useState<MediaAccessError | null>(null);\n const streamRef = useRef<MediaStream | null>(null);\n const generation = useRef(0);\n\n const release = useCallback((): void => {\n streamRef.current?.getTracks().forEach((track) => track.stop());\n streamRef.current = null;\n }, []);\n\n const stop = useCallback((): void => {\n generation.current += 1;\n release();\n setStream(null);\n setStatus(\"idle\");\n setError(null);\n }, [release]);\n\n const start = useCallback((): void => {\n if (streamRef.current) return;\n const run = (generation.current += 1);\n setStatus(\"requesting\");\n setError(null);\n\n if (!isMediaCaptureSupported()) {\n setError(missingCaptureApiError(\"microphone\"));\n setStatus(\"error\");\n return;\n }\n\n const request: MediaStreamConstraints = constraints ?? {\n audio: {\n echoCancellation,\n noiseSuppression,\n autoGainControl,\n ...(deviceId ? { deviceId: { exact: deviceId } } : {}),\n },\n video: false,\n };\n\n void navigator.mediaDevices\n .getUserMedia(request)\n .then((next) => {\n if (run !== generation.current) {\n next.getTracks().forEach((track) => track.stop());\n return;\n }\n streamRef.current = next;\n setStream(next);\n setStatus(\"ready\");\n })\n .catch((err: unknown) => {\n if (run !== generation.current) return;\n setError(classifyMediaError(err, \"microphone\"));\n setStatus(\"error\");\n });\n }, [constraints, deviceId, echoCancellation, noiseSuppression, autoGainControl]);\n\n useEffect(() => {\n if (autoStart) start();\n // `start` is stable per constraint set; re-running on every identity change\n // would reopen the device on each render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [autoStart]);\n\n useEffect(() => release, [release]);\n\n return { status, stream, error, start, stop };\n}\n"],"mappings":"6DAqEA,SAAgB,EAAc,EAAgC,CAAC,EAAwB,CACnF,GAAM,CACF,YAAY,GACZ,WACA,mBAAmB,GACnB,mBAAmB,GACnB,kBAAkB,GAClB,eACA,EAEE,CAAC,EAAQ,IAAA,EAAA,EAAA,SAAA,CAAwC,MAAM,EACvD,CAAC,EAAQ,IAAA,EAAA,EAAA,SAAA,CAA0C,IAAI,EACvD,CAAC,EAAO,IAAA,EAAA,EAAA,SAAA,CAA8C,IAAI,EAC1D,GAAA,EAAA,EAAA,OAAA,CAAuC,IAAI,EAC3C,GAAA,EAAA,EAAA,OAAA,CAAoB,CAAC,EAErB,GAAA,EAAA,EAAA,YAAA,KAAkC,CACpC,EAAU,SAAS,UAAU,CAAC,CAAC,QAAS,GAAU,EAAM,KAAK,CAAC,EAC9D,EAAU,QAAU,IACxB,EAAG,CAAC,CAAC,EAEC,GAAA,EAAA,EAAA,YAAA,KAA+B,CACjC,EAAW,SAAW,EACtB,EAAQ,EACR,EAAU,IAAI,EACd,EAAU,MAAM,EAChB,EAAS,IAAI,CACjB,EAAG,CAAC,CAAO,CAAC,EAEN,GAAA,EAAA,EAAA,YAAA,KAAgC,CAClC,GAAI,EAAU,QAAS,OACvB,IAAM,EAAO,EAAW,SAAW,EAInC,GAHA,EAAU,YAAY,EACtB,EAAS,IAAI,EAET,CAAC,EAAA,wBAAwB,EAAG,CAC5B,EAAS,EAAA,uBAAuB,YAAY,CAAC,EAC7C,EAAU,OAAO,EACjB,MACJ,CAEA,IAAM,EAAkC,GAAe,CACnD,MAAO,CACH,mBACA,mBACA,kBACA,GAAI,EAAW,CAAE,SAAU,CAAE,MAAO,CAAS,CAAE,EAAI,CAAC,CACxD,EACA,MAAO,EACX,EAEA,UAAe,aACV,aAAa,CAAO,CAAC,CACrB,KAAM,GAAS,CACZ,GAAI,IAAQ,EAAW,QAAS,CAC5B,EAAK,UAAU,CAAC,CAAC,QAAS,GAAU,EAAM,KAAK,CAAC,EAChD,MACJ,CACA,EAAU,QAAU,EACpB,EAAU,CAAI,EACd,EAAU,OAAO,CACrB,CAAC,CAAC,CACD,MAAO,GAAiB,CACjB,IAAQ,EAAW,UACvB,EAAS,EAAA,mBAAmB,EAAK,YAAY,CAAC,EAC9C,EAAU,OAAO,EACrB,CAAC,CACT,EAAG,CAAC,EAAa,EAAU,EAAkB,EAAkB,CAAe,CAAC,EAW/E,OATA,EAAA,EAAA,UAAA,KAAgB,CACR,GAAW,EAAM,CAIzB,EAAG,CAAC,CAAS,CAAC,GAEd,EAAA,EAAA,UAAA,KAAgB,EAAS,CAAC,CAAO,CAAC,EAE3B,CAAE,SAAQ,SAAQ,QAAO,QAAO,MAAK,CAChD"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { classifyMediaError as e, isMediaCaptureSupported as t, missingCaptureApiError as n } from "./media-access.js";
|
|
2
|
+
import { useCallback as r, useEffect as i, useRef as a, useState as o } from "react";
|
|
3
|
+
//#region src/audio/use-microphone.ts
|
|
4
|
+
function s(s = {}) {
|
|
5
|
+
let { autoStart: c = !1, deviceId: l, echoCancellation: u = !0, noiseSuppression: d = !0, autoGainControl: f = !0, constraints: p } = s, [m, h] = o("idle"), [g, _] = o(null), [v, y] = o(null), b = a(null), x = a(0), S = r(() => {
|
|
6
|
+
b.current?.getTracks().forEach((e) => e.stop()), b.current = null;
|
|
7
|
+
}, []), C = r(() => {
|
|
8
|
+
x.current += 1, S(), _(null), h("idle"), y(null);
|
|
9
|
+
}, [S]), w = r(() => {
|
|
10
|
+
if (b.current) return;
|
|
11
|
+
let r = x.current += 1;
|
|
12
|
+
if (h("requesting"), y(null), !t()) {
|
|
13
|
+
y(n("microphone")), h("error");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
let i = p ?? {
|
|
17
|
+
audio: {
|
|
18
|
+
echoCancellation: u,
|
|
19
|
+
noiseSuppression: d,
|
|
20
|
+
autoGainControl: f,
|
|
21
|
+
...l ? { deviceId: { exact: l } } : {}
|
|
22
|
+
},
|
|
23
|
+
video: !1
|
|
24
|
+
};
|
|
25
|
+
navigator.mediaDevices.getUserMedia(i).then((e) => {
|
|
26
|
+
if (r !== x.current) {
|
|
27
|
+
e.getTracks().forEach((e) => e.stop());
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
b.current = e, _(e), h("ready");
|
|
31
|
+
}).catch((t) => {
|
|
32
|
+
r === x.current && (y(e(t, "microphone")), h("error"));
|
|
33
|
+
});
|
|
34
|
+
}, [
|
|
35
|
+
p,
|
|
36
|
+
l,
|
|
37
|
+
u,
|
|
38
|
+
d,
|
|
39
|
+
f
|
|
40
|
+
]);
|
|
41
|
+
return i(() => {
|
|
42
|
+
c && w();
|
|
43
|
+
}, [c]), i(() => S, [S]), {
|
|
44
|
+
status: m,
|
|
45
|
+
stream: g,
|
|
46
|
+
error: v,
|
|
47
|
+
start: w,
|
|
48
|
+
stop: C
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { s as useMicrophone };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=use-microphone.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-microphone.js","names":[],"sources":["../../src/audio/use-microphone.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport {\n classifyMediaError,\n isMediaCaptureSupported,\n missingCaptureApiError,\n type MediaAccessError,\n} from \"./media-access\";\n\n/** Lifecycle of a microphone stream. */\nexport type MicrophoneStatus = \"idle\" | \"requesting\" | \"ready\" | \"error\";\n\n/** Options for {@link useMicrophone}. */\nexport interface UseMicrophoneOptions {\n /**\n * Open the stream on mount instead of waiting for `start()`. Default `false`.\n *\n * Left off on purpose. Opening on mount means prompting on mount, and a\n * permission prompt the user did not provoke is the most reliable way to earn a\n * permanent \"Block\" — after which `getUserMedia` rejects without ever prompting\n * again. Wire `start()` to the button that needs the microphone.\n */\n autoStart?: boolean;\n /** Specific microphone, from `useMediaDevices().audioInputs`. */\n deviceId?: string;\n /**\n * Browser voice processing. All three default to `true`, which is what speech\n * wants; turn them off for music, where a gate chewing on a decaying note is\n * worse than the room noise it removes.\n */\n echoCancellation?: boolean;\n noiseSuppression?: boolean;\n autoGainControl?: boolean;\n /** Escape hatch: full constraints, replacing everything above. */\n constraints?: MediaStreamConstraints;\n}\n\n/** Value returned by {@link useMicrophone}. */\nexport interface UseMicrophoneResult {\n status: MicrophoneStatus;\n /** The live stream, or `null`. Feed it to {@link useAudioRecorder} or an analyser. */\n stream: MediaStream | null;\n /** Classified error, or `null` outside the `error` status. */\n error: MediaAccessError | null;\n /** Open the stream. Safe to call when already open — it is a no-op. */\n start: () => void;\n /** Release the microphone. The browser's recording indicator only clears here. */\n stop: () => void;\n}\n\n/**\n * Acquire a microphone `MediaStream`, classified errors included, and release it\n * properly.\n *\n * Releasing matters more than it looks. Every track has to be stopped by hand:\n * dropping the last reference to a `MediaStream` does **not** turn off the\n * microphone, so the browser keeps showing its recording indicator and the OS keeps\n * the device busy — which then makes the *next* `getUserMedia` fail with\n * `NotReadableError` in another tab. The hook stops tracks on `stop()`, on unmount,\n * and before re-opening.\n *\n * @param options - See {@link UseMicrophoneOptions}.\n * @returns The stream, its status, a classified error, and `start`/`stop`.\n *\n * @example\n * const mic = useMicrophone();\n * const recorder = useAudioRecorder(mic.stream);\n * <button onClick={mic.start}>Liberar microfone</button>\n */\nexport function useMicrophone(options: UseMicrophoneOptions = {}): UseMicrophoneResult {\n const {\n autoStart = false,\n deviceId,\n echoCancellation = true,\n noiseSuppression = true,\n autoGainControl = true,\n constraints,\n } = options;\n\n const [status, setStatus] = useState<MicrophoneStatus>(\"idle\");\n const [stream, setStream] = useState<MediaStream | null>(null);\n const [error, setError] = useState<MediaAccessError | null>(null);\n const streamRef = useRef<MediaStream | null>(null);\n const generation = useRef(0);\n\n const release = useCallback((): void => {\n streamRef.current?.getTracks().forEach((track) => track.stop());\n streamRef.current = null;\n }, []);\n\n const stop = useCallback((): void => {\n generation.current += 1;\n release();\n setStream(null);\n setStatus(\"idle\");\n setError(null);\n }, [release]);\n\n const start = useCallback((): void => {\n if (streamRef.current) return;\n const run = (generation.current += 1);\n setStatus(\"requesting\");\n setError(null);\n\n if (!isMediaCaptureSupported()) {\n setError(missingCaptureApiError(\"microphone\"));\n setStatus(\"error\");\n return;\n }\n\n const request: MediaStreamConstraints = constraints ?? {\n audio: {\n echoCancellation,\n noiseSuppression,\n autoGainControl,\n ...(deviceId ? { deviceId: { exact: deviceId } } : {}),\n },\n video: false,\n };\n\n void navigator.mediaDevices\n .getUserMedia(request)\n .then((next) => {\n if (run !== generation.current) {\n next.getTracks().forEach((track) => track.stop());\n return;\n }\n streamRef.current = next;\n setStream(next);\n setStatus(\"ready\");\n })\n .catch((err: unknown) => {\n if (run !== generation.current) return;\n setError(classifyMediaError(err, \"microphone\"));\n setStatus(\"error\");\n });\n }, [constraints, deviceId, echoCancellation, noiseSuppression, autoGainControl]);\n\n useEffect(() => {\n if (autoStart) start();\n // `start` is stable per constraint set; re-running on every identity change\n // would reopen the device on each render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [autoStart]);\n\n useEffect(() => release, [release]);\n\n return { status, stream, error, start, stop };\n}\n"],"mappings":";;;AAqEA,SAAgB,EAAc,IAAgC,CAAC,GAAwB;CACnF,IAAM,EACF,eAAY,IACZ,aACA,sBAAmB,IACnB,sBAAmB,IACnB,qBAAkB,IAClB,mBACA,GAEE,CAAC,GAAQ,KAAa,EAA2B,MAAM,GACvD,CAAC,GAAQ,KAAa,EAA6B,IAAI,GACvD,CAAC,GAAO,KAAY,EAAkC,IAAI,GAC1D,IAAY,EAA2B,IAAI,GAC3C,IAAa,EAAO,CAAC,GAErB,IAAU,QAAwB;EAEpC,AADA,EAAU,SAAS,UAAU,CAAC,CAAC,SAAS,MAAU,EAAM,KAAK,CAAC,GAC9D,EAAU,UAAU;CACxB,GAAG,CAAC,CAAC,GAEC,IAAO,QAAwB;EAKjC,AAJA,EAAW,WAAW,GACtB,EAAQ,GACR,EAAU,IAAI,GACd,EAAU,MAAM,GAChB,EAAS,IAAI;CACjB,GAAG,CAAC,CAAO,CAAC,GAEN,IAAQ,QAAwB;EAClC,IAAI,EAAU,SAAS;EACvB,IAAM,IAAO,EAAW,WAAW;EAInC,IAHA,EAAU,YAAY,GACtB,EAAS,IAAI,GAET,CAAC,EAAwB,GAAG;GAE5B,AADA,EAAS,EAAuB,YAAY,CAAC,GAC7C,EAAU,OAAO;GACjB;EACJ;EAEA,IAAM,IAAkC,KAAe;GACnD,OAAO;IACH;IACA;IACA;IACA,GAAI,IAAW,EAAE,UAAU,EAAE,OAAO,EAAS,EAAE,IAAI,CAAC;GACxD;GACA,OAAO;EACX;EAEA,UAAe,aACV,aAAa,CAAO,CAAC,CACrB,MAAM,MAAS;GACZ,IAAI,MAAQ,EAAW,SAAS;IAC5B,EAAK,UAAU,CAAC,CAAC,SAAS,MAAU,EAAM,KAAK,CAAC;IAChD;GACJ;GAGA,AAFA,EAAU,UAAU,GACpB,EAAU,CAAI,GACd,EAAU,OAAO;EACrB,CAAC,CAAC,CACD,OAAO,MAAiB;GACjB,MAAQ,EAAW,YACvB,EAAS,EAAmB,GAAK,YAAY,CAAC,GAC9C,EAAU,OAAO;EACrB,CAAC;CACT,GAAG;EAAC;EAAa;EAAU;EAAkB;EAAkB;CAAe,CAAC;CAW/E,OATA,QAAgB;EACZ,AAAI,KAAW,EAAM;CAIzB,GAAG,CAAC,CAAS,CAAC,GAEd,QAAgB,GAAS,CAAC,CAAO,CAAC,GAE3B;EAAE;EAAQ;EAAQ;EAAO;EAAO;CAAK;AAChD"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e({channels:e,sampleRate:t}){let n=Math.max(1,e.length),r=e[0]?.length??0,i=r*n*2,a=new ArrayBuffer(44+i),o=new DataView(a),s=(e,t)=>{for(let n=0;n<t.length;n+=1)o.setUint8(e+n,t.charCodeAt(n))},c=t*n*2;s(0,`RIFF`),o.setUint32(4,36+i,!0),s(8,`WAVE`),s(12,`fmt `),o.setUint32(16,16,!0),o.setUint16(20,1,!0),o.setUint16(22,n,!0),o.setUint32(24,t,!0),o.setUint32(28,c,!0),o.setUint16(32,n*2,!0),o.setUint16(34,16,!0),s(36,`data`),o.setUint32(40,i,!0);let l=44;for(let t=0;t<r;t+=1)for(let r=0;r<n;r+=1){let n=e[r]?.[t]??0,i=n<-1?-1:n>1?1:n;o.setInt16(l,i<0?i*32768:i*32767,!0),l+=2}return new Blob([a],{type:`audio/wav`})}function t(e){let t=e.length,n=new Float32Array(t);for(let r=0;r<e.numberOfChannels;r+=1){let i=e.getChannelData(r);for(let e=0;e<t;e+=1)n[e]+=i[e]}if(e.numberOfChannels>1)for(let r=0;r<t;r+=1)n[r]/=e.numberOfChannels;return n}async function n(n,r={}){let{mono:i=!1,sampleRate:a}=r,o=typeof AudioContext<`u`?AudioContext:globalThis.webkitAudioContext;if(!o)throw Error(`Web Audio is not available in this environment.`);let s=new o,c;try{c=await s.decodeAudioData(await n.arrayBuffer())}finally{s.close().catch(()=>void 0)}let l=i?1:c.numberOfChannels,u=a??c.sampleRate;if(u===c.sampleRate)return e({channels:i?[t(c)]:Array.from({length:c.numberOfChannels},(e,t)=>c.getChannelData(t)),sampleRate:c.sampleRate});let d=typeof OfflineAudioContext<`u`?OfflineAudioContext:void 0;if(!d)throw Error(`Resampling requires OfflineAudioContext.`);let f=new d(l,Math.max(1,Math.round(c.duration*u|0)||1),u),p=f.createBufferSource();p.buffer=c,p.connect(f.destination),p.start();let m=await f.startRendering();return e({channels:Array.from({length:m.numberOfChannels},(e,t)=>m.getChannelData(t)),sampleRate:m.sampleRate})}exports.blobToWav=n,exports.encodeWav=e;
|
|
2
|
+
//# sourceMappingURL=wav.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wav.cjs","names":[],"sources":["../../src/audio/wav.ts"],"sourcesContent":["/** Raw PCM ready to be wrapped in a RIFF container. */\nexport interface PcmAudio {\n /** One `Float32Array` per channel, samples in −1…1. */\n channels: readonly Float32Array[];\n sampleRate: number;\n}\n\n/** Options for {@link blobToWav}. */\nexport interface WavOptions {\n /**\n * Mix down to one channel. Default `false`.\n *\n * Worth turning on for speech headed to a server: a microphone recorded in stereo\n * carries two nearly identical channels and doubles the upload for nothing.\n */\n mono?: boolean;\n /**\n * Resample to this rate. Default: keep the source rate.\n *\n * 16000 is the rate most speech-to-text APIs want, and dropping 48 kHz to 16 kHz\n * removes two thirds of the bytes with no audible loss on voice.\n */\n sampleRate?: number;\n}\n\n/** Bytes per sample in the output. 16-bit PCM is what every decoder accepts. */\nconst BYTES_PER_SAMPLE = 2;\n\n/**\n * Wrap PCM in a 16-bit RIFF/WAVE container.\n *\n * The header is the 44-byte canonical form: `RIFF` size, `WAVE`, a 16-byte `fmt `\n * chunk declaring format 1 (uncompressed PCM), then `data`. Samples are interleaved\n * and clamped before scaling, because a value even slightly past ±1 wraps around when\n * truncated to 16 bits and turns a loud passage into a burst of noise.\n *\n * Implemented here rather than pulled from a package: it is a fixed header and a\n * scaling loop, roughly forty lines, and a dependency for it would put its own\n * version bounds on every consumer of this SDK to save writing them.\n *\n * @param audio - Channels and sample rate.\n * @returns A `Blob` of type `audio/wav`.\n */\nexport function encodeWav({ channels, sampleRate }: PcmAudio): Blob {\n const channelCount = Math.max(1, channels.length);\n const frameCount = channels[0]?.length ?? 0;\n const dataBytes = frameCount * channelCount * BYTES_PER_SAMPLE;\n const buffer = new ArrayBuffer(44 + dataBytes);\n const view = new DataView(buffer);\n\n const ascii = (offset: number, text: string): void => {\n for (let index = 0; index < text.length; index += 1) {\n view.setUint8(offset + index, text.charCodeAt(index));\n }\n };\n\n const byteRate = sampleRate * channelCount * BYTES_PER_SAMPLE;\n ascii(0, \"RIFF\");\n view.setUint32(4, 36 + dataBytes, true);\n ascii(8, \"WAVE\");\n ascii(12, \"fmt \");\n view.setUint32(16, 16, true);\n view.setUint16(20, 1, true);\n view.setUint16(22, channelCount, true);\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, byteRate, true);\n view.setUint16(32, channelCount * BYTES_PER_SAMPLE, true);\n view.setUint16(34, BYTES_PER_SAMPLE * 8, true);\n ascii(36, \"data\");\n view.setUint32(40, dataBytes, true);\n\n let offset = 44;\n for (let frame = 0; frame < frameCount; frame += 1) {\n for (let channel = 0; channel < channelCount; channel += 1) {\n const sample = channels[channel]?.[frame] ?? 0;\n const clamped = sample < -1 ? -1 : sample > 1 ? 1 : sample;\n // Asymmetric scaling: 16-bit PCM runs −32768…32767.\n view.setInt16(offset, clamped < 0 ? clamped * 0x8000 : clamped * 0x7fff, true);\n offset += BYTES_PER_SAMPLE;\n }\n }\n\n return new Blob([buffer], { type: \"audio/wav\" });\n}\n\n/** Average every channel of a decoded buffer into one. */\nfunction mixToMono(buffer: AudioBuffer): Float32Array {\n const frames = buffer.length;\n const out = new Float32Array(frames);\n for (let channel = 0; channel < buffer.numberOfChannels; channel += 1) {\n const data = buffer.getChannelData(channel);\n for (let frame = 0; frame < frames; frame += 1) out[frame] += data[frame];\n }\n if (buffer.numberOfChannels > 1) {\n for (let frame = 0; frame < frames; frame += 1) out[frame] /= buffer.numberOfChannels;\n }\n return out;\n}\n\n/**\n * Convert a recording to 16-bit WAV, with no dependency.\n *\n * `MediaRecorder` cannot produce WAV — it emits Opus on Chromium and Firefox and AAC\n * on Safari — so a backend that insists on WAV has to be served either by a\n * server-side transcode or by this. It decodes through `AudioContext.decodeAudioData`,\n * which is the browser's own decoder for whatever container the recorder chose, then\n * re-encodes the PCM.\n *\n * !!! The cost is real: WAV is uncompressed, so the same voice note that is 40 KB in\n * Opus is roughly 500 KB here at 48 kHz stereo. `{ mono: true, sampleRate: 16000 }`\n * takes that to about 80 KB and is what a speech-to-text endpoint wants anyway.\n *\n * Resampling uses `OfflineAudioContext`, i.e. the browser's own resampler, rather\n * than a hand-rolled one — this is exactly the \"we want the underlying call, not a\n * wrapper\" case.\n *\n * @param blob - A recording from {@link createAudioRecorder}, or any decodable audio.\n * @param options - See {@link WavOptions}.\n * @returns A `Blob` of type `audio/wav`.\n * @throws When the environment has no Web Audio, or the blob cannot be decoded.\n */\nexport async function blobToWav(blob: Blob, options: WavOptions = {}): Promise<Blob> {\n const { mono = false, sampleRate: target } = options;\n\n const Ctor: typeof AudioContext | undefined =\n typeof AudioContext !== \"undefined\"\n ? AudioContext\n : (globalThis as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!Ctor) throw new Error(\"Web Audio is not available in this environment.\");\n\n const context = new Ctor();\n let decoded: AudioBuffer;\n try {\n decoded = await context.decodeAudioData(await blob.arrayBuffer());\n } finally {\n void context.close().catch(() => undefined);\n }\n\n const wantedChannels = mono ? 1 : decoded.numberOfChannels;\n const wantedRate = target ?? decoded.sampleRate;\n\n if (wantedRate === decoded.sampleRate) {\n const channels = mono\n ? [mixToMono(decoded)]\n : Array.from({ length: decoded.numberOfChannels }, (_, index) =>\n decoded.getChannelData(index),\n );\n return encodeWav({ channels, sampleRate: decoded.sampleRate });\n }\n\n const Offline: typeof OfflineAudioContext | undefined =\n typeof OfflineAudioContext !== \"undefined\" ? OfflineAudioContext : undefined;\n if (!Offline) throw new Error(\"Resampling requires OfflineAudioContext.\");\n\n const frames = Math.max(1, Math.round((decoded.duration * wantedRate) | 0) || 1);\n const offline = new Offline(wantedChannels, frames, wantedRate);\n const source = offline.createBufferSource();\n source.buffer = decoded;\n source.connect(offline.destination);\n source.start();\n const rendered = await offline.startRendering();\n\n return encodeWav({\n channels: Array.from({ length: rendered.numberOfChannels }, (_, index) =>\n rendered.getChannelData(index),\n ),\n sampleRate: rendered.sampleRate,\n });\n}\n"],"mappings":"AA2CA,SAAgB,EAAU,CAAE,WAAU,cAA8B,CAChE,IAAM,EAAe,KAAK,IAAI,EAAG,EAAS,MAAM,EAC1C,EAAa,EAAS,EAAE,EAAE,QAAU,EACpC,EAAY,EAAa,EAAe,EACxC,EAAS,IAAI,YAAY,GAAK,CAAS,EACvC,EAAO,IAAI,SAAS,CAAM,EAE1B,GAAS,EAAgB,IAAuB,CAClD,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAK,OAAQ,GAAS,EAC9C,EAAK,SAAS,EAAS,EAAO,EAAK,WAAW,CAAK,CAAC,CAE5D,EAEM,EAAW,EAAa,EAAe,EAC7C,EAAM,EAAG,MAAM,EACf,EAAK,UAAU,EAAG,GAAK,EAAW,EAAI,EACtC,EAAM,EAAG,MAAM,EACf,EAAM,GAAI,MAAM,EAChB,EAAK,UAAU,GAAI,GAAI,EAAI,EAC3B,EAAK,UAAU,GAAI,EAAG,EAAI,EAC1B,EAAK,UAAU,GAAI,EAAc,EAAI,EACrC,EAAK,UAAU,GAAI,EAAY,EAAI,EACnC,EAAK,UAAU,GAAI,EAAU,EAAI,EACjC,EAAK,UAAU,GAAI,EAAe,EAAkB,EAAI,EACxD,EAAK,UAAU,GAAI,GAAsB,EAAI,EAC7C,EAAM,GAAI,MAAM,EAChB,EAAK,UAAU,GAAI,EAAW,EAAI,EAElC,IAAI,EAAS,GACb,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAY,GAAS,EAC7C,IAAK,IAAI,EAAU,EAAG,EAAU,EAAc,GAAW,EAAG,CACxD,IAAM,EAAS,EAAS,EAAQ,GAAG,IAAU,EACvC,EAAU,EAAS,GAAK,GAAK,EAAS,EAAI,EAAI,EAEpD,EAAK,SAAS,EAAQ,EAAU,EAAI,EAAU,MAAS,EAAU,MAAQ,EAAI,EAC7E,GAAU,CACd,CAGJ,OAAO,IAAI,KAAK,CAAC,CAAM,EAAG,CAAE,KAAM,WAAY,CAAC,CACnD,CAGA,SAAS,EAAU,EAAmC,CAClD,IAAM,EAAS,EAAO,OAChB,EAAM,IAAI,aAAa,CAAM,EACnC,IAAK,IAAI,EAAU,EAAG,EAAU,EAAO,iBAAkB,GAAW,EAAG,CACnE,IAAM,EAAO,EAAO,eAAe,CAAO,EAC1C,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAQ,GAAS,EAAG,EAAI,IAAU,EAAK,EACvE,CACA,GAAI,EAAO,iBAAmB,EAC1B,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAQ,GAAS,EAAG,EAAI,IAAU,EAAO,iBAEzE,OAAO,CACX,CAwBA,eAAsB,EAAU,EAAY,EAAsB,CAAC,EAAkB,CACjF,GAAM,CAAE,OAAO,GAAO,WAAY,GAAW,EAEvC,EACF,OAAO,aAAiB,IAClB,aACC,WAA4D,mBACvE,GAAI,CAAC,EAAM,MAAU,MAAM,iDAAiD,EAE5E,IAAM,EAAU,IAAI,EAChB,EACJ,GAAI,CACA,EAAU,MAAM,EAAQ,gBAAgB,MAAM,EAAK,YAAY,CAAC,CACpE,QAAU,CACN,EAAa,MAAM,CAAC,CAAC,UAAY,IAAA,EAAS,CAC9C,CAEA,IAAM,EAAiB,EAAO,EAAI,EAAQ,iBACpC,EAAa,GAAU,EAAQ,WAErC,GAAI,IAAe,EAAQ,WAMvB,OAAO,EAAU,CAAE,SALF,EACX,CAAC,EAAU,CAAO,CAAC,EACnB,MAAM,KAAK,CAAE,OAAQ,EAAQ,gBAAiB,GAAI,EAAG,IACjD,EAAQ,eAAe,CAAK,CAChC,EACuB,WAAY,EAAQ,UAAW,CAAC,EAGjE,IAAM,EACF,OAAO,oBAAwB,IAAc,oBAAsB,IAAA,GACvE,GAAI,CAAC,EAAS,MAAU,MAAM,0CAA0C,EAGxE,IAAM,EAAU,IAAI,EAAQ,EADb,KAAK,IAAI,EAAG,KAAK,MAAO,EAAQ,SAAW,EAAc,CAAC,GAAK,CAClC,EAAQ,CAAU,EACxD,EAAS,EAAQ,mBAAmB,EAC1C,EAAO,OAAS,EAChB,EAAO,QAAQ,EAAQ,WAAW,EAClC,EAAO,MAAM,EACb,IAAM,EAAW,MAAM,EAAQ,eAAe,EAE9C,OAAO,EAAU,CACb,SAAU,MAAM,KAAK,CAAE,OAAQ,EAAS,gBAAiB,GAAI,EAAG,IAC5D,EAAS,eAAe,CAAK,CACjC,EACA,WAAY,EAAS,UACzB,CAAC,CACL"}
|