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