diff --git a/.gitignore b/.gitignore index 6992ac9..f9d03e5 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ RES.txt /Packer/checksum.txt /bgArrayLib /fcwslib +test_lyric-mido.py # Byte-compiled / optimized __pycache__/ diff --git a/Musicreater/__init__.py b/Musicreater/__init__.py index 3330222..4f07f8e 100644 --- a/Musicreater/__init__.py +++ b/Musicreater/__init__.py @@ -22,8 +22,8 @@ The Licensor of Musicreater("this project") is Eilles, bgArray. # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md -__version__ = "2.4.0.1" -__vername__ = "全景声支持、音量调节修复" +__version__ = "2.4.1" +__vername__ = "Midi 歌词支持,文档格式更新" __author__ = ( ("金羿", "Eilles"), ("诸葛亮与八卦阵", "bgArray"), @@ -41,13 +41,26 @@ __all__ = [ "SingleNoteBox", "ProgressBarStyle", # "TimeStamp", 未来功能 - # 默认值 + # 字典键 "MIDI_PROGRAM", "MIDI_VOLUME", "MIDI_PAN", + # 默认值 "MIDI_DEFAULT_PROGRAM_VALUE", "MIDI_DEFAULT_VOLUME_VALUE", "DEFAULT_PROGRESSBAR_STYLE", + # Midi 自己的对照表 + "MIDI_PITCH_NAME_TABLE", + "MIDI_PITCHED_NOTE_NAME_GROUP", + "MIDI_PITCHED_NOTE_NAME_TABLE", + "MIDI_PERCUSSION_NOTE_NAME_TABLE", + # Minecraft 自己的对照表 + "MC_PERCUSSION_INSTRUMENT_LIST", + "MC_PITCHED_INSTRUMENT_LIST", + "MC_INSTRUMENT_BLOCKS_TABLE", + "MC_EILLES_RTJE12_INSTRUMENT_REPLACE_TABLE", + "MC_EILLES_RTBETA_INSTRUMENT_REPLACE_TABLE", + # Midi 与 游戏 的对照表 "MM_INSTRUMENT_RANGE_TABLE", "MM_CLASSIC_PITCHED_INSTRUMENT_TABLE", "MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE", @@ -62,10 +75,68 @@ __all__ = [ "velocity_2_distance_straight", "panning_2_rotation_linear", "panning_2_rotation_trigonometric", + # 工具函数 "load_decode_musicsequence_metainfo", "load_decode_msq_flush_release", "load_decode_fsq_flush_release", "guess_deviation", + "mctick2timestr", + "midi_inst_to_mc_sound", ] -from .main import * +from .main import MusicSequence, MidiConvert + +from .subclass import ( + MineNote, + MineCommand, + SingleNoteBox, + ProgressBarStyle, + mctick2timestr, + DEFAULT_PROGRESSBAR_STYLE, +) + +from .utils import ( + # 兼容性函数 + load_decode_musicsequence_metainfo, + load_decode_msq_flush_release, + load_decode_fsq_flush_release, + # 工具函数 + guess_deviation, + midi_inst_to_mc_sound, + # 处理用函数 + velocity_2_distance_natural, + velocity_2_distance_straight, + panning_2_rotation_linear, + panning_2_rotation_trigonometric, +) + +from .constants import ( + # 字典键 + MIDI_PROGRAM, + MIDI_PAN, + MIDI_VOLUME, + # 默认值 + MIDI_DEFAULT_PROGRAM_VALUE, + MIDI_DEFAULT_VOLUME_VALUE, + # MIDI 表 + MIDI_PITCH_NAME_TABLE, + MIDI_PITCHED_NOTE_NAME_GROUP, + MIDI_PITCHED_NOTE_NAME_TABLE, + MIDI_PERCUSSION_NOTE_NAME_TABLE, + # 我的世界 表 + MC_EILLES_RTBETA_INSTRUMENT_REPLACE_TABLE, + MC_EILLES_RTJE12_INSTRUMENT_REPLACE_TABLE, + MC_INSTRUMENT_BLOCKS_TABLE, + MC_PERCUSSION_INSTRUMENT_LIST, + MC_PITCHED_INSTRUMENT_LIST, + # MIDI 到 我的世界 表 + MM_INSTRUMENT_RANGE_TABLE, + MM_CLASSIC_PITCHED_INSTRUMENT_TABLE, + MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE, + MM_TOUCH_PITCHED_INSTRUMENT_TABLE, + MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE, + MM_DISLINK_PITCHED_INSTRUMENT_TABLE, + MM_DISLINK_PERCUSSION_INSTRUMENT_TABLE, + MM_NBS_PITCHED_INSTRUMENT_TABLE, + MM_NBS_PERCUSSION_INSTRUMENT_TABLE, +) diff --git a/Musicreater/exceptions.py b/Musicreater/exceptions.py index 6614af6..0285a79 100644 --- a/Musicreater/exceptions.py +++ b/Musicreater/exceptions.py @@ -113,13 +113,20 @@ class NoteOnOffMismatchError(MidiFormatException): """音符开音和停止不匹配的错误""" super().__init__("音符不匹配", *args) +class LyricMismatchError(MSCTBaseException): + """歌词匹配解析错误""" + + def __init__(self, *args): + """有可能产生了错误的歌词解析""" + super().__init__("歌词解析错误", *args) + class ZeroSpeedError(MSCTBaseException, ZeroDivisionError): """以0作为播放速度的错误""" def __init__(self, *args): """以0作为播放速度的错误""" - super().__init__("播放速度为0", *args) + super().__init__("播放速度为零", *args) class IllegalMinimumVolumeError(MSCTBaseException, ValueError): diff --git a/Musicreater/experiment.py b/Musicreater/experiment.py index 71940d2..33c024e 100644 --- a/Musicreater/experiment.py +++ b/Musicreater/experiment.py @@ -34,6 +34,99 @@ from .types import ChannelType, FittingFunctionType from .utils import * +class FutureMidiConvertLyricSupport(MidiConvert): + """ + 歌词测试支持 + """ + + def to_command_list_in_delay( + self, + player_selector: str = "@a", + using_lyric: bool = True, + ) -> Tuple[List[MineCommand], int, int]: + """ + 将midi转换为我的世界命令列表,并输出每个音符之后的延迟 + + Parameters + ---------- + player_selector: str + 玩家选择器,默认为`@a` + + Returns + ------- + tuple( list[MineCommand指令,...], int音乐时长游戏刻, int最大同时播放的指令数量 ) + """ + + notes_list: List[MineNote] = sorted( + [i for j in self.channels.values() for i in j], + key=lambda note: note.start_tick, + ) + + # 此处 我们把通道视为音轨 + self.music_command_list = [] + multi = max_multi = 0 + delaytime_previous = 0 + + for note in notes_list: + if (tickdelay := (note.start_tick - delaytime_previous)) == 0: + multi += 1 + else: + max_multi = max(max_multi, multi) + multi = 0 + + ( + mc_sound_ID, + relative_coordinates, + volume_percentage, + mc_pitch, + ) = minenote_to_command_paramaters( + note, + pitch_deviation=self.music_deviation, + ) + self.music_command_list.append( + MineCommand( + command=( + self.execute_cmd_head.format(player_selector) + + r"playsound {} @s ^{} ^{} ^{} {} {} {}".format( + mc_sound_ID, + *relative_coordinates, + volume_percentage, + 1.0 if note.percussive else mc_pitch, + self.minimum_volume, + ) + ), + annotation=( + "在{}播放噪音{}".format( + mctick2timestr(note.start_tick), + mc_sound_ID, + ) + if note.percussive + else "在{}播放乐音{}".format( + mctick2timestr(note.start_tick), + "{}:{:.2f}".format(mc_sound_ID, mc_pitch), + ) + ), + tick_delay=tickdelay, + ), + ) + if using_lyric and note.extra_info["LYRIC_TEXT"]: + self.music_command_list.append( + MineCommand( + self.execute_cmd_head.format(player_selector) + + 'title @s title " "' + ) + ) + self.music_command_list.append( + MineCommand( + self.execute_cmd_head.format(player_selector) + + "title @s subtitle {}".format(note.extra_info["LYRIC_TEXT"]) + ) + ) + delaytime_previous = note.start_tick + + return self.music_command_list, notes_list[-1].start_tick, max_multi + 1 + + class FutureMidiConvertKamiRES(MidiConvert): """ 神羽资源包之测试支持 @@ -758,10 +851,21 @@ class FutureMidiConvertM4(MidiConvert): _note: MineNote, _apply_time_division: float = 10, ) -> List[MineNote]: - """传入音符数据,返回分割后的插值列表 - :param _note: MineNote 音符 - :param _apply_time_division: int 间隔帧数 - :return list[tuple(int开始时间(毫秒), int乐器, int音符, int力度(内置), float音量(播放)),]""" + """ + 传入音符数据,返回分割后的插值列表 + + Parameters + ------------ + _note: MineNote + 音符 + _apply_time_division: int + 间隔帧数 + + Returns + --------- + list[tuple[int, int, int, int, float]] + 分割后的插值列表,每个元素为 (开始时间(毫秒), 乐器, 音符, 力度(内置), 音量(播放)) + """ if _note.percussive: return [ diff --git a/Musicreater/main.py b/Musicreater/main.py index 5831585..e59b0a6 100644 --- a/Musicreater/main.py +++ b/Musicreater/main.py @@ -130,9 +130,7 @@ class MusicSequence: if minimum_volume_of_music > 1 or minimum_volume_of_music <= 0: raise IllegalMinimumVolumeError( - "自订的最小音量参数错误:{},应在 (0,1] 范围内。".format( - minimum_volume_of_music - ) + "最小音量不得为 {},应在 (0,1] 范围内。".format(minimum_volume_of_music) ) # max_volume = 1 if max_volume > 1 else (0.001 if max_volume <= 0 else max_volume) @@ -594,7 +592,7 @@ class MusicSequence: else: raise MusicSequenceTypeError( - "输入的二进制字节码不是合法的音符序列格式,无法解码,码头前 10 字节为:", + "输入的二进制字节码不是正确的音符序列格式,无法解码,码前十字节为:", bytes_buffer_in[:10], ) @@ -851,12 +849,12 @@ class MusicSequence: Returns ------- - 以频道作为分割的Midi音符列表字典, 音符总数, 乐器使用统计: Tuple[MineNoteChannelType, int, Dict[str, int]] + 以通道作为分割的Midi音符列表字典, 音符总数, 乐器使用统计 """ if speed == 0: - raise ZeroSpeedError("播放速度为 0 ,其需要(0,1]范围内的实数。") + raise ZeroSpeedError("播放速度不得为零,应为 (0,1] 范围内的实数。") # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 midi_channels: MineNoteChannelType = empty_midi_channels(default_staff=[]) @@ -893,8 +891,15 @@ class MusicSequence: ], ] = empty_midi_channels(default_staff=[]) + lyric_cache: List[Tuple[int, str]] = [] + # 直接使用mido.midifiles.tracks.merge_tracks转为单轨 - # 采用的时遍历信息思路 + # 采用的是遍历信息思路 + + # 来自 202508 的留言 + # 该处代码有点问题 + # merged track 丢失了 track 信息,会导致音符不匹配的问题出现 + # 应该用遍历 Track 的方式来处理 for msg in midi.merged_track: if msg.time != 0: # 微秒 @@ -904,36 +909,66 @@ class MusicSequence: if msg.type == "set_tempo": tempo = msg.tempo elif msg.type == "program_change": + # 检测 乐器变化 之 midi 事件 channel_controler[msg.channel][MIDI_PROGRAM] = msg.program elif msg.is_cc(7): + # Control Change 更改当前通道的 音量 的事件(大幅度) channel_controler[msg.channel][MIDI_VOLUME] = msg.value elif msg.is_cc(10): + # Control Change 更改当前通道的 音调偏移 的事件(大幅度) channel_controler[msg.channel][MIDI_PAN] = msg.value + elif msg.type == "lyrics": + # 歌词事件 + lyric_cache.append((microseconds, msg.text)) + # print(lyric_cache, flush=True) + elif msg.type == "note_on" and msg.velocity != 0: + # 一个音符开始弹奏 + + # 加入音符队列甲(按通道分隔) + # (音高,乐器) note_queue_A[msg.channel].append( (msg.note, channel_controler[msg.channel][MIDI_PROGRAM]) ) + # 音符队列乙(按通道分隔) + # (力度,微秒) note_queue_B[msg.channel].append((msg.velocity, microseconds)) elif (msg.type == "note_off") or ( msg.type == "note_on" and msg.velocity == 0 ): + # 一个音符结束弹奏 + if ( msg.note, channel_controler[msg.channel][MIDI_PROGRAM], ) in note_queue_A[msg.channel]: + # 在甲队列中发现了同一个 音高和乐器 的音符 + + # 获取其音符力度和微秒数 _velocity, _ms = note_queue_B[msg.channel][ note_queue_A[msg.channel].index( (msg.note, channel_controler[msg.channel][MIDI_PROGRAM]) ) ] + + # 在队列中删除此音符 note_queue_A[msg.channel].remove( (msg.note, channel_controler[msg.channel][MIDI_PROGRAM]) ) note_queue_B[msg.channel].remove((_velocity, _ms)) + _lyric = "" + # 找一找歌词吧 + if lyric_cache: + for i in range(len(lyric_cache)): + if lyric_cache[i][0] >= _ms: + _lyric = lyric_cache.pop(i)[1] + break + + # 更新结果信息 midi_channels[msg.channel].append( that_note := midi_msgs_to_minenote( inst_=( @@ -961,14 +996,19 @@ class MusicSequence: volume_processing_method_=vol_processing_function, panning_processing_method_=pan_processing_function, note_table_replacement=note_rtable_replacement, + lyric_line=_lyric, ) ) + + # 更新统计信息 note_count += 1 if that_note.sound_name in note_count_per_instrument.keys(): note_count_per_instrument[that_note.sound_name] += 1 else: note_count_per_instrument[that_note.sound_name] = 1 + else: + # 什么?找不到 note on 消息?? if ignore_mismatch_error: print( "[WARRING] MIDI格式错误 音符不匹配 {} 无法在上文中找到与之匹配的音符开音消息".format( @@ -993,7 +1033,24 @@ class MusicSequence: 3 音符结束消息 ("NoteE", 结束的音符ID, 距离演奏开始的毫秒)""" + del tempo + + if lyric_cache: + # 怎么有歌词多啊 + if ignore_mismatch_error: + print( + "[WARRING] MIDI 解析错误 歌词对应错误,以下歌词未能填入音符之中,已经填入的仍可能有误 {}".format( + lyric_cache + ) + ) + else: + raise LyricMismatchError( + "MIDI 解析产生错误", + "歌词解析过程中无法对应音符,已填入的音符仍可能有误", + lyric_cache, + ) + channels = dict( [ (channel_no, sorted(channel_notes, key=lambda note: note.start_tick)) @@ -1126,6 +1183,7 @@ class MidiConvert(MusicSequence): pan_processing_func: FittingFunctionType = panning_2_rotation_linear, music_pitch_deviation: float = 0, note_table_replacement: Dict[str, str] = {}, + midi_charset: str = "utf-8", ): """ 直接输入文件地址,将 midi 文件读入 @@ -1171,6 +1229,7 @@ class MidiConvert(MusicSequence): return cls.from_mido_obj( midi_obj=mido.MidiFile( midi_file_path, + charset=midi_charset, clip=True, ), midi_name=midi_music_name, diff --git a/Musicreater/plugin/archive.py b/Musicreater/plugin/archive.py index e3e5030..a44df14 100644 --- a/Musicreater/plugin/archive.py +++ b/Musicreater/plugin/archive.py @@ -25,12 +25,28 @@ from typing import List, Literal, Union def compress_zipfile(sourceDir, outFilename, compression=8, exceptFile=None): - """使用compression指定的算法打包目录为zip文件\n - 默认算法为DEFLATED(8),可用算法如下:\n - STORED = 0\n - DEFLATED = 8\n - BZIP2 = 12\n - LZMA = 14\n + """ + 使用指定的压缩算法将目录打包为zip文件 + + Parameters + ------------ + sourceDir: str + 要压缩的源目录路径 + outFilename: str + 输出的zip文件路径 + compression: int, 可选 + 压缩算法,默认为8 (DEFLATED) + 可用算法: + STORED = 0 + DEFLATED = 8 (默认) + BZIP2 = 12 + LZMA = 14 + exceptFile: list[str], 可选 + 需要排除在压缩包外的文件名称列表(可选) + + Returns + --------- + None """ zipf = zipfile.ZipFile(outFilename, "w", compression) diff --git a/Musicreater/plugin/bdx.py b/Musicreater/plugin/bdx.py index b43e5df..7f4a802 100644 --- a/Musicreater/plugin/bdx.py +++ b/Musicreater/plugin/bdx.py @@ -60,50 +60,55 @@ def form_command_block_in_BDX_bytes( customName: str = "", executeOnFirstTick: bool = False, trackOutput: bool = True, -): +) -> bytes: """ - 使用指定项目返回指定的指令方块放置指令项 - :param command: `str` + 使用指定参数生成指定的指令方块放置指令项 + + Parameters + ------------ + command: str 指令 - :param particularValue: + particularValue: int 方块特殊值,即朝向 - :0 下 无条件 - :1 上 无条件 - :2 z轴负方向 无条件 - :3 z轴正方向 无条件 - :4 x轴负方向 无条件 - :5 x轴正方向 无条件 - :6 下 无条件 - :7 下 无条件 - - :8 下 有条件 - :9 上 有条件 - :10 z轴负方向 有条件 - :11 z轴正方向 有条件 - :12 x轴负方向 有条件 - :13 x轴正方向 有条件 - :14 下 有条件 - :14 下 有条件 + :0 下 无条件 + :1 上 无条件 + :2 z轴负方向 无条件 + :3 z轴正方向 无条件 + :4 x轴负方向 无条件 + :5 x轴正方向 无条件 + :6 下 无条件 + :7 下 无条件 + :8 下 有条件 + :9 上 有条件 + :10 z轴负方向 有条件 + :11 z轴正方向 有条件 + :12 x轴负方向 有条件 + :13 x轴正方向 有条件 + :14 下 有条件 + :14 下 有条件 注意!此处特殊值中的条件会被下面condition参数覆写 - :param impluse: `int 0|1|2` + impluse: int (0|1|2) 方块类型 - 0脉冲 1循环 2连锁 - :param condition: `bool` + 0脉冲 1循环 2连锁 + condition: bool 是否有条件 - :param needRedstone: `bool` + needRedstone: bool 是否需要红石 - :param tickDelay: `int` + tickDelay: int 执行延时 - :param customName: `str` + customName: str 悬浮字 - lastOutput: `str` - 上次输出字符串,注意此处需要留空 - :param executeOnFirstTick: `bool` - 首刻执行(循环指令方块是否激活后立即执行,若为False,则从激活时起延迟后第一次执行) - :param trackOutput: `bool` - 是否输出 + lastOutput: str + 命令方块的上次输出字符串,注意此处需要留空 + executeOnFirstTick: bool + 是否启用首刻执行(循环指令方块是否激活后立即执行,若为False,则从激活时起延迟后第一次执行) + trackOutput: bool + 是否启用命令方块输出 - :return:str + Returns + --------- + bytes + 用以生成 bdx 结构的字节码 """ block = b"\x24" + particularValue.to_bytes(2, byteorder="big", signed=False) @@ -127,9 +132,19 @@ def commands_to_BDX_bytes( max_height: int = 64, ): """ - :param commands: 指令列表(指令, 延迟) - :param max_height: 生成结构最大高度 - :return 成功与否,成功返回(True,未经过压缩的源,结构占用大小),失败返回(False,str失败原因) + 指令列表转换为用以生成 bdx 结构的字节码 + + Parameters + ------------ + commands: list[tuple[str, int]] + 指令列表,每个元素为 (指令, 延迟) + max_height: int + 生成结构最大高度 + + Returns + --------- + tuple[bool, bytes, int] or tuple[bool, str] + 成功与否,成功返回 (True, 未经过压缩的源, 结构占用大小),失败返回 (False, str失败原因) """ _sideLength = bottem_side_length_of_smallest_square_bottom_box( diff --git a/Musicreater/plugin/common.py b/Musicreater/plugin/common.py index aead656..7fe050e 100644 --- a/Musicreater/plugin/common.py +++ b/Musicreater/plugin/common.py @@ -19,9 +19,22 @@ Terms & Conditions: License.md in the root directory import math -def bottem_side_length_of_smallest_square_bottom_box(total: int, maxHeight: int): - """给定总方块数量和最大高度,返回所构成的图形外切正方形的边长 - :param total: 总方块数量 - :param maxHeight: 最大高度 - :return: 外切正方形的边长 int""" - return math.ceil(math.sqrt(math.ceil(total / maxHeight))) +def bottem_side_length_of_smallest_square_bottom_box( + _total_block_count: int, _max_height: int +): + """ + 给定结构的总方块数量和规定的最大高度,返回该结构应当构成的图形,在底面的外切正方形之边长 + + Parameters + ------------ + _total_block_count: int + 总方块数量 + _max_height: int + 规定的结构最大高度 + + Returns + --------- + int + 外切正方形的边长 + """ + return math.ceil(math.sqrt(math.ceil(_total_block_count / _max_height))) diff --git a/Musicreater/plugin/mcstructure.py b/Musicreater/plugin/mcstructure.py index a0d6f53..7d0c098 100644 --- a/Musicreater/plugin/mcstructure.py +++ b/Musicreater/plugin/mcstructure.py @@ -70,17 +70,25 @@ def form_note_block_in_NBT_struct( instrument: str = "note.harp", powered: bool = False, compability_version_number: int = COMPABILITY_VERSION_119, -): - """生成音符盒方块 - :param note: `int`(0~24) +) -> Block: + """ + 生成音符盒方块 + + Parameters + ------------ + note: int (0~24) 音符的音高 - :param coordinate: `tuple[int,int,int]` + coordinate: tuple[int, int, int] 此方块所在之相对坐标 - :param instrument: `str` + instrument: str 音符盒的乐器 - :param powered: `bool` + powered: bool 是否已被激活 - :return Block + + Returns + ------- + Block + 生成的方块对象 """ return Block( @@ -108,15 +116,26 @@ def form_repeater_in_NBT_struct( delay: int, facing: int, compability_version_number: int = COMPABILITY_VERSION_119, -): - """生成中继器方块 - :param facing: 朝向: +) -> Block: + """ + 生成中继器方块 + + Parameters + ---------- + facing: int (0~3) + 朝向: Z- 北 0 X- 东 1 Z+ 南 2 X+ 西 3 - :param delay: 0~3 - :return Block()""" + delay: int (0~3) + 信号延迟 + + Returns + ------- + Block + 生成的方块对象 + """ return Block( "minecraft", @@ -141,50 +160,58 @@ def form_command_block_in_NBT_struct( executeOnFirstTick: bool = False, trackOutput: bool = True, compability_version_number: int = COMPABILITY_VERSION_119, -): +) -> Block: """ - 使用指定项目返回指定的指令方块结构 - :param command: `str` + 使用指定参数生成指令方块 + + + Parameters + ---------- + command: str 指令 - :param coordinate: `tuple[int,int,int]` + coordinate: tuple[int,int,int] 此方块所在之相对坐标 - :param particularValue: + particularValue: int 方块特殊值,即朝向 - :0 下 无条件 - :1 上 无条件 - :2 z轴负方向 无条件 - :3 z轴正方向 无条件 - :4 x轴负方向 无条件 - :5 x轴正方向 无条件 - :6 下 无条件 - :7 下 无条件 - - :8 下 有条件 - :9 上 有条件 - :10 z轴负方向 有条件 - :11 z轴正方向 有条件 - :12 x轴负方向 有条件 - :13 x轴正方向 有条件 - :14 下 有条件 - :14 下 有条件 + :0 下 无条件 + :1 上 无条件 + :2 z轴负方向 无条件 + :3 z轴正方向 无条件 + :4 x轴负方向 无条件 + :5 x轴正方向 无条件 + :6 下 无条件 + :7 下 无条件 + :8 下 有条件 + :9 上 有条件 + :10 z轴负方向 有条件 + :11 z轴正方向 有条件 + :12 x轴负方向 有条件 + :13 x轴正方向 有条件 + :14 下 有条件 + :14 下 有条件 注意!此处特殊值中的条件会被下面condition参数覆写 - :param impluse: `int 0|1|2` + impluse: int (0|1|2) 方块类型 - 0脉冲 1循环 2连锁 - :param condition: `bool` + 0脉冲 1循环 2连锁 + condition: bool 是否有条件 - :param alwaysRun: `bool` + alwaysRun: bool 是否始终执行 - :param tickDelay: `int` + tickDelay: int 执行延时 - :param customName: `str` + customName: str 悬浮字 - :param executeOnFirstTick: `bool` - 首刻执行(循环指令方块是否激活后立即执行,若为False,则从激活时起延迟后第一次执行) - :param trackOutput: `bool` - 是否输出 + executeOnFirstTick: bool + 是否启用首刻执行(循环指令方块是否激活后立即执行,若为False,则从激活时起延迟后第一次执行) + trackOutput: bool + 是否启用命令方块输出 + compability_version_number: int + 版本兼容代号 - :return:str + Returns + ------- + Block + 生成的方块对象 """ return Block( @@ -231,9 +258,19 @@ def commands_to_structure( compability_version_: int = COMPABILITY_VERSION_119, ): """ - :param commands: 指令列表 - :param max_height: 生成结构最大高度 - :return 结构类,结构占用大小,终点坐标 + 由指令列表生成(纯指令方块)结构 + + Parameters + ------------ + commands: list + 指令列表 + max_height: int + 生成结构最大高度 + + Returns + --------- + Structure, tuple[int, int, int], tuple[int, int, int] + 结构类, 结构占用大小, 终点坐标 """ _sideLength = bottem_side_length_of_smallest_square_bottom_box( @@ -321,12 +358,25 @@ def commands_to_redstone_delay_structure( compability_version_: int = COMPABILITY_VERSION_119, ) -> Tuple[Structure, Tuple[int, int, int], Tuple[int, int, int]]: """ - :param commands: 指令列表 - :param delay_length: 延时总长 - :param max_multicmd_length: 最大同时播放的音符数量 - :param base_block: 生成结构的基底方块 - :param axis_: 生成结构的延展方向 - :return 结构类,结构占用大小,终点坐标 + 由指令列表生成由红石中继器延迟的结构 + + Parameters + ------------ + commands: list + 指令列表 + delay_length: int + 延时总长 + max_multicmd_length: int + 最大同时播放的音符数量 + base_block: Block + 生成结构的基底方块 + axis_: str + 生成结构的延展方向 + + Returns + --------- + Structure, tuple[int, int, int], tuple[int, int, int] + 结构类, 结构占用大小, 终点坐标 """ if axis_ in ["z+", "Z+"]: extensioon_direction = z diff --git a/Musicreater/subclass.py b/Musicreater/subclass.py index a9d90e6..26a9ea3 100644 --- a/Musicreater/subclass.py +++ b/Musicreater/subclass.py @@ -70,23 +70,41 @@ class MineNote: azimuth: Optional[Tuple[float, float]] = None, extra_information: Optional[Dict[str, Any]] = None, ): - """用于存储单个音符的类 + """ + 用于存储单个音符的类 - :param mc_sound_name:`str` 《我的世界》声音ID - :param midi_pitch:`int` midi音高 - :param midi_velocity:`int` midi响度(力度) - :param start_time:`int` 开始之时(命令刻) + Parameters + ------------ + mc_sound_name: str + 《我的世界》声音ID + midi_pitch: int + midi音高 + midi_velocity: int + midi响度(力度) + start_time: int + 开始之时(命令刻) 注:此处的时间是用从乐曲开始到当前的刻数 - :param last_time:`int` 音符延续时间(命令刻) - :param mass_precision_time:`int` 高精度的开始时间偏移量(1/1250秒) - :param is_percussion:`bool` 是否作为打击乐器 - :param distance: `float` 发声源距离玩家的距离(半径 `r`) + last_time: int + 音符延续时间(命令刻) + mass_precision_time: int + 高精度的开始时间偏移量(1/1250秒) + is_percussion: bool + 是否作为打击乐器 + distance: float + 发声源距离玩家的距离(半径 `r`) 注:距离越近,音量越高,默认为 0。此参数可以与音量成某种函数关系。 - :param azimuth:`tuple[float, float]` 声源方位 + azimuth: tuple[float, float] + 声源方位 注:此参数为tuple,包含两个元素,分别表示: `rV` 发声源在竖直(上下)轴上,从玩家视角正前方开始,向顺时针旋转的角度 `rH` 发声源在水平(左右)轴上,从玩家视角正前方开始,向上(到达玩家正上方顶点后变为向下,以此类推的旋转)旋转的角度 - :param extra_information:`Any` 附加信息""" + extra_information: Any + 附加信息,尽量存储为字典 + + Returns + --------- + MineNote 类 + """ self.sound_name: str = mc_sound_name """乐器ID""" self.note_pitch: int = 66 if midi_pitch is None else midi_pitch @@ -94,9 +112,9 @@ class MineNote: self.velocity: int = midi_velocity """响度(力度)""" self.start_tick: int = start_time - """开始之时 tick""" + """开始之时 命令刻""" self.duration: int = last_time - """音符持续时间 tick""" + """音符持续时间 命令刻""" self.high_precision_time: int = mass_precision_time """高精度开始时间偏量 0.4 毫秒""" @@ -133,17 +151,35 @@ class MineNote: displacement: Optional[Tuple[float, float, float]] = None, extra_information: Optional[Any] = None, ): - """用于存储单个音符的类 - :param mc_sound_name:`str` 《我的世界》声音ID - :param midi_pitch:`int` midi音高 - :param midi_velocity:`int` midi响度(力度) - :param start_time:`int` 开始之时(命令刻) + """ + 从传统音像位移格式传参,写入用于存储单个音符的类 + + Parameters + ------------ + mc_sound_name: str + 《我的世界》声音ID + midi_pitch: int + midi音高 + midi_velocity: int + midi响度(力度) + start_time: int + 开始之时(命令刻) 注:此处的时间是用从乐曲开始到当前的刻数 - :param last_time:`int` 音符延续时间(命令刻) - :param mass_precision_time:`int` 高精度的开始时间偏移量(1/1250秒) - :param is_percussion:`bool` 是否作为打击乐器 - :param displacement:`tuple[float,float,float]` 声像位移 - :param extra_information:`Any` 附加信息""" + last_time: int + 音符延续时间(命令刻) + mass_precision_time: int + 高精度的开始时间偏移量(1/1250秒) + is_percussion: bool + 是否作为打击乐器 + displacement: tuple[float, float, float] + 声像位移 + extra_information: Any + 附加信息,尽量为字典。 + + Returns + --------- + MineNote 类 + """ if displacement is None: displacement = (0, 0, 0) @@ -246,10 +282,17 @@ class MineNote: """ 将数据打包为字节码 - :param is_displacement_included:`bool` 是否包含声像偏移数据,默认为**是** - :param is_high_time_precision:`bool` 是否启用高精度,默认为**是** + Parameters + ------------ + is_displacement_included: bool + 是否包含声像偏移数据,默认为**是** + is_high_time_precision: bool + 是否启用高精度,默认为**是** - :return bytes 打包好的字节码 + Returns + --------- + bytes + 打包好的字节码 """ # MineNote 的字节码共有三个顺次版本分别如下 @@ -545,12 +588,26 @@ class SingleNoteBox: percussion: Optional[bool] = None, annotation: str = "", ): - """用于存储单个音符盒的类 - :param instrument_block_ 音符盒演奏所使用的乐器方块 - :param note_value_ 音符盒的演奏音高 - :param percussion 此音符盒乐器是否作为打击乐处理 + """ + 用于存储单个音符盒的类 + + Parameters + ------------ + instrument_block_: str + 音符盒演奏所使用的乐器方块 + note_value_: int + 音符盒的演奏音高 + percussion: bool + 此音符盒乐器是否作为打击乐处理 注:若为空,则自动识别是否为打击乐器 - :param annotation 音符注释""" + annotation: Any + 音符注释 + + Returns + --------- + SingleNoteBox 类 + """ + self.instrument_block = instrument_block_ """乐器方块""" self.note_value = note_value_ @@ -634,7 +691,8 @@ class ProgressBarStyle: to_play_s: Optional[str] = None, played_s: Optional[str] = None, ): - """用于存储进度条样式的类 + """ + 用于存储进度条样式的类 | 标识符 | 指定的可变量 | |---------|----------------| @@ -646,9 +704,18 @@ class ProgressBarStyle: | `%%%` | 当前进度比率 | | `_` | 用以表示进度条占位| - :param base_s 基础样式,用以定义进度条整体 - :param to_play_s 进度条样式:尚未播放的样子 - :param played_s 已经播放的样子 + Parameters + ------------ + base_s: str + 基础样式,用以定义进度条整体 + to_play_s: str + 进度条样式:尚未播放的样子 + played_s: str + 已经播放的样子 + + Returns + --------- + ProgressBarStyle 类 """ self.base_style = ( @@ -709,9 +776,19 @@ class ProgressBarStyle: """ 直接依照此格式输出一个进度条 - :param played_delays: int 当前播放进度积分值 - :param total_delays: int 乐器总延迟数(积分数) - :param music_name: str 曲名 + Parameters + ------------ + played_delays: int + 当前播放进度积分值 + total_delays: int + 乐器总延迟数(计分板值) + music_name: str + 曲名 + + Returns + --------- + str + 进度条字符串 """ return ( diff --git a/Musicreater/utils.py b/Musicreater/utils.py index 994af5f..509a909 100644 --- a/Musicreater/utils.py +++ b/Musicreater/utils.py @@ -136,7 +136,7 @@ def velocity_2_distance_natural( Parameters ---------- vol: int - midi音符力度值 + midi 音符力度值 Returns ------- @@ -162,7 +162,7 @@ def velocity_2_distance_straight(vol: float) -> float: Parameters ---------- vol: int - midi音符力度值 + midi 音符力度值 Returns ------- @@ -179,7 +179,7 @@ def panning_2_rotation_linear(pan_: float) -> float: ---------- pan_: int Midi 左右平衡偏移值 - 注:此参数为int,范围从0到127,当为 64 时,声源居中 + 注:此参数为int,范围从 0 到 127,当为 64 时,声源居中 Returns ------- @@ -197,7 +197,7 @@ def panning_2_rotation_trigonometric(pan_: float) -> float: ---------- pan_: int Midi 左右平衡偏移值 - 注:此参数为int,范围从0到127,当为 64 时,声源居中 + 注:此参数为int,范围从 0 到 127,当为 64 时,声源居中 Returns ------- @@ -222,11 +222,19 @@ def minenote_to_command_paramaters( Union[float, Literal[None]], ]: """ - 将MineNote对象转为《我的世界》音符播放所需之参数 - :param note_:MineNote 音符对象 - :param deviation:float 音调偏移量 + 将 MineNote 对象转为《我的世界》音符播放所需之参数 - :return str[我的世界音符ID], Tuple[float,float,float]播放视角坐标, float[指令音量参数], float[指令音调参数] + Parameters + ------------ + note_: MineNote + 音符对象 + deviation: float + 音调偏移量 + + Returns + --------- + str, tuple[float, float, float], float, float + 我的世界音符ID, 播放视角坐标, 指令音量参数, 指令音调参数 """ return ( @@ -252,7 +260,6 @@ def minenote_to_command_paramaters( ) - def midi_msgs_to_minenote( inst_: int, # 乐器编号 note_: int, @@ -267,24 +274,46 @@ def midi_msgs_to_minenote( volume_processing_method_: FittingFunctionType, panning_processing_method_: FittingFunctionType, note_table_replacement: Dict[str, str] = {}, + lyric_line: str = "", ) -> MineNote: """ 将Midi信息转为我的世界音符对象 - :param inst_: int 乐器编号 - :param note_: int 音高编号(音符编号) - :param percussive_: bool 是否作为打击乐器启用 - :param volume_: int 音量 - :param velocity_: int 力度 - :param panning_: int 声相偏移 - :param start_time_: int 音符起始时间(微秒) - :param duration_: int 音符持续时间(微秒) - :param play_speed: float 曲目播放速度 - :param midi_reference_table: Dict[int, str] 转换对照表 - :param volume_processing_method_: Callable[[float], float] 音量处理函数 - :param panning_processing_method_: Callable[[float], float] 立体声相偏移处理函数 - :param note_table_replacement: Dict[str, str] 音符替换表,定义 Minecraft 音符字串的替换 - :return MineNote我的世界音符对象 + Parameters + ------------ + inst_: int + 乐器编号 + note_: int + 音高编号(音符编号) + percussive_: bool + 是否作为打击乐器启用 + volume_: int + 音量 + velocity_: int + 力度 + panning_: int + 声相偏移 + start_time_: int + 音符起始时间(微秒) + duration_: int + 音符持续时间(微秒) + play_speed: float + 曲目播放速度 + midi_reference_table: Dict[int, str] + 转换对照表 + volume_processing_method_: Callable[[float], float] + 音量处理函数 + panning_processing_method_: Callable[[float], float] + 立体声相偏移处理函数 + note_table_replacement: Dict[str, str] + 音符替换表,定义 Minecraft 音符字串的替换 + lyric_line: str + 该音符的歌词 + + Returns + --------- + MineNote + 我的世界音符对象 """ mc_sound_ID = midi_inst_to_mc_sound( inst_, @@ -302,6 +331,11 @@ def midi_msgs_to_minenote( is_percussion=percussive_, distance=volume_processing_method_(volume_), azimuth=(panning_processing_method_(panning_), 0), + extra_information={ + "LYRIC_TEXT": lyric_line, + "VOLUME_VALUE": volume_, + "PIN_VALUE": panning_, + }, ) @@ -319,24 +353,46 @@ def midi_msgs_to_minenote_using_kami_respack( volume_processing_method_: Callable[[float], float], panning_processing_method_: FittingFunctionType, note_table_replacement: Dict[str, str] = {}, + lyric_line: str = "", ) -> MineNote: """ - 将Midi信息转为我的世界音符对象 - :param inst_: int 乐器编号 - :param note_: int 音高编号(音符编号) - :param percussive_: bool 是否作为打击乐器启用 - :param volume_: int 音量 - :param velocity_: int 力度 - :param panning_: int 声相偏移 - :param start_time_: int 音符起始时间(微秒) - :param duration_: int 音符持续时间(微秒) - :param play_speed: float 曲目播放速度 - :param midi_reference_table: Dict[int, str] 转换对照表 - :param volume_processing_method_: Callable[[float], float] 音量处理函数 - :param panning_processing_method_: Callable[[float], float] 立体声相偏移处理函数 - :param note_table_replacement: Dict[str, str] 音符替换表,定义 Minecraft 音符字串的替换 + 将Midi信息转为我的世界音符对象,使用神羽资源包兼容格式 - :return MineNote我的世界音符对象 + Parameters + ------------ + inst_: int + 乐器编号 + note_: int + 音高编号(音符编号) + percussive_: bool + 是否作为打击乐器启用 + volume_: int + 音量 + velocity_: int + 力度 + panning_: int + 声相偏移 + start_time_: int + 音符起始时间(微秒) + duration_: int + 音符持续时间(微秒) + play_speed: float + 曲目播放速度 + midi_reference_table: Dict[int, str] + 转换对照表 + volume_processing_method_: Callable[[float], float] + 音量处理函数 + panning_processing_method_: Callable[[float], float] + 立体声相偏移处理函数 + note_table_replacement: Dict[str, str] + 音符替换表,定义 Minecraft 音符字串的替换 + lyric_line: str + 该音符的歌词 + + Returns + --------- + MineNote + 我的世界音符对象 """ using_original = False @@ -371,6 +427,7 @@ def midi_msgs_to_minenote_using_kami_respack( "USING_ORIGINAL_SOUND": using_original, # 判断 extra_information 中是否有 USING_ORIGINAL_SOUND 键是判断是否使用神羽资源包解析的一个显著方法 "INST_VALUE": note_ if percussive_ else inst_, "NOTE_VALUE": inst_ if percussive_ else note_, + "LYRIC_TEXT": lyric_line, "VOLUME_VALUE": volume_, "PIN_VALUE": panning_, }, diff --git a/test_future_lyric.py b/test_future_lyric.py new file mode 100644 index 0000000..0d31e15 --- /dev/null +++ b/test_future_lyric.py @@ -0,0 +1,33 @@ +import Musicreater.experiment +import Musicreater.plugin +import Musicreater.plugin.mcstructfile + +msct = Musicreater.experiment.FutureMidiConvertLyricSupport.from_midi_file( + input("midi路径:"), old_exe_format=False +) + +opt = input("输出路径:") + +# print( +# "乐器使用情况", +# ) + +# for name in sorted( +# set( +# [ +# n.split(".")[0].replace("c", "").replace("d", "") +# for n in msct.note_count_per_instrument.keys() +# ] +# ) +# ): +# print("\t", name, flush=True) + +print( + "\n输出:", + Musicreater.plugin.mcstructfile.to_mcstructure_file_in_delay( + msct, + opt, + # Musicreater.plugin.ConvertConfig(input("输出路径:"),), + max_height=32, + ), +)