Skip to content

Commit

Permalink
Implement an optional "master volume control" command #830
Browse files Browse the repository at this point in the history
* Enabled using env VOLUME_CONTROL=true
* Disabled by default
  • Loading branch information
FoxxMD committed Feb 2, 2024
1 parent 03d960d commit 012bc2f
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 13 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ SPOTIFY_CLIENT_SECRET=
# BOT_ACTIVITY_TYPE=
# BOT_ACTIVITY_URL=
# BOT_ACTIVITY=

# VOLUME_CONTROL=
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,9 @@ In the default state, Muse has the status "Online" and the text "Listening to Mu
### Bot-wide commands

If you have Muse running in a lot of guilds (10+) you may want to switch to registering commands bot-wide rather than for each guild. (The downside to this is that command updates can take up to an hour to propagate.) To do this, set the environment variable `REGISTER_COMMANDS_ON_BOT` to `true`.

### Volume Control

By default Muse does not allow "master control" of the audio volume by anyone with the idea being that users should have full control over their listening volume using Discord's built in "user volume".

To enable "master control" of the audio volume run the bot with the environmental variable `VOLUME_CONTROL=true`. Only users who can manage the guild will have volume control commands.
47 changes: 47 additions & 0 deletions src/commands/volume.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {ChatInputCommandInteraction, PermissionFlagsBits} 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 player volume level')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild.toString())
.addIntegerOption(option =>
option.setName('level')
.setDescription('percentage as number EG 0 is muted, 100 is default max 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);

if (!player.canControlVolume()) {
throw new Error('Ope, sorry but I\'m not configured to allow volume control! You can enable it by starting the bot with env VOLUME_CONTROL=true');
}

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
7 changes: 5 additions & 2 deletions src/managers/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,25 @@ import {inject, injectable} from 'inversify';
import {TYPES} from '../types.js';
import Player from '../services/player.js';
import FileCacheProvider from '../services/file-cache.js';
import Config from '../services/config';

@injectable()
export default class {
private readonly guildPlayers: Map<string, Player>;
private readonly fileCache: FileCacheProvider;
private readonly volumeControl: boolean;

constructor(@inject(TYPES.FileCache) fileCache: FileCacheProvider) {
constructor(@inject(TYPES.FileCache) fileCache: FileCacheProvider, @inject(TYPES.Config) config: Config) {
this.guildPlayers = new Map();
this.fileCache = fileCache;
this.volumeControl = config.VOLUME_CONTROL;
}

get(guildId: string): Player {
let player = this.guildPlayers.get(guildId);

if (!player) {
player = new Player(this.fileCache, guildId);
player = new Player(this.fileCache, guildId, this.volumeControl);

this.guildPlayers.set(guildId, player);
}
Expand Down
2 changes: 2 additions & 0 deletions src/services/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const CONFIG_MAP = {
BOT_ACTIVITY_TYPE: process.env.BOT_ACTIVITY_TYPE ?? 'LISTENING',
BOT_ACTIVITY_URL: process.env.BOT_ACTIVITY_URL ?? '',
BOT_ACTIVITY: process.env.BOT_ACTIVITY ?? 'music',
VOLUME_CONTROL: process.env.VOLUME_CONTROL === 'true',
} as const;

const BOT_ACTIVITY_TYPE_MAP = {
Expand All @@ -45,6 +46,7 @@ export default class Config {
readonly BOT_ACTIVITY_TYPE!: Exclude<ActivityType, ActivityType.Custom>;
readonly BOT_ACTIVITY_URL!: string;
readonly BOT_ACTIVITY!: string;
readonly VOLUME_CONTROL!: boolean;

constructor() {
for (const [key, value] of Object.entries(CONFIG_MAP)) {
Expand Down
50 changes: 40 additions & 10 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 @@ -68,16 +68,19 @@ export default class {
private queue: QueuedSong[] = [];
private queuePosition = 0;
private audioPlayer: AudioPlayer | null = null;
private audioResource: AudioResource | null = null;
private nowPlaying: QueuedSong | null = null;
private playPositionInterval: NodeJS.Timeout | undefined;
private lastSongURL = '';

private positionInSeconds = 0;
private readonly fileCache: FileCacheProvider;
private readonly volumeControl: boolean;
private disconnectTimer: NodeJS.Timeout | null = null;

constructor(fileCache: FileCacheProvider, guildId: string) {
constructor(fileCache: FileCacheProvider, guildId: string, volumeControl: boolean) {
this.fileCache = fileCache;
this.volumeControl = volumeControl;
this.guildId = guildId;
}

Expand Down Expand Up @@ -117,6 +120,7 @@ export default class {

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

Expand Down Expand Up @@ -152,9 +156,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 +219,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 +403,24 @@ export default class {
return this.queue[this.queuePosition + to];
}

setVolume(level: number): void {
// Level should be a number between 0 and 100 = 0% => 100%
// Spotify expects a float between 0 and 1 to represent this percentage
this.audioResource?.volume?.setVolume(level / 100);
}

getVolume(): number | undefined {
if (!this.volumeControl || this.audioResource === null || this.audioResource.volume === undefined) {
return undefined;
}

return this.audioResource.volume.volume * 100;
}

canControlVolume(): boolean {
return this.volumeControl;
}

private getHashForCache(url: string): string {
return hasha(url);
}
Expand Down Expand Up @@ -599,4 +615,18 @@ export default class {
resolve(returnedStream);
});
}

private createAudioStream(stream: Readable) {
return createAudioResource(stream, {
inputType: StreamType.WebmOpus,
inlineVolume: this.volumeControl,
});
}

private playAudioPlayerResource(resource: AudioResource) {
if (this.audioPlayer !== null) {
this.audioResource = resource;
this.audioPlayer.play(resource);
}
}
}
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 012bc2f

Please sign in to comment.