├── .husky ├── .gitignore ├── pre-commit └── commit-msg ├── testing ├── datasets │ └── .gitkeep ├── index.html ├── types.ts ├── tools │ ├── generate-manifest.ts │ ├── utils.ts │ └── bpm-reporter.ts └── index.ts ├── .versionrc.json ├── docs ├── vite-env.d.ts ├── public │ ├── realtime-bpm-analyzer-icon.png │ └── realtime-bpm-analyzer-share.png ├── .vitepress │ ├── theme │ │ ├── index.ts │ │ └── custom.css │ ├── config.ts │ └── components │ │ └── ExampleEmbed.vue ├── .gitignore ├── favicon-settings.json ├── package.json ├── scripts │ ├── dev-examples.ts │ └── build-examples.ts ├── readme.md ├── examples │ ├── basic-usage.md │ ├── streaming-audio.md │ ├── microphone-input.md │ ├── react.md │ └── vue.md ├── guide │ ├── migration-v5.md │ ├── introduction.md │ └── getting-started.md └── index.md ├── codecov.yml ├── examples ├── 04-react-basic │ ├── src │ │ ├── vite-env.d.ts │ │ ├── main.tsx │ │ ├── app.tsx │ │ ├── app.css │ │ └── components │ │ │ ├── bpm-analyzer.css │ │ │ └── bpm-analyzer.tsx │ ├── vite.config.ts │ ├── index.html │ ├── readme.md │ ├── package.json │ └── tsconfig.json ├── 05-react-streaming │ ├── src │ │ ├── vite-env.d.ts │ │ ├── main.tsx │ │ ├── app.tsx │ │ ├── app.css │ │ └── components │ │ │ └── bpm-analyzer.css │ ├── vite.config.ts │ ├── index.html │ ├── readme.md │ ├── tsconfig.json │ └── package.json ├── 06-react-microphone │ ├── src │ │ ├── vite-env.d.ts │ │ ├── main.tsx │ │ ├── app.tsx │ │ ├── app.css │ │ └── components │ │ │ ├── bpm-analyzer.css │ │ │ └── bpm-analyzer.tsx │ ├── vite.config.ts │ ├── index.html │ ├── readme.md │ ├── tsconfig.json │ └── package.json ├── 08-vue-streaming │ ├── src │ │ ├── vite-env.d.ts │ │ ├── main.ts │ │ ├── app.vue │ │ └── style.css │ ├── vite.config.ts │ ├── index.html │ ├── package.json │ ├── readme.md │ └── tsconfig.json ├── 09-vue-microphone │ ├── src │ │ ├── vite-env.d.ts │ │ ├── main.ts │ │ ├── app.vue │ │ ├── style.css │ │ └── components │ │ │ └── bpm-analyzer.vue │ ├── vite.config.ts │ ├── index.html │ ├── readme.md │ ├── package.json │ └── tsconfig.json ├── 07-vue-basic │ ├── src │ │ ├── main.ts │ │ ├── app.vue │ │ ├── style.css │ │ └── components │ │ │ └── bpm-analyzer.vue │ ├── vite.config.ts │ ├── index.html │ ├── package.json │ ├── readme.md │ └── tsconfig.json ├── 01-vanilla-basic │ ├── vite.config.ts │ ├── tsconfig.json │ ├── package.json │ ├── readme.md │ ├── src │ │ └── main.ts │ └── index.html ├── 02-vanilla-streaming │ ├── vite.config.ts │ ├── tsconfig.json │ ├── package.json │ ├── readme.md │ ├── src │ │ └── main.ts │ └── index.html ├── 03-vanilla-microphone │ ├── vite.config.ts │ ├── tsconfig.json │ ├── package.json │ ├── readme.md │ ├── index.html │ └── src │ │ └── main.ts └── readme.md ├── brand ├── icon-vector.ai ├── github-share.png ├── github-share.psd ├── icon-vector-rgb.ai ├── realtime-bpm-analyzer-icon.png ├── realtime-bpm-analyzer-icon.psd └── icon.svg ├── tests ├── fixtures │ └── bass-test.wav ├── lib │ ├── analyzer.ts │ ├── utils.ts │ └── realtime-bpm-analyzer.ts └── integration │ └── create-processor.test.ts ├── tsconfig.docs.json ├── .commitlintrc.cjs ├── types └── env.d.ts ├── web-dev-server.config.mjs ├── .gitignore ├── .npmignore ├── tsconfig.json ├── bin └── build │ └── library.sh ├── .editorconfig ├── .releaserc.json ├── code-of-conduct.md ├── web-test-runner.config.mjs ├── contributing.md ├── typedoc.json ├── src ├── core │ ├── consts.ts │ └── utils.ts └── processor │ └── realtime-bpm-processor.ts ├── security.md ├── .github └── workflows │ ├── main.yml │ └── codeql.yml ├── readme.md ├── privacy.md └── package.json /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /testing/datasets/.gitkeep: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | -------------------------------------------------------------------------------- /.versionrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "infile": "changelog.md" 3 | } 4 | -------------------------------------------------------------------------------- /docs/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | token: 9d63d0e7-3a40-4010-9727-eed86c66aec2 3 | -------------------------------------------------------------------------------- /examples/04-react-basic/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/05-react-streaming/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/06-react-microphone/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/08-vue-streaming/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/09-vue-microphone/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /brand/icon-vector.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlepaux/realtime-bpm-analyzer/HEAD/brand/icon-vector.ai -------------------------------------------------------------------------------- /brand/github-share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlepaux/realtime-bpm-analyzer/HEAD/brand/github-share.png -------------------------------------------------------------------------------- /brand/github-share.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlepaux/realtime-bpm-analyzer/HEAD/brand/github-share.psd -------------------------------------------------------------------------------- /brand/icon-vector-rgb.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlepaux/realtime-bpm-analyzer/HEAD/brand/icon-vector-rgb.ai -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /tests/fixtures/bass-test.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlepaux/realtime-bpm-analyzer/HEAD/tests/fixtures/bass-test.wav -------------------------------------------------------------------------------- /brand/realtime-bpm-analyzer-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlepaux/realtime-bpm-analyzer/HEAD/brand/realtime-bpm-analyzer-icon.png -------------------------------------------------------------------------------- /brand/realtime-bpm-analyzer-icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlepaux/realtime-bpm-analyzer/HEAD/brand/realtime-bpm-analyzer-icon.psd -------------------------------------------------------------------------------- /docs/public/realtime-bpm-analyzer-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlepaux/realtime-bpm-analyzer/HEAD/docs/public/realtime-bpm-analyzer-icon.png -------------------------------------------------------------------------------- /docs/public/realtime-bpm-analyzer-share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlepaux/realtime-bpm-analyzer/HEAD/docs/public/realtime-bpm-analyzer-share.png -------------------------------------------------------------------------------- /examples/07-vue-basic/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './app.vue'; 3 | import './style.css'; 4 | 5 | createApp(App).mount('#app'); 6 | -------------------------------------------------------------------------------- /examples/08-vue-streaming/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './app.vue'; 3 | import './style.css'; 4 | 5 | createApp(App).mount('#app'); 6 | -------------------------------------------------------------------------------- /examples/09-vue-microphone/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './app.vue'; 3 | import './style.css'; 4 | 5 | createApp(App).mount('#app'); 6 | -------------------------------------------------------------------------------- /tsconfig.docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts"], 4 | "exclude": ["tests/**", "testing/**", "node_modules/**", "dist/**", "docs/**"] 5 | } 6 | -------------------------------------------------------------------------------- /testing/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/01-vanilla-basic/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig({ 4 | server: { 5 | port: process.env.PORT ? parseInt(process.env.PORT) : 3001, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /examples/02-vanilla-streaming/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig({ 4 | server: { 5 | port: process.env.PORT ? parseInt(process.env.PORT) : 3002, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /examples/03-vanilla-microphone/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig({ 4 | server: { 5 | port: process.env.PORT ? parseInt(process.env.PORT) : 3003, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /.commitlintrc.cjs: -------------------------------------------------------------------------------- 1 | const commitlint = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'body-max-line-length': [0, 'never'], // Disable the line length check 5 | }, 6 | }; 7 | 8 | module.exports = commitlint; 9 | -------------------------------------------------------------------------------- /types/env.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | namespace NodeJS { 5 | interface ProcessEnv { // eslint-disable-line @typescript-eslint/consistent-type-definitions 6 | processors: Record; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/04-react-basic/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './app'; 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /examples/07-vue-basic/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | 4 | export default defineConfig({ 5 | plugins: [vue()], 6 | server: { 7 | port: process.env.PORT ? parseInt(process.env.PORT) : 3007, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /testing/types.ts: -------------------------------------------------------------------------------- 1 | import type {Tempo} from '../src/index'; 2 | 3 | export type Manifest = Record; 4 | 5 | export type Closure = () => Promise; 6 | 7 | export type AudioFile = { 8 | filename: string; 9 | bpm: number; 10 | tempos: Tempo[]; 11 | }; 12 | -------------------------------------------------------------------------------- /examples/05-react-streaming/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './app'; 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /examples/06-react-microphone/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './app'; 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /examples/08-vue-streaming/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | 4 | export default defineConfig({ 5 | plugins: [vue()], 6 | server: { 7 | port: process.env.PORT ? parseInt(process.env.PORT) : 3008, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/09-vue-microphone/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | 4 | export default defineConfig({ 5 | plugins: [vue()], 6 | server: { 7 | port: process.env.PORT ? parseInt(process.env.PORT) : 3009, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/04-react-basic/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | server: { 7 | port: process.env.PORT ? parseInt(process.env.PORT) : 3004, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/05-react-streaming/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | server: { 7 | port: process.env.PORT ? parseInt(process.env.PORT) : 3005, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/06-react-microphone/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | server: { 7 | port: process.env.PORT ? parseInt(process.env.PORT) : 3006, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /web-dev-server.config.mjs: -------------------------------------------------------------------------------- 1 | import {esbuildPlugin} from '@web/dev-server-esbuild'; 2 | 3 | export default { 4 | open: false, 5 | watch: false, 6 | nodeResolve: true, 7 | appIndex: 'testing/index.html', 8 | rootDir: '.', 9 | plugins: [esbuildPlugin({ts: true, target: 'auto'})], 10 | }; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | *.DS_Store 3 | coverage 4 | dist 5 | node_modules 6 | tests/datasets/* 7 | testing/datasets 8 | testing/manifest.json 9 | 10 | # Generated files (auto-created during build) 11 | src/generated-processor.ts 12 | 13 | # VitePress documentation build output 14 | docs/.vitepress/dist 15 | docs/.vitepress/cache 16 | docs/node_modules 17 | -------------------------------------------------------------------------------- /examples/07-vue-basic/src/app.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /examples/07-vue-basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BPM Analyzer - Vue 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/04-react-basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BPM Analyzer - React 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/08-vue-streaming/src/app.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /examples/08-vue-streaming/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BPM Analyzer - Vue Streaming 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/09-vue-microphone/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BPM Analyzer - Vue Microphone 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/09-vue-microphone/src/app.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /examples/05-react-streaming/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BPM Analyzer - React Streaming 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/06-react-microphone/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BPM Analyzer - React Microphone 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme' 2 | import type { Theme } from 'vitepress' 3 | import ExampleEmbed from '../components/ExampleEmbed.vue' 4 | import './custom.css' 5 | 6 | export default { 7 | extends: DefaultTheme, 8 | enhanceApp({ app }: { app: any }) { 9 | // Register ExampleEmbed component globally 10 | app.component('ExampleEmbed', ExampleEmbed) 11 | } 12 | } satisfies Theme 13 | -------------------------------------------------------------------------------- /examples/01-vanilla-basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "moduleResolution": "bundler", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "noEmit": true 13 | }, 14 | "include": ["src"] 15 | } 16 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # VitePress build output 2 | .vitepress/dist 3 | .vitepress/cache 4 | .vitepress/.temp 5 | 6 | # TypeDoc legacy HTML output (we use markdown now) 7 | docs/ 8 | api/ 9 | 10 | # Dependencies 11 | node_modules/ 12 | 13 | # System files 14 | .DS_Store 15 | 16 | # Copied from root 17 | contributing.md 18 | code-of-conduct.md 19 | security.md 20 | privacy.md 21 | 22 | # Generated assets 23 | public/favicon/ 24 | favicon.json 25 | -------------------------------------------------------------------------------- /examples/02-vanilla-streaming/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "moduleResolution": "bundler", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "noEmit": true 13 | }, 14 | "include": ["src"] 15 | } 16 | -------------------------------------------------------------------------------- /examples/03-vanilla-microphone/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "moduleResolution": "bundler", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "noEmit": true 13 | }, 14 | "include": ["src"] 15 | } 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .husky 3 | bin 4 | brand 5 | coverage 6 | docs 7 | github-pages 8 | node_modules 9 | src 10 | testing 11 | tests 12 | types 13 | .commitlintrc.cjs 14 | .editorconfig 15 | .eslintignore 16 | .gitignore 17 | .npmignore 18 | .versionrc.json 19 | build.ts 20 | changelog.md 21 | code-of-conduct.md 22 | codecov.yml 23 | contributing.md 24 | tsconfig.json 25 | typedoc.json 26 | web-test-runner.config.mjs 27 | web-dev-server.config.mjs 28 | -------------------------------------------------------------------------------- /examples/01-vanilla-basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "realtime-bpm-analyzer-example-vanilla-basic", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "realtime-bpm-analyzer": "file:../.." 13 | }, 14 | "devDependencies": { 15 | "typescript": "^5.6.0", 16 | "vite": "^5.4.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/02-vanilla-streaming/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "realtime-bpm-analyzer-example-vanilla-streaming", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "realtime-bpm-analyzer": "file:../.." 13 | }, 14 | "devDependencies": { 15 | "typescript": "^5.6.0", 16 | "vite": "^5.4.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/04-react-basic/src/app.tsx: -------------------------------------------------------------------------------- 1 | import BpmAnalyzer from './components/bpm-analyzer'; 2 | import './app.css'; 3 | 4 | function App() { 5 | return ( 6 |
7 |
8 |
9 |

🎵 BPM Analyzer

10 |

React Integration Example

11 |
12 | 13 |
14 |
15 | ); 16 | } 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/custom.css: -------------------------------------------------------------------------------- 1 | /* Custom styles for documentation */ 2 | 3 | /* Enhance code blocks */ 4 | .vp-doc div[class*='language-'] { 5 | margin: 1.5rem 0; 6 | } 7 | 8 | /* Make example embeds stand out */ 9 | .example-embed { 10 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); 11 | } 12 | 13 | /* Dark mode adjustments */ 14 | .dark .example-embed { 15 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); 16 | } 17 | 18 | .dark .example-frame { 19 | background: #1a1a1a; 20 | } 21 | -------------------------------------------------------------------------------- /examples/01-vanilla-basic/readme.md: -------------------------------------------------------------------------------- 1 | # Basic Usage Example 2 | 3 | A minimal example demonstrating how to analyze BPM from audio files using offline analysis. 4 | 5 | ## Features 6 | 7 | - File upload interface 8 | - Offline BPM analysis with `analyzeFullBuffer` 9 | - Clean, modern UI 10 | - No audio playback required 11 | 12 | ## Run Locally 13 | 14 | ```bash 15 | npm install 16 | npm run dev 17 | ``` 18 | 19 | Open [http://localhost:3000](http://localhost:3000) in your browser. 20 | -------------------------------------------------------------------------------- /examples/03-vanilla-microphone/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "realtime-bpm-analyzer-example-vanilla-microphone", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "realtime-bpm-analyzer": "file:../.." 13 | }, 14 | "devDependencies": { 15 | "typescript": "^5.6.0", 16 | "vite": "^5.4.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/02-vanilla-streaming/readme.md: -------------------------------------------------------------------------------- 1 | # Streaming Audio Example 2 | 3 | Demonstrates real-time BPM analysis from streaming audio sources (URLs). 4 | 5 | ## Features 6 | 7 | - Load audio from remote URLs 8 | - Real-time BPM detection during playback 9 | - Continuous analysis with confidence scores 10 | - Play/pause/stop controls 11 | 12 | ## Run Locally 13 | 14 | ```bash 15 | npm install 16 | npm run dev 17 | ``` 18 | 19 | Open [http://localhost:3001](http://localhost:3001) in your browser. 20 | -------------------------------------------------------------------------------- /examples/05-react-streaming/src/app.tsx: -------------------------------------------------------------------------------- 1 | import BpmAnalyzer from './components/bpm-analyzer'; 2 | import './app.css'; 3 | 4 | function App() { 5 | return ( 6 |
7 |
8 |
9 |

🎵 BPM Analyzer

10 |

Analyze BPM from streaming audio URLs

11 |
12 | 13 |
14 |
15 | ); 16 | } 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /examples/06-react-microphone/src/app.tsx: -------------------------------------------------------------------------------- 1 | import BpmAnalyzer from './components/bpm-analyzer'; 2 | import './app.css'; 3 | 4 | function App() { 5 | return ( 6 |
7 |
8 |
9 |

🎵 BPM Analyzer

10 |

Analyze BPM from microphone input in real-time

11 |
12 | 13 |
14 |
15 | ); 16 | } 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /examples/03-vanilla-microphone/readme.md: -------------------------------------------------------------------------------- 1 | # Microphone Input Example 2 | 3 | Real-time BPM detection from microphone or line-in audio input. 4 | 5 | ## Features 6 | 7 | - Real-time microphone input processing 8 | - Live BPM detection with continuous analysis 9 | - Tempo categorization (Slow, Moderate, Fast, etc.) 10 | - Visual feedback with animated microphone icon 11 | 12 | ## Run Locally 13 | 14 | ```bash 15 | npm install 16 | npm run dev 17 | ``` 18 | 19 | Open [http://localhost:3002](http://localhost:3002) in your browser. 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "module": "commonjs", 5 | "lib": ["dom", "dom.iterable", "es6"], 6 | "skipLibCheck": true, 7 | "declaration": true, 8 | "strict": true, 9 | "moduleResolution": "node", 10 | "sourceMap": true, 11 | "types" : ["mocha"] 12 | }, 13 | "include": ["tests/**/*.ts", "testing/**/*.ts", "github-pages/*.ts", "types/*.ts", "src/*.ts", "processor/*.ts"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /bin/build/library.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e # Exit on error 4 | 5 | echo "🔨 Building JavaScript bundles..." 6 | ts-node build.ts 7 | 8 | echo "📝 Generating TypeScript declarations..." 9 | tsc --skipLibCheck --emitDeclarationOnly src/index.ts --declaration --outdir dist 10 | tsc --skipLibCheck --emitDeclarationOnly src/processor/realtime-bpm-processor.ts --declaration --outfile dist/realtime-bpm-processor.d.ts 11 | 12 | echo "✅ Build complete! Files in dist/:" 13 | ls -lh dist/ | grep -E '\.(js|d\.ts)$' | awk '{print " " $9 " (" $5 ")"}' 14 | -------------------------------------------------------------------------------- /examples/07-vue-basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "realtime-bpm-analyzer-example-vue-basic", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "vue": "^3.5.0", 13 | "realtime-bpm-analyzer": "file:../.." 14 | }, 15 | "devDependencies": { 16 | "@vitejs/plugin-vue": "^5.1.0", 17 | "typescript": "^5.6.0", 18 | "vite": "^5.4.0", 19 | "vue-tsc": "^2.1.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/07-vue-basic/readme.md: -------------------------------------------------------------------------------- 1 | # Vue Example 2 | 3 | Vue 3 integration with Composition API and TypeScript for offline BPM analysis. 4 | 5 | ## Features 6 | 7 | - Vue 3 with Composition API 8 | - ` 145 | 146 | 147 | -------------------------------------------------------------------------------- /docs/examples/react.md: -------------------------------------------------------------------------------- 1 | # React Integration 2 | 3 | How to integrate Realtime BPM Analyzer in React applications with hooks and best practices. 4 | 5 | ## Live Examples 6 | 7 | We've built complete React examples demonstrating different use cases. Each is a fully functional application you can interact with: 8 | 9 | ### Basic File Upload 10 | 11 | Upload and analyze audio files to detect their BPM. 12 | 13 | 14 | 15 | ::: tip View Source 16 | Full source: [`examples/04-react-basic`](https://github.com/dlepaux/realtime-bpm-analyzer/tree/main/examples/04-react-basic) 17 | ::: 18 | 19 | ### Streaming Audio 20 | 21 | Analyze BPM from live audio streams or URLs. 22 | 23 | 24 | 25 | ::: tip View Source 26 | Full source: [`examples/05-react-streaming`](https://github.com/dlepaux/realtime-bpm-analyzer/tree/main/examples/05-react-streaming) 27 | ::: 28 | 29 | ### Microphone Input 30 | 31 | Real-time BPM detection from your microphone. 32 | 33 | 34 | 35 | ::: tip View Source 36 | Full source: [`examples/06-react-microphone`](https://github.com/dlepaux/realtime-bpm-analyzer/tree/main/examples/06-react-microphone) 37 | ::: 38 | 39 | ## Installation 40 | 41 | ```bash 42 | npm install realtime-bpm-analyzer 43 | ``` 44 | 45 | ## Key Concepts for React 46 | 47 | ### 1. Use State for Audio Context 48 | 49 | Create and store the audio context in component state to reuse it: 50 | 51 | ```typescript 52 | const [audioContext, setAudioContext] = useState(null); 53 | const [bpmAnalyzer, setBpmAnalyzer] = useState(null); 54 | ``` 55 | 56 | ### 2. Cleanup in useEffect 57 | 58 | Always cleanup audio nodes when component unmounts: 59 | 60 | ```typescript 61 | useEffect(() => { 62 | return () => { 63 | bpmAnalyzer?.disconnect(); 64 | audioContext?.close(); 65 | }; 66 | }, []); // Empty deps - cleanup only on unmount 67 | ``` 68 | 69 | ### 3. Handle Events with State Updates 70 | 71 | Convert BPM events to React state updates: 72 | 73 | ```typescript 74 | bpmAnalyzer.on('bpmStable', (data) => { 75 | if (data.bpm.length > 0) { 76 | setCurrentBpm(data.bpm[0].tempo); 77 | } 78 | }); 79 | ``` 80 | 81 | ## Basic Pattern 82 | 83 | Here's the essential pattern for any React integration: 84 | 85 | ```typescript 86 | import { createRealtimeBpmAnalyzer } from 'realtime-bpm-analyzer'; 87 | import { useState, useEffect } from 'react'; 88 | 89 | function BPMAnalyzer() { 90 | const [bpm, setBpm] = useState(null); 91 | const [audioContext, setAudioContext] = useState(null); 92 | const [bpmAnalyzer, setBpmAnalyzer] = useState(null); 93 | 94 | const startAnalysis = async () => { 95 | // Create audio context 96 | const ctx = new AudioContext(); 97 | setAudioContext(ctx); 98 | 99 | // Create analyzer 100 | const analyzer = await createRealtimeBpmAnalyzer(ctx); 101 | setBpmAnalyzer(analyzer); 102 | 103 | // Listen for BPM 104 | analyzer.on('bpmStable', (data) => { 105 | if (data.bpm.length > 0) { 106 | setBpm(data.bpm[0].tempo); 107 | } 108 | }); 109 | 110 | // Connect your audio source here... 111 | }; 112 | 113 | useEffect(() => { 114 | return () => { 115 | bpmAnalyzer?.disconnect(); 116 | audioContext?.close(); 117 | }; 118 | }, []); // Cleanup only on unmount 119 | 120 | return ( 121 |
122 | 123 | {bpm &&
BPM: {bpm}
} 124 |
125 | ); 126 | } 127 | ``` 128 | 129 | ## TypeScript Support 130 | 131 | The library is fully typed. Import types as needed: 132 | 133 | ```typescript 134 | import type { BpmAnalyzer, BpmCandidates } from 'realtime-bpm-analyzer'; 135 | ``` 136 | 137 | ## Next Steps 138 | 139 | - Explore the [live examples above](#live-examples) to see complete implementations 140 | - Check out [Vue Integration](/examples/vue) for Vue.js 141 | - Read the [API Documentation](/api/) for detailed reference 142 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | 3 | // https://vitepress.dev/reference/site-config 4 | export default defineConfig({ 5 | title: "Realtime BPM Analyzer", 6 | description: "A powerful TypeScript/JavaScript library for detecting the beats-per-minute (BPM) of audio or video sources in real-time", 7 | 8 | // Base URL - using root since deployed to www.realtime-bpm-analyzer.com 9 | base: '/', 10 | 11 | // Clean URLs (remove .html extension) 12 | cleanUrls: true, 13 | 14 | // Last updated timestamp 15 | lastUpdated: true, 16 | 17 | // Ignore dead links for pages we haven't created yet 18 | ignoreDeadLinks: [], 19 | 20 | head: [ 21 | ['meta', { name: 'theme-color', content: '#646cff' }], 22 | ['meta', { name: 'og:type', content: 'website' }], 23 | ['meta', { name: 'og:locale', content: 'en' }], 24 | ['meta', { name: 'og:site_name', content: 'Realtime BPM Analyzer' }], 25 | ['link', { rel: 'icon', type: 'image/png', href: '/favicon/favicon-96x96.png', sizes: '96x96' }], 26 | ['link', { rel: 'icon', type: 'image/svg+xml', href: '/favicon/favicon.svg' }], 27 | ['link', { rel: 'shortcut icon', href: '/favicon/favicon.ico' }], 28 | ['link', { rel: 'apple-touch-icon', sizes: '180x180', href: '/favicon/apple-touch-icon.png' }], 29 | ['meta', { name: 'apple-mobile-web-app-title', content: 'Realtime BPM Analyzer' }], 30 | ['link', { rel: 'manifest', href: '/favicon/site.webmanifest'}], 31 | ], 32 | 33 | 34 | themeConfig: { 35 | // https://vitepress.dev/reference/default-theme-config 36 | logo: '/logo.svg', 37 | 38 | nav: [ 39 | { text: 'Guide', link: '/guide/getting-started' }, 40 | { text: 'Examples', link: '/examples/basic-usage' }, 41 | { text: 'API Reference', link: '/api/' }, 42 | { 43 | text: 'v5.0.0', 44 | items: [ 45 | { text: 'Changelog', link: 'https://github.com/dlepaux/realtime-bpm-analyzer/blob/main/changelog.md' }, 46 | { text: 'Contributing', link: '/contributing' }, 47 | { text: 'Code of Conduct', link: '/code-of-conduct' }, 48 | { text: 'Security', link: '/security' }, 49 | { text: 'Privacy', link: '/privacy' } 50 | ] 51 | } 52 | ], 53 | 54 | sidebar: { 55 | '/guide/': [ 56 | { 57 | text: 'Introduction', 58 | items: [ 59 | { text: 'What is Realtime BPM Analyzer?', link: '/guide/introduction' }, 60 | { text: 'Getting Started', link: '/guide/getting-started' }, 61 | { text: 'Core Concepts', link: '/guide/core-concepts' } 62 | ] 63 | }, 64 | { 65 | text: 'Advanced', 66 | items: [ 67 | { text: 'How It Works', link: '/guide/how-it-works' }, 68 | { text: 'Performance Tips', link: '/guide/performance' }, 69 | { text: 'Browser Compatibility', link: '/guide/browser-compatibility' } 70 | ] 71 | } 72 | ], 73 | '/examples/': [ 74 | { 75 | text: 'Examples', 76 | items: [ 77 | { text: 'Basic Usage', link: '/examples/basic-usage' }, 78 | { text: 'Streaming Audio', link: '/examples/streaming-audio' }, 79 | { text: 'Microphone Input', link: '/examples/microphone-input' } 80 | ] 81 | }, 82 | { 83 | text: 'Framework Integration', 84 | items: [ 85 | { text: 'React', link: '/examples/react' }, 86 | { text: 'Vue', link: '/examples/vue' } 87 | ] 88 | } 89 | ] 90 | }, 91 | 92 | socialLinks: [ 93 | { icon: 'github', link: 'https://github.com/dlepaux/realtime-bpm-analyzer' }, 94 | { icon: 'npm', link: 'https://www.npmjs.com/package/realtime-bpm-analyzer' } 95 | ], 96 | 97 | footer: { 98 | message: 'Released under the Apache License 2.0', 99 | copyright: 'Copyright © 2025 David Lepaux' 100 | }, 101 | 102 | search: { 103 | provider: 'local' 104 | }, 105 | 106 | editLink: { 107 | pattern: 'https://github.com/dlepaux/realtime-bpm-analyzer/edit/main/docs/:path', 108 | text: 'Edit this page on GitHub' 109 | } 110 | } 111 | }) 112 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Realtime BPM Analyzer 2 | 3 | [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/xojs/xo) 4 | [![npm](https://img.shields.io/npm/dm/realtime-bpm-analyzer.svg)](https://www.npmjs.com/package/realtime-bpm-analyzer) 5 | [![npm](https://img.shields.io/npm/v/realtime-bpm-analyzer.svg)](https://www.npmjs.com/package/realtime-bpm-analyzer) 6 | [![CI Actions Status](https://github.com/dlepaux/realtime-bpm-analyzer/workflows/CI/badge.svg)](https://github.com/dlepaux/realtime-bpm-analyzer/actions) 7 | [![codecov](https://codecov.io/gh/dlepaux/realtime-bpm-analyzer/branch/main/graph/badge.svg)](https://codecov.io/gh/dlepaux/realtime-bpm-analyzer) 8 | 9 |
10 | Realtime BPM Analyzer 11 |
12 | 13 |

14 | A powerful TypeScript library for detecting beats-per-minute (BPM) in real-time. 15 |

16 | 17 |

18 | 📚 Documentation • 19 | 🎯 Examples • 20 | 🚀 Getting Started 21 |

22 | 23 | --- 24 | 25 | ## Features 26 | 27 | - **Zero dependencies** - Uses native Web Audio API 28 | - **Real-time analysis** - Analyze audio/video as it plays 29 | - **Multiple sources** - Works with files, streams, microphone, or any audio node 30 | - **Typed events** - Full TypeScript support with autocomplete 31 | - **Client-side only** - 100% privacy-focused, no data collection 32 | - **Supports MP3, FLAC, WAV** formats 33 | 34 | ## Installation 35 | 36 | ```bash 37 | npm install realtime-bpm-analyzer 38 | ``` 39 | 40 | ## Quick Example 41 | 42 | ```typescript 43 | import { createRealtimeBpmAnalyzer } from 'realtime-bpm-analyzer'; 44 | 45 | // Create analyzer 46 | const analyzer = await createRealtimeBpmAnalyzer(audioContext); 47 | 48 | // Connect your audio source 49 | audioSource.connect(analyzer.node); 50 | 51 | // Listen for BPM detection 52 | analyzer.on('bpm', (data) => { 53 | console.log('BPM detected:', data.bpm[0].tempo); 54 | }); 55 | ``` 56 | 57 | ## Documentation 58 | 59 | **📖 [Full Documentation](https://www.realtime-bpm-analyzer.com)** - Complete guides, API reference, and tutorials 60 | 61 | **Key sections:** 62 | - [Quick Start Guide](https://www.realtime-bpm-analyzer.com/guide/quick-start) - Get up and running 63 | - [API Reference](https://www.realtime-bpm-analyzer.com/api) - Complete API documentation 64 | - [Examples](https://www.realtime-bpm-analyzer.com/examples) - Live demos and code samples 65 | - [Migration to v5](https://www.realtime-bpm-analyzer.com/guide/migration-v5) - Upgrading from v4.x 66 | 67 | ## Use Cases 68 | 69 | - **Audio Players** - Display BPM while playing tracks 70 | - **Live Streams** - Continuous BPM detection from radio/streaming 71 | - **File Analysis** - Offline BPM detection from uploaded files 72 | - **Microphone Input** - Real-time analysis from live audio 73 | - **DJ Applications** - Beat matching and tempo detection 74 | 75 | ## Running Examples Locally 76 | 77 | ```bash 78 | # Install dependencies 79 | npm install 80 | 81 | # Run a specific example 82 | npm run dev --workspace=examples/01-vanilla-basic 83 | 84 | # Available examples: 85 | # - 01-vanilla-basic, 02-vanilla-streaming, 03-vanilla-microphone 86 | # - 04-react-basic, 05-react-streaming, 06-react-microphone 87 | # - 07-vue-basic, 08-vue-streaming, 09-vue-microphone 88 | ``` 89 | 90 | ## Privacy & Security 91 | 92 | - **100% client-side** processing 93 | - **No data collection** or transmission 94 | - **No audio recording** - real-time analysis only 95 | - **Open source** - fully auditable code 96 | 97 | See [privacy.md](privacy.md) and [security.md](security.md) for details. 98 | 99 | ## Contributing 100 | 101 | Contributions are welcome! See [contributing.md](contributing.md) for guidelines. 102 | 103 | ## License 104 | 105 | Apache-2.0 License - See [licence.md](licence.md) for details. 106 | 107 | For commercial licensing inquiries, contact: d.lepaux@gmail.com 108 | 109 | ## Credits 110 | 111 | Inspired by [Tornqvist's bpm-detective](https://github.com/tornqvist/bpm-detective) and [Joe Sullivan's algorithm](http://joesul.li/van/beat-detection-using-web-audio/). 112 | 113 | -------------------------------------------------------------------------------- /privacy.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | ## Overview 4 | 5 | Realtime BPM Analyzer is designed with privacy as a core principle. All audio processing happens entirely in your browser using the Web Audio API. 6 | 7 | ## Data Processing 8 | 9 | ### What We Process 10 | 11 | - **Audio signals** - Analyzed in real-time to detect tempo/BPM 12 | - **Peak detection data** - Temporary calculations for beat identification 13 | - **BPM candidates** - Results of the analysis algorithm 14 | 15 | ### What We DON'T Collect or Store 16 | 17 | - ❌ No audio recording or storage 18 | - ❌ No data sent to external servers 19 | - ❌ No telemetry or analytics 20 | - ❌ No cookies or tracking 21 | - ❌ No user information 22 | - ❌ No third-party data sharing 23 | 24 | ## How It Works 25 | 26 | ### Client-Side Processing 27 | 28 | All audio analysis happens **locally in your browser**: 29 | 30 | 1. Audio data flows through the Web Audio API 31 | 2. Analysis runs in an AudioWorklet (separate thread) 32 | 3. Results are returned to your application 33 | 4. No data leaves your device 34 | 35 | ### Memory Management 36 | 37 | - Audio data is processed in small chunks 38 | - Temporary buffers are cleared after analysis 39 | - No persistent storage of audio information 40 | - Memory is automatically managed by the browser 41 | 42 | ## Microphone Usage 43 | 44 | When using microphone input: 45 | 46 | ### Permission 47 | 48 | - Requires explicit browser permission via `getUserMedia` 49 | - You control when to request access 50 | - Users can deny or revoke permission at any time 51 | 52 | ### Processing 53 | 54 | - Audio is analyzed in real-time only 55 | - No recording or saving of microphone input 56 | - Processing stops when you stop the analyzer 57 | - Audio stream is released when disconnected 58 | 59 | ### Best Practices 60 | 61 | **For Developers:** 62 | - Always explain why microphone access is needed before requesting 63 | - Provide clear visual indicators when microphone is active 64 | - Stop the audio stream when done: `stream.getTracks().forEach(track => track.stop())` 65 | - Handle permission denial gracefully 66 | 67 | **For Users:** 68 | - The library never records your audio 69 | - Check for visual indicators (browser shows mic icon when active) 70 | - You can revoke permissions anytime in browser settings 71 | 72 | ## Audio Files 73 | 74 | When analyzing audio files: 75 | 76 | - Files are decoded in browser memory 77 | - Processing happens entirely client-side 78 | - No files are uploaded to any server 79 | - Analyzed data is temporary and not stored 80 | 81 | ## Third-Party Content 82 | 83 | ### No External Dependencies 84 | 85 | - This library has **zero runtime dependencies** 86 | - No third-party code runs during audio processing 87 | - No external APIs are called 88 | 89 | ### Remote Audio Streams 90 | 91 | If you analyze remote audio (URLs, streams): 92 | 93 | - Audio is fetched directly by your browser 94 | - Subject to CORS policies (server must allow it) 95 | - We don't intercept or proxy the audio 96 | - Privacy depends on the audio source provider 97 | 98 | ## Compliance 99 | 100 | ### GDPR & Privacy Laws 101 | 102 | - No personal data is processed by this library 103 | - No consent mechanisms needed (no data collection) 104 | - No data retention (nothing is stored) 105 | - No data processing agreements needed 106 | 107 | ### Your Responsibility 108 | 109 | As a developer using this library, you are responsible for: 110 | 111 | - Informing your users about your app's audio processing 112 | - Complying with applicable privacy laws in your jurisdiction 113 | - Managing permissions and user consent for your application 114 | - Implementing your own privacy policy if needed 115 | 116 | ## Transparency 117 | 118 | ### Open Source 119 | 120 | - All code is publicly available on [GitHub](https://github.com/dlepaux/realtime-bpm-analyzer) 121 | - Fully auditable implementation 122 | - Community-reviewed and maintained 123 | 124 | ### Audit Trail 125 | 126 | You can verify our privacy claims by: 127 | 128 | - Reviewing the source code 129 | - Checking network activity (no requests are made) 130 | - Inspecting browser memory (no persistent storage) 131 | - Using browser developer tools to monitor behavior 132 | 133 | ## Questions 134 | 135 | If you have privacy concerns or questions: 136 | 137 | - Open an issue on [GitHub](https://github.com/dlepaux/realtime-bpm-analyzer/issues) 138 | - Email: [d.lepaux@gmail.com](mailto:d.lepaux@gmail.com) 139 | 140 | --- 141 | 142 | **Last updated:** November 2025 143 | -------------------------------------------------------------------------------- /examples/07-vue-basic/src/components/bpm-analyzer.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 91 | 92 | 177 | -------------------------------------------------------------------------------- /tests/lib/realtime-bpm-analyzer.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {RealTimeBpmAnalyzer} from '../../src/core/realtime-bpm-analyzer'; 3 | import {createTestAudioContext, closeAudioContext, loadTestAudio, audioBufferToChunks, createEventCollector} from '../setup'; 4 | 5 | describe('RealTimeBpmAnalyzer - Unit tests', () => { 6 | it('should create a new RealTimeBpmAnalyzer instance', () => { 7 | const realTimeBpmAnalyzer = new RealTimeBpmAnalyzer(); 8 | expect(realTimeBpmAnalyzer).to.be.instanceOf(RealTimeBpmAnalyzer); 9 | }); 10 | 11 | it('should reset to initial state', () => { 12 | const realTimeBpmAnalyzer = new RealTimeBpmAnalyzer(); 13 | realTimeBpmAnalyzer.skipIndexes = 100; 14 | realTimeBpmAnalyzer.reset(); 15 | expect(realTimeBpmAnalyzer.skipIndexes).to.equal(1); 16 | }); 17 | 18 | it('should accept custom options', () => { 19 | const realTimeBpmAnalyzer = new RealTimeBpmAnalyzer({ 20 | continuousAnalysis: true, 21 | stabilizationTime: 10000, 22 | muteTimeInIndexes: 5000, 23 | debug: true, 24 | }); 25 | expect(realTimeBpmAnalyzer.options.continuousAnalysis).to.be.true; 26 | expect(realTimeBpmAnalyzer.options.stabilizationTime).to.equal(10000); 27 | }); 28 | }); 29 | 30 | describe('RealTimeBpmAnalyzer - Integration tests', () => { 31 | let audioContext: AudioContext; 32 | 33 | beforeEach(() => { 34 | audioContext = createTestAudioContext(); 35 | }); 36 | 37 | afterEach(async () => { 38 | await closeAudioContext(audioContext); 39 | }); 40 | 41 | it('should analyze chunks of PCM data and emit events', async () => { 42 | const realTimeBpmAnalyzer = new RealTimeBpmAnalyzer(); 43 | const bufferSize = 4096; 44 | const audioBuffer = await loadTestAudio(audioContext); 45 | const chunks = audioBufferToChunks(audioBuffer, bufferSize); 46 | const collector = createEventCollector(); 47 | 48 | for (const channelData of chunks) { 49 | await realTimeBpmAnalyzer.analyzeChunk({ 50 | audioSampleRate: audioContext.sampleRate, 51 | channelData, 52 | bufferSize, 53 | postMessage: collector.postMessage, 54 | }); 55 | } 56 | 57 | // Should have emitted bpm events 58 | const bpmEvents = collector.getEventsByType('bpm'); 59 | expect(bpmEvents.length).to.be.greaterThan(0); 60 | 61 | // Verify event structure 62 | const firstEvent = bpmEvents[0]; 63 | expect(firstEvent.data).to.have.property('bpm'); 64 | expect(firstEvent.data).to.have.property('threshold'); 65 | }); 66 | 67 | it('should emit analyzerReset in continuous mode', async function () { 68 | this.timeout(10000); 69 | 70 | const realTimeBpmAnalyzer = new RealTimeBpmAnalyzer({ 71 | continuousAnalysis: true, 72 | stabilizationTime: 1, // Very short for testing 73 | debug: false, 74 | }); 75 | 76 | const bufferSize = 4096; 77 | const audioBuffer = await loadTestAudio(audioContext); 78 | const chunks = audioBufferToChunks(audioBuffer, bufferSize); 79 | const collector = createEventCollector(); 80 | 81 | for (const channelData of chunks) { 82 | await realTimeBpmAnalyzer.analyzeChunk({ 83 | audioSampleRate: audioContext.sampleRate, 84 | channelData, 85 | bufferSize, 86 | postMessage: collector.postMessage, 87 | }); 88 | } 89 | 90 | // With very short stabilization time and long audio, should reset 91 | const resetEvents = collector.getEventsByType('analyzerReset'); 92 | expect(resetEvents.length).to.be.greaterThan(0); 93 | }); 94 | 95 | it('should emit debug events when debug mode is enabled', async () => { 96 | const realTimeBpmAnalyzer = new RealTimeBpmAnalyzer({ 97 | debug: true, 98 | }); 99 | 100 | const bufferSize = 4096; 101 | const audioBuffer = await loadTestAudio(audioContext); 102 | const chunks = audioBufferToChunks(audioBuffer, bufferSize); 103 | const collector = createEventCollector(); 104 | 105 | // Process just a few chunks for debug events 106 | for (let i = 0; i < 5 && i < chunks.length; i++) { 107 | await realTimeBpmAnalyzer.analyzeChunk({ 108 | audioSampleRate: audioContext.sampleRate, 109 | channelData: chunks[i], 110 | bufferSize, 111 | postMessage: collector.postMessage, 112 | }); 113 | } 114 | 115 | // Should have debug events 116 | const debugEvents = collector.events.filter(e => e.type === 'analyzeChunk' || e.type === 'validPeak'); 117 | expect(debugEvents.length).to.be.greaterThan(0); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /examples/03-vanilla-microphone/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BPM Analyzer - Microphone Input 7 | 147 | 148 | 149 |
150 |

