", on_progressbar_drag_stop)
# Stop the previous update thread if it exists
stop_update_thread()
# Start the progress update timer
self.update_timer = threading.Thread(target=update_progress, daemon=True)
self.update_timer.start()
if __name__ == "__main__":
app = MISSTapp()
MISSThelpers.update_rpc(app, Ltext="Idle", Dtext="Nothing is playing")
app.mainloop()
================================================
FILE: MISST/MISSThelpers.py
================================================
import ctypes
import os
import platform
import re
import shutil
import sys
import threading
import time
import tkinter
import uuid
from colorsys import hls_to_rgb, rgb_to_hls
import customtkinter
import demucs
import music_tag
import psutil
import requests
import torch
from MISSTplayer import MISSTplayer
from MISSTsettings import MISSTsettings
from vcolorpicker import getColor, hex2rgb, rgb2hex, useLightTheme
class MISSTconsole():
"""
A class to handle the console output of MISST
"""
def __init__(self, terminal:customtkinter.CTkTextbox, ogText:str) -> None:
"""
Parameters
terminal : tkinter.Text
The tkinter.Text widget to be used as the console
ogText : str
The original text to be displayed in the console
"""
self.consoleText = ogText
self.terminal = terminal
self.curThread = None
self.terminal.delete("0.0", "end") # delete all text
self.terminal.insert("0.0", self.consoleText)
self.terminal.configure(state="disabled")
def updateThread(self, text:str) -> None:
"""
A thread to update the console text
Args:
text (str): The text to be added to the console
"""
t = 0
while True:
time.sleep(0.5)
if t > 3:
t -= t
periods = ["", ".", "..", "..."]
self.terminal.configure(state="normal")
self.terminal.delete("0.0", "end")
self.terminal.insert("0.0", f"{self.consoleText}{text}{periods[t]}")
self.terminal.configure(state="disabled")
t += 1
def update(self, text:str) -> None:
"""
Update the console text
Args:
text (str): The text to be added to the console
"""
self.curThread = threading.Thread(target=self.updateThread, args=(text,), daemon=True)
self.curThread.start()
def endUpdate(self) -> None:
"""
End the update thread
Args:
text (str): The text to be added to the console
"""
MISSThelpers.terminate_thread(self, self.curThread)
self.terminal.configure(state="normal")
self.terminal.delete("0.0", "end")
self.terminal.insert("0.0", self.consoleText)
self.terminal.configure(state="disabled")
def addLine(self, text:str) -> None:
"""
Add a line to the console
Args:
text (str): The text to be added to the console
"""
self.consoleText += f"{text}"
self.terminal.configure(state="normal")
self.terminal.delete("0.0", "end")
self.terminal.insert("0.0", self.consoleText)
self.terminal.configure(state="disabled")
def editLine(self, text:str, line_number:int) -> None:
"""
Edit a line in the console
Args:
text (str): The text to be added to the console
line_number (int): The line number to be edited
"""
self.consoleText = text
self.terminal.configure(state="normal")
self.terminal.delete(f"{line_number + 1}.0", f"end")
self.terminal.insert(f"{line_number + 1}.0", text)
self.terminal.configure(state="disabled")
class MISSThelpers():
"""
A class filled with helper methods for MISST
"""
def update_rpc(
self,
Ltext:str = None,
Dtext:str = None,
image:str = "icon-0",
large_text:str = "MISST",
end_time:int = None,
small_image:str = None,
) -> None:
"""
Update the Discord Rich Presence
Args:
Ltext (str, optional): The large text to be displayed. Defaults to None.
Dtext (str, optional): The details text to be displayed. Defaults to None.
image (str, optional): The image to be displayed. Defaults to "icon-0".
large_text (str, optional): The large text to be displayed. Defaults to "MISST".
end_time (int, optional): The end time of the activity. Defaults to None.
small_image (str, optional): The small image to be displayed. Defaults to None.
"""
start_time = time.time()
if self.RPC_CONNECTED:
try:
self.RPC.update(
large_image=image,
small_image=small_image,
start=start_time,
end=end_time,
large_text=large_text,
state=Ltext,
details=Dtext,
)
except:
return
return
def apple_music(url:str, outdir:str) -> None:
"""
Download an Apple Music song
Args:
url (str): The Apple Music song URL
outdir (str): The output directory
"""
host = 'https://api.fabdl.com'
info = requests.get(host + '/apple-music/get?url=', params={'url': url}).json()['result']
convert_task = requests.get(host + f'/apple-music/mp3-convert-task/{info["gid"]}/{info["id"]}')
tid = convert_task.json()['result']['tid']
convert_task = requests.get(host + f'/apple-music/mp3-convert-progress/{tid}')
r = requests.get(host + convert_task.json()['result']['download_url'])
with open(f"{outdir}/{info['artists'] + ' - ' + info['name']}.mp3", 'wb') as f:
f.write(r.content)
try:
audiofile = music_tag.load_file(f"{outdir}/{info['artists'] + ' - ' + info['name']}.mp3")
audiofile['artwork'] = requests.get(info['image']).content
audiofile.save()
except:
pass
def change_theme(theme:str) -> None:
"""
Change the theme of the application
Args:
theme (str): The theme to be changed to
"""
customtkinter.set_appearance_mode(theme)
def checkbox_event(checkbox:customtkinter.CTkCheckBox, export_slider:customtkinter.CTkSlider, sound:str, player:MISSTplayer, slider:customtkinter.CTkSlider) -> None:
"""
Change the volume of a sound
Args:
checkbox (tkinter.Checkbutton): The checkbox
sound (str): The sound to be changed
player (MISSTplayer): The sound player
slider (tkinter.Scale): The volume slider
"""
if checkbox.get() == "on":
player.set_volume(sound, slider.get())
else:
slider.set(0)
player.set_volume(sound, slider.get())
MISSThelpers.slider_event(slider.get(), export_slider, sound, player, checkbox)
def slider_event(value:int, export_slider:customtkinter.CTkSlider, sound:str, player:MISSTplayer, checkbox:customtkinter.CTkCheckBox) -> None:
"""
Change the volume of a sound
Args:
value (int): The volume value
sound (str): The sound to be changed
player (MISSTplayer): The sound player
checkbox (tkinter.Checkbutton): The checkbox
"""
settings = MISSTsettings()
export_slider[0].set(value)
if value >= 0.01:
checkbox.set("on")
export_slider[1].configure(border_color=settings.getSetting("chosenLightColor") if customtkinter.get_appearance_mode() == "Light" else settings.getSetting("chosenDarkColor"))
player.set_volume(sound, value)
else:
checkbox.set("off")
export_slider[1].configure(border_color="#3E454A")
player.set_volume(sound, value)
def MISSTlistdir(self, directory:str) -> list:
"""
List all MISST folders in a directory
Args:
directory (str): The directory to be searched
"""
try:
os_list = os.listdir(directory)
misst_list = []
for _ in os_list:
required_files = ["bass.flac", "drums.flac", "other.flac", "vocals.flac", ".misst"]
found = 0
for file in required_files:
if os.path.isfile(f"{directory}/{_}/{file}"):
found += 1
if len(required_files) == found:
misst_list.append(_)
return misst_list
except:
return []
def getsize(self, dir:str) -> int:
"""
Get the size of a directory
Args:
dir (str): The directory to be searched
"""
total = 0
for entry in os.scandir(dir):
if entry.is_file():
total += entry.stat().st_size
elif entry.is_dir():
total += self.getsize(self, entry.path)
return total
def adjust_color_lightness(r:int, g:int, b:int, factor:int) -> str:
"""
Adjust the lightness of a color
Args:
r (int): The red value
g (int): The green value
b (int): The blue value
factor (int): The factor to be adjusted by
"""
h, l, s = rgb_to_hls(r / 255.0, g / 255.0, b / 255.0)
l = max(min(l * factor, 1.0), 0.0)
r, g, b = hls_to_rgb(h, l, s)
return f"#{rgb2hex(int(r * 255), int(g * 255), int(b * 255))}"
def darken_color(r:int, g:int, b:int, factor:int = 0.1) -> str:
"""
Darken a color
Args:
r (int): The red value
g (int): The green value
b (int): The blue value
factor (int, optional): The factor to be darkened by. Defaults to 0.1.
"""
return MISSThelpers.adjust_color_lightness(r, g, b, 1 - factor)
def updateTheme(self, color:str) -> str:
"""
Update the theme of the application
Args:
color (str): The color to be changed
"""
if color == "light":
cur_color = self.settings.getSetting("chosenLightColor")
old_color = (hex2rgb(cur_color.replace("#", "")))
useLightTheme(True if customtkinter.get_appearance_mode() == "Light" else False)
self.withdraw() # Hide the window
chosen_color = f"#{rgb2hex(getColor(old_color))}"
self.deiconify() # Show the window
rgb_chosen = hex2rgb(chosen_color.replace("#", ""))
self.settings.setSetting("chosenLightColor", chosen_color)
self.settings.setSetting("chosenLightHoverColor", MISSThelpers.darken_color(rgb_chosen[0], rgb_chosen[1], rgb_chosen[2], 0.2))
self.settings.setSetting("chosenLightDarker", MISSThelpers.darken_color(rgb_chosen[0], rgb_chosen[1], rgb_chosen[2], 0.35))
self.settings.applyThemeSettings("./Assets/Themes/MISST.json", "./Assets/Themes/maluableJSON")
self.button_light.configure(fg_color=chosen_color, hover_color=chosen_color)
else:
cur_color = self.settings.getSetting("chosenDarkColor")
old_color = (hex2rgb(cur_color.replace("#", "")))
useLightTheme(True if customtkinter.get_appearance_mode() == "Light" else False)
self.withdraw() # Hide the window
chosen_color = f"#{rgb2hex(getColor(old_color))}"
self.deiconify() # Show the window
rgb_chosen = hex2rgb(chosen_color.replace("#", ""))
self.settings.setSetting("chosenDarkColor", chosen_color)
self.settings.setSetting("chosenDarkHoverColor", MISSThelpers.darken_color(rgb_chosen[0], rgb_chosen[1], rgb_chosen[2], 0.2))
self.settings.setSetting("chosenDarkDarker", MISSThelpers.darken_color(rgb_chosen[0], rgb_chosen[1], rgb_chosen[2], 0.35))
self.settings.applyThemeSettings("./Assets/Themes/MISST.json", "./Assets/Themes/maluableJSON")
self.button_dark.configure(fg_color=chosen_color, hover_color=chosen_color)
return chosen_color
def resetSettings(self) -> None:
"""
Reset the settings of the application
"""
cuda = torch.cuda.is_available()
self.settings.resetDefaultTheme("./Assets/Themes/MISST.json", "./Assets/Themes/maluableJSON")
self.settings.setSetting("rpc", "true")
self.settings.setSetting("autoplay", "true")
self.settings.setSetting("accelerate_on_gpu", "true" if cuda else "false")
self.rpc_box.select()
self.autoplay_box.select()
self.preprocess_method_box.select() if cuda else self.preprocess_method_box.deselect()
self.button_light.configure(fg_color=self.settings.getSetting("defaultLightColor"), hover_color=self.settings.getSetting("defaultLightColor"))
self.button_dark.configure(fg_color=self.settings.getSetting("defaultDarkColor"), hover_color=self.settings.getSetting("defaultDarkColor"))
self.model_select.set("htdemucs")
self.settings.setSetting("chosen_model", "htdemucs")
self.change_model(self.model_select.get())
def autoplay_event(self) -> None:
"""
Event for when the autoplay box is checked
"""
if self.autoplay_box.get() == 1:
self.settings.setSetting("autoplay", "true")
else:
self.settings.setSetting("autoplay", "false")
def rpc_event(self) -> None:
"""
Event for when the rpc box is checked
"""
if self.rpc_box.get() == 1:
self.settings.setSetting("rpc", "true")
else:
self.settings.setSetting("rpc", "false")
def accelerate_event(self) -> None:
"""
Event for when the accelerate box is checked
"""
if self.preprocess_method_box.get() == 1 and torch.cuda.is_available() == True:
self.settings.setSetting("accelerate_on_gpu", "true")
elif self.preprocess_method_box.get() == 1 and torch.cuda.is_available() == False:
self.preprocess_method_box.deselect()
self.preprocess_method_box.place(relx=0.39, rely=0.85, anchor=tkinter.CENTER)
self.preprocess_method_box.configure(text="CUDA Not Available")
self.settings.setSetting("accelerate_on_gpu", "false")
time.sleep(1.5)
self.preprocess_method_box.place(relx=0.38, rely=0.85, anchor=tkinter.CENTER)
self.preprocess_method_box.configure(text="Accelerate on GPU?")
else:
self.settings.setSetting("accelerate_on_gpu", "false")
def clearDownloads(self) -> None:
"""
Clear the downloads folder
"""
self.confirmation_frame = customtkinter.CTkFrame(
master=self.settings_window, width=350, height=350, corner_radius=0
)
self.confirmation_frame.place(relx=0.25, rely=0.5, anchor=tkinter.CENTER)
self.confirmation_header = customtkinter.CTkLabel(
master=self.confirmation_frame, text="Are you sure?", font=(self.FONT, -16)
)
self.confirmation_header.place(relx=0.5, rely=0.45, anchor=tkinter.CENTER)
self.confirmation_yes = customtkinter.CTkButton(
master=self.confirmation_frame,
text="Yes",
font=(self.FONT, -12),
command=lambda: clear(),
width=80,
)
self.confirmation_yes.place(relx=0.35, rely=0.55, anchor=tkinter.CENTER)
self.confirmation_no = customtkinter.CTkButton(
master=self.confirmation_frame,
text="No",
font=(self.FONT, -12),
command=lambda: self.confirmation_frame.destroy(),
width=80,
)
self.confirmation_no.place(relx=0.65, rely=0.55, anchor=tkinter.CENTER)
def clear():
try:
shutil.rmtree(self.importsDest)
os.mkdir(self.importsDest)
self.confirmation_frame.destroy()
bytes = MISSThelpers.getsize(MISSThelpers, self.importsDest)
gb = bytes / 1000000000
gb = round(gb, 2)
text = str(gb) + " GB"
self.downloads_info.configure(text=text)
except:
pass
def change_location(self) -> None:
"""
Change the location of the downloads folder
"""
try:
importsdest_nocheck = tkinter.filedialog.askdirectory(
initialdir=os.path.abspath(self.importsDest)
)
importsdest_nocheck = importsdest_nocheck.replace("\\", "/")
if importsdest_nocheck != "" and os.path.isdir(
os.path.abspath(importsdest_nocheck)
):
dummypath = os.path.join(importsdest_nocheck, str(uuid.uuid4()))
try:
with open(dummypath, "w"):
pass
os.remove(dummypath)
importsdest = importsdest_nocheck
except IOError:
importsdest = os.path.abspath(importsdest)
self.settings.setSetting("importsDest", importsdest)
self.importsDest = importsdest
dir = os.path.abspath(self.importsDest)
dirlen = len(dir)
n = 20
location_text = dir if dirlen <= n else "..." + dir[-(n - dirlen) :]
self.storage_location_info.configure(text=location_text)
return None
else:
self.importsDest = os.path.abspath(self.importsDest)
return None
except Exception as e:
print(e)
self.importsDest = os.path.abspath(self.importsDest)
return None
def loading_label(label:customtkinter.CTkLabel, text:str, og_text:str = "") -> None:
"""
Loading animation for the settings window
Args:
label (tkinter.Label): The label to animate
text (str): The text to append to the label
og_text (str, optional): The original text of the label. Defaults to "".
"""
t = 0
try:
while True:
time.sleep(0.5)
if label.cget("text") == "":
break
if t > 3:
t -= t
periods = ["", ".", "..", "..."]
label.configure(text=f"{og_text}{text}{periods[t]}")
t += 1
except:
pass
return
def GenerateSystemInfo(self) -> str:
"""
Generate system info for the settings window
"""
info = ""
info += "Python version:\t%s\n" % sys.version
info += "System:\t%s\n" % platform.platform()
info += "CPU:\t%s\n" % platform.processor()
info += "Memory:\t%.3fMB\n" % (psutil.virtual_memory().total / 1048576)
info += "PyTorch version:\t%s\n" % torch.__version__
info += "CUDA available:\t%s\n" % torch.cuda.is_available()
if torch.cuda.is_available():
for i in range(torch.cuda.device_count()):
info += "CUDA %d:\t%s\n" % (i, re.findall("\\((.*)\\)", str(torch.cuda.get_device_properties(i)))[0])
info += "Demucs version:\t%s\n" % demucs.__version__
info += "FFMpeg available:\t%s\n" % self.FFMpegAvailable
info += "MISST version:\t%s\n" % self.version
return info
def freeimage_upload(self, img:str) -> str:
"""
Upload an image to freeimage.host
Args:
img (str): The path to the image
"""
key = "6d207e02198a847aa98d0a2a901485a5"
response = requests.post(
url="https://freeimage.host/api/1/upload",
data={"key": key},
files={"source": img},
)
if not response.ok:
raise Exception("Error uploading image", response.json())
return response.json()["image"]["url"]
def terminate_thread(self, thread:threading.Thread) -> None:
"""
Terminate a thread
Args:
thread (threading.Thread): The thread to terminate
"""
if not thread.is_alive():
return
exc = ctypes.py_object(SystemExit)
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
ctypes.c_long(thread.ident), exc)
if res == 0:
raise ValueError("nonexistent thread id")
elif res > 1:
# """if it returns a number greater than one, you're in trouble,
# and you should call it again with exc=NULL to revert the effect"""
ctypes.pythonapi.PyThreadState_SetAsyncExc(thread.ident, None)
raise SystemError("PyThreadState_SetAsyncExc failed")
================================================
FILE: MISST/MISSTlogger.py
================================================
import datetime
import logging
import sys
class MISSTlogger:
"""
MISSTlogger class
"""
def __init__(self) -> None:
"""
Initialize the logger
"""
loggerName = "MISST"
logFormatter = logging.Formatter(fmt=" %(name)s :: %(levelname)-8s :: %(message)s")
self.logger = logging.getLogger(loggerName)
logging.basicConfig(
filename="MISST.log",
filemode="a",
format=" %(name)s :: %(levelname)-8s :: %(message)s",
level=logging.INFO,
)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(logFormatter)
self.logger.addHandler(consoleHandler)
self.logger.info(f'Logger initialized ({str(datetime.datetime.now()).split(".")[0]})')
sys.excepthook = self.handler
def handler(self, type, value, tb) -> None:
self.logger.exception("Uncaught exception: {0}".format(str(value)))
================================================
FILE: MISST/MISSTplayer.py
================================================
import base64
import io
import threading
from typing import List
import music_tag # for exporting song metadata
import numpy as np
import pyaudio
import soundfile as sf
from MISSTsettings import MISSTsettings
from scipy.signal import butter, fftconvolve, filtfilt
class MISSTplayer:
"""
MISSTplayer class
"""
def __init__(self, files:list, volumes:list) -> None:
"""
Initialize the player
Args:
files (list): List of file paths
volumes (list): List of volume values
"""
self.files = files
self.p = pyaudio.PyAudio()
self.streams = []
self.paused = False
self.positions = [0] * len(files)
self.volumes = volumes
self.chunk_size = 1024
self.effects = False
self.settings = MISSTsettings()
self.bands = lambda: [self.settings.getSetting(f"eq_{n}") for n in range(1,10)] # lambda so the player can access the latest information
self.eq = lambda: True if self.settings.getSetting("eq") == "true" else False
self.center_freqs = [62, 125, 250, 500, 1_000, 2_500, 4_000, 8_000, 16_000] # 62 Hz, 125 Hz, 250 Hz, 500 Hz, 1 KHz, 2.5 KHz, 4 KHz, 8 KHz, 16 KHz
for file in self.files:
data, self.frame_rate = sf.read(file, dtype='int16')
self.duration = len(data) / self.frame_rate
self.channels = data.shape[1]
stream = self.p.open(format=self.p.get_format_from_width(2),
channels=self.channels,
rate=self.frame_rate,
output=True)
self.streams.append(stream)
reverb_length = int(self.frame_rate * 0.3)
self.impulse_response = self.generate_impulse_response(reverb_length, 0.5)
self.reverb_tails = [np.zeros(self.chunk_size, dtype=np.float32) for _ in self.streams]
def play(self) -> None:
"""
Play the audio
"""
self.paused = False
while not self.paused:
for i, stream in enumerate(self.streams):
data = self.get_data(i)
if data:
stream.write(data)
else:
break
def get_data(self, stream_index:int) -> bytes:
"""
Get the audio data
Args:
stream_index (int): Index of the stream
"""
data, _ = sf.read(self.files[stream_index], dtype='int16', start=self.positions[stream_index], frames=self.chunk_size)
if len(data) > 0:
self.positions[stream_index] += len(data)
data = self.adjust_volume(data, self.volumes[stream_index])
if self.effects == True:
try:
data = self.apply_effects(data, float(self.settings.getSetting("speed")),
float(self.settings.getSetting("reverb")),
float(self.settings.getSetting("pitch")),
stream_index)
except:
# json error
pass
try:
if self.eq() == True:
data = self.apply_eq(data) # apply eq last so it can be applied to nightcore data as well
except:
# json error
pass
return data
def get_position(self, stream_index:int) -> float:
"""
Get the position of the audio
Args:
stream_index (int): Index of the stream
"""
return self.positions[stream_index] / float(self.frame_rate)
def adjust_volume(self, data:bytes, volume:float) -> bytes:
"""
Adjust the volume of the audio
Args:
data (bytes): Audio data
volume (float): Volume value
"""
data = bytearray(data)
for i in range(0, len(data), 2):
sample = int.from_bytes(data[i:i+2], byteorder='little', signed=True)
sample = int(sample * volume)
data[i:i+2] = sample.to_bytes(2, byteorder='little', signed=True)
return bytes(data)
def apply_effects(self, data: bytes, speed_factor: float, reverb_factor: float, pitch_shift: float, stream_index:int) -> bytes:
"""
Modify the audio
Args:
data (bytes): Audio data
speed_factor (float): Speed value between 0.5 and 2.0 (0.5 = half speed, 2.0 = double speed)
reverb_factor (float): Reverb value between 0.0 and 1.0 (0.0 = no reverb, 1.0 = full reverb)
delay_time (float): Delay time in seconds
stream_index (int): Index of the stream
"""
# Convert bytes to numpy array
samples = np.frombuffer(data, dtype=np.int16)
samples = samples.astype(np.float32)
samples = samples.reshape((len(samples) // 2, 2)).T
samples = samples.mean(axis=0)
# Modify speed
samples = self.modify_speed(samples, speed_factor)
# Modify pitch
samples = self.modify_pitch(samples, pitch_shift)
# Add reverb
samples = self.apply_reverb(samples, reverb_factor, stream_index)
# Convert back to bytes
samples = samples.astype(np.int16)
samples = np.repeat(samples, 2)
return samples.tobytes()
def modify_speed(self, samples:np.ndarray, speed_factor:float) -> bytes:
"""
Modify the speed of the audio
Args:
samples (np.ndarray): Audio data
speed_factor (float): Speed value between 0.5 and 2.0 (0.5 = half speed, 2.0 = double speed)
"""
if speed_factor == 1.0:
return samples
num_samples = len(samples)
new_num_samples = int(num_samples / speed_factor)
# Resample the audio to the new length
resampled = np.interp(
np.linspace(0, num_samples, new_num_samples, endpoint=False),
np.arange(num_samples),
samples
)
return resampled
def apply_antialiasing_filter(self, samples: np.ndarray, cutoff_freq: float, frame_rate: int) -> np.ndarray:
"""
Apply an anti-aliasing filter to the audio
Args:
samples (np.ndarray): Audio data
cutoff_freq (float): Cutoff frequency in Hz
frame_rate (int): Frame rate in Hz
"""
# Create a low-pass Butterworth filter
nyquist = 0.5 * frame_rate
normal_cutoff = cutoff_freq / nyquist
b, a = butter(8, normal_cutoff, btype='low', analog=False)
# Apply the filter to the samples
filtered_samples = filtfilt(b, a, samples, axis=0)
return filtered_samples
def modify_pitch(self, samples:np.ndarray, pitch_shift:float) -> bytes:
"""
Modify the pitch of the audio
Args:
samples (np.ndarray): Audio data
pitch_shift (float): Pitch value between -12.0 and 12.0 (-12.0 = one octave down, 12.0 = one octave up)
"""
if pitch_shift == 0:
return samples
pitch_shift *= 12 # Convert pitch shift from octaves to semitones
samples = samples.astype(np.float32) / np.iinfo(np.int16).max # Convert samples to float32 between -1.0 and 1.0
# Continue with the pitch shift as before
shift_ratio = 2 ** (pitch_shift / 12) # Shift by half-steps (semitones)
time_axis = np.arange(samples.shape[0]) / self.frame_rate
shifted_audio = np.interp(time_axis * shift_ratio, time_axis, samples)
# Apply an anti-aliasing filter to attenuate high frequencies
cutoff_freq = 0.9 * self.frame_rate / 2.0 # You can adjust the cutoff frequency as needed
shifted_audio = self.apply_antialiasing_filter(shifted_audio, cutoff_freq, self.frame_rate)
shifted_audio = np.clip(shifted_audio, -1.0, 1.0) # Clip samples to prevent overflow
# Convert samples back to 16-bit integer range
return shifted_audio * np.iinfo(np.int16).max
def generate_impulse_response(self, length:int, decay_factor:float) -> np.ndarray:
"""
Generate impulse response
Args:
length (int): Length of the impulse response in samples (44100 samples = 1 second)
decay_factor (float): Decay factor between 0.0 and 1.0 (0.0 = no decay, 1.0 = full decay)
"""
impulse_response = np.zeros(length, dtype=np.float32)
impulse_response[0] = 1.0
# Generate decaying exponential tail
for i in range(1, length):
impulse_response[i] = impulse_response[i - 1] * decay_factor
# Add some random noise for early reflections
impulse_response += np.random.normal(0, 0.05, length)
return impulse_response
def apply_reverb(self, samples: np.ndarray, reverb_factor: float, stream_index: int) -> bytes:
"""
Add reverb to the audio
Args:
samples (np.ndarray): Audio data
reverb_factor (float): Reverb value between 0.0 and 1.0 (0.0 = no reverb, 1.0 = full reverb)
stream_index (int): Index of the stream
"""
if reverb_factor == 0:
return samples
samples = samples.astype(np.float32) / np.iinfo(np.int16).max # Convert samples to float32 between -1.0 and 1.0
# Apply reverb impulse response to the current chunk
reverb_audio = fftconvolve(samples, self.impulse_response, mode='full')
# Concatenate previous reverb tail with the current reverb tail
reverb_audio[:len(self.reverb_tails[stream_index])] += self.reverb_tails[stream_index]
# Store the current reverb tail for the next chunk
self.reverb_tails[stream_index] = reverb_audio[len(samples):]
mixed_audio = samples * (1 - reverb_factor) + reverb_audio[:len(samples)] * reverb_factor # Mix original and reverb audio
mixed_audio = np.clip(mixed_audio, -1.0, 1.0) # Clip samples to prevent overflow
# Convert samples back to 16-bit integer range
return mixed_audio * np.iinfo(np.int16).max
def apply_eq(self, data:bytes) -> bytes:
"""
Apply the equalizer effect to the audio
Args:
data (bytes): Audio data
"""
# Convert data to float32
audio_data = np.frombuffer(data, dtype=np.int16)
try:
gains = np.array(self.bands(), dtype=np.float32) # Gain values in dB
except:
gains = np.array([0] * 9, dtype=np.float32)
# Number of frequency bands
num_bands = len(gains)
# Generate frequency range (20Hz to 20kHz)
frequencies = np.fft.rfftfreq(len(audio_data), d=1 / 44100)
# Create equalizer response
response = np.zeros_like(frequencies)
# Apply gain to each frequency band
for i in range(num_bands):
lower_cutoff = self.center_freqs[i] / np.sqrt(2) # Lower cutoff frequency
upper_cutoff = self.center_freqs[i] * np.sqrt(2) # Upper cutoff frequency
# Find indices within the frequency range
indices = np.where((frequencies >= lower_cutoff) & (frequencies <= upper_cutoff))[0]
# Apply gain to the corresponding indices
response[indices] += gains[i]
# Convert response to linear scale and normalize
response_linear = 10 ** (response / 20)
# Apply equalization to audio data
audio_fft = np.fft.rfft(audio_data)
audio_fft *= response_linear[:len(audio_fft)]
equalized_audio = np.fft.irfft(audio_fft).astype(np.int16)
# Clip the audio to avoid distortion
equalized_audio = np.clip(equalized_audio, -32768, 32767)
return equalized_audio.tobytes()
def set_effects(self, effects:bool) -> None:
"""
Set the effects of the audio
Args:
effects (bool): Effects value
"""
self.effects = effects
def set_volume(self, stream_index:int, volume:float) -> None:
"""
Set the volume of the audio
Args:
stream_index (int): Index of the stream
volume (float): Volume value
"""
self.volumes[stream_index] = volume
def set_position(self, stream_index:int, position:float) -> None:
"""
Set the position of the audio
Args:
stream_index (int): Index of the stream
position (float): Position value
"""
self.positions[stream_index] = position
def save(self, files:List[str], volumes:List[int], filename:str, cover_art:bytes=None) -> None:
"""
Save the audio
Args:
files (List[str]): List of audio files
volumes (List[int]): List of volume values
filename (str): Name of the file
"""
# Load the audio files and get the sample rate
audio_data = []
sample_rate = None
for file in files:
data, sr = sf.read(file)
audio_data.append(data)
if sample_rate is None:
sample_rate = sr
adjusted_data = []
for i in range(len(audio_data)):
adjusted_data.append(audio_data[i] * volumes[i])
overlapped_data = np.sum(adjusted_data, axis=0)
sf.write(filename, overlapped_data, sample_rate)
if cover_art is not None and cover_art != "null":
byte_data = base64.b64decode(cover_art)
byte_stream = io.BytesIO(byte_data)
byte_stream.seek(0)
f = music_tag.load_file(filename)
f['artwork'] = byte_stream.read()
f.save()
def pause(self) -> None:
"""
Pause the audio
"""
self.paused = True
def resume(self) -> None:
"""
Resume the audio
"""
if self.paused:
self.paused = False
threading.Thread(target=self.play, daemon=True).start()
def stop(self) -> None:
"""
Stop the audio
"""
for i, stream in enumerate(self.streams):
stream.stop_stream()
stream.close()
self.p.terminate()
def change_files(self, new_files:list, volumes:list) -> None:
"""
Change the files of the audio
Args:
new_files (list): List of new files
volumes (list): List of volumes
"""
self.paused = True
self.volumes = volumes
for i, stream in enumerate(self.streams):
stream.stop_stream()
stream.close()
self.streams.clear()
self.files = new_files
self.positions = [0] * len(new_files)
for file in self.files:
data, self.frame_rate = sf.read(file, dtype='int16')
self.duration = len(data) / self.frame_rate
self.channels = data.shape[1]
stream = self.p.open(format=self.p.get_format_from_width(2),
channels=self.channels,
rate=self.frame_rate,
output=True)
self.streams.append(stream)
self.paused = False
threading.Thread(target=self.play, daemon=True).start()
================================================
FILE: MISST/MISSTpreprocess.py
================================================
import concurrent.futures
import logging
import os
import pathlib
import time
import traceback
import wave
import julius
import numpy as np
import soundfile
import soundfile as sf
import torch
from demucs.apply import BagOfModels, apply_model
from demucs.pretrained import get_model
from MISSThelpers import MISSTconsole
# Modified functions from https://github.com/facebookresearch/demucs,
# https://pytorch.org/audio/main/tutorials/hybrid_demucs_tutorial.html
# and https://github.com/CarlGao4/Demucs-Gui
class MISSTpreprocess():
"""
MISSTpreprocess class
"""
def __init__(self) -> None:
pass
def LoadModel(self, name:str, repo:str = None, device:str = "cuda" if torch.cuda.is_available() else "cpu",) -> BagOfModels:
"""
Load the model
Args:
name (str): Name of the model
repo (str): Repository of the model
device (str): Device to use
"""
model = get_model(name=name, repo=repo)
model.to(device)
model.eval()
return model
def GetData(self, model:BagOfModels) -> dict:
"""
Get the data from the model
Args:
model (demucs.pretrained.BagOfModels): Model to get the data from
"""
res = {}
res["channels"] = model.audio_channels
res["samplerate"] = model.samplerate
if isinstance(model, BagOfModels):
res["models"] = len(model.models)
else:
res["models"] = 1
res["sources"] = model.sources
return res
def Apply(self, model:BagOfModels, wav:np.ndarray, shifts:int = 1) -> dict:
"""
Apply the model to the audio
Args:
model (demucs.pretrained.BagOfModels): Model to apply
wav (numpy.ndarray): Audio data
shifts (int): Number of shifts
"""
audio = wav
ref = audio.mean(0)
audio -= ref.mean()
audio /= ref.std()
sources = apply_model(model, audio[None], shifts=shifts, split=False, overlap=0.25, progress=False)[0]
sources *= ref.std()
sources += ref.mean()
return dict(zip(model.sources, sources))
def convert_audio_channels(self, wav:np.ndarray, channels:int = 2) -> np.ndarray:
"""
Convert the audio channels
Args:
wav (numpy.ndarray): Audio data
channels (int): Number of channels
"""
*shape, src_channels, length = wav.shape
if src_channels == channels:
pass
elif channels == 1:
wav = wav.mean(dim=-2, keepdim=True)
elif src_channels == 1:
wav = wav.expand(*shape, channels, length)
elif src_channels >= channels:
wav = wav[..., :channels, :]
else:
raise Exception("Error changing audio dims")
return wav
def write_wav(self, wav:np.ndarray, filename:str, samplerate:int) -> None:
"""
Write the audio to a WAV file
Args:
wav (numpy.ndarray): Audio data
filename (str): Filename
samplerate (int): Samplerate
"""
if wav.dtype.is_floating_point:
wav = (wav.clamp_(-1, 1) * (2**15 - 1)).short()
with wave.open(filename, "wb") as f:
f.setnchannels(wav.shape[1])
f.setsampwidth(2)
f.setframerate(samplerate)
f.writeframes(bytearray(wav.numpy()))
def compress_wav_to_flac(self, wav_file:str, flac_file:str) -> None:
"""
Compress the WAV file to a FLAC file
Args:
wav_file (str): WAV filename
flac_file (str): FLAC filename
"""
# Read the WAV file
data, samplerate = sf.read(wav_file)
# Write the FLAC file and delete the WAV file
sf.write(flac_file, data, samplerate, format='FLAC')
os.remove(wav_file)
def convert_audio(self, wav:np.ndarray, from_samplerate:int, to_samplerate:int, channels:int) -> np.ndarray:
"""
Convert the audio
Args:
wav (numpy.ndarray): Audio data
from_samplerate (int): From samplerate
to_samplerate (int): To samplerate
channels (int): Number of channels
"""
wav = self.convert_audio_channels(wav, channels)
return julius.resample_frac(wav, from_samplerate, to_samplerate)
def load_audio(self, fn:str, sr:int) -> np.ndarray:
"""
Load the audio
Args:
fn (str): Filename
sr (int): Samplerate
"""
audio, raw_sr = soundfile.read(fn, dtype="float32")
if len(audio.shape) == 1:
audio = np.atleast_2d(audio).transpose()
converted = self.convert_audio(torch.from_numpy(audio.transpose()), raw_sr, sr, 2)
return converted.numpy()
def apply_fade_in_out(self, input_file:str, output_file:str, fade_duration:float) -> None:
"""
Apply fade in and out to the audio
Args:
input_file (str): Input filename
output_file (str): Output filename
fade_duration (float): Fade duration
"""
# Read the audio file
audio_data, sample_rate = sf.read(input_file)
# Calculate the number of samples for the fade duration
fade_samples = int(fade_duration * sample_rate)
# Apply fade-in
fade_in_curve = np.linspace(0.0, 1.0, fade_samples)
audio_data[:fade_samples] *= fade_in_curve[:, np.newaxis]
# Apply fade-out
fade_out_curve = np.linspace(1.0, 0.0, fade_samples)
audio_data[-fade_samples:] *= fade_out_curve[:, np.newaxis]
# Save the modified audio to a new file
sf.write(output_file, audio_data, sample_rate)
def process(
self,
model:BagOfModels,
infile:str,
write:bool = True,
outpath:pathlib.Path = pathlib.Path(""),
split:float = 5.0,
overlap:float = 0.25,
sample_rate:int = 44100,
device:str = "cuda" if torch.cuda.is_available() else "cpu",
logger:logging.Logger = logging.getLogger("MISST"),
console:MISSTconsole = None,
) -> None:
"""
Process the audio
Args:
model (demucs.pretrained.BagOfModels): Model to apply
infile (str): Input filename
write (bool): Write the output
outpath (pathlib.Path): Output path
split (float): Split (seconds)
overlap (float): Overlap
sample_rate (int): Sample rate
device (str): Device
logger (logging.Logger): Logger
console (MISSTConsole): Console
"""
# Start the timer
start_time = time.time()
split = int(split * sample_rate)
overlap = int(overlap * split)
logger.info("Loading file")
audio = self.load_audio(str(infile), sample_rate)
logger.info(f"Loaded audio of shape {audio.shape}")
orig_len = audio.shape[1]
n = int(np.ceil((orig_len - overlap) / (split - overlap)))
audio = np.pad(audio, [(0, 0), (0, n * (split - overlap) + overlap - orig_len)])
logger.info("Loading model to device %s" % device)
model.to(device)
stems = self.GetData(model)["sources"]
new_audio = np.zeros((len(stems), 2, audio.shape[1]))
total = np.zeros(audio.shape[1])
logger.info("Total splits of '%s' : %d" % (str(infile), n))
def separation(i):
logger.info("Separation %d/%d" % (i + 1, n))
console.editLine(f"MISST Preprocessor\nCopyright (C) @MISST App.\n\nMISST> Split {i + 1}/{n} ({((i + 1)/n) * 100:.1f}%)", 0)
l = i * (split - overlap)
r = l + split
result = self.Apply(model, torch.from_numpy(audio[:, l:r]).to(device))
for (j, stem) in enumerate(stems):
new_audio[j, :, l:r] += result[stem].cpu().numpy()
total[l:r] += 1
workers = 1 if device == "cuda" else (os.cpu_count() // 2) # CPU parallelization on half the cores available, CUDA kernels are already parallelized
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
futures = [executor.submit(separation, i) for i in range(n)]
concurrent.futures.wait(futures)
if write:
console.endUpdate()
console.addLine("\nMISST> Preprocessed.")
console.update("\nMISST> Writing to file")
outpath.mkdir(exist_ok=True)
for i in range(len(stems)):
if np.all(total == 0):
stem = np.zeros_like(new_audio[i])
else:
stem = (new_audio[i] / total)[:, :orig_len]
self.write_wav(torch.from_numpy(stem.transpose()), str(outpath / f"{stems[i]}.wav"), sample_rate)
self.compress_wav_to_flac(str(outpath / f"{stems[i]}.wav"), str(outpath / f"{stems[i]}.flac"))
self.apply_fade_in_out(str(outpath / f"{stems[i]}.flac"), str(outpath / f"{stems[i]}.flac"), 3.5)
logger.info("Wrote to %s" % str(outpath))
logger.info("Done in %.2f seconds" % (time.time() - start_time))
console.endUpdate()
else:
pass
del model # Free up memory
def preprocess(self, file:str, outDir:str, chosen_model:str, device:str = "cuda") -> None:
"""
Preprocess the audio
Args:
file (str): Input filename
outDir (str): Output directory
device (str): Device
"""
self.logger.info(f"Preprocessing {file}...")
console = MISSTconsole(self.preprocess_terminal_text, "MISST Preprocessor\nCopyright (C) @MISST App.\n")
try:
savename = os.path.basename(file).replace('.mp3', '').replace('.wav', '').replace('.flac', '')
console.update("\nMISST> Loading model")
processor = MISSTpreprocess()
model = processor.LoadModel(name=chosen_model, repo=pathlib.Path("Pretrained"))
console.endUpdate()
console.addLine("\nMISST> Model loaded.")
console.update("\nMISST> Preprocessing")
processor.process(model, infile=pathlib.Path(file), outpath=pathlib.Path(f"{outDir}/{savename}"), device=device, console=console)
del model # Free up memory
except Exception as e:
self.logger.error(e)
self.logger.error(traceback.format_exc())
console.addLine("\nMISST> Error.")
pass
console.addLine("\nMISST> Done.")
self.import_file_button.configure(state='normal')
self.import_button.configure(state='normal')
return
================================================
FILE: MISST/MISSTsettings.py
================================================
import json
import os
import shutil
import torch
class MISSTsettings():
"""
Class for handling the settings of MISST
"""
def __init__(self) -> None:
"""
Initialize the settings
"""
# Check if the config file exists, if not create it
# Path: MISST\MISSTsettings.py
if not os.path.isfile("config.json"):
self.createSettings()
return
def getSetting(self, setting:str) -> str:
"""
Get the setting from the saved config file
Args:
setting (str): The setting to be retrieved
"""
# Get the setting from the saved config file
# Path: MISST\MISSTsettings.py
# setting: The setting to be retrieved
with open("config.json", "r") as f:
data = json.load(f)
return data[setting]
def setSetting(self, setting:str, value:str) -> None:
"""
Set the setting in the saved config file
Args:
setting (str): The setting to be set
value (str): The value to be set
"""
# Set the setting in the saved config file
# Path: MISST\MISSTsettings.py
# setting: The setting to be set
# value: The value to be set
with open("config.json", "r") as f:
data = json.load(f)
data[setting] = value
with open("config.json", "w") as f:
json.dump(data, f, indent=4)
def applyThemeSettings(self, themeFile:str, baseTheme:str) -> None:
"""
Apply the chosen colorways to the theme file
Args:
themeFile (str): The theme file to be modified
baseTheme (str): The base theme file
"""
# Apply the chosen colorways to the theme file
# Path: MISST\MISSTsettings.py
# themeFile: The theme file to be modified
# colorways: The colorways to be applied
with open(baseTheme, "r") as f:
data = f.read()
colorways = ["defaultLightColor", "defaultDarkColor", "defaultLightHoverColor", "defaultDarkHoverColor", "defaultLightDarker", "defaultDarkDarker"]
for colorway in colorways:
data = data.replace(colorway, self.getSetting(colorway.replace('default', 'chosen')))
with open(themeFile, "w") as f:
f.write(data)
def resetDefaultTheme(self, themeFile:str, baseTheme:str) -> None:
"""
Reset the theme file to the default theme
Args:
themeFile (str): The theme file to be modified
baseTheme (str): The base theme file
"""
# Reset the theme file to the default theme
# Path: MISST\MISSTsettings.py
# themeFile: The theme file to be modified
# colorways: The colorways to be applied
with open(baseTheme, "r") as f:
data = f.read()
colorways = ["defaultLightColor", "defaultDarkColor", "defaultLightHoverColor", "defaultDarkHoverColor", "defaultLightDarker", "defaultDarkDarker"]
for colorway in colorways:
self.setSetting(colorway.replace('default', 'chosen'), self.getSetting(colorway))
data = data.replace(colorway, self.getSetting(colorway))
with open(themeFile, "w") as f:
f.write(data)
def createSettings(self) -> None:
"""
Create the config file
"""
# Creates a new config file in the apps directory with all the default settings.
shutil.copy("Assets/config_base.json", "config.json")
self.setSetting("accelerate_on_gpu", "true" if torch.cuda.is_available() else "false") #Automatically set GPU acceleration to true if available.
class MISSTconfig:
"""
Class for handling the metadata of the current song
"""
def __init__(self, configPath:str) -> None:
"""
Initialize the config for the current song
Args:
configPath (str): The path to the config file
"""
# Check if the config file exists, if not create it
# Path: MISST\MISSTsettings.py
if not os.path.isfile(f"{configPath}/.misst"):
self.createConfig(configPath)
return
def getConfig(self, configPath:str) -> str:
"""
Get the setting from the saved config file
Args:
configPath (str): The path to the config file
"""
# Get the setting from the saved config file
# Path: MISST\MISSTsettings.py
# setting: The setting to be retrieved
with open(f"{configPath}/.misst", "r") as f:
data = json.load(f)
return data
def setConfig(self, configPath:str, setting:str, value:str) -> None:
"""
Set the setting in the saved config file
Args:
configPath (str): The path to the config file
setting (str): The setting to be set
value (str): The value to be set
"""
# Set the setting in the saved config file
# Path: MISST\MISSTsettings.py
# setting: The setting to be set
# value: The value to be set
with open(f"{configPath}/.misst", "r") as f:
data = json.load(f)
data[setting] = value
with open(f"{configPath}/.misst", "w") as f:
json.dump(data, f, indent=4)
def createConfig(self, configPath:str) -> None:
"""
Create the config file
Args:
configPath (str): The path to the config file
"""
# Creates a new config file in the apps directory with all the default settings.
with open(f"{configPath}/.misst", "w") as f:
json.dump({"image_url": "null", "image_raw": "null", "lyrics": "null"}, f, indent=4)
f.close()
================================================
FILE: MISST/Pretrained/files.txt
================================================
# MDX Models
root: mdx_final/
0d19c1c6-0f06f20e.th
5d2d6c55-db83574e.th
7d865c68-3d5dd56b.th
7ecf8ec1-70f50cc9.th
a1d90b5c-ae9d2452.th
c511e2ab-fe698775.th
cfa93e08-61801ae1.th
e51eebcc-c1b80bdd.th
6b9c2ca1-3fd82607.th
b72baf4e-8778635e.th
42e558d4-196e0e1b.th
305bc58f-18378783.th
14fc6a69-a89dd0ee.th
464b36d7-e5a9386e.th
7fd6ef75-a905dd85.th
83fc094f-4a16d450.th
1ef250f1-592467ce.th
902315c2-b39ce9c9.th
9a6b4851-03af0aa6.th
fa0cb7f9-100d8bf4.th
# Hybrid Transformer models
root: hybrid_transformer/
955717e8-8726e21a.th
f7e0c4bc-ba3fe64a.th
d12395a8-e57c48e6.th
92cfc3b6-ef3bcb9c.th
04573f0d-f3cf25b2.th
75fc33f5-1941ce65.th
================================================
FILE: MISST/Pretrained/hdemucs_mmi.yaml
================================================
models: ['75fc33f5']
segment: 44
================================================
FILE: MISST/Pretrained/htdemucs.yaml
================================================
models: ['955717e8']
================================================
FILE: MISST/Pretrained/htdemucs_ft.yaml
================================================
models: ['f7e0c4bc', 'd12395a8', '92cfc3b6', '04573f0d']
weights: [
[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.],
]
================================================
FILE: MISST/Pretrained/mdx.yaml
================================================
models: ['0d19c1c6', '7ecf8ec1', 'c511e2ab', '7d865c68']
weights: [
[1., 1., 0., 0.],
[0., 1., 0., 0.],
[1., 0., 1., 1.],
[1., 0., 1., 1.],
]
segment: 44
================================================
FILE: MISST/Pretrained/mdx_extra.yaml
================================================
models: ['e51eebcc', 'a1d90b5c', '5d2d6c55', 'cfa93e08']
segment: 44
================================================
FILE: MISST/Pretrained/mdx_extra_q.yaml
================================================
models: ['83fc094f', '464b36d7', '14fc6a69', '7fd6ef75']
segment: 44
================================================
FILE: MISST/Pretrained/mdx_q.yaml
================================================
models: ['6b9c2ca1', 'b72baf4e', '42e558d4', '305bc58f']
weights: [
[1., 1., 0., 0.],
[0., 1., 0., 0.],
[1., 0., 1., 1.],
[1., 0., 1., 1.],
]
segment: 44
================================================
FILE: MISST/Pretrained/repro_mdx_a.yaml
================================================
models: ['9a6b4851', '1ef250f1', 'fa0cb7f9', '902315c2']
segment: 44
================================================
FILE: MISST/Pretrained/repro_mdx_a_hybrid_only.yaml
================================================
models: ['fa0cb7f9', '902315c2', 'fa0cb7f9', '902315c2']
segment: 44
================================================
FILE: MISST/Pretrained/repro_mdx_a_time_only.yaml
================================================
models: ['9a6b4851', '9a6b4851', '1ef250f1', '1ef250f1']
segment: 44
================================================
FILE: MISST/__version__.py
================================================
__version__ = '3.1.0'
================================================
FILE: README.md
================================================
[](https://github.com/Frikallo/MISST)
[](https://github.com/Frikallo/MISST/releases/latest) [](https://github.com/Frikallo/MISST/releases/latest) [](https://github.com/Frikallo/MISST/blob/main/LICENSE) [](https://github.com/Frikallo/MISST/graphs/contributors)
---

| _`MISST` on Windows 11 with Dark mode and 'Blue' theme with 'Steve Lacy's Infrunami' playing_

| _`MISST` on Windows 11 with Light mode and 'Blue' theme with 'Frank Ocean's Ivy' playing_

| _`MISST` on Windows 11 Showcasing how versatile and personal you can be with MISST!_

| _`MISST` on Windows 11 Showcasing how importing audios is as easy as two clicks!_
###
Original Repository of MISST : **M**usic/**I**nstrumental **S**tem **S**eparation **T**ool.
This application uses state-of-the-art [demucs](https://github.com/facebookresearch/demucs) source separation models to extract the 4 core stems from audio files (Bass, Drums, Other Instrumentals and Vocals). But it is not limited to this. MISST acts as a developped music player aswell, fit to enjoy and medal with your audio files as you see fit. MISST even comes prepared to import songs and playlists directly from your music library.
This project is OpenSource, feel free to use, study and/or send pull request.
## Objectives:
- [x] Import songs and playlists from your music library
- [x] Easy to use UI
- [x] Play your songs and playlists
- [x] Extract and manipulate the 4 core stems from your audio files as they play
- [x] Discord rich presence to show off your music to your friends
- [x] Save your stems as audio files
- [x] If imported from your music library, view lyrics and metadata just as you would in your old music player
- [x] Minimal memory usage
- [x] Customizable themes
- [x] Additional Efects like nightcore
- [x] Easy to use equalizer
- [x] Preprocessing service available on both CPU and GPU
- [x] Ability to change the pre-trained model used for separation
- [x] Small save size, comparable to the size of the inputted audio
- [ ] Docker image (WIP)
- [ ] Stable on Windows, Linux and MacOS (WIP)
- [ ] Proper installer/updater (Not a priority)
## Installation
As of version 3.1.0, MISST is only available on windows with guaranteed compatibility.
Until a later release :
- if you are **not on a windows device** please refer to [Manual Installation](https://github.com/Frikallo/MISST/#manual-installation).
- if you are **using conda on MacOs** device please refer to [Manual Installation - MacOS](https://github.com/CAprogs/MISST/blob/main/Installation%20Guide%20-%20MacOS.md)
- Otherwise, refer to the latest [Release](https://github.com/Frikallo/MISST/releases/latest)
## Manual Installation
These instructions are for those installing MISST v3.1.0 **manually** only.
1. Download & install Python 3.9 or higher (but no lower than 3.9.0) [here](https://www.python.org/downloads/)
- **Note:** Ensure the *"Add Python to PATH"* box is checked
2. Download the Source code [here](https://github.com/Frikallo/MISST/releases/latest)
3. Open the command prompt from the MISST directory and run the following commands, separately -
```
$ python3 -m venv ./venv
$ pip install -r requirements.txt
$ python3 MISSTapp.py
```
- **Note:** Install `requirements-minimal.txt` if you don't intend to accelerate preprocessing with your GPU.
From here you should be able to open and run the MISSTapp.py file
- CUDA
- CUDA must be installed and configured for the application to process any track with GPU acceleration. You will need to look up instruction on how to configure it on your operating system. Click [here](https://developer.nvidia.com/cuda-downloads) for nvidia's installation guide.
## Benchmark
The audio processing performance was evaluated using an **NVIDIA GeForce RTX 2070 SUPER** with **8GB VRAM** and an **AMD Ryzen 3700X 8-Core Processor** on the htdemucs pretrained model. This test aimed to compare the processing time of audio on a CPU versus a GPU.
Here are the results of the test:
| Source | Source Length | CPU | GPU | Model |
|------------------------------------------------------------------|---------------|-----------|-----------| --------- |
| [Frank Ocean - Ivy](https://www.youtube.com/watch?v=AE005nZeF-A) | 4m 09.00s | 2m 22.16s | 0m 28.04s | htdemucs |
## Demo
https://github.com/Frikallo/MISST/assets/88942100/15fb7ce3-9f83-4228-9ab0-f453593be632
[Open in YouTube](https://www.youtube.com/watch?v=XYJm5WW9Zvs)
## License
The **MISST** code is [GPL-licensed](LICENSE).
- **Please Note:** For all third-party application developers who wish to use MISST or its code, please honor the GPL license by providing credit to MISST and its developer.
## Issue Reporting
Please be as detailed as possible when posting a new issue.
If possible, check the "MISST.log" file in your install directory for detailed error information that can be provided to me.
## Contributing
- For anyone interested in the ongoing development of **MISST**, please send us a pull request, and I will review it.
- This project is 100% open-source and free for anyone to use and modify as they wish.
- I only maintain the development for **MISST** and the models provided.
## More documentation to come...