AudioPlayerAPI (#404)

* Add AudioPlayerAPI.

* Separate preload and persistent.

* Edge case on restarting in quick succession.

* Pass type to processors.

* Add more clicks to make faster typing clearer.

* Simplify stopAudio.

* Fix.

* Missing patch.

* Relocate patch.
This commit is contained in:
Etorix 2025-09-20 09:51:28 -07:00 • committed by GitHub
parent d1e3a68eae
commit 1546776a9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 633 additions and 236 deletions

245
src/api/AudioPlayer.ts Normal file
View file

@ -0,0 +1,245 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Logger } from "@utils/Logger";
import { findByCodeLazy, findLazy } from "webpack";
let defaultSounds: null | string[] = null;
const findDefaultSounds = findLazy(module => module.resolve && module.id && module.keys().some(key => key.endsWith(".mp3")), false);
const AudioPlayerConstructor = findByCodeLazy("could not play audio");
export type AudioProcessor = (data: PreprocessAudioData) => void;
export type AudioCallback = (() => void);
export type AudioErrorHandler = ((error: Error) => void);
export const audioProcessorFunctions: Record<string, AudioProcessor> = {};
export const AudioPlayerAPILogger = new Logger("AudioPlayerAPI");
export enum AudioType {
URL = "url",
DATA = "data-uri",
BLOB = "blob",
PATH = "file-path",
OTHER = "other",
DISCORD = "discord"
}
export interface PreprocessAudioData {
audio: string;
readonly type: AudioType;
volume: number;
speed: number;
}
export interface AudioPlayerInternal {
preprocessDataOriginal: PreprocessAudioData;
preprocessDataPrevious: PreprocessAudioData | null;
preprocessDataCurrent: PreprocessAudioData;
audio: string;
_audio: null | Promise<HTMLAudioElement>;
_volume: number;
_speed: number;
outputChannel: string;
type: AudioType;
preload: boolean;
persistent: boolean;
onEnded?: AudioCallback;
onError?: AudioErrorHandler;
processAudio: () => void;
ensureAudio(): Promise<HTMLAudioElement>;
destroyAudio(): void;
loop(): void;
play(): void;
pause(): void;
stop(restart?: boolean): void;
}
export interface AudioPlayerInterface {
/** The internal Discord audio filename (e.g. "discodo"), a data URI, or an external URL that follows the CSP. */
audio: string;
/** The read-only type of audio determined during processing. */
readonly type: AudioType;
/** The duration of the audio in seconds, or null if not yet loaded. */
readonly duration: Promise<number> | null;
/** The current time of the audio in seconds, or null if not yet loaded. */
time: Promise<number> | null;
/** The paused state of the audio, or null if not yet loaded. */
paused: Promise<boolean> | null;
/** The muted state of the audio, or null if not yet loaded. */
muted: Promise<boolean> | null;
/** The volume of the audio between 0 and 100. */
volume: number;
/** The playback speed of the audio between 0.0625 and 16. */
speed: number;
/** Whether to load the audio immediately. If persistent is false, this will only apply until the first playback. */
preload?: boolean;
/** Whether the audio element is persistent and not recreated for every playback. */
persistent: boolean;
/** Preloads the audio before playback. Automatically called when persistent is true. */
load(): void;
/** Sets the audio to loop until paused or stopped. */
loop(): void;
/** Plays the audio. */
play(): void;
/** Pauses the audio. */
pause(): void;
/** Stops the audio. */
stop(): void;
/** Plays the audio from the beginning. */
restart(): void;
/** Seeks to a specific time in seconds. */
seek(time: number): void;
/** Mutes the audio. */
mute(): void;
/** Unmutes the audio. */
unmute(): void;
/** Deletes the audio element. Necessary if persistent is true. */
delete(): void;
}
export interface AudioPlayerOptions {
volume?: number;
speed?: number;
preload?: boolean;
persistent?: boolean;
onEnded?: AudioCallback;
onError?: AudioErrorHandler;
}
// Wrap the player to allow reprocessing the audio when properties are changed and to alleviate
// the confusion between the public API accepting 0-100 volume while the internal API uses 0-1 volume.
class AudioPlayerWrapper implements AudioPlayerInterface {
private internalPlayer: AudioPlayerInternal;
constructor(internalPlayer: AudioPlayerInternal) { this.internalPlayer = internalPlayer; }
get audio(): string { return this.internalPlayer.audio; }
set audio(value: string) { this.internalPlayer.preprocessDataOriginal.audio = value; this.internalPlayer.processAudio(); }
get volume(): number { return this.internalPlayer._volume * 100; }
set volume(value: number) { this.internalPlayer.preprocessDataOriginal.volume = Math.max(0, Math.min(1, value / 100)); this.internalPlayer.processAudio(); }
get speed(): number { return this.internalPlayer._speed; }
set speed(value: number) { this.internalPlayer.preprocessDataOriginal.speed = Math.max(0.0625, Math.min(16, value)); this.internalPlayer.processAudio(); }
get time(): Promise<number> | null { return this.internalPlayer._audio?.then(audio => audio.currentTime) ?? null; }
set time(value: number) { this.internalPlayer.ensureAudio().then(audio => audio.currentTime = value); }
get persistent(): boolean { return this.internalPlayer.persistent; }
set persistent(value: boolean) { this.internalPlayer.persistent = value; }
get muted(): Promise<boolean> | null { return this.internalPlayer._audio?.then(audio => audio.muted) ?? null; }
set muted(value: boolean) { this.internalPlayer.ensureAudio().then(audio => audio.muted = value); }
get paused(): Promise<boolean> | null { return this.internalPlayer._audio?.then(audio => audio.paused) ?? null; }
set paused(value: boolean) { value ? this.internalPlayer.pause() : this.internalPlayer.play(); }
get type(): AudioType { return this.internalPlayer.type; }
get duration(): Promise<number> | null { return this.internalPlayer._audio?.then(audio => audio.duration) ?? null; }
load(): void { this.internalPlayer.ensureAudio(); }
loop(): void { this.internalPlayer.loop(); }
play(): void { this.internalPlayer.play(); }
pause(): void { this.internalPlayer.pause(); }
stop(restart?: boolean): void { this.internalPlayer.stop(restart); }
restart(): void { this.internalPlayer.stop(true); }
seek(time: number): void { this.internalPlayer.ensureAudio().then(audio => audio.currentTime = time); }
mute(): void { this.internalPlayer.ensureAudio().then(audio => audio.muted = true); }
unmute(): void { this.internalPlayer.ensureAudio().then(audio => audio.muted = false); }
delete(): void { this.internalPlayer.destroyAudio(); }
}
/**
* Creates an audio player.
* @param audio The internal Discord audio filename (e.g. "discodo"), a data URI, or an external URL that follows the CSP.
* @param options Additional options for the audio player.
* @param options.volume The volume of the audio, between 0 and 100, defaulting to 100.
* @param options.speed The playback speed of the audio, between 0.0625 and 16, defaulting to 1.
* @param options.preload Whether to load the audio immediately. If persistent is false, this will only apply until the first playback.
* @param options.persistent Whether the audio element is persistent and not recreated for every playback. If persistent, you must call delete() to free the memory. Defaults to false.
* @param options.onEnded An optional callback that is called every time the audio finishes playing.
* @param options.onError An optional error handler that is passed an Error object when an error occurs during audio playback.
* @return The created audio player.
*/
export function createAudioPlayer(
audio: string,
options: AudioPlayerOptions = {}
): AudioPlayerInterface {
const internalPlayer: AudioPlayerInternal = new AudioPlayerConstructor(
audio,
null,
null,
"default",
options
);
return new AudioPlayerWrapper(internalPlayer);
}
/**
* Plays an audio instantly and returns the player.
* @param audio The internal Discord audio filename (e.g. "discodo"), a data URI, or an external URL that follows the CSP.
* @param options Additional options for the audio player.
* @param options.volume The volume of the audio, between 0 and 100, defaulting to 100.
* @param options.speed The playback speed of the audio, between 0.0625 and 16, defaulting to 1.
* @param options.preload Whether to load the audio immediately. If persistent is false, this will only apply until the first playback.
* @param options.persistent Whether the audio element is persistent and not recreated for every playback. If persistent, you must call delete() to free the memory. Defaults to false.
* @param options.onEnded An optional callback that is called every time the audio finishes playing.
* @param options.onError An optional error handler that is passed an Error object when an error occurs during audio playback.
* @return The created audio player.
*/
export function playAudio(audio: string, options: AudioPlayerOptions = {}): AudioPlayerInterface {
const player = createAudioPlayer(audio, options);
player.play();
return player;
}
/**
* Identifies the type of audio based on its string.
* @param audio The audio string to identify.
* @returns The identified AudioType.
*/
export function identifyAudioType(audio: string): AudioType {
if (defaultAudioNames().includes(audio)) return AudioType.DISCORD;
try {
const url = new URL(audio);
if (url.protocol === "http:" || url.protocol === "https:") return AudioType.URL;
if (url.protocol === "data:") return AudioType.DATA;
if (url.protocol === "blob:") return AudioType.BLOB;
if (url.protocol === "file:") return AudioType.PATH;
return AudioType.OTHER;
} catch {
return AudioType.OTHER;
}
}
/**
* Adds a function to process an audio before it is played.
* @param key A unique identifier for this audio processor. Plugin name is recommended.
* @param processor A function that takes an audio, volume (0-100), and type (AudioType), and returns the replacement audio and volume.
*/
export function addAudioProcessor(key: string, processor: AudioProcessor): void {
AudioPlayerAPILogger.log("Adding Audio Processor:", { key, processor });
audioProcessorFunctions[key] = processor;
}
/**
* Removes an audio processor by its key.
* @param key The unique identifier of the audio processor to remove.
*/
export function removeAudioProcessor(key: string): void {
AudioPlayerAPILogger.log("Removing Audio Processor:", { key });
delete audioProcessorFunctions[key];
}
/** Returns an array of all internal Discord audio filenames. */
export function defaultAudioNames(): string[] {
defaultSounds ??= (findDefaultSounds.keys() || []).map(key => {
const match = key.match(/((?:\w|-)+)\.mp3$/);
return match ? match[1] : null;
}).filter(Boolean) as string[];
return defaultSounds;
}

