Ключ наставника: дырки fw05 (партиционированная свёртка)
Ключ к fw05 (не выдавать до попытки ученика)
Дырка 1 (почему FFT_N = 2·BLOCK). Линейная свёртка блока длины B с партицией длины B даёт 2B−1 отсчётов; FFT-умножение спектров реализует круговую свёртку длиной FFT_N — если FFT_N < 2B−1, хвост «заворачивается» на начало (time-aliasing) и портит звук металлическим призвуком. 256 ≥ 255 — впритык и достаточно. Ожидаемое объяснение своими словами: «дайте свёртке место, куда положить хвост». Типовая неверная попытка: «для запаса по частотному разрешению» — разрешение тут ни при чём.
Дырка 2 (NPART). #define NPART ((IR_TAPS + BLOCK - 1) / BLOCK) = 128 партиций при 16384/128. Округление вверх — иначе последний неполный кусок IR потеряется. Проверочный вопрос: сколько партиций при IR_TAPS=500? (4, последняя дополнена нулями — это уже сделано в pconv_init через n.)
Дырка 3. arm_rfft_fast_f32(&rfft, fft_buf, ir_spectra[p], 0); — прямое преобразование (ifftFlag=0). Частая ошибка: попытка «сохранить» fft_buf — CMSIS rfft портит входной буфер, потому партиция каждый раз собирается заново (это уже учтено в каркасе, обратить внимание ученика).
Дырка 4 (комплексное умножение).
ac[2*k] += ar*br - ai*bi;
ac[2*k+1] += ar*bi + ai*br;Требовать вывод на бумаге из (ar+j·ai)(br+j·bi). Типовая ошибка — знак минус потерян: звук «почти правильный», но АЧХ дикая; отличная демонстрация, почему сверка с эталоном fw04 (финальное задание 1) обязательна.
Дырка 5 (обновление overlap). memcpy(overlap, buf, BLOCK*sizeof(float32_t)); — запомнить текущий вход как «хвост прошлого» для следующего вызова. До FFT — потому что buf ниже будет перезаписан результатом (in-place). Кто скопирует после — получит рассинхрон на блок: звук с «эхом-заиканием», узнаваемым по fw02-заданию 3.
Дырка 6 (индекс кольца). uint32_t idx = (head + p) % NPART; — свежий спектр (p=0) лежит в head, более старые — дальше по кольцу (голова движется назад в п.6, поэтому «+p», а не «−p»). Проверить руками на NPART=4, нарисовав кольцо на бумаге за два шага времени — требовать рисунок.
Дырка 7 (какая половина валидна). memcpy(buf, &fft_buf[BLOCK], BLOCK*sizeof(float32_t)); — вторая половина. Первая содержит круговой «захлёст» свёртки хвоста окна — это и есть суть overlap-save: мы жертвуем половиной окна ради непрерывности. Ожидаемое объяснение через картинку окна [старый|новый].
Критерий зачёта рубежа: побитовая сверка с fw04 (расхождение ≤ 1e-5 RMS), профиль DHO804 для 16384 отводов снят, латентность не выросла. Устно — любые две дырки по выбору наставника с объяснением «почему», не «что».