fix(expert): 专业模式仅按指板即可续奏下一和弦,无需再拨片

App 连机专业模式下,起奏后按亮灯指板即可继续播放:
- 本地曲目(App 和弦曲目)/段落停止时:指板键投递 MSG_ID_BP_KEY 给主任务,
  走与「指板+拨片」完全相同的 Pick_Handle 起奏路径(扫描线程内直接
  Stop+Start 会无声,主任务上下文正常),Pick_Handle 内 Return_Light_buff
  通知 App 亮下一节
- 节奏库播放中:保持说明书行为,仅 Note_On 换和弦音 + Return_Light_buff
- 去掉 Last_Tm1926D_Key 去重:相邻两节可能是同一和弦键,松手再按必须能续奏
- 新增 Band_TrackPlayTick/Band_IsPlaying:8ms 周期跟踪 AutoBand PlayTick
  判定伴奏是否进行中
- 抽出 Expert_StartWithChord 公共起奏函数,拨片/指板共用
- tools/_test_expert_fret_restart.py: 指板续奏 RTT 回归测试

HIL 验证: pitch reg OK=41 FAIL=0; packs 9/9 PASS

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yuquanjun 2026-09-15 19:13:10 +08:00
parent 2d9f234a7d
commit 5f41af27e7
2 changed files with 162 additions and 35 deletions

View File

