Создание аудиоплагинов, часть 14

  • Tutorial
Все посты серии:
Часть 1. Введение и настройка
Часть 2. Изучение кода
Часть 3. VST и AU
Часть 4. Цифровой дисторшн
Часть 5. Пресеты и GUI
Часть 6. Синтез сигналов
Часть 7. Получение MIDI сообщений
Часть 8. Виртуальная клавиатура
Часть 9. Огибающие
Часть 10. Доработка GUI
Часть 11. Фильтр
Часть 12. Низкочастотный осциллятор
Часть 13. Редизайн
Часть 14. Полифония 1
Часть 15. Полифония 2
Часть 16. Антиалиасинг



Приступим к созданию полифонического синтезатора из тех компонентов, которые у нас имеются!

В прошлый раз мы работали над параметрами и пользовательским интерфейсом, сегодня мы начнем работу над лежащей в основе плагина полифонической обработкой аудио. В нашем случае мы сможем играть до 64-х нот одновременно. Это требует основательных изменений в структуре плагина, но мы сможем использовать уже написанные нами классы Oscillator, EnvelopeGenerator, MIDIReceiver и Filter.

В этом посте мы напишем класс Voice (голос), представляющий одну звучащую ноту. Затем создадим класс VoiceManager, следящий за тем, чтобы все ноты звучали и заглушались вовремя.
В следующем посте мы почистим код от ненужных устаревших частей, добавим модуляцию тона и приведем элементы управления интерфейса в рабочее состояние. На первый взгляд, работы полно. Но во-первых, у нас уже есть практически все нужные компоненты, а во-вторых, в конце у нас будет настоящий-полифонический-субстрактивный-блин-синтезатор!



Что куда?



Задумаемся на минутку над тем, какие части архитектуры плагина глобальны, а какие существуют обособленно для каждой отдельной ноты. Представьте, вот вы играете несколько нот на клавишах. При каждом нажатии на клавишу появляется тон, который затухает и, возможно, тембр которого меняется фильтром по некоторой огибающей. Когда нажимаете вторую клавишу, первая все еще звучит, и появляется второй тон со своими огибающими амплитуды и фильтра. Второе нажатие никак не влияет на первый тон, он звучит и меняется сам по себе. Так что каждый голос независим и имеет свои огибающие амплитуды и фильтра.
LFO является глобальным и единственным, он просто работает и не перезапускается при нажатии на клавиши.
Что касается фильтра, понятно, что частота среза и резонанс глобальны, потому что все голоса смотрят на одни и те же ручки среза и резонанса в GUI. Но частота среза фильтра модулируется огибающей, так что в каждый момент времени вычисленная частота среза для каждого голоса разная. Взгляните на Filter::cutoff — в ней вызывается getCalculatedCutoff. Так что для каждого голоса нужен свой фильтр.
Можем ли мы обойтись двумя осцилляторами для всех голосов? Каждый Voice играет свою ноту, т.е. у него своя частота, а значит, и свой независимый Oscillator.

Вкратце, структура такая:

  • В плагине есть один MIDIReceiver и один VoiceManager
  • У VoiceManager есть один LFO и много голосов Voice
  • У Voice есть два Oscillator, два геренатора огибающих EnvelopeGenerators (для амплитуды и фильтра) и один Filter


Класс Voice



Как обычно, создайте новый класс, назовите его Voice. И, как обычно, не забудьте добавить его во все таргеты XCode и все проекты VS. В Voice.h добавьте:

#include "Oscillator.h"
#include "EnvelopeGenerator.h"
#include "Filter.h"


В теле класса начнем с секции private:

private:
    Oscillator mOscillatorOne;
    Oscillator mOscillatorTwo;
    EnvelopeGenerator mVolumeEnvelope;
    EnvelopeGenerator mFilterEnvelope;
    Filter mFilter;


Здесь ничего нового: у каждого голоса два осциллятора, фильтр и две огибающих.
Каждый голос запускается с определенной MIDI ноты и громкости. Допишите туда же:

    int mNoteNumber;
    int mVelocity;


Каждая из следующих переменных задает величину модуляции параметров:

    double mFilterEnvelopeAmount;
    double mOscillatorMix;
    double mFilterLFOAmount;
    double mOscillatorOnePitchAmount;
    double mOscillatorTwoPitchAmount;
    double mLFOValue;


