From aae98255b19e83ce78aaea389627dbae504910f8 Mon Sep 17 00:00:00 2001 From: Yitong Xiao Date: Sun, 15 Sep 2024 17:53:05 -0400 Subject: feat: automatically turn down volume when people talks --- src/services/config.ts | 4 +++ src/services/player.ts | 67 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) (limited to 'src/services') diff --git a/src/services/config.ts b/src/services/config.ts index b6b9aea..ef2a7c9 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -14,6 +14,8 @@ const CONFIG_MAP = { YOUTUBE_API_KEY: process.env.YOUTUBE_API_KEY, SPOTIFY_CLIENT_ID: process.env.SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET: process.env.SPOTIFY_CLIENT_SECRET, + TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK: process.env.TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK === 'true', + TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK_TARGET: process.env.TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK_TARGET ?? 20, REGISTER_COMMANDS_ON_BOT: process.env.REGISTER_COMMANDS_ON_BOT === 'true', DATA_DIR, CACHE_DIR: path.join(DATA_DIR, 'cache'), @@ -43,6 +45,8 @@ export default class Config { readonly DATA_DIR!: string; readonly CACHE_DIR!: string; readonly CACHE_LIMIT_IN_BYTES!: number; + readonly TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK!: boolean; + readonly TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK_TARGET!: number; readonly BOT_STATUS!: PresenceStatusData; readonly BOT_ACTIVITY_TYPE!: Exclude; readonly BOT_ACTIVITY_URL!: string; diff --git a/src/services/player.ts b/src/services/player.ts index 5e284a6..00081ca 100644 --- a/src/services/player.ts +++ b/src/services/player.ts @@ -20,6 +20,7 @@ import FileCacheProvider from './file-cache.js'; import debug from '../utils/debug.js'; import {getGuildSettings} from '../utils/get-guild-settings.js'; import {buildPlayingMessageEmbed} from '../utils/build-embed.js'; +import Config from './config.js'; export enum MediaSource { Youtube, @@ -82,9 +83,13 @@ export default class { private readonly fileCache: FileCacheProvider; private disconnectTimer: NodeJS.Timeout | null = null; - constructor(fileCache: FileCacheProvider, guildId: string) { + private readonly channelToSpeakingUsers: Map> = new Map(); + private readonly config: Config; + + constructor(fileCache: FileCacheProvider, guildId: string, config: Config) { this.fileCache = fileCache; this.guildId = guildId; + this.config = config; } async connect(channel: VoiceChannel): Promise { @@ -96,6 +101,7 @@ export default class { this.voiceConnection = joinVoiceChannel({ channelId: channel.id, guildId: channel.guild.id, + selfDeaf: false, adapterCreator: channel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator, }); @@ -115,6 +121,9 @@ export default class { /* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */ this.currentChannel = channel; + if (newState.status === VoiceConnectionStatus.Ready) { + this.registerVoiceActivityListener(); + } }); } @@ -302,6 +311,62 @@ export default class { } } + registerVoiceActivityListener(): void { + if (!this.config.TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK || !this.voiceConnection) { + return; + } + + this.voiceConnection.receiver.speaking.on('start', (userId: string) => { + if (!this.currentChannel) { + return; + } + + const member = this.currentChannel.members.get(userId); + const channelId = this.currentChannel?.id; + + if (member) { + if (!this.channelToSpeakingUsers.has(channelId)) { + this.channelToSpeakingUsers.set(channelId, new Set()); + } + + this.channelToSpeakingUsers.get(channelId)?.add(member.id); + } + + this.suppressVoiceWhenPeopleAreSpeaking(); + }); + + this.voiceConnection.receiver.speaking.on('end', (userId: string) => { + if (!this.currentChannel) { + return; + } + + const member = this.currentChannel.members.get(userId); + const channelId = this.currentChannel.id; + if (member) { + if (!this.channelToSpeakingUsers.has(channelId)) { + this.channelToSpeakingUsers.set(channelId, new Set()); + } + + this.channelToSpeakingUsers.get(channelId)?.delete(member.id); + } + + this.suppressVoiceWhenPeopleAreSpeaking(); + }); + } + + suppressVoiceWhenPeopleAreSpeaking(): void { + if (!this.currentChannel) { + return; + } + + const speakingUsers = this.channelToSpeakingUsers.get(this.currentChannel.id); + if (speakingUsers && speakingUsers.size > 0) { + this.setVolume(this.config.TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK_TARGET); + } else { + this.setVolume(this.defaultVolume); + } + } + canGoForward(skip: number) { return (this.queuePosition + skip - 1) < this.queue.length; } -- cgit v1.2.3 From 53af0074fccad9ddb888e121ecebc497f20aab7f Mon Sep 17 00:00:00 2001 From: Hazzajenko Date: Fri, 1 Nov 2024 17:38:23 +1100 Subject: Update dotenv line to latest version of muse. Removed obsolete TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK and TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK_TARGET environment variables and corresponding class properties as we will use the discord commands instead. --- src/services/config.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'src/services') diff --git a/src/services/config.ts b/src/services/config.ts index ef2a7c9..019df07 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -5,7 +5,7 @@ import path from 'path'; import xbytes from 'xbytes'; import {ConditionalKeys} from 'type-fest'; import {ActivityType, PresenceStatusData} from 'discord.js'; -dotenv.config(); +dotenv.config({path: process.env.ENV_FILE ?? path.resolve(process.cwd(), '.env')}); export const DATA_DIR = path.resolve(process.env.DATA_DIR ? process.env.DATA_DIR : './data'); @@ -14,8 +14,6 @@ const CONFIG_MAP = { YOUTUBE_API_KEY: process.env.YOUTUBE_API_KEY, SPOTIFY_CLIENT_ID: process.env.SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET: process.env.SPOTIFY_CLIENT_SECRET, - TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK: process.env.TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK === 'true', - TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK_TARGET: process.env.TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK_TARGET ?? 20, REGISTER_COMMANDS_ON_BOT: process.env.REGISTER_COMMANDS_ON_BOT === 'true', DATA_DIR, CACHE_DIR: path.join(DATA_DIR, 'cache'), @@ -45,8 +43,6 @@ export default class Config { readonly DATA_DIR!: string; readonly CACHE_DIR!: string; readonly CACHE_LIMIT_IN_BYTES!: number; - readonly TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK!: boolean; - readonly TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK_TARGET!: number; readonly BOT_STATUS!: PresenceStatusData; readonly BOT_ACTIVITY_TYPE!: Exclude; readonly BOT_ACTIVITY_URL!: string; -- cgit v1.2.3 From 825c9a0c530d39b2b1c2f2f02904a866b7398001 Mon Sep 17 00:00:00 2001 From: Hazzajenko Date: Fri, 1 Nov 2024 17:43:39 +1100 Subject: Refactor player service to use getGuildSettings instead of passing in the config. --- src/services/player.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) (limited to 'src/services') diff --git a/src/services/player.ts b/src/services/player.ts index 00081ca..b833022 100644 --- a/src/services/player.ts +++ b/src/services/player.ts @@ -20,7 +20,7 @@ import FileCacheProvider from './file-cache.js'; import debug from '../utils/debug.js'; import {getGuildSettings} from '../utils/get-guild-settings.js'; import {buildPlayingMessageEmbed} from '../utils/build-embed.js'; -import Config from './config.js'; +import {Setting} from '@prisma/client'; export enum MediaSource { Youtube, @@ -84,12 +84,10 @@ export default class { private disconnectTimer: NodeJS.Timeout | null = null; private readonly channelToSpeakingUsers: Map> = new Map(); - private readonly config: Config; - constructor(fileCache: FileCacheProvider, guildId: string, config: Config) { + constructor(fileCache: FileCacheProvider, guildId: string) { this.fileCache = fileCache; this.guildId = guildId; - this.config = config; } async connect(channel: VoiceChannel): Promise { @@ -105,6 +103,8 @@ export default class { adapterCreator: channel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator, }); + const guildSettings = await getGuildSettings(this.guildId); + // Workaround to disable keepAlive this.voiceConnection.on('stateChange', (oldState, newState) => { /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */ @@ -122,7 +122,7 @@ export default class { this.currentChannel = channel; if (newState.status === VoiceConnectionStatus.Ready) { - this.registerVoiceActivityListener(); + this.registerVoiceActivityListener(guildSettings); } }); } @@ -311,8 +311,9 @@ export default class { } } - registerVoiceActivityListener(): void { - if (!this.config.TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK || !this.voiceConnection) { + registerVoiceActivityListener(guildSettings: Setting) { + const {turnDownVolumeWhenPeopleSpeak, turnDownVolumeWhenPeopleSpeakTarget} = guildSettings; + if (!turnDownVolumeWhenPeopleSpeak || !this.voiceConnection) { return; } @@ -332,7 +333,7 @@ export default class { this.channelToSpeakingUsers.get(channelId)?.add(member.id); } - this.suppressVoiceWhenPeopleAreSpeaking(); + this.suppressVoiceWhenPeopleAreSpeaking(turnDownVolumeWhenPeopleSpeakTarget); }); this.voiceConnection.receiver.speaking.on('end', (userId: string) => { @@ -350,18 +351,18 @@ export default class { this.channelToSpeakingUsers.get(channelId)?.delete(member.id); } - this.suppressVoiceWhenPeopleAreSpeaking(); + this.suppressVoiceWhenPeopleAreSpeaking(turnDownVolumeWhenPeopleSpeakTarget); }); } - suppressVoiceWhenPeopleAreSpeaking(): void { + suppressVoiceWhenPeopleAreSpeaking(turnDownVolumeWhenPeopleSpeakTarget: number): void { if (!this.currentChannel) { return; } const speakingUsers = this.channelToSpeakingUsers.get(this.currentChannel.id); if (speakingUsers && speakingUsers.size > 0) { - this.setVolume(this.config.TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK_TARGET); + this.setVolume(turnDownVolumeWhenPeopleSpeakTarget); } else { this.setVolume(this.defaultVolume); } -- cgit v1.2.3