├── .DS_Store ├── .gitignore ├── LICENSE ├── README.md ├── bin └── react-sounds-cli.js ├── package-lock.json ├── package.json ├── public └── sounds │ └── silence.mp3 ├── rollup.config.js ├── scripts ├── generate-manifest.js ├── generate-types.js └── upload-to-cdn.js ├── sounds ├── .DS_Store ├── ambient │ ├── .DS_Store │ ├── campfire.mp3 │ ├── heartbeat.mp3 │ ├── rain.mp3 │ ├── water_stream.mp3 │ └── wind.mp3 ├── arcade │ ├── .DS_Store │ ├── coin.mp3 │ ├── coin_bling.mp3 │ ├── jump.mp3 │ ├── level_down.mp3 │ ├── level_up.mp3 │ ├── power_down.mp3 │ ├── power_up.mp3 │ └── upgrade.mp3 ├── game │ ├── .DS_Store │ ├── coin.mp3 │ ├── hit.mp3 │ ├── miss.mp3 │ ├── portal_closing.mp3 │ ├── portal_opening.mp3 │ └── void.mp3 ├── misc │ ├── .DS_Store │ └── silence.mp3 ├── notification │ ├── .DS_Store │ ├── completed.mp3 │ ├── error.mp3 │ ├── info.mp3 │ ├── message.mp3 │ ├── notification.mp3 │ ├── popup.mp3 │ ├── reminder.mp3 │ ├── success.mp3 │ └── warning.mp3 ├── system │ ├── .DS_Store │ ├── boot_down.mp3 │ ├── boot_up.mp3 │ ├── device_connect.mp3 │ ├── device_disconnect.mp3 │ ├── lock.mp3 │ ├── screenshot.mp3 │ └── trash.mp3 └── ui │ ├── .DS_Store │ ├── blocked.mp3 │ ├── button_hard.mp3 │ ├── button_hard_double.mp3 │ ├── button_medium.mp3 │ ├── button_soft.mp3 │ ├── button_soft_double.mp3 │ ├── button_squishy.mp3 │ ├── buzz.mp3 │ ├── buzz_deep.mp3 │ ├── buzz_long.mp3 │ ├── copy.mp3 │ ├── input_blur.mp3 │ ├── input_focus.mp3 │ ├── item_deselect.mp3 │ ├── item_select.mp3 │ ├── keystroke_hard.mp3 │ ├── keystroke_medium.mp3 │ ├── keystroke_soft.mp3 │ ├── panel_collapse.mp3 │ ├── panel_expand.mp3 │ ├── pop_close.mp3 │ ├── pop_open.mp3 │ ├── popup_close.mp3 │ ├── popup_open.mp3 │ ├── radio_select.mp3 │ ├── send.mp3 │ ├── submit.mp3 │ ├── success_bling.mp3 │ ├── success_blip.mp3 │ ├── success_chime.mp3 │ ├── tab_close.mp3 │ ├── tab_open.mp3 │ ├── toggle_off.mp3 │ ├── toggle_on.mp3 │ ├── window_close.mp3 │ └── window_open.mp3 ├── src ├── components.tsx ├── hooks.ts ├── index.ts ├── manifest.json ├── runtime.ts ├── types.ts └── utils.ts ├── tsconfig.json └── website ├── .gitignore ├── README.md ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── public └── favicon.svg ├── src ├── App.css ├── App.tsx ├── assets │ ├── music.mp3 │ └── react.svg ├── components │ ├── AdvancedSoundDemo.tsx │ ├── CodeBlock.tsx │ ├── FeatureCard.tsx │ ├── Footer.tsx │ ├── Header.tsx │ └── SoundSelector.tsx ├── declarations.d.ts ├── index.css ├── main.tsx ├── manifest.json ├── pages │ ├── DocumentationPage.tsx │ ├── HomePage.tsx │ └── SoundLibraryPage.tsx └── utils │ └── cn.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # vitepress build output 108 | **/.vitepress/dist 109 | 110 | # vitepress cache directory 111 | **/.vitepress/cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | 138 | # custom 139 | .cursor/ 140 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Aedilic Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-sounds 🔊 2 | 3 |

4 | npm version 5 | License: MIT 6 | PRs Welcome 7 |

8 | 9 |

10 | Hundreds of ready-to-play sound effects for your React applications
11 | Add delight to your UI with just a few lines of code 12 |

13 | 14 |

15 | Demo • 16 | Documentation • 17 | Sound Explorer 18 |

19 | 20 | ## ✨ Why react-sounds? 21 | 22 | - 🪶 **Lightweight**: Only loads JS wrappers, audio files stay on CDN until needed 23 | - 🔄 **Lazy Loading**: Sounds are fetched only when they're used 24 | - 📦 **Offline Support**: Download sounds for self-hosting with the included CLI 25 | - 🎯 **Simple API**: Intuitive hooks and components 26 | - 🔊 **Extensive Library**: Hundreds of categorized sounds (UI, notification, game) 27 | 28 | ## 🚀 Quick Start 29 | 30 | ```bash 31 | npm install react-sounds howler 32 | # or 33 | yarn add react-sounds howler 34 | ``` 35 | 36 | ```tsx 37 | import { useSound } from 'react-sounds'; 38 | 39 | function Button() { 40 | const { play } = useSound('ui/button_1'); 41 | 42 | return ( 43 | 46 | ); 47 | } 48 | ``` 49 | 50 | ## 📚 Documentation 51 | 52 | For complete documentation including advanced usage, visit [reactsounds.com/docs](https://www.reactsounds.com/docs) 53 | 54 | ## 🎮 Live Demo 55 | 56 | Try the interactive demo at [reactsounds.com](https://www.reactsounds.com) 57 | 58 | ## 🔍 Explore All Sounds 59 | 60 | Browse and play all available sounds at [reactsounds.com/sounds](https://www.reactsounds.com/sounds) 61 | 62 | ## 💻 Browser Support 63 | 64 | Works in all modern browsers that support the Web Audio API (Chrome, Firefox, Safari, Edge) 65 | 66 | ## 📄 License 67 | 68 | MIT © Aedilic Inc. 69 | 70 | --- 71 | 72 |

73 | Made with ♥ by Aedilic Inc 74 |