🎵 BPM Analyzer

151 |

Analyze BPM from microphone input in real-time

152 | 153 |
154 | 155 | 156 |
157 | 158 |
159 | 160 |
161 |
--
162 |
BPM
163 |
164 |
165 | 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "realtime-bpm-analyzer", 3 | "version": "5.0.0", 4 | "description": "This dependency free library can analyze the BPM (Tempo) of an audio/video node or any stream in realtime on your browser", 5 | "author": { 6 | "name": "David Lepaux", 7 | "email": "d.lepaux@gmail.com", 8 | "url": "https://github.com/dlepaux" 9 | }, 10 | "keywords": [ 11 | "webaudioapi", 12 | "audiobuffer", 13 | "audio", 14 | "stream", 15 | "microphone", 16 | "realtime", 17 | "no-dependency", 18 | "tempo", 19 | "bpm", 20 | "beats", 21 | "analyzer" 22 | ], 23 | "license": "Apache-2.0", 24 | "main": "dist/index.js", 25 | "module": "dist/index.esm.js", 26 | "types": "dist/index.d.ts", 27 | "exports": { 28 | ".": { 29 | "types": "./dist/index.d.ts", 30 | "import": "./dist/index.esm.js", 31 | "require": "./dist/index.js" 32 | }, 33 | "./processor": { 34 | "types": "./dist/realtime-bpm-processor.d.ts", 35 | "default": "./dist/realtime-bpm-processor.js" 36 | }, 37 | "./package.json": "./package.json" 38 | }, 39 | "files": [ 40 | "dist", 41 | "readme.md", 42 | "licence.md" 43 | ], 44 | "sideEffects": false, 45 | "publishConfig": { 46 | "access": "public", 47 | "provenance": true 48 | }, 49 | "workspaces": [ 50 | "examples/*" 51 | ], 52 | "scripts": { 53 | "build": "bash bin/build/library.sh", 54 | "docs:dev": "cd docs && npm run dev", 55 | "docs:build": "cd docs && npm run build", 56 | "docs:preview": "cd docs && npm run preview", 57 | "examples:install": "npm install --workspaces", 58 | "examples:build": "npm run build --workspaces --if-present", 59 | "examples:dev": "cd docs && npm run dev:examples", 60 | "lint": "xo", 61 | "lint:fix": "xo --fix", 62 | "prepare": "husky", 63 | "testing:prepare": "ts-node testing/tools/generate-manifest.ts", 64 | "testing": "web-dev-server", 65 | "test": "web-test-runner", 66 | "test:watch": "web-test-runner --watch", 67 | "test:unit": "web-test-runner --files 'tests/unit/**/*.test.ts'", 68 | "test:integration": "web-test-runner --files 'tests/integration/**/*.test.ts'", 69 | "test:lib": "web-test-runner --files 'tests/lib/**/*.ts'", 70 | "test:coverage": "web-test-runner --coverage" 71 | }, 72 | "repository": { 73 | "type": "git", 74 | "url": "https://github.com/dlepaux/realtime-bpm-analyzer" 75 | }, 76 | "devDependencies": { 77 | "@commitlint/cli": "^18.5.0", 78 | "@commitlint/config-conventional": "^18.5.0", 79 | "@semantic-release/changelog": "^6.0.3", 80 | "@semantic-release/commit-analyzer": "^13.0.1", 81 | "@semantic-release/git": "^10.0.1", 82 | "@semantic-release/npm": "^13.1.2", 83 | "@semantic-release/release-notes-generator": "^14.1.0", 84 | "@types/chai": "^4.3.11", 85 | "@types/mocha": "^10.0.6", 86 | "@web/dev-server": "^0.4.1", 87 | "@web/dev-server-esbuild": "^1.0.1", 88 | "@web/test-runner": "^0.18.0", 89 | "@web/test-runner-puppeteer": "^0.18.0", 90 | "chai": "^5.0.3", 91 | "esbuild": "^0.25.0", 92 | "eslint": "^8.56.0", 93 | "husky": "^8.0.3", 94 | "mocha": "^10.2.0", 95 | "music-metadata": "^7.14.0", 96 | "semantic-release": "^25.0.2", 97 | "ts-node": "^10.9.2", 98 | "typescript": "^5.3.3", 99 | "xo": "^0.56.0" 100 | }, 101 | "xo": { 102 | "space": 2, 103 | "semicolon": true, 104 | "ignores": [ 105 | "examples/**", 106 | "docs/scripts/**" 107 | ], 108 | "globals": [ 109 | "process", 110 | "document", 111 | "navigator", 112 | "window" 113 | ], 114 | "rules": { 115 | "n/prefer-global/process": "off", 116 | "node/prefer-global/process": 0, 117 | "no-await-in-loop": "off", 118 | "jsdoc/tag-lines": "off", 119 | "import/no-unassigned-import": "off", 120 | "import/no-anonymous-default-export": "off", 121 | "import/extensions": "off", 122 | "unicorn/no-array-reduce": "off", 123 | "unicorn/numeric-separators-style": "off", 124 | "unicorn/prefer-top-level-await": "off", 125 | "unicorn/prefer-add-event-listener": "off", 126 | "@typescript-eslint/triple-slash-reference": "off", 127 | "@typescript-eslint/no-loss-of-precision": "off", 128 | "@typescript-eslint/no-unused-expressions": "off", 129 | "no-promise-executor-return": "off", 130 | "@typescript-eslint/no-empty-function": "off", 131 | "unicorn/prevent-abbreviations": "off" 132 | } 133 | }, 134 | "lint-staged": { 135 | "**/*": "xo --fix" 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/processor/realtime-bpm-processor.ts: -------------------------------------------------------------------------------- 1 | import {realtimeBpmProcessorName} from '../core/consts'; 2 | import {chunkAggregator} from '../core/utils'; 3 | import {RealTimeBpmAnalyzer} from '../core/realtime-bpm-analyzer'; 4 | import type {ProcessorInputMessage, AggregateData, RealTimeBpmAnalyzerParameters, ProcessorOutputEvent} from '../core/types'; 5 | 6 | /** 7 | * Those declaration are from the package @types/audioworklet. But it is not compatible with the lib 'dom'. 8 | */ 9 | /* eslint-disable no-var, @typescript-eslint/prefer-function-type, @typescript-eslint/no-empty-interface, @typescript-eslint/consistent-type-definitions, @typescript-eslint/no-redeclare, @typescript-eslint/naming-convention */ 10 | declare var sampleRate: number; 11 | 12 | interface AudioWorkletProcessor { 13 | readonly port: AuthorizedMessagePort; 14 | } 15 | 16 | // Define a type for a message port that only accepts specific message types 17 | interface AuthorizedMessagePort extends MessagePort { 18 | postMessage(message: ProcessorOutputEvent): void; 19 | } 20 | 21 | type AudioWorkletProcessorParameters = { 22 | numberOfInputs: number; 23 | numberOfOutputs: number; 24 | processorOptions: RealTimeBpmAnalyzerParameters; 25 | }; 26 | 27 | declare var AudioWorkletProcessor: { 28 | prototype: AudioWorkletProcessor; 29 | new(options?: AudioWorkletProcessorParameters): AudioWorkletProcessor; 30 | }; 31 | 32 | interface AudioWorkletProcessorImpl extends AudioWorkletProcessor { 33 | process(inputs: Float32Array[][], outputs: Float32Array[][], parameters: Record): boolean; 34 | } 35 | 36 | interface WorkletGlobalScope {} 37 | 38 | declare var WorkletGlobalScope: { 39 | prototype: WorkletGlobalScope; 40 | new(): WorkletGlobalScope; 41 | }; 42 | 43 | interface AudioWorkletGlobalScope extends WorkletGlobalScope { 44 | readonly currentFrame: number; 45 | readonly currentTime: number; 46 | readonly sampleRate: number; 47 | registerProcessor(name: string, processorCtor: AudioWorkletProcessorConstructor): void; 48 | } 49 | 50 | declare var AudioWorkletGlobalScope: { 51 | prototype: AudioWorkletGlobalScope; 52 | new(): AudioWorkletGlobalScope; 53 | }; 54 | 55 | interface AudioWorkletProcessorConstructor { 56 | new (options: any): AudioWorkletProcessorImpl; 57 | } 58 | 59 | declare function registerProcessor(name: string, processorCtor: AudioWorkletProcessorConstructor): void; 60 | /* eslint-enable no-var, @typescript-eslint/prefer-function-type, @typescript-eslint/no-empty-interface, @typescript-eslint/consistent-type-definitions, @typescript-eslint/no-redeclare, @typescript-eslint/naming-convention */ 61 | 62 | export class RealTimeBpmProcessor extends AudioWorkletProcessor { 63 | aggregate: (pcmData: Float32Array) => AggregateData; 64 | realTimeBpmAnalyzer: RealTimeBpmAnalyzer; 65 | stopped = false; 66 | 67 | constructor(options: AudioWorkletProcessorParameters) { 68 | super(options); 69 | 70 | this.aggregate = chunkAggregator(); 71 | this.realTimeBpmAnalyzer = new RealTimeBpmAnalyzer(options.processorOptions); 72 | 73 | this.port.addEventListener('message', this.onMessage.bind(this)); 74 | this.port.start(); 75 | } 76 | 77 | /** 78 | * Handle message event 79 | * @param _event Contain event data from main process 80 | */ 81 | onMessage(_event: ProcessorInputMessage): void { 82 | // Handle custom message client 83 | } 84 | 85 | /** 86 | * Process function to handle chunks of data 87 | * @param inputs Inputs (the data we need to process) 88 | * @param _outputs Outputs (not useful for now) 89 | * @param _parameters Parameters 90 | * @returns Process ended successfully 91 | */ 92 | process(inputs: Float32Array[][], _outputs: Float32Array[][], _parameters: Record): boolean { 93 | const currentChunk = inputs[0][0]; 94 | 95 | if (this.stopped) { 96 | return true; 97 | } 98 | 99 | if (!currentChunk) { 100 | return true; 101 | } 102 | 103 | const {isBufferFull, buffer, bufferSize} = this.aggregate(currentChunk); 104 | 105 | if (isBufferFull) { 106 | // The variable sampleRate is global ! thanks to the AudioWorkletProcessor 107 | this.realTimeBpmAnalyzer.analyzeChunk({audioSampleRate: sampleRate, channelData: buffer, bufferSize, postMessage: event => { 108 | this.port.postMessage(event); 109 | }}).catch((error: unknown) => { 110 | // Emit error event to allow user-level error handling 111 | this.port.postMessage({ 112 | type: 'error', 113 | data: { 114 | message: error instanceof Error ? error.message : 'Unknown error during BPM analysis', 115 | error: error instanceof Error ? error : new Error(String(error)), 116 | }, 117 | }); 118 | }); 119 | } 120 | 121 | return true; 122 | } 123 | } 124 | 125 | /** 126 | * Mandatory Registration to use the processor 127 | */ 128 | registerProcessor(realtimeBpmProcessorName, RealTimeBpmProcessor); 129 | 130 | export default {}; 131 | -------------------------------------------------------------------------------- /examples/readme.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains complete, working examples demonstrating how to use the `realtime-bpm-analyzer` library across different use cases and frameworks. 4 | 5 | ## Overview 6 | 7 | The examples are organized by framework and use case: 8 | 9 | ### Vanilla JavaScript Examples 10 | 11 | - **[01-vanilla-basic](./01-vanilla-basic/)** - Basic offline BPM analysis from uploaded audio files 12 | - **[02-vanilla-streaming](./02-vanilla-streaming/)** - Real-time analysis from streaming audio URLs 13 | - **[03-vanilla-microphone](./03-vanilla-microphone/)** - Real-time analysis from microphone input 14 | 15 | ### React Examples 16 | 17 | - **[04-react-basic](./04-react-basic/)** - Basic offline BPM analysis with React hooks 18 | - **[05-react-streaming](./05-react-streaming/)** - Streaming audio analysis with React 19 | - **[06-react-microphone](./06-react-microphone/)** - Microphone input analysis with React 20 | 21 | ### Vue 3 Examples 22 | 23 | - **[07-vue-basic](./07-vue-basic/)** - Basic offline BPM analysis with Vue Composition API 24 | - **[08-vue-streaming](./08-vue-streaming/)** - Streaming audio analysis with Vue 25 | - **[09-vue-microphone](./09-vue-microphone/)** - Microphone input analysis with Vue 26 | 27 | ## Use Cases 28 | 29 | ### 1. Basic / Offline Analysis (`analyzeFullBuffer`) 30 | 31 | **When to use:** You have an audio file and want fast, accurate BPM detection without playback. 32 | 33 | **Examples:** 01-vanilla-basic, 04-react-basic, 07-vue-basic 34 | 35 | **Key Features:** 36 | - Fast offline analysis 37 | - No playback needed 38 | - Returns array of BPM candidates 39 | - Best for file uploads 40 | 41 | ### 2. Streaming Audio (`createRealtimeBpmAnalyzer` + MediaElementSource) 42 | 43 | **When to use:** Analyzing BPM from streaming URLs like internet radio, remote audio files. 44 | 45 | **Examples:** 02-vanilla-streaming, 05-react-streaming, 08-vue-streaming 46 | 47 | **Key Features:** 48 | - Real-time analysis during playback 49 | - Uses `getBiquadFilter()` for audio processing 50 | - Connection: `source → filter → analyzer.node → destination` 51 | - Listens for `bpmStable` event 52 | 53 | ### 3. Microphone Input (`createRealtimeBpmAnalyzer` + MediaStreamSource) 54 | 55 | **When to use:** Live BPM detection from microphone, instruments, or any audio input. 56 | 57 | **Examples:** 03-vanilla-microphone, 06-react-microphone, 09-vue-microphone 58 | 59 | **Key Features:** 60 | - Real-time analysis from live input 61 | - Requires microphone permissions 62 | - Connection: `source → analyser` and `source → analyzer.node` 63 | - Proper cleanup with `suspend()` and `disconnect()` 64 | 65 | ## Running Examples 66 | 67 | Each example is a standalone project. To run any example: 68 | 69 | ```bash 70 | cd examples/ 71 | npm install 72 | npm run dev 73 | ``` 74 | 75 | Then open the URL shown in the terminal (typically http://localhost:5173). 76 | 77 | ## Testing All Examples 78 | 79 | From the root of the repository: 80 | 81 | ```bash 82 | # Install dependencies for all examples 83 | npm run examples:install 84 | 85 | # Build all examples 86 | npm run examples:build 87 | 88 | # Run a specific example 89 | npm run dev --workspace=examples/01-vanilla-basic 90 | ``` 91 | 92 | ## Architecture Patterns 93 | 94 | All examples follow these best practices from the allegro-project reference implementation: 95 | 96 | 1. **Proper Audio Graph Connections** 97 | - Always connect analyzer to the audio graph 98 | - Use appropriate source types (MediaElementSource, MediaStreamSource) 99 | - Include AnalyserNode for potential visualizations 100 | 101 | 2. **Event Handling** 102 | - Listen for `bpmStable` event for stable BPM detection 103 | - Use `BpmCandidates` type with array of tempo values 104 | - Handle multiple BPM candidates when needed 105 | 106 | 3. **Resource Management** 107 | - Call `suspend()` before `disconnect()` 108 | - Stop media streams when done 109 | - Clean up all audio nodes 110 | - Properly handle component unmounting 111 | 112 | 4. **Error Handling** 113 | - Handle microphone permission errors 114 | - Catch audio loading failures 115 | - Provide user-friendly error messages 116 | 117 | ## Technologies Used 118 | 119 | - **Build Tool:** Vite 120 | - **TypeScript:** Full type safety 121 | - **React:** v18.3+ with hooks 122 | - **Vue:** v3.5+ with Composition API 123 | - **Styling:** Inline CSS with consistent design system 124 | 125 | ## Contributing 126 | 127 | When adding new examples: 128 | 129 | 1. Follow the naming convention: `##-framework-usecase` 130 | 2. Include a README.md explaining the example 131 | 3. Use the same visual styling (purple gradient theme) 132 | 4. Follow the architecture patterns from allegro-project 133 | 5. Test the example thoroughly before committing 134 | 135 | ## Need Help? 136 | 137 | - Check the [main documentation](../docs/) 138 | - Review the [API reference](../docs/api/) 139 | - Look at similar examples for your framework 140 | - Compare with the allegro-project reference implementation 141 | -------------------------------------------------------------------------------- /docs/examples/vue.md: -------------------------------------------------------------------------------- 1 | # Vue Integration 2 | 3 | How to integrate Realtime BPM Analyzer in Vue 3 applications using the Composition API. 4 | 5 | ## Live Examples 6 | 7 | We've built complete Vue 3 examples demonstrating different use cases. Each is a fully functional application you can interact with: 8 | 9 | ### Basic File Upload 10 | 11 | Upload and analyze audio files to detect their BPM. 12 | 13 | 14 | 15 | ::: tip View Source 16 | Full source: [`examples/07-vue-basic`](https://github.com/dlepaux/realtime-bpm-analyzer/tree/main/examples/07-vue-basic) 17 | ::: 18 | 19 | ### Streaming Audio 20 | 21 | Analyze BPM from live audio streams or URLs. 22 | 23 | 24 | 25 | ::: tip View Source 26 | Full source: [`examples/08-vue-streaming`](https://github.com/dlepaux/realtime-bpm-analyzer/tree/main/examples/08-vue-streaming) 27 | ::: 28 | 29 | ### Microphone Input 30 | 31 | Real-time BPM detection from your microphone. 32 | 33 | 34 | 35 | ::: tip View Source 36 | Full source: [`examples/09-vue-microphone`](https://github.com/dlepaux/realtime-bpm-analyzer/tree/main/examples/09-vue-microphone) 37 | ::: 38 | 39 | ## Installation 40 | 41 | ```bash 42 | npm install realtime-bpm-analyzer 43 | ``` 44 | 45 | ## Key Concepts for Vue 46 | 47 | ### 1. Use Refs for Reactive State 48 | 49 | Store BPM and analyzer state in Vue refs: 50 | 51 | ```typescript 52 | import { ref } from 'vue'; 53 | 54 | const bpm = ref(0); 55 | const audioContext = ref(null); 56 | const bpmAnalyzer = ref(null); 57 | ``` 58 | 59 | ### 2. Cleanup with onUnmounted 60 | 61 | Always cleanup audio nodes when component unmounts: 62 | 63 | ```typescript 64 | import { onUnmounted } from 'vue'; 65 | 66 | onUnmounted(() => { 67 | bpmAnalyzer.value?.disconnect(); 68 | audioContext.value?.close(); 69 | }); 70 | ``` 71 | 72 | ### 3. Handle Events with Ref Updates 73 | 74 | Convert BPM events to reactive updates: 75 | 76 | ```typescript 77 | bpmAnalyzer.value?.on('bpmStable', (data) => { 78 | if (data.bpm.length > 0) { 79 | bpm.value = data.bpm[0].tempo; 80 | } 81 | }); 82 | ``` 83 | 84 | ## Basic Pattern 85 | 86 | Here's the essential pattern for Vue integration using Composition API: 87 | 88 | ```vue 89 | 120 | 121 | 127 | ``` 128 | 129 | ## Composables Pattern 130 | 131 | You can create a reusable composable for BPM analysis: 132 | 133 | ```typescript 134 | // composables/useBpmAnalyzer.ts 135 | import { ref, onUnmounted } from 'vue'; 136 | import { createRealtimeBpmAnalyzer, type BpmAnalyzer } from 'realtime-bpm-analyzer'; 137 | 138 | export function useBpmAnalyzer() { 139 | const bpm = ref(0); 140 | const isAnalyzing = ref(false); 141 | 142 | let audioContext: AudioContext | null = null; 143 | let analyzer: BpmAnalyzer | null = null; 144 | 145 | const start = async (audioSource: AudioNode) => { 146 | audioContext = new AudioContext(); 147 | analyzer = await createRealtimeBpmAnalyzer(audioContext); 148 | 149 | audioSource.connect(analyzer.node); 150 | 151 | analyzer.on('bpmStable', (data) => { 152 | bpm.value = data.bpm[0]?.tempo || 0; 153 | }); 154 | 155 | isAnalyzing.value = true; 156 | }; 157 | 158 | const stop = async () => { 159 | analyzer?.disconnect(); 160 | await audioContext?.close(); 161 | isAnalyzing.value = false; 162 | }; 163 | 164 | onUnmounted(() => stop()); 165 | 166 | return { bpm, isAnalyzing, start, stop }; 167 | } 168 | ``` 169 | 170 | ## TypeScript Support 171 | 172 | The library is fully typed. Import types as needed: 173 | 174 | ```typescript 175 | import type { BpmAnalyzer, BpmCandidates } from 'realtime-bpm-analyzer'; 176 | ``` 177 | 178 | ## Next Steps 179 | 180 | - Explore the [live examples above](#live-examples) to see complete implementations 181 | - Check out [React Integration](/examples/react) for React 182 | - Read the [API Documentation](/api/) for detailed reference 183 | -------------------------------------------------------------------------------- /examples/03-vanilla-microphone/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createRealtimeBpmAnalyzer, type BpmAnalyzer, type BpmCandidates } from 'realtime-bpm-analyzer'; 2 | 3 | // Get DOM elements 4 | const startBtn = document.getElementById('startBtn') as HTMLButtonElement; 5 | const stopBtn = document.getElementById('stopBtn') as HTMLButtonElement; 6 | const statusElement = document.getElementById('status') as HTMLDivElement; 7 | const bpmDisplay = document.getElementById('bpmDisplay') as HTMLDivElement; 8 | const bpmValue = document.getElementById('bpmValue') as HTMLDivElement; 9 | 10 | // State 11 | let audioContext: AudioContext | null = null; 12 | let mediaStream: MediaStream | null = null; 13 | let source: MediaStreamAudioSourceNode | null = null; 14 | let bpmAnalyzer: BpmAnalyzer | null = null; 15 | let analyser: AnalyserNode | null = null; 16 | 17 | // Start listening to microphone 18 | startBtn.addEventListener('click', async () => { 19 | try { 20 | showStatus('Starting microphone...', 'analyzing'); 21 | 22 | // Create audio context 23 | const audioCtx = audioContext ?? new AudioContext(); 24 | audioContext = audioCtx; 25 | 26 | // Resume audio context if suspended 27 | if (audioCtx.state === 'suspended') { 28 | await audioCtx.resume(); 29 | } 30 | 31 | // Request microphone access 32 | mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true }); 33 | 34 | // Handle the stream 35 | await handleStream(audioCtx, mediaStream); 36 | 37 | // Update UI 38 | startBtn.disabled = true; 39 | stopBtn.disabled = false; 40 | showStatus('Listening for music - play something!', 'success'); 41 | } catch (error) { 42 | console.error('Error accessing microphone:', error); 43 | 44 | let errorMessage = 'Failed to access microphone'; 45 | if (error instanceof Error) { 46 | if (error.name === 'NotAllowedError') { 47 | errorMessage = 'Microphone access denied. Please allow microphone access.'; 48 | } else if (error.name === 'NotFoundError') { 49 | errorMessage = 'No microphone found. Please connect a microphone.'; 50 | } else { 51 | errorMessage = error.message; 52 | } 53 | } 54 | 55 | showStatus(errorMessage, 'error'); 56 | cleanup(); 57 | } 58 | }); 59 | 60 | async function handleStream(audioCtx: AudioContext, stream: MediaStream) { 61 | // Resume audio context 62 | await audioCtx.resume(); 63 | 64 | // Create BPM analyzer 65 | const analyzer = await createRealtimeBpmAnalyzer(audioCtx); 66 | bpmAnalyzer = analyzer; 67 | 68 | // Create analyser node for visualization (optional, but following the pattern) 69 | const analyserNode = audioCtx.createAnalyser(); 70 | analyserNode.fftSize = 2048; 71 | analyser = analyserNode; 72 | 73 | // Create media stream source 74 | const mediaStreamSource = audioCtx.createMediaStreamSource(stream); 75 | source = mediaStreamSource; 76 | 77 | // Connect everything together 78 | // CRITICAL: BPM analyzer MUST be in the audio graph! 79 | mediaStreamSource.connect(analyserNode); 80 | mediaStreamSource.connect(analyzer.node); 81 | 82 | // Setup event listeners for BPM detection 83 | analyzer.on('bpmStable', onBpmStable); 84 | } 85 | 86 | function onBpmStable(data: BpmCandidates) { 87 | if (data.bpm.length > 0) { 88 | const primaryBpm = data.bpm[0].tempo; 89 | displayBpm(primaryBpm); 90 | showStatus(`Stable BPM detected: ${Math.round(primaryBpm)}`, 'success'); 91 | } 92 | } 93 | 94 | // Stop listening 95 | stopBtn.addEventListener('click', () => { 96 | disconnect(); 97 | showStatus('Stopped listening', 'analyzing'); 98 | hideBpm(); 99 | }); 100 | 101 | // Helper functions 102 | function showStatus(message: string, type: 'analyzing' | 'success' | 'error') { 103 | statusElement.textContent = message; 104 | statusElement.className = `status visible ${type}`; 105 | } 106 | 107 | function displayBpm(bpm: number) { 108 | bpmValue.textContent = Math.round(bpm).toString(); 109 | bpmDisplay.classList.add('visible'); 110 | } 111 | 112 | function hideBpm() { 113 | bpmDisplay.classList.remove('visible'); 114 | bpmValue.textContent = '--'; 115 | } 116 | 117 | async function disconnect(): Promise { 118 | if (!audioContext || !source || !bpmAnalyzer || !analyser) { 119 | return; 120 | } 121 | 122 | await audioContext.suspend(); 123 | 124 | // Disconnect everything 125 | source.disconnect(); 126 | analyser.disconnect(); 127 | bpmAnalyzer.disconnect(); 128 | 129 | // Reset UI 130 | startBtn.disabled = false; 131 | stopBtn.disabled = true; 132 | } 133 | 134 | function cleanup() { 135 | if (source) { 136 | source.disconnect(); 137 | source = null; 138 | } 139 | 140 | if (analyser) { 141 | analyser.disconnect(); 142 | analyser = null; 143 | } 144 | 145 | if (bpmAnalyzer) { 146 | bpmAnalyzer.disconnect(); 147 | bpmAnalyzer = null; 148 | } 149 | 150 | if (mediaStream) { 151 | mediaStream.getTracks().forEach(track => track.stop()); 152 | mediaStream = null; 153 | } 154 | 155 | startBtn.disabled = false; 156 | stopBtn.disabled = true; 157 | } 158 | 159 | // Cleanup on page unload 160 | window.addEventListener('beforeunload', cleanup); 161 | -------------------------------------------------------------------------------- /examples/06-react-microphone/src/components/bpm-analyzer.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react'; 2 | import { 3 | createRealtimeBpmAnalyzer, 4 | type BpmAnalyzer as BpmAnalyzerType, 5 | type BpmCandidates 6 | } from 'realtime-bpm-analyzer'; 7 | import './bpm-analyzer.css'; 8 | 9 | function BpmAnalyzer() { 10 | const [bpm, setBpm] = useState(); 11 | const [isRecording, setIsRecording] = useState(false); 12 | const [error, setError] = useState(); 13 | 14 | const audioContextRef = useRef(null); 15 | const mediaStreamRef = useRef(null); 16 | const sourceRef = useRef(null); 17 | const bpmAnalyzerRef = useRef(null); 18 | const analyserRef = useRef(null); 19 | 20 | // Initialize audio context 21 | useEffect(() => { 22 | audioContextRef.current = new AudioContext(); 23 | 24 | return () => { 25 | disconnect().catch((error) => { 26 | console.error('Error during cleanup on unmount', error); 27 | }); 28 | audioContextRef.current?.close(); 29 | }; 30 | }, []); 31 | 32 | const disconnect = async () => { 33 | if (!audioContextRef.current || !sourceRef.current || !bpmAnalyzerRef.current || !analyserRef.current) { 34 | return; 35 | } 36 | 37 | await audioContextRef.current.suspend(); 38 | 39 | sourceRef.current.disconnect(); 40 | analyserRef.current.disconnect(); 41 | bpmAnalyzerRef.current.disconnect(); 42 | 43 | if (mediaStreamRef.current) { 44 | mediaStreamRef.current.getTracks().forEach(track => track.stop()); 45 | mediaStreamRef.current = null; 46 | } 47 | 48 | setIsRecording(false); 49 | setBpm(undefined); 50 | }; 51 | 52 | const handleStream = async (audioCtx: AudioContext, stream: MediaStream) => { 53 | await audioCtx.resume(); 54 | 55 | // Create BPM analyzer 56 | const analyzer = await createRealtimeBpmAnalyzer(audioCtx); 57 | bpmAnalyzerRef.current = analyzer; 58 | 59 | // Create analyser node 60 | const analyserNode = audioCtx.createAnalyser(); 61 | analyserNode.fftSize = 2048; 62 | analyserRef.current = analyserNode; 63 | 64 | // Create media stream source 65 | const mediaStreamSource = audioCtx.createMediaStreamSource(stream); 66 | sourceRef.current = mediaStreamSource; 67 | 68 | // Connect everything together 69 | mediaStreamSource.connect(analyserNode); 70 | mediaStreamSource.connect(analyzer.node); 71 | 72 | // Setup event listeners 73 | analyzer.on('bpmStable', (data: BpmCandidates) => { 74 | if (data.bpm.length > 0) { 75 | setBpm(data.bpm[0].tempo); 76 | } 77 | }); 78 | }; 79 | 80 | const handleStart = async () => { 81 | try { 82 | setError(undefined); 83 | 84 | const audioCtx = audioContextRef.current; 85 | if (!audioCtx) throw new Error('Audio context not initialized'); 86 | 87 | if (audioCtx.state === 'suspended') { 88 | await audioCtx.resume(); 89 | } 90 | 91 | // Request microphone access 92 | const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); 93 | mediaStreamRef.current = stream; 94 | 95 | await handleStream(audioCtx, stream); 96 | 97 | setIsRecording(true); 98 | } catch (err) { 99 | console.error('Error accessing microphone:', err); 100 | 101 | let errorMessage = 'Failed to access microphone'; 102 | if (err instanceof Error) { 103 | if (err.name === 'NotAllowedError') { 104 | errorMessage = 'Microphone access denied. Please allow microphone access.'; 105 | } else if (err.name === 'NotFoundError') { 106 | errorMessage = 'No microphone found. Please connect a microphone.'; 107 | } else { 108 | errorMessage = err.message; 109 | } 110 | } 111 | 112 | setError(errorMessage); 113 | } 114 | }; 115 | 116 | const handleStop = () => { 117 | disconnect(); 118 | }; 119 | 120 | return ( 121 |
122 |
123 | 130 | 136 |
137 | 138 | {error && ( 139 |
140 | {error} 141 |
142 | )} 143 | 144 | {isRecording && !error && bpm !== undefined && ( 145 |
146 | Stable BPM detected: {Math.round(bpm)} 147 |
148 | )} 149 | 150 | {isRecording && !error && bpm === undefined && ( 151 |
152 | Listening for music - play something! 153 |
154 | )} 155 | 156 |
157 |
{bpm !== undefined ? Math.round(bpm) : '--'}
158 |
BPM
159 |
160 |
161 | ); 162 | } 163 | 164 | export default BpmAnalyzer; 165 | -------------------------------------------------------------------------------- /docs/.vitepress/components/ExampleEmbed.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 |