Все они, кроме mLFOValue, связаны со значениями ручек интерфейса. На самом деле эти величины одинаковы для всех голосов, но мы не будем делать их глобальными и закидывать в класс плагина. Каждому голосу нужен доступ к этим параметрам каждый семпл, а класс Voice даже не знает о существовании класса плагина (отсутствует #include "SpaceBass.h"). Настроить такой доступ было бы трудоемкой задачей.
И есть еще один параметр. Вы помните, мы добавили флаг isMuted в класс Oscillator? Переместим его в Voice, чтобы когда голос молчит, не вычислялись значения осциллятора, огибающих и фильтра:

    bool isActive;


Теперь перед private добавим public. Начнем с конструктора:

public:
    Voice()
    : mNoteNumber(-1),
    mVelocity(0),
    mFilterEnvelopeAmount(0.0),
    mFilterLFOAmount(0.0),
    mOscillatorOnePitchAmount(0.0),
    mOscillatorTwoPitchAmount(0.0),
    mOscillatorMix(0.5),
    mLFOValue(0.0),
    isActive(false) {
        // Set myself free everytime my volume envelope has fully faded out of RELEASE stage:
        mVolumeEnvelope.finishedEnvelopeCycle.Connect(this, &Voice::setFree);
    };


Эти строки инициализируют переменные с разумными значениями. По умолчанию Voice не активен. Также, используя сигналы и слоты EnvelopeGenerator, мы «освобождаем» голос как только огибающая амплитуды выходит из стадии release.
Добавим сеттеры в public:

 inline void setFilterEnvelopeAmount(double amount) { mFilterEnvelopeAmount = amount; }
    inline void setFilterLFOAmount(double amount) { mFilterLFOAmount = amount; }
    inline void setOscillatorOnePitchAmount(double amount) { mOscillatorOnePitchAmount = amount; }
    inline void setOscillatorTwoPitchAmount(double amount) { mOscillatorTwoPitchAmount = amount; }
    inline void setOscillatorMix(double mix) { mOscillatorMix = mix; }
    inline void setLFOValue(double value) { mLFOValue = value; }

    inline void setNoteNumber(int noteNumber) {
        mNoteNumber = noteNumber;
        double frequency = 440.0 * pow(2.0, (mNoteNumber - 69.0) / 12.0);
        mOscillatorOne.setFrequency(frequency);
        mOscillatorTwo.setFrequency(frequency);
    }


Единственный интересный момент здесь — это setNoteNumber. Она вычисляет частоту для данной ноты по уже известной нам формуле и передает ее обоим осцилляторам. После нее добавьте:

    double nextSample();
    void setFree();


Как Oscillator::nextSample дает нам выход Oscillator, так и Voice::nextSample выдает результирующее значение голоса после огибающей амплитуды и фильтра. Напишем имплементацию в Voice.cpp:

double Voice::nextSample() {
    if (!isActive) return 0.0;

    double oscillatorOneOutput = mOscillatorOne.nextSample();
    double oscillatorTwoOutput = mOscillatorTwo.nextSample();
    double oscillatorSum = ((1 - mOscillatorMix) * oscillatorOneOutput) + (mOscillatorMix * oscillatorTwoOutput);

    double volumeEnvelopeValue = mVolumeEnvelope.nextSample();
    double filterEnvelopeValue = mFilterEnvelope.nextSample();

    mFilter.setCutoffMod(filterEnvelopeValue * mFilterEnvelopeAmount + mLFOValue * mFilterLFOAmount);

    return mFilter.process(oscillatorSum * volumeEnvelopeValue * mVelocity / 127.0);
}


Первая сточка гарантирует, что когда голос неактивен ничего не вычисляется и возвращается ноль. Следующие три строки вычисляют nextSample для обоих осцилляторов и смешивают их в соответствии с mOscillatorMix. Когда mOscillatorMix равен нулю, слышен только oscillatorOneOutput. При 0.5 оба осциллятора имеют равную амплитуду.
Затем вычисляется следующий семпл обеих огибающих. Мы применяем filterEnvelopeValue к частоте среза фильтра и берем в расчет значение LFO. Общая модуляция среза это сумма огибающей фильтра и LFO.
Модуляция тона обоих осцилляторов это просто выход LFO помноженный на величину модуляции. Мы напишем это через минутку.
Интересна последняя строка. Сначала содержание скобок: берем сумму двух осцилляторов, применяем огибающую громкости и значение громкости ноты. Затем пропускаем результат через mFilter.process, в результате получаем отфильтрованный выход, который и возвращаем.

Имплементация setFree предельно простая:

void Voice::setFree() {
    isActive = false;
}


Как уже говорилось, вызов этой функции осуществляется каждый раз, когда mVolumeEnvelope полностью затухает.

VoiceManager



Пора написать класс для управления голосами. Создайте класс с именем VoiceManager. В хедере начните с этих строк:

#include "Voice.h"

class VoiceManager {
};


И продолжите private членами класса:

static const int NumberOfVoices = 64;
Voice voices[NumberOfVoices];
Oscillator mLFO;
Voice* findFreeVoice();


Константа NumberOfVoices обозначает максимальное количество одновременно звучащих голосов. В следующей строке создается массив из голосов. Эта структура использует место под 64 голоса, так что лучше задуматься о динамическом распределении памяти. Впрочем, класс плагина и так распределен динамически (поищите "new PLUG_CLASS_NAME" в Iplug_include_in_plug_src.h), так что все члены класса плагина тоже в куче.

mLFO — это глобальный LFO для плагина. Он никогда не перезапускается, просто независимо осциллирует. Можно поспорить, что он должен быть внутри класса плагина (VoiceManager не нужно знать об LFO). Но это внесет еще один слой разграничения между голосами Voice и LFO, а значит, нам понадобится больше склеивающего кода.
findFreeVoice это вспомогательная функция для поиска голосов, которые не звучат в данный момент. Добавьте ее имплементацию в VoiceManager.cpp:

Voice* VoiceManager::findFreeVoice() {
    Voice* freeVoice = NULL;
    for (int i = 0; i < NumberOfVoices; i++) {
        if (!voices[i].isActive) {
            freeVoice = &(voices[i]);
            break;
        }
    }
    return freeVoice;
}


Она просто итерирует над всеми голосами и находит первый молчащий. Мы возвращаем указатель (вместо ссылки &), потому что в таком случае, в отличие от ссылки, можно вернуть NULL. Это будет означать, что все голоса звучат.

Теперь добавим в public такие заголовки функций:

void onNoteOn(int noteNumber, int velocity);
void onNoteOff(int noteNumber, int velocity);
double nextSample();


Как понятно из имени, onNoteOn вызывается при получении MIDI сообщения Note On. onNoteOff, Соответственно, вызывается при Note Off сообщении. Напишем код этих функций в .cpp файле класса:

void VoiceManager::onNoteOn(int noteNumber, int velocity) {
    Voice* voice = findFreeVoice();
    if (!voice) {
        return;
    }
    voice->reset();
    voice->setNoteNumber(noteNumber);
    voice->mVelocity = velocity;
    voice->isActive = true;
    voice->mVolumeEnvelope.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_ATTACK);
    voice->mFilterEnvelope.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_ATTACK);
}


Сначала находим свободный голос при помощи findFreeVoice. Если ничего не нашлось, мы ничего не возвращаем. Это значит, что когда все голоса звучат, нажатие еще одной клавиши не будет иметь никакого результата. Реализация подхода voice stealing будет одной из тем следующего поста. Если находится свободный голос, нам нужно его обновить до начального состояния (reset, мы сделаем это очень скоро). После этого мы задаем правильные значения setNoteNumber и mVelocity. Помечаем голос как активный и переводим обе огибающие в стадию attack.
Если запустить сборку прямо сейчас, выскочит ошибка о том, что мы пытаемся получить доступ к private членам Voice извне. На мой взгляд, лучшим решением в этой ситуации будет использовать ключевое слово friend. Добавьте соответствующую строчку перед public в Voice.h:

friend class VoiceManager;


Благодаря этой строчке Voice дает VoiceManager доступ к своим private членам. Я не сторонник обширного использования этого подхода, но если у вас есть класс Foo и класс FooManager, это хороший способ избежать написания множества сеттеров.

onNoteOff выглядит так:

void VoiceManager::onNoteOff(int noteNumber, int velocity) {
    // Find the voice(s) with the given noteNumber:
    for (int i = 0; i < NumberOfVoices; i++) {
        Voice& voice = voices[i];
        if (voice.isActive && voice.mNoteNumber == noteNumber) {
            voice.mVolumeEnvelope.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_RELEASE);
            voice.mFilterEnvelope.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_RELEASE);
        }
    }
}