75 | -------------------------------------------------------------------------------- /bin/react-sounds-cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const https = require("https"); 6 | 7 | // Load the manifest 8 | const manifestPath = path.join(__dirname, "../dist/manifest.json"); 9 | let manifest; 10 | 11 | try { 12 | manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); 13 | } catch (err) { 14 | console.error("Error loading manifest:", err.message); 15 | process.exit(1); 16 | } 17 | 18 | // Get the CDN base URL 19 | const cdnBaseUrl = process.env.REACT_SOUNDS_CDN || "https://reacticons.sfo3.cdn.digitaloceanspaces.com/v1"; 20 | 21 | // Parse command line arguments 22 | const args = process.argv.slice(2); 23 | let command = args[0]; 24 | let soundNames = []; 25 | let outputDir = "./public/sounds"; 26 | 27 | if (command === "pick") { 28 | soundNames = args.slice(1).filter((arg) => !arg.startsWith("--")); 29 | 30 | // Check for output directory option 31 | const outputOption = args.find((arg) => arg.startsWith("--output=")); 32 | if (outputOption) { 33 | outputDir = outputOption.split("=")[1]; 34 | } else { 35 | const outputIndex = args.indexOf("--output"); 36 | if (outputIndex !== -1 && args[outputIndex + 1]) { 37 | outputDir = args[outputIndex + 1]; 38 | } 39 | } 40 | 41 | if (soundNames.length === 0) { 42 | console.error("Please specify at least one sound to pick."); 43 | console.log("Usage: npx react-sounds-cli pick [--output=]"); 44 | process.exit(1); 45 | } 46 | 47 | // Create output directory 48 | try { 49 | fs.mkdirSync(outputDir, { recursive: true }); 50 | 51 | // Create category subdirectories 52 | const categories = ["ui", "notification", "game"]; 53 | for (const category of categories) { 54 | fs.mkdirSync(path.join(outputDir, category), { recursive: true }); 55 | } 56 | } catch (err) { 57 | console.error(`Error creating directory ${outputDir}:`, err.message); 58 | process.exit(1); 59 | } 60 | 61 | console.log(`\n📦 Downloading ${soundNames.length} sounds to ${outputDir}...`); 62 | 63 | // Download each sound 64 | let successCount = 0; 65 | let failCount = 0; 66 | 67 | const downloadPromises = soundNames.map((name) => { 68 | return new Promise((resolve) => { 69 | // Check if sound exists in manifest 70 | if (!manifest.sounds[name]) { 71 | console.error(`❌ Sound "${name}" not found in manifest.`); 72 | failCount++; 73 | resolve(); 74 | return; 75 | } 76 | 77 | const soundInfo = manifest.sounds[name]; 78 | const cdnPath = soundInfo.src; 79 | const targetPath = path.join(outputDir, name + ".mp3"); 80 | 81 | // Ensure target directory exists 82 | const targetDir = path.dirname(targetPath); 83 | fs.mkdirSync(targetDir, { recursive: true }); 84 | 85 | // Download the file 86 | const file = fs.createWriteStream(targetPath); 87 | const url = `${cdnBaseUrl}/${cdnPath}`; 88 | 89 | https 90 | .get(url, (response) => { 91 | if (response.statusCode !== 200) { 92 | console.error(`❌ Failed to download "${name}": HTTP ${response.statusCode}`); 93 | failCount++; 94 | resolve(); 95 | return; 96 | } 97 | 98 | response.pipe(file); 99 | 100 | file.on("finish", () => { 101 | file.close(); 102 | console.log(`✅ Downloaded: ${name}`); 103 | successCount++; 104 | resolve(); 105 | }); 106 | }) 107 | .on("error", (err) => { 108 | fs.unlink(targetPath, () => {}); // Clean up on error 109 | console.error(`❌ Failed to download "${name}":`, err.message); 110 | failCount++; 111 | resolve(); 112 | }); 113 | }); 114 | }); 115 | 116 | Promise.all(downloadPromises).then(() => { 117 | console.log("\n📊 Summary:"); 118 | console.log(`✅ ${successCount} sounds downloaded successfully`); 119 | 120 | if (failCount > 0) { 121 | console.log(`❌ ${failCount} sounds failed to download`); 122 | } 123 | 124 | console.log("\n🔧 Sounds are now available for offline use!"); 125 | console.log("To use them in your project, set the CDN base URL:"); 126 | console.log("\nimport { setCDNUrl } from 'react-sounds';"); 127 | console.log(`setCDNUrl('${path.relative(process.cwd(), outputDir)}');\n`); 128 | }); 129 | } else if (command === "list") { 130 | // List all available sounds 131 | console.log("\n🔊 Available sounds:\n"); 132 | 133 | const categories = {}; 134 | 135 | // Group sounds by category 136 | for (const name in manifest.sounds) { 137 | const category = name.split("/")[0]; 138 | if (!categories[category]) { 139 | categories[category] = []; 140 | } 141 | categories[category].push(name); 142 | } 143 | 144 | // Print grouped sounds 145 | for (const category in categories) { 146 | console.log(`📁 ${category}:`); 147 | for (const name of categories[category]) { 148 | console.log(` - ${name}`); 149 | } 150 | console.log(""); 151 | } 152 | } else { 153 | // Show help 154 | console.log("\n🔊 react-sounds-cli"); 155 | console.log("A CLI for managing sounds in the react-sounds library.\n"); 156 | console.log("Commands:"); 157 | console.log(" pick [--output=] Download sounds for offline use"); 158 | console.log(" list List all available sounds\n"); 159 | console.log("Examples:"); 160 | console.log(" npx react-sounds-cli pick ui/click ui/hover notification/success"); 161 | console.log(" npx react-sounds-cli pick ui/click --output=./public/sounds"); 162 | console.log(" npx react-sounds-cli list\n"); 163 | } 164 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-sounds", 3 | "version": "1.0.25", 4 | "description": "A library of ready-to-play sound effects for React applications.", 5 | "main": "dist/index.js", 6 | "module": "dist/index.esm.js", 7 | "types": "dist/index.d.ts", 8 | "sideEffects": false, 9 | "scripts": { 10 | "build": "rm -rf dist/ && rollup -c && cp src/manifest.json dist/", 11 | "build:sounds": "npm run generate-manifest && npm run generate-types && npm run upload-to-cdn", 12 | "build:all": "npm run build:sounds && npm run build", 13 | "lint": "eslint src", 14 | "generate-manifest": "node scripts/generate-manifest.js", 15 | "upload-to-cdn": "node scripts/upload-to-cdn.js", 16 | "generate-types": "node scripts/generate-types.js" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "sounds", 21 | "audio", 22 | "sound-effects", 23 | "ui-sounds" 24 | ], 25 | "author": "Lukas Schneider ", 26 | "license": "MIT", 27 | "peerDependencies": { 28 | "howler": "^2.2.3", 29 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" 30 | }, 31 | "devDependencies": { 32 | "@aws-sdk/client-s3": "^3.797.0", 33 | "@rollup/plugin-commonjs": "^22.0.0", 34 | "@rollup/plugin-json": "^6.1.0", 35 | "@rollup/plugin-node-resolve": "^13.3.0", 36 | "@rollup/plugin-typescript": "^8.3.2", 37 | "@types/howler": "^2.2.7", 38 | "@types/node": "^18.0.0", 39 | "@types/react": "^18.0.14", 40 | "aws-cli": "^0.0.2", 41 | "dotenv": "^16.5.0", 42 | "eslint": "^8.18.0", 43 | "rollup": "^2.75.6", 44 | "rollup-plugin-terser": "^7.0.2", 45 | "tslib": "^2.8.1", 46 | "typescript": "^4.7.4", 47 | "yargs": "^17.7.2" 48 | }, 49 | "files": [ 50 | "dist", 51 | "bin" 52 | ], 53 | "bin": { 54 | "react-sounds-cli": "./bin/react-sounds-cli.js" 55 | }, 56 | "repository": { 57 | "type": "git", 58 | "url": "https://github.com/aediliclabs/react-sounds.git" 59 | }, 60 | "bugs": { 61 | "url": "https://github.com/aediliclabs/react-sounds/issues" 62 | }, 63 | "homepage": "https://reactsounds.com", 64 | "dependencies": { 65 | "howler": "^2.2.3" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /public/sounds/silence.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/public/sounds/silence.mp3 -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from "@rollup/plugin-commonjs"; 2 | import json from "@rollup/plugin-json"; 3 | import resolve from "@rollup/plugin-node-resolve"; 4 | import typescript from "@rollup/plugin-typescript"; 5 | import { terser } from "rollup-plugin-terser"; 6 | 7 | const packageJson = require("./package.json"); 8 | 9 | export default { 10 | input: "src/index.ts", 11 | output: [ 12 | { 13 | file: packageJson.main, 14 | format: "cjs", 15 | sourcemap: true, 16 | exports: "named", 17 | }, 18 | { 19 | file: packageJson.module, 20 | format: "esm", 21 | sourcemap: true, 22 | exports: "named", 23 | }, 24 | ], 25 | plugins: [ 26 | resolve(), 27 | commonjs(), 28 | json(), 29 | typescript({ 30 | tsconfig: "./tsconfig.json", 31 | exclude: ["**/__tests__/**", "**/*.test.ts", "**/*.test.tsx"], 32 | }), 33 | terser(), 34 | ], 35 | external: ["react", "react-dom", "howler"], 36 | }; 37 | -------------------------------------------------------------------------------- /scripts/generate-manifest.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This script generates a manifest.json file by scanning the sounds directory 5 | * and collecting metadata about each sound file. 6 | */ 7 | 8 | const fs = require("fs"); 9 | const path = require("path"); 10 | const crypto = require("crypto"); 11 | const { execSync } = require("child_process"); 12 | 13 | // Configuration 14 | const SOUNDS_DIR = path.resolve(__dirname, "../sounds"); 15 | const OUTPUT_DIR = path.resolve(__dirname, "../src"); 16 | const MANIFEST_FILE = path.join(OUTPUT_DIR, "manifest.json"); 17 | 18 | // Create output directory if it doesn't exist 19 | if (!fs.existsSync(OUTPUT_DIR)) { 20 | fs.mkdirSync(OUTPUT_DIR, { recursive: true }); 21 | } 22 | 23 | // Generate a hash for a file 24 | function generateFileHash(filePath) { 25 | const fileBuffer = fs.readFileSync(filePath); 26 | const hashSum = crypto.createHash("md5"); 27 | hashSum.update(fileBuffer); 28 | return hashSum.digest("hex").substring(0, 7); 29 | } 30 | 31 | // Get all valid category directories 32 | function getCategories() { 33 | const items = fs.readdirSync(SOUNDS_DIR, { withFileTypes: true }); 34 | return items.filter((item) => item.isDirectory() && item.name !== ".DS_Store").map((item) => item.name); 35 | } 36 | 37 | // Main function 38 | async function generateManifest() { 39 | console.log("Generating manifest..."); 40 | 41 | const manifest = { 42 | version: "1.0.0", 43 | sounds: {}, 44 | }; 45 | 46 | // Get all categories from the sounds directory 47 | const categories = getCategories(); 48 | console.log(`Found categories: ${categories.join(", ")}`); 49 | 50 | // Iterate through all categories and sound files 51 | for (const category of categories) { 52 | const categoryPath = path.join(SOUNDS_DIR, category); 53 | 54 | const files = fs.readdirSync(categoryPath); 55 | 56 | for (const file of files) { 57 | // Only process MP3 files 58 | if (!file.endsWith(".mp3")) continue; 59 | 60 | const filePath = path.join(categoryPath, file); 61 | const soundName = file.replace(".mp3", ""); 62 | const soundId = `${category}/${soundName}`; 63 | 64 | // Generate hash for the file 65 | const hash = generateFileHash(filePath); 66 | 67 | // Get file metadata (duration, etc.) 68 | let duration = 0; 69 | 70 | try { 71 | // Try to get audio duration using ffprobe if available 72 | const result = execSync(`ffprobe -i "${filePath}" -show_entries format=duration -v quiet -of csv="p=0"`, { 73 | encoding: "utf8", 74 | }); 75 | duration = parseFloat(result.trim()); 76 | } catch (e) { 77 | console.warn(`Could not get duration for ${filePath}. Setting default duration.`); 78 | // Set a default duration based on file size 79 | const stats = fs.statSync(filePath); 80 | duration = Math.round((stats.size / 16000) * 10) / 10; // Rough estimate 81 | } 82 | 83 | // Add to manifest 84 | manifest.sounds[soundId] = { 85 | src: `${category}/${soundName}.${hash}.mp3`, 86 | duration: duration, 87 | }; 88 | 89 | console.log(`Added ${soundId} to manifest`); 90 | } 91 | } 92 | 93 | // Save manifest 94 | fs.writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2)); 95 | console.log(`Manifest saved to ${MANIFEST_FILE}`); 96 | } 97 | 98 | // Run the script 99 | generateManifest().catch((err) => { 100 | console.error("Error generating manifest:", err); 101 | process.exit(1); 102 | }); 103 | -------------------------------------------------------------------------------- /scripts/generate-types.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This script generates TypeScript type definitions for sound names 5 | * based on the manifest.json file. 6 | */ 7 | 8 | const fs = require("fs"); 9 | const path = require("path"); 10 | 11 | // Configuration 12 | const MANIFEST_FILE = path.resolve(__dirname, "../src/manifest.json"); 13 | const TYPES_FILE = path.resolve(__dirname, "../src/types.ts"); 14 | 15 | // Main function 16 | async function generateTypes() { 17 | console.log("Generating TypeScript types..."); 18 | 19 | // Load manifest 20 | if (!fs.existsSync(MANIFEST_FILE)) { 21 | console.error(`Manifest file not found: ${MANIFEST_FILE}`); 22 | process.exit(1); 23 | } 24 | 25 | const manifest = JSON.parse(fs.readFileSync(MANIFEST_FILE, "utf8")); 26 | 27 | // Group sounds by category 28 | const soundsByCategory = {}; 29 | 30 | for (const soundId in manifest.sounds) { 31 | const [category, name] = soundId.split("/"); 32 | 33 | if (!soundsByCategory[category]) { 34 | soundsByCategory[category] = []; 35 | } 36 | 37 | soundsByCategory[category].push(name); 38 | } 39 | 40 | // Generate TypeScript content 41 | let content = ` 42 | /** 43 | * Categories of sounds available in the library 44 | */ 45 | export type SoundCategory = ${Object.keys(soundsByCategory) 46 | .map((cat) => `'${cat}'`) 47 | .join(" | ")}; 48 | 49 | `; 50 | 51 | // Generate type definitions for each category 52 | for (const category in soundsByCategory) { 53 | const soundNames = soundsByCategory[category]; 54 | const categoryTypeName = `${category.charAt(0).toUpperCase()}${category.slice(1)}SoundName`; 55 | 56 | content += `/** 57 | * ${categoryTypeName} sound names 58 | */ 59 | export type ${categoryTypeName} = ${soundNames.map((name) => `'${name}'`).join(" | ")}; 60 | 61 | `; 62 | } 63 | 64 | // Generate SoundName type 65 | content += `/** 66 | * All available sound names 67 | */ 68 | export type LibrarySoundName = 69 | ${Object.entries(soundsByCategory) 70 | .map(([category, _]) => { 71 | const categoryTypeName = `${category.charAt(0).toUpperCase()}${category.slice(1)}SoundName`; 72 | return `| \`${category}/\${${categoryTypeName}}\``; 73 | }) 74 | .join("\n ")}; 75 | 76 | /** 77 | * Sound options for playback 78 | */ 79 | export interface SoundOptions { 80 | /** 81 | * Volume of the sound (0.0 to 1.0) 82 | */ 83 | volume?: number; 84 | 85 | /** 86 | * Playback rate (1.0 is normal speed) 87 | */ 88 | rate?: number; 89 | 90 | /** 91 | * Sound should loop 92 | */ 93 | loop?: boolean; 94 | } 95 | 96 | /** 97 | * Return type for useSound hook 98 | */ 99 | export interface SoundHookReturn { 100 | /** 101 | * Play the sound with optional options 102 | */ 103 | play: (options?: SoundOptions) => Promise; 104 | 105 | /** 106 | * Stop the sound 107 | */ 108 | stop: () => void; 109 | 110 | /** 111 | * Pause the sound 112 | */ 113 | pause: () => void; 114 | 115 | /** 116 | * Resume the sound 117 | */ 118 | resume: () => void; 119 | 120 | /** 121 | * Check if the sound is currently playing 122 | */ 123 | isPlaying: boolean; 124 | 125 | /** 126 | * Check if the sound is loaded 127 | */ 128 | isLoaded: boolean; 129 | }`; 130 | 131 | // Save the file 132 | fs.writeFileSync(TYPES_FILE, content); 133 | console.log(`TypeScript types saved to ${TYPES_FILE}`); 134 | } 135 | 136 | // Run the script 137 | generateTypes().catch((err) => { 138 | console.error("Error generating TypeScript types:", err); 139 | process.exit(1); 140 | }); 141 | -------------------------------------------------------------------------------- /scripts/upload-to-cdn.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This script uploads sound files to DigitalOcean Spaces 5 | * using the manifest to determine filenames. 6 | * 7 | * Environment variables: 8 | * - DO_SPACES_KEY: DigitalOcean Spaces access key 9 | * - DO_SPACES_SECRET: DigitalOcean Spaces secret key 10 | * - DO_SPACES_ENDPOINT: DigitalOcean Spaces endpoint (e.g., nyc3.digitaloceanspaces.com) 11 | * - DO_SPACES_NAME: DigitalOcean Space name 12 | * - CDN_PATH: Optional path prefix (defaults to "v1") 13 | * 14 | * Command line arguments: 15 | * --delete: Delete existing files before uploading 16 | */ 17 | 18 | require("dotenv").config(); 19 | 20 | const fs = require("fs"); 21 | const path = require("path"); 22 | const yargs = require("yargs/yargs"); 23 | const { hideBin } = require("yargs/helpers"); 24 | const { S3Client, PutObjectCommand, ListObjectsV2Command, DeleteObjectCommand } = require("@aws-sdk/client-s3"); 25 | 26 | // Parse command line arguments 27 | const argv = yargs(hideBin(process.argv)) 28 | .option("delete", { 29 | alias: "d", 30 | type: "boolean", 31 | description: "Delete existing files before uploading", 32 | default: false, 33 | }) 34 | .help().argv; 35 | 36 | // Configuration 37 | const SOUNDS_DIR = path.resolve(__dirname, "../sounds"); 38 | const MANIFEST_FILE = path.resolve(__dirname, "../src/manifest.json"); 39 | const CDN_PATH = process.env.CDN_PATH || "v1"; 40 | 41 | // Check if manifest exists 42 | if (!fs.existsSync(MANIFEST_FILE)) { 43 | console.error(`Manifest file not found: ${MANIFEST_FILE}`); 44 | console.error(`Please run 'npm run generate-manifest' first.`); 45 | process.exit(1); 46 | } 47 | 48 | // Check if required environment variables are set 49 | const requiredEnvVars = ["DO_SPACES_KEY", "DO_SPACES_SECRET", "DO_SPACES_ENDPOINT", "DO_SPACES_NAME"]; 50 | const missingEnvVars = requiredEnvVars.filter((envVar) => !process.env[envVar]); 51 | 52 | if (missingEnvVars.length > 0) { 53 | console.error(`Missing required environment variables: ${missingEnvVars.join(", ")}`); 54 | process.exit(1); 55 | } 56 | 57 | // Create an S3 client for DigitalOcean Spaces 58 | const s3Client = new S3Client({ 59 | credentials: { 60 | accessKeyId: process.env.DO_SPACES_KEY, 61 | secretAccessKey: process.env.DO_SPACES_SECRET, 62 | }, 63 | endpoint: `https://${process.env.DO_SPACES_ENDPOINT}`, 64 | region: "us-east-1", // This is required but not used for DO Spaces 65 | forcePathStyle: false, 66 | }); 67 | 68 | // Main function 69 | async function uploadToSpaces() { 70 | const spaceName = process.env.DO_SPACES_NAME; 71 | 72 | console.log(`Uploading sound files to DigitalOcean Spaces (${spaceName})...`); 73 | 74 | // Clear existing files in the CDN directory if --delete flag is provided 75 | if (argv.delete) { 76 | console.log(`Clearing existing files in ${CDN_PATH}...`); 77 | await clearDirectory(spaceName, CDN_PATH); 78 | } else { 79 | console.log(`Skipping file deletion (use --delete flag to clear existing files)`); 80 | } 81 | 82 | // Load manifest 83 | const manifest = JSON.parse(fs.readFileSync(MANIFEST_FILE, "utf8")); 84 | 85 | // Count for statistics 86 | let uploadCount = 0; 87 | let errorCount = 0; 88 | 89 | // Process each sound in the manifest 90 | for (const [soundId, soundInfo] of Object.entries(manifest.sounds)) { 91 | const [category, name] = soundId.split("/"); 92 | const sourceFile = path.join(SOUNDS_DIR, category, `${name}.mp3`); 93 | const cdnFilename = soundInfo.src; 94 | 95 | // Check if source file exists 96 | if (!fs.existsSync(sourceFile)) { 97 | console.error(`Source file not found: ${sourceFile}`); 98 | errorCount++; 99 | continue; 100 | } 101 | 102 | // Upload to DigitalOcean Spaces 103 | try { 104 | const spaceKey = `${CDN_PATH}/${cdnFilename}`; 105 | const fileContent = fs.readFileSync(sourceFile); 106 | 107 | const uploadParams = { 108 | Bucket: spaceName, 109 | Key: spaceKey, 110 | Body: fileContent, 111 | ContentType: "audio/mpeg", 112 | ACL: "public-read", // Make the file publicly accessible 113 | }; 114 | 115 | await s3Client.send(new PutObjectCommand(uploadParams)); 116 | 117 | console.log(`✅ Uploaded: ${soundId} -> ${cdnFilename}`); 118 | uploadCount++; 119 | } catch (error) { 120 | console.error(`❌ Failed to upload ${soundId}: ${error.message}`); 121 | errorCount++; 122 | } 123 | } 124 | 125 | console.log("\n📊 Upload Summary:"); 126 | console.log(`✅ ${uploadCount} sounds uploaded successfully`); 127 | 128 | if (errorCount > 0) { 129 | console.log(`❌ ${errorCount} sounds failed to upload`); 130 | process.exit(1); 131 | } 132 | } 133 | 134 | // Function to clear all objects in a directory 135 | async function clearDirectory(bucketName, directoryPath) { 136 | try { 137 | // List all objects in the directory 138 | const listParams = { 139 | Bucket: bucketName, 140 | Prefix: directoryPath, 141 | }; 142 | 143 | const listCommand = new ListObjectsV2Command(listParams); 144 | const listedObjects = await s3Client.send(listCommand); 145 | 146 | if (!listedObjects.Contents || listedObjects.Contents.length === 0) { 147 | console.log(`No existing objects found in ${directoryPath}`); 148 | return; 149 | } 150 | 151 | console.log(`Found ${listedObjects.Contents.length} objects to delete...`); 152 | 153 | // Delete each object 154 | let deletedCount = 0; 155 | for (const object of listedObjects.Contents) { 156 | const deleteParams = { 157 | Bucket: bucketName, 158 | Key: object.Key, 159 | }; 160 | 161 | await s3Client.send(new DeleteObjectCommand(deleteParams)); 162 | deletedCount++; 163 | 164 | if (deletedCount % 10 === 0 || deletedCount === listedObjects.Contents.length) { 165 | console.log(`Deleted ${deletedCount}/${listedObjects.Contents.length} objects...`); 166 | } 167 | } 168 | 169 | console.log(`Successfully cleared ${deletedCount} objects from ${directoryPath}`); 170 | } catch (error) { 171 | console.error(`Error clearing directory ${directoryPath}:`, error); 172 | throw error; 173 | } 174 | } 175 | 176 | // Run the script 177 | uploadToSpaces().catch((err) => { 178 | console.error("Error uploading to DigitalOcean Spaces:", err); 179 | process.exit(1); 180 | }); 181 | -------------------------------------------------------------------------------- /sounds/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/.DS_Store -------------------------------------------------------------------------------- /sounds/ambient/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ambient/.DS_Store -------------------------------------------------------------------------------- /sounds/ambient/campfire.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ambient/campfire.mp3 -------------------------------------------------------------------------------- /sounds/ambient/heartbeat.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ambient/heartbeat.mp3 -------------------------------------------------------------------------------- /sounds/ambient/rain.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ambient/rain.mp3 -------------------------------------------------------------------------------- /sounds/ambient/water_stream.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ambient/water_stream.mp3 -------------------------------------------------------------------------------- /sounds/ambient/wind.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ambient/wind.mp3 -------------------------------------------------------------------------------- /sounds/arcade/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/arcade/.DS_Store -------------------------------------------------------------------------------- /sounds/arcade/coin.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/arcade/coin.mp3 -------------------------------------------------------------------------------- /sounds/arcade/coin_bling.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/arcade/coin_bling.mp3 -------------------------------------------------------------------------------- /sounds/arcade/jump.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/arcade/jump.mp3 -------------------------------------------------------------------------------- /sounds/arcade/level_down.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/arcade/level_down.mp3 -------------------------------------------------------------------------------- /sounds/arcade/level_up.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/arcade/level_up.mp3 -------------------------------------------------------------------------------- /sounds/arcade/power_down.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/arcade/power_down.mp3 -------------------------------------------------------------------------------- /sounds/arcade/power_up.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/arcade/power_up.mp3 -------------------------------------------------------------------------------- /sounds/arcade/upgrade.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/arcade/upgrade.mp3 -------------------------------------------------------------------------------- /sounds/game/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/game/.DS_Store -------------------------------------------------------------------------------- /sounds/game/coin.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/game/coin.mp3 -------------------------------------------------------------------------------- /sounds/game/hit.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/game/hit.mp3 -------------------------------------------------------------------------------- /sounds/game/miss.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/game/miss.mp3 -------------------------------------------------------------------------------- /sounds/game/portal_closing.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/game/portal_closing.mp3 -------------------------------------------------------------------------------- /sounds/game/portal_opening.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/game/portal_opening.mp3 -------------------------------------------------------------------------------- /sounds/game/void.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/game/void.mp3 -------------------------------------------------------------------------------- /sounds/misc/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/misc/.DS_Store -------------------------------------------------------------------------------- /sounds/misc/silence.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/misc/silence.mp3 -------------------------------------------------------------------------------- /sounds/notification/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/notification/.DS_Store -------------------------------------------------------------------------------- /sounds/notification/completed.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/notification/completed.mp3 -------------------------------------------------------------------------------- /sounds/notification/error.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/notification/error.mp3 -------------------------------------------------------------------------------- /sounds/notification/info.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/notification/info.mp3 -------------------------------------------------------------------------------- /sounds/notification/message.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/notification/message.mp3 -------------------------------------------------------------------------------- /sounds/notification/notification.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/notification/notification.mp3 -------------------------------------------------------------------------------- /sounds/notification/popup.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/notification/popup.mp3 -------------------------------------------------------------------------------- /sounds/notification/reminder.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/notification/reminder.mp3 -------------------------------------------------------------------------------- /sounds/notification/success.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/notification/success.mp3 -------------------------------------------------------------------------------- /sounds/notification/warning.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/notification/warning.mp3 -------------------------------------------------------------------------------- /sounds/system/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/system/.DS_Store -------------------------------------------------------------------------------- /sounds/system/boot_down.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/system/boot_down.mp3 -------------------------------------------------------------------------------- /sounds/system/boot_up.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/system/boot_up.mp3 -------------------------------------------------------------------------------- /sounds/system/device_connect.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/system/device_connect.mp3 -------------------------------------------------------------------------------- /sounds/system/device_disconnect.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/system/device_disconnect.mp3 -------------------------------------------------------------------------------- /sounds/system/lock.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/system/lock.mp3 -------------------------------------------------------------------------------- /sounds/system/screenshot.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/system/screenshot.mp3 -------------------------------------------------------------------------------- /sounds/system/trash.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/system/trash.mp3 -------------------------------------------------------------------------------- /sounds/ui/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/.DS_Store -------------------------------------------------------------------------------- /sounds/ui/blocked.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/blocked.mp3 -------------------------------------------------------------------------------- /sounds/ui/button_hard.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/button_hard.mp3 -------------------------------------------------------------------------------- /sounds/ui/button_hard_double.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/button_hard_double.mp3 -------------------------------------------------------------------------------- /sounds/ui/button_medium.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/button_medium.mp3 -------------------------------------------------------------------------------- /sounds/ui/button_soft.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/button_soft.mp3 -------------------------------------------------------------------------------- /sounds/ui/button_soft_double.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/button_soft_double.mp3 -------------------------------------------------------------------------------- /sounds/ui/button_squishy.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/button_squishy.mp3 -------------------------------------------------------------------------------- /sounds/ui/buzz.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/buzz.mp3 -------------------------------------------------------------------------------- /sounds/ui/buzz_deep.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/buzz_deep.mp3 -------------------------------------------------------------------------------- /sounds/ui/buzz_long.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/buzz_long.mp3 -------------------------------------------------------------------------------- /sounds/ui/copy.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/copy.mp3 -------------------------------------------------------------------------------- /sounds/ui/input_blur.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/input_blur.mp3 -------------------------------------------------------------------------------- /sounds/ui/input_focus.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/input_focus.mp3 -------------------------------------------------------------------------------- /sounds/ui/item_deselect.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/item_deselect.mp3 -------------------------------------------------------------------------------- /sounds/ui/item_select.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/item_select.mp3 -------------------------------------------------------------------------------- /sounds/ui/keystroke_hard.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/keystroke_hard.mp3 -------------------------------------------------------------------------------- /sounds/ui/keystroke_medium.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/keystroke_medium.mp3 -------------------------------------------------------------------------------- /sounds/ui/keystroke_soft.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/keystroke_soft.mp3 -------------------------------------------------------------------------------- /sounds/ui/panel_collapse.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/panel_collapse.mp3 -------------------------------------------------------------------------------- /sounds/ui/panel_expand.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/panel_expand.mp3 -------------------------------------------------------------------------------- /sounds/ui/pop_close.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/pop_close.mp3 -------------------------------------------------------------------------------- /sounds/ui/pop_open.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/pop_open.mp3 -------------------------------------------------------------------------------- /sounds/ui/popup_close.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/popup_close.mp3 -------------------------------------------------------------------------------- /sounds/ui/popup_open.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/popup_open.mp3 -------------------------------------------------------------------------------- /sounds/ui/radio_select.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/radio_select.mp3 -------------------------------------------------------------------------------- /sounds/ui/send.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/send.mp3 -------------------------------------------------------------------------------- /sounds/ui/submit.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/submit.mp3 -------------------------------------------------------------------------------- /sounds/ui/success_bling.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/success_bling.mp3 -------------------------------------------------------------------------------- /sounds/ui/success_blip.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/success_blip.mp3 -------------------------------------------------------------------------------- /sounds/ui/success_chime.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/success_chime.mp3 -------------------------------------------------------------------------------- /sounds/ui/tab_close.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/tab_close.mp3 -------------------------------------------------------------------------------- /sounds/ui/tab_open.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/tab_open.mp3 -------------------------------------------------------------------------------- /sounds/ui/toggle_off.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/toggle_off.mp3 -------------------------------------------------------------------------------- /sounds/ui/toggle_on.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/toggle_on.mp3 -------------------------------------------------------------------------------- /sounds/ui/window_close.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/window_close.mp3 -------------------------------------------------------------------------------- /sounds/ui/window_open.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/sounds/ui/window_open.mp3 -------------------------------------------------------------------------------- /src/components.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, ReactNode, useCallback, useEffect, useState } from "react"; 2 | import { setSoundContext, useSound } from "./hooks"; 3 | import { 4 | isSoundEnabled, 5 | playSound, 6 | preloadSounds, 7 | setSoundEnabled, 8 | SoundName, 9 | subscribeSoundState, 10 | unlockAudioContext, 11 | } from "./runtime"; 12 | import { SoundOptions } from "./types"; 13 | 14 | /** 15 | * Props for the Sound component 16 | */ 17 | interface SoundProps { 18 | /** 19 | * The sound to play (will use local bundled sounds if available) 20 | */ 21 | name: SoundName; 22 | 23 | /** 24 | * When to play the sound 25 | */ 26 | trigger?: "mount" | "unmount" | "none"; 27 | 28 | /** 29 | * Sound playback options 30 | */ 31 | options?: SoundOptions; 32 | 33 | /** 34 | * Children components 35 | */ 36 | children?: ReactNode; 37 | 38 | /** 39 | * Event handler for when the sound is loaded 40 | */ 41 | onLoad?: () => void; 42 | 43 | /** 44 | * Event handler for when the sound is played 45 | */ 46 | onPlay?: () => void; 47 | 48 | /** 49 | * Event handler for when the sound is stopped 50 | */ 51 | onStop?: () => void; 52 | 53 | /** 54 | * Event handler for when the sound fails to play 55 | */ 56 | onError?: (error: Error) => void; 57 | } 58 | 59 | /** 60 | * A component for playing a sound. 61 | * Will use locally downloaded sounds if available before falling back to CDN. 62 | */ 63 | export function Sound({ 64 | name, 65 | trigger = "none", 66 | options, 67 | children, 68 | onLoad, 69 | onPlay, 70 | onStop, 71 | onError, 72 | }: SoundProps): React.ReactElement { 73 | const { play, stop, isLoaded, isPlaying } = useSound(name, options); 74 | 75 | useEffect(() => { 76 | if (isLoaded && onLoad) { 77 | onLoad(); 78 | } 79 | }, [isLoaded, onLoad]); 80 | 81 | useEffect(() => { 82 | if (isPlaying && onPlay) { 83 | onPlay(); 84 | } 85 | }, [isPlaying, onPlay]); 86 | 87 | useEffect(() => { 88 | if (trigger === "mount") { 89 | play(options).catch((error) => { 90 | if (onError) onError(error); 91 | }); 92 | } 93 | 94 | return () => { 95 | if (trigger === "unmount") playSound(name, options); 96 | 97 | stop(); 98 | if (onStop) onStop(); 99 | }; 100 | }, [trigger, name, play, stop, onStop, options, onError]); 101 | 102 | return <>{children}; 103 | } 104 | 105 | /** 106 | * Props for the SoundButton component 107 | */ 108 | interface SoundButtonProps extends React.ButtonHTMLAttributes { 109 | /** 110 | * The sound to play when clicked (will use local bundled sounds if available) 111 | */ 112 | sound: SoundName; 113 | 114 | /** 115 | * Sound playback options 116 | */ 117 | soundOptions?: SoundOptions; 118 | 119 | /** 120 | * Children components 121 | */ 122 | children?: ReactNode; 123 | 124 | /** 125 | * Event handler for when the sound fails to play 126 | */ 127 | onSoundError?: (error: Error) => void; 128 | } 129 | 130 | /** 131 | * A button that plays a sound when clicked. 132 | * Will use locally downloaded sounds if available before falling back to CDN. 133 | */ 134 | export function SoundButton({ 135 | sound, 136 | soundOptions, 137 | children, 138 | onClick, 139 | onSoundError, 140 | ...props 141 | }: SoundButtonProps): React.ReactElement { 142 | const { play } = useSound(sound, soundOptions); 143 | 144 | const handleClick = (e: React.MouseEvent) => { 145 | play().catch((error) => { 146 | if (onSoundError) onSoundError(error); 147 | }); 148 | 149 | if (onClick) onClick(e); 150 | }; 151 | 152 | return ( 153 | 156 | ); 157 | } 158 | 159 | /** 160 | * Sound context for managing sound state 161 | */ 162 | interface SoundContextType { 163 | enabled: boolean; 164 | setEnabled: (enabled: boolean) => void; 165 | } 166 | 167 | export const SoundContext = createContext(null); 168 | 169 | // Register the context with hooks 170 | setSoundContext(SoundContext); 171 | 172 | /** 173 | * Props for the SoundProvider component 174 | */ 175 | interface SoundProviderProps { 176 | /** 177 | * Sounds to preload (will use local bundled sounds if available) 178 | */ 179 | preload?: SoundName[]; 180 | 181 | /** 182 | * Initial sound enabled state (uses localStorage if not provided) 183 | */ 184 | initialEnabled?: boolean; 185 | 186 | /** 187 | * How often to clean up unused sounds (in ms), set to 0 to disable 188 | */ 189 | cleanupInterval?: number; 190 | 191 | /** 192 | * Children components 193 | */ 194 | children: ReactNode; 195 | } 196 | 197 | /** 198 | * A provider that manages sound state and preloads sounds. 199 | * Will use locally downloaded sounds if available before falling back to CDN. 200 | */ 201 | export function SoundProvider({ preload = [], initialEnabled, children }: SoundProviderProps): React.ReactElement { 202 | // Initialize sound enabled state from props or runtime 203 | const [enabled, setEnabledState] = useState(() => { 204 | if (initialEnabled !== undefined) return initialEnabled; 205 | return isSoundEnabled(); 206 | }); 207 | 208 | // Update global state when React state changes 209 | const setEnabled = useCallback((newEnabled: boolean) => { 210 | setSoundEnabled(newEnabled); 211 | }, []); 212 | 213 | // Sync with global state changes from outside React 214 | useEffect(() => { 215 | return subscribeSoundState((newEnabled) => { 216 | setEnabledState(newEnabled); 217 | }); 218 | }, []); 219 | 220 | // Ensure audio context is unlocked when component mounts 221 | useEffect(() => { 222 | unlockAudioContext(); 223 | }, []); 224 | 225 | // Preload sounds when the component mounts 226 | useEffect(() => { 227 | if (preload.length > 0) { 228 | // Start preloading immediately but don't block rendering 229 | const preloadPromise = preloadSounds(preload); 230 | 231 | // Log any preloading errors but don't break the app 232 | preloadPromise.catch((error) => { 233 | console.error("Error preloading sounds:", error); 234 | }); 235 | } 236 | }, [preload]); 237 | 238 | return {children}; 239 | } 240 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import { Howl } from "howler"; 2 | import { useCallback, useContext, useEffect, useRef, useState } from "react"; 3 | import { claimSound, freeSound, isSoundEnabled, preloadSounds, SoundName, unlockAudioContext } from "./runtime"; 4 | import { SoundHookReturn, SoundOptions } from "./types"; 5 | 6 | interface SoundContextType { 7 | enabled: boolean; 8 | setEnabled: (enabled: boolean) => void; 9 | } 10 | 11 | // We'll get the actual context from components.tsx when using it 12 | type SoundContextValue = React.Context; 13 | 14 | // This will be set by SoundProvider from components.tsx 15 | let SoundContext: SoundContextValue; 16 | 17 | // Function to set the context from components.tsx 18 | export function setSoundContext(context: SoundContextValue): void { 19 | SoundContext = context; 20 | } 21 | 22 | /** 23 | * Hook for using a sound in a React component. 24 | * Will use local bundled sounds if available before falling back to remote. 25 | */ 26 | export function useSound(soundName: SoundName, defaultOptions: SoundOptions = {}): SoundHookReturn { 27 | const [isLoaded, setIsLoaded] = useState(false); 28 | const [isPlaying, setIsPlaying] = useState(false); 29 | const soundRef = useRef(null); 30 | const activeSoundsRef = useRef void }>>([]); 31 | 32 | // Get sound enabled state from context if available, fall back to global state 33 | let enabled = isSoundEnabled(); 34 | try { 35 | const soundContext = SoundContext ? useContext(SoundContext) : null; 36 | if (soundContext) enabled = soundContext.enabled; 37 | } catch (e) {} 38 | 39 | // Lazy loading approach - only load the sound when needed 40 | const ensureLoaded = useCallback(async (): Promise => { 41 | if (soundRef.current) return soundRef.current; 42 | 43 | try { 44 | const howl = await claimSound(soundName); 45 | 46 | // Set up event listeners 47 | howl.on("end", (id) => { 48 | const soundIndex = activeSoundsRef.current.findIndex((sound) => sound.id === id); 49 | if (soundIndex >= 0) { 50 | const sound = activeSoundsRef.current[soundIndex]; 51 | 52 | if (sound.resolver) sound.resolver(); // Resolve sound (eg. created in play()) 53 | if (!sound.loop) activeSoundsRef.current.splice(soundIndex, 1); 54 | } 55 | 56 | // Only update isPlaying state if no active sounds remain 57 | if (activeSoundsRef.current.length === 0) { 58 | soundRef.current = freeSound(soundName); 59 | setIsPlaying(false); 60 | } 61 | }); 62 | 63 | soundRef.current = howl; 64 | setIsLoaded(true); 65 | return howl; 66 | } catch (error) { 67 | console.error("Error loading sound:", error); 68 | throw error; 69 | } 70 | }, [soundName, setIsPlaying]); 71 | 72 | const play = useCallback( 73 | async (options: SoundOptions = defaultOptions) => { 74 | if (!enabled) return; 75 | 76 | try { 77 | // Ensure audio context is unlocked before playing 78 | await unlockAudioContext(); 79 | 80 | // Ensure the sound is loaded 81 | const howl = await ensureLoaded(); 82 | 83 | const loop = options.loop !== undefined ? options.loop : false; 84 | if (options.volume !== undefined) howl.volume(options.volume); 85 | if (options.rate !== undefined) howl.rate(options.rate); 86 | howl.loop(loop); 87 | 88 | const id = howl.play(); 89 | setIsPlaying(true); 90 | 91 | if (loop) { 92 | // For looped sounds, we just track them but don't resolve 93 | activeSoundsRef.current.push({ id, loop }); 94 | return; 95 | } 96 | 97 | // For non-looped sounds, return a promise that resolves when the sound ends 98 | return new Promise((resolve) => { 99 | activeSoundsRef.current.push({ id, loop, resolver: () => resolve() }); 100 | }); 101 | } catch (error) { 102 | console.error("Error playing sound:", error); 103 | throw error; 104 | } 105 | }, 106 | // isLoaded is a required dep for handling changed sound name 107 | [defaultOptions, enabled, ensureLoaded, isLoaded] 108 | ); 109 | 110 | const stop = useCallback(() => { 111 | if (!soundRef.current) return; 112 | 113 | // Resolve any pending promises 114 | activeSoundsRef.current.forEach((sound) => { 115 | if (sound.resolver) sound.resolver(); 116 | }); 117 | 118 | soundRef.current.stop(); 119 | soundRef.current = freeSound(soundName); 120 | activeSoundsRef.current = []; 121 | setIsPlaying(false); 122 | }, []); 123 | 124 | const pause = useCallback(() => { 125 | if (!soundRef.current) return; 126 | 127 | soundRef.current.pause(); 128 | setIsPlaying(false); 129 | }, []); 130 | 131 | const resume = useCallback(() => { 132 | if (!soundRef.current || !enabled || activeSoundsRef.current.length === 0) return; 133 | 134 | // Try to unlock audio context before resuming 135 | unlockAudioContext().then(() => { 136 | activeSoundsRef.current.forEach(({ id }) => soundRef.current?.play(id)); 137 | setIsPlaying(true); 138 | }); 139 | }, [enabled]); 140 | 141 | useEffect(() => { 142 | if (!enabled && isPlaying) pause(); // Pause all sounds when disabled 143 | }, [enabled, isPlaying, pause]); 144 | 145 | // Cleanup on unmount 146 | useEffect(() => { 147 | preloadSounds([soundName]).then(() => setIsLoaded(true)); 148 | 149 | return () => { 150 | setIsLoaded(false); 151 | setIsPlaying(false); 152 | 153 | activeSoundsRef.current.forEach((sound) => { 154 | if (sound.resolver) sound.resolver(); // Resolve any pending promises 155 | }); 156 | activeSoundsRef.current = []; 157 | 158 | if (soundRef.current) { 159 | soundRef.current.stop(); 160 | soundRef.current = freeSound(soundName); 161 | } 162 | }; 163 | }, [soundName]); 164 | 165 | return { play, stop, pause, resume, isPlaying, isLoaded }; 166 | } 167 | 168 | interface UseSoundOnChangeOptions extends SoundOptions { 169 | initial?: boolean; 170 | } 171 | 172 | /** 173 | * Hook for playing a sound when a value changes 174 | */ 175 | export function useSoundOnChange(soundName: SoundName, value: T, options?: UseSoundOnChangeOptions): void { 176 | const { play } = useSound(soundName); 177 | const initialRef = useRef(true); 178 | 179 | useEffect(() => { 180 | const skipThisInitialRun = initialRef.current && options?.initial === false; 181 | initialRef.current = false; 182 | if (skipThisInitialRun) return; 183 | 184 | play(options).catch((err) => console.error("Failed to play sound:", err)); 185 | }, [value]); 186 | } 187 | 188 | /** 189 | * Hook for accessing and controlling the sound enabled state 190 | */ 191 | export function useSoundEnabled(): [boolean, (enabled: boolean) => void] { 192 | const context = useContext(SoundContext); 193 | if (!context) throw new Error("useSoundEnabled must be used within a SoundProvider"); 194 | 195 | return [context.enabled, context.setEnabled]; 196 | } 197 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Export core functionality 2 | export { 3 | fetchSoundBlob, 4 | getCDNUrl, 5 | isSoundEnabled, 6 | makeRemoteSound, 7 | playSound, 8 | preloadSounds, 9 | setCDNUrl, 10 | setSoundEnabled, 11 | } from "./runtime"; 12 | 13 | // Export hooks 14 | export { useSound, useSoundEnabled, useSoundOnChange } from "./hooks"; 15 | 16 | // Export components 17 | export { Sound, SoundButton, SoundProvider } from "./components"; 18 | 19 | // Export types 20 | export type { 21 | GameSoundName, 22 | LibrarySoundName, 23 | NotificationSoundName, 24 | SoundCategory, 25 | SoundHookReturn, 26 | SoundOptions, 27 | UiSoundName, 28 | } from "./types"; 29 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "sounds": { 4 | "ambient/campfire": { 5 | "src": "ambient/campfire.eb676cb.mp3", 6 | "duration": 22.047313 7 | }, 8 | "ambient/heartbeat": { 9 | "src": "ambient/heartbeat.3680f81.mp3", 10 | "duration": 22.047313 11 | }, 12 | "ambient/rain": { 13 | "src": "ambient/rain.96a6bc5.mp3", 14 | "duration": 22.047313 15 | }, 16 | "ambient/water_stream": { 17 | "src": "ambient/water_stream.9383548.mp3", 18 | "duration": 22.047313 19 | }, 20 | "ambient/wind": { 21 | "src": "ambient/wind.13b95c3.mp3", 22 | "duration": 22.047313 23 | }, 24 | "arcade/coin": { 25 | "src": "arcade/coin.5ec00e3.mp3", 26 | "duration": 0.862 27 | }, 28 | "arcade/coin_bling": { 29 | "src": "arcade/coin_bling.e7e6644.mp3", 30 | "duration": 0.862 31 | }, 32 | "arcade/jump": { 33 | "src": "arcade/jump.6baf978.mp3", 34 | "duration": 0.862 35 | }, 36 | "arcade/level_down": { 37 | "src": "arcade/level_down.7f42195.mp3", 38 | "duration": 1.123265 39 | }, 40 | "arcade/level_up": { 41 | "src": "arcade/level_up.0aba301.mp3", 42 | "duration": 1.48898 43 | }, 44 | "arcade/power_down": { 45 | "src": "arcade/power_down.222ab81.mp3", 46 | "duration": 1.071 47 | }, 48 | "arcade/power_up": { 49 | "src": "arcade/power_up.bcafcc5.mp3", 50 | "duration": 1.071 51 | }, 52 | "arcade/upgrade": { 53 | "src": "arcade/upgrade.1da1db4.mp3", 54 | "duration": 0.862 55 | }, 56 | "game/coin": { 57 | "src": "game/coin.21575b3.mp3", 58 | "duration": 0.914286 59 | }, 60 | "game/hit": { 61 | "src": "game/hit.7f64763.mp3", 62 | "duration": 0.313469 63 | }, 64 | "game/miss": { 65 | "src": "game/miss.b1d5a19.mp3", 66 | "duration": 0.287347 67 | }, 68 | "game/portal_closing": { 69 | "src": "game/portal_closing.f434407.mp3", 70 | "duration": 2.08975 71 | }, 72 | "game/portal_opening": { 73 | "src": "game/portal_opening.54935de.mp3", 74 | "duration": 3.474286 75 | }, 76 | "game/void": { 77 | "src": "game/void.ab99118.mp3", 78 | "duration": 0.862 79 | }, 80 | "misc/silence": { 81 | "src": "misc/silence.01e0b9b.mp3", 82 | "duration": 1.152 83 | }, 84 | "notification/completed": { 85 | "src": "notification/completed.31e527e.mp3", 86 | "duration": 0.862 87 | }, 88 | "notification/error": { 89 | "src": "notification/error.b92d3c6.mp3", 90 | "duration": 0.548563 91 | }, 92 | "notification/info": { 93 | "src": "notification/info.fc3baa4.mp3", 94 | "duration": 0.862 95 | }, 96 | "notification/message": { 97 | "src": "notification/message.1eefe18.mp3", 98 | "duration": 0.862 99 | }, 100 | "notification/notification": { 101 | "src": "notification/notification.595d086.mp3", 102 | "duration": 0.862 103 | }, 104 | "notification/popup": { 105 | "src": "notification/popup.cf74b54.mp3", 106 | "duration": 0.313469 107 | }, 108 | "notification/reminder": { 109 | "src": "notification/reminder.6d68587.mp3", 110 | "duration": 0.862 111 | }, 112 | "notification/success": { 113 | "src": "notification/success.f38c2ed.mp3", 114 | "duration": 1.227755 115 | }, 116 | "notification/warning": { 117 | "src": "notification/warning.207aed9.mp3", 118 | "duration": 0.862 119 | }, 120 | "system/boot_down": { 121 | "src": "system/boot_down.7baf040.mp3", 122 | "duration": 1.593469 123 | }, 124 | "system/boot_up": { 125 | "src": "system/boot_up.7369806.mp3", 126 | "duration": 3.134694 127 | }, 128 | "system/device_connect": { 129 | "src": "system/device_connect.e609d62.mp3", 130 | "duration": 0.862 131 | }, 132 | "system/device_disconnect": { 133 | "src": "system/device_disconnect.bd814fa.mp3", 134 | "duration": 0.862 135 | }, 136 | "system/lock": { 137 | "src": "system/lock.4063aab.mp3", 138 | "duration": 0.862 139 | }, 140 | "system/screenshot": { 141 | "src": "system/screenshot.f3483cb.mp3", 142 | "duration": 0.235102 143 | }, 144 | "system/trash": { 145 | "src": "system/trash.ed51a4e.mp3", 146 | "duration": 0.862 147 | }, 148 | "ui/blocked": { 149 | "src": "ui/blocked.be40409.mp3", 150 | "duration": 0.940408 151 | }, 152 | "ui/button_hard": { 153 | "src": "ui/button_hard.011f516.mp3", 154 | "duration": 0.835918 155 | }, 156 | "ui/button_hard_double": { 157 | "src": "ui/button_hard_double.2c7e778.mp3", 158 | "duration": 0.862 159 | }, 160 | "ui/button_medium": { 161 | "src": "ui/button_medium.f1076ea.mp3", 162 | "duration": 0.862 163 | }, 164 | "ui/button_soft": { 165 | "src": "ui/button_soft.896771c.mp3", 166 | "duration": 0.862 167 | }, 168 | "ui/button_soft_double": { 169 | "src": "ui/button_soft_double.ef0aec4.mp3", 170 | "duration": 0.862 171 | }, 172 | "ui/button_squishy": { 173 | "src": "ui/button_squishy.69c5c9a.mp3", 174 | "duration": 0.391837 175 | }, 176 | "ui/buzz": { 177 | "src": "ui/buzz.6d6857f.mp3", 178 | "duration": 0.548563 179 | }, 180 | "ui/buzz_deep": { 181 | "src": "ui/buzz_deep.c1f597f.mp3", 182 | "duration": 0.809796 183 | }, 184 | "ui/buzz_long": { 185 | "src": "ui/buzz_long.d506d81.mp3", 186 | "duration": 1.071 187 | }, 188 | "ui/copy": { 189 | "src": "ui/copy.4aadb27.mp3", 190 | "duration": 0.130612 191 | }, 192 | "ui/input_blur": { 193 | "src": "ui/input_blur.607531b.mp3", 194 | "duration": 0.862 195 | }, 196 | "ui/input_focus": { 197 | "src": "ui/input_focus.80b402e.mp3", 198 | "duration": 0.287347 199 | }, 200 | "ui/item_deselect": { 201 | "src": "ui/item_deselect.9955ec7.mp3", 202 | "duration": 0.862 203 | }, 204 | "ui/item_select": { 205 | "src": "ui/item_select.5d88832.mp3", 206 | "duration": 0.862 207 | }, 208 | "ui/keystroke_hard": { 209 | "src": "ui/keystroke_hard.6d8eb42.mp3", 210 | "duration": 0.548563 211 | }, 212 | "ui/keystroke_medium": { 213 | "src": "ui/keystroke_medium.2b4ae6c.mp3", 214 | "duration": 0.548563 215 | }, 216 | "ui/keystroke_soft": { 217 | "src": "ui/keystroke_soft.fcd4503.mp3", 218 | "duration": 0.548563 219 | }, 220 | "ui/panel_collapse": { 221 | "src": "ui/panel_collapse.1b8441f.mp3", 222 | "duration": 0.862 223 | }, 224 | "ui/panel_expand": { 225 | "src": "ui/panel_expand.ef3ca39.mp3", 226 | "duration": 0.862 227 | }, 228 | "ui/pop_close": { 229 | "src": "ui/pop_close.1f2dc35.mp3", 230 | "duration": 0.809796 231 | }, 232 | "ui/pop_open": { 233 | "src": "ui/pop_open.360c640.mp3", 234 | "duration": 0.835918 235 | }, 236 | "ui/popup_close": { 237 | "src": "ui/popup_close.1bd2a1b.mp3", 238 | "duration": 1.071 239 | }, 240 | "ui/popup_open": { 241 | "src": "ui/popup_open.97597a8.mp3", 242 | "duration": 1.071 243 | }, 244 | "ui/radio_select": { 245 | "src": "ui/radio_select.4fbe4e3.mp3", 246 | "duration": 0.862 247 | }, 248 | "ui/send": { 249 | "src": "ui/send.396090f.mp3", 250 | "duration": 0.182857 251 | }, 252 | "ui/submit": { 253 | "src": "ui/submit.1e228b1.mp3", 254 | "duration": 0.548563 255 | }, 256 | "ui/success_bling": { 257 | "src": "ui/success_bling.3f44a2f.mp3", 258 | "duration": 0.809796 259 | }, 260 | "ui/success_blip": { 261 | "src": "ui/success_blip.911b304.mp3", 262 | "duration": 0.626939 263 | }, 264 | "ui/success_chime": { 265 | "src": "ui/success_chime.436ed4a.mp3", 266 | "duration": 1.619592 267 | }, 268 | "ui/tab_close": { 269 | "src": "ui/tab_close.3c1a646.mp3", 270 | "duration": 0.548563 271 | }, 272 | "ui/tab_open": { 273 | "src": "ui/tab_open.3745bcd.mp3", 274 | "duration": 0.548563 275 | }, 276 | "ui/toggle_off": { 277 | "src": "ui/toggle_off.7103845.mp3", 278 | "duration": 0.261224 279 | }, 280 | "ui/toggle_on": { 281 | "src": "ui/toggle_on.2f87bf7.mp3", 282 | "duration": 0.287347 283 | }, 284 | "ui/window_close": { 285 | "src": "ui/window_close.0e958da.mp3", 286 | "duration": 0.548563 287 | }, 288 | "ui/window_open": { 289 | "src": "ui/window_open.7478756.mp3", 290 | "duration": 0.548563 291 | } 292 | } 293 | } -------------------------------------------------------------------------------- /src/runtime.ts: -------------------------------------------------------------------------------- 1 | import { Howl, Howler } from "howler"; 2 | import manifest from "./manifest.json"; 3 | import { LibrarySoundName, SoundOptions } from "./types"; 4 | 5 | export type SoundName = LibrarySoundName | string; 6 | 7 | export function isLibrarySoundName(name: any): name is LibrarySoundName { 8 | return name in manifest.sounds; 9 | } 10 | 11 | // Default CDN base URL 12 | let cdnBaseUrl = "https://reacticons.sfo3.cdn.digitaloceanspaces.com/v1"; 13 | 14 | // Global sound enabled state 15 | let soundEnabledGlobal = true; 16 | 17 | // Global event listeners for sound state changes 18 | const soundStateListeners: Array<(enabled: boolean) => void> = []; 19 | 20 | // Track if we've already set up audio context unlocking 21 | let audioUnlockInitialized = false; 22 | 23 | /** 24 | * Set a custom CDN base URL 25 | */ 26 | export function setCDNUrl(url: string): void { 27 | cdnBaseUrl = url; 28 | } 29 | 30 | /** 31 | * Get the CDN base URL 32 | */ 33 | export function getCDNUrl(): string { 34 | return cdnBaseUrl; 35 | } 36 | 37 | /** 38 | * Cache of preloaded sound blobs to avoid HTML5 audio pool exhaustion 39 | */ 40 | const soundBlobCache: Record> = {}; 41 | 42 | // TODO: turn "subscriptions" into a list of ids for better lock tracking 43 | export type HowlEntry = { instance: Howl; subscriptions: number }; 44 | 45 | /** 46 | * Cache of created Howl instances (only created when actually played) 47 | */ 48 | const howlInstanceCache: Record = {}; 49 | 50 | /** 51 | * Make a sound loader function that first tries local files then falls back to CDN 52 | */ 53 | export function makeRemoteSound(name: SoundName): () => Promise { 54 | return async () => { 55 | // First fetch the blob data if not already in cache 56 | if (!soundBlobCache[name]) { 57 | soundBlobCache[name] = fetchSoundBlob(name); 58 | } 59 | 60 | // Create or return the Howl instance 61 | return createOrGetHowlInstance(name); 62 | }; 63 | } 64 | 65 | export async function claimSound(name: SoundName): Promise { 66 | const entry = await makeRemoteSound(name)(); 67 | entry.subscriptions += 1; 68 | 69 | return entry.instance; 70 | } 71 | 72 | export function freeSound(name: SoundName) { 73 | const entry = howlInstanceCache[name]; 74 | if (entry && entry.subscriptions > 0) entry.subscriptions -= 1; 75 | 76 | cleanupUnusedSound(name); 77 | 78 | return null; 79 | } 80 | 81 | /** 82 | * Fetches sound data as a blob, from local filesystem or CDN 83 | */ 84 | export async function fetchSoundBlob(name: SoundName): Promise { 85 | try { 86 | // Try to fetch from local first 87 | const localPath = await getLocalSoundPath(name); 88 | if (localPath) { 89 | const response = await fetch(localPath); 90 | if (response.ok) return await response.blob(); 91 | } 92 | } catch (error) { 93 | console.warn(`Error loading local sound "${name}", falling back to CDN:`, error); 94 | } 95 | 96 | if (!isLibrarySoundName(name)) throw new Error(`Failed to load custom sound "${name}"`); 97 | 98 | // Fall back to CDN 99 | const soundInfo = manifest.sounds[name]; 100 | const soundUrl = `${cdnBaseUrl}/${soundInfo.src}`; 101 | 102 | const response = await fetch(soundUrl); 103 | if (!response.ok) { 104 | throw new Error(`Failed to fetch sound "${name}" from CDN`); 105 | } 106 | 107 | return await response.blob(); 108 | } 109 | 110 | /** 111 | * Creates a Howl instance from a cached blob or fetches it if needed 112 | */ 113 | async function createOrGetHowlInstance(name: SoundName): Promise { 114 | // Return existing instance if available 115 | if (howlInstanceCache[name]) { 116 | return howlInstanceCache[name]; 117 | } 118 | 119 | // Fetch the blob if not already in progress 120 | if (!soundBlobCache[name]) { 121 | soundBlobCache[name] = fetchSoundBlob(name); 122 | } 123 | 124 | // Wait for the blob to be fetched 125 | const blob = await soundBlobCache[name]; 126 | 127 | // Create object URL from blob 128 | const objectUrl = URL.createObjectURL(blob); 129 | 130 | // Create new Howl instance 131 | const howl = new Howl({ 132 | src: [objectUrl], 133 | format: ["mp3"], 134 | html5: true, // Enable streaming to reduce memory usage 135 | onload: () => { 136 | // Store object URL reference to revoke later 137 | (howl as any)._objectUrl = objectUrl; 138 | }, 139 | }); 140 | 141 | // Store in cache 142 | howlInstanceCache[name] = { instance: howl, subscriptions: 0 }; 143 | 144 | return howlInstanceCache[name]; 145 | } 146 | 147 | /** 148 | * Preload multiple sounds by fetching their data without creating Howl instances 149 | */ 150 | export function preloadSounds(names: SoundName[]): Promise { 151 | return Promise.all( 152 | names.map(async (name) => { 153 | if (!soundBlobCache[name]) { 154 | soundBlobCache[name] = fetchSoundBlob(name); 155 | } 156 | // Just ensure the blob is fetched but don't create Howl instance yet 157 | await soundBlobCache[name]; 158 | }) 159 | ); 160 | } 161 | 162 | /** 163 | * Play a sound by name 164 | */ 165 | export async function playSound(name: SoundName, options?: SoundOptions): Promise { 166 | if (!isSoundEnabled()) return; 167 | 168 | const sound = await claimSound(name); 169 | let freed = false; 170 | 171 | if (options) { 172 | if (options.volume !== undefined) sound.volume(options.volume); 173 | if (options.rate !== undefined) sound.rate(options.rate); 174 | if (options.loop !== undefined) sound.loop(options.loop); 175 | } 176 | 177 | sound.on("end", () => { 178 | if (freed || options?.loop) return; 179 | freeSound(name); 180 | freed = true; 181 | }); 182 | 183 | sound.play(); 184 | } 185 | 186 | /** 187 | * Check if sounds are enabled 188 | */ 189 | export function isSoundEnabled(): boolean { 190 | if (typeof window === "undefined") return false; // Server environment not supported 191 | return soundEnabledGlobal; 192 | } 193 | 194 | /** 195 | * Enable or disable all sounds 196 | * This is used internally by the SoundProvider 197 | */ 198 | export function setSoundEnabled(enabled: boolean): void { 199 | soundEnabledGlobal = enabled; 200 | 201 | // Store in localStorage for persistence 202 | if (typeof localStorage !== "undefined") localStorage.setItem("react-sounds-enabled", enabled ? "true" : "false"); 203 | 204 | soundStateListeners.forEach((listener) => listener(enabled)); 205 | } 206 | 207 | /** 208 | * Initialize the sound enabled state 209 | * Called at startup to load saved preference 210 | */ 211 | export function initSoundEnabledState(): void { 212 | if (typeof localStorage === "undefined") return; 213 | 214 | const savedPreference = localStorage.getItem("react-sounds-enabled"); 215 | if (savedPreference !== null) { 216 | soundEnabledGlobal = savedPreference !== "false"; 217 | } 218 | } 219 | 220 | /** 221 | * Subscribe to sound enabled state changes 222 | */ 223 | export function subscribeSoundState(callback: (enabled: boolean) => void): () => void { 224 | soundStateListeners.push(callback); 225 | 226 | // Return unsubscribe function 227 | return () => { 228 | const index = soundStateListeners.indexOf(callback); 229 | if (index !== -1) soundStateListeners.splice(index, 1); 230 | }; 231 | } 232 | 233 | /** 234 | * Get the local path for a sound when using the offline mode 235 | */ 236 | export async function getLocalSoundPath(name: SoundName): Promise { 237 | if (!isLibrarySoundName(name)) return name; 238 | 239 | const publicPath = `/sounds/${name}.mp3`; 240 | try { 241 | // Add a timeout of 300ms to quickly fall back to CDN if file isn't available 242 | const controller = new AbortController(); 243 | const timeoutId = setTimeout(() => controller.abort(), 300); 244 | 245 | const response = await fetch(publicPath, { method: "HEAD", signal: controller.signal }); 246 | clearTimeout(timeoutId); 247 | 248 | if (!response.ok) return null; 249 | if (!response.headers.get("content-type")?.toLowerCase().startsWith("audio")) return null; 250 | 251 | return publicPath; 252 | } catch (e) {} 253 | 254 | return null; 255 | } 256 | 257 | /** 258 | * Clean up unused Howl instances and blob cache entries 259 | * Call this when running into memory constraints 260 | */ 261 | export function cleanupUnusedSound(name: string): void { 262 | const entry = howlInstanceCache[name]; 263 | if (!entry || entry.instance.playing() || entry.subscriptions > 0) return; 264 | 265 | const { instance } = entry; 266 | if ((instance as any)._objectUrl) URL.revokeObjectURL((instance as any)._objectUrl); 267 | instance.unload(); 268 | delete howlInstanceCache[name]; 269 | } 270 | 271 | /** 272 | * Unlock the audio context globally to allow playback without direct user interaction 273 | */ 274 | export async function unlockAudioContext(): Promise { 275 | if (typeof window === "undefined" || !Howler.ctx) return; 276 | if (Howler.ctx.state !== "suspended") return; 277 | 278 | try { 279 | await Howler.ctx.resume(); 280 | } catch (error) { 281 | console.warn("Failed to unlock audio context:", error); 282 | } 283 | } 284 | 285 | /** 286 | * Setup global event listeners to unlock audio context on user interaction 287 | * Can be called multiple times safely (will only set up listeners once) 288 | */ 289 | export function initAudioContextUnlock(): () => void { 290 | if (typeof window === "undefined" || audioUnlockInitialized) return () => {}; 291 | 292 | audioUnlockInitialized = true; 293 | 294 | const events = ["click", "touchstart", "keydown"]; 295 | 296 | const handleInteraction = () => { 297 | unlockAudioContext(); 298 | events.forEach((event) => document.removeEventListener(event, handleInteraction)); 299 | }; 300 | 301 | events.forEach((event) => document.addEventListener(event, handleInteraction)); 302 | 303 | // Return cleanup function 304 | return () => { 305 | events.forEach((event) => document.removeEventListener(event, handleInteraction)); 306 | }; 307 | } 308 | 309 | // Initialize sound state 310 | initSoundEnabledState(); 311 | 312 | // Initialize audio context unlocking if we're in a browser environment 313 | if (typeof window !== "undefined") { 314 | initAudioContextUnlock(); 315 | } 316 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Categories of sounds available in the library 4 | */ 5 | export type SoundCategory = 'ambient' | 'arcade' | 'game' | 'misc' | 'notification' | 'system' | 'ui'; 6 | 7 | /** 8 | * AmbientSoundName sound names 9 | */ 10 | export type AmbientSoundName = 'campfire' | 'heartbeat' | 'rain' | 'water_stream' | 'wind'; 11 | 12 | /** 13 | * ArcadeSoundName sound names 14 | */ 15 | export type ArcadeSoundName = 'coin' | 'coin_bling' | 'jump' | 'level_down' | 'level_up' | 'power_down' | 'power_up' | 'upgrade'; 16 | 17 | /** 18 | * GameSoundName sound names 19 | */ 20 | export type GameSoundName = 'coin' | 'hit' | 'miss' | 'portal_closing' | 'portal_opening' | 'void'; 21 | 22 | /** 23 | * MiscSoundName sound names 24 | */ 25 | export type MiscSoundName = 'silence'; 26 | 27 | /** 28 | * NotificationSoundName sound names 29 | */ 30 | export type NotificationSoundName = 'completed' | 'error' | 'info' | 'message' | 'notification' | 'popup' | 'reminder' | 'success' | 'warning'; 31 | 32 | /** 33 | * SystemSoundName sound names 34 | */ 35 | export type SystemSoundName = 'boot_down' | 'boot_up' | 'device_connect' | 'device_disconnect' | 'lock' | 'screenshot' | 'trash'; 36 | 37 | /** 38 | * UiSoundName sound names 39 | */ 40 | export type UiSoundName = 'blocked' | 'button_hard' | 'button_hard_double' | 'button_medium' | 'button_soft' | 'button_soft_double' | 'button_squishy' | 'buzz' | 'buzz_deep' | 'buzz_long' | 'copy' | 'input_blur' | 'input_focus' | 'item_deselect' | 'item_select' | 'keystroke_hard' | 'keystroke_medium' | 'keystroke_soft' | 'panel_collapse' | 'panel_expand' | 'pop_close' | 'pop_open' | 'popup_close' | 'popup_open' | 'radio_select' | 'send' | 'submit' | 'success_bling' | 'success_blip' | 'success_chime' | 'tab_close' | 'tab_open' | 'toggle_off' | 'toggle_on' | 'window_close' | 'window_open'; 41 | 42 | /** 43 | * All available sound names 44 | */ 45 | export type LibrarySoundName = 46 | | `ambient/${AmbientSoundName}` 47 | | `arcade/${ArcadeSoundName}` 48 | | `game/${GameSoundName}` 49 | | `misc/${MiscSoundName}` 50 | | `notification/${NotificationSoundName}` 51 | | `system/${SystemSoundName}` 52 | | `ui/${UiSoundName}`; 53 | 54 | /** 55 | * Sound options for playback 56 | */ 57 | export interface SoundOptions { 58 | /** 59 | * Volume of the sound (0.0 to 1.0) 60 | */ 61 | volume?: number; 62 | 63 | /** 64 | * Playback rate (1.0 is normal speed) 65 | */ 66 | rate?: number; 67 | 68 | /** 69 | * Sound should loop 70 | */ 71 | loop?: boolean; 72 | } 73 | 74 | /** 75 | * Return type for useSound hook 76 | */ 77 | export interface SoundHookReturn { 78 | /** 79 | * Play the sound with optional options 80 | */ 81 | play: (options?: SoundOptions) => Promise; 82 | 83 | /** 84 | * Stop the sound 85 | */ 86 | stop: () => void; 87 | 88 | /** 89 | * Pause the sound 90 | */ 91 | pause: () => void; 92 | 93 | /** 94 | * Resume the sound 95 | */ 96 | resume: () => void; 97 | 98 | /** 99 | * Check if the sound is currently playing 100 | */ 101 | isPlaying: boolean; 102 | 103 | /** 104 | * Check if the sound is loaded 105 | */ 106 | isLoaded: boolean; 107 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns an RFC 4122–compliant UUID v4 3 | * Works in every modern browser and in legacy environments without crypto 4 | */ 5 | export function uuidv4() { 6 | let r = ""; 7 | for (let i = 0; i < 32; i++) r += ((Math.random() * 16) | 0).toString(16); 8 | 9 | return ( 10 | r.slice(0, 8) + 11 | "-" + 12 | r.slice(8, 12) + 13 | "-4" + 14 | r.slice(13, 16) + 15 | "-" + // force version 4 16 | ((parseInt(r[16], 16) & 0x3) | 0x8).toString(16) + // force variant 10 17 | r.slice(17, 20) + 18 | "-" + 19 | r.slice(20) 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "declaration": true, 7 | "sourceMap": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitReturns": true, 13 | "noImplicitThis": true, 14 | "noImplicitAny": true, 15 | "strictNullChecks": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "esModuleInterop": true, 19 | "resolveJsonModule": true, 20 | "jsx": "react", 21 | "outDir": "dist", 22 | "rootDir": "src" 23 | }, 24 | "include": ["src"], 25 | "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] 26 | } 27 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. 13 | -------------------------------------------------------------------------------- /website/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | 6 | export default [ 7 | { ignores: ['dist'] }, 8 | { 9 | files: ['**/*.{js,jsx}'], 10 | languageOptions: { 11 | ecmaVersion: 2020, 12 | globals: globals.browser, 13 | parserOptions: { 14 | ecmaVersion: 'latest', 15 | ecmaFeatures: { jsx: true }, 16 | sourceType: 'module', 17 | }, 18 | }, 19 | plugins: { 20 | 'react-hooks': reactHooks, 21 | 'react-refresh': reactRefresh, 22 | }, 23 | rules: { 24 | ...js.configs.recommended.rules, 25 | ...reactHooks.configs.recommended.rules, 26 | 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], 27 | 'react-refresh/only-export-components': [ 28 | 'warn', 29 | { allowConstantExport: true }, 30 | ], 31 | }, 32 | }, 33 | ] 34 | -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | react-sounds - Sound Effects for React 25 | 26 | 35 | 36 | 37 |
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-sounds-website", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "rm -rf dist/ && npm run sync && vite build", 9 | "start": "serve -s dist", 10 | "sync": "cp ../src/manifest.json ./src/", 11 | "lint": "eslint .", 12 | "preview": "vite preview" 13 | }, 14 | "dependencies": { 15 | "clsx": "^2.1.1", 16 | "howler": "^2.2.4", 17 | "prismjs": "^1.30.0", 18 | "react": "^19.0.0", 19 | "react-dom": "^19.0.0", 20 | "react-router-dom": "^7.5.2", 21 | "react-sounds": "^1.0.25", 22 | "serve": "^14.2.1", 23 | "tailwind-merge": "^3.2.0" 24 | }, 25 | "devDependencies": { 26 | "@eslint/js": "^9.22.0", 27 | "@tailwindcss/vite": "^4.1.4", 28 | "@types/prismjs": "^1.26.5", 29 | "@types/react": "^19.0.10", 30 | "@types/react-dom": "^19.0.4", 31 | "@typescript-eslint/eslint-plugin": "^8.31.1", 32 | "@typescript-eslint/parser": "^8.31.1", 33 | "@typescript-eslint/utils": "^8.31.1", 34 | "@vitejs/plugin-react": "^4.3.4", 35 | "autoprefixer": "^10.4.21", 36 | "eslint": "^9.25.1", 37 | "eslint-plugin-react-hooks": "^5.2.0", 38 | "eslint-plugin-react-refresh": "^0.4.19", 39 | "globals": "^16.0.0", 40 | "picomatch": "^4.0.2", 41 | "postcss": "^8.5.3", 42 | "tailwindcss": "^4.1.4", 43 | "typescript": "^5.3.3", 44 | "vite": "^6.3.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /website/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /website/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /website/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Routes } from "react-router-dom"; 3 | import Footer from "./components/Footer"; 4 | import Header from "./components/Header"; 5 | import "./index.css"; 6 | import DocumentationPage from "./pages/DocumentationPage"; 7 | import HomePage from "./pages/HomePage"; 8 | import SoundLibraryPage from "./pages/SoundLibraryPage"; 9 | 10 | const App: React.FC = () => { 11 | return ( 12 |
13 |
14 |
15 | 16 | } /> 17 | } /> 18 | } /> 19 | 20 |
21 |
22 |
23 | ); 24 | }; 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /website/src/assets/music.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aediliclabs/react-sounds/47051a762479cf0b639e3d48146de3b43ab0001c/website/src/assets/music.mp3 -------------------------------------------------------------------------------- /website/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/src/components/AdvancedSoundDemo.tsx: -------------------------------------------------------------------------------- 1 | import CodeBlock from "@/components/CodeBlock"; 2 | import React, { useState } from "react"; 3 | import { LibrarySoundName, useSound } from "react-sounds"; 4 | import SoundSelector from "./SoundSelector"; 5 | 6 | interface SoundButtonProps { 7 | soundName: LibrarySoundName; 8 | label: string; 9 | } 10 | 11 | interface CategorySoundButtonProps { 12 | sound: LibrarySoundName; 13 | } 14 | 15 | interface Category { 16 | name: string; 17 | description: string; 18 | sounds: LibrarySoundName[]; 19 | } 20 | 21 | // Enhanced sound demo component with more interactive features 22 | const AdvancedSoundDemo: React.FC = () => { 23 | return ( 24 |
25 |

