├── README.md ├── requirements.txt ├── stembridge.py └── stemplayerplayer.py /README.md: -------------------------------------------------------------------------------- 1 | # Stem Player Player 2 | Stem Player Player 3 | 4 | # Usage 5 | Download the latest release [here](https://github.com/nn9dev/stemplayerplayer/releases/latest)\ 6 | Optional: install [ffmpeg](https://ffmpeg.org), instructions [here](https://www.google.com/search?q=how+to+install+ffmpeg+and+add+it+to+path) 7 | 8 | NOTE: DOES NOT ENABLE DOWNLOADS FROM THE STEM PLAYER SITE. FOR THAT, PLEASE USE [THIS PROJECT](https://github.com/krystalgamer/stem-player-emulator) 9 | 10 | fairly complete! 11 | 12 | Default Keybinds\ 13 | 1 - instrumental\ 14 | 2 - vocals\ 15 | 3 - bass\ 16 | 4 - drums\ 17 | (i believe that's the numbers they download from the site) 18 | 19 | # Features 20 | - ability to merge stems (requires [ffmpeg](https://ffmpeg.org)) 21 | - full control over channel volume 22 | - ui 23 | - custom keybinds 24 | 25 | # Known issues 26 | - Keybinds don't work on Mac. See [issue #2](https://github.com/nn9dev/stemplayerplayer/issues/2) 27 | - 28 | # TODO : 29 | - finish Stem Bridge (import songs from site) 30 | - play albums/folders in folders -> up next 31 | - maybe ability to create stems? website does that though 32 | - take requests! 33 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | keyboard 2 | pygame 3 | Pillow 4 | pydub 5 | flask 6 | -------------------------------------------------------------------------------- /stembridge.py: -------------------------------------------------------------------------------- 1 | import flask 2 | import os 3 | import threading #god save the queen or something why do people hate threadss 4 | import json 5 | import types 6 | from flask import Flask, flash, request, redirect, url_for 7 | from werkzeug.utils import secure_filename 8 | #os.environ["FLASK_ENV"] = "production" 9 | 10 | TESTING = False 11 | SOLO = False 12 | 13 | homedir = os.path.expanduser("~") 14 | with open(homedir + "/stemplayerplayer_config.json") as json_file: 15 | tempjson = json.load(json_file) 16 | UPLOAD_FOLDER = tempjson['SPP_HOME'] 17 | ALLOWED_EXTENSIONS = {'mp3','wav','flac'} 18 | app = Flask(__name__) 19 | app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER 20 | #startstop_bridge = 1 21 | globalthread = types.SimpleNamespace() 22 | 23 | # this was absolutely copied 24 | class ThreadingExample(object): 25 | def __init__(self): 26 | thread = threading.Thread(target=self.run, args=()) 27 | thread.daemon = True # Daemonize thread 28 | thread.start() # Start the execution 29 | thread = globalthread 30 | 31 | def run(self): 32 | """ Method that runs forever """ 33 | while True: 34 | # Do something 35 | app.run(port=1337) 36 | 37 | def start_bridge(startstop_bridge): 38 | print(startstop_bridge) 39 | global globalthread 40 | if startstop_bridge==1: 41 | #startstop_bridge=0 42 | #app.config 43 | example = ThreadingExample() 44 | print("bridge started!\n") 45 | elif startstop_bridge==0: 46 | #startstop_bridge=1 47 | globalthread._running = False 48 | print("bridge stopped!") 49 | #return startstop_bridge 50 | 51 | def allowed_file(filename): 52 | return '.' in filename and \ 53 | filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS 54 | 55 | @app.route('/stembridge_upload', methods=['GET', 'POST']) 56 | def upload_file(): 57 | if request.method == 'POST': 58 | print(request.form['folder']) 59 | # check if the post request has the file part 60 | if 'file' not in request.files: 61 | flash('No file specified') 62 | return redirect(request.url) 63 | file = request.files['file'] 64 | folder = request.form['folder'] 65 | # If the user does not select a file, the browser submits an 66 | # empty file without a filename. 67 | if file.filename == '' or folder is None: 68 | flash('No selected file') 69 | return redirect(request.url) 70 | if file and allowed_file(file.filename): 71 | filename = secure_filename(file.filename) 72 | if not os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], folder)): 73 | os.makedirs(os.path.join(app.config['UPLOAD_FOLDER'], folder)) 74 | if folder: 75 | file.save(os.path.join(app.config['UPLOAD_FOLDER'], folder, filename)) 76 | else: 77 | file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) 78 | return "success" 79 | else: return "fail" 80 | if TESTING is True: 81 | return ''' 82 | 83 | Upload new File 84 |

