C++ в плагине: классы, шаблоны и время жизни

Как в C++ устроены классы, конструкторы, наследование и шаблоны, которые встречаются в коде плагина на JUCE?

Опубликовано 14 мин чтения

В треках:Программист, этап 7

Эта статья продолжает «Основы C++ для JUCE» и разбирает остальную часть языка, которая нужна для чтения плагина (plug-in)плагин (plug-in) — Программный модуль эффекта или инструмента, который загружается в DAW или другую программу-хост и обрабатывает переданные ему аудиоблоки. тремоло (tremolo)тремоло (tremolo) — Эффект периодического изменения амплитуды аудиосигнала. Скорость пульсации задаёт частота управляющего генератора, а диапазон усиления зависит от глубины и способа её преобразования. из официального курса JUCE: классы и структуры, конструкторы, наследование и виртуальные методы, шаблоны, время жизни объектов (object lifetime)время жизни объекта (object lifetime) — Промежуток от окончания конструктора до начала деструктора, в который к объекту можно обращаться. Зависит от того, где объект создан: в блоке, внутри другого объекта или в куче., запрет копирования, неявные преобразования, макросы JUCE и тесты. Каждое понятие показано на фрагменте готовой версии из папки complete (JUCE 8.0.12, C++23); на полях стоят ссылки на уроки Learn C++ и справочник cppreference.

Владение объектами (ownership)владение объектом (ownership) — Ответственность за удаление объекта. Владелец удаляет объект, наблюдатели только пользуются им через указатель или ссылку и должны перестать обращаться к нему до удаления. в куче (heap)куча (heap) — Динамическая память для объектов, созданных выражением new. Такой объект живёт до явного delete или до удаления его владельцем, например умным указателем. разобрано отдельно, в статье «Кто владеет объектом», а правила аудиопотока (audio thread)аудиопоток (audio thread) — Поток с повышенным приоритетом, который вызывает обработку звука для каждого блока. Каждый вызов должен закончиться до срока блока, поэтому в нём не выделяют память, не берут блокировки и не обращаются к диску. — в «C++ в аудиопотоке»; здесь они упомянуты кратко.

Классы и структуры

Класс (class) и структура (struct) в C++ различаются одним: у class члены по умолчанию закрыты (private), у struct открыты (public). По соглашению struct берут для простых наборов данных, а class — для объектов с поведением и инвариантами. Точка с запятой после закрывающей скобки обязательна. Методы объявляются внутри класса, а все поля описываются заранее вместе с типами.

Parameters — структура из трёх ссылок на параметры, а BypassTransitionSmoother — класс с закрытым состоянием. Доступ задают метки public, protected и private; метка действует до следующей. private разрешает доступ только методам этого класса, protected — ещё и наследникам, public — всем.

C++
class BypassTransitionSmoother {
public:
  void prepare(const juce::dsp::ProcessSpec& spec) { /* ... */ }
  [[nodiscard]] bool isTransitioning() const noexcept { /* ... */ }

private:
  double crossfadeLengthSeconds = 0.0;
  double sampleRateHz = 0.0;
  juce::LinearSmoothedValue<float> dryGain{0.f};
  juce::LinearSmoothedValue<float> wetGain{1.f};
  juce::AudioBuffer<float> dryBuffer;
};

Значения полей по умолчанию

Запись = 0.0 или {0.f} рядом с полем (C++11) задаёт значение, если конструктор не указал другое. Поле, у которого нет ни такой записи, ни инициализации в конструкторе, получает значение по умолчанию своего типа: dryBuffer создаст конструктор по умолчанию класса AudioBuffer. Поле встроенного типа (int, double, указатель) без инициализатора содержит произвольное значение, а не ноль, как в Go.

Константные методы

const после списка параметров обещает, что метод не меняет поля объекта: bool isBypassed() const noexcept. Только такие методы можно вызвать у константного объекта и через const T&. Компилятор проверяет обещание, и попытка изменить поле в const-методе становится ошибкой. Из-за этого prepare(const juce::dsp::ProcessSpec& spec) может читать spec.sampleRate, но не может его менять.

