-
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 音符的几种基本类型:
TAP
SLIDE
HOLD
-
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
类。