added effects to songs!

This commit is contained in:
2025-11-27 15:13:59 +00:00
parent c7e033acb6
commit 9660246d6a
3 changed files with 535 additions and 82 deletions

View File

@@ -176,3 +176,170 @@ class music(commands.Cog):
# Safe to ignore error for now # Safe to ignore error for now
ctx.voice_client.stop() ctx.voice_client.stop()
@commands.command(
help="Toggle loop mode: off -> song -> queue -> off",
aliases=['l', 'repeat'])
async def loop(self, ctx: Context, mode: str = None):
"""Toggle between loop modes or set a specific mode"""
server = ctx.guild
if server is None:
raise commands.CommandError("Command must be issued in a server")
current_mode = await queue.get_loop_mode(server.id)
# If no mode specified, cycle through modes
if mode is None:
if current_mode == 'off':
new_mode = 'song'
elif current_mode == 'song':
new_mode = 'queue'
else:
new_mode = 'off'
else:
# Set specific mode
mode = mode.lower()
if mode not in ['off', 'song', 'queue']:
raise commands.CommandError("Loop mode must be: off, song, or queue")
new_mode = mode
await queue.set_loop_mode(server.id, new_mode)
# Response messages
emojis = {'off': '⏹️', 'song': '🔂', 'queue': '🔁'}
messages = {
'off': 'Loop disabled',
'song': 'Looping current song 🔂',
'queue': 'Looping entire queue 🔁'
}
await ctx.send(f"{emojis[new_mode]} {messages[new_mode]}")
@commands.command(
help="Shuffle the queue randomly",
aliases=['mix', 'randomize'])
async def shuffle(self, ctx: Context):
"""Shuffle all songs in the queue"""
server = ctx.guild
if server is None:
raise commands.CommandError("Command must be issued in a server")
success = await queue.shuffle_queue(server.id)
if not success:
await ctx.send("🚫 Not enough songs in the queue to shuffle!")
else:
await ctx.message.add_reaction('🔀')
await ctx.send("🔀 Queue shuffled!")
@commands.command(
help="Set playback volume (0-200%)",
aliases=['vol', 'v'])
async def volume(self, ctx: Context, vol: str = None):
"""Set or display the current volume"""
server = ctx.guild
if server is None:
raise commands.CommandError("Command must be issued in a server")
if vol is None:
# Display current volume
current_vol = await queue.get_volume(server.id)
await ctx.send(f"🔊 Current volume: {current_vol}%")
return
# Set volume
if not vol.isdigit():
raise commands.CommandError("Volume must be a number (0-200)")
vol = int(vol)
if vol < 0 or vol > 200:
raise commands.CommandError("Volume must be between 0 and 200")
new_vol = await queue.set_volume(server.id, vol)
# Update the current playing song's volume if something is playing
if ctx.voice_client and ctx.voice_client.source:
ctx.voice_client.source.volume = new_vol / 100.0
# Pick an emoji based on volume
if new_vol == 0:
emoji = '🔇'
elif new_vol < 50:
emoji = '🔉'
elif new_vol < 100:
emoji = '🔊'
else:
emoji = '📢'
await ctx.send(f"{emoji} Volume set to {new_vol}%")
@commands.command(
help="Apply audio effects to playback",
aliases=['fx', 'filter'])
async def effect(self, ctx: Context, effect_name: str = None):
"""Apply or list audio effects"""
server = ctx.guild
if server is None:
raise commands.CommandError("Command must be issued in a server")
# If no effect specified, show current effect and list options
if effect_name is None:
current = await queue.get_effect(server.id)
emoji = queue.get_effect_emoji(current)
desc = queue.get_effect_description(current)
effects_list = '\n'.join([
f"`{e}` - {queue.get_effect_description(e)}"
for e in queue.list_all_effects()[:7] # Show first 7
])
more_effects = '\n'.join([
f"`{e}` - {queue.get_effect_description(e)}"
for e in queue.list_all_effects()[7:] # Show rest
])
await ctx.send(
f"{emoji} **Current effect:** {current} - {desc}\n\n"
f"**Available effects:**\n{effects_list}\n\n"
f"**More effects:**\n{more_effects}\n\n"
f"Use `=effect <name>` to apply an effect!"
)
return
# Set effect
effect_name = effect_name.lower()
if effect_name not in queue.list_all_effects():
raise commands.CommandError(
f"Unknown effect! Use `=effect` to see available effects."
)
await queue.set_effect(server.id, effect_name)
emoji = queue.get_effect_emoji(effect_name)
desc = queue.get_effect_description(effect_name)
# Special warning for earrape
if effect_name == 'earrape':
await ctx.send(
f"⚠️ **EARRAPE MODE ACTIVATED** ⚠️\n"
f"RIP your eardrums. Effect will apply to next song.\n"
f"Use `=effect none` to disable."
)
else:
await ctx.send(
f"{emoji} Effect set to **{effect_name}**\n"
f"{desc}\n"
f"Effect will apply to next song!"
)
# If something is currently playing, notify about skip
if ctx.voice_client and ctx.voice_client.is_playing():
await ctx.send("💡 Tip: Use `=skip` to apply effect to current song immediately!")