@ -106,6 +106,9 @@ const uint8_t Version[3] = {0,2,12}; /* major.minor.patch — 0.2.12: autoclose
uint8_t Led = 8; uint8_t Led = 8;
bool PressFlag = 0; bool PressFlag = 0;
void Return_Light_buff(uint8_t key_id_1629);
uint8_t Band_IsPlaying(void);
uint8_t AutoCloseTime = 15; uint8_t AutoCloseTime = 15;
uint16_t mAutoPowerOffCount = 15 * 60; uint16_t mAutoPowerOffCount = 15 * 60;
bool AutoCloseFlag = true; bool AutoCloseFlag = true;
@ -774,11 +777,77 @@ uint8_t BP_Transpose(uint8_t key_idx)
} }
/* 伴奏活动跟踪:AutoBand PlayTick 前进即视为播放中。
* 供专业模式指板键区分「播放中换和弦」与「段落结束后续奏重启」。 */
static uint64_t s_play_tick_last = 0;
static uint32_t s_play_adv_tick = 0; /* 最近一次 PlayTick 前进的系统 tick */
void Band_TrackPlayTick(void)
{
uint64_t pt;
if (!StartFlag)
{
s_play_tick_last = 0;
s_play_adv_tick = 0;
return;
}
pt = AutoBandTop1_GetCurrentPlayTick();
if (pt != s_play_tick_last)
{
s_play_tick_last = pt;
s_play_adv_tick = rt_tick_get();
}
}
uint8_t Band_IsPlaying(void)
{
if (!StartFlag || s_play_adv_tick == 0)
return 0;
return (rt_tick_get() - s_play_adv_tick) < (RT_TICK_PER_SECOND / 4);
}
/* 专业模式公共起奏:拨片首次起奏与起奏后的指板续奏共用。
* 节奏库走 piece 循环;本地曲目按当前段落走 StartWithNote。 */
static void Expert_StartWithChord(uint8_t chord_id, uint8_t *midi_note_buff,
uint8_t note_cnt, const char *trigger)
{
uint8_t midi = 0;
StartFlag = 1;
if (mGuiData[GUI_AUTOBAND_SW].Current)
{
AutoBandTop1_Stop();
AutoBandTop1_SetLoopMode(0, 1);
AutoBandTop1_SetRunVar(mGuiData[GUI_PLAY_SECTION].Current);
midi = 0x3C + mGuiData[GUI_TRANSPOSE].Current;
AutoBandTop1_StartWithNote(midi);
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
LOG_I("pick", "local/expert root=%u name=%s sw=%u tab=%u trigger=%s chord=%u",
midi,
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?",
(unsigned)mGuiData[GUI_AUTOBAND_SW].Current,
(unsigned)mGuiData[GUI_TAB_INDEX].Current,
trigger,
(unsigned)chord_id);
}
else
{
AutoBandTop1_SetPiecePlayMode(1, 0, 1920);
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
AutoBandTop1_SetLoopMode(0, 1);
LOG_I("pick", "expert/rhythm piece name=%s trigger=%s chord=%u",
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?",
trigger,
(unsigned)chord_id);
}
}
void TM1629_Handle(uint8_t key) void TM1629_Handle(uint8_t key)
{ {
STRING_MIDI *MIDI; STRING_MIDI *MIDI;
const STRING_MIDI *ORGAN_MIDI; const STRING_MIDI *ORGAN_MIDI;
static uint8_t Last_Tm1926D_Key = 0;
/* 万能:指板不改和弦、不启停伴奏;保留拍速/停止 */ /* 万能:指板不改和弦、不启停伴奏;保留拍速/停止 */
if (mGuiData[GUI_TAB_INDEX].Current == 0) if (mGuiData[GUI_TAB_INDEX].Current == 0)
@ -857,18 +926,30 @@ void TM1629_Handle(uint8_t key)
USE_MIDI.Midi_5 += mGuiData[GUI_TRANSPOSE].Current; USE_MIDI.Midi_5 += mGuiData[GUI_TRANSPOSE].Current;
USE_MIDI.Midi_6 += mGuiData[GUI_TRANSPOSE].Current; USE_MIDI.Midi_6 += mGuiData[GUI_TRANSPOSE].Current;
if(mGuiData[GUI_TAB_INDEX].Current == 1 && StartFlag && Last_Tm1926D_Key != KEY_ID_1629) if(mGuiData[GUI_TAB_INDEX].Current == 1 && StartFlag)
{ {
uint8_t midi_note_buff[4] = {0,}; /* 不按 Last_Tm1926D_Key 去重:App 曲目相邻两节可能是同一和弦键,
uint8_t note_cnt = 0; * 松手再按同一键也必须能续奏(与「指板+拨片」一致,拨片本无去重) */
Last_Tm1926D_Key = KEY_ID_1629; if (mGuiData[GUI_AUTOBAND_SW].Current || !Band_IsPlaying())
//AutoBandTop1_SetPiecePlayMode(1,0,480);
//AutoBandTop1_StartWithNotes(midi_note_buff,GetChordNotesByType(KEY_ID_1629,chord_type_index_map[KEY_ID_1629].type,midi_note_buff));
//AutoBandTop1_SetLoopMode(0,1);
note_cnt = GetChordNotesByType(KEY_ID_1629,chord_type_index_map[KEY_ID_1629].type,midi_note_buff);
for(uint8_t i = 0;i < note_cnt;i++)
{ {
AutoBandTop1_Note_On(midi_note_buff[i],0x50); /* 本地曲目(App 和弦曲目):段落结束/循环点处仅按指板即续奏下一节。
* 转主任务走 Pick_Handle,与「指板+拨片」完全等价——
* 扫描线程内直接 Stop+Start 重启会无声,主任务上下文则正常。
* Pick_Handle 内会调 Return_Light_buff 通知 App 亮下一节。 */
MainTask_Sendmsg(MSG_ID_BP_KEY, 0, 0, 0);
LOG_I("pick", "expert/fret continue chord=%u", (unsigned)KEY_ID_1629);
}
else
{
/* 节奏库播放中:仅换和弦音,不重启段落(说明书:左手切换和弦) */
uint8_t midi_note_buff[4] = {0,};
uint8_t note_cnt = GetChordNotesByType(KEY_ID_1629,chord_type_index_map[KEY_ID_1629].type,midi_note_buff);
for(uint8_t i = 0;i < note_cnt;i++)
{
AutoBandTop1_Note_On(midi_note_buff[i],0x50);
}
Return_Light_buff(KEY_ID_1629);
LOG_I("pick", "expert/fret change chord=%u", (unsigned)KEY_ID_1629);
} }
} }
} }
@ -1002,30 +1083,8 @@ void Pick_Handle()
break; break;
case 1: case 1:
StartFlag = 1; /* 首次拨片起奏与后续指板续奏共用同一专业起奏路径 */
if (mGuiData[GUI_AUTOBAND_SW].Current) Expert_StartWithChord(chord_id, midi_note_buff, note_cnt, "pick");
{
/* 本地曲目:与普通模式同一套起奏(0x3C 根音 + 和弦音) */
AutoBandTop1_Stop();
AutoBandTop1_SetLoopMode(0,1);
AutoBandTop1_SetRunVar(mGuiData[GUI_PLAY_SECTION].Current);
midi = 0x3C + mGuiData[GUI_TRANSPOSE].Current;
AutoBandTop1_StartWithNote(midi);
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
LOG_I("pick", "local/expert root=%u name=%s sw=%u tab=%u",
midi,
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?",
(unsigned)mGuiData[GUI_AUTOBAND_SW].Current,
(unsigned)mGuiData[GUI_TAB_INDEX].Current);
}
else
{
AutoBandTop1_SetPiecePlayMode(1,0,1920);
AutoBandTop1_StartWithNotes(midi_note_buff, note_cnt);
AutoBandTop1_SetLoopMode(0,1);
LOG_I("pick", "expert/rhythm piece name=%s",
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "?");
}
break; break;
case 0: case 0:
/* 万能:和弦走向用 piece 模式;须 SetLoopMode,否则无循环时常听不到伴奏 */ /* 万能:和弦走向用 piece 模式;须 SetLoopMode,否则无循环时常听不到伴奏 */
@ -1509,6 +1568,8 @@ void Read_Cur_Measure()
(tab == 0 || tab == 2) || (tab == 0 || tab == 2) ||
(tab == 3 && (last == 0 || last == 2)); (tab == 3 && (last == 0 || last == 2));
Band_TrackPlayTick();
if (loop_light_ok && StartFlag) if (loop_light_ok && StartFlag)
{ {
if(now - last_tick_i >= 10) if(now - last_tick_i >= 10)

View File

@ -0,0 +1,66 @@
"""One-off: expert rhythm (1.bin) piece mode ends after the piece window;
press fretboard only (no pick) after it stops -> must restart (trigger=fret + PITCH)."""
import os
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import rtt_pitch_reg_test as R
def main() -> int:
R.import_deps()
jlink = R.connect_jlink("AT32F403AC") # same default as rtt_pitch_reg_test
try:
cb = R.find_rtt_control_block(jlink)
if cb is None:
print("FAIL: RTT CB not found")
return 1
print(f"RTT CB @ 0x{cb:08X}", flush=True)
jlink.rtt_start(cb)
R.wait_rtt_ready(jlink)
sess = R.RttSession(jlink)
sess.pump(0.3)
sess.cmd("log clear", 0.3)
sess.cmd("tone bin1 0", 1.2) # expert + rhythm 1.bin (piece 0..1920ms)
sess.cmd("tone start", 0.8) # pick start, piece plays then stops
print(">> playing window (piece ~2s) ...", flush=True)
sess.pump(4.0) # wait until piece ended, PlayTick frozen
base = len(sess.lines)
sess.cmd("chord key 0", 0.4)
sess.cmd("chord key 8", 0.2) # fretboard only, no pick
sess.pump(3.0) # capture restart
new = sess.lines[base:]
joined = "\n".join(new)
trig = [l for l in new if "trigger=fret" in l]
change = [l for l in new if "expert/fret change" in l]
pitch = [l for l in new if "[PITCH] on" in l]
print("---- full session ----", flush=True)
for l in sess.lines:
print(l, flush=True)
print("---- captured after fret-only press ----", flush=True)
for l in new:
print(l, flush=True)
print("---- verdict ----", flush=True)
handled = trig or change
if handled and pitch:
path = "restart" if trig else "note_on-change"
print(f"PASS fret-only continue: path={path}, PITCH on x{len(pitch)}")
return 0
print("FAIL: no handler log or no pitch")
return 1
finally:
try:
sess.cmd("chord key 23", 0.3) # stop band, leave device quiet
sess.pump(0.3)
except Exception:
pass
jlink.close()
if __name__ == "__main__":
raise SystemExit(main())