-
Notifications
You must be signed in to change notification settings - Fork 0
Home
本文档旨在为使用 PySimaiParser 库的开发者提供技术参考。PySimaiParser 是一个用于解析 Simai 谱面文件并将其转换为结构化 JSON 格式的 Python 库,同时也支持将 JSON 数据转换回 Simai 文本格式。
本文档将详细介绍库中的核心数据结构、类、主要方法及其功能。
SimaiChart 是解析和存储 Simai 谱面数据的主要类。
-
metadata(dict): 存储谱面的元数据。-
"title"(str): 歌曲标题。 -
"artist"(str): 艺术家名称。 -
"designer"(str): 谱面设计者。 -
"first_offset_sec"(float): 从&first命令获取的初始谱面偏移时间(秒)。 -
"levels"(list[str]): 包含7个元素的列表,对应&lv_1到&lv_7的难度等级字符串。 -
"other_commands_raw"(str): 其他未被特别解析的元数据命令的原始文本。
-
-
fumens_raw(list[str]): 包含7个元素的列表,存储每个难度(&inote_1到&inote_7)的原始谱面字符串。 -
processed_fumens_data(list[dict]): 存储解析和处理后的谱面数据。每个字典代表一个难度,包含音符事件和时间点事件。
-
__init__(self):- 初始化
metadata和fumens_raw属性。
- 初始化
-
load_from_text(self, simai_text_content: str):- 功能: 从包含 Simai 谱面内容的字符串中加载并解析数据。
-
流程:
- 按行分割输入文本。
- 遍历每一行,根据行首的标记(如
&title=、&inote_)区分元数据和谱面数据。 - 解析元数据并存入
self.metadata。 - 提取各个难度的原始谱面字符串(
&inote_X块内的内容)并存入self.fumens_raw。 - 调用
_process_all_fumens()方法处理原始谱面数据。
-
_process_all_fumens(self):- 功能: 处理所有已加载的原始谱面字符串,将其转换为结构化的音符和时间点数据。
-
流程:
- 遍历
self.fumens_raw中的每个原始谱面文本。 - 如果谱面文本非空,则调用
_parse_single_fumen()进行解析。 - 将解析结果(音符事件列表和逗号时间点事件列表)存入
self.processed_fumens_data中,每个元素对应一个难度。
- 遍历
-
_parse_single_fumen(self, fumen_text: str) -> tuple[list[SimaiTimingPoint], list[SimaiTimingPoint]]:-
功能: 解析单个难度谱面(
&inote_块内)的字符串。 -
参数:
-
fumen_text(str): 单个难度的谱面字符串。
-
-
返回:
-
note_events_list(list[SimaiTimingPoint]): 包含实际音符的SimaiTimingPoint对象列表。 -
timing_events_at_commas_list(list[SimaiTimingPoint]): 代表每个逗号(谱面分隔符)的SimaiTimingPoint对象列表。
-
-
核心逻辑:
- 初始化当前 BPM、节拍数、时间(秒)、变速等状态变量。
- 逐字符遍历谱面文本。
-
处理注释:
|| ... \n,跳过注释内容。 -
处理 BPM 变化:
(value),更新当前 BPM。 -
处理节拍记号变化:
{value},更新每小节的节拍数。 -
处理变速变化:
<Hvalue>或<HS*value>,更新当前变速值。 -
处理换行符:
\n,更新行号。 -
处理逗号分隔符:
,- 调用
_finalize_note_segment()处理逗号前的音符内容。 - 创建一个
SimaiTimingPoint对象代表此逗号,并添加到timing_events_at_commas_list。 - 根据当前 BPM 和节拍数,计算并增加
current_time_sec。
- 调用
-
累积音符字符: 其他字符被视为音符定义的一部分,累积到
note_buffer。 - 谱面结束时,再次调用
_finalize_note_segment()处理剩余的note_buffer。 - 对
note_events_list和timing_events_at_commas_list按时间排序。
-
功能: 解析单个难度谱面(
-
_finalize_note_segment(self,note_buffer_str: str, time_sec: float, x_pos: int, y_pos: int, bpm: float, hspeed: float, note_events_list_ref:list):- 功能: 处理收集到的音符片段字符串(通常是两个逗号之间的内容)。
-
核心逻辑:
- 去除音符字符串两端的空白。
- 如果包含 ``` (反引号,用于伪同时音符/装饰音):
- 按 ``` 分割字符串。
- 为每个部分创建一个
SimaiTimingPoint,时间上略作偏移(基于BPM的极小间隔)。 - 调用
parse_notes_from_content()解析每个部分的音符。
- 如果不包含 ```:
- 创建一个
SimaiTimingPoint对象。 - 调用
parse_notes_from_content()解析音符。
- 创建一个
- 如果解析出了实际音符,则将
SimaiTimingPoint对象添加到note_events_list_ref。
-
to_json(self, indent: int = 2) -> str:-
功能: 将整个解析后的
SimaiChart对象(包括元数据和处理后的谱面数据)转换为 JSON 字符串。 -
参数:
-
indent(int): JSON 输出的缩进级别。
-
- 返回: JSON 格式的字符串。
-
功能: 将整个解析后的
SimaiTimingPoint 代表谱面中的一个特定时间点,通常由 Simai 格式中的逗号标记。它包含在该时间点发生的音符以及当时的有效 BPM 和变速(HSpeed)信息。
-
time(float): 从歌曲开始计算的绝对时间(秒)。 -
raw_text_pos_x(int): 在原始谱面文本中的 X 字符位置(列号)。 -
raw_text_pos_y(int): 在原始谱面文本中的 Y 行位置(行号)。 -
notes_content_raw(str): 在此时间点的原始音符字符串 (例如,"1/2h[4:1]"). -
current_bpm(float): 此时间点有效的 BPM。 -
hspeed(float): 此时间点有效的变速倍率。 -
notes(list[SimaiNote]): 从notes_content_raw解析出的SimaiNote对象列表。
-
__init__(self, time, raw_text_pos_x=0, raw_text_pos_y=0, notes_content="", bpm=0.0, hspeed=1.0):- 初始化
SimaiTimingPoint对象的各个属性。
- 初始化
-
parse_notes_from_content(self):-
功能: 解析
self.notes_content_raw原始音符字符串,并填充self.notes列表。 -
核心逻辑:
- 处理特殊情况,如两个数字直接相连("12" 解析为两个独立的 TAP)。
- 处理
/分隔的同时音符 (例如,"1/2/E1"):递归调用_parse_single_note_token或_parse_same_head_slide。 - 处理
*定义的同头滑键 (例如,"1*V[4:1]"):调用_parse_same_head_slide。 - 对于单个音符标记,调用
_parse_single_note_token。
-
功能: 解析
-
_parse_same_head_slide(self, content_token: str) -> list[SimaiNote]:-
功能: 解析同头滑键组 (例如,
"1*V[4:1]*<[4:1]")。第一个部分定义头部,后续部分是从同一头部开始的无头滑键。 -
返回:
SimaiNote对象列表。
-
功能: 解析同头滑键组 (例如,
-
_parse_single_note_token(self, note_text_orig: str) -> SimaiNote:-
功能: 解析单个音符标记字符串 (例如,
"1b","A2h[4:1]","3-4[8:1]bx") 为一个SimaiNote对象。这是个复杂的方法,因为它需要处理多种可能的修饰符和语法。 -
核心步骤:
-
识别基础音符类型和位置:
- 判断是 TAP (数字开头
1-8) 还是 TOUCH (字母开头A-E,C)。 - 设置
note.note_type和note.start_position/note.touch_area。
- 判断是 TAP (数字开头
-
解析修饰符并细化音符类型:
-
Hanabi (
f): 设置is_hanabi。 -
Hold (
h):- 如果音符类型是 TOUCH,则变为 TOUCH_HOLD。
- 如果音符类型是 TAP,则变为 HOLD。
- 调用
_get_time_from_beats_duration()计算hold_time。
-
Slide (路径字符
-^v<>Vpqszw):- 音符类型变为 SLIDE。
- 调用
_get_time_from_beats_duration()计算slide_time。 - 调用
_get_star_wait_time()计算slide_start_time_offset(滑键星出现前的等待时间)。 - 处理
!或?(无头滑键)。
-
Break (
b):- 根据
b出现的位置和上下文(是否在 SLIDE 中,是否后跟[),设置is_break或is_slide_break。
- 根据
-
EX (
x): 设置is_ex。 -
Star (
$,$$): 设置is_force_star和is_fake_rotate。
-
Hanabi (
-
识别基础音符类型和位置:
-
返回: 一个配置好的
SimaiNote对象。
-
功能: 解析单个音符标记字符串 (例如,
-
_get_time_from_beats_duration(self, note_text_token: str) -> float:-
功能: 从 Simai 的节拍表示法(如
[4:1]、[bpm#N:D]、[#绝对秒数])中解析持续时间,用于 HOLD 和 SLIDE。 - 返回: 持续时间(秒)。
-
支持的格式:
-
[N:D]: 在当前 BPM 下,N分音符持续D个。时间 =(60.0 / current_bpm) * (4.0 / N) * D。 -
[custom_bpm#N:D]: 在指定的custom_bpm下计算。 -
[#abs_time]: 直接指定绝对秒数。 -
[wait_time##duration_val]:duration_val是持续秒数。 -
[custom_bpm#abs_time_val_for_this_bpm]: 在指定custom_bpm下的绝对秒数。
-
-
功能: 从 Simai 的节拍表示法(如
-
_get_star_wait_time(self, note_text_token: str) -> float:-
功能: 解析 SLIDE 音符的星星(star)视觉效果的等待时间。通常来自
[等待BPM#N:D]或[#绝对等待秒数##...]这样的标记。默认等待时间是当前谱面 BPM 下的一个节拍。 - 返回: 等待时间(秒)。
-
支持的格式:
- 默认:
(60.0 / current_bpm)。 -
[abs_wait_time##...]:abs_wait_time是等待秒数。 -
[wait_bpm_override#...]: 使用wait_bpm_override计算一个节拍的等待时间。
- 默认:
-
功能: 解析 SLIDE 音符的星星(star)视觉效果的等待时间。通常来自
-
to_dict(self) -> dict:-
功能: 将
SimaiTimingPoint对象转换为字典,方便 JSON 序列化。
-
功能: 将
SimaiNote 代表 Simai 谱面中的单个音符或操作。它包含类型、位置、持续时间以及各种游戏性修饰符等属性。
定义了 Simai 音符的几种基本类型:
TAPSLIDEHOLD-
TOUCH(外圈感应区音符) -
TOUCH_HOLD(外圈感应区长按音符)
-
note_type(SimaiNoteType): 音符类型。 -
start_position(int | None): 起始位置。对于普通音符是 1-8。对于 TOUCH 音符,如果是A1-E8中的数字部分,或者对于C区固定为 8 (根据 C# 实现的约定)。 -
touch_area(str | None): TOUCH 音符的感应区域 (A-E, C)。 -
hold_time(float): HOLD 音符的持续时间(秒)。 -
slide_time(float): SLIDE 音符的持续时间(秒)。 -
slide_start_time_offset(float): SLIDE 音符的星标(star)出现时间相对于音符时间点的时间偏移(秒)。 -
is_break(bool): 是否为 BREAK 音符 (通常得分较高或有特殊音效)。 -
is_ex(bool): 是否为 EX 音符 (通常有更强的视觉/声音效果)。 -
is_hanabi(bool): 是否为烟花效果 (f标记)。 -
is_slide_no_head(bool): SLIDE 是否无头 (!或?标记)。 -
is_force_star(bool): 是否强制 SLIDE 显示星标 ($标记)。 -
is_fake_rotate(bool): 是否为伪旋转效果 ($$标记)。 -
is_slide_break(bool): SLIDE 音符的滑键段本身是否为 BREAK (b标记在滑键路径上,如1-b[4:1])。 -
raw_note_text(str): 该音符的原始文本,用于调试或参考。
-
__init__(self):- 初始化所有属性为其默认值。
-
to_dict(self) -> dict:-
功能: 将
SimaiNote对象转换为字典,方便 JSON 序列化。
-
功能: 将
JsonSimaiConverter 用于将之前由 PySimaiParser 生成的 JSON 格式的谱面数据转换回 Simai 文本格式。
-
chart_data(dict): 包含谱面数据的字典,期望有metadata和fumens_data键。 -
metadata(dict): 从chart_data中提取的元数据。 -
fumens_data(list[dict]): 从chart_data中提取的谱面数据。 -
standard_x_values(list[float]): 标准的 X 值列表(用于{X}节拍标记,代表一个全音符中可以容纳多少个当前类型的音符)。
-
__init__(self, chart_data_dict: dict):- 初始化转换器,加载输入的谱面数据字典。
-
from_json_file(cls, filepath: str, encoding: str = 'utf-8') -> 'JsonSimaiConverter':-
类方法: 从 JSON 文件加载数据并创建一个
JsonSimaiConverter实例。
-
类方法: 从 JSON 文件加载数据并创建一个
-
from_json_text(cls, json_text: str) -> 'JsonSimaiConverter':-
类方法: 从 JSON 字符串加载数据并创建一个
JsonSimaiConverter实例。
-
类方法: 从 JSON 字符串加载数据并创建一个
-
to_simai_text(self) -> str:- 功能: 将加载的谱面数据转换回 Simai 文本格式。
-
核心逻辑:
-
输出元数据:
- 写入
&title,&artist,&des。 - 调用
_determine_chart_global_bpm()确定并写入&wholebpm。 - 写入
&first和&lv_X。
- 写入
-
处理每个谱面 (fumen):
- 遍历
self.fumens_data中的每个谱面。 - 写入谱面头
&inote_X=. -
事件排序: 合并
note_events和timing_events_at_commas并按时间排序。 -
预计算 X 值: 遍历所有逗号事件,使用
_calculate_x_for_segment()计算每个由逗号分隔的片段的 X 值,并存储在comma_times_and_x字典中。 -
初始化谱面状态: 设置初始 BPM 和 HSpeed,并输出相应的
(BPM)和<HSpeed>命令。 -
主事件循环: 遍历排序后的所有事件点 (音符或逗号)。
-
处理 BPM/HSpeed 变化: 如果当前事件点的 BPM/HSpeed 与活动的不同,则先调用
flush_current_line()输出当前行,然后输出新的(BPM)或<HSpeed>命令。 -
处理音符事件: 将音符的
notes_content_raw添加到notes_since_last_boundary缓冲区。 -
处理逗号事件:
- 将
notes_since_last_boundary和逗号本身组合成一个小片段字符串。 - 从
comma_times_and_x获取此片段的 X 值。 - 根据 X 值决定当前 Simai 行最多能容纳多少个小片段 (
max_segments_this_line)。 - 如果当前行是空的,或者当前片段的 X 值与行主导 X 值接近且未超出行容量,则将小片段加入当前行 (
current_line_output_segments)。 - 否则,调用
flush_current_line()输出已满的行,并开始新行。
- 将
-
处理 BPM/HSpeed 变化: 如果当前事件点的 BPM/HSpeed 与活动的不同,则先调用
-
行刷新 (
flush_current_line):- 如果当前行的主导 X 值 (
x_governing_current_line) 不是标准 X 值,则尝试使用_find_closest_standard_x()找到最接近的标准 X 值。 - 如果选择了不同的标准 X 值,为了保持片段的实际时间长度不变,需要调整 BPM:
new_BPM = active_BPM * (original_X / target_X)。输出调整后的(BPM)命令。 - 输出
{X}标记(使用标准化的 X 值)。 - 输出当前行累积的所有小片段。
- 如果当前行的主导 X 值 (
-
末尾处理: 循环结束后,再次调用
flush_current_line()和输出剩余的notes_since_last_boundary(通常是E标记)。
- 遍历
- 清理输出: 移除多余的空行,确保文件末尾有单个换行符。
-
输出元数据:
- 返回: Simai 格式的字符串。
-
_determine_chart_global_bpm(self) -> float | None:-
功能: 决定谱面的全局 BPM,用于写入
&wholebpm元数据。 -
逻辑: 检查每个谱面(fumen)的初始 BPM。如果一致,则使用该 BPM。否则,如果某个 BPM 出现频率显著较高,则使用它。最后回退到元数据中的
wholebpm(如果存在) 或找到的第一个 BPM。
-
功能: 决定谱面的全局 BPM,用于写入
-
_calculate_x_for_segment(self, segment_duration: float, bpm_at_segment_start: float) -> float:- 功能: 根据片段持续时间和片段开始时的 BPM 计算 X 值。
-
公式:
X=240.0 / (segment_duration * bpm_at_segment_start)。 - 如果持续时间或 BPM 无效,则默认为 4.0。
-
_find_closest_standard_x(self, x_val: float) -> float | None:-
功能: 给定一个计算出的 X 值,找到
self.standard_x_values中最接近的标准 X 值。 -
逻辑:
- 如果
x_val与某个标准值非常接近(容差内),则返回该标准值。 - 尝试将
x_val转换为一个分母较小的简单分数,如果转换后的值与原值接近且是一个标准值或一个“良好”的整数,则使用它。 - 否则,返回算术上最接近的标准值。
- 如果
-
功能: 给定一个计算出的 X 值,找到
当调用 SimaiChart.to_json() 方法时,会生成一个 JSON 对象,其主要结构如下:
{
"metadata": {
"title": "歌曲标题",
"artist": "艺术家",
"designer": "谱师",
"first_offset_sec": 1.25,
"levels": ["", "", "", "13+", "", "", ""], // &lv_1 到 &lv_7
"other_commands_raw": "任何其他未解析的元数据行"
},
"fumens_data": [
// 每个元素代表一个难度,从索引 0 (对应 &inote_1) 开始
{ // 示例:索引 0,对应 &inote_1
"difficulty_index": 0,
"level_info": "", // 来自 metadata.levels[0]
"note_events": [], // SimaiTimingPoint 对象的列表 (转换为字典)
"timing_events_at_commas": [] // SimaiTimingPoint 对象的列表 (转换为字典)
},
// ... 其他难度 ...
{ // 示例:索引 3,对应 &inote_4
"difficulty_index": 3,
"level_info": "13+", // 来自 metadata.levels[3]
"note_events": [
// SimaiTimingPoint 对象 (转换为字典)
{
"time": 1.75, // 绝对时间 (秒)
"raw_text_pos_x": 0,
"raw_text_pos_y": 2, // 假设在谱面文本的第3行 (0-indexed)
"notes_content_raw": "1",
"current_bpm_at_event": 120.0,
"hspeed_at_event": 1.0,
"notes": [
// SimaiNote 对象 (转换为字典)
{
"note_type": "TAP",
"start_position": 1,
"touch_area": null,
"hold_time": 0.0,
"slide_time": 0.0,
"slide_start_time_offset": 0.0,
"is_break": false,
"is_ex": false,
"is_hanabi": false,
"is_slide_no_head": false,
"is_force_star": false,
"is_fake_rotate": false,
"is_slide_break": false,
"raw_note_text": "1"
}
]
},
// ... 更多 note_events ...
],
"timing_events_at_commas": [
// SimaiTimingPoint 对象 (转换为字典), notes_content_raw 为 ""
{
"time": 1.75, // 逗号对应的时间点
"raw_text_pos_x": 1, // 逗号在 "1," 中的位置
"raw_text_pos_y": 2,
"notes_content_raw": "", // 对于纯粹的逗号时间点,这里为空
"current_bpm_at_event": 120.0,
"hspeed_at_event": 1.0,
"notes": [] // 通常为空,除非逗号后紧跟 E 标记且被解析为 note
},
// ... 更多 timing_events_at_commas ...
]
}
// ... 直到索引 6 (对应 &inote_7) ...
]
}
键值对如 SimaiChart.metadata 属性所述。
这是一个列表,每个元素是一个字典,代表一个难度的谱面数据。列表的索引对应谱面难度(0 对应 &inote_1,1 对应 &inote_2,以此类推)。
每个难度字典包含以下键:
-
"difficulty_index"(int): 当前难度的索引 (0-6)。 -
"level_info"(str): 该难度的等级信息,来自metadata["levels"][difficulty_index]。 -
"note_events"(list[dict]): 一个列表,其中每个元素是将SimaiTimingPoint对象(包含实际音符)调用to_dict()后得到的字典。这些事件点代表了谱面中实际需要玩家操作的音符。 -
"timing_events_at_commas"(list[dict]): 一个列表,其中每个元素是将代表谱面中每个逗号分隔符的SimaiTimingPoint对象调用to_dict()后得到的字典。这些主要用于标记时间流逝和节拍结构,其notes_content_raw通常为空,notes列表也为空。
SimaiTimingPoint 和 SimaiNote 字典的结构分别对应其类中 to_dict() 方法的输出。
cli.py 提供了一个命令行界面,用于直接使用 PySimaiParser 的核心功能将 Simai 文本文件转换为 JSON。
-
主要功能:
- 接收输入 Simai 文件路径。
- 可选输出 JSON 文件路径。
- 可选 JSON 缩进级别。
-
使用:
python cli.py <input_file.txt> [-o <output_file.json>] [-i <indent_level>] -
内部实现:
- 解析命令行参数。
- 读取输入文件内容。
- 创建
SimaiChart实例。 - 调用
chart.load_from_text()解析谱面内容。 - 调用
chart.to_json()生成 JSON 字符串。 - 将 JSON 输出到指定文件或标准输出。
该工具依赖于 SimaiParser 包中的 SimaiChart 类。