Implement volume control #830 (#994)

Co-authored-by: Max Isom <hi@maxisom.me>
This commit is contained in:
Matt Foxx 2024-03-12 22:25:45 -04:00 committed by GitHub
parent 786e6fd8f5
commit 6baaffb730
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 141 additions and 11 deletions

View file

@ -40,6 +40,15 @@ export default class implements Command {
.setName('value')
.setDescription('whether to announce the next song in the queue automatically')
.setRequired(true)))
.addSubcommand(subcommand => subcommand
.setName('set-default-volume')
.setDescription('set default volume used when entering the voice channel')
.addIntegerOption(option => option
.setName('level')
.setDescription('volume percentage (0 is muted, 100 is max & default)')
.setMinValue(0)
.setMaxValue(100)
.setRequired(true)))
.addSubcommand(subcommand => subcommand
.setName('get')
.setDescription('show all settings'));
@ -121,6 +130,23 @@ export default class implements Command {
break;
}
case 'set-default-volume': {
const value = interaction.options.getInteger('level')!;
await prisma.setting.update({
where: {
guildId: interaction.guild!.id,
},
data: {
defaultVolume: value,
},
});
await interaction.reply('👍 volume setting updated');
break;
}
case 'get': {
const embed = new EmbedBuilder().setTitle('Config');
@ -133,6 +159,7 @@ export default class implements Command {
: `${config.secondsToWaitAfterQueueEmpties}s`,
'Leave if there are no listeners': config.leaveIfNoListeners ? 'yes' : 'no',
'Auto announce next song in queue': config.autoAnnounceNextSong ? 'yes' : 'no',
'Default Volume': config.defaultVolume,
};
let description = '';

42
src/commands/volume.ts Normal file
View file

@ -0,0 +1,42 @@
import {ChatInputCommandInteraction} from 'discord.js';
import {TYPES} from '../types.js';
import {inject, injectable} from 'inversify';
import PlayerManager from '../managers/player.js';
import Command from '.';
import {SlashCommandBuilder} from '@discordjs/builders';
@injectable()
export default class implements Command {
public readonly slashCommand = new SlashCommandBuilder()
.setName('volume')
.setDescription('set current player volume level')
.addIntegerOption(option =>
option.setName('level')
.setDescription('volume percentage (0 is muted, 100 is max & default)')
.setMinValue(0)
.setMaxValue(100)
.setRequired(true),
);
public requiresVC = true;
private readonly playerManager: PlayerManager;
constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager) {
this.playerManager = playerManager;
}
public async execute(interaction: ChatInputCommandInteraction): Promise<void> {
const player = this.playerManager.get(interaction.guild!.id);
const currentSong = player.getCurrent();
if (!currentSong) {
throw new Error('nothing is playing');
}
const level = interaction.options.getInteger('level') ?? 100;
player.setVolume(level);
await interaction.reply(`Set volume to ${level}%`);
}
}

View file

@ -37,6 +37,7 @@ import Shuffle from './commands/shuffle.js';
import Skip from './commands/skip.js';
import Stop from './commands/stop.js';
import Unskip from './commands/unskip.js';
import Volume from './commands/volume.js';
import ThirdParty from './services/third-party.js';
import FileCacheProvider from './services/file-cache.js';
import KeyValueCacheProvider from './services/key-value-cache.js';
@ -85,6 +86,7 @@ container.bind<SpotifyAPI>(TYPES.Services.SpotifyAPI).to(SpotifyAPI).inSingleton
Skip,
Stop,
Unskip,
Volume,
].forEach(command => {
container.bind<Command>(TYPES.Command).to(command).inSingletonScope();
});

View file