Мы находим все голоса с номером отпущенной ноты и переводим их огибающие в стадию release. Почему голоса, а не голос? Представьте себе, что у вас очень длительная стадия затухания в огибающей амплитуды. Вы нажимаете на клавишу и отпускаете ее, и, пока хвост ноты все еще звучит, быстро жмете эту клавишу еще раз. Естественно, вы не хотите обрубить предыдущую звучащую ноту. Это было бы очень некрасиво. Нужно и чтобы дозвучала предыдущая нота, и чтобы новая начала параллельно звучать. Таким образом, вам понадобится больше одного голоса на ноту. Если долбить по клавишам очень быстро, то понадобится много голосов.
Так что происходит, если, например, у нас пять активных голосов для До третьей октавы и мы отпускаем эту клавишу? Вызывается onNoteOff и переводит огибающие всех пяти голосов в стадию release. Четыре из них и так уже в этой стадии, так что давайте посмотрим на первую строку EnvelopeGenerator::enterStage:

if (currentStage == newStage) return;


Как видите, для этих четырех нот ничего не произойдет, никаких загвоздок тут не будет.

Давайте теперь напишем функцию-член nextSample для VoiceManager. Она должна выводить суммарное значение для всех активных голосов:

double VoiceManager::nextSample() {
    double output = 0.0;
    double lfoValue = mLFO.nextSample();
    for (int i = 0; i < NumberOfVoices; i++) {
        Voice& voice = voices[i];
        voice.setLFOValue(lfoValue);
        output += voice.nextSample();
    }
    return output;
}


Мы начинаем с тишины (0.0), итерируем над всеми голосами, устанавливаем текущее значение LFO и добавляем выход голоса к суммарному выходу. Как мы помним, если голос неактивен, его функция Voice::nextSample не будет ничего вычислять и сразу завершится.

Многоразовые компоненты



До текущего момента мы создавали объекты Oscillator и Filter и использовали их на протяжении всего времени работы плагина. Но VoiceManager повторно использует свободные голоса, так что надо придумать, как полностью перевести голос в начальное состояние. Начнем с добавления функции в public хедера Voice:

void reset();


Тело функции напишем в .cpp:

void Voice::reset() {
    mNoteNumber = -1;
    mVelocity = 0;
    mOscillatorOne.reset();
    mOscillatorTwo.reset();
    mVolumeEnvelope.reset();
    mFilterEnvelope.reset();
    mFilter.reset();
}


Как видно, здесь сбрасывются mNoteNumber и mVelocity, затем сбрасываются осцилляторы, огибающие и фильтр. Давайте это напишем!

В public секции Oscillator.h добавьте:

void reset() { mPhase = 0.0; }


Это позволяет запускать форму волны сначала каждый раз, когда начинает звучать голос.

Заодно, пока мы там, удалите флаг isMuted из секции private. Не забудьте удалить его также из списка инициализации конструктора и удалить функцию-член setMuted. Мы теперь отслеживаем состояние активности на уровне Voice, так что осциллятору это все больше не нужно. Удалите эту строчку из функции Oscillator::nextSample:

// remove this line:
if(isMuted) return value;


Функция reset в EnvelopeGenerator немного длиннее. В секции public хедера EnvelopeGenerator напишите следующее:

void reset() {
    currentStage = ENVELOPE_STAGE_OFF;
    currentLevel = minimumLevel;
    multiplier = 1.0;
    currentSampleIndex = 0;
    nextStageSampleIndex = 0;
}


Тут просто нужно сбросить больше значений, все линейно. Осталось добавить reset для класса Filter (тоже в public):

void reset() {
    buf0 = buf1 = buf2 = buf3 = 0.0;
}


Как вы наверное помните, эти буферы содержат в себе предыдущие выходные семплы фильтра. Когда мы повторно используем голос, эти буферы должны быть пустыми.

