Add bass/chord register mapping with RTT inject and bin1/3 probes.
Intercept AutoBand MIDI by channel (bass ch8, drum ch9), add chord-key RTT cmds, and ship scripts to verify 1.bin/3.bin pitch rules on device. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
91b270d856
commit
1cf6562236
102
APP/app_log.c
102
APP/app_log.c
|
|
@ -11,7 +11,7 @@ static uint32_t s_overflow;
|
||||||
static uint32_t s_boot_count;
|
static uint32_t s_boot_count;
|
||||||
static char s_ui_page[16] = "boot";
|
static char s_ui_page[16] = "boot";
|
||||||
static char s_debug_level = 'D';
|
static char s_debug_level = 'D';
|
||||||
static char s_cmd_buf[48];
|
static char s_cmd_buf[64];
|
||||||
static uint8_t s_cmd_len;
|
static uint8_t s_cmd_len;
|
||||||
|
|
||||||
/* RTT 二进制烧录:写到 W25Q128(ui0902 或本地曲目) */
|
/* RTT 二进制烧录:写到 W25Q128(ui0902 或本地曲目) */
|
||||||
|
|
@ -398,6 +398,73 @@ uint8_t app_log_try_command(const char *cmd)
|
||||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
return 1U;
|
return 1U;
|
||||||
}
|
}
|
||||||
|
/* tone bin1 [idx] → 1.bin 节奏(专业+节奏类型) */
|
||||||
|
if (strncmp(cmd, "tone bin1", 9) == 0) {
|
||||||
|
char line[160];
|
||||||
|
int ret;
|
||||||
|
unsigned idx = 0;
|
||||||
|
int count;
|
||||||
|
if (cmd[9] == ' ' && cmd[10] != '\0')
|
||||||
|
idx = (unsigned)strtoul(cmd + 10, NULL, 10);
|
||||||
|
mGuiData[GUI_TAB_INDEX].Current = 1; /* 专业 */
|
||||||
|
mGuiData[GUI_AUTOBAND_SW].Current = 0; /* 节奏类型→1.bin */
|
||||||
|
ParamGuiData[SONG_MODE_PARAM].Current = (uint8_t)idx;
|
||||||
|
UI_ApplyToneAddress();
|
||||||
|
AutoBandTop1_Stop();
|
||||||
|
StartFlag = 0;
|
||||||
|
count = AutoBandTop1_GetPresetItemCount();
|
||||||
|
if (count > 0 && (int)idx >= count)
|
||||||
|
idx = (unsigned)(count - 1);
|
||||||
|
ParamGuiData[SONG_MODE_PARAM].Current = (uint8_t)idx;
|
||||||
|
ret = AutoBandTop1_LoadPresetItemFromFlash((int)idx);
|
||||||
|
snprintf(line, sizeof(line),
|
||||||
|
"TONE_BIN1 ret=%d idx=%u addr=0x%08lX map=BIN1@0x%08lX name=%s count=%d %s\n",
|
||||||
|
ret, idx, (unsigned long)ADDRESS,
|
||||||
|
(unsigned long)EXTFLASH_BIN1_RHYTHM_ADDR,
|
||||||
|
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "(null)",
|
||||||
|
AutoBandTop1_GetPresetItemCount(),
|
||||||
|
(ADDRESS == EXTFLASH_BIN1_RHYTHM_ADDR) ? "OK" : "MISMATCH");
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
/* tone bin3 [idx] → 3.bin 万能和弦走向 */
|
||||||
|
if (strncmp(cmd, "tone bin3", 9) == 0) {
|
||||||
|
char line[160];
|
||||||
|
int ret;
|
||||||
|
unsigned idx = 0;
|
||||||
|
int count;
|
||||||
|
if (cmd[9] == ' ' && cmd[10] != '\0')
|
||||||
|
idx = (unsigned)strtoul(cmd + 10, NULL, 10);
|
||||||
|
mGuiData[GUI_TAB_INDEX].Current = 0; /* 万能 */
|
||||||
|
mGuiData[GUI_AUTOBAND_SW].Current = 0;
|
||||||
|
ParamGuiData[ALL_MODE_PARAM].Current = (uint8_t)idx;
|
||||||
|
UI_ApplyToneAddress();
|
||||||
|
AutoBandTop1_Stop();
|
||||||
|
StartFlag = 0;
|
||||||
|
count = AutoBandTop1_GetPresetItemCount();
|
||||||
|
if (count > 0 && (int)idx >= count)
|
||||||
|
idx = (unsigned)(count - 1);
|
||||||
|
ParamGuiData[ALL_MODE_PARAM].Current = (uint8_t)idx;
|
||||||
|
ret = AutoBandTop1_LoadPresetItemFromFlash((int)idx);
|
||||||
|
snprintf(line, sizeof(line),
|
||||||
|
"TONE_BIN3 ret=%d idx=%u addr=0x%08lX map=BIN3@0x%08lX name=%s count=%d %s\n",
|
||||||
|
ret, idx, (unsigned long)ADDRESS,
|
||||||
|
(unsigned long)EXTFLASH_BIN3_UNIVERSAL_ADDR,
|
||||||
|
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "(null)",
|
||||||
|
AutoBandTop1_GetPresetItemCount(),
|
||||||
|
(ADDRESS == EXTFLASH_BIN3_UNIVERSAL_ADDR) ? "OK" : "MISMATCH");
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
/* tone start:在当前已加载音色上拨片起奏(不改 TAB/库) */
|
||||||
|
if (strncmp(cmd, "tone start", 10) == 0) {
|
||||||
|
StartFlag = 0;
|
||||||
|
if (!(cmd[10] == ' ' && (cmd[11] == 'k' || cmd[11] == 'K')))
|
||||||
|
KEY_ID_1629 = 0;
|
||||||
|
Pick_Handle();
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "TONE_START_DONE\n");
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
if (strncmp(cmd, "tone pick", 9) == 0) {
|
if (strncmp(cmd, "tone pick", 9) == 0) {
|
||||||
/* tone pick uni → 万能;默认仍测专业+本地曲目 */
|
/* tone pick uni → 万能;默认仍测专业+本地曲目 */
|
||||||
if (cmd[9] == ' ' && (cmd[10] == 'u' || cmd[10] == 'U')) {
|
if (cmd[9] == ' ' && (cmd[10] == 'u' || cmd[10] == 'U')) {
|
||||||
|
|
@ -413,15 +480,44 @@ uint8_t app_log_try_command(const char *cmd)
|
||||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "TONE_PICK_UNI_DONE\n");
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "TONE_PICK_UNI_DONE\n");
|
||||||
return 1U;
|
return 1U;
|
||||||
}
|
}
|
||||||
/* 模拟专业+本地曲目拨片(无和弦板) */
|
/* 模拟专业+本地曲目拨片;chord pick keep → 保留已注入的 KEY_ID */
|
||||||
mGuiData[GUI_TAB_INDEX].Current = 1;
|
mGuiData[GUI_TAB_INDEX].Current = 1;
|
||||||
mGuiData[GUI_AUTOBAND_SW].Current = 1;
|
mGuiData[GUI_AUTOBAND_SW].Current = 1;
|
||||||
KEY_ID_1629 = 0; /* 走默认和弦兜底 */
|
if (!(cmd[9] == ' ' && (cmd[10] == 'k' || cmd[10] == 'K'))) {
|
||||||
|
KEY_ID_1629 = 0; /* 默认走一级和弦兜底 */
|
||||||
|
}
|
||||||
StartFlag = 0;
|
StartFlag = 0;
|
||||||
Pick_Handle();
|
Pick_Handle();
|
||||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "TONE_PICK_DONE\n");
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "TONE_PICK_DONE\n");
|
||||||
return 1U;
|
return 1U;
|
||||||
}
|
}
|
||||||
|
/* 测试注入:chord key N(0释放 / 1~21和弦 / 22拍速 / 23停止) */
|
||||||
|
if (strncmp(cmd, "chord key ", 10) == 0 && cmd[10] != '\0') {
|
||||||
|
unsigned key = (unsigned)strtoul(cmd + 10, NULL, 10);
|
||||||
|
char line[48];
|
||||||
|
if (key > 23U) {
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "CHORD_KEY_BAD\n");
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
app_tm1629_inject_key((uint8_t)key);
|
||||||
|
snprintf(line, sizeof(line), "CHORD_KEY_OK key=%u\n", key);
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
/* 测试注入:chord xpose N(0~11,C=0) */
|
||||||
|
if (strncmp(cmd, "chord xpose ", 12) == 0 && cmd[12] != '\0') {
|
||||||
|
unsigned xp = (unsigned)strtoul(cmd + 12, NULL, 10);
|
||||||
|
char line[48];
|
||||||
|
if (xp > 11U) {
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "CHORD_XPOSE_BAD\n");
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
|
mGuiData[GUI_TRANSPOSE].Current = (uint8_t)xp;
|
||||||
|
snprintf(line, sizeof(line), "CHORD_XPOSE_OK xp=%u\n", xp);
|
||||||
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||||
|
LOG_I("REG", "xpose set %u", xp);
|
||||||
|
return 1U;
|
||||||
|
}
|
||||||
if (strncmp(cmd, "flash erase chip", 16) == 0) {
|
if (strncmp(cmd, "flash erase chip", 16) == 0) {
|
||||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "FLASH_ERASE_BEGIN\n");
|
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "FLASH_ERASE_BEGIN\n");
|
||||||
LOG_I("FLASH", "W25Q128 chip erase start");
|
LOG_I("FLASH", "W25Q128 chip erase start");
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,13 @@ uint8_t app_tm1629_Scan_Key(void)
|
||||||
return KEY_NONE;
|
return KEY_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void app_tm1629_inject_key(uint8_t key)
|
||||||
|
{
|
||||||
|
key_last = key;
|
||||||
|
LOG_I("KEY", "inject key=%u", (unsigned)key);
|
||||||
|
TM1629_Handle(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void app_tm1629_led_set(LedNum_TypeDef led, LedColor_TypeDef color, uint8_t on)
|
static void app_tm1629_led_set(LedNum_TypeDef led, LedColor_TypeDef color, uint8_t on)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
void app_tm1629_init(void);
|
void app_tm1629_init(void);
|
||||||
uint8_t app_tm1629_Scan_Key(void);
|
uint8_t app_tm1629_Scan_Key(void);
|
||||||
|
void app_tm1629_inject_key(uint8_t key); /* RTT/测试:绕过扫描直接注入 */
|
||||||
void app_tm1629_set_chord_led(uint8_t key_idx, LedColor_TypeDef color);
|
void app_tm1629_set_chord_led(uint8_t key_idx, LedColor_TypeDef color);
|
||||||
void app_tm1629_set_led(LedNum_TypeDef led, LedColor_TypeDef color);
|
void app_tm1629_set_led(LedNum_TypeDef led, LedColor_TypeDef color);
|
||||||
void app_tm1629_all_off(void);
|
void app_tm1629_all_off(void);
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,318 @@ static uint8_t pStoreBuffer[PRESET_BUFFER_SIZE];
|
||||||
//static uint8_t* pStoreBuffer;
|
//static uint8_t* pStoreBuffer;
|
||||||
#define DEFAULT_TEMPO 100
|
#define DEFAULT_TEMPO 100
|
||||||
|
|
||||||
|
/* ======== 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 */
|
||||||
|
#define CHORD_REG_FIX_EN 1
|
||||||
|
#define BASS_CH 8 /* 日志 ch8 */
|
||||||
|
#define DRUM_CH 9 /* 日志 ch9 */
|
||||||
|
#define MIDI_E1 28
|
||||||
|
#define MIDI_E2 40
|
||||||
|
#define MIDI_GSHARP4 68
|
||||||
|
#define XPOSE_FS 6 /* C=0 时 #F=6 */
|
||||||
|
|
||||||
|
/* 映射策略指纹:变化时 CC123 清旧音,防挂音(不建大音符表) */
|
||||||
|
static uint8_t s_reg_fp = 0xFFu;
|
||||||
|
/* 每个指纹周期内 NoteOn 映射日志限额(和弦与 bass 分开,避免和弦占满看不到 bass) */
|
||||||
|
static uint8_t s_pitch_log_left = 0;
|
||||||
|
static uint8_t s_bass_log_left = 0;
|
||||||
|
static uint16_t s_ch_ever_mask = 0; /* 会话内各通道是否出现过 NoteOn */
|
||||||
|
#define PITCH_LOG_PER_FP 24
|
||||||
|
#define BASS_LOG_PER_FP 16
|
||||||
|
|
||||||
|
static int App_Auto_ClampMidi(int n)
|
||||||
|
{
|
||||||
|
if (n < 0) return 0;
|
||||||
|
if (n > 127) return 127;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int App_Auto_ChordDegree(void)
|
||||||
|
{
|
||||||
|
if (KEY_ID_1629 < 1 || KEY_ID_1629 > 21)
|
||||||
|
return 0;
|
||||||
|
return (int)((KEY_ID_1629 - 1) / 3) + 1; /* 1=I .. 7=VII */
|
||||||
|
}
|
||||||
|
|
||||||
|
static int App_Auto_NeedRegisterFix(void)
|
||||||
|
{
|
||||||
|
/* 键 1~6 = I/II:不动;7~21 = III~VII */
|
||||||
|
return (KEY_ID_1629 >= 7 && KEY_ID_1629 <= 21) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int App_Auto_XposeExtra(void)
|
||||||
|
{
|
||||||
|
return (mGuiData[GUI_TRANSPOSE].Current >= XPOSE_FS) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t App_Auto_RegFingerprint(void)
|
||||||
|
{
|
||||||
|
/* bit0=need_fix, bit1=xpose_extra, bit2.. = 粗粒度级数 (key/3) */
|
||||||
|
uint8_t fp = 0;
|
||||||
|
if (App_Auto_NeedRegisterFix()) fp |= 0x01u;
|
||||||
|
if (App_Auto_XposeExtra()) fp |= 0x02u;
|
||||||
|
if (KEY_ID_1629 >= 1 && KEY_ID_1629 <= 21)
|
||||||
|
fp |= (uint8_t)(((KEY_ID_1629 - 1) / 3) << 2);
|
||||||
|
return fp;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void App_Auto_QueueAllNotesOffMelodic(void)
|
||||||
|
{
|
||||||
|
MidiFifoItem_t m;
|
||||||
|
int ch;
|
||||||
|
for (ch = 0; ch < 16; ch++)
|
||||||
|
{
|
||||||
|
if (ch == DRUM_CH) continue;
|
||||||
|
m.msg[0] = (uint8_t)(0xB0 | ch);
|
||||||
|
m.msg[1] = 123; /* All Notes Off */
|
||||||
|
m.msg[2] = 0;
|
||||||
|
m.msglen = 3;
|
||||||
|
mymidififo_InQueue(&m_MidiSendFifo, &m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 超出 #G4:在当前和弦内音(GetChordNotesByType,对照和弦表「和弦伴奏」列)中就近 */
|
||||||
|
static int App_Auto_NearestChordTone(int target)
|
||||||
|
{
|
||||||
|
uint8_t buff[4] = {0, 0, 0, 0};
|
||||||
|
uint8_t n;
|
||||||
|
uint8_t pcs[4];
|
||||||
|
int i, oct, best = -1, best_dist = 9999;
|
||||||
|
uint8_t type;
|
||||||
|
|
||||||
|
if (KEY_ID_1629 < 1 || KEY_ID_1629 > 21)
|
||||||
|
return App_Auto_ClampMidi(target);
|
||||||
|
|
||||||
|
type = chord_type_index_map[KEY_ID_1629].type;
|
||||||
|
n = GetChordNotesByType(KEY_ID_1629, type, buff);
|
||||||
|
if (n == 0)
|
||||||
|
return App_Auto_ClampMidi(target > MIDI_GSHARP4 ? MIDI_GSHARP4 : target);
|
||||||
|
|
||||||
|
for (i = 0; i < (int)n; i++)
|
||||||
|
pcs[i] = (uint8_t)(buff[i] % 12);
|
||||||
|
|
||||||
|
for (i = 0; i < (int)n; i++)
|
||||||
|
{
|
||||||
|
for (oct = 0; oct < 11; oct++)
|
||||||
|
{
|
||||||
|
int cand = (int)pcs[i] + oct * 12;
|
||||||
|
int dist;
|
||||||
|
if (cand < MIDI_E2 || cand > MIDI_GSHARP4)
|
||||||
|
continue;
|
||||||
|
dist = cand - target;
|
||||||
|
if (dist < 0) dist = -dist;
|
||||||
|
/* 并列:优先较低者(不超过上限) */
|
||||||
|
if (dist < best_dist || (dist == best_dist && (best < 0 || cand < best)))
|
||||||
|
{
|
||||||
|
best_dist = dist;
|
||||||
|
best = cand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (best < 0)
|
||||||
|
return MIDI_GSHARP4;
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int App_Auto_MapBassNote(int key, uint8_t *flags)
|
||||||
|
{
|
||||||
|
int out = key - 12;
|
||||||
|
uint8_t f = 0x01u; /* bit0: -12 applied */
|
||||||
|
while (out < MIDI_E1)
|
||||||
|
{
|
||||||
|
out += 12;
|
||||||
|
f |= 0x02u; /* bit1: floored up to E1 */
|
||||||
|
}
|
||||||
|
if (App_Auto_XposeExtra())
|
||||||
|
{
|
||||||
|
out -= 12;
|
||||||
|
f |= 0x04u; /* bit2: xpose extra -12 */
|
||||||
|
while (out < MIDI_E1)
|
||||||
|
{
|
||||||
|
out += 12;
|
||||||
|
f |= 0x02u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (flags) *flags = f;
|
||||||
|
return App_Auto_ClampMidi(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int App_Auto_MapChordNote(int key, uint8_t *flags)
|
||||||
|
{
|
||||||
|
int out = key - 12;
|
||||||
|
uint8_t f = 0x01u; /* bit0: -12 */
|
||||||
|
|
||||||
|
while (out < MIDI_E2)
|
||||||
|
{
|
||||||
|
out += 12;
|
||||||
|
f |= 0x08u; /* bit3: raised to E2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out > MIDI_GSHARP4)
|
||||||
|
{
|
||||||
|
out = App_Auto_NearestChordTone(out);
|
||||||
|
f |= 0x10u; /* bit4: nearest chord tone */
|
||||||
|
}
|
||||||
|
|
||||||
|
if (App_Auto_XposeExtra())
|
||||||
|
{
|
||||||
|
out -= 12;
|
||||||
|
f |= 0x04u;
|
||||||
|
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;
|
||||||
|
if (flags) *flags = 0;
|
||||||
|
|
||||||
|
#if CHORD_REG_FIX_EN
|
||||||
|
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);
|
||||||
|
else
|
||||||
|
out = App_Auto_MapChordNote(key, flags);
|
||||||
|
#else
|
||||||
|
(void)channel;
|
||||||
|
#endif
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *App_Auto_RoleName(int channel)
|
||||||
|
{
|
||||||
|
if (channel == DRUM_CH) return "drum";
|
||||||
|
if (channel == BASS_CH) return "bass";
|
||||||
|
return "chord";
|
||||||
|
}
|
||||||
|
|
||||||
|
static void App_Auto_LogPitch(int channel, int key, int out, uint8_t flags, int is_on)
|
||||||
|
{
|
||||||
|
const char *role;
|
||||||
|
int deg;
|
||||||
|
int fix;
|
||||||
|
int xf;
|
||||||
|
int pass;
|
||||||
|
|
||||||
|
if (channel == DRUM_CH)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (channel == BASS_CH)
|
||||||
|
{
|
||||||
|
if (s_bass_log_left == 0)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (s_pitch_log_left == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
role = App_Auto_RoleName(channel);
|
||||||
|
deg = App_Auto_ChordDegree();
|
||||||
|
fix = App_Auto_NeedRegisterFix();
|
||||||
|
xf = App_Auto_XposeExtra();
|
||||||
|
pass = (!fix || 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",
|
||||||
|
(unsigned)channel, role, /* 0 基,与 BASS_CH=8 一致 */
|
||||||
|
key, out, out - key,
|
||||||
|
deg, (unsigned)KEY_ID_1629,
|
||||||
|
(unsigned)mGuiData[GUI_TRANSPOSE].Current, xf,
|
||||||
|
(unsigned)flags,
|
||||||
|
pass ? "PASS " : "",
|
||||||
|
(flags & 0x10u) ? "NEAR " : "",
|
||||||
|
(flags & 0x02u) ? "E1UP " : "",
|
||||||
|
(flags & 0x08u) ? "E2UP " : "");
|
||||||
|
|
||||||
|
if (channel == BASS_CH)
|
||||||
|
{
|
||||||
|
if (s_bass_log_left > 0)
|
||||||
|
s_bass_log_left--;
|
||||||
|
}
|
||||||
|
else if (s_pitch_log_left > 0)
|
||||||
|
{
|
||||||
|
s_pitch_log_left--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NoteOn 路径:策略变化时清旧音;返回映射后音高 */
|
||||||
|
static int App_Auto_MapNoteOn(int channel, int key)
|
||||||
|
{
|
||||||
|
#if CHORD_REG_FIX_EN
|
||||||
|
uint8_t fp = App_Auto_RegFingerprint();
|
||||||
|
uint8_t flags = 0;
|
||||||
|
int out;
|
||||||
|
|
||||||
|
if (fp != s_reg_fp)
|
||||||
|
{
|
||||||
|
if (s_reg_fp != 0xFFu)
|
||||||
|
{
|
||||||
|
LOG_I("REG", "fp %u->%u chord=%u deg=%d xp=%u xf=%d",
|
||||||
|
(unsigned)s_reg_fp, (unsigned)fp,
|
||||||
|
(unsigned)KEY_ID_1629,
|
||||||
|
App_Auto_ChordDegree(),
|
||||||
|
(unsigned)mGuiData[GUI_TRANSPOSE].Current,
|
||||||
|
App_Auto_XposeExtra());
|
||||||
|
App_Auto_QueueAllNotesOffMelodic();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG_I("REG", "fp init %u chord=%u deg=%d xp=%u",
|
||||||
|
(unsigned)fp, (unsigned)KEY_ID_1629,
|
||||||
|
App_Auto_ChordDegree(),
|
||||||
|
(unsigned)mGuiData[GUI_TRANSPOSE].Current);
|
||||||
|
}
|
||||||
|
s_reg_fp = fp;
|
||||||
|
s_pitch_log_left = PITCH_LOG_PER_FP;
|
||||||
|
s_bass_log_left = BASS_LOG_PER_FP;
|
||||||
|
s_ch_ever_mask = 0; /* 新策略周期重新报到通道 */
|
||||||
|
}
|
||||||
|
|
||||||
|
out = App_Auto_MapNote(channel, key, &flags);
|
||||||
|
if (channel >= 0 && channel < 16)
|
||||||
|
{
|
||||||
|
uint16_t bit = (uint16_t)(1u << channel);
|
||||||
|
if ((s_ch_ever_mask & bit) == 0u)
|
||||||
|
{
|
||||||
|
s_ch_ever_mask |= bit;
|
||||||
|
LOG_I("REG", "ch-seen ch%u role=%s key=%d",
|
||||||
|
(unsigned)channel, App_Auto_RoleName(channel), key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
App_Auto_LogPitch(channel, key, out, flags, 1);
|
||||||
|
return out;
|
||||||
|
#else
|
||||||
|
(void)channel;
|
||||||
|
return key;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
static void Func_CallBack_ProgramChange(int channel, int program)
|
static void Func_CallBack_ProgramChange(int channel, int program)
|
||||||
{
|
{
|
||||||
MidiFifoItem_t midimsg;
|
MidiFifoItem_t midimsg;
|
||||||
midimsg.msg[0] = 0xc0 | channel;
|
LOG_I("REG", "pc ch%u prog=%d", (unsigned)channel, program);
|
||||||
|
midimsg.msg[0] = 0xc0 | (channel & 0x0F);
|
||||||
midimsg.msg[1] = program;
|
midimsg.msg[1] = program;
|
||||||
midimsg.msg[2] = 0;
|
midimsg.msg[2] = 0;
|
||||||
midimsg.msglen = 2;
|
midimsg.msglen = 2;
|
||||||
|
|
@ -32,8 +340,10 @@ static void Func_CallBack_ProgramChange(int channel, int program)
|
||||||
static void Func_CallBack_NoteOff(int channel,int key)
|
static void Func_CallBack_NoteOff(int channel,int key)
|
||||||
{
|
{
|
||||||
MidiFifoItem_t midimsg;
|
MidiFifoItem_t midimsg;
|
||||||
midimsg.msg[0] = 0x80 | channel;
|
uint8_t flags = 0;
|
||||||
midimsg.msg[1] = key;
|
int out_key = App_Auto_MapNote(channel, key, &flags);
|
||||||
|
midimsg.msg[0] = 0x80 | (channel & 0x0F);
|
||||||
|
midimsg.msg[1] = (uint8_t)out_key;
|
||||||
midimsg.msg[2] = 0;
|
midimsg.msg[2] = 0;
|
||||||
midimsg.msglen = 3;
|
midimsg.msglen = 3;
|
||||||
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
|
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
|
||||||
|
|
@ -43,9 +353,19 @@ static void Func_CallBack_NoteOff(int channel,int key)
|
||||||
static void Func_CallBack_NoteOn(int channel,int key,int vel)
|
static void Func_CallBack_NoteOn(int channel,int key,int vel)
|
||||||
{
|
{
|
||||||
MidiFifoItem_t midimsg;
|
MidiFifoItem_t midimsg;
|
||||||
midimsg.msg[0] = 0x90 | channel;
|
int out_key;
|
||||||
midimsg.msg[1] = key;
|
|
||||||
midimsg.msg[2] = vel;
|
if (vel == 0)
|
||||||
|
{
|
||||||
|
uint8_t flags = 0;
|
||||||
|
out_key = App_Auto_MapNote(channel, key, &flags); /* 视同 NoteOff */
|
||||||
|
}
|
||||||
|
else
|
||||||
|
out_key = App_Auto_MapNoteOn(channel, key);
|
||||||
|
|
||||||
|
midimsg.msg[0] = 0x90 | (channel & 0x0F);
|
||||||
|
midimsg.msg[1] = (uint8_t)out_key;
|
||||||
|
midimsg.msg[2] = (uint8_t)vel;
|
||||||
midimsg.msglen = 3;
|
midimsg.msglen = 3;
|
||||||
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
|
mymidififo_InQueue(&m_MidiSendFifo,&midimsg);
|
||||||
|
|
||||||
|
|
@ -135,6 +455,9 @@ static int Func_CallBack_ReadFlash(int address,int length,uint8_t* pOutput)
|
||||||
//
|
//
|
||||||
void App_Auto_Init(void)
|
void App_Auto_Init(void)
|
||||||
{
|
{
|
||||||
|
LOG_I("REG", "ch map bass=%d drum=%d fix=%d E1=%d E2=%d Gs4=%d",
|
||||||
|
BASS_CH, DRUM_CH, CHORD_REG_FIX_EN, MIDI_E1, MIDI_E2, MIDI_GSHARP4);
|
||||||
|
LOG_I("REG", "PITCH log: chN(role) in->out d=delta deg=I..VII xp=xpose xf=#Fextra fl=flags");
|
||||||
AutoBandTop1_SetPresetStoreBuffer(pStoreBuffer,PRESET_BUFFER_SIZE);
|
AutoBandTop1_SetPresetStoreBuffer(pStoreBuffer,PRESET_BUFFER_SIZE);
|
||||||
AutoBandTop1_Init();
|
AutoBandTop1_Init();
|
||||||
AutoBandTop1_RegisterCallBack_NoteOn(Func_CallBack_NoteOn);
|
AutoBandTop1_RegisterCallBack_NoteOn(Func_CallBack_NoteOn);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,387 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Laptop-mic pitch analyzer for K1 accompaniment register checks.
|
||||||
|
|
||||||
|
Uses autocorrelation F0 estimate (numpy only + sounddevice).
|
||||||
|
Helps verify bass/chord octave when MIDI channel logs lack ch8(bass).
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
python tools/mic_pitch_analyze.py --list
|
||||||
|
python tools/mic_pitch_analyze.py --seconds 8 --out mic_i.log
|
||||||
|
python tools/mic_pitch_analyze.py --live
|
||||||
|
python tools/mic_pitch_analyze.py --compare mic_i.wav mic_iii.wav
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import wave
|
||||||
|
from collections import Counter
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
try:
|
||||||
|
import sounddevice as sd
|
||||||
|
except ImportError as exc:
|
||||||
|
raise SystemExit("Missing sounddevice: pip install sounddevice") from exc
|
||||||
|
|
||||||
|
A4_HZ = 440.0
|
||||||
|
A4_MIDI = 69
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PitchFrame:
|
||||||
|
t: float
|
||||||
|
hz: float
|
||||||
|
midi: float
|
||||||
|
note: str
|
||||||
|
conf: float
|
||||||
|
rms: float
|
||||||
|
|
||||||
|
|
||||||
|
NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
|
||||||
|
|
||||||
|
|
||||||
|
def hz_to_midi(hz: float) -> float:
|
||||||
|
if hz <= 0:
|
||||||
|
return float("nan")
|
||||||
|
return A4_MIDI + 12.0 * np.log2(hz / A4_HZ)
|
||||||
|
|
||||||
|
|
||||||
|
def midi_to_name(midi: float) -> str:
|
||||||
|
if not np.isfinite(midi):
|
||||||
|
return "--"
|
||||||
|
n = int(round(midi))
|
||||||
|
return f"{NOTE_NAMES[n % 12]}{n // 12 - 1}"
|
||||||
|
|
||||||
|
|
||||||
|
def list_devices() -> None:
|
||||||
|
if hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
for i, d in enumerate(sd.query_devices()):
|
||||||
|
name = str(d.get("name", "")).encode("utf-8", "replace").decode("utf-8", "replace")
|
||||||
|
print(
|
||||||
|
f"{i}: in={d['max_input_channels']} out={d['max_output_channels']} "
|
||||||
|
f"sr={d['default_samplerate']} {name}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
print("default device pair:", sd.default.device, flush=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def record(seconds: float, sr: int, device: int | None) -> np.ndarray:
|
||||||
|
frames = int(seconds * sr)
|
||||||
|
print(f"Recording {seconds:.1f}s @ {sr} Hz (Ctrl+C to abort)...", flush=True)
|
||||||
|
audio = sd.rec(frames, samplerate=sr, channels=1, dtype="float32", device=device)
|
||||||
|
sd.wait()
|
||||||
|
return audio[:, 0]
|
||||||
|
|
||||||
|
|
||||||
|
def save_wav(path: str, audio: np.ndarray, sr: int) -> None:
|
||||||
|
pcm = np.clip(audio, -1.0, 1.0)
|
||||||
|
pcm16 = (pcm * 32767.0).astype(np.int16)
|
||||||
|
with wave.open(path, "wb") as w:
|
||||||
|
w.setnchannels(1)
|
||||||
|
w.setsampwidth(2)
|
||||||
|
w.setframerate(sr)
|
||||||
|
w.writeframes(pcm16.tobytes())
|
||||||
|
|
||||||
|
|
||||||
|
def load_wav(path: str) -> tuple[np.ndarray, int]:
|
||||||
|
with wave.open(path, "rb") as w:
|
||||||
|
sr = w.getframerate()
|
||||||
|
nch = w.getnchannels()
|
||||||
|
raw = w.readframes(w.getnframes())
|
||||||
|
pcm = np.frombuffer(raw, dtype=np.int16).astype(np.float32) / 32768.0
|
||||||
|
if nch > 1:
|
||||||
|
pcm = pcm.reshape(-1, nch).mean(axis=1)
|
||||||
|
return pcm, sr
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_f0_acorr(
|
||||||
|
frame: np.ndarray,
|
||||||
|
sr: int,
|
||||||
|
fmin: float,
|
||||||
|
fmax: float,
|
||||||
|
) -> tuple[float, float]:
|
||||||
|
"""Return (hz, confidence). confidence in [0,1] from normalized peak."""
|
||||||
|
x = frame.astype(np.float64)
|
||||||
|
x = x - np.mean(x)
|
||||||
|
rms = float(np.sqrt(np.mean(x * x)) + 1e-12)
|
||||||
|
if rms < 1e-4:
|
||||||
|
return 0.0, 0.0
|
||||||
|
|
||||||
|
# Hamming window
|
||||||
|
x = x * np.hamming(len(x))
|
||||||
|
corr = np.correlate(x, x, mode="full")
|
||||||
|
corr = corr[len(corr) // 2 :]
|
||||||
|
|
||||||
|
i_min = max(1, int(sr / fmax))
|
||||||
|
i_max = min(len(corr) - 1, int(sr / fmin))
|
||||||
|
if i_max <= i_min:
|
||||||
|
return 0.0, 0.0
|
||||||
|
|
||||||
|
seg = corr[i_min : i_max + 1]
|
||||||
|
peak_rel = int(np.argmax(seg))
|
||||||
|
peak = peak_rel + i_min
|
||||||
|
if corr[0] <= 1e-12:
|
||||||
|
return 0.0, 0.0
|
||||||
|
conf = float(corr[peak] / corr[0])
|
||||||
|
if conf < 0.25:
|
||||||
|
return 0.0, conf
|
||||||
|
|
||||||
|
# parabolic interpolation around peak
|
||||||
|
if 1 <= peak < len(corr) - 1:
|
||||||
|
a, b, c = corr[peak - 1], corr[peak], corr[peak + 1]
|
||||||
|
denom = a - 2 * b + c
|
||||||
|
if abs(denom) > 1e-12:
|
||||||
|
peak = peak + 0.5 * (a - c) / denom
|
||||||
|
|
||||||
|
hz = float(sr / peak)
|
||||||
|
if hz < fmin or hz > fmax:
|
||||||
|
return 0.0, conf
|
||||||
|
return hz, conf
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_audio(
|
||||||
|
audio: np.ndarray,
|
||||||
|
sr: int,
|
||||||
|
hop_ms: float = 50.0,
|
||||||
|
win_ms: float = 80.0,
|
||||||
|
fmin: float = 40.0,
|
||||||
|
fmax: float = 600.0,
|
||||||
|
conf_min: float = 0.35,
|
||||||
|
rms_min: float = 0.01,
|
||||||
|
) -> list[PitchFrame]:
|
||||||
|
hop = max(1, int(sr * hop_ms / 1000.0))
|
||||||
|
win = max(hop, int(sr * win_ms / 1000.0))
|
||||||
|
out: list[PitchFrame] = []
|
||||||
|
if len(audio) < win:
|
||||||
|
return out
|
||||||
|
|
||||||
|
for start in range(0, len(audio) - win, hop):
|
||||||
|
frame = audio[start : start + win]
|
||||||
|
rms = float(np.sqrt(np.mean(frame.astype(np.float64) ** 2)))
|
||||||
|
hz, conf = estimate_f0_acorr(frame, sr, fmin, fmax)
|
||||||
|
t = start / sr
|
||||||
|
if hz <= 0 or conf < conf_min or rms < rms_min:
|
||||||
|
out.append(PitchFrame(t, 0.0, float("nan"), "--", conf, rms))
|
||||||
|
continue
|
||||||
|
midi = hz_to_midi(hz)
|
||||||
|
out.append(PitchFrame(t, hz, midi, midi_to_name(midi), conf, rms))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def summarize(frames: list[PitchFrame], label: str = "") -> dict:
|
||||||
|
voiced = [f for f in frames if f.hz > 0 and np.isfinite(f.midi)]
|
||||||
|
if not voiced:
|
||||||
|
return {"label": label, "voiced": 0, "total": len(frames)}
|
||||||
|
|
||||||
|
midis = np.array([f.midi for f in voiced], dtype=np.float64)
|
||||||
|
hz = np.array([f.hz for f in voiced], dtype=np.float64)
|
||||||
|
notes = [f.note for f in voiced]
|
||||||
|
# round to nearest MIDI for histogram
|
||||||
|
rounded = [int(round(m)) for m in midis]
|
||||||
|
top = Counter(rounded).most_common(8)
|
||||||
|
# bass-ish: MIDI <= 48 (C3)
|
||||||
|
bass_ratio = float(np.mean(midis <= 48.0))
|
||||||
|
low_ratio = float(np.mean(midis <= 40.0)) # <= E2
|
||||||
|
return {
|
||||||
|
"label": label,
|
||||||
|
"voiced": len(voiced),
|
||||||
|
"total": len(frames),
|
||||||
|
"hz_median": float(np.median(hz)),
|
||||||
|
"hz_p10": float(np.percentile(hz, 10)),
|
||||||
|
"hz_p90": float(np.percentile(hz, 90)),
|
||||||
|
"midi_median": float(np.median(midis)),
|
||||||
|
"midi_p10": float(np.percentile(midis, 10)),
|
||||||
|
"midi_p90": float(np.percentile(midis, 90)),
|
||||||
|
"note_median": midi_to_name(float(np.median(midis))),
|
||||||
|
"top_notes": [(midi_to_name(float(n)), c) for n, c in top],
|
||||||
|
"bass_le_C3_ratio": bass_ratio,
|
||||||
|
"low_le_E2_ratio": low_ratio,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def print_summary(s: dict) -> None:
|
||||||
|
if s.get("voiced", 0) == 0:
|
||||||
|
print(f"[{s.get('label','')}] no pitched frames", flush=True)
|
||||||
|
return
|
||||||
|
print(
|
||||||
|
f"[{s.get('label','')}] voiced={s['voiced']}/{s['total']} "
|
||||||
|
f"median={s['note_median']} ({s['midi_median']:.1f} / {s['hz_median']:.1f}Hz) "
|
||||||
|
f"p10={s['midi_p10']:.1f} p90={s['midi_p90']:.1f} "
|
||||||
|
f"<=E2={s['low_le_E2_ratio']*100:.0f}% <=C3={s['bass_le_C3_ratio']*100:.0f}%",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
tops = ", ".join(f"{n}×{c}" for n, c in s["top_notes"][:5])
|
||||||
|
print(f" top: {tops}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def write_frame_log(path: str, frames: list[PitchFrame], summary: dict) -> None:
|
||||||
|
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(f"# mic pitch {datetime.now().isoformat(timespec='seconds')}\n")
|
||||||
|
f.write("# " + json.dumps(summary, ensure_ascii=False) + "\n")
|
||||||
|
f.write("t_s,hz,midi,note,conf,rms\n")
|
||||||
|
for fr in frames:
|
||||||
|
midi = "" if not np.isfinite(fr.midi) else f"{fr.midi:.2f}"
|
||||||
|
f.write(
|
||||||
|
f"{fr.t:.3f},{fr.hz:.2f},{midi},{fr.note},{fr.conf:.3f},{fr.rms:.4f}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def live_monitor(
|
||||||
|
sr: int,
|
||||||
|
device: int | None,
|
||||||
|
fmin: float,
|
||||||
|
fmax: float,
|
||||||
|
seconds: float,
|
||||||
|
) -> None:
|
||||||
|
win = int(sr * 0.08)
|
||||||
|
hop = int(sr * 0.05)
|
||||||
|
print("Live pitch (Ctrl+C stop)...", flush=True)
|
||||||
|
buf = np.zeros(0, dtype=np.float32)
|
||||||
|
t0 = time.time()
|
||||||
|
|
||||||
|
def callback(indata, frames, time_info, status): # noqa: ARG001
|
||||||
|
nonlocal buf
|
||||||
|
if status:
|
||||||
|
print(status, flush=True)
|
||||||
|
buf = np.concatenate([buf, indata[:, 0].copy()])
|
||||||
|
while len(buf) >= win:
|
||||||
|
frame = buf[:win]
|
||||||
|
buf = buf[hop:]
|
||||||
|
hz, conf = estimate_f0_acorr(frame, sr, fmin, fmax)
|
||||||
|
rms = float(np.sqrt(np.mean(frame.astype(np.float64) ** 2)))
|
||||||
|
if hz > 0 and conf >= 0.35 and rms >= 0.01:
|
||||||
|
midi = hz_to_midi(hz)
|
||||||
|
print(
|
||||||
|
f"{time.time()-t0:6.1f}s {midi_to_name(midi):4s} "
|
||||||
|
f"midi={midi:5.1f} {hz:6.1f}Hz conf={conf:.2f} rms={rms:.3f}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with sd.InputStream(samplerate=sr, channels=1, dtype="float32", device=device, callback=callback):
|
||||||
|
if seconds > 0:
|
||||||
|
sd.sleep(int(seconds * 1000))
|
||||||
|
else:
|
||||||
|
while True:
|
||||||
|
sd.sleep(200)
|
||||||
|
|
||||||
|
|
||||||
|
def compare_summaries(a: dict, b: dict) -> None:
|
||||||
|
print("\n======== COMPARE ========", flush=True)
|
||||||
|
print_summary(a)
|
||||||
|
print_summary(b)
|
||||||
|
if a.get("voiced", 0) == 0 or b.get("voiced", 0) == 0:
|
||||||
|
print("Need pitched content in both takes.", flush=True)
|
||||||
|
return
|
||||||
|
d_midi = b["midi_median"] - a["midi_median"]
|
||||||
|
print(f"median delta (B-A): {d_midi:+.2f} semitones", flush=True)
|
||||||
|
if d_midi <= -9:
|
||||||
|
print("PASS-ish: B is about an octave lower than A (expected III+ vs I/II).", flush=True)
|
||||||
|
elif d_midi <= -5:
|
||||||
|
print("PARTIAL: B lower than A but less than full octave.", flush=True)
|
||||||
|
elif abs(d_midi) < 2:
|
||||||
|
print("FAIL-ish: medians similar — register fix may not be audible on mic mix.", flush=True)
|
||||||
|
else:
|
||||||
|
print("CHECK: unexpected direction/amount; inspect top notes / low_le_E2 ratios.", flush=True)
|
||||||
|
print(
|
||||||
|
f"low<=E2: A={a['low_le_E2_ratio']*100:.0f}% B={b['low_le_E2_ratio']*100:.0f}%",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
if hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
p = argparse.ArgumentParser(description="Mic pitch analyzer for K1 register tests")
|
||||||
|
p.add_argument("--list", action="store_true", help="List audio devices")
|
||||||
|
p.add_argument("--device", type=int, default=None, help="Input device index")
|
||||||
|
p.add_argument("--sr", type=int, default=16000)
|
||||||
|
p.add_argument("--seconds", type=float, default=8.0)
|
||||||
|
p.add_argument("--live", action="store_true")
|
||||||
|
p.add_argument("--wav", default="", help="Analyze existing wav instead of recording")
|
||||||
|
p.add_argument("--out", default="", help="Prefix for wav/csv outputs")
|
||||||
|
p.add_argument("--fmin", type=float, default=40.0, help="Min F0 Hz (bass ~41=E1)")
|
||||||
|
p.add_argument("--fmax", type=float, default=600.0, help="Max F0 Hz")
|
||||||
|
p.add_argument(
|
||||||
|
"--compare",
|
||||||
|
nargs=2,
|
||||||
|
metavar=("A", "B"),
|
||||||
|
help="Compare two wav/csv summary sources (wav preferred)",
|
||||||
|
)
|
||||||
|
p.add_argument("--label", default="")
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
if args.list:
|
||||||
|
list_devices()
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.compare:
|
||||||
|
summaries = []
|
||||||
|
for i, path in enumerate(args.compare):
|
||||||
|
label = "A" if i == 0 else "B"
|
||||||
|
if path.lower().endswith(".wav"):
|
||||||
|
audio, sr = load_wav(path)
|
||||||
|
frames = analyze_audio(audio, sr, fmin=args.fmin, fmax=args.fmax)
|
||||||
|
s = summarize(frames, label=f"{label}:{os.path.basename(path)}")
|
||||||
|
else:
|
||||||
|
raise SystemExit("compare expects .wav files")
|
||||||
|
summaries.append(s)
|
||||||
|
compare_summaries(summaries[0], summaries[1])
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.live:
|
||||||
|
live_monitor(args.sr, args.device, args.fmin, args.fmax, args.seconds if args.seconds > 0 else 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
prefix = args.out or f"mic_pitch_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||||
|
if args.wav:
|
||||||
|
audio, sr = load_wav(args.wav)
|
||||||
|
label = args.label or os.path.basename(args.wav)
|
||||||
|
else:
|
||||||
|
audio = record(args.seconds, args.sr, args.device)
|
||||||
|
sr = args.sr
|
||||||
|
label = args.label or "rec"
|
||||||
|
wav_path = prefix if prefix.lower().endswith(".wav") else prefix + ".wav"
|
||||||
|
save_wav(wav_path, audio, sr)
|
||||||
|
print(f"Saved wav: {os.path.abspath(wav_path)}", flush=True)
|
||||||
|
|
||||||
|
frames = analyze_audio(audio, sr, fmin=args.fmin, fmax=args.fmax)
|
||||||
|
summary = summarize(frames, label=label)
|
||||||
|
print_summary(summary)
|
||||||
|
log_path = prefix if prefix.lower().endswith(".csv") else prefix + ".csv"
|
||||||
|
# if prefix was .wav, still write .csv alongside
|
||||||
|
if log_path.lower().endswith(".wav.csv"):
|
||||||
|
log_path = log_path[:-8] + ".csv"
|
||||||
|
elif prefix.lower().endswith(".wav"):
|
||||||
|
log_path = prefix[:-4] + ".csv"
|
||||||
|
write_frame_log(log_path, frames, summary)
|
||||||
|
print(f"Saved log: {os.path.abspath(log_path)}", flush=True)
|
||||||
|
print(
|
||||||
|
"Tip: record I/II then III+ separately, then:\n"
|
||||||
|
" python tools/mic_pitch_analyze.py --compare mic_i.wav mic_iii.wav",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nStopped.", flush=True)
|
||||||
|
sys.exit(130)
|
||||||
|
|
@ -0,0 +1,276 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Probe 1.bin (rhythm) and 3.bin (universal) for bass ch8 + plan compliance.
|
||||||
|
|
||||||
|
Requires FW with: tone bin1 / tone bin3 / tone start / chord key / chord xpose / PITCH ch8(bass)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from rtt_pitch_reg_test import ( # noqa: E402
|
||||||
|
RttSession,
|
||||||
|
analyze_lines,
|
||||||
|
connect_jlink,
|
||||||
|
expect_for_event,
|
||||||
|
find_rtt_control_block,
|
||||||
|
import_deps,
|
||||||
|
parse_pitch_line,
|
||||||
|
wait_rtt_ready,
|
||||||
|
)
|
||||||
|
|
||||||
|
PITCH_RE_CH = re.compile(r"\[PITCH\].*?ch(\d+)\((\w+)\)")
|
||||||
|
CH_SEEN_RE = re.compile(r"ch-seen ch(\d+) role=(\w+) key=(-?\d+)")
|
||||||
|
TONE_OK_RE = re.compile(r"TONE_BIN([13]).*name=(\S+).*count=(\d+)")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PresetResult:
|
||||||
|
bin_id: str
|
||||||
|
idx: int
|
||||||
|
name: str = ""
|
||||||
|
channels: Counter = field(default_factory=Counter)
|
||||||
|
roles: Counter = field(default_factory=Counter)
|
||||||
|
bass_n: int = 0
|
||||||
|
drum_n: int = 0
|
||||||
|
chord_n: int = 0
|
||||||
|
pass_ok: int = 0
|
||||||
|
pass_fail: int = 0
|
||||||
|
iii_ok: int = 0
|
||||||
|
iii_fail: int = 0
|
||||||
|
xf_ok: int = 0
|
||||||
|
xf_fail: int = 0
|
||||||
|
bass_ok: int = 0
|
||||||
|
bass_fail: int = 0
|
||||||
|
samples: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
def run_preset(sess: RttSession, bin_id: str, idx: int, hold: float) -> PresetResult:
|
||||||
|
res = PresetResult(bin_id=bin_id, idx=idx)
|
||||||
|
mark = len(sess.lines)
|
||||||
|
|
||||||
|
if bin_id == "1":
|
||||||
|
ok = sess.cmd_ack(f"tone bin1 {idx}", "TONE_BIN1", timeout_s=3.0, retries=4)
|
||||||
|
else:
|
||||||
|
ok = sess.cmd_ack(f"tone bin3 {idx}", "TONE_BIN3", timeout_s=3.0, retries=4)
|
||||||
|
if not ok:
|
||||||
|
res.samples.append("FAIL load ack")
|
||||||
|
return res
|
||||||
|
|
||||||
|
# parse name from recent lines
|
||||||
|
for line in sess.lines[-8:]:
|
||||||
|
m = TONE_OK_RE.search(line)
|
||||||
|
if m and m.group(1) == bin_id:
|
||||||
|
res.name = m.group(2)
|
||||||
|
break
|
||||||
|
|
||||||
|
sess.set_xpose(2)
|
||||||
|
sess.cmd("tone start", settle=0.7)
|
||||||
|
|
||||||
|
# I/II
|
||||||
|
sess.set_key(0)
|
||||||
|
sess.set_key(2)
|
||||||
|
sess.pump(hold)
|
||||||
|
|
||||||
|
# III
|
||||||
|
sess.set_key(0)
|
||||||
|
sess.set_key(8)
|
||||||
|
sess.pump(hold)
|
||||||
|
|
||||||
|
# VII + #F
|
||||||
|
sess.set_xpose(6)
|
||||||
|
sess.set_key(0)
|
||||||
|
sess.set_key(20)
|
||||||
|
sess.pump(hold)
|
||||||
|
|
||||||
|
sess.set_key(23) # stop
|
||||||
|
sess.pump(0.3)
|
||||||
|
|
||||||
|
chunk = sess.lines[mark:]
|
||||||
|
for line in chunk:
|
||||||
|
m = CH_SEEN_RE.search(line)
|
||||||
|
if m:
|
||||||
|
ch, role = int(m.group(1)), m.group(2)
|
||||||
|
res.channels[ch] += 1
|
||||||
|
res.roles[role] += 1
|
||||||
|
res.samples.append(line.strip())
|
||||||
|
ev = parse_pitch_line(line)
|
||||||
|
if not ev or not ev.is_on:
|
||||||
|
continue
|
||||||
|
res.channels[ev.ch] += 1
|
||||||
|
res.roles[ev.role] += 1
|
||||||
|
if ev.role == "bass" or ev.ch == 8:
|
||||||
|
res.bass_n += 1
|
||||||
|
elif ev.role == "drum" or ev.ch == 9:
|
||||||
|
res.drum_n += 1
|
||||||
|
else:
|
||||||
|
res.chord_n += 1
|
||||||
|
|
||||||
|
detail, ok = expect_for_event(ev)
|
||||||
|
tag = f"ch{ev.ch}({ev.role}) {ev.key_in}->{ev.key_out} chord={ev.chord} | {detail}"
|
||||||
|
if ev.chord <= 6:
|
||||||
|
if ok:
|
||||||
|
res.pass_ok += 1
|
||||||
|
else:
|
||||||
|
res.pass_fail += 1
|
||||||
|
res.samples.append("FAIL " + tag)
|
||||||
|
elif ev.xf:
|
||||||
|
if ok:
|
||||||
|
res.xf_ok += 1
|
||||||
|
else:
|
||||||
|
res.xf_fail += 1
|
||||||
|
res.samples.append("FAIL " + tag)
|
||||||
|
elif ev.role == "bass" or ev.ch == 8:
|
||||||
|
if ok:
|
||||||
|
res.bass_ok += 1
|
||||||
|
else:
|
||||||
|
res.bass_fail += 1
|
||||||
|
res.samples.append("FAIL " + tag)
|
||||||
|
else:
|
||||||
|
if ok:
|
||||||
|
res.iii_ok += 1
|
||||||
|
else:
|
||||||
|
res.iii_fail += 1
|
||||||
|
res.samples.append("FAIL " + tag)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--hold", type=float, default=2.2)
|
||||||
|
ap.add_argument("--bin1", default="0,1,2,3,4,5,8,12,16,20,24,28", help="1.bin indices")
|
||||||
|
ap.add_argument("--bin3", default="0,1,2", help="3.bin indices")
|
||||||
|
ap.add_argument("--out", default="")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
bin1_idxs = [int(x) for x in args.bin1.split(",") if x.strip() != ""]
|
||||||
|
bin3_idxs = [int(x) for x in args.bin3.split(",") if x.strip() != ""]
|
||||||
|
|
||||||
|
import_deps()
|
||||||
|
jlink = connect_jlink("AT32F403AC")
|
||||||
|
results: list[PresetResult] = []
|
||||||
|
try:
|
||||||
|
cb = find_rtt_control_block(jlink)
|
||||||
|
if cb is None:
|
||||||
|
raise SystemExit("RTT CB not found")
|
||||||
|
print(f"RTT CB @ 0x{cb:08X}", flush=True)
|
||||||
|
jlink.rtt_start(cb)
|
||||||
|
wait_rtt_ready(jlink)
|
||||||
|
sess = RttSession(jlink)
|
||||||
|
sess.pump(0.3)
|
||||||
|
sess.cmd("log clear", 0.3)
|
||||||
|
|
||||||
|
print("\n######## BIN1 / 1.bin rhythms ########", flush=True)
|
||||||
|
for idx in bin1_idxs:
|
||||||
|
print(f"\n--- BIN1 idx={idx} ---", flush=True)
|
||||||
|
r = run_preset(sess, "1", idx, args.hold)
|
||||||
|
results.append(r)
|
||||||
|
print(
|
||||||
|
f"name={r.name} ch={dict(r.channels)} roles={dict(r.roles)} "
|
||||||
|
f"bass={r.bass_n} drum={r.drum_n} chord={r.chord_n} "
|
||||||
|
f"I/II {r.pass_ok}/{r.pass_fail} III {r.iii_ok}/{r.iii_fail} "
|
||||||
|
f"xf {r.xf_ok}/{r.xf_fail} bassMap {r.bass_ok}/{r.bass_fail}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n######## BIN3 / 3.bin universal ########", flush=True)
|
||||||
|
for idx in bin3_idxs:
|
||||||
|
print(f"\n--- BIN3 idx={idx} ---", flush=True)
|
||||||
|
r = run_preset(sess, "3", idx, args.hold)
|
||||||
|
results.append(r)
|
||||||
|
print(
|
||||||
|
f"name={r.name} ch={dict(r.channels)} roles={dict(r.roles)} "
|
||||||
|
f"bass={r.bass_n} drum={r.drum_n} chord={r.chord_n} "
|
||||||
|
f"I/II {r.pass_ok}/{r.pass_fail} III {r.iii_ok}/{r.iii_fail} "
|
||||||
|
f"xf {r.xf_ok}/{r.xf_fail} bassMap {r.bass_ok}/{r.bass_fail}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Summary vs plan
|
||||||
|
print("\n======== PLAN CHECK (bass和弦分通道八度) ========", flush=True)
|
||||||
|
with_bass = [r for r in results if r.bass_n > 0]
|
||||||
|
print(f"Presets tested: {len(results)}; with ch8(bass) NoteOn: {len(with_bass)}", flush=True)
|
||||||
|
if with_bass:
|
||||||
|
for r in with_bass:
|
||||||
|
print(
|
||||||
|
f" BASS HIT {r.bin_id}.bin[{r.idx}] {r.name}: "
|
||||||
|
f"bass_n={r.bass_n} map_ok={r.bass_ok} fail={r.bass_fail}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(" NO preset produced ch8(bass). Plan bass rule NOT verified on-device.", flush=True)
|
||||||
|
|
||||||
|
# Aggregate chord rules
|
||||||
|
pass_ok = sum(r.pass_ok for r in results)
|
||||||
|
pass_fail = sum(r.pass_fail for r in results)
|
||||||
|
iii_ok = sum(r.iii_ok for r in results)
|
||||||
|
iii_fail = sum(r.iii_fail for r in results)
|
||||||
|
xf_ok = sum(r.xf_ok for r in results)
|
||||||
|
xf_fail = sum(r.xf_fail for r in results)
|
||||||
|
bass_ok = sum(r.bass_ok for r in results)
|
||||||
|
bass_fail = sum(r.bass_fail for r in results)
|
||||||
|
|
||||||
|
def gate(name, ok, fail, need=True):
|
||||||
|
status = "PASS" if fail == 0 and (ok > 0 or not need) else ("FAIL" if fail else "SKIP")
|
||||||
|
print(f" [{status}] {name}: ok={ok} fail={fail}", flush=True)
|
||||||
|
return status != "FAIL"
|
||||||
|
|
||||||
|
all_ok = True
|
||||||
|
all_ok &= gate("I/II PASS (keys1-6)", pass_ok, pass_fail)
|
||||||
|
all_ok &= gate("III+ chord map", iii_ok, iii_fail)
|
||||||
|
all_ok &= gate("xpose>=#F extra-12", xf_ok, xf_fail)
|
||||||
|
all_ok &= gate("bass ch8 map", bass_ok, bass_fail, need=False)
|
||||||
|
if not with_bass:
|
||||||
|
all_ok = False
|
||||||
|
print(" [FAIL] plan requires bass on code ch8 — never seen across 1.bin/3.bin samples", flush=True)
|
||||||
|
|
||||||
|
# channel histogram
|
||||||
|
ch_all: Counter = Counter()
|
||||||
|
for r in results:
|
||||||
|
ch_all.update(r.channels)
|
||||||
|
print(f"Channel histogram: {dict(sorted(ch_all.items()))}", flush=True)
|
||||||
|
|
||||||
|
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
out = args.out or os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||||
|
f"bin13_reg_{stamp}.log",
|
||||||
|
)
|
||||||
|
with open(out, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(f"# bin1/bin3 plan check {datetime.now().isoformat(timespec='seconds')}\n")
|
||||||
|
for line in sess.lines:
|
||||||
|
f.write(line + "\n")
|
||||||
|
f.write("\n# SUMMARY\n")
|
||||||
|
for r in results:
|
||||||
|
f.write(
|
||||||
|
f"# {r.bin_id}[{r.idx}] {r.name} ch={dict(r.channels)} "
|
||||||
|
f"bass={r.bass_n} I={r.pass_ok}/{r.pass_fail} "
|
||||||
|
f"III={r.iii_ok}/{r.iii_fail} xf={r.xf_ok}/{r.xf_fail} "
|
||||||
|
f"bassMap={r.bass_ok}/{r.bass_fail}\n"
|
||||||
|
)
|
||||||
|
print(f"Log: {out}", flush=True)
|
||||||
|
return 0 if all_ok else 1
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
jlink.rtt_stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Drive K1 via RTT while recording laptop mic; compare I/II vs III+ pitch."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Reuse helpers from sibling tools
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from mic_pitch_analyze import ( # noqa: E402
|
||||||
|
analyze_audio,
|
||||||
|
compare_summaries,
|
||||||
|
print_summary,
|
||||||
|
record,
|
||||||
|
save_wav,
|
||||||
|
summarize,
|
||||||
|
write_frame_log,
|
||||||
|
)
|
||||||
|
from rtt_pitch_reg_test import ( # noqa: E402
|
||||||
|
RttSession,
|
||||||
|
connect_jlink,
|
||||||
|
find_rtt_control_block,
|
||||||
|
import_deps,
|
||||||
|
wait_rtt_ready,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
out_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
hold = 8.0
|
||||||
|
sr = 16000
|
||||||
|
device = None # default mic
|
||||||
|
|
||||||
|
import_deps()
|
||||||
|
jlink = connect_jlink("AT32F403AC")
|
||||||
|
try:
|
||||||
|
cb = find_rtt_control_block(jlink)
|
||||||
|
if cb is None:
|
||||||
|
raise SystemExit("RTT CB not found")
|
||||||
|
print(f"RTT CB @ 0x{cb:08X}", flush=True)
|
||||||
|
jlink.rtt_start(cb)
|
||||||
|
wait_rtt_ready(jlink)
|
||||||
|
sess = RttSession(jlink)
|
||||||
|
sess.pump(0.3)
|
||||||
|
|
||||||
|
sess.cmd("log clear", 0.2)
|
||||||
|
sess.cmd("tone local", 1.0)
|
||||||
|
sess.set_xpose(2)
|
||||||
|
sess.cmd("tone pick", 0.8)
|
||||||
|
|
||||||
|
# ---- Take A: I/II ----
|
||||||
|
print("\n=== TAKE A: I/II (key2) — put guitar near mic ===", flush=True)
|
||||||
|
sess.set_key(0)
|
||||||
|
sess.set_key(2)
|
||||||
|
sess.pump(0.8)
|
||||||
|
print("MIC record A starting...", flush=True)
|
||||||
|
audio_a = record(hold, sr, device)
|
||||||
|
wav_a = os.path.join(out_dir, f"mic_A_I_{stamp}.wav")
|
||||||
|
save_wav(wav_a, audio_a, sr)
|
||||||
|
frames_a = analyze_audio(audio_a, sr, fmin=40, fmax=500)
|
||||||
|
sum_a = summarize(frames_a, label="A:I/II")
|
||||||
|
print_summary(sum_a)
|
||||||
|
write_frame_log(os.path.join(out_dir, f"mic_A_I_{stamp}.csv"), frames_a, sum_a)
|
||||||
|
|
||||||
|
# ---- Take B: III ----
|
||||||
|
print("\n=== TAKE B: III (key8) ===", flush=True)
|
||||||
|
sess.set_key(0)
|
||||||
|
sess.set_key(8)
|
||||||
|
sess.pump(0.8)
|
||||||
|
print("MIC record B starting...", flush=True)
|
||||||
|
audio_b = record(hold, sr, device)
|
||||||
|
wav_b = os.path.join(out_dir, f"mic_B_III_{stamp}.wav")
|
||||||
|
save_wav(wav_b, audio_b, sr)
|
||||||
|
frames_b = analyze_audio(audio_b, sr, fmin=40, fmax=500)
|
||||||
|
sum_b = summarize(frames_b, label="B:III")
|
||||||
|
print_summary(sum_b)
|
||||||
|
write_frame_log(os.path.join(out_dir, f"mic_B_III_{stamp}.csv"), frames_b, sum_b)
|
||||||
|
|
||||||
|
# ---- Take C: VII + xpose #F (extra -12) ----
|
||||||
|
print("\n=== TAKE C: VII key20 + xpose=#F ===", flush=True)
|
||||||
|
sess.set_xpose(6)
|
||||||
|
sess.set_key(0)
|
||||||
|
sess.set_key(20)
|
||||||
|
sess.pump(0.8)
|
||||||
|
print("MIC record C starting...", flush=True)
|
||||||
|
audio_c = record(hold, sr, device)
|
||||||
|
wav_c = os.path.join(out_dir, f"mic_C_VII_xf_{stamp}.wav")
|
||||||
|
save_wav(wav_c, audio_c, sr)
|
||||||
|
frames_c = analyze_audio(audio_c, sr, fmin=40, fmax=500)
|
||||||
|
sum_c = summarize(frames_c, label="C:VII+#F")
|
||||||
|
print_summary(sum_c)
|
||||||
|
write_frame_log(os.path.join(out_dir, f"mic_C_VII_xf_{stamp}.csv"), frames_c, sum_c)
|
||||||
|
|
||||||
|
# Also bass-biased analysis (prefer low F0)
|
||||||
|
print("\n=== Bass-biased (fmax=180Hz) ===", flush=True)
|
||||||
|
for label, audio in (("A", audio_a), ("B", audio_b), ("C", audio_c)):
|
||||||
|
fr = analyze_audio(audio, sr, fmin=40, fmax=180)
|
||||||
|
print_summary(summarize(fr, label=f"{label}-bassband"))
|
||||||
|
|
||||||
|
print("\n=== A vs B (I/II vs III) ===", flush=True)
|
||||||
|
compare_summaries(sum_a, sum_b)
|
||||||
|
print("\n=== A vs C (I/II vs VII+#F) ===", flush=True)
|
||||||
|
compare_summaries(sum_a, sum_c)
|
||||||
|
|
||||||
|
print("\n=== stop ===", flush=True)
|
||||||
|
sess.stop_band()
|
||||||
|
|
||||||
|
# Save RTT lines
|
||||||
|
rtt_path = os.path.join(out_dir, f"mic_rtt_{stamp}.log")
|
||||||
|
with open(rtt_path, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(f"# rtt+mic test {datetime.now().isoformat(timespec='seconds')}\n")
|
||||||
|
for line in sess.lines:
|
||||||
|
f.write(line + "\n")
|
||||||
|
print(f"RTT log: {rtt_path}", flush=True)
|
||||||
|
print(f"WAV A/B/C: {wav_a}\n {wav_b}\n {wav_c}", flush=True)
|
||||||
|
return 0
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
jlink.rtt_stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -0,0 +1,589 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""K1 bass/chord register auto-test via J-Link RTT.
|
||||||
|
|
||||||
|
Requires firmware with:
|
||||||
|
- [PITCH]/[REG] logs (App_Auto.c)
|
||||||
|
- RTT cmds: chord key N / chord xpose N / tone pick / log clear
|
||||||
|
|
||||||
|
Modes:
|
||||||
|
python tools/rtt_pitch_reg_test.py # inject + live assert
|
||||||
|
python tools/rtt_pitch_reg_test.py --listen # you press keys; we assert
|
||||||
|
python tools/rtt_pitch_reg_test.py --analyze oct_reg3.log
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
DEFAULT_DEVICES = ("Cortex-M4", "AT32F403AC", "AT32F403A")
|
||||||
|
|
||||||
|
MIDI_E1 = 28
|
||||||
|
MIDI_E2 = 40
|
||||||
|
MIDI_GSHARP4 = 68
|
||||||
|
XPOSE_FS = 6
|
||||||
|
BASS_CH_DISP = 8 # 代码/日志通道;BASS_CH=8
|
||||||
|
DRUM_CH_DISP = 9
|
||||||
|
|
||||||
|
PITCH_RE = re.compile(
|
||||||
|
r"\[PITCH\]\s+(on|off)\s+ch(\d+)\((\w+)\)\s+"
|
||||||
|
r"(\d+)->(\d+)\s+d=(-?\d+)\s+deg=(\d+)\s+chord=(\d+)\s+"
|
||||||
|
r"xp=(\d+)\s+xf=(-?\d+)\s+fl=0x([0-9A-Fa-f]+)\s*(.*)$"
|
||||||
|
)
|
||||||
|
REG_RE = re.compile(
|
||||||
|
r"\[REG\s*\]\s+fp\s+(\d+)->(\d+)\s+chord=(\d+)\s+deg=(\d+)\s+xp=(\d+)\s+xf=(-?\d+)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PitchEvent:
|
||||||
|
is_on: bool
|
||||||
|
ch: int
|
||||||
|
role: str
|
||||||
|
key_in: int
|
||||||
|
key_out: int
|
||||||
|
delta: int
|
||||||
|
deg: int
|
||||||
|
chord: int
|
||||||
|
xp: int
|
||||||
|
xf: int
|
||||||
|
flags: int
|
||||||
|
tags: str
|
||||||
|
raw: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CheckResult:
|
||||||
|
ok: int = 0
|
||||||
|
fail: int = 0
|
||||||
|
skip: int = 0
|
||||||
|
messages: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
def add_ok(self, msg: str) -> None:
|
||||||
|
self.ok += 1
|
||||||
|
self.messages.append(f"OK {msg}")
|
||||||
|
|
||||||
|
def add_fail(self, msg: str) -> None:
|
||||||
|
self.fail += 1
|
||||||
|
self.messages.append(f"FAIL {msg}")
|
||||||
|
|
||||||
|
def add_skip(self, msg: str) -> None:
|
||||||
|
self.skip += 1
|
||||||
|
self.messages.append(f"SKIP {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def import_deps() -> None:
|
||||||
|
try:
|
||||||
|
import pylink # noqa: F401
|
||||||
|
except ImportError as exc:
|
||||||
|
raise SystemExit("Missing dependency: pip install pylink-square") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def connect_jlink(device: str):
|
||||||
|
import pylink
|
||||||
|
|
||||||
|
jlink = pylink.JLink()
|
||||||
|
jlink.open()
|
||||||
|
try:
|
||||||
|
jlink.exec_command("HideDeviceSelection = 1")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.set_tif(pylink.enums.JLinkInterfaces.SWD)
|
||||||
|
|
||||||
|
last_error = None
|
||||||
|
for candidate in (device, *DEFAULT_DEVICES):
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
jlink.exec_command(f"Device = {candidate}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.connect(candidate)
|
||||||
|
print(f"Connected as {candidate}", flush=True)
|
||||||
|
return jlink
|
||||||
|
except pylink.errors.JLinkException as exc:
|
||||||
|
last_error = exc
|
||||||
|
jlink.close()
|
||||||
|
raise SystemExit(f"Failed to connect J-Link: {last_error}")
|
||||||
|
|
||||||
|
|
||||||
|
def find_rtt_control_block(jlink, ram_base: int = 0x20000000, ram_size: int = 0x18000) -> int | None:
|
||||||
|
needle = b"SEGGER RTT"
|
||||||
|
chunk = 0x1000
|
||||||
|
was_halted = jlink.halted()
|
||||||
|
if not was_halted:
|
||||||
|
jlink.halt()
|
||||||
|
try:
|
||||||
|
for off in range(0, ram_size, chunk):
|
||||||
|
data = bytes(jlink.memory_read8(ram_base + off, min(chunk, ram_size - off)))
|
||||||
|
idx = data.find(needle)
|
||||||
|
if idx >= 0:
|
||||||
|
return ram_base + off + idx
|
||||||
|
finally:
|
||||||
|
if not was_halted:
|
||||||
|
if hasattr(jlink, "restart"):
|
||||||
|
jlink.restart()
|
||||||
|
else:
|
||||||
|
jlink.go()
|
||||||
|
time.sleep(0.1)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def wait_rtt_ready(jlink, timeout_s: float = 15.0) -> None:
|
||||||
|
deadline = time.time() + timeout_s
|
||||||
|
while time.time() < deadline:
|
||||||
|
try:
|
||||||
|
if jlink.rtt_get_num_up_buffers() > 0:
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(0.2)
|
||||||
|
raise SystemExit("RTT control block found but buffers not ready")
|
||||||
|
|
||||||
|
|
||||||
|
def send_command(jlink, payload: bytes, retries: int = 30) -> None:
|
||||||
|
if isinstance(payload, str):
|
||||||
|
payload = payload.encode("ascii")
|
||||||
|
if not payload.endswith(b"\n"):
|
||||||
|
payload += b"\n"
|
||||||
|
for _ in range(retries):
|
||||||
|
wrote = jlink.rtt_write(0, list(payload))
|
||||||
|
if wrote > 0:
|
||||||
|
print(f">> {payload.decode('ascii', errors='replace').strip()}", flush=True)
|
||||||
|
return
|
||||||
|
time.sleep(0.2)
|
||||||
|
raise SystemExit(f"Failed to send RTT cmd: {payload!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def drain_rtt(jlink, seconds: float = 0.15) -> str:
|
||||||
|
buf = b""
|
||||||
|
deadline = time.time() + seconds
|
||||||
|
while time.time() < deadline:
|
||||||
|
chunk = jlink.rtt_read(0, 4096)
|
||||||
|
if chunk:
|
||||||
|
buf += bytes(chunk)
|
||||||
|
else:
|
||||||
|
time.sleep(0.01)
|
||||||
|
return buf.decode("utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_pitch_line(line: str) -> PitchEvent | None:
|
||||||
|
m = PITCH_RE.search(line)
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
return PitchEvent(
|
||||||
|
is_on=m.group(1) == "on",
|
||||||
|
ch=int(m.group(2)),
|
||||||
|
role=m.group(3),
|
||||||
|
key_in=int(m.group(4)),
|
||||||
|
key_out=int(m.group(5)),
|
||||||
|
delta=int(m.group(6)),
|
||||||
|
deg=int(m.group(7)),
|
||||||
|
chord=int(m.group(8)),
|
||||||
|
xp=int(m.group(9)),
|
||||||
|
xf=int(m.group(10)),
|
||||||
|
flags=int(m.group(11), 16),
|
||||||
|
tags=m.group(12).strip(),
|
||||||
|
raw=line.strip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def chord_degree(chord: int) -> int:
|
||||||
|
if chord < 1 or chord > 21:
|
||||||
|
return 0
|
||||||
|
return (chord - 1) // 3 + 1
|
||||||
|
|
||||||
|
|
||||||
|
def need_register_fix(chord: int) -> bool:
|
||||||
|
return 7 <= chord <= 21
|
||||||
|
|
||||||
|
|
||||||
|
def map_bass(key: int, xp: int) -> tuple[int, int]:
|
||||||
|
"""Return (out, flags) mirroring App_Auto_MapBassNote."""
|
||||||
|
out = key - 12
|
||||||
|
flags = 0x01
|
||||||
|
while out < MIDI_E1:
|
||||||
|
out += 12
|
||||||
|
flags |= 0x02
|
||||||
|
if xp >= XPOSE_FS:
|
||||||
|
out -= 12
|
||||||
|
flags |= 0x04
|
||||||
|
while out < MIDI_E1:
|
||||||
|
out += 12
|
||||||
|
flags |= 0x02
|
||||||
|
return max(0, min(127, out)), flags
|
||||||
|
|
||||||
|
|
||||||
|
def map_chord(key: int, xp: int) -> tuple[int, int, bool]:
|
||||||
|
"""Return (out_or_clamp_hint, flags, need_near).
|
||||||
|
|
||||||
|
When need_near is True, exact out is unknown without chord table;
|
||||||
|
caller should only require out in [E2, #G4].
|
||||||
|
"""
|
||||||
|
out = key - 12
|
||||||
|
flags = 0x01
|
||||||
|
while out < MIDI_E2:
|
||||||
|
out += 12
|
||||||
|
flags |= 0x08
|
||||||
|
need_near = out > MIDI_GSHARP4
|
||||||
|
if need_near:
|
||||||
|
flags |= 0x10
|
||||||
|
out = MIDI_GSHARP4 # placeholder; exact nearest checked soft
|
||||||
|
if xp >= XPOSE_FS:
|
||||||
|
out -= 12
|
||||||
|
flags |= 0x04
|
||||||
|
while out < MIDI_E2:
|
||||||
|
out += 12
|
||||||
|
flags |= 0x08
|
||||||
|
if out > MIDI_GSHARP4:
|
||||||
|
flags |= 0x10
|
||||||
|
need_near = True
|
||||||
|
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:
|
||||||
|
return "note-off ignored", True
|
||||||
|
|
||||||
|
if ev.delta != ev.key_out - ev.key_in:
|
||||||
|
return f"d mismatch {ev.delta} != {ev.key_out - ev.key_in}", False
|
||||||
|
|
||||||
|
deg_exp = chord_degree(ev.chord)
|
||||||
|
if deg_exp and ev.deg != deg_exp:
|
||||||
|
return f"deg {ev.deg} != expected {deg_exp}", False
|
||||||
|
|
||||||
|
xf_exp = 1 if ev.xp >= XPOSE_FS else 0
|
||||||
|
if ev.xf != xf_exp:
|
||||||
|
return f"xf {ev.xf} != expected {xf_exp} (xp={ev.xp})", False
|
||||||
|
|
||||||
|
if ev.ch == DRUM_CH_DISP or ev.role == "drum":
|
||||||
|
return "drum should not appear in PITCH", False
|
||||||
|
|
||||||
|
fix = need_register_fix(ev.chord)
|
||||||
|
if not fix:
|
||||||
|
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:
|
||||||
|
# flags may be 0 on pass path
|
||||||
|
pass
|
||||||
|
return f"PASS I/II {ev.key_in}", True
|
||||||
|
|
||||||
|
if ev.ch == BASS_CH_DISP or ev.role == "bass":
|
||||||
|
exp, exp_fl = map_bass(ev.key_in, ev.xp)
|
||||||
|
if ev.key_out != exp:
|
||||||
|
return f"bass expect {ev.key_in}->{exp}, got {ev.key_out}", False
|
||||||
|
if ev.key_out < MIDI_E1:
|
||||||
|
return f"bass below E1: {ev.key_out}", False
|
||||||
|
# flag bits that must be present
|
||||||
|
if (exp_fl & 0x01) and not (ev.flags & 0x01):
|
||||||
|
return f"bass missing -12 flag fl=0x{ev.flags:02X}", False
|
||||||
|
return f"bass {ev.key_in}->{ev.key_out}", True
|
||||||
|
|
||||||
|
# chord path
|
||||||
|
exp, exp_fl, need_near = map_chord(ev.key_in, ev.xp)
|
||||||
|
if not (MIDI_E2 <= ev.key_out <= MIDI_GSHARP4):
|
||||||
|
return f"chord out {ev.key_out} not in [{MIDI_E2},{MIDI_GSHARP4}]", False
|
||||||
|
if need_near:
|
||||||
|
if not (ev.flags & 0x10) and "NEAR" not in ev.tags:
|
||||||
|
# soft: range ok is enough if nearest not flagged but clamped somehow
|
||||||
|
return f"chord NEAR expected (in={ev.key_in} out={ev.key_out})", True
|
||||||
|
return f"chord NEAR {ev.key_in}->{ev.key_out}", True
|
||||||
|
if ev.key_out != exp:
|
||||||
|
return f"chord expect {ev.key_in}->{exp}, got {ev.key_out}", False
|
||||||
|
return f"chord {ev.key_in}->{ev.key_out}", True
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_lines(lines: Iterable[str], result: CheckResult | None = None) -> CheckResult:
|
||||||
|
result = result or CheckResult()
|
||||||
|
pitch_n = 0
|
||||||
|
roles: set[str] = set()
|
||||||
|
chords: set[int] = set()
|
||||||
|
for line in lines:
|
||||||
|
line = line.rstrip("\n")
|
||||||
|
if "[REG" in line and "fp " in line:
|
||||||
|
m = REG_RE.search(line)
|
||||||
|
if m:
|
||||||
|
chord = int(m.group(3))
|
||||||
|
deg = int(m.group(4))
|
||||||
|
xp = int(m.group(5))
|
||||||
|
xf = int(m.group(6))
|
||||||
|
deg_exp = chord_degree(chord)
|
||||||
|
xf_exp = 1 if xp >= XPOSE_FS else 0
|
||||||
|
if deg_exp and deg != deg_exp:
|
||||||
|
result.add_fail(f"REG deg {deg}!={deg_exp} chord={chord}")
|
||||||
|
elif xf != xf_exp:
|
||||||
|
result.add_fail(f"REG xf {xf}!={xf_exp} xp={xp}")
|
||||||
|
else:
|
||||||
|
result.add_ok(f"REG fp chord={chord} deg={deg} xp={xp} xf={xf}")
|
||||||
|
|
||||||
|
ev = parse_pitch_line(line)
|
||||||
|
if not ev:
|
||||||
|
continue
|
||||||
|
pitch_n += 1
|
||||||
|
roles.add(ev.role)
|
||||||
|
chords.add(ev.chord)
|
||||||
|
detail, ok = expect_for_event(ev)
|
||||||
|
msg = f"ch{ev.ch}({ev.role}) chord={ev.chord} {ev.key_in}->{ev.key_out} | {detail}"
|
||||||
|
if ok:
|
||||||
|
result.add_ok(msg)
|
||||||
|
else:
|
||||||
|
result.add_fail(msg)
|
||||||
|
|
||||||
|
if pitch_n == 0:
|
||||||
|
result.add_skip("no [PITCH] lines found")
|
||||||
|
else:
|
||||||
|
if "bass" not in roles and not any(
|
||||||
|
f"ch{BASS_CH_DISP}(" in m for m in result.messages
|
||||||
|
):
|
||||||
|
result.add_skip(f"no bass PITCH on ch{BASS_CH_DISP} (style may omit bass, or old FW)")
|
||||||
|
if not any(need_register_fix(c) for c in chords):
|
||||||
|
result.add_skip("no III+ chord keys observed")
|
||||||
|
if not any(not need_register_fix(c) and 1 <= c <= 6 for c in chords):
|
||||||
|
result.add_skip("no I/II chord keys observed")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def print_report(result: CheckResult, out_path: str | None = None) -> int:
|
||||||
|
print("\n======== PITCH REG TEST REPORT ========", flush=True)
|
||||||
|
for msg in result.messages:
|
||||||
|
# only print fails + summary skips loudly; OK compacted
|
||||||
|
if msg.startswith("FAIL") or msg.startswith("SKIP"):
|
||||||
|
print(msg, flush=True)
|
||||||
|
ok_short = [m for m in result.messages if m.startswith("OK")]
|
||||||
|
print(f"OK={result.ok} FAIL={result.fail} SKIP={result.skip}", flush=True)
|
||||||
|
if ok_short:
|
||||||
|
print(f"(first OK samples: {len(ok_short)} total)", flush=True)
|
||||||
|
for m in ok_short[:8]:
|
||||||
|
print(m, flush=True)
|
||||||
|
if len(ok_short) > 8:
|
||||||
|
print(f"... +{len(ok_short) - 8} more OK", flush=True)
|
||||||
|
if out_path:
|
||||||
|
with open(out_path, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(f"# pitch reg report {datetime.now().isoformat(timespec='seconds')}\n")
|
||||||
|
f.write(f"OK={result.ok} FAIL={result.fail} SKIP={result.skip}\n")
|
||||||
|
for m in result.messages:
|
||||||
|
f.write(m + "\n")
|
||||||
|
print(f"Report saved: {os.path.abspath(out_path)}", flush=True)
|
||||||
|
return 0 if result.fail == 0 and result.ok > 0 else 1
|
||||||
|
|
||||||
|
|
||||||
|
class RttSession:
|
||||||
|
def __init__(self, jlink):
|
||||||
|
self.jlink = jlink
|
||||||
|
self.buf = b""
|
||||||
|
self.lines: list[str] = []
|
||||||
|
|
||||||
|
def cmd(self, text: str, settle: float = 0.2) -> None:
|
||||||
|
send_command(self.jlink, text.encode("ascii"))
|
||||||
|
self.pump(settle)
|
||||||
|
|
||||||
|
def pump(self, seconds: float) -> list[str]:
|
||||||
|
new_lines: list[str] = []
|
||||||
|
deadline = time.time() + seconds
|
||||||
|
while time.time() < deadline:
|
||||||
|
chunk = self.jlink.rtt_read(0, 4096)
|
||||||
|
if chunk:
|
||||||
|
self.buf += bytes(chunk)
|
||||||
|
while b"\n" in self.buf:
|
||||||
|
raw, self.buf = self.buf.split(b"\n", 1)
|
||||||
|
line = raw.decode("utf-8", errors="replace").rstrip("\r")
|
||||||
|
if line:
|
||||||
|
self.lines.append(line)
|
||||||
|
new_lines.append(line)
|
||||||
|
if (
|
||||||
|
"[PITCH]" in line
|
||||||
|
or "[REG" in line
|
||||||
|
or "[KEY" in line
|
||||||
|
or "CHORD_" in line
|
||||||
|
or "TONE_" in line
|
||||||
|
):
|
||||||
|
print(line, flush=True)
|
||||||
|
else:
|
||||||
|
time.sleep(0.01)
|
||||||
|
return new_lines
|
||||||
|
|
||||||
|
def cmd_ack(self, text: str, ack: str, timeout_s: float = 3.0, retries: int = 6) -> bool:
|
||||||
|
"""Send command until ack substring appears (handles RTT down loss under load)."""
|
||||||
|
for attempt in range(1, retries + 1):
|
||||||
|
# brief quiet drain then send
|
||||||
|
self.pump(0.15)
|
||||||
|
send_command(self.jlink, text.encode("ascii"))
|
||||||
|
deadline = time.time() + timeout_s
|
||||||
|
while time.time() < deadline:
|
||||||
|
for line in self.pump(0.1):
|
||||||
|
if ack in line:
|
||||||
|
return True
|
||||||
|
print(f"!! no ack '{ack}' for '{text}' (try {attempt}/{retries})", flush=True)
|
||||||
|
time.sleep(0.25)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def stop_band(self) -> None:
|
||||||
|
self.cmd_ack("chord key 23", "CHORD_KEY_OK", timeout_s=2.0, retries=4)
|
||||||
|
self.pump(0.4)
|
||||||
|
|
||||||
|
def set_xpose(self, xp: int) -> bool:
|
||||||
|
return self.cmd_ack(f"chord xpose {xp}", "CHORD_XPOSE_OK", timeout_s=2.0, retries=5)
|
||||||
|
|
||||||
|
def set_key(self, key: int) -> bool:
|
||||||
|
return self.cmd_ack(f"chord key {key}", "CHORD_KEY_OK", timeout_s=2.0, retries=5)
|
||||||
|
|
||||||
|
def phase_switch(self, name: str, key: int, xpose: int | None, hold_s: float) -> None:
|
||||||
|
"""Switch chord/xpose while accompaniment is already running.
|
||||||
|
Note: plain `tone pick` clears KEY_ID to 1 — do not call it after inject.
|
||||||
|
"""
|
||||||
|
print(f"\n=== {name}: key={key} xpose={xpose} ===", flush=True)
|
||||||
|
if xpose is not None:
|
||||||
|
if not self.set_xpose(xpose):
|
||||||
|
print(f"FAIL setup xpose={xpose}", flush=True)
|
||||||
|
self.set_key(0)
|
||||||
|
if not self.set_key(key):
|
||||||
|
print(f"FAIL setup key={key}", flush=True)
|
||||||
|
self.pump(hold_s)
|
||||||
|
|
||||||
|
|
||||||
|
def run_auto(device: str, hold_s: float, report: str | None, log_path: str | None) -> int:
|
||||||
|
import_deps()
|
||||||
|
jlink = connect_jlink(device)
|
||||||
|
try:
|
||||||
|
cb = find_rtt_control_block(jlink)
|
||||||
|
if cb is None:
|
||||||
|
raise SystemExit("SEGGER RTT CB not found")
|
||||||
|
print(f"RTT CB @ 0x{cb:08X}", flush=True)
|
||||||
|
jlink.rtt_start(cb)
|
||||||
|
wait_rtt_ready(jlink)
|
||||||
|
sess = RttSession(jlink)
|
||||||
|
sess.pump(0.3)
|
||||||
|
|
||||||
|
sess.cmd("log clear", 0.3)
|
||||||
|
sess.cmd("tone local", 1.0)
|
||||||
|
# Start band first (pick forces chord=1), then inject keys while playing
|
||||||
|
if not sess.set_xpose(2):
|
||||||
|
print("FAIL initial xpose=2", flush=True)
|
||||||
|
sess.cmd("tone pick", settle=0.8)
|
||||||
|
print("\n=== I (default after pick) ===", flush=True)
|
||||||
|
sess.pump(hold_s)
|
||||||
|
|
||||||
|
sess.phase_switch("I/II key2 PASS", key=2, 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 xpose=#F", key=20, xpose=6, hold_s=hold_s)
|
||||||
|
|
||||||
|
print("\n=== stop ===", flush=True)
|
||||||
|
sess.stop_band()
|
||||||
|
sess.pump(0.4)
|
||||||
|
|
||||||
|
if log_path:
|
||||||
|
with open(log_path, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(f"# auto pitch test {datetime.now().isoformat(timespec='seconds')}\n")
|
||||||
|
for line in sess.lines:
|
||||||
|
f.write(line + "\n")
|
||||||
|
print(f"Log saved: {os.path.abspath(log_path)}", flush=True)
|
||||||
|
|
||||||
|
joined = "\n".join(sess.lines)
|
||||||
|
if "CHORD_KEY_OK" not in joined and "CHORD_KEY_BAD" not in joined:
|
||||||
|
print(
|
||||||
|
"WARNING: no CHORD_KEY_OK — firmware may lack 'chord key' RTT cmd. "
|
||||||
|
"Rebuild/flash, or use --listen / --analyze.",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = analyze_lines(sess.lines)
|
||||||
|
has_pass = any("PASS I/II" in m for m in result.messages)
|
||||||
|
has_iii_raw = any(
|
||||||
|
"[PITCH]" in line and ("chord=8" in line or "chord=20" in line or "deg=3" in line or "deg=7" in line)
|
||||||
|
for line in sess.lines
|
||||||
|
)
|
||||||
|
has_iii_ok = any(
|
||||||
|
m.startswith("OK") and "PASS" not in m and "(chord)" in m
|
||||||
|
for m in result.messages
|
||||||
|
)
|
||||||
|
has_xf = any("[PITCH]" in line and "xf=1" in line and "deg=7" in line for line in sess.lines)
|
||||||
|
if not has_pass:
|
||||||
|
result.add_fail("coverage: no I/II PASS samples")
|
||||||
|
if not has_iii_raw:
|
||||||
|
result.add_fail("coverage: no III+/VII chord in PITCH")
|
||||||
|
elif not has_iii_ok:
|
||||||
|
result.add_fail("coverage: III+ present but mapping asserts failed")
|
||||||
|
if not has_xf:
|
||||||
|
result.add_skip("coverage: no VII+xf=1 samples")
|
||||||
|
|
||||||
|
return print_report(result, report)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
jlink.rtt_stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.close()
|
||||||
|
|
||||||
|
|
||||||
|
def run_listen(device: str, seconds: float, report: str | None, log_path: str | None) -> int:
|
||||||
|
import_deps()
|
||||||
|
jlink = connect_jlink(device)
|
||||||
|
try:
|
||||||
|
cb = find_rtt_control_block(jlink)
|
||||||
|
if cb is None:
|
||||||
|
raise SystemExit("SEGGER RTT CB not found")
|
||||||
|
print(f"RTT CB @ 0x{cb:08X}", flush=True)
|
||||||
|
jlink.rtt_start(cb)
|
||||||
|
wait_rtt_ready(jlink)
|
||||||
|
sess = RttSession(jlink)
|
||||||
|
sess.pump(0.2)
|
||||||
|
sess.cmd("log clear", 0.2)
|
||||||
|
print(
|
||||||
|
f"Listening {seconds:.0f}s — press I/II then III+ chords; set transpose>=#F if possible.",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
sess.pump(seconds)
|
||||||
|
if log_path:
|
||||||
|
with open(log_path, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(f"# listen pitch test {datetime.now().isoformat(timespec='seconds')}\n")
|
||||||
|
for line in sess.lines:
|
||||||
|
f.write(line + "\n")
|
||||||
|
print(f"Log saved: {os.path.abspath(log_path)}", flush=True)
|
||||||
|
result = analyze_lines(sess.lines)
|
||||||
|
return print_report(result, report)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
jlink.rtt_stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jlink.close()
|
||||||
|
|
||||||
|
|
||||||
|
def run_analyze(path: str, report: str | None) -> int:
|
||||||
|
with open(path, encoding="utf-8", errors="replace") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
result = analyze_lines(lines)
|
||||||
|
return print_report(result, report)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="K1 pitch/register RTT auto-test")
|
||||||
|
parser.add_argument("--device", default="AT32F403AC")
|
||||||
|
parser.add_argument("--hold", type=float, default=2.5, help="Seconds to hold each injected key")
|
||||||
|
parser.add_argument("--seconds", type=float, default=45.0, help="Listen duration")
|
||||||
|
parser.add_argument("--listen", action="store_true", help="Do not inject; monitor only")
|
||||||
|
parser.add_argument("--analyze", metavar="LOG", help="Offline analyze a dump log")
|
||||||
|
parser.add_argument("--out-log", default="", help="Save captured RTT lines")
|
||||||
|
parser.add_argument("--report", default="", help="Save pass/fail report")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
report = args.report or None
|
||||||
|
log_path = args.out_log or None
|
||||||
|
|
||||||
|
if args.analyze:
|
||||||
|
raise SystemExit(run_analyze(args.analyze, report))
|
||||||
|
if args.listen:
|
||||||
|
raise SystemExit(run_listen(args.device, args.seconds, report, log_path))
|
||||||
|
raise SystemExit(run_auto(args.device, args.hold, report, log_path))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue