play-sound.ts

1import {
2	ChatInputCommandInteraction,
3	SlashCommandBuilder,
4	GuildMember,
5	ChannelType,
6} from 'discord.js'
7import { Command } from '@/types/bot'
8import {
9	joinVoiceChannel,
10	createAudioPlayer,
11	createAudioResource,
12	AudioPlayerStatus,
13	VoiceConnectionStatus,
14	entersState,
15} from '@discordjs/voice'
16
17export default {
18	data: new SlashCommandBuilder()
19		.setName('play-sound')
20		.setDescription('Play a sound file in your voice channel')
21		.addAttachmentOption((option) =>
22			option
23				.setName('sound')
24				.setDescription('The sound file to play (MP3, WAV, OGG, etc.)')
25				.setRequired(true),
26		)
27		.addStringOption((option) =>
28			option
29				.setName('volume')
30				.setDescription('The volume of the sound file (0-100)')
31				.setRequired(false),
32		)
33		.addChannelOption((option) =>
34			option
35				.setName('channel')
36				.setDescription('The channel to play the sound in')
37				.addChannelTypes(ChannelType.GuildVoice)
38				.setRequired(false),
39		),
40	async execute(interaction: ChatInputCommandInteraction) {
41		// Get the target voice channel (either specified or user's current channel)
42		const targetChannel =
43			interaction.options.getChannel('channel') ||
44			(interaction.member instanceof GuildMember ?
45				interaction.member.voice.channel
46			:	null)
47
48		if (!targetChannel) {
49			return interaction.reply({
50				content:
51					'You need to be in a voice channel or specify a voice channel!',
52				ephemeral: true,
53			})
54		}
55
56		const attachment = interaction.options.getAttachment('sound', true)
57		const volumeOption = interaction.options.getString('volume')
58		const volume =
59			volumeOption ? Math.min(Math.max(parseInt(volumeOption) / 100, 0), 1) : 1
60
61		// Validate file type
62		if (!attachment.contentType?.includes('audio')) {
63			return interaction.reply({
64				content: 'Please upload a valid audio file!',
65				ephemeral: true,
66			})
67		}
68
69		try {
70			// Join the voice channel
71			const connection = joinVoiceChannel({
72				channelId: targetChannel.id,
73				guildId: interaction.guildId!,
74				adapterCreator: interaction.guild!.voiceAdapterCreator as any,
75			})
76
77			// Create audio player and resource with volume
78			const player = createAudioPlayer()
79			const resource = createAudioResource(attachment.url, {
80				inlineVolume: true,
81			})
82
83			if (resource.volume) {
84				resource.volume.setVolume(volume)
85			}
86
87			// Handle connection ready
88			connection.on(VoiceConnectionStatus.Ready, () => {
89				player.play(resource)
90				connection.subscribe(player)
91			})
92
93			// Handle when audio finishes playing
94			player.on(AudioPlayerStatus.Idle, () => {
95				connection.destroy()
96			})
97
98			// Handle errors
99			connection.on('error', (error) => {
100				console.error('Voice connection error:', error)
101				connection.destroy()
102			})
103
104			player.on('error', (error) => {
105				console.error('Audio player error:', error)
106				connection.destroy()
107			})
108
109			// Wait for connection to be ready
110			await entersState(connection, VoiceConnectionStatus.Ready, 5000)
111
112			await interaction.reply({
113				content: '🎵 Playing your sound!',
114				ephemeral: true,
115			})
116		} catch (error) {
117			console.error('Error playing sound:', error)
118			await interaction.reply({
119				content: 'Failed to play the sound file!',
120				ephemeral: true,
121			})
122		}
123	},
124} as Command
125