# Creator SDK для C++

Встроенный чат и его автоматический цикл разработки описаны в [AI-помощник Creator](CREATOR_AI_RU.md). Контракт DSP SDK и переносимые форматы при добавлении чата не меняются.

Полное руководство для автора мини-модулей VLTONE и для нейросети, которая пишет для них код. Документ описывает реализованный SDK 1, сверенный с исходниками 7 октября 2026 года. Здесь приведены правила языка и графа, весь публичный API, жизненный цикл DSP, работа нескольких функций, создание карточки, форматы файлов и проверенные примеры.

Мини-модуль создаётся в Creator как граф. Его C++-ноды обрабатывают аудиокадры или управляющие значения. Creator сам строит порты, генерирует оболочку, компилирует переносимый WebAssembly и устанавливает `.vltmini`. Нативный код для исполнения создаётся приложением локально. Visual Studio или отдельный компилятор автору модуля не нужны.

Для нейросети можно передать **этот документ целиком**: в приложениях находятся полный заголовок SDK и исходники всех примеров. Краткий контракт и шаблон задания находятся в [AI_GUIDE_RU.md](AI_GUIDE_RU.md). Готовые проекты примеров лежат в [examples/projects](examples/projects), а редактируемые исходники — в [examples](examples).

<!-- SDK_TOC_START -->

## Содержание

