mirror of
https://github.com/TriM-Organization/Musicreater.git
synced 2026-01-24 20:51:54 +00:00
帅
This commit is contained in:
@@ -4,8 +4,6 @@
|
||||
存储音·创新数据存储类
|
||||
"""
|
||||
|
||||
# WARNING 本文件中使用之功能尚未启用
|
||||
|
||||
"""
|
||||
版权所有 © 2025 金羿
|
||||
Copyright © 2025 Eilles
|
||||
@@ -18,42 +16,21 @@ Terms & Conditions: License.md in the root directory
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
# WARNING 本文件中使用之功能尚未启用
|
||||
|
||||
from math import sin, cos, asin, radians, degrees, sqrt, atan, inf
|
||||
from math import sin, cos, asin, radians, degrees, sqrt, atan, inf, ceil
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Any, List, Tuple, Union, Dict, Sequence, Callable
|
||||
import bisect
|
||||
from typing import Optional, Any, List, Tuple, Union, Dict, Sequence, Callable, Generator
|
||||
from enum import Enum
|
||||
|
||||
from .types import FittingFunctionType
|
||||
from .constants import MC_PITCHED_INSTRUMENT_LIST
|
||||
|
||||
|
||||
class ArgumentCurve:
|
||||
|
||||
base_line: float = 0
|
||||
"""基线/默认值"""
|
||||
|
||||
default_curve: Callable[[float], float]
|
||||
"""默认曲线"""
|
||||
|
||||
defined_curves: Dict[float, "ArgumentCurve"] = {}
|
||||
"""调整后的曲线集合"""
|
||||
|
||||
left_border: float = 0
|
||||
"""定义域左边界"""
|
||||
|
||||
right_border: float = inf
|
||||
"""定义域右边界"""
|
||||
|
||||
def __init__(self, baseline: float = 0, default_function: Callable[[float], float] = lambda x: 0, function_set: Dict = {}) -> None:
|
||||
pass
|
||||
|
||||
def __call__(self, *args: Any, **kwds: Any) -> Any:
|
||||
pass
|
||||
|
||||
from .paramcurve import ParamCurve
|
||||
|
||||
|
||||
class SoundAtmos:
|
||||
"""声源方位类"""
|
||||
|
||||
sound_distance: float
|
||||
"""声源距离 方块"""
|
||||
@@ -66,6 +43,20 @@ class SoundAtmos:
|
||||
distance: Optional[float] = None,
|
||||
azimuth: Optional[Tuple[float, float]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
定义一个发声方位
|
||||
|
||||
Parameters
|
||||
------------
|
||||
distance: float
|
||||
发声源距离玩家的距离(半径 `r`)
|
||||
注:距离越近,音量越高,默认为 0。此参数可以与音量成某种函数关系。
|
||||
azimuth: tuple[float, float]
|
||||
声源方位
|
||||
注:此参数为tuple,包含两个元素,分别表示:
|
||||
`rV` 发声源在竖直(上下)轴上,从玩家视角正前方开始,向顺时针旋转的角度
|
||||
`rH` 发声源在水平(左右)轴上,从玩家视角正前方开始,向上(到达玩家正上方顶点后变为向下,以此类推的旋转)旋转的角度
|
||||
"""
|
||||
|
||||
self.sound_azimuth = (azimuth[0] % 360, azimuth[1] % 360) if azimuth else (0, 0)
|
||||
"""声源方位"""
|
||||
@@ -103,7 +94,7 @@ class SoundAtmos:
|
||||
|
||||
@property
|
||||
def position_displacement(self) -> Tuple[float, float, float]:
|
||||
"""声像位移"""
|
||||
"""声像位移,直接可应用于我的世界的相对视角的坐标参考系中(^x ^y ^z)"""
|
||||
dk1 = self.sound_distance * round(cos(radians(self.sound_azimuth[1])), 8)
|
||||
return (
|
||||
-dk1 * round(sin(radians(self.sound_azimuth[0])), 8),
|
||||
@@ -161,14 +152,6 @@ class SingleNote:
|
||||
高精度的开始时间偏移量(1/1250秒)
|
||||
is_percussion: bool
|
||||
是否作为打击乐器
|
||||
distance: float
|
||||
发声源距离玩家的距离(半径 `r`)
|
||||
注:距离越近,音量越高,默认为 0。此参数可以与音量成某种函数关系。
|
||||
azimuth: tuple[float, float]
|
||||
声源方位
|
||||
注:此参数为tuple,包含两个元素,分别表示:
|
||||
`rV` 发声源在竖直(上下)轴上,从玩家视角正前方开始,向顺时针旋转的角度
|
||||
`rH` 发声源在水平(左右)轴上,从玩家视角正前方开始,向上(到达玩家正上方顶点后变为向下,以此类推的旋转)旋转的角度
|
||||
extra_information: Dict[str, Any]
|
||||
附加信息,尽量存储为字典
|
||||
|
||||
@@ -332,6 +315,17 @@ class SingleNote:
|
||||
return self.start_tick > other.start_tick
|
||||
|
||||
|
||||
class CurvableParam(str, Enum):
|
||||
"""可曲线化的参数枚举类"""
|
||||
PITCH = "note-pitch"
|
||||
VELOCITY = "note-velocity"
|
||||
VOLUME = "note-volume"
|
||||
DISTANCE = "note-sound-distance"
|
||||
HORIZONTAL_PANNING = "note-H-panning-degree"
|
||||
VERTICAL_PANNING = "note-V-panning-degree"
|
||||
|
||||
|
||||
|
||||
class SingleTrack(list):
|
||||
"""存储单个轨道的类"""
|
||||
|
||||
@@ -353,7 +347,7 @@ class SingleTrack(list):
|
||||
sound_position: SoundAtmos
|
||||
"""声像方位"""
|
||||
|
||||
argument_curves: Dict[str, FittingFunctionType]
|
||||
argument_curves: Dict[CurvableParam, ParamCurve]
|
||||
"""参数曲线"""
|
||||
|
||||
extra_info: Dict[str, Any]
|
||||
@@ -397,6 +391,11 @@ class SingleTrack(list):
|
||||
"""音符数"""
|
||||
return len(self)
|
||||
|
||||
@property
|
||||
def track_notes(self) -> List[SingleNote]:
|
||||
"""音符列表"""
|
||||
return self
|
||||
|
||||
def set_info(self, key: Union[str, Sequence[str]], value: Any):
|
||||
"""设置附加信息"""
|
||||
if isinstance(key, str):
|
||||
@@ -416,6 +415,90 @@ class SingleTrack(list):
|
||||
"""获取附加信息"""
|
||||
return self.extra_info.get(key, default)
|
||||
|
||||
class SingleMusic(object):
|
||||
|
||||
def get_notes(self) -> Generator[SingleNote, Any, None]:
|
||||
|
||||
# TODO : 添加其他参数以及参数曲线到这里来
|
||||
for note in self:
|
||||
yield note
|
||||
|
||||
|
||||
class SingleMusic(list):
|
||||
"""存储单个曲子的类"""
|
||||
|
||||
music_name: str
|
||||
"""乐曲名称"""
|
||||
|
||||
music_creator: str
|
||||
"""本我的世界曲目的制作者"""
|
||||
|
||||
music_original_author: str
|
||||
"""曲目的原作者"""
|
||||
|
||||
music_description: str
|
||||
"""当前曲目的简介"""
|
||||
|
||||
music_credits: str
|
||||
"""曲目的版权信息"""
|
||||
|
||||
# 感叹一下什么交冗余设计啊!(叉腰)
|
||||
extra_info: Dict[str, Any]
|
||||
"""这还得放东西?"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "未命名乐曲",
|
||||
creator: str = "未命名制作者",
|
||||
original_author: str = "未命名原作者",
|
||||
description: str = "未命名简介",
|
||||
credits: str = "未命名版权信息",
|
||||
*args: SingleTrack,
|
||||
extra_information: Dict[str, Any] = {},
|
||||
):
|
||||
self.music_name = name
|
||||
"""乐曲名称"""
|
||||
|
||||
self.music_creator = creator
|
||||
"""曲目制作者"""
|
||||
|
||||
self.music_original_author = original_author
|
||||
"""乐曲原作者"""
|
||||
|
||||
self.music_description = description
|
||||
"""简介"""
|
||||
|
||||
self.music_credits = credits
|
||||
"""版权信息"""
|
||||
|
||||
self.extra_info = extra_information if extra_information else {}
|
||||
|
||||
super().__init__(*args)
|
||||
|
||||
@property
|
||||
def track_amount(self) -> int:
|
||||
"""音轨数"""
|
||||
return len(self)
|
||||
|
||||
@property
|
||||
def music_tracks(self) -> List[SingleTrack]:
|
||||
"""音轨列表"""
|
||||
return self
|
||||
|
||||
def set_info(self, key: Union[str, Sequence[str]], value: Any):
|
||||
"""设置附加信息"""
|
||||
if isinstance(key, str):
|
||||
self.extra_info[key] = value
|
||||
elif (
|
||||
isinstance(key, Sequence)
|
||||
and isinstance(value, Sequence)
|
||||
and (k := len(key)) == len(value)
|
||||
):
|
||||
for i in range(k):
|
||||
self.extra_info[key[i]] = value[i]
|
||||
else:
|
||||
# 提供简单报错就行了,如果放一堆 if 语句,降低处理速度
|
||||
raise TypeError("参数类型错误;键:`{}` 值:`{}`".format(key, value))
|
||||
|
||||
def get_info(self, key: str, default: Any = None) -> Any:
|
||||
"""获取附加信息"""
|
||||
return self.extra_info.get(key, default)
|
||||
|
||||
580
Musicreater/paramcurve.py
Normal file
580
Musicreater/paramcurve.py
Normal file
@@ -0,0 +1,580 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
存储音·创音轨所需的参数曲线
|
||||
"""
|
||||
|
||||
"""
|
||||
版权所有 © 2025 金羿
|
||||
Copyright © 2025 Eilles
|
||||
|
||||
开源相关声明请见 仓库根目录下的 License.md
|
||||
Terms & Conditions: License.md in the root directory
|
||||
"""
|
||||
|
||||
# 睿乐组织 开发交流群 861684859
|
||||
# Email TriM-Organization@hotmail.com
|
||||
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md
|
||||
|
||||
|
||||
# WARNING 本文件中使用之功能尚未启用
|
||||
# 鉴于白谭若佬给出的建议:本功能应是处于低优先级开发的
|
||||
# 因此暂时用处不大,可以稍微放一会再进行开发
|
||||
# 目前用人工智能生成了部分代码,只经过简单的测试
|
||||
# 可以等伶伦工作站开发出来后再进行完整的测试
|
||||
|
||||
|
||||
from math import ceil
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Any, List, Tuple
|
||||
from enum import Enum
|
||||
import bisect
|
||||
|
||||
from .types import FittingFunctionType
|
||||
|
||||
|
||||
def _evaluate_bezier_segment(
|
||||
t0: float,
|
||||
v0: float,
|
||||
t1: float,
|
||||
v1: float,
|
||||
out_tangent: Optional[Tuple[float, float]],
|
||||
in_tangent: Optional[Tuple[float, float]],
|
||||
u: float,
|
||||
) -> float:
|
||||
"""
|
||||
计算贝塞尔区间 [t0, t1] 在归一化参数 u ∈ [0,1] 处的 y 值。
|
||||
|
||||
控制点:
|
||||
P0 = (t0, v0)
|
||||
P1 = (t0 + out_dt, v0 + out_dv)
|
||||
P2 = (t1 - in_dt, v1 - in_dv) ← 注意:in_tangent 是相对于 t1 的偏移
|
||||
P3 = (t1, v1)
|
||||
"""
|
||||
# 默认控制点:退化为线性
|
||||
p0 = (t0, v0)
|
||||
p3 = (t1, v1)
|
||||
|
||||
if out_tangent is not None:
|
||||
p1 = (t0 + out_tangent[0], v0 + out_tangent[1])
|
||||
else:
|
||||
p1 = p0 # 无出手柄 → 与起点重合
|
||||
|
||||
if in_tangent is not None:
|
||||
p2 = (t1 - in_tangent[0], v1 - in_tangent[1])
|
||||
else:
|
||||
p2 = p3 # 无入手柄 → 与终点重合
|
||||
|
||||
# 三次贝塞尔 y(t)
|
||||
mt = 1.0 - u
|
||||
return mt**3 * p0[1] + 3 * mt**2 * u * p1[1] + 3 * mt * u**2 * p2[1] + u**3 * p3[1]
|
||||
|
||||
|
||||
class InterpolationMethod:
|
||||
"""
|
||||
预定义的标准化插值函数集合。所有函数接受归一化输入 u ∈ [0,1],返回 v ∈ [0,1]。
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def linear(u: float) -> float:
|
||||
"""
|
||||
线性插值。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u : float
|
||||
归一化时间,范围 [0, 1]。
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
插值权重,范围 [0, 1]。
|
||||
"""
|
||||
return u
|
||||
|
||||
@staticmethod
|
||||
def ease_in_quad(u: float) -> float:
|
||||
"""
|
||||
二次缓入(慢进快出)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u : float
|
||||
归一化时间,范围 [0, 1]。
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
插值权重。
|
||||
"""
|
||||
return u * u
|
||||
|
||||
@staticmethod
|
||||
def ease_out_quad(u: float) -> float:
|
||||
"""
|
||||
二次缓出(快进慢出)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u : float
|
||||
归一化时间,范围 [0, 1]。
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
插值权重。
|
||||
"""
|
||||
return 1 - (1 - u) ** 2
|
||||
|
||||
@staticmethod
|
||||
def ease_in_out_quad(u: float) -> float:
|
||||
"""
|
||||
二次缓入缓出。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u : float
|
||||
归一化时间,范围 [0, 1]。
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
插值权重。
|
||||
"""
|
||||
if u < 0.5:
|
||||
return 2 * u * u
|
||||
else:
|
||||
return 1 - pow(-2 * u + 2, 2) / 2
|
||||
|
||||
@staticmethod
|
||||
def hold(u: float) -> float:
|
||||
"""
|
||||
阶梯保持模式占位函数。实际插值逻辑在 ParamCurve.value_at 中特殊处理。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
u : float
|
||||
归一化时间(忽略)。
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
无意义,仅作标识。
|
||||
"""
|
||||
return 0.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class Keyframe:
|
||||
"""
|
||||
参数曲线上的一个关键帧,支持完整的入/出切线控制。
|
||||
|
||||
插值优先级:
|
||||
1. 若 use_bezier=True → 使用贝塞尔模式(需 in_tangent / out_tangent)
|
||||
2. 否则 → 使用 out_interp 函数(in_interp 被忽略)
|
||||
"""
|
||||
|
||||
time: float
|
||||
value: float
|
||||
|
||||
# 函数插值模式
|
||||
out_interp: Optional[FittingFunctionType] = None
|
||||
|
||||
# 贝塞尔模式
|
||||
in_tangent: Optional[Tuple[float, float]] = (
|
||||
None # (dt, dv) ← 相对于自身(负 dt 表示左侧)
|
||||
)
|
||||
out_tangent: Optional[Tuple[float, float]] = (
|
||||
None # (dt, dv) → 相对于自身(正 dt 表示右侧)
|
||||
)
|
||||
use_bezier: bool = False
|
||||
|
||||
|
||||
class BoundaryBehaviour(str, Enum):
|
||||
"""
|
||||
边界行为枚举。
|
||||
"""
|
||||
|
||||
CONSTANT = "constant"
|
||||
"""返回默认基线值"""
|
||||
HOLD = "hold"
|
||||
"""保持首/尾关键帧的值"""
|
||||
|
||||
|
||||
class ParamCurve:
|
||||
"""
|
||||
参数曲线类
|
||||
"""
|
||||
|
||||
"""
|
||||
支持动态节点编辑
|
||||
用户通过添加/修改关键帧(时间-值对)来定义曲线,类自动在相邻关键帧之间生成插值段。
|
||||
支持多种插值模式:线性('linear')、平滑缓动('smooth')、保持('hold')或自定义函数。
|
||||
"""
|
||||
|
||||
base_line: float = 0.0
|
||||
"""基线/默认值"""
|
||||
|
||||
base_interpolation_function: FittingFunctionType
|
||||
"""默认(未指定区间时的)关键帧插值模式"""
|
||||
|
||||
boundary_behaviour: BoundaryBehaviour
|
||||
"""边界行为,控制参数曲线在已定义的范围外的返回值"""
|
||||
|
||||
_keys: List[Keyframe]
|
||||
"""关键帧列表"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_value: float = 0.0,
|
||||
default_interpolation_function: FittingFunctionType = InterpolationMethod.linear,
|
||||
boundary_mode: BoundaryBehaviour = BoundaryBehaviour.CONSTANT,
|
||||
):
|
||||
"""
|
||||
初始化参数曲线。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
base_value : float
|
||||
边界外默认值(当 boundary_mode 为 BoundaryBehaviour.CONSTANT 时使用)。
|
||||
default_interp : FittingFunctionType
|
||||
新关键帧的默认 out_interp。
|
||||
boundary_mode : BoundaryBehaviour
|
||||
范围外行为:
|
||||
- BoundaryBehaviour.CONSTANT: 返回 base_value
|
||||
- BoundaryBehaviour.HOLD: 保持首/尾关键帧值
|
||||
"""
|
||||
self.base_line = base_value
|
||||
self.base_interpolation_function = default_interpolation_function
|
||||
self.boundary_behaviour = boundary_mode
|
||||
|
||||
self._keys: List[Keyframe] = []
|
||||
|
||||
def add_key(
|
||||
self,
|
||||
time: float,
|
||||
value: float,
|
||||
out_interp: Optional[FittingFunctionType] = None,
|
||||
in_tangent: Optional[Tuple[float, float]] = None,
|
||||
out_tangent: Optional[Tuple[float, float]] = None,
|
||||
use_bezier: bool = False,
|
||||
):
|
||||
"""
|
||||
添加或更新关键帧。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
time : float
|
||||
关键帧时间。
|
||||
value : float
|
||||
参数值。
|
||||
out_interp : Optional[Callable]
|
||||
出插值函数(若 use_bezier=False)。
|
||||
in_tangent : Optional[Tuple[float, float]]
|
||||
入切线偏移 (dt, dv)。dt 通常为负(表示左侧),但存储为绝对偏移。
|
||||
out_tangent : Optional[Tuple[float, float]]
|
||||
出切线偏移 (dt, dv)。dt 通常为正。
|
||||
use_bezier : bool
|
||||
是否使用贝塞尔插值。
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
|
||||
Notes
|
||||
-----
|
||||
若时间已存在,更新该关键帧的所有属性。
|
||||
"""
|
||||
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)
|
||||
|
||||
idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time)
|
||||
if idx < len(self._keys) and self._keys[idx].time == time:
|
||||
self._keys[idx] = new_key
|
||||
else:
|
||||
self._keys.insert(idx, new_key)
|
||||
|
||||
def remove_key(self, time: float):
|
||||
"""
|
||||
移除指定时间的关键帧。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
time : float
|
||||
要移除的关键帧时间。
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
idx = bisect.bisect_left(self._keys, time, key=lambda k: k.time)
|
||||
if idx < len(self._keys) and self._keys[idx].time == time:
|
||||
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]
|
||||
self._keys[idx] = Keyframe(
|
||||
time, new_value, k.out_interp, k.in_tangent, k.out_tangent, k.use_bezier
|
||||
)
|
||||
|
||||
def update_key_interp(
|
||||
self,
|
||||
time: float,
|
||||
out_interp: Optional[FittingFunctionType] = None,
|
||||
in_tangent: Optional[Tuple[float, float]] = None,
|
||||
out_tangent: Optional[Tuple[float, float]] = None,
|
||||
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
|
||||
self._keys[idx] = Keyframe(
|
||||
time, new_value, interp, in_tangent, out_tangent, use_bezier
|
||||
)
|
||||
|
||||
def set_key_tangents(
|
||||
self,
|
||||
time: float,
|
||||
in_tangent: Optional[Tuple[float, float]] = None,
|
||||
out_tangent: Optional[Tuple[float, float]] = None,
|
||||
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]
|
||||
self._keys[idx] = Keyframe(
|
||||
time,
|
||||
k.value,
|
||||
out_interp=k.out_interp,
|
||||
in_tangent=in_tangent,
|
||||
out_tangent=out_tangent,
|
||||
use_bezier=use_bezier,
|
||||
)
|
||||
|
||||
def make_key_smooth(self, time: 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]
|
||||
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
|
||||
|
||||
# 默认切线长度:时间差的 1/3,值差按比例
|
||||
dt_in = dt_out = 0.1
|
||||
dv_in = dv_out = 0.0
|
||||
|
||||
if prev_k and next_k:
|
||||
dt_total = next_k.time - prev_k.time
|
||||
dv_total = next_k.value - prev_k.value
|
||||
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_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_out = dt_in
|
||||
dv_out = dv_in
|
||||
|
||||
self.set_key_tangents(
|
||||
time,
|
||||
in_tangent=(-dt_in, -dv_in), # in_tangent 存储为偏移,使用时做减法
|
||||
out_tangent=(dt_out, dv_out),
|
||||
use_bezier=True,
|
||||
)
|
||||
|
||||
def _get_boundary_value(self, t: float) -> float:
|
||||
"""根据 boundary_mode 获取范围外的值。"""
|
||||
if not self._keys:
|
||||
return self.base_line
|
||||
if self.boundary_behaviour == BoundaryBehaviour.CONSTANT:
|
||||
return self.base_line
|
||||
elif self.boundary_behaviour == BoundaryBehaviour.HOLD:
|
||||
if t < self._keys[0].time:
|
||||
return self._keys[0].value
|
||||
else:
|
||||
return self._keys[-1].value
|
||||
else: # 可能会有别的模式吗?
|
||||
return self.base_line
|
||||
|
||||
def value_at(self, t: float) -> float:
|
||||
"""
|
||||
计算时间 t 处的曲线值。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
t : float
|
||||
查询时间。
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
插值结果。
|
||||
"""
|
||||
keys = self._keys
|
||||
if not keys:
|
||||
return self._get_boundary_value(t)
|
||||
|
||||
if t < keys[0].time or t > keys[-1].time:
|
||||
return self._get_boundary_value(t)
|
||||
|
||||
times = [k.time for k in keys]
|
||||
idx = bisect.bisect_right(times, t) - 1
|
||||
|
||||
if idx < 0:
|
||||
return self._get_boundary_value(t)
|
||||
if idx >= len(keys) - 1:
|
||||
return keys[-1].value
|
||||
|
||||
k0 = keys[idx]
|
||||
k1 = keys[idx + 1]
|
||||
|
||||
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)
|
||||
|
||||
def __call__(self, t: float) -> float:
|
||||
return self.value_at(t)
|
||||
|
||||
def get_all_keys(self) -> List[Tuple[float, float]]:
|
||||
"""返回 (time, value) 列表。"""
|
||||
return [(k.time, k.value) for k in self._keys]
|
||||
|
||||
def set_default_interpolation_function(self, interp_func: FittingFunctionType):
|
||||
"""设置默认插值函数。"""
|
||||
self.base_interpolation_function = interp_func
|
||||
|
||||
def set_boundary_mode(
|
||||
self, mode: BoundaryBehaviour, base_value: Optional[float] = None
|
||||
):
|
||||
"""
|
||||
设置边界行为。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mode : BoundaryBehaviour
|
||||
边界行为设定
|
||||
base_value : Optional[float]
|
||||
当 mode=BoundaryBehaviour.CONSTANT 时,指定新的默认值。
|
||||
"""
|
||||
self.boundary_behaviour = mode
|
||||
if base_value is not None:
|
||||
self.base_line = base_value
|
||||
|
||||
def bake(
|
||||
self,
|
||||
start: float,
|
||||
end: float,
|
||||
sample_rate: Optional[float] = None,
|
||||
num_samples: Optional[int] = None,
|
||||
dtype: Any = None,
|
||||
) -> "np.ndarray": # type: ignore 这里这样用会报错吗?不知道,但是人工智能这样写了都,大抵是能用的吧
|
||||
"""
|
||||
将参数曲线在指定时间范围内烘焙为 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 自动化需求。
|
||||
"""
|
||||
if start >= end:
|
||||
raise ValueError("起始值须小于结束值。")
|
||||
|
||||
if num_samples is not None:
|
||||
if num_samples <= 0:
|
||||
raise ValueError("烘焙的采样数须为非零自然数。")
|
||||
n = num_samples
|
||||
elif sample_rate is not None:
|
||||
if sample_rate <= 0:
|
||||
raise ValueError("烘焙的采样率须为正值。")
|
||||
duration = end - start
|
||||
n = int(ceil(duration * sample_rate))
|
||||
# 别因为小数数值会产生的问题而越界了来着
|
||||
if n == 0:
|
||||
n = 1
|
||||
else:
|
||||
raise ValueError("烘焙参数时,须提供采样率或采样数。")
|
||||
|
||||
import numpy as np
|
||||
|
||||
# 生成对应时间的节点:[start, ..., end - dt]
|
||||
times = np.linspace(start, end, n, endpoint=False)
|
||||
|
||||
# 计算每个时间节点上的参数值
|
||||
# 我们认为在数字音频工作站的环境里,此值可能最多到 ~1e6 的样子,因此这样 for 一下应当可以接受
|
||||
# WARNING: 人工智能是这样理解的,如果有问题的话后续可能需要更改
|
||||
values = np.empty(n, dtype=dtype or np.float64)
|
||||
for i in range(n):
|
||||
values[i] = self.value_at(float(times[i]))
|
||||
|
||||
return values
|
||||
@@ -47,6 +47,7 @@
|
||||
full = [
|
||||
"TrimMCStruct <= 0.0.5.9",
|
||||
"brotli >= 1.0.0",
|
||||
"numpy"
|
||||
]
|
||||
dev = [
|
||||
"TrimMCStruct <= 0.0.5.9",
|
||||
|
||||
Reference in New Issue
Block a user