From 72dfdfeb3461f06ccc9cdc9cf3153dcf77e33926 Mon Sep 17 00:00:00 2001 From: EillesWan Date: Mon, 2 Oct 2023 18:24:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E8=BF=9BChannel=E7=9A=84=E5=BA=94?= =?UTF-8?q?=E7=94=A8=EF=BC=8C=E5=87=8F=E5=B0=91=E5=86=85=E5=AD=98=E5=8D=A0?= =?UTF-8?q?=E7=94=A8=EF=BC=8C=E4=BC=98=E5=8C=96=E7=BB=93=E6=9E=84=E7=94=9F?= =?UTF-8?q?=E6=88=90=EF=BC=8C=E6=8F=90=E9=AB=98MIDI=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E6=94=B9API=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Musicreater/__init__.py | 6 +- Musicreater/constants.py | 5 +- Musicreater/exceptions.py | 25 +- Musicreater/experiment.py | 3 +- Musicreater/main.py | 388 +++++++------------- Musicreater/plugin/__init__.py | 2 +- Musicreater/plugin/addonpack/main.py | 8 +- Musicreater/plugin/archive.py | 4 +- Musicreater/plugin/bdxfile/__init__.py | 8 +- Musicreater/plugin/main.py | 2 +- Musicreater/plugin/mcstructfile/__init__.py | 3 +- Musicreater/plugin/mcstructfile/main.py | 7 +- Musicreater/plugin/mcstructure.py | 25 +- Musicreater/plugin/noteblock.py | 9 +- Musicreater/previous.py | 103 +++++- Musicreater/subclass.py | 89 ++++- Musicreater/types.py | 64 ++++ Musicreater/utils.py | 54 ++- example.py | 10 +- 19 files changed, 485 insertions(+), 330 deletions(-) create mode 100644 Musicreater/types.py diff --git a/Musicreater/__init__.py b/Musicreater/__init__.py index 74a2a82..233f14d 100644 --- a/Musicreater/__init__.py +++ b/Musicreater/__init__.py @@ -17,8 +17,8 @@ Terms & Conditions: License.md in the root directory # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md -__version__ = "1.5.2" -__vername__ = "添加midi对应的键盘符号表" +__version__ = "1.6.0" +__vername__ = "切换ChannelType类型为NoteChannelType类型" __author__ = ( ("金羿", "Eilles Wan"), ("诸葛亮与八卦阵", "bgArray"), @@ -29,7 +29,7 @@ __all__ = [ # 主要类 "MidiConvert", # 附加类 - # "SingleNote", + "SingleNote", "SingleCommand", # "TimeStamp", 未来功能 ] diff --git a/Musicreater/constants.py b/Musicreater/constants.py index 8fb66a3..0567fd2 100644 --- a/Musicreater/constants.py +++ b/Musicreater/constants.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from typing import Dict, List, Tuple - """ 存放常量与数值性内容 """ @@ -18,6 +16,7 @@ Terms & Conditions: License.md in the root directory # Email TriM-Organization@hotmail.com # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md +from typing import Dict, List, Tuple x = "x" """ @@ -368,7 +367,7 @@ PERCUSSION_INSTRUMENT_LIST: List[str] = [ "fire.ignite", ] -INSTRUMENT_BLOCKS_TABLE: Dict[str, Tuple[str]] = { +INSTRUMENT_BLOCKS_TABLE: Dict[str, Tuple[str, ...]] = { "note.bass": ("planks",), "note.snare": ("sand",), "note.hat": ("glass",), diff --git a/Musicreater/exceptions.py b/Musicreater/exceptions.py index 53847a2..5016e40 100644 --- a/Musicreater/exceptions.py +++ b/Musicreater/exceptions.py @@ -24,7 +24,7 @@ class MSCTBaseException(Exception): """音·创库版本的所有错误均继承于此""" super().__init__(*args) - def miao( + def meow( self, ): for i in self.args: @@ -66,12 +66,15 @@ class CommandFormatError(RuntimeError): super().__init__("指令格式不匹配", *args) -class CrossNoteError(MidiFormatException): - """同通道下同音符交叉出现所产生的错误""" +# class CrossNoteError(MidiFormatException): +# """同通道下同音符交叉出现所产生的错误""" - def __init__(self, *args): - """同通道下同音符交叉出现所产生的错误""" - super().__init__("同通道下同音符交叉", *args) +# def __init__(self, *args): +# """同通道下同音符交叉出现所产生的错误""" +# super().__init__("同通道下同音符交叉", *args) +# 这TM是什么错误? +# 我什么时候写的这玩意? +# 我哪知道这说的是啥? class NotDefineTempoError(MidiFormatException): @@ -98,7 +101,15 @@ class NotDefineProgramError(MidiFormatException): super().__init__("未指定演奏乐器", *args) -class ZeroSpeedError(MidiFormatException): +class NoteOnOffMismatchError(MidiFormatException): + """音符开音和停止不匹配的错误""" + + def __init__(self, *args): + """音符开音和停止不匹配的错误""" + super().__init__("音符不匹配", *args) + + +class ZeroSpeedError(ZeroDivisionError): """以0作为播放速度的错误""" def __init__(self, *args): diff --git a/Musicreater/experiment.py b/Musicreater/experiment.py index e0eb4f9..786ea3a 100644 --- a/Musicreater/experiment.py +++ b/Musicreater/experiment.py @@ -17,14 +17,13 @@ Terms & Conditions: License.md in the root directory # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md import random -from typing import Dict, List, Tuple, Union from .constants import INSTRUMENT_BLOCKS_TABLE from .exceptions import * from .main import MidiConvert from .subclass import * from .utils import * - +from .types import Tuple, List, Dict class FutureMidiConvertRSNB(MidiConvert): """ diff --git a/Musicreater/main.py b/Musicreater/main.py index add33dc..c5f3f69 100644 --- a/Musicreater/main.py +++ b/Musicreater/main.py @@ -27,39 +27,13 @@ Terms & Conditions: License.md in the root directory import math import os -from typing import List, Literal, Tuple, Union - -import mido from .constants import * from .exceptions import * from .subclass import * +from .types import * from .utils import * -VoidMido = Union[mido.MidiFile, None] # void mido -""" -空Midi类类型 -""" - -ChannelType = Dict[ - int, - Dict[ - int, - List[ - Union[ - Tuple[Literal["PgmC"], int, int], - Tuple[Literal["NoteS"], int, int, int], - Tuple[Literal["NoteE"], int, int], - ] - ], - ], -] -""" -以字典所标记的频道信息类型 - -Dict[int,Dict[int,List[Union[Tuple[Literal["PgmC"], int, int],Tuple[Literal["NoteS"], int, int, int],Tuple[Literal["NoteE"], int, int],]],],] -""" - """ 学习笔记: tempo: microseconds per quarter note 毫秒每四分音符,换句话说就是一拍占多少毫秒 @@ -112,7 +86,7 @@ class MidiConvert: execute_cmd_head: str """execute指令头部""" - channels: ChannelType + channels: Union[ChannelType, NoteChannelType] """频道信息字典""" music_command_list: List[SingleCommand] @@ -195,57 +169,6 @@ class MidiConvert: # ……真的那么重要吗 # 我又几曾何时,知道祂真的会抛下我 - @staticmethod - def inst_to_souldID_withX( - instrumentID: int, - ) -> Tuple[str, int]: - """ - 返回midi的乐器ID对应的我的世界乐器名,对于音域转换算法,如下: - 2**( ( msg.note - 60 - X ) / 12 ) 即为MC的音高,其中 - X的取值随乐器不同而变化: - 竖琴harp、电钢琴pling、班卓琴banjo、方波bit、颤音琴iron_xylophone 的时候为6 - 吉他的时候为7 - 贝斯bass、迪吉里杜管didgeridoo的时候为8 - 长笛flute、牛铃cou_bell的时候为5 - 钟琴bell、管钟chime、木琴xylophone的时候为4 - 而存在一些打击乐器bd(basedrum)、hat、snare,没有音域,则没有X,那么我们返回7即可 - - Parameters - ---------- - instrumentID: int - midi的乐器ID - - Returns - ------- - tuple(str我的世界乐器名, int转换算法中的X) - """ - try: - return PITCHED_INSTRUMENT_TABLE[instrumentID] - except KeyError: - return "note.flute", 5 - - @staticmethod - def perc_inst_to_soundID_withX(instrumentID: int) -> Tuple[str, int]: - """ - 对于Midi第10通道所对应的打击乐器,返回我的世界乐器名 - - Parameters - ---------- - instrumentID: int - midi的乐器ID - - Returns - ------- - tuple(str我的世界乐器名, int转换算法中的X) - """ - try: - return PERCUSSION_INSTRUMENT_TABLE[instrumentID] - except KeyError: - return "note.bd", 7 - - # 明明已经走了 - # 凭什么还要在我心里留下缠绵缱绻 - def form_progress_bar( self, max_score: int, @@ -522,24 +445,26 @@ class MidiConvert: self.progress_bar_command = result return result - def to_music_channels( + def to_music_note_channels( self, - ) -> ChannelType: + ignore_mismatch_error: bool = True, + ) -> NoteChannelType: """ - 使用金羿的转换思路,将midi解析并转换为频道信息字典 + 将midi解析并转换为频道音符字典 Returns ------- - 以频道作为分割的Midi信息字典: - Dict[int,Dict[int,List[Union[Tuple[Literal["PgmC"], int, int],Tuple[Literal["NoteS"], int, int, int],Tuple[Literal["NoteE"], int, int],]],],] + 以频道作为分割的Midi音符列表字典: + Dict[int,List[SingleNote,]] """ + if self.midi is None: raise MidiUnboundError( "你是否正在使用的是一个由 copy_important 生成的MidiConvert对象?这是不可复用的。" ) # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 - midi_channels: ChannelType = empty_midi_channels() + midi_channels: NoteChannelType = empty_midi_channels(staff=[]) tempo = mido.midifiles.midifiles.DEFAULT_TEMPO # 我们来用通道统计音乐信息 @@ -549,6 +474,27 @@ class MidiConvert: if not track: continue + note_queue_A: Dict[ + int, + List[ + Tuple[ + int, + int, + ] + ], + ] = empty_midi_channels(staff=[]) + note_queue_B: Dict[ + int, + List[ + Tuple[ + int, + int, + ] + ], + ] = empty_midi_channels(staff=[]) + + channel_program: Dict[int, int] = empty_midi_channels(staff=-1) + for msg in track: if msg.time != 0: microseconds += msg.time * tempo / self.midi.ticks_per_beat / 1000 @@ -557,28 +503,58 @@ class MidiConvert: if msg.type == "set_tempo": tempo = msg.tempo else: - try: - if not track_no in midi_channels[msg.channel].keys(): - midi_channels[msg.channel][track_no] = [] - except AttributeError as E: - print(msg, E) - if msg.type == "program_change": - midi_channels[msg.channel][track_no].append( - ("PgmC", msg.program, microseconds) - ) + channel_program[msg.channel] = msg.program elif msg.type == "note_on" and msg.velocity != 0: - midi_channels[msg.channel][track_no].append( - ("NoteS", msg.note, msg.velocity, microseconds) + note_queue_A[msg.channel].append( + (msg.note, channel_program[msg.channel]) ) + note_queue_B[msg.channel].append((msg.velocity, microseconds)) - elif (msg.type == "note_on" and msg.velocity == 0) or ( - msg.type == "note_off" + elif (msg.type == "note_off") or ( + msg.type == "note_on" and msg.velocity == 0 ): - midi_channels[msg.channel][track_no].append( - ("NoteE", msg.note, microseconds) - ) + if (msg.note, channel_program[msg.channel]) in note_queue_A[ + msg.channel + ]: + _velocity, _ms = note_queue_B[msg.channel][ + note_queue_A[msg.channel].index( + (msg.note, channel_program[msg.channel]) + ) + ] + note_queue_A[msg.channel].remove( + (msg.note, channel_program[msg.channel]) + ) + note_queue_B[msg.channel].remove((_velocity, _ms)) + midi_channels[msg.channel].append( + SingleNote( + instrument=msg.note, + pitch=channel_program[msg.channel], + velocity=_velocity, + startime=_ms, + lastime=microseconds - _ms, + track_number=track_no, + is_percussion=True, + ) + if msg.channel == 9 + else SingleNote( + instrument=channel_program[msg.channel], + pitch=msg.note, + velocity=_velocity, + startime=_ms, + lastime=microseconds - _ms, + track_number=track_no, + is_percussion=False, + ) + ) + else: + if ignore_mismatch_error: + print("[WARRING] MIDI格式错误 音符不匹配 {} 无法在上文中找到与之匹配的音符开音消息".format(msg)) + else: + raise NoteOnOffMismatchError( + "当前的MIDI很可能有损坏之嫌……", msg, "无法在上文中找到与之匹配的音符开音消息。" + ) """整合后的音乐通道格式 每个通道包括若干消息元素其中逃不过这三种: @@ -592,9 +568,14 @@ class MidiConvert: 3 音符结束消息 ("NoteE", 结束的音符ID, 距离演奏开始的毫秒)""" del tempo, self.channels - self.channels = midi_channels - # [print([print(no,tno,sum([True if i[0] == 'NoteS' else False for i in track])) for tno,track in cna.items()]) if cna else False for no,cna in midi_channels.items()] - return midi_channels + self.channels = dict( + [ + (channel_no, sorted(channel_notes, key=lambda note: note.start_time)) + for channel_no, channel_notes in midi_channels.items() + ] + ) + + return self.channels def to_command_list_in_score( self, @@ -623,75 +604,46 @@ class MidiConvert: raise ZeroSpeedError("播放速度仅可为正实数") max_volume = 1 if max_volume > 1 else (0.001 if max_volume <= 0 else max_volume) - tracks = [] - cmdAmount = 0 - maxScore = 0 - InstID = -1 - - self.to_music_channels() + command_channels = [] + command_amount = 0 + max_score = 0 # 此处 我们把通道视为音轨 - for i in self.channels.keys(): + for channel in self.to_music_note_channels().values(): # 如果当前通道为空 则跳过 - if not self.channels[i]: + if not channel: continue - # 第十通道是打击乐通道 - SpecialBits = True if i == 9 else False + this_channel = [] - for track_no, track in self.channels[i].items(): - nowTrack = [] + for note in channel: + score_now = round(note.start_time / float(speed) / 50) + max_score = max(max_score, score_now) - for msg in track: - if msg[0] == "PgmC": - InstID = msg[1] - - elif msg[0] == "NoteS": - soundID, _X = ( - self.perc_inst_to_soundID_withX(msg[1]) - if SpecialBits - else self.inst_to_souldID_withX(InstID) - ) - score_now = round(msg[-1] / float(speed) / 50) - maxScore = max(maxScore, score_now) - mc_pitch = "" if SpecialBits else 2 ** ((msg[1] - 60 - _X) / 12) - mc_distance_volume = 128 / max_volume / msg[2] + ( - 1 if SpecialBits else -1 + this_channel.append( + SingleCommand( + self.execute_cmd_head.format( + "@a[scores=({}={})]".format(scoreboard_name, score_now) + .replace("(", r"{") + .replace(")", r"}") ) + + note.to_command(max_volume), + annotation="在{}播放{}%的{}音".format( + mctick2timestr(score_now), + max_volume * 100, + "{}:{}".format(note.mc_sound_ID, note.mc_pitch), + ), + ), + ) - nowTrack.append( - SingleCommand( - self.execute_cmd_head.format( - "@a[scores=({}={})]".format( - scoreboard_name, score_now - ) - .replace("(", r"{") - .replace(")", r"}") - ) - + "playsound {} @s ^ ^ ^{} {} {}".format( - soundID, - mc_distance_volume, - msg[2] / 128, - mc_pitch, - ), - annotation="在{}播放{}%的{}音".format( - mctick2timestr(score_now), - max_volume * 100, - "{}:{}".format(soundID, mc_pitch), - ), - ), - ) + command_amount += 1 - cmdAmount += 1 + if this_channel: + self.music_command_list.extend(this_channel) + command_channels.append(this_channel) - if nowTrack: - self.music_command_list.extend(nowTrack) - tracks.append(nowTrack) - - # print(cmdAmount) - del InstID - self.music_tick_num = maxScore - return (tracks, cmdAmount, maxScore) + self.music_tick_num = max_score + return (command_channels, command_amount, max_score) def to_command_list_in_delay( self, @@ -720,96 +672,40 @@ class MidiConvert: raise ZeroSpeedError("播放速度仅可为正实数") max_volume = 1 if max_volume > 1 else (0.001 if max_volume <= 0 else max_volume) - self.to_music_channels() - - tracks = {} - InstID = -1 - # cmd_amount = 0 + notes_list: List[SingleNote] = [] # 此处 我们把通道视为音轨 - for i in self.channels.keys(): - # 如果当前通道为空 则跳过 - if not self.channels[i]: - continue + for channel in self.to_music_note_channels().values(): + notes_list.extend(channel) - # 第十通道是打击乐通道 - SpecialBits = True if i == 9 else False + notes_list.sort(key=lambda a: a.start_time) + self.music_command_list = [] + multi = max_multi = 0 + delaytime_previous = 0 - # nowChannel = [] - - for track_no, track in self.channels[i].items(): - for msg in track: - if msg[0] == "PgmC": - InstID = msg[1] - - elif msg[0] == "NoteS": - soundID, _X = ( - self.perc_inst_to_soundID_withX(msg[1]) - if SpecialBits - else self.inst_to_souldID_withX(InstID) - ) - - delaytime_now = round(msg[-1] / float(speed) / 50) - mc_pitch = "" if SpecialBits else 2 ** ((msg[1] - 60 - _X) / 12) - mc_distance_volume = 128 / max_volume / msg[2] + ( - 1 if SpecialBits else -1 - ) - - try: - tracks[delaytime_now].append( - self.execute_cmd_head.format(player_selector) - + "playsound {} @s ^ ^ ^{} {} {}".format( - soundID, - mc_distance_volume, - msg[2] / 128, - mc_pitch, - ) - ) - except KeyError: - tracks[delaytime_now] = [ - self.execute_cmd_head.format(player_selector) - + "playsound {} @s ^ ^ ^{} {} {}".format( - soundID, - mc_distance_volume, - msg[2] / 128, - mc_pitch, - ) - ] - - # cmd_amount += 1 - - # print(cmd_amount) - - del InstID - all_ticks = list(tracks.keys()) - all_ticks.sort() - results = [] - max_multi = 0 - - for i in range(len(all_ticks)): - max_multi = max(max_multi, len(tracks[all_ticks[i]])) - for j in range(len(tracks[all_ticks[i]])): - results.append( - SingleCommand( - tracks[all_ticks[i]][j], - tick_delay=( - 0 - if j != 0 - else ( - all_ticks[i] - all_ticks[i - 1] - if i != 0 - else all_ticks[i] - ) - ), - annotation="在{}播放{}%的{}音".format( - mctick2timestr(i), max_volume * 100, "" - ), - ) + for note in notes_list: + delaytime_now = round(note.start_time / speed / 50) + if (tickdelay := (delaytime_now - delaytime_previous)) == 0: + multi += 1 + else: + max_multi = max(max_multi, multi) + multi = 0 + self.music_command_list.append( + SingleCommand( + self.execute_cmd_head.format(player_selector) + + note.to_command(max_volume), + tick_delay=tickdelay, + annotation="在{}播放{}%的{}音".format( + mctick2timestr(delaytime_now), + max_volume * 100, + "{}:{}".format(note.mc_sound_ID, note.mc_pitch), + ), ) + ) + delaytime_previous = delaytime_now - self.music_command_list = results - self.music_tick_num = max(all_ticks) - return results, self.music_tick_num, max_multi + self.music_tick_num = round(notes_list[-1].start_time / speed / 50) + return self.music_command_list, self.music_tick_num, max_multi + 1 def copy_important(self): dst = MidiConvert( diff --git a/Musicreater/plugin/__init__.py b/Musicreater/plugin/__init__.py index a6cfdd6..1de904b 100644 --- a/Musicreater/plugin/__init__.py +++ b/Musicreater/plugin/__init__.py @@ -19,4 +19,4 @@ __all__ = [ ] __author__ = (("金羿", "Eilles Wan"), ("诸葛亮与八卦阵", "bgArray")) -from .main import * \ No newline at end of file +from .main import * diff --git a/Musicreater/plugin/addonpack/main.py b/Musicreater/plugin/addonpack/main.py index a4a25e9..df4924d 100644 --- a/Musicreater/plugin/addonpack/main.py +++ b/Musicreater/plugin/addonpack/main.py @@ -22,11 +22,11 @@ from ...main import MidiConvert from ..archive import behavior_mcpack_manifest, compress_zipfile from ..main import ConvertConfig from ..mcstructure import ( - commands_to_structure, - form_command_block_in_NBT_struct, - commands_to_redstone_delay_structure, COMPABILITY_VERSION_117, COMPABILITY_VERSION_119, + commands_to_redstone_delay_structure, + commands_to_structure, + form_command_block_in_NBT_struct, ) @@ -393,7 +393,7 @@ def to_addon_pack_in_repeater( with open(f"{data_cfg.dist_path}/temp/manifest.json", "w", encoding="utf-8") as f: json.dump( behavior_mcpack_manifest( - pack_description=f"{midi_cvt.midi_music_name} 音乐播放包,MCSTRUCTURE(MCPACK) 延迟播放器 - 由 音·创 生成", + pack_description=f"{midi_cvt.midi_music_name} 音乐播放包,MCSTRUCTURE(MCPACK) 中继器播放器 - 由 音·创 生成", pack_name=midi_cvt.midi_music_name + "播放", modules_description=f"无 - 由 音·创 生成", ), diff --git a/Musicreater/plugin/archive.py b/Musicreater/plugin/archive.py index a44182c..50298c1 100644 --- a/Musicreater/plugin/archive.py +++ b/Musicreater/plugin/archive.py @@ -17,11 +17,11 @@ Terms & Conditions: License.md in the root directory # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md +import datetime import os import uuid import zipfile -import datetime -from typing import List, Union, Literal +from typing import List, Literal, Union def compress_zipfile(sourceDir, outFilename, compression=8, exceptFile=None): diff --git a/Musicreater/plugin/bdxfile/__init__.py b/Musicreater/plugin/bdxfile/__init__.py index e47c1ef..cbf6661 100644 --- a/Musicreater/plugin/bdxfile/__init__.py +++ b/Musicreater/plugin/bdxfile/__init__.py @@ -14,11 +14,7 @@ Terms & Conditions: License.md in the root directory # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md -__all__ = [ - "to_BDX_file_in_score", - "to_BDX_file_in_delay" -] +__all__ = ["to_BDX_file_in_score", "to_BDX_file_in_delay"] __author__ = (("金羿", "Eilles Wan"),) -from .main import to_BDX_file_in_delay,to_BDX_file_in_score - +from .main import to_BDX_file_in_delay, to_BDX_file_in_score diff --git a/Musicreater/plugin/main.py b/Musicreater/plugin/main.py index dcdbf39..3fa234c 100644 --- a/Musicreater/plugin/main.py +++ b/Musicreater/plugin/main.py @@ -17,7 +17,7 @@ Terms & Conditions: License.md in the root directory from dataclasses import dataclass -from typing import Tuple, Union, Literal +from typing import Literal, Tuple, Union from ..constants import DEFAULT_PROGRESSBAR_STYLE diff --git a/Musicreater/plugin/mcstructfile/__init__.py b/Musicreater/plugin/mcstructfile/__init__.py index e158ec1..25a8c2d 100644 --- a/Musicreater/plugin/mcstructfile/__init__.py +++ b/Musicreater/plugin/mcstructfile/__init__.py @@ -20,5 +20,4 @@ __all__ = [ ] __author__ = (("金羿", "Eilles Wan"),) -from .main import to_mcstructure_file_in_delay,to_mcstructure_file_in_repeater - +from .main import to_mcstructure_file_in_delay, to_mcstructure_file_in_repeater diff --git a/Musicreater/plugin/mcstructfile/main.py b/Musicreater/plugin/mcstructfile/main.py index c2cecad..74cedf1 100644 --- a/Musicreater/plugin/mcstructfile/main.py +++ b/Musicreater/plugin/mcstructfile/main.py @@ -12,17 +12,16 @@ Terms & Conditions: License.md in the root directory # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md import os - from typing import Literal from ...exceptions import CommandFormatError from ...main import MidiConvert from ..main import ConvertConfig from ..mcstructure import ( - commands_to_structure, - commands_to_redstone_delay_structure, - COMPABILITY_VERSION_119, COMPABILITY_VERSION_117, + COMPABILITY_VERSION_119, + commands_to_redstone_delay_structure, + commands_to_structure, ) diff --git a/Musicreater/plugin/mcstructure.py b/Musicreater/plugin/mcstructure.py index f8dc01e..be0a2ae 100644 --- a/Musicreater/plugin/mcstructure.py +++ b/Musicreater/plugin/mcstructure.py @@ -351,23 +351,26 @@ def commands_to_redstone_delay_structure( command_actually_length = sum([int(bool(cmd.delay)) for cmd in commands]) - # a = 1 - # for cmd in commands: - # # print("\r 正在进行处理:",end="") - # if cmd.delay > 2: - # a = 1 - # else: - # a += 1 + a = 1 + a_max = 0 + total_cmd = 0 + for cmd in commands: + # print("\r 正在进行处理:",end="") + if cmd.delay > 2: + a_max = max(a,a_max) + total_cmd += (a := 1) + else: + a += 1 struct = Structure( size=( - round(delay_length / 2 + command_actually_length) + round(delay_length / 2 + total_cmd) if extensioon_direction == x - else max_multicmd_length, + else a_max, 3, - round(delay_length / 2 + command_actually_length) + round(delay_length / 2 + total_cmd) if extensioon_direction == z - else max_multicmd_length, + else a_max, ), fill=Block("minecraft", "air", compability_version=compability_version_), compability_version=compability_version_, diff --git a/Musicreater/plugin/noteblock.py b/Musicreater/plugin/noteblock.py index d7211c2..77d1134 100644 --- a/Musicreater/plugin/noteblock.py +++ b/Musicreater/plugin/noteblock.py @@ -19,6 +19,7 @@ Terms & Conditions: License.md in the root directory from ..exceptions import NotDefineProgramError, ZeroSpeedError from ..main import MidiConvert from ..subclass import SingleCommand +from ..utils import inst_to_souldID_withX, perc_inst_to_soundID_withX # 你以为写完了吗?其实并没有 @@ -68,15 +69,15 @@ def to_note_list( elif msg[0] == "NoteS": try: soundID, _X = ( - midi_cvt.perc_inst_to_soundID_withX(InstID) + perc_inst_to_soundID_withX(InstID) if SpecialBits - else midi_cvt.inst_to_souldID_withX(InstID) + else inst_to_souldID_withX(InstID) ) except UnboundLocalError as E: soundID, _X = ( - midi_cvt.perc_inst_to_soundID_withX(-1) + perc_inst_to_soundID_withX(-1) if SpecialBits - else midi_cvt.inst_to_souldID_withX(-1) + else inst_to_souldID_withX(-1) ) score_now = round(msg[-1] / float(speed) / 50) # print(score_now) diff --git a/Musicreater/previous.py b/Musicreater/previous.py index 7f15185..f7cd7cf 100644 --- a/Musicreater/previous.py +++ b/Musicreater/previous.py @@ -18,6 +18,7 @@ Terms & Conditions: License.md in the root directory from .exceptions import * from .main import MidiConvert, mido from .subclass import * +from .types import ChannelType from .utils import * @@ -27,6 +28,82 @@ class ObsoleteMidiConvert(MidiConvert): 这些破烂老代码能跑得起来就是谢天谢地,你们还指望我怎么样?这玩意真的不会再维护了,我发誓! """ + def to_music_channels( + self, + ) -> ChannelType: + """ + 使用金羿的转换思路,将midi解析并转换为频道信息字典 + + Returns + ------- + 以频道作为分割的Midi信息字典: + Dict[int,Dict[int,List[Union[Tuple[Literal["PgmC"], int, int],Tuple[Literal["NoteS"], int, int, int],Tuple[Literal["NoteE"], int, int],]],],] + """ + if self.midi is None: + raise MidiUnboundError( + "你是否正在使用的是一个由 copy_important 生成的MidiConvert对象?这是不可复用的。" + ) + + # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 + midi_channels: ChannelType = empty_midi_channels() + tempo = mido.midifiles.midifiles.DEFAULT_TEMPO + + # 我们来用通道统计音乐信息 + # 但是是用分轨的思路的 + for track_no, track in enumerate(self.midi.tracks): + microseconds = 0 + if not track: + continue + + note_queue = empty_midi_channels(staff=[]) + + for msg in track: + if msg.time != 0: + microseconds += msg.time * tempo / self.midi.ticks_per_beat / 1000 + + if msg.is_meta: + if msg.type == "set_tempo": + tempo = msg.tempo + else: + try: + if not track_no in midi_channels[msg.channel].keys(): + midi_channels[msg.channel][track_no] = [] + except AttributeError as E: + print(msg, E) + + if msg.type == "program_change": + midi_channels[msg.channel][track_no].append( + ("PgmC", msg.program, microseconds) + ) + + elif msg.type == "note_on" and msg.velocity != 0: + midi_channels[msg.channel][track_no].append( + ("NoteS", msg.note, msg.velocity, microseconds) + ) + + elif (msg.type == "note_on" and msg.velocity == 0) or ( + msg.type == "note_off" + ): + midi_channels[msg.channel][track_no].append( + ("NoteE", msg.note, microseconds) + ) + + """整合后的音乐通道格式 + 每个通道包括若干消息元素其中逃不过这三种: + + 1 切换乐器消息 + ("PgmC", 切换后的乐器ID: int, 距离演奏开始的毫秒) + + 2 音符开始消息 + ("NoteS", 开始的音符ID, 力度(响度), 距离演奏开始的毫秒) + + 3 音符结束消息 + ("NoteE", 结束的音符ID, 距离演奏开始的毫秒)""" + del tempo, self.channels + self.channels = midi_channels + # [print([print(no,tno,sum([True if i[0] == 'NoteS' else False for i in track])) for tno,track in cna.items()]) if cna else False for no,cna in midi_channels.items()] + return midi_channels + def to_command_list_method1( self, scoreboard_name: str = "mscplay", @@ -76,9 +153,9 @@ class ObsoleteMidiConvert(MidiConvert): ) maxscore = max(maxscore, nowscore) if msg.channel == 9: - soundID, _X = self.perc_inst_to_soundID_withX(instrumentID) + soundID, _X = perc_inst_to_soundID_withX(instrumentID) else: - soundID, _X = self.inst_to_souldID_withX(instrumentID) + soundID, _X = inst_to_souldID_withX(instrumentID) singleTrack.append( "execute @a[scores={" @@ -135,7 +212,7 @@ class ObsoleteMidiConvert(MidiConvert): (ticks * tempo) / ((self.midi.ticks_per_beat * float(speed)) * 50000) # type: ignore ) maxscore = max(maxscore, nowscore) - soundID, _X = self.inst_to_souldID_withX(instrumentID) + soundID, _X = inst_to_souldID_withX(instrumentID) singleTrack.append( "execute @a[scores={" + str(scoreboardname) @@ -152,7 +229,7 @@ class ObsoleteMidiConvert(MidiConvert): # 原本这个算法的转换效果应该和上面的算法相似的 def _toCmdList_m2( - self: MidiConvert, + self, scoreboard_name: str = "mscplay", MaxVolume: float = 1.0, speed: float = 1.0, @@ -189,16 +266,16 @@ class ObsoleteMidiConvert(MidiConvert): nowTrack = [] - for track_no, track in self.channels[i].items(): + for track_no, track in self.channels[i].items(): # type: ignore for msg in track: if msg[0] == "PgmC": InstID = msg[1] elif msg[0] == "NoteS": soundID, _X = ( - self.perc_inst_to_soundID_withX(msg[1]) + perc_inst_to_soundID_withX(msg[1]) if SpecialBits - else self.inst_to_souldID_withX(InstID) + else inst_to_souldID_withX(InstID) ) score_now = round(msg[-1] / float(speed) / 50) maxScore = max(maxScore, score_now) @@ -221,7 +298,7 @@ class ObsoleteMidiConvert(MidiConvert): return tracks, cmdAmount, maxScore def _toCmdList_withDelay_m1( - self: MidiConvert, + self, MaxVolume: float = 1.0, speed: float = 1.0, player: str = "@a", @@ -262,7 +339,7 @@ class ObsoleteMidiConvert(MidiConvert): (ticks * tempo) / ((self.midi.ticks_per_beat * float(speed)) * 50000) ) - soundID, _X = self.inst_to_souldID_withX(instrumentID) + soundID, _X = inst_to_souldID_withX(instrumentID) try: tracks[now_tick].append( self.execute_cmd_head.format(player) @@ -297,7 +374,7 @@ class ObsoleteMidiConvert(MidiConvert): return [results, max(all_ticks)] def _toCmdList_withDelay_m2( - self: MidiConvert, + self, MaxVolume: float = 1.0, speed: float = 1.0, player: str = "@a", @@ -329,16 +406,16 @@ class ObsoleteMidiConvert(MidiConvert): else: SpecialBits = False - for track_no, track in self.channels[i].items(): + for track_no, track in self.channels[i].items(): # type: ignore for msg in track: if msg[0] == "PgmC": InstID = msg[1] elif msg[0] == "NoteS": soundID, _X = ( - self.perc_inst_to_soundID_withX(msg[1]) + perc_inst_to_soundID_withX(msg[1]) if SpecialBits - else self.inst_to_souldID_withX(InstID) + else inst_to_souldID_withX(InstID) ) score_now = round(msg[-1] / float(speed) / 50) diff --git a/Musicreater/subclass.py b/Musicreater/subclass.py index e20b9b0..545c917 100644 --- a/Musicreater/subclass.py +++ b/Musicreater/subclass.py @@ -21,6 +21,7 @@ from dataclasses import dataclass from typing import Optional from .constants import PERCUSSION_INSTRUMENT_LIST +from .utils import inst_to_souldID_withX, perc_inst_to_soundID_withX @dataclass(init=False) @@ -45,14 +46,18 @@ class SingleNote: track_no: int """音符所处的音轨""" + percussive: bool + """是否为打击乐器""" + def __init__( self, instrument: int, pitch: int, velocity: int, - startTime: int, - lastTime: int, + startime: int, + lastime: int, track_number: int = 0, + is_percussion: Optional[bool] = None, ): """用于存储单个音符的类 :param instrument 乐器编号 @@ -67,12 +72,21 @@ class SingleNote: """音符编号""" self.velocity: int = velocity """力度/响度""" - self.start_time: int = startTime + self.start_time: int = startime """开始之时 ms""" - self.duration: int = lastTime + self.duration: int = lastime """音符持续时间 ms""" self.track_no: int = track_number """音符所处的音轨""" + self.track_no: int = track_number + """音符所处的音轨""" + + self.percussive = ( + (is_percussion in PERCUSSION_INSTRUMENT_LIST) + if (is_percussion is None) + else is_percussion + ) + """是否为打击乐器""" @property def inst(self): @@ -89,28 +103,73 @@ class SingleNote: return self.note def __str__(self): - return ( - f"Note(inst = {self.inst}, pitch = {self.note}, velocity = {self.velocity}, " - f"startTime = {self.start_time}, lastTime = {self.duration}, )" + return "{}Note(Instrument = {}, {}Velocity = {}, StartTime = {}, Duration = {},)".format( + "Percussive" if self.percussive else "", + self.inst, + "" if self.percussive else "Pitch = {}, ".format(self.pitch), + self.start_time, + self.duration, ) def __tuple__(self): - return self.inst, self.note, self.velocity, self.start_time, self.duration + return ( + (self.percussive, self.inst, self.velocity, self.start_time, self.duration) + if self.percussive + else ( + self.percussive, + self.inst, + self.note, + self.velocity, + self.start_time, + self.duration, + ) + ) def __dict__(self): - return { - "inst": self.inst, - "pitch": self.note, - "velocity": self.velocity, - "startTime": self.start_time, - "lastTime": self.duration, - } + return ( + { + "Percussive": self.percussive, + "Instrument": self.inst, + "Velocity": self.velocity, + "StartTime": self.start_time, + "Duration": self.duration, + } + if self.percussive + else { + "Percussive": self.percussive, + "Instrument": self.inst, + "Pitch": self.note, + "Velocity": self.velocity, + "StartTime": self.start_time, + "Duration": self.duration, + } + ) def __eq__(self, other) -> bool: if not isinstance(other, self.__class__): return False return self.__str__() == other.__str__() + def to_command(self, volume_percentage) -> str: + self.mc_sound_ID, _X = ( + perc_inst_to_soundID_withX(self.inst) + if self.percussive + else inst_to_souldID_withX(self.inst) + ) + + # delaytime_now = round(self.start_time / float(speed) / 50) + self.mc_pitch = "" if self.percussive else 2 ** ((self.note - 60 - _X) / 12) + self.mc_distance_volume = 128 / volume_percentage / self.velocity + ( + 1 if self.percussive else self.velocity / 32 + ) + + return "playsound {} @s ^ ^ ^{} {} {}".format( + self.mc_sound_ID, + self.mc_distance_volume, + self.velocity / 128, + self.mc_pitch, + ) + @dataclass(init=False) class SingleCommand: diff --git a/Musicreater/types.py b/Musicreater/types.py new file mode 100644 index 0000000..30053b4 --- /dev/null +++ b/Musicreater/types.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +""" +存放数据类型的定义 +""" + +""" +版权所有 © 2023 音·创 开发者 +Copyright © 2023 all the developers of Musicreater + +开源相关声明请见 仓库根目录下的 License.md +Terms & Conditions: License.md in the root directory +""" + +# 睿穆组织 开发交流群 861684859 +# Email TriM-Organization@hotmail.com +# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md + +from typing import Any, Dict, List, Literal, Optional, Tuple, Union + +import mido + +from .subclass import SingleNote + +ProgressStyle = Tuple[str, Tuple[str, str]] +""" +进度条样式类型 +""" + +VoidMido = Union[mido.MidiFile, None] # void mido +""" +空Midi类类型 +""" + + +NoteChannelType = Dict[ + int, + List[SingleNote,], +] +""" +频道信息类型 + +Dict[int,Dict[int,List[SingleNote,],],] +""" + + +ChannelType = Dict[ + int, + Dict[ + int, + List[ + Union[ + Tuple[Literal["PgmC"], int, int], + Tuple[Literal["NoteS"], int, int, int], + Tuple[Literal["NoteE"], int, int], + ] + ], + ], +] +""" +以字典所标记的频道信息类型(即将弃用) + +Dict[int,Dict[int,List[Union[Tuple[Literal["PgmC"], int, int],Tuple[Literal["NoteS"], int, int, int],Tuple[Literal["NoteE"], int, int],]],],] +""" diff --git a/Musicreater/utils.py b/Musicreater/utils.py index 001ff5b..e6eabba 100644 --- a/Musicreater/utils.py +++ b/Musicreater/utils.py @@ -15,7 +15,8 @@ Terms & Conditions: License.md in the root directory # Email TriM-Organization@hotmail.com # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md -from typing import Any, Dict +from .constants import PERCUSSION_INSTRUMENT_TABLE, PITCHED_INSTRUMENT_TABLE +from typing import Any, Dict, Tuple def mctick2timestr(mc_tick: int) -> str: @@ -37,3 +38,54 @@ def empty_midi_channels(channel_count: int = 17, staff: Any = {}) -> Dict[int, A ) # 这告诉我们,你不能忽略任何一个复制的序列,因为它真的,我哭死,折磨我一整天,全在这个bug上了 for i in range(channel_count) ) + + +def inst_to_souldID_withX( + instrumentID: int, +) -> Tuple[str, int]: + """ + 返回midi的乐器ID对应的我的世界乐器名,对于音域转换算法,如下: + 2**( ( msg.note - 60 - X ) / 12 ) 即为MC的音高,其中 + X的取值随乐器不同而变化: + 竖琴harp、电钢琴pling、班卓琴banjo、方波bit、颤音琴iron_xylophone 的时候为6 + 吉他的时候为7 + 贝斯bass、迪吉里杜管didgeridoo的时候为8 + 长笛flute、牛铃cou_bell的时候为5 + 钟琴bell、管钟chime、木琴xylophone的时候为4 + 而存在一些打击乐器bd(basedrum)、hat、snare,没有音域,则没有X,那么我们返回7即可 + + Parameters + ---------- + instrumentID: int + midi的乐器ID + + Returns + ------- + tuple(str我的世界乐器名, int转换算法中的X) + """ + try: + return PITCHED_INSTRUMENT_TABLE[instrumentID] + except KeyError: + return "note.flute", 5 + + +def perc_inst_to_soundID_withX(instrumentID: int) -> Tuple[str, int]: + """ + 对于Midi第10通道所对应的打击乐器,返回我的世界乐器名 + + Parameters + ---------- + instrumentID: int + midi的乐器ID + + Returns + ------- + tuple(str我的世界乐器名, int转换算法中的X) + """ + try: + return PERCUSSION_INSTRUMENT_TABLE[instrumentID] + except KeyError: + return "note.bd", 7 + + # 明明已经走了 + # 凭什么还要在我心里留下缠绵缱绻 diff --git a/example.py b/example.py index 65924df..63ef167 100644 --- a/example.py +++ b/example.py @@ -20,12 +20,12 @@ import os import Musicreater from Musicreater.plugin import ConvertConfig -from Musicreater.plugin.bdxfile import to_BDX_file_in_delay, to_BDX_file_in_score from Musicreater.plugin.addonpack import ( to_addon_pack_in_delay, to_addon_pack_in_repeater, to_addon_pack_in_score, ) +from Musicreater.plugin.bdxfile import to_BDX_file_in_delay, to_BDX_file_in_score # 获取midi列表 midi_path = input(f"请输入MIDI路径:") @@ -41,13 +41,13 @@ playerFormat = int(input(f"请选择播放方式[红石(2) 或 计分板(1) 或 # 真假字符串判断 -def bool_str(sth: str) -> bool: +def bool_str(sth: str): try: return bool(float(sth)) - except ValueError: - if str(sth).lower() == "true": + except: + if str(sth).lower() in ("true", "真", "是", "y", "t"): return True - elif str(sth).lower() == "false": + elif str(sth).lower() in ("false", "假", "否", "f", "n"): return False else: raise ValueError("布尔字符串啊?")