Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement volume control #830 #994

Merged
merged 9 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added 🔊
- A `/volume` command is now available.
- Set the default volume with `/config set-default-volume`

## [2.6.0] - 2024-03-03

### Added
Expand Down
17 changes: 17 additions & 0 deletions migrations/20240312135407_add_default_volume/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Setting" (
"guildId" TEXT NOT NULL PRIMARY KEY,
"playlistLimit" INTEGER NOT NULL DEFAULT 50,
"secondsToWaitAfterQueueEmpties" INTEGER NOT NULL DEFAULT 30,
"leaveIfNoListeners" BOOLEAN NOT NULL DEFAULT true,
"autoAnnounceNextSong" BOOLEAN NOT NULL DEFAULT false,
"defaultVolume" INTEGER NOT NULL DEFAULT 100,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Setting" ("autoAnnounceNextSong", "createdAt", "guildId", "leaveIfNoListeners", "playlistLimit", "secondsToWaitAfterQueueEmpties", "updatedAt") SELECT "autoAnnounceNextSong", "createdAt", "guildId", "leaveIfNoListeners", "playlistLimit", "secondsToWaitAfterQueueEmpties", "updatedAt" FROM "Setting";
DROP TABLE "Setting";
ALTER TABLE "new_Setting" RENAME TO "Setting";
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;
1 change: 1 addition & 0 deletions schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ model Setting {
secondsToWaitAfterQueueEmpties Int @default(30)
leaveIfNoListeners Boolean @default(true)
autoAnnounceNextSong Boolean @default(false)
defaultVolume Int @default(100)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Expand Down
27 changes: 27 additions & 0 deletions src/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down Expand Up @@ -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');

Expand All @@ -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 = '';
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('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}%`);
}
}
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 @@ -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;
Expand All @@ -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 = '';
Expand All @@ -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,
Expand Down Expand Up @@ -120,6 +130,7 @@ export default class {

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

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
}
5 changes: 3 additions & 2 deletions src/utils/build-embed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
Loading