react-audio-wavekit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/LICENSE +116 -0
  2. package/README.md +231 -0
  3. package/dist/constants.cjs +20 -0
  4. package/dist/constants.js +20 -0
  5. package/dist/index.cjs +12 -0
  6. package/dist/index.d.ts +235 -0
  7. package/dist/index.js +12 -0
  8. package/dist/recorder/live-recorder/index.cjs +125 -0
  9. package/dist/recorder/live-recorder/index.js +125 -0
  10. package/dist/recorder/live-streaming/recorder/recorder-compound.cjs +244 -0
  11. package/dist/recorder/live-streaming/recorder/recorder-compound.js +244 -0
  12. package/dist/recorder/live-streaming/recorder/recorder-context.cjs +20 -0
  13. package/dist/recorder/live-streaming/recorder/recorder-context.js +20 -0
  14. package/dist/recorder/live-streaming/stack-recorder/stack-recorder-compound.cjs +126 -0
  15. package/dist/recorder/live-streaming/stack-recorder/stack-recorder-compound.js +126 -0
  16. package/dist/recorder/live-streaming/use-recording-amplitudes.cjs +92 -0
  17. package/dist/recorder/live-streaming/use-recording-amplitudes.js +92 -0
  18. package/dist/recorder/use-audio-analyser.cjs +59 -0
  19. package/dist/recorder/use-audio-analyser.js +59 -0
  20. package/dist/recorder/use-audio-recorder.cjs +139 -0
  21. package/dist/recorder/use-audio-recorder.js +139 -0
  22. package/dist/recorder/util-mime-type.cjs +15 -0
  23. package/dist/recorder/util-mime-type.js +15 -0
  24. package/dist/waveform/index.cjs +73 -0
  25. package/dist/waveform/index.js +73 -0
  26. package/dist/waveform/util-audio-decoder.cjs +45 -0
  27. package/dist/waveform/util-audio-decoder.js +45 -0
  28. package/dist/waveform/util-suspense.cjs +24 -0
  29. package/dist/waveform/util-suspense.js +24 -0
  30. package/dist/waveform/waveform-renderer.cjs +105 -0
  31. package/dist/waveform/waveform-renderer.js +105 -0
  32. package/package.json +74 -0
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ async function decodeAudioBlob(blob, sampleCount) {
4
+ const audioContext = new AudioContext();
5
+ const arrayBuffer = await blob.arrayBuffer();
6
+ if (arrayBuffer.byteLength === 0) {
7
+ await audioContext.close();
8
+ throw new Error("Audio blob is empty");
9
+ }
10
+ let audioBuffer;
11
+ try {
12
+ audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
13
+ } catch {
14
+ await audioContext.close();
15
+ throw new Error(
16
+ `Unable to decode audio data (type: ${blob.type}, size: ${blob.size} bytes). This may be due to an unsupported audio format or corrupted data.`
17
+ );
18
+ }
19
+ const channelData = audioBuffer.getChannelData(0);
20
+ const blockSize = Math.floor(channelData.length / sampleCount);
21
+ const peaks = [];
22
+ for (let i = 0; i < sampleCount; i++) {
23
+ const start = i * blockSize;
24
+ let sum = 0;
25
+ for (let j = 0; j < blockSize; j++) {
26
+ sum += Math.abs(channelData[start + j] || 0);
27
+ }
28
+ peaks.push(sum / blockSize);
29
+ }
30
+ const maxPeak = Math.max(...peaks);
31
+ const normalizedPeaks = maxPeak > 0 ? peaks.map((p) => p / maxPeak) : peaks;
32
+ await audioContext.close();
33
+ return normalizedPeaks;
34
+ }
35
+ const audioDataCache = /* @__PURE__ */ new WeakMap();
36
+ function getAudioData(blob, sampleCount) {
37
+ let promise = audioDataCache.get(blob);
38
+ if (!promise) {
39
+ promise = decodeAudioBlob(blob, sampleCount);
40
+ audioDataCache.set(blob, promise);
41
+ }
42
+ return promise;
43
+ }
44
+ exports.decodeAudioBlob = decodeAudioBlob;
45
+ exports.getAudioData = getAudioData;
@@ -0,0 +1,45 @@
1
+ async function decodeAudioBlob(blob, sampleCount) {
2
+ const audioContext = new AudioContext();
3
+ const arrayBuffer = await blob.arrayBuffer();
4
+ if (arrayBuffer.byteLength === 0) {
5
+ await audioContext.close();
6
+ throw new Error("Audio blob is empty");
7
+ }
8
+ let audioBuffer;
9
+ try {
10
+ audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
11
+ } catch {
12
+ await audioContext.close();
13
+ throw new Error(
14
+ `Unable to decode audio data (type: ${blob.type}, size: ${blob.size} bytes). This may be due to an unsupported audio format or corrupted data.`
15
+ );
16
+ }
17
+ const channelData = audioBuffer.getChannelData(0);
18
+ const blockSize = Math.floor(channelData.length / sampleCount);
19
+ const peaks = [];
20
+ for (let i = 0; i < sampleCount; i++) {
21
+ const start = i * blockSize;
22
+ let sum = 0;
23
+ for (let j = 0; j < blockSize; j++) {
24
+ sum += Math.abs(channelData[start + j] || 0);
25
+ }
26
+ peaks.push(sum / blockSize);
27
+ }
28
+ const maxPeak = Math.max(...peaks);
29
+ const normalizedPeaks = maxPeak > 0 ? peaks.map((p) => p / maxPeak) : peaks;
30
+ await audioContext.close();
31
+ return normalizedPeaks;
32
+ }
33
+ const audioDataCache = /* @__PURE__ */ new WeakMap();
34
+ function getAudioData(blob, sampleCount) {
35
+ let promise = audioDataCache.get(blob);
36
+ if (!promise) {
37
+ promise = decodeAudioBlob(blob, sampleCount);
38
+ audioDataCache.set(blob, promise);
39
+ }
40
+ return promise;
41
+ }
42
+ export {
43
+ decodeAudioBlob,
44
+ getAudioData
45
+ };
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ function unwrapPromise(promise) {
4
+ const cached = promise;
5
+ if (!cached._status) {
6
+ cached._status = "pending";
7
+ cached._value = void 0;
8
+ cached._error = void 0;
9
+ cached.then(
10
+ (value) => {
11
+ cached._status = "fulfilled";
12
+ cached._value = value;
13
+ },
14
+ (error) => {
15
+ cached._status = "rejected";
16
+ cached._error = error;
17
+ }
18
+ );
19
+ }
20
+ if (cached._status === "rejected") throw cached._error;
21
+ if (cached._status === "pending") throw promise;
22
+ return cached._value;
23
+ }
24
+ exports.unwrapPromise = unwrapPromise;
@@ -0,0 +1,24 @@
1
+ function unwrapPromise(promise) {
2
+ const cached = promise;
3
+ if (!cached._status) {
4
+ cached._status = "pending";
5
+ cached._value = void 0;
6
+ cached._error = void 0;
7
+ cached.then(
8
+ (value) => {
9
+ cached._status = "fulfilled";
10
+ cached._value = value;
11
+ },
12
+ (error) => {
13
+ cached._status = "rejected";
14
+ cached._error = error;
15
+ }
16
+ );
17
+ }
18
+ if (cached._status === "rejected") throw cached._error;
19
+ if (cached._status === "pending") throw promise;
20
+ return cached._value;
21
+ }
22
+ export {
23
+ unwrapPromise
24
+ };
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const react = require("react");
5
+ const constants = require("../constants.cjs");
6
+ const WaveformRenderer = react.forwardRef(function WaveformRenderer2({ peaks, appearance, currentTime, duration, onSeek, onClick, style, ...props }, ref) {
7
+ const canvasRef = react.useRef(null);
8
+ const sizeRef = react.useRef({ width: 0, height: 0 });
9
+ const rafRef = react.useRef(0);
10
+ react.useImperativeHandle(ref, () => ({
11
+ canvas: canvasRef.current
12
+ }));
13
+ const drawWaveform = react.useCallback(() => {
14
+ const canvas = canvasRef.current;
15
+ const { width, height } = sizeRef.current;
16
+ if (!canvas || !peaks || width === 0 || height === 0) return;
17
+ const ctx = canvas.getContext("2d");
18
+ if (!ctx) return;
19
+ const dpr = window.devicePixelRatio || 1;
20
+ const targetWidth = width * dpr;
21
+ const targetHeight = height * dpr;
22
+ if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
23
+ canvas.width = targetWidth;
24
+ canvas.height = targetHeight;
25
+ }
26
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
27
+ ctx.clearRect(0, 0, width, height);
28
+ const barColor = appearance?.barColor ?? constants.DEFAULT_WAVEFORM_APPEARANCE.barColor;
29
+ const barWidth = appearance?.barWidth ?? constants.DEFAULT_WAVEFORM_APPEARANCE.barWidth;
30
+ const barGap = appearance?.barGap ?? constants.DEFAULT_WAVEFORM_APPEARANCE.barGap;
31
+ const barRadius = appearance?.barRadius ?? constants.DEFAULT_WAVEFORM_APPEARANCE.barRadius;
32
+ const barHeightScale = appearance?.barHeightScale ?? constants.DEFAULT_WAVEFORM_APPEARANCE.barHeightScale;
33
+ const totalBarWidth = barWidth + barGap;
34
+ const barsCount = Math.floor(width / totalBarWidth);
35
+ const step = peaks.length / barsCount;
36
+ ctx.fillStyle = barColor;
37
+ for (let i = 0; i < barsCount; i++) {
38
+ const peakIndex = Math.min(Math.floor(i * step), peaks.length - 1);
39
+ const peak = peaks[peakIndex];
40
+ const barHeight = Math.max(peak * height * barHeightScale, 2);
41
+ const x = i * totalBarWidth;
42
+ const y = (height - barHeight) / 2;
43
+ if (barRadius > 0) {
44
+ ctx.beginPath();
45
+ ctx.roundRect(x, y, barWidth, barHeight, barRadius);
46
+ ctx.fill();
47
+ } else {
48
+ ctx.fillRect(x, y, barWidth, barHeight);
49
+ }
50
+ }
51
+ if (currentTime !== void 0 && duration !== void 0 && duration > 0) {
52
+ const playheadX = currentTime / duration * width;
53
+ const playheadColor = appearance?.playheadColor ?? constants.DEFAULT_PLAYHEAD_APPEARANCE.playheadColor;
54
+ const playheadWidth = appearance?.playheadWidth ?? constants.DEFAULT_PLAYHEAD_APPEARANCE.playheadWidth;
55
+ ctx.fillStyle = playheadColor;
56
+ ctx.fillRect(playheadX - playheadWidth / 2, 0, playheadWidth, height);
57
+ }
58
+ }, [peaks, appearance, currentTime, duration]);
59
+ react.useEffect(() => {
60
+ const canvas = canvasRef.current;
61
+ if (!canvas) return;
62
+ const resizeObserver = new ResizeObserver((entries) => {
63
+ const entry = entries[0];
64
+ if (!entry) return;
65
+ const { width, height } = entry.contentRect;
66
+ if (sizeRef.current.width === width && sizeRef.current.height === height) return;
67
+ sizeRef.current = { width, height };
68
+ cancelAnimationFrame(rafRef.current);
69
+ rafRef.current = requestAnimationFrame(drawWaveform);
70
+ });
71
+ resizeObserver.observe(canvas);
72
+ return () => {
73
+ resizeObserver.disconnect();
74
+ cancelAnimationFrame(rafRef.current);
75
+ };
76
+ }, [drawWaveform]);
77
+ react.useEffect(() => {
78
+ drawWaveform();
79
+ }, [drawWaveform]);
80
+ const handleClick = react.useCallback(
81
+ (e) => {
82
+ if (!onSeek || !duration || duration <= 0) return;
83
+ const canvas = canvasRef.current;
84
+ if (!canvas) return;
85
+ const rect = canvas.getBoundingClientRect();
86
+ const x = e.clientX - rect.left;
87
+ const clickRatio = x / rect.width;
88
+ const newTime = Math.max(0, Math.min(clickRatio * duration, duration));
89
+ onSeek(newTime);
90
+ },
91
+ [onSeek, duration]
92
+ );
93
+ return /* @__PURE__ */ jsxRuntime.jsx(
94
+ "canvas",
95
+ {
96
+ ref: canvasRef,
97
+ role: "img",
98
+ "aria-label": "Audio waveform",
99
+ onClick: handleClick,
100
+ style: { cursor: onSeek ? "pointer" : void 0, ...style },
101
+ ...props
102
+ }
103
+ );
104
+ });
105
+ exports.WaveformRenderer = WaveformRenderer;
@@ -0,0 +1,105 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { forwardRef, useRef, useImperativeHandle, useCallback, useEffect } from "react";
3
+ import { DEFAULT_WAVEFORM_APPEARANCE, DEFAULT_PLAYHEAD_APPEARANCE } from "../constants.js";
4
+ const WaveformRenderer = forwardRef(function WaveformRenderer2({ peaks, appearance, currentTime, duration, onSeek, onClick, style, ...props }, ref) {
5
+ const canvasRef = useRef(null);
6
+ const sizeRef = useRef({ width: 0, height: 0 });
7
+ const rafRef = useRef(0);
8
+ useImperativeHandle(ref, () => ({
9
+ canvas: canvasRef.current
10
+ }));
11
+ const drawWaveform = useCallback(() => {
12
+ const canvas = canvasRef.current;
13
+ const { width, height } = sizeRef.current;
14
+ if (!canvas || !peaks || width === 0 || height === 0) return;
15
+ const ctx = canvas.getContext("2d");
16
+ if (!ctx) return;
17
+ const dpr = window.devicePixelRatio || 1;
18
+ const targetWidth = width * dpr;
19
+ const targetHeight = height * dpr;
20
+ if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
21
+ canvas.width = targetWidth;
22
+ canvas.height = targetHeight;
23
+ }
24
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
25
+ ctx.clearRect(0, 0, width, height);
26
+ const barColor = appearance?.barColor ?? DEFAULT_WAVEFORM_APPEARANCE.barColor;
27
+ const barWidth = appearance?.barWidth ?? DEFAULT_WAVEFORM_APPEARANCE.barWidth;
28
+ const barGap = appearance?.barGap ?? DEFAULT_WAVEFORM_APPEARANCE.barGap;
29
+ const barRadius = appearance?.barRadius ?? DEFAULT_WAVEFORM_APPEARANCE.barRadius;
30
+ const barHeightScale = appearance?.barHeightScale ?? DEFAULT_WAVEFORM_APPEARANCE.barHeightScale;
31
+ const totalBarWidth = barWidth + barGap;
32
+ const barsCount = Math.floor(width / totalBarWidth);
33
+ const step = peaks.length / barsCount;
34
+ ctx.fillStyle = barColor;
35
+ for (let i = 0; i < barsCount; i++) {
36
+ const peakIndex = Math.min(Math.floor(i * step), peaks.length - 1);
37
+ const peak = peaks[peakIndex];
38
+ const barHeight = Math.max(peak * height * barHeightScale, 2);
39
+ const x = i * totalBarWidth;
40
+ const y = (height - barHeight) / 2;
41
+ if (barRadius > 0) {
42
+ ctx.beginPath();
43
+ ctx.roundRect(x, y, barWidth, barHeight, barRadius);
44
+ ctx.fill();
45
+ } else {
46
+ ctx.fillRect(x, y, barWidth, barHeight);
47
+ }
48
+ }
49
+ if (currentTime !== void 0 && duration !== void 0 && duration > 0) {
50
+ const playheadX = currentTime / duration * width;
51
+ const playheadColor = appearance?.playheadColor ?? DEFAULT_PLAYHEAD_APPEARANCE.playheadColor;
52
+ const playheadWidth = appearance?.playheadWidth ?? DEFAULT_PLAYHEAD_APPEARANCE.playheadWidth;
53
+ ctx.fillStyle = playheadColor;
54
+ ctx.fillRect(playheadX - playheadWidth / 2, 0, playheadWidth, height);
55
+ }
56
+ }, [peaks, appearance, currentTime, duration]);
57
+ useEffect(() => {
58
+ const canvas = canvasRef.current;
59
+ if (!canvas) return;
60
+ const resizeObserver = new ResizeObserver((entries) => {
61
+ const entry = entries[0];
62
+ if (!entry) return;
63
+ const { width, height } = entry.contentRect;
64
+ if (sizeRef.current.width === width && sizeRef.current.height === height) return;
65
+ sizeRef.current = { width, height };
66
+ cancelAnimationFrame(rafRef.current);
67
+ rafRef.current = requestAnimationFrame(drawWaveform);
68
+ });
69
+ resizeObserver.observe(canvas);
70
+ return () => {
71
+ resizeObserver.disconnect();
72
+ cancelAnimationFrame(rafRef.current);
73
+ };
74
+ }, [drawWaveform]);
75
+ useEffect(() => {
76
+ drawWaveform();
77
+ }, [drawWaveform]);
78
+ const handleClick = useCallback(
79
+ (e) => {
80
+ if (!onSeek || !duration || duration <= 0) return;
81
+ const canvas = canvasRef.current;
82
+ if (!canvas) return;
83
+ const rect = canvas.getBoundingClientRect();
84
+ const x = e.clientX - rect.left;
85
+ const clickRatio = x / rect.width;
86
+ const newTime = Math.max(0, Math.min(clickRatio * duration, duration));
87
+ onSeek(newTime);
88
+ },
89
+ [onSeek, duration]
90
+ );
91
+ return /* @__PURE__ */ jsx(
92
+ "canvas",
93
+ {
94
+ ref: canvasRef,
95
+ role: "img",
96
+ "aria-label": "Audio waveform",
97
+ onClick: handleClick,
98
+ style: { cursor: onSeek ? "pointer" : void 0, ...style },
99
+ ...props
100
+ }
101
+ );
102
+ });
103
+ export {
104
+ WaveformRenderer
105
+ };
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "react-audio-wavekit",
3
+ "version": "0.1.0",
4
+ "license": "CC0-1.0",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "sideEffects": [
10
+ "**/*.css"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "import": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "require": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.cjs"
21
+ }
22
+ }
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "vite build",
29
+ "dev": "vite build --watch",
30
+ "storybook": "storybook dev -p 6006",
31
+ "build-storybook": "storybook build",
32
+ "deploy-storybook": "storybook build && netlify deploy --dir=storybook-static --prod --site 9971038c-e3a3-4338-acb5-d593e65775a8",
33
+ "check": "biome check . && tsgo --noEmit",
34
+ "fix": "biome check --write .",
35
+ "test": "vitest run",
36
+ "test:watch": "vitest",
37
+ "prepublishOnly": "bun run check && bun run build",
38
+ "release": "npm publish --access public"
39
+ },
40
+ "engines": {
41
+ "node": ">=20.19.0 || >=22.12.0"
42
+ },
43
+ "peerDependencies": {
44
+ "react": ">=18.0.0",
45
+ "react-dom": ">=18.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "@biomejs/biome": "2.3.8",
49
+ "@storybook/addon-docs": "10.1.4",
50
+ "@storybook/react": "10.1.4",
51
+ "@storybook/react-vite": "10.1.4",
52
+ "@tailwindcss/vite": "4.1.17",
53
+ "@testing-library/react": "^16.3.0",
54
+ "@types/node": "24.10.1",
55
+ "@types/react": "19.2.7",
56
+ "@types/react-dom": "19.2.3",
57
+ "@typescript/native-preview": "7.0.0-dev.20251205.1",
58
+ "@vitejs/plugin-react": "5.1.1",
59
+ "jsdom": "^27.2.0",
60
+ "lucide-react": "0.555.0",
61
+ "react": "19.2.1",
62
+ "react-dom": "19.2.1",
63
+ "storybook": "10.1.4",
64
+ "tailwindcss": "4.1.17",
65
+ "typescript": "5.9.3",
66
+ "vite": "7.2.6",
67
+ "vite-plugin-dts": "4.5.4",
68
+ "vitest": "^4.0.15"
69
+ },
70
+ "dependencies": {
71
+ "overlayscrollbars": "^2.13.0",
72
+ "overlayscrollbars-react": "^0.5.6"
73
+ }
74
+ }