В объявлении const juce::String getName() const override два const. Второй, после списка параметров, делает метод константным. Первый стоит перед типом результата и относится к возвращаемому значению: метод возвращает копию строки, и эту копию нельзя изменить. Для значения, возвращаемого по копии, такой запрет бесполезен: вызывающий код всё равно получает собственный объект. С C++11 он ещё и мешает перемещению, о котором речь ниже. В PluginProcessor этот const написан потому, что так объявлен чисто виртуальный метод в juce::AudioProcessor: virtual const String getName() const = 0;. Переопределение должно возвращать тот же тип, что и метод базы, и убрать const только в наследнике нельзя.

Статические члены и this

Статический метод принадлежит классу, а не объекту, и не имеет this. JsonSerializer — класс без полей, который служит группой функций: JsonSerializer::serialize(parameters, outputStream) вызывается без объекта. Статическое поле одно на весь класс; static constexpr auto modulationDepth = 0.4f; в Tremolo — константа класса, известная при компиляции. Слово static в C++ означает разное в зависимости от места: у члена класса — «принадлежит классу», у локальной переменной — «живёт до конца программы», у функции вне класса — «видна только в этом файле».

Внутри метода поля и методы доступны без префикса: в PluginEditor::resized() вызов getLocalBounds() означает this->getLocalBounds(). Слово this — указатель на текущий объект, его пишут явно только для устранения неоднозначности или чтобы передать объект дальше, как в лямбде [this]. Вложенные типы вроде Tremolo::LfoWaveform объявлены внутри класса и снаружи называются через ::; в наследнике они видны без квалификации: BusesLayout из juce::AudioProcessor используется в PluginProcessor как есть.

Конструкторы и инициализация

Конструктор (constructor) — метод с именем класса и без типа результата; он вызывается при создании объекта. Если конструкторов нет, компилятор создаёт конструктор по умолчанию. Часть после двоеточия и до тела — список инициализации членов (member initializer list). Поля создаются сразу с нужными значениями до выполнения тела. Присваивание в теле сначала создало бы поле со значением по умолчанию, а потом перезаписало.

C++
explicit BypassTransitionSmoother(double crossfadeLengthSecondsValue = 0.01)
    : crossfadeLengthSeconds{crossfadeLengthSecondsValue} {
  jassert(0.0 < crossfadeLengthSeconds);
  reset();
}

Для ссылок, const-полей и классов без конструктора по умолчанию список инициализации — единственный способ. Так написан конструктор Parameters: поля-ссылки rate, bypassed и waveform невозможно создать пустыми. Поля инициализируются в порядке объявления в классе, а не в порядке записи в списке; если порядки расходятся, срабатывает -Wreorder.

Конструктор базового класса тоже вызывается в списке инициализации, как super().__init__(...) в Python. База создаётся раньше полей наследника:

C++
PluginProcessor::PluginProcessor()
    : AudioProcessor(
          BusesProperties()
              .withInput("Input", juce::AudioChannelSet::stereo(), true)
              .withOutput("Output", juce::AudioChannelSet::stereo(), true)) {}

