From 3fdac15b5102525dfca8b13604a39fa8ace43895 Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Mon, 31 Aug 2020 21:13:39 +0700 Subject: [PATCH 01/22] [Breaking changes] Remove audio-related features and upgrade nbsio main.py: - Remove audio exporting feature - Remove experimental playback feature - Refactor FileBrowseFile and FileOpenFile function - Remove the open file form nbsio.py: - Supports newest format (version 4) - The header reader is separated from the NBS reader - NBSReader now supports HTTPResponse objects --- attr.py | 484 ++++++----- main.py | 2556 +++++++++++++++++++++++++++--------------------------- nbsio.py | 448 +++++----- 3 files changed, 1795 insertions(+), 1693 deletions(-) diff --git a/attr.py b/attr.py index 858805e..1d92cf4 100644 --- a/attr.py +++ b/attr.py @@ -1,221 +1,289 @@ # This file is a part of: -#‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -# ███▄▄▄▄ ▀█████████▄ ▄████████ ███ ▄██████▄ ▄██████▄ ▄█ -# ███▀▀▀██▄ ███ ███ ███ ███ ▀█████████▄ ███ ███ ███ ███ ███ -# ███ ███ ███ ███ ███ █▀ ▀███▀▀██ ███ ███ ███ ███ ███ -# ███ ███ ▄███▄▄▄██▀ ███ ███ ▀ ███ ███ ███ ███ ███ -# ███ ███ ▀▀███▀▀▀██▄ ▀███████████ ███ ███ ███ ███ ███ ███ -# ███ ███ ███ ██▄ ███ ███ ███ ███ ███ ███ ███ -# ███ ███ ███ ███ ▄█ ███ ███ ███ ███ ███ ███ ███▌ ▄ -# ▀█ █▀ ▄█████████▀ ▄████████▀ ▄████▀ ▀██████▀ ▀██████▀ █████▄▄██ -#__________________________________________________________________________________ +# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +# ███▄▄▄▄ ▀█████████▄ ▄████████ ███ ▄██████▄ ▄██████▄ ▄█ +# ███▀▀▀██▄ ███ ███ ███ ███ ▀█████████▄ ███ ███ ███ ███ ███ +# ███ ███ ███ ███ ███ █▀ ▀███▀▀██ ███ ███ ███ ███ ███ +# ███ ███ ▄███▄▄▄██▀ ███ ███ ▀ ███ ███ ███ ███ ███ +# ███ ███ ▀▀███▀▀▀██▄ ▀███████████ ███ ███ ███ ███ ███ ███ +# ███ ███ ███ ██▄ ███ ███ ███ ███ ███ ███ ███ +# ███ ███ ███ ███ ▄█ ███ ███ ███ ███ ███ ███ ███▌ ▄ +# ▀█ █▀ ▄█████████▀ ▄████████▀ ▄████▀ ▀██████▀ ▀██████▀ █████▄▄██ +# __________________________________________________________________________________ # NBSTool is a tool to work with .nbs (Note Block Studio) files. # Author: IoeCmcomc (https://github.com/IoeCmcomc) # Programming language: Python # License: MIT license # Version: 0.7.0 # Source codes are hosted on: GitHub (https://github.com/IoeCmcomc/NBSTool) -#‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -import sys +import sys, math + class Attr: - def __init__(self, value=None, parent=None): - self._value_ = value - self._parent_ = parent - - def __eq__(self, other): - return self._value_ == other - def __ne__(self, other): - return self._value_ != other - def __lt__(self, other): - return self._value_ < other - def __gt__(self, other): - return self._value_ > other - def __le__(self, other): - return self._value_ <= other - def __ge__(self, other): - return self._value_ >= other - - def __pos__(self): - return +self._value_ - def __neg__(self): - return -self._value_ - def __abs__(self): - return abs(self._value_) - def __invert__(self): - return ~self._value_ - def __round__(self, n): - return round(self._value_, n) - def __floor__(self): - return math.floor(self._value_) - def __ceil__(self): - return math.ceil(self._value_) - def __trunc__(self): - return math.trunc(self._value_) - - def __add__(self, other): - return self._value_ + other - def __sub__(self, other): - return self._value_ - other - def __mul__(self, other): - return self._value_ * other - def __floordiv__(self, other): - return self._value_ // other - def __div__(self, other): - return self._value_ / other - def __truediv__(self, other): - return self._value_ / other - def __mod__(self, other): - return self._value_ % other - def __divmod__(self, other): - return divmod(self._value_, other) - def __pow__(self, other): - return self._value_ ** other - def __lshift__(self, other): - return self._value_ << other - def __rshift__(self, other): - return self._value_ >> other - def __and__(self, other): - return self._value_ & other - def __or__(self, other): - return self._value_ | other - def __xor__(self, other): - return self._value_ ^ other - - def __radd__(self, other): - return other + self._value_ - def __rsub__(self, other): - return other - self._value_ - def __rmul__(self, other): - return other * self._value_ - def __rfloordiv__(self, other): - return other // self._value_ - def __rdiv__(self, other): - return other / self._value_ - def __rtruediv__(self, other): - return other / self._value_ - def __rmod__(self, other): - return other % self._value_ - def __rdivmod__(self, other): - return divmod(other, self._value_) - def __rpow__(self, other): - return other ** self._value_ - def __rlshift__(self, other): - return other << self._value_ - def __rrshift__(self, other): - return other >> self._value_ - def __rand__(self, other): - return other & self._value_ - def __ror__(self, other): - return other | self._value_ - def __rxor__(self, other): - return other ^ self._value_ - - def __iadd__(self, other): - return self._value_ + other - def __isub__(self, other): - return self._value_ - other - def __imul__(self, other): - return self._value_ * other - def __ifloordiv__(self, other): - return self._value_ // other - def __idiv__(self, other): - return self._value_ / other - def __itruediv__(self, other): - return self._value_ / other - def __imod__(self, other): - return self._value_ % other - def __idivmod__(self, other): - return divmod(self._value_, other) - def __ipow__(self, other): - return self._value_ ** other - def __ilshift__(self, other): - return self._value_ << other - def __irshift__(self, other): - return self._value_ >> other - def __iand__(self, other): - return self._value_ & other - def __ior__(self, other): - return self._value_ | other - def __ixor__(self, other): - return self._value_ ^ other - - def __int__(self): - return int(self._value_) - def __long__(self): - return long(self._value_) - def __float__(self): - return float(self._value_) - def __complex__(self): - return complex(self._value_) - def __oct__(self): - return oct(self._value_) - def __hex__(self): - return hex(self._value_) - def __index__(self): - return self._value_ - def __trunc__(self): - return math.trunc(self._value_) - def __coerce__(self, other): - return coerce(self._value_, other) - - def __str__(self): - return str(self._value_) - def __repr__(self): - #return repr(self._value_) - reprStr = "".format \ - (hex(id(self)), repr(self._value_.__class__.__name__)) - return reprStr - def __unicode__(self): - return unicode(self._value_) - def __format__(self, formatstr): - pattern = '{0:'+formatstr+'}' - return pattern.format(self._value_) - def __hash__(self): - return hash(self._value_) - def __nonzero__(self): - return bool(self._value_) - def __dir__(self): - return super().__dir__() - def __sizeof__(self): - return sys.getsizeof(self._value_) - - def __setattr__(self, name, value): - if name[:1] == '_': - if len(name) == 1: - setattr(self, '_value_', value) - else: - super().__setattr__(name, value) - else: - super().__setattr__(name, Attr(value, self)) - def __getattr__(self, name): - valueAttr = getattr(self._value_, name, None) - if 'method' in valueAttr.__class__.__name__ or 'function' in valueAttr.__class__.__name__: - return valueAttr - elif name == '_': - - return self._value_ - else: - setattr(self, name, None) - return getattr(self, name) - - def __len__(self): - return len(self._value_) - def __getitem__(self, key): - return self._value_[key] - def __setitem__(self, key, value): - self._value_[key] = value - def __delitem__(self, key): - del self._value_[key] - def __iter__(self): - return iter(self._value_) - def __reversed__(self): - return reversed(self._value_) - def __contains__(self, item): - return item in self._value_ - ''' + def __init__(self, value=None, parent=None): + self._value_ = value + self._parent_ = parent + + def __eq__(self, other): + return self._value_ == other + + def __ne__(self, other): + return self._value_ != other + + def __lt__(self, other): + return self._value_ < other + + def __gt__(self, other): + return self._value_ > other + + def __le__(self, other): + return self._value_ <= other + + def __ge__(self, other): + return self._value_ >= other + + def __pos__(self): + return +self._value_ + + def __neg__(self): + return -self._value_ + + def __abs__(self): + return abs(self._value_) + + def __invert__(self): + return self._value_.__invert__() + + def __round__(self, n): + return round(self._value_, n) + + def __floor__(self): + return math.floor(self._value_) + + def __ceil__(self): + return math.ceil(self._value_) + + def __trunc__(self): + return math.trunc(self._value_) + + def __add__(self, other): + return self._value_ + other + + def __sub__(self, other): + return self._value_ - other + + def __mul__(self, other): + return self._value_ * other + + def __floordiv__(self, other): + return self._value_ // other + + def __div__(self, other): + return self._value_ / other + + def __truediv__(self, other): + return self._value_ / other + + def __mod__(self, other): + return self._value_ % other + + def __divmod__(self, other): + return divmod(self._value_, other) + + def __pow__(self, other): + return self._value_ ** other + + def __lshift__(self, other): + return self._value_ << other + + def __rshift__(self, other): + return self._value_ >> other + + def __and__(self, other): + return self._value_ & other + + def __or__(self, other): + return self._value_ | other + + def __xor__(self, other): + return self._value_ ^ other + + def __radd__(self, other): + return other + self._value_ + + def __rsub__(self, other): + return other - self._value_ + + def __rmul__(self, other): + return other * self._value_ + + def __rfloordiv__(self, other): + return other // self._value_ + + def __rdiv__(self, other): + return other / self._value_ + + def __rtruediv__(self, other): + return other / self._value_ + + def __rmod__(self, other): + return other % self._value_ + + def __rdivmod__(self, other): + return divmod(other, self._value_) + + def __rpow__(self, other): + return other ** self._value_ + + def __rlshift__(self, other): + return other << self._value_ + + def __rrshift__(self, other): + return other >> self._value_ + + def __rand__(self, other): + return other & self._value_ + + def __ror__(self, other): + return other | self._value_ + + def __rxor__(self, other): + return other ^ self._value_ + + def __iadd__(self, other): + return self._value_ + other + + def __isub__(self, other): + return self._value_ - other + + def __imul__(self, other): + return self._value_ * other + + def __ifloordiv__(self, other): + return self._value_ // other + + def __idiv__(self, other): + return self._value_ / other + + def __itruediv__(self, other): + return self._value_ / other + + def __imod__(self, other): + return self._value_ % other + + def __idivmod__(self, other): + return divmod(self._value_, other) + + def __ipow__(self, other): + return self._value_ ** other + + def __ilshift__(self, other): + return self._value_ << other + + def __irshift__(self, other): + return self._value_ >> other + + def __iand__(self, other): + return self._value_ & other + + def __ior__(self, other): + return self._value_ | other + + def __ixor__(self, other): + return self._value_ ^ other + + def __int__(self): + return int(self._value_) + + # def __long__(self): + # return long(self._value_) + + def __float__(self): + return float(self._value_) + + def __complex__(self): + return complex(self._value_) + + def __oct__(self): + return oct(self._value_) + + def __hex__(self): + return hex(self._value_) + + def __index__(self): + return self._value_ + + def __str__(self): + return str(self._value_) + + def __repr__(self): + # return repr(self._value_) + reprStr = "".format( + hex(id(self)), repr(self._value_.__class__.__name__)) + return reprStr + + # def __unicode__(self): + # return unicode(self._value_) + + def __format__(self, formatstr): + pattern = '{0:'+formatstr+'}' + return pattern.format(self._value_) + + def __hash__(self): + return hash(self._value_) + + def __nonzero__(self): + return bool(self._value_) + + def __dir__(self): + return super().__dir__() + + def __sizeof__(self): + return sys.getsizeof(self._value_) + + def __setattr__(self, name, value): + if name[:1] == '_': + if len(name) == 1: + setattr(self, '_value_', value) + else: + super().__setattr__(name, value) + else: + super().__setattr__(name, Attr(value, self)) + + def __getattr__(self, name): + valueAttr = getattr(self._value_, name, None) + if 'method' in valueAttr.__class__.__name__ or 'function' in valueAttr.__class__.__name__: + return valueAttr + elif name == '_': + + return self._value_ + else: + setattr(self, name, None) + return getattr(self, name) + + def __len__(self): + return len(self._value_) + + def __getitem__(self, key): + return self._value_[key] + + def __setitem__(self, key, value): + self._value_[key] = value + + def __delitem__(self, key): + del self._value_[key] + + def __iter__(self): + return iter(self._value_) + + def __reversed__(self): + return reversed(self._value_) + + def __contains__(self, item): + return item in self._value_ + ''' def __missing__(self, key): return super().__missing__(key) @@ -224,4 +292,4 @@ def __call__(self, value=None, parent=None): return self._value_ if len(sys.argv) > 1: if self._value_ is not value: self._value_ = value - if self._parent_ is not parent: self._parent_ = parent''' \ No newline at end of file + if self._parent_ is not parent: self._parent_ = parent''' diff --git a/main.py b/main.py index dd9dec1..03b6818 100644 --- a/main.py +++ b/main.py @@ -1,24 +1,31 @@ # This file is a part of: -#‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -# ███▄▄▄▄ ▀█████████▄ ▄████████ ███ ▄██████▄ ▄██████▄ ▄█ -# ███▀▀▀██▄ ███ ███ ███ ███ ▀█████████▄ ███ ███ ███ ███ ███ -# ███ ███ ███ ███ ███ █▀ ▀███▀▀██ ███ ███ ███ ███ ███ -# ███ ███ ▄███▄▄▄██▀ ███ ███ ▀ ███ ███ ███ ███ ███ -# ███ ███ ▀▀███▀▀▀██▄ ▀███████████ ███ ███ ███ ███ ███ ███ -# ███ ███ ███ ██▄ ███ ███ ███ ███ ███ ███ ███ -# ███ ███ ███ ███ ▄█ ███ ███ ███ ███ ███ ███ ███▌ ▄ -# ▀█ █▀ ▄█████████▀ ▄████████▀ ▄████▀ ▀██████▀ ▀██████▀ █████▄▄██ -#__________________________________________________________________________________ +# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +# ███▄▄▄▄ ▀█████████▄ ▄████████ ███ ▄██████▄ ▄██████▄ ▄█ +# ███▀▀▀██▄ ███ ███ ███ ███ ▀█████████▄ ███ ███ ███ ███ ███ +# ███ ███ ███ ███ ███ █▀ ▀███▀▀██ ███ ███ ███ ███ ███ +# ███ ███ ▄███▄▄▄██▀ ███ ███ ▀ ███ ███ ███ ███ ███ +# ███ ███ ▀▀███▀▀▀██▄ ▀███████████ ███ ███ ███ ███ ███ ███ +# ███ ███ ███ ██▄ ███ ███ ███ ███ ███ ███ ███ +# ███ ███ ███ ███ ▄█ ███ ███ ███ ███ ███ ███ ███▌ ▄ +# ▀█ █▀ ▄█████████▀ ▄████████▀ ▄████▀ ▀██████▀ ▀██████▀ █████▄▄██ +# __________________________________________________________________________________ # NBSTool is a tool to work with .nbs (Note Block Studio) files. # Author: IoeCmcomc (https://github.com/IoeCmcomc) # Programming language: Python # License: MIT license # Version: 0.7.0 # Source codes are hosted on: GitHub (https://github.com/IoeCmcomc/NBSTool) -#‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -import sys, os, operator, webbrowser, copy, traceback, re, json +import sys +import os +import operator +import webbrowser +import copy +import traceback +import re +import json import tkinter as tk import tkinter.ttk as ttk @@ -35,8 +42,6 @@ #from collections import deque from PIL import Image, ImageTk -from pydub import AudioSegment -from pydub.playback import _play_with_simpleaudio import music21 as m21 import music21.stream as m21s import music21.instrument as m21i @@ -45,1322 +50,1297 @@ from nbsio import opennbs, writenbs, DataPostprocess from ncfio import writencf -#Credit: https://stackoverflow.com/questions/42474560/pyinstaller-single-exe-file-ico-image-in-title-of-tkinter-main-window +# Credit: https://stackoverflow.com/questions/42474560/pyinstaller-single-exe-file-ico-image-in-title-of-tkinter-main-window + + def resource_path(*args): - if len(args) > 1: - relative_path = os.path.join(*args) - else: relative_path = args[0] - if getattr(sys, 'frozen', False): - datadir = os.path.dirname(sys.executable) - r = os.path.join(datadir, relative_path) - else: - try: - r = os.path.join(sys._MEIPASS, relative_path) - except Exception: - r = os.path.join(os.path.abspath("."), relative_path) - #print(r) - return r + if len(args) > 1: + relative_path = os.path.join(*args) + else: + relative_path = args[0] + if getattr(sys, 'frozen', False): + datadir = os.path.dirname(sys.executable) + r = os.path.join(datadir, relative_path) + else: + try: + r = os.path.join(sys._MEIPASS, relative_path) + except Exception: + r = os.path.join(os.path.abspath("."), relative_path) + # print(r) + return r + class MainWindow(tk.Frame): - def __init__(self, parent): - tk.Frame.__init__(self, parent) - self.parent = parent - self.properties() - self.elements() - self.WindowBind() - self.toggleExpOptiGrp() - #self.update() - self.pack(fill='both', expand=True) - self.update() - WindowGeo(self.parent, self.parent, 800, 500, 600, 500) - self.lift() - self.focus_force() - self.grab_set() - self.grab_release() - - def properties(self): - self.VERSION = '0.7.0' - self.filePath = None - self.inputFileData = None - self.noteSounds = None - self.last = Attr() - self.last.inputFileData = None - self.var = Attr() - self.PlayingState = 'stop' - self.PlayingTick = -1 - self.SongPlayerAfter = None - self.exportFilePath = tk.StringVar() - - def elements(self): - self.parent.title("NBS Tool") - self.style = ttk.Style() - #self.style.theme_use("default") - try: - self.style.theme_use("vista") - except Exception as e: - print(repr(e), e.__class__.__name__) - self.style.theme_use("winnative") - - #Menu bar - self.menuBar = tk.Menu(self) - self.parent.configure(menu=self.menuBar) - self.menus() - - #Tabs - self.NbTabs = ttk.Notebook(self) - self.tabs() - self.NbTabs.enable_traversal() - self.NbTabs.pack(fill='both', expand=True) - - #Footer - tk.Frame(self, height=5).pack() - - self.footer = tk.Frame(self, relief='groove', borderwidth=1, height=25, width=self.winfo_width()) - self.footer.pack_propagate(False) - self.footerElements() - self.footer.pack(side='top', fill='x') - - def menus(self): - # 'File' menu - self.fileMenu = tk.Menu(self.menuBar, tearoff=False) - self.menuBar.add_cascade(label="File", menu=self.fileMenu) - - self.fileMenu.add_command(label="Open", accelerator="Ctrl+O", command = lambda: self.OnBrowseFile(True)) - self.fileMenu.add_command(label="Save", accelerator="Ctrl+S", command=self.OnSaveFile) - self.fileMenu.add_command(label="Save as new file", accelerator="Ctrl+Shift+S", command = lambda: self.OnSaveFile(True)) - self.fileMenu.add_separator() - self.fileMenu.add_command(label="Quit", accelerator="Esc", command=self.onClose) - - self.helpMenu = tk.Menu(self.menuBar, tearoff=False) - self.menuBar.add_cascade(label="Help", menu=self.helpMenu) - - self.helpMenu.add_command(label="About", command=lambda: AboutWindow(self)) - - def tabs(self): - #"General" tab - self.GeneralTab = tk.Frame(self.NbTabs) - - self.GeneralTab.rowconfigure(0) - self.GeneralTab.rowconfigure(1, weight=1) - - self.GeneralTab.columnconfigure(0, weight=1, uniform='a') - self.GeneralTab.columnconfigure(1, weight=1, uniform='a') - - self.GeneralTabElements() - self.NbTabs.add(self.GeneralTab, text="General") - - #"Play" tab - self.PlayTab = tk.Frame(self.NbTabs) - - self.PlayTabElements() - self.NbTabs.add(self.PlayTab, text="Play") - - #"Tools" tab - self.ToolsTab = tk.Frame(self.NbTabs) - - self.ToolsTab.rowconfigure(0, weight=1, uniform='b') - self.ToolsTab.rowconfigure(1, weight=1, uniform='b') - self.ToolsTab.rowconfigure(2) - - self.ToolsTab.columnconfigure(0, weight=1, uniform='b') - self.ToolsTab.columnconfigure(1, weight=1, uniform='b') - - self.ToolsTabElements() - self.NbTabs.add(self.ToolsTab, text="Tools") - - #"Export" tab - self.ExportTab = tk.Frame(self.NbTabs) - - self.ExportTabElements() - self.NbTabs.add(self.ExportTab, text="Export") - - def GeneralTabElements(self): - fpadx, fpady = 10, 10 - padx, pady = 5, 5 - - #"Open file" frame - self.OpenFileFrame = tk.Frame(self.GeneralTab, relief='ridge', borderwidth=1) - self.OpenFileFrame.grid(row=0, columnspan=2, sticky='nsew') - - self.OpenFileLabel = tk.Label(self.OpenFileFrame, text="Open file:", anchor='w', width=8) - self.OpenFileLabel.pack(side='left', padx=padx, pady=pady) - - self.OpenFileEntry = tk.Entry(self.OpenFileFrame) - self.OpenFileEntry.pack(side='left', fill='x', padx=padx, expand=True) - - self.BrowseFileButton = ttk.Button(self.OpenFileFrame, text="Browse", command = lambda: self.OnBrowseFile() ) - self.BrowseFileButton.pack(side='left', padx=padx, pady=pady) - - self.OpenFileButton = ttk.Button(self.OpenFileFrame, text="Open", command = lambda: self.OnOpenFile('', True) ) - self.OpenFileButton.pack(side='left', padx=padx, pady=pady) - - #File metadata frame - self.FileMetaFrame = tk.LabelFrame(self.GeneralTab, text="Metadata") - self.FileMetaFrame.grid(row=1, column=0, padx=fpadx, pady=fpady, sticky='nsew') - - self.FileMetaMess = tk.Message(self.FileMetaFrame, text="No flie was found.") - self.FileMetaMess.pack(fill='both', expand=True, padx=padx, pady=padx) - - #More infomation frame - self.FileInfoFrame = tk.LabelFrame(self.GeneralTab, text="Infomations") - self.FileInfoFrame.grid(row=1, column=1, padx=fpadx, pady=fpady, sticky='nsew') - - self.FileInfoMess = tk.Message(self.FileInfoFrame, text="No flie was found.") - self.FileInfoMess.pack(fill='both', expand=True, padx=padx, pady=pady) - - def PlayTabElements(self): - fpadx, fpady = 10, 10 - padx, pady = 5, 5 - - self.PlayCanvasFrame = tk.Frame(self.PlayTab, relief='groove', borderwidth=1) - self.PlayCanvasFrame.pack(fill='both', expand=True, padx=fpadx, pady=fpady) - - tk.Message(self.PlayCanvasFrame, text='Experimental feature', font=("Arial", 44)).pack(fill='both', expand=True) - - self.PlayCtrlFrame = tk.Frame(self.PlayTab, relief='groove', borderwidth=1) - self.PlayCtrlFrame.pack(fill='both', padx=fpadx, pady=fpady) - - self.PlayCtrlScale = tk.Scale(self.PlayCtrlFrame, from_=0, to=self.inputFileData['headers']['length'] if self.inputFileData is not None else 0, orient='horizontal', sliderlength=25) - self.PlayCtrlScale.pack(side='top', fill='x', expand=True) - self.PlayCtrlScale.bind("", lambda _: self.CtrlSongPlayer(state='pause')) - #self.PlayCtrlScale.bind("", lambda e: setattr(self, 'PlayingTick', e.widget.get())) - self.PlayCtrlScale.bind("", lambda _: self.CtrlSongPlayer(state='play')) - - self.CtrlBtnSW = StackingWidget(self.PlayCtrlFrame, relief='groove', borderwidth=1) - self.CtrlBtnSW.pack(side='left', anchor='sw', padx=padx, pady=pady) - - #self.PlaySongBtn = SquareButton(self.PlayCtrlFrame, text ='▶', size=20, command=lambda: self.CtrlSongPlayer(state='play')) - self.CtrlBtnSW.append(tk.Button(self.CtrlBtnSW, text ='Play', command=lambda: self.CtrlSongPlayer(state='play')), 'play') - self.CtrlBtnSW.pack('play') - - self.CtrlBtnSW.append(tk.Button(self.CtrlBtnSW, text ='Pause', command=lambda: self.CtrlSongPlayer(state='pause')), 'pause') - self.CtrlBtnSW.pack('pause') - - #self.StopSongBtn = SquareButton(self.PlayCtrlFrame, text ='⏹', size=20, command=lambda: self.CtrlSongPlayer(state='stop')) - self.StopSongBtn = tk.Button(self.PlayCtrlFrame, text ='Stop', command=lambda: self.CtrlSongPlayer(state='stop')) - self.StopSongBtn.pack(side='left', anchor='sw', padx=padx, pady=pady) - - def ToolsTabElements(self): - fpadx, fpady = 10, 10 - padx, pady = 5, 0 - - #Flip tool - self.FlipToolFrame = tk.LabelFrame(self.ToolsTab, text="Flipping") - self.FlipToolFrame.grid(row=0, column=0, sticky='nsew', padx=fpadx, pady=fpady) - - self.FlipToolMess = tk.Message(self.FlipToolFrame, anchor='w', text="Flip the note sequence horizontally (by tick), vertically (by layer) or both: ") - self.FlipToolMess.pack(fill='both', expand=True, padx=padx, pady=pady) - - self.var.tool.flip.vertical = tk.IntVar() - self.FilpToolCheckV = tk.Checkbutton(self.FlipToolFrame, text="Vertically", variable=self.var.tool.flip.vertical) - self.FilpToolCheckV.pack(side='left', padx=padx, pady=pady) - - self.var.tool.flip.horizontal = tk.IntVar() - self.FilpToolCheckH = tk.Checkbutton(self.FlipToolFrame, text="Horizontally", variable=self.var.tool.flip.horizontal) - self.FilpToolCheckH.pack(side='left', padx=padx, pady=pady) - - #Instrument tool - self.InstToolFrame = tk.LabelFrame(self.ToolsTab, text="Note's instrument") - self.InstToolFrame.grid(row=0, column=1, sticky='nsew', padx=fpadx, pady=fpady) - - self.InstToolMess = tk.Message(self.InstToolFrame, anchor='w', text="Change all note's instrument to:") - self.InstToolMess.pack(fill='both', expand=True, padx=padx, pady=pady) - - self.var.tool.opts = opts = ["(not applied)"] + [x['name'] for x in vaniNoteSounds] + ["Random"] - self.InstToolCombox = ttk.Combobox(self.InstToolFrame, state='readonly', values=opts) - self.InstToolCombox.current(0) - self.InstToolCombox.pack(side='left', fill='both' ,expand=True, padx=padx, pady=pady) - - #Reduce tool - self.ReduceToolFrame = tk.LabelFrame(self.ToolsTab, text="Reducing") - self.ReduceToolFrame.grid(row=1, column=0, sticky='nsew', padx=fpadx, pady=fpady) - - self.ReduceToolMess = tk.Message(self.ReduceToolFrame, anchor='w', text="Delete as many note as possible to reduce file size.") - self.ReduceToolMess.pack(fill='both', expand=True, padx=padx, pady=pady) - - self.var.tool.reduce.opt1 = tk.IntVar() - self.CompactToolChkOpt1 = FlexCheckbutton(self.ReduceToolFrame, text="Delete duplicate notes", variable=self.var.tool.reduce.opt1, anchor='w') - self.CompactToolChkOpt1.pack(padx=padx, pady=pady) - - self.var.tool.reduce.opt2 = tk.IntVar() - self.CompactToolChkOpt2 = FlexCheckbutton(self.ReduceToolFrame, text=" In every tick, delete all notes except the first note", variable=self.var.tool.reduce.opt2, anchor='w') - self.CompactToolChkOpt2.pack(padx=padx, pady=pady) - - self.var.tool.reduce.opt3 = tk.IntVar() - self.CompactToolChkOpt3 = FlexCheckbutton(self.ReduceToolFrame, text=" In every tick, delete all notes except the last note", variable=self.var.tool.reduce.opt3, anchor='w') - self.CompactToolChkOpt3.pack(padx=padx, pady=(pady, 10)) - - #Compact tool - self.CompactToolFrame = tk.LabelFrame(self.ToolsTab, text="Compacting") - self.CompactToolFrame.grid(row=1, column=1, sticky='nsew', padx=fpadx, pady=fpady) - - self.CompactToolMess = tk.Message(self.CompactToolFrame, anchor='w', text="Remove spaces between notes vertically (by layer) and group them by instruments.") - self.CompactToolMess.pack(fill='both', expand=True, padx=padx, pady=pady) - - self.var.tool.compact = tk.IntVar() - self.CompactToolCheck = FlexCheckbutton(self.CompactToolFrame, text="Compact notes", variable=self.var.tool.compact, command=self.toggleCompactToolOpt, anchor='w') - self.CompactToolCheck.pack(padx=padx, pady=pady) - - self.var.tool.compact.opt1 = tk.IntVar() - self.CompactToolChkOpt1 = FlexCheckbutton(self.CompactToolFrame, text="Automatic separate notes by instruments (remain some spaces)", variable=self.var.tool.compact.opt1, state='disabled', command=lambda: self.toggleCompactToolOpt(2), anchor='w') - self.CompactToolChkOpt1.select() - self.CompactToolChkOpt1.pack(padx=padx*5, pady=pady) - - self.var.tool.compact.opt1_1 = tk.IntVar() - self.CompactToolChkOpt1_1 = FlexCheckbutton(self.CompactToolFrame, text="Group percussions into one layer", variable=self.var.tool.compact.opt1_1, state='disabled', anchor='w') - self.CompactToolChkOpt1_1.select() - self.CompactToolChkOpt1_1.pack(padx=padx*5*2, pady=pady) - - #'Apply' botton - self.ToolsTabButton = ttk.Button(self.ToolsTab, text="Apply", state='disabled', command = self.OnApplyTool ) - self.ToolsTabButton.grid(row=2, column=1, sticky='se', padx=fpadx, pady=fpady) - - def ExportTabElements(self): - fpadx, fpady = 10, 10 - padx, pady = 5, 5 - - #Upper frame - self.ExpConfigFrame = tk.LabelFrame(self.ExportTab, text="Option") - self.ExpConfigFrame.pack(fill='both', expand=True, padx=fpadx, pady=fpady) - - #"Select mode" frame - self.ExpConfigGrp1 = tk.Frame(self.ExpConfigFrame, relief='groove', borderwidth=1) - self.ExpConfigGrp1.pack(fill='both', padx=fpadx) - - self.ExpConfigLabel = tk.Label(self.ExpConfigGrp1, text="Export the song as a:", anchor='w') - self.ExpConfigLabel.pack(side='left', fill='x', padx=padx, pady=pady) - - self.var.export.mode = tk.IntVar() - self.ExpConfigMode1 = tk.Radiobutton(self.ExpConfigGrp1, text="File", variable=self.var.export.mode, value=1) - self.ExpConfigMode1.pack(side='left', padx=padx, pady=pady) - self.ExpConfigMode1.select() - self.ExpConfigMode2 = tk.Radiobutton(self.ExpConfigGrp1, text="Datapack", variable=self.var.export.mode, value=0) - self.ExpConfigMode2.pack(side='left', padx=padx, pady=pady) - - self.var.export.type.file = \ - [('Musical Instrument Digital files', '*.mid'), - ('Nokia Composer Format', '*.txt'), - ('MPEG-1 Layer 3', '*.mp3'), - ('Waveform Audio File Format', '*.wav'), - ('Ogg Vorbis files', '*.ogg'), - ('Free Lossless Audio Codec files', '*.flac')] - self.var.export.type.dtp = ['Wireless note block song', 'other'] - self.ExpConfigCombox = ttk.Combobox(self.ExpConfigGrp1, state='readonly', values=["{} ({})".format(tup[0], tup[1]) for tup in self.var.export.type.file]) - self.ExpConfigCombox.current(0) - self.ExpConfigCombox.bind("<>", self.toggleExpOptiGrp) - self.ExpConfigCombox.pack(side='left', fill='x', padx=padx, pady=pady) - - self.var.export.mode.trace('w', self.toggleExpOptiGrp) - - ttk.Separator(self.ExpConfigFrame, orient="horizontal").pack(fill='x', expand=False, padx=padx*3, pady=pady) - - self.ExpOptiSW = StackingWidget(self.ExpConfigFrame, relief='groove', borderwidth=1) - self.ExpOptiSW.pack(fill='both', expand=True, padx=fpadx) - - #Midi export options frame - self.ExpOptiSW.append(tk.Frame(self.ExpOptiSW), 'Midi') - self.ExpOptiSW.pack('Midi', side='top', fill='both', expand=True) - - self.var.export.midi.opt1 = tk.IntVar() - self.ExpMidi1Rad1 = tk.Radiobutton(self.ExpOptiSW['Midi'], text="Sort notes to MIDI tracks by note's layer", variable=self.var.export.midi.opt1, value=1) - self.ExpMidi1Rad1.pack(anchor='nw', padx=padx, pady=(pady, 0)) - self.ExpMidi1Rad2 = tk.Radiobutton(self.ExpOptiSW['Midi'], text="Sort notes to MIDI tracks by note's instrument", variable=self.var.export.midi.opt1, value=0) - self.ExpMidi1Rad2.pack(anchor='nw', padx=padx, pady=(0, pady)) - - #Nokia export options frame - self.ExpOptiSW.append(tk.Frame(self.ExpOptiSW), 'NCF') - self.ExpOptiSW.pack('NCF', side='top', fill='both', expand=True) - - self.NCFOutput = ScrolledText(self.ExpOptiSW['NCF'], state="disabled", height=10) - self.NCFOutput.pack(fill='both', expand=True) - - #'Wireless song datapack' export options frame - self.ExpOptiSW.append(tk.Frame(self.ExpOptiSW), 'Wnbs') - self.ExpOptiSW.pack('Wnbs', side='top', fill='both', expand=True) - - self.WnbsIDLabel = tk.Label(self.ExpOptiSW['Wnbs'], text="Unique name:") - self.WnbsIDLabel.pack(anchor='nw', padx=padx, pady=pady) - - #vcmd = (self.register(self.onValidate), '%P') - self.WnbsIDEntry = tk.Entry(self.ExpOptiSW['Wnbs'], validate="key", - validatecommand=(self.register(lambda P: bool(re.match("^(\d|\w|[-_])*$", P))), '%P')) - self.WnbsIDEntry.pack(anchor='nw', padx=padx, pady=pady) - - #Other export options frame - self.ExpOptiSW.append(tk.Frame(self.ExpOptiSW), 'Other') - self.ExpOptiSW.pack('Other', side='top', fill='both', expand=True) - - self.ExpMusicLabel = tk.Label(self.ExpOptiSW['Other'], text="There is no option available.") - self.ExpMusicLabel.pack(anchor='nw', padx=padx, pady=pady) - - #Output frame - self.ExpOutputFrame = tk.LabelFrame(self.ExportTab, text="Output") - self.ExpOutputFrame.pack(fill='both', padx=fpadx, pady=(0, fpady)) - - self.ExpOutputLabel = tk.Label(self.ExpOutputFrame, text="File path:", anchor='w', width=8) - self.ExpOutputLabel.pack(side='left', fill='x', padx=padx, pady=pady) - - self.ExpOutputEntry = tk.Entry(self.ExpOutputFrame, textvariable=self.exportFilePath) - self.ExpOutputEntry.pack(side='left', fill='x', padx=padx, expand=True) - - self.ExpBrowseButton = ttk.Button(self.ExpOutputFrame, text="Browse", command = self.OnBrowseExp ) - self.ExpBrowseButton.pack(side='left', padx=padx, pady=pady) - - self.ExpSaveButton = ttk.Button(self.ExpOutputFrame, text="Export", command = self.OnExport ) - self.ExpSaveButton.pack(side='left', padx=padx, pady=pady) - - def footerElements(self): - self.footerLabel = tk.Label(self.footer, text="Ready") - self.footerLabel.pack(side='left', fill='x') - self.var.footerLabel = 0 - - self.sizegrip = ttk.Sizegrip(self.footer) - self.sizegrip.pack(side='right', anchor='se') - - self.progressbar = ttk.Progressbar(self.footer, orient="horizontal", length=300 ,mode="determinate") - self.progressbar["value"] = 0 - self.progressbar["maximum"] = 100 - #self.progressbar.start() - #self.progressbar.stop() - - def WindowBind(self): - #Keys - self.parent.bind('', self.onClose) - self.parent.bind('', lambda _: self.OnBrowseFile(True)) - self.parent.bind('', self.OnSaveFile) - self.parent.bind('', lambda _: self.OnSaveFile(True)) - self.parent.bind('', lambda _: self.OnSaveFile(True)) - - #Bind class - self.bind_class("Message" ,"", lambda e: e.widget.configure(width=e.width-10)) - - for tkclass in ('TButton', 'Checkbutton', 'Radiobutton'): - self.bind_class(tkclass, '', lambda e: e.widget.event_generate('', when='tail')) - - self.bind_class("TCombobox", "", lambda e: e.widget.event_generate('')) - - for tkclass in ("Entry", "Text", "ScrolledText"): - self.bind_class(tkclass ,"", self.popupmenus) - - self.bind_class("TNotebook", "<>", self._on_tab_changed) - - - #Credit: http://code.activestate.com/recipes/580726-tkinter-notebook-that-fits-to-the-height-of-every-/ - def _on_tab_changed(self,event): - event.widget.update_idletasks() - - tab = event.widget.nametowidget(event.widget.select()) - event.widget.configure(height=tab.winfo_reqheight()) - - def popupmenus(self, event): - w = event.widget - self.popupMenu = tk.Menu(self, tearoff=False) - self.popupMenu.add_command(label="Select all", accelerator="Ctrl+A", command=lambda: w.event_generate("")) - self.popupMenu.add_separator() - self.popupMenu.add_command(label="Cut", accelerator="Ctrl+X", command=lambda: w.event_generate("")) - self.popupMenu.add_command(label="Copy", accelerator="Ctrl+C", command=lambda: w.event_generate("")) - self.popupMenu.add_command(label="Paste", accelerator="Ctrl+V", command=lambda: w.event_generate("")) - self.popupMenu.tk.call("tk_popup", self.popupMenu, event.x_root, event.y_root) - - def onClose(self, event=None): - self.parent.quit() - self.parent.destroy() - - def OnBrowseFile(self, doOpen=False, filename=None): - types = [('Note Block Studio files', '*.nbs'), ('All files', '*')] - if filename is None: filename = askopenfilename(filetypes = types) - if self.filePath is None or bool(filename): - self.filePath = filename - self.OpenFileEntry.delete(0,'end') - self.OpenFileEntry.insert(0, filename) - if doOpen: - self.OnOpenFile(filename, None) - - def OnOpenFile(self, fileName, fromEntry=False): - if fromEntry: fileName = self.OpenFileEntry.get() - self.UpdateProgBar(20) - if fileName != '': - data = opennbs(fileName) - if data is not None: - self.UpdateProgBar(80) - self.inputFileData = data - self.ExpOutputEntry.delete(0, 'end') - self.exportFilePath.set('') - print(type(data)) - - self.UpdateVar() - self.parent.title('"{}" – NBS Tool'.format(fileName.split('/')[-1])) - self.RaiseFooter('Opened') - self.UpdateProgBar(100) - self.UpdateProgBar(-1) - - def OnSaveFile(self, saveAsNewFile=False): - if self.inputFileData is not None: - if saveAsNewFile is True: - types = [('Note Block Studio files', '*.nbs'), ('All files', '*')] - self.filePath = asksaveasfilename(filetypes = types) - if not self.filePath.lower().endswith('.nbs'): self.filePath = self.filePath.split('.')[0] + '.nbs' - self.UpdateProgBar(50) - - writenbs(self.filePath, self.inputFileData) - - if saveAsNewFile is True: - self.OpenFileEntry.delete(0,'end') - self.OpenFileEntry.insert(0, self.filePath) - - self.parent.title('"{}" – NBS Tool'.format(self.filePath.split('/')[-1])) - self.UpdateProgBar(100) - self.RaiseFooter('Saved') - self.UpdateProgBar(-1) - - def toggleCompactToolOpt(self, id=1): - if id <= 2: - a = ((self.var.tool.compact.opt1.get() == 0) or (self.var.tool.compact.get() == 0)) - self.CompactToolChkOpt1_1["state"] = "disable" if a is True else "normal" - if id <= 1: - self.CompactToolChkOpt1["state"] = "disable" if self.var.tool.compact.get() == 0 else "normal" - - def CtrlSongPlayer(self, event=None, state=None, repeat=False): - if self.inputFileData is None: return - hds = self.inputFileData['headers'] - notes = self.inputFileData['notes'] - layers = self.inputFileData['layers'] - tickIndexes = self.inputFileData['indexByTick'] - noteSounds = self.noteSounds - - if state == 'play' and self.PlayingState != 'play' or repeat and self.SongPlayerAfter is not None: - if self.PlayingTick <= hds['length'] - 1: - self.PlayingState = 'play' - self.CtrlBtnSW.switch('pause') - self.PlayingTick = int(self.PlayCtrlScale.get()) - self.PlayCtrlScale.set(self.PlayingTick + 1) - - toUnsigned = lambda x: 256 + x if x < 0 else x - tickSoundLength = max(len(x['obj']) for x in noteSounds) - currNotes = tickIndexes[self.PlayingTick][1] - SoundToPlay = None - - for n, i in enumerate(currNotes ): - note = notes[i] - currLayer = layers[note['layer']] - inst = note['inst'] - - currNoteSound = noteSounds[inst] - noteSoundObj = currNoteSound['obj'] - - ANoteSound = noteSoundObj._spawn(noteSoundObj.raw_data, overrides={'frame_rate': int(noteSoundObj.frame_rate * (2.0 ** ((note['key'] - currNoteSound['pitch']) / 12))) }).set_frame_rate(44100) - if 0 < currLayer['volume'] < 100: ANoteSound = ANoteSound.apply_gain(noteSoundObj.dBFS - noteSoundObj.dBFS * (currLayer['volume'] / 100)) + 3 - if currLayer['stereo'] != 100: ANoteSound = ANoteSound.pan((toUnsigned(currLayer['stereo']) - 100) / 100) - - if len(currNotes) == 1: SoundToPlay = ANoteSound - elif n == 0: SoundToPlay = ANoteSound + AudioSegment.silent(duration=tickSoundLength - len(ANoteSound)) - else: SoundToPlay = SoundToPlay.overlay(ANoteSound) - - if len(currNotes) > 0: _play_with_simpleaudio(SoundToPlay.set_frame_rate(44100)) - - #self.SongPlayerAfter = self.after(40, lambda: self.CtrlSongPlayer(state='play', repeat=True) ) - self.SongPlayerAfter = self.after(int(1000 / hds['tempo'] * 0.9), lambda: self.CtrlSongPlayer(state='play', repeat=True) ) - else: - state = 'stop' - - if state == 'pause' and self.PlayingState != 'pause': - if self.PlayingState == 'stop': return - self.PlayingState = 'pause' - self.CtrlBtnSW.switch('play') - self.after_cancel(self.SongPlayerAfter) - self.SongPlayerAfter = None - - if state == 'stop' and self.PlayingState != 'stop' and self.SongPlayerAfter: - self.PlayingState = 'stop' - self.PlayingTick = -1 - self.CtrlBtnSW.switch('play') - self.after_cancel(self.SongPlayerAfter) - self.SongPlayerAfter = None - self.PlayCtrlScale.set(0) - - - def OnApplyTool(self): - self.ToolsTabButton['state'] = 'disabled' - self.UpdateProgBar(0) - data = self.inputFileData - ticklen = data['headers']['length'] - layerlen = data['maxLayer'] - instOpti = self.InstToolCombox.current() - self.UpdateProgBar(20) - for note in data['notes']: - #Flip - if bool(self.var.tool.flip.horizontal.get()): note['tick'] = ticklen - note['tick'] - if bool(self.var.tool.flip.vertical.get()): note['layer'] = layerlen - note['layer'] - - #Instrument change - if instOpti > 0: - note['inst'] = randrange(len(self.var.tool.opts)-2) if instOpti > len(self.noteSounds) else instOpti-1 - self.UpdateProgBar(30) - #Reduce - if bool(self.var.tool.reduce.opt2.get()) and bool(self.var.tool.reduce.opt3.get()): - data['notes'] = [note for i, note in enumerate(data['notes']) if note == data['notes'][-1] or note['tick'] != data['notes'][i-1]['tick'] or note['tick'] != data['notes'][i+1]['tick']] - elif bool(self.var.tool.reduce.opt2.get()): - data['notes'] = [note for i, note in enumerate(data['notes']) if note['tick'] != data['notes'][i-1]['tick']] - elif bool(self.var.tool.reduce.opt3.get()): - data['notes'] = [data['notes'][i-1] for i, note in enumerate(data['notes']) if note['tick'] != data['notes'][i-1]['tick']] - self.UpdateProgBar(60) - if bool(self.var.tool.reduce.opt1.get()): - data['notes'] = sorted(data['notes'], key = operator.itemgetter('tick', 'inst', 'key', 'layer') ) - data['notes'] = [note for i, note in enumerate(data['notes']) if note['tick'] != data['notes'][i-1]['tick'] or note['inst'] != data['notes'][i-1]['inst'] or note['key'] != data['notes'][i-1]['key']] - data['notes'] = sorted(data['notes'], key = operator.itemgetter('tick', 'layer') ) - - self.UpdateProgBar(50) - #Compact - if bool(self.var.tool.compact.get()): data = compactNotes(data, self.var.tool.compact.opt1.get(), self.var.tool.compact.opt1_1.get()) - self.UpdateProgBar(60) - #Sort notes - data['notes'] = sorted(data['notes'], key = operator.itemgetter('tick', 'layer') ) - - self.UpdateProgBar(60) - data = DataPostprocess(data) - - self.UpdateProgBar(90) - self.UpdateVar() - #self.UpdateProgBar(100) - self.RaiseFooter('Applied') - self.UpdateProgBar(-1) - self.ToolsTabButton['state'] = 'normal' - - def UpdateVar(self): - #print("Started updating….") - data = self.inputFileData - if data is not None: - self.ToolsTabButton['state'] = 'normal' - self.ExpSaveButton['state'] = 'normal' - if data != self.last.inputFileData: - self.UpdateProgBar(10) - self.parent.title('*"{}" – NBS Tool'.format(self.filePath.split('/')[-1])) - self.UpdateProgBar(20) - headers = data['headers'] - self.UpdateProgBar(30) - text = "File version: {}\nFirst custom inst index: {}\nSong length: {}\nSong height: {}\nSong name: {}\nSong author: {}\nComposer: {}\nSong description: {}\nTempo: {} TPS\nAuto-saving: {}\nTime signature: {}/4\nMinutes spent: {}\nLeft clicks: {}\nRight clicks: {}\nBlocks added: {}\nBlocks removed: {}\nMIDI/Schematic file name: {}".format \ - ( headers['file_version'], headers['vani_inst'], headers['length'], headers['height'], headers['name'], headers['orig_author'], headers['author'], headers['description'], headers['tempo'], "Enabled (save every {} minutes(s)".format(headers['auto-saving_time']) if headers['auto-saving'] else "Disabled", headers['time_sign'], headers['minutes_spent'], headers['left_clicks'], headers['right_clicks'], headers['block_added'], headers['block_removed'], headers['import_name'] ) - self.FileMetaMess.configure(text=text) - self.UpdateProgBar(40) - text = "File format: {}\nHas percussion: {}\nMax layer with at least 1 note block: {}\nCustom instrument(s): {}".format \ - ( "Old" if data['IsOldVersion'] else "New", data['hasPerc'], data['maxLayer'], headers['inst_count'] ) - self.FileInfoMess.configure(text=text) - self.UpdateProgBar(50) - self.PlayCtrlScale['to'] = self.inputFileData['headers']['length'] - self.UpdateProgBar(60) - customInsts = [ {'name': item['name'], 'filepath': resource_path('sounds', item['filename']), 'obj': AudioSegment.from_ogg(resource_path('sounds', item['filename'])), 'pitch': item['pitch']} for item in data['customInsts']] - self.noteSounds = vaniNoteSounds + customInsts - self.UpdateProgBar(70) - self.var.tool.opts = opts = ["(not applied)"] + [x['name'] for x in self.noteSounds] + ["Random"] - self.InstToolCombox.configure(values=opts) - self.UpdateProgBar(80) - if data['maxLayer'] == 0: - text = writencf(data) - else: - text = "The song must have only 1 layer in order to export as Nokia Composer Format." - self.NCFOutput.configure(state='normal') - self.NCFOutput.delete('1.0', 'end') - self.NCFOutput.insert('end', text) - self.NCFOutput.configure(state='disabled') - self.UpdateProgBar(100) - self.last.inputFileData = copy.deepcopy(data) - self.RaiseFooter('Updated') - #print("Updated class properties…", data == self.last.inputFileData) - - self.UpdateProgBar(-1) - else: - self.ToolsTabButton['state'] = 'disabled' - self.ExpSaveButton['state'] = 'disabled' - - self.update_idletasks() - - def toggleExpOptiGrp(self ,n=None, m=None, y=None): - asFile = bool(self.var.export.mode.get()) - key = max(0, self.ExpConfigCombox.current()) - if asFile: - if key == 0: self.ExpOptiSW.switch('Midi') - elif key == 1: self.ExpOptiSW.switch('NCF') - else: self.ExpOptiSW.switch('Other') - self.ExpConfigCombox.configure(values=["{} ({})".format(tup[0], tup[1]) for tup in self.var.export.type.file]) - else: - if key == 0: self.ExpOptiSW.switch('Wnbs') - else: self.ExpOptiSW.switch('Other') - self.ExpConfigCombox.configure(values=["{} ({})".format(tup[0], tup[1]) if isinstance(tup, tuple) else tup for tup in self.var.export.type.dtp]) - key = min(key, len(self.ExpConfigCombox['values'])-1) - self.ExpConfigCombox.current(key) - self.ExpConfigCombox.configure(width=len(self.ExpConfigCombox.get())) - - if self.exportFilePath.get(): - print(self.exportFilePath.get()) - fext = (self.var.export.type.file[self.ExpConfigCombox.current()],)[0][1][1:] - if asFile: - if not self.exportFilePath.get().lower().endswith(self.WnbsIDEntry.get()): self.exportFilePath.set( "{}/{}".format(self.exportFilePath.get(), self.WnbsIDEntry.get()) ) - self.WnbsIDEntry.delete(0, 'end') - if not self.exportFilePath.get().lower().endswith(fext): self.exportFilePath.set( self.exportFilePath.get().split('.')[0] + fext ) - else: - if '.' in self.exportFilePath.get(): - self.WnbsIDEntry.delete(0, 'end') - self.WnbsIDEntry.insert(0, self.exportFilePath.get().split('/')[-1].split('.')[0]) - self.exportFilePath.set( '/'.join(self.exportFilePath.get().split('/')[0:-1]) ) - - def OnBrowseExp(self): - if self.filePath and self.inputFileData: - asFile = bool(self.var.export.mode.get()) - if asFile: - curr = (self.var.export.type.file[self.ExpConfigCombox.current()],) - fext = curr[0][1][1:] - self.exportFilePath.set( asksaveasfilename(title="Export file", initialfile=os.path.splitext(os.path.basename(self.filePath))[0]+fext, filetypes=curr) ) - else: - curr = [(self.var.export.type.dtp[self.ExpConfigCombox.current()], '*.'),] - fext ='' - self.exportFilePath.set( askdirectory(title="Export datapack (choose the directory to put the datapack)", initialdir=os.path.dirname(self.filePath), mustexist=False) ) - if self.exportFilePath.get(): - if asFile: - if not self.exportFilePath.get().lower().endswith(fext): self.exportFilePath.set( self.exportFilePath.get().split('.')[0] + fext ) - else: - if '.' in self.exportFilePath.get().split('/')[-1]: - self.exportFilePath.set( '/'.join(self.exportFilePath.get().split('/')[0:-1]) ) - - def OnExport(self): - if self.exportFilePath.get() is not None: - self.ExpBrowseButton['state'] = self.ExpSaveButton['state'] = 'disabled' - self.UpdateProgBar(10) - data, path = self.inputFileData, self.exportFilePath.get() - asFile = bool(self.var.export.mode.get()) - type = self.ExpConfigCombox.current() - if asFile: - if type == 0: - exportMIDI(self, path, self.var.export.midi.opt1.get()) - elif type == 1: - with open(path, "w") as f: - f.write(writencf(data)) - elif type in {2, 3, 4, 5}: - fext = self.var.export.type.file[self.ExpConfigCombox.current()][1][2:] - exportMusic(self, path, fext) - else: - if type == 0: - exportDatapack(self, os.path.join(path, self.WnbsIDEntry.get()), 'wnbs') - - #self.UpdateProgBar(100) - self.RaiseFooter('Exported') - self.UpdateProgBar(-1) - - self.ExpBrowseButton['state'] = self.ExpSaveButton['state'] = 'normal' - - def UpdateProgBar(self, value, time=0.001): - if value != self.progressbar["value"]: - if value == -1 or time < 0: - self.progressbar.pack_forget() - self.configure(cursor='arrow') - else: - self.configure(cursor='wait') - self.progressbar["value"] = value - self.progressbar.pack(side='right') - self.progressbar.update() - if time != 0: sleep(time) - - def RaiseFooter(self, text='', color='green', hid=False): - if hid == False: - #self.RaiseFooter(hid=True) - text.replace('\s', ' ') - self.footerLabel.configure(text=text, foreground=color) - self.footerLabel.pack(side='left', fill='x') - self.after(999, lambda: self.RaiseFooter(text=text, color=color, hid=True)) - else: - self.footerLabel.pack_forget() - self.footerLabel.update() + def __init__(self, parent): + tk.Frame.__init__(self, parent) + self.parent = parent + self.properties() + self.elements() + self.WindowBind() + self.toggleExpOptiGrp() + # self.update() + self.pack(fill='both', expand=True) + self.update() + WindowGeo(self.parent, self.parent, 800, 500, 600, 500) + self.lift() + self.focus_force() + self.grab_set() + self.grab_release() + + def properties(self): + self.VERSION = '0.7.0' + self.filePath = None + self.inputFileData = None + self.noteSounds = None + self.last = Attr() + self.last.inputFileData = None + self.var = Attr() + self.PlayingState = 'stop' + self.PlayingTick = -1 + self.SongPlayerAfter = None + self.exportFilePath = tk.StringVar() + + def elements(self): + self.parent.title("NBS Tool") + self.style = ttk.Style() + # self.style.theme_use("default") + try: + self.style.theme_use("vista") + except Exception as e: + print(repr(e), e.__class__.__name__) + try: + self.style.theme_use("winnative") + except Exception: + pass + + # Menu bar + self.menuBar = tk.Menu(self) + self.parent.configure(menu=self.menuBar) + self.menus() + + # Tabs + self.NbTabs = ttk.Notebook(self) + self.tabs() + self.NbTabs.enable_traversal() + self.NbTabs.pack(fill='both', expand=True) + + # Footer + tk.Frame(self, height=5).pack() + + self.footer = tk.Frame( + self, relief='groove', borderwidth=1, height=25, width=self.winfo_width()) + self.footer.pack_propagate(False) + self.footerElements() + self.footer.pack(side='top', fill='x') + + def menus(self): + # 'File' menu + self.fileMenu = tk.Menu(self.menuBar, tearoff=False) + self.menuBar.add_cascade(label="File", menu=self.fileMenu) + + self.fileMenu.add_command( + label="Open", accelerator="Ctrl+O", command=self.OnBrowseFile) + self.fileMenu.add_command( + label="Save", accelerator="Ctrl+S", command=self.OnSaveFile) + self.fileMenu.add_command( + label="Save as new file", accelerator="Ctrl+Shift+S", command=lambda: self.OnSaveFile(True)) + self.fileMenu.add_separator() + self.fileMenu.add_command( + label="Quit", accelerator="Esc", command=self.onClose) + + self.helpMenu = tk.Menu(self.menuBar, tearoff=False) + self.menuBar.add_cascade(label="Help", menu=self.helpMenu) + + self.helpMenu.add_command( + label="About", command=lambda: AboutWindow(self)) + + def tabs(self): + # "General" tab + self.GeneralTab = tk.Frame(self.NbTabs) + + self.GeneralTab.rowconfigure(0) + self.GeneralTab.rowconfigure(1, weight=1) + + self.GeneralTab.columnconfigure(0, weight=1, uniform='a') + self.GeneralTab.columnconfigure(1, weight=1, uniform='a') + + self.GeneralTabElements() + self.NbTabs.add(self.GeneralTab, text="General") + + # "Tools" tab + self.ToolsTab = tk.Frame(self.NbTabs) + + self.ToolsTab.rowconfigure(0, weight=1, uniform='b') + self.ToolsTab.rowconfigure(1, weight=1, uniform='b') + self.ToolsTab.rowconfigure(2) + + self.ToolsTab.columnconfigure(0, weight=1, uniform='b') + self.ToolsTab.columnconfigure(1, weight=1, uniform='b') + + self.ToolsTabElements() + self.NbTabs.add(self.ToolsTab, text="Tools") + + # "Export" tab + self.ExportTab = tk.Frame(self.NbTabs) + + self.ExportTabElements() + self.NbTabs.add(self.ExportTab, text="Export") + + def GeneralTabElements(self): + fpadx, fpady = 10, 10 + padx, pady = 5, 5 + + # File metadata frame + self.FileMetaFrame = tk.LabelFrame(self.GeneralTab, text="Metadata") + self.FileMetaFrame.grid( + row=1, column=0, padx=fpadx, pady=fpady, sticky='nsew') + + self.FileMetaMess = tk.Message( + self.FileMetaFrame, text="No flie was found.") + self.FileMetaMess.pack(fill='both', expand=True, padx=padx, pady=padx) + + # More infomation frame + self.FileInfoFrame = tk.LabelFrame(self.GeneralTab, text="Infomations") + self.FileInfoFrame.grid( + row=1, column=1, padx=fpadx, pady=fpady, sticky='nsew') + + self.FileInfoMess = tk.Message( + self.FileInfoFrame, text="No flie was found.") + self.FileInfoMess.pack(fill='both', expand=True, padx=padx, pady=pady) + + def ToolsTabElements(self): + fpadx, fpady = 10, 10 + padx, pady = 5, 0 + + # Flip tool + self.FlipToolFrame = tk.LabelFrame(self.ToolsTab, text="Flipping") + self.FlipToolFrame.grid( + row=0, column=0, sticky='nsew', padx=fpadx, pady=fpady) + + self.FlipToolMess = tk.Message( + self.FlipToolFrame, anchor='w', text="Flip the note sequence horizontally (by tick), vertically (by layer) or both: ") + self.FlipToolMess.pack(fill='both', expand=True, padx=padx, pady=pady) + + self.var.tool.flip.vertical = tk.IntVar() + self.FilpToolCheckV = tk.Checkbutton( + self.FlipToolFrame, text="Vertically", variable=self.var.tool.flip.vertical) + self.FilpToolCheckV.pack(side='left', padx=padx, pady=pady) + + self.var.tool.flip.horizontal = tk.IntVar() + self.FilpToolCheckH = tk.Checkbutton( + self.FlipToolFrame, text="Horizontally", variable=self.var.tool.flip.horizontal) + self.FilpToolCheckH.pack(side='left', padx=padx, pady=pady) + + # Instrument tool + self.InstToolFrame = tk.LabelFrame( + self.ToolsTab, text="Note's instrument") + self.InstToolFrame.grid( + row=0, column=1, sticky='nsew', padx=fpadx, pady=fpady) + + self.InstToolMess = tk.Message( + self.InstToolFrame, anchor='w', text="Change all note's instrument to:") + self.InstToolMess.pack(fill='both', expand=True, padx=padx, pady=pady) + + self.var.tool.opts = opts = [ + "(not applied)"] + [x['name'] for x in vaniNoteSounds] + ["Random"] + self.InstToolCombox = ttk.Combobox( + self.InstToolFrame, state='readonly', values=opts) + self.InstToolCombox.current(0) + self.InstToolCombox.pack( + side='left', fill='both', expand=True, padx=padx, pady=pady) + + # Reduce tool + self.ReduceToolFrame = tk.LabelFrame(self.ToolsTab, text="Reducing") + self.ReduceToolFrame.grid( + row=1, column=0, sticky='nsew', padx=fpadx, pady=fpady) + + self.ReduceToolMess = tk.Message( + self.ReduceToolFrame, anchor='w', text="Delete as many note as possible to reduce file size.") + self.ReduceToolMess.pack( + fill='both', expand=True, padx=padx, pady=pady) + + self.var.tool.reduce.opt1 = tk.IntVar() + self.CompactToolChkOpt1 = FlexCheckbutton( + self.ReduceToolFrame, text="Delete duplicate notes", variable=self.var.tool.reduce.opt1, anchor='w') + self.CompactToolChkOpt1.pack(padx=padx, pady=pady) + + self.var.tool.reduce.opt2 = tk.IntVar() + self.CompactToolChkOpt2 = FlexCheckbutton( + self.ReduceToolFrame, text=" In every tick, delete all notes except the first note", variable=self.var.tool.reduce.opt2, anchor='w') + self.CompactToolChkOpt2.pack(padx=padx, pady=pady) + + self.var.tool.reduce.opt3 = tk.IntVar() + self.CompactToolChkOpt3 = FlexCheckbutton( + self.ReduceToolFrame, text=" In every tick, delete all notes except the last note", variable=self.var.tool.reduce.opt3, anchor='w') + self.CompactToolChkOpt3.pack(padx=padx, pady=(pady, 10)) + + # Compact tool + self.CompactToolFrame = tk.LabelFrame(self.ToolsTab, text="Compacting") + self.CompactToolFrame.grid( + row=1, column=1, sticky='nsew', padx=fpadx, pady=fpady) + + self.CompactToolMess = tk.Message( + self.CompactToolFrame, anchor='w', text="Remove spaces between notes vertically (by layer) and group them by instruments.") + self.CompactToolMess.pack( + fill='both', expand=True, padx=padx, pady=pady) + + self.var.tool.compact = tk.IntVar() + self.CompactToolCheck = FlexCheckbutton( + self.CompactToolFrame, text="Compact notes", variable=self.var.tool.compact, command=self.toggleCompactToolOpt, anchor='w') + self.CompactToolCheck.pack(padx=padx, pady=pady) + + self.var.tool.compact.opt1 = tk.IntVar() + self.CompactToolChkOpt1 = FlexCheckbutton(self.CompactToolFrame, text="Automatic separate notes by instruments (remain some spaces)", + variable=self.var.tool.compact.opt1, state='disabled', command=lambda: self.toggleCompactToolOpt(2), anchor='w') + self.CompactToolChkOpt1.select() + self.CompactToolChkOpt1.pack(padx=padx*5, pady=pady) + + self.var.tool.compact.opt1_1 = tk.IntVar() + self.CompactToolChkOpt1_1 = FlexCheckbutton( + self.CompactToolFrame, text="Group percussions into one layer", variable=self.var.tool.compact.opt1_1, state='disabled', anchor='w') + self.CompactToolChkOpt1_1.select() + self.CompactToolChkOpt1_1.pack(padx=padx*5*2, pady=pady) + + # 'Apply' botton + self.ToolsTabButton = ttk.Button( + self.ToolsTab, text="Apply", state='disabled', command=self.OnApplyTool) + self.ToolsTabButton.grid( + row=2, column=1, sticky='se', padx=fpadx, pady=fpady) + + def ExportTabElements(self): + fpadx, fpady = 10, 10 + padx, pady = 5, 5 + + # Upper frame + self.ExpConfigFrame = tk.LabelFrame(self.ExportTab, text="Option") + self.ExpConfigFrame.pack( + fill='both', expand=True, padx=fpadx, pady=fpady) + + # "Select mode" frame + self.ExpConfigGrp1 = tk.Frame( + self.ExpConfigFrame, relief='groove', borderwidth=1) + self.ExpConfigGrp1.pack(fill='both', padx=fpadx) + + self.ExpConfigLabel = tk.Label( + self.ExpConfigGrp1, text="Export the song as a:", anchor='w') + self.ExpConfigLabel.pack(side='left', fill='x', padx=padx, pady=pady) + + self.var.export.mode = tk.IntVar() + self.ExpConfigMode1 = tk.Radiobutton( + self.ExpConfigGrp1, text="File", variable=self.var.export.mode, value=1) + self.ExpConfigMode1.pack(side='left', padx=padx, pady=pady) + self.ExpConfigMode1.select() + self.ExpConfigMode2 = tk.Radiobutton( + self.ExpConfigGrp1, text="Datapack", variable=self.var.export.mode, value=0) + self.ExpConfigMode2.pack(side='left', padx=padx, pady=pady) + + self.var.export.type.file = \ + [('Musical Instrument Digital files', '*.mid'), + ('Nokia Composer Format', '*.txt'),] + self.var.export.type.dtp = ['Wireless note block song', 'other'] + self.ExpConfigCombox = ttk.Combobox(self.ExpConfigGrp1, state='readonly', values=[ + "{} ({})".format(tup[0], tup[1]) for tup in self.var.export.type.file]) + self.ExpConfigCombox.current(0) + self.ExpConfigCombox.bind( + "<>", self.toggleExpOptiGrp) + self.ExpConfigCombox.pack(side='left', fill='x', padx=padx, pady=pady) + + self.var.export.mode.trace('w', self.toggleExpOptiGrp) + + ttk.Separator(self.ExpConfigFrame, orient="horizontal").pack( + fill='x', expand=False, padx=padx*3, pady=pady) + + self.ExpOptiSW = StackingWidget( + self.ExpConfigFrame, relief='groove', borderwidth=1) + self.ExpOptiSW.pack(fill='both', expand=True, padx=fpadx) + + # Midi export options frame + self.ExpOptiSW.append(tk.Frame(self.ExpOptiSW), 'Midi') + self.ExpOptiSW.pack('Midi', side='top', fill='both', expand=True) + + self.var.export.midi.opt1 = tk.IntVar() + self.ExpMidi1Rad1 = tk.Radiobutton( + self.ExpOptiSW['Midi'], text="Sort notes to MIDI tracks by note's layer", variable=self.var.export.midi.opt1, value=1) + self.ExpMidi1Rad1.pack(anchor='nw', padx=padx, pady=(pady, 0)) + self.ExpMidi1Rad2 = tk.Radiobutton( + self.ExpOptiSW['Midi'], text="Sort notes to MIDI tracks by note's instrument", variable=self.var.export.midi.opt1, value=0) + self.ExpMidi1Rad2.pack(anchor='nw', padx=padx, pady=(0, pady)) + + # Nokia export options frame + self.ExpOptiSW.append(tk.Frame(self.ExpOptiSW), 'NCF') + self.ExpOptiSW.pack('NCF', side='top', fill='both', expand=True) + + self.NCFOutput = ScrolledText( + self.ExpOptiSW['NCF'], state="disabled", height=10) + self.NCFOutput.pack(fill='both', expand=True) + + # 'Wireless song datapack' export options frame + self.ExpOptiSW.append(tk.Frame(self.ExpOptiSW), 'Wnbs') + self.ExpOptiSW.pack('Wnbs', side='top', fill='both', expand=True) + + self.WnbsIDLabel = tk.Label( + self.ExpOptiSW['Wnbs'], text="Unique name:") + self.WnbsIDLabel.pack(anchor='nw', padx=padx, pady=pady) + + #vcmd = (self.register(self.onValidate), '%P') + self.WnbsIDEntry = tk.Entry(self.ExpOptiSW['Wnbs'], validate="key", + validatecommand=(self.register(lambda P: bool(re.match("^(\d|\w|[-_])*$", P))), '%P')) + self.WnbsIDEntry.pack(anchor='nw', padx=padx, pady=pady) + + # Other export options frame + self.ExpOptiSW.append(tk.Frame(self.ExpOptiSW), 'Other') + self.ExpOptiSW.pack('Other', side='top', fill='both', expand=True) + + self.ExpMusicLabel = tk.Label( + self.ExpOptiSW['Other'], text="There is no option available.") + self.ExpMusicLabel.pack(anchor='nw', padx=padx, pady=pady) + + # Output frame + self.ExpOutputFrame = tk.LabelFrame(self.ExportTab, text="Output") + self.ExpOutputFrame.pack(fill='both', padx=fpadx, pady=(0, fpady)) + + self.ExpOutputLabel = tk.Label( + self.ExpOutputFrame, text="File path:", anchor='w', width=8) + self.ExpOutputLabel.pack(side='left', fill='x', padx=padx, pady=pady) + + self.ExpOutputEntry = tk.Entry( + self.ExpOutputFrame, textvariable=self.exportFilePath) + self.ExpOutputEntry.pack(side='left', fill='x', padx=padx, expand=True) + + self.ExpBrowseButton = ttk.Button( + self.ExpOutputFrame, text="Browse", command=self.OnBrowseExp) + self.ExpBrowseButton.pack(side='left', padx=padx, pady=pady) + + self.ExpSaveButton = ttk.Button( + self.ExpOutputFrame, text="Export", command=self.OnExport) + self.ExpSaveButton.pack(side='left', padx=padx, pady=pady) + + def footerElements(self): + self.footerLabel = tk.Label(self.footer, text="Ready") + self.footerLabel.pack(side='left', fill='x') + self.var.footerLabel = 0 + + self.sizegrip = ttk.Sizegrip(self.footer) + self.sizegrip.pack(side='right', anchor='se') + + self.progressbar = ttk.Progressbar( + self.footer, orient="horizontal", length=300, mode="determinate") + self.progressbar["value"] = 0 + self.progressbar["maximum"] = 100 + # self.progressbar.start() + # self.progressbar.stop() + + def WindowBind(self): + # Keys + self.parent.bind('', self.onClose) + self.parent.bind('', self.OnBrowseFile) + self.parent.bind('', self.OnSaveFile) + self.parent.bind('', lambda _: self.OnSaveFile(True)) + self.parent.bind('', lambda _: self.OnSaveFile(True)) + + # Bind class + self.bind_class("Message", "", + lambda e: e.widget.configure(width=e.width-10)) + + for tkclass in ('TButton', 'Checkbutton', 'Radiobutton'): + self.bind_class(tkclass, '', lambda e: e.widget.event_generate( + '', when='tail')) + + self.bind_class("TCombobox", "", + lambda e: e.widget.event_generate('')) + + for tkclass in ("Entry", "Text", "ScrolledText"): + self.bind_class(tkclass, "", self.popupmenus) + + self.bind_class("TNotebook", "<>", + self._on_tab_changed) + + # Credit: http://code.activestate.com/recipes/580726-tkinter-notebook-that-fits-to-the-height-of-every-/ + def _on_tab_changed(self, event): + event.widget.update_idletasks() + + tab = event.widget.nametowidget(event.widget.select()) + event.widget.configure(height=tab.winfo_reqheight()) + + def popupmenus(self, event): + w = event.widget + self.popupMenu = tk.Menu(self, tearoff=False) + self.popupMenu.add_command( + label="Select all", accelerator="Ctrl+A", command=lambda: w.event_generate("")) + self.popupMenu.add_separator() + self.popupMenu.add_command( + label="Cut", accelerator="Ctrl+X", command=lambda: w.event_generate("")) + self.popupMenu.add_command( + label="Copy", accelerator="Ctrl+C", command=lambda: w.event_generate("")) + self.popupMenu.add_command( + label="Paste", accelerator="Ctrl+V", command=lambda: w.event_generate("")) + self.popupMenu.tk.call("tk_popup", self.popupMenu, + event.x_root, event.y_root) + + def onClose(self, event=None): + self.parent.quit() + self.parent.destroy() + + def OnBrowseFile(self, _): + types = [('Note Block Studio files', '*.nbs'), ('All files', '*')] + filename = askopenfilename(filetypes=types) + if filename != '': + self.OnOpenFile(filename) + + def OnOpenFile(self, fileName): + self.UpdateProgBar(20) + if self.filePath != '': + data = opennbs(fileName) + if data is not None: + self.UpdateProgBar(80) + self.inputFileData = data + self.ExpOutputEntry.delete(0, 'end') + self.exportFilePath.set('') + print(type(data)) + + self.filePath = fileName + self.UpdateVar() + self.parent.title('"{}" – NBS Tool'.format( + fileName.split('/')[-1])) + self.RaiseFooter('Opened') + self.UpdateProgBar(100) + self.UpdateProgBar(-1) + + def OnSaveFile(self, saveAsNewFile=False): + if self.inputFileData is not None: + if saveAsNewFile is True: + types = [('Note Block Studio files', '*.nbs'), + ('All files', '*')] + self.filePath = asksaveasfilename(filetypes=types) + if not self.filePath.lower().endswith('.nbs'): + self.filePath = self.filePath.split('.')[0] + '.nbs' + self.UpdateProgBar(50) + + writenbs(self.filePath, self.inputFileData) + + self.parent.title('"{}" – NBS Tool'.format( + self.filePath.split('/')[-1])) + self.UpdateProgBar(100) + self.RaiseFooter('Saved') + self.UpdateProgBar(-1) + + def toggleCompactToolOpt(self, id=1): + if id <= 2: + a = ((self.var.tool.compact.opt1.get() == 0) + or (self.var.tool.compact.get() == 0)) + self.CompactToolChkOpt1_1["state"] = "disable" if a is True else "normal" + if id <= 1: + self.CompactToolChkOpt1["state"] = "disable" if self.var.tool.compact.get( + ) == 0 else "normal" + + def OnApplyTool(self): + self.ToolsTabButton['state'] = 'disabled' + self.UpdateProgBar(0) + data = self.inputFileData + ticklen = data['headers']['length'] + layerlen = data['maxLayer'] + instOpti = self.InstToolCombox.current() + self.UpdateProgBar(20) + for note in data['notes']: + # Flip + if bool(self.var.tool.flip.horizontal.get()): + note['tick'] = ticklen - note['tick'] + if bool(self.var.tool.flip.vertical.get()): + note['layer'] = layerlen - note['layer'] + + # Instrument change + if instOpti > 0: + note['inst'] = randrange( + len(self.var.tool.opts)-2) if instOpti > len(self.noteSounds) else instOpti-1 + self.UpdateProgBar(30) + # Reduce + if bool(self.var.tool.reduce.opt2.get()) and bool(self.var.tool.reduce.opt3.get()): + data['notes'] = [note for i, note in enumerate( + data['notes']) if note == data['notes'][-1] or note['tick'] != data['notes'][i-1]['tick'] or note['tick'] != data['notes'][i+1]['tick']] + elif bool(self.var.tool.reduce.opt2.get()): + data['notes'] = [note for i, note in enumerate( + data['notes']) if note['tick'] != data['notes'][i-1]['tick']] + elif bool(self.var.tool.reduce.opt3.get()): + data['notes'] = [data['notes'][i-1] + for i, note in enumerate(data['notes']) if note['tick'] != data['notes'][i-1]['tick']] + self.UpdateProgBar(60) + if bool(self.var.tool.reduce.opt1.get()): + data['notes'] = sorted(data['notes'], key=operator.itemgetter( + 'tick', 'inst', 'key', 'layer')) + data['notes'] = [note for i, note in enumerate(data['notes']) if note['tick'] != data['notes'][i-1] + ['tick'] or note['inst'] != data['notes'][i-1]['inst'] or note['key'] != data['notes'][i-1]['key']] + data['notes'] = sorted( + data['notes'], key=operator.itemgetter('tick', 'layer')) + + self.UpdateProgBar(50) + # Compact + if bool(self.var.tool.compact.get()): + data = compactNotes( + data, self.var.tool.compact.opt1.get(), self.var.tool.compact.opt1_1.get()) + self.UpdateProgBar(60) + # Sort notes + data['notes'] = sorted( + data['notes'], key=operator.itemgetter('tick', 'layer')) + + self.UpdateProgBar(60) + data = DataPostprocess(data) + + self.UpdateProgBar(90) + self.UpdateVar() + # self.UpdateProgBar(100) + self.RaiseFooter('Applied') + self.UpdateProgBar(-1) + self.ToolsTabButton['state'] = 'normal' + + def UpdateVar(self): + #print("Started updating….") + data = self.inputFileData + if data is not None: + self.ToolsTabButton['state'] = 'normal' + self.ExpSaveButton['state'] = 'normal' + if data != self.last.inputFileData: + self.UpdateProgBar(10) + self.parent.title( + '*"{}" – NBS Tool'.format(self.filePath.split('/')[-1])) + self.UpdateProgBar(20) + headers = data['headers'] + self.UpdateProgBar(30) + text = "File loaded" + self.FileMetaMess.configure(text=text) + self.UpdateProgBar(40) + text = "File loaded" + self.FileInfoMess.configure(text=text) + self.UpdateProgBar(50) + customInsts = [{'name': item['name'], 'filepath': resource_path('sounds', item['filename']), 'obj': AudioSegment.from_ogg( + resource_path('sounds', item['filename'])), 'pitch': item['pitch']} for item in data['customInsts']] + self.noteSounds = vaniNoteSounds + customInsts + self.UpdateProgBar(70) + self.var.tool.opts = opts = [ + "(not applied)"] + [x['name'] for x in self.noteSounds] + ["Random"] + self.InstToolCombox.configure(values=opts) + self.UpdateProgBar(80) + if data['maxLayer'] == 0: + text = writencf(data) + else: + text = "The song must have only 1 layer in order to export as Nokia Composer Format." + self.NCFOutput.configure(state='normal') + self.NCFOutput.delete('1.0', 'end') + self.NCFOutput.insert('end', text) + self.NCFOutput.configure(state='disabled') + self.UpdateProgBar(100) + self.last.inputFileData = copy.deepcopy(data) + self.RaiseFooter('Updated') + #print("Updated class properties…", data == self.last.inputFileData) + + self.UpdateProgBar(-1) + else: + self.ToolsTabButton['state'] = 'disabled' + self.ExpSaveButton['state'] = 'disabled' + + self.update_idletasks() + + def toggleExpOptiGrp(self, n=None, m=None, y=None): + asFile = bool(self.var.export.mode.get()) + key = max(0, self.ExpConfigCombox.current()) + if asFile: + if key == 0: + self.ExpOptiSW.switch('Midi') + elif key == 1: + self.ExpOptiSW.switch('NCF') + else: + self.ExpOptiSW.switch('Other') + self.ExpConfigCombox.configure(values=["{} ({})".format( + tup[0], tup[1]) for tup in self.var.export.type.file]) + else: + if key == 0: + self.ExpOptiSW.switch('Wnbs') + else: + self.ExpOptiSW.switch('Other') + self.ExpConfigCombox.configure(values=["{} ({})".format(tup[0], tup[1]) if isinstance( + tup, tuple) else tup for tup in self.var.export.type.dtp]) + key = min(key, len(self.ExpConfigCombox['values'])-1) + self.ExpConfigCombox.current(key) + self.ExpConfigCombox.configure(width=len(self.ExpConfigCombox.get())) + + if self.exportFilePath.get(): + print(self.exportFilePath.get()) + fext = (self.var.export.type.file[self.ExpConfigCombox.current()],)[ + 0][1][1:] + if asFile: + if not self.exportFilePath.get().lower().endswith(self.WnbsIDEntry.get()): + self.exportFilePath.set( + "{}/{}".format(self.exportFilePath.get(), self.WnbsIDEntry.get())) + self.WnbsIDEntry.delete(0, 'end') + if not self.exportFilePath.get().lower().endswith(fext): + self.exportFilePath.set( + self.exportFilePath.get().split('.')[0] + fext) + else: + if '.' in self.exportFilePath.get(): + self.WnbsIDEntry.delete(0, 'end') + self.WnbsIDEntry.insert( + 0, self.exportFilePath.get().split('/')[-1].split('.')[0]) + self.exportFilePath.set( + '/'.join(self.exportFilePath.get().split('/')[0:-1])) + + def OnBrowseExp(self): + if self.filePath and self.inputFileData: + asFile = bool(self.var.export.mode.get()) + if asFile: + curr = ( + self.var.export.type.file[self.ExpConfigCombox.current()],) + fext = curr[0][1][1:] + self.exportFilePath.set(asksaveasfilename(title="Export file", initialfile=os.path.splitext( + os.path.basename(self.filePath))[0]+fext, filetypes=curr)) + else: + curr = [ + (self.var.export.type.dtp[self.ExpConfigCombox.current()], '*.'), ] + fext = '' + self.exportFilePath.set(askdirectory( + title="Export datapack (choose the directory to put the datapack)", initialdir=os.path.dirname(self.filePath), mustexist=False)) + if self.exportFilePath.get(): + if asFile: + if not self.exportFilePath.get().lower().endswith(fext): + self.exportFilePath.set( + self.exportFilePath.get().split('.')[0] + fext) + else: + if '.' in self.exportFilePath.get().split('/')[-1]: + self.exportFilePath.set( + '/'.join(self.exportFilePath.get().split('/')[0:-1])) + + def OnExport(self): + if self.exportFilePath.get() is not None: + self.ExpBrowseButton['state'] = self.ExpSaveButton['state'] = 'disabled' + self.UpdateProgBar(10) + data, path = self.inputFileData, self.exportFilePath.get() + asFile = bool(self.var.export.mode.get()) + type = self.ExpConfigCombox.current() + if asFile: + if type == 0: + exportMIDI(self, path, self.var.export.midi.opt1.get()) + elif type == 1: + with open(path, "w") as f: + f.write(writencf(data)) + elif type in {2, 3, 4, 5}: + fext = self.var.export.type.file[self.ExpConfigCombox.current( + )][1][2:] + # exportMusic(self, path, fext) + else: + if type == 0: + exportDatapack(self, os.path.join( + path, self.WnbsIDEntry.get()), 'wnbs') + + # self.UpdateProgBar(100) + self.RaiseFooter('Exported') + self.UpdateProgBar(-1) + + self.ExpBrowseButton['state'] = self.ExpSaveButton['state'] = 'normal' + + def UpdateProgBar(self, value, time=0.001): + if value != self.progressbar["value"]: + if value == -1 or time < 0: + self.progressbar.pack_forget() + self.configure(cursor='arrow') + else: + self.configure(cursor='wait') + self.progressbar["value"] = value + self.progressbar.pack(side='right') + self.progressbar.update() + if time != 0: + sleep(time) + + def RaiseFooter(self, text='', color='green', hid=False): + if hid == False: + # self.RaiseFooter(hid=True) + text.replace('\s', ' ') + self.footerLabel.configure(text=text, foreground=color) + self.footerLabel.pack(side='left', fill='x') + self.after(999, lambda: self.RaiseFooter( + text=text, color=color, hid=True)) + else: + self.footerLabel.pack_forget() + self.footerLabel.update() class AboutWindow(tk.Toplevel): - def __init__(self, parent): - tk.Toplevel.__init__(self) - self.parent = parent - self.title("About this application...") + def __init__(self, parent): + tk.Toplevel.__init__(self) + self.parent = parent + self.title("About this application...") - self.logo = ImageTk.PhotoImage(Image.open(resource_path('icon.ico')).resize((128 ,128), Image.ANTIALIAS)) + self.logo = ImageTk.PhotoImage(Image.open(resource_path( + 'icon.ico')).resize((128, 128), Image.ANTIALIAS)) - logolabel = tk.Label(self, text="NBSTool", font=("Arial", 44), image=self.logo, compound='left') - logolabel.pack(padx=30, pady=(10*2, 10)) + logolabel = tk.Label(self, text="NBSTool", font=( + "Arial", 44), image=self.logo, compound='left') + logolabel.pack(padx=30, pady=(10*2, 10)) - description = tk.Message(self, text="A tool to work with .nbs (Note Block Studio) files.\nAuthor: IoeCmcomc\nVersion: {}".format(parent.VERSION), justify='center') - description.pack(fill='both', expand=False, padx=10, pady=10) + description = tk.Message(self, text="A tool to work with .nbs (Note Block Studio) files.\nAuthor: IoeCmcomc\nVersion: {}".format( + parent.VERSION), justify='center') + description.pack(fill='both', expand=False, padx=10, pady=10) - githubLink = ttk.Button(self, text='GitHub', command= lambda: webbrowser.open("https://github.com/IoeCmcomc/NBSTool",new=True)) - githubLink.pack(padx=10, pady=10) + githubLink = ttk.Button(self, text='GitHub', command=lambda: webbrowser.open( + "https://github.com/IoeCmcomc/NBSTool", new=True)) + githubLink.pack(padx=10, pady=10) - self.lift() - self.focus_force() - self.grab_set() - #self.grab_release() + self.lift() + self.focus_force() + self.grab_set() + # self.grab_release() - self.resizable(False, False) - self.transient(self.parent) + self.resizable(False, False) + self.transient(self.parent) - self.bind("", self.Alarm) - self.bind('', lambda _: self.destroy()) + self.bind("", self.Alarm) + self.bind('', lambda _: self.destroy()) - self.iconbitmap(resource_path("icon.ico")) + self.iconbitmap(resource_path("icon.ico")) - WindowGeo(self, parent, 500, 300) + WindowGeo(self, parent, 500, 300) + + def Alarm(self, event): + self.focus_force() + self.bell() - def Alarm(self, event): - self.focus_force() - self.bell() class FlexCheckbutton(tk.Checkbutton): - def __init__(self, *args, **kwargs): - okwargs = dict(kwargs) - if 'multiline' in kwargs: - self.multiline = kwargs['multiline'] - del okwargs['multiline'] - else: - self.multiline = True - - tk.Checkbutton.__init__(self, *args, **okwargs) - - self.text = kwargs['text'] - if 'anchor' in kwargs: - self.anchor = kwargs['anchor'] - else: - self.anchor = 'w' - self['anchor'] = self.anchor - - if 'justify' in kwargs: - self.justify = kwargs['justify'] - else: self.justify = 'left' - self['justify'] = self.justify - - if self.multiline: - self.bind("", lambda event: self.configure(width=event.width-10, justify=self.justify, anchor=self.anchor, wraplength=event.width-20, text=self.text+' '*999) ) -#Currently unused + def __init__(self, *args, **kwargs): + okwargs = dict(kwargs) + if 'multiline' in kwargs: + self.multiline = kwargs['multiline'] + del okwargs['multiline'] + else: + self.multiline = True + + tk.Checkbutton.__init__(self, *args, **okwargs) + + self.text = kwargs['text'] + if 'anchor' in kwargs: + self.anchor = kwargs['anchor'] + else: + self.anchor = 'w' + self['anchor'] = self.anchor + + if 'justify' in kwargs: + self.justify = kwargs['justify'] + else: + self.justify = 'left' + self['justify'] = self.justify + + if self.multiline: + self.bind("", lambda event: self.configure(width=event.width-10, + justify=self.justify, anchor=self.anchor, wraplength=event.width-20, text=self.text+' '*999)) +# Currently unused + class SquareButton(tk.Button): - def __init__(self, *args, **kwargs): - self.blankImg = tk.PhotoImage() + def __init__(self, *args, **kwargs): + self.blankImg = tk.PhotoImage() + + if "size" in kwargs: + # print(kwargs['size']) + self.size = kwargs['size'] + del kwargs['size'] + else: + self.size = 30 - if "size" in kwargs: - #print(kwargs['size']) - self.size = kwargs['size'] - del kwargs['size'] - else: - self.size = 30 + pprint(kwargs) - pprint(kwargs) + tk.Button.__init__(self, *args, **kwargs) - tk.Button.__init__(self, *args, **kwargs) + self.configure(image=self.blankImg, font=("Arial", self.size-3), + width=self.size, height=self.size, compound=tk.CENTER) - self.configure(image=self.blankImg, font=("Arial", self.size-3), width=self.size, height=self.size, compound=tk.CENTER) class StackingWidget(tk.Frame): - def __init__(self, parent, **kwargs): - super().__init__(parent, **kwargs) - self._frames = {} - self._i = 0 - #self._shown = None - def __getitem__(self, key): - if key in self._frames: return self._frames[key][0] - super().__getitem__(key) - def append(self, frame, key=None): - if isinstance(frame, (tk.Widget, ttk.Widget)): - if not key: - key = self._i - self._i += 1 - self._frames[key] = [frame, None] - #self._shown = key - #return self._frames[name] - def switch(self, key): - for k, (w, o) in self._frames.items(): - if k == key: - if o: w.pack(**o) - else: w.pack() - else: w.pack_forget() - def pack(self, key=None, **opts): - if key: - self._frames[key][1] = opts - #self._frames[key][0].pack(**opts) - else: super().pack(**opts) - if len(self._frames) == 1: - self.switch(key) + def __init__(self, parent, **kwargs): + super().__init__(parent, **kwargs) + self._frames = {} + self._i = 0 + #self._shown = None + + def __getitem__(self, key): + if key in self._frames: + return self._frames[key][0] + super().__getitem__(key) + + def append(self, frame, key=None): + if isinstance(frame, (tk.Widget, ttk.Widget)): + if not key: + key = self._i + self._i += 1 + self._frames[key] = [frame, None] + #self._shown = key + # return self._frames[name] + + def switch(self, key): + for k, (w, o) in self._frames.items(): + if k == key: + if o: + w.pack(**o) + else: + w.pack() + else: + w.pack_forget() + + def pack(self, key=None, **opts): + if key: + self._frames[key][1] = opts + # self._frames[key][0].pack(**opts) + else: + super().pack(**opts) + if len(self._frames) == 1: + self.switch(key) def WindowGeo(obj, parent, width, height, mwidth=None, mheight=None): - ScreenWidth = root.winfo_screenwidth() - ScreenHeight = root.winfo_screenheight() - - WindowWidth = width or obj.winfo_reqwidth() - WindowHeight = height or obj.winfo_reqheight() - - WinPosX = int(ScreenWidth / 2 - WindowWidth / 2) - WinPosY = int(ScreenHeight / 2.3 - WindowHeight / 2) + ScreenWidth = root.winfo_screenwidth() + ScreenHeight = root.winfo_screenheight() - obj.minsize(mwidth or obj.winfo_width(), mheight or obj.winfo_height()) - obj.geometry("{}x{}+{}+{}".format(WindowWidth, WindowHeight, WinPosX, WinPosY)) - #obj.update() - obj.update_idletasks() + WindowWidth = width or obj.winfo_reqwidth() + WindowHeight = height or obj.winfo_reqheight() + + WinPosX = int(ScreenWidth / 2 - WindowWidth / 2) + WinPosY = int(ScreenHeight / 2.3 - WindowHeight / 2) + + obj.minsize(mwidth or obj.winfo_width(), mheight or obj.winfo_height()) + obj.geometry("{}x{}+{}+{}".format(WindowWidth, + WindowHeight, WinPosX, WinPosY)) + # obj.update() + obj.update_idletasks() def compactNotes(data, sepInst=1, groupPerc=1): - sepInst, groupPerc = bool(sepInst), bool(groupPerc) - r = data - PrevNote = {'layer':-1, 'tick':-1} - if sepInst: - OuterLayer = 0 - iter = r['usedInsts'][0] - if not groupPerc: iter += r['usedInsts'][1] - for inst in iter: - #print('Instrument: {}'.format(inst)) - InnerLayer = LocalLayer = c = 0 - #print('OuterLayer: {}; Innerlayer: {}; LocalLayer: {}; c: {}'.format(OuterLayer, InnerLayer, LocalLayer, c)) - for note in r['notes']: - if note['inst'] == inst: - c += 1 - if note['tick'] == PrevNote['tick']: - LocalLayer += 1 - InnerLayer = max(InnerLayer, LocalLayer) - note['layer'] = LocalLayer + OuterLayer - else: - LocalLayer = 0 - note['layer'] = LocalLayer + OuterLayer - PrevNote = note - #print('OuterLayer: {}; Innerlayer: {}; LocalLayer: {}; c: {}'.format(OuterLayer, InnerLayer, LocalLayer, c)) - OuterLayer += InnerLayer + 1 - #print('OuterLayer: {}; Innerlayer: {}; LocalLayer: {}; c: {}'.format(OuterLayer, InnerLayer, LocalLayer, c)) - if groupPerc: - InnerLayer = LocalLayer = c = 0 - for note in r['notes']: - if note['inst'] in r['usedInsts'][1]: - c += 1 - if note['tick'] == PrevNote['tick']: - LocalLayer += 1 - InnerLayer = max(InnerLayer, LocalLayer) - note['layer'] = LocalLayer + OuterLayer - else: - LocalLayer = 0 - note['layer'] = LocalLayer + OuterLayer - PrevNote = note - OuterLayer += InnerLayer + 1 - r['maxLayer'] = OuterLayer - 1 - else: - layer = 0 - for note in r['notes']: - if note['tick'] == PrevNote['tick']: - layer += 1 - note['layer'] = layer - else: - layer = 0 - note['layer'] = layer - PrevNote = note - return r + sepInst, groupPerc = bool(sepInst), bool(groupPerc) + r = data + PrevNote = {'layer': -1, 'tick': -1} + if sepInst: + OuterLayer = 0 + iter = r['usedInsts'][0] + if not groupPerc: + iter += r['usedInsts'][1] + for inst in iter: + #print('Instrument: {}'.format(inst)) + InnerLayer = LocalLayer = c = 0 + #print('OuterLayer: {}; Innerlayer: {}; LocalLayer: {}; c: {}'.format(OuterLayer, InnerLayer, LocalLayer, c)) + for note in r['notes']: + if note['inst'] == inst: + c += 1 + if note['tick'] == PrevNote['tick']: + LocalLayer += 1 + InnerLayer = max(InnerLayer, LocalLayer) + note['layer'] = LocalLayer + OuterLayer + else: + LocalLayer = 0 + note['layer'] = LocalLayer + OuterLayer + PrevNote = note + #print('OuterLayer: {}; Innerlayer: {}; LocalLayer: {}; c: {}'.format(OuterLayer, InnerLayer, LocalLayer, c)) + OuterLayer += InnerLayer + 1 + #print('OuterLayer: {}; Innerlayer: {}; LocalLayer: {}; c: {}'.format(OuterLayer, InnerLayer, LocalLayer, c)) + if groupPerc: + InnerLayer = LocalLayer = c = 0 + for note in r['notes']: + if note['inst'] in r['usedInsts'][1]: + c += 1 + if note['tick'] == PrevNote['tick']: + LocalLayer += 1 + InnerLayer = max(InnerLayer, LocalLayer) + note['layer'] = LocalLayer + OuterLayer + else: + LocalLayer = 0 + note['layer'] = LocalLayer + OuterLayer + PrevNote = note + OuterLayer += InnerLayer + 1 + r['maxLayer'] = OuterLayer - 1 + else: + layer = 0 + for note in r['notes']: + if note['tick'] == PrevNote['tick']: + layer += 1 + note['layer'] = layer + else: + layer = 0 + note['layer'] = layer + PrevNote = note + return r -def exportMIDI(cls, path, byLayer=False): - data = copy.deepcopy(cls.inputFileData) - byLayer = bool(byLayer) - - if not byLayer: - data = DataPostprocess(data) - data = compactNotes(data) - - UniqInstEachLayer = {} - for note in data['notes']: - if note['layer'] not in UniqInstEachLayer: - if note['isPerc']: UniqInstEachLayer[note['layer']] = -1 - else: UniqInstEachLayer[note['layer']] = note['inst'] - else: - if not note['isPerc']: note['inst'] = UniqInstEachLayer[note['layer']] - - lenTrack = data['maxLayer'] + 1 - for i in range(lenTrack): - if i not in UniqInstEachLayer: UniqInstEachLayer[i] = 0 - - main_score = m21s.Score() - - percussions = ( - #(percussion_key, instrument, key) - (35, 2, 64), - (36, 2, 60), - (37, 4, 60), - (38, 3, 62), - #(39, 4, 60), - (40, 3, 58), - #(41, 2, 60), - (42, 3, 76), - (43, 2, 67), - #(44, 3, 76), - (45, 2, 69), - (46, 2, 72), - (47, 2, 74), - (48, 2, 77), - (49, 2, 71), - (50, 3, 77), - (51, 3, 78), - (52, 3, 62), - (53, 3, 67), - (54, 3, 72), - (55, 3, 73), - (56, 4, 55), - #(57, 3, 67), - (58, 4, 56), - #(59, 3, 67), - (60, 4, 63), - (61, 4, 57), - (62, 4, 62), - (63, 2, 76), - (64, 3, 69), - #(65, 3, 67), - #(66, 3, 62), - #(67, 4, 62), - (68, 4, 58), - (69, 4, 74), - (70, 4, 77), - (73, 3, 71), - (74, 4, 65), - (75, 4, 72), - (76, 4, 64), - (77, 4, 59), - (80, 4, 71), - (81, 4, 76), - (82, 3, 78) - ) - - instrument_codes = {-1:m21i.Percussion, - 0: m21i.Harp, - 1: m21i.AcousticBass, - 5: m21i.Guitar, - 6: m21i.Flute, - 7: m21i.Handbells, - 8: m21i.ChurchBells, - 9: m21i.Xylophone, - 10: m21i.Xylophone, - 11: m21i.Piano, - 12: m21i.Piano, - 13: m21i.Piano, - 14: m21i.Banjo, - 15: m21i.Piano, - } - - timeSign = data['headers']['time_sign'] - time = 0 - tempo = data['headers']['tempo'] * 60 / timeSign - volume = 127 - - c = 0 - for i in range(lenTrack): - staff = m21s.Part() - staff.append(m21.tempo.MetronomeMark(number=tempo)) #Tempo - staff.timeSignature = m21.meter.TimeSignature('{}/4'.format(timeSign)) #Time signature - staff.clef = m21.clef.TrebleClef() #Clef - try: - staff.append(instrument_codes[UniqInstEachLayer[i]]()) - except KeyError: - staff.append(m21i.Piano()) - main_score.append(staff) - - for i, note in enumerate(data['notes']): - time = note['tick'] / timeSign - pitch = note['key'] + 21 - duration = 2 if note['duration'] == 0 else note['duration'] / timeSign - track = note['layer'] - - if note['isPerc']: - for a, b, c in percussions: - if c == pitch and b == note['inst']: pitch = a - - if byLayer: - volume = int(data['layers'][note['layer']]['volume'] / 100 * 127) - - #print("track: {}, channel: {}, pitch: {}, time: {}, duration: {}, volume: {}".format(track, channel, pitch, time, duration, volume)) - - a_note = m21.note.Note() - a_note.pitch.midi = pitch #Pitch - a_note.duration.quarterLength = 1 / 4 #Duration - a_note.volume = volume - main_score[track].append(a_note) - a_note.offset = time - - cls.UpdateProgBar(10 + int( note['tick'] / (data['headers']['length']+1) * 80), 0) - - mt = m21.metadata.Metadata() - mt.title = mt.popularTitle = 'Title' - mt.composer = 'Composer' - main_score.insert(0, mt) - - #fn = main_score.write("midi", path) - - mid = m21.midi.translate.streamToMidiFile(main_score) - - if data['hasPerc']: - for i in range(lenTrack): - if UniqInstEachLayer[i] == -1: - for el in mid.tracks[i].events: - el.channel = 10 - - cls.UpdateProgBar(95) - - mid.open(path, 'wb') - mid.write() - mid.close() - - #exper_s = m21.midi.translate.midiFileToStream(mid) - #exper_s.write("midi", path+'_test.mid') - -def exportMusic(cls, path, ext): - start = time() - noteSounds = cls.noteSounds - notes = cls.inputFileData['notes'] - headers = cls.inputFileData['headers'] - tempo = headers['tempo'] - length = headers['length']+1 - layers = cls.inputFileData['layers'] - tickIndexes = cls.inputFileData['indexByTick'] - toUnsigned = lambda x: 256 + x if x < 0 else x - tickSoundLength = max(len(x['obj']) for x in noteSounds) - music = AudioSegment.silent(duration=int(length / tempo * 1000 + 500)) - lastInst = -1 - - for currtick in range(length): - currNotes = tickIndexes[currtick][1] - - for n, i in enumerate(currNotes): - note = notes[i] - currLayer = layers[note['layer']] - inst = note['inst'] - - if inst != lastInst: - currNoteSound = noteSounds[inst] - noteSoundObj = currNoteSound['obj'] - - ANoteSound = noteSoundObj._spawn(noteSoundObj.raw_data, overrides={'frame_rate': int(noteSoundObj.frame_rate * (2.0 ** ((note['key'] - currNoteSound['pitch']) / 12))) }).set_frame_rate(44100) - - if 0 < currLayer['volume'] < 100: ANoteSound = ANoteSound.apply_gain(noteSoundObj.dBFS - noteSoundObj.dBFS * (currLayer['volume'] / 100)) + 3 - - if currLayer['stereo'] != 100: ANoteSound = ANoteSound.pan((toUnsigned(currLayer['stereo']) - 100) / 100) - - if len(currNotes) == 1: tickSound = ANoteSound - elif n == 0: tickSound = ANoteSound + AudioSegment.silent(duration=tickSoundLength - len(ANoteSound)) - else: tickSound = tickSound.overlay(ANoteSound) - - lastInst = note['inst'] - - if len(currNotes) > 0: music = music.overlay(tickSound.set_frame_rate(44100), position=int(note['tick'] / tempo * 1000)) - - cls.UpdateProgBar(10 + int(note['tick'] / length * 80), 0) - #print('Processing {}/{} tick. Time: {:3f}. Done in {:3f} seconds.'.format(note['tick'], length, time() - start, time() - lstart)) - - - meta = {'album': '', - 'artist': headers['author'], - 'comment': headers['description'], - 'date': str(date.today().year), - 'genre': 'Minecraft note block', - 'title': headers['name'], - 'track': ''} - - #meta = {'genre': 'Minecraft note block'} - - cls.UpdateProgBar(95) - music.export(path, format=ext, tags=meta) - print("Exported in {:3f} seconds.".format(time() - start)) +def exportMIDI(cls, path, byLayer=False): + data = copy.deepcopy(cls.inputFileData) + byLayer = bool(byLayer) + + if not byLayer: + data = DataPostprocess(data) + data = compactNotes(data) + + UniqInstEachLayer = {} + for note in data['notes']: + if note['layer'] not in UniqInstEachLayer: + if note['isPerc']: + UniqInstEachLayer[note['layer']] = -1 + else: + UniqInstEachLayer[note['layer']] = note['inst'] + else: + if not note['isPerc']: + note['inst'] = UniqInstEachLayer[note['layer']] + + lenTrack = data['maxLayer'] + 1 + for i in range(lenTrack): + if i not in UniqInstEachLayer: + UniqInstEachLayer[i] = 0 + + main_score = m21s.Score() + + percussions = ( + #(percussion_key, instrument, key) + (35, 2, 64), + (36, 2, 60), + (37, 4, 60), + (38, 3, 62), + #(39, 4, 60), + (40, 3, 58), + #(41, 2, 60), + (42, 3, 76), + (43, 2, 67), + #(44, 3, 76), + (45, 2, 69), + (46, 2, 72), + (47, 2, 74), + (48, 2, 77), + (49, 2, 71), + (50, 3, 77), + (51, 3, 78), + (52, 3, 62), + (53, 3, 67), + (54, 3, 72), + (55, 3, 73), + (56, 4, 55), + #(57, 3, 67), + (58, 4, 56), + #(59, 3, 67), + (60, 4, 63), + (61, 4, 57), + (62, 4, 62), + (63, 2, 76), + (64, 3, 69), + #(65, 3, 67), + #(66, 3, 62), + #(67, 4, 62), + (68, 4, 58), + (69, 4, 74), + (70, 4, 77), + (73, 3, 71), + (74, 4, 65), + (75, 4, 72), + (76, 4, 64), + (77, 4, 59), + (80, 4, 71), + (81, 4, 76), + (82, 3, 78) + ) + + instrument_codes = {-1: m21i.Percussion, + 0: m21i.Harp, + 1: m21i.AcousticBass, + 5: m21i.Guitar, + 6: m21i.Flute, + 7: m21i.Handbells, + 8: m21i.ChurchBells, + 9: m21i.Xylophone, + 10: m21i.Xylophone, + 11: m21i.Piano, + 12: m21i.Piano, + 13: m21i.Piano, + 14: m21i.Banjo, + 15: m21i.Piano, + } + + timeSign = data['headers']['time_sign'] + time = 0 + tempo = data['headers']['tempo'] * 60 / timeSign + volume = 127 + + c = 0 + for i in range(lenTrack): + staff = m21s.Part() + staff.append(m21.tempo.MetronomeMark(number=tempo)) # Tempo + staff.timeSignature = m21.meter.TimeSignature( + '{}/4'.format(timeSign)) # Time signature + staff.clef = m21.clef.TrebleClef() # Clef + try: + staff.append(instrument_codes[UniqInstEachLayer[i]]()) + except KeyError: + staff.append(m21i.Piano()) + main_score.append(staff) + + for i, note in enumerate(data['notes']): + time = note['tick'] / timeSign + pitch = note['key'] + 21 + track = note['layer'] + + if note['isPerc']: + for a, b, c in percussions: + if c == pitch and b == note['inst']: + pitch = a + + if byLayer: + volume = int(data['layers'][note['layer']]['volume'] / 100 * 127) + + #print("track: {}, channel: {}, pitch: {}, time: {}, duration: {}, volume: {}".format(track, channel, pitch, time, duration, volume)) + + a_note = m21.note.Note() + a_note.pitch.midi = pitch # Pitch + a_note.duration.quarterLength = 1 / 4 # Duration + a_note.volume = volume + main_score[track].append(a_note) + a_note.offset = time + + cls.UpdateProgBar( + 10 + int(note['tick'] / (data['headers']['length']+1) * 80), 0) + + mt = m21.metadata.Metadata() + mt.title = mt.popularTitle = 'Title' + mt.composer = 'Composer' + main_score.insert(0, mt) + + #fn = main_score.write("midi", path) + + mid = m21.midi.translate.streamToMidiFile(main_score) + + if data['hasPerc']: + for i in range(lenTrack): + if UniqInstEachLayer[i] == -1: + for el in mid.tracks[i].events: + el.channel = 10 + + cls.UpdateProgBar(95) + + mid.open(path, 'wb') + mid.write() + mid.close() + + #exper_s = m21.midi.translate.midiFileToStream(mid) + #exper_s.write("midi", path+'_test.mid') def exportDatapack(cls, path, mode='none'): - def writejson(path, jsout): - with open(path, 'w') as f: - json.dump(jsout, f, ensure_ascii=False) - def writemcfunction(path, text): - with open(path, 'w') as f: - f.write(text) - - def makeFolderTree(inp, a=[]): - print(a) - if isinstance(inp, (tuple, set)): - for el in inp: - makeFolderTree(el, copy.copy(a)) - elif isinstance(inp, dict): - for k, v in inp.items(): - makeFolderTree(v, a + [k]) - elif isinstance(inp, str): - p = os.path.join(*a, inp) - #print(p) - os.makedirs(p, exist_ok=True) - else: - return - - def wnbs(): - scoreObj = "wnbs_" + bname[:7] - speed = int(min(data['headers']['tempo'] * 4, 120)) - length = data['headers']['length'] - - # os.path.exists() - - makeFolderTree( - {path:{ - 'data':{ - bname:{ - 'functions':{ - 'notes', - 'tree', - }, - }, - 'minecraft':{ - 'tags':'functions', - }, - }, - }, - } - ) - - writejson(os.path.join(path, 'pack.mcmeta'), {"pack":{"description":"Note block song made with NBSTool.", "pack_format":1}} ) - writejson(os.path.join(path, 'data', 'minecraft', 'tags', 'functions', 'load.json'), jsout = {"values":["{}:load".format(bname)]} ) - writejson(os.path.join(path, 'data', 'minecraft', 'tags', 'functions', 'tick.json'), jsout = {"values":["{}:tick".format(bname)]} ) - - writemcfunction(os.path.join(path, 'data', bname, 'functions', 'load.mcfunction'), -"""scoreboard objectives add {0} dummy + def writejson(path, jsout): + with open(path, 'w') as f: + json.dump(jsout, f, ensure_ascii=False) + + def writemcfunction(path, text): + with open(path, 'w') as f: + f.write(text) + + def makeFolderTree(inp, a=[]): + print(a) + if isinstance(inp, (tuple, set)): + for el in inp: + makeFolderTree(el, copy.copy(a)) + elif isinstance(inp, dict): + for k, v in inp.items(): + makeFolderTree(v, a + [k]) + elif isinstance(inp, str): + p = os.path.join(*a, inp) + # print(p) + os.makedirs(p, exist_ok=True) + else: + return + + def wnbs(): + scoreObj = "wnbs_" + bname[:7] + speed = int(min(data['headers']['tempo'] * 4, 120)) + length = data['headers']['length'] + + # os.path.exists() + + makeFolderTree( + {path: { + 'data': { + bname: { + 'functions': { + 'notes', + 'tree', + }, + }, + 'minecraft': { + 'tags': 'functions', + }, + }, + }, + } + ) + + writejson(os.path.join(path, 'pack.mcmeta'), {"pack": { + "description": "Note block song made with NBSTool.", "pack_format": 1}}) + writejson(os.path.join(path, 'data', 'minecraft', 'tags', 'functions', + 'load.json'), jsout={"values": ["{}:load".format(bname)]}) + writejson(os.path.join(path, 'data', 'minecraft', 'tags', 'functions', + 'tick.json'), jsout={"values": ["{}:tick".format(bname)]}) + + writemcfunction(os.path.join(path, 'data', bname, 'functions', 'load.mcfunction'), + """scoreboard objectives add {0} dummy scoreboard objectives add {0}_t dummy -scoreboard players set speed {0} {1}""".format(scoreObj, speed) ) - writemcfunction(os.path.join(path, 'data', bname, 'functions', 'tick.mcfunction'), -"""execute as @a[tag={0}] run scoreboard players operation @s {0} += speed {0} +scoreboard players set speed {0} {1}""".format(scoreObj, speed)) + writemcfunction(os.path.join(path, 'data', bname, 'functions', 'tick.mcfunction'), + """execute as @a[tag={0}] run scoreboard players operation @s {0} += speed {0} execute as @a[tag={0}] run function {1}:tree/0_{2} -execute as @e[type=armor_stand, tag=WNBS_Marker] at @s unless block ~ ~-1 ~ minecraft:note_block run kill @s""".format(scoreObj, bname, 2**(floor(log2(length))+1)-1) ) - writemcfunction(os.path.join(path, 'data', bname, 'functions', 'play.mcfunction'), -"""tag @s add {0} +execute as @e[type=armor_stand, tag=WNBS_Marker] at @s unless block ~ ~-1 ~ minecraft:note_block run kill @s""".format(scoreObj, bname, 2**(floor(log2(length))+1)-1)) + writemcfunction(os.path.join(path, 'data', bname, 'functions', 'play.mcfunction'), + """tag @s add {0} scoreboard players set @s {0}_t -1 -""".format(scoreObj) ) - writemcfunction(os.path.join(path, 'data', bname, 'functions', 'pause.mcfunction'), - "tag @s remove {}".format(scoreObj) ) - writemcfunction(os.path.join(path, 'data', bname, 'functions', 'stop.mcfunction'), -"""tag @s remove {0} +""".format(scoreObj)) + writemcfunction(os.path.join(path, 'data', bname, 'functions', 'pause.mcfunction'), + "tag @s remove {}".format(scoreObj)) + writemcfunction(os.path.join(path, 'data', bname, 'functions', 'stop.mcfunction'), + """tag @s remove {0} scoreboard players reset @s {0} -scoreboard players reset @s {0}_t""".format(scoreObj) ) - writemcfunction(os.path.join(path, 'data', bname, 'functions', 'remove.mcfunction'), -"""scoreboard objectives remove {0} -scoreboard objectives remove {0}_t""".format(scoreObj) ) - - text = '' - for k, v in instLayers.items(): - for i in range(len(v)): - text += 'execute run give @s minecraft:armor_stand{{display: {{Name: "{{\\"text\\":\\"{}\\"}}" }}, EntityTag: {{Marker: 1b, NoGravity:1b, Invisible: 1b, Tags: ["WNBS_Marker"], CustomName: "{{\\"text\\":\\"{}\\"}}" }} }}\n'.format( - "{}-{}".format(noteSounds[k]['name'], i), "{}-{}".format(k, i) - ) - writemcfunction(os.path.join(path, 'data', bname, 'functions', 'give.mcfunction'), text) - - colNotes = { tick: [x for x in data['notes'] if x['tick'] == tick] for tick in range(length) } - #pprint(colNotes) - print(len(colNotes)) - - for tick in range(length): - currNotes = colNotes[tick] - text = "" - #text = "say Calling function {}:notes/{}\n".format(bname, tick) - for note in currNotes: - text += \ -"""execute as @e[type=armor_stand, tag=WNBS_Marker, name=\"{inst}-{order}\"] at @s positioned ~ ~-1 ~ if block ~ ~ ~ minecraft:note_block[instrument={instname}] run setblock ~ ~ ~ minecraft:note_block[instrument={instname},note={key}] replace +scoreboard players reset @s {0}_t""".format(scoreObj)) + writemcfunction(os.path.join(path, 'data', bname, 'functions', 'remove.mcfunction'), + """scoreboard objectives remove {0} +scoreboard objectives remove {0}_t""".format(scoreObj)) + + text = '' + for k, v in instLayers.items(): + for i in range(len(v)): + text += 'execute run give @s minecraft:armor_stand{{display: {{Name: "{{\\"text\\":\\"{}\\"}}" }}, EntityTag: {{Marker: 1b, NoGravity:1b, Invisible: 1b, Tags: ["WNBS_Marker"], CustomName: "{{\\"text\\":\\"{}\\"}}" }} }}\n'.format( + "{}-{}".format(noteSounds[k]['name'], + i), "{}-{}".format(k, i) + ) + writemcfunction(os.path.join(path, 'data', bname, + 'functions', 'give.mcfunction'), text) + + colNotes = {tick: [x for x in data['notes'] + if x['tick'] == tick] for tick in range(length)} + # pprint(colNotes) + print(len(colNotes)) + + for tick in range(length): + currNotes = colNotes[tick] + text = "" + #text = "say Calling function {}:notes/{}\n".format(bname, tick) + for note in currNotes: + text += \ + """execute as @e[type=armor_stand, tag=WNBS_Marker, name=\"{inst}-{order}\"] at @s positioned ~ ~-1 ~ if block ~ ~ ~ minecraft:note_block[instrument={instname}] run setblock ~ ~ ~ minecraft:note_block[instrument={instname},note={key}] replace execute as @e[type=armor_stand, tag=WNBS_Marker, name=\"{inst}-{order}\"] at @s positioned ~ ~-1 ~ if block ~ ~ ~ minecraft:note_block[instrument={instname}] run fill ^ ^ ^-1 ^ ^ ^-1 minecraft:redstone_block replace minecraft:air execute as @e[type=armor_stand, tag=WNBS_Marker, name=\"{inst}-{order}\"] at @s positioned ~ ~-1 ~ if block ~ ~ ~ minecraft:note_block[instrument={instname}] run fill ^ ^ ^-1 ^ ^ ^-1 minecraft:air replace minecraft:redstone_block """.format( - obj=scoreObj, tick=tick, inst=note['inst'], order=instLayers[note['inst']].index(note['layer']), instname=noteSounds[note['inst']]['name'], key=max(33, min(57, note['key'])) - 33 - ) - if tick == length-1: text += "execute run function {}:stop".format(bname) - else: text += "scoreboard players set @s {}_t {}".format(scoreObj, tick) - if text != "": writemcfunction(os.path.join(path, 'data', bname, 'functions', 'notes', str(tick)+'.mcfunction'), text) - - steps = floor(log2(length)) + 1 - pow = 2**steps - for step in range(steps): - searchrange = floor(pow / (2**step)) - segments = floor(pow / searchrange) - for segment in range(segments): - text = "" - half = floor(searchrange / 2) - lower = searchrange * segment - - min1 = lower - max1 = lower + half - 1 - min2 = lower + half - max2 = lower + searchrange - 1 - - #print(str(step) + " " + str(segments) + " " + str(min1) + " " + str(max1) + " " + str(min2) + " " + string(max2)) - - if min1 <= length: - if step == steps-1: # Last step, play the tick - try: - if len(colNotes[min1]) > 0: text += "execute as @s[scores={{{0}={1}..{2}, {0}_t=..{3}}}] run function {4}:notes/{5}\n".format(scoreObj, min1*80, (max1+1)*80+40, min1-1, bname, min1) - except KeyError: break - if min2 <= length: - try: - if len(colNotes[min2]) > 0: text += "execute as @s[scores={{{0}={1}..{2}, {0}_t=..{3}}}] run function {4}:notes/{5}".format(scoreObj, min2*80, (max2+1)*80+40, min2-1, bname, min2) - except KeyError: break - else: # Don't play yet, refine the search - for i in range(min1, min(max1, length)+1): - try: - if len(colNotes[i]) > 0: - text += "execute as @s[scores={{{}={}..{}}}] run function {}:tree/{}_{}\n".format(scoreObj, min1*80, (max1+1)*80+40, bname, min1, max1) - break - except KeyError: break - for i in range(min2, min(max2, length)+1): - try: - if len(colNotes[i]) > 0: - text += "execute as @s[scores={{{}={}..{}}}] run function {}:tree/{}_{}".format(scoreObj, min2*80, (max2+2)*80+40, bname, min2, max2) - break - except KeyError: break - if text != "": - #text = "say Calling function {}:tree/{}_{}\n".format(bname, min1, max2) + text - writemcfunction(os.path.join(path, 'data', bname, 'functions', 'tree', '{}_{}.mcfunction'.format(min1, max2)), text) - else: break - - path = os.path.join(*os.path.normpath(path).split()) - bname = os.path.basename(path) - - data = DataPostprocess(cls.inputFileData) - data = compactNotes(data, groupPerc=False) - - noteSounds = cls.noteSounds - - instLayers = {} - for note in data['notes']: - if note['inst'] not in instLayers: - instLayers[note['inst']] = [note['layer']] - elif note['layer'] not in instLayers[note['inst']]: - instLayers[note['inst']].append(note['layer']) - #pprint(instLayers) - - locals()[mode]() - print("Done!") + obj=scoreObj, tick=tick, inst=note['inst'], order=instLayers[note['inst']].index(note['layer']), instname=noteSounds[note['inst']]['name'], key=max(33, min(57, note['key'])) - 33 + ) + if tick == length-1: + text += "execute run function {}:stop".format(bname) + else: + text += "scoreboard players set @s {}_t {}".format( + scoreObj, tick) + if text != "": + writemcfunction(os.path.join( + path, 'data', bname, 'functions', 'notes', str(tick)+'.mcfunction'), text) + + steps = floor(log2(length)) + 1 + pow = 2**steps + for step in range(steps): + searchrange = floor(pow / (2**step)) + segments = floor(pow / searchrange) + for segment in range(segments): + text = "" + half = floor(searchrange / 2) + lower = searchrange * segment + + min1 = lower + max1 = lower + half - 1 + min2 = lower + half + max2 = lower + searchrange - 1 + + #print(str(step) + " " + str(segments) + " " + str(min1) + " " + str(max1) + " " + str(min2) + " " + string(max2)) + + if min1 <= length: + if step == steps-1: # Last step, play the tick + try: + if len(colNotes[min1]) > 0: + text += "execute as @s[scores={{{0}={1}..{2}, {0}_t=..{3}}}] run function {4}:notes/{5}\n".format( + scoreObj, min1*80, (max1+1)*80+40, min1-1, bname, min1) + except KeyError: + break + if min2 <= length: + try: + if len(colNotes[min2]) > 0: + text += "execute as @s[scores={{{0}={1}..{2}, {0}_t=..{3}}}] run function {4}:notes/{5}".format( + scoreObj, min2*80, (max2+1)*80+40, min2-1, bname, min2) + except KeyError: + break + else: # Don't play yet, refine the search + for i in range(min1, min(max1, length)+1): + try: + if len(colNotes[i]) > 0: + text += "execute as @s[scores={{{}={}..{}}}] run function {}:tree/{}_{}\n".format( + scoreObj, min1*80, (max1+1)*80+40, bname, min1, max1) + break + except KeyError: + break + for i in range(min2, min(max2, length)+1): + try: + if len(colNotes[i]) > 0: + text += "execute as @s[scores={{{}={}..{}}}] run function {}:tree/{}_{}".format( + scoreObj, min2*80, (max2+2)*80+40, bname, min2, max2) + break + except KeyError: + break + if text != "": + #text = "say Calling function {}:tree/{}_{}\n".format(bname, min1, max2) + text + writemcfunction(os.path.join( + path, 'data', bname, 'functions', 'tree', '{}_{}.mcfunction'.format(min1, max2)), text) + else: + break + + path = os.path.join(*os.path.normpath(path).split()) + bname = os.path.basename(path) + + data = DataPostprocess(cls.inputFileData) + data = compactNotes(data, groupPerc=False) + + noteSounds = cls.noteSounds + + instLayers = {} + for note in data['notes']: + if note['inst'] not in instLayers: + instLayers[note['inst']] = [note['layer']] + elif note['layer'] not in instLayers[note['inst']]: + instLayers[note['inst']].append(note['layer']) + # pprint(instLayers) + + locals()[mode]() + print("Done!") + if __name__ == "__main__": - - vaniNoteSounds = [ - {'filename': 'harp.ogg', 'name': 'harp'}, - {'filename': 'dbass.ogg', 'name': 'bass'}, - {'filename': 'bdrum.ogg', 'name': 'basedrum'}, - {'filename': 'sdrum.ogg', 'name': 'snare'}, - {'filename': 'click.ogg', 'name': 'hat'}, - {'filename': 'guitar.ogg', 'name': 'guitar'}, - {'filename': 'flute.ogg', 'name': 'flute'}, - {'filename': 'bell.ogg', 'name': 'bell'}, - {'filename': 'icechime.ogg', 'name': 'chime'}, - {'filename': 'xylobone.ogg', 'name': 'xylophone'}, - {'filename': 'iron_xylophone.ogg', 'name': 'iron_xylophone'}, - {'filename': 'cow_bell.ogg', 'name': 'cow_bell'}, - {'filename': 'didgeridoo.ogg', 'name': 'didgeridoo'}, - {'filename': 'bit.ogg', 'name': 'bit'}, - {'filename': 'banjo.ogg', 'name': 'banjo'}, - {'filename': 'pling.ogg', 'name': 'pling'} -] - - print('Importing sounds...') - vaniNoteSounds = [ {'name': item['name'], 'filepath': resource_path('sounds', item['filename']), 'obj': AudioSegment.from_ogg(resource_path('sounds', item['filename'])), 'pitch': 45} for item in vaniNoteSounds] - - print('Imported sounds.') - print('Creating root...') - - root = tk.Tk() - app = MainWindow(root) - print('Creating app...') - - print(sys.argv) - if len(sys.argv) == 2: app.OnBrowseFile(True, sys.argv[1]) - - root.iconbitmap(resource_path("icon.ico")) - print('Ready') - root.mainloop() - - print("The app was closed.") \ No newline at end of file + + vaniNoteSounds = [ + {'filename': 'harp.ogg', 'name': 'harp'}, + {'filename': 'dbass.ogg', 'name': 'bass'}, + {'filename': 'bdrum.ogg', 'name': 'basedrum'}, + {'filename': 'sdrum.ogg', 'name': 'snare'}, + {'filename': 'click.ogg', 'name': 'hat'}, + {'filename': 'guitar.ogg', 'name': 'guitar'}, + {'filename': 'flute.ogg', 'name': 'flute'}, + {'filename': 'bell.ogg', 'name': 'bell'}, + {'filename': 'icechime.ogg', 'name': 'chime'}, + {'filename': 'xylobone.ogg', 'name': 'xylophone'}, + {'filename': 'iron_xylophone.ogg', 'name': 'iron_xylophone'}, + {'filename': 'cow_bell.ogg', 'name': 'cow_bell'}, + {'filename': 'didgeridoo.ogg', 'name': 'didgeridoo'}, + {'filename': 'bit.ogg', 'name': 'bit'}, + {'filename': 'banjo.ogg', 'name': 'banjo'}, + {'filename': 'pling.ogg', 'name': 'pling'} + ] + print('Creating root...') + + root = tk.Tk() + app = MainWindow(root) + print('Creating app...') + + print(sys.argv) + if len(sys.argv) == 2: + app.OnOpenFile(sys.argv[1]) + + root.iconbitmap(resource_path("icon.ico")) + print('Ready') + root.mainloop() + + print("The app was closed.") diff --git a/nbsio.py b/nbsio.py index e2f0539..cd5581e 100644 --- a/nbsio.py +++ b/nbsio.py @@ -28,213 +28,267 @@ SHORT = Struct('= 3: + headers['length'] = readNumeric(f, SHORT) + headers['height'] = readNumeric(f, SHORT) #Height + headers['name'] = readString(f) #Name + headers['author'] = readString(f) #Author + headers['orig_author'] = readString(f) #OriginalAuthor + headers['description'] = readString(f) #Description + headers['tempo'] = readNumeric(f, SHORT)/100 #Tempo + headers['auto-saving'] = readNumeric(f, BYTE) == 1 #Auto-saving enabled + headers['auto-saving_time'] = readNumeric(f, BYTE) #Auto-saving duration + headers['time_sign'] = readNumeric(f, BYTE) #Time signature + headers['minutes_spent'] = readNumeric(f, INT) #Minutes spent + headers['left_clicks'] = readNumeric(f, INT) #Left clicks + headers['right_clicks'] = readNumeric(f, INT) #Right clicks + headers['block_added'] = readNumeric(f, INT) #Total block added + headers['block_removed'] = readNumeric(f, INT) #Total block removed + headers['import_name'] = readString(f) #MIDI file name + if headers['file_version'] >= 4: + headers['loop'] = readNumeric(f, BYTE) #Loop enabled + headers['loop_max'] = readNumeric(f, BYTE) #Max loop count + headers['loop_start'] = readNumeric(f, SHORT) #Loop start tick + return headers -def readnbs(filename): - IsOldVersion = False - headers = {} - notes = deque() - maxLayer = 0 - usedInsts = [[], []] - hasPerc = False - layers = deque() - customInsts = deque() +def readnbs(fn): + notes = deque() + maxLayer = 0 + usedInsts = [[], []] + hasPerc = False + layers = deque() + customInsts = deque() + appendix = None - if filename is not '': - with open(filename, "rb") as f: - #Header - sign = readNumeric(f, SHORT) == 0 #Sign - if not sign: - IsOldVersion = True - headers['file_version'] = headers['vani_inst'] = -999 - f.seek(0, 0) - else: - headers['file_version'] = readNumeric(f, BYTE) #Version - headers['vani_inst'] = readNumeric(f, BYTE) - if IsOldVersion or headers['file_version'] >= 3: - headers['length'] = readNumeric(f, SHORT) - else: - headers['length'] = None - headers['height'] = readNumeric(f, SHORT) #Height - headers['name'] = readString(f) #Name - headers['author'] = readString(f) #Author - headers['orig_author'] = readString(f) #OriginalAuthor - headers['description'] = readString(f) #Description - headers['tempo'] = readNumeric(f, SHORT)/100 #Tempo - headers['auto-saving'] = readNumeric(f, BYTE) == 1 #Auto-saving enabled - headers['auto-saving_time'] = readNumeric(f, BYTE) #Auto-saving duration - headers['time_sign'] = readNumeric(f, BYTE) #Time signature - headers['minutes_spent'] = readNumeric(f, INT) #Minutes spent - headers['left_clicks'] = readNumeric(f, INT) #Left clicks - headers['right_clicks'] = readNumeric(f, INT) #Right clicks - headers['block_added'] = readNumeric(f, INT) #Total block added - headers['block_removed'] = readNumeric(f, INT) #Total block removed - headers['import_name'] = readString(f) #MIDI file name - #Notes - tick = -1 - tickJumps = layerJumps = 0 - while True: - tickJumps = readNumeric(f, SHORT) - #if notes: notes[-1]['duration'] = tickJumps - if tickJumps == 0: break - tick += tickJumps - layer = -1 - while True: - layerJumps = readNumeric(f, SHORT) - if layerJumps == 0: break - layer += layerJumps - inst = readNumeric(f, BYTE) - key = readNumeric(f, BYTE)#+21 - if inst in {2, 3, 4}: - hasPerc = isPerc = True - if inst not in usedInsts[1]: usedInsts[1].append(inst) - else: - isPerc = False - if inst not in usedInsts[0]: usedInsts[0].append(inst) - duraKey = None - for idx, note in enumerate(notes): - if note['layer'] == layer: duraKey = idx - if duraKey is not None: - if notes: notes[duraKey]['duration'] = tick - notes[duraKey]['tick'] - notes.append({'tick':tick, 'layer':layer, 'inst':inst, 'key':key, 'isPerc':isPerc, 'duration':8}) - maxLayer = max(layer, maxLayer) - if headers['length'] is None: headers['length'] = tick + 1 - indexByTick = tuple([ (tk, tuple([notes.index(nt) for nt in notes if nt['tick'] == tk]) ) for tk in range(headers['length']+1) ]) - tick = tickJumps = layerJumps = layer = inst = key = duraKey = isPerc = tk = nt = None - #Layers - for i in range(headers['height']): - name = readString(f) #Layer name - vol = readNumeric(f, BYTE) #Volume - if sign: - stereo = readNumeric(f, BYTE) #Stereo - else: - stereo = 100 - layers.append({'name':name, 'volume':vol, 'stereo':stereo}) - name = vol = stereo = None - #Custom instrument - headers['inst_count'] = readNumeric(f, BYTE) - for i in range(headers['inst_count']): - name = readString(f) #Instrument name - file = readString(f) #Sound filename - pitch = readNumeric(f, BYTE) #Pitch - shouldPressKeys = bool(readNumeric(f, BYTE)) #Press key - customInsts.append({'name':name, 'filename':file, 'pitch':pitch, 'pressKeys':shouldPressKeys}) - sortedNotes = sorted(notes, key = operator.itemgetter('tick', 'layer') ) - data = {'headers':headers, 'notes':sortedNotes, 'layers':layers, 'customInsts':customInsts, 'IsOldVersion':IsOldVersion, 'hasPerc':hasPerc, 'maxLayer':maxLayer, 'usedInsts':usedInsts, 'indexByTick':indexByTick } - return data + if fn != '': + if fn.__class__.__name__ == 'HTTPResponse': + f = fn + else: + f = open(fn, "rb") + try: + headers = readnbsheader(f) + #Notes + tick = -1 + tickJumps = layerJumps = 0 + while True: + tickJumps = readNumeric(f, SHORT) + #if notes: notes[-1]['duration'] = tickJumps + if tickJumps == 0: break + tick += tickJumps + layer = -1 + while True: + layerJumps = readNumeric(f, SHORT) + if layerJumps == 0: break + layer += layerJumps + inst = readNumeric(f, BYTE) + key = readNumeric(f, BYTE)#+21 + if headers['file_version'] >= 4: + vel = readNumeric(f, BYTE) + pan = readNumeric(f, BYTE) + pitch = readNumeric(f, SHORT) + else: + vel = 100 + pan = 100 + pitch = 0 + + if inst in {2, 3, 4}: + hasPerc = isPerc = True + if inst not in usedInsts[1]: usedInsts[1].append(inst) + else: + isPerc = False + if inst not in usedInsts[0]: usedInsts[0].append(inst) + duraKey = None + for idx, note in enumerate(notes): + if note['layer'] == layer: duraKey = idx + if duraKey is not None: + if notes: notes[duraKey]['duration'] = tick - notes[duraKey]['tick'] + notes.append({'tick':tick, 'layer':layer, 'inst':inst, 'key':key, 'vel':vel, 'pan':pan, 'pitch':pitch, 'isPerc':isPerc, 'duration':8}) + maxLayer = max(layer, maxLayer) + if headers['length'] is None: headers['length'] = tick + 1 + indexByTick = tuple([ (tk, tuple([notes.index(nt) for nt in notes if nt['tick'] == tk]) ) for tk in range(headers['length']+1) ]) + tick = tickJumps = layerJumps = layer = inst = key = duraKey = isPerc = tk = nt = None + #Layers + for i in range(headers['height']): + name = readString(f) #Layer name + if headers['file_version'] >= 4: + lock = readNumeric(f, BYTE) == 1 #Lock + else: + lock = False + vol = readNumeric(f, BYTE) #Volume + vol = 100 if vol == -1 else vol + if headers['file_version'] >= 2: + stereo = readNumeric(f, BYTE) #Stereo + else: + stereo = 100 + layers.append({'name':name, 'lock':lock, 'volume':vol, 'stereo':stereo}) + name = vol = stereo = None + #Custom instrument + headers['inst_count'] = readNumeric(f, BYTE) + for i in range(headers['inst_count']): + name = readString(f) #Instrument name + file = readString(f) #Sound fn + pitch = readNumeric(f, BYTE) #Pitch + shouldPressKeys = bool(readNumeric(f, BYTE)) #Press key + customInsts.append({'name':name, 'fn':file, 'pitch':pitch, 'pressKeys':shouldPressKeys}) + #Rest of the file + appendix = f.read() + finally: + try: + f.close() + except: + pass + sortedNotes = sorted(notes, key = operator.itemgetter('tick', 'layer') ) + data = {'headers':headers, 'notes':sortedNotes, 'layers':layers, 'customInsts':customInsts, 'hasPerc':hasPerc, 'maxLayer':maxLayer, 'usedInsts':(tuple(usedInsts[0]), tuple(usedInsts[1])), 'indexByTick':indexByTick } + if appendix: data['appendix'] = appendix + return data -def opennbs(filename, printOutput=False): - data = readnbs(filename) - if printOutput: pprint(data) - return data - +def opennbs(fn, printOutput=False): + data = readnbs(fn) + if printOutput: pprint(data) + return data + def DataPostprocess(data): - headers = data['headers'] - notes = data['notes'] - usedInsts = [[], []] - maxLayer = 0 - data['hasPerc'] = False - for i, note in enumerate(data['notes']): - tick, inst, layer = note['tick'], note['inst'], note['layer'] - if inst in {2, 3, 4}: - data['hasPerc'] = note['isPerc'] = True - if inst not in usedInsts[1]: usedInsts[1].append(inst) - else: - note['isPerc'] = False - if inst not in usedInsts[0]: usedInsts[0].append(inst) - duraKey = None - for idx, note in enumerate(data['notes']): - if note['layer'] == layer: duraKey = idx - if duraKey is not None: - if i > 0: data['notes'][duraKey]['duration'] = tick - data['notes'][duraKey]['tick'] - else: - note['duration'] = 8 - maxLayer = max(layer, maxLayer) - data['headers']['length'] = tick - data['maxLayer'] = maxLayer - data['usedInsts'] = usedInsts - data['indexByTick'] = tuple([ (tk, set([notes.index(nt) for nt in notes if nt['tick'] == tk]) ) for tk in range(headers['length']+1) ]) - note = tick = inst = layer = duraKey = usedInsts = maxLayer = tk = nt = None - return data + headers = data['headers'] + notes = data['notes'] + usedInsts = [[], []] + maxLayer = 0 + data['hasPerc'] = False + for i, note in enumerate(data['notes']): + tick, inst, layer = note['tick'], note['inst'], note['layer'] + if inst in {2, 3, 4}: + data['hasPerc'] = note['isPerc'] = True + if inst not in usedInsts[1]: usedInsts[1].append(inst) + else: + note['isPerc'] = False + if inst not in usedInsts[0]: usedInsts[0].append(inst) + duraKey = None + for idx, note in enumerate(data['notes']): + if note['layer'] == layer: duraKey = idx + if duraKey is not None: + if i > 0: data['notes'][duraKey]['duration'] = tick - data['notes'][duraKey]['tick'] + else: + note['duration'] = 8 + maxLayer = max(layer, maxLayer) + data['headers']['length'] = tick + data['maxLayer'] = maxLayer + data['usedInsts'] = (tuple(usedInsts[0]), tuple(usedInsts[1])) + data['indexByTick'] = tuple([ (tk, set([notes.index(nt) for nt in notes if nt['tick'] == tk]) ) for tk in range(headers['length']+1) ]) + note = tick = inst = layer = duraKey = usedInsts = maxLayer = tk = nt = None + return data def writeNumeric(f, fmt, v): - f.write(fmt.pack(v)) + f.write(fmt.pack(v)) def writeString(f, v): - writeNumeric(f, INT, len(v)) - f.write(v.encode()) - -def writenbs(filename, data): - if filename is not '' and data is not None: - data = DataPostprocess(data) - headers, notes, layers, customInsts, IsOldVersion = \ - data['headers'], data['notes'], data['layers'], data['customInsts'], data['IsOldVersion'] - with open(filename, "wb") as f: - #Header - if not IsOldVersion: - writeNumeric(f, SHORT, 0) - writeNumeric(f, BYTE, headers['file_version']) #Version - writeNumeric(f, BYTE, headers['vani_inst']) - if IsOldVersion or headers['file_version'] >= 3: - writeNumeric(f, SHORT, headers['length']) #Length - writeNumeric(f, SHORT, headers['height']) #Height - writeString(f, headers['name']) #Name - writeString(f, headers['author']) #Author - writeString(f, headers['orig_author']) #OriginalAuthor - writeString(f, headers['description']) #Description - writeNumeric(f, SHORT, int(headers['tempo']*100)) #Tempo - writeNumeric(f, BYTE, headers['auto-saving']) #Auto-saving enabled - writeNumeric(f, BYTE, headers['auto-saving_time']) #Auto-saving duration - writeNumeric(f, BYTE, headers['time_sign']) #Time signature - writeNumeric(f, INT, headers['minutes_spent']) #Minutes spent - writeNumeric(f, INT, headers['left_clicks']) #Left clicks - writeNumeric(f, INT, headers['right_clicks']) #Right clicks - writeNumeric(f, INT, headers['block_added']) #Total block added - writeNumeric(f, INT, headers['block_removed']) #Total block removed - writeString(f, headers['import_name']) #MIDI file name - #Notes - shuffle(notes) - sortedNotes = sorted(notes, key = operator.itemgetter('tick', 'layer') ) - #pprint(sortedNotes) - tick = layer = -1 - fstNote = sortedNotes[0] - for note in sortedNotes: - if tick != note['tick']: - if note != fstNote: - writeNumeric(f, SHORT, 0) - layer = -1 - writeNumeric(f, SHORT, note['tick'] - tick) - tick = note['tick'] - if layer != note['layer']: - writeNumeric(f, SHORT, note['layer'] - layer) - layer = note['layer'] - writeNumeric(f, BYTE, note['inst']) - writeNumeric(f, BYTE, note['key'])#-21 - writeNumeric(f, SHORT, 0) - writeNumeric(f, SHORT, 0) - #Layers - for layer in layers: - writeString(f, layer['name']) #Layer name - writeNumeric(f, BYTE, layer['volume']) #Volume - if not IsOldVersion: - writeNumeric(f, BYTE, layer['stereo']) #Stereo - #Custom instrument - pprint(customInsts) - if len(customInsts) == 0: writeNumeric(f, BYTE, 0) - else: - writeNumeric(f, BYTE, len(customInsts)) - if len(customInsts) > 0: - for customInst in customInsts: - writeString(f, customInst['name']) #Instrument name - writeString(f, customInst['filename']) #Sound filename - writeNumeric(f, BYTE, customInst['pitch']) #Pitch - writeNumeric(f, BYTE, customInst['pressKeys']) #Press key + writeNumeric(f, INT, len(v)) + f.write(v.encode()) + +def writenbs(fn, data): + if fn != '' and data is not None: + data = DataPostprocess(data) + headers, notes, layers, customInsts = \ + data['headers'], data['notes'], data['layers'], data['customInsts'] + with open(fn, "wb") as f: + #Header + if not headers['is_classic']: + writeNumeric(f, SHORT, 0) + writeNumeric(f, BYTE, headers['file_version']) #Version + writeNumeric(f, BYTE, headers['vani_inst']) + if headers['is_classic'] or headers['file_version'] >= 3: + writeNumeric(f, SHORT, headers['length']) #Length + writeNumeric(f, SHORT, headers['height']) #Height + writeString(f, headers['name']) #Name + writeString(f, headers['author']) #Author + writeString(f, headers['orig_author']) #OriginalAuthor + writeString(f, headers['description']) #Description + writeNumeric(f, SHORT, int(headers['tempo']*100)) #Tempo + writeNumeric(f, BYTE, headers['auto-saving']) #Auto-saving enabled + writeNumeric(f, BYTE, headers['auto-saving_time']) #Auto-saving duration + writeNumeric(f, BYTE, headers['time_sign']) #Time signature + writeNumeric(f, INT, headers['minutes_spent']) #Minutes spent + writeNumeric(f, INT, headers['left_clicks']) #Left clicks + writeNumeric(f, INT, headers['right_clicks']) #Right clicks + writeNumeric(f, INT, headers['block_added']) #Total block added + writeNumeric(f, INT, headers['block_removed']) #Total block removed + writeString(f, headers['import_name']) #MIDI file name + if headers['file_version'] >= 4: + writeNumeric(f, BYTE, headers['loop']) #Loop enabled + writeNumeric(f, BYTE, headers['loop_max']) #Max loop count + writeNumeric(f, SHORT, headers['loop_start']) #Loop start tick + #Notes + # shuffle(notes) + sortedNotes = sorted(notes, key = operator.itemgetter('tick', 'layer') ) + #pprint(sortedNotes) + tick = layer = -1 + fstNote = sortedNotes[0] + for note in sortedNotes: + if tick != note['tick']: + if note != fstNote: + writeNumeric(f, SHORT, 0) + layer = -1 + writeNumeric(f, SHORT, note['tick'] - tick) + tick = note['tick'] + if layer != note['layer']: + writeNumeric(f, SHORT, note['layer'] - layer) + layer = note['layer'] + writeNumeric(f, BYTE, note['inst']) + writeNumeric(f, BYTE, note['key'])#-21 + if headers['file_version'] >= 4: + writeNumeric(f, BYTE, note['vel']) + writeNumeric(f, BYTE, note['pan']) + writeNumeric(f, SHORT, note['pitch']) + writeNumeric(f, SHORT, 0) + writeNumeric(f, SHORT, 0) + #Layers + for layer in layers: + writeString(f, layer['name']) #Layer name + if headers['file_version'] >= 4: + writeNumeric(f, BYTE, layer['lock']) #Lock + writeNumeric(f, BYTE, layer['volume']) #Volume + if headers['file_version'] >= 2: + writeNumeric(f, BYTE, layer['stereo']) #Stereo + #Custom instrument + pprint(customInsts) + if len(customInsts) == 0: writeNumeric(f, BYTE, 0) + else: + writeNumeric(f, BYTE, len(customInsts)) + if len(customInsts) > 0: + for customInst in customInsts: + writeString(f, customInst['name']) #Instrument name + writeString(f, customInst['fn']) #Sound fn + writeNumeric(f, BYTE, customInst['pitch']) #Pitch + writeNumeric(f, BYTE, customInst['pressKeys']) #Press key if __name__ == "__main__": - import sys - opennbs(sys.argv[1], sys.argv[2]) \ No newline at end of file + import sys + if len(sys.argv) == 2: in_ra = True + else: in_ra = sys.argv[2] + opennbs(sys.argv[1], in_ra) \ No newline at end of file From b5fd38cceac1e7d657cf53734d39773685cb97c8 Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Mon, 7 Sep 2020 16:09:04 +0700 Subject: [PATCH 02/22] Minor change Signed-off-by: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 03b6818..c5c7679 100644 --- a/main.py +++ b/main.py @@ -481,7 +481,7 @@ def onClose(self, event=None): self.parent.quit() self.parent.destroy() - def OnBrowseFile(self, _): + def OnBrowseFile(self, _=None): types = [('Note Block Studio files', '*.nbs'), ('All files', '*')] filename = askopenfilename(filetypes=types) if filename != '': From cbf894e66a1949d392254e77b160eda3f7d9afd0 Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Thu, 10 Sep 2020 17:59:16 +0700 Subject: [PATCH 03/22] [Breaking changes] Redesign the UI, remove some redundant functions MainWindow: - Use pygubu library with the toplevel.ui file for the GUI - Remove status bar - Support multiple files. nbsio: - Remove opennbs() function - Notes will no longer have "duration" property - Song data will no longer have "indexByTick" - Song loading should be as fast as expected. Signed-off-by: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> --- main.py | 173 ++++++++++++++--------------- nbsio.py | 86 ++++++--------- toplevel.ui | 306 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 421 insertions(+), 144 deletions(-) create mode 100644 toplevel.ui diff --git a/main.py b/main.py index c5c7679..d221c2a 100644 --- a/main.py +++ b/main.py @@ -30,29 +30,26 @@ import tkinter as tk import tkinter.ttk as ttk -#import tkinter.messagebox as tkmsgbox -from tkinter.filedialog import askopenfilename, asksaveasfilename, askdirectory -from tkinter.scrolledtext import ScrolledText +from tkinter.messagebox import showerror +from tkinter.filedialog import askopenfilename, asksaveasfilename, askdirectory, askopenfilenames -from time import sleep, time +import pygubu + +from time import time, strftime from pprint import pprint from random import randrange from math import floor, log2 -from datetime import date -#from collections import deque +from datetime import timedelta from PIL import Image, ImageTk import music21 as m21 import music21.stream as m21s import music21.instrument as m21i -from attr import Attr -from nbsio import opennbs, writenbs, DataPostprocess +from nbsio import readnbs, writenbs, DataPostprocess from ncfio import writencf # Credit: https://stackoverflow.com/questions/42474560/pyinstaller-single-exe-file-ico-image-in-title-of-tkinter-main-window - - def resource_path(*args): if len(args) > 1: relative_path = os.path.join(*args) @@ -70,38 +67,14 @@ def resource_path(*args): return r -class MainWindow(tk.Frame): - def __init__(self, parent): - tk.Frame.__init__(self, parent) - self.parent = parent - self.properties() - self.elements() - self.WindowBind() - self.toggleExpOptiGrp() - # self.update() - self.pack(fill='both', expand=True) - self.update() - WindowGeo(self.parent, self.parent, 800, 500, 600, 500) - self.lift() - self.focus_force() - self.grab_set() - self.grab_release() - - def properties(self): - self.VERSION = '0.7.0' - self.filePath = None - self.inputFileData = None - self.noteSounds = None - self.last = Attr() - self.last.inputFileData = None - self.var = Attr() - self.PlayingState = 'stop' - self.PlayingTick = -1 - self.SongPlayerAfter = None - self.exportFilePath = tk.StringVar() - - def elements(self): - self.parent.title("NBS Tool") +class MainWindow: + def __init__(self): + self.builder = builder = pygubu.Builder() + builder.add_from_file(resource_path('toplevel.ui')) + self.toplevel = builder.get_object('toplevel') + self.mainwin = builder.get_object('mainFrame') + + self.toplevel.title("NBS Tool") self.style = ttk.Style() # self.style.theme_use("default") try: @@ -112,48 +85,71 @@ def elements(self): self.style.theme_use("winnative") except Exception: pass + + self.setupMenuBar() + self.windowBind() + + builder.connect_callbacks(self) + + self.mainwin.lift() + self.mainwin.focus_force() + self.mainwin.grab_set() + self.mainwin.grab_release() - # Menu bar - self.menuBar = tk.Menu(self) - self.parent.configure(menu=self.menuBar) - self.menus() - - # Tabs - self.NbTabs = ttk.Notebook(self) - self.tabs() - self.NbTabs.enable_traversal() - self.NbTabs.pack(fill='both', expand=True) - - # Footer - tk.Frame(self, height=5).pack() - - self.footer = tk.Frame( - self, relief='groove', borderwidth=1, height=25, width=self.winfo_width()) - self.footer.pack_propagate(False) - self.footerElements() - self.footer.pack(side='top', fill='x') + self.VERSION = '0.7.0' + self.filePaths = [] + self.songsData = [] - def menus(self): + def setupMenuBar(self): # 'File' menu - self.fileMenu = tk.Menu(self.menuBar, tearoff=False) + self.menuBar = menuBar = self.builder.get_object('menubar') + self.toplevel.configure(menu=menuBar) + + self.fileMenu = tk.Menu(menuBar, tearoff=False) self.menuBar.add_cascade(label="File", menu=self.fileMenu) self.fileMenu.add_command( - label="Open", accelerator="Ctrl+O", command=self.OnBrowseFile) + label="Open", accelerator="Ctrl+O", command=self.openFiles) self.fileMenu.add_command( - label="Save", accelerator="Ctrl+S", command=self.OnSaveFile) - self.fileMenu.add_command( - label="Save as new file", accelerator="Ctrl+Shift+S", command=lambda: self.OnSaveFile(True)) + label="Save all", accelerator="Ctrl+S", command=self.OnSaveFile) self.fileMenu.add_separator() self.fileMenu.add_command( label="Quit", accelerator="Esc", command=self.onClose) - - self.helpMenu = tk.Menu(self.menuBar, tearoff=False) + + self.importMenu = tk.Menu(menuBar, tearoff=False) + self.menuBar.add_cascade(label="Import", menu=self.importMenu) + + self.exportMenu = tk.Menu(menuBar, tearoff=False) + self.menuBar.add_cascade(label="Export", menu=self.importMenu) + + self.helpMenu = tk.Menu(menuBar, tearoff=False) self.menuBar.add_cascade(label="Help", menu=self.helpMenu) self.helpMenu.add_command( label="About", command=lambda: AboutWindow(self)) + def openFiles(self, _=None): + types = [('Note Block Studio files', '*.nbs'), ('All files', '*')] + self.filePaths = askopenfilenames(filetypes=types) + fileTable = self.builder.get_object('fileTable') + + fileTable.delete(*fileTable.get_children()) + for filePath in self.filePaths: + try: + songData = readnbs(filePath) + self.songsData.append(songData) + except Exception: + showerror("Reading file error", "Cannot read or parse file: "+filePath) + print(traceback.format_exc()) + continue + headers = songData['headers'] + length = timedelta(seconds=floor(headers['length'] / headers['tempo'])) if headers['length'] != None else "Not calculated" + name = headers['name'] + author = headers['author'] + orig_author = headers['orig_author'] + fileTable.insert("", 'end', text=filePath, values=(length, name, author, orig_author)) + self.mainwin.update() + def tabs(self): # "General" tab self.GeneralTab = tk.Frame(self.NbTabs) @@ -430,29 +426,29 @@ def footerElements(self): # self.progressbar.start() # self.progressbar.stop() - def WindowBind(self): + def windowBind(self): # Keys - self.parent.bind('', self.onClose) - self.parent.bind('', self.OnBrowseFile) - self.parent.bind('', self.OnSaveFile) - self.parent.bind('', lambda _: self.OnSaveFile(True)) - self.parent.bind('', lambda _: self.OnSaveFile(True)) + self.toplevel.bind('', self.onClose) + self.toplevel.bind('', self.openFiles) + self.toplevel.bind('', self.OnSaveFile) + self.toplevel.bind('', lambda _: self.OnSaveFile(True)) + self.toplevel.bind('', lambda _: self.OnSaveFile(True)) # Bind class - self.bind_class("Message", "", + self.mainwin.bind_class("Message", "", lambda e: e.widget.configure(width=e.width-10)) for tkclass in ('TButton', 'Checkbutton', 'Radiobutton'): - self.bind_class(tkclass, '', lambda e: e.widget.event_generate( + self.mainwin.bind_class(tkclass, '', lambda e: e.widget.event_generate( '', when='tail')) - self.bind_class("TCombobox", "", + self.mainwin.bind_class("TCombobox", "", lambda e: e.widget.event_generate('')) - for tkclass in ("Entry", "Text", "ScrolledText"): - self.bind_class(tkclass, "", self.popupmenus) + for tkclass in ("Entry", "Text", "ScrolledText", "TCombobox"): + self.mainwin.bind_class(tkclass, "", self.popupmenus) - self.bind_class("TNotebook", "<>", + self.mainwin.bind_class("TNotebook", "<>", self._on_tab_changed) # Credit: http://code.activestate.com/recipes/580726-tkinter-notebook-that-fits-to-the-height-of-every-/ @@ -464,7 +460,7 @@ def _on_tab_changed(self, event): def popupmenus(self, event): w = event.widget - self.popupMenu = tk.Menu(self, tearoff=False) + self.popupMenu = tk.Menu(self.mainwin, tearoff=False) self.popupMenu.add_command( label="Select all", accelerator="Ctrl+A", command=lambda: w.event_generate("")) self.popupMenu.add_separator() @@ -478,8 +474,8 @@ def popupmenus(self, event): event.x_root, event.y_root) def onClose(self, event=None): - self.parent.quit() - self.parent.destroy() + self.toplevel.quit() + self.toplevel.destroy() def OnBrowseFile(self, _=None): types = [('Note Block Studio files', '*.nbs'), ('All files', '*')] @@ -1329,18 +1325,15 @@ def wnbs(): {'filename': 'banjo.ogg', 'name': 'banjo'}, {'filename': 'pling.ogg', 'name': 'pling'} ] - print('Creating root...') - - root = tk.Tk() - app = MainWindow(root) + + app = MainWindow() print('Creating app...') print(sys.argv) if len(sys.argv) == 2: app.OnOpenFile(sys.argv[1]) - root.iconbitmap(resource_path("icon.ico")) print('Ready') - root.mainloop() + app.mainwin.mainloop() print("The app was closed.") diff --git a/nbsio.py b/nbsio.py index cd5581e..c92315f 100644 --- a/nbsio.py +++ b/nbsio.py @@ -20,9 +20,9 @@ from struct import Struct from pprint import pprint -from random import shuffle +# from random import shuffle from collections import deque -import operator +from operator import itemgetter BYTE = Struct('= 3: @@ -87,7 +86,8 @@ def readnbs(fn): layers = deque() customInsts = deque() appendix = None - + readNumeric = read_numeric + if fn != '': if fn.__class__.__name__ == 'HTTPResponse': f = fn @@ -95,12 +95,12 @@ def readnbs(fn): f = open(fn, "rb") try: headers = readnbsheader(f) + file_version = headers['file_version'] #Notes tick = -1 tickJumps = layerJumps = 0 while True: tickJumps = readNumeric(f, SHORT) - #if notes: notes[-1]['duration'] = tickJumps if tickJumps == 0: break tick += tickJumps layer = -1 @@ -110,7 +110,7 @@ def readnbs(fn): layer += layerJumps inst = readNumeric(f, BYTE) key = readNumeric(f, BYTE)#+21 - if headers['file_version'] >= 4: + if file_version >= 4: vel = readNumeric(f, BYTE) pan = readNumeric(f, BYTE) pitch = readNumeric(f, SHORT) @@ -125,16 +125,10 @@ def readnbs(fn): else: isPerc = False if inst not in usedInsts[0]: usedInsts[0].append(inst) - duraKey = None - for idx, note in enumerate(notes): - if note['layer'] == layer: duraKey = idx - if duraKey is not None: - if notes: notes[duraKey]['duration'] = tick - notes[duraKey]['tick'] - notes.append({'tick':tick, 'layer':layer, 'inst':inst, 'key':key, 'vel':vel, 'pan':pan, 'pitch':pitch, 'isPerc':isPerc, 'duration':8}) + notes.append({'tick':tick, 'layer':layer, 'inst':inst, 'key':key, 'vel':vel, 'pan':pan, 'pitch':pitch, 'isPerc':isPerc}) maxLayer = max(layer, maxLayer) if headers['length'] is None: headers['length'] = tick + 1 - indexByTick = tuple([ (tk, tuple([notes.index(nt) for nt in notes if nt['tick'] == tk]) ) for tk in range(headers['length']+1) ]) - tick = tickJumps = layerJumps = layer = inst = key = duraKey = isPerc = tk = nt = None + # indexByTick = tuple([ (tk, tuple([notes.index(nt) for nt in notes if nt['tick'] == tk]) ) for tk in range(headers['length']+1) ]) #Layers for i in range(headers['height']): name = readString(f) #Layer name @@ -144,10 +138,7 @@ def readnbs(fn): lock = False vol = readNumeric(f, BYTE) #Volume vol = 100 if vol == -1 else vol - if headers['file_version'] >= 2: - stereo = readNumeric(f, BYTE) #Stereo - else: - stereo = 100 + stereo = readNumeric(f, BYTE) if file_version >= 2 else 100 #Stereo layers.append({'name':name, 'lock':lock, 'volume':vol, 'stereo':stereo}) name = vol = stereo = None #Custom instrument @@ -165,23 +156,17 @@ def readnbs(fn): f.close() except: pass - sortedNotes = sorted(notes, key = operator.itemgetter('tick', 'layer') ) - data = {'headers':headers, 'notes':sortedNotes, 'layers':layers, 'customInsts':customInsts, 'hasPerc':hasPerc, 'maxLayer':maxLayer, 'usedInsts':(tuple(usedInsts[0]), tuple(usedInsts[1])), 'indexByTick':indexByTick } + data = {'headers':headers, 'notes':notes, 'layers':layers, 'customInsts':customInsts, 'hasPerc':hasPerc, 'maxLayer':maxLayer, 'usedInsts':(tuple(usedInsts[0]), tuple(usedInsts[1])), } if appendix: data['appendix'] = appendix return data - -def opennbs(fn, printOutput=False): - data = readnbs(fn) - if printOutput: pprint(data) - return data def DataPostprocess(data): - headers = data['headers'] + # headers = data['headers'] notes = data['notes'] usedInsts = [[], []] maxLayer = 0 data['hasPerc'] = False - for i, note in enumerate(data['notes']): + for i, note in enumerate(notes): tick, inst, layer = note['tick'], note['inst'], note['layer'] if inst in {2, 3, 4}: data['hasPerc'] = note['isPerc'] = True @@ -189,19 +174,11 @@ def DataPostprocess(data): else: note['isPerc'] = False if inst not in usedInsts[0]: usedInsts[0].append(inst) - duraKey = None - for idx, note in enumerate(data['notes']): - if note['layer'] == layer: duraKey = idx - if duraKey is not None: - if i > 0: data['notes'][duraKey]['duration'] = tick - data['notes'][duraKey]['tick'] - else: - note['duration'] = 8 maxLayer = max(layer, maxLayer) data['headers']['length'] = tick data['maxLayer'] = maxLayer data['usedInsts'] = (tuple(usedInsts[0]), tuple(usedInsts[1])) - data['indexByTick'] = tuple([ (tk, set([notes.index(nt) for nt in notes if nt['tick'] == tk]) ) for tk in range(headers['length']+1) ]) - note = tick = inst = layer = duraKey = usedInsts = maxLayer = tk = nt = None + # data['indexByTick'] = tuple([ (tk, set([notes.index(nt) for nt in notes if nt['tick'] == tk]) ) for tk in range(headers['length']+1) ]) return data def writeNumeric(f, fmt, v): @@ -216,13 +193,14 @@ def writenbs(fn, data): data = DataPostprocess(data) headers, notes, layers, customInsts = \ data['headers'], data['notes'], data['layers'], data['customInsts'] + file_version = headers['file_version'] with open(fn, "wb") as f: #Header - if not headers['is_classic']: + if file_version != 0: writeNumeric(f, SHORT, 0) - writeNumeric(f, BYTE, headers['file_version']) #Version + writeNumeric(f, BYTE, file_version) #Version writeNumeric(f, BYTE, headers['vani_inst']) - if headers['is_classic'] or headers['file_version'] >= 3: + if (file_version == 0) or (file_version >= 3): writeNumeric(f, SHORT, headers['length']) #Length writeNumeric(f, SHORT, headers['height']) #Height writeString(f, headers['name']) #Name @@ -239,14 +217,13 @@ def writenbs(fn, data): writeNumeric(f, INT, headers['block_added']) #Total block added writeNumeric(f, INT, headers['block_removed']) #Total block removed writeString(f, headers['import_name']) #MIDI file name - if headers['file_version'] >= 4: + if file_version >= 4: writeNumeric(f, BYTE, headers['loop']) #Loop enabled writeNumeric(f, BYTE, headers['loop_max']) #Max loop count writeNumeric(f, SHORT, headers['loop_start']) #Loop start tick #Notes # shuffle(notes) - sortedNotes = sorted(notes, key = operator.itemgetter('tick', 'layer') ) - #pprint(sortedNotes) + sortedNotes = sorted(notes, key = itemgetter('tick', 'layer') ) tick = layer = -1 fstNote = sortedNotes[0] for note in sortedNotes: @@ -261,7 +238,7 @@ def writenbs(fn, data): layer = note['layer'] writeNumeric(f, BYTE, note['inst']) writeNumeric(f, BYTE, note['key'])#-21 - if headers['file_version'] >= 4: + if file_version >= 4: writeNumeric(f, BYTE, note['vel']) writeNumeric(f, BYTE, note['pan']) writeNumeric(f, SHORT, note['pitch']) @@ -270,10 +247,10 @@ def writenbs(fn, data): #Layers for layer in layers: writeString(f, layer['name']) #Layer name - if headers['file_version'] >= 4: + if file_version >= 4: writeNumeric(f, BYTE, layer['lock']) #Lock writeNumeric(f, BYTE, layer['volume']) #Volume - if headers['file_version'] >= 2: + if file_version >= 2: writeNumeric(f, BYTE, layer['stereo']) #Stereo #Custom instrument pprint(customInsts) @@ -291,4 +268,5 @@ def writenbs(fn, data): import sys if len(sys.argv) == 2: in_ra = True else: in_ra = sys.argv[2] - opennbs(sys.argv[1], in_ra) \ No newline at end of file + data = readnbs(sys.argv[1]) + if in_ra: pprint(data) \ No newline at end of file diff --git a/toplevel.ui b/toplevel.ui new file mode 100644 index 0000000..2c62342 --- /dev/null +++ b/toplevel.ui @@ -0,0 +1,306 @@ + + + + 550x550 + 550 + 200|200 + both + false + NBSTool + 550 + + + + true + both + True + top + + + + 100 + nw + Files + 200 + + false + both + 5 + 5 + True + top + + + + both + false + + false + x + True + top + + + + 100 + extended + 105 + + false + 5 + 5 + True + top + + + + w + w + 20 + true + File path + true + true + 200 + + + + + e + w + 50 + false + Length + false + true + 80 + + + + + w + w + 20 + true + Name + false + true + 150 + + + + + w + w + 20 + true + Author + false + true + 150 + + + + + w + w + 20 + true + Original author + false + true + 200 + + + + + + + + + Open... + 10 + + w + 5 + 5 + True + left + + + + + + Save + 10 + + w + 5 + 5 + True + left + + + + + + Remove + 10 + + e + 5 + 5 + True + right + + + + + + + + nw + Tools + + true + both + 5 + 5 + True + top + + + + + true + both + 5 + 5 + True + top + + + + Metadata + + + + True + top + + + + + + + + Format + + + + True + top + + + + Changes files's format to: + + 5 + 5 + True + top + + + + + + + True + top + + + + + + + + + + Flip + + + + True + top + + + + left + Filp notes: + + 5 + 5 + True + top + + + + + + Horizontally + + n + 10 + True + top + + + + + + Vertically + + 10 + True + top + + + + + + + + + + + + arrow + 1 + Apply + 10 + + 5 + 5 + True + right + + + + + + + + + True + right + + + + + + + + arrow + false + + From dbd216ad26bd0d65299356a377b311469590041f Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Sun, 13 Sep 2020 08:59:18 +0700 Subject: [PATCH 04/22] [Impl] Support song saving, improve the fileTable General: - Support saving selected files or all files. fileTable: - Multiple selection with Shift+Up and Shift+Down is now supported; - Opening files will now removes all loaded file, which is different from adding files; - Add files removing feature; - Some buttons will only be pressable when the selection is not empty. nbsio: - Add docstring and type hints to function; - Minor optimizations. --- main.py | 271 +++++++++++++++++++++++++++++----------------------- nbsio.py | 60 ++++++++---- toplevel.ui | 41 ++++++-- 3 files changed, 224 insertions(+), 148 deletions(-) diff --git a/main.py b/main.py index d221c2a..1aae866 100644 --- a/main.py +++ b/main.py @@ -27,6 +27,8 @@ import re import json +from pathlib import Path + import tkinter as tk import tkinter.ttk as ttk @@ -35,7 +37,7 @@ import pygubu -from time import time, strftime +from time import time from pprint import pprint from random import randrange from math import floor, log2 @@ -75,6 +77,7 @@ def __init__(self): self.mainwin = builder.get_object('mainFrame') self.toplevel.title("NBS Tool") + WindowGeo(self.toplevel, 550, 550) self.style = ttk.Style() # self.style.theme_use("default") try: @@ -89,6 +92,18 @@ def __init__(self): self.setupMenuBar() self.windowBind() + def on_fileTable_select(event): + selectionNotEmpty = len(event.widget.selection()) > 0 + if selectionNotEmpty: + builder.get_object('saveFilesBtn')["state"] = "normal" + builder.get_object('removeEntriesBtn')["state"] = "normal" + else: + builder.get_object('saveFilesBtn')["state"] = "disabled" + builder.get_object('removeEntriesBtn')["state"] = "disabled" + + fileTable = builder.get_object('fileTable') + fileTable.bind("<>", on_fileTable_select) + builder.connect_callbacks(self) self.mainwin.lift() @@ -111,7 +126,9 @@ def setupMenuBar(self): self.fileMenu.add_command( label="Open", accelerator="Ctrl+O", command=self.openFiles) self.fileMenu.add_command( - label="Save all", accelerator="Ctrl+S", command=self.OnSaveFile) + label="Save", accelerator="Ctrl+S", command=self.saveAll) + self.fileMenu.add_command( + label="Save all", accelerator="Ctrl+Shift+S", command=self.saveAll) self.fileMenu.add_separator() self.fileMenu.add_command( label="Quit", accelerator="Esc", command=self.onClose) @@ -126,29 +143,71 @@ def setupMenuBar(self): self.menuBar.add_cascade(label="Help", menu=self.helpMenu) self.helpMenu.add_command( - label="About", command=lambda: AboutWindow(self)) + label="About") def openFiles(self, _=None): - types = [('Note Block Studio files', '*.nbs'), ('All files', '*')] - self.filePaths = askopenfilenames(filetypes=types) - fileTable = self.builder.get_object('fileTable') + fileTable = self.builder.get_object('fileTable') + fileTable.delete(*fileTable.get_children()) + self.filePaths.clear() + self.songsData.clear() + self.addFiles() + + def addFiles(self, _=None): + types = [('Note Block Studio files', '*.nbs'), ('All files', '*')] + addedPaths = askopenfilenames(filetypes=types) + if len(addedPaths) == 0: return + fileTable = self.builder.get_object('fileTable') + for filePath in addedPaths: + try: + songData = readnbs(filePath) + self.songsData.append(songData) + except Exception: + showerror("Reading file error", "Cannot read or parse file: "+filePath) + print(traceback.format_exc()) + continue + headers = songData['headers'] + length = timedelta(seconds=floor(headers['length'] / headers['tempo'])) if headers['length'] != None else "Not calculated" + name = headers['name'] + author = headers['author'] + orig_author = headers['orig_author'] + fileTable.insert("", 'end', text=filePath, values=(length, name, author, orig_author)) + self.mainwin.update() + self.filePaths.extend(addedPaths) + + def saveFiles(self, _=None): + if len(self.filePaths) == 0: return + fileTable = self.builder.get_object('fileTable') + if len(selection := fileTable.selection()) > 0: + if len(selection) == 1: + filePath = os.path.basename(self.filePaths[fileTable.index(selection[0])]) + types = [('Note Block Studio files', '*.nbs'), ('All files', '*')] + path = asksaveasfilename(filetypes=types, initialfile=filePath, defaultextension=".nbs") + else: + path = askdirectory(title="Select folder to save") + if path == '': return + Path(path).mkdir(parents=True, exist_ok=True) + for item in selection: + i = fileTable.index(item) + filePath = self.filePaths[i] + writenbs(os.path.join(path, os.path.basename(filePath)), self.songsData[i]) + + def saveAll(self, _=None): + if len(self.filePaths) == 0: return + path = askdirectory(title="Select folder to save") + if path == '': return + Path(path).mkdir(parents=True, exist_ok=True) + for i, filePath in enumerate(self.filePaths): + writenbs(os.path.join(path, os.path.basename(filePath)), self.songsData[i]) - fileTable.delete(*fileTable.get_children()) - for filePath in self.filePaths: - try: - songData = readnbs(filePath) - self.songsData.append(songData) - except Exception: - showerror("Reading file error", "Cannot read or parse file: "+filePath) - print(traceback.format_exc()) - continue - headers = songData['headers'] - length = timedelta(seconds=floor(headers['length'] / headers['tempo'])) if headers['length'] != None else "Not calculated" - name = headers['name'] - author = headers['author'] - orig_author = headers['orig_author'] - fileTable.insert("", 'end', text=filePath, values=(length, name, author, orig_author)) - self.mainwin.update() + def removeSelectedFiles(self): + if len(self.filePaths) == 0: return + fileTable = self.builder.get_object('fileTable') + if len(selection := fileTable.selection()) > 0: + for item in reversed(selection): + i = fileTable.index(item) + fileTable.delete(item) + del self.filePaths[i] + del self.songsData[i] def tabs(self): # "General" tab @@ -427,29 +486,35 @@ def footerElements(self): # self.progressbar.stop() def windowBind(self): + toplevel = self.toplevel + mainwin = self.mainwin # Keys - self.toplevel.bind('', self.onClose) - self.toplevel.bind('', self.openFiles) - self.toplevel.bind('', self.OnSaveFile) - self.toplevel.bind('', lambda _: self.OnSaveFile(True)) - self.toplevel.bind('', lambda _: self.OnSaveFile(True)) + toplevel.bind('', self.onClose) + toplevel.bind('', self.openFiles) + toplevel.bind('', self.saveFiles) + toplevel.bind('', self.saveAll) + toplevel.bind('', self.saveAll) # Bind class - self.mainwin.bind_class("Message", "", + mainwin.bind_class("Message", "", lambda e: e.widget.configure(width=e.width-10)) for tkclass in ('TButton', 'Checkbutton', 'Radiobutton'): - self.mainwin.bind_class(tkclass, '', lambda e: e.widget.event_generate( + mainwin.bind_class(tkclass, '', lambda e: e.widget.event_generate( '', when='tail')) - self.mainwin.bind_class("TCombobox", "", + mainwin.bind_class("TCombobox", "", lambda e: e.widget.event_generate('')) for tkclass in ("Entry", "Text", "ScrolledText", "TCombobox"): - self.mainwin.bind_class(tkclass, "", self.popupmenus) + mainwin.bind_class(tkclass, "", self.popupmenus) - self.mainwin.bind_class("TNotebook", "<>", + mainwin.bind_class("TNotebook", "<>", self._on_tab_changed) + + mainwin.bind_class("Treeview", "", self._on_treeview_shift_down) + mainwin.bind_class("Treeview", "", self._on_treeview_shift_up) + mainwin.bind_class("Treeview", "", self._on_treeview_left_click, add='+') # Credit: http://code.activestate.com/recipes/580726-tkinter-notebook-that-fits-to-the-height-of-every-/ def _on_tab_changed(self, event): @@ -457,7 +522,41 @@ def _on_tab_changed(self, event): tab = event.widget.nametowidget(event.widget.select()) event.widget.configure(height=tab.winfo_reqheight()) - + + # Credit: https://stackoverflow.com/questions/57939932/treeview-how-to-select-multiple-rows-using-cursor-up-and-down-keys + def _on_treeview_shift_down(self, event): + tree = event.widget + cur_item = tree.focus() + next_item = tree.next(cur_item) + if next_item == '': return 'break' + selection = tree.selection() + if next_item in selection: + tree.selection_remove(cur_item) + else: + tree.selection_add(cur_item) + tree.selection_add(next_item) + tree.focus(next_item) + tree.see(next_item) + + def _on_treeview_shift_up(self, event): + tree = event.widget + cur_item = tree.focus() + prev_item = tree.prev(cur_item) + if prev_item == '': return 'break' + selection = tree.selection() + if prev_item in selection: + tree.selection_remove(cur_item) + else: + tree.selection_add(cur_item) + tree.selection_add(prev_item) + tree.focus(prev_item) + tree.see(prev_item) + + def _on_treeview_left_click(self, event): + tree = event.widget + if tree.identify_row(event.y) == '': + tree.selection_set(tuple()) + def popupmenus(self, event): w = event.widget self.popupMenu = tk.Menu(self.mainwin, tearoff=False) @@ -477,49 +576,6 @@ def onClose(self, event=None): self.toplevel.quit() self.toplevel.destroy() - def OnBrowseFile(self, _=None): - types = [('Note Block Studio files', '*.nbs'), ('All files', '*')] - filename = askopenfilename(filetypes=types) - if filename != '': - self.OnOpenFile(filename) - - def OnOpenFile(self, fileName): - self.UpdateProgBar(20) - if self.filePath != '': - data = opennbs(fileName) - if data is not None: - self.UpdateProgBar(80) - self.inputFileData = data - self.ExpOutputEntry.delete(0, 'end') - self.exportFilePath.set('') - print(type(data)) - - self.filePath = fileName - self.UpdateVar() - self.parent.title('"{}" – NBS Tool'.format( - fileName.split('/')[-1])) - self.RaiseFooter('Opened') - self.UpdateProgBar(100) - self.UpdateProgBar(-1) - - def OnSaveFile(self, saveAsNewFile=False): - if self.inputFileData is not None: - if saveAsNewFile is True: - types = [('Note Block Studio files', '*.nbs'), - ('All files', '*')] - self.filePath = asksaveasfilename(filetypes=types) - if not self.filePath.lower().endswith('.nbs'): - self.filePath = self.filePath.split('.')[0] + '.nbs' - self.UpdateProgBar(50) - - writenbs(self.filePath, self.inputFileData) - - self.parent.title('"{}" – NBS Tool'.format( - self.filePath.split('/')[-1])) - self.UpdateProgBar(100) - self.RaiseFooter('Saved') - self.UpdateProgBar(-1) - def toggleCompactToolOpt(self, id=1): if id <= 2: a = ((self.var.tool.compact.opt1.get() == 0) @@ -757,47 +813,6 @@ def RaiseFooter(self, text='', color='green', hid=False): self.footerLabel.update() -class AboutWindow(tk.Toplevel): - def __init__(self, parent): - tk.Toplevel.__init__(self) - self.parent = parent - self.title("About this application...") - - self.logo = ImageTk.PhotoImage(Image.open(resource_path( - 'icon.ico')).resize((128, 128), Image.ANTIALIAS)) - - logolabel = tk.Label(self, text="NBSTool", font=( - "Arial", 44), image=self.logo, compound='left') - logolabel.pack(padx=30, pady=(10*2, 10)) - - description = tk.Message(self, text="A tool to work with .nbs (Note Block Studio) files.\nAuthor: IoeCmcomc\nVersion: {}".format( - parent.VERSION), justify='center') - description.pack(fill='both', expand=False, padx=10, pady=10) - - githubLink = ttk.Button(self, text='GitHub', command=lambda: webbrowser.open( - "https://github.com/IoeCmcomc/NBSTool", new=True)) - githubLink.pack(padx=10, pady=10) - - self.lift() - self.focus_force() - self.grab_set() - # self.grab_release() - - self.resizable(False, False) - self.transient(self.parent) - - self.bind("", self.Alarm) - self.bind('', lambda _: self.destroy()) - - self.iconbitmap(resource_path("icon.ico")) - - WindowGeo(self, parent, 500, 300) - - def Alarm(self, event): - self.focus_force() - self.bell() - - class FlexCheckbutton(tk.Checkbutton): def __init__(self, *args, **kwargs): okwargs = dict(kwargs) @@ -887,9 +902,25 @@ def pack(self, key=None, **opts): self.switch(key) -def WindowGeo(obj, parent, width, height, mwidth=None, mheight=None): - ScreenWidth = root.winfo_screenwidth() - ScreenHeight = root.winfo_screenheight() +def WindowGeo(obj, width, height, mwidth=None, mheight=None): + # Credit: https://stackoverflow.com/questions/3129322/how-do-i-get-monitor-resolution-in-python/56913005#56913005 + def get_curr_screen_size(): + """ + Workaround to get the size of the current screen in a multi-screen setup. + + Returns: + Size(Tuple): (width, height) + """ + root = tk.Tk() + root.update_idletasks() + root.attributes('-fullscreen', True) + root.state('iconic') + size = (root.winfo_width(), root.winfo_height(),) + root.destroy() + print(size) + return size + + ScreenWidth, ScreenHeight = get_curr_screen_size() WindowWidth = width or obj.winfo_reqwidth() WindowHeight = height or obj.winfo_reqheight() diff --git a/nbsio.py b/nbsio.py index c92315f..8271ee4 100644 --- a/nbsio.py +++ b/nbsio.py @@ -20,31 +20,43 @@ from struct import Struct from pprint import pprint -# from random import shuffle from collections import deque from operator import itemgetter +from typing import BinaryIO +from time import time BYTE = Struct(' dict: + '''Read a .nbs file header from a file object''' + headers = {} headers['length'] = None readNumeric = read_numeric + readString = read_string #Header first = readNumeric(f, SHORT) #Sign @@ -78,7 +90,9 @@ def readnbsheader(f): headers['loop_start'] = readNumeric(f, SHORT) #Loop start tick return headers -def readnbs(fn): +def readnbs(fn) -> dict: + '''Read a .nbs file from disk or URL.''' + notes = deque() maxLayer = 0 usedInsts = [[], []] @@ -87,6 +101,7 @@ def readnbs(fn): customInsts = deque() appendix = None readNumeric = read_numeric + readString = read_string if fn != '': if fn.__class__.__name__ == 'HTTPResponse': @@ -113,12 +128,11 @@ def readnbs(fn): if file_version >= 4: vel = readNumeric(f, BYTE) pan = readNumeric(f, BYTE) - pitch = readNumeric(f, SHORT) + pitch = readNumeric(f, SHORT_SIGNED) else: vel = 100 pan = 100 pitch = 0 - if inst in {2, 3, 4}: hasPerc = isPerc = True if inst not in usedInsts[1]: usedInsts[1].append(inst) @@ -150,7 +164,7 @@ def readnbs(fn): shouldPressKeys = bool(readNumeric(f, BYTE)) #Press key customInsts.append({'name':name, 'fn':file, 'pitch':pitch, 'pressKeys':shouldPressKeys}) #Rest of the file - appendix = f.read() + appendix = f.read() finally: try: f.close() @@ -181,19 +195,25 @@ def DataPostprocess(data): # data['indexByTick'] = tuple([ (tk, set([notes.index(nt) for nt in notes if nt['tick'] == tk]) ) for tk in range(headers['length']+1) ]) return data -def writeNumeric(f, fmt, v): +def write_numeric(f, fmt, v): f.write(fmt.pack(v)) -def writeString(f, v): - writeNumeric(f, INT, len(v)) +def write_string(f, v): + write_numeric(f, INT, len(v)) f.write(v.encode()) -def writenbs(fn, data): +def writenbs(fn: str, data: dict) -> None: + '''Save nbs data to a file on disk with the path given.''' + + start = time() if fn != '' and data is not None: + writeNumeric = write_numeric + writeString = write_string data = DataPostprocess(data) headers, notes, layers, customInsts = \ data['headers'], data['notes'], data['layers'], data['customInsts'] file_version = headers['file_version'] + with open(fn, "wb") as f: #Header if file_version != 0: @@ -222,7 +242,6 @@ def writenbs(fn, data): writeNumeric(f, BYTE, headers['loop_max']) #Max loop count writeNumeric(f, SHORT, headers['loop_start']) #Loop start tick #Notes - # shuffle(notes) sortedNotes = sorted(notes, key = itemgetter('tick', 'layer') ) tick = layer = -1 fstNote = sortedNotes[0] @@ -241,9 +260,10 @@ def writenbs(fn, data): if file_version >= 4: writeNumeric(f, BYTE, note['vel']) writeNumeric(f, BYTE, note['pan']) - writeNumeric(f, SHORT, note['pitch']) - writeNumeric(f, SHORT, 0) - writeNumeric(f, SHORT, 0) + writeNumeric(f, SHORT_SIGNED, note['pitch']) + # writeNumeric(f, SHORT, 0) + # writeNumeric(f, SHORT, 0) + writeNumeric(f, INT, 0) #Layers for layer in layers: writeString(f, layer['name']) #Layer name @@ -253,7 +273,6 @@ def writenbs(fn, data): if file_version >= 2: writeNumeric(f, BYTE, layer['stereo']) #Stereo #Custom instrument - pprint(customInsts) if len(customInsts) == 0: writeNumeric(f, BYTE, 0) else: writeNumeric(f, BYTE, len(customInsts)) @@ -263,6 +282,9 @@ def writenbs(fn, data): writeString(f, customInst['fn']) #Sound fn writeNumeric(f, BYTE, customInst['pitch']) #Pitch writeNumeric(f, BYTE, customInst['pressKeys']) #Press key + #Appendix + if 'appendix' in data: f.write(data['appendix']) + print(time() - start) if __name__ == "__main__": import sys diff --git a/toplevel.ui b/toplevel.ui index 2c62342..e07dd66 100644 --- a/toplevel.ui +++ b/toplevel.ui @@ -1,12 +1,13 @@ + test 550x550 550 200|200 both false - NBSTool + NBS Tool 550 @@ -23,7 +24,6 @@ Files 200 - false both 5 5 @@ -35,8 +35,9 @@ both false - false - x + both + 5 + 5 True top @@ -46,9 +47,8 @@ extended 105 - false - 5 - 5 + true + both True top @@ -118,6 +118,9 @@ + openFiles + left + false Open... 10 @@ -131,7 +134,9 @@ - Save + saveFiles + disabled + Save... 10 w @@ -143,7 +148,9 @@ - + + removeSelectedFiles + disabled Remove 10 @@ -155,6 +162,22 @@ + + + addFiles + top + false + Add... + 10 + + e + 5 + 5 + True + right + + + From 35d52694fd4133fa3f688e6f882f956d77ec0592 Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Mon, 14 Sep 2020 23:22:27 +0700 Subject: [PATCH 05/22] refactor: Start separating custom widgets, refactor the nbsio file nbsio: - Add more type hints; - Raise error when opening file's format is newer than supported formats new WrapMessage: autowrap tkinter.Message Signed-off-by: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> --- customwidgets.py | 18 +++++++++++++ main.py | 67 ++++++++++++++++++++++-------------------------- nbsio.py | 40 +++++++++++++++-------------- toplevel.ui | 56 +++++++++++++++++++--------------------- wrapmessage.py | 51 ++++++++++++++++++++++++++++++++++++ 5 files changed, 147 insertions(+), 85 deletions(-) create mode 100644 customwidgets.py create mode 100644 wrapmessage.py diff --git a/customwidgets.py b/customwidgets.py new file mode 100644 index 0000000..699fcc2 --- /dev/null +++ b/customwidgets.py @@ -0,0 +1,18 @@ +from pygubu import BuilderObject, register_widget +from wrapmessage import WrapMessage + +print(__name__) + +class WrapMessageBuilder(BuilderObject): + class_ = WrapMessage + + OPTIONS_STANDARD = ('anchor', 'background', 'borderwidth', 'cursor', 'font', + 'foreground', 'highlightbackground', 'highlightcolor', + 'highlightthickness', 'padx', 'pady', 'relief', 'takefocus', + 'text', 'textvariable') + OPTIONS_SPECIFIC = ('aspect', 'justify', 'width', 'padding') + properties = OPTIONS_STANDARD + OPTIONS_SPECIFIC + +register_widget('customwidgets.wrapmessage', WrapMessageBuilder, + 'WrapMessage', ('tk', 'Custom')) + diff --git a/main.py b/main.py index 1aae866..ba605b0 100644 --- a/main.py +++ b/main.py @@ -48,7 +48,7 @@ import music21.stream as m21s import music21.instrument as m21i -from nbsio import readnbs, writenbs, DataPostprocess +from nbsio import readnbs, writenbs, DataPostprocess, NBS_VERSION from ncfio import writencf # Credit: https://stackoverflow.com/questions/42474560/pyinstaller-single-exe-file-ico-image-in-title-of-tkinter-main-window @@ -73,7 +73,10 @@ class MainWindow: def __init__(self): self.builder = builder = pygubu.Builder() builder.add_from_file(resource_path('toplevel.ui')) + print("The following exception is not an error:") + print('='*20) self.toplevel = builder.get_object('toplevel') + print('='*20) self.mainwin = builder.get_object('mainFrame') self.toplevel.title("NBS Tool") @@ -89,20 +92,29 @@ def __init__(self): except Exception: pass - self.setupMenuBar() + builder.import_variables(self) + + self.initMenuBar() + self.initFormatTab() self.windowBind() + applyBtn = builder.get_object('applyBtn') + def on_fileTable_select(event): selectionNotEmpty = len(event.widget.selection()) > 0 if selectionNotEmpty: builder.get_object('saveFilesBtn')["state"] = "normal" builder.get_object('removeEntriesBtn')["state"] = "normal" + applyBtn["state"] = "normal" else: builder.get_object('saveFilesBtn')["state"] = "disabled" builder.get_object('removeEntriesBtn')["state"] = "disabled" + applyBtn["state"] = "disabled" fileTable = builder.get_object('fileTable') fileTable.bind("<>", on_fileTable_select) + + applyBtn.configure(command=self.applyTool) builder.connect_callbacks(self) @@ -115,7 +127,7 @@ def on_fileTable_select(event): self.filePaths = [] self.songsData = [] - def setupMenuBar(self): + def initMenuBar(self): # 'File' menu self.menuBar = menuBar = self.builder.get_object('menubar') self.toplevel.configure(menu=menuBar) @@ -209,37 +221,10 @@ def removeSelectedFiles(self): del self.filePaths[i] del self.songsData[i] - def tabs(self): - # "General" tab - self.GeneralTab = tk.Frame(self.NbTabs) - - self.GeneralTab.rowconfigure(0) - self.GeneralTab.rowconfigure(1, weight=1) - - self.GeneralTab.columnconfigure(0, weight=1, uniform='a') - self.GeneralTab.columnconfigure(1, weight=1, uniform='a') - - self.GeneralTabElements() - self.NbTabs.add(self.GeneralTab, text="General") - - # "Tools" tab - self.ToolsTab = tk.Frame(self.NbTabs) - - self.ToolsTab.rowconfigure(0, weight=1, uniform='b') - self.ToolsTab.rowconfigure(1, weight=1, uniform='b') - self.ToolsTab.rowconfigure(2) - - self.ToolsTab.columnconfigure(0, weight=1, uniform='b') - self.ToolsTab.columnconfigure(1, weight=1, uniform='b') - - self.ToolsTabElements() - self.NbTabs.add(self.ToolsTab, text="Tools") - - # "Export" tab - self.ExportTab = tk.Frame(self.NbTabs) - - self.ExportTabElements() - self.NbTabs.add(self.ExportTab, text="Export") + def initFormatTab(self): + combobox = self.builder.get_object('formatCombo') + combobox.configure(values=("(not selected)", '4', '3', '2', '1', "Classic")) + combobox.current(0) def GeneralTabElements(self): fpadx, fpady = 10, 10 @@ -496,9 +481,6 @@ def windowBind(self): toplevel.bind('', self.saveAll) # Bind class - mainwin.bind_class("Message", "", - lambda e: e.widget.configure(width=e.width-10)) - for tkclass in ('TButton', 'Checkbutton', 'Radiobutton'): mainwin.bind_class(tkclass, '', lambda e: e.widget.event_generate( '', when='tail')) @@ -585,6 +567,17 @@ def toggleCompactToolOpt(self, id=1): self.CompactToolChkOpt1["state"] = "disable" if self.var.tool.compact.get( ) == 0 else "normal" + def applyTool(self): + builder = self.builder + fileTable = builder.get_object('fileTable') + songsData = self.songsData + if (formatComboIndex := builder.get_object('formatCombo').current()) > 0: + outputVersion = (NBS_VERSION + 1) - formatComboIndex + selectedIndexes = (fileTable.index(item) for item in fileTable.selection()) + for i in selectedIndexes: + songsData[i]['headers']['file_version'] = outputVersion + + def OnApplyTool(self): self.ToolsTabButton['state'] = 'disabled' self.UpdateProgBar(0) diff --git a/nbsio.py b/nbsio.py index 8271ee4..c432ae6 100644 --- a/nbsio.py +++ b/nbsio.py @@ -30,11 +30,11 @@ SHORT_SIGNED = Struct(' int: '''Read the following bytes from file and return a number.''' raw = f.read(fmt.size) @@ -42,7 +42,7 @@ def read_numeric(f: BinaryIO, fmt: Struct): if beginInst: print("{0:<2}{1:<20}{2:<10}{3:<11}".format(fmt.size, str(raw), raw.hex(), rawInt)) return fmt.unpack(raw)[0] -def read_string(f: BinaryIO): +def read_string(f: BinaryIO) -> str: '''Read the following bytes from file and return a ASCII string.''' length = read_numeric(f, INT) @@ -61,13 +61,15 @@ def readnbsheader(f: BinaryIO) -> dict: #Header first = readNumeric(f, SHORT) #Sign if first != 0: #File is old type - headers['file_version'] = 0 + headers['file_version'] = file_version = 0 headers['vani_inst'] = 10 headers['length'] = first else: #File is new type - headers['file_version'] = readNumeric(f, BYTE) #Version + headers['file_version'] = file_version = readNumeric(f, BYTE) #Version + if file_version > NBS_VERSION: + raise NotImplementedError("This format version ({}) is not supported.".format(file_version)) headers['vani_inst'] = readNumeric(f, BYTE) - if headers['file_version'] >= 3: + if file_version >= 3: headers['length'] = readNumeric(f, SHORT) headers['height'] = readNumeric(f, SHORT) #Height headers['name'] = readString(f) #Name @@ -84,7 +86,7 @@ def readnbsheader(f: BinaryIO) -> dict: headers['block_added'] = readNumeric(f, INT) #Total block added headers['block_removed'] = readNumeric(f, INT) #Total block removed headers['import_name'] = readString(f) #MIDI file name - if headers['file_version'] >= 4: + if file_version >= 4: headers['loop'] = readNumeric(f, BYTE) #Loop enabled headers['loop_max'] = readNumeric(f, BYTE) #Max loop count headers['loop_start'] = readNumeric(f, SHORT) #Loop start tick @@ -174,7 +176,7 @@ def readnbs(fn) -> dict: if appendix: data['appendix'] = appendix return data -def DataPostprocess(data): +def DataPostprocess(data: dict) -> dict: # headers = data['headers'] notes = data['notes'] usedInsts = [[], []] @@ -195,10 +197,10 @@ def DataPostprocess(data): # data['indexByTick'] = tuple([ (tk, set([notes.index(nt) for nt in notes if nt['tick'] == tk]) ) for tk in range(headers['length']+1) ]) return data -def write_numeric(f, fmt, v): +def write_numeric(f: BinaryIO, fmt: Struct, v) -> None: f.write(fmt.pack(v)) -def write_string(f, v): +def write_string(f: BinaryIO, v) -> None: write_numeric(f, INT, len(v)) f.write(v.encode()) @@ -219,7 +221,7 @@ def writenbs(fn: str, data: dict) -> None: if file_version != 0: writeNumeric(f, SHORT, 0) writeNumeric(f, BYTE, file_version) #Version - writeNumeric(f, BYTE, headers['vani_inst']) + writeNumeric(f, BYTE, headers.get('vani_inst', 10)) if (file_version == 0) or (file_version >= 3): writeNumeric(f, SHORT, headers['length']) #Length writeNumeric(f, SHORT, headers['height']) #Height @@ -238,9 +240,9 @@ def writenbs(fn: str, data: dict) -> None: writeNumeric(f, INT, headers['block_removed']) #Total block removed writeString(f, headers['import_name']) #MIDI file name if file_version >= 4: - writeNumeric(f, BYTE, headers['loop']) #Loop enabled - writeNumeric(f, BYTE, headers['loop_max']) #Max loop count - writeNumeric(f, SHORT, headers['loop_start']) #Loop start tick + writeNumeric(f, BYTE, headers.get('loop', False)) #Loop enabled + writeNumeric(f, BYTE, headers.get('loop_max', 0)) #Max loop count + writeNumeric(f, SHORT, headers.get('loop_start', 0)) #Loop start tick #Notes sortedNotes = sorted(notes, key = itemgetter('tick', 'layer') ) tick = layer = -1 @@ -258,9 +260,9 @@ def writenbs(fn: str, data: dict) -> None: writeNumeric(f, BYTE, note['inst']) writeNumeric(f, BYTE, note['key'])#-21 if file_version >= 4: - writeNumeric(f, BYTE, note['vel']) - writeNumeric(f, BYTE, note['pan']) - writeNumeric(f, SHORT_SIGNED, note['pitch']) + writeNumeric(f, BYTE, note.get('vel', 100)) + writeNumeric(f, BYTE, note.get('pan', 100)) + writeNumeric(f, SHORT_SIGNED, note.get('pitch', 0)) # writeNumeric(f, SHORT, 0) # writeNumeric(f, SHORT, 0) writeNumeric(f, INT, 0) @@ -268,10 +270,10 @@ def writenbs(fn: str, data: dict) -> None: for layer in layers: writeString(f, layer['name']) #Layer name if file_version >= 4: - writeNumeric(f, BYTE, layer['lock']) #Lock + writeNumeric(f, BYTE, layer.get('lock', False)) #Lock writeNumeric(f, BYTE, layer['volume']) #Volume if file_version >= 2: - writeNumeric(f, BYTE, layer['stereo']) #Stereo + writeNumeric(f, BYTE, layer.get('stereo', 100)) #Stereo #Custom instrument if len(customInsts) == 0: writeNumeric(f, BYTE, 0) else: diff --git a/toplevel.ui b/toplevel.ui index e07dd66..5ea8478 100644 --- a/toplevel.ui +++ b/toplevel.ui @@ -1,14 +1,11 @@ - test - 550x550 - 550 - 200|200 + 600x600 + 350|350 both false NBS Tool - 550 @@ -19,7 +16,6 @@ - 100 nw Files 200 @@ -31,7 +27,7 @@ top - + both false @@ -43,9 +39,7 @@ - 100 extended - 105 true both @@ -185,7 +179,7 @@ nw Tools - true + false both 5 5 @@ -196,37 +190,27 @@ true - both + x 5 5 True top - - - Metadata - - - - True - top - - - - - Format - + + 1 + 2 True - top + 1 + 2 - Changes files's format to: + Changes files format to: 5 5 @@ -237,7 +221,21 @@ + readonly + string:formatComboCurrent + + True + top + + + + + + Changing files's format to a older version can cause data to be lost + + true + both True top @@ -314,8 +312,9 @@ + se True - right + bottom @@ -323,7 +322,6 @@ - arrow false diff --git a/wrapmessage.py b/wrapmessage.py new file mode 100644 index 0000000..c9fceb7 --- /dev/null +++ b/wrapmessage.py @@ -0,0 +1,51 @@ +from __future__ import print_function + +import tkinter as tk +import tkinter.ttk as ttk + +from tkinter.messagebox import showinfo + +from tkinter import Message + +class WrapMessage(Message): + padding = 10 + + def __init__(self, master=None, **kwargs): + super().__init__(master, **kwargs) + self.bind("", self._adjustWidth) + + def configure(self, cnf={}, **kw): + # showinfo("Input config", '{} {}'.format(cnf, kw)) + + key = 'padding' + if key in cnf: + self.padding = int(cnf[key]) + del cnf[key] + if key in kw: + self.padding = int(kw[key]) + del kw[key] + + # showinfo("Output config", '{} {}'.format(cnf, kw)) + + super().configure(cnf, **kw) + + config = configure + + def cget(self, key): + option = 'padding' + if key == option: + return self.padding + + return super().cget(key) + + def _adjustWidth(self, event): + print(self.padding) + event.widget.configure(width=event.width-self.padding) + + +if __name__ == '__main__': + root = tk.Tk() + msg = WrapMessage(root) + msg.configure(padding=40, text="This is a WrapMessage.") + msg.pack(fill='both', expand=True) + root.mainloop() \ No newline at end of file From 7ca0ef036ac0a929be62dd4bc8341a5da08db69a Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Mon, 21 Sep 2020 12:56:02 +0700 Subject: [PATCH 06/22] impl: Readd the datapack exporting feature DatapackExportDialog: - Only use with 1 file at one time. MainWindow: - Remove unused functions - Fix a bug which makes datapack song can't be stoppe Signed-off-by: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> --- .gitignore | 4 +- attr.py | 295 ---------------------------------- datapackexportdialog.ui | 61 +++++++ main.py | 340 ++++++++++++++-------------------------- nbsio.py | 14 +- toplevel.ui | 1 + 6 files changed, 188 insertions(+), 527 deletions(-) delete mode 100644 attr.py create mode 100644 datapackexportdialog.ui diff --git a/.gitignore b/.gitignore index 29ed922..6a69b2c 100644 --- a/.gitignore +++ b/.gitignore @@ -333,8 +333,7 @@ ASALocalRun/ *.nbs *.mid *.mp3 -add*/ -pack.mcmeta/ +datapacks/ ## Ignore other files/folders. #*.ico @@ -350,5 +349,4 @@ dist/ main.build/ main.dist/ -sounds/block.note.sax.ogg setup.py diff --git a/attr.py b/attr.py deleted file mode 100644 index 1d92cf4..0000000 --- a/attr.py +++ /dev/null @@ -1,295 +0,0 @@ -# This file is a part of: -# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -# ███▄▄▄▄ ▀█████████▄ ▄████████ ███ ▄██████▄ ▄██████▄ ▄█ -# ███▀▀▀██▄ ███ ███ ███ ███ ▀█████████▄ ███ ███ ███ ███ ███ -# ███ ███ ███ ███ ███ █▀ ▀███▀▀██ ███ ███ ███ ███ ███ -# ███ ███ ▄███▄▄▄██▀ ███ ███ ▀ ███ ███ ███ ███ ███ -# ███ ███ ▀▀███▀▀▀██▄ ▀███████████ ███ ███ ███ ███ ███ ███ -# ███ ███ ███ ██▄ ███ ███ ███ ███ ███ ███ ███ -# ███ ███ ███ ███ ▄█ ███ ███ ███ ███ ███ ███ ███▌ ▄ -# ▀█ █▀ ▄█████████▀ ▄████████▀ ▄████▀ ▀██████▀ ▀██████▀ █████▄▄██ -# __________________________________________________________________________________ -# NBSTool is a tool to work with .nbs (Note Block Studio) files. -# Author: IoeCmcomc (https://github.com/IoeCmcomc) -# Programming language: Python -# License: MIT license -# Version: 0.7.0 -# Source codes are hosted on: GitHub (https://github.com/IoeCmcomc/NBSTool) -# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ - - -import sys, math - - -class Attr: - def __init__(self, value=None, parent=None): - self._value_ = value - self._parent_ = parent - - def __eq__(self, other): - return self._value_ == other - - def __ne__(self, other): - return self._value_ != other - - def __lt__(self, other): - return self._value_ < other - - def __gt__(self, other): - return self._value_ > other - - def __le__(self, other): - return self._value_ <= other - - def __ge__(self, other): - return self._value_ >= other - - def __pos__(self): - return +self._value_ - - def __neg__(self): - return -self._value_ - - def __abs__(self): - return abs(self._value_) - - def __invert__(self): - return self._value_.__invert__() - - def __round__(self, n): - return round(self._value_, n) - - def __floor__(self): - return math.floor(self._value_) - - def __ceil__(self): - return math.ceil(self._value_) - - def __trunc__(self): - return math.trunc(self._value_) - - def __add__(self, other): - return self._value_ + other - - def __sub__(self, other): - return self._value_ - other - - def __mul__(self, other): - return self._value_ * other - - def __floordiv__(self, other): - return self._value_ // other - - def __div__(self, other): - return self._value_ / other - - def __truediv__(self, other): - return self._value_ / other - - def __mod__(self, other): - return self._value_ % other - - def __divmod__(self, other): - return divmod(self._value_, other) - - def __pow__(self, other): - return self._value_ ** other - - def __lshift__(self, other): - return self._value_ << other - - def __rshift__(self, other): - return self._value_ >> other - - def __and__(self, other): - return self._value_ & other - - def __or__(self, other): - return self._value_ | other - - def __xor__(self, other): - return self._value_ ^ other - - def __radd__(self, other): - return other + self._value_ - - def __rsub__(self, other): - return other - self._value_ - - def __rmul__(self, other): - return other * self._value_ - - def __rfloordiv__(self, other): - return other // self._value_ - - def __rdiv__(self, other): - return other / self._value_ - - def __rtruediv__(self, other): - return other / self._value_ - - def __rmod__(self, other): - return other % self._value_ - - def __rdivmod__(self, other): - return divmod(other, self._value_) - - def __rpow__(self, other): - return other ** self._value_ - - def __rlshift__(self, other): - return other << self._value_ - - def __rrshift__(self, other): - return other >> self._value_ - - def __rand__(self, other): - return other & self._value_ - - def __ror__(self, other): - return other | self._value_ - - def __rxor__(self, other): - return other ^ self._value_ - - def __iadd__(self, other): - return self._value_ + other - - def __isub__(self, other): - return self._value_ - other - - def __imul__(self, other): - return self._value_ * other - - def __ifloordiv__(self, other): - return self._value_ // other - - def __idiv__(self, other): - return self._value_ / other - - def __itruediv__(self, other): - return self._value_ / other - - def __imod__(self, other): - return self._value_ % other - - def __idivmod__(self, other): - return divmod(self._value_, other) - - def __ipow__(self, other): - return self._value_ ** other - - def __ilshift__(self, other): - return self._value_ << other - - def __irshift__(self, other): - return self._value_ >> other - - def __iand__(self, other): - return self._value_ & other - - def __ior__(self, other): - return self._value_ | other - - def __ixor__(self, other): - return self._value_ ^ other - - def __int__(self): - return int(self._value_) - - # def __long__(self): - # return long(self._value_) - - def __float__(self): - return float(self._value_) - - def __complex__(self): - return complex(self._value_) - - def __oct__(self): - return oct(self._value_) - - def __hex__(self): - return hex(self._value_) - - def __index__(self): - return self._value_ - - def __str__(self): - return str(self._value_) - - def __repr__(self): - # return repr(self._value_) - reprStr = "".format( - hex(id(self)), repr(self._value_.__class__.__name__)) - return reprStr - - # def __unicode__(self): - # return unicode(self._value_) - - def __format__(self, formatstr): - pattern = '{0:'+formatstr+'}' - return pattern.format(self._value_) - - def __hash__(self): - return hash(self._value_) - - def __nonzero__(self): - return bool(self._value_) - - def __dir__(self): - return super().__dir__() - - def __sizeof__(self): - return sys.getsizeof(self._value_) - - def __setattr__(self, name, value): - if name[:1] == '_': - if len(name) == 1: - setattr(self, '_value_', value) - else: - super().__setattr__(name, value) - else: - super().__setattr__(name, Attr(value, self)) - - def __getattr__(self, name): - valueAttr = getattr(self._value_, name, None) - if 'method' in valueAttr.__class__.__name__ or 'function' in valueAttr.__class__.__name__: - return valueAttr - elif name == '_': - - return self._value_ - else: - setattr(self, name, None) - return getattr(self, name) - - def __len__(self): - return len(self._value_) - - def __getitem__(self, key): - return self._value_[key] - - def __setitem__(self, key, value): - self._value_[key] = value - - def __delitem__(self, key): - del self._value_[key] - - def __iter__(self): - return iter(self._value_) - - def __reversed__(self): - return reversed(self._value_) - - def __contains__(self, item): - return item in self._value_ - ''' - def __missing__(self, key): - return super().__missing__(key) - - def __call__(self, value=None, parent=None): - print('called') - return self._value_ - if len(sys.argv) > 1: - if self._value_ is not value: self._value_ = value - if self._parent_ is not parent: self._parent_ = parent''' diff --git a/datapackexportdialog.ui b/datapackexportdialog.ui new file mode 100644 index 0000000..e87fb44 --- /dev/null +++ b/datapackexportdialog.ui @@ -0,0 +1,61 @@ + + + + 100 + true + Datapack export - NBSTool + 200 + + + 200 + 200 + + true + both + 10 + 5 + True + top + + + + Unique scorebroad ID: + + 5 + 5 + True + top + + + + + + true + key + + x + 5 + 5 + True + top + + + + + + export + disabled + Export + 10 + + 5 + 5 + True + top + + + + + + + diff --git a/main.py b/main.py index ba605b0..93bb825 100644 --- a/main.py +++ b/main.py @@ -51,20 +51,35 @@ from nbsio import readnbs, writenbs, DataPostprocess, NBS_VERSION from ncfio import writencf +vaniNoteSounds = [ + {'filename': 'harp.ogg', 'name': 'harp'}, + {'filename': 'dbass.ogg', 'name': 'bass'}, + {'filename': 'bdrum.ogg', 'name': 'basedrum'}, + {'filename': 'sdrum.ogg', 'name': 'snare'}, + {'filename': 'click.ogg', 'name': 'hat'}, + {'filename': 'guitar.ogg', 'name': 'guitar'}, + {'filename': 'flute.ogg', 'name': 'flute'}, + {'filename': 'bell.ogg', 'name': 'bell'}, + {'filename': 'icechime.ogg', 'name': 'chime'}, + {'filename': 'xylobone.ogg', 'name': 'xylophone'}, + {'filename': 'iron_xylophone.ogg', 'name': 'iron_xylophone'}, + {'filename': 'cow_bell.ogg', 'name': 'cow_bell'}, + {'filename': 'didgeridoo.ogg', 'name': 'didgeridoo'}, + {'filename': 'bit.ogg', 'name': 'bit'}, + {'filename': 'banjo.ogg', 'name': 'banjo'}, + {'filename': 'pling.ogg', 'name': 'pling'} +] + # Credit: https://stackoverflow.com/questions/42474560/pyinstaller-single-exe-file-ico-image-in-title-of-tkinter-main-window def resource_path(*args): - if len(args) > 1: - relative_path = os.path.join(*args) - else: - relative_path = args[0] if getattr(sys, 'frozen', False): datadir = os.path.dirname(sys.executable) - r = os.path.join(datadir, relative_path) + r = os.path.join(datadir, *args) else: try: - r = os.path.join(sys._MEIPASS, relative_path) + r = os.path.join(sys._MEIPASS, *args) except Exception: - r = os.path.join(os.path.abspath("."), relative_path) + r = os.path.join(os.path.abspath("."), *args) # print(r) return r @@ -79,8 +94,11 @@ def __init__(self): print('='*20) self.mainwin = builder.get_object('mainFrame') + self.fileTable = builder.get_object('fileTable') + applyBtn = builder.get_object('applyBtn') + self.toplevel.title("NBS Tool") - WindowGeo(self.toplevel, 550, 550) + WindowGeo(self.toplevel) self.style = ttk.Style() # self.style.theme_use("default") try: @@ -97,24 +115,23 @@ def __init__(self): self.initMenuBar() self.initFormatTab() self.windowBind() - - applyBtn = builder.get_object('applyBtn') def on_fileTable_select(event): + selectionLen = len(event.widget.selection()) selectionNotEmpty = len(event.widget.selection()) > 0 if selectionNotEmpty: + self.fileMenu.entryconfig(1, state="normal") builder.get_object('saveFilesBtn')["state"] = "normal" builder.get_object('removeEntriesBtn')["state"] = "normal" applyBtn["state"] = "normal" else: + self.fileMenu.entryconfig(1, state="disabled") builder.get_object('saveFilesBtn')["state"] = "disabled" builder.get_object('removeEntriesBtn')["state"] = "disabled" applyBtn["state"] = "disabled" + self.exportMenu.entryconfig(1, state="normal" if selectionLen == 1 else "disable") - fileTable = builder.get_object('fileTable') - fileTable.bind("<>", on_fileTable_select) - - applyBtn.configure(command=self.applyTool) + self.fileTable.bind("<>", on_fileTable_select) builder.connect_callbacks(self) @@ -138,9 +155,9 @@ def initMenuBar(self): self.fileMenu.add_command( label="Open", accelerator="Ctrl+O", command=self.openFiles) self.fileMenu.add_command( - label="Save", accelerator="Ctrl+S", command=self.saveAll) + label="Save", accelerator="Ctrl+S", command=self.saveAll, state="disabled") self.fileMenu.add_command( - label="Save all", accelerator="Ctrl+Shift+S", command=self.saveAll) + label="Save all", accelerator="Ctrl+Shift+S", command=self.saveAll, state="disabled") self.fileMenu.add_separator() self.fileMenu.add_command( label="Quit", accelerator="Esc", command=self.onClose) @@ -149,7 +166,10 @@ def initMenuBar(self): self.menuBar.add_cascade(label="Import", menu=self.importMenu) self.exportMenu = tk.Menu(menuBar, tearoff=False) - self.menuBar.add_cascade(label="Export", menu=self.importMenu) + self.menuBar.add_cascade(label="Export", menu=self.exportMenu) + + self.exportMenu.add_command( + label="Export as datapack...", command=self.callDatapackExportDialog, state="disabled") self.helpMenu = tk.Menu(menuBar, tearoff=False) self.menuBar.add_cascade(label="Help", menu=self.helpMenu) @@ -158,17 +178,18 @@ def initMenuBar(self): label="About") def openFiles(self, _=None): - fileTable = self.builder.get_object('fileTable') - fileTable.delete(*fileTable.get_children()) + self.fileTable.delete(*self.fileTable.get_children()) self.filePaths.clear() self.songsData.clear() self.addFiles() - def addFiles(self, _=None): + def addFiles(self, _=None, paths=None): types = [('Note Block Studio files', '*.nbs'), ('All files', '*')] - addedPaths = askopenfilenames(filetypes=types) + if len(paths) > 0: + addedPaths = paths + else: + addedPaths = askopenfilenames(filetypes=types) if len(addedPaths) == 0: return - fileTable = self.builder.get_object('fileTable') for filePath in addedPaths: try: songData = readnbs(filePath) @@ -182,13 +203,14 @@ def addFiles(self, _=None): name = headers['name'] author = headers['author'] orig_author = headers['orig_author'] - fileTable.insert("", 'end', text=filePath, values=(length, name, author, orig_author)) + self.fileTable.insert("", 'end', text=filePath, values=(length, name, author, orig_author)) self.mainwin.update() self.filePaths.extend(addedPaths) + self.fileMenu.entryconfig(2, state="normal" if len(self.filePaths) > 0 else "disabled") def saveFiles(self, _=None): if len(self.filePaths) == 0: return - fileTable = self.builder.get_object('fileTable') + fileTable = self.fileTable if len(selection := fileTable.selection()) > 0: if len(selection) == 1: filePath = os.path.basename(self.filePaths[fileTable.index(selection[0])]) @@ -213,7 +235,7 @@ def saveAll(self, _=None): def removeSelectedFiles(self): if len(self.filePaths) == 0: return - fileTable = self.builder.get_object('fileTable') + fileTable = self.fileTable if len(selection := fileTable.selection()) > 0: for item in reversed(selection): i = fileTable.index(item) @@ -225,28 +247,11 @@ def initFormatTab(self): combobox = self.builder.get_object('formatCombo') combobox.configure(values=("(not selected)", '4', '3', '2', '1', "Classic")) combobox.current(0) - - def GeneralTabElements(self): - fpadx, fpady = 10, 10 - padx, pady = 5, 5 - - # File metadata frame - self.FileMetaFrame = tk.LabelFrame(self.GeneralTab, text="Metadata") - self.FileMetaFrame.grid( - row=1, column=0, padx=fpadx, pady=fpady, sticky='nsew') - - self.FileMetaMess = tk.Message( - self.FileMetaFrame, text="No flie was found.") - self.FileMetaMess.pack(fill='both', expand=True, padx=padx, pady=padx) - - # More infomation frame - self.FileInfoFrame = tk.LabelFrame(self.GeneralTab, text="Infomations") - self.FileInfoFrame.grid( - row=1, column=1, padx=fpadx, pady=fpady, sticky='nsew') - - self.FileInfoMess = tk.Message( - self.FileInfoFrame, text="No flie was found.") - self.FileInfoMess.pack(fill='both', expand=True, padx=padx, pady=pady) + + def callDatapackExportDialog(self): + dialog = DatapackExportDialog(self.toplevel, self) + dialog.run() + del dialog def ToolsTabElements(self): fpadx, fpady = 10, 10 @@ -580,12 +585,10 @@ def applyTool(self): def OnApplyTool(self): self.ToolsTabButton['state'] = 'disabled' - self.UpdateProgBar(0) data = self.inputFileData ticklen = data['headers']['length'] layerlen = data['maxLayer'] instOpti = self.InstToolCombox.current() - self.UpdateProgBar(20) for note in data['notes']: # Flip if bool(self.var.tool.flip.horizontal.get()): @@ -597,7 +600,6 @@ def OnApplyTool(self): if instOpti > 0: note['inst'] = randrange( len(self.var.tool.opts)-2) if instOpti > len(self.noteSounds) else instOpti-1 - self.UpdateProgBar(30) # Reduce if bool(self.var.tool.reduce.opt2.get()) and bool(self.var.tool.reduce.opt3.get()): data['notes'] = [note for i, note in enumerate( @@ -608,7 +610,6 @@ def OnApplyTool(self): elif bool(self.var.tool.reduce.opt3.get()): data['notes'] = [data['notes'][i-1] for i, note in enumerate(data['notes']) if note['tick'] != data['notes'][i-1]['tick']] - self.UpdateProgBar(60) if bool(self.var.tool.reduce.opt1.get()): data['notes'] = sorted(data['notes'], key=operator.itemgetter( 'tick', 'inst', 'key', 'layer')) @@ -617,73 +618,17 @@ def OnApplyTool(self): data['notes'] = sorted( data['notes'], key=operator.itemgetter('tick', 'layer')) - self.UpdateProgBar(50) # Compact if bool(self.var.tool.compact.get()): data = compactNotes( data, self.var.tool.compact.opt1.get(), self.var.tool.compact.opt1_1.get()) - self.UpdateProgBar(60) # Sort notes data['notes'] = sorted( data['notes'], key=operator.itemgetter('tick', 'layer')) - self.UpdateProgBar(60) data = DataPostprocess(data) - - self.UpdateProgBar(90) - self.UpdateVar() - # self.UpdateProgBar(100) - self.RaiseFooter('Applied') - self.UpdateProgBar(-1) self.ToolsTabButton['state'] = 'normal' - def UpdateVar(self): - #print("Started updating….") - data = self.inputFileData - if data is not None: - self.ToolsTabButton['state'] = 'normal' - self.ExpSaveButton['state'] = 'normal' - if data != self.last.inputFileData: - self.UpdateProgBar(10) - self.parent.title( - '*"{}" – NBS Tool'.format(self.filePath.split('/')[-1])) - self.UpdateProgBar(20) - headers = data['headers'] - self.UpdateProgBar(30) - text = "File loaded" - self.FileMetaMess.configure(text=text) - self.UpdateProgBar(40) - text = "File loaded" - self.FileInfoMess.configure(text=text) - self.UpdateProgBar(50) - customInsts = [{'name': item['name'], 'filepath': resource_path('sounds', item['filename']), 'obj': AudioSegment.from_ogg( - resource_path('sounds', item['filename'])), 'pitch': item['pitch']} for item in data['customInsts']] - self.noteSounds = vaniNoteSounds + customInsts - self.UpdateProgBar(70) - self.var.tool.opts = opts = [ - "(not applied)"] + [x['name'] for x in self.noteSounds] + ["Random"] - self.InstToolCombox.configure(values=opts) - self.UpdateProgBar(80) - if data['maxLayer'] == 0: - text = writencf(data) - else: - text = "The song must have only 1 layer in order to export as Nokia Composer Format." - self.NCFOutput.configure(state='normal') - self.NCFOutput.delete('1.0', 'end') - self.NCFOutput.insert('end', text) - self.NCFOutput.configure(state='disabled') - self.UpdateProgBar(100) - self.last.inputFileData = copy.deepcopy(data) - self.RaiseFooter('Updated') - #print("Updated class properties…", data == self.last.inputFileData) - - self.UpdateProgBar(-1) - else: - self.ToolsTabButton['state'] = 'disabled' - self.ExpSaveButton['state'] = 'disabled' - - self.update_idletasks() - def toggleExpOptiGrp(self, n=None, m=None, y=None): asFile = bool(self.var.export.mode.get()) key = max(0, self.ExpConfigCombox.current()) @@ -754,8 +699,6 @@ def OnBrowseExp(self): def OnExport(self): if self.exportFilePath.get() is not None: - self.ExpBrowseButton['state'] = self.ExpSaveButton['state'] = 'disabled' - self.UpdateProgBar(10) data, path = self.inputFileData, self.exportFilePath.get() asFile = bool(self.var.export.mode.get()) type = self.ExpConfigCombox.current() @@ -774,37 +717,48 @@ def OnExport(self): exportDatapack(self, os.path.join( path, self.WnbsIDEntry.get()), 'wnbs') - # self.UpdateProgBar(100) - self.RaiseFooter('Exported') - self.UpdateProgBar(-1) - self.ExpBrowseButton['state'] = self.ExpSaveButton['state'] = 'normal' - def UpdateProgBar(self, value, time=0.001): - if value != self.progressbar["value"]: - if value == -1 or time < 0: - self.progressbar.pack_forget() - self.configure(cursor='arrow') - else: - self.configure(cursor='wait') - self.progressbar["value"] = value - self.progressbar.pack(side='right') - self.progressbar.update() - if time != 0: - sleep(time) - - def RaiseFooter(self, text='', color='green', hid=False): - if hid == False: - # self.RaiseFooter(hid=True) - text.replace('\s', ' ') - self.footerLabel.configure(text=text, foreground=color) - self.footerLabel.pack(side='left', fill='x') - self.after(999, lambda: self.RaiseFooter( - text=text, color=color, hid=True)) - else: - self.footerLabel.pack_forget() - self.footerLabel.update() +class DatapackExportDialog: + def __init__(self, master, parent): + self.master = master + self.parent = parent + + self.builder = builder = pygubu.Builder() + builder.add_resource_path(resource_path()) + builder.add_from_file(resource_path('datapackexportdialog.ui')) + + self.dialog = builder.get_object('dialog', master) + WindowGeo(self.dialog.toplevel) + + button = builder.get_object('exportBtn') + button.configure(command=self.export) + if len(parent.fileTable.selection()) == 0: + button["state"] = "disabled" + + def wnbsIDVaildate(P): + isVaild = bool(re.match("^(\d|\w|[-_])*$", P)) + button["state"] = "normal" if isVaild and (14 >= len(P) > 0) else "disabled" + return isVaild + + self.entry = entry = builder.get_object('IDEntry') + vcmd = (master.register(wnbsIDVaildate), '%P') + entry.configure(validatecommand=vcmd) + + builder.connect_callbacks(self) + + def run(self): + self.builder.get_object('dialog').run() + + def export(self, _=None): + self.dialog.close() + fileTable = self.parent.fileTable + index = fileTable.index(fileTable.selection()[0]) + + path = askdirectory(title="Select folder to save") + if path == '': return + exportDatapack(self.parent.songsData[index], os.path.join(path, self.entry.get()), self.entry.get(), 'wnbs') class FlexCheckbutton(tk.Checkbutton): def __init__(self, *args, **kwargs): @@ -833,26 +787,6 @@ def __init__(self, *args, **kwargs): if self.multiline: self.bind("", lambda event: self.configure(width=event.width-10, justify=self.justify, anchor=self.anchor, wraplength=event.width-20, text=self.text+' '*999)) -# Currently unused - -class SquareButton(tk.Button): - def __init__(self, *args, **kwargs): - self.blankImg = tk.PhotoImage() - - if "size" in kwargs: - # print(kwargs['size']) - self.size = kwargs['size'] - del kwargs['size'] - else: - self.size = 30 - - pprint(kwargs) - - tk.Button.__init__(self, *args, **kwargs) - - self.configure(image=self.blankImg, font=("Arial", self.size-3), - width=self.size, height=self.size, compound=tk.CENTER) - class StackingWidget(tk.Frame): def __init__(self, parent, **kwargs): @@ -872,8 +806,6 @@ def append(self, frame, key=None): key = self._i self._i += 1 self._frames[key] = [frame, None] - #self._shown = key - # return self._frames[name] def switch(self, key): for k, (w, o) in self._frames.items(): @@ -888,14 +820,13 @@ def switch(self, key): def pack(self, key=None, **opts): if key: self._frames[key][1] = opts - # self._frames[key][0].pack(**opts) else: super().pack(**opts) if len(self._frames) == 1: self.switch(key) -def WindowGeo(obj, width, height, mwidth=None, mheight=None): +def WindowGeo(obj, width=None, height=None, mwidth=None, mheight=None): # Credit: https://stackoverflow.com/questions/3129322/how-do-i-get-monitor-resolution-in-python/56913005#56913005 def get_curr_screen_size(): """ @@ -910,13 +841,12 @@ def get_curr_screen_size(): root.state('iconic') size = (root.winfo_width(), root.winfo_height(),) root.destroy() - print(size) return size ScreenWidth, ScreenHeight = get_curr_screen_size() - WindowWidth = width or obj.winfo_reqwidth() - WindowHeight = height or obj.winfo_reqheight() + WindowWidth = width or obj.winfo_width() + WindowHeight = height or obj.winfo_height() WinPosX = int(ScreenWidth / 2 - WindowWidth / 2) WinPosY = int(ScreenHeight / 2.3 - WindowHeight / 2) @@ -1113,9 +1043,6 @@ def exportMIDI(cls, path, byLayer=False): main_score[track].append(a_note) a_note.offset = time - cls.UpdateProgBar( - 10 + int(note['tick'] / (data['headers']['length']+1) * 80), 0) - mt = m21.metadata.Metadata() mt.title = mt.popularTitle = 'Title' mt.composer = 'Composer' @@ -1131,7 +1058,6 @@ def exportMIDI(cls, path, byLayer=False): for el in mid.tracks[i].events: el.channel = 10 - cls.UpdateProgBar(95) mid.open(path, 'wb') mid.write() @@ -1140,7 +1066,7 @@ def exportMIDI(cls, path, byLayer=False): #exper_s = m21.midi.translate.midiFileToStream(mid) #exper_s.write("midi", path+'_test.mid') -def exportDatapack(cls, path, mode='none'): +def exportDatapack(data, path, bname, mode=None, master=None): def writejson(path, jsout): with open(path, 'w') as f: json.dump(jsout, f, ensure_ascii=False) @@ -1150,7 +1076,6 @@ def writemcfunction(path, text): f.write(text) def makeFolderTree(inp, a=[]): - print(a) if isinstance(inp, (tuple, set)): for el in inp: makeFolderTree(el, copy.copy(a)) @@ -1165,12 +1090,10 @@ def makeFolderTree(inp, a=[]): return def wnbs(): - scoreObj = "wnbs_" + bname[:7] + scoreObj = bname[:13] speed = int(min(data['headers']['tempo'] * 4, 120)) length = data['headers']['length'] - # os.path.exists() - makeFolderTree( {path: { 'data': { @@ -1213,7 +1136,7 @@ def wnbs(): """tag @s remove {0} scoreboard players reset @s {0} scoreboard players reset @s {0}_t""".format(scoreObj)) - writemcfunction(os.path.join(path, 'data', bname, 'functions', 'remove.mcfunction'), + writemcfunction(os.path.join(path, 'data', bname, 'functions', 'uninstall.mcfunction'), """scoreboard objectives remove {0} scoreboard objectives remove {0}_t""".format(scoreObj)) @@ -1227,28 +1150,28 @@ def wnbs(): writemcfunction(os.path.join(path, 'data', bname, 'functions', 'give.mcfunction'), text) - colNotes = {tick: [x for x in data['notes'] - if x['tick'] == tick] for tick in range(length)} - # pprint(colNotes) - print(len(colNotes)) - + tick = 0 + colNotes = {tick: []} + for note in data['notes']: + colNotes[tick].append(note) + while note['tick'] != tick: + tick += 1 + colNotes[tick] = [] + for tick in range(length): - currNotes = colNotes[tick] text = "" - #text = "say Calling function {}:notes/{}\n".format(bname, tick) - for note in currNotes: - text += \ - """execute as @e[type=armor_stand, tag=WNBS_Marker, name=\"{inst}-{order}\"] at @s positioned ~ ~-1 ~ if block ~ ~ ~ minecraft:note_block[instrument={instname}] run setblock ~ ~ ~ minecraft:note_block[instrument={instname},note={key}] replace + if tick in colNotes: + currNotes = colNotes[tick] + for note in currNotes: + text += \ +"""execute as @e[type=armor_stand, tag=WNBS_Marker, name=\"{inst}-{order}\"] at @s positioned ~ ~-1 ~ if block ~ ~ ~ minecraft:note_block[instrument={instname}] run setblock ~ ~ ~ minecraft:note_block[instrument={instname},note={key}] replace execute as @e[type=armor_stand, tag=WNBS_Marker, name=\"{inst}-{order}\"] at @s positioned ~ ~-1 ~ if block ~ ~ ~ minecraft:note_block[instrument={instname}] run fill ^ ^ ^-1 ^ ^ ^-1 minecraft:redstone_block replace minecraft:air execute as @e[type=armor_stand, tag=WNBS_Marker, name=\"{inst}-{order}\"] at @s positioned ~ ~-1 ~ if block ~ ~ ~ minecraft:note_block[instrument={instname}] run fill ^ ^ ^-1 ^ ^ ^-1 minecraft:air replace minecraft:redstone_block -""".format( - obj=scoreObj, tick=tick, inst=note['inst'], order=instLayers[note['inst']].index(note['layer']), instname=noteSounds[note['inst']]['name'], key=max(33, min(57, note['key'])) - 33 - ) - if tick == length-1: - text += "execute run function {}:stop".format(bname) +""".format(obj=scoreObj, tick=tick, inst=note['inst'], order=instLayers[note['inst']].index(note['layer']), instname=noteSounds[note['inst']]['name'], key=max(33, min(57, note['key'])) - 33) + if tick < length-1: + text += "scoreboard players set @s {}_t {}".format(scoreObj, tick) else: - text += "scoreboard players set @s {}_t {}".format( - scoreObj, tick) + text += "execute run function {}:stop".format(bname) if text != "": writemcfunction(os.path.join( path, 'data', bname, 'functions', 'notes', str(tick)+'.mcfunction'), text) @@ -1268,8 +1191,6 @@ def wnbs(): min2 = lower + half max2 = lower + searchrange - 1 - #print(str(step) + " " + str(segments) + " " + str(min1) + " " + str(max1) + " " + str(min2) + " " + string(max2)) - if min1 <= length: if step == steps-1: # Last step, play the tick try: @@ -1303,7 +1224,6 @@ def wnbs(): except KeyError: break if text != "": - #text = "say Calling function {}:tree/{}_{}\n".format(bname, min1, max2) + text writemcfunction(os.path.join( path, 'data', bname, 'functions', 'tree', '{}_{}.mcfunction'.format(min1, max2)), text) else: @@ -1312,10 +1232,10 @@ def wnbs(): path = os.path.join(*os.path.normpath(path).split()) bname = os.path.basename(path) - data = DataPostprocess(cls.inputFileData) + data = DataPostprocess(data) data = compactNotes(data, groupPerc=False) - noteSounds = cls.noteSounds + noteSounds = vaniNoteSounds + data['customInsts'] instLayers = {} for note in data['notes']: @@ -1323,39 +1243,19 @@ def wnbs(): instLayers[note['inst']] = [note['layer']] elif note['layer'] not in instLayers[note['inst']]: instLayers[note['inst']].append(note['layer']) - # pprint(instLayers) - - locals()[mode]() - print("Done!") + if mode: + locals()[mode]() if __name__ == "__main__": - - vaniNoteSounds = [ - {'filename': 'harp.ogg', 'name': 'harp'}, - {'filename': 'dbass.ogg', 'name': 'bass'}, - {'filename': 'bdrum.ogg', 'name': 'basedrum'}, - {'filename': 'sdrum.ogg', 'name': 'snare'}, - {'filename': 'click.ogg', 'name': 'hat'}, - {'filename': 'guitar.ogg', 'name': 'guitar'}, - {'filename': 'flute.ogg', 'name': 'flute'}, - {'filename': 'bell.ogg', 'name': 'bell'}, - {'filename': 'icechime.ogg', 'name': 'chime'}, - {'filename': 'xylobone.ogg', 'name': 'xylophone'}, - {'filename': 'iron_xylophone.ogg', 'name': 'iron_xylophone'}, - {'filename': 'cow_bell.ogg', 'name': 'cow_bell'}, - {'filename': 'didgeridoo.ogg', 'name': 'didgeridoo'}, - {'filename': 'bit.ogg', 'name': 'bit'}, - {'filename': 'banjo.ogg', 'name': 'banjo'}, - {'filename': 'pling.ogg', 'name': 'pling'} - ] app = MainWindow() print('Creating app...') print(sys.argv) + if len(sys.argv) == 2: - app.OnOpenFile(sys.argv[1]) + app.addFiles(paths=[sys.argv[1],]) print('Ready') app.mainwin.mainloop() diff --git a/nbsio.py b/nbsio.py index c432ae6..0b631be 100644 --- a/nbsio.py +++ b/nbsio.py @@ -23,7 +23,6 @@ from collections import deque from operator import itemgetter from typing import BinaryIO -from time import time BYTE = Struct(' int: '''Read the following bytes from file and return a number.''' raw = f.read(fmt.size) rawInt = int.from_bytes(raw, byteorder='little', signed=True) - if beginInst: print("{0:<2}{1:<20}{2:<10}{3:<11}".format(fmt.size, str(raw), raw.hex(), rawInt)) + # print("{0:<2}{1:<20}{2:<10}{3:<11}".format(fmt.size, str(raw), raw.hex(), rawInt)) return fmt.unpack(raw)[0] def read_string(f: BinaryIO) -> str: @@ -47,7 +44,7 @@ def read_string(f: BinaryIO) -> str: length = read_numeric(f, INT) raw = f.read(length) - if beginInst: print("{0:<20}{1}".format(length, raw)) + # print("{0:<20}{1}".format(length, raw)) return raw.decode('unicode_escape') # ONBS doesn't support UTF-8 def readnbsheader(f: BinaryIO) -> dict: @@ -100,7 +97,7 @@ def readnbs(fn) -> dict: usedInsts = [[], []] hasPerc = False layers = deque() - customInsts = deque() + customInsts = [] appendix = None readNumeric = read_numeric readString = read_string @@ -143,7 +140,8 @@ def readnbs(fn) -> dict: if inst not in usedInsts[0]: usedInsts[0].append(inst) notes.append({'tick':tick, 'layer':layer, 'inst':inst, 'key':key, 'vel':vel, 'pan':pan, 'pitch':pitch, 'isPerc':isPerc}) maxLayer = max(layer, maxLayer) - if headers['length'] is None: headers['length'] = tick + 1 + # if headers['length'] is None: + headers['length'] = tick + 1 # indexByTick = tuple([ (tk, tuple([notes.index(nt) for nt in notes if nt['tick'] == tk]) ) for tk in range(headers['length']+1) ]) #Layers for i in range(headers['height']): @@ -207,7 +205,6 @@ def write_string(f: BinaryIO, v) -> None: def writenbs(fn: str, data: dict) -> None: '''Save nbs data to a file on disk with the path given.''' - start = time() if fn != '' and data is not None: writeNumeric = write_numeric writeString = write_string @@ -286,7 +283,6 @@ def writenbs(fn: str, data: dict) -> None: writeNumeric(f, BYTE, customInst['pressKeys']) #Press key #Appendix if 'appendix' in data: f.write(data['appendix']) - print(time() - start) if __name__ == "__main__": import sys diff --git a/toplevel.ui b/toplevel.ui index 5ea8478..19fe252 100644 --- a/toplevel.ui +++ b/toplevel.ui @@ -295,6 +295,7 @@ + applyTool arrow 1 Apply From d3b6e483079f7996cd0fd62ba53231c0c785698a Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Wed, 30 Sep 2020 16:27:30 +0700 Subject: [PATCH 07/22] refactor: Store song data in the NbsSong class - Import Addict library with Dict class, which allow values to be accessed as both keys and attributes; - Add requirements.txt file; - Run formattor. NbsSong: - Inherited from Dict; - Support reading, writting data. MainWindow: - Remove the exportMIDI function; - Clean up the DatexportDatapack function. Signed-off-by: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> --- main.py | 405 ++++++++++++++------------------------ nbsio.py | 495 +++++++++++++++++++++++++---------------------- requirements.txt | 4 + 3 files changed, 412 insertions(+), 492 deletions(-) create mode 100644 requirements.txt diff --git a/main.py b/main.py index 93bb825..5876d65 100644 --- a/main.py +++ b/main.py @@ -44,33 +44,32 @@ from datetime import timedelta from PIL import Image, ImageTk -import music21 as m21 -import music21.stream as m21s -import music21.instrument as m21i -from nbsio import readnbs, writenbs, DataPostprocess, NBS_VERSION +from nbsio import NBS_VERSION, NbsSong from ncfio import writencf vaniNoteSounds = [ - {'filename': 'harp.ogg', 'name': 'harp'}, - {'filename': 'dbass.ogg', 'name': 'bass'}, - {'filename': 'bdrum.ogg', 'name': 'basedrum'}, - {'filename': 'sdrum.ogg', 'name': 'snare'}, - {'filename': 'click.ogg', 'name': 'hat'}, - {'filename': 'guitar.ogg', 'name': 'guitar'}, - {'filename': 'flute.ogg', 'name': 'flute'}, - {'filename': 'bell.ogg', 'name': 'bell'}, - {'filename': 'icechime.ogg', 'name': 'chime'}, - {'filename': 'xylobone.ogg', 'name': 'xylophone'}, - {'filename': 'iron_xylophone.ogg', 'name': 'iron_xylophone'}, - {'filename': 'cow_bell.ogg', 'name': 'cow_bell'}, - {'filename': 'didgeridoo.ogg', 'name': 'didgeridoo'}, - {'filename': 'bit.ogg', 'name': 'bit'}, - {'filename': 'banjo.ogg', 'name': 'banjo'}, - {'filename': 'pling.ogg', 'name': 'pling'} + {'filename': 'harp.ogg', 'name': 'harp'}, + {'filename': 'dbass.ogg', 'name': 'bass'}, + {'filename': 'bdrum.ogg', 'name': 'basedrum'}, + {'filename': 'sdrum.ogg', 'name': 'snare'}, + {'filename': 'click.ogg', 'name': 'hat'}, + {'filename': 'guitar.ogg', 'name': 'guitar'}, + {'filename': 'flute.ogg', 'name': 'flute'}, + {'filename': 'bell.ogg', 'name': 'bell'}, + {'filename': 'icechime.ogg', 'name': 'chime'}, + {'filename': 'xylobone.ogg', 'name': 'xylophone'}, + {'filename': 'iron_xylophone.ogg', 'name': 'iron_xylophone'}, + {'filename': 'cow_bell.ogg', 'name': 'cow_bell'}, + {'filename': 'didgeridoo.ogg', 'name': 'didgeridoo'}, + {'filename': 'bit.ogg', 'name': 'bit'}, + {'filename': 'banjo.ogg', 'name': 'banjo'}, + {'filename': 'pling.ogg', 'name': 'pling'} ] # Credit: https://stackoverflow.com/questions/42474560/pyinstaller-single-exe-file-ico-image-in-title-of-tkinter-main-window + + def resource_path(*args): if getattr(sys, 'frozen', False): datadir = os.path.dirname(sys.executable) @@ -93,12 +92,12 @@ def __init__(self): self.toplevel = builder.get_object('toplevel') print('='*20) self.mainwin = builder.get_object('mainFrame') - + self.fileTable = builder.get_object('fileTable') applyBtn = builder.get_object('applyBtn') - + self.toplevel.title("NBS Tool") - WindowGeo(self.toplevel) + centerToplevel(self.toplevel) self.style = ttk.Style() # self.style.theme_use("default") try: @@ -109,7 +108,7 @@ def __init__(self): self.style.theme_use("winnative") except Exception: pass - + builder.import_variables(self) self.initMenuBar() @@ -129,12 +128,13 @@ def on_fileTable_select(event): builder.get_object('saveFilesBtn')["state"] = "disabled" builder.get_object('removeEntriesBtn')["state"] = "disabled" applyBtn["state"] = "disabled" - self.exportMenu.entryconfig(1, state="normal" if selectionLen == 1 else "disable") - + self.exportMenu.entryconfig( + 1, state="normal" if selectionLen == 1 else "disable") + self.fileTable.bind("<>", on_fileTable_select) - + builder.connect_callbacks(self) - + self.mainwin.lift() self.mainwin.focus_force() self.mainwin.grab_set() @@ -148,7 +148,7 @@ def initMenuBar(self): # 'File' menu self.menuBar = menuBar = self.builder.get_object('menubar') self.toplevel.configure(menu=menuBar) - + self.fileMenu = tk.Menu(menuBar, tearoff=False) self.menuBar.add_cascade(label="File", menu=self.fileMenu) @@ -161,13 +161,13 @@ def initMenuBar(self): self.fileMenu.add_separator() self.fileMenu.add_command( label="Quit", accelerator="Esc", command=self.onClose) - + self.importMenu = tk.Menu(menuBar, tearoff=False) self.menuBar.add_cascade(label="Import", menu=self.importMenu) - + self.exportMenu = tk.Menu(menuBar, tearoff=False) self.menuBar.add_cascade(label="Export", menu=self.exportMenu) - + self.exportMenu.add_command( label="Export as datapack...", command=self.callDatapackExportDialog, state="disabled") @@ -182,72 +182,88 @@ def openFiles(self, _=None): self.filePaths.clear() self.songsData.clear() self.addFiles() - - def addFiles(self, _=None, paths=None): + + def addFiles(self, _=None, paths=()): types = [('Note Block Studio files', '*.nbs'), ('All files', '*')] if len(paths) > 0: addedPaths = paths else: addedPaths = askopenfilenames(filetypes=types) - if len(addedPaths) == 0: return + if len(addedPaths) == 0: + return for filePath in addedPaths: try: - songData = readnbs(filePath) + songData = NbsSong(filePath) self.songsData.append(songData) except Exception: - showerror("Reading file error", "Cannot read or parse file: "+filePath) + showerror("Reading file error", + "Cannot read or parse file: "+filePath) print(traceback.format_exc()) continue - headers = songData['headers'] - length = timedelta(seconds=floor(headers['length'] / headers['tempo'])) if headers['length'] != None else "Not calculated" - name = headers['name'] - author = headers['author'] - orig_author = headers['orig_author'] - self.fileTable.insert("", 'end', text=filePath, values=(length, name, author, orig_author)) + header = songData['header'] + length = timedelta(seconds=floor( + header['length'] / header['tempo'])) if header['length'] != None else "Not calculated" + name = header['name'] + author = header['author'] + orig_author = header['orig_author'] + self.fileTable.insert("", 'end', text=filePath, values=( + length, name, author, orig_author)) self.mainwin.update() self.filePaths.extend(addedPaths) - self.fileMenu.entryconfig(2, state="normal" if len(self.filePaths) > 0 else "disabled") + self.fileMenu.entryconfig(2, state="normal" if len( + self.filePaths) > 0 else "disabled") def saveFiles(self, _=None): - if len(self.filePaths) == 0: return + if len(self.filePaths) == 0: + return fileTable = self.fileTable if len(selection := fileTable.selection()) > 0: if len(selection) == 1: - filePath = os.path.basename(self.filePaths[fileTable.index(selection[0])]) - types = [('Note Block Studio files', '*.nbs'), ('All files', '*')] - path = asksaveasfilename(filetypes=types, initialfile=filePath, defaultextension=".nbs") + filePath = os.path.basename( + self.filePaths[fileTable.index(selection[0])]) + types = [('Note Block Studio files', '*.nbs'), + ('All files', '*')] + path = asksaveasfilename( + filetypes=types, initialfile=filePath, defaultextension=".nbs") else: path = askdirectory(title="Select folder to save") - if path == '': return + if path == '': + return Path(path).mkdir(parents=True, exist_ok=True) for item in selection: i = fileTable.index(item) filePath = self.filePaths[i] - writenbs(os.path.join(path, os.path.basename(filePath)), self.songsData[i]) + self.songsData[i].write(os.path.join( + path, os.path.basename(filePath))) def saveAll(self, _=None): - if len(self.filePaths) == 0: return + if len(self.filePaths) == 0: + return path = askdirectory(title="Select folder to save") - if path == '': return + if path == '': + return Path(path).mkdir(parents=True, exist_ok=True) for i, filePath in enumerate(self.filePaths): - writenbs(os.path.join(path, os.path.basename(filePath)), self.songsData[i]) - + self.songsData[i].write(os.path.join( + path, os.path.basename(filePath))) + def removeSelectedFiles(self): - if len(self.filePaths) == 0: return + if len(self.filePaths) == 0: + return fileTable = self.fileTable if len(selection := fileTable.selection()) > 0: for item in reversed(selection): i = fileTable.index(item) fileTable.delete(item) del self.filePaths[i] - del self.songsData[i] + del self.songsData[i] def initFormatTab(self): combobox = self.builder.get_object('formatCombo') - combobox.configure(values=("(not selected)", '4', '3', '2', '1', "Classic")) + combobox.configure( + values=("(not selected)", '4', '3', '2', '1', "Classic")) combobox.current(0) - + def callDatapackExportDialog(self): dialog = DatapackExportDialog(self.toplevel, self) dialog.run() @@ -381,7 +397,7 @@ def ExportTabElements(self): self.var.export.type.file = \ [('Musical Instrument Digital files', '*.mid'), - ('Nokia Composer Format', '*.txt'),] + ('Nokia Composer Format', '*.txt'), ] self.var.export.type.dtp = ['Wireless note block song', 'other'] self.ExpConfigCombox = ttk.Combobox(self.ExpConfigGrp1, state='readonly', values=[ "{} ({})".format(tup[0], tup[1]) for tup in self.var.export.type.file]) @@ -491,17 +507,20 @@ def windowBind(self): '', when='tail')) mainwin.bind_class("TCombobox", "", - lambda e: e.widget.event_generate('')) + lambda e: e.widget.event_generate('')) for tkclass in ("Entry", "Text", "ScrolledText", "TCombobox"): mainwin.bind_class(tkclass, "", self.popupmenus) mainwin.bind_class("TNotebook", "<>", - self._on_tab_changed) - - mainwin.bind_class("Treeview", "", self._on_treeview_shift_down) - mainwin.bind_class("Treeview", "", self._on_treeview_shift_up) - mainwin.bind_class("Treeview", "", self._on_treeview_left_click, add='+') + self._on_tab_changed) + + mainwin.bind_class("Treeview", "", + self._on_treeview_shift_down) + mainwin.bind_class("Treeview", "", + self._on_treeview_shift_up) + mainwin.bind_class("Treeview", "", + self._on_treeview_left_click, add='+') # Credit: http://code.activestate.com/recipes/580726-tkinter-notebook-that-fits-to-the-height-of-every-/ def _on_tab_changed(self, event): @@ -509,13 +528,14 @@ def _on_tab_changed(self, event): tab = event.widget.nametowidget(event.widget.select()) event.widget.configure(height=tab.winfo_reqheight()) - + # Credit: https://stackoverflow.com/questions/57939932/treeview-how-to-select-multiple-rows-using-cursor-up-and-down-keys def _on_treeview_shift_down(self, event): tree = event.widget cur_item = tree.focus() next_item = tree.next(cur_item) - if next_item == '': return 'break' + if next_item == '': + return 'break' selection = tree.selection() if next_item in selection: tree.selection_remove(cur_item) @@ -524,12 +544,13 @@ def _on_treeview_shift_down(self, event): tree.selection_add(next_item) tree.focus(next_item) tree.see(next_item) - + def _on_treeview_shift_up(self, event): tree = event.widget cur_item = tree.focus() prev_item = tree.prev(cur_item) - if prev_item == '': return 'break' + if prev_item == '': + return 'break' selection = tree.selection() if prev_item in selection: tree.selection_remove(cur_item) @@ -538,12 +559,12 @@ def _on_treeview_shift_up(self, event): tree.selection_add(prev_item) tree.focus(prev_item) tree.see(prev_item) - + def _on_treeview_left_click(self, event): tree = event.widget if tree.identify_row(event.y) == '': tree.selection_set(tuple()) - + def popupmenus(self, event): w = event.widget self.popupMenu = tk.Menu(self.mainwin, tearoff=False) @@ -578,15 +599,15 @@ def applyTool(self): songsData = self.songsData if (formatComboIndex := builder.get_object('formatCombo').current()) > 0: outputVersion = (NBS_VERSION + 1) - formatComboIndex - selectedIndexes = (fileTable.index(item) for item in fileTable.selection()) + selectedIndexes = (fileTable.index(item) + for item in fileTable.selection()) for i in selectedIndexes: - songsData[i]['headers']['file_version'] = outputVersion - + songsData[i]['header']['file_version'] = outputVersion def OnApplyTool(self): self.ToolsTabButton['state'] = 'disabled' data = self.inputFileData - ticklen = data['headers']['length'] + ticklen = data['header']['length'] layerlen = data['maxLayer'] instOpti = self.InstToolCombox.current() for note in data['notes']: @@ -626,7 +647,7 @@ def OnApplyTool(self): data['notes'] = sorted( data['notes'], key=operator.itemgetter('tick', 'layer')) - data = DataPostprocess(data) + data.correctData() self.ToolsTabButton['state'] = 'normal' def toggleExpOptiGrp(self, n=None, m=None, y=None): @@ -724,41 +745,45 @@ class DatapackExportDialog: def __init__(self, master, parent): self.master = master self.parent = parent - + self.builder = builder = pygubu.Builder() builder.add_resource_path(resource_path()) builder.add_from_file(resource_path('datapackexportdialog.ui')) self.dialog = builder.get_object('dialog', master) - WindowGeo(self.dialog.toplevel) - + centerToplevel(self.dialog.toplevel) + button = builder.get_object('exportBtn') button.configure(command=self.export) if len(parent.fileTable.selection()) == 0: button["state"] = "disabled" - + def wnbsIDVaildate(P): isVaild = bool(re.match("^(\d|\w|[-_])*$", P)) - button["state"] = "normal" if isVaild and (14 >= len(P) > 0) else "disabled" + button["state"] = "normal" if isVaild and ( + 14 >= len(P) > 0) else "disabled" return isVaild - + self.entry = entry = builder.get_object('IDEntry') vcmd = (master.register(wnbsIDVaildate), '%P') entry.configure(validatecommand=vcmd) - + builder.connect_callbacks(self) - + def run(self): self.builder.get_object('dialog').run() - + def export(self, _=None): self.dialog.close() fileTable = self.parent.fileTable index = fileTable.index(fileTable.selection()[0]) - + path = askdirectory(title="Select folder to save") - if path == '': return - exportDatapack(self.parent.songsData[index], os.path.join(path, self.entry.get()), self.entry.get(), 'wnbs') + if path == '': + return + exportDatapack(self.parent.songsData[index], os.path.join( + path, self.entry.get()), self.entry.get(), 'wnbs') + class FlexCheckbutton(tk.Checkbutton): def __init__(self, *args, **kwargs): @@ -788,6 +813,7 @@ def __init__(self, *args, **kwargs): self.bind("", lambda event: self.configure(width=event.width-10, justify=self.justify, anchor=self.anchor, wraplength=event.width-20, text=self.text+' '*999)) + class StackingWidget(tk.Frame): def __init__(self, parent, **kwargs): super().__init__(parent, **kwargs) @@ -826,7 +852,7 @@ def pack(self, key=None, **opts): self.switch(key) -def WindowGeo(obj, width=None, height=None, mwidth=None, mheight=None): +def centerToplevel(obj, width=None, height=None, mwidth=None, mheight=None): # Credit: https://stackoverflow.com/questions/3129322/how-do-i-get-monitor-resolution-in-python/56913005#56913005 def get_curr_screen_size(): """ @@ -914,157 +940,8 @@ def compactNotes(data, sepInst=1, groupPerc=1): def exportMIDI(cls, path, byLayer=False): - data = copy.deepcopy(cls.inputFileData) - byLayer = bool(byLayer) - - if not byLayer: - data = DataPostprocess(data) - data = compactNotes(data) - - UniqInstEachLayer = {} - for note in data['notes']: - if note['layer'] not in UniqInstEachLayer: - if note['isPerc']: - UniqInstEachLayer[note['layer']] = -1 - else: - UniqInstEachLayer[note['layer']] = note['inst'] - else: - if not note['isPerc']: - note['inst'] = UniqInstEachLayer[note['layer']] - - lenTrack = data['maxLayer'] + 1 - for i in range(lenTrack): - if i not in UniqInstEachLayer: - UniqInstEachLayer[i] = 0 - - main_score = m21s.Score() - - percussions = ( - #(percussion_key, instrument, key) - (35, 2, 64), - (36, 2, 60), - (37, 4, 60), - (38, 3, 62), - #(39, 4, 60), - (40, 3, 58), - #(41, 2, 60), - (42, 3, 76), - (43, 2, 67), - #(44, 3, 76), - (45, 2, 69), - (46, 2, 72), - (47, 2, 74), - (48, 2, 77), - (49, 2, 71), - (50, 3, 77), - (51, 3, 78), - (52, 3, 62), - (53, 3, 67), - (54, 3, 72), - (55, 3, 73), - (56, 4, 55), - #(57, 3, 67), - (58, 4, 56), - #(59, 3, 67), - (60, 4, 63), - (61, 4, 57), - (62, 4, 62), - (63, 2, 76), - (64, 3, 69), - #(65, 3, 67), - #(66, 3, 62), - #(67, 4, 62), - (68, 4, 58), - (69, 4, 74), - (70, 4, 77), - (73, 3, 71), - (74, 4, 65), - (75, 4, 72), - (76, 4, 64), - (77, 4, 59), - (80, 4, 71), - (81, 4, 76), - (82, 3, 78) - ) - - instrument_codes = {-1: m21i.Percussion, - 0: m21i.Harp, - 1: m21i.AcousticBass, - 5: m21i.Guitar, - 6: m21i.Flute, - 7: m21i.Handbells, - 8: m21i.ChurchBells, - 9: m21i.Xylophone, - 10: m21i.Xylophone, - 11: m21i.Piano, - 12: m21i.Piano, - 13: m21i.Piano, - 14: m21i.Banjo, - 15: m21i.Piano, - } - - timeSign = data['headers']['time_sign'] - time = 0 - tempo = data['headers']['tempo'] * 60 / timeSign - volume = 127 - - c = 0 - for i in range(lenTrack): - staff = m21s.Part() - staff.append(m21.tempo.MetronomeMark(number=tempo)) # Tempo - staff.timeSignature = m21.meter.TimeSignature( - '{}/4'.format(timeSign)) # Time signature - staff.clef = m21.clef.TrebleClef() # Clef - try: - staff.append(instrument_codes[UniqInstEachLayer[i]]()) - except KeyError: - staff.append(m21i.Piano()) - main_score.append(staff) - - for i, note in enumerate(data['notes']): - time = note['tick'] / timeSign - pitch = note['key'] + 21 - track = note['layer'] - - if note['isPerc']: - for a, b, c in percussions: - if c == pitch and b == note['inst']: - pitch = a - - if byLayer: - volume = int(data['layers'][note['layer']]['volume'] / 100 * 127) - - #print("track: {}, channel: {}, pitch: {}, time: {}, duration: {}, volume: {}".format(track, channel, pitch, time, duration, volume)) - - a_note = m21.note.Note() - a_note.pitch.midi = pitch # Pitch - a_note.duration.quarterLength = 1 / 4 # Duration - a_note.volume = volume - main_score[track].append(a_note) - a_note.offset = time - - mt = m21.metadata.Metadata() - mt.title = mt.popularTitle = 'Title' - mt.composer = 'Composer' - main_score.insert(0, mt) - - #fn = main_score.write("midi", path) - - mid = m21.midi.translate.streamToMidiFile(main_score) - - if data['hasPerc']: - for i in range(lenTrack): - if UniqInstEachLayer[i] == -1: - for el in mid.tracks[i].events: - el.channel = 10 - + pass - mid.open(path, 'wb') - mid.write() - mid.close() - - #exper_s = m21.midi.translate.midiFileToStream(mid) - #exper_s.write("midi", path+'_test.mid') def exportDatapack(data, path, bname, mode=None, master=None): def writejson(path, jsout): @@ -1089,10 +966,24 @@ def makeFolderTree(inp, a=[]): else: return - def wnbs(): - scoreObj = bname[:13] - speed = int(min(data['headers']['tempo'] * 4, 120)) - length = data['headers']['length'] + path = os.path.join(*os.path.normpath(path).split()) + bname = os.path.basename(path) + + data.correctData() + data = compactNotes(data, groupPerc=False) + + noteSounds = vaniNoteSounds + data['customInsts'] + + instLayers = {} + for note in data['notes']: + if note['inst'] not in instLayers: + instLayers[note['inst']] = [note['layer']] + elif note['layer'] not in instLayers[note['inst']]: + instLayers[note['inst']].append(note['layer']) + + scoreObj = bname[:13] + speed = int(min(data['header']['tempo'] * 4, 120)) + length = data['header']['length'] makeFolderTree( {path: { @@ -1157,19 +1048,20 @@ def wnbs(): while note['tick'] != tick: tick += 1 colNotes[tick] = [] - + for tick in range(length): text = "" if tick in colNotes: currNotes = colNotes[tick] for note in currNotes: text += \ -"""execute as @e[type=armor_stand, tag=WNBS_Marker, name=\"{inst}-{order}\"] at @s positioned ~ ~-1 ~ if block ~ ~ ~ minecraft:note_block[instrument={instname}] run setblock ~ ~ ~ minecraft:note_block[instrument={instname},note={key}] replace + """execute as @e[type=armor_stand, tag=WNBS_Marker, name=\"{inst}-{order}\"] at @s positioned ~ ~-1 ~ if block ~ ~ ~ minecraft:note_block[instrument={instname}] run setblock ~ ~ ~ minecraft:note_block[instrument={instname},note={key}] replace execute as @e[type=armor_stand, tag=WNBS_Marker, name=\"{inst}-{order}\"] at @s positioned ~ ~-1 ~ if block ~ ~ ~ minecraft:note_block[instrument={instname}] run fill ^ ^ ^-1 ^ ^ ^-1 minecraft:redstone_block replace minecraft:air execute as @e[type=armor_stand, tag=WNBS_Marker, name=\"{inst}-{order}\"] at @s positioned ~ ~-1 ~ if block ~ ~ ~ minecraft:note_block[instrument={instname}] run fill ^ ^ ^-1 ^ ^ ^-1 minecraft:air replace minecraft:redstone_block """.format(obj=scoreObj, tick=tick, inst=note['inst'], order=instLayers[note['inst']].index(note['layer']), instname=noteSounds[note['inst']]['name'], key=max(33, min(57, note['key'])) - 33) if tick < length-1: - text += "scoreboard players set @s {}_t {}".format(scoreObj, tick) + text += "scoreboard players set @s {}_t {}".format( + scoreObj, tick) else: text += "execute run function {}:stop".format(bname) if text != "": @@ -1229,33 +1121,16 @@ def wnbs(): else: break - path = os.path.join(*os.path.normpath(path).split()) - bname = os.path.basename(path) - - data = DataPostprocess(data) - data = compactNotes(data, groupPerc=False) - - noteSounds = vaniNoteSounds + data['customInsts'] - - instLayers = {} - for note in data['notes']: - if note['inst'] not in instLayers: - instLayers[note['inst']] = [note['layer']] - elif note['layer'] not in instLayers[note['inst']]: - instLayers[note['inst']].append(note['layer']) - - if mode: - locals()[mode]() if __name__ == "__main__": - + app = MainWindow() print('Creating app...') print(sys.argv) if len(sys.argv) == 2: - app.addFiles(paths=[sys.argv[1],]) + app.addFiles(paths=[sys.argv[1], ]) print('Ready') app.mainwin.mainloop() diff --git a/nbsio.py b/nbsio.py index 0b631be..79254d9 100644 --- a/nbsio.py +++ b/nbsio.py @@ -24,6 +24,8 @@ from operator import itemgetter from typing import BinaryIO +from addict import Dict + BYTE = Struct(' str: # print("{0:<20}{1}".format(length, raw)) return raw.decode('unicode_escape') # ONBS doesn't support UTF-8 -def readnbsheader(f: BinaryIO) -> dict: - '''Read a .nbs file header from a file object''' - - headers = {} - headers['length'] = None - readNumeric = read_numeric - readString = read_string +class NbsSong(Dict): + def __init__(self, f=None): + self.header = Dict({ + 'file_version': 4, + 'vani_inst': 10, + 'length': 0, + 'length': 0, + 'height': 0, + 'name': '', + 'author': '', + 'orig_author': '', + 'description': '', + 'tempo': 10, + 'time_sign': 4, + 'minutes_spent': 0, + 'left_clicks': 0, + 'right_clicks': 0, + 'block_added': 0, + 'block_removed': 0, + 'import_name': '', + 'loop': False, + 'loop_max': 0, + 'loop_start': 0, + }) + self.notes = deque() + self.layers = deque() + self.customInsts = [] + self.appendix = None + if f: self.read(f) + + def __repr__(self): + return "".format(len(self.notes), len(self.layers), len(self.customInsts)) - #Header - first = readNumeric(f, SHORT) #Sign - if first != 0: #File is old type - headers['file_version'] = file_version = 0 - headers['vani_inst'] = 10 - headers['length'] = first - else: #File is new type - headers['file_version'] = file_version = readNumeric(f, BYTE) #Version - if file_version > NBS_VERSION: - raise NotImplementedError("This format version ({}) is not supported.".format(file_version)) - headers['vani_inst'] = readNumeric(f, BYTE) - if file_version >= 3: - headers['length'] = readNumeric(f, SHORT) - headers['height'] = readNumeric(f, SHORT) #Height - headers['name'] = readString(f) #Name - headers['author'] = readString(f) #Author - headers['orig_author'] = readString(f) #OriginalAuthor - headers['description'] = readString(f) #Description - headers['tempo'] = readNumeric(f, SHORT)/100 #Tempo - headers['auto-saving'] = readNumeric(f, BYTE) == 1 #Auto-saving enabled - headers['auto-saving_time'] = readNumeric(f, BYTE) #Auto-saving duration - headers['time_sign'] = readNumeric(f, BYTE) #Time signature - headers['minutes_spent'] = readNumeric(f, INT) #Minutes spent - headers['left_clicks'] = readNumeric(f, INT) #Left clicks - headers['right_clicks'] = readNumeric(f, INT) #Right clicks - headers['block_added'] = readNumeric(f, INT) #Total block added - headers['block_removed'] = readNumeric(f, INT) #Total block removed - headers['import_name'] = readString(f) #MIDI file name - if file_version >= 4: - headers['loop'] = readNumeric(f, BYTE) #Loop enabled - headers['loop_max'] = readNumeric(f, BYTE) #Max loop count - headers['loop_start'] = readNumeric(f, SHORT) #Loop start tick - return headers - -def readnbs(fn) -> dict: - '''Read a .nbs file from disk or URL.''' + def readHeader(self, f: BinaryIO) -> None: + '''Read a .nbs file header from a file object''' - notes = deque() - maxLayer = 0 - usedInsts = [[], []] - hasPerc = False - layers = deque() - customInsts = [] - appendix = None - readNumeric = read_numeric - readString = read_string + header = Dict() + header['length'] = None + readNumeric = read_numeric + readString = read_string + + #Header + first = readNumeric(f, SHORT) #Sign + if first != 0: #File is old type + header['file_version'] = file_version = 0 + header['vani_inst'] = 10 + header['length'] = first + else: #File is new type + header['file_version'] = file_version = readNumeric(f, BYTE) #Version + if file_version > NBS_VERSION: + raise NotImplementedError("This format version ({}) is not supported.".format(file_version)) + header['vani_inst'] = readNumeric(f, BYTE) + if file_version >= 3: + header['length'] = readNumeric(f, SHORT) + header['height'] = readNumeric(f, SHORT) #Height + header['name'] = readString(f) #Name + header['author'] = readString(f) #Author + header['orig_author'] = readString(f) #OriginalAuthor + header['description'] = readString(f) #Description + header['tempo'] = readNumeric(f, SHORT)/100 #Tempo + header['auto_save'] = readNumeric(f, BYTE) == 1 #auto_save enabled + header['auto_save_time'] = readNumeric(f, BYTE) #auto_save duration + header['time_sign'] = readNumeric(f, BYTE) #Time signature + header['minutes_spent'] = readNumeric(f, INT) #Minutes spent + header['left_clicks'] = readNumeric(f, INT) #Left clicks + header['right_clicks'] = readNumeric(f, INT) #Right clicks + header['block_added'] = readNumeric(f, INT) #Total block added + header['block_removed'] = readNumeric(f, INT) #Total block removed + header['import_name'] = readString(f) #MIDI file name + if file_version >= 4: + header['loop'] = readNumeric(f, BYTE) #Loop enabled + header['loop_max'] = readNumeric(f, BYTE) #Max loop count + header['loop_start'] = readNumeric(f, SHORT) #Loop start tick + self.header = header - if fn != '': - if fn.__class__.__name__ == 'HTTPResponse': - f = fn - else: - f = open(fn, "rb") - try: - headers = readnbsheader(f) - file_version = headers['file_version'] - #Notes - tick = -1 - tickJumps = layerJumps = 0 - while True: - tickJumps = readNumeric(f, SHORT) - if tickJumps == 0: break - tick += tickJumps - layer = -1 + def read(self, fn) -> None: + '''Read a .nbs file from disk or URL.''' + + notes = deque() + maxLayer = 0 + usedInsts = [[], []] + hasPerc = False + layers = deque() + customInsts = [] + appendix = None + readNumeric = read_numeric + readString = read_string + + if fn != '': + if fn.__class__.__name__ == 'HTTPResponse': + f = fn + else: + f = open(fn, "rb") + try: + self.readHeader(f) + header = self.header + file_version = header.file_version + #Notes + tick = -1 + tickJumps = layerJumps = 0 while True: - layerJumps = readNumeric(f, SHORT) - if layerJumps == 0: break - layer += layerJumps - inst = readNumeric(f, BYTE) - key = readNumeric(f, BYTE)#+21 - if file_version >= 4: - vel = readNumeric(f, BYTE) - pan = readNumeric(f, BYTE) - pitch = readNumeric(f, SHORT_SIGNED) + tickJumps = readNumeric(f, SHORT) + if tickJumps == 0: break + tick += tickJumps + layer = -1 + while True: + layerJumps = readNumeric(f, SHORT) + if layerJumps == 0: break + layer += layerJumps + inst = readNumeric(f, BYTE) + key = readNumeric(f, BYTE)#+21 + if file_version >= 4: + vel = readNumeric(f, BYTE) + pan = readNumeric(f, BYTE) + pitch = readNumeric(f, SHORT_SIGNED) + else: + vel = 100 + pan = 100 + pitch = 0 + if inst in {2, 3, 4}: + hasPerc = isPerc = True + if inst not in usedInsts[1]: usedInsts[1].append(inst) + else: + isPerc = False + if inst not in usedInsts[0]: usedInsts[0].append(inst) + notes.append(Dict({'tick':tick, 'layer':layer, 'inst':inst, 'key':key, 'vel':vel, 'pan':pan, 'pitch':pitch, 'isPerc':isPerc})) + maxLayer = max(layer, maxLayer) + # if header['length'] is None: + header['length'] = tick + 1 + # indexByTick = tuple([ (tk, tuple([notes.index(nt) for nt in notes if nt['tick'] == tk]) ) for tk in range(header['length']+1) ]) + #Layers + for i in range(header['height']): + name = readString(f) #Layer name + if header['file_version'] >= 4: + lock = readNumeric(f, BYTE) == 1 #Lock else: - vel = 100 - pan = 100 - pitch = 0 - if inst in {2, 3, 4}: - hasPerc = isPerc = True - if inst not in usedInsts[1]: usedInsts[1].append(inst) - else: - isPerc = False - if inst not in usedInsts[0]: usedInsts[0].append(inst) - notes.append({'tick':tick, 'layer':layer, 'inst':inst, 'key':key, 'vel':vel, 'pan':pan, 'pitch':pitch, 'isPerc':isPerc}) - maxLayer = max(layer, maxLayer) - # if headers['length'] is None: - headers['length'] = tick + 1 - # indexByTick = tuple([ (tk, tuple([notes.index(nt) for nt in notes if nt['tick'] == tk]) ) for tk in range(headers['length']+1) ]) - #Layers - for i in range(headers['height']): - name = readString(f) #Layer name - if headers['file_version'] >= 4: - lock = readNumeric(f, BYTE) == 1 #Lock - else: - lock = False - vol = readNumeric(f, BYTE) #Volume - vol = 100 if vol == -1 else vol - stereo = readNumeric(f, BYTE) if file_version >= 2 else 100 #Stereo - layers.append({'name':name, 'lock':lock, 'volume':vol, 'stereo':stereo}) - name = vol = stereo = None - #Custom instrument - headers['inst_count'] = readNumeric(f, BYTE) - for i in range(headers['inst_count']): - name = readString(f) #Instrument name - file = readString(f) #Sound fn - pitch = readNumeric(f, BYTE) #Pitch - shouldPressKeys = bool(readNumeric(f, BYTE)) #Press key - customInsts.append({'name':name, 'fn':file, 'pitch':pitch, 'pressKeys':shouldPressKeys}) - #Rest of the file - appendix = f.read() - finally: - try: - f.close() - except: - pass - data = {'headers':headers, 'notes':notes, 'layers':layers, 'customInsts':customInsts, 'hasPerc':hasPerc, 'maxLayer':maxLayer, 'usedInsts':(tuple(usedInsts[0]), tuple(usedInsts[1])), } - if appendix: data['appendix'] = appendix - return data + lock = False + vol = readNumeric(f, BYTE) #Volume + vol = 100 if vol == -1 else vol + stereo = readNumeric(f, BYTE) if file_version >= 2 else 100 #Stereo + layers.append(Dict({'name':name, 'lock':lock, 'volume':vol, 'stereo':stereo})) + name = vol = stereo = None + #Custom instrument + header['inst_count'] = readNumeric(f, BYTE) + for i in range(header['inst_count']): + name = readString(f) #Instrument name + file = readString(f) #Sound fn + pitch = readNumeric(f, BYTE) #Pitch + shouldPressKeys = bool(readNumeric(f, BYTE)) #Press key + customInsts.append(Dict({'name':name, 'fn':file, 'pitch':pitch, 'pressKeys':shouldPressKeys})) + #Rest of the file + appendix = f.read() + finally: + try: + f.close() + except: + pass + self.notes = notes + self.layers = layers + self.customInsts = customInsts + self.hasPerc = hasPerc + self.maxLayer = maxLayer + self.usedInsts = (tuple(usedInsts[0]), tuple(usedInsts[1])) + if appendix: self.appendix = appendix + + def correctData(self) -> None: + notes = self.notes + usedInsts = [[], []] + maxLayer = 0 + self.hasPerc = False + for i, note in enumerate(notes): + tick, inst, layer = note['tick'], note['inst'], note['layer'] + if inst in {2, 3, 4}: + self.hasPerc = note['isPerc'] = True + if inst not in usedInsts[1]: usedInsts[1].append(inst) + else: + note['isPerc'] = False + if inst not in usedInsts[0]: usedInsts[0].append(inst) + maxLayer = max(layer, maxLayer) + self['header']['length'] = tick + self['maxLayer'] = maxLayer + self['usedInsts'] = (tuple(usedInsts[0]), tuple(usedInsts[1])) + # self['indexByTick'] = tuple([ (tk, set([notes.index(nt) for nt in notes if nt['tick'] == tk]) ) for tk in range(header['length']+1) ]) -def DataPostprocess(data: dict) -> dict: - # headers = data['headers'] - notes = data['notes'] - usedInsts = [[], []] - maxLayer = 0 - data['hasPerc'] = False - for i, note in enumerate(notes): - tick, inst, layer = note['tick'], note['inst'], note['layer'] - if inst in {2, 3, 4}: - data['hasPerc'] = note['isPerc'] = True - if inst not in usedInsts[1]: usedInsts[1].append(inst) - else: - note['isPerc'] = False - if inst not in usedInsts[0]: usedInsts[0].append(inst) - maxLayer = max(layer, maxLayer) - data['headers']['length'] = tick - data['maxLayer'] = maxLayer - data['usedInsts'] = (tuple(usedInsts[0]), tuple(usedInsts[1])) - # data['indexByTick'] = tuple([ (tk, set([notes.index(nt) for nt in notes if nt['tick'] == tk]) ) for tk in range(headers['length']+1) ]) - return data + def write(self, fn: str) -> None: + '''Save nbs data to a file on disk with the path given.''' + + if fn != '' and self.header and self.notes: + writeNumeric = write_numeric + writeString = write_string + self.correctData() + header, notes, layers, customInsts = \ + self['header'], self['notes'], self['layers'], self['customInsts'] + file_version = header['file_version'] + + with open(fn, "wb") as f: + #Header + if file_version != 0: + writeNumeric(f, SHORT, 0) + writeNumeric(f, BYTE, file_version) #Version + writeNumeric(f, BYTE, header.get('vani_inst', 10)) + if (file_version == 0) or (file_version >= 3): + writeNumeric(f, SHORT, header['length']) #Length + writeNumeric(f, SHORT, header['height']) #Height + writeString(f, header['name']) #Name + writeString(f, header['author']) #Author + writeString(f, header['orig_author']) #OriginalAuthor + writeString(f, header['description']) #Description + writeNumeric(f, SHORT, int(header['tempo']*100)) #Tempo + writeNumeric(f, BYTE, header['auto_save']) #auto_save enabled + writeNumeric(f, BYTE, header['auto_save_time']) #auto_save duration + writeNumeric(f, BYTE, header['time_sign']) #Time signature + writeNumeric(f, INT, header['minutes_spent']) #Minutes spent + writeNumeric(f, INT, header['left_clicks']) #Left clicks + writeNumeric(f, INT, header['right_clicks']) #Right clicks + writeNumeric(f, INT, header['block_added']) #Total block added + writeNumeric(f, INT, header['block_removed']) #Total block removed + writeString(f, header['import_name']) #MIDI file name + if file_version >= 4: + writeNumeric(f, BYTE, header.get('loop', False)) #Loop enabled + writeNumeric(f, BYTE, header.get('loop_max', 0)) #Max loop count + writeNumeric(f, SHORT, header.get('loop_start', 0)) #Loop start tick + #Notes + sortedNotes = sorted(notes, key = itemgetter('tick', 'layer') ) + tick = layer = -1 + fstNote = sortedNotes[0] + for note in sortedNotes: + if tick != note['tick']: + if note != fstNote: + writeNumeric(f, SHORT, 0) + layer = -1 + writeNumeric(f, SHORT, note['tick'] - tick) + tick = note['tick'] + if layer != note['layer']: + writeNumeric(f, SHORT, note['layer'] - layer) + layer = note['layer'] + writeNumeric(f, BYTE, note['inst']) + writeNumeric(f, BYTE, note['key'])#-21 + if file_version >= 4: + writeNumeric(f, BYTE, note.get('vel', 100)) + writeNumeric(f, BYTE, note.get('pan', 100)) + writeNumeric(f, SHORT_SIGNED, note.get('pitch', 0)) + # writeNumeric(f, SHORT, 0) + # writeNumeric(f, SHORT, 0) + writeNumeric(f, INT, 0) + #Layers + for layer in layers: + writeString(f, layer['name']) #Layer name + if file_version >= 4: + writeNumeric(f, BYTE, layer.get('lock', False)) #Lock + writeNumeric(f, BYTE, layer['volume']) #Volume + if file_version >= 2: + writeNumeric(f, BYTE, layer.get('stereo', 100)) #Stereo + #Custom instrument + if len(customInsts) == 0: writeNumeric(f, BYTE, 0) + else: + writeNumeric(f, BYTE, len(customInsts)) + if len(customInsts) > 0: + for customInst in customInsts: + writeString(f, customInst['name']) #Instrument name + writeString(f, customInst['fn']) #Sound fn + writeNumeric(f, BYTE, customInst['pitch']) #Pitch + writeNumeric(f, BYTE, customInst['pressKeys']) #Press key + #Appendix + if 'appendix' in data: f.write(data['appendix']) def write_numeric(f: BinaryIO, fmt: Struct, v) -> None: f.write(fmt.pack(v)) @@ -201,92 +321,13 @@ def write_numeric(f: BinaryIO, fmt: Struct, v) -> None: def write_string(f: BinaryIO, v) -> None: write_numeric(f, INT, len(v)) f.write(v.encode()) - -def writenbs(fn: str, data: dict) -> None: - '''Save nbs data to a file on disk with the path given.''' - - if fn != '' and data is not None: - writeNumeric = write_numeric - writeString = write_string - data = DataPostprocess(data) - headers, notes, layers, customInsts = \ - data['headers'], data['notes'], data['layers'], data['customInsts'] - file_version = headers['file_version'] - - with open(fn, "wb") as f: - #Header - if file_version != 0: - writeNumeric(f, SHORT, 0) - writeNumeric(f, BYTE, file_version) #Version - writeNumeric(f, BYTE, headers.get('vani_inst', 10)) - if (file_version == 0) or (file_version >= 3): - writeNumeric(f, SHORT, headers['length']) #Length - writeNumeric(f, SHORT, headers['height']) #Height - writeString(f, headers['name']) #Name - writeString(f, headers['author']) #Author - writeString(f, headers['orig_author']) #OriginalAuthor - writeString(f, headers['description']) #Description - writeNumeric(f, SHORT, int(headers['tempo']*100)) #Tempo - writeNumeric(f, BYTE, headers['auto-saving']) #Auto-saving enabled - writeNumeric(f, BYTE, headers['auto-saving_time']) #Auto-saving duration - writeNumeric(f, BYTE, headers['time_sign']) #Time signature - writeNumeric(f, INT, headers['minutes_spent']) #Minutes spent - writeNumeric(f, INT, headers['left_clicks']) #Left clicks - writeNumeric(f, INT, headers['right_clicks']) #Right clicks - writeNumeric(f, INT, headers['block_added']) #Total block added - writeNumeric(f, INT, headers['block_removed']) #Total block removed - writeString(f, headers['import_name']) #MIDI file name - if file_version >= 4: - writeNumeric(f, BYTE, headers.get('loop', False)) #Loop enabled - writeNumeric(f, BYTE, headers.get('loop_max', 0)) #Max loop count - writeNumeric(f, SHORT, headers.get('loop_start', 0)) #Loop start tick - #Notes - sortedNotes = sorted(notes, key = itemgetter('tick', 'layer') ) - tick = layer = -1 - fstNote = sortedNotes[0] - for note in sortedNotes: - if tick != note['tick']: - if note != fstNote: - writeNumeric(f, SHORT, 0) - layer = -1 - writeNumeric(f, SHORT, note['tick'] - tick) - tick = note['tick'] - if layer != note['layer']: - writeNumeric(f, SHORT, note['layer'] - layer) - layer = note['layer'] - writeNumeric(f, BYTE, note['inst']) - writeNumeric(f, BYTE, note['key'])#-21 - if file_version >= 4: - writeNumeric(f, BYTE, note.get('vel', 100)) - writeNumeric(f, BYTE, note.get('pan', 100)) - writeNumeric(f, SHORT_SIGNED, note.get('pitch', 0)) - # writeNumeric(f, SHORT, 0) - # writeNumeric(f, SHORT, 0) - writeNumeric(f, INT, 0) - #Layers - for layer in layers: - writeString(f, layer['name']) #Layer name - if file_version >= 4: - writeNumeric(f, BYTE, layer.get('lock', False)) #Lock - writeNumeric(f, BYTE, layer['volume']) #Volume - if file_version >= 2: - writeNumeric(f, BYTE, layer.get('stereo', 100)) #Stereo - #Custom instrument - if len(customInsts) == 0: writeNumeric(f, BYTE, 0) - else: - writeNumeric(f, BYTE, len(customInsts)) - if len(customInsts) > 0: - for customInst in customInsts: - writeString(f, customInst['name']) #Instrument name - writeString(f, customInst['fn']) #Sound fn - writeNumeric(f, BYTE, customInst['pitch']) #Pitch - writeNumeric(f, BYTE, customInst['pressKeys']) #Press key - #Appendix - if 'appendix' in data: f.write(data['appendix']) if __name__ == "__main__": import sys - if len(sys.argv) == 2: in_ra = True - else: in_ra = sys.argv[2] - data = readnbs(sys.argv[1]) - if in_ra: pprint(data) \ No newline at end of file + if len(sys.argv) > 1: + if len(sys.argv) == 2: in_ra = True + else: in_ra = sys.argv[2] + data = NbsSong(sys.argv[1]) + if in_ra: pprint(data) + pprint(NbsSong()) + print(NbsSong('Hold The Line.nbs')) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0889e38 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +addict==2.3.0 +Pillow==7.0.0 +pygubu==0.10.2 +pygubu-designer==0.10 \ No newline at end of file From ffc68c043d782f6fb66dd6ad059648bed6c061cc Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Sat, 10 Oct 2020 15:23:07 +0700 Subject: [PATCH 08/22] feat: Add flip functionalities, cleanup NbsSong MainWindow: - Add horizontal. and vertical flip functionalities. NbsSong: - correctData() to make the data consistent; - Sort notes using sortNotes(). Signed-off-by: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> --- main.py | 47 +++++++++++++++++++++++++++++------ nbsio.py | 41 +++++++++++++++---------------- toplevel.ui | 71 +++++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 122 insertions(+), 37 deletions(-) diff --git a/main.py b/main.py index 5876d65..f40c51f 100644 --- a/main.py +++ b/main.py @@ -113,6 +113,7 @@ def __init__(self): self.initMenuBar() self.initFormatTab() + self.initFlipTab() self.windowBind() def on_fileTable_select(event): @@ -200,7 +201,7 @@ def addFiles(self, _=None, paths=()): "Cannot read or parse file: "+filePath) print(traceback.format_exc()) continue - header = songData['header'] + header = songData.header length = timedelta(seconds=floor( header['length'] / header['tempo'])) if header['length'] != None else "Not calculated" name = header['name'] @@ -225,6 +226,8 @@ def saveFiles(self, _=None): ('All files', '*')] path = asksaveasfilename( filetypes=types, initialfile=filePath, defaultextension=".nbs") + self.songsData[fileTable.index(selection[0])].write(path) + return else: path = askdirectory(title="Select folder to save") if path == '': @@ -264,6 +267,10 @@ def initFormatTab(self): values=("(not selected)", '4', '3', '2', '1', "Classic")) combobox.current(0) + def initFlipTab(self): + self.builder.get_object('flipHorizontallyCheck').deselect() + self.builder.get_object('flipVerticallyCheck').deselect() + def callDatapackExportDialog(self): dialog = DatapackExportDialog(self.toplevel, self) dialog.run() @@ -595,14 +602,39 @@ def toggleCompactToolOpt(self, id=1): def applyTool(self): builder = self.builder - fileTable = builder.get_object('fileTable') + builder.get_object('applyBtn')['state'] = 'disabled' + fileTable = self.fileTable songsData = self.songsData + selectedIndexes = (fileTable.index(item) + for item in fileTable.selection()) + + outputVersion = -1 + if (formatComboIndex := builder.get_object('formatCombo').current()) > 0: outputVersion = (NBS_VERSION + 1) - formatComboIndex - selectedIndexes = (fileTable.index(item) - for item in fileTable.selection()) - for i in selectedIndexes: - songsData[i]['header']['file_version'] = outputVersion + for i in selectedIndexes: + songData = songsData[i] + length = songData.header['length'] + maxLayer = songData.maxLayer + + if outputVersion > -1: + songData.header['file_version'] = outputVersion + for note in songData.notes: + if bool(self.flipHorizontallyCheckVar.get()): + note['tick'] = length - note['tick'] + if bool(self.flipVerticallyCheckVar.get()): + note['layer'] = maxLayer - note['layer'] + + if self.arrangeMode.get(): + for note in songData.notes: + if self.arrangeMode.get() == 'collapse': + pass + + songData.sortNotes() + + builder.get_object('applyBtn')['state'] = 'normal' + + print('Applied!') def OnApplyTool(self): self.ToolsTabButton['state'] = 'disabled' @@ -644,8 +676,7 @@ def OnApplyTool(self): data = compactNotes( data, self.var.tool.compact.opt1.get(), self.var.tool.compact.opt1_1.get()) # Sort notes - data['notes'] = sorted( - data['notes'], key=operator.itemgetter('tick', 'layer')) + data.correctData() self.ToolsTabButton['state'] = 'normal' diff --git a/nbsio.py b/nbsio.py index 79254d9..23593e7 100644 --- a/nbsio.py +++ b/nbsio.py @@ -49,6 +49,13 @@ def read_string(f: BinaryIO) -> str: # print("{0:<20}{1}".format(length, raw)) return raw.decode('unicode_escape') # ONBS doesn't support UTF-8 +def write_numeric(f: BinaryIO, fmt: Struct, v) -> None: + f.write(fmt.pack(v)) + +def write_string(f: BinaryIO, v) -> None: + write_numeric(f, INT, len(v)) + f.write(v.encode()) + class NbsSong(Dict): def __init__(self, f=None): self.header = Dict({ @@ -206,15 +213,17 @@ def read(self, fn) -> None: f.close() except: pass - self.notes = notes - self.layers = layers - self.customInsts = customInsts - self.hasPerc = hasPerc - self.maxLayer = maxLayer - self.usedInsts = (tuple(usedInsts[0]), tuple(usedInsts[1])) + self.notes, self.layers, self.customInsts, self.hasPerc, self.maxLayer, self.usedInsts = \ + notes, layers, customInsts, hasPerc, maxLayer, (tuple(usedInsts[0]), tuple(usedInsts[1])) if appendix: self.appendix = appendix - + + def sortNotes(self) -> None: + self.notes = sorted(self.notes, key=itemgetter('tick', 'layer', 'key', 'inst')) + def correctData(self) -> None: + '''Make song data consistent.''' + + self.sortNotes() notes = self.notes usedInsts = [[], []] maxLayer = 0 @@ -272,10 +281,9 @@ def write(self, fn: str) -> None: writeNumeric(f, BYTE, header.get('loop_max', 0)) #Max loop count writeNumeric(f, SHORT, header.get('loop_start', 0)) #Loop start tick #Notes - sortedNotes = sorted(notes, key = itemgetter('tick', 'layer') ) tick = layer = -1 - fstNote = sortedNotes[0] - for note in sortedNotes: + fstNote = notes[0] + for note in notes: if tick != note['tick']: if note != fstNote: writeNumeric(f, SHORT, 0) @@ -313,14 +321,7 @@ def write(self, fn: str) -> None: writeNumeric(f, BYTE, customInst['pitch']) #Pitch writeNumeric(f, BYTE, customInst['pressKeys']) #Press key #Appendix - if 'appendix' in data: f.write(data['appendix']) - -def write_numeric(f: BinaryIO, fmt: Struct, v) -> None: - f.write(fmt.pack(v)) - -def write_string(f: BinaryIO, v) -> None: - write_numeric(f, INT, len(v)) - f.write(v.encode()) + if self.appendix: f.write(self.appendix) if __name__ == "__main__": import sys @@ -328,6 +329,4 @@ def write_string(f: BinaryIO, v) -> None: if len(sys.argv) == 2: in_ra = True else: in_ra = sys.argv[2] data = NbsSong(sys.argv[1]) - if in_ra: pprint(data) - pprint(NbsSong()) - print(NbsSong('Hold The Line.nbs')) \ No newline at end of file + if in_ra: pprint(data) \ No newline at end of file diff --git a/toplevel.ui b/toplevel.ui index 19fe252..b7d8569 100644 --- a/toplevel.ui +++ b/toplevel.ui @@ -187,7 +187,7 @@ top - + true x @@ -201,12 +201,9 @@ Format - - 1 - 2 + True - 1 - 2 + top @@ -267,8 +264,9 @@ - + Horizontally + string:flipHorizontallyCheckVar n 10 @@ -278,8 +276,9 @@ - + Vertically + string:flipVerticallyCheckVar 10 True @@ -291,6 +290,61 @@ + + + Arrange + + + 200 + 200 + + True + top + + + + Not arrange + string:arrangeMode + + nw + 5 + 5 + True + top + + + + + + Collapse all notes + collpase + string:arrangeMode + + nw + 5 + True + top + + + + + + Arrange by instruments + instruments + string:arrangeMode + + nw + 5 + 5 + True + top + + + + + + + @@ -298,6 +352,7 @@ applyTool arrow 1 + disabled Apply 10 From 78e77877b967ad91e0c498f15a90ac900480837d Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Wed, 9 Jun 2021 10:55:04 +0700 Subject: [PATCH 09/22] feat: Add arrange functionality, update max NBS version Signed-off-by: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> --- main.py | 134 ++++++++++++++++++++++++++-------------------------- nbsio.py | 50 ++++++++++---------- toplevel.ui | 3 +- 3 files changed, 95 insertions(+), 92 deletions(-) diff --git a/main.py b/main.py index f40c51f..a215651 100644 --- a/main.py +++ b/main.py @@ -264,7 +264,7 @@ def removeSelectedFiles(self): def initFormatTab(self): combobox = self.builder.get_object('formatCombo') combobox.configure( - values=("(not selected)", '4', '3', '2', '1', "Classic")) + values=("(not selected)", '5', '4', '3', '2', '1', "Classic")) combobox.current(0) def initFlipTab(self): @@ -616,26 +616,42 @@ def applyTool(self): songData = songsData[i] length = songData.header['length'] maxLayer = songData.maxLayer - + if outputVersion > -1: songData.header['file_version'] = outputVersion + for note in songData.notes: if bool(self.flipHorizontallyCheckVar.get()): note['tick'] = length - note['tick'] if bool(self.flipVerticallyCheckVar.get()): note['layer'] = maxLayer - note['layer'] - - if self.arrangeMode.get(): - for note in songData.notes: - if self.arrangeMode.get() == 'collapse': - pass - + + songData.sortNotes() + + print(self.arrangeMode.get()) + if self.arrangeMode.get() == 'collapse': + self.collapseNotes(songData.notes) + elif self.arrangeMode.get() == 'instruments': + compactNotes(songData, True) + songData.sortNotes() - + builder.get_object('applyBtn')['state'] = 'normal' - + print('Applied!') + def collapseNotes(self, notes) -> None: + layer = 0 + prevNote = {'layer': -1, 'tick': -1} + for note in notes: + if note['tick'] == prevNote['tick']: + layer += 1 + note['layer'] = layer + else: + layer = 0 + note['layer'] = layer + prevNote = note + def OnApplyTool(self): self.ToolsTabButton['state'] = 'disabled' data = self.inputFileData @@ -676,7 +692,7 @@ def OnApplyTool(self): data = compactNotes( data, self.var.tool.compact.opt1.get(), self.var.tool.compact.opt1_1.get()) # Sort notes - + data.correctData() self.ToolsTabButton['state'] = 'normal' @@ -915,60 +931,46 @@ def get_curr_screen_size(): obj.update_idletasks() -def compactNotes(data, sepInst=1, groupPerc=1): - sepInst, groupPerc = bool(sepInst), bool(groupPerc) - r = data - PrevNote = {'layer': -1, 'tick': -1} - if sepInst: - OuterLayer = 0 - iter = r['usedInsts'][0] - if not groupPerc: - iter += r['usedInsts'][1] - for inst in iter: - #print('Instrument: {}'.format(inst)) - InnerLayer = LocalLayer = c = 0 - #print('OuterLayer: {}; Innerlayer: {}; LocalLayer: {}; c: {}'.format(OuterLayer, InnerLayer, LocalLayer, c)) - for note in r['notes']: - if note['inst'] == inst: - c += 1 - if note['tick'] == PrevNote['tick']: - LocalLayer += 1 - InnerLayer = max(InnerLayer, LocalLayer) - note['layer'] = LocalLayer + OuterLayer - else: - LocalLayer = 0 - note['layer'] = LocalLayer + OuterLayer - PrevNote = note - #print('OuterLayer: {}; Innerlayer: {}; LocalLayer: {}; c: {}'.format(OuterLayer, InnerLayer, LocalLayer, c)) - OuterLayer += InnerLayer + 1 - #print('OuterLayer: {}; Innerlayer: {}; LocalLayer: {}; c: {}'.format(OuterLayer, InnerLayer, LocalLayer, c)) - if groupPerc: - InnerLayer = LocalLayer = c = 0 - for note in r['notes']: - if note['inst'] in r['usedInsts'][1]: - c += 1 - if note['tick'] == PrevNote['tick']: - LocalLayer += 1 - InnerLayer = max(InnerLayer, LocalLayer) - note['layer'] = LocalLayer + OuterLayer - else: - LocalLayer = 0 - note['layer'] = LocalLayer + OuterLayer - PrevNote = note - OuterLayer += InnerLayer + 1 - r['maxLayer'] = OuterLayer - 1 - else: - layer = 0 - for note in r['notes']: - if note['tick'] == PrevNote['tick']: - layer += 1 - note['layer'] = layer - else: - layer = 0 - note['layer'] = layer - PrevNote = note - return r - +def compactNotes(data, groupPerc=1) -> None: + groupPerc = bool(groupPerc) + prevNote = {'layer': -1, 'tick': -1} + outerLayer = 0 + it = data['usedInsts'][0] + if not groupPerc: + it += data['usedInsts'][1] + for inst in it: + #print('Instrument: {}'.format(inst)) + innerLayer = localLayer = c = 0 + #print('OuterLayer: {}; Innerlayer: {}; LocalLayer: {}; c: {}'.format(OuterLayer, InnerLayer, LocalLayer, c)) + for note in data['notes']: + if note['inst'] == inst: + c += 1 + if note['tick'] == prevNote['tick']: + localLayer += 1 + innerLayer = max(innerLayer, localLayer) + note['layer'] = localLayer + outerLayer + else: + localLayer = 0 + note['layer'] = localLayer + outerLayer + prevNote = note + #print('OuterLayer: {}; Innerlayer: {}; LocalLayer: {}; c: {}'.format(OuterLayer, InnerLayer, LocalLayer, c)) + outerLayer += innerLayer + 1 + #print('OuterLayer: {}; Innerlayer: {}; LocalLayer: {}; c: {}'.format(OuterLayer, InnerLayer, LocalLayer, c)) + if groupPerc: + innerLayer = localLayer = c = 0 + for note in data['notes']: + if note['inst'] in data['usedInsts'][1]: + c += 1 + if note['tick'] == prevNote['tick']: + localLayer += 1 + innerLayer = max(innerLayer, localLayer) + note['layer'] = localLayer + outerLayer + else: + localLayer = 0 + note['layer'] = localLayer + outerLayer + prevNote = note + outerLayer += innerLayer + 1 + data['maxLayer'] = outerLayer - 1 def exportMIDI(cls, path, byLayer=False): pass @@ -1001,7 +1003,7 @@ def makeFolderTree(inp, a=[]): bname = os.path.basename(path) data.correctData() - data = compactNotes(data, groupPerc=False) + compactNotes(data, groupPerc=False) noteSounds = vaniNoteSounds + data['customInsts'] diff --git a/nbsio.py b/nbsio.py index 23593e7..f9a1d6a 100644 --- a/nbsio.py +++ b/nbsio.py @@ -1,13 +1,13 @@ # This file is a part of: #‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -# ███▄▄▄▄ ▀█████████▄ ▄████████ ███ ▄██████▄ ▄██████▄ ▄█ -# ███▀▀▀██▄ ███ ███ ███ ███ ▀█████████▄ ███ ███ ███ ███ ███ -# ███ ███ ███ ███ ███ █▀ ▀███▀▀██ ███ ███ ███ ███ ███ -# ███ ███ ▄███▄▄▄██▀ ███ ███ ▀ ███ ███ ███ ███ ███ -# ███ ███ ▀▀███▀▀▀██▄ ▀███████████ ███ ███ ███ ███ ███ ███ -# ███ ███ ███ ██▄ ███ ███ ███ ███ ███ ███ ███ -# ███ ███ ███ ███ ▄█ ███ ███ ███ ███ ███ ███ ███▌ ▄ -# ▀█ █▀ ▄█████████▀ ▄████████▀ ▄████▀ ▀██████▀ ▀██████▀ █████▄▄██ +# ███▄▄▄▄ ▀█████████▄ ▄████████ ███ ▄██████▄ ▄██████▄ ▄█ +# ███▀▀▀██▄ ███ ███ ███ ███ ▀█████████▄ ███ ███ ███ ███ ███ +# ███ ███ ███ ███ ███ █▀ ▀███▀▀██ ███ ███ ███ ███ ███ +# ███ ███ ▄███▄▄▄██▀ ███ ███ ▀ ███ ███ ███ ███ ███ +# ███ ███ ▀▀███▀▀▀██▄ ▀███████████ ███ ███ ███ ███ ███ ███ +# ███ ███ ███ ██▄ ███ ███ ███ ███ ███ ███ ███ +# ███ ███ ███ ███ ▄█ ███ ███ ███ ███ ███ ███ ███▌ ▄ +# ▀█ █▀ ▄█████████▀ ▄████████▀ ▄████▀ ▀██████▀ ▀██████▀ █████▄▄██ #__________________________________________________________________________________ # NBSTool is a tool to work with .nbs (Note Block Studio) files. # Author: IoeCmcomc (https://github.com/IoeCmcomc) @@ -31,11 +31,11 @@ SHORT_SIGNED = Struct(' int: '''Read the following bytes from file and return a number.''' - + raw = f.read(fmt.size) rawInt = int.from_bytes(raw, byteorder='little', signed=True) # print("{0:<2}{1:<20}{2:<10}{3:<11}".format(fmt.size, str(raw), raw.hex(), rawInt)) @@ -43,7 +43,7 @@ def read_numeric(f: BinaryIO, fmt: Struct) -> int: def read_string(f: BinaryIO) -> str: '''Read the following bytes from file and return a ASCII string.''' - + length = read_numeric(f, INT) raw = f.read(length) # print("{0:<20}{1}".format(length, raw)) @@ -59,8 +59,8 @@ def write_string(f: BinaryIO, v) -> None: class NbsSong(Dict): def __init__(self, f=None): self.header = Dict({ - 'file_version': 4, - 'vani_inst': 10, + 'file_version': 5, + 'vani_inst': 16, 'length': 0, 'length': 0, 'height': 0, @@ -85,10 +85,10 @@ def __init__(self, f=None): self.customInsts = [] self.appendix = None if f: self.read(f) - + def __repr__(self): return "".format(len(self.notes), len(self.layers), len(self.customInsts)) - + def readHeader(self, f: BinaryIO) -> None: '''Read a .nbs file header from a file object''' @@ -96,7 +96,7 @@ def readHeader(self, f: BinaryIO) -> None: header['length'] = None readNumeric = read_numeric readString = read_string - + #Header first = readNumeric(f, SHORT) #Sign if first != 0: #File is old type @@ -114,7 +114,7 @@ def readHeader(self, f: BinaryIO) -> None: header['name'] = readString(f) #Name header['author'] = readString(f) #Author header['orig_author'] = readString(f) #OriginalAuthor - header['description'] = readString(f) #Description + header['description'] = readString(f) #Description header['tempo'] = readNumeric(f, SHORT)/100 #Tempo header['auto_save'] = readNumeric(f, BYTE) == 1 #auto_save enabled header['auto_save_time'] = readNumeric(f, BYTE) #auto_save duration @@ -130,7 +130,7 @@ def readHeader(self, f: BinaryIO) -> None: header['loop_max'] = readNumeric(f, BYTE) #Max loop count header['loop_start'] = readNumeric(f, SHORT) #Loop start tick self.header = header - + def read(self, fn) -> None: '''Read a .nbs file from disk or URL.''' @@ -143,7 +143,7 @@ def read(self, fn) -> None: appendix = None readNumeric = read_numeric readString = read_string - + if fn != '': if fn.__class__.__name__ == 'HTTPResponse': f = fn @@ -216,13 +216,13 @@ def read(self, fn) -> None: self.notes, self.layers, self.customInsts, self.hasPerc, self.maxLayer, self.usedInsts = \ notes, layers, customInsts, hasPerc, maxLayer, (tuple(usedInsts[0]), tuple(usedInsts[1])) if appendix: self.appendix = appendix - + def sortNotes(self) -> None: self.notes = sorted(self.notes, key=itemgetter('tick', 'layer', 'key', 'inst')) - + def correctData(self) -> None: '''Make song data consistent.''' - + self.sortNotes() notes = self.notes usedInsts = [[], []] @@ -241,7 +241,7 @@ def correctData(self) -> None: self['maxLayer'] = maxLayer self['usedInsts'] = (tuple(usedInsts[0]), tuple(usedInsts[1])) # self['indexByTick'] = tuple([ (tk, set([notes.index(nt) for nt in notes if nt['tick'] == tk]) ) for tk in range(header['length']+1) ]) - + def write(self, fn: str) -> None: '''Save nbs data to a file on disk with the path given.''' @@ -252,7 +252,7 @@ def write(self, fn: str) -> None: header, notes, layers, customInsts = \ self['header'], self['notes'], self['layers'], self['customInsts'] file_version = header['file_version'] - + with open(fn, "wb") as f: #Header if file_version != 0: @@ -265,7 +265,7 @@ def write(self, fn: str) -> None: writeString(f, header['name']) #Name writeString(f, header['author']) #Author writeString(f, header['orig_author']) #OriginalAuthor - writeString(f, header['description']) #Description + writeString(f, header['description']) #Description writeNumeric(f, SHORT, int(header['tempo']*100)) #Tempo writeNumeric(f, BYTE, header['auto_save']) #auto_save enabled writeNumeric(f, BYTE, header['auto_save_time']) #auto_save duration diff --git a/toplevel.ui b/toplevel.ui index b7d8569..525838a 100644 --- a/toplevel.ui +++ b/toplevel.ui @@ -304,6 +304,7 @@ Not arrange + none string:arrangeMode nw @@ -317,7 +318,7 @@ Collapse all notes - collpase + collapse string:arrangeMode nw From d558d98d249c164e49f2ceeba9abd1c5fa679b78 Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Wed, 9 Jun 2021 21:38:16 +0700 Subject: [PATCH 10/22] impl: Improve the NbsSong class - Process non-ascii strings before writting - Adjust some default values - Update somes header data when correcting --- nbsio.py | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/nbsio.py b/nbsio.py index f9a1d6a..8d22c91 100644 --- a/nbsio.py +++ b/nbsio.py @@ -23,16 +23,20 @@ from collections import deque from operator import itemgetter from typing import BinaryIO +import warnings +from warnings import warn from addict import Dict -BYTE = Struct(' int: '''Read the following bytes from file and return a number.''' @@ -53,21 +57,27 @@ def write_numeric(f: BinaryIO, fmt: Struct, v) -> None: f.write(fmt.pack(v)) def write_string(f: BinaryIO, v) -> None: + if not v.isascii(): + warn("The string '{}' will be written to file as '{}'.".format(v, v.encode("ascii", "replace"))) + v = v.encode("ascii", "replace") + else: + v = v.encode() write_numeric(f, INT, len(v)) - f.write(v.encode()) + f.write(v) class NbsSong(Dict): def __init__(self, f=None): self.header = Dict({ - 'file_version': 5, + 'file_version': NBS_VERSION, 'vani_inst': 16, 'length': 0, - 'length': 0, 'height': 0, 'name': '', 'author': '', 'orig_author': '', 'description': '', + 'auto_save': False, + 'auto_save_time': 0, 'tempo': 10, 'time_sign': 4, 'minutes_spent': 0, @@ -126,7 +136,7 @@ def readHeader(self, f: BinaryIO) -> None: header['block_removed'] = readNumeric(f, INT) #Total block removed header['import_name'] = readString(f) #MIDI file name if file_version >= 4: - header['loop'] = readNumeric(f, BYTE) #Loop enabled + header['loop'] = readNumeric(f, BYTE) == 1 #Loop enabled header['loop_max'] = readNumeric(f, BYTE) #Max loop count header['loop_start'] = readNumeric(f, SHORT) #Loop start tick self.header = header @@ -225,8 +235,10 @@ def correctData(self) -> None: self.sortNotes() notes = self.notes + header = self.header usedInsts = [[], []] maxLayer = 0 + tick = -1 self.hasPerc = False for i, note in enumerate(notes): tick, inst, layer = note['tick'], note['inst'], note['layer'] @@ -237,9 +249,13 @@ def correctData(self) -> None: note['isPerc'] = False if inst not in usedInsts[0]: usedInsts[0].append(inst) maxLayer = max(layer, maxLayer) - self['header']['length'] = tick - self['maxLayer'] = maxLayer - self['usedInsts'] = (tuple(usedInsts[0]), tuple(usedInsts[1])) + + header.length = tick + header.height = len(self.layers) + header.vani_inst = 16 if (len(usedInsts[0]) + len(usedInsts[1]) > 10) else 10 + self.maxLayer = maxLayer + + self.usedInsts = (tuple(usedInsts[0]), tuple(usedInsts[1])) # self['indexByTick'] = tuple([ (tk, set([notes.index(nt) for nt in notes if nt['tick'] == tk]) ) for tk in range(header['length']+1) ]) def write(self, fn: str) -> None: From d48b61cd517590fa0b6d08e01fbec53fc084f871 Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Thu, 10 Jun 2021 14:51:11 +0700 Subject: [PATCH 11/22] ui: Tweak the UI and add option to treats percussions as an instrument --- main.py | 18 ++++++++++++----- nbsio.py | 6 +++--- requirements.txt | 4 ++-- toplevel.ui | 51 +++++++++++++++++++++++++++++++++--------------- 4 files changed, 53 insertions(+), 26 deletions(-) diff --git a/main.py b/main.py index a215651..cb946c4 100644 --- a/main.py +++ b/main.py @@ -110,10 +110,12 @@ def __init__(self): pass builder.import_variables(self) + builder.connect_callbacks(self) self.initMenuBar() self.initFormatTab() self.initFlipTab() + self.initArrangeTab() self.windowBind() def on_fileTable_select(event): @@ -134,8 +136,6 @@ def on_fileTable_select(event): self.fileTable.bind("<>", on_fileTable_select) - builder.connect_callbacks(self) - self.mainwin.lift() self.mainwin.focus_force() self.mainwin.grab_set() @@ -271,6 +271,10 @@ def initFlipTab(self): self.builder.get_object('flipHorizontallyCheck').deselect() self.builder.get_object('flipVerticallyCheck').deselect() + def initArrangeTab(self): + self.builder.get_object('notArrangeRadio').select() + self.builder.get_object('arrangeGroupPrec').deselect() + def callDatapackExportDialog(self): dialog = DatapackExportDialog(self.toplevel, self) dialog.run() @@ -600,6 +604,10 @@ def toggleCompactToolOpt(self, id=1): self.CompactToolChkOpt1["state"] = "disable" if self.var.tool.compact.get( ) == 0 else "normal" + def onArrangeModeChanged(self): + self.builder.get_object('arrangeGroupPrec')['state'] = 'normal' if (self.arrangeMode.get() == 'instruments') else 'disabled' + + def applyTool(self): builder = self.builder builder.get_object('applyBtn')['state'] = 'disabled' @@ -621,9 +629,9 @@ def applyTool(self): songData.header['file_version'] = outputVersion for note in songData.notes: - if bool(self.flipHorizontallyCheckVar.get()): + if self.flipHorizontallyCheckVar.get(): note['tick'] = length - note['tick'] - if bool(self.flipVerticallyCheckVar.get()): + if self.flipVerticallyCheckVar.get(): note['layer'] = maxLayer - note['layer'] songData.sortNotes() @@ -632,7 +640,7 @@ def applyTool(self): if self.arrangeMode.get() == 'collapse': self.collapseNotes(songData.notes) elif self.arrangeMode.get() == 'instruments': - compactNotes(songData, True) + compactNotes(songData, self.groupPerc) songData.sortNotes() diff --git a/nbsio.py b/nbsio.py index 8d22c91..3d79579 100644 --- a/nbsio.py +++ b/nbsio.py @@ -76,9 +76,9 @@ def __init__(self, f=None): 'author': '', 'orig_author': '', 'description': '', - 'auto_save': False, - 'auto_save_time': 0, - 'tempo': 10, + 'auto_save': True, + 'auto_save_time': 10, + 'tempo': 1000, # 10 TPS 'time_sign': 4, 'minutes_spent': 0, 'left_clicks': 0, diff --git a/requirements.txt b/requirements.txt index 0889e38..99256ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ addict==2.3.0 Pillow==7.0.0 -pygubu==0.10.2 -pygubu-designer==0.10 \ No newline at end of file +pygubu==0.11 +pygubu-designer==0.17 \ No newline at end of file diff --git a/toplevel.ui b/toplevel.ui index 525838a..5a441a3 100644 --- a/toplevel.ui +++ b/toplevel.ui @@ -1,8 +1,8 @@ - + - 600x600 - 350|350 + 600x620 + 350|370 both false NBS Tool @@ -20,6 +20,7 @@ Files 200 + true both 5 5 @@ -29,8 +30,9 @@ both - false + true + true both 5 5 @@ -42,7 +44,6 @@ extended true - both True top @@ -112,7 +113,7 @@ - openFiles + openFiles left false Open... @@ -128,7 +129,7 @@ - saveFiles + saveFiles disabled Save... 10 @@ -143,7 +144,7 @@ - removeSelectedFiles + removeSelectedFiles disabled Remove 10 @@ -158,7 +159,7 @@ - addFiles + addFiles top false Add... @@ -176,8 +177,10 @@ + 250 nw Tools + 200 false both @@ -201,6 +204,7 @@ Format + 200 True top @@ -231,8 +235,7 @@ Changing files's format to a older version can cause data to be lost - true - both + x True top @@ -247,6 +250,7 @@ Flip + 200 True top @@ -266,7 +270,7 @@ Horizontally - string:flipHorizontallyCheckVar + boolean:flipHorizontallyCheckVar n 10 @@ -278,7 +282,7 @@ Vertically - string:flipVerticallyCheckVar + boolean:flipVerticallyCheckVar 10 True @@ -303,6 +307,7 @@ + onArrangeModeChanged Not arrange none string:arrangeMode @@ -317,6 +322,7 @@ + onArrangeModeChanged Collapse all notes collapse string:arrangeMode @@ -330,13 +336,27 @@ + onArrangeModeChanged Arrange by instruments instruments string:arrangeMode nw 5 - 5 + 0 + True + top + + + + + + disabled + Treats percussions as an instrument + boolean:groupPerc + + nw + 25 True top @@ -350,9 +370,8 @@ - applyTool + applyTool arrow - 1 disabled Apply 10 From 2faf2f5ae2105eb621c9067727d2673b771d7d1a Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Thu, 10 Jun 2021 16:10:36 +0700 Subject: [PATCH 12/22] ui: Move the menu bar from theo code to the .ui file - Fix some buttons not updating when files are removed. --- main.py | 59 ++++++++++----------------------- toplevel.ui | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 42 deletions(-) diff --git a/main.py b/main.py index cb946c4..ba2ee50 100644 --- a/main.py +++ b/main.py @@ -36,6 +36,7 @@ from tkinter.filedialog import askopenfilename, asksaveasfilename, askdirectory, askopenfilenames import pygubu +from pygubu import Builder from time import time from pprint import pprint @@ -68,8 +69,6 @@ ] # Credit: https://stackoverflow.com/questions/42474560/pyinstaller-single-exe-file-ico-image-in-title-of-tkinter-main-window - - def resource_path(*args): if getattr(sys, 'frozen', False): datadir = os.path.dirname(sys.executable) @@ -83,17 +82,18 @@ def resource_path(*args): return r -class MainWindow: +class MainWindow(): def __init__(self): - self.builder = builder = pygubu.Builder() + builder: Builder = pygubu.Builder() + self.builder: Builder = builder builder.add_from_file(resource_path('toplevel.ui')) print("The following exception is not an error:") print('='*20) - self.toplevel = builder.get_object('toplevel') + self.toplevel: tk.Toplevel = builder.get_object('toplevel') print('='*20) - self.mainwin = builder.get_object('mainFrame') + self.mainwin: tk.Frame = builder.get_object('mainFrame') - self.fileTable = builder.get_object('fileTable') + self.fileTable: ttk.Treeview = builder.get_object('fileTable') applyBtn = builder.get_object('applyBtn') self.toplevel.title("NBS Tool") @@ -109,10 +109,11 @@ def __init__(self): except Exception: pass + self.initMenuBar() + builder.import_variables(self) builder.connect_callbacks(self) - self.initMenuBar() self.initFormatTab() self.initFlipTab() self.initArrangeTab() @@ -122,17 +123,18 @@ def on_fileTable_select(event): selectionLen = len(event.widget.selection()) selectionNotEmpty = len(event.widget.selection()) > 0 if selectionNotEmpty: - self.fileMenu.entryconfig(1, state="normal") + builder.get_object('fileMenu').entryconfig(1, state="normal") builder.get_object('saveFilesBtn')["state"] = "normal" builder.get_object('removeEntriesBtn')["state"] = "normal" applyBtn["state"] = "normal" else: - self.fileMenu.entryconfig(1, state="disabled") + builder.get_object('fileMenu').entryconfig(1, state="disabled") builder.get_object('saveFilesBtn')["state"] = "disabled" builder.get_object('removeEntriesBtn')["state"] = "disabled" applyBtn["state"] = "disabled" - self.exportMenu.entryconfig( - 1, state="normal" if selectionLen == 1 else "disable") + exportMenu: tk.Menu = builder.get_object('exportMenu') + exportMenu.entryconfig(0, state="normal" if selectionLen == 1 else "disable") + exportMenu.entryconfig(1, state="normal" if selectionLen > 0 else "disable") self.fileTable.bind("<>", on_fileTable_select) @@ -146,38 +148,9 @@ def on_fileTable_select(event): self.songsData = [] def initMenuBar(self): - # 'File' menu self.menuBar = menuBar = self.builder.get_object('menubar') self.toplevel.configure(menu=menuBar) - self.fileMenu = tk.Menu(menuBar, tearoff=False) - self.menuBar.add_cascade(label="File", menu=self.fileMenu) - - self.fileMenu.add_command( - label="Open", accelerator="Ctrl+O", command=self.openFiles) - self.fileMenu.add_command( - label="Save", accelerator="Ctrl+S", command=self.saveAll, state="disabled") - self.fileMenu.add_command( - label="Save all", accelerator="Ctrl+Shift+S", command=self.saveAll, state="disabled") - self.fileMenu.add_separator() - self.fileMenu.add_command( - label="Quit", accelerator="Esc", command=self.onClose) - - self.importMenu = tk.Menu(menuBar, tearoff=False) - self.menuBar.add_cascade(label="Import", menu=self.importMenu) - - self.exportMenu = tk.Menu(menuBar, tearoff=False) - self.menuBar.add_cascade(label="Export", menu=self.exportMenu) - - self.exportMenu.add_command( - label="Export as datapack...", command=self.callDatapackExportDialog, state="disabled") - - self.helpMenu = tk.Menu(menuBar, tearoff=False) - self.menuBar.add_cascade(label="Help", menu=self.helpMenu) - - self.helpMenu.add_command( - label="About") - def openFiles(self, _=None): self.fileTable.delete(*self.fileTable.get_children()) self.filePaths.clear() @@ -186,6 +159,7 @@ def openFiles(self, _=None): def addFiles(self, _=None, paths=()): types = [('Note Block Studio files', '*.nbs'), ('All files', '*')] + addedPaths = [] if len(paths) > 0: addedPaths = paths else: @@ -211,7 +185,7 @@ def addFiles(self, _=None, paths=()): length, name, author, orig_author)) self.mainwin.update() self.filePaths.extend(addedPaths) - self.fileMenu.entryconfig(2, state="normal" if len( + self.builder.get_object('fileMenu').entryconfig(2, state="normal" if len( self.filePaths) > 0 else "disabled") def saveFiles(self, _=None): @@ -260,6 +234,7 @@ def removeSelectedFiles(self): fileTable.delete(item) del self.filePaths[i] del self.songsData[i] + fileTable.selection_remove(fileTable.selection()) def initFormatTab(self): combobox = self.builder.get_object('formatCombo') diff --git a/toplevel.ui b/toplevel.ui index 5a441a3..58a2aaf 100644 --- a/toplevel.ui +++ b/toplevel.ui @@ -399,5 +399,98 @@ false + + + File + false + 0 + + + Ctrl+O + openFiles + Open + 0 + + + + + Ctrl+S + saveFiles + Save + disabled + 0 + + + + + Ctrl+Shift+S + saveAll + Save all + disabled + 5 + + + + + + + + Esc + onClose + Quit + 0 + + + + + + + Import + false + 0 + + + callDatapackExportDialog + from MuseScore files... + 0 + + + + + + + Export + false + 0 + + + as datapack... + disabled + 0 + + + + + as MIDI file... + disabled + 3 + + + + + + + Help + help + false + 0 + + + About + 0 + + + + From 8826d6ccd371c57321f8f7416b76553c2cb7b090 Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Fri, 11 Jun 2021 22:05:12 +0700 Subject: [PATCH 13/22] feat: Show a progress dialog while applying tools to files ProgressDialog: - Consists of 2 progress bar: a current bar and a total bar - Run the async function in its loops MainWindow: - Run the code for processing in async functions and update the progress dialog when necessary - Deeply copy selected song data to process --- main.py | 156 ++++++++++++++++++++++++++++++++++++---------- progressdialog.ui | 105 +++++++++++++++++++++++++++++++ wrapmessage.py | 3 +- 3 files changed, 230 insertions(+), 34 deletions(-) create mode 100644 progressdialog.ui diff --git a/main.py b/main.py index ba2ee50..7b7a7a6 100644 --- a/main.py +++ b/main.py @@ -26,6 +26,7 @@ import traceback import re import json +import asyncio from pathlib import Path @@ -40,9 +41,10 @@ from time import time from pprint import pprint -from random import randrange +from random import randrange, randint from math import floor, log2 from datetime import timedelta +from copy import deepcopy from PIL import Image, ImageTk @@ -582,46 +584,68 @@ def toggleCompactToolOpt(self, id=1): def onArrangeModeChanged(self): self.builder.get_object('arrangeGroupPrec')['state'] = 'normal' if (self.arrangeMode.get() == 'instruments') else 'disabled' - def applyTool(self): - builder = self.builder + builder: Builder = self.builder builder.get_object('applyBtn')['state'] = 'disabled' fileTable = self.fileTable - songsData = self.songsData - selectedIndexes = (fileTable.index(item) - for item in fileTable.selection()) + changedSongData = {} + selectedIndexes: set = [fileTable.index(item) + for item in fileTable.selection()] + selectionLen = len(selectedIndexes) outputVersion = -1 if (formatComboIndex := builder.get_object('formatCombo').current()) > 0: outputVersion = (NBS_VERSION + 1) - formatComboIndex - for i in selectedIndexes: - songData = songsData[i] - length = songData.header['length'] - maxLayer = songData.maxLayer - - if outputVersion > -1: - songData.header['file_version'] = outputVersion - - for note in songData.notes: - if self.flipHorizontallyCheckVar.get(): - note['tick'] = length - note['tick'] - if self.flipVerticallyCheckVar.get(): - note['layer'] = maxLayer - note['layer'] - - songData.sortNotes() - - print(self.arrangeMode.get()) - if self.arrangeMode.get() == 'collapse': - self.collapseNotes(songData.notes) - elif self.arrangeMode.get() == 'instruments': - compactNotes(songData, self.groupPerc) - - songData.sortNotes() - - builder.get_object('applyBtn')['state'] = 'normal' - print('Applied!') + async def work(dialog: ProgressDialog = None): + print("async work() start") + try: + for i, index in enumerate(selectedIndexes): + dialog.totalProgress.set(i) + fileName = os.path.split(self.filePaths[i])[1] + dialog.currentText.set("Current file: {}".format(fileName)) + dialog.totalText.set("Processing {} / {} files".format(i+1, selectionLen)) + dialog.currentProgress.set(0) + songData = deepcopy(self.songsData[index]) + dialog.setCurrentPercentage(randint(20, 25)) + await asyncio.sleep(0.001) + dialog.currentMax = len(songData.notes)*2 + length = songData.header['length'] + maxLayer = songData.maxLayer + + if outputVersion > -1: + songData.header['file_version'] = outputVersion + if self.flipHorizontallyCheckVar.get() or self.flipVerticallyCheckVar.get(): + for note in songData.notes: + if self.flipHorizontallyCheckVar.get(): + note['tick'] = length - note['tick'] + if self.flipVerticallyCheckVar.get(): + note['layer'] = maxLayer - note['layer'] + dialog.currentProgress.set(dialog.currentProgress.get()+1) + songData.sortNotes() + if self.arrangeMode.get() == 'collapse': + self.collapseNotes(songData.notes) + elif self.arrangeMode.get() == 'instruments': + compactNotes(songData, self.groupPerc) + dialog.setCurrentPercentage(randint(75, 85)) + await asyncio.sleep(0.001) + songData.sortNotes() + changedSongData[index] = songData + await asyncio.sleep(0.001) + + for k, v in changedSongData.items(): + self.songsData[k] = v + dialog.totalProgress.set(i+1) + except asyncio.CancelledError: + raise + finally: + builder.get_object('applyBtn')['state'] = 'normal' + + dialog = ProgressDialog(self.toplevel, self) + dialog.d.toplevel.title("Applying tools to {} files".format(selectionLen)) + dialog.totalMax = selectionLen + dialog.run(work) def collapseNotes(self, notes) -> None: layer = 0 @@ -814,6 +838,74 @@ def export(self, _=None): exportDatapack(self.parent.songsData[index], os.path.join( path, self.entry.get()), self.entry.get(), 'wnbs') +class ProgressDialog: + def __init__(self, master, parent): + self.master = master + self.parent = parent + self.work = None + + self.builder = builder = pygubu.Builder() + builder.add_resource_path(resource_path()) + builder.add_from_file(resource_path('progressdialog.ui')) + + self.d = builder.get_object('dialog1', master) + self.d.toplevel.protocol('WM_DELETE_WINDOW', self.onCancel) + # centerToplevel(self.dialog.toplevel) + builder.connect_callbacks(self) + builder.import_variables(self) + + @property + def currentMax(self) -> int: + return self.builder.get_object('currentProgressBar')['maximum'] + + @currentMax.setter + def currentMax(self, value: int) -> None: + self.builder.get_object('currentProgressBar')['maximum'] = value + + @property + def totalMax(self) -> int: + return self.builder.get_object('totalProgressBar')['maximum'] + + @totalMax.setter + def totalMax(self, value: int) -> None: + self.builder.get_object('totalProgressBar')['maximum'] = value + + def run(self, func=None): + self.builder.get_object('dialog1').run() + if asyncio.iscoroutinefunction(func): + self.work = func + self.d.toplevel.after(0, self.startWork) + + def startWork(self) -> None: + if self.totalProgress.get() >= self.totalMax: + self.d.destroy() + return + asyncio.run(self.updateProgress()) + print("startWork() about to after") + self.d.toplevel.after(0, self.startWork) + + async def updateProgress(self) -> None: + print("async updateProgressDialog() start") + self.task = asyncio.create_task(self.work(dialog=self)) + while True: # wait the async task finish + done, pending = await asyncio.wait({self.task}, timeout=0) + self.d.toplevel.update() + if self.task in done: + await self.task + break + + def setCurrentPercentage(self, value: int) -> None: + self.currentProgress.set(round(self.currentMax * value / 100)) + + def onCancel(self) -> None: + try: + allTasks = asyncio.all_tasks() + for task in allTasks: + task.cancel() + except RuntimeError: # if you have cancel the task it will raise RuntimeError + pass + self.d.destroy() + class FlexCheckbutton(tk.Checkbutton): def __init__(self, *args, **kwargs): diff --git a/progressdialog.ui b/progressdialog.ui new file mode 100644 index 0000000..90f7eb3 --- /dev/null +++ b/progressdialog.ui @@ -0,0 +1,105 @@ + + + + 100 + true + 200 + + + 200 + 200 + + nw + true + both + 10 + 10 + True + top + + + + + nw + true + both + True + top + + + + + + Curent: + string:currentText + + nw + True + top + + + + + + horizontal + int:currentProgress + + nw + x + True + top + + + + + + Total: + string:totalText + + nw + True + top + + + + + + 400 + horizontal + int:totalProgress + + x + True + top + + + + + + 15 + + nw + true + both + True + top + + + + + + onCancel + 20 + Cancel + + ne + 0 + True + bottom + + + + + + + diff --git a/wrapmessage.py b/wrapmessage.py index c9fceb7..e7e83ec 100644 --- a/wrapmessage.py +++ b/wrapmessage.py @@ -39,13 +39,12 @@ def cget(self, key): return super().cget(key) def _adjustWidth(self, event): - print(self.padding) event.widget.configure(width=event.width-self.padding) if __name__ == '__main__': root = tk.Tk() msg = WrapMessage(root) - msg.configure(padding=40, text="This is a WrapMessage.") + msg.configure(padding=40, text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus gravida libero ac commodo molestie. Donec iaculis velit sem, consequat bibendum libero cursus ut. Nulla ullamcorper placerat libero malesuada dignissim. Aliquam et hendrerit erat, non aliquet mi. Ut eu urna ligula. Donec mattis sollicitudin purus. Proin tellus libero, interdum porta mauris ac, interdum gravida sapien. Proin maximus purus ut dui ultrices, eget blandit est consectetur.") msg.pack(fill='both', expand=True) root.mainloop() \ No newline at end of file From 7328d7f5131d0f5af376b6dc425d23a7c548fe9d Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Tue, 15 Jun 2021 09:59:32 +0700 Subject: [PATCH 14/22] feat: Add header information viewing functionality - If multiple files are selected, they must have the same format number and only entries which are the same are shown --- main.py | 97 +++++++++++- toplevel.ui | 423 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 507 insertions(+), 13 deletions(-) diff --git a/main.py b/main.py index 7b7a7a6..22fd99f 100644 --- a/main.py +++ b/main.py @@ -94,6 +94,9 @@ def __init__(self): self.toplevel: tk.Toplevel = builder.get_object('toplevel') print('='*20) self.mainwin: tk.Frame = builder.get_object('mainFrame') + style = ttk.Style(self.toplevel) + style.layout('Barless.TNotebook.Tab', []) # turn off tabs + style.configure('Barless.TNotebook', borderwidth=0, highlightthickness=0) self.fileTable: ttk.Treeview = builder.get_object('fileTable') applyBtn = builder.get_object('applyBtn') @@ -117,26 +120,30 @@ def __init__(self): builder.connect_callbacks(self) self.initFormatTab() + self.initHeaderTab() self.initFlipTab() self.initArrangeTab() self.windowBind() def on_fileTable_select(event): - selectionLen = len(event.widget.selection()) - selectionNotEmpty = len(event.widget.selection()) > 0 + selection: tuple = event.widget.selection() + selectionLen = len(selection) + selectionNotEmpty = selectionLen > 0 if selectionNotEmpty: builder.get_object('fileMenu').entryconfig(1, state="normal") builder.get_object('saveFilesBtn')["state"] = "normal" builder.get_object('removeEntriesBtn')["state"] = "normal" + self.updateHeaderNotebook([event.widget.index(item) for item in selection]) applyBtn["state"] = "normal" else: builder.get_object('fileMenu').entryconfig(1, state="disabled") builder.get_object('saveFilesBtn')["state"] = "disabled" builder.get_object('removeEntriesBtn')["state"] = "disabled" + builder.get_object('headerNotebook').select(0) applyBtn["state"] = "disabled" exportMenu: tk.Menu = builder.get_object('exportMenu') exportMenu.entryconfig(0, state="normal" if selectionLen == 1 else "disable") - exportMenu.entryconfig(1, state="normal" if selectionLen > 0 else "disable") + exportMenu.entryconfig(1, state="normal" if selectionNotEmpty else "disable") self.fileTable.bind("<>", on_fileTable_select) @@ -149,6 +156,74 @@ def on_fileTable_select(event): self.filePaths = [] self.songsData = [] + def isInteger(self, value) -> bool: + print("isInteger", value, value == '' or value.isdigit()) + return value == '' or value.isdigit() + + def selectedFilesVersion(self, selection: tuple) -> int: + fileVersion = -1 + for i in selection: + header = self.songsData[i].header + ver: int = header.file_version + if (ver != fileVersion) and (fileVersion != -1): + return -1 + else: + fileVersion = ver + return fileVersion + + def updateHeaderNotebook(self, selection: tuple) -> None: + def updateCheckbutton(i: int, var: tk.StringVar, widget: ttk.Checkbutton, value: bool) -> bool: + ret = i > 0 and var.get() != str(value) + if ret: + widget.state(['alternate']) + else: + var.set(value) + return not ret + + def updateSpinbox(i: int, var: tk.StringVar, value: int) -> None: + var.set('' if (i > 0 and var.get() != str(value)) else value) + + get_object = self.builder.get_object + notebook: ttk.Notebook = self.builder.get_object('headerNotebook') + fileVersion = self.selectedFilesVersion(selection) + if fileVersion == -1: + notebook.select(0) + return + notebook.select(1) + for i, index in enumerate(selection): + header = self.songsData[index].header + if fileVersion >= 4: + loop: int = header.loop + checkBox: ttk.Checkbutton = get_object('headerLoopCheck') + get_object('headerLoopCheck')['state'] = 'normal' + for child in get_object('headerLoopFrame').winfo_children(): + if child is not checkBox: + child.configure(state='normal' if loop else 'disabled') + if updateCheckbutton(i, self.headerLoop, checkBox, loop): + updateSpinbox(i, self.headerLoopCount, header.loop_max) + updateSpinbox(i, self.headerLoopStart, header.loop_start) + else: + # Credit: https://stackoverflow.com/questions/24942760/is-there-a-way-to-gray-out-disable-a-tkinter-frame + for child in get_object('headerLoopFrame').winfo_children(): + child.configure(state='disable') + + autoSave = header.auto_save + label = get_object('headerAutosaveLabel') + spinbox = get_object('headerAutosaveSpin') + label['state'] = 'disabled' + spinbox['state'] = 'disabled' + if updateCheckbutton(i, self.headerAutosave, get_object('headerAutosaveCheck'), autoSave): + updateSpinbox(i, self.headerAutosaveInterval, header.auto_save_time) + label['state'] = 'normal' if autoSave else 'disabled' + spinbox['state'] = 'normal' if autoSave else 'disabled' + + updateSpinbox(i, self.headerMinuteSpent, header.minutes_spent) + updateSpinbox(i, self.headerLeftClicks, header.left_clicks) + updateSpinbox(i, self.headerRightClicks, header.right_clicks) + updateSpinbox(i, self.headerBlockAdded, header.block_added) + updateSpinbox(i, self.headerBlockRemoved, header.block_removed) + + def initMenuBar(self): self.menuBar = menuBar = self.builder.get_object('menubar') self.toplevel.configure(menu=menuBar) @@ -160,7 +235,7 @@ def openFiles(self, _=None): self.addFiles() def addFiles(self, _=None, paths=()): - types = [('Note Block Studio files', '*.nbs'), ('All files', '*')] + types = [("Note Block Studio files", '*.nbs'), ('All files', '*')] addedPaths = [] if len(paths) > 0: addedPaths = paths @@ -202,6 +277,8 @@ def saveFiles(self, _=None): ('All files', '*')] path = asksaveasfilename( filetypes=types, initialfile=filePath, defaultextension=".nbs") + if path == '': + return self.songsData[fileTable.index(selection[0])].write(path) return else: @@ -244,6 +321,10 @@ def initFormatTab(self): values=("(not selected)", '5', '4', '3', '2', '1', "Classic")) combobox.current(0) + def initHeaderTab(self): + self.builder.get_object('headerAutosaveCheck').state(['alternate']) + self.builder.get_object('headerLoopCheck').state(['alternate']) + def initFlipTab(self): self.builder.get_object('flipHorizontallyCheck').deselect() self.builder.get_object('flipVerticallyCheck').deselect() @@ -519,7 +600,7 @@ def _on_tab_changed(self, event): # Credit: https://stackoverflow.com/questions/57939932/treeview-how-to-select-multiple-rows-using-cursor-up-and-down-keys def _on_treeview_shift_down(self, event): - tree = event.widget + tree: ttk.Treeview = event.widget cur_item = tree.focus() next_item = tree.next(cur_item) if next_item == '': @@ -534,7 +615,7 @@ def _on_treeview_shift_down(self, event): tree.see(next_item) def _on_treeview_shift_up(self, event): - tree = event.widget + tree: ttk.Treeview = event.widget cur_item = tree.focus() prev_item = tree.prev(cur_item) if prev_item == '': @@ -589,7 +670,7 @@ def applyTool(self): builder.get_object('applyBtn')['state'] = 'disabled' fileTable = self.fileTable changedSongData = {} - selectedIndexes: set = [fileTable.index(item) + selectedIndexes = [fileTable.index(item) for item in fileTable.selection()] selectionLen = len(selectedIndexes) @@ -813,7 +894,7 @@ def __init__(self, master, parent): button["state"] = "disabled" def wnbsIDVaildate(P): - isVaild = bool(re.match("^(\d|\w|[-_])*$", P)) + isVaild = bool(re.match("^(\d|\w|[-_.])+$", P)) button["state"] = "normal" if isVaild and ( 14 >= len(P) > 0) else "disabled" return isVaild diff --git a/toplevel.ui b/toplevel.ui index 58a2aaf..1736577 100644 --- a/toplevel.ui +++ b/toplevel.ui @@ -1,7 +1,7 @@ - 600x620 + 620x690 350|370 both false @@ -182,7 +182,7 @@ Tools 200 - false + true both 5 5 @@ -193,9 +193,9 @@ true - x + both 5 - 5 + 3 True top @@ -241,6 +241,418 @@ + + + 200 + 200 + + True + top + + + + + + + + + + Header + + + 200 + 200 + + True + top + + + + 200 + Barless.TNotebook + 200 + + true + both + True + top + + + + + + 0 + 200 + 0 + flat + 200 + + True + top + + + + center + At least one file must be selected and all selected files must have the same format number in order to modify header data. + + x + 20 + 10 + True + top + + + + + + + + + + + + 200 + 5 + 5 + 200 + + True + top + + + + 200 + sw + 5 + Auto-save + 200 + + true + both + 3 + 3 + True + left + + + + Enabled + boolean:headerAutosave + + 0 + 2 + True + 0 + 1 + + + + + + disabled + Inteval: + + 0 + True + 1 + w + 1 + + + + + + 0 + 1 + disabled + string:headerAutosaveInterval + 255 + focusout + isInteger + 4 + + 1 + True + 1 + e + 0 + 1 + + + + + + + + 200 + sw + Stats + 200 + + true + both + 3 + 3 + True + left + + + + Minutes spent: + + 0 + True + 0 + w + 0 + 1 + 0 + + + + + + Left-clicks: + + 0 + True + 1 + w + 0 + 1 + + + + + + Right-clicks: + + 0 + True + 2 + w + 0 + 1 + + + + + + Note blocks added: + + 0 + True + 3 + w + 0 + 1 + + + + + + Note blocks removed: + + 0 + True + 4 + w + 0 + 1 + + + + + + 0 + 1 + string:headerMinuteSpent + 2147483647 + focusout + isInteger + 11 + + 1 + True + 0 + e + 1 + 0 + + + + + + 0 + 1 + + + string:headerLeftClicks + 2147483647 + focusout + isInteger + 11 + + 1 + True + 1 + e + 1 + + + + + + 0 + 1 + string:headerRightClicks + 2147483647 + focusout + isInteger + 11 + + 1 + True + 2 + e + 1 + + + + + + 0 + 1 + string:headerBlockAdded + 2147483647 + focusout + isInteger + 11 + + 1 + True + 3 + e + 1 + + + + + + 0 + 1 + string:headerBlockRemoved + 2147483647 + focusout + isInteger + 11 + + 1 + True + 4 + e + 1 + + + + + + + + 200 + sw + Loop + 200 + + true + both + 3 + 3 + True + left + + + + Enabled + boolean:headerLoop + + 0 + 2 + True + 0 + 1 + + + + + + Max loop count: + + 0 + True + 1 + w + 1 + + + + + + Loop start tick: + + 0 + True + 2 + sw + 1 + + + + + + 0 + 1 + string:headerLoopCount + 255 + focusout + isInteger + 4 + + 1 + True + 1 + e + 1 + + + + + + 0 + 1 + string:headerLoopStart + 32767 + + focusout + isInteger + 6 + + 1 + True + 2 + e + 1 + + + + + + + + + + + @@ -376,6 +788,7 @@ Apply 10 + y 5 5 True @@ -450,7 +863,6 @@ 0 - callDatapackExportDialog from MuseScore files... 0 @@ -464,6 +876,7 @@ 0 + callDatapackExportDialog as datapack... disabled 0 From ca0b9a45283a7454d4a6c3659677669a824069e0 Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Tue, 15 Jun 2021 15:08:16 +0700 Subject: [PATCH 15/22] Signed-off-by: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> minor: Show selected files version number in the Format tab --- main.py | 5 ++++- toplevel.ui | 41 ++++++++++++++++++++++++++--------------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/main.py b/main.py index 22fd99f..7909dd4 100644 --- a/main.py +++ b/main.py @@ -140,6 +140,7 @@ def on_fileTable_select(event): builder.get_object('saveFilesBtn')["state"] = "disabled" builder.get_object('removeEntriesBtn')["state"] = "disabled" builder.get_object('headerNotebook').select(0) + self.selectedFilesVerStr.set("No file are selected.") applyBtn["state"] = "disabled" exportMenu: tk.Menu = builder.get_object('exportMenu') exportMenu.entryconfig(0, state="normal" if selectionLen == 1 else "disable") @@ -188,7 +189,9 @@ def updateSpinbox(i: int, var: tk.StringVar, value: int) -> None: fileVersion = self.selectedFilesVersion(selection) if fileVersion == -1: notebook.select(0) + self.selectedFilesVerStr.set("Selected file(s) don't have the same version number.") return + self.selectedFilesVerStr.set("Selected file(s) format version: {: >8}".format(fileVersion if fileVersion > 0 else 'Classic')) notebook.select(1) for i, index in enumerate(selection): header = self.songsData[index].header @@ -318,7 +321,7 @@ def removeSelectedFiles(self): def initFormatTab(self): combobox = self.builder.get_object('formatCombo') combobox.configure( - values=("(not selected)", '5', '4', '3', '2', '1', "Classic")) + values=("(not selected)", *range(NBS_VERSION, 1-1, -1), "Classic")) combobox.current(0) def initHeaderTab(self): diff --git a/toplevel.ui b/toplevel.ui index 1736577..9ddf191 100644 --- a/toplevel.ui +++ b/toplevel.ui @@ -1,7 +1,7 @@ - 620x690 + 620x670 350|370 both false @@ -204,16 +204,28 @@ Format + 10 200 True top - - Changes files format to: + + No file selected. + string:selectedFilesVerStr - 5 + True + top + + + + + + horizontal + + x + 10 5 True top @@ -221,9 +233,8 @@ - - readonly - string:formatComboCurrent + + Changes files format version to: True top @@ -231,21 +242,21 @@ - - Changing files's format to a older version can cause data to be lost - + + readonly + string:formatComboCurrent - x True top - - 200 - 200 + + Changing files's format to a older version can cause data to be lost + + x True top @@ -259,7 +270,7 @@ Header - + 200 200 From 3fc23cc9129b796bb3f9e3aef111a3aa1b8dad62 Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Fri, 18 Jun 2021 09:23:40 +0700 Subject: [PATCH 16/22] feat: Support modifying files' header data MainWindow: - Disable some spinboxes when the corresponding check button is unchecked - Disable the file table and the "Apply" button when opening & saving files NbsSong: - Fix an error caused the default header not to be used when reading --- .gitignore | 7 +-- main.py | 118 ++++++++++++++++++++++++++++++++++++--------- nbsio.py | 7 +-- toplevel.ui | 136 ++++++++++++++++++++++++++-------------------------- 4 files changed, 172 insertions(+), 96 deletions(-) diff --git a/.gitignore b/.gitignore index 6a69b2c..2dd1feb 100644 --- a/.gitignore +++ b/.gitignore @@ -221,7 +221,7 @@ ClientBin/ *.publishsettings orleans.codegen.cs -# Including strong name files can present a security risk +# Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk @@ -317,7 +317,7 @@ __pycache__/ # OpenCover UI analysis results OpenCover/ -# Azure Stream Analytics local run output +# Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log @@ -326,7 +326,7 @@ ASALocalRun/ # NVidia Nsight GPU debugger configuration file *.nvuser -# MFractors (Xamarin productivity tool) working folder +# MFractors (Xamarin productivity tool) working folder .mfractor/ ## Ignore output/exported files and datapacks. @@ -334,6 +334,7 @@ ASALocalRun/ *.mid *.mp3 datapacks/ +Downloaded song/ ## Ignore other files/folders. #*.ico diff --git a/main.py b/main.py index 7909dd4..6abb6ad 100644 --- a/main.py +++ b/main.py @@ -126,14 +126,17 @@ def __init__(self): self.windowBind() def on_fileTable_select(event): - selection: tuple = event.widget.selection() + fileTable: ttk.Treeview = event.widget + if fileTable['selectmode'] == 'none': + return + selection: tuple = fileTable.selection() selectionLen = len(selection) selectionNotEmpty = selectionLen > 0 if selectionNotEmpty: builder.get_object('fileMenu').entryconfig(1, state="normal") builder.get_object('saveFilesBtn')["state"] = "normal" builder.get_object('removeEntriesBtn')["state"] = "normal" - self.updateHeaderNotebook([event.widget.index(item) for item in selection]) + self.updateHeaderNotebook([fileTable.index(item) for item in selection]) applyBtn["state"] = "normal" else: builder.get_object('fileMenu').entryconfig(1, state="disabled") @@ -156,12 +159,13 @@ def on_fileTable_select(event): self.VERSION = '0.7.0' self.filePaths = [] self.songsData = [] + self.selectedFilesVersion = -1 def isInteger(self, value) -> bool: print("isInteger", value, value == '' or value.isdigit()) return value == '' or value.isdigit() - def selectedFilesVersion(self, selection: tuple) -> int: + def getSelectedFilesVersion(self, selection: tuple) -> int: fileVersion = -1 for i in selection: header = self.songsData[i].header @@ -174,7 +178,7 @@ def selectedFilesVersion(self, selection: tuple) -> int: def updateHeaderNotebook(self, selection: tuple) -> None: def updateCheckbutton(i: int, var: tk.StringVar, widget: ttk.Checkbutton, value: bool) -> bool: - ret = i > 0 and var.get() != str(value) + ret = (i > 0) and (var.get() != value) if ret: widget.state(['alternate']) else: @@ -182,43 +186,38 @@ def updateCheckbutton(i: int, var: tk.StringVar, widget: ttk.Checkbutton, value: return not ret def updateSpinbox(i: int, var: tk.StringVar, value: int) -> None: - var.set('' if (i > 0 and var.get() != str(value)) else value) + var.set('' if ((i > 0) and (var.get() != str(value))) else value) get_object = self.builder.get_object notebook: ttk.Notebook = self.builder.get_object('headerNotebook') - fileVersion = self.selectedFilesVersion(selection) + fileVersion = self.getSelectedFilesVersion(selection) if fileVersion == -1: notebook.select(0) self.selectedFilesVerStr.set("Selected file(s) don't have the same version number.") + self.selectedFilesVersion = -1 return + self.selectedFilesVersion = fileVersion self.selectedFilesVerStr.set("Selected file(s) format version: {: >8}".format(fileVersion if fileVersion > 0 else 'Classic')) notebook.select(1) for i, index in enumerate(selection): header = self.songsData[index].header if fileVersion >= 4: - loop: int = header.loop + loop = header.loop checkBox: ttk.Checkbutton = get_object('headerLoopCheck') - get_object('headerLoopCheck')['state'] = 'normal' - for child in get_object('headerLoopFrame').winfo_children(): - if child is not checkBox: - child.configure(state='normal' if loop else 'disabled') + checkBox['state'] = 'normal' if updateCheckbutton(i, self.headerLoop, checkBox, loop): updateSpinbox(i, self.headerLoopCount, header.loop_max) updateSpinbox(i, self.headerLoopStart, header.loop_start) + self.onLoopCheckBtn() else: # Credit: https://stackoverflow.com/questions/24942760/is-there-a-way-to-gray-out-disable-a-tkinter-frame for child in get_object('headerLoopFrame').winfo_children(): child.configure(state='disable') autoSave = header.auto_save - label = get_object('headerAutosaveLabel') - spinbox = get_object('headerAutosaveSpin') - label['state'] = 'disabled' - spinbox['state'] = 'disabled' if updateCheckbutton(i, self.headerAutosave, get_object('headerAutosaveCheck'), autoSave): updateSpinbox(i, self.headerAutosaveInterval, header.auto_save_time) - label['state'] = 'normal' if autoSave else 'disabled' - spinbox['state'] = 'normal' if autoSave else 'disabled' + self.onAutosaveCheckBtn() updateSpinbox(i, self.headerMinuteSpent, header.minutes_spent) updateSpinbox(i, self.headerLeftClicks, header.left_clicks) @@ -226,11 +225,33 @@ def updateSpinbox(i: int, var: tk.StringVar, value: int) -> None: updateSpinbox(i, self.headerBlockAdded, header.block_added) updateSpinbox(i, self.headerBlockRemoved, header.block_removed) + def onAutosaveCheckBtn(self): + label = self.builder.get_object('headerAutosaveLabel') + spinbox = self.builder.get_object('headerAutosaveSpin') + state = 'normal' if ((not 'alternate' in self.builder.get_object('headerAutosaveCheck').state()) and self.headerAutosave.get()) else 'disabled' + label['state'] = state + spinbox['state'] = state + + def onLoopCheckBtn(self): + checkBox: ttk.Checkbutton = self.builder.get_object('headerLoopCheck') + loop = self.headerLoop.get() + state = 'normal' if ((not 'alternate' in self.builder.get_object('headerLoopCheck').state()) and self.headerLoop.get()) else 'disabled' + for child in self.builder.get_object('headerLoopFrame').winfo_children(): + if child is not checkBox: + child.configure(state=state) def initMenuBar(self): self.menuBar = menuBar = self.builder.get_object('menubar') self.toplevel.configure(menu=menuBar) + def disabledFileTable(self): + self.fileTable.state(('disabled',)) + self.fileTable['selectmode'] = 'none' + + def enableFileTable(self): + self.fileTable.state(('!disabled',)) + self.fileTable['selectmode'] = 'extended' + def openFiles(self, _=None): self.fileTable.delete(*self.fileTable.get_children()) self.filePaths.clear() @@ -246,6 +267,8 @@ def addFiles(self, _=None, paths=()): addedPaths = askopenfilenames(filetypes=types) if len(addedPaths) == 0: return + self.builder.get_object('applyBtn')['state'] = 'disabled' + self.disabledFileTable() for filePath in addedPaths: try: songData = NbsSong(filePath) @@ -265,6 +288,8 @@ def addFiles(self, _=None, paths=()): length, name, author, orig_author)) self.mainwin.update() self.filePaths.extend(addedPaths) + self.enableFileTable() + self.builder.get_object('applyBtn')['state'] = 'normal' self.builder.get_object('fileMenu').entryconfig(2, state="normal" if len( self.filePaths) > 0 else "disabled") @@ -282,18 +307,26 @@ def saveFiles(self, _=None): filetypes=types, initialfile=filePath, defaultextension=".nbs") if path == '': return + self.builder.get_object('applyBtn')['state'] = 'disabled' + self.disabledFileTable() + self.mainwin.update() self.songsData[fileTable.index(selection[0])].write(path) + self.enableFileTable() + self.builder.get_object('applyBtn')['state'] = 'normal' return else: path = askdirectory(title="Select folder to save") if path == '': return Path(path).mkdir(parents=True, exist_ok=True) + self.disabledFileTable() for item in selection: i = fileTable.index(item) filePath = self.filePaths[i] self.songsData[i].write(os.path.join( path, os.path.basename(filePath))) + self.enableFileTable() + self.builder.get_object('applyBtn')['state'] = 'normal' def saveAll(self, _=None): if len(self.filePaths) == 0: @@ -302,9 +335,14 @@ def saveAll(self, _=None): if path == '': return Path(path).mkdir(parents=True, exist_ok=True) + self.builder.get_object('applyBtn')['state'] = 'disabled' + self.disabledFileTable() + self.mainwin.update() for i, filePath in enumerate(self.filePaths): self.songsData[i].write(os.path.join( path, os.path.basename(filePath))) + self.enableFileTable() + self.builder.get_object('applyBtn')['state'] = 'normal' def removeSelectedFiles(self): if len(self.filePaths) == 0: @@ -669,37 +707,42 @@ def onArrangeModeChanged(self): self.builder.get_object('arrangeGroupPrec')['state'] = 'normal' if (self.arrangeMode.get() == 'instruments') else 'disabled' def applyTool(self): - builder: Builder = self.builder - builder.get_object('applyBtn')['state'] = 'disabled' + get_object = self.builder.get_object + get_object('applyBtn')['state'] = 'disabled' fileTable = self.fileTable changedSongData = {} selectedIndexes = [fileTable.index(item) for item in fileTable.selection()] selectionLen = len(selectedIndexes) - outputVersion = -1 - if (formatComboIndex := builder.get_object('formatCombo').current()) > 0: + if (formatComboIndex := get_object('formatCombo').current()) > 0: outputVersion = (NBS_VERSION + 1) - formatComboIndex async def work(dialog: ProgressDialog = None): print("async work() start") try: + notebook : ttk.Notebook = get_object('headerNotebook') + headerModifiable = notebook.index(notebook.select()) == 1 for i, index in enumerate(selectedIndexes): dialog.totalProgress.set(i) fileName = os.path.split(self.filePaths[i])[1] dialog.currentText.set("Current file: {}".format(fileName)) dialog.totalText.set("Processing {} / {} files".format(i+1, selectionLen)) dialog.currentProgress.set(0) - songData = deepcopy(self.songsData[index]) + songData: NbsSong = deepcopy(self.songsData[index]) dialog.setCurrentPercentage(randint(20, 25)) await asyncio.sleep(0.001) dialog.currentMax = len(songData.notes)*2 length = songData.header['length'] maxLayer = songData.maxLayer + if headerModifiable: + self.modifyHeaders(songData.header) + if outputVersion > -1: songData.header['file_version'] = outputVersion + if self.flipHorizontallyCheckVar.get() or self.flipVerticallyCheckVar.get(): for note in songData.notes: if self.flipHorizontallyCheckVar.get(): @@ -708,10 +751,12 @@ async def work(dialog: ProgressDialog = None): note['layer'] = maxLayer - note['layer'] dialog.currentProgress.set(dialog.currentProgress.get()+1) songData.sortNotes() + if self.arrangeMode.get() == 'collapse': self.collapseNotes(songData.notes) elif self.arrangeMode.get() == 'instruments': compactNotes(songData, self.groupPerc) + dialog.setCurrentPercentage(randint(75, 85)) await asyncio.sleep(0.001) songData.sortNotes() @@ -724,7 +769,7 @@ async def work(dialog: ProgressDialog = None): except asyncio.CancelledError: raise finally: - builder.get_object('applyBtn')['state'] = 'normal' + get_object('applyBtn')['state'] = 'normal' dialog = ProgressDialog(self.toplevel, self) dialog.d.toplevel.title("Applying tools to {} files".format(selectionLen)) @@ -743,6 +788,33 @@ def collapseNotes(self, notes) -> None: note['layer'] = layer prevNote = note + def modifyHeaders(self, header): + def setAttrFromStrVar(key: str, value: str): + if value != '' and value.isdigit(): + try: + setattr(header, key, int(value)) + except: + print(f'Non-integer value: {value}') + get_object = self.builder.get_object + if not 'alternate' in get_object('headerAutosaveCheck').state(): + autoSave = self.headerAutosave.get() + header.auto_save = autoSave + if autoSave: + setAttrFromStrVar('auto_save_time', self.headerAutosaveInterval.get()) + + setAttrFromStrVar('minutes_spent', self.headerMinuteSpent.get()) + setAttrFromStrVar('left_clicks', self.headerLeftClicks.get()) + setAttrFromStrVar('right_clicks', self.headerRightClicks.get()) + setAttrFromStrVar('block_added', self.headerBlockAdded.get()) + setAttrFromStrVar('block_removed', self.headerBlockRemoved.get()) + + if not 'alternate' in get_object('headerLoopCheck').state(): + loop = self.headerLoop.get() + header.loop = loop + if loop: + setAttrFromStrVar('loop_max', self.headerLoopCount.get()) + setAttrFromStrVar('loop_start', self.headerLoopStart.get()) + def OnApplyTool(self): self.ToolsTabButton['state'] = 'disabled' data = self.inputFileData diff --git a/nbsio.py b/nbsio.py index 3d79579..77a8785 100644 --- a/nbsio.py +++ b/nbsio.py @@ -102,7 +102,7 @@ def __repr__(self): def readHeader(self, f: BinaryIO) -> None: '''Read a .nbs file header from a file object''' - header = Dict() + header = self.header header['length'] = None readNumeric = read_numeric readString = read_string @@ -238,10 +238,12 @@ def correctData(self) -> None: header = self.header usedInsts = [[], []] maxLayer = 0 + maxInst = 0 tick = -1 self.hasPerc = False for i, note in enumerate(notes): tick, inst, layer = note['tick'], note['inst'], note['layer'] + maxInst = max(maxInst, inst) if inst in {2, 3, 4}: self.hasPerc = note['isPerc'] = True if inst not in usedInsts[1]: usedInsts[1].append(inst) @@ -252,9 +254,8 @@ def correctData(self) -> None: header.length = tick header.height = len(self.layers) - header.vani_inst = 16 if (len(usedInsts[0]) + len(usedInsts[1]) > 10) else 10 + header.vani_inst = 16 if header.file_version > 0 else 10 self.maxLayer = maxLayer - self.usedInsts = (tuple(usedInsts[0]), tuple(usedInsts[1])) # self['indexByTick'] = tuple([ (tk, set([notes.index(nt) for nt in notes if nt['tick'] == tk]) ) for tk in range(header['length']+1) ]) diff --git a/toplevel.ui b/toplevel.ui index 9ddf191..ee06ef1 100644 --- a/toplevel.ui +++ b/toplevel.ui @@ -199,73 +199,6 @@ True top - - - Format - - - 10 - 200 - - True - top - - - - No file selected. - string:selectedFilesVerStr - - True - top - - - - - - horizontal - - x - 10 - 5 - True - top - - - - - - Changes files format version to: - - True - top - - - - - - readonly - string:formatComboCurrent - - True - top - - - - - - Changing files's format to a older version can cause data to be lost - - - x - True - top - - - - - - - Header @@ -347,6 +280,7 @@ + onAutosaveCheckBtn Enabled boolean:headerAutosave @@ -584,6 +518,7 @@ + onLoopCheckBtn Enabled boolean:headerLoop @@ -668,6 +603,73 @@ + + + Format + + + 10 + 200 + + True + top + + + + No file selected. + string:selectedFilesVerStr + + True + top + + + + + + horizontal + + x + 10 + 5 + True + top + + + + + + Changes files format version to: + + True + top + + + + + + readonly + string:formatComboCurrent + + True + top + + + + + + Changing files's format to a older version can cause data to be lost + + + x + True + top + + + + + + + Flip From dc43862297fb8e61cf3aa54da64cd82d08a63d85 Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Sat, 19 Jun 2021 22:56:26 +0700 Subject: [PATCH 17/22] feat: Add MIDI exporting functionality - Files can be exported in the same path as theirs or in a specific folder; - Songs will be temporarily arranged by instruments before exporting. --- .gitignore | 1 + main.py | 509 ++++++++------------------------------------ midiexportdialog.ui | 95 +++++++++ nbs2midi.py | 184 ++++++++++++++++ progressdialog.ui | 2 +- toplevel.ui | 1 + 6 files changed, 372 insertions(+), 420 deletions(-) create mode 100644 midiexportdialog.ui create mode 100644 nbs2midi.py diff --git a/.gitignore b/.gitignore index 2dd1feb..64485a3 100644 --- a/.gitignore +++ b/.gitignore @@ -347,6 +347,7 @@ version.* build/ dist/ +test/ main.build/ main.dist/ diff --git a/main.py b/main.py index 6abb6ad..71cbcdc 100644 --- a/main.py +++ b/main.py @@ -38,6 +38,7 @@ import pygubu from pygubu import Builder +from pygubu.widgets.dialog import Dialog from time import time from pprint import pprint @@ -50,6 +51,7 @@ from nbsio import NBS_VERSION, NbsSong from ncfio import writencf +from nbs2midi import nbs2midi vaniNoteSounds = [ {'filename': 'harp.ogg', 'name': 'harp'}, @@ -363,8 +365,8 @@ def initFormatTab(self): combobox.current(0) def initHeaderTab(self): - self.builder.get_object('headerAutosaveCheck').state(['alternate']) - self.builder.get_object('headerLoopCheck').state(['alternate']) + self.builder.get_object('headerAutosaveCheck').state(['alternate']) + self.builder.get_object('headerLoopCheck').state(['alternate']) def initFlipTab(self): self.builder.get_object('flipHorizontallyCheck').deselect() @@ -379,227 +381,10 @@ def callDatapackExportDialog(self): dialog.run() del dialog - def ToolsTabElements(self): - fpadx, fpady = 10, 10 - padx, pady = 5, 0 - - # Flip tool - self.FlipToolFrame = tk.LabelFrame(self.ToolsTab, text="Flipping") - self.FlipToolFrame.grid( - row=0, column=0, sticky='nsew', padx=fpadx, pady=fpady) - - self.FlipToolMess = tk.Message( - self.FlipToolFrame, anchor='w', text="Flip the note sequence horizontally (by tick), vertically (by layer) or both: ") - self.FlipToolMess.pack(fill='both', expand=True, padx=padx, pady=pady) - - self.var.tool.flip.vertical = tk.IntVar() - self.FilpToolCheckV = tk.Checkbutton( - self.FlipToolFrame, text="Vertically", variable=self.var.tool.flip.vertical) - self.FilpToolCheckV.pack(side='left', padx=padx, pady=pady) - - self.var.tool.flip.horizontal = tk.IntVar() - self.FilpToolCheckH = tk.Checkbutton( - self.FlipToolFrame, text="Horizontally", variable=self.var.tool.flip.horizontal) - self.FilpToolCheckH.pack(side='left', padx=padx, pady=pady) - - # Instrument tool - self.InstToolFrame = tk.LabelFrame( - self.ToolsTab, text="Note's instrument") - self.InstToolFrame.grid( - row=0, column=1, sticky='nsew', padx=fpadx, pady=fpady) - - self.InstToolMess = tk.Message( - self.InstToolFrame, anchor='w', text="Change all note's instrument to:") - self.InstToolMess.pack(fill='both', expand=True, padx=padx, pady=pady) - - self.var.tool.opts = opts = [ - "(not applied)"] + [x['name'] for x in vaniNoteSounds] + ["Random"] - self.InstToolCombox = ttk.Combobox( - self.InstToolFrame, state='readonly', values=opts) - self.InstToolCombox.current(0) - self.InstToolCombox.pack( - side='left', fill='both', expand=True, padx=padx, pady=pady) - - # Reduce tool - self.ReduceToolFrame = tk.LabelFrame(self.ToolsTab, text="Reducing") - self.ReduceToolFrame.grid( - row=1, column=0, sticky='nsew', padx=fpadx, pady=fpady) - - self.ReduceToolMess = tk.Message( - self.ReduceToolFrame, anchor='w', text="Delete as many note as possible to reduce file size.") - self.ReduceToolMess.pack( - fill='both', expand=True, padx=padx, pady=pady) - - self.var.tool.reduce.opt1 = tk.IntVar() - self.CompactToolChkOpt1 = FlexCheckbutton( - self.ReduceToolFrame, text="Delete duplicate notes", variable=self.var.tool.reduce.opt1, anchor='w') - self.CompactToolChkOpt1.pack(padx=padx, pady=pady) - - self.var.tool.reduce.opt2 = tk.IntVar() - self.CompactToolChkOpt2 = FlexCheckbutton( - self.ReduceToolFrame, text=" In every tick, delete all notes except the first note", variable=self.var.tool.reduce.opt2, anchor='w') - self.CompactToolChkOpt2.pack(padx=padx, pady=pady) - - self.var.tool.reduce.opt3 = tk.IntVar() - self.CompactToolChkOpt3 = FlexCheckbutton( - self.ReduceToolFrame, text=" In every tick, delete all notes except the last note", variable=self.var.tool.reduce.opt3, anchor='w') - self.CompactToolChkOpt3.pack(padx=padx, pady=(pady, 10)) - - # Compact tool - self.CompactToolFrame = tk.LabelFrame(self.ToolsTab, text="Compacting") - self.CompactToolFrame.grid( - row=1, column=1, sticky='nsew', padx=fpadx, pady=fpady) - - self.CompactToolMess = tk.Message( - self.CompactToolFrame, anchor='w', text="Remove spaces between notes vertically (by layer) and group them by instruments.") - self.CompactToolMess.pack( - fill='both', expand=True, padx=padx, pady=pady) - - self.var.tool.compact = tk.IntVar() - self.CompactToolCheck = FlexCheckbutton( - self.CompactToolFrame, text="Compact notes", variable=self.var.tool.compact, command=self.toggleCompactToolOpt, anchor='w') - self.CompactToolCheck.pack(padx=padx, pady=pady) - - self.var.tool.compact.opt1 = tk.IntVar() - self.CompactToolChkOpt1 = FlexCheckbutton(self.CompactToolFrame, text="Automatic separate notes by instruments (remain some spaces)", - variable=self.var.tool.compact.opt1, state='disabled', command=lambda: self.toggleCompactToolOpt(2), anchor='w') - self.CompactToolChkOpt1.select() - self.CompactToolChkOpt1.pack(padx=padx*5, pady=pady) - - self.var.tool.compact.opt1_1 = tk.IntVar() - self.CompactToolChkOpt1_1 = FlexCheckbutton( - self.CompactToolFrame, text="Group percussions into one layer", variable=self.var.tool.compact.opt1_1, state='disabled', anchor='w') - self.CompactToolChkOpt1_1.select() - self.CompactToolChkOpt1_1.pack(padx=padx*5*2, pady=pady) - - # 'Apply' botton - self.ToolsTabButton = ttk.Button( - self.ToolsTab, text="Apply", state='disabled', command=self.OnApplyTool) - self.ToolsTabButton.grid( - row=2, column=1, sticky='se', padx=fpadx, pady=fpady) - - def ExportTabElements(self): - fpadx, fpady = 10, 10 - padx, pady = 5, 5 - - # Upper frame - self.ExpConfigFrame = tk.LabelFrame(self.ExportTab, text="Option") - self.ExpConfigFrame.pack( - fill='both', expand=True, padx=fpadx, pady=fpady) - - # "Select mode" frame - self.ExpConfigGrp1 = tk.Frame( - self.ExpConfigFrame, relief='groove', borderwidth=1) - self.ExpConfigGrp1.pack(fill='both', padx=fpadx) - - self.ExpConfigLabel = tk.Label( - self.ExpConfigGrp1, text="Export the song as a:", anchor='w') - self.ExpConfigLabel.pack(side='left', fill='x', padx=padx, pady=pady) - - self.var.export.mode = tk.IntVar() - self.ExpConfigMode1 = tk.Radiobutton( - self.ExpConfigGrp1, text="File", variable=self.var.export.mode, value=1) - self.ExpConfigMode1.pack(side='left', padx=padx, pady=pady) - self.ExpConfigMode1.select() - self.ExpConfigMode2 = tk.Radiobutton( - self.ExpConfigGrp1, text="Datapack", variable=self.var.export.mode, value=0) - self.ExpConfigMode2.pack(side='left', padx=padx, pady=pady) - - self.var.export.type.file = \ - [('Musical Instrument Digital files', '*.mid'), - ('Nokia Composer Format', '*.txt'), ] - self.var.export.type.dtp = ['Wireless note block song', 'other'] - self.ExpConfigCombox = ttk.Combobox(self.ExpConfigGrp1, state='readonly', values=[ - "{} ({})".format(tup[0], tup[1]) for tup in self.var.export.type.file]) - self.ExpConfigCombox.current(0) - self.ExpConfigCombox.bind( - "<>", self.toggleExpOptiGrp) - self.ExpConfigCombox.pack(side='left', fill='x', padx=padx, pady=pady) - - self.var.export.mode.trace('w', self.toggleExpOptiGrp) - - ttk.Separator(self.ExpConfigFrame, orient="horizontal").pack( - fill='x', expand=False, padx=padx*3, pady=pady) - - self.ExpOptiSW = StackingWidget( - self.ExpConfigFrame, relief='groove', borderwidth=1) - self.ExpOptiSW.pack(fill='both', expand=True, padx=fpadx) - - # Midi export options frame - self.ExpOptiSW.append(tk.Frame(self.ExpOptiSW), 'Midi') - self.ExpOptiSW.pack('Midi', side='top', fill='both', expand=True) - - self.var.export.midi.opt1 = tk.IntVar() - self.ExpMidi1Rad1 = tk.Radiobutton( - self.ExpOptiSW['Midi'], text="Sort notes to MIDI tracks by note's layer", variable=self.var.export.midi.opt1, value=1) - self.ExpMidi1Rad1.pack(anchor='nw', padx=padx, pady=(pady, 0)) - self.ExpMidi1Rad2 = tk.Radiobutton( - self.ExpOptiSW['Midi'], text="Sort notes to MIDI tracks by note's instrument", variable=self.var.export.midi.opt1, value=0) - self.ExpMidi1Rad2.pack(anchor='nw', padx=padx, pady=(0, pady)) - - # Nokia export options frame - self.ExpOptiSW.append(tk.Frame(self.ExpOptiSW), 'NCF') - self.ExpOptiSW.pack('NCF', side='top', fill='both', expand=True) - - self.NCFOutput = ScrolledText( - self.ExpOptiSW['NCF'], state="disabled", height=10) - self.NCFOutput.pack(fill='both', expand=True) - - # 'Wireless song datapack' export options frame - self.ExpOptiSW.append(tk.Frame(self.ExpOptiSW), 'Wnbs') - self.ExpOptiSW.pack('Wnbs', side='top', fill='both', expand=True) - - self.WnbsIDLabel = tk.Label( - self.ExpOptiSW['Wnbs'], text="Unique name:") - self.WnbsIDLabel.pack(anchor='nw', padx=padx, pady=pady) - - #vcmd = (self.register(self.onValidate), '%P') - self.WnbsIDEntry = tk.Entry(self.ExpOptiSW['Wnbs'], validate="key", - validatecommand=(self.register(lambda P: bool(re.match("^(\d|\w|[-_])*$", P))), '%P')) - self.WnbsIDEntry.pack(anchor='nw', padx=padx, pady=pady) - - # Other export options frame - self.ExpOptiSW.append(tk.Frame(self.ExpOptiSW), 'Other') - self.ExpOptiSW.pack('Other', side='top', fill='both', expand=True) - - self.ExpMusicLabel = tk.Label( - self.ExpOptiSW['Other'], text="There is no option available.") - self.ExpMusicLabel.pack(anchor='nw', padx=padx, pady=pady) - - # Output frame - self.ExpOutputFrame = tk.LabelFrame(self.ExportTab, text="Output") - self.ExpOutputFrame.pack(fill='both', padx=fpadx, pady=(0, fpady)) - - self.ExpOutputLabel = tk.Label( - self.ExpOutputFrame, text="File path:", anchor='w', width=8) - self.ExpOutputLabel.pack(side='left', fill='x', padx=padx, pady=pady) - - self.ExpOutputEntry = tk.Entry( - self.ExpOutputFrame, textvariable=self.exportFilePath) - self.ExpOutputEntry.pack(side='left', fill='x', padx=padx, expand=True) - - self.ExpBrowseButton = ttk.Button( - self.ExpOutputFrame, text="Browse", command=self.OnBrowseExp) - self.ExpBrowseButton.pack(side='left', padx=padx, pady=pady) - - self.ExpSaveButton = ttk.Button( - self.ExpOutputFrame, text="Export", command=self.OnExport) - self.ExpSaveButton.pack(side='left', padx=padx, pady=pady) - - def footerElements(self): - self.footerLabel = tk.Label(self.footer, text="Ready") - self.footerLabel.pack(side='left', fill='x') - self.var.footerLabel = 0 - - self.sizegrip = ttk.Sizegrip(self.footer) - self.sizegrip.pack(side='right', anchor='se') - - self.progressbar = ttk.Progressbar( - self.footer, orient="horizontal", length=300, mode="determinate") - self.progressbar["value"] = 0 - self.progressbar["maximum"] = 100 - # self.progressbar.start() - # self.progressbar.stop() + def callMidiExportDialog(self): + dialog = MidiExportDialog(self.toplevel, self) + dialog.run() + del dialog def windowBind(self): toplevel = self.toplevel @@ -694,15 +479,6 @@ def onClose(self, event=None): self.toplevel.quit() self.toplevel.destroy() - def toggleCompactToolOpt(self, id=1): - if id <= 2: - a = ((self.var.tool.compact.opt1.get() == 0) - or (self.var.tool.compact.get() == 0)) - self.CompactToolChkOpt1_1["state"] = "disable" if a is True else "normal" - if id <= 1: - self.CompactToolChkOpt1["state"] = "disable" if self.var.tool.compact.get( - ) == 0 else "normal" - def onArrangeModeChanged(self): self.builder.get_object('arrangeGroupPrec')['state'] = 'normal' if (self.arrangeMode.get() == 'instruments') else 'disabled' @@ -772,7 +548,7 @@ async def work(dialog: ProgressDialog = None): get_object('applyBtn')['state'] = 'normal' dialog = ProgressDialog(self.toplevel, self) - dialog.d.toplevel.title("Applying tools to {} files".format(selectionLen)) + dialog.d.set_title("Applying tools to {} files".format(selectionLen)) dialog.totalMax = selectionLen dialog.run(work) @@ -815,142 +591,6 @@ def setAttrFromStrVar(key: str, value: str): setAttrFromStrVar('loop_max', self.headerLoopCount.get()) setAttrFromStrVar('loop_start', self.headerLoopStart.get()) - def OnApplyTool(self): - self.ToolsTabButton['state'] = 'disabled' - data = self.inputFileData - ticklen = data['header']['length'] - layerlen = data['maxLayer'] - instOpti = self.InstToolCombox.current() - for note in data['notes']: - # Flip - if bool(self.var.tool.flip.horizontal.get()): - note['tick'] = ticklen - note['tick'] - if bool(self.var.tool.flip.vertical.get()): - note['layer'] = layerlen - note['layer'] - - # Instrument change - if instOpti > 0: - note['inst'] = randrange( - len(self.var.tool.opts)-2) if instOpti > len(self.noteSounds) else instOpti-1 - # Reduce - if bool(self.var.tool.reduce.opt2.get()) and bool(self.var.tool.reduce.opt3.get()): - data['notes'] = [note for i, note in enumerate( - data['notes']) if note == data['notes'][-1] or note['tick'] != data['notes'][i-1]['tick'] or note['tick'] != data['notes'][i+1]['tick']] - elif bool(self.var.tool.reduce.opt2.get()): - data['notes'] = [note for i, note in enumerate( - data['notes']) if note['tick'] != data['notes'][i-1]['tick']] - elif bool(self.var.tool.reduce.opt3.get()): - data['notes'] = [data['notes'][i-1] - for i, note in enumerate(data['notes']) if note['tick'] != data['notes'][i-1]['tick']] - if bool(self.var.tool.reduce.opt1.get()): - data['notes'] = sorted(data['notes'], key=operator.itemgetter( - 'tick', 'inst', 'key', 'layer')) - data['notes'] = [note for i, note in enumerate(data['notes']) if note['tick'] != data['notes'][i-1] - ['tick'] or note['inst'] != data['notes'][i-1]['inst'] or note['key'] != data['notes'][i-1]['key']] - data['notes'] = sorted( - data['notes'], key=operator.itemgetter('tick', 'layer')) - - # Compact - if bool(self.var.tool.compact.get()): - data = compactNotes( - data, self.var.tool.compact.opt1.get(), self.var.tool.compact.opt1_1.get()) - # Sort notes - - - data.correctData() - self.ToolsTabButton['state'] = 'normal' - - def toggleExpOptiGrp(self, n=None, m=None, y=None): - asFile = bool(self.var.export.mode.get()) - key = max(0, self.ExpConfigCombox.current()) - if asFile: - if key == 0: - self.ExpOptiSW.switch('Midi') - elif key == 1: - self.ExpOptiSW.switch('NCF') - else: - self.ExpOptiSW.switch('Other') - self.ExpConfigCombox.configure(values=["{} ({})".format( - tup[0], tup[1]) for tup in self.var.export.type.file]) - else: - if key == 0: - self.ExpOptiSW.switch('Wnbs') - else: - self.ExpOptiSW.switch('Other') - self.ExpConfigCombox.configure(values=["{} ({})".format(tup[0], tup[1]) if isinstance( - tup, tuple) else tup for tup in self.var.export.type.dtp]) - key = min(key, len(self.ExpConfigCombox['values'])-1) - self.ExpConfigCombox.current(key) - self.ExpConfigCombox.configure(width=len(self.ExpConfigCombox.get())) - - if self.exportFilePath.get(): - print(self.exportFilePath.get()) - fext = (self.var.export.type.file[self.ExpConfigCombox.current()],)[ - 0][1][1:] - if asFile: - if not self.exportFilePath.get().lower().endswith(self.WnbsIDEntry.get()): - self.exportFilePath.set( - "{}/{}".format(self.exportFilePath.get(), self.WnbsIDEntry.get())) - self.WnbsIDEntry.delete(0, 'end') - if not self.exportFilePath.get().lower().endswith(fext): - self.exportFilePath.set( - self.exportFilePath.get().split('.')[0] + fext) - else: - if '.' in self.exportFilePath.get(): - self.WnbsIDEntry.delete(0, 'end') - self.WnbsIDEntry.insert( - 0, self.exportFilePath.get().split('/')[-1].split('.')[0]) - self.exportFilePath.set( - '/'.join(self.exportFilePath.get().split('/')[0:-1])) - - def OnBrowseExp(self): - if self.filePath and self.inputFileData: - asFile = bool(self.var.export.mode.get()) - if asFile: - curr = ( - self.var.export.type.file[self.ExpConfigCombox.current()],) - fext = curr[0][1][1:] - self.exportFilePath.set(asksaveasfilename(title="Export file", initialfile=os.path.splitext( - os.path.basename(self.filePath))[0]+fext, filetypes=curr)) - else: - curr = [ - (self.var.export.type.dtp[self.ExpConfigCombox.current()], '*.'), ] - fext = '' - self.exportFilePath.set(askdirectory( - title="Export datapack (choose the directory to put the datapack)", initialdir=os.path.dirname(self.filePath), mustexist=False)) - if self.exportFilePath.get(): - if asFile: - if not self.exportFilePath.get().lower().endswith(fext): - self.exportFilePath.set( - self.exportFilePath.get().split('.')[0] + fext) - else: - if '.' in self.exportFilePath.get().split('/')[-1]: - self.exportFilePath.set( - '/'.join(self.exportFilePath.get().split('/')[0:-1])) - - def OnExport(self): - if self.exportFilePath.get() is not None: - data, path = self.inputFileData, self.exportFilePath.get() - asFile = bool(self.var.export.mode.get()) - type = self.ExpConfigCombox.current() - if asFile: - if type == 0: - exportMIDI(self, path, self.var.export.midi.opt1.get()) - elif type == 1: - with open(path, "w") as f: - f.write(writencf(data)) - elif type in {2, 3, 4, 5}: - fext = self.var.export.type.file[self.ExpConfigCombox.current( - )][1][2:] - # exportMusic(self, path, fext) - else: - if type == 0: - exportDatapack(self, os.path.join( - path, self.WnbsIDEntry.get()), 'wnbs') - - self.ExpBrowseButton['state'] = self.ExpSaveButton['state'] = 'normal' - - class DatapackExportDialog: def __init__(self, master, parent): self.master = master @@ -960,8 +600,7 @@ def __init__(self, master, parent): builder.add_resource_path(resource_path()) builder.add_from_file(resource_path('datapackexportdialog.ui')) - self.dialog = builder.get_object('dialog', master) - centerToplevel(self.dialog.toplevel) + self.d: Dialog = builder.get_object('dialog', master) button = builder.get_object('exportBtn') button.configure(command=self.export) @@ -981,10 +620,10 @@ def wnbsIDVaildate(P): builder.connect_callbacks(self) def run(self): - self.builder.get_object('dialog').run() + self.d.run() def export(self, _=None): - self.dialog.close() + self.d.close() fileTable = self.parent.fileTable index = fileTable.index(fileTable.selection()[0]) @@ -994,6 +633,81 @@ def export(self, _=None): exportDatapack(self.parent.songsData[index], os.path.join( path, self.entry.get()), self.entry.get(), 'wnbs') +class MidiExportDialog: + def __init__(self, master, parent): + self.master = master + self.parent = parent + + self.builder = builder = pygubu.Builder() + builder.add_resource_path(resource_path()) + builder.add_from_file(resource_path('midiexportdialog.ui')) + + self.d: Dialog = builder.get_object('dialog', master) + builder.get_object('pathChooser').bind('<>', self.pathChanged) + + builder.connect_callbacks(self) + builder.import_variables(self) + + self.exportModeChanged() + + def run(self): + self.d.run() + + def exportModeChanged(self): + self.isFolderMode = self.exportMode.get() == 'folder' + self.builder.get_object('pathChooser')['state'] = 'normal' if self.isFolderMode else 'disabled' + self.pathChanged() + + def pathChanged(self, _=None): + self.builder.get_object('exportBtn')['state'] = 'normal' if (not self.isFolderMode) or (self.exportPath.get() != '') else 'disabled' + + def export(self, _=None): + path = os.path + fileTable = self.parent.fileTable + indexes = [fileTable.index(i) for i in fileTable.selection()] + + if self.isFolderMode: + self.pathChanged() + if self.exportPath.get() != '': + Path(self.exportPath.get()).mkdir(parents=True, exist_ok=True) + else: + return + + async def work(dialog: ProgressDialog = None): + try: + songsData = self.parent.songsData + filePaths = self.parent.filePaths + for i in indexes: + dialog.totalProgress.set(i) + dialog.totalText.set("Exporting {} / {} files".format(i+1, len(indexes))) + dialog.currentProgress.set(0) + origPath = filePaths[i] + baseName = path.basename(origPath) + if baseName.endswith('.nbs'): + baseName = baseName[:-4] + baseName += '.mid' + + filePath = '' + if not self.isFolderMode: + filePath = path.join(path.dirname(origPath), baseName) + else: + filePath = path.join(self.exportPath.get(), baseName) + dialog.currentText.set("Current file: {}".format(filePath)) + songData = deepcopy(songsData[i]) + compactNotes(songData, True) + dialog.currentProgress.set(10) # 10% + await nbs2midi(songData, filePath, dialog) + dialog.totalProgress.set(dialog.currentMax) + except asyncio.CancelledError: + raise + + dialog = ProgressDialog(self.d.toplevel, self) + dialog.d.bind('<>', lambda _: self.d.destroy()) + dialog.d.set_title("Exporting {} files to MIDI".format(len(indexes))) + dialog.totalMax = len(indexes) + dialog.run(work) + dialog.d.destroy() + class ProgressDialog: def __init__(self, master, parent): self.master = master @@ -1004,9 +718,9 @@ def __init__(self, master, parent): builder.add_resource_path(resource_path()) builder.add_from_file(resource_path('progressdialog.ui')) - self.d = builder.get_object('dialog1', master) + self.d: Dialog = builder.get_object('dialog1', master) self.d.toplevel.protocol('WM_DELETE_WINDOW', self.onCancel) - # centerToplevel(self.dialog.toplevel) + builder.connect_callbacks(self) builder.import_variables(self) @@ -1027,7 +741,7 @@ def totalMax(self, value: int) -> None: self.builder.get_object('totalProgressBar')['maximum'] = value def run(self, func=None): - self.builder.get_object('dialog1').run() + self.builder.get_object('dialog1', self.master).run() if asyncio.iscoroutinefunction(func): self.work = func self.d.toplevel.after(0, self.startWork) @@ -1091,45 +805,6 @@ def __init__(self, *args, **kwargs): self.bind("", lambda event: self.configure(width=event.width-10, justify=self.justify, anchor=self.anchor, wraplength=event.width-20, text=self.text+' '*999)) - -class StackingWidget(tk.Frame): - def __init__(self, parent, **kwargs): - super().__init__(parent, **kwargs) - self._frames = {} - self._i = 0 - #self._shown = None - - def __getitem__(self, key): - if key in self._frames: - return self._frames[key][0] - super().__getitem__(key) - - def append(self, frame, key=None): - if isinstance(frame, (tk.Widget, ttk.Widget)): - if not key: - key = self._i - self._i += 1 - self._frames[key] = [frame, None] - - def switch(self, key): - for k, (w, o) in self._frames.items(): - if k == key: - if o: - w.pack(**o) - else: - w.pack() - else: - w.pack_forget() - - def pack(self, key=None, **opts): - if key: - self._frames[key][1] = opts - else: - super().pack(**opts) - if len(self._frames) == 1: - self.switch(key) - - def centerToplevel(obj, width=None, height=None, mwidth=None, mheight=None): # Credit: https://stackoverflow.com/questions/3129322/how-do-i-get-monitor-resolution-in-python/56913005#56913005 def get_curr_screen_size(): @@ -1203,10 +878,6 @@ def compactNotes(data, groupPerc=1) -> None: outerLayer += innerLayer + 1 data['maxLayer'] = outerLayer - 1 -def exportMIDI(cls, path, byLayer=False): - pass - - def exportDatapack(data, path, bname, mode=None, master=None): def writejson(path, jsout): with open(path, 'w') as f: diff --git a/midiexportdialog.ui b/midiexportdialog.ui new file mode 100644 index 0000000..f5be65a --- /dev/null +++ b/midiexportdialog.ui @@ -0,0 +1,95 @@ + + + + 100 + true + none + 200 + + + 200 + 200 + + true + both + 3 + 5 + True + top + + + + Export location + 200 + + nw + true + both + 5 + 5 + True + top + + + + exportModeChanged + left + Export all files in a folder: + folder + string:exportMode + + nw + True + top + + + + + + disabled + string:exportPath + Select folder to export + directory + + nw + x + True + top + + + + + + exportModeChanged + left + Export each file in the same folder as the original file. Only ask export paths for untitled files. + + current + string:exportMode + 500 + + nw + True + top + + + + + + + + export + normal + Export + + ne + 5 + True + top + + + + + + + diff --git a/nbs2midi.py b/nbs2midi.py new file mode 100644 index 0000000..2de4924 --- /dev/null +++ b/nbs2midi.py @@ -0,0 +1,184 @@ +from asyncio import sleep + +from mido import Message, MetaMessage, MidiFile, MidiTrack, bpm2tempo + +from nbsio import NbsSong + +PERCUSSIONS = ( + #(percussion_key, instrument, key) + (35, 2, 64), + (36, 2, 60), + (37, 4, 60), + (38, 3, 62), + #(39, 4, 60), + (40, 3, 58), + #(41, 2, 60), + (42, 3, 76), + (43, 2, 67), + #(44, 3, 76), + (45, 2, 69), + (46, 2, 72), + (47, 2, 74), + (48, 2, 77), + (49, 2, 71), + (50, 3, 77), + (51, 3, 78), + (52, 3, 62), + (53, 3, 67), + (54, 3, 72), + (55, 3, 73), + (56, 4, 55), + #(57, 3, 67), + (58, 4, 56), + #(59, 3, 67), + (60, 4, 63), + (61, 4, 57), + (62, 4, 62), + (63, 2, 76), + (64, 3, 69), + #(65, 3, 67), + #(66, 3, 62), + #(67, 4, 62), + (68, 4, 58), + (69, 4, 74), + (70, 4, 77), + (73, 3, 71), + (74, 4, 65), + (75, 4, 72), + (76, 4, 64), + (77, 4, 59), + (80, 4, 71), + (81, 4, 76), + (82, 3, 78) + ) + +INST_PROGRAMS = { + 0: 1, # Harp: Piano + 1: 33, # Double bass: Acoustic bass + 5: 25, # Guitar: Acoustic guitar (nylon) + 6: 74, # Flute: Flute + 7: 10, # Bell: Glockenspiel + 8: 113, # Chime: Tinkle bell (wind chime) + 9: 14, # Xylophone: Xylophone + 10: 12, # Iron Xylophone: Vibraphone + 11: 1, + 12: 1, + 13: 81, # Bit: Lead 1 (square) + 14: 106, # Banjo: Banjo + 15: 6, # Pling: Electric piano 2 +} + +INST2PITCH = { + 2: 36, + 3: 44, + 4: 81, +} + +def firstInstInLayer(nbs: NbsSong, layer: int) -> int: + if layer > nbs['maxLayer']: + return 0 + for note in nbs['notes']: + if note['layer'] == layer: + return note['inst'] + return 0 + +class MsgComparator: + def __init__(self, msg) -> None: + self.msg = msg + self.isMeta = isinstance(msg, MetaMessage) + + def __lt__(self, other) -> bool: + if (not self.isMeta) and isinstance(other.msg, Message): + return self.msg.time < other.msg.time + else: + return False + +def absTrack2DeltaTrack(track) -> MidiTrack: + track.sort(key=MsgComparator) + ret = MidiTrack() + ret.append(track[0]) + for i in range(1, len(track)): + msg = track[i] + # print(msg.time - track[i-1].time) + ret.append(msg.copy(time=msg.time - track[i-1].time)) + return ret + + +async def nbs2midi(data: NbsSong, filepath: str, dialog = None): + headers, notes, layers = data['header'], data['notes'], data['layers'] + + timeSign = headers['time_sign'] + tempo = headers['tempo'] * 60 / 4 + height = headers['height'] + layersLen = len(layers) + + mid = MidiFile(type=1) + tpb = mid.ticks_per_beat + note_tpb = int(tpb / 4) + tracks = [] + + for i in range(data.maxLayer+1): + programCode = INST_PROGRAMS.get(firstInstInLayer(data, i), 1) - 1 + + track = MidiTrack() + track.append(MetaMessage('set_tempo', tempo=bpm2tempo(tempo), time=0)) + track.append(Message('program_change', program=programCode, time=0)) + + tracks.append(track) + if dialog: + dialog.currentProgress.set(25) # 25% + await sleep(0.001) + + accumulate_time = 0 + for note in notes: + abs_time = int(note['tick'] / timeSign * tpb) + pitch = note['key'] + 21 + trackIndex = note['layer'] + layerVel = 100 + if trackIndex < layersLen: + layerVel = layers[trackIndex]['volume'] + velocity = int(note['vel'] * (layerVel / 100) / 100 * 127) + + isPerc = False + if note['isPerc']: + inst: int = note['inst'] + for a, b, c in PERCUSSIONS: + if c == pitch and b == inst: + pitch = a + break + else: + pitch = INST2PITCH[inst] + isPerc = True + + if isPerc: + tracks[trackIndex].append(Message('note_on', channel=9, note=pitch, velocity=127, time=abs_time)) + tracks[trackIndex].append(Message('note_off', channel=9, note=pitch, velocity=127, time=abs_time + note_tpb)) + else: + tracks[trackIndex].append(Message('note_on', note=pitch, velocity=127, time=abs_time)) + tracks[trackIndex].append(Message('note_off', note=pitch, velocity=127, time=abs_time + note_tpb)) + + if dialog: + dialog.currentProgress.set(50) # 50% + await sleep(0.001) + mid.tracks = [absTrack2DeltaTrack(track) for track in tracks] + if dialog: + dialog.currentProgress.set(75) # 75% + await sleep(0.001) + + if not filepath.endswith('.mid'): + filepath += '.mid' + mid.save(filepath) + +if __name__ == "__main__": + # fn = 'graphite_diamond' + # fn = 'AYBHCM_1_2' + # fn = "The Ground's Colour Is Yellow" + # fn = "sandstorm" + # fn = "Through the Fire and Flames" + # fn = "Vì yêu cứ đâm đầu" + # fn = "Shining_in_the_Sky" + fn = "Megalovania - Super Smash Bros. Ultimate" + + data = NbsSong(fn + '.nbs') + data.correctData() + nbs2midi(data, fn) diff --git a/progressdialog.ui b/progressdialog.ui index 90f7eb3..f6f4f39 100644 --- a/progressdialog.ui +++ b/progressdialog.ui @@ -64,7 +64,7 @@ - 400 + 550 horizontal int:totalProgress diff --git a/toplevel.ui b/toplevel.ui index ee06ef1..e2a6276 100644 --- a/toplevel.ui +++ b/toplevel.ui @@ -897,6 +897,7 @@ + callMidiExportDialog as MIDI file... disabled 3 From 0f99eeae9bdf93ef4d85d274dffc99462f241e65 Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Sun, 20 Jun 2021 19:20:53 +0700 Subject: [PATCH 18/22] ui: Replace some Tk widgets with their ttk counterparts - Fix wrong identation in the datapack export function --- datapackexportdialog.ui | 19 ++- main.py | 257 ++++++++++++++++++++-------------------- progressdialog.ui | 25 ++-- toplevel.ui | 82 +++++-------- 4 files changed, 175 insertions(+), 208 deletions(-) diff --git a/datapackexportdialog.ui b/datapackexportdialog.ui index e87fb44..84ce9da 100644 --- a/datapackexportdialog.ui +++ b/datapackexportdialog.ui @@ -1,8 +1,10 @@ - + 100 true + 5 + 5 Datapack export - NBSTool 200 @@ -21,8 +23,6 @@ Unique scorebroad ID: - 5 - 5 True top @@ -34,24 +34,21 @@ key x - 5 - 5 + 10 True top - - export + + export + normal disabled Export - 10 - 5 - 5 True - top + bottom diff --git a/main.py b/main.py index 71cbcdc..c915056 100644 --- a/main.py +++ b/main.py @@ -271,7 +271,7 @@ def addFiles(self, _=None, paths=()): return self.builder.get_object('applyBtn')['state'] = 'disabled' self.disabledFileTable() - for filePath in addedPaths: + for i, filePath in enumerate(addedPaths): try: songData = NbsSong(filePath) self.songsData.append(songData) @@ -288,7 +288,8 @@ def addFiles(self, _=None, paths=()): orig_author = header['orig_author'] self.fileTable.insert("", 'end', text=filePath, values=( length, name, author, orig_author)) - self.mainwin.update() + if i % 3 == 0: + self.mainwin.update() self.filePaths.extend(addedPaths) self.enableFileTable() self.builder.get_object('applyBtn')['state'] = 'normal' @@ -369,12 +370,12 @@ def initHeaderTab(self): self.builder.get_object('headerLoopCheck').state(['alternate']) def initFlipTab(self): - self.builder.get_object('flipHorizontallyCheck').deselect() - self.builder.get_object('flipVerticallyCheck').deselect() + self.builder.get_object('flipHorizontallyCheck').state(('!selected',)) + self.builder.get_object('flipVerticallyCheck').state(('!selected',)) def initArrangeTab(self): - self.builder.get_object('notArrangeRadio').select() - self.builder.get_object('arrangeGroupPrec').deselect() + self.arrangeMode.set('none') + # self.builder.get_object('arrangeGroupPrec').state(('!selected',)) def callDatapackExportDialog(self): dialog = DatapackExportDialog(self.toplevel, self) @@ -407,8 +408,8 @@ def windowBind(self): for tkclass in ("Entry", "Text", "ScrolledText", "TCombobox"): mainwin.bind_class(tkclass, "", self.popupmenus) - mainwin.bind_class("TNotebook", "<>", - self._on_tab_changed) + # mainwin.bind_class("TNotebook", "<>", + # self._on_tab_changed) mainwin.bind_class("Treeview", "", self._on_treeview_shift_down) @@ -700,13 +701,13 @@ async def work(dialog: ProgressDialog = None): dialog.totalProgress.set(dialog.currentMax) except asyncio.CancelledError: raise + self.d.toplevel.after(1, self.d.destroy) dialog = ProgressDialog(self.d.toplevel, self) dialog.d.bind('<>', lambda _: self.d.destroy()) dialog.d.set_title("Exporting {} files to MIDI".format(len(indexes))) dialog.totalMax = len(indexes) dialog.run(work) - dialog.d.destroy() class ProgressDialog: def __init__(self, master, parent): @@ -916,151 +917,149 @@ def makeFolderTree(inp, a=[]): elif note['layer'] not in instLayers[note['inst']]: instLayers[note['inst']].append(note['layer']) - scoreObj = bname[:13] - speed = int(min(data['header']['tempo'] * 4, 120)) - length = data['header']['length'] - - makeFolderTree( - {path: { - 'data': { - bname: { - 'functions': { - 'notes', - 'tree', - }, - }, - 'minecraft': { - 'tags': 'functions', + scoreObj = bname[:13] + speed = int(min(data['header']['tempo'] * 4, 120)) + length = data['header']['length'] + + makeFolderTree( + {path: { + 'data': { + bname: { + 'functions': { + 'notes', + 'tree', }, }, + 'minecraft': { + 'tags': 'functions', + }, }, - } - ) - - writejson(os.path.join(path, 'pack.mcmeta'), {"pack": { - "description": "Note block song made with NBSTool.", "pack_format": 1}}) - writejson(os.path.join(path, 'data', 'minecraft', 'tags', 'functions', - 'load.json'), jsout={"values": ["{}:load".format(bname)]}) - writejson(os.path.join(path, 'data', 'minecraft', 'tags', 'functions', - 'tick.json'), jsout={"values": ["{}:tick".format(bname)]}) - - writemcfunction(os.path.join(path, 'data', bname, 'functions', 'load.mcfunction'), - """scoreboard objectives add {0} dummy + }, + } + ) + + writejson(os.path.join(path, 'pack.mcmeta'), {"pack": { + "description": "Note block song datapack made with NBSTool.", "pack_format": 6}}) + writejson(os.path.join(path, 'data', 'minecraft', 'tags', 'functions', + 'load.json'), jsout={"values": ["{}:load".format(bname)]}) + writejson(os.path.join(path, 'data', 'minecraft', 'tags', 'functions', + 'tick.json'), jsout={"values": ["{}:tick".format(bname)]}) + + writemcfunction(os.path.join(path, 'data', bname, 'functions', 'load.mcfunction'), + """scoreboard objectives add {0} dummy scoreboard objectives add {0}_t dummy scoreboard players set speed {0} {1}""".format(scoreObj, speed)) - writemcfunction(os.path.join(path, 'data', bname, 'functions', 'tick.mcfunction'), - """execute as @a[tag={0}] run scoreboard players operation @s {0} += speed {0} + writemcfunction(os.path.join(path, 'data', bname, 'functions', 'tick.mcfunction'), + """execute as @a[tag={0}] run scoreboard players operation @s {0} += speed {0} execute as @a[tag={0}] run function {1}:tree/0_{2} execute as @e[type=armor_stand, tag=WNBS_Marker] at @s unless block ~ ~-1 ~ minecraft:note_block run kill @s""".format(scoreObj, bname, 2**(floor(log2(length))+1)-1)) - writemcfunction(os.path.join(path, 'data', bname, 'functions', 'play.mcfunction'), - """tag @s add {0} + writemcfunction(os.path.join(path, 'data', bname, 'functions', 'play.mcfunction'), + """tag @s add {0} scoreboard players set @s {0}_t -1 """.format(scoreObj)) - writemcfunction(os.path.join(path, 'data', bname, 'functions', 'pause.mcfunction'), - "tag @s remove {}".format(scoreObj)) - writemcfunction(os.path.join(path, 'data', bname, 'functions', 'stop.mcfunction'), - """tag @s remove {0} + writemcfunction(os.path.join(path, 'data', bname, 'functions', 'pause.mcfunction'), + "tag @s remove {}".format(scoreObj)) + writemcfunction(os.path.join(path, 'data', bname, 'functions', 'stop.mcfunction'), + """tag @s remove {0} scoreboard players reset @s {0} scoreboard players reset @s {0}_t""".format(scoreObj)) - writemcfunction(os.path.join(path, 'data', bname, 'functions', 'uninstall.mcfunction'), - """scoreboard objectives remove {0} + writemcfunction(os.path.join(path, 'data', bname, 'functions', 'uninstall.mcfunction'), + """scoreboard objectives remove {0} scoreboard objectives remove {0}_t""".format(scoreObj)) - text = '' - for k, v in instLayers.items(): - for i in range(len(v)): - text += 'execute run give @s minecraft:armor_stand{{display: {{Name: "{{\\"text\\":\\"{}\\"}}" }}, EntityTag: {{Marker: 1b, NoGravity:1b, Invisible: 1b, Tags: ["WNBS_Marker"], CustomName: "{{\\"text\\":\\"{}\\"}}" }} }}\n'.format( - "{}-{}".format(noteSounds[k]['name'], - i), "{}-{}".format(k, i) - ) - writemcfunction(os.path.join(path, 'data', bname, - 'functions', 'give.mcfunction'), text) - - tick = 0 - colNotes = {tick: []} - for note in data['notes']: - colNotes[tick].append(note) - while note['tick'] != tick: - tick += 1 - colNotes[tick] = [] - - for tick in range(length): - text = "" - if tick in colNotes: - currNotes = colNotes[tick] - for note in currNotes: - text += \ - """execute as @e[type=armor_stand, tag=WNBS_Marker, name=\"{inst}-{order}\"] at @s positioned ~ ~-1 ~ if block ~ ~ ~ minecraft:note_block[instrument={instname}] run setblock ~ ~ ~ minecraft:note_block[instrument={instname},note={key}] replace + text = '' + for k, v in instLayers.items(): + for i in range(len(v)): + text += 'execute run give @s minecraft:armor_stand{{display: {{Name: "{{\\"text\\":\\"{}\\"}}" }}, EntityTag: {{Marker: 1b, NoGravity:1b, Invisible: 1b, Tags: ["WNBS_Marker"], CustomName: "{{\\"text\\":\\"{}\\"}}" }} }}\n'.format( + "{}-{}".format(noteSounds[k]['name'], + i), "{}-{}".format(k, i) + ) + writemcfunction(os.path.join(path, 'data', bname, + 'functions', 'give.mcfunction'), text) + + tick = 0 + colNotes = {tick: []} + for note in data['notes']: + colNotes[tick].append(note) + while note['tick'] != tick: + tick += 1 + colNotes[tick] = [] + + for tick in range(length): + text = "" + if tick in colNotes: + currNotes = colNotes[tick] + for note in currNotes: + text += \ + """execute as @e[type=armor_stand, tag=WNBS_Marker, name=\"{inst}-{order}\"] at @s positioned ~ ~-1 ~ if block ~ ~ ~ minecraft:note_block[instrument={instname}] run setblock ~ ~ ~ minecraft:note_block[instrument={instname},note={key}] replace execute as @e[type=armor_stand, tag=WNBS_Marker, name=\"{inst}-{order}\"] at @s positioned ~ ~-1 ~ if block ~ ~ ~ minecraft:note_block[instrument={instname}] run fill ^ ^ ^-1 ^ ^ ^-1 minecraft:redstone_block replace minecraft:air execute as @e[type=armor_stand, tag=WNBS_Marker, name=\"{inst}-{order}\"] at @s positioned ~ ~-1 ~ if block ~ ~ ~ minecraft:note_block[instrument={instname}] run fill ^ ^ ^-1 ^ ^ ^-1 minecraft:air replace minecraft:redstone_block """.format(obj=scoreObj, tick=tick, inst=note['inst'], order=instLayers[note['inst']].index(note['layer']), instname=noteSounds[note['inst']]['name'], key=max(33, min(57, note['key'])) - 33) - if tick < length-1: - text += "scoreboard players set @s {}_t {}".format( - scoreObj, tick) - else: - text += "execute run function {}:stop".format(bname) - if text != "": - writemcfunction(os.path.join( - path, 'data', bname, 'functions', 'notes', str(tick)+'.mcfunction'), text) - - steps = floor(log2(length)) + 1 - pow = 2**steps - for step in range(steps): - searchrange = floor(pow / (2**step)) - segments = floor(pow / searchrange) - for segment in range(segments): - text = "" - half = floor(searchrange / 2) - lower = searchrange * segment - - min1 = lower - max1 = lower + half - 1 - min2 = lower + half - max2 = lower + searchrange - 1 - - if min1 <= length: - if step == steps-1: # Last step, play the tick + if tick < length-1: + text += "scoreboard players set @s {}_t {}".format( + scoreObj, tick) + else: + text += "execute run function {}:stop".format(bname) + if text != "": + writemcfunction(os.path.join( + path, 'data', bname, 'functions', 'notes', str(tick)+'.mcfunction'), text) + + steps = floor(log2(length)) + 1 + pow = 2**steps + for step in range(steps): + searchrange = floor(pow / (2**step)) + segments = floor(pow / searchrange) + for segment in range(segments): + text = "" + half = floor(searchrange / 2) + lower = searchrange * segment + + min1 = lower + max1 = lower + half - 1 + min2 = lower + half + max2 = lower + searchrange - 1 + + if min1 <= length: + if step == steps-1: # Last step, play the tick + try: + if len(colNotes[min1]) > 0: + text += "execute as @s[scores={{{0}={1}..{2}, {0}_t=..{3}}}] run function {4}:notes/{5}\n".format( + scoreObj, min1*80, (max1+1)*80+40, min1-1, bname, min1) + except KeyError: + break + if min2 <= length: try: - if len(colNotes[min1]) > 0: - text += "execute as @s[scores={{{0}={1}..{2}, {0}_t=..{3}}}] run function {4}:notes/{5}\n".format( - scoreObj, min1*80, (max1+1)*80+40, min1-1, bname, min1) + if len(colNotes[min2]) > 0: + text += "execute as @s[scores={{{0}={1}..{2}, {0}_t=..{3}}}] run function {4}:notes/{5}".format( + scoreObj, min2*80, (max2+1)*80+40, min2-1, bname, min2) except KeyError: break - if min2 <= length: - try: - if len(colNotes[min2]) > 0: - text += "execute as @s[scores={{{0}={1}..{2}, {0}_t=..{3}}}] run function {4}:notes/{5}".format( - scoreObj, min2*80, (max2+1)*80+40, min2-1, bname, min2) - except KeyError: - break - else: # Don't play yet, refine the search - for i in range(min1, min(max1, length)+1): - try: - if len(colNotes[i]) > 0: - text += "execute as @s[scores={{{}={}..{}}}] run function {}:tree/{}_{}\n".format( - scoreObj, min1*80, (max1+1)*80+40, bname, min1, max1) - break - except KeyError: + else: # Don't play yet, refine the search + for i in range(min1, min(max1, length)+1): + try: + if len(colNotes[i]) > 0: + text += "execute as @s[scores={{{}={}..{}}}] run function {}:tree/{}_{}\n".format( + scoreObj, min1*80, (max1+1)*80+40, bname, min1, max1) break - for i in range(min2, min(max2, length)+1): - try: - if len(colNotes[i]) > 0: - text += "execute as @s[scores={{{}={}..{}}}] run function {}:tree/{}_{}".format( - scoreObj, min2*80, (max2+2)*80+40, bname, min2, max2) - break - except KeyError: + except KeyError: + break + for i in range(min2, min(max2, length)+1): + try: + if len(colNotes[i]) > 0: + text += "execute as @s[scores={{{}={}..{}}}] run function {}:tree/{}_{}".format( + scoreObj, min2*80, (max2+2)*80+40, bname, min2, max2) break - if text != "": - writemcfunction(os.path.join( - path, 'data', bname, 'functions', 'tree', '{}_{}.mcfunction'.format(min1, max2)), text) - else: - break + except KeyError: + break + if text != "": + writemcfunction(os.path.join( + path, 'data', bname, 'functions', 'tree', '{}_{}.mcfunction'.format(min1, max2)), text) + else: + break if __name__ == "__main__": - app = MainWindow() - print('Creating app...') print(sys.argv) diff --git a/progressdialog.ui b/progressdialog.ui index f6f4f39..36b159d 100644 --- a/progressdialog.ui +++ b/progressdialog.ui @@ -84,19 +84,18 @@ True top - - - - - onCancel - 20 - Cancel - - ne - 0 - True - bottom - + + + onCancel + normal + Cancel + + se + True + bottom + + + diff --git a/toplevel.ui b/toplevel.ui index e2a6276..d16339d 100644 --- a/toplevel.ui +++ b/toplevel.ui @@ -1,7 +1,6 @@ - 620x670 350|370 both false @@ -22,7 +21,7 @@ true both - 5 + 10 5 True top @@ -112,62 +111,44 @@ - - openFiles - left - false + + openFiles Open... - 10 - w - 5 - 5 + 3 True left - - saveFiles + + saveFiles disabled Save... - 10 - w - 5 - 5 True left - - removeSelectedFiles + + removeSelectedFiles disabled Remove - 10 - e 5 - 5 True right - - addFiles - top - false + + addFiles Add... - 10 - e - 5 - 5 True right @@ -184,7 +165,7 @@ true both - 5 + 10 5 True top @@ -675,6 +656,8 @@ Flip + 5 + 5 200 True @@ -685,31 +668,26 @@ left Filp notes: - 5 - 5 True top - + Horizontally boolean:flipHorizontallyCheckVar - n - 10 True top - + Vertically boolean:flipVerticallyCheckVar - 10 True top @@ -725,63 +703,60 @@ 200 + 7 + 5 200 True top - + onArrangeModeChanged Not arrange none string:arrangeMode nw - 5 - 5 True top - + onArrangeModeChanged Collapse all notes collapse string:arrangeMode nw - 5 True top - + onArrangeModeChanged Arrange by instruments instruments string:arrangeMode nw - 5 - 0 True top - + disabled Treats percussions as an instrument boolean:groupPerc nw - 25 + 20 True top @@ -794,18 +769,15 @@ - - applyTool - arrow + + applyTool disabled Apply - 10 - y - 5 - 5 + e + 10 True - right + top From d8ffad515bb6f4110a0f82631b99db6c2d0c9f99 Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Thu, 24 Jun 2021 16:12:13 +0700 Subject: [PATCH 19/22] feat: Add ability to import from MuseScore 3 files MainWindow: - Update file path after having saved a unsaved file (i.e a file imported from MuseScore). MuseScoreImportDialog: - Allow selecting multiple files; - Allow specifying if note spaces should be expanded automatically; - Show an error dialog if a file couldn't be converted and skip that file. musescore2nbs: - Support converting: - Song title, author, tempo and time signature (with some limits); - Most of GM instruments to Minecraft instruments; - Percussion instruments (drum set); - Dotted, tied and normal notes and rests; - Notes' tuning and velocity; - Tuplets and notes whose duration less than 16th (by expanding the spaces between notes); - Voices. - Currently not supported: - Grace notes, glissando, arpeggio and other effects; - Dynamics; - Repeat signs with different endings; - Anything else. --- .gitignore | 2 + main.py | 175 ++++++++++++-- musescore2nbs.py | 482 +++++++++++++++++++++++++++++++++++++++ musescoreimportdialog.ui | 119 ++++++++++ nbsio.py | 11 +- toplevel.ui | 1 + 6 files changed, 769 insertions(+), 21 deletions(-) create mode 100644 musescore2nbs.py create mode 100644 musescoreimportdialog.ui diff --git a/.gitignore b/.gitignore index 64485a3..05f37db 100644 --- a/.gitignore +++ b/.gitignore @@ -333,6 +333,8 @@ ASALocalRun/ *.nbs *.mid *.mp3 +*.mscx +*.mscz datapacks/ Downloaded song/ diff --git a/main.py b/main.py index c915056..192a955 100644 --- a/main.py +++ b/main.py @@ -29,6 +29,7 @@ import asyncio from pathlib import Path +from ast import literal_eval import tkinter as tk import tkinter.ttk as ttk @@ -52,6 +53,7 @@ from nbsio import NBS_VERSION, NbsSong from ncfio import writencf from nbs2midi import nbs2midi +from musescore2nbs import musescore2nbs vaniNoteSounds = [ {'filename': 'harp.ogg', 'name': 'harp'}, @@ -72,6 +74,8 @@ {'filename': 'pling.ogg', 'name': 'pling'} ] +globalIncVar = 0 + # Credit: https://stackoverflow.com/questions/42474560/pyinstaller-single-exe-file-ico-image-in-title-of-tkinter-main-window def resource_path(*args): if getattr(sys, 'frozen', False): @@ -260,6 +264,17 @@ def openFiles(self, _=None): self.songsData.clear() self.addFiles() + def addFileInfo(self, filePath: str, songData: NbsSong) -> None: + global globalIncVar + if filePath == '': + globalIncVar += 1 + filePath = "[Not saved] ({})".format(globalIncVar) + header = songData.header + length = timedelta(seconds=floor( + header['length'] / header['tempo'])) if header['length'] != None else "Not calculated" + self.fileTable.insert("", 'end', text=filePath, values=( + length, header.name, header.author, header.orig_author)) + def addFiles(self, _=None, paths=()): types = [("Note Block Studio files", '*.nbs'), ('All files', '*')] addedPaths = [] @@ -280,14 +295,7 @@ def addFiles(self, _=None, paths=()): "Cannot read or parse file: "+filePath) print(traceback.format_exc()) continue - header = songData.header - length = timedelta(seconds=floor( - header['length'] / header['tempo'])) if header['length'] != None else "Not calculated" - name = header['name'] - author = header['author'] - orig_author = header['orig_author'] - self.fileTable.insert("", 'end', text=filePath, values=( - length, name, author, orig_author)) + self.addFileInfo(filePath, songData) if i % 3 == 0: self.mainwin.update() self.filePaths.extend(addedPaths) @@ -302,18 +310,25 @@ def saveFiles(self, _=None): fileTable = self.fileTable if len(selection := fileTable.selection()) > 0: if len(selection) == 1: - filePath = os.path.basename( - self.filePaths[fileTable.index(selection[0])]) + i = fileTable.index(selection[0]) + filePath = self.filePaths[i] types = [('Note Block Studio files', '*.nbs'), ('All files', '*')] + fileName = '' + if filePath != '': + fileName = os.path.basename(filePath) + else: + fileName = self.songsData[i].header.import_name path = asksaveasfilename( - filetypes=types, initialfile=filePath, defaultextension=".nbs") + filetypes=types, initialfile=fileName.rsplit('.', 1)[0], defaultextension=".nbs") if path == '': return + if filePath == '': + fileTable.item(selection[0], text=os.path.join(path)) self.builder.get_object('applyBtn')['state'] = 'disabled' self.disabledFileTable() self.mainwin.update() - self.songsData[fileTable.index(selection[0])].write(path) + self.songsData[i].write(path) self.enableFileTable() self.builder.get_object('applyBtn')['state'] = 'normal' return @@ -326,8 +341,12 @@ def saveFiles(self, _=None): for item in selection: i = fileTable.index(item) filePath = self.filePaths[i] - self.songsData[i].write(os.path.join( - path, os.path.basename(filePath))) + if filePath == '': + filePath = self.songsData[i].header.import_name.rsplit('.', 1)[0] + '.nbs' + savedPath = os.path.join(path, filePath) + fileTable.item(item, text=savedPath) + self.filePaths[i] = savedPath + self.songsData[i].write(os.path.join(path, os.path.basename(filePath))) self.enableFileTable() self.builder.get_object('applyBtn')['state'] = 'normal' @@ -341,9 +360,14 @@ def saveAll(self, _=None): self.builder.get_object('applyBtn')['state'] = 'disabled' self.disabledFileTable() self.mainwin.update() + items = self.fileTable.get_children() for i, filePath in enumerate(self.filePaths): - self.songsData[i].write(os.path.join( - path, os.path.basename(filePath))) + if filePath == '': + filePath = self.songsData[i].header.import_name.rsplit('.', 1)[0] + '.nbs' + savedPath = os.path.join(path, filePath) + self.fileTable.item(items[i], text=savedPath) + self.filePaths[i] = savedPath + self.songsData[i].write(os.path.join(path, os.path.basename(filePath))) self.enableFileTable() self.builder.get_object('applyBtn')['state'] = 'normal' @@ -377,6 +401,11 @@ def initArrangeTab(self): self.arrangeMode.set('none') # self.builder.get_object('arrangeGroupPrec').state(('!selected',)) + def callMuseScoreImportDialog(self): + dialog = MuseScoreImportDialog(self.toplevel, self) + dialog.run() + del dialog + def callDatapackExportDialog(self): dialog = DatapackExportDialog(self.toplevel, self) dialog.run() @@ -777,6 +806,119 @@ def onCancel(self) -> None: pass self.d.destroy() +def parseFilePaths(string: str) -> list: + strLen = len(string) + ret = [] + filePath = '' + isQuoted = False + for i, char in enumerate(string): + i: int + char: str + if char == '(': + isQuoted = True + elif char == ')': + isQuoted = False + elif i+1 == strLen and filePath != '': + ret.append(filePath) + elif char.isspace(): + if isQuoted: + filePath += char + elif filePath != '': + ret.append(filePath) + filePath = '' + else: + filePath += char + return tuple(ret) + +class MuseScoreImportDialog: + def __init__(self, master, parent): + self.master = master + self.parent = parent + + self.builder = builder = pygubu.Builder() + builder.add_resource_path(resource_path()) + builder.add_from_file(resource_path('musescoreimportdialog.ui')) + + self.d: Dialog = builder.get_object('dialog', master) + + builder.connect_callbacks(self) + builder.import_variables(self) + + self.autoExpand.set(True) + self.filePaths.trace_add("write", self.pathChanged) + + def run(self): + self.d.run() + + def browse(self): + types = (("MuseScore files", ('*.mscz', '*.mscx')), ('All files', '*'),) + paths = askopenfilenames(filetypes=types) + + self.filePaths.set(paths) + + def autoExpandChanged(self): + self.builder.get_object('expandScale')['state'] = 'disabled' if self.autoExpand.get() else 'normal' + + def pathChanged(self, *args): + self.builder.get_object('importBtn')['state'] = 'normal' if (self.filePaths.get() != '') or (self.exportPath.get() != '') else 'disabled' + + def onImport(self, _=None): + fileTable = self.parent.fileTable + + if not self.autoExpand.get(): + self.pathChanged() + if self.filePaths.get() == '': + self.pathChanged() + return + + paths = literal_eval(self.filePaths.get()) + if isinstance(paths, str): + paths = parseFilePaths(paths) + fileCount = len(paths) + + async def work(dialog: ProgressDialog = None): + try: + songsData: list = self.parent.songsData + filePaths: list = self.parent.filePaths + for i, filePath in enumerate(paths): + try: + dialog.totalProgress.set(i) + dialog.totalText.set("Importing {} / {} files".format(i+1, fileCount)) + dialog.currentProgress.set(0) + dialog.currentText.set("Current file: {}".format(filePath)) + task = asyncio.create_task(musescore2nbs(filePath, self.expandMult.get(), self.autoExpand.get(), dialog)) + while True: + done, pending = await asyncio.wait({task}, timeout=0) + if task in done: + songData = task.result() + await task + break + if not songData: + raise Exception("The file {} cannot be read as a vaild XML file.".format(filePath)) + dialog.currentProgress.set(80) + await asyncio.sleep(0.001) + songsData.append(songData) + filePaths.append('') + self.parent.addFileInfo('', songData) + await asyncio.sleep(0.001) + except asyncio.CancelledError: + raise + except Exception as e: + print('except Exception as e:', e) + showerror("Importing file error", "Cannot import file \"{}\"\n{}".format(filePath, e)) + print(traceback.format_exc()) + continue + dialog.totalProgress.set(dialog.currentMax) + except asyncio.CancelledError: + raise + # self.d.toplevel.after(1, self.d.destroy) + + print('Creating dialog') + dialog = ProgressDialog(self.d.toplevel, self) + # dialog.d.bind('<>', lambda _: self.d.destroy()) + dialog.d.set_title("Importing {} MuseScore files".format(fileCount)) + dialog.totalMax = fileCount + dialog.run(work) class FlexCheckbutton(tk.Checkbutton): def __init__(self, *args, **kwargs): @@ -897,7 +1039,6 @@ def makeFolderTree(inp, a=[]): makeFolderTree(v, a + [k]) elif isinstance(inp, str): p = os.path.join(*a, inp) - # print(p) os.makedirs(p, exist_ok=True) else: return diff --git a/musescore2nbs.py b/musescore2nbs.py new file mode 100644 index 0000000..9b1991a --- /dev/null +++ b/musescore2nbs.py @@ -0,0 +1,482 @@ +from addict import Dict +from lxml import etree +import zipfile +from nbsio import NbsSong +from functools import lru_cache +from os.path import basename +from re import search +from asyncio import sleep + +INST_INFO = ( + ('Acoustic Grand Piano', 0, 0, 'Grand Piano'), + ('Bright Acoustic Piano', 0, 0, 'Acoustic Piano'), + ('Electric Grand Piano', 13, 0, 'E. Grand Piano'), + ('Honky - tonk Piano', 0, 0, 'H.T. Piano'), + ('Electric Piano 1', 13, 0, 'E. Piano 1'), + ('Electric Piano 2', 13, 0, 'E. Piano 2'), + ('Harpsichord', 0, 1, ''), + ('Clavinet', 0, 0, ''), + ('Celesta', 11, -1, ''), + ('Glockenspiel', 11, 0, ''), + ('Music Box', 11, 0, ''), + ('Vibraphone', 11, 0, ''), + ('Marimba', 11, 0, ''), + ('Xylophone', 9, 0, ''), + ('Tubular Bells', 7, -1, 'T. Bells'), + ('Dulcimer', 7, 0, ''), + ('Drawbar Organ', 1, 1, 'D. Organ'), + ('Percussive Organ', 1, 1, 'P. Organ'), + ('Rock Organ', 0, 0, ''), + ('Church Organ', 0, 0, ''), + ('Reed Organ', 0, 0, ''), + ('Accordion', 0, 0, ''), + ('Harmonica', 0, 0, ''), + ('Tango Accordion', 0, 0, 'T. Accordion'), + ('Acoustic Guitar (nylon)', 5, 0, 'A.Guitar(nylon)'), + ('Acoustic Guitar (steel)', 5, 0, 'A.Guitar(steel)'), + ('Electric Guitar (jazz)', 5, 1, 'E.Guitar(jazz)'), + ('Electric Guitar (clean)', 5, 0, 'E.Guitar(clean)'), + ('Electric Guitar (muted)', -1, 0, 'E.Guitar(mute)'), + ('Overdriven Guitar', 5, -1, 'OD Guitar'), + ('Distortion Guitar', 5, -1, 'Dist. Guitar'), + ('Guitar Harmonics', 5, 0, 'Guitar H.'), + ('Acoustic Bass', 1, 1, 'A. Bass'), + ('Electric Bass (finger)', 1, 2, 'E.Bass (finger)'), + ('Electric Bass (pick)', 1, 2, 'E.Bass (pick)'), + ('Fretless Bass', 1, 2, ''), + ('Slap Bass 1', 1, 2, ''), + ('Slap Bass 2', 1, 2, ''), + ('Synth Bass 1', 1, 2, ''), + ('Synth Bass 2', 1, 2, ''), + ('Violin', 6, 0, ''), + ('Viola', 6, 0, ''), + ('Cello', 6, 0, ''), + ('Contrabass', 6, 0, ''), + ('Tremolo Strings', 0, 0, 'T. Strings'), + ('Pizzicato Strings', 0, 0, 'P. Strings'), + ('Orchestral Harp', 8, 0, 'O. Harp'), + ('Timpani', 3, 1, ''), + ('String Ensemble 1', 0, 0, 'String E. 1'), + ('String Ensemble 2', 0, 0, 'String E. 2'), + ('Synth Strings 1', 0, 0, 'S. Strings 1'), + ('Synth Strings 2', 0, 0, 'S. Strings 2'), + ('Choir Aahs', 0, 0, ''), + ('Voice Oohs', 0, 0, ''), + ('Synth Choir', 0, 0, ''), + ('Orchestra hit', 0, 0, 'O. Hit'), + ('Trumpet', 0, 0, ''), + ('Trombone', 0, 0, ''), + ('Tuba', 0, 0, ''), + ('Muted Trumpet', 0, 0, ''), + ('French Horn', 0, 0, ''), + ('Brass Section', 0, 0, ''), + ('Synth Brass 1', 1, 1, 'S. Brass 1'), + ('Synth Brass 2', 1, 1, 'S. Brass 2'), + ('Soprano Sax', 6, 0, ''), + ('Alto Sax', 6, 0, ''), + ('Tenor Sax', 6, 0, ''), + ('Baritone Sax', 6, 0, ''), + ('Oboe', 6, 0, ''), + ('English Horn', 6, 0, ''), + ('Bassoon', 6, -1, ''), + ('Clarinet', 6, 0, ''), + ('Piccolo', 6, -1, ''), + ('Flute', 6, -1, ''), + ('Recorder', 6, -1, ''), + ('Pan Flute', 6, -1, ''), + ('Blown Bottle', 6, -1, ''), + ('Shakuhachi', 6, -1, ''), + ('Whistle', 6, -1, ''), + ('Ocarina', 6, -1, ''), + ('Lead 1 (square)', 0, 0, 'L.1 (square)'), + ('Lead 2 (sawtooth)', 0, 0, 'L.2 (sawtooth)'), + ('Lead 3 (calliope)', 0, 0, 'L.3 (calliope)'), + ('Lead 4 (chiff)', 0, 0, 'L.4 (chiff)'), + ('Lead 5 (charang)', 0, 0, 'L.5 (charang)'), + ('Lead 6 (voice)', 0, 0, 'L.6 (voice)'), + ('Lead 7 (fifths)', 0, 0, 'L.7 (fifths)'), + ('Lead 8 (bass + lead)', 0, 1, 'L.8 (bass + lead)'), + ('Pad 1 (new age)', 0, 0, 'P.1 (new age)'), + ('Pad 2 (warm)', 0, 0, 'P.2 (warm)'), + ('Pad 3 (polysynth)', 0, 0, 'P.3 (polysynth)'), + ('Pad 4 (choir)', 0, 0, 'P.4 (choir)'), + ('Pad 5 (bowed)', 0, 0, 'P.5 (bowed)'), + ('Pad 6 (metallic)', 0, 0, 'P.6 (metallic)'), + ('Pad 7 (halo)', 0, 0, 'P.7 (halo)'), + ('Pad 8 (sweep)', 0, 0, 'P.8 (sweep)'), + ('FX 1 (rain)', -1, 0, 'Fx (rain)'), + ('FX 2 (soundtrack)', -1, 0, 'Fx (strack)'), + ('FX 3 (crystal)', 13, 0, 'Fx (crystal)'), + ('FX 4 (atmosphere)', 0, 0, 'Fx (atmosph.)'), + ('FX 5 (brightness)', 0, 0, 'Fx (bright.)'), + ('FX 6 (goblins)', -1, 0, 'Fx (goblins)'), + ('FX 7 (echoes)', -1, 0, 'Fx (echoes)'), + ('FX 8 (sci - fi)', -1, 0, 'Fx (sci - fi)'), + ('Sitar', 14, 0, ''), + ('Banjo', 14, 0, ''), + ('Shamisen', 14, 0, ''), + ('Koto', 14, 0, ''), + ('Kalimba', 1, 1, ''), + ('Bagpipe', 0, 0, ''), + ('Fiddle', 0, 0, ''), + ('Shanai', 0, 0, ''), + ('Tinkle Bell', 7, -1, ''), + ('Agogo', 0, 0, ''), + ('Steel Drums', 10, 0, ''), + ('Woodblock', 4, 0, ''), + ('Taiko Drum', 3, 0, ''), + ('Melodic Tom', 3, -1, ''), + ('Synth Drum', 3, 0, ''), + ('Reverse Cymbal', -1, 0, 'Rev. Cymbal'), + ('Guitar Fret Noise', -1, 0, 'Guitar F. Noise'), + ('Breath Noise', -1, 0, ''), + ('Seashore', -1, 0, ''), + ('Bird Tweet', -1, 0, ''), + ('Telephone Ring', -1, 0, 'Telephone'), + ('Helicopter', -1, 0, ''), + ('Applause', -1, 0, ''), + ('Gunshot', 0, 0, ''), + ('Percussion', -1, 0, ''), +) + +DRUM_INFO = ( + None, None, None, None, None, + None, None, None, None, None, + None, None, None, None, None, + None, None, None, None, None, + None, None, None, None, + ('Zap', -1, 0), + ('Brush hit hard', -1, 0), + ('Brush circle', -1, 0), + ('Brush hit soft', -1, 0), + ('Brush hit and circle', -1, 0), + ('Drumroll', -1, 0), + ('Castanets', -1, 0), + ('Snare Drum 3', -1, 0), + ('Drumsticks hitting', -1, 0), + ('Bass Drum 3', -1, 0), + ('Hard hit snare', -1, 0), + ('Bass Drum 2', 2, 10), + ('Bass Drum 1', 2, 6), + ('Side Stick', 4, 6), + ('Snare Drum 1', 3, 8), + ('Hand Clap', 4, 6), + ('Snare Drum 2', 3, 4), + ('Low Tom 2', 2, 6), + ('Closed Hi - hat', 3, 22), + ('Low Tom 1', 2, 13), + ('Pedal Hi - hat', 3, 22), + ('Mid Tom 2', 2, 15), + ('Open Hi - hat', 3, 18), + ('Mid Tom 1', 2, 20), + ('High Tom 2', 2, 23), + ('Crash Cymbal 1', 3, 17), + ('High Tom 1', 2, 23), + ('Ride Cymbal 1', 3, 24), + ('Chinese Cymbal', 3, 8), + ('Ride Bell', 3, 13), + ('Tambourine', 4, 18), + ('Splash Cymbal', 3, 18), + ('Cowbell', 4, 1), + ('Crash Cymbal 2', 3, 13), + ('Vibra Slap', 4, 2), + ('Ride Cymbal 2', 3, 13), + ('High Bongo', 4, 9), + ('Low Bongo', 4, 2), + ('Mute High Conga', 4, 8), + ('Open High Conga', 2, 22), + ('Low Conga', 2, 15), + ('High Timbale', 3, 13), + ('Low Timbale', 3, 8), + ('High Agog�', 4, 8), + ('Low Agog�', 4, 3), + ('Cabasa', 4, 20), + ('Maracas', 4, 23), + ('Short Whistle', -1, 0), + ('Long Whistle', -1, 0), + ('Short G�iro', 4, 17), + ('Long G�iro', 4, 11), + ('Claves', 4, 18), + ('High Wood Block', 4, 9), + ('Low Wood Block', 4, 5), + ('Mute Cu�ca', -1, 0), + ('Open Cu�ca', -1, 0), + ('Mute Triangle', 4, 17), + ('Open Triangle', 4, 22), + ('Shaker', 3, 22), + ('Jingle bell', -1, 0), + ('Bell tree', -1, 0), + ('Castanets', 4, 21), + ('Mute Surdo', 2, 14), + ('Open Surdo', 2, 7), +) + +expandMulDict = { + "64th": 4, + "32nd": 2, +} +tupletMulDict = { + "64th": 6, + "32nd": 5, + "16th": 4, + "eighth": 3, + "quarter": 2, +} + +durationMap = { + "128th": 0.125, + "64th": 0.25, + "32nd": 0.5, + "16th": 1, + "eighth": 2, + "quarter": 4, + "half": 8, + "whole": 16, +} + +MAX_TEMPO = 30 + +@lru_cache +def fraction2length(fraction: str) -> int: + if isinstance(fraction, str): + parts = fraction.split('/') + return int(parts[0]) * int(16 / int(parts[1])) + return 0 + +async def musescore2nbs(filepath: str, expandMultiplier=1, autoExpand=True, dialog=None) -> NbsSong: + """Convert a MuseScore file and return a NbsSong instance. + +if the conversation fails, this function returns None. + +Args: +- filepath: The path of the input file. .mscz and .mscx files are supported. +- expandMultiplier: Multiplys all note positions by this variable. + The default is 1, meaning not multiplying +- autoExpand: Optional; If it's True, the expand multiplier will be detected automatically. +- dialog: Optional; The ProgressDialog to be used for reporting progress. + +Return: + A NbsSong contains meta-information and notes' data (position, pitch, velocity + and tuning). None if the conversation fails. + """ + + if autoExpand: + expandMultiplier = 1 + + # Reads the input file + + xml: etree.ElementTree = None + if filepath.endswith(".mscz"): + with zipfile.ZipFile(filepath, 'r') as zip: + filename: str = "" + for name in zip.namelist(): + if name.endswith(".mscx"): + filename = name + break + if filename: + with zip.open(filename) as file: + xml = etree.parse(file, parser=None) + elif filepath.endswith(".mscx"): + xml = etree.parse(filepath, parser=None) + + if xml is None: + return None + if version := xml.findtext('programVersion'): + if version.startswith('2.'): + raise NotImplementedError("MuseScore 2 files are not supported. Please use MuseScore 3 to re-save the files before importing.") + + nbs: NbsSong = NbsSong() + + # Get meta-information + + header: Dict = nbs.header + score: etree.Element = xml.find("Score") + header.import_name = basename(filepath) + header.name = (score.xpath("metaTag[@name='workTitle']/text()") or ('',))[0] + header.author = (score.xpath("metaTag[@name='arranger']/text()") or ('',))[0] + header.orig_author = (score.xpath("metaTag[@name='composer']/text()") or ('',))[0] + + if timeSign := score.findtext("Staff/Measure/voice/TimeSig/sigN"): + header.time_sign = int(timeSign) + + if tempoTxt := score.findtext("Staff/Measure/voice/Tempo/tempo"): + bpm: float = 60 * float(tempoTxt) + tps: float = bpm * 4 / 60 + header.tempo = tps + + if dialog: + dialog.currentProgress.set(20) + await sleep(0.001) + + # Remove empty layers + + emptyStaffs: list = [] + staffCount = 0 + for staff in score.iterfind("Staff"): + staffCount += 1 + for elem in staff.xpath("Measure/voice/*"): + if elem.tag == "Chord": + break + else: + staffId = int(staff.get("id")) + emptyStaffs.append(staffId) + + # Get layer instruments from staff program IDs + + staffInsts = {} + for part in score.iterfind("Part"): + isPerc = bool(part.xpath("Instrument[@id='drumset']")) or bool(part.xpath("Staff/StaffType[@group='percussion']")) + program = int(part.find("Instrument/Channel/program").get("value")) + for staff in part.iterfind("Staff"): + staffId = int(staff.get("id")) + if staffId not in emptyStaffs: + # INST_INFO[-1] is the percussion (drumset) instrument + staffInsts[staffId] = INST_INFO[program] if not isPerc else INST_INFO[-1] + + tempo: float = header.tempo + + if dialog: + dialog.currentProgress.set(30) + await sleep(0.001) + + # Perform note auto-expanding (if specified) and tuplet detection + + hasComplexTuplets = False + for elem in score.xpath("Staff/Measure/voice/*"): + if elem.tag == "Tuplet": + normalNotes = int(elem.findtext("normalNotes")) + actualNotes = int(elem.findtext("actualNotes")) + if not hasComplexTuplets: + hasComplexTuplets = actualNotes != normalNotes + if hasComplexTuplets and autoExpand and (baseNote := elem.findtext("baseNote")): + if baseNote in tupletMulDict: + multiplier = max(tupletMulDict[baseNote], expandMultiplier) + if (tempo * multiplier) <= MAX_TEMPO: + expandMultiplier = multiplier + if autoExpand and (duration := elem.findtext("durationType")): + if duration in expandMulDict: + multiplier = max(expandMulDict[duration], expandMultiplier) + if (tempo * multiplier) <= MAX_TEMPO: + expandMultiplier = multiplier + + header.tempo = int(tempo * expandMultiplier) + header.time_sign *= expandMultiplier + + if dialog: + dialog.currentProgress.set(40) + await sleep(0.001) + + # Import note data + + baseLayer = -1 + ceilingLayer = baseLayer + for staff in score.iterfind("Staff"): + staffId = int(staff.get("id")) + if staffId in emptyStaffs: + continue + baseLayer = ceilingLayer + 1 + chords = 0 + rests = 0 + tick = 0 + lastTick = -1 + layer = -1 + staffInst = staffInsts[staffId][1] + for measure in staff.iterfind("Measure"): + beginTick = tick + endTick = -1 + innerBaseLayer = baseLayer + innerCeilingLayer = innerBaseLayer + for voi, voice in enumerate(measure.iterfind("voice")): + tick = beginTick + tick += fraction2length(voice.findtext("location/fractions")) * expandMultiplier + tupletNotesRemaining = 0 + normalNotes = 0 + actualNotes = 0 + for elem in voice: + #print(f'{elem.tag=}, {tick=}, {tickCheckpoint=}') + dots = int(elem.findtext("dots") or 0) + if elem.tag == "Chord": + chords += 1 + enoughSpace = int(tick) != int(lastTick) + # print(f'{int(lastTick)=} {int(tick)=} {enoughSpace=}') + for i, note in enumerate(elem.iterfind("Note")): + if voi > 0: + innerBaseLayer = innerCeilingLayer + if note.xpath("Spanner[@type='Tie']/prev"): + break + if not enoughSpace: + layer += 1 + else: + layer = innerBaseLayer+i + if layer >= len(nbs.layers): + nbs.layers.append(Dict({'name': "{} (v. {})".format(staffInsts[staffId][0], voi+1), 'lock':False, 'volume':100, 'stereo':100})) + inst = staffInst + isPerc = False + if inst > -1: + key = int(note.find("pitch").text) - 21 + else: + drumIndex = int(note.find("pitch").text) + if drumIndex > 23: # Vrevent access to None values in DRUM_INFO + _, inst, key = DRUM_INFO[drumIndex if drumIndex < len(DRUM_INFO) else 24] + key += 36 + isPerc = True + if inst == -1: inst = 0 + tuning = note.find("tuning") + pitch = int(float(tuning.text)) if tuning is not None else 0 + # TODO: Support relative velocity + vel = max(min(int(note.findtext("velocity") or 100), 127), 0) + nbs.notes.append(Dict({'tick': int(tick), 'layer': layer, 'inst': inst, + 'key': key, 'vel': vel, 'pan': 100, 'pitch': pitch, 'isPerc': isPerc})) + ceilingLayer = max(ceilingLayer, layer) + innerCeilingLayer = max(innerCeilingLayer, i) + lastTick = tick + length = durationMap[elem.findtext("durationType")] * (2-(1/(2**dots))) * expandMultiplier + if hasComplexTuplets and (tupletNotesRemaining > 0): + length = length * normalNotes / actualNotes + tupletNotesRemaining -= 1 + tick += length + elif elem.tag == "Rest": + rests += 1 + durationType = elem.findtext("durationType") + length = 0 + if durationType == "measure": + length = fraction2length(elem.findtext("duration")) * expandMultiplier + else: + length = int(durationMap[durationType] * (2-(1/(2**dots))) * expandMultiplier) + if hasComplexTuplets and (tupletNotesRemaining > 0): + length = length * normalNotes / actualNotes + tupletNotesRemaining -= 1 + tick += length + elif (elem.tag == "Tuplet") and hasComplexTuplets: + normalNotes = int(elem.findtext("normalNotes")) + actualNotes = int(elem.findtext("actualNotes")) + tupletNotesRemaining = actualNotes + endTick = max(endTick, tick) + tick = round(endTick) + # print(f'{tick=}, {chords=}, {rests=}, {ceilingLayer=}') + if dialog: + dialog.currentProgress.set(40 + staffId * 40 / staffCount) + await sleep(0.001) + + nbs.correctData() + return nbs + +if __name__ == '__main__': + # filepath = "sayonara_30_seconds.mscx" + # filepath = "chord.mscx" + # filepath = "Vì_yêu_cứ_đâm_đầu.mscx" + # filepath = "Ai_yêu_Bác_Hồ_Chí_Minh.mscz" + # filepath = "test.mscx" + # filepath = "test_microtones.mscx" + # filepath = "Shining_in_the_Sky.mscx" + # filepath = "voices.mscx" + # filepath = "AYBHCM_1_2.mscx" + # filepath = "inst.mscx" + # filepath = "Chay ngay di.mscx" + # filepath = "1note.mscx" + # filepath = "Ai_đưa_em_về_-_Nguyễn_Ánh_9_-_Phiên_bản_dễ_(Easy_version).mscx" + # filepath = 'halfTempo.mscx' + filepath = "A_Tender_Feeling_(Sword_Art_Online).mscz" + nbs = musescore2nbs(filepath, autoExpand=False) + print(nbs) + nbs.write(filepath.rsplit('.', 1)[0] + '.nbs') diff --git a/musescoreimportdialog.ui b/musescoreimportdialog.ui new file mode 100644 index 0000000..50e6d2a --- /dev/null +++ b/musescoreimportdialog.ui @@ -0,0 +1,119 @@ + + + + 100 + false + 200 + + + 200 + 5 + 200 + + true + both + 5 + 3 + True + top + + + + 200 + 5 + 5 + Select files + 200 + + true + both + True + top + + + + string:filePaths + + true + x + True + left + + + + + + browse + Browse... + + True + left + + + + + + + + 200 + 5 + 5 + Options + 200 + + true + both + 7 + True + top + + + + autoExpandChanged + Automatically expand the spaces between notes to fit as many notes as possible. + boolean:autoExpand + + nw + True + top + + + + + + 1 + Expand multiplier: +(will be ignored if the calculated tempo > 30 TPS) + 300 + horizontal + 1 + disabled + 1 + 30 + string:expandMult + + true + x + True + top + + + + + + + + onImport + disabled + Import + + e + True + bottom + + + + + + + diff --git a/nbsio.py b/nbsio.py index 77a8785..4c86c8f 100644 --- a/nbsio.py +++ b/nbsio.py @@ -28,10 +28,10 @@ from addict import Dict -BYTE = Struct(' None: def write(self, fn: str) -> None: '''Save nbs data to a file on disk with the path given.''' + if not fn.endswith('.nbs'): + fn += '.nbs' + if fn != '' and self.header and self.notes: writeNumeric = write_numeric writeString = write_string diff --git a/toplevel.ui b/toplevel.ui index d16339d..c163323 100644 --- a/toplevel.ui +++ b/toplevel.ui @@ -848,6 +848,7 @@ 0 + callMuseScoreImportDialog from MuseScore files... 0 From 4dab67929a34e4226d04ce88d190f161a21f0cd6 Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Thu, 24 Jun 2021 21:16:54 +0700 Subject: [PATCH 20/22] cleanup: Improve error handling and update requirements --- .gitignore | 3 +- .vscode/launch.json | 78 +++++++++++++++++++++++++++++++++++++++++++ .vscode/settings.json | 5 +++ main.py | 59 ++++++++++++++++++-------------- musescore2nbs.py | 15 ++++++--- requirements.txt | 10 +++--- 6 files changed, 134 insertions(+), 36 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 05f37db..bceed5d 100644 --- a/.gitignore +++ b/.gitignore @@ -345,12 +345,11 @@ Downloaded song/ *.pyo version.* -.vscode/ - build/ dist/ test/ main.build/ main.dist/ +.mscbackup/ setup.py diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..7761838 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,78 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File (Integrated Terminal)", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + }, + { + "name": "Python: Current File and Libs", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "justMyCode": false + }, + { + "name": "Python: Remote Attach", + "type": "python", + "request": "attach", + "port": 5678, + "host": "localhost", + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "." + } + ] + }, + { + "name": "Python: Module", + "type": "python", + "request": "launch", + "module": "enter-your-module-name-here", + "console": "integratedTerminal" + }, + { + "name": "Python: Django", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/manage.py", + "console": "integratedTerminal", + "args": [ + "runserver", + "--noreload", + "--nothreading" + ], + "django": true + }, + { + "name": "Python: Flask", + "type": "python", + "request": "launch", + "module": "flask", + "env": { + "FLASK_APP": "app.py" + }, + "args": [ + "run", + "--no-debugger", + "--no-reload" + ], + "jinja": true + }, + { + "name": "Python: Current File (External Terminal)", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "externalTerminal" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2a3f8d1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.pythonPath": "C:\\Users\\Admin\\AppData\\Local\\Programs\\Python\\Python38-32\\python.exe", + "python.jediEnabled": false, + "python.languageServer": "Microsoft", +} \ No newline at end of file diff --git a/main.py b/main.py index 192a955..0858414 100644 --- a/main.py +++ b/main.py @@ -290,9 +290,8 @@ def addFiles(self, _=None, paths=()): try: songData = NbsSong(filePath) self.songsData.append(songData) - except Exception: - showerror("Reading file error", - "Cannot read or parse file: "+filePath) + except Exception as e: + showerror("Opening file error", 'Cannot open file "{}"\n{}: {}'.format(filePath, e.__class__.__name__, e)) print(traceback.format_exc()) continue self.addFileInfo(filePath, songData) @@ -339,14 +338,18 @@ def saveFiles(self, _=None): Path(path).mkdir(parents=True, exist_ok=True) self.disabledFileTable() for item in selection: - i = fileTable.index(item) - filePath = self.filePaths[i] - if filePath == '': - filePath = self.songsData[i].header.import_name.rsplit('.', 1)[0] + '.nbs' - savedPath = os.path.join(path, filePath) - fileTable.item(item, text=savedPath) - self.filePaths[i] = savedPath - self.songsData[i].write(os.path.join(path, os.path.basename(filePath))) + try: + i = fileTable.index(item) + filePath = self.filePaths[i] + if filePath == '': + filePath = self.songsData[i].header.import_name.rsplit('.', 1)[0] + '.nbs' + savedPath = os.path.join(path, filePath) + fileTable.item(item, text=savedPath) + self.filePaths[i] = savedPath + self.songsData[i].write(os.path.join(path, os.path.basename(filePath))) + except Exception as e: + showerror("Saving file error", 'Cannot save file "{}"\n{}: {}'.format(os.path.join(path, os.path.basename(filePath)), e.__class__.__name__, e)) + print(traceback.format_exc()) self.enableFileTable() self.builder.get_object('applyBtn')['state'] = 'normal' @@ -362,12 +365,16 @@ def saveAll(self, _=None): self.mainwin.update() items = self.fileTable.get_children() for i, filePath in enumerate(self.filePaths): - if filePath == '': - filePath = self.songsData[i].header.import_name.rsplit('.', 1)[0] + '.nbs' - savedPath = os.path.join(path, filePath) - self.fileTable.item(items[i], text=savedPath) - self.filePaths[i] = savedPath - self.songsData[i].write(os.path.join(path, os.path.basename(filePath))) + try: + if filePath == '': + filePath = self.songsData[i].header.import_name.rsplit('.', 1)[0] + '.nbs' + savedPath = os.path.join(path, filePath) + self.fileTable.item(items[i], text=savedPath) + self.filePaths[i] = savedPath + self.songsData[i].write(os.path.join(path, os.path.basename(filePath))) + except Exception as e: + showerror("Saving file error", 'Cannot save file "{}"\n{}: {}'.format(os.path.join(path, os.path.basename(filePath)), e.__class__.__name__, e)) + print(traceback.format_exc()) self.enableFileTable() self.builder.get_object('applyBtn')['state'] = 'normal' @@ -722,11 +729,15 @@ async def work(dialog: ProgressDialog = None): filePath = path.join(path.dirname(origPath), baseName) else: filePath = path.join(self.exportPath.get(), baseName) - dialog.currentText.set("Current file: {}".format(filePath)) - songData = deepcopy(songsData[i]) - compactNotes(songData, True) - dialog.currentProgress.set(10) # 10% - await nbs2midi(songData, filePath, dialog) + try: + dialog.currentText.set("Current file: {}".format(filePath)) + songData = deepcopy(songsData[i]) + compactNotes(songData, True) + dialog.currentProgress.set(10) # 10% + await nbs2midi(songData, filePath, dialog) + except Exception as e: + showerror("Exporting file error", 'Cannot export file "{}"\n{}: {}'.format(filePath, e.__class__.__name__, e)) + print(traceback.format_exc()) dialog.totalProgress.set(dialog.currentMax) except asyncio.CancelledError: raise @@ -904,8 +915,7 @@ async def work(dialog: ProgressDialog = None): except asyncio.CancelledError: raise except Exception as e: - print('except Exception as e:', e) - showerror("Importing file error", "Cannot import file \"{}\"\n{}".format(filePath, e)) + showerror("Importing file error", 'Cannot import file "{}"\n{}: {}'.format(filePath, e.__class__.__name__, e)) print(traceback.format_exc()) continue dialog.totalProgress.set(dialog.currentMax) @@ -913,7 +923,6 @@ async def work(dialog: ProgressDialog = None): raise # self.d.toplevel.after(1, self.d.destroy) - print('Creating dialog') dialog = ProgressDialog(self.d.toplevel, self) # dialog.d.bind('<>', lambda _: self.d.destroy()) dialog.d.set_title("Importing {} MuseScore files".format(fileCount)) diff --git a/musescore2nbs.py b/musescore2nbs.py index 9b1991a..061a957 100644 --- a/musescore2nbs.py +++ b/musescore2nbs.py @@ -236,6 +236,9 @@ MAX_TEMPO = 30 +class FileError(Exception): + pass + @lru_cache def fraction2length(fraction: str) -> int: if isinstance(fraction, str): @@ -257,7 +260,7 @@ async def musescore2nbs(filepath: str, expandMultiplier=1, autoExpand=True, dial Return: A NbsSong contains meta-information and notes' data (position, pitch, velocity - and tuning). None if the conversation fails. + and tuning). """ if autoExpand: @@ -276,14 +279,16 @@ async def musescore2nbs(filepath: str, expandMultiplier=1, autoExpand=True, dial if filename: with zip.open(filename) as file: xml = etree.parse(file, parser=None) + else: + raise FileError("This file isn't a MuseScore file or it doesn't contain a MuseScore file.") elif filepath.endswith(".mscx"): xml = etree.parse(filepath, parser=None) + else: + raise FileError("This file isn't a MuseScore file") - if xml is None: - return None if version := xml.findtext('programVersion'): - if version.startswith('2.'): - raise NotImplementedError("MuseScore 2 files are not supported. Please use MuseScore 3 to re-save the files before importing.") + if not version.startswith('3'): + raise NotImplementedError("This file is created by a older version of MuseScore. Please use MuseScore 3 to re-save the files before importing.") nbs: NbsSong = NbsSong() diff --git a/requirements.txt b/requirements.txt index 99256ec..bb9309a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ -addict==2.3.0 -Pillow==7.0.0 -pygubu==0.11 -pygubu-designer==0.17 \ No newline at end of file +addict~=2.3.0 +Pillow~=7.0.0 +pygubu~=0.11 +mido>=1.2.9 +lxml~=4.6.3 +Pillow~=7.1.0 \ No newline at end of file From 94fb854a3350fd9847a0701df557d8b52433a102 Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Fri, 25 Jun 2021 16:01:58 +0700 Subject: [PATCH 21/22] refactor: Make changes so that PyInstaller can be used for distributing - All .ui files are moved to the /ui folder; - Add an "About" dialog; - Custom widgets are moved to a new package; - Remove the app version in source code files. main.py: - Add pygubu's explict import calls so that PyInstaller can find them; - Add the resource path to pygubu builders --- .gitignore | 1 - README.md | 7 ++ customwidgets.py | 18 ----- customwidgets/__init__.py | 40 +++++++++++ customwidgets/wrapmessage.py | 65 ++++++++++++++++++ main.py | 56 ++++++++++----- musescore2nbs.py | 20 +++++- nbs2midi.py | 19 +++++ nbsio.py | 1 - ncfio.py | 38 +++++----- ui/256x256.png | Bin 0 -> 4662 bytes ui/aboutdialog.ui | 57 +++++++++++++++ .../datapackexportdialog.ui | 0 midiexportdialog.ui => ui/midiexportdialog.ui | 0 .../musescoreimportdialog.ui | 0 progressdialog.ui => ui/progressdialog.ui | 0 toplevel.ui => ui/toplevel.ui | 3 + version.txt | 28 ++++++++ wrapmessage.py | 50 -------------- 19 files changed, 293 insertions(+), 110 deletions(-) delete mode 100644 customwidgets.py create mode 100644 customwidgets/__init__.py create mode 100644 customwidgets/wrapmessage.py create mode 100644 ui/256x256.png create mode 100644 ui/aboutdialog.ui rename datapackexportdialog.ui => ui/datapackexportdialog.ui (100%) rename midiexportdialog.ui => ui/midiexportdialog.ui (100%) rename musescoreimportdialog.ui => ui/musescoreimportdialog.ui (100%) rename progressdialog.ui => ui/progressdialog.ui (100%) rename toplevel.ui => ui/toplevel.ui (99%) create mode 100644 version.txt delete mode 100644 wrapmessage.py diff --git a/.gitignore b/.gitignore index bceed5d..1fbd570 100644 --- a/.gitignore +++ b/.gitignore @@ -343,7 +343,6 @@ Downloaded song/ *.spec *.bat *.pyo -version.* build/ dist/ diff --git a/README.md b/README.md index 155bed6..594bd94 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,10 @@ [![License](https://img.shields.io/github/license/IoeCmcomc/NBSTool "License")](https://opensource.org/licenses/MIT "License") A tool to work with .nbs (Note Block Studio) files. + +## Features +- Process multiple files simultaneously; +- Modify header information and change between versions; +- Arrange notes; +- Import .nbs files from MuseScore files; +- Export .nbs files to MIDI files. diff --git a/customwidgets.py b/customwidgets.py deleted file mode 100644 index 699fcc2..0000000 --- a/customwidgets.py +++ /dev/null @@ -1,18 +0,0 @@ -from pygubu import BuilderObject, register_widget -from wrapmessage import WrapMessage - -print(__name__) - -class WrapMessageBuilder(BuilderObject): - class_ = WrapMessage - - OPTIONS_STANDARD = ('anchor', 'background', 'borderwidth', 'cursor', 'font', - 'foreground', 'highlightbackground', 'highlightcolor', - 'highlightthickness', 'padx', 'pady', 'relief', 'takefocus', - 'text', 'textvariable') - OPTIONS_SPECIFIC = ('aspect', 'justify', 'width', 'padding') - properties = OPTIONS_STANDARD + OPTIONS_SPECIFIC - -register_widget('customwidgets.wrapmessage', WrapMessageBuilder, - 'WrapMessage', ('tk', 'Custom')) - diff --git a/customwidgets/__init__.py b/customwidgets/__init__.py new file mode 100644 index 0000000..79261e5 --- /dev/null +++ b/customwidgets/__init__.py @@ -0,0 +1,40 @@ +# This file is a part of: +# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +# ███▄▄▄▄ ▀█████████▄ ▄████████ ███ ▄██████▄ ▄██████▄ ▄█ +# ███▀▀▀██▄ ███ ███ ███ ███ ▀█████████▄ ███ ███ ███ ███ ███ +# ███ ███ ███ ███ ███ █▀ ▀███▀▀██ ███ ███ ███ ███ ███ +# ███ ███ ▄███▄▄▄██▀ ███ ███ ▀ ███ ███ ███ ███ ███ +# ███ ███ ▀▀███▀▀▀██▄ ▀███████████ ███ ███ ███ ███ ███ ███ +# ███ ███ ███ ██▄ ███ ███ ███ ███ ███ ███ ███ +# ███ ███ ███ ███ ▄█ ███ ███ ███ ███ ███ ███ ███▌ ▄ +# ▀█ █▀ ▄█████████▀ ▄████████▀ ▄████▀ ▀██████▀ ▀██████▀ █████▄▄██ +# __________________________________________________________________________________ +# NBSTool is a tool to work with .nbs (Note Block Studio) files. +# Author: IoeCmcomc (https://github.com/IoeCmcomc) +# Programming language: Python +# License: MIT license +# Source codes are hosted on: GitHub (https://github.com/IoeCmcomc/NBSTool) +# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ + + +print(f'{__name__}') + +if __name__ == '__init__': # Pygubu import call + from wrapmessage import WrapMessage +else: # Normal import call + from .wrapmessage import WrapMessage +from pygubu import BuilderObject, register_widget + +class WrapMessageBuilder(BuilderObject): + class_ = WrapMessage + + OPTIONS_STANDARD = ('anchor', 'background', 'borderwidth', 'cursor', 'font', + 'foreground', 'highlightbackground', 'highlightcolor', + 'highlightthickness', 'padx', 'pady', 'relief', 'takefocus', + 'text', 'textvariable') + OPTIONS_SPECIFIC = ('aspect', 'justify', 'width', 'padding') + properties = OPTIONS_STANDARD + OPTIONS_SPECIFIC + +register_widget('customwidgets.wrapmessage', WrapMessageBuilder, + 'WrapMessage', ('tk', 'Custom')) + diff --git a/customwidgets/wrapmessage.py b/customwidgets/wrapmessage.py new file mode 100644 index 0000000..3a545f0 --- /dev/null +++ b/customwidgets/wrapmessage.py @@ -0,0 +1,65 @@ +# This file is a part of: +# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +# ███▄▄▄▄ ▀█████████▄ ▄████████ ███ ▄██████▄ ▄██████▄ ▄█ +# ███▀▀▀██▄ ███ ███ ███ ███ ▀█████████▄ ███ ███ ███ ███ ███ +# ███ ███ ███ ███ ███ █▀ ▀███▀▀██ ███ ███ ███ ███ ███ +# ███ ███ ▄███▄▄▄██▀ ███ ███ ▀ ███ ███ ███ ███ ███ +# ███ ███ ▀▀███▀▀▀██▄ ▀███████████ ███ ███ ███ ███ ███ ███ +# ███ ███ ███ ██▄ ███ ███ ███ ███ ███ ███ ███ +# ███ ███ ███ ███ ▄█ ███ ███ ███ ███ ███ ███ ███▌ ▄ +# ▀█ █▀ ▄█████████▀ ▄████████▀ ▄████▀ ▀██████▀ ▀██████▀ █████▄▄██ +# __________________________________________________________________________________ +# NBSTool is a tool to work with .nbs (Note Block Studio) files. +# Author: IoeCmcomc (https://github.com/IoeCmcomc) +# Programming language: Python +# License: MIT license +# Source codes are hosted on: GitHub (https://github.com/IoeCmcomc/NBSTool) +# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ + + +from __future__ import print_function + +import tkinter as tk +import tkinter.ttk as ttk + +from tkinter.messagebox import showinfo + +from tkinter import Message + +class WrapMessage(Message): + padding = 10 + + def __init__(self, master=None, **kwargs): + super().__init__(master, **kwargs) + self.bind("", self._adjustWidth) + + def configure(self, cnf={}, **kw): + key = 'padding' + if key in cnf: + self.padding = int(cnf[key]) + del cnf[key] + if key in kw: + self.padding = int(kw[key]) + del kw[key] + + super().configure(cnf, **kw) + + config = configure + + def cget(self, key): + option = 'padding' + if key == option: + return self.padding + + return super().cget(key) + + def _adjustWidth(self, event): + event.widget.configure(width=event.width-self.padding) + + +if __name__ == '__main__': + root = tk.Tk() + msg = WrapMessage(root) + msg.configure(padding=40, text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus gravida libero ac commodo molestie. Donec iaculis velit sem, consequat bibendum libero cursus ut. Nulla ullamcorper placerat libero malesuada dignissim. Aliquam et hendrerit erat, non aliquet mi. Ut eu urna ligula. Donec mattis sollicitudin purus. Proin tellus libero, interdum porta mauris ac, interdum gravida sapien. Proin maximus purus ut dui ultrices, eget blandit est consectetur.") + msg.pack(fill='both', expand=True) + root.mainloop() \ No newline at end of file diff --git a/main.py b/main.py index 0858414..7b0097d 100644 --- a/main.py +++ b/main.py @@ -13,7 +13,6 @@ # Author: IoeCmcomc (https://github.com/IoeCmcomc) # Programming language: Python # License: MIT license -# Version: 0.7.0 # Source codes are hosted on: GitHub (https://github.com/IoeCmcomc/NBSTool) # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ @@ -40,6 +39,10 @@ import pygubu from pygubu import Builder from pygubu.widgets.dialog import Dialog +# Explict import for PyInstaller +from pygubu.builder import ttkstdwidgets, tkstdwidgets +from pygubu.builder.widgets import tkscrollbarhelper, dialog, pathchooserinput +import customwidgets from time import time from pprint import pprint @@ -94,11 +97,9 @@ class MainWindow(): def __init__(self): builder: Builder = pygubu.Builder() self.builder: Builder = builder - builder.add_from_file(resource_path('toplevel.ui')) - print("The following exception is not an error:") - print('='*20) + builder.add_resource_path(resource_path()) + builder.add_from_file('ui/toplevel.ui') self.toplevel: tk.Toplevel = builder.get_object('toplevel') - print('='*20) self.mainwin: tk.Frame = builder.get_object('mainFrame') style = ttk.Style(self.toplevel) style.layout('Barless.TNotebook.Tab', []) # turn off tabs @@ -109,16 +110,6 @@ def __init__(self): self.toplevel.title("NBS Tool") centerToplevel(self.toplevel) - self.style = ttk.Style() - # self.style.theme_use("default") - try: - self.style.theme_use("vista") - except Exception as e: - print(repr(e), e.__class__.__name__) - try: - self.style.theme_use("winnative") - except Exception: - pass self.initMenuBar() @@ -423,6 +414,10 @@ def callMidiExportDialog(self): dialog.run() del dialog + def callAboutDialog(self): + dialog = AboutDialog(self.toplevel, self) + del dialog + def windowBind(self): toplevel = self.toplevel mainwin = self.mainwin @@ -628,6 +623,7 @@ def setAttrFromStrVar(key: str, value: str): setAttrFromStrVar('loop_max', self.headerLoopCount.get()) setAttrFromStrVar('loop_start', self.headerLoopStart.get()) + class DatapackExportDialog: def __init__(self, master, parent): self.master = master @@ -635,7 +631,7 @@ def __init__(self, master, parent): self.builder = builder = pygubu.Builder() builder.add_resource_path(resource_path()) - builder.add_from_file(resource_path('datapackexportdialog.ui')) + builder.add_from_file(resource_path('ui/datapackexportdialog.ui')) self.d: Dialog = builder.get_object('dialog', master) @@ -670,6 +666,7 @@ def export(self, _=None): exportDatapack(self.parent.songsData[index], os.path.join( path, self.entry.get()), self.entry.get(), 'wnbs') + class MidiExportDialog: def __init__(self, master, parent): self.master = master @@ -677,7 +674,7 @@ def __init__(self, master, parent): self.builder = builder = pygubu.Builder() builder.add_resource_path(resource_path()) - builder.add_from_file(resource_path('midiexportdialog.ui')) + builder.add_from_file(resource_path('ui/midiexportdialog.ui')) self.d: Dialog = builder.get_object('dialog', master) builder.get_object('pathChooser').bind('<>', self.pathChanged) @@ -749,6 +746,7 @@ async def work(dialog: ProgressDialog = None): dialog.totalMax = len(indexes) dialog.run(work) + class ProgressDialog: def __init__(self, master, parent): self.master = master @@ -757,7 +755,7 @@ def __init__(self, master, parent): self.builder = builder = pygubu.Builder() builder.add_resource_path(resource_path()) - builder.add_from_file(resource_path('progressdialog.ui')) + builder.add_from_file(resource_path('ui/progressdialog.ui')) self.d: Dialog = builder.get_object('dialog1', master) self.d.toplevel.protocol('WM_DELETE_WINDOW', self.onCancel) @@ -817,6 +815,7 @@ def onCancel(self) -> None: pass self.d.destroy() + def parseFilePaths(string: str) -> list: strLen = len(string) ret = [] @@ -841,6 +840,7 @@ def parseFilePaths(string: str) -> list: filePath += char return tuple(ret) + class MuseScoreImportDialog: def __init__(self, master, parent): self.master = master @@ -848,7 +848,7 @@ def __init__(self, master, parent): self.builder = builder = pygubu.Builder() builder.add_resource_path(resource_path()) - builder.add_from_file(resource_path('musescoreimportdialog.ui')) + builder.add_from_file(resource_path('ui/musescoreimportdialog.ui')) self.d: Dialog = builder.get_object('dialog', master) @@ -929,6 +929,24 @@ async def work(dialog: ProgressDialog = None): dialog.totalMax = fileCount dialog.run(work) + +class AboutDialog: + def __init__(self, master, parent): + self.master = master + self.parent = parent + + self.builder = builder = pygubu.Builder() + builder.add_resource_path(resource_path()) + builder.add_from_file(resource_path('ui/aboutdialog.ui')) + + self.d: Dialog = builder.get_object('dialog', master) + builder.connect_callbacks(self) + self.d.run() + + def github(self): + webbrowser.open_new_tab("https://github.com/IoeCmcomc/NBSTool") + + class FlexCheckbutton(tk.Checkbutton): def __init__(self, *args, **kwargs): okwargs = dict(kwargs) diff --git a/musescore2nbs.py b/musescore2nbs.py index 061a957..d0a1e4b 100644 --- a/musescore2nbs.py +++ b/musescore2nbs.py @@ -1,10 +1,28 @@ +# This file is a part of: +# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +# ███▄▄▄▄ ▀█████████▄ ▄████████ ███ ▄██████▄ ▄██████▄ ▄█ +# ███▀▀▀██▄ ███ ███ ███ ███ ▀█████████▄ ███ ███ ███ ███ ███ +# ███ ███ ███ ███ ███ █▀ ▀███▀▀██ ███ ███ ███ ███ ███ +# ███ ███ ▄███▄▄▄██▀ ███ ███ ▀ ███ ███ ███ ███ ███ +# ███ ███ ▀▀███▀▀▀██▄ ▀███████████ ███ ███ ███ ███ ███ ███ +# ███ ███ ███ ██▄ ███ ███ ███ ███ ███ ███ ███ +# ███ ███ ███ ███ ▄█ ███ ███ ███ ███ ███ ███ ███▌ ▄ +# ▀█ █▀ ▄█████████▀ ▄████████▀ ▄████▀ ▀██████▀ ▀██████▀ █████▄▄██ +# __________________________________________________________________________________ +# NBSTool is a tool to work with .nbs (Note Block Studio) files. +# Author: IoeCmcomc (https://github.com/IoeCmcomc) +# Programming language: Python +# License: MIT license +# Source codes are hosted on: GitHub (https://github.com/IoeCmcomc/NBSTool) +# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ + + from addict import Dict from lxml import etree import zipfile from nbsio import NbsSong from functools import lru_cache from os.path import basename -from re import search from asyncio import sleep INST_INFO = ( diff --git a/nbs2midi.py b/nbs2midi.py index 2de4924..d380442 100644 --- a/nbs2midi.py +++ b/nbs2midi.py @@ -1,3 +1,22 @@ +# This file is a part of: +# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +# ███▄▄▄▄ ▀█████████▄ ▄████████ ███ ▄██████▄ ▄██████▄ ▄█ +# ███▀▀▀██▄ ███ ███ ███ ███ ▀█████████▄ ███ ███ ███ ███ ███ +# ███ ███ ███ ███ ███ █▀ ▀███▀▀██ ███ ███ ███ ███ ███ +# ███ ███ ▄███▄▄▄██▀ ███ ███ ▀ ███ ███ ███ ███ ███ +# ███ ███ ▀▀███▀▀▀██▄ ▀███████████ ███ ███ ███ ███ ███ ███ +# ███ ███ ███ ██▄ ███ ███ ███ ███ ███ ███ ███ +# ███ ███ ███ ███ ▄█ ███ ███ ███ ███ ███ ███ ███▌ ▄ +# ▀█ █▀ ▄█████████▀ ▄████████▀ ▄████▀ ▀██████▀ ▀██████▀ █████▄▄██ +# __________________________________________________________________________________ +# NBSTool is a tool to work with .nbs (Note Block Studio) files. +# Author: IoeCmcomc (https://github.com/IoeCmcomc) +# Programming language: Python +# License: MIT license +# Source codes are hosted on: GitHub (https://github.com/IoeCmcomc/NBSTool) +# ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ + + from asyncio import sleep from mido import Message, MetaMessage, MidiFile, MidiTrack, bpm2tempo diff --git a/nbsio.py b/nbsio.py index 4c86c8f..38d063e 100644 --- a/nbsio.py +++ b/nbsio.py @@ -13,7 +13,6 @@ # Author: IoeCmcomc (https://github.com/IoeCmcomc) # Programming language: Python # License: MIT license -# Version: 0,7.0 # Source codes are hosted on: GitHub (https://github.com/IoeCmcomc/NBSTool) #‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ diff --git a/ncfio.py b/ncfio.py index a3b01db..2a1cb56 100644 --- a/ncfio.py +++ b/ncfio.py @@ -1,42 +1,41 @@ # This file is a part of: #‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -# ███▄▄▄▄ ▀█████████▄ ▄████████ ███ ▄██████▄ ▄██████▄ ▄█ -# ███▀▀▀██▄ ███ ███ ███ ███ ▀█████████▄ ███ ███ ███ ███ ███ -# ███ ███ ███ ███ ███ █▀ ▀███▀▀██ ███ ███ ███ ███ ███ -# ███ ███ ▄███▄▄▄██▀ ███ ███ ▀ ███ ███ ███ ███ ███ -# ███ ███ ▀▀███▀▀▀██▄ ▀███████████ ███ ███ ███ ███ ███ ███ -# ███ ███ ███ ██▄ ███ ███ ███ ███ ███ ███ ███ -# ███ ███ ███ ███ ▄█ ███ ███ ███ ███ ███ ███ ███▌ ▄ -# ▀█ █▀ ▄█████████▀ ▄████████▀ ▄████▀ ▀██████▀ ▀██████▀ █████▄▄██ +# ███▄▄▄▄ ▀█████████▄ ▄████████ ███ ▄██████▄ ▄██████▄ ▄█ +# ███▀▀▀██▄ ███ ███ ███ ███ ▀█████████▄ ███ ███ ███ ███ ███ +# ███ ███ ███ ███ ███ █▀ ▀███▀▀██ ███ ███ ███ ███ ███ +# ███ ███ ▄███▄▄▄██▀ ███ ███ ▀ ███ ███ ███ ███ ███ +# ███ ███ ▀▀███▀▀▀██▄ ▀███████████ ███ ███ ███ ███ ███ ███ +# ███ ███ ███ ██▄ ███ ███ ███ ███ ███ ███ ███ +# ███ ███ ███ ███ ▄█ ███ ███ ███ ███ ███ ███ ███▌ ▄ +# ▀█ █▀ ▄█████████▀ ▄████████▀ ▄████▀ ▀██████▀ ▀██████▀ █████▄▄██ #__________________________________________________________________________________ # NBSTool is a tool to work with .nbs (Note Block Studio) files. # Author: IoeCmcomc (https://github.com/IoeCmcomc) # Programming language: Python # License: MIT license -# Version: 0.7.0 # Source codes are hosted on: GitHub (https://github.com/IoeCmcomc/NBSTool) #‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -import re, math +import re, math, operator def readncf(text): groups = re.findall(r"((?:1|2|4|8|16|32)\.?)(?:(#?[a-g])([1-3])|-)", text) - + notes = [] tick = 0 for tup in groups: dur = tup[0] if dur[-1] == '.': dur = 4 / int(dur[:-1]) * 1.5 else: dur = 4 / int(dur) - if bool(tup[1]): key = 27 + note.index(tup[1]) + 12*(int(tup[2])-1) + if bool(tup[1]): key = 27 + notes.index(tup[1]) + 12*(int(tup[2])-1) else: key = None if key is not None and dur >= 0.25: notes.append({'tick':tick, 'layer': 0, 'inst':0, 'key':key, 'isPerc': False, 'duration': dur*4}) tick += dur*4 headers = {} headers['file_version'] = 3 - headers['vani_inst'] = 16 + headers['vani_inst'] = 16 headers['length'] = 0 headers['height'] = 1 headers['name'] = '' @@ -51,7 +50,7 @@ def readncf(text): headers['left_clicks'] = 0 headers['right_clicks'] = 0 headers['block_added'] = 0 - headers['block_removed'] = 0 + headers['block_removed'] = 0 headers['import_name'] = '' headers['inst_count'] = 0 @@ -83,23 +82,22 @@ def writencf(data): while dur >= 0.125 and sub > 0: sub = 2**(int(math.log(dur*8, 2))-3) subi = int(4 / sub)# if idur > 4 else sub - #print('idur: {}; dur: {}; sub: {}; subi: {}, c: {}'.format(idur, dur, sub, subi, c)) if c == 0: if idur > 4: ele = '1' + ele else: - ele = str(subi) + ele + ele = str(subi) + ele else: ext.append('{}-'.format(subi)) dur -= sub c += 1 - + key = note['key'] - 27 - + ele += '{}{}'.format(tunes[key%12], key//12 + 1) + ' '.join(ext) - + out.append(ele) - + out = ' '.join(out) return out \ No newline at end of file diff --git a/ui/256x256.png b/ui/256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..6f62e8e0087d0547a15f27a161547f1c2971afcf GIT binary patch literal 4662 zcmcgwcT`hbmp@668U$3D5}E>H0RaUel+eTiSODn=L`6`Fv_L=%Sm;euN-!X5K$-&b zC=v`x2t{gmNDW2QL^=eJ(7wDkf4o^UZ@x8e{+PYiU1#5O?%Mb4z3u=(kcbG{ zSB@qbXlfT|<$FC4sZ>2x_g=CxgL&1>**^wqN~%TRK3c>eEdcNHg&ld>JGg7 z!SZ*)#Lp+QZPSCMq+Qh`^Q?u{&m7R;dg<=GaJlVhN@i&NI?uR->bTC*1|c*e=SvPY z{ChysXME<>h$Ue1-^99JexLl;{hZ5UPBuV_25SZnOmy-vUJ=%Bz89&FTeDp10zH~p zM-~Yg(kMDVruT}H-;ijv=Pv{Cv7J=z)Fx!;$ud<$!#KG%7xpQn-<~YfTBeo5XWgHx=#Yt1d`$36v)?#pSx(efHmV1<(Q=8%*VkLGIkLgW`K`^*|khkLPQNe#1`g zgQIZ0#jn?hm#y`R$;psC>-lUIoJ~y~a`l;{URZp%xB7huFYaNx;1iym-)F5TFF^gp ziD)FCXv~tI=d*oh3@%Mj^LC*@tJ6uzXOL7)Q0byy3PnmWqRg#U+g$WjM8#njh+chP z9*jik!^SjU7<1h;R&l6O>d`xe>GE^~t)IWOumcKnm*fWdT^}>`j2Cfrpu{f4;?`O} zOF}0Uj;gtEa=fG8`N9#HUp-TM|1WE<(%E84yj?9!n* zTJ{8A)J-|_a@!Tli;#hhq0=r|sqnq7kRY3L7HMAh-zkKhTUDVMGp5vl{_)!ZpE7Q* z2M8G+6C&TBg>`i0f)6SQ`cod8T0VmwSoj?g*q9Q3?8`h1SbPUf&mve|oik>2-$Ji0 z30D)TT^HfF^Cpk1N^Pi~_uLDaM-_gvcgjCOtD8wCMhnAShbKE83JCZ_h~%!4392_P zqFh>RCx1+5^{tmO)ilun{(7dQr1h>8JGD@syLo`Pn&HRlS9>bxKKf`Rb|l(ju1o~x zO72n{(FC?x^oiYJvUVAX`L-F-?)$jO#!J#&AC#CAG;N=ZRXI?^GMfZQ+|Qaw=UMcOD&C)S{uI zaFCN$IWn|OZJaNjfQ~(HGJi4|k)zE$26w7d3BGr~YbQmJSY_u`6!+U~3!_v}PZ)q4{?!XtsNa#xo3**99GSN%{?-27l|yl_uG8C|P%e6{uR za6pSeCSgG|Z0Cn)NqmGWw(0a`*J^#0$5SSjv&jU@#t^bneTs&$~W(O~q8s-Ev@ zIcLW;ZHKEqwbUw9whW@AcvRPcHKx%=Mk8e2+r}PB=+5s!3gj*M@A@f_?~XBUTUW)G z3!;JwXMg&c71JOQLSz24ux1@`!-e;LhgMDnsRzGcs^u3=>u)(!5WnLcE-t?VZKAosyESeE6d>>Y12o8j5<)wdIeDCl&bChqi_!D!w8*?YiVqX5^7YH=2yxi*gaf zG+utcqpI>Qw$m~Voj`)dki_=wvuqN3(AzS>pkiWg`Qvx{|7ND+KJ&Zpu+NbJLav9am61=IKRtqsV*xn275+_>;> zdEL7gzV`}pW`pi#KfJTHY5Lx!YyK!mrAPqzcr{=mh6XW5nt-r<0z|f40*1P(#*J%s zhyQOMSUB9kNO-01oUFu77yITQo>U7C|0JlkRx%nTfDRh=08)p1OJ%`z1j7aXsqMer zUBrIjwN|rxGB1{AYB*j!|A^`?6*Yl_wN{iH*k0=1&LX%8*6;Yg`0}^!`$y6Ef3$Gf z4U6hZd?UdVnZcebYibk`Hm z8&BPT2+hxEebJfOvvdHEV(CKV79$#_BU6w81_izA38F#l_A|#n=E8Q9WI`5F2ocIR z6uT=V<2+NXSvygL%VU>RWpbKxSlhC8kG-cOq@iEh%vQEM0JQyh&< zC3COaMioGQ)e9@PV7}kc13#V4IsOWLN^x@@_?{{cUncz;HemrsrFs55d;P2P7u0+* z2kVfc3!3#s4W~?=3AZ2UxqD3SyTm9L0J44+3#>T9Ao&aS{H|TB19=*cc_yp6T2Cty zeCAFEPFe`V%=5{X)=zi&+*a$xUfYMBdZBd&?#(Cn|yj*!M8q@>XQ`AW>I zoD(7ch(*}74Sv_^RiB7EfZW6Bd{20pR$vw?`_SX!e6d*2Z0CXBW;FF)+O=+plKlpl z&UafhU(Io6@8b#?;aKB_(4k!!-e*VY;<-u&!f!eG@g7{cu1Fy0u*%%eci33T-jR>$ z`y8D*RbjzgCQ=_8$-)2-zM)BQQDgHKhE8W#UbMu0TXXvM(p+~W=}!P$U|k%VqpNK1 zhGdsLyV@XdF@yHt>?{<}q7wpQ(Pv91M1tFu zYy9e`Q2Cmih5p<2R^agA%J~#qt-*s@{}YB=OP&Af3M_lN%D#RBDq5rmNr#20fuxTh z``^MvU9pHqf5AMpt zjKW{xV(7j75nayTe0VKxBiosV9X+Afbe%(&dW+*VtK~eS|AQ_Kh*Z5&rB`&bgx$f0 zsoWpc^8ttHWiqu*SM*I}+BJg>gd%TZ;Rp)&)z#Q8h;Ewc)Z=Q<1urb-w2kjUH};B} zi7jT7ZXW~sVmLIFQzxHJA02sbF7*73ZPML>Vym=bUw_jfP@+NlcEB6slsWQccsYwH z7Z}c=mcKUo7u3NIJV7Jsyy?6|GV`erckX8=La{+Uzm7~^$$V$oS1H+T$e)CCye3-x zBY6BeYdOfRDz_6i49C1M+EMbz^Q8mYQ6Z214HT8^6S(9WPWWV;Vmy& zTOpzvc0u>+PAP2RcHh>1g@_}E)VO6)4-rTe%1FY0s%&CwgI9TB!YEsXsVM~2+sd@= zZ-~1f0_nr!NAYx-(Bl=GS3x#V0N3yjYSO|<+=9~5s1%YIcB8ld)!fJi8x^bc$PkWG zkl$BIC>3c3lo=4#ew`yAEb?C^_57v*=HSKYrTAJg Q&OaV-#?;cJ)W|jJU-dX|1^@s6 literal 0 HcmV?d00001 diff --git a/ui/aboutdialog.ui b/ui/aboutdialog.ui new file mode 100644 index 0000000..478230f --- /dev/null +++ b/ui/aboutdialog.ui @@ -0,0 +1,57 @@ + + + + 100 + true + About this application... + 200 + + + 200 + 200 + + true + both + 10 + True + top + + + + left + {Arial} 57 {} + 256x256.png + NBSTool + + True + top + + + + + + center + A tool to work with Open Note Block Studio files. +Version: 0.7.0 +Author: IoeCmcomc + + 10 + True + top + + + + + + github + GitHub + + True + bottom + + + + + + + diff --git a/datapackexportdialog.ui b/ui/datapackexportdialog.ui similarity index 100% rename from datapackexportdialog.ui rename to ui/datapackexportdialog.ui diff --git a/midiexportdialog.ui b/ui/midiexportdialog.ui similarity index 100% rename from midiexportdialog.ui rename to ui/midiexportdialog.ui diff --git a/musescoreimportdialog.ui b/ui/musescoreimportdialog.ui similarity index 100% rename from musescoreimportdialog.ui rename to ui/musescoreimportdialog.ui diff --git a/progressdialog.ui b/ui/progressdialog.ui similarity index 100% rename from progressdialog.ui rename to ui/progressdialog.ui diff --git a/toplevel.ui b/ui/toplevel.ui similarity index 99% rename from toplevel.ui rename to ui/toplevel.ui index c163323..5d6f136 100644 --- a/toplevel.ui +++ b/ui/toplevel.ui @@ -1,6 +1,8 @@ + icon.ico + 256x256.png 350|370 both false @@ -886,6 +888,7 @@ 0 + callAboutDialog About 0 diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..434566a --- /dev/null +++ b/version.txt @@ -0,0 +1,28 @@ +VSVersionInfo( + ffi=FixedFileInfo( + filevers=(0, 7, 0, 0), + prodvers=(0, 7, 0, 0), + mask=0x3f, + flags=0x0, + OS=0x40004, + fileType=0x1, + subtype=0x0, + date=(0, 0) + ), + kids=[ + StringFileInfo( + [ + StringTable( + u'040904B0', + [StringStruct(u'CompanyName', u''), + StringStruct(u'FileDescription', u'A tool to work with .nbs (Note Block Studio) files.'), + StringStruct(u'FileVersion', u'0.7.0 (32 bit)'), + StringStruct(u'InternalName', u'nbstool'), + StringStruct(u'LegalCopyright', u'\xa9 IoeCmcomc.'), + StringStruct(u'OriginalFilename', u'NBSTool-32bit-v0.7.0_pyinstaller.exe'), + StringStruct(u'ProductName', u'NBSTool'), + StringStruct(u'ProductVersion', u'0.7.0')]) + ]), + VarFileInfo([VarStruct(u'Translation', [1033, 1200])]) + ] +) \ No newline at end of file diff --git a/wrapmessage.py b/wrapmessage.py deleted file mode 100644 index e7e83ec..0000000 --- a/wrapmessage.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import print_function - -import tkinter as tk -import tkinter.ttk as ttk - -from tkinter.messagebox import showinfo - -from tkinter import Message - -class WrapMessage(Message): - padding = 10 - - def __init__(self, master=None, **kwargs): - super().__init__(master, **kwargs) - self.bind("", self._adjustWidth) - - def configure(self, cnf={}, **kw): - # showinfo("Input config", '{} {}'.format(cnf, kw)) - - key = 'padding' - if key in cnf: - self.padding = int(cnf[key]) - del cnf[key] - if key in kw: - self.padding = int(kw[key]) - del kw[key] - - # showinfo("Output config", '{} {}'.format(cnf, kw)) - - super().configure(cnf, **kw) - - config = configure - - def cget(self, key): - option = 'padding' - if key == option: - return self.padding - - return super().cget(key) - - def _adjustWidth(self, event): - event.widget.configure(width=event.width-self.padding) - - -if __name__ == '__main__': - root = tk.Tk() - msg = WrapMessage(root) - msg.configure(padding=40, text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus gravida libero ac commodo molestie. Donec iaculis velit sem, consequat bibendum libero cursus ut. Nulla ullamcorper placerat libero malesuada dignissim. Aliquam et hendrerit erat, non aliquet mi. Ut eu urna ligula. Donec mattis sollicitudin purus. Proin tellus libero, interdum porta mauris ac, interdum gravida sapien. Proin maximus purus ut dui ultrices, eget blandit est consectetur.") - msg.pack(fill='both', expand=True) - root.mainloop() \ No newline at end of file From 358e7c407282e2c71d9b6608a4f19967eccd6f96 Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Fri, 25 Jun 2021 20:30:55 +0700 Subject: [PATCH 22/22] release: Release version 1.0 Signed-off-by: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> --- main.py | 14 ++++---------- ui/aboutdialog.ui | 2 +- version.txt | 10 +++++----- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/main.py b/main.py index 7b0097d..ee1fc27 100644 --- a/main.py +++ b/main.py @@ -79,17 +79,11 @@ globalIncVar = 0 -# Credit: https://stackoverflow.com/questions/42474560/pyinstaller-single-exe-file-ico-image-in-title-of-tkinter-main-window def resource_path(*args): if getattr(sys, 'frozen', False): - datadir = os.path.dirname(sys.executable) - r = os.path.join(datadir, *args) + r = os.path.join(sys._MEIPASS, *args) else: - try: - r = os.path.join(sys._MEIPASS, *args) - except Exception: - r = os.path.join(os.path.abspath("."), *args) - # print(r) + r = os.path.join(os.path.abspath('.'), *args) return r @@ -98,7 +92,7 @@ def __init__(self): builder: Builder = pygubu.Builder() self.builder: Builder = builder builder.add_resource_path(resource_path()) - builder.add_from_file('ui/toplevel.ui') + builder.add_from_file(resource_path('ui/toplevel.ui')) self.toplevel: tk.Toplevel = builder.get_object('toplevel') self.mainwin: tk.Frame = builder.get_object('mainFrame') style = ttk.Style(self.toplevel) @@ -153,7 +147,7 @@ def on_fileTable_select(event): self.mainwin.grab_set() self.mainwin.grab_release() - self.VERSION = '0.7.0' + self.VERSION = '1.0.0' self.filePaths = [] self.songsData = [] self.selectedFilesVersion = -1 diff --git a/ui/aboutdialog.ui b/ui/aboutdialog.ui index 478230f..14e9437 100644 --- a/ui/aboutdialog.ui +++ b/ui/aboutdialog.ui @@ -32,7 +32,7 @@ center A tool to work with Open Note Block Studio files. -Version: 0.7.0 +Version: 1.0.0 Author: IoeCmcomc 10 diff --git a/version.txt b/version.txt index 434566a..1a83910 100644 --- a/version.txt +++ b/version.txt @@ -1,7 +1,7 @@ VSVersionInfo( ffi=FixedFileInfo( - filevers=(0, 7, 0, 0), - prodvers=(0, 7, 0, 0), + filevers=(1, 0, 0, 0), + prodvers=(1, 0, 0, 0), mask=0x3f, flags=0x0, OS=0x40004, @@ -16,12 +16,12 @@ VSVersionInfo( u'040904B0', [StringStruct(u'CompanyName', u''), StringStruct(u'FileDescription', u'A tool to work with .nbs (Note Block Studio) files.'), - StringStruct(u'FileVersion', u'0.7.0 (32 bit)'), + StringStruct(u'FileVersion', u'1.0 (32 bit)'), StringStruct(u'InternalName', u'nbstool'), StringStruct(u'LegalCopyright', u'\xa9 IoeCmcomc.'), - StringStruct(u'OriginalFilename', u'NBSTool-32bit-v0.7.0_pyinstaller.exe'), + StringStruct(u'OriginalFilename', u'NBSTool-32bit-v1.0_pyinstaller.exe'), StringStruct(u'ProductName', u'NBSTool'), - StringStruct(u'ProductVersion', u'0.7.0')]) + StringStruct(u'ProductVersion', u'1.0')]) ]), VarFileInfo([VarStruct(u'Translation', [1033, 1200])]) ]