Interactive Sound Demo

26 | 27 |
28 | 29 | 30 |
31 | 32 |
33 |

Sound Effects Categories

34 | 35 |
36 |
37 | ); 38 | }; 39 | 40 | // Simple sound buttons section 41 | const BasicSoundButtons: React.FC = () => { 42 | return ( 43 |
44 |

Basic Usage

45 |

Click the buttons to hear different sound effects.

46 |
47 | 48 | 49 | 50 | 51 |
52 | 53 | {`import { useSound } from 'react-sounds'; 54 | 55 | function Button() { 56 | const { play } = useSound('ui/button_1'); 57 | return ; 58 | }`} 59 | 60 |
61 | ); 62 | }; 63 | 64 | // Simple button component that plays a sound 65 | const SoundButton: React.FC = ({ soundName, label }) => { 66 | const { play, isLoaded } = useSound(soundName); 67 | return ( 68 | 75 | ); 76 | }; 77 | 78 | // Advanced sound controller with volume, rate controls 79 | const AdvancedSoundController: React.FC = () => { 80 | const [volume, setVolume] = useState(0.7); 81 | const [rate, setRate] = useState(1.0); 82 | const [loop, setLoop] = useState(false); 83 | const [selectedSound, setSelectedSound] = useState("notification/success"); 84 | 85 | const { play, stop, pause, resume, isPlaying, isLoaded } = useSound(selectedSound); 86 | 87 | const handlePlay = (): void => { 88 | play({ volume, rate, loop }); 89 | }; 90 | 91 | return ( 92 |
93 |