View file

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as $AudioPlayer from "./AudioPlayer";
import * as $Badges from "./Badges";
import * as $ChatButtons from "./ChatButtons";
import * as $Commands from "./Commands";
@ -37,27 +38,30 @@ import * as $UserSettings from "./UserSettings";
/**
* An API allowing you to listen to Message Clicks or run your own logic
* before a message is sent
* before a message is sent.
*
* If your plugin uses this, you must add MessageEventsAPI to its dependencies
* If your plugin uses this, you must add MessageEventsAPI to its dependencies.
*/
export const MessageEvents = $MessageEventsAPI;
/**
* An API allowing you to create custom notices
* (snackbars on the top, like the Update prompt)
* (snackbars on the top, like the Update prompt).
*/
export const Notices = $Notices;
/**
* An API allowing you to register custom commands
* An API allowing you to register custom commands.
*/
export const Commands = $Commands;
/**
* A wrapper around IndexedDB. This can store arbitrarily
* large data and supports a lot of datatypes (Blob, Map, ...).
* For a full list, see the mdn link below
* For a full list, see the mdn link below.
*
* This should always be preferred over the Settings API if possible, as
* localstorage has very strict size restrictions and blocks the event loop
* localstorage has very strict size restrictions and blocks the event loop.
*
* Make sure your keys are unique (tip: prefix them with ur plugin name)
* and please clean up no longer needed entries.
@ -66,70 +70,84 @@ export const Commands = $Commands;
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types}
*/
export const DataStore = $DataStore;
/**
* An API allowing you to add custom components as message accessories
* An API allowing you to add custom components as message accessories.
*/
export const MessageAccessories = $MessageAccessories;
/**
* An API allowing you to add custom buttons in the message popover
* An API allowing you to add custom buttons in the message popover.
*/
export const MessagePopover = $MessagePopover;
/**
* An API allowing you to add badges to user profiles
* An API allowing you to add badges to user profiles.
*/
export const Badges = $Badges;
/**
* An API allowing you to add custom elements to the server list
* An API allowing you to add custom elements to the server list.
*/
export const ServerList = $ServerList;
/**
* An API allowing you to add components as message accessories
* An API allowing you to add components as message accessories.
*/
export const MessageDecorations = $MessageDecorations;
/**
* An API allowing you to add components to member list users, in both DM's and servers
* An API allowing you to add components to member list users, in both DM's and servers.
*/
export const MemberListDecorators = $MemberListDecorators;
/**
* An API allowing you to persist data
* An API allowing you to persist data.
*/
export const Settings = $Settings;
/**
* An API allowing you to dynamically load styles
* An API allowing you to dynamically load styles.
* a
*/
export const Styles = $Styles;
/**
* An API allowing you to display notifications
* An API allowing you to display notifications.
*/
export const Notifications = $Notifications;
/**
* An api allowing you to patch and add/remove items to/from context menus
* An api allowing you to patch and add/remove items to/from context menus.
*/
export const ContextMenu = $ContextMenu;
/**
* An API allowing you to add buttons to the chat input
* An API allowing you to add buttons to the chat input.
*/
export const ChatButtons = $ChatButtons;
/**
* An API allowing you to update and re-render messages
* An API allowing you to update and re-render messages.
*/
export const MessageUpdater = $MessageUpdater;
/**
* An API allowing you to get an user setting
* An API allowing you to get an user setting.
*/
export const UserSettings = $UserSettings;
/**
* An API allowing you to add icons to the nickname, in profiles
* An API allowing you to add icons to the nickname, in profiles.
*/
export const NicknameIcons = $NicknameIcons;
/**
* Just used to identify if user is on Equicord as Vencord doesnt have this
* An API allowing you to play internal Discord audio files or external audio URLs/URIs.
*/
export const AudioPlayer = $AudioPlayer;
/**
* Just used to identify if user is on Equicord as Vencord doesn't have this.
*/
export const isEquicord = true;

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { playAudio } from "@api/AudioPlayer";
import { type NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Notifications } from "@api/index";
import { definePluginSettings } from "@api/Settings";
@ -59,7 +60,7 @@ async function showNotification(message: Message, guildId: string | undefined):
});
if (settings.store.notificationSound) {
new Audio("https://discord.com/assets/9422aef94aa931248105.mp3").play();
playAudio("message1");
}
} catch (error) {
new Logger("BypassStatus").error("Failed to notify user: ", error);
@ -150,6 +151,7 @@ export default definePlugin({
name: "BypassStatus",
description: "Still get notifications from specific sources when in do not disturb mode. Right-click on users/channels/guilds to set them to bypass do not disturb mode.",
authors: [Devs.Inbestigator],
dependencies: ["AudioPlayerAPI"],
flux: {
async MESSAGE_CREATE({ message, guildId, channelId }: IMessageCreate): Promise<void> {
try {

View file

@ -4,17 +4,18 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { AudioPlayerInterface, playAudio } from "@api/AudioPlayer";
import { classNameFactory } from "@api/Styles";
import { Margins } from "@utils/margins";
import { useForceUpdater } from "@utils/react";
import { makeRange } from "@utils/types";
import { findByCodeLazy, findLazy } from "@webpack";
import { findLazy } from "@webpack";
import { Button, Card, Forms, React, Select, showToast, Slider, Switch } from "@webpack/common";
import { ComponentType, Ref, SyntheticEvent } from "react";
import { deleteAudio, getAllAudio, saveAudio, StoredAudioFile } from "./audioStore";
import { ensureDataURICached } from "./index";
import { SoundOverride, SoundPlayer, SoundType } from "./types";
import { SoundOverride, SoundType } from "./types";
type FileInput = ComponentType<{
ref: Ref<HTMLInputElement>;
@ -25,7 +26,6 @@ type FileInput = ComponentType<{
const AUDIO_EXTENSIONS = ["mp3", "wav", "ogg", "m4a", "aac", "flac", "webm", "wma", "mp4"];
const cl = classNameFactory("vc-custom-sounds-");
const playSound: (id: string) => SoundPlayer = findByCodeLazy(".playWithListener().then");
const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef);
const capitalizeWords = (str: string) =>
@ -38,7 +38,7 @@ export function SoundOverrideComponent({ type, override, onChange }: {
}) {
const fileInputRef = React.useRef<HTMLInputElement>(null);
const update = useForceUpdater();
const sound = React.useRef<SoundPlayer | null>(null);
const sound = React.useRef<AudioPlayerInterface | null>(null);
const [files, setFiles] = React.useState<Record<string, StoredAudioFile>>({});
React.useEffect(() => {
@ -54,7 +54,7 @@ export function SoundOverrideComponent({ type, override, onChange }: {
sound.current?.stop();
if (!override.enabled) {
sound.current = playSound(type.id);
sound.current = playAudio(type.id);
return;
}
@ -69,32 +69,20 @@ export function SoundOverrideComponent({ type, override, onChange }: {
return;
}
const audio = new Audio(dataUri);
audio.volume = override.volume / 100;
audio.onerror = e => {
console.error("[CustomSounds] Error playing custom audio:", e);
showToast("Error playing custom sound. File may be corrupted.");
};
await audio.play();
sound.current = {
play: () => audio.play(),
pause: () => audio.pause(),
stop: () => {
audio.pause();
audio.currentTime = 0;
},
loop: () => { audio.loop = true; }
};
sound.current = playAudio(dataUri, {
volume: override.volume, onError: e => {
console.error("[CustomSounds] Error playing custom audio:", e);
showToast("Error playing custom sound. File may be corrupted.");
}
});
} catch (error) {
console.error("[CustomSounds] Error in previewSound:", error);
showToast("Error playing sound.");
}
} else if (selectedSound === "default") {
sound.current = playSound(type.id);
sound.current = playAudio(type.id);
} else {
sound.current = playSound(selectedSound);
sound.current = playAudio(selectedSound);
}
};
@ -207,6 +195,7 @@ export function SoundOverrideComponent({ type, override, onChange }: {
markers={makeRange(0, 100, 10)}
initialValue={override.volume}
onValueChange={val => {
sound.current && (sound.current.volume = val);
override.volume = val;
saveAndNotify();
}}

View file

@ -6,6 +6,7 @@
import "./styles.css";
import { addAudioProcessor, AudioProcessor, PreprocessAudioData, removeAudioProcessor } from "@api/AudioPlayer";
import { get as getFromDataStore } from "@api/DataStore";
import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
@ -42,42 +43,55 @@ function setOverride(id: string, override: SoundOverride) {
settings.store[id] = JSON.stringify(override);
}
export function getCustomSoundURL(id: string): string | null {
const override = getOverride(id);
export const getCustomSoundURL: AudioProcessor = (data: PreprocessAudioData) => {
let audioOverride = data.audio;
if (data.audio in seasonalSounds) {
audioOverride = soundTypes.find(sound => sound.seasonal?.includes(data.audio))?.id || data.audio;
}
const override = getOverride(audioOverride);
if (!override?.enabled) {
return null;
return;
}
if (override.selectedSound === "custom" && override.selectedFileId) {
const dataUri = dataUriCache.get(override.selectedFileId);
if (dataUri) {
console.log(`[CustomSounds] Returning cached data URI for ${id}`);
return dataUri;
data.audio = dataUri;
data.volume = override.volume;
return;
} else {
console.warn(`[CustomSounds] No cached data URI for ${id} with file ID ${override.selectedFileId}`);
return null;
return;
}
}
if (override.selectedSound !== "default" && override.selectedSound !== "custom") {
if (override.selectedSound in seasonalSounds) {
return seasonalSounds[override.selectedSound];
data.audio = seasonalSounds[override.selectedSound];
data.volume = override.volume;
return;
}
const soundType = allSoundTypes.find(t => t.id === id);
const soundType = allSoundTypes.find(t => t.id === data.audio);
if (soundType?.seasonal) {
const seasonalId = soundType.seasonal.find(seasonalId =>
seasonalId.startsWith(`${override.selectedSound}_`)
);
if (seasonalId && seasonalId in seasonalSounds) {
return seasonalSounds[seasonalId];
data.audio = seasonalSounds[seasonalId];
data.volume = override.volume;
return;
}
}
}
return null;
}
data.volume = override.volume;
return;
};
export async function ensureDataURICached(fileId: string): Promise<string | null> {
if (dataUriCache.has(fileId)) {
@ -379,43 +393,15 @@ export default definePlugin({
name: "CustomSounds",
description: "Customize Discord's sounds.",
authors: [Devs.ScattrdBlade, Devs.TheKodeToad],
patches: [
{
find: 'Error("could not play audio")',
replacement: [
{
match: /(?<=new Audio;\i\.src=).{0,75}.concat\(this\.name,"\.mp3"\)\)/,
replace: "$self.getSoundUrl(this.name,$&)"
},
{
match: /Math.min\(\i\.\i\.getOutputVolume\(\).{0,20}volume/,
replace: "$& * ($self.findOverride(this.name)?.volume ?? 100) / 100"
}
]
},
{
find: ".playWithListener().then",
replacement: {
match: /\i\.\i\.getSoundpack\(\)/,
replace: '$self.isOverriden(arguments[0]) ? "classic" : $&'
}
}
],
getSoundUrl(name, extra) {
const customUrl = this.getCustomSoundURL(name);
return customUrl || extra;
},
dependencies: ["AudioPlayerAPI"],
settings,
findOverride,
isOverriden,
getCustomSoundURL,
refreshDataURI,
ensureDataURICached,
debugCustomSounds,
startAt: StartAt.Init,
async start() {
console.log("[CustomSounds] Plugin starting...");
addAudioProcessor("CustomSounds", this.getCustomSoundURL);
try {
await preloadDataURIs();
@ -423,5 +409,10 @@ export default definePlugin({
} catch (error) {
console.error("[CustomSounds] Startup failed:", error);
}
},
stop() {
removeAudioProcessor("CustomSounds");
console.log("[CustomSounds] Plugin stopped");
}
});

View file

@ -18,13 +18,6 @@ export interface SoundOverride {
selectedFileId?: string;
}
export interface SoundPlayer {
loop(): void;
play(): void;
pause(): void;
stop(): void;
}
export const seasonalSounds = {
"halloween_call_calling": "https://canary.discord.com/assets/0950a7ea4f1dd037870b.mp3",
"winter_call_calling": "https://canary.discord.com/assets/7b945e7be3f86c5b7c82.mp3",

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { playAudio } from "@api/AudioPlayer";
import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Devs, EquicordDevs } from "@utils/constants";
@ -72,6 +73,7 @@ function ToggleModal() {
}
function handleToggle() {
console.log("Toggling demonstration mode");
const style = document.getElementById("DemonstrationStyle");
if (style != null) {
style.remove();
@ -118,18 +120,19 @@ function handleKeydown(event: KeyboardEvent) {
}
async function playSound(url: string) {
const audio = new Audio(url);
audio.volume = settings.store.soundVolume;
await audio.play().catch(error => {
console.error("Error playing sound:", error);
playAudio(url, {
volume: settings.store.soundVolume * 100,
onError: error => {
console.error("Error playing sound:", error);
}
});
audio.remove();
}
export default definePlugin({
name: "Demonstration",
description: "Plugin for taking theme screenshots - censors identifying images and text.",
authors: [Devs.Samwich, EquicordDevs.Panniku],
dependencies: ["AudioPlayerAPI"],
settings,
toolboxActions: {
"Toggle Demonstration": (() => handleToggle())

View file

@ -4,20 +4,20 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { AudioPlayerInterface, createAudioPlayer } from "@api/AudioPlayer";
import { definePluginSettings } from "@api/Settings";
import { Devs, EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
let clickCount = 0;
let croissant: AudioPlayerInterface | null = null;
function play() {
const triggerAmount = settings.store.amount;
clickCount++;
const triggerAmount = settings.store.amount;
if (clickCount % triggerAmount === 0) {
const audio = new Audio("https://github.com/Equicord/Equibored/raw/main/sounds/equissant/croissant.mp3");
audio.play();
croissant?.play();
clickCount = 0;
}
}
@ -34,11 +34,14 @@ export default definePlugin({
name: "Equissant",
description: "Crossant every specified amount of clicks :trolley:",
authors: [EquicordDevs.SomeAspy, Devs.thororen],
dependencies: ["AudioPlayerAPI"],
settings,
start() {
croissant = createAudioPlayer("https://github.com/Equicord/Equibored/raw/main/sounds/equissant/croissant.mp3", { persistent: true });
document.addEventListener("click", play);
},
stop() {
croissant?.delete();
document.removeEventListener("click", play);
}
});

View file

@ -6,6 +6,7 @@
import "./styles.css";
import { createAudioPlayer } from "@api/AudioPlayer";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
@ -48,6 +49,7 @@ export default definePlugin({
name: "Jumpscare",
description: "Adds a configurable chance of jumpscaring you whenever you open a channel. Inspired by Geometry Dash Mega Hack",
authors: [Devs.surgedevs],
dependencies: ["AudioPlayerAPI"],
settings,
start() {
@ -63,8 +65,7 @@ export default definePlugin({
JumpscareComponent() {
const [isPlaying, setIsPlaying] = useState(false);
const audio = new Audio(settings.store.audioSource);
const jumpscareAudio = createAudioPlayer(settings.store.audioSource, { volume: 100, onEnded: () => { setIsPlaying(false); } });
const jumpscare = event => {
if (isPlaying) return;
@ -73,13 +74,7 @@ export default definePlugin({
if (Math.random() > chance) return;
setIsPlaying(true);
audio.play();
console.log(isPlaying);
setTimeout(() => {
setIsPlaying(false);
}, 1000);
jumpscareAudio.play();
};
useEffect(() => {

View file

@ -16,58 +16,75 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { AudioPlayerInterface, createAudioPlayer } from "@api/AudioPlayer";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
let click1, click2, click3, backspace;
let sounds = {
click1,
click2,
click3,
backspace
};
let backspace: AudioPlayerInterface;
let clicks: Array<{ playing: boolean; player: AudioPlayerInterface; }> = [];
const keysCurrentlyPressed = new Set<string>();
let previousSoundIndex = 0;
const ignoredKeys = ["CapsLock", "ShiftLeft", "ShiftRight", "ControlLeft", "ControlRight", "AltLeft", "AltRight", "MetaLeft", "MetaRight", "ArrowUp", "ArrowRight", "ArrowLeft", "ArrowDown", "MediaPlayPause", "MediaStop", "MediaTrackNext", "MediaTrackPrevious", "MediaSelect", "MediaEject", "MediaVolumeUp", "MediaVolumeDown", "AudioVolumeUp", "AudioVolumeDown"];
const keyup = (e: KeyboardEvent) => { keysCurrentlyPressed.delete(e.code); };
const keydown = (e: KeyboardEvent) => {
if (ignoredKeys.includes(e.code)) return;
for (const sound of Object.values(sounds)) sound.pause();
if (!clicks.length || !backspace) return;
if (keysCurrentlyPressed.has(e.code)) return;
keysCurrentlyPressed.add(e.code);
if (e.code === "Backspace") {
sounds.backspace.currentTime = 0;
sounds.backspace.play();
backspace.restart();
} else {
const click = sounds[`click${Math.floor(Math.random() * 3) + 1}`];
click.currentTime = 0;
click.play();
const nonplayingClicks = clicks.filter(click => !click.playing);
const randomIndex = Math.floor(Math.random() * nonplayingClicks.length);
const chosenClick = nonplayingClicks.length ? nonplayingClicks[randomIndex] : clicks[previousSoundIndex];
previousSoundIndex = randomIndex;
chosenClick.playing = true;
chosenClick.player.restart();
}
};
function assignSounds(volume: number) {
backspace = createAudioPlayer("https://github.com/Equicord/Equibored/raw/main/sounds/keyboard/backspace.wav", { volume, preload: true, persistent: true });
clicks = [];
for (let i = 0; i < 3; i++) {
const baseIndex = i * 3;
clicks.push({ playing: false, player: createAudioPlayer("https://github.com/Equicord/Equibored/raw/main/sounds/keyboard/click1.wav", { volume, preload: true, persistent: true, onEnded: () => { clicks[baseIndex].playing = false; } }) });
clicks.push({ playing: false, player: createAudioPlayer("https://github.com/Equicord/Equibored/raw/main/sounds/keyboard/click2.wav", { volume, preload: true, persistent: true, onEnded: () => { clicks[baseIndex + 1].playing = false; } }) });
clicks.push({ playing: false, player: createAudioPlayer("https://github.com/Equicord/Equibored/raw/main/sounds/keyboard/click3.wav", { volume, preload: true, persistent: true, onEnded: () => { clicks[baseIndex + 2].playing = false; } }) });
}
}
const settings = definePluginSettings({
volume: {
description: "Volume of the keyboard sounds.",
type: OptionType.SLIDER,
markers: [0, 25, 50, 75, 100],
stickToMarkers: false,
default: 100,
onChange: value => { assignSounds(value); }
}
});
export default definePlugin({
name: "KeyboardSounds",
description: "Adds the Opera GX Keyboard Sounds to Discord",
authors: [Devs.HypedDomi],
start: () => {
click1 = new Audio("https://github.com/Equicord/Equibored/raw/main/sounds/keyboard/click1.wav");
click2 = new Audio("https://github.com/Equicord/Equibored/raw/main/sounds/keyboard/click2.wav");
click3 = new Audio("https://github.com/Equicord/Equibored/raw/main/sounds/keyboard/click3.wav");
backspace = new Audio("https://github.com/Equicord/Equibored/raw/main/sounds/keyboard/backspace.wav");
sounds = {
click1,
click2,
click3,
backspace,
};
dependencies: ["AudioPlayerAPI"],
settings,
start() {
assignSounds(settings.store.volume);
document.addEventListener("keyup", keyup);
document.addEventListener("keydown", keydown);
},
stop: () => document.removeEventListener("keydown", keydown),
options: {
volume: {
description: "Volume",
type: OptionType.SLIDER,
markers: [0, 100],
stickToMarkers: false,
default: 100,
onChange: value => { for (const sound of Object.values(sounds)) sound.volume = value / 100; }
}
}
stop: () => {
[...clicks, { player: backspace }].forEach(sound => sound.player.delete());
document.removeEventListener("keyup", keyup);
document.removeEventListener("keydown", keydown);
},
});

View file

@ -790,7 +790,7 @@ export default definePlugin({
name: "Questify",
description: "Enhance your Quest experience with a suite of features, or disable them entirely if they're not your thing.",
authors: [EquicordDevs.Etorix],
dependencies: ["ServerListAPI"],
dependencies: ["AudioPlayerAPI", "ServerListAPI"],
startAt: StartAt.Init, // Needed in order to beat Read All Messages to inserting above the server list.
settings,
@ -812,29 +812,6 @@ export default definePlugin({
activeQuestIntervals,
patches: [
{
find: "could not play audio",
group: true,
replacement: [
{
// Enables external audio sources for playing audio.
match: /(?<=new Audio;\i\.src=)/,
replace: "this.name.startsWith('https')?this.name:"
},
{
// Adds an optional callback to the audio player. This is needed to detect
// when the audio has finished playing as playWithListener() relies on a duration
// variable which is never present.
match: /(constructor\(\i,\i,\i,\i)(\){)/,
replace: "$1,callback$2this.callback=callback||null,"
},
{
// Makes use of the callback if provided.
match: /(?<=.onended=\(\)=>)(this.destroyAudio\(\))/,
replace: "{this.callback?this.callback():null;$1;}"
}
]
},
{
// Hides the notice in the gift inventory that Quests have been relocated to the Discovery tab.
find: "quests-wumpus-hikes-mountain-transparent-background",

View file

@ -4,19 +4,18 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { defaultAudioNames, playAudio } from "@api/AudioPlayer";
import { definePluginSettings } from "@api/Settings";
import { ErrorBoundary } from "@components/index";
import { Logger } from "@utils/Logger";
import { OptionType } from "@utils/types";
import { findLazy } from "@webpack";
import { Button, ColorPicker, ContextMenuApi, Forms, Menu, Select, TextInput, useEffect, useRef, useState } from "@webpack/common";
import { JSX } from "react";
import { activeQuestIntervals, getQuestTileClasses, getQuestTileStyle } from "./index";
import { DynamicDropdown, DynamicDropdownSettingOption, ExcludedQuest, GuildlessServerListItem, Quest, QuestIcon, QuestStatus, QuestTile, RadioGroup, RadioOption, SelectOption, SoundIcon } from "./utils/components";
import { AudioPlayer, decimalToRGB, fetchAndDispatchQuests, getFormattedNow, getIgnoredQuestIDs, getQuestStatus, isDarkish, isSoundAllowed, leftClick, middleClick, q, QuestifyLogger, QuestsStore, rightClick, setIgnoredQuestIDs, validCommaSeparatedList } from "./utils/misc";
import { decimalToRGB, fetchAndDispatchQuests, getFormattedNow, getIgnoredQuestIDs, getQuestStatus, isDarkish, isSoundAllowed, leftClick, middleClick, q, QuestifyLogger, QuestsStore, rightClick, setIgnoredQuestIDs, validCommaSeparatedList } from "./utils/misc";
let defaultSounds: string[] | null = null;
let autoFetchInterval: null | ReturnType<typeof setInterval> = null;
const defaultLeftClickAction = "open-quests";
const defaultMiddleClickAction = "plugin-settings";
@ -30,7 +29,6 @@ const defaultIgnoredColor = 8334124;
const defaultExpiredColor = 2368553;
const defaultRestyleQuestsGradient = "intense";
const defaultFetchQuestsAlert = "discodo";
const findDefaultSounds = findLazy(module => module.resolve && module.id && module.keys().some(key => key.endsWith(".mp3")), false);
export const minimumAutoFetchIntervalValue = 30 * 60;
export const maximumAutoFetchIntervalValue = 12 * 60 * 60;
@ -51,7 +49,7 @@ export function fetchAndAlertQuests(source: string, logger: Logger): void {
if (shouldAlert) {
logger.info(`[${getFormattedNow()}] New Quests detected. Playing alert sound.`);
AudioPlayer(shouldAlert, 1).play();
playAudio(shouldAlert);
} else {
logger.info(`[${getFormattedNow()}] New Quests detected.`);
}
@ -1112,12 +1110,7 @@ function FetchingQuestsSetting(): JSX.Element {
{ value: 60 * 60 * 6, label: "12 Hours" },
];
defaultSounds ??= (findDefaultSounds.keys() || []).map(key => {
const match = key.match(/((?:\w|-)+)\.mp3$/);
return match ? match[1] : null;
}).filter(Boolean) as string[];
const resolvedSounds: SelectOption[] = defaultSounds.map(sound => {
const resolvedSounds: SelectOption[] = defaultAudioNames().map(sound => {
const label = sound.toUpperCase().replace(/_/g, " ").replace(/(\d+)/g, " $1");
return { value: sound, label };
});
@ -1392,8 +1385,7 @@ function FetchingQuestsSetting(): JSX.Element {
if (activePlayer.current) {
clearActivePlayer();
} else {
activePlayer.current = AudioPlayer(currentAlertSelection.value as string, 1, clearActivePlayer);
activePlayer.current?.play();
activePlayer.current = playAudio(currentAlertSelection.value as string, { onEnded: clearActivePlayer });
setIsPlaying(true);
}
}

View file

@ -6,7 +6,7 @@
import { classNameFactory } from "@api/Styles";
import { Logger } from "@utils/Logger";
import { findByCodeLazy, findStoreLazy } from "@webpack";
import { findStoreLazy } from "@webpack";
import { FluxDispatcher, RestAPI, UserStore } from "@webpack/common";
import { questIsIgnored, settings } from "../settings";
@ -14,11 +14,6 @@ import { Quest, QuestStatus, RGB } from "./components";
export const q = classNameFactory("questify-");
export const QuestifyLogger = new Logger("Questify");
// Only takes an audio name or URL. Plays instantly.
export const PlayAudio = findByCodeLazy("Unable to find sound for pack name:");
// Takes an audio name or URL, volume, and a callback function. Plays when play() is called.
const AudioPlayerConstructor = findByCodeLazy("sound has no duration");
export function AudioPlayer(name: string, volume: number = 1, callback?: () => void): any { return new AudioPlayerConstructor(name, null, volume, "default", callback ?? (() => { })); }
export const QuestsStore = findStoreLazy("QuestsStore");
export const questPath = "/discovery/quests";
export const leftClick = 0;

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { AudioPlayerInterface, createAudioPlayer } from "@api/AudioPlayer";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { EquicordDevs } from "@utils/constants";
@ -13,19 +14,28 @@ import { findComponentByCodeLazy } from "@webpack";
import { React } from "@webpack/common";
const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"');
let boopSound: AudioPlayerInterface;
let song: AudioPlayerInterface;
let preloadSong, preloadBoopSound, song, boopSound;
function assignSong(url: string, volume: number) {
song?.delete();
song = createAudioPlayer(url, { volume, preload: true, persistent: true });
song.load();
}
function assignBoop(url: string, volume: number) {
boopSound?.delete();
boopSound = createAudioPlayer(url, { volume, preload: true, persistent: true });
boopSound.load();
}
function SoggyModal(props: ModalProps) {
if (settings.store.songVolume !== 0) {
React.useEffect(() => {
song = new Audio(preloadSong.src);
song.volume = settings.store.songVolume;
song.play();
song?.loop();
return () => {
song.pause();
song.remove();
song?.stop();
};
}, []);
}
@ -42,9 +52,7 @@ function SoggyModal(props: ModalProps) {
offsetY >= region.y &&
offsetY <= region.y + region.height
) {
boopSound = new Audio(preloadBoopSound.src);
boopSound.volume = settings.store.boopVolume;
boopSound.play();
boopSound?.play();
}
};
@ -115,16 +123,16 @@ const settings = definePluginSettings({
description: "URL for the song to play",
type: OptionType.STRING,
default: "https://github.com/Equicord/Equibored/raw/main/sounds/soggy/song.mp3?raw=true",
onChange: (value: string) => {
song = new Audio(value);
}
onChange(newValue) {
assignSong(newValue, settings.store.songVolume * 100);
},
},
boopLink: {
description: "URL for the boop sound",
type: OptionType.STRING,
default: "https://github.com/Equicord/Equibored/raw/main/sounds/soggy/honk.wav?raw=true",
onChange: (value: string) => {
boopSound = new Audio(value);
onChange(newValue) {
assignBoop(newValue, settings.store.boopVolume * 100);
}
}
});
@ -134,6 +142,7 @@ export default definePlugin({
description: "Adds a soggy button to the toolbox",
authors: [EquicordDevs.sliwka],
settings,
dependencies: ["AudioPlayerAPI"],
patches: [
{
find: ".controlButtonWrapper,",
@ -144,9 +153,14 @@ export default definePlugin({
}
],
start: () => {
preloadSong = new Audio(settings.store.songLink);
preloadBoopSound = new Audio(settings.store.boopLink);
start() {
assignBoop(settings.store.boopLink, settings.store.boopVolume * 100);
assignSong(settings.store.songLink, settings.store.songVolume * 100);
},
stop() {
boopSound?.delete();
song?.delete();
},
// taken from message logger lol

View file

@ -22,7 +22,7 @@ export default definePlugin({
name: "SoundBoardLogger",
authors: [Devs.Moxxie, EquicordDevs.Fres, Devs.amy, Devs.thororen],
description: "Logs all soundboards that are played in a voice chat and allows you to download them",
dependencies: ["ChatInputButtonAPI"],
dependencies: ["AudioPlayerAPI", "ChatInputButtonAPI"],
patches: [
{
predicate: () => settings.store.IconLocation === "toolbar",

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { playAudio } from "@api/AudioPlayer";
import { classNameFactory } from "@api/Styles";
import { proxyLazy } from "@utils/lazy";
import { LazyComponent } from "@utils/react";
@ -40,9 +41,7 @@ export function getEmojiUrl(emoji) {
}
export const playSound = id => {
const audio = new Audio(`https://cdn.discordapp.com/soundboard-sounds/${id}`);
audio.volume = settings.store.soundVolume;
audio.play();
playAudio(`https://cdn.discordapp.com/soundboard-sounds/${id}`, { volume: settings.store.soundVolume * 100 });
};
export async function downloadAudio(id: string): Promise<void> {

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { playAudio } from "@api/AudioPlayer";
import { EquicordDevs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord";
import definePlugin from "@utils/types";
@ -16,22 +17,14 @@ async function readOutText(text: string): Promise<void> {
});
const mp3JSON = await mp3Response.json();
const audioElement = new Audio();
audioElement.src = `data:audio/mpeg;base64,${mp3JSON.data}`;
audioElement.volume = 0.5;
audioElement.onended = () => audioElement.remove();
document.body.appendChild(audioElement);
audioElement.play();
playAudio(`data:audio/mpeg;base64,${mp3JSON.data}`, { volume: 50 });
}
export default definePlugin({
name: "TiktokTTS",
description: "Reads out chat messages with the good ol' Tiktok TTS voice :sob:",
authors: [EquicordDevs.VillainsRule],
dependencies: ["AudioPlayerAPI"],
flux: {
async MESSAGE_CREATE({ optimistic, type, message }) {
if (optimistic || type !== "MESSAGE_CREATE") return;

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { playAudio } from "@api/AudioPlayer";
import { definePluginSettings } from "@api/Settings";
import { Devs, EquicordDevs } from "@utils/constants";
import { Margins } from "@utils/margins";
@ -90,10 +91,7 @@ async function speak(text: string, { volume, rate, customVoice } = settings.stor
// 1. Check the in-memory cache (fast check)
if (ttsCache.has(cacheKey)) {
const cachedUrl = ttsCache.get(cacheKey)!;
const audio = new Audio(cachedUrl);
audio.volume = volume;
audio.playbackRate = rate;
audio.play();
playAudio(cachedUrl, { volume: volume * 100, speed: rate });
return;
}
@ -105,10 +103,7 @@ async function speak(text: string, { volume, rate, customVoice } = settings.stor
const url = URL.createObjectURL(cachedBlob);
// Save it in the in-memory cache for next time.
ttsCache.set(cacheKey, url);
const audio = new Audio(url);
audio.volume = volume;
audio.playbackRate = rate;
audio.play();
playAudio(url, { volume: volume * 100, speed: rate });
return;
}
} catch (err) {
@ -155,10 +150,7 @@ async function speak(text: string, { volume, rate, customVoice } = settings.stor
console.error("Error storing in IndexedDB:", err);
}
const audio = new Audio(url);
audio.volume = volume;
audio.playbackRate = rate;
audio.play();
playAudio(url, { volume: volume * 100, speed: rate });
}
function clean(str: string) {

View file

@ -0,0 +1,179 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { AudioPlayerAPILogger, AudioPlayerInternal, AudioPlayerOptions, audioProcessorFunctions, AudioType, identifyAudioType, playAudio } from "@api/AudioPlayer";
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "AudioPlayerAPI",
description: "API to play internal Discord audio files or external audio links.",
authors: [EquicordDevs.Etorix],
AudioType,
playAudio,
patches: [
{
find: "could not play audio",
group: true,
replacement: [
{
// Uses the audio as-is if external, otherwise checks for an internal Discord sound.
// Also force loads the internal sounds module to account for the second patch group below,
// as well as accounting for not calling the module in this patch when this.type is not DISCORD.
match: /(let \i=class.{0,900}?new Audio;\i.src=)((\i\(\d+\)).{0,50}concat\()this.name(,".mp3"\)\))/,
replace: "$3;$1this.type!==$self.AudioType.DISCORD?this.audio:$2this.audio$4"
},
{
// Adds an optional persistent boolean as well as a callback and error handler to the
// audio player which is called after the audio finishes playing and when an error occurs.
// Also processes the audio before playing to apply override functions set by plugins.
match: /(?<=constructor\((\i,\i,\i,\i)).{0,200}outputChannel=\i/,
replace: ",options){$self.buildPlayer(this,$1,options);"
},
{
// Prevents an error from the source being cleared during destroyAudio().
match: /(\i.pause\(\),(\i).src="".{0,20}?null)/,
replace: "$2.onerror=()=>{},$1"
},
{
// Applies the playback rate passed through options.
match: /(?<=(\i).onloadeddata=\(\)=>{)/,
replace: "$1.playbackRate=this._speed,"
},
{
// Makes use of the error handler if an error occurs during playback.
match: /(onerror=\()(\)=>)(\i\(Error\("[^"]+"\)\)),/,
replace: "$1error$2{this.onError?.(error);$3;},"
},
{
// Makes use of the onEnded callback and persists flag once the audio ends.
match: /(?<=onended=\(\)=>)(.{0,40}?),/,
replace: "{$self.stopAudio(this);this.onEnded?.();},"
},
{
// Makes use of the persists flag once the audio is stopped.
match: /(stop\()(\){)this.destroyAudio\(\)/,
replace: "$1restart$2$self.stopAudio(this,restart);"
},
{
// Use our playAudio function in place of the default playGiftSound.
match: /let \i=new Audio\((\(0,\i.\i\)\(\i\)).{0,35}?play\(\)/,
replace: "$self.playAudio($1)"
}
]
},
{
// Prevents Discord from forcing full volume for the "discodo" effect on client load.
// The internal sounds module being loaded on startup relies on one of these calls to volume
// regardless of if the "discodo" effect is enabled or not. This is due to the volume setter
// internally calling the ensureAudio function which is where the internal sounds module is loaded
// by default. To account for this, the module is force loaded in the first patch in the above group.
find: "UPDATE_OPEN_ON_STARTUP",
group: true,
replacement: [
{
// Pass the unprocessed volume as 1 instead of overwriting it to 1 afterwards.
match: /(?<=discodo",\i)(\);return )\i.volume=1,/,
replace: ",1$1"
},
{
// Don't re-set volume since it was already set during initialization.
match: /,(this._connectedSound.volume)=1/,
replace: ";"
}
]
}
],
stopAudio(player: AudioPlayerInternal, restart?: boolean) {
if (restart) {
player.ensureAudio().then(audio => {
audio.currentTime = 0;
audio.play();
});
} else {
if (!player.persistent) {
player.destroyAudio();
} else {
// Use _audio instead of ensureAudio() to avoid
// loading the audio if there is none loaded.
player._audio?.then(audio => {
audio.pause();
audio.currentTime = 0;
});
}
}
},
processAudio(player: AudioPlayerInternal) {
AudioPlayerAPILogger.log("Processing Audio:", player.preprocessDataOriginal);
player.preprocessDataPrevious = player.preprocessDataCurrent ? structuredClone(player.preprocessDataCurrent) : null;
player.preprocessDataCurrent = structuredClone(player.preprocessDataOriginal);
player.preprocessDataCurrent.volume *= 100;
for (const processor of Object.values(audioProcessorFunctions)) {
processor(player.preprocessDataCurrent);
}
player.preprocessDataCurrent.volume /= 100;
player.audio = player.preprocessDataCurrent.audio;
player.type = identifyAudioType(player.preprocessDataCurrent.audio);
player._volume = Math.max(0, Math.min(1, player.preprocessDataCurrent.volume));
player._speed = Math.max(0.0625, Math.min(16, player.preprocessDataCurrent.speed));
if (player.preprocessDataCurrent.audio !== player.preprocessDataPrevious?.audio) {
player.destroyAudio();
player.persistent && player.ensureAudio();
}
if (player.preprocessDataCurrent.volume !== player.preprocessDataPrevious?.volume) {
player._audio?.then(audio => {
audio.volume = player._volume;
});
}
if (player.preprocessDataCurrent.speed !== player.preprocessDataPrevious?.speed) {
player._audio?.then(audio => {
audio.playbackRate = player._speed;
});
}
AudioPlayerAPILogger.log("Processed Audio:", player.preprocessDataCurrent);
},
buildPlayer(
player: AudioPlayerInternal,
audio: string,
unused: any,
internalVolume: number,
channel: string,
options: AudioPlayerOptions = {}
) {
player.preprocessDataOriginal = {
audio: audio,
type: identifyAudioType(audio),
volume: Math.max(0, Math.min(1, (internalVolume || (options.volume ? options.volume / 100 : 1)))),
speed: Math.max(0.0625, Math.min(16, options.speed ?? 1)),
};
player.audio = player.preprocessDataOriginal.audio;
player._audio = null;
player._volume = player.preprocessDataOriginal.volume;
player._speed = player.preprocessDataOriginal.speed;
player.preload = options.preload ?? false;
player.persistent = options.persistent ?? false;
player.type = identifyAudioType(audio);
player.outputChannel = channel;
player.onEnded = options.onEnded;
player.onError = options.onError;
player.processAudio = () => this.processAudio(player);
player.processAudio();
player.preload && player.ensureAudio();
}
});