- [Версии и границы SDK](#версии-и-границы-sdk)
- [Нодовый DSP без C++](#нодовый-dsp-без-c)
- [Быстрый запуск одной функции](#быстрый-запуск-одной-функции)
- [Как взаимодействуют окно, граф и код](#как-взаимодействуют-окно-граф-и-код)
- [Сигнатура входной функции](#сигнатура-входной-функции)
- [Типизированные порты и стабильные ID](#типизированные-порты-и-стабильные-id)
- [Память DSP и жизненный цикл](#память-dsp-и-жизненный-цикл)
- [Полный публичный API](#полный-публичный-api)
- [Несколько функций в одной ноде](#несколько-функций-в-одной-ноде)
- [Несколько нод с вызовами Function](#несколько-нод-с-вызовами-function)
- [Внешние ручки и оформление мини-модуля](#внешние-ручки-и-оформление-мини-модуля)
- [Редактирование и сохранение](#редактирование-и-сохранение)
- [Обновить ноду и Compile](#обновить-ноду-и-compile)
- [Переносимость и структура файлов](#переносимость-и-структура-файлов)
- [Каталог встроенных нод](#каталог-встроенных-нод)
- [Ограничения исполнения и качество DSP](#ограничения-исполнения-и-качество-dsp)
- [Диагностика типичных ошибок](#диагностика-типичных-ошибок)
- [Проверенные примеры](#проверенные-примеры)
- [Как давать задание нейросети](#как-давать-задание-нейросети)
- [Источники контракта и сопровождение](#источники-контракта-и-сопровождение)
- [Приложение A Полный заголовок SDK](#приложение-a-полный-заголовок-sdk)
- [Приложение B Полные исходники примеров](#приложение-b-полные-исходники-примеров)

<!-- SDK_TOC_END -->

## Версии и границы SDK

| Компонент | Текущая версия или возможность |
|---|---|
| Язык | C++23, Clang, целевая платформа `wasm32-wasip1` |
| Публичный SDK и ABI кода | 1 |
| Проект Creator `.vltcreator` | 3 (читает 1–2) |
| Переносимый мини-модуль `.vltmini` | 5 (читает 1–4) |
| Проект DAW | 16 |
| Пресет Channel Strip | 6 |
| Аудио | Mono и stereo, тип сэмплов `float` |
| Исполнение | Локальный WAMR AOT внутри последовательного графа |
| Создание интерфейса | Нода Interface, до двух внешних ручек |
| Режимы | До восьми именованных графов с общими внешними ручками |

MIDI, полифонический голосовой движок, встроенный AI, наблюдение за внешним редактором, сторонние DLL и совместное редактирование мини-модулей в SDK 1 не входят. Встроенные эффекты подключаются нодами графа: публичный C++-заголовок не содержит классов вроде `vlt::Chorus`, `vlt::Compressor` или JUCE-процессоров.

## Нодовый DSP без C++

Новые типы Integer, Array, List и Buffer, память отсчёта, обратные связи, подграфы, личная библиотека и передискретизация описаны в [NODE_DSP_RU.md](NODE_DSP_RU.md). Это расширение графа; публичные типы сигнатур C++ SDK 1 остаются AudioFrame, float, bool и Function. Готовые проекты без исходного кода находятся в [examples/nodes](examples/nodes).

## Быстрый запуск одной функции

1. Открой **View → Creator**. В русской локализации ищи Creator в меню вида программы.
2. Создай проект, указав имя проекта и имя мини-модуля. Это отдельный проект Creator.
3. На пустом участке канваса нажми правую кнопку мыши и выбери **Code → C++ Function**. Также доступны библиотека нод и клавиша Tab.
4. В диалоге выбери имя `process`. После создания открой редактор кода на карточке и замени заготовку полным исходником ниже.
5. В поле **Entry function** должно стоять `process`. Нажми **Update node / Обновить ноду**.
6. Соедини зелёные аудиопорты `Input.out → process.input → Output.in`. Прямая исходная связь Input → Output должна быть заменена: у входа Output может быть только один источник.
7. Для первого теста оставь `gain = 1`. Для ручки на карточке соедини выход первой ручки Interface со входом `gain`, задай ей диапазон 0–2 и начальное значение 1.
8. Сохрани `.vltcreator` в выбранную тобой папку и нажми **Compile**. В Channel Strip открой «+» секции MINI и выбери свой модуль.

Полный файл одной функции:

```cpp
#include <vlt/creator.hpp>

VLT_NODE vlt::AudioFrame process(
    VLT_PORT("input") vlt::AudioFrame input,
    VLT_PORT("gain") float gain = 1.0f)
{
    return input * vlt::clamp(gain, 0.0f, 2.0f);
}
```

При `gain = 1` результат равен входу, при `gain = 0` — тишина. Эта функция не хранит память и не требует State, Context, prepare или reset. Она получает **один аудиокадр**, а не блок и не весь файл.

## Как взаимодействуют окно, граф и код

| Понятие | Назначение |
|---|---|
| Проект Creator | Исходники, графы режимов, расположение нод, оформление, черновики |
| Определение мини-модуля | Переносимый алгоритм, внешний интерфейс, стабильный ID |
| Экземпляр на Channel Strip | Собственные параметры, bypass, положение до/после Audio FX и DSP-состояние |
| Input и Output | Граница аудиосигнала всего мини-модуля |
| Interface | Оформление карточки и источники значений её внешних ручек |
| C++ Function | Одна выбранная входная функция плюс её локальные вспомогательные функции |
| Signal connection | Поток аудио, чисел или Gate, вычисляемый графом |
| Function connection | Возможность вызвать функцию другой ноды из своего C++ |

Путь обработки на канале: вход канала → мини-модули **до Audio FX** → Audio FX → мини-модули **после Audio FX** → фейдер. Обе позиции находятся до фейдера. Позиция принадлежит экземпляру карточки, а не C++-сигнатуре. Внутри каждой позиции модули обрабатываются в порядке карточек. Лимит трёх относится к карточкам на полосе; внутри одной карточки допустимы десятки нод.

Сигнальная часть графа ациклическая. Последовательные и параллельные ветви, буферы и компенсацию заявленных задержек готовит хост. Creator использует `GraphProcessor(1)` и не создаёт пользовательские DSP-потоки внутри модуля. Ноды, не участвующие в выходном сигнале, не исполняются как отдельные сигнальные процессоры. Их C++-исходник всё равно должен быть корректным; функция может оставаться нужной как вызываемая зависимость.

Вызов пользовательского кода происходит внутри сгенерированного цикла по кадрам блока. Автор пишет алгоритм одного кадра. Размер блока не должен быть зашит в функцию. Управляющие порты меняются с точностью до кадра, включая автоматизацию внутри блока.

## Сигнатура входной функции

Входная функция — свободная функция в глобальной области своего исходника, с именем из поля Entry function. Она не должна быть перегружена, шаблонной функцией или методом класса. Рекомендуемое имя — `process`; `saturate`, `filter` и другие допустимые C++-имена тоже подходят. Используй латинские буквы, цифры после первого символа и `_`, избегая ключевых слов C++.

`VLT_NODE` отмечает функцию как ноду. Порты определяются выбранной сигнатурой. Исходник может содержать другие обычные C++-функции и типы, но отдельные карточки для них автоматически не возникают.

| Аргумент | Что появляется в Creator | Передача |
|---|---|---|
| `vlt::AudioFrame input` | Audio-вход | По значению |
| `float drive` | Number-вход и ручное числовое значение | По значению |
| `bool enabled` | Gate-вход и ручное логическое значение | По значению |
| `vlt::Function<R(Args...)> helper` | Обязательный Function-вход | По значению |
| `State& state` | Скрытая память экземпляра | Ссылка |
| `const vlt::Context& context` | Скрытый контекст времени | Константная ссылка |

Допускается максимум один аргумент состояния и один контекст. Они не расходуют обычные порты. Обычных и функциональных входов вместе может быть до 16. Аудио и функциональный вход требуют связи, когда нода исполняется в сигнальном графе. Неподключённые Number и Gate используют сохранённые ручные значения.

`double`, `int`, указатели, массивы, ссылки на сигнальные значения, строки и произвольные классы **не являются типами портов**. Их можно использовать как подходящие локальные типы вычислений или поля состояния, но наружу передавать только поддерживаемые сигналы. Например, фазу LFO удобно хранить в `double`, а его выход должен быть `float`.

Сигнальные аргументы передаются по значению. Используй `vlt::AudioFrame input`, а не `const vlt::AudioFrame& input`. Возвращаемое значение тоже должно быть значением поддерживаемого типа, без ссылок и указателей.

### Возвращаемые значения

| Возврат | Порты |
|---|---|
| `vlt::AudioFrame` | Один Audio-выход с ID `out` |
| `float` | Один Number-выход с ID `out` |
| `bool` | Один Gate-выход с ID `out` |
| Aggregate struct с именованными полями | По одному выходу на поле, от 1 до 16 |

Структура выходов должна иметь открытые обычные поля только `vlt::AudioFrame`, `float` или `bool`, без вложенных структур, массивов, указателей, пользовательского конструктора и виртуальных методов. Присвой каждому полю `VLT_PORT`. `void`, `std::tuple`, `std::pair`, возврат вызываемой Function и выходы через неконстантные ссылки не входят в контракт SDK.

Несколько выходов рассчитываются **одним вызовом ноды на кадр**. Далее хост раздаёт поля по отдельным подготовленным буферам. Пример с аудио, огибающей и Gate находится в `MultiOutput.cpp` в приложении.

### Значения по умолчанию

Значение `float drive = 1.5f` задаёт начальное ручное значение нового порта. Для чисел используй константы с суффиксом `f`, для Gate — `true` или `false`. У численного порта без явного значения начальным значением обычно будет 0. Для Function допустима заготовка `= {}`, но это **не рабочая функция по умолчанию**: до сборки её вход всё равно нужно соединить.

При обновлении существующей ноды сохранённое ручное значение порта может остаться прежним. Новый C++ default не является командой сбросить уже настроенный модуль. Начальные значения внешних ручек задаются отдельно в Interface.

Диапазон Number-порта по умолчанию широк: −1 000 000…1 000 000. Диапазон и единица измерения из C++ default не выводятся. Поэтому алгоритм сам ограничивает допустимые значения через `vlt::clamp`, а для удобного управления задаются границы ручки Interface. Число поступает в физических единицах, выбранных автором: например, 1500 означает 1500 Гц, а не нормализованное положение ручки.

## Типизированные порты и стабильные ID

| Тип | Вид порта | Смысл |
|---|---|---|
| Audio | Зелёный круг | Текущие сэмплы L/R |
| Number | Синий ромб | Одно числовое значение на кадр |
| Gate | Фиолетовый квадрат | Логическое состояние на кадр |
| Function | Янтарный шестиугольник, пунктирная связь | Вызов функции с совместимой сигнатурой |

Один выход можно подключить к нескольким входам. У каждого входа может быть один источник. Для суммирования и смешивания используй подходящие ноды, а не несколько проводов в один вход. Number → Audio преобразуется через Number to Audio; Audio → Number — через Audio to Number или Envelope Follower. Gate и Number автоматически взаимозаменяемыми не становятся.

Подключённый вход берёт значение провода вместо ручного значения. После отключения провода возвращается ручное значение. Ни провод Interface, ни Number-модуляция сами по себе не гарантируют сглаживание внутри авторского C++.

`VLT_PORT("drive")` — постоянный ID порта, а `drive` после типа — имя C++-аргумента. Например, переименование аргумента в `saturation` с сохранённой аннотацией `VLT_PORT("drive")` оставляет сигнальные соединения привязанными к прежнему порту.

Если аннотации нет, «Обновить ноду» добавляет её в исходник. Сохраняй эти аннотации при последующих правках. ID должны быть непустыми, уникальными среди входов и среди выходов, не длиннее 128 символов. ID `function` и начало `$` зарезервированы. Рекомендуются короткие латинские ID: `audio_in`, `drive`, `cutoff`, `envelope`.

После изменения сигнатуры снова нажми «Обновить ноду». При ошибке разбора сохраняются последние корректные порты. Удалённые или несовместимые порты оставляют проблемные связи в черновике; их нужно удалить или переподключить до Compile.

Перестановка параметров при сохранённых ID сохраняет **сигнальные провода**. Для вызова через Function порядок аргументов остаётся обычным порядком C++: при перестановке двух `float` проверка типов не распознает подмену их смысла. Проверь все места вызова вручную.

Есть три разных уровня идентичности: ID мини-модуля связывает установленные версии, ID внешнего регулятора сохраняет адрес автоматизации, ID C++-порта сохраняет провода внутри графа. Совпадение надписей не заменяет ни один из этих ID.

## Память DSP и жизненный цикл

Обычная локальная переменная функции создаётся заново на каждом кадре. Историю фильтра, индекс задержки, фазу генератора, сглаженные параметры и состояние детектора храни в явном `State&`. Для каждой отдельной сигнальной ноды и каждого функционального использования хост создаёт собственное состояние.

Тип не обязан называться State, но такое имя удобно и использовано во всех примерах. Он должен быть trivially copyable и иметь тривиальный деструктор. Подходят числа, `AudioFrame`, небольшие `std::array`, `vlt::Buffer<T>` и `vlt::Random`. Поддерживаются начальные значения полей. Владение `std::vector`, `std::string`, `std::unique_ptr`, мьютексами и подобными ресурсами в состоянии для этого SDK не подходит.

Изменяемые глобальные переменные, изменяемые `static` и `thread_local` запрещены. Константы `constexpr` допустимы. Не используй глобальное состояние для связи между нодами: передачу значений выполняют порты, а память всегда принадлежит экземпляру.

### Последовательность вызовов

1. Creator анализирует код, компилирует его и создаёт локальный AOT вне аудиопотока.
2. Создаются начальные значения State и функциональных зависимостей.
3. Вызывается необязательная `prepare(State&, vlt::PrepareContext&)`. Здесь выделяется подготовленная память и объявляются latency/tail.
4. Хост сохраняет снимок подготовленной памяти.
5. Во время обработки хост вызывает входную функцию по одному разу на кадр для сигнальной ноды. Вызовы других функций выполняются там и столько раз, где и сколько их вызывает её C++.
6. При reset хост восстанавливает подготовленный снимок и затем вызывает необязательную `reset(State&)`, включая зависимости.

`prepare` и `reset` — специальные свободные функции с точными именами. Они имеют смысл только при наличии State у входной функции. Используй именно эти формы:

```cpp
void prepare(State& state, vlt::PrepareContext& context);
void reset(State& state);
```

Это объявления формата, не самостоятельная программа. Полные определения приведены в примерах Delay и Chorus. Не вызывай prepare из process. В reset можно очистить уже выделенный буфер, но нельзя заново выделять его. `state = {}` в reset уничтожит указатели подготовленного Buffer; сбрасывай нужные поля отдельно.

При отсутствии своего reset подготовленный снимок всё равно восстанавливается. Поэтому SeededNoise воспроизводит начальную последовательность после сброса без дополнительного callback. При изменении частоты, конфигурации или алгоритма хост подготавливает соответствующий экземпляр; код не должен зависеть от того, сколько раз приложение создаст или подготовит DSP.

### Подготовленные буферы

`vlt::Buffer<T>` хранится в State. Его `prepare(count)` вызывается в prepare ноды. Размер задаётся числом **элементов T**, а не байтов. Память выделяется из подготовительной арены экземпляра и очищается значениями `T{}`.

| Метод | Поведение |
|---|---|
| `buffer.prepare(unsigned count)` | Выделить и обнулить подготовленный буфер; только в prepare |
| `buffer.size()` | Число элементов |
| `buffer.clear()` | Записать `T{}` во все элементы без нового выделения |
| `buffer[index]` | Доступ по индексу с проверкой `index < size()` |
| Константный `operator[]` | Чтение по индексу с той же проверкой |

Буфер не предоставляет resize, push_back или самостоятельное освобождение. Повторный prepare расходует новую память арены, поэтому не используй его как частую операцию изменения размера. Выделяй сразу необходимый максимум, например `ceil(sampleRate * maxDelaySeconds) + 2` для интерполируемой задержки. Значения ручек во время воспроизведения меняют положение чтения, а не размер памяти.

Ограничение 16 MiB относится ко всей линейной памяти Wasm-экземпляра: состояниям, буферам, стеку и служебным данным вместе. Оно не даёт каждой ноде отдельные 16 MiB. Обычные malloc/new, включая выделение через контейнеры STL, не заменяют Buffer даже в prepare.

Для маленькой фиксированной памяти допустим `std::array` в State. Для задержки, зависящей от sampleRate, удобнее Buffer. Большие локальные массивы внутри process расходуют стек и создаются на каждом кадре. Проверки Wasm защищают границы линейной памяти; обычный C++-массив внутри неё всё равно требует корректных индексов. Buffer дополнительно проверяет границы именно своего массива.

### Задержка и хвост

`PrepareContext::latency` и `tail` измеряются **в сэмплах**, не в миллисекундах. По умолчанию оба равны нулю.

Latency — компенсируемая алгоритмическая задержка, например look-ahead или задержка преобразования. Хост использует её для выравнивания параллельных сигнальных ветвей. Внутренняя линия задержки хоруса или художественное эхо не означают автоматически такую же latency. У показанных Delay и Chorus есть непосредственный dry-путь, поэтому latency равна 0.

Tail — время, в течение которого алгоритм способен выдавать остаток сигнала после окончания входа. Для задержки без feedback достаточно максимального времени задержки. При feedback нужен запас на затухание повторов. Объявляй максимальные значения, допустимые при автоматизации параметров; менять эти объявления из process нельзя. Текущий хост принимает latency до 30 секунд и tail до 120 секунд.

Когда функция вызывает другую через Function, хост подготавливает её память, но не выводит суммарную задержку из произвольного C++-порядка вызовов. Подготовка вызывающей функции обязана объявить latency/tail всего собственного алгоритма. Для последовательного вызова двух задерживающих стадий это может требовать сложения задержек; для параллельной обработки — собственного выравнивания и соответствующего объявления. Между обычными сигнальными нодами выравнивание выполняет граф.

## Полный публичный API

Единственный специальный include автора: `#include <vlt/creator.hpp>`. Заголовок также подключает `<algorithm>`, `<array>`, `<cmath>`, `<cstddef>`, `<cstdint>`. Доступна поставляемая стандартная библиотека C++ в рамках запретов на ОС, выделение памяти и исключения. Для ясности собственные используемые стандартные заголовки можно включать явно.

### AudioFrame

```cpp
struct AudioFrame {
    float left = 0;
    float right = 0;
};
```

Тип находится в `namespace vlt`. Он представляет один момент времени сразу для L/R. Номинальный аудиодиапазон обычно −1…1, но API не ограничивает каждую амплитуду этим диапазоном: допустим внутренний запас по уровню. Автор отвечает за разумный выход и отсутствие NaN/Infinity.

| Операция | Результат |
|---|---|
| `frame * scalar` | Умножение обоих каналов на float |
| `a + b` | Поканальное сложение AudioFrame |
| `a - b` | Поканальное вычитание AudioFrame |
| `vlt::tanh(frame)` | Поканальный tanh |
| `vlt::mix(a, b, t)` | Поканальная линейная интерполяция |

Операторы `scalar * frame`, `frame / scalar`, `frame * frame` и составные присваивания SDK не объявляет. Вместо деления используй `frame * (1.0f / divisor)`, предварительно защитив divisor от нуля. Если нужна поканальная математика, обращайся к `.left` и `.right`.

В mono хост зеркалирует вход в оба поля и использует левый выход. Для моносовместимого фильтра или эффекта обрабатывай оба поля согласованно. Стереохорус может специально модулировать их по-разному, создавая ширину на стереоканале; в настоящем монотракте будет слышна его левая обработка.

### Context

| Поле | Тип | Значение для автора |
|---|---|---|
| `sampleRate` | `double` | Частота дискретизации в Гц; используй её вместо константы 48000 |
| `tempo` | `double` | Темп транспорта в BPM |
| `beat` | `double` | Позиция транспорта в долях PPQ; обновляется для каждого кадра |
| `sampleTime` | `std::int64_t` | Позиция кадра на временной шкале в сэмплах |
| `seed` | `std::uint64_t` | Seed, переданный модулю при подготовке |
| `channels` | `unsigned` | 1 для mono, 2 для stereo |
| `playing` | `bool` | Состояние воспроизведения транспорта |

Передавай контекст как `const vlt::Context&`. Он не содержит указателя на блок, MIDI, имени режима, путей файлов или UI. SampleTime/beat могут измениться скачком при навигации по проекту; для свободно работающего LFO храни свою фазу в State. Для синхронизации с музыкальной позицией используй beat. При `playing == false` не нужно автоматически возвращать нули: эффект может обрабатывать входной мониторинг или хвост.

Context во вложенный Function-вызов передаётся хостом автоматически, он соответствует тому же аудиокадру. Повторный вызов внутри этого кадра не двигает Context вперёд сам по себе.

### PrepareContext

| Поле | Тип | Использование |
|---|---|---|
| `sampleRate` | `double` | Рассчитать размеры и коэффициенты |
| `channels` | `unsigned` | Подготовить mono/stereo-конфигурацию |
| `maxBlockSize` | `unsigned` | Максимальный размер блока хоста; process по-прежнему получает кадр |
| `seed` | `std::uint64_t` | Инициализировать случайное состояние данного использования |
| `latency` | `unsigned` | Автор записывает задержку в сэмплах |
| `tail` | `unsigned` | Автор записывает хвост в сэмплах |

В этом контексте нет текущих значений входных портов. Если размер памяти зависит от будущей ручки, выдели максимальный допустимый размер. Хост разделяет seed подготовки между нодами и функциональными использованиями. Для независимых воспроизводимых генераторов используй именно `prepareContext.seed` и сохраняй генератор в State.

### Математические функции

| Вызов | Смысл |
|---|---|
| `vlt::clamp(float x, float lo, float hi)` | Ограничить x; автор обязан обеспечить `lo <= hi` |
| `vlt::tanh(float x)` | Гиперболический тангенс |
| `vlt::tanh(AudioFrame x)` | То же для L/R |
| `vlt::sin(float x)` | Синус, аргумент в радианах |
| `vlt::cos(float x)` | Косинус, аргумент в радианах |
| `vlt::exp(float x)` | Экспонента |
| `vlt::pow(float x, float y)` | Степень |
| `vlt::mix(float a, float b, float t)` | `a + (b - a) * t` |
| `vlt::mix(AudioFrame a, AudioFrame b, float t)` | Та же формула для L/R |

Mix сам не ограничивает t: значение вне 0…1 даёт экстраполяцию. Clamp не является защитой от уже возникшего NaN. Проверяй область определения перед sqrt/log/pow и делением. Для модуля, принимающего широкий внешний сигнал, полезны `std::isfinite`, `std::abs`, `std::min`, `std::max` из стандартной библиотеки.

### Random

`vlt::Random` — небольшой генератор с собственным состоянием. `seed(std::uint64_t value)` задаёт последовательность; нулевой seed заменяется единицей. `next()` возвращает float в диапазоне `[0, 1)`. Для биполярного значения используй `next() * 2.0f - 1.0f`.

Инициализируй Random один раз в prepare. Переустановка одинакового seed на каждом кадре будет повторять одно и то же число. Каждый вызов next продвигает генератор. В SeededNoise два вызова дают разные значения каналам. Reset хоста восстанавливает подготовленное состояние генератора.

### Function и служебные определения

`vlt::Function<R(Args...)>` — передаваемый по значению вызываемый объект с `operator()(Args...)`. В сигнатуре указываются только обычные сигнальные аргументы вызываемой функции и её результат. State, Context и собственные Function-зависимости дочерней ноды в этот список не включаются.

Объект заполняется хостом из янтарного провода. Не конструируй его самостоятельно из сырых указателей и не сохраняй в State: хост создаёт действительные привязки для текущего вызова. Его конструктор по умолчанию создаёт пустой объект, который вызывать нельзя.

В заголовке также видны `FunctionTraits<Signature>::Result`, `FunctionArgument<Index, Signature>::Type`, тип `Function::Thunk` и служебный `vlt::detail::reserve`. Traits используются генерируемой оболочкой для типов; reserve реализует подготовительную арену. Авторскому DSP обычно нужны только Function и Buffer. Не вызывай внутренние экспорты `vlt_*` или `__vlt_*` и не подменяй их определения.

## Несколько функций в одной ноде

В одном исходнике можно определить обычные вспомогательные функции и вызвать их из входной функции. В `LocalHelpers.cpp` входная `process` дважды вызывает локальную `saturate(float, float)` для L/R. Это обычные вызовы C++, они не создают дополнительных портов или карточек.

Сигнатуры локальных helpers не определяют интерфейс ноды. В их локальных вычислениях можно использовать свои структуры и передавать части State по ссылке. Правила безопасной памяти и ограничений исполнения при этом сохраняются. Если helper меняет состояние, количество его вызовов влияет на алгоритм.

Каждый исходник ноды при общей сборке помещается в отдельную область имён. Поэтому у разных нод могут быть собственные State и process. Прямая декларация `extern` или попытка написать имя соседней ноды в C++ не создаёт межнодовую связь. Для связи используются аудиопорты или Function.

## Несколько нод с вызовами Function

### Аудиосвязь и вызов функции

В обычной цепи `Input → Saturate → Main → Output` Saturate получает свои сигнальные входы, выполняется графом и передаёт готовое аудио Main.

При связи Function аудио может идти `Input → Main → Output`, а янтарный выход Saturate соединяется с функциональным входом Main. Теперь Main сам вызывает `saturate(input, drive)` там, где это нужно его алгоритму. Если вызова нет, одно наличие провода звук не меняет.

| Вопрос | Сигнальная обработка | Вызов через Function |
|---|---|---|
| Кто выбирает аргументы | Провода и ручные значения самой ноды | C++ вызывающей функции |
| Кто запускает | Планировщик графа | Строка вызова в коде |
| Частота вызова | Один раз на кадр сигнальной ноды | Сколько раз написал автор |
| Состояние | Собственное состояние сигнальной ноды | Собственное состояние привязки Function |
| Влияют ли обычные входы дочерней карточки | Да | Нет, аргументы приходят из вызова |

### Пример Saturate и вызывающей функции

Открой готовый `examples/projects/CallSaturate.vltcreator` или собери две ноды:

1. В первую импортируй `Saturate.cpp`; Entry function — `saturate`.
2. Во вторую импортируй `CallSaturate.cpp`; Entry function — `process`.
3. Обнови обе ноды.
4. Соедини янтарный выход `saturate()` первой ноды с янтарным входом `saturate` второй.
5. Соедини `Input.out → process.input`, обычный аудиовыход `process.out → Output.in`.
6. Настрой drive на вызывающей ноде либо подключи к нему ручку Interface с диапазоном 0,1–6 и default 1,5.

У вызывающей функции параметр имеет тип:

```cpp
// Фрагмент списка аргументов входной функции.
VLT_PORT("saturate")
vlt::Function<vlt::AudioFrame(vlt::AudioFrame, float)> saturate = {}
```

Первая AudioFrame в шаблоне — результат, AudioFrame и float в скобках — аргументы вызова. Код `saturate(input, drive)` получает готовый AudioFrame. Это янтарная связь Function; зелёный Audio-выход Saturate к функциональному входу не подойдёт.

Самостоятельную сигнальную обработку Saturate можно включить отдельно, подключив её обычные входы и аудиовыход. Она будет иметь отдельное состояние. Если Saturate используется только как Function, её Audio-вход не нужно подключать: его значение поставляет вызывающий код.

### Состояние нескольких использований

Один янтарный выход можно соединить с несколькими функциональными входами. Исходник общий, DSP-состояния разные. Пример `TwoFilters.cpp` подключает **одну и ту же** OnePole к `filter_a` и `filter_b`, затем рассчитывает две независимые фильтрации и смешивает результат.

| Использование | Состояние |
|---|---|
| Два вызова `filterA(...)` через один аргумент в одном кадре | Общее; второй продолжает изменение после первого |
| `filterA(...)` и `filterB(...)` через разные аргументы | Независимое, даже при одном исходном Function-выходе |
| Та же функция у двух разных вызывающих нод | Независимое |
| Та же нода одновременно как сигнальный процессор и как Function | Независимое |
| Копия карточки мини-модуля на другом канале | Независимое |

Поэтому не вызывай один stateful float-фильтр сначала для левого, потом для правого канала, ожидая независимых историй. Используй функцию с AudioFrame и отдельными полями истории L/R либо две функциональные привязки.

Одинаковый sampleTime у двух вызовов не делает их одним вызовом. Для нескольких выходов вызывай функцию один раз, сохраняй структуру результата и затем читай поля. Пример `CallMultiOutput.cpp` показывает именно это.

### Вложенные зависимости

Функция может сама принимать Function. В примере NestedCall родитель вызывает `processor(input, drive)`, processor — это CallSaturate, а её функциональный вход связан с Saturate. Контекст и независимые вложенные состояния готовит хост.

В сигнатуру `Function<...>` родителя входят только обычные сигнальные параметры processor. Её собственный функциональный вход уже соединён в графе и подставляется хостом. Не передавай State, Context или ещё один Function как аргумент такого пользовательского вызова.

Зависимости Function проверяются отдельно от сигнальных проводов. Прямые и косвенные циклы между нодами запрещены. Даже если в коде вызов условный, нельзя соединить A → B → A по Function. Внутри C++ избегай рекурсии и неограниченных циклов: ограничения стека и вычислительного бюджета остановят такой модуль.

### Совместимость сигнатур

Проверяются вид результата — одиночное значение или структура, последовательность типов выходов и последовательность типов обычных входов. State и Context не являются частью вызываемой сигнатуры. Имена аргументов и полей не заменяют проверку смысла алгоритма.

Структура с одним полем отличается от одиночного возврата того же типа. Для структуры результата вызывающая нода объявляет свой соответствующий aggregate-тип: например, Levels с полями AudioFrame, float, bool. Хост переносит поля по порядку. Для читаемости сохраняй одинаковый порядок и имена полей на обеих сторонах. Изменение типов или числа полей требует обновления обеих нод и перепроверки связи.

### Создать функцию

1. Открой редактор исходной ноды и нажми **Create function / Создать функцию**.
2. Укажи уникальное имя функции и сигнальные входы/выходы.
3. Creator создаст дочернюю ноду, добавит в исходник родителя тип Function и аргумент, затем соединит их янтарным проводом.
4. Самостоятельно вставь вызов новой функции в нужное место алгоритма родителя.
5. После правок обнови ноды и скомпилируй модуль.

Создание и соединение — одна операция Undo. Имя функции не должно конфликтовать с уже существующим входом родителя. Сгенерированные `using VltFunction_...` и ID `p_...` допустимы; их не нужно переименовывать для работоспособности.

### Вынести функцию

В `LocalHelpers.cpp` выбери **Extract function / Вынести функцию** и введи `saturate`. Creator переносит её определение в отдельную ноду, объявляет функциональный вход родителя и создаёт связь. Существующие вызовы по имени начинают обращаться к переданному Function. Вся операция отменяется одним Undo.

Вынос работает для самостоятельного helper с уникальным именем. Входную функцию, prepare/reset, перегруженный helper, функцию с собственным State или зависимостями от локальных пользовательских типов и других недоступных объявлений автоматически выносить нельзя. Неудачная операция оставляет исходник прежним и объясняет причину. Для сложного stateful helper создай отдельную ноду вручную с её State и свяжи через Function.

## Внешние ручки и оформление мини-модуля

Выбери Interface на канвасе. Во вкладке Card задаются имя проекта, имя устанавливаемого модуля, тема, фон и общий вид регуляторов. Во вкладках ручек настраиваются имя, единица измерения, минимум, максимум, default, логарифмическая шкала и индивидуальный стиль.

| Настройка | Доступно сейчас |
|---|---|
| Темы карточки | `studio`, `graphite`, `ivory`, `copper` |
| Регуляторы | Ring, Disc, Segments, Fader |
| Внутренние ID стилей | `machined`, `rubber`, `glass`, `fader` соответственно |
| Фон | Цвет или загруженная картинка, встроенная в файл модуля |
| Внешние параметры | От нуля до двух |
| Шкала | Линейная или логарифмическая; у логарифмической minimum > 0 |
| Предпросмотр | Настоящая карточка Channel Strip шириной 100 px |

Численный аргумент C++ сам по себе создаёт вход на ноде, но не внешнюю ручку карточки. Для ручки нужен выход Interface, соединённый с этим входом. Один выход Interface можно развести на несколько нод; это даёт макрорегулятор. При разных физических диапазонах используй Map Range, Multiply/Add или свою функцию преобразования.

Например, для Chorus настрой Amount 0–100, default 35, unit `%`; Rate 0,05–1,5, default 0,25, unit `Hz`, log включён. Соедини соответствующие выходы Interface с `amount` и `rate`. Проценты в данном примере передаются как 35, а не 0,35. В примерах с mix 0–1 передаётся доля, поэтому не меняй диапазон ручки на 0–100 без преобразования.

Регуляторы узла можно менять вертикальным перетаскиванием. Shift даёт точную настройку, двойной щелчок — ввод числа, Ctrl-click — сброс. Высокочастотная модуляция приходит по Number-проводам. Плавность параметров, например времени задержки, автор реализует в State; образец есть в Chorus и Delay.

Не удаляй и не создавай заново внешнюю ручку ради переименования: новая ручка получает новый ID, что меняет адрес автоматизации. Переименовывай существующую. То же касается ID самого модуля: изменение названия сохраняет его связь с установленными экземплярами.

### Режимы

Через меню Mode и настройки режимов можно добавить до восьми именованных вариантов. Добавление копирует текущий граф. Затем активный режим редактируется отдельно; у него могут быть свои ноды, исходники, соединения и алгоритм.

Внешние ручки и оформление общие: одинаковые ID, имена, единицы, диапазоны, default и шкалы во всех режимах. Для режима нет специального строкового аргумента в Context. Выбор режима переключает соответствующее определение графа. Compile проверяет все режимы, поэтому ошибка в неактивном режиме тоже мешает установке.

## Редактирование и сохранение

| Действие | Управление |
|---|---|
| Добавить ноду | Правая кнопка на канвасе, библиотека или Tab |
| Переместить ноду | Перетащить заголовок |
| Соединить | Выход → вход подходящего типа; также выбор источника в правой панели |
| Панорамировать | Средняя кнопка или Space + перетаскивание |
| Масштаб | Ctrl + колесо; Ctrl+0 — 100%; F — уместить граф |
| Копировать и вставить ноды | Ctrl+C / Ctrl+V |
| Дублировать | Ctrl+D |
| Удалить выделение | Delete |
| Сохранить | Ctrl+S |
| Compile | Ctrl+Enter |
| Найти в исходнике | Ctrl+F |
| Подсказки SDK | Ctrl+Space в редакторе |

Таблица соответствует сочетаниям текущей Windows-сборки. Стандартные действия Qt на другой платформе могут использовать системный модификатор. Ввод в редакторе принадлежит редактору: Delete удаляет текст, а не ноду. Панель кода меняет высоту перетаскиванием разделителя. Диагностика содержит строку и столбец; двойной щелчок переводит к проблемному месту.

Creator хранит собственную Undo-историю. Редактирование текста, обновление портов и создание/вынос функций поддерживают Undo/Redo. Это история исходного проекта; откат текста после Compile сам по себе не публикует старый DSP. Для публикации отредактированного состояния нужно Compile.

`.vltcreator` сохраняется в папку, выбранную в диалоге Save. Папка проектов Creator и папка установленных мини-модулей — разные назначения. Можно сохранять незавершённый или временно некорректный исходник. Положение курсора, открытая нода и расположение графа входят в проект. Для изменённого черновика предусмотрено периодическое восстановление примерно раз в пять секунд в данных приложения, раздел Creator; стартовое окно предлагает восстановление, если такой файл есть.

Импорт `.cpp` заменяет исходник выбранной ноды. Экспорт `.cpp` сохраняет её исходник. Такой файл сам по себе не содержит графа, связей Function, внешних ручек или других нод. Автоматического наблюдения за изменениями внешнего `.cpp` в SDK 1 нет; после внешней правки импортируй его заново.

## Обновить ноду и Compile

После **любого изменения кода**, даже только тела функции, нажми «Обновить ноду»: проверка актуальности учитывает весь исходник. Это действие анализирует настоящий C++ через Clang, обновляет порты и сохраняет корректную сигнатуру. Оно не устанавливает новый звук в Channel Strip.

Compile сначала сохраняет проект Creator, проверяет графы и актуальность функций, собирает Wasm, проверяет его ограничения, подготавливает локальный AOT и новый DSP. После успешной подготовки `.vltmini` атомарно устанавливается в:

```text
<системная папка Documents>/VLTONE/MiniModules
```

На Windows Documents может быть перенесена или находиться под OneDrive; приложение использует системный путь, а не жёстко заданную строку `C:\Users\...\Documents`.

Повторная сборка того же ID обновляет соответствующие карточки текущего открытого проекта, включая Master. Сохраняются ID экземпляров, совпадающие внешние параметры и их автоматизация, bypass и расположение до/после Audio FX. Изменение DSP инвалидирует затронутый Freeze. В проекте это одна операция Undo; файл в библиотеке и история исходников Creator имеют отдельный жизненный цикл.

При одинаковой задержке подготовленные экземпляры плавно переключаются; при изменении задержки хост согласует переключение и компенсацию. Если идёт экспорт или Freeze, применение результата ждёт его завершения. Ошибка кода, подготовки или установки оставляет прежний установленный файл и работающий DSP. Отмена и результат устаревшей фоновой сборки не публикуются.

В текущей реализации мини-модули предназначены для локальной работы. Обновление модулей в совместных проектах пока не включено. Наличие обычного моста приложения в локальном проекте само по себе не должно блокировать Compile.

## Переносимость и структура файлов

| Файл | Что переносит |
|---|---|
| `.cpp` | Один исходник; может содержать entry, State и helpers |
| `.vltcreator` | Исходный проект со всеми режимами, нодами, кодом, позициями, черновиками |
| `.vltmini` | Устанавливаемый модуль: графы, оформление, исходники, сигнатуры, SDK/ABI и переносимый Wasm |
| Локальный AOT-кэш | Машинный код для конкретной платформы, создаваемый приложением |

Поделиться готовым эффектом можно одним `.vltmini`. Получатель копирует файл в свою папку MiniModules или импортирует его из меню рэка. Картинка фона встроена в файл. Если получателю нужно продолжать редактирование с исходным расположением графа, передай также `.vltcreator`.

Копирование файла в библиотеку делает его доступным для выбора. Не следует считать это командой заменить алгоритмы во всех сохранённых проектах: закрытый проект хранит собственное встроенное определение модуля. Автоматическое обновление работающих карточек описано для успешного Compile в текущем проекте.

Нативные сборки из чужого файла не исполняются. AOT создаётся локально из проверяемого Wasm; ключ кэша зависит от содержимого кода, зависимостей, версии SDK/ABI, инструментария и платформы. Исходник должен быть переносимым C++ без Windows API, Objective-C, платформенных DLL и произвольных локальных include.

В `.vltcreator` и `.vltmini` используется JSON. Верхний уровень содержит `format`, `version`, `definition`. У проекта дополнительно есть имя, активный режим, позиции, viewport и состояние редактора. В definition находятся стабильный ID, имя, nodes, connections, controls, modes, appearance и code. У C++-ноды сохраняются исходник, entry, SDK, проанализированная сигнатура, аннотации, признаки State/Context/prepare/reset. Соединение адресует ID исходной/целевой ноды и ID обоих портов.

Для авторства используй Creator: он согласует хеши исходников и артефакта. Ручное редактирование JSON не заменяет «Обновить ноду» и Compile. Не придумывай значения analyzedHash/sourceHash и не переносись на новую версию формата простой заменой числа. Полные корректные файлы находятся среди готовых примеров.

## Каталог встроенных нод

Это ноды текущего реестра версии 1. Они соединяются с C++ теми же Audio/Number/Gate-проводами. Параметры с численным входом могут модулироваться; дискретные структурные настройки остаются полями ноды без модулирующего порта. Полный список параметров конкретного встроенного эффекта показывает Creator.

### Routing

| Нода и ID | Назначение |
|---|---|
| Input `input` | Аудиовход модуля, выход `out` |
| Output `output` | Аудиовыход модуля, вход `in` |
| Interface `interface` | Number-выход для каждой внешней ручки с её стабильным ID |
| Gain `gain` | Audio `in`, Number `gain` 0–2, Audio `out` |
| Mix `mix` | Два Audio-входа `a`, `b`, Number `mix` 0–1, Audio `out` |
| Number to Audio `number_to_audio` | Number `value` превращается в аудиосигнал |

В графе должен быть ровно один Input и один Output, Interface — не более одного. В созданном через окно проекте эти базовые ноды уже присутствуют. Даже у генератора Input остаётся в проекте, хотя может не использоваться в вычислениях.

### Math

| Нода | ID | Входы и смысл |
|---|---|---|
| Constant | `constant` | `value`, постоянное число или переданный поток |
| Add | `add` | `a + b` |
| Subtract | `subtract` | `a - b` |
| Multiply | `multiply` | `a * b` |
| Divide | `divide` | `a / b`; при нулевом делителе встроенная нода выдаёт 0 |
| Minimum | `min` | Меньшее из `a`, `b` |
| Maximum | `max` | Большее из `a`, `b` |
| Power | `power` | Возведение `a` в степень `b` |
| Absolute | `abs` | Модуль `value` |
| Negate | `negate` | Изменение знака `value` |
| Square root | `sqrt` | Квадратный корень `value` |
| Sine | `sin` | Синус `value` |
| Cosine | `cos` | Косинус `value` |
| Clamp | `clamp` | `value` между `min` и `max` |
| Map Range | `map_range` | Преобразование `value` из `in_min…in_max` в `out_min…out_max` |

Эти порты — Number. Чтобы вычислять по амплитуде аудио, сначала используй Audio to Number либо Envelope Follower. Защитное поведение встроенного Divide не распространяется на обычное `/` в C++: автор проверяет делитель сам.

### Logic и Modulation

| Нода и ID | Назначение и основные настройки |
|---|---|
| Compare `compare` | Сравнить `a`, `b`, получить Gate; `>`, `<`, `=`, `>=`, `<=` |
| Select `select` | Gate выбирает Number A или B; true выбирает B |
| Smooth `smooth` | Сглаживание Number; время 0,1–10000 мс |
| Sample & Hold `sample_hold` | Захват Number по фронту Gate false → true |
| LFO `lfo` | Rate 0,01–40 Гц, amount, offset, форма, синхронизация с темпом, reset Gate |
| Random `random` | Частота случайных значений 0,01–100 Гц, сглаживание, seed, reset Gate |

Gate — состояние, не MIDI-событие. Если нужно действие только один раз при включении Gate, храни предыдущий bool и проверяй фронт. В собственном ControlLFO показана проверка `restart && !previousReset`.

### Analysis и Generators

| Нода и ID | Назначение |
|---|---|
| Envelope Follower `envelope` | Audio → Number-огибающая; Peak/RMS, attack 0,1–1000 мс, release 1–5000 мс |
| Audio to Number `audio_to_number` | Текущее аудиозначение: полусумма L/R, левый или правый канал |
| Oscillator `oscillator` | Audio-генератор; частота 1–20000 Гц, уровень 0–1, форма, reset Gate |
| White Noise `noise` | Audio-шум, уровень 0–1 и seed |

### Effects и Code

| Нода | ID | Назначение |
|---|---|---|
| Color | `color` | Аналоговая окраска |
| Doubler | `doubler` | Удвоение и стереорасширение |
| Chorus | `chorus` | Хорус |
| Flanger | `flanger` | Флэнжер |
| Phaser | `phaser` | Фэйзер |
| Delay | `delay` | Задержка |
| Compressor | `compressor` | Компрессия |
| EQ Band | `eq_band` | Одна полоса EQ: тип, частота, gain и Q |
| Reverb | `reverb` | Реверберация: mix, decay 0,1–12 с, damping, Room/Hall |
| C++ Function | `cpp_function` | Динамические порты из C++-сигнатуры |

Все перечисленные эффекты принимают Audio и возвращают Audio. Список их внутренних настроек шире двух регуляторов готовой карточки: лимит двух действует только на внешние контролы модуля. Можно собрать собственный алгоритм из встроенных эффектов, а C++ использовать для управления ими. Например: Envelope Follower → C++-преобразование Number → cutoff EQ Band.

## Ограничения исполнения и качество DSP

| Ограничение | Значение текущей реализации |
|---|---|
| Карточек на Channel Strip | 3 |
| Развёрнутых нод в графе v5 | 512; C++-слотов отдельно максимум 64 |
| Соединений в одном графе | 128, включая функциональные |
| Внешних регуляторов | 2 |
| Режимов | 8 |
| Входов C++-ноды | До 16 обычных и функциональных вместе |
| Выходов C++-ноды | 1–16 сигнальных; отдельный Function-выход добавляет хост |
| Исходник одной функции-ноды | До 256 KiB UTF-8 |
| Переносимый Wasm | До 8 MiB |
| `.vltcreator` или `.vltmini` | До 32 MiB |
| Встроенная картинка | PNG data URI до 1 MiB после преобразования |
| Память Wasm | До 16 MiB на экземпляр кода |
| Линейный стек, задаваемый линкером | 64 KiB; избегай больших локальных объектов |
| Развёрнутые функциональные зависимости | До 512 экземпляров, включая корень, для одного дерева вызовов |
| Заявленная latency / tail | До 30 / 120 секунд соответственно |

Эти пределы — границы формата и защиты, а не обещание приемлемой загрузки CPU при заполнении каждого максимума.

Код компилируется без исключений и RTTI. Системные вызовы, файлы, сеть, пользовательские потоки, нативные библиотеки и произвольные внешние импорты не разрешены. Include ищется в поставляемых SDK, sysroot и ресурсах Clang; рядом лежащий `myhelper.h` автоматически доступным не становится. Локальные helpers помещай в тот же исходник или в отдельную Function-ноду.

Перед AOT WebAssembly проверяется и получает счётчик вычислений через Binaryen. В текущей реализации счётчик учитывает стоимость тел функций и циклов, пополняясь для обрабатываемой ноды/блока до `max(100000, frames * 20000)` единиц. Это не счётчик процессорных тактов и не запас, который следует специально вырабатывать. Изменение memory/table через grow из пользовательского кода запрещено; подготовительное выделение выполняет хост.

Бесконечный цикл, исчерпание бюджета, ошибка памяти/стека или нечисловой выход приводят к ошибке модуля. Хост передаёт диагностику вне аудиопотока; offline export сообщает об ошибке вместо успешного сохранения подменённого результата.

Для устойчивого DSP соблюдай следующие правила:

1. Считай один кадр за вызов; не запускай внутри process цикл по воображаемому размеру блока.
2. Получай частоту из Context и готовь память под реальные sampleRate/максимальные параметры.
3. Храни независимые истории L/R и не смешивай их случайно через одно состояние scalar-helper.
4. Защищай деление, индексы, диапазоны частот и feedback. Проверяй конечность внешних вычислений до использования в индексах.
5. Сглаживай параметры, если их скачок создаёт щелчок. Изменение времени delay может менять высоту тона — сглаживание не устраняет саму физику эффекта.
6. Не выделяй память, не форматируй строки, не делай I/O, ожидания и блокировки в process/reset.
7. Тяжёлые постоянные вычисления выноси в prepare. Зависящие от параметров коэффициенты можно кэшировать в State и пересчитывать при изменении входа.
8. Локальную обратную связь реализуй через State и задержку. Сигнальный провод по кругу недопустим.
9. Не считай tanh автоматически качественной моделью аналогового прибора. Нелинейная обработка может давать алиасинг; oversampling и его фильтры автор проектирует отдельно либо использует встроенный Color.
10. Проверяй уровень при сравнении dry/wet и объявляй алгоритмическую задержку и хвост. Сильное изменение громкости затрудняет оценку качества.

Автоматизация доставляет текущие значения, но хост не придумывает единицы измерения и характер сглаживания авторского алгоритма. Ограничения и плавность должны быть понятны из кода и таблицы параметров.

## Диагностика типичных ошибок

| Сообщение или симптом | Причина и действие |
|---|---|
| `Entry function not found` | Поле Entry function не совпадает с именем свободной функции |
| `The entry function cannot be overloaded` | Несколько определений с именем выбранной entry |
| `The entry must be a non-template free function` | Entry находится в namespace/классе или является шаблоном; вынеси её в глобальную область исходника |
| `Signal ports are passed by value` | Убери `&` или `*` из сигнального аргумента |
| `Ports support float, bool, vlt::AudioFrame...` | Неподдерживаемый тип порта или запрещённый ID |
| `Return float, bool, AudioFrame or a struct...` | Неподдерживаемый возвращаемый тип, например void |
| `State must have a trivial destructor...` | Замени владение динамическими контейнерами на Buffer и простые поля |
| `Mutable global/static variables are not supported` | Перенеси память в State |
| `prepare/reset require a State argument` | Добавь State& у entry или убери ненужные lifecycle-функции |
| `Update C++ function ports` / `Function ports changed` | После правки исходника снова нажми «Обновить ноду» |
| `Connect required input` | Подключи обязательный Audio/Function-вход используемой ноды |
| `Input already connected` | Удали второй источник входа; используй Mix/математическую ноду |
| `Unknown port` | В черновике остался провод к удалённому или переименованному без сохранения ID порту |
| `Incompatible port types` / `Incompatible function binding` | Проверь Audio/Number/Gate и полную Function-сигнатуру |
| `Graph contains a feedback cycle` | Разорви цикл сигнальных проводов; память обратной связи помести в алгоритм |
| `Function dependencies contain a recursive cycle` | Убери рекурсивную зависимость нод |
| `allocation outside prepare` | Buffer.prepare вызван во время process/reset |
| `memory limit exceeded` | Сократи буферы, число функциональных использований или максимальную задержку |
| `C++ latency or tail exceeds its limit` | Проверь единицы: нужны сэмплы, а также соблюдение пределов |
| `C++ function produced NaN or infinity` | Проверь деление, pow/log/sqrt, нестабильную обратную связь и входные данные |
| Ошибка bounds/stack/budget | Проверь индексы, локальные массивы, циклы, рекурсию и сложность на кадр |
| `System calls and external imports are not permitted` | Удали файловый/сетевой/системный API или зависимость от нативной библиотеки |
| `Creator compiler is missing` | Проверь комплектность установки приложения и CreatorTools |
| `Project is not available for a local module update` | Для текущей сборки проверь локальный режим и отсутствие недопустимого обновления; старые stage могли ошибочно блокировать локальный проект |
| Провода правильные, но звук прежний | Убедись, что Output подключён к результату нужной ноды, выполнен Compile, в рэке выбран нужный модуль и bypass выключен |
| Ручка карточки не влияет | Свяжи Interface с параметром; проверь диапазон, единицы, код и активный режим |
| Helper подключён, но не работает | Добавь его вызов в C++; янтарный провод сам ничего не вычисляет для родителя |

Если компиляция не проходит, сначала исправь первую диагностическую ошибку: остальные часто следуют из неё. Не меняй стабильные ID, формат файла и ABI ради обхода диагностики.

## Проверенные примеры

Готовые проекты `examples/projects/*.vltcreator` уже содержат провода и настройки Interface. Соответствующие `.vltmini` можно установить вручную. Исходники `.cpp` входят в приложение этого справочника, чтобы документ оставался самодостаточным для нейросети.

| Проект | Чему учит | Ручки |
|---|---|---|
| Gain | Одна функция без состояния | Gain 0–2, default 1 |
| LocalHelpers | Локальный helper и его вынос | Drive 0,1–6 / Mix 0–1 |
| OnePole | Фильтр с независимой памятью L/R | Cutoff 20–20000 Гц |
| MultiOutput | Audio, Number и Gate за один вызов | Threshold 0–1 |
| Delay | Buffer, prepare/reset, интерполяция, tail | Time 1–500 мс / Mix 0–1 |
| Chorus | Модулируемая стереозадержка и сглаживание | Amount 0–100 / Rate 0,05–1,5 Гц |
| ControlLFO | Number-выход, Gate-фронт; готовый проект модулирует Gain | Rate 0,01–20 Гц / Depth 0–1 |
| SeededNoise | Детерминированный Random в State | Level 0–0,2 |
| CallSaturate | Две C++-ноды, янтарная связь и вызов | Drive 0,1–6 |
| TwoFilters | Один Function-источник, две независимые истории | Cutoff 20–5000 Гц / Mix 0–1 |
| NestedCall | Три ноды с вложенным вызовом | Drive 0,1–6 / Mix 0–1 |
| CallMultiOutput | Возвращаемая структура через Function | Threshold 0–1 |

Saturate.cpp — отдельный исходник дочерней ноды в CallSaturate и NestedCall. В комплекте 13 исходников для 12 готовых модулей. Chorus использует подготовленный буфер и объявленный хвост. Основная аудиофункция у него одна, а prepare/reset обслуживают её состояние.

Примеры служат проверяемыми образцами использования API. Простые saturate/filter-алгоритмы показывают механику SDK; они не заявлены как замена более сложным встроенным моделям Color, эквалайзеру или профессиональному mastering-процессору.

### Выполненная проверка и её пределы

На Windows 4 октября 2026 года каждый из 12 проектов прошёл Clang-анализ, сборку в WebAssembly, проверку графа, локальную подготовку WAMR и обработку стерео при 48 кГц с конечным выходом. Отдельно прошла штатная команда Extract function для `LocalHelpers.saturate`. Это проверка работоспособности API; она не заменяет прослушивание и измерение качества конкретного эффекта.

Результаты находятся в `examples/projects/verification.json`. Перенос на macOS этими локальными проверками не измерялся. Примеры используют переносимый SDK, но совместимость поставки и конкретной платформы следует проверять запуском на этой платформе.

Для повторной проверки в рабочем репозитории:

```text
python creator/sdk/verify_examples.py --tools build-windows/stage/bin/CreatorTools --validator build-windows/bin/creator_graph_test.exe
```

Python нужен только этому вспомогательному скрипту разработчика. Создание модулей через окно Creator его не требует. Скрипт собирает примеры в `examples/projects`; в пользовательскую библиотеку он их не устанавливает. Аргумент `--out` задаёт другую папку результата. Без `--validator` выполняется разбор и сборка, а запуск DSP помечается как не выполненный.

Для своего готового алгоритма дополнительно проверь mono/stereo, 44,1/48/96/192 кГц, размеры блока, крайние параметры, автоматизацию внутри блока, reset, сохранение проекта и обновление существующего экземпляра. Для эффектов с задержкой проверь параллельную ветвь и хвост экспорта. Для связей Function проверь повторные вызовы, несколько вызывающих нод и независимость состояний.

## Как давать задание нейросети

Приложи этот справочник и опиши эффект, диапазоны ручек и желаемую схему нод. Для короткого контекста используй AI_GUIDE_RU.md вместе с заголовком `vlt/creator.hpp` и подходящим проверенным примером.

Проси вернуть полный исходник каждой ноды, имя Entry function, таблицу портов, отдельную таблицу связей и настройки двух внешних ручек. Для нескольких файлов обязательно укажи, какая связь Audio, а какая Function. Проси перечислить используемую память, latency/tail и способ сброса состояния.

Если нейросети не хватает API, она должна предложить реализацию через доступные примитивы либо явно назвать ограничение. Она не должна придумывать заголовки, типы, классы эффектов или обещать, что код скомпилирован без фактической проверки. После получения кода выполни «Обновить ноду» для каждой ноды, затем Compile и проверку звука.

## Источники контракта и сопровождение

| Реализация | Что определяет |
|---|---|
| [vlt/creator.hpp](vlt/creator.hpp) | Полный открытый C++ API |
| [FunctionAnalysis.cpp](../FunctionAnalysis.cpp) | Разбор сигнатур, аннотации портов, создание и вынос функций |
| [CodeGeneration.cpp](../CodeGeneration.cpp) | Оболочка блока, вызовы, отдельные состояния, prepare/reset |
| [Instrumentation.cpp](../Instrumentation.cpp) | Проверка Wasm и вычислительный бюджет |
| [WasmProgram.cpp](../../plugins/Creator/WasmProgram.cpp) | Память, AOT, reset, время и ошибки исполнения |
| [FunctionDefinition.hpp](../../plugins/Creator/FunctionDefinition.hpp) | Версия SDK и ограничения функций |
| [MiniNodeRegistry.cpp](../../plugins/Internal/MiniNodeRegistry.cpp) | Каталог нод и проверка графа |
| [CreatorWindow.cpp](../../app/CreatorWindow.cpp) | UI, операции над кодом и атомарная установка |
| [CreatorProject.cpp](../../app/CreatorProject.cpp) | Исходный проект, режимы и состояние редактора |
| [MiniModuleLibrary.cpp](../../app/MiniModuleLibrary.cpp) | Папка установки и формат `.vltmini` |

При изменении SDK обновляй версии и правила этого справочника, затем повторяй проверку примеров. Исходный заголовок и реализация установленной версии остаются основанием для совместимости. Ссылки на реализацию работают внутри репозитория; автономному читателю достаточно встроенного ниже заголовка, примеров и правил.

<!-- SDK_REFERENCE_APPENDICES -->

## Приложение A Полный заголовок SDK

Это точная текстовая копия `vlt/creator.hpp` из данного комплекта. При создании ноды подключай заголовок через `#include <vlt/creator.hpp>`; саму реализацию SDK в исходник ноды переносить не требуется.

```cpp
#pragma once
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#define VLT_NODE __attribute__((annotate("vlt.node")))
#define VLT_PORT(id) __attribute__((annotate("vlt.port:" id)))

namespace vlt {
struct AudioFrame {
  float left = 0, right = 0;
};
inline AudioFrame operator*(AudioFrame a, float b) {
  return {a.left * b, a.right * b};
}
inline AudioFrame operator+(AudioFrame a, AudioFrame b) {
  return {a.left + b.left, a.right + b.right};
}
inline AudioFrame operator-(AudioFrame a, AudioFrame b) {
  return {a.left - b.left, a.right - b.right};
}
struct Context {
  double sampleRate = 48000, tempo = 120, beat = 0;
  std::int64_t sampleTime = 0;
  std::uint64_t seed = 0;
  unsigned channels = 2;
  bool playing = false;
};
struct PrepareContext {
  double sampleRate = 48000;
  unsigned channels = 2, maxBlockSize = 512;
  std::uint64_t seed = 0;
  unsigned latency = 0, tail = 0;
};
template <class Signature> class Function;
template <class Signature> struct FunctionTraits;
template <class R, class... A> struct FunctionTraits<R(A...)> {
  using Result = R;
};
template <class R, class... A>
struct FunctionTraits<R(A...) noexcept> : FunctionTraits<R(A...)> {};
template <unsigned Index, class Signature> struct FunctionArgument;
template <unsigned Index, class R, class A, class... Rest>
struct FunctionArgument<Index, R(A, Rest...)>
    : FunctionArgument<Index - 1, R(Rest...)> {};
template <class R, class A, class... Rest>
struct FunctionArgument<0, R(A, Rest...)> {
  using Type = A;
};
template <unsigned Index, class R, class... A>
struct FunctionArgument<Index, R(A...) noexcept>
    : FunctionArgument<Index, R(A...)> {};
template <class R, class... A> class Function<R(A...)> {
public:
  using Result = R;
  using Thunk = R (*)(void *, const Context &, A...);
  Function() = default;
  Function(void *state, const Context *context, Thunk thunk)
      : state_(state), context_(context), thunk_(thunk) {}
  R operator()(A... args) const { return thunk_(state_, *context_, args...); }

private:
  void *state_ = nullptr;
  const Context *context_ = nullptr;
  Thunk thunk_ = nullptr;
};
namespace detail {
extern "C"
    __attribute__((import_module("vlt"), import_name("reserve"))) unsigned
    reserve(unsigned bytes, unsigned alignment);
}
// Storage comes from the instance's preparation arena. reserve traps during
// DSP.
template <class T> class Buffer {
public:
  void prepare(unsigned count) {
    if (count > 16 * 1024 * 1024 / sizeof(T))
      __builtin_trap();
    data_ =
        reinterpret_cast<T *>(detail::reserve(count * sizeof(T), alignof(T)));
    count_ = count;
    clear();
  }
  unsigned size() const { return count_; }
  void clear() {
    for (unsigned i = 0; i < count_; ++i)
      data_[i] = T{};
  }
  T &operator[](unsigned i) {
    if (i >= count_)
      __builtin_trap();
    return data_[i];
  }
  const T &operator[](unsigned i) const {
    if (i >= count_)
      __builtin_trap();
    return data_[i];
  }

private:
  T *data_ = nullptr;
  unsigned count_ = 0;
};
inline float clamp(float x, float lo, float hi) {
  return std::clamp(x, lo, hi);
}
inline float tanh(float x) { return std::tanh(x); }
inline AudioFrame tanh(AudioFrame x) { return {tanh(x.left), tanh(x.right)}; }
inline float sin(float x) { return std::sin(x); }
inline float cos(float x) { return std::cos(x); }
inline float exp(float x) { return std::exp(x); }
inline float pow(float x, float y) { return std::pow(x, y); }
inline float mix(float a, float b, float t) { return a + (b - a) * t; }
inline AudioFrame mix(AudioFrame a, AudioFrame b, float t) {
  return a + (b - a) * t;
}
class Random {
public:
  void seed(std::uint64_t value) { value_ = value ? value : 1; }
  float next() {
    value_ ^= value_ << 13;
    value_ ^= value_ >> 7;
    value_ ^= value_ << 17;
    return float(value_ >> 40) * (1.f / 16777216.f);
  }

private:
  std::uint64_t value_ = 1;
};
} // namespace vlt
```

## Приложение B Полные исходники примеров

Каждый следующий блок — полный исходник одной C++-ноды. Для самостоятельных эффектов Entry function равна `process`; в Saturate — `saturate`. Ноды с Function-входами требуют янтарных связей, описанных в руководстве и сохранённых в готовых проектах.

### Пример Gain

Файл: `examples/Gain.cpp`.

```cpp
#include <vlt/creator.hpp>

VLT_NODE vlt::AudioFrame process(
    VLT_PORT("input") vlt::AudioFrame input,
    VLT_PORT("gain") float gain = 1.0f)
{
    return input * vlt::clamp(gain, 0.0f, 2.0f);
}
```

### Пример LocalHelpers

Файл: `examples/LocalHelpers.cpp`.

```cpp
#include <vlt/creator.hpp>

// This helper can also be moved with Extract function > saturate.
float saturate(float sample, float drive)
{
    drive = vlt::clamp(drive, 0.1f, 6.0f);
    return vlt::tanh(sample * drive) / vlt::tanh(drive);
}

VLT_NODE vlt::AudioFrame process(
    VLT_PORT("input") vlt::AudioFrame input,
    VLT_PORT("drive") float drive = 1.5f,
    VLT_PORT("mix") float mix = 0.3f)
{
    const vlt::AudioFrame wet{
        saturate(input.left, drive), saturate(input.right, drive)};
    return vlt::mix(input, wet, vlt::clamp(mix, 0.0f, 1.0f));
}
```

### Пример OnePole

Файл: `examples/OnePole.cpp`.

```cpp
#include <vlt/creator.hpp>

struct State {
    vlt::AudioFrame history{};
};

void reset(State& state)
{
    state.history = {};
}

VLT_NODE vlt::AudioFrame process(
    State& state, const vlt::Context& context,
    VLT_PORT("input") vlt::AudioFrame input,
    VLT_PORT("cutoff") float cutoff = 1500.0f)
{
    cutoff = vlt::clamp(cutoff, 20.0f, float(context.sampleRate * 0.45));
    const float alpha = 1.0f - vlt::exp(
        -6.28318530718f * cutoff / float(context.sampleRate));
    state.history = state.history + (input - state.history) * alpha;
    return state.history;
}
```

### Пример MultiOutput

Файл: `examples/MultiOutput.cpp`.

```cpp
#include <vlt/creator.hpp>

struct State {
    float envelope = 0.0f;
};

struct Outputs {
    VLT_PORT("audio") vlt::AudioFrame audio;
    VLT_PORT("envelope") float envelope;
    VLT_PORT("gate") bool gate;
};

VLT_NODE Outputs process(
    State& state, const vlt::Context& context,
    VLT_PORT("input") vlt::AudioFrame input,
    VLT_PORT("threshold") float threshold = 0.1f)
{
    const float peak = std::max(std::abs(input.left), std::abs(input.right));
    const float seconds = peak > state.envelope ? 0.005f : 0.1f;
    const float alpha = 1.0f - vlt::exp(
        -1.0f / (seconds * float(context.sampleRate)));
    state.envelope += alpha * (peak - state.envelope);
    return {input, state.envelope,
            state.envelope > vlt::clamp(threshold, 0.0f, 1.0f)};
}
```

### Пример Delay

Файл: `examples/Delay.cpp`.

```cpp
#include <vlt/creator.hpp>

struct State {
    vlt::Buffer<vlt::AudioFrame> delay;
    unsigned write = 0;
    float smoothing = 0.0f;
    float milliseconds = 120.0f;
    float mix = 0.25f;
};

void prepare(State& state, vlt::PrepareContext& context)
{
    state.delay.prepare(unsigned(std::ceil(context.sampleRate * 0.5)) + 2);
    state.smoothing = 1.0f / float(context.sampleRate * 0.02 + 1.0);
    context.latency = 0; // The dry path is immediate.
    context.tail = unsigned(std::ceil(context.sampleRate * 0.5));
}

void reset(State& state)
{
    state.delay.clear();
    state.write = 0;
    state.milliseconds = 120.0f;
    state.mix = 0.25f;
}

VLT_NODE vlt::AudioFrame process(
    State& state, const vlt::Context& context,
    VLT_PORT("input") vlt::AudioFrame input,
    VLT_PORT("time") float timeMs = 120.0f,
    VLT_PORT("mix") float mix = 0.25f)
{
    state.milliseconds += state.smoothing *
        (vlt::clamp(timeMs, 1.0f, 500.0f) - state.milliseconds);
    state.mix += state.smoothing *
        (vlt::clamp(mix, 0.0f, 1.0f) - state.mix);
    const unsigned size = state.delay.size();
    state.delay[state.write] = input;
    const float distance = vlt::clamp(
        state.milliseconds * float(context.sampleRate * 0.001),
        1.0f, float(size - 2));
    float position = float(state.write) - distance;
    if (position < 0.0f) position += float(size);
    const unsigned a = unsigned(position);
    const unsigned b = (a + 1) % size;
    const auto wet = vlt::mix(state.delay[a], state.delay[b],
                              position - float(a));
    state.write = (state.write + 1) % size;
    return vlt::mix(input, wet, state.mix);
}
```

### Пример Chorus

Файл: `examples/Chorus.cpp`.

```cpp
#include <vlt/creator.hpp>

struct State {
    vlt::Buffer<vlt::AudioFrame> delay;
    unsigned write = 0;
    double phase = 0.0;
    float smoothing = 0.0f;
    float amount = 35.0f;
    float rate = 0.25f;
};

void prepare(State& state, vlt::PrepareContext& context)
{
    state.delay.prepare(unsigned(std::ceil(context.sampleRate * 0.022)) + 2);
    state.smoothing = 1.0f / float(context.sampleRate * 0.02 + 1.0);
    context.latency = 0;
    context.tail = unsigned(std::ceil(context.sampleRate * 0.018));
}

void reset(State& state)
{
    state.delay.clear();
    state.write = 0;
    state.phase = 0.0;
    state.amount = 35.0f;
    state.rate = 0.25f;
}

VLT_NODE vlt::AudioFrame process(
    State& state, const vlt::Context& context,
    VLT_PORT("input") vlt::AudioFrame input,
    VLT_PORT("amount") float amount = 35.0f,
    VLT_PORT("rate") float rate = 0.25f)
{
    constexpr double twoPi = 6.283185307179586;
    state.amount += state.smoothing *
        (vlt::clamp(amount, 0.0f, 100.0f) - state.amount);
    state.rate += state.smoothing *
        (vlt::clamp(rate, 0.05f, 1.5f) - state.rate);
    state.delay[state.write] = input;
    const unsigned size = state.delay.size();
    float wet[2]{};
    for (unsigned channel = 0; channel < 2; ++channel) {
        const float lfo = vlt::sin(float(state.phase) +
                                   float(channel) * 1.5707963268f);
        const float distance = vlt::clamp(
            float(context.sampleRate) * (0.014f + 0.004f * lfo),
            1.0f, float(size - 2));
        float position = float(state.write) - distance;
        if (position < 0.0f) position += float(size);
        const unsigned a = unsigned(position), b = (a + 1) % size;
        const float x = channel == 0 ? state.delay[a].left : state.delay[a].right;
        const float y = channel == 0 ? state.delay[b].left : state.delay[b].right;
        wet[channel] = vlt::mix(x, y, position - float(a));
    }
    state.write = (state.write + 1) % size;
    state.phase += twoPi * double(state.rate) / context.sampleRate;
    if (state.phase >= twoPi) state.phase -= twoPi;
    return vlt::mix(input, vlt::AudioFrame{wet[0], wet[1]}, state.amount * 0.005f);
}
```

### Пример ControlLFO

Файл: `examples/ControlLFO.cpp`.

```cpp
#include <vlt/creator.hpp>

struct State {
    double phase = 0.0;
    bool previousReset = false;
};

// Number output: wire it to Gain.gain for a tremolo effect.
VLT_NODE float process(
    State& state, const vlt::Context& context,
    VLT_PORT("rate") float rate = 1.0f,
    VLT_PORT("depth") float depth = 0.5f,
    VLT_PORT("reset") bool restart = false)
{
    constexpr double twoPi = 6.283185307179586;
    if (restart && !state.previousReset) state.phase = 0.0;
    state.previousReset = restart;
    const float wave = 0.5f + 0.5f * vlt::sin(float(state.phase));
    state.phase += twoPi * double(vlt::clamp(rate, 0.01f, 20.0f))
                   / context.sampleRate;
    if (state.phase >= twoPi) state.phase -= twoPi;
    return 1.0f - vlt::clamp(depth, 0.0f, 1.0f) * wave;
}
```

### Пример SeededNoise

Файл: `examples/SeededNoise.cpp`.

```cpp
#include <vlt/creator.hpp>

struct State {
    vlt::Random random;
};

void prepare(State& state, vlt::PrepareContext& context)
{
    state.random.seed(context.seed);
}

VLT_NODE vlt::AudioFrame process(
    State& state, VLT_PORT("level") float level = 0.03f)
{
    level = vlt::clamp(level, 0.0f, 0.2f);
    return {(state.random.next() * 2.0f - 1.0f) * level,
            (state.random.next() * 2.0f - 1.0f) * level};
}
```

### Пример Saturate

Файл: `examples/Saturate.cpp`.

```cpp
#include <vlt/creator.hpp>

VLT_NODE vlt::AudioFrame saturate(
    VLT_PORT("input") vlt::AudioFrame input,
    VLT_PORT("drive") float drive = 1.5f)
{
    drive = vlt::clamp(drive, 0.1f, 6.0f);
    return vlt::tanh(input * drive) * (1.0f / vlt::tanh(drive));
}
```

### Пример CallSaturate

Файл: `examples/CallSaturate.cpp`.

```cpp
#include <vlt/creator.hpp>

VLT_NODE vlt::AudioFrame process(
    VLT_PORT("input") vlt::AudioFrame input,
    VLT_PORT("drive") float drive = 1.5f,
    VLT_PORT("saturate")
        vlt::Function<vlt::AudioFrame(vlt::AudioFrame, float)> saturate = {})
{
    return saturate(input, vlt::clamp(drive, 0.1f, 6.0f));
}
```

### Пример TwoFilters

Файл: `examples/TwoFilters.cpp`.

```cpp
#include <vlt/creator.hpp>

VLT_NODE vlt::AudioFrame process(
    VLT_PORT("input") vlt::AudioFrame input,
    VLT_PORT("cutoff") float cutoff = 500.0f,
    VLT_PORT("mix") float mix = 0.5f,
    VLT_PORT("filter_a")
        vlt::Function<vlt::AudioFrame(vlt::AudioFrame, float)> filterA = {},
    VLT_PORT("filter_b")
        vlt::Function<vlt::AudioFrame(vlt::AudioFrame, float)> filterB = {})
{
    // Connect the SAME OnePole function output to both function inputs.
    // Each input owns independent filter history.
    const auto low = filterA(input, cutoff);
    const auto high = filterB(input, cutoff * 4.0f);
    return vlt::mix(low, high, vlt::clamp(mix, 0.0f, 1.0f));
}
```

### Пример NestedCall

Файл: `examples/NestedCall.cpp`.

```cpp
#include <vlt/creator.hpp>

VLT_NODE vlt::AudioFrame process(
    VLT_PORT("input") vlt::AudioFrame input,
    VLT_PORT("drive") float drive = 1.5f,
    VLT_PORT("mix") float mix = 0.3f,
    VLT_PORT("processor")
        vlt::Function<vlt::AudioFrame(vlt::AudioFrame, float)> processor = {})
{
    // processor may itself depend on another connected Function.
    const auto wet = processor(input, drive);
    return vlt::mix(input, wet, vlt::clamp(mix, 0.0f, 1.0f));
}
```

### Пример CallMultiOutput

Файл: `examples/CallMultiOutput.cpp`.

```cpp
#include <vlt/creator.hpp>

struct Levels {
    vlt::AudioFrame audio;
    float envelope;
    bool gate;
};

VLT_NODE vlt::AudioFrame process(
    VLT_PORT("input") vlt::AudioFrame input,
    VLT_PORT("threshold") float threshold = 0.1f,
    VLT_PORT("analyze")
        vlt::Function<Levels(vlt::AudioFrame, float)> analyze = {})
{
    // One call computes all three fields. Do not call analyze again per field.
    const auto result = analyze(input, threshold);
    return result.audio * (1.0f / (1.0f + result.envelope));
}
```
