0. Об имени
Инструмент называется Tymbal.
Характер задаёт учитель — обработанный сигнал, на который натаскивается уточнитель. Смените учителя, и характер сменится целиком, а конструкция останется прежней.
Tymbal — орган звукоизвлечения цикады: ребристая мембрана, которая щёлкает при нелинейном выгибании. Маленький аппарат, громкий и богатый обертонами результат, и весь механизм держится на одной нелинейности. У нас нелинейность учителя переносится сетью на детерминированный скелет, и всё это живёт на одном чипе.
Слово к тому же приходит оттуда же, откуда «тембр». Merriam‑Webster выводит tymbal как изменённое от более раннего timbal (литавра), а timbre — от греческого tympanon, барабана: в старофранцузском это ещё барабан, в среднефранцузском — колокол под молоточком, и лишь потом слово стало означать окраску звука. Имя инструмента и слово «тембр» — родня по барабану.
Первый вариант я написал через i, как в литавре, потом переименовал: у цикады это tymbal, через y.
N6 ниже по тексту — рабочий индекс по имени чипа, он остался в названиях файлов и веток.
1. Замысел
Полифонический синтезатор реального времени на одной плате NUCLEO‑N657×0-Q (STM32N657: Cortex‑M55 на 800 МГц + нейроускоритель Neural‑ART). Управление — MIDI‑клавиатура Arturia KeyStep по DIN, выход — I2S‑DAC. Эстетика — синтетический глитч‑мелодизм в духе Alva Noto × Ryuichi Sakamoto, сознательно не имитация акустического инструмента.
Инженерная постановка жёстче художественной. Звук считается блоками (hop) по 4 мс; на 800 МГц это 3.2 миллиона тактов на всё: голоса, гармонический синтез, нейросеть, банк фильтров, детонацию, лимитер. Ни одного пропущенного блока, underrun строго ноль.
Дальше — как я в этот бюджет влезал, что пришлось измерить и какие мои представления о чипе оказались неверными. Неверными оказались почти все.
Роль нейросети выбрана не декоративной: она обязана делать то, чего дешёвым DSP не сделать (мера — в § 4), и работать в каждом hop’е.
2. Архитектура тракта
MIDI (DIN, USART3) → голосовой менеджер (250 Гц кадры: f0, amp, оси тембра)
→ скелет: гармонический банк (до 100 гармоник) + шум,
рендер СРАЗУ в 4 подполосы PQMF (12 кГц каждая) [M55, MVE]
→ рефайнер residual: FIR-банк 4×4 (линейная часть) [M55]
+ TCN-сеть (нелинейная дельта) [NPU]
→ PQMF-синтез 4×12к → 48 кГц [M55, MVE]
→ wow/flutter + hiss (винтажный слой) [M55]
→ пиковый лимитер → SAI → DAC
Ключевые решения формы:
PQMF‑домен. Вся «музыкальная» работа идёт в четырёх подполосах по 12 кГц: и рендер скелета, и вход/выход сети. Это делит стоимость на четыре и, что важнее, даёт сети компактный вход. Банк: K=4, прототип N=128 (β=8.80), реконструкция 67.5 дБ, задержка синтеза 1.32 мс.
Скелет рендерится прямо в подполосах (вариант B): для каждой гармоники берётся комплексный отклик полосы на её частоте из таблицы (шаг 25 Гц), и гармоника вносится сразу в свою полосу. Эквивалентность честному пути «рендер 48к → анализ» доказана численно: −43.5 дБ на глиссандо через все швы полос, рябь шва 0.21 дБ.
Анти‑чиптюн слой. Голый гармонический банк с идеальной фазой и статичным спектром звучит как приставка из девяностых. Так и должно быть: идеальная фаза даёт именно этот звук.
Лечение стоит ~ноль тактов, потому что живёт на кадровой сетке 250 Гц. Авто‑унисон: одиночная нота дублируется свободным голосом с детюном 7 центов, бюджет второго голоса всё равно уже оплачен сетью. Спектральный bloom: наклон гармоник дышит с огибающей, атака раскрывается, затухание темнеет. OU‑микродрейф f0: СКО 3 цента, τ≈0.4 с. Три ручки, и калибровал я их ушами по A/B‑рендерам того же Си‑кода на хосте. Перекрут (12 ц / 0.55 / 4 ц) отвергается на слух сразу — жижа под пальцами.
Конвейер глубиной в единицу. Пока NPU считает блок i−1, процессор рендерит блок i; результат сети подмешивается с задержкой в один hop. Вход сети живёт в ping‑pong‑буфере и не переписывается до готовности — при промахе сети блок звучит голым скелетом, деградация без щелчка.
3. Сеть
Рефайнер — каузальный TCN: 12 слоёв, дилатации 1…32 в двух стеках, C=88 каналов, вход [подполосы×4 + огибающая + две оси тембра + макро‑ось t], T=48 отсчётов подполосы за hop, два голоса раздельно, у каждого своё состояние. Стриминг — через явные FIFO‑состояния; эквивалентность чанкового прогона полному доказана точно, ошибка 0.0.
Квантование — int8 целиком, PTQ через onnxruntime quantize_static. Компилятор Neural‑ART принимает только такой QDQ‑диалект: ручную хирургию графа его матчер отвергает, я пробовал. Калибровка — корпусом в стриминге, с настоящими состояниями, а не нулями; scale и zero‑point у пар состояний вход/выход связаны принудительно, иначе по кольцу состояний пойдёт переквант.
4. А нужна ли тут сеть?
Первый содержательный результат проекта — отрицательный.
Сеть, обученная «в лоб» предсказывать residual учителя, у всех шести кандидатов в учителя проигрывала оптимальному линейному FIR из 520 коэффициентов (МНК по тому же корпусу): «сеть − FIR» от −0.94 до −3.73 дБ. 281 тысяча параметров хуже пятисот двадцати чисел.
Задним числом причина понятна: линейная часть задачи доминирует, и SGD тратит всю ёмкость на то, что фильтр делает бесплатно. Задним числом вообще всё понятно.
Лечение: отдать линейную часть даром. FIR замораживается «нулевым слоем» (pred = FIR(x) + net(x)), и сеть учится только на нелинейном остатке. Дельта сети растёт с корпусом: +2.78 дБ (2 мин) → +7.09 (9 мин) → +9.18 дБ (28 мин). Цифры роста — из обучающей метрики exam_delta.
Финальная сквозная оценка — одной шкалой, на hold‑out, включая int8-версию, которая реально играет на плате (eval_chain): FIR один +11.25 дБ подавления residual, FIR+сеть fp32 +17.16, int8 +16.89. Контроль «ONNX fp32 против torch» сошёлся в 0.000 дБ, цена квантования — 0.27 дБ, меньше 5% выигрыша. Вклад сети — +5.64 дБ сверх линейной базы. Это и есть ответ на вопрос, зачем здесь нейропроцессор.
В рантайме конструкция та же: FIR‑банк 4×4 (K=65, модуляция осью t) считает M55, ~100 к MAC на hop на два голоса; сеть — NPU.
5. Форма графа
Матрица из 30+ вариантов графа и 12 диагностических раундов компилятора. Правил из неё вышло немного, и каждое стоило недели:
-
dilations > 1в ONNX запрещён. atonn 4.0 эмулирует дилатацию пирамидой Space‑to‑Depth/Depth‑to‑Space с примерно квадратичной ценой: канонический граф исполнялся 37.36 мс при бюджете 4 мс на hop — в девять раз мимо. Рабочая форма — «gather2»: явные Slice‑тапы + канальный Concat + Conv 1×1. Те же математические операции, в 10 раз быстрее. -
Полосы — как каналы. Из двух проводок многополосности выполнима одна.
-
Всё on‑chip. Веса (273 кБ) и активации живут в npuRAM; внешняя флеш только хранит блоб, копируется при старте, CRC32 до копирования. Рассинхрон весов и графа даёт неверный инференс без всякой диагностики.
-
Форма заморожена по замерам на железе: C=88, V=2, T=48, L=12 — 2866 мкс/hop у голого графа, запас 10%. Больше receptive field (L=24) — +58% времени, мимо бюджета.
6. Прошивка
Ни одна ошибка инициализации на этом чипе не сообщает о себе.
Симптом всегда один: тишина в терминале, hard fault или тихо неверный результат. Работали только printf‑крошки вокруг каждого шага, печать сырых значений и чтение исходников ST. Документация про это молчит.
Что собралось за bring‑up (полный журнал в репозитории на Гитхабе — docs/chip_findings.md, docs/m2_notes.md):
-
У TCM есть ECC. Содержимое после сброса случайно, биты ECC ему не соответствуют — первое же ЧТЕНИЕ даёт precise hard fault. Область надо один раз записать словами до любого обращения.
-
Банки AXISRAM3…6 после загрузчика выключены (такт + shutdown): первая запись вешает ядро. NPU‑мастеру нужен RIF/RISAF, которых CubeMX не генерит.
-
Тракт к NOR‑флеш: пять причин отказа подряд, и каждая маскировала следующую. Домен питания VDDIO3. Своя инициализация вместо BSP. XSPI на 266 МГц там, где чип живёт на 50. Dummy‑циклы: в шаблоне 6, драйвер пишет 20. И снова частота. Пять раз подряд я думал, что нашёл причину. Рабочая точка всё это время лежала в дереве ST, в
STM32_ExtMem_Manager/custom/memories/— один файл, одно число. -
Делители тактов NPU стояли «не используется» со времён заглушки: NPU крутился на 400 МГц вместо 800. Отсюда и первое разочарование временем инференса — я грешил на архитектуру, а дело было в делителе.
-
Ложный assert в npu_cache.c при выключенном кэше AXI: Debug‑сборка вставала на проверке, которую сам код ниже делает безопасной.
7. Бюджетная война
Стартовая точка после включения настоящей сети: hop при игре 4.18 M из 3.2 M. Не влезаем на 30%.
Финал полного тракта, с FIR‑ступенью: 2.11 M в тишине, 2.47 M при худшем аккорде, пики смен аккорда 2.81 M, underrun 0, и учёт закрыт — сумма счётчиков сходится с полным hop до сотен тактов.
7.1. Измерительный контур
Три первых раунда оптимизации скелета я проектировал «по модели в голове». Результат: −16% вместо обещанных 50×, потом ноль, потом минус.
После третьего работа остановилась, и вместо правок я построил контур из четырёх уровней:
-
Хост (
make test) — одиннадцать тестов против питоновских эталонов; -
Модель Cortex‑M55 в QEMU (
mps3-an547) — тот же декодер MVE: числа векторных веток без платы и ИНСТРУКЦИИ на участок. Не такты: кэш и двойной выпуск не моделируются, стенд меряет объём работы, времени он не знает; -
Плата — DWT‑циклы, счётчики по ступеням в heartbeat;
-
Компилятор графа локально:
atonn— статический x86-64 ELF внутри виндовой установки ST Edge AI, работает в Linux‑контейнере; варианты графа просеиваются по таблице эпох без Windows и без платы.
Приём, окупившийся дважды: отношение «такты платы / инструкции QEMU» по одному и тому же счётчику. Около единицы — упор в число операций, лечится алгоритмом. Сильно больше — упор в память, лечится форматом данных.
Так участок wt (интерполяция отклика полос) оказался памятным: 2.8–3.7 такта на инструкцию против 0.9–1.3 у остальных. Три предыдущих раунда правили ему ФОРМУЛЫ. Формулы там были ни при чём.
7.2. Профиль по эпохам
Ступень NPU стоила 2.53 M тактов M55 на hop — одинаково в тишине и при игре, 79% бюджета до первой ноты. Разложить её на 88 эпох было нечем, пока не нашёлся штатный, но выключенный механизм: LL_ATON_EB_DBG_INFO кладёт в каждый блок эпох оценку компилятора, а LL_ATON_RT_SetEpochCallback зовётся в четырёх точках блока. Отсюда две величины: m55 — такты процессора внутри блока, это цена; и wall — полное время с ожиданием NPU, а это не цена. Профиль объяснил всё расхождение: из 2.53 M — 1.28 M тактов процессора в 54 «гибридных» эпохах, где ядро исполняет Concat/Slice программно.
Инструмент лежал внутри рантайма и был выключен флагом.
7.3. Пампинг
LL_ATON_RT_RunEpochBlock не блокирующий: он двигает конечный автомат и возвращается. 13 209 вызовов на 69 блоков за hop, ~90 тактов каждый — 1.2 M тактов процессор крутился в опросе. И всё это время рендер скелета, 0.6 M полезной работы, СТОЯЛ В ОЧЕРЕДИ перед простоем.
Лечение: рендер режется на отрезки, между отрезками — пампинг NPU; шаг адаптивный, полное число опросов прошлого hop’а делится на число отрезков. Арифметика не меняется ни на бит, проверено побайтовым сравнением выходного блоба.
После этого ступени перекрываются, и складывать их больше нельзя. Бюджет считается по максимуму: hop ≈ wall инференса + синтез + винтаж + хвост. Скелет при худшем аккорде (605 к) прячется в ожидание NPU целиком.
7.4. DTCM
128 кБ DTCM — памяти, до которой ядру нуль тактов, — проект не использовал вовсе. Туда переехали горячие таблицы скелета, ~40 кБ: участок wt подешевел вдвое (228 к → 107 к), весь скелет −26%.
Две обязательные детали: ECC‑инициализация до первого чтения и безопасный псевдоним адреса — 0×3000_0000, не 0×2000_0000, потому что FSBL живёт в secure‑мире.
7.5. Кольцо состояний
Сеть стримит 12 пар состояний, 43.7 кБ, и каждый hop выходы копировались во входы. Я разделил замер — копия против обслуживания кэша, 270 к против 35 к, — и оказалось, что дорога сама копия: 6.2 такта на байт. При том, что memcpy из newlib‑nano не побайтовый; развёрнутый ldr/str по 16 слов, проверено дизассемблером.
Узкое место — задержка пути M55 → npuRAM: простой цикл держит один промах за раз. MVE‑копия с четырьмя загрузками в полёте — 1.36 такта на байт, −78%.
А потом копирование исчезло совсем. Граф перегенерирован с user‑allocated IO, и вход/выход каждой пары состояний склеены в ОДИН буфер. Это законно, и не по общим соображениям — по отчёту генерации: state_out_k объявлен как Slice(cat_k), срез склейки, читаемой до записи, порядок гарантирован зависимостью по данным. Своп: 307 460 → 237 тактов.
Признак user‑IO надо читать из рантайм‑поля буфера. Макрос заголовка для этого не годится: он молча становится false, если заголовок не включён, и код едет по мусорным указателям.
Кэш AXI на этом профиле — штраф: +335K тактов на hop. Буферы лежат on‑chip, NPU читает их мимо кэша; ST в своих mpool тоже не ставит cacheable на внутренние пулы.
7.6. Concat и memcpy
Самый крупный пункт профиля — двенадцать блоков Concat, 1 019 501 такт, 32% бюджета. Ни одна опция компилятора его не двигала.
Ответ лежал в исходнике рантайма. LL_ATON_LIB_Concat включает быстрый путь с DMA только когда ось склейки — самая левая значащая. У нашей формы слева стоит V=2, проверка падает, и склейка исполняется общей веткой: четырьмя вызовами memcpy на блок.
Арифметика не оставила места гипотезам. Блоки переносят 145 728 байт за hop — ровно суммарная оценка компилятора, он считает Concat как элемент за такт. 1 019 501 / 145 728 = 6.997 такта на байт, и эта цифра одинакова у всех шести размеров блока. Так выглядит подпись цикла копирования; у настройки DMA разброс был бы другой.
Лечение — в линковке: свой memcpy. MVE, четыре 16-байтные загрузки до первой записи, хвост предикатом, ни одного скалярного цикла — GCC умеет опознать побайтовую идиому и заменить её вызовом memcpy, то есть самой этой функцией. Сильный символ вытесняет newlib и достаётся всему коду, включая файлы ST, которые править нельзя. Самопроверка на 300 длинах × 8 смещениях при инициализации: провал печатается в баннере раньше, чем его можно услышать.
Замер: Concat 1 019 501 → 343 459, 2.36 такта на байт. Предсказывал я ~1.4; по блокам вышло 2.21–2.88 с ростом к мелким — надбавка на вызов и короткие строки. Hop в тишине 2 826 500 → 2 104 000 (обе цифры ещё без FIR‑ступени, итоговые числа полного тракта — в § 9). Побочно ускорилось всё, что копирует: PQMF‑синтез −11 к, Slice‑гибриды −10%.
Треть бюджета реального времени оказалась библиотечной функцией копирования.
7.7. Дорога целиком
Худший аккорд, такты на hop: 4 180 000 → 3 669 167 (профиль + EC + частоты NPU) → 3 261 862 (пампинг) → 3 079 811 (DTCM) → 2 972 700 (user‑IO, кэш выкл) → 2 242 000 (свой memcpy).
Включение FIR‑ступени добавило функцию, не оверхед: полный тракт — 2 473 000. Сама ступень после MVE — 245K; первая, скалярная версия стоила 652K, про неё в § 8. Бюджет 3 200 000; запас ~23% на удержании и ~12% на пиках смен аккорда.
8. Что не сработало
-
Три раунда оптимизации «из головы» (§ 7.1). Инструмент измерения ставится до первой оптимизации. Я поставил после третьей.
-
Каждый крупный промах предсказания (50× → −16%; «exp2f стоит 60 к» → 13.8 к; «копия станет 1.4 т/б» → 2.36) указывал на механизм, которого нет в моей модели. Искать его надо счётчиком.
-
«FIR — это ~3% бюджета даже скаляром» — оказалось 20%, 652K тактов, 6.5 на MAC. Дот «назад по сигналу»
s[-k]не разворачивается компилятором и не дружит с префетчем. Разворот весов при пересчёте + MVE — 245 к. -
«B3-учитель невыучиваем формой сети» — оказалось артефактом остановки до сходимости. B3 ЗАПОМИНАЕТСЯ (+15.8 дБ оверфита), но не обобщается. Отбор учителя — только через оверфит‑экзамен и hold‑out.
-
Вынос трёх локальных переменных в файловые статики стоил 16% скелета: компилятор обязан перечитывать глобал в цикле. Поймано стендом QEMU за минуту.
9. Итог и открытые хвосты
Работает: полифоническая игра с MIDI‑клавиатуры, сеть в каждом hop’е, полный тракт D-17 — скелет + FIR‑банк + int8-сеть. underrun=0, hop 2.11 M в тишине и 2.47 M на худшем удержанном аккорде (пики смен 2.81 M) из 3.2 M, учёт по ступеням закрыт, расхождение — сотни тактов на миллионы. Вклад NPU измерен в децибелах подавления residual.
Сквозная численная оценка тракта (eval_chain, hold‑out 3 фразы, метрика — подавление residual, та же формула во всех отчётах проекта):
|
звено |
всего, дБ |
Δ к FIR |
полосы 0–3 |
|---|---|---|---|
|
FIR один |
+11.25 |
— |
+11.4 / +5.1 / +7.8 / +4.9 |
|
FIR + сеть, torch fp32 |
+17.16 |
+5.91 |
+17.6 / +7.3 / +9.2 / +5.2 |
|
FIR + сеть, ONNX fp32 (контроль) |
+17.16 |
+5.91 |
— совпал в 0.000 дБ |
|
FIR + сеть, int8 (плата) |
+16.89 |
+5.64 |
+17.3 / +6.3 / +8.4 / +4.1 |
Квантование стоит 0.27 дБ; худшая полоса int8 — третья, общая цифра это маскирует.
И ещё одно, про материал для прослушки: на статичном дроне все ступени тракта неотличимы по построению. Учитель динамический (OTT‑компрессия), его вклад слышен только на атаках и затуханиях.
Первые вердикты ушами (05.08): full приятнее dry, рефайнер слышен в плюс. Калибровка анти‑чиптюн слоя: пресет 7 ц/0.35/3 ц оказался «самое то» против 12/0.55/4 — «жижа под пальцами».
Прослушка пар на фразах pulse и line (08.08). Численно вклад сети на этих фразах — +1.29 дБ на pulse и +4.26 дБ на line: втрое больше там, где звук развивается, чем на повторяющихся ударах. Я предполагал обратное. Думал, уточнитель покупает прежде всего атаку.
Слухом различимы все три пары, включая pulse, где разница численно наименьшая. Записал тогда же, слушая:
«Сеть даёт округлость и вельвет, махровость. Плюс атака получилась с призвуком органа или сякухати, такая с передувкой как на духовых. FIR менее махровый, но тоже отличается от DRY»
Все три слова про вклад сети — про текстуру. Ни яркости, ни громкости в них нет. Это ушная подпись многополосной компрессии, а учитель здесь называется A2_ottpress: его выбрала численная процедура поиска, и ухо потом независимо описало именно его характер. Линейная часть при этом забирает основную долю подавления (+14.6 и +19.5 дБ из итоговых +15.9 и +23.8), а сеть докладывает то, что числами описывается хуже всего.
Сравнение с учителем дало четыре ответа сразу:
«У учителя тоже есть передув. Характер воспроизведён. Середина вполне себе похожа. Песок у учителя тоже есть. Звук получился немного ярче учителя»
Первые два закрывают главный вопрос метода. Чифф на атаке принадлежит цели, и рантайм его воспроизвёл; установившаяся середина похожа, то есть остаток различия лежит в тембре и до структуры не доходит. По условию, записанному до прослушки, это означает честный потолок: переобучение не начинается.
Третий ответ опроверг мою собственную диагностику. «Песок», который днём раньше я записал в дефекты, есть и у учителя. Логично: многополосная компрессия поднимает всё низкоуровневое, включая шумовую полку. Часть моего шума оказалась верностью цели — выпрями его «под ноль», и тракт уедет от учителя. Дефект переформулируется: сам шум законен, ему недостаёт спектральной формы.
Четвёртый ответ, «немного ярче учителя», — вероятно, то же наблюдение с другой стороны: шум добавляется с одинаковым весом во все четыре подполосы, а это по определению избыток высокочастотной энергии.
Отсюда предсказание, которое можно проверить численно и без переобучения. Наклон шума по полосам должен одновременно убрать избыток песка и подтянуть наш спектральный наклон к учительскому, то есть улучшить метрику. Если он её ухудшит — значит шум нёс нагрузку. Что из этого вышло — ниже, в «Наклоне шума».
Первый звук (07.08.2026)
Заиграл через PCM5102A на четвёртый день после того, как в тракт легла последняя ступень.
Первое включение — тишина. Ровная, без единого щелчка. XSMT на модуле сидел на земле, то есть ЦАП стоял в программном mute; притянул к 3.3 В — поехало. В инструкции сборки про это написано «не верить заводской установке перемычек, измерить», и вот ровно поэтому написано.
Дальше вылез перегруз при сильном нажатии, и он оказался мой собственный. Выходной лимитер с порогом 0.98 работал практически всегда, потому что сырой тракт даёт пик 1.77 на аккорде при velocity 127. Побочно это же значило, что выше velocity 90 инструмент переставал отвечать на силу удара: бьёшь сильнее, а он не громче. Диагноз занял один прогон хостового рендера с отключённым лимитером. Лечение — мастер‑уровень перед лимитером (D-22).
Значение подбирал дважды и по разным причинам. −6 дБ хватало, чтобы лимитер замолчал, но у наушников AKG K512 (32 Ом, 109 дБ SPL/V) в тракте оказалось лишних около 30 дБ, и весь рабочий ход линейного потенциометра умещался в первой десятой оборота. Свёл к −22 дБ (out_gain = 0.079): в перегрузку не уходит и нет ощущения, что недостаточно выхлопа.
Записал по горячим следам, до того как полез в счётчики:
«Ощущение синтетической жизни под пальцами — каждая нота немного не такая, как до этого, созвучия интересно играют и переливаются»
Это приёмка D-19, только не в децибелах. «Каждая нота немного не такая» — микро‑дрейф f0: у каждого голоса своё OU‑блуждание, повторное нажатие той же клавиши даёт не тот же звук. «Созвучия переливаются» — дрейф независим по голосам, биения между нотами аккорда всё время расходятся и сходятся.
Обе мысли про поведение во времени. Про тембр — ничего, и это ровно то, чего не хватало скелету, когда он звучал как 8-битный звук из Марио.
Шум скелета (07.08)
Первое, что я подумал про звук после дня игры: довольно много шума, песка параллельно каждому звучащему голосу.
«Параллельно» тут диагностично. Шум привязан к огибающей голоса — значит, я его синтезирую сам, и наводка ни при чём.
Шумовая ветвь скелета устроена предельно просто: к каждой подполосе добавляется белый шум с уровнем 0.15 × timbreB × огибающая. Коэффициент при этом один и тот же для всех четырёх подполос PQMF, тогда как гармоническая часть падает с частотой по естественному наклону. В верхней полосе тон уже почти кончился, а шум идёт в полную силу.
Замер (нота A3, velocity 100; шум выделен вычитанием рендера при timbreB = 0 — генератор псевдослучайных детерминирован, вычитание корректно):
|
timbreB |
шум/сигнал широкополосно |
шум/сигнал выше 4 кГц |
|---|---|---|
|
0.039 |
−30.5 дБ |
−10.7 дБ |
|
0.150 (заводская установка) |
−19.0 дБ |
−2.4 дБ |
|
0.315 |
−12.7 дБ |
−0.7 дБ |
Разница между двумя способами счёта — 16.6 дБ, и она объясняет, почему я не поймал это раньше: широкополосные −19 дБ выглядят приличным результатом. А ухо ищет шипение наверху, и там на заводской установке шум всего на два с половиной децибела тише тона.
Лечить надо форму. В каноническом DDSP шумовая ветвь — это фильтрованный шум с обученной огибающей по полосам; у меня на её месте одна константа. Жить с этим можно, избыток убирается полоской модуляции, которая правит ровно timbreB. Но одну часть недостающей ступени — наклон по полосам — удалось проверить и внедрить на следующий день.
A/B сети под пальцами (07.08)
Вопрос «что именно даёт нейросетевой уточнитель» до этого имел только численный ответ: +5.64 дБ спектрального расстояния до учителя. Чтобы получить ответ ушами, в конвейер добавлен принудительный вход в ветку «скелет + FIR» (D-24). Кнопка на плате гасит подмешивание остатка, при этом сеть продолжает считаться: состояние графа связно, бюджет тактов не меняется, между двумя положениями отличается ровно один фактор.
Записал сразу после сравнения:
«Теперь в сети действительно ощущается жизнь. Разница именно в динамике, и она прямо вот это и характеризует — инструмент становится живым»
Скелет отвечает за то, что звучит: высоту, гармонический состав, огибающую. Сеть отвечает за то, как это меняется во времени — за поведение атаки и затухания, за ту часть, которую детерминированная модель воспроизводит хуже всего и которую слушатель считывает как «живое». Потому и дрон оказался бесполезным материалом для прослушки ещё на хостовых сравнениях: на установившемся тоне разница минимальна по построению.
Обратная сторона того же сравнения: с сетью заметно больше высокочастотного шума, чем без неё. Для меня это некритично, избыток убирается полоской модуляции (CC1 правит шумовую компоненту скелета), но факт остаётся — уточнитель шумовую дорожку не подавляет, он её добавляет.
Вероятный механизм — квантование остатка в int8: шаг квантования даёт широкополосный «песок» примерно постоянной амплитуды, и в верхних полосах, где полезный сигнал уже мал, он выходит на первый план. Метрика цены квантования (−0.27 дБ по спектральному расстоянию) этого не показывает: усреднение по полосам почти слепо к ровной широкополосной подсыпке, а ухо ищет её там, где сигнала меньше всего.
Наклон шума (08.08)
Предсказание из предыдущего раздела было записано до опыта — в этом вся его ценность.
Наклон должен был убрать избыток песка и при этом не ухудшить метрику. Если бы метрика просела, значит плоский шум нёс какую‑то нагрузку и трогать его нельзя. Проверял на хосте, на тех же двух фразах, полным трактом с сетью fp32:
|
наклон |
|
|
ВЧ‑шум скелета |
|---|---|---|---|
|
0 (было) |
+19.53 → +23.79 |
+14.62 → +15.91 |
−23.4 дБ |
|
6 дБ/полосу |
+19.76 → +24.36 |
+14.72 → +16.01 |
−30.1 дБ |
|
12 дБ/полосу |
+19.74 → +24.39 |
+14.72 → +15.99 |
— |
Высокочастотный шум скелета упал на 7 дБ, а метрика не просела, а выросла — на 0.57 дБ на line и 0.10 на pulse. Форма спектра относительно учителя осталась прежней: СКО отличия по третьоктавам на line — 0.22 дБ до и после. На 12 дБ/полосу выигрыша уже нет, шесть — это колено.
Стоит назвать и то, чего я боялся и что не случилось. Сеть обучена на плоском шуме, и смена входного распределения могла её сломать. Не сломала — значит уточнитель опирается на структуру сигнала, а не на шумовую дорожку.
Реализация: nw[b] = SKB_NOISE_B 10^(-tilt * b/20), посчитанное один раз при заводе голоса. В горячем цикле остаётся столько же умножений, сколько было, и я объявил ступень бесплатной. Зря: QEMU намерил +1024 инструкции на hop — 0.15% замеренного участка и 0.03% бюджета платы. Непосредственная константа стала загрузкой из памяти. Считать надо было не только умножения.
По умолчанию noise_tilt_db = 0, то есть байт‑в-байт прежнее поведение: golden, CK4 и qemu‑ck4 не двигаются, боевой пресет ставит 6.0. Тот же флаг добавлен в питон‑скелет, контроль при нуле воспроизвёл прежние числа ровно, хост‑тесты 11/11. Приёмка ушами на плате: песка действительно стало меньше, приятнее.
Хвост на v2: сеть по‑прежнему обучена на плоском шуме, и переобучение на наклонённом скелете может дать ещё.
Латентность без осциллографа (08.08)
Осциллографа у меня нет. Выяснил, что он и не нужен: в этом тракте каждое слагаемое задержки известно из конструкции и потому детерминировано.
|
слагаемое |
мс |
откуда |
|---|---|---|
|
MIDI DIN, 3 байта @ 31 250 бод |
1.0 |
10 бит на байт, 320 мкс/байт |
|
квантование по сетке hop |
2.0…4.0 |
нота приходит равномерно внутри хопа |
|
слот конвейера (полубуфер SAI) |
4.0 |
двойной буфер, полубуфер = хоп |
|
PQMF‑синтез |
1.32 |
замер на плате |
|
интерполяционный фильтр ЦАПа |
0.44 |
21/fs при 48 кГц (даташит PCM5102A: 20–22/fs; измерение TI — 21/fs) |
|
итого |
8.7 медиана / 10.7 худшая |
ТЗ § 5.4 выполнено |
Единственное слагаемое, которое до сих пор бралось «по типовому», — задержка цифрового фильтра ЦАПа. Сверил с даташитом и с измерением производителя: оказалась ровно той, что закладывалась в бюджет ещё на этапе проектирования конвейера.
Приёмку тут выдаёт человек. После дня игры я сформулировал так: по ощущениям играбельно, задержка не ощущается. Порог заметности на клавишах у большинства исполнителей лежит в районе 10–15 мс; медиана 8.7 попадает под него с запасом, а худший случай 10.7 — на границе, но приходится на ноты, пришедшие в конец хопа, то есть случаен и не образует ощущения «тормозит».
Если однажды понадобится настоящий замер, осциллограф всё равно не нужен. Доминирующую часть — квантование плюс слот конвейера — прошивка может померить сама: засечь счётчик тактов на разборе Note On и на старте передачи того полубуфера, куда нота попала. Это даст точное распределение 6…8 мс без единого прибора. Независимая грубая сверка — запись на телефон щелчка клавиши и атаки звука в один файл: на 48 кГц разрешения хватает, чтобы подтвердить порядок величины.
Стенд
Инструмент в собранном виде. Arturia KeyStep как клавиатура и источник DIN‑MIDI, оптронная развязка MIDI на макетке, модуль PCM5102A (фиолетовый, с подписью LINE OUT) и плата NUCLEO‑N657×0-Q. Всё лежит прямо на клавишах — единственный корпус, который этот проект получил. Синяя кнопка на плате справа — переключатель «сеть в тракте / только скелет + FIR» (D-24); ею и сделано сравнение, давшее «разница именно в динамике».
Пять проводов от гребёнки к ЦАПу — BCK на D12, LRCK на D11, DIN на D10, земля с соседней ноги CN14 и питание с CN5. Красный светодиод на модуле — его собственное питание, звук он никак не индицирует. Вывод SCK притянут к земле: мастер‑клока плата не выдаёт, и без этой перемычки внутренний ФАПЧ ЦАПа не стартует — модуль молчит, выглядя при этом совершенно исправным.
Чип
STM32N657×0H3Q в корпусе VFBGA264. Внутри — Cortex‑M55 на 800 МГц с расширением Helium и нейроускоритель Neural‑ART. Весь проект — это спор о том, как поделить между ними четыре миллисекунды.
Рядом с процессором — внешняя NOR‑память MX25UM51245G на 50 МГц (справа вверху). Оттуда грузится прошивка: у STM32N6 нет внутренней флеш‑памяти. Это одно из первых обстоятельств, которое приходится принять, перенося привычные рефлексы с других STM32.
Плата с двух сторон — для тех, кто будет повторять: распиновка I2S, клеммники питания и перемычки BOOT0/BOOT1 (обе в положении Flash boot) видны на шелкографии.
Спектр и пики
Спектрограммы четырёх дорожек рядом красивы и малоинформативны: разница между ними живёт в единицах децибел и на глаз не читается. Полезнее два графика, каждый отвечает на конкретный вопрос.
Первый — форма спектра относительно цели, при выровненном RMS. Одна линейная часть отклоняется от учителя на 2…5 дБ почти во всей полосе; полный тракт выше 220 Гц ложится в ±0.3 дБ. Остаток расхождения сосредоточен ниже двухсот герц, где мы горячее учителя на 3.5…7.5 дБ — то есть там, где у этих фраз почти нет полезной энергии.
Второй график объясняет ушную оценку «наш звук немного ярче учителя», и объясняет неожиданно. Средний спектр совпадает с точностью до десятых долей децибела — значит дело не в тембре. Медианный кратковременный крест‑фактор тоже совпадает: 7.41 дБ против 7.42 у учителя. Совпадает вообще всё вплоть до девяносто девятого процентиля.
Расходятся только редкие выбросы: на 99.9-м процентиле мы на 1.8 дБ выше цели, а по абсолютному пику — на 2.9 дБ на line и 3.8 дБ на pulse. Учитель — это многополосная компрессия, она такие выбросы срезает; наш уточнитель их воспроизводит, местами усиливая.
Я сообщил о спектральном отличии, а измерение показало временно́е. Редкие короткие выбросы на атаках воспринимаются как «ярче», хотя средний спектр не сдвинут ни на децибел.
Хвосты, сознательно оставленные: Concat можно опустить ещё (~343 к → DMA или форма графа без склеек, требует перегона компилятором), формат таблицы отклика f16 (−100 к на аккордах), кэш AXI как тема отдельного разбора.
10. Что из этого работает не только здесь
У выводов разный срок годности.
Дольше всего проживёт физика памяти. Иерархия бьёт мегагерцы: переезд таблиц в DTCM — −53% на участке. Дорога стоит дороже ширины: одиночные обращения к npuRAM ~66 тактов за транзакцию, четыре промаха в полёте превращают 7 тактов/байт в 1.4 — этим я вылечил три разных места, своп состояний, Concat рантайма и FIR‑дот. Направление обхода имеет цену: дот «назад по сигналу» — 6.5 такта/MAC, разворот данных при подготовке — 2.45. Лучшая копия — которой нет: склейка буферов по доказанной зависимости данных, 307 460 → 237 тактов.
Это свойства любой шины с ускорителем, от MCU до серверного GPU. Туда же — метод: цикл измерения строится до первой оптимизации; промах предсказания разворачивает к счётчику; отношение «такты платы / инструкции модели» как компас (около единицы — арифметика, больше двух — память); учёт, сходящийся в ноль.
Поколенческое — про сегодняшний класс MCU+NPU. Пиковым GOPS верить нельзя: мой ускоритель занят 61% времени при игре (wall инференса ~1 970 к тактов из 3.2 М на hop) при утилизации MAC‑ов в единицы процентов. Решает форма графа, а форму диктует компилятор: dilations запрещены, канон родился из тридцати вариантов. Половину «инференса» может исполнять ядро в гибридных эпохах, а треть бюджета — оказаться библиотечным memcpy. Лечится это чтением исходников рантайма и локальным прогоном компилятора до железа.
Всё это устареет вместе с тулчейнами. Но ближайшие годы именно это сэкономит недели любому, кто возьмёт чип этого класса.
Архитектурное — главный экспортный результат. Паттерн «детерминированный скелет + маленькая сеть, обученная на дельту поверх замороженной линейной части, с вкладом, измеренным на hold‑out» переносится далеко за аудио: сенсорика, управление, RF. Сеть получает только то, чего DSP не умеет, и её полезность выражается числом — у меня это вклад сети +5.64 дБ сверх линейной базы при int8-тракте +16.89, измерено на hold‑out. К паттерну прилагается лестница даунскейла: есть NPU — сеть живёт на сэмпл‑рейте; нет — та же идея сжимается до декодера на кадровой сетке 250 Гц, который влезает и в чип без ускорителя.
Идея, выживающая при смене железа, и была целью. Объяснение — продукт.
Приложение А. Числа тракта (канон)
|
Параметр |
Значение |
|---|---|
|
hop |
192 отсч. @48к = 4 мс = 3.2 Mcyc @800 МГц |
|
PQMF |
K=4 × 12 кГц, N=128, реконструкция 67.5 дБ |
|
скелет |
≤100 гармоник + шум, рендер в подполосах, MVE |
|
сеть |
TCN L=12, C=88, V=2, T=48, int8, RF 21 мс |
|
веса |
273 кБ on‑chip (NOR → AXISRAM5, CRC32) |
|
FIR‑банк |
4×4, K=65, ось t; 99 840 MAC/hop (V=2) = 245 к тактов, MVE |
|
инференс (wall) |
2304 мкс в тишине; m55-цена ступени ~636 к |
|
hop полного тракта |
2.11 M тишина / 2.47 M аккорд / 2.81 M пики смен |
|
мастер‑уровень выхода |
−22 дБ ( |
|
наклон шума скелета |
6 дБ на полосу PQMF ( |
|
латентность тракта |
8.7 мс медиана / 10.7 худшая (MIDI 1.0 + квантование 2…4 + слот 4.0 + синтез 1.32 + ЦАП 0.44) |
Приложение Б. Воспроизведение
Репозиторий: https://github.com/mikekss/tymbal
dsp/ — эталоны Python с самопроверками; train/ — обучение, экспорт gather2, квантование; fw/ — прошивка с хост‑тестами (make test), стендом QEMU (make qemu-ck4), CK4-сверкой на плате и рендером боевого тракта в wav без платы (make play); tools/atonn/ — локальный прогон компилятора графа. Все контракты и допуски — в шапках файлов; журналы решений и находок — в docs/.
Автор: Michael_KS