Upload new File

85 |
86 | 87 | 88 | 89 |
90 | ''' 91 | else: return "enabled" 92 | 93 | #test variable 94 | if SOLO is True: 95 | app.run(port=1337) 96 | -------------------------------------------------------------------------------- /stemplayerplayer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from os.path import expanduser 4 | os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide" #shhh pygame 5 | import subprocess 6 | import keyboard 7 | import pygame as pg 8 | import json 9 | import glob 10 | import tkinter as tk 11 | import tkinter.messagebox 12 | from tkinter import * 13 | from PIL import ImageTk, Image 14 | from tkinter import filedialog 15 | from pydub import AudioSegment 16 | 17 | homedir = os.path.expanduser("~") 18 | 19 | state=1 20 | stem_list = [] 21 | note_objects=[] 22 | mutevols=[0,0,0,0] 23 | merging = False 24 | 25 | root = tk.Tk() 26 | startstop_bridge = tk.IntVar() 27 | startstop_bridge.set(0) 28 | root.title('Stem Player Player') 29 | root.protocol("WM_DELETE_WINDOW", lambda: close_window()) 30 | 31 | def create_config(): 32 | import platform 33 | if not os.path.exists(homedir + "/stemplayerplayer_config.json"): ##create config if doesn't exist 34 | default_config = { 35 | "KEY_INSTRUMENTALS": "1", 36 | "KEY_VOCALS": "2", 37 | "KEY_BASS": "3", 38 | "KEY_DRUMS": "4", 39 | "KEYBINDS_ENABLED": 0, 40 | } 41 | if 'SPP_HOME' not in default_config: 42 | if platform.system() == 'Windows': 43 | default_config["SPP_HOME"] = homedir + "\\stemplayer" 44 | else: 45 | default_config["SPP_HOME"] = homedir + "/stemplayer" 46 | tempjson = json.dumps(default_config, indent=2) 47 | with open(homedir + "/stemplayerplayer_config.json", "w") as jsonfile: 48 | jsonfile.write(tempjson) 49 | tempjson.close() 50 | 51 | create_config() 52 | #load config 53 | with open(homedir + "/stemplayerplayer_config.json", encoding="utf-8") as config_file: 54 | SPP_CONFIG = json.load(config_file) 55 | KEY_INSTRUMENTALS = keyboard.key_to_scan_codes(SPP_CONFIG["KEY_INSTRUMENTALS"])[0] 56 | KEY_VOCALS = keyboard.key_to_scan_codes(SPP_CONFIG["KEY_VOCALS"])[0] 57 | KEY_BASS = keyboard.key_to_scan_codes(SPP_CONFIG["KEY_BASS"])[0] 58 | KEY_DRUMS = keyboard.key_to_scan_codes(SPP_CONFIG["KEY_DRUMS"])[0] 59 | KEYBINDS_ENABLED = SPP_CONFIG["KEYBINDS_ENABLED"] 60 | SPP_HOME = SPP_CONFIG["SPP_HOME"] 61 | if not os.path.exists(SPP_HOME): 62 | os.makedirs(SPP_HOME) 63 | 64 | from stembridge import * 65 | 66 | def open_config(): 67 | import platform 68 | global KEY_INSTRUMENTALS, KEY_VOCALS, KEY_BASS, KEY_DRUMS 69 | if platform.system() == 'Windows': 70 | subprocess.Popen("notepad.exe " + homedir + "/stemplayerplayer_config.json", creationflags=0x08000000) #flag for CREATE_NO_WINDOW 71 | elif platform.system() == 'Linux': 72 | subprocess.Popen("xdg-open " + homedir + "/stemplayerplayer_config.json") 73 | elif platform.system() == 'Darwin': 74 | subprocess.Popen("open " + homedir + "/stemplayerplayer_config.json") 75 | 76 | ''' 77 | with open(homedir + "/stemplayerplayer_config.json", encoding="utf-8") as config_file: #reimport keybinds 78 | SPP_CONFIG = json.load(config_file) 79 | KEY_INSTRUMENTALS = keyboard.key_to_scan_codes(SPP_CONFIG["KEY_INSTRUMENTALS"])[0] 80 | KEY_VOCALS = keyboard.key_to_scan_codes(SPP_CONFIG["KEY_VOCALS"])[0] 81 | KEY_BASS = keyboard.key_to_scan_codes(SPP_CONFIG["KEY_BASS"])[0] 82 | KEY_DRUMS = keyboard.key_to_scan_codes(SPP_CONFIG["KEY_DRUMS"])[0] 83 | SPP_HOME = SPP_CONFIG["SPP_HOME"] 84 | ''' 85 | 86 | 87 | def slider(value): 88 | i = instrumentals_Scale.get() 89 | v = vocals_Scale.get() 90 | b = bass_Scale.get() 91 | d = drums_Scale.get() 92 | 93 | if note_objects: #suppress stupid error 94 | note_objects[0].set_volume(i) 95 | note_objects[1].set_volume(v) 96 | note_objects[2].set_volume(b) 97 | note_objects[3].set_volume(d) 98 | 99 | def open_new(): 100 | pg.mixer.stop() 101 | global note_objects 102 | global stem_list 103 | stem_list=[] 104 | folder_path = filedialog.askdirectory(title="Select Tracks Folder") 105 | print(folder_path) 106 | if not folder_path: 107 | return 108 | elif glob.glob(folder_path + "/*.flac"): 109 | print("Using FLAC...") 110 | stem_list = glob.glob(folder_path + '/*[0-9].flac') 111 | elif glob.glob(folder_path + "/*.wav"): 112 | print("Using WAV...") 113 | stem_list = glob.glob(folder_path + '/*[0-9].wav') 114 | elif glob.glob(folder_path + "/*.mp3"): 115 | print("Using MP3...") 116 | stem_list = glob.glob(folder_path + '/*[0-9].mp3') 117 | 118 | a1Note = pg.mixer.Sound(stem_list[0]) 119 | a2Note = pg.mixer.Sound(stem_list[1]) 120 | a3Note = pg.mixer.Sound(stem_list[2]) 121 | a4Note = pg.mixer.Sound(stem_list[3]) 122 | if merging == False: 123 | a1Note.play() 124 | a2Note.play() 125 | a3Note.play() 126 | a4Note.play() 127 | note_objects = [a1Note, a2Note, a3Note, a4Note] 128 | 129 | def merge_stems(): 130 | global stem_list 131 | stem_list = [] 132 | global merging 133 | from shutil import which 134 | print(which('ffmpeg')) 135 | if which('ffmpeg') is not None: 136 | merging = True 137 | open_new() 138 | merging = False 139 | if stem_list: 140 | text = stem_list[0] 141 | soundformat = re.split('[1-4]\.', text)[1] 142 | startname = re.split('[1-4]\.', text)[0] 143 | print(soundformat) 144 | print(startname) 145 | print(stem_list[0]) 146 | print(re.split('[1-4]\.', text)[0] + "." + soundformat) 147 | if os.path.exists(os.path.normpath(startname + "." + soundformat)): 148 | os.remove(os.path.normpath(startname + "." + soundformat)) 149 | 150 | stem1 = AudioSegment.from_file(os.path.normpath(stem_list[0])) 151 | stem2 = AudioSegment.from_file(os.path.normpath(stem_list[1])) 152 | stem3 = AudioSegment.from_file(os.path.normpath(stem_list[2])) 153 | stem4 = AudioSegment.from_file(os.path.normpath(stem_list[3])) 154 | overlay = stem1.overlay(stem2.overlay(stem3.overlay(stem4))) 155 | file_handle = overlay.export(startname + "." + soundformat, format=soundformat) 156 | file_handle.flush() 157 | file_handle.close() 158 | 159 | else: 160 | tk.messagebox.showerror(title="ffmpeg not found", message="ffmpeg not found on your system. Please install ffmpeg and make sure it is added to your PATH.") 161 | 162 | def toggle_keybinds(): 163 | print(onoff.get()) 164 | 165 | def check_keybinds(): 166 | ##hell keybinds 167 | if keyboard.is_pressed(KEY_INSTRUMENTALS) and onoff.get() == 1: 168 | toggle_channel(1) 169 | 170 | if keyboard.is_pressed(KEY_VOCALS)and onoff.get() == 1: 171 | toggle_channel(2) 172 | 173 | if keyboard.is_pressed(KEY_BASS) and onoff.get() == 1: 174 | toggle_channel(3) 175 | 176 | if keyboard.is_pressed(KEY_DRUMS) and onoff.get() == 1: 177 | toggle_channel(4) 178 | root.after(1, check_keybinds) 179 | 180 | def toggle_channel(toToggle): 181 | #print("hello from togglechannel") 182 | if note_objects: #suppress stupid error pt 2 183 | if toToggle == 1: 184 | print ("1") 185 | if note_objects[0].get_volume() != 0.0: 186 | mutevols[0] = note_objects[0].get_volume() 187 | note_objects[0].set_volume(0.0) 188 | elif note_objects[0].get_volume() == 0.0: 189 | note_objects[0].set_volume(mutevols[0]) 190 | print(note_objects[0].get_volume()) 191 | while keyboard.is_pressed(KEY_INSTRUMENTALS): 192 | keyboard.block_key(KEY_INSTRUMENTALS) 193 | keyboard.unhook_all() 194 | elif toToggle == 2: 195 | print ("2") 196 | if note_objects[1].get_volume() != 0.0: 197 | mutevols[1] = note_objects[1].get_volume() 198 | note_objects[1].set_volume(0.0) 199 | elif note_objects[1].get_volume() == 0.0: 200 | note_objects[1].set_volume(mutevols[1]) 201 | print(note_objects[1].get_volume()) 202 | while keyboard.is_pressed(KEY_VOCALS): 203 | keyboard.block_key(KEY_VOCALS) 204 | keyboard.unhook_all() 205 | elif toToggle == 3: 206 | print ("3") 207 | if note_objects[2].get_volume() != 0.0: 208 | mutevols[2] = note_objects[2].get_volume() 209 | note_objects[2].set_volume(0.0) 210 | elif note_objects[2].get_volume() == 0.0: 211 | note_objects[2].set_volume(mutevols[2]) 212 | print(note_objects[2].get_volume()) 213 | while keyboard.is_pressed(KEY_BASS): 214 | keyboard.block_key(KEY_BASS) 215 | keyboard.unhook_all() 216 | elif toToggle == 4: 217 | print ("4") 218 | if note_objects[3].get_volume() != 0.0: 219 | mutevols[3] = note_objects[3].get_volume() 220 | note_objects[3].set_volume(0.0) 221 | elif note_objects[3].get_volume() == 0.0: 222 | note_objects[3].set_volume(mutevols[3]) 223 | print(note_objects[3].get_volume()) 224 | while keyboard.is_pressed(KEY_DRUMS): 225 | keyboard.block_key(KEY_DRUMS) 226 | keyboard.unhook_all() 227 | else: 228 | return 229 | 230 | 231 | def pause_play(): 232 | global state 233 | if state==1: 234 | state=0 235 | print("Pausing!") 236 | pg.mixer.pause() 237 | elif state==0: 238 | state=1 239 | print("Unpausing!") 240 | pg.mixer.unpause() 241 | 242 | def close_window(): 243 | global onoff 244 | with open(homedir + "/stemplayerplayer_config.json") as json_file: 245 | jsontemp = json.load(json_file) 246 | jsontemp['KEYBINDS_ENABLED'] = onoff.get() 247 | 248 | with open(homedir + "/stemplayerplayer_config.json", 'w') as json_file: 249 | json.dump(jsontemp, json_file) 250 | pg.mixer.quit() 251 | pg.quit() 252 | root.destroy() 253 | exit() 254 | 255 | 256 | pg.mixer.init() 257 | 258 | frame = Frame(root, bd=1, relief=None) 259 | frame.pack(pady=5) 260 | 261 | instrumentals_label = Label(frame, text="Instrumentals", font=("Times New Roman", 12, "bold")) 262 | instrumentals_label.grid(row=0, column=0) 263 | 264 | instrumentals_Scale = Scale(frame, resolution=0.01, from_=0.0, to=1.0, length=210, orient=HORIZONTAL, command=slider) 265 | instrumentals_Scale.grid(row=0, column=1) 266 | instrumentals_Scale.set(1.0) 267 | 268 | vocals_label = Label(frame, text="Vocals", font=("Times New Roman", 12, "bold")) 269 | vocals_label.grid(row=1, column=0) 270 | 271 | vocals_Scale = Scale(frame, resolution=0.01, from_=0.0, to=1.0, length=210, orient=HORIZONTAL, command=slider) 272 | vocals_Scale.grid(row=1, column=1) 273 | vocals_Scale.set(1.0) 274 | 275 | bass_label = Label(frame, text="Bass", font=("Times New Roman", 12, "bold")) 276 | bass_label.grid(row=2, column=0) 277 | 278 | bass_Scale = Scale(frame, resolution=0.01, from_=0.0, to=1.0, length=210, orient=HORIZONTAL, command=slider) 279 | bass_Scale.grid(row=2, column=1) 280 | bass_Scale.set(1.0) 281 | 282 | drums_label = Label(frame, text="Drums", font=("Times New Roman", 12, "bold")) 283 | drums_label.grid(row=3, column=0) 284 | 285 | drums_Scale = Scale(frame, resolution=0.01, from_=0.0, to=1.0, length=210, orient=HORIZONTAL, command=slider) 286 | drums_Scale.grid(row=3, column=1) 287 | drums_Scale.set(1.0) 288 | 289 | ###commands 290 | frame2 = Frame(root, bd=1, relief=None) 291 | frame2.pack(pady=5) 292 | 293 | pauseplay = Button(frame2, text="Pause/Play", font=("Times New Roman", 12, "bold"), command=lambda: pause_play()) 294 | pauseplay.grid(row=3, column=1, sticky=E) 295 | 296 | newtrack = Button(frame2, text="New Track", font=("Times New Roman", 12, "bold"), command=lambda: open_new()) 297 | newtrack.grid(row=3, column=2) 298 | 299 | keybindsbutton = Button(frame2, text="Edit config", font=("Times New Roman", 12, "bold"), command=lambda: open_config()) 300 | keybindsbutton.grid(row=3, column=3, sticky=W) 301 | 302 | mergebutton = Button(frame2, text="Merge Stems", font=("Times New Roman", 12, "bold"), command=lambda: merge_stems()) 303 | mergebutton.grid(row=4, column=2, pady=2) 304 | 305 | #startbridge = Button(frame2, text="Bridge on/off", font=("Times New Roman", 12, "bold"), command=lambda: start_bridge()) 306 | #startbridge.grid(row=4, column=2, pady=2) 307 | 308 | 309 | startbridge = tk.Checkbutton(root, 310 | text='Bridge Enabled', 311 | command=lambda: start_bridge(startstop_bridge.get()), 312 | font=("Times New Roman", 12, "bold"), 313 | variable=startstop_bridge, 314 | onvalue=1, 315 | offvalue=0) 316 | startbridge.pack() 317 | 318 | 319 | 320 | onoff = tk.IntVar() #Keybinds toggle box 321 | onoff.set(KEYBINDS_ENABLED) 322 | tgkb = tk.Checkbutton(root, 323 | text='Keybinds Enabled', 324 | command=toggle_keybinds, 325 | font=("Times New Roman", 12, "bold"), 326 | variable=onoff, 327 | onvalue=1, 328 | offvalue=0) 329 | tgkb.pack() 330 | 331 | root.after(1, check_keybinds) 332 | root.mainloop() 333 | --------------------------------------------------------------------------------