Advanced Controls

94 |

Try adjusting these parameters before playing the sound.

95 | 96 |
97 |
98 | 99 | setVolume(parseFloat(e.target.value))} 106 | className="w-full h-2 bg-blue-100 rounded-lg appearance-none cursor-pointer" 107 | /> 108 |
109 | 110 |
111 | 112 | setRate(parseFloat(e.target.value))} 119 | className="w-full h-2 bg-blue-100 rounded-lg appearance-none cursor-pointer" 120 | /> 121 |
122 | 123 | 124 | 125 |
126 | setLoop(e.target.checked)} 131 | className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" 132 | /> 133 | 136 |
137 |
138 | 139 |
140 | 147 | 154 | 161 | 168 |
169 | 170 |
171 | Status: {isLoaded ? "Loaded" : "Loading"}, {isPlaying ? "Playing" : "Stopped"} 172 |
173 |
174 | ); 175 | }; 176 | 177 | // Sound categories showcase 178 | const SoundCategories: React.FC = () => { 179 | const categories: Category[] = [ 180 | { 181 | name: "Notification Sounds", 182 | description: "Ideal for alerts, confirmations, and system events", 183 | sounds: ["notification/info", "notification/popup", "notification/success", "notification/error"], 184 | }, 185 | { 186 | name: "Game Sounds", 187 | description: "Great for achievements, collectibles, and game events", 188 | sounds: ["game/coin", "game/void", "game/hit", "game/miss"], 189 | }, 190 | { 191 | name: "UI Sounds", 192 | description: "Perfect for buttons, toggles, and interface interactions", 193 | sounds: ["ui/blocked", "ui/button_medium", "ui/panel_expand", "ui/panel_collapse"], 194 | }, 195 | ]; 196 | 197 | return ( 198 |
199 | {categories.map((category, index) => ( 200 |
201 |
{category.name}
202 |

{category.description}

203 |
204 | {category.sounds.map((sound, idx) => ( 205 | 206 | ))} 207 |
208 |
209 | ))} 210 |
211 | ); 212 | }; 213 | 214 | // Category sound button with name display 215 | const CategorySoundButton: React.FC = ({ sound }) => { 216 | const { play, isLoaded } = useSound(sound); 217 | const displayName = sound.split("/").pop(); 218 | 219 | return ( 220 | 228 | ); 229 | }; 230 | 231 | export default AdvancedSoundDemo; 232 | -------------------------------------------------------------------------------- /website/src/components/CodeBlock.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/utils/cn"; 2 | import Prism from "prismjs"; 3 | import "prismjs/components/prism-bash"; 4 | import "prismjs/components/prism-javascript"; 5 | import "prismjs/components/prism-jsx"; 6 | import "prismjs/components/prism-tsx"; 7 | import "prismjs/components/prism-typescript"; 8 | import "prismjs/themes/prism-tomorrow.css"; 9 | import React, { useEffect } from "react"; 10 | 11 | interface CodeBlockProps { 12 | className?: string; 13 | children: React.ReactNode; 14 | language?: string; 15 | } 16 | 17 | const CodeBlock: React.FC = ({ className, children, language = "tsx" }) => { 18 | const langClass = `language-${language}`; 19 | 20 | useEffect(() => { 21 | Prism.highlightAll(); 22 | }, [children, language]); 23 | 24 | return ( 25 |
26 |       {children}
27 |     
28 | ); 29 | }; 30 | 31 | export default CodeBlock; 32 | -------------------------------------------------------------------------------- /website/src/components/FeatureCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface FeatureCardProps { 4 | title: string; 5 | description: string; 6 | icon?: string; 7 | } 8 | 9 | const FeatureCard: React.FC = ({ title, description, icon }) => { 10 | return ( 11 |
12 | {icon &&
{icon}
} 13 |

{title}

14 |

{description}

15 |
16 | ); 17 | }; 18 | 19 | export default FeatureCard; 20 | -------------------------------------------------------------------------------- /website/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Footer: React.FC = () => { 4 | return ( 5 |
6 |
7 |

© {new Date().getFullYear()} Aedilic Inc. Made with ♥.

8 |
9 |
10 | ); 11 | }; 12 | 13 | export default Footer; 14 | -------------------------------------------------------------------------------- /website/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { playSound, useSoundEnabled, useSoundOnChange } from "react-sounds"; 4 | 5 | const Header: React.FC = () => { 6 | const [soundIsEnabled, setIsEnabled] = useSoundEnabled(); 7 | 8 | const handleSoundToggle = async () => { 9 | if (soundIsEnabled) await playSound("ui/toggle_off"); 10 | setIsEnabled(!soundIsEnabled); 11 | }; 12 | 13 | useSoundOnChange("ui/toggle_on", soundIsEnabled, { initial: false }); 14 | 15 | return ( 16 |
17 | 66 |
67 | ); 68 | }; 69 | 70 | export default Header; 71 | -------------------------------------------------------------------------------- /website/src/components/SoundSelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from "react"; 2 | import { LibrarySoundName } from "react-sounds"; 3 | import manifest from "../manifest.json"; 4 | 5 | interface SoundSelectorProps { 6 | onSelect: (sound: LibrarySoundName) => void; 7 | value?: LibrarySoundName; 8 | } 9 | 10 | const SoundSelector: React.FC = ({ onSelect, value }) => { 11 | const [searchTerm, setSearchTerm] = useState(value ?? ""); 12 | const [isOpen, setIsOpen] = useState(false); 13 | 14 | // Group sounds by category for better organization 15 | const soundGroups = useMemo(() => { 16 | const groups: Record = {}; 17 | 18 | // Extract sounds from manifest and group by category 19 | Object.keys(manifest.sounds).forEach((soundKey) => { 20 | const [category, name] = soundKey.split("/"); 21 | const categoryName = category.charAt(0).toUpperCase() + category.slice(1) + " Sounds"; 22 | 23 | if (!groups[categoryName]) { 24 | groups[categoryName] = []; 25 | } 26 | groups[categoryName].push(soundKey as LibrarySoundName); 27 | }); 28 | 29 | return groups; 30 | }, []); 31 | 32 | // Filter sounds based on search term 33 | const filteredSounds = useMemo(() => { 34 | if (!searchTerm) return soundGroups; 35 | 36 | const filtered: Record = {}; 37 | Object.entries(soundGroups).forEach(([category, sounds]) => { 38 | const matchingSounds = sounds.filter((sound) => sound.toLowerCase().includes(searchTerm.toLowerCase())); 39 | if (matchingSounds.length > 0) { 40 | filtered[category] = matchingSounds; 41 | } 42 | }); 43 | return filtered; 44 | }, [searchTerm, soundGroups]); 45 | 46 | const handleSelect = (sound: LibrarySoundName) => { 47 | onSelect(sound); 48 | setSearchTerm(sound); 49 | setIsOpen(false); 50 | }; 51 | 52 | // Close dropdown when clicking outside 53 | useEffect(() => { 54 | const handleClickOutside = (event: MouseEvent) => { 55 | const target = event.target as HTMLElement; 56 | if (!target.closest(".sound-selector")) { 57 | setIsOpen(false); 58 | } 59 | }; 60 | 61 | document.addEventListener("mousedown", handleClickOutside); 62 | return () => document.removeEventListener("mousedown", handleClickOutside); 63 | }, []); 64 | 65 | return ( 66 |
67 |
68 | { 72 | setSearchTerm(e.target.value); 73 | setIsOpen(true); 74 | }} 75 | onFocus={() => setIsOpen(true)} 76 | placeholder="Search for a sound..." 77 | className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition duration-150 ease-in-out" 78 | /> 79 |
80 | 86 | 87 | 88 |
89 |
90 | 91 | {isOpen && ( 92 |
93 | {Object.entries(filteredSounds).map(([category, sounds]) => ( 94 |
95 |
{category}
96 | {sounds.map((sound) => ( 97 | 104 | ))} 105 |
106 | ))} 107 | {Object.keys(filteredSounds).length === 0 && ( 108 |
No sounds found
109 | )} 110 |
111 | )} 112 |
113 | ); 114 | }; 115 | 116 | export default SoundSelector; 117 | -------------------------------------------------------------------------------- /website/src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.mp3" { 2 | const src: string; 3 | export default src; 4 | } 5 | 6 | declare module "*.wav" { 7 | const src: string; 8 | export default src; 9 | } 10 | 11 | declare module "*.ogg" { 12 | const src: string; 13 | export default src; 14 | } 15 | -------------------------------------------------------------------------------- /website/src/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | -------------------------------------------------------------------------------- /website/src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client"; 2 | import { BrowserRouter } from "react-router-dom"; 3 | import { SoundProvider } from "react-sounds"; 4 | import App from "./App"; 5 | import "./index.css"; 6 | 7 | const rootElement = document.getElementById("root"); 8 | if (!rootElement) throw new Error("Failed to find the root element"); 9 | 10 | ReactDOM.createRoot(rootElement).render( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /website/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "sounds": { 4 | "ambient/campfire": { 5 | "src": "ambient/campfire.eb676cb.mp3", 6 | "duration": 22.047313 7 | }, 8 | "ambient/heartbeat": { 9 | "src": "ambient/heartbeat.3680f81.mp3", 10 | "duration": 22.047313 11 | }, 12 | "ambient/rain": { 13 | "src": "ambient/rain.96a6bc5.mp3", 14 | "duration": 22.047313 15 | }, 16 | "ambient/water_stream": { 17 | "src": "ambient/water_stream.9383548.mp3", 18 | "duration": 22.047313 19 | }, 20 | "ambient/wind": { 21 | "src": "ambient/wind.13b95c3.mp3", 22 | "duration": 22.047313 23 | }, 24 | "arcade/coin": { 25 | "src": "arcade/coin.5ec00e3.mp3", 26 | "duration": 0.862 27 | }, 28 | "arcade/coin_bling": { 29 | "src": "arcade/coin_bling.e7e6644.mp3", 30 | "duration": 0.862 31 | }, 32 | "arcade/jump": { 33 | "src": "arcade/jump.6baf978.mp3", 34 | "duration": 0.862 35 | }, 36 | "arcade/level_down": { 37 | "src": "arcade/level_down.7f42195.mp3", 38 | "duration": 1.123265 39 | }, 40 | "arcade/level_up": { 41 | "src": "arcade/level_up.0aba301.mp3", 42 | "duration": 1.48898 43 | }, 44 | "arcade/power_down": { 45 | "src": "arcade/power_down.222ab81.mp3", 46 | "duration": 1.071 47 | }, 48 | "arcade/power_up": { 49 | "src": "arcade/power_up.bcafcc5.mp3", 50 | "duration": 1.071 51 | }, 52 | "arcade/upgrade": { 53 | "src": "arcade/upgrade.1da1db4.mp3", 54 | "duration": 0.862 55 | }, 56 | "game/coin": { 57 | "src": "game/coin.21575b3.mp3", 58 | "duration": 0.914286 59 | }, 60 | "game/hit": { 61 | "src": "game/hit.7f64763.mp3", 62 | "duration": 0.313469 63 | }, 64 | "game/miss": { 65 | "src": "game/miss.b1d5a19.mp3", 66 | "duration": 0.287347 67 | }, 68 | "game/portal_closing": { 69 | "src": "game/portal_closing.f434407.mp3", 70 | "duration": 2.08975 71 | }, 72 | "game/portal_opening": { 73 | "src": "game/portal_opening.54935de.mp3", 74 | "duration": 3.474286 75 | }, 76 | "game/void": { 77 | "src": "game/void.ab99118.mp3", 78 | "duration": 0.862 79 | }, 80 | "misc/silence": { 81 | "src": "misc/silence.01e0b9b.mp3", 82 | "duration": 1.152 83 | }, 84 | "notification/completed": { 85 | "src": "notification/completed.31e527e.mp3", 86 | "duration": 0.862 87 | }, 88 | "notification/error": { 89 | "src": "notification/error.b92d3c6.mp3", 90 | "duration": 0.548563 91 | }, 92 | "notification/info": { 93 | "src": "notification/info.fc3baa4.mp3", 94 | "duration": 0.862 95 | }, 96 | "notification/message": { 97 | "src": "notification/message.1eefe18.mp3", 98 | "duration": 0.862 99 | }, 100 | "notification/notification": { 101 | "src": "notification/notification.595d086.mp3", 102 | "duration": 0.862 103 | }, 104 | "notification/popup": { 105 | "src": "notification/popup.cf74b54.mp3", 106 | "duration": 0.313469 107 | }, 108 | "notification/reminder": { 109 | "src": "notification/reminder.6d68587.mp3", 110 | "duration": 0.862 111 | }, 112 | "notification/success": { 113 | "src": "notification/success.f38c2ed.mp3", 114 | "duration": 1.227755 115 | }, 116 | "notification/warning": { 117 | "src": "notification/warning.207aed9.mp3", 118 | "duration": 0.862 119 | }, 120 | "system/boot_down": { 121 | "src": "system/boot_down.7baf040.mp3", 122 | "duration": 1.593469 123 | }, 124 | "system/boot_up": { 125 | "src": "system/boot_up.7369806.mp3", 126 | "duration": 3.134694 127 | }, 128 | "system/device_connect": { 129 | "src": "system/device_connect.e609d62.mp3", 130 | "duration": 0.862 131 | }, 132 | "system/device_disconnect": { 133 | "src": "system/device_disconnect.bd814fa.mp3", 134 | "duration": 0.862 135 | }, 136 | "system/lock": { 137 | "src": "system/lock.4063aab.mp3", 138 | "duration": 0.862 139 | }, 140 | "system/screenshot": { 141 | "src": "system/screenshot.f3483cb.mp3", 142 | "duration": 0.235102 143 | }, 144 | "system/trash": { 145 | "src": "system/trash.ed51a4e.mp3", 146 | "duration": 0.862 147 | }, 148 | "ui/blocked": { 149 | "src": "ui/blocked.be40409.mp3", 150 | "duration": 0.940408 151 | }, 152 | "ui/button_hard": { 153 | "src": "ui/button_hard.011f516.mp3", 154 | "duration": 0.835918 155 | }, 156 | "ui/button_hard_double": { 157 | "src": "ui/button_hard_double.2c7e778.mp3", 158 | "duration": 0.862 159 | }, 160 | "ui/button_medium": { 161 | "src": "ui/button_medium.f1076ea.mp3", 162 | "duration": 0.862 163 | }, 164 | "ui/button_soft": { 165 | "src": "ui/button_soft.896771c.mp3", 166 | "duration": 0.862 167 | }, 168 | "ui/button_soft_double": { 169 | "src": "ui/button_soft_double.ef0aec4.mp3", 170 | "duration": 0.862 171 | }, 172 | "ui/button_squishy": { 173 | "src": "ui/button_squishy.69c5c9a.mp3", 174 | "duration": 0.391837 175 | }, 176 | "ui/buzz": { 177 | "src": "ui/buzz.6d6857f.mp3", 178 | "duration": 0.548563 179 | }, 180 | "ui/buzz_deep": { 181 | "src": "ui/buzz_deep.c1f597f.mp3", 182 | "duration": 0.809796 183 | }, 184 | "ui/buzz_long": { 185 | "src": "ui/buzz_long.d506d81.mp3", 186 | "duration": 1.071 187 | }, 188 | "ui/copy": { 189 | "src": "ui/copy.4aadb27.mp3", 190 | "duration": 0.130612 191 | }, 192 | "ui/input_blur": { 193 | "src": "ui/input_blur.607531b.mp3", 194 | "duration": 0.862 195 | }, 196 | "ui/input_focus": { 197 | "src": "ui/input_focus.80b402e.mp3", 198 | "duration": 0.287347 199 | }, 200 | "ui/item_deselect": { 201 | "src": "ui/item_deselect.9955ec7.mp3", 202 | "duration": 0.862 203 | }, 204 | "ui/item_select": { 205 | "src": "ui/item_select.5d88832.mp3", 206 | "duration": 0.862 207 | }, 208 | "ui/keystroke_hard": { 209 | "src": "ui/keystroke_hard.6d8eb42.mp3", 210 | "duration": 0.548563 211 | }, 212 | "ui/keystroke_medium": { 213 | "src": "ui/keystroke_medium.2b4ae6c.mp3", 214 | "duration": 0.548563 215 | }, 216 | "ui/keystroke_soft": { 217 | "src": "ui/keystroke_soft.fcd4503.mp3", 218 | "duration": 0.548563 219 | }, 220 | "ui/panel_collapse": { 221 | "src": "ui/panel_collapse.1b8441f.mp3", 222 | "duration": 0.862 223 | }, 224 | "ui/panel_expand": { 225 | "src": "ui/panel_expand.ef3ca39.mp3", 226 | "duration": 0.862 227 | }, 228 | "ui/pop_close": { 229 | "src": "ui/pop_close.1f2dc35.mp3", 230 | "duration": 0.809796 231 | }, 232 | "ui/pop_open": { 233 | "src": "ui/pop_open.360c640.mp3", 234 | "duration": 0.835918 235 | }, 236 | "ui/popup_close": { 237 | "src": "ui/popup_close.1bd2a1b.mp3", 238 | "duration": 1.071 239 | }, 240 | "ui/popup_open": { 241 | "src": "ui/popup_open.97597a8.mp3", 242 | "duration": 1.071 243 | }, 244 | "ui/radio_select": { 245 | "src": "ui/radio_select.4fbe4e3.mp3", 246 | "duration": 0.862 247 | }, 248 | "ui/send": { 249 | "src": "ui/send.396090f.mp3", 250 | "duration": 0.182857 251 | }, 252 | "ui/submit": { 253 | "src": "ui/submit.1e228b1.mp3", 254 | "duration": 0.548563 255 | }, 256 | "ui/success_bling": { 257 | "src": "ui/success_bling.3f44a2f.mp3", 258 | "duration": 0.809796 259 | }, 260 | "ui/success_blip": { 261 | "src": "ui/success_blip.911b304.mp3", 262 | "duration": 0.626939 263 | }, 264 | "ui/success_chime": { 265 | "src": "ui/success_chime.436ed4a.mp3", 266 | "duration": 1.619592 267 | }, 268 | "ui/tab_close": { 269 | "src": "ui/tab_close.3c1a646.mp3", 270 | "duration": 0.548563 271 | }, 272 | "ui/tab_open": { 273 | "src": "ui/tab_open.3745bcd.mp3", 274 | "duration": 0.548563 275 | }, 276 | "ui/toggle_off": { 277 | "src": "ui/toggle_off.7103845.mp3", 278 | "duration": 0.261224 279 | }, 280 | "ui/toggle_on": { 281 | "src": "ui/toggle_on.2f87bf7.mp3", 282 | "duration": 0.287347 283 | }, 284 | "ui/window_close": { 285 | "src": "ui/window_close.0e958da.mp3", 286 | "duration": 0.548563 287 | }, 288 | "ui/window_open": { 289 | "src": "ui/window_open.7478756.mp3", 290 | "duration": 0.548563 291 | } 292 | } 293 | } -------------------------------------------------------------------------------- /website/src/pages/DocumentationPage.tsx: -------------------------------------------------------------------------------- 1 | import CodeBlock from "@/components/CodeBlock"; 2 | import { cn } from "@/utils/cn"; 3 | import React, { useRef, useState } from "react"; 4 | import { playSound } from "react-sounds"; 5 | 6 | const DocumentationPage: React.FC = () => { 7 | const [copyStatus, setCopyStatus] = useState("Copy to Clipboard"); 8 | const docRef = useRef(null); 9 | 10 | const copyToClipboard = (): void => { 11 | if (!docRef.current) return; 12 | 13 | playSound("ui/copy"); 14 | const content = docRef.current.innerText; 15 | navigator.clipboard 16 | .writeText(content) 17 | .then(() => { 18 | setCopyStatus("Copied!"); 19 | setTimeout(() => setCopyStatus("Copy to Clipboard"), 2000); 20 | }) 21 | .catch((err) => { 22 | console.error("Failed to copy: ", err); 23 | setCopyStatus("Failed to copy"); 24 | setTimeout(() => setCopyStatus("Copy to Clipboard"), 2000); 25 | }); 26 | }; 27 | 28 | return ( 29 |
30 |
31 |

Documentation

32 | 43 |
44 | 45 | {/* Introduction */} 46 |
47 |

Introduction

48 |

49 | react-sounds provides a library of 50 | ready-to-play sound effects for your React applications, designed to be lightweight, easy to use, and 51 | flexible. It's built on top of Howler.js and provides a simple API for playing sounds in your React apps. 52 |

53 |
54 | 55 | {/* Key Features */} 56 |
57 |

Key Features

58 |
    59 |
  • 60 | 🔊 Extensive Sound Library: Organized by category (ui, notification, game, etc.). 61 |
  • 62 |
  • 63 | 🎵 Custom Sounds: Use your own sound files by requiring them directly. 64 |
  • 65 |
  • 66 | 🪶 Lightweight: Only JS wrappers included; audio files hosted on CDN. 67 |
  • 68 |
  • 69 | 🔄 Lazy Loading: Sounds fetched only when needed. 70 |
  • 71 |
  • 72 | 📦 Offline Support: Local sound files with automatic fallback to CDN. 73 |
  • 74 |
  • 75 | 🎯 Simple API: Easy-to-use hooks and components. 76 |
  • 77 |
  • 78 | 💾 Persistent Settings: Sound preferences saved to localStorage. 79 |
  • 80 |
  • 81 | 🎮 Controls: Play, pause, resume and stop with sound state management. 82 |
  • 83 |
84 |
85 | 86 | {/* Installation */} 87 |
88 |

Installation

89 |

Install the library and its peer dependency, Howler.js:

90 | 91 | {`npm install react-sounds howler 92 | # or 93 | yarn add react-sounds howler`} 94 | 95 |

96 | Note:{" "} 97 | 103 | Howler.js 104 | {" "} 105 | is a required peer dependency. 106 |

107 |
108 | 109 | {/* Basic Usage */} 110 |
111 |

Basic Usage

112 | 113 |

114 | Playing a Sound with the useSound Hook 115 |

116 |

117 | The useSound hook is the primary way to 118 | interact with sounds. 119 |

120 | 121 | {`import { useSound } from 'react-sounds'; 122 | 123 | function Button() { 124 | const { play } = useSound('ui/button_1'); 125 | 126 | return ( 127 | 130 | ); 131 | }`} 132 | 133 | 134 |

Using Custom Sound Files

135 |

You can use your own custom sound files by requiring them directly:

136 | 137 | {`import { useSound } from 'react-sounds'; 138 | import customClickSound from '../assets/sounds/click.mp3'; 139 | 140 | function Button() { 141 | // Use a custom sound file by requiring it 142 | const { play } = useSound(customClickSound); 143 | 144 | return ( 145 | 148 | ); 149 | }`} 150 | 151 | 152 |

153 | Direct Sound Playing with playSound 154 |

155 |

For simple, one-off sound playback without needing state or controls.

156 | 157 | {`import { playSound } from 'react-sounds'; 158 | import customSound from '../assets/sounds/notification.mp3'; 159 | 160 | function Button() { 161 | // You can use built-in sounds 162 | const handleClick = () => playSound('ui/button_1'); 163 | 164 | // Or your own custom sounds 165 | const handleCustomSound = () => playSound(customSound); 166 | 167 | return ( 168 |
169 | 172 | 175 |
176 | ); 177 | }`} 178 |
179 | 180 |

181 | Setup with SoundProvider 182 |

183 |

184 | For best results, wrap your application with the{" "} 185 | SoundProvider component. This allows control 186 | of sound enabled state, preloads sounds, and establishes the sound context. 187 |

188 | 189 | {`import { SoundProvider } from 'react-sounds'; 190 | import customSound from '../assets/sounds/startup.mp3'; 191 | 192 | function App() { 193 | return ( 194 | 200 | 201 | 202 | ); 203 | }`} 204 | 205 |
206 | 207 | {/* Core API */} 208 |
209 |

Core API

210 | 211 |

212 | The useSound Hook 213 |

214 |

215 | The useSound hook provides full control over 216 | sound playback. It works with both built-in sound names and custom sound files. 217 |

218 | 219 | {`import { useSound } from 'react-sounds'; 220 | import customSound from '../assets/sounds/custom.mp3'; 221 | 222 | function SoundPlayer() { 223 | // Using a built-in sound 224 | const { 225 | play, // Function to play the sound 226 | stop, // Function to stop the sound 227 | pause, // Function to pause the sound 228 | resume, // Function to resume a paused sound 229 | isPlaying, // Boolean indicating if sound is currently playing 230 | isLoaded, // Boolean indicating if sound has been loaded 231 | } = useSound('ui/button_1'); 232 | 233 | // Using a custom sound file with options 234 | const { play: playCustom } = useSound(customSound, { 235 | volume: 0.8, 236 | rate: 1.2, 237 | loop: false 238 | }); 239 | 240 | return ( 241 |
242 | 243 | 244 | 245 | 246 | 247 |
248 | ); 249 | }`} 250 |
251 | 252 |

React Components

253 |

For declarative sound playback within your components.

254 | 255 | {`import { Sound, SoundButton, SoundProvider } from 'react-sounds'; 256 | 257 | // Component that plays a sound on mount, unmount, or manually 258 | function NotificationBanner() { 259 | return ( 260 | console.log('Sound loaded')} 265 | onPlay={() => console.log('Sound playing')} 266 | onStop={() => console.log('Sound stopped')} 267 | onError={(error) => console.error('Sound error:', error)} 268 | > 269 |
Notification Banner
270 |
271 | ); 272 | } 273 | 274 | import customNotification from '../assets/sounds/notification.mp3'; 275 | 276 | // Button that plays a sound when clicked 277 | function ActionButton({ onClick }) { 278 | return ( 279 | console.error('Sound error:', error)} 284 | > 285 | Action Button 286 | 287 | ); 288 | }`} 289 |
290 | 291 |

Utility Hooks

292 |

Additional hooks for specific sound use cases.

293 | 294 | {`import { useSoundOnChange, useSoundEnabled } from 'react-sounds'; 295 | 296 | // Play a sound when a value changes 297 | function Counter() { 298 | const [count, setCount] = useState(0); 299 | 300 | // Play a sound whenever count changes 301 | useSoundOnChange('ui/increment', count, { volume: 0.5 }); 302 | 303 | return ( 304 | 307 | ); 308 | } 309 | 310 | // Access and control the global sound enabled state 311 | function SoundToggle() { 312 | // Must be used within a SoundProvider 313 | const [enabled, setEnabled] = useSoundEnabled(); 314 | 315 | return ( 316 | 319 | ); 320 | }`} 321 | 322 |
323 | 324 | {/* Advanced Usage */} 325 |
326 |

Advanced Usage

327 | 328 |

Configuring CDN URL

329 |

330 | Configure where built-in sound files are loaded from. Note: This doesn't affect custom sound files that you 331 | import directly. 332 |

333 | 334 | {`import { setCDNUrl, getCDNUrl } from 'react-sounds'; 335 | 336 | // Set a custom CDN base URL for built-in sounds 337 | setCDNUrl('https://your-cdn.com/sounds'); 338 | 339 | // Get the current CDN URL 340 | const currentUrl = getCDNUrl();`} 341 | 342 | 343 |

Sound Enabling/Disabling

344 |

345 | Globally enable or disable all sounds. The setting is automatically persisted in localStorage. 346 |

347 | 348 | {`import { setSoundEnabled, isSoundEnabled } from 'react-sounds'; 349 | 350 | // Check if sounds are enabled 351 | const enabled = isSoundEnabled(); 352 | 353 | // Disable all sounds 354 | setSoundEnabled(false); 355 | 356 | // Re-enable sounds 357 | setSoundEnabled(true);`} 358 | 359 | 360 |

Preloading Sounds

361 |

Preload sounds to ensure they are ready for immediate playback.

362 | 363 | {`import { preloadSounds } from 'react-sounds'; 364 | import customSound from '../assets/sounds/custom.mp3'; 365 | 366 | // Preload multiple sounds at once (both built-in and custom) 367 | preloadSounds(['ui/button_1', 'notification/success', customSound]) 368 | .then(() => console.log('All sounds preloaded')) 369 | .catch((error) => console.error('Error preloading sounds:', error));`} 370 | 371 | 372 |

Custom Sound Options

373 |

Customize sound playback with various options.

374 | 375 | {`import { useSound, playSound, SoundOptions } from 'react-sounds'; 376 | import customExplosion from '../assets/sounds/explosion.mp3'; 377 | 378 | // SoundOptions interface: 379 | // { 380 | // volume?: number; // 0.0 to 1.0 381 | // rate?: number; // playback speed (1.0 is normal) 382 | // loop?: boolean; // whether to loop the sound 383 | // } 384 | 385 | function SoundPlayer() { 386 | const { play } = useSound(customExplosion); 387 | 388 | const playWithOptions = () => { 389 | // Play with custom options 390 | play({ 391 | volume: 0.8, // 80% volume 392 | rate: 1.2, // 20% faster 393 | loop: false // don't loop 394 | }); 395 | 396 | // Direct play with options 397 | playSound('ui/click', { volume: 0.5 }); 398 | }; 399 | 400 | return ; 401 | }`} 402 | 403 | 404 |

CLI Tool for Offline Sounds

405 |

406 | Use the CLI tool to download sounds for offline use or explore available sounds. 407 |

408 | 409 | {`# List all available sounds 410 | npx react-sounds-cli list 411 | 412 | # Download specific sounds for offline use 413 | npx react-sounds-cli pick ui/click notification/success 414 | 415 | # Download sounds to a custom directory 416 | npx react-sounds-cli pick ui/click --output=./public/sounds`} 417 | 418 |
419 | 420 | {/* Type Information */} 421 |
422 |

TypeScript Support

423 |

424 | react-sounds provides TypeScript definitions for all exported functions, hooks, components, and types. 425 |

426 | 427 |

Sound Types

428 |

Sounds can be either built-in sound names or custom imported sound files.

429 | 430 | {`import type { 431 | LibrarySoundName, // Union of all built-in sound categories 432 | UiSoundName, // UI sounds (clicks, toggles, etc.) 433 | GameSoundName, // Game sounds (achievements, actions, etc.) 434 | NotificationSoundName // Notification sounds (alerts, success, etc.) 435 | } from 'react-sounds'; 436 | import customSound from '../assets/sounds/custom.mp3'; 437 | 438 | // Using with type safety 439 | function PlaySounds() { 440 | // For built-in sounds, use the type for type checking 441 | const playUiSound = (sound: UiSoundName) => playSound(sound); 442 | 443 | // For custom sounds, just import and use directly 444 | const playCustomSound = () => playSound(customSound); 445 | 446 | return ( 447 |
448 | 449 | 450 |
451 | ); 452 | }`} 453 |
454 | 455 |

Sound Hook Return Type

456 |

457 | Type definition for what useSound hook 458 | returns. 459 |

460 | 461 | {`import type { SoundHookReturn } from 'react-sounds'; 462 | 463 | // SoundHookReturn contains: 464 | // { 465 | // play: (options?: SoundOptions) => Promise | undefined; 466 | // stop: () => void; 467 | // pause: () => void; 468 | // resume: () => void; 469 | // isPlaying: boolean; 470 | // isLoaded: boolean; 471 | // }`} 472 | 473 |
474 |
475 | ); 476 | }; 477 | 478 | export default DocumentationPage; 479 | -------------------------------------------------------------------------------- /website/src/pages/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { useSound, useSoundEnabled } from "react-sounds"; 4 | import AdvancedSoundDemo from "../components/AdvancedSoundDemo"; 5 | import CodeBlock from "../components/CodeBlock"; 6 | import FeatureCard from "../components/FeatureCard"; 7 | import { cn } from "../utils/cn"; 8 | 9 | // CSS for the heartbeat pulse animation 10 | const heartbeatPulseStyle = ` 11 | @keyframes heartbeatPulse { 12 | 0%, 100% { transform: scale(1.05); } 13 | 10% { transform: scale(1.1); } /* First beat */ 14 | 20% { transform: scale(1.05); } /* Reset after first beat */ 15 | 30% { transform: scale(1.08); } /* Second beat (slightly smaller) */ 16 | 40% { transform: scale(1.05); } /* Reset after second beat */ 17 | } 18 | 19 | .heartbeat-pulse { 20 | animation: heartbeatPulse 0.5s infinite; 21 | } 22 | `; 23 | 24 | interface Feature { 25 | title: string; 26 | description: string; 27 | icon: string; 28 | } 29 | 30 | const HomePage: React.FC = () => { 31 | const hoverSound = useSound("ambient/heartbeat", { loop: true }); 32 | const [soundIsEnabled] = useSoundEnabled(); 33 | const [hasInteracted, setHasInteracted] = useState(false); 34 | const [isPulsing, setIsPulsing] = useState(false); 35 | 36 | // Function to handle hovering over the button 37 | const handleHoverStart = () => { 38 | if (!hoverSound.isPlaying) { 39 | hoverSound.play().catch(() => { 40 | // If the sound fails to play due to locked AudioContext, don't do anything 41 | // It will not unexpectedly play later 42 | }); 43 | } 44 | setIsPulsing(true); 45 | }; 46 | 47 | const handleHoverEnd = () => { 48 | hoverSound.stop(); 49 | setIsPulsing(false); 50 | }; 51 | 52 | // Track user's first click to know they've interacted 53 | const handleFirstInteraction = () => { 54 | setHasInteracted(true); 55 | }; 56 | 57 | const features: Feature[] = [ 58 | { 59 | title: "🔊 Extensive Library", 60 | description: "Access curated sounds organized by category (UI, notification, game, etc.).", 61 | icon: "🔊", 62 | }, 63 | { 64 | title: "🪶 Lightweight", 65 | description: 66 | "Only JS wrappers included in the package. Audio files are hosted on a CDN to keep your bundle size small.", 67 | icon: "🪶", 68 | }, 69 | { 70 | title: "🔄 Lazy Loading", 71 | description: "Sounds are fetched efficiently only when they are needed, improving initial load performance.", 72 | icon: "🔄", 73 | }, 74 | { 75 | title: "📦 Offline Support", 76 | description: "Easily download and bundle sounds for self-hosting using the included CLI tool.", 77 | icon: "📦", 78 | }, 79 | { 80 | title: "🎯 Simple API", 81 | description: 82 | "Integrate sounds effortlessly with easy-to-use hooks (like useSound) and components (like SoundButton).", 83 | icon: "🎯", 84 | }, 85 | { 86 | title: "⚙️ Configurable", 87 | description: "Customize CDN URLs, preload sounds, enable/disable sounds globally, and control playback options.", 88 | icon: "⚙️", 89 | }, 90 | ]; 91 | 92 | return ( 93 |
94 | {/* Style tag for custom animations */} 95 |