Подводя итог: каждый раз, когда VoiceManager использует Voice, он вызывает функцию reset для сброса голоса до начального состояния. Эта функция, в свою очередь, сбрасывает осцилляторы голоса, его генераторы огибающих и фильтр.

static или не static?



Переменные-члены для всех голосов одни и те же:

  • Oscillator: mOscillatorMode
  • Filter: cutoff, resonance, mode
  • EnvelopeGenerator: stageValue


Сначала я думал, что подобная избыточность это зло, и все эти штуки должны быть статическими членами. Давайте представим, что mOscillatorMode — статический. Тогда у LFO была бы та же форма волны, что и у остальных осцилляторов, а этого мы не хотим. Далее, если бы значения stageValue генератора огибающих EnvelopeGenerator были статическими, огибающие амплитуды и фильтра были бы одинаковыми.

Это можно было бы исправить путем наследования: создав классы VolumeEnvelope и FilterEnvelope, которые наследовали бы от класса EnvelopeGenerator. Параметр stageValue мог бы быть статическим и VolumeEnvelope и FilterEnvelope могли бы его менять. Это четко разделило бы огибающие и все голоса могли бы иметь доступ к статическим членам. Но в данном случае речь не идет о больших объемах памяти. Все, что приходится делать при той структуре, которую создали мы, это синхронизировать эти переменные между огибающими амплитуд и фильтров всех голосов.

Однако одна вещь может быть статической: sampleRate. Нет смысла в том, чтобы компоненты синтезатора работали на разных частотах дискретизации. Давайте подправим это в Oscillator.h:

static double mSampleRate;


А значит, мы не должны инициализировать эту переменную через список инициализации. Удалите mSampleRate(44100.0). В Oscillator.cpp после #include добавьте:

double Oscillator::mSampleRate = 44100.0;


Частота дискретизации теперь статическая и все осцилляторы используют одно ее значение.
Давайте сделаем то же самое для EnvelopeGenerator. Сделайте sampleRate статическим, удалите из списка инициализации конструктора и допишите в EnvelopeGenerator.cpp:

double EnvelopeGenerator::sampleRate = 44100.0;


В EnvelopeGenerator.h сделайте статическим сеттер:

static void setSampleRate(double newSampleRate);


Мы добавили много нового! В следующий раз мы почистим лишнее и приведем GUI в рабочее состояние.

Код можно скачать отсюда.
Оригинал поста.
Поделиться публикацией

Похожие публикации

