├── README.md ├── SoundOverrideComponent.tsx ├── audioStore.ts ├── index.tsx ├── styles.css └── types.ts /README.md: -------------------------------------------------------------------------------- 1 | **UPDATE: New and improved UI. Added ability to save/upload multiple custom sounds.** 2 | > [!TIP] 3 | > **If you run into any issues, please let me know on [Discord](https://discord.gg/jHDJaW9Gyz)** 4 | # Custom Sounds (Vencord) 5 | This is a Vencord plugin that allows you to change any native Discord sound. Features custom audio uploads, built-in Discord presets, volume control, sound preview, and settings import/export. 6 | 7 | ## DOWNLOAD INSTRUCTIONS 8 | You can either __clone__ the repository OR __manually install__ it by downloading it as a zip file.
9 | > [!WARNING] 10 | > Make sure you have the Vencord [developer build](https://docs.vencord.dev/installing/) installed.
11 | 12 | ### CLONE INSTALLATION 13 | The cloning installation guide can be found [here](https://discord.com/channels/1015060230222131221/1257038407503446176/1257038407503446176) or via [the official Vencord Docs](https://docs.vencord.dev/installing/custom-plugins/). 14 | 15 | ### MANUAL INSTALLATION 16 | > [!IMPORTANT] 17 | > Inside the `Vencord` folder should be a folder called `src`. If you haven't already, create a folder called `userplugins` inside the `src` folder. 18 | 1. Click the green `<> Code` button at the top right of the repository and select `Download ZIP` 19 | 2. Unzip the downloaded ZIP file into the `userplugins` folder. 20 | 3. Ensure it's structured as `src/userplugins/customSounds` or `src/userplugins/customSounds-main` 21 | 5. Run `pnpm build` in the terminal (command prompt/CMD) and the plugin should be added. 22 | -------------------------------------------------------------------------------- /SoundOverrideComponent.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Vencord, a Discord client mod 3 | * Copyright (c) 2025 Vendicated and contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { classNameFactory } from "@api/Styles"; 8 | import { makeRange } from "@components/PluginSettings/components"; 9 | import { Margins } from "@utils/margins"; 10 | import { useForceUpdater } from "@utils/react"; 11 | import { findByCodeLazy, findLazy } from "@webpack"; 12 | import { Button, Card, Forms, React, Select, showToast, Slider, Switch } from "@webpack/common"; 13 | import { ComponentType, Ref, SyntheticEvent } from "react"; 14 | 15 | import { deleteAudio, getAllAudio, saveAudio, StoredAudioFile } from "./audioStore"; 16 | import { ensureDataURICached } from "./index"; 17 | import { SoundOverride, SoundPlayer, SoundType } from "./types"; 18 | 19 | type FileInput = ComponentType<{ 20 | ref: Ref; 21 | onChange: (e: SyntheticEvent) => void; 22 | multiple?: boolean; 23 | filters?: { name?: string; extensions: string[]; }[]; 24 | }>; 25 | 26 | const AUDIO_EXTENSIONS = ["mp3", "wav", "ogg", "m4a", "aac", "flac", "webm", "wma", "mp4"]; 27 | const cl = classNameFactory("vc-custom-sounds-"); 28 | const playSound: (id: string) => SoundPlayer = findByCodeLazy(".playWithListener().then"); 29 | const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef); 30 | 31 | const capitalizeWords = (str: string) => 32 | str.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()); 33 | 34 | export function SoundOverrideComponent({ type, override, onChange }: { 35 | type: SoundType; 36 | override: SoundOverride; 37 | onChange: () => Promise; 38 | }) { 39 | const fileInputRef = React.useRef(null); 40 | const update = useForceUpdater(); 41 | const sound = React.useRef(null); 42 | const [files, setFiles] = React.useState>({}); 43 | 44 | React.useEffect(() => { 45 | getAllAudio().then(setFiles); 46 | }, []); 47 | 48 | const saveAndNotify = async () => { 49 | await onChange(); 50 | update(); 51 | }; 52 | 53 | const previewSound = async () => { 54 | sound.current?.stop(); 55 | 56 | if (!override.enabled) { 57 | sound.current = playSound(type.id); 58 | return; 59 | } 60 | 61 | const { selectedSound } = override; 62 | 63 | if (selectedSound === "custom" && override.selectedFileId) { 64 | try { 65 | const dataUri = await ensureDataURICached(override.selectedFileId); 66 | 67 | if (!dataUri || !dataUri.startsWith("data:audio/")) { 68 | showToast("No custom sound file available for preview"); 69 | return; 70 | } 71 | 72 | const audio = new Audio(dataUri); 73 | audio.volume = override.volume / 100; 74 | 75 | audio.onerror = e => { 76 | console.error("[CustomSounds] Error playing custom audio:", e); 77 | showToast("Error playing custom sound. File may be corrupted."); 78 | }; 79 | 80 | await audio.play(); 81 | sound.current = { 82 | play: () => audio.play(), 83 | pause: () => audio.pause(), 84 | stop: () => { 85 | audio.pause(); 86 | audio.currentTime = 0; 87 | }, 88 | loop: () => { audio.loop = true; } 89 | }; 90 | } catch (error) { 91 | console.error("[CustomSounds] Error in previewSound:", error); 92 | showToast("Error playing sound."); 93 | } 94 | } else if (selectedSound === "default") { 95 | sound.current = playSound(type.id); 96 | } else { 97 | sound.current = playSound(selectedSound); 98 | } 99 | }; 100 | 101 | const uploadFile = async (event: React.ChangeEvent) => { 102 | const file = event.target.files?.[0]; 103 | if (!file) return; 104 | 105 | const fileExtension = file.name.split(".").pop()?.toLowerCase(); 106 | if (!fileExtension || !AUDIO_EXTENSIONS.includes(fileExtension)) { 107 | showToast("Invalid file type. Please upload an audio file."); 108 | event.target.value = ""; 109 | return; 110 | } 111 | 112 | try { 113 | showToast("Uploading file..."); 114 | const id = await saveAudio(file); 115 | 116 | const savedFiles = await getAllAudio(); 117 | setFiles(savedFiles); 118 | 119 | override.selectedFileId = id; 120 | override.selectedSound = "custom"; 121 | 122 | await ensureDataURICached(id); 123 | await saveAndNotify(); 124 | 125 | showToast(`File uploaded successfully: ${file.name}`); 126 | } catch (error) { 127 | console.error("[CustomSounds] Error uploading file:", error); 128 | showToast(`Error uploading file: ${error}`); 129 | } 130 | 131 | event.target.value = ""; 132 | }; 133 | 134 | const deleteFile = async (id: string) => { 135 | try { 136 | await deleteAudio(id); 137 | const updated = await getAllAudio(); 138 | setFiles(updated); 139 | 140 | if (override.selectedFileId === id) { 141 | override.selectedFileId = undefined; 142 | override.selectedSound = "default"; 143 | await saveAndNotify(); 144 | } else { 145 | update(); 146 | } 147 | showToast("File deleted successfully"); 148 | } catch (error) { 149 | console.error("[CustomSounds] Error deleting file:", error); 150 | showToast("Error deleting file."); 151 | } 152 | }; 153 | 154 | const customFileOptions = Object.entries(files) 155 | .filter(([id, file]) => !!id && !!file?.name) 156 | .map(([id, file]) => ({ 157 | value: id, 158 | label: file.name 159 | })); 160 | 161 | return ( 162 | 163 | { 166 | console.log(`[CustomSounds] Setting ${type.id} enabled to:`, val); 167 | 168 | override.enabled = val; 169 | 170 | if (val && override.selectedSound === "custom" && override.selectedFileId) { 171 | try { 172 | await ensureDataURICached(override.selectedFileId); 173 | } catch (error) { 174 | console.error(`[CustomSounds] Failed to cache data URI for ${type.id}:`, error); 175 | showToast("Error loading custom sound file"); 176 | } 177 | } 178 | 179 | await saveAndNotify(); 180 | console.log("[CustomSounds] After setting enabled, override.enabled =", override.enabled); 181 | }} 182 | className={Margins.bottom16} 183 | hideBorder 184 | > 185 | {type.name} 186 | 187 | 188 | {override.enabled && ( 189 | <> 190 |
191 | 197 | 203 |
204 | 205 | Volume 206 | { 210 | override.volume = val; 211 | saveAndNotify(); 212 | }} 213 | className={Margins.bottom16} 214 | disabled={!override.enabled} 215 | /> 216 | 217 | Sound Source 218 | v === (override.selectedFileId || "")} 252 | select={async id => { 253 | if (!id) { 254 | override.selectedFileId = undefined; 255 | } else { 256 | override.selectedFileId = id; 257 | await ensureDataURICached(id); 258 | } 259 | 260 | await saveAndNotify(); 261 | }} 262 | serialize={opt => opt.value} 263 | className={Margins.bottom8} 264 | /> 265 | 272 |
273 | 279 | 280 | {override.selectedFileId && files[override.selectedFileId] && ( 281 | 287 | )} 288 |
289 | 290 | )} 291 | 292 | )} 293 |
294 | ); 295 | } 296 | -------------------------------------------------------------------------------- /audioStore.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vencord, a Discord client mod 3 | * Copyright (c) 2025 Vendicated and contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { get, set } from "@api/DataStore"; 8 | 9 | const STORAGE_KEY = "ScattrdCustomSounds"; 10 | 11 | export interface StoredAudioFile { 12 | id: string; 13 | name: string; 14 | buffer: ArrayBuffer; 15 | type: string; 16 | dataUri?: string; 17 | } 18 | 19 | export async function saveAudio(file: File): Promise { 20 | const id = crypto.randomUUID(); 21 | const buffer = await file.arrayBuffer(); 22 | 23 | const dataUri = await generateDataURI(buffer, file.type, file.name); 24 | 25 | const current = (await get(STORAGE_KEY)) ?? {}; 26 | current[id] = { 27 | id, 28 | name: file.name, 29 | buffer, 30 | type: file.type, 31 | dataUri 32 | }; 33 | await set(STORAGE_KEY, current); 34 | return id; 35 | } 36 | 37 | export async function getAllAudio(): Promise> { 38 | return (await get(STORAGE_KEY)) ?? {}; 39 | } 40 | 41 | async function generateDataURI(buffer: ArrayBuffer, type: string, name: string): Promise { 42 | try { 43 | let mimeType = type || "audio/mpeg"; 44 | 45 | if (!mimeType || mimeType === "application/octet-stream") { 46 | if (name) { 47 | const extension = name.split(".").pop()?.toLowerCase(); 48 | switch (extension) { 49 | case "ogg": mimeType = "audio/ogg"; break; 50 | case "mp3": mimeType = "audio/mpeg"; break; 51 | case "wav": mimeType = "audio/wav"; break; 52 | case "m4a": 53 | case "mp4": mimeType = "audio/mp4"; break; 54 | case "flac": mimeType = "audio/flac"; break; 55 | case "aac": mimeType = "audio/aac"; break; 56 | case "webm": mimeType = "audio/webm"; break; 57 | case "wma": mimeType = "audio/x-ms-wma"; break; 58 | default: mimeType = "audio/mpeg"; 59 | } 60 | } 61 | } 62 | 63 | const uint8Array = new Uint8Array(buffer); 64 | const blob = new Blob([uint8Array], { type: mimeType }); 65 | 66 | return new Promise((resolve, reject) => { 67 | const reader = new FileReader(); 68 | reader.onloadend = () => resolve(reader.result as string); 69 | reader.onerror = reject; 70 | reader.readAsDataURL(blob); 71 | }); 72 | } catch (error) { 73 | console.error("[CustomSounds] Error generating data URI:", error); 74 | 75 | const uint8Array = new Uint8Array(buffer); 76 | let binary = ""; 77 | const chunkSize = 8192; 78 | 79 | for (let i = 0; i < uint8Array.length; i += chunkSize) { 80 | const chunk = uint8Array.slice(i, i + chunkSize); 81 | binary += String.fromCharCode(...chunk); 82 | } 83 | 84 | const base64 = btoa(binary); 85 | return `data:${type || "audio/mpeg"};base64,${base64}`; 86 | } 87 | } 88 | 89 | export async function getAudioDataURI(id: string): Promise { 90 | const all = await getAllAudio(); 91 | const entry = all[id]; 92 | if (!entry) return undefined; 93 | 94 | if (entry.dataUri) { 95 | return entry.dataUri; 96 | } 97 | 98 | console.log(`[CustomSounds] No cached data URI for ${id}, generating...`); 99 | const dataUri = await generateDataURI(entry.buffer, entry.type, entry.name); 100 | 101 | const current = await getAllAudio(); 102 | current[id].dataUri = dataUri; 103 | await set(STORAGE_KEY, current); 104 | 105 | return dataUri; 106 | } 107 | 108 | export async function deleteAudio(id: string): Promise { 109 | const all = await getAllAudio(); 110 | delete all[id]; 111 | await set(STORAGE_KEY, all); 112 | } 113 | -------------------------------------------------------------------------------- /index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Vencord, a Discord client mod 3 | * Copyright (c) 2025 Vendicated and contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import "./styles.css"; 8 | 9 | import { get as getFromDataStore } from "@api/DataStore"; 10 | import { definePluginSettings } from "@api/Settings"; 11 | import { classNameFactory } from "@api/Styles"; 12 | import { Devs } from "@utils/constants"; 13 | import definePlugin, { OptionType, StartAt } from "@utils/types"; 14 | import { Button, Forms, React, showToast, TextInput } from "@webpack/common"; 15 | 16 | import { getAllAudio, getAudioDataURI } from "./audioStore"; 17 | import { SoundOverrideComponent } from "./SoundOverrideComponent"; 18 | import { makeEmptyOverride, seasonalSounds, SoundOverride, soundTypes } from "./types"; 19 | 20 | const cl = classNameFactory("vc-custom-sounds-"); 21 | 22 | const allSoundTypes = soundTypes || []; 23 | 24 | const AUDIO_STORE_KEY = "ScattrdCustomSounds"; 25 | 26 | const dataUriCache = new Map(); 27 | 28 | function getOverride(id: string): SoundOverride { 29 | const stored = settings.store[id]; 30 | if (!stored) return makeEmptyOverride(); 31 | 32 | if (typeof stored === "object") return stored; 33 | 34 | try { 35 | return JSON.parse(stored); 36 | } catch { 37 | return makeEmptyOverride(); 38 | } 39 | } 40 | 41 | function setOverride(id: string, override: SoundOverride) { 42 | settings.store[id] = JSON.stringify(override); 43 | } 44 | 45 | export function getCustomSoundURL(id: string): string | null { 46 | const override = getOverride(id); 47 | 48 | if (!override?.enabled) { 49 | return null; 50 | } 51 | 52 | if (override.selectedSound === "custom" && override.selectedFileId) { 53 | const dataUri = dataUriCache.get(override.selectedFileId); 54 | if (dataUri) { 55 | console.log(`[CustomSounds] Returning cached data URI for ${id}`); 56 | return dataUri; 57 | } else { 58 | console.warn(`[CustomSounds] No cached data URI for ${id} with file ID ${override.selectedFileId}`); 59 | return null; 60 | } 61 | } 62 | 63 | if (override.selectedSound !== "default" && override.selectedSound !== "custom") { 64 | if (override.selectedSound in seasonalSounds) { 65 | return seasonalSounds[override.selectedSound]; 66 | } 67 | 68 | const soundType = allSoundTypes.find(t => t.id === id); 69 | if (soundType?.seasonal) { 70 | const seasonalId = soundType.seasonal.find(seasonalId => 71 | seasonalId.startsWith(`${override.selectedSound}_`) 72 | ); 73 | if (seasonalId && seasonalId in seasonalSounds) { 74 | return seasonalSounds[seasonalId]; 75 | } 76 | } 77 | } 78 | 79 | return null; 80 | } 81 | 82 | export async function ensureDataURICached(fileId: string): Promise { 83 | if (dataUriCache.has(fileId)) { 84 | return dataUriCache.get(fileId)!; 85 | } 86 | 87 | try { 88 | const dataUri = await getAudioDataURI(fileId); 89 | if (dataUri) { 90 | dataUriCache.set(fileId, dataUri); 91 | console.log(`[CustomSounds] Cached data URI for file ${fileId}`); 92 | return dataUri; 93 | } 94 | } catch (error) { 95 | console.error(`[CustomSounds] Error generating data URI for ${fileId}:`, error); 96 | } 97 | 98 | return null; 99 | } 100 | 101 | export async function refreshDataURI(id: string): Promise { 102 | const override = getOverride(id); 103 | if (!override?.selectedFileId) { 104 | console.log(`[CustomSounds] refreshDataURI called for ${id} but no selectedFileId`); 105 | return; 106 | } 107 | 108 | console.log(`[CustomSounds] Refreshing data URI for ${id} with file ID ${override.selectedFileId}`); 109 | 110 | const dataUri = await ensureDataURICached(override.selectedFileId); 111 | if (dataUri) { 112 | console.log(`[CustomSounds] Successfully cached data URI for ${id} (length: ${dataUri.length})`); 113 | } else { 114 | console.error(`[CustomSounds] Failed to cache data URI for ${id}`); 115 | } 116 | } 117 | 118 | async function preloadDataURIs() { 119 | console.log("[CustomSounds] Preloading data URIs into memory cache..."); 120 | 121 | for (const soundType of allSoundTypes) { 122 | const override = getOverride(soundType.id); 123 | if (override?.enabled && override.selectedSound === "custom" && override.selectedFileId) { 124 | try { 125 | await ensureDataURICached(override.selectedFileId); 126 | console.log(`[CustomSounds] Preloaded data URI for ${soundType.id}`); 127 | } catch (error) { 128 | console.error(`[CustomSounds] Failed to preload data URI for ${soundType.id}:`, error); 129 | } 130 | } 131 | } 132 | 133 | console.log(`[CustomSounds] Memory cache contains ${dataUriCache.size} data URIs`); 134 | } 135 | 136 | export async function debugCustomSounds() { 137 | console.log("[CustomSounds] === DEBUG INFO ==="); 138 | 139 | const rawDataStore = await getFromDataStore(AUDIO_STORE_KEY); 140 | console.log("[CustomSounds] Raw DataStore content:", rawDataStore); 141 | 142 | const allFiles = await getAllAudio(); 143 | console.log(`[CustomSounds] Stored files: ${Object.keys(allFiles).length}`); 144 | 145 | let totalBufferSize = 0; 146 | let totalDataUriSize = 0; 147 | 148 | for (const [id, file] of Object.entries(allFiles)) { 149 | const bufferSize = file.buffer?.byteLength || 0; 150 | const dataUriSize = file.dataUri?.length || 0; 151 | totalBufferSize += bufferSize; 152 | totalDataUriSize += dataUriSize; 153 | 154 | console.log(`[CustomSounds] File ${id}:`, { 155 | name: file.name, 156 | type: file.type, 157 | bufferSize: `${(bufferSize / 1024).toFixed(1)}KB`, 158 | hasValidBuffer: file.buffer instanceof ArrayBuffer, 159 | hasDataUri: !!file.dataUri, 160 | dataUriSize: `${(dataUriSize / 1024).toFixed(1)}KB` 161 | }); 162 | } 163 | 164 | console.log(`[CustomSounds] Total storage - Buffers: ${(totalBufferSize / 1024).toFixed(1)}KB, DataURIs: ${(totalDataUriSize / 1024).toFixed(1)}KB`); 165 | 166 | console.log(`[CustomSounds] Memory cache contains ${dataUriCache.size} data URIs`); 167 | 168 | console.log("[CustomSounds] Settings store structure:", Object.keys(settings.store)); 169 | 170 | console.log("[CustomSounds] Sound override status:"); 171 | let enabledCount = 0; 172 | let totalSettingsSize = 0; 173 | 174 | for (const [soundId, storedValue] of Object.entries(settings.store)) { 175 | if (soundId === "overrides") continue; 176 | 177 | const override = getOverride(soundId); 178 | const settingsSize = JSON.stringify(override).length; 179 | totalSettingsSize += settingsSize; 180 | 181 | console.log(`[CustomSounds] ${soundId}:`, { 182 | enabled: override.enabled, 183 | selectedSound: override.selectedSound, 184 | selectedFileId: override.selectedFileId, 185 | volume: override.volume, 186 | settingsSize: `${settingsSize}B` 187 | }); 188 | 189 | if (override.enabled) enabledCount++; 190 | } 191 | 192 | console.log(`[CustomSounds] Total enabled overrides: ${enabledCount}`); 193 | console.log(`[CustomSounds] Estimated settings size: ${(totalSettingsSize / 1024).toFixed(1)}KB`); 194 | console.log("[CustomSounds] === END DEBUG ==="); 195 | } 196 | 197 | const soundSettings = Object.fromEntries( 198 | allSoundTypes.map(type => [ 199 | type.id, 200 | { 201 | type: OptionType.STRING, 202 | description: `Override for ${type.name}`, 203 | default: JSON.stringify(makeEmptyOverride()), 204 | hidden: true 205 | } 206 | ]) 207 | ); 208 | 209 | const settings = definePluginSettings({ 210 | ...soundSettings, 211 | overrides: { 212 | type: OptionType.COMPONENT, 213 | description: "", 214 | component: () => { 215 | const [resetTrigger, setResetTrigger] = React.useState(0); 216 | const [searchQuery, setSearchQuery] = React.useState(""); 217 | const fileInputRef = React.useRef(null); 218 | 219 | React.useEffect(() => { 220 | allSoundTypes.forEach(type => { 221 | if (!settings.store[type.id]) { 222 | setOverride(type.id, makeEmptyOverride()); 223 | } 224 | }); 225 | }, []); 226 | 227 | const resetOverrides = () => { 228 | allSoundTypes.forEach(type => { 229 | setOverride(type.id, makeEmptyOverride()); 230 | }); 231 | dataUriCache.clear(); 232 | setResetTrigger(prev => prev + 1); 233 | showToast("All overrides reset successfully!"); 234 | }; 235 | 236 | const triggerFileUpload = () => { 237 | fileInputRef.current?.click(); 238 | }; 239 | 240 | const handleSettingsUpload = (event: React.ChangeEvent) => { 241 | const file = event.target.files?.[0]; 242 | if (file) { 243 | const reader = new FileReader(); 244 | reader.onload = async (e: ProgressEvent) => { 245 | try { 246 | resetOverrides(); 247 | const imported = JSON.parse(e.target?.result as string); 248 | 249 | if (imported.overrides && Array.isArray(imported.overrides)) { 250 | imported.overrides.forEach((setting: any) => { 251 | if (setting.id) { 252 | const override: SoundOverride = { 253 | enabled: setting.enabled ?? false, 254 | selectedSound: setting.selectedSound ?? "default", 255 | selectedFileId: setting.selectedFileId ?? undefined, 256 | volume: setting.volume ?? 100, 257 | useFile: false 258 | }; 259 | setOverride(setting.id, override); 260 | } 261 | }); 262 | } 263 | 264 | setResetTrigger(prev => prev + 1); 265 | showToast("Settings imported successfully!"); 266 | } catch (error) { 267 | console.error("Error importing settings:", error); 268 | showToast("Error importing settings. Check console for details."); 269 | } 270 | }; 271 | 272 | reader.readAsText(file); 273 | event.target.value = ""; 274 | } 275 | }; 276 | 277 | const downloadSettings = async () => { 278 | const overrides = allSoundTypes.map(type => { 279 | const override = getOverride(type.id); 280 | return { 281 | id: type.id, 282 | enabled: override.enabled, 283 | selectedSound: override.selectedSound, 284 | selectedFileId: override.selectedFileId ?? undefined, 285 | volume: override.volume 286 | }; 287 | }).filter(o => o.enabled || o.selectedSound !== "default"); 288 | 289 | const exportPayload = { 290 | overrides, 291 | __note: "Audio files are not included in exports and will need to be re-uploaded after import" 292 | }; 293 | 294 | const blob = new Blob([JSON.stringify(exportPayload, null, 2)], { type: "application/json" }); 295 | const url = URL.createObjectURL(blob); 296 | const a = document.createElement("a"); 297 | a.href = url; 298 | a.download = "customSounds-settings.json"; 299 | a.click(); 300 | URL.revokeObjectURL(url); 301 | 302 | showToast(`Exported ${overrides.length} settings (audio files not included)`); 303 | }; 304 | 305 | const filteredSoundTypes = allSoundTypes.filter(type => 306 | type.name.toLowerCase().includes(searchQuery.toLowerCase()) || 307 | type.id.toLowerCase().includes(searchQuery.toLowerCase()) 308 | ); 309 | 310 | return ( 311 |
312 |
313 | 314 | 315 | 316 | 317 | 324 |
325 | 326 |
327 | Search Sounds 328 | setSearchQuery(e)} 331 | placeholder="Search by name or ID" 332 | /> 333 |
334 | 335 |
336 | {filteredSoundTypes.map(type => { 337 | const currentOverride = getOverride(type.id); 338 | 339 | return ( 340 | { 345 | 346 | setOverride(type.id, currentOverride); 347 | 348 | if (currentOverride.enabled && currentOverride.selectedSound === "custom" && currentOverride.selectedFileId) { 349 | try { 350 | await ensureDataURICached(currentOverride.selectedFileId); 351 | } catch (error) { 352 | console.error(`[CustomSounds] Failed to cache data URI for ${type.id}:`, error); 353 | showToast("Error loading custom sound file"); 354 | } 355 | } 356 | 357 | console.log(`[CustomSounds] Settings saved for ${type.id}:`, currentOverride); 358 | }} 359 | /> 360 | ); 361 | })} 362 |
363 |
364 | ); 365 | } 366 | } 367 | }); 368 | 369 | export function isOverriden(id: string): boolean { 370 | return !!getOverride(id)?.enabled; 371 | } 372 | 373 | export function findOverride(id: string): SoundOverride | null { 374 | const override = getOverride(id); 375 | return override?.enabled ? override : null; 376 | } 377 | 378 | export default definePlugin({ 379 | name: "CustomSounds", 380 | description: "Customize Discord's sounds.", 381 | authors: [Devs.ScattrdBlade, Devs.TheKodeToad], 382 | patches: [ 383 | { 384 | find: 'Error("could not play audio")', 385 | replacement: [ 386 | { 387 | match: /(?<=new Audio;\i\.src=)\i\([0-9]+\)\("\.\/"\.concat\(this\.name,"\.mp3"\)\)/, 388 | replace: "(() => { const customUrl = $self.getCustomSoundURL(this.name); return customUrl || $& })()" 389 | }, 390 | { 391 | match: /Math.min\(\i\.\i\.getOutputVolume\(\)\/100\*this\._volume/, 392 | replace: "$& * ($self.findOverride(this.name)?.volume ?? 100) / 100" 393 | } 394 | ] 395 | }, 396 | { 397 | find: ".playWithListener().then", 398 | replacement: { 399 | match: /\i\.\i\.getSoundpack\(\)/, 400 | replace: '$self.isOverriden(arguments[0]) ? "classic" : $&' 401 | } 402 | } 403 | ], 404 | settings, 405 | findOverride, 406 | isOverriden, 407 | getCustomSoundURL, 408 | refreshDataURI, 409 | ensureDataURICached, 410 | debugCustomSounds, 411 | startAt: StartAt.Init, 412 | 413 | async start() { 414 | console.log("[CustomSounds] Plugin starting..."); 415 | 416 | try { 417 | await preloadDataURIs(); 418 | console.log("[CustomSounds] Startup complete"); 419 | } catch (error) { 420 | console.error("[CustomSounds] Startup failed:", error); 421 | } 422 | } 423 | }); 424 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .vc-custom-sounds-card { 2 | padding: 1em 1em 0; 3 | } 4 | 5 | .vc-custom-sounds-id { 6 | color: var(--text-muted); 7 | } 8 | 9 | .vc-custom-sounds-upload { 10 | display: inline; 11 | } 12 | 13 | .vc-custom-sounds-seasonal-title { 14 | margin-top: 16px; 15 | padding-top: 8px; 16 | border-top: 1px solid var(--background-modifier-accent); 17 | } 18 | 19 | .vc-custom-sounds-sound-section { 20 | margin-bottom: 16px; 21 | } 22 | 23 | .vc-custom-sounds-url-input { 24 | width: 100%; 25 | padding: 8px; 26 | background: var(--input-background); 27 | border: none; 28 | border-radius: 3px; 29 | color: var(--text-normal); 30 | } 31 | 32 | .vc-custom-sounds-buttons { 33 | display: flex; 34 | gap: 8px; 35 | margin-bottom: 16px; 36 | } 37 | 38 | .vc-custom-sounds-search { 39 | margin-bottom: 16px; 40 | } 41 | 42 | .vc-custom-sounds-search input { 43 | width: 100%; 44 | padding: 8px; 45 | background: var(--input-background); 46 | border: none; 47 | border-radius: 3px; 48 | color: var(--text-normal); 49 | } 50 | 51 | .vc-custom-sounds-sounds-list { 52 | display: flex; 53 | flex-direction: column; 54 | gap: 16px; 55 | } 56 | 57 | .vc-custom-sounds-preview-controls { 58 | display: flex; 59 | } 60 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vencord, a Discord client mod 3 | * Copyright (c) 2025 Vendicated and contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | export interface SoundType { 8 | name: string; 9 | id: string; 10 | seasonal?: string[]; 11 | } 12 | 13 | export interface SoundOverride { 14 | enabled: boolean; 15 | selectedSound: string; 16 | volume: number; 17 | useFile: boolean; 18 | selectedFileId?: string; 19 | } 20 | 21 | export interface SoundPlayer { 22 | loop(): void; 23 | play(): void; 24 | pause(): void; 25 | stop(): void; 26 | } 27 | 28 | export const seasonalSounds = { 29 | "halloween_call_calling": "https://canary.discord.com/assets/0950a7ea4f1dd037870b.mp3", 30 | "winter_call_calling": "https://canary.discord.com/assets/7b945e7be3f86c5b7c82.mp3", 31 | "halloween_call_ringing": "https://canary.discord.com/assets/1b883b366ae11a303b82.mp3", 32 | "winter_call_ringing": "https://canary.discord.com/assets/e087eb83aaa4c43a44bc.mp3", 33 | "call_ringing_beat": "https://canary.discord.com/assets/3b3a2f5f29b9cb656efb.mp3", 34 | "call_ringing_snow_halation": "https://canary.discord.com/assets/99b1d8a6fe0b95e99827.mp3", 35 | "call_ringing_snowsgiving": "https://canary.discord.com/assets/54527e70cf0ddaeff76f.mp3", 36 | "halloween_deafen": "https://canary.discord.com/assets/c4aedda3b528df50221c.mp3", 37 | "winter_deafen": "https://canary.discord.com/assets/9bb77985afdb60704817.mp3", 38 | "halloween_disconnect": "https://canary.discord.com/assets/ca7d2e46cb5a16819aff.mp3", 39 | "winter_disconnect": "https://canary.discord.com/assets/ec5d85405877c27caeda.mp3", 40 | "halloween_message1": "https://canary.discord.com/assets/e386c839fb98675c6a79.mp3", 41 | "halloween_mute": "https://canary.discord.com/assets/ee7fdadf4c714eed6254.mp3", 42 | "winter_mute": "https://canary.discord.com/assets/6d7616e08466ab9f1c6d.mp3", 43 | "halloween_undeafen": "https://canary.discord.com/assets/045e5b9608df1607e0cf.mp3", 44 | "winter_undeafen": "https://canary.discord.com/assets/fa8da1499894ecac36c7.mp3", 45 | "halloween_unmute": "https://canary.discord.com/assets/260c581568eacca03f7e.mp3", 46 | "winter_unmute": "https://canary.discord.com/assets/9dbfb1c211e3815cd7b1.mp3", 47 | "halloween_user_join": "https://canary.discord.com/assets/80cf806f45467a5898cd.mp3", 48 | "winter_user_join": "https://canary.discord.com/assets/badc42c2a9063b4a962c.mp3", 49 | "halloween_user_leave": "https://canary.discord.com/assets/f407ad88a1dc40541769.mp3", 50 | "winter_user_leave": "https://canary.discord.com/assets/ec3d9eaea30b33e16da6.mp3" 51 | } as const; 52 | 53 | export const soundTypes: readonly SoundType[] = [ 54 | { name: "Activity End", id: "activity_end" }, 55 | { name: "Activity Launch", id: "activity_launch" }, 56 | { name: "Activity User Join", id: "activity_user_join" }, 57 | { name: "Activity User Left", id: "activity_user_left" }, 58 | { name: "ASMR Message", id: "asmr_message1" }, 59 | { name: "Bit Message", id: "bit_message1" }, 60 | { name: "Bop Message", id: "bop_message1" }, 61 | { name: "Call Calling", id: "call_calling", seasonal: ["halloween_call_calling", "winter_call_calling"] }, 62 | { 63 | name: "Call Ringing", 64 | id: "call_ringing", 65 | seasonal: [ 66 | "halloween_call_ringing", 67 | "winter_call_ringing", 68 | "call_ringing_beat", 69 | "call_ringing_snow_halation", 70 | "call_ringing_snowsgiving" 71 | ] 72 | }, 73 | { name: "Clip Error", id: "clip_error" }, 74 | { name: "Clip Save", id: "clip_save" }, 75 | { name: "DDR Down", id: "ddr-down" }, 76 | { name: "DDR Left", id: "ddr-left" }, 77 | { name: "DDR Right", id: "ddr-right" }, 78 | { name: "DDR Up", id: "ddr-up" }, 79 | { name: "Deafen", id: "deafen", seasonal: ["halloween_deafen", "winter_deafen"] }, 80 | { name: "Discodo", id: "discodo" }, 81 | { name: "Disconnect", id: "disconnect", seasonal: ["halloween_disconnect", "winter_disconnect"] }, 82 | { name: "Ducky Message", id: "ducky_message1" }, 83 | { name: "Hang Status Select", id: "hang_status_select" }, 84 | { name: "Highfive Clap", id: "highfive_clap" }, 85 | { name: "Highfive Whistle", id: "highfive_whistle" }, 86 | { name: "Human Man", id: "human_man" }, 87 | { name: "LoFi Message", id: "lofi_message1" }, 88 | { name: "Mention 1", id: "mention1" }, 89 | { name: "Mention 2", id: "mention2" }, 90 | { name: "Mention 3", id: "mention3" }, 91 | { name: "Message 1", id: "message1", seasonal: ["halloween_message1"] }, 92 | { name: "Message 2", id: "message2" }, 93 | { name: "Message 3", id: "message3" }, 94 | { name: "Mute", id: "mute", seasonal: ["halloween_mute", "winter_mute"] }, 95 | { name: "Overlay Unlock", id: "overlayunlock" }, 96 | { name: "Poggermode Achievement", id: "poggermode_achievement_unlock" }, 97 | { name: "Poggermode Applause", id: "poggermode_applause" }, 98 | { name: "Poggermode Enabled", id: "poggermode_enabled" }, 99 | { name: "Poggermode Message", id: "poggermode_message_send" }, 100 | { name: "PTT Start", id: "ptt_start" }, 101 | { name: "PTT Stop", id: "ptt_stop" }, 102 | { name: "Reconnect", id: "reconnect" }, 103 | { name: "Robot Man", id: "robot_man" }, 104 | { name: "Stage Waiting", id: "stage_waiting" }, 105 | { name: "Stream Ended", id: "stream_ended" }, 106 | { name: "Stream Started", id: "stream_started" }, 107 | { name: "Stream User Joined", id: "stream_user_joined" }, 108 | { name: "Stream User Left", id: "stream_user_left" }, 109 | { name: "Success", id: "success" }, 110 | { name: "Undeafen", id: "undeafen", seasonal: ["halloween_undeafen", "winter_undeafen"] }, 111 | { name: "Unmute", id: "unmute", seasonal: ["halloween_unmute", "winter_unmute"] }, 112 | { name: "User Join", id: "user_join", seasonal: ["halloween_user_join", "winter_user_join"] }, 113 | { name: "User Leave", id: "user_leave", seasonal: ["halloween_user_leave", "winter_user_leave"] }, 114 | { name: "User Moved", id: "user_moved" }, 115 | { name: "Vibing Wumpus", id: "vibing_wumpus" } 116 | ] as const; 117 | 118 | export function makeEmptyOverride(): SoundOverride { 119 | return { 120 | enabled: false, 121 | selectedSound: "default", 122 | volume: 100, 123 | useFile: false, 124 | selectedFileId: undefined 125 | }; 126 | } 127 | --------------------------------------------------------------------------------