From 6940a211b401a482485f25fdb02cf3c314d2a759 Mon Sep 17 00:00:00 2001 From: Eilles Date: Fri, 3 Jul 2026 03:04:40 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=99=E9=83=A8=E5=88=86=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E5=86=85=E5=AE=B9=E5=A2=9E=E8=AE=BEcopy?= =?UTF-8?q?=E5=92=8Cdelete=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE.md | 6 +- Musicreater/data.py | 90 ++++++++++++++++++++- Musicreater/paramcurve.py | 161 +++++++++++++++++++++++++++++++------- 3 files changed, 225 insertions(+), 32 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 94c54c8..55c0f7f 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # 汉钰律许可协议,第一版 -**总第一版 第二次修订 · 二〇二四年七月七日编 二〇二五年四月二十六日修订** +**总第一版 第三次修订 · 二〇二四年七月七日编 二〇二六年七月三日修订** ## 一、重要须知 @@ -60,7 +60,7 @@ 11. 如果用户对任何民事主体,因其在进行本协议所授权进行的活动中侵犯该用户的专利而提起诉讼,那么根据本协议授予该用户的所有关于此作品的任何其他专利许可将在提起上述诉讼之日起终止。 -12. 如果本作品作为用户的其他作品的不可分割的一部分进行任何民事活动,本协议依旧对本作品(即该用户的其他作品的一部分)生效;若本作品完全融入该用户的其他作品之中而不可独立存在,则该用户需要保证其作品存在与本协议冲突的条款;除非该作品已获得此作品颁发者的特殊许可、或该用户即为此作品颁发者本人。 +12. 如果本作品作为用户的其他作品的不可分割的一部分进行任何民事活动,本协议依旧对本作品(即该用户的其他作品的一部分)生效;若本作品完全融入该用户的其他作品之中而不可独立存在,则该用户需要保证其作品不存在与本协议冲突的条款;除非该作品已获得此作品颁发者的特殊许可、或该用户即为此作品颁发者本人。 ## 四、使用条件 @@ -90,7 +90,7 @@ ## 六、商标相关 -本协议并未授予用户,将颁发者的商标、专属标记或特定产品名称,用于合理的或惯例性的描述或此类声明之外其他任何位置的权利。 +本协议不允许用户,在合理或惯例性的描述、或者用于此类声明之外,在任何其他位置使用颁发者的商标、专用标记或特定产品名称。 ## 七、免责声明 diff --git a/Musicreater/data.py b/Musicreater/data.py index 37b4ff8..5b46986 100644 --- a/Musicreater/data.py +++ b/Musicreater/data.py @@ -131,6 +131,9 @@ class SoundAtmos: self.sound_distance, *self.sound_azimuth ) + def copy(self) -> "SoundAtmos": + return SoundAtmos(self.sound_distance, self.sound_azimuth) + @dataclass(init=False) class SingleNote: @@ -182,7 +185,7 @@ class SingleNote: is_percussion: bool 是否作为打击乐器 extra_information: Dict[str, Any] - 附加信息,尽量存储为字典 + 附加信息,尽量存储为字典,该内容不被任何“复制”方法保存 Returns --------- @@ -202,6 +205,8 @@ class SingleNote: self.extra_info = extra_information if extra_information else {} + + @classmethod def decode(cls, code_buffer: bytes, is_high_time_precision: bool = True): """自字节码析出 SingleNote 类""" @@ -308,6 +313,15 @@ class SingleNote: ", ExtraData = {})".format(self.extra_info) if include_extra_data else ")" ) + def copy(self) -> "SingleNote": + return SingleNote( + note_pitch=self.midi_pitch, + note_volume=self.volume, + start_tick=self.start_time, + keep_tick=self.duration, + mass_precision_time=self.high_precision_start_time, + ) + # def __list__(self) -> List[int]: # 我不认为这个类应当被作为列表使用 @@ -476,6 +490,30 @@ class SingleTrack(List[SingleNote]): super().__init__(*args) super().sort() + @classmethod + def from_note_list( + cls, + note_list: Iterable[SingleNote], + track_name: str = "未命名音轨", + track_instrument: str = "", + precise_time: bool = True, + percussion: bool = False, + sound_direction: Optional[SoundAtmos] = None, + extra_information: Dict[str, Any] = {}, + ) -> "SingleTrack": + """从音符列表创建 SingleTrack 对象""" + + single_track = cls( + track_name=track_name, + track_instrument=track_instrument, + precise_time=precise_time, + percussion=percussion, + sound_direction=sound_direction, + extra_information=extra_information, + ) + single_track.update(note_list) + return single_track + def disable(self) -> None: """禁用音轨""" @@ -519,8 +557,56 @@ class SingleTrack(List[SingleNote]): super().extend(items) super().sort() # =========================== TODO 需要优化 + def copy( + self, + start_time: float = 0, + end_time: float = inf, + with_argument_curve: bool = False, + keep_zone_boundary_argument_value: bool = False, + global_boundary_argument_safe: bool = True, + ) -> "SingleTrack": + """ + 通过时间范围来复制音轨,返回一个音轨 + """ + single_track = SingleTrack.from_note_list( + [x for x in self if start_time <= x.start_time <= end_time], + track_instrument=self.instrument, + precise_time=self.is_high_time_precision, + percussion=self.is_percussive, + sound_direction=self.sound_position.copy(), + ) + if with_argument_curve: + single_track.argument_curves = { + item: ( + None + if curve is None + else curve.copy( + start=start_time, + end=end_time, + keep_zone_boundary_value=keep_zone_boundary_argument_value, + global_boundary_safe=global_boundary_argument_safe, + ) + ) + for item, curve in self.argument_curves.items() + } + return single_track + + def delete( + self, start_time: float, end_time: float, with_argument_curve: bool = False + ): + """ + 删除时间范围内的音符 + """ + if with_argument_curve: + for curve in self.argument_curves.values(): + if curve is not None: + curve.delete(start_time, end_time) + for i, note in enumerate(self): + if start_time <= note.start_time <= end_time: + del self[i] + def get(self, time: int) -> Generator[SingleNote, None, None]: - """通过开始时间来获取音符""" + """通过确切的开始时间来获取音符""" return (x for x in self if x.start_time == time) diff --git a/Musicreater/paramcurve.py b/Musicreater/paramcurve.py index d8645b7..d64ca18 100644 --- a/Musicreater/paramcurve.py +++ b/Musicreater/paramcurve.py @@ -24,7 +24,7 @@ Terms & Conditions: License.md in the root directory # 可以等伶伦工作站开发出来后再进行完整的测试 -from math import ceil +from math import ceil, inf from dataclasses import dataclass from typing import Optional, Any, List, Tuple, Callable from enum import Enum @@ -187,6 +187,16 @@ class Keyframe: ) use_bezier: bool = False + def copy(self) -> "Keyframe": + return Keyframe( + time=self.time, + value=self.value, + out_interp=self.out_interp, + in_tangent=self.in_tangent, + out_tangent=self.out_tangent, + use_bezier=self.use_bezier, + ) + class BoundaryBehaviour(str, Enum): """ @@ -225,7 +235,9 @@ class ParamCurve: def __init__( self, base_value: float = 0.0, - default_interpolation_function: Callable[[float], float] = InterpolationMethod.linear, + default_interpolation_function: Callable[ + [float], float + ] = InterpolationMethod.linear, boundary_mode: BoundaryBehaviour = BoundaryBehaviour.CONSTANT, ): """ @@ -251,6 +263,100 @@ class ParamCurve: def __bool__(self) -> bool: return bool(self._keys) or (self.base_line != 0) + def find_key(self, time: float) -> Tuple[int, Optional[Keyframe]]: + idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time) + if idx < len(self._keys) and self._keys[idx].time == time: + return idx, self._keys[idx] + else: + print("[警告] ParamCurve.find_key: 找不到指定时间点所对应之关键帧") + return idx, None + + def copy( + self, + start: float = 0, + end: float = inf, + keep_zone_boundary_value: bool = False, + global_boundary_safe: bool = True, + ) -> "ParamCurve": + """ + 返回参数曲线在某时间段内的副本 + + Parameters + ---------- + start : float + 起始时间。 + end : float + 结束时间。 + keep_zone_boundary_value : bool + 是否保留边界值(即在副本中的当前选区之边界创建以当前曲线值作为值的关键帧)。 + global_boundary_safe : bool + 当保留边界值启用时,是否忽略本身就处于参数曲线边界外的值,使其不创建关键帧。 + + Returns + ------- + ParamCurve + 参数曲线之副本 + """ + param_curve = ParamCurve( + self.base_line, + self.base_interpolation_function, + self.boundary_behaviour, + ) + if start >= end: + return param_curve + start_index, starter_keyframe = self.find_key(start) + end_index, ender_keyframe = self.find_key(end) + if starter_keyframe and ender_keyframe: + param_curve._keys = [ + key.copy() for key in self._keys[start_index : end_index + 1] + ] + elif starter_keyframe: + if end_index >= len(self._keys) and global_boundary_safe: + param_curve._keys = [key.copy() for key in self._keys[start_index:]] + else: + param_curve._keys = [ + key.copy() for key in self._keys[start_index:end_index] + ] + if keep_zone_boundary_value: + param_curve.add_key(end, self.value_at(end)) + elif ender_keyframe: + if start_index <= 0 and global_boundary_safe: + param_curve._keys = [key.copy() for key in self._keys[:end_index]] + else: + param_curve._keys = [ + key.copy() for key in self._keys[start_index : end_index + 1] + ] + if keep_zone_boundary_value: + param_curve.add_key(start, self.value_at(start)) + else: + if ( + start_index <= 0 + and end_index >= len(self._keys) + and global_boundary_safe + ): + param_curve._keys = [key.copy() for key in self._keys] + else: + param_curve._keys = [ + key.copy() for key in self._keys[start_index:end_index] + ] + if keep_zone_boundary_value: + param_curve.add_key(start, self.value_at(start)) + param_curve.add_key(end, self.value_at(end)) + return param_curve + + def delete(self, start: float, end: float): + """ + 删除参数曲线在某时间段内的关键帧 + """ + if start > end: + return + start_index, starter_keyframe = self.find_key(start) + end_index, ender_keyframe = self.find_key(end) + if ender_keyframe: + del self._keys[start_index : end_index + 1] + else: + del self._keys[start_index:end_index] + def add_key( self, time: float, @@ -291,8 +397,8 @@ class ParamCurve: ) new_key = Keyframe(time, value, interp, in_tangent, out_tangent, use_bezier) - idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time) - if idx < len(self._keys) and self._keys[idx].time == time: + idx, old_key = self.find_key(time) + if old_key: self._keys[idx] = new_key else: self._keys.insert(idx, new_key) @@ -310,17 +416,21 @@ class ParamCurve: ------- None """ - idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time) - if idx < len(self._keys) and self._keys[idx].time == time: + idx, key = self.find_key(time) + if key: del self._keys[idx] def update_key_value(self, time: float, new_value: float): """更新关键帧值,保留其他属性。""" - idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time) - if idx < len(self._keys) and self._keys[idx].time == time: - k = self._keys[idx] + idx, key = self.find_key(time) + if key: self._keys[idx] = Keyframe( - time, new_value, k.out_interp, k.in_tangent, k.out_tangent, k.use_bezier + time, + new_value, + key.out_interp, + key.in_tangent, + key.out_tangent, + key.use_bezier, ) def update_key_interp( @@ -332,11 +442,10 @@ class ParamCurve: use_bezier: bool = False, ): """更新关键帧的插值属性。""" - idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time) - if idx < len(self._keys) and self._keys[idx].time == time: - k = self._keys[idx] - new_value = k.value - interp = out_interp if out_interp is not None else k.out_interp + idx, key = self.find_key(time) + if key: + new_value = key.value + interp = out_interp if out_interp is not None else key.out_interp self._keys[idx] = Keyframe( time, new_value, interp, in_tangent, out_tangent, use_bezier ) @@ -349,13 +458,12 @@ class ParamCurve: use_bezier: bool = True, ): """单独设置关键帧的切线,不改变值。""" - idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time) - if idx < len(self._keys) and self._keys[idx].time == time: - k = self._keys[idx] + idx, key = self.find_key(time) + if key: self._keys[idx] = Keyframe( time, - k.value, - out_interp=k.out_interp, + key.value, + out_interp=key.out_interp, in_tangent=in_tangent, out_tangent=out_tangent, use_bezier=use_bezier, @@ -366,9 +474,8 @@ class ParamCurve: 将关键帧设为“平滑”模式(自动对称切线,并设为贝塞尔模式)。 切线长度基于相邻关键帧的时间和值差。 """ - idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time) - if idx < len(self._keys) and self._keys[idx].time == time: - k = self._keys[idx] + idx, key = self.find_key(time) + if key: prev_k = self._keys[idx - 1] if idx > 0 else None next_k = self._keys[idx + 1] if idx + 1 < len(self._keys) else None @@ -382,13 +489,13 @@ class ParamCurve: dt_in = dt_out = dt_total / 3.0 dv_in = dv_out = dv_total / 3.0 elif prev_k: - dt_out = (k.time - prev_k.time) / 2.0 - dv_out = (k.value - prev_k.value) / 2.0 + dt_out = (key.time - prev_k.time) / 2.0 + dv_out = (key.value - prev_k.value) / 2.0 dt_in = dt_out dv_in = dv_out elif next_k: - dt_in = (next_k.time - k.time) / 2.0 - dv_in = (next_k.value - k.value) / 2.0 + dt_in = (next_k.time - key.time) / 2.0 + dv_in = (next_k.value - key.value) / 2.0 dt_out = dt_in dv_out = dv_in