diff --git a/.env.example b/.env.example index cb4fc193..98c21393 100644 --- a/.env.example +++ b/.env.example @@ -18,3 +18,4 @@ SPOTIFY_CLIENT_SECRET= # BOT_ACTIVITY_TYPE= # BOT_ACTIVITY_URL= # BOT_ACTIVITY= +# TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK= \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fd81138..84bde24d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [2.9.5] - 2024-09-15 + +### Added +- An optional `TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK` config to automatically turn + down the volume when people are speaking in the channel +- An optional `TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK_TARGET` config to set the target volume when people are speaking in the channel ## [2.9.4] - 2024-08-28 diff --git a/README.md b/README.md index 7fec19f5..5fe0ffc4 100644 --- a/README.md +++ b/README.md @@ -141,3 +141,10 @@ 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`. + +### Automatically turn down volume when people speak + +You can let the bot automatically turn down the volume when people are speaking +in the channel though environment variable: +- `TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK=true` +- `TURN_DOWN_VOLUME_WHEN_PEOPLE_SPEAK_TARGET=70` (optional, default is 70%) diff --git a/package.json b/package.json index 2a2b89a6..d61839fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "muse", - "version": "2.9.4", + "version": "2.9.5", "description": "🎧 a self-hosted Discord music bot that doesn't suck ", "repository": "git@github.com:museofficial/muse.git", "author": "Max Isom ", diff --git a/src/managers/player.ts b/src/managers/player.ts index 420cf488..577d9f30 100644 --- a/src/managers/player.ts +++ b/src/managers/player.ts @@ -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.js'; @injectable() export default class { private readonly guildPlayers: Map; private readonly fileCache: FileCacheProvider; + private readonly config: Config; - 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.config = config; } 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.config); this.guildPlayers.set(guildId, player); } diff --git a/src/services/config.ts b/src/services/config.ts index b6b9aeaa..ef2a7c95 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 5e284a66..00081cac 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; }