├── 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 |
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 |
--------------------------------------------------------------------------------