Animalese plugin! (#361)

* Animalese plugin!

* rm readme and license :3

* oopsies :3c

* lint

* import devs :3

* Rewind time

* Update index.ts

---------

Co-authored-by: mocha <mocha@mochas-MacBook-Pro.local>
Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
Mocha 2025-08-23 08:41:58 -04:00 • committed by GitHub
parent 2541de0e92
commit 62ef6c62b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 238 additions and 0 deletions

View file

@ -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)

View file

@ -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<string, AudioBuffer> = {};
// 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<AudioBuffer> {
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<AudioBuffer | null> {
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;
},
});

View file

@ -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<string, Dev>);
// iife so #__PURE__ works correctly