Комментарии 17

    +2
    Больше всего поражает ваша упорность в написании статей, за которые отдельное спасибо :)
      0
      Пожалуйста :) Правда, я их не пишу, а перевожу, от себя добавляю мало.
        +1
        Пожалуйста, не переставайте! Не смотря на отсутствие активности в комментариях, статьи очень полезны и, уверен, множество благодарных читателей приходят с гугла :)
          0
          Осталось совсем немного до конца цикла :)
          Да, я надеюсь, приходят. Несколько человек писали мне в соцсетях, что эти статьи им полезны. DSP, даже ограниченная обработкой звука, это очень объемная тема, в ней масса суперинтересных и непростых задач. Было бы здорово иметь даже небольшое сообщество для обсуждения. С другой стороны, конкуренция в этой области достаточно высока, и таких сообществ в интернете вообще не так много.
      +1
      Отличный цикл статей, продолжайте дальше. Технические статьи всегда собирают довольно мало комментариев, но они оказываются в дальнейшем самыми полезными!
      0
      Как базис — неплохо, все рассказано и показано очень доступно, но за доступностью скрывается много проблем. Начиная с производительности (в основном осцилляторов) и заканчивая отсутствием возможности sample accurate изменения параметров (cutoff, res, etc). А это must have для современного синта.
        0
        Буду рад, если вы дадите ссылки на более производительные алгоритмы, я этим тоже озабочен. Следующий пост будет, к слову, об осцилляторах.

        Что касается sample precision — тут с вами поспорю.
        Да, есть направления, где это необходимо (что-нибудь типа работ Ryoji Ikeda или Alva Noto), но масса прекрасных вещей делается и без sample precision — NIN, GusGus, Moderat, Depeche Mode, Jon Hopkins и т.д.
        В конце концов, теплый ламповый звук — это полная противоположность точности.
        Пардон за нэймдроппинг :)
          +1
          Я смотрел следующую статью в оригинале, там речь о bandlimited осциляторах, что является полезным знанием, но приведенный осциллятор с большой натяжкой можно назвать bandlimited.

          Ссылок дать не могу. В основном используются заранее сгенерированные таблицы одного периода волны, выборка из которых может быть или прямой, или интерполироваться. Чтобы избежать алиасинга, я использую N таблиц каждая последующая в 2 раза длиннее предыдущей, а таблица выбирается в зависимости от шага (pitch). Если у осциллятора постоянный шаг, то для большей точности можно использовать fixed point для фазы и шага.

          Если в композиции есть плавно нарастающая или убывающая cutoff frequency, то без sample accurate param changes не обойтись, иначе при слишком большом размере выходного буффера будет скачкообразное изменение параметра, что недопустимо. Как-то так.

          И да, спасибо за проделанную работу. :)
            0
            Я, честно говоря, не совсем понял про фильтр. Опишите пожалуйста пример подробнее :)
              +1
              Фильтр тут в качестве примера. Тут может быть любой изменяемый параметр (ручка).

              Начну тогда из далека. Аудиоплагин используется не сам по себе, а в какой-нибудь DAW, которая посылает ему различные комманды, аудио на вход и ожидает на выходе получить какой-то результат. Это может происходить в реальном времени, во время воспроизведения трэка или при финальном рендеринге трэка.

              Допустим в трэке есть секция возрастания какого-то параметра (обычно это называется automation),
              и при воспроизведении или рендеренге трэка DAW должна каким-то образом посылать это изменение в плагин.

              В текущей реализации параметр изменяется между вызовами ProcessDoubleReplacing, а внутри ProcessDoubleReplacing он имеет одно и то же значение для всех сэмплов. В ProcessDoubleReplacing обрабатывается nFrames сэмплов за раз.

              То есть вместо плавного возрастания получается лесенка, где длинна каждой ступени nFrames сэмплов.
              Чем больше длинна ступеньки, тем больше высота этой ступеньки. При достаточно больших значениях nFrames, получается не плавное, а скачкообразное возрастание параметра.

              Если параметр начинает возрастать где-то по середине буфера переданного в ProcessDoubleReplacing, то изменения начнутся лишь в начале следующего ProcessDoubleReplacing. А если параметр будет колебаться с периодом меньше, чем за nFrames сэмплов (например automation, записанный с помощью mod wheel), никакой модуляции мы не услышим.

              В качестве другого примера можно выставить большой размер выходного буфера и покрутить ручки синта. Будет тот самый эффект.

              Почему nFrames может иметь большое значение? Потому что чем больше nFrames, тем меньше тратиться процессорных тактов. И для рендеринга часто ставят значение побольше. Хотя и для воспроизведения может быть выставлено большое значение, если при малом значение слышны glitch'и.

              Например, в VST начиная кажется с 3-ей версии добавили специальные комманды для sample accurate изменения параметров (IParameterChanges и IParamValueQueue).
              Обрабатывать их это то еще веселье, но это обязательно для современного синта.

              Существуют и другие проблемы, но эта самая очевидная.

              Надеюсь понятно объяснил.
                0
                Объяснили здорово. И вернули меня к одному старому вопросу: кто решает, какой именно длины будет nFrames. Возможно, ответ скрыт где-то в комментах к коду, но я его пока не нашел.
                  +1
                  Решает DAW и обычно размер буфера находиться в настройках. Чтобы плагин был готов, на этапе конфигурации передается это значение. Кстати, оно не обязано быть постоянным. Главное, что не будет превышать значения переданного при конфигурации.
                  0
                  Надо покопаться и проверить, но я предполагаю, что в WDL-OL должны быть способы менять IParameterChanges и IParamValueQueue.
                    0
                    В WDL-OL есть обработка этих интерфейсов, но примитивнейшая. Берется последнее значение параметра и устанавливается для всех сэмплов. В обертке для AU практически то же самое.
                      0
                      Получается, «лесенку» никак не обойти?
                        0
                        Стандартными средствами никак. Но всегда можно форкнуть WDL и переписать какую-либо часть. :)

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое