Skip to content

Commit 0b0d4a6

Browse files
committed
refactor: 重构导入谱面中的延迟调整/padding相关逻辑
1. 将原来复杂而不统一的padding计算,整理重构为统一的方法。具体而言,仅有两种padding:chartPadding,由ShiftMode决定;audioPadding,由chartPadding加first来计算。具体的含义详见MaidataImportService.cs 中的注释。 2. 将调用SetAudioApi时进行的音频padding,由原来前、后端分别用C#和JS实现相同的逻辑,改为:全部由后端在ImportChartCheck时进行计算,前端直接以后端的计算结果为依据。避免同样的代码在两个地方分别维护带来的维护性困难等问题。
1 parent cfdd2c9 commit 0b0d4a6

6 files changed

Lines changed: 110 additions & 118 deletions

File tree

MaiChartManager/Controllers/Charts/ImportChartController.cs

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,7 @@ namespace MaiChartManager.Controllers.Charts;
1111
public class ImportChartController(StaticSettings settings, ILogger<StaticSettings> logger,
1212
MaidataImportService importService) : ControllerBase
1313
{
14-
private static float getFirstBarFromChart(MaiChart chart)
15-
{
16-
var bpm = chart.TimingChanges[0].tempo;
17-
if (bpm == 0)
18-
{
19-
throw new DivideByZeroException(Locale.ChartBpmZero);
20-
}
21-
22-
return 60 / bpm * 4;
23-
}
24-
25-
public record ImportChartCheckResult(bool Accept, IEnumerable<ImportChartMessage> Errors, float MusicPadding, bool IsDx, string? Title, float first, float bar);
14+
public record ImportChartCheckResult(bool Accept, IEnumerable<ImportChartMessage> Errors, Dictionary<ShiftMethod, float> chartPaddings, bool IsDx, string? Title, float first);
2615

2716
[HttpPost]
2817
public ImportChartCheckResult ImportChartCheck(IFormFile file, [FromForm] bool isReplacement = false)
@@ -110,12 +99,12 @@ public ImportChartCheckResult ImportChartCheck(IFormFile file, [FromForm] bool i
11099
{
111100
errors.Add(new ImportChartMessage(Locale.MusicNoCharts, MessageLevel.Fatal));
112101
fatal = true;
113-
return new ImportChartCheckResult(!fatal, errors, 0, false, title, 0, 0);
102+
return new ImportChartCheckResult(!fatal, errors, new Dictionary<ShiftMethod, float>(), false, title, 0);
114103
}
115104

116-
var paddings = new List<float>();
117105
float.TryParse(maiData.GetValueOrDefault("first"), out var first);
118106
var isDx = false;
107+
var maiCharts = new List<MaiChart>();
119108

120109
foreach (var kvp in allChartText)
121110
{
@@ -135,7 +124,7 @@ public ImportChartCheckResult ImportChartCheck(IFormFile file, [FromForm] bool i
135124
try
136125
{
137126
var chart = importService.TryParseChartSimaiSharp(chartText, kvp.Key, errors);
138-
paddings.Add(MaidataImportService.CalcMusicPadding(chart, first));
127+
maiCharts.Add(chart);
139128

140129
var candidate = importService.TryParseChart(chartText, chart, kvp.Key, errors);
141130
if (candidate is null) throw new Exception(Locale.ChartParseGenericError);
@@ -151,19 +140,16 @@ public ImportChartCheckResult ImportChartCheck(IFormFile file, [FromForm] bool i
151140
foreachAllChartTextContinue: ;
152141
}
153142

154-
var padding = paddings.Max();
155-
156-
// 计算 bar
157-
var bar = getFirstBarFromChart(importService.TryParseChartSimaiSharp(allChartText.First().Value, allChartText.First().Key, errors));
143+
var chartPaddings = MaidataImportService.CalcChartPadding(maiCharts, out _);
158144

159-
return new ImportChartCheckResult(!fatal, errors, padding, isDx, title, first, bar);
145+
return new ImportChartCheckResult(!fatal, errors, chartPaddings, isDx, title, first);
160146
}
161147
catch (Exception e)
162148
{
163149
logger.LogError(e, "解析谱面失败(大)");
164150
errors.Add(new ImportChartMessage(Locale.ChartParseFailedGlobal, MessageLevel.Fatal));
165151
fatal = true;
166-
return new ImportChartCheckResult(!fatal, errors, 0, false, "", 0, 0);
152+
return new ImportChartCheckResult(!fatal, errors, new Dictionary<ShiftMethod, float>(), false, "", 0);
167153
}
168154
}
169155

MaiChartManager/Front/src/client/apiGen.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,14 +264,18 @@ export interface ISectionState {
264264
export interface ImportChartCheckResult {
265265
accept?: boolean;
266266
errors?: ImportChartMessage[] | null;
267-
/** @format float */
268-
musicPadding?: number;
267+
chartPaddings?: {
268+
/** @format float */
269+
Legacy?: number;
270+
/** @format float */
271+
Bar?: number;
272+
/** @format float */
273+
NoShift?: number;
274+
} | null;
269275
isDx?: boolean;
270276
title?: string | null;
271277
/** @format float */
272278
first?: number;
273-
/** @format float */
274-
bar?: number;
275279
}
276280

277281
export interface ImportChartMessage {

MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/ImportAlert.tsx

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineComponent, PropType } from "vue";
1+
import { computed, defineComponent, PropType } from "vue";
22
import { MessageLevel, ShiftMethod } from "@/client/apiGen";
33
import { ImportChartMessageEx, TempOptions } from "./types";
44
import { showNeedPurchaseDialog } from "@/store/refs";
@@ -9,36 +9,32 @@ export default defineComponent({
99
tempOptions: {type: Object as PropType<TempOptions>, required: true},
1010
errors: {type: Array as PropType<ImportChartMessageEx[]>, required: true}
1111
},
12-
setup(props, {emit}) {
12+
setup(props) {
1313
const {t} = useI18n();
1414

15+
const i18nPostfix = computed(() => {
16+
switch (props.tempOptions.shift) {
17+
case ShiftMethod.Legacy: return "Padding"
18+
case ShiftMethod.NoShift: return "First"
19+
}
20+
})
21+
1522
return () => <div class="of-y-auto cst max-h-24vh">
1623
<div class="flex flex-col gap-2">
1724
{
1825
props.errors.map((error, i) => {
1926
if ('first' in error) {
20-
if (error.padding > 0 && props.tempOptions.shift === ShiftMethod.Legacy) {
21-
return <div key={i} class="p-3 rounded border border-blue/30 bg-blue/10">
22-
<div class="font-bold mb-1">{error.name}</div>
23-
{t('chart.import.addPadding', {padding: error.padding.toFixed(3)})}
24-
</div>
25-
}
26-
if (error.padding < 0 && props.tempOptions.shift === ShiftMethod.Legacy) {
27-
return <div key={i} class="p-3 rounded border border-blue/30 bg-blue/10">
28-
<div class="font-bold mb-1">{error.name}</div>
29-
{t('chart.import.trimPadding', {padding: (-error.padding).toFixed(3)})}
30-
</div>
31-
}
32-
if (error.first > 0 && props.tempOptions.shift === ShiftMethod.NoShift) {
27+
const padding = error.chartPaddings?.[props.tempOptions.shift]! - error.first
28+
if (padding > 0){
3329
return <div key={i} class="p-3 rounded border border-blue/30 bg-blue/10">
3430
<div class="font-bold mb-1">{error.name}</div>
35-
{t('chart.import.trimFirst', {first: error.first.toFixed(3)})}
31+
{t('chart.import.add' + i18nPostfix.value, {padding: padding.toFixed(3)})}
3632
</div>
3733
}
38-
if (error.first < 0 && props.tempOptions.shift === ShiftMethod.NoShift) {
34+
else if (padding < 0) {
3935
return <div key={i} class="p-3 rounded border border-blue/30 bg-blue/10">
4036
<div class="font-bold mb-1">{error.name}</div>
41-
{t('chart.import.addFirst', {first: (-error.first).toFixed(3)})}
37+
{t('chart.import.trim' + i18nPostfix.value, {padding: (-padding).toFixed(3)})}
4238
</div>
4339
}
4440
return <></>

MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/index.tsx

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { defineComponent, ref } from "vue";
22
import { Button } from "@munet/ui";
33
import SelectFileTypeTip from "./SelectFileTypeTip";
4-
import { LicenseStatus, MessageLevel, ShiftMethod } from "@/client/apiGen";
4+
import { LicenseStatus, MessageLevel } from "@/client/apiGen";
55
import CheckingModal from "./CheckingModal";
66
import api, { getUrl } from "@/client/api";
7-
import { globalCapture, musicList, selectedADir, selectMusicId, updateMusicList, version as appVersion } from "@/store/refs";
7+
import { globalCapture, selectedADir, selectMusicId, updateMusicList, version as appVersion } from "@/store/refs";
88
import { appSettings } from "@/store/settings";
99
import ErrorDisplayIdInput from "./ErrorDisplayIdInput";
1010
import ImportStepDisplay from "./ImportStepDisplay";
@@ -68,15 +68,14 @@ export default defineComponent({
6868
errors.value.push({ level: MessageLevel.Warning, message: t('chart.import.error.convertPaidFeature'), name: dir.name, isPaid: true });
6969
}
7070

71-
let musicPadding = 0, first = 0, bar = 0, name = dir.name, isDx = false;
71+
let first = 0, chartPaddings, name = dir.name, isDx = false;
7272
if (maidata) {
7373
const checkRet = (await api.ImportChartCheck({ file: maidata })).data;
7474
reject = reject || !checkRet.accept;
7575
errors.value.push(...(checkRet.errors || []).map(it => ({ ...it, name: dir.name })));
76-
musicPadding = checkRet.musicPadding!;
7776
first = checkRet.first!;
78-
bar = checkRet.bar!;
79-
errors.value.push({ first, padding: musicPadding, name: dir.name });
77+
chartPaddings = checkRet.chartPaddings!;
78+
errors.value.push({ first, chartPaddings, name: dir.name });
8079
// 为了本地的错误和远程的错误都显示本地的名称,这里在修改 name
8180
name = checkRet.title!;
8281
if (checkRet.isDx) id += 1e4;
@@ -85,7 +84,7 @@ export default defineComponent({
8584

8685
if (!reject) {
8786
meta.value.push({
88-
id, maidata, bg, track, musicPadding, name, first, movie, bar, isDx,
87+
id, maidata, bg, track, chartPaddings, name, first, movie, isDx,
8988
importStep: IMPORT_STEP.start,
9089
})
9190
}
@@ -164,25 +163,18 @@ export default defineComponent({
164163
}
165164

166165
music.importStep = IMPORT_STEP.music;
167-
let padding = 0;
168-
if (tempOptions.value.shift === ShiftMethod.Legacy) {
169-
padding = music.musicPadding;
170-
} else if (tempOptions.value.shift === ShiftMethod.Bar) {
171-
if (music.musicPadding + music.first > 0.1)
172-
padding = music.bar - music.first;
173-
else
174-
padding = -music.first;
175-
} else if (tempOptions.value.shift === ShiftMethod.NoShift) {
176-
padding = -music.first;
177-
}
166+
let chartPadding = music.chartPaddings?.[tempOptions.value.shift]!;
167+
// 参见Services/MaidataImportService.cs:CalcChartPadding 中的注释,
168+
// 音频上应该应用的延迟audioPadding = 谱面上应用的延迟chartPadding - &first
169+
let audioPadding = chartPadding - music.first;
178170

179-
await api.SetAudio(music.id, selectedADir.value, { file: music.track, padding });
171+
await api.SetAudio(music.id, selectedADir.value, { file: music.track, padding: audioPadding });
180172

181173
if (music.movie && !tempOptions.value.disableBga) {
182174
currentMovieProgress.value = 0;
183175
music.importStep = IMPORT_STEP.movie;
184176
try {
185-
await uploadMovie(music.id, music.movie, padding);
177+
await uploadMovie(music.id, music.movie, audioPadding);
186178
} catch (e: any) {
187179
errors.value.push({ level: MessageLevel.Warning, message: t('chart.import.error.videoConvertFailed') + `: ${e.error?.message || e.error?.detail || e?.message || e?.toString() || t('error.unknown')}`, name: music.name });
188180
}

MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/types.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ImportChartMessage, ShiftMethod } from "@/client/apiGen";
1+
import { ImportChartCheckResult, ImportChartMessage, ShiftMethod } from "@/client/apiGen";
22

33
export enum STEP {
44
none,
@@ -27,13 +27,12 @@ export type ImportMeta = {
2727
bg?: File,
2828
movie?: File,
2929
name: string,
30-
musicPadding: number,
30+
chartPaddings: ImportChartCheckResult['chartPaddings'],
3131
first: number,
32-
bar: number,
3332
isDx: boolean,
3433
}
3534

36-
export type FirstPaddingMessage = { first: number, padding: number }
35+
export type FirstPaddingMessage = { first: number, chartPaddings: ImportChartCheckResult['chartPaddings']}
3736
export type ImportChartMessageEx = (ImportChartMessage | FirstPaddingMessage) & { name: string, isPaid?: boolean }
3837

3938
export const dummyMeta = {name: '', importStep: IMPORT_STEP.start} as ImportMeta

MaiChartManager/Services/MaidataImportService.cs

Lines changed: 66 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,20 @@ public MaidataImportService(ILogger<MaidataImportService> logger)
5252
[GeneratedRegex(@"\{([\d\.]+)\}")]
5353
public static partial Regex MeasureTagRegex();
5454

55-
private static string Add1Bar(string maidata)
55+
private static string Add1Bar(string maidata, float bpm)
5656
{
5757
var regex = BpmTagRegex();
58-
var bpm = regex.Match(maidata).Value;
59-
// 这里使用 {4},,,, 而不是 {1}, 因为要是谱面一开始根本没有写 {x} 的话,默认是 {4}。要是用了 {1}, 会覆盖默认的 {4}
60-
return string.Concat(bpm, "{4},,,,", maidata.AsSpan(bpm.Length));
58+
var bpmStr = regex.Match(maidata).Value;
59+
if (Math.Abs(float.Parse(bpmStr[1..^1]) - bpm) < 0.001f) // 本质是比较maidata中声明的(bpm)和我们参数传进来的bpm是否不等。以防浮点运算误差使用了range比较。
60+
{
61+
// 如果相等的话,则把新的小节插在bpmStr后面即可
62+
// 这里使用 {4},,,, 而不是 {1}, 因为要是谱面一开始根本没有写 {x} 的话,默认是 {4}。要是用了 {1}, 会覆盖默认的 {4}
63+
return string.Concat(bpmStr, "{4},,,,", maidata.AsSpan(bpmStr.Length));
64+
}
65+
else
66+
{
67+
return string.Concat($"({bpm})", "{4},,,,", maidata);
68+
}
6169
}
6270

6371
[GeneratedRegex(@"(\d){")]
@@ -195,16 +203,55 @@ public MaiChart TryParseChartSimaiSharp(string chartText, int level, List<Import
195203
}
196204
}
197205

198-
public static float CalcMusicPadding(MaiChart chart, float first)
206+
public static Dictionary<ShiftMethod, float> CalcChartPadding(List<MaiChart> charts, out float addBarBpm)
199207
{
200-
// TimingChanges 对应的是所有的 {int}
201-
var bpm = chart.TimingChanges[0].tempo;
202-
// 一小节多长
203-
var bar = 60 / bpm * 4;
204-
205-
// 第一押什么时候出来
206-
var firstTiming = chart.NoteCollections[0].time + first;
207-
return bar - firstTiming;
208+
// 谱面导入时,会有两个地方涉及到时间的调整:
209+
// 1. 对谱面的调整。在下方的ImportMaidata函数中应用,对谱面进行相应的调整(用simaisharp移动一定的时间,或加上一小节)。
210+
// 2. 对音频的调整,通过向CueConverter.SetAudio API中传入padding参数,来对音频进行裁剪。
211+
// - 详见Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/index.tsx
212+
// 上述两个调整之间存在这样的关系:对音频的调整 一定等于 对谱面的调整 + 谱面本身蕴含的谱面相对于音频的偏移(即&first)。
213+
// 只要输入的谱面本身在simai的语义下正确,上述关系就必定是成立的。
214+
// PS:我们的代码里chartPadding为正表示谱面后移、音频开头相应加空白;而maidata的&first为正表示裁剪掉音频开头。因此实际计算中,应该满足的是audioPadding=chartPadding-&first。、
215+
// 因此,我们只需要在这里计算好每种ShiftMode下的 (1.对谱面的调整) chartPadding,发送给前端。前端-&first后作为 (2.对音频的调整) 发给SetAudioApi即可。
216+
217+
// 首先计算一个概念:notePadding = bar - firstTiming
218+
// 其中,firstTiming是从谱面开头到谱面的第一押的时间。(PS:这里的谱面开头指的是经过&first修正后的、逻辑上的谱面开头。=原始音频文件的开头+&first修正。)
219+
// 因此,notePadding = bar - firstTiming 其实是一个 **衡量第一押距离第二小节开头有多远的量** 。
220+
// 当它是正数时表示第一押在第二小节开头的前面,(所以需要增加一小节/延后谱面)。负数则表示第一押已经在在第二小节开头的后面了。
221+
// PS:由于Bar模式下计算chartPadding时将会用到bpm,这里把notePadding和bpm一起返回
222+
var notePaddingOfEachChart = charts.Select(chart =>
223+
{
224+
var bpm = chart.TimingChanges[0].tempo;
225+
var bar = 60 / bpm * 4;
226+
227+
var firstTiming = chart.NoteCollections[0].time; // 从谱面开头到谱面的第一押的时间
228+
var notePadding = bar - firstTiming;
229+
return (notePadding, bpm);
230+
}).ToList();
231+
232+
// 取notePadding的最大值作为整个谱的偏移量,这是因为如果有多张谱面,我们需要保证所有谱面的第一押都移出第一小节之外
233+
var (notePadding, bpm) = notePaddingOfEachChart.Max();
234+
235+
addBarBpm = 0f;
236+
var result = new Dictionary<ShiftMethod, float>();
237+
// 接下来为每种ShiftMode具体计算chartPadding:
238+
result[ShiftMethod.NoShift] = 0f; // NoShift时,显然
239+
240+
// 由于notePadding的含义就是 *第一押距离第二小节开头的距离*,所以Legacy模式下为了把第一押对到第二小节开头上,所需的东西就是这个。
241+
result[ShiftMethod.Legacy] = notePadding;
242+
243+
// Bar模式下,(为了数值计算上的稳定),我们仅在notePadding > 0.01的情况下才addBar。
244+
if (notePadding > 0.01)
245+
{
246+
result[ShiftMethod.Bar] = 60 / bpm * 4; // 取值为bpm所对应的bar长度
247+
addBarBpm = bpm;
248+
}
249+
else
250+
{
251+
result[ShiftMethod.Bar] = 0f; // 否则,不对谱面做任何移动,等价于NoShift了。
252+
}
253+
254+
return result;
208255
}
209256

210257
private record AllChartsEntry(string chartText, MaiChart simaiSharpChart);
@@ -244,32 +291,17 @@ public ImportChartResult ImportMaidata(
244291

245292
float.TryParse(maiData.GetValueOrDefault("first"), out var first);
246293

247-
var paddings = allCharts.Values.Select(chart => CalcMusicPadding(chart.simaiSharpChart, first)).ToList();
248-
// 音频前面被增加了多少
249-
var audioPadding = paddings.Max(); // bar - firstTiming = bar - 谱面前面休止符的时间 - &first
250-
var shouldAddBar = false;
251-
float chartPadding;
252-
switch (shift)
253-
{
254-
case ShiftMethod.Legacy:
255-
chartPadding = audioPadding + first;
256-
break;
257-
case ShiftMethod.Bar when audioPadding + first > 0.1:
258-
shouldAddBar = true;
259-
chartPadding = 0f;
260-
break;
261-
default:
262-
chartPadding = 0f;
263-
break;
264-
}
265-
266-
if (shouldAddBar)
294+
var chartPaddingDict = CalcChartPadding(allCharts.Values.Select(entry => entry.simaiSharpChart).ToList(), out var addBarBpm);
295+
var chartPadding = chartPaddingDict[shift]; // 当前所选择的模式所具体对应的chartPadding
296+
297+
if (shift == ShiftMethod.Bar && chartPadding > 0)
267298
{
268299
foreach (var (level, chart) in allCharts)
269300
{
270-
var newText = Add1Bar(chart.chartText);
301+
var newText = Add1Bar(chart.chartText, addBarBpm);
271302
allCharts[level] = new AllChartsEntry(newText, TryParseChartSimaiSharp(newText, level, errors));
272303
}
304+
chartPadding = 0f; // 已经add1Bar过了,所以要防止后面的逻辑再次调用simaisharp的谱面平移
273305
}
274306

275307
foreach (var targetChart in music.Charts)
@@ -288,23 +320,6 @@ public ImportChartResult ImportMaidata(
288320
// 一个小节多少秒
289321
var bar = 60 / bpm * 4;
290322

291-
// 我们要让这个谱面真正的内容(忽略 first)延后多少
292-
// levelPadding 似乎不需要算,因为每个谱面真正的内容都是从同一个地方开始
293-
// 所以只要在前面加上 audioPadding + first 时间的休止符
294-
// 最早出音符的那个谱面的第一押之前一定是 1bar(小节)的休止符
295-
// |_| levelPadding
296-
// |_______| audioPadding
297-
// |________________| bar
298-
// |________________| bar
299-
// |-------|---|----|-----|-----
300-
// | | | | | 这个谱面的第一押
301-
// | | | | 可能是另一个谱面难度的第一押,firstTiming,它可能导致 audioPadding > levelPadding
302-
// | | |____| 这一段是休止符
303-
// | | | 每个谱面真正的内容都是从这里开始
304-
// | |___| first skip 掉的部分
305-
// | | 原先音频的开头
306-
// | 加了 padding 的音频开头
307-
308323
# region 设定 targetLevel
309324

310325
var targetLevel = level - 2;

0 commit comments

Comments
 (0)