@ -8,7 +8,7 @@ import shuffle from 'array-shuffle';
import {
AudioPlayer,
AudioPlayerState,
AudioPlayerStatus,
AudioPlayerStatus, AudioResource,
createAudioPlayer,
createAudioResource, DiscordGatewayAdapterCreator,
joinVoiceChannel,
@ -59,6 +59,8 @@ export interface PlayerEvents {
type YTDLVideoFormat = videoFormat & {loudnessDb?: number};
export const DEFAULT_VOLUME = 100;
export default class {
public voiceConnection: VoiceConnection | null = null;
public status = STATUS.PAUSED;
@ -69,6 +71,9 @@ export default class {
private queue: QueuedSong[] = [];
private queuePosition = 0;
private audioPlayer: AudioPlayer | null = null;
private audioResource: AudioResource | null = null;
private volume?: number;
private defaultVolume: number = DEFAULT_VOLUME;
private nowPlaying: QueuedSong | null = null;
private playPositionInterval: NodeJS.Timeout | undefined;
private lastSongURL = '';
@ -83,6 +88,11 @@ export default class {
}
async connect(channel: VoiceChannel): Promise<void> {
// Always get freshest default volume setting value
const settings = await getGuildSettings(this.guildId);
const {defaultVolume = DEFAULT_VOLUME} = settings;
this.defaultVolume = defaultVolume;
this.voiceConnection = joinVoiceChannel({
channelId: channel.id,
guildId: channel.guild.id,
@ -120,6 +130,7 @@ export default class {
this.voiceConnection = null;
this.audioPlayer = null;
this.audioResource = null;
}
}
@ -155,9 +166,7 @@ export default class {
},
});
this.voiceConnection.subscribe(this.audioPlayer);
this.audioPlayer.play(createAudioResource(stream, {
inputType: StreamType.WebmOpus,
}));
this.playAudioPlayerResource(this.createAudioStream(stream));
this.attachListeners();
this.startTrackingPosition(positionSeconds);
@ -220,11 +229,7 @@ export default class {
},
});
this.voiceConnection.subscribe(this.audioPlayer);
const resource = createAudioResource(stream, {
inputType: StreamType.WebmOpus,
});
this.audioPlayer.play(resource);
this.playAudioPlayerResource(this.createAudioStream(stream));
this.attachListeners();
@ -408,6 +413,17 @@ export default class {
return this.queue[this.queuePosition + to];
}
setVolume(level: number): void {
// Level should be a number between 0 and 100 = 0% => 100%
this.volume = level;
this.setAudioPlayerVolume(level);
}
getVolume(): number {
// Only use default volume if player volume is not already set (in the event of a reconnect we shouldn't reset)
return this.volume ?? this.defaultVolume;
}
private getHashForCache(url: string): string {
return hasha(url);
}
@ -610,4 +626,24 @@ export default class {
resolve(returnedStream);
});
}
private createAudioStream(stream: Readable) {
return createAudioResource(stream, {
inputType: StreamType.WebmOpus,
inlineVolume: true,
});
}
private playAudioPlayerResource(resource: AudioResource) {
if (this.audioPlayer !== null) {
this.audioResource = resource;
this.setAudioPlayerVolume();
this.audioPlayer.play(this.audioResource);
}
}
private setAudioPlayerVolume(level?: number) {
// Audio resource expects a float between 0 and 1 to represent level percentage
this.audioResource?.volume?.setVolume((level ?? this.getVolume()) / 100);
}
}

View file

@ -44,10 +44,11 @@ const getPlayerUI = (player: Player) => {
const position = player.getPosition();
const button = player.status === STATUS.PLAYING ? '⏹️' : '▶️';
const progressBar = getProgressBar(15, position / song.length);
const progressBar = getProgressBar(10, position / song.length);
const elapsedTime = song.isLive ? 'live' : `${prettyTime(position)}/${prettyTime(song.length)}`;
const loop = player.loopCurrentSong ? '🔂' : player.loopCurrentQueue ? '🔁' : '';
return `${button} ${progressBar} \`[${elapsedTime}]\` 🔉 ${loop}`;
const vol: string = typeof player.getVolume() === 'number' ? `${player.getVolume()!}%` : '';
return `${button} ${progressBar} \`[${elapsedTime}]\`🔉 ${vol} ${loop}`;
};
export const buildPlayingMessageEmbed = (player: Player): EmbedBuilder => {