Skip to content

Commit

Permalink
feat: Implement volume control #830 #994
Browse files Browse the repository at this point in the history
  • Loading branch information
FoxxMD committed Feb 14, 2024
1 parent 017a36c commit fadaa1a
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 10 deletions.
27 changes: 27 additions & 0 deletions src/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ export default class implements Command {
.setName('value')
.setDescription('whether to leave when everyone else leaves')
.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('percentage as number EG 0 is muted, 100 is default max volume')
.setMinValue(0)
.setMaxValue(100)
.setRequired(true)))
.addSubcommand(subcommand => subcommand
.setName('get')
.setDescription('show all settings'));
Expand Down Expand Up @@ -94,6 +103,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('👍 leave setting updated');

break;
}

case 'get': {
const embed = new EmbedBuilder().setTitle('Config');

Expand All @@ -105,6 +131,7 @@ export default class implements Command {
? 'never leave'
: `${config.secondsToWaitAfterQueueEmpties}s`,
'Leave if there are no listeners': config.leaveIfNoListeners ? 'yes' : 'no',
'Default Volume': config.defaultVolume,
};

let description = '';
Expand Down
42 changes: 42 additions & 0 deletions src/commands/volume.ts
Original file line number Diff line number Diff line change
@@ -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('percentage as number EG 0 is muted, 100 is default max (normal) volume')
.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}%`);
}
}
2 changes: 2 additions & 0 deletions src/inversify.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
});
Expand Down
54 changes: 45 additions & 9 deletions src/services/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import shuffle from 'array-shuffle';
import {
AudioPlayer,
AudioPlayerState,
AudioPlayerStatus,
AudioPlayerStatus, AudioResource,
createAudioPlayer,
createAudioResource, DiscordGatewayAdapterCreator,
joinVoiceChannel,
Expand Down Expand Up @@ -58,6 +58,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;
Expand All @@ -68,6 +70,8 @@ export default class {
private queue: QueuedSong[] = [];
private queuePosition = 0;
private audioPlayer: AudioPlayer | null = null;
private audioResource: AudioResource | null = null;
private volume?: number;
private nowPlaying: QueuedSong | null = null;
private playPositionInterval: NodeJS.Timeout | undefined;
private lastSongURL = '';
Expand All @@ -82,6 +86,13 @@ export default class {
}

async connect(channel: VoiceChannel): Promise<void> {
// Only use default volume if player volume is not already set (in the event of a reconnect we shouldn't reset)
if (this.volume === undefined) {
const settings = await getGuildSettings(this.guildId);
const {defaultVolume} = settings;
this.volume = defaultVolume;
}

this.voiceConnection = joinVoiceChannel({
channelId: channel.id,
guildId: channel.guild.id,
Expand Down Expand Up @@ -117,6 +128,7 @@ export default class {

this.voiceConnection = null;
this.audioPlayer = null;
this.audioResource = null;
}
}

Expand Down Expand Up @@ -152,9 +164,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);

Expand Down Expand Up @@ -217,11 +227,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();

Expand Down Expand Up @@ -405,6 +411,16 @@ 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 {
return this.volume ?? DEFAULT_VOLUME;
}

private getHashForCache(url: string): string {
return hasha(url);
}
Expand Down Expand Up @@ -599,4 +615,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);
}
}
3 changes: 2 additions & 1 deletion src/utils/build-embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ const getPlayerUI = (player: Player) => {
const progressBar = getProgressBar(15, 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 => {
Expand Down

0 comments on commit fadaa1a

Please sign in to comment.