View File

@@ -1,5 +1,6 @@
from http import server from http import server
import sqlite3 import sqlite3
import random
import discord import discord
import asyncio import asyncio
@@ -8,14 +9,98 @@ from .translate import search_song
db_path = "./data/music.db" db_path = "./data/music.db"
FFMPEG_OPTS = { # Base FFmpeg options (will be modified by effects)
'before_options': BASE_FFMPEG_OPTS = {
'-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', 'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
'options': '-vn'
'options':
'-vn'
} }
# Audio effects configurations
def get_effect_options(effect_name):
"""Get FFmpeg options for a specific audio effect"""
effects = {
'none': {
**BASE_FFMPEG_OPTS
},
'bassboost': {
**BASE_FFMPEG_OPTS,
'options': '-vn -af "bass=g=20,dynaudnorm"'
},
'nightcore': {
**BASE_FFMPEG_OPTS,
'options': '-vn -af "asetrate=48000*1.25,atempo=1.25,bass=g=5"'
},
'slowed': {
**BASE_FFMPEG_OPTS,
'options': '-vn -af "atempo=0.8,asetrate=48000*0.8,aecho=0.8:0.9:1000:0.3"'
},
'earrape': {
**BASE_FFMPEG_OPTS,
# Aggressive compression + hard clipping + bitcrushing for maximum distortion
'options': '-vn -af "volume=8,acompressor=threshold=0.001:ratio=30:attack=0.1:release=5,acrusher=bits=8:mix=0.7,volume=2,alimiter=limit=0.8"'
},
'deepfry': {
**BASE_FFMPEG_OPTS,
# Extreme bitcrushing + bass boost + compression (meme audio effect)
'options': '-vn -af "acrusher=bits=4:mode=log:aa=1,bass=g=15,acompressor=threshold=0.001:ratio=20,volume=3"'
},
'distortion': {
**BASE_FFMPEG_OPTS,
# Pure bitcrushing distortion
'options': '-vn -af "acrusher=bits=6:mix=0.9,acompressor=threshold=0.01:ratio=15,volume=2"'
},
'reverse': {
**BASE_FFMPEG_OPTS,
'options': '-vn -af "areverse"'
},
'chipmunk': {
**BASE_FFMPEG_OPTS,
'options': '-vn -af "asetrate=48000*1.5,atempo=1.5"'
},
'demonic': {
**BASE_FFMPEG_OPTS,
'options': '-vn -af "asetrate=48000*0.7,atempo=0.7,aecho=0.8:0.9:1000:0.5"'
},
'underwater': {
**BASE_FFMPEG_OPTS,
'options': '-vn -af "lowpass=f=800,aecho=0.8:0.88:60:0.4"'
},
'robot': {
**BASE_FFMPEG_OPTS,
'options': '-vn -af "afftfilt=real=\'hypot(re,im)*sin(0)\':imag=\'hypot(re,im)*cos(0)\':win_size=512:overlap=0.75"'
},
'8d': {
**BASE_FFMPEG_OPTS,
'options': '-vn -af "apulsator=hz=0.08"'
},
'vibrato': {
**BASE_FFMPEG_OPTS,
'options': '-vn -af "vibrato=f=7:d=0.5"'
},
'tremolo': {
**BASE_FFMPEG_OPTS,
'options': '-vn -af "tremolo=f=5:d=0.9"'
},
'echo': {
**BASE_FFMPEG_OPTS,
'options': '-vn -af "aecho=0.8:0.88:60:0.4"'
},
'phone': {
**BASE_FFMPEG_OPTS,
# Sounds like a phone call (bandpass filter)
'options': '-vn -af "bandpass=f=1500:width_type=h:w=1000,volume=2"'
},
'megaphone': {
**BASE_FFMPEG_OPTS,
# Megaphone/radio effect
'options': '-vn -af "highpass=f=300,lowpass=f=3000,volume=2,acompressor=threshold=0.1:ratio=8"'
}
}
return effects.get(effect_name, effects['none'])
# Creates the tables if they don't exist # Creates the tables if they don't exist
def initialize_tables(): def initialize_tables():
# Connect to the database # Connect to the database
@@ -26,12 +111,31 @@ def initialize_tables():
cursor.execute('''CREATE TABLE IF NOT EXISTS servers ( cursor.execute('''CREATE TABLE IF NOT EXISTS servers (
server_id TEXT PRIMARY KEY, server_id TEXT PRIMARY KEY,
is_playing INTEGER DEFAULT 0, is_playing INTEGER DEFAULT 0,
song_name TEXT song_name TEXT,
loop_mode TEXT DEFAULT 'off',
volume INTEGER DEFAULT 100,
effect TEXT DEFAULT 'none'
);''') );''')
# Set all to not playing # Set all to not playing
cursor.execute("UPDATE servers SET is_playing = 0;") cursor.execute("UPDATE servers SET is_playing = 0;")
# Add new columns if they don't exist (for existing databases)
try:
cursor.execute("ALTER TABLE servers ADD COLUMN loop_mode TEXT DEFAULT 'off';")
except sqlite3.OperationalError:
pass # Column already exists
try:
cursor.execute("ALTER TABLE servers ADD COLUMN volume INTEGER DEFAULT 100;")
except sqlite3.OperationalError:
pass # Column already exists
try:
cursor.execute("ALTER TABLE servers ADD COLUMN effect TEXT DEFAULT 'none';")
except sqlite3.OperationalError:
pass # Column already exists
# Create queue table if it doesn't exist # Create queue table if it doesn't exist
cursor.execute('''CREATE TABLE IF NOT EXISTS songs ( cursor.execute('''CREATE TABLE IF NOT EXISTS songs (
server_id TEXT NOT NULL, server_id TEXT NOT NULL,
@@ -105,7 +209,7 @@ async def add_song(server_id, details, queued_by):
return max_order_num return max_order_num
# Pop song from server # Pop song from server (respects loop mode)
async def pop(server_id, ignore=False): async def pop(server_id, ignore=False):
# Connect to db # Connect to db
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
@@ -138,10 +242,19 @@ async def pop(server_id, ignore=False):
song = song[0] song = song[0]
await set_current_song(server_id, song['title']) await set_current_song(server_id, song['title'])
# Check loop mode before removing
loop_mode = await get_loop_mode(server_id)
if loop_mode != 'song': # Only remove if not looping song
await mark_song_as_finished(server_id, result[3]) await mark_song_as_finished(server_id, result[3])
return song['url'] return song['url']
await set_current_song(server_id, result[4]) await set_current_song(server_id, result[4])
# Check loop mode before removing
loop_mode = await get_loop_mode(server_id)
if loop_mode != 'song': # Only remove if not looping song
await mark_song_as_finished(server_id, result[3]) await mark_song_as_finished(server_id, result[3])
return result[1] return result[1]
@@ -159,8 +272,8 @@ async def add_server(server_id, cursor, conn):
# If the server doesn't exist, add it # If the server doesn't exist, add it
if not server_exists: if not server_exists:
cursor.execute('''INSERT INTO servers (server_id) cursor.execute('''INSERT INTO servers (server_id, loop_mode, volume, effect)
VALUES (?)''', (server_id,)) VALUES (?, 'off', 100, 'none')''', (server_id,))
conn.commit() conn.commit()
@@ -313,9 +426,223 @@ async def grab_songs(server_id):
return max, songs return max, songs
# ============= LOOP/SHUFFLE/VOLUME FEATURES =============
# Get/Set loop mode
async def get_loop_mode(server_id):
"""Get the current loop mode: 'off', 'song', or 'queue'"""
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
await add_server(server_id, cursor, conn)
cursor.execute("""SELECT loop_mode
FROM servers
WHERE server_id = ?""",
(server_id,))
result = cursor.fetchone()
conn.commit()
conn.close()
return result[0] if result else 'off'
async def set_loop_mode(server_id, mode):
"""Set loop mode: 'off', 'song', or 'queue'"""
if mode not in ['off', 'song', 'queue']:
return False
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
await add_server(server_id, cursor, conn)
cursor.execute("""UPDATE servers
SET loop_mode = ?
WHERE server_id = ?""",
(mode, server_id))
conn.commit()
conn.close()
return True
# Get/Set volume
async def get_volume(server_id):
"""Get the current volume (0-200)"""
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
await add_server(server_id, cursor, conn)
cursor.execute("""SELECT volume
FROM servers
WHERE server_id = ?""",
(server_id,))
result = cursor.fetchone()
conn.commit()
conn.close()
return result[0] if result else 100
async def set_volume(server_id, volume):
"""Set volume (0-200)"""
volume = max(0, min(200, volume)) # Clamp between 0-200
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
await add_server(server_id, cursor, conn)
cursor.execute("""UPDATE servers
SET volume = ?
WHERE server_id = ?""",
(volume, server_id))
conn.commit()
conn.close()
return volume
# Shuffle the queue
async def shuffle_queue(server_id):
"""Randomize the order of songs in the queue"""
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
await add_server(server_id, cursor, conn)
# Get all songs
cursor.execute('''SELECT position, song_link, queued_by, title, thumbnail, duration
FROM songs
WHERE server_id = ?
ORDER BY position''', (server_id,))
songs = cursor.fetchall()
if len(songs) <= 1:
conn.close()
return False # Nothing to shuffle
# Shuffle the songs (keep positions but randomize order)
random.shuffle(songs)
# Delete all current songs
cursor.execute('''DELETE FROM songs WHERE server_id = ?''', (server_id,))
# Re-insert in shuffled order
for i, song in enumerate(songs):
cursor.execute("""INSERT INTO songs (server_id, song_link, queued_by, position, title, thumbnail, duration)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(server_id, song[1], song[2], i, song[3], song[4], song[5]))
conn.commit()
conn.close()
return True
# ============= AUDIO EFFECTS FEATURES =============
async def get_effect(server_id):
"""Get the current audio effect"""
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
await add_server(server_id, cursor, conn)
cursor.execute("""SELECT effect
FROM servers
WHERE server_id = ?""",
(server_id,))
result = cursor.fetchone()
conn.commit()
conn.close()
return result[0] if result else 'none'
async def set_effect(server_id, effect_name):
"""Set the audio effect"""
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
await add_server(server_id, cursor, conn)
cursor.execute("""UPDATE servers
SET effect = ?
WHERE server_id = ?""",
(effect_name, server_id))
conn.commit()
conn.close()
return True
def list_all_effects():
"""Return a list of all available effects"""
return [
'none', 'bassboost', 'nightcore', 'slowed', 'earrape', 'deepfry', 'distortion',
'reverse', 'chipmunk', 'demonic', 'underwater', 'robot',
'8d', 'vibrato', 'tremolo', 'echo', 'phone', 'megaphone'
]
def get_effect_emoji(effect_name):
"""Get emoji representation for each effect"""
emojis = {
'none': '🔊',
'bassboost': '🔉💥',
'nightcore': '⚡🎀',
'slowed': '🐌💤',
'earrape': '💀📢',
'deepfry': '🍟💥',
'distortion': '⚡🔊',
'reverse': '⏪🔄',
'chipmunk': '🐿️',
'demonic': '😈🔥',
'underwater': '🌊💦',
'robot': '🤖',
'8d': '🎧🌀',
'vibrato': '〰️',
'tremolo': '📳',
'echo': '🗣️💭',
'phone': '📞',
'megaphone': '📢📣'
}
return emojis.get(effect_name, '🔊')
def get_effect_description(effect_name):
"""Get user-friendly description for each effect"""
descriptions = {
'none': 'Normal audio',
'bassboost': 'MAXIMUM BASS 🔊',
'nightcore': 'Speed + pitch up (anime vibes)',
'slowed': 'Slowed + reverb (TikTok aesthetic)',
'earrape': '⚠️ Aggressive compression + distortion + clipping ⚠️',
'deepfry': '🍟 EXTREME bitcrushing + bass (meme audio) 🍟',
'distortion': 'Heavy bitcrushing distortion',
'reverse': 'Plays audio BACKWARDS',
'chipmunk': 'High pitched and fast (Alvin mode)',
'demonic': 'Low pitched and slow (cursed)',
'underwater': 'Muffled underwater sound',
'robot': 'Robotic vocoder',
'8d': 'Panning audio (use headphones!)',
'vibrato': 'Warbling pitch effect',
'tremolo': 'Volume oscillation',
'echo': 'Echo/reverb effect',
'phone': 'Sounds like a phone call',
'megaphone': 'Megaphone/radio effect'
}
return descriptions.get(effect_name, 'Unknown effect')
# Play and loop songs in server # Play and loop songs in server
async def play(ctx): async def play(ctx):
"""Main playback loop - plays songs from queue sequentially""" """Main playback loop - plays songs from queue sequentially with effects"""
server_id = ctx.guild.id server_id = ctx.guild.id
voice_client = ctx.voice_client voice_client = ctx.voice_client
@@ -337,8 +664,16 @@ async def play(ctx):
return return
try: try:
# Create audio source # Get volume and effect settings
audio_source = discord.FFmpegPCMAudio(url, **FFMPEG_OPTS) volume_percent = await get_volume(server_id)
volume = volume_percent / 100.0 # Convert to 0.0-2.0 range
current_effect = await get_effect(server_id)
ffmpeg_opts = get_effect_options(current_effect)
# Create audio source with effect and volume control
audio_source = discord.FFmpegPCMAudio(url, **ffmpeg_opts)
audio_source = discord.PCMVolumeTransformer(audio_source, volume=volume)
# Play with callback to continue queue # Play with callback to continue queue
def after_playing(error): def after_playing(error):

View File

@@ -3,10 +3,6 @@ from discord.ext.commands.context import Context
from discord.ext.commands.converter import CommandError from discord.ext.commands.converter import CommandError
import config import config
from . import queue from . import queue
import asyncio
# Track last activity time for each server
last_activity = {}
# Joining/moving to the user's vc in a guild # Joining/moving to the user's vc in a guild
async def join_vc(ctx: Context): async def join_vc(ctx: Context):
@@ -24,14 +20,11 @@ async def join_vc(ctx: Context):
# Join or move to the user's vc # Join or move to the user's vc
if ctx.voice_client is None: if ctx.voice_client is None:
vc = await vc.connect(reconnect=True, timeout=60.0) vc = await vc.connect()
else: else:
# Safe to ignore type error for now # Safe to ignore type error for now
vc = await ctx.voice_client.move_to(vc) vc = await ctx.voice_client.move_to(vc)
# Update last activity
last_activity[ctx.guild.id] = asyncio.get_event_loop().time()
return vc return vc
@@ -52,60 +45,8 @@ async def leave_vc(ctx: Context):
if author_vc is None or vc != author_vc: if author_vc is None or vc != author_vc:
raise CommandError("You are not in this voice channel") raise CommandError("You are not in this voice channel")
# Clear the queue for this server # Disconnect
await queue.clear(ctx.guild.id) await ctx.voice_client.disconnect(force=False)
# Stop any currently playing audio
if ctx.voice_client.is_playing():
ctx.voice_client.stop()
# Disconnect with force to ensure it actually leaves
try:
await ctx.voice_client.disconnect(force=True)
except Exception as e:
print(f"Error disconnecting: {e}")
# If regular disconnect fails, try cleanup
await ctx.voice_client.cleanup()
# Remove from activity tracker
if ctx.guild.id in last_activity:
del last_activity[ctx.guild.id]
# Auto-disconnect if inactive
async def check_inactivity(bot):
"""Background task to check for inactive voice connections"""
while True:
try:
current_time = asyncio.get_event_loop().time()
for guild_id, last_time in list(last_activity.items()):
# If inactive for more than 5 minutes
if current_time - last_time > 300: # 300 seconds = 5 minutes
# Find the guild and voice client
guild = bot.get_guild(guild_id)
if guild and guild.voice_client:
# Check if not playing
if not guild.voice_client.is_playing():
print(f"Auto-disconnecting from {guild.name} due to inactivity")
await queue.clear(guild_id)
try:
await guild.voice_client.disconnect(force=True)
except:
pass
del last_activity[guild_id]
# Check every 30 seconds
await asyncio.sleep(30)
except Exception as e:
print(f"Error in inactivity checker: {e}")
await asyncio.sleep(30)
# Update activity timestamp when playing
def update_activity(guild_id):
"""Call this when a song starts playing"""
last_activity[guild_id] = asyncio.get_event_loop().time()
# Build a display message for queuing a new song # Build a display message for queuing a new song
@@ -131,7 +72,17 @@ async def display_server_queue(ctx: Context, songs, n):
color=config.get_color("main")) color=config.get_color("main"))
current_song = await queue.get_current_song(ctx.guild.id) current_song = await queue.get_current_song(ctx.guild.id)
display = f"🔊 Currently playing: ``{current_song}``\n\n" loop_mode = await queue.get_loop_mode(ctx.guild.id)
volume = await queue.get_volume(ctx.guild.id)
effect = await queue.get_effect(ctx.guild.id)
# Add loop emoji based on mode
loop_emojis = {'off': '', 'song': '🔂', 'queue': '🔁'}
loop_emoji = loop_emojis.get(loop_mode, '')
effect_emoji = queue.get_effect_emoji(effect)
display = f"🔊 Currently playing: ``{current_song}`` {loop_emoji}\n"
display += f"Volume: {volume}% | Loop: {loop_mode} | Effect: {effect_emoji} {effect}\n\n"
for i, song in enumerate(songs): for i, song in enumerate(songs):