完成2.0.0正式版构建,现已支持音乐保存……

This commit is contained in:
EillesWan 2024-03-10 18:47:10 +08:00
parent 77351d767b
commit f1ab2373b5
9 changed files with 327 additions and 569 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@
/*.midi /*.midi
/*.mcpack /*.mcpack
/*.bdx /*.bdx
/*.msq
/*.json /*.json
/*.mcstructure /*.mcstructure
.mscbackup .mscbackup

View File

@ -17,18 +17,19 @@ Terms & Conditions: License.md in the root directory
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
__version__ = "2.0.0-beta" __version__ = "2.0.0"
__vername__ = "全新组织架构" __vername__ = "全新组织架构"
__author__ = ( __author__ = (
("金羿", "Eilles Wan"), ("金羿", "Eilles Wan"),
("诸葛亮与八卦阵", "bgArray"), ("诸葛亮与八卦阵", "bgArray"),
("偷吃不是Touch", "Touch"), ("偷吃不是Touch", "Touch"),
("鸣凤鸽子", "MingFengPigeon"), ("鱼旧梦", "ElapsingDreams"),
) )
__all__ = [ __all__ = [
# 主要类 # 主要类
"MusicSequence", "MusicSequence",
"MidiConvert", "MidiConvert",
"MusicSave",
# 附加类 # 附加类
"SingleNote", "SingleNote",
"MineNote", "MineNote",

View File

@ -467,15 +467,15 @@ MM_INSTRUMENT_DEVIATION_TABLE: Dict[str, int] = {
"note.banjo": 6, "note.banjo": 6,
"note.flute": 18, "note.flute": 18,
"note.bass": -18, "note.bass": -18,
"note.snare": -1, "note.snare": 0,
"note.didgeridoo": -18, "note.didgeridoo": -18,
"mob.zombie.wood": -1, "mob.zombie.wood": 0,
"note.bit": 6, "note.bit": 6,
"note.hat": -1, "note.hat": 0,
"note.bd": -1, "note.bd": 0,
"firework.blast": -1, "firework.blast": 0,
"firework.twinkle": -1, "firework.twinkle": 0,
"fire.ignite": -1, "fire.ignite": 0,
"note.cow_bell": 6, "note.cow_bell": 6,
} }
"""不同乐器的音调偏离对照表""" """不同乐器的音调偏离对照表"""

View File

@ -157,7 +157,7 @@ class FutureMidiConvertM4(MidiConvert):
*relative_coordinates, *relative_coordinates,
volume_percentage, volume_percentage,
1.0 if note.percussive else mc_pitch, 1.0 if note.percussive else mc_pitch,
self.minium_volume, self.minimum_volume,
) )
), ),
annotation=( annotation=(

View File

@ -88,7 +88,7 @@ class MusicSequence:
note_count_per_instrument: Dict[str, int] note_count_per_instrument: Dict[str, int]
"""所使用的乐器""" """所使用的乐器"""
minium_volume: float minimum_volume: float
"""乐曲最小音量""" """乐曲最小音量"""
music_deviation: float music_deviation: float
@ -100,11 +100,11 @@ class MusicSequence:
channels_of_notes: MineNoteChannelType, channels_of_notes: MineNoteChannelType,
music_note_count: Optional[int] = None, music_note_count: Optional[int] = None,
note_used_per_instrument: Optional[Dict[str, int]] = None, note_used_per_instrument: Optional[Dict[str, int]] = None,
minium_volume_of_music: float = 0.1, minimum_volume_of_music: float = 0.1,
deviation_value: Optional[float] = None, deviation_value: Optional[float] = None,
) -> None: ) -> None:
""" """
我的世界音符序列类 音符序列类
Paramaters Paramaters
========== ==========
@ -112,21 +112,27 @@ class MusicSequence:
乐曲名称 乐曲名称
channels_of_notes: MineNoteChannelType channels_of_notes: MineNoteChannelType
音乐音轨 音乐音轨
minium_volume_of_music: float music_note_count: int
总音符数
note_used_per_instrument: Dict[str, int]
全曲乐器使用统计
minimum_volume_of_music: float
音乐最小音量(0,1] 音乐最小音量(0,1]
deviation_value: float
全曲音调偏移值
""" """
if minium_volume_of_music > 1 or minium_volume_of_music <= 0: if minimum_volume_of_music > 1 or minimum_volume_of_music <= 0:
raise IllegalMinimumVolumeError( raise IllegalMinimumVolumeError(
"自订的最小音量参数错误:{},应在 (0,1] 范围内。".format( "自订的最小音量参数错误:{},应在 (0,1] 范围内。".format(
minium_volume_of_music minimum_volume_of_music
) )
) )
# max_volume = 1 if max_volume > 1 else (0.001 if max_volume <= 0 else max_volume) # max_volume = 1 if max_volume > 1 else (0.001 if max_volume <= 0 else max_volume)
self.music_name = name_of_music self.music_name = name_of_music
self.channels = channels_of_notes self.channels = channels_of_notes
self.minium_volume = minium_volume_of_music self.minimum_volume = minimum_volume_of_music
if (note_used_per_instrument is None) or (music_note_count is None): if (note_used_per_instrument is None) or (music_note_count is None):
kp = [i.sound_name for j in self.channels.values() for i in j] kp = [i.sound_name for j in self.channels.values() for i in j]
@ -151,17 +157,43 @@ class MusicSequence:
midi_music_name: str, midi_music_name: str,
mismatch_error_ignorance: bool = True, mismatch_error_ignorance: bool = True,
speed_multiplier: float = 1, speed_multiplier: float = 1,
default_tempo: int = mido.midifiles.midifiles.DEFAULT_TEMPO,
pitched_note_referance_table: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE, pitched_note_referance_table: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
percussion_note_referance_table: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE, percussion_note_referance_table: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
minium_vol: float = 0.1, minimum_vol: float = 0.1,
volume_processing_function: FittingFunctionType = natural_curve, volume_processing_function: FittingFunctionType = natural_curve,
default_tempo: int = mido.midifiles.midifiles.DEFAULT_TEMPO,
deviation: float = 0, deviation: float = 0,
): ):
"""
自mido对象导入一个音符序列类
Paramaters
==========
mido_file: mido.MidiFile 对象
需要处理的midi对象
midi_music_name: str
音乐名称
mismatch_error_ignorance bool
是否在导入时忽略音符不匹配错误
speed_multiplier: float
音乐播放速度倍数
default_tempo: int
默认的MIDI TEMPO值
pitched_note_referance_table: Dict[int, Tuple[str, int]]
乐音乐器Midi-MC对照表
percussion_note_referance_table: Dict[int, Tuple[str, int]]
打击乐器Midi-MC对照表
minimum_vol: float
播放的最小音量 应为 (0,1] 范围内的小数
volume_processing_function: Callable[[float], float]
声像偏移拟合函数
deviation: float
全曲音调偏移值
"""
( (
note_channels, note_channels,
note_count_total, note_count_total,
inst_note_count, # qualified_inst_note_count, inst_note_count,
) = cls.to_music_note_channels( ) = cls.to_music_note_channels(
midi=mido_file, midi=mido_file,
speed=speed_multiplier, speed=speed_multiplier,
@ -176,22 +208,34 @@ class MusicSequence:
channels_of_notes=note_channels, channels_of_notes=note_channels,
music_note_count=note_count_total, music_note_count=note_count_total,
note_used_per_instrument=inst_note_count, note_used_per_instrument=inst_note_count,
minium_volume_of_music=minium_vol, minimum_volume_of_music=minimum_vol,
deviation_value=deviation, deviation_value=deviation,
) )
def set_min_volume(self, volume_value: int): def set_min_volume(self, volume_value: int):
self.minium_volume = volume_value """重新设置全曲最小音量"""
if volume_value > 1 or volume_value <= 0:
raise IllegalMinimumVolumeError(
"自订的最小音量参数错误:{},应在 (0,1] 范围内。".format(volume_value)
)
self.minimum_volume = volume_value
def set_deviation(self, deviation_value: int): def set_deviation(self, deviation_value: int):
"""重新设置全曲音调偏移"""
self.music_deviation = deviation_value self.music_deviation = deviation_value
def rename_music(self, new_name: str): def rename_music(self, new_name: str):
"""重命名此音乐"""
self.music_name = new_name self.music_name = new_name
def add_note(self, channel_no: int, note: MineNote, is_sort: bool = False): def add_note(self, channel_no: int, note: MineNote, is_sort: bool = False):
"""在指定通道添加一个音符"""
self.channels[channel_no].append(note) self.channels[channel_no].append(note)
self.total_note_count += 1 self.total_note_count += 1
if note.sound_name in self.note_count_per_instrument.keys():
self.note_count_per_instrument[note.sound_name] += 1
else:
self.note_count_per_instrument[note.sound_name] = 1
if is_sort: if is_sort:
self.channels[channel_no].sort(key=lambda note: note.start_tick) self.channels[channel_no].sort(key=lambda note: note.start_tick)
@ -247,34 +291,45 @@ class MusicSequence:
midi: mido.MidiFile, midi: mido.MidiFile,
ignore_mismatch_error: bool = True, ignore_mismatch_error: bool = True,
speed: float = 1.0, speed: float = 1.0,
default_tempo_value: int = mido.midifiles.midifiles.DEFAULT_TEMPO,
pitched_note_rtable: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE, pitched_note_rtable: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
percussion_note_rtable: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE, percussion_note_rtable: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
default_tempo_value: int = mido.midifiles.midifiles.DEFAULT_TEMPO,
vol_processing_function: FittingFunctionType = natural_curve, vol_processing_function: FittingFunctionType = natural_curve,
) -> Tuple[MineNoteChannelType, int, Dict[str, int]]: # , Dict[str, int]]: ) -> Tuple[MineNoteChannelType, int, Dict[str, int]]:
""" """
将midi解析并转换为频道音符字典 将midi解析并转换为频道音符字典
Parameters
----------
midi: mido.MidiFile 对象
需要处理的midi对象
ignore_mismatch_error bool
是否在导入时忽略音符不匹配错误
speed: float
音乐播放速度倍数
default_tempo_value: int
默认的MIDI TEMPO值
pitched_note_rtable: Dict[int, Tuple[str, int]]
乐音乐器Midi-MC对照表
percussion_note_rtable: Dict[int, Tuple[str, int]]
打击乐器Midi-MC对照表
vol_processing_function: Callable[[float], float]
声像偏移拟合函数
Returns Returns
------- -------
以频道作为分割的Midi音符列表字典: 以频道作为分割的Midi音符列表字典, 音符总数, 乐器使用统计:
Dict[int,List[SingleNote,]] Tuple[MineNoteChannelType, int, Dict[str, int]]
""" """
if speed == 0: if speed == 0:
raise ZeroSpeedError("播放速度为 0 ,其需要(0,1]范围内的实数。") raise ZeroSpeedError("播放速度为 0 ,其需要(0,1]范围内的实数。")
# if midi is None:
# raise MidiUnboundError(
# "Midi参量为空。你是否正在使用的是一个由 copy_important 生成的MidiConvert对象这是不可复用的。"
# )
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
midi_channels: MineNoteChannelType = empty_midi_channels(staff=[]) midi_channels: MineNoteChannelType = empty_midi_channels(staff=[])
tempo = default_tempo_value tempo = default_tempo_value
note_count = 0 note_count = 0
note_count_per_instrument: Dict[str, int] = {} note_count_per_instrument: Dict[str, int] = {}
# qualified_note_count_per_instruments: Dict[str, int] = {}
# 我们来用通道统计音乐信息 # 我们来用通道统计音乐信息
# 但是是用分轨的思路的 # 但是是用分轨的思路的
@ -366,14 +421,8 @@ class MusicSequence:
note_count += 1 note_count += 1
if that_note.sound_name in note_count_per_instrument.keys(): if that_note.sound_name in note_count_per_instrument.keys():
note_count_per_instrument[that_note.sound_name] += 1 note_count_per_instrument[that_note.sound_name] += 1
# qualified_note_count_per_instruments[
# that_note.sound_name
# ] += is_note_in_diapason(that_note)
else: else:
note_count_per_instrument[that_note.sound_name] = 1 note_count_per_instrument[that_note.sound_name] = 1
# qualified_note_count_per_instruments[
# that_note.sound_name
# ] = int(is_note_in_diapason(that_note))
else: else:
if ignore_mismatch_error: if ignore_mismatch_error:
print( print(
@ -411,7 +460,6 @@ class MusicSequence:
channels, channels,
note_count, note_count,
note_count_per_instrument, note_count_per_instrument,
# qualified_note_count_per_instruments,
) )
@ -442,9 +490,8 @@ class MidiConvert(MusicSequence):
default_tempo_value: int = mido.midifiles.midifiles.DEFAULT_TEMPO, default_tempo_value: int = mido.midifiles.midifiles.DEFAULT_TEMPO,
pitched_note_rtable: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE, pitched_note_rtable: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
percussion_note_rtable: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE, percussion_note_rtable: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
# enable_devation_guess: bool = True,
enable_old_exe_format: bool = False, enable_old_exe_format: bool = False,
minium_volume: float = 0.1, minimum_volume: float = 0.1,
vol_processing_function: FittingFunctionType = natural_curve, vol_processing_function: FittingFunctionType = natural_curve,
): ):
""" """
@ -454,14 +501,24 @@ class MidiConvert(MusicSequence):
---------- ----------
midi_obj: mido.MidiFile 对象 midi_obj: mido.MidiFile 对象
需要处理的midi对象 需要处理的midi对象
midi_name: MIDI乐曲名称 midi_name: str
此音乐之名 此音乐之名称
enable_old_exe_format: bool ignore_mismatch_error bool
是否启用旧版(1.19)指令格式默认为否 是否在导入时忽略音符不匹配错误
playment_speed: float
音乐播放速度倍数
default_tempo_value: int
默认的MIDI TEMPO值
pitched_note_rtable: Dict[int, Tuple[str, int]] pitched_note_rtable: Dict[int, Tuple[str, int]]
乐音乐器Midi-MC对照表 乐音乐器Midi-MC对照表
percussion_note_rtable: Dict[int, Tuple[str, int]] percussion_note_rtable: Dict[int, Tuple[str, int]]
打击乐器Midi-MC对照表 打击乐器Midi-MC对照表
enable_old_exe_format: bool
是否启用旧版(1.19)指令格式默认为否
minimum_volume: float
最小播放音量
vol_processing_function: Callable[[float], float]
声像偏移拟合函数
""" """
cls.enable_old_exe_format: bool = enable_old_exe_format cls.enable_old_exe_format: bool = enable_old_exe_format
@ -481,10 +538,9 @@ class MidiConvert(MusicSequence):
speed_multiplier=playment_speed, speed_multiplier=playment_speed,
pitched_note_referance_table=pitched_note_rtable, pitched_note_referance_table=pitched_note_rtable,
percussion_note_referance_table=percussion_note_rtable, percussion_note_referance_table=percussion_note_rtable,
minium_vol=minium_volume, minimum_vol=minimum_volume,
volume_processing_function=vol_processing_function, volume_processing_function=vol_processing_function,
default_tempo=default_tempo_value, default_tempo=default_tempo_value,
# devation_guess_enabled=enable_devation_guess,
mismatch_error_ignorance=ignore_mismatch_error, mismatch_error_ignorance=ignore_mismatch_error,
) )
@ -497,7 +553,6 @@ class MidiConvert(MusicSequence):
default_tempo: int = mido.midifiles.midifiles.DEFAULT_TEMPO, default_tempo: int = mido.midifiles.midifiles.DEFAULT_TEMPO,
pitched_note_table: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE, pitched_note_table: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE,
percussion_note_table: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE, percussion_note_table: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE,
# devation_guess_enabled: bool = True,
old_exe_format: bool = False, old_exe_format: bool = False,
min_volume: float = 0.1, min_volume: float = 0.1,
vol_processing_func: FittingFunctionType = natural_curve, vol_processing_func: FittingFunctionType = natural_curve,
@ -509,17 +564,22 @@ class MidiConvert(MusicSequence):
---------- ----------
midi_file_path: str midi_file_path: str
midi文件地址 midi文件地址
mismatch_error_ignorance bool
speed: float 是否在导入时忽略音符不匹配错误
速度注意这里的速度指的是播放倍率其原理为在播放音频的时候每个音符的播放时间除以 speed play_speed: float
音乐播放速度倍数
default_tempo: int
默认的MIDI TEMPO值
pitched_note_table: Dict[int, Tuple[str, int]] pitched_note_table: Dict[int, Tuple[str, int]]
乐音乐器Midi-MC对照表 乐音乐器Midi-MC对照表
percussion_note_table: Dict[int, Tuple[str, int]] percussion_note_table: Dict[int, Tuple[str, int]]
打击乐器Midi-MC对照表 打击乐器Midi-MC对照表
enable_old_exe_format: bool old_exe_format: bool
是否启用旧版(1.19)指令格式默认为否 是否启用旧版(1.19)指令格式默认为否
min_volume: float min_volume: float
最小播放音量注意这里的音量范围为(0,1]如果超出将被处理为正确值 最小播放音量
vol_processing_func: Callable[[float], float]
声像偏移拟合函数
""" """
midi_music_name = os.path.splitext(os.path.basename(midi_file_path))[0].replace( midi_music_name = os.path.splitext(os.path.basename(midi_file_path))[0].replace(
@ -539,9 +599,8 @@ class MidiConvert(MusicSequence):
default_tempo_value=default_tempo, default_tempo_value=default_tempo,
pitched_note_rtable=pitched_note_table, pitched_note_rtable=pitched_note_table,
percussion_note_rtable=percussion_note_table, percussion_note_rtable=percussion_note_table,
# enable_devation_guess=devation_guess_enabled,
enable_old_exe_format=old_exe_format, enable_old_exe_format=old_exe_format,
minium_volume=min_volume, minimum_volume=min_volume,
vol_processing_function=vol_processing_func, vol_processing_function=vol_processing_func,
) )
except (ValueError, TypeError) as E: except (ValueError, TypeError) as E:
@ -549,9 +608,6 @@ class MidiConvert(MusicSequence):
except FileNotFoundError as E: except FileNotFoundError as E:
raise FileNotFoundError(f"文件{midi_file_path}不存在:{E}") raise FileNotFoundError(f"文件{midi_file_path}不存在:{E}")
# ……真的那么重要吗
# 我又几曾何时,知道祂真的会抛下我
def form_progress_bar( def form_progress_bar(
self, self,
max_score: int, max_score: int,
@ -564,17 +620,15 @@ class MidiConvert(MusicSequence):
Parameters Parameters
---------- ----------
max_score: int max_score: int
midi的乐器ID 最大的积分值
scoreboard_name: str scoreboard_name: str
所使用的计分板名称 所使用的计分板名称
progressbar_style: ProgressBarStyle progressbar_style: ProgressBarStyle
此参数详见 ../docs/库的生成与功能文档.md#进度条自定义 此参数详见 ../docs/库的生成与功能文档.md#进度条自定义
Returns Returns
------- -------
list[SingleCommand,] list[MineCommand,]
""" """
pgs_style = progressbar_style.base_style pgs_style = progressbar_style.base_style
"""用于被替换的进度条原始样式""" """用于被替换的进度条原始样式"""
@ -665,10 +719,6 @@ class MidiConvert(MusicSequence):
) )
) )
# 那是假的
# 一切都并未留下痕迹啊
# 那梦又是多么的真实……
if r"%%t" in pgs_style: if r"%%t" in pgs_style:
result.append( result.append(
MineCommand( MineCommand(
@ -841,7 +891,7 @@ class MidiConvert(MusicSequence):
Returns Returns
------- -------
tuple( list[list[SingleCommand指令,... ],... ], int指令数量, int音乐时长游戏刻 ) tuple( list[list[MineCommand指令,... ],... ], int指令数量, int音乐时长游戏刻 )
""" """
command_channels = [] command_channels = []
@ -884,7 +934,7 @@ class MidiConvert(MusicSequence):
*relative_coordinates, *relative_coordinates,
volume_percentage, volume_percentage,
1.0 if note.percussive else mc_pitch, 1.0 if note.percussive else mc_pitch,
self.minium_volume, self.minimum_volume,
) )
), ),
annotation=( annotation=(
@ -923,7 +973,7 @@ class MidiConvert(MusicSequence):
Returns Returns
------- -------
tuple( list[SingleCommand,...], int音乐时长游戏刻, int最大同时播放的指令数量 ) tuple( list[MineCommand指令,...], int音乐时长游戏刻, int最大同时播放的指令数量 )
""" """
notes_list: List[MineNote] = sorted( notes_list: List[MineNote] = sorted(
@ -962,7 +1012,7 @@ class MidiConvert(MusicSequence):
*relative_coordinates, *relative_coordinates,
volume_percentage, volume_percentage,
1.0 if note.percussive else mc_pitch, 1.0 if note.percussive else mc_pitch,
self.minium_volume, self.minimum_volume,
) )
), ),
annotation=( annotation=(
@ -997,7 +1047,7 @@ class MidiConvert(MusicSequence):
Returns Returns
------- -------
Tuple[Dict[str, List[MineCommand]], int音乐时长游戏刻, int最大同时播放的指令数量 ) Tuple[Dict[str, List[MineCommand指令]], int音乐时长游戏刻, int最大同时播放的指令数量 )
""" """
notes_list: List[MineNote] = sorted( notes_list: List[MineNote] = sorted(
@ -1048,7 +1098,7 @@ class MidiConvert(MusicSequence):
*relative_coordinates, *relative_coordinates,
volume_percentage, volume_percentage,
1.0 if note.percussive else mc_pitch, 1.0 if note.percussive else mc_pitch,
self.minium_volume, self.minimum_volume,
) )
), ),
annotation=( annotation=(
@ -1082,3 +1132,72 @@ class MidiConvert(MusicSequence):
dst.music_command_list = [i.copy() for i in self.music_command_list] dst.music_command_list = [i.copy() for i in self.music_command_list]
dst.progress_bar_command = [i.copy() for i in self.progress_bar_command] dst.progress_bar_command = [i.copy() for i in self.progress_bar_command]
return dst return dst
class MusicSave(MusicSequence):
@classmethod
def load_decode(
cls,
bytes_buffer_in: bytes,
):
"""从字节码导入音乐序列"""
group_1 = int.from_bytes(bytes_buffer_in[4:6], "big")
music_name_ = bytes_buffer_in[8 : (stt_index := 8 + (group_1 >> 10))].decode(
"utf-8"
)
channels_: MineNoteChannelType = empty_midi_channels(staff=[])
for channel_index in channels_.keys():
for i in range(
int.from_bytes(
bytes_buffer_in[stt_index : (stt_index := stt_index + 4)], "big"
)
):
try:
end_index = stt_index + 14 + (bytes_buffer_in[stt_index] >> 2)
channels_[channel_index].append(
MineNote.decode(bytes_buffer_in[stt_index:end_index])
)
stt_index = end_index
except:
print(channels_)
raise
return cls(
name_of_music=music_name_,
channels_of_notes=channels_,
minimum_volume_of_music=(group_1 & 0b1111111111) / 1000,
deviation_value=int.from_bytes(bytes_buffer_in[6:8], "big", signed=True)
/ 1000,
)
def encode_dump(
self,
) -> bytes:
"""将音乐序列转为二进制字节码"""
# 音乐名称长度 6 位 支持到 63
# 最小音量 minimum_volume 10 位 最大支持 1023 即三位小数
# 共 16 位 合 2 字节
# +++
# 总音调偏移 music_deviation 16 位 最大支持 -32768 ~ 32767 即 三位小数
# 共 16 位 合 2 字节
# +++
# 音乐名称 music_name 长度最多63 支持到 21 个中文字符 或 63 个西文字符
bytes_buffer = (
b"MSQ#"
+ (
(len(r := self.music_name.encode("utf-8")) << 10)
+ round(self.minimum_volume * 1000)
).to_bytes(2, "big")
+ round(self.music_deviation * 1000).to_bytes(2, "big", signed=True)
+ r
)
for channel_index, note_list in self.channels.items():
bytes_buffer += len(note_list).to_bytes(4, "big")
for note_ in note_list:
bytes_buffer += note_.encode()
return bytes_buffer

View File

@ -1,483 +0,0 @@
# -*- coding: utf-8 -*-
"""
旧版本转换功能以及已经弃用的函数
"""
"""
版权所有 © 2024 · 开发者
Copyright © 2024 all the developers of Musicreater
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory
"""
# 睿乐组织 开发交流群 861684859
# Email TriM-Organization@hotmail.com
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
from .exceptions import *
from .main import MidiConvert, mido
from .subclass import *
from .types import ChannelType
from .utils import *
from .constants import *
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",
MaxVolume: float = 1.0,
speed: float = 1.0,
) -> list:
"""
使用Dislink Sforza的转换思路将midi转换为我的世界命令列表
:param scoreboard_name: 我的世界的计分板名称
:param speed: 速度注意这里的速度指的是播放倍率其原理为在播放音频的时候每个音符的播放时间除以 speed
:return: tuple(命令列表, 命令个数, 计分板最大值)
"""
# :param volume: 音量,注意:这里的音量范围为(0,1],如果超出将被处理为正确值,其原理为在距离玩家 (1 / volume -1) 的地方播放音频
tracks = []
if speed == 0:
raise ZeroSpeedError("播放速度仅可为正实数")
if not self.midi:
raise MidiUnboundError(
"你是否正在使用的是一个由 copy_important 生成的MidiConvert对象这是不可复用的。"
)
MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume)
commands = 0
maxscore = 0
tempo = mido.midifiles.midifiles.DEFAULT_TEMPO
# 分轨的思路其实并不好,但这个算法就是这样
# 所以我建议用第二个方法 _toCmdList_m2
for i, track in enumerate(self.midi.tracks):
ticks = 0
instrumentID = 0
singleTrack = []
for msg in track:
ticks += msg.time
if msg.is_meta:
if msg.type == "set_tempo":
tempo = msg.tempo
else:
if msg.type == "program_change":
instrumentID = msg.program
if msg.type == "note_on" and msg.velocity != 0:
nowscore = round(
(ticks * tempo)
/ ((self.midi.ticks_per_beat * float(speed)) * 50000)
)
maxscore = max(maxscore, nowscore)
if msg.channel == 9:
soundID, _X = inst_to_sould_with_deviation(
instrumentID, MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE
)
else:
soundID, _X = inst_to_sould_with_deviation(
instrumentID, MM_CLASSIC_PITCHED_INSTRUMENT_TABLE
)
singleTrack.append(
"execute @a[scores={"
+ str(scoreboard_name)
+ "="
+ str(nowscore)
+ "}"
+ f"] ~ ~ ~ playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg.velocity / 128} "
f"{2 ** ((msg.note - 60 - _X) / 12)}"
)
commands += 1
if len(singleTrack) != 0:
tracks.append(singleTrack)
return [tracks, commands, maxscore]
def _toCmdList_m1(
self, scoreboardname: str = "mscplay", volume: float = 1.0, speed: float = 1.0
) -> list:
"""
使用Dislink Sforza的转换思路将midi转换为我的世界命令列表
:param scoreboardname: 我的世界的计分板名称
:param volume: 音量注意这里的音量范围为(0,1]如果超出将被处理为正确值其原理为在距离玩家 (1 / volume -1) 的地方播放音频
:param speed: 速度注意这里的速度指的是播放倍率其原理为在播放音频的时候每个音符的播放时间除以 speed
:return: tuple(命令列表, 命令个数, 计分板最大值)
"""
tracks = []
if volume > 1:
volume = 1
if volume <= 0:
volume = 0.001
commands = 0
maxscore = 0
for i, track in enumerate(self.midi.tracks): # type:ignore
ticks = 0
instrumentID = 0
singleTrack = []
for msg in track:
ticks += msg.time
# print(msg)
if msg.is_meta:
if msg.type == "set_tempo":
tempo = msg.tempo
else:
if msg.type == "program_change":
# print("TT")
instrumentID = msg.program
if msg.type == "note_on" and msg.velocity != 0:
nowscore = round(
(ticks * tempo) / ((self.midi.ticks_per_beat * float(speed)) * 50000) # type: ignore
)
maxscore = max(maxscore, nowscore)
if msg.channel == 9:
soundID, _X = inst_to_sould_with_deviation(
instrumentID, MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE
)
else:
soundID, _X = inst_to_sould_with_deviation(
instrumentID, MM_CLASSIC_PITCHED_INSTRUMENT_TABLE
)
singleTrack.append(
"execute @a[scores={"
+ str(scoreboardname)
+ "="
+ str(nowscore)
+ "}"
+ f"] ~ ~ ~ playsound {soundID} @s ~ ~{1 / volume - 1} ~ {msg.velocity * (0.7 if msg.channel == 0 else 0.9)} {2 ** ((msg.note - 60 - _X) / 12)}"
)
commands += 1
if len(singleTrack) != 0:
tracks.append(singleTrack)
return [tracks, commands, maxscore]
# 原本这个算法的转换效果应该和上面的算法相似的
def _toCmdList_m2(
self,
scoreboard_name: str = "mscplay",
MaxVolume: float = 1.0,
speed: float = 1.0,
) -> tuple:
"""
使用神羽和金羿的转换思路将midi转换为我的世界命令列表
:param scoreboard_name: 我的世界的计分板名称
:param MaxVolume: 音量注意这里的音量范围为(0,1]如果超出将被处理为正确值其原理为在距离玩家 (1 / volume -1) 的地方播放音频
:param speed: 速度注意这里的速度指的是播放倍率其原理为在播放音频的时候每个音符的播放时间除以 speed
:return: tuple(命令列表, 命令个数, 计分板最大值)
"""
if speed == 0:
raise ZeroSpeedError("播放速度仅可为正实数")
MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume)
tracks = []
cmdAmount = 0
maxScore = 0
InstID = -1
self.to_music_channels()
# 此处 我们把通道视为音轨
for i in self.channels.keys():
# 如果当前通道为空 则跳过
if not self.channels[i]:
continue
if i == 9:
SpecialBits = True
else:
SpecialBits = False
nowTrack = []
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 = (
inst_to_sould_with_deviation(
msg[1], MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE
)
if SpecialBits
else inst_to_sould_with_deviation(
InstID, MM_CLASSIC_PITCHED_INSTRUMENT_TABLE
)
)
score_now = round(msg[-1] / float(speed) / 50)
maxScore = max(maxScore, score_now)
nowTrack.append(
self.execute_cmd_head.format(
"@a[scores=({}={})]".format(scoreboard_name, score_now)
.replace("(", r"{")
.replace(")", r"}")
)
+ f"playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg[2] / 128} "
f"{2 ** ((msg[1] - 60 - _X) / 12)}"
)
cmdAmount += 1
if nowTrack:
tracks.append(nowTrack)
return tracks, cmdAmount, maxScore
def _toCmdList_withDelay_m1(
self,
MaxVolume: float = 1.0,
speed: float = 1.0,
player: str = "@a",
) -> list:
"""
使用Dislink Sforza的转换思路将midi转换为我的世界命令列表并输出每个音符之后的延迟
:param MaxVolume: 最大播放音量注意这里的音量范围为(0,1]如果超出将被处理为正确值其原理为在距离玩家 (1 / volume -1) 的地方播放音频
:param speed: 速度注意这里的速度指的是播放倍率其原理为在播放音频的时候每个音符的播放时间除以 speed
:param player: 玩家选择器默认为`@a`
:return: 全部指令列表[ ( str指令, int距离上一个指令的延迟 ),...]
"""
tracks = {}
if speed == 0:
raise ZeroSpeedError("播放速度仅可为正实数")
if not self.midi:
raise MidiUnboundError(
"你是否正在使用的是一个由 copy_important 生成的MidiConvert对象这是不可复用的。"
)
MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume)
tempo = mido.midifiles.midifiles.DEFAULT_TEMPO
for i, track in enumerate(self.midi.tracks):
instrumentID = 0
ticks = 0
for msg in track:
ticks += msg.time
if msg.is_meta:
if msg.type == "set_tempo":
tempo = msg.tempo
else:
if msg.type == "program_change":
instrumentID = msg.program
if msg.type == "note_on" and msg.velocity != 0:
now_tick = round(
(ticks * tempo)
/ ((self.midi.ticks_per_beat * float(speed)) * 50000)
)
if msg.channel == 9:
soundID, _X = inst_to_sould_with_deviation(
instrumentID, MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE
)
else:
soundID, _X = inst_to_sould_with_deviation(
instrumentID, MM_CLASSIC_PITCHED_INSTRUMENT_TABLE
)
try:
tracks[now_tick].append(
self.execute_cmd_head.format(player)
+ f"playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg.velocity / 128} "
f"{2 ** ((msg.note - 60 - _X) / 12)}"
)
except KeyError:
tracks[now_tick] = [
self.execute_cmd_head.format(player)
+ f"playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg.velocity / 128} "
f"{2 ** ((msg.note - 60 - _X) / 12)}"
]
results = []
all_ticks = list(tracks.keys())
all_ticks.sort()
for i in range(len(all_ticks)):
if i != 0:
for j in range(len(tracks[all_ticks[i]])):
if j != 0:
results.append((tracks[all_ticks[i]][j], 0))
else:
results.append(
(tracks[all_ticks[i]][j], all_ticks[i] - all_ticks[i - 1])
)
else:
for j in range(len(tracks[all_ticks[i]])):
results.append((tracks[all_ticks[i]][j], all_ticks[i]))
return [results, max(all_ticks)]
def _toCmdList_withDelay_m2(
self,
MaxVolume: float = 1.0,
speed: float = 1.0,
player: str = "@a",
) -> list:
"""
使用神羽和金羿的转换思路将midi转换为我的世界命令列表并输出每个音符之后的延迟
:param MaxVolume: 最大播放音量注意这里的音量范围为(0,1]如果超出将被处理为正确值其原理为在距离玩家 (1 / volume -1) 的地方播放音频
:param speed: 速度注意这里的速度指的是播放倍率其原理为在播放音频的时候每个音符的播放时间除以 speed
:param player: 玩家选择器默认为`@a`
:return: 全部指令列表[ ( str指令, int距离上一个指令的延迟 ),...]
"""
tracks = {}
if speed == 0:
raise ZeroSpeedError("播放速度仅可为正实数")
MaxVolume = 1 if MaxVolume > 1 else (0.001 if MaxVolume <= 0 else MaxVolume)
InstID = -1
self.to_music_channels()
results = []
for i in self.channels.keys():
# 如果当前通道为空 则跳过
if not self.channels[i]:
continue
if i == 9:
SpecialBits = True
else:
SpecialBits = False
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 = (
inst_to_sould_with_deviation(
msg[1], MM_CLASSIC_PERCUSSION_INSTRUMENT_TABLE
)
if SpecialBits
else inst_to_sould_with_deviation(
InstID, MM_CLASSIC_PITCHED_INSTRUMENT_TABLE
)
)
score_now = round(msg[-1] / float(speed) / 50)
try:
tracks[score_now].append(
self.execute_cmd_head.format(player)
+ f"playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg[2] / 128} "
f"{2 ** ((msg[1] - 60 - _X) / 12)}"
)
except KeyError:
tracks[score_now] = [
self.execute_cmd_head.format(player)
+ f"playsound {soundID} @s ^ ^ ^{1 / MaxVolume - 1} {msg[2] / 128} "
f"{2 ** ((msg[1] - 60 - _X) / 12)}"
]
all_ticks = list(tracks.keys())
all_ticks.sort()
for i in range(len(all_ticks)):
for j in range(len(tracks[all_ticks[i]])):
results.append(
(
tracks[all_ticks[i]][j],
(
0
if j != 0
else (
all_ticks[i] - all_ticks[i - 1]
if i != 0
else all_ticks[i]
)
),
)
)
return [results, max(all_ticks)]

View File

@ -104,10 +104,124 @@ class MineNote:
self.extra_info = extra_information self.extra_info = extra_information
# @property @classmethod
# def get_mc_pitch(self,table: Dict[int, Tuple[str, int]]) -> float: def decode(cls, code_buffer: bytes):
# self.mc_sound_ID, _X = inst_to_sould_with_deviation(self.inst,table,"note.bd" if self.percussive else "note.flute",) """自字节码析出MineNote类"""
# return -1 if self.percussive else 2 ** ((self.note - 60 - _X) / 12) group_1 = int.from_bytes(code_buffer[:6], "big")
percussive_ = bool(group_1 & 0b1)
duration_ = (group_1 := group_1 >> 1) & 0b11111111111111111
start_tick_ = (group_1 := group_1 >> 17) & 0b11111111111111111
note_pitch_ = (group_1 := group_1 >> 17) & 0b1111111
sound_name_length = group_1 >> 7
if code_buffer[6] & 0b1:
position_displacement_ = (
int.from_bytes(
code_buffer[8 + sound_name_length : 10 + sound_name_length],
"big",
)
/ 1000,
int.from_bytes(
code_buffer[10 + sound_name_length : 12 + sound_name_length],
"big",
)
/ 1000,
int.from_bytes(
code_buffer[12 + sound_name_length : 14 + sound_name_length],
"big",
)
/ 1000,
)
else:
position_displacement_ = (0, 0, 0)
try:
return cls(
mc_sound_name=code_buffer[8 : 8 + sound_name_length].decode(
encoding="utf-8"
),
midi_pitch=note_pitch_,
midi_velocity=code_buffer[6] >> 1,
start_time=start_tick_,
last_time=duration_,
track_number=code_buffer[7],
is_percussion=percussive_,
displacement=position_displacement_,
)
except:
print(code_buffer, "\n", code_buffer[8 : 8 + sound_name_length])
raise
def encode(self, is_displacement_included: bool = True) -> bytes:
"""
将数据打包为字节码
:param is_displacement_included:`bool` 是否包含声像偏移数据默认为****
:return bytes 打包好的字节码
"""
# 字符串长度 6 位 支持到 63
# note_pitch 7 位 支持到 127
# start_tick 17 位 支持到 131071 即 109.22583 分钟 合 1.8204305 小时
# duration 17 位 支持到 131071 即 109.22583 分钟 合 1.8204305 小时
# percussive 长度 1 位 支持到 1
# 共 48 位 合 6 字节
# +++
# velocity 长度 7 位 支持到 127
# is_displacement_included 长度 1 位 支持到 1
# 共 8 位 合 1 字节
# +++
# track_no 长度 8 位 支持到 255 合 1 字节
# +++
# sound_name 长度最多63 支持到 21 个中文字符 或 63 个西文字符
# +++
# position_displacement 每个元素长 16 位 合 2 字节
# 共 48 位 合 6 字节 支持存储三位小数和两位整数,其值必须在 [0, 65.535] 之间
return (
(
(
(
(
(
(
(
(
len(
r := self.sound_name.encode(
encoding="utf-8"
)
)
<< 7
)
+ self.note_pitch
)
<< 17
)
+ self.start_tick
)
<< 17
)
+ self.duration
)
<< 1
)
+ self.percussive
).to_bytes(6, "big")
+ ((self.velocity << 1) + is_displacement_included).to_bytes(1, "big")
+ self.track_no.to_bytes(1, "big")
+ r
+ (
(
round(self.position_displacement[0] * 1000).to_bytes(2, "big")
+ round(self.position_displacement[1] * 1000).to_bytes(2, "big")
+ round(self.position_displacement[2] * 1000).to_bytes(2, "big")
)
if is_displacement_included
else b""
)
)
def set_info(self, sth: Any): def set_info(self, sth: Any):
"""设置附加信息""" """设置附加信息"""

View File

@ -42,7 +42,9 @@ Midi乐器对照表类型
""" """
FittingFunctionType = Callable[[float], float] FittingFunctionType = Callable[[float], float]
"""
声像偏移音量拟合函数类型
"""
ChannelType = Dict[ ChannelType = Dict[
int, int,

View File

@ -74,10 +74,12 @@ def inst_to_sould_with_deviation(
midi的乐器ID midi的乐器ID
reference_table: Dict[int, Tuple[str, int]] reference_table: Dict[int, Tuple[str, int]]
转换乐器参照表 转换乐器参照表
default_instrument: str
查无此乐器时的替换乐器
Returns Returns
------- -------
tuple(str我的世界乐器名, int转换算法中的X) tuple(str我的世界乐器名, int转换算法中的偏移量)
""" """
sound_id = midi_inst_to_mc_sound( sound_id = midi_inst_to_mc_sound(
instrumentID=instrumentID, instrumentID=instrumentID,
@ -106,6 +108,8 @@ def midi_inst_to_mc_sound(
midi的乐器ID midi的乐器ID
reference_table: Dict[int, Tuple[str, int]] reference_table: Dict[int, Tuple[str, int]]
转换乐器参照表 转换乐器参照表
default_instrument: str
查无此乐器时的替换乐器
Returns Returns
------- -------
@ -274,14 +278,14 @@ def midi_msgs_to_minenote(
mc_distance_volume = volume_processing_method_(velocity_) mc_distance_volume = volume_processing_method_(velocity_)
return MineNote( return MineNote(
mc_sound_ID, mc_sound_name=mc_sound_ID,
note_, midi_pitch=note_,
velocity_, midi_velocity=velocity_,
round(start_time_ / float(play_speed) / 50), start_time=round(start_time_ / float(play_speed) / 50),
round(duration_ / float(play_speed) / 50), last_time=round(duration_ / float(play_speed) / 50),
track_no_, track_number=track_no_,
percussive_, is_percussion=percussive_,
(0, mc_distance_volume, 0), displacement=(0, mc_distance_volume, 0),
) )