├── .gitignore ├── src ├── index.ts ├── visualizerWrapper.ts ├── useVisualizer.tsx └── Visualizer.tsx ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── tsconfig.json ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | .npmrc 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useVisualizer'; 2 | export * from './Visualizer'; 3 | export * from './visualizerWrapper'; 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "npm" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Ensure Types Are Correct 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | push: 7 | branches: [ main ] 8 | 9 | jobs: 10 | ATTW: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | - name: Setup NodeJS 16 | uses: actions/setup-node@v3 17 | - name: Install Dependencies (Along With ATTW) 18 | run: npm install @arethetypeswrong/cli 19 | - name: Are The Types Wrong? 20 | # package should be built automatically using `prepack` 21 | run: npx @arethetypeswrong/cli --pack 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "react-jsx", 17 | "declaration": true, 18 | "outDir": "./lib", 19 | "rootDir": "./src/" 20 | }, 21 | "include": ["src"] 22 | } 23 | -------------------------------------------------------------------------------- /src/visualizerWrapper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | currentVisualizer, 3 | continuousVisualizer, 4 | VisualizerFunctions, 5 | } from "sound-visualizer"; 6 | import { UseVisualizerOptions } from "./useVisualizer"; 7 | 8 | type UndefinedValues = { 9 | [K in keyof T]: undefined; 10 | }; 11 | 12 | export function visualizerWrapper( 13 | audio: MediaStream | null, 14 | canvas: HTMLCanvasElement | null, 15 | options: UseVisualizerOptions = {} 16 | ): VisualizerFunctions | UndefinedValues { 17 | const { mode, ...drawOptions } = options; 18 | 19 | if (!audio || !canvas) 20 | return { stop: undefined, start: undefined, reset: undefined }; 21 | 22 | if (mode === "current") return currentVisualizer(audio, canvas, drawOptions); 23 | 24 | return continuousVisualizer(audio, canvas, drawOptions); 25 | } 26 | -------------------------------------------------------------------------------- /src/useVisualizer.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { 3 | DrawCurrentOptions, 4 | DrawContinuousOptions, 5 | } from "sound-visualizer"; 6 | import { visualizerWrapper } from "./visualizerWrapper"; 7 | 8 | type CurrentOptions = { 9 | mode: "current"; 10 | } & DrawCurrentOptions; 11 | 12 | type ContinuousOptions = { 13 | mode?: "continuous"; 14 | } & DrawContinuousOptions; 15 | 16 | /** 17 | * The options passed to `useVisualizer`. 18 | * Also part of the props for `Visualizer`, and the options for `visualizerWrapper`. 19 | **/ 20 | export type UseVisualizerOptions = CurrentOptions | ContinuousOptions; 21 | 22 | /** 23 | * Hook that wraps the `visualizer` functions from `sound-visualizer` with a `useMemo`. 24 | * 25 | * @param audio the audio to visualize 26 | * @param canvas the canvas to draw on 27 | * @param options additional options for the `draw` functions. 28 | * 29 | * @returns either the `VisualizerFunctions` from `sound-visualizer` or an empty object, depending on if `audio` or `canvas` are `null` or not. 30 | **/ 31 | export function useVisualizer( 32 | audio: MediaStream | null, 33 | canvas: HTMLCanvasElement | null, 34 | options: UseVisualizerOptions 35 | ) { 36 | return useMemo(() => { 37 | return visualizerWrapper(audio, canvas, options); 38 | }, [canvas]); 39 | } 40 | -------------------------------------------------------------------------------- /src/Visualizer.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useVisualizer, UseVisualizerOptions } from "./useVisualizer"; 3 | 4 | /** 5 | * The props passed to the function supplied as the child of `Visualizer`. 6 | **/ 7 | export interface VisualizerChildrenProps { 8 | /** 9 | * Sets the canvas for the `Visualizer` to draw on. 10 | * 11 | * Should be passed as the `ref` prop to an HTML `canvas` element. 12 | **/ 13 | canvasRef: (canvas: HTMLCanvasElement) => void; 14 | /** 15 | * Starts the visualization on the canvas. 16 | **/ 17 | start?: () => void; 18 | /** 19 | * Stops the visualization, but does not clear the canvas. 20 | **/ 21 | stop?: () => void; 22 | /** 23 | * Stops the visualization and clears the canvas. 24 | **/ 25 | reset?: () => void; 26 | } 27 | 28 | export type VisualizerProps = UseVisualizerOptions & { 29 | /** 30 | * A functional component that renders out the canvas and functions used by the `Visualizer`. 31 | **/ 32 | children?: React.FC; 33 | /** 34 | * The audio to visualize. 35 | * 36 | * **Note:** it is the responsibility of the comoponent rendering `Visualizer` to stop the `MediaStream`'s tracks, 37 | * in case it's a recording. 38 | **/ 39 | audio: MediaStream | null; 40 | /** 41 | * When `true`, the `start` function will be run as soon as there is audio and a canvas. 42 | **/ 43 | autoStart?: boolean; 44 | }; 45 | 46 | export const Visualizer: React.FC = (props) => { 47 | const { audio, children: Children, autoStart, ...visualizerOptions } = props; 48 | 49 | const [canvas, setCanvas] = useState(null); 50 | 51 | const functions = useVisualizer(audio, canvas, visualizerOptions); 52 | 53 | useEffect(() => { 54 | if (!autoStart) return; 55 | 56 | if (functions.start) functions.start(); 57 | }, [audio, canvas]); 58 | 59 | return <>{!!Children && }; 60 | }; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-sound-visualizer", 3 | "version": "1.2.0", 4 | "description": "A lightweight wrapper for the sound-visualizer library", 5 | "main": "./lib/index.js", 6 | "module": "./lib/index.mjs", 7 | "types": "./lib/index.d.ts", 8 | "files": [ 9 | "./lib" 10 | ], 11 | "exports": { 12 | ".": { 13 | "require": "./lib/index.js", 14 | "import": "./lib/index.mjs", 15 | "types": "./lib/index.d.ts" 16 | }, 17 | "./lib/useVisualizer": { 18 | "require": "./lib/useVisualizer.js", 19 | "import": "./lib/useVisualizer.mjs", 20 | "types": "./lib/useVisualizer.d.ts" 21 | }, 22 | "./lib/Visualizer": { 23 | "require": "./lib/Visualizer.js", 24 | "import": "./lib/Visualizer.mjs", 25 | "types": "./lib/Visualizer.d.ts" 26 | }, 27 | "./lib/visualizerWrapper": { 28 | "require": "./lib/visualizerWrapper.js", 29 | "import": "./lib/visualizerWrapper.mjs", 30 | "types": "./lib/visualizerWrapper.d.ts" 31 | } 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/ej-shafran/react-sound-visualizer.git" 36 | }, 37 | "keywords": [ 38 | "react", 39 | "audio", 40 | "sound", 41 | "visualizer", 42 | "sound-visualizer", 43 | "audio-visualizer", 44 | "waveform", 45 | "waveform-visualizer", 46 | "microphone", 47 | "recording", 48 | "microphone-visualizer", 49 | "recording-visualizer", 50 | "microphone-recording" 51 | ], 52 | "author": "ej-shafran", 53 | "license": "ISC", 54 | "scripts": { 55 | "prepublish": "npm run build", 56 | "prepack": "npm run build", 57 | "build": "npx tsup ./src/ --format cjs,esm --dts --clean --out-dir lib" 58 | }, 59 | "bugs": { 60 | "url": "https://github.com/ej-shafran/react-sound-visualizer/issues" 61 | }, 62 | "homepage": "https://github.com/ej-shafran/react-sound-visualizer#readme", 63 | "devDependencies": { 64 | "@types/react": "^19.0.6", 65 | "@types/react-dom": "^19.0.3", 66 | "react-dom": "^19.0.0", 67 | "tsup": "^8.0.0", 68 | "typescript": "^5.1.6" 69 | }, 70 | "peerDependencies": { 71 | "react": ">= 16" 72 | }, 73 | "dependencies": { 74 | "sound-visualizer": "^1.2.0" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-sound-visualizer 2 | 3 | ## Description 4 | 5 | This library acts as a lightweight React wrapper for [sound-visualizer](https://github.com/ej-shafran/sound-visualizer). 6 | 7 | You can check it out by visiting [the codesandbox](https://codesandbox.io/s/react-sound-visualizer-demo-gi8uhd). 8 | 9 | ## Getting Started 10 | 11 | ### Installation 12 | 13 | ```bash 14 | npm install react-sound-visualizer 15 | ``` 16 | 17 | ### Usage 18 | 19 | You'll mainly want to use the `Visualizer` component (you can see more documentation for it below): 20 | 21 | ```tsx 22 | import { useEffect, useState } from 'react'; 23 | import { Visualizer } from 'react-sound-visualizer'; 24 | 25 | function App() { 26 | const [audio, setAudio] = useState(null); 27 | 28 | useEffect(() => { 29 | navigator.mediaDevices 30 | .getUserMedia({ 31 | audio: true, 32 | video: false, 33 | }) 34 | .then(setAudio); 35 | }, []); 36 | 37 | return ( 38 | 39 | {({ canvasRef, stop, start, reset }) => ( 40 | <> 41 | 42 | 43 |
44 | 45 | 46 | 47 |
48 | 49 | )} 50 |
51 | ); 52 | } 53 | 54 | export default App; 55 | ``` 56 | 57 | If you need even more control over the visualization, the `useVisualizer` hook (used by `Visualizer` behind the scenes) is also exported. 58 | 59 | ## Documentation 60 | 61 | ### Components 62 | 63 | #### Visualizer 64 | 65 | ##### Props 66 | 67 | | **Prop** | **Type** | **Default** | **Explanation** | | 68 | |-----------------|--------------------------------------------|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---| 69 | | **audio** | `MediaStream \| null` | | The audio to visualize. Can be `null` since usually that's how your state will be initialized, but note that the functions will not be defined while it is `null`. | | 70 | | **children** | `React.FC` | | See **Types** section below. | | 71 | | **mode** | `"current" \| "continuous"` | `"continuous"` | The visualizer version to use. You can check out the [sound-visualizer](https://github.com/ej-shafran/sound-visualizer) docs to learn more about what each version means. | | 72 | | **lineWidth** | `number \| "thin" \| "thick" \| "default"` | `"default"` | The width of each line drawn onto the canvas for the visualization. If a thickness string is provided, it will be translated into a percentage of the canvas's width; if a number is provided it will be used a `px` value. | | 73 | | **strokeColor** | `string` | `"#000"` | The color of each line drawn onto the canvas for the visualization. | | 74 | | **autoStart** | `boolean` | `false` | When set to `true`, the `start` function will be called as soon as the `audio` is available. | | 75 | | **slices** | `number` | `100` | The number of slices drawn onto the canvas to make up the wave. *Only available as a prop when `mode` is `"continuous"`* | | 76 | | **heightNorm** | `number` | `1` | A number used to normalize the height of the wave; the wave function is multiplied by this number, so a number larger than 1 will exaggerate the height of the wave, while a number between 0 and 1 will minimize it. *Only available as a prop when `mode` is `"current"`* | | 77 | 78 | 79 | ### Hooks 80 | 81 | #### useVisualizer 82 | 83 | ```typescript 84 | export function useVisualizer( 85 | audio: MediaStream | null, 86 | canvas: HTMLCanvasElement | null, 87 | options?: UseVisualizerOptions 88 | ): Partial; 89 | ``` 90 | 91 | The `useVisualizer` hook acts as a simple wrapper for both the `currentVisualizer` and `continuousVisualizer` functions from 92 | `sound-visualizer`, which allows the user to pass `null` for the `audio` and `canvas` parameters 93 | and returns an empty object if `null` is passed for either. 94 | It also wraps the `VisualizerFunctions` that are returned in a `useMemo` hook. 95 | (*Note:* if you would rather not use a memo, you can directly use the `visualizerWrapper` function) 96 | 97 | ### Types 98 | 99 | #### VisualizerChildrenProps 100 | 101 | ```typescript 102 | export interface VisualizerChildrenProps { 103 | canvasRef: (canvas: HTMLCanvasElement) => void; 104 | start?: () => void; 105 | stop?: () => void; 106 | reset?: () => void; 107 | } 108 | ``` 109 | 110 | The `Visualizer`'s `children` prop must be a function that returns a `ReactNode`. 111 | The `canvasRef` must be passed as the `ref` prop to an HTML `canvas` element, 112 | *or else the visualizer will not draw anything!* 113 | 114 | #### UseVisualizerOptions 115 | 116 | ```typescript 117 | export type UseVisualizerOptions = 118 | | { mode: "current" } & DrawCurrentOptions 119 | | { mode?: "continuous" & DrawContinuousOptions 120 | ``` 121 | 122 | Where `DrawCurrentOptions` and `DrawContinuousOptions` are the types from `sound-visualizer`. 123 | 124 | ## Notes 125 | 126 | The `visualizerWrapper` function, used internally by `useVisualizer`, is exposed for convenience. 127 | --------------------------------------------------------------------------------