mirror of
https://github.com/TriM-Organization/Musicreater.git
synced 2026-07-31 15:34:57 +00:00
完善新的音量设计,现在我们通过了标准的类型检查
This commit is contained in:
+19
-35
@@ -43,6 +43,7 @@ from typing import (
|
|||||||
Set,
|
Set,
|
||||||
Type,
|
Type,
|
||||||
Mapping,
|
Mapping,
|
||||||
|
overload,
|
||||||
)
|
)
|
||||||
|
|
||||||
if sys.version_info >= (3, 11):
|
if sys.version_info >= (3, 11):
|
||||||
@@ -336,9 +337,7 @@ class MusicInputPluginBase(TopInOutPluginBase, ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def loadbytes(
|
def loadbytes(self, bytes_buffer_in: BinaryIO, config) -> "SingleMusic":
|
||||||
self, bytes_buffer_in: BinaryIO, config: Optional[PluginConfig]
|
|
||||||
) -> "SingleMusic":
|
|
||||||
"""从字节流加载数据到完整曲目
|
"""从字节流加载数据到完整曲目
|
||||||
|
|
||||||
参数
|
参数
|
||||||
@@ -353,10 +352,9 @@ class MusicInputPluginBase(TopInOutPluginBase, ABC):
|
|||||||
SingleMusic
|
SingleMusic
|
||||||
解析得到的完整曲目对象
|
解析得到的完整曲目对象
|
||||||
"""
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
pass
|
def load(self, file_path: Path, config) -> "SingleMusic":
|
||||||
|
|
||||||
def load(self, file_path: Path, config: Optional[PluginConfig]) -> "SingleMusic":
|
|
||||||
"""从文件加载数据到完整曲目
|
"""从文件加载数据到完整曲目
|
||||||
|
|
||||||
参数
|
参数
|
||||||
@@ -390,9 +388,7 @@ class TrackInputPluginBase(TopInOutPluginBase, ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def loadbytes(
|
def loadbytes(self, bytes_buffer_in: BinaryIO, config) -> "SingleTrack":
|
||||||
self, bytes_buffer_in: BinaryIO, config: Optional[PluginConfig]
|
|
||||||
) -> "SingleTrack":
|
|
||||||
"""从字节流加载音符数据到单个音轨
|
"""从字节流加载音符数据到单个音轨
|
||||||
|
|
||||||
参数
|
参数
|
||||||
@@ -407,9 +403,9 @@ class TrackInputPluginBase(TopInOutPluginBase, ABC):
|
|||||||
SingleTrack
|
SingleTrack
|
||||||
解析得到的单个音轨对象
|
解析得到的单个音轨对象
|
||||||
"""
|
"""
|
||||||
pass
|
...
|
||||||
|
|
||||||
def load(self, file_path: Path, config: Optional[PluginConfig]) -> "SingleTrack":
|
def load(self, file_path: Path, config) -> "SingleTrack":
|
||||||
"""从文件加载音符数据到单个音轨
|
"""从文件加载音符数据到单个音轨
|
||||||
|
|
||||||
参数
|
参数
|
||||||
@@ -443,9 +439,7 @@ class MusicOperatePluginBase(TopPluginBase, ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def process(
|
def process(self, data: "SingleMusic", config) -> "SingleMusic":
|
||||||
self, data: "SingleMusic", config: Optional[PluginConfig]
|
|
||||||
) -> "SingleMusic":
|
|
||||||
"""处理完整曲目的数据
|
"""处理完整曲目的数据
|
||||||
|
|
||||||
参数
|
参数
|
||||||
@@ -460,7 +454,7 @@ class MusicOperatePluginBase(TopPluginBase, ABC):
|
|||||||
SingleMusic
|
SingleMusic
|
||||||
处理后的完整曲目
|
处理后的完整曲目
|
||||||
"""
|
"""
|
||||||
pass
|
...
|
||||||
|
|
||||||
|
|
||||||
class TrackOperatePluginBase(TopPluginBase, ABC):
|
class TrackOperatePluginBase(TopPluginBase, ABC):
|
||||||
@@ -478,9 +472,7 @@ class TrackOperatePluginBase(TopPluginBase, ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def process(
|
def process(self, data: "SingleTrack", config) -> "SingleTrack":
|
||||||
self, data: "SingleTrack", config: Optional[PluginConfig]
|
|
||||||
) -> "SingleTrack":
|
|
||||||
"""处理单个音轨的音符数据
|
"""处理单个音轨的音符数据
|
||||||
|
|
||||||
参数
|
参数
|
||||||
@@ -495,7 +487,7 @@ class TrackOperatePluginBase(TopPluginBase, ABC):
|
|||||||
SingleTrack
|
SingleTrack
|
||||||
处理后的单个音轨
|
处理后的单个音轨
|
||||||
"""
|
"""
|
||||||
pass
|
...
|
||||||
|
|
||||||
|
|
||||||
class MusicOutputPluginBase(TopInOutPluginBase, ABC):
|
class MusicOutputPluginBase(TopInOutPluginBase, ABC):
|
||||||
@@ -513,9 +505,7 @@ class MusicOutputPluginBase(TopInOutPluginBase, ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def stream_dump(
|
def stream_dump(self, data: "SingleMusic", config) -> Iterator[bytes]:
|
||||||
self, data: "SingleMusic", config: Optional[PluginConfig]
|
|
||||||
) -> Iterator[bytes]:
|
|
||||||
"""将完整曲目导出为对应格式的字节流
|
"""将完整曲目导出为对应格式的字节流
|
||||||
|
|
||||||
参数
|
参数
|
||||||
@@ -530,11 +520,9 @@ class MusicOutputPluginBase(TopInOutPluginBase, ABC):
|
|||||||
Iterator[bytes]
|
Iterator[bytes]
|
||||||
分块导出的二进制字节串
|
分块导出的二进制字节串
|
||||||
"""
|
"""
|
||||||
pass
|
...
|
||||||
|
|
||||||
def dump(
|
def dump(self, data: "SingleMusic", file_path: Path, config):
|
||||||
self, data: "SingleMusic", file_path: Path, config: Optional[PluginConfig]
|
|
||||||
):
|
|
||||||
"""将完整曲目导出为对应格式的文件
|
"""将完整曲目导出为对应格式的文件
|
||||||
|
|
||||||
参数
|
参数
|
||||||
@@ -567,9 +555,7 @@ class TrackOutputPluginBase(TopInOutPluginBase, ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def stream_dump(
|
def stream_dump(self, data: "SingleTrack", config) -> Iterator[bytes]:
|
||||||
self, data: "SingleTrack", config: Optional[PluginConfig]
|
|
||||||
) -> Iterator[bytes]:
|
|
||||||
"""将单个音轨导出为对应格式的字节流
|
"""将单个音轨导出为对应格式的字节流
|
||||||
|
|
||||||
参数
|
参数
|
||||||
@@ -584,11 +570,9 @@ class TrackOutputPluginBase(TopInOutPluginBase, ABC):
|
|||||||
Iterator[bytes]
|
Iterator[bytes]
|
||||||
分块导出的二进制字节串
|
分块导出的二进制字节串
|
||||||
"""
|
"""
|
||||||
pass
|
...
|
||||||
|
|
||||||
def dump(
|
def dump(self, data: "SingleTrack", file_path: Path, config):
|
||||||
self, data: "SingleTrack", file_path: Path, config: Optional[PluginConfig]
|
|
||||||
):
|
|
||||||
"""将单个音轨导出为对应格式的文件
|
"""将单个音轨导出为对应格式的文件
|
||||||
|
|
||||||
参数
|
参数
|
||||||
@@ -620,7 +604,7 @@ class ServicePluginBase(TopPluginBase, ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def serve(self, config: Optional[PluginConfig]) -> None:
|
def serve(self, config=None) -> None:
|
||||||
"""服务插件的运行逻辑
|
"""服务插件的运行逻辑
|
||||||
|
|
||||||
参数
|
参数
|
||||||
@@ -628,7 +612,7 @@ class ServicePluginBase(TopPluginBase, ABC):
|
|||||||
config: Optional[PluginConfig]
|
config: Optional[PluginConfig]
|
||||||
插件配置;**可选**
|
插件配置;**可选**
|
||||||
"""
|
"""
|
||||||
pass
|
...
|
||||||
|
|
||||||
|
|
||||||
class LibraryPluginBase(TopPluginBase, ABC):
|
class LibraryPluginBase(TopPluginBase, ABC):
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ class MusicExportToMcstructureWithDelayPlayerPlugin(MusicOutputPluginBase):
|
|||||||
with file_path.open("wb") as f:
|
with file_path.open("wb") as f:
|
||||||
struct.dump(f)
|
struct.dump(f)
|
||||||
|
|
||||||
return size, max_delay
|
# return size, max_delay
|
||||||
|
|
||||||
def stream_dump(
|
def stream_dump(
|
||||||
self, data: SingleMusic, config: McstructureExportConfig
|
self, data: SingleMusic, config: McstructureExportConfig
|
||||||
|
|||||||
@@ -170,6 +170,8 @@ class TrackDivisionDict(
|
|||||||
self.division_by_volume = midi_import_config.divide_tracks_by_volume
|
self.division_by_volume = midi_import_config.divide_tracks_by_volume
|
||||||
self.division_by_panning = midi_import_config.divide_tracks_by_panning
|
self.division_by_panning = midi_import_config.divide_tracks_by_panning
|
||||||
|
|
||||||
|
self.create_volume_curve = midi_import_config.keep_velocity_to_param_curve
|
||||||
|
|
||||||
def __getitem__(
|
def __getitem__(
|
||||||
self,
|
self,
|
||||||
key: Tuple[
|
key: Tuple[
|
||||||
@@ -192,7 +194,9 @@ class TrackDivisionDict(
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
self[key] = SingleTrack(precise_time=True)
|
self[key] = SingleTrack(precise_time=True)
|
||||||
if self.create_volume_curve:
|
if self.create_volume_curve:
|
||||||
self[key].argument_curves[CurvableParam.VOLUME] = ParamCurve()
|
self[key].argument_curves[CurvableParam.VOLUME] = ParamCurve(
|
||||||
|
base_value=1.0, value_min=0.0, value_max=1.0
|
||||||
|
)
|
||||||
return self[key]
|
return self[key]
|
||||||
|
|
||||||
|
|
||||||
@@ -287,12 +291,14 @@ class MidiImport2MusicPlugin(MusicInputPluginBase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
midi_tempo = config.default_tempo_value
|
note_count: int = 0
|
||||||
"""微秒每拍"""
|
|
||||||
note_count = 0
|
|
||||||
"""音符计数"""
|
"""音符计数"""
|
||||||
note_count_per_instrument: Dict[str, int] = {}
|
note_count_per_instrument: Dict[str, int] = {}
|
||||||
"""乐器使用统计"""
|
"""乐器使用统计"""
|
||||||
|
master_volume: int = 16383
|
||||||
|
"""主音量"""
|
||||||
|
music_name: str = ""
|
||||||
|
"""音乐名称"""
|
||||||
|
|
||||||
note_queue_A: Dict[int, List[Tuple[int, int]]] = (
|
note_queue_A: Dict[int, List[Tuple[int, int]]] = (
|
||||||
enumerated_stuffcopy_dictionary(staff=[])
|
enumerated_stuffcopy_dictionary(staff=[])
|
||||||
@@ -312,24 +318,98 @@ class MidiImport2MusicPlugin(MusicInputPluginBase):
|
|||||||
"""Midi 版权列表"""
|
"""Midi 版权列表"""
|
||||||
midi_track_name_dict: Dict[int, str] = {}
|
midi_track_name_dict: Dict[int, str] = {}
|
||||||
"""轨道名称字典 Dict[int轨道编号, str轨道名称]"""
|
"""轨道名称字典 Dict[int轨道编号, str轨道名称]"""
|
||||||
|
track_0 = midi.tracks[0].copy()
|
||||||
|
should_insert_controls = len(midi.tracks) > 1
|
||||||
|
|
||||||
for track_no, message_track in enumerate(midi.tracks):
|
for track_no, message_track in enumerate(midi.tracks):
|
||||||
# 每个音轨单独重置
|
# 每个音轨单独重置
|
||||||
|
|
||||||
microseconds = 0
|
if track_no == 0 and should_insert_controls:
|
||||||
|
# 第零个音轨在多轨模式下只是作为全局的控制轨出现的,因此忽略
|
||||||
|
# 但是,以防这里出现了不符合的情况,我们做如下控制
|
||||||
|
trk = []
|
||||||
|
tk0 = []
|
||||||
|
for msg in mido.midifiles.tracks._to_abstime(track_0):
|
||||||
|
if msg.type == "track_name":
|
||||||
|
music_name = (
|
||||||
|
msg.name.encode("latin1").decode(config.string_encoding)
|
||||||
|
if config.string_encoding
|
||||||
|
else msg.name
|
||||||
|
)
|
||||||
|
elif (msg.type == "set_tempo") or (msg.type == "sysex"):
|
||||||
|
tk0.append(msg)
|
||||||
|
else:
|
||||||
|
trk.append(msg)
|
||||||
|
|
||||||
|
track_0 = mido.MidiTrack(
|
||||||
|
mido.midifiles.tracks.fix_end_of_track(
|
||||||
|
mido.midifiles.tracks._to_reltime(
|
||||||
|
sorted(tk0, key=lambda a: a.time), skip_checks=True
|
||||||
|
),
|
||||||
|
skip_checks=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
midi.tracks.insert(
|
||||||
|
1,
|
||||||
|
mido.MidiTrack(
|
||||||
|
mido.midifiles.tracks.fix_end_of_track(
|
||||||
|
mido.midifiles.tracks._to_reltime(
|
||||||
|
sorted(trk, key=lambda a: a.time), skip_checks=True
|
||||||
|
),
|
||||||
|
skip_checks=True,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
del tk0, trk
|
||||||
|
continue
|
||||||
|
|
||||||
|
# print("正在处理轨道:", track_no, "需要合并:", should_insert_controls, "合并长度:", len(track_0))
|
||||||
|
|
||||||
|
midi_tempo: int = config.default_tempo_value
|
||||||
|
"""微秒每拍"""
|
||||||
|
microseconds: int = 0
|
||||||
"""当前的微妙时间"""
|
"""当前的微妙时间"""
|
||||||
for msg in message_track:
|
|
||||||
if msg.type == "set_tempo":
|
for msg in (
|
||||||
# Tempo 改变是一个全局的控制
|
mido.merge_tracks((track_0, message_track), skip_checks=True)
|
||||||
# 而且应该是很早出现的一个 Midi 消息
|
if should_insert_controls
|
||||||
midi_tempo = msg.tempo
|
else message_track
|
||||||
|
):
|
||||||
|
|
||||||
if msg.time != 0:
|
if msg.time != 0:
|
||||||
# 微秒
|
# 微秒
|
||||||
# 通常情况下,tempo 是 500000,tpb 在
|
# 通常情况下,tempo 是 500000,tpb 在
|
||||||
microseconds += msg.time * midi_tempo / midi.ticks_per_beat
|
microseconds += msg.time * midi_tempo / midi.ticks_per_beat
|
||||||
|
|
||||||
if msg.type == "program_change":
|
if msg.type == "set_tempo":
|
||||||
|
# Tempo 改变是一个全局的控制
|
||||||
|
# 而且应该是很早出现的一个 Midi 消息
|
||||||
|
midi_tempo = msg.tempo
|
||||||
|
# print(
|
||||||
|
# "Tempo 改变:",
|
||||||
|
# midi_tempo,
|
||||||
|
# "出现在轨道:",
|
||||||
|
# track_no,
|
||||||
|
# "时间:",
|
||||||
|
# microseconds,
|
||||||
|
# flush=True,
|
||||||
|
# )
|
||||||
|
elif msg.type == "sysex":
|
||||||
|
# 系统执行消息
|
||||||
|
data = msg.data # 不包含 F0 和 F7
|
||||||
|
# data: (7F, device_id, 04, 01, LSB, MSB)
|
||||||
|
if (
|
||||||
|
len(data) >= 6
|
||||||
|
and data[0] == 0x7F
|
||||||
|
and data[2] == 0x04
|
||||||
|
and data[3] == 0x01
|
||||||
|
):
|
||||||
|
# 检查 Master Volume 消息
|
||||||
|
master_volume = (data[5] << 7) | data[
|
||||||
|
4
|
||||||
|
] # 14-bit value: 0 ~ 16383
|
||||||
|
elif msg.type == "program_change":
|
||||||
# 检测 乐器变化 之 midi 事件
|
# 检测 乐器变化 之 midi 事件
|
||||||
value_controler_per_channel[msg.channel][
|
value_controler_per_channel[msg.channel][
|
||||||
ControlerKeys.MIDI_PROGRAM
|
ControlerKeys.MIDI_PROGRAM
|
||||||
@@ -359,8 +439,10 @@ class MidiImport2MusicPlugin(MusicInputPluginBase):
|
|||||||
midi_copyright_list.append(msg.text)
|
midi_copyright_list.append(msg.text)
|
||||||
elif msg.type == "track_name":
|
elif msg.type == "track_name":
|
||||||
# 检测轨道名称事件
|
# 检测轨道名称事件
|
||||||
midi_track_name_dict[track_no] = msg.name.encode("latin1").decode(
|
midi_track_name_dict[track_no] = (
|
||||||
config.string_encoding
|
msg.name.encode("latin1").decode(config.string_encoding)
|
||||||
|
if config.string_encoding
|
||||||
|
else msg.name
|
||||||
)
|
)
|
||||||
elif msg.type == "note_on" and msg.velocity != 0:
|
elif msg.type == "note_on" and msg.velocity != 0:
|
||||||
# 一个音符开始弹奏
|
# 一个音符开始弹奏
|
||||||
@@ -429,6 +511,7 @@ class MidiImport2MusicPlugin(MusicInputPluginBase):
|
|||||||
note=(_program if _is_percussion else msg.note),
|
note=(_program if _is_percussion else msg.note),
|
||||||
percussive=_is_percussion,
|
percussive=_is_percussion,
|
||||||
volume=_volume,
|
volume=_volume,
|
||||||
|
master_volume=master_volume,
|
||||||
velocity=_velocity,
|
velocity=_velocity,
|
||||||
panning=_panning,
|
panning=_panning,
|
||||||
start_time=_start_ms, # 微秒
|
start_time=_start_ms, # 微秒
|
||||||
@@ -455,7 +538,7 @@ class MidiImport2MusicPlugin(MusicInputPluginBase):
|
|||||||
track_no,
|
track_no,
|
||||||
msg.channel,
|
msg.channel,
|
||||||
sound_name,
|
sound_name,
|
||||||
_volume,
|
_volume * master_volume // 16383,
|
||||||
sound_rotation,
|
sound_rotation,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -465,6 +548,7 @@ class MidiImport2MusicPlugin(MusicInputPluginBase):
|
|||||||
time=that_note.precise_start_time,
|
time=that_note.precise_start_time,
|
||||||
value=_velocity / 127,
|
value=_velocity / 127,
|
||||||
)
|
)
|
||||||
|
# print("VOLUME:", _velocity / 127, flush=True, end=", ")
|
||||||
|
|
||||||
# 更新统计信息
|
# 更新统计信息
|
||||||
note_count += 1
|
note_count += 1
|
||||||
@@ -479,7 +563,8 @@ class MidiImport2MusicPlugin(MusicInputPluginBase):
|
|||||||
print(
|
print(
|
||||||
"[WARRING] MIDI格式错误 音符不匹配`{}`无法在上文`{}`中找到与之匹配的音符开音消息".format(
|
"[WARRING] MIDI格式错误 音符不匹配`{}`无法在上文`{}`中找到与之匹配的音符开音消息".format(
|
||||||
msg, note_queue_A[msg.channel]
|
msg, note_queue_A[msg.channel]
|
||||||
)
|
),
|
||||||
|
flush=True,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise NoteOnOffMismatchError(
|
raise NoteOnOffMismatchError(
|
||||||
@@ -488,15 +573,14 @@ class MidiImport2MusicPlugin(MusicInputPluginBase):
|
|||||||
"无法在上文中找到与之匹配的音符开音消息。",
|
"无法在上文中找到与之匹配的音符开音消息。",
|
||||||
)
|
)
|
||||||
|
|
||||||
del midi_tempo
|
|
||||||
|
|
||||||
if midi_lyric_cache:
|
if midi_lyric_cache:
|
||||||
# 怎么有歌词多啊
|
# 怎么有歌词多啊
|
||||||
if config.ignore_errors:
|
if config.ignore_errors:
|
||||||
print(
|
print(
|
||||||
"[WARRING] MIDI 解析错误 歌词对应错误,以下歌词未能填入音符之中,已经填入的仍可能有误 {}".format(
|
"[WARRING] MIDI 解析错误 歌词对应错误,以下歌词未能填入音符之中,已经填入的仍可能有误 {}".format(
|
||||||
midi_lyric_cache
|
midi_lyric_cache
|
||||||
)
|
),
|
||||||
|
flush=True,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise LyricMismatchError(
|
raise LyricMismatchError(
|
||||||
@@ -506,6 +590,7 @@ class MidiImport2MusicPlugin(MusicInputPluginBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
final_music = SingleMusic(
|
final_music = SingleMusic(
|
||||||
|
name=music_name,
|
||||||
credits="; ".join(midi_copyright_list),
|
credits="; ".join(midi_copyright_list),
|
||||||
extra_information={
|
extra_information={
|
||||||
"MIDI_TEXT_LIST": midi_text_list,
|
"MIDI_TEXT_LIST": midi_text_list,
|
||||||
@@ -518,14 +603,20 @@ class MidiImport2MusicPlugin(MusicInputPluginBase):
|
|||||||
if track_properties[0] and (
|
if track_properties[0] and (
|
||||||
track_name := midi_track_name_dict.get(track_properties[0], "无名音轨")
|
track_name := midi_track_name_dict.get(track_properties[0], "无名音轨")
|
||||||
): # 音轨编号
|
): # 音轨编号
|
||||||
every_single_track.name = track_name
|
every_single_track.name = track_name or "空名称"
|
||||||
if track_properties[2]: # 乐器名称
|
if track_properties[2]: # 乐器名称
|
||||||
every_single_track.instrument = Instrument(
|
every_single_track.instrument = Instrument(
|
||||||
track_properties[2],
|
track_properties[2],
|
||||||
config.instrument_information_table[track_properties[2]]["C-LUFS"],
|
config.instrument_information_table[track_properties[2]]["C-LUFS"],
|
||||||
)
|
)
|
||||||
if track_properties[3]: # 音量权
|
if track_properties[3]: # 音量权
|
||||||
every_single_track.__volume_weight = track_properties[3]
|
# print(
|
||||||
|
# "[INFO] 音轨 {} 音量权已设置为 {}".format(
|
||||||
|
# every_single_track.name, track_properties[3]
|
||||||
|
# ),
|
||||||
|
# flush=True,
|
||||||
|
# )
|
||||||
|
every_single_track._volume_weight = track_properties[3]
|
||||||
if track_properties[4]: # 声相
|
if track_properties[4]: # 声相
|
||||||
every_single_track.sound_position = track_properties[4]
|
every_single_track.sound_position = track_properties[4]
|
||||||
super(SingleMusic, final_music).append(every_single_track)
|
super(SingleMusic, final_music).append(every_single_track)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from typing import Callable, Dict, List, Optional, Sequence, Tuple, Mapping
|
|||||||
from Musicreater import SingleNote, SoundAtmos
|
from Musicreater import SingleNote, SoundAtmos
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def panning_2_rotation_linear(pan_: float) -> float:
|
def panning_2_rotation_linear(pan_: float) -> float:
|
||||||
"""
|
"""
|
||||||
Midi 左右平衡偏移值线性转为声源旋转角度
|
Midi 左右平衡偏移值线性转为声源旋转角度
|
||||||
@@ -97,6 +98,7 @@ def midi_msgs_to_noteinfo(
|
|||||||
note: int,
|
note: int,
|
||||||
percussive: bool, # 是否作为打击乐器启用
|
percussive: bool, # 是否作为打击乐器启用
|
||||||
volume: int,
|
volume: int,
|
||||||
|
master_volume: int,
|
||||||
velocity: int,
|
velocity: int,
|
||||||
panning: int,
|
panning: int,
|
||||||
start_time: int,
|
start_time: int,
|
||||||
@@ -120,6 +122,8 @@ def midi_msgs_to_noteinfo(
|
|||||||
是否作为打击乐器启用
|
是否作为打击乐器启用
|
||||||
volume: int
|
volume: int
|
||||||
音量
|
音量
|
||||||
|
master_volume: int
|
||||||
|
主音量
|
||||||
velocity: int
|
velocity: int
|
||||||
力度
|
力度
|
||||||
panning: int
|
panning: int
|
||||||
@@ -165,6 +169,7 @@ def midi_msgs_to_noteinfo(
|
|||||||
"LYRIC_TEXT": lyric_line,
|
"LYRIC_TEXT": lyric_line,
|
||||||
"VELOCITY_VALUE": velocity,
|
"VELOCITY_VALUE": velocity,
|
||||||
"VOLUME_VALUE": volume,
|
"VOLUME_VALUE": volume,
|
||||||
|
"MASTER_VOLUME_VALUE": master_volume,
|
||||||
"PIN_VALUE": panning,
|
"PIN_VALUE": panning,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -90,6 +90,11 @@ class PcmConversionConfig(PluginConfig):
|
|||||||
音调处理精度,小数点后位数
|
音调处理精度,小数点后位数
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
global_volume: float = 1.0
|
||||||
|
"""
|
||||||
|
全局音量控制
|
||||||
|
"""
|
||||||
|
|
||||||
global_deviation: float = 0
|
global_deviation: float = 0
|
||||||
"""
|
"""
|
||||||
全曲音调偏移
|
全曲音调偏移
|
||||||
@@ -120,6 +125,7 @@ class NoteDataConvert2PcmPlugin(MusicOutputPluginBase):
|
|||||||
sample_rate=config.sample_rate,
|
sample_rate=config.sample_rate,
|
||||||
value_get_method=config.value_get_method,
|
value_get_method=config.value_get_method,
|
||||||
pitch_accuracy_decimals=config.pitch_accuracy_decimals,
|
pitch_accuracy_decimals=config.pitch_accuracy_decimals,
|
||||||
|
music_volume=config.global_volume,
|
||||||
music_deviation=config.global_deviation,
|
music_deviation=config.global_deviation,
|
||||||
)
|
)
|
||||||
music_preview.to_wav_file(data, file_path)
|
music_preview.to_wav_file(data, file_path)
|
||||||
@@ -138,6 +144,7 @@ class NoteDataConvert2PcmPlugin(MusicOutputPluginBase):
|
|||||||
sample_rate=config.sample_rate,
|
sample_rate=config.sample_rate,
|
||||||
value_get_method=config.value_get_method,
|
value_get_method=config.value_get_method,
|
||||||
pitch_accuracy_decimals=config.pitch_accuracy_decimals,
|
pitch_accuracy_decimals=config.pitch_accuracy_decimals,
|
||||||
|
music_volume=config.global_volume,
|
||||||
music_deviation=config.global_deviation,
|
music_deviation=config.global_deviation,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class MusicPreview:
|
|||||||
sample_rate: int = 44100,
|
sample_rate: int = 44100,
|
||||||
value_get_method: Literal[0, 1] = 1,
|
value_get_method: Literal[0, 1] = 1,
|
||||||
pitch_accuracy_decimals: int = 0,
|
pitch_accuracy_decimals: int = 0,
|
||||||
|
music_volume: float = 1,
|
||||||
music_deviation: float = 0,
|
music_deviation: float = 0,
|
||||||
):
|
):
|
||||||
|
|
||||||
@@ -107,6 +108,7 @@ class MusicPreview:
|
|||||||
# 此处 TODO,需待旧梦来完善
|
# 此处 TODO,需待旧梦来完善
|
||||||
self.pitch_precision_decimals = 0
|
self.pitch_precision_decimals = 0
|
||||||
|
|
||||||
|
self.music_volume = music_volume
|
||||||
self.pitch_deviation = music_deviation
|
self.pitch_deviation = music_deviation
|
||||||
|
|
||||||
for file in self.resources_path.iterdir():
|
for file in self.resources_path.iterdir():
|
||||||
@@ -370,23 +372,22 @@ class MusicPreview:
|
|||||||
# note.percussive,
|
# note.percussive,
|
||||||
# note.duration_tick,
|
# note.duration_tick,
|
||||||
# )
|
# )
|
||||||
if not note.percussive:
|
# print(note.position)
|
||||||
overlay(
|
# print("参数倍率", self.music_volume, (1-(note.position.sound_distance / 48)), flush=True)
|
||||||
self.res_data[note.instrument][accurate_pitch]
|
|
||||||
* (1 - (note.position.sound_distance / 48)),
|
# print("部分数据(前):",self.res_data[note.instrument][accurate_pitch][:5])
|
||||||
note.start_tick,
|
overlay(
|
||||||
)
|
self.res_data[note.instrument][accurate_pitch]
|
||||||
# 上述参数的原顺序应为
|
* self.music_volume
|
||||||
# x * (音量 / 127) * (1 / (距离 + 0.5))
|
* (1 - (note.position.sound_distance / 48)),
|
||||||
# 乘法优先是为了提高计算精度,小的数的除法优先同理
|
note.start_tick,
|
||||||
# 下面是后来的注释
|
)
|
||||||
# 这种方式是错误的 —— 金羿 20260720
|
# print("部分数据(后):", wav_model[:5])
|
||||||
else:
|
# 上述参数的原顺序应为
|
||||||
overlay(
|
# x * (音量 / 127) * (1 / (距离 + 0.5))
|
||||||
self.res_data[note.instrument][accurate_pitch]
|
# 乘法优先是为了提高计算精度,小的数的除法优先同理
|
||||||
* (1 - (note.position.sound_distance / 48)),
|
# 下面是后来的注释
|
||||||
note.start_tick,
|
# 这种方式是错误的 —— 金羿 20260720
|
||||||
)
|
|
||||||
|
|
||||||
if self.get_value_method == 0:
|
if self.get_value_method == 0:
|
||||||
# 归一化,抚摸耳朵 (bushi
|
# 归一化,抚摸耳朵 (bushi
|
||||||
|
|||||||
+49
-33
@@ -95,7 +95,7 @@ class SoundAtmos:
|
|||||||
|
|
||||||
# 如果指定为零,那么为零,但如果不指定或者指定为负数,则为 0.01 的距离
|
# 如果指定为零,那么为零,但如果不指定或者指定为负数,则为 0.01 的距离
|
||||||
self.sound_distance = (
|
self.sound_distance = (
|
||||||
(16 if distance > 16 else (distance if distance >= 0 else 0.01))
|
(49 if distance > 48 else (distance if distance >= 0 else 0.01))
|
||||||
if distance is not None
|
if distance is not None
|
||||||
else 0.01
|
else 0.01
|
||||||
)
|
)
|
||||||
@@ -110,7 +110,7 @@ class SoundAtmos:
|
|||||||
"""设置声源距离"""
|
"""设置声源距离"""
|
||||||
|
|
||||||
self.sound_distance = (
|
self.sound_distance = (
|
||||||
(16 if distance > 16 else (distance if distance >= 0 else 0.01))
|
(49 if distance > 48 else (distance if distance >= 0 else 0.01))
|
||||||
if distance is not None
|
if distance is not None
|
||||||
else 0.01
|
else 0.01
|
||||||
)
|
)
|
||||||
@@ -195,7 +195,7 @@ class DefaultInstrument(Enum):
|
|||||||
HARP = Instrument("note.harp", -1448)
|
HARP = Instrument("note.harp", -1448)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False, eq=False, order=False)
|
||||||
class SingleNote:
|
class SingleNote:
|
||||||
"""存储单个音符的类"""
|
"""存储单个音符的类"""
|
||||||
|
|
||||||
@@ -388,7 +388,9 @@ class SingleNote:
|
|||||||
self.high_precision_start_time,
|
self.high_precision_start_time,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __dict__(self):
|
def __dict__( # pyright: ignore[reportIncompatibleVariableOverride]
|
||||||
|
self,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"Pitch": self.midi_pitch,
|
"Pitch": self.midi_pitch,
|
||||||
"StartTick": self.start_time,
|
"StartTick": self.start_time,
|
||||||
@@ -397,7 +399,9 @@ class SingleNote:
|
|||||||
"ExtraData": self.extra_info,
|
"ExtraData": self.extra_info,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __eq__(self, other: "SingleNote") -> bool:
|
def __eq__( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
|
self, other: "SingleNote"
|
||||||
|
) -> bool:
|
||||||
"""比较两个音符是否具有相同的属性,不计附加信息"""
|
"""比较两个音符是否具有相同的属性,不计附加信息"""
|
||||||
if not isinstance(other, self.__class__):
|
if not isinstance(other, self.__class__):
|
||||||
return False
|
return False
|
||||||
@@ -452,7 +456,7 @@ class MineNote:
|
|||||||
is_percussive_note: bool,
|
is_percussive_note: bool,
|
||||||
sound_position: SoundAtmos,
|
sound_position: SoundAtmos,
|
||||||
adjust_note_pitch: float = 0.0,
|
adjust_note_pitch: float = 0.0,
|
||||||
adjust_note_volume_weight: float = 0.0,
|
multiple_note_volume_weight: float = 0.0,
|
||||||
adjust_note_sound_distance: float = 0.0,
|
adjust_note_sound_distance: float = 0.0,
|
||||||
adjust_note_leftright_panning_degree: float = 0.0,
|
adjust_note_leftright_panning_degree: float = 0.0,
|
||||||
adjust_note_updown_panning_degree: float = 0.0,
|
adjust_note_updown_panning_degree: float = 0.0,
|
||||||
@@ -467,21 +471,25 @@ class MineNote:
|
|||||||
percussive=is_percussive_note,
|
percussive=is_percussive_note,
|
||||||
position=(
|
position=(
|
||||||
SoundAtmos(
|
SoundAtmos(
|
||||||
sound_position.sound_distance
|
(
|
||||||
+ adjust_note_sound_distance
|
|
||||||
+ (
|
|
||||||
(
|
(
|
||||||
(
|
(
|
||||||
(
|
(
|
||||||
(sound_position.sound_distance - 48)
|
(
|
||||||
* (note_volume_weight + adjust_note_volume_weight)
|
sound_position.sound_distance
|
||||||
|
+ adjust_note_sound_distance
|
||||||
|
- 48
|
||||||
|
)
|
||||||
|
* (note_volume_weight * multiple_note_volume_weight)
|
||||||
/ note_volume_weight
|
/ note_volume_weight
|
||||||
)
|
)
|
||||||
+ 48
|
+ 48
|
||||||
)
|
)
|
||||||
|
) # 此处调整须保证前提:音量权的增值必须为负
|
||||||
|
if multiple_note_volume_weight
|
||||||
|
else (
|
||||||
|
sound_position.sound_distance + adjust_note_sound_distance
|
||||||
)
|
)
|
||||||
if adjust_note_volume_weight
|
|
||||||
else 0
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
sound_position.sound_azimuth[0]
|
sound_position.sound_azimuth[0]
|
||||||
@@ -491,7 +499,7 @@ class MineNote:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
adjust_note_volume_weight
|
multiple_note_volume_weight
|
||||||
or adjust_note_sound_distance
|
or adjust_note_sound_distance
|
||||||
or adjust_note_leftright_panning_degree
|
or adjust_note_leftright_panning_degree
|
||||||
or adjust_note_updown_panning_degree
|
or adjust_note_updown_panning_degree
|
||||||
@@ -505,7 +513,7 @@ class CurvableParam(str, Enum):
|
|||||||
"""可曲线化的参数 枚举类"""
|
"""可曲线化的参数 枚举类"""
|
||||||
|
|
||||||
PITCH = "adjust_note_pitch"
|
PITCH = "adjust_note_pitch"
|
||||||
VOLUME = "adjust_note_volume_weight"
|
VOLUME = "multiple_note_volume_weight"
|
||||||
DISTANCE = "adjust_note_sound_distance"
|
DISTANCE = "adjust_note_sound_distance"
|
||||||
LR_PANNING = "adjust_note_leftright_panning_degree"
|
LR_PANNING = "adjust_note_leftright_panning_degree"
|
||||||
UD_PANNING = "adjust_note_updown_panning_degree"
|
UD_PANNING = "adjust_note_updown_panning_degree"
|
||||||
@@ -529,10 +537,10 @@ class SingleTrack(List[SingleNote]):
|
|||||||
is_percussive: bool
|
is_percussive: bool
|
||||||
"""该音轨是否标记为打击乐器轨道"""
|
"""该音轨是否标记为打击乐器轨道"""
|
||||||
|
|
||||||
__volume_weight: float = 10
|
_volume_weight: float = 10
|
||||||
"""音量权"""
|
"""音量权"""
|
||||||
|
|
||||||
__sound_distance: float = -1
|
_sound_distance: float = -1
|
||||||
"""声源距离"""
|
"""声源距离"""
|
||||||
|
|
||||||
sound_position: Tuple[float, float]
|
sound_position: Tuple[float, float]
|
||||||
@@ -596,10 +604,10 @@ class SingleTrack(List[SingleNote]):
|
|||||||
self.is_percussive = percussion
|
self.is_percussive = percussion
|
||||||
"""是否为打击乐器"""
|
"""是否为打击乐器"""
|
||||||
|
|
||||||
self.__volume_weight = volume_weight
|
self._volume_weight = volume_weight
|
||||||
"""音量权"""
|
"""音量权"""
|
||||||
|
|
||||||
self.__sound_distance = -1
|
self._sound_distance = -1
|
||||||
"""声源距离"""
|
"""声源距离"""
|
||||||
|
|
||||||
self.sound_position = sound_direction
|
self.sound_position = sound_direction
|
||||||
@@ -643,18 +651,18 @@ class SingleTrack(List[SingleNote]):
|
|||||||
@property
|
@property
|
||||||
def volume_weight(self) -> float:
|
def volume_weight(self) -> float:
|
||||||
"""音量权"""
|
"""音量权"""
|
||||||
return self.__volume_weight
|
return self._volume_weight
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sound_distance(self) -> float:
|
def sound_distance(self) -> float:
|
||||||
"""声源距离"""
|
"""声源距离"""
|
||||||
if self.__sound_distance < 0:
|
if self._sound_distance < 0:
|
||||||
raise ParameterCacheUnfreshError(
|
raise ParameterCacheUnfreshError(
|
||||||
"未设置声源距离(`{}`),操作时不应直接访问 `__volume_weight` 属性".format(
|
"未设置声源距离(`{}`),操作时不应直接访问 `__volume_weight` 属性".format(
|
||||||
self.__sound_distance
|
self._sound_distance
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return self.__sound_distance
|
return self._sound_distance
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def note_amount(self) -> int:
|
def note_amount(self) -> int:
|
||||||
@@ -689,9 +697,9 @@ class SingleTrack(List[SingleNote]):
|
|||||||
由于我们存储的响度都是 百-LUFS,所以下面的计算除数为 2000
|
由于我们存储的响度都是 百-LUFS,所以下面的计算除数为 2000
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.__volume_weight = weight
|
self._volume_weight = weight
|
||||||
|
|
||||||
self.__sound_distance = volume_weight_to_sound_distance(
|
self._sound_distance = volume_weight_to_sound_distance(
|
||||||
max_weight, minimal_loadness, weight, self.instrument.loadness
|
max_weight, minimal_loadness, weight, self.instrument.loadness
|
||||||
)
|
)
|
||||||
# print(
|
# print(
|
||||||
@@ -702,7 +710,7 @@ class SingleTrack(List[SingleNote]):
|
|||||||
# ",设置声源距离为:",
|
# ",设置声源距离为:",
|
||||||
# self.__sound_distance,
|
# self.__sound_distance,
|
||||||
# )
|
# )
|
||||||
return self.__sound_distance
|
return self._sound_distance
|
||||||
|
|
||||||
# 类操作
|
# 类操作
|
||||||
|
|
||||||
@@ -757,7 +765,7 @@ class SingleTrack(List[SingleNote]):
|
|||||||
volume_weight=self.volume_weight,
|
volume_weight=self.volume_weight,
|
||||||
sound_direction=self.sound_position,
|
sound_direction=self.sound_position,
|
||||||
)
|
)
|
||||||
single_track.__sound_distance = self.sound_distance
|
single_track._sound_distance = self.sound_distance
|
||||||
if with_argument_curve:
|
if with_argument_curve:
|
||||||
single_track.argument_curves = {
|
single_track.argument_curves = {
|
||||||
item: (
|
item: (
|
||||||
@@ -996,6 +1004,7 @@ class SingleMusic(List[SingleTrack]):
|
|||||||
# 前置方法
|
# 前置方法
|
||||||
|
|
||||||
def _calc_values(self):
|
def _calc_values(self):
|
||||||
|
# print("收到音乐参数计算请求,当前最大:",self.__max_volume_weight, end=";")
|
||||||
for track in self:
|
for track in self:
|
||||||
self.__max_volume_weight = max(
|
self.__max_volume_weight = max(
|
||||||
track.volume_weight, self.__max_volume_weight
|
track.volume_weight, self.__max_volume_weight
|
||||||
@@ -1009,6 +1018,7 @@ class SingleMusic(List[SingleTrack]):
|
|||||||
minimal_loadness=self.__minimal_loadness,
|
minimal_loadness=self.__minimal_loadness,
|
||||||
weight=track.volume_weight,
|
weight=track.volume_weight,
|
||||||
)
|
)
|
||||||
|
# print("计算后最大:",self.__max_volume_weight)
|
||||||
|
|
||||||
# 类的构建方法
|
# 类的构建方法
|
||||||
|
|
||||||
@@ -1076,7 +1086,7 @@ class SingleMusic(List[SingleTrack]):
|
|||||||
super().append(track)
|
super().append(track)
|
||||||
self._calc_values()
|
self._calc_values()
|
||||||
|
|
||||||
def extend(self, tracks: Sequence[SingleTrack]) -> None:
|
def extend(self, tracks: Iterable[SingleTrack]) -> None:
|
||||||
"""
|
"""
|
||||||
将一个音轨列表添加到全曲中,依顺序在所有音轨之后排列。
|
将一个音轨列表添加到全曲中,依顺序在所有音轨之后排列。
|
||||||
|
|
||||||
@@ -1085,7 +1095,7 @@ class SingleMusic(List[SingleTrack]):
|
|||||||
super().extend(tracks)
|
super().extend(tracks)
|
||||||
self._calc_values()
|
self._calc_values()
|
||||||
|
|
||||||
def insert(self, index: int, track: SingleTrack) -> None:
|
def insert(self, index: SupportsIndex, track: SingleTrack) -> None:
|
||||||
"""
|
"""
|
||||||
将一个音轨插入到全曲中,在指定位置。
|
将一个音轨插入到全曲中,在指定位置。
|
||||||
|
|
||||||
@@ -1136,7 +1146,9 @@ class SingleMusic(List[SingleTrack]):
|
|||||||
@overload
|
@overload
|
||||||
def __setitem__(self, key: slice, value: Sequence[SingleTrack]) -> None: ...
|
def __setitem__(self, key: slice, value: Sequence[SingleTrack]) -> None: ...
|
||||||
|
|
||||||
def __setitem__(self, key, value) -> None:
|
def __setitem__( # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
|
self, key, value
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
设置一个音轨
|
设置一个音轨
|
||||||
|
|
||||||
@@ -1183,18 +1195,20 @@ class SingleMusic(List[SingleTrack]):
|
|||||||
"""音轨列表,不包含被禁用的音轨"""
|
"""音轨列表,不包含被禁用的音轨"""
|
||||||
return (track for track in self if track.is_enabled)
|
return (track for track in self if track.is_enabled)
|
||||||
|
|
||||||
def set_loadness(self, track: Union[int, SingleTrack], weight: float):
|
def set_loadness(self, track: Union[int, SingleTrack], weight: float) -> None:
|
||||||
if weight > self.__max_volume_weight:
|
if weight > self.__max_volume_weight:
|
||||||
if isinstance(track, int):
|
if isinstance(track, int):
|
||||||
self[track].__volume_weight = weight
|
self[track]._volume_weight = weight
|
||||||
|
|
||||||
elif isinstance(track, SingleTrack):
|
elif isinstance(track, SingleTrack):
|
||||||
if track in self:
|
if track in self:
|
||||||
track.__volume_weight = weight
|
track._volume_weight = weight
|
||||||
else:
|
else:
|
||||||
raise ParameterValueError(
|
raise ParameterValueError(
|
||||||
"音轨:`{}`不属于此曲目".format(track.name)
|
"音轨:`{}`不属于此曲目".format(track.name)
|
||||||
)
|
)
|
||||||
self._calc_values()
|
self._calc_values()
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
if isinstance(track, int):
|
if isinstance(track, int):
|
||||||
self[track].set_volume_weight(
|
self[track].set_volume_weight(
|
||||||
@@ -1202,12 +1216,14 @@ class SingleMusic(List[SingleTrack]):
|
|||||||
minimal_loadness=self.__minimal_loadness,
|
minimal_loadness=self.__minimal_loadness,
|
||||||
weight=weight,
|
weight=weight,
|
||||||
)
|
)
|
||||||
|
return
|
||||||
elif isinstance(track, SingleTrack):
|
elif isinstance(track, SingleTrack):
|
||||||
track.set_volume_weight(
|
track.set_volume_weight(
|
||||||
max_weight=self.__max_volume_weight,
|
max_weight=self.__max_volume_weight,
|
||||||
minimal_loadness=self.__minimal_loadness,
|
minimal_loadness=self.__minimal_loadness,
|
||||||
weight=weight,
|
weight=weight,
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
|
||||||
raise ParameterTypeError("音轨不得为`{}`类型".format(type(track)))
|
raise ParameterTypeError("音轨不得为`{}`类型".format(type(track)))
|
||||||
|
|
||||||
|
|||||||
+220
-85
@@ -218,6 +218,7 @@ class ParamCurve:
|
|||||||
支持动态节点编辑
|
支持动态节点编辑
|
||||||
用户通过添加/修改关键帧(时间-值对)来定义曲线,类自动在相邻关键帧之间生成插值段。
|
用户通过添加/修改关键帧(时间-值对)来定义曲线,类自动在相邻关键帧之间生成插值段。
|
||||||
支持多种插值模式:线性('linear')、平滑缓动('smooth')、保持('hold')或自定义函数。
|
支持多种插值模式:线性('linear')、平滑缓动('smooth')、保持('hold')或自定义函数。
|
||||||
|
支持值域限制:可设定参数值的上限与下限,所有输出及关键帧写入均受约束。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
base_line: float = 0.0
|
base_line: float = 0.0
|
||||||
@@ -229,6 +230,12 @@ class ParamCurve:
|
|||||||
boundary_behaviour: BoundaryBehaviour
|
boundary_behaviour: BoundaryBehaviour
|
||||||
"""边界行为,控制参数曲线在已定义的范围外的返回值"""
|
"""边界行为,控制参数曲线在已定义的范围外的返回值"""
|
||||||
|
|
||||||
|
value_min: Optional[float] = None
|
||||||
|
"""参数值下限(None 表示不限制)"""
|
||||||
|
|
||||||
|
value_max: Optional[float] = None
|
||||||
|
"""参数值上限(None 表示不限制)"""
|
||||||
|
|
||||||
_keys: List[Keyframe]
|
_keys: List[Keyframe]
|
||||||
"""关键帧列表"""
|
"""关键帧列表"""
|
||||||
|
|
||||||
@@ -239,6 +246,8 @@ class ParamCurve:
|
|||||||
[float], float
|
[float], float
|
||||||
] = InterpolationMethod.linear,
|
] = InterpolationMethod.linear,
|
||||||
boundary_mode: BoundaryBehaviour = BoundaryBehaviour.CONSTANT,
|
boundary_mode: BoundaryBehaviour = BoundaryBehaviour.CONSTANT,
|
||||||
|
value_min: Optional[float] = None,
|
||||||
|
value_max: Optional[float] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
初始化参数曲线。
|
初始化参数曲线。
|
||||||
@@ -247,19 +256,155 @@ class ParamCurve:
|
|||||||
----------
|
----------
|
||||||
base_value : float
|
base_value : float
|
||||||
边界外默认值(当 boundary_mode 为 BoundaryBehaviour.CONSTANT 时使用)。
|
边界外默认值(当 boundary_mode 为 BoundaryBehaviour.CONSTANT 时使用)。
|
||||||
default_interpolation_function : FittingFunctionType
|
default_interpolation_function : Callable
|
||||||
新关键帧的默认 out_interp。
|
新关键帧的默认 out_interp。
|
||||||
boundary_mode : BoundaryBehaviour
|
boundary_mode : BoundaryBehaviour
|
||||||
范围外行为:
|
范围外行为:
|
||||||
- BoundaryBehaviour.CONSTANT: 返回 base_value
|
- BoundaryBehaviour.CONSTANT: 返回 base_value
|
||||||
- BoundaryBehaviour.HOLD: 保持首/尾关键帧值
|
- BoundaryBehaviour.HOLD: 保持首/尾关键帧值
|
||||||
|
value_min : Optional[float]
|
||||||
|
参数值下限。None 表示无下限。
|
||||||
|
value_max : Optional[float]
|
||||||
|
参数值上限。None 表示无上限。
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
若 value_min 和 value_max 均非 None 且 value_min > value_max。
|
||||||
"""
|
"""
|
||||||
self.base_line = base_value
|
# 先设定值域,再设定 base_line(以便 base_line 受约束)
|
||||||
|
self._set_value_range_internal(value_min, value_max)
|
||||||
|
|
||||||
|
self.base_line = self._clamp_value(base_value)
|
||||||
self.base_interpolation_function = default_interpolation_function
|
self.base_interpolation_function = default_interpolation_function
|
||||||
self.boundary_behaviour = boundary_mode
|
self.boundary_behaviour = boundary_mode
|
||||||
|
|
||||||
self._keys: List[Keyframe] = []
|
self._keys: List[Keyframe] = []
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# 值域限制 相关
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _set_value_range_internal(
|
||||||
|
self,
|
||||||
|
value_min: Optional[float],
|
||||||
|
value_max: Optional[float],
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
内部方法:设定值域并校验合法性。
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
若 value_min > value_max。
|
||||||
|
"""
|
||||||
|
if value_min is not None and value_max is not None and value_min > value_max:
|
||||||
|
raise ValueError(f"值域下限 ({value_min}) 不得大于上限 ({value_max})。")
|
||||||
|
self.value_min = value_min
|
||||||
|
self.value_max = value_max
|
||||||
|
|
||||||
|
def _clamp_value(self, value: float) -> float:
|
||||||
|
"""
|
||||||
|
将给定值限制在 [value_min, value_max] 范围内。
|
||||||
|
仅在低频操作(add_key、update_key_value、set_value_range 等)中使用。
|
||||||
|
高频路径 value_at() 使用内联版本。
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
value : float
|
||||||
|
待限制的值。
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
限制后的值。若对应边界为 None,则该方向不做限制。
|
||||||
|
"""
|
||||||
|
if self.value_min is not None and value < self.value_min:
|
||||||
|
return self.value_min
|
||||||
|
if self.value_max is not None and value > self.value_max:
|
||||||
|
return self.value_max
|
||||||
|
return value
|
||||||
|
|
||||||
|
def set_value_range(
|
||||||
|
self,
|
||||||
|
value_min: Optional[float] = None,
|
||||||
|
value_max: Optional[float] = None,
|
||||||
|
clamp_existing_keys: bool = True,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
设置参数曲线的值域(最大/最小值限制)。
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
value_min : Optional[float]
|
||||||
|
参数值下限。传入 None 表示取消下限限制。
|
||||||
|
value_max : Optional[float]
|
||||||
|
参数值上限。传入 None 表示取消上限限制。
|
||||||
|
clamp_existing_keys : bool
|
||||||
|
若为 True(默认),则立即将已有关键帧的值及 base_line
|
||||||
|
限制到新的值域范围内。若为 False,则仅影响后续输出与新增关键帧。
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
若 value_min > value_max。
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
>>> curve = ParamCurve(base_value=0.5)
|
||||||
|
>>> curve.add_key(0.0, 1.5)
|
||||||
|
>>> curve.add_key(1.0, -0.3)
|
||||||
|
>>> curve.set_value_range(0.0, 1.0) # 限制到 [0, 1]
|
||||||
|
>>> curve.value_at(0.0)
|
||||||
|
1.0
|
||||||
|
>>> curve.value_at(1.0)
|
||||||
|
0.0
|
||||||
|
"""
|
||||||
|
self._set_value_range_internal(value_min, value_max)
|
||||||
|
|
||||||
|
if clamp_existing_keys:
|
||||||
|
# 限制 base_line
|
||||||
|
self.base_line = self._clamp_value(self.base_line)
|
||||||
|
# 限制所有已有关键帧的值
|
||||||
|
for i, key in enumerate(self._keys):
|
||||||
|
clamped = self._clamp_value(key.value)
|
||||||
|
if clamped != key.value:
|
||||||
|
self._keys[i] = Keyframe(
|
||||||
|
time=key.time,
|
||||||
|
value=clamped,
|
||||||
|
out_interp=key.out_interp,
|
||||||
|
in_tangent=key.in_tangent,
|
||||||
|
out_tangent=key.out_tangent,
|
||||||
|
use_bezier=key.use_bezier,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_value_range(self) -> Tuple[Optional[float], Optional[float]]:
|
||||||
|
"""
|
||||||
|
获取当前值域设定。
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Tuple[Optional[float], Optional[float]]
|
||||||
|
(value_min, value_max),None 表示该方向无限制。
|
||||||
|
"""
|
||||||
|
return (self.value_min, self.value_max)
|
||||||
|
|
||||||
|
def clear_value_range(self, clamp_existing_keys: bool = False):
|
||||||
|
"""
|
||||||
|
清除值域限制(恢复为无限制状态)。
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
clamp_existing_keys : bool
|
||||||
|
通常为 False(清除限制无需 clamp)。保留此参数仅为接口对称。
|
||||||
|
"""
|
||||||
|
self.value_min = None
|
||||||
|
self.value_max = None
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# 原有功能
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
|
||||||
def __bool__(self) -> bool:
|
def __bool__(self) -> bool:
|
||||||
return bool(self._keys) or (self.base_line != 0)
|
return bool(self._keys) or (self.base_line != 0)
|
||||||
|
|
||||||
@@ -301,6 +446,8 @@ class ParamCurve:
|
|||||||
self.base_line,
|
self.base_line,
|
||||||
self.base_interpolation_function,
|
self.base_interpolation_function,
|
||||||
self.boundary_behaviour,
|
self.boundary_behaviour,
|
||||||
|
value_min=self.value_min,
|
||||||
|
value_max=self.value_max,
|
||||||
)
|
)
|
||||||
if start >= end:
|
if start >= end:
|
||||||
return param_curve
|
return param_curve
|
||||||
@@ -374,7 +521,7 @@ class ParamCurve:
|
|||||||
time : float
|
time : float
|
||||||
关键帧时间。
|
关键帧时间。
|
||||||
value : float
|
value : float
|
||||||
参数值。
|
参数值(将自动受值域限制约束)。
|
||||||
out_interp : Optional[Callable]
|
out_interp : Optional[Callable]
|
||||||
出插值函数(若 use_bezier=False)。
|
出插值函数(若 use_bezier=False)。
|
||||||
in_tangent : Optional[Tuple[float, float]]
|
in_tangent : Optional[Tuple[float, float]]
|
||||||
@@ -391,11 +538,15 @@ class ParamCurve:
|
|||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
若时间已存在,更新该关键帧的所有属性。
|
若时间已存在,更新该关键帧的所有属性。
|
||||||
|
写入的 value 会被自动 clamp 到 [value_min, value_max]。
|
||||||
"""
|
"""
|
||||||
interp = (
|
interp = (
|
||||||
out_interp if out_interp is not None else self.base_interpolation_function
|
out_interp if out_interp is not None else self.base_interpolation_function
|
||||||
)
|
)
|
||||||
new_key = Keyframe(time, value, interp, in_tangent, out_tangent, use_bezier)
|
clamped_value = self._clamp_value(value)
|
||||||
|
new_key = Keyframe(
|
||||||
|
time, clamped_value, interp, in_tangent, out_tangent, use_bezier
|
||||||
|
)
|
||||||
|
|
||||||
idx, old_key = self.find_key(time)
|
idx, old_key = self.find_key(time)
|
||||||
if old_key:
|
if old_key:
|
||||||
@@ -421,12 +572,13 @@ class ParamCurve:
|
|||||||
del self._keys[idx]
|
del self._keys[idx]
|
||||||
|
|
||||||
def update_key_value(self, time: float, new_value: float):
|
def update_key_value(self, time: float, new_value: float):
|
||||||
"""更新关键帧值,保留其他属性。"""
|
"""更新关键帧值,保留其他属性。值受值域限制约束。"""
|
||||||
idx, key = self.find_key(time)
|
idx, key = self.find_key(time)
|
||||||
if key:
|
if key:
|
||||||
|
clamped_value = self._clamp_value(new_value)
|
||||||
self._keys[idx] = Keyframe(
|
self._keys[idx] = Keyframe(
|
||||||
time,
|
time,
|
||||||
new_value,
|
clamped_value,
|
||||||
key.out_interp,
|
key.out_interp,
|
||||||
key.in_tangent,
|
key.in_tangent,
|
||||||
key.out_tangent,
|
key.out_tangent,
|
||||||
@@ -471,7 +623,7 @@ class ParamCurve:
|
|||||||
|
|
||||||
def make_key_smooth(self, time: float):
|
def make_key_smooth(self, time: float):
|
||||||
"""
|
"""
|
||||||
将关键帧设为“平滑”模式(自动对称切线,并设为贝塞尔模式)。
|
将关键帧设为"平滑"模式(自动对称切线,并设为贝塞尔模式)。
|
||||||
切线长度基于相邻关键帧的时间和值差。
|
切线长度基于相邻关键帧的时间和值差。
|
||||||
"""
|
"""
|
||||||
idx, key = self.find_key(time)
|
idx, key = self.find_key(time)
|
||||||
@@ -524,6 +676,9 @@ class ParamCurve:
|
|||||||
"""
|
"""
|
||||||
计算时间 t 处的曲线值。
|
计算时间 t 处的曲线值。
|
||||||
|
|
||||||
|
返回值始终受 [value_min, value_max] 值域限制约束。
|
||||||
|
即使贝塞尔插值产生过冲(overshoot),输出也会被 clamp。
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
t : float
|
t : float
|
||||||
@@ -532,57 +687,61 @@ class ParamCurve:
|
|||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
float
|
float
|
||||||
插值结果。
|
插值结果(已 clamp)。
|
||||||
"""
|
"""
|
||||||
keys = self._keys
|
keys = self._keys
|
||||||
if not keys:
|
if not keys:
|
||||||
return self._get_boundary_value(t)
|
raw = self._get_boundary_value(t)
|
||||||
|
else:
|
||||||
|
if t < keys[0].time or t > keys[-1].time:
|
||||||
|
raw = self._get_boundary_value(t)
|
||||||
|
else:
|
||||||
|
times = [k.time for k in keys]
|
||||||
|
idx = bisect.bisect_right(times, t) - 1
|
||||||
|
|
||||||
if t < keys[0].time or t > keys[-1].time:
|
if idx < 0:
|
||||||
return self._get_boundary_value(t)
|
raw = self._get_boundary_value(t)
|
||||||
|
elif idx >= len(keys) - 1:
|
||||||
|
raw = keys[-1].value
|
||||||
|
else:
|
||||||
|
|
||||||
times = [k.time for k in keys]
|
k0 = keys[idx]
|
||||||
idx = bisect.bisect_right(times, t) - 1
|
k1 = keys[idx + 1]
|
||||||
|
|
||||||
if idx < 0:
|
if k0.time == k1.time:
|
||||||
return self._get_boundary_value(t)
|
raw = k0.value
|
||||||
if idx >= len(keys) - 1:
|
elif k0.time == t:
|
||||||
return keys[-1].value
|
raw = k0.value
|
||||||
|
elif k1.time == t:
|
||||||
|
raw = k1.value
|
||||||
|
else:
|
||||||
|
t0, v0 = k0.time, k0.value
|
||||||
|
t1, v1 = k1.time, k1.value
|
||||||
|
u = (t - t0) / (t1 - t0)
|
||||||
|
u = max(0.0, min(1.0, u))
|
||||||
|
|
||||||
k0 = keys[idx]
|
# 贝塞尔模式(高优先级)
|
||||||
k1 = keys[idx + 1]
|
if k0.use_bezier or k1.use_bezier:
|
||||||
|
raw = _evaluate_bezier_segment(
|
||||||
|
t0,
|
||||||
|
v0,
|
||||||
|
t1,
|
||||||
|
v1,
|
||||||
|
out_tangent=k0.out_tangent,
|
||||||
|
in_tangent=k1.in_tangent,
|
||||||
|
u=u,
|
||||||
|
)
|
||||||
|
# 函数插值模式,优先处理阶梯保持模式
|
||||||
|
elif k0.out_interp is InterpolationMethod.hold:
|
||||||
|
raw = v0
|
||||||
|
else:
|
||||||
|
interp_func = (
|
||||||
|
k0.out_interp or self.base_interpolation_function
|
||||||
|
)
|
||||||
|
v_norm = interp_func(u)
|
||||||
|
raw = v0 + v_norm * (v1 - v0)
|
||||||
|
|
||||||
if k0.time == k1.time:
|
return self._clamp_value(raw)
|
||||||
return k0.value
|
|
||||||
if k0.time == t:
|
|
||||||
return k0.value
|
|
||||||
if k1.time == t:
|
|
||||||
return k1.value
|
|
||||||
|
|
||||||
t0, v0 = k0.time, k0.value
|
|
||||||
t1, v1 = k1.time, k1.value
|
|
||||||
u = (t - t0) / (t1 - t0)
|
|
||||||
u = max(0.0, min(1.0, u))
|
|
||||||
|
|
||||||
# 贝塞尔模式(高优先级)
|
|
||||||
if k0.use_bezier or k1.use_bezier:
|
|
||||||
return _evaluate_bezier_segment(
|
|
||||||
t0,
|
|
||||||
v0,
|
|
||||||
t1,
|
|
||||||
v1,
|
|
||||||
out_tangent=k0.out_tangent,
|
|
||||||
in_tangent=k1.in_tangent, # ← 关键:使用下一帧的 in_tangent!
|
|
||||||
u=u,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 函数插值模式,优先处理阶梯保持模式
|
|
||||||
elif k0.out_interp is InterpolationMethod.hold:
|
|
||||||
return v0
|
|
||||||
|
|
||||||
interp_func = k0.out_interp or self.base_interpolation_function
|
|
||||||
v_norm = interp_func(u)
|
|
||||||
return v0 + v_norm * (v1 - v0)
|
|
||||||
|
|
||||||
def __call__(self, t: float) -> float:
|
def __call__(self, t: float) -> float:
|
||||||
return self.value_at(t)
|
return self.value_at(t)
|
||||||
@@ -606,11 +765,11 @@ class ParamCurve:
|
|||||||
mode : BoundaryBehaviour
|
mode : BoundaryBehaviour
|
||||||
边界行为设定
|
边界行为设定
|
||||||
base_value : Optional[float]
|
base_value : Optional[float]
|
||||||
当 mode=BoundaryBehaviour.CONSTANT 时,指定新的默认值。
|
当 mode=BoundaryBehaviour.CONSTANT 时,指定新的默认值(受值域约束)。
|
||||||
"""
|
"""
|
||||||
self.boundary_behaviour = mode
|
self.boundary_behaviour = mode
|
||||||
if base_value is not None:
|
if base_value is not None:
|
||||||
self.base_line = base_value
|
self.base_line = self._clamp_value(base_value)
|
||||||
|
|
||||||
def bake(
|
def bake(
|
||||||
self,
|
self,
|
||||||
@@ -619,41 +778,12 @@ class ParamCurve:
|
|||||||
sample_rate: Optional[float] = None,
|
sample_rate: Optional[float] = None,
|
||||||
num_samples: Optional[int] = None,
|
num_samples: Optional[int] = None,
|
||||||
dtype: Any = None,
|
dtype: Any = None,
|
||||||
) -> "np.ndarray": # type: ignore 这里这样用会报错吗?不知道,但是人工智能这样写了都,大抵是能用的吧
|
) -> "np.ndarray": # type: ignore
|
||||||
"""
|
"""
|
||||||
将参数曲线在指定时间范围内烘焙为 NumPy 数组,用于高性能实时查询或音频渲染。
|
将参数曲线在指定时间范围内烘焙为 NumPy 数组。
|
||||||
|
|
||||||
Parameters
|
当值域限制已启用时,使用 np.clip 进行向量化 clamp,
|
||||||
----------
|
避免逐样本调用 Python 层的 clamp 逻辑。
|
||||||
start : float
|
|
||||||
烘焙起始时间(包含)。
|
|
||||||
end : float
|
|
||||||
烘焙结束时间(不包含)。
|
|
||||||
sample_rate : Optional[float]
|
|
||||||
采样率(单位:样本/时间单位)。例如,若时间单位为秒,sample_rate=48000 表示每秒 48k 样本。
|
|
||||||
必须与 `num_samples` 二选一提供。
|
|
||||||
num_samples : Optional[int]
|
|
||||||
输出数组的总样本数。若提供,则忽略 `sample_rate`。
|
|
||||||
dtype : Any, optional
|
|
||||||
输出数组的数据类型(如 np.float32)。默认为 np.float64。
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
np.ndarray
|
|
||||||
一维 NumPy 数组,长度为 `num_samples`,`arr[i] ≈ curve(start + i / sample_rate)`。
|
|
||||||
|
|
||||||
Exceptions
|
|
||||||
----------
|
|
||||||
ValueError
|
|
||||||
- 若 `start >= end`
|
|
||||||
- 若未提供 `sample_rate` 且未提供 `num_samples`
|
|
||||||
- 若 `num_samples <= 0`
|
|
||||||
|
|
||||||
Notes
|
|
||||||
-----
|
|
||||||
- 内部使用 `np.linspace` 生成时间轴,然后逐点调用 `self.value_at(t)`。
|
|
||||||
- 虽然目前是 Python 循环,但对于典型自动化曲线(<1000 关键帧),NumPy 向量化优势主要体现在内存布局和后续处理。
|
|
||||||
- 如需极致性能(如 >1M 样本),可未来优化为 C++/Numba 加速,但当前已满足 DAW 自动化需求。
|
|
||||||
"""
|
"""
|
||||||
if start >= end:
|
if start >= end:
|
||||||
raise ValueError("起始值须小于结束值。")
|
raise ValueError("起始值须小于结束值。")
|
||||||
@@ -685,4 +815,9 @@ class ParamCurve:
|
|||||||
for i in range(n):
|
for i in range(n):
|
||||||
values[i] = self.value_at(float(times[i]))
|
values[i] = self.value_at(float(times[i]))
|
||||||
|
|
||||||
|
# 向量化 clamp —— 比逐点 Python 调用快一个数量级
|
||||||
|
vmin, vmax = self.value_min, self.value_max
|
||||||
|
if vmin is not None or vmax is not None:
|
||||||
|
np.clip(values, vmin, vmax, out=values)
|
||||||
|
|
||||||
return values
|
return values
|
||||||
|
|||||||
+1
-1
@@ -126,4 +126,4 @@
|
|||||||
|
|
||||||
|
|
||||||
[tool.pyright]
|
[tool.pyright]
|
||||||
typeCheckingMode = "basic"
|
typeCheckingMode = "standard"
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ print("当前支持的导出格式:", _global_plugin_registry.supported_output
|
|||||||
|
|
||||||
msct = MusiCreater.import_music(
|
msct = MusiCreater.import_music(
|
||||||
Path(input("文件路径:")).resolve(),
|
Path(input("文件路径:")).resolve(),
|
||||||
plugin_config=MidiImportConfig(),
|
plugin_config=MidiImportConfig(
|
||||||
|
divide_tracks_by_volume=True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
print("全局插件注册表:", _global_plugin_registry)
|
print("全局插件注册表:", _global_plugin_registry)
|
||||||
|
|||||||
Reference in New Issue
Block a user