Discord Stem Separation Bot
~30 minBuild a bot for your music community
What You'll Build
A Discord bot that lets users separate stems from audio files with simple slash commands. Perfect for music production servers, remix communities, and DJ groups.
/separate
Slash commands
All Stems
Vocals, drums, bass, etc.
Community Ready
Multi-user support
How It Works
User:/separate vocals+ attached audio file
Bot:๐ต Processing "song.mp3"... (estimated: 45 seconds)
Bot:โ
Done! Here are your stems:
๐ vocals.mp3
๐ instrumental.mp3
Prerequisites
- Node.js 18+ installed
- StemSplit API key (get one here)
- Discord account with a server to test
1
Create Discord Application
- Go to Discord Developer Portal
- Click "New Application" and give it a name (e.g., "StemSplit Bot")
- Go to the "Bot" tab and click "Add Bot"
- Copy the Bot Token (keep this secret!)
- Under "Privileged Gateway Intents", enable:
- Message Content Intent
- Go to "OAuth2" โ "URL Generator", select:
- Scopes:
bot,applications.commands - Bot Permissions: Send Messages, Attach Files, Use Slash Commands
- Scopes:
- Use the generated URL to invite the bot to your server
2
Set Up Your Project
Terminal
# Create project directory
mkdir stemsplit-discord-bot && cd stemsplit-discord-bot
# Initialize npm project
npm init -y
# Install dependencies
npm install discord.js axios dotenv
# Create files
touch index.js .env.env
DISCORD_TOKEN=your_discord_bot_token_here
STEMSPLIT_API_KEY=sk_live_your_api_key_here3
Build the Bot
Here's the complete bot code with slash commands:
index.js
require('dotenv').config();
const { Client, GatewayIntentBits, SlashCommandBuilder, REST, Routes, AttachmentBuilder } = require('discord.js');
const axios = require('axios');
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages]
});
const STEMSPLIT_API = 'https://stemsplit.io/api/v1';
// Register slash commands
const commands = [
new SlashCommandBuilder()
.setName('separate')
.setDescription('Separate stems from an audio file')
.addAttachmentOption(option =>
option.setName('audio')
.setDescription('The audio file to process')
.setRequired(true))
.addStringOption(option =>
option.setName('type')
.setDescription('What to extract')
.setRequired(true)
.addChoices(
{ name: 'Vocals Only', value: 'VOCALS' },
{ name: 'Instrumental Only', value: 'INSTRUMENTAL' },
{ name: 'All Stems (vocals, drums, bass, other)', value: 'ALL_STEMS' }
))
].map(cmd => cmd.toJSON());
// Deploy commands on startup
client.once('ready', async () => {
console.log(`โ
Logged in as ${client.user.tag}`);
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);
await rest.put(Routes.applicationCommands(client.user.id), { body: commands });
console.log('โ
Slash commands registered');
});
// Handle slash commands
client.on('interactionCreate', async interaction => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName !== 'separate') return;
const attachment = interaction.options.getAttachment('audio');
const outputType = interaction.options.getString('type');
// Validate file
const validTypes = ['audio/mpeg', 'audio/wav', 'audio/flac', 'audio/mp4', 'video/mp4'];
if (!validTypes.some(t => attachment.contentType?.includes(t))) {
return interaction.reply({ content: 'โ Please upload an audio file (MP3, WAV, FLAC)', ephemeral: true });
}
// Check file size (25MB Discord limit, but let's be safe)
if (attachment.size > 20 * 1024 * 1024) {
return interaction.reply({ content: 'โ File too large. Max 20MB.', ephemeral: true });
}
await interaction.deferReply();
try {
// Create job with source URL (Discord CDN)
const jobResponse = await axios.post(`${STEMSPLIT_API}/jobs`, {
sourceUrl: attachment.url,
outputType: outputType,
quality: 'BEST',
outputFormat: 'MP3'
}, {
headers: { 'Authorization': `Bearer ${process.env.STEMSPLIT_API_KEY}` }
});
const jobId = jobResponse.data.id;
await interaction.editReply(`๐ต Processing "${attachment.name}"... This may take a minute.`);
// Poll for completion
let job;
while (true) {
await new Promise(r => setTimeout(r, 5000)); // Wait 5 seconds
const statusResponse = await axios.get(`${STEMSPLIT_API}/jobs/${jobId}`, {
headers: { 'Authorization': `Bearer ${process.env.STEMSPLIT_API_KEY}` }
});
job = statusResponse.data;
if (job.status === 'COMPLETED') break;
if (job.status === 'FAILED') {
return interaction.editReply(`โ Processing failed: ${job.error || 'Unknown error'}`);
}
}
// Download and send stems
const files = [];
for (const [stemName, stemData] of Object.entries(job.outputs)) {
const response = await axios.get(stemData.url, { responseType: 'arraybuffer' });
files.push(new AttachmentBuilder(Buffer.from(response.data), { name: `${stemName}.mp3` }));
}
await interaction.editReply({
content: `โ
Done! Here are your stems for "${attachment.name}":`,
files: files
});
} catch (error) {
console.error('Error:', error.response?.data || error.message);
await interaction.editReply(`โ Error: ${error.response?.data?.error || error.message}`);
}
});
client.login(process.env.DISCORD_TOKEN);4
Run Your Bot
Terminal
node index.jsYou should see:
โ
Logged in as StemSplit Bot#1234
โ
Slash commands registered
Now go to your Discord server and try /separate!
5
Deploy for 24/7 Uptime
For production, deploy your bot to a hosting service:
Railway (Recommended)
Free tier available. Just connect your GitHub repo and set environment variables.
railway.app โYour Own VPS
Use PM2 to keep the bot running: pm2 start index.js --name stemsplit-bot
๐ Enhancement Ideas
YouTube Support
Use our YouTube API to let users paste YouTube links
User Credit System
Track usage per user with a database like SQLite
Queue System
Handle multiple requests with a job queue
Webhooks
Use webhooks instead of polling for faster responses
Tips for Music Servers
- โข Set up a dedicated #stem-separation channel to keep things organized
- โข Add rate limiting to prevent abuse (e.g., 5 requests per user per hour)
- โข Consider a "premium" role for unlimited access funded by Patreon
- โข Credit usage: ~1 credit per second of audio processed