From 13ba2a310d0d6d17cc08245afd84a73349d4b35b Mon Sep 17 00:00:00 2001 From: IoeCmcomc <53734763+IoeCmcomc@users.noreply.github.com> Date: Wed, 12 Jun 2024 00:01:53 +0700 Subject: [PATCH] impl: Add logging to the program --- .gitignore | 1 + audio_common.py | 12 ++ common.py | 16 +- customwidgets/builder.py | 8 +- main.py | 371 +++++++++++++++++++++++---------------- nbs2audio.py | 7 +- nbs2impulsetracker.py | 30 ++-- poetry.lock | 45 ++++- pyproject.toml | 1 + 9 files changed, 309 insertions(+), 182 deletions(-) create mode 100644 audio_common.py diff --git a/.gitignore b/.gitignore index fa4634c..b17e69c 100644 --- a/.gitignore +++ b/.gitignore @@ -343,6 +343,7 @@ Downloaded song/ *build/ dist/ +logs/ main.dist/ .mscbackup/ diff --git a/audio_common.py b/audio_common.py new file mode 100644 index 0000000..41736c0 --- /dev/null +++ b/audio_common.py @@ -0,0 +1,12 @@ +from functools import lru_cache + +from pydub import AudioSegment + + +@lru_cache(maxsize=32) +def load_sound(path: str) -> AudioSegment: + """A patched version of nbswave.audio.load_song() which caches loaded sounds""" + if not path: + return AudioSegment.empty() + else: + return AudioSegment.from_file(path) diff --git a/common.py b/common.py index 0d9a992..bf0aa84 100644 --- a/common.py +++ b/common.py @@ -20,10 +20,6 @@ import sys from collections import namedtuple from os.path import abspath, dirname, join, normpath -from functools import lru_cache -from typing import Optional - -from pydub import AudioSegment # from main import __file__ as __mainfile__ @@ -232,7 +228,7 @@ SOUND_FOLDER = "sounds" BASE_RESOURCE_PATH = '' -if getattr(sys, 'frozen', False): # PyInstaller +if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): # PyInstaller BASE_RESOURCE_PATH = sys._MEIPASS # type: ignore elif '__compiled__' in globals(): # Nuitka BASE_RESOURCE_PATH = dirname(__file__) @@ -241,12 +237,4 @@ assert BASE_RESOURCE_PATH != '' def resource_path(*args: str): - return normpath(join(BASE_RESOURCE_PATH, *args)) - -@lru_cache(maxsize=32) -def load_sound(path: str) -> AudioSegment: - """A patched version of nbswave.audio.load_song() which caches loaded sounds""" - if not path: - return AudioSegment.empty() - else: - return AudioSegment.from_file(path) + return normpath(join(BASE_RESOURCE_PATH, *args)) \ No newline at end of file diff --git a/customwidgets/builder.py b/customwidgets/builder.py index f063962..dbb8783 100644 --- a/customwidgets/builder.py +++ b/customwidgets/builder.py @@ -25,8 +25,12 @@ except ImportError as e: from pygubu import BuilderObject, register_widget # type: ignore -from wrapmessage import WrapMessage -from checkablelabelframe import CheckableLabelFrame +try: + from wrapmessage import WrapMessage + from checkablelabelframe import CheckableLabelFrame +except ModuleNotFoundError: + from .wrapmessage import WrapMessage + from .checkablelabelframe import CheckableLabelFrame class WrapMessageBuilder(BuilderObject): # type: ignore class_ = WrapMessage diff --git a/main.py b/main.py index e328ccb..8b8bdc6 100644 --- a/main.py +++ b/main.py @@ -39,9 +39,11 @@ import os import re import sys +import platform +import inspect +import warnings import tkinter as tk import tkinter.ttk as ttk -import traceback import webbrowser from ast import literal_eval from asyncio import CancelledError, sleep @@ -65,6 +67,7 @@ # logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) +from loguru import logger import pygubu import pygubu.widgets.combobox from jsonschema import validate @@ -74,10 +77,10 @@ from pygubu.widgets import dialog, pathchooserinput, tkscrollbarhelper from pygubu.widgets.dialog import Dialog -import customwidgets +import customwidgets.builder from common import BASE_RESOURCE_PATH, resource_path -if os.name == 'nt': # Windows +if os.name == 'nt': # Windows # Add the path of the ffmpeg before the first pydub import statement os.environ["PATH"] += resource_path('ffmpeg', 'bin') @@ -95,109 +98,109 @@ __version__ = '1.3.0' NBS_JSON_SCHEMA = { - "type":"object", - "properties":{ - "header":{ - "type":"object", - "properties":{ - "length":{ - "type":"integer", + "type": "object", + "properties": { + "header": { + "type": "object", + "properties": { + "length": { + "type": "integer", "minimun": 0, "maximum": 65535 }, - "file_version":{ - "type":"integer", + "file_version": { + "type": "integer", "minimun": 0, "maximum": 255 }, - "vani_inst":{ - "type":"integer", + "vani_inst": { + "type": "integer", "minimun": 0, "maximum": 65535 }, - "height":{ - "type":"integer", + "height": { + "type": "integer", "minimun": 0, "maximum": 65535 }, - "name":{ - "type":"string", + "name": { + "type": "string", "maxLength": 4294967295 }, - "author":{ - "type":"string", + "author": { + "type": "string", "maxLength": 4294967295 }, - "orig_author":{ - "type":"string", + "orig_author": { + "type": "string", "maxLength": 4294967295 }, - "description":{ - "type":"string", + "description": { + "type": "string", "maxLength": 4294967295 }, - "tempo":{ - "type":"number", + "tempo": { + "type": "number", "minimun": 0, "maximum": 655.35 }, - "auto_save":{ - "type":"boolean" + "auto_save": { + "type": "boolean" }, - "auto_save_time":{ - "type":"integer", + "auto_save_time": { + "type": "integer", "minimun": 0, "maximum": 255 }, - "time_sign":{ - "type":"integer", + "time_sign": { + "type": "integer", "minimun": 0, "maximum": 255 }, - "minutes_spent":{ - "type":"integer", + "minutes_spent": { + "type": "integer", "minimun": 0, "maximum": 4294967295 }, - "left_clicks":{ - "type":"integer", + "left_clicks": { + "type": "integer", "minimun": 0, "maximum": 4294967295 }, - "right_clicks":{ - "type":"integer", + "right_clicks": { + "type": "integer", "minimun": 0, "maximum": 4294967295 }, - "block_added":{ - "type":"integer", + "block_added": { + "type": "integer", "minimun": 0, "maximum": 4294967295 }, - "block_removed":{ - "type":"integer", + "block_removed": { + "type": "integer", "minimun": 0, "maximum": 4294967295 }, - "import_name":{ - "type":"string", + "import_name": { + "type": "string", "maxLength": 4294967295 }, - "loop":{ - "type":"boolean" + "loop": { + "type": "boolean" }, - "loop_max":{ - "type":"integer", + "loop_max": { + "type": "integer", "minimun": 0, "maximum": 255 }, - "loop_start":{ - "type":"integer", + "loop_start": { + "type": "integer", "minimun": 0, "maximum": 65535 } }, - "required":[ + "required": [ "author", "auto_save", "auto_save_time", @@ -218,49 +221,49 @@ "vani_inst" ] }, - "notes":{ - "type":"array", + "notes": { + "type": "array", # "uniqueItems": True, - "items":{ - "type":"object", - "properties":{ - "tick":{ - "type":"integer", + "items": { + "type": "object", + "properties": { + "tick": { + "type": "integer", "minimun": 0, "maximum": 65535 }, - "layer":{ - "type":"integer", + "layer": { + "type": "integer", "minimun": 0, "maximum": 65535 }, - "inst":{ - "type":"integer", + "inst": { + "type": "integer", "minimun": 0, "maximum": 255 }, - "key":{ - "type":"integer", + "key": { + "type": "integer", "minimun": 0, "maximum": 87 }, - "vel":{ - "type":"integer", + "vel": { + "type": "integer", "minimun": 0, "maximum": 100 }, - "pan":{ - "type":"integer", + "pan": { + "type": "integer", "minimun": -100, "maximum": 100 }, - "pitch":{ - "type":"integer", + "pitch": { + "type": "integer", "minimun": -32768, "maximum": 32767 } }, - "required":[ + "required": [ "inst", "key", "layer", @@ -268,61 +271,61 @@ ] } }, - "layers":{ - "type":"array", - "items":{ - "type":"object", - "properties":{ - "name":{ - "type":"string", + "layers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", "maxLength": 4294967295 }, - "lock":{ - "type":"boolean" + "lock": { + "type": "boolean" }, - "volume":{ - "type":"integer", + "volume": { + "type": "integer", "minimun": 0, "maximum": 100 }, - "pan":{ - "type":"integer", + "pan": { + "type": "integer", "minimun": -100, "maximum": 100 } }, - "required":[ + "required": [ "name" ] } }, - "custom_instruments":{ - "type":"array", - "items":{ - "type":"object", - "properties":{ - "name":{ - "type":"string", + "custom_instruments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", "maxLength": 4294967295 }, - "filePath":{ - "type":"string", + "filePath": { + "type": "string", "maxLength": 4294967295 }, - "pitch":{ - "type":"integer", + "pitch": { + "type": "integer", "minimun": 0, "maximum": 87 }, - "pressKeys":{ - "type":"boolean" + "pressKeys": { + "type": "boolean" }, - "sound_id":{ - "type":"string", + "sound_id": { + "type": "string", "maxLength": 4294967295 } }, - "required":[ + "required": [ "filePath", "name", "pitch", @@ -332,7 +335,7 @@ } } }, - "required":[ + "required": [ "custom_instruments", "header", "layers", @@ -343,6 +346,8 @@ NBSTOOL_FIRST_COMMIT_TIMESTAMP = 1565100420 CONSONANTS = "bcdfghjklmnpqrstvwxyzBCDFGHJLKMNPQRSTVWXYZ" VOWELS = "ueoai" + + def genRandomFilename(prefix: str = '') -> str: randChars = (item for sublist in ( (choice(CONSONANTS), choice(VOWELS)) for _ in range(4)) for item in sublist) @@ -362,7 +367,7 @@ def __init__(self): style.layout('Barless.TNotebook.Tab', []) # turn off tabs style.configure('Barless.TNotebook', borderwidth=0, highlightthickness=0) - if os.name =='posix': + if os.name == 'posix': ttk.Style().theme_use('clam') self.fileTable: ttk.Treeview = builder.get_object('fileTable') @@ -580,8 +585,9 @@ def addFiles(self, _=None, paths=()): "This file format is not supported. However, you can try importing from the 'Import' menu instead.") self.songsData.append(songData) except Exception as e: - showerror("Opening file error", f'Cannot open file "{filePath}"\n{e.__class__.__name__}: {e}') - print(traceback.format_exc()) + showerror("Opening file error", + f'Cannot open file "{filePath}"\n{e.__class__.__name__}: {e}') + logger.exception(e) continue self.addFileInfo(filePath, songData) if i % 3 == 0: @@ -620,7 +626,7 @@ def saveFiles(self, _=None): self.enableFileTable() self.builder.get_object('applyBtn')['state'] = 'normal' return - + path = askdirectory(title="Select folder to save") if path == '': return @@ -630,7 +636,8 @@ def saveFiles(self, _=None): i = fileTable.index(item) filePath = self.filePaths[i] if filePath == '': - fileName = self.songsData[i].header.import_name.rsplit('.', 1)[0] + fileName = self.songsData[i].header.import_name.rsplit('.', 1)[ + 0] if fileName == '': fileName = genRandomFilename('untitled_') filePath = fileName + '.nbs' @@ -642,9 +649,9 @@ def saveFiles(self, _=None): path, os.path.basename(filePath))) except Exception as e: showerror("Saving file error", - f'Cannot save file "{os_path.join(path, os_path.basename(filePath))}"\n{e.__class__.__name__}: {e}' - ) - print(traceback.format_exc()) + f'Cannot save file "{os_path.join(path, os_path.basename(filePath))}"\n{e.__class__.__name__}: {e}' + ) + logger.exception(e) self.enableFileTable() self.builder.get_object('applyBtn')['state'] = 'normal' @@ -670,9 +677,9 @@ def saveAll(self, _=None): self.songsData[i].write(os.path.join( path, os.path.basename(filePath))) except Exception as e: - showerror("Saving file error", - f'Cannot save file "{os_path.join(path, os_path.basename(filePath))}"\n{e.__class__.__name__}: {e}') - print(traceback.format_exc()) + showerror("Saving file error", + f'Cannot save file "{os_path.join(path, os_path.basename(filePath))}"\n{e.__class__.__name__}: {e}') + logger.exception(e) self.enableFileTable() self.builder.get_object('applyBtn')['state'] = 'normal' @@ -710,7 +717,7 @@ def callMidiImportDialog(self): dialogue = MidiImportDialog(self.toplevel, self) dialogue.run() del dialogue - + def callMuseScoreImportDialog(self): dialogue = MuseScoreImportDialog(self.toplevel, self) dialogue.run() @@ -738,7 +745,7 @@ def callAudioExportDialog(self): def callJsonExportDialog(self): JsonExportDialog(self.toplevel, self).run() - + def callImpulseExportDialog(self): ImpulseExportDialog(self.toplevel, self).run() @@ -915,7 +922,8 @@ async def work(dialog: ProgressDialog): default_layer = Layer("") if (songData.maxLayer >= songData.header.height): - songData.layers.extend(repeat(default_layer, songData.maxLayer+1 - songData.header.height)) + songData.layers.extend( + repeat(default_layer, songData.maxLayer+1 - songData.header.height)) for note in songData.notes: layer = songData.layers[note.layer] @@ -1032,7 +1040,7 @@ def export(self, _=None): path = askdirectory(title="Select folder to save") if path == '': return - + lyrics: Optional[str] = None try: @@ -1123,6 +1131,7 @@ def onCancel(self) -> None: ExportDialogFunc = Callable[[NbsSong, str, ProgressDialog], Coroutine] + class ExportDialog: def __init__(self, master, parent, fileExt: str, title: Optional[str], progressTitle: str, func: ExportDialogFunc, ui_file='ui/exportdialog.ui'): @@ -1157,7 +1166,8 @@ def run(self): def exportModeChanged(self): self.isFolderMode = self.exportMode.get() == 'folder' - self.builder.get_object('pathChooser').configure(state='normal' if self.isFolderMode else 'disabled') + self.builder.get_object('pathChooser').configure( + state='normal' if self.isFolderMode else 'disabled') self.pathChanged() def pathChanged(self, _=None): @@ -1187,12 +1197,14 @@ async def work(dialog: ProgressDialog): origPath = filePaths[i] if not origPath: origPath = asksaveasfilename( - filetypes=((self.fileExt+' files', self.fileExt),), - initialfile=path.basename(songsData[i].header.import_name).rsplit('.', 1)[0], - defaultextension=self.fileExt) + filetypes=((self.fileExt+' files', self.fileExt),), + initialfile=path.basename( + songsData[i].header.import_name).rsplit('.', 1)[0], + defaultextension=self.fileExt) if not origPath: if self.d.toplevel: - self.d.toplevel.after(1, self.d.destroy) # type: ignore + self.d.toplevel.after( + 1, self.d.destroy) # type: ignore return baseName = path.basename(origPath) if baseName.endswith('.nbs'): @@ -1218,10 +1230,10 @@ async def work(dialog: ProgressDialog): raise except Exception as e: showerror("Exporting files error", - f'Cannot export file "{filePath}"\n{e.__class__.__name__}: {e}') - print(traceback.format_exc()) + f'Cannot export file "{filePath}"\n{e.__class__.__name__}: {e}') + logger.exception(e) dialog.totalProgress.set(dialog.currentMax) - + self.d.toplevel.after(1, self.d.destroy) # type: ignore dialogue = ProgressDialog(self.d.toplevel, self) @@ -1285,6 +1297,7 @@ def checkFFmpeg(ps: str = '') -> bool: else: return True + class AudioExportDialog(ExportDialog): def __init__(self, master, parent): self.formatVar: tk.StringVar @@ -1329,9 +1342,11 @@ def __init__(self, master, parent): super().__init__(master, parent, '.it', "Impulse Tracker exporting", "Exporting {} files to Impulse Tracker format (.it)...", nbs2it) self.shouldCompactNotes = False - + if not checkFFmpeg(): - self.d.destroy() + self.d.close() + self.d.toplevel.after(1, self.d.destroy) # type: ignore + def parseFilePaths(string: str) -> tuple: strLen = len(string) @@ -1358,7 +1373,9 @@ def parseFilePaths(string: str) -> tuple: return tuple(ret) -ImportDialogFunc = Callable[[str, ProgressDialog], Coroutine[Any, Any, NbsSong]] +ImportDialogFunc = Callable[[str, ProgressDialog], + Coroutine[Any, Any, NbsSong]] + class ImportDialog: def __init__(self, master, parent, fileExts: tuple, title: Optional[str], progressTitle: str, @@ -1370,7 +1387,6 @@ def __init__(self, master, parent, fileExts: tuple, title: Optional[str], progre self.progressTitle = progressTitle self.func = func - self.filePaths: StringVar self.builder = builder = pygubu.Builder() @@ -1440,8 +1456,8 @@ async def work(dialog: ProgressDialog): raise except Exception as e: showerror("Importing file error", - f'Cannot import file "{filePath}"\n{e.__class__.__name__}: {e}') - print(traceback.format_exc()) + f'Cannot import file "{filePath}"\n{e.__class__.__name__}: {e}') + logger.exception(e) continue dialog.totalProgress.set(dialog.currentMax) # self.d.toplevel.after(1, self.d.destroy) @@ -1457,8 +1473,8 @@ class JsonImportDialog(ImportDialog): def __init__(self, master, parent): fileExts = (("JSON files", '*.json'), ('All files', '*'),) super().__init__(master, parent, fileExts, "Import from JSON files", - "Importing {} JSON files", self.convert) - + "Importing {} JSON files", self.convert) + async def convert(self, filepath: str, dialog: ProgressDialog) -> NbsSong: j = {} with open(filepath, 'r', encoding='ascii') as f: @@ -1489,9 +1505,10 @@ def __init__(self, master, parent): self.autoExpand: BooleanVar self.expandMult: IntVar - fileExts = (("MuseScore files", ('*.mscz', '*.mscx')), ('All files', '*'),) + fileExts = (("MuseScore files", ('*.mscz', '*.mscx')), + ('All files', '*'),) super().__init__(master, parent, fileExts, None, - "Importing {} MIDI files", self.convert, "ui/musescoreimportdialog.ui") + "Importing {} MIDI files", self.convert, "ui/musescoreimportdialog.ui") self.autoExpand.set(True) @@ -1513,11 +1530,11 @@ def __init__(self, master, parent): self.importVelocity: BooleanVar self.importPitch: BooleanVar self.importPanning: BooleanVar - + fileExts = (("Musical Instrument Digital Interface (MIDI) files", ('*.mid', '*.midi')), ('All files', '*'),) super().__init__(master, parent, fileExts, None, - "Importing {} MIDI files", self.convert, "ui/midiimportdialog.ui") + "Importing {} MIDI files", self.convert, "ui/midiimportdialog.ui") self.autoExpand.set(True) self.importPitch.set(True) @@ -1525,11 +1542,12 @@ def __init__(self, master, parent): self.importVelocity.set(True) async def convert(self, filepath: str, dialog: ProgressDialog) -> NbsSong: - expandMult = int(self.expandMult.get()) if not self.autoExpand.get() else 0 + expandMult = int(self.expandMult.get() + ) if not self.autoExpand.get() else 0 return await midi2nbs(filepath, expandMult, self.importDuration.get(), - self.durationSpacing.get(), self.importVelocity.get(), - self.importPanning.get(), self.importPitch.get(), - dialog) + self.durationSpacing.get(), self.importVelocity.get(), + self.importPanning.get(), self.importPitch.get(), + dialog) def autoExpandChanged(self): self.builder.get_object('expandScale')[ @@ -1667,6 +1685,7 @@ def to_signed_32(n: int) -> int: n &= 0xffffffff return (n ^ 0x80000000) - 0x80000000 + def exportDatapack(data: NbsSong, path: str, _bname: str, mode=None, lyrics=None): def writejson(path, jsout): with open(path, 'w') as f: @@ -1704,8 +1723,10 @@ def _makeFolderTree(inp, a: list): lyrics_inst = 0 if lyrics: - lyrics_layer = next((i for i, x in enumerate(data.layers) if 'lyric' in x.name.lower()), -1) - lyrics_inst = next((x.inst for i, x in enumerate(data.notes) if x.layer == lyrics_layer), -1) + lyrics_layer = next((i for i, x in enumerate( + data.layers) if 'lyric' in x.name.lower()), -1) + lyrics_inst = next((x.inst for i, x in enumerate( + data.notes) if x.layer == lyrics_layer), -1) compactNotes(data, groupPerc=False) data.correctData() @@ -1750,9 +1771,11 @@ def _makeFolderTree(inp, a: list): lyrics_uuid = uuid.uuid4() uuid_int = lyrics_uuid.int # Source: https://stackoverflow.com/a/32053256/12682038 - uuid_array = ((uuid_int >> x) & 0xFFFFFFFF for x in reversed(range(0, 128, 32))) + uuid_array = ((uuid_int >> x) & + 0xFFFFFFFF for x in reversed(range(0, 128, 32))) uuid_arr_str = ', '.join(str(to_signed_32(num)) for num in uuid_array) - lyrics_layer = next((i for i, x in enumerate(layers) if 'lyric' in x.name.lower()), -1) + lyrics_layer = next((i for i, x in enumerate( + layers) if 'lyric' in x.name.lower()), -1) writejson(os.path.join(path, 'pack.mcmeta'), {"pack": { "description": "Note block song datapack made with NBSTool.", "pack_format": 8}}) @@ -1784,12 +1807,12 @@ def _makeFolderTree(inp, a: list): """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(instruments[k].sound_id, i), scoreObj, "{}-{}".format(k, i) + "{}-{}".format(instruments[k].sound_id, + i), scoreObj, "{}-{}".format(k, i) ) if lyrics: assert not uuid_arr_str is None @@ -1833,7 +1856,6 @@ def _makeFolderTree(inp, a: list): raw_text = json.dumps(component, ensure_ascii=False) text += f"data modify entity {lyrics_uuid} CustomName set value '{raw_text}'\n" - if tick < length-1: text += "scoreboard players set @s {}_t {}".format( scoreObj, tick) @@ -1897,10 +1919,59 @@ def _makeFolderTree(inp, a: list): break -if __name__ == "__main__": +class InterceptHandler(logging.Handler): + def emit(self, record: logging.LogRecord) -> None: + # Get corresponding Loguru level if it exists. + level: Union[str, int] + try: + level = logger.level(record.levelname).name + except ValueError: + level = record.levelno + + # Find caller from where originated the logged message. + frame, depth = inspect.currentframe(), 0 + while frame and (depth == 0 or frame.f_code.co_filename == logging.__file__): + frame = frame.f_back + depth += 1 + + logger.opt(depth=depth, exception=record.exc_info).log( + level, record.getMessage()) + + +logger.add(resource_path("logs", "latest.log"), retention=10, compression='bz2') +logging.basicConfig(handlers=[InterceptHandler()], level=logging.INFO, force=True) + +showwarning_ = warnings.showwarning + +def _showwarning(message, *args, **kwargs): + logger.warning(message) + showwarning_(message, *args, **kwargs) + +warnings.showwarning = _showwarning + +@logger.catch +def main() -> None: + logger.info("NBSTool v{}", __version__) + logger.info("Platform: {}", platform.platform()) + if '__compiled__' in globals(): + logger.info("Running in Nuitka-compiled mode") + logger.info("Resource path: {}", resource_path()) + if _ffmpeg_path := which('ffmpeg'): + logger.info("ffmpeg path: {}", _ffmpeg_path) + else: + logger.warning("ffmpeg not found; audio and .it export will not work") + if _ffprobe_path := which('ffprobe'): + logger.info("ffprobe path: {}", _ffprobe_path) + else: + logger.warning("ffprobe not found; audio and .it export will not work") + app = MainWindow() if len(sys.argv) == 2: app.addFiles(paths=[sys.argv[1], ]) - app.mainwin.mainloop() \ No newline at end of file + app.mainwin.mainloop() + + +if __name__ == "__main__": + main() diff --git a/nbs2audio.py b/nbs2audio.py index 21a844b..82f2991 100644 --- a/nbs2audio.py +++ b/nbs2audio.py @@ -25,14 +25,15 @@ from pydub import AudioSegment from pynbs import File, Header, Instrument, Layer, Note -from common import load_sound - from nbswave import SongRenderer, audio, nbs from nbswave.main import MissingInstrumentException -from common import resource_path, SOUND_FOLDER from nbsio import VANILLA_INSTS, NbsSong +from audio_common import load_sound +from common import resource_path, SOUND_FOLDER + + audio.load_sound = load_sound def convert(data: NbsSong) -> File: diff --git a/nbs2impulsetracker.py b/nbs2impulsetracker.py index 45c038a..542b591 100644 --- a/nbs2impulsetracker.py +++ b/nbs2impulsetracker.py @@ -28,6 +28,7 @@ from functools import total_ordering from os import path from asyncio import sleep +from warnings import warn from pydub import AudioSegment from pydub.effects import normalize @@ -35,13 +36,17 @@ from nbsio import Note as NbsNote from nbsio import NbsSong, Layer, Instrument, VANILLA_INSTS -from common import load_sound, SOUND_FOLDER +from common import SOUND_FOLDER +from audio_common import load_sound + DEFAULT_PATTERN_LENGTH = 64 TRACKER_VERSION = 0xa00a SAMPLE_PITCH_SHIFT = -10 LAST_SAMPLE_PITCH_SHIFT = 6 # The last need special treatment DEFAULT_SPEED = 6 +MAX_LENGTH = 40000 +MAX_CHANNEL = 127 BYTE = Struct(" None: song.correctData() header = song.header - layers = song.layers if not fn.endswith('.it'): fn += '.it' + song.notes = list( + filter(lambda note: note.tick < MAX_LENGTH and note.layer < MAX_CHANNEL, song.notes)) + song.correctData() + layers = song.layers + all_instruments = VANILLA_INSTS[:header.vani_inst] + song.customInsts delete_missing_instruments(song, all_instruments) @@ -269,20 +278,13 @@ async def nbs2it(song: NbsSong, fn: str, dialog=None) -> None: instrument_mapping = {index: i for i, index in enumerate(instrument_indexes)} - instrument_count = len(instruments) + pattern_length = DEFAULT_PATTERN_LENGTH pattern_count = ceil(header.length / pattern_length) while pattern_count > 200 and pattern_length <= 200: pattern_length += 16 pattern_count = ceil(header.length / pattern_length) - if pattern_length > 200: - pattern_length = 200 - max_length = 40000 - song.notes = list( - filter(lambda note: note.tick < max_length, song.notes)) - song.correctData() - pattern_count = ceil(header.length / pattern_length) order_count = pattern_count + 1 time_sign = 4 @@ -456,7 +458,11 @@ async def nbs2it(song: NbsSong, fn: str, dialog=None) -> None: dialog.currentProgress.set(60) await sleep(0.001) - channels = tuple(map(Channel.from_instance, layers)) + channels = list(map(Channel.from_instance, layers)) + dummy_channel = Channel("") + if len(channels) < song.maxLayer + 1: + for _ in range(song.maxLayer + 1 - len(channels)): + channels.append(dummy_channel) def note_converter(note): return Note.from_instance( note, pattern_length) diff --git a/poetry.lock b/poetry.lock index ce91e8e..0357525 100644 --- a/poetry.lock +++ b/poetry.lock @@ -109,6 +109,17 @@ files = [ [package.dependencies] pycparser = "*" +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + [[package]] name = "cython" version = "3.0.10" @@ -249,6 +260,24 @@ interegular = ["interegular (>=0.3.1,<0.4.0)"] nearley = ["js2py"] regex = ["regex"] +[[package]] +name = "loguru" +version = "0.7.2" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = ">=3.5" +files = [ + {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, + {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] + [[package]] name = "lxml" version = "5.2.2" @@ -1009,6 +1038,20 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + [[package]] name = "zipp" version = "3.19.2" @@ -1086,4 +1129,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.0" python-versions = ">=3.8, <3.11" -content-hash = "43c04319a4e11b5d971f1048dc14f537a594c109cfd255f8e96dc8398725d563" +content-hash = "e811f2e9d1bc5861975a2599315b9c67f7bac85773fe986709c79a2275b2d3ac" diff --git a/pyproject.toml b/pyproject.toml index a29d760..8b4cc00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ lxml = ">=4.9.1" lark = ">=1.0.0" jsonschema = ">=4.17.3" nbswave = {git = "https://github.com/Bentroen/nbswave.git"} +loguru = "^0.7.2" [tool.poetry.dev-dependencies]