diff --git a/Musicreater/__init__.py b/Musicreater/__init__.py index df9b37a..1b83261 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.3.2" -__vername__ = "支持神羽资源包" +__version__ = "2.4.0" +__vername__ = "全景声支持、音量调节修复" __author__ = ( ("金羿", "Eilles"), ("诸葛亮与八卦阵", "bgArray"), @@ -42,7 +42,11 @@ __all__ = [ "ProgressBarStyle", # "TimeStamp", 未来功能 # 默认值 + "MIDI_PROGRAM", + "MIDI_VOLUME", + "MIDI_PAN", "MIDI_DEFAULT_PROGRAM_VALUE", + "MIDI_DEFAULT_VOLUME_VALUE", "DEFAULT_PROGRESSBAR_STYLE", "MM_INSTRUMENT_RANGE_TABLE", "MM_CLASSIC_PITCHED_INSTRUMENT_TABLE", @@ -56,6 +60,8 @@ __all__ = [ # 操作性函数 "natural_curve", "straight_line", + "panning_2_rotation_linear", + "panning_2_rotation_trigonometric", "load_decode_musicsequence_metainfo", "load_decode_msq_flush_release", "load_decode_fsq_flush_release", diff --git a/Musicreater/constants.py b/Musicreater/constants.py index 294ef69..327058c 100644 --- a/Musicreater/constants.py +++ b/Musicreater/constants.py @@ -34,9 +34,22 @@ z = "z" z """ +MIDI_PROGRAM = "program" +"""Midi乐器编号""" + +MIDI_VOLUME = "volume" +"""Midi通道音量""" + +MIDI_PAN = "pan" +"""Midi通道立体声场偏移""" + # Midi用对照表 +MIDI_DEFAULT_VOLUME_VALUE: int = ( + 64 # Midi默认音量,当用户未指定时,默认使用折中默认音量 +) + MIDI_DEFAULT_PROGRAM_VALUE: int = ( 74 # 当 Midi 本身与用户皆未指定音色时,默认 Flute 长笛 ) diff --git a/Musicreater/experiment.py b/Musicreater/experiment.py index 9f01202..42e1bf7 100644 --- a/Musicreater/experiment.py +++ b/Musicreater/experiment.py @@ -27,6 +27,8 @@ from .main import ( MidiConvert, mido, ) + +from .constants import MIDI_PAN, MIDI_PROGRAM, MIDI_VOLUME from .subclass import * from .types import ChannelType, FittingFunctionType from .utils import * @@ -36,16 +38,19 @@ class FutureMidiConvertKamiRES(MidiConvert): """ 神羽资源包之测试支持 """ + @staticmethod def to_music_note_channels( midi: mido.MidiFile, ignore_mismatch_error: bool = True, speed: float = 1.0, default_program_value: int = -1, + default_volume_value: int = 64, default_tempo_value: int = mido.midifiles.midifiles.DEFAULT_TEMPO, pitched_note_rtable: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE, percussion_note_rtable: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE, vol_processing_function: FittingFunctionType = natural_curve, + pan_processing_function: FittingFunctionType = panning_2_rotation_trigonometric, note_rtable_replacement: Dict[str, str] = {}, ) -> Tuple[MineNoteChannelType, int, Dict[str, int]]: """ @@ -59,6 +64,8 @@ class FutureMidiConvertKamiRES(MidiConvert): 音乐播放速度倍数 default_program_value: int 默认的 MIDI 乐器值 + default_volume_value: int + 默认的通道音量值 default_tempo_value: int 默认的 MIDI TEMPO 值 pitched_note_rtable: Dict[int, Tuple[str, int]] @@ -66,7 +73,9 @@ class FutureMidiConvertKamiRES(MidiConvert): percussion_note_rtable: Dict[int, Tuple[str, int]] 打击乐器Midi-MC对照表 vol_processing_function: Callable[[float], float] - 声像偏移拟合函数 + 音量对播放距离的拟合函数 + pan_processing_function: Callable[[float], float] + 声像偏移对播放旋转角度的拟合函数 note_rtable_replacement: Dict[str, str] 音符名称替换表,此表用于对 Minecraft 乐器名称进行替换,而非 Midi Program 的替换 @@ -81,9 +90,15 @@ class FutureMidiConvertKamiRES(MidiConvert): # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 midi_channels: MineNoteChannelType = empty_midi_channels(default_staff=[]) - channel_program: Dict[int, int] = empty_midi_channels( - default_staff=default_program_value + + channel_controler: Dict[int, Dict[str, int]] = empty_midi_channels( + default_staff={ + MIDI_PROGRAM: default_program_value, + MIDI_VOLUME: default_volume_value, + MIDI_PAN: 64, + } ) + tempo = default_tempo_value note_count = 0 note_count_per_instrument: Dict[str, int] = {} @@ -118,76 +133,84 @@ class FutureMidiConvertKamiRES(MidiConvert): # 简化 if msg.type == "set_tempo": tempo = msg.tempo - else: - if msg.type == "program_change": - channel_program[msg.channel] = msg.program + elif msg.type == "program_change": + channel_controler[msg.channel][MIDI_PROGRAM] = msg.program - elif msg.type == "note_on" and msg.velocity != 0: - note_queue_A[msg.channel].append( - (msg.note, channel_program[msg.channel]) + elif msg.is_cc(7): + channel_controler[msg.channel][MIDI_VOLUME] = msg.value + elif msg.is_cc(10): + channel_controler[msg.channel][MIDI_PAN] = msg.value + + 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].append((msg.velocity, microseconds)) + note_queue_B[msg.channel].remove((_velocity, _ms)) - elif (msg.type == "note_off") or ( - msg.type == "note_on" and msg.velocity == 0 - ): - 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]) + midi_channels[msg.channel].append( + that_note := midi_msgs_to_minenote_using_kami_respack( + inst_=( + msg.note + if msg.channel == 9 + else channel_controler[msg.channel][MIDI_PROGRAM] + ), + note_=( + channel_controler[msg.channel][MIDI_PROGRAM] + if msg.channel == 9 + else msg.note + ), + percussive_=(msg.channel == 9), + volume_=channel_controler[msg.channel][MIDI_VOLUME], + velocity_=_velocity, + panning_=channel_controler[msg.channel][MIDI_PAN], + start_time_=_ms, # 微秒 + duration_=microseconds - _ms, # 微秒 + play_speed=speed, + midi_reference_table=( + percussion_note_rtable + if msg.channel == 9 + else pitched_note_rtable + ), + volume_processing_method_=vol_processing_function, + panning_processing_method_=pan_processing_function, + note_table_replacement=note_rtable_replacement, ) - note_queue_B[msg.channel].remove((_velocity, _ms)) - - midi_channels[msg.channel].append( - that_note := midi_msgs_to_minenote_using_kami_respack( - inst_=( - msg.note - if msg.channel == 9 - else channel_program[msg.channel] - ), - note_=( - channel_program[msg.channel] - if msg.channel == 9 - else msg.note - ), - percussive_=(msg.channel == 9), - velocity_=_velocity, - start_time_=_ms, # 微秒 - duration_=microseconds - _ms, # 微秒 - play_speed=speed, - midi_reference_table=( - percussion_note_rtable - if msg.channel == 9 - else pitched_note_rtable - ), - volume_processing_method_=vol_processing_function, - note_table_replacement=note_rtable_replacement, - ) - ) - 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 + ) + note_count += 1 + if that_note.sound_name in note_count_per_instrument.keys(): + note_count_per_instrument[that_note.sound_name] += 1 else: - if ignore_mismatch_error: - print( - "[WARRING] MIDI格式错误 音符不匹配 {} 无法在上文中找到与之匹配的音符开音消息".format( - msg - ) - ) - else: - raise NoteOnOffMismatchError( - "当前的MIDI很可能有损坏之嫌……", - msg, - "无法在上文中找到与之匹配的音符开音消息。", + note_count_per_instrument[that_note.sound_name] = 1 + else: + if ignore_mismatch_error: + print( + "[WARRING] MIDI格式错误 音符不匹配 {} 无法在上文中找到与之匹配的音符开音消息".format( + msg ) + ) + else: + raise NoteOnOffMismatchError( + "当前的MIDI很可能有损坏之嫌……", + msg, + "无法在上文中找到与之匹配的音符开音消息。", + ) """整合后的音乐通道格式 每个通道包括若干消息元素其中逃不过这三种: @@ -214,7 +237,6 @@ class FutureMidiConvertKamiRES(MidiConvert): note_count_per_instrument, ) - def to_command_list_in_score( self, scoreboard_name: str = "mscplay", @@ -362,7 +384,6 @@ class FutureMidiConvertKamiRES(MidiConvert): return self.music_command_list, notes_list[-1].start_tick, max_multi + 1 - class FutureMidiConvertJavaE(MidiConvert): def form_java_progress_bar( diff --git a/Musicreater/main.py b/Musicreater/main.py index 8d89b81..2373175 100644 --- a/Musicreater/main.py +++ b/Musicreater/main.py @@ -164,11 +164,13 @@ class MusicSequence: mismatch_error_ignorance: bool = True, speed_multiplier: float = 1, default_midi_program: int = MIDI_DEFAULT_PROGRAM_VALUE, + default_midi_volume: int = MIDI_DEFAULT_VOLUME_VALUE, default_tempo: int = mido.midifiles.midifiles.DEFAULT_TEMPO, pitched_note_referance_table: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE, percussion_note_referance_table: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE, minimum_vol: float = 0.1, volume_processing_function: FittingFunctionType = natural_curve, + panning_processing_function: FittingFunctionType = panning_2_rotation_linear, deviation: float = 0, note_referance_table_replacement: Dict[str, str] = {}, ): @@ -177,7 +179,7 @@ class MusicSequence: Paramaters ========== - mido_file: mido.MidiFile 对象 + mido_file: mido.MidiFile 需要处理的midi对象 midi_music_name: str 音乐名称 @@ -186,7 +188,9 @@ class MusicSequence: speed_multiplier: float 音乐播放速度倍数 default_midi_program: int - 默认的MIDI Program值 + 默认的 MIDI Program值 + default_midi_volume: int + 默认的 MIDI 音量 default_tempo: int 默认的MIDI TEMPO值 pitched_note_referance_table: Dict[int, Tuple[str, int]] @@ -196,7 +200,9 @@ class MusicSequence: minimum_vol: float 播放的最小音量 应为 (0,1] 范围内的小数 volume_processing_function: Callable[[float], float] - 声像偏移拟合函数 + 音量对播放距离的拟合函数 + panning_processing_function: Callable[[float], float] + 声像偏移对播放旋转角度的拟合函数 deviation: float 全曲音调偏移值 note_referance_table_replacement: Dict[str, str] @@ -210,13 +216,15 @@ class MusicSequence: inst_note_count, ) = cls.to_music_note_channels( midi=mido_file, + ignore_mismatch_error=mismatch_error_ignorance, speed=speed_multiplier, + default_program_value=default_midi_program, + default_volume_value=default_midi_volume, + default_tempo_value=default_tempo, pitched_note_rtable=pitched_note_referance_table, percussion_note_rtable=percussion_note_referance_table, - default_program_value=default_midi_program, - default_tempo_value=default_tempo, vol_processing_function=volume_processing_function, - ignore_mismatch_error=mismatch_error_ignorance, + pan_processing_function=panning_processing_function, note_rtable_replacement=note_referance_table_replacement, ) else: @@ -239,7 +247,7 @@ class MusicSequence: verify: bool = True, ): """ - 从字节码导入音乐序列,目前支持 MSQ 第二、三版和 FSQ 第一版。 + 从字节码导入音乐序列,目前支持 MSQ 第二、三、四版和 FSQ 第一、二版。 Paramaters ========== @@ -250,7 +258,9 @@ class MusicSequence: """ - if bytes_buffer_in[:4] == b"MSQ!": + if bytes_buffer_in[:4] in (b"MSQ!", b"MSQ$"): + + note_format_v3 = bytes_buffer_in[0] == b"MSQ$" group_1 = int.from_bytes(bytes_buffer_in[4:6], "big", signed=False) group_2 = int.from_bytes(bytes_buffer_in[6:8], "big", signed=False) @@ -289,6 +299,11 @@ class MusicSequence: code_buffer=bytes_buffer_in[stt_index:end_index], is_high_time_precision=high_quantity, ) + if note_format_v3 + else decode_note_bytes_v2( + bytes_buffer_in[stt_index:end_index], + is_high_time_precision=high_quantity, + ) ) channel_note_count += 1 stt_index = end_index @@ -371,7 +386,9 @@ class MusicSequence: ), ) - elif bytes_buffer_in[:4] == b"FSQ!": + elif bytes_buffer_in[:4] in (b"FSQ!", b"FSQ$"): + + note_format_v3 = bytes_buffer_in[:4] == b"FSQ$" group_1 = int.from_bytes(bytes_buffer_in[4:6], "big", signed=False) group_2 = int.from_bytes(bytes_buffer_in[6:8], "big", signed=False) @@ -436,9 +453,16 @@ class MusicSequence: + high_quantity + (bytes_buffer_in[stt_index] >> 2) ) - _read_note = MineNote.decode( - code_buffer=bytes_buffer_in[stt_index:end_index], - is_high_time_precision=high_quantity, + _read_note = ( + MineNote.decode( + code_buffer=bytes_buffer_in[stt_index:end_index], + is_high_time_precision=high_quantity, + ) + if note_format_v3 + else decode_note_bytes_v2( + code_buffer_bytes=bytes_buffer_in[stt_index:end_index], + is_high_time_precision=high_quantity, + ) ) stt_index = end_index except Exception as _err: @@ -515,8 +539,8 @@ class MusicSequence: + (bytes_buffer_in[stt_index] >> 2) ) channels_[channel_index].append( - MineNote.decode( - code_buffer=bytes_buffer_in[stt_index:end_index], + decode_note_bytes_v2( + code_buffer_bytes=bytes_buffer_in[stt_index:end_index], is_high_time_precision=high_quantity, ) ) @@ -553,7 +577,7 @@ class MusicSequence: 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]) + decode_note_bytes_v1(bytes_buffer_in[stt_index:end_index]) ) stt_index = end_index except: @@ -631,8 +655,12 @@ class MusicSequence: # (已废弃) # 第二版 MSQ 的码头: MSQ@ 字串编码: GB18030 + # # 第三版 MSQ 的码头: MSQ! 字串编码: GB18030 大端字节序 # 第一版 FSQ 的码头: FSQ! + # 第四版 MSQ 和 第二版 FSQ 的码头分别为 MSQ$ 和 FSQ$ + # 其序列存储格式与第三版一致,但在每个音频的识别上做了调整 + # 音频内容的调整见 subclass.py # 音乐名称长度 6 位 支持到 63 # 最小音量 minimum_volume 10 位 最大支持 1023 即三位小数 @@ -786,11 +814,13 @@ class MusicSequence: midi: mido.MidiFile, ignore_mismatch_error: bool = True, speed: float = 1.0, - default_program_value: int = -1, + default_program_value: int = MIDI_DEFAULT_PROGRAM_VALUE, + default_volume_value: int = MIDI_DEFAULT_VOLUME_VALUE, default_tempo_value: int = mido.midifiles.midifiles.DEFAULT_TEMPO, pitched_note_rtable: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE, percussion_note_rtable: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE, vol_processing_function: FittingFunctionType = natural_curve, + pan_processing_function: FittingFunctionType = panning_2_rotation_trigonometric, note_rtable_replacement: Dict[str, str] = {}, ) -> Tuple[MineNoteChannelType, int, Dict[str, int]]: """ @@ -804,6 +834,8 @@ class MusicSequence: 音乐播放速度倍数 default_program_value: int 默认的 MIDI 乐器值 + default_volume_value: int + 默认的通道音量值 default_tempo_value: int 默认的 MIDI TEMPO 值 pitched_note_rtable: Dict[int, Tuple[str, int]] @@ -811,7 +843,9 @@ class MusicSequence: percussion_note_rtable: Dict[int, Tuple[str, int]] 打击乐器Midi-MC对照表 vol_processing_function: Callable[[float], float] - 声像偏移拟合函数 + 音量对播放距离的拟合函数 + pan_processing_function: Callable[[float], float] + 声像偏移对播放旋转角度的拟合函数 note_rtable_replacement: Dict[str, str] 音符名称替换表,此表用于对 Minecraft 乐器名称进行替换,而非 Midi Program 的替换 @@ -826,9 +860,15 @@ class MusicSequence: # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 midi_channels: MineNoteChannelType = empty_midi_channels(default_staff=[]) - channel_program: Dict[int, int] = empty_midi_channels( - default_staff=default_program_value + + channel_controler: Dict[int, Dict[str, int]] = empty_midi_channels( + default_staff={ + MIDI_PROGRAM: default_program_value, + MIDI_VOLUME: default_volume_value, + MIDI_PAN: 64, + } ) + tempo = default_tempo_value note_count = 0 note_count_per_instrument: Dict[str, int] = {} @@ -863,76 +903,84 @@ class MusicSequence: # 简化 if msg.type == "set_tempo": tempo = msg.tempo - else: - if msg.type == "program_change": - channel_program[msg.channel] = msg.program + elif msg.type == "program_change": + channel_controler[msg.channel][MIDI_PROGRAM] = msg.program - elif msg.type == "note_on" and msg.velocity != 0: - note_queue_A[msg.channel].append( - (msg.note, channel_program[msg.channel]) + elif msg.is_cc(7): + channel_controler[msg.channel][MIDI_VOLUME] = msg.value + elif msg.is_cc(10): + channel_controler[msg.channel][MIDI_PAN] = msg.value + + 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].append((msg.velocity, microseconds)) + note_queue_B[msg.channel].remove((_velocity, _ms)) - elif (msg.type == "note_off") or ( - msg.type == "note_on" and msg.velocity == 0 - ): - 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]) + midi_channels[msg.channel].append( + that_note := midi_msgs_to_minenote( + inst_=( + msg.note + if msg.channel == 9 + else channel_controler[msg.channel][MIDI_PROGRAM] + ), + note_=( + channel_controler[msg.channel][MIDI_PROGRAM] + if msg.channel == 9 + else msg.note + ), + percussive_=(msg.channel == 9), + volume_=channel_controler[msg.channel][MIDI_VOLUME], + velocity_=_velocity, + panning_=channel_controler[msg.channel][MIDI_PAN], + start_time_=_ms, # 微秒 + duration_=microseconds - _ms, # 微秒 + play_speed=speed, + midi_reference_table=( + percussion_note_rtable + if msg.channel == 9 + else pitched_note_rtable + ), + volume_processing_method_=vol_processing_function, + panning_processing_method_=pan_processing_function, + note_table_replacement=note_rtable_replacement, ) - note_queue_B[msg.channel].remove((_velocity, _ms)) - - midi_channels[msg.channel].append( - that_note := midi_msgs_to_minenote( - inst_=( - msg.note - if msg.channel == 9 - else channel_program[msg.channel] - ), - note_=( - channel_program[msg.channel] - if msg.channel == 9 - else msg.note - ), - velocity_=_velocity, - start_time_=_ms, # 微秒 - duration_=microseconds - _ms, # 微秒 - percussive_=(msg.channel == 9), - play_speed=speed, - midi_reference_table=( - percussion_note_rtable - if msg.channel == 9 - else pitched_note_rtable - ), - volume_processing_method_=vol_processing_function, - note_table_replacement=note_rtable_replacement, - ) - ) - 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 + ) + note_count += 1 + if that_note.sound_name in note_count_per_instrument.keys(): + note_count_per_instrument[that_note.sound_name] += 1 else: - if ignore_mismatch_error: - print( - "[WARRING] MIDI格式错误 音符不匹配 {} 无法在上文中找到与之匹配的音符开音消息".format( - msg - ) - ) - else: - raise NoteOnOffMismatchError( - "当前的MIDI很可能有损坏之嫌……", - msg, - "无法在上文中找到与之匹配的音符开音消息。", + note_count_per_instrument[that_note.sound_name] = 1 + else: + if ignore_mismatch_error: + print( + "[WARRING] MIDI格式错误 音符不匹配 {} 无法在上文中找到与之匹配的音符开音消息".format( + msg ) + ) + else: + raise NoteOnOffMismatchError( + "当前的MIDI很可能有损坏之嫌……", + msg, + "无法在上文中找到与之匹配的音符开音消息。", + ) """整合后的音乐通道格式 每个通道包括若干消息元素其中逃不过这三种: @@ -985,12 +1033,14 @@ class MidiConvert(MusicSequence): ignore_mismatch_error: bool = True, playment_speed: float = 1, default_midi_program_value: int = MIDI_DEFAULT_PROGRAM_VALUE, + default_midi_volume_value: int = MIDI_DEFAULT_VOLUME_VALUE, default_tempo_value: int = mido.midifiles.midifiles.DEFAULT_TEMPO, pitched_note_rtable: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE, percussion_note_rtable: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE, enable_old_exe_format: bool = False, minimum_volume: float = 0.1, vol_processing_function: FittingFunctionType = natural_curve, + pan_processing_function: FittingFunctionType = panning_2_rotation_trigonometric, pitch_deviation: float = 0, note_rtable_replacement: Dict[str, str] = {}, ): @@ -1009,6 +1059,8 @@ class MidiConvert(MusicSequence): 音乐播放速度倍数 default_midi_program_value: int 默认的 MIDI Program 值,当 Midi 文件没有指定 Program 值时,使用此值 + default_midi_volume_value: int + 默认的 MIDI 音量值,当 Midi 文件没有指定此值时,使用此值 default_tempo_value: int 默认的 MIDI TEMPO 值,同上理 pitched_note_rtable: Dict[int, Tuple[str, int]] @@ -1020,7 +1072,9 @@ class MidiConvert(MusicSequence): minimum_volume: float 最小播放音量 vol_processing_function: Callable[[float], float] - 声像偏移拟合函数 + 音量对播放距离的拟合函数 + pan_processing_function: Callable[[float], float] + 声像偏移对播放旋转角度的拟合函数 pitch_deviation: float 音调偏移量,手动指定全曲音调偏移量 note_rtable_replacement: Dict[str, str] @@ -1044,11 +1098,13 @@ class MidiConvert(MusicSequence): mismatch_error_ignorance=ignore_mismatch_error, speed_multiplier=playment_speed, default_midi_program=default_midi_program_value, + default_midi_volume=default_midi_volume_value, default_tempo=default_tempo_value, pitched_note_referance_table=pitched_note_rtable, percussion_note_referance_table=percussion_note_rtable, minimum_vol=minimum_volume, volume_processing_function=vol_processing_function, + panning_processing_function=pan_processing_function, deviation=pitch_deviation, note_referance_table_replacement=note_rtable_replacement, ) @@ -1060,12 +1116,14 @@ class MidiConvert(MusicSequence): mismatch_error_ignorance: bool = True, play_speed: float = 1, default_midi_program: int = MIDI_DEFAULT_PROGRAM_VALUE, + default_midi_volume: int = MIDI_DEFAULT_VOLUME_VALUE, default_tempo: int = mido.midifiles.midifiles.DEFAULT_TEMPO, pitched_note_table: MidiInstrumentTableType = MM_TOUCH_PITCHED_INSTRUMENT_TABLE, percussion_note_table: MidiInstrumentTableType = MM_TOUCH_PERCUSSION_INSTRUMENT_TABLE, old_exe_format: bool = False, min_volume: float = 0.1, vol_processing_func: FittingFunctionType = natural_curve, + pan_processing_func: FittingFunctionType = panning_2_rotation_linear, music_pitch_deviation: float = 0, note_table_replacement: Dict[str, str] = {}, ): @@ -1082,6 +1140,8 @@ class MidiConvert(MusicSequence): 音乐播放速度倍数 default_midi_program: int 默认的 MIDI Program 值,当 Midi 文件没有指定 Program 值时,使用此值 + default_midi_volume: int + 默认每个通道的音量值,当 Midi 文件没有指定音量值时,使用此值 default_tempo: int 默认的MIDI TEMPO值 pitched_note_table: Dict[int, Tuple[str, int]] @@ -1093,7 +1153,9 @@ class MidiConvert(MusicSequence): min_volume: float 最小播放音量 vol_processing_func: Callable[[float], float] - 声像偏移拟合函数 + 音量对播放距离的拟合函数 + pan_processing_func: Callable[[float], float] + 声像偏移对播放旋转角度的拟合函数 music_pitch_deviation: float 全曲音符的音调偏移量 note_table_replacement: Dict[str, str] @@ -1115,12 +1177,14 @@ class MidiConvert(MusicSequence): ignore_mismatch_error=mismatch_error_ignorance, playment_speed=play_speed, default_midi_program_value=default_midi_program, + default_midi_volume_value=default_midi_volume, default_tempo_value=default_tempo, pitched_note_rtable=pitched_note_table, percussion_note_rtable=percussion_note_table, enable_old_exe_format=old_exe_format, minimum_volume=min_volume, vol_processing_function=vol_processing_func, + pan_processing_function=pan_processing_func, pitch_deviation=music_pitch_deviation, note_rtable_replacement=note_table_replacement, ) diff --git a/Musicreater/subclass.py b/Musicreater/subclass.py index b61c3bb..d0642fe 100644 --- a/Musicreater/subclass.py +++ b/Musicreater/subclass.py @@ -16,9 +16,9 @@ Terms & Conditions: License.md in the root directory # Email TriM-Organization@hotmail.com # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md - +from math import sin, cos, asin, radians, degrees, sqrt, atan from dataclasses import dataclass -from typing import Optional, Any, List, Tuple, Union +from typing import Optional, Any, List, Tuple, Union, Dict from .constants import MC_PITCHED_INSTRUMENT_LIST @@ -34,7 +34,7 @@ class MineNote: """midi音高""" velocity: int - """响度(力度)""" + """力度""" start_tick: int """开始之时 命令刻""" @@ -48,8 +48,11 @@ class MineNote: percussive: bool """是否作为打击乐器启用""" - position_displacement: Tuple[float, float, float] - """声像位移""" + sound_distance: float + """声源距离""" + + sound_azimuth: Tuple[float, float] + """声源方位""" extra_info: Any """你觉得放什么好?""" @@ -63,19 +66,26 @@ class MineNote: last_time: int, mass_precision_time: int = 0, is_percussion: Optional[bool] = None, - displacement: Optional[Tuple[float, float, float]] = None, - extra_information: Optional[Any] = None, + distance: Optional[float] = None, + 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` 开始之时(命令刻) - 注:此处的时间是用从乐曲开始到当前的毫秒数 + 注:此处的时间是用从乐曲开始到当前的刻数 :param last_time:`int` 音符延续时间(命令刻) :param mass_precision_time:`int` 高精度的开始时间偏移量(1/1250秒) :param is_percussion:`bool` 是否作为打击乐器 - :param displacement:`tuple[int,int,int]` 声像位移 + :param distance: `float` 发声源距离玩家的距离(半径 `r`) + 注:距离越近,音量越高,默认为 0。此参数可以与音量成某种函数关系。 + :param azimuth:`tuple[float, float]` 声源方位 + 注:此参数为tuple,包含两个元素,分别表示: + `rV` 发声源在竖直(上下)轴上,从玩家视角正前方开始,向顺时针旋转的角度 + `rH` 发声源在水平(左右)轴上,从玩家视角正前方开始,向上(到达玩家正上方顶点后变为向下,以此类推的旋转)旋转的角度 :param extra_information:`Any` 附加信息""" self.sound_name: str = mc_sound_name """乐器ID""" @@ -97,13 +107,85 @@ class MineNote: ) """是否为打击乐器""" - self.position_displacement = ( - (0, 0, 0) if (displacement is None) else displacement + self.sound_distance = ( + (16 if distance > 16 else (distance if distance > 0 else 0)) + if distance + else 0 ) - """声像位移""" + """声源距离""" + + self.sound_azimuth = (azimuth[0] % 360, azimuth[1] % 360) if azimuth else (0, 0) + """声源方位""" self.extra_info = extra_information + @classmethod + def from_traditional( + cls, + mc_sound_name: str, + midi_pitch: Optional[int], + midi_velocity: int, + start_time: int, + last_time: int, + mass_precision_time: int = 0, + is_percussion: Optional[bool] = None, + 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` 开始之时(命令刻) + 注:此处的时间是用从乐曲开始到当前的刻数 + :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` 附加信息""" + + if displacement is None: + displacement = (0, 0, 0) + r = 0 + alpha_v = 0 + beta_h = 0 + else: + r = sqrt(displacement[0] ** 2 + displacement[1] ** 2 + displacement[2] ** 2) + if r == 0: + alpha_v = 0 + beta_h = 0 + else: + beta_h = round(degrees(asin(displacement[1] / r)), 8) + if displacement[2] == 0: + alpha_v = -90 if displacement[0] > 0 else 90 + else: + alpha_v = round( + degrees(atan(-displacement[0] / displacement[2])), 8 + ) + + return cls( + mc_sound_name=mc_sound_name, + midi_pitch=midi_pitch, + midi_velocity=midi_velocity, + start_time=start_time, + last_time=last_time, + mass_precision_time=mass_precision_time, + is_percussion=is_percussion, + distance=r, + azimuth=(alpha_v, beta_h), + extra_information=extra_information, + ) + + @property + def position_displacement(self) -> Tuple[float, float, float]: + """声像位移""" + dk1 = self.sound_distance * round(cos(radians(self.sound_azimuth[1])), 8) + return ( + -dk1 * round(sin(radians(self.sound_azimuth[0])), 8), + self.sound_distance * round(sin(radians(self.sound_azimuth[1])), 8), + dk1 * round(cos(radians(self.sound_azimuth[0])), 8), + ) + @classmethod def decode(cls, code_buffer: bytes, is_high_time_precision: bool = True): """自字节码析出MineNote类""" @@ -115,39 +197,25 @@ class MineNote: 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] - if is_high_time_precision - else code_buffer[7 + sound_name_length : 9 + sound_name_length] - ), - "big", - ) - / 1000, - int.from_bytes( - ( - code_buffer[10 + sound_name_length : 12 + sound_name_length] - if is_high_time_precision - else code_buffer[9 + sound_name_length : 11 + sound_name_length] - ), - "big", - ) - / 1000, - int.from_bytes( - ( - code_buffer[12 + sound_name_length : 14 + sound_name_length] - if is_high_time_precision - else code_buffer[ - 11 + sound_name_length : 13 + sound_name_length - ] - ), - "big", - ) - / 1000, + distance_ = ( + code_buffer[8 + sound_name_length] + if is_high_time_precision + else code_buffer[7 + sound_name_length] + ) / 15 + + group_2 = int.from_bytes( + ( + code_buffer[9 + sound_name_length : 14 + sound_name_length] + if is_high_time_precision + else code_buffer[8 + sound_name_length : 13 + sound_name_length] + ), + "big", ) + azimuth_ = ((group_2 >> 20) / 2912, (group_2 & 0xFFFFF) / 2912) + else: - position_displacement_ = (0, 0, 0) + distance_ = 0 + azimuth_ = (0, 0) try: return cls( @@ -164,7 +232,8 @@ class MineNote: last_time=duration_, mass_precision_time=code_buffer[7] if is_high_time_precision else 0, is_percussion=percussive_, - displacement=position_displacement_, + distance=distance_, + azimuth=azimuth_, ) except: print(code_buffer, "\n", o) @@ -182,6 +251,8 @@ class MineNote: :return bytes 打包好的字节码 """ + # MineNote 的字节码共有三个顺次版本分别如下 + # 字符串长度 6 位 支持到 63 # note_pitch 7 位 支持到 127 # start_tick 17 位 支持到 131071 即 109.22583 分钟 合 1.8204305 小时 @@ -202,8 +273,12 @@ class MineNote: # 第一版编码: UTF-8 # 第二版编码: GB18030 # +++ + # (在第三版中已废弃) # position_displacement 每个元素长 16 位 合 2 字节 # 共 48 位 合 6 字节 支持存储三位小数和两位整数,其值必须在 [0, 65.535] 之间 + # (在第三版中新增) + # sound_distance 8 位 支持到 255 即 16 格 合 1 字节(按值放大 15 倍存储,精度可达 1 / 15) + # sound_azimuth 每个元素长 20 位 共 40 位 合 5 字节。每个值放大 2912 倍存储,即支持到 360.08756868131866 度,精度同理 return ( ( @@ -245,9 +320,11 @@ class MineNote: + 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") + round(self.sound_distance * 15).to_bytes(1, "big") + + ( + (round(self.sound_azimuth[0] * 2912) << 20) + + round(self.sound_azimuth[1] * 2912) + ).to_bytes(5, "big") ) if is_displacement_included else b"" @@ -258,24 +335,32 @@ class MineNote: """设置附加信息""" self.extra_info = sth - def __str__(self, is_displacement: bool = False): - return "{}Note(Instrument = {}, {}Velocity = {}, StartTick = {}, Duration = {}{}{})".format( - "Percussive" if self.percussive else "", - self.sound_name, - "" if self.percussive else "NotePitch = {}, ".format(self.note_pitch), - self.velocity, - self.start_tick, - self.duration, - ( - ", PositionDisplacement = {}".format(self.position_displacement) - if is_displacement + def stringize( + self, include_displacement: bool = False, include_extra_data: bool = False + ) -> str: + return ( + "{}Note(Instrument = {}, {}Velocity = {}, StartTick = {}, Duration = {}{}".format( + "Percussive" if self.percussive else "", + self.sound_name, + "" if self.percussive else "NotePitch = {}, ".format(self.note_pitch), + self.velocity, + self.start_tick, + self.duration, + ) + + ( + ", SoundDistance = `r`{}, SoundAzimuth = (`αV`{}, `βH`{})".format( + self.sound_distance, *self.sound_azimuth + ) + if include_displacement else "" - ), + ) + + (", ExtraData = {}".format(self.extra_info) if include_extra_data else "") + + ")" ) def tuplize(self, is_displacement: bool = False): tuplized = self.__tuple__() - return tuplized[:-2] + ((tuplized[-1],) if is_displacement else ()) + return tuplized[:-2] + (tuplized[-2:] if is_displacement else ()) def __list__(self) -> List: return ( @@ -285,7 +370,8 @@ class MineNote: self.velocity, self.start_tick, self.duration, - self.position_displacement, + self.sound_distance, + self.sound_azimuth, ] if self.percussive else [ @@ -295,15 +381,16 @@ class MineNote: self.velocity, self.start_tick, self.duration, - self.position_displacement, + self.sound_distance, + self.sound_azimuth, ] ) def __tuple__( self, ) -> Union[ - Tuple[bool, str, int, int, int, int, Tuple[float, float, float]], - Tuple[bool, str, int, int, int, Tuple[float, float, float]], + Tuple[bool, str, int, int, int, int, float, Tuple[float, float]], + Tuple[bool, str, int, int, int, float, Tuple[float, float]], ]: return ( ( @@ -312,7 +399,8 @@ class MineNote: self.velocity, self.start_tick, self.duration, - self.position_displacement, + self.sound_distance, + self.sound_azimuth, ) if self.percussive else ( @@ -322,7 +410,8 @@ class MineNote: self.velocity, self.start_tick, self.duration, - self.position_displacement, + self.sound_distance, + self.sound_azimuth, ) ) @@ -334,7 +423,9 @@ class MineNote: "Velocity": self.velocity, "StartTick": self.start_tick, "Duration": self.duration, - "PositionDisplacement": self.position_displacement, + "SoundDistance": self.sound_distance, + "SoundAzimuth": self.sound_azimuth, + "ExtraData": self.extra_info, } if self.percussive else { @@ -344,7 +435,9 @@ class MineNote: "Velocity": self.velocity, "StartTick": self.start_tick, "Duration": self.duration, - "PositionDisplacement": self.position_displacement, + "SoundDistance": self.sound_distance, + "SoundAzimuth": self.sound_azimuth, + "ExtraData": self.extra_info, } ) diff --git a/Musicreater/types.py b/Musicreater/types.py index c2a8a07..6f3a86f 100644 --- a/Musicreater/types.py +++ b/Musicreater/types.py @@ -32,7 +32,7 @@ Midi乐器对照表类型 FittingFunctionType = Callable[[float], float] """ -声像偏移音量拟合函数类型 +拟合函数类型 """ ChannelType = Dict[ diff --git a/Musicreater/utils.py b/Musicreater/utils.py index 38edcef..d1b862d 100644 --- a/Musicreater/utils.py +++ b/Musicreater/utils.py @@ -42,7 +42,7 @@ from .constants import ( ) from .exceptions import MusicSequenceDecodeError from .subclass import MineNote, mctick2timestr -from .types import MidiInstrumentTableType, MineNoteChannelType +from .types import MidiInstrumentTableType, MineNoteChannelType, FittingFunctionType def empty_midi_channels( @@ -171,6 +171,47 @@ def straight_line(vol: float) -> float: return vol / -8 + 16 +def panning_2_rotation_linear(pan_: float) -> float: + """ + Midi 左右平衡偏移值线性转为声源旋转角度 + + Parameters + ---------- + pan_: int + Midi 左右平衡偏移值 + 注:此参数为int,范围从0到127,当为 64 时,声源居中 + + Returns + ------- + float + 声源旋转角度 + """ + return (pan_ - 64) * 90 / 63 + + +def panning_2_rotation_trigonometric(pan_: float) -> float: + """ + Midi 左右平衡偏移值,依照圆的声场定位,转为声源旋转角度 + + Parameters + ---------- + pan_: int + Midi 左右平衡偏移值 + 注:此参数为int,范围从0到127,当为 64 时,声源居中 + + Returns + ------- + float + 声源旋转角度 + """ + if pan_ <= 0: + return -90 + elif pan_ >= 127: + return 90 + else: + return math.degrees(math.acos((64 - pan_) / 63)) - 90 + + def minenote_to_command_paramaters( note_: MineNote, pitch_deviation: float = 0, @@ -252,12 +293,15 @@ def midi_msgs_to_minenote( inst_: int, # 乐器编号 note_: int, percussive_: bool, # 是否作为打击乐器启用 + volume_: int, velocity_: int, + panning_: int, start_time_: int, duration_: int, play_speed: float, midi_reference_table: MidiInstrumentTableType, - volume_processing_method_: Callable[[float], float], + volume_processing_method_: FittingFunctionType, + panning_processing_method_: FittingFunctionType, note_table_replacement: Dict[str, str] = {}, ) -> MineNote: """ @@ -265,12 +309,15 @@ def midi_msgs_to_minenote( :param inst_: int 乐器编号 :param note_: int 音高编号(音符编号) :param percussive_: bool 是否作为打击乐器启用 - :param velocity_: int 力度(响度) + :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_proccessing_method_: Callable[[float], float] 音量处理函数 + :param volume_processing_method_: Callable[[float], float] 音量处理函数 + :param panning_processing_method_: Callable[[float], float] 立体声相偏移处理函数 :param note_table_replacement: Dict[str, str] 音符替换表,定义 Minecraft 音符字串的替换 :return MineNote我的世界音符对象 @@ -281,8 +328,6 @@ def midi_msgs_to_minenote( "note.bd" if percussive_ else "note.flute", ) - mc_distance_volume = volume_processing_method_(velocity_) - return MineNote( mc_sound_name=note_table_replacement.get(mc_sound_ID, mc_sound_ID), midi_pitch=note_, @@ -291,7 +336,8 @@ def midi_msgs_to_minenote( last_time=round(duration_ / float(play_speed) / 50000), mass_precision_time=round((start_time_ / float(play_speed) - tk * 50000) / 800), is_percussion=percussive_, - displacement=(0, mc_distance_volume, 0), + distance=volume_processing_method_(volume_), + azimuth=(panning_processing_method_(panning_), 0), ) @@ -299,12 +345,15 @@ def midi_msgs_to_minenote_using_kami_respack( inst_: int, # 乐器编号 note_: int, percussive_: bool, # 是否作为打击乐器启用 + volume_: int, velocity_: int, + panning_: int, start_time_: int, duration_: int, play_speed: float, midi_reference_table: MidiInstrumentTableType, volume_processing_method_: Callable[[float], float], + panning_processing_method_: FittingFunctionType, note_table_replacement: Dict[str, str] = {}, ) -> MineNote: """ @@ -312,12 +361,15 @@ def midi_msgs_to_minenote_using_kami_respack( :param inst_: int 乐器编号 :param note_: int 音高编号(音符编号) :param percussive_: bool 是否作为打击乐器启用 - :param velocity_: int 力度(响度) + :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_proccessing_method_: Callable[[float], float] 音量处理函数 + :param volume_processing_method_: Callable[[float], float] 音量处理函数 + :param panning_processing_method_: Callable[[float], float] 立体声相偏移处理函数 :param note_table_replacement: Dict[str, str] 音符替换表,定义 Minecraft 音符字串的替换 :return MineNote我的世界音符对象 @@ -327,7 +379,9 @@ def midi_msgs_to_minenote_using_kami_respack( if not percussive_ and (0 <= inst_ <= 119): mc_sound_ID = "{}{}.{}".format( # inst_, "d" if duration_ < 500_000 else "c", note_ - inst_, "d", note_ + inst_, + "d", + note_, ) elif percussive_ and (27 <= inst_ <= 87): mc_sound_ID = "-1d.{}".format(inst_) @@ -339,8 +393,6 @@ def midi_msgs_to_minenote_using_kami_respack( "note.bd" if percussive_ else "note.flute", ) - mc_distance_volume = volume_processing_method_(velocity_) - return MineNote( mc_sound_name=note_table_replacement.get(mc_sound_ID, mc_sound_ID), midi_pitch=note_ if using_original else 1, @@ -349,11 +401,14 @@ def midi_msgs_to_minenote_using_kami_respack( last_time=round(duration_ / float(play_speed) / 50000), mass_precision_time=round((start_time_ / float(play_speed) - tk * 50000) / 800), is_percussion=percussive_, - displacement=(0, mc_distance_volume, 0), + distance=volume_processing_method_(volume_), + azimuth=(panning_processing_method_(panning_), 0), extra_information={ "USING_ORIGINAL_SOUND": using_original, # 判断 extra_information 中是否有 USING_ORIGINAL_SOUND 键是判断是否使用神羽资源包解析的一个显著方法 "INST_VALUE": note_ if percussive_ else inst_, "NOTE_VALUE": inst_ if percussive_ else note_, + "VOLUME_VALUE": volume_, + "PIN_VALUE": panning_, }, ) @@ -456,7 +511,7 @@ def soundID_to_blockID( def load_decode_musicsequence_metainfo( buffer_in: BinaryIO, -) -> Tuple[str, float, float, bool, int]: +) -> Tuple[str, float, float, bool, int, bool]: """ 以流的方式解码音乐序列元信息 @@ -468,10 +523,10 @@ def load_decode_musicsequence_metainfo( Returns ------- Tuple[str, float, float, bool, int] - 音乐名称,最小音量,音调偏移,是否启用高精度,最后的流指针位置 + 音乐名称,最小音量,音调偏移,是否启用高精度,最后的流指针位置,是否使用新的音符存储格式(MineNote第三版) """ - buffer_in.seek(4, 0) + note_format_v3 = buffer_in.read(4) in (b"MSQ$", b"FSQ$") group_1 = int.from_bytes(buffer_in.read(2), "big") group_2 = int.from_bytes(buffer_in.read(2), "big", signed=False) @@ -490,6 +545,7 @@ def load_decode_musicsequence_metainfo( ), bool(group_2 & 0b1000000000000000), stt_index + 8, + note_format_v3, ) @@ -497,6 +553,7 @@ def load_decode_fsq_flush_release( buffer_in: BinaryIO, starter_index: int, high_quantity_note: bool, + new_note_format: bool, ) -> Generator[MineNote, Any, None]: """ 以流的方式解码FSQ音乐序列的音符序列并流式返回 @@ -509,6 +566,8 @@ def load_decode_fsq_flush_release( 字节流中,音符序列的起始索引 high_quantity_note : bool 是否启用高精度音符解析 + new_note_format : bool + 是否启用新音符格式解析(MineNote第三版) Returns ------- @@ -540,9 +599,16 @@ def load_decode_fsq_flush_release( 12 + high_quantity_note + ((_first_byte := (buffer_in.read(1)))[0] >> 2) ) - yield MineNote.decode( - code_buffer=_first_byte + buffer_in.read(_note_bytes_length), - is_high_time_precision=high_quantity_note, + yield ( + MineNote.decode( + code_buffer=_first_byte + buffer_in.read(_note_bytes_length), + is_high_time_precision=high_quantity_note, + ) + if new_note_format + else decode_note_bytes_v2( + code_buffer_bytes=_first_byte + buffer_in.read(_note_bytes_length), + is_high_time_precision=high_quantity_note, + ) ) except Exception as _err: # print(bytes_buffer_in[stt_index:end_index]) @@ -557,6 +623,7 @@ def load_decode_msq_flush_release( buffer_in: BinaryIO, starter_index: int, high_quantity_note: bool, + new_note_format: bool, ) -> Generator[Tuple[int, MineNote], Any, None]: """以流的方式解码MSQ音乐序列的音符序列并流式返回 @@ -568,6 +635,8 @@ def load_decode_msq_flush_release( 字节流中,音符序列的起始索引 high_quantity_note : bool 是否启用高精度音符解析 + new_note_format : bool + 是否启用新音符格式解析(MineNote第三版) Returns ------- @@ -703,9 +772,18 @@ def load_decode_msq_flush_release( # print("读取音符字节串", _bytes_buffer_in[_stt_index:_end_index]) _read_in_note_list.append( ( - MineNote.decode( - code_buffer=_bytes_buffer_in[_stt_index:_end_index], - is_high_time_precision=high_quantity_note, + ( + MineNote.decode( + code_buffer=_bytes_buffer_in[_stt_index:_end_index], + is_high_time_precision=high_quantity_note, + ) + if new_note_format + else decode_note_bytes_v2( + code_buffer_bytes=_bytes_buffer_in[ + _stt_index:_end_index + ], + is_high_time_precision=high_quantity_note, + ) ), __channel_index, ) @@ -802,3 +880,128 @@ def guess_deviation( / total_instrument_count / total_note_count ) + + +# 延长支持用 + + +def decode_note_bytes_v1( + code_buffer_bytes: bytes, +) -> MineNote: + """使用第一版的 MineNote 字节码标准析出MineNote类""" + group_1 = int.from_bytes(code_buffer_bytes[: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_bytes[6] & 0b1: + position_displacement_ = ( + int.from_bytes( + code_buffer_bytes[8 + sound_name_length : 10 + sound_name_length], + "big", + ) + / 1000, + int.from_bytes( + code_buffer_bytes[10 + sound_name_length : 12 + sound_name_length], + "big", + ) + / 1000, + int.from_bytes( + code_buffer_bytes[12 + sound_name_length : 14 + sound_name_length], + "big", + ) + / 1000, + ) + else: + position_displacement_ = (0, 0, 0) + + try: + return MineNote.from_traditional( + mc_sound_name=code_buffer_bytes[8 : 8 + sound_name_length].decode( + encoding="utf-8" + ), + midi_pitch=note_pitch_, + midi_velocity=code_buffer_bytes[6] >> 1, + start_time=start_tick_, + last_time=duration_, + is_percussion=percussive_, + displacement=position_displacement_, + extra_information={"track_number": code_buffer_bytes[7]}, + ) + except: + print(code_buffer_bytes, "\n", code_buffer_bytes[8 : 8 + sound_name_length]) + raise + + +def decode_note_bytes_v2( + code_buffer_bytes: bytes, is_high_time_precision: bool = True +) -> MineNote: + """使用第二版的 MineNote 字节码标准析出MineNote类""" + group_1 = int.from_bytes(code_buffer_bytes[: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_bytes[6] & 0b1: + position_displacement_ = ( + int.from_bytes( + ( + code_buffer_bytes[8 + sound_name_length : 10 + sound_name_length] + if is_high_time_precision + else code_buffer_bytes[ + 7 + sound_name_length : 9 + sound_name_length + ] + ), + "big", + ) + / 1000, + int.from_bytes( + ( + code_buffer_bytes[10 + sound_name_length : 12 + sound_name_length] + if is_high_time_precision + else code_buffer_bytes[ + 9 + sound_name_length : 11 + sound_name_length + ] + ), + "big", + ) + / 1000, + int.from_bytes( + ( + code_buffer_bytes[12 + sound_name_length : 14 + sound_name_length] + if is_high_time_precision + else code_buffer_bytes[ + 11 + sound_name_length : 13 + sound_name_length + ] + ), + "big", + ) + / 1000, + ) + else: + position_displacement_ = (0, 0, 0) + + try: + return MineNote.from_traditional( + mc_sound_name=( + o := ( + code_buffer_bytes[8 : 8 + sound_name_length] + if is_high_time_precision + else code_buffer_bytes[7 : 7 + sound_name_length] + ) + ).decode(encoding="GB18030"), + midi_pitch=note_pitch_, + midi_velocity=code_buffer_bytes[6] >> 1, + start_time=start_tick_, + last_time=duration_, + mass_precision_time=code_buffer_bytes[7] if is_high_time_precision else 0, + is_percussion=percussive_, + displacement=position_displacement_, + ) + except: + print(code_buffer_bytes, "\n", o) + raise diff --git a/pyproject.toml b/pyproject.toml index 217a217..01288bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,3 +93,6 @@ source = "file" path = "Musicreater/__init__.py" + +[tool.pyright] + typeCheckingMode = "basic" diff --git a/test_fsq_opera.py b/test_fsq_opera.py index cacee93..e6f2a96 100644 --- a/test_fsq_opera.py +++ b/test_fsq_opera.py @@ -31,7 +31,7 @@ with open("test.fsq", "rb") as f: pprint(metas := load_decode_musicsequence_metainfo(f)) pprint("流式 FSQ 音符序列:") cnt = 0 - for i in load_decode_fsq_flush_release(f, metas[-1], metas[-2]): + for i in load_decode_fsq_flush_release(f, metas[-2], metas[-3], metas[-1]): pprint( i, ) diff --git a/test_future_kamires.py b/test_future_kamires.py index 4857b38..ac9f907 100644 --- a/test_future_kamires.py +++ b/test_future_kamires.py @@ -12,8 +12,8 @@ print( "乐器使用情况", ) -for name in set( - sorted( +for name in sorted( + set( [ n.split(".")[0].replace("c", "").replace("d", "") for n in msct.note_count_per_instrument.keys() diff --git a/test_msq_opera.py b/test_msq_opera.py index a4580ec..45f6588 100644 --- a/test_msq_opera.py +++ b/test_msq_opera.py @@ -30,5 +30,5 @@ with open("test.msq", "rb") as f: pprint("流式 MSQ 元数据:") pprint(metas := load_decode_musicsequence_metainfo(f)) pprint("流式 MSQ 音符序列:") - for i in load_decode_msq_flush_release(f, metas[-1], metas[-2]): + for i in load_decode_msq_flush_release(f, metas[-2], metas[-3], metas[-1]): pprint(i)