diff --git a/MyCode/App_Auto.c b/MyCode/App_Auto.c index 19b8b36..8fa13cf 100644 --- a/MyCode/App_Auto.c +++ b/MyCode/App_Auto.c @@ -21,9 +21,11 @@ static uint8_t pStoreBuffer[PRESET_BUFFER_SIZE]; /* ======== Bass/和弦分通道八度(无 AutoBand 源码时的输出侧映射) ======== * 代码通道(0 起,与 MIDI status 低 4 位一致):8=bass,9=鼓;其余=和弦。 - * 日志打印同号(ch8=bass)。I/II(键1~6)原样;III+:bass -12 地板 E1; - * 和弦 -12 夹在 E2~#G4,超上限就近和弦内音。移调≥#F(6) 时再 -12。 - * 对照 Doc/吉他和弦表最终版.xlsx */ + * 日志打印同号(ch8=bass)。 + * ① 指板 I/II(键1~6):原样;移调≥#F(6) 时表格「整体-12」(bass、和弦)。 + * ② 指板 III+(键7~21):bass -12 地板 E1;和弦 -12 夹在 E2~#G4; + * 移调≥#F 时再叠加 -12(与①同一转调表)。 + * 对照 Doc/吉他和弦表最终版.xlsx / 转调说明表 */ #define CHORD_REG_FIX_EN 1 #define BASS_CH 8 /* 日志 ch8 */ #define DRUM_CH 9 /* 日志 ch9 */ @@ -195,6 +197,37 @@ static int App_Auto_MapChordNote(int key, uint8_t *flags) 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) { 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) return key; - if (!App_Auto_NeedRegisterFix()) - return key; - - if (channel == BASS_CH) - out = App_Auto_MapBassNote(key, flags); + if (App_Auto_NeedRegisterFix()) + { + /* III+:既有把位音区修正;≥#F 时 Map* 内再叠转调 -12 */ + if (channel == BASS_CH) + out = App_Auto_MapBassNote(key, flags); + else + out = App_Auto_MapChordNote(key, flags); + } + else if (App_Auto_XposeExtra()) + { + /* I/II + 移调≥#F:表格「整体-12 (bass、和弦)」 */ + out = App_Auto_MapXposeOverall(channel, key, flags); + } else - out = App_Auto_MapChordNote(key, flags); + { + /* I/II + 移调 C~F:正常和弦逻辑,不改八度 */ + return key; + } #else (void)channel; #endif @@ -249,7 +293,8 @@ static void App_Auto_LogPitch(int channel, int key, int out, uint8_t flags, int deg = App_Auto_ChordDegree(); fix = App_Auto_NeedRegisterFix(); 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", is_on ? "on" : "off", diff --git a/tools/rtt_pitch_reg_test.py b/tools/rtt_pitch_reg_test.py index 5bdf9ca..09c53f3 100644 --- a/tools/rtt_pitch_reg_test.py +++ b/tools/rtt_pitch_reg_test.py @@ -247,6 +247,30 @@ def map_chord(key: int, xp: int) -> tuple[int, int, bool]: 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]: """Return (detail, ok).""" if not ev.is_on: @@ -268,6 +292,23 @@ def expect_for_event(ev: PitchEvent) -> tuple[str, bool]: fix = need_register_fix(ev.chord) 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: 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: @@ -476,6 +517,8 @@ def run_auto(device: str, hold_s: float, report: str | None, log_path: str | Non 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 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("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)