改进Channel的应用,减少内存占用,优化结构生成,提高MIDI兼容,修改API结构

This commit is contained in:
EillesWan 2023-10-02 18:24:09 +08:00
parent 9a580132e5
commit 72dfdfeb34
19 changed files with 485 additions and 330 deletions

View File

@ -17,8 +17,8 @@ Terms & Conditions: License.md in the root directory
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
__version__ = "1.5.2" __version__ = "1.6.0"
__vername__ = "添加midi对应的键盘符号表" __vername__ = "切换ChannelType类型为NoteChannelType类型"
__author__ = ( __author__ = (
("金羿", "Eilles Wan"), ("金羿", "Eilles Wan"),
("诸葛亮与八卦阵", "bgArray"), ("诸葛亮与八卦阵", "bgArray"),
@ -29,7 +29,7 @@ __all__ = [
# 主要类 # 主要类
"MidiConvert", "MidiConvert",
# 附加类 # 附加类
# "SingleNote", "SingleNote",
"SingleCommand", "SingleCommand",
# "TimeStamp", 未来功能 # "TimeStamp", 未来功能
] ]

View File

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Dict, List, Tuple
""" """
存放常量与数值性内容 存放常量与数值性内容
""" """
@ -18,6 +16,7 @@ Terms & Conditions: License.md in the root directory
# Email TriM-Organization@hotmail.com # Email TriM-Organization@hotmail.com
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
from typing import Dict, List, Tuple
x = "x" x = "x"
""" """
@ -368,7 +367,7 @@ PERCUSSION_INSTRUMENT_LIST: List[str] = [
"fire.ignite", "fire.ignite",
] ]
INSTRUMENT_BLOCKS_TABLE: Dict[str, Tuple[str]] = { INSTRUMENT_BLOCKS_TABLE: Dict[str, Tuple[str, ...]] = {
"note.bass": ("planks",), "note.bass": ("planks",),
"note.snare": ("sand",), "note.snare": ("sand",),
"note.hat": ("glass",), "note.hat": ("glass",),

View File

@ -24,7 +24,7 @@ class MSCTBaseException(Exception):
"""音·创库版本的所有错误均继承于此""" """音·创库版本的所有错误均继承于此"""
super().__init__(*args) super().__init__(*args)
def miao( def meow(
self, self,
): ):
for i in self.args: for i in self.args:
@ -66,12 +66,15 @@ class CommandFormatError(RuntimeError):
super().__init__("指令格式不匹配", *args) super().__init__("指令格式不匹配", *args)
class CrossNoteError(MidiFormatException): # class CrossNoteError(MidiFormatException):
"""同通道下同音符交叉出现所产生的错误""" # """同通道下同音符交叉出现所产生的错误"""
def __init__(self, *args): # def __init__(self, *args):
"""同通道下同音符交叉出现所产生的错误""" # """同通道下同音符交叉出现所产生的错误"""
super().__init__("同通道下同音符交叉", *args) # super().__init__("同通道下同音符交叉", *args)
# 这TM是什么错误
# 我什么时候写的这玩意?
# 我哪知道这说的是啥?
class NotDefineTempoError(MidiFormatException): class NotDefineTempoError(MidiFormatException):
@ -98,7 +101,15 @@ class NotDefineProgramError(MidiFormatException):
super().__init__("未指定演奏乐器", *args) super().__init__("未指定演奏乐器", *args)
class ZeroSpeedError(MidiFormatException): class NoteOnOffMismatchError(MidiFormatException):
"""音符开音和停止不匹配的错误"""
def __init__(self, *args):
"""音符开音和停止不匹配的错误"""
super().__init__("音符不匹配", *args)
class ZeroSpeedError(ZeroDivisionError):
"""以0作为播放速度的错误""" """以0作为播放速度的错误"""
def __init__(self, *args): def __init__(self, *args):

View File

@ -17,14 +17,13 @@ Terms & Conditions: License.md in the root directory
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
import random import random
from typing import Dict, List, Tuple, Union
from .constants import INSTRUMENT_BLOCKS_TABLE from .constants import INSTRUMENT_BLOCKS_TABLE
from .exceptions import * from .exceptions import *
from .main import MidiConvert from .main import MidiConvert
from .subclass import * from .subclass import *
from .utils import * from .utils import *
from .types import Tuple, List, Dict
class FutureMidiConvertRSNB(MidiConvert): class FutureMidiConvertRSNB(MidiConvert):
""" """

View File

@ -27,39 +27,13 @@ Terms & Conditions: License.md in the root directory
import math import math
import os import os
from typing import List, Literal, Tuple, Union
import mido
from .constants import * from .constants import *
from .exceptions import * from .exceptions import *
from .subclass import * from .subclass import *
from .types import *
from .utils import * from .utils import *
VoidMido = Union[mido.MidiFile, None] # void mido
"""
空Midi类类型
"""
ChannelType = Dict[
int,
Dict[
int,
List[
Union[
Tuple[Literal["PgmC"], int, int],
Tuple[Literal["NoteS"], int, int, int],
Tuple[Literal["NoteE"], int, int],
]
],
],
]
"""
以字典所标记的频道信息类型
Dict[int,Dict[int,List[Union[Tuple[Literal["PgmC"], int, int],Tuple[Literal["NoteS"], int, int, int],Tuple[Literal["NoteE"], int, int],]],],]
"""
""" """
学习笔记 学习笔记
tempo: microseconds per quarter note 毫秒每四分音符换句话说就是一拍占多少毫秒 tempo: microseconds per quarter note 毫秒每四分音符换句话说就是一拍占多少毫秒
@ -112,7 +86,7 @@ class MidiConvert:
execute_cmd_head: str execute_cmd_head: str
"""execute指令头部""" """execute指令头部"""
channels: ChannelType channels: Union[ChannelType, NoteChannelType]
"""频道信息字典""" """频道信息字典"""
music_command_list: List[SingleCommand] music_command_list: List[SingleCommand]
@ -195,57 +169,6 @@ class MidiConvert:
# ……真的那么重要吗 # ……真的那么重要吗
# 我又几曾何时,知道祂真的会抛下我 # 我又几曾何时,知道祂真的会抛下我
@staticmethod
def inst_to_souldID_withX(
instrumentID: int,
) -> Tuple[str, int]:
"""
返回midi的乐器ID对应的我的世界乐器名对于音域转换算法如下
2**( ( msg.note - 60 - X ) / 12 ) 即为MC的音高其中
X的取值随乐器不同而变化
竖琴harp电钢琴pling班卓琴banjo方波bit颤音琴iron_xylophone 的时候为6
吉他的时候为7
贝斯bass迪吉里杜管didgeridoo的时候为8
长笛flute牛铃cou_bell的时候为5
钟琴bell管钟chime木琴xylophone的时候为4
而存在一些打击乐器bd(basedrum)hatsnare没有音域则没有X那么我们返回7即可
Parameters
----------
instrumentID: int
midi的乐器ID
Returns
-------
tuple(str我的世界乐器名, int转换算法中的X)
"""
try:
return PITCHED_INSTRUMENT_TABLE[instrumentID]
except KeyError:
return "note.flute", 5
@staticmethod
def perc_inst_to_soundID_withX(instrumentID: int) -> Tuple[str, int]:
"""
对于Midi第10通道所对应的打击乐器返回我的世界乐器名
Parameters
----------
instrumentID: int
midi的乐器ID
Returns
-------
tuple(str我的世界乐器名, int转换算法中的X)
"""
try:
return PERCUSSION_INSTRUMENT_TABLE[instrumentID]
except KeyError:
return "note.bd", 7
# 明明已经走了
# 凭什么还要在我心里留下缠绵缱绻
def form_progress_bar( def form_progress_bar(
self, self,
max_score: int, max_score: int,
@ -522,24 +445,26 @@ class MidiConvert:
self.progress_bar_command = result self.progress_bar_command = result
return result return result
def to_music_channels( def to_music_note_channels(
self, self,
) -> ChannelType: ignore_mismatch_error: bool = True,
) -> NoteChannelType:
""" """
使用金羿的转换思路将midi解析并转换为频道信息字典 将midi解析并转换为频道音符字典
Returns Returns
------- -------
以频道作为分割的Midi信息字典: 以频道作为分割的Midi音符列表字典:
Dict[int,Dict[int,List[Union[Tuple[Literal["PgmC"], int, int],Tuple[Literal["NoteS"], int, int, int],Tuple[Literal["NoteE"], int, int],]],],] Dict[int,List[SingleNote,]]
""" """
if self.midi is None: if self.midi is None:
raise MidiUnboundError( raise MidiUnboundError(
"你是否正在使用的是一个由 copy_important 生成的MidiConvert对象这是不可复用的。" "你是否正在使用的是一个由 copy_important 生成的MidiConvert对象这是不可复用的。"
) )
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨 # 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
midi_channels: ChannelType = empty_midi_channels() midi_channels: NoteChannelType = empty_midi_channels(staff=[])
tempo = mido.midifiles.midifiles.DEFAULT_TEMPO tempo = mido.midifiles.midifiles.DEFAULT_TEMPO
# 我们来用通道统计音乐信息 # 我们来用通道统计音乐信息
@ -549,6 +474,27 @@ class MidiConvert:
if not track: if not track:
continue continue
note_queue_A: Dict[
int,
List[
Tuple[
int,
int,
]
],
] = empty_midi_channels(staff=[])
note_queue_B: Dict[
int,
List[
Tuple[
int,
int,
]
],
] = empty_midi_channels(staff=[])
channel_program: Dict[int, int] = empty_midi_channels(staff=-1)
for msg in track: for msg in track:
if msg.time != 0: if msg.time != 0:
microseconds += msg.time * tempo / self.midi.ticks_per_beat / 1000 microseconds += msg.time * tempo / self.midi.ticks_per_beat / 1000
@ -557,27 +503,57 @@ class MidiConvert:
if msg.type == "set_tempo": if msg.type == "set_tempo":
tempo = msg.tempo tempo = msg.tempo
else: 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": if msg.type == "program_change":
midi_channels[msg.channel][track_no].append( channel_program[msg.channel] = msg.program
("PgmC", msg.program, microseconds)
)
elif msg.type == "note_on" and msg.velocity != 0: elif msg.type == "note_on" and msg.velocity != 0:
midi_channels[msg.channel][track_no].append( note_queue_A[msg.channel].append(
("NoteS", msg.note, msg.velocity, microseconds) (msg.note, channel_program[msg.channel])
) )
note_queue_B[msg.channel].append((msg.velocity, microseconds))
elif (msg.type == "note_on" and msg.velocity == 0) or ( elif (msg.type == "note_off") or (
msg.type == "note_off" msg.type == "note_on" and msg.velocity == 0
): ):
midi_channels[msg.channel][track_no].append( if (msg.note, channel_program[msg.channel]) in note_queue_A[
("NoteE", msg.note, microseconds) msg.channel
]:
_velocity, _ms = note_queue_B[msg.channel][
note_queue_A[msg.channel].index(
(msg.note, channel_program[msg.channel])
)
]
note_queue_A[msg.channel].remove(
(msg.note, channel_program[msg.channel])
)
note_queue_B[msg.channel].remove((_velocity, _ms))
midi_channels[msg.channel].append(
SingleNote(
instrument=msg.note,
pitch=channel_program[msg.channel],
velocity=_velocity,
startime=_ms,
lastime=microseconds - _ms,
track_number=track_no,
is_percussion=True,
)
if msg.channel == 9
else SingleNote(
instrument=channel_program[msg.channel],
pitch=msg.note,
velocity=_velocity,
startime=_ms,
lastime=microseconds - _ms,
track_number=track_no,
is_percussion=False,
)
)
else:
if ignore_mismatch_error:
print("[WARRING] MIDI格式错误 音符不匹配 {} 无法在上文中找到与之匹配的音符开音消息".format(msg))
else:
raise NoteOnOffMismatchError(
"当前的MIDI很可能有损坏之嫌……", msg, "无法在上文中找到与之匹配的音符开音消息。"
) )
"""整合后的音乐通道格式 """整合后的音乐通道格式
@ -592,9 +568,14 @@ class MidiConvert:
3 音符结束消息 3 音符结束消息
("NoteE", 结束的音符ID, 距离演奏开始的毫秒)""" ("NoteE", 结束的音符ID, 距离演奏开始的毫秒)"""
del tempo, self.channels del tempo, self.channels
self.channels = midi_channels self.channels = dict(
# [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 (channel_no, sorted(channel_notes, key=lambda note: note.start_time))
for channel_no, channel_notes in midi_channels.items()
]
)
return self.channels
def to_command_list_in_score( def to_command_list_in_score(
self, self,
@ -623,75 +604,46 @@ class MidiConvert:
raise ZeroSpeedError("播放速度仅可为正实数") raise ZeroSpeedError("播放速度仅可为正实数")
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)
tracks = [] command_channels = []
cmdAmount = 0 command_amount = 0
maxScore = 0 max_score = 0
InstID = -1
self.to_music_channels()
# 此处 我们把通道视为音轨 # 此处 我们把通道视为音轨
for i in self.channels.keys(): for channel in self.to_music_note_channels().values():
# 如果当前通道为空 则跳过 # 如果当前通道为空 则跳过
if not self.channels[i]: if not channel:
continue continue
# 第十通道是打击乐通道 this_channel = []
SpecialBits = True if i == 9 else False
for track_no, track in self.channels[i].items(): for note in channel:
nowTrack = [] score_now = round(note.start_time / float(speed) / 50)
max_score = max(max_score, score_now)
for msg in track: this_channel.append(
if msg[0] == "PgmC":
InstID = msg[1]
elif msg[0] == "NoteS":
soundID, _X = (
self.perc_inst_to_soundID_withX(msg[1])
if SpecialBits
else self.inst_to_souldID_withX(InstID)
)
score_now = round(msg[-1] / float(speed) / 50)
maxScore = max(maxScore, score_now)
mc_pitch = "" if SpecialBits else 2 ** ((msg[1] - 60 - _X) / 12)
mc_distance_volume = 128 / max_volume / msg[2] + (
1 if SpecialBits else -1
)
nowTrack.append(
SingleCommand( SingleCommand(
self.execute_cmd_head.format( self.execute_cmd_head.format(
"@a[scores=({}={})]".format( "@a[scores=({}={})]".format(scoreboard_name, score_now)
scoreboard_name, score_now
)
.replace("(", r"{") .replace("(", r"{")
.replace(")", r"}") .replace(")", r"}")
) )
+ "playsound {} @s ^ ^ ^{} {} {}".format( + note.to_command(max_volume),
soundID,
mc_distance_volume,
msg[2] / 128,
mc_pitch,
),
annotation="{}播放{}%{}".format( annotation="{}播放{}%{}".format(
mctick2timestr(score_now), mctick2timestr(score_now),
max_volume * 100, max_volume * 100,
"{}:{}".format(soundID, mc_pitch), "{}:{}".format(note.mc_sound_ID, note.mc_pitch),
), ),
), ),
) )
cmdAmount += 1 command_amount += 1
if nowTrack: if this_channel:
self.music_command_list.extend(nowTrack) self.music_command_list.extend(this_channel)
tracks.append(nowTrack) command_channels.append(this_channel)
# print(cmdAmount) self.music_tick_num = max_score
del InstID return (command_channels, command_amount, max_score)
self.music_tick_num = maxScore
return (tracks, cmdAmount, maxScore)
def to_command_list_in_delay( def to_command_list_in_delay(
self, self,
@ -720,96 +672,40 @@ class MidiConvert:
raise ZeroSpeedError("播放速度仅可为正实数") raise ZeroSpeedError("播放速度仅可为正实数")
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.to_music_channels() notes_list: List[SingleNote] = []
tracks = {}
InstID = -1
# cmd_amount = 0
# 此处 我们把通道视为音轨 # 此处 我们把通道视为音轨
for i in self.channels.keys(): for channel in self.to_music_note_channels().values():
# 如果当前通道为空 则跳过 notes_list.extend(channel)
if not self.channels[i]:
continue
# 第十通道是打击乐通道 notes_list.sort(key=lambda a: a.start_time)
SpecialBits = True if i == 9 else False self.music_command_list = []
multi = max_multi = 0
delaytime_previous = 0
# nowChannel = [] for note in notes_list:
delaytime_now = round(note.start_time / speed / 50)
for track_no, track in self.channels[i].items(): if (tickdelay := (delaytime_now - delaytime_previous)) == 0:
for msg in track: multi += 1
if msg[0] == "PgmC": else:
InstID = msg[1] max_multi = max(max_multi, multi)
multi = 0
elif msg[0] == "NoteS": self.music_command_list.append(
soundID, _X = (
self.perc_inst_to_soundID_withX(msg[1])
if SpecialBits
else self.inst_to_souldID_withX(InstID)
)
delaytime_now = round(msg[-1] / float(speed) / 50)
mc_pitch = "" if SpecialBits else 2 ** ((msg[1] - 60 - _X) / 12)
mc_distance_volume = 128 / max_volume / msg[2] + (
1 if SpecialBits else -1
)
try:
tracks[delaytime_now].append(
self.execute_cmd_head.format(player_selector)
+ "playsound {} @s ^ ^ ^{} {} {}".format(
soundID,
mc_distance_volume,
msg[2] / 128,
mc_pitch,
)
)
except KeyError:
tracks[delaytime_now] = [
self.execute_cmd_head.format(player_selector)
+ "playsound {} @s ^ ^ ^{} {} {}".format(
soundID,
mc_distance_volume,
msg[2] / 128,
mc_pitch,
)
]
# cmd_amount += 1
# print(cmd_amount)
del InstID
all_ticks = list(tracks.keys())
all_ticks.sort()
results = []
max_multi = 0
for i in range(len(all_ticks)):
max_multi = max(max_multi, len(tracks[all_ticks[i]]))
for j in range(len(tracks[all_ticks[i]])):
results.append(
SingleCommand( SingleCommand(
tracks[all_ticks[i]][j], self.execute_cmd_head.format(player_selector)
tick_delay=( + note.to_command(max_volume),
0 tick_delay=tickdelay,
if j != 0
else (
all_ticks[i] - all_ticks[i - 1]
if i != 0
else all_ticks[i]
)
),
annotation="{}播放{}%{}".format( annotation="{}播放{}%{}".format(
mctick2timestr(i), max_volume * 100, "" mctick2timestr(delaytime_now),
max_volume * 100,
"{}:{}".format(note.mc_sound_ID, note.mc_pitch),
), ),
) )
) )
delaytime_previous = delaytime_now
self.music_command_list = results self.music_tick_num = round(notes_list[-1].start_time / speed / 50)
self.music_tick_num = max(all_ticks) return self.music_command_list, self.music_tick_num, max_multi + 1
return results, self.music_tick_num, max_multi
def copy_important(self): def copy_important(self):
dst = MidiConvert( dst = MidiConvert(

View File

@ -22,11 +22,11 @@ from ...main import MidiConvert
from ..archive import behavior_mcpack_manifest, compress_zipfile from ..archive import behavior_mcpack_manifest, compress_zipfile
from ..main import ConvertConfig from ..main import ConvertConfig
from ..mcstructure import ( from ..mcstructure import (
commands_to_structure,
form_command_block_in_NBT_struct,
commands_to_redstone_delay_structure,
COMPABILITY_VERSION_117, COMPABILITY_VERSION_117,
COMPABILITY_VERSION_119, COMPABILITY_VERSION_119,
commands_to_redstone_delay_structure,
commands_to_structure,
form_command_block_in_NBT_struct,
) )
@ -393,7 +393,7 @@ def to_addon_pack_in_repeater(
with open(f"{data_cfg.dist_path}/temp/manifest.json", "w", encoding="utf-8") as f: with open(f"{data_cfg.dist_path}/temp/manifest.json", "w", encoding="utf-8") as f:
json.dump( json.dump(
behavior_mcpack_manifest( behavior_mcpack_manifest(
pack_description=f"{midi_cvt.midi_music_name} 音乐播放包MCSTRUCTURE(MCPACK) 延迟播放器 - 由 音·创 生成", pack_description=f"{midi_cvt.midi_music_name} 音乐播放包MCSTRUCTURE(MCPACK) 中继器播放器 - 由 音·创 生成",
pack_name=midi_cvt.midi_music_name + "播放", pack_name=midi_cvt.midi_music_name + "播放",
modules_description=f"无 - 由 音·创 生成", modules_description=f"无 - 由 音·创 生成",
), ),

View File

@ -17,11 +17,11 @@ Terms & Conditions: License.md in the root directory
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
import datetime
import os import os
import uuid import uuid
import zipfile import zipfile
import datetime from typing import List, Literal, Union
from typing import List, Union, Literal
def compress_zipfile(sourceDir, outFilename, compression=8, exceptFile=None): def compress_zipfile(sourceDir, outFilename, compression=8, exceptFile=None):

View File

@ -14,11 +14,7 @@ Terms & Conditions: License.md in the root directory
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
__all__ = [ __all__ = ["to_BDX_file_in_score", "to_BDX_file_in_delay"]
"to_BDX_file_in_score",
"to_BDX_file_in_delay"
]
__author__ = (("金羿", "Eilles Wan"),) __author__ = (("金羿", "Eilles Wan"),)
from .main import to_BDX_file_in_delay, to_BDX_file_in_score from .main import to_BDX_file_in_delay, to_BDX_file_in_score

View File

@ -17,7 +17,7 @@ Terms & Conditions: License.md in the root directory
from dataclasses import dataclass from dataclasses import dataclass
from typing import Tuple, Union, Literal from typing import Literal, Tuple, Union
from ..constants import DEFAULT_PROGRESSBAR_STYLE from ..constants import DEFAULT_PROGRESSBAR_STYLE

View File

@ -21,4 +21,3 @@ __all__ = [
__author__ = (("金羿", "Eilles Wan"),) __author__ = (("金羿", "Eilles Wan"),)
from .main import to_mcstructure_file_in_delay, to_mcstructure_file_in_repeater from .main import to_mcstructure_file_in_delay, to_mcstructure_file_in_repeater

View File

@ -12,17 +12,16 @@ Terms & Conditions: License.md in the root directory
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
import os import os
from typing import Literal from typing import Literal
from ...exceptions import CommandFormatError from ...exceptions import CommandFormatError
from ...main import MidiConvert from ...main import MidiConvert
from ..main import ConvertConfig from ..main import ConvertConfig
from ..mcstructure import ( from ..mcstructure import (
commands_to_structure,
commands_to_redstone_delay_structure,
COMPABILITY_VERSION_119,
COMPABILITY_VERSION_117, COMPABILITY_VERSION_117,
COMPABILITY_VERSION_119,
commands_to_redstone_delay_structure,
commands_to_structure,
) )

View File

@ -351,23 +351,26 @@ def commands_to_redstone_delay_structure(
command_actually_length = sum([int(bool(cmd.delay)) for cmd in commands]) command_actually_length = sum([int(bool(cmd.delay)) for cmd in commands])
# a = 1 a = 1
# for cmd in commands: a_max = 0
# # print("\r 正在进行处理:",end="") total_cmd = 0
# if cmd.delay > 2: for cmd in commands:
# a = 1 # print("\r 正在进行处理:",end="")
# else: if cmd.delay > 2:
# a += 1 a_max = max(a,a_max)
total_cmd += (a := 1)
else:
a += 1
struct = Structure( struct = Structure(
size=( size=(
round(delay_length / 2 + command_actually_length) round(delay_length / 2 + total_cmd)
if extensioon_direction == x if extensioon_direction == x
else max_multicmd_length, else a_max,
3, 3,
round(delay_length / 2 + command_actually_length) round(delay_length / 2 + total_cmd)
if extensioon_direction == z if extensioon_direction == z
else max_multicmd_length, else a_max,
), ),
fill=Block("minecraft", "air", compability_version=compability_version_), fill=Block("minecraft", "air", compability_version=compability_version_),
compability_version=compability_version_, compability_version=compability_version_,

View File

@ -19,6 +19,7 @@ Terms & Conditions: License.md in the root directory
from ..exceptions import NotDefineProgramError, ZeroSpeedError from ..exceptions import NotDefineProgramError, ZeroSpeedError
from ..main import MidiConvert from ..main import MidiConvert
from ..subclass import SingleCommand from ..subclass import SingleCommand
from ..utils import inst_to_souldID_withX, perc_inst_to_soundID_withX
# 你以为写完了吗?其实并没有 # 你以为写完了吗?其实并没有
@ -68,15 +69,15 @@ def to_note_list(
elif msg[0] == "NoteS": elif msg[0] == "NoteS":
try: try:
soundID, _X = ( soundID, _X = (
midi_cvt.perc_inst_to_soundID_withX(InstID) perc_inst_to_soundID_withX(InstID)
if SpecialBits if SpecialBits
else midi_cvt.inst_to_souldID_withX(InstID) else inst_to_souldID_withX(InstID)
) )
except UnboundLocalError as E: except UnboundLocalError as E:
soundID, _X = ( soundID, _X = (
midi_cvt.perc_inst_to_soundID_withX(-1) perc_inst_to_soundID_withX(-1)
if SpecialBits if SpecialBits
else midi_cvt.inst_to_souldID_withX(-1) else inst_to_souldID_withX(-1)
) )
score_now = round(msg[-1] / float(speed) / 50) score_now = round(msg[-1] / float(speed) / 50)
# print(score_now) # print(score_now)

View File

@ -18,6 +18,7 @@ Terms & Conditions: License.md in the root directory
from .exceptions import * from .exceptions import *
from .main import MidiConvert, mido from .main import MidiConvert, mido
from .subclass import * from .subclass import *
from .types import ChannelType
from .utils import * from .utils import *
@ -27,6 +28,82 @@ class ObsoleteMidiConvert(MidiConvert):
这些破烂老代码能跑得起来就是谢天谢地你们还指望我怎么样这玩意真的不会再维护了我发誓 这些破烂老代码能跑得起来就是谢天谢地你们还指望我怎么样这玩意真的不会再维护了我发誓
""" """
def to_music_channels(
self,
) -> ChannelType:
"""
使用金羿的转换思路将midi解析并转换为频道信息字典
Returns
-------
以频道作为分割的Midi信息字典:
Dict[int,Dict[int,List[Union[Tuple[Literal["PgmC"], int, int],Tuple[Literal["NoteS"], int, int, int],Tuple[Literal["NoteE"], int, int],]],],]
"""
if self.midi is None:
raise MidiUnboundError(
"你是否正在使用的是一个由 copy_important 生成的MidiConvert对象这是不可复用的。"
)
# 一个midi中仅有16个通道 我们通过通道来识别而不是音轨
midi_channels: ChannelType = empty_midi_channels()
tempo = mido.midifiles.midifiles.DEFAULT_TEMPO
# 我们来用通道统计音乐信息
# 但是是用分轨的思路的
for track_no, track in enumerate(self.midi.tracks):
microseconds = 0
if not track:
continue
note_queue = empty_midi_channels(staff=[])
for msg in track:
if msg.time != 0:
microseconds += msg.time * tempo / self.midi.ticks_per_beat / 1000
if msg.is_meta:
if msg.type == "set_tempo":
tempo = msg.tempo
else:
try:
if not track_no in midi_channels[msg.channel].keys():
midi_channels[msg.channel][track_no] = []
except AttributeError as E:
print(msg, E)
if msg.type == "program_change":
midi_channels[msg.channel][track_no].append(
("PgmC", msg.program, microseconds)
)
elif msg.type == "note_on" and msg.velocity != 0:
midi_channels[msg.channel][track_no].append(
("NoteS", msg.note, msg.velocity, microseconds)
)
elif (msg.type == "note_on" and msg.velocity == 0) or (
msg.type == "note_off"
):
midi_channels[msg.channel][track_no].append(
("NoteE", msg.note, microseconds)
)
"""整合后的音乐通道格式
每个通道包括若干消息元素其中逃不过这三种
1 切换乐器消息
("PgmC", 切换后的乐器ID: int, 距离演奏开始的毫秒)
2 音符开始消息
("NoteS", 开始的音符ID, 力度响度, 距离演奏开始的毫秒)
3 音符结束消息
("NoteE", 结束的音符ID, 距离演奏开始的毫秒)"""
del tempo, self.channels
self.channels = midi_channels
# [print([print(no,tno,sum([True if i[0] == 'NoteS' else False for i in track])) for tno,track in cna.items()]) if cna else False for no,cna in midi_channels.items()]
return midi_channels
def to_command_list_method1( def to_command_list_method1(
self, self,
scoreboard_name: str = "mscplay", scoreboard_name: str = "mscplay",
@ -76,9 +153,9 @@ class ObsoleteMidiConvert(MidiConvert):
) )
maxscore = max(maxscore, nowscore) maxscore = max(maxscore, nowscore)
if msg.channel == 9: if msg.channel == 9:
soundID, _X = self.perc_inst_to_soundID_withX(instrumentID) soundID, _X = perc_inst_to_soundID_withX(instrumentID)
else: else:
soundID, _X = self.inst_to_souldID_withX(instrumentID) soundID, _X = inst_to_souldID_withX(instrumentID)
singleTrack.append( singleTrack.append(
"execute @a[scores={" "execute @a[scores={"
@ -135,7 +212,7 @@ class ObsoleteMidiConvert(MidiConvert):
(ticks * tempo) / ((self.midi.ticks_per_beat * float(speed)) * 50000) # type: ignore (ticks * tempo) / ((self.midi.ticks_per_beat * float(speed)) * 50000) # type: ignore
) )
maxscore = max(maxscore, nowscore) maxscore = max(maxscore, nowscore)
soundID, _X = self.inst_to_souldID_withX(instrumentID) soundID, _X = inst_to_souldID_withX(instrumentID)
singleTrack.append( singleTrack.append(
"execute @a[scores={" "execute @a[scores={"
+ str(scoreboardname) + str(scoreboardname)
@ -152,7 +229,7 @@ class ObsoleteMidiConvert(MidiConvert):
# 原本这个算法的转换效果应该和上面的算法相似的 # 原本这个算法的转换效果应该和上面的算法相似的
def _toCmdList_m2( def _toCmdList_m2(
self: MidiConvert, self,
scoreboard_name: str = "mscplay", scoreboard_name: str = "mscplay",
MaxVolume: float = 1.0, MaxVolume: float = 1.0,
speed: float = 1.0, speed: float = 1.0,
@ -189,16 +266,16 @@ class ObsoleteMidiConvert(MidiConvert):
nowTrack = [] nowTrack = []
for track_no, track in self.channels[i].items(): for track_no, track in self.channels[i].items(): # type: ignore
for msg in track: for msg in track:
if msg[0] == "PgmC": if msg[0] == "PgmC":
InstID = msg[1] InstID = msg[1]
elif msg[0] == "NoteS": elif msg[0] == "NoteS":
soundID, _X = ( soundID, _X = (
self.perc_inst_to_soundID_withX(msg[1]) perc_inst_to_soundID_withX(msg[1])
if SpecialBits if SpecialBits
else self.inst_to_souldID_withX(InstID) else inst_to_souldID_withX(InstID)
) )
score_now = round(msg[-1] / float(speed) / 50) score_now = round(msg[-1] / float(speed) / 50)
maxScore = max(maxScore, score_now) maxScore = max(maxScore, score_now)
@ -221,7 +298,7 @@ class ObsoleteMidiConvert(MidiConvert):
return tracks, cmdAmount, maxScore return tracks, cmdAmount, maxScore
def _toCmdList_withDelay_m1( def _toCmdList_withDelay_m1(
self: MidiConvert, self,
MaxVolume: float = 1.0, MaxVolume: float = 1.0,
speed: float = 1.0, speed: float = 1.0,
player: str = "@a", player: str = "@a",
@ -262,7 +339,7 @@ class ObsoleteMidiConvert(MidiConvert):
(ticks * tempo) (ticks * tempo)
/ ((self.midi.ticks_per_beat * float(speed)) * 50000) / ((self.midi.ticks_per_beat * float(speed)) * 50000)
) )
soundID, _X = self.inst_to_souldID_withX(instrumentID) soundID, _X = inst_to_souldID_withX(instrumentID)
try: try:
tracks[now_tick].append( tracks[now_tick].append(
self.execute_cmd_head.format(player) self.execute_cmd_head.format(player)
@ -297,7 +374,7 @@ class ObsoleteMidiConvert(MidiConvert):
return [results, max(all_ticks)] return [results, max(all_ticks)]
def _toCmdList_withDelay_m2( def _toCmdList_withDelay_m2(
self: MidiConvert, self,
MaxVolume: float = 1.0, MaxVolume: float = 1.0,
speed: float = 1.0, speed: float = 1.0,
player: str = "@a", player: str = "@a",
@ -329,16 +406,16 @@ class ObsoleteMidiConvert(MidiConvert):
else: else:
SpecialBits = False SpecialBits = False
for track_no, track in self.channels[i].items(): for track_no, track in self.channels[i].items(): # type: ignore
for msg in track: for msg in track:
if msg[0] == "PgmC": if msg[0] == "PgmC":
InstID = msg[1] InstID = msg[1]
elif msg[0] == "NoteS": elif msg[0] == "NoteS":
soundID, _X = ( soundID, _X = (
self.perc_inst_to_soundID_withX(msg[1]) perc_inst_to_soundID_withX(msg[1])
if SpecialBits if SpecialBits
else self.inst_to_souldID_withX(InstID) else inst_to_souldID_withX(InstID)
) )
score_now = round(msg[-1] / float(speed) / 50) score_now = round(msg[-1] / float(speed) / 50)

View File

@ -21,6 +21,7 @@ from dataclasses import dataclass
from typing import Optional from typing import Optional
from .constants import PERCUSSION_INSTRUMENT_LIST from .constants import PERCUSSION_INSTRUMENT_LIST
from .utils import inst_to_souldID_withX, perc_inst_to_soundID_withX
@dataclass(init=False) @dataclass(init=False)
@ -45,14 +46,18 @@ class SingleNote:
track_no: int track_no: int
"""音符所处的音轨""" """音符所处的音轨"""
percussive: bool
"""是否为打击乐器"""
def __init__( def __init__(
self, self,
instrument: int, instrument: int,
pitch: int, pitch: int,
velocity: int, velocity: int,
startTime: int, startime: int,
lastTime: int, lastime: int,
track_number: int = 0, track_number: int = 0,
is_percussion: Optional[bool] = None,
): ):
"""用于存储单个音符的类 """用于存储单个音符的类
:param instrument 乐器编号 :param instrument 乐器编号
@ -67,12 +72,21 @@ class SingleNote:
"""音符编号""" """音符编号"""
self.velocity: int = velocity self.velocity: int = velocity
"""力度/响度""" """力度/响度"""
self.start_time: int = startTime self.start_time: int = startime
"""开始之时 ms""" """开始之时 ms"""
self.duration: int = lastTime self.duration: int = lastime
"""音符持续时间 ms""" """音符持续时间 ms"""
self.track_no: int = track_number self.track_no: int = track_number
"""音符所处的音轨""" """音符所处的音轨"""
self.track_no: int = track_number
"""音符所处的音轨"""
self.percussive = (
(is_percussion in PERCUSSION_INSTRUMENT_LIST)
if (is_percussion is None)
else is_percussion
)
"""是否为打击乐器"""
@property @property
def inst(self): def inst(self):
@ -89,28 +103,73 @@ class SingleNote:
return self.note return self.note
def __str__(self): def __str__(self):
return ( return "{}Note(Instrument = {}, {}Velocity = {}, StartTime = {}, Duration = {},)".format(
f"Note(inst = {self.inst}, pitch = {self.note}, velocity = {self.velocity}, " "Percussive" if self.percussive else "",
f"startTime = {self.start_time}, lastTime = {self.duration}, )" self.inst,
"" if self.percussive else "Pitch = {}, ".format(self.pitch),
self.start_time,
self.duration,
) )
def __tuple__(self): def __tuple__(self):
return self.inst, self.note, self.velocity, self.start_time, self.duration return (
(self.percussive, self.inst, self.velocity, self.start_time, self.duration)
if self.percussive
else (
self.percussive,
self.inst,
self.note,
self.velocity,
self.start_time,
self.duration,
)
)
def __dict__(self): def __dict__(self):
return { return (
"inst": self.inst, {
"pitch": self.note, "Percussive": self.percussive,
"velocity": self.velocity, "Instrument": self.inst,
"startTime": self.start_time, "Velocity": self.velocity,
"lastTime": self.duration, "StartTime": self.start_time,
"Duration": self.duration,
} }
if self.percussive
else {
"Percussive": self.percussive,
"Instrument": self.inst,
"Pitch": self.note,
"Velocity": self.velocity,
"StartTime": self.start_time,
"Duration": self.duration,
}
)
def __eq__(self, other) -> bool: def __eq__(self, other) -> bool:
if not isinstance(other, self.__class__): if not isinstance(other, self.__class__):
return False return False
return self.__str__() == other.__str__() return self.__str__() == other.__str__()
def to_command(self, volume_percentage) -> str:
self.mc_sound_ID, _X = (
perc_inst_to_soundID_withX(self.inst)
if self.percussive
else inst_to_souldID_withX(self.inst)
)
# delaytime_now = round(self.start_time / float(speed) / 50)
self.mc_pitch = "" if self.percussive else 2 ** ((self.note - 60 - _X) / 12)
self.mc_distance_volume = 128 / volume_percentage / self.velocity + (
1 if self.percussive else self.velocity / 32
)
return "playsound {} @s ^ ^ ^{} {} {}".format(
self.mc_sound_ID,
self.mc_distance_volume,
self.velocity / 128,
self.mc_pitch,
)
@dataclass(init=False) @dataclass(init=False)
class SingleCommand: class SingleCommand:

64
Musicreater/types.py Normal file
View File

@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
"""
存放数据类型的定义
"""
"""
版权所有 © 2023 · 开发者
Copyright © 2023 all the developers of Musicreater
开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory
"""
# 睿穆组织 开发交流群 861684859
# Email TriM-Organization@hotmail.com
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
import mido
from .subclass import SingleNote
ProgressStyle = Tuple[str, Tuple[str, str]]
"""
进度条样式类型
"""
VoidMido = Union[mido.MidiFile, None] # void mido
"""
空Midi类类型
"""
NoteChannelType = Dict[
int,
List[SingleNote,],
]
"""
频道信息类型
Dict[int,Dict[int,List[SingleNote,],],]
"""
ChannelType = Dict[
int,
Dict[
int,
List[
Union[
Tuple[Literal["PgmC"], int, int],
Tuple[Literal["NoteS"], int, int, int],
Tuple[Literal["NoteE"], int, int],
]
],
],
]
"""
以字典所标记的频道信息类型即将弃用
Dict[int,Dict[int,List[Union[Tuple[Literal["PgmC"], int, int],Tuple[Literal["NoteS"], int, int, int],Tuple[Literal["NoteE"], int, int],]],],]
"""

View File

@ -15,7 +15,8 @@ Terms & Conditions: License.md in the root directory
# Email TriM-Organization@hotmail.com # Email TriM-Organization@hotmail.com
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md # 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
from typing import Any, Dict from .constants import PERCUSSION_INSTRUMENT_TABLE, PITCHED_INSTRUMENT_TABLE
from typing import Any, Dict, Tuple
def mctick2timestr(mc_tick: int) -> str: def mctick2timestr(mc_tick: int) -> str:
@ -37,3 +38,54 @@ def empty_midi_channels(channel_count: int = 17, staff: Any = {}) -> Dict[int, A
) # 这告诉我们你不能忽略任何一个复制的序列因为它真的我哭死折磨我一整天全在这个bug上了 ) # 这告诉我们你不能忽略任何一个复制的序列因为它真的我哭死折磨我一整天全在这个bug上了
for i in range(channel_count) for i in range(channel_count)
) )
def inst_to_souldID_withX(
instrumentID: int,
) -> Tuple[str, int]:
"""
返回midi的乐器ID对应的我的世界乐器名对于音域转换算法如下
2**( ( msg.note - 60 - X ) / 12 ) 即为MC的音高其中
X的取值随乐器不同而变化
竖琴harp电钢琴pling班卓琴banjo方波bit颤音琴iron_xylophone 的时候为6
吉他的时候为7
贝斯bass迪吉里杜管didgeridoo的时候为8
长笛flute牛铃cou_bell的时候为5
钟琴bell管钟chime木琴xylophone的时候为4
而存在一些打击乐器bd(basedrum)hatsnare没有音域则没有X那么我们返回7即可
Parameters
----------
instrumentID: int
midi的乐器ID
Returns
-------
tuple(str我的世界乐器名, int转换算法中的X)
"""
try:
return PITCHED_INSTRUMENT_TABLE[instrumentID]
except KeyError:
return "note.flute", 5
def perc_inst_to_soundID_withX(instrumentID: int) -> Tuple[str, int]:
"""
对于Midi第10通道所对应的打击乐器返回我的世界乐器名
Parameters
----------
instrumentID: int
midi的乐器ID
Returns
-------
tuple(str我的世界乐器名, int转换算法中的X)
"""
try:
return PERCUSSION_INSTRUMENT_TABLE[instrumentID]
except KeyError:
return "note.bd", 7
# 明明已经走了
# 凭什么还要在我心里留下缠绵缱绻

View File

@ -20,12 +20,12 @@ import os
import Musicreater import Musicreater
from Musicreater.plugin import ConvertConfig from Musicreater.plugin import ConvertConfig
from Musicreater.plugin.bdxfile import to_BDX_file_in_delay, to_BDX_file_in_score
from Musicreater.plugin.addonpack import ( from Musicreater.plugin.addonpack import (
to_addon_pack_in_delay, to_addon_pack_in_delay,
to_addon_pack_in_repeater, to_addon_pack_in_repeater,
to_addon_pack_in_score, to_addon_pack_in_score,
) )
from Musicreater.plugin.bdxfile import to_BDX_file_in_delay, to_BDX_file_in_score
# 获取midi列表 # 获取midi列表
midi_path = input(f"请输入MIDI路径") midi_path = input(f"请输入MIDI路径")
@ -41,13 +41,13 @@ playerFormat = int(input(f"请选择播放方式[红石(2) 或 计分板(1) 或
# 真假字符串判断 # 真假字符串判断
def bool_str(sth: str) -> bool: def bool_str(sth: str):
try: try:
return bool(float(sth)) return bool(float(sth))
except ValueError: except:
if str(sth).lower() == "true": if str(sth).lower() in ("true", "", "", "y", "t"):
return True return True
elif str(sth).lower() == "false": elif str(sth).lower() in ("false", "", "", "f", "n"):
return False return False
else: else:
raise ValueError("布尔字符串啊?") raise ValueError("布尔字符串啊?")