fix(reg): apply overall -12 for transpose >= #F on I/II too

Match 转调说明 table: C~F keep normal register; #F~B lower bass/chord by one octave for all fret degrees. III+ register fix unchanged. RTT pitch test covers I/II+#F.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yuquanjun 2026-09-21 17:18:30 +08:00
parent ada2031e38
commit feb084699c
2 changed files with 98 additions and 10 deletions

View File

@ -21,9 +21,11 @@ static uint8_t pStoreBuffer[PRESET_BUFFER_SIZE];
/* ======== Bass/和弦分通道八度(无 AutoBand 源码时的输出侧映射) ======== /* ======== Bass/和弦分通道八度(无 AutoBand 源码时的输出侧映射) ========
* 代码通道(0 起,与 MIDI status 低 4 位一致):8=bass,9=鼓;其余=和弦。 * 代码通道(0 起,与 MIDI status 低 4 位一致):8=bass,9=鼓;其余=和弦。
* 日志打印同号(ch8=bass)。I/II(键1~6)原样;III+:bass -12 地板 E1; * 日志打印同号(ch8=bass)。
* 和弦 -12 夹在 E2~#G4,超上限就近和弦内音。移调≥#F(6) 时再 -12。 * ① 指板 I/II(键1~6):原样;移调≥#F(6) 时表格「整体-12」(bass、和弦)。
* 对照 Doc/吉他和弦表最终版.xlsx */ * ② 指板 III+(键7~21):bass -12 地板 E1;和弦 -12 夹在 E2~#G4;
* 移调≥#F 时再叠加 -12(与①同一转调表)。
* 对照 Doc/吉他和弦表最终版.xlsx / 转调说明表 */
#define CHORD_REG_FIX_EN 1 #define CHORD_REG_FIX_EN 1
#define BASS_CH 8 /* 日志 ch8 */ #define BASS_CH 8 /* 日志 ch8 */
#define DRUM_CH 9 /* 日志 ch9 */ #define DRUM_CH 9 /* 日志 ch9 */
@ -195,6 +197,37 @@ static int App_Auto_MapChordNote(int key, uint8_t *flags)
return App_Auto_ClampMidi(out); return App_Auto_ClampMidi(out);
} }
/* 转调说明:移调≥#F 时 I/II 也「整体-12」(bass、和弦);不含 III+ 把位音区修正 */
static int App_Auto_MapXposeOverall(int channel, int key, uint8_t *flags)
{
int out = key - 12;
uint8_t f = 0x04u; /* bit2: xpose overall -12 */
if (channel == BASS_CH)
{
while (out < MIDI_E1)
{
out += 12;
f |= 0x02u;
}
}
else
{
while (out < MIDI_E2)
{
out += 12;
f |= 0x08u;
}
if (out > MIDI_GSHARP4)
{
out = App_Auto_NearestChordTone(out);
f |= 0x10u;
}
}
if (flags) *flags = f;
return App_Auto_ClampMidi(out);
}
static int App_Auto_MapNote(int channel, int key, uint8_t *flags) static int App_Auto_MapNote(int channel, int key, uint8_t *flags)
{ {
int out = key; int out = key;
@ -204,13 +237,24 @@ static int App_Auto_MapNote(int channel, int key, uint8_t *flags)
if (channel == DRUM_CH || key < 0 || key > 127) if (channel == DRUM_CH || key < 0 || key > 127)
return key; return key;
if (!App_Auto_NeedRegisterFix()) if (App_Auto_NeedRegisterFix())
return key; {
/* III+:既有把位音区修正;≥#F 时 Map* 内再叠转调 -12 */
if (channel == BASS_CH) if (channel == BASS_CH)
out = App_Auto_MapBassNote(key, flags); out = App_Auto_MapBassNote(key, flags);
else else
out = App_Auto_MapChordNote(key, flags); out = App_Auto_MapChordNote(key, flags);
}
else if (App_Auto_XposeExtra())
{
/* I/II + 移调≥#F:表格「整体-12 (bass、和弦)」 */
out = App_Auto_MapXposeOverall(channel, key, flags);
}
else
{
/* I/II + 移调 C~F:正常和弦逻辑,不改八度 */
return key;
}
#else #else
(void)channel; (void)channel;
#endif #endif
@ -249,7 +293,8 @@ static void App_Auto_LogPitch(int channel, int key, int out, uint8_t flags, int
deg = App_Auto_ChordDegree(); deg = App_Auto_ChordDegree();
fix = App_Auto_NeedRegisterFix(); fix = App_Auto_NeedRegisterFix();
xf = App_Auto_XposeExtra(); xf = App_Auto_XposeExtra();
pass = (!fix || channel == DRUM_CH) ? 1 : 0; /* PASS = 未做任何八度映射(I/II 且移调 C~F) */
pass = ((!fix && !xf) || channel == DRUM_CH) ? 1 : 0;
LOG_I("PITCH", "%s ch%u(%s) %d->%d d=%d deg=%d chord=%u xp=%u xf=%d fl=0x%02X %s%s%s%s", LOG_I("PITCH", "%s ch%u(%s) %d->%d d=%d deg=%d chord=%u xp=%u xf=%d fl=0x%02X %s%s%s%s",
is_on ? "on" : "off", is_on ? "on" : "off",

View File

@ -247,6 +247,30 @@ def map_chord(key: int, xp: int) -> tuple[int, int, bool]:
return max(0, min(127, out)), flags, need_near return max(0, min(127, out)), flags, need_near
def map_xpose_overall_bass(key: int) -> tuple[int, int]:
"""I/II + xp>=#F: overall -12 only (mirrors App_Auto_MapXposeOverall bass)."""
out = key - 12
flags = 0x04
while out < MIDI_E1:
out += 12
flags |= 0x02
return max(0, min(127, out)), flags
def map_xpose_overall_chord(key: int) -> tuple[int, int, bool]:
"""I/II + xp>=#F: overall -12 only (mirrors App_Auto_MapXposeOverall chord)."""
out = key - 12
flags = 0x04
while out < MIDI_E2:
out += 12
flags |= 0x08
need_near = out > MIDI_GSHARP4
if need_near:
flags |= 0x10
out = MIDI_GSHARP4
return max(0, min(127, out)), flags, need_near
def expect_for_event(ev: PitchEvent) -> tuple[str, bool]: def expect_for_event(ev: PitchEvent) -> tuple[str, bool]:
"""Return (detail, ok).""" """Return (detail, ok)."""
if not ev.is_on: if not ev.is_on:
@ -268,6 +292,23 @@ def expect_for_event(ev: PitchEvent) -> tuple[str, bool]:
fix = need_register_fix(ev.chord) fix = need_register_fix(ev.chord)
if not fix: if not fix:
# I/II: C~F 原样;≥#F 表格整体-12
if ev.xp >= XPOSE_FS:
if ev.ch == BASS_CH_DISP or ev.role == "bass":
exp, exp_fl = map_xpose_overall_bass(ev.key_in)
if ev.key_out != exp:
return f"I/II xpose bass expect {ev.key_in}->{exp}, got {ev.key_out}", False
if not (ev.flags & 0x04):
return f"I/II xpose bass missing xf flag fl=0x{ev.flags:02X}", False
return f"I/II xpose bass {ev.key_in}->{ev.key_out}", True
exp, exp_fl, need_near = map_xpose_overall_chord(ev.key_in)
if not (MIDI_E2 <= ev.key_out <= MIDI_GSHARP4):
return f"I/II xpose chord out {ev.key_out} not in range", False
if need_near:
return f"I/II xpose chord NEAR {ev.key_in}->{ev.key_out}", True
if ev.key_out != exp:
return f"I/II xpose chord expect {ev.key_in}->{exp}, got {ev.key_out}", False
return f"I/II xpose chord {ev.key_in}->{ev.key_out}", True
if ev.key_out != ev.key_in or ev.delta != 0: if ev.key_out != ev.key_in or ev.delta != 0:
return f"I/II should PASS {ev.key_in}->{ev.key_in}, got {ev.key_out}", False return f"I/II should PASS {ev.key_in}->{ev.key_in}, got {ev.key_out}", False
if "PASS" not in ev.tags and ev.flags != 0: if "PASS" not in ev.tags and ev.flags != 0:
@ -476,6 +517,8 @@ def run_auto(device: str, hold_s: float, report: str | None, log_path: str | Non
sess.pump(hold_s) sess.pump(hold_s)
sess.phase_switch("I/II key2 PASS", key=2, xpose=None, hold_s=hold_s) sess.phase_switch("I/II key2 PASS", key=2, xpose=None, hold_s=hold_s)
sess.phase_switch("I/II key2 xpose=#F overall-12", key=2, xpose=6, hold_s=hold_s)
sess.phase_switch("I/II key2 xpose=F restore", key=2, xpose=2, hold_s=hold_s)
sess.phase_switch("III key8 -12", key=8, xpose=None, hold_s=hold_s) sess.phase_switch("III key8 -12", key=8, xpose=None, hold_s=hold_s)
sess.phase_switch("VII key20 -12", key=20, xpose=None, hold_s=hold_s) sess.phase_switch("VII key20 -12", key=20, xpose=None, hold_s=hold_s)
sess.phase_switch("VII key20 xpose=#F", key=20, xpose=6, hold_s=hold_s) sess.phase_switch("VII key20 xpose=#F", key=20, xpose=6, hold_s=hold_s)