diff --git a/src/equicordplugins/animalese/README.md b/src/equicordplugins/animalese/README.md new file mode 100644 index 000000000..02a59c96b --- /dev/null +++ b/src/equicordplugins/animalese/README.md @@ -0,0 +1,21 @@ +## vencord-animalese +animalese yapping in your discord client + +> [!WARNING] +> don't go to Vencord support if you can't install this. + +### demo + +https://github.com/user-attachments/assets/3f1fa925-bbd2-451d-b63e-ea14dbe4be44 + +### settings + +##### settings used in the video +![CleanShot 2024-12-26 at 22 11 49@2x](https://github.com/user-attachments/assets/35895fcc-d703-42cb-b0ef-bdef39ab15be) + + +you can change the settings in the settings but otherwise, enjoy the yap + +### credits + +- [sounds: animalese-generator by equalo-official](https://github.com/equalo-official/animalese-generator/tree/master) diff --git a/src/equicordplugins/animalese/index.ts b/src/equicordplugins/animalese/index.ts new file mode 100644 index 000000000..2ec6ca338 --- /dev/null +++ b/src/equicordplugins/animalese/index.ts @@ -0,0 +1,209 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { EquicordDevs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { FluxDispatcher } from "@webpack/common"; + +const settings = definePluginSettings({ + volume: { + type: OptionType.SLIDER, + description: "Volume of the animalese sound", + default: 0.5, + markers: [0, 0.25, 0.5, 0.75, 1], + }, + speed: { + type: OptionType.SLIDER, + description: "Speed of the animalese sound", + default: 1, + markers: [0.5, 0.75, 1, 1.25, 1.5], + }, +}); + +let audioContext: AudioContext | null = null; +let currentChannelId: string | null = null; + +// better than my old hardcoded garbage +const highSounds = Array.from( + { length: 30 }, + (_, i) => `sound${String(i + 1).padStart(2, "0")}.wav` +); +const soundBuffers: Record = {}; + +// todo implement other pitch sounds but theoretically plugging this in from said repo should work, right? +const BASE_URL_HIGH = + "https://github.com/Equicord/Equibored/raw/refs/heads/main/sounds/animalese/high"; + +async function initSoundBuffers() { + if (!audioContext) audioContext = new AudioContext(); + for (const file of highSounds) { + const nameWithoutExt = file.replace(".wav", ""); + soundBuffers[nameWithoutExt] = await loadSound( + `${BASE_URL_HIGH}/${file}` + ); + } +} + +async function loadSound(url: string): Promise { + if (!audioContext) audioContext = new AudioContext(); + const response = await fetch(url); + if (!response.ok) throw new Error("Network response was not OK"); + const arrayBuffer = await response.arrayBuffer(); + return audioContext.decodeAudioData(arrayBuffer); +} + +async function generateAnimalese(text: string): Promise { + if (!audioContext) audioContext = new AudioContext(); + + const soundIndices: string[] = []; + const text_lower = text.toLowerCase(); + + for (let i = 0; i < text_lower.length; i++) { + const char = text_lower[i]; + if (char === "s" && text_lower[i + 1] === "h") { + soundIndices.push("sound27"); + i++; + } else if (char === "t" && text_lower[i + 1] === "h") { + soundIndices.push("sound28"); + i++; + } else if ( + char === "h" && + (text_lower[i - 1] === "s" || text_lower[i - 1] === "t") + ) { + continue; + } else if (char === "," || char === "?") { + soundIndices.push("sound30"); + } else if (char === text_lower[i - 1]) { + continue; + } else if (char.match(/[a-z]/)) { + const index = char.charCodeAt(0) - 96; + soundIndices.push(`sound${String(index).padStart(2, "0")}`); + } + } + + // No valid characters? Just return null + if (soundIndices.length === 0) { + return null; + } + + const totalDuration = soundIndices.length * 0.1; + const frameCount = Math.max(1, Math.floor(audioContext.sampleRate * totalDuration)); + + const outputBuffer = audioContext.createBuffer( + 1, + frameCount, + audioContext.sampleRate + ); + const outputData = outputBuffer.getChannelData(0); + + let offset = 0; + for (let i = 0; i < soundIndices.length; i++) { + const soundIndex = soundIndices[i]; + const buffer = soundBuffers[soundIndex]; + if (!buffer) continue; + + const variation = 0.15; + let pitchShift = 2.8 + Math.random() * variation; + + const isQuestion = text_lower.endsWith("?"); + if (isQuestion && i >= soundIndices.length * 0.8) { + const progress = + (i - soundIndices.length * 0.8) / (soundIndices.length * 0.2); + pitchShift += progress * 0.1 + 0.1; + } + + const inputData = buffer.getChannelData(0); + const inputLength = inputData.length; + const outputLength = Math.floor(inputLength / pitchShift); + + for (let j = 0; j < outputLength; j++) { + const inputIndex = Math.floor(j * pitchShift); + if (inputIndex < inputLength && offset + j < outputData.length) { + outputData[offset + j] = inputData[inputIndex]; + } + } + offset += outputLength; + } + + return outputBuffer; +} + + +async function playSound(buffer: AudioBuffer, volume: number) { + if (!audioContext) audioContext = new AudioContext(); + const source = audioContext.createBufferSource(); + const gainNode = audioContext.createGain(); + + source.buffer = buffer; + gainNode.gain.value = volume; + + source.connect(gainNode); + gainNode.connect(audioContext.destination); + + source.start(); +} + +export default definePlugin({ + name: "Animalese", + description: "Plays animalese (they yap a lot) on message sent", + authors: [EquicordDevs.ryanamay, EquicordDevs.Mocha], + settings, + + start() { + // Only subscribe once! + const init = async () => { + if (!audioContext) { + audioContext = new AudioContext(); + await initSoundBuffers(); + } + document.removeEventListener("click", init); + }; + document.addEventListener("click", init); + + this.channelSelectListener = ({ channelId }) => { + currentChannelId = channelId; + }; + FluxDispatcher.subscribe("CHANNEL_SELECT", this.channelSelectListener); + + this.messageCreateListener = async ({ optimistic, type, message }) => { + // DO NOT REMOVE THIS OR EELSE IT WILL BE YAP CENTRAL + if (optimistic || type !== "MESSAGE_CREATE") return; + + if ( + !message?.content || + message.author?.bot || + message.channel_id !== currentChannelId + ) { + return; + } + + try { + const buffer = await generateAnimalese(message.content); + await playSound(buffer, settings.store.volume); + } catch (err) { + console.error("[Animalese]", err); + } + }; + FluxDispatcher.subscribe("MESSAGE_CREATE", this.messageCreateListener); + }, + + stop() { + FluxDispatcher.unsubscribe( + "CHANNEL_SELECT", + this.channelSelectListener + ); + FluxDispatcher.unsubscribe( + "MESSAGE_CREATE", + this.messageCreateListener + ); + if (audioContext) { + audioContext.close(); + audioContext = null; + } + currentChannelId = null; + }, +}); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index d13e44f1c..4b111e8d0 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1129,6 +1129,14 @@ export const EquicordDevs = Object.freeze({ name: "DiabeloDEV", id: 1231342375465390100n }, + ryanamay: { + name: "ryanamay", + id: 1262793452236570667n + }, + Mocha: { + name: "Mocha", + id: 808802000224518264n + }, } satisfies Record); // iife so #__PURE__ works correctly