PluginEditor::PluginEditor(PluginProcessor& p) : AudioProcessorEditor(&p), /* ... */ {

Из этого порядка следует, что поле Parameters parameters{*this}; в PluginProcessor создаётся, когда часть AudioProcessor уже построена, и конструктор Parameters может добавлять в неё параметры. Подробнее — в «Кто владеет объектом».

explicit

Неявное преобразование превращает одно значение в объект другого типа, когда тип аргумента не совпадает с ожидаемым. Для этого компилятор ищет у нужного класса конструктор, который можно вызвать с одним аргументом: исходное значение одно, и других данных для создания объекта у него нет. Подходит и конструктор, у которого остальные параметры имеют значения по умолчанию, как у BypassTransitionSmoother(double crossfadeLengthSecondsValue = 0.01).

Если бы этот конструктор не был помечен explicit, следующий код компилировался бы без ошибок. Функция inspect придумана для примера:

C++
void inspect(const BypassTransitionSmoother& smoother);

inspect(0.5);                             // из 0.5 создаётся временный BypassTransitionSmoother
BypassTransitionSmoother smoother = 0.5;  // то же при инициализации через =

Число 0.5 молча превратилось бы в объект с переходом длиной полсекунды, хотя по вызову inspect(0.5) этого не видно. С explicit обе строки — ошибка компиляции, и объект создаётся только явно: inspect(BypassTransitionSmoother{0.5}). Так же объявлены explicit Parameters(juce::AudioProcessor&) и explicit PluginEditor(PluginProcessor&): без explicit процессор можно было бы передать в функцию, которая ждёт const PluginEditor&, и компилятор создал бы для этого вызова временный редактор.

Конструктор с несколькими параметрами тоже может участвовать в неявном преобразовании, но только из списка в фигурных скобках: вызов f({a, b}) создаст объект параметра из a и b. explicit запрещает и этот случай. Обычно его ставят у всех конструкторов, которые можно вызвать с одним аргументом, если неявное преобразование не нужно.

Фигурные скобки

В C++ несколько синтаксисов создания объекта; в коде плагина в основном используются фигурные скобки (C++11): BypassTransitionSmoother testee{1.0};, juce::LinearSmoothedValue<float> dryGain{0.f};, PluginProcessor processor{};. Отличия от круглых скобок: {} запрещает сужающие преобразования (int x{3.7}; не компилируется, а int x(3.7); молча даст 3), а int x{}; гарантирует ноль, тогда как int x; оставляет произвольное значение.

Строка T x(); объявляет функцию x, возвращающую T, а не объект. Это следствие правила грамматики «если запись можно разобрать как объявление функции, выбирается оно», и её называют «most vexing parse» («самый досадный разбор»). Фигурные скобки T x{}; однозначно создают объект.

Простые структуры без конструкторов, например juce::dsp::ProcessSpec, инициализируют по полям. В Tremolo::prepare это записано с именами полей (C++20, designated initializers), а в тесте — списком в порядке полей, который создаёт временный объект параметра:

C++
const juce::dsp::ProcessSpec processSpec{
    .sampleRate = sampleRate,
    .maximumBlockSize = static_cast<juce::uint32>(expectedMaxFramesPerBlock),
    .numChannels = 1u,
};

testee.prepare({sampleRate, blockSize, channelCount});  // в тесте

Во втором вызове тип временного объекта компилятор берёт из сигнатуры метода, поэтому писать ProcessSpec{...} не нужно. Здесь значения sampleRate, blockSize и channelCount — constexpr int, а поля ProcessSpec беззнаковые. Сужение для констант, значение которых известно при компиляции и помещается в тип, разрешено.

Наследование и виртуальные методы

Запись class PluginProcessor : public juce::AudioProcessor — открытое наследование (inheritance): публичные члены базы остаются публичными, а объект наследника можно использовать везде, где ждут базу. Почти всегда пишут именно public; множественного наследования в плагине нет. CustomLookAndFeel наследует juce::LookAndFeel_V4 и переопределяет методы отрисовки; остальное поведение унаследовано.

virtual и override

Хост знает о плагине только как об указателе на juce::AudioProcessor и вызывает processBlock через него. Вызов попадает в версию наследника потому, что в базе этот метод объявлен со словом virtual. В Python, TypeScript и Java все методы виртуальные, в C++ нет: метод без virtual вызывается по статическому типу указателя, и «переопределение» в наследнике через указатель на базу не сработает.

C++
void prepareToPlay(double sampleRate, int expectedMaxFramesPerBlock) override;
void resized() override;

override (C++11) просит компилятор проверить, что метод действительно переопределяет виртуальный метод базы. Опечатка в имени или типе параметра без override создаст новый метод, который никто не вызовет; с override это ошибка компиляции. Тот же override стоит у деструктора: ~PluginEditor() override;.

Многие методы juce::AudioProcessor объявлены как virtual ... = 0; — чисто виртуальные, без реализации. Класс с хотя бы одним таким методом называется абстрактным: создать его объект нельзя, только объект наследника, реализовавшего все такие методы. Поэтому PluginProcessor определяет getName, acceptsMidi, getNumPrograms и другие, даже когда они просто возвращают false или 0.

Сокрытие имён и using

У базы две перегрузки processBlock: для float и для double. Если наследник объявляет метод с этим именем, он скрывает все перегрузки базы, даже с другими параметрами. Строка using AudioProcessor::processBlock; возвращает скрытые перегрузки в область видимости наследника. Без неё компилятор выдаст предупреждение -Woverloaded-virtual.

C++
void processBlock(juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
using AudioProcessor::processBlock;

Шаблоны

Шаблон (template) позволяет один раз описать алгоритм или контейнер для разных типов. По назначению он близок к дженерикам других языков. juce::AudioBuffer<float> — буфер отсчётов (sample)отсчёт (sample) — Значение сигнала в один момент дискретизации, одно число. В звукозаписи его также называют сэмплом. float, AudioBuffer<double> — буфер double; логика класса общая, а тип элементов выбирается при использовании. Компилятор инстанцирует (instantiate) шаблон для каждого набора аргументов, поэтому определение шаблона должно быть видно в месте использования, и шаблонный код обычно лежит в заголовках.

Шаблон принимает и значения: в std::array<juce::dsp::Oscillator<float>, 2u> lfos второй аргумент задаёт число элементов. Плагин объявляет и собственные шаблоны. SampleFifo<SampleType> передаёт отсчёты LFO (low-frequency oscillator, LFO)низкочастотный генератор (low-frequency oscillator, LFO) — Генератор медленно повторяющегося управляющего сигнала, которым изменяют параметры звука, например усиление, частоту или панораму. Общей строгой верхней границы частоты у LFO нет. из аудиопотока в интерфейс, а StridedQueue<T, Size> хранит точки графика:

C++
template <typename SampleType>
class SampleFifo { /* ... */ };

template <typename T, size_t Size>
class StridedQueue { /* ... */ };

Для функций параметры шаблона обычно выводятся из аргументов:

C++
template <typename T>
T twice(T value) {
  return value + value;
}

auto integer = twice(3);          // T выводится как int
auto sample = twice(0.5f);        // T выводится как float
auto precise = twice<double>(3);  // T задан явно, аргумент преобразуется в double

Вариативные шаблоны, CTAD и span

Функция juce::ignoreUnused, о которой шла речь в первой части, написана как вариативный шаблон (C++11): пакет параметров typename... Types принимает любое число аргументов любых типов. Тело у неё пустое:

C++
template <typename... Types>
void ignoreUnused(Types&&...) noexcept {}

С C++17 компилятор выводит и аргументы шаблона класса (class template argument deduction, CTAD). Поэтому в LfoVisualizer.cpp записано std::span{...} без <const float>:

C++
lfoSamplesToPlot.pushBack(std::span{
    buffer.getReadPointer(0), static_cast<size_t>(buffer.getNumSamples())});

std::span (C++20) — «вид» на непрерывный кусок памяти: указатель и длина, без владения данными. Это аналог среза в Go без возможности расти или memoryview в Python. Через него в pushBack передаются указатель на отсчёты канала и их число, а сами отсчёты не копируются.

enum class

enum class (C++11) — перечисление со своей областью видимости: значения пишутся как LfoWaveform::sine, а не просто sine, и неявно в число не превращаются. Старый enum из C++98 выбрасывал имена значений наружу и молча приводился к int. LfoWaveform объявлено внутри Tremolo с явным базовым типом size_t, чтобы значение подходило как индекс массива.

C++
class Tremolo {
public:
  enum class LfoWaveform : size_t {
    sine = 0,
    triangle = 1,
  };
  // ...
};

Переход к числу и обратно делают явно. JUCE предоставляет juce::toUnderlyingType для первого направления, static_cast — для второго:

C++
lfos[juce::toUnderlyingType(currentLfo)].processSample(0.f);
static_cast<Tremolo::LfoWaveform>(parameters.waveform.getIndex())

Рядом в Tremolo.h объявлено и enum class ApplySmoothing { no, yes };. Это приём читаемости: вызов setModulationRateHz(5.f, ApplySmoothing::no) понятнее, чем setModulationRateHz(5.f, false).

Время жизни объектов и RAII

В C++ нет сборщика мусора, объект существует столько, сколько определено правилами языка. Локальная переменная живёт до конца блока { }, и при выходе из него вызывается её деструктор (метод ~ClassName()) в порядке, обратном объявлению. Поле живёт, пока жив объект-владелец. Объект из new живёт, пока его не удалят через delete. Подробно эти три случая разобраны в статье «Кто владеет объектом».

RAII (resource acquisition is initialization)RAII (resource acquisition is initialization) — Приём C++: ресурс принадлежит объекту, а освобождение привязано к деструктору этого объекта, который вызывается при любом выходе из области видимости. — идиома, при которой ресурс захватывается в конструкторе и освобождается в деструкторе. В processBlock плагина её показывает одна строка:

C++
void PluginProcessor::processBlock(juce::AudioBuffer<float>& buffer,
                                   juce::MidiBuffer& midiMessages) {
  juce::ignoreUnused(midiMessages);

  juce::ScopedNoDenormals noDenormals;
  // ...
}

ScopedNoDenormals в конструкторе переключает процессор в режим, где очень малые числа с плавающей точкой (денормализованные) обнуляются: арифметика с ними выполняется заметно медленнее. Деструктор возвращает прежний режим при выходе из processBlock. Переменная noDenormals нигде не используется, её смысл целиком в конструкторе и деструкторе. Освобождение срабатывает при любом выходе из области видимости, включая return и исключение; в Python для этого служит with, в Go — defer. Так же работают std::unique_ptr, блокировки мьютексов и файловые потоки.

Временный объект без имени, например juce::String{"Triangle"} или BusesProperties(), живёт до конца полного выражения, обычно до ;. Сохранять ссылку на него дольше нельзя.

Копирование и запрет копирования

По умолчанию объекты C++ копируются при присваивании, передаче по значению и возврате: компилятор сам создаёт конструктор копирования и оператор присваивания, которые копируют поле за полем. Для некоторых классов копия не имеет смысла: копия процессора плагина или окна редактора — ошибка. Запрет записывается так (C++11):

C++
PluginProcessor(const PluginProcessor&) = delete;
PluginProcessor& operator=(const PluginProcessor&) = delete;

= delete удаляет функцию: любая попытка скопировать объект становится ошибкой компиляции. В C++98 эти функции объявляли private и не определяли, и ошибка появлялась позже, при компоновке.

В PluginProcessor эти две строки пишет макрос JUCE (см. раздел о макросах). В C++11 появилась и семантика перемещения (T&&, std::move): вместо копирования содержимое «забирается» у объекта, который больше не нужен, например у временного. Поэтому возврат большого объекта из функции по значению в современном C++ обычно не копирует данные. std::unique_ptr не копируется, а только перемещается, и в статье «Кто владеет объектом» это объяснено на примере параметров плагина.

Неявные преобразования и перегрузка операторов

Класс может определить собственные версии операторов: operator==, operator!=, operator=, operator[] и других, а также операторы преобразования к другому типу. Перегрузка операторов есть в Python (__eq__, __add__), но нет в Go и TypeScript. Из-за неявных преобразований часть строк плагина выглядит проще, чем работает.

Параметры плагина хранятся как ссылки на объекты JUCE, а не как числа. Эти классы определяют оператор преобразования и перегруженное присваивание:

C++
tremolo.setModulationRateHz(parameters.rate, applySmoothing);  // operator float()
bypassTransitionSmoother.setBypass(parameters.bypassed);       // operator bool()
parameters.rate = 10.f;                                        // operator=(float)

juce::AudioParameterFloat объявляет operator float() const noexcept и operator=(float), juce::AudioParameterBool — operator bool() и operator=(bool). Присваивание числа выглядит обычно, но вызывает метод, который уведомляет хост о смене значения. Поэтому parameters.rate читается как число, хотя это объект.

Другой вид неявного преобразования — конструктор с одним параметром. Метод juce::dsp::AudioBlock<float> getBlock() { return buffer; } в тестах возвращает AudioBlock, хотя buffer — AudioBuffer<float>: у AudioBlock есть конструктор из буфера, и компилятор вызывает его сам. Блок — «вид» на данные буфера, отсчёты не копируются. Так же строковый литерал "None" превращается в juce::String в return "None";.

Макросы JUCE

Макрос (#define) — текстовая подстановка препроцессора. Имена макросов по соглашению пишут заглавными буквами. В классах плагина используются макросы JUCE jassert и семейство JUCE_DECLARE_NON_COPYABLE.

jassert

Макрос jassert(условие) проверяет инвариант. В отладочной сборке при ложном условии срабатывает точка останова в отладчике на этой строке. В релизной сборке макрос превращается в пустую инструкцию, условие даже не вычисляется, поэтому внутри jassert нельзя писать код с побочными эффектами. Это определено в juce_PlatformDefs.h. Аналог стандартного assert из <cassert>.

C++
jassert(0.0 < crossfadeLengthSeconds);
jassert(buffer.getNumSamples() <= dryBuffer.getNumSamples());

Запрет копирования и детектор утечек

Макрос JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Class) раскрывается в две удалённые функции, Class(const Class&) = delete; и Class& operator=(const Class&) = delete; (их даёт макрос JUCE_DECLARE_NON_COPYABLE), и в JUCE_LEAK_DETECTOR(Class). Второй добавляет счётчик живых объектов, который сообщает об утечке при завершении программы. Макрос ставят в конец класса: им пользуются PluginProcessor и PluginEditor, а Parameters — парой JUCE_DECLARE_NON_COPYABLE(Parameters) и JUCE_DECLARE_NON_MOVEABLE(Parameters), которая запрещает и перемещение. О детекторе утечек подробнее в статье «C++ в аудиопотоке».

Макрос JUCE_CALLTYPE, который задаёт соглашение о вызове, разобран в первой части.

Тесты на GoogleTest

Тесты плагина написаны на GoogleTest и лежат в папке test/. TEST(Группа, Имя) раскрывается в определение класса-наследника testing::Test и его метода, тело которого вы пишете в фигурных скобках. TEST_F делает то же, но наследуется от вашего класса-фикстуры (fixture), как BypassTransitionSmootherTest: перед каждым тестом создаётся новый объект фикстуры и вызывается её SetUp(). Аналог — setUp в unittest Python или beforeEach в Jest.

C++
class BypassTransitionSmootherTest : public testing::Test {
protected:
  void SetUp() override { /* подготовка */ }

  BypassTransitionSmoother testee{1.0};
  juce::AudioBuffer<float> buffer;
};

TEST_F(BypassTransitionSmootherTest, OffOnTransitionIsSmooth) {
  testee.setBypass(true);
  ASSERT_TRUE(testee.isTransitioning());
  // ...
  EXPECT_FALSE(testee.isTransitioning());
}

Проверки EXPECT_EQ, EXPECT_TRUE, EXPECT_NEAR, EXPECT_FLOAT_EQ при неудаче помечают тест проваленным, но он продолжает выполняться. ASSERT_* при неудаче прерывает тест сразу, и его берут для предусловий, без которых дальнейшие проверки бессмысленны. Поля фикстуры объявлены protected, потому что каждый TEST_F технически является наследником фикстуры. Для сравнения вещественных чисел используют EXPECT_NEAR и EXPECT_FLOAT_EQ, а не ==, о чём предупреждает флаг -Wfloat-equal.

Стандартная библиотека и лямбды

Таблица 1 перечисляет стандартные заголовки, подключённые в tremolo_plugin.h.

Таблица 1. Стандартные заголовки, подключённые в tremolo_plugin.h, и то, что из них используется в плагине.
ЗаголовокЧто даёт
<vector>std::vector<T> — динамический массив, как list в Python и срез в Go. Поле lfoSamples в Tremolo.
<array>std::array<T, N> — массив фиксированного размера, заданного при компиляции. Поле lfos.
<memory>std::unique_ptr и std::shared_ptr; std::make_unique при создании параметров.
<functional>std::function<Сигнатура> — хранит любую вызываемую сущность, включая лямбду.
<algorithm>std::min, std::max, std::rotate и другие алгоритмы; std::rotate в StridedQueue.
<cmath>std::sin, std::abs, std::floor; triangle в Tremolo.
<ranges>std::views::iota и другие ленивые адаптеры диапазонов.
<span>std::span — вид на непрерывную память.

Лямбды

Лямбда-выражение (lambda expression, C++11) — анонимная функция вида [захват](параметры) { тело }. В квадратных скобках перечислено, что она берёт из окружения: [this] — указатель на текущий объект, [x] — копию x, [&x] — ссылку на x, [=] и [&] — всё по значению или по ссылке. В отличие от замыканий Python и Go, захват по ссылке не продлевает жизнь объекта: если лямбда переживёт захваченную переменную, она обратится к уже освобождённой памяти.

В плагине лямбды связывают редактор с процессором и реагируют на события интерфейса:

C++
bypassButton.onClick = [this]() {
  bypassButton.setButtonText(bypassButton.getToggleState() ? "Bypassed" : "Off");
};

lfoVisualizer{
    [&p](juce::AudioBuffer<float>& b) { p.readAllLfoSamples(b); },
    [&p] { return p.getSampleRateThreadSafe(); },
    [&p] { return p.getParameterRefs().bypassed.get(); }},

Захват [&p] безопасен, потому что редактор живёт не дольше процессора: по правилам JUCE редактор удаляется раньше процессора. Лямбда [](auto phase) { ... } в Tremolo.h принимает параметр типа auto (C++14), то есть работает как шаблон: тип выводится при вызове.

Что произойдёт, если в PluginEditor написать void resize() вместо void resized() и не поставить override?

Компилятор примет resize() как новый метод. JUCE вызывает виртуальный resized() базового класса Component, он останется пустым, и компоненты редактора не получат размеры. Ошибки при сборке не будет. С override опечатка остановит компиляцию: у базы нет метода resize(), который можно переопределить.

Почему в конструкторе BypassTransitionSmoother параметр назван crossfadeLengthSecondsValue, а не crossfadeLengthSeconds?

Параметр с тем же именем, что и поле, скрыл бы его в теле конструктора, и -Wshadow предупредил бы об этом. Разные имена позволяют использовать поле в jassert(0.0 < crossfadeLengthSeconds) и в теле, не путая его с параметром.

Почему вместо parameters.rate в processBlock иногда пишут parameters.bypassed.get()?

parameters.bypassed — ссылка на объект AudioParameterBool, а не bool. Там, где нужен bool, компилятор сам вызывает operator bool(). Явный .get() делает то же самое и показывает, что читается значение параметра, а не передаётся сам объект; это нужно, например, в выражении parameters.bypassed.get() && !bypassTransitionSmoother.isTransitioning().

Что делает JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PluginEditor)?

Удаляет конструктор копирования и оператор присваивания PluginEditor, поэтому копировать редактор нельзя: попытка не скомпилируется. Кроме того, добавляет детектор утечек, который при завершении программы сообщает об объектах PluginEditor, не удалённых к этому моменту.

Источники

Связи

Поиск

Ищем по названиям, тексту статей и английским терминам. Например: «частота дискретизации», «Nyquist», «